From 7394ca47792152495973b2aa681fdaf08b1701ca Mon Sep 17 00:00:00 2001 From: Eric Le Lay Date: Thu, 23 Jul 2020 22:31:55 +0200 Subject: [PATCH] add feedcore unittests --- .travis.yml | 2 +- README.md | 9 +- makefile | 3 +- pytest.ini | 2 - src/gpodder/unittests.py | 106 ---------------------- {src/gpodder/test => tests}/__init__.py | 0 {src/gpodder/test => tests}/model.py | 0 tests/test_feedcore.py | 115 ++++++++++++++++++++++++ 8 files changed, 123 insertions(+), 114 deletions(-) delete mode 100644 pytest.ini delete mode 100644 src/gpodder/unittests.py rename {src/gpodder/test => tests}/__init__.py (100%) rename {src/gpodder/test => tests}/model.py (100%) create mode 100644 tests/test_feedcore.py diff --git a/.travis.yml b/.travis.yml index 56d96bc2..6cd635b1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,7 @@ python: install: - sudo apt-get update -q - sudo apt-get install intltool desktop-file-utils - - pip3 install coverage==4.5.4 minimock pycodestyle isort requests + - pip3 install pytest-cov minimock pycodestyle isort requests pytest pytest-httpserver - python3 tools/localdepends.py script: - make lint diff --git a/README.md b/README.md index 71856f30..1c1d05a5 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,9 @@ PyPI. With this, you get a self-contained gPodder CLI codebase. ### Test Dependencies - python-minimock -- python-coverage +- pytest +- pytest-httpserver +- pytest-cov - desktop-file-utils ## Testing @@ -86,9 +88,8 @@ Tests in gPodder are written in two different ways: - [unittests](http://docs.python.org/3/library/unittest.html) If you want to add doctests, simply write the doctest and make sure that -the module appears in "doctest_modules" in src/gpodder/unittests.py. For -example, the doctests in src/gpodder/util.py are added as 'util' (the -"gpodder" prefix must not be specified there). +the module appears after `--doctest-modules` in `pytest.ini`. If you +add tests to any module in `src/gpodder` you have nothing to do. If you want to add unit tests for a specific module (ex: gpodder.model), you should add the tests as gpodder.test.model, or in other words: diff --git a/makefile b/makefile index a4c154fb..ea2320c3 100644 --- a/makefile +++ b/makefile @@ -61,7 +61,8 @@ help: ########################################################################## unittest: - LC_ALL=C PYTHONPATH=src/ $(PYTHON) -m gpodder.unittests + LC_ALL=C PYTHONPATH=src/ pytest --ignore=tests --ignore=src/gpodder/utilwin32ctypes.py --doctest-modules src/gpodder/util.py src/gpodder/jsonconfig.py + LC_ALL=C PYTHONPATH=src/ pytest tests --ignore=src/gpodder/utilwin32ctypes.py --ignore=src/mygpoclient --cov=gpodder ISORTOPTS := -rc -c share src/gpodder tools bin/* *.py lint: diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index cece9726..00000000 --- a/pytest.ini +++ /dev/null @@ -1,2 +0,0 @@ -[pytest] -addopts = --doctest-modules src/gpodder \ No newline at end of file diff --git a/src/gpodder/unittests.py b/src/gpodder/unittests.py deleted file mode 100644 index c681e79a..00000000 --- a/src/gpodder/unittests.py +++ /dev/null @@ -1,106 +0,0 @@ -# -*- coding: utf-8 -*- -# -# gPodder - A media aggregator and podcast client -# Copyright (c) 2005-2018 The gPodder Team -# -# gPodder 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. -# -# gPodder 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 . -# - - -# Run Doctests and Unittests for gPodder modules -# 2009-02-25 Thomas Perl - - -import doctest -import sys -import unittest - -try: - # Unused here locally, but we import it to be able to give an early - # warning about this missing dependency in order to avoid bogus errors. - import minimock -except ImportError as e: - print(""" - Error: Unit tests require the "minimock" module (python-minimock). - Please install it before running the unit tests. - """, file=sys.stderr) - sys.exit(2) - -# Main package and test package (for modules in main package) -package = 'gpodder' -test_package = '.'.join((package, 'test')) - -suite = unittest.TestSuite() -coverage_modules = [] - - -# Modules (in gpodder) for which doctests exist -# ex: Doctests embedded in "gpodder.util", coverage reported for "gpodder.util" -doctest_modules = ['util', 'jsonconfig'] - -for module in doctest_modules: - doctest_mod = __import__('.'.join((package, module)), fromlist=[module]) - - suite.addTest(doctest.DocTestSuite(doctest_mod)) - coverage_modules.append(doctest_mod) - - -# Modules (in gpodder) for which unit tests (in gpodder.test) exist -# ex: Tests are in "gpodder.test.model", coverage reported for "gpodder.model" -test_modules = ['model'] - -for module in test_modules: - test_mod = __import__('.'.join((test_package, module)), fromlist=[module]) - coverage_mod = __import__('.'.join((package, module)), fromlist=[module]) - - suite.addTest(unittest.defaultTestLoader.loadTestsFromModule(test_mod)) - coverage_modules.append(coverage_mod) - -try: - # If you want a HTML-based test report, install HTMLTestRunner from: - # http://tungwaiyip.info/software/HTMLTestRunner.html - import HTMLTestRunner - REPORT_FILENAME = 'test_report.html' - runner = HTMLTestRunner.HTMLTestRunner(stream=open(REPORT_FILENAME, 'w')) - print(""" - HTML Test Report will be written to %s - """ % REPORT_FILENAME) -except ImportError: - runner = unittest.TextTestRunner(verbosity=2) - -try: - import coverage -except ImportError: - coverage = None - -if __name__ == '__main__': - if coverage is not None: - cov = coverage.Coverage() - cov.erase() - cov.start() - - result = runner.run(suite) - - if not result.wasSuccessful(): - sys.exit(1) - - if coverage is not None: - cov.stop() - cov.report(coverage_modules) - cov.erase() - else: - print(""" - No coverage reporting done (Python module "coverage" is missing) - Please install the python-coverage package to get coverage reporting. - """, file=sys.stderr) diff --git a/src/gpodder/test/__init__.py b/tests/__init__.py similarity index 100% rename from src/gpodder/test/__init__.py rename to tests/__init__.py diff --git a/src/gpodder/test/model.py b/tests/model.py similarity index 100% rename from src/gpodder/test/model.py rename to tests/model.py diff --git a/tests/test_feedcore.py b/tests/test_feedcore.py new file mode 100644 index 00000000..957a32ff --- /dev/null +++ b/tests/test_feedcore.py @@ -0,0 +1,115 @@ +# -*- coding: utf-8 -*- +# +# gPodder - A media aggregator and podcast client +# Copyright (c) 2005-2023 The gPodder Team +# +# gPodder 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. +# +# gPodder 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 . +# +import io + +import pytest +import requests.exceptions + +from gpodder.feedcore import Fetcher, Result, NEW_LOCATION, NOT_MODIFIED, UPDATED_FEED + + +class MyFetcher(Fetcher): + def parse_feed(self, url, data_stream, headers, status, **kwargs): + return Result(status, { + 'parse_feed': { + 'url': url, + 'data_stream': data_stream, + 'headers': headers, + 'extra_args': dict(**kwargs), + }, + }) + + +SIMPLE_RSS = """ + + + Feed Name + + Some Episode Title + urn:test/ep1 + Sun, 25 Nov 2018 17:28:03 +0000 + + + + +""" + +def test_easy(httpserver): + res_data = SIMPLE_RSS + httpserver.expect_request('/feed').respond_with_data(SIMPLE_RSS, content_type='text/xml') + res = MyFetcher().fetch(httpserver.url_for('/feed'), custom_key='value') + assert res.status == UPDATED_FEED + args = res.feed['parse_feed'] + assert args['headers']['content-type'] == 'text/xml' + assert isinstance(args['data_stream'], io.BytesIO) + assert args['data_stream'].getvalue().decode('utf-8') == SIMPLE_RSS + assert args['url'] == httpserver.url_for('/feed') + assert args['extra_args']['custom_key'] == 'value' + +def test_redirect(httpserver): + res_data = SIMPLE_RSS + httpserver.expect_request('/endfeed').respond_with_data(SIMPLE_RSS, content_type='text/xml') + redir_headers = { + 'Location': '/endfeed', + } + # temporary redirect + httpserver.expect_request('/feed').respond_with_data(status=302, headers=redir_headers) + httpserver.expect_request('/permanentfeed').respond_with_data(status=301, headers=redir_headers) + + res = MyFetcher().fetch(httpserver.url_for('/feed')) + assert res.status == UPDATED_FEED + args = res.feed['parse_feed'] + assert args['headers']['content-type'] == 'text/xml' + assert isinstance(args['data_stream'], io.BytesIO) + assert args['data_stream'].getvalue().decode('utf-8') == SIMPLE_RSS + assert args['url'] == httpserver.url_for('/feed') + + res = MyFetcher().fetch(httpserver.url_for('/permanentfeed')) + assert res.status == NEW_LOCATION + assert res.feed == httpserver.url_for('/endfeed') + + +def test_redirect_loop(httpserver): + """ verify that feedcore fetching will not loop indefinitely on redirects """ + redir_headers = { + 'Location': '/feed', # it loops + } + httpserver.expect_request('/feed').respond_with_data(status=302, headers=redir_headers) + + with pytest.raises(requests.exceptions.TooManyRedirects): + res = MyFetcher().fetch(httpserver.url_for('/feed')) + assert res.status == UPDATED_FEED + args = res.feed['parse_feed'] + assert args['headers']['content-type'] == 'text/xml' + assert isinstance(args['data_stream'], io.BytesIO) + assert args['data_stream'].getvalue().decode('utf-8') == SIMPLE_RSS + assert args['url'] == httpserver.url_for('/feed') + +def test_temporary_error_retry(httpserver): + httpserver.expect_ordered_request('/feed').respond_with_data(status=503) + res_data = SIMPLE_RSS + httpserver.expect_ordered_request('/feed').respond_with_data(SIMPLE_RSS, content_type='text/xml') + res = MyFetcher().fetch(httpserver.url_for('/feed')) + assert res.status == UPDATED_FEED + args = res.feed['parse_feed'] + assert args['headers']['content-type'] == 'text/xml' + assert args['url'] == httpserver.url_for('/feed') \ No newline at end of file