aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCody Logan <cody@lokken.dev>2023-11-07 11:41:49 -0800
committerGitHub <noreply@github.com>2023-11-07 11:41:49 -0800
commit5129ad62392948a033ee988e4093d095f5005c77 (patch)
tree5df144f6970d7d015bce94cff4516a388b5cddab
parentbc5d19c8150bf7960839243ceeb6f62a9df54e18 (diff)
parent7c2dadfa38ac08a060f2df987b7d0b7f2f0b5ad0 (diff)
downloadwikiget-5129ad62392948a033ee988e4093d095f5005c77.tar.gz
wikiget-5129ad62392948a033ee988e4093d095f5005c77.zip
Merge pull request #12 from clpo13/improve-tests
Improve tests and test coverage
-rw-r--r--.github/workflows/python.yml2
-rw-r--r--pyproject.toml2
-rw-r--r--src/wikiget/client.py1
-rw-r--r--src/wikiget/file.py20
-rw-r--r--tests/conftest.py28
-rw-r--r--tests/test_client.py160
-rw-r--r--tests/test_dl.py398
-rw-r--r--tests/test_file_class.py13
-rw-r--r--tests/test_logging.py110
-rw-r--r--tests/test_parse.py10
-rw-r--r--tests/test_validations.py26
-rw-r--r--tests/test_wikiget_cli.py87
12 files changed, 677 insertions, 180 deletions
diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml
index ba5ab90..76387d9 100644
--- a/.github/workflows/python.yml
+++ b/.github/workflows/python.yml
@@ -27,7 +27,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip setuptools wheel
- python -m pip install flake8 pytest "coverage[toml]"
+ python -m pip install flake8 pytest requests_mock "coverage[toml]"
python -m pip install .
- name: Lint with flake8
run: |
diff --git a/pyproject.toml b/pyproject.toml
index 36a90ae..9c88c2e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -69,6 +69,7 @@ exclude = [
dependencies = [
"coverage[toml]>=6.5",
"pytest",
+ "requests_mock",
]
[tool.hatch.envs.default.scripts]
test = "python -m pytest {args}"
@@ -199,5 +200,6 @@ module = [
"mwclient",
"mwclient.image",
"pytest",
+ "requests_mock",
]
ignore_missing_imports = true
diff --git a/src/wikiget/client.py b/src/wikiget/client.py
index 6551142..69051a7 100644
--- a/src/wikiget/client.py
+++ b/src/wikiget/client.py
@@ -34,6 +34,7 @@ def connect_to_site(site_name: str, args: Namespace) -> Site:
try:
site = Site(site_name, path=args.path, clients_useragent=wikiget.USER_AGENT)
if args.username and args.password:
+ logger.info("Attempting to authenticate with credentials")
site.login(args.username, args.password)
except ConnectionError as e:
# usually this means there is no such site, or there's no network connection,
diff --git a/src/wikiget/file.py b/src/wikiget/file.py
index 0f639d3..8de62ae 100644
--- a/src/wikiget/file.py
+++ b/src/wikiget/file.py
@@ -15,6 +15,8 @@
# You should have received a copy of the GNU General Public License
# along with Wikiget. If not, see <https://www.gnu.org/licenses/>.
+from typing import Any
+
from mwclient.image import Image
from wikiget import DEFAULT_SITE
@@ -43,3 +45,21 @@ class File:
self.name = name
self.dest = dest if dest else name
self.site = site if site else DEFAULT_SITE
+
+ def __eq__(self, other: Any) -> bool:
+ """
+ Compares this File object with another for equality.
+
+ :param other: another File to compare
+ :type other: File
+ :return: True if the Files are equal and False otherwise
+ :rtype: bool
+ """
+ if not isinstance(other, File):
+ return NotImplemented
+ return (
+ self.image == other.image
+ and self.name == other.name
+ and self.dest == other.dest
+ and self.site == other.site
+ )
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..94fc053
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,28 @@
+# wikiget - CLI tool for downloading files from Wikimedia sites
+# Copyright (C) 2023 Cody Logan
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+# Wikiget 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.
+#
+# Wikiget 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 Wikiget. If not, see <https://www.gnu.org/licenses/>.
+
+import pytest
+import requests_mock as rm
+
+
+@pytest.fixture
+def mock_get(requests_mock: rm.Mocker) -> None:
+ # fake the download request for File:Example.jpg
+ requests_mock.get(
+ "https://upload.wikimedia.org/wikipedia/commons/a/a9/Example.jpg",
+ text="data",
+ )
diff --git a/tests/test_client.py b/tests/test_client.py
index 9b1b8a4..4cbf702 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -16,40 +16,160 @@
# along with Wikiget. If not, see <https://www.gnu.org/licenses/>.
import logging
-from unittest.mock import MagicMock, patch
+from unittest.mock import MagicMock, patch, sentinel
import pytest
+from mwclient import APIError, InvalidResponse
+from requests import ConnectionError, HTTPError
-from wikiget import USER_AGENT
+from wikiget import DEFAULT_SITE
from wikiget.client import connect_to_site, query_api
from wikiget.wikiget import parse_args
-# TODO: don't hit the actual API when doing tests
-class TestQueryApi:
- @patch("mwclient.Site.__new__")
- def test_connect_to_site(
- self, mock_site: MagicMock, caplog: pytest.LogCaptureFixture
- ) -> None:
+class TestConnectSite:
+ # this message is logged when the level is at INFO or below
+ info_msg = f"Connecting to {DEFAULT_SITE}"
+
+ def test_connect_to_site(self, caplog: pytest.LogCaptureFixture) -> None:
"""
- The connect_to_site function should create a debug log message recording the
+ The connect_to_site function should create an info log message recording the
name of the site we're connecting to.
"""
+ caplog.set_level(logging.INFO)
+ args = parse_args(["File:Example.jpg"])
+
+ with patch("wikiget.client.Site"):
+ _ = connect_to_site(DEFAULT_SITE, args)
+
+ assert caplog.record_tuples == [
+ ("wikiget.client", logging.INFO, self.info_msg),
+ ]
+
+ def test_connect_to_site_with_creds(self, caplog: pytest.LogCaptureFixture) -> None:
+ """
+ If a username and password are provided, connect_to_site should use them to
+ log in to the site.
+ """
+ caplog.set_level(logging.INFO)
+ args = parse_args(["-u", "username", "-p", "password", "File:Example.jpg"])
+
+ with patch("wikiget.client.Site"):
+ _ = connect_to_site(DEFAULT_SITE, args)
+
+ # TODO: it should be possible to test if Site.login was called, making the log
+ # message unnecessary
+ assert caplog.record_tuples[1] == (
+ "wikiget.client",
+ logging.INFO,
+ "Attempting to authenticate with credentials",
+ )
+
+ def test_connect_to_site_connection_error(
+ self, caplog: pytest.LogCaptureFixture
+ ) -> None:
+ """
+ The connect_to_site function should log the correct messages if a
+ ConnectionError exception is raised.
+ """
caplog.set_level(logging.DEBUG)
- mock_site.return_value = MagicMock()
args = parse_args(["File:Example.jpg"])
- _ = connect_to_site("commons.wikimedia.org", args)
- assert mock_site.called
- assert "Connecting to commons.wikimedia.org" in caplog.text
- @pytest.mark.skip(reason="skip tests that query a live API")
- def test_query_api(self, caplog: pytest.LogCaptureFixture) -> None:
+ with patch("wikiget.client.Site") as mock_site:
+ mock_site.side_effect = ConnectionError("connection error message")
+ with pytest.raises(ConnectionError):
+ _ = connect_to_site(DEFAULT_SITE, args)
+
+ assert "Could not connect to specified site" in caplog.text
+ assert caplog.record_tuples == [
+ ("wikiget.client", logging.INFO, self.info_msg),
+ ("wikiget.client", logging.ERROR, "Could not connect to specified site"),
+ ("wikiget.client", logging.DEBUG, "connection error message"),
+ ]
+
+ def test_connect_to_site_http_error(self, caplog: pytest.LogCaptureFixture) -> None:
"""
- The query_api function should create a debug log message containing the user
- agent we're sending to the API.
+ The connect_to_site function should log the correct messages if an HTTPError
+ exception is raised.
"""
caplog.set_level(logging.DEBUG)
args = parse_args(["File:Example.jpg"])
- site = connect_to_site("commons.wikimedia.org", args)
- _ = query_api("Example.jpg", site)
- assert USER_AGENT in caplog.text
+
+ with patch("wikiget.client.Site") as mock_site:
+ mock_site.side_effect = HTTPError
+ with pytest.raises(HTTPError):
+ _ = connect_to_site(DEFAULT_SITE, args)
+
+ assert caplog.record_tuples == [
+ ("wikiget.client", logging.INFO, self.info_msg),
+ (
+ "wikiget.client",
+ logging.ERROR,
+ "Could not find the specified wiki's api.php. "
+ "Check the value of --path.",
+ ),
+ ("wikiget.client", logging.DEBUG, ""),
+ ]
+
+ def test_connect_to_site_other_error(
+ self, caplog: pytest.LogCaptureFixture
+ ) -> None:
+ """
+ The connect_to_site function should log an error if some other exception type
+ is raised.
+ """
+ args = parse_args(["File:Example.jpg"])
+
+ with patch("wikiget.client.Site") as mock_site:
+ mock_site.side_effect = InvalidResponse
+ with pytest.raises(InvalidResponse):
+ _ = connect_to_site("commons.wikimedia.org", args)
+
+ for record in caplog.records:
+ assert record.levelname == "ERROR"
+
+
+class TestQueryApi:
+ def test_query_api(self) -> None:
+ """
+ The query_api function should return an Image object when given a name and a
+ valid Site.
+ """
+ # These mock objects represent Site and Image objects that the real program
+ # would have created using the MediaWiki API. The Site.images attribute is
+ # normally populated during Site init, but since we're not doing that, a mock
+ # dict is created for query_api to parse.
+ mock_site = MagicMock()
+ mock_site.images = {"Example.jpg": sentinel.mock_image}
+
+ image = query_api("Example.jpg", mock_site)
+
+ assert image == sentinel.mock_image
+
+ def test_query_api_error(self, caplog: pytest.LogCaptureFixture) -> None:
+ """
+ The query_api function should log an error if an APIError exception is caught,
+ as well as debug log entries with additional information about the error.
+ """
+ caplog.set_level(logging.DEBUG)
+
+ mock_site = MagicMock()
+ mock_site.images = MagicMock()
+ mock_site.images.__getitem__.side_effect = APIError(
+ "error code", "error info", "error kwargs"
+ )
+
+ with pytest.raises(APIError):
+ _ = query_api("Example.jpg", mock_site)
+
+ assert caplog.record_tuples == [
+ (
+ "wikiget.client",
+ logging.ERROR,
+ "Access denied. Try providing credentials with "
+ "--username and --password.",
+ ),
+ ("wikiget.client", logging.DEBUG, "error code"),
+ ("wikiget.client", logging.DEBUG, "error info"),
+ ("wikiget.client", logging.DEBUG, "error kwargs"),
+ ]
diff --git a/tests/test_dl.py b/tests/test_dl.py
index a57c3c1..d39c352 100644
--- a/tests/test_dl.py
+++ b/tests/test_dl.py
@@ -15,26 +15,40 @@
# You should have received a copy of the GNU General Public License
# along with Wikiget. If not, see <https://www.gnu.org/licenses/>.
+import logging
from pathlib import Path
-from unittest.mock import MagicMock, patch
+from unittest.mock import MagicMock, Mock, patch
import pytest
+import requests
+from mwclient import Site
-from wikiget.dl import prep_download, process_download
+from wikiget.dl import batch_download, download, prep_download, process_download
+from wikiget.exceptions import ParseError
from wikiget.file import File
from wikiget.wikiget import parse_args
-# TODO: don't hit the actual API when doing tests
class TestPrepDownload:
- @pytest.mark.skip(reason="skip tests that query a live API")
- def test_prep_download(self) -> None:
- """
- The prep_download function should create a file object.
- """
+ @patch("wikiget.dl.query_api")
+ @patch("wikiget.dl.connect_to_site")
+ def test_prep_download(
+ self, mock_connect_to_site: MagicMock, mock_query_api: MagicMock
+ ) -> None:
+ """The prep_download function should create the expected file object."""
+ mock_site = Mock()
+ mock_image = Mock()
+
+ mock_connect_to_site.return_value = mock_site
+ mock_query_api.return_value = mock_image
+
+ expected_file = File(name="Example.jpg")
+ expected_file.image = mock_image
+
args = parse_args(["File:Example.jpg"])
file = prep_download(args.FILE, args)
- assert file is not None
+
+ assert file == expected_file
def test_prep_download_with_existing_file(self, tmp_path: Path) -> None:
"""
@@ -50,59 +64,381 @@ class TestPrepDownload:
class TestProcessDownload:
@patch("wikiget.dl.batch_download")
- def test_batch_download(self, mock_batch_download: MagicMock) -> None:
- """
- A successful batch download should not return any errors.
- """
- args = parse_args(["-a", "batch.txt"])
+ def test_process_batch_download(self, mock_batch_download: MagicMock) -> None:
+ """A successful batch download should not return any errors."""
mock_batch_download.return_value = 0
+
+ args = parse_args(["-a", "batch.txt"])
exit_code = process_download(args)
- assert mock_batch_download.called
+
assert exit_code == 0
@patch("wikiget.dl.batch_download")
- def test_batch_download_with_errors(
+ def test_process_batch_download_with_errors(
self, mock_batch_download: MagicMock, caplog: pytest.LogCaptureFixture
) -> None:
"""
Any errors during batch download should create a log message containing the
number of errors and result in a non-zero exit code.
"""
- args = parse_args(["-a", "batch.txt"])
mock_batch_download.return_value = 4
+
+ args = parse_args(["-a", "batch.txt"])
exit_code = process_download(args)
- assert mock_batch_download.called
+
assert exit_code == 1
assert "4 problems encountered during batch processing" in caplog.text
@patch("wikiget.dl.prep_download")
@patch("wikiget.dl.download")
- def test_single_download(
+ def test_process_single_download(
self, mock_download: MagicMock, mock_prep_download: MagicMock
) -> None:
- """
- A successful download should not return any errors.
- """
- args = parse_args(["File:Example.jpg"])
+ """A successful download should not return any errors."""
mock_download.return_value = 0
- mock_prep_download.return_value = MagicMock(File)
+ mock_prep_download.return_value = File("Example.jpg")
+
+ args = parse_args(["File:Example.jpg"])
exit_code = process_download(args)
- assert mock_prep_download.called
- assert mock_download.called
+
assert exit_code == 0
@patch("wikiget.dl.prep_download")
@patch("wikiget.dl.download")
- def test_single_download_with_errors(
+ def test_process_single_download_with_errors(
self, mock_download: MagicMock, mock_prep_download: MagicMock
) -> None:
+ """Any errors during download should result in a non-zero exit code."""
+ mock_download.return_value = 1
+ mock_prep_download.return_value = File("Example.jpg")
+
+ args = parse_args(["File:Example.jpg"])
+ exit_code = process_download(args)
+
+ assert exit_code == 1
+
+ @patch("wikiget.dl.prep_download")
+ def test_process_single_download_parse_error(
+ self, mock_prep_download: MagicMock, caplog: pytest.LogCaptureFixture
+ ) -> None:
"""
- Any errors during download should result in a non-zero exit code.
+ If process_download catches a ParseError, it should create an error log message.
"""
+ mock_prep_download.side_effect = ParseError("error message")
+
+ args = parse_args(["File:Example.jpg"])
+ _ = process_download(args)
+
+ assert mock_prep_download.called
+ assert caplog.record_tuples == [("wikiget.dl", logging.ERROR, "error message")]
+
+ @patch("wikiget.dl.prep_download")
+ def test_process_single_download_file_exists_error(
+ self, mock_prep_download: MagicMock, caplog: pytest.LogCaptureFixture
+ ) -> None:
+ """
+ If process_download catches a FileExistsError, it should create a warning log
+ message.
+ """
+ mock_prep_download.side_effect = FileExistsError("warning message")
+
+ args = parse_args(["File:Example.jpg"])
+ _ = process_download(args)
+
+ assert mock_prep_download.called
+ assert caplog.record_tuples == [
+ ("wikiget.dl", logging.WARNING, "warning message"),
+ ]
+
+ @patch("wikiget.dl.prep_download")
+ def test_process_single_download_other_error(
+ self, mock_prep_download: MagicMock
+ ) -> None:
+ """
+ If process_download catches any other errors, it should return 1.
+ """
+ mock_prep_download.side_effect = requests.ConnectionError
+
args = parse_args(["File:Example.jpg"])
- mock_download.return_value = 1
- mock_prep_download.return_value = MagicMock(File)
exit_code = process_download(args)
+
assert mock_prep_download.called
- assert mock_download.called
assert exit_code == 1
+
+
+class TestBatchDownload:
+ @patch("wikiget.dl.download")
+ @patch("wikiget.dl.prep_download")
+ @patch("wikiget.dl.read_batch_file")
+ def test_batch_download(
+ self,
+ mock_read_batch_file: MagicMock,
+ mock_prep_download: MagicMock,
+ mock_download: MagicMock,
+ caplog: pytest.LogCaptureFixture,
+ ) -> None:
+ caplog.set_level(logging.INFO)
+
+ # set dummy return values for read_batch_file() and download()
+ mock_read_batch_file.return_value = {1: "File:Example.jpg"}
+ mock_download.return_value = 0
+
+ args = parse_args(["-a", "batch.txt"])
+ errors = batch_download(args)
+
+ assert mock_read_batch_file.called
+ assert mock_prep_download.called
+ assert mock_download.called
+ assert caplog.record_tuples == [
+ ("wikiget.dl", logging.INFO, "Processing 'File:Example.jpg' at line 1")
+ ]
+ assert errors == 0
+
+ @patch("wikiget.dl.read_batch_file")
+ def test_batch_download_os_error(
+ self, mock_read_batch_file: MagicMock, caplog: pytest.LogCaptureFixture
+ ) -> None:
+ """
+ If batch_download catches an OSError, it should print an error log message
+ and exit the program.
+ """
+ mock_read_batch_file.side_effect = OSError("error message")
+
+ args = parse_args(["-a", "batch.txt"])
+ with pytest.raises(SystemExit):
+ _ = batch_download(args)
+
+ assert mock_read_batch_file.called
+ assert caplog.record_tuples == [
+ ("wikiget.dl", logging.ERROR, "File could not be read: error message"),
+ ]
+
+ @patch("wikiget.dl.prep_download")
+ @patch("wikiget.dl.read_batch_file")
+ def test_batch_download_parse_error(
+ self,
+ mock_read_batch_file: MagicMock,
+ mock_prep_download: MagicMock,
+ caplog: pytest.LogCaptureFixture,
+ ) -> None:
+ mock_read_batch_file.return_value = {1: "File:Example.jpg"}
+ mock_prep_download.side_effect = ParseError("warning message")
+
+ args = parse_args(["-a", "batch.txt"])
+ errors = batch_download(args)
+
+ assert mock_read_batch_file.called
+ assert mock_prep_download.called
+ assert caplog.record_tuples == [
+ ("wikiget.dl", logging.WARNING, "warning message (line 1)"),
+ ]
+ assert errors == 1
+
+ @patch("wikiget.dl.prep_download")
+ @patch("wikiget.dl.read_batch_file")
+ def test_batch_download_file_exists_error(
+ self,
+ mock_read_batch_file: MagicMock,
+ mock_prep_download: MagicMock,
+ caplog: pytest.LogCaptureFixture,
+ ) -> None:
+ mock_read_batch_file.return_value = {1: "File:Example.jpg"}
+ mock_prep_download.side_effect = FileExistsError("warning message")
+
+ args = parse_args(["-a", "batch.txt"])
+ errors = batch_download(args)
+
+ assert mock_read_batch_file.called
+ assert mock_prep_download.called
+ assert caplog.record_tuples == [
+ ("wikiget.dl", logging.WARNING, "warning message"),
+ ]
+ assert errors == 1
+
+ @patch("wikiget.dl.prep_download")
+ @patch("wikiget.dl.read_batch_file")
+ def test_batch_download_other_error(
+ self,
+ mock_read_batch_file: MagicMock,
+ mock_prep_download: MagicMock,
+ caplog: pytest.LogCaptureFixture,
+ ) -> None:
+ mock_read_batch_file.return_value = {1: "File:Example.jpg"}
+ mock_prep_download.side_effect = requests.ConnectionError
+
+ args = parse_args(["-a", "batch.txt"])
+ errors = batch_download(args)
+
+ assert mock_read_batch_file.called
+ assert mock_prep_download.called
+ assert caplog.record_tuples == [
+ (
+ "wikiget.dl",
+ logging.WARNING,
+ "Unable to download 'File:Example.jpg' (line 1) due to an error",
+ ),
+ ]
+ assert errors == 1
+
+
+@pytest.mark.usefixtures("mock_get")
+class TestDownload:
+ @pytest.fixture
+ def mock_file(self, tmp_path: Path) -> File:
+ file = File(name="Example.jpg", dest=str(tmp_path / "Example.jpg"))
+ file.image = Mock()
+ file.image.exists = True
+ file.image.imageinfo = {
+ "url": "https://upload.wikimedia.org/wikipedia/commons/a/a9/Example.jpg",
+ "size": 9022,
+ "sha1": "d01b79a6781c72ac9bfff93e5e2cfbeef4efc840",
+ }
+ file.image.site = MagicMock(Site)
+ file.image.site.host = "commons.wikimedia.org"
+ file.image.site.connection = requests.Session()
+ return file
+
+ def test_download(self, mock_file: File, caplog: pytest.LogCaptureFixture) -> None:
+ caplog.set_level(logging.INFO)
+
+ with patch("wikiget.dl.verify_hash") as mock_verify_hash:
+ mock_verify_hash.return_value = "d01b79a6781c72ac9bfff93e5e2cfbeef4efc840"
+ args = parse_args(["File:Example.jpg"])
+ errors = download(mock_file, args)
+
+ assert caplog.record_tuples == [
+ (
+ "wikiget.dl",
+ logging.INFO,
+ "[Example.jpg] Downloading 'Example.jpg' (9022 bytes) from "
+ "commons.wikimedia.org",
+ ),
+ (
+ "wikiget.dl",
+ logging.INFO,
+ "[Example.jpg] "
+ "https://upload.wikimedia.org/wikipedia/commons/a/a9/Example.jpg",
+ ),
+ (
+ "wikiget.dl",
+ logging.INFO,
+ "[Example.jpg] Remote file SHA1 is "
+ "d01b79a6781c72ac9bfff93e5e2cfbeef4efc840",
+ ),
+ (
+ "wikiget.dl",
+ logging.INFO,
+ "[Example.jpg] Local file SHA1 is "
+ "d01b79a6781c72ac9bfff93e5e2cfbeef4efc840",
+ ),
+ ("wikiget.dl", logging.INFO, "[Example.jpg] Hashes match!"),
+ ("wikiget.dl", logging.INFO, "[Example.jpg] 'Example.jpg' downloaded"),
+ ]
+ assert errors == 0
+
+ def test_download_with_output(
+ self, mock_file: File, caplog: pytest.LogCaptureFixture
+ ) -> None:
+ caplog.set_level(logging.INFO)
+
+ tmp_file = mock_file.dest
+
+ with patch("wikiget.dl.verify_hash") as mock_verify_hash:
+ mock_verify_hash.return_value = "d01b79a6781c72ac9bfff93e5e2cfbeef4efc840"
+ args = parse_args(["-o", tmp_file, "File:Example.jpg"])
+ errors = download(mock_file, args)
+
+ assert caplog.record_tuples[0] == (
+ "wikiget.dl",
+ logging.INFO,
+ "[Example.jpg] Downloading 'Example.jpg' (9022 bytes) from "
+ f"commons.wikimedia.org to '{tmp_file}'",
+ )
+ assert caplog.record_tuples[5] == (
+ "wikiget.dl",
+ logging.INFO,
+ f"[Example.jpg] 'Example.jpg' downloaded to '{tmp_file}'",
+ )
+ assert errors == 0
+
+ def test_download_dry_run(
+ self, mock_file: File, caplog: pytest.LogCaptureFixture
+ ) -> None:
+ caplog.set_level(logging.INFO)
+
+ args = parse_args(["-n", "File:Example.jpg"])
+ errors = download(mock_file, args)
+
+ # ignore first two log records since we tested for those earlier
+ assert caplog.record_tuples[2:] == [
+ ("wikiget.dl", logging.WARNING, "[Example.jpg] Dry run; download skipped"),
+ ]
+ assert errors == 0
+
+ def test_download_os_error(
+ self, mock_file: File, caplog: pytest.LogCaptureFixture
+ ) -> None:
+ with patch("wikiget.dl.open") as mock_open:
+ mock_open.side_effect = OSError("write error")
+ args = parse_args(["File:Example.jpg"])
+ errors = download(mock_file, args)
+
+ assert caplog.record_tuples == [
+ (
+ "wikiget.dl",
+ logging.ERROR,
+ "[Example.jpg] File could not be written: write error",
+ ),
+ ]
+ assert errors == 1
+
+ def test_download_verify_os_error(
+ self, mock_file: File, caplog: pytest.LogCaptureFixture
+ ) -> None:
+ with patch("wikiget.dl.verify_hash") as mock_verify_hash:
+ mock_verify_hash.side_effect = OSError("read error")
+ args = parse_args(["File:Example.jpg"])
+ errors = download(mock_file, args)
+
+ assert caplog.record_tuples == [
+ (
+ "wikiget.dl",
+ logging.ERROR,
+ "[Example.jpg] File downloaded but could not be verified: read error",
+ )
+ ]
+ assert errors == 1
+
+ def test_download_verify_hash_mismatch(
+ self, mock_file: File, caplog: pytest.LogCaptureFixture
+ ) -> None:
+ with patch("wikiget.dl.verify_hash") as mock_verify_hash:
+ mock_verify_hash.return_value = "mismatch"
+ args = parse_args(["File:Example.jpg"])
+ errors = download(mock_file, args)
+
+ assert caplog.record_tuples == [
+ (
+ "wikiget.dl",
+ logging.ERROR,
+ "[Example.jpg] Hash mismatch! Downloaded file may be corrupt.",
+ )
+ ]
+ assert errors == 1
+
+ def test_download_nonexistent_file(
+ self, mock_file: File, caplog: pytest.LogCaptureFixture
+ ) -> None:
+ mock_file.image.exists = False
+
+ args = parse_args(["File:Example.jpg"])
+ errors = download(mock_file, args)
+
+ assert caplog.record_tuples == [
+ (
+ "wikiget.dl",
+ logging.WARNING,
+ "[Example.jpg] Target does not appear to be a valid file",
+ ),
+ ]
+ assert errors == 1
diff --git a/tests/test_file_class.py b/tests/test_file_class.py
index b935efc..42f5667 100644
--- a/tests/test_file_class.py
+++ b/tests/test_file_class.py
@@ -22,7 +22,7 @@ from wikiget.file import File
class TestFileClass:
- @pytest.fixture(scope="class")
+ @pytest.fixture
def file_with_name(self) -> File:
"""
A File object created with only a name should set its destination property to
@@ -39,7 +39,7 @@ class TestFileClass:
def test_file_with_name_site(self, file_with_name: File) -> None:
assert file_with_name.site == DEFAULT_SITE
- @pytest.fixture(scope="class")
+ @pytest.fixture
def file_with_name_and_dest(self) -> File:
"""
A File object created with a name and destination should set those properties
@@ -60,3 +60,12 @@ class TestFileClass:
"""
file = File("foobar.jpg", site="en.wikipedia.org")
assert file.site == "en.wikipedia.org"
+
+ def test_file_equality(self, file_with_name: File) -> None:
+ assert File(name="foobar.jpg") == file_with_name
+
+ def test_file_inequality(self, file_with_name: File) -> None:
+ assert File(name="foobaz.jpg", dest="output.jpg") != file_with_name
+
+ def test_file_comparison_with_non_file(self, file_with_name: File) -> None:
+ assert file_with_name.__eq__({"name": "foobar.jpg"}) == NotImplemented
diff --git a/tests/test_logging.py b/tests/test_logging.py
index b189c28..55a3397 100644
--- a/tests/test_logging.py
+++ b/tests/test_logging.py
@@ -23,67 +23,51 @@ from pytest import LogCaptureFixture
from wikiget.logging import FileLogAdapter, configure_logging
from wikiget.wikiget import parse_args
-logger = logging.getLogger()
-
-def test_custom_log_adapter(caplog: LogCaptureFixture) -> None:
- """
- The custom log adapter should prepend the filename to log messages.
- """
- args = parse_args(["File:Example.jpg"])
- configure_logging(args.verbose, args.logfile, quiet=args.quiet)
- adapter = FileLogAdapter(logger, {"filename": "Example.jpg"})
- adapter.warning("test log")
- assert "[Example.jpg] test log" in caplog.text
-
-
-def test_file_logging(tmp_path: Path) -> None:
- """
- Logging to a file should create the file in the specified location.
- """
- logfile_location = tmp_path / "test.log"
- args = parse_args(["File:Example.jpg", "-l", str(logfile_location)])
- configure_logging(args.verbose, args.logfile, quiet=args.quiet)
- assert logfile_location.is_file()
-
-
-def test_default_logging() -> None:
- """
- The default log level should be set to WARNING.
- """
- args = parse_args(["File:Example.jpg"])
- configure_logging(args.verbose, args.logfile, quiet=args.quiet)
- # each call of configure_logging() adds a new handler to the logger, so we need to
- # grab the most recently added one to test
- handler = logger.handlers[-1]
- assert handler.level == logging.WARNING
-
-
-def test_verbose_logging() -> None:
- """
- When -v is passed, the log level should be set to INFO.
- """
- args = parse_args(["File:Example.jpg", "-v"])
- configure_logging(args.verbose, args.logfile, quiet=args.quiet)
- handler = logger.handlers[-1]
- assert handler.level == logging.INFO
-
-
-def test_very_verbose_logging() -> None:
- """
- When -vv is passed, the log level should be set to DEBUG.
- """
- args = parse_args(["File:Example.jpg", "-vv"])
- configure_logging(args.verbose, args.logfile, quiet=args.quiet)
- handler = logger.handlers[-1]
- assert handler.level == logging.DEBUG
-
-
-def test_quiet_logging() -> None:
- """
- When -q is passed, the log level should be set to ERROR.
- """
- args = parse_args(["File:Example.jpg", "-q"])
- configure_logging(args.verbose, args.logfile, quiet=args.quiet)
- handler = logger.handlers[-1]
- assert handler.level == logging.ERROR
+class TestLogging:
+ logger = logging.getLogger()
+
+ def test_custom_log_adapter(self, caplog: LogCaptureFixture) -> None:
+ """The custom log adapter should prepend the filename to log messages."""
+ args = parse_args(["File:Example.jpg"])
+ configure_logging(args.verbose, args.logfile, quiet=args.quiet)
+ adapter = FileLogAdapter(self.logger, {"filename": "Example.jpg"})
+ adapter.warning("test log")
+ assert "[Example.jpg] test log" in caplog.text
+
+ def test_file_logging(self, tmp_path: Path) -> None:
+ """Logging to a file should create the file in the specified location."""
+ logfile_location = tmp_path / "test.log"
+ args = parse_args(["File:Example.jpg", "-l", str(logfile_location)])
+ configure_logging(args.verbose, args.logfile, quiet=args.quiet)
+ assert logfile_location.is_file()
+
+ def test_default_logging(self) -> None:
+ """The default log level should be set to WARNING."""
+ args = parse_args(["File:Example.jpg"])
+ configure_logging(args.verbose, args.logfile, quiet=args.quiet)
+ # each call of configure_logging() adds a new handler to the logger, so we need
+ # to grab the most recently added one to test
+ handler = self.logger.handlers[-1]
+ assert handler.level == logging.WARNING
+
+ def test_verbose_logging(self) -> None:
+ """When -v is passed, the log level should be set to INFO."""
+ args = parse_args(["File:Example.jpg", "-v"])
+ configure_logging(args.verbose, args.logfile, quiet=args.quiet)
+ handler = self.logger.handlers[-1]
+ assert handler.level == logging.INFO
+
+ def test_very_verbose_logging(self) -> None:
+ """When -vv is passed, the log level should be set to DEBUG."""
+ args = parse_args(["File:Example.jpg", "-vv"])
+ configure_logging(args.verbose, args.logfile, quiet=args.quiet)
+ handler = self.logger.handlers[-1]
+ assert handler.level == logging.DEBUG
+
+ def test_quiet_logging(self) -> None:
+ """When -q is passed, the log level should be set to ERROR."""
+ args = parse_args(["File:Example.jpg", "-q"])
+ configure_logging(args.verbose, args.logfile, quiet=args.quiet)
+ handler = self.logger.handlers[-1]
+ assert handler.level == logging.ERROR
diff --git a/tests/test_parse.py b/tests/test_parse.py
index e767597..e0ac707 100644
--- a/tests/test_parse.py
+++ b/tests/test_parse.py
@@ -29,7 +29,7 @@ from wikiget.wikiget import parse_args
class TestGetDest:
- @pytest.fixture(scope="class")
+ @pytest.fixture
def file_with_filename(self) -> File:
"""
When a filename is passed to get_dest, it should create a File object with the
@@ -47,7 +47,7 @@ class TestGetDest:
def test_get_dest_site_with_filename(self, file_with_filename: File) -> None:
assert file_with_filename.site == "commons.wikimedia.org"
- @pytest.fixture(scope="class")
+ @pytest.fixture
def file_with_url(self) -> File:
"""
When a URL is passed to get_dest, it should create a File object with the
@@ -92,7 +92,7 @@ class TestGetDest:
class TestReadBatchFile:
- @pytest.fixture()
+ @pytest.fixture
def dl_dict(self, tmp_path: Path) -> Dict[int, str]:
"""
Create and process a test batch file with three lines, returning a dictionary.
@@ -129,7 +129,7 @@ class TestReadBatchFile:
expected_list = {1: "File:Foo.jpg", 2: "File:Bar.jpg", 3: "File:Baz.jpg"}
assert dl_dict == expected_list
- @pytest.fixture()
+ @pytest.fixture
def dl_dict_stdin(self, monkeypatch: pytest.MonkeyPatch) -> Dict[int, str]:
"""
Pass three lines of filenames from stdin to read_batch_file and return a dict.
@@ -165,7 +165,7 @@ class TestReadBatchFile:
expected_list = {1: "File:Foo.jpg", 2: "File:Bar.jpg", 3: "File:Baz.jpg"}
assert dl_dict_stdin == expected_list
- @pytest.fixture()
+ @pytest.fixture
def dl_dict_with_comment(self, tmp_path: Path) -> Dict[int, str]:
"""
Create and process a test batch file with four lines, one of which is
diff --git a/tests/test_validations.py b/tests/test_validations.py
index 30e59a3..b3976b6 100644
--- a/tests/test_validations.py
+++ b/tests/test_validations.py
@@ -26,7 +26,6 @@ from wikiget.validations import valid_file, valid_site, verify_hash
class TestSiteInput:
@pytest.fixture(
- scope="class",
params=[
"example.com",
"vim.wikia.com",
@@ -38,7 +37,6 @@ class TestSiteInput:
return valid_site(request.param)
@pytest.fixture(
- scope="class",
params=[
"en.wikipedia.org",
"commons.wikimedia.org",
@@ -50,20 +48,16 @@ class TestSiteInput:
return valid_site(request.param)
def test_invalid_site_input(self, invalid_input: None) -> None:
- """
- Invalid site strings should not return regex match objects.
- """
+ """Invalid site strings should not return regex match objects."""
assert invalid_input is None
def test_valid_site_input(self, valid_input: Match) -> None:
- """
- Valid site strings should return regex match objects.
- """
+ """Valid site strings should return regex match objects."""
assert valid_input is not None
class TestFileRegex:
- @pytest.fixture(scope="class")
+ @pytest.fixture
def file_match(self) -> Optional[Match]:
"""
File regex should return a match object with match groups corresponding
@@ -86,7 +80,6 @@ class TestFileRegex:
class TestFileInput:
@pytest.fixture(
- scope="class",
params=[
"file:example",
"example.jpg",
@@ -98,7 +91,6 @@ class TestFileInput:
return valid_file(request.param)
@pytest.fixture(
- scope="class",
params=[
"Image:example.jpg",
"file:example.jpg",
@@ -112,23 +104,17 @@ class TestFileInput:
return valid_file(request.param)
def test_invalid_file_input(self, invalid_input: None) -> None:
- """
- Invalid file strings should not return regex match objects.
- """
+ """Invalid file strings should not return regex match objects."""
assert invalid_input is None
def test_valid_file_input(self, valid_input: Match) -> None:
- """
- Valid file strings should return regex match objects.
- """
+ """Valid file strings should return regex match objects."""
assert valid_input is not None
class TestVerifyHash:
def test_verify_hash(self, tmp_path: Path) -> None:
- """
- Confirm that verify_hash returns the proper SHA1 hash.
- """
+ """Confirm that verify_hash returns the proper SHA1 hash."""
file_name = "testfile"
file_contents = "foobar"
file_sha1 = "8843d7f92416211de9ebb963ff4ce28125932878"
diff --git a/tests/test_wikiget_cli.py b/tests/test_wikiget_cli.py
index 99d113e..0306579 100644
--- a/tests/test_wikiget_cli.py
+++ b/tests/test_wikiget_cli.py
@@ -23,47 +23,58 @@ from wikiget import USER_AGENT, __version__
from wikiget.wikiget import cli
-def test_cli_no_params(monkeypatch: pytest.MonkeyPatch):
- monkeypatch.setattr("sys.argv", ["wikiget"])
- with pytest.raises(SystemExit) as e:
- cli()
- assert e.value.code == 2
+class TestCli:
+ def test_cli_no_params(self, monkeypatch: pytest.MonkeyPatch) -> None:
+ with monkeypatch.context() as m:
+ m.setattr("sys.argv", ["wikiget"])
+ with pytest.raises(SystemExit) as e:
+ cli()
+ assert e.value.code == 2
+ @patch("wikiget.wikiget.process_download")
+ def test_cli_completed_successfully(
+ self, mock_process_download: MagicMock, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
+ # a successful call to process_download returns 0
+ mock_process_download.return_value = 0
-@patch("wikiget.wikiget.process_download")
-def test_cli_completed_successfully(
- mock_process_download: MagicMock, monkeypatch: pytest.MonkeyPatch
-) -> None:
- monkeypatch.setattr("sys.argv", ["wikiget", "File:Example.jpg"])
- mock_process_download.return_value = 0
- with pytest.raises(SystemExit) as e:
- cli()
- assert mock_process_download.called
- assert e.value.code == 0
+ with monkeypatch.context() as m:
+ m.setattr("sys.argv", ["wikiget", "File:Example.jpg"])
+ with pytest.raises(SystemExit) as e:
+ cli()
+ assert e.value.code == 0
-@patch("wikiget.wikiget.process_download")
-def test_cli_completed_with_problems(
- mock_process_download: MagicMock, monkeypatch: pytest.MonkeyPatch
-) -> None:
- monkeypatch.setattr("sys.argv", ["wikiget", "File:Example.jpg"])
- mock_process_download.return_value = 1
- with pytest.raises(SystemExit) as e:
- cli()
- assert mock_process_download.called
- assert e.value.code == 1
+ @patch("wikiget.wikiget.process_download")
+ def test_cli_completed_with_problems(
+ self, mock_process_download: MagicMock, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
+ # an unsuccessful call to process_download returns 1
+ mock_process_download.return_value = 1
+ with monkeypatch.context() as m:
+ m.setattr("sys.argv", ["wikiget", "File:Example.jpg"])
-@patch("wikiget.wikiget.process_download")
-def test_cli_logs(
- mock_process_download: MagicMock,
- monkeypatch: pytest.MonkeyPatch,
- caplog: pytest.LogCaptureFixture,
-) -> None:
- monkeypatch.setattr("sys.argv", ["wikiget", "File:Example.jpg"])
- mock_process_download.return_value = 0
- with pytest.raises(SystemExit):
- cli()
- assert mock_process_download.called
- assert f"Starting download session using wikiget {__version__}" in caplog.text
- assert USER_AGENT in caplog.text
+ with pytest.raises(SystemExit) as e:
+ cli()
+ assert e.value.code == 1
+
+ @patch("wikiget.wikiget.process_download")
+ def test_cli_logs(
+ self,
+ mock_process_download: MagicMock,
+ monkeypatch: pytest.MonkeyPatch,
+ caplog: pytest.LogCaptureFixture,
+ ) -> None:
+ # a successful call to process_download returns 0
+ mock_process_download.return_value = 0
+
+ with monkeypatch.context() as m:
+ m.setattr("sys.argv", ["wikiget", "File:Example.jpg"])
+
+ with pytest.raises(SystemExit):
+ cli()
+ assert (
+ f"Starting download session using wikiget {__version__}" in caplog.text
+ )
+ assert USER_AGENT in caplog.text