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