Implement ExitStack to handle mocks in pytest-bdd

- Fix failing DayOne test
- Make format and clean up extraneous comment

Co-authored-by: Jonathan Wren <jonathan@nowandwren.com>
This commit is contained in:
Micah Jerome Ellison 2021-06-05 15:54:28 -07:00 committed by Jonathan Wren
parent 0c8efd5331
commit fd349fb0fc
2 changed files with 56 additions and 33 deletions

View file

@ -196,16 +196,14 @@ Feature: Writing new entries.
And "entries.0.creator.software_agent" in the parsed output should contain And "entries.0.creator.software_agent" in the parsed output should contain
jrnl jrnl
# fails when system time is UTC (as on Travis-CI)
# @skip
Scenario: Title with an embedded period on DayOne journal Scenario: Title with an embedded period on DayOne journal
Given we use the config "dayone.yaml" Given we use the config "dayone.yaml"
When we run "jrnl 04-24-2014: "Ran 6.2 miles today in 1:02:03. I'm feeling sore because I forgot to stretch."" When we run "jrnl 04-24-2014: Ran 6.2 miles today in 1:02:03. I am feeling sore because I forgot to stretch."
Then we should see the message "Entry added" Then we should see the message "Entry added"
When we run "jrnl -1" When we run "jrnl -1"
Then the output should be Then the output should be
2014-04-24 09:00 Ran 6.2 miles today in 1:02:03. 2014-04-24 09:00 Ran 6.2 miles today in 1:02:03.
| I'm feeling sore because I forgot to stretch. | I am feeling sore because I forgot to stretch.
Scenario: Opening an folder that's not a DayOne folder should treat as folder journal Scenario: Opening an folder that's not a DayOne folder should treat as folder journal
Given we use the config "empty_folder.yaml" Given we use the config "empty_folder.yaml"

View file

