947 lines
38 KiB
Python
947 lines
38 KiB
Python
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.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 = [
|
|
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_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.cli.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.cli, "find_repo_root", return_value=root):
|
|
for command in ("configure", "lsp"):
|
|
with self.subTest(command=command), unittest.mock.patch.object(modulus.cli, 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.cli, "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.cli.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.cli, "upload_all_packages", return_value=37)
|
|
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
|
|
@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):
|
|
with unittest.mock.patch.dict(os.environ, {}, clear=True):
|
|
status = modulus.cli.upload_packages(self.context(), replace=True)
|
|
self.assertEqual(status, 1)
|
|
configure.assert_not_called()
|
|
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)
|
|
@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,
|
|
{
|
|
"MODULUS_ALLOW_PACKAGE_REPLACE": "1",
|
|
},
|
|
clear=True,
|
|
):
|
|
status = modulus.cli.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.cli, "run", return_value=0)
|
|
def test_upload_defaults_to_latest_reference(self, run):
|
|
with unittest.mock.patch.dict(os.environ, {}, clear=True):
|
|
status = modulus.cli.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.cli, "run", return_value=0)
|
|
def test_replace_upload_uses_conan_force(self, run):
|
|
status = modulus.cli.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.cli.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.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)
|
|
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.cli, "in_docker", return_value=True)
|
|
def test_run_is_disabled_by_default(self, docker):
|
|
status = modulus.cli.product_run(self.context(), [])
|
|
self.assertEqual(status, 1)
|
|
docker.assert_not_called()
|
|
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
|
@unittest.mock.patch.object(modulus.cli, "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.cli.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.cli, "docker_run", return_value=0)
|
|
@unittest.mock.patch.object(modulus.cli, "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.cli.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.cli, "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.cli.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.clangd.image_label}={modulus.cli.modulus.clangd.recipe_digest(context)}",
|
|
"--build-arg",
|
|
"MODULUS_TOOLS=tools",
|
|
".",
|
|
],
|
|
)
|
|
@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_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.cli.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.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,
|
|
{
|
|
"CI": "true",
|
|
},
|
|
clear=True,
|
|
):
|
|
self.assertEqual(modulus.cli.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.cli.conan_credentials()
|
|
self.assertEqual(user, "dev")
|
|
self.assertEqual(token, "secret")
|
|
@unittest.mock.patch.object(modulus.cli, "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.cli.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.cli, "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.cli.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.cli, "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.cli.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.cli, "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.cli.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",
|
|
"<hidden>",
|
|
],
|
|
)
|
|
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.cli.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.cli, "run", return_value=0)
|
|
@unittest.mock.patch.object(modulus.cli, "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.cli.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.cli, "run", return_value=0)
|
|
@unittest.mock.patch.object(modulus.cli, "configure_one", return_value=0)
|
|
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
|
|
@unittest.mock.patch.object(modulus.cli, "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.cli.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.cli, "run", return_value=0)
|
|
@unittest.mock.patch.object(modulus.cli, "configure_one", return_value=0)
|
|
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
|
|
@unittest.mock.patch.object(modulus.cli, "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.cli.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.cli, "run", return_value=0)
|
|
@unittest.mock.patch.object(modulus.cli, "configure_one", return_value=0)
|
|
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
|
|
@unittest.mock.patch.object(modulus.cli, "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.cli.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.cli, "run", return_value=0)
|
|
@unittest.mock.patch.object(modulus.cli, "configure_one", return_value=37)
|
|
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
|
|
@unittest.mock.patch.object(modulus.cli, "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.cli.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.cli, "configure_one", return_value=0)
|
|
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
|
|
@unittest.mock.patch.object(modulus.cli, "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.cli.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.cli, "upload_packages", return_value=0)
|
|
@unittest.mock.patch.object(modulus.cli, "create_package", return_value=0)
|
|
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
|
|
@unittest.mock.patch.object(modulus.cli, "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.cli.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.cli, "upload_packages", return_value=0)
|
|
@unittest.mock.patch.object(modulus.cli, "create_package", return_value=0)
|
|
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
|
|
@unittest.mock.patch.object(modulus.cli, "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.cli.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.cli, "upload_packages", return_value=1)
|
|
@unittest.mock.patch.object(modulus.cli, "create_package", return_value=0)
|
|
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
|
|
@unittest.mock.patch.object(modulus.cli, "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.cli.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.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_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.cli.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.cli, "clear_conan_deps")
|
|
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
|
def test_configure_one_passes_no_remote_to_conan_install(self, run, clear):
|
|
context = self.context()
|
|
status = modulus.cli.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.cli, "clear_conan_deps")
|
|
@unittest.mock.patch.object(modulus.cli, "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.cli.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.cli, "configure_lsp_database", return_value=0)
|
|
@unittest.mock.patch.object(modulus.cli, "configure_one", return_value=0)
|
|
@unittest.mock.patch.object(modulus.cli, "configure_conan_remote", return_value=0)
|
|
@unittest.mock.patch.object(modulus.cli, "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.cli.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.cli, "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.cli.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.cli, "clear_conan_deps")
|
|
@unittest.mock.patch.object(modulus.cli, "run", return_value=0)
|
|
def test_configure_one_allows_public_repos_to_build_missing_dependencies(self, run, clear):
|
|
context = self.context()
|
|
status = modulus.cli.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.cli, "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.cli.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.cli, "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.cli.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.cli.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.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"
|
|
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.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):
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
context = self.context(repo_root=pathlib.Path(temporary))
|
|
context.package["artifact"]["enabled"] = True
|
|
self.assertEqual(modulus.cli.package_artifacts(context, ["--no-remote"]), 0)
|
|
create.assert_called_once_with(context, context.default_target, False, no_remote=True)
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|