aboutsummaryrefslogtreecommitdiff
# SPDX-License-Identifier: CC0-1.0

# Copyright (C) 2022 Wojtek Kosior <koszko@koszko.org>
#
# Available under the terms of Creative Commons Zero v1.0 Universal.

import pytest
import re
import dataclasses as dc

from immutables import Map

from hydrilla import pattern_tree

from .url_patterns_common import *

@pytest.mark.parametrize('_in, out', [
    (Map(),                                                  True),
    ({'children': Map(non_empty='non_emtpy')},               False),
    ({'literal_match': 'non-None'},                          False),
    ({'children': Map(non_empty='non_emtpy')},               False),
    ({'literal_match': 'non-None', 'children': 'non-empty'}, False)
])
def test_pattern_tree_node_is_empty(_in, out):
    """...."""
    assert pattern_tree.PatternTreeNode(**_in).is_empty() == out

def test_pattern_tree_node_update_literal_match():
    """...."""
    node1 = pattern_tree.PatternTreeNode()
    node2 = node1.update_literal_match('dummy match item')

    assert node1.literal_match is None
    assert node2.literal_match == 'dummy match item'

def test_pattern_tree_node_get_child():
    """...."""
    node = pattern_tree.PatternTreeNode(children=Map(dummy_key='dummy_val'))

    assert node.get_child('dummy_key') == 'dummy_val'
    assert node.get_child('other_key') is None

def test_pattern_tree_node_remove_child():
    """...."""
    node1 = pattern_tree.PatternTreeNode(children=Map(dummy_key='dummy_val'))
    node2 = node1.remove_child('dummy_key')

    assert node1.children == Map(dummy_key='dummy_val')
    assert node2.children == Map()

def test_pattern_tree_node_set_child():
    """...."""
    node1 = pattern_tree.PatternTreeNode(children=Map(dummy_key='dummy_val'))
    node2 = node1.set_child('other_key', 'other_val')

    assert node1.children == Map(dummy_key='dummy_val')
    assert node2.children == Map(dummy_key='dummy_val', other_key='other_val')

@pytest.mark.parametrize('root_empty', [True, False])
def test_pattern_tree_branch_is_empty(root_empty):
    """...."""
    class DummyEmptyRoot:
        """...."""
        is_empty = lambda: root_empty

    branch = pattern_tree.PatternTreeBranch(root_node=DummyEmptyRoot)
    assert branch.is_empty() == root_empty

# def test_pattern_tree_branch_copy():
#     """...."""
#     class DummyRoot:
#         """...."""
#         pass

#     branch1 = pattern_tree.PatternTreeBranch(root_node=DummyRoot)
#     branch2 = branch1.copy()

#     assert branch1 is not branch2
#     for val_b1, val_b2 in zip(dc.astuple(branch1), dc.astuple(branch2)):
#         assert val_b1 is val_b2

@pytest.fixture
def empty_branch():
    """...."""
    return pattern_tree.PatternTreeBranch(
        root_node = pattern_tree.PatternTreeNode()
    )

@pytest.fixture
def branch_with_a_b():
    """...."""
    return pattern_tree.PatternTreeBranch(
        root_node = pattern_tree.PatternTreeNode(
            children = Map(
                a = pattern_tree.PatternTreeNode(
                    children = Map(
                        b = pattern_tree.PatternTreeNode(
                            literal_match = frozenset({'myitem'})
                        )
                    )
                )
            )
        )
    )

def test_pattern_tree_branch_update_add_first(empty_branch, branch_with_a_b):
    """...."""
    updated_branch = empty_branch.update(
        ['a', 'b'],
        lambda s: frozenset({*(s or []), 'myitem'})
    )

    assert updated_branch                  == branch_with_a_b
    assert empty_branch.root_node.children == Map()

def test_pattern_tree_branch_update_add_second(branch_with_a_b):
    """...."""
    updated_branch = branch_with_a_b.update(
        ['a', 'b'],
        lambda s: frozenset({*(s or []), 'myotheritem'})
    )

    leaf_node = updated_branch.root_node.children['a'].children['b']
    assert leaf_node.literal_match == frozenset({'myitem', 'myotheritem'})

def test_pattern_tree_branch_update_add_different_path(branch_with_a_b):
    """...."""
    updated_branch = branch_with_a_b.update(
        ['a', 'not_b'],
        lambda s: frozenset({*(s or []), 'myotheritem'})
    )

    for segment, item in [('b', 'myitem'), ('not_b', 'myotheritem')]:
        leaf_node = updated_branch.root_node.children['a'].children[segment]
        assert leaf_node.literal_match == frozenset({item})

# def test_pattern_tree_branch_update_is_value_copied(branch_with_a_b):
#     """...."""
#     updated_branch = branch_with_a_b.update(['a', 'b'], lambda s: s)

#     leaf_node_orig = updated_branch.root_node.children['a'].children['b']
#     leaf_node_new  = branch_with_a_b.root_node.children['a'].children['b']

#     assert leaf_node_orig.literal_match == leaf_node_new.literal_match
#     assert leaf_node_orig.literal_match is not leaf_node_new.literal_match

