test_container_harness.py (8988B)
1 """Engine-free controls; real distro acceptance remains an explicit matrix run.""" 2 import argparse 3 import hashlib 4 import subprocess 5 import io 6 import json 7 from pathlib import Path 8 import tarfile 9 import tempfile 10 import unittest 11 from unittest.mock import patch 12 13 import container_matrix as matrix 14 import container_bindings as bindings 15 from container_probe import extract, incompatible_loader_error 16 17 18 class HarnessTests(unittest.TestCase): 19 def test_glibc_control(self): 20 self.assertTrue(incompatible_loader_error("libc.so.6: version `GLIBC_2.38' not found (required by libtricu)", '2.35')) 21 for error in ('libgmp.so.10: cannot open shared object file', 'Segmentation fault', 22 "version `GLIBC_2.34' not found", "version `GLIBCXX_3.4.30' not found", 23 "ModuleNotFoundError: No module named 'ctypes'"): 24 self.assertFalse(incompatible_loader_error(error, '2.35')) 25 26 def test_image_identity(self): 27 base = dict(Architecture='amd64', Os='linux', Id='a'*64) 28 with patch.object(matrix, 'checked', return_value=json.dumps([base])): 29 self.assertEqual(matrix.inspect('podman', 'test')['Id'], 'sha256:'+'a'*64) 30 for change in (dict(Architecture='arm64'), dict(Os='windows'), dict(Id='latest')): 31 with patch.object(matrix, 'checked', return_value=json.dumps([dict(base, **change)])): 32 with self.assertRaises(ValueError): 33 matrix.inspect('docker', 'test') 34 35 def test_lock_requires_fixed_expectation_and_evidence(self): 36 with tempfile.TemporaryDirectory() as directory: 37 root = Path(directory) 38 (root/'ubuntu22').mkdir() 39 row = dict(name='ubuntu22', image_id='sha256:'+'a'*64, expectation='glibc-rejection') 40 for file, key in [('Dockerfile', 'dockerfile_sha256'), ('packages.txt', 'packages_sha256')]: 41 (root/'ubuntu22'/file).write_text(file) 42 row[key] = hashlib.sha256(file.encode()).hexdigest() 43 path = root/'images.json' 44 def save(rows): 45 path.write_text(json.dumps(dict(schema='tricu.distro-images.v1', images=rows))) 46 save([row]) 47 self.assertEqual(matrix.load_lock(path)['images'], [row]) 48 for rows in ([], [row, row], [dict(row, image_id='ubuntu:22.04')], 49 [dict(row, expectation='pass')], [dict(row, name='../escape')]): 50 save(rows) 51 with self.assertRaises(ValueError): 52 matrix.load_lock(path) 53 save([row]) 54 (root/'ubuntu22/packages.txt').write_text('changed') 55 with self.assertRaises(ValueError): 56 matrix.load_lock(path) 57 58 def test_bindings_preparation_and_lock(self): 59 with tempfile.TemporaryDirectory() as directory: 60 root = Path(directory) 61 for name in matrix.BINDING_FILES: 62 path = root/'kit/bindings'/name 63 path.parent.mkdir(parents=True, exist_ok=True) 64 path.write_text('{}') 65 args = argparse.Namespace(directory=root/'images', distro=['ubuntu24'], 66 engine='podman', bindings=True, node_version='22.22.0') 67 image = dict(Id='sha256:'+'a'*64, RepoDigests=['example/node@sha256:'+'b'*64]) 68 with patch.object(matrix, '__file__', str(root/'kit/container_matrix.py')), \ 69 patch.object(matrix, 'checked', return_value='packages'), \ 70 patch.object(matrix, 'inspect', return_value=image), \ 71 patch.object(matrix.subprocess, 'run', return_value=subprocess.CompletedProcess([], 0, '', '')): 72 matrix.prepare(args) 73 lock = matrix.load_lock(args.directory/'images.json') 74 self.assertEqual(lock['profile'], 'bindings') 75 self.assertEqual(lock['node']['version'], '22.22.0') 76 recipe = (args.directory/'ubuntu24/Dockerfile').read_text() 77 self.assertIn('npm ci --omit=dev', recipe) 78 self.assertIn('php-ffi', recipe) 79 self.assertIn('COPY --from='+image['RepoDigests'][0], recipe) 80 path = args.directory/'images.json' 81 for change in (dict(version='20.19.0'), dict(digest='latest'), 82 dict(digest='other/node@sha256:'+'c'*64)): 83 path.write_text(json.dumps(dict(lock, node=dict(lock['node'], **change)))) 84 with self.assertRaises(ValueError): 85 matrix.load_lock(path) 86 path.write_text(json.dumps(lock)) 87 (args.directory/'ubuntu24/package-lock.json').write_text('changed') 88 with self.assertRaises(ValueError): 89 matrix.load_lock(path) 90 for enabled, version in ((True, None), (False, '22.22.0'), (True, 'latest'), (True, '20.19.0')): 91 args.bindings, args.node_version = enabled, version 92 with self.assertRaises(ValueError): 93 matrix.prepare(args) 94 95 def test_packaged_bindings_layout(self): 96 with tempfile.TemporaryDirectory() as directory: 97 kit = Path(directory)/'kit' 98 for name in matrix.BINDING_FILES: 99 path = kit/'bindings'/name 100 path.parent.mkdir(parents=True, exist_ok=True) 101 path.write_text('{}') 102 matrix.validate_bindings_kit(kit) 103 for name in matrix.BINDING_FILES: 104 path = kit/'bindings'/name 105 renamed = path.with_name('store-hash-'+path.name) 106 path.rename(renamed) 107 with self.assertRaises(ValueError): 108 matrix.validate_bindings_kit(kit) 109 renamed.rename(path) 110 (kit/'bindings/ext/js/package.json').unlink() 111 args = argparse.Namespace(directory=Path(directory)/'images', distro=['ubuntu24'], 112 engine='podman', bindings=True, node_version='22.22.0') 113 with patch.object(matrix, '__file__', str(kit/'container_matrix.py')), \ 114 patch.object(matrix, 'checked') as engine: 115 with self.assertRaises(ValueError): 116 matrix.prepare(args) 117 engine.assert_not_called() 118 self.assertFalse(args.directory.exists()) 119 120 def test_binding_results_and_runtime_versions(self): 121 with tempfile.TemporaryDirectory() as directory: 122 root = Path(directory) 123 source = root/'work/bindings' 124 for name, body in { 125 'work/bindings/ext/js/package-lock.json': json.dumps({'packages': {'node_modules/koffi': {'version': '2.16.2'}}}), 126 'opt/tricu-js/node_modules/koffi/package.json': '{"version":"2.16.2"}', 127 'work/bindings/ext/haskell/bindings/fixtures.json': '{"expected":{"result":42}}', 128 'release/include/tricu.h': 'header', 129 }.items(): 130 path = root/name 131 path.parent.mkdir(parents=True, exist_ok=True) 132 path.write_text(body) 133 (source/'ext/php').mkdir() 134 php = json.dumps(dict(ffi=True, integer_bytes=8, version='8.3', ffi_enable='1')) 135 good = ['v22.22.0', php, '{"result":42}', '{"result":42}', '{"result":42}'] 136 with patch.object(bindings, 'Path', side_effect=lambda name: root/str(name).lstrip('/')): 137 with patch.object(bindings, 'checked', side_effect=good): 138 self.assertEqual(bindings.run(root/'release', '22.22.0')['passed'], ['python', 'js', 'php']) 139 for values in (['v20.19.0'], ['v22.22.0', '{"ffi":false,"integer_bytes":8}'], 140 good[:3]+['{"result":0}']): 141 with patch.object(bindings, 'checked', side_effect=values): 142 with self.assertRaises(ValueError): 143 bindings.run(root/'release', '22.22.0') 144 145 def test_archive_members(self): 146 with tempfile.TemporaryDirectory() as directory: 147 root = Path(directory) 148 archive = root/'test.tar.gz' 149 for name, kind in [('tricu/file', tarfile.REGTYPE), ('../escape', tarfile.REGTYPE), 150 ('/tricu/file', tarfile.REGTYPE), ('tricu/link', tarfile.SYMTYPE)]: 151 with tarfile.open(archive, 'w:gz') as stream: 152 entry = tarfile.TarInfo(name) 153 entry.type = kind 154 entry.size = 1 if kind == tarfile.REGTYPE else 0 155 entry.linkname = '/outside' if kind == tarfile.SYMTYPE else '' 156 stream.addfile(entry, io.BytesIO(b'x')) 157 if name == 'tricu/file': 158 self.assertEqual((extract(archive, root/'out')/'file').read_bytes(), b'x') 159 else: 160 with self.assertRaises(ValueError): 161 extract(archive, root/'out') 162 163 164 if __name__ == '__main__': 165 unittest.main()