Coverage for tests/test_quantumGraph.py: 20%

321 statements  

« prev     ^ index     » next       coverage.py v7.2.3, created at 2023-04-27 02:47 -0700

1# This file is part of pipe_base. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (http://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 <http://www.gnu.org/licenses/>. 

21 

22import os 

23import pickle 

24import random 

25import tempfile 

26import unittest 

27import uuid 

28import warnings 

29from itertools import chain 

30from typing import Iterable 

31 

32import lsst.pipe.base.connectionTypes as cT 

33import lsst.utils.tests 

34from lsst.daf.butler import ( 

35 Config, 

36 DataCoordinate, 

37 DatasetRef, 

38 DatasetType, 

39 DimensionUniverse, 

40 Quantum, 

41 UnresolvedRefWarning, 

42) 

43from lsst.pex.config import Field 

44from lsst.pipe.base import ( 

45 DatasetTypeName, 

46 PipelineTask, 

47 PipelineTaskConfig, 

48 PipelineTaskConnections, 

49 QuantumGraph, 

50 TaskDef, 

51) 

52from lsst.pipe.base.graph.quantumNode import QuantumNode 

53from lsst.utils.introspection import get_full_type_name 

54 

55METADATA = {"a": [1, 2, 3]} 

56 

57 

58class Dummy1Connections(PipelineTaskConnections, dimensions=("A", "B")): 

59 initOutput = cT.InitOutput(name="Dummy1InitOutput", storageClass="ExposureF", doc="n/a") 

60 input = cT.Input(name="Dummy1Input", storageClass="ExposureF", doc="n/a", dimensions=("A", "B")) 

61 output = cT.Output(name="Dummy1Output", storageClass="ExposureF", doc="n/a", dimensions=("A", "B")) 

62 

63 

64class Dummy1Config(PipelineTaskConfig, pipelineConnections=Dummy1Connections): 

65 conf1 = Field(dtype=int, default=1, doc="dummy config") 

66 

67 

68class Dummy1PipelineTask(PipelineTask): 

69 ConfigClass = Dummy1Config 

70 

71 

72class Dummy2Connections(PipelineTaskConnections, dimensions=("A", "B")): 

73 initInput = cT.InitInput(name="Dummy1InitOutput", storageClass="ExposureF", doc="n/a") 

74 initOutput = cT.InitOutput(name="Dummy2InitOutput", storageClass="ExposureF", doc="n/a") 

75 input = cT.Input(name="Dummy1Output", storageClass="ExposureF", doc="n/a", dimensions=("A", "B")) 

76 output = cT.Output(name="Dummy2Output", storageClass="ExposureF", doc="n/a", dimensions=("A", "B")) 

77 

78 

79class Dummy2Config(PipelineTaskConfig, pipelineConnections=Dummy2Connections): 

80 conf1 = Field(dtype=int, default=1, doc="dummy config") 

81 

82 

83class Dummy2PipelineTask(PipelineTask): 

84 ConfigClass = Dummy2Config 

85 

86 

87class Dummy3Connections(PipelineTaskConnections, dimensions=("A", "B")): 

88 initInput = cT.InitInput(name="Dummy2InitOutput", storageClass="ExposureF", doc="n/a") 

89 initOutput = cT.InitOutput(name="Dummy3InitOutput", storageClass="ExposureF", doc="n/a") 

90 input = cT.Input(name="Dummy2Output", storageClass="ExposureF", doc="n/a", dimensions=("A", "B")) 

91 output = cT.Output(name="Dummy3Output", storageClass="ExposureF", doc="n/a", dimensions=("A", "B")) 

92 

93 

94class Dummy3Config(PipelineTaskConfig, pipelineConnections=Dummy3Connections): 

95 conf1 = Field(dtype=int, default=1, doc="dummy config") 

96 

97 

98class Dummy3PipelineTask(PipelineTask): 

99 ConfigClass = Dummy3Config 

100 

101 

102# Test if a Task that does not interact with the other Tasks works fine in 

103# the graph. 

104class Dummy4Connections(PipelineTaskConnections, dimensions=("A", "B")): 

