Coverage for tests/test_tmaUtils.py: 24%

356 statements  

« prev     ^ index     » next       coverage.py v7.5.1, created at 2024-05-16 05:26 -0700

1# This file is part of summit_utils. 

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 program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <https://www.gnu.org/licenses/>. 

21 

22"""Test cases for utils.""" 

23 

24import asyncio 

25import os 

26import unittest 

27 

28import matplotlib.pyplot as plt 

29import numpy as np 

30import pandas as pd 

31from astropy.time import TimeDelta 

32from utils import getVcr 

33 

34import lsst.utils.tests 

35from lsst.summit.utils.efdUtils import calcNextDay, getDayObsStartTime, makeEfdClient 

36from lsst.summit.utils.enums import PowerState 

37from lsst.summit.utils.tmaUtils import ( 

38 AxisMotionState, 

39 TMAEvent, 

40 TMAEventMaker, 

41 TMAState, 

42 TMAStateMachine, 

43 _initializeTma, 

44 filterBadValues, 

45 getAxisAndType, 

46 getAzimuthElevationDataForEvent, 

47 getCommandsDuringEvent, 

48 getSlewsFromEventList, 

49 getTracksFromEventList, 

50 plotEvent, 

51) 

52 

53__all__ = [ 

54 "writeNewTmaEventTestTruthValues", 

55] 

56 

57TESTDIR = os.path.abspath(os.path.dirname(__file__)) 

58vcr = getVcr() 

59 

60 

61def getTmaEventTestTruthValues(): 

62 """Get the current truth values for the TMA event test cases. 

63 

64 Returns 

65 ------- 

66 seqNums : `np.array` of `int` 

67 The sequence numbers of the events. 

68 startRows : `np.array` of `int` 

69 The _startRow numbers of the events. 

70 endRows : `np.array` of `int` 

71 The _endRow numbers of the events. 

72 types : `np.array` of `str` 

73 The event types, as a string, i.e. the ``TMAEvent.name`` of the event's 

74 ``event.type``. 

75 endReasons : `np.array` of `str` 

76 The event end reasons, as a string, i.e. the ``TMAEvent.name`` of the 

77 event's ``event.endReason``. 

78 """ 

79 dataFilename = os.path.join(TESTDIR, "data", "tmaEventData.txt") 

80 

81 seqNums, startRows, endRows, types, endReasons = np.genfromtxt( 

82 dataFilename, delimiter=",", dtype=None, names=True, encoding="utf-8", unpack=True 

83 ) 

84 return seqNums, startRows, endRows, types, endReasons 

85 

86 

87def writeNewTmaEventTestTruthValues(): 

88 """This function is used to write out the truth values for the test cases. 

89 

90 If the internal event creation logic changes, these values can change, and 

91 will need to be updated. Run this function, and check the new values into 

92 git. 

93 

94 Note: if you have cause to update values with this function, make sure to 

95 update the version number on the TMAEvent class. 

96 """ 

97 dayObs = 20230531 # obviously must match the day in the test class 

98 

99 eventMaker = TMAEventMaker() 

100 events = eventMaker.getEvents(dayObs) 

101 

102 dataFilename = os.path.join(TESTDIR, "data", "tmaEventData.txt") 

103 

104 columnHeader = "seqNum,startRow,endRow,type,endReason" 

105 with open(dataFilename, "w") as f: 

106 f.write(columnHeader + "\n") 

107 for event in events: 

108 line = ( 

109 f"{event.seqNum},{event._startRow},{event._endRow},{event.type.name}," 

110 f"{event.endReason.name}" 

111 ) 

112 f.write(line + "\n") 

113 

114 

115def makeValid(tma): 

116 """Helper function to turn a TMA into a valid state.""" 

117 for name, value in tma._parts.items(): 

118 if value == tma._UNINITIALIZED_VALUE: 

119 tma._parts[name] = 1 

120 

121 

122def _turnOn(tma): 

123 """Helper function to turn TMA axes on for testing. 

124 

125 Do not call directly in normal usage or code, as this just arbitrarily 

126 sets values to turn the axes on. 

127 

128 Parameters 

129 ---------- 

130 tma : `lsst.summit.utils.tmaUtils.TMAStateMachine` 

131 The TMA state machine model to initialize. 

132 """ 