def test_pattern_tree_branch_remove(branch_with_a_b, empty_branch):
    """...."""
    updated_branch = branch_with_a_b.update(['a', 'b'], lambda s: None)

    assert updated_branch == empty_branch

def test_pattern_tree_branch_search_empty(empty_branch):
    """...."""
    assert [*empty_branch.search(['a', 'b'])] == []

@pytest.fixture
def branch_with_wildcards():
    """...."""
    return pattern_tree.PatternTreeBranch(
        root_node = pattern_tree.PatternTreeNode(
            children = Map(
                a = pattern_tree.PatternTreeNode(
                    children = Map(
                        b = pattern_tree.PatternTreeNode(
                            children = Map({
                                'c': pattern_tree.PatternTreeNode(
                                    literal_match = 'dummy/c'
                                ),
                                '*': pattern_tree.PatternTreeNode(
                                    literal_match = 'dummy/*'
                                ),
                                '**': pattern_tree.PatternTreeNode(
                                    literal_match = 'dummy/**'
                                ),
                                '***': pattern_tree.PatternTreeNode(
                                    literal_match = 'dummy/***'
                                )
                            })
                        )
                    )
                )
            )
        )
    )

@pytest.mark.parametrize('_in, out', [
    (['a'],                       []),
    (['a', 'x', 'y', 'z'],        []),
    (['a', 'b'],                  ['dummy/***']),
    (['a', 'b', 'c'],             ['dummy/c', 'dummy/*', 'dummy/***']),
    (['a', 'b', 'u'],             ['dummy/*', 'dummy/***']),
    (['a', 'b', '*'],             ['dummy/*', 'dummy/***']),
    (['a', 'b', '**'],            ['dummy/**', 'dummy/*', 'dummy/***']),
    (['a', 'b', '***'],           ['dummy/***', 'dummy/*']),
    (['a', 'b', 'u', 'l'],        ['dummy/**', 'dummy/***']),
    (['a', 'b', 'u', 'l', 'y'],   ['dummy/**', 'dummy/***'])
])
def test_pattern_tree_branch_search_wildcards(_in, out, branch_with_wildcards):
    """...."""
    assert [*branch_with_wildcards.search(_in)] == out

def test_filter_by_trailing_slash(sample_url_parsed):
    """...."""
    sample_url_parsed2 = dc.replace(sample_url_parsed, has_trailing_slash=True)
    item1 = pattern_tree.StoredTreeItem('dummy_it1', sample_url_parsed)
    item2 = pattern_tree.StoredTreeItem('dummy_it2', sample_url_parsed2)

    assert pattern_tree.filter_by_trailing_slash((item1, item2), False) == \
        frozenset({item1})

    assert pattern_tree.filter_by_trailing_slash((item1, item2), True) == \
        frozenset({item2})

@pytest.mark.parametrize('register_mode',  [True, False])
@pytest.mark.parametrize('empty_at_start', [True, False])
@pytest.mark.parametrize('empty_at_end',   [True, False])
def test_pattern_tree_privatemethod_register(
        register_mode,
        empty_at_start,
        empty_at_end,
        monkeypatch,
        sample_url_parsed
):
    """...."""
    dummy_it       = pattern_tree.StoredTreeItem('dummy_it', sample_url_parsed)
    other_dummy_it = pattern_tree.StoredTreeItem(
        item    = 'other_dummy_it',
        pattern = sample_url_parsed
    )

    class MockedTreeBranch:
        """...."""
        def is_empty(self):
            """...."""
            return empty_at_end

        def update(self, segments, item_updater):
            """...."""
            if segments == ('com', 'example'):
                return self._update_as_domain_branch(item_updater)
            else:
                assert segments == ('aa', 'bb')
                return self._update_as_path_branch(item_updater)

        def _update_as_domain_branch(self, item_updater):
            """...."""
            for updater_input in (None, MockedTreeBranch()):
                updated = item_updater(updater_input)
                if empty_at_end:
                    assert updated is None
                else:
                    assert type(updated) is MockedTreeBranch

            return MockedTreeBranch()

        def _update_as_path_branch(self, item_updater):
            """...."""
            set_with_1_item  = frozenset()
            set_with_2_items = frozenset({dummy_it, other_dummy_it})
            for updater_input in (None, set_with_1_item, set_with_2_items):
                updated = item_updater(updater_input)
                if register_mode:
                    assert dummy_it in updated
                elif updater_input is set_with_2_items:
                    assert dummy_it not in updated
                else:
                    assert updated is None

            return MockedTreeBranch()

    monkeypatch.setattr(pattern_tree, 'PatternTreeBranch', MockedTreeBranch)

    initial_root = Map() if empty_at_start else \
        Map({('http', 80): MockedTreeBranch()})

    tree = pattern_tree.PatternTree(_by_scheme_and_port=initial_root)

    new_tree = tree._register(
        sample_url_parsed,
        'dummy_it',
        register=register_mode
    )

    assert new_tree is not tree

    if empty_at_end:
        assert new_tree._by_scheme_and_port == Map()
    else:
        assert len(new_tree._by_scheme_and_port) == 1
        assert type(new_tree._by_scheme_and_port[('http', 80)]) is \
            MockedTreeBranch

