Repository: pymeasure/pymeasure

Issue:
How to test adapter.connection calls
Some instruments call the adapter's `connection`property, like #697 which makes ProtocolAdapter fail. Instruments should not be adjusted to make the ProtocolAdapter work.

The problem is, that you are not supposed to use normally the adapter's connection directly, because the `connection` depends on the specific adapter type. If an instrument calls the connection, the ProtocolAdapter fails. Instruments should not be adjusted to make the ProtocolAdapter work.

Two solutions:

- A generic mock connection seems good, because that would care for many use cases. Otherwise we had to add several methods (clear, close, timeout property, get_stb...) individually.
- Or we could add timeout as a property to the `Adapter`, which will call its connection depending on the Adapter type (ProtocolAdapter stores it as a variable). Similarly the "read_stb" and `clear` etc.


Discussion:
Option 1 sounds preferable/easier to get right.
Also #778 requires the timeout as a property. I guess the best path is a generic mock, but also a timeout property in the `Adapter`.

Sometimes, there are solutions to use VISA methods or general methods (e.g. setting timeout temporarily to 0). The latter is open for more adapters, while the former requires a VISAAdapter.
Therefore, I prefer the more general approach.

What do you think about adding a timeout property to the Adapter (or to all the subclasses) @bilderbuchi?
Every adapter has a timeout, therefore we can make it a universal property.

I'll add also a connection mock call to Protocol for those cases, where we do not have that specific method(flush something, clear...).

Another advantage of a `Adapter.timeout` property: That way we can ensure the right timeout setting regardless of the unit of the adapter (see difference VISA and pyserial). If an instrument sets the timeout to 500, it will be half a second, for the VISAAdapter and for the SerialAdapter, because the latter's property transforms it to 0.5 internally.
> Another advantage of a Adapter.timeout property: That way we can ensure the right timeout setting regardless of the unit of the adapter (see difference VISA and pyserial). If an instrument sets the timeout to 500, it will be half a second, for the VISAAdapter and for the SerialAdapter, because the latter's property transforms it to 0.5 internally.

This is a good application, harmonising that would be useful indeed. Let's use the pyVISA way as a baseline. One thing that concerns me is that this will now make our configuration API more complex, as we need to special-case the `timeout` parameter, and also probably intercept it if people supply it already (but assume the "other" convention), and because it does not land directly in the adapter.connection as we have been saying until now. :-/

Should we use a different name? `timeout_ms` would make the unit clear, allow us to cleanly intercept/deprecate use of `timeout`, and break the correspondence with the respective connection kwarg (pyVISA, serial), so less potential for confusion.
I did not think about the parameters of the adapter, but how the instrument interacts with the present adapter.
However I see the confusion in starting a serial adapter with a timeout of 0.5 and later setting the timeout to 500 for the same effect.

Maybe we just change the serial adapters interpretation of timeout? 

Resolving patch:
diff --git a/pymeasure/adapters/protocol.py b/pymeasure/adapters/protocol.py
--- a/pymeasure/adapters/protocol.py
+++ b/pymeasure/adapters/protocol.py
@@ -23,6 +23,7 @@
 #
 
 import logging
+from unittest.mock import MagicMock
 
 from .adapter import Adapter
 
@@ -50,15 +51,29 @@ class ProtocolAdapter(Adapter):
 
     This adapter is primarily meant for use within :func:`pymeasure.test.expected_protocol()`.
 
+    The :attr:`connection` attribute is a :class:`unittest.mock.MagicMock` such
+    that every call returns. If you want to set a return value, you can use
+    :code:`adapter.connection.some_method.return_value = 7`,
+    such that a call to :code:`adapter.connection.some_method()` will return `7`.
+    Similarly, you can verify that this call to the connection method happened
+    with :code:`assert adapter.connection.some_method.called is True`.
+    You can specify dictionaries with return values of attributes and methods.
+
     :param list comm_pairs: List of "reference" message pair tuples. The first element is
         what is sent to the instrument, the second one is the returned message.
         'None' indicates that a pair member (write or read) does not exist.
         The messages do **not** include the termination characters.
+    :param connection_attributes: Dictionary of connection attributes and their values.
+    :param connection_methods: Dictionary of method names of the connection and their return values.
     """
 
-    def __init__(self, comm_pairs=[], preprocess_reply=None, **kwargs):
+    def __init__(self, comm_pairs=[], preprocess_reply=None,
+                 connection_attributes={},
+                 connection_methods={},
+                 **kwargs):
         """Generate the adapter and initialize internal buffers."""
         super().__init__(preprocess_reply=preprocess_reply, **kwargs)
+        # Setup communication
         assert isinstance(comm_pairs, (list, tuple)), (
             "Parameter comm_pairs has to be a list or tuple.")
         for pair in comm_pairs:
@@ -68,6 +83,15 @@ def __init__(self, comm_pairs=[], preprocess_reply=None, **kwargs):
         self._write_buffer = b""
         self.comm_pairs = comm_pairs
         self._index = 0
+        # Setup attributes
+        self._setup_connection(connection_attributes, connection_methods)
+
+    def _setup_connection(self, connection_attributes, connection_methods):
+        self.connection = MagicMock()
+        for key, value in connection_attributes.items():
+            setattr(self.connection, key, value)
+        for key, value in connection_methods.items():
+            getattr(self.connection, key).return_value = value
 
     def _write(self, command, **kwargs):
         """Compare the command with the expected one and fill the read."""


Test patch:
diff --git a/pymeasure/test.py b/pymeasure/test.py
--- a/pymeasure/test.py
+++ b/pymeasure/test.py
@@ -28,7 +28,9 @@
 
 
 @contextmanager
-def expected_protocol(instrument_cls, comm_pairs, **kwargs):
+def expected_protocol(instrument_cls, comm_pairs,
+                      connection_attributes={}, connection_methods={},
+                      **kwargs):
     """Context manager that checks sent/received instrument commands without a
     device connected.
 
