60 lines
6.5 KiB
PHP
60 lines
6.5 KiB
PHP
<?php
|
|
require_once __DIR__ . '/../web/lib/experiment.class.php';
|
|
require_once __DIR__ . '/../web/lib/job.class.php';
|
|
$tmp=sys_get_temp_dir().'/exoplasim-backend-'.bin2hex(random_bytes(5)); mkdir($tmp,0700);
|
|
$config=json_decode(file_get_contents(__DIR__.'/../config/web.json'),true); $config['storage']['root']=$tmp; $config['storage']['catalog']='catalog/parameters.json';
|
|
mkdir($tmp.'/catalog',0700);
|
|
exec(escapeshellarg(dirname(__DIR__).'/.venv/bin/python').' '.escapeshellarg(dirname(__DIR__).'/scripts/web/parameters.py').' --output '.escapeshellarg($tmp.'/catalog/parameters.json').' 2>&1', $output, $exitCode);
|
|
if($exitCode!==0 || !is_file($tmp.'/catalog/parameters.json')) throw new RuntimeException('Could not generate parameter catalog: '.implode("\n",$output));
|
|
$owner=$config['workspace'];
|
|
function check_backend($value,$message){if(!$value)throw new RuntimeException($message);}
|
|
file_put_contents($tmp.'/config.json',json_encode($config)); putenv('EXOPLASIM_CONFIG='.$tmp.'/config.json');
|
|
try {
|
|
$saved=Experiment::save($owner,['name'=>'test','spec'=>['model'=>['resolution'=>'T21','layers'=>10,'ncpus'=>1],'parameters'=>[],'run'=>['cycles'=>1,'steps'=>1440]]]);
|
|
if(empty($saved['ok'])) throw new RuntimeException('Experiment save failed: '.json_encode($saved));
|
|
check_backend(is_object($saved['data']['spec']['parameters']),'Saved experiment API must encode empty parameters as an object.');
|
|
$query=LabDb::connection()->prepare('SELECT spec FROM experiments WHERE id=?');$query->execute([$saved['data']['id']]);$raw=$query->fetchColumn();
|
|
check_backend(is_object(json_decode($raw)->parameters),'Saved experiment JSON must contain an empty object for parameters.');
|
|
$job=Job::enqueue($owner,$saved['data']); check_backend($job['ok'],'Model queue failed.');
|
|
check_backend(is_object($job['data']['spec']['parameters']),'Job API must encode empty parameters as an object.');
|
|
check_backend(Job::get($job['data']['id'],'other-workspace')===null,'Job ownership must be scoped.');
|
|
check_backend(Job::cancel($job['data']['id'],$owner)['ok'],'Model cancellation failed.');
|
|
$modelRetry=Job::retry($job['data']['id'],$owner);
|
|
check_backend($modelRetry['ok']&&$modelRetry['data']['kind']==='model','Model retry must preserve kind.');
|
|
$mapBytes=pack('g*',...array_fill(0,2048,100.0));
|
|
$height=Maps::save('height','T21','height',$mapBytes);check_backend($height['ok'],'Height map save failed.');
|
|
$mapSpec=['model'=>['resolution'=>'T21'],'maps'=>['height'=>$height['data']['id']]];
|
|
$capture=Maps::captures($mapSpec);check_backend($capture['ok']&&hash('sha256',$capture['data']['height']['data'])===$height['data']['digest'],'Map capture must preserve immutable bytes.');
|
|
$land=Maps::save('landmask','T21','land',pack('g*',...array_fill(0,2048,1.0)));check_backend($land['ok'],'Land map save failed.');
|
|
$mapped=Experiment::save($owner,['name'=>'mapped','spec'=>['model'=>['resolution'=>'T21'],'parameters'=>[],'run'=>['cycles'=>1,'steps'=>1440],'maps'=>['landmask'=>$land['data']['id'],'height'=>$height['data']['id']]]]);
|
|
$mappedJob=Job::enqueue($owner,$mapped['data']);check_backend($mappedJob['ok'],'Mapped job enqueue failed.');
|
|
$captured=Job::mapCaptures($mappedJob['data']['id']);check_backend($captured['height']['data']===$mapBytes,'Job must capture exact map bytes.');check_backend(Job::validatedMapCaptures($mappedJob['data'])['ok'],'Map capture validation must not depend on JSON key order.');
|
|
$badCapture=$mappedJob['data'];$badCapture['spec']['maps']=[];check_backend(empty(Job::validatedMapCaptures($badCapture)['ok']),'Missing map capture rows must fail validation.');
|
|
$changed=Maps::save('height','T21','later edit',pack('g*',...array_fill(0,2048,200.0)));check_backend($changed['ok']&&Job::mapCaptures($mappedJob['data']['id'])['height']['data']===$mapBytes,'Later map edits must not change queued job inputs.');
|
|
check_backend(Job::cancel($mappedJob['data']['id'],$owner)['ok'],'Mapped cancellation failed.');$mappedRetry=Job::retry($mappedJob['data']['id'],$owner);check_backend($mappedRetry['ok']&&Job::mapCaptures($mappedRetry['data']['id'])['height']['data']===$mapBytes,'Retry must copy the original immutable map capture.');
|
|
$invalid=Maps::save('landmask','T21','bad',pack('g*',...array_fill(0,2048,0.5)));check_backend(empty($invalid['ok']),'Non-binary land mask must fail.');
|
|
check_backend(empty(Maps::save('height','T21','wrong size',pack('g*',1.0))['ok']),'Wrong map dimensions must fail.');
|
|
$nonfinite=Maps::save('height','T21','nonfinite',pack('g*',...array_merge([NAN],array_fill(0,2047,0.0))));check_backend(empty($nonfinite['ok']),'Nonfinite map values must fail.');
|
|
$otherResolution=Maps::save('height','T42','other grid',pack('g*',...array_fill(0,8192,10.0)));check_backend($otherResolution['ok'],'T42 map save failed.');
|
|
$cross=Experiment::save($owner,['name'=>'cross','spec'=>['model'=>['resolution'=>'T21'],'maps'=>['height'=>$otherResolution['data']['id']]]]);check_backend(empty(Job::enqueue($owner,$cross['data'])['ok']),'Cross-resolution map enqueue must fail.');
|
|
$planet=Job::enqueueKind($owner,'planet','generated',['model'=>[],'parameters'=>[],'run'=>['cycles'=>5],'surface'=>['seed'=>12]]);
|
|
check_backend($planet['ok'],'Planet enqueue failed.');
|
|
check_backend($planet['ok']&&$planet['data']['kind']==='planet'&&(int)$planet['data']['total_cycles']===1,'Planet jobs must have one generation cycle.');
|
|
check_backend(is_object($planet['data']['spec']['parameters']),'Planet job API must encode empty parameters as an object.');
|
|
check_backend(Job::cancel($planet['data']['id'],$owner)['ok'],'Planet cancellation failed.');
|
|
$planetRetry=Job::retry($planet['data']['id'],$owner);
|
|
check_backend($planetRetry['ok']&&$planetRetry['data']['kind']==='planet'&&(int)$planetRetry['data']['total_cycles']===1,'Planet retry must preserve kind and one generation cycle.');
|
|
check_backend(empty(Job::enqueueKind($owner,'arbitrary','bad',[])['ok']),'Unsupported task kind must fail.');
|
|
echo "backend PHP tests passed\n";
|
|
} finally {
|
|
putenv('EXOPLASIM_CONFIG');
|
|
foreach (glob($tmp.'/jobs/*')?:[] as $f) { if(is_dir($f)) rmdir($f); else unlink($f); }
|
|
if (is_dir($tmp.'/jobs')) rmdir($tmp.'/jobs');
|
|
foreach (glob($tmp.'/data/*')?:[] as $f) if(is_file($f)) unlink($f);
|
|
if(is_dir($tmp.'/data')) rmdir($tmp.'/data');
|
|
foreach (glob($tmp.'/catalog/*')?:[] as $f) if(is_file($f)) unlink($f);
|
|
if(is_dir($tmp.'/catalog')) rmdir($tmp.'/catalog');
|
|
foreach (glob($tmp.'/*')?:[] as $f) if(is_file($f)) unlink($f);
|
|
rmdir($tmp);
|
|
}
|