|
| 1 | +""" |
| 2 | +FIO processor: benchmark version extraction. |
| 3 | +
|
| 4 | +Tests that FIO extracts benchmark version (e.g., "fio-3.36") from fio-results.json |
| 5 | +and stores it in test.version instead of wrapper version. |
| 6 | +
|
| 7 | +Validates RPOPC-1318. |
| 8 | +""" |
| 9 | + |
| 10 | +import json |
| 11 | +import pytest |
| 12 | +from pathlib import Path |
| 13 | +from unittest.mock import patch |
| 14 | + |
| 15 | +from chronicler.processors.fio_processor import FioProcessor |
| 16 | + |
| 17 | +pytestmark = pytest.mark.unit |
| 18 | + |
| 19 | + |
| 20 | +@pytest.fixture |
| 21 | +def result_dir_with_fio_version(tmp_path): |
| 22 | + """Create FIO result directory with version in fio-results.json and wrapper version in test_info.""" |
| 23 | + # Create fio-results.json with FIO version |
| 24 | + fio_data = { |
| 25 | + "fio version": "fio-3.36", |
| 26 | + "timestamp": 1776775499, |
| 27 | + "jobs": [ |
| 28 | + { |
| 29 | + "jobname": "test-job", |
| 30 | + "groupid": 0, |
| 31 | + "elapsed": 120, |
| 32 | + "read": { |
| 33 | + "io_bytes": 1000000, |
| 34 | + "bw": 8333, |
| 35 | + "iops": 2083, |
| 36 | + "runtime": 120000, |
| 37 | + "total_ios": 250000, |
| 38 | + "lat_ns": {"mean": 480000, "min": 100000, "max": 1000000}, |
| 39 | + "clat_ns": {"mean": 470000, "min": 90000, "max": 990000}, |
| 40 | + "slat_ns": {"mean": 10000, "min": 1000, "max": 50000} |
| 41 | + } |
| 42 | + } |
| 43 | + ] |
| 44 | + } |
| 45 | + |
| 46 | + fio_json_path = tmp_path / "fio-results.json" |
| 47 | + with open(fio_json_path, "w") as f: |
| 48 | + json.dump(fio_data, f) |
| 49 | + |
| 50 | + # Create test_info with wrapper version |
| 51 | + test_info = { |
| 52 | + "fio": { |
| 53 | + "test_name": "fio", |
| 54 | + "repo_file": "v2.1.tar.gz" |
| 55 | + } |
| 56 | + } |
| 57 | + |
| 58 | + test_info_path = tmp_path / "test_info" |
| 59 | + with open(test_info_path, "w") as f: |
| 60 | + json.dump(test_info, f) |
| 61 | + |
| 62 | + return tmp_path |
| 63 | + |
| 64 | + |
| 65 | +@pytest.fixture |
| 66 | +def result_dir_without_fio_version(tmp_path): |
| 67 | + """Create FIO result directory without version in fio-results.json.""" |
| 68 | + # Create fio-results.json WITHOUT FIO version |
| 69 | + fio_data = { |
| 70 | + "timestamp": 1776775499, |
| 71 | + "jobs": [ |
| 72 | + { |
| 73 | + "jobname": "test-job", |
| 74 | + "groupid": 0, |
| 75 | + "elapsed": 120, |
| 76 | + "read": { |
| 77 | + "io_bytes": 1000000, |
| 78 | + "bw": 8333, |
| 79 | + "iops": 2083, |
| 80 | + "runtime": 120000, |
| 81 | + "total_ios": 250000, |
| 82 | + "lat_ns": {"mean": 480000, "min": 100000, "max": 1000000}, |
| 83 | + "clat_ns": {"mean": 470000, "min": 90000, "max": 990000}, |
| 84 | + "slat_ns": {"mean": 10000, "min": 1000, "max": 50000} |
| 85 | + } |
| 86 | + } |
| 87 | + ] |
| 88 | + } |
| 89 | + |
| 90 | + fio_json_path = tmp_path / "fio-results.json" |
| 91 | + with open(fio_json_path, "w") as f: |
| 92 | + json.dump(fio_data, f) |
| 93 | + |
| 94 | + # Create test_info with wrapper version |
| 95 | + test_info = { |
| 96 | + "fio": { |
| 97 | + "test_name": "fio", |
| 98 | + "repo_file": "v2.1.tar.gz" |
| 99 | + } |
| 100 | + } |
| 101 | + |
| 102 | + test_info_path = tmp_path / "test_info" |
| 103 | + with open(test_info_path, "w") as f: |
| 104 | + json.dump(test_info, f) |
| 105 | + |
| 106 | + return tmp_path |
| 107 | + |
| 108 | + |
| 109 | +def test_fio_version_in_test_version_field(result_dir_with_fio_version): |
| 110 | + """Test that FIO benchmark version is stored in test.version.""" |
| 111 | + processor = FioProcessor(str(result_dir_with_fio_version)) |
| 112 | + extracted_result = {"files": {"fio_results_json": str(result_dir_with_fio_version / "fio-results.json")}, |
| 113 | + "extracted_path": str(result_dir_with_fio_version)} |
| 114 | + |
| 115 | + # Mock archive extraction and call build_test_info |
| 116 | + with patch.object(processor.archive_handler, "extract_result_archive") as mock_extract: |
| 117 | + mock_extract.return_value = extracted_result |
| 118 | + |
| 119 | + # Parse runs to extract FIO version (this should populate _fio_version) |
| 120 | + processor.parse_runs(extracted_result) |
| 121 | + |
| 122 | + # Now build_test_info should use the extracted FIO version |
| 123 | + test_info = processor.build_test_info() |
| 124 | + |
| 125 | + # Verify test.version contains benchmark version, not wrapper version |
| 126 | + assert test_info.version == "fio-3.36", \ |
| 127 | + f"Expected test.version='fio-3.36', got '{test_info.version}'" |
| 128 | + |
| 129 | + # Verify wrapper_version contains wrapper version |
| 130 | + assert test_info.wrapper_version == "v2.1", \ |
| 131 | + f"Expected wrapper_version='v2.1', got '{test_info.wrapper_version}'" |
| 132 | + |
| 133 | + |
| 134 | +def test_fio_version_fallback_to_wrapper_version(result_dir_without_fio_version): |
| 135 | + """Test that wrapper version is used when FIO version is missing.""" |
| 136 | + processor = FioProcessor(str(result_dir_without_fio_version)) |
| 137 | + extracted_result = {"files": {"fio_results_json": str(result_dir_without_fio_version / "fio-results.json")}, |
| 138 | + "extracted_path": str(result_dir_without_fio_version)} |
| 139 | + |
| 140 | + # Mock archive extraction and call build_test_info |
| 141 | + with patch.object(processor.archive_handler, "extract_result_archive") as mock_extract: |
| 142 | + mock_extract.return_value = extracted_result |
| 143 | + |
| 144 | + # Parse runs (no FIO version to extract) |
| 145 | + processor.parse_runs(extracted_result) |
| 146 | + |
| 147 | + # build_test_info should fall back to wrapper version |
| 148 | + test_info = processor.build_test_info() |
| 149 | + |
| 150 | + # When FIO version is missing, should fall back to wrapper version |
| 151 | + assert test_info.version == "v2.1", \ |
| 152 | + f"Expected test.version='v2.1' (fallback), got '{test_info.version}'" |
| 153 | + |
| 154 | + assert test_info.wrapper_version == "v2.1", \ |
| 155 | + f"Expected wrapper_version='v2.1', got '{test_info.wrapper_version}'" |
| 156 | + |
| 157 | + |
| 158 | +def test_fio_version_no_wrapper_version(tmp_path): |
| 159 | + """Test FIO version extraction when no wrapper version exists.""" |
| 160 | + # Create fio-results.json with FIO version but NO test_info file |
| 161 | + fio_data = { |
| 162 | + "fio version": "fio-3.36", |
| 163 | + "timestamp": 1776775499, |
| 164 | + "jobs": [ |
| 165 | + { |
| 166 | + "jobname": "test-job", |
| 167 | + "groupid": 0, |
| 168 | + "elapsed": 120, |
| 169 | + "read": { |
| 170 | + "io_bytes": 1000000, |
| 171 | + "bw": 8333, |
| 172 | + "iops": 2083, |
| 173 | + "runtime": 120000, |
| 174 | + "total_ios": 250000, |
| 175 | + "lat_ns": {"mean": 480000, "min": 100000, "max": 1000000}, |
| 176 | + "clat_ns": {"mean": 470000, "min": 90000, "max": 990000}, |
| 177 | + "slat_ns": {"mean": 10000, "min": 1000, "max": 50000} |
| 178 | + } |
| 179 | + } |
| 180 | + ] |
| 181 | + } |
| 182 | + |
| 183 | + fio_json_path = tmp_path / "fio-results.json" |
| 184 | + with open(fio_json_path, "w") as f: |
| 185 | + json.dump(fio_data, f) |
| 186 | + |
| 187 | + processor = FioProcessor(str(tmp_path)) |
| 188 | + extracted_result = {"files": {"fio_results_json": str(fio_json_path)}, |
| 189 | + "extracted_path": str(tmp_path)} |
| 190 | + |
| 191 | + # Mock archive extraction |
| 192 | + with patch.object(processor.archive_handler, "extract_result_archive") as mock_extract: |
| 193 | + mock_extract.return_value = extracted_result |
| 194 | + |
| 195 | + # Parse runs to extract FIO version |
| 196 | + processor.parse_runs(extracted_result) |
| 197 | + |
| 198 | + # build_test_info should use extracted FIO version |
| 199 | + test_info = processor.build_test_info() |
| 200 | + |
| 201 | + # Should still extract FIO version |
| 202 | + assert test_info.version == "fio-3.36", \ |
| 203 | + f"Expected test.version='fio-3.36', got '{test_info.version}'" |
| 204 | + |
| 205 | + # Wrapper version should be "unknown" when test_info is missing |
| 206 | + assert test_info.wrapper_version == "unknown", \ |
| 207 | + f"Expected wrapper_version='unknown', got '{test_info.wrapper_version}'" |
| 208 | + |
| 209 | + |
| 210 | +def test_fio_version_resets_between_parses(tmp_path): |
| 211 | + """Processor reuse: version state should not leak between parse_runs() calls.""" |
| 212 | + # First parse: fio-results.json WITH benchmark version |
| 213 | + fio_data_with_version = { |
| 214 | + "fio version": "fio-3.36", |
| 215 | + "timestamp": 1776775499, |
| 216 | + "jobs": [ |
| 217 | + { |
| 218 | + "jobname": "test-job-1", |
| 219 | + "groupid": 0, |
| 220 | + "elapsed": 120, |
| 221 | + "read": { |
| 222 | + "io_bytes": 1000000, |
| 223 | + "bw": 8333, |
| 224 | + "iops": 2083, |
| 225 | + "runtime": 120000, |
| 226 | + "total_ios": 250000, |
| 227 | + "lat_ns": {"mean": 480000, "min": 100000, "max": 1000000}, |
| 228 | + "clat_ns": {"mean": 470000, "min": 90000, "max": 990000}, |
| 229 | + "slat_ns": {"mean": 10000, "min": 1000, "max": 50000} |
| 230 | + } |
| 231 | + } |
| 232 | + ] |
| 233 | + } |
| 234 | + |
| 235 | + fio_json_path1 = tmp_path / "fio-results-1.json" |
| 236 | + with open(fio_json_path1, "w") as f: |
| 237 | + json.dump(fio_data_with_version, f) |
| 238 | + |
| 239 | + # Create test_info file with wrapper version for fallback |
| 240 | + test_info = { |
| 241 | + "fio": { |
| 242 | + "test_name": "fio", |
| 243 | + "repo_file": "v2.1.tar.gz" |
| 244 | + } |
| 245 | + } |
| 246 | + test_info_path = tmp_path / "test_info" |
| 247 | + with open(test_info_path, "w") as f: |
| 248 | + json.dump(test_info, f) |
| 249 | + |
| 250 | + processor = FioProcessor(str(tmp_path)) |
| 251 | + extracted_result1 = {"files": {"fio_results_json": str(fio_json_path1)}, |
| 252 | + "extracted_path": str(tmp_path)} |
| 253 | + |
| 254 | + # First parse |
| 255 | + with patch.object(processor.archive_handler, "extract_result_archive") as mock_extract: |
| 256 | + mock_extract.return_value = extracted_result1 |
| 257 | + processor.parse_runs(extracted_result1) |
| 258 | + test_info1 = processor.build_test_info() |
| 259 | + |
| 260 | + assert test_info1.version == "fio-3.36", "First parse should extract benchmark version" |
| 261 | + |
| 262 | + # Second parse: fio-results.json WITHOUT benchmark version (reusing same processor instance) |
| 263 | + fio_data_without_version = { |
| 264 | + "timestamp": 1776775500, |
| 265 | + "jobs": [ |
| 266 | + { |
| 267 | + "jobname": "test-job-2", |
| 268 | + "groupid": 0, |
| 269 | + "elapsed": 120, |
| 270 | + "read": { |
| 271 | + "io_bytes": 2000000, |
| 272 | + "bw": 16666, |
| 273 | + "iops": 4166, |
| 274 | + "runtime": 120000, |
| 275 | + "total_ios": 500000, |
| 276 | + "lat_ns": {"mean": 240000, "min": 50000, "max": 500000}, |
| 277 | + "clat_ns": {"mean": 235000, "min": 45000, "max": 495000}, |
| 278 | + "slat_ns": {"mean": 5000, "min": 500, "max": 25000} |
| 279 | + } |
| 280 | + } |
| 281 | + ] |
| 282 | + } |
| 283 | + |
| 284 | + fio_json_path2 = tmp_path / "fio-results-2.json" |
| 285 | + with open(fio_json_path2, "w") as f: |
| 286 | + json.dump(fio_data_without_version, f) |
| 287 | + |
| 288 | + extracted_result2 = {"files": {"fio_results_json": str(fio_json_path2)}, |
| 289 | + "extracted_path": str(tmp_path)} |
| 290 | + |
| 291 | + # Second parse with same processor instance |
| 292 | + with patch.object(processor.archive_handler, "extract_result_archive") as mock_extract: |
| 293 | + mock_extract.return_value = extracted_result2 |
| 294 | + processor.parse_runs(extracted_result2) |
| 295 | + test_info2 = processor.build_test_info() |
| 296 | + |
| 297 | + # Should fall back to wrapper version, NOT retain stale "fio-3.36" |
| 298 | + assert test_info2.version == "v2.1", \ |
| 299 | + f"Second parse should fall back to wrapper version, not retain stale 'fio-3.36'. Got '{test_info2.version}'" |
| 300 | + assert test_info2.wrapper_version == "v2.1" |
0 commit comments