@@ -47,10 +49,13 @@ def expected_protocol(instrument_cls, comm_pairs, **kwargs):
         'None' indicates that a pair member (command or response) does not
         exist, e.g. `(None, 'RESP1')`. Commands and responses are without
         termination characters.
+    :param connection_attributes: Dictionary of connection attributes and their values.
+    :param connection_methods: Dictionary of method names of the connection and their return values.
     :param \\**kwargs:
         Keyword arguments for the instantiation of the instrument.
     """
-    protocol = ProtocolAdapter(comm_pairs)
+    protocol = ProtocolAdapter(comm_pairs, connection_attributes=connection_attributes,
+                               connection_methods=connection_methods)
     instr = instrument_cls(protocol, **kwargs)
     yield instr
     assert protocol._index == len(comm_pairs), (
diff --git a/tests/adapters/test_protocol.py b/tests/adapters/test_protocol.py
--- a/tests/adapters/test_protocol.py
+++ b/tests/adapters/test_protocol.py
@@ -22,11 +22,19 @@
 # THE SOFTWARE.
 #
 
+from unittest.mock import call
+import pytest
+
 from pymeasure.adapters.protocol import to_bytes, ProtocolAdapter
 
 from pytest import mark, raises, fixture
 
 
+@pytest.fixture
+def adapter():
+    return ProtocolAdapter()
+
+
 @mark.parametrize("input, output", (("superXY", b"superXY"),
                                     ([1, 2, 3, 4], b"\x01\x02\x03\x04"),
                                     (5, b"5"),
@@ -46,6 +54,27 @@ def test_protocol_instantiation():
     assert a.comm_pairs == [("write", "read"), ("write_only", None)]
 
 
+@pytest.fixture
+def mockAdapter():
+    adapter = ProtocolAdapter(connection_attributes={'timeout': 100},
+                              connection_methods={'stb': 17})
+    return adapter
+
+
+def test_connection_call(mockAdapter):
+    """Test whether a call to the connection is registered."""
+    mockAdapter.connection.clear(7)
+    assert mockAdapter.connection.clear.call_args_list == [call(7)]
+
+
+def test_connection_attribute(mockAdapter):
+    assert mockAdapter.connection.timeout == 100
+
+
+def test_connection_method(mockAdapter):
+    assert mockAdapter.connection.stb() == 17
+
+
 class Test_write:
     def test_write(self):
         a = ProtocolAdapter([("written 5", 5)])
diff --git a/tests/instruments/lecroy/test_lecroyT3DSO1204.py b/tests/instruments/lecroy/test_lecroyT3DSO1204.py
--- a/tests/instruments/lecroy/test_lecroyT3DSO1204.py
+++ b/tests/instruments/lecroy/test_lecroyT3DSO1204.py
@@ -259,7 +259,8 @@ def test_download_one_point():
              (b"C1:VDIV?", b"5.00E-02"),
              (b"C1:OFST?", b"-1.50E-01"),
              (b"C1:UNIT?", b"V")
-             ]
+             ],
+            connection_attributes={'chunk_size': 0},
     ) as instr:
         y, x, preamble = instr.download_waveform(source="c1", requested_points=1, sparsing=1)
         assert preamble == {
@@ -308,7 +309,8 @@ def test_download_two_points():
              (b"C1:VDIV?", b"5.00E-02"),
              (b"C1:OFST?", b"-1.50E-01"),
              (b"C1:UNIT?", b"V")
-             ]
+             ],
+            connection_attributes={'chunk_size': 0},
     ) as instr:
         y, x, preamble = instr.download_waveform(source="c1", requested_points=2, sparsing=1)
         assert preamble == {
diff --git a/tests/test_expected_protocol.py b/tests/test_expected_protocol.py
--- a/tests/test_expected_protocol.py
+++ b/tests/test_expected_protocol.py
@@ -22,7 +22,6 @@
 # THE SOFTWARE.
 #
 
-import pytest
 from pytest import raises
 
 from pymeasure.test import expected_protocol
@@ -126,16 +125,19 @@ def values(self, command, **kwargs):
         assert instr.simple == 3.12345
 
 
-@pytest.mark.xfail
-def test_preprocess_reply_on_init():
-    # TODO: For now preprocess_reply at init level is nonfunctional because this
-    #  lives in the real adapter, should be fixed with #567
-    class InstrumentWithPreprocess(BasicTestInstrument):
-        def __init__(self, adapter, **kwargs):
-            super().__init__(adapter, preprocess_reply=lambda v: v + "2345", **kwargs)
+class TestConnectionCalls:
+    def test_connection_method_call(self):
+        with expected_protocol(
+                BasicTestInstrument,
+                [],
+                connection_methods={'stb': 17}
+        ) as inst:
+            assert inst.adapter.connection.stb() == 17
 
-    with expected_protocol(
-        InstrumentWithPreprocess,
-        [("VOLT?", "3.1")]
-    ) as instr:
-        assert instr.simple == 3.12345
+    def test_connection_attribute(self):
+        with expected_protocol(
+                BasicTestInstrument,
+                [],
+                connection_attributes={'timeout': 100}
+        ) as inst:
+            assert inst.adapter.connection.timeout == 100