summaryrefslogtreecommitdiff
path: root/src/sardana/macroserver/macros/test/base.py
blob: 7c2c0f51c2915473f7e49682f5f56e9193e904dc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
#!/usr/bin/env python

##############################################################################
##
## This file is part of Sardana
##
## http://www.tango-controls.org/static/sardana/latest/doc/html/index.html
##
## Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain
##
## Sardana is free software: you can redistribute it and/or modify
## it under the terms of the GNU Lesser General Public License as published by
## the Free Software Foundation, either version 3 of the License, or
## (at your option) any later version.
##
## Sardana is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU Lesser General Public License for more details.
##
## You should have received a copy of the GNU Lesser General Public License
## along with Sardana.  If not, see <http://www.gnu.org/licenses/>.
##
##############################################################################

"""System tests for Macros"""

__all__ = ['macroTest', 'BaseMacroTestCase', 'RunMacroTestCase',
           'RunStopMacroTestCase', 'testRun', 'testFail' 'testStop']
import time
import functools
from sardana import sardanacustomsettings
from sardana.macroserver.macros.test import MacroExecutorFactory


#Define a "_NOT_PASSED" object to mark a keyword arg which is not passed
# Note that we do not want to use None because one may want to pass None
class __NotPassedType(int):
    pass
_NOT_PASSED = __NotPassedType()


def macroTest(klass=None, helper_name=None, test_method_name=None,
              test_method_doc=None, **helper_kwargs):
    """Decorator to insert test methods from a helper method that accepts
    arguments.

    macroTest provides a very economic API for creating new tests for a given
    class based on a helper method.

    macroTest accepts the following arguments:

        - helper_name (str): the name of the helper method. macroTest will
                             insert a test method which calls the helper with
                             any  the helper_kwargs (see below).
        - test_method_name (str): Optional. Name of the test method to be used.
                                 If None given, one will be generated from the
                                 macro and helper names.
        - test_method_doc (str): The docstring for the inserted test method
                                 (this shows in the unit test output). If None
                                 given, a default one is generated which
                                 includes the input parameters and the helper
                                 name.
        - \*\*helper_kwargs: Any remaining keyword arguments are passed to the
                           helper.

    macroTest assumes that the decorated class inherits from unittest.TestCase
    and that it has a macro_name class member.

    This decorator can be considered a "base" decorator. It is often used to
    create other decorators in which the helper method is pre-set. Some
    of them are already provided in this module:

    - :meth:`testRun` is equivalent to macroTest with helper_name='macro_runs'
    - :meth:`testStop` is equivalent to macroTest with helper_name='macro_stops'
    - :meth:`testFail` is equivalent to macroTest with helper_name='macro_fails'

    The advantage of using the decorators compared to writing the test methods
    directly is that the helper method can get keyword arguments and therefore
    avoid duplication of code for very similar tests (think, e.g. on writing
    similar tests for various sets of macro input parameters):

    Consider the following code written using the
    :meth:`RunMacroTestCase.macro_runs` helper::

        class FooTest(RunMacroTestCase, unittest.TestCase)
            macro_name = twice

            def test_foo_runs_with_input_2(self):
                '''test that twice(2) runs'''
                self.macro_runs(macro_params=['2'])

            def test_foo_runs_with_input_minus_1(self):
                '''test that twice(2) runs'''
                self.macro_runs(macro_params=['-1'])

    The equivalent code could be written as::

        @macroTest(helper_name='macro_runs', macro_params=['2'])
        @macroTest(helper_name='macro_runs', macro_params=['-1'])
        class FooTest(RunMacroTestCase, unittest.TestCase):
            macro_name = 'twice'

    Or, even better, using the specialized testRun decorator::

        @testRun(macro_params=['2'])
        @testRun(macro_params=['-1'])
        class FooTest(RunMacroTestCase, unittest.TestCase):
            macro_name = 'twice'

    """
    #TODO: Note: this could be generalized to general tests.
    #      In fact the only "macro-specific" thing here is the assumption
    #      that klass.macro_name exists

    if klass is None:  # recipe to use
        return functools.partial(macroTest, helper_name=helper_name,
                                 test_method_name=test_method_name,
                                 test_method_doc=test_method_doc,
                                 **helper_kwargs)

    if helper_name is None:
        raise ValueError('helper_name argument is not optional')

    if test_method_name is None:
        test_method_name = 'test_%s_%s' % (klass.macro_name, helper_name)
    #Append an index if necessary to avoid overwriting the test method
    name, i = test_method_name, 1
    while (hasattr(klass, name)):
        i += 1
        name = "%s_%i" % (test_method_name, i)
    test_method_name = name

    if test_method_doc is None:
        argsrep = ', '.join(['%s=%s' % (k, v)
                            for k, v in helper_kwargs.items()])
        test_method_doc = 'Testing %s with %s(%s)' % (klass.macro_name,
                                                      helper_name, argsrep)

    # New test implementation
    # Sets the passed parameters and adds super and self implementation
    def newTest(obj):
        helper = getattr(obj, helper_name)
        return helper(**helper_kwargs)

    #setup a custom docstring
    newTest.__doc__ = test_method_doc

    # Add the new test method with the new implementation
    setattr(klass, test_method_name, newTest)

    return klass

