|
19 | 19 | failed_metrics, write_json) # noqa: E402 |
20 | 20 |
|
21 | 21 |
|
22 | | -def resource(): |
| 22 | +def resource(resident=100, threads=2, descriptors=3): |
23 | 23 | return { |
24 | | - "residentBytes": 100, |
25 | | - "threads": 2, |
26 | | - "handlesOrDescriptors": 3, |
| 24 | + "residentBytes": resident, |
| 25 | + "threads": threads, |
| 26 | + "handlesOrDescriptors": descriptors, |
27 | 27 | } |
28 | 28 |
|
29 | 29 |
|
@@ -100,11 +100,11 @@ def valid_metrics(): |
100 | 100 | "resolveBatchResourceSamples": [ |
101 | 101 | {"batch": batch, "resources": resource()} for batch in range(2) |
102 | 102 | ], |
103 | | - "preResolveResources": resource(), |
| 103 | + "preResolveResources": resource(resident=100), |
104 | 104 | "barrierObservedResources": resource(), |
105 | 105 | "postOverlapResources": resource(), |
106 | 106 | "observedResourcePeak": resource(), |
107 | | - "resourceAfter": resource(), |
| 107 | + "resourceAfter": resource(resident=90), |
108 | 108 | "rssDeltaFromPreResolveBytes": -10, |
109 | 109 | } |
110 | 110 |
|
@@ -163,6 +163,13 @@ def write_payloads(self, *payloads): |
163 | 163 | def artifact(self): |
164 | 164 | return json.loads(self.output.read_text(encoding="utf-8")) |
165 | 165 |
|
| 166 | + def assert_metrics_rejected(self, metrics, message): |
| 167 | + self.write_payloads(json.dumps(metrics)) |
| 168 | + with self.assertRaisesRegex(MetricsError, message): |
| 169 | + extract_metrics(self.input, self.output, 0, "fast") |
| 170 | + self.assertEqual(self.artifact()["status"], "failed") |
| 171 | + self.assertFalse(self.artifact()["metricsProduced"]) |
| 172 | + |
166 | 173 | def test_no_metrics_fails_closed_and_persists_failure(self): |
167 | 174 | self.input.write_text("benchmark output only\n", encoding="utf-8") |
168 | 175 | with self.assertRaisesRegex(MetricsError, "exactly one"): |
@@ -224,6 +231,83 @@ def test_success_preserves_validated_metrics(self): |
224 | 231 | self.assertTrue(metrics["metricsProduced"]) |
225 | 232 | self.assertEqual(len(metrics["refreshScenarioSamples"]), 9) |
226 | 233 |
|
| 234 | + def test_resource_peak_rejects_low_and_high_values_for_every_field(self): |
| 235 | + for field in ("residentBytes", "threads", "handlesOrDescriptors"): |
| 236 | + for difference in (-1, 1): |
| 237 | + with self.subTest(field=field, difference=difference): |
| 238 | + metrics = valid_metrics() |
| 239 | + metrics["observedResourcePeak"][field] += difference |
| 240 | + self.assert_metrics_rejected(metrics, "observedResourcePeak") |
| 241 | + |
| 242 | + def test_resource_peak_includes_every_sample_category(self): |
| 243 | + categories = ( |
| 244 | + ("inventory", lambda metrics: metrics["inventoryResourceSamples"][0]["resources"]), |
| 245 | + ("pre-resolve", lambda metrics: metrics["preResolveResources"]), |
| 246 | + ("barrier", lambda metrics: metrics["barrierObservedResources"]), |
| 247 | + ("post-overlap", lambda metrics: metrics["postOverlapResources"]), |
| 248 | + ("resolve-batch", lambda metrics: metrics["resolveBatchResourceSamples"][0]["resources"]), |
| 249 | + ("after", lambda metrics: metrics["resourceAfter"]), |
| 250 | + ) |
| 251 | + for name, select_sample in categories: |
| 252 | + with self.subTest(category=name): |
| 253 | + metrics = valid_metrics() |
| 254 | + sample = select_sample(metrics) |
| 255 | + sample.update( |
| 256 | + residentBytes=1000, |
| 257 | + threads=1001, |
| 258 | + handlesOrDescriptors=1002, |
| 259 | + ) |
| 260 | + metrics["observedResourcePeak"] = resource(1000, 1001, 1002) |
| 261 | + metrics["rssDeltaFromPreResolveBytes"] = ( |
| 262 | + metrics["resourceAfter"]["residentBytes"] |
| 263 | + - metrics["preResolveResources"]["residentBytes"] |
| 264 | + ) |
| 265 | + self.write_payloads(json.dumps(metrics)) |
| 266 | + extract_metrics(self.input, self.output, 0, "fast") |
| 267 | + |
| 268 | + def test_resource_peak_optional_fields_support_all_null_and_mixed_samples(self): |
| 269 | + metrics = valid_metrics() |
| 270 | + samples = [ |
| 271 | + *(entry["resources"] for entry in metrics["inventoryResourceSamples"]), |
| 272 | + metrics["preResolveResources"], |
| 273 | + metrics["barrierObservedResources"], |
| 274 | + metrics["postOverlapResources"], |
| 275 | + *(entry["resources"] for entry in metrics["resolveBatchResourceSamples"]), |
| 276 | + metrics["resourceAfter"], |
| 277 | + ] |
| 278 | + for sample in samples: |
| 279 | + sample["threads"] = None |
| 280 | + sample["handlesOrDescriptors"] = None |
| 281 | + metrics["observedResourcePeak"]["threads"] = None |
| 282 | + metrics["observedResourcePeak"]["handlesOrDescriptors"] = None |
| 283 | + self.write_payloads(json.dumps(metrics)) |
| 284 | + extract_metrics(self.input, self.output, 0, "fast") |
| 285 | + |
| 286 | + samples[0]["threads"] = 7 |
| 287 | + samples[-1]["threads"] = 5 |
| 288 | + samples[1]["handlesOrDescriptors"] = 8 |
| 289 | + samples[-2]["handlesOrDescriptors"] = 6 |
| 290 | + metrics["observedResourcePeak"]["threads"] = 7 |
| 291 | + metrics["observedResourcePeak"]["handlesOrDescriptors"] = 8 |
| 292 | + self.write_payloads(json.dumps(metrics)) |
| 293 | + extract_metrics(self.input, self.output, 0, "fast") |
| 294 | + |
| 295 | + def test_rss_delta_accepts_positive_negative_and_zero_values(self): |
| 296 | + for after, expected_delta in ((110, 10), (90, -10), (100, 0)): |
| 297 | + with self.subTest(expected_delta=expected_delta): |
| 298 | + metrics = valid_metrics() |
| 299 | + metrics["resourceAfter"]["residentBytes"] = after |
| 300 | + metrics["rssDeltaFromPreResolveBytes"] = expected_delta |
| 301 | + if after > metrics["observedResourcePeak"]["residentBytes"]: |
| 302 | + metrics["observedResourcePeak"]["residentBytes"] = after |
| 303 | + self.write_payloads(json.dumps(metrics)) |
| 304 | + extract_metrics(self.input, self.output, 0, "fast") |
| 305 | + |
| 306 | + def test_inconsistent_rss_delta_fails_closed_with_failure_artifact(self): |
| 307 | + metrics = valid_metrics() |
| 308 | + metrics["rssDeltaFromPreResolveBytes"] = -9 |
| 309 | + self.assert_metrics_rejected(metrics, "rssDeltaFromPreResolveBytes") |
| 310 | + |
227 | 311 | def test_malformed_nested_types_fail_closed_with_failure_artifacts(self): |
228 | 312 | fields = [ |
229 | 313 | ("sizes", 0), ("samplesPerSize",), ("resolveConcurrency",), |
|
0 commit comments