1*760c253cSXin Li# -*- coding: utf-8 -*- 2*760c253cSXin Li# Copyright 2011 The ChromiumOS Authors 3*760c253cSXin Li# Use of this source code is governed by a BSD-style license that can be 4*760c253cSXin Li# found in the LICENSE file. 5*760c253cSXin Li 6*760c253cSXin Li"""The experiment file module. It manages the input file of crosperf.""" 7*760c253cSXin Li 8*760c253cSXin Li 9*760c253cSXin Liimport os.path 10*760c253cSXin Liimport re 11*760c253cSXin Li 12*760c253cSXin Lifrom settings_factory import SettingsFactory 13*760c253cSXin Li 14*760c253cSXin Li 15*760c253cSXin Liclass ExperimentFile(object): 16*760c253cSXin Li """Class for parsing the experiment file format. 17*760c253cSXin Li 18*760c253cSXin Li The grammar for this format is: 19*760c253cSXin Li 20*760c253cSXin Li experiment = { _FIELD_VALUE_RE | settings } 21*760c253cSXin Li settings = _OPEN_SETTINGS_RE 22*760c253cSXin Li { _FIELD_VALUE_RE } 23*760c253cSXin Li _CLOSE_SETTINGS_RE 24*760c253cSXin Li 25*760c253cSXin Li Where the regexes are terminals defined below. This results in an format 26*760c253cSXin Li which looks something like: 27*760c253cSXin Li 28*760c253cSXin Li field_name: value 29*760c253cSXin Li settings_type: settings_name { 30*760c253cSXin Li field_name: value 31*760c253cSXin Li field_name: value 32*760c253cSXin Li } 33*760c253cSXin Li """ 34*760c253cSXin Li 35*760c253cSXin Li # Field regex, e.g. "iterations: 3" 36*760c253cSXin Li _FIELD_VALUE_RE = re.compile(r"(\+)?\s*(\w+?)(?:\.(\S+))?\s*:\s*(.*)") 37*760c253cSXin Li # Open settings regex, e.g. "label {" 38*760c253cSXin Li _OPEN_SETTINGS_RE = re.compile(r"(?:([\w.-]+):)?\s*([\w.-]+)\s*{") 39*760c253cSXin Li # Close settings regex. 40*760c253cSXin Li _CLOSE_SETTINGS_RE = re.compile(r"}") 41*760c253cSXin Li 42*760c253cSXin Li def __init__(self, experiment_file, overrides=None): 43*760c253cSXin Li """Construct object from file-like experiment_file. 44*760c253cSXin Li 45*760c253cSXin Li Args: 46*760c253cSXin Li experiment_file: file-like object with text description of experiment. 47*760c253cSXin Li overrides: A settings object that will override fields in other settings. 48*760c253cSXin Li 49*760c253cSXin Li Raises: 50*760c253cSXin Li Exception: if invalid build type or description is invalid. 51*760c253cSXin Li """ 52*760c253cSXin Li self.all_settings = [] 53*760c253cSXin Li self.global_settings = SettingsFactory().GetSettings("global", "global") 54*760c253cSXin Li self.all_settings.append(self.global_settings) 55*760c253cSXin Li 56*760c253cSXin Li self._Parse(experiment_file) 57*760c253cSXin Li 58*760c253cSXin Li for settings in self.all_settings: 59*760c253cSXin Li settings.Inherit() 60*760c253cSXin Li settings.Validate() 61*760c253cSXin Li if overrides: 62*760c253cSXin Li settings.Override(overrides) 63*760c253cSXin Li 64*760c253cSXin Li def GetSettings(self, settings_type): 65*760c253cSXin Li """Return nested fields from the experiment file.""" 66*760c253cSXin Li res = [] 67*760c253cSXin Li for settings in self.all_settings: 68*760c253cSXin Li if settings.settings_type == settings_type: 69*760c253cSXin Li res.append(settings) 70*760c253cSXin Li return res 71*760c253cSXin Li 72*760c253cSXin Li def GetGlobalSettings(self): 73*760c253cSXin Li """Return the global fields from the experiment file.""" 74*760c253cSXin Li return self.global_settings 75*760c253cSXin Li 76*760c253cSXin Li def _ParseField(self, reader): 77*760c253cSXin Li """Parse a key/value field.""" 78*760c253cSXin Li line = reader.CurrentLine().strip() 79*760c253cSXin Li match = ExperimentFile._FIELD_VALUE_RE.match(line) 80*760c253cSXin Li append, name, _, text_value = match.groups() 81*760c253cSXin Li return (name, text_value, append) 82*760c253cSXin Li 83*760c253cSXin Li def _ParseSettings(self, reader): 84*760c253cSXin Li """Parse a settings block.""" 85*760c253cSXin Li line = reader.CurrentLine().strip() 86*760c253cSXin Li match = ExperimentFile._OPEN_SETTINGS_RE.match(line) 87*760c253cSXin Li settings_type = match.group(1) 88*760c253cSXin Li if settings_type is None: 89*760c253cSXin Li settings_type = "" 90*760c253cSXin Li settings_name = match.group(2) 91*760c253cSXin Li settings = SettingsFactory().GetSettings(settings_name, settings_type) 92*760c253cSXin Li settings.SetParentSettings(self.global_settings) 93*760c253cSXin Li 94*760c253cSXin Li while reader.NextLine(): 95*760c253cSXin Li line = reader.CurrentLine().strip() 96*760c253cSXin Li 97*760c253cSXin Li if not line: 98*760c253cSXin Li continue 99*760c253cSXin Li 100*760c253cSXin Li if ExperimentFile._FIELD_VALUE_RE.match(line): 101*760c253cSXin Li field = self._ParseField(reader) 102*760c253cSXin Li settings.SetField(field[0], field[1], field[2]) 103*760c253cSXin Li elif ExperimentFile._CLOSE_SETTINGS_RE.match(line): 104*760c253cSXin Li return settings, settings_type 105*760c253cSXin Li 106*760c253cSXin Li raise EOFError("Unexpected EOF while parsing settings block.") 107*760c253cSXin Li 108*760c253cSXin Li def _Parse(self, experiment_file): 109*760c253cSXin Li """Parse experiment file and create settings.""" 110*760c253cSXin Li reader = ExperimentFileReader(experiment_file) 111*760c253cSXin Li settings_names = {} 112*760c253cSXin Li try: 113*760c253cSXin Li while reader.NextLine(): 114*760c253cSXin Li line = reader.CurrentLine().strip() 115*760c253cSXin Li 116*760c253cSXin Li if not line: 117*760c253cSXin Li continue 118*760c253cSXin Li 119*760c253cSXin Li if ExperimentFile._OPEN_SETTINGS_RE.match(line): 120*760c253cSXin Li new_settings, settings_type = self._ParseSettings(reader) 121*760c253cSXin Li # We will allow benchmarks with duplicated settings name for now. 122*760c253cSXin Li # Further decision will be made when parsing benchmark details in 123*760c253cSXin Li # ExperimentFactory.GetExperiment(). 124*760c253cSXin Li if settings_type != "benchmark": 125*760c253cSXin Li if new_settings.name in settings_names: 126*760c253cSXin Li raise SyntaxError( 127*760c253cSXin Li "Duplicate settings name: '%s'." 128*760c253cSXin Li % new_settings.name 129*760c253cSXin Li ) 130*760c253cSXin Li settings_names[new_settings.name] = True 131*760c253cSXin Li self.all_settings.append(new_settings) 132*760c253cSXin Li elif ExperimentFile._FIELD_VALUE_RE.match(line): 133*760c253cSXin Li field = self._ParseField(reader) 134*760c253cSXin Li self.global_settings.SetField(field[0], field[1], field[2]) 135*760c253cSXin Li else: 136*760c253cSXin Li raise IOError("Unexpected line.") 137*760c253cSXin Li except Exception as err: 138*760c253cSXin Li raise RuntimeError( 139*760c253cSXin Li "Line %d: %s\n==> %s" 140*760c253cSXin Li % (reader.LineNo(), str(err), reader.CurrentLine(False)) 141*760c253cSXin Li ) 142*760c253cSXin Li 143*760c253cSXin Li def Canonicalize(self): 144*760c253cSXin Li """Convert parsed experiment file back into an experiment file.""" 145*760c253cSXin Li res = "" 146*760c253cSXin Li board = "" 147*760c253cSXin Li for field_name in self.global_settings.fields: 148*760c253cSXin Li field = self.global_settings.fields[field_name] 149*760c253cSXin Li if field.assigned: 150*760c253cSXin Li res += "%s: %s\n" % (field.name, field.GetString()) 151*760c253cSXin Li if field.name == "board": 152*760c253cSXin Li board = field.GetString() 153*760c253cSXin Li res += "\n" 154*760c253cSXin Li 155*760c253cSXin Li for settings in self.all_settings: 156*760c253cSXin Li if settings.settings_type != "global": 157*760c253cSXin Li res += "%s: %s {\n" % (settings.settings_type, settings.name) 158*760c253cSXin Li for field_name in settings.fields: 159*760c253cSXin Li field = settings.fields[field_name] 160*760c253cSXin Li if field.assigned: 161*760c253cSXin Li res += "\t%s: %s\n" % (field.name, field.GetString()) 162*760c253cSXin Li if field.name == "chromeos_image": 163*760c253cSXin Li real_file = os.path.realpath( 164*760c253cSXin Li os.path.expanduser(field.GetString()) 165*760c253cSXin Li ) 166*760c253cSXin Li if real_file != field.GetString(): 167*760c253cSXin Li res += "\t#actual_image: %s\n" % real_file 168*760c253cSXin Li if field.name == "build": 169*760c253cSXin Li chromeos_root_field = settings.fields[ 170*760c253cSXin Li "chromeos_root" 171*760c253cSXin Li ] 172*760c253cSXin Li if chromeos_root_field: 173*760c253cSXin Li chromeos_root = chromeos_root_field.GetString() 174*760c253cSXin Li value = field.GetString() 175*760c253cSXin Li autotest_field = settings.fields["autotest_path"] 176*760c253cSXin Li autotest_path = "" 177*760c253cSXin Li if autotest_field.assigned: 178*760c253cSXin Li autotest_path = autotest_field.GetString() 179*760c253cSXin Li debug_field = settings.fields["debug_path"] 180*760c253cSXin Li debug_path = "" 181*760c253cSXin Li if debug_field.assigned: 182*760c253cSXin Li debug_path = autotest_field.GetString() 183*760c253cSXin Li # Do not download the debug symbols since this function is for 184*760c253cSXin Li # canonicalizing experiment file. 185*760c253cSXin Li downlad_debug = False 186*760c253cSXin Li ( 187*760c253cSXin Li image_path, 188*760c253cSXin Li autotest_path, 189*760c253cSXin Li debug_path, 190*760c253cSXin Li ) = settings.GetXbuddyPath( 191*760c253cSXin Li value, 192*760c253cSXin Li autotest_path, 193*760c253cSXin Li debug_path, 194*760c253cSXin Li board, 195*760c253cSXin Li chromeos_root, 196*760c253cSXin Li "quiet", 197*760c253cSXin Li downlad_debug, 198*760c253cSXin Li ) 199*760c253cSXin Li res += "\t#actual_image: %s\n" % image_path 200*760c253cSXin Li if not autotest_field.assigned: 201*760c253cSXin Li res += ( 202*760c253cSXin Li "\t#actual_autotest_path: %s\n" 203*760c253cSXin Li % autotest_path 204*760c253cSXin Li ) 205*760c253cSXin Li if not debug_field.assigned: 206*760c253cSXin Li res += "\t#actual_debug_path: %s\n" % debug_path 207*760c253cSXin Li 208*760c253cSXin Li res += "}\n\n" 209*760c253cSXin Li 210*760c253cSXin Li return res 211*760c253cSXin Li 212*760c253cSXin Li 213*760c253cSXin Liclass ExperimentFileReader(object): 214*760c253cSXin Li """Handle reading lines from an experiment file.""" 215*760c253cSXin Li 216*760c253cSXin Li def __init__(self, file_object): 217*760c253cSXin Li self.file_object = file_object 218*760c253cSXin Li self.current_line = None 219*760c253cSXin Li self.current_line_no = 0 220*760c253cSXin Li 221*760c253cSXin Li def CurrentLine(self, strip_comment=True): 222*760c253cSXin Li """Return the next line from the file, without advancing the iterator.""" 223*760c253cSXin Li if strip_comment: 224*760c253cSXin Li return self._StripComment(self.current_line) 225*760c253cSXin Li return self.current_line 226*760c253cSXin Li 227*760c253cSXin Li def NextLine(self, strip_comment=True): 228*760c253cSXin Li """Advance the iterator and return the next line of the file.""" 229*760c253cSXin Li self.current_line_no += 1 230*760c253cSXin Li self.current_line = self.file_object.readline() 231*760c253cSXin Li return self.CurrentLine(strip_comment) 232*760c253cSXin Li 233*760c253cSXin Li def _StripComment(self, line): 234*760c253cSXin Li """Strip comments starting with # from a line.""" 235*760c253cSXin Li if "#" in line: 236*760c253cSXin Li line = line[: line.find("#")] + line[-1] 237*760c253cSXin Li return line 238*760c253cSXin Li 239*760c253cSXin Li def LineNo(self): 240*760c253cSXin Li """Return the current line number.""" 241*760c253cSXin Li return self.current_line_no 242