133 tma._parts["azimuthSystemState"] = PowerState.ON 

134 tma._parts["elevationSystemState"] = PowerState.ON 

135 

136 

137class TmaUtilsTestCase(lsst.utils.tests.TestCase): 

138 def test_tmaInit(self): 

139 tma = TMAStateMachine() 

140 self.assertFalse(tma._isValid) 

141 

142 # setting one axis should not make things valid 

143 tma._parts["azimuthMotionState"] = 1 

144 self.assertFalse(tma._isValid) 

145 

146 # setting all the other components should make things valid 

147 tma._parts["azimuthInPosition"] = 1 

148 tma._parts["azimuthSystemState"] = 1 

149 tma._parts["elevationInPosition"] = 1 

150 tma._parts["elevationMotionState"] = 1 

151 tma._parts["elevationSystemState"] = 1 

152 self.assertTrue(tma._isValid) 

153 

154 def test_tmaReferences(self): 

155 """Check the linkage between the component lists and the _parts 

156 dict. 

157 """ 

158 tma = TMAStateMachine() 

159 

160 # setting one axis should not make things valid 

161 self.assertEqual(tma._parts["azimuthMotionState"], tma._UNINITIALIZED_VALUE) 

162 self.assertEqual(tma._parts["elevationMotionState"], tma._UNINITIALIZED_VALUE) 

163 tma.motion[0] = AxisMotionState.TRACKING # set azimuth to 0 

164 tma.motion[1] = AxisMotionState.TRACKING # set azimuth to 0 

165 self.assertEqual(tma._parts["azimuthMotionState"], AxisMotionState.TRACKING) 

166 self.assertEqual(tma._parts["elevationMotionState"], AxisMotionState.TRACKING) 

167 

168 def test_getAxisAndType(self): 

169 # check both the long and short form names work 

170 for s in ["azimuthMotionState", "lsst.sal.MTMount.logevent_azimuthMotionState"]: 

171 self.assertEqual(getAxisAndType(s), ("azimuth", "MotionState")) 

172 

173 # check in position, and use elevation instead of azimuth to test that 

174 for s in ["elevationInPosition", "lsst.sal.MTMount.logevent_elevationInPosition"]: 

175 self.assertEqual(getAxisAndType(s), ("elevation", "InPosition")) 

176 

177 for s in ["azimuthSystemState", "lsst.sal.MTMount.logevent_azimuthSystemState"]: 

178 self.assertEqual(getAxisAndType(s), ("azimuth", "SystemState")) 

179 

180 def test_initStateLogic(self): 

181 tma = TMAStateMachine() 

182 self.assertFalse(tma._isValid) 

183 self.assertFalse(tma.isMoving) 

184 self.assertFalse(tma.canMove) 

185 self.assertFalse(tma.isTracking) 

186 self.assertFalse(tma.isSlewing) 

187 self.assertEqual(tma.state, TMAState.UNINITIALIZED) 

188 

189 _initializeTma(tma) # we're valid, but still aren't moving and can't 

190 self.assertTrue(tma._isValid) 

191 self.assertNotEqual(tma.state, TMAState.UNINITIALIZED) 

192 self.assertTrue(tma.canMove) 

193 self.assertTrue(tma.isNotMoving) 

194 self.assertFalse(tma.isMoving) 

195 self.assertFalse(tma.isTracking) 

196 self.assertFalse(tma.isSlewing) 

197 

198 _turnOn(tma) # can now move, still valid, but not in motion 

199 self.assertTrue(tma._isValid) 

200 self.assertTrue(tma.canMove) 

201 self.assertTrue(tma.isNotMoving) 

202 self.assertFalse(tma.isMoving) 

203 self.assertFalse(tma.isTracking) 

204 self.assertFalse(tma.isSlewing) 

205 

206 # consider manipulating the axes by hand here and testing these? 

207 # it's likely not worth it, given how much this exercised elsewhere, 

208 # but these are the only functions not yet being directly tested 

209 # tma._axesInFault() 

