conformance.php (5632B)
1 <?php 2 declare(strict_types=1); 3 ini_set('display_errors', 'stderr'); 4 require __DIR__.'/../src/ffi.php'; 5 use Arboricx\{Library, TricuError, Context}; 6 function check(bool $ok, string $message='conformance failed'): void { if (!$ok) throw new RuntimeException($message); } 7 function expectStatus(int $status, callable $operation): void { 8 try { $operation(); } catch (TricuError $error) { check($error->status === $status, $error->getMessage()); return; } 9 throw new RuntimeException('invalid operation accepted'); 10 } 11 [$script,$libraryPath,$fixtureDir,$specification] = $argv; 12 $f = json_decode(file_get_contents($specification),true,flags:JSON_THROW_ON_ERROR); 13 $library = new Library($libraryPath); $ctx = $library->context(); $other = $library->context(); 14 $load = fn(string $file,string $name) => $ctx->loadBundle(file_get_contents("$fixtureDir/$file"),$name); 15 $report = []; 16 try { 17 $report['numbers'] = array_map(fn($n) => $ctx->toNumber($ctx->number($n)),$f['numbers']); 18 $report['strings'] = array_map(fn($s) => $ctx->toString($ctx->string($s)),$f['strings']); 19 $report['bytesHex'] = array_map(fn($h) => bin2hex($ctx->toBytes($ctx->bytes(hex2bin($h)))),$f['bytesHex']); 20 $worker=$load('id.arboricx','id'); $argument=$ctx->string($f['identity']); 21 $application=$ctx->apply($worker,$argument); $ctx->release($worker); $ctx->release($argument); 22 $report['identity']=$ctx->toString($ctx->reduce($application)); 23 $append=$load('append.arboricx','append'); 24 foreach ($f['append'] as $s) $append=$ctx->apply($append,$ctx->string($s)); 25 $report['append']=$ctx->toString($append); 26 $report['booleans']=array_map(fn($name) => $ctx->toBool($load($name.'.arboricx',$name)),['true','false']); 27 $report['notQKind']=$ctx->kind($ctx->reduce($load('notQ.arboricx','not?'))); 28 $report['outcomes']=[]; 29 foreach ([0,2,9] as $code) { 30 $program=$ctx->fork($ctx->string('tricuIO'),$ctx->fork($ctx->number(2),$ctx->fork($ctx->number($code),$ctx->string('opaque payload')))); 31 $r=$ctx->runIO($program); 32 $report['outcomes'][]=['outcome'=>$r['outcome'],'payload'=>$ctx->toString($r['value']),'cleanupKind'=>$ctx->kind($r['cleanup'])]; 33 $ctx->release($program); $ctx->release($r['value']); $ctx->release($r['cleanup']); 34 } 35 $root=$ctx->string('owned'); $alias=$ctx->retain($root); 36 check($root->cdata !== $alias->cdata); $ctx->release($root); check($ctx->toString($alias)==='owned'); 37 $wrong=$ctx->stem($ctx->stem($ctx->leaf())); 38 $report['negativeStatuses']=[]; 39 foreach ([fn()=>$ctx->release($root),fn()=>$other->retain($alias),fn()=>$ctx->toNumber($wrong),fn()=>$ctx->toBool($wrong), 40 fn()=>$ctx->loadBundle('bad bundle','id'),fn()=>$load('id.arboricx','missing'),fn()=>$ctx->runIO($ctx->leaf())] as $operation) { 41 try { $operation(); throw new LogicException('invalid operation accepted'); } 42 catch (TricuError $error) { check($error->getMessage()!==''); $report['negativeStatuses'][]=$error->status; } 43 } 44 foreach ($f['invalidUtf8Hex'] as $h) expectStatus(3,fn()=>$ctx->string(hex2bin($h))); 45 foreach ([-1,'18446744073709551616','1.5'] as $n) { 46 try { $ctx->number($n); throw new LogicException('invalid number accepted'); } 47 catch (RangeException $error) {} 48 } 49 if (isset($argv[4])) checkCopier($library,$argv[4],$f); 50 // PHP array equality ignores object-key order while retaining value types. 51 check($report == $f['expected'],json_encode($report)); 52 } finally { $other->close(); $ctx->close(); } 53 expectStatus(1,fn()=>$ctx->leaf()); $ctx->close(); 54 for ($i=0;$i<100;$i++) { $fresh=$library->context(); $fresh->close(); } 55 echo json_encode($report,JSON_THROW_ON_ERROR|JSON_UNESCAPED_UNICODE)."\n"; 56 57 function checkCopier(Library $library,string $bundle,array $f): void 58 { 59 $dir=sys_get_temp_dir().'/tricu-php-'.bin2hex(random_bytes(8)); 60 mkdir($dir); $source="$dir/in"; $target="$dir/out"; 61 $body=str_repeat(hex2bin($f['copyHex']),$f['copyRepeats']); 62 $ctx=$library->context(); $bytes=file_get_contents($bundle); $worker=$ctx->loadBundle($bytes,'copy'); 63 $run=fn($fn,$destination=null)=>$ctx->runIO($ctx->apply($ctx->apply($fn,$ctx->string($source)),$ctx->string($destination??$target))); 64 $count=fn()=>is_dir('/proc/self/fd') ? count(scandir('/proc/self/fd')) : null; 65 try { 66 file_put_contents($source,$body); $baseline=null; 67 for ($i=0;$i<3;$i++) { 68 $r=$run($worker); 69 check($r['outcome']===0 && $ctx->kind($r['cleanup'])===0); 70 check($ctx->toBool($ctx->child($r['value'],0))===true); 71 check(file_get_contents($target)===$body); 72 if ($i===0) $baseline=$count(); else check($count()===$baseline,'descriptor leak'); 73 $ctx->release($r['value']); $ctx->release($r['cleanup']); 74 } 75 $r=$run($ctx->loadBundle($bytes,'cancelledCopy')); 76 check($r['outcome']===0 && $ctx->toString($ctx->child($r['value'],0))==='cancelled'); 77 check($ctx->toString($ctx->child($ctx->child($r['value'],1),0))==='requested'); 78 check(file_get_contents($target)===substr($body,0,4096)); 79 $r=$run($worker,"$dir/missing-parent/out"); 80 check($r['outcome']===0 && $ctx->toBool($ctx->child($r['value'],0))===false); 81 check($count()===$baseline,'descriptor leak on failure'); 82 unlink($source); unlink($target); 83 $r=$run($worker); 84 check($r['outcome']===0 && $ctx->toBool($ctx->child($r['value'],0))===false); 85 check(!file_exists($target)); 86 } finally { 87 $ctx->close(); 88 foreach (glob("$dir/*") as $file) unlink($file); 89 rmdir($dir); 90 } 91 }