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