210 # tma._axesOff() 

211 # tma._axesOn() 

212 # tma._axesInMotion() 

213 # tma._axesTRACKING() 

214 # tma._axesInPosition() 

215 

216 

217@vcr.use_cassette() 

218class TMAEventMakerTestCase(lsst.utils.tests.TestCase): 

219 @classmethod 

220 @vcr.use_cassette() 

221 def setUpClass(cls): 

222 try: 

223 cls.client = makeEfdClient(testing=True) 

224 except RuntimeError: 

225 raise unittest.SkipTest("Could not instantiate an EFD client") 

226 

227 cls.dayObs = 20230531 

228 cls.dayObsWithBlockInfo = 20230615 

229 # get a sample expRecord here to test expRecordToTimespan 

230 cls.tmaEventMaker = TMAEventMaker(cls.client) 

231 cls.events = cls.tmaEventMaker.getEvents(cls.dayObs) # does the fetch 

232 cls.sampleData = cls.tmaEventMaker._data[cls.dayObs] # pull the data from the object and test length 

233 

234 @vcr.use_cassette() 

235 def tearDown(self): 

236 loop = asyncio.get_event_loop() 

237 loop.run_until_complete(self.client.influx_client.close()) 

238 

239 @vcr.use_cassette() 

240 def test_events(self): 

241 data = self.sampleData 

242 self.assertIsInstance(data, pd.DataFrame) 

243 self.assertEqual(len(data), 993) 

244 

245 @vcr.use_cassette() 

246 def test_rowDataForValues(self): 

247 rowsFor = set(self.sampleData["rowFor"]) 

248 self.assertEqual(len(rowsFor), 6) 

249 

250 # hard coding these ensures that you can't extend the axes/model 

251 # without being explicit about it here. 

252 correct = { 

253 "azimuthInPosition", 

254 "azimuthMotionState", 

255 "azimuthSystemState", 

256 "elevationInPosition", 

257 "elevationMotionState", 

258 "elevationSystemState", 

259 } 

260 self.assertSetEqual(rowsFor, correct) 

261 

262 @vcr.use_cassette() 

263 def test_monotonicTimeInDataframe(self): 

264 # ensure that each row is later than the previous 

265 times = self.sampleData["private_efdStamp"] 

266 self.assertTrue(np.all(np.diff(times) > 0)) 

267 

268 @vcr.use_cassette() 

269 def test_monotonicTimeApplicationOfRows(self): 

270 # ensure you can apply rows in the correct order 

271 tma = TMAStateMachine() 

272 row1 = self.sampleData.iloc[0] 

273 row2 = self.sampleData.iloc[1] 

274 

275 # just running this check it is OK 

276 tma.apply(row1) 

277 tma.apply(row2) 

278 

279 # and that if you apply them in reverse order then things will raise 

280 tma = TMAStateMachine() 

281 with self.assertRaises(ValueError): 

282 tma.apply(row2) 

283 tma.apply(row1) 

284 

285 @vcr.use_cassette() 

286 def test_fullDaySequence(self): 

287 # make sure we can apply all the data from the day without falling 

288 # through the logic sieve 

289 for engineering in (True, False): 

290 tma = TMAStateMachine(engineeringMode=engineering) 

291 

292 _initializeTma(tma) 

293 

294 for rowNum, row in self.sampleData.iterrows(): 

295 tma.apply(row) 

296 

297 @vcr.use_cassette() 

298 def test_endToEnd(self): 

299 eventMaker = self.tmaEventMaker 

300 events = eventMaker.getEvents(self.dayObs) 

301 self.assertIsInstance(events, list) 

302 self.assertEqual(len(events), 200) 

303 self.assertIsInstance(events[0], TMAEvent) 

304 

305 slews = [e for e in events if e.type == TMAState.SLEWING] 

306 tracks = [e for e in events if e.type == TMAState.TRACKING] 

307 self.assertEqual(len(slews), 157) 

308 self.assertEqual(len(tracks), 43) 

309 

310 seqNums, startRows, endRows, types, endReasons = getTmaEventTestTruthValues() 

311 for eventNum, event in enumerate(events): 