105 input = cT.Input(name="Dummy4Input", storageClass="ExposureF", doc="n/a", dimensions=("A", "B")) 

106 output = cT.Output(name="Dummy4Output", storageClass="ExposureF", doc="n/a", dimensions=("A", "B")) 

107 

108 

109class Dummy4Config(PipelineTaskConfig, pipelineConnections=Dummy4Connections): 

110 conf1 = Field(dtype=int, default=1, doc="dummy config") 

111 

112 

113class Dummy4PipelineTask(PipelineTask): 

114 ConfigClass = Dummy4Config 

115 

116 

117class QuantumGraphTestCase(unittest.TestCase): 

118 """Tests the various functions of a quantum graph""" 

119 

120 def setUp(self): 

121 self.config = Config( 

122 { 

123 "version": 1, 

124 "namespace": "pipe_base_test", 

125 "skypix": { 

126 "common": "htm7", 

127 "htm": { 

128 "class": "lsst.sphgeom.HtmPixelization", 

129 "max_level": 24, 

130 }, 

131 }, 

132 "elements": { 

133 "A": { 

134 "keys": [ 

135 { 

136 "name": "id", 

137 "type": "int", 

138 } 

139 ], 

140 "storage": { 

141 "cls": "lsst.daf.butler.registry.dimensions.table.TableDimensionRecordStorage", 

142 }, 

143 }, 

144 "B": { 

145 "keys": [ 

146 { 

147 "name": "id", 

148 "type": "int", 

149 } 

150 ], 

151 "storage": { 

152 "cls": "lsst.daf.butler.registry.dimensions.table.TableDimensionRecordStorage", 

153 }, 

154 }, 

155 }, 

156 "packers": {}, 

157 } 

158 ) 

159 universe = DimensionUniverse(config=self.config) 

160 

161 def _makeDatasetType(connection): 

162 return DatasetType( 

163 connection.name, 

164 getattr(connection, "dimensions", ()), 

165 storageClass=connection.storageClass, 

166 universe=universe, 

167 ) 

168 

169 # need to make a mapping of TaskDef to set of quantum 

170 quantumMap = {} 

171 tasks = [] 

172 initInputs = {} 

173 initOutputs = {} 

174 dataset_types = set() 

175 for task, label in ( 

176 (Dummy1PipelineTask, "R"), 

177 (Dummy2PipelineTask, "S"), 

178 (Dummy3PipelineTask, "T"), 

179 (Dummy4PipelineTask, "U"), 

180 ): 

181 config = task.ConfigClass() 

182 taskDef = TaskDef(get_full_type_name(task), config, task, label) 

183 tasks.append(taskDef) 

184 quantumSet = set() 

185 connections = taskDef.connections 

186 if connections.initInputs: 

187 initInputDSType = _makeDatasetType(connections.initInput) 

188 with warnings.catch_warnings(): 

189 warnings.simplefilter("ignore", category=UnresolvedRefWarning) 

190 initRefs = [DatasetRef(initInputDSType, DataCoordinate.makeEmpty(universe))] 

191 initInputs[taskDef] = initRefs 

192 dataset_types.add(initInputDSType) 

193 else: 

194 initRefs = None 

195 if connections.initOutputs: 

196 initOutputDSType = _makeDatasetType(connections.initOutput) 

197 with warnings.catch_warnings(): 

198 warnings.simplefilter("ignore", category=UnresolvedRefWarning) 

199 initRefs = [DatasetRef(initOutputDSType, DataCoordinate.makeEmpty(universe))] 

200 initOutputs[taskDef] = initRefs 

201 dataset_types.add(initOutputDSType) 

202 inputDSType = _makeDatasetType(connections.input) 

203 dataset_types.add(inputDSType) 

204 outputDSType = _makeDatasetType(connections.output) 

205 dataset_types.add(outputDSType) 

206 for a, b in ((1, 2), (3, 4)): 

207 with warnings.catch_warnings(): 

208 warnings.simplefilter("ignore", category=UnresolvedRefWarning) 

209 inputRefs = [ 

210 DatasetRef( 

211 inputDSType, DataCoordinate.standardize({"A": a, "B": b}, universe=universe) 

212 ) 

213 ] 

