1# Copyright 2020 The Chromium OS Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4"""Implementation of the graphics_TraceReplayExtended server test.""" 5 6from enum import Enum 7import logging 8import os 9import threading 10import time 11 12from autotest_lib.client.common_lib import error 13from autotest_lib.server import test 14from autotest_lib.server.cros.graphics import graphics_power 15from autotest_lib.server.site_tests.tast import tast 16 17 18class TastTestResult(): 19 """Stores the test result for a single Tast subtest""" 20 21 class TestStatus(Enum): 22 """Encodes all actionable Tast subtest completion statuses""" 23 Passed = 1 24 Skipped = 2 25 Failed = 3 26 27 def __init__(self, name, status, errors): 28 self.name = name # type: str 29 self.status = status # type: self.TestStatus 30 self.errors = errors # type: List[json-string] 31 32 33class TastManagerThread(threading.Thread): 34 """Thread for running a local tast test from an autotest server test.""" 35 36 def __init__(self, 37 host, 38 tast_instance, 39 client_test, 40 max_duration_minutes, 41 build_bundle, 42 varslist=None, 43 command_args=None): 44 """Initializes the thread. 45 46 Args: 47 host: An autotest host instance. 48 tast_instance: An instance of the tast.tast() class. 49 client_test: String identifying which tast test to run. 50 max_duration_minutes: Float defining the maximum running time of the 51 managed sub-test. 52 build_bundle: String defining which tast test bundle to build and 53 query for the client_test. 54 varslist: list of strings that define dynamic variables made 55 available to tast tests at runtime via `tast run -var=name=value 56 ...`. Each string should be formatted as 'name=value'. 57 command_args: list of strings that are passed as args to the `tast 58 run` command. 59 """ 60 super(TastManagerThread, self).__init__(name=__name__) 61 self.tast = tast_instance 62 self.tast.initialize( 63 host=host, 64 test_exprs=[client_test], 65 ignore_test_failures=True, 66 max_run_sec=max_duration_minutes * 60, 67 command_args=command_args if command_args else [], 68 build_bundle=build_bundle, 69 varslist=varslist) 70 71 def run(self): 72 logging.info('Started thread: %s', self.__class__.__name__) 73 self.tast.run_once() 74 75 def get_subtest_results(self): 76 """Returns the status for the tast subtest managed by this class. 77 78 Parses the Tast client tests' json-formatted result payloads to 79 determine the status and associated messages for each. 80 81 self.tast._test_results is populated with JSON objects for each test 82 during self.tast.run_once(). The JSON spec is detailed at 83 src/platform/tast/src/chromiumos/tast/cmd/tast/internal/run/results.go. 84 """ 85 subtest_results = [] 86 for res in self.tast._test_results: 87 name = res.get('name') 88 skip_reason = res.get('skipReason') 89 errors = res.get('errors') 90 if skip_reason: 91 logging.info('Tast subtest "%s" was skipped with reason: %s', 92 name, skip_reason) 93 status = TastTestResult.TestStatus.Skipped 94 elif errors: 95 logging.info('Tast subtest "%s" failed with errors: %s', name, 96 str([err.get('reason') for err in errors])) 97 status = TastTestResult.TestStatus.Failed 98 else: 99 logging.info('Tast subtest "%s" succeeded.', name) 100 status = TastTestResult.TestStatus.Passed 101 subtest_results.append(TastTestResult(name, status, errors)) 102 return subtest_results 103 104 105class GraphicsTraceReplayExtendedBase(test.test): 106 """Base Autotest server test for running repeated trace replays in a VM. 107 108 This test simultaneously initiates system performance logging and extended 109 trace replay processes on a target host, and parses their test results for 110 combined analysis and reporting. 111 """ 112 version = 1 113 114 @staticmethod 115 def _initialize_dir_on_host(host, directory): 116 """Initialize a directory to a consistent (empty) state on the host. 117 118 Args: 119 host: An autotest host instance. 120 directory: String defining the location of the directory to 121 initialize. 122 123 Raises: 124 TestFail: If the directory cannot be initialized. 125 """ 126 try: 127 host.run('rm -r %(0)s 2>/dev/null || true; ! test -d %(0)s' % 128 {'0': directory}) 129 host.run('mkdir -p %s' % directory) 130 except (error.AutotestHostRunCmdError, error.AutoservRunError) as err: 131 logging.exception(err) 132 raise error.TestFail( 133 'Failed to initialize directory "%s" on the test host' % 134 directory) 135 136 @staticmethod 137 def _cleanup_dir_on_host(host, directory): 138 """Ensure that a directory and its contents are deleted on the host. 139 140 Args: 141 host: An autotest host instance. 142 directory: String defining the location of the directory to delete. 143 144 Raises: 145 TestFail: If the directory remains on the host. 146 """ 147 try: 148 host.run('rm -r %(0)s || true; ! test -d %(0)s' % {'0': directory}) 149 except (error.AutotestHostRunCmdError, error.AutoservRunError) as err: 150 logging.exception(err) 151 raise error.TestFail( 152 'Failed to cleanup directory "%s" on the test host' % directory) 153 154 def run_once(self, 155 host, 156 client_tast_test, 157 max_duration_minutes, 158 tast_build_bundle='cros', 159 tast_varslist=None, 160 tast_command_args=None, 161 pdash_note=None): 162 """Runs the test. 163 164 Args: 165 host: An autotest host instance. 166 client_tast_test: String defining which tast test to run. 167 max_duration_minutes: Float defining the maximum running time of the 168 managed sub-test. 169 tast_build_bundle: String defining which tast test bundle to build 170 and query for the client_test. 171 tast_varslist: list of strings that define dynamic variables made 172 available to tast tests at runtime via `tast run -var=name=value 173 ...`. Each string should be formatted as 'name=value'. 174 tast_command_args: list of strings that are passed as args to the 175 `tast run` command. 176 """ 177 # Construct a suffix tag indicating which managing test is using logged 178 # data from the graphics_Power subtest. 179 trace_name = client_tast_test.split('.')[-1] 180 181 # workaround for running test locally since crrev/c/2374267 and 182 # crrev/i/2374267 183 if not tast_command_args: 184 tast_command_args = [] 185 tast_command_args.extend([ 186 'extraallowedbuckets=termina-component-testing,cros-containers-staging' 187 ]) 188 189 # Define paths of signal files for basic RPC/IPC between sub-tests. 190 temp_io_root = '/tmp/%s/' % self.__class__.__name__ 191 result_dir = os.path.join(temp_io_root, 'results') 192 signal_running_file = os.path.join(temp_io_root, 'signal_running') 193 signal_checkpoint_file = os.path.join(temp_io_root, 'signal_checkpoint') 194 195 # This test is responsible for creating/deleting root and resultdir. 196 logging.debug('Creating temporary IPC/RPC dir: %s', temp_io_root) 197 self._initialize_dir_on_host(host, temp_io_root) 198 self._initialize_dir_on_host(host, result_dir) 199 200 # Start background system performance monitoring process on the test 201 # target (via an autotest client 'power_Test'). 202 logging.debug('Connecting to autotest client on host') 203 graphics_power_thread = graphics_power.GraphicsPowerThread( 204 host=host, 205 max_duration_minutes=max_duration_minutes, 206 test_tag='Trace' + '.' + trace_name, 207 pdash_note=pdash_note, 208 result_dir=result_dir, 209 signal_running_file=signal_running_file, 210 signal_checkpoint_file=signal_checkpoint_file) 211 graphics_power_thread.start() 212 213 logging.info('Waiting for graphics_Power subtest to initialize...') 214 try: 215 graphics_power_thread.wait_until_running(timeout=10 * 60) 216 except Exception as err: 217 logging.exception(err) 218 raise error.TestFail( 219 'An error occured during graphics_Power subtest initialization') 220 logging.info('The graphics_Power subtest was properly initialized') 221 222 # Start repeated trace replay process on the test target (via a tast 223 # local test). 224 logging.info('Running Tast test: %s', client_tast_test) 225 tast_outputdir = os.path.join(self.outputdir, 'tast') 226 if not os.path.exists(tast_outputdir): 227 logging.debug('Creating tast outputdir: %s', tast_outputdir) 228 os.makedirs(tast_outputdir) 229 230 if not tast_varslist: 231 tast_varslist = [] 232 tast_varslist.extend([ 233 'PowerTest.resultDir=' + result_dir, 234 'PowerTest.signalRunningFile=' + signal_running_file, 235 'PowerTest.signalCheckpointFile=' + signal_checkpoint_file, 236 ]) 237 238 tast_instance = tast.tast( 239 job=self.job, bindir=self.bindir, outputdir=tast_outputdir) 240 tast_manager_thread = TastManagerThread( 241 host, 242 tast_instance, 243 client_tast_test, 244 max_duration_minutes, 245 tast_build_bundle, 246 varslist=tast_varslist, 247 command_args=tast_command_args) 248 tast_manager_thread.start() 249 250 # Block until both subtests finish. 251 threads = [graphics_power_thread, tast_manager_thread] 252 stop_attempts = 0 253 while threads: 254 # TODO(ryanneph): Move stop signal emission to tast test instance. 255 if (not tast_manager_thread.is_alive() and 256 graphics_power_thread.is_alive() and stop_attempts < 1): 257 logging.info('Attempting to stop graphics_Power thread') 258 graphics_power_thread.stop(timeout=0) 259 stop_attempts += 1 260 261 # Raise test failure if graphics_Power thread ends before tast test. 262 if (not graphics_power_thread.is_alive() and 263 tast_manager_thread.is_alive()): 264 raise error.TestFail( 265 'The graphics_Power subtest ended too soon.') 266 267 for thread in list(threads): 268 if not thread.is_alive(): 269 logging.info('Thread "%s" has ended', 270 thread.__class__.__name__) 271 threads.remove(thread) 272 time.sleep(1) 273 274 # Aggregate subtest results and report overall test result 275 subtest_results = tast_manager_thread.get_subtest_results() 276 num_failed_subtests = 0 277 for res in subtest_results: 278 num_failed_subtests += int( 279 res.status == TastTestResult.TestStatus.Failed) 280 if num_failed_subtests: 281 raise error.TestFail('%d of %d Tast subtests have failed.' % 282 (num_failed_subtests, len(subtest_results))) 283 elif all([res.status == TastTestResult.TestStatus.Skipped 284 for res in subtest_results]): 285 raise error.TestNAError('All %d Tast subtests have been skipped' % 286 len(subtest_results)) 287 288 client_result_dir = os.path.join(self.outputdir, 'client_results') 289 logging.info('Saving client results to %s', client_result_dir) 290 host.get_file(result_dir, client_result_dir) 291 292 # Ensure the host filesystem is clean for the next test. 293 self._cleanup_dir_on_host(host, result_dir) 294 self._cleanup_dir_on_host(host, temp_io_root) 295 296 # TODO(ryanneph): Implement results parsing/analysis/reporting 297