Files @ d9360f1ceafe
Branch filter:

Location: NPO-Accounting/conservancy_beancount/tests/test_cliutil_searchterm.py - annotation

Brett Smith
audit_report: Use concurrent.futures for parallelization.

This is basically a pure maintainability change: concurrent.futures is the
nicest API that's available in both Python 3.6 and 3.7, and our other tools
are using it.
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
cd1b28ae3eb1
"""test_cliutil_searchterm - Unit tests for cliutil.SearchTerm"""
# Copyright © 2020  Brett Smith
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.

import re

import pytest

from . import testutil

from conservancy_beancount import cliutil
from conservancy_beancount import data

TICKET_LINKS = [
    '123',
    'rt:123',
    'rt://ticket/123',
]

ATTACHMENT_LINKS = [
    '123/456',
    'rt:123/456',
    'rt://ticket/123/attachments/456',
]

REPOSITORY_LINKS = [
    '789.pdf',
    'Documents/789.pdf',
]

RT_LINKS = TICKET_LINKS + ATTACHMENT_LINKS
ALL_LINKS = RT_LINKS + REPOSITORY_LINKS

INVALID_METADATA_KEYS = [
    # Must start with a-z
    ';key',
    ' key',
    'ákey',
    'Key',
    # Must only contain alphanumerics, -, and _
    'key;',
    'key ',
    'a.key',
]

@pytest.fixture(scope='module')
def defaults_parser():
    return cliutil.SearchTerm.arg_parser('document', 'rt-id')

@pytest.fixture(scope='module')
def one_default_parser():
    return cliutil.SearchTerm.arg_parser('default')

@pytest.fixture(scope='module')
def no_default_parser():
    return cliutil.SearchTerm.arg_parser()

def check_link_regexp(regexp, match_s, first_link_only=False):
    assert regexp
    assert re.search(regexp, match_s)
    assert re.search(regexp, match_s + ' postlink')
    assert re.search(regexp, match_s + '0') is None
    assert re.search(regexp, '1' + match_s) is None
    end_match = re.search(regexp, 'prelink ' + match_s)
    if first_link_only:
        assert end_match is None
    else:
        assert end_match

@pytest.mark.parametrize('arg', TICKET_LINKS)
def test_search_term_parse_ticket(defaults_parser, arg):
    key, regexp = defaults_parser(arg)
    assert key == 'rt-id'
    check_link_regexp(regexp, 'rt:123', first_link_only=True)
    check_link_regexp(regexp, 'rt://ticket/123', first_link_only=True)

@pytest.mark.parametrize('arg', ATTACHMENT_LINKS)
def test_search_term_parse_attachment(defaults_parser, arg):
    key, regexp = defaults_parser(arg)
    assert key == 'document'
    check_link_regexp(regexp, 'rt:123/456')
    check_link_regexp(regexp, 'rt://ticket/123/attachments/456')

@pytest.mark.parametrize('key,query', testutil.combine_values(
    ['approval', 'contract', 'invoice'],
    RT_LINKS,
))
def test_search_term_parse_metadata_rt_shortcut(defaults_parser, key, query):
    actual_key, regexp = defaults_parser(f'{key}={query}')
    assert actual_key == key
    if query.endswith('/456'):
        check_link_regexp(regexp, 'rt:123/456')
        check_link_regexp(regexp, 'rt://ticket/123/attachments/456')
    else:
        check_link_regexp(regexp, 'rt:123')
        check_link_regexp(regexp, 'rt://ticket/123')

@pytest.mark.parametrize('search_prefix', [
    '',
    'approval=',
    'contract=',
    'invoice=',
])
def test_search_term_parse_repo_link(defaults_parser, search_prefix):
    document = '1234.pdf'
    actual_key, regexp = defaults_parser(f'{search_prefix}{document}')
    if search_prefix:
        expect_key = search_prefix.rstrip('=')
    else:
        expect_key = 'document'
    assert actual_key == expect_key
    check_link_regexp(regexp, document)

@pytest.mark.parametrize('search,unmatched', [
    ('1234.pdf', '1234_pdf'),
])
def test_search_term_parse_regexp_escaping(defaults_parser, search, unmatched):
    _, regexp = defaults_parser(search)
    assert re.search(regexp, unmatched) is None

@pytest.mark.parametrize('key', INVALID_METADATA_KEYS)
def test_non_metadata_key(one_default_parser, key):
    document = f'{key}=890'
    key, pattern = one_default_parser(document)
    assert key == 'default'
    check_link_regexp(pattern, document)

@pytest.mark.parametrize('arg', ALL_LINKS)
def test_default_parser(one_default_parser, arg):
    key, _ = one_default_parser(arg)
    assert key == 'default'

@pytest.mark.parametrize('arg', ALL_LINKS + [
    f'{nonkey}={link}' for nonkey, link in testutil.combine_values(
        INVALID_METADATA_KEYS, ALL_LINKS,
    )
])
def test_no_key(no_default_parser, arg):
    with pytest.raises(ValueError):
        key, pattern = no_default_parser(arg)

@pytest.mark.parametrize('key', ['zero', 'one', 'two'])
def test_filter_postings(key):
    txn = testutil.Transaction(postings=[
        ('Income:Other', 3, {'one': '1', 'two': '2'}),
        ('Income:Other', 2, {'two': '2'}),
        ('Income:Other', 1, {'one': '1'}),
    ])
    search = cliutil.SearchTerm(key, '.')
    actual = list(search.filter_postings(data.Posting.from_txn(txn)))
    assert len(actual) == 0 if key == 'zero' else 2
    assert all(post.meta.get(key) for post in actual)