1- import copy
2- import enum
31import os
42import random
53import string
64import subprocess
75import time
8- from contextlib import contextmanager
96from pathlib import Path
10- from typing import Any , Literal
7+ from typing import Any
118
12- import numpy as np
139import pytest
1410from aioca import purge_channel_caches
15- from pytest_mock import MockerFixture
1611
17- from fastcs .attributes import AttrR , AttrRW , AttrW , Handler , Sender , Updater
18- from fastcs .controller import Controller , SubController
19- from fastcs .datatypes import Bool , Enum , Float , Int , String , WaveForm
20- from fastcs .wrappers import command , scan
12+ from fastcs .attributes import AttrR , AttrRW , AttrW
13+ from fastcs .datatypes import Bool , Float , Int , String
14+ from tests .assertable_controller import (
15+ TestController ,
16+ TestHandler ,
17+ TestSender ,
18+ TestUpdater ,
19+ )
2120
2221DATA_PATH = Path (__file__ ).parent / "data"
2322
2423
24+ class BackendTestController (TestController ):
25+ read_int : AttrR = AttrR (Int (), handler = TestUpdater ())
26+ read_write_int : AttrRW = AttrRW (Int (), handler = TestHandler ())
27+ read_write_float : AttrRW = AttrRW (Float ())
28+ read_bool : AttrR = AttrR (Bool ())
29+ write_bool : AttrW = AttrW (Bool (), handler = TestSender ())
30+ read_string : AttrRW = AttrRW (String ())
31+ big_enum : AttrR = AttrR (
32+ Int (
33+ allowed_values = list (range (17 )),
34+ ),
35+ )
36+
37+
38+ @pytest .fixture
39+ def controller ():
40+ return BackendTestController ()
41+
42+
2543@pytest .fixture
2644def data () -> Path :
2745 return DATA_PATH
@@ -45,131 +63,6 @@ def pytest_internalerror(excinfo: pytest.ExceptionInfo[Any]):
4563 raise excinfo .value
4664
4765
48- class TestUpdater (Updater ):
49- update_period = 1
50-
51- async def update (self , controller , attr ):
52- print (f"{ controller } update { attr } " )
53-
54-
55- class TestSender (Sender ):
56- async def put (self , controller , attr , value ):
57- print (f"{ controller } : { attr } = { value } " )
58-
59-
60- class TestHandler (Handler , TestUpdater , TestSender ):
61- pass
62-
63-
64- class TestSubController (SubController ):
65- read_int : AttrR = AttrR (Int (), handler = TestUpdater ())
66-
67-
68- class TestController (Controller ):
69- def __init__ (self ) -> None :
70- super ().__init__ ()
71-
72- self ._sub_controllers : list [TestSubController ] = []
73- for index in range (1 , 3 ):
74- controller = TestSubController ()
75- self ._sub_controllers .append (controller )
76- self .register_sub_controller (f"SubController{ index :02d} " , controller )
77-
78- read_int : AttrR = AttrR (Int (), handler = TestUpdater ())
79- read_write_int : AttrRW = AttrRW (Int (), handler = TestHandler ())
80- read_write_float : AttrRW = AttrRW (Float ())
81- read_bool : AttrR = AttrR (Bool ())
82- write_bool : AttrW = AttrW (Bool (), handler = TestSender ())
83- read_string : AttrRW = AttrRW (String ())
84- enum : AttrRW = AttrRW (Enum (enum .IntEnum ("Enum" , {"RED" : 0 , "GREEN" : 1 , "BLUE" : 2 })))
85- one_d_waveform : AttrRW = AttrRW (WaveForm (np .int32 , (10 ,)))
86- two_d_waveform : AttrRW = AttrRW (WaveForm (np .int32 , (10 , 10 )))
87- big_enum : AttrR = AttrR (
88- Int (
89- allowed_values = list (range (17 )),
90- ),
91- )
92-
93- initialised = False
94- connected = False
95- count = 0
96-
97- async def initialise (self ) -> None :
98- self .initialised = True
99-
100- async def connect (self ) -> None :
101- self .connected = True
102-
103- @command ()
104- async def go (self ):
105- pass
106-
107- @scan (0.01 )
108- async def counter (self ):
109- self .count += 1
110-
111-
112- class AssertableController (TestController ):
113- def __init__ (self , mocker : MockerFixture ) -> None :
114- super ().__init__ ()
115- self .mocker = mocker
116-
117- @contextmanager
118- def assert_read_here (self , path : list [str ]):
119- yield from self ._assert_method (path , "get" )
120-
121- @contextmanager
122- def assert_write_here (self , path : list [str ]):
123- yield from self ._assert_method (path , "process" )
124-
125- @contextmanager
126- def assert_execute_here (self , path : list [str ]):
127- yield from self ._assert_method (path , "" )
128-
129- def _assert_method (self , path : list [str ], method : Literal ["get" , "process" , "" ]):
130- """
131- This context manager can be used to confirm that a fastcs
132- controller's respective attribute or command methods are called
133- a single time within a context block
134- """
135- queue = copy .deepcopy (path )
136-
137- # Navigate to subcontroller
138- controller = self
139- item_name = queue .pop (- 1 )
140- for item in queue :
141- controllers = controller .get_sub_controllers ()
142- controller = controllers [item ]
143-
144- # create probe
145- if method :
146- attr = getattr (controller , item_name )
147- spy = self .mocker .spy (attr , method )
148- else :
149- spy = self .mocker .spy (controller , item_name )
150- initial = spy .call_count
151- try :
152- yield # Enter context
153- except Exception as e :
154- raise e
155- else : # Exit context
156- final = spy .call_count
157- assert final == initial + 1 , (
158- f"Expected { '.' .join (path + [method ] if method else path )} "
159- f"to be called once, but it was called { final - initial } times."
160- )
161-
162-
163- @pytest .fixture
164- def controller ():
165- return TestController ()
166-
167-
168- @pytest .fixture (scope = "class" )
169- def assertable_controller (class_mocker : MockerFixture ):
170- return AssertableController (class_mocker )
171-
172-
17366PV_PREFIX = "" .join (random .choice (string .ascii_lowercase ) for _ in range (12 ))
17467HERE = Path (os .path .dirname (os .path .abspath (__file__ )))
17568
0 commit comments