214 outputRefs = [ 

215 DatasetRef( 

216 outputDSType, DataCoordinate.standardize({"A": a, "B": b}, universe=universe) 

217 ) 

218 ] 

219 quantumSet.add( 

220 Quantum( 

221 taskName=task.__qualname__, 

222 dataId=DataCoordinate.standardize({"A": a, "B": b}, universe=universe), 

223 taskClass=task, 

224 initInputs=initRefs, 

225 inputs={inputDSType: inputRefs}, 

226 outputs={outputDSType: outputRefs}, 

227 ) 

228 ) 

229 quantumMap[taskDef] = quantumSet 

230 self.tasks = tasks 

231 self.quantumMap = quantumMap 

232 self.packagesDSType = DatasetType("packages", universe.empty, storageClass="Packages") 

233 dataset_types.add(self.packagesDSType) 

234 with warnings.catch_warnings(): 

235 warnings.simplefilter("ignore", category=UnresolvedRefWarning) 

236 globalInitOutputs = [DatasetRef(self.packagesDSType, DataCoordinate.makeEmpty(universe))] 

237 self.qGraph = QuantumGraph( 

238 quantumMap, 

239 metadata=METADATA, 

240 universe=universe, 

241 initInputs=initInputs, 

242 initOutputs=initOutputs, 

243 globalInitOutputs=globalInitOutputs, 

244 registryDatasetTypes=dataset_types, 

245 ) 

246 self.universe = universe 

247 self.num_dataset_types = len(dataset_types) 

248 

249 def testTaskGraph(self): 

250 for taskDef in self.quantumMap.keys(): 

251 self.assertIn(taskDef, self.qGraph.taskGraph) 

252 

253 def testGraph(self): 

254 graphSet = {q.quantum for q in self.qGraph.graph} 

255 for quantum in chain.from_iterable(self.quantumMap.values()): 

256 self.assertIn(quantum, graphSet) 

257 

258 def testGetQuantumNodeByNodeId(self): 

259 inputQuanta = tuple(self.qGraph.inputQuanta) 

260 node = self.qGraph.getQuantumNodeByNodeId(inputQuanta[0].nodeId) 

261 self.assertEqual(node, inputQuanta[0]) 

262 wrongNode = uuid.uuid4() 

263 with self.assertRaises(KeyError): 

264 self.qGraph.getQuantumNodeByNodeId(wrongNode) 

265 

266 def testPickle(self): 

267 stringify = pickle.dumps(self.qGraph) 

268 restore: QuantumGraph = pickle.loads(stringify) 

269 self.assertEqual(self.qGraph, restore) 

270 

271 def testInputQuanta(self): 

272 inputs = {q.quantum for q in self.qGraph.inputQuanta} 

273 self.assertEqual(self.quantumMap[self.tasks[0]] | self.quantumMap[self.tasks[3]], inputs) 

274 

275 def testOutputQuanta(self): 

276 outputs = {q.quantum for q in self.qGraph.outputQuanta} 

277 self.assertEqual(self.quantumMap[self.tasks[2]] | self.quantumMap[self.tasks[3]], outputs) 

278 

279 def testLength(self): 

280 self.assertEqual(len(self.qGraph), 2 * len(self.tasks)) 

281 

282 def testGetQuantaForTask(self): 

283 for task in self.tasks: 

284 self.assertEqual(self.qGraph.getQuantaForTask(task), self.quantumMap[task]) 

285 

286 def testGetNumberOfQuantaForTask(self): 

287 for task in self.tasks: 

288 self.assertEqual(self.qGraph.getNumberOfQuantaForTask(task), len(self.quantumMap[task])) 

289 

290 def testGetNodesForTask(self): 

291 for task in self.tasks: 

292 nodes: Iterable[QuantumNode] = self.qGraph.getNodesForTask(task) 

293 quanta_in_node = set(n.quantum for n in nodes) 

294 self.assertEqual(quanta_in_node, self.quantumMap[task]) 

295 

296 def testFindTasksWithInput(self): 

297 self.assertEqual( 

298 tuple(self.qGraph.findTasksWithInput(DatasetTypeName("Dummy1Output")))[0], self.tasks[1] 

299 ) 

