104 lines
12 KiB
PHP
104 lines
12 KiB
PHP
<?php
|
|
if (getenv('EXOPLASIM_API_TEST_CHILD') === '1') {
|
|
parse_str(getenv('EXOPLASIM_API_TEST_QUERY') ?: '', $_GET);
|
|
$_POST=[];
|
|
$_SERVER['REQUEST_METHOD']='GET';
|
|
$_SERVER['SCRIPT_NAME']='/exoplasim/web/index.php';
|
|
$_SERVER['PHP_SELF']='/exoplasim/web/index.php';
|
|
$_SERVER['HTTP_HOST']='localhost';
|
|
$_SERVER['HTTPS']='on';
|
|
require __DIR__.'/../web/index.php';
|
|
exit;
|
|
}
|
|
|
|
require_once __DIR__.'/../web/lib/labdb.class.php';
|
|
|
|
function check(bool $ok,string $message): void { if(!$ok)throw new RuntimeException($message); }
|
|
function request_api(array $query): string {
|
|
putenv('EXOPLASIM_API_TEST_CHILD=1');
|
|
putenv('EXOPLASIM_API_TEST_QUERY='.http_build_query($query));
|
|
$pipes=[];$process=proc_open([PHP_BINARY,__FILE__],[0=>['pipe','r'],1=>['pipe','w'],2=>['pipe','w']],$pipes,dirname(__DIR__));
|
|
if(!is_resource($process))throw new RuntimeException('Could not start PHP route fixture.');
|
|
fclose($pipes[0]);$body=stream_get_contents($pipes[1]);$error=stream_get_contents($pipes[2]);fclose($pipes[1]);fclose($pipes[2]);$status=proc_close($process);
|
|
putenv('EXOPLASIM_API_TEST_CHILD');putenv('EXOPLASIM_API_TEST_QUERY');
|
|
if($status!==0)throw new RuntimeException('PHP route fixture failed: '.$error);
|
|
return $body;
|
|
}
|
|
function reply_data(string $body): array {
|
|
$reply=json_decode($body,true);
|
|
check(is_array($reply),'Expected a JSON response.');
|
|
return $reply;
|
|
}
|
|
function create_job(PDO $db,string $id,string $kind,string $spec,string $now): void {
|
|
$s=$db->prepare("INSERT INTO jobs(id,owner,name,spec,kind,status,total_cycles,created_at,updated_at) VALUES(?,?,?,?,?,'completed',1,?,?)");
|
|
$s->execute([$id,'local',$kind.' test',$spec,$kind,$now,$now]);
|
|
}
|
|
function write_json(string $path,array $value): void { file_put_contents($path,json_encode($value,JSON_THROW_ON_ERROR)); }
|
|
|
|
$root=dirname(__DIR__);$tmp=sys_get_temp_dir().'/exoplasim-api-'.bin2hex(random_bytes(5));mkdir($tmp,0700,true);
|
|
foreach(['catalog','jobs','runtime','data'] as $dir)mkdir($tmp.'/'.$dir,0750,true);
|
|
$config=json_decode(file_get_contents($root.'/config/web.json'),true,512,JSON_THROW_ON_ERROR);
|
|
$config['storage']['root']=$tmp;$config['storage']['catalog']='catalog/parameters.json';$config['max_result_bytes']=256;$config['max_field_memory_bytes']=64;
|
|
file_put_contents($tmp.'/config.json',json_encode($config,JSON_THROW_ON_ERROR));putenv('EXOPLASIM_CONFIG='.$tmp.'/config.json');
|
|
try {
|
|
$command=escapeshellarg($root.'/.venv/bin/python').' '.escapeshellarg($root.'/scripts/web/parameters.py').' --config '.escapeshellarg($tmp.'/config.json').' --output '.escapeshellarg($tmp.'/catalog/parameters.json').' 2>&1';
|
|
exec($command,$output,$status);check($status===0,'Could not generate the test catalog: '.implode("\n",$output));
|
|
$db=LabDb::connection();$now=gmdate('c');$modelId=str_repeat('a',32);$modelDataset=str_repeat('c',32);$planetId=str_repeat('b',32);$planetDataset=str_repeat('d',32);$token=str_repeat('e',32);
|
|
$spec=json_encode(['model'=>[],'run'=>['cycles'=>1],'parameters'=>(object)[],'maps'=>['height'=>$token]],JSON_THROW_ON_ERROR);
|
|
create_job($db,$modelId,'model',$spec,$now);create_job($db,$planetId,'planet',$spec,$now);
|
|
$modelRoot=$tmp.'/jobs/'.$modelId;$modelArtifacts=$modelRoot.'/artifacts';$modelData=$modelArtifacts.'/datasets/'.$modelDataset;
|
|
mkdir($modelData,0750,true);mkdir($modelRoot.'/runtime',0750,true);mkdir($modelRoot.'/inputs/maps',0750,true);mkdir($modelRoot.'/inputs/native',0750,true);
|
|
file_put_contents($modelRoot.'/inputs/maps/height.f32','fixture input map');file_put_contents($modelRoot.'/inputs/native/height.sra','fixture native input');
|
|
file_put_contents($modelRoot.'/worker.log','fixture worker log');file_put_contents($modelRoot.'/runtime/GUI.cfg','fixture model config');file_put_contents($modelRoot.'/runtime/MOST.00000.npz','fixture raw model NPZ');file_put_contents($modelRoot.'/runtime/MOST.00000_metadata.npz','fixture raw model metadata');file_put_contents($modelRoot.'/runtime/plasim_restart','fixture restart');file_put_contents($modelRoot.'/runtime/MOST_REST.00000','fixture cycle restart');
|
|
$index=['id'=>$modelDataset,'lat'=>[-30,30],'lon'=>[0,120,240],'time'=>[0,1],'time_unit'=>'years','levels'=>[0],'fields'=>[['name'=>'temperature','label'=>'Temperature','unit'=>'K','shape'=>[2,1,2,3]]]];
|
|
write_json($modelData.'/index.json',$index);write_json($modelData.'/temperature.json',['mean'=>[[280],[281]],'min'=>[[270],[271]],'max'=>[[290],[291]]]);$fieldBytes=pack('g*',1,2,3,4,5,6,7,8,9,10,11,12);file_put_contents($modelData.'/temperature.f32',$fieldBytes);
|
|
write_json($modelArtifacts.'/index.json',['datasets'=>[['id'=>$modelDataset,'label'=>'Cycle 1']],'latest'=>$modelDataset]);
|
|
$planetRoot=$tmp.'/jobs/'.$planetId;$planetArtifacts=$planetRoot.'/artifacts';$planetData=$planetArtifacts.'/datasets/'.$planetDataset;
|
|
mkdir($planetData,0750,true);mkdir($planetArtifacts.'/planet-work',0750,true);mkdir($planetRoot.'/runtime',0750,true);
|
|
file_put_contents($planetRoot.'/worker.log','planet worker log');file_put_contents($planetArtifacts.'/planet-work/surface-'.$token.'.npz','fixture terrain NPZ');file_put_contents($planetArtifacts.'/planet-work/surface-'.$token.'_metadata.npz','fixture terrain metadata');file_put_contents($planetArtifacts.'/planet.json','fixture terrain recipe');file_put_contents($planetArtifacts.'/planet-work/N064_surf_0129.sra','fixture topography SRA');file_put_contents($planetArtifacts.'/planet-work/N064_surf_0172.sra',str_repeat('s',257));
|
|
$planetIndex=['id'=>$planetDataset,'lat'=>[-30,30],'lon'=>[0,120,240],'time'=>[0],'time_unit'=>'','levels'=>[0],'fields'=>[['name'=>'elevation','label'=>'Elevation','unit'=>'m','shape'=>[1,1,2,3]]]];
|
|
write_json($planetData.'/index.json',$planetIndex);write_json($planetArtifacts.'/index.json',['datasets'=>[['id'=>$planetDataset,'label'=>'Generated planet surface']],'latest'=>$planetDataset]);
|
|
|
|
$schema=reply_data(request_api(['api'=>'schema']));check($schema['ok']===true&&isset($schema['data']['fields']),'The ordinary api=schema query must resolve.');
|
|
$unknown=reply_data(request_api(['api'=>'not_found']));check(empty($unknown['ok'])&&isset($unknown['errors']['route']),'Unknown query operations must return a structured error.');
|
|
$modelResults=reply_data(request_api(['api'=>'results','id'=>$modelId]));check($modelResults['ok']===true,'The query-param results route must resolve.');$modelLinks=array_column($modelResults['data']['downloads'],'id');
|
|
foreach(['log','model-config','raw:'.$modelDataset,'raw-metadata:'.$modelDataset,'dataset-index:'.$modelDataset,'restart:plasim_restart','restart:MOST_REST.00000','field:'.$modelDataset.':temperature'] as $link)check(in_array($link,$modelLinks,true),'Results must list '.$link.'.');
|
|
foreach(['input-map:height','native-input:height'] as $link)check(in_array($link,$modelLinks,true),'Results must list '.$link.'.');
|
|
check(request_api(['api'=>'download','id'=>$modelId,'artifact'=>'input-map:height'])==='fixture input map','Captured map download must work.');
|
|
check(request_api(['api'=>'download','id'=>$modelId,'artifact'=>'native-input:height'])==='fixture native input','Retained native input download must work.');
|
|
check(empty(reply_data(request_api(['api'=>'download','id'=>$modelId,'artifact'=>'native-input:unknown']))['ok']),'Unknown native inputs must fail.');
|
|
check(request_api(['api'=>'download','id'=>$modelId,'artifact'=>'log'])==='fixture worker log','Log download must work without dataset or field parameters.');
|
|
check(request_api(['api'=>'download','id'=>$modelId,'artifact'=>'model-config'])==='fixture model config','Model config download must work.');
|
|
check(request_api(['api'=>'download','id'=>$modelId,'artifact'=>'raw:'.$modelDataset])==='fixture raw model NPZ','Raw model output download must work.');
|
|
check(request_api(['api'=>'download','id'=>$modelId,'artifact'=>'raw-metadata:'.$modelDataset])==='fixture raw model metadata','Raw model metadata download must work.');
|
|
check(request_api(['api'=>'download','id'=>$modelId,'artifact'=>'dataset-index:'.$modelDataset])===file_get_contents($modelData.'/index.json'),'Dataset index download must work.');
|
|
check(request_api(['api'=>'download','id'=>$modelId,'artifact'=>'restart:plasim_restart'])==='fixture restart','Model restart download must work.');
|
|
check(request_api(['api'=>'download','id'=>$modelId,'artifact'=>'restart:MOST_REST.00000'])==='fixture cycle restart','Cycle restart download must work.');
|
|
check(request_api(['api'=>'download','id'=>$modelId,'artifact'=>'field:'.$modelDataset.':temperature'])===$fieldBytes,'Field download must return the allowlisted binary field.');
|
|
$missing=reply_data(request_api(['api'=>'download','id'=>$modelId,'artifact'=>'field:'.$modelDataset.':missing']));check(empty($missing['ok']),'Missing field download must fail.');
|
|
$arbitrary=reply_data(request_api(['api'=>'download','id'=>$modelId,'artifact'=>'../../etc/passwd']));check(empty($arbitrary['ok']),'Arbitrary paths must not be downloadable.');
|
|
$frame=reply_data(request_api(['api'=>'frame','id'=>$modelId,'dataset'=>$modelDataset,'field'=>'temperature','time'=>2,'level'=>0]));check(empty($frame['ok'])&&isset($frame['errors']['selection']),'Out-of-range frames must fail.');
|
|
check(request_api(['api'=>'frame','id'=>$modelId,'dataset'=>$modelDataset,'field'=>'temperature','time'=>0,'level'=>0])===substr($fieldBytes,0,24),'Valid frames must return one binary frame.');
|
|
$secondDataset=str_repeat('f',32);$secondData=$modelArtifacts.'/datasets/'.$secondDataset;mkdir($secondData,0750,true);$secondIndex=$index;$secondIndex['id']=$secondDataset;
|
|
write_json($secondData.'/index.json',$secondIndex);file_put_contents($secondData.'/temperature.f32',$fieldBytes);file_put_contents($modelRoot.'/runtime/MOST.00001.npz','second cycle NPZ');
|
|
write_json($modelArtifacts.'/index.json',['datasets'=>[['id'=>$modelDataset,'label'=>'Cycle 1'],['id'=>$secondDataset,'label'=>'Cycle 2']],'latest'=>$secondDataset]);
|
|
$secondResults=reply_data(request_api(['api'=>'results','id'=>$modelId]));$secondLinks=array_column($secondResults['data']['downloads'],'id');
|
|
check(in_array('raw:'.$secondDataset,$secondLinks,true),'Latest-cycle results must list their own raw output.');
|
|
check(!in_array('raw:'.$modelDataset,$secondLinks,true)&&!in_array('field:'.$modelDataset.':temperature',$secondLinks,true),'Download listings must not scan and include unrelated cycles.');
|
|
check(request_api(['api'=>'download','id'=>$modelId,'artifact'=>'raw:'.$modelDataset])==='fixture raw model NPZ','Older-cycle downloads must still resolve directly.');
|
|
$planetResults=reply_data(request_api(['api'=>'results','id'=>$planetId]));check($planetResults['ok']===true,'Planet results route must resolve.');$planetLinks=array_column($planetResults['data']['downloads'],'id');
|
|
foreach(['terrain-recipe','dataset-index:'.$planetDataset,'sra:N064_surf_0129.sra','raw:'.$planetDataset.':surface-'.$token.'.npz','raw-metadata:'.$planetDataset.':surface-'.$token] as $link)check(in_array($link,$planetLinks,true),'Planet results must list '.$link.'.');
|
|
check(request_api(['api'=>'download','id'=>$planetId,'artifact'=>'sra:N064_surf_0129.sra'])==='fixture topography SRA','Native SRA download must work.');
|
|
check(request_api(['api'=>'download','id'=>$planetId,'artifact'=>'raw:'.$planetDataset.':surface-'.$token.'.npz'])==='fixture terrain NPZ','Raw terrain preview download must work.');
|
|
check(request_api(['api'=>'download','id'=>$planetId,'artifact'=>'raw-metadata:'.$planetDataset.':surface-'.$token])==='fixture terrain metadata','Raw terrain metadata download must work.');
|
|
check(request_api(['api'=>'download','id'=>$planetId,'artifact'=>'dataset-index:'.$planetDataset])===file_get_contents($planetData.'/index.json'),'Planet dataset index download must work.');
|
|
check(request_api(['api'=>'download','id'=>$planetId,'artifact'=>'terrain-recipe'])==='fixture terrain recipe','Terrain recipe download must work.');
|
|
$oversized=reply_data(request_api(['api'=>'download','id'=>$planetId,'artifact'=>'sra:N064_surf_0172.sra']));check(empty($oversized['ok'])&&str_contains($oversized['errors']['artifact']??'','size limit'),'Oversized artifacts must be rejected.');
|
|
echo "PHP query-route artifact tests passed\n";
|
|
} finally {
|
|
putenv('EXOPLASIM_CONFIG');putenv('EXOPLASIM_API_TEST_CHILD');putenv('EXOPLASIM_API_TEST_QUERY');
|
|
$it=new RecursiveIteratorIterator(new RecursiveDirectoryIterator($tmp,FilesystemIterator::SKIP_DOTS),RecursiveIteratorIterator::CHILD_FIRST);
|
|
foreach($it as $file){$file->isDir()?rmdir($file->getPathname()):unlink($file->getPathname());}
|
|
rmdir($tmp);
|
|
}
|