diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3c654418..b8ca0285 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -25,6 +25,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* Enhanced autohooks activate cli to show additional information if a autohooks
git hook is already installed
[#30](https://github.com/greenbone/autohooks/pull/30)
+* Added plugin API for additional info status output
+ [#39](https://github.com/greenbone/autohooks/pull/39)
+* Added plugin API for additional message printing
+ [#39](https://github.com/greenbone/autohooks/pull/39)
### Changed
@@ -40,6 +44,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#30](https://github.com/greenbone/autohooks/pull/30)
* A warning is raised during git hook execution if the current mode is different
to the configured mode [#30](https://github.com/greenbone/autohooks/pull/30)
+* Improved output formatting [#39](https://github.com/greenbone/autohooks/pull/39)
### Deprecated
### Fixed
diff --git a/autohooks/api/__init__.py b/autohooks/api/__init__.py
index 0cafa34e..70e54884 100644
--- a/autohooks/api/__init__.py
+++ b/autohooks/api/__init__.py
@@ -17,8 +17,44 @@
import sys
-from autohooks.terminal import ok, fail, error, warning
+from autohooks.terminal import Terminal
+
+__term = None # pylint: disable=invalid-name
+
+__all__ = [
+ 'error',
+ 'fail',
+ 'info',
+ 'ok',
+ 'out',
+ 'warning',
+]
+
+
+def ok(message: str) -> None:
+ __term.ok(message)
+
+
+def fail(message: str) -> None:
+ __term.fail(message)
+
+
+def error(message: str) -> None:
+ __term.error(message)
+
+
+def warning(message: str) -> None:
+ __term.warning(message)
+
+
+def info(message: str) -> None:
+ __term.info(message)
def out(message: str):
- print(message)
+ __term.print(message)
+
+
+def _set_terminal(term: Terminal):
+ global __term # pylint: disable=global-statement, invalid-name
+ __term = term
diff --git a/autohooks/cli/__init__.py b/autohooks/cli/__init__.py
index ec4f27e0..647d9d7e 100644
--- a/autohooks/cli/__init__.py
+++ b/autohooks/cli/__init__.py
@@ -17,6 +17,7 @@
import argparse
+from autohooks.terminal import Terminal
from autohooks.settings import Mode
from autohooks.version import get_version
@@ -60,10 +61,11 @@ def main():
if not args.command:
parser.print_usage()
+ term = Terminal()
if args.command == 'activate':
- install_hooks(args)
+ install_hooks(term, args)
elif args.command == 'check':
- check_hooks()
+ check_hooks(term)
if __name__ == "__main__":
diff --git a/autohooks/cli/activate.py b/autohooks/cli/activate.py
index d1f77580..fc069653 100644
--- a/autohooks/cli/activate.py
+++ b/autohooks/cli/activate.py
@@ -20,39 +20,39 @@
from autohooks.config import (
load_config_from_pyproject_toml,
get_pyproject_toml_path,
- AUTOHOOKS_SECTION,
)
from autohooks.hooks import PreCommitHook
from autohooks.settings import Mode
-from autohooks.terminal import ok, warning, info
+from autohooks.terminal import Terminal
-def install_hooks(args: Namespace) -> None:
+def install_hooks(term: Terminal, args: Namespace) -> None:
pre_commit_hook = PreCommitHook()
pyproject_toml = get_pyproject_toml_path()
config = load_config_from_pyproject_toml(pyproject_toml)
if pre_commit_hook.exists() and not args.force:
- ok(
- 'pre-commit hook is already installed at {}.'.format(
+ term.ok(
+ 'autohooks pre-commit hook is already installed at {}.'.format(
str(pre_commit_hook)
)
)
- info(
- "Run 'autohooks activate --force' to override the current "
- "installed pre-commit hook."
- )
- info(
- "Run 'autohooks check' to validate the current status of "
- "the installed pre-commit hook."
- )
+ with term.indent():
+ term.print()
+ term.info(
+ "Run 'autohooks activate --force' to override the current "
+ "installed pre-commit hook."
+ )
+ term.info(
+ "Run 'autohooks check' to validate the current status of "
+ "the installed pre-commit hook."
+ )
else:
if not config.is_autohooks_enabled():
- warning(
- 'Warning: autohooks is not enabled in your {} file. Please add '
- 'a "{}" section. Run autohooks check for more details.'.format(
- str(pyproject_toml), AUTOHOOKS_SECTION
- )
+ term.warning(
+ 'autohooks is not enabled in your {} file. '
+ 'Run \'autohooks check\' for more '
+ 'details.'.format(str(pyproject_toml))
)
if args.mode:
@@ -62,8 +62,8 @@ def install_hooks(args: Namespace) -> None:
pre_commit_hook.write(mode=mode)
- ok(
- 'pre-commit hook installed at {} using {} mode'.format(
+ term.ok(
+ 'autohooks pre-commit hook installed at {} using {} mode.'.format(
str(pre_commit_hook), str(mode.get_effective_mode())
)
)
diff --git a/autohooks/cli/check.py b/autohooks/cli/check.py
index 35bd0c15..ed87a459 100644
--- a/autohooks/cli/check.py
+++ b/autohooks/cli/check.py
@@ -34,34 +34,47 @@
from autohooks.settings import Mode
-from autohooks.terminal import ok, error, warning
+from autohooks.terminal import Terminal
-def check_hooks() -> None:
+def check_hooks(term: Terminal) -> None:
pre_commit_hook = PreCommitHook()
+ hook_mode = pre_commit_hook.read_mode()
- check_pre_commit_hook(pre_commit_hook)
+ check_pre_commit_hook(term, pre_commit_hook, hook_mode)
pyproject_toml = get_pyproject_toml_path()
- check_config(pyproject_toml, pre_commit_hook.read_mode())
+ check_config(term, pyproject_toml, pre_commit_hook, hook_mode)
-def check_pre_commit_hook(pre_commit_hook: PreCommitHook) -> None:
+def check_pre_commit_hook(
+ term: Terminal, pre_commit_hook: PreCommitHook, hook_mode: Mode,
+) -> None:
if pre_commit_hook.exists():
if pre_commit_hook.is_autohooks_pre_commit_hook():
- ok('autohooks pre-commit hook is active.')
+ term.ok('autohooks pre-commit hook is active.')
if pre_commit_hook.is_current_autohooks_pre_commit_hook():
- ok('autohooks pre-commit hook is up-to-date.')
+ term.ok('autohooks pre-commit hook is up-to-date.')
else:
- warning(
+ term.warning(
'autohooks pre-commit hook is outdated. Please run '
'\'autohooks activate --force\' to update your pre-commit '
'hook.'
)
+
+ hook_mode = pre_commit_hook.read_mode()
+ if hook_mode == Mode.UNKNOWN:
+ term.warning(
+ 'Unknown autohooks mode in {}. Falling back to "{}" '
+ 'mode.'.format(
+ str(pre_commit_hook),
+ str(hook_mode.get_effective_mode()),
+ )
+ )
else:
- error(
+ term.error(
'autohooks pre-commit hook is not active. But a different '
'pre-commit hook has been found at {}.'.format(
str(pre_commit_hook)
@@ -69,39 +82,66 @@ def check_pre_commit_hook(pre_commit_hook: PreCommitHook) -> None:
)
else:
- error(
+ term.error(
'autohooks pre-commit hook not active. Please run \'autohooks '
'activate\'.'
)
-def check_config(pyproject_toml: Path, hook_mode: Mode) -> None:
+def check_config(
+ term: Terminal,
+ pyproject_toml: Path,
+ pre_commit_hook: PreCommitHook,
+ hook_mode: Mode,
+) -> None:
if not pyproject_toml.exists():
- error(
+ term.error(
'Missing {} file. Please add a pyproject.toml file and include '
'a "{}" section.'.format(str(pyproject_toml), AUTOHOOKS_SECTION)
)
else:
config = load_config_from_pyproject_toml(pyproject_toml)
if not config.is_autohooks_enabled():
- error(
+ term.error(
'autohooks is not enabled in your {} file. Please add '
'a "{}" section.'.format(str(pyproject_toml), AUTOHOOKS_SECTION)
)
else:
- if config.get_mode() != hook_mode:
- warning(
- 'autohooks mode in pre-commit hook ("{}") differs from '
- 'mode in {} file ("{}")'.format(
+ config_mode = config.get_mode()
+ if config_mode == Mode.UNDEFINED:
+ term.warning(
+ 'autohooks mode is not defined in {}.'.format(
+ str(pyproject_toml)
+ )
+ )
+ elif config_mode == Mode.UNKNOWN:
+ term.warning(
+ 'Unknown autohooks mode in {}.'.format(str(pyproject_toml))
+ )
+
+ if (
+ config_mode.get_effective_mode()
+ != hook_mode.get_effective_mode()
+ ):
+ term.warning(
+ 'autohooks mode "{}" in pre-commit hook {} differs from '
+ 'mode "{}" in {}.'.format(
str(hook_mode),
+ str(pre_commit_hook),
+ str(config_mode),
str(pyproject_toml),
- str(config.get_mode()),
)
)
+ term.info(
+ 'Using autohooks mode "{}".'.format(
+ str(hook_mode.get_effective_mode())
+ )
+ )
+
plugins = config.get_pre_commit_script_names()
if not plugins:
- error(
+ term.error(
'No autohooks plugin is activated in {} for your pre '
'commit hook. Please add a '
'"pre-commit = [plugin1, plugin2]" '
@@ -113,7 +153,7 @@ def check_config(pyproject_toml: Path, hook_mode: Mode) -> None:
try:
plugin = load_plugin(name)
if not has_precommit_function(plugin):
- error(
+ term.error(
'Plugin "{}" has no precommit function. '
'The function is required to run the '
'plugin as git pre commit hook.'.format(
@@ -121,19 +161,19 @@ def check_config(pyproject_toml: Path, hook_mode: Mode) -> None:
)
)
elif not has_precommit_parameters(plugin):
- warning(
+ term.warning(
'Plugin "{}" uses a deprecated signature '
'for its precommit function. It is missing '
'the **kwargs parameter.'.format(name)
)
else:
- ok(
+ term.ok(
'Plugin "{}" active and loadable.'.format(
name
)
)
except ImportError as e:
- error(
+ term.error(
'"{}" is not a valid autohooks '
'plugin. {}'.format(name, e)
)
diff --git a/autohooks/precommit/run.py b/autohooks/precommit/run.py
index bb0e2b47..bfa1796f 100644
--- a/autohooks/precommit/run.py
+++ b/autohooks/precommit/run.py
@@ -24,10 +24,11 @@
from contextlib import contextmanager
+from autohooks.api import _set_terminal
from autohooks.config import load_config_from_pyproject_toml
from autohooks.hooks import PreCommitHook
from autohooks.settings import Mode
-from autohooks.terminal import error, warning
+from autohooks.terminal import Terminal
from autohooks.utils import get_project_autohooks_plugins_path
@@ -58,35 +59,40 @@ def has_precommit_parameters(plugin: ModuleType) -> bool:
return bool(signature.parameters)
-def check_hook_is_current(pre_commit_hook: PreCommitHook):
+def check_hook_is_current(
+ term: Terminal, pre_commit_hook: PreCommitHook
+) -> None:
if not pre_commit_hook.is_current_autohooks_pre_commit_hook():
- warning(
+ term.warning(
'autohooks pre-commit hook is outdated. Please run '
'\'autohooks activate --force\' to update your pre-commit '
'hook.'
)
-def check_hook_mode(config_mode: Mode, hook_mode: Mode) -> None:
- if config_mode != hook_mode:
- warning(
- 'autohooks mode in pre-commit hook ("{}") differs from '
- 'mode in pyproject.toml file ("{}"). Please run \'autohooks '
- 'activate --force\' to enforce {} mode.'.format(
- str(hook_mode), str(config_mode), str(config_mode)
+def check_hook_mode(term: Terminal, config_mode: Mode, hook_mode: Mode) -> None:
+ if config_mode.get_effective_mode() != hook_mode.get_effective_mode():
+ term.warning(
+ 'autohooks mode "{}" in pre-commit hook differs from '
+ 'mode "{}" in pyproject.toml file.'.format(
+ str(hook_mode), str(config_mode)
)
)
def run() -> int:
- print('autohooks => pre-commit')
+ term = Terminal()
+
+ _set_terminal(term)
config = load_config_from_pyproject_toml()
pre_commit_hook = PreCommitHook()
- check_hook_is_current(pre_commit_hook)
- check_hook_mode(config.get_mode(), pre_commit_hook.read_mode())
+ check_hook_is_current(term, pre_commit_hook)
+
+ if config.has_autohooks_config():
+ check_hook_mode(term, config.get_mode(), pre_commit_hook.read_mode())
plugins = get_project_autohooks_plugins_path()
plugins_dir_name = str(plugins)
@@ -94,40 +100,47 @@ def run() -> int:
if plugins.is_dir():
sys.path.append(plugins_dir_name)
- with autohooks_module_path():
+ term.print('autohooks => pre-commit')
+
+ with autohooks_module_path(), term.indent():
for name in config.get_pre_commit_script_names():
- try:
- plugin = load_plugin(name)
- if not has_precommit_function(plugin):
- error(
- 'No precommit function found in plugin {}. '
- 'Your autohooks settings may be invalid.'.format(name)
+ term.print('Running {}'.format(name))
+
+ with term.indent():
+ try:
+ plugin = load_plugin(name)
+ if not has_precommit_function(plugin):
+ term.error(
+ 'No precommit function found in plugin {}. '
+ 'Your autohooks settings may be invalid.'.format(
+ name
+ )
+ )
+ return 1
+
+ if has_precommit_parameters(plugin):
+ retval = plugin.precommit(config=config.get_config())
+ else:
+ term.warning(
+ 'precommit function without kwargs is deprecated. '
+ 'Please update {} to a newer version.'.format(name)
+ )
+ retval = plugin.precommit()
+
+ if retval:
+ return retval
+
+ except ImportError as e:
+ term.error(
+ 'An error occurred while importing pre-commit '
+ 'hook {}. {}.'.format(name, e)
)
return 1
-
- if has_precommit_parameters(plugin):
- retval = plugin.precommit(config=config.get_config())
- else:
- warning(
- 'precommit function without kwargs is deprecated. '
- 'Please update {} to a newer version.'.format(name)
+ except Exception as e: # pylint: disable=broad-except
+ term.error(
+ 'An error occurred while running pre-commit '
+ 'hook {}. {}.'.format(name, e)
)
- retval = plugin.precommit()
-
- if retval:
- return retval
-
- except ImportError as e:
- error(
- 'An error occurred while importing pre-commit '
- 'hook {}. {}.'.format(name, e)
- )
- return 1
- except Exception as e: # pylint: disable=broad-except
- error(
- 'An error occurred while running pre-commit '
- 'hook {}. {}.'.format(name, e)
- )
- return 1
+ return 1
return 0
diff --git a/autohooks/terminal.py b/autohooks/terminal.py
index ab20de61..f014ef34 100644
--- a/autohooks/terminal.py
+++ b/autohooks/terminal.py
@@ -16,31 +16,62 @@
# along with this program. If not, see .
import curses
-from blessings import Terminal
+from contextlib import contextmanager
-try:
- term = Terminal() # pylint: disable=invalid-name
-except curses.error:
- # handle issues with terminals and force not to style anything
- # should not be necessary with blessings > 1.7 anymore
- term = Terminal(force_styling=None) # pylint: disable=invalid-name
+from typing import Callable, Generator
+from blessings import Terminal as Term
-def ok(message: str) -> None:
- print(message, '[', term.green('ok'), ']')
+class Terminal:
+ def __init__(self):
+ self._indent = 0
+ try:
+ self._term = Term()
+ except curses.error:
+ # handle issues with terminals and force not to style anything
+ # should not be necessary with blessings > 1.7 anymore
+ self._term = Term(force_styling=None)
-def fail(message: str) -> None:
- print(message, '[', term.red('fail'), ']')
+ def _print_end(self, message: str, status: str, color: Callable) -> None:
+ width = self._term.width - 1
+ extra = 4 # '[ ', ' ]'
+ with self._term.location():
+ self.print(
+ message,
+ self._term.move_x(width - extra - len(status)),
+ '[',
+ color(status),
+ ']',
+ )
+ @contextmanager
+ def indent(self, indentation: int = 4) -> Generator:
+ current_indent = self._indent
+ self.add_indent(indentation)
-def error(message: str) -> None:
- print(message, '[', term.red('error'), ']')
+ yield self
+ self._indent = current_indent
-def warning(message: str) -> None:
- print(message, '[', term.yellow('warning'), ']')
+ def add_indent(self, indentation: int = 4) -> None:
+ self._indent += indentation
+ def print(self, *messages: str) -> None:
+ with self._term.location(x=self._indent):
+ print(*messages)
-def info(message: str) -> None:
- print(message, '[', term.cyan('info'), ']')
+ def ok(self, message: str) -> None:
+ self._print_end(message, 'ok', self._term.green)
+
+ def fail(self, message: str) -> None:
+ self._print_end(message, 'fail', self._term.red)
+
+ def error(self, message: str) -> None:
+ self._print_end(message, 'error', self._term.red)
+
+ def warning(self, message: str) -> None:
+ self._print_end(message, 'warning', self._term.yellow)
+
+ def info(self, message: str) -> None:
+ self._print_end(message, 'info', self._term.cyan)
diff --git a/tests/api/test_path.py b/tests/api/test_path.py
index 1efceb33..85289941 100644
--- a/tests/api/test_path.py
+++ b/tests/api/test_path.py
@@ -58,3 +58,7 @@ def test_match_files_in_subdir(self):
self.assertFalse(match(Path('foo/bar.js'), patterns))
self.assertFalse(match(Path('bar/foo.py'), patterns))
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tests/api/test_terminal.py b/tests/api/test_terminal.py
new file mode 100644
index 00000000..ed0084e3
--- /dev/null
+++ b/tests/api/test_terminal.py
@@ -0,0 +1,57 @@
+# Copyright (C) 2019 Greenbone Networks GmbH
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+import unittest
+
+from unittest.mock import Mock
+
+from autohooks.api import _set_terminal, error, fail, info, ok, out, warning
+from autohooks.terminal import Terminal
+
+
+class TerminalOutputApiTestCase(unittest.TestCase):
+ def setUp(self):
+ self.term = Mock(spec=Terminal)
+ _set_terminal(self.term)
+
+ def test_error(self):
+ error('foo bar')
+ self.term.error.assert_called_with('foo bar')
+
+ def test_fail(self):
+ fail('foo bar')
+ self.term.fail.assert_called_with('foo bar')
+
+ def test_info(self):
+ info('foo bar')
+ self.term.info.assert_called_with('foo bar')
+
+ def test_ok(self):
+ ok('foo bar')
+ self.term.ok.assert_called_with('foo bar')
+
+ def test_out(self):
+ out('foo bar')
+ self.term.print.assert_called_with('foo bar')
+
+ def test_warning(self):
+ warning('foo bar')
+ self.term.warning.assert_called_with('foo bar')
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tests/test_terminal.py b/tests/test_terminal.py
new file mode 100644
index 00000000..85cdf380
--- /dev/null
+++ b/tests/test_terminal.py
@@ -0,0 +1,206 @@
+# Copyright (C) 2019 Greenbone Networks GmbH
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+# pylint: disable=invalid-name, protected-access
+
+import unittest
+
+import sys
+
+from unittest.mock import Mock, MagicMock, patch
+
+from autohooks.terminal import Terminal
+
+
+class PropertyMagicMock(MagicMock):
+ def __get__(self, obj, obj_type=None):
+ return self()
+
+ def __set__(self, obj, val):
+ self(val)
+
+ def _get_child_mock(self, **kwargs):
+ return MagicMock(**kwargs)
+
+
+class TerminalTestCase(unittest.TestCase):
+ def assertCalled(self, mock):
+ if sys.version_info[1] < 6:
+ self.assertEqual(
+ mock.call_count,
+ 1,
+ '{} not called'.format(mock._mock_name or 'mock'),
+ )
+ else:
+ mock.assert_called()
+
+ def setUp(self):
+ self.print_patcher = patch('builtins.print')
+ self.terminal_patcher = patch('autohooks.terminal.Term', spec=True)
+
+ terminal_mock_class = self.terminal_patcher.start()
+ self.print_mock = self.print_patcher.start()
+
+ self.width_mock = PropertyMagicMock(return_value=80, spec=1)
+
+ self.terminal_mock = terminal_mock_class.return_value
+
+ # "Because of the way mock attributes are stored you can’t directly
+ # attach a PropertyMock to a mock object. Instead you can attach it to
+ # the mock type object"
+ # https://docs.python.org/3/library/unittest.mock.html#unittest.mock.PropertyMock
+ type(self.terminal_mock).width = self.width_mock
+
+ self.terminal_mock.cyan = Mock()
+ self.terminal_mock.green = Mock()
+ self.terminal_mock.red = Mock()
+ self.terminal_mock.yellow = Mock()
+
+ self.terminal_mock.move_x = Mock()
+
+ def tearDown(self):
+ self.print_patcher.stop()
+ self.terminal_patcher.stop()
+
+ def test_error(self):
+ term = Terminal()
+ term.error('foo bar')
+
+ # width has been calculated
+ self.width_mock.assert_called_with()
+
+ # 70 == 80 - 5 - len('error')
+ self.terminal_mock.move_x.assert_called_with(70)
+
+ # error has been printed in red
+ self.terminal_mock.red.assert_called_with('error')
+
+ # an actual output has been generated
+ self.assertCalled(self.print_mock)
+
+ def test_fail(self):
+ term = Terminal()
+ term.fail('foo bar')
+
+ # width has been calculated
+ self.width_mock.assert_called_with()
+
+ # 71 == 80 - 5 - len('fail')
+ self.terminal_mock.move_x.assert_called_with(71)
+
+ # fail has been printed in red
+ self.terminal_mock.red.assert_called_with('fail')
+
+ # an actual output has been generated
+ self.assertCalled(self.print_mock)
+
+ def test_info(self):
+ term = Terminal()
+ term.info('foo bar')
+
+ # width has been calculated
+ self.width_mock.assert_called_with()
+
+ # 71 == 80 - 5 - len('info')
+ self.terminal_mock.move_x.assert_called_with(71)
+
+ # info has been printed in cyan
+ self.terminal_mock.cyan.assert_called_with('info')
+
+ # an actual output has been generated
+ self.assertCalled(self.print_mock)
+
+ def test_ok(self):
+ term = Terminal()
+ term.ok('foo bar')
+
+ # width has been calculated
+ self.width_mock.assert_called_with()
+
+ # 73 == 80 - 5 - len('ok')
+ self.terminal_mock.move_x.assert_called_with(73)
+
+ # ok has been printed in green
+ self.terminal_mock.green.assert_called_with('ok')
+
+ # an actual output has been generated
+ self.assertCalled(self.print_mock)
+
+ def test_warning(self):
+ term = Terminal()
+ term.warning('foo bar')
+
+ # width has been calculated
+ self.width_mock.assert_called_with()
+
+ # 68 == 80 - 5 - len('warning')
+ self.terminal_mock.move_x.assert_called_with(68)
+
+ # warning has been printed in yellow
+ self.terminal_mock.yellow.assert_called_with('warning')
+
+ # an actual output has been generated
+ self.assertCalled(self.print_mock)
+
+ def test_print(self):
+ term = Terminal()
+ term.print('foo bar')
+
+ # printed output at current indent location
+ self.terminal_mock.location.assert_called_with(x=0)
+
+ # an actual output has been generated
+ self.print_mock.assert_called_with('foo bar')
+
+ def test_add_indent(self):
+ term = Terminal()
+ term.add_indent(6)
+ term.print('foo')
+
+ # printed output at current indent location
+ self.terminal_mock.location.assert_called_with(x=6)
+
+ # an actual output has been generated
+ self.print_mock.assert_called_with('foo')
+
+ term.add_indent(4)
+ term.print('bar')
+
+ # printed output at current indent location
+ self.terminal_mock.location.assert_called_with(x=10)
+
+ # an actual output has been generated
+ self.print_mock.assert_called_with('bar')
+
+ def test_with_indent(self):
+ term = Terminal()
+
+ with term.indent(2):
+ term.print('foo')
+
+ self.terminal_mock.location.assert_called_with(x=2)
+ self.print_mock.assert_called_with('foo')
+
+ term.print('bar')
+
+ # indentation has been removed
+ self.terminal_mock.location.assert_called_with(x=0)
+ self.print_mock.assert_called_with('bar')
+
+
+if __name__ == '__main__':
+ unittest.main()