Files
SGF/scripts/modular-regression-run.mjs
T
2026-11-02 08:06:45 +02:00

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');
}