@@ -5,6 +5,7 @@
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
@@ -270,3 +271,22 @@ BOOST_AUTO_TEST_CASE(modulus_load_config_handles_files) {
|
||||
auto invalid = modulus::load_config(invalid_path.string());
|
||||
BOOST_REQUIRE(!invalid.has_value());
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(modulus_json_float_fields_validate_numeric_types_and_bounds) {
|
||||
auto object = boost::json::object{};
|
||||
auto options = modulus::json_config::options{.category = "domain", .invalid_argument_code = 42};
|
||||
auto read = [&] { return modulus::json_config::optional_float64_field(object, "gain", 0.5, 0.0, 1.0, options); };
|
||||
BOOST_REQUIRE(read());
|
||||
BOOST_TEST(static_cast<double>(*read()) == 0.5);
|
||||
for (auto const &value : {boost::json::value{0}, boost::json::value{std::uint64_t{1}}, boost::json::value{0.25}}) {
|
||||
object["gain"] = value;
|
||||
BOOST_REQUIRE(read());
|
||||
}
|
||||
for (auto const &value : {boost::json::value{-1}, boost::json::value{2U}, boost::json::value{true}, boost::json::value{"0.5"}, boost::json::value{nullptr}, boost::json::value{std::numeric_limits<double>::infinity()}, boost::json::value{std::numeric_limits<double>::quiet_NaN()}}) {
|
||||
object["gain"] = value;
|
||||
auto result = read();
|
||||
BOOST_REQUIRE(!result);
|
||||
BOOST_TEST(std::string{result.error().category()} == "domain");
|
||||
BOOST_TEST(result.error().code() == 42);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'))
|
||||
@@ -23,6 +23,7 @@ class clangd_test(unittest.TestCase):
|
||||
self.context.docker_marker = self.root / "not-in-container"
|
||||
self.context.dockerfile.parent.mkdir(parents=True)
|
||||
self.context.dockerfile.write_text("FROM sample\n")
|
||||
self.context.docker_inputs[0].write_text("autopep8==2.3.2\n")
|
||||
self.image = "sha256:" + "1" * 64
|
||||
def test_defaults_and_custom_settings(self):
|
||||
self.assertEqual(modulus.clangd.settings(self.context)["executable"], "/usr/bin/clangd-19")
|
||||
|
||||
+17
-12
@@ -110,13 +110,15 @@ class upload_packages_test(unittest.TestCase):
|
||||
self.assertEqual(cache.read_text(encoding="utf-8"), "keep")
|
||||
@unittest.mock.patch.object(modulus.cli, "upload_all_packages", return_value=37)
|
||||
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
|
||||
def test_failed_upload_does_not_force_by_default(self, configure, upload):
|
||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||
def test_failed_upload_does_not_force_by_default(self, run, configure, upload):
|
||||
context = self.context()
|
||||
with unittest.mock.patch.dict(os.environ, {}, clear=True):
|
||||
status = modulus.cli.upload_packages(context)
|
||||
self.assertEqual(status, 37)
|
||||
configure.assert_called_once_with(context, authenticate=True)
|
||||
upload.assert_called_once_with(context, "origin", force=False)
|
||||
run.assert_called_once_with(context, ["conan", "remote", "logout", "origin"])
|
||||
@unittest.mock.patch.object(modulus.cli, "upload_all_packages")
|
||||
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote")
|
||||
def test_replace_requires_guard_env(self, configure, upload):
|
||||
@@ -127,7 +129,8 @@ class upload_packages_test(unittest.TestCase):
|
||||
upload.assert_not_called()
|
||||
@unittest.mock.patch.object(modulus.cli, "upload_all_packages", return_value=0)
|
||||
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
|
||||
def test_replace_forces_upload_when_guarded(self, configure, upload):
|
||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||
def test_replace_forces_upload_when_guarded(self, run, configure, upload):
|
||||
context = self.context()
|
||||
with unittest.mock.patch.dict(
|
||||
os.environ,
|
||||
@@ -186,8 +189,9 @@ class upload_packages_test(unittest.TestCase):
|
||||
def test_docker_passthrough_includes_replace_env(self):
|
||||
passthrough = modulus.cli.docker_passthrough_env(self.context())
|
||||
self.assertIn("MODULUS_ALLOW_PACKAGE_REPLACE", passthrough)
|
||||
self.assertIn("PACKAGE_REGISTRY_USER", passthrough)
|
||||
self.assertIn("PACKAGE_REGISTRY_TOKEN", passthrough)
|
||||
for name in ("CONAN_USER", "CONAN_TOKEN", "PACKAGE_REGISTRY_USER", "PACKAGE_REGISTRY_TOKEN"):
|
||||
self.assertNotIn(name, passthrough)
|
||||
self.assertIn(name, modulus.cli.docker_passthrough_env(self.context(), publishing=True))
|
||||
self.assertIn("SAMPLE_PACKAGE_UPLOAD", passthrough)
|
||||
self.assertNotIn("CONAN_PACKAGE_VERSION", passthrough)
|
||||
self.assertNotIn("MODULUS_EXPORT_RUNTIME", passthrough)
|
||||
@@ -368,7 +372,8 @@ class upload_packages_test(unittest.TestCase):
|
||||
with unittest.mock.patch.dict(os.environ, {}, clear=True):
|
||||
status = modulus.cli.configure_conan_remote(self.context())
|
||||
self.assertEqual(status, 0)
|
||||
self.assertEqual(len(run.call_args_list), 1)
|
||||
self.assertEqual(len(run.call_args_list), 2)
|
||||
self.assertEqual(run.call_args_list[-1].args[1], ["conan", "remote", "logout", "origin"])
|
||||
self.assertEqual(
|
||||
run.call_args_list[0].args[1],
|
||||
[
|
||||
@@ -381,11 +386,11 @@ class upload_packages_test(unittest.TestCase):
|
||||
],
|
||||
)
|
||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||
def test_configure_remote_rejects_partial_conan_credentials(self, run):
|
||||
def test_configure_remote_ignores_partial_conan_credentials(self, run):
|
||||
with unittest.mock.patch.dict(os.environ, {"CONAN_USER": "dev"}, clear=True):
|
||||
status = modulus.cli.configure_conan_remote(self.context())
|
||||
self.assertEqual(status, 1)
|
||||
run.assert_not_called()
|
||||
self.assertEqual(status, 0)
|
||||
self.assertEqual(run.call_args_list[-1].args[1], ["conan", "remote", "logout", "origin"])
|
||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||
def test_configure_remote_skips_login_until_authentication_is_requested(self, run):
|
||||
context = self.context()
|
||||
@@ -399,7 +404,7 @@ class upload_packages_test(unittest.TestCase):
|
||||
):
|
||||
status = modulus.cli.configure_conan_remote(context)
|
||||
self.assertEqual(status, 0)
|
||||
self.assertEqual(len(run.call_args_list), 1)
|
||||
self.assertEqual(len(run.call_args_list), 2)
|
||||
self.assertEqual(
|
||||
run.call_args_list[0].args[1],
|
||||
[
|
||||
@@ -744,7 +749,7 @@ class upload_packages_test(unittest.TestCase):
|
||||
@unittest.mock.patch.object(modulus.cli, "create_package", return_value=0)
|
||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||
@unittest.mock.patch.object(modulus.cli, "in_docker", return_value=True)
|
||||
def test_package_rejects_partial_conan_credentials_before_create(self, docker, run, create_package):
|
||||
def test_package_checks_credentials_only_at_upload(self, docker, run, create_package):
|
||||
context = self.context()
|
||||
with unittest.mock.patch.dict(
|
||||
os.environ,
|
||||
@@ -762,8 +767,8 @@ class upload_packages_test(unittest.TestCase):
|
||||
],
|
||||
)
|
||||
self.assertEqual(status, 1)
|
||||
create_package.assert_not_called()
|
||||
run.assert_not_called()
|
||||
create_package.assert_called_once()
|
||||
self.assertEqual(run.call_args_list[-1].args[1], ["conan", "remote", "logout", "origin"])
|
||||
docker.assert_called_once()
|
||||
@unittest.mock.patch.object(modulus.cli, "clear_conan_deps")
|
||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||
|
||||
Reference in New Issue
Block a user