base.py 6.66 KB
Newer Older
1 2 3 4
# Copyright 2017 the V8 project authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

5
from itertools import zip_longest
6

7 8
from ..testproc.base import (
    DROP_RESULT, DROP_OUTPUT, DROP_PASS_OUTPUT, DROP_PASS_STDOUT)
9
from ..local import statusfile
10
from ..testproc.result import Result
11 12


13 14
OUTCOMES_PASS = [statusfile.PASS]
OUTCOMES_FAIL = [statusfile.FAIL]
15
OUTCOMES_TIMEOUT = [statusfile.TIMEOUT]
16
OUTCOMES_PASS_OR_TIMEOUT = [statusfile.PASS, statusfile.TIMEOUT]
17
OUTCOMES_FAIL_OR_TIMEOUT = [statusfile.FAIL, statusfile.TIMEOUT]
18
OUTCOMES_FAIL_OR_PASS = [statusfile.FAIL, statusfile.PASS]
19

20 21

class BaseOutProc(object):
22 23
  def process(self, output, reduction=None):
    has_unexpected_output = self.has_unexpected_output(output)
24 25
    if has_unexpected_output:
      self.regenerate_expected_files(output)
26
    return self._create_result(has_unexpected_output, output, reduction)
27

28 29 30
  def regenerate_expected_files(self, output):
    return

31 32 33
  def has_unexpected_output(self, output):
    return self.get_outcome(output) not in self.expected_outcomes

34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
  def _create_result(self, has_unexpected_output, output, reduction):
    """Creates Result instance. When reduction is passed it tries to drop some
    parts of the result to save memory and time needed to send the result
    across process boundary. None disables reduction and full result is created.
    """
    if reduction == DROP_RESULT:
      return None
    if reduction == DROP_OUTPUT:
      return Result(has_unexpected_output, None)
    if not has_unexpected_output:
      if reduction == DROP_PASS_OUTPUT:
        return Result(has_unexpected_output, None)
      if reduction == DROP_PASS_STDOUT:
        return Result(has_unexpected_output, output.without_text())

    return Result(has_unexpected_output, output)

51 52 53 54 55 56 57 58 59 60 61 62
  def get_outcome(self, output):
    if output.HasCrashed():
      return statusfile.CRASH
    elif output.HasTimedOut():
      return statusfile.TIMEOUT
    elif self._has_failed(output):
      return statusfile.FAIL
    else:
      return statusfile.PASS

  def _has_failed(self, output):
    execution_failed = self._is_failure_output(output)
63
    if self.negative:
64 65 66 67
      return not execution_failed
    return execution_failed

  def _is_failure_output(self, output):
68 69 70 71 72
    return output.exit_code != 0

  @property
  def negative(self):
    return False
73

74 75 76 77 78
  @property
  def expected_outcomes(self):
    raise NotImplementedError()


79 80 81 82
class Negative(object):
  @property
  def negative(self):
    return True
83 84 85 86 87 88 89 90 91 92 93 94


class PassOutProc(BaseOutProc):
  """Output processor optimized for positive tests expected to PASS."""
  def has_unexpected_output(self, output):
    return self.get_outcome(output) != statusfile.PASS

  @property
  def expected_outcomes(self):
    return OUTCOMES_PASS


95 96 97 98 99
class NegPassOutProc(Negative, PassOutProc):
  """Output processor optimized for negative tests expected to PASS"""
  pass


100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
class OutProc(BaseOutProc):
  """Output processor optimized for positive tests with expected outcomes
  different than a single PASS.
  """
  def __init__(self, expected_outcomes):
    self._expected_outcomes = expected_outcomes

  @property
  def expected_outcomes(self):
    return self._expected_outcomes

  # TODO(majeski): Inherit from PassOutProc in case of OUTCOMES_PASS and remove
  # custom get/set state.
  def __getstate__(self):
    d = self.__dict__
    if self._expected_outcomes is OUTCOMES_PASS:
      d = d.copy()
      del d['_expected_outcomes']
    return d

  def __setstate__(self, d):
    if '_expected_outcomes' not in d:
      d['_expected_outcomes'] = OUTCOMES_PASS
    self.__dict__.update(d)

125

126 127
# TODO(majeski): Override __reduce__ to make it deserialize as one instance.
DEFAULT = PassOutProc()
128
DEFAULT_NEGATIVE = NegPassOutProc()
129 130 131


class ExpectedOutProc(OutProc):
132 133 134
  """Output processor that has is_failure_output depending on comparing the
  output with the expected output.
  """
135 136
  def __init__(self, expected_outcomes, expected_filename,
                regenerate_expected_files=False):
137
    super(ExpectedOutProc, self).__init__(expected_outcomes)
138
    self._expected_filename = expected_filename
139
    self._regenerate_expected_files = regenerate_expected_files
140 141

  def _is_failure_output(self, output):
142
    if output.exit_code != 0:
143
      return True
144

145
    with open(self._expected_filename, 'r', encoding='utf-8') as f:
146 147 148
      expected_lines = f.readlines()

    for act_iterator in self._act_block_iterator(output):
149
      for expected, actual in zip_longest(
150 151 152 153 154 155
          self._expected_iterator(expected_lines),
          act_iterator,
          fillvalue=''
      ):
        if expected != actual:
          return True
156
      return False
157

158 159 160 161 162 163 164 165
  def regenerate_expected_files(self, output):
    if not self._regenerate_expected_files:
      return
    lines = output.stdout.splitlines()
    with open(self._expected_filename, 'w') as f:
      for _, line in enumerate(lines):
        f.write(line+'\n')

166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199
  def _act_block_iterator(self, output):
    """Iterates over blocks of actual output lines."""
    lines = output.stdout.splitlines()
    start_index = 0
    found_eqeq = False
    for index, line in enumerate(lines):
      # If a stress test separator is found:
      if line.startswith('=='):
        # Iterate over all lines before a separator except the first.
        if not found_eqeq:
          found_eqeq = True
        else:
          yield self._actual_iterator(lines[start_index:index])
        # The next block of output lines starts after the separator.
        start_index = index + 1
    # Iterate over complete output if no separator was found.
    if not found_eqeq:
      yield self._actual_iterator(lines)

  def _actual_iterator(self, lines):
    return self._iterator(lines, self._ignore_actual_line)

  def _expected_iterator(self, lines):
    return self._iterator(lines, self._ignore_expected_line)

  def _ignore_actual_line(self, line):
    """Ignore empty lines, valgrind output, Android output and trace
    incremental marking output.
    """
    if not line:
      return True
    return (line.startswith('==') or
            line.startswith('**') or
            line.startswith('ANDROID') or
200
            line.startswith('###') or
201
            # Android linker warning.
202
            line.startswith('WARNING: linker:') or
203 204 205 206 207 208 209 210 211 212 213 214 215
            # FIXME(machenbach): The test driver shouldn't try to use slow
            # asserts if they weren't compiled. This fails in optdebug=2.
            line == 'Warning: unknown flag --enable-slow-asserts.' or
            line == 'Try --help for options')

  def _ignore_expected_line(self, line):
    return not line

  def _iterator(self, lines, ignore_predicate):
    for line in lines:
      line = line.strip()
      if not ignore_predicate(line):
        yield line