proj-flow 0.20.3__py3-none-any.whl → 0.22.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- proj_flow/__init__.py +1 -1
- proj_flow/api/arg.py +2 -0
- proj_flow/api/completers.py +1 -1
- proj_flow/api/env.py +45 -15
- proj_flow/api/release.py +1 -1
- proj_flow/api/step.py +20 -5
- proj_flow/{ext/cplusplus/cmake/presets.py → base/cmake_presets.py} +40 -16
- proj_flow/base/plugins.py +12 -7
- proj_flow/cli/finder.py +4 -3
- proj_flow/dependency.py +6 -2
- proj_flow/ext/cplusplus/cmake/__init__.py +2 -2
- proj_flow/ext/cplusplus/cmake/parser.py +4 -3
- proj_flow/ext/cplusplus/cmake/steps.py +9 -9
- proj_flow/ext/cplusplus/conan/__init__.py +8 -4
- proj_flow/ext/github/cli.py +1 -3
- proj_flow/ext/github/publishing.py +1 -0
- proj_flow/ext/python/rtdocs.py +37 -8
- proj_flow/ext/python/steps.py +5 -4
- proj_flow/ext/python/version.py +12 -12
- proj_flow/ext/sign/__init__.py +2 -2
- proj_flow/ext/test_runner/__init__.py +6 -0
- proj_flow/ext/test_runner/cli.py +416 -0
- proj_flow/ext/test_runner/driver/__init__.py +2 -0
- proj_flow/ext/test_runner/driver/commands.py +74 -0
- proj_flow/ext/test_runner/driver/test.py +610 -0
- proj_flow/ext/test_runner/driver/testbed.py +141 -0
- proj_flow/ext/test_runner/utils/__init__.py +2 -0
- proj_flow/ext/test_runner/utils/archives.py +56 -0
- proj_flow/log/rich_text/api.py +3 -4
- proj_flow/minimal/list.py +126 -10
- proj_flow/minimal/run.py +3 -2
- proj_flow/project/api.py +1 -0
- {proj_flow-0.20.3.dist-info → proj_flow-0.22.0.dist-info}/METADATA +17 -7
- {proj_flow-0.20.3.dist-info → proj_flow-0.22.0.dist-info}/RECORD +37 -29
- {proj_flow-0.20.3.dist-info → proj_flow-0.22.0.dist-info}/WHEEL +0 -0
- {proj_flow-0.20.3.dist-info → proj_flow-0.22.0.dist-info}/entry_points.txt +0 -0
- {proj_flow-0.20.3.dist-info → proj_flow-0.22.0.dist-info}/licenses/LICENSE +0 -0
proj_flow/ext/python/rtdocs.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
"""
|
|
5
5
|
The **proj_flow.ext.python.rtdocs** defines RTDocs step (`"RTD"`), which uses
|
|
6
|
-
|
|
6
|
+
``.readthedocs.yaml`` to build the HTML documentation.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
import os
|
|
@@ -20,20 +20,24 @@ from proj_flow.base import cmd
|
|
|
20
20
|
|
|
21
21
|
@step.register
|
|
22
22
|
class RTDocs:
|
|
23
|
-
|
|
23
|
+
"""Runs the jobs defined by ``.readthedocs.yaml`` to build the docs"""
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def name(self):
|
|
27
|
+
return "RTD"
|
|
24
28
|
|
|
25
29
|
def platform_dependencies(self):
|
|
26
30
|
return ["python -m PyYAML"]
|
|
27
31
|
|
|
28
32
|
def is_active(self, config: env.Config, rt: env.Runtime) -> bool:
|
|
29
|
-
return
|
|
33
|
+
return (rt.root / ".readthedocs.yaml").is_file()
|
|
30
34
|
|
|
31
35
|
def run(self, config: env.Config, rt: env.Runtime) -> int:
|
|
32
36
|
import venv
|
|
33
37
|
|
|
34
38
|
import yaml
|
|
35
39
|
|
|
36
|
-
with open(
|
|
40
|
+
with open(rt.root / ".readthedocs.yaml") as rtd_yaml:
|
|
37
41
|
data = yaml.load(rtd_yaml, Loader=yaml.Loader)
|
|
38
42
|
|
|
39
43
|
formats = ["html"]
|
|
@@ -100,6 +104,8 @@ class RTDocs:
|
|
|
100
104
|
|
|
101
105
|
|
|
102
106
|
class Builder(ABC):
|
|
107
|
+
"""Base class for any recognized builder in the ``.readthedocs.yaml`` config."""
|
|
108
|
+
|
|
103
109
|
@property
|
|
104
110
|
@abstractmethod
|
|
105
111
|
def READTHEDOCS_OUTPUT(self) -> str: ...
|
|
@@ -112,17 +118,40 @@ class Builder(ABC):
|
|
|
112
118
|
|
|
113
119
|
|
|
114
120
|
class Sphinx(Builder):
|
|
115
|
-
|
|
121
|
+
"""Builder used, if Sphinx config is found in the config file.
|
|
122
|
+
|
|
123
|
+
:param config: filename of a Python script Sphinx should use for the configuration
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def READTHEDOCS_OUTPUT(self) -> str:
|
|
128
|
+
"""A ``build/`` subdirectory placed in the same dir, as ``config`` parameter"""
|
|
129
|
+
return self.output
|
|
116
130
|
|
|
117
131
|
def __init__(self, config: str):
|
|
118
132
|
self.config = config
|
|
119
133
|
self.source = os.path.dirname(config)
|
|
120
|
-
self.
|
|
134
|
+
self.output = os.path.join(os.path.dirname(self.source), "build")
|
|
121
135
|
|
|
122
|
-
def build(self, target: str):
|
|
136
|
+
def build(self, target: str) -> int:
|
|
137
|
+
"""Uses ``spinx-build`` to create the documentation.
|
|
138
|
+
|
|
139
|
+
:param target: name of the docs format from YAML config
|
|
140
|
+
:returns: exit code forwarded from the build tool
|
|
141
|
+
"""
|
|
123
142
|
builder = "latex" if target == "pdf" else target
|
|
143
|
+
print(shutil.which("sphinx-build"))
|
|
144
|
+
|
|
124
145
|
return subprocess.run(
|
|
125
|
-
[
|
|
146
|
+
[
|
|
147
|
+
PYTHON_EXECUTABLE,
|
|
148
|
+
"-m",
|
|
149
|
+
"sphinx.cmd.build",
|
|
150
|
+
"-M",
|
|
151
|
+
builder,
|
|
152
|
+
self.source,
|
|
153
|
+
self.READTHEDOCS_OUTPUT,
|
|
154
|
+
],
|
|
126
155
|
shell=False,
|
|
127
156
|
).returncode
|
|
128
157
|
|
proj_flow/ext/python/steps.py
CHANGED
|
@@ -22,7 +22,7 @@ class Install:
|
|
|
22
22
|
return ["python -m pip"]
|
|
23
23
|
|
|
24
24
|
def run(self, config: env.Config, rt: env.Runtime) -> int:
|
|
25
|
-
return rt.cmd("python", "-m", "pip", "install", rt.root)
|
|
25
|
+
return rt.cmd("python", "-m", "pip", "install", rt.root.as_posix())
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
@step.register
|
|
@@ -48,8 +48,9 @@ class CheckTwine:
|
|
|
48
48
|
return ["twine"]
|
|
49
49
|
|
|
50
50
|
def run(self, config: env.Config, rt: env.Runtime) -> int:
|
|
51
|
-
filenames = []
|
|
52
|
-
|
|
51
|
+
filenames: list[str] = []
|
|
52
|
+
root = rt.root / "dist"
|
|
53
|
+
for root, dirnames, filenames in root.walk():
|
|
53
54
|
dirnames[:] = []
|
|
54
55
|
|
|
55
56
|
_, project = release.project_suites.find(lambda suite: suite.get_project(rt))
|
|
@@ -67,5 +68,5 @@ class CheckTwine:
|
|
|
67
68
|
return 0
|
|
68
69
|
|
|
69
70
|
return rt.cmd(
|
|
70
|
-
"twine", "check", *(
|
|
71
|
+
"twine", "check", *((root / filename).as_posix() for filename in filenames)
|
|
71
72
|
)
|
proj_flow/ext/python/version.py
CHANGED
|
@@ -7,7 +7,7 @@ The **proj_flow.ext.python.version** provides project suite plugin.
|
|
|
7
7
|
|
|
8
8
|
import os
|
|
9
9
|
import re
|
|
10
|
-
from typing import NamedTuple, Optional
|
|
10
|
+
from typing import NamedTuple, Optional, cast
|
|
11
11
|
|
|
12
12
|
import toml
|
|
13
13
|
|
|
@@ -28,8 +28,8 @@ class ProjectSuite(release.ProjectSuite):
|
|
|
28
28
|
return None
|
|
29
29
|
|
|
30
30
|
try:
|
|
31
|
-
with open(
|
|
32
|
-
text =
|
|
31
|
+
with open(rt.root / path, encoding="UTF-8") as in_file:
|
|
32
|
+
text = in_file.read()
|
|
33
33
|
except FileNotFoundError:
|
|
34
34
|
return None
|
|
35
35
|
|
|
@@ -63,27 +63,27 @@ class ProjectSuite(release.ProjectSuite):
|
|
|
63
63
|
return path
|
|
64
64
|
|
|
65
65
|
def _pyproject_hatch(self, rt: env.Runtime):
|
|
66
|
-
pyproject_path = os.path.join(rt.root, "pyproject.toml")
|
|
67
66
|
try:
|
|
68
|
-
data = toml.load(
|
|
69
|
-
project = data.get("project", {})
|
|
70
|
-
hatch = data.get("tool", {}).get("hatch", {})
|
|
71
|
-
wheels = (
|
|
67
|
+
data = toml.load(rt.root / "pyproject.toml")
|
|
68
|
+
project = cast(dict, data.get("project", {}))
|
|
69
|
+
hatch = cast(dict, data.get("tool", {}).get("hatch", {}))
|
|
70
|
+
wheels = cast(
|
|
71
|
+
dict,
|
|
72
72
|
hatch.get("build", {})
|
|
73
73
|
.get("targets", {})
|
|
74
74
|
.get("wheel", {})
|
|
75
|
-
.get("packages", [])
|
|
75
|
+
.get("packages", []),
|
|
76
76
|
)
|
|
77
77
|
|
|
78
|
-
name = project.get("name")
|
|
78
|
+
name = cast(str | None, project.get("name"))
|
|
79
79
|
if len(wheels) > 0:
|
|
80
80
|
first_wheel = wheels[0].split("/")[-1]
|
|
81
81
|
if first_wheel:
|
|
82
82
|
name = first_wheel
|
|
83
83
|
|
|
84
|
-
dynamic = project.get("dynamic", [])
|
|
84
|
+
dynamic = cast(list[str], project.get("dynamic", []))
|
|
85
85
|
if "version" in dynamic:
|
|
86
|
-
version_dict = hatch.get("version", {})
|
|
86
|
+
version_dict = cast(dict[str, str | None], hatch.get("version", {}))
|
|
87
87
|
return QuickProjectInfo(
|
|
88
88
|
name=name,
|
|
89
89
|
path=version_dict.get("path"),
|
proj_flow/ext/sign/__init__.py
CHANGED
|
@@ -54,7 +54,7 @@ class SignBase(step.Step):
|
|
|
54
54
|
self._runs_after = runs_after
|
|
55
55
|
self._runs_before = runs_before
|
|
56
56
|
|
|
57
|
-
def is_active(self, config: env.Config, rt: env.Runtime) ->
|
|
57
|
+
def is_active(self, config: env.Config, rt: env.Runtime) -> bool:
|
|
58
58
|
self._active_tools = [
|
|
59
59
|
tool for tool in api.signing_tool.get() if tool.is_active(config, rt)
|
|
60
60
|
]
|
|
@@ -120,7 +120,7 @@ class SignMsi(SignBase):
|
|
|
120
120
|
runs_before=["StorePackages", "Store"],
|
|
121
121
|
)
|
|
122
122
|
|
|
123
|
-
def is_active(self, config: env.Config, rt: env.Runtime) ->
|
|
123
|
+
def is_active(self, config: env.Config, rt: env.Runtime) -> bool:
|
|
124
124
|
return super().is_active(config, rt) and "WIX" in config.items.get(
|
|
125
125
|
"cpack_generator", []
|
|
126
126
|
)
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
# Copyright (c) 2026 Marcin Zdun
|
|
2
|
+
# This code is licensed under MIT license (see LICENSE for details)
|
|
3
|
+
|
|
4
|
+
import concurrent.futures
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import tempfile
|
|
10
|
+
from os import PathLike
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Annotated, Any, Generator, cast
|
|
13
|
+
|
|
14
|
+
from proj_flow.api import arg, env, release
|
|
15
|
+
from proj_flow.base.cmake_presets import Presets
|
|
16
|
+
from proj_flow.ext.test_runner.driver.commands import HANDLERS
|
|
17
|
+
from proj_flow.ext.test_runner.driver.test import Env, Test
|
|
18
|
+
from proj_flow.ext.test_runner.driver.testbed import Counters, task
|
|
19
|
+
|
|
20
|
+
RUN_LINEAR = os.environ.get("RUN_LINEAR", 0) != 0
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@arg.command("tools", "test-runner")
|
|
24
|
+
def test_runner(
|
|
25
|
+
preset_name: Annotated[
|
|
26
|
+
str,
|
|
27
|
+
arg.Argument(
|
|
28
|
+
help="Set name of CMake build preset",
|
|
29
|
+
meta="CONFIG",
|
|
30
|
+
names=["--preset"],
|
|
31
|
+
),
|
|
32
|
+
],
|
|
33
|
+
tests: Annotated[
|
|
34
|
+
str,
|
|
35
|
+
arg.Argument(
|
|
36
|
+
help="Point to directory with the JSON test cases; test cases are enumerated recursively",
|
|
37
|
+
meta="DIR",
|
|
38
|
+
),
|
|
39
|
+
],
|
|
40
|
+
version: Annotated[
|
|
41
|
+
str | None,
|
|
42
|
+
arg.Argument(
|
|
43
|
+
help="Select version to patch output with; defaults to automatic detection",
|
|
44
|
+
meta="SEMVER",
|
|
45
|
+
opt=True,
|
|
46
|
+
),
|
|
47
|
+
],
|
|
48
|
+
run: Annotated[
|
|
49
|
+
list[str],
|
|
50
|
+
arg.Argument(
|
|
51
|
+
help="Filter the tests to run",
|
|
52
|
+
meta="ID",
|
|
53
|
+
action="extend",
|
|
54
|
+
opt=True,
|
|
55
|
+
nargs="*",
|
|
56
|
+
default=[],
|
|
57
|
+
),
|
|
58
|
+
],
|
|
59
|
+
nullify: Annotated[
|
|
60
|
+
bool,
|
|
61
|
+
arg.FlagArgument(
|
|
62
|
+
help='Set the "expected" field of the test cases to null',
|
|
63
|
+
),
|
|
64
|
+
],
|
|
65
|
+
rt: env.Runtime,
|
|
66
|
+
) -> int:
|
|
67
|
+
"""Run specified test steps"""
|
|
68
|
+
|
|
69
|
+
if os.name == "nt":
|
|
70
|
+
sys.stdout.reconfigure(encoding="utf-8") # type: ignore
|
|
71
|
+
|
|
72
|
+
if not version:
|
|
73
|
+
proj = release.get_project(rt)
|
|
74
|
+
version = str(proj.version)
|
|
75
|
+
|
|
76
|
+
presets = Presets().visit_file(Path("CMakePresets.json")) or {}
|
|
77
|
+
preset = presets.get(preset_name)
|
|
78
|
+
|
|
79
|
+
if preset is None:
|
|
80
|
+
print(f"error: preset `{preset_name}` not found", file=sys.stderr)
|
|
81
|
+
return 1
|
|
82
|
+
|
|
83
|
+
binary_dir = preset.expand()
|
|
84
|
+
build_type = preset.build_type
|
|
85
|
+
|
|
86
|
+
if not binary_dir:
|
|
87
|
+
print(
|
|
88
|
+
f"error: preset `{preset_name}` has no binaryDir attached to it",
|
|
89
|
+
file=sys.stderr,
|
|
90
|
+
)
|
|
91
|
+
return 1
|
|
92
|
+
|
|
93
|
+
if not build_type:
|
|
94
|
+
print(
|
|
95
|
+
f"error: preset `{preset_name}` has no CMAKE_BUILD_TYPE attached to it",
|
|
96
|
+
file=sys.stderr,
|
|
97
|
+
)
|
|
98
|
+
return 1
|
|
99
|
+
|
|
100
|
+
config = cast(dict, rt._cfg.get("test-runner", {}))
|
|
101
|
+
target = cast(str | None, config.get("target"))
|
|
102
|
+
|
|
103
|
+
if not isinstance(target, str):
|
|
104
|
+
print(
|
|
105
|
+
"error: cannot find test target; "
|
|
106
|
+
"please add name of the executable as `target' property of "
|
|
107
|
+
"`test-runner' config in flow's configuration file",
|
|
108
|
+
file=sys.stderr,
|
|
109
|
+
)
|
|
110
|
+
return 1
|
|
111
|
+
|
|
112
|
+
ext = ".exe" if sys.platform == "win32" else ""
|
|
113
|
+
target_path = Path(binary_dir) / "bin" / (target + ext)
|
|
114
|
+
if not target_path.is_file():
|
|
115
|
+
print(
|
|
116
|
+
f"error: cannot find {target + ext} in {target_path.parent.as_posix()}",
|
|
117
|
+
file=sys.stderr,
|
|
118
|
+
)
|
|
119
|
+
return 1
|
|
120
|
+
|
|
121
|
+
install_components = cast(list[str], config.get("install", []))
|
|
122
|
+
patches = cast(dict[str, str], config.get("patches"))
|
|
123
|
+
env_prefix = cast(str | None, config.get("report_env"))
|
|
124
|
+
|
|
125
|
+
testsuite_config = cast(dict, config.get("testsuite", {}))
|
|
126
|
+
test_root = cast(str | None, testsuite_config.get("root"))
|
|
127
|
+
test_root_path = Path(test_root).resolve() if test_root else None
|
|
128
|
+
if not test_root_path:
|
|
129
|
+
print(
|
|
130
|
+
"error: cannot find test root directory; "
|
|
131
|
+
"please add name of the directory as `root' property of "
|
|
132
|
+
"`test-runner/testsuite' config in flow's configuration file",
|
|
133
|
+
file=sys.stderr,
|
|
134
|
+
)
|
|
135
|
+
return 1
|
|
136
|
+
|
|
137
|
+
test_data_dir = cast(str | None, testsuite_config.get("data"))
|
|
138
|
+
data_dir = (
|
|
139
|
+
(test_root_path / test_data_dir).resolve()
|
|
140
|
+
if isinstance(test_data_dir, str)
|
|
141
|
+
else None
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
test_default_set = cast(str | None, testsuite_config.get("default-set"))
|
|
145
|
+
if isinstance(test_default_set, str):
|
|
146
|
+
if (
|
|
147
|
+
not (test_root_path / tests).is_dir()
|
|
148
|
+
and (test_root_path / test_default_set / tests).is_dir()
|
|
149
|
+
):
|
|
150
|
+
tests = f"{test_default_set}/{tests}"
|
|
151
|
+
|
|
152
|
+
test_set_dir = test_root_path / tests
|
|
153
|
+
|
|
154
|
+
test_files = _enum_tests(test_set_dir, data_dir)
|
|
155
|
+
tests_to_run = [int(x) for s in (run or []) for x in s.split(",")]
|
|
156
|
+
if not tests_to_run:
|
|
157
|
+
tests_to_run = list(range(1, len(test_files) + 1))
|
|
158
|
+
|
|
159
|
+
independent_tests, linear_tests = _load_tests(test_files, tests_to_run)
|
|
160
|
+
|
|
161
|
+
if nullify:
|
|
162
|
+
for sequence in (independent_tests, linear_tests):
|
|
163
|
+
for test in sequence:
|
|
164
|
+
test[0].nullify(lang=None)
|
|
165
|
+
return 0
|
|
166
|
+
|
|
167
|
+
if not independent_tests and not linear_tests:
|
|
168
|
+
print("No tests to run.", file=sys.stderr)
|
|
169
|
+
return 0
|
|
170
|
+
|
|
171
|
+
env = _make_env(
|
|
172
|
+
target_path,
|
|
173
|
+
data_dir or Path(binary_dir),
|
|
174
|
+
version,
|
|
175
|
+
len(independent_tests) + len(linear_tests),
|
|
176
|
+
patches,
|
|
177
|
+
env_prefix,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
print("target: ", env.target, env.version)
|
|
181
|
+
if env.data_dir_alt is None:
|
|
182
|
+
print("data: ", env.data_dir)
|
|
183
|
+
else:
|
|
184
|
+
print("data: ", env.data_dir, env.data_dir_alt)
|
|
185
|
+
print("tests: ", tests)
|
|
186
|
+
if env.tempdir_alt is None:
|
|
187
|
+
print("$TEMP: ", env.tempdir)
|
|
188
|
+
else:
|
|
189
|
+
print("$TEMP: ", env.tempdir, env.tempdir_alt)
|
|
190
|
+
|
|
191
|
+
os.makedirs(env.tempdir, exist_ok=True)
|
|
192
|
+
|
|
193
|
+
install_dir = Path("build").resolve() / ".test-runner"
|
|
194
|
+
if not _install(
|
|
195
|
+
install_dir,
|
|
196
|
+
binary_dir,
|
|
197
|
+
build_type,
|
|
198
|
+
install_components,
|
|
199
|
+
env,
|
|
200
|
+
):
|
|
201
|
+
return 1
|
|
202
|
+
|
|
203
|
+
return _run_and_report_tests(
|
|
204
|
+
independent_tests=independent_tests,
|
|
205
|
+
linear_tests=linear_tests,
|
|
206
|
+
install_dir=install_dir,
|
|
207
|
+
env=env,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _run_and_report_tests(
|
|
212
|
+
independent_tests: list[tuple[Test, int]],
|
|
213
|
+
linear_tests: list[tuple[Test, int]],
|
|
214
|
+
install_dir: Path,
|
|
215
|
+
env: Env,
|
|
216
|
+
):
|
|
217
|
+
RUN_LINEAR = os.environ.get("RUN_LINEAR", 0) != 0
|
|
218
|
+
if RUN_LINEAR:
|
|
219
|
+
linear_tests[0:0] = independent_tests
|
|
220
|
+
independent_tests = []
|
|
221
|
+
|
|
222
|
+
counters = Counters()
|
|
223
|
+
|
|
224
|
+
if independent_tests:
|
|
225
|
+
try:
|
|
226
|
+
thread_count = int(os.environ.get("POOL_SIZE", "not-a-number"))
|
|
227
|
+
except (ValueError, TypeError):
|
|
228
|
+
thread_count = max(1, (os.cpu_count() or 0)) * 2
|
|
229
|
+
print("threads:", thread_count)
|
|
230
|
+
|
|
231
|
+
_report_tests(counters, _run_async_tests(independent_tests, env, thread_count))
|
|
232
|
+
|
|
233
|
+
_report_tests(counters, _run_sync_tests(linear_tests, env))
|
|
234
|
+
|
|
235
|
+
shutil.rmtree(install_dir, ignore_errors=True)
|
|
236
|
+
shutil.rmtree("build/.testing", ignore_errors=True)
|
|
237
|
+
|
|
238
|
+
if not counters.summary(len(independent_tests) + len(linear_tests)):
|
|
239
|
+
return 1
|
|
240
|
+
|
|
241
|
+
return 0
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _run_async_tests(tests: list[tuple[Test, int]], env: Env, thread_count: int):
|
|
245
|
+
with concurrent.futures.ThreadPoolExecutor(thread_count) as executor:
|
|
246
|
+
futures: list[concurrent.futures.Future[tuple[int, str, str | None, str]]] = []
|
|
247
|
+
for test, counter in tests:
|
|
248
|
+
futures.append(executor.submit(task, env, test, counter))
|
|
249
|
+
|
|
250
|
+
for future in concurrent.futures.as_completed(futures):
|
|
251
|
+
yield future.result()
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _run_sync_tests(tests: list[tuple[Test, int]], env: Env):
|
|
255
|
+
for test, counter in tests:
|
|
256
|
+
yield task(env, test, counter)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _report_tests(
|
|
260
|
+
counters: Counters,
|
|
261
|
+
results: Generator[tuple[int, str, str | None, str], Any, None],
|
|
262
|
+
):
|
|
263
|
+
for outcome, test_id, message, tempdir in results:
|
|
264
|
+
counters.report(outcome, test_id, message)
|
|
265
|
+
shutil.rmtree(tempdir, ignore_errors=True)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _enum_tests(test_root: Path, data_dir: Path | None):
|
|
269
|
+
test_files: list[Path] = []
|
|
270
|
+
for root, _, files in test_root.walk():
|
|
271
|
+
for filename in files:
|
|
272
|
+
if not Path(filename).suffix in [".json", ".yaml", ".yml"]:
|
|
273
|
+
continue
|
|
274
|
+
test_path = root / filename
|
|
275
|
+
if data_dir and test_path.is_relative_to(data_dir):
|
|
276
|
+
continue
|
|
277
|
+
test_files.append(test_path)
|
|
278
|
+
|
|
279
|
+
return test_files
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _load_tests(testsuite: list[Path], run: list[int]):
|
|
283
|
+
counter: int = 0
|
|
284
|
+
independent_tests: list[tuple[Test, int]] = []
|
|
285
|
+
linear_tests: list[tuple[Test, int]] = []
|
|
286
|
+
|
|
287
|
+
for filename in sorted(testsuite):
|
|
288
|
+
counter += 1
|
|
289
|
+
if counter not in run:
|
|
290
|
+
continue
|
|
291
|
+
|
|
292
|
+
try:
|
|
293
|
+
test = Test.load(filename, counter)
|
|
294
|
+
if not test.ok:
|
|
295
|
+
continue
|
|
296
|
+
except Exception:
|
|
297
|
+
print(">>>>>>", filename.as_posix(), file=sys.stderr)
|
|
298
|
+
raise
|
|
299
|
+
|
|
300
|
+
if test.linear:
|
|
301
|
+
linear_tests.append((test, counter))
|
|
302
|
+
else:
|
|
303
|
+
independent_tests.append((test, counter))
|
|
304
|
+
|
|
305
|
+
return independent_tests, linear_tests
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _cmake_executable(binary_dir: str | PathLike[str]):
|
|
309
|
+
cache = Path(binary_dir) / "CMakeCache.txt"
|
|
310
|
+
if not cache.is_file():
|
|
311
|
+
return "cmake"
|
|
312
|
+
|
|
313
|
+
with cache.open(encoding="UTF-8") as cache_file:
|
|
314
|
+
for line in cache_file:
|
|
315
|
+
line = line.strip()
|
|
316
|
+
if not line.startswith("CMAKE_COMMAND:INTERNAL="):
|
|
317
|
+
continue
|
|
318
|
+
return line.split("=", 1)[1]
|
|
319
|
+
|
|
320
|
+
return "cmake"
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _run_with(test: Test, *args: str):
|
|
324
|
+
cwd = None if test.linear else test.cwd
|
|
325
|
+
subprocess.run(args, shell=False, cwd=cwd)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _target(target: str):
|
|
329
|
+
def impl(test: Test, aditional: list[str]):
|
|
330
|
+
_run_with(test, target, *aditional)
|
|
331
|
+
|
|
332
|
+
return impl
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _make_env(
|
|
336
|
+
target: Path,
|
|
337
|
+
data_dir: Path,
|
|
338
|
+
version: str,
|
|
339
|
+
counter_total: int,
|
|
340
|
+
patches: dict[str, str],
|
|
341
|
+
env_prefix: str | None,
|
|
342
|
+
):
|
|
343
|
+
target_name = target.stem if os.name == "nt" else target.name
|
|
344
|
+
tempdir = (Path(tempfile.gettempdir()) / "test-runner").resolve()
|
|
345
|
+
tempdir_alt = None
|
|
346
|
+
data_dir_alt = None
|
|
347
|
+
|
|
348
|
+
if os.sep != "/":
|
|
349
|
+
tempdir_alt = str(tempdir)
|
|
350
|
+
data_dir_alt = str(data_dir)
|
|
351
|
+
|
|
352
|
+
length = counter_total
|
|
353
|
+
digits = 1
|
|
354
|
+
while length > 9:
|
|
355
|
+
digits += 1
|
|
356
|
+
length = length // 10
|
|
357
|
+
|
|
358
|
+
return Env(
|
|
359
|
+
target=str(target),
|
|
360
|
+
target_name=target_name,
|
|
361
|
+
build_dir=str(target.parent.parent),
|
|
362
|
+
data_dir=data_dir.as_posix(),
|
|
363
|
+
inst_dir=str(data_dir.parent / "copy" / "bin"),
|
|
364
|
+
tempdir=tempdir.as_posix(),
|
|
365
|
+
version=version,
|
|
366
|
+
counter_digits=digits,
|
|
367
|
+
counter_total=counter_total,
|
|
368
|
+
handlers=HANDLERS,
|
|
369
|
+
data_dir_alt=data_dir_alt,
|
|
370
|
+
tempdir_alt=tempdir_alt,
|
|
371
|
+
builtin_patches=patches,
|
|
372
|
+
reportable_env_prefix=env_prefix,
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _install(
|
|
377
|
+
dst: str | PathLike[str],
|
|
378
|
+
binary_dir: str | PathLike[str],
|
|
379
|
+
build_type: str,
|
|
380
|
+
components: list[str],
|
|
381
|
+
env: Env,
|
|
382
|
+
):
|
|
383
|
+
dst = Path(dst)
|
|
384
|
+
shutil.rmtree(dst, ignore_errors=True)
|
|
385
|
+
dst.mkdir(parents=True, exist_ok=True)
|
|
386
|
+
|
|
387
|
+
args = [
|
|
388
|
+
_cmake_executable(binary_dir),
|
|
389
|
+
"--install",
|
|
390
|
+
str(binary_dir),
|
|
391
|
+
"--config",
|
|
392
|
+
build_type,
|
|
393
|
+
"--prefix",
|
|
394
|
+
str(dst),
|
|
395
|
+
]
|
|
396
|
+
|
|
397
|
+
if not components:
|
|
398
|
+
proc = subprocess.run(args, capture_output=True)
|
|
399
|
+
return proc.returncode == 0
|
|
400
|
+
|
|
401
|
+
args.append("--component")
|
|
402
|
+
args.append("")
|
|
403
|
+
|
|
404
|
+
for component in components:
|
|
405
|
+
args[-1] = component
|
|
406
|
+
proc = subprocess.run(args, capture_output=True)
|
|
407
|
+
if proc.returncode != 0:
|
|
408
|
+
return False
|
|
409
|
+
|
|
410
|
+
env.target = str(dst / "bin" / os.path.basename(env.target))
|
|
411
|
+
target_name = os.path.split(env.target)[1]
|
|
412
|
+
if os.name == "nt":
|
|
413
|
+
target_name = os.path.splitext(target_name)[0]
|
|
414
|
+
env.handlers[target_name] = (0, _target(env.target))
|
|
415
|
+
|
|
416
|
+
return True
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Copyright (c) 2026 Marcin Zdun
|
|
2
|
+
# This code is licensed under MIT license (see LICENSE for details)
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import stat
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from typing import Callable
|
|
9
|
+
|
|
10
|
+
from proj_flow.ext.test_runner.driver import test
|
|
11
|
+
from proj_flow.ext.test_runner.utils.archives import locate_unpack
|
|
12
|
+
|
|
13
|
+
_file_cache = {}
|
|
14
|
+
_rw_mask = stat.S_IWRITE | stat.S_IWGRP | stat.S_IWOTH
|
|
15
|
+
_ro_mask = 0o777 ^ _rw_mask
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _touch(test: test.Test, args: list[str]):
|
|
19
|
+
filename = test.path(args[0])
|
|
20
|
+
os.makedirs(os.path.dirname(filename), exist_ok=True)
|
|
21
|
+
with open(filename, "wb") as f:
|
|
22
|
+
if len(args) > 1:
|
|
23
|
+
f.write(args[1].encode("UTF-8"))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _make_RO(test: test.Test, args: list[str]):
|
|
27
|
+
filename = test.path(args[0])
|
|
28
|
+
mode = os.stat(filename).st_mode
|
|
29
|
+
_file_cache[filename] = mode
|
|
30
|
+
os.chmod(filename, mode & _ro_mask)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _make_RW(test: test.Test, args: list[str]):
|
|
34
|
+
filename = test.path(args[0])
|
|
35
|
+
try:
|
|
36
|
+
mode = _file_cache[filename]
|
|
37
|
+
except KeyError:
|
|
38
|
+
mode = os.stat(filename).st_mode | _rw_mask
|
|
39
|
+
os.chmod(filename, mode)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _unpack(test: test.Test, args: list[str]):
|
|
43
|
+
archive = args[0]
|
|
44
|
+
dst = args[1]
|
|
45
|
+
unpack = locate_unpack(archive)[0]
|
|
46
|
+
unpack(archive, test.path(dst))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _cat(test: test.Test, args: list[str]):
|
|
50
|
+
filename = args[0]
|
|
51
|
+
with open(test.path(filename)) as f:
|
|
52
|
+
sys.stdout.write(f.read())
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _shell(test: test.Test, args: list[str]):
|
|
56
|
+
print("shell!!!")
|
|
57
|
+
print("target:", test.current_env.target if test.current_env is not None else "?")
|
|
58
|
+
subprocess.call("pwsh" if os.name == "nt" else "bash", shell=True, cwd=test.cwd)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
HANDLERS: dict[str, tuple[int, Callable[[test.Test, list[str]], None]]] = {
|
|
62
|
+
"mkdirs": (1, lambda test, args: test.makedirs(args[0])),
|
|
63
|
+
"cd": (1, lambda test, args: test.chdir(args[0])),
|
|
64
|
+
"rm": (1, lambda test, args: test.rmtree(args[0])),
|
|
65
|
+
"touch": (1, _touch),
|
|
66
|
+
"unpack": (2, _unpack),
|
|
67
|
+
"store": (2, lambda test, args: test.store_output(args[0], args[1:])),
|
|
68
|
+
"ro": (1, _make_RO),
|
|
69
|
+
"cp": (2, lambda test, args: test.cp(args[0], args[1])),
|
|
70
|
+
"rw": (1, _make_RW),
|
|
71
|
+
"ls": (1, lambda test, args: test.ls(args[0])),
|
|
72
|
+
"cat": (1, _cat),
|
|
73
|
+
"shell": (0, _shell),
|
|
74
|
+
}
|