1#!/usr/bin/env python3
2#
3#   Copyright 2018 - The Android Open Source Project
4#
5#   Licensed under the Apache License, Version 2.0 (the "License");
6#   you may not use this file except in compliance with the License.
7#   You may obtain a copy of the License at
8#
9#       http://www.apache.org/licenses/LICENSE-2.0
10#
11#   Unless required by applicable law or agreed to in writing, software
12#   distributed under the License is distributed on an "AS IS" BASIS,
13#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14#   See the License for the specific language governing permissions and
15#   limitations under the License.
16
17import enum
18import logging
19import os
20
21from acts.event import event_bus
22from acts.event.event import Event
23from acts.event.event import TestCaseBeginEvent
24from acts.event.event import TestCaseEndEvent
25from acts.event.event import TestCaseEvent
26from acts.event.event import TestClassBeginEvent
27from acts.event.event import TestClassEndEvent
28from acts.event.event import TestClassEvent
29
30
31class ContextLevel(enum.IntEnum):
32    ROOT = 0
33    TESTCLASS = 1
34    TESTCASE = 2
35
36
37def get_current_context(depth=None):
38    """Get the current test context at the specified depth.
39    Pulls the most recently created context, with a level at or below the given
40    depth, from the _contexts stack.
41
42    Args:
43        depth: The desired context level. For example, the TESTCLASS level would
44            yield the current test class context, even if the test is currently
45            within a test case.
46
47    Returns: An instance of TestContext.
48    """
49    if depth is None:
50        return _contexts[-1]
51    return _contexts[min(depth, len(_contexts)-1)]
52
53
54def get_context_for_event(event):
55    """Creates and returns a TestContext from the given event.
56    A TestClassContext is created for a TestClassEvent, and a TestCaseContext
57    is created for a TestCaseEvent.
58
59    Args:
60        event: An instance of TestCaseEvent or TestClassEvent.
61
62    Returns: An instance of TestContext corresponding to the event.
63
64    Raises: TypeError if event is neither a TestCaseEvent nor TestClassEvent
65    """
66    if isinstance(event, TestCaseEvent):
67        return _get_context_for_test_case_event(event)
68    if isinstance(event, TestClassEvent):
69        return _get_context_for_test_class_event(event)
70    raise TypeError('Unrecognized event type: %s %s', event, event.__class__)
71
72
73def _get_context_for_test_case_event(event):
74    """Generate a TestCaseContext from the given TestCaseEvent."""
75    return TestCaseContext(event.test_class, event.test_case)
76
77
78def _get_context_for_test_class_event(event):
79    """Generate a TestClassContext from the given TestClassEvent."""
80    return TestClassContext(event.test_class)
81
82
83class NewContextEvent(Event):
84    """The event posted when a test context has changed."""
85
86
87class NewTestClassContextEvent(NewContextEvent):
88    """The event posted when the test class context has changed."""
89
90
91class NewTestCaseContextEvent(NewContextEvent):
92    """The event posted when the test case context has changed."""
93
94
95def _update_test_class_context(event):
96    """Pushes a new TestClassContext to the _contexts stack upon a
97    TestClassBeginEvent. Pops the most recent context off the stack upon a
98    TestClassEndEvent. Posts the context change to the event bus.
99
100    Args:
101        event: An instance of TestClassBeginEvent or TestClassEndEvent.
102    """
103    if isinstance(event, TestClassBeginEvent):
104        _contexts.append(_get_context_for_test_class_event(event))
105    if isinstance(event, TestClassEndEvent):
106        if _contexts:
107            _contexts.pop()
108    event_bus.post(NewTestClassContextEvent())
109
110
111def _update_test_case_context(event):
112    """Pushes a new TestCaseContext to the _contexts stack upon a
113    TestCaseBeginEvent. Pops the most recent context off the stack upon a
114    TestCaseEndEvent. Posts the context change to the event bus.
115
116    Args:
117        event: An instance of TestCaseBeginEvent or TestCaseEndEvent.
118    """
119    if isinstance(event, TestCaseBeginEvent):
120        _contexts.append(_get_context_for_test_case_event(event))
121    if isinstance(event, TestCaseEndEvent):
122        if _contexts:
123            _contexts.pop()
124    event_bus.post(NewTestCaseContextEvent())
125
126
127event_bus.register(TestClassEvent, _update_test_class_context)
128event_bus.register(TestCaseBeginEvent, _update_test_case_context, order=-100)
129event_bus.register(TestCaseEndEvent, _update_test_case_context, order=100)
130
131
132class TestContext(object):
133    """An object representing the current context in which a test is executing.
134
135    The context encodes the current state of the test runner with respect to a
136    particular scenario in which code is being executed. For example, if some
137    code is being executed as part of a test case, then the context should
138    encode information about that test case such as its name or enclosing
139    class.
140
141    The subcontext specifies a relative path in which certain outputs,
142    e.g. logcat, should be kept for the given context.
143
144    The full output path is given by
145    <base_output_path>/<context_dir>/<subcontext>.
146
147    Attributes:
148        _base_output_paths: a dictionary mapping a logger's name to its base
149                            output path
150        _subcontexts: a dictionary mapping a logger's name to its
151                      subcontext-level output directory
152    """
153
154    _base_output_paths = {}
155    _subcontexts = {}
156
157    def get_base_output_path(self, log_name=None):
158        """Gets the base output path for this logger.
159
160        The base output path is interpreted as the reporting root for the
161        entire test runner.
162
163        If a path has been added with add_base_output_path, it is returned.
164        Otherwise, a default is determined by _get_default_base_output_path().
165
166        Args:
167            log_name: The name of the logger.
168
169        Returns:
170            The output path.
171        """
172        if log_name in self._base_output_paths:
173            return self._base_output_paths[log_name]
174        return self._get_default_base_output_path()
175
176    @classmethod
177    def add_base_output_path(cls, log_name, base_output_path):
178        """Store the base path for this logger.
179
180        Args:
181            log_name: The name of the logger.
182            base_output_path: The base path of output files for this logger.
183            """
184        cls._base_output_paths[log_name] = base_output_path
185
186    def get_subcontext(self, log_name=None):
187        """Gets the subcontext for this logger.
188
189        The subcontext is interpreted as the directory, relative to the
190        context-level path, where all outputs of the given logger are stored.
191
192        If a path has been added with add_subcontext, it is returned.
193        Otherwise, the empty string is returned.
194
195        Args:
196            log_name: The name of the logger.
197
198        Returns:
199            The output path.
200        """
201        return self._subcontexts.get(log_name, '')
202
203    @classmethod
204    def add_subcontext(cls, log_name, subcontext):
205        """Store the subcontext path for this logger.
206
207        Args:
208            log_name: The name of the logger.
209            subcontext: The relative subcontext path of output files for this
210                        logger.
211        """
212        cls._subcontexts[log_name] = subcontext
213
214    def get_full_output_path(self, log_name=None):
215        """Gets the full output path for this context.
216
217        The full path represents the absolute path to the output directory,
218        as given by <base_output_path>/<context_dir>/<subcontext>
219
220        Args:
221            log_name: The name of the logger. Used to specify the base output
222                      path and the subcontext.
223
224        Returns:
225            The output path.
226        """
227
228        path = os.path.join(self.get_base_output_path(log_name),
229                            self._get_default_context_dir(),
230                            self.get_subcontext(log_name))
231        os.makedirs(path, exist_ok=True)
232        return path
233
234    @property
235    def identifier(self):
236        raise NotImplementedError()
237
238    def _get_default_base_output_path(self):
239        """Gets the default base output path.
240
241        This will attempt to use the ACTS logging path set up in the global
242        logger.
243
244        Returns:
245            The logging path.
246
247        Raises:
248            EnvironmentError: If the ACTS logger has not been initialized.
249        """
250        try:
251            return logging.log_path
252        except AttributeError as e:
253            raise EnvironmentError(
254                'The ACTS logger has not been set up and'
255                ' "base_output_path" has not been set.') from e
256
257    def _get_default_context_dir(self):
258        """Gets the default output directory for this context."""
259        raise NotImplementedError()
260
261
262class RootContext(TestContext):
263    """A TestContext that represents a test run."""
264
265    @property
266    def identifier(self):
267        return 'root'
268
269    def _get_default_context_dir(self):
270        """Gets the default output directory for this context.
271
272        Logs at the root level context are placed directly in the base level
273        directory, so no context-level path exists."""
274        return ''
275
276
277class TestClassContext(TestContext):
278    """A TestContext that represents a test class.
279
280    Attributes:
281        test_class: The test class instance that this context represents.
282    """
283
284    def __init__(self, test_class):
285        """Initializes a TestClassContext for the given test class.
286
287        Args:
288            test_class: A test class object. Must be an instance of the test
289                        class, not the class object itself.
290        """
291        self.test_class = test_class
292
293    @property
294    def test_class_name(self):
295        return self.test_class.__class__.__name__
296
297    @property
298    def identifier(self):
299        return self.test_class_name
300
301    def _get_default_context_dir(self):
302        """Gets the default output directory for this context.
303
304        For TestClassContexts, this will be the name of the test class. This is
305        in line with the ACTS logger itself.
306        """
307        return self.test_class_name
308
309
310class TestCaseContext(TestContext):
311    """A TestContext that represents a test case.
312
313    Attributes:
314        test_case: The string name of the test case.
315        test_class: The test class instance enclosing the test case.
316    """
317
318    def __init__(self, test_class, test_case):
319        """Initializes a TestCaseContext for the given test case.
320
321        Args:
322            test_class: A test class object. Must be an instance of the test
323                        class, not the class object itself.
324            test_case: The string name of the test case.
325        """
326        self.test_class = test_class
327        self.test_case = test_case
328
329    @property
330    def test_case_name(self):
331        return self.test_case
332
333    @property
334    def test_class_name(self):
335        return self.test_class.__class__.__name__
336
337    @property
338    def identifier(self):
339        return '%s.%s' % (self.test_class_name, self.test_case_name)
340
341    def _get_default_context_dir(self):
342        """Gets the default output directory for this context.
343
344        For TestCaseContexts, this will be the name of the test class followed
345        by the name of the test case. This is in line with the ACTS logger
346        itself.
347        """
348        return os.path.join(
349            self.test_class_name,
350            self.test_case_name)
351
352
353# stack for keeping track of the current test context
354_contexts = [RootContext()]
355