@ -5,6 +5,7 @@ import json
import os import os
from datetime import datetime from datetime import datetime
from collections import defaultdict from collections import defaultdict
from contextlib import ExitStack
from keyring import backend from keyring import backend
from keyring import set_keyring from keyring import set_keyring
from keyring import errors from keyring import errors
@ -121,6 +122,11 @@ def cli_run():
return {"status": 0, "stdout": None, "stderr": None} return {"status": 0, "stdout": None, "stderr": None}
@fixture
def mocks():
return dict()
@fixture @fixture
def temp_dir(): def temp_dir():
return tempfile.TemporaryDirectory() return tempfile.TemporaryDirectory()
@ -154,11 +160,6 @@ def input_method():
return "" return ""
@fixture
def now_date():
return {"datetime": datetime, "calendar_parse": __get_pdt_calendar()}
@fixture @fixture
def cache_dir(): def cache_dir():
return {"exists": False, "path": ""} return {"exists": False, "path": ""}
@ -268,27 +269,37 @@ def we_enter_editor(editor_method, editor_input, editor_state):
editor_state["intent"] = {"method": file_method, "input": editor_input} editor_state["intent"] = {"method": file_method, "input": editor_input}
@given(parse('now is "<date_str>"'), target_fixture="now_date") @given(parse('now is "<date_str>"'))
@given(parse('now is "{date_str}"'), target_fixture="now_date") @given(parse('now is "{date_str}"'))
def now_is_str(date_str): def now_is_str(date_str, mocks):
class DatetimeMagicMock(MagicMock): class DatetimeMagicMock(MagicMock):
# needed because jrnl does some reflection on datetime # needed because jrnl does some reflection on datetime
def __instancecheck__(self, subclass): def __instancecheck__(self, subclass):
return isinstance(subclass, datetime) return isinstance(subclass, datetime)
my_date = datetime.strptime(date_str, "%Y-%m-%d %I:%M:%S %p") def mocked_now(tz=None):
now = datetime.strptime(date_str, "%Y-%m-%d %I:%M:%S %p")
if tz:
time_zone = datetime.utcnow().astimezone().tzinfo
now = now.replace(tzinfo=time_zone)
return now
# jrnl uses two different classes to parse dates, so both must be mocked # jrnl uses two different classes to parse dates, so both must be mocked
datetime_mock = DatetimeMagicMock(wraps=datetime) datetime_mock = DatetimeMagicMock(wraps=datetime)
datetime_mock.now.return_value = my_date datetime_mock.now.side_effect = mocked_now
pdt = __get_pdt_calendar() pdt = __get_pdt_calendar()
calendar_mock = MagicMock(wraps=pdt) calendar_mock = MagicMock(wraps=pdt)
calendar_mock.parse.side_effect = lambda date_str_input: pdt.parse( calendar_mock.parse.side_effect = lambda date_str_input: pdt.parse(
date_str_input, my_date date_str_input, mocked_now()
) )
return {"datetime": datetime_mock, "calendar_parse": calendar_mock} mocks["datetime"] = patch("datetime.datetime", new=datetime_mock)
mocks["calendar_parse"] = patch(
"jrnl.time.__get_pdt_calendar", return_value=calendar_mock
)
@then(parse("the editor should have been called")) @then(parse("the editor should have been called"))
@ -381,9 +392,9 @@ def we_run(
password, password,
cache_dir, cache_dir,
editor, editor,
now_date,
keyring, keyring,
input_method, input_method,
mocks,
): ):
assert input_method in ["", "enter", "pipe"] assert input_method in ["", "enter", "pipe"]
is_tty = input_method != "pipe" is_tty = input_method != "pipe"
@ -403,21 +414,36 @@ def we_run(
if not password and user_input: if not password and user_input:
password = user_input password = user_input
# fmt: off with ExitStack() as stack:
# see: https://github.com/psf/black/issues/664
# @todo https://docs.python.org/3/library/contextlib.html#contextlib.ExitStack stack.enter_context(patch("sys.argv", ["jrnl"] + args))
with \
patch("sys.argv", ['jrnl'] + args), \ mock_stdin = stack.enter_context(
patch("sys.stdin.read", side_effect=user_input) as mock_stdin, \ patch("sys.stdin.read", side_effect=user_input)
patch("sys.stdin.isatty", return_value=is_tty), \ )
patch("builtins.input", side_effect=user_input) as mock_input, \ stack.enter_context(patch("sys.stdin.isatty", return_value=is_tty))
patch("getpass.getpass", side_effect=password) as mock_getpass, \ mock_input = stack.enter_context(
patch("datetime.datetime", new=now_date["datetime"]), \ patch("builtins.input", side_effect=user_input)
patch("jrnl.time.__get_pdt_calendar", return_value=now_date["calendar_parse"]), \ )
patch("jrnl.install.get_config_path", return_value=config_path), \ mock_getpass = stack.enter_context(
patch("jrnl.config.get_config_path", return_value=config_path), \ patch("getpass.getpass", side_effect=password)
patch("subprocess.call", side_effect=editor) as mock_editor \ )
: # @TODO: single point of truth for get_config_path (move from all calls from install to config)
if "datetime" in mocks:
stack.enter_context(mocks["datetime"])
stack.enter_context(mocks["calendar_parse"])
# stack.enter_context(patch("datetime.datetime", new=mocks["datetime"]))
# stack.enter_context(patch("jrnl.time.__get_pdt_calendar", return_value=mocks["calendar_parse"]))
stack.enter_context(
patch("jrnl.install.get_config_path", return_value=config_path)
)
stack.enter_context(
patch("jrnl.config.get_config_path", return_value=config_path)
)
mock_editor = stack.enter_context(patch("subprocess.call", side_effect=editor))
try: try:
cli(args) cli(args)
except StopIteration: except StopIteration:
@ -425,7 +451,6 @@ def we_run(
pass pass
except SystemExit as e: except SystemExit as e:
status = e.code status = e.code
# fmt: on
captured = capsys.readouterr() captured = capsys.readouterr()