xref: /aosp_15_r20/external/autotest/server/cros/graphics/graphics_tracereplayextended.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
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