tricu

A lazy functional language based on Tree Calculus
Log | Files | Refs | README | LICENSE

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()