Changeset - 13df0390a19b
[Not reviewed]
0 1 0
Brett Smith - 4 years ago 2020-05-23 12:49:20
brettcsmith@brettcsmith.org
tests: Generate configuration in accrual-report tests.

Usually reduces the amount of testing boilerplate.
1 file changed with 10 insertions and 16 deletions:
0 comments (0 inline, 0 general)
tests/test_reports_accrual.py
Show inline comments
...
 
@@ -157,273 +157,267 @@ def test_filter_search(accrual_postings, search_terms, expect_count, check_func)
 
    actual = list(accrual.filter_search(accrual_postings, search_terms))
 
    if expect_count < ACCRUALS_COUNT:
 
        assert ACCRUALS_COUNT > len(actual) >= expect_count
 
    else:
 
        assert len(actual) == ACCRUALS_COUNT
 
    for post in actual:
 
        assert check_func(post)
 

	
 
@pytest.mark.parametrize('arg,expected', [
 
    ('balance', accrual.balance_report),
 
    ('outgoing', accrual.outgoing_report),
 
    ('bal', accrual.balance_report),
 
    ('out', accrual.outgoing_report),
 
    ('outgoings', accrual.outgoing_report),
 
])
 
def test_report_type_by_name(arg, expected):
 
    assert accrual.ReportType.by_name(arg.lower()) is expected
 
    assert accrual.ReportType.by_name(arg.title()) is expected
 
    assert accrual.ReportType.by_name(arg.upper()) is expected
 

	
 
@pytest.mark.parametrize('arg', [
 
    'unknown',
 
    'blance',
 
    'outgong',
 
])
 
def test_report_type_by_unknown_name(arg):
 
    # Raising ValueError helps argparse generate good messages.
 
    with pytest.raises(ValueError):
 
        accrual.ReportType.by_name(arg)
 

	
 
@pytest.mark.parametrize('invoice,expected', [
 
    # No outstanding balance
 
    ('rt:505/5050', accrual.balance_report),
 
    ('rt:510/5100', accrual.balance_report),
 
    # Outstanding receivable
 
    ('rt://ticket/515/attachments/5150', accrual.balance_report),
 
    # Outstanding payable
 
    ('rt:510/6100', accrual.outgoing_report),
 
])
 
def test_default_report_type(accrual_postings, invoice, expected):
 
    related = core.RelatedPostings()
 
    for post in accrual_postings:
 
        if (post.account.is_under('Assets:Receivable', 'Liabilities:Payable')
 
            and post.meta.get('invoice') == invoice):
 
            related.add(post)
 
    groups = {invoice: related}
 
    report_type, report_groups = accrual.ReportType.default_for(groups)
 
    assert report_type is expected
 
    assert report_groups == groups
 

	
 
@pytest.mark.parametrize('entity,exp_type,exp_invoices', [
 
    ('^Lawyer$', accrual.outgoing_report, {'rt:510/6100'}),
 
    ('^Donor[AB]$', accrual.balance_report, {'rt://ticket/515/attachments/5150'}),
 
    ('^(Lawyer|DonorB)$', accrual.balance_report,
 
     {'rt:510/6100', 'rt://ticket/515/attachments/5150'}),
 
])
 
def test_default_report_type_multi_invoices(accrual_postings, entity, exp_type, exp_invoices):
 
    groups = core.RelatedPostings.group_by_meta((
 
        post for post in accrual_postings
 
        if post.account.is_under('Assets:Receivable', 'Liabilities:Payable')
 
        and re.match(entity, post.meta.get('entity', ''))
 
    ), 'invoice')
 
    report_type, report_groups = accrual.ReportType.default_for(groups)
 
    assert report_type is exp_type
 
    assert set(report_groups.keys()) == exp_invoices
 
    assert all(len(related) > 0 for related in report_groups.values())
 

	
 
@pytest.mark.parametrize('meta_key,account', testutil.combine_values(
 
    CONSISTENT_METADATA,
 
    ACCOUNTS,
 
))
 
def test_consistency_check_when_consistent(meta_key, account):
 
    invoice = f'test-{meta_key}-invoice'
 
    meta = {
 
        'invoice': invoice,
 
        meta_key: f'test-{meta_key}-value',
 
    }
 
    txn = testutil.Transaction(postings=[
 
        (account, 100, meta),
 
        (account, -100, meta),
 
    ])
 
    related = core.RelatedPostings(data.Posting.from_txn(txn))
 
    assert not list(accrual.consistency_check({invoice: related}))
 

	
 
@pytest.mark.parametrize('meta_key,account', testutil.combine_values(
 
    ['approval', 'fx-rate', 'statement'],
 
    ACCOUNTS,
 
))
 
