コード例 #1
0
    def runBenchmark(self, cmd):
        """
        Run one benchmark.

        This includes creating ssh connection to the remote machine
        and running one benchmark. It will remove the benchmark from
        the benchmark list.
        """

        from dispatcher import RunningTask

        # nothing to run
        if not self._benchmarks:
            return None

        self._count -= 1
        name, cat = self._benchmarks.pop()

        ecmd = self.expandSpecialVariables(cmd, name, cat)
        ecmd = expandVariables(ecmd)
        dbg('running: {0}'.format(ecmd))

        p = subprocess.Popen(ecmd,
                             Task.BUFSIZE,
                             shell=True,
                             stdout=subprocess.PIPE,
                             stderr=subprocess.STDOUT)

        return RunningTask(cmd, p, self, name, cat)
コード例 #2
0
  def resolve_variable(self, track, field, i, depth):
    if field == '':
      if self.debug:
        dbg('output of single percent at char %s' % i, depth)
      return EvaluatorAtom('%', False)

    local_field = field
    if not self.case_sensitive:
      local_field = field.upper()
    if self.debug:
      dbg('parsed variable %s at char %s' % (local_field, i), depth)

    resolved = None

    if not self.magic:
      resolved = track.get(local_field)
    else:
      resolved = self.magic_resolve_variable(track, local_field, depth)

    if resolved is None or resolved is False:
      return None

    if self.for_filename:
      resolved = re.sub('[\\\\/:|]', '-', resolved)

    return EvaluatorAtom(resolved, True)
コード例 #3
0
ファイル: sync.py プロジェクト: ufwt/satt
def rsync_tool_runner(tasks):
    satt_log('Synchronizing...')

    if not configs.has_key('sync-cmd'):
        dbg('No sync command')
        return

    add_tasks(tasks)

    d = SyncDispatcher(tasks)
    d.run()
コード例 #4
0
ファイル: sync.py プロジェクト: staticafi/satt
def rsync_tool_runner(tasks):
    satt_log('Synchronizing...')

    if not configs.has_key('sync-cmd'):
        dbg('No sync command')
        return

    add_tasks(tasks)

    d = SyncDispatcher(tasks)
    d.run()
コード例 #5
0
  def parse_fn_arg(self, track, current_fn, current, current_argv, c, i,
      depth=0, offset=0, memory={}):
    next_current = ''

    if self.debug:
      dbg('finished argument %s for function "%s" at char %s' % (
          len(current_argv), current_fn, i), depth)
    # The lazy expression will parse the current buffer if it's ever needed.
    lazy = LazyExpression(
        self, track, current, False, depth + 1, offset, memory)
    return (next_current, lazy)
コード例 #6
0
ファイル: sync.py プロジェクト: staticafi/satt
    def __init__(self, tasks):
        Dispatcher.__init__(self, tasks, SyncReporter(tasks))

        # create remote directory if it does not exists
        # this create remote-dir and remote-dir/satt
        for t in tasks:
            m = '{0}@{1}'.format(configs['ssh-user'], t.getMachine())

            dbg('Creating remote directory on {0}'.format(t.getMachine()))
            subprocess.call(['ssh', m,
                            'mkdir', '-p',
                            '{0}/satt'.format(configs['remote-dir'])])
コード例 #7
0
ファイル: sync.py プロジェクト: ufwt/satt
    def __init__(self, tasks):
        Dispatcher.__init__(self, tasks, SyncReporter(tasks))

        # create remote directory if it does not exists
        # this create remote-dir and remote-dir/satt
        for t in tasks:
            m = '{0}@{1}'.format(configs['ssh-user'], t.getMachine())

            dbg('Creating remote directory on {0}'.format(t.getMachine()))
            subprocess.call([
                'ssh', m, 'mkdir', '-p',
                '{0}/satt'.format(configs['remote-dir'])
            ])
