# Copyright (c) 2011 The Chromium OS Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """The experiment file module. It manages the input file of crosperf.""" from __future__ import print_function import os.path import re from settings_factory import SettingsFactory class ExperimentFile(object): """Class for parsing the experiment file format. The grammar for this format is: experiment = { _FIELD_VALUE_RE | settings } settings = _OPEN_SETTINGS_RE { _FIELD_VALUE_RE } _CLOSE_SETTINGS_RE Where the regexes are terminals defined below. This results in an format which looks something like: field_name: value settings_type: settings_name { field_name: value field_name: value } """ # Field regex, e.g. "iterations: 3" _FIELD_VALUE_RE = re.compile(r'(\+)?\s*(\w+?)(?:\.(\S+))?\s*:\s*(.*)') # Open settings regex, e.g. "label {" _OPEN_SETTINGS_RE = re.compile(r'(?:([\w.-]+):)?\s*([\w.-]+)\s*{') # Close settings regex. _CLOSE_SETTINGS_RE = re.compile(r'}') def __init__(self, experiment_file, overrides=None): """Construct object from file-like experiment_file. Args: experiment_file: file-like object with text description of experiment. overrides: A settings object that will override fields in other settings. Raises: Exception: if invalid build type or description is invalid. """ self.all_settings = [] self.global_settings = SettingsFactory().GetSettings('global', 'global') self.all_settings.append(self.global_settings) self._Parse(experiment_file) for settings in self.all_settings: settings.Inherit() settings.Validate() if overrides: settings.Override(overrides) def GetSettings(self, settings_type): """Return nested fields from the experiment file.""" res = [] for settings in self.all_settings: if settings.settings_type == settings_type: res.append(settings) return res def GetGlobalSettings(self): """Return the global fields from the experiment file.""" return self.global_settings def _ParseField(self, reader): """Parse a key/value field.""" line = reader.CurrentLine().strip() match = ExperimentFile._FIELD_VALUE_RE.match(line) append, name, _, text_value = match.groups() return (name, text_value, append) def _ParseSettings(self, reader): """Parse a settings block.""" line = reader.CurrentLine().strip() match = ExperimentFile._OPEN_SETTINGS_RE.match(line) settings_type = match.group(1) if settings_type is None: settings_type = '' settings_name = match.group(2) settings = SettingsFactory().GetSettings(settings_name, settings_type) settings.SetParentSettings(self.global_settings) while reader.NextLine(): line = reader.CurrentLine().strip() if not line: continue elif ExperimentFile._FIELD_VALUE_RE.match(line): field = self._ParseField(reader) settings.SetField(field[0], field[1], field[2]) elif ExperimentFile._CLOSE_SETTINGS_RE.match(line): return settings raise EOFError('Unexpected EOF while parsing settings block.') def _Parse(self, experiment_file): """Parse experiment file and create settings.""" reader = ExperimentFileReader(experiment_file) settings_names = {} try: while reader.NextLine(): line = reader.CurrentLine().strip() if not line: continue elif ExperimentFile._OPEN_SETTINGS_RE.match(line): new_settings = self._ParseSettings(reader) if new_settings.name in settings_names: raise SyntaxError( "Duplicate settings name: '%s'." % new_settings.name) settings_names[new_settings.name] = True self.all_settings.append(new_settings) elif ExperimentFile._FIELD_VALUE_RE.match(line): field = self._ParseField(reader) self.global_settings.SetField(field[0], field[1], field[2]) else: raise IOError('Unexpected line.') except Exception, err: raise RuntimeError('Line %d: %s\n==> %s' % (reader.LineNo(), str(err), reader.CurrentLine(False))) def Canonicalize(self): """Convert parsed experiment file back into an experiment file.""" res = '' board = '' for field_name in self.global_settings.fields: field = self.global_settings.fields[field_name] if field.assigned: res += '%s: %s\n' % (field.name, field.GetString()) if field.name == 'board': board = field.GetString() res += '\n' for settings in self.all_settings: if settings.settings_type != 'global': res += '%s: %s {\n' % (settings.settings_type, settings.name) for field_name in settings.fields: field = settings.fields[field_name] if field.assigned: res += '\t%s: %s\n' % (field.name, field.GetString()) if field.name == 'chromeos_image': real_file = ( os.path.realpath(os.path.expanduser(field.GetString()))) if real_file != field.GetString(): res += '\t#actual_image: %s\n' % real_file if field.name == 'build': chromeos_root_field = settings.fields['chromeos_root'] if chromeos_root_field: chromeos_root = chromeos_root_field.GetString() value = field.GetString() autotest_field = settings.fields['autotest_path'] autotest_path = '' if autotest_field.assigned: autotest_path = autotest_field.GetString() image_path, autotest_path = settings.GetXbuddyPath( value, autotest_path, board, chromeos_root, 'quiet') res += '\t#actual_image: %s\n' % image_path if not autotest_field.assigned: res += '\t#actual_autotest_path: %s\n' % autotest_path res += '}\n\n' return res class ExperimentFileReader(object): """Handle reading lines from an experiment file.""" def __init__(self, file_object): self.file_object = file_object self.current_line = None self.current_line_no = 0 def CurrentLine(self, strip_comment=True): """Return the next line from the file, without advancing the iterator.""" if strip_comment: return self._StripComment(self.current_line) return self.current_line def NextLine(self, strip_comment=True): """Advance the iterator and return the next line of the file.""" self.current_line_no += 1 self.current_line = self.file_object.readline() return self.CurrentLine(strip_comment) def _StripComment(self, line): """Strip comments starting with # from a line.""" if '#' in line: line = line[:line.find('#')] + line[-1] return line def LineNo(self): """Return the current line number.""" return self.current_line_no