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