コード例 #8
0
def assign_set(dirpath, path, tasks, should_skip):
    old_dir = os.getcwd()
    epath = expand(dirpath)
    os.chdir(epath)

    # there is not so much of .set files, so there won't be
    # any significant time penalty if we parse the configs
    # here every time
    if configs.configs.has_key('exclude')\
        and configs.configs['exclude'].strip() != '':
        exclude = configs.configs['exclude'].split(',')

        bname = os.path.basename(path)
        for e in exclude:
            e = e.strip()
            if re.search(e, bname):
                print('Skiping {0} benchmarks'.format(bname))
                os.chdir(old_dir)
                return False

    try:
        f = open(path, 'r')
    except OSError as e:
        err("Failed opening set of benchmarks ({0}): {1}".format(
            path, e.strerror))

    num = len(tasks)
    assert num > 0

    cat = path[:-4]

    for line in f:
        line = line.strip()
        if not line:
            continue

        n = 0
        for it in glob.iglob(line):
            bench = ('benchmarks/c/{0}'.format(it), cat)
            if should_skip(bench):
                dbg('Skipping benchmark {0}'.format(it))
            else:
                tasks[n % num].add(bench)
                n += 1

    f.close()
    os.chdir(old_dir)

    return n != 0
コード例 #9
0
def get_benchmarks(files, tasks):
    items = files.split(',')

    skip_known_id = configs.configs['skip-known-benchmarks']
    if skip_known_id != 'no':
        from database_proxy import DatabaseProxy
        dbproxy = DatabaseProxy()
        year_id = dbproxy.getYearID(configs.configs['year'])
        if year_id is None:
            err('Wrong year: {0}'.format(configs.configs['year']))

        try:
            toolid = int(skip_known_id)
        except ValueError:
            err('Invalid tool id for skip-known-benchmarks')

        dbg('Will skip benchmarks from tool {0}'.format(toolid))
        should_skip = lambda x: _should_skip_with_db(dbproxy, toolid, year_id,
                                                     x)
    else:
        should_skip = lambda x: False

    for it in items:
        paths = glob.glob(expand(it))
        if not paths:
            sys.stderr.write('Directory does not exist: {0}\n'.format(it))
            return

        for path in paths:
            # get folder or .set file
            if os.path.isdir(path):
                path = '{0}/c'.format(path)
                assign_set_dir(path, tasks, should_skip)
            elif os.path.isfile(path):
                dirpath = os.path.dirname(path)
                basename = os.path.basename(path)
                assign_set(dirpath, basename, tasks, should_skip)

    # return number of found benchmarks
    num = 0
    for t in tasks:
        num += t.getCount()

    return num
コード例 #10
0
    def run(self):
        """ Dispatch tasks over network and wait for outcomes """

        dbg('[local] Started dispatching benchmarks')

        # take every task and call as many of benchmarks as
        # you are allowed. Later, when a task ends,
        # we will spawn only one new (one new for one done)
        try:
            for task in self._tasks:
                for n in range(0, task.getParallel()):
                    if self._runBenchmark(task) is None:
                        break

            # monitor the tasks
            self._monitorTasks()
        except KeyboardInterrupt:
            self._dontSendResults = True
            self._killTasks()
            satt_log('Stopping...')
コード例 #11
0
  def parse_literal(self, c, i, lookbehind, literal_count, include_quote,
      depth=0, offset=0):
    next_output = ''
    next_literal_state = True
    literal_chars_parsed = 0

    if c == "'":
      if lookbehind == "'" and literal_count == 0:
        if self.debug:
          dbg('output of single quote due to lookbehind at char %s' % i, depth)
        next_output += c
      elif include_quote:
        next_output += c
      if self.debug:
        dbg('leaving literal mode at char %s' % i, depth)
      next_literal_state = False
    else:
      next_output += c
      literal_chars_parsed += 1

    return (next_output, next_literal_state, literal_chars_parsed)
