Skip to content

Commit fe9c9db

Browse files
authored
Merge pull request #48 from redhat-performance/fix/RPOPC-1316-version-conflation
RPOPC-1316: Fix BaseProcessor.build_test_info() version conflation
2 parents 56473c1 + 3073ae4 commit fe9c9db

2 files changed

Lines changed: 207 additions & 6 deletions

File tree

src/chronicler/processors/base_processor.py

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -196,10 +196,42 @@ def build_metadata(self) -> Metadata:
196196
)
197197

198198
def build_test_info(self) -> TestInfo:
199-
"""Build test information section"""
199+
"""
200+
Build test information section.
201+
202+
By default, this method extracts the wrapper version from the test_info
203+
file (the orchestrator's test repository version, e.g., "v2.8") and sets
204+
BOTH test.version and test.wrapper_version to this same value.
205+
206+
IMPORTANT: This is intentional base behavior, but often incorrect semantically.
207+
For benchmarks with independent versioning (e.g., FIO 3.36, STREAMS 5.10),
208+
processors MUST override this method to extract and return the benchmark's
209+
own version in test.version while preserving wrapper_version.
210+
211+
Examples of processors that should override:
212+
- FIO: test.version = "fio-3.36", wrapper_version = "v2.1"
213+
- STREAMS: test.version = "5.10", wrapper_version = "v2.8"
214+
- CoreMark: test.version = "v1.01", wrapper_version = "v2.0"
215+
216+
See VERSION_CONFLATION_IMPACT.md for full analysis of affected processors.
217+
218+
Override pattern:
219+
def build_test_info(self) -> TestInfo:
220+
base_info = super().build_test_info()
221+
benchmark_version = self._extract_benchmark_version()
222+
return TestInfo(
223+
name=self.get_test_name(),
224+
version=benchmark_version or base_info.version,
225+
wrapper_version=base_info.wrapper_version
226+
)
227+
228+
Returns:
229+
TestInfo with name, version (wrapper by default), and wrapper_version
230+
"""
200231
test_name = self.get_test_name()
201232

202-
# Try to get version from test_info file
233+
# Extract wrapper version from orchestrator's test_info file
234+
# This contains the test wrapper repository version (e.g., "v2.8" from "v2.8.tar.gz")
203235
test_info_file = self.result_dir / "test_info"
204236
version = None
205237

@@ -211,15 +243,28 @@ def build_test_info(self) -> TestInfo:
211243
# Find test in test_info
212244
for key, test_data in test_info_data.items():
213245
if test_data.get('test_name') == test_name:
214-
version = test_data.get('repo_file', '').replace('.tar.gz', '')
246+
# Extract wrapper version from repo_file
247+
# Handle various archive extensions (.tar.gz, .tar.xz, .zip, etc.)
248+
repo_file = test_data.get('repo_file', '')
249+
if isinstance(repo_file, str):
250+
# Strip common archive extensions
251+
for ext in ['.tar.gz', '.tar.xz', '.tar.bz2', '.zip', '.tgz']:
252+
if repo_file.endswith(ext):
253+
version = repo_file[:-len(ext)]
254+
break
255+
else:
256+
# No known extension found, use as-is
257+
version = repo_file if repo_file else None
215258
break
216-
except (OSError, json.JSONDecodeError, KeyError, TypeError) as e:
259+
except (OSError, json.JSONDecodeError, KeyError, TypeError, AttributeError) as e:
217260
logger.warning(f"Failed to parse test_info: {e}")
218261

219262
return TestInfo(
220263
name=test_name,
221-
version=version or "unknown",
222-
wrapper_version=version or "unknown"
264+
# NOTE: Both fields set to wrapper version by default
265+
# Processors with independent benchmark versions MUST override to fix this
266+
version=version or "unknown", # Should be benchmark version (override needed)
267+
wrapper_version=version or "unknown" # Correct: wrapper repository version
223268
)
224269

225270
def build_system_under_test(self) -> SystemUnderTest:

