| # Copyright 2022 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import os |
| import re |
| import logging |
| import json |
| |
| import common |
| from autotest_lib.server import autotest, test |
| from autotest_lib.client.common_lib import error |
| |
| from google.protobuf.text_format import Parse |
| |
| # run protoc --proto_path=./ pass_criteria.proto --python_out ./ |
| # with caution for version/upgrade compatibility |
| from . import pass_criteria_pb2 |
| |
| |
| class test_with_pass_criteria(test.test): |
| """ |
| test_with_pass_criteria extends the base test implementation to allow for |
| test result comparison between the performance keyvalues output from a |
| target test, and the input pass_criteria dictionary. |
| |
| It can be used to create a domain specific test wrapper such as |
| power_QualTestWrapper. |
| """ |
| |
| def initialize(self, test_to_wrap): |
| """ |
| initialize implements the initialize call in test.test, is called before |
| execution of the test |
| """ |
| self._test_prefix = [] |
| self._perf_dict = {} |
| self._attr_dict = {} |
| self._results_path = self.job._server_offload_dir_path() |
| self._wrapper_results = self._results_path + self.tagged_testname + '/' |
| logging.debug('...results going to %s', str(self._results_path)) |
| self._wrapped_test_results_keyval_path = (self._wrapper_results + |
| test_to_wrap + |
| '/results/keyval') |
| self._wrapped_test_keyval_path = self._wrapper_results + test_to_wrap + '/keyval' |
| self._wrapper_test_keyval_path = self._wrapper_results + 'keyval' |
| |
| def _check_wrapped_test_passed(self, test_name): |
| results_path = self._wrapper_results + test_name + "" |
| |
| def _load_proto_to_pass_criteria(self): |
| """ |
| _load_proto_to_pass_criteria optionally inputs a textproto file |
| or a ':' separated string which represents the pass criteria for |
| the test, and adds it to the pass criteria dictionary. |
| """ |
| for textproto in self._textproto_path.split(':'): |
| if not os.path.exists(textproto): |
| raise error.TestFail('provided textproto path ' + textproto + |
| ' does not exist') |
| |
| logging.info('loading criteria from textproto %s', textproto) |
| with open(textproto) as textpb: |
| textproto_criteria = Parse(textpb.read(), |
| pass_criteria_pb2.PassCriteria()) |
| for criteria in textproto_criteria.criteria: |
| lower_bound = criteria.lower_bound.bound if ( |
| criteria.HasField('lower_bound')) else None |
| upper_bound = criteria.upper_bound.bound if ( |
| criteria.HasField('upper_bound')) else None |
| if criteria.test_name != self._test_to_wrap and criteria.test_name != '': |
| logging.info('criteria %s does not apply', |
| criteria.name_regex) |
| continue |
| try: |
| self._pass_criteria[criteria.name_regex] = (lower_bound, |
| upper_bound) |
| logging.info('adding criteria %s', criteria.name_regex) |
| except: |
| raise error.TestFail('invalid pass criteria provided') |
| |
| def add_prefix_test(self, test='', prefix_args_dict=None): |
| """ |
| add_prefix_test takes a test_name and args_dict for that test. |
| This function allows a user creating a domain specific test wrapper |
| to add any prefix tests that must run prior to execution of the |
| target test. |
| |
| @param test: the name of the test to add as a prefix test operation |
| @param prefix_args_dict: the dictionary of args to pass to the test |
| when it is run |
| """ |
| if prefix_args_dict is None: |
| prefix_args_dict = {} |
| self._test_prefix.append((test, prefix_args_dict)) |
| |
| def _print_bounds_error(self, criteria, failed_criteria, value): |
| """ |
| _print_bounds_error will indicate missing pass criteria, printing the |
| error string with failing criteria and target range |
| |
| @param criteria: the name of the pass criteria to log a failure on |
| @param failed_criteria: the name of the criteria that regex matched |
| @param value: the actual value of the failing pass criteria |
| """ |
| logging.info('criteria %s: %s out of range %s', failed_criteria, |
| str(value), str(self._pass_criteria[criteria])) |
| |
| def _parse_wrapped_results_keyvals(self): |
| """ |
| _parse_wrapped_results_keyvals first loads all of the performance and |
| and attribute keyvals from the wrapped test, and then copies all of |
| the test_attribute keyvals from that wrapped test into the wrapper. |
| Without these keyvals being copied over, none of the metadata from |
| the client job are captured in the job summary. |
| |
| @raises: error.TestFail: If any of the respective keyvals are missing |
| """ |
| if os.path.exists(self._wrapped_test_results_keyval_path): |
| with open(self._wrapped_test_results_keyval_path |
| ) as results_keyval_file: |
| keyval_result = results_keyval_file.readline() |
| while keyval_result: |
| regmatch = re.search(r'(.*){(.*)}=(.*)', keyval_result) |
| if regmatch is None: |
| break |
| key = regmatch.group(1) |
| which_dict = regmatch.group(2) |
| value = regmatch.group(3) |
| if which_dict != 'perf': |
| continue |
| |
| self._perf_dict[key] = value |
| keyval_result = results_keyval_file.readline() |
| |
| with open(self._wrapped_test_keyval_path, |
| 'r') as wrapped_test_keyval_file, open( |
| self._wrapper_test_keyval_path, |
| 'a') as test_keyval_file: |
| for keyval in wrapped_test_keyval_file: |
| test_keyval_file.write(keyval) |
| |
| def _find_matching_keyvals(self): |
| for c in self._pass_criteria: |
| self._criteria_to_keyvals[c] = [] |
| for key in self._perf_dict.keys(): |
| if re.fullmatch(c, key): |
| logging.info('adding %s as matched key', key) |
| self._criteria_to_keyvals[c].append(key) |
| |
| def _verify_criteria(self): |
| failing_criteria = 0 |
| for criteria in self._pass_criteria: |
| logging.info('Checking %s now', criteria) |
| if type(criteria) is not str: |
| criteria = criteria.decode('utf-8') |
| range_spec = self._pass_criteria[criteria] |
| |
| for perf_val in self._criteria_to_keyvals[criteria]: |
| logging.info('Checking: %s against %s', str(criteria), |
| perf_val) |
| actual_value = self._perf_dict[perf_val] |
| logging.info('%s value is %s, spec is %s', perf_val, |
| float(actual_value), range_spec) |
| |
| # range_spec is passed into the dictionary as a tuple of upper and lower |
| lower_bound, upper_bound = range_spec |
| |
| if lower_bound is not None and not (float(actual_value) >= |
| float(lower_bound)): |
| failing_criteria = failing_criteria + 1 |
| self._print_bounds_error(criteria, perf_val, actual_value) |
| |
| if upper_bound is not None and not (float(actual_value) < |
| float(upper_bound)): |
| failing_criteria = failing_criteria + 1 |
| self._print_bounds_error(criteria, perf_val, actual_value) |
| |
| if failing_criteria > 0: |
| raise error.TestFail( |
| str(failing_criteria) + |
| ' criteria failed, see log for detail') |
| |
| def run_once(self, |
| host=None, |
| test_to_wrap=None, |
| pdash_note='', |
| wrap_args={}, |
| pass_criteria={}): |
| """ |
| run_once implements the run_once call in test.test, is called to begin |
| execution of the test |
| |
| @param host: host from control file with which to run the test |
| @param test_to_wrap: test name to execute in the wrapper |
| @param pdash_note: note to annotate results on the dashboard |
| @param wrap_args: args to pass to the wrapped test execution |
| @param pass_criteria: dictionary of criteria to compare results against |
| |
| @raises error.TestFail: on failure of the wrapped tests |
| """ |
| logging.debug('running test_with_pass_criteria run_once') |
| logging.debug('with test name %s', str(self.tagged_testname)) |
| self._wrap_args = wrap_args |
| self._test_to_wrap = test_to_wrap |
| if self._test_to_wrap == None: |
| raise error.TestFail('No test_to_wrap given') |
| |
| if isinstance(pass_criteria, dict): |
| self._pass_criteria = pass_criteria |
| else: |
| logging.info('loading from string dict %s', pass_criteria) |
| self._pass_criteria = json.loads(pass_criteria) |
| |
| self._textproto_path = self._pass_criteria.get('textproto_path', None) |
| if self._textproto_path is None: |
| logging.info('not using textproto criteria definitions') |
| else: |
| self._pass_criteria.pop('textproto_path') |
| self._load_proto_to_pass_criteria() |
| |
| logging.debug('wrapping test %s', self._test_to_wrap) |
| logging.debug('with wrap args %s', str(self._wrap_args)) |
| logging.debug('and pass criteria %s', str(self._pass_criteria)) |
| client_at = autotest.Autotest(host) |
| |
| for test, argv in self._test_prefix: |
| argv['pdash_note'] = pdash_note |
| try: |
| client_at.run_test(test, check_client_result=True, **argv) |
| except: |
| raise error.TestFail('Prefix test failed, see log for details') |
| |
| try: |
| client_at.run_test(self._test_to_wrap, |
| check_client_result=True, |
| **self._wrap_args) |
| except: |
| self.postprocess() |
| raise error.TestFail('Wrapped test failed, see log for details') |
| |
| def postprocess(self): |
| """ |
| postprocess is called after the completion of run_once by the test framework |
| |
| @raises error.TestFail: on any pass criteria failure |
| """ |
| self._parse_wrapped_results_keyvals() |
| if self._pass_criteria == {}: |
| return |
| self._criteria_to_keyvals = {} |
| self._find_matching_keyvals() |
| self._verify_criteria() |