コード例 #12
0
def assign_set_dir(dirpath, tasks, should_skip):
    edirpath = os.path.expanduser(dirpath)
    dbg('Looking for benchmarks in: {0}'.format(edirpath))

    try:
        files = os.listdir(edirpath)
    except OSError as e:
        err('Failed opening dir with benchmarks ({0}): {1}'.format(
            edirpath, e.strerror))

    gotany = False

    for f in files:
        if f[-4:] != '.set':
            continue

        # this path needs to be relative, since it can appear
        # on remote computer
        gotany |= assign_set(dirpath, f, tasks, should_skip)

    if not gotany:
        sys.stderr.write('Warning: Haven\'t found any .set file\n')
コード例 #13
0
    def sendResults(self):
        """ Send results by e-mail to given people """

        if self._dontSendResults:
            return

        emails = []

        try:
            f = open('emails.txt')
            for email in f:
                emails.append(email.strip())
            f.close()
        except IOError as e:
            dbg('Not sending email with results, because'
                ' error occured while processing file with emails')

        # if we don't have any e-mail, we don't have to send anything
        if not emails:
            return

        self._report.sendEmail('localhost',
                               '*****@*****.**', emails)
コード例 #14
0
ファイル: reporter.py プロジェクト: staticafi/satt
    def points(self, ok, res, witness = None, categ=None):
       if res is None:
           return 0

       res = res.lower()

       if res == 'unknown' or res == 'error' or res == 'timeout':
           return self.unknown
       elif res == 'false':
           if ok:
                if witness == 'confirmed' or no_witness_categ(categ):
                    return self.false_correct
                else:
                    return self.unknown
           else:
               return self.false_incorrect
       elif res == 'true':
           if ok:
               return self.true_correct
           else:
               return self.true_incorrect
       else:
           dbg('Unknown result, skipping points')
           return 0
コード例 #15
0
ファイル: reporter.py プロジェクト: ufwt/satt
    def points(self, ok, res, witness=None, categ=None):
        if res is None:
            return 0

        res = res.lower()

        if res == 'unknown' or res == 'error' or res == 'timeout':
            return self.unknown
        elif res == 'false':
            if ok:
                if witness == 'confirmed' or no_witness_categ(categ):
                    return self.false_correct
                else:
                    return self.unknown
            else:
                return self.false_incorrect
        elif res == 'true':
            if ok:
                return self.true_correct
            else:
                return self.true_incorrect
        else:
            dbg('Unknown result, skipping points')
            return 0
コード例 #16
0
ファイル: reporter.py プロジェクト: staticafi/satt
    def sendEmail(self, server, from_addr, to_addrs):
        import smtplib
        from email.mime.text import MIMEText

        time_format = '%Y-%m-%d-%H-%S'
        raw_started_at = strptime(configs.configs['started_at'], time_format)
        started_at = strftime('%a %b %d %H:%M:%S %Y', raw_started_at)
        finished_at = strftime('%a %b %d %H:%M:%S %Y')

        text = """
This is automatically generated message. Do not answer.
=======================================================

Satt on tool {0} started at {1}, finished {2}
with parameters: {3}
on benchmarks from year {4}

Note: {5}

Results:

""".format(configs.configs['tool'],
           started_at,
           finished_at,
           configs.configs['params'],
           configs.configs['year'],
           configs.configs['note'])

        q = """
        SELECT result, is_correct, witness, count(*)
            FROM task_results
            WHERE run_id = {0}
            GROUP BY result, is_correct, witness""".format(self.run_id)

        res = self._db.query(q)
        if not res:
            err('No results stored to db after this run?')

        total = 0
        for row in res:
            result = row[0]
            if result == 'true' or result == 'false':
                if row[1] == 0:
                    result += ' incorrect'
                else:
                    result += ' correct'

            if not row[2] is None:
                text += '{0:<15} (witness {1}): {2}\n'.format(result, row[2], row[3])
            else:
                text += '{0:<15}: {1}\n'.format(result, row[3])

            total += row[3]

        text += '\nTotal number of benchmarks: {0}'.format(total)

        q = """SELECT tool_id FROM task_results
               WHERE run_id = {0}""".format(self.run_id)
        res = self._db.query(q)
        if not res:
            err('Failed querying db for tool\'s id')

        tool_id = res[0][0]

        text += '\n\nYou can check the results here:\n'
        text += 'http://macdui.fi.muni.cz:3000/tools/{0}'.format(tool_id)

        text += '\n\nHave a nice day!\n'

        msg = MIMEText(text)
        msg['Subject'] = 'Satt results from {0}'.format(started_at)
        msg['From'] = from_addr
        msg['To'] = '*****@*****.**'

        s = smtplib.SMTP(server)
        ret = s.sendmail(from_addr, to_addrs, msg.as_string())
        s.quit()

        for r in ret:
            dbg('Failed sending e-mail to {0},'
                'err: {1}'.format(r[0], r[1]))
