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
jrnl
# fails when system time is UTC (as on Travis-CI)
# @skip
Scenario: Title with an embedded period on DayOne journal
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"
When we run "jrnl -1"
Then the output should be
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
Given we use the config "empty_folder.yaml"

View file

@ -5,6 +5,7 @@ import json
import os
from datetime import datetime
from collections import defaultdict
from contextlib import ExitStack
from keyring import backend
from keyring import set_keyring
from keyring import errors
@ -121,6 +122,11 @@ def cli_run():
return {"status": 0, "stdout": None, "stderr": None}
@fixture
def mocks():
return dict()
@fixture
def temp_dir():
return tempfile.TemporaryDirectory()
@ -154,11 +160,6 @@ def input_method():
return ""
@fixture
def now_date():
return {"datetime": datetime, "calendar_parse": __get_pdt_calendar()}
@fixture
def cache_dir():
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}
@given(parse('now is "<date_str>"'), target_fixture="now_date")
@given(parse('now is "{date_str}"'), target_fixture="now_date")
def now_is_str(date_str):
@given(parse('now is "<date_str>"'))
@given(parse('now is "{date_str}"'))
def now_is_str(date_str, mocks):
class DatetimeMagicMock(MagicMock):
# needed because jrnl does some reflection on datetime
def __instancecheck__(self, subclass):
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
datetime_mock = DatetimeMagicMock(wraps=datetime)
datetime_mock.now.return_value = my_date
datetime_mock.now.side_effect = mocked_now
pdt = __get_pdt_calendar()
calendar_mock = MagicMock(wraps=pdt)
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"))
@ -381,9 +392,9 @@ def we_run(
password,
cache_dir,
editor,
now_date,
keyring,
input_method,
mocks,
):
assert input_method in ["", "enter", "pipe"]
is_tty = input_method != "pipe"
@ -403,21 +414,36 @@ def we_run(
if not password and user_input:
password = user_input
# fmt: off
# see: https://github.com/psf/black/issues/664
# @todo https://docs.python.org/3/library/contextlib.html#contextlib.ExitStack
with \
patch("sys.argv", ['jrnl'] + args), \
patch("sys.stdin.read", side_effect=user_input) as mock_stdin, \
patch("sys.stdin.isatty", return_value=is_tty), \
patch("builtins.input", side_effect=user_input) as mock_input, \
patch("getpass.getpass", side_effect=password) as mock_getpass, \
patch("datetime.datetime", new=now_date["datetime"]), \
patch("jrnl.time.__get_pdt_calendar", return_value=now_date["calendar_parse"]), \
patch("jrnl.install.get_config_path", return_value=config_path), \
patch("jrnl.config.get_config_path", return_value=config_path), \
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)
with ExitStack() as stack:
stack.enter_context(patch("sys.argv", ["jrnl"] + args))
mock_stdin = stack.enter_context(
patch("sys.stdin.read", side_effect=user_input)
)
stack.enter_context(patch("sys.stdin.isatty", return_value=is_tty))
mock_input = stack.enter_context(
patch("builtins.input", side_effect=user_input)
)
mock_getpass = stack.enter_context(
patch("getpass.getpass", side_effect=password)
)
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:
cli(args)
except StopIteration:
@ -425,7 +451,6 @@ def we_run(
pass
except SystemExit as e:
status = e.code
# fmt: on
captured = capsys.readouterr()