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