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