refactor(application): refactor questions to separate class (#7)

* refactor(application): refactor questions to separate class
* test(application): add tests for`Application` class
This commit is contained in:
Nathaniel Landau
2023-01-25 12:20:59 -05:00
committed by GitHub
parent 1e4fbcb4e2
commit 455a2c9e86
11 changed files with 1169 additions and 304 deletions

View File

@@ -1,19 +1,414 @@
# type: ignore
"""Tests for the application module."""
"""Tests for the application module.
How mocking works in this test suite:
1. The main_app() method is mocked using a side effect iterable. This allows us to pass a value in the first run, and then a KeyError in the second run to exit the loop.
2. All questions are mocked using return_value. This allows us to pass in a value to the question and then the method will return that value. This is useful for testing questionary prompts without user input.
"""
import re
import pytest
from tests.helpers import Regex
from obsidian_metadata._config import Config
from obsidian_metadata.models.application import Application
def test_load_vault(test_vault) -> None:
def test_instantiate_application(test_application) -> None:
"""Test application."""
vault_path = test_vault
config = Config(config_path="tests/fixtures/test_vault_config.toml", vault_path=vault_path)
vault_config = config.vaults[0]
app = Application(config=vault_config, dry_run=False)
app = test_application
app.load_vault()
assert app.dry_run is False
assert app.config == vault_config
assert app.vault.num_notes() == 3
assert app.config.name == "command_line_vault"
assert app.config.exclude_paths == [".git", ".obsidian"]
assert app.dry_run is False
assert app.vault.num_notes() == 13
def test_abort(test_application, mocker, capsys) -> None:
"""Test renaming a key."""
app = test_application
app.load_vault()
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_main_application",
return_value="abort",
)
app.main_app()
captured = capsys.readouterr()
assert "Vault Info" in captured.out
assert "Done!" in captured.out
def test_list_notes(test_application, mocker, capsys) -> None:
"""Test renaming a key."""
app = test_application
app.load_vault()
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_main_application",
side_effect=["list_notes", KeyError],
)
with pytest.raises(KeyError):
app.main_app()
captured = capsys.readouterr()
assert "04 no metadata/no_metadata_1.md" in captured.out
assert "02 inline/inline 2.md" in captured.out
assert "+inbox/Untitled.md" in captured.out
assert "00 meta/templates/data sample.md" in captured.out
def test_all_metadata(test_application, mocker, capsys) -> None:
"""Test renaming a key."""
app = test_application
app.load_vault()
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_main_application",
side_effect=["all_metadata", KeyError],
)
with pytest.raises(KeyError):
app.main_app()
captured = capsys.readouterr()
expected = re.escape("┃ Keys ┃ Values")
assert captured.out == Regex(expected)
expected = re.escape("Inline Tags │ breakfast")
assert captured.out == Regex(expected)
def test_filter_notes(test_application, mocker, capsys) -> None:
"""Test renaming a key."""
app = test_application
app.load_vault()
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_main_application",
side_effect=["filter_notes", "list_notes", KeyError],
)
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_for_filter_path",
return_value="inline",
)
with pytest.raises(KeyError):
app.main_app()
captured = capsys.readouterr()
assert "04 no metadata/no_metadata_1.md" not in captured.out
assert "02 inline/inline 1.md" in captured.out
assert "02 inline/inline 2.md" in captured.out
assert "+inbox/Untitled.md" not in captured.out
assert "00 meta/templates/data sample.md" not in captured.out
def test_rename_key_success(test_application, mocker, capsys) -> None:
"""Test renaming a key."""
app = test_application
app.load_vault()
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_main_application",
side_effect=["rename_key", KeyError],
)
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_for_existing_key",
return_value="tags",
)
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_for_new_key",
return_value="new_tags",
)
with pytest.raises(KeyError):
app.main_app()
captured = capsys.readouterr()
assert captured.out == Regex(r"Renamed.*tags.*to.*new_tags.*in.*\d+.*notes", re.DOTALL)
def test_rename_key_fail(test_application, mocker, capsys) -> None:
"""Test renaming a key."""
app = test_application
app.load_vault()
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_main_application",
side_effect=["rename_key", KeyError],
)
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_for_existing_key",
return_value="tag",
)
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_for_new_key",
return_value="new_tags",
)
with pytest.raises(KeyError):
app.main_app()
captured = capsys.readouterr()
assert "WARNING | No notes were changed" in captured.out
def test_rename_inline_tag_success(test_application, mocker, capsys) -> None:
"""Test renaming an inline tag."""
app = test_application
app.load_vault()
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_main_application",
side_effect=["rename_inline_tag", KeyError],
)
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_for_existing_inline_tag",
return_value="breakfast",
)
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_for_new_tag",
return_value="new_tag",
)
with pytest.raises(KeyError):
app.main_app()
captured = capsys.readouterr()
assert captured.out == Regex(r"Renamed.*breakfast.*to.*new_tag.*in.*\d+.*notes", re.DOTALL)
def test_rename_inline_tag_fail(test_application, mocker, capsys) -> None:
"""Test renaming an inline tag."""
app = test_application
app.load_vault()
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_main_application",
side_effect=["rename_inline_tag", KeyError],
)
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_for_existing_inline_tag",
return_value="not_a_tag",
)
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_for_new_tag",
return_value="new_tag",
)
with pytest.raises(KeyError):
app.main_app()
captured = capsys.readouterr()
assert captured.out == Regex(r"WARNING +\| No notes were changed", re.DOTALL)
def test_delete_inline_tag_success(test_application, mocker, capsys) -> None:
"""Test renaming an inline tag."""
app = test_application
app.load_vault()
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_main_application",
side_effect=["delete_inline_tag", KeyError],
)
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_for_existing_inline_tag",
return_value="breakfast",
)
with pytest.raises(KeyError):
app.main_app()
captured = capsys.readouterr()
assert captured.out == Regex(r"SUCCESS +\| Deleted.*\d+.*notes", re.DOTALL)
def test_delete_inline_tag_fail(test_application, mocker, capsys) -> None:
"""Test renaming an inline tag."""
app = test_application
app.load_vault()
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_main_application",
side_effect=["delete_inline_tag", KeyError],
)
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_for_existing_inline_tag",
return_value="not_a_tag_in_vault",
)
with pytest.raises(KeyError):
app.main_app()
captured = capsys.readouterr()
assert captured.out == Regex(r"WARNING +\| No notes were changed", re.DOTALL)
def test_delete_key_success(test_application, mocker, capsys) -> None:
"""Test renaming an inline tag."""
app = test_application
app.load_vault()
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_main_application",
side_effect=["delete_key", KeyError],
)
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_for_existing_keys_regex",
return_value=r"d\w+",
)
with pytest.raises(KeyError):
app.main_app()
captured = capsys.readouterr()
assert captured.out == Regex(
r"SUCCESS +\|.*Deleted.*keys.*matching:.*d\\w\+.*from.*10", re.DOTALL
)
def test_delete_key_fail(test_application, mocker, capsys) -> None:
"""Test renaming an inline tag."""
app = test_application
app.load_vault()
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_main_application",
side_effect=["delete_key", KeyError],
)
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_for_existing_keys_regex",
return_value=r"\d{7}",
)
with pytest.raises(KeyError):
app.main_app()
captured = capsys.readouterr()
assert captured.out == Regex(r"WARNING +\| No notes found with a.*key.*matching", re.DOTALL)
def test_rename_value_success(test_application, mocker, capsys) -> None:
"""Test renaming an inline tag."""
app = test_application
app.load_vault()
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_main_application",
side_effect=["rename_value", KeyError],
)
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_for_existing_key",
return_value="area",
)
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_for_existing_value",
return_value="frontmatter",
)
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_for_new_value",
return_value="new_key",
)
with pytest.raises(KeyError):
app.main_app()
captured = capsys.readouterr()
assert captured.out == Regex(
r"SUCCESS | Renamed 'area:frontmatter' to 'area:new_key'", re.DOTALL
)
assert captured.out == Regex(r".*in.*\d+.*notes.*", re.DOTALL)
def test_rename_value_fail(test_application, mocker, capsys) -> None:
"""Test renaming an inline tag."""
app = test_application
app.load_vault()
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_main_application",
side_effect=["rename_value", KeyError],
)
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_for_existing_key",
return_value="area",
)
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_for_existing_value",
return_value="not_exists",
)
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_for_new_value",
return_value="new_key",
)
with pytest.raises(KeyError):
app.main_app()
captured = capsys.readouterr()
assert captured.out == Regex(r"WARNING +\| No notes were changed", re.DOTALL)
def test_delete_value_success(test_application, mocker, capsys) -> None:
"""Test renaming an inline tag."""
app = test_application
app.load_vault()
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_main_application",
side_effect=["delete_value", KeyError],
)
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_for_existing_key",
return_value="area",
)
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_for_existing_value_regex",
return_value=r"^front\w+$",
)
with pytest.raises(KeyError):
app.main_app()
captured = capsys.readouterr()
assert captured.out == Regex(
r"SUCCESS +\| Deleted value.*\^front\\w\+\$.*from.*key.*area.*in.*\d+.*notes", re.DOTALL
)
def test_delete_value_fail(test_application, mocker, capsys) -> None:
"""Test renaming an inline tag."""
app = test_application
app.load_vault()
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_main_application",
side_effect=["delete_value", KeyError],
)
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_for_existing_key",
return_value="area",
)
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_for_existing_value_regex",
return_value=r"\d{7}",
)
with pytest.raises(KeyError):
app.main_app()
captured = capsys.readouterr()
assert captured.out == Regex(r"WARNING +\| No notes found matching:", re.DOTALL)
def test_review_no_changes(test_application, mocker, capsys) -> None:
"""Review changes when no changes to vault."""
app = test_application
app.load_vault()
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_main_application",
side_effect=["review_changes", KeyError],
)
with pytest.raises(KeyError):
app.main_app()
captured = capsys.readouterr()
assert captured.out == Regex(r"INFO +\| No changes to review", re.DOTALL)
def test_review_changes(test_application, mocker, capsys) -> None:
"""Review changes when no changes to vault."""
app = test_application
app.load_vault()
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_main_application",
side_effect=["delete_key", "review_changes", KeyError],
)
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_for_existing_keys_regex",
return_value=r"d\w+",
)
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_confirm",
return_value=True,
)
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_for_selection",
side_effect=[1, "return"],
)
with pytest.raises(KeyError):
app.main_app()
captured = capsys.readouterr()
assert captured.out == Regex(r".*Found.*\d+.*changed notes in the vault.*", re.DOTALL)
assert "- date_created: 2022-12-22" in captured.out
assert "+ - breakfast" in captured.out