def test_consistency_check_ignored_metadata(meta_key, account):
 
    invoice = f'test-{meta_key}-invoice'
 
    txn = testutil.Transaction(postings=[
 
        (account, 100, {'invoice': invoice, meta_key: 'credit'}),
 
        (account, -100, {'invoice': invoice, meta_key: 'debit'}),
 
    ])
 
    related = core.RelatedPostings(data.Posting.from_txn(txn))
 
    assert not list(accrual.consistency_check({invoice: related}))
 

	
 
@pytest.mark.parametrize('meta_key,account', testutil.combine_values(
 
    CONSISTENT_METADATA,
 
    ACCOUNTS,
 
))
 
def test_consistency_check_when_inconsistent(meta_key, account):
 
    invoice = f'test-{meta_key}-invoice'
 
    txn = testutil.Transaction(postings=[
 
        (account, 100, {'invoice': invoice, meta_key: 'credit', 'lineno': 1}),
 
        (account, -100, {'invoice': invoice, meta_key: 'debit', 'lineno': 2}),
 
    ])
 
    related = core.RelatedPostings(data.Posting.from_txn(txn))
 
    errors = list(accrual.consistency_check({invoice: related}))
 
    for exp_lineno, (actual, exp_msg) in enumerate(itertools.zip_longest(errors, [
 
            f'inconsistent {meta_key} for invoice {invoice}: credit',
 
            f'inconsistent {meta_key} for invoice {invoice}: debit',
 
    ]), 1):
 
        assert actual.message == exp_msg
 
        assert actual.entry is txn
 
        assert actual.source.get('lineno') == exp_lineno
 

	
 
def check_output(output, expect_patterns):
 
    output.seek(0)
 
    testutil.check_lines_match(iter(output), expect_patterns)
 

	
 
@pytest.mark.parametrize('invoice,expected', [
 
    ('rt:505/5050', "Zero balance outstanding since 2020-05-05"),
 
    ('rt:510/5100', "Zero balance outstanding since 2020-05-10"),
 
    ('rt:510/6100', "-280.00 USD outstanding since 2020-06-10"),
 
    ('rt://ticket/515/attachments/5150', "1500.00 USD outstanding since 2020-05-15",),
 
])
 
def test_balance_report(accrual_postings, invoice, expected):
 
    related = core.RelatedPostings(
 
        post for post in accrual_postings
 
        if post.meta.get('invoice') == invoice
 
        and post.account.is_under('Assets:Receivable', 'Liabilities:Payable')
 
    )
 
    output = io.StringIO()
 
    accrual.balance_report({invoice: related}, output)
 
    check_output(output, [invoice, expected])
 

	
 
def test_outgoing_report(accrual_postings):
 
    invoice = 'rt:510/6100'
 
    related = core.RelatedPostings(
 
        post for post in accrual_postings
 
        if post.meta.get('invoice') == invoice
 
        and post.account.is_under('Assets:Receivable', 'Liabilities:Payable')
 
    )
 
    output = io.StringIO()
 
    errors = io.StringIO()
 
    rt_client = RTClient()
 
    rt_cache = rtutil.RT(rt_client)
 
    accrual.outgoing_report({invoice: related}, output, errors, rt_client, rt_cache)
 
    assert not errors.getvalue()
 
    rt_url = rt_client.DEFAULT_URL[:-9]
 
    rt_id_url = rf'\b{re.escape(f"{rt_url}Ticket/Display.html?id=510")}\b'
 
    contract_url = rf'\b{re.escape(f"{rt_url}Ticket/Attachment/4000/4000/contract.pdf")}\b'
 
    print(output.getvalue())
 
    check_output(output, [
 
        r'^PAYMENT FOR APPROVAL:$',
 
        r'^REQUESTOR: Mx\. 510 <mx510@example\.org>$',
 
        r'^TOTAL TO PAY: 280\.00 USD$',
 
        fr'^AGREEMENT: {contract_url}',
 
        r'^PAYMENT TO: Hon\. Mx\. 510$',
 
        r'^PAYMENT METHOD: payment method 510$',
 
        r'^BEANCOUNT ENTRIES:$',
 
        # For each transaction, check for the date line, a metadata, and the
 
        # Expenses posting.
 
        r'^\s*2020-06-10\s',
 
        fr'^\s+rt-id: "{rt_id_url}"$',
 
        r'^\s+Expenses:Services:Legal\s+220\.00 USD$',
 
        r'^\s*2020-06-12\s',
 
        fr'^\s+contract: "{contract_url}"$',
 
        r'^\s+Expenses:FilingFees\s+60\.00 USD$',
 
    ])
 

	
 