#Definition of specializations of the macroTest decorator:
testRun = functools.partial(macroTest, helper_name='macro_runs')
testStop = functools.partial(macroTest, helper_name='macro_stops')
testFail = functools.partial(macroTest, helper_name='macro_fails')


class BaseMacroTestCase(object):

    """An abstract class for macro testing.
    BaseMacroTestCase will provide a `macro_executor` member which is an
    instance of BaseMacroExecutor and which can be used to run a macro.

    To use it, simply inherit from BaseMacroTestCase *and* unittest.TestCase
    and provide the following class members:

      - macro_name (string) name of the macro to be tested (mandatory)
      - door_name (string) name of the door where the macro will be executed.
                 This is optional. If not set,
                 `sardanacustomsettings.UNITTEST_DOOR_NAME` is used

    Then you may define test methods.
    """
    macro_name = None
    door_name = getattr(sardanacustomsettings, 'UNITTEST_DOOR_NAME')

    def setUp(self):
        """ A macro_executor instance must be created
        """
        if self.macro_name is None:
            msg = '%s does not define macro_name' % self.__class__.__name__
            raise NotImplementedError(msg)

        mefact = MacroExecutorFactory()
        self.macro_executor = mefact.getMacroExecutor(self.door_name)

    def tearDown(self):
        """The macro_executor instance must be removed
        """
        self.macro_executor.unregisterAll()
        self.macro_executor = None


class RunMacroTestCase(BaseMacroTestCase):

    """A base class for testing execution of arbitrary Sardana macros.
    See :class:`BaseMacroTestCase` for requirements.

    It provides the following helper methods:
      - :meth:`macro_runs`
      - :meth:`macro_fails`
    """

    def assertFinished(self, msg):
        """Asserts that macro has finished.
        """
        finishStates = [u'finish']
        state = self.macro_executor.getState()
        #TODO buffer is just for debugging, attach only the last state
        state_buffer = self.macro_executor.getStateBuffer()
        msg = msg + '; State history=%s' % state_buffer
        self.assertIn(state, finishStates, msg)

    def setUp(self):
        """Preconditions:
        - Those from :class:`BaseMacroTestCase`
        - the macro executor registers to all the log levels
        """
        BaseMacroTestCase.setUp(self)
        self.macro_executor.registerAll()

    def macro_runs(self, macro_params=None, wait_timeout=float("inf"),
                   data=_NOT_PASSED):
        """A helper method to create tests that check if the macro can be
        successfully executed for the given input parameters. It may also
        optionally perform checks on the outputs from the execution.

        :param macro_params: (seq<str>): parameters for running the macro.
                             If passed, they must be given as a sequence of
                             their string representations.
        :param wait_timeout: (float) maximum allowed time (in s) for the macro
                             to finish. By default infinite timeout is used.
        :param data: (obj) Optional. If passed, the macro data after the
                     execution is tested to be equal to this.
        """
        self.macro_executor.run(macro_name=self.macro_name,
                                macro_params=macro_params,
                                sync=True, timeout=wait_timeout)
        self.assertFinished('Macro %s did not finish' % self.macro_name)

        #check if the data of the macro is the expected one
        if data is not _NOT_PASSED:
            actual_data = self.macro_executor.getData()
            msg = 'Macro data does not match expected data:\n' + \
                  'obtained=%s\nexpected=%s' % (actual_data, data)
            self.assertEqual(actual_data, data, msg)

        #TODO: implement generic asserts for macro result and macro output, etc
        #      in a similar way to what is done for macro data

    def macro_fails(self, macro_params=None, wait_timeout=float("inf"),
                    exception=None):
        """Check that the macro fails to run for the given input parameters

        :param macro_params: (seq<str>) input parameters for the macro
        :param wait_timeout: maximum allowed time for the macro to fail. By
                             default infinite timeout is used.
        :param exception: (str or Exception) if given, an additional check of
                        the type of the exception is done.
                        (IMPORTANT: this is just a comparison of str
                        representations of exception objects)
        """
        self.macro_executor.run(macro_name=self.macro_name,
                                macro_params=macro_params,
                                sync=True, timeout=wait_timeout)
        state = self.macro_executor.getState()
        actual_exc_str = self.macro_executor.getExceptionStr()
        msg = 'Post-execution state should be "exception" (got "%s")' % state
        self.assertEqual(state, 'exception', msg)

        if exception is not None:
            msg = 'Raised exception does not match expected exception:\n' + \
                  'raised=%s\nexpected=%s' % (actual_exc_str, exception)
            self.assertEqual(actual_exc_str, str(exception), msg)