312 self.assertEqual(event.seqNum, seqNums[eventNum]) 

313 self.assertEqual(event._startRow, startRows[eventNum]) 

314 self.assertEqual(event._endRow, endRows[eventNum]) 

315 self.assertEqual(event.type.name, types[eventNum]) 

316 self.assertEqual(event.endReason.name, endReasons[eventNum]) 

317 

318 eventSet = set(slews) # check we can hash 

319 eventSet.update(slews) # check it ignores duplicates 

320 self.assertEqual(len(eventSet), len(slews)) 

321 

322 @vcr.use_cassette() 

323 def test_noDataBehaviour(self): 

324 eventMaker = self.tmaEventMaker 

325 noDataDayObs = 19500101 # do not use 19700101 - there is data for that day! 

326 with self.assertLogs(level="WARNING") as cm: 

327 correctMsg = f"No EFD data found for dayObs={noDataDayObs}" 

328 events = eventMaker.getEvents(noDataDayObs) 

329 self.assertIsInstance(events, list) 

330 self.assertEqual(len(events), 0) 

331 msg = cm.output[0] 

332 self.assertIn(correctMsg, msg) 

333 

334 @vcr.use_cassette() 

335 def test_helperFunctions(self): 

336 eventMaker = self.tmaEventMaker 

337 events = eventMaker.getEvents(self.dayObs) 

338 

339 slews = [e for e in events if e.type == TMAState.SLEWING] 

340 tracks = [e for e in events if e.type == TMAState.TRACKING] 

341 foundSlews = getSlewsFromEventList(events) 

342 foundTracks = getTracksFromEventList(events) 

343 self.assertEqual(slews, foundSlews) 

344 self.assertEqual(tracks, foundTracks) 

345 

346 def test_filterBadValues(self): 

347 # NB: if you add enough spurious values that the median is no longer 

348 # the value around which your "good" values are oscillating the first 

349 # two points will get replaced and this can be very confusing! 

350 

351 # test no bad values 

352 # mean = median = 1.0 

353 values = np.array([1.0, 0.96, 1.0, 1.04, 0.95, 1.0, 1.05, 1.0, 1.05, 1.0, 0.95]) 

354 mean = np.mean(values) 

355 nReplaced = filterBadValues(values) 

356 self.assertEqual(nReplaced, 0) 

357 self.assertEqual(np.mean(values), mean) 

358 

359 # test with one bad values 

360 values = np.array([1.0, 0.96, 1.0, 1.04, 2.95, 1.0, 1.05, 1.0, 1.05, 1.0, 0.95]) 

361 nReplaced = filterBadValues(values) 

362 self.assertEqual(nReplaced, 1) 

363 

364 # test with two consecutive bad values 

365 values = np.array([1.0, 0.96, 1.0, 1.04, 2.95, 3.0, 1.05, 1.0, 1.05, 1.0, 0.95]) 

366 nReplaced = filterBadValues(values) 

367 self.assertEqual(nReplaced, 2) 

368 

369 # test with three consecutive bad values 

370 values = np.array([1.0, 0.96, 1.0, 1.04, 2.95, 3.0, 4.05, 1.0, 1.05, 1.0, 0.95]) 

371 nReplaced = filterBadValues(values) 

372 self.assertEqual(nReplaced, 3) 

373 

374 # test with three consecutive bad values and another at the end 

375 values = np.array([1.0, 0.96, 1.0, 1.04, 2.95, 3.0, 4.05, 1.0, 1.05, 1.0, 3.95]) 

376 nReplaced = filterBadValues(values) 

377 self.assertEqual(nReplaced, 4) 

378 

379 # test with more than three consecutive bad values 

380 values = np.array([1.0, 0.96, 1.0, 1.04, 2.95, 3.0, 4.05, 5.0, 1.05, 1.0, 0.95]) 

381 nReplaced = filterBadValues(values) 

382 self.assertEqual(nReplaced, 3) 

383 self.assertIn(5.0, values) # check the last bad value is still there specifically 

384 

385 # test with more than three consecutive bad values and another bad 

386 # value at the end 

