fimfarchive/tests/test_utils.py
2020-03-28 20:42:08 +01:00

368 lines
9.3 KiB
Python

"""
Utility tests.
"""
#
# Fimfarchive, preserves stories from Fimfiction.
# Copyright (C) 2019 Joakim Soderlund
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import json
import os
from pathlib import Path
from unittest.mock import call, patch
import pytest
from fimfarchive import utils
from fimfarchive.flavors import DataFormat, MetaFormat, MetaPurity
from fimfarchive.utils import (
find_flavor, get_path, is_blacklisted,
Empty, JayWalker, PersistedDict,
)
class TestEmpty:
"""
Empty tests.
"""
def test_empty_class_is_not_none(self):
"""
Tests `Empty` class is different from `None`.
"""
assert Empty is not None
assert Empty != None # noqa
def test_empty_class_evaluates_to_false(self):
"""
Tests `Empty` class evaluates to `False`.
"""
assert not Empty
def test_empty_class_is_empty(self):
"""
Tests `Empty` class can be identified.
"""
assert Empty is Empty
assert Empty == Empty
def test_empty_instance_evaluates_to_false(self):
"""
Tests `Empty` instance evaulates to `False`
"""
assert not Empty()
def test_empty_instance_is_unique(self):
"""
Tests `Empty` instances are unique.
"""
empty = Empty()
assert isinstance(empty, Empty)
assert empty is not Empty
assert empty != Empty
assert empty is not Empty()
assert empty != Empty()
assert empty is empty
assert empty == empty
class TestPersistedDict:
"""
PersistedDict tests.
"""
@pytest.fixture
def sample(self):
"""
Returns a sample dictionary.
"""
return {'key': 'value'}
@pytest.fixture
def tmppath(self, tmpdir):
"""
Returns a temporary file path to nothing.
"""
return str(tmpdir.join('sample.json'))
@pytest.fixture
def tmpfile(self, tmppath, sample):
"""
Returns a temporary file path to sample data.
"""
with open(tmppath, 'wt') as fobj:
json.dump(sample, fobj)
return tmppath
def test_saves_data(self, tmppath, sample):
"""
Tests data is saved to file.
"""
data = PersistedDict(tmppath)
data.update(sample)
assert not os.path.exists(tmppath)
data.save()
with open(tmppath, 'rt') as fobj:
saved = json.load(fobj)
assert dict(data) == saved
def test_loads_values(self, tmpfile, sample):
"""
Tests data is loaded from file.
"""
data = PersistedDict(tmpfile)
assert dict(data) == sample
def test_load_replaces_data(self, tmpfile, sample):
"""
Tests data is replaced on load.
"""
extra = {'key': object()}
data = PersistedDict(tmpfile)
data.update(extra)
data.load()
assert dict(data) == sample
def test_load_empty_replaces_data(self, tmppath, sample):
"""
Tests data is replaced on load if file does not exist.
"""
data = PersistedDict(tmppath)
data.update(sample)
data.load()
assert dict(data) == dict()
def test_load_restores_defaults(self, tmpfile, sample):
"""
Tests defaults are restored on load.
"""
extra = {object(): object()}
data = PersistedDict(tmpfile, default=extra)
data.clear()
assert dict(data) == dict()
data.load()
assert dict(data) == {**sample, **extra}
def test_default_in_empty(self, tmppath, sample):
"""
Tests defaults are inserted when data is empty.
"""
data = PersistedDict(tmppath, default=sample)
assert dict(data) == sample
def test_default_in_mixed(self, tmpfile, sample):
"""
Tests defaults are inserted alongside loaded data.
"""
extra = {object(): object()}
data = PersistedDict(tmpfile, default=extra)
assert dict(data) == {**sample, **extra}
def test_default_does_not_override(self, tmpfile, sample):
"""
Tests defaults do not override loaded data.
"""
extra = {k: object() for k in sample.keys()}
data = PersistedDict(tmpfile, default=extra)
assert dict(data) == sample
class TestJayWalker:
"""
JayWalker tests.
"""
@pytest.fixture
def walker(self):
"""
Returns a walker instance.
"""
walker = JayWalker()
with patch.object(walker, 'handle', wraps=walker.handle):
yield walker
@pytest.mark.parametrize('obj', [None, 'alpaca', 42])
def test_ignored_walk(self, walker, obj):
"""
Tests walker ignores plain values.
"""
walker.walk(obj)
walker.handle.assert_not_called()
def test_list_walk(self, walker):
"""
Tests walker can walk lists.
"""
data = ['a', 'b', 'c']
walker.walk(data)
assert walker.handle.mock_calls == [
call(data, 0, 'a'),
call(data, 1, 'b'),
call(data, 2, 'c'),
]
def test_dict_walk(self, walker):
"""
Tests walker can walk dicts.
"""
data = {0: 'a', 1: 'b', 2: 'c'}
walker.walk(data)
assert walker.handle.mock_calls == [
call(data, 0, 'a'),
call(data, 1, 'b'),
call(data, 2, 'c'),
]
def test_nested_walk(self, walker):
"""
Tests walker can walk nested objects.
"""
data = {
'a': ['b', 'c'],
'd': {'e': 'f'},
}
walker.walk([data])
assert walker.handle.mock_calls == [
call([data], 0, data),
call(data, 'a', data['a']),
call(data['a'], 0, 'b'),
call(data['a'], 1, 'c'),
call(data, 'd', data['d']),
call(data['d'], 'e', 'f'),
]
class TestFindFlavor:
"""
find_flavor tests.
"""
@pytest.fixture
def story(self, story):
"""
Returns a meta-flavored story.
"""
return story.merge(flavors=[
MetaFormat.BETA,
MetaPurity.CLEAN,
])
def test_present_flavor(self, story):
"""
Tests flavor is returned when present.
"""
found = find_flavor(story, MetaFormat)
assert found is MetaFormat.BETA
def test_missing_flavor(self, story):
"""
Tests None is returned when flavor is missing.
"""
found = find_flavor(story, DataFormat)
assert found is None
class TestGetPath:
"""
get_path tests.
"""
@pytest.mark.parametrize('source,target', (
(None, None),
('', Path().resolve()),
('alpaca', Path('alpaca').resolve()),
(Path('alpaca'), Path('alpaca').resolve()),
))
def test_return_values(self, source, target):
"""
Tests function returns the correct value.
"""
assert target == get_path(source)
class TestIsBlacklisted:
"""
is_blacklisted tests.
"""
BLACKLISTED_AUTHOR = 1
BLACKLISTED_STORY = 2
WHITELISTED_STORY = 3
UNLISTED_AUTHOR = 4
UNLISTED_STORY = 5
COMBINATIONS = [
(BLACKLISTED_STORY, BLACKLISTED_AUTHOR, True),
(BLACKLISTED_STORY, UNLISTED_AUTHOR, True),
(UNLISTED_STORY, BLACKLISTED_AUTHOR, True),
(UNLISTED_STORY, UNLISTED_AUTHOR, False),
(WHITELISTED_STORY, BLACKLISTED_AUTHOR, False),
(WHITELISTED_STORY, UNLISTED_AUTHOR, False),
]
@pytest.fixture
def utils(self):
"""
Patches the blacklists and whitelists.
"""
ab = patch.object(utils, 'AUTHOR_BLACKLIST', {self.BLACKLISTED_AUTHOR})
sb = patch.object(utils, 'STORY_BLACKLIST', {self.BLACKLISTED_STORY})
sw = patch.object(utils, 'STORY_WHITELIST', {self.WHITELISTED_STORY})
with ab, sb, sw:
yield utils
@pytest.mark.parametrize('key,author,result', COMBINATIONS)
def test_blacklisted(self, utils, story, key, author, result):
"""
Tests the various blacklist combinations.
"""
meta = {'id': key, 'author': {'id': author}}
story = story.merge(key=key, meta=meta)
assert result is is_blacklisted(story)
@pytest.mark.parametrize('key,author,result', COMBINATIONS)
def test_blacklisted_string(self, utils, story, key, author, result):
"""
Tests the various blacklist combinations when IDs are strings.
"""
meta = {'id': str(key), 'author': {'id': str(author)}}
story = story.merge(key=key, meta=meta)
assert result is is_blacklisted(story)