Improve handling of mocking logic in pytest (#1382)

* WIP

* fix handling of user input (stdin, input, getpass)

* take out redundant pytest step

* fix handling of 'we should' statements

* fix test that doesn't use a config file

* fix another test that uses stdin

Co-authored-by: Micah Jerome Ellison <micah.jerome.ellison@gmail.com>

* remove .tool-versions file per PR feedback

* add comment to clarify why disembodied variables are here

Co-authored-by: Micah Jerome Ellison <micah.jerome.ellison@gmail.com>
This commit is contained in:
Jonathan Wren 2021-12-11 12:35:32 -08:00 committed by GitHub
parent 3518e37087
commit 2ab485de8c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 259 additions and 197 deletions

4
.gitignore vendored
View file

@ -2,7 +2,6 @@
# C extensions
*.so
.python-version
# Packages
*.egg
@ -17,7 +16,10 @@ sdist
develop-eggs
.installed.cfg
lib64
# Versioning
.python-version
.tool-version
# Installer logs
pip-log.txt

View file

@ -29,7 +29,7 @@ Feature: Multiple journals
Given the config "multiple.yaml" exists
And we use the config "basic_onefile.yaml"
When we run "jrnl --cf multiple.yaml work a long day in the office"
Then we should see the message "Entry added to work journal"
Then the output should contain "Entry added to work journal"
Scenario: Write to specified journal with a timestamp using an alternate config
Given the config "multiple.yaml" exists
@ -64,7 +64,7 @@ Feature: Multiple journals
Given the config "bug343.yaml" exists
And we use the config "basic_onefile.yaml"
When we run "jrnl --cf bug343.yaml a long day in the office"
Then we should see the message "No default journal configured"
Then the output should contain "No default journal configured"
Scenario: Don't crash if no file exists for a configured encrypted journal using an alternate config
Given the config "multiple.yaml" exists
@ -73,7 +73,7 @@ Feature: Multiple journals
these three eyes
these three eyes
n
Then we should see the message "Encrypted journal 'new_encrypted' created"
Then the output should contain "Encrypted journal 'new_encrypted' created"
Scenario: Don't overwrite main config when encrypting a journal in an alternate config
Given the config "basic_onefile.yaml" exists
@ -82,11 +82,14 @@ Feature: Multiple journals
these three eyes
these three eyes
n
Then we should see the message "Journal encrypted to features/journals/basic_onefile.journal"
And the config should contain "encrypt: false" # multiple.yaml remains unchanged
Then the output should contain "Journal encrypted to features/journals/basic_onefile.journal"
And the config should contain "encrypt: false"
Scenario: Don't overwrite main config when decrypting a journal in an alternate config
Given the config "editor_encrypted.yaml" exists
And we use the password "bad doggie no biscuit" if prompted
And we use the config "basic_encrypted.yaml"
When we run "jrnl --cf editor_encrypted.yaml --decrypt"
Then the config should contain "encrypt: true" # basic_encrypted remains unchanged
Then the config should contain "encrypt: true"
And the output should not contain "Wrong password"

View file

@ -4,7 +4,7 @@ Feature: Reading and writing to journal with custom date formats
# https://github.com/jrnl-org/jrnl/issues/117
Given we use the config "simple.yaml"
When we run "jrnl 2013-11-30 15:42: Project Started."
Then we should see the message "Entry added"
Then the output should contain "Entry added"
When we run "jrnl -999"
Then the output should contain "2013-11-30 15:42 Project Started."
@ -13,7 +13,7 @@ Feature: Reading and writing to journal with custom date formats
# https://github.com/jrnl-org/jrnl/issues/185
Given we use the config "simple.yaml"
When we run "jrnl 26/06/2099: Planet? Earth. Year? 2099."
Then we should see the message "Entry added"
Then the output should contain "Entry added"
When we run "jrnl -999"
Then the output should contain "2099-06-26 09:00 Planet?"
@ -34,7 +34,7 @@ Feature: Reading and writing to journal with custom date formats
Scenario Outline: Writing an entry from command line with custom date
Given we use the config "<config_file>"
When we run "jrnl <command>"
Then we should see the message "Entry added"
Then the output should contain "Entry added"
When we run "jrnl -n 1"
Then the output should contain "<expected_output>"
@ -87,7 +87,7 @@ Feature: Reading and writing to journal with custom date formats
Given we use the config "simple.yaml"
And now is "2019-03-12 01:30:32 PM"
When we run "jrnl <command>"
Then we should see the message "Entry added"
Then the output should contain "Entry added"
When we run "jrnl -1"
Then the output should contain "<expected_output>"
Then the output should contain the date "<date>"
@ -109,7 +109,7 @@ Feature: Reading and writing to journal with custom date formats
Given we use the config "simple.yaml"
And now is "2019-03-12 01:30:32 PM"
When we run "jrnl <command>"
Then we should see the message "Entry added"
Then the output should contain "Entry added"
When we run "jrnl -1"
Then the output should contain "<expected_output>"
Then the output should contain the date "<date>"

View file

@ -2,8 +2,9 @@ Feature: Encrypting and decrypting journals
Scenario: Decrypting a journal
Given we use the config "encrypted.yaml"
# And we use the password "bad doggie no biscuit" if prompted
When we run "jrnl --decrypt" and enter "bad doggie no biscuit"
Then we should see the message "Journal decrypted"
Then the output should contain "Journal decrypted"
And the config for journal "default" should contain "encrypt: false"
When we run "jrnl -99 --short"
Then the output should be
@ -35,7 +36,7 @@ Feature: Encrypting and decrypting journals
swordfish
n
Then we should get no error
And we should see the message "Journal encrypted"
And the output should contain "Journal encrypted"
And the config for journal "default" should contain "encrypt: true"
When we run "jrnl -n 1" and enter "swordfish"
Then we should be prompted for a password

View file

@ -3,7 +3,7 @@ Feature: Journals iteracting with the file system in a way that users can see
Scenario: Adding entries to a Folder journal should generate date files
Given we use the config "empty_folder.yaml"
When we run "jrnl 23 July 2013: Testing folder journal."
Then we should see the message "Entry added"
Then the output should contain "Entry added"
And the journal directory should contain
2013/07/23.txt
@ -11,7 +11,7 @@ Feature: Journals iteracting with the file system in a way that users can see
Given we use the config "empty_folder.yaml"
When we run "jrnl 23 July 2013: Testing folder journal."
And we run "jrnl 3/7/2014: Second entry of journal."
Then we should see the message "Entry added"
Then the output should contain "Entry added"
And the journal directory should contain
2013/07/23.txt
@ -32,6 +32,7 @@ Feature: Journals iteracting with the file system in a way that users can see
Then the output should contain "This is a new entry in my journal"
Scenario: Creating journal with relative path should update to absolute path
Given we use no config
When we run "jrnl hello world" and enter
test.txt
n

View file

@ -34,7 +34,7 @@ Feature: Multiple journals
Scenario: Tell user which journal was used
Given we use the config "multiple.yaml"
When we run "jrnl work a long day in the office"
Then we should see the message "Entry added to work journal"
Then the output should contain "Entry added to work journal"
Scenario: Write to specified journal with a timestamp
Given we use the config "multiple.yaml"

View file

@ -3,9 +3,12 @@ Feature: Implementing Runtime Overrides for Select Configuration Keys
Scenario: Override configured editor with built-in input === editor:''
Given we use the config "basic_encrypted.yaml"
And we use the password "test" if prompted
When we run "jrnl --config-override editor ''"
When we run "jrnl --config-override editor ''" and enter
This is a journal entry
Then the stdin prompt should have been called
And the editor should not have been called
When we run "jrnl -1"
Then the output should contain "This is a journal entry"
Scenario: Postconfig commands with overrides
@ -61,7 +64,7 @@ Feature: Implementing Runtime Overrides for Select Configuration Keys
And we use the password "test" if prompted
When we run "jrnl --config-override journals.default features/journals/simple.journal 20 Mar 2000: The rain in Spain comes from clouds"
Then we should get no error
And we should see the message "Entry added"
And the output should contain "Entry added"
When we run "jrnl -3 --config-override journals.default features/journals/simple.journal"
Then the output should be
2000-03-20 09:00 The rain in Spain comes from clouds
@ -78,7 +81,7 @@ Feature: Implementing Runtime Overrides for Select Configuration Keys
And we use the password "test" if prompted
When we run "jrnl --config-override journals.temp features/journals/simple.journal temp Sep 06 1969: @say Ni"
Then we should get no error
And we should see the message "Entry added"
And the output should contain "Entry added"
When we run "jrnl --config-override journals.temp features/journals/simple.journal temp -3"
Then the output should be
1969-09-06 09:00 @say Ni

View file

@ -55,7 +55,7 @@ Feature: Using the installed keyring
this password will not be saved in keyring
this password will not be saved in keyring
y
Then we should see the message "Failed to retrieve keyring"
Then the output should contain "Failed to retrieve keyring"
And we should get no error
And we should be prompted for a password
And the config for journal "default" should contain "encrypt: true"
@ -69,7 +69,7 @@ Feature: Using the installed keyring
Then the error output should contain "Failed to retrieve keyring"
And we should get no error
And we should be prompted for a password
And we should see the message "Journal decrypted"
And the output should contain "Journal decrypted"
And the config for journal "default" should contain "encrypt: false"
When we run "jrnl --short"
Then we should not be prompted for a password
@ -96,7 +96,7 @@ Feature: Using the installed keyring
swordfish
sordfish
Then we should be prompted for a password
And we should see the message "Passwords did not match"
And the output should contain "Passwords did not match"
And the config for journal "default" should not contain "encrypt: true"
When we run "jrnl --short"
Then the output should be
@ -113,8 +113,8 @@ Feature: Using the installed keyring
swordfish
n
Then we should be prompted for a password
And we should see the message "Passwords did not match"
And we should see the message "Journal encrypted"
And the output should contain "Passwords did not match"
And the output should contain "Journal encrypted"
And the config for journal "default" should contain "encrypt: true"
When we run "jrnl -1" and enter "swordfish"
Then we should be prompted for a password

View file

@ -3,7 +3,7 @@ Feature: Searching in a journal
Scenario Outline: Displaying entries using -on today should display entries created today
Given we use the config "<config_file>"
When we run "jrnl today: Adding an entry right now."
Then we should see the message "Entry added"
Then the output should contain "Entry added"
When we run "jrnl -on today"
Then the output should contain "Adding an entry right now."
But the output should not contain "Everything is alright"
@ -18,11 +18,11 @@ Feature: Searching in a journal
Scenario Outline: Displaying entries using -from day should display correct entries
Given we use the config "<config_file>"
When we run "jrnl yesterday: This thing happened yesterday"
Then we should see the message "Entry added"
Then the output should contain "Entry added"
When we run "jrnl today at 11:59pm: Adding an entry right now."
Then we should see the message "Entry added"
Then the output should contain "Entry added"
When we run "jrnl tomorrow: A future entry."
Then we should see the message "Entry added"
Then the output should contain "Entry added"
When we run "jrnl -from today"
Then the output should contain "Adding an entry right now."
And the output should contain "A future entry."
@ -37,11 +37,11 @@ Feature: Searching in a journal
Scenario Outline: Displaying entries using -from and -to day should display correct entries
Given we use the config "<config_file>"
When we run "jrnl yesterday: This thing happened yesterday"
Then we should see the message "Entry added"
Then the output should contain "Entry added"
When we run "jrnl today at 11:59pm: Adding an entry right now."
Then we should see the message "Entry added"
Then the output should contain "Entry added"
When we run "jrnl tomorrow: A future entry."
Then we should see the message "Entry added"
Then the output should contain "Entry added"
When we run "jrnl -from yesterday -to today"
Then the output should contain "This thing happened yesterday"
And the output should contain "Adding an entry right now."
@ -118,9 +118,9 @@ Feature: Searching in a journal
Scenario: Out of order entries to a Folder journal should be listed in date order
Given we use the config "empty_folder.yaml"
When we run "jrnl 3/7/2014 4:37pm: Second entry of journal."
Then we should see the message "Entry added"
Then the output should contain "Entry added"
When we run "jrnl 23 July 2013: Testing folder journal."
Then we should see the message "Entry added"
Then the output should contain "Entry added"
When we run "jrnl -2"
Then the output should be
2013-07-23 09:00 Testing folder journal.

View file

@ -3,7 +3,7 @@ Feature: Starring entries
Scenario Outline: Starring an entry will mark it in the journal file
Given we use the config "<config_file>"
When we run "jrnl 20 july 2013 *: Best day of my life!"
Then we should see the message "Entry added"
Then the output should contain "Entry added"
When we run "jrnl -on 2013-07-20 -starred"
Then the output should contain "2013-07-20 09:00 Best day of my life!"
@ -30,6 +30,6 @@ Feature: Starring entries
Scenario: Starring an entry will mark it in an encrypted journal
Given we use the config "encrypted.yaml"
When we run "jrnl 20 july 2013 *: Best day of my life!" and enter "bad doggie no biscuit"
Then we should see the message "Entry added"
Then the output should contain "Entry added"
When we run "jrnl -on 2013-07-20 -starred" and enter "bad doggie no biscuit"
Then the output should contain "2013-07-20 09:00 Best day of my life!"

View file

@ -46,7 +46,7 @@ Feature: Writing new entries.
Given we use the config "<config_file>"
And we use the password "bad doggie no biscuit" if prompted
When we run "jrnl 23 july 2013: A cold and stormy day. I ate crisps on the sofa."
Then we should see the message "Entry added"
Then the output should contain "Entry added"
When we run "jrnl -n 1"
Then the output should contain "2013-07-23 09:00 A cold and stormy day."
@ -61,7 +61,7 @@ Feature: Writing new entries.
Given we use the config "<config_file>"
And we use the password "test" if prompted
When we run "jrnl this is a partial --edit"
Then we should see the message "Entry added"
Then the output should contain "Entry added"
Then the editor should have been called
And the editor file content should be
this is a partial
@ -110,7 +110,7 @@ Feature: Writing new entries.
Given we use the config "<config_file>"
And we use the password "bad doggie no biscuit" if prompted
When we run "jrnl 23 july 2013: A cold and stormy day. I ate crisps on the sofa."
Then we should see the message "Entry added"
Then the output should contain "Entry added"
When we run "jrnl -n 1"
Then the output should not contain "Life is good"
@ -125,7 +125,7 @@ Feature: Writing new entries.
Given we use the config "<config_file>"
And we use the password "bad doggie no biscuit" if prompted
When we run "jrnl 04-24-2014: Created a new website - empty.com. Hope to get a lot of traffic."
Then we should see the message "Entry added"
Then the output should contain "Entry added"
When we run "jrnl -1"
Then the output should be
2014-04-24 09:00 Created a new website - empty.com.
@ -142,7 +142,7 @@ Feature: Writing new entries.
Given we use the config "<config_file>"
And we use the password "bad doggie no biscuit" if prompted
When we run "jrnl 23 july 2013: 🌞 sunny day. Saw an 🐘"
Then we should see the message "Entry added"
Then the output should contain "Entry added"
When we run "jrnl -n 1"
Then the output should contain "🌞"
And the output should contain "🐘"
@ -199,7 +199,7 @@ Feature: Writing new entries.
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 am feeling sore because I forgot to stretch."
Then we should see the message "Entry added"
Then the output should contain "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.
@ -208,7 +208,7 @@ Feature: Writing new entries.
Scenario: Opening an folder that's not a DayOne folder should treat as folder journal
Given we use the config "empty_folder.yaml"
When we run "jrnl 23 july 2013: Testing folder journal."
Then we should see the message "Entry added"
Then the output should contain "Entry added"
When we run "jrnl -1"
Then the output should be "2013-07-23 09:00 Testing folder journal."

View file

@ -8,17 +8,19 @@ import tempfile
from keyring import backend
from keyring import errors
from keyring import set_keyring
from pytest import fixture
from unittest.mock import patch
from .helpers import get_fixture
import toml
from jrnl.config import load_config
from jrnl.os_compat import split_args
# --- Keyring --- #
@fixture
def keyring():
set_keyring(NoKeyring())
return NoKeyring()
@fixture
@ -75,13 +77,90 @@ class FailedKeyring(backend.KeyringBackend):
# ----- Misc ----- #
@fixture
def cli_run():
return {"status": 0, "stdout": None, "stderr": None}
def cli_run(
mock_factories,
mock_args,
mock_is_tty,
mock_config_path,
mock_editor,
mock_user_input,
mock_overrides,
mock_password,
):
# Check if we need more mocks
mock_factories.update(mock_args)
mock_factories.update(mock_is_tty)
mock_factories.update(mock_overrides)
mock_factories.update(mock_editor)
mock_factories.update(mock_config_path)
mock_factories.update(mock_user_input)
mock_factories.update(mock_password)
return {
"status": 0,
"stdout": None,
"stderr": None,
"mocks": {},
"mock_factories": mock_factories,
}
@fixture
def mocks():
return dict()
def mock_factories():
return {}
@fixture
def mock_args(cache_dir, request):
def _mock_args():
command = get_fixture(request, "command", "")
if cache_dir["exists"]:
command = command.format(cache_dir=cache_dir["path"])
args = split_args(command)
return patch("sys.argv", ["jrnl"] + args)
return {"args": _mock_args}
@fixture
def mock_is_tty(is_tty):
return {"is_tty": lambda: patch("sys.stdin.isatty", return_value=is_tty)}
@fixture
def mock_overrides(config_in_memory):
from jrnl.override import apply_overrides
def my_overrides(*args, **kwargs):
result = apply_overrides(*args, **kwargs)
config_in_memory["overrides"] = result
return result
return {
"overrides": lambda: patch(
"jrnl.jrnl.apply_overrides", side_effect=my_overrides
)
}
@fixture
def mock_config_path(request):
config_path = get_fixture(request, "config_path")
if not config_path:
return {}
return {
"config_path_install": lambda: patch(
"jrnl.install.get_config_path", return_value=config_path
),
"config_path_config": lambda: patch(
"jrnl.config.get_config_path", return_value=config_path
),
}
@fixture
@ -94,12 +173,6 @@ def working_dir(request):
return os.path.join(request.config.rootpath, "tests")
@fixture
def config_path(temp_dir):
os.chdir(temp_dir.name)
return temp_dir.name + "/jrnl.yaml"
@fixture
def toml_version(working_dir):
pyproject = os.path.join(working_dir, "..", "pyproject.toml")
@ -108,8 +181,23 @@ def toml_version(working_dir):
@fixture
def password():
return ""
def mock_password(request):
def _mock_password():
password = get_fixture(request, "password")
user_input = get_fixture(request, "user_input")
if password:
password = password.splitlines()
elif user_input:
password = user_input.splitlines()
if not password:
password = Exception("Unexpected call for password")
return patch("getpass.getpass", side_effect=password)
return {"getpass": _mock_password}
@fixture
@ -127,19 +215,36 @@ def str_value():
return ""
@fixture
def command():
return ""
@fixture
def should_not():
return False
@fixture
def user_input():
return ""
def mock_user_input(request, is_tty):
def _generator(target):
def _mock_user_input():
user_input = get_fixture(request, "user_input", None)
if user_input is None:
user_input = Exception("Unexpected call for user input")
else:
user_input = user_input.splitlines() if is_tty else [user_input]
return patch(target, side_effect=user_input)
return _mock_user_input
return {
"stdin": _generator("sys.stdin.read"),
"input": _generator("builtins.input"),
}
@fixture
def is_tty(input_method):
assert input_method in ["", "enter", "pipe"]
return input_method != "pipe"
@fixture
@ -187,7 +292,7 @@ def editor_state():
@fixture
def editor(editor_state):
def mock_editor(editor_state):
def _mock_editor(editor_command):
tmpfile = editor_command[-1]
@ -203,4 +308,4 @@ def editor(editor_state):
file_content = f.read()
editor_state["tmpfile"]["content"] = file_content
return _mock_editor
return {"editor": lambda: patch("subprocess.call", side_effect=_mock_editor)}

View file

@ -11,7 +11,6 @@ from unittest.mock import MagicMock
from unittest.mock import patch
from xml.etree import ElementTree
from keyring import set_keyring
from pytest_bdd import given
from pytest_bdd.parsers import parse
@ -20,6 +19,7 @@ from jrnl.time import __get_pdt_calendar
from .fixtures import FailedKeyring
from .fixtures import TestKeyring
from .helpers import get_fixture
@given(parse("we {editor_method} to the editor if opened\n{editor_input}"))
@ -36,9 +36,8 @@ 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>"'))
@given(parse('now is "{date_str}"'))
def now_is_str(date_str, mocks):
def now_is_str(date_str, mock_factories):
class DatetimeMagicMock(MagicMock):
# needed because jrnl does some reflection on datetime
def __instancecheck__(self, subclass):
@ -63,8 +62,8 @@ def now_is_str(date_str, mocks):
date_str_input, mocked_now()
)
mocks["datetime"] = patch("datetime.datetime", new=datetime_mock)
mocks["calendar_parse"] = patch(
mock_factories["datetime"] = lambda: patch("datetime.datetime", new=datetime_mock)
mock_factories["calendar_parse"] = lambda: patch(
"jrnl.time.__get_pdt_calendar", return_value=calendar_mock
)
@ -73,17 +72,22 @@ def now_is_str(date_str, mocks):
@given(parse("we have a {keyring_type} keyring"), target_fixture="keyring")
def we_have_type_of_keyring(keyring_type):
if keyring_type == "failed":
set_keyring(FailedKeyring())
return FailedKeyring()
else:
set_keyring(TestKeyring())
return TestKeyring()
@given(parse('we use the config "{config_file}"'), target_fixture="config_path")
@given('we use the config "<config_file>"', target_fixture="config_path")
def we_use_the_config(config_file, temp_dir, working_dir):
@given(parse("we use no config"), target_fixture="config_path")
def we_use_the_config(request, temp_dir, working_dir):
config_file = get_fixture(request, "config_file")
# Move into temp dir as cwd
os.chdir(temp_dir.name)
if not config_file:
return os.path.join(temp_dir.name, "non_existing_config.yaml")
# Copy the config file over
config_source = os.path.join(working_dir, "data", "configs", config_file)
config_dest = os.path.join(temp_dir.name, config_file)
@ -106,7 +110,6 @@ def we_use_the_config(config_file, temp_dir, working_dir):
@given(parse('the config "{config_file}" exists'), target_fixture="config_path")
@given('the config "<config_file>" exists', target_fixture="config_path")
def config_exists(config_file, temp_dir, working_dir):
config_source = os.path.join(working_dir, "data", "configs", config_file)
config_dest = os.path.join(temp_dir.name, config_file)

View file

@ -49,3 +49,24 @@ def get_nested_val(dictionary, path, *default):
if default:
return default[0]
raise
# @see: https://stackoverflow.com/a/41599695/569146
def spy_wrapper(wrapped_function):
from unittest import mock
mock = mock.MagicMock()
def wrapper(self, *args, **kwargs):
mock(*args, **kwargs)
return wrapped_function(self, *args, **kwargs)
wrapper.mock = mock
return wrapper
def get_fixture(request, name, default=None):
result = default
if name in request.node.fixturenames:
result = request.getfixturevalue(name)
return result

View file

@ -30,38 +30,47 @@ def output_should_match(regex, cli_run):
assert matches, f"\nRegex didn't match:\n{regex}\n{str(out)}\n{str(matches)}"
@then(parse("the output should contain\n{expected_output}"))
@then(parse('the output should contain "{expected_output}"'))
@then('the output should contain "<expected_output>"')
@then(parse("the {which_output_stream} output should contain\n{expected_output}"))
@then(parse('the {which_output_stream} output should contain "{expected_output}"'))
def output_should_contain(expected_output, which_output_stream, cli_run):
@then(parse("the output {should_or_should_not} contain\n{expected_output}"))
@then(parse('the output {should_or_should_not} contain "{expected_output}"'))
@then(
parse(
"the {which_output_stream} output {should_or_should_not} contain\n{expected_output}"
)
)
@then(
parse(
'the {which_output_stream} output {should_or_should_not} contain "{expected_output}"'
)
)
def output_should_contain(
expected_output, which_output_stream, cli_run, should_or_should_not
):
we_should = parse_should_or_should_not(should_or_should_not)
assert expected_output
if which_output_stream is None:
assert (expected_output in cli_run["stdout"]) or (
expected_output in cli_run["stderr"]
assert ((expected_output in cli_run["stdout"]) == we_should) or (
(expected_output in cli_run["stderr"]) == we_should
)
elif which_output_stream == "standard":
assert expected_output in cli_run["stdout"]
assert (expected_output in cli_run["stdout"]) == we_should
elif which_output_stream == "error":
assert expected_output in cli_run["stderr"]
assert (expected_output in cli_run["stderr"]) == we_should
else:
assert expected_output in cli_run[which_output_stream]
assert (expected_output in cli_run[which_output_stream]) == we_should
@then(parse("the output should not contain\n{expected_output}"))
@then(parse('the output should not contain "{expected_output}"'))
@then('the output should not contain "<expected_output>"')
def output_should_not_contain(expected_output, cli_run):
assert expected_output not in cli_run["stdout"]
@then(parse("the output should be\n{expected_output}"))
@then(parse('the output should be "{expected_output}"'))
@then('the output should be "<expected_output>"')
def output_should_be(expected_output, cli_run):
actual = cli_run["stdout"].strip()
expected = expected_output.strip()
@ -75,7 +84,6 @@ def output_should_be_empty(cli_run):
@then(parse('the output should contain the date "{date}"'))
@then('the output should contain the date "<date>"')
def output_should_contain_date(date, cli_run):
assert date and date in cli_run["stdout"]
@ -94,12 +102,6 @@ def output_should_be_columns_wide(cli_run, width):
assert len(line) <= width
@then(parse('we should see the message "{text}"'))
def should_see_the_message(text, cli_run):
out = cli_run["stderr"]
assert text in out, [text, out]
@then(
parse(
'the config for journal "{journal_name}" {should_or_should_not} contain "{some_yaml}"'
@ -126,10 +128,7 @@ def config_var_on_disk(config_on_disk, journal_name, should_or_should_not, some_
# `expected` objects formatted in yaml only compare one level deep
actual_slice = {key: actual.get(key, None) for key in expected.keys()}
if we_should:
assert expected == actual_slice
else:
assert expected != actual_slice
assert (expected == actual_slice) == we_should
@then(
@ -160,10 +159,7 @@ def config_var_in_memory(
# `expected` objects formatted in yaml only compare one level deep
actual_slice = {key: get_nested_val(actual, key) for key in expected.keys()}
if we_should:
assert expected == actual_slice
else:
assert expected != actual_slice
assert (expected == actual_slice) == we_should
@then("we should be prompted for a password")
@ -355,10 +351,7 @@ def count_elements(number, item, cli_run):
def count_editor_args(num_args, cli_run, editor_state, should_or_should_not):
we_should = parse_should_or_should_not(should_or_should_not)
if we_should:
assert cli_run["mocks"]["editor"].called
else:
assert not cli_run["mocks"]["editor"].called
assert cli_run["mocks"]["editor"].called == we_should
if isinstance(num_args, int):
assert len(editor_state["command"]) == int(num_args)
@ -368,10 +361,7 @@ def count_editor_args(num_args, cli_run, editor_state, should_or_should_not):
def stdin_prompt_called(cli_run, should_or_should_not):
we_should = parse_should_or_should_not(should_or_should_not)
if we_should:
assert cli_run["mocks"]["stdin"].called
else:
assert not cli_run["mocks"]["stdin"].called
assert cli_run["mocks"]["stdin"].called == we_should
@then(parse('the editor filename should end with "{suffix}"'))

View file

@ -3,14 +3,12 @@
from contextlib import ExitStack
import os
from unittest.mock import patch
from pytest_bdd import parsers
from pytest_bdd import when
from pytest_bdd.parsers import parse
from pytest_bdd.parsers import re
from jrnl.cli import cli
from jrnl.os_compat import split_args
@when(parse('we change directory to "{directory_name}"'))
@ -21,103 +19,38 @@ def when_we_change_directory(directory_name):
os.chdir(directory_name)
# These variables are used in the `@when(re(...))` section below
command = '(?P<command>[^"]+)'
input_method = "(?P<input_method>enter|pipe)"
user_input = '(?P<user_input>[^"]+)'
@when(parse('we run "jrnl {command}" and {input_method}\n{user_input}'))
@when(
parsers.re(
'we run "jrnl (?P<command>[^"]+)" and (?P<input_method>enter|pipe) "(?P<user_input>[^"]+)"'
)
)
@when(parse('we run "jrnl" and {input_method} "{user_input}"'))
@when(re(f'we run "jrnl {command}" and {input_method} "{user_input}"'))
@when(re(f'we run "jrnl" and {input_method} "{user_input}"'))
@when(parse('we run "jrnl {command}"'))
@when('we run "jrnl <command>"')
@when('we run "jrnl"')
def we_run(
command,
config_path,
config_in_memory,
user_input,
cli_run,
capsys,
password,
cache_dir,
editor,
keyring,
input_method,
mocks,
):
assert input_method in ["", "enter", "pipe"]
is_tty = input_method != "pipe"
def we_run_jrnl(cli_run, capsys, keyring):
from keyring import set_keyring
if cache_dir["exists"]:
command = command.format(cache_dir=cache_dir["path"])
args = split_args(command)
status = 0
if user_input:
user_input = user_input.splitlines() if is_tty else [user_input]
if password:
password = password.splitlines()
if not password and user_input:
password = user_input
set_keyring(keyring)
with ExitStack() as stack:
# Always mock
from jrnl.override import apply_overrides
mocks = cli_run["mocks"]
factories = cli_run["mock_factories"]
def my_overrides(*args, **kwargs):
result = apply_overrides(*args, **kwargs)
config_in_memory["overrides"] = result
return result
stack.enter_context(
patch("jrnl.jrnl.apply_overrides", side_effect=my_overrides)
)
# Conditionally mock
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("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))
for id in factories:
mocks[id] = stack.enter_context(factories[id]())
try:
cli(args)
cli()
except StopIteration:
# This happens when input is expected, but don't have any input left
pass
except SystemExit as e:
status = e.code
cli_run["status"] = e.code
captured = capsys.readouterr()
cli_run["status"] = status
cli_run["stdout"] = captured.out
cli_run["stderr"] = captured.err
cli_run["mocks"] = {
"stdin": mock_stdin,
"input": mock_input,
"getpass": mock_getpass,
"editor": mock_editor,
}