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 21 22class ContextLevel(enum.IntEnum): 23 ROOT = 0 24 TESTCLASS = 1 25 TESTCASE = 2 26 27 28def get_current_context(depth=None): 29 """Get the current test context at the specified depth. 30 Pulls the most recently created context, with a level at or below the given 31 depth, from the _contexts stack. 32 33 Args: 34 depth: The desired context level. For example, the TESTCLASS level would 35 yield the current test class context, even if the test is currently 36 within a test case. 37 38 Returns: An instance of TestContext. 39 """ 40 if depth is None: 41 return _contexts[-1] 42 return _contexts[min(depth, len(_contexts) - 1)] 43 44 45def append_test_context(test_class_name, test_name): 46 """Add test-specific context to the _contexts stack. 47 A test should should call append_test_context() at test start and 48 pop_test_context() upon test end. 49 50 Args: 51 test_class_name: name of the test class. 52 test_name: name of the test. 53 """ 54 if _contexts: 55 _contexts.append(TestCaseContext(test_class_name, test_name)) 56 57 58def pop_test_context(): 59 """Remove the latest test-specific context from the _contexts stack. 60 A test should should call append_test_context() at test start and 61 pop_test_context() upon test end. 62 """ 63 if _contexts: 64 _contexts.pop() 65 66 67class TestContext(object): 68 """An object representing the current context in which a test is executing. 69 70 The context encodes the current state of the test runner with respect to a 71 particular scenario in which code is being executed. For example, if some 72 code is being executed as part of a test case, then the context should 73 encode information about that test case such as its name or enclosing 74 class. 75 76 The subcontext specifies a relative path in which certain outputs, 77 e.g. logcat, should be kept for the given context. 78 79 The full output path is given by 80 <base_output_path>/<context_dir>/<subcontext>. 81 82 Attributes: 83 _base_output_paths: a dictionary mapping a logger's name to its base 84 output path 85 _subcontexts: a dictionary mapping a logger's name to its 86 subcontext-level output directory 87 """ 88 89 _base_output_paths = {} 90 _subcontexts = {} 91 92 def get_base_output_path(self, log_name=None): 93 """Gets the base output path for this logger. 94 95 The base output path is interpreted as the reporting root for the 96 entire test runner. 97 98 If a path has been added with add_base_output_path, it is returned. 99 Otherwise, a default is determined by _get_default_base_output_path(). 100 101 Args: 102 log_name: The name of the logger. 103 104 Returns: 105 The output path. 106 """ 107 if log_name in self._base_output_paths: 108 return self._base_output_paths[log_name] 109 return self._get_default_base_output_path() 110 111 def get_subcontext(self, log_name=None): 112 """Gets the subcontext for this logger. 113 114 The subcontext is interpreted as the directory, relative to the 115 context-level path, where all outputs of the given logger are stored. 116 117 If a path has been added with add_subcontext, it is returned. 118 Otherwise, the empty string is returned. 119 120 Args: 121 log_name: The name of the logger. 122 123 Returns: 124 The output path. 125 """ 126 return self._subcontexts.get(log_name, '') 127 128 def get_full_output_path(self, log_name=None): 129 """Gets the full output path for this context. 130 131 The full path represents the absolute path to the output directory, 132 as given by <base_output_path>/<context_dir>/<subcontext> 133 134 Args: 135 log_name: The name of the logger. Used to specify the base output 136 path and the subcontext. 137 138 Returns: 139 The output path. 140 """ 141 142 path = os.path.join( 143 self.get_base_output_path(log_name), self._get_default_context_dir(), self.get_subcontext(log_name)) 144 os.makedirs(path, exist_ok=True) 145 return path 146 147 def _get_default_base_output_path(self): 148 """Gets the default base output path. 149 150 This will attempt to use logging path set up in the global 151 logger. 152 153 Returns: 154 The logging path. 155 156 Raises: 157 EnvironmentError: If logger has not been initialized. 158 """ 159 try: 160 return logging.log_path 161 except AttributeError as e: 162 raise EnvironmentError('The Mobly logger has not been set up and' 163 ' "base_output_path" has not been set.') from e 164 165 def _get_default_context_dir(self): 166 """Gets the default output directory for this context.""" 167 raise NotImplementedError() 168 169 170class RootContext(TestContext): 171 """A TestContext that represents a test run.""" 172 173 @property 174 def identifier(self): 175 return 'root' 176 177 def _get_default_context_dir(self): 178 """Gets the default output directory for this context. 179 180 Logs at the root level context are placed directly in the base level 181 directory, so no context-level path exists.""" 182 return '' 183 184 185class TestCaseContext(TestContext): 186 """A TestContext that represents a test case. 187 188 Attributes: 189 test_case: the name of the test case. 190 test_class: the name of the test class. 191 """ 192 193 def __init__(self, test_class, test_case): 194 """Initializes a TestCaseContext for the given test case. 195 196 Args: 197 test_class: test-class name. 198 test_case: test name. 199 """ 200 self.test_class = test_class 201 self.test_case = test_case 202 203 @property 204 def test_case_name(self): 205 return self.test_case 206 207 @property 208 def test_class_name(self): 209 return self.test_class 210 211 @property 212 def identifier(self): 213 return '%s.%s' % (self.test_class_name, self.test_case_name) 214 215 def _get_default_context_dir(self): 216 """Gets the default output directory for this context. 217 218 For TestCaseContexts, this will be the name of the test itself. 219 """ 220 return self.test_case_name 221 222 223# stack for keeping track of the current test context 224_contexts = [RootContext()] 225