@@ -0,0 +1,125 @@
|
||||
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'))
|
||||
Reference in New Issue
Block a user