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