|
1 | | -import batchtools.prom_metrics as pm |
2 | | -import types |
3 | | -from datetime import datetime |
| 1 | +import re |
4 | 2 | from unittest import mock |
| 3 | +import batchtools.prom_metrics as pm |
5 | 4 |
|
6 | 5 |
|
7 | | -def test_now_rfc3339_parses_with_timezone(): |
8 | | - s = pm.now_rfc3339() |
9 | | - # ISO 8601 parseable and includes timezone info (+00:00) |
10 | | - dt = datetime.fromisoformat(s) |
11 | | - assert dt.tzinfo is not None |
12 | | - assert s.endswith("+00:00") |
13 | | - |
14 | | - |
15 | | -def _labels(job="job-x", gpu="none", queue="dummy-localqueue", instance="ns"): |
16 | | - return {"job": job, "gpu": gpu, "queue": queue, "instance": instance} |
17 | | - |
18 | | - |
19 | | -def _registry_text() -> str: |
20 | | - body, ctype = pm.generate_metrics_text() |
21 | | - assert isinstance(body, str) |
22 | | - assert ctype.startswith("text/plain") |
23 | | - return body |
| 6 | +def _base_labels(): |
| 7 | + return { |
| 8 | + "job_name": "job-none-xyz", |
| 9 | + "gpu": "none", |
| 10 | + "queue": "dummy-localqueue", |
| 11 | + "instance": pm.PROMETHEUS_INSTANCE or "test", |
| 12 | + } |
24 | 13 |
|
25 | 14 |
|
26 | 15 | def test_record_batch_observation_updates_hist_and_counters(): |
27 | | - labels = _labels() |
28 | | - pm.record_batch_observation(labels=labels, elapsed_sec=7.5, result="succeeded") |
29 | | - text = _registry_text() |
30 | | - assert "batch_duration_seconds_bucket" in text |
31 | | - assert "batch_duration_total_seconds" in text # counter that sums durations |
32 | | - assert "batch_runs_total" in text |
33 | | - # spot-check that our label set appears on at least one line |
34 | | - for k, v in {**labels, "result": "succeeded"}.items(): |
35 | | - assert f'{k}="{v}"' in text |
| 16 | + labels = _base_labels() |
| 17 | + pm.record_batch_observation(labels=labels, elapsed_sec=1.23, result="succeeded") |
| 18 | + body, _ = pm.generate_metrics_text() |
| 19 | + # Verify histogram/counter series were created with the expected labels |
| 20 | + assert "batch_duration_seconds_bucket" in body |
| 21 | + assert "batch_duration_total_seconds" in body |
| 22 | + assert "batch_runs_total" in body |
| 23 | + # spot-check that our labelset is present |
| 24 | + assert 'job_name="job-none-xyz"' in body |
| 25 | + assert 'queue="dummy-localqueue"' in body |
| 26 | + assert 'result="succeeded"' in body |
36 | 27 |
|
37 | 28 |
|
38 | 29 | def test_record_queue_and_wall_observation_update_metrics(): |
39 | | - labels = _labels(job="job-y") |
40 | | - pm.record_queue_observation(labels=labels, elapsed_sec=3.0, result="succeeded") |
41 | | - pm.record_wall_observation(labels=labels, elapsed_sec=10.0, result="succeeded") |
42 | | - text = _registry_text() |
43 | | - assert "batch_queue_wait_seconds_bucket" in text |
44 | | - assert "batch_queue_wait_total_seconds" in text |
45 | | - assert "batch_total_wall_seconds_bucket" in text |
46 | | - assert "batch_total_wall_total_seconds" in text |
47 | | - |
48 | | - |
49 | | -def _metric_samples(text: str, name: str): |
50 | | - """Yield lines for a metric family (not HELP/TYPE).""" |
51 | | - for line in text.splitlines(): |
52 | | - if line.startswith(name) and not line.startswith("#"): |
53 | | - yield line |
54 | | - |
55 | | - |
56 | | -def _any_sample_with_value(text: str, name: str, value: float) -> bool: |
57 | | - target = f" {value:.1f}" |
58 | | - return any(line.endswith(target) for line in _metric_samples(text, name)) |
| 30 | + labels = _base_labels() |
| 31 | + pm.record_queue_observation(labels=labels, elapsed_sec=2.5, result="succeeded") |
| 32 | + pm.record_wall_observation(labels=labels, elapsed_sec=3.5, result="succeeded") |
| 33 | + body, _ = pm.generate_metrics_text() |
| 34 | + assert "batch_queue_wait_seconds_bucket" in body |
| 35 | + assert "batch_queue_wait_total_seconds" in body |
| 36 | + assert "batch_total_wall_seconds_bucket" in body |
| 37 | + assert "batch_total_wall_total_seconds" in body |
59 | 38 |
|
60 | 39 |
|
61 | 40 | def test_set_in_progress_inc_and_dec_affect_gauge(): |
62 | | - labels = _labels(job="job-z") |
| 41 | + labels = _base_labels() |
| 42 | + # inc |
63 | 43 | pm.set_in_progress(labels=labels, result="running", inc=True) |
64 | | - text1 = _registry_text() |
65 | | - assert "batch_in_progress" in text1 |
66 | | - assert _any_sample_with_value(text1, "batch_in_progress", 1.0) |
67 | | - |
| 44 | + body_inc, _ = pm.generate_metrics_text() |
| 45 | + assert "batch_in_progress" in body_inc |
| 46 | + assert 'result="running"' in body_inc |
| 47 | + # dec (should not throw; series may remain visible in exposition) |
68 | 48 | pm.set_in_progress(labels=labels, result="running", inc=False) |
69 | | - text2 = _registry_text() |
70 | | - assert _any_sample_with_value(text2, "batch_in_progress", 0.0) |
| 49 | + body_dec, _ = pm.generate_metrics_text() |
| 50 | + assert "batch_in_progress" in body_dec |
71 | 51 |
|
72 | 52 |
|
73 | | -def test_generate_metrics_text_returns_valid_payload_and_ctype(): |
74 | | - body, ctype = pm.generate_metrics_text() |
75 | | - assert "# HELP" in body |
76 | | - assert "# TYPE" in body |
77 | | - assert ctype.startswith("text/plain") |
78 | | - |
79 | | - |
80 | | -def test_push_registry_text_no_url_prints_payload(capsys): |
81 | | - # Temporarily clear the push URL so it prints instead of POSTing |
82 | | - with mock.patch.object(pm, "PROMETHEUS_PUSH_URL", "", create=True): |
83 | | - pm.push_registry_text() |
| 53 | +def test_push_registry_text_no_url_prints_payload(capsys, monkeypatch): |
| 54 | + # Simulate "no address configured" by blanking the module variable. |
| 55 | + monkeypatch.setattr(pm, "PUSHGATEWAY_ADDR", "", raising=False) |
| 56 | + pm.push_registry_text() |
84 | 57 | out = capsys.readouterr().out |
85 | | - assert "PROMETHEUS_PUSH_URL not set" in out |
86 | | - assert "# HELP" in out # the metrics text is printed |
87 | | - |
88 | | - |
89 | | -def test_push_registry_text_posts_success(capsys): |
90 | | - with ( |
91 | | - mock.patch.object( |
92 | | - pm, "PROMETHEUS_PUSH_URL", "http://example/metrics", create=True |
93 | | - ), |
94 | | - mock.patch.object(pm.subprocess, "run") as mock_run, |
95 | | - ): |
96 | | - mock_run.return_value = types.SimpleNamespace(returncode=0) |
97 | | - |
98 | | - pm.push_registry_text() |
99 | | - out = capsys.readouterr().out |
100 | | - assert "metrics successfully pushed" in out |
| 58 | + assert "PUSHGATEWAY_ADDR not set; below is the metrics payload:" in out |
| 59 | + assert "# HELP" in out and "# TYPE" in out # payload printed |
101 | 60 |
|
102 | | - # Verify we invoked curl with POST and sent our payload |
103 | | - assert mock_run.called |
104 | | - args, kwargs = mock_run.call_args |
105 | | - argv = args[0] |
106 | | - assert "curl" in argv[0] |
107 | | - assert "-X" in argv and "POST" in argv |
108 | | - assert "http://example/metrics" in argv |
109 | 61 |
|
110 | | - payload = kwargs.get("input", b"").decode("utf-8") |
111 | | - assert "# HELP" in payload |
112 | | - assert "# TYPE" in payload |
113 | | - |
114 | | - |
115 | | -def test_push_registry_text_posts_failure(capsys): |
116 | | - with ( |
117 | | - mock.patch.object( |
118 | | - pm, "PROMETHEUS_PUSH_URL", "http://example/metrics", create=True |
119 | | - ), |
120 | | - mock.patch.object(pm.subprocess, "run") as mock_run, |
121 | | - ): |
122 | | - mock_run.return_value = types.SimpleNamespace(returncode=7) |
123 | | - |
124 | | - pm.push_registry_text() |
125 | | - out = capsys.readouterr().out |
126 | | - assert "curl returned nonzero exit 7" in out |
| 62 | +def test_push_registry_text_posts_success(capsys, monkeypatch): |
| 63 | + monkeypatch.setattr( |
| 64 | + pm, "PUSHGATEWAY_ADDR", "pushgateway.example:9091", raising=False |
| 65 | + ) |
| 66 | + with mock.patch.object(pm, "pushadd_to_gateway", autospec=True) as m: |
| 67 | + pm.push_registry_text(grouping_key={"instance": "test", "job_name": "job-1"}) |
| 68 | + m.assert_called_once() |
| 69 | + out = capsys.readouterr().out |
| 70 | + assert "PROM: metrics pushed to pushgateway=pushgateway.example:9091" in out |
127 | 71 |
|
128 | 72 |
|
129 | | -def test_push_registry_text_handles_exception(capsys): |
130 | | - with ( |
131 | | - mock.patch.object( |
132 | | - pm, "PROMETHEUS_PUSH_URL", "http://example/metrics", create=True |
133 | | - ), |
134 | | - mock.patch.object(pm.subprocess, "run", side_effect=RuntimeError("boom")), |
| 73 | +def test_push_registry_text_posts_failure(capsys, monkeypatch): |
| 74 | + monkeypatch.setattr( |
| 75 | + pm, "PUSHGATEWAY_ADDR", "pushgateway.example:9091", raising=False |
| 76 | + ) |
| 77 | + with mock.patch.object( |
| 78 | + pm, "pushadd_to_gateway", side_effect=Exception("boom"), autospec=True |
135 | 79 | ): |
136 | | - pm.push_registry_text() |
| 80 | + pm.push_registry_text(grouping_key={"instance": "test", "job_name": "job-2"}) |
137 | 81 | out = capsys.readouterr().out |
138 | | - assert "failed to push metrics via curl" in out |
| 82 | + # Example: "PROM: failed to push metrics to pushgateway pushgateway.example:9091: boom" |
| 83 | + assert re.search(r"PROM: failed to push metrics to pushgateway .*: boom", out) |
0 commit comments