1#!/usr/bin/env python
2#
3# Copyright 2008, Google Inc.
4# All rights reserved.
5#
6# Redistribution and use in source and binary forms, with or without
7# modification, are permitted provided that the following conditions are
8# met:
9#
10#     * Redistributions of source code must retain the above copyright
11# notice, this list of conditions and the following disclaimer.
12#     * Redistributions in binary form must reproduce the above
13# copyright notice, this list of conditions and the following disclaimer
14# in the documentation and/or other materials provided with the
15# distribution.
16#     * Neither the name of Google Inc. nor the names of its
17# contributors may be used to endorse or promote products derived from
18# this software without specific prior written permission.
19#
20# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31
32r"""Tests the text output of Google C++ Testing and Mocking Framework.
33
34To update the golden file:
35googletest_output_test.py --build_dir=BUILD/DIR --gengolden
36where BUILD/DIR contains the built googletest-output-test_ file.
37googletest_output_test.py --gengolden
38googletest_output_test.py
39"""
40
41import difflib
42import os
43import re
44import sys
45from googletest.test import gtest_test_utils
46
47
48# The flag for generating the golden file
49GENGOLDEN_FLAG = '--gengolden'
50CATCH_EXCEPTIONS_ENV_VAR_NAME = 'GTEST_CATCH_EXCEPTIONS'
51
52# The flag indicating stacktraces are not supported
53NO_STACKTRACE_SUPPORT_FLAG = '--no_stacktrace_support'
54
55IS_LINUX = os.name == 'posix' and os.uname()[0] == 'Linux'
56IS_WINDOWS = os.name == 'nt'
57
58GOLDEN_NAME = 'googletest-output-test-golden-lin.txt'
59
60PROGRAM_PATH = gtest_test_utils.GetTestExecutablePath('googletest-output-test_')
61
62# At least one command we exercise must not have the
63# 'internal_skip_environment_and_ad_hoc_tests' argument.
64COMMAND_LIST_TESTS = ({}, [PROGRAM_PATH, '--gtest_list_tests'])
65COMMAND_WITH_COLOR = ({}, [PROGRAM_PATH, '--gtest_color=yes'])
66COMMAND_WITH_TIME = (
67    {},
68    [
69        PROGRAM_PATH,
70        '--gtest_print_time',
71        'internal_skip_environment_and_ad_hoc_tests',
72        '--gtest_filter=FatalFailureTest.*:LoggingTest.*',
73    ],
74)
75COMMAND_WITH_DISABLED = (
76    {},
77    [
78        PROGRAM_PATH,
79        '--gtest_also_run_disabled_tests',
80        'internal_skip_environment_and_ad_hoc_tests',
81        '--gtest_filter=*DISABLED_*',
82    ],
83)
84COMMAND_WITH_SHARDING = (
85    {'GTEST_SHARD_INDEX': '1', 'GTEST_TOTAL_SHARDS': '2'},
86    [
87        PROGRAM_PATH,
88        'internal_skip_environment_and_ad_hoc_tests',
89        '--gtest_filter=PassingTest.*',
90    ],
91)
92
93GOLDEN_PATH = os.path.join(gtest_test_utils.GetSourceDir(), GOLDEN_NAME)
94
95
96def ToUnixLineEnding(s):
97  """Changes all Windows/Mac line endings in s to UNIX line endings."""
98
99  return s.replace('\r\n', '\n').replace('\r', '\n')
100
101
102def RemoveLocations(test_output):
103  """Removes all file location info from a Google Test program's output.
104
105  Args:
106       test_output:  the output of a Google Test program.
107
108  Returns:
109       output with all file location info (in the form of
110       'DIRECTORY/FILE_NAME:LINE_NUMBER: 'or
111       'DIRECTORY\\FILE_NAME(LINE_NUMBER): ') replaced by
112       'FILE_NAME:#: '.
113  """
114
115  return re.sub(
116      r'.*[/\\]((googletest-output-test_|gtest).cc)(\:\d+|\(\d+\))\: ',
117      r'\1:#: ',
118      test_output,
119  )
120
121
122def RemoveStackTraceDetails(output):
123  """Removes all stack traces from a Google Test program's output."""
124
125  # *? means "find the shortest string that matches".
126  return re.sub(
127      r'Stack trace:(.|\n)*?\n\n', 'Stack trace: (omitted)\n\n', output
128  )
129
130
131def RemoveStackTraces(output):
132  """Removes all traces of stack traces from a Google Test program's output."""
133
134  # *? means "find the shortest string that matches".
135  return re.sub(r'Stack trace:(.|\n)*?\n', '', output)
136
137
138def RemoveTime(output):
139  """Removes all time information from a Google Test program's output."""
140
141  return re.sub(r'\(\d+ ms', '(? ms', output)
142
143
144def RemoveTypeInfoDetails(test_output):
145  """Removes compiler-specific type info from Google Test program's output.
146
147  Args:
148       test_output:  the output of a Google Test program.
149
150  Returns:
151       output with type information normalized to canonical form.
152  """
153
154  # some compilers output the name of type 'unsigned int' as 'unsigned'
155  return re.sub(r'unsigned int', 'unsigned', test_output)
156
157
158def NormalizeToCurrentPlatform(test_output):
159  """Normalizes platform specific output details for easier comparison."""
160
161  if IS_WINDOWS:
162    # Removes the color information that is not present on Windows.
163    test_output = re.sub('\x1b\\[(0;3\d)?m', '', test_output)
164    # Changes failure message headers into the Windows format.
165    test_output = re.sub(r': Failure\n', r': error: ', test_output)
166    # Changes file(line_number) to file:line_number.
167    test_output = re.sub(r'((\w|\.)+)\((\d+)\):', r'\1:\3:', test_output)
168
169  return test_output
170
171
172def RemoveTestCounts(output):
173  """Removes test counts from a Google Test program's output."""
174
175  output = re.sub(r'\d+ tests?, listed below', '? tests, listed below', output)
176  output = re.sub(r'\d+ FAILED TESTS', '? FAILED TESTS', output)
177  output = re.sub(
178      r'\d+ tests? from \d+ test cases?', '? tests from ? test cases', output
179  )
180  output = re.sub(r'\d+ tests? from ([a-zA-Z_])', r'? tests from \1', output)
181  return re.sub(r'\d+ tests?\.', '? tests.', output)
182
183
184def RemoveMatchingTests(test_output, pattern):
185  """Removes output of specified tests from a Google Test program's output.
186
187  This function strips not only the beginning and the end of a test but also
188  all output in between.
189
190  Args:
191    test_output:       A string containing the test output.
192    pattern:           A regex string that matches names of test cases or tests
193      to remove.
194
195  Returns:
196    Contents of test_output with tests whose names match pattern removed.
197  """
198
199  test_output = re.sub(
200      r'.*\[ RUN      \] .*%s(.|\n)*?\[(  FAILED  |       OK )\] .*%s.*\n'
201      % (pattern, pattern),
202      '',
203      test_output,
204  )
205  return re.sub(r'.*%s.*\n' % pattern, '', test_output)
206
207
208def NormalizeOutput(output):
209  """Normalizes output (the output of googletest-output-test_.exe)."""
210
211  output = ToUnixLineEnding(output)
212  output = RemoveLocations(output)
213  output = RemoveStackTraceDetails(output)
214  output = RemoveTime(output)
215  return output
216
217
218def GetShellCommandOutput(env_cmd):
219  """Runs a command in a sub-process, and returns its output in a string.
220
221  Args:
222    env_cmd: The shell command. A 2-tuple where element 0 is a dict of extra
223      environment variables to set, and element 1 is a string with the command
224      and any flags.
225
226  Returns:
227    A string with the command's combined standard and diagnostic output.
228  """
229
230  # Spawns cmd in a sub-process, and gets its standard I/O file objects.
231  # Set and save the environment properly.
232  environ = os.environ.copy()
233  environ.update(env_cmd[0])
234  p = gtest_test_utils.Subprocess(env_cmd[1], env=environ)
235
236  return p.output
237
238
239def GetCommandOutput(env_cmd):
240  """Runs a command and returns output with all file location info stripped off.
241
242  Args:
243    env_cmd:  The shell command. A 2-tuple where element 0 is a dict of extra
244      environment variables to set, and element 1 is a string with the command
245      and any flags.
246
247  Returns:
248    A string with the command's combined standard and diagnostic output. File
249    location info is stripped.
250  """
251
252  # Disables exception pop-ups on Windows.
253  environ, cmdline = env_cmd
254  environ = dict(environ)  # Ensures we are modifying a copy.
255  environ[CATCH_EXCEPTIONS_ENV_VAR_NAME] = '1'
256  return NormalizeOutput(GetShellCommandOutput((environ, cmdline)))
257
258
259def GetOutputOfAllCommands():
260  """Returns concatenated output from several representative commands."""
261
262  return (
263      GetCommandOutput(COMMAND_WITH_COLOR)
264      + GetCommandOutput(COMMAND_WITH_TIME)
265      + GetCommandOutput(COMMAND_WITH_DISABLED)
266      + GetCommandOutput(COMMAND_WITH_SHARDING)
267  )
268
269
270test_list = GetShellCommandOutput(COMMAND_LIST_TESTS)
271SUPPORTS_DEATH_TESTS = 'DeathTest' in test_list
272SUPPORTS_TYPED_TESTS = 'TypedTest' in test_list
273SUPPORTS_THREADS = 'ExpectFailureWithThreadsTest' in test_list
274SUPPORTS_STACK_TRACES = NO_STACKTRACE_SUPPORT_FLAG not in sys.argv
275
276CAN_GENERATE_GOLDEN_FILE = (
277    SUPPORTS_DEATH_TESTS
278    and SUPPORTS_TYPED_TESTS
279    and SUPPORTS_THREADS
280    and SUPPORTS_STACK_TRACES
281)
282
283
284class GTestOutputTest(gtest_test_utils.TestCase):
285
286  def RemoveUnsupportedTests(self, test_output):
287    if not SUPPORTS_DEATH_TESTS:
288      test_output = RemoveMatchingTests(test_output, 'DeathTest')
289    if not SUPPORTS_TYPED_TESTS:
290      test_output = RemoveMatchingTests(test_output, 'TypedTest')
291      test_output = RemoveMatchingTests(test_output, 'TypedDeathTest')
292      test_output = RemoveMatchingTests(test_output, 'TypeParamDeathTest')
293    if not SUPPORTS_THREADS:
294      test_output = RemoveMatchingTests(
295          test_output, 'ExpectFailureWithThreadsTest'
296      )
297      test_output = RemoveMatchingTests(
298          test_output, 'ScopedFakeTestPartResultReporterTest'
299      )
300      test_output = RemoveMatchingTests(test_output, 'WorksConcurrently')
301    if not SUPPORTS_STACK_TRACES:
302      test_output = RemoveStackTraces(test_output)
303
304    return test_output
305
306  def testOutput(self):
307    output = GetOutputOfAllCommands()
308
309    golden_file = open(GOLDEN_PATH, 'rb')
310    # A mis-configured source control system can cause \r appear in EOL
311    # sequences when we read the golden file irrespective of an operating
312    # system used. Therefore, we need to strip those \r's from newlines
313    # unconditionally.
314    golden = ToUnixLineEnding(golden_file.read().decode())
315    golden_file.close()
316
317    # We want the test to pass regardless of certain features being
318    # supported or not.
319
320    # We still have to remove type name specifics in all cases.
321    normalized_actual = RemoveTypeInfoDetails(output)
322    normalized_golden = RemoveTypeInfoDetails(golden)
323
324    if CAN_GENERATE_GOLDEN_FILE:
325      self.assertEqual(
326          normalized_golden,
327          normalized_actual,
328          '\n'.join(
329              difflib.unified_diff(
330                  normalized_golden.split('\n'),
331                  normalized_actual.split('\n'),
332                  'golden',
333                  'actual',
334              )
335          ),
336      )
337    else:
338      normalized_actual = NormalizeToCurrentPlatform(
339          RemoveTestCounts(normalized_actual)
340      )
341      normalized_golden = NormalizeToCurrentPlatform(
342          RemoveTestCounts(self.RemoveUnsupportedTests(normalized_golden))
343      )
344
345      # This code is very handy when debugging golden file differences:
346      if os.getenv('DEBUG_GTEST_OUTPUT_TEST'):
347        open(
348            os.path.join(
349                gtest_test_utils.GetSourceDir(),
350                '_googletest-output-test_normalized_actual.txt',
351            ),
352            'wb',
353        ).write(normalized_actual)
354        open(
355            os.path.join(
356                gtest_test_utils.GetSourceDir(),
357                '_googletest-output-test_normalized_golden.txt',
358            ),
359            'wb',
360        ).write(normalized_golden)
361
362      self.assertEqual(normalized_golden, normalized_actual)
363
364
365if __name__ == '__main__':
366  if NO_STACKTRACE_SUPPORT_FLAG in sys.argv:
367    # unittest.main() can't handle unknown flags
368    sys.argv.remove(NO_STACKTRACE_SUPPORT_FLAG)
369
370  if GENGOLDEN_FLAG in sys.argv:
371    if CAN_GENERATE_GOLDEN_FILE:
372      output = GetOutputOfAllCommands()
373      golden_file = open(GOLDEN_PATH, 'wb')
374      golden_file.write(output.encode())
375      golden_file.close()
376    else:
377      message = """Unable to write a golden file when compiled in an environment
378that does not support all the required features (death tests,
379typed tests, stack traces, and multiple threads).
380Please build this test and generate the golden file using Blaze on Linux."""
381
382      sys.stderr.write(message)
383      sys.exit(1)
384  else:
385    gtest_test_utils.Main()
386