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