View File

@@ -7,7 +7,14 @@ from textwrap import dedent
import pytest
import typer
from obsidian_metadata._config import Config
from obsidian_metadata._config.config import Config, ConfigQuestions
def test_validate_valid_dir() -> None:
"""Test vault validation."""
assert ConfigQuestions._validate_valid_dir("tests/") is True
assert "Path is not a directory" in ConfigQuestions._validate_valid_dir("pyproject.toml")
assert "Path does not exist" in ConfigQuestions._validate_valid_dir("tests/vault2")
def test_broken_config_file(capsys) -> None:
@@ -79,8 +86,10 @@ def test_no_config_no_vault(tmp_path, mocker) -> None:
"""Test creating a config on first run."""
fake_vault = Path(tmp_path / "vault")
fake_vault.mkdir()
mocker.patch(
"obsidian_metadata._config.config.Questions.ask_for_vault_path", return_value=fake_vault
"obsidian_metadata._config.config.ConfigQuestions.ask_for_vault_path",
return_value=fake_vault,
)
config_file = Path(tmp_path / "config.toml")

View File

@@ -6,6 +6,9 @@ from pathlib import Path
import pytest
from obsidian_metadata._config import Config
from obsidian_metadata.models.application import Application
def remove_all(root: Path):
"""Remove all files and directories in a directory."""
@@ -72,3 +75,27 @@ def test_vault(tmp_path) -> Path:
if backup_dir.exists():
shutil.rmtree(backup_dir)
@pytest.fixture()
def test_application(tmp_path) -> Application:
"""Fixture which creates a sample vault."""
source_dir = Path(__file__).parent / "fixtures" / "sample_vault"
dest_dir = Path(tmp_path / "application")
backup_dir = Path(f"{dest_dir}.bak")
if not source_dir.exists():
raise FileNotFoundError(f"Sample vault not found: {source_dir}")
shutil.copytree(source_dir, dest_dir)
config = Config(config_path="tests/fixtures/test_vault_config.toml", vault_path=dest_dir)
vault_config = config.vaults[0]
app = Application(config=vault_config, dry_run=False)
yield app
# after test - remove fixtures
shutil.rmtree(dest_dir)
if backup_dir.exists():
shutil.rmtree(backup_dir)

