Skip to content

Commit b76f1c3

Browse files
jtratnerdbieber
authored andcommitted
Write help and trace messages to stderr (google#54)
* Change test helper `assertOutputMatches` to track stdout and stderr + adds `assertRaisesRegex` * Write help text to stderr * Add some test cases for output matching
1 parent 8eb6aa5 commit b76f1c3

File tree

4 files changed

+109
-28
lines changed

4 files changed

+109
-28
lines changed

fire/core.py

+14-7
Original file line numberDiff line numberDiff line change
@@ -122,26 +122,33 @@ def Fire(component=None, command=None, name=None):
122122
if help_flag in component_trace.elements[-1].args:
123123
command = '{cmd} -- --help'.format(cmd=component_trace.GetCommand())
124124
print(('WARNING: The proper way to show help is {cmd}.\n'
125-
'Showing help anyway.\n').format(cmd=pipes.quote(command)))
125+
'Showing help anyway.\n').format(cmd=pipes.quote(command)),
126+
file=sys.stderr)
126127

127-
print('Fire trace:\n{trace}\n'.format(trace=component_trace))
128+
print('Fire trace:\n{trace}\n'.format(trace=component_trace),
129+
file=sys.stderr)
128130
result = component_trace.GetResult()
129131
print(
130-
helputils.HelpString(result, component_trace, component_trace.verbose))
132+
helputils.HelpString(result, component_trace, component_trace.verbose),
133+
file=sys.stderr)
131134
raise FireExit(2, component_trace)
132135
elif component_trace.show_trace and component_trace.show_help:
133-
print('Fire trace:\n{trace}\n'.format(trace=component_trace))
136+
print('Fire trace:\n{trace}\n'.format(trace=component_trace),
137+
file=sys.stderr)
134138
result = component_trace.GetResult()
135139
print(
136-
helputils.HelpString(result, component_trace, component_trace.verbose))
140+
helputils.HelpString(result, component_trace, component_trace.verbose),
141+
file=sys.stderr)
137142
raise FireExit(0, component_trace)
138143
elif component_trace.show_trace:
139-
print('Fire trace:\n{trace}'.format(trace=component_trace))
144+
print('Fire trace:\n{trace}'.format(trace=component_trace),
145+
file=sys.stderr)
140146
raise FireExit(0, component_trace)
141147
elif component_trace.show_help:
142148
result = component_trace.GetResult()
143149
print(
144-
helputils.HelpString(result, component_trace, component_trace.verbose))
150+
helputils.HelpString(result, component_trace, component_trace.verbose),
151+
file=sys.stderr)
145152
raise FireExit(0, component_trace)
146153
else:
147154
_PrintResult(component_trace, verbose=component_trace.verbose)

fire/core_test.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,9 @@ def testFireErrorMultipleValues(self):
9999
self.assertIsNotNone(error)
100100

101101
def testPrintEmptyDict(self):
102-
with self.assertStdoutMatches('{}'):
102+
with self.assertOutputMatches(stdout='{}', stderr=None):
103103
core.Fire(tc.EmptyDictOutput, 'totally_empty')
104-
with self.assertStdoutMatches('{}'):
104+
with self.assertOutputMatches(stdout='{}', stderr=None):
105105
core.Fire(tc.EmptyDictOutput, 'nothing_printable')
106106

107107

fire/testutils.py

+38-19
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,44 @@ class BaseTestCase(unittest.TestCase):
3434
"""Shared test case for Python Fire tests."""
3535

