BioImager  3.9.1
A .NET microscopy imaging library. Supports various microscopes by using imported libraries & GUI automation. Supported libraries include PriorĀ® & ZeissĀ® & all devices supported by Micromanager 2.0 and python-microscope.
Loading...
Searching...
No Matches
test_devices.py
1#!/usr/bin/env python3
2
3## Copyright (C) 2020 David Miguel Susano Pinto <carandraug@gmail.com>
4##
5## This file is part of Microscope.
6##
7## Microscope is free software: you can redistribute it and/or modify
8## it under the terms of the GNU General Public License as published by
9## the Free Software Foundation, either version 3 of the License, or
10## (at your option) any later version.
11##
12## Microscope is distributed in the hope that it will be useful,
13## but WITHOUT ANY WARRANTY; without even the implied warranty of
14## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15## GNU General Public License for more details.
16##
17## You should have received a copy of the GNU General Public License
18## along with Microscope. If not, see <http://www.gnu.org/licenses/>.
19
20"""Test all the concrete device classes.
21
22We have the same tests for all devices of the same type. To do this,
23there is a :class:`unittest.TestCase` class for each device that
24subclasses from that device type class of tests. Each such class only
25needs to implement the `setUp` method. It may also add device
26specific tests.
27
28Using lasers as example, there is a :class:`.LightSourceTests` class
29full of `test_*` methods, each of them a test on its own. For each
30light source device supported there is one test class, e.g.,
31`TestOmicronDeepstarLaser`, and `TestCoherentSapphireLaser`. These
32subclass from both :class:`unittest.TestCase` and `LightSourceTests`
33and need only to implement `setUp` which sets up the fake and
34constructs the device instance required to run the tests.
35
36"""
37
38import unittest
39import unittest.mock
40
41import numpy
42
43import microscope.testsuite.devices as dummies
44import microscope.testsuite.mock_devices as mocks
45from microscope import simulators
46
47
48class TestSerialMock(unittest.TestCase):
49 # Our tests for serial devices depend on our SerialMock base
50 # class working properly so yeah, we need tests for that too.
51 class Serial(mocks.SerialMock):
52 eol = b"\r\n"
53
54 def handle(self, command):
55 if command.startswith(b"echo "):
56 self.in_buffer.write(command[5:] + self.eol)
57 elif command in [b"foo", b"bar"]:
58 pass
59 else:
60 raise RuntimeError("unknown command '%s'" % command.decode())
61
62 def setUp(self):
63 self.serial = TestSerialMock.Serial()
64 patcher = unittest.mock.patch.object(
65 TestSerialMock.Serial, "handle", wraps=self.serial.handle
66 )
67 self.addCleanup(patcher.stop)
68 self.mock = patcher.start()
69
70 def test_simple_commands(self):
71 self.serial.write(b"foo\r\n")
72 self.mock.assert_called_once_with(b"foo")
73
74 def test_partial_commands(self):
75 self.serial.write(b"fo")
76 self.serial.write(b"o")
77 self.serial.write(b"\r\n")
78 self.serial.handle.assert_called_once_with(b"foo")
79
80 def test_multiple_commands(self):
81 self.serial.write(b"foo\r\nbar\r\n")
82 calls = [unittest.mock.call(x) for x in [b"foo", b"bar"]]
83 self.assertEqual(self.serial.handle.mock_calls, calls)
84
85 def test_unix_eol(self):
86 self.serial.eol = b"\n"
87 self.serial.write(b"foo\nbar\n")
88 calls = [unittest.mock.call(x) for x in [b"foo", b"bar"]]
89 self.assertEqual(self.serial.handle.mock_calls, calls)
90
91 def test_write(self):
92 self.serial.write(b"echo qux\r\n")
93 self.assertEqual(self.serial.readline(), b"qux\r\n")
94
95
97 """Tests cases for all devices.
98
99 This collection of tests cover the very basic behaviour of
100 devices,stuff like initialising and enabling the device. Classes
101 of tests specific to each device type should subclass from it.
102
103 Subclasses must define a `device` property during `setUp`, an
104 instance of :class:`Device`.
105
106 """
107
109 """Device can be turned on and off"""
110 self.device.initialize()
111 self.device.shutdown()
112
113 def test_enable_and_disable(self):
114 # TODO: we need to define what happens when enable is called
115 # and device has not been initialised. See issue #69
116 self.device.initialize()
117 self.device.enable()
118 self.assertTrue(self.device.enabled)
119 # We don't check if it is disabled after shutdown because
120 # some devices can't be turned off.
121 # TODO: add a `has_disabled_state` to the fake so we can
122 # query whether we can check about being disabled.
123 self.device.disable()
124 self.device.shutdown()
125
127 """Handles enabling of an already enabled device"""
128 self.device.initialize()
129 self.device.enable()
130 self.assertTrue(self.device.enabled)
131 self.device.enable()
132 self.assertTrue(self.device.enabled)
133
135 """Handles disabling of an already disabled device.
136
137 Test disabling twice, both before and after enabling it for
138 the first time.
139 """
140 self.device.initialize()
141 self.device.disable()
142 self.device.disable()
143 self.device.enable()
144 self.assertTrue(self.device.enabled)
145 self.device.disable()
146 self.device.disable()
147
148
150 def test_connection_defaults(self):
151 self.assertEqual(self.device.connection.baudrate, self.fake.baudrate)
152 self.assertEqual(self.device.connection.parity, self.fake.parity)
153 self.assertEqual(self.device.connection.bytesize, self.fake.bytesize)
154 self.assertEqual(self.device.connection.stopbits, self.fake.stopbits)
155 self.assertEqual(self.device.connection.rtscts, self.fake.rtscts)
156 self.assertEqual(self.device.connection.dsrdtr, self.fake.dsrdtr)
157
158
160 """Base class for :class:`LightSource` tests.
161
162 This class implements all the general laser tests and is meant to
163 be mixed with :class:`unittest.TestCase`. Subclasses must
164 implement the `setUp` method which must add two properties:
165
166 `device`
167 Instance of the :class:`LightSource` implementation being
168 tested.
169
170 `fake`
171 Object with a multiple attributes that specify the hardware
172 and control the tests, such as the device max and min power
173 values. Such attributes may as well be attributes in the
174 class that fakes the hardware.
175
176 """
177
178 def assertEqualMW(self, first, second, msg=None):
179 # We could be smarter, but rounding the values should be
180 # enough to check the values when comparing power levels.
181 self.assertEqual(round(first), round(second), msg)
182
183 def test_get_is_on(self):
184 self.assertEqual(self.device.connection.light, self.device.get_is_on())
185 self.device.enable()
186 self.assertEqual(self.device.connection.light, self.device.get_is_on())
187 self.device.disable()
188 self.assertEqual(self.device.connection.light, self.device.get_is_on())
189
190 def test_off_after_constructor(self):
191 # Some lasers, such as our Coherent Sapphire emit laser
192 # radiation as soon as the key is switched on. We should
193 # ensure that the laser is turned off during the
194 # construction.
195 self.assertFalse(self.device.get_is_on())
196
197 def test_turning_on_and_off(self):
198 self.device.enable()
199 self.assertTrue(self.device.get_is_on())
200 self.device.disable()
201 self.assertFalse(self.device.get_is_on())
202
203 def test_shutdown(self):
204 self.device.enable()
205 self.device.disable()
206 self.device.shutdown()
207
208 def test_power_when_off(self):
209 self.device.disable()
210 self.assertIsInstance(self.device.power, float)
211 self.assertEqual(self.device.power, 0.0)
212
213 def test_setting_power(self):
214 self.device.enable()
215 self.assertIsInstance(self.device.power, float)
216 power_mw = self.device.power * self.fake.max_power
217 self.assertEqualMW(power_mw, self.fake.default_power)
218 self.assertEqualMW(self.device.power, self.device.get_set_power())
219
220 new_power = 0.5
221 new_power_mw = new_power * self.fake.max_power
222 self.device.power = new_power
223 self.assertEqualMW(
224 self.device.power * self.fake.max_power, new_power_mw
225 )
226 self.assertEqualMW(new_power, self.device.get_set_power())
227
228 def test_setting_power_outside_limit(self):
229 self.device.enable()
230 self.device.power = -0.1
231 self.assertEqual(
232 self.device.power,
233 self.fake.min_power / self.fake.max_power,
234 "clip setting power below 0",
235 )
236 self.device.power = 1.1
237 self.assertEqual(self.device.power, 1.0, "clip setting power above 1")
238
239 def test_status(self):
240 status = self.device.get_status()
241 self.assertIsInstance(status, list)
242 for msg in status:
243 self.assertIsInstance(msg, str)
244
245
247 pass
248
249
250class ControllerTests(DeviceTests):
251 pass
252
253
255 def test_get_and_set_position(self):
256 self.assertEqual(self.device.position, 0)
257 max_pos = self.device.n_positions - 1
258 self.device.position = max_pos
259 self.assertEqual(self.device.position, max_pos)
260
261 def test_set_position_to_negative(self):
262 with self.assertRaisesRegex(Exception, "can't move to position"):
263 self.device.position = -1
264
265 def test_set_position_above_limit(self):
266 with self.assertRaisesRegex(Exception, "can't move to position"):
267 self.device.position = self.device.n_positions
268
269
271 """Collection of test cases for deformable mirrors.
272
273 Should have the following properties defined during `setUp`:
274 `planned_n_actuators` (int): number of actuators
275 `device` (DeformableMirror): the microscope device instance
276 `fake`: an object with the method `get_current_pattern`
277 """
278
279 def assertCurrentPattern(self, expected_pattern, msg=""):
280 numpy.testing.assert_array_equal(
281 self.fake.get_current_pattern(), expected_pattern, msg
282 )
283
284 def test_get_number_of_actuators(self):
285 self.assertIsInstance(self.device.n_actuators, int)
286 self.assertGreater(self.device.n_actuators, 0)
287 self.assertEqual(self.device.n_actuators, self.planned_n_actuators)
288
289 def test_applying_pattern(self):
290 pattern = numpy.full((self.planned_n_actuators,), 0.2)
291 self.device.apply_pattern(pattern)
292 self.assertCurrentPattern(pattern)
293
294 def test_out_of_range_pattern(self):
295 # While we expect values in the [0 1] range, we should not
296 # actually be checking for that.
297 pattern = numpy.zeros((self.planned_n_actuators,))
298 for v in [-1000, -1, 0, 1, 3]:
299 pattern[:] = v
300 self.device.apply_pattern(pattern)
301 self.assertCurrentPattern(pattern)
302
303 def test_software_triggering(self):
304 n_patterns = 5
305 patterns = numpy.random.rand(n_patterns, self.planned_n_actuators)
306 self.device.queue_patterns(patterns)
307 for i in range(n_patterns):
308 self.device.next_pattern()
309 self.assertCurrentPattern(patterns[i])
310
311 def test_validate_pattern_too_long(self):
312 patterns = numpy.zeros((self.planned_n_actuators + 1))
313 with self.assertRaisesRegex(Exception, "length of second dimension"):
314 self.device.apply_pattern(patterns)
315
316 def test_validate_pattern_swapped_dimensions(self):
317 patterns = numpy.zeros((self.planned_n_actuators, 1))
318 with self.assertRaisesRegex(Exception, "length of second dimension"):
319 self.device.apply_pattern(patterns)
320
321 def test_validate_pattern_with_extra_dimension(self):
322 patterns = numpy.zeros((2, 1, self.planned_n_actuators))
323 with self.assertRaisesRegex(
324 Exception, "dimensions \\(must be 1 or 2\\)"
325 ):
326 self.device.apply_pattern(patterns)
327
328
330 pass
331
332
333class DSPTests(DeviceTests):
334 pass
335
336
337class TestDummyLightSource(unittest.TestCase, LightSourceTests):
338 def setUp(self):
339 self.device = simulators.SimulatedLightSource()
340
341 # TODO: we need to rethink the test so this is not needed.
342 self.fake = self.device
343 self.fake.default_power = self.fake._set_point
344 self.fake.min_power = 0.0
345 self.fake.max_power = 100.0
346
347 def test_get_is_on(self):
348 # TODO: this test assumes the connection property to be the
349 # fake. We need to rethink how the mock lasers work.
350 pass
351
352
353class TestCoherentSapphireLaser(
354 unittest.TestCase, LightSourceTests, SerialDeviceTests
355):
356 def setUp(self):
357 from microscope.lights.sapphire import SapphireLaser
358 from microscope.testsuite.mock_devices import CoherentSapphireLaserMock
359
360 with unittest.mock.patch(
361 "microscope.lights.sapphire.serial.Serial",
362 new=CoherentSapphireLaserMock,
363 ):
364 self.device = SapphireLaser("/dev/null")
365 self.device.initialize()
366
367 self.fake = CoherentSapphireLaserMock
368
369
371 def setUp(self):
372 from microscope.lights.cobolt import CoboltLaser
373 from microscope.testsuite.mock_devices import CoboltLaserMock
374
375 with unittest.mock.patch(
376 "microscope.lights.cobolt.serial.Serial", new=CoboltLaserMock
377 ):
378 self.device = CoboltLaser("/dev/null")
379 self.device.initialize()
380
381 self.fake = CoboltLaserMock
382
383
385 unittest.TestCase, LightSourceTests, SerialDeviceTests
386):
387 def setUp(self):
388 from microscope.lights.deepstar import DeepstarLaser
389 from microscope.testsuite.mock_devices import OmicronDeepstarLaserMock
390
391 with unittest.mock.patch(
392 "microscope.lights.deepstar.serial.Serial",
393 new=OmicronDeepstarLaserMock,
394 ):
395 self.device = DeepstarLaser("/dev/null")
396 self.device.initialize()
397
398 self.fake = OmicronDeepstarLaserMock
399
400 def test_weird_initial_state(self):
401 # The initial state of the laser may not be ideal to actual
402 # turn it on, so test that weird settings are reset to
403 # something adequate.
404
405 self.device.connection.internal_peak_power = False
406 self.device.connection.bias_modulation = True
407 self.device.connection.digital_modulation = True
408 self.device.connection.analog2digital = True
409
410 self.device.enable()
411 self.assertTrue(self.device.get_is_on())
412
413 self.assertTrue(self.device.connection.internal_peak_power)
414 self.assertFalse(self.device.connection.bias_modulation)
415 self.assertFalse(self.device.connection.digital_modulation)
416 self.assertFalse(self.device.connection.analog2digital)
417
418
419class TestDummyCamera(unittest.TestCase, CameraTests):
420 def setUp(self):
422
423
424class TestImageGenerator(unittest.TestCase):
425 def test_non_square_patterns_shape(self):
426 # TODO: we should also be testing this via the camera but the
427 # TestCamera is only square. In the mean time, we only test
428 # directly the _ImageGenerator.
429 width = 16
430 height = 32
431 generator = simulators._ImageGenerator()
432 patterns = list(generator.get_methods())
433 for i, pattern in enumerate(patterns):
434 with self.subTest(pattern):
435 generator.set_method(i)
436 array = generator.get_image(width, height, 0, 255)
437 # In matplotlib, an M-wide by N-tall image has M columns
438 # and N rows, so a shape of (N, M)
439 self.assertEqual(array.shape, (height, width))
440
441
442class TestDummyController(unittest.TestCase, ControllerTests):
443 def setUp(self):
447 {"laser": self.laser, "filterwheel": self.filterwheel}
448 )
449
450 def test_device_names(self):
451 self.assertSetEqual(
452 {"laser", "filterwheel"}, set(self.device.devices.keys())
453 )
454
455 def test_control_filterwheel(self):
456 self.assertEqual(self.device.devices["filterwheel"].position, 0)
457 self.device.devices["filterwheel"].position = 2
458 self.assertEqual(self.device.devices["filterwheel"].position, 2)
459
460 def test_control_laser(self):
461 self.assertEqual(self.device.devices["laser"].power, 0.0)
462 self.device.devices["laser"].enable()
463 self.device.devices["laser"].power = 0.8
464 self.assertEqual(self.device.devices["laser"].power, 0.8)
465
466
467class TestEmptyDummyFilterWheel(unittest.TestCase):
468 def test_zero_positions(self):
469 with self.assertRaisesRegex(
470 ValueError, "positions must be a positive number"
471 ):
473
474
476 def setUp(self):
477 self.device = simulators.SimulatedFilterWheel(positions=1)
478
479
481 def setUp(self):
482 self.device = simulators.SimulatedFilterWheel(positions=6)
483
484
486 def setUp(self):
487 self.planned_n_actuators = 86
490 )
491 self.fake = self.device
492
493
494class TestDummySLM(unittest.TestCase, SLMTests):
495 def setUp(self):
496 self.device = dummies.DummySLM()
497
498
499class TestDummyDSP(unittest.TestCase, DSPTests):
500 def setUp(self):
501 self.device = dummies.DummyDSP()
502
503
504class TestBaseDevice(unittest.TestCase):
506 """Unexpected kwargs on constructor raise exception.
507
508 Test first that we can construct the device. Then test that
509 it fails if there are unused kwargs. This is an issue when
510 there are default arguments, there's a typo on the argument
511 name, and the class uses the default instead of an error. See
512 issue #84.
513 """
515 # XXX: Device.__del__ calls shutdown(). However, if __init__
516 # failed the device is not complete and shutdown() fails
517 # because the logger has not been created. See comments on
518 # issue #69. patch __del__ to workaround this issue.
519 with unittest.mock.patch("microscope.devices.Device.__del__"):
520 with self.assertRaisesRegex(TypeError, "argument 'power'"):
522
523
524if __name__ == "__main__":
525 unittest.main()
def assertCurrentPattern(self, expected_pattern, msg="")
def assertEqualMW(self, first, second, msg=None)