Skip to content

Commit 1b2cbe2

Browse files
AlexanderLaninMaximilianSoerenPollakmmr1909
authored
docs: fix test execution via pytest from venv (#954)
* docs: fix test execution via pytest from venv * fix: store only string in config * misc: cleanup * fix: handle empty filter in parse_checks_filter and improve error reporting in extract_test_data * chore: remove unused apply_enabled_check_filter function And add CHECK option for rst-test files to run a single check Co-authored-by: Maximilian Sören Pollak <maximilian.pollak@expleogroup.com> Co-authored-by: Michael Müller <42868757+mmr1909@users.noreply.github.com>
1 parent be12a83 commit 1b2cbe2

7 files changed

Lines changed: 129 additions & 38 deletions

File tree

tooling/docs/_tooling/extensions/score_metamodel/__init__.py

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,21 @@
2424

2525
logger = logging.get_logger(__name__)
2626

27-
local_checks: list[Callable[[Sphinx, NeedsInfoType, CheckLogger], None]] = []
28-
graph_checks: list[Callable[[Sphinx, list[NeedsInfoType], CheckLogger], None]] = []
27+
local_check_function = Callable[[Sphinx, NeedsInfoType, CheckLogger], None]
28+
graph_check_function = Callable[[Sphinx, list[NeedsInfoType], CheckLogger], None]
29+
30+
local_checks: list[local_check_function] = []
31+
graph_checks: list[graph_check_function] = []
32+
33+
34+
def parse_checks_filter(filter: str) -> list[str]:
35+
"""
36+
Parse the checks filter string into a list of individual checks.
37+
When empty, an empty list is returned = all checks are enabled.
38+
"""
39+
if not filter:
40+
return []
41+
return [check.strip() for check in filter.split(",")]
2942

3043

3144
def discover_checks():
@@ -41,14 +54,14 @@ def discover_checks():
4154
importlib.import_module(f"{package_name}.{module_name}", __package__)
4255

4356

44-
def local_check(func: Callable[[Sphinx, NeedsInfoType, CheckLogger], None]):
57+
def local_check(func: local_check_function):
4558
"""Use this decorator to mark a function as a local check."""
4659
logger.debug(f"new local_check: {func}")
4760
local_checks.append(func)
4861
return func
4962

5063

51-
def graph_check(func: Callable[[Sphinx, list[NeedsInfoType], CheckLogger], None]):
64+
def graph_check(func: graph_check_function):
5265
"""Use this decorator to mark a function as a graph check."""
5366
logger.debug(f"new graph_check: {func}")
5467
graph_checks.append(func)
@@ -68,21 +81,27 @@ def _run_checks(app: Sphinx, exception: Exception | None) -> None:
6881

6982
log = CheckLogger(logger, prefix)
7083

84+
checks_filter = parse_checks_filter(app.config.score_metamodel_checks)
85+
86+
def is_check_enabled(check: local_check_function | graph_check_function):
87+
return not checks_filter or check.__name__ in checks_filter
88+
7189
# Need-Local checks: checks which can be checked file-local, without a
7290
# graph of other needs.
7391
for need in needs_all_needs.values():
74-
for check in local_checks:
92+
for check in [c for c in local_checks if is_check_enabled(c)]:
93+
logger.info(f"Running local check {check} for need {need['id']}")
7594
check(app, need, log)
7695

7796
# Graph-Based checks: These warnings require a graph of all other needs to
7897
# be checked.
7998
needs = list(needs_all_needs.values())
80-
for check in graph_checks:
99+
for check in [c for c in graph_checks if is_check_enabled(c)]:
100+
logger.info(f"Running graph check {check} for all needs")
81101
check(app, needs, log)
82102

83103
if log.has_warnings:
84104
log.warning("Some needs have issues. See the log for more information.")
85-
# TODO: exit code
86105

87106

88107
def load_metamodel_data():
@@ -247,6 +266,15 @@ def setup(app: Sphinx) -> dict[str, str | bool]:
247266

248267
discover_checks()
249268

269+
app.add_config_value(
270+
"score_metamodel_checks",
271+
"",
272+
rebuild="env",
273+
description=(
274+
"Comma separated list of enabled checks. When empty, all checks are enabled"
275+
),
276+
)
277+
250278
app.connect("build-finished", _run_checks)
251279

252280
return {

tooling/docs/_tooling/extensions/score_metamodel/tests/README.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,19 @@ To add a new test case create a new rst file in the rst directory.
1616
The test files can also be organized in a subfolder structure below directory rst.
1717
The test files are expected to contain the following format:
1818

19+
#CHECK: <check functions>
20+
1921
#EXPECT: <warning message>
2022
#EXPECT-NOT: <warning message>
2123

2224
<need information>
2325

26+
**\<check functions>**<br>
27+
Check functions (comma separated) to be used for the Sphinx build. Following
28+
warnings will only be generated by these check functions.
29+
Only one CHECK statement per file. Usually at the very top.
30+
If CHECK is not provided, all checks are enabled.
31+
2432
**\<warning message>**<br>
2533
Message text which is expected/not expected during the
2634
Sphinx build to be shown.
@@ -33,11 +41,12 @@ One or more Sphinx-Needs directives needed for the
3341

3442
**Example:**
3543

36-
EXPECT: std_wp__test__abcd: is missing required option: `status`.
44+
#CHECK: check_options
45+
#EXPECT: std_wp__test__abcd: is missing required option: `status`.
3746

3847
.. std_wp:: Test requirement
3948
:id: std_wp__test__abcd
4049

4150
This example verifies that the warning message
42-
*std_wp__test__abcd: is missing required option: `status`*
43-
is shown during the Sphinx build.
51+
*std_wp__test__abcd: is missing required option: \`status\`*
52+
is shown during the Sphinx build. Only the *check_options* check is enabled.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# *******************************************************************************
2+
# Copyright (c) 2024 Contributors to the Eclipse Foundation
3+
#
4+
# See the NOTICE file(s) distributed with this work for additional
5+
# information regarding copyright ownership.
6+
#
7+
# This program and the accompanying materials are made available under the
8+
# terms of the Apache License Version 2.0 which is available at
9+
# https://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# SPDX-License-Identifier: Apache-2.0
12+
# *******************************************************************************
13+
14+
# Configuration file for the Sphinx documentation builder.
15+
#
16+
# For the full list of built-in configuration values, see the documentation:
17+
# https://www.sphinx-doc.org/en/master/usage/configuration.html
18+
19+
extensions = [
20+
"sphinx_needs",
21+
"score_metamodel",
22+
]

tooling/docs/_tooling/extensions/score_metamodel/tests/rst/graph/test_metamodel_graph.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#
1212
# SPDX-License-Identifier: Apache-2.0
1313
# *******************************************************************************
14+
#CHECK: check_metamodel_graph
1415

1516
.. feat_req:: Parent requirement
1617
:id: feat_req__parent__abcd

tooling/docs/_tooling/extensions/score_metamodel/tests/rst/options/test_options_extra_option.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#
1212
# SPDX-License-Identifier: Apache-2.0
1313
# *******************************************************************************
14+
#CHECK: check_extra_options
1415

1516
#EXPECT: std_wp__test__abcd: has these extra options: `safety`.
1617

tooling/docs/_tooling/extensions/score_metamodel/tests/rst/options/test_options_options.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#
1212
# SPDX-License-Identifier: Apache-2.0
1313
# *******************************************************************************
14+
#CHECK: check_options
1415

1516
#EXPECT: std_wp__test__abcd: is missing required option: `status`.
1617

tooling/docs/_tooling/extensions/score_metamodel/tests/test_rules_file_based.py

Lines changed: 57 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@
2020
import pytest
2121
from sphinx.testing.util import SphinxTestApp
2222

23+
from docs._tooling.extensions.score_metamodel import (
24+
graph_check_function,
25+
local_check_function,
26+
)
27+
2328
RST_DIR = Path(__file__).absolute().parent / "rst"
2429
DOCS_DIR = Path(__file__).absolute().parent.parent.parent.parent.parent
2530
TOOLING_DIR_NAME = "_tooling"
@@ -32,7 +37,7 @@
3237
def sphinx_base_dir(tmp_path_factory: pytest.TempPathFactory) -> Path:
3338
### Create a temporary directory for Sphinx and copy all necessary files.
3439
base_dir: Path = tmp_path_factory.mktemp("docs")
35-
shutil.copy(DOCS_DIR / "conf.py", base_dir)
40+
shutil.copy(RST_DIR / "conf.py", base_dir)
3641
shutil.copytree(
3742
DOCS_DIR / TOOLING_DIR_NAME,
3843
base_dir / TOOLING_DIR_NAME,
@@ -82,36 +87,43 @@ def _create_app(rst_file: Path) -> SphinxTestApp:
8287
@dataclass
8388
class WarningInfo:
8489
#### Class to hold information about warnings
85-
# The class contains the filename of the rst file,
86-
# line number and the expected and not expected warnings.
87-
filename: str = ""
90+
# Contains the line number and the expected and not expected warnings.
8891
lineno: int = 0
8992
expected: list[str] = field(default_factory=list)
9093
not_expected: list[str] = field(default_factory=list)
9194

9295

93-
def extract_warning(line: str) -> str:
96+
@dataclass
97+
class RstData:
98+
#### Holds filename, all infos about warnings and
99+
# which checks to enable if not all
100+
filename: str
101+
enabled_checks: str = ""
102+
warning_infos: list[WarningInfo] = field(default_factory=list)
103+
104+
105+
def parse_line_for_message(line: str) -> str:
94106
#### Extract the warning message from the line
95107
# The line format is "#EXPECT: <warning message>"
96108
# or "#EXPECT-NOT: <warning message>"
109+
# or "#CHECK: <checks>"
97110
return line.split(": ", 1)[1].strip()
98111

99112

100-
def extract_test_data(rst_file: Path) -> list[WarningInfo] | None:
113+
def extract_test_data(rst_file: Path) -> RstData | None:
101114
### Extract test data from the given rst file
102115
# The function returns a list of WarningInfo objects
103116
# containing the line number and the expected and not expected warnings.
104117
# If no test data is found, it returns None.
118+
rst_data = RstData(filename=rst_file.name)
105119
with open(rst_file) as f:
106-
statements: list[WarningInfo] = []
107120
test_info: WarningInfo | None = None
108121
for no, line in enumerate(f, start=1):
109122
if line.startswith(".. "): # Beginning of new need
110123
if test_info:
111-
test_info.filename = rst_file.name
112124
test_info.lineno = no
113-
statements.append(test_info)
114-
test_info = None
125+
rst_data.warning_infos.append(test_info)
126+
test_info = None
115127
elif line.startswith("#EXPECT:") or line.startswith("#EXPECT-NOT:"):
116128
if test_info is None:
117129
test_info = WarningInfo()
@@ -120,47 +132,64 @@ def extract_test_data(rst_file: Path) -> list[WarningInfo] | None:
120132
if line.startswith("#EXPECT:")
121133
else test_info.not_expected
122134
)
123-
target_list.append(extract_warning(line))
135+
target_list.append(parse_line_for_message(line))
136+
elif line.startswith("#CHECK:"):
137+
assert not rst_data.enabled_checks, "only one CHECK per file allowed"
138+
rst_data.enabled_checks = parse_line_for_message(line)
124139
# Check last InfoElement
125140
if test_info:
126-
print("ERROR: Teststatement without according need found")
127-
return statements
141+
raise AssertionError(
142+
"Last EXPECT/EXPECT-NOT line is not followed by a need."
143+
)
144+
return rst_data
128145

129146

130147
def warning_matches(
131-
warning_info: WarningInfo, expected_message: str, warnings: list[str]
148+
rst_data: RstData,
149+
warning_info: WarningInfo,
150+
expected_message: str,
151+
warnings: list[str],
132152
) -> bool:
133153
### Checks if any element of the warning list is includes the given warning info.
134154
# It returns True if found otherwise False.
135155
for warning in warnings:
136156
if (
137-
f"{warning_info.filename}:{str(warning_info.lineno)}" in warning
157+
f"{rst_data.filename}:{str(warning_info.lineno)}" in warning
138158
and expected_message in warning
139159
):
140160
return True
141161
return False
142162

143163

144164
@pytest.mark.parametrize("rst_file", RST_FILES)
145-
def test_check_rules(
165+
def test_rst_files(
146166
rst_file: str, sphinx_app_setup: Callable[[Path], SphinxTestApp]
147167
) -> None:
148168
### Test function to check rules in the given rst file
149169
# The function uses the SphinxTestApp to build the documentation
150170
# and checks for the expected/unexpected warnings.
151-
assert (
152-
test_data := extract_test_data(RST_DIR / rst_file)
153-
), "Unable to extract test data"
171+
rst_data = extract_test_data(RST_DIR / rst_file)
172+
if not rst_data:
173+
raise AssertionError(
174+
"Unable to extract test data from the rst file: "
175+
f"{rst_file}. Please check the file for the correct format."
176+
)
177+
154178
app: SphinxTestApp = sphinx_app_setup(RST_DIR / rst_file)
155179
os.chdir(app.srcdir) # Change working directory to the source directory
180+
181+
# Build the documentation with the enabled checks
182+
app.config.score_metamodel_checks = rst_data.enabled_checks
156183
app.build()
184+
185+
# Collect the warnings
157186
warnings = app.warning.getvalue().splitlines()
158-
for test in test_data:
159-
for expected in test.expected:
160-
assert warning_matches(
161-
test, expected, warnings
162-
), f"Expected warning: {expected} not found"
163-
for not_expected in test.not_expected:
164-
assert not warning_matches(
165-
test, not_expected, warnings
166-
), f"Unexpected warning: {not_expected} found"
187+
188+
# Check if the expected warnings are present
189+
for warning_info in rst_data.warning_infos:
190+
for w in warning_info.expected:
191+
if not warning_matches(rst_data, warning_info, w, warnings):
192+
raise AssertionError(f"Expected warning: '{w}' not found")
193+
for w in warning_info.not_expected:
194+
if warning_matches(rst_data, warning_info, w, warnings):
195+
raise AssertionError(f"Unexpected warning: '{w}' found")

0 commit comments

Comments
 (0)