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