Skip to content

Commit 40998ce

Browse files
committed
add more tests to Conan command wrapper
1 parent 61c6a3d commit 40998ce

File tree

4 files changed

+160
-40
lines changed

4 files changed

+160
-40
lines changed

src/cpp_dev/common/process.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def run_command(command: str, *args: str) -> tuple[int, str, str]:
1717
This function blocks until the command has finished.
1818
"""
1919
logging.debug(f"Running command: {command} {args}")
20-
result = subprocess.run([command, *args], check=True, capture_output=True) # noqa: S603
20+
result = subprocess.run([command, *args], check=False, capture_output=True) # noqa: S603
2121

2222
logging.debug(f"Command return code: {result.returncode}")
2323

src/cpp_dev/dependency/conan/command_wrapper.py

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# For a copy, see <https://opensource.org/license/bsd-3-clause>.
55

66
import json
7+
import re
78
from collections.abc import Mapping
89
from pathlib import Path
910
from typing import Literal, Optional
@@ -19,6 +20,17 @@
1920
# Public API ###
2021
###############################################################################
2122

23+
class ConanCommandException(Exception):
24+
"""Exception for raising issues during Conan command execution."""
25+
def __init__(self, command: str, msg: str) -> None:
26+
self._command = command
27+
self._msg = msg
28+
super().__init__(f"{self._command} failed: {self._msg}")
29+
30+
31+
ConanSetting = Literal["compiler", "compiler.cppstd"]
32+
33+
2234
############################
2335
### Conan Config Install ###
2436
############################
@@ -54,9 +66,7 @@ def conan_list(remote: str, name: str) -> Mapping[ConanPackageReference, dict]:
5466
f"--remote={remote}",
5567
f"{name}/",
5668
)
57-
print(stdout)
5869
parsed_data = ConanListResult.model_validate_json(stdout)
59-
print(parsed_data)
6070
return parsed_data.root[remote]
6171

6272

@@ -77,31 +87,75 @@ class ConanRecipeAttributes(BaseModel):
7787
class ConanGraphBuildOrder(BaseModel):
7888
order: list[list[ConanRecipeAttributes]]
7989

80-
def conan_graph_buildorder(conanfile_path: Path, profile: str) -> ConanGraphBuildOrder:
90+
91+
COMMAND_GRAPH_BUILDORDER = "graph-buildorder"
92+
93+
def _handle_package_resolution_error(stderr: str) -> None:
94+
regex_unable_to_find = re.compile(r"Unable to find '([^']+)'")
95+
match = regex_unable_to_find.search(stderr)
96+
if match:
97+
raise ConanCommandException(
98+
command=COMMAND_GRAPH_BUILDORDER,
99+
msg=f"unable to find package '{match.group(1)}'",
100+
)
101+
102+
def _handle_package_version_conflict(stderr: str) -> None:
103+
regex_version_conflict = re.compile(r"Version conflict: Conflict between ([^ ]+) and ([^ ]+) in the graph")
104+
match = regex_version_conflict.search(stderr)
105+
if match:
106+
raise ConanCommandException(
107+
command=COMMAND_GRAPH_BUILDORDER,
108+
msg=f"version conflict between '{match.group(1)}' and '{match.group(2)}'",
109+
)
110+
111+
def _handle_graph_buildorder_error(stderr: str) -> None:
112+
_handle_package_resolution_error(stderr)
113+
_handle_package_version_conflict(stderr)
114+
115+
raise ConanCommandException(
116+
command=COMMAND_GRAPH_BUILDORDER,
117+
msg="generic error",
118+
)
119+
120+
def conan_graph_buildorder(conanfile_path: Path, profile: str, settings: dict[ConanSetting, object]) -> ConanGraphBuildOrder:
81121
"""Run "conan graph buildorder"."""
82-
stdout, _ = run_command_assert_success(
122+
command = [
83123
"conan",
84124
"graph",
85125
"build-order",
86126
str(conanfile_path),
87127
"-pr:a", profile,
88128
"-f", "json",
89129
"--order-by", "recipe",
130+
]
131+
for key, value in settings.items():
132+
command.extend(["-s:a", f"{key}={value}"])
133+
rc, stdout, stderr = run_command(
134+
*command
90135
)
91-
print(stdout)
136+
if rc != 0:
137+
print(f"STDOUT: {stderr}")
138+
_handle_graph_buildorder_error(stderr)
139+
92140
return ConanGraphBuildOrder.model_validate_json(stdout)
93141

94142

95143
####################
96144
### Conan Create ###
97145
####################
98-
def conan_create(package_dir: Path, profile: str) -> None:
146+
147+
def conan_create(package_dir: Path, profile: str, settings: dict[ConanSetting, object]) -> None:
99148
"""Run "conan create"."""
100-
run_command_assert_success(
149+
command = [
101150
"conan",
102151
"create",
103152
str(package_dir),
104153
"-pr:a", profile,
154+
]
155+
for key, value in settings.items():
156+
command.extend(["-s:a", f"{key}={value}"])
157+
run_command_assert_success(
158+
*command
105159
)
106160

107161
####################

src/tests/cpp_dev/dependency/conan/test_command_wrapper.py

Lines changed: 48 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,25 @@
33
# This work is licensed under the terms of the BSD-3-Clause license.
44
# For a copy, see <https://opensource.org/license/bsd-3-clause>.
55

6-
import json
76
from collections.abc import Generator
8-
from dataclasses import dataclass
97
from pathlib import Path
108
from textwrap import dedent
119
from unittest.mock import AsyncMock, MagicMock, patch
1210

1311
import pytest
1412

15-
from cpp_dev.dependency.conan.command_wrapper import (conan_create,
13+
from cpp_dev.dependency.conan.command_wrapper import (ConanCommandException,
14+
ConanSetting,
15+
conan_create,
1616
conan_graph_buildorder,
1717
conan_list,
1818
conan_remote_login,
1919
conan_upload)
2020
from cpp_dev.dependency.conan.setup import CONAN_REMOTE
2121
from cpp_dev.dependency.conan.types import ConanPackageReference
22-
from cpp_dev.dependency.conan.utils import conan_env
2322

2423
from .utils.env import ConanTestEnv, ConanTestPackage, create_conan_test_env
25-
from .utils.server import ConanServer, launch_conan_test_server
24+
from .utils.server import launch_conan_test_server
2625

2726
MockType = MagicMock | AsyncMock
2827

@@ -31,28 +30,20 @@ def patched_run_command_assert_success() -> Generator[MockType]:
3130
with patch("cpp_dev.dependency.conan.command_wrapper.run_command_assert_success") as mock_run_command:
3231
yield mock_run_command
3332

34-
def test_conan_remote_login(patched_run_command_assert_success: MockType) -> None:
35-
conan_remote_login(CONAN_REMOTE, "user", "password")
36-
patched_run_command_assert_success.assert_called_once_with(
37-
"conan",
38-
"remote",
39-
"login",
40-
CONAN_REMOTE,
41-
"user",
42-
"-p",
43-
"password",
44-
)
45-
4633
def test_conan_create(patched_run_command_assert_success: MockType) -> None:
47-
conan_create(Path("package_dir"), "profile")
34+
# todo: this test currently uses a mock, but wil later be changed to test with a real server.
35+
conan_create(Path("package_dir"), "profile", {"compiler": "test", "compiler.cppstd": "c++17"})
4836
patched_run_command_assert_success.assert_called_once_with(
4937
"conan",
5038
"create",
5139
"package_dir",
5240
"-pr:a", "profile",
41+
"-s:a", "compiler=test",
42+
"-s:a", "compiler.cppstd=c++17",
5343
)
5444

5545
def test_conan_upload(patched_run_command_assert_success: MockType) -> None:
46+
# todo: this test currently uses a mock, but wil later be changed to test with a real server.
5647
package_ref = ConanPackageReference("cpd/1.0.0@official/cppdev")
5748
conan_upload(package_ref, CONAN_REMOTE)
5849
patched_run_command_assert_success.assert_called_once_with(
@@ -73,7 +64,7 @@ def conan_test_environment(tmp_path: Path, unused_http_port: int) -> Generator[C
7364
cpp_standard="c++17",
7465
),
7566
ConanTestPackage(
76-
ref=ConanPackageReference("cpd1/1.0.0@official/cppdev"),
67+
ref=ConanPackageReference("dep/2.0.0@official/cppdev"),
7768
dependencies=[],
7869
cpp_standard="c++17",
7970
),
@@ -82,10 +73,19 @@ def conan_test_environment(tmp_path: Path, unused_http_port: int) -> Generator[C
8273
dependencies=[ConanPackageReference("dep/1.0.0@official/cppdev")],
8374
cpp_standard="c++17",
8475
),
76+
ConanTestPackage(
77+
ref=ConanPackageReference("cpd1/1.0.0@official/cppdev"),
78+
dependencies=[ConanPackageReference("dep/2.0.0@official/cppdev")],
79+
cpp_standard="c++17",
80+
),
8581
]
8682
with create_conan_test_env(tmp_path / "conan", server.http_port, TEST_PACKAGES) as conan_test_env:
8783
yield conan_test_env
8884

85+
@pytest.mark.conan_remote
86+
def test_conan_remote_login(conan_test_environment: ConanTestEnv) -> None:
87+
conan_remote_login(CONAN_REMOTE, conan_test_environment.server.user, conan_test_environment.server.password)
88+
8989

9090
@pytest.mark.conan_remote
9191
@pytest.mark.usefixtures("conan_test_environment")
@@ -95,6 +95,9 @@ def test_conan_list() -> None:
9595
assert ConanPackageReference("cpd/1.0.0@official/cppdev") in result
9696

9797

98+
def _construct_settings(test_env: ConanTestEnv) -> dict[ConanSetting, object]:
99+
return {"compiler": test_env.compiler, "compiler.cppstd": test_env.cppstd}
100+
98101
@pytest.mark.conan_remote
99102
def test_conan_graph_buildorder(tmp_path: Path, conan_test_environment: ConanTestEnv) -> None:
100103
conanfile_path = tmp_path / "conanfile.txt"
@@ -103,7 +106,7 @@ def test_conan_graph_buildorder(tmp_path: Path, conan_test_environment: ConanTes
103106
cpd/1.0.0@official/cppdev
104107
""")
105108
)
106-
graph_build_order = conan_graph_buildorder(conanfile_path, conan_test_environment.profile)
109+
graph_build_order = conan_graph_buildorder(conanfile_path, conan_test_environment.profile, _construct_settings(conan_test_environment))
107110
assert len(graph_build_order.order) == 2
108111
assert len(graph_build_order.order[0]) == 1
109112
dep_recipe = graph_build_order.order[0][0]
@@ -116,3 +119,28 @@ def test_conan_graph_buildorder(tmp_path: Path, conan_test_environment: ConanTes
116119
assert cpd_recipe.depends[0].startswith("dep/1.0.0@official/cppdev")
117120

118121

122+
@pytest.mark.conan_remote
123+
def test_conan_graph_buildorder_dependency_does_not_exist(tmp_path: Path, conan_test_environment: ConanTestEnv) -> None:
124+
conanfile_path = tmp_path / "conanfile.txt"
125+
conanfile_path.write_text(dedent("""
126+
[requires]
127+
cpd/0.0.0@official/cppdev
128+
""")
129+
)
130+
131+
with pytest.raises(ConanCommandException, match="unable to find package") as e:
132+
conan_graph_buildorder(conanfile_path, conan_test_environment.profile, _construct_settings(conan_test_environment))
133+
134+
135+
@pytest.mark.conan_remote
136+
def test_conan_graph_buildorder_multiple_dependencies(tmp_path: Path, conan_test_environment: ConanTestEnv) -> None:
137+
conanfile_path = tmp_path / "conanfile.txt"
138+
conanfile_path.write_text(dedent("""
139+
[requires]
140+
cpd/[>=0.0.0]@official/cppdev
141+
cpd1/[<2.0.0]@official/cppdev
142+
""")
143+
)
144+
145+
with pytest.raises(ConanCommandException, match="version conflict") as e:
146+
conan_graph_buildorder(conanfile_path, conan_test_environment.profile, _construct_settings(conan_test_environment))

0 commit comments

Comments
 (0)