gha-utils 4.14.0__tar.gz → 4.14.2__tar.gz
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.
Potentially problematic release.
This version of gha-utils might be problematic. Click here for more details.
- {gha_utils-4.14.0 → gha_utils-4.14.2}/PKG-INFO +6 -3
- {gha_utils-4.14.0 → gha_utils-4.14.2}/gha_utils/__init__.py +1 -1
- {gha_utils-4.14.0 → gha_utils-4.14.2}/gha_utils/cli.py +21 -12
- {gha_utils-4.14.0 → gha_utils-4.14.2}/gha_utils/metadata.py +1 -1
- gha_utils-4.14.2/gha_utils/test_plan.py +274 -0
- {gha_utils-4.14.0 → gha_utils-4.14.2}/gha_utils.egg-info/PKG-INFO +6 -3
- {gha_utils-4.14.0 → gha_utils-4.14.2}/pyproject.toml +2 -2
- {gha_utils-4.14.0 → gha_utils-4.14.2}/readme.md +5 -2
- {gha_utils-4.14.0 → gha_utils-4.14.2}/tests/test_mailmap.py +2 -2
- gha_utils-4.14.2/tests/test_metadata.py +143 -0
- gha_utils-4.14.0/gha_utils/test_plan.py +0 -226
- gha_utils-4.14.0/tests/test_metadata.py +0 -89
- {gha_utils-4.14.0 → gha_utils-4.14.2}/gha_utils/__main__.py +0 -0
- {gha_utils-4.14.0 → gha_utils-4.14.2}/gha_utils/changelog.py +0 -0
- {gha_utils-4.14.0 → gha_utils-4.14.2}/gha_utils/mailmap.py +0 -0
- {gha_utils-4.14.0 → gha_utils-4.14.2}/gha_utils/matrix.py +0 -0
- {gha_utils-4.14.0 → gha_utils-4.14.2}/gha_utils/py.typed +0 -0
- {gha_utils-4.14.0 → gha_utils-4.14.2}/gha_utils.egg-info/SOURCES.txt +0 -0
- {gha_utils-4.14.0 → gha_utils-4.14.2}/gha_utils.egg-info/dependency_links.txt +0 -0
- {gha_utils-4.14.0 → gha_utils-4.14.2}/gha_utils.egg-info/entry_points.txt +0 -0
- {gha_utils-4.14.0 → gha_utils-4.14.2}/gha_utils.egg-info/requires.txt +0 -0
- {gha_utils-4.14.0 → gha_utils-4.14.2}/gha_utils.egg-info/top_level.txt +0 -0
- {gha_utils-4.14.0 → gha_utils-4.14.2}/setup.cfg +0 -0
- {gha_utils-4.14.0 → gha_utils-4.14.2}/tests/test_changelog.py +0 -0
- {gha_utils-4.14.0 → gha_utils-4.14.2}/tests/test_matrix.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: gha-utils
|
|
3
|
-
Version: 4.14.
|
|
3
|
+
Version: 4.14.2
|
|
4
4
|
Summary: ⚙️ CLI helpers for GitHub Actions + reuseable workflows
|
|
5
5
|
Author-email: Kevin Deldycke <kevin@deldycke.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/kdeldycke/workflows
|
|
@@ -103,7 +103,6 @@ Thanks to `uv`, you can install and run `gha-utils` in one command, without poll
|
|
|
103
103
|
|
|
104
104
|
```shell-session
|
|
105
105
|
$ uvx gha-utils
|
|
106
|
-
Installed 45 packages in 45ms
|
|
107
106
|
Usage: gha-utils [OPTIONS] COMMAND [ARGS]...
|
|
108
107
|
|
|
109
108
|
Options:
|
|
@@ -118,8 +117,11 @@ Options:
|
|
|
118
117
|
utils/*.{toml,yaml,yml,json,ini,xml}]
|
|
119
118
|
--show-params Show all CLI parameters, their provenance, defaults
|
|
120
119
|
and value, then exit.
|
|
121
|
-
|
|
120
|
+
--verbosity LEVEL Either CRITICAL, ERROR, WARNING, INFO, DEBUG.
|
|
122
121
|
[default: WARNING]
|
|
122
|
+
-v, --verbose Increase the default WARNING verbosity by one level
|
|
123
|
+
for each additional repetition of the option.
|
|
124
|
+
[default: 0]
|
|
123
125
|
--version Show the version and exit.
|
|
124
126
|
-h, --help Show this message and exit.
|
|
125
127
|
|
|
@@ -127,6 +129,7 @@ Commands:
|
|
|
127
129
|
changelog Maintain a Markdown-formatted changelog
|
|
128
130
|
mailmap-sync Update Git's .mailmap file with missing contributors
|
|
129
131
|
metadata Output project metadata
|
|
132
|
+
test-plan Run a test plan from a file against a binary
|
|
130
133
|
```
|
|
131
134
|
|
|
132
135
|
```shell-session
|
|
@@ -27,7 +27,7 @@ import click
|
|
|
27
27
|
from click_extra import (
|
|
28
28
|
Choice,
|
|
29
29
|
Context,
|
|
30
|
-
|
|
30
|
+
FloatRange,
|
|
31
31
|
argument,
|
|
32
32
|
echo,
|
|
33
33
|
extra_group,
|
|
@@ -279,29 +279,38 @@ def mailmap_sync(ctx, source, create_if_missing, destination_mailmap):
|
|
|
279
279
|
# `file_path` type.
|
|
280
280
|
type=click.Path(exists=True, executable=True, resolve_path=True),
|
|
281
281
|
required=True,
|
|
282
|
-
|
|
282
|
+
metavar="FILE_PATH",
|
|
283
|
+
help="Path to the binary file to test.",
|
|
283
284
|
)
|
|
284
285
|
@option(
|
|
285
286
|
"--plan",
|
|
286
287
|
type=file_path(exists=True, readable=True, resolve_path=True),
|
|
287
|
-
|
|
288
|
+
metavar="FILE_PATH",
|
|
289
|
+
help="Path to the test plan file in YAML. If not provided, a default test "
|
|
290
|
+
"plan will be executed.",
|
|
288
291
|
)
|
|
289
292
|
@option(
|
|
290
293
|
"-t",
|
|
291
294
|
"--timeout",
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
+
# Timeout passed to subprocess.run() is a float that is silently clamped to
|
|
296
|
+
# 0.0 is negative values are provided, so we mimic this behavior here:
|
|
297
|
+
# https://github.com/python/cpython/blob/5740b95076b57feb6293cda4f5504f706a7d622d/Lib/subprocess.py#L1596-L1597
|
|
298
|
+
type=FloatRange(min=0, clamp=True),
|
|
299
|
+
metavar="SECONDS",
|
|
300
|
+
help="Set the default timeout for each CLI call, if not specified in the "
|
|
301
|
+
"test plan.",
|
|
295
302
|
)
|
|
296
|
-
def test_plan(binary, plan, timeout):
|
|
303
|
+
def test_plan(binary: Path, plan: Path | None, timeout: float | None) -> None:
|
|
297
304
|
# Load test plan from workflow input, or use a default one.
|
|
298
305
|
if plan:
|
|
299
|
-
logging.
|
|
306
|
+
logging.info(f"Read test plan from {plan}")
|
|
300
307
|
test_plan = parse_test_plan(plan)
|
|
301
308
|
else:
|
|
302
|
-
logging.warning(
|
|
303
|
-
test_plan = DEFAULT_TEST_PLAN
|
|
309
|
+
logging.warning("No test plan provided: use default test plan.")
|
|
310
|
+
test_plan = DEFAULT_TEST_PLAN # type: ignore[assignment]
|
|
311
|
+
logging.debug(f"Test plan: {test_plan}")
|
|
304
312
|
|
|
305
313
|
for index, test_case in enumerate(test_plan):
|
|
306
|
-
logging.info(f"Run test #{index}")
|
|
307
|
-
|
|
314
|
+
logging.info(f"Run test #{index + 1}")
|
|
315
|
+
logging.debug(f"Test case parameters: {test_case}")
|
|
316
|
+
test_case.check_cli_test(binary, default_timeout=timeout)
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# Copyright Kevin Deldycke <kevin@deldycke.com> and contributors.
|
|
2
|
+
#
|
|
3
|
+
# This program is Free Software; you can redistribute it and/or
|
|
4
|
+
# modify it under the terms of the GNU General Public License
|
|
5
|
+
# as published by the Free Software Foundation; either version 2
|
|
6
|
+
# of the License, or (at your option) any later version.
|
|
7
|
+
#
|
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
11
|
+
# GNU General Public License for more details.
|
|
12
|
+
#
|
|
13
|
+
# You should have received a copy of the GNU General Public License
|
|
14
|
+
# along with this program; if not, write to the Free Software
|
|
15
|
+
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
import re
|
|
21
|
+
import shlex
|
|
22
|
+
import sys
|
|
23
|
+
from dataclasses import asdict, dataclass, field
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from subprocess import TimeoutExpired, run
|
|
26
|
+
from typing import Generator, Sequence
|
|
27
|
+
|
|
28
|
+
import yaml
|
|
29
|
+
from boltons.iterutils import flatten
|
|
30
|
+
from boltons.strutils import strip_ansi
|
|
31
|
+
from click_extra.testing import args_cleanup, print_cli_run
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(order=True)
|
|
35
|
+
class TestCase:
|
|
36
|
+
cli_parameters: tuple[str, ...] | str = field(default_factory=tuple)
|
|
37
|
+
"""Parameters, arguments and options to pass to the CLI."""
|
|
38
|
+
|
|
39
|
+
timeout: float | str | None = None
|
|
40
|
+
exit_code: int | str | None = None
|
|
41
|
+
strip_ansi: bool = False
|
|
42
|
+
output_contains: tuple[str, ...] | str = field(default_factory=tuple)
|
|
43
|
+
stdout_contains: tuple[str, ...] | str = field(default_factory=tuple)
|
|
44
|
+
stderr_contains: tuple[str, ...] | str = field(default_factory=tuple)
|
|
45
|
+
output_regex_matches: tuple[re.Pattern | str, ...] | str = field(
|
|
46
|
+
default_factory=tuple
|
|
47
|
+
)
|
|
48
|
+
stdout_regex_matches: tuple[re.Pattern | str, ...] | str = field(
|
|
49
|
+
default_factory=tuple
|
|
50
|
+
)
|
|
51
|
+
stderr_regex_matches: tuple[re.Pattern | str, ...] | str = field(
|
|
52
|
+
default_factory=tuple
|
|
53
|
+
)
|
|
54
|
+
output_regex_fullmatch: re.Pattern | str | None = None
|
|
55
|
+
stdout_regex_fullmatch: re.Pattern | str | None = None
|
|
56
|
+
stderr_regex_fullmatch: re.Pattern | str | None = None
|
|
57
|
+
|
|
58
|
+
def __post_init__(self) -> None:
|
|
59
|
+
"""Normalize all fields."""
|
|
60
|
+
for field_id, field_data in asdict(self).items():
|
|
61
|
+
# Validates and normalize integer properties.
|
|
62
|
+
if field_id == "exit_code":
|
|
63
|
+
if isinstance(field_data, str):
|
|
64
|
+
field_data = int(field_data)
|
|
65
|
+
elif field_data is not None and not isinstance(field_data, int):
|
|
66
|
+
raise ValueError(f"exit_code is not an integer: {field_data}")
|
|
67
|
+
|
|
68
|
+
# Validates and normalize float properties.
|
|
69
|
+
elif field_id == "timeout":
|
|
70
|
+
if isinstance(field_data, str):
|
|
71
|
+
field_data = float(field_data)
|
|
72
|
+
elif field_data is not None and not isinstance(field_data, float):
|
|
73
|
+
raise ValueError(f"timeout is not a float: {field_data}")
|
|
74
|
+
# Timeout can only be unset or positive.
|
|
75
|
+
if field_data and field_data < 0:
|
|
76
|
+
raise ValueError(f"timeout is negative: {field_data}")
|
|
77
|
+
|
|
78
|
+
# Validates and normalize boolean properties.
|
|
79
|
+
elif field_id == "strip_ansi":
|
|
80
|
+
if not isinstance(field_data, bool):
|
|
81
|
+
raise ValueError(f"strip_ansi is not a boolean: {field_data}")
|
|
82
|
+
|
|
83
|
+
# Validates and normalize tuple of strings.
|
|
84
|
+
else:
|
|
85
|
+
if field_data:
|
|
86
|
+
# Wraps single string and other types into a tuple.
|
|
87
|
+
if isinstance(field_data, str) or not isinstance(
|
|
88
|
+
field_data, Sequence
|
|
89
|
+
):
|
|
90
|
+
# CLI parameters needs to be split on Unix-like systems.
|
|
91
|
+
# XXX If we need the same for Windows, have a look at:
|
|
92
|
+
# https://github.com/maxpat78/w32lex
|
|
93
|
+
if field_id == "cli_parameters" and sys.platform != "win32":
|
|
94
|
+
field_data = tuple(shlex.split(field_data))
|
|
95
|
+
else:
|
|
96
|
+
field_data = (field_data,)
|
|
97
|
+
|
|
98
|
+
for item in field_data:
|
|
99
|
+
if not isinstance(item, str):
|
|
100
|
+
raise ValueError(f"Invalid string in {field_id}: {item}")
|
|
101
|
+
# Ignore blank value.
|
|
102
|
+
field_data = tuple(i for i in field_data if i.strip())
|
|
103
|
+
|
|
104
|
+
# Validates fields containing one or more regexes.
|
|
105
|
+
if "_regex_" in field_id and field_data:
|
|
106
|
+
# Compile all regexes.
|
|
107
|
+
valid_regexes = []
|
|
108
|
+
for regex in flatten((field_data,)):
|
|
109
|
+
try:
|
|
110
|
+
# Let dots in regex match newlines.
|
|
111
|
+
valid_regexes.append(re.compile(regex, re.DOTALL))
|
|
112
|
+
except re.error as ex:
|
|
113
|
+
raise ValueError(
|
|
114
|
+
f"Invalid regex in {field_id}: {regex}"
|
|
115
|
+
) from ex
|
|
116
|
+
# Normalize single regex to a single element.
|
|
117
|
+
if field_id.endswith("_fullmatch"):
|
|
118
|
+
if valid_regexes:
|
|
119
|
+
field_data = valid_regexes.pop()
|
|
120
|
+
else:
|
|
121
|
+
field_data = None
|
|
122
|
+
else:
|
|
123
|
+
field_data = tuple(valid_regexes)
|
|
124
|
+
|
|
125
|
+
setattr(self, field_id, field_data)
|
|
126
|
+
|
|
127
|
+
def check_cli_test(self, binary: str | Path, default_timeout: float | None):
|
|
128
|
+
"""Run a CLI command and check its output against the test case.
|
|
129
|
+
|
|
130
|
+
..todo::
|
|
131
|
+
Add support for environment variables.
|
|
132
|
+
|
|
133
|
+
..todo::
|
|
134
|
+
Add support for proper mixed stdout/stderr stream as a single,
|
|
135
|
+
intertwined output.
|
|
136
|
+
"""
|
|
137
|
+
if self.timeout is None and default_timeout is not None:
|
|
138
|
+
logging.info(f"Set default test case timeout to {default_timeout} seconds")
|
|
139
|
+
self.timeout = default_timeout
|
|
140
|
+
|
|
141
|
+
clean_args = args_cleanup(binary, self.cli_parameters)
|
|
142
|
+
try:
|
|
143
|
+
result = run(
|
|
144
|
+
clean_args,
|
|
145
|
+
capture_output=True,
|
|
146
|
+
timeout=self.timeout, # type: ignore[arg-type]
|
|
147
|
+
# XXX Do not force encoding to let CLIs figure out by
|
|
148
|
+
# themselves the contextual encoding to use. This avoid
|
|
149
|
+
# UnicodeDecodeError on output in Window's console which still
|
|
150
|
+
# defaults to legacy encoding (e.g. cp1252, cp932, etc...):
|
|
151
|
+
#
|
|
152
|
+
# Traceback (most recent call last):
|
|
153
|
+
# File "…\__main__.py", line 49, in <module>
|
|
154
|
+
# File "…\__main__.py", line 45, in main
|
|
155
|
+
# File "…\click\core.py", line 1157, in __call__
|
|
156
|
+
# File "…\click_extra\commands.py", line 347, in main
|
|
157
|
+
# File "…\click\core.py", line 1078, in main
|
|
158
|
+
# File "…\click_extra\commands.py", line 377, in invoke
|
|
159
|
+
# File "…\click\core.py", line 1688, in invoke
|
|
160
|
+
# File "…\click_extra\commands.py", line 377, in invoke
|
|
161
|
+
# File "…\click\core.py", line 1434, in invoke
|
|
162
|
+
# File "…\click\core.py", line 783, in invoke
|
|
163
|
+
# File "…\cloup\_context.py", line 47, in new_func
|
|
164
|
+
# File "…\mpm\cli.py", line 570, in managers
|
|
165
|
+
# File "…\mpm\output.py", line 187, in print_table
|
|
166
|
+
# File "…\click_extra\tabulate.py", line 97, in render_csv
|
|
167
|
+
# File "encodings\cp1252.py", line 19, in encode
|
|
168
|
+
# UnicodeEncodeError: 'charmap' codec can't encode character
|
|
169
|
+
# '\u2713' in position 128: character maps to <undefined>
|
|
170
|
+
#
|
|
171
|
+
# encoding="utf-8",
|
|
172
|
+
text=True,
|
|
173
|
+
)
|
|
174
|
+
except TimeoutExpired:
|
|
175
|
+
raise TimeoutError(
|
|
176
|
+
f"CLI timed out after {self.timeout} seconds: {' '.join(clean_args)}"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
print_cli_run(clean_args, result)
|
|
180
|
+
|
|
181
|
+
for field_id, field_data in asdict(self).items():
|
|
182
|
+
if field_id == "exit_code":
|
|
183
|
+
if field_data is not None:
|
|
184
|
+
logging.info(f"Test exit code, expecting: {field_data}")
|
|
185
|
+
if result.returncode != field_data:
|
|
186
|
+
raise AssertionError(
|
|
187
|
+
f"CLI exited with code {result.returncode}, "
|
|
188
|
+
f"expected {field_data}"
|
|
189
|
+
)
|
|
190
|
+
# The specific exit code matches, let's proceed to the next test.
|
|
191
|
+
continue
|
|
192
|
+
|
|
193
|
+
# Ignore non-output fields, and empty test cases.
|
|
194
|
+
elif not (
|
|
195
|
+
field_id.startswith(("output_", "stdout_", "stderr_")) and field_data
|
|
196
|
+
):
|
|
197
|
+
continue
|
|
198
|
+
|
|
199
|
+
# Prepare output and name for comparison.
|
|
200
|
+
output = ""
|
|
201
|
+
name = ""
|
|
202
|
+
if field_id.startswith("output_"):
|
|
203
|
+
raise NotImplementedError("<stdout>/<stderr> output mix")
|
|
204
|
+
# output = result.output
|
|
205
|
+
# name = "output"
|
|
206
|
+
elif field_id.startswith("stdout_"):
|
|
207
|
+
output = result.stdout
|
|
208
|
+
name = "<stdout>"
|
|
209
|
+
elif field_id.startswith("stderr_"):
|
|
210
|
+
output = result.stderr
|
|
211
|
+
name = "<stderr>"
|
|
212
|
+
|
|
213
|
+
if self.strip_ansi:
|
|
214
|
+
logging.info(f"Strip ANSI escape sequences from CLI's {name}")
|
|
215
|
+
output = strip_ansi(output)
|
|
216
|
+
|
|
217
|
+
if field_id.endswith("_contains"):
|
|
218
|
+
for sub_string in field_data:
|
|
219
|
+
logging.info(f"Check if CLI's {name} contains: {sub_string!r}")
|
|
220
|
+
if sub_string not in output:
|
|
221
|
+
raise AssertionError(
|
|
222
|
+
f"CLI's {name} does not contain {sub_string!r}"
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
elif field_id.endswith("_regex_matches"):
|
|
226
|
+
for regex in field_data:
|
|
227
|
+
logging.info(f"Check if CLI's {name} matches: {sub_string!r}")
|
|
228
|
+
if not regex.search(output):
|
|
229
|
+
raise AssertionError(
|
|
230
|
+
f"CLI's {name} does not match regex {regex}"
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
elif field_id.endswith("_regex_fullmatch"):
|
|
234
|
+
regex = field_data
|
|
235
|
+
if not regex.fullmatch(output):
|
|
236
|
+
raise AssertionError(
|
|
237
|
+
f"CLI's {name} does not fully match regex {regex}"
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
logging.info("All tests passed for CLI.")
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
DEFAULT_TEST_PLAN = (
|
|
244
|
+
# Output the version of the CLI.
|
|
245
|
+
TestCase(cli_parameters="--version"),
|
|
246
|
+
# Test combination of version and verbosity.
|
|
247
|
+
TestCase(cli_parameters=("--verbosity", "DEBUG", "--version")),
|
|
248
|
+
# Test help output.
|
|
249
|
+
TestCase(cli_parameters="--help"),
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def parse_test_plan(plan_path: Path) -> Generator[TestCase, None, None]:
|
|
254
|
+
plan = yaml.full_load(plan_path.read_text(encoding="UTF-8"))
|
|
255
|
+
|
|
256
|
+
# Validates test plan structure.
|
|
257
|
+
if not plan:
|
|
258
|
+
raise ValueError(f"Empty test plan file {plan_path}")
|
|
259
|
+
if not isinstance(plan, list):
|
|
260
|
+
raise ValueError(f"Test plan is not a list: {plan}")
|
|
261
|
+
|
|
262
|
+
directives = frozenset(TestCase.__dataclass_fields__.keys())
|
|
263
|
+
|
|
264
|
+
for index, test_case in enumerate(plan):
|
|
265
|
+
# Validates test case structure.
|
|
266
|
+
if not isinstance(test_case, dict):
|
|
267
|
+
raise ValueError(f"Test case #{index + 1} is not a dict: {test_case}")
|
|
268
|
+
if not directives.issuperset(test_case):
|
|
269
|
+
raise ValueError(
|
|
270
|
+
f"Test case #{index + 1} contains invalid directives:"
|
|
271
|
+
f"{set(test_case) - directives}"
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
yield TestCase(**test_case)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: gha-utils
|
|
3
|
-
Version: 4.14.
|
|
3
|
+
Version: 4.14.2
|
|
4
4
|
Summary: ⚙️ CLI helpers for GitHub Actions + reuseable workflows
|
|
5
5
|
Author-email: Kevin Deldycke <kevin@deldycke.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/kdeldycke/workflows
|
|
@@ -103,7 +103,6 @@ Thanks to `uv`, you can install and run `gha-utils` in one command, without poll
|
|
|
103
103
|
|
|
104
104
|
```shell-session
|
|
105
105
|
$ uvx gha-utils
|
|
106
|
-
Installed 45 packages in 45ms
|
|
107
106
|
Usage: gha-utils [OPTIONS] COMMAND [ARGS]...
|
|
108
107
|
|
|
109
108
|
Options:
|
|
@@ -118,8 +117,11 @@ Options:
|
|
|
118
117
|
utils/*.{toml,yaml,yml,json,ini,xml}]
|
|
119
118
|
--show-params Show all CLI parameters, their provenance, defaults
|
|
120
119
|
and value, then exit.
|
|
121
|
-
|
|
120
|
+
--verbosity LEVEL Either CRITICAL, ERROR, WARNING, INFO, DEBUG.
|
|
122
121
|
[default: WARNING]
|
|
122
|
+
-v, --verbose Increase the default WARNING verbosity by one level
|
|
123
|
+
for each additional repetition of the option.
|
|
124
|
+
[default: 0]
|
|
123
125
|
--version Show the version and exit.
|
|
124
126
|
-h, --help Show this message and exit.
|
|
125
127
|
|
|
@@ -127,6 +129,7 @@ Commands:
|
|
|
127
129
|
changelog Maintain a Markdown-formatted changelog
|
|
128
130
|
mailmap-sync Update Git's .mailmap file with missing contributors
|
|
129
131
|
metadata Output project metadata
|
|
132
|
+
test-plan Run a test plan from a file against a binary
|
|
130
133
|
```
|
|
131
134
|
|
|
132
135
|
```shell-session
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
# Docs: https://packaging.python.org/en/latest/guides/writing-pyproject-toml/
|
|
3
3
|
name = "gha-utils"
|
|
4
|
-
version = "4.14.
|
|
4
|
+
version = "4.14.2"
|
|
5
5
|
# Python versions and their status: https://devguide.python.org/versions/
|
|
6
6
|
requires-python = ">= 3.10"
|
|
7
7
|
description = "⚙️ CLI helpers for GitHub Actions + reuseable workflows"
|
|
@@ -138,7 +138,7 @@ addopts = [
|
|
|
138
138
|
xfail_strict = true
|
|
139
139
|
|
|
140
140
|
[tool.bumpversion]
|
|
141
|
-
current_version = "4.14.
|
|
141
|
+
current_version = "4.14.2"
|
|
142
142
|
allow_dirty = true
|
|
143
143
|
ignore_missing_files = true
|
|
144
144
|
|
|
@@ -36,7 +36,6 @@ Thanks to `uv`, you can install and run `gha-utils` in one command, without poll
|
|
|
36
36
|
|
|
37
37
|
```shell-session
|
|
38
38
|
$ uvx gha-utils
|
|
39
|
-
Installed 45 packages in 45ms
|
|
40
39
|
Usage: gha-utils [OPTIONS] COMMAND [ARGS]...
|
|
41
40
|
|
|
42
41
|
Options:
|
|
@@ -51,8 +50,11 @@ Options:
|
|
|
51
50
|
utils/*.{toml,yaml,yml,json,ini,xml}]
|
|
52
51
|
--show-params Show all CLI parameters, their provenance, defaults
|
|
53
52
|
and value, then exit.
|
|
54
|
-
|
|
53
|
+
--verbosity LEVEL Either CRITICAL, ERROR, WARNING, INFO, DEBUG.
|
|
55
54
|
[default: WARNING]
|
|
55
|
+
-v, --verbose Increase the default WARNING verbosity by one level
|
|
56
|
+
for each additional repetition of the option.
|
|
57
|
+
[default: 0]
|
|
56
58
|
--version Show the version and exit.
|
|
57
59
|
-h, --help Show this message and exit.
|
|
58
60
|
|
|
@@ -60,6 +62,7 @@ Commands:
|
|
|
60
62
|
changelog Maintain a Markdown-formatted changelog
|
|
61
63
|
mailmap-sync Update Git's .mailmap file with missing contributors
|
|
62
64
|
metadata Output project metadata
|
|
65
|
+
test-plan Run a test plan from a file against a binary
|
|
63
66
|
```
|
|
64
67
|
|
|
65
68
|
```shell-session
|
|
@@ -27,7 +27,7 @@ def test_remove_header():
|
|
|
27
27
|
# Generated by gha-utils mailmap-sync v4.4.3 - https://github.com/kdeldycke/workflows
|
|
28
28
|
# Timestamp: 2024-08-12T08:15:41.083405
|
|
29
29
|
# Format is:
|
|
30
|
-
# Preferred Name <preferred e-mail>
|
|
30
|
+
# Preferred Name <preferred e-mail> Other Name <other e-mail>
|
|
31
31
|
#
|
|
32
32
|
# Reference: https://git-scm.com/docs/git-blame#_mapping_authors
|
|
33
33
|
|
|
@@ -36,7 +36,7 @@ def test_remove_header():
|
|
|
36
36
|
|
|
37
37
|
assert remove_header(content) == dedent("""\
|
|
38
38
|
# Format is:
|
|
39
|
-
# Preferred Name <preferred e-mail>
|
|
39
|
+
# Preferred Name <preferred e-mail> Other Name <other e-mail>
|
|
40
40
|
#
|
|
41
41
|
# Reference: https://git-scm.com/docs/git-blame#_mapping_authors
|
|
42
42
|
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# Copyright Kevin Deldycke <kevin@deldycke.com> and contributors.
|
|
2
|
+
#
|
|
3
|
+
# This program is Free Software; you can redistribute it and/or
|
|
4
|
+
# modify it under the terms of the GNU General Public License
|
|
5
|
+
# as published by the Free Software Foundation; either version 2
|
|
6
|
+
# of the License, or (at your option) any later version.
|
|
7
|
+
#
|
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
11
|
+
# GNU General Public License for more details.
|
|
12
|
+
#
|
|
13
|
+
# You should have received a copy of the GNU General Public License
|
|
14
|
+
# along with this program; if not, write to the Free Software
|
|
15
|
+
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import re
|
|
20
|
+
|
|
21
|
+
from gha_utils.metadata import Dialects, Metadata
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_metadata_github_format():
|
|
25
|
+
metadata = Metadata()
|
|
26
|
+
|
|
27
|
+
assert re.fullmatch(
|
|
28
|
+
(
|
|
29
|
+
r"new_commits=\n"
|
|
30
|
+
r"release_commits=\n"
|
|
31
|
+
r"gitignore_exists=true\n"
|
|
32
|
+
r"python_files=[\S ]*\n"
|
|
33
|
+
r"doc_files=[\S ]*\n"
|
|
34
|
+
r"is_python_project=true\n"
|
|
35
|
+
r"package_name=gha-utils\n"
|
|
36
|
+
r"blacken_docs_params=--target-version py310 --target-version py311 "
|
|
37
|
+
r"--target-version py312 --target-version py313\n"
|
|
38
|
+
r"mypy_params=--python-version 3\.10\n"
|
|
39
|
+
r"current_version=\n"
|
|
40
|
+
r"released_version=\n"
|
|
41
|
+
r"is_sphinx=false\n"
|
|
42
|
+
r"active_autodoc=false\n"
|
|
43
|
+
r"release_notes=\n"
|
|
44
|
+
r"new_commits_matrix=\n"
|
|
45
|
+
r"release_commits_matrix=\n"
|
|
46
|
+
r'nuitka_matrix=\{"os": \["ubuntu-24\.04", "ubuntu-24\.04-arm", '
|
|
47
|
+
r'"macos-15", "macos-13", "windows-2022"\], '
|
|
48
|
+
r'"entry_point": \["gha-utils"\], "commit": \["[a-z0-9]+"\], '
|
|
49
|
+
r'"include": \[\{"entry_point": "gha-utils", '
|
|
50
|
+
r'"cli_id": "gha-utils", "module_id": "gha_utils\.__main__", '
|
|
51
|
+
r'"callable_id": "main", '
|
|
52
|
+
r'"module_path": "gha_utils(/|\\\\)__main__\.py"\}, '
|
|
53
|
+
r'\{"commit": "[a-z0-9]+", "short_sha": "[a-z0-9]+", '
|
|
54
|
+
r'"current_version": "[0-9\.]+"\}, \{"os": "ubuntu-24\.04", '
|
|
55
|
+
r'"platform_id": "linux", "arch": "x64", "extension": "bin"\}, '
|
|
56
|
+
r'\{"os": "ubuntu-24\.04-arm", "platform_id": "linux", '
|
|
57
|
+
r'"arch": "arm64", "extension": "bin"\}, \{"os": "macos-15", '
|
|
58
|
+
r'"platform_id": "macos", "arch": "arm64", "extension": "bin"\}, '
|
|
59
|
+
r'\{"os": "macos-13", "platform_id": "macos", "arch": "x64", '
|
|
60
|
+
r'"extension": "bin"\}, \{"os": "windows-2022", '
|
|
61
|
+
r'"platform_id": "windows", "arch": "x64", "extension": "exe"\}, '
|
|
62
|
+
r'\{"os": "ubuntu-24\.04", "entry_point": "gha-utils", '
|
|
63
|
+
r'"commit": "[a-z0-9]+", '
|
|
64
|
+
r'"bin_name": "gha-utils-linux-x64-build-[a-z0-9]+\.bin"\}, '
|
|
65
|
+
r'\{"os": "ubuntu-24\.04-arm", "entry_point": "gha-utils", '
|
|
66
|
+
r'"commit": "[a-z0-9]+", '
|
|
67
|
+
r'"bin_name": "gha-utils-linux-arm64-build-[a-z0-9]+\.bin"\}, '
|
|
68
|
+
r'\{"os": "macos-15", "entry_point": "gha-utils", '
|
|
69
|
+
r'"commit": "[a-z0-9]+", '
|
|
70
|
+
r'"bin_name": "gha-utils-macos-arm64-build-[a-z0-9]+\.bin"\}, '
|
|
71
|
+
r'\{"os": "macos-13", "entry_point": "gha-utils", '
|
|
72
|
+
r'"commit": "[a-z0-9]+", '
|
|
73
|
+
r'"bin_name": "gha-utils-macos-x64-build-[a-z0-9]+\.bin"\}, '
|
|
74
|
+
r'\{"os": "windows-2022", "entry_point": "gha-utils", '
|
|
75
|
+
r'"commit": "[a-z0-9]+", '
|
|
76
|
+
r'"bin_name": "gha-utils-windows-x64-build-[a-z0-9]+\.exe"\}\]\}\n'
|
|
77
|
+
),
|
|
78
|
+
metadata.dump(Dialects.github),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_metadata_plain_format():
|
|
83
|
+
metadata = Metadata()
|
|
84
|
+
|
|
85
|
+
assert re.fullmatch(
|
|
86
|
+
(
|
|
87
|
+
r"\{"
|
|
88
|
+
r"'new_commits': None, "
|
|
89
|
+
r"'release_commits': None, "
|
|
90
|
+
r"'gitignore_exists': True, "
|
|
91
|
+
r"'python_files': <generator object Metadata\.python_files at \S+>, "
|
|
92
|
+
r"'doc_files': <generator object Metadata\.doc_files at \S+>, "
|
|
93
|
+
r"'is_python_project': True, "
|
|
94
|
+
r"'package_name': 'gha-utils', "
|
|
95
|
+
r"'blacken_docs_params': \("
|
|
96
|
+
r"'--target-version py310', "
|
|
97
|
+
r"'--target-version py311', "
|
|
98
|
+
r"'--target-version py312', "
|
|
99
|
+
r"'--target-version py313'\), "
|
|
100
|
+
r"'mypy_params': '--python-version 3\.10', "
|
|
101
|
+
r"'current_version': None, "
|
|
102
|
+
r"'released_version': None, "
|
|
103
|
+
r"'is_sphinx': False, "
|
|
104
|
+
r"'active_autodoc': False, "
|
|
105
|
+
r"'release_notes': None, "
|
|
106
|
+
r"'new_commits_matrix': None, "
|
|
107
|
+
r"'release_commits_matrix': None, "
|
|
108
|
+
r"'nuitka_matrix': <Matrix: \{"
|
|
109
|
+
r"'os': \('ubuntu-24\.04', 'ubuntu-24\.04-arm', "
|
|
110
|
+
r"'macos-15', 'macos-13', 'windows-2022'\), "
|
|
111
|
+
r"'entry_point': \('gha-utils',\), "
|
|
112
|
+
r"'commit': \('[a-z0-9]+',\)\}; "
|
|
113
|
+
r"include=\(\{'entry_point': 'gha-utils', 'cli_id': 'gha-utils', "
|
|
114
|
+
r"'module_id': 'gha_utils\.__main__', 'callable_id': 'main', "
|
|
115
|
+
r"'module_path': 'gha_utils(/|\\\\)__main__\.py'\}, "
|
|
116
|
+
r"\{'commit': '[a-z0-9]+', 'short_sha': '[a-z0-9]+', "
|
|
117
|
+
r"'current_version': '[0-9\.]+'\}, \{'os': 'ubuntu-24\.04', "
|
|
118
|
+
r"'platform_id': 'linux', 'arch': 'x64', 'extension': 'bin'}, "
|
|
119
|
+
r"{'os': 'ubuntu-24\.04-arm', 'platform_id': 'linux', "
|
|
120
|
+
r"'arch': 'arm64', 'extension': 'bin'\}, \{'os': 'macos-15', "
|
|
121
|
+
r"'platform_id': 'macos', 'arch': 'arm64', 'extension': 'bin'\}, "
|
|
122
|
+
r"\{'os': 'macos-13', 'platform_id': 'macos', 'arch': 'x64', "
|
|
123
|
+
r"'extension': 'bin'\}, \{'os': 'windows-2022', 'platform_id': "
|
|
124
|
+
r"'windows', 'arch': 'x64', 'extension': 'exe'\}, "
|
|
125
|
+
r"\{'os': 'ubuntu-24\.04', 'entry_point': 'gha-utils', "
|
|
126
|
+
r"'commit': '[a-z0-9]+', "
|
|
127
|
+
r"'bin_name': 'gha-utils-linux-x64-build-[a-z0-9]+\.bin'\}, "
|
|
128
|
+
r"\{'os': 'ubuntu-24\.04-arm', 'entry_point': 'gha-utils', "
|
|
129
|
+
r"'commit': '[a-z0-9]+', "
|
|
130
|
+
r"'bin_name': 'gha-utils-linux-arm64-build-[a-z0-9]+\.bin'\}, "
|
|
131
|
+
r"\{'os': 'macos-15', 'entry_point': 'gha-utils', "
|
|
132
|
+
r"'commit': '[a-z0-9]+', "
|
|
133
|
+
r"'bin_name': 'gha-utils-macos-arm64-build-[a-z0-9]+\.bin'\}, "
|
|
134
|
+
r"\{'os': 'macos-13', 'entry_point': 'gha-utils', "
|
|
135
|
+
r"'commit': '[a-z0-9]+', 'bin_name': "
|
|
136
|
+
r"'gha-utils-macos-x64-build-[a-z0-9]+\.bin'\}, "
|
|
137
|
+
r"\{'os': 'windows-2022', 'entry_point': 'gha-utils', "
|
|
138
|
+
r"'commit': '[a-z0-9]+', "
|
|
139
|
+
r"'bin_name': 'gha-utils-windows-x64-build-[a-z0-9]+\.exe'\}\); "
|
|
140
|
+
r"exclude=\(\)>\}"
|
|
141
|
+
),
|
|
142
|
+
metadata.dump(Dialects.plain),
|
|
143
|
+
)
|
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
# Copyright Kevin Deldycke <kevin@deldycke.com> and contributors.
|
|
2
|
-
#
|
|
3
|
-
# This program is Free Software; you can redistribute it and/or
|
|
4
|
-
# modify it under the terms of the GNU General Public License
|
|
5
|
-
# as published by the Free Software Foundation; either version 2
|
|
6
|
-
# of the License, or (at your option) any later version.
|
|
7
|
-
#
|
|
8
|
-
# This program is distributed in the hope that it will be useful,
|
|
9
|
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
10
|
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
11
|
-
# GNU General Public License for more details.
|
|
12
|
-
#
|
|
13
|
-
# You should have received a copy of the GNU General Public License
|
|
14
|
-
# along with this program; if not, write to the Free Software
|
|
15
|
-
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
|
16
|
-
|
|
17
|
-
from __future__ import annotations
|
|
18
|
-
|
|
19
|
-
import re
|
|
20
|
-
from dataclasses import asdict, dataclass, field
|
|
21
|
-
from pathlib import Path
|
|
22
|
-
from subprocess import run
|
|
23
|
-
from typing import Generator, Sequence
|
|
24
|
-
|
|
25
|
-
import yaml
|
|
26
|
-
from boltons.iterutils import flatten
|
|
27
|
-
from boltons.strutils import strip_ansi
|
|
28
|
-
from click_extra.testing import args_cleanup, print_cli_run
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
@dataclass(order=True)
|
|
32
|
-
class TestCase:
|
|
33
|
-
cli_parameters: tuple[str, ...] | str = field(default_factory=tuple)
|
|
34
|
-
"""Parameters, arguments and options to pass to the CLI."""
|
|
35
|
-
|
|
36
|
-
exit_code: int | str | None = None
|
|
37
|
-
strip_ansi: bool = False
|
|
38
|
-
output_contains: tuple[str, ...] | str = field(default_factory=tuple)
|
|
39
|
-
stdout_contains: tuple[str, ...] | str = field(default_factory=tuple)
|
|
40
|
-
stderr_contains: tuple[str, ...] | str = field(default_factory=tuple)
|
|
41
|
-
output_regex_matches: tuple[str, ...] | str = field(default_factory=tuple)
|
|
42
|
-
stdout_regex_matches: tuple[str, ...] | str = field(default_factory=tuple)
|
|
43
|
-
stderr_regex_matches: tuple[str, ...] | str = field(default_factory=tuple)
|
|
44
|
-
output_regex_fullmatch: str | None = None
|
|
45
|
-
stdout_regex_fullmatch: str | None = None
|
|
46
|
-
stderr_regex_fullmatch: str | None = None
|
|
47
|
-
|
|
48
|
-
def __post_init__(self) -> None:
|
|
49
|
-
"""Normalize all fields."""
|
|
50
|
-
for field_id, field_data in asdict(self).items():
|
|
51
|
-
# Validates and normalize exit code.
|
|
52
|
-
if field_id == "exit_code":
|
|
53
|
-
if isinstance(field_data, str):
|
|
54
|
-
field_data = int(field_data)
|
|
55
|
-
elif field_data is not None and not isinstance(field_data, int):
|
|
56
|
-
raise ValueError(f"exit_code is not an integer: {field_data}")
|
|
57
|
-
|
|
58
|
-
elif field_id == "strip_ansi":
|
|
59
|
-
if not isinstance(field_data, bool):
|
|
60
|
-
raise ValueError(f"strip_ansi is not a boolean: {field_data}")
|
|
61
|
-
|
|
62
|
-
# Validates and normalize regex fullmatch fields.
|
|
63
|
-
elif field_id.endswith("_fullmatch"):
|
|
64
|
-
if field_data:
|
|
65
|
-
if not isinstance(field_data, str):
|
|
66
|
-
raise ValueError(f"{field_id} is not a string: {field_data}")
|
|
67
|
-
# Normalize empty strings to None.
|
|
68
|
-
else:
|
|
69
|
-
field_data = None
|
|
70
|
-
|
|
71
|
-
# Validates and normalize tuple of strings.
|
|
72
|
-
else:
|
|
73
|
-
# Wraps single string into a tuple.
|
|
74
|
-
if isinstance(field_data, str):
|
|
75
|
-
field_data = (field_data,)
|
|
76
|
-
if not isinstance(field_data, Sequence):
|
|
77
|
-
raise ValueError(
|
|
78
|
-
f"{field_id} is not a tuple or a list: {field_data}"
|
|
79
|
-
)
|
|
80
|
-
if not all(isinstance(i, str) for i in field_data):
|
|
81
|
-
raise ValueError(
|
|
82
|
-
f"{field_id} contains non-string elements: {field_data}"
|
|
83
|
-
)
|
|
84
|
-
# Ignore blank value.
|
|
85
|
-
field_data = tuple(i.strip() for i in field_data if i.strip())
|
|
86
|
-
|
|
87
|
-
# Validates regexps.
|
|
88
|
-
if field_data and "_regex_" in field_id:
|
|
89
|
-
for regex in flatten((field_data,)):
|
|
90
|
-
try:
|
|
91
|
-
re.compile(regex)
|
|
92
|
-
except re.error as ex:
|
|
93
|
-
raise ValueError(
|
|
94
|
-
f"Invalid regex in {field_id}: {regex}"
|
|
95
|
-
) from ex
|
|
96
|
-
|
|
97
|
-
setattr(self, field_id, field_data)
|
|
98
|
-
|
|
99
|
-
def check_cli_test(self, binary: str | Path, timeout: int | None = None):
|
|
100
|
-
"""Run a CLI command and check its output against the test case.
|
|
101
|
-
|
|
102
|
-
..todo::
|
|
103
|
-
Add support for environment variables.
|
|
104
|
-
|
|
105
|
-
..todo::
|
|
106
|
-
Add support for ANSI code stripping.
|
|
107
|
-
|
|
108
|
-
..todo::
|
|
109
|
-
Add support for proper mixed stdout/stderr stream as a single,
|
|
110
|
-
intertwined output.
|
|
111
|
-
"""
|
|
112
|
-
clean_args = args_cleanup(binary, self.cli_parameters)
|
|
113
|
-
result = run(
|
|
114
|
-
clean_args,
|
|
115
|
-
capture_output=True,
|
|
116
|
-
timeout=timeout,
|
|
117
|
-
# XXX Do not force encoding to let CLIs figure out by themselves the
|
|
118
|
-
# contextual encoding to use. This avoid UnicodeDecodeError on output in
|
|
119
|
-
# Window's console which still defaults to legacy encoding (e.g. cp1252,
|
|
120
|
-
# cp932, etc...):
|
|
121
|
-
#
|
|
122
|
-
# Traceback (most recent call last):
|
|
123
|
-
# File "…\__main__.py", line 49, in <module>
|
|
124
|
-
# File "…\__main__.py", line 45, in main
|
|
125
|
-
# File "…\click\core.py", line 1157, in __call__
|
|
126
|
-
# File "…\click_extra\commands.py", line 347, in main
|
|
127
|
-
# File "…\click\core.py", line 1078, in main
|
|
128
|
-
# File "…\click_extra\commands.py", line 377, in invoke
|
|
129
|
-
# File "…\click\core.py", line 1688, in invoke
|
|
130
|
-
# File "…\click_extra\commands.py", line 377, in invoke
|
|
131
|
-
# File "…\click\core.py", line 1434, in invoke
|
|
132
|
-
# File "…\click\core.py", line 783, in invoke
|
|
133
|
-
# File "…\cloup\_context.py", line 47, in new_func
|
|
134
|
-
# File "…\mpm\cli.py", line 570, in managers
|
|
135
|
-
# File "…\mpm\output.py", line 187, in print_table
|
|
136
|
-
# File "…\click_extra\tabulate.py", line 97, in render_csv
|
|
137
|
-
# File "encodings\cp1252.py", line 19, in encode
|
|
138
|
-
# UnicodeEncodeError: 'charmap' codec can't encode character
|
|
139
|
-
# '\u2713' in position 128: character maps to <undefined>
|
|
140
|
-
#
|
|
141
|
-
# encoding="utf-8",
|
|
142
|
-
text=True,
|
|
143
|
-
)
|
|
144
|
-
print_cli_run(clean_args, result)
|
|
145
|
-
|
|
146
|
-
for field_id, field_data in asdict(self).items():
|
|
147
|
-
if field_id == "cli_parameters" or (not field_data and field_data != 0):
|
|
148
|
-
continue
|
|
149
|
-
|
|
150
|
-
if field_id == "exit_code":
|
|
151
|
-
if result.returncode != field_data:
|
|
152
|
-
raise AssertionError(
|
|
153
|
-
f"CLI exited with code {result.returncode}, "
|
|
154
|
-
f"expected {field_data}"
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
output = ""
|
|
158
|
-
name = ""
|
|
159
|
-
if field_id.startswith("output_"):
|
|
160
|
-
raise NotImplementedError("Output mixing <stdout>/<stderr>")
|
|
161
|
-
# output = result.output
|
|
162
|
-
# name = "output"
|
|
163
|
-
elif field_id.startswith("stdout_"):
|
|
164
|
-
output = result.stdout
|
|
165
|
-
name = "<stdout>"
|
|
166
|
-
elif field_id.startswith("stderr_"):
|
|
167
|
-
output = result.stderr
|
|
168
|
-
name = "<stderr>"
|
|
169
|
-
|
|
170
|
-
if self.strip_ansi:
|
|
171
|
-
output = strip_ansi(output)
|
|
172
|
-
|
|
173
|
-
if field_id.endswith("_contains"):
|
|
174
|
-
for sub_string in field_data:
|
|
175
|
-
if sub_string not in output:
|
|
176
|
-
raise AssertionError(
|
|
177
|
-
f"CLI's {name} does not contain {sub_string!r}"
|
|
178
|
-
)
|
|
179
|
-
|
|
180
|
-
elif field_id.endswith("_regex_matches"):
|
|
181
|
-
for regex in field_data:
|
|
182
|
-
if not re.search(regex, output):
|
|
183
|
-
raise AssertionError(
|
|
184
|
-
f"CLI's {name} does not match regex {regex!r}"
|
|
185
|
-
)
|
|
186
|
-
|
|
187
|
-
elif field_id.endswith("_regex_fullmatch"):
|
|
188
|
-
regex = field_data
|
|
189
|
-
if not re.fullmatch(regex, output):
|
|
190
|
-
raise AssertionError(
|
|
191
|
-
f"CLI's {name} does not fully match regex {regex!r}"
|
|
192
|
-
)
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
DEFAULT_TEST_PLAN = (
|
|
196
|
-
# Output the version of the CLI.
|
|
197
|
-
TestCase(cli_parameters="--version"),
|
|
198
|
-
# Test combination of version and verbosity.
|
|
199
|
-
TestCase(cli_parameters=("--verbosity", "DEBUG", "--version")),
|
|
200
|
-
# Test help output.
|
|
201
|
-
TestCase(cli_parameters="--help"),
|
|
202
|
-
)
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
def parse_test_plan(plan_path: Path) -> Generator[TestCase, None, None]:
|
|
206
|
-
plan = yaml.full_load(plan_path.read_text(encoding="UTF-8"))
|
|
207
|
-
|
|
208
|
-
# Validates test plan structure.
|
|
209
|
-
if not plan:
|
|
210
|
-
raise ValueError(f"Empty test plan file {plan_path}")
|
|
211
|
-
if not isinstance(plan, list):
|
|
212
|
-
raise ValueError(f"Test plan is not a list: {plan}")
|
|
213
|
-
|
|
214
|
-
directives = frozenset(TestCase.__dataclass_fields__.keys())
|
|
215
|
-
|
|
216
|
-
for index, test_case in enumerate(plan):
|
|
217
|
-
# Validates test case structure.
|
|
218
|
-
if not isinstance(test_case, dict):
|
|
219
|
-
raise ValueError(f"Test case #{index + 1} is not a dict: {test_case}")
|
|
220
|
-
if not directives.issuperset(test_case):
|
|
221
|
-
raise ValueError(
|
|
222
|
-
f"Test case #{index + 1} contains invalid directives:"
|
|
223
|
-
f"{set(test_case) - directives}"
|
|
224
|
-
)
|
|
225
|
-
|
|
226
|
-
yield TestCase(**test_case)
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
# Copyright Kevin Deldycke <kevin@deldycke.com> and contributors.
|
|
2
|
-
#
|
|
3
|
-
# This program is Free Software; you can redistribute it and/or
|
|
4
|
-
# modify it under the terms of the GNU General Public License
|
|
5
|
-
# as published by the Free Software Foundation; either version 2
|
|
6
|
-
# of the License, or (at your option) any later version.
|
|
7
|
-
#
|
|
8
|
-
# This program is distributed in the hope that it will be useful,
|
|
9
|
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
10
|
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
11
|
-
# GNU General Public License for more details.
|
|
12
|
-
#
|
|
13
|
-
# You should have received a copy of the GNU General Public License
|
|
14
|
-
# along with this program; if not, write to the Free Software
|
|
15
|
-
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
|
16
|
-
|
|
17
|
-
from __future__ import annotations
|
|
18
|
-
|
|
19
|
-
import re
|
|
20
|
-
|
|
21
|
-
from gha_utils.metadata import Dialects, Metadata
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def test_metadata_github_format():
|
|
25
|
-
metadata = Metadata()
|
|
26
|
-
|
|
27
|
-
assert re.fullmatch(
|
|
28
|
-
(
|
|
29
|
-
r"new_commits=\n"
|
|
30
|
-
r"release_commits=\n"
|
|
31
|
-
r"gitignore_exists=true\n"
|
|
32
|
-
r"python_files=[\S ]*\n"
|
|
33
|
-
r"doc_files=[\S ]*\n"
|
|
34
|
-
r"is_python_project=true\n"
|
|
35
|
-
r"package_name=gha-utils\n"
|
|
36
|
-
r"blacken_docs_params=--target-version py310 --target-version py311 "
|
|
37
|
-
r"--target-version py312 --target-version py313\n"
|
|
38
|
-
r"mypy_params=--python-version 3\.10\n"
|
|
39
|
-
r"current_version=\n"
|
|
40
|
-
r"released_version=\n"
|
|
41
|
-
r"is_sphinx=false\n"
|
|
42
|
-
r"active_autodoc=false\n"
|
|
43
|
-
r"release_notes=\n"
|
|
44
|
-
r"new_commits_matrix=\n"
|
|
45
|
-
r"release_commits_matrix=\n"
|
|
46
|
-
r'nuitka_matrix=\{"os": \["ubuntu-24\.04", "ubuntu-24\.04-arm", '
|
|
47
|
-
r'"macos-15", "macos-13", "windows-2022"\], '
|
|
48
|
-
r'"entry_point": \["gha-utils"\], '
|
|
49
|
-
r'"commit": \["[a-z0-9]+"\]\}\n'
|
|
50
|
-
),
|
|
51
|
-
metadata.dump(Dialects.github),
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def test_metadata_plain_format():
|
|
56
|
-
metadata = Metadata()
|
|
57
|
-
|
|
58
|
-
assert re.fullmatch(
|
|
59
|
-
(
|
|
60
|
-
r"\{"
|
|
61
|
-
r"'new_commits': None, "
|
|
62
|
-
r"'release_commits': None, "
|
|
63
|
-
r"'gitignore_exists': True, "
|
|
64
|
-
r"'python_files': <generator object Metadata\.python_files at \S+>, "
|
|
65
|
-
r"'doc_files': <generator object Metadata\.doc_files at \S+>, "
|
|
66
|
-
r"'is_python_project': True, "
|
|
67
|
-
r"'package_name': 'gha-utils', "
|
|
68
|
-
r"'blacken_docs_params': \("
|
|
69
|
-
r"'--target-version py310', "
|
|
70
|
-
r"'--target-version py311', "
|
|
71
|
-
r"'--target-version py312', "
|
|
72
|
-
r"'--target-version py313'\), "
|
|
73
|
-
r"'mypy_params': '--python-version 3\.10', "
|
|
74
|
-
r"'current_version': None, "
|
|
75
|
-
r"'released_version': None, "
|
|
76
|
-
r"'is_sphinx': False, "
|
|
77
|
-
r"'active_autodoc': False, "
|
|
78
|
-
r"'release_notes': None, "
|
|
79
|
-
r"'new_commits_matrix': None, "
|
|
80
|
-
r"'release_commits_matrix': None, "
|
|
81
|
-
r"'nuitka_matrix': <Matrix: \{"
|
|
82
|
-
r"'os': \('ubuntu-24\.04', 'ubuntu-24\.04-arm', "
|
|
83
|
-
r"'macos-15', 'macos-13', 'windows-2022'\), "
|
|
84
|
-
r"'entry_point': \('gha-utils',\), "
|
|
85
|
-
r"'commit': \('[a-z0-9]+',\)\}; "
|
|
86
|
-
r"include=\(\); exclude=\(\)>\}"
|
|
87
|
-
),
|
|
88
|
-
metadata.dump(Dialects.plain),
|
|
89
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|