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