300 

301 def testFindTasksWithOutput(self): 

302 self.assertEqual(self.qGraph.findTaskWithOutput(DatasetTypeName("Dummy1Output")), self.tasks[0]) 

303 

304 def testTaskWithDSType(self): 

305 self.assertEqual( 

306 set(self.qGraph.tasksWithDSType(DatasetTypeName("Dummy1Output"))), set(self.tasks[:2]) 

307 ) 

308 

309 def testFindTaskDefByName(self): 

310 self.assertEqual(self.qGraph.findTaskDefByName(Dummy1PipelineTask.__qualname__)[0], self.tasks[0]) 

311 

312 def testFindTaskDefByLabel(self): 

313 self.assertEqual(self.qGraph.findTaskDefByLabel("R"), self.tasks[0]) 

314 

315 def testFindQuantaWIthDSType(self): 

316 self.assertEqual( 

317 self.qGraph.findQuantaWithDSType(DatasetTypeName("Dummy1Input")), self.quantumMap[self.tasks[0]] 

318 ) 

319 

320 def testAllDatasetTypes(self): 

321 allDatasetTypes = set(self.qGraph.allDatasetTypes) 

322 truth = set() 

323 for conClass in (Dummy1Connections, Dummy2Connections, Dummy3Connections, Dummy4Connections): 

324 for connection in conClass.allConnections.values(): # type: ignore 

325 if not isinstance(connection, cT.InitOutput): 

326 truth.add(connection.name) 

327 self.assertEqual(allDatasetTypes, truth) 

328 

329 def testSubset(self): 

330 allNodes = list(self.qGraph) 

331 firstNode = allNodes[0] 

332 subset = self.qGraph.subset(firstNode) 

333 self.assertEqual(len(subset), 1) 

334 subsetList = list(subset) 

335 self.assertEqual(firstNode.quantum, subsetList[0].quantum) 

336 self.assertEqual(self.qGraph._buildId, subset._buildId) 

337 self.assertEqual(len(subset.globalInitOutputRefs()), 1) 

338 # Depending on which task was first the list can contain different 

339 # number of datasets. The first task can be either Dummy1 or Dummy4. 

340 num_types = {"R": 4, "U": 3} 

341 self.assertEqual(len(subset.registryDatasetTypes()), num_types[firstNode.taskDef.label]) 

342 

343 def testSubsetToConnected(self): 

344 # False because there are two quantum chains for two distinct sets of 

345 # dimensions 

346 self.assertFalse(self.qGraph.isConnected) 

347 

348 connectedGraphs = self.qGraph.subsetToConnected() 

349 self.assertEqual(len(connectedGraphs), 4) 

350 self.assertTrue(connectedGraphs[0].isConnected) 

351 self.assertTrue(connectedGraphs[1].isConnected) 

352 self.assertTrue(connectedGraphs[2].isConnected) 

353 self.assertTrue(connectedGraphs[3].isConnected) 

354 

355 # Split out task[3] because it is expected to be on its own 

356 for cg in connectedGraphs: 

357 if self.tasks[3] in cg.taskGraph: 

358 self.assertEqual(len(cg), 1) 

359 else: 

360 self.assertEqual(len(cg), 3) 

361 

362 self.assertNotEqual(connectedGraphs[0], connectedGraphs[1]) 

363 

364 count = 0 

365 for node in self.qGraph: 

366 if connectedGraphs[0].checkQuantumInGraph(node.quantum): 

367 count += 1 

368 if connectedGraphs[1].checkQuantumInGraph(node.quantum): 

369 count += 1 

370 if connectedGraphs[2].checkQuantumInGraph(node.quantum): 

371 count += 1 

372 if connectedGraphs[3].checkQuantumInGraph(node.quantum): 

373 count += 1 

374 self.assertEqual(len(self.qGraph), count) 

375 

376 taskSets = {len(tg := s.taskGraph): set(tg) for s in connectedGraphs} 

377 for setLen, tskSet in taskSets.items(): 

378 if setLen == 3: 

379 self.assertEqual(set(self.tasks[:-1]), tskSet) 

380 elif setLen == 1: 

381 self.assertEqual({self.tasks[-1]}, tskSet) 