コード例 #17
0
ファイル: configs.py プロジェクト: staticafi/satt
def parse_command_line():
    from common import err, dbg

    try:
        opts, args = getopt.getopt(sys.argv[1:], '',
                                  ['help', 'machines=', 'benchmarks=',
                                   'no-sync', 'no-db', 'sync=', 'debug',
                                   'year=', 'exclude=', 'params=', 'note=',
                                   'save-new-tasks', 'ignore-duplicates',
                                   'no-email', 'tool-tag=', 'skip-known-benchmarks='])

    except getopt.GetoptError as e:
        err('{0}'.format(str(e)))

    for opt, arg in opts:
        if opt == '--help':
            usage()
            sys.exit(1)
        elif opt == '--machines':
            configs['machines'] = arg
        elif opt == '--benchmarks':
            configs['benchmarks'] = arg
        elif opt == '--no-sync':
            configs['sync'] = 'no'
        elif opt == '--sync':
            configs['sync'] = arg
        elif opt == '--no-db':
            configs['no-db'] = 'yes'
        elif opt == '--save-new-tasks':
            configs['save-new-tasks'] = 'yes'
        elif opt == '--ignore-duplicates':
            configs['ignore-duplicates'] = 'yes'
        elif opt == '--debug':
            configs['debug'] = 'yes'
        elif opt == '--year':
            configs['year'] = arg
        elif opt == '--exclude':
            configs['exclude'] = arg
        elif opt == '--note':
            configs['note'] = arg
        elif opt == '--tool-tag':
            configs['tool-tag'] = arg
        # this switch takes int argument just due
        # to technicall constrains. The tool id we can infer after
        # we got the result, but we don't want to run it at all
        elif opt == '--skip-known-benchmarks':
            configs['skip-known-benchmarks'] = arg
        elif opt == '--no-email':
            configs['send-email'] = 'no'
        elif opt == '--params':
            if configs.has_key('params'):
                # if we have some params, just update
                configs['params'] = params_from_string(arg, configs['params'])
            else:
                configs['params'] = params_from_string(arg)
        else:
            err('Unknown switch {0}'.format(opt))

    if len(args) > 1:
        usage()
        sys.exit(1)
    elif len(args) == 1:
        configs['tool'] = args[0]

    # print debug
    for l, r in configs.items():
        dbg('{0} = {1}'.format(l, r))

    return configs['tool']
