33 lines
7.2 KiB
PHP
33 lines
7.2 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
if(($argv[1]??'')==='--crash-child') {require dirname(__DIR__).'/bootstrap.php';$p=json_decode(file_get_contents($argv[2]),true,64,JSON_THROW_ON_ERROR);PrelandAdmin\Remote::execute($p);exit;}
|
|
require __DIR__.'/backend-security.php';
|
|
use PrelandAdmin\{Config,Sets,Conflict};
|
|
require dirname(__DIR__,2).'/components/utm-codec.php';
|
|
$resolverConfig=['utm'=>['indpic'=>['default'=>'2','weights'=>['1'=>0,'2'=>0]]]];
|
|
check(prelandResolveUtm(['indpic'=>'abcd2'],$resolverConfig)['digits']['indpic']==='2','configured zero-weight indpic remains selectable explicitly by actual resolver');
|
|
check(prelandResolveUtm(['indpic'=>'abcd0'],$resolverConfig)['digits']['indpic']==='1','actual generic resolver zero-sum selects first configured variant');
|
|
$legacy=Config::decode('{"host":"zoomzilla.ru","utm":{"registration":{"default":"before","weights":{"before":100,"after":0},"extra":true}}}');
|
|
$changed=Config::patch($legacy,[['path'=>['utm','reg','default'],'value'=>'2']]);check($changed->utm->registration->default==='after'&&!isset($changed->utm->reg)&&$changed->utm->registration->extra===true,'canonical default adapts to legacy target without duplicate alias');
|
|
$changed=Config::patch($legacy,[['path'=>['utm','reg','weights'],'value'=>(object)['1'=>0,'2'=>2]]]);check($changed->utm->registration->weights->after===2,'numeric weight group adapts to legacy representation');
|
|
$modern=Config::decode('{"host":"zoomzilla.ru","utm":{"reg":{"default":"1","weights":{"1":100,"2":0}}}}');$changed=Config::patch($modern,[['path'=>['utm','registration','default'],'value'=>'after']]);check($changed->utm->reg->default==='2'&&!isset($changed->utm->registration),'legacy default adapts to modern target');
|
|
$changed=Config::patch($modern,[['path'=>['utm','registration','weights','before'],'value'=>5]]);check($changed->utm->reg->weights->{'1'}===5,'legacy weight path adapts to numeric target');
|
|
rejects(fn()=>Config::patch(Config::decode('{"utm":{"reg":{},"registration":{}}}'),[['path'=>['utm','reg','default'],'value'=>'2']]),'pre-existing two aliases rejected before adapter');
|
|
check(Config::validate(Config::decode('{"utm":{"indpic":{"default":"1","weights":{"1":100,"9":0}}}}'))===[],'zero-weight indpic can reference manually entered variant');
|
|
check(Config::validate(Config::decode('{"utm":{"theme":{"default":"1","weights":{}}}}'))===[],'enum default does not invent weights-membership restriction');
|
|
$store->write('catalog.json',['host'=>'zoomzilla.ru','generated_at'=>gmdate(DATE_ATOM),'groups'=>['degr'=>[],'tp'=>[],'cons'=>[]]]);
|
|
$manual=Config::decode('{"host":"zoomzilla.ru","utm":{"degr":{"default":"9","weights":{"9":100}}}}');check(Config::validate($manual)===[],'manual groups require no catalog lookup or availability warnings');
|
|
$validPrepared=$sets->prepareZip($zipPath,1);$source=$sets->manifest($target,1);$desired=Sets::preparedManifest($validPrepared,1);
|
|
$original=file_get_contents($root.'/site/1/01_desk_1-source.jpg');$img=new Imagick();$img->newImage(1696,920,'#aabbcc');$img->setImageFormat('jpg');file_put_contents($root.'/site/1/01_desk_1-source.jpg',$img->getImagesBlob());$img->clear();$foreign=$sets->manifest($target,1);
|
|
rejects(fn()=>$sets->upload($target,1,$validPrepared,'CAS',$source,true),'set CAS rejects third-party manifest changed after read');check($sets->manifest($target,1)===$foreign,'set CAS preserves third-party images');
|
|
$unknown=$jobs->create('upload',['targets'=>['test'],'number'=>1,'prepared'=>$validPrepared,'name'=>'Retry']);$r=$store->read('jobs/'.$unknown['id'].'.json');$r['status']='completed';$r['results'][0]=['target'=>'test','status'=>'unknown','message'=>'fixture','source_manifest'=>$source,'desired_manifest'=>$desired];$store->write('jobs/'.$unknown['id'].'.json',$r);$jobs->retry($unknown['id'],['test']);$jobs->run($unknown['id']);check($jobs->get($unknown['id'])['results'][0]['status']==='conflict'&&$sets->manifest($target,1)===$foreign,'unknown upload retry detects third-party publication and never overwrites it');
|
|
$sets->upload($target,1,$validPrepared,'CAS',$foreign,true);$unknown2=$jobs->create('upload',['targets'=>['test'],'number'=>1,'prepared'=>$validPrepared,'name'=>'Retry']);$r=$store->read('jobs/'.$unknown2['id'].'.json');$r['status']='completed';$r['results'][0]=['target'=>'test','status'=>'unknown','message'=>'fixture','source_manifest'=>$foreign,'desired_manifest'=>$desired];$store->write('jobs/'.$unknown2['id'].'.json',$r);$before=count(glob($store->dir.'/local-*/backup-1-*')?:[]);$jobs->retry($unknown2['id'],['test']);$jobs->run($unknown2['id']);check($jobs->get($unknown2['id'])['results'][0]['status']==='success'&&count(glob($store->dir.'/local-*/backup-1-*')?:[])===$before,'already-published desired upload reconciles without another swap or backup');
|
|
// SIGKILL в реальном libc rename, после переноса old→backup, без production test hooks.
|
|
$library=$root.'/kill-rename.so';$compile=proc_open(['gcc','-shared','-fPIC','-o',$library,__DIR__.'/backend-kill-rename.c','-ldl'],[0=>['pipe','r'],1=>STDOUT,2=>STDERR],$pipes);fclose($pipes[0]);check(proc_close($compile)===0,'fault injector builds inside Docker');
|
|
$files=[];foreach(array_keys(PrelandAdmin\IndpicSetStorage::expectedDimensions()) as $name)$files[$name]=['data'=>base64_encode(file_get_contents($validPrepared.'/1/'.$name)),'sha256'=>hash_file('sha256',$validPrepared.'/1/'.$name)];$private=$store->dir.'/local-'.hash('sha256',$target['root']);$request=['op'=>'set-write','root'=>$target['root'],'local_private'=>$private,'number'=>1,'files'=>$files,'expected_manifest'=>$sets->manifest($target,1)];$input=$root.'/swap.json';file_put_contents($input,json_encode($request,JSON_THROW_ON_ERROR));$env=getenv();$env['LD_PRELOAD']=$library;$child=proc_open([PHP_BINARY,__FILE__,'--crash-child',$input],[0=>['pipe','r'],1=>STDOUT,2=>STDERR],$pipes,null,$env);fclose($pipes[0]);$exit=proc_close($child);check($exit!==0&&!is_dir($target['root'].'/1')&&is_file($private.'/swap-1.json'),'SIGKILL after first rename leaves durable recoverable journal');
|
|
$crashed=$jobs->create('upload',['targets'=>['test'],'number'=>1,'prepared'=>$validPrepared,'name'=>'Crash recovery']);$record=$store->read('jobs/'.$crashed['id'].'.json');$record['status']='running';$record['results'][0]['status']='running';$record['results'][0]['desired_manifest']=$desired;$store->write('jobs/'.$crashed['id'].'.json',$record);
|
|
$jobs->interrupted($crashed['id']);clearstatcache();check(is_dir($target['root'].'/1')&&!is_file($private.'/swap-1.json'),'supervisor interruption automatically restores active folder after SIGKILL without user read');
|
|
check($jobs->get($crashed['id'])['results'][0]['status']==='unknown'&&$sets->manifest($target,1)===$desired,'automatic journal recovery preserves complete set and unknown operation status');
|
|
$unavailable=$jobs->create('upload',['targets'=>['missing'],'number'=>1,'prepared'=>$validPrepared]);$record=$store->read('jobs/'.$unavailable['id'].'.json');$record['status']='running';$record['results'][0]['status']='running';$store->write('jobs/'.$unavailable['id'].'.json',$record);$jobs->interrupted($unavailable['id']);$result=$jobs->get($unavailable['id'])['results'][0];check($result['status']==='unknown'&&($result['recovery_checked']??null)===false,'failed automatic recovery remains explicitly unconfirmed and unknown');
|
|
echo "Review regression complete\n";
|