# @pytest.mark.parametrize('register_mode', [True, False])
# def test_pattern_tree_privatemethod_register(
#         register_mode,
#         monkeypatch,
#         sample_url_parsed
# ):
#     """...."""
#     registered_count = 0

#     def mocked_parse_pattern(url_pattern):
#         """...."""
#         assert url_pattern == 'dummy_pattern'

#         for _ in range(2):
#             yield sample_url_parsed

#     monkeypatch.setattr(pattern_tree, 'parse_pattern', mocked_parse_pattern)

#     def mocked_reconstruct_url(self):
#         """...."""
#         return 'dummy_reconstructed_pattern'

#     monkeypatch.setattr(pattern_tree.ParsedUrl, 'reconstruct_url',
#                         mocked_reconstruct_url)

#     def mocked_register_with_parsed_pattern(
#             self,
#             parsed_pat,
#             wrapped_item,
#             register=True
#     ):
#         """...."""
#         nonlocal registered_count

#         assert parsed_pat is sample_url_parsed
#         assert wrapped_item.pattern == 'dummy_reconstructed_pattern'
#         assert register == register_mode

#         registered_count += 1

#         return 'dummy_new_tree' if registered_count == 2 else dc.replace(self)

#     monkeypatch.setattr(
#         pattern_tree.PatternTree,
#         '_register_with_parsed_pattern',
#         mocked_register_with_parsed_pattern
#     )

#     pattern_tree = pattern_tree.PatternTree()

#     new_tree = pattern_tree._register(
#         'dummy_pattern',
#         'dummy_item',
#         register_mode
#     )

#     assert new_tree == 'dummy_new_tree'

@pytest.mark.parametrize('method_name, register_mode', [
    ('register',   True),
    ('deregister', False)
])
def test_pattern_tree_register(method_name, register_mode, monkeypatch):
    """...."""
    def mocked_privatemethod_register(self, parsed_pat, item, register=True):
        """...."""
        assert (parsed_pat, item, register) == \
            ('dummy_pattern', 'dummy_url', register_mode)

        return 'dummy_new_tree'

    monkeypatch.setattr(
        pattern_tree.PatternTree,
        '_register',
        mocked_privatemethod_register
    )

    method = getattr(pattern_tree.PatternTree(), method_name)
    assert method('dummy_pattern', 'dummy_url') == 'dummy_new_tree'

@pytest.fixture
def mock_parse_url(monkeypatch, sample_url_parsed):
    """...."""
    def mocked_parse_url(url):
        """...."""
        assert url == 'dummy_url'
        return dc.replace(
            sample_url_parsed,
            **getattr(mocked_parse_url, 'url_mod', {})
        )

    monkeypatch.setattr(pattern_tree, 'parse_url', mocked_parse_url)

    return mocked_parse_url

@pytest.mark.usefixtures('mock_parse_url')
def test_pattern_tree_search_empty(sample_url_parsed):
    """...."""
    for url in ('dummy_url', sample_url_parsed):
        assert [*pattern_tree.PatternTree().search(url)] == []

@pytest.mark.parametrize('url_mod, out', [
    ({},
     ['dummy_set_A', 'dummy_set_B', 'dummy_set_C']),

    ({'has_trailing_slash': True},
     ['dummy_set_A_with_slash', 'dummy_set_A',
      'dummy_set_B_with_slash', 'dummy_set_B',
      'dummy_set_C_with_slash', 'dummy_set_C'])
])
def test_pattern_tree_search(
        url_mod,
        out,
        monkeypatch,
        sample_url_parsed,
        mock_parse_url,
):
    """...."""
    mock_parse_url.url_mod = url_mod

    dummy_tree_contents = [
        ['dummy_set_A', 'dummy_set_B'],
        [],
        ['dummy_empty_set'] * 3,
        ['dummy_set_C']
    ]

    def mocked_filter_by_trailing_slash(items, with_slash):
        """...."""
        if items == 'dummy_empty_set':
            return frozenset()

        return items + ('_with_slash' if with_slash else '')

    monkeypatch.setattr(pattern_tree, 'filter_by_trailing_slash',
                        mocked_filter_by_trailing_slash)

    class MockedDomainBranch:
        """...."""
        def search(self, labels):
            """...."""
            assert labels == sample_url_parsed.domain_labels

            for item_sets in dummy_tree_contents:
                class MockedPathBranch:
                    """...."""
                    def search(self, segments, item_sets=item_sets):
                        """...."""
                        assert segments == sample_url_parsed.path_segments

                        for dummy_items_set in item_sets:
                            yield dummy_items_set

                yield MockedPathBranch()

    tree = pattern_tree.PatternTree(
        _by_scheme_and_port = {('http', 80): MockedDomainBranch()}
    )

    for url in ('dummy_url', mock_parse_url('dummy_url')):
        assert [*tree.search(url)] == out