class RunStopMacroTestCase(RunMacroTestCase):

    """This is an extension of :class:`RunMacroTestCase` to include helpers for
    testing the abort process of a macro. Useful for Runnable and Stopable
    macros.

    It provides the :meth:`macro_stops` helper
    """

    def assertStopped(self, msg):
        """Asserts that macro was stopped
        """
        stoppedStates = [u'stop']
        state = self.macro_executor.getState()
        #TODO buffer is just for debugging, attach only the last state
        state_buffer = self.macro_executor.getStateBuffer()
        msg = msg + '; State buffer was %s' % state_buffer
        self.assertIn(state, stoppedStates, msg)

    def macro_stops(self, macro_params=None, stop_delay=0.1,
                    wait_timeout=float("inf")):
        """A helper method to create tests that check if the macro can be
        successfully stoped (a.k.a. aborted) after it has been launched.

        :param macro_params: (seq<str>): parameters for running the macro.
                             If passed, they must be given as a sequence of
                             their string representations.
        :param stop_delay: (float) Time (in s) to wait between launching the
                           macro and sending the stop command. default=0.1
        :param wait_timeout: (float) maximum allowed time (in s) for the macro
                             to finish. By default infinite timeout is used.
        """
        self.macro_executor.run(macro_name=self.macro_name,
                                macro_params=macro_params,
                                sync=False)

        if stop_delay is not None:
            time.sleep(stop_delay)
        self.macro_executor.stop()
        self.macro_executor.wait(timeout=wait_timeout)
        self.assertStopped('Macro %s did not stop' % self.macro_name)


if __name__ == '__main__':
    import unittest
    from sardana.macroserver.macros.test import SarDemoEnv

    _m1 = SarDemoEnv().getMotors()[0]

    #@testRun(macro_params=[_m1, '0', '100', '4', '.1'])
    @testRun(macro_params=[_m1, '1', '0', '2', '.1'])
    @testRun(macro_params=[_m1, '0', '1', '4', '.1'])
    class dummyAscanTest(RunStopMacroTestCase, unittest.TestCase):
        macro_name = 'ascan'

    @testRun(macro_params=['1'], data={'in': 1, 'out': 2})
    @testRun(macro_params=['5'])
    @testRun
    class dummyTwiceTest(RunStopMacroTestCase, unittest.TestCase):
        macro_name = 'twice'

    @testFail
    @testFail(exception=Exception)
    class dummyRaiseException(RunStopMacroTestCase, unittest.TestCase):
        macro_name = 'raise_exception'

    suite = unittest.defaultTestLoader.loadTestsFromTestCase(
        dummyRaiseException)
    unittest.TextTestRunner(descriptions=True, verbosity=2).run(suite)