import modulus.process import modulus.build import modulus.build_context import modulus.conan import modulus.container import modulus.export import modulus.lsp import modulus.packaging import modulus.run import json import os import pathlib import subprocess import sys import tarfile import tempfile import unittest import unittest.mock import modulus.cli import modulus.config class upload_packages_test(unittest.TestCase): def context(self, repo_root=None, package_name="sample"): config = { "package": { "name": package_name, "artifact": { "enabled": False, "targets": [], "install_prefix": "", "cmake_definitions": {}, "conan_options": {}, "registry_url": "https://example.test/api/packages/acme/generic", "upload_env": "SAMPLE_PACKAGE_UPLOAD", }, }, "tooling": { "default_target": "native", "targets": { "native": { "profile": "build/build_tools/conan/profiles/native", "build_dir": "build/build_native", "can_run": True, "build_type": "Release", }, }, "docker": { "image": "sample-build:dev", "marker": "/opt/sample-build-image", }, "upload_env": "SAMPLE_CONAN_UPLOAD", "remote": { "name": "origin", "url": "https://example.test/conan", }, "run": { "enabled": False, "target": "native", "executable": "sample", "env": {}, "args": [], "ports": [], }, }, "tests": { "name": "sample", }, } return modulus.build_context.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 = [ sys.executable, "-c", "import sys; import modulus.cli; raise SystemExit(1 if 'modulus.recipe' in sys.modules else 0)", ] completed = subprocess.run(command, cwd=repo, check=False) self.assertEqual(completed.returncode, 0) def test_build_context_does_not_import_command_or_execution_modules(self): repo = pathlib.Path(__file__).resolve().parents[1] command = [sys.executable, "-c", "import sys; import modulus.build_context; forbidden = {'modulus.cli', 'modulus.build', 'modulus.lsp', 'modulus.container', 'modulus.packaging', 'modulus.process'}; raise SystemExit(bool(forbidden & sys.modules.keys()))"] self.assertEqual(subprocess.run(command, cwd=repo, check=False).returncode, 0) def test_project_root_build_is_rejected(self): with tempfile.TemporaryDirectory() as directory: root = pathlib.Path(directory) context = self.context(root) context.config["tooling"]["targets"]["native"]["build_dir"] = "." with self.assertRaisesRegex(modulus.config.config_error, "project and source roots"): modulus.build_context.build_context(root, context.config) def test_configure_and_lsp_refresh_project_editor_settings(self): with tempfile.TemporaryDirectory() as directory: root = pathlib.Path(directory) fragments = [ {"CompileFlags": {"CompilationDatabase": "build/native"}}, {"If": {"PathMatch": "src/pico/.*"}, "CompileFlags": {"CompilationDatabase": "build/pico"}}, ] config = {"package": {"name": "sample"}, "tooling": {"editor": {"clangd": fragments}}} (root / "modulus.json").write_text(json.dumps(config), encoding="utf-8") with unittest.mock.patch.object(modulus.build_context, "find_repo_root", return_value=root): for command in ("configure", "lsp"): with self.subTest(command=command), unittest.mock.patch.object({'configure': modulus.build, 'lsp': modulus.lsp}[command], command, return_value=0): (root / ".clangd").write_text("stale", encoding="utf-8") self.assertEqual(modulus.cli.main(command), 0) content = (root / ".clangd").read_text(encoding="utf-8").split("\n", 1)[1] self.assertEqual([json.loads(part) for part in content.split("\n---\n")], fragments) before = {name: (root / name).stat().st_mtime_ns for name in modulus.export.editor_files.values()} self.assertEqual(modulus.cli.main(command), 0) self.assertEqual(before, {name: (root / name).stat().st_mtime_ns for name in before}) def test_lsp_rejects_source_root_before_cache_cleanup(self): with tempfile.TemporaryDirectory() as directory: root = pathlib.Path(directory) context = self.context(root) cache = root / "CMakeCache.txt" cache.write_text("keep", encoding="utf-8") with unittest.mock.patch.object(modulus.lsp, "clear_lsp_cmake_cache") as clear: for build_dir in (".", "alias", "subproject"): if build_dir == "alias": (root / "alias").symlink_to(root, target_is_directory=True) with self.subTest(build_dir=build_dir), self.assertRaises(modulus.config.config_error): modulus.lsp.configure_lsp_database(context, {"source_dir": "subproject", "build_dir": build_dir}) clear.assert_not_called() self.assertEqual(cache.read_text(encoding="utf-8"), "keep") @unittest.mock.patch.object(modulus.packaging, "upload_all_packages", return_value=37) @unittest.mock.patch.object(modulus.conan, "configure_conan_remote", return_value=0) @unittest.mock.patch.object(modulus.process, "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.packaging.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.packaging, "upload_all_packages") @unittest.mock.patch.object(modulus.conan, "configure_conan_remote") def test_replace_requires_guard_env(self, configure, upload): with unittest.mock.patch.dict(os.environ, {}, clear=True): status = modulus.packaging.upload_packages(self.context(), replace=True) self.assertEqual(status, 1) configure.assert_not_called() upload.assert_not_called() @unittest.mock.patch.object(modulus.packaging, "upload_all_packages", return_value=0) @unittest.mock.patch.object(modulus.conan, "configure_conan_remote", return_value=0) @unittest.mock.patch.object(modulus.process, "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, { "MODULUS_ALLOW_PACKAGE_REPLACE": "1", }, clear=True, ): status = modulus.packaging.upload_packages(context, replace=True) self.assertEqual(status, 0) configure.assert_called_once_with(context, authenticate=True) upload.assert_called_once_with(context, "origin", force=True) @unittest.mock.patch.object(modulus.process, "run", return_value=0) def test_upload_defaults_to_latest_reference(self, run): with unittest.mock.patch.dict(os.environ, {}, clear=True): status = modulus.packaging.upload_all_packages(self.context(), "origin") self.assertEqual(status, 0) self.assertIn("sample/latest:*", run.call_args.args[1]) self.assertNotIn("--force", run.call_args.args[1]) @unittest.mock.patch.object(modulus.process, "run", return_value=0) def test_replace_upload_uses_conan_force(self, run): status = modulus.packaging.upload_all_packages(self.context(), "origin", force=True) self.assertEqual(status, 0) self.assertIn("--force", run.call_args.args[1]) def test_checked_in_ci_overwrites_latest_on_master(self): workflow = (pathlib.Path(__file__).resolve().parents[1] / ".gitea" / "workflows" / "ci.yml").read_text(encoding="utf-8") self.assertIn("gitea.ref == 'refs/heads/master'", workflow) self.assertIn("workflow_dispatch", workflow) self.assertIn("MODULUS_ALLOW_PACKAGE_REPLACE", workflow) self.assertIn("--replace", workflow) self.assertNotIn("--seed-dependencies", workflow) self.assertNotIn("--no-remote", workflow) self.assertNotIn("CONAN_PACKAGE_API_URL", workflow) self.assertNotIn("VERSION_TAG", workflow) self.assertIn("CONAN_HOME: build/build_common/conan_cache", workflow) self.assertNotIn("./tools/bench.py", workflow) self.assertNotIn("--examples", workflow) self.assertNotIn("--volume \"${PWD}:${PWD}\"", workflow) self.assertNotIn("CONAN_PUBLIC_REMOTE_URL", workflow) self.assertNotIn("docs.py", workflow) def test_removed_public_commands_are_rejected(self): with tempfile.TemporaryDirectory() as repo_dir: repo = pathlib.Path(repo_dir) (repo / "modulus.json").write_text( '{"package":{"name":"sample"}}', encoding="utf-8", ) previous_cwd = pathlib.Path.cwd() os.chdir(repo) try: for command in ("image", "docs", "shell", "docker_support"): with self.assertRaisesRegex(SystemExit, "unknown modulus command"): modulus.cli.main(command) finally: os.chdir(previous_cwd) def test_docker_passthrough_includes_replace_env(self): passthrough = modulus.container.docker_passthrough_env(self.context()) self.assertIn("MODULUS_ALLOW_PACKAGE_REPLACE", passthrough) for name in ("CONAN_USER", "CONAN_TOKEN", "PACKAGE_REGISTRY_USER", "PACKAGE_REGISTRY_TOKEN"): self.assertNotIn(name, passthrough) self.assertIn(name, modulus.container.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) self.assertNotIn("GITHUB_TOKEN", passthrough) self.assertNotIn("GITEA_TOKEN", passthrough) def test_run_ports_default_to_empty_list(self): config = modulus.config.normalize({ "package": { "name": "sample", }, }) self.assertEqual(config["tooling"]["run"]["ports"], []) def test_run_accepts_docker_publish_mapping(self): config = modulus.config.normalize({ "package": { "name": "sample", }, "tooling": { "run": { "ports": ["127.0.0.1:8000:8000/tcp"], }, }, }) self.assertEqual(config["tooling"]["run"]["ports"], ["127.0.0.1:8000:8000/tcp"]) def test_run_rejects_invalid_docker_publish_mapping(self): with self.assertRaisesRegex(modulus.config.config_error, "tooling.run.ports\\[0\\]"): modulus.config.normalize({ "package": { "name": "sample", }, "tooling": { "run": { "ports": ["127.0.0.1:8000:8000/tcp --privileged"], }, }, }) @unittest.mock.patch.object(modulus.process, "in_docker", return_value=True) def test_run_is_disabled_by_default(self, docker): status = modulus.run.product_run(self.context(), []) self.assertEqual(status, 1) docker.assert_not_called() @unittest.mock.patch.object(modulus.process, "run", return_value=0) @unittest.mock.patch.object(modulus.process, "in_docker", return_value=True) def test_run_executes_configured_product_executable(self, docker, run): with tempfile.TemporaryDirectory() as repo_dir: repo = pathlib.Path(repo_dir) context = self.context(repo_root=repo) context.run_settings.update({ "enabled": True, "target": "native", "executable": "sample-app", "env": { "SAMPLE_WEB_ROOT": "build/web", }, "args": ["--serve"], }) context.default_target.build_dir.mkdir(parents=True) executable = context.default_target.build_dir / "sample-app" executable.write_text("#!/bin/sh\n", encoding="utf-8") status = modulus.run.product_run( context, [ "--", "--port", "8000", ], ) self.assertEqual(status, 0) self.assertEqual( run.call_args.args[1], [ executable, "--serve", "--port", "8000", ], ) self.assertEqual(run.call_args.kwargs["env_overrides"], {"SAMPLE_WEB_ROOT": "build/web"}) docker.assert_called_once() @unittest.mock.patch.object(modulus.container, "docker_run", return_value=0) @unittest.mock.patch.object(modulus.process, "in_docker", return_value=False) def test_run_publishes_configured_ports_when_wrapped_by_docker(self, docker, docker_run): context = self.context() context.run_settings.update({ "enabled": True, "ports": ["127.0.0.1:8000:8000/tcp"], }) status = modulus.run.product_run(context, ["--target", "native"]) self.assertEqual(status, 0) docker_run.assert_called_once_with( context, [ f"./{context.tools_rel}/run.py", "--target", "native", ], docker_args=[ "--publish", "127.0.0.1:8000:8000/tcp", ], ) docker.assert_called_once() @unittest.mock.patch.object(modulus.process, "run", return_value=0) def test_build_image_honors_docker_platform_env(self, run): context = self.context() with unittest.mock.patch.dict( os.environ, { "MODULUS_DOCKER_PLATFORM": "linux/arm64", }, clear=True, ): status = modulus.container.build_image(context) self.assertEqual(status, 0) self.assertEqual( run.call_args.args[1], [ "docker", "build", "--platform", "linux/arm64", "--file", context.dockerfile, "--tag", "sample-build:dev", "--label", f"{modulus.cli.modulus.container.image_label}={modulus.cli.modulus.container.recipe_digest(context)}", "--build-arg", "MODULUS_TOOLS=tools", ".", ], ) @unittest.mock.patch.object(modulus.container, "ensure_build_image", return_value=0) @unittest.mock.patch.object(modulus.process, "run", return_value=0) @unittest.mock.patch.object(modulus.process, "in_container", return_value=False) def test_docker_run_honors_docker_platform_env(self, in_container, run, ensure_image): context = self.context() with unittest.mock.patch.dict( os.environ, { "MODULUS_DOCKER_PLATFORM": "linux/arm64", }, clear=True, ): status = modulus.container.docker_run(context, ["true"]) self.assertEqual(status, 0) self.assertIn("--platform", run.call_args.args[1]) self.assertIn("linux/arm64", run.call_args.args[1]) ensure_image.assert_called_once_with(context) in_container.assert_called_once() def test_conan_remote_url_uses_config_by_default(self): with unittest.mock.patch.dict(os.environ, {}, clear=True): self.assertEqual(modulus.conan.conan_remote_url(self.context()), "https://example.test/conan") @unittest.mock.patch.object(modulus.container, "ensure_build_image", return_value=0) @unittest.mock.patch.object(modulus.process, "run", return_value=0) @unittest.mock.patch.object(modulus.process, "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.container.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.container.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, { "CI": "true", }, clear=True, ): self.assertEqual(modulus.conan.conan_remote_url(self.context()), "https://example.test/conan") def test_conan_credentials_only_uses_conan_environment(self): with unittest.mock.patch.dict( os.environ, { "CONAN_USER": "dev", "CONAN_TOKEN": "secret", "GITEA_TOKEN": "ignored", }, clear=True, ): user, token = modulus.conan.conan_credentials() self.assertEqual(user, "dev") self.assertEqual(token, "secret") @unittest.mock.patch.object(modulus.process, "run", return_value=0) def test_configure_remote_skips_login_without_conan_credentials(self, run): with unittest.mock.patch.dict(os.environ, {}, clear=True): status = modulus.conan.configure_conan_remote(self.context()) self.assertEqual(status, 0) 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], [ "conan", "remote", "add", "-f", "origin", "https://example.test/conan", ], ) @unittest.mock.patch.object(modulus.process, "run", return_value=0) def test_configure_remote_ignores_partial_conan_credentials(self, run): with unittest.mock.patch.dict(os.environ, {"CONAN_USER": "dev"}, clear=True): status = modulus.conan.configure_conan_remote(self.context()) self.assertEqual(status, 0) self.assertEqual(run.call_args_list[-1].args[1], ["conan", "remote", "logout", "origin"]) @unittest.mock.patch.object(modulus.process, "run", return_value=0) def test_configure_remote_skips_login_until_authentication_is_requested(self, run): context = self.context() with unittest.mock.patch.dict( os.environ, { "CONAN_USER": "dev", "CONAN_TOKEN": "secret", }, clear=True, ): status = modulus.conan.configure_conan_remote(context) self.assertEqual(status, 0) self.assertEqual(len(run.call_args_list), 2) self.assertEqual( run.call_args_list[0].args[1], [ "conan", "remote", "add", "-f", "origin", "https://example.test/conan", ], ) @unittest.mock.patch.object(modulus.process, "run", return_value=0) def test_configure_remote_logs_in_with_hidden_token_when_authenticating(self, run): context = self.context() with unittest.mock.patch.dict( os.environ, { "CONAN_USER": "dev", "CONAN_TOKEN": "secret", }, clear=True, ): status = modulus.conan.configure_conan_remote(context, authenticate=True) self.assertEqual(status, 0) self.assertEqual( run.call_args_list[0].args[1], [ "conan", "remote", "add", "-f", "origin", "https://example.test/conan", ], ) self.assertEqual( run.call_args_list[1].args[1], [ "conan", "remote", "login", "origin", "dev", "-p", "secret", ], ) self.assertEqual( run.call_args_list[1].kwargs["display_command"], [ "conan", "remote", "login", "origin", "dev", "-p", "", ], ) def multi_target_context(self): config = { "package": { "name": "sample", "artifact": { "enabled": False, "targets": [], "install_prefix": "", "cmake_definitions": {}, "conan_options": {}, "registry_url": "https://example.test/api/packages/acme/generic", "upload_env": "SAMPLE_PACKAGE_UPLOAD", }, }, "tooling": { "default_target": "debug", "targets": { "debug": { "profile": "build/build_tools/conan/profiles/debug", "build_dir": "build/build_debug", "can_run": True, "build_type": "Debug", }, "release": { "profile": "build/build_tools/conan/profiles/release", "build_dir": "build/build_release", "can_run": True, "build_type": "Release", }, }, "docker": { "image": "sample-build:dev", "marker": "/opt/sample-build-image", }, "upload_env": "SAMPLE_CONAN_UPLOAD", "remote": { "name": "origin", "url": "https://example.test/conan", }, "run": { "enabled": False, "target": "debug", "executable": "sample", "env": {}, "args": [], }, }, "tests": { "name": "sample", }, } return modulus.build_context.build_context(pathlib.Path(os.getcwd()), config) def test_selected_targets_default_to_default_target_only(self): context = self.multi_target_context() selected = context.selected_targets( [], False, ) self.assertEqual([target.name for target in selected], ["debug"]) def test_selected_targets_all_returns_every_target(self): context = self.multi_target_context() selected = context.selected_targets( [], True, ) self.assertEqual([target.name for target in selected], ["debug", "release"]) @unittest.mock.patch.object(modulus.process, "run", return_value=0) @unittest.mock.patch.object(modulus.process, "in_docker", return_value=True) def test_build_can_target_one_cmake_target(self, docker, run): directory = tempfile.TemporaryDirectory() self.addCleanup(directory.cleanup) context = self.context(pathlib.Path(directory.name)) context.default_target.build_dir.mkdir(parents=True, exist_ok=True) (context.default_target.build_dir / "CMakeCache.txt").write_text("", encoding="utf-8") (context.default_target.build_dir / "build.ninja").write_text("", encoding="utf-8") status = modulus.build.build( context, [ "--target", "native", "--cmake-target", "sample-example", ], ) self.assertEqual(status, 0) self.assertEqual( run.call_args.args[1], [ "cmake", "--build", context.default_target.build_dir, "--target", "sample-example", "--parallel", "2", ], ) docker.assert_called_once() @unittest.mock.patch.object(modulus.process, "run", return_value=0) @unittest.mock.patch.object(modulus.build, "configure_one", return_value=0) @unittest.mock.patch.object(modulus.conan, "configure_conan_remote", return_value=0) @unittest.mock.patch.object(modulus.process, "in_docker", return_value=True) def test_build_auto_configures_missing_target(self, docker, configure_remote, configure_one, run): with tempfile.TemporaryDirectory() as repo_dir: context = self.context(repo_root=pathlib.Path(repo_dir)) status = modulus.build.build(context, []) self.assertEqual(status, 0) configure_remote.assert_called_once_with(context) configure_one.assert_called_once_with(context, context.default_target, update=False) self.assertEqual( run.call_args.args[1], [ "cmake", "--build", context.default_target.build_dir, "--parallel", "2", ], ) docker.assert_called_once() @unittest.mock.patch.object(modulus.process, "run", return_value=0) @unittest.mock.patch.object(modulus.build, "configure_one", return_value=0) @unittest.mock.patch.object(modulus.conan, "configure_conan_remote", return_value=0) @unittest.mock.patch.object(modulus.process, "in_docker", return_value=True) def test_build_skips_configure_when_target_is_configured(self, docker, configure_remote, configure_one, run): with tempfile.TemporaryDirectory() as repo_dir: context = self.context(repo_root=pathlib.Path(repo_dir)) context.default_target.build_dir.mkdir(parents=True) (context.default_target.build_dir / "CMakeCache.txt").write_text("", encoding="utf-8") (context.default_target.build_dir / "build.ninja").write_text("", encoding="utf-8") status = modulus.build.build(context, []) self.assertEqual(status, 0) configure_remote.assert_not_called() configure_one.assert_not_called() self.assertEqual( run.call_args.args[1], [ "cmake", "--build", context.default_target.build_dir, "--parallel", "2", ], ) docker.assert_called_once() @unittest.mock.patch.object(modulus.process, "run", return_value=0) @unittest.mock.patch.object(modulus.build, "configure_one", return_value=0) @unittest.mock.patch.object(modulus.conan, "configure_conan_remote", return_value=0) @unittest.mock.patch.object(modulus.process, "in_docker", return_value=True) def test_build_reconfigures_when_target_cmake_definition_differs(self, docker, configure_remote, configure_one, run): with tempfile.TemporaryDirectory() as repo_dir: context = self.context(repo_root=pathlib.Path(repo_dir)) context.default_target.cmake_definitions = { "SAMPLE_WITH_WEB": True, } context.default_target.build_dir.mkdir(parents=True) (context.default_target.build_dir / "CMakeCache.txt").write_text("SAMPLE_WITH_WEB:BOOL=OFF\n", encoding="utf-8") (context.default_target.build_dir / "build.ninja").write_text("", encoding="utf-8") status = modulus.build.build(context, []) self.assertEqual(status, 0) configure_remote.assert_called_once_with(context) configure_one.assert_called_once_with(context, context.default_target, update=False) self.assertEqual( run.call_args.args[1], [ "cmake", "--build", context.default_target.build_dir, "--parallel", "2", ], ) docker.assert_called_once() @unittest.mock.patch.object(modulus.process, "run", return_value=0) @unittest.mock.patch.object(modulus.build, "configure_one", return_value=37) @unittest.mock.patch.object(modulus.conan, "configure_conan_remote", return_value=0) @unittest.mock.patch.object(modulus.process, "in_docker", return_value=True) def test_build_stops_when_auto_configure_fails(self, docker, configure_remote, configure_one, run): with tempfile.TemporaryDirectory() as repo_dir: context = self.context(repo_root=pathlib.Path(repo_dir)) status = modulus.build.build(context, []) self.assertEqual(status, 37) configure_remote.assert_called_once_with(context) configure_one.assert_called_once_with(context, context.default_target, update=False) run.assert_not_called() docker.assert_called_once() @unittest.mock.patch.object(modulus.build, "configure_one", return_value=0) @unittest.mock.patch.object(modulus.conan, "configure_conan_remote", return_value=0) @unittest.mock.patch.object(modulus.process, "in_docker", return_value=True) def test_configure_no_remote_skips_remote_setup(self, docker, configure_remote, configure_one): with tempfile.TemporaryDirectory() as repo_dir: context = self.context(repo_root=pathlib.Path(repo_dir)) status = modulus.build.configure( context, [ "--target", "native", "--no-remote", ], ) self.assertEqual(status, 0) configure_remote.assert_not_called() configure_one.assert_called_once_with(context, context.default_target, False, no_remote=True) docker.assert_called_once() @unittest.mock.patch.object(modulus.packaging, "upload_packages", return_value=0) @unittest.mock.patch.object(modulus.packaging, "create_package", return_value=0) @unittest.mock.patch.object(modulus.conan, "configure_conan_remote", return_value=0) @unittest.mock.patch.object(modulus.process, "in_docker", return_value=True) def test_package_no_remote_skips_remote_setup_and_upload(self, docker, configure_remote, create_package, upload): context = self.context() with unittest.mock.patch.dict( os.environ, { "SAMPLE_CONAN_UPLOAD": "1", }, clear=True, ): status = modulus.packaging.package( context, [ "--target", "native", "--no-remote", ], ) self.assertEqual(status, 0) configure_remote.assert_not_called() create_package.assert_called_once_with(context, context.default_target, False, no_remote=True) upload.assert_not_called() docker.assert_called_once() @unittest.mock.patch.object(modulus.packaging, "upload_packages", return_value=0) @unittest.mock.patch.object(modulus.packaging, "create_package", return_value=0) @unittest.mock.patch.object(modulus.conan, "configure_conan_remote", return_value=0) @unittest.mock.patch.object(modulus.process, "in_docker", return_value=True) def test_package_configures_remote_once_before_upload(self, docker, configure_remote, create_package, upload): context = self.context() with unittest.mock.patch.dict( os.environ, { "CONAN_USER": "dev", "CONAN_TOKEN": "secret", "SAMPLE_CONAN_UPLOAD": "1", }, clear=True, ): status = modulus.packaging.package( context, [ "--target", "native", ], ) self.assertEqual(status, 0) configure_remote.assert_called_once_with(context) create_package.assert_called_once_with(context, context.default_target, False, no_remote=False) upload.assert_called_once_with(context, replace=False) docker.assert_called_once() @unittest.mock.patch.object(modulus.packaging, "upload_packages", return_value=1) @unittest.mock.patch.object(modulus.packaging, "create_package", return_value=0) @unittest.mock.patch.object(modulus.conan, "configure_conan_remote", return_value=0) @unittest.mock.patch.object(modulus.process, "in_docker", return_value=True) def test_package_defers_conan_upload_credentials_until_after_create(self, docker, configure_remote, create_package, upload): context = self.context() with unittest.mock.patch.dict( os.environ, { "SAMPLE_CONAN_UPLOAD": "1", }, clear=True, ): status = modulus.packaging.package( context, [ "--target", "native", ], ) self.assertEqual(status, 1) configure_remote.assert_called_once_with(context) create_package.assert_called_once_with(context, context.default_target, False, no_remote=False) upload.assert_called_once_with(context, replace=False) docker.assert_called_once() @unittest.mock.patch.object(modulus.packaging, "create_package", return_value=0) @unittest.mock.patch.object(modulus.process, "run", return_value=0) @unittest.mock.patch.object(modulus.process, "in_docker", return_value=True) def test_package_checks_credentials_only_at_upload(self, docker, run, create_package): context = self.context() with unittest.mock.patch.dict( os.environ, { "CONAN_USER": "dev", "SAMPLE_CONAN_UPLOAD": "1", }, clear=True, ): status = modulus.packaging.package( context, [ "--target", "native", ], ) self.assertEqual(status, 1) 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.build, "clear_conan_deps") @unittest.mock.patch.object(modulus.process, "run", return_value=0) def test_configure_one_passes_no_remote_to_conan_install(self, run, clear): context = self.context() status = modulus.build.configure_one(context, context.default_target, update=False, no_remote=True) self.assertEqual(status, 0) conan_command = run.call_args_list[0].args[1] self.assertIn("--no-remote", conan_command) self.assertIn("--build=missing", conan_command) self.assertNotIn("-s", conan_command) self.assertNotIn("-c", conan_command) self.assertNotIn("glfw/*:with_x11=False", conan_command) self.assertIn(f"-DCMAKE_TOOLCHAIN_FILE={context.default_target.build_dir / 'conan' / 'conan_toolchain.cmake'}", run.call_args_list[1].args[1]) self.assertIn("-DCMAKE_BUILD_TYPE=Release", run.call_args_list[1].args[1]) clear.assert_called_once() @unittest.mock.patch.object(modulus.build, "clear_conan_deps") @unittest.mock.patch.object(modulus.process, "run", return_value=0) def test_configure_one_passes_target_cmake_definitions(self, run, clear): context = self.context() context.default_target.cmake_definitions = { "SAMPLE_WITH_WEB": True, "SAMPLE_MODE": "dev", "SAMPLE_LIMIT": 2, } status = modulus.build.configure_one(context, context.default_target, update=False) self.assertEqual(status, 0) cmake_command = run.call_args_list[1].args[1] self.assertIn("-DSAMPLE_WITH_WEB=ON", cmake_command) self.assertIn("-DSAMPLE_MODE=dev", cmake_command) self.assertIn("-DSAMPLE_LIMIT=2", cmake_command) clear.assert_called_once() @unittest.mock.patch.object(modulus.lsp, "configure_lsp_database", return_value=0) @unittest.mock.patch.object(modulus.build, "configure_one", return_value=0) @unittest.mock.patch.object(modulus.conan, "configure_conan_remote", return_value=0) @unittest.mock.patch.object(modulus.process, "in_docker", return_value=True) def test_lsp_configures_default_target_and_extra_databases(self, docker, configure_remote, configure_one, configure_lsp): context = self.context() database = { "name": "pico", "source_dir": ".", "build_dir": "build/build_pico_debug", "generator": "Ninja", "cmake_definitions": { "VAMP_PICO_FIRMWARE_PROJECT": True, }, "env": {}, "unset_env": ["CC", "CXX"], } context.lsp_settings = { "enabled": True, "compile_databases": [database], } status = modulus.lsp.lsp(context, []) self.assertEqual(status, 0) configure_remote.assert_called_once_with(context) configure_one.assert_called_once_with(context, context.default_target, False, no_remote=False) configure_lsp.assert_called_once_with(context, database) docker.assert_called_once() @unittest.mock.patch.object(modulus.process, "run", return_value=0) def test_configure_lsp_database_passes_cmake_definitions_and_unsets_env(self, run): with tempfile.TemporaryDirectory() as repo_dir: repo = pathlib.Path(repo_dir) context = self.context(repo_root=repo) status = modulus.lsp.configure_lsp_database( context, { "name": "pico", "source_dir": ".", "build_dir": "build/build_pico_debug", "generator": "Ninja", "cmake_definitions": { "VAMP_PICO_FIRMWARE_PROJECT": True, }, "env": { "PICO_SDK_PATH": "/opt/pico-sdk", }, "unset_env": ["CC", "CXX"], }, ) self.assertEqual(status, 0) command = run.call_args.args[1] self.assertIn("-DVAMP_PICO_FIRMWARE_PROJECT=ON", command) self.assertEqual(run.call_args.kwargs["env_overrides"], {"PICO_SDK_PATH": "/opt/pico-sdk"}) self.assertEqual(run.call_args.kwargs["env_unset"], ["CC", "CXX"]) @unittest.mock.patch.object(modulus.build, "clear_conan_deps") @unittest.mock.patch.object(modulus.process, "run", return_value=0) def test_configure_one_allows_public_repos_to_build_missing_dependencies(self, run, clear): context = self.context() status = modulus.build.configure_one(context, context.default_target, update=False) self.assertEqual(status, 0) conan_command = run.call_args_list[0].args[1] self.assertIn("--build=missing", conan_command) clear.assert_called_once() @unittest.mock.patch.object(modulus.process, "run", return_value=0) def test_create_package_passes_no_remote_to_conan_create(self, run): with tempfile.TemporaryDirectory() as repo_dir: context = self.context(repo_root=pathlib.Path(repo_dir)) status = modulus.packaging.create_package(context, context.default_target, update=False, no_remote=True) self.assertEqual(status, 0) self.assertIn("&:with_tests=False", run.call_args.args[1]) self.assertIn("--build=sample/*", run.call_args.args[1]) self.assertIn("--build=missing", run.call_args.args[1]) self.assertIn("--no-remote", run.call_args.args[1]) @unittest.mock.patch.object(modulus.process, "run", return_value=0) def test_create_package_allows_public_repos_to_build_missing_dependencies(self, run): with tempfile.TemporaryDirectory() as repo_dir: context = self.context(repo_root=pathlib.Path(repo_dir)) status = modulus.packaging.create_package(context, context.default_target, update=False) self.assertEqual(status, 0) self.assertIn("--build=sample/*", run.call_args.args[1]) self.assertIn("--build=missing", run.call_args.args[1]) def test_create_artifact_package_installs_archives_and_checksums_tree(self): with tempfile.TemporaryDirectory() as repo_dir: repo = pathlib.Path(repo_dir) context = self.context(repo_root=repo) context.package["artifact"].update({ "enabled": True, "targets": ["native"], "install_prefix": "usr", "cmake_definitions": { "SAMPLE_WITH_WEB": True, }, "conan_options": { "with_appliance_adapters": True, }, }) def fake_run(run_context, command, **kwargs): if command[:2] == ["cmake", "--install"]: install_root = modulus.packaging.package_artifact_build_root(context, context.default_target) / "stage" / "usr" (install_root / "bin").mkdir(parents=True) (install_root / "bin" / "sample").write_text("binary\n", encoding="utf-8") return 0 with unittest.mock.patch.object(modulus.process, "run", side_effect=fake_run) as run: status, artifacts = modulus.packaging.create_artifact_package(context, context.default_target, update=False, no_remote=True) self.assertEqual(status, 0) 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()) self.assertTrue(checksum.is_file()) with tarfile.open(archive, "r:gz") as input_archive: self.assertIn("usr/bin/sample", input_archive.getnames()) self.assertIn(archive.name, checksum.read_text(encoding="utf-8")) conan_command = run.call_args_list[0].args[1] self.assertEqual(conan_command[0:3], ["conan", "install", "."]) self.assertIn("&:with_appliance_adapters=True", conan_command) self.assertIn("&:with_tests=False", conan_command) self.assertIn("--no-remote", conan_command) self.assertIn("--build=missing", conan_command) 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.process, "in_docker", return_value=True) @unittest.mock.patch.object(modulus.packaging, "create_artifact_package", return_value=(0, [])) def test_artifact_command_needs_no_release_argument(self, create, docker): with tempfile.TemporaryDirectory() as temporary: context = self.context(repo_root=pathlib.Path(temporary)) context.package["artifact"]["enabled"] = True self.assertEqual(modulus.packaging.package_artifacts(context, ["--no-remote"]), 0) create.assert_called_once_with(context, context.default_target, False, no_remote=True) if __name__ == "__main__": unittest.main()