11import env
22import unittest
3+ from unittest import mock
34
45from hardwarelibrary .physicaldevice import PhysicalDevice
6+ from hardwarelibrary .communication import PrologixGPIBPort
7+ from hardwarelibrary .communication .communicationport import CommunicationReadTimeout
58from hardwarelibrary .daq import (
69 AnalogInputDevice , AnalogOutputDevice , AnalogInputStreamDevice ,
710 PhaseLockedDetectionDevice , TriggerableDevice ,
811 InputSource , AuxInput , AuxOutput , StreamChannel , TriggerSource , SampleClock ,
912 SR830Device , DebugSR830Device , DebugPrologixGPIBPort ,
1013)
1114
15+ SR830_IDN = "Stanford_Research_Systems,SR830,s/n86552,ver1.07"
16+
1217
1318class TestDebugSR830Device (unittest .TestCase ):
1419 def setUp (self ):
@@ -183,6 +188,35 @@ def testSampleRateSnapsToNearest(self):
183188 self .assertEqual (self .device ._sampleRateIndexFor (512 ), self .device .sampleRates .index (512 ))
184189 self .assertEqual (self .device ._sampleRateIndexFor (60 ), self .device .sampleRates .index (64 ))
185190
191+ def testSupportedSampleRates (self ):
192+ rates = self .device .supportedSampleRates ()
193+ self .assertEqual (rates , sorted (rates ))
194+ self .assertEqual (rates [- 1 ], 512 )
195+
196+ def testSensitivityClampsToLargestStep (self ):
197+ # A request above the 1 V maximum full-scale clamps to that largest step
198+ # rather than raising.
199+ self .device .setSensitivity (100.0 )
200+ self .assertAlmostEqual (self .device .getSensitivity (), 1.0 )
201+
202+ def testSetTriggerSourceRejectsUnknown (self ):
203+ with self .assertRaises (ValueError ):
204+ self .device .setTriggerSource (SampleClock .Internal )
205+
206+ def testSetInputSourceRejectsUnknown (self ):
207+ with self .assertRaises (ValueError ):
208+ self .device .setInputSource (SampleClock .Internal )
209+
210+ def testSnapReadsAuxAndFrequencyCodes (self ):
211+ # Codes 5-8 are Aux1-4, 9 is the reference frequency, and an unknown code
212+ # reads back as 0.0.
213+ values = self .device .snap (5 , 6 , 7 , 8 , 9 , 99 )
214+ self .assertEqual (len (values ), 6 )
215+ self .assertAlmostEqual (values [0 ], 0.10 )
216+ self .assertAlmostEqual (values [3 ], 0.40 )
217+ self .assertAlmostEqual (values [4 ], 1.0e3 )
218+ self .assertAlmostEqual (values [5 ], 0.0 )
219+
186220
187221class TestPhaseLockedDetectionContract (unittest .TestCase ):
188222 def testDeclaresAllCapabilities (self ):
@@ -194,25 +228,84 @@ def testDeclaresAllCapabilities(self):
194228 self .assertIsInstance (device , TriggerableDevice )
195229
196230
197- class TestPrologixGPIBPort (unittest .TestCase ):
198- def testControllerHandshakeOnOpen (self ):
199- port = DebugPrologixGPIBPort ()
200- sent = []
201- original = port .writeData
231+ class _MinimalLockIn (PhaseLockedDetectionDevice , TriggerableDevice ):
232+ """A bare capability implementation that supplies only the abstract hooks, so
233+ the base-class optional hooks and the base getDemodulatedValues are exercised
234+ (SR830Device overrides all of these)."""
202235
203- def recordingWriteData (data , endPoint = None ):
204- sent .append (bytes (data ).decode ("utf-8" ).strip ())
205- return original (data , endPoint )
236+ def getInPhaseVoltage (self ):
237+ return 0.1
206238
207- port .writeData = recordingWriteData
208- port .open ()
209- # Simulate the same handshake PrologixGPIBPort.open() sends.
210- for command in ("++mode 1" , "++addr 8" , "++auto 1" , "++eoi 1" , "++eos 2" ,
211- "++eot_enable 1" , "++eot_char 10" , "++read_tmo_ms 1500" ):
212- port .writeString (command + "\n " )
213- self .assertIn ("++mode 1" , sent )
239+ def getQuadratureVoltage (self ):
240+ return 0.2
241+
242+ def getMagnitude (self ):
243+ return 0.3
244+
245+ def getPhase (self ):
246+ return 45.0
247+
248+ def getReferenceFrequency (self ):
249+ return 1000.0
250+
251+ def getInputSource (self ):
252+ return InputSource .SingleEnded
253+
254+ def setInputSource (self , source ):
255+ pass
256+
257+ def getSensitivity (self ):
258+ return 1.0
259+
260+ def setSensitivity (self , volts ):
261+ pass
262+
263+ def getTimeConstant (self ):
264+ return 0.1
265+
266+ def setTimeConstant (self , seconds ):
267+ pass
268+
269+ def setTriggerSource (self , source ):
270+ pass
271+
272+ def getTriggerSource (self ):
273+ return TriggerSource .Internal
274+
275+ def softwareTrigger (self ):
276+ pass
277+
278+
279+ class TestCapabilityDefaults (unittest .TestCase ):
280+ def testOptionalHooksDefaultToNone (self ):
281+ device = _MinimalLockIn ()
282+ self .assertIsNone (device .supportedInputSources ())
283+ self .assertIsNone (device .supportedSensitivities ())
284+ self .assertIsNone (device .supportedTimeConstants ())
285+ self .assertIsNone (device .supportedTriggerSources ())
286+
287+ def testBaseGetDemodulatedValues (self ):
288+ values = _MinimalLockIn ().getDemodulatedValues ()
289+ self .assertEqual (set (values ),
290+ {"X" , "Y" , "R" , "theta" , "referenceFrequency" })
291+ self .assertAlmostEqual (values ["R" ], 0.3 )
292+ self .assertAlmostEqual (values ["referenceFrequency" ], 1000.0 )
293+
294+
295+ class TestPrologixGPIBPort (unittest .TestCase ):
296+ def testControllerHandshakeUsesManualRead (self ):
297+ # Exercise the real PrologixGPIBPort.configureController (it only calls
298+ # writeString, so no serial line is needed) and assert the manual-read
299+ # handshake: ++auto 0 with no controller-appended reply terminator.
300+ port = PrologixGPIBPort (gpibAddress = 8 )
301+ sent = []
302+ port .writeString = lambda text : sent .append (text .strip ())
303+ port .configureController ()
304+ self .assertEqual (sent [0 ], "++mode 1" )
214305 self .assertIn ("++addr 8" , sent )
215- self .assertIn ("++auto 1" , sent )
306+ self .assertIn ("++auto 0" , sent )
307+ self .assertIn ("++eot_enable 0" , sent )
308+ self .assertNotIn ("++auto 1" , sent )
216309
217310 def testQueryRoundTripsThroughStringPrimitives (self ):
218311 port = DebugPrologixGPIBPort ()
@@ -291,5 +384,151 @@ def testAcquireWaveform(self):
291384 self .assertTrue (all (isinstance (value , float ) for value in waveform [StreamChannel .X ]))
292385
293386
387+ class TestDebugPrologixGPIBPort (unittest .TestCase ):
388+ def setUp (self ):
389+ self .port = DebugPrologixGPIBPort ()
390+ self .port .open ()
391+
392+ def testIsOpenReflectsState (self ):
393+ self .assertTrue (self .port .isOpen )
394+ self .port .close ()
395+ self .assertFalse (self .port .isOpen )
396+
397+ def testBytesAvailableAndFlush (self ):
398+ self .port .writeString ("FREQ?\n " )
399+ self .assertGreater (self .port .bytesAvailable (), 0 )
400+ self .port .flush ()
401+ self .assertEqual (self .port .bytesAvailable (), 0 )
402+
403+ def testSampleRateRegisterRoundTrip (self ):
404+ self .port .writeString ("SRAT 7\n " )
405+ self .port .writeString ("SRAT?\n " )
406+ self .assertEqual (self .port .readString ().strip (), "7" )
407+
408+ def testReadTimesOutWhenBufferShort (self ):
409+ with self .assertRaises (CommunicationReadTimeout ):
410+ self .port .readData (1 )
411+
412+ def testControllerLineProducesNoReply (self ):
413+ self .port .writeString ("++mode 1\n " )
414+ self .assertEqual (self .port .bytesAvailable (), 0 )
415+
416+ def testUnknownCommandProducesNoReply (self ):
417+ self .port .writeString ("BOGUS?\n " )
418+ self .assertEqual (self .port .bytesAvailable (), 0 )
419+
420+ def testOutputValueUnknownIndexReadsZero (self ):
421+ self .port .writeString ("OUTP? 9\n " )
422+ self .assertAlmostEqual (float (self .port .readString ()), 0.0 )
423+
424+
425+ class _FakeComPort :
426+ """Stand-in for a serial.tools.list_ports entry."""
427+
428+ def __init__ (self , device , serialNumber , vid = 0x0403 , pid = 0x6001 ):
429+ self .device = device
430+ self .serial_number = serialNumber
431+ self .vid = vid
432+ self .pid = pid
433+
434+
435+ class _FakeAdaptorPort (DebugPrologixGPIBPort ):
436+ """A Prologix port whose *IDN? reply depends on which serial port it opened,
437+ so a discovery probe can be simulated without hardware. identitiesByPath maps
438+ a portPath to its *IDN? reply; a missing/None entry means the adaptor never
439+ answers (the read times out)."""
440+
441+ identitiesByPath = {}
442+
443+ def __init__ (self , gpibAddress , portPath = None , ** kwargs ):
444+ super ().__init__ ()
445+ self .gpibAddress = gpibAddress
446+ self .portPath = portPath
447+
448+ def _process (self , line ):
449+ if line == "*IDN?" :
450+ return type (self ).identitiesByPath .get (self .portPath )
451+ return super ()._process (line )
452+
453+
454+ class TestSR830Discovery (unittest .TestCase ):
455+ """Hardware-free coverage of SR830Device.doInitializeDevice / _candidateAdaptors,
456+ which DebugSR830Device bypasses by overriding doInitializeDevice."""
457+
458+ def setUp (self ):
459+ _FakeAdaptorPort .identitiesByPath = {}
460+
461+ def _patched (self , comportList ):
462+ return (
463+ mock .patch ("hardwarelibrary.daq.sr830device.PrologixGPIBPort" , _FakeAdaptorPort ),
464+ mock .patch ("hardwarelibrary.daq.sr830device.comports" , return_value = comportList ),
465+ )
466+
467+ def testDiscoversSR830AndPinsSerial (self ):
468+ _FakeAdaptorPort .identitiesByPath = {
469+ "/dev/A" : "Keithley,MODEL 2000,1234,A01" ,
470+ "/dev/B" : SR830_IDN ,
471+ }
472+ comportList = [_FakeComPort ("/dev/A" , "AAAA" ), _FakeComPort ("/dev/B" , "BBBB" )]
473+ portPatch , comportPatch = self ._patched (comportList )
474+ with portPatch , comportPatch :
475+ device = SR830Device ()
476+ device .initializeDevice ()
477+ try :
478+ self .assertIn ("SR830" , device .idn )
479+ self .assertEqual (device .serialNumber , "BBBB" )
480+ finally :
481+ device .shutdownDevice ()
482+
483+ def testRaisesWhenNoAdaptorPresent (self ):
484+ comportList = [_FakeComPort ("/dev/Z" , "ZZZZ" , vid = 0x1234 , pid = 0x5678 )]
485+ portPatch , comportPatch = self ._patched (comportList )
486+ with portPatch , comportPatch :
487+ device = SR830Device ()
488+ with self .assertRaises (PhysicalDevice .UnableToInitialize ):
489+ device .initializeDevice ()
490+
491+ def testRaisesWhenNoSR830AmongAdaptors (self ):
492+ _FakeAdaptorPort .identitiesByPath = {
493+ "/dev/A" : None ,
494+ "/dev/B" : "Keithley,MODEL 2000,1234,A01" ,
495+ }
496+ comportList = [_FakeComPort ("/dev/A" , "AAAA" ), _FakeComPort ("/dev/B" , "BBBB" )]
497+ portPatch , comportPatch = self ._patched (comportList )
498+ with portPatch , comportPatch :
499+ device = SR830Device ()
500+ with self .assertRaises (PhysicalDevice .UnableToInitialize ):
501+ device .initializeDevice ()
502+
503+ def testExplicitPortPathIsSoleCandidate (self ):
504+ _FakeAdaptorPort .identitiesByPath = {"/dev/X" : SR830_IDN }
505+ with mock .patch ("hardwarelibrary.daq.sr830device.PrologixGPIBPort" , _FakeAdaptorPort ), \
506+ mock .patch ("hardwarelibrary.daq.sr830device.comports" ,
507+ side_effect = AssertionError ("comports() must not be consulted with an explicit portPath" )):
508+ device = SR830Device (portPath = "/dev/X" )
509+ device .initializeDevice ()
510+ try :
511+ self .assertIn ("SR830" , device .idn )
512+ finally :
513+ device .shutdownDevice ()
514+
515+ def testSerialNumberNarrowsCandidates (self ):
516+ _FakeAdaptorPort .identitiesByPath = {"/dev/match" : SR830_IDN }
517+ comportList = [
518+ _FakeComPort ("/dev/other" , "OTHER" ),
519+ _FakeComPort ("/dev/none" , None ),
520+ _FakeComPort ("/dev/match" , "WANTED" ),
521+ ]
522+ portPatch , comportPatch = self ._patched (comportList )
523+ with portPatch , comportPatch :
524+ device = SR830Device (serialNumber = "WANT" )
525+ device .initializeDevice ()
526+ try :
527+ self .assertIn ("SR830" , device .idn )
528+ self .assertEqual (device .serialNumber , "WANTED" )
529+ finally :
530+ device .shutdownDevice ()
531+
532+
294533if __name__ == '__main__' :
295534 unittest .main ()
0 commit comments