def test_outgoing_report_custom_field_fallbacks(accrual_postings):
 
    invoice = 'rt:510/6100'
 
    related = core.RelatedPostings(
 
        post for post in accrual_postings
 
        if post.meta.get('invoice') == invoice
 
        and post.account.is_under('Assets:Receivable', 'Liabilities:Payable')
 
    )
 
    output = io.StringIO()
 
    errors = io.StringIO()
 
    rt_client = RTClient(want_cfs=False)
 
    rt_cache = rtutil.RT(rt_client)
 
    accrual.outgoing_report({invoice: related}, output, errors, rt_client, rt_cache)
 
    assert not errors.getvalue()
 
    check_output(output, [
 
        r'^PAYMENT FOR APPROVAL:$',
 
        r'^REQUESTOR: <mx510@example\.org>$',
 
        r'^PAYMENT TO:\s*$',
 
        r'^PAYMENT METHOD:\s*$',
 
    ])
 

	
 
def run_main(arglist, config):
 
def run_main(arglist, config=None):
 
    if config is None:
 
        config = testutil.TestConfig(
 
            books_path=testutil.test_path('books/accruals.beancount'),
 
            rt_client=RTClient(),
 
        )
 
    output = io.StringIO()
 
    errors = io.StringIO()
 
    retcode = accrual.main(arglist, output, errors, config)
 
    return retcode, output, errors
 

	
 
def check_main_fails(arglist, config, error_flags, error_patterns):
 
    retcode, output, errors = run_main(arglist, config)
 
    assert retcode > 16
 
    assert (retcode - 16) & error_flags
 
    check_output(errors, error_patterns)
 
    assert not output.getvalue()
 

	
 
@pytest.mark.parametrize('arglist', [
 
    ['--report-type=outgoing'],
 
    ['510'],
 
    ['510/6100'],
 
    ['entity=Lawyer'],
 
])
 
def test_main_outgoing_report(arglist):
 
    rt_client = RTClient()
 
    config = testutil.TestConfig(
 
        books_path=testutil.test_path('books/accruals.beancount'),
 
        rt_client=rt_client,
 
    )
 
    retcode, output, errors = run_main(arglist, config)
 
    retcode, output, errors = run_main(arglist)
 
    assert not errors.getvalue()
 
    assert retcode == 0
 
    rt_url = rt_client.DEFAULT_URL[:-9]
 
    rt_url = RTClient.DEFAULT_URL[:-9]
 
    rt_id_url = re.escape(f'<{rt_url}Ticket/Display.html?id=510>')
 
    contract_url = re.escape(f'<{rt_url}Ticket/Attachment/4000/4000/contract.pdf>')
 
    check_output(output, [
 
        r'^REQUESTOR: Mx\. 510 <mx510@example\.org>$',
 
        r'^TOTAL TO PAY: 280\.00 USD$',
 
        r'^\s*2020-06-12\s',
 
        r'^\s+Expenses:FilingFees\s+60\.00 USD$',
 
    ])
 

	
 
@pytest.mark.parametrize('arglist', [
 
    ['-t', 'balance'],
 
    ['515'],
 
    ['515/5150'],
 
    ['entity=DonorB'],
 
])
 
def test_main_balance_report(arglist):
 
    config = testutil.TestConfig(
 
        books_path=testutil.test_path('books/accruals.beancount'),
 
    )
 
    retcode, output, errors = run_main(arglist, config)
 
    retcode, output, errors = run_main(arglist)
 
    assert not errors.getvalue()
 
    assert retcode == 0
 
    check_output(output, [
 
        r'\brt://ticket/515/attachments/5150:$',
 
        r'^\s+1500\.00 USD outstanding since 2020-05-15$',
 
    ])
 

	
 
def test_main_no_books():
 
    check_main_fails([], testutil.TestConfig(), 1 | 8, [
 
        r':1: +no books to load in configuration\b',
 
    ])
 

	
 
@pytest.mark.parametrize('arglist', [
 
    ['499'],
 
    ['505/99999'],
 
    ['entity=NonExistent'],
 
])
 
def test_main_no_matches(arglist):
 
    config = testutil.TestConfig(
 
        books_path=testutil.test_path('books/accruals.beancount'),
 
    )
 
    check_main_fails(arglist, config, 8, [
 
    check_main_fails(arglist, None, 8, [
 
        r'^warning: no matching entries found to report$',
 
    ])
 

	
 
def test_main_no_rt():
 
    config = testutil.TestConfig(
 
        books_path=testutil.test_path('books/accruals.beancount'),
 
    )
 
    check_main_fails(['-t', 'out'], config, 4, [
 
        r'^error: unable to generate outgoing report: RT client is required\b',
 
    ])
0 comments (0 inline, 0 general)