387 values = np.array([1.0, 0.96, 1.0, 1.04, 2.95, 3.0, 4.05, 5.0, 1.05, 1.0, 2.95]) 

388 nReplaced = filterBadValues(values) 

389 self.assertEqual(nReplaced, 4) 

390 

391 # test with bad values in first two positions 

392 values = np.array([2.0, 1.96, 1.0, 1.04, 0.95, 1.0, 1.05, 1.0, 1.05, 1.0, 0.95]) # median = 1.0 

393 nReplaced = filterBadValues(values) 

394 self.assertEqual(nReplaced, 2) 

395 

396 # test with bad values in first two positions and one in the middle 

397 values = np.array([2.0, 1.96, 1.0, 1.04, 0.95, 5.0, 1.04, 1.0, 1.05, 1.0, 0.95]) 

398 nReplaced = filterBadValues(values) 

399 self.assertEqual(nReplaced, 3) 

400 

401 # check that the last two good values are always used for correction, 

402 # including when there are more than three consecutive bad values. 

403 values = np.array([1.0, 0.96, 1.0, 1.02, 2.95, 3.0, 4.05, 5.0, 1.05, 1.0, 2.95]) 

404 expected = np.array([1.0, 0.96, 1.0, 1.02, 1.01, 1.01, 1.01, 5.0, 1.05, 1.0, 1.025]) 

405 nReplaced = filterBadValues(values) 

406 residuals = np.abs(values - expected) 

407 self.assertEqual(nReplaced, 4) 

408 self.assertTrue(np.all(residuals < 1e-6)) 

409 

410 # check with one good point after an overflowing run of bad to make 

411 # sure the correction is always applied with good values, not the naive 

412 # average of the last two even if they might be bad 

413 values = np.array([1.0, 0.96, 1.0, 1.02, 2.95, 3.0, 4.05, 5.0, 1.05, 2.95, 1.0]) 

414 expected = np.array([1.0, 0.96, 1.0, 1.02, 1.01, 1.01, 1.01, 5.0, 1.05, 1.035, 1.0]) 

415 nReplaced = filterBadValues(values) 

416 residuals = np.abs(values - expected) 

417 self.assertEqual(nReplaced, 4) 

418 self.assertTrue(np.all(residuals < 1e-6)) 

419 

420 # check with non-default maxDelta 

421 values = np.array([1.0, 0.96, 1.0, 1.02, 2.95, 3.0, 4.05, 5.0, 1.05, 1.0, 2.95]) 

422 nReplaced = filterBadValues(values, maxDelta=10) 

423 self.assertEqual(nReplaced, 0) 

424 

425 values = np.array( 

426 [ 

427 1.0, 

428 1.0, 

429 1.0, 

430 1.1, 

431 1.0, 

432 1.0, 

433 1.0, 

434 1.0, 

435 1.0, 

436 1.0, 

437 ] 

438 ) 

439 nReplaced = filterBadValues(values, maxDelta=0.01) 

440 self.assertEqual(nReplaced, 1) 

441 

442 @vcr.use_cassette() 

443 def test_getEvent(self): 

444 # test the singular event getter, and what happens if the event doesn't 

445 # exist for the day 

446 eventMaker = self.tmaEventMaker 

447 events = eventMaker.getEvents(self.dayObs) 

448 nEvents = len(events) 

449 

450 event = eventMaker.getEvent(self.dayObs, 0) 

451 self.assertIsInstance(event, TMAEvent) 

452 self.assertEqual(event, events[0]) 

453 event = eventMaker.getEvent(self.dayObs, 100) 

454 self.assertIsInstance(event, TMAEvent) 

455 self.assertEqual(event, events[100]) 

456 

457 with self.assertLogs(level="WARNING") as cm: 

458 correctMsg = f"Event {nEvents+1} not found for {self.dayObs}" 

459 event = eventMaker.getEvent(self.dayObs, nEvents + 1) 

460 msg = cm.output[0] 

461 self.assertIn(correctMsg, msg) 

462 

463 @vcr.use_cassette() 

464 def test_printing(self): 

465 eventMaker = self.tmaEventMaker 

466 events = eventMaker.getEvents(self.dayObs) 

467 