tests/test_base_processor.py

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
"""
2+
Tests for BaseProcessor.build_test_info() method.
3+
4+
Tests verify wrapper version extraction from test_info file
5+
and document the current behavior where both version and
6+
wrapper_version fields are set to the same value.
7+
"""
8+
9+
import json
10+
import pytest
11+
from pathlib import Path
12+
13+
from chronicler.processors.base_processor import BaseProcessor
14+
from chronicler.schema import TestInfo
15+
16+
pytestmark = pytest.mark.unit
17+
18+
19+
class MinimalProcessor(BaseProcessor):
20+
"""Minimal concrete processor for testing BaseProcessor methods."""
21+
22+
def get_test_name(self) -> str:
23+
return "minimal_test"
24+
25+
def parse_runs(self, extracted_result):
26+
return {}
27+
28+
29+
def test_build_test_info_extracts_wrapper_version_from_test_info(tmp_path):
30+
"""
31+
Verify build_test_info() extracts wrapper version from test_info file.
32+
33+
The test_info file contains wrapper repository versions (e.g., "v2.8")
34+
which should be extracted and set to both version and wrapper_version
35+
fields (current behavior).
36+
"""
37+
# Setup: Create test_info file with wrapper version
38+
test_info_data = {
39+
"minimal_test": {
40+
"test_name": "minimal_test",
41+
"repo_file": "v2.8.tar.gz"
42+
}
43+
}
44+
test_info_file = tmp_path / "test_info"
45+
test_info_file.write_text(json.dumps(test_info_data))
46+
47+
# Execute
48+
processor = MinimalProcessor(str(tmp_path))
49+
result = processor.build_test_info()
50+
51+
# Verify
52+
assert isinstance(result, TestInfo)
53+
assert result.name == "minimal_test"
54+
assert result.version == "v2.8", "Should extract wrapper version from repo_file"
55+
assert result.wrapper_version == "v2.8", "Should set wrapper_version to same value"
56+
57+
58+
def test_build_test_info_returns_unknown_when_no_test_info_file(tmp_path):
59+
"""
60+
When test_info file is missing, both version fields should be "unknown".
61+
"""
62+
# Execute (no test_info file created)
63+
processor = MinimalProcessor(str(tmp_path))
64+
result = processor.build_test_info()
65+
66+
# Verify
67+
assert result.version == "unknown"
68+
assert result.wrapper_version == "unknown"
69+
70+
71+
def test_build_test_info_returns_unknown_when_test_not_in_test_info(tmp_path):
72+
"""
73+
When test_info exists but doesn't contain the test, return "unknown".
74+
"""
75+
# Setup: Create test_info with different test
76+
test_info_data = {
77+
"other_test": {
78+
"test_name": "other_test",
79+
"repo_file": "v1.0.tar.gz"
80+
}
81+
}
82+
test_info_file = tmp_path / "test_info"
83+
test_info_file.write_text(json.dumps(test_info_data))
84+
85+
# Execute
86+
processor = MinimalProcessor(str(tmp_path))
87+
result = processor.build_test_info()
88+
89+
# Verify
90+
assert result.version == "unknown"
91+
assert result.wrapper_version == "unknown"
92+
93+
94+
def test_build_test_info_handles_malformed_json(tmp_path):
95+
"""
96+
Malformed test_info file should log warning and return "unknown".
97+
"""
98+
# Setup: Create invalid JSON
99+
test_info_file = tmp_path / "test_info"
100+
test_info_file.write_text("{ invalid json }")
101+
102+
# Execute
103+
processor = MinimalProcessor(str(tmp_path))
104+
result = processor.build_test_info()
105+
106+
# Verify
107+
assert result.version == "unknown"
108+
assert result.wrapper_version == "unknown"
109+
110+
111+
def test_build_test_info_handles_non_string_repo_file(tmp_path):
112+
"""
113+
Non-string repo_file values should be handled gracefully.
114+
115+
If test_info contains valid JSON but repo_file is null, an integer,
116+
or other non-string value, the processor should handle it gracefully
117+
rather than raising AttributeError.
118+
"""
119+
# Setup: Create test_info with null repo_file
120+
test_info_data = {
121+
"minimal_test": {
122+
"test_name": "minimal_test",
123+
"repo_file": None
124+
}
125+
}
126+
test_info_file = tmp_path / "test_info"
127+
test_info_file.write_text(json.dumps(test_info_data))
128+
129+
# Execute
130+
processor = MinimalProcessor(str(tmp_path))
131+
result = processor.build_test_info()
132+
133+
# Verify - should fall back to "unknown" rather than crash
134+
assert result.version == "unknown"
135+
assert result.wrapper_version == "unknown"
136+
137+
138+
def test_build_test_info_handles_non_dict_test_info(tmp_path):
139+
"""
140+
Non-dict test_info data should be handled gracefully.
141+
142+
If test_info contains a list or other non-dict structure,
143+
the processor should handle it gracefully rather than raising
144+
AttributeError when calling .items().
145+
"""
146+
# Setup: Create test_info with list instead of dict
147+
test_info_file = tmp_path / "test_info"
148+
test_info_file.write_text("[]")
149+
150+
# Execute
151+
processor = MinimalProcessor(str(tmp_path))
152+
result = processor.build_test_info()
153+
154+
# Verify - should fall back to "unknown" rather than crash
155+
assert result.version == "unknown"
156+
assert result.wrapper_version == "unknown"

0 commit comments

Comments
 (0)