View File

@@ -1,12 +1,113 @@
# type: ignore
"""Test the questions class."""
from pathlib import Path
from obsidian_metadata._utils import Questions
from obsidian_metadata._config import Config
from obsidian_metadata.models.questions import Questions
from obsidian_metadata.models.vault import Vault
VAULT_PATH = Path("tests/fixtures/test_vault")
CONFIG = Config(config_path="tests/fixtures/test_vault_config.toml", vault_path=VAULT_PATH)
VAULT_CONFIG = CONFIG.vaults[0]
VAULT = Vault(config=VAULT_CONFIG)
def test_vault_validation():
def test_validate_valid_dir() -> None:
"""Test vault validation."""
assert Questions._validate_vault("tests/") is True
assert "Path is not a directory" in Questions._validate_vault("pyproject.toml")
assert "Path does not exist" in Questions._validate_vault("tests/vault2")
questions = Questions(vault=VAULT)
assert questions._validate_valid_dir("tests/") is True
assert "Path is not a directory" in questions._validate_valid_dir("pyproject.toml")
assert "Path does not exist" in questions._validate_valid_dir("tests/vault2")
def test_validate_valid_regex() -> None:
"""Test regex validation."""
questions = Questions(vault=VAULT)
assert questions._validate_valid_vault_regex(r".*\.md") is True
assert "Invalid regex" in questions._validate_valid_vault_regex("[")
assert "Regex cannot be empty" in questions._validate_valid_vault_regex("")
assert "Regex does not match paths" in questions._validate_valid_vault_regex(r"\d\d\d\w\d")
def test_validate_key_exists() -> None:
"""Test key validation."""
questions = Questions(vault=VAULT)
assert "'test' does not exist" in questions._validate_key_exists("test")
assert "Key cannot be empty" in questions._validate_key_exists("")
assert questions._validate_key_exists("frontmatter_Key1") is True
def test_validate_new_key() -> None:
"""Test new key validation."""
questions = Questions(vault=VAULT)
assert "Key cannot contain spaces or special characters" in questions._validate_new_key(
"new key"
)
assert "Key cannot contain spaces or special characters" in questions._validate_new_key(
"new_key!"
)
assert "New key cannot be empty" in questions._validate_new_key("")
assert questions._validate_new_key("new_key") is True
def test_validate_new_tag() -> None:
"""Test new tag validation."""
questions = Questions(vault=VAULT)
assert "New tag cannot be empty" in questions._validate_new_tag("")
assert "Tag cannot contain spaces or special characters" in questions._validate_new_tag(
"new tag"
)
assert questions._validate_new_tag("new_tag") is True
def test_validate_existing_inline_tag() -> None:
"""Test existing tag validation."""
questions = Questions(vault=VAULT)
assert "Tag cannot be empty" in questions._validate_existing_inline_tag("")
assert "'test' does not exist" in questions._validate_existing_inline_tag("test")
assert questions._validate_existing_inline_tag("shared_tag") is True
def test_validate_key_exists_regex() -> None:
"""Test key exists regex validation."""
questions = Questions(vault=VAULT)
assert "'test' does not exist" in questions._validate_key_exists_regex("test")
assert "Key cannot be empty" in questions._validate_key_exists_regex("")
assert "Invalid regex" in questions._validate_key_exists_regex("[")
assert questions._validate_key_exists_regex(r"\w+_Key\d") is True
def test_validate_value() -> None:
"""Test value validation."""
questions = Questions(vault=VAULT)
assert questions._validate_value("test") is True
assert "Value cannot be empty" in questions._validate_value("")
questions2 = Questions(vault=VAULT, key="frontmatter_Key1")
assert questions2._validate_value("test") == "frontmatter_Key1:test does not exist"
assert "Value cannot be empty" in questions2._validate_value("")
assert questions2._validate_value("author name") is True
def test_validate_value_exists_regex() -> None:
"""Test value exists regex validation."""
questions2 = Questions(vault=VAULT, key="frontmatter_Key1")
assert "Invalid regex" in questions2._validate_value_exists_regex("[")
assert "Regex cannot be empty" in questions2._validate_value_exists_regex("")
assert (
questions2._validate_value_exists_regex(r"\d\d\d\w\d")
== r"No values in frontmatter_Key1 match regex: \d\d\d\w\d"
)
assert questions2._validate_value_exists_regex(r"^author \w+") is True
def test_validate_new_value() -> None:
"""Test new value validation."""
questions = Questions(vault=VAULT, key="frontmatter_Key1")
assert questions._validate_new_value("new_value") is True
assert "Value cannot be empty" in questions._validate_new_value("")
assert (
questions._validate_new_value("author name")
== "frontmatter_Key1:author name already exists"
)