382 for cg in connectedGraphs: 

383 if len(cg.taskGraph) == 1: 

384 continue 

385 allNodes = list(cg) 

386 node = cg.determineInputsToQuantumNode(allNodes[1]) 

387 self.assertEqual(set([allNodes[0]]), node) 

388 node = cg.determineInputsToQuantumNode(allNodes[1]) 

389 self.assertEqual(set([allNodes[0]]), node) 

390 

391 def testDetermineOutputsOfQuantumNode(self): 

392 testNodes = self.qGraph.getNodesForTask(self.tasks[0]) 

393 matchNodes = self.qGraph.getNodesForTask(self.tasks[1]) 

394 connections = set() 

395 for node in testNodes: 

396 connections |= set(self.qGraph.determineOutputsOfQuantumNode(node)) 

397 self.assertEqual(matchNodes, connections) 

398 

399 def testDetermineConnectionsOfQuantum(self): 

400 testNodes = self.qGraph.getNodesForTask(self.tasks[1]) 

401 matchNodes = self.qGraph.getNodesForTask(self.tasks[0]) | self.qGraph.getNodesForTask(self.tasks[2]) 

402 # outputs contain nodes tested for because it is a complete graph 

403 matchNodes |= set(testNodes) 

404 connections = set() 

405 for node in testNodes: 

406 connections |= set(self.qGraph.determineConnectionsOfQuantumNode(node)) 

407 self.assertEqual(matchNodes, connections) 

408 

409 def testDetermineAnsestorsOfQuantumNode(self): 

410 testNodes = self.qGraph.getNodesForTask(self.tasks[1]) 

411 matchNodes = self.qGraph.getNodesForTask(self.tasks[0]) 

412 matchNodes |= set(testNodes) 

413 connections = set() 

414 for node in testNodes: 

415 connections |= set(self.qGraph.determineAncestorsOfQuantumNode(node)) 

416 self.assertEqual(matchNodes, connections) 

417 

418 def testFindCycle(self): 

419 self.assertFalse(self.qGraph.findCycle()) 

420 

421 def testSaveLoad(self): 

422 with tempfile.TemporaryFile(suffix=".qgraph") as tmpFile: 

423 self.qGraph.save(tmpFile) 

424 tmpFile.seek(0) 

425 restore = QuantumGraph.load(tmpFile, self.universe) 

426 self.assertEqual(self.qGraph, restore) 

427 # Load in just one node 

428 tmpFile.seek(0) 

429 nodeId = [n.nodeId for n in self.qGraph][0] 

430 restoreSub = QuantumGraph.load(tmpFile, self.universe, nodes=(nodeId,)) 

431 self.assertEqual(len(restoreSub), 1) 

432 self.assertEqual(list(restoreSub)[0], restore.getQuantumNodeByNodeId(nodeId)) 

433 self.assertEqual(len(restoreSub.globalInitOutputRefs()), 1) 

434 self.assertEqual(len(restoreSub.registryDatasetTypes()), self.num_dataset_types) 

435 # Check that InitInput and InitOutput refs are restored correctly. 

436 for taskDef in restore.iterTaskGraph(): 

437 if taskDef.label in ("S", "T"): 

438 refs = restore.initInputRefs(taskDef) 

439 self.assertIsNotNone(refs) 

440 self.assertGreater(len(refs), 0) 

441 if taskDef.label in ("R", "S", "T"): 

442 refs = restore.initOutputRefs(taskDef) 

443 self.assertIsNotNone(refs) 

444 self.assertGreater(len(refs), 0) 

445 

446 # Different universes. 

447 tmpFile.seek(0) 

448 different_config = self.config.copy() 

449 different_config["version"] = 1_000_000 

450 different_universe = DimensionUniverse(config=different_config) 

451 with self.assertLogs("lsst.daf.butler", "INFO"): 

452 QuantumGraph.load(tmpFile, different_universe) 

453 

454 different_config["namespace"] = "incompatible" 

455 different_universe = DimensionUniverse(config=different_config) 

456 print("Trying with uni ", different_universe) 

457 tmpFile.seek(0) 

458 with self.assertRaises(RuntimeError) as cm: 

