1import os 2 3from autotest_lib.client.common_lib import utils 4from autotest_lib.tko import utils as tko_utils 5 6 7class job(object): 8 """Represents a job.""" 9 10 def __init__(self, dir, user, label, machine, queued_time, started_time, 11 finished_time, machine_owner, machine_group, aborted_by, 12 aborted_on, keyval_dict): 13 self.dir = dir 14 self.tests = [] 15 self.user = user 16 self.label = label 17 self.machine = machine 18 self.queued_time = queued_time 19 self.started_time = started_time 20 self.finished_time = finished_time 21 self.machine_owner = machine_owner 22 self.machine_group = machine_group 23 self.aborted_by = aborted_by 24 self.aborted_on = aborted_on 25 self.keyval_dict = keyval_dict 26 27 28 @staticmethod 29 def read_keyval(dir): 30 """ 31 Read job keyval files. 32 33 @param dir: String name of directory containing job keyval files. 34 35 @return A dictionary containing job keyvals. 36 37 """ 38 dir = os.path.normpath(dir) 39 top_dir = tko_utils.find_toplevel_job_dir(dir) 40 if not top_dir: 41 top_dir = dir 42 assert(dir.startswith(top_dir)) 43 44 # Pull in and merge all the keyval files, with higher-level 45 # overriding values in the lower-level ones. 46 keyval = {} 47 while True: 48 try: 49 upper_keyval = utils.read_keyval(dir) 50 # HACK: exclude hostname from the override - this is a special 51 # case where we want lower to override higher. 52 if 'hostname' in upper_keyval and 'hostname' in keyval: 53 del upper_keyval['hostname'] 54 keyval.update(upper_keyval) 55 except IOError: 56 pass # If the keyval can't be read just move on to the next. 57 if dir == top_dir: 58 break 59 else: 60 assert(dir != '/') 61 dir = os.path.dirname(dir) 62 return keyval 63 64 65class kernel(object): 66 """Represents a kernel.""" 67 68 def __init__(self, base, patches, kernel_hash): 69 self.base = base 70 self.patches = patches 71 self.kernel_hash = kernel_hash 72 73 74 @staticmethod 75 def compute_hash(base, hashes): 76 """Compute a hash given the base string and hashes for each patch. 77 78 @param base: A string representing the kernel base. 79 @param hashes: A list of hashes, where each hash is associated with a 80 patch of this kernel. 81 82 @return A string representing the computed hash. 83 84 """ 85 key_string = ','.join([base] + hashes) 86 return utils.hash('md5', key_string).hexdigest() 87 88 89class test(object): 90 """Represents a test.""" 91 92 def __init__(self, subdir, testname, status, reason, test_kernel, 93 machine, started_time, finished_time, iterations, 94 attributes, perf_values, labels): 95 self.subdir = subdir 96 self.testname = testname 97 self.status = status 98 self.reason = reason 99 self.kernel = test_kernel 100 self.machine = machine 101 self.started_time = started_time 102 self.finished_time = finished_time 103 self.iterations = iterations 104 self.attributes = attributes 105 self.perf_values = perf_values 106 self.labels = labels 107 108 109 @staticmethod 110 def load_iterations(keyval_path): 111 """Abstract method to load a list of iterations from a keyval file. 112 113 @param keyval_path: String path to a keyval file. 114 115 @return A list of iteration objects. 116 117 """ 118 raise NotImplementedError 119 120 121 @staticmethod 122 def load_perf_values(perf_values_file): 123 """Loads perf values from a perf measurements file. 124 125 @param perf_values_file: The string path to a perf measurements file. 126 127 @return A list of perf_value_iteration objects. 128 129 """ 130 raise NotImplementedError 131 132 133 @classmethod 134 def parse_test(cls, job, subdir, testname, status, reason, test_kernel, 135 started_time, finished_time, existing_instance=None): 136 """ 137 Parse test result files to construct a complete test instance. 138 139 Given a job and the basic metadata about the test that can be 140 extracted from the status logs, parse the test result files (keyval 141 files and perf measurement files) and use them to construct a complete 142 test instance. 143 144 @param job: A job object. 145 @param subdir: The string subdirectory name for the given test. 146 @param testname: The name of the test. 147 @param status: The status of the test. 148 @param reason: The reason string for the test. 149 @param test_kernel: The kernel of the test. 150 @param started_time: The start time of the test. 151 @param finished_time: The finish time of the test. 152 @param existing_instance: An existing test instance. 153 154 @return A test instance that has the complete information. 155 156 """ 157 tko_utils.dprint("parsing test %s %s" % (subdir, testname)) 158 159 if subdir: 160 # Grab iterations from the results keyval. 161 iteration_keyval = os.path.join(job.dir, subdir, 162 'results', 'keyval') 163 iterations = cls.load_iterations(iteration_keyval) 164 165 # Grab perf values from the perf measurements file. 166 perf_values_file = os.path.join(job.dir, subdir, 167 'results', 'perf_measurements') 168 perf_values = cls.load_perf_values(perf_values_file) 169 170 # Grab test attributes from the subdir keyval. 171 test_keyval = os.path.join(job.dir, subdir, 'keyval') 172 attributes = test.load_attributes(test_keyval) 173 else: 174 iterations = [] 175 perf_values = [] 176 attributes = {} 177 178 # Grab test+host attributes from the host keyval. 179 host_keyval = cls.parse_host_keyval(job.dir, job.machine) 180 attributes.update(dict(('host-%s' % k, v) 181 for k, v in host_keyval.iteritems())) 182 183 if existing_instance: 184 def constructor(*args, **dargs): 185 """Initializes an existing test instance.""" 186 existing_instance.__init__(*args, **dargs) 187 return existing_instance 188 else: 189 constructor = cls 190 191 return constructor(subdir, testname, status, reason, test_kernel, 192 job.machine, started_time, finished_time, 193 iterations, attributes, perf_values, []) 194 195 196 @classmethod 197 def parse_partial_test(cls, job, subdir, testname, reason, test_kernel, 198 started_time): 199 """ 200 Create a test instance representing a partial test result. 201 202 Given a job and the basic metadata available when a test is 203 started, create a test instance representing the partial result. 204 Assume that since the test is not complete there are no results files 205 actually available for parsing. 206 207 @param job: A job object. 208 @param subdir: The string subdirectory name for the given test. 209 @param testname: The name of the test. 210 @param reason: The reason string for the test. 211 @param test_kernel: The kernel of the test. 212 @param started_time: The start time of the test. 213 214 @return A test instance that has partial test information. 215 216 """ 217 tko_utils.dprint('parsing partial test %s %s' % (subdir, testname)) 218 219 return cls(subdir, testname, 'RUNNING', reason, test_kernel, 220 job.machine, started_time, None, [], {}, [], []) 221 222 223 @staticmethod 224 def load_attributes(keyval_path): 225 """ 226 Load test attributes from a test keyval path. 227 228 Load the test attributes into a dictionary from a test 229 keyval path. Does not assume that the path actually exists. 230 231 @param keyval_path: The string path to a keyval file. 232 233 @return A dictionary representing the test keyvals. 234 235 """ 236 if not os.path.exists(keyval_path): 237 return {} 238 return utils.read_keyval(keyval_path) 239 240 241 @staticmethod 242 def parse_host_keyval(job_dir, hostname): 243 """ 244 Parse host keyvals. 245 246 @param job_dir: The string directory name of the associated job. 247 @param hostname: The string hostname. 248 249 @return A dictionary representing the host keyvals. 250 251 """ 252 # The "real" job dir may be higher up in the directory tree. 253 job_dir = tko_utils.find_toplevel_job_dir(job_dir) 254 if not job_dir: 255 return {} # We can't find a top-level job dir with host keyvals. 256 257 # The keyval is <job_dir>/host_keyvals/<hostname> if it exists. 258 keyval_path = os.path.join(job_dir, 'host_keyvals', hostname) 259 if os.path.isfile(keyval_path): 260 return utils.read_keyval(keyval_path) 261 else: 262 return {} 263 264 265class patch(object): 266 """Represents a patch.""" 267 268 def __init__(self, spec, reference, hash): 269 self.spec = spec 270 self.reference = reference 271 self.hash = hash 272 273 274class iteration(object): 275 """Represents an iteration.""" 276 277 def __init__(self, index, attr_keyval, perf_keyval): 278 self.index = index 279 self.attr_keyval = attr_keyval 280 self.perf_keyval = perf_keyval 281 282 283 @staticmethod 284 def parse_line_into_dicts(line, attr_dict, perf_dict): 285 """ 286 Abstract method to parse a keyval line and insert it into a dictionary. 287 288 @param line: The string line to parse. 289 @param attr_dict: Dictionary of generic iteration attributes. 290 @param perf_dict: Dictionary of iteration performance results. 291 292 """ 293 raise NotImplementedError 294 295 296 @classmethod 297 def load_from_keyval(cls, keyval_path): 298 """ 299 Load a list of iterations from an iteration keyval file. 300 301 Keyval data from separate iterations is separated by blank 302 lines. Makes use of the parse_line_into_dicts method to 303 actually parse the individual lines. 304 305 @param keyval_path: The string path to a keyval file. 306 307 @return A list of iteration objects. 308 309 """ 310 if not os.path.exists(keyval_path): 311 return [] 312 313 iterations = [] 314 index = 1 315 attr, perf = {}, {} 316 for line in file(keyval_path): 317 line = line.strip() 318 if line: 319 cls.parse_line_into_dicts(line, attr, perf) 320 else: 321 iterations.append(cls(index, attr, perf)) 322 index += 1 323 attr, perf = {}, {} 324 if attr or perf: 325 iterations.append(cls(index, attr, perf)) 326 return iterations 327 328 329class perf_value_iteration(object): 330 """Represents a perf value iteration.""" 331 332 def __init__(self, index, perf_measurements): 333 """ 334 Initializes the perf values for a particular test iteration. 335 336 @param index: The integer iteration number. 337 @param perf_measurements: A list of dictionaries, where each dictionary 338 contains the information for a measured perf metric from the 339 current iteration. 340 341 """ 342 self.index = index 343 self.perf_measurements = perf_measurements 344 345 346 def add_measurement(self, measurement): 347 """ 348 Appends to the list of perf measurements for this iteration. 349 350 @param measurement: A dictionary containing information for a measured 351 perf metric. 352 353 """ 354 self.perf_measurements.append(measurement) 355 356 357 @staticmethod 358 def parse_line_into_dict(line): 359 """ 360 Abstract method to parse an individual perf measurement line. 361 362 @param line: A string line from the perf measurement output file. 363 364 @return A dicionary representing the information for a measured perf 365 metric from one line of the perf measurement output file, or an 366 empty dictionary if the line cannot be parsed successfully. 367 368 """ 369 raise NotImplementedError 370 371 372 @classmethod 373 def load_from_perf_values_file(cls, perf_values_file): 374 """ 375 Load perf values from each iteration in a perf measurements file. 376 377 Multiple measurements for the same perf metric description are assumed 378 to come from different iterations. Makes use of the 379 parse_line_into_dict function to actually parse the individual lines. 380 381 @param perf_values_file: The string name of the output file containing 382 perf measurements. 383 384 @return A list of |perf_value_iteration| objects, where position 0 of 385 the list contains the object representing the first iteration, 386 position 1 contains the object representing the second iteration, 387 and so forth. 388 389 """ 390 if not os.path.exists(perf_values_file): 391 return [] 392 393 perf_value_iterations = [] 394 # For each description string representing a unique perf metric, keep 395 # track of the next iteration that it belongs to (multiple occurrences 396 # of the same description are assumed to come from different 397 # iterations). 398 desc_to_next_iter = {} 399 with open(perf_values_file) as fp: 400 for line in [ln for ln in fp if ln.strip()]: 401 perf_value_dict = cls.parse_line_into_dict(line) 402 if not perf_value_dict: 403 continue 404 desc = perf_value_dict['description'] 405 iter_to_set = desc_to_next_iter.setdefault(desc, 1) 406 desc_to_next_iter[desc] = iter_to_set + 1 407 if iter_to_set > len(perf_value_iterations): 408 # We have information that needs to go into a new 409 # |perf_value_iteration| object. 410 perf_value_iterations.append(cls(iter_to_set, [])) 411 # Add the perf measurement to the appropriate 412 # |perf_value_iteration| object. 413 perf_value_iterations[iter_to_set - 1].add_measurement( 414 perf_value_dict) 415 return perf_value_iterations 416