| #!/usr/bin/env python3 |
| # |
| # Copyright 2018 - The Android Open Source Project |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| import enum |
| import logging |
| import os |
| |
| |
| class ContextLevel(enum.IntEnum): |
| ROOT = 0 |
| TESTCLASS = 1 |
| TESTCASE = 2 |
| |
| |
| def get_current_context(depth=None): |
| """Get the current test context at the specified depth. |
| Pulls the most recently created context, with a level at or below the given |
| depth, from the _contexts stack. |
| |
| Args: |
| depth: The desired context level. For example, the TESTCLASS level would |
| yield the current test class context, even if the test is currently |
| within a test case. |
| |
| Returns: An instance of TestContext. |
| """ |
| if depth is None: |
| return _contexts[-1] |
| return _contexts[min(depth, len(_contexts) - 1)] |
| |
| |
| def append_test_context(test_class_name, test_name): |
| """Add test-specific context to the _contexts stack. |
| A test should should call append_test_context() at test start and |
| pop_test_context() upon test end. |
| |
| Args: |
| test_class_name: name of the test class. |
| test_name: name of the test. |
| """ |
| if _contexts: |
| _contexts.append(TestCaseContext(test_class_name, test_name)) |
| |
| |
| def pop_test_context(): |
| """Remove the latest test-specific context from the _contexts stack. |
| A test should should call append_test_context() at test start and |
| pop_test_context() upon test end. |
| """ |
| if _contexts: |
| _contexts.pop() |
| |
| |
| class TestContext(object): |
| """An object representing the current context in which a test is executing. |
| |
| The context encodes the current state of the test runner with respect to a |
| particular scenario in which code is being executed. For example, if some |
| code is being executed as part of a test case, then the context should |
| encode information about that test case such as its name or enclosing |
| class. |
| |
| The subcontext specifies a relative path in which certain outputs, |
| e.g. logcat, should be kept for the given context. |
| |
| The full output path is given by |
| <base_output_path>/<context_dir>/<subcontext>. |
| |
| Attributes: |
| _base_output_paths: a dictionary mapping a logger's name to its base |
| output path |
| _subcontexts: a dictionary mapping a logger's name to its |
| subcontext-level output directory |
| """ |
| |
| _base_output_paths = {} |
| _subcontexts = {} |
| |
| def get_base_output_path(self, log_name=None): |
| """Gets the base output path for this logger. |
| |
| The base output path is interpreted as the reporting root for the |
| entire test runner. |
| |
| If a path has been added with add_base_output_path, it is returned. |
| Otherwise, a default is determined by _get_default_base_output_path(). |
| |
| Args: |
| log_name: The name of the logger. |
| |
| Returns: |
| The output path. |
| """ |
| if log_name in self._base_output_paths: |
| return self._base_output_paths[log_name] |
| return self._get_default_base_output_path() |
| |
| def get_subcontext(self, log_name=None): |
| """Gets the subcontext for this logger. |
| |
| The subcontext is interpreted as the directory, relative to the |
| context-level path, where all outputs of the given logger are stored. |
| |
| If a path has been added with add_subcontext, it is returned. |
| Otherwise, the empty string is returned. |
| |
| Args: |
| log_name: The name of the logger. |
| |
| Returns: |
| The output path. |
| """ |
| return self._subcontexts.get(log_name, '') |
| |
| def get_full_output_path(self, log_name=None): |
| """Gets the full output path for this context. |
| |
| The full path represents the absolute path to the output directory, |
| as given by <base_output_path>/<context_dir>/<subcontext> |
| |
| Args: |
| log_name: The name of the logger. Used to specify the base output |
| path and the subcontext. |
| |
| Returns: |
| The output path. |
| """ |
| |
| path = os.path.join( |
| self.get_base_output_path(log_name), self._get_default_context_dir(), self.get_subcontext(log_name)) |
| os.makedirs(path, exist_ok=True) |
| return path |
| |
| def _get_default_base_output_path(self): |
| """Gets the default base output path. |
| |
| This will attempt to use logging path set up in the global |
| logger. |
| |
| Returns: |
| The logging path. |
| |
| Raises: |
| EnvironmentError: If logger has not been initialized. |
| """ |
| try: |
| return logging.log_path |
| except AttributeError as e: |
| raise EnvironmentError('The Mobly logger has not been set up and' |
| ' "base_output_path" has not been set.') from e |
| |
| def _get_default_context_dir(self): |
| """Gets the default output directory for this context.""" |
| raise NotImplementedError() |
| |
| |
| class RootContext(TestContext): |
| """A TestContext that represents a test run.""" |
| |
| @property |
| def identifier(self): |
| return 'root' |
| |
| def _get_default_context_dir(self): |
| """Gets the default output directory for this context. |
| |
| Logs at the root level context are placed directly in the base level |
| directory, so no context-level path exists.""" |
| return '' |
| |
| |
| class TestCaseContext(TestContext): |
| """A TestContext that represents a test case. |
| |
| Attributes: |
| test_case: the name of the test case. |
| test_class: the name of the test class. |
| """ |
| |
| def __init__(self, test_class, test_case): |
| """Initializes a TestCaseContext for the given test case. |
| |
| Args: |
| test_class: test-class name. |
| test_case: test name. |
| """ |
| self.test_class = test_class |
| self.test_case = test_case |
| |
| @property |
| def test_case_name(self): |
| return self.test_case |
| |
| @property |
| def test_class_name(self): |
| return self.test_class |
| |
| @property |
| def identifier(self): |
| return '%s.%s' % (self.test_class_name, self.test_case_name) |
| |
| def _get_default_context_dir(self): |
| """Gets the default output directory for this context. |
| |
| For TestCaseContexts, this will be the name of the test itself. |
| """ |
| return self.test_case_name |
| |
| |
| # stack for keeping track of the current test context |
| _contexts = [RootContext()] |