View File

@@ -190,8 +190,8 @@ def test_delete_inline_tag(test_vault) -> None:
vault_config = config.vaults[0]
vault = Vault(config=vault_config)
assert vault.delete_inline_tag("no tag") is False
assert vault.delete_inline_tag("intext_tag2") is True
assert vault.delete_inline_tag("no tag") == 0
assert vault.delete_inline_tag("intext_tag2") == 2
assert vault.metadata.dict["Inline Tags"] == [
"ignored_file_tag2",
"inline_tag_bottom1",
@@ -227,8 +227,8 @@ def test_rename_inline_tag(test_vault) -> None:
vault_config = config.vaults[0]
vault = Vault(config=vault_config)
assert vault.rename_inline_tag("no tag", "new_tag") is False
assert vault.rename_inline_tag("intext_tag2", "new_tag") is True
assert vault.rename_inline_tag("no tag", "new_tag") == 0
assert vault.rename_inline_tag("intext_tag2", "new_tag") == 2
assert vault.metadata.dict["Inline Tags"] == [
"ignored_file_tag2",
"inline_tag_bottom1",
@@ -248,10 +248,10 @@ def test_rename_metadata(test_vault) -> None:
vault_config = config.vaults[0]
vault = Vault(config=vault_config)
assert vault.rename_metadata("no key", "new_key") is False
assert vault.rename_metadata("tags", "nonexistent_value", "new_vaule") is False
assert vault.rename_metadata("no key", "new_key") == 0
assert vault.rename_metadata("tags", "nonexistent_value", "new_vaule") == 0
assert vault.rename_metadata("tags", "frontmatter_tag1", "new_vaule") is True
assert vault.rename_metadata("tags", "frontmatter_tag1", "new_vaule") == 2
assert vault.metadata.dict["tags"] == [
"frontmatter_tag2",
"frontmatter_tag3",
@@ -261,7 +261,7 @@ def test_rename_metadata(test_vault) -> None:
"📅/frontmatter_tag3",
]
assert vault.rename_metadata("tags", "new_key") is True
assert vault.rename_metadata("tags", "new_key") == 2
assert "tags" not in vault.metadata.dict
assert vault.metadata.dict["new_key"] == [
"frontmatter_tag2",