Skip to content

Commit 95b5e74

Browse files
Fixing Workflow unit-test execution & Adding Test-Reports functionality (#343)
* Run unit-tests instead of consumer-tests Currently consumer-tests are run when the documentation is build. This is not needed as the consumer tests are executed separately as well, and the normal build should execute the unit-tests instead. This also enables the testlinker & xml parser to look for the 'tests-report' folder to enable testing being done outside of bazel. Closes: #328
1 parent aa24fdf commit 95b5e74

File tree

5 files changed

+154
-73
lines changed

5 files changed

+154
-73
lines changed

.github/workflows/test.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ name: Run Bazel Tests
1515
on:
1616
pull_request:
1717
types: [opened, reopened, synchronize]
18+
workflow_call:
1819
jobs:
1920
code:
2021
runs-on: ubuntu-latest
@@ -39,3 +40,19 @@ jobs:
3940
run: |
4041
bazel run //:ide_support
4142
bazel test //src/...
43+
44+
- name: Prepare bundled consumer report
45+
if: always()
46+
# Creating tests-report directory
47+
# Follow Symlinks via '-L' to copy correctly
48+
# Copy everything inside the 'test-reports' folder
49+
run: |
50+
mkdir -p tests-report
51+
rsync -amL --include='*/' --include='test.xml' --include='test.log' --exclude='*' bazel-testlogs/ tests-report/
52+
53+
- name: Upload bundled consumer report
54+
if: always()
55+
uses: actions/upload-artifact@v4
56+
with:
57+
name: tests-report
58+
path: tests-report

.github/workflows/test_and_docs.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,13 @@ jobs:
3838
bazel-docs-verify-target: "//:docs_check"
3939

4040
# This is the user configurable part of the workflow
41-
consumer-tests:
42-
uses: ./.github/workflows/consumer_test.yml
41+
unit-tests:
42+
uses: ./.github/workflows/test.yml
4343
secrets: inherit
4444

4545
docs-build:
4646
# Waits for consumer-tests but run only when docs verification succeeded
47-
needs: [docs-verify, consumer-tests]
47+
needs: [docs-verify, unit-tests]
4848
if: ${{ always() && needs.docs-verify.result == 'success' }}
4949
uses: eclipse-score/cicd-workflows/.github/workflows/docs.yml@main
5050
permissions:

docs/internals/extensions/source_code_linker.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ These tags are extracted and matched to Sphinx needs via the `source_code_link`
6464

6565
### ✅ TestLink: Test Result Integration
6666

67-
TestLink scans test result XMLs from Bazel and converts each test case with metadata into Sphinx external needs, allowing links from tests to requirements.
67+
TestLink scans test result XMLs from Bazel (bazel-testlogs) or in the folder 'tests-report' and converts each test case with metadata into Sphinx external needs, allowing links from tests to requirements.
6868
This depends on the `attribute_plugin` in our tooling repository, find it [here](https://github.com/eclipse-score/tooling/tree/main/python_basics/score_pytest)
6969
#### Test Tagging Options
7070

src/extensions/score_source_code_linker/tests/test_xml_parser.py

Lines changed: 117 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"""
1818

1919
import xml.etree.ElementTree as ET
20+
from collections.abc import Callable
2021
from pathlib import Path
2122
from typing import Any
2223

@@ -30,85 +31,136 @@
3031

3132

3233
# Unsure if I should make these last a session or not
34+
def _write_test_xml(
35+
path: Path,
36+
name: str,
37+
result: str = "",
38+
props: dict[str, str] | None = None,
39+
file: str = "",
40+
line: int = 0,
41+
):
42+
"""Helper to create the XML structure for a test case."""
43+
ts = ET.Element("testsuites")
44+
suite = ET.SubElement(ts, "testsuite")
45+
46+
# Create testcase with attributes
47+
tc_attrs = {"name": name}
48+
if file:
49+
tc_attrs["file"] = file
50+
if line:
51+
tc_attrs["line"] = str(line)
52+
tc = ET.SubElement(suite, "testcase", tc_attrs)
53+
54+
# Add failure/skipped status
55+
if result == "failed":
56+
ET.SubElement(tc, "failure", {"message": "failmsg"})
57+
elif result == "skipped":
58+
ET.SubElement(tc, "skipped", {"message": "skipmsg"})
59+
60+
# Add properties if provided
61+
if props:
62+
props_el = ET.SubElement(tc, "properties")
63+
for k, v in props.items():
64+
ET.SubElement(props_el, "property", {"name": k, "value": v})
65+
66+
# Save to file
67+
ET.ElementTree(ts).write(path, encoding="utf-8", xml_declaration=True)
68+
69+
3370
@pytest.fixture
34-
def tmp_xml_dirs(tmp_path: Path) -> tuple[Path, Path, Path]:
35-
root: Path = tmp_path / "bazel-testlogs"
36-
dir1: Path = root / "with_props"
37-
dir2: Path = root / "no_props"
38-
dir1.mkdir(parents=True)
39-
dir2.mkdir(parents=True)
40-
41-
def write(file_path: Path, testcases: list[ET.Element]):
42-
ts = ET.Element("testsuites")
43-
suite = ET.SubElement(ts, "testsuite")
44-
for tc in testcases:
45-
suite.append(tc)
46-
tree = ET.ElementTree(ts)
47-
tree.write(file_path, encoding="utf-8", xml_declaration=True)
48-
49-
def make_tc(
50-
name: str,
51-
result: str = "",
52-
props: dict[str, str] | None = None,
53-
file: str = "",
54-
line: int = 0,
55-
):
56-
tc = ET.Element("testcase", {"name": name})
57-
if file:
58-
tc.set("file", file)
59-
if line:
60-
tc.set("line", str(line))
61-
if result == "failed":
62-
ET.SubElement(tc, "failure", {"message": "failmsg"})
63-
elif result == "skipped":
64-
ET.SubElement(tc, "skipped", {"message": "skipmsg"})
65-
if props:
66-
props_el = ET.SubElement(tc, "properties")
67-
for k, v in props.items():
68-
ET.SubElement(props_el, "property", {"name": k, "value": v})
69-
return tc
70-
71-
# File with properties
72-
tc1 = make_tc(
73-
"tc_with_props",
74-
result="failed",
75-
props={
76-
"PartiallyVerifies": "REQ1",
77-
"FullyVerifies": "",
78-
"TestType": "type",
79-
"DerivationTechnique": "tech",
80-
"Description": "desc",
81-
},
82-
file="path1",
83-
line=10,
84-
)
85-
write(dir1 / "test.xml", [tc1])
86-
87-
# File without properties
88-
# HINT: Once the assertions in xml_parser are back and active, this should allow us
89-
# to catch that the tests Need to be changed too.
90-
tc2 = make_tc("tc_no_props", file="path2", line=20)
91-
write(dir2 / "test.xml", [tc2])
92-
93-
return root, dir1, dir2
71+
def tmp_xml_dirs(tmp_path: Path) -> Callable[..., tuple[Path, Path, Path]]:
72+
def _tmp_xml_dirs(test_folder: str = "bazel-testlogs") -> tuple[Path, Path, Path]:
73+
root = tmp_path / test_folder
74+
dir1, dir2 = root / "with_props", root / "no_props"
75+
76+
for d in (dir1, dir2):
77+
d.mkdir(parents=True, exist_ok=True)
78+
79+
# File with properties
80+
_write_test_xml(
81+
dir1 / "test.xml",
82+
name="tc_with_props",
83+
result="failed",
84+
file="path1",
85+
line=10,
86+
props={
87+
"PartiallyVerifies": "REQ1",
88+
"FullyVerifies": "",
89+
"TestType": "type",
90+
"DerivationTechnique": "tech",
91+
"Description": "desc",
92+
},
93+
)
94+
95+
# File without properties
96+
_write_test_xml(dir2 / "test.xml", name="tc_no_props", file="path2", line=20)
97+
98+
return root, dir1, dir2
99+
100+
return _tmp_xml_dirs
94101

95102

96103
@add_test_properties(
97104
partially_verifies=["tool_req__docs_test_link_testcase"],
98105
test_type="requirements-based",
99106
derivation_technique="requirements-analysis",
100107
)
101-
def test_find_xml_files(tmp_xml_dirs: tuple[Path, Path, Path]):
102-
"""Ensure xml files are found as expected"""
108+
def test_find_xml_files(tmp_xml_dirs: Callable[..., tuple[Path, Path, Path]]):
109+
"""Ensure xml files are found as expected if bazel-testlogs is used"""
103110
root: Path
104111
dir1: Path
105112
dir2: Path
106-
root, dir1, dir2 = tmp_xml_dirs
113+
root, dir1, dir2 = tmp_xml_dirs()
107114
found = xml_parser.find_xml_files(root)
108115
expected: set[Path] = {dir1 / "test.xml", dir2 / "test.xml"}
109116
assert set(found) == expected
110117

111118

119+
def test_find_xml_folder(tmp_xml_dirs: Callable[..., tuple[Path, Path, Path]]):
120+
"""Ensure xml files are found as expected if bazel-testlogs is used"""
121+
root: Path
122+
root, _, _ = tmp_xml_dirs()
123+
found = xml_parser.find_test_folder(base_path=root.parent)
124+
assert found is not None
125+
assert found == root
126+
127+
128+
def test_find_xml_folder_test_reports(
129+
tmp_xml_dirs: Callable[..., tuple[Path, Path, Path]],
130+
):
131+
# root is the 'tests-report' folder inside tmp_path
132+
root, _, _ = tmp_xml_dirs(test_folder="tests-report")
133+
# We pass the PARENT of 'tests-report' as the workspace root
134+
found = xml_parser.find_test_folder(base_path=root.parent)
135+
assert found is not None
136+
assert found == root
137+
138+
139+
def test_find_xml_files_test_reports(
140+
tmp_xml_dirs: Callable[..., tuple[Path, Path, Path]],
141+
):
142+
"""Ensure xml files are found as expected if tests-report is used"""
143+
root: Path
144+
dir1: Path
145+
dir2: Path
146+
root, dir1, dir2 = tmp_xml_dirs(test_folder="tests-report")
147+
found = xml_parser.find_xml_files(dir=root)
148+
assert found is not None
149+
expected: set[Path] = {root / dir1 / "test.xml", root / dir2 / "test.xml"}
150+
assert set(found) == expected
151+
152+
153+
def test_early_return(tmp_path: Path):
154+
"""
155+
Ensure that if tests-report & bazel-testlogs is not found,
156+
we return None for early return inside extension
157+
"""
158+
# Move the test execution context to a 100% empty folder
159+
160+
found = xml_parser.find_test_folder(tmp_path)
161+
assert found is None
162+
163+
112164
@add_test_properties(
113165
partially_verifies=["tool_req__docs_test_link_testcase"],
114166
test_type="requirements-based",
@@ -152,12 +204,12 @@ def test_parse_properties():
152204
test_type="requirements-based",
153205
derivation_technique="requirements-analysis",
154206
)
155-
def test_read_test_xml_file(tmp_xml_dirs: tuple[Path, Path, Path]):
207+
def test_read_test_xml_file(tmp_xml_dirs: Callable[..., tuple[Path, Path, Path]]):
156208
"""Ensure a whole pre-defined xml file is parsed correctly"""
157209
_: Path
158210
dir1: Path
159211
dir2: Path
160-
_, dir1, dir2 = tmp_xml_dirs
212+
_, dir1, dir2 = tmp_xml_dirs()
161213

162214
needs1, no_props1 = xml_parser.read_test_xml_file(dir1 / "test.xml")
163215
assert isinstance(needs1, list) and len(needs1) == 1

src/extensions/score_source_code_linker/xml_parser.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -171,16 +171,28 @@ def find_xml_files(dir: Path) -> list[Path]:
171171
return xml_paths
172172

173173

174+
def find_test_folder(base_path: Path | None = None) -> Path | None:
175+
ws_root = base_path if base_path is not None else find_ws_root()
176+
assert ws_root is not None
177+
if os.path.isdir(ws_root / "tests-report"):
178+
return ws_root / "tests-report"
179+
if os.path.isdir(ws_root / "bazel-testlogs"):
180+
return ws_root / "bazel-testlogs"
181+
logger.info("could not find tests-report or bazel-testlogs to parse testcases")
182+
return None
183+
184+
174185
def run_xml_parser(app: Sphinx, env: BuildEnvironment):
175186
"""
176187
This is the 'main' function for parsing test.xml's and
177188
building testcase needs.
178189
It gets called from the source_code_linker __init__
179190
"""
180-
ws_root = find_ws_root()
181-
assert ws_root is not None
182-
bazel_testlogs = ws_root / "bazel-testlogs"
183-
xml_file_paths = find_xml_files(bazel_testlogs)
191+
testlogs_dir = find_test_folder()
192+
# early return
193+
if testlogs_dir is None:
194+
return
195+
xml_file_paths = find_xml_files(testlogs_dir)
184196
test_case_needs = build_test_needs_from_files(app, env, xml_file_paths)
185197
# Saving the test case needs for cache
186198
store_data_of_test_case_json(

0 commit comments

Comments
 (0)