gtest_output_test.py 12 KB
Newer Older
shiqian's avatar
shiqian committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#!/usr/bin/env python
#
# Copyright 2008, Google Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
#     * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#     * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following disclaimer
# in the documentation and/or other materials provided with the
# distribution.
#     * Neither the name of Google Inc. nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

"""Tests the text output of Google C++ Testing Framework.

SYNOPSIS
vladlosev's avatar
vladlosev committed
35
       gtest_output_test.py --build_dir=BUILD/DIR --gengolden
36
         # where BUILD/DIR contains the built gtest_output_test_ file.
shiqian's avatar
shiqian committed
37
38
39
40
41
42
       gtest_output_test.py --gengolden
       gtest_output_test.py
"""

__author__ = 'wan@google.com (Zhanyong Wan)'

kosak's avatar
kosak committed
43
import difflib
shiqian's avatar
shiqian committed
44
45
46
import os
import re
import sys
47
import gtest_test_utils
shiqian's avatar
shiqian committed
48
49
50
51


# The flag for generating the golden file
GENGOLDEN_FLAG = '--gengolden'
52
CATCH_EXCEPTIONS_ENV_VAR_NAME = 'GTEST_CATCH_EXCEPTIONS'
shiqian's avatar
shiqian committed
53
54
55

IS_WINDOWS = os.name == 'nt'

56
57
# TODO(vladl@google.com): remove the _lin suffix.
GOLDEN_NAME = 'gtest_output_test_golden_lin.txt'
shiqian's avatar
shiqian committed
58

59
PROGRAM_PATH = gtest_test_utils.GetTestExecutablePath('gtest_output_test_')
60
61

# At least one command we exercise must not have the
kosak's avatar
kosak committed
62
# 'internal_skip_environment_and_ad_hoc_tests' argument.
63
64
65
66
COMMAND_LIST_TESTS = ({}, [PROGRAM_PATH, '--gtest_list_tests'])
COMMAND_WITH_COLOR = ({}, [PROGRAM_PATH, '--gtest_color=yes'])
COMMAND_WITH_TIME = ({}, [PROGRAM_PATH,
                          '--gtest_print_time',
kosak's avatar
kosak committed
67
                          'internal_skip_environment_and_ad_hoc_tests',
68
69
70
71
                          '--gtest_filter=FatalFailureTest.*:LoggingTest.*'])
COMMAND_WITH_DISABLED = (
    {}, [PROGRAM_PATH,
         '--gtest_also_run_disabled_tests',
kosak's avatar
kosak committed
72
         'internal_skip_environment_and_ad_hoc_tests',
73
74
75
76
         '--gtest_filter=*DISABLED_*'])
COMMAND_WITH_SHARDING = (
    {'GTEST_SHARD_INDEX': '1', 'GTEST_TOTAL_SHARDS': '2'},
    [PROGRAM_PATH,
kosak's avatar
kosak committed
77
     'internal_skip_environment_and_ad_hoc_tests',
78
     '--gtest_filter=PassingTest.*'])
shiqian's avatar
shiqian committed
79

80
GOLDEN_PATH = os.path.join(gtest_test_utils.GetSourceDir(), GOLDEN_NAME)
shiqian's avatar
shiqian committed
81

82

shiqian's avatar
shiqian committed
83
84
85
86
87
88
def ToUnixLineEnding(s):
  """Changes all Windows/Mac line endings in s to UNIX line endings."""

  return s.replace('\r\n', '\n').replace('\r', '\n')


89
def RemoveLocations(test_output):
shiqian's avatar
shiqian committed
90
91
92
  """Removes all file location info from a Google Test program's output.

  Args:
93
       test_output:  the output of a Google Test program.
shiqian's avatar
shiqian committed
94
95
96

  Returns:
       output with all file location info (in the form of
97
98
       'DIRECTORY/FILE_NAME:LINE_NUMBER: 'or
       'DIRECTORY\\FILE_NAME(LINE_NUMBER): ') replaced by
shiqian's avatar
shiqian committed
99
100
101
       'FILE_NAME:#: '.
  """

102
  return re.sub(r'.*[/\\](.+)(\:\d+|\(\d+\))\: ', r'\1:#: ', test_output)
shiqian's avatar
shiqian committed
103
104


105
def RemoveStackTraceDetails(output):
shiqian's avatar
shiqian committed
106
107
108
109
110
111
112
  """Removes all stack traces from a Google Test program's output."""

  # *? means "find the shortest string that matches".
  return re.sub(r'Stack trace:(.|\n)*?\n\n',
                'Stack trace: (omitted)\n\n', output)


113
114
115
116
117
118
119
def RemoveStackTraces(output):
  """Removes all traces of stack traces from a Google Test program's output."""

  # *? means "find the shortest string that matches".
  return re.sub(r'Stack trace:(.|\n)*?\n\n', '', output)


