xref: /aosp_15_r20/external/autotest/server/test.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1# Lint as: python2, python3
2# Copyright (c) 2007 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6# Copyright Martin J. Bligh, Andy Whitcroft, 2007
7#
8# Define the server-side test class
9#
10# pylint: disable=missing-docstring
11
12import logging
13import os
14import tempfile
15
16from autotest_lib.client.common_lib import log
17from autotest_lib.client.common_lib import test as common_test
18from autotest_lib.client.common_lib import utils
19from autotest_lib.server import hosts, autotest
20
21
22class test(common_test.base_test):
23    disable_sysinfo_install_cache = False
24    host_parameter = None
25
26
27_sysinfo_before_test_script = """\
28import pickle
29from autotest_lib.client.bin import test
30mytest = test.test(job, '', %r)
31job.sysinfo.log_before_each_test(mytest)
32sysinfo_pickle = os.path.join(mytest.outputdir, 'sysinfo.pickle')
33pickle.dump(job.sysinfo, open(sysinfo_pickle, 'wb'))
34job.record('GOOD', '', 'sysinfo.before')
35"""
36
37_sysinfo_after_test_script = """\
38import pickle
39from autotest_lib.client.bin import test
40mytest = test.test(job, '', %r)
41# success is passed in so diffable_logdir can decide if or not to collect
42# full log content.
43mytest.success = %s
44mytest.collect_full_logs = %s
45sysinfo_pickle = os.path.join(mytest.outputdir, 'sysinfo.pickle')
46if os.path.exists(sysinfo_pickle):
47    try:
48        with open(sysinfo_pickle, 'r') as rf:
49            job.sysinfo = pickle.load(rf)
50    except UnicodeDecodeError:
51        with open(sysinfo_pickle, 'rb') as rf:
52            job.sysinfo = pickle.load(rf)
53    job.sysinfo.__init__(job.resultdir)
54job.sysinfo.log_after_each_test(mytest)
55job.record('GOOD', '', 'sysinfo.after')
56"""
57
58# this script is ran after _sysinfo_before_test_script and before
59# _sysinfo_after_test_script which means the pickle file exists
60# already and should be dumped with updated state for
61# _sysinfo_after_test_script to pick it up later
62_sysinfo_iteration_script = """\
63import pickle
64from autotest_lib.client.bin import test
65mytest = test.test(job, '', %r)
66sysinfo_pickle = os.path.join(mytest.outputdir, 'sysinfo.pickle')
67if os.path.exists(sysinfo_pickle):
68    try:
69        with open(sysinfo_pickle, 'r') as rf:
70            job.sysinfo = pickle.load(rf)
71    except UnicodeDecodeError:
72        with open(sysinfo_pickle, 'rb') as rf:
73            job.sysinfo = pickle.load(rf)
74    job.sysinfo.__init__(job.resultdir)
75job.sysinfo.%s(mytest, iteration=%d)
76pickle.dump(job.sysinfo, open(sysinfo_pickle, 'wb'))
77job.record('GOOD', '', 'sysinfo.iteration.%s')
78"""
79
80
81def install_autotest_and_run(func):
82    def wrapper(self, mytest):
83        host, at, outputdir = self._install()
84        try:
85            host.erase_dir_contents(outputdir)
86            func(self, mytest, host, at, outputdir)
87        finally:
88            # the test class can define this flag to make us remove the
89            # sysinfo install files and outputdir contents after each run
90            if mytest.disable_sysinfo_install_cache:
91                self.cleanup(host_close=False)
92
93    return wrapper
94
95
96class _sysinfo_logger(object):
97    AUTOTEST_PARENT_DIR = '/tmp/sysinfo'
98    OUTPUT_PARENT_DIR = '/tmp'
99
100    def __init__(self, job):
101        self.job = job
102        self.pickle = None
103
104        # for now support a single host
105        self.host = None
106        self.autotest = None
107        self.outputdir = None
108
109        if len(job.machines) != 1:
110            # disable logging on multi-machine tests
111            self.before_hook = self.after_hook = None
112            self.before_iteration_hook = self.after_iteration_hook = None
113
114
115    def _install(self):
116        if not self.host:
117            self.host = hosts.create_target_machine(
118                    self.job.machine_dict_list[0])
119            try:
120                # Remove existing autoserv-* directories before creating more
121                self.host.delete_all_tmp_dirs([self.AUTOTEST_PARENT_DIR,
122                                               self.OUTPUT_PARENT_DIR])
123
124                tmp_dir = self.host.get_tmp_dir(self.AUTOTEST_PARENT_DIR)
125                self.autotest = autotest.Autotest(self.host)
126                self.autotest.install(autodir=tmp_dir)
127                self.outputdir = self.host.get_tmp_dir(self.OUTPUT_PARENT_DIR)
128            except:
129                # if installation fails roll back the host
130                try:
131                    self.host.close()
132                except:
133                    logging.exception("Unable to close host %s",
134                                      self.host.hostname)
135                self.host = None
136                self.autotest = None
137                raise
138        else:
139            # if autotest client dir does not exist, reinstall (it may have
140            # been removed by the test code)
141            autodir = self.host.get_autodir()
142            if not autodir or not self.host.path_exists(autodir):
143                self.autotest.install(autodir=autodir)
144
145            # if the output dir does not exist, recreate it
146            if not self.host.path_exists(self.outputdir):
147                self.host.run('mkdir -p %s' % self.outputdir)
148
149        return self.host, self.autotest, self.outputdir
150
151
152    def _pull_pickle(self, host, outputdir):
153        """Pulls from the client the pickle file with the saved sysinfo state.
154        """
155        fd, path = tempfile.mkstemp(dir=self.job.tmpdir)
156        os.close(fd)
157        host.get_file(os.path.join(outputdir, "sysinfo.pickle"), path)
158        self.pickle = path
159
160
161    def _push_pickle(self, host, outputdir):
162        """Pushes the server saved sysinfo pickle file to the client.
163        """
164        if self.pickle:
165            host.send_file(self.pickle,
166                           os.path.join(outputdir, "sysinfo.pickle"))
167            os.remove(self.pickle)
168            self.pickle = None
169
170
171    def _pull_sysinfo_keyval(self, host, outputdir, mytest):
172        """Pulls sysinfo and keyval data from the client.
173        """
174        # pull the sysinfo data back on to the server
175        host.get_file(os.path.join(outputdir, "sysinfo"), mytest.outputdir)
176
177        # pull the keyval data back into the local one
178        fd, path = tempfile.mkstemp(dir=self.job.tmpdir)
179        os.close(fd)
180        host.get_file(os.path.join(outputdir, "keyval"), path)
181        keyval = utils.read_keyval(path)
182        os.remove(path)
183        mytest.write_test_keyval(keyval)
184
185
186    @log.log_and_ignore_errors("pre-test server sysinfo error:")
187    @install_autotest_and_run
188    def before_hook(self, mytest, host, at, outputdir):
189        # run the pre-test sysinfo script
190        at.run(_sysinfo_before_test_script % outputdir,
191               results_dir=self.job.resultdir)
192
193        self._pull_pickle(host, outputdir)
194
195
196    @log.log_and_ignore_errors("pre-test iteration server sysinfo error:")
197    @install_autotest_and_run
198    def before_iteration_hook(self, mytest, host, at, outputdir):
199        # this function is called after before_hook() se we have sysinfo state
200        # to push to the server
201        self._push_pickle(host, outputdir);
202        # run the pre-test iteration sysinfo script
203        at.run(_sysinfo_iteration_script %
204               (outputdir, 'log_before_each_iteration', mytest.iteration,
205                'before'),
206               results_dir=self.job.resultdir)
207
208        # get the new sysinfo state from the client
209        self._pull_pickle(host, outputdir)
210
211
212    @log.log_and_ignore_errors("post-test iteration server sysinfo error:")
213    @install_autotest_and_run
214    def after_iteration_hook(self, mytest, host, at, outputdir):
215        # push latest sysinfo state to the client
216        self._push_pickle(host, outputdir);
217        # run the post-test iteration sysinfo script
218        at.run(_sysinfo_iteration_script %
219               (outputdir, 'log_after_each_iteration', mytest.iteration,
220                'after'),
221               results_dir=self.job.resultdir)
222
223        # get the new sysinfo state from the client
224        self._pull_pickle(host, outputdir)
225
226
227    @log.log_and_ignore_errors("post-test server sysinfo error:")
228    @install_autotest_and_run
229    def after_hook(self, mytest, host, at, outputdir):
230        self._push_pickle(host, outputdir);
231        # run the post-test sysinfo script
232        at.run(_sysinfo_after_test_script %
233               (outputdir, mytest.success, mytest.force_full_log_collection),
234               results_dir=self.job.resultdir)
235
236        self._pull_sysinfo_keyval(host, outputdir, mytest)
237
238
239    @log.log_and_ignore_errors("post-test server crossystem error:")
240    def after_hook_crossystem_fast(self, mytest):
241        """Collects crossystem log file in fast mode
242
243        This is used in place of after_hook in fast mode. This function will
244        grab output of crossystem but not process other sysinfo logs.
245        """
246        if not self.host:
247            self.host = hosts.create_target_machine(
248                    self.job.machine_dict_list[0])
249        output_path = '%s/sysinfo' % mytest.outputdir
250        utils.run('mkdir -p %s' % output_path)
251        crossystem_output = self.host.run_output('crossystem')
252        with open('%s/crossystem' % output_path, 'w') as f:
253            f.write(crossystem_output)
254
255    def cleanup(self, host_close=True):
256        if self.host and self.autotest:
257            try:
258                try:
259                    self.autotest.uninstall()
260                finally:
261                    if host_close:
262                        self.host.close()
263                    else:
264                        self.host.erase_dir_contents(self.outputdir)
265
266            except Exception:
267                # ignoring exceptions here so that we don't hide the true
268                # reason of failure from runtest
269                logging.exception('Error cleaning up the sysinfo autotest/host '
270                                  'objects, ignoring it')
271
272
273def runtest(job, url, tag, args, dargs):
274    """Server-side runtest.
275
276    @param job: A server_job instance.
277    @param url: URL to the test.
278    @param tag: Test tag that will be appended to the test name.
279                See client/common_lib/test.py:runtest
280    @param args: args to pass to the test.
281    @param dargs: key-val based args to pass to the test.
282    """
283
284    disable_before_test_hook = dargs.pop('disable_before_test_sysinfo', False)
285    disable_after_test_hook = dargs.pop('disable_after_test_sysinfo', False)
286    disable_before_iteration_hook = dargs.pop(
287            'disable_before_iteration_sysinfo', False)
288    disable_after_iteration_hook = dargs.pop(
289            'disable_after_iteration_sysinfo', False)
290
291    disable_sysinfo = dargs.pop('disable_sysinfo', False)
292    logger = _sysinfo_logger(job)
293    if job.fast and not disable_sysinfo:
294        # Server job will be executed in fast mode, which means
295        # 1) if job succeeds, no hook will be executed.
296        # 2) if job failed, after_hook will be executed.
297        logging_args = [None, logger.after_hook, None,
298                        logger.after_iteration_hook]
299    elif not disable_sysinfo:
300        logging_args = [
301            logger.before_hook if not disable_before_test_hook else None,
302            logger.after_hook if not disable_after_test_hook else None,
303            (logger.before_iteration_hook
304                 if not disable_before_iteration_hook else None),
305            (logger.after_iteration_hook
306                 if not disable_after_iteration_hook else None),
307        ]
308    else:
309        logging_args = [None, logger.after_hook_crossystem_fast, None, None]
310
311    # add in a hook that calls host.log_kernel if we can
312    def log_kernel_hook(mytest, existing_hook=logging_args[0]):
313        if mytest.host_parameter:
314            host = dargs[mytest.host_parameter]
315            if host:
316                host.log_kernel()
317        # chain this call with any existing hook
318        if existing_hook:
319            existing_hook(mytest)
320    logging_args[0] = log_kernel_hook
321
322    try:
323        common_test.runtest(job, url, tag, args, dargs, locals(), globals(),
324                            *logging_args)
325    finally:
326        if logger:
327            logger.cleanup()
328