Coverage for tests/test_report.py: 21%

139 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-03-26 02:56 -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/>. 

27 

28"""Tests for reporting mechanism.""" 

29 

30import io 

31import unittest 

32 

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) 

43 

44 

45class FakeRunReport(BaseRunReport): 

46 """A fake run report.""" 

47 

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]) 

51 

52 

53class FakeRunReportTestCase(unittest.TestCase): 

54 """Test shared methods.""" 

55 

56 def setUp(self): 

57 self.fields = [("ID", "S"), ("STATE", "S")] 

58 

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)) 

62 

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) 

69 

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) 

75 

76 def testLength(self): 

77 self.assertEqual(len(self.report), 2) 

78 

79 def testClear(self): 

80 """Test clearing the report.""" 

81 self.report.clear() 

82 self.assertEqual(len(self.report), 0) 

83 

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) 

91 

92 actual_output = io.StringIO() 

93 self.report.sort("ID") 

94 print(self.report, file=actual_output) 

95 

96 self.assertEqual(actual_output.getvalue(), expected_output.getvalue()) 

97 

98 expected_output.close() 

99 actual_output.close() 

100 

101 def testSortWithUnknownKey(self): 

102 """Test sorting the report using unknown column.""" 

103 with self.assertRaises(AttributeError): 

104 self.report.sort("foo") 

105 

106 

107class SummaryRunReportTestCase(unittest.TestCase): 

108 """Test a summary run report.""" 

109 

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) 

142 

143 self.expected = Table(dtype=self.fields) 

144 self.expected.add_row(["", "RUNNING", "50", "1.0", "tester", "dev", "testing", "test", "run"]) 

145 

146 self.expected_output = io.StringIO() 

147 self.actual_output = io.StringIO() 

148 

149 def tearDown(self): 

150 self.expected_output.close() 

151 self.actual_output.close() 

152 

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) 

156 

157 self.report.add(self.run) 

158 print(self.report, file=self.actual_output) 

159 

160 self.assertEqual(self.actual_output.getvalue(), self.expected_output.getvalue()) 

161 

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) 

166 

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) 

173 

174 self.assertEqual(self.actual_output.getvalue(), self.expected_output.getvalue()) 

175 

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) 

180 

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) 

187 

188 self.assertEqual(self.actual_output.getvalue(), self.expected_output.getvalue()) 

189 

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) 

194 

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) 

201 

202 self.assertEqual(self.actual_output.getvalue(), self.expected_output.getvalue()) 

203 

204 

205class DetailedRunReportTestCase(unittest.TestCase): 

206 """Test a detailed run report.""" 

207 

208 def setUp(self): 

209 self.fields = [("", "S")] + [(state.name, "I") for state in WmsStates] + [("EXPECTED", "i")] 

210 

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) 

220 

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 ) 

246 

247 self.actual = DetailedRunReport(self.fields) 

248 

249 def testAddWithJobSummary(self): 

250 """Test adding a run with a job summary.""" 

251 self.run.jobs = None 

252 self.actual.add(self.run) 

253 

254 self.assertEqual(self.actual, self.expected) 

255 

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) 

260 

261 self.assertEqual(self.actual, self.expected) 

262 

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) 

268 

269 self.assertEqual(len(self.actual), 1) 

270 self.assertRegex(self.actual.message, r"^WARNING.*incomplete") 

271 

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) 

283 

284 self.run.run_summary = None 

285 self.actual.add(self.run) 

286 

287 self.assertRegex(self.actual.message, r"^WARNING.*sorted alphabetically") 

288 self.assertEqual(self.actual, expected) 

289 

290 

291class ExitCodesReportTestCase(unittest.TestCase): 

292 """Test an exit code report.""" 

293 

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 ] 

302 

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) 

307 

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 ) 

337 

338 self.actual = ExitCodesReport(self.fields) 

339 

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) 

345 

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) 

351 

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") 

357 

358 

359if __name__ == "__main__": 359 ↛ 360line 359 didn't jump to line 360, because the condition on line 359 was never true

360 unittest.main()