Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add pytest hooks for importer integration tests #118

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 181 additions & 0 deletions beangulp/regression_pytest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
"""Support for implementing regression tests on sample files using pytest.

This module provides definitions for testing a custom importer against a set of
existing downloaded files, running the various importer interface methods on it,
and comparing the output to an expected text file. (Expected test files can be
auto-generated using the --generate option). You use it like this:

from beangulp import regression_pytest
...
import mymodule
...

# Create your importer instance used for testing.
importer = mymodule.Importer(...)

# Select a directory where your test files are to be located.
directory = ...

# Create a test case using the base in this class.

@regression_pytest.with_importer(importer)
@regression_pytest.with_testdir(directory)
class TestImporter(regtest.ImporterTestBase):
pass

Also, to add the --generate option to 'pytest', you must create a conftest.py
somewhere in one of the roots above your importers with this module as a plugin:

pytest_plugins = "beancount.ingest.regression_pytest"

See beancount/example/ingest for a full working example.

How to invoke the tests:

Via pytest. First run your test with the --generate option to generate all the
expected files. Then inspect them visually for correctness. Finally, check them
in to preserve them. You should be able to regress against those correct outputs
in the future. Use version control to your advantage to visualize the
differences.
"""
__copyright__ = "Copyright (C) 2018 Martin Blais"
__license__ = "GNU GPLv2"

from os import path
import io
import os
import pytest
import re
import unittest

from beangulp import cache, extract

from beancount.parser import printer


def pytest_addoption(parser):
"""Add an option to generate the expected files for the tests."""
group = parser.getgroup("beancount")
group.addoption(
"--generate",
"--gen",
action="store_true",
help="Don't test; rather, generate the expected files",
)


def with_importer(importer):
"""Parametrizing fixture that provides the importer to test."""
return pytest.mark.parametrize("importer", [importer])


def with_testdir(directory):
"""Parametrizing fixture that provides files from a directory."""
directory = os.path.abspath(directory) # TODO: make this more robust

return pytest.mark.parametrize(
"file", list(find_input_files(directory))
)


def find_input_files(directory):
"""Find the input files in the module where the class is defined.

Args:
directory: A string, the path to a root directory to check for.
Yields:
Strings, the absolute filenames of sample input and expected files.
"""
for sroot, dirs, files in os.walk(directory):
for filename in files:
if re.match(
r".*\.(extract|file_date|file_name|file_account|py|pyc|DS_Store)$",
filename,
):
continue
yield path.join(sroot, filename)


def assertStringEqualNoWS(actual_string: str, expected_string: str):
"""Assert two strings are equal disregarding whitespace."""
actual_string_nows = re.sub(r"[ \t\n]+", " ", actual_string.strip())
expected_string_nows = re.sub(r"[ \t\n]+", " ", expected_string.strip())
msg = f"{actual_string} != {expected_string}"
assert actual_string_nows == expected_string_nows


def compare_contents_or_generate(actual_string, expect_fn, generate):
"""Compare a string to the contents of an expect file.

Assert if different; auto-generate otherwise.

Args:
actual_string: The expected string contents.
expect_fn: The filename whose contents to read and compare against.
generate: A boolean, true if we are to generate the tests.
"""
if generate:
with open(expect_fn, "w", encoding="utf-8") as expect_file:
expect_file.write(actual_string)
if actual_string and not actual_string.endswith("\n"):
expect_file.write("\n")
pytest.skip("Generated '{}'".format(expect_fn))
else:
# Run the test on an existing expected file.
assert path.exists(
expect_fn
), "Expected file '{}' is missing. Generate it?".format(expect_fn)
with open(expect_fn, encoding="utf-8") as infile:
expect_string = infile.read()
assertStringEqualNoWS(expect_string, actual_string)


class ImporterTestBase:
def test_identify(self, importer, file):
"""Attempt to identify a file and expect results to be true.

This method does not need to check against an existing expect file. It
is just assumed it should return True if your test is setup well (the
importer should always identify the test file).
"""
assert importer.identify(file)

def test_extract(self, importer, file, pytestconfig):
"""Extract entries from a test file and compare against expected output."""
entries = extract.extract_from_file(importer, file, None)
oss = io.StringIO()
printer.print_entries(entries, file=oss)
string = oss.getvalue()
compare_contents_or_generate(
string,
"{}.extract".format(file),
pytestconfig.getoption("generate", False),
)

def test_file_date(self, importer, file, pytestconfig):
"""Compute the imported file date and compare to an expected output."""
date = importer.date(file)
string = date.isoformat() if date else ""
compare_contents_or_generate(
string,
"{}.file_date".format(file),
pytestconfig.getoption("generate", False),
)

def test_file_name(self, importer, file, pytestconfig):
"""Compute the imported file name and compare to an expected output."""
filename = importer.filename(file) or ""
compare_contents_or_generate(
filename,
"{}.file_name".format(file),
pytestconfig.getoption("generate", False),
)

def test_file_account(self, importer, file, pytestconfig):
"""Compute the selected filing account and compare to an expected output."""
account = importer.account(file) or ""
compare_contents_or_generate(
account,
"{}.file_account".format(file),
pytestconfig.getoption("generate", False),
)