コード例 #18
0
ファイル: configs.py プロジェクト: ufwt/satt
def parse_command_line():
    from common import err, dbg

    try:
        opts, args = getopt.getopt(sys.argv[1:], '', [
            'help', 'machines=', 'benchmarks=', 'no-sync', 'no-db', 'sync=',
            'debug', 'year=', 'exclude=', 'params=', 'note=', 'save-new-tasks',
            'ignore-duplicates', 'no-email', 'tool-tag=',
            'skip-known-benchmarks='
        ])

    except getopt.GetoptError as e:
        err('{0}'.format(str(e)))

    for opt, arg in opts:
        if opt == '--help':
            usage()
            sys.exit(1)
        elif opt == '--machines':
            configs['machines'] = arg
        elif opt == '--benchmarks':
            configs['benchmarks'] = arg
        elif opt == '--no-sync':
            configs['sync'] = 'no'
        elif opt == '--sync':
            configs['sync'] = arg
        elif opt == '--no-db':
            configs['no-db'] = 'yes'
        elif opt == '--save-new-tasks':
            configs['save-new-tasks'] = 'yes'
        elif opt == '--ignore-duplicates':
            configs['ignore-duplicates'] = 'yes'
        elif opt == '--debug':
            configs['debug'] = 'yes'
        elif opt == '--year':
            configs['year'] = arg
        elif opt == '--exclude':
            configs['exclude'] = arg
        elif opt == '--note':
            configs['note'] = arg
        elif opt == '--tool-tag':
            configs['tool-tag'] = arg
        # this switch takes int argument just due
        # to technicall constrains. The tool id we can infer after
        # we got the result, but we don't want to run it at all
        elif opt == '--skip-known-benchmarks':
            configs['skip-known-benchmarks'] = arg
        elif opt == '--no-email':
            configs['send-email'] = 'no'
        elif opt == '--params':
            if configs.has_key('params'):
                # if we have some params, just update
                configs['params'] = params_from_string(arg, configs['params'])
            else:
                configs['params'] = params_from_string(arg)
        else:
            err('Unknown switch {0}'.format(opt))

    if len(args) > 1:
        usage()
        sys.exit(1)
    elif len(args) == 1:
        configs['tool'] = args[0]

    # print debug
    for l, r in configs.items():
        dbg('{0} = {1}'.format(l, r))

    return configs['tool']
コード例 #19
0
ファイル: reporter.py プロジェクト: ufwt/satt
    def sendEmail(self, server, from_addr, to_addrs):
        import smtplib
        from email.mime.text import MIMEText

        time_format = '%Y-%m-%d-%H-%S'
        raw_started_at = strptime(configs.configs['started_at'], time_format)
        started_at = strftime('%a %b %d %H:%M:%S %Y', raw_started_at)
        finished_at = strftime('%a %b %d %H:%M:%S %Y')

        text = """
This is automatically generated message. Do not answer.
=======================================================

Satt on tool {0} started at {1}, finished {2}
with parameters: {3}
on benchmarks from year {4}

Note: {5}

Results:

""".format(configs.configs['tool'], started_at, finished_at,
           configs.configs['params'], configs.configs['year'],
           configs.configs['note'])

        q = """
        SELECT result, is_correct, witness, count(*)
            FROM task_results
            WHERE run_id = {0}
            GROUP BY result, is_correct, witness""".format(self.run_id)

        res = self._db.query(q)
        if not res:
            err('No results stored to db after this run?')

        total = 0
        for row in res:
            result = row[0]
            if result == 'true' or result == 'false':
                if row[1] == 0:
                    result += ' incorrect'
                else:
                    result += ' correct'

            if not row[2] is None:
                text += '{0:<15} (witness {1}): {2}\n'.format(
                    result, row[2], row[3])
            else:
                text += '{0:<15}: {1}\n'.format(result, row[3])

            total += row[3]

        text += '\nTotal number of benchmarks: {0}'.format(total)

        q = """SELECT tool_id FROM task_results
               WHERE run_id = {0}""".format(self.run_id)
        res = self._db.query(q)
        if not res:
            err('Failed querying db for tool\'s id')

        tool_id = res[0][0]

        text += '\n\nYou can check the results here:\n'
        text += 'http://macdui.fi.muni.cz:3000/tools/{0}'.format(tool_id)

        text += '\n\nHave a nice day!\n'

        msg = MIMEText(text)
        msg['Subject'] = 'Satt results from {0}'.format(started_at)
        msg['From'] = from_addr
        msg['To'] = '*****@*****.**'

        s = smtplib.SMTP(server)
        ret = s.sendmail(from_addr, to_addrs, msg.as_string())
        s.quit()

        for r in ret:
            dbg('Failed sending e-mail to {0},' 'err: {1}'.format(r[0], r[1]))
