20"""Test all the concrete device classes.
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
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.
43import microscope.testsuite.devices as dummies
44import microscope.testsuite.mock_devices as mocks
45from microscope import simulators
48class TestSerialMock(unittest.TestCase):
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"]:
60 raise RuntimeError(
"unknown command '%s'" % command.decode())
64 patcher = unittest.mock.patch.object(
67 self.addCleanup(patcher.stop)
68 self.mock = patcher.start()
70 def test_simple_commands(self):
71 self.serial.write(b
"foo\r\n")
72 self.mock.assert_called_once_with(b
"foo")
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")
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)
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)
92 self.serial.write(b
"echo qux\r\n")
93 self.assertEqual(self.serial.readline(), b
"qux\r\n")
97 """Tests cases for all devices.
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.
103 Subclasses must define a `device` property during `setUp`, an
104 instance of :
class:`Device`.
109 """Device can be turned on and off"""
110 self.device.initialize()
111 self.device.shutdown()
113 def test_enable_and_disable(self):
116 self.device.initialize()
118 self.assertTrue(self.device.enabled)
123 self.device.disable()
124 self.device.shutdown()
127 """Handles enabling of an already enabled device"""
128 self.device.initialize()
130 self.assertTrue(self.device.enabled)
132 self.assertTrue(self.device.enabled)
135 """Handles disabling of an already disabled device.
137 Test disabling twice, both before and after enabling it
for
140 self.device.initialize()
141 self.device.disable()
142 self.device.disable()
144 self.assertTrue(self.device.enabled)
145 self.device.disable()
146 self.device.disable()
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)
160 """Base class for :class:`LightSource` tests.
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:
167 Instance of the :
class:`LightSource` implementation being
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.
178 def assertEqualMW(self, first, second, msg=None):
181 self.assertEqual(round(first), round(second), msg)
183 def test_get_is_on(self):
184 self.assertEqual(self.device.connection.light, self.device.get_is_on())
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())
190 def test_off_after_constructor(self):
195 self.assertFalse(self.device.get_is_on())
197 def test_turning_on_and_off(self):
199 self.assertTrue(self.device.get_is_on())
200 self.device.disable()
201 self.assertFalse(self.device.get_is_on())
203 def test_shutdown(self):
205 self.device.disable()
206 self.device.shutdown()
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)
213 def test_setting_power(self):
215 self.assertIsInstance(self.device.power, float)
216 power_mw = self.device.power * self.fake.max_power
218 self.
assertEqualMW(self.device.power, self.device.get_set_power())
221 new_power_mw = new_power * self.fake.max_power
222 self.device.power = new_power
224 self.device.power * self.fake.max_power, new_power_mw
228 def test_setting_power_outside_limit(self):
230 self.device.power = -0.1
233 self.fake.min_power / self.fake.max_power,
234 "clip setting power below 0",
236 self.device.power = 1.1
237 self.assertEqual(self.device.power, 1.0,
"clip setting power above 1")
239 def test_status(self):
240 status = self.device.get_status()
241 self.assertIsInstance(status, list)
243 self.assertIsInstance(msg, str)
250class ControllerTests(DeviceTests):
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)
261 def test_set_position_to_negative(self):
262 with self.assertRaisesRegex(Exception,
"can't move to position"):
263 self.device.position = -1
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
271 """Collection of test cases for deformable mirrors.
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`
279 def assertCurrentPattern(self, expected_pattern, msg=""):
280 numpy.testing.assert_array_equal(
281 self.fake.get_current_pattern(), expected_pattern, msg
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)
289 def test_applying_pattern(self):
290 pattern = numpy.full((self.planned_n_actuators,), 0.2)
291 self.device.apply_pattern(pattern)
294 def test_out_of_range_pattern(self):
297 pattern = numpy.zeros((self.planned_n_actuators,))
298 for v
in [-1000, -1, 0, 1, 3]:
300 self.device.apply_pattern(pattern)
303 def test_software_triggering(self):
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()
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)
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)
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\\)"
326 self.device.apply_pattern(patterns)
333class DSPTests(DeviceTests):
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
347 def test_get_is_on(self):
353class TestCoherentSapphireLaser(
354 unittest.TestCase, LightSourceTests, SerialDeviceTests
360 with unittest.mock.patch(
361 "microscope.lights.sapphire.serial.Serial",
362 new=CoherentSapphireLaserMock,
364 self.device = SapphireLaser(
"/dev/null")
365 self.device.initialize()
367 self.fake = CoherentSapphireLaserMock
375 with unittest.mock.patch(
376 "microscope.lights.cobolt.serial.Serial", new=CoboltLaserMock
378 self.
device = CoboltLaser(
"/dev/null")
381 self.
fake = CoboltLaserMock
385 unittest.TestCase, LightSourceTests, SerialDeviceTests
391 with unittest.mock.patch(
392 "microscope.lights.deepstar.serial.Serial",
393 new=OmicronDeepstarLaserMock,
395 self.
device = DeepstarLaser(
"/dev/null")
398 self.
fake = OmicronDeepstarLaserMock
400 def test_weird_initial_state(self):
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
411 self.assertTrue(self.
device.get_is_on())
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)
425 def test_non_square_patterns_shape(self):
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)
439 self.assertEqual(array.shape, (height, width))
450 def test_device_names(self):
452 {
"laser",
"filterwheel"}, set(self.
device.devices.keys())
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)
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)
468 def test_zero_positions(self):
469 with self.assertRaisesRegex(
470 ValueError,
"positions must be a positive number"
496 self.
device = dummies.DummySLM()
501 self.
device = dummies.DummyDSP()
506 """Unexpected kwargs on constructor raise exception.
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
519 with unittest.mock.patch(
"microscope.devices.Device.__del__"):
520 with self.assertRaisesRegex(TypeError,
"argument 'power'"):
524if __name__ ==
"__main__":
def test_on_and_off(self)
def test_enable_enabled(self)
def test_disable_disabled(self)
def assertEqualMW(self, first, second, msg=None)
def test_unexpected_kwargs_raise_exception(self)