Files
modulus/tests/test_bootstrap.py
T
khodowany 0c9ba1bff4
publish / publish (push) Successful in 10m36s
Cleanup
2026-09-24 19:40:03 -05:00

126 lines
7.7 KiB
Python

import ast
import json
import os
from pathlib import Path
import subprocess
import tempfile
import unittest
from unittest.mock import patch
import modulus.bootstrap
import modulus.cli
import modulus.config
import modulus.export
class BootstrapTests(unittest.TestCase):
def test_recipe_exports_prune_generated_trees(self):
workspace = Path(modulus.bootstrap.__file__).parents[2]
for name in ('modulus', 'ligo', 'noto', 'luma', 'sonus', 'product-vamp'):
recipe = workspace / name / 'conanfile.py'
if not recipe.exists():
continue
fields = {target.id: ast.literal_eval(node.value) for node in ast.walk(ast.parse(recipe.read_text())) if isinstance(node, ast.Assign) for target in node.targets if isinstance(target, ast.Name) and target.id in {'exports', 'exports_sources'}}
for field in ('exports', 'exports_sources'):
self.assertTrue({'!build', '!.git', '!test_package/build'}.issubset(fields[field]), f'{name}: {field}')
def test_consumers_use_canonical_bootstrap(self):
canonical = Path(modulus.bootstrap.__file__)
for name in ('ligo', 'noto', 'luma', 'sonus', 'product-vamp'):
consumer = canonical.parents[2] / name / 'modulus.py'
if consumer.exists():
self.assertEqual(canonical.read_bytes(), consumer.read_bytes(), name)
def test_read_environments_strip_all_credential_forms(self):
credentials = dict.fromkeys(('CONAN_USER', 'CONAN_TOKEN', 'CONAN_LOGIN_USERNAME', 'CONAN_PASSWORD', 'CONAN_LOGIN_USERNAME_KHODO', 'CONAN_PASSWORD_KHODO'), 'secret')
root = Path('/tmp/project')
context = modulus.cli.build_context(root, modulus.config.normalize({'package': {'name': 'sample'}}))
with patch.dict(os.environ, credentials):
for env in (modulus.bootstrap.fetch_env(root), modulus.cli.subprocess_environment(context, ['conan', 'install'])):
self.assertFalse(set(credentials) & env.keys())
self.assertEqual(env['CONAN_NON_INTERACTIVE'], '1')
self.assertEqual(env['CONAN_HOME'], str(root / 'build/build_common/conan_cache'))
def test_update_refreshes_runtime_and_offline_stays_offline(self):
root = Path('/tmp/project')
with patch.object(modulus.bootstrap, 'runtime_ready', return_value=True), patch.object(modulus.bootstrap, 'materialize_runtime') as refresh:
modulus.bootstrap.ensure_runtime(root)
refresh.assert_not_called()
modulus.bootstrap.ensure_runtime(root, update=True, no_remote=True)
refresh.assert_called_once_with(root, no_remote=True)
with patch.object(modulus.bootstrap, 'run_fetch', return_value=subprocess.CompletedProcess([], 0, '{}')) as fetch:
with self.assertRaisesRegex(SystemExit, 'local Conan cache'):
modulus.bootstrap.find_modulus(root)
self.assertNotIn('-r', fetch.call_args.args[1])
def test_fetch_failure_preserves_runtime(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
runtime = root / modulus.bootstrap.RUNTIME_DIR / 'modulus/cli.py'
runtime.parent.mkdir(parents=True)
runtime.write_text('old runtime')
with patch.object(modulus.bootstrap, 'remote_config', return_value=('khodo', 'https://example.test')), patch.object(modulus.bootstrap, 'configure_remote'), patch.object(modulus.bootstrap, 'find_modulus', side_effect=SystemExit(1)):
with self.assertRaises(SystemExit):
modulus.bootstrap.materialize_runtime(root)
self.assertEqual(runtime.read_text(), 'old runtime')
def test_offline_bootstrap_never_builds_a_fetch_image(self):
with patch.object(modulus.bootstrap.subprocess, 'run', return_value=subprocess.CompletedProcess([], 1)), patch.object(modulus.bootstrap, 'run') as build:
with self.assertRaisesRegex(SystemExit, 'offline bootstrap requires'):
modulus.bootstrap.ensure_fetch_image(offline=True)
build.assert_not_called()
def test_export_rolls_back_when_editor_refresh_fails(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
(root / 'modulus.json').write_text(json.dumps({'package': {'name': 'sample'}}))
runtime = root / modulus.export.runtime_dir / 'modulus/cli.py'
runtime.parent.mkdir(parents=True)
runtime.write_text('old runtime')
package = root / 'package'
(package / 'modulus').mkdir(parents=True)
(package / 'modulus/cli.py').write_text('new runtime')
(package / 'tools').mkdir()
with patch.object(modulus.export, 'sync_editor_files', side_effect=OSError('editor is read-only')):
with self.assertRaisesRegex(OSError, 'read-only'):
modulus.export.sync_project(root, package)
self.assertEqual(runtime.read_text(), 'old runtime')
self.assertFalse((root / modulus.export.tools_dir).exists())
def test_public_remote_error_is_noninteractive_and_does_not_delete_packages(self):
completed = subprocess.CompletedProcess([], 1, '', '401 Unauthorized')
with patch.object(modulus.bootstrap, 'direct_mode', return_value=True), patch.object(modulus.bootstrap.subprocess, 'run', return_value=completed) as run:
with self.assertRaises(SystemExit):
modulus.bootstrap.find_modulus(Path('/tmp/project'), 'khodo')
self.assertEqual(run.call_args.args[0][:2], ['conan', 'list'])
self.assertEqual(run.call_args.kwargs['env']['CONAN_NON_INTERACTIVE'], '1')
def test_export_stages_all_payloads_before_replacing_runtime(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
(root / 'modulus.json').write_text(json.dumps({'package': {'name': 'sample'}}))
runtime = root / modulus.export.runtime_dir / 'modulus/cli.py'
runtime.parent.mkdir(parents=True)
runtime.write_text('old runtime')
package = root / 'package'
(package / 'modulus').mkdir(parents=True)
(package / 'modulus/cli.py').write_text('new runtime')
with self.assertRaisesRegex(RuntimeError, 'missing tools'):
modulus.export.sync_project(root, package)
self.assertEqual(runtime.read_text(), 'old runtime')
def test_upload_exception_still_logs_out(self):
context = modulus.cli.build_context(Path('/tmp/project'), modulus.config.normalize({'package': {'name': 'sample'}}))
with patch.object(modulus.cli, 'configure_conan_remote', return_value=0), patch.object(modulus.cli, 'upload_all_packages', side_effect=RuntimeError('upload failed')), patch.object(modulus.cli, 'run', return_value=0) as run:
with self.assertRaisesRegex(RuntimeError, 'upload failed'):
modulus.cli.upload_packages(context)
run.assert_called_once_with(context, ['conan', 'remote', 'logout', 'khodo'])
def test_socket_is_scoped_and_docker_inputs_change_fingerprint(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
config = modulus.config.normalize({'package': {'name': 'sample'}, 'tooling': {'docker': {'dockerfile': 'package/build/Dockerfile', 'inputs': ['requirements.txt'], 'socket_commands': ['build', 'package']}}})
context = modulus.cli.build_context(root, config)
context.dockerfile.parent.mkdir(parents=True)
context.dockerfile.write_text('FROM scratch')
context.docker_inputs[0].parent.mkdir(parents=True)
for path in context.docker_inputs:
path.write_text('old')
before = modulus.clangd.recipe_digest(context)
context.docker_inputs[-1].write_text('new')
self.assertNotEqual(before, modulus.clangd.recipe_digest(context))
with patch.object(Path, 'is_socket', return_value=True), patch.object(Path, 'stat') as stat:
stat.return_value.st_gid = 123
for command in ('configure', 'test', 'lsp', 'clangd', 'format'):
self.assertEqual(modulus.cli.docker_socket_args(context, command), [])
self.assertIn('/var/run/docker.sock:/var/run/docker.sock', modulus.cli.docker_socket_args(context, 'package'))