Add config overrides steps to pytest

This requires some patching around the config object, which now happens
in every test.

Co-authored-by: Micah Jerome Ellison <micah.jerome.ellison@gmail.com>
This commit is contained in:
Jonathan Wren 2021-10-02 16:52:47 -07:00
parent e16e3a1f62
commit 9cd2250a61
8 changed files with 138 additions and 39 deletions

View file

@ -40,9 +40,7 @@ def run(args):
original_config = config.copy() original_config = config.copy()
# Apply config overrides # Apply config overrides
overrides = args.config_override config = apply_overrides(args, config)
if overrides:
config = apply_overrides(overrides, config)
args = get_journal_name(args, config) args = get_journal_name(args, config)
config = scope_config(config, args.journal_name) config = scope_config(config, args.journal_name)

View file

@ -1,7 +1,8 @@
from .config import update_config, make_yaml_valid_dict from .config import update_config, make_yaml_valid_dict
from argparse import Namespace
# import logging # import logging
def apply_overrides(overrides: list, base_config: dict) -> dict: def apply_overrides(args: Namespace, base_config: dict) -> dict:
"""Unpack CLI provided overrides into the configuration tree. """Unpack CLI provided overrides into the configuration tree.
:param overrides: List of configuration key-value pairs collected from the CLI :param overrides: List of configuration key-value pairs collected from the CLI
@ -11,6 +12,10 @@ def apply_overrides(overrides: list, base_config: dict) -> dict:
:return: Configuration to be used during runtime with the overrides applied :return: Configuration to be used during runtime with the overrides applied
:rtype: dict :rtype: dict
""" """
overrides = vars(args).get("config_override", None)
if not overrides:
return base_config
cfg_with_overrides = base_config.copy() cfg_with_overrides = base_config.copy()
for pairs in overrides: for pairs in overrides:

View file

@ -8,13 +8,11 @@ Feature: Implementing Runtime Overrides for Select Configuration Keys
And the editor should not have been called And the editor should not have been called
# @todo implement this step in pytest (doesn't currently support overrides)
@skip
Scenario: Postconfig commands with overrides Scenario: Postconfig commands with overrides
Given we use the config "basic_encrypted.yaml" Given we use the config "basic_encrypted.yaml"
And we use the password "test" if prompted And we use the password "test" if prompted
When we run "jrnl --decrypt --config-override highlight false --config-override editor nano" When we run "jrnl --decrypt --config-override highlight false --config-override editor nano"
Then the config should contain "highlight: false" Then the config in memory should contain "highlight: false"
Then the editor should not have been called Then the editor should not have been called
@ -38,23 +36,24 @@ Feature: Implementing Runtime Overrides for Select Configuration Keys
# @todo implement this step in pytest (doesn't currently support overrides)
@skip
Scenario: Override color selections with runtime overrides Scenario: Override color selections with runtime overrides
Given we use the config "basic_encrypted.yaml" Given we use the config "basic_encrypted.yaml"
And we use the password "test" if prompted And we use the password "test" if prompted
When we run "jrnl -1 --config-override colors.body blue" When we run "jrnl -1 --config-override colors.body blue"
Then the config should have "colors.body" set to "blue" Then the config in memory should contain "colors.body: blue"
# @todo implement this step in pytest (doesn't currently support overrides)
@skip
Scenario: Apply multiple config overrides Scenario: Apply multiple config overrides
Given we use the config "basic_encrypted.yaml" Given we use the config "basic_encrypted.yaml"
And we use the password "test" if prompted And we use the password "test" if prompted
When we run "jrnl -1 --config-override colors.body green --config-override editor 'nano'" When we run "jrnl -1 --config-override colors.body green --config-override editor 'nano'"
Then the config should have "colors.body" set to "green" Then the config in memory should contain
And the config should have "editor" set to "nano" editor: nano
colors:
title: none
body: green
tags: none
date: none
Scenario: Override default journal Scenario: Override default journal

View file

@ -143,10 +143,15 @@ def user_input():
@fixture @fixture
def config_data(config_path): def config_on_disk(config_path):
return load_config(config_path) return load_config(config_path)
@fixture
def config_in_memory():
return dict()
@fixture @fixture
def journal_name(): def journal_name():
return None return None

View file