120
121
122
123
124
125
def RemoveTime(output):
  """Removes all time information from a Google Test program's output."""

  return re.sub(r'\(\d+ ms', '(? ms', output)


126
127
128
129
130
131
132
133
134
135
136
137
138
139
def RemoveTypeInfoDetails(test_output):
  """Removes compiler-specific type info from Google Test program's output.

  Args:
       test_output:  the output of a Google Test program.

  Returns:
       output with type information normalized to canonical form.
  """

  # some compilers output the name of type 'unsigned int' as 'unsigned'
  return re.sub(r'unsigned int', 'unsigned', test_output)


140
141
142
143
144
145
146
147
148
149
150
151
152
153
def NormalizeToCurrentPlatform(test_output):
  """Normalizes platform specific output details for easier comparison."""

  if IS_WINDOWS:
    # Removes the color information that is not present on Windows.
    test_output = re.sub('\x1b\\[(0;3\d)?m', '', test_output)
    # Changes failure message headers into the Windows format.
    test_output = re.sub(r': Failure\n', r': error: ', test_output)
    # Changes file(line_number) to file:line_number.
    test_output = re.sub(r'((\w|\.)+)\((\d+)\):', r'\1:\3:', test_output)

  return test_output


154
155
156
def RemoveTestCounts(output):
  """Removes test counts from a Google Test program's output."""

157
  output = re.sub(r'\d+ tests?, listed below',
158
159
160
                  '? tests, listed below', output)
  output = re.sub(r'\d+ FAILED TESTS',
                  '? FAILED TESTS', output)
161
  output = re.sub(r'\d+ tests? from \d+ test cases?',
162
                  '? tests from ? test cases', output)
163
  output = re.sub(r'\d+ tests? from ([a-zA-Z_])',
164
                  r'? tests from \1', output)
165
  return re.sub(r'\d+ tests?\.', '? tests.', output)
166
167


168
def RemoveMatchingTests(test_output, pattern):
169
  """Removes output of specified tests from a Google Test program's output.
170

171
172
  This function strips not only the beginning and the end of a test but also
  all output in between.
173
174
175

  Args:
    test_output:       A string containing the test output.
176
177
    pattern:           A regex string that matches names of test cases or
                       tests to remove.
178
179

  Returns:
180
    Contents of test_output with tests whose names match pattern removed.
181
182
183
  """

  test_output = re.sub(
184
      r'.*\[ RUN      \] .*%s(.|\n)*?\[(  FAILED  |       OK )\] .*%s.*\n' % (
185
186
187
188
          pattern, pattern),
      '',
      test_output)
  return re.sub(r'.*%s.*\n' % pattern, '', test_output)
189
190


shiqian's avatar
shiqian committed
191
192
193
194
195
def NormalizeOutput(output):
  """Normalizes output (the output of gtest_output_test_.exe)."""

  output = ToUnixLineEnding(output)
  output = RemoveLocations(output)
196
  output = RemoveStackTraceDetails(output)
197
  output = RemoveTime(output)
shiqian's avatar
shiqian committed
198
199
200
  return output


201
202
def GetShellCommandOutput(env_cmd):
  """Runs a command in a sub-process, and returns its output in a string.
shiqian's avatar
shiqian committed
203
204

  Args:
205
206
207
    env_cmd: The shell command. A 2-tuple where element 0 is a dict of extra
             environment variables to set, and element 1 is a string with
             the command and any flags.
shiqian's avatar
shiqian committed
208

209
210
  Returns:
    A string with the command's combined standard and diagnostic output.
shiqian's avatar
shiqian committed
211
212
213
  """

  # Spawns cmd in a sub-process, and gets its standard I/O file objects.
214
  # Set and save the environment properly.
215
216
  environ = os.environ.copy()
  environ.update(env_cmd[0])
vladlosev's avatar
vladlosev committed
217
  p = gtest_test_utils.Subprocess(env_cmd[1], env=environ)
shiqian's avatar
shiqian committed
218

219
  return p.output
shiqian's avatar
shiqian committed
220
221


222
def GetCommandOutput(env_cmd):
shiqian's avatar
shiqian committed
223
224
225
226
  """Runs a command and returns its output with all file location
  info stripped off.

  Args:
227
228
229
    env_cmd:  The shell command. A 2-tuple where element 0 is a dict of extra
              environment variables to set, and element 1 is a string with
              the command and any flags.
shiqian's avatar
shiqian committed
230
231
232
  """

  # Disables exception pop-ups on Windows.
233
234
235
236
  environ, cmdline = env_cmd
  environ = dict(environ)  # Ensures we are modifying a copy.
  environ[CATCH_EXCEPTIONS_ENV_VAR_NAME] = '1'
  return NormalizeOutput(GetShellCommandOutput((environ, cmdline)))
237
238
239
240
241
242
243
244
245


