gtest_output_test.py 11.7 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
43
44
45
       gtest_output_test.py --gengolden
       gtest_output_test.py
"""

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

import os
import re
import sys
46
import gtest_test_utils
shiqian's avatar
shiqian committed
47
48
49
50


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

IS_WINDOWS = os.name == 'nt'

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

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

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

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

81

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

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


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

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

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

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


104
def RemoveStackTraceDetails(output):
shiqian's avatar
shiqian committed
105
106
107
108
109
110
111
  """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)


112
113
114
115
116
117
118
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)


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

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


125
126
127
128
129
130
131
132
133
134
135
136
137
138
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)


139
140
141
142
143
144
145
146
147
148
149
150
151
152
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


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

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


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

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

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

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

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


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

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


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

  Args:
204
205
206
    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
207

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

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

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


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

  Args:
226
227
228
    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
229
230
231
  """

  # Disables exception pop-ups on Windows.
232
233
234
235
  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)))
236
237
238
239
240
241
242
243
244


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
245
246


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

253
254
CAN_GENERATE_GOLDEN_FILE = (SUPPORTS_DEATH_TESTS and
                            SUPPORTS_TYPED_TESTS and
255
                            SUPPORTS_THREADS)
256
257


258
class GTestOutputTest(gtest_test_utils.TestCase):
259
260
261
262
263
  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')
264
265
      test_output = RemoveMatchingTests(test_output, 'TypedDeathTest')
      test_output = RemoveMatchingTests(test_output, 'TypeParamDeathTest')
266
267
268
269
270
271
272
273
274
275
    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)

276
277
    return test_output

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

shiqian's avatar
shiqian committed
281
    golden_file = open(GOLDEN_PATH, 'rb')
282
283
284
285
286
    # 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
287
288
    golden_file.close()

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

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

296
    if CAN_GENERATE_GOLDEN_FILE:
297
      self.assertEqual(normalized_golden, normalized_actual)
298
    else:
299
300
301
302
      normalized_actual = NormalizeToCurrentPlatform(
          RemoveTestCounts(normalized_actual))
      normalized_golden = NormalizeToCurrentPlatform(
          RemoveTestCounts(self.RemoveUnsupportedTests(normalized_golden)))
303

304
305
306
307
308
309
310
311
312
313
      # 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)
314

315
      self.assertEqual(normalized_golden, normalized_actual)
shiqian's avatar
shiqian committed
316
317
318
319


if __name__ == '__main__':
  if sys.argv[1:] == [GENGOLDEN_FLAG]:
320
    if CAN_GENERATE_GOLDEN_FILE:
321
322
323
324
325
      output = GetOutputOfAllCommands()
      golden_file = open(GOLDEN_PATH, 'wb')
      golden_file.write(output)
      golden_file.close()
    else:
326
327
      message = (
          """Unable to write a golden file when compiled in an environment
328
329
330
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.""")
331
332

      sys.stderr.write(message)
333
      sys.exit(1)
shiqian's avatar
shiqian committed
334
335
  else:
    gtest_test_utils.Main()