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