コード例 #20
0
 def invoke_function(
     self, track, function_name, function_argv, depth=0, offset=0, memory={}):
   if self.debug:
     dbg('invoking function %s, args %s' % (
         function_name, function_argv), depth)
   return vinvoke(track, function_name, function_argv, memory)
コード例 #21
0
  def magic_resolve_variable(self, track, field, depth):
    field_lower = field.lower()
    if self.debug:
      dbg('checking %s for magic mappings' % field_lower, depth)
    if field_lower in magic_mappings:
      mapping = magic_mappings[field_lower]
      if not mapping:
        dbg('mapping "%s" is not valid' % field_lower, depth)
        return track.get(field)
      else:
        # First try to call it -- the mapping can be a function.
        try:
          magically_resolved = mapping(self, track)
          if self.debug:
            dbg('mapped %s via function mapping' % field_lower, depth)
          return magically_resolved
        except TypeError:
          # That didn't work. It's a list.
          if self.debug:
            dbg('mapping "%s" is not a function' % field_lower, depth)
          for each in mapping:
            if self.debug:
              dbg('attempting to map "%s"' % each, depth)
            if each in track:
              return track.get(each)
            if self.case_sensitive:
              each_lower = each.lower()
              if self.debug:
                dbg('attempting to map "%s"' % each_lower, depth)
              if each_lower in track:
                return track.get(each_lower)

          # Still couldn't find it.
          if self.debug:
            dbg('mapping %s failed to map magic variable' % field_lower, depth)
          return track.get(field)

    if self.debug:
      dbg('mapping %s not found in magic variables' % field_lower, depth)
    return track.get(field)
