container_bindings.py (3794B)
1 """Distro Python/PHP and upstream Node conformance against one relocated host.""" 2 import json 3 import os 4 from pathlib import Path 5 import shutil 6 import subprocess 7 import sys 8 9 10 def checked(command, env=None): 11 result = subprocess.run(command, text=True, capture_output=True, timeout=90, env=env) 12 if result.returncode: 13 raise RuntimeError(f'{command[0]} failed ({result.returncode}): {result.stdout}\n{result.stderr}') 14 return result.stdout.strip() 15 16 17 def run(release, node_version): 18 source = Path('/work/bindings') 19 if not source.exists(): 20 shutil.copytree('/kit/bindings', source) 21 (source/'ext/js/node_modules').symlink_to('/opt/tricu-js/node_modules') 22 # PHP consumes the transferred header. 23 shutil.copyfile(release/'include/tricu.h', source/'ext/php/tricu.h') 24 node = '/opt/node/bin/node' 25 actual_node = checked([node, '--version']) 26 if actual_node != 'v'+node_version: 27 raise ValueError('upstream Node version differs from image lock') 28 php = ['/usr/bin/php', '-d', 'ffi.enable=true'] 29 php_info = json.loads(checked(php+['-r', 'echo json_encode(["version"=>PHP_VERSION,"ffi"=>extension_loaded("FFI"),"ffi_enable"=>ini_get("ffi.enable"),"integer_bytes"=>PHP_INT_SIZE]);'])) 30 if not php_info['ffi'] or php_info['integer_bytes'] != 8: 31 raise ValueError('64-bit distro PHP with FFI required') 32 koffi = json.loads(Path('/opt/tricu-js/node_modules/koffi/package.json').read_text())['version'] 33 locked = json.loads((source/'ext/js/package-lock.json').read_text())['packages']['node_modules/koffi']['version'] 34 if koffi != locked: 35 raise ValueError('installed Koffi differs from package lock') 36 specification = source/'ext/haskell/bindings/fixtures.json' 37 expected = json.loads(specification.read_text())['expected'] 38 arguments = [str(release/'lib/libtricu-haskell.so'), '/tests/fixtures', str(specification), '/tests/copy.arboricx'] 39 js = '''import {readFileSync} from 'node:fs'; 40 import {pathToFileURL} from 'node:url'; 41 import {strict as assert} from 'node:assert'; 42 const {conformance} = await import(pathToFileURL(process.argv[1])); 43 const report = conformance(...process.argv.slice(2)); 44 const maps = readFileSync('/proc/self/maps','utf8'); 45 assert(!maps.includes('/nix/store/')); 46 assert(maps.includes(process.argv[2])); 47 assert(maps.includes('/opt/tricu-js/node_modules/koffi/')); 48 console.log(JSON.stringify(report));''' 49 php_script = '''array_shift($argv); require $argv[0]; 50 $maps = file_get_contents('/proc/self/maps'); 51 if (str_contains($maps, '/nix/store/') || !str_contains($maps, $argv[1])) { 52 throw new RuntimeException('unexpected native mappings'); 53 }''' 54 commands = { 55 'python': [sys.executable, str(source/'ext/haskell/bindings/python.py')], 56 'js': [node, '--input-type=module', '-e', js, str(source/'ext/js/test/conformance.mjs')], 57 'php': php+['-r', php_script, str(source/'ext/php/tests/conformance.php')], 58 } 59 env = dict(os.environ, PYTHONPATH=str(release/'share/tricu'), PYTHONDONTWRITEBYTECODE='1') 60 results = {} 61 for language, command in commands.items(): 62 actual = json.loads(checked(command+arguments, env)) 63 if actual != expected: 64 raise ValueError(f'{language}: independent conformance expectations differ') 65 results[language] = actual 66 return dict(passed=list(results), results=results, node=actual_node, koffi=koffi, php=php_info, 67 provenance=dict(node='upstream official Node image, exact version and registry digest', 68 php='distro package with process-local ffi.enable=true', 69 koffi='npm ci from committed package lock'), 70 scope='CLI bindings, codecs, ownership, error controls and compiled copier IO')