Skip to content

Commit

Permalink
Write help and trace messages to stderr (google#54)
Browse files Browse the repository at this point in the history
* Change test helper `assertOutputMatches` to track stdout and stderr + adds `assertRaisesRegex`
* Write help text to stderr
* Add some test cases for output matching
  • Loading branch information
jtratner authored and dbieber committed Apr 11, 2017
1 parent 8eb6aa5 commit b76f1c3
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 28 deletions.
21 changes: 14 additions & 7 deletions fire/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,26 +122,33 @@ def Fire(component=None, command=None, name=None):
if help_flag in component_trace.elements[-1].args:
command = '{cmd} -- --help'.format(cmd=component_trace.GetCommand())
print(('WARNING: The proper way to show help is {cmd}.\n'
'Showing help anyway.\n').format(cmd=pipes.quote(command)))
'Showing help anyway.\n').format(cmd=pipes.quote(command)),
file=sys.stderr)

print('Fire trace:\n{trace}\n'.format(trace=component_trace))
print('Fire trace:\n{trace}\n'.format(trace=component_trace),
file=sys.stderr)
result = component_trace.GetResult()
print(
helputils.HelpString(result, component_trace, component_trace.verbose))
helputils.HelpString(result, component_trace, component_trace.verbose),
file=sys.stderr)
raise FireExit(2, component_trace)
elif component_trace.show_trace and component_trace.show_help:
print('Fire trace:\n{trace}\n'.format(trace=component_trace))
print('Fire trace:\n{trace}\n'.format(trace=component_trace),
file=sys.stderr)
result = component_trace.GetResult()
print(
helputils.HelpString(result, component_trace, component_trace.verbose))
helputils.HelpString(result, component_trace, component_trace.verbose),
file=sys.stderr)
raise FireExit(0, component_trace)
elif component_trace.show_trace:
print('Fire trace:\n{trace}'.format(trace=component_trace))
print('Fire trace:\n{trace}'.format(trace=component_trace),
file=sys.stderr)
raise FireExit(0, component_trace)
elif component_trace.show_help:
result = component_trace.GetResult()
print(
helputils.HelpString(result, component_trace, component_trace.verbose))
helputils.HelpString(result, component_trace, component_trace.verbose),
file=sys.stderr)
raise FireExit(0, component_trace)
else:
_PrintResult(component_trace, verbose=component_trace.verbose)
Expand Down
4 changes: 2 additions & 2 deletions fire/core_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,9 @@ def testFireErrorMultipleValues(self):
self.assertIsNotNone(error)

def testPrintEmptyDict(self):
with self.assertStdoutMatches('{}'):
with self.assertOutputMatches(stdout='{}', stderr=None):
core.Fire(tc.EmptyDictOutput, 'totally_empty')
with self.assertStdoutMatches('{}'):
with self.assertOutputMatches(stdout='{}', stderr=None):
core.Fire(tc.EmptyDictOutput, 'nothing_printable')


Expand Down
57 changes: 38 additions & 19 deletions fire/testutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,44 @@ class BaseTestCase(unittest.TestCase):
"""Shared test case for Python Fire tests."""

@contextlib.contextmanager
def assertStdoutMatches(self, regexp):
"""Asserts that the context generates stdout matching regexp."""
stdout = six.StringIO()
with mock.patch.object(sys, 'stdout', stdout):
yield
value = stdout.getvalue()
if not re.search(regexp, value, re.DOTALL | re.MULTILINE):
raise AssertionError('Expected %r to match %r' % (value, regexp))
def assertOutputMatches(self, stdout='.*', stderr='.*', capture=True):
"""Asserts that the context generates stdout and stderr matching regexps.
Note: If wrapped code raises an exception, stdout and stderr will not be
checked.
Args:
stdout: (str) regexp to match against stdout (None will check no stdout)
stderr: (str) regexp to match against stderr (None will check no stderr)
capture: (bool, default True) do not bubble up stdout or stderr
Yields:
Yields to the wrapped context.
"""
stdout_fp = six.StringIO()
stderr_fp = six.StringIO()
try:
with mock.patch.object(sys, 'stdout', stdout_fp):
with mock.patch.object(sys, 'stderr', stderr_fp):
yield
finally:
if not capture:
sys.stdout.write(stdout_fp.getvalue())
sys.stderr.write(stderr_fp.getvalue())

for name, regexp, fp in [('stdout', stdout, stdout_fp),
('stderr', stderr, stderr_fp)]:
value = fp.getvalue()
if regexp is None:
if value:
raise AssertionError('%s: Expected no output. Got: %r' %
(name, value))
else:
if not re.search(regexp, value, re.DOTALL | re.MULTILINE):
raise AssertionError('%s: Expected %r to match %r' %
(name, value, regexp))

@contextlib.contextmanager
def assertRaisesFireExit(self, code, regexp=None):
def assertRaisesFireExit(self, code, regexp='.*'):
"""Asserts that a FireExit error is raised in the context.
Allows tests to check that Fire's wrapper around SystemExit is raised
Expand All @@ -56,23 +83,15 @@ def assertRaisesFireExit(self, code, regexp=None):
Yields:
Yields to the wrapped context.
"""
if regexp is None:
regexp = '.*'
with self.assertRaises(core.FireExit):
stdout = six.StringIO()
with mock.patch.object(sys, 'stdout', stdout):
with self.assertOutputMatches(stderr=regexp):
with self.assertRaises(core.FireExit):
try:
yield
except core.FireExit as exc:
if exc.code != code:
raise AssertionError('Incorrect exit code: %r != %r' % (exc.code,
code))
self.assertIsInstance(exc.trace, trace.FireTrace)
stdout.flush()
stdout.seek(0)
value = stdout.getvalue()
if not re.search(regexp, value, re.DOTALL | re.MULTILINE):
raise AssertionError('Expected %r to match %r' % (value, regexp))
raise


Expand Down
55 changes: 55 additions & 0 deletions fire/testutils_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Copyright (C) 2017 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Test the test utilities for Fire's tests."""

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import sys

import six

from fire import testutils


class TestTestUtils(testutils.BaseTestCase):
"""Let's get meta."""

def testNoCheckOnException(self):
with self.assertRaises(ValueError):
with self.assertOutputMatches(stdout='blah'):
raise ValueError()

def testCheckStdoutOrStderrNone(self):
with six.assertRaisesRegex(self, AssertionError, 'stdout:'):
with self.assertOutputMatches(stdout=None):
print('blah')

with six.assertRaisesRegex(self, AssertionError, 'stderr:'):
with self.assertOutputMatches(stderr=None):
print('blah', file=sys.stderr)

with six.assertRaisesRegex(self, AssertionError, 'stderr:'):
with self.assertOutputMatches(stdout='apple', stderr=None):
print('apple')
print('blah', file=sys.stderr)

def testCorrectOrderingOfAssertRaises(self):
# Check to make sure FireExit tests are correct
with self.assertOutputMatches(stdout='Yep.*first.*second'):
with self.assertRaises(ValueError):
print('Yep, this is the first line.\nIt should match to the second')
raise ValueError()

0 comments on commit b76f1c3

Please sign in to comment.