def GetOutputOfAllCommands():
  """Returns concatenated output from several representative commands."""

  return (GetCommandOutput(COMMAND_WITH_COLOR) +
          GetCommandOutput(COMMAND_WITH_TIME) +
          GetCommandOutput(COMMAND_WITH_DISABLED) +
          GetCommandOutput(COMMAND_WITH_SHARDING))
shiqian's avatar
shiqian committed
246
247


248
test_list = GetShellCommandOutput(COMMAND_LIST_TESTS)
249
250
SUPPORTS_DEATH_TESTS = 'DeathTest' in test_list
SUPPORTS_TYPED_TESTS = 'TypedTest' in test_list
251
252
253
SUPPORTS_THREADS = 'ExpectFailureWithThreadsTest' in test_list
SUPPORTS_STACK_TRACES = False

254
255
CAN_GENERATE_GOLDEN_FILE = (SUPPORTS_DEATH_TESTS and
                            SUPPORTS_TYPED_TESTS and
256
257
                            SUPPORTS_THREADS and
                            not IS_WINDOWS)
258

259
class GTestOutputTest(gtest_test_utils.TestCase):
260
261
262
263
264
  def RemoveUnsupportedTests(self, test_output):
    if not SUPPORTS_DEATH_TESTS:
      test_output = RemoveMatchingTests(test_output, 'DeathTest')
    if not SUPPORTS_TYPED_TESTS:
      test_output = RemoveMatchingTests(test_output, 'TypedTest')
265
266
      test_output = RemoveMatchingTests(test_output, 'TypedDeathTest')
      test_output = RemoveMatchingTests(test_output, 'TypeParamDeathTest')
267
268
269
270
271
272
273
274
275
276
    if not SUPPORTS_THREADS:
      test_output = RemoveMatchingTests(test_output,
                                        'ExpectFailureWithThreadsTest')
      test_output = RemoveMatchingTests(test_output,
                                        'ScopedFakeTestPartResultReporterTest')
      test_output = RemoveMatchingTests(test_output,
                                        'WorksConcurrently')
    if not SUPPORTS_STACK_TRACES:
      test_output = RemoveStackTraces(test_output)

277
278
    return test_output

shiqian's avatar
shiqian committed
279
  def testOutput(self):
280
    output = GetOutputOfAllCommands()
281

shiqian's avatar
shiqian committed
282
    golden_file = open(GOLDEN_PATH, 'rb')
283
284
285
286
287
    # A mis-configured source control system can cause \r appear in EOL
    # sequences when we read the golden file irrespective of an operating
    # system used. Therefore, we need to strip those \r's from newlines
    # unconditionally.
    golden = ToUnixLineEnding(golden_file.read())
shiqian's avatar
shiqian committed
288
289
    golden_file.close()

290
    # We want the test to pass regardless of certain features being
291
    # supported or not.
292
293
294
295
296

    # We still have to remove type name specifics in all cases.
    normalized_actual = RemoveTypeInfoDetails(output)
    normalized_golden = RemoveTypeInfoDetails(golden)

297
    if CAN_GENERATE_GOLDEN_FILE:
kosak's avatar
kosak committed
298
299
300
301
302
      self.assertEqual(normalized_golden, normalized_actual,
                       '\n'.join(difflib.unified_diff(
                           normalized_golden.split('\n'),
                           normalized_actual.split('\n'),
                           'golden', 'actual')))
303
    else:
304
305
306
307
      normalized_actual = NormalizeToCurrentPlatform(
          RemoveTestCounts(normalized_actual))
      normalized_golden = NormalizeToCurrentPlatform(
          RemoveTestCounts(self.RemoveUnsupportedTests(normalized_golden)))
308

309
310
311
312
313
314
315
316
317
318
      # This code is very handy when debugging golden file differences:
      if os.getenv('DEBUG_GTEST_OUTPUT_TEST'):
        open(os.path.join(
            gtest_test_utils.GetSourceDir(),
            '_gtest_output_test_normalized_actual.txt'), 'wb').write(
                normalized_actual)
        open(os.path.join(
            gtest_test_utils.GetSourceDir(),
            '_gtest_output_test_normalized_golden.txt'), 'wb').write(
                normalized_golden)
319

320
      self.assertEqual(normalized_golden, normalized_actual)
shiqian's avatar
shiqian committed
321
322
323
324


if __name__ == '__main__':
  if sys.argv[1:] == [GENGOLDEN_FLAG]:
325
    if CAN_GENERATE_GOLDEN_FILE:
326
327
328
329
330
      output = GetOutputOfAllCommands()
      golden_file = open(GOLDEN_PATH, 'wb')
      golden_file.write(output)
      golden_file.close()
    else:
331
332
      message = (
          """Unable to write a golden file when compiled in an environment
333
334
335
that does not support all the required features (death tests, typed tests,
and multiple threads).  Please generate the golden file using a binary built
with those features enabled.""")
336
337

      sys.stderr.write(message)
338
      sys.exit(1)
shiqian's avatar
shiqian committed
339
340
  else:
    gtest_test_utils.Main()