# Copyright 2020 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. """Implementation of the graphics_TraceReplayExtended server test.""" from enum import Enum import logging import os import threading import time from autotest_lib.client.common_lib import error from autotest_lib.server import test from autotest_lib.server.cros.graphics import graphics_power from autotest_lib.server.site_tests.tast import tast class TastTestResult(): """Stores the test result for a single Tast subtest""" class TestStatus(Enum): """Encodes all actionable Tast subtest completion statuses""" Passed = 1 Skipped = 2 Failed = 3 def __init__(self, name, status, errors): self.name = name # type: str self.status = status # type: self.TestStatus self.errors = errors # type: List[json-string] class TastManagerThread(threading.Thread): """Thread for running a local tast test from an autotest server test.""" def __init__(self, host, tast_instance, client_test, max_duration_minutes, build_bundle, varslist=None, command_args=None): """Initializes the thread. Args: host: An autotest host instance. tast_instance: An instance of the tast.tast() class. client_test: String identifying which tast test to run. max_duration_minutes: Float defining the maximum running time of the managed sub-test. build_bundle: String defining which tast test bundle to build and query for the client_test. varslist: list of strings that define dynamic variables made available to tast tests at runtime via `tast run -var=name=value ...`. Each string should be formatted as 'name=value'. command_args: list of strings that are passed as args to the `tast run` command. """ super(TastManagerThread, self).__init__(name=__name__) self.tast = tast_instance self.tast.initialize( host=host, test_exprs=[client_test], ignore_test_failures=True, max_run_sec=max_duration_minutes * 60, command_args=command_args if command_args else [], build_bundle=build_bundle, varslist=varslist) def run(self): logging.info('Started thread: %s', self.__class__.__name__) self.tast.run_once() def get_subtest_results(self): """Returns the status for the tast subtest managed by this class. Parses the Tast client tests' json-formatted result payloads to determine the status and associated messages for each. self.tast._test_results is populated with JSON objects for each test during self.tast.run_once(). The JSON spec is detailed at src/platform/tast/src/chromiumos/tast/cmd/tast/internal/run/results.go. """ subtest_results = [] for res in self.tast._test_results: name = res.get('name') skip_reason = res.get('skipReason') errors = res.get('errors') if skip_reason: logging.info('Tast subtest "%s" was skipped with reason: %s', name, skip_reason) status = TastTestResult.TestStatus.Skipped elif errors: logging.info('Tast subtest "%s" failed with errors: %s', name, str([err.get('reason') for err in errors])) status = TastTestResult.TestStatus.Failed else: logging.info('Tast subtest "%s" succeeded.', name) status = TastTestResult.TestStatus.Passed subtest_results.append(TastTestResult(name, status, errors)) return subtest_results class GraphicsTraceReplayExtendedBase(test.test): """Base Autotest server test for running repeated trace replays in a VM. This test simultaneously initiates system performance logging and extended trace replay processes on a target host, and parses their test results for combined analysis and reporting. """ version = 1 @staticmethod def _initialize_dir_on_host(host, directory): """Initialize a directory to a consistent (empty) state on the host. Args: host: An autotest host instance. directory: String defining the location of the directory to initialize. Raises: TestFail: If the directory cannot be initialized. """ try: host.run('rm -r %(0)s 2>/dev/null || true; ! test -d %(0)s' % {'0': directory}) host.run('mkdir -p %s' % directory) except (error.AutotestHostRunCmdError, error.AutoservRunError) as err: logging.exception(err) raise error.TestFail( 'Failed to initialize directory "%s" on the test host' % directory) @staticmethod def _cleanup_dir_on_host(host, directory): """Ensure that a directory and its contents are deleted on the host. Args: host: An autotest host instance. directory: String defining the location of the directory to delete. Raises: TestFail: If the directory remains on the host. """ try: host.run('rm -r %(0)s || true; ! test -d %(0)s' % {'0': directory}) except (error.AutotestHostRunCmdError, error.AutoservRunError) as err: logging.exception(err) raise error.TestFail( 'Failed to cleanup directory "%s" on the test host' % directory) def run_once(self, host, client_tast_test, max_duration_minutes, tast_build_bundle='cros', tast_varslist=None, tast_command_args=None, pdash_note=None): """Runs the test. Args: host: An autotest host instance. client_tast_test: String defining which tast test to run. max_duration_minutes: Float defining the maximum running time of the managed sub-test. tast_build_bundle: String defining which tast test bundle to build and query for the client_test. tast_varslist: list of strings that define dynamic variables made available to tast tests at runtime via `tast run -var=name=value ...`. Each string should be formatted as 'name=value'. tast_command_args: list of strings that are passed as args to the `tast run` command. """ # Construct a suffix tag indicating which managing test is using logged # data from the graphics_Power subtest. trace_name = client_tast_test.split('.')[-1] # workaround for running test locally since crrev/c/2374267 and # crrev/i/2374267 if not tast_command_args: tast_command_args = [] tast_command_args.extend([ 'extraallowedbuckets=termina-component-testing,cros-containers-staging' ]) # Define paths of signal files for basic RPC/IPC between sub-tests. temp_io_root = '/tmp/%s/' % self.__class__.__name__ result_dir = os.path.join(temp_io_root, 'results') signal_running_file = os.path.join(temp_io_root, 'signal_running') signal_checkpoint_file = os.path.join(temp_io_root, 'signal_checkpoint') # This test is responsible for creating/deleting root and resultdir. logging.debug('Creating temporary IPC/RPC dir: %s', temp_io_root) self._initialize_dir_on_host(host, temp_io_root) self._initialize_dir_on_host(host, result_dir) # Start background system performance monitoring process on the test # target (via an autotest client 'power_Test'). logging.debug('Connecting to autotest client on host') graphics_power_thread = graphics_power.GraphicsPowerThread( host=host, max_duration_minutes=max_duration_minutes, test_tag='Trace' + '.' + trace_name, pdash_note=pdash_note, result_dir=result_dir, signal_running_file=signal_running_file, signal_checkpoint_file=signal_checkpoint_file) graphics_power_thread.start() logging.info('Waiting for graphics_Power subtest to initialize...') try: graphics_power_thread.wait_until_running(timeout=10 * 60) except Exception as err: logging.exception(err) raise error.TestFail( 'An error occured during graphics_Power subtest initialization') logging.info('The graphics_Power subtest was properly initialized') # Start repeated trace replay process on the test target (via a tast # local test). logging.info('Running Tast test: %s', client_tast_test) tast_outputdir = os.path.join(self.outputdir, 'tast') if not os.path.exists(tast_outputdir): logging.debug('Creating tast outputdir: %s', tast_outputdir) os.makedirs(tast_outputdir) if not tast_varslist: tast_varslist = [] tast_varslist.extend([ 'PowerTest.resultDir=' + result_dir, 'PowerTest.signalRunningFile=' + signal_running_file, 'PowerTest.signalCheckpointFile=' + signal_checkpoint_file, ]) tast_instance = tast.tast( job=self.job, bindir=self.bindir, outputdir=tast_outputdir) tast_manager_thread = TastManagerThread( host, tast_instance, client_tast_test, max_duration_minutes, tast_build_bundle, varslist=tast_varslist, command_args=tast_command_args) tast_manager_thread.start() # Block until both subtests finish. threads = [graphics_power_thread, tast_manager_thread] stop_attempts = 0 while threads: # TODO(ryanneph): Move stop signal emission to tast test instance. if (not tast_manager_thread.is_alive() and graphics_power_thread.is_alive() and stop_attempts < 1): logging.info('Attempting to stop graphics_Power thread') graphics_power_thread.stop(timeout=0) stop_attempts += 1 # Raise test failure if graphics_Power thread ends before tast test. if (not graphics_power_thread.is_alive() and tast_manager_thread.is_alive()): raise error.TestFail( 'The graphics_Power subtest ended too soon.') for thread in list(threads): if not thread.is_alive(): logging.info('Thread "%s" has ended', thread.__class__.__name__) threads.remove(thread) time.sleep(1) # Aggregate subtest results and report overall test result subtest_results = tast_manager_thread.get_subtest_results() num_failed_subtests = 0 for res in subtest_results: num_failed_subtests += int( res.status == TastTestResult.TestStatus.Failed) if num_failed_subtests: raise error.TestFail('%d of %d Tast subtests have failed.' % (num_failed_subtests, len(subtest_results))) elif all([res.status == TastTestResult.TestStatus.Skipped for res in subtest_results]): raise error.TestNAError('All %d Tast subtests have been skipped' % len(subtest_results)) client_result_dir = os.path.join(self.outputdir, 'client_results') logging.info('Saving client results to %s', client_result_dir) host.get_file(result_dir, client_result_dir) # Ensure the host filesystem is clean for the next test. self._cleanup_dir_on_host(host, result_dir) self._cleanup_dir_on_host(host, temp_io_root) # TODO(ryanneph): Implement results parsing/analysis/reporting