blob: e467fa179238479e21897f502363b02c982ca9a2 [file] [log] [blame]
# 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 graphics_TraceReplayExtended(test.test):
"""Autotest server test for running repeated trace replays.
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):
"""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='',
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=120)
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