75 lines
6.3 KiB
JavaScript
75 lines
6.3 KiB
JavaScript
// Disposable Windows regression host. Never connects to the installed databases.
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import crypto from 'node:crypto';
|
|
import {spawnSync} from 'node:child_process';
|
|
|
|
const source=process.cwd(), runtime='C:/laragon/data/sgf-tempo/run', pg='C:/Program Files/PostgreSQL/17/bin';
|
|
const pointer=path.join(runtime,'modular-regression-current.json');
|
|
const action=process.argv[2]||'prepare';
|
|
function run(exe,args,options={}) {
|
|
const r=spawnSync(exe,args,{encoding:'utf8',windowsHide:true,maxBuffer:24*1024*1024,...options});
|
|
if(r.status!==0)throw Error(`${path.basename(exe)} failed (${r.status}): ${(r.stdout||'')+(r.stderr||'')}`);
|
|
return r.stdout;
|
|
}
|
|
function files(dir){return fs.readdirSync(dir,{withFileTypes:true}).flatMap(e=>e.isDirectory()?files(path.join(dir,e.name)):[path.join(dir,e.name)]);}
|
|
function sync(state){
|
|
for(const dir of ['app','bootstrap','config','database','resources','routes','tests','packages','scripts','public']) {
|
|
fs.cpSync(path.join(source,dir),path.join(state.app,dir),{recursive:true,filter:p=>!p.includes('bootstrap'+path.sep+'cache'+path.sep)&&!p.endsWith('.update.lock')});
|
|
}
|
|
for(const file of ['artisan','composer.json','composer.lock','phpunit.xml','package.json'])fs.copyFileSync(path.join(source,file),path.join(state.app,file));
|
|
// The sole staging adaptation to application logic is its isolated loopback
|
|
// port. The source guard stays fixed and unchanged at 55432.
|
|
const rewrites=[];
|
|
for(const dir of ['app','database','tests','scripts'])for(const file of files(path.join(state.app,dir)).filter(p=>/\.(php|sql|ps1|mjs)$/.test(p))){
|
|
const original=fs.readFileSync(file,'utf8'),adapted=original.replaceAll('55432',String(state.port));
|
|
if(adapted!==original){fs.writeFileSync(file,adapted);rewrites.push(path.relative(state.app,file));}
|
|
}
|
|
fs.writeFileSync(path.join(state.root,'port-adaptations.json'),JSON.stringify(rewrites,null,2));
|
|
}
|
|
if(action==='prepare'){
|
|
const id=crypto.randomBytes(6).toString('hex'),root=path.join(runtime,'modular-regression-'+id);
|
|
const state={id,root,app:path.join(root,'app'),cluster:path.join(root,'postgres'),port:55439,web_port:8039};
|
|
fs.mkdirSync(state.app,{recursive:true});
|
|
fs.mkdirSync(path.join(root,'private'),{recursive:true});
|
|
const admin=crypto.randomBytes(30).toString('hex'),password=crypto.randomBytes(30).toString('hex');
|
|
fs.writeFileSync(path.join(root,'private','admin-password.txt'),admin);
|
|
fs.writeFileSync(path.join(root,'private','runner-password.txt'),password);
|
|
sync(state);
|
|
fs.cpSync(path.join(source,'vendor'),path.join(state.app,'vendor'),{recursive:true});
|
|
for(const dir of ['storage/framework/cache/data','storage/framework/sessions','storage/framework/views','storage/logs','bootstrap/cache'])fs.mkdirSync(path.join(state.app,dir),{recursive:true});
|
|
const common=`APP_NAME=SGF-Modular-QA\nAPP_KEY=base64:${crypto.randomBytes(32).toString('base64')}\nAPP_DEBUG=false\nAPP_URL=http://127.0.0.1:${state.web_port}\nDB_CONNECTION=pgsql\nDB_HOST=127.0.0.1\nDB_PORT=${state.port}\nDB_DATABASE=sgf_tempo_test\nDB_USERNAME=sgf_test_runner\nDB_PASSWORD=${password}\nDB_URL=\nSESSION_DRIVER=array\nCACHE_STORE=array\n`;
|
|
fs.writeFileSync(path.join(state.app,'.env.testing'),common+'APP_ENV=testing\n');
|
|
fs.writeFileSync(path.join(state.app,'.env'),common.replace('DB_DATABASE=sgf_tempo_test','DB_DATABASE=sgf_tempo_dev')+'APP_ENV=local\n');
|
|
run(path.join(pg,'initdb.exe'),['-D',state.cluster,'-U','sgf_regression_owner','-A','scram-sha-256','--pwfile='+path.join(root,'private','admin-password.txt'),'-E','UTF8','--locale=C']);
|
|
fs.appendFileSync(path.join(state.cluster,'postgresql.conf'),`\nlisten_addresses='127.0.0.1'\nport=${state.port}\ncluster_name='sgf_modular_regression_${id}'\n`);
|
|
run(path.join(pg,'pg_ctl.exe'),['-D',state.cluster,'-l',path.join(root,'postgres.log'),'start'],{stdio:'ignore'});
|
|
run(path.join(pg,'psql.exe'),['-h','127.0.0.1','-p',String(state.port),'-U','sgf_regression_owner','-d','postgres','-v','ON_ERROR_STOP=1'],{
|
|
env:{...process.env,PGPASSWORD:admin},input:`CREATE ROLE sgf_test_runner LOGIN PASSWORD '${password}' NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS;
|
|
CREATE DATABASE sgf_tempo_test OWNER sgf_test_runner;
|
|
REVOKE ALL ON DATABASE sgf_tempo_test FROM PUBLIC;
|
|
CREATE DATABASE sgf_tempo_dev;
|
|
REVOKE ALL ON DATABASE sgf_tempo_dev FROM PUBLIC;
|
|
`});
|
|
fs.writeFileSync(pointer,JSON.stringify(state,null,2));
|
|
console.log(JSON.stringify({prepared:true,...state,source_guard_unchanged:true,credentials_printed:false}));
|
|
}else{
|
|
const state=JSON.parse(fs.readFileSync(pointer,'utf8'));
|
|
if(path.dirname(path.resolve(state.root))!==path.resolve(runtime)||!/^modular-regression-[a-f0-9]{12}$/.test(path.basename(state.root))
|
|
||path.resolve(state.app)!==path.resolve(state.root,'app')||path.resolve(state.cluster)!==path.resolve(state.root,'postgres')||state.port!==55439)throw Error('Unexpected runtime root');
|
|
if(action==='sync'){sync(state);console.log('Isolated source refreshed; original databases and guard untouched.');}
|
|
else if(action==='test'||action==='focused'){
|
|
const start=new Date().toISOString();
|
|
const extra=process.argv.slice(3);
|
|
const suffix=action==='focused'?'-focused':'';
|
|
const selection=action==='focused'?['--group','modular-focused']:['--exclude-group','modular-focused'];
|
|
const r=spawnSync('php',['vendor/phpunit/phpunit/phpunit',...selection,...extra,'--log-junit',path.join(state.root,'junit'+suffix+'.xml')],{
|
|
cwd:state.app,encoding:'utf8',windowsHide:true,maxBuffer:24*1024*1024,
|
|
env:{...process.env,SGF_ISOLATED_REGRESSION_RUN:state.id}});
|
|
fs.writeFileSync(path.join(state.root,'regression-output'+suffix+'.txt'),(r.stdout||'')+(r.stderr||''));
|
|
console.log(((r.stdout||'')+(r.stderr||'')).split(/\r?\n/).filter(line=>/^(PHPUnit|Runtime:|Configuration:|Time:|OK |Tests:|There (was|were)|[0-9]+\))/.test(line.replace(/\x1b\[[0-9;]*m/g,''))).join('\n'));
|
|
fs.writeFileSync(path.join(state.root,'result'+suffix+'.json'),JSON.stringify({started_at:start,finished_at:new Date().toISOString(),exit_code:r.status,port:state.port,source_guard_unchanged:true},null,2));
|
|
process.exitCode=r.status||0;
|
|
}else throw Error('Use prepare, sync, test or focused');
|
|
}
|