468 # test str(), repr(), and _ipython_display_() for an event 

469 print(str(events[0])) 

470 print(repr(events[0])) 

471 print(events[0]._ipython_display_()) 

472 

473 # spot-check both a slow and a track to print 

474 slews = [e for e in events if e.type == TMAState.SLEWING] 

475 tracks = [e for e in events if e.type == TMAState.TRACKING] 

476 eventMaker.printEventDetails(slews[0]) 

477 eventMaker.printEventDetails(tracks[0]) 

478 eventMaker.printEventDetails(events[-1]) 

479 

480 # check the full day trick works 

481 eventMaker.printFullDayStateEvolution(self.dayObs) 

482 

483 tma = TMAStateMachine() 

484 _initializeTma(tma) # the uninitialized state contains wrong types for printing 

485 eventMaker.printTmaDetailedState(tma) 

486 

487 @vcr.use_cassette() 

488 def test_getAxisData(self): 

489 eventMaker = self.tmaEventMaker 

490 events = eventMaker.getEvents(self.dayObs) 

491 

492 azData, elData = getAzimuthElevationDataForEvent(self.client, events[0]) 

493 self.assertIsInstance(azData, pd.DataFrame) 

494 self.assertIsInstance(elData, pd.DataFrame) 

495 

496 paddedAzData, paddedElData = getAzimuthElevationDataForEvent( 

497 self.client, events[0], prePadding=2, postPadding=1 

498 ) 

499 self.assertGreater(len(paddedAzData), len(azData)) 

500 self.assertGreater(len(paddedElData), len(elData)) 

501 

502 # just check this doesn't raise when called, and check we can pass the 

503 # data in 

504 plotEvent(self.client, events[0], azimuthData=azData, elevationData=elData) 

505 

506 @vcr.use_cassette() 

507 def test_plottingAndCommands(self): 

508 eventMaker = self.tmaEventMaker 

509 events = eventMaker.getEvents(self.dayObs) 

510 event = events[28] # this one has commands, and we'll check that later 

511 

512 # check we _can_ plot without a figure, and then stop doing that 

513 plotEvent(self.client, event) 

514 

515 fig = plt.figure(figsize=(10, 8)) 

516 # just check this doesn't raise when called 

517 plotEvent(self.client, event, fig=fig) 

518 plt.close(fig) 

519 

520 commandsToPlot = ["raDecTarget", "moveToTarget", "startTracking", "stopTracking"] 

521 commands = getCommandsDuringEvent(self.client, event, commandsToPlot, doLog=False) 

522 self.assertTrue(not all([time is None for time in commands.values()])) # at least one command 

523 

524 plotEvent(self.client, event, fig=fig, commands=commands) 

525 

526 del fig 

527 

528 @vcr.use_cassette() 

529 def test_findEvent(self): 

530 eventMaker = self.tmaEventMaker 

531 events = eventMaker.getEvents(self.dayObs) 

532 event = events[28] # this one has a contiguous event before it 

533 

534 time = event.begin 

535 found = eventMaker.findEvent(time) 

536 self.assertEqual(found, event) 

537 

538 dt = TimeDelta(0.01, format="sec") 

539 # must be just inside to get the same event back, because if a moment 

540 # is shared it gives the one which starts with the moment (whilst 

541 # logging info messages about it) 

542 time = event.end - dt 

543 found = eventMaker.findEvent(time) 

544 self.assertEqual(found, event) 

545 

546 # now check that if we're a hair after, we don't get the same event 

547 time = event.end + dt 

548 found = eventMaker.findEvent(time) 

549 self.assertNotEqual(found, event) 

550 

551 # Now check the cases which don't find an event at all. It would be 

552 # nice to check the log messages here, but it seems too fragile to be 

553 # worth it 

554 dt = TimeDelta(1, format="sec") 

555 tooEarlyOnDay = getDayObsStartTime(self.dayObs) + dt # 1 second after start of day 

556 found = eventMaker.findEvent(tooEarlyOnDay) 

557 self.assertIsNone(found) 

558 

559 # 1 second before end of day and this day does not end with an open 

560 # event 

561 tooLateOnDay = getDayObsStartTime(calcNextDay(self.dayObs)) - dt 