コード例 #22
0
  def eval(self, track, title_format, conditional=False, depth=0, offset=0,
      memory={}):
    lookbehind = None
    outputting = True
    literal = False
    literal_count = None
    parsing_variable = False
    parsing_function = False
    parsing_function_args = False
    parsing_function_recursive = False
    parsing_conditional = False
    offset_start = 0
    fn_offset_start = 0
    bad_var_char = None
    conditional_parse_count = 0
    evaluation_count = 0
    recursive_lparen_count = 0
    recursive_rparen_count = 0
    output = ''
    current = ''
    current_fn = ''
    current_argv = []

    if self.debug:
      dbg('fresh call to eval(); format="%s" offset=%s' % (
        title_format, offset), depth)

    for i, c in enumerate(title_format):
      if outputting:
        if literal:
          next_output, literal, chars_parsed = self.parse_literal(
              c, i, lookbehind, literal_count, False, depth, offset + i)
          output += next_output
          literal_count += chars_parsed
        else:
          if c == "'":
            if self.debug:
              dbg('entering literal mode at char %s' % i, depth)
            literal = True
            literal_count = 0
          elif c == '%':
            if self.debug:
              dbg('begin parsing variable at char %s' % i, depth)
            if parsing_variable or parsing_function or parsing_conditional:
              raise TitleFormatParseException(
                  "Something went horribly wrong while parsing token '%'")
            outputting = False
            parsing_variable = True
          elif c == '$':
            if self.debug:
              dbg('begin parsing function at char %s' % i, depth)
            if parsing_variable or parsing_function or parsing_conditional:
              raise TitleFormatParseException(
                  "Something went horribly wrong while parsing token '$'")
            outputting = False
            parsing_function = True
            fn_offset_start = i + 1
          elif c == '[':
            if self.debug:
              dbg('begin parsing conditional at char %s' % i, depth)
            if parsing_variable or parsing_function or parsing_conditional:
              raise TitleFormatParseException(
                  "Something went horribly wrong while parsing token '['")
            outputting = False
            parsing_conditional = True
            offset_start = i + 1
          elif c == ']':
            message = self.make_backwards_error(']', '[', offset, i)
            raise TitleFormatParseException(message)
          else:
            output += c
      else:
        if parsing_variable:
          if literal:
            raise TitleFormatParseException(
                'Invalid parse state: Cannot parse names while in literal mode')
          if c == '%':
            evaluated_value = self.resolve_variable(track, current, i, depth)

            if self.debug:
              dbg('value is: %s' % evaluated_value, depth)
            evaluated_value_str = unistr(evaluated_value)
            if evaluated_value or evaluated_value == '':
              if evaluated_value is not True:
                output += evaluated_value_str
              evaluation_count += 1
            elif evaluated_value is not None and evaluated_value is not False:
              # This is the case where no evaluation happened but there is still
              # a string value (that won't output conditionally).
              output += evaluated_value_str
            else:
              output += '?'
            if self.debug:
              dbg('evaluation count is now %s' % evaluation_count, depth)

            current = ''
            outputting = True
            parsing_variable = False
          elif not self.is_valid_var_identifier(c):
            dbg('probably an invalid character: %s at char %i' % (c, i), depth)
            # Only record the first instance.
            if bad_var_char is None:
              bad_var_char = (c, offset + i)

            current += c
          else:
            current += c
        elif parsing_function:
          if literal:
            raise TitleFormatParseException(
                'Invalid parse state: Cannot parse names while in literal mode')
          if c == '(':
            if current == '':
              raise TitleFormatParseException(
                  "Can't call function with no name at char %s" % i)
            if self.debug:
              dbg('parsed function %s at char %s' % (current, i), depth)

            current_fn = current
            current = ''
            parsing_function = False
            parsing_function_args = True
            offset_start = i + 1
          elif c == ')':
            message = self.make_backwards_error(')', '(', offset, i)
            raise TitleFormatParseException(message)
          elif not (c == '_' or c.isalnum()):
            raise TitleFormatParseException(
                "Illegal token '%s' encountered at char %s" % (c, i))
          else:
            current += c
        elif parsing_function_args:
          if not parsing_function_recursive:
            if literal:
              next_current, literal, chars_parsed = self.parse_literal(
                  c, i, lookbehind, literal_count, True, depth, offset + i)
              current += next_current
              literal_count += chars_parsed
            else:
              if c == ')':
                if current != '' or len(current_argv) > 0:
                  current, arg = self.parse_fn_arg(track, current_fn, current,
                      current_argv, c, i, depth, offset + offset_start, memory)
                  current_argv.append(arg)

                if self.debug:
                  dbg('finished parsing function arglist at char %s' % i, depth)

                fn_result = self.invoke_function(
                    track, current_fn, current_argv,
                    depth, offset + fn_offset_start)

                if self.debug:
                  dbg('finished invoking function %s, value: %s' % (
                      current_fn, repr(fn_result)), depth)

                if fn_result is not None and fn_result is not False:
                  str_fn_result = unistr(fn_result)
                  if str_fn_result:
                    output += str_fn_result
                if fn_result:
                  evaluation_count += 1

                if self.debug:
                  dbg('evaluation count is now %s' % evaluation_count, depth)

                current_argv = []
                outputting = True
                parsing_function_args = False
              elif c == "'":
                if self.debug:
                  dbg('entering arglist literal mode at char %s' % i, depth)
                literal = True
                literal_count = 0
                # Include the quotes because we reparse function arguments.
                current += c
              elif c == ',':
                current, arg = self.parse_fn_arg(track, current_fn, current,
                    current_argv, c, i, depth, offset + offset_start, memory)
                current_argv.append(arg)
                offset_start = i + 1
              elif c == '$':
                if self.debug:
                  dbg('stopped evaluation for function in arg at char %s' % i,
                      depth)
                current += c
                parsing_function_recursive = True
                recursive_lparen_count = 0
                recursive_rparen_count = 0
              else:
                current += c
          else: # parsing_function_recursive
            current += c
            if c == '(':
              recursive_lparen_count += 1
            elif c == ')':
              recursive_rparen_count += 1
              if recursive_lparen_count == recursive_rparen_count:
                # Stop skipping evaluation.
                if self.debug:
                  dbg('resumed evaluation at char %s' % i, depth)
                parsing_function_recursive = False
              elif recursive_lparen_count < recursive_rparen_count:
                message = self.make_backwards_error(')', '(', offset, i)
                raise TitleFormatParseException(message)
        elif parsing_conditional:
          if literal:
            current += c
            if c == "'":
              if self.debug:
                dbg('leaving conditional literal mode at char %s' % i, depth)
              literal = False
          else:
            if c == '[':
              if self.debug:
                dbg('found a pending conditional at char %s' % i, depth)
              conditional_parse_count += 1
              if self.debug:
                dbg('conditional parse count now %s' % conditional_parse_count,
                    depth)
              current += c
            elif c == ']':
              if conditional_parse_count > 0:
                if self.debug:
                  dbg('found a terminating conditional at char %s' % i, depth)
                conditional_parse_count -= 1
                if self.debug:
                  dbg('conditional parse count now %s at char %s' % (
                    conditional_parse_count, i), depth)
                current += c
              else:
                if self.debug:
                  dbg('finished parsing conditional at char %s' % i, depth)
                evaluated_value = self.eval(
                    track, current, True, depth + 1, offset + offset_start,
                    memory)

                if self.debug:
                  dbg('value is: %s' % evaluated_value, depth)
                if evaluated_value:
                  output += unistr(evaluated_value)
                  evaluation_count += 1
                if self.debug:
                  dbg('evaluation count is now %s' % evaluation_count, depth)

                current = ''
                conditional_parse_count = 0
                outputting = True
                parsing_conditional = False
            elif c == "'":
              if self.debug:
                dbg('entering conditional literal mode at char %s' % i, depth)
              current += c
              literal = True
            else:
              current += c
        else:
          # Whatever is happening is invalid.
          raise TitleFormatParseException(
              "Invalid title format parse state: Can't handle character " + c)
      lookbehind = c

    # At this point, we have reached the end of the input.
    if outputting:
      if literal:
        message = self.make_unterminated_error('literal', "'", offset, i)
        raise TitleFormatParseException(message)
    else:
      message = None
      if parsing_variable:
        message = self.make_unterminated_error('variable', '%', offset, i)
        if bad_var_char is not None:
          message += " (probably caused by char '%s' in position %s)" % (
              bad_var_char[0], bad_var_char[1])
      elif parsing_function:
        message = self.make_unterminated_error('function', '(', offset, i)
      elif parsing_function_args:
        message = self.make_unterminated_error('function call', ')', offset, i)
      elif parsing_conditional:
        message = self.make_unterminated_error('conditional', ']', offset, i)
      else:
        message = "Invalid title format parse state: Unknown error"

      raise TitleFormatParseException(message)

    if conditional and evaluation_count == 0:
      if self.debug:
        dbg('about to return nothing for output: %s' % output, depth)
      return None

    if depth == 0 and self.for_filename:
      system = platform.system()
      if system == 'Windows' and re.match('^[A-Z]:', output, flags=re.I):
        disk_id = output[0:2]
        output = disk_id + re.sub('[\\\\/:|]', re.escape(os.sep), output[2:])
      else:
        output = re.sub('[\\\\/:|]', re.escape(os.sep), output)
      output = re.sub('[*]', 'x', output)
      output = re.sub('"', "''", output)
      output = re.sub('[?<>]', '_', output)

    result = EvaluatorAtom(output, False if evaluation_count == 0 else True)

    if self.debug:
      dbg('eval() is returning: ' + repr(result), depth)

    return result