1# Copyright Martin J. Bligh, Andy Whitcroft, 2007
2#
3# Define the server-side test class
4#
5# pylint: disable=missing-docstring
6
7import logging
8import os
9import tempfile
10
11from autotest_lib.client.common_lib import log
12from autotest_lib.client.common_lib import test as common_test
13from autotest_lib.client.common_lib import utils
14
15
16class test(common_test.base_test):
17    disable_sysinfo_install_cache = False
18    host_parameter = None
19
20
21_sysinfo_before_test_script = """\
22import pickle
23from autotest_lib.client.bin import test
24mytest = test.test(job, '', %r)
25job.sysinfo.log_before_each_test(mytest)
26sysinfo_pickle = os.path.join(mytest.outputdir, 'sysinfo.pickle')
27pickle.dump(job.sysinfo, open(sysinfo_pickle, 'w'))
28job.record('GOOD', '', 'sysinfo.before')
29"""
30
31_sysinfo_after_test_script = """\
32import pickle
33from autotest_lib.client.bin import test
34mytest = test.test(job, '', %r)
35# success is passed in so diffable_logdir can decide if or not to collect
36# full log content.
37mytest.success = %s
38sysinfo_pickle = os.path.join(mytest.outputdir, 'sysinfo.pickle')
39if os.path.exists(sysinfo_pickle):
40    job.sysinfo = pickle.load(open(sysinfo_pickle))
41    job.sysinfo.__init__(job.resultdir)
42job.sysinfo.log_after_each_test(mytest)
43job.record('GOOD', '', 'sysinfo.after')
44"""
45
46# this script is ran after _sysinfo_before_test_script and before
47# _sysinfo_after_test_script which means the pickle file exists
48# already and should be dumped with updated state for
49# _sysinfo_after_test_script to pick it up later
50_sysinfo_iteration_script = """\
51import pickle
52from autotest_lib.client.bin import test
53mytest = test.test(job, '', %r)
54sysinfo_pickle = os.path.join(mytest.outputdir, 'sysinfo.pickle')
55if os.path.exists(sysinfo_pickle):
56    job.sysinfo = pickle.load(open(sysinfo_pickle))
57    job.sysinfo.__init__(job.resultdir)
58job.sysinfo.%s(mytest, iteration=%d)
59pickle.dump(job.sysinfo, open(sysinfo_pickle, 'w'))
60job.record('GOOD', '', 'sysinfo.iteration.%s')
61"""
62
63
64def install_autotest_and_run(func):
65    def wrapper(self, mytest):
66        host, at, outputdir = self._install()
67        # TODO(kevcheng): remove when host client install is supported for
68        # ADBHost. crbug.com/543702
69        if not host.is_client_install_supported:
70            logging.debug('host client install not supported, skipping %s:',
71                          func.__name__)
72            return
73
74        try:
75            host.erase_dir_contents(outputdir)
76            func(self, mytest, host, at, outputdir)
77        finally:
78            # the test class can define this flag to make us remove the
79            # sysinfo install files and outputdir contents after each run
80            if mytest.disable_sysinfo_install_cache:
81                self.cleanup(host_close=False)
82
83    return wrapper
84
85
86class _sysinfo_logger(object):
87    AUTOTEST_PARENT_DIR = '/tmp/sysinfo'
88    OUTPUT_PARENT_DIR = '/tmp'
89
90    def __init__(self, job):
91        self.job = job
92        self.pickle = None
93
94        # for now support a single host
95        self.host = None
96        self.autotest = None
97        self.outputdir = None
98
99        if len(job.machines) != 1:
100            # disable logging on multi-machine tests
101            self.before_hook = self.after_hook = None
102            self.before_iteration_hook = self.after_iteration_hook = None
103
104
105    def _install(self):
106        if not self.host:
107            from autotest_lib.server import hosts, autotest
108            self.host = hosts.create_target_machine(
109                    self.job.machine_dict_list[0])
110            # TODO(kevcheng): remove when host client install is supported for
111            # ADBHost. crbug.com/543702
112            if not self.host.is_client_install_supported:
113                return self.host, None, None
114            try:
115                # Remove existing autoserv-* directories before creating more
116                self.host.delete_all_tmp_dirs(self.AUTOTEST_PARENT_DIR)
117                self.host.delete_all_tmp_dirs(self.OUTPUT_PARENT_DIR)
118
119                tmp_dir = self.host.get_tmp_dir(self.AUTOTEST_PARENT_DIR)
120                self.autotest = autotest.Autotest(self.host)
121                self.autotest.install(autodir=tmp_dir)
122                self.outputdir = self.host.get_tmp_dir(self.OUTPUT_PARENT_DIR)
123            except:
124                # if installation fails roll back the host
125                try:
126                    self.host.close()
127                except:
128                    logging.exception("Unable to close host %s",
129                                      self.host.hostname)
130                self.host = None
131                self.autotest = None
132                raise
133        else:
134            # TODO(kevcheng): remove when host client install is supported for
135            # ADBHost. crbug.com/543702
136            if not self.host.is_client_install_supported:
137                return self.host, None, None
138
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 % (outputdir, mytest.success),
233               results_dir=self.job.resultdir)
234
235        self._pull_sysinfo_keyval(host, outputdir, mytest)
236
237
238    def cleanup(self, host_close=True):
239        if self.host and self.autotest:
240            try:
241                try:
242                    self.autotest.uninstall()
243                finally:
244                    if host_close:
245                        self.host.close()
246                    else:
247                        self.host.erase_dir_contents(self.outputdir)
248
249            except Exception:
250                # ignoring exceptions here so that we don't hide the true
251                # reason of failure from runtest
252                logging.exception('Error cleaning up the sysinfo autotest/host '
253                                  'objects, ignoring it')
254
255
256def runtest(job, url, tag, args, dargs):
257    """Server-side runtest.
258
259    @param job: A server_job instance.
260    @param url: URL to the test.
261    @param tag: Test tag that will be appended to the test name.
262                See client/common_lib/test.py:runtest
263    @param args: args to pass to the test.
264    @param dargs: key-val based args to pass to the test.
265    """
266
267    disable_before_test_hook = dargs.pop('disable_before_test_sysinfo', False)
268    disable_after_test_hook = dargs.pop('disable_after_test_sysinfo', False)
269    disable_before_iteration_hook = dargs.pop(
270            'disable_before_iteration_sysinfo', False)
271    disable_after_iteration_hook = dargs.pop(
272            'disable_after_iteration_sysinfo', False)
273
274    disable_sysinfo = dargs.pop('disable_sysinfo', False)
275    if job.fast and not disable_sysinfo:
276        # Server job will be executed in fast mode, which means
277        # 1) if job succeeds, no hook will be executed.
278        # 2) if job failed, after_hook will be executed.
279        logger = _sysinfo_logger(job)
280        logging_args = [None, logger.after_hook, None,
281                        logger.after_iteration_hook]
282    elif not disable_sysinfo:
283        logger = _sysinfo_logger(job)
284        logging_args = [
285            logger.before_hook if not disable_before_test_hook else None,
286            logger.after_hook if not disable_after_test_hook else None,
287            (logger.before_iteration_hook
288                 if not disable_before_iteration_hook else None),
289            (logger.after_iteration_hook
290                 if not disable_after_iteration_hook else None),
291        ]
292    else:
293        logger = None
294        logging_args = [None, None, None, None]
295
296    # add in a hook that calls host.log_kernel if we can
297    def log_kernel_hook(mytest, existing_hook=logging_args[0]):
298        if mytest.host_parameter:
299            host = dargs[mytest.host_parameter]
300            if host:
301                host.log_kernel()
302        # chain this call with any existing hook
303        if existing_hook:
304            existing_hook(mytest)
305    logging_args[0] = log_kernel_hook
306
307    try:
308        common_test.runtest(job, url, tag, args, dargs, locals(), globals(),
309                            *logging_args)
310    finally:
311        if logger:
312            logger.cleanup()
313