562 found = eventMaker.findEvent(tooLateOnDay) 

563 self.assertIsNone(found) 

564 

565 # going just inside the last event of the day should be fine 

566 lastEvent = events[-1] 

567 found = eventMaker.findEvent(lastEvent.end - dt) 

568 self.assertEqual(found, lastEvent) 

569 

570 # going at the very end of the last event of the day should actually 

571 # find nothing, because the last moment of an event isn't actually in 

572 # the event itself, because of how contiguous events are defined to 

573 # behave (being half-open intervals) 

574 found = eventMaker.findEvent(lastEvent.end) 

575 self.assertIsNone(found, lastEvent) 

576 

577 @vcr.use_cassette() 

578 def test_eventAssociatedWith(self): 

579 eventMaker = self.tmaEventMaker 

580 events = eventMaker.getEvents(self.dayObsWithBlockInfo) 

581 eventsWithBlockInfo = [e for e in events if e.blockInfos] 

582 eventsWithoutBlockInfo = [e for e in events if not e.blockInfos] 

583 self.assertEqual(len(events), 69) 

584 self.assertEqual(len(eventsWithBlockInfo), 65) 

585 self.assertEqual(len(eventsWithoutBlockInfo), 4) 

586 

587 self.assertIsNotNone(eventsWithoutBlockInfo[0].blockInfos) 

588 self.assertIsInstance(eventsWithoutBlockInfo[0].blockInfos, list) 

589 self.assertEqual(len(eventsWithoutBlockInfo[0].blockInfos), 0) 

590 

591 event = eventsWithBlockInfo[0] 

592 self.assertIsInstance(event, TMAEvent) 

593 self.assertTrue(event.associatedWith(ticket="SITCOM-906")) 

594 self.assertFalse(event.associatedWith(ticket="SITCOM-905")) 

595 

596 self.assertTrue(event.associatedWith(salIndex=100017)) 

597 self.assertFalse(event.associatedWith(salIndex=100018)) 

598 

599 self.assertTrue(event.associatedWith(block=6)) 

600 self.assertFalse(event.associatedWith(block=5)) 

601 

602 # check it works with any and all of the arguments 

603 self.assertTrue(event.associatedWith(block=6, salIndex=100017)) 

604 self.assertTrue(event.associatedWith(block=6, salIndex=100017, ticket="SITCOM-906")) 

605 

606 # check it's false if any are false 

607 self.assertFalse(event.associatedWith(block=7, salIndex=100017, ticket="SITCOM-906")) # 1 wrong 

608 self.assertFalse(event.associatedWith(block=6, salIndex=100018, ticket="SITCOM-906")) # 1 wrong 

609 self.assertFalse(event.associatedWith(block=6, salIndex=100017, ticket="SITCOM-907")) # 1 wrong 

610 self.assertFalse(event.associatedWith(block=1, salIndex=1, ticket="SITCOM-1")) # all wrong 

611 

612 # check with the blockSeqNum, with and without the other items 

613 self.assertTrue(event.associatedWith(block=6, blockSeqNum=1)) 

614 self.assertFalse(event.associatedWith(block=6, blockSeqNum=2)) 

615 self.assertTrue(event.associatedWith(block=6, blockSeqNum=1, salIndex=100017)) 

616 self.assertFalse(event.associatedWith(block=6, blockSeqNum=2, salIndex=100017)) 

617 self.assertTrue(event.associatedWith(block=6, blockSeqNum=1, salIndex=100017, ticket="SITCOM-906")) 

618 self.assertFalse(event.associatedWith(block=6, blockSeqNum=2, salIndex=100017, ticket="SITCOM-906")) 

619 

620 with self.assertRaises(ValueError): 

621 event.associatedWith() 

622 event.associatedWith(blockSeqNum=1) # nonsense to ask for a seqNum without a block number 

623 

624 

625class TestMemory(lsst.utils.tests.MemoryTestCase): 

626 pass 

627 

628 

629def setup_module(module): 

630 lsst.utils.tests.init() 

631 

632 

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

634 lsst.utils.tests.init() 

635 unittest.main()