Coverage for tests/test_report.py: 21%
139 statements
« prev ^ index » next coverage.py v7.5.0, created at 2024-04-26 02:57 -0700
« prev ^ index » next coverage.py v7.5.0, created at 2024-04-26 02:57 -0700
1# This file is part of ctrl_bps.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This software is dual licensed under the GNU General Public License and also
10# under a 3-clause BSD license. Recipients may choose which of these licenses
11# to use; please see the files gpl-3.0.txt and/or bsd_license.txt,
12# respectively. If you choose the GPL option then the following text applies
13# (but note that there is still no warranty even if you opt for BSD instead):
14#
15# This program is free software: you can redistribute it and/or modify
16# it under the terms of the GNU General Public License as published by
17# the Free Software Foundation, either version 3 of the License, or
18# (at your option) any later version.
19#
20# This program is distributed in the hope that it will be useful,
21# but WITHOUT ANY WARRANTY; without even the implied warranty of
22# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23# GNU General Public License for more details.
24#
25# You should have received a copy of the GNU General Public License
26# along with this program. If not, see <https://www.gnu.org/licenses/>.
28"""Tests for reporting mechanism."""
30import io
31import unittest
33from astropy.table import Table
34from lsst.ctrl.bps import (
35 BaseRunReport,
36 DetailedRunReport,
37 ExitCodesReport,
38 SummaryRunReport,
39 WmsJobReport,
40 WmsRunReport,
41 WmsStates,
42)
45class FakeRunReport(BaseRunReport):
46 """A fake run report."""
48 def add(self, run_report, use_global_id=False):
49 id_ = run_report.global_wms_id if use_global_id else run_report.wms_id
50 self._table.add_row([id_, run_report.state.name])
53class FakeRunReportTestCase(unittest.TestCase):
54 """Test shared methods."""
56 def setUp(self):
57 self.fields = [("ID", "S"), ("STATE", "S")]
59 self.report = FakeRunReport(self.fields)
60 self.report.add(WmsRunReport(wms_id="2.0", state=WmsStates.RUNNING))
61 self.report.add(WmsRunReport(wms_id="1.0", state=WmsStates.SUCCEEDED))
63 def testEquality(self):
64 """Test if two reports are identical."""
65 other = FakeRunReport(self.fields)
66 other.add(WmsRunReport(wms_id="2.0", state=WmsStates.RUNNING))
67 other.add(WmsRunReport(wms_id="1.0", state=WmsStates.SUCCEEDED))
68 self.assertEqual(self.report, other)
70 def testInequality(self):
71 """Test if two reports are not identical."""
72 other = FakeRunReport(self.fields)
73 other.add(WmsRunReport(wms_id="1.0", state=WmsStates.FAILED))
74 self.assertNotEqual(self.report, other)
76 def testLength(self):
77 self.assertEqual(len(self.report), 2)
79 def testClear(self):
80 """Test clearing the report."""
81 self.report.clear()
82 self.assertEqual(len(self.report), 0)
84 def testSortWithKnownKey(self):
85 """Test sorting the report using known column."""
86 expected_output = io.StringIO()
87 expected = Table(dtype=self.fields)
88 expected.add_row(["1.0", WmsStates.SUCCEEDED.name])
89 expected.add_row(["2.0", WmsStates.RUNNING.name])
90 print(expected, file=expected_output)
92 actual_output = io.StringIO()
93 self.report.sort("ID")
94 print(self.report, file=actual_output)
96 self.assertEqual(actual_output.getvalue(), expected_output.getvalue())
98 expected_output.close()
99 actual_output.close()
101 def testSortWithUnknownKey(self):
102 """Test sorting the report using unknown column."""
103 with self.assertRaises(AttributeError):
104 self.report.sort("foo")
107class SummaryRunReportTestCase(unittest.TestCase):
108 """Test a summary run report."""
110 def setUp(self):
111 self.fields = [
112 ("X", "S"),
113 ("STATE", "S"),
114 ("%S", "S"),
115 ("ID", "S"),
116 ("OPERATOR", "S"),
117 ("PROJECT", "S"),
118 ("CAMPAIGN", "S"),
119 ("PAYLOAD", "S"),
120 ("RUN", "S"),
121 ]
122 self.run = WmsRunReport(
123 wms_id="1.0",
124 global_wms_id="foo#1.0",
125 path="/path/to/run",
126 label="label",
127 run="run",
128 project="dev",
129 campaign="testing",
130 payload="test",
131 operator="tester",
132 run_summary="foo:1;bar:1",
133 state=WmsStates.RUNNING,
134 jobs=None,
135 total_number_jobs=2,
136 job_state_counts={
137 state: 1 if state in {WmsStates.SUCCEEDED, WmsStates.RUNNING} else 0 for state in WmsStates
138 },
139 job_summary=None,
140 )
141 self.report = SummaryRunReport(self.fields)
143 self.expected = Table(dtype=self.fields)
144 self.expected.add_row(["", "RUNNING", "50", "1.0", "tester", "dev", "testing", "test", "run"])
146 self.expected_output = io.StringIO()
147 self.actual_output = io.StringIO()
149 def tearDown(self):
150 self.expected_output.close()
151 self.actual_output.close()
153 def testAddWithNoFlag(self):
154 """Test adding a report for a run with no issues."""
155 print("\n".join(self.expected.pformat_all()), file=self.expected_output)
157 self.report.add(self.run)
158 print(self.report, file=self.actual_output)
160 self.assertEqual(self.actual_output.getvalue(), self.expected_output.getvalue())
162 def testAddWithFailedFlag(self):
163 """Test adding a run with a failed job."""
164 self.expected["X"][0] = "F"
165 print("\n".join(self.expected.pformat_all()), file=self.expected_output)
167 # Alter the run report to include a failed job.
168 self.run.job_state_counts = {
169 state: 1 if state in {WmsStates.FAILED, WmsStates.SUCCEEDED} else 0 for state in WmsStates
170 }
171 self.report.add(self.run)
172 print(self.report, file=self.actual_output)
174 self.assertEqual(self.actual_output.getvalue(), self.expected_output.getvalue())
176 def testAddWithHeldFlag(self):
177 """Test adding a run with a held job."""
178 self.expected["X"][0] = "H"
179 print("\n".join(self.expected.pformat_all()), file=self.expected_output)
181 # Alter the run report to include a held job.
182 self.run.job_state_counts = {
183 state: 1 if state in {WmsStates.SUCCEEDED, WmsStates.HELD} else 0 for state in WmsStates
184 }
185 self.report.add(self.run)
186 print(self.report, file=self.actual_output)
188 self.assertEqual(self.actual_output.getvalue(), self.expected_output.getvalue())
190 def testAddWithDeletedFlag(self):
191 """Test adding a run with a deleted job."""
192 self.expected["X"][0] = "D"
193 print("\n".join(self.expected.pformat_all()), file=self.expected_output)
195 # Alter the run report to include a deleted job.
196 self.run.job_state_counts = {
197 state: 1 if state in {WmsStates.SUCCEEDED, WmsStates.DELETED} else 0 for state in WmsStates
198 }
199 self.report.add(self.run)
200 print(self.report, file=self.actual_output)
202 self.assertEqual(self.actual_output.getvalue(), self.expected_output.getvalue())
205class DetailedRunReportTestCase(unittest.TestCase):
206 """Test a detailed run report."""
208 def setUp(self):
209 self.fields = [("", "S")] + [(state.name, "I") for state in WmsStates] + [("EXPECTED", "i")]
211 table = Table(dtype=self.fields)
212 table.add_row(
213 ["TOTAL"]
214 + [1 if state in {WmsStates.RUNNING, WmsStates.SUCCEEDED} else 0 for state in WmsStates]
215 + [2]
216 )
217 table.add_row(["foo"] + [1 if state == WmsStates.SUCCEEDED else 0 for state in WmsStates] + [1])
218 table.add_row(["bar"] + [1 if state == WmsStates.RUNNING else 0 for state in WmsStates] + [1])
219 self.expected = DetailedRunReport.from_table(table)
221 self.run = WmsRunReport(
222 wms_id="1.0",
223 global_wms_id="foo#1.0",
224 path="/path/to/run",
225 label="label",
226 run="run",
227 project="dev",
228 campaign="testing",
229 payload="test",
230 operator="tester",
231 run_summary="foo:1;bar:1",
232 state=WmsStates.RUNNING,
233 jobs=[
234 WmsJobReport(wms_id="1.0", name="", label="foo", state=WmsStates.SUCCEEDED),
235 WmsJobReport(wms_id="2.0", name="", label="bar", state=WmsStates.RUNNING),
236 ],
237 total_number_jobs=2,
238 job_state_counts={
239 state: 1 if state in {WmsStates.SUCCEEDED, WmsStates.RUNNING} else 0 for state in WmsStates
240 },
241 job_summary={
242 "foo": {state: 1 if state == WmsStates.SUCCEEDED else 0 for state in WmsStates},
243 "bar": {state: 1 if state == WmsStates.RUNNING else 0 for state in WmsStates},
244 },
245 )
247 self.actual = DetailedRunReport(self.fields)
249 def testAddWithJobSummary(self):
250 """Test adding a run with a job summary."""
251 self.run.jobs = None
252 self.actual.add(self.run)
254 self.assertEqual(self.actual, self.expected)
256 def testAddWithJobs(self):
257 """Test adding a run with a job info, but not job summary."""
258 self.run.job_summary = None
259 self.actual.add(self.run)
261 self.assertEqual(self.actual, self.expected)
263 def testAddWithoutJobInfo(self):
264 """Test adding a run without either a job summary or job info."""
265 self.run.jobs = None
266 self.run.job_summary = None
267 self.actual.add(self.run)
269 self.assertEqual(len(self.actual), 1)
270 self.assertRegex(self.actual.message, r"^WARNING.*incomplete")
272 def testAddWithoutRunSummary(self):
273 """Test adding a run without a run summary."""
274 table = Table(dtype=self.fields)
275 table.add_row(
276 ["TOTAL"]
277 + [1 if state in {WmsStates.RUNNING, WmsStates.SUCCEEDED} else 0 for state in WmsStates]
278 + [2]
279 )
280 table.add_row(["bar"] + [1 if state == WmsStates.RUNNING else 0 for state in WmsStates] + [-1])
281 table.add_row(["foo"] + [1 if state == WmsStates.SUCCEEDED else 0 for state in WmsStates] + [-1])
282 expected = DetailedRunReport.from_table(table)
284 self.run.run_summary = None
285 self.actual.add(self.run)
287 self.assertRegex(self.actual.message, r"^WARNING.*sorted alphabetically")
288 self.assertEqual(self.actual, expected)
291class ExitCodesReportTestCase(unittest.TestCase):
292 """Test an exit code report."""
294 def setUp(self):
295 self.fields = [
296 (" ", "S"),
297 ("PAYLOAD ERROR COUNT", "i"),
298 ("PAYLOAD ERROR CODES", "S"),
299 ("INFRASTRUCTURE ERROR COUNT", "i"),
300 ("INFRASTRUCTURE ERROR CODES", "S"),
301 ]
303 table = Table(dtype=self.fields)
304 table.add_row(["foo", 0, "None", 0, "None"])
305 table.add_row(["bar", 2, "1, 2", 2, "3, 4"])
306 self.expected = ExitCodesReport.from_table(table)
308 self.run = WmsRunReport(
309 wms_id="1.0",
310 global_wms_id="foo#1.0",
311 path="/path/to/run",
312 label="label",
313 run="run",
314 project="dev",
315 campaign="testing",
316 payload="test",
317 operator="tester",
318 run_summary="foo:1;bar:1",
319 state=WmsStates.RUNNING,
320 jobs=[
321 WmsJobReport(wms_id="1.0", name="", label="foo", state=WmsStates.SUCCEEDED),
322 WmsJobReport(wms_id="2.0", name="", label="bar", state=WmsStates.RUNNING),
323 ],
324 total_number_jobs=2,
325 job_state_counts={
326 state: 1 if state in {WmsStates.SUCCEEDED, WmsStates.RUNNING} else 0 for state in WmsStates
327 },
328 job_summary={
329 "foo": {state: 1 if state == WmsStates.SUCCEEDED else 0 for state in WmsStates},
330 "bar": {state: 1 if state == WmsStates.RUNNING else 0 for state in WmsStates},
331 },
332 exit_code_summary={
333 "foo": [],
334 "bar": [1, 2, 3, 4],
335 },
336 )
338 self.actual = ExitCodesReport(self.fields)
340 def testAddWithJobSummary(self):
341 """Test adding a run with a job summary."""
342 self.run.jobs = None
343 self.actual.add(self.run)
344 self.assertEqual(self.actual, self.expected)
346 def testAddWithJobs(self):
347 """Test adding a run with a job info, but not job summary."""
348 self.run.job_summary = None
349 self.actual.add(self.run)
350 self.assertEqual(self.actual, self.expected)
352 def testAddWithoutRunSummary(self):
353 """Test adding a run without a run summary."""
354 self.run.run_summary = None
355 self.actual.add(self.run)
356 self.assertRegex(self.actual.message, r"^WARNING.*incomplete")
359if __name__ == "__main__": 359 ↛ 360line 359 didn't jump to line 360, because the condition on line 359 was never true
360 unittest.main()