xref: /aosp_15_r20/external/autotest/client/common_lib/base_job.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1*9c5db199SXin Li# Lint as: python2, python3
2*9c5db199SXin Li# pylint: disable=missing-docstring
3*9c5db199SXin Li
4*9c5db199SXin Lifrom __future__ import absolute_import
5*9c5db199SXin Lifrom __future__ import division
6*9c5db199SXin Lifrom __future__ import print_function
7*9c5db199SXin Liimport copy
8*9c5db199SXin Liimport errno
9*9c5db199SXin Liimport fcntl
10*9c5db199SXin Liimport logging
11*9c5db199SXin Liimport os
12*9c5db199SXin Liimport re
13*9c5db199SXin Liimport six
14*9c5db199SXin Liimport six.moves.cPickle as pickle
15*9c5db199SXin Liimport tempfile
16*9c5db199SXin Liimport time
17*9c5db199SXin Liimport traceback
18*9c5db199SXin Liimport weakref
19*9c5db199SXin Lifrom autotest_lib.client.common_lib import autotemp, error, log
20*9c5db199SXin Li
21*9c5db199SXin Li
22*9c5db199SXin Liclass job_directory(object):
23*9c5db199SXin Li    """Represents a job.*dir directory."""
24*9c5db199SXin Li
25*9c5db199SXin Li
26*9c5db199SXin Li    class JobDirectoryException(error.AutotestError):
27*9c5db199SXin Li        """Generic job_directory exception superclass."""
28*9c5db199SXin Li
29*9c5db199SXin Li
30*9c5db199SXin Li    class MissingDirectoryException(JobDirectoryException):
31*9c5db199SXin Li        """Raised when a directory required by the job does not exist."""
32*9c5db199SXin Li        def __init__(self, path):
33*9c5db199SXin Li            Exception.__init__(self, 'Directory %s does not exist' % path)
34*9c5db199SXin Li
35*9c5db199SXin Li
36*9c5db199SXin Li    class UncreatableDirectoryException(JobDirectoryException):
37*9c5db199SXin Li        """Raised when a directory required by the job is missing and cannot
38*9c5db199SXin Li        be created."""
39*9c5db199SXin Li        def __init__(self, path, error):
40*9c5db199SXin Li            msg = 'Creation of directory %s failed with exception %s'
41*9c5db199SXin Li            msg %= (path, error)
42*9c5db199SXin Li            Exception.__init__(self, msg)
43*9c5db199SXin Li
44*9c5db199SXin Li
45*9c5db199SXin Li    class UnwritableDirectoryException(JobDirectoryException):
46*9c5db199SXin Li        """Raised when a writable directory required by the job exists
47*9c5db199SXin Li        but is not writable."""
48*9c5db199SXin Li        def __init__(self, path):
49*9c5db199SXin Li            msg = 'Directory %s exists but is not writable' % path
50*9c5db199SXin Li            Exception.__init__(self, msg)
51*9c5db199SXin Li
52*9c5db199SXin Li
53*9c5db199SXin Li    def __init__(self, path, is_writable=False):
54*9c5db199SXin Li        """
55*9c5db199SXin Li        Instantiate a job directory.
56*9c5db199SXin Li
57*9c5db199SXin Li        @param path: The path of the directory. If None a temporary directory
58*9c5db199SXin Li            will be created instead.
59*9c5db199SXin Li        @param is_writable: If True, expect the directory to be writable.
60*9c5db199SXin Li
61*9c5db199SXin Li        @raise MissingDirectoryException: raised if is_writable=False and the
62*9c5db199SXin Li            directory does not exist.
63*9c5db199SXin Li        @raise UnwritableDirectoryException: raised if is_writable=True and
64*9c5db199SXin Li            the directory exists but is not writable.
65*9c5db199SXin Li        @raise UncreatableDirectoryException: raised if is_writable=True, the
66*9c5db199SXin Li            directory does not exist and it cannot be created.
67*9c5db199SXin Li        """
68*9c5db199SXin Li        if path is None:
69*9c5db199SXin Li            if is_writable:
70*9c5db199SXin Li                self._tempdir = autotemp.tempdir(unique_id='autotest')
71*9c5db199SXin Li                self.path = self._tempdir.name
72*9c5db199SXin Li            else:
73*9c5db199SXin Li                raise self.MissingDirectoryException(path)
74*9c5db199SXin Li        else:
75*9c5db199SXin Li            self._tempdir = None
76*9c5db199SXin Li            self.path = path
77*9c5db199SXin Li        self._ensure_valid(is_writable)
78*9c5db199SXin Li
79*9c5db199SXin Li
80*9c5db199SXin Li    def _ensure_valid(self, is_writable):
81*9c5db199SXin Li        """
82*9c5db199SXin Li        Ensure that this is a valid directory.
83*9c5db199SXin Li
84*9c5db199SXin Li        Will check if a directory exists, can optionally also enforce that
85*9c5db199SXin Li        it be writable. It can optionally create it if necessary. Creation
86*9c5db199SXin Li        will still fail if the path is rooted in a non-writable directory, or
87*9c5db199SXin Li        if a file already exists at the given location.
88*9c5db199SXin Li
89*9c5db199SXin Li        @param dir_path A path where a directory should be located
90*9c5db199SXin Li        @param is_writable A boolean indicating that the directory should
91*9c5db199SXin Li            not only exist, but also be writable.
92*9c5db199SXin Li
93*9c5db199SXin Li        @raises MissingDirectoryException raised if is_writable=False and the
94*9c5db199SXin Li            directory does not exist.
95*9c5db199SXin Li        @raises UnwritableDirectoryException raised if is_writable=True and
96*9c5db199SXin Li            the directory is not wrtiable.
97*9c5db199SXin Li        @raises UncreatableDirectoryException raised if is_writable=True, the
98*9c5db199SXin Li            directory does not exist and it cannot be created
99*9c5db199SXin Li        """
100*9c5db199SXin Li        # ensure the directory exists
101*9c5db199SXin Li        if is_writable:
102*9c5db199SXin Li            try:
103*9c5db199SXin Li                os.makedirs(self.path)
104*9c5db199SXin Li            except OSError as e:
105*9c5db199SXin Li                if e.errno != errno.EEXIST or not os.path.isdir(self.path):
106*9c5db199SXin Li                    raise self.UncreatableDirectoryException(self.path, e)
107*9c5db199SXin Li        elif not os.path.isdir(self.path):
108*9c5db199SXin Li            raise self.MissingDirectoryException(self.path)
109*9c5db199SXin Li
110*9c5db199SXin Li        # if is_writable=True, also check that the directory is writable
111*9c5db199SXin Li        if is_writable and not os.access(self.path, os.W_OK):
112*9c5db199SXin Li            raise self.UnwritableDirectoryException(self.path)
113*9c5db199SXin Li
114*9c5db199SXin Li
115*9c5db199SXin Li    @staticmethod
116*9c5db199SXin Li    def property_factory(attribute):
117*9c5db199SXin Li        """
118*9c5db199SXin Li        Create a job.*dir -> job._*dir.path property accessor.
119*9c5db199SXin Li
120*9c5db199SXin Li        @param attribute A string with the name of the attribute this is
121*9c5db199SXin Li            exposed as. '_'+attribute must then be attribute that holds
122*9c5db199SXin Li            either None or a job_directory-like object.
123*9c5db199SXin Li
124*9c5db199SXin Li        @returns A read-only property object that exposes a job_directory path
125*9c5db199SXin Li        """
126*9c5db199SXin Li        @property
127*9c5db199SXin Li        def dir_property(self):
128*9c5db199SXin Li            underlying_attribute = getattr(self, '_' + attribute)
129*9c5db199SXin Li            if underlying_attribute is None:
130*9c5db199SXin Li                return None
131*9c5db199SXin Li            else:
132*9c5db199SXin Li                return underlying_attribute.path
133*9c5db199SXin Li        return dir_property
134*9c5db199SXin Li
135*9c5db199SXin Li
136*9c5db199SXin Li# decorator for use with job_state methods
137*9c5db199SXin Lidef with_backing_lock(method):
138*9c5db199SXin Li    """A decorator to perform a lock-*-unlock cycle.
139*9c5db199SXin Li
140*9c5db199SXin Li    When applied to a method, this decorator will automatically wrap
141*9c5db199SXin Li    calls to the method in a backing file lock and before the call
142*9c5db199SXin Li    followed by a backing file unlock.
143*9c5db199SXin Li    """
144*9c5db199SXin Li    def wrapped_method(self, *args, **dargs):
145*9c5db199SXin Li        already_have_lock = self._backing_file_lock is not None
146*9c5db199SXin Li        if not already_have_lock:
147*9c5db199SXin Li            self._lock_backing_file()
148*9c5db199SXin Li        try:
149*9c5db199SXin Li            return method(self, *args, **dargs)
150*9c5db199SXin Li        finally:
151*9c5db199SXin Li            if not already_have_lock:
152*9c5db199SXin Li                self._unlock_backing_file()
153*9c5db199SXin Li    wrapped_method.__name__ = method.__name__
154*9c5db199SXin Li    wrapped_method.__doc__ = method.__doc__
155*9c5db199SXin Li    return wrapped_method
156*9c5db199SXin Li
157*9c5db199SXin Li
158*9c5db199SXin Li# decorator for use with job_state methods
159*9c5db199SXin Lidef with_backing_file(method):
160*9c5db199SXin Li    """A decorator to perform a lock-read-*-write-unlock cycle.
161*9c5db199SXin Li
162*9c5db199SXin Li    When applied to a method, this decorator will automatically wrap
163*9c5db199SXin Li    calls to the method in a lock-and-read before the call followed by a
164*9c5db199SXin Li    write-and-unlock. Any operation that is reading or writing state
165*9c5db199SXin Li    should be decorated with this method to ensure that backing file
166*9c5db199SXin Li    state is consistently maintained.
167*9c5db199SXin Li    """
168*9c5db199SXin Li    @with_backing_lock
169*9c5db199SXin Li    def wrapped_method(self, *args, **dargs):
170*9c5db199SXin Li        self._read_from_backing_file()
171*9c5db199SXin Li        try:
172*9c5db199SXin Li            return method(self, *args, **dargs)
173*9c5db199SXin Li        finally:
174*9c5db199SXin Li            self._write_to_backing_file()
175*9c5db199SXin Li    wrapped_method.__name__ = method.__name__
176*9c5db199SXin Li    wrapped_method.__doc__ = method.__doc__
177*9c5db199SXin Li    return wrapped_method
178*9c5db199SXin Li
179*9c5db199SXin Li
180*9c5db199SXin Li
181*9c5db199SXin Liclass job_state(object):
182*9c5db199SXin Li    """A class for managing explicit job and user state, optionally persistent.
183*9c5db199SXin Li
184*9c5db199SXin Li    The class allows you to save state by name (like a dictionary). Any state
185*9c5db199SXin Li    stored in this class should be picklable and deep copyable. While this is
186*9c5db199SXin Li    not enforced it is recommended that only valid python identifiers be used
187*9c5db199SXin Li    as names. Additionally, the namespace 'stateful_property' is used for
188*9c5db199SXin Li    storing the valued associated with properties constructed using the
189*9c5db199SXin Li    property_factory method.
190*9c5db199SXin Li    """
191*9c5db199SXin Li
192*9c5db199SXin Li    NO_DEFAULT = object()
193*9c5db199SXin Li    PICKLE_PROTOCOL = 2  # highest protocol available in python 2.4
194*9c5db199SXin Li
195*9c5db199SXin Li
196*9c5db199SXin Li    def __init__(self):
197*9c5db199SXin Li        """Initialize the job state."""
198*9c5db199SXin Li        self._state = {}
199*9c5db199SXin Li        self._backing_file = None
200*9c5db199SXin Li        self._backing_file_initialized = False
201*9c5db199SXin Li        self._backing_file_lock = None
202*9c5db199SXin Li
203*9c5db199SXin Li
204*9c5db199SXin Li    def _lock_backing_file(self):
205*9c5db199SXin Li        """Acquire a lock on the backing file."""
206*9c5db199SXin Li        if self._backing_file:
207*9c5db199SXin Li            self._backing_file_lock = open(self._backing_file, 'a')
208*9c5db199SXin Li            fcntl.flock(self._backing_file_lock, fcntl.LOCK_EX)
209*9c5db199SXin Li
210*9c5db199SXin Li
211*9c5db199SXin Li    def _unlock_backing_file(self):
212*9c5db199SXin Li        """Release a lock on the backing file."""
213*9c5db199SXin Li        if self._backing_file_lock:
214*9c5db199SXin Li            fcntl.flock(self._backing_file_lock, fcntl.LOCK_UN)
215*9c5db199SXin Li            self._backing_file_lock.close()
216*9c5db199SXin Li            self._backing_file_lock = None
217*9c5db199SXin Li
218*9c5db199SXin Li
219*9c5db199SXin Li    def read_from_file(self, file_path, merge=True):
220*9c5db199SXin Li        """Read in any state from the file at file_path.
221*9c5db199SXin Li
222*9c5db199SXin Li        When merge=True, any state specified only in-memory will be preserved.
223*9c5db199SXin Li        Any state specified on-disk will be set in-memory, even if an in-memory
224*9c5db199SXin Li        setting already exists.
225*9c5db199SXin Li
226*9c5db199SXin Li        @param file_path: The path where the state should be read from. It must
227*9c5db199SXin Li            exist but it can be empty.
228*9c5db199SXin Li        @param merge: If true, merge the on-disk state with the in-memory
229*9c5db199SXin Li            state. If false, replace the in-memory state with the on-disk
230*9c5db199SXin Li            state.
231*9c5db199SXin Li
232*9c5db199SXin Li        @warning: This method is intentionally concurrency-unsafe. It makes no
233*9c5db199SXin Li            attempt to control concurrent access to the file at file_path.
234*9c5db199SXin Li        """
235*9c5db199SXin Li
236*9c5db199SXin Li        # we can assume that the file exists
237*9c5db199SXin Li        if os.path.getsize(file_path) == 0:
238*9c5db199SXin Li            on_disk_state = {}
239*9c5db199SXin Li        else:
240*9c5db199SXin Li            # This _is_ necessary in the instance that the pickled job is transferred between the
241*9c5db199SXin Li            # server_job and the job on the DUT. The two can be on different autotest versions
242*9c5db199SXin Li            # (e.g. for non-SSP / client tests the server-side is versioned with the drone vs
243*9c5db199SXin Li            # client-side versioned with the ChromeOS being tested).
244*9c5db199SXin Li            try:
245*9c5db199SXin Li                with open(file_path, 'r') as rf:
246*9c5db199SXin Li                    on_disk_state = pickle.load(rf)
247*9c5db199SXin Li            except UnicodeDecodeError:
248*9c5db199SXin Li                with open(file_path, 'rb') as rf:
249*9c5db199SXin Li                    on_disk_state = pickle.load(rf)
250*9c5db199SXin Li        if merge:
251*9c5db199SXin Li            # merge the on-disk state with the in-memory state
252*9c5db199SXin Li            for namespace, namespace_dict in six.iteritems(on_disk_state):
253*9c5db199SXin Li                in_memory_namespace = self._state.setdefault(namespace, {})
254*9c5db199SXin Li                for name, value in six.iteritems(namespace_dict):
255*9c5db199SXin Li                    if name in in_memory_namespace:
256*9c5db199SXin Li                        if in_memory_namespace[name] != value:
257*9c5db199SXin Li                            logging.info('Persistent value of %s.%s from %s '
258*9c5db199SXin Li                                         'overridding existing in-memory '
259*9c5db199SXin Li                                         'value', namespace, name, file_path)
260*9c5db199SXin Li                            in_memory_namespace[name] = value
261*9c5db199SXin Li                        else:
262*9c5db199SXin Li                            logging.debug('Value of %s.%s is unchanged, '
263*9c5db199SXin Li                                          'skipping import', namespace, name)
264*9c5db199SXin Li                    else:
265*9c5db199SXin Li                        logging.debug('Importing %s.%s from state file %s',
266*9c5db199SXin Li                                      namespace, name, file_path)
267*9c5db199SXin Li                        in_memory_namespace[name] = value
268*9c5db199SXin Li        else:
269*9c5db199SXin Li            # just replace the in-memory state with the on-disk state
270*9c5db199SXin Li            self._state = on_disk_state
271*9c5db199SXin Li
272*9c5db199SXin Li        # lock the backing file before we refresh it
273*9c5db199SXin Li        with_backing_lock(self.__class__._write_to_backing_file)(self)
274*9c5db199SXin Li
275*9c5db199SXin Li
276*9c5db199SXin Li    def write_to_file(self, file_path):
277*9c5db199SXin Li        """Write out the current state to the given path.
278*9c5db199SXin Li
279*9c5db199SXin Li        @param file_path: The path where the state should be written out to.
280*9c5db199SXin Li            Must be writable.
281*9c5db199SXin Li
282*9c5db199SXin Li        @warning: This method is intentionally concurrency-unsafe. It makes no
283*9c5db199SXin Li            attempt to control concurrent access to the file at file_path.
284*9c5db199SXin Li        """
285*9c5db199SXin Li        with open(file_path, 'wb') as wf:
286*9c5db199SXin Li            pickle.dump(self._state, wf, self.PICKLE_PROTOCOL)
287*9c5db199SXin Li
288*9c5db199SXin Li    def _read_from_backing_file(self):
289*9c5db199SXin Li        """Refresh the current state from the backing file.
290*9c5db199SXin Li
291*9c5db199SXin Li        If the backing file has never been read before (indicated by checking
292*9c5db199SXin Li        self._backing_file_initialized) it will merge the file with the
293*9c5db199SXin Li        in-memory state, rather than overwriting it.
294*9c5db199SXin Li        """
295*9c5db199SXin Li        if self._backing_file:
296*9c5db199SXin Li            merge_backing_file = not self._backing_file_initialized
297*9c5db199SXin Li            self.read_from_file(self._backing_file, merge=merge_backing_file)
298*9c5db199SXin Li            self._backing_file_initialized = True
299*9c5db199SXin Li
300*9c5db199SXin Li
301*9c5db199SXin Li    def _write_to_backing_file(self):
302*9c5db199SXin Li        """Flush the current state to the backing file."""
303*9c5db199SXin Li        if self._backing_file:
304*9c5db199SXin Li            self.write_to_file(self._backing_file)
305*9c5db199SXin Li
306*9c5db199SXin Li
307*9c5db199SXin Li    @with_backing_file
308*9c5db199SXin Li    def _synchronize_backing_file(self):
309*9c5db199SXin Li        """Synchronizes the contents of the in-memory and on-disk state."""
310*9c5db199SXin Li        # state is implicitly synchronized in _with_backing_file methods
311*9c5db199SXin Li        pass
312*9c5db199SXin Li
313*9c5db199SXin Li
314*9c5db199SXin Li    def set_backing_file(self, file_path):
315*9c5db199SXin Li        """Change the path used as the backing file for the persistent state.
316*9c5db199SXin Li
317*9c5db199SXin Li        When a new backing file is specified if a file already exists then
318*9c5db199SXin Li        its contents will be added into the current state, with conflicts
319*9c5db199SXin Li        between the file and memory being resolved in favor of the file
320*9c5db199SXin Li        contents. The file will then be kept in sync with the (combined)
321*9c5db199SXin Li        in-memory state. The syncing can be disabled by setting this to None.
322*9c5db199SXin Li
323*9c5db199SXin Li        @param file_path: A path on the filesystem that can be read from and
324*9c5db199SXin Li            written to, or None to turn off the backing store.
325*9c5db199SXin Li        """
326*9c5db199SXin Li        self._synchronize_backing_file()
327*9c5db199SXin Li        self._backing_file = file_path
328*9c5db199SXin Li        self._backing_file_initialized = False
329*9c5db199SXin Li        self._synchronize_backing_file()
330*9c5db199SXin Li
331*9c5db199SXin Li
332*9c5db199SXin Li    @with_backing_file
333*9c5db199SXin Li    def get(self, namespace, name, default=NO_DEFAULT):
334*9c5db199SXin Li        """Returns the value associated with a particular name.
335*9c5db199SXin Li
336*9c5db199SXin Li        @param namespace: The namespace that the property should be stored in.
337*9c5db199SXin Li        @param name: The name the value was saved with.
338*9c5db199SXin Li        @param default: A default value to return if no state is currently
339*9c5db199SXin Li            associated with var.
340*9c5db199SXin Li
341*9c5db199SXin Li        @return: A deep copy of the value associated with name. Note that this
342*9c5db199SXin Li            explicitly returns a deep copy to avoid problems with mutable
343*9c5db199SXin Li            values; mutations are not persisted or shared.
344*9c5db199SXin Li        @raise KeyError: raised when no state is associated with var and a
345*9c5db199SXin Li            default value is not provided.
346*9c5db199SXin Li        """
347*9c5db199SXin Li        if self.has(namespace, name):
348*9c5db199SXin Li            return copy.deepcopy(self._state[namespace][name])
349*9c5db199SXin Li        elif default is self.NO_DEFAULT:
350*9c5db199SXin Li            raise KeyError('No key %s in namespace %s' % (name, namespace))
351*9c5db199SXin Li        else:
352*9c5db199SXin Li            return default
353*9c5db199SXin Li
354*9c5db199SXin Li
355*9c5db199SXin Li    @with_backing_file
356*9c5db199SXin Li    def set(self, namespace, name, value):
357*9c5db199SXin Li        """Saves the value given with the provided name.
358*9c5db199SXin Li
359*9c5db199SXin Li        @param namespace: The namespace that the property should be stored in.
360*9c5db199SXin Li        @param name: The name the value should be saved with.
361*9c5db199SXin Li        @param value: The value to save.
362*9c5db199SXin Li        """
363*9c5db199SXin Li        namespace_dict = self._state.setdefault(namespace, {})
364*9c5db199SXin Li        namespace_dict[name] = copy.deepcopy(value)
365*9c5db199SXin Li        logging.debug('Persistent state %s.%s now set to %r', namespace,
366*9c5db199SXin Li                      name, value)
367*9c5db199SXin Li
368*9c5db199SXin Li
369*9c5db199SXin Li    @with_backing_file
370*9c5db199SXin Li    def has(self, namespace, name):
371*9c5db199SXin Li        """Return a boolean indicating if namespace.name is defined.
372*9c5db199SXin Li
373*9c5db199SXin Li        @param namespace: The namespace to check for a definition.
374*9c5db199SXin Li        @param name: The name to check for a definition.
375*9c5db199SXin Li
376*9c5db199SXin Li        @return: True if the given name is defined in the given namespace and
377*9c5db199SXin Li            False otherwise.
378*9c5db199SXin Li        """
379*9c5db199SXin Li        return namespace in self._state and name in self._state[namespace]
380*9c5db199SXin Li
381*9c5db199SXin Li
382*9c5db199SXin Li    @with_backing_file
383*9c5db199SXin Li    def discard(self, namespace, name):
384*9c5db199SXin Li        """If namespace.name is a defined value, deletes it.
385*9c5db199SXin Li
386*9c5db199SXin Li        @param namespace: The namespace that the property is stored in.
387*9c5db199SXin Li        @param name: The name the value is saved with.
388*9c5db199SXin Li        """
389*9c5db199SXin Li        if self.has(namespace, name):
390*9c5db199SXin Li            del self._state[namespace][name]
391*9c5db199SXin Li            if len(self._state[namespace]) == 0:
392*9c5db199SXin Li                del self._state[namespace]
393*9c5db199SXin Li            logging.debug('Persistent state %s.%s deleted', namespace, name)
394*9c5db199SXin Li        else:
395*9c5db199SXin Li            logging.debug(
396*9c5db199SXin Li                'Persistent state %s.%s not defined so nothing is discarded',
397*9c5db199SXin Li                namespace, name)
398*9c5db199SXin Li
399*9c5db199SXin Li
400*9c5db199SXin Li    @with_backing_file
401*9c5db199SXin Li    def discard_namespace(self, namespace):
402*9c5db199SXin Li        """Delete all defined namespace.* names.
403*9c5db199SXin Li
404*9c5db199SXin Li        @param namespace: The namespace to be cleared.
405*9c5db199SXin Li        """
406*9c5db199SXin Li        if namespace in self._state:
407*9c5db199SXin Li            del self._state[namespace]
408*9c5db199SXin Li        logging.debug('Persistent state %s.* deleted', namespace)
409*9c5db199SXin Li
410*9c5db199SXin Li
411*9c5db199SXin Li    @staticmethod
412*9c5db199SXin Li    def property_factory(state_attribute, property_attribute, default,
413*9c5db199SXin Li                         namespace='global_properties'):
414*9c5db199SXin Li        """
415*9c5db199SXin Li        Create a property object for an attribute using self.get and self.set.
416*9c5db199SXin Li
417*9c5db199SXin Li        @param state_attribute: A string with the name of the attribute on
418*9c5db199SXin Li            job that contains the job_state instance.
419*9c5db199SXin Li        @param property_attribute: A string with the name of the attribute
420*9c5db199SXin Li            this property is exposed as.
421*9c5db199SXin Li        @param default: A default value that should be used for this property
422*9c5db199SXin Li            if it is not set.
423*9c5db199SXin Li        @param namespace: The namespace to store the attribute value in.
424*9c5db199SXin Li
425*9c5db199SXin Li        @return: A read-write property object that performs self.get calls
426*9c5db199SXin Li            to read the value and self.set calls to set it.
427*9c5db199SXin Li        """
428*9c5db199SXin Li        def getter(job):
429*9c5db199SXin Li            state = getattr(job, state_attribute)
430*9c5db199SXin Li            return state.get(namespace, property_attribute, default)
431*9c5db199SXin Li        def setter(job, value):
432*9c5db199SXin Li            state = getattr(job, state_attribute)
433*9c5db199SXin Li            state.set(namespace, property_attribute, value)
434*9c5db199SXin Li        return property(getter, setter)
435*9c5db199SXin Li
436*9c5db199SXin Li
437*9c5db199SXin Liclass status_log_entry(object):
438*9c5db199SXin Li    """Represents a single status log entry."""
439*9c5db199SXin Li
440*9c5db199SXin Li    RENDERED_NONE_VALUE = '----'
441*9c5db199SXin Li    TIMESTAMP_FIELD = 'timestamp'
442*9c5db199SXin Li    LOCALTIME_FIELD = 'localtime'
443*9c5db199SXin Li
444*9c5db199SXin Li    # non-space whitespace is forbidden in any fields
445*9c5db199SXin Li    BAD_CHAR_REGEX = re.compile(r'[\t\n\r\v\f]')
446*9c5db199SXin Li
447*9c5db199SXin Li    def _init_message(self, message):
448*9c5db199SXin Li        """Handle the message which describs event to be recorded.
449*9c5db199SXin Li
450*9c5db199SXin Li        Break the message line into a single-line message that goes into the
451*9c5db199SXin Li        database, and a block of additional lines that goes into the status
452*9c5db199SXin Li        log but will never be parsed
453*9c5db199SXin Li        When detecting a bad char in message, replace it with space instead
454*9c5db199SXin Li        of raising an exception that cannot be parsed by tko parser.
455*9c5db199SXin Li
456*9c5db199SXin Li        @param message: the input message.
457*9c5db199SXin Li
458*9c5db199SXin Li        @return: filtered message without bad characters.
459*9c5db199SXin Li        """
460*9c5db199SXin Li        message_lines = message.splitlines()
461*9c5db199SXin Li        if message_lines:
462*9c5db199SXin Li            self.message = message_lines[0]
463*9c5db199SXin Li            self.extra_message_lines = message_lines[1:]
464*9c5db199SXin Li        else:
465*9c5db199SXin Li            self.message = ''
466*9c5db199SXin Li            self.extra_message_lines = []
467*9c5db199SXin Li
468*9c5db199SXin Li        self.message = self.message.replace('\t', ' ' * 8)
469*9c5db199SXin Li        self.message = self.BAD_CHAR_REGEX.sub(' ', self.message)
470*9c5db199SXin Li
471*9c5db199SXin Li
472*9c5db199SXin Li    def __init__(self, status_code, subdir, operation, message, fields,
473*9c5db199SXin Li                 timestamp=None):
474*9c5db199SXin Li        """Construct a status.log entry.
475*9c5db199SXin Li
476*9c5db199SXin Li        @param status_code: A message status code. Must match the codes
477*9c5db199SXin Li            accepted by autotest_lib.common_lib.log.is_valid_status.
478*9c5db199SXin Li        @param subdir: A valid job subdirectory, or None.
479*9c5db199SXin Li        @param operation: Description of the operation, or None.
480*9c5db199SXin Li        @param message: A printable string describing event to be recorded.
481*9c5db199SXin Li        @param fields: A dictionary of arbitrary alphanumeric key=value pairs
482*9c5db199SXin Li            to be included in the log, or None.
483*9c5db199SXin Li        @param timestamp: An optional integer timestamp, in the same format
484*9c5db199SXin Li            as a time.time() timestamp. If unspecified, the current time is
485*9c5db199SXin Li            used.
486*9c5db199SXin Li
487*9c5db199SXin Li        @raise ValueError: if any of the parameters are invalid
488*9c5db199SXin Li        """
489*9c5db199SXin Li        if not log.is_valid_status(status_code):
490*9c5db199SXin Li            raise ValueError('status code %r is not valid' % status_code)
491*9c5db199SXin Li        self.status_code = status_code
492*9c5db199SXin Li
493*9c5db199SXin Li        if subdir and self.BAD_CHAR_REGEX.search(subdir):
494*9c5db199SXin Li            raise ValueError('Invalid character in subdir string')
495*9c5db199SXin Li        self.subdir = subdir
496*9c5db199SXin Li
497*9c5db199SXin Li        if operation and self.BAD_CHAR_REGEX.search(operation):
498*9c5db199SXin Li            raise ValueError('Invalid character in operation string')
499*9c5db199SXin Li        self.operation = operation
500*9c5db199SXin Li
501*9c5db199SXin Li        self._init_message(message)
502*9c5db199SXin Li
503*9c5db199SXin Li        if not fields:
504*9c5db199SXin Li            self.fields = {}
505*9c5db199SXin Li        else:
506*9c5db199SXin Li            self.fields = fields.copy()
507*9c5db199SXin Li        for key, value in six.iteritems(self.fields):
508*9c5db199SXin Li            if type(value) is int:
509*9c5db199SXin Li                value = str(value)
510*9c5db199SXin Li            if self.BAD_CHAR_REGEX.search(key + value):
511*9c5db199SXin Li                raise ValueError('Invalid character in %r=%r field'
512*9c5db199SXin Li                                 % (key, value))
513*9c5db199SXin Li
514*9c5db199SXin Li        # build up the timestamp
515*9c5db199SXin Li        if timestamp is None:
516*9c5db199SXin Li            timestamp = int(time.time())
517*9c5db199SXin Li        self.fields[self.TIMESTAMP_FIELD] = str(timestamp)
518*9c5db199SXin Li        self.fields[self.LOCALTIME_FIELD] = time.strftime(
519*9c5db199SXin Li            '%b %d %H:%M:%S', time.localtime(timestamp))
520*9c5db199SXin Li
521*9c5db199SXin Li
522*9c5db199SXin Li    def is_start(self):
523*9c5db199SXin Li        """Indicates if this status log is the start of a new nested block.
524*9c5db199SXin Li
525*9c5db199SXin Li        @return: A boolean indicating if this entry starts a new nested block.
526*9c5db199SXin Li        """
527*9c5db199SXin Li        return self.status_code == 'START'
528*9c5db199SXin Li
529*9c5db199SXin Li
530*9c5db199SXin Li    def is_end(self):
531*9c5db199SXin Li        """Indicates if this status log is the end of a nested block.
532*9c5db199SXin Li
533*9c5db199SXin Li        @return: A boolean indicating if this entry ends a nested block.
534*9c5db199SXin Li        """
535*9c5db199SXin Li        return self.status_code.startswith('END ')
536*9c5db199SXin Li
537*9c5db199SXin Li
538*9c5db199SXin Li    def render(self):
539*9c5db199SXin Li        """Render the status log entry into a text string.
540*9c5db199SXin Li
541*9c5db199SXin Li        @return: A text string suitable for writing into a status log file.
542*9c5db199SXin Li        """
543*9c5db199SXin Li        # combine all the log line data into a tab-delimited string
544*9c5db199SXin Li        subdir = self.subdir or self.RENDERED_NONE_VALUE
545*9c5db199SXin Li        operation = self.operation or self.RENDERED_NONE_VALUE
546*9c5db199SXin Li        extra_fields = ['%s=%s' % field for field in six.iteritems(self.fields)]
547*9c5db199SXin Li        line_items = [self.status_code, subdir, operation]
548*9c5db199SXin Li        line_items += extra_fields + [self.message]
549*9c5db199SXin Li        first_line = '\t'.join(line_items)
550*9c5db199SXin Li
551*9c5db199SXin Li        # append the extra unparsable lines, two-space indented
552*9c5db199SXin Li        all_lines = [first_line]
553*9c5db199SXin Li        all_lines += ['  ' + line for line in self.extra_message_lines]
554*9c5db199SXin Li        return '\n'.join(all_lines)
555*9c5db199SXin Li
556*9c5db199SXin Li
557*9c5db199SXin Li    @classmethod
558*9c5db199SXin Li    def parse(cls, line):
559*9c5db199SXin Li        """Parse a status log entry from a text string.
560*9c5db199SXin Li
561*9c5db199SXin Li        This method is the inverse of render; it should always be true that
562*9c5db199SXin Li        parse(entry.render()) produces a new status_log_entry equivalent to
563*9c5db199SXin Li        entry.
564*9c5db199SXin Li
565*9c5db199SXin Li        @return: A new status_log_entry instance with fields extracted from the
566*9c5db199SXin Li            given status line. If the line is an extra message line then None
567*9c5db199SXin Li            is returned.
568*9c5db199SXin Li        """
569*9c5db199SXin Li        # extra message lines are always prepended with two spaces
570*9c5db199SXin Li        if line.startswith('  '):
571*9c5db199SXin Li            return None
572*9c5db199SXin Li
573*9c5db199SXin Li        line = line.lstrip('\t')  # ignore indentation
574*9c5db199SXin Li        entry_parts = line.split('\t')
575*9c5db199SXin Li        if len(entry_parts) < 4:
576*9c5db199SXin Li            raise ValueError('%r is not a valid status line' % line)
577*9c5db199SXin Li        status_code, subdir, operation = entry_parts[:3]
578*9c5db199SXin Li        if subdir == cls.RENDERED_NONE_VALUE:
579*9c5db199SXin Li            subdir = None
580*9c5db199SXin Li        if operation == cls.RENDERED_NONE_VALUE:
581*9c5db199SXin Li            operation = None
582*9c5db199SXin Li        message = entry_parts[-1]
583*9c5db199SXin Li        fields = dict(part.split('=', 1) for part in entry_parts[3:-1])
584*9c5db199SXin Li        if cls.TIMESTAMP_FIELD in fields:
585*9c5db199SXin Li            timestamp = int(fields[cls.TIMESTAMP_FIELD])
586*9c5db199SXin Li        else:
587*9c5db199SXin Li            timestamp = None
588*9c5db199SXin Li        return cls(status_code, subdir, operation, message, fields, timestamp)
589*9c5db199SXin Li
590*9c5db199SXin Li
591*9c5db199SXin Liclass status_indenter(object):
592*9c5db199SXin Li    """Abstract interface that a status log indenter should use."""
593*9c5db199SXin Li
594*9c5db199SXin Li    @property
595*9c5db199SXin Li    def indent(self):
596*9c5db199SXin Li        raise NotImplementedError
597*9c5db199SXin Li
598*9c5db199SXin Li
599*9c5db199SXin Li    def increment(self):
600*9c5db199SXin Li        """Increase indentation by one level."""
601*9c5db199SXin Li        raise NotImplementedError
602*9c5db199SXin Li
603*9c5db199SXin Li
604*9c5db199SXin Li    def decrement(self):
605*9c5db199SXin Li        """Decrease indentation by one level."""
606*9c5db199SXin Li
607*9c5db199SXin Li
608*9c5db199SXin Liclass status_logger(object):
609*9c5db199SXin Li    """Represents a status log file. Responsible for translating messages
610*9c5db199SXin Li    into on-disk status log lines.
611*9c5db199SXin Li
612*9c5db199SXin Li    @property global_filename: The filename to write top-level logs to.
613*9c5db199SXin Li    @property subdir_filename: The filename to write subdir-level logs to.
614*9c5db199SXin Li    """
615*9c5db199SXin Li    def __init__(self, job, indenter, global_filename='status',
616*9c5db199SXin Li                 subdir_filename='status', record_hook=None):
617*9c5db199SXin Li        """Construct a logger instance.
618*9c5db199SXin Li
619*9c5db199SXin Li        @param job: A reference to the job object this is logging for. Only a
620*9c5db199SXin Li            weak reference to the job is held, to avoid a
621*9c5db199SXin Li            status_logger <-> job circular reference.
622*9c5db199SXin Li        @param indenter: A status_indenter instance, for tracking the
623*9c5db199SXin Li            indentation level.
624*9c5db199SXin Li        @param global_filename: An optional filename to initialize the
625*9c5db199SXin Li            self.global_filename attribute.
626*9c5db199SXin Li        @param subdir_filename: An optional filename to initialize the
627*9c5db199SXin Li            self.subdir_filename attribute.
628*9c5db199SXin Li        @param record_hook: An optional function to be called before an entry
629*9c5db199SXin Li            is logged. The function should expect a single parameter, a
630*9c5db199SXin Li            copy of the status_log_entry object.
631*9c5db199SXin Li        """
632*9c5db199SXin Li        self._jobref = weakref.ref(job)
633*9c5db199SXin Li        self._indenter = indenter
634*9c5db199SXin Li        self.global_filename = global_filename
635*9c5db199SXin Li        self.subdir_filename = subdir_filename
636*9c5db199SXin Li        self._record_hook = record_hook
637*9c5db199SXin Li
638*9c5db199SXin Li
639*9c5db199SXin Li    def render_entry(self, log_entry):
640*9c5db199SXin Li        """Render a status_log_entry as it would be written to a log file.
641*9c5db199SXin Li
642*9c5db199SXin Li        @param log_entry: A status_log_entry instance to be rendered.
643*9c5db199SXin Li
644*9c5db199SXin Li        @return: The status log entry, rendered as it would be written to the
645*9c5db199SXin Li            logs (including indentation).
646*9c5db199SXin Li        """
647*9c5db199SXin Li        if log_entry.is_end():
648*9c5db199SXin Li            indent = self._indenter.indent - 1
649*9c5db199SXin Li        else:
650*9c5db199SXin Li            indent = self._indenter.indent
651*9c5db199SXin Li        return '\t' * indent + log_entry.render().rstrip('\n')
652*9c5db199SXin Li
653*9c5db199SXin Li
654*9c5db199SXin Li    def record_entry(self, log_entry, log_in_subdir=True):
655*9c5db199SXin Li        """Record a status_log_entry into the appropriate status log files.
656*9c5db199SXin Li
657*9c5db199SXin Li        @param log_entry: A status_log_entry instance to be recorded into the
658*9c5db199SXin Li                status logs.
659*9c5db199SXin Li        @param log_in_subdir: A boolean that indicates (when true) that subdir
660*9c5db199SXin Li                logs should be written into the subdirectory status log file.
661*9c5db199SXin Li        """
662*9c5db199SXin Li        # acquire a strong reference for the duration of the method
663*9c5db199SXin Li        job = self._jobref()
664*9c5db199SXin Li        if job is None:
665*9c5db199SXin Li            logging.warning('Something attempted to write a status log entry '
666*9c5db199SXin Li                            'after its job terminated, ignoring the attempt.')
667*9c5db199SXin Li            logging.warning(traceback.format_stack())
668*9c5db199SXin Li            return
669*9c5db199SXin Li
670*9c5db199SXin Li        # call the record hook if one was given
671*9c5db199SXin Li        if self._record_hook:
672*9c5db199SXin Li            self._record_hook(log_entry)
673*9c5db199SXin Li
674*9c5db199SXin Li        # figure out where we need to log to
675*9c5db199SXin Li        log_files = [os.path.join(job.resultdir, self.global_filename)]
676*9c5db199SXin Li        if log_in_subdir and log_entry.subdir:
677*9c5db199SXin Li            log_files.append(os.path.join(job.resultdir, log_entry.subdir,
678*9c5db199SXin Li                                          self.subdir_filename))
679*9c5db199SXin Li
680*9c5db199SXin Li        # write out to entry to the log files
681*9c5db199SXin Li        log_text = self.render_entry(log_entry)
682*9c5db199SXin Li        for log_file in log_files:
683*9c5db199SXin Li            fileobj = open(log_file, 'a')
684*9c5db199SXin Li            try:
685*9c5db199SXin Li                print(log_text, file=fileobj)
686*9c5db199SXin Li            finally:
687*9c5db199SXin Li                fileobj.close()
688*9c5db199SXin Li
689*9c5db199SXin Li        # adjust the indentation if this was a START or END entry
690*9c5db199SXin Li        if log_entry.is_start():
691*9c5db199SXin Li            self._indenter.increment()
692*9c5db199SXin Li        elif log_entry.is_end():
693*9c5db199SXin Li            self._indenter.decrement()
694*9c5db199SXin Li
695*9c5db199SXin Li
696*9c5db199SXin Liclass base_job(object):
697*9c5db199SXin Li    """An abstract base class for the various autotest job classes.
698*9c5db199SXin Li
699*9c5db199SXin Li    @property autodir: The top level autotest directory.
700*9c5db199SXin Li    @property clientdir: The autotest client directory.
701*9c5db199SXin Li    @property serverdir: The autotest server directory. [OPTIONAL]
702*9c5db199SXin Li    @property resultdir: The directory where results should be written out.
703*9c5db199SXin Li        [WRITABLE]
704*9c5db199SXin Li
705*9c5db199SXin Li    @property pkgdir: The job packages directory. [WRITABLE]
706*9c5db199SXin Li    @property tmpdir: The job temporary directory. [WRITABLE]
707*9c5db199SXin Li    @property testdir: The job test directory. [WRITABLE]
708*9c5db199SXin Li    @property site_testdir: The job site test directory. [WRITABLE]
709*9c5db199SXin Li
710*9c5db199SXin Li    @property bindir: The client bin/ directory.
711*9c5db199SXin Li    @property profdir: The client profilers/ directory.
712*9c5db199SXin Li    @property toolsdir: The client tools/ directory.
713*9c5db199SXin Li
714*9c5db199SXin Li    @property control: A path to the control file to be executed. [OPTIONAL]
715*9c5db199SXin Li    @property hosts: A set of all live Host objects currently in use by the
716*9c5db199SXin Li        job. Code running in the context of a local client can safely assume
717*9c5db199SXin Li        that this set contains only a single entry.
718*9c5db199SXin Li    @property machines: A list of the machine names associated with the job.
719*9c5db199SXin Li    @property user: The user executing the job.
720*9c5db199SXin Li    @property tag: A tag identifying the job. Often used by the scheduler to
721*9c5db199SXin Li        give a name of the form NUMBER-USERNAME/HOSTNAME.
722*9c5db199SXin Li    @property args: A list of addtional miscellaneous command-line arguments
723*9c5db199SXin Li        provided when starting the job.
724*9c5db199SXin Li
725*9c5db199SXin Li    @property automatic_test_tag: A string which, if set, will be automatically
726*9c5db199SXin Li        added to the test name when running tests.
727*9c5db199SXin Li
728*9c5db199SXin Li    @property default_profile_only: A boolean indicating the default value of
729*9c5db199SXin Li        profile_only used by test.execute. [PERSISTENT]
730*9c5db199SXin Li    @property drop_caches: A boolean indicating if caches should be dropped
731*9c5db199SXin Li        before each test is executed.
732*9c5db199SXin Li    @property drop_caches_between_iterations: A boolean indicating if caches
733*9c5db199SXin Li        should be dropped before each test iteration is executed.
734*9c5db199SXin Li    @property run_test_cleanup: A boolean indicating if test.cleanup should be
735*9c5db199SXin Li        run by default after a test completes, if the run_cleanup argument is
736*9c5db199SXin Li        not specified. [PERSISTENT]
737*9c5db199SXin Li
738*9c5db199SXin Li    @property num_tests_run: The number of tests run during the job. [OPTIONAL]
739*9c5db199SXin Li    @property num_tests_failed: The number of tests failed during the job.
740*9c5db199SXin Li        [OPTIONAL]
741*9c5db199SXin Li
742*9c5db199SXin Li    @property harness: An instance of the client test harness. Only available
743*9c5db199SXin Li        in contexts where client test execution happens. [OPTIONAL]
744*9c5db199SXin Li    @property logging: An instance of the logging manager associated with the
745*9c5db199SXin Li        job.
746*9c5db199SXin Li    @property profilers: An instance of the profiler manager associated with
747*9c5db199SXin Li        the job.
748*9c5db199SXin Li    @property sysinfo: An instance of the sysinfo object. Only available in
749*9c5db199SXin Li        contexts where it's possible to collect sysinfo.
750*9c5db199SXin Li    @property warning_manager: A class for managing which types of WARN
751*9c5db199SXin Li        messages should be logged and which should be supressed. [OPTIONAL]
752*9c5db199SXin Li    @property warning_loggers: A set of readable streams that will be monitored
753*9c5db199SXin Li        for WARN messages to be logged. [OPTIONAL]
754*9c5db199SXin Li    @property max_result_size_KB: Maximum size of test results should be
755*9c5db199SXin Li        collected in KB. [OPTIONAL]
756*9c5db199SXin Li
757*9c5db199SXin Li    Abstract methods:
758*9c5db199SXin Li        _find_base_directories [CLASSMETHOD]
759*9c5db199SXin Li            Returns the location of autodir, clientdir and serverdir
760*9c5db199SXin Li
761*9c5db199SXin Li        _find_resultdir
762*9c5db199SXin Li            Returns the location of resultdir. Gets a copy of any parameters
763*9c5db199SXin Li            passed into base_job.__init__. Can return None to indicate that
764*9c5db199SXin Li            no resultdir is to be used.
765*9c5db199SXin Li
766*9c5db199SXin Li        _get_status_logger
767*9c5db199SXin Li            Returns a status_logger instance for recording job status logs.
768*9c5db199SXin Li    """
769*9c5db199SXin Li
770*9c5db199SXin Li    # capture the dependency on several helper classes with factories
771*9c5db199SXin Li    _job_directory = job_directory
772*9c5db199SXin Li    _job_state = job_state
773*9c5db199SXin Li
774*9c5db199SXin Li
775*9c5db199SXin Li    # all the job directory attributes
776*9c5db199SXin Li    autodir = _job_directory.property_factory('autodir')
777*9c5db199SXin Li    clientdir = _job_directory.property_factory('clientdir')
778*9c5db199SXin Li    serverdir = _job_directory.property_factory('serverdir')
779*9c5db199SXin Li    resultdir = _job_directory.property_factory('resultdir')
780*9c5db199SXin Li    pkgdir = _job_directory.property_factory('pkgdir')
781*9c5db199SXin Li    tmpdir = _job_directory.property_factory('tmpdir')
782*9c5db199SXin Li    testdir = _job_directory.property_factory('testdir')
783*9c5db199SXin Li    site_testdir = _job_directory.property_factory('site_testdir')
784*9c5db199SXin Li    bindir = _job_directory.property_factory('bindir')
785*9c5db199SXin Li    profdir = _job_directory.property_factory('profdir')
786*9c5db199SXin Li    toolsdir = _job_directory.property_factory('toolsdir')
787*9c5db199SXin Li
788*9c5db199SXin Li
789*9c5db199SXin Li    # all the generic persistent properties
790*9c5db199SXin Li    tag = _job_state.property_factory('_state', 'tag', '')
791*9c5db199SXin Li    default_profile_only = _job_state.property_factory(
792*9c5db199SXin Li        '_state', 'default_profile_only', False)
793*9c5db199SXin Li    run_test_cleanup = _job_state.property_factory(
794*9c5db199SXin Li        '_state', 'run_test_cleanup', True)
795*9c5db199SXin Li    automatic_test_tag = _job_state.property_factory(
796*9c5db199SXin Li        '_state', 'automatic_test_tag', None)
797*9c5db199SXin Li    max_result_size_KB = _job_state.property_factory(
798*9c5db199SXin Li        '_state', 'max_result_size_KB', 0)
799*9c5db199SXin Li    fast = _job_state.property_factory(
800*9c5db199SXin Li        '_state', 'fast', False)
801*9c5db199SXin Li    extended_timeout = _job_state.property_factory(
802*9c5db199SXin Li        '_state', 'extended_timeout', None)
803*9c5db199SXin Li    # the use_sequence_number property
804*9c5db199SXin Li    _sequence_number = _job_state.property_factory(
805*9c5db199SXin Li        '_state', '_sequence_number', None)
806*9c5db199SXin Li    def _get_use_sequence_number(self):
807*9c5db199SXin Li        return bool(self._sequence_number)
808*9c5db199SXin Li    def _set_use_sequence_number(self, value):
809*9c5db199SXin Li        if value:
810*9c5db199SXin Li            self._sequence_number = 1
811*9c5db199SXin Li        else:
812*9c5db199SXin Li            self._sequence_number = None
813*9c5db199SXin Li    use_sequence_number = property(_get_use_sequence_number,
814*9c5db199SXin Li                                   _set_use_sequence_number)
815*9c5db199SXin Li
816*9c5db199SXin Li    # parent job id is passed in from autoserv command line. It's only used in
817*9c5db199SXin Li    # server job. The property is added here for unittest
818*9c5db199SXin Li    # (base_job_unittest.py) to be consistent on validating public properties of
819*9c5db199SXin Li    # a base_job object.
820*9c5db199SXin Li    parent_job_id = None
821*9c5db199SXin Li
822*9c5db199SXin Li    def __init__(self, *args, **dargs):
823*9c5db199SXin Li        # initialize the base directories, all others are relative to these
824*9c5db199SXin Li        autodir, clientdir, serverdir = self._find_base_directories()
825*9c5db199SXin Li        self._autodir = self._job_directory(autodir)
826*9c5db199SXin Li        self._clientdir = self._job_directory(clientdir)
827*9c5db199SXin Li        # TODO(scottz): crosbug.com/38259, needed to pass unittests for now.
828*9c5db199SXin Li        self.label = None
829*9c5db199SXin Li        if serverdir:
830*9c5db199SXin Li            self._serverdir = self._job_directory(serverdir)
831*9c5db199SXin Li        else:
832*9c5db199SXin Li            self._serverdir = None
833*9c5db199SXin Li
834*9c5db199SXin Li        # initialize all the other directories relative to the base ones
835*9c5db199SXin Li        self._initialize_dir_properties()
836*9c5db199SXin Li        self._resultdir = self._job_directory(
837*9c5db199SXin Li            self._find_resultdir(*args, **dargs), True)
838*9c5db199SXin Li        self._execution_contexts = []
839*9c5db199SXin Li
840*9c5db199SXin Li        # initialize all the job state
841*9c5db199SXin Li        self._state = self._job_state()
842*9c5db199SXin Li
843*9c5db199SXin Li
844*9c5db199SXin Li    @classmethod
845*9c5db199SXin Li    def _find_base_directories(cls):
846*9c5db199SXin Li        raise NotImplementedError()
847*9c5db199SXin Li
848*9c5db199SXin Li
849*9c5db199SXin Li    def _initialize_dir_properties(self):
850*9c5db199SXin Li        """
851*9c5db199SXin Li        Initializes all the secondary self.*dir properties. Requires autodir,
852*9c5db199SXin Li        clientdir and serverdir to already be initialized.
853*9c5db199SXin Li        """
854*9c5db199SXin Li        # create some stubs for use as shortcuts
855*9c5db199SXin Li        def readonly_dir(*args):
856*9c5db199SXin Li            return self._job_directory(os.path.join(*args))
857*9c5db199SXin Li        def readwrite_dir(*args):
858*9c5db199SXin Li            return self._job_directory(os.path.join(*args), True)
859*9c5db199SXin Li
860*9c5db199SXin Li        # various client-specific directories
861*9c5db199SXin Li        self._bindir = readonly_dir(self.clientdir, 'bin')
862*9c5db199SXin Li        self._profdir = readonly_dir(self.clientdir, 'profilers')
863*9c5db199SXin Li        self._pkgdir = readwrite_dir(self.clientdir, 'packages')
864*9c5db199SXin Li        self._toolsdir = readonly_dir(self.clientdir, 'tools')
865*9c5db199SXin Li
866*9c5db199SXin Li        # directories which are in serverdir on a server, clientdir on a client
867*9c5db199SXin Li        # tmp tests, and site_tests need to be read_write for client, but only
868*9c5db199SXin Li        # read for server.
869*9c5db199SXin Li        if self.serverdir:
870*9c5db199SXin Li            root = self.serverdir
871*9c5db199SXin Li            r_or_rw_dir = readonly_dir
872*9c5db199SXin Li        else:
873*9c5db199SXin Li            root = self.clientdir
874*9c5db199SXin Li            r_or_rw_dir = readwrite_dir
875*9c5db199SXin Li        self._testdir = r_or_rw_dir(root, 'tests')
876*9c5db199SXin Li        self._site_testdir = r_or_rw_dir(root, 'site_tests')
877*9c5db199SXin Li
878*9c5db199SXin Li        # various server-specific directories
879*9c5db199SXin Li        if self.serverdir:
880*9c5db199SXin Li            self._tmpdir = readwrite_dir(tempfile.gettempdir())
881*9c5db199SXin Li        else:
882*9c5db199SXin Li            self._tmpdir = readwrite_dir(root, 'tmp')
883*9c5db199SXin Li
884*9c5db199SXin Li
885*9c5db199SXin Li    def _find_resultdir(self, *args, **dargs):
886*9c5db199SXin Li        raise NotImplementedError()
887*9c5db199SXin Li
888*9c5db199SXin Li
889*9c5db199SXin Li    def push_execution_context(self, resultdir):
890*9c5db199SXin Li        """
891*9c5db199SXin Li        Save off the current context of the job and change to the given one.
892*9c5db199SXin Li
893*9c5db199SXin Li        In practice method just changes the resultdir, but it may become more
894*9c5db199SXin Li        extensive in the future. The expected use case is for when a child
895*9c5db199SXin Li        job needs to be executed in some sort of nested context (for example
896*9c5db199SXin Li        the way parallel_simple does). The original context can be restored
897*9c5db199SXin Li        with a pop_execution_context call.
898*9c5db199SXin Li
899*9c5db199SXin Li        @param resultdir: The new resultdir, relative to the current one.
900*9c5db199SXin Li        """
901*9c5db199SXin Li        new_dir = self._job_directory(
902*9c5db199SXin Li            os.path.join(self.resultdir, resultdir), True)
903*9c5db199SXin Li        self._execution_contexts.append(self._resultdir)
904*9c5db199SXin Li        self._resultdir = new_dir
905*9c5db199SXin Li
906*9c5db199SXin Li
907*9c5db199SXin Li    def pop_execution_context(self):
908*9c5db199SXin Li        """
909*9c5db199SXin Li        Reverse the effects of the previous push_execution_context call.
910*9c5db199SXin Li
911*9c5db199SXin Li        @raise IndexError: raised when the stack of contexts is empty.
912*9c5db199SXin Li        """
913*9c5db199SXin Li        if not self._execution_contexts:
914*9c5db199SXin Li            raise IndexError('No old execution context to restore')
915*9c5db199SXin Li        self._resultdir = self._execution_contexts.pop()
916*9c5db199SXin Li
917*9c5db199SXin Li
918*9c5db199SXin Li    def get_state(self, name, default=_job_state.NO_DEFAULT):
919*9c5db199SXin Li        """Returns the value associated with a particular name.
920*9c5db199SXin Li
921*9c5db199SXin Li        @param name: The name the value was saved with.
922*9c5db199SXin Li        @param default: A default value to return if no state is currently
923*9c5db199SXin Li            associated with var.
924*9c5db199SXin Li
925*9c5db199SXin Li        @return: A deep copy of the value associated with name. Note that this
926*9c5db199SXin Li            explicitly returns a deep copy to avoid problems with mutable
927*9c5db199SXin Li            values; mutations are not persisted or shared.
928*9c5db199SXin Li        @raise KeyError: raised when no state is associated with var and a
929*9c5db199SXin Li            default value is not provided.
930*9c5db199SXin Li        """
931*9c5db199SXin Li        try:
932*9c5db199SXin Li            return self._state.get('public', name, default=default)
933*9c5db199SXin Li        except KeyError:
934*9c5db199SXin Li            raise KeyError(name)
935*9c5db199SXin Li
936*9c5db199SXin Li
937*9c5db199SXin Li    def set_state(self, name, value):
938*9c5db199SXin Li        """Saves the value given with the provided name.
939*9c5db199SXin Li
940*9c5db199SXin Li        @param name: The name the value should be saved with.
941*9c5db199SXin Li        @param value: The value to save.
942*9c5db199SXin Li        """
943*9c5db199SXin Li        self._state.set('public', name, value)
944*9c5db199SXin Li
945*9c5db199SXin Li
946*9c5db199SXin Li    def _build_tagged_test_name(self, testname, dargs):
947*9c5db199SXin Li        """Builds the fully tagged testname and subdirectory for job.run_test.
948*9c5db199SXin Li
949*9c5db199SXin Li        @param testname: The base name of the test
950*9c5db199SXin Li        @param dargs: The ** arguments passed to run_test. And arguments
951*9c5db199SXin Li            consumed by this method will be removed from the dictionary.
952*9c5db199SXin Li
953*9c5db199SXin Li        @return: A 3-tuple of the full name of the test, the subdirectory it
954*9c5db199SXin Li            should be stored in, and the full tag of the subdir.
955*9c5db199SXin Li        """
956*9c5db199SXin Li        tag_parts = []
957*9c5db199SXin Li
958*9c5db199SXin Li        # build up the parts of the tag used for the test name
959*9c5db199SXin Li        main_testpath = dargs.get('main_testpath', "")
960*9c5db199SXin Li        base_tag = dargs.pop('tag', None)
961*9c5db199SXin Li        if base_tag:
962*9c5db199SXin Li            tag_parts.append(str(base_tag))
963*9c5db199SXin Li        if self.use_sequence_number:
964*9c5db199SXin Li            tag_parts.append('_%02d_' % self._sequence_number)
965*9c5db199SXin Li            self._sequence_number += 1
966*9c5db199SXin Li        if self.automatic_test_tag:
967*9c5db199SXin Li            tag_parts.append(self.automatic_test_tag)
968*9c5db199SXin Li        full_testname = '.'.join([testname] + tag_parts)
969*9c5db199SXin Li
970*9c5db199SXin Li        # build up the subdir and tag as well
971*9c5db199SXin Li        subdir_tag = dargs.pop('subdir_tag', None)
972*9c5db199SXin Li        if subdir_tag:
973*9c5db199SXin Li            tag_parts.append(subdir_tag)
974*9c5db199SXin Li        subdir = '.'.join([testname] + tag_parts)
975*9c5db199SXin Li        subdir = os.path.join(main_testpath, subdir)
976*9c5db199SXin Li        tag = '.'.join(tag_parts)
977*9c5db199SXin Li
978*9c5db199SXin Li        return full_testname, subdir, tag
979*9c5db199SXin Li
980*9c5db199SXin Li
981*9c5db199SXin Li    def _make_test_outputdir(self, subdir):
982*9c5db199SXin Li        """Creates an output directory for a test to run it.
983*9c5db199SXin Li
984*9c5db199SXin Li        @param subdir: The subdirectory of the test. Generally computed by
985*9c5db199SXin Li            _build_tagged_test_name.
986*9c5db199SXin Li
987*9c5db199SXin Li        @return: A job_directory instance corresponding to the outputdir of
988*9c5db199SXin Li            the test.
989*9c5db199SXin Li        @raise TestError: If the output directory is invalid.
990*9c5db199SXin Li        """
991*9c5db199SXin Li        # explicitly check that this subdirectory is new
992*9c5db199SXin Li        path = os.path.join(self.resultdir, subdir)
993*9c5db199SXin Li        if os.path.exists(path):
994*9c5db199SXin Li            msg = ('%s already exists; multiple tests cannot run with the '
995*9c5db199SXin Li                   'same subdirectory' % subdir)
996*9c5db199SXin Li            raise error.TestError(msg)
997*9c5db199SXin Li
998*9c5db199SXin Li        # create the outputdir and raise a TestError if it isn't valid
999*9c5db199SXin Li        try:
1000*9c5db199SXin Li            outputdir = self._job_directory(path, True)
1001*9c5db199SXin Li            return outputdir
1002*9c5db199SXin Li        except self._job_directory.JobDirectoryException as e:
1003*9c5db199SXin Li            logging.exception('%s directory creation failed with %s',
1004*9c5db199SXin Li                              subdir, e)
1005*9c5db199SXin Li            raise error.TestError('%s directory creation failed' % subdir)
1006*9c5db199SXin Li
1007*9c5db199SXin Li
1008*9c5db199SXin Li    def record(self, status_code, subdir, operation, status='',
1009*9c5db199SXin Li               optional_fields=None):
1010*9c5db199SXin Li        """Record a job-level status event.
1011*9c5db199SXin Li
1012*9c5db199SXin Li        Logs an event noteworthy to the Autotest job as a whole. Messages will
1013*9c5db199SXin Li        be written into a global status log file, as well as a subdir-local
1014*9c5db199SXin Li        status log file (if subdir is specified).
1015*9c5db199SXin Li
1016*9c5db199SXin Li        @param status_code: A string status code describing the type of status
1017*9c5db199SXin Li            entry being recorded. It must pass log.is_valid_status to be
1018*9c5db199SXin Li            considered valid.
1019*9c5db199SXin Li        @param subdir: A specific results subdirectory this also applies to, or
1020*9c5db199SXin Li            None. If not None the subdirectory must exist.
1021*9c5db199SXin Li        @param operation: A string describing the operation that was run.
1022*9c5db199SXin Li        @param status: An optional human-readable message describing the status
1023*9c5db199SXin Li            entry, for example an error message or "completed successfully".
1024*9c5db199SXin Li        @param optional_fields: An optional dictionary of addtional named fields
1025*9c5db199SXin Li            to be included with the status message. Every time timestamp and
1026*9c5db199SXin Li            localtime entries are generated with the current time and added
1027*9c5db199SXin Li            to this dictionary.
1028*9c5db199SXin Li        """
1029*9c5db199SXin Li        entry = status_log_entry(status_code, subdir, operation, status,
1030*9c5db199SXin Li                                 optional_fields)
1031*9c5db199SXin Li        self.record_entry(entry)
1032*9c5db199SXin Li
1033*9c5db199SXin Li
1034*9c5db199SXin Li    def record_entry(self, entry, log_in_subdir=True):
1035*9c5db199SXin Li        """Record a job-level status event, using a status_log_entry.
1036*9c5db199SXin Li
1037*9c5db199SXin Li        This is the same as self.record but using an existing status log
1038*9c5db199SXin Li        entry object rather than constructing one for you.
1039*9c5db199SXin Li
1040*9c5db199SXin Li        @param entry: A status_log_entry object
1041*9c5db199SXin Li        @param log_in_subdir: A boolean that indicates (when true) that subdir
1042*9c5db199SXin Li                logs should be written into the subdirectory status log file.
1043*9c5db199SXin Li        """
1044*9c5db199SXin Li        self._get_status_logger().record_entry(entry, log_in_subdir)
1045