3636
@contextlib.contextmanager
37-
def assertStdoutMatches(self, regexp):
38-
"""Asserts that the context generates stdout matching regexp."""
39-
stdout = six.StringIO()
40-
with mock.patch.object(sys, 'stdout', stdout):
41-
yield
42-
value = stdout.getvalue()
43-
if not re.search(regexp, value, re.DOTALL | re.MULTILINE):
44-
raise AssertionError('Expected %r to match %r' % (value, regexp))
37+
def assertOutputMatches(self, stdout='.*', stderr='.*', capture=True):
38+
"""Asserts that the context generates stdout and stderr matching regexps.
39+
40+
Note: If wrapped code raises an exception, stdout and stderr will not be
41+
checked.
42+
43+
Args:
44+
stdout: (str) regexp to match against stdout (None will check no stdout)
45+
stderr: (str) regexp to match against stderr (None will check no stderr)
46+
capture: (bool, default True) do not bubble up stdout or stderr
47+
Yields:
48+
Yields to the wrapped context.
49+
"""
50+
stdout_fp = six.StringIO()
51+
stderr_fp = six.StringIO()
52+
try:
53+
with mock.patch.object(sys, 'stdout', stdout_fp):
54+
with mock.patch.object(sys, 'stderr', stderr_fp):
55+
yield
56+
finally:
57+
if not capture:
58+
sys.stdout.write(stdout_fp.getvalue())
59+
sys.stderr.write(stderr_fp.getvalue())
60+
61+
for name, regexp, fp in [('stdout', stdout, stdout_fp),
62+
('stderr', stderr, stderr_fp)]:
63+
value = fp.getvalue()
64+
if regexp is None:
65+
if value:
66+
raise AssertionError('%s: Expected no output. Got: %r' %
67+
(name, value))
68+
else:
69+
if not re.search(regexp, value, re.DOTALL | re.MULTILINE):
70+
raise AssertionError('%s: Expected %r to match %r' %
71+
(name, value, regexp))
4572

4673
@contextlib.contextmanager
47-
def assertRaisesFireExit(self, code, regexp=None):
74+
def assertRaisesFireExit(self, code, regexp='.*'):
4875
"""Asserts that a FireExit error is raised in the context.
4976
5077
Allows tests to check that Fire's wrapper around SystemExit is raised
@@ -56,23 +83,15 @@ def assertRaisesFireExit(self, code, regexp=None):
5683
Yields:
5784
Yields to the wrapped context.
5885
"""
59-
if regexp is None:
60-
regexp = '.*'
61-
with self.assertRaises(core.FireExit):
62-
stdout = six.StringIO()
63-
with mock.patch.object(sys, 'stdout', stdout):
86+
with self.assertOutputMatches(stderr=regexp):
87+
with self.assertRaises(core.FireExit):
6488
try:
6589
yield
6690
except core.FireExit as exc:
6791
if exc.code != code:
6892
raise AssertionError('Incorrect exit code: %r != %r' % (exc.code,
6993
code))
7094
self.assertIsInstance(exc.trace, trace.FireTrace)
71-
stdout.flush()
72-
stdout.seek(0)
73-
value = stdout.getvalue()
74-
if not re.search(regexp, value, re.DOTALL | re.MULTILINE):
75-
raise AssertionError('Expected %r to match %r' % (value, regexp))
7695
raise
7796

7897

fire/testutils_test.py

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Copyright (C) 2017 Google Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Test the test utilities for Fire's tests."""
16+
17+
from __future__ import absolute_import
18+
from __future__ import division
19+
from __future__ import print_function
20+
21+
import sys
22+
23+
import six
24+
25+
from fire import testutils
26+
27+
28+
class TestTestUtils(testutils.BaseTestCase):
29+
"""Let's get meta."""
30+
31+
def testNoCheckOnException(self):
32+
with self.assertRaises(ValueError):
33+
with self.assertOutputMatches(stdout='blah'):
34+
raise ValueError()
35+
36+
def testCheckStdoutOrStderrNone(self):
37+
with six.assertRaisesRegex(self, AssertionError, 'stdout:'):
38+
with self.assertOutputMatches(stdout=None):
39+
print('blah')
40+
41+
with six.assertRaisesRegex(self, AssertionError, 'stderr:'):
42+
with self.assertOutputMatches(stderr=None):
43+
print('blah', file=sys.stderr)
44+
45+
with six.assertRaisesRegex(self, AssertionError, 'stderr:'):
46+
with self.assertOutputMatches(stdout='apple', stderr=None):
47+
print('apple')
48+
print('blah', file=sys.stderr)
49+
50+
def testCorrectOrderingOfAssertRaises(self):
51+
# Check to make sure FireExit tests are correct
52+
with self.assertOutputMatches(stdout='Yep.*first.*second'):
53+
with self.assertRaises(ValueError):
54+
print('Yep, this is the first line.\nIt should match to the second')
55+
raise ValueError()

0 commit comments

Comments
 (0)