@@ -1,20 +1,20 @@
|
||||
add_executable(modulus-test
|
||||
add_executable(modulus_test
|
||||
test.cpp
|
||||
)
|
||||
target_include_directories(modulus-test PRIVATE
|
||||
target_include_directories(modulus_test PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
)
|
||||
target_link_libraries(modulus-test PRIVATE
|
||||
target_link_libraries(modulus_test PRIVATE
|
||||
modulus::modulus
|
||||
)
|
||||
target_compile_features(modulus-test PRIVATE cxx_std_23)
|
||||
target_compile_definitions(modulus-test PRIVATE
|
||||
target_compile_features(modulus_test PRIVATE cxx_std_23)
|
||||
target_compile_definitions(modulus_test PRIVATE
|
||||
$<$<CONFIG:Debug>:MODULUS_EXPECT_DEBUG_ASSERTS>
|
||||
$<$<CONFIG:Release>:MODULUS_EXPECT_NDEBUG>
|
||||
$<$<CONFIG:RelWithDebInfo>:MODULUS_EXPECT_NDEBUG>
|
||||
$<$<CONFIG:MinSizeRel>:MODULUS_EXPECT_NDEBUG>
|
||||
)
|
||||
add_test(NAME modulus COMMAND modulus-test)
|
||||
add_test(NAME modulus COMMAND modulus_test)
|
||||
set_tests_properties(modulus PROPERTIES TIMEOUT 20)
|
||||
add_custom_target(check
|
||||
COMMAND ${CMAKE_CTEST_COMMAND}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import os
|
||||
import pathlib
|
||||
import tempfile
|
||||
import unittest
|
||||
import unittest.mock
|
||||
import urllib.error
|
||||
|
||||
import modulus.artifacts
|
||||
import modulus.cli
|
||||
import modulus.config
|
||||
|
||||
class ArtifactTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temporary = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.temporary.cleanup)
|
||||
self.root = pathlib.Path(self.temporary.name)
|
||||
config = modulus.config.normalize({'package': {'name': 'example', 'artifact': {'enabled': True, 'targets': ['arm_64_gcc12_release']}}})
|
||||
self.context = modulus.cli.build_context(self.root, config)
|
||||
self.directory = self.root / 'build/packages'
|
||||
self.directory.mkdir(parents=True)
|
||||
self.archive = self.directory / modulus.artifacts.archive_name('example', 'arm_64_gcc12_release')
|
||||
self.archive.write_bytes(b'package')
|
||||
self.checksum = modulus.artifacts.write_checksum(self.archive)
|
||||
self.enterContext(unittest.mock.patch.dict(os.environ, {'PACKAGE_REGISTRY_USER': 'test', 'PACKAGE_REGISTRY_TOKEN': 'secret'}))
|
||||
def test_discovery_uses_the_same_names_as_creation(self):
|
||||
self.assertEqual(self.archive.name, 'example_arm_64_gcc12_release.tar.gz')
|
||||
self.assertEqual(modulus.artifacts.discover_artifacts(self.directory, 'example', ['arm_64_gcc12_release']), [self.archive, self.checksum])
|
||||
def test_missing_or_invalid_checksums_never_touch_registry(self):
|
||||
with unittest.mock.patch.object(modulus.artifacts, 'request') as request:
|
||||
self.archive.write_bytes(b'corrupt')
|
||||
with self.assertRaisesRegex(SystemExit, 'checksum mismatch'):
|
||||
modulus.artifacts.main(self.context, [])
|
||||
self.checksum.unlink()
|
||||
with self.assertRaisesRegex(SystemExit, 'missing artifact'):
|
||||
modulus.artifacts.main(self.context, [])
|
||||
request.assert_not_called()
|
||||
def test_replacement_and_cleanup_are_ordered_after_validation(self):
|
||||
events = []
|
||||
with unittest.mock.patch.object(modulus.artifacts, 'list_versions', return_value=['latest', 'old']), \
|
||||
unittest.mock.patch.object(modulus.artifacts, 'delete_version', side_effect=lambda *args: events.append(('delete', args[-2]))), \
|
||||
unittest.mock.patch.object(modulus.artifacts, 'upload_file', side_effect=lambda *args: events.append(('upload', args[2].name))):
|
||||
self.assertEqual(modulus.artifacts.main(self.context, []), 0)
|
||||
self.assertEqual(events, [('delete', 'latest'), ('upload', self.archive.name), ('upload', self.checksum.name), ('delete', 'old')])
|
||||
def test_failed_upload_does_not_delete_other_slots(self):
|
||||
with unittest.mock.patch.object(modulus.artifacts, 'list_versions', return_value=['latest', 'old']), \
|
||||
unittest.mock.patch.object(modulus.artifacts, 'delete_version') as delete, \
|
||||
unittest.mock.patch.object(modulus.artifacts, 'upload_file', side_effect=SystemExit('upload failed')):
|
||||
with self.assertRaisesRegex(SystemExit, 'upload failed'):
|
||||
modulus.artifacts.main(self.context, [])
|
||||
self.assertEqual([call.args[-2] for call in delete.call_args_list], ['latest'])
|
||||
def test_request_errors_and_credentials_are_not_logged(self):
|
||||
for code in (401, 500):
|
||||
with unittest.mock.patch.object(modulus.artifacts.urllib.request, 'urlopen', side_effect=urllib.error.HTTPError('https://example.test', code, 'failed', {}, None)):
|
||||
with self.assertRaises(SystemExit) as caught:
|
||||
modulus.artifacts.request('PUT', 'https://example.test', 'secret')
|
||||
self.assertNotIn('secret', str(caught.exception))
|
||||
@@ -114,7 +114,7 @@ class clangd_test(unittest.TestCase):
|
||||
patch.object(subprocess, "Popen", return_value=process), contextlib.redirect_stderr(io.StringIO()):
|
||||
self.assertEqual(modulus.clangd.prepare(self.context), 1)
|
||||
self.assertFalse(cache.exists())
|
||||
self.assertEqual(list(cache.parent.glob(".headers-*")), [])
|
||||
self.assertEqual(list(cache.parent.glob(".headers_*")), [])
|
||||
def test_cleanup_always_waits_for_child_even_if_docker_removal_times_out(self):
|
||||
process = MagicMock()
|
||||
process.wait.side_effect = [0, subprocess.TimeoutExpired("docker", 5), 0]
|
||||
|
||||
@@ -138,7 +138,7 @@ class container_clangd_test(unittest.TestCase):
|
||||
session.close()
|
||||
def test_eof_and_termination_remove_container(self):
|
||||
def containers():
|
||||
return set(subprocess.check_output(["docker", "ps", "-q", "--filter", "name=modulus-clangd-"], text=True).split())
|
||||
return set(subprocess.check_output(["docker", "ps", "-q", "--filter", "name=modulus_clangd_"], text=True).split())
|
||||
before = containers()
|
||||
for terminate in (False, True):
|
||||
session = lsp_session(self.roots()[0])
|
||||
|
||||
+13
-54
@@ -58,7 +58,7 @@ class upload_packages_test(unittest.TestCase):
|
||||
"name": "sample",
|
||||
},
|
||||
}
|
||||
return modulus.cli.build_context(repo_root or pathlib.Path(os.getcwd()), config)
|
||||
return modulus.cli.build_context(repo_root or pathlib.Path(os.getcwd()), modulus.config.normalize(config))
|
||||
def test_cli_import_does_not_import_conan_recipe_module(self):
|
||||
repo = pathlib.Path(__file__).resolve().parents[1]
|
||||
command = [
|
||||
@@ -345,6 +345,17 @@ class upload_packages_test(unittest.TestCase):
|
||||
def test_conan_remote_url_uses_config_by_default(self):
|
||||
with unittest.mock.patch.dict(os.environ, {}, clear=True):
|
||||
self.assertEqual(modulus.cli.conan_remote_url(self.context()), "https://example.test/conan")
|
||||
@unittest.mock.patch.object(modulus.cli, "ensure_build_image", return_value=0)
|
||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||
@unittest.mock.patch.object(modulus.cli, "in_container", return_value=False)
|
||||
def test_publish_forwards_credentials_but_python_does_not(self, in_container, run, ensure_image):
|
||||
context = self.context()
|
||||
with unittest.mock.patch.dict(os.environ, {"PACKAGE_REGISTRY_TOKEN": "secret"}, clear=True):
|
||||
modulus.cli.docker_run(context, ["tools/publish.py"])
|
||||
self.assertIn("PACKAGE_REGISTRY_TOKEN", run.call_args.args[1])
|
||||
self.assertNotIn("secret", run.call_args.args[1])
|
||||
modulus.cli.docker_run(context, ["tools/python.py", "-m", "generator"])
|
||||
self.assertNotIn("PACKAGE_REGISTRY_TOKEN", run.call_args.args[1])
|
||||
def test_conan_remote_url_uses_config_in_ci(self):
|
||||
with unittest.mock.patch.dict(
|
||||
os.environ,
|
||||
@@ -905,7 +916,7 @@ class upload_packages_test(unittest.TestCase):
|
||||
with unittest.mock.patch.object(modulus.cli, "run", side_effect=fake_run) as run:
|
||||
status, artifacts = modulus.cli.create_artifact_package(context, context.default_target, update=False, no_remote=True)
|
||||
self.assertEqual(status, 0)
|
||||
archive = repo / "build" / "packages" / "sample-native.tar.gz"
|
||||
archive = repo / "build" / "packages" / "sample_native.tar.gz"
|
||||
checksum = archive.with_name(archive.name + ".sha256")
|
||||
self.assertEqual(artifacts, [archive, checksum])
|
||||
self.assertTrue(archive.is_file())
|
||||
@@ -922,58 +933,6 @@ class upload_packages_test(unittest.TestCase):
|
||||
cmake_command = run.call_args_list[1].args[1]
|
||||
self.assertIn("-DSAMPLE_WITH_WEB=ON", cmake_command)
|
||||
self.assertIn(f"-DCMAKE_INSTALL_PREFIX={repo / 'build' / 'build_common' / 'artifacts' / 'native' / 'stage' / 'usr'}", cmake_command)
|
||||
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
||||
@unittest.mock.patch.object(modulus.cli, "run_capture")
|
||||
@unittest.mock.patch.object(pathlib.Path, "is_file", return_value=True)
|
||||
def test_generic_artifact_upload_uses_package_registry_credentials(self, exists, capture, run):
|
||||
context = self.context()
|
||||
archive = pathlib.Path("build/packages/sample-native.tar.gz")
|
||||
with unittest.mock.patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"PACKAGE_REGISTRY_USER": "dev",
|
||||
"PACKAGE_REGISTRY_TOKEN": "secret",
|
||||
},
|
||||
clear=True,
|
||||
):
|
||||
for response in ("204", "404"):
|
||||
capture.return_value = subprocess.CompletedProcess([], 0, stdout=response, stderr="")
|
||||
self.assertEqual(modulus.cli.upload_generic_artifact(context, archive), 0)
|
||||
self.assertEqual(capture.call_args.args[1], ["curl", "--silent", "--show-error", "--user", "dev:secret", "--request", "DELETE", "--output", os.devnull, "--write-out", "%{http_code}", "https://example.test/api/packages/acme/generic/sample/latest/sample-native.tar.gz"])
|
||||
self.assertEqual(capture.call_args.kwargs["display_command"][4], "dev:<hidden>")
|
||||
self.assertEqual(
|
||||
run.call_args.args[1],
|
||||
[
|
||||
"curl",
|
||||
"--fail",
|
||||
"--silent",
|
||||
"--show-error",
|
||||
"--user",
|
||||
"dev:secret",
|
||||
"--upload-file",
|
||||
archive,
|
||||
"https://example.test/api/packages/acme/generic/sample/latest/sample-native.tar.gz",
|
||||
],
|
||||
)
|
||||
self.assertEqual(
|
||||
run.call_args.kwargs["display_command"][5],
|
||||
"dev:<hidden>",
|
||||
)
|
||||
@unittest.mock.patch.object(modulus.cli, "package_registry_credentials", return_value=("dev", "secret"))
|
||||
@unittest.mock.patch.object(pathlib.Path, "is_file", return_value=True)
|
||||
@unittest.mock.patch.object(modulus.cli, "run_capture")
|
||||
@unittest.mock.patch.object(modulus.cli, "run")
|
||||
def test_artifact_upload_stops_if_replacement_fails(self, run, capture, exists, credentials):
|
||||
for status, response in ((0, "401"), (0, "500"), (7, "000")):
|
||||
with self.subTest(status=status, response=response):
|
||||
capture.return_value = subprocess.CompletedProcess([], status, stdout=response, stderr="")
|
||||
self.assertNotEqual(modulus.cli.upload_generic_artifact(self.context(), pathlib.Path("sample-native.tar.gz")), 0)
|
||||
run.assert_not_called()
|
||||
@unittest.mock.patch.object(modulus.cli, "run_capture")
|
||||
def test_missing_artifact_does_not_touch_registry(self, capture):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
self.assertEqual(modulus.cli.upload_generic_artifact(self.context(), pathlib.Path(temporary) / "missing.tar.gz"), 1)
|
||||
capture.assert_not_called()
|
||||
@unittest.mock.patch.object(modulus.cli, "in_docker", return_value=True)
|
||||
@unittest.mock.patch.object(modulus.cli, "create_artifact_package", return_value=(0, []))
|
||||
def test_artifact_command_needs_no_release_argument(self, create, docker):
|
||||
|
||||
@@ -47,11 +47,11 @@ class modulus_export_test(unittest.TestCase):
|
||||
(package / "tools" / "lsp.py").write_text("#!/usr/bin/env python3\n", encoding="utf-8")
|
||||
(package / "tools" / "run.py").write_text("#!/usr/bin/env python3\n", encoding="utf-8")
|
||||
(package / "tools" / "format.py").write_text("#!/usr/bin/env python3\n", encoding="utf-8")
|
||||
(package / "tools" / "docker" / "format-requirements.txt").write_text("autopep8==2.3.2\n", encoding="utf-8")
|
||||
(package / "tools" / "docker" / "format_requirements.txt").write_text("autopep8==2.3.2\n", encoding="utf-8")
|
||||
(package / "tools" / "docker" / "Dockerfile").write_text("FROM scratch\n", encoding="utf-8")
|
||||
(package / "include" / "modulus" / "types.hpp").write_text("", encoding="utf-8")
|
||||
(package / "tests" / "test.cpp").write_text("", encoding="utf-8")
|
||||
(package / "lib" / "cmake" / "modulus" / "modulus-config.cmake").write_text("", encoding="utf-8")
|
||||
(package / "lib" / "cmake" / "modulus" / "modulusConfig.cmake").write_text("", encoding="utf-8")
|
||||
(package / "share" / "modulus" / "duplicate.py").write_text("", encoding="utf-8")
|
||||
(package / "bin" / "modulus-tool").write_text("", encoding="utf-8")
|
||||
def test_boost_requirement_options_enable_configured_components(self):
|
||||
@@ -205,8 +205,8 @@ class modulus_export_test(unittest.TestCase):
|
||||
self.assertEqual(config["tooling"]["targets"]["x86_64_gcc12_release"]["profile"], "conan/profiles/x86_64_gcc12_release")
|
||||
def test_docker_defaults_use_shared_modulus_marker(self):
|
||||
config = modulus.config.normalize(self.config())
|
||||
self.assertEqual(config["tooling"]["docker"]["image"], "modulus-build:dev")
|
||||
self.assertEqual(config["tooling"]["docker"]["marker"], "/opt/modulus-build-image")
|
||||
self.assertEqual(config["tooling"]["docker"]["image"], "modulus_build:dev")
|
||||
self.assertEqual(config["tooling"]["docker"]["marker"], "/opt/modulus_build_image")
|
||||
self.assertNotIn("apt_architectures", config["tooling"]["docker"])
|
||||
self.assertNotIn("apt_packages", config["tooling"]["docker"])
|
||||
def test_export_project_copies_runtime_and_exported_tools(self):
|
||||
@@ -233,7 +233,7 @@ class modulus_export_test(unittest.TestCase):
|
||||
self.assertTrue((runtime / "modulus" / "formatting.py").is_file())
|
||||
self.assertTrue((runtime / "modulus" / "format_layout.py").is_file())
|
||||
self.assertTrue((repo / "build/build_tools/format.py").is_file())
|
||||
self.assertTrue((repo / "build/build_tools/docker/format-requirements.txt").is_file())
|
||||
self.assertTrue((repo / "build/build_tools/docker/format_requirements.txt").is_file())
|
||||
self.assertFalse((runtime / "CMakeLists.txt").exists())
|
||||
self.assertFalse((runtime / "README.md").exists())
|
||||
self.assertFalse((runtime / "conanfile.py").exists())
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import pathlib
|
||||
import tempfile
|
||||
import unittest
|
||||
import unittest.mock
|
||||
|
||||
import modulus.filesystem
|
||||
|
||||
class PublicationTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temporary = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.temporary.cleanup)
|
||||
self.root = pathlib.Path(self.temporary.name)
|
||||
self.source, self.target = self.root / 'new', self.root / 'installed'
|
||||
for path, text in ((self.source, 'new'), (self.target, 'old')):
|
||||
path.mkdir()
|
||||
(path / 'content').write_text(text)
|
||||
def test_publish_replaces_complete_directory(self):
|
||||
with modulus.filesystem.publish_directories([(self.source, self.target)]):
|
||||
self.assertEqual((self.target / 'content').read_text(), 'new')
|
||||
self.assertFalse(self.source.exists())
|
||||
self.assertEqual(list(self.root.iterdir()), [self.target])
|
||||
def test_failure_restores_previous_directory(self):
|
||||
with self.assertRaisesRegex(RuntimeError, 'failed'):
|
||||
with modulus.filesystem.publish_directories([(self.source, self.target)]):
|
||||
raise RuntimeError('failed')
|
||||
self.assertEqual((self.target / 'content').read_text(), 'old')
|
||||
self.assertFalse(list(self.root.glob('.publish_*')))
|
||||
def test_failed_second_replacement_rolls_back_first(self):
|
||||
second = self.root / 'second'
|
||||
second.mkdir()
|
||||
rename = pathlib.Path.rename
|
||||
def fail(path, destination):
|
||||
if path == second:
|
||||
raise OSError('failed')
|
||||
return rename(path, destination)
|
||||
with unittest.mock.patch.object(pathlib.Path, 'rename', fail), self.assertRaises(OSError):
|
||||
with modulus.filesystem.publish_directories([(self.source, self.target), (second, self.root / 'last')]):
|
||||
pass
|
||||
self.assertEqual((self.target / 'content').read_text(), 'old')
|
||||
def test_overlaps_and_symlinks_rejected_before_replacement(self):
|
||||
link = self.root / 'link'
|
||||
link.symlink_to(self.target, target_is_directory=True)
|
||||
for target in (self.source, self.source / 'child', self.source / '..' / 'new', self.root, link, link / 'child'):
|
||||
with self.subTest(target=target), self.assertRaises(ValueError):
|
||||
with modulus.filesystem.publish_directories([(self.source, target)]):
|
||||
pass
|
||||
self.assertEqual((self.target / 'content').read_text(), 'old')
|
||||
def test_failed_rollback_retains_the_previous_directory_for_recovery(self):
|
||||
rename = pathlib.Path.rename
|
||||
def fail(path, destination):
|
||||
if path.name == 'previous':
|
||||
raise OSError('restore failed')
|
||||
return rename(path, destination)
|
||||
with unittest.mock.patch.object(pathlib.Path, 'rename', fail), self.assertRaisesRegex(RuntimeError, 'recovery directories retained'):
|
||||
with modulus.filesystem.publish_directories([(self.source, self.target)]):
|
||||
raise RuntimeError('publication failed')
|
||||
backup = next(self.root.glob('.publish_*/previous/content'))
|
||||
self.assertEqual(backup.read_text(), 'old')
|
||||
@@ -0,0 +1,59 @@
|
||||
import pathlib
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
import unittest.mock
|
||||
|
||||
import modulus.cli
|
||||
import modulus.config
|
||||
import modulus.python
|
||||
|
||||
class PythonTests(unittest.TestCase):
|
||||
def context(self, root, groups):
|
||||
return modulus.cli.build_context(root, modulus.config.normalize({'package': {'name': 'example'}, 'tooling': {'python': {'groups': groups}}}))
|
||||
def test_optional_packages_are_not_needed_by_default(self):
|
||||
with tempfile.TemporaryDirectory() as temporary, unittest.mock.patch.object(subprocess, 'run') as run:
|
||||
context = self.context(pathlib.Path(temporary), [])
|
||||
self.assertEqual(modulus.python.interpreter(context, install=True), pathlib.Path(sys.executable))
|
||||
run.assert_not_called()
|
||||
def test_conan_reuses_the_project_environment_and_preserves_offline_mode(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
context = self.context(pathlib.Path(temporary), ['assets'])
|
||||
environment = modulus.cli.subprocess_environment(context, ['conan', 'install', '.', '--no-remote'])
|
||||
self.assertEqual(environment['MODULUS_PYTHON_COMMON_DIR'], str(context.common_dir))
|
||||
self.assertEqual(environment['MODULUS_PYTHON_OFFLINE'], '1')
|
||||
environment = modulus.cli.subprocess_environment(context, ['conan', 'install', '.'])
|
||||
self.assertEqual(environment['MODULUS_PYTHON_OFFLINE'], '0')
|
||||
def test_missing_dependencies_require_configure_and_never_download_during_build(self):
|
||||
with tempfile.TemporaryDirectory() as temporary, unittest.mock.patch.object(subprocess, 'run') as run:
|
||||
context = self.context(pathlib.Path(temporary), ['assets'])
|
||||
for options in ({}, {'install': True, 'offline': True}):
|
||||
with self.assertRaisesRegex(RuntimeError, 'configure'):
|
||||
modulus.python.interpreter(context, **options)
|
||||
run.assert_not_called()
|
||||
def test_unknown_or_duplicate_groups_are_rejected(self):
|
||||
for groups in (['unknown'], ['assets', 'assets'], 'assets'):
|
||||
with self.assertRaises(modulus.config.config_error):
|
||||
self.context(pathlib.Path('/tmp'), groups)
|
||||
def test_matching_environment_is_reused_and_changed_pins_require_configure(self):
|
||||
with tempfile.TemporaryDirectory() as temporary, unittest.mock.patch.object(subprocess, 'run') as run:
|
||||
context = self.context(pathlib.Path(temporary), ['assets'])
|
||||
executable = context.common_dir / 'python/bin/python3'
|
||||
executable.parent.mkdir(parents=True)
|
||||
executable.touch()
|
||||
marker = executable.parents[1] / 'requirements.sha256'
|
||||
marker.write_text(modulus.python.identity(modulus.python.requirement_files(context)))
|
||||
self.assertEqual(modulus.python.interpreter(context, offline=True), executable)
|
||||
with unittest.mock.patch.object(modulus.python, 'identity', return_value='changed'):
|
||||
with self.assertRaisesRegex(RuntimeError, 'configure'):
|
||||
modulus.python.interpreter(context)
|
||||
run.assert_not_called()
|
||||
def test_runner_imports_exported_package_even_with_bootstrap_in_working_directory(self):
|
||||
source = pathlib.Path(__file__).resolve().parents[1]
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = pathlib.Path(temporary)
|
||||
(root / 'modulus.py').write_text("raise AssertionError('bootstrap shadowed package')")
|
||||
result = subprocess.run([sys.executable, str(source / 'tools/python_runner.py'), str(source), '-c', 'import modulus.filesystem; print("loaded")'], cwd=root, text=True, capture_output=True)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(result.stdout.strip(), 'loaded')
|
||||
Reference in New Issue
Block a user