459 QuantumGraph.load(tmpFile, different_universe) 

460 self.assertIn("not compatible with", str(cm.exception)) 

461 

462 def testSaveLoadUri(self): 

463 uri = None 

464 try: 

465 with tempfile.NamedTemporaryFile(delete=False, suffix=".qgraph") as tmpFile: 

466 uri = tmpFile.name 

467 self.qGraph.saveUri(uri) 

468 restore = QuantumGraph.loadUri(uri) 

469 self.assertEqual(restore.metadata, METADATA) 

470 self.assertEqual(self.qGraph, restore) 

471 nodeNumberId = random.randint(0, len(self.qGraph) - 1) 

472 nodeNumber = [n.nodeId for n in self.qGraph][nodeNumberId] 

473 restoreSub = QuantumGraph.loadUri( 

474 uri, self.universe, nodes=(nodeNumber,), graphID=self.qGraph._buildId 

475 ) 

476 self.assertEqual(len(restoreSub), 1) 

477 self.assertEqual(list(restoreSub)[0], restore.getQuantumNodeByNodeId(nodeNumber)) 

478 # verify that more than one node works 

479 nodeNumberId2 = random.randint(0, len(self.qGraph) - 1) 

480 # ensure it is a different node number 

481 while nodeNumberId2 == nodeNumberId: 

482 nodeNumberId2 = random.randint(0, len(self.qGraph) - 1) 

483 nodeNumber2 = [n.nodeId for n in self.qGraph][nodeNumberId2] 

484 restoreSub = QuantumGraph.loadUri(uri, self.universe, nodes=(nodeNumber, nodeNumber2)) 

485 self.assertEqual(len(restoreSub), 2) 

486 self.assertEqual( 

487 set(restoreSub), 

488 set( 

489 ( 

490 restore.getQuantumNodeByNodeId(nodeNumber), 

491 restore.getQuantumNodeByNodeId(nodeNumber2), 

492 ) 

493 ), 

494 ) 

495 # verify an error when requesting a non existant node number 

496 with self.assertRaises(ValueError): 

497 QuantumGraph.loadUri(uri, self.universe, nodes=(99,)) 

498 

499 # verify a graphID that does not match will be an error 

500 with self.assertRaises(ValueError): 

501 QuantumGraph.loadUri(uri, self.universe, graphID="NOTRIGHT") 

502 

503 except Exception as e: 

504 raise e 

505 finally: 

506 if uri is not None: 

507 os.remove(uri) 

508 

509 with self.assertRaises(TypeError): 

510 self.qGraph.saveUri("test.notgraph") 

511 

512 def testSaveLoadNoRegistryDatasetTypes(self): 

513 """Test for reading quantum that is missing registry dataset types. 

514 

515 This test depends on internals of QuantumGraph implementation, in 

516 particular that empty list of registry dataset types is not stored, 

517 which makes save file identical to the "old" format. 

518 """ 

519 # Reset the list, this is safe as QuantumGraph itself does not use it. 

520 self.qGraph._registryDatasetTypes = [] 

521 with tempfile.TemporaryFile(suffix=".qgraph") as tmpFile: 

522 self.qGraph.save(tmpFile) 

523 tmpFile.seek(0) 

524 restore = QuantumGraph.load(tmpFile, self.universe) 

525 self.assertEqual(self.qGraph, restore) 

526 self.assertEqual(restore.registryDatasetTypes(), []) 

527 

528 def testContains(self): 

529 firstNode = next(iter(self.qGraph)) 

530 self.assertIn(firstNode, self.qGraph) 

531 

532 def testDimensionUniverseInSave(self): 

533 _, header = self.qGraph._buildSaveObject(returnHeader=True) 

534 # type ignore because buildSaveObject does not have method overload 

535 self.assertEqual(header["universe"], self.universe.dimensionConfig.toDict()) # type: ignore 

536 

537 

538class MyMemoryTestCase(lsst.utils.tests.MemoryTestCase): 

539 pass 

540 

541 

542def setup_module(module): 

543 lsst.utils.tests.init() 

544 

545 

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

547 lsst.utils.tests.init() 

548 unittest.main()