@ -1,6 +1,7 @@
# Copyright (C) 2012-2021 jrnl contributors # Copyright (C) 2012-2021 jrnl contributors
# License: https://www.gnu.org/licenses/gpl-3.0.html # License: https://www.gnu.org/licenses/gpl-3.0.html
import functools
import os import os
@ -38,3 +39,13 @@ def assert_equal_tags_ignoring_order(
[actual_tags, expected_tags], [actual_tags, expected_tags],
[expected_content, actual_content], [expected_content, actual_content],
] ]
# @see: https://stackoverflow.com/a/65782539/569146
def get_nested_val(dictionary, path, *default):
try:
return functools.reduce(lambda x, y: x[y], path.split("."), dictionary)
except KeyError:
if default:
return default[0]
raise

View file

@ -15,6 +15,7 @@ from jrnl.config import scope_config
from .helpers import assert_equal_tags_ignoring_order from .helpers import assert_equal_tags_ignoring_order
from .helpers import does_directory_contain_files from .helpers import does_directory_contain_files
from .helpers import parse_should_or_should_not from .helpers import parse_should_or_should_not
from .helpers import get_nested_val
@then("we should get no error") @then("we should get no error")
@ -110,10 +111,10 @@ def should_see_the_message(text, cli_run):
) )
@then(parse('the config {should_or_should_not} contain "{some_yaml}"')) @then(parse('the config {should_or_should_not} contain "{some_yaml}"'))
@then(parse("the config {should_or_should_not} contain\n{some_yaml}")) @then(parse("the config {should_or_should_not} contain\n{some_yaml}"))
def config_var(config_data, journal_name, should_or_should_not, some_yaml): def config_var_on_disk(config_on_disk, journal_name, should_or_should_not, some_yaml):
we_should = parse_should_or_should_not(should_or_should_not) we_should = parse_should_or_should_not(should_or_should_not)
actual = config_data actual = config_on_disk
if journal_name: if journal_name:
actual = actual["journals"][journal_name] actual = actual["journals"][journal_name]
@ -121,6 +122,7 @@ def config_var(config_data, journal_name, should_or_should_not, some_yaml):
actual_slice = actual actual_slice = actual
if type(actual) is dict: if type(actual) is dict:
# `expected` objects formatted in yaml only compare one level deep
actual_slice = {key: actual.get(key, None) for key in expected.keys()} actual_slice = {key: actual.get(key, None) for key in expected.keys()}
if we_should: if we_should:
@ -129,6 +131,40 @@ def config_var(config_data, journal_name, should_or_should_not, some_yaml):
assert expected != actual_slice assert expected != actual_slice
@then(
parse(
'the config in memory for journal "{journal_name}" {should_or_should_not} contain "{some_yaml}"'
)
)
@then(
parse(
'the config in memory for journal "{journal_name}" {should_or_should_not} contain\n{some_yaml}'
)
)
@then(parse('the config in memory {should_or_should_not} contain "{some_yaml}"'))
@then(parse("the config in memory {should_or_should_not} contain\n{some_yaml}"))
def config_var_in_memory(
config_in_memory, journal_name, should_or_should_not, some_yaml
):
we_should = parse_should_or_should_not(should_or_should_not)
actual = config_in_memory["overrides"]
if journal_name:
actual = actual["journals"][journal_name]
expected = yaml.load(some_yaml, Loader=yaml.SafeLoader)
actual_slice = actual
if type(actual) is dict:
# `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
@then("we should be prompted for a password") @then("we should be prompted for a password")
def password_was_called(cli_run): def password_was_called(cli_run):
assert cli_run["mocks"]["getpass"].called assert cli_run["mocks"]["getpass"].called
@ -145,15 +181,15 @@ def assert_dir_contains_files(file_list, cache_dir):
@then(parse("the journal directory should contain\n{file_list}")) @then(parse("the journal directory should contain\n{file_list}"))
def journal_directory_should_contain(config_data, file_list): def journal_directory_should_contain(config_on_disk, file_list):
scoped_config = scope_config(config_data, "default") scoped_config = scope_config(config_on_disk, "default")
assert does_directory_contain_files(file_list, scoped_config["journal"]) assert does_directory_contain_files(file_list, scoped_config["journal"])
@then(parse('journal "{journal_name}" should not exist')) @then(parse('journal "{journal_name}" should not exist'))
def journal_directory_should_not_exist(config_data, journal_name): def journal_directory_should_not_exist(config_on_disk, journal_name):
scoped_config = scope_config(config_data, journal_name) scoped_config = scope_config(config_on_disk, journal_name)
assert not does_directory_contain_files( assert not does_directory_contain_files(
scoped_config["journal"], "." scoped_config["journal"], "."
@ -161,8 +197,8 @@ def journal_directory_should_not_exist(config_data, journal_name):
@then(parse("the journal {should_or_should_not} exist")) @then(parse("the journal {should_or_should_not} exist"))
def journal_should_not_exist(config_data, should_or_should_not): def journal_should_not_exist(config_on_disk, should_or_should_not):
scoped_config = scope_config(config_data, "default") scoped_config = scope_config(config_on_disk, "default")
expected_path = scoped_config["journal"] expected_path = scoped_config["journal"]
contains_files = does_directory_contain_files(expected_path, ".") contains_files = does_directory_contain_files(expected_path, ".")

View file

@ -34,6 +34,7 @@ def when_we_change_directory(directory_name):
def we_run( def we_run(
command, command,
config_path, config_path,
config_in_memory,
user_input, user_input,
cli_run, cli_run,
capsys, capsys,
@ -63,7 +64,19 @@ def we_run(
password = user_input password = user_input
with ExitStack() as stack: with ExitStack() as stack:
# Always mock
from jrnl.override import apply_overrides
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)) stack.enter_context(patch("sys.argv", ["jrnl"] + args))
mock_stdin = stack.enter_context( mock_stdin = stack.enter_context(

View file

@ -6,6 +6,8 @@ from jrnl.override import _get_key_and_value_from_pair
from jrnl.override import _recursively_apply from jrnl.override import _recursively_apply
from jrnl.override import apply_overrides from jrnl.override import apply_overrides
from argparse import Namespace
@pytest.fixture() @pytest.fixture()
def minimal_config(): def minimal_config():
@ -18,31 +20,61 @@ def minimal_config():
return cfg return cfg
def expected_args(overrides):
default_args = {
"contains": None,
"debug": False,
"delete": False,
"edit": False,
"end_date": None,
"today_in_history": False,
"month": None,
"day": None,
"year": None,
"excluded": [],
"export": False,
"filename": None,
"limit": None,
"on_date": None,
"preconfig_cmd": None,
"postconfig_cmd": None,
"short": False,
"starred": False,
"start_date": None,
"strict": False,
"tags": False,
"text": [],
"config_override": [],
}
return Namespace(**{**default_args, **overrides})
def test_apply_override(minimal_config): def test_apply_override(minimal_config):
overrides = [["editor", "nano"]] overrides = {"config_override": [["editor", "nano"]]}
apply_overrides(overrides, minimal_config) apply_overrides(expected_args(overrides), minimal_config)
assert minimal_config["editor"] == "nano" assert minimal_config["editor"] == "nano"
def test_override_dot_notation(minimal_config): def test_override_dot_notation(minimal_config):
overrides = [["colors.body", "blue"]] overrides = {"config_override": [["colors.body", "blue"]]}
apply_overrides(expected_args(overrides), minimal_config)
cfg = apply_overrides(overrides=overrides, base_config=minimal_config) assert minimal_config["colors"] == {"body": "blue", "date": "green"}
assert cfg["colors"] == {"body": "blue", "date": "green"}
def test_multiple_overrides(minimal_config): def test_multiple_overrides(minimal_config):
overrides = [ overrides = {
"config_override": [
["colors.title", "magenta"], ["colors.title", "magenta"],
["editor", "nano"], ["editor", "nano"],
["journals.burner", "/tmp/journals/burner.jrnl"], ["journals.burner", "/tmp/journals/burner.jrnl"],
] # as returned by parse_args, saved in parser.config_override ]
}
cfg = apply_overrides(overrides, minimal_config) actual = apply_overrides(expected_args(overrides), minimal_config)
assert cfg["editor"] == "nano" assert actual["editor"] == "nano"
assert cfg["colors"]["title"] == "magenta" assert actual["colors"]["title"] == "magenta"
assert "burner" in cfg["journals"] assert "burner" in actual["journals"]
assert cfg["journals"]["burner"] == "/tmp/journals/burner.jrnl" assert actual["journals"]["burner"] == "/tmp/journals/burner.jrnl"
def test_recursively_apply(): def test_recursively_apply():