From 08a5907bd8b34e2f99a0c74e6756c66134ceb7d3 Mon Sep 17 00:00:00 2001 From: Cody Logan Date: Fri, 3 Nov 2023 10:40:07 -0700 Subject: Move from unittest.mock to pytest's monkeypatch where feasible --- tests/test_client.py | 2 +- tests/test_dl.py | 115 ++++++++++++++++++++++++---------------------- tests/test_file_class.py | 4 +- tests/test_logging.py | 110 +++++++++++++++++++------------------------- tests/test_parse.py | 10 ++-- tests/test_validations.py | 26 +++-------- tests/test_wikiget_cli.py | 84 +++++++++++++++++---------------- 7 files changed, 165 insertions(+), 186 deletions(-) (limited to 'tests') diff --git a/tests/test_client.py b/tests/test_client.py index 9b1b8a4..c34a842 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -25,7 +25,6 @@ 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( @@ -42,6 +41,7 @@ class TestQueryApi: assert mock_site.called assert "Connecting to commons.wikimedia.org" in caplog.text + # TODO: don't hit the actual API when doing tests @pytest.mark.skip(reason="skip tests that query a live API") def test_query_api(self, caplog: pytest.LogCaptureFixture) -> None: """ diff --git a/tests/test_dl.py b/tests/test_dl.py index a57c3c1..297c59d 100644 --- a/tests/test_dl.py +++ b/tests/test_dl.py @@ -16,7 +16,6 @@ # along with Wikiget. If not, see . from pathlib import Path -from unittest.mock import MagicMock, patch import pytest @@ -25,13 +24,11 @@ from wikiget.file import File from wikiget.wikiget import parse_args -# TODO: don't hit the actual API when doing tests class TestPrepDownload: + # TODO: don't hit the actual API when doing tests @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. - """ + """The prep_download function should create a file object.""" args = parse_args(["File:Example.jpg"]) file = prep_download(args.FILE, args) assert file is not None @@ -49,60 +46,68 @@ 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"]) - mock_batch_download.return_value = 0 - exit_code = process_download(args) - assert mock_batch_download.called - assert exit_code == 0 + def test_batch_download(self, monkeypatch: pytest.MonkeyPatch) -> None: + """A successful batch download should not return any errors.""" + + def mock_batch_download(*args, **kwargs): # noqa: ARG001 + return 0 + + with monkeypatch.context() as m: + m.setattr("wikiget.dl.batch_download", mock_batch_download) + + args = parse_args(["-a", "batch.txt"]) + exit_code = process_download(args) + assert exit_code == 0 - @patch("wikiget.dl.batch_download") def test_batch_download_with_errors( - self, mock_batch_download: MagicMock, caplog: pytest.LogCaptureFixture + self, monkeypatch: pytest.MonkeyPatch, 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 - 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( - self, mock_download: MagicMock, mock_prep_download: MagicMock - ) -> None: - """ - A successful download should not return any errors. - """ - args = parse_args(["File:Example.jpg"]) - mock_download.return_value = 0 - mock_prep_download.return_value = MagicMock(File) - 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( - self, mock_download: MagicMock, mock_prep_download: MagicMock - ) -> None: - """ - Any errors during download should result in a non-zero exit code. - """ - 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 + + def mock_batch_download(*args, **kwargs): # noqa: ARG001 + return 4 + + with monkeypatch.context() as m: + m.setattr("wikiget.dl.batch_download", mock_batch_download) + + args = parse_args(["-a", "batch.txt"]) + exit_code = process_download(args) + assert exit_code == 1 + assert "4 problems encountered during batch processing" in caplog.text + + def test_single_download(self, monkeypatch: pytest.MonkeyPatch) -> None: + """A successful download should not return any errors.""" + + def mock_download(*args, **kwargs): # noqa: ARG001 + return 0 + + def mock_prep_download(*args, **kwargs): # noqa ARG001 + return File("Example.jpg") + + with monkeypatch.context() as m: + m.setattr("wikiget.dl.download", mock_download) + m.setattr("wikiget.dl.prep_download", mock_prep_download) + + args = parse_args(["File:Example.jpg"]) + exit_code = process_download(args) + assert exit_code == 0 + + def test_single_download_with_errors(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Any errors during download should result in a non-zero exit code.""" + + def mock_download(*args, **kwargs): # noqa: ARG001 + return 1 + + def mock_prep_download(*args, **kwargs): # noqa ARG001 + return File("Example.jpg") + + with monkeypatch.context() as m: + m.setattr("wikiget.dl.download", mock_download) + m.setattr("wikiget.dl.prep_download", mock_prep_download) + + args = parse_args(["File:Example.jpg"]) + exit_code = process_download(args) + assert exit_code == 1 diff --git a/tests/test_file_class.py b/tests/test_file_class.py index b935efc..ede4be1 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 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..86b3780 100644 --- a/tests/test_wikiget_cli.py +++ b/tests/test_wikiget_cli.py @@ -15,55 +15,59 @@ # You should have received a copy of the GNU General Public License # along with Wikiget. If not, see . -from unittest.mock import MagicMock, patch - import pytest 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: + monkeypatch.setattr("sys.argv", ["wikiget"]) + with pytest.raises(SystemExit) as e: + cli() + assert e.value.code == 2 + + def test_cli_completed_successfully(self, monkeypatch: pytest.MonkeyPatch) -> None: + def mock_process_download(*args, **kwargs) -> int: # noqa: ARG001 + """A successful call to process_download returns 0.""" + return 0 + + with monkeypatch.context() as m: + m.setattr("sys.argv", ["wikiget", "File:Example.jpg"]) + m.setattr("wikiget.wikiget.process_download", mock_process_download) + + with pytest.raises(SystemExit) as e: + cli() + assert e.value.code == 0 + def test_cli_completed_with_problems(self, monkeypatch: pytest.MonkeyPatch) -> None: + def mock_process_download(*args, **kwargs) -> int: # noqa: ARG001 + """An unsuccessful call to process_download returns 1.""" + return 1 -@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"]) + m.setattr("wikiget.wikiget.process_download", mock_process_download) + with pytest.raises(SystemExit) as e: + cli() + assert e.value.code == 1 -@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 + def test_cli_logs( + self, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture + ) -> None: + def mock_process_download(*args, **kwargs) -> int: # noqa: ARG001 + """A successful call to process_download returns 0.""" + return 0 + with monkeypatch.context() as m: + m.setattr("sys.argv", ["wikiget", "File:Example.jpg"]) + m.setattr("wikiget.wikiget.process_download", mock_process_download) -@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): + cli() + assert ( + f"Starting download session using wikiget {__version__}" in caplog.text + ) + assert USER_AGENT in caplog.text -- cgit v1.2.3 From f28e25c999169a0ea0dc6dc12895a4b6dfab148f Mon Sep 17 00:00:00 2001 From: Cody Logan Date: Fri, 3 Nov 2023 12:09:42 -0700 Subject: Test for exceptions in connect_to_site --- tests/test_client.py | 81 ++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 69 insertions(+), 12 deletions(-) (limited to 'tests') diff --git a/tests/test_client.py b/tests/test_client.py index c34a842..18af3f9 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -16,30 +16,87 @@ # along with Wikiget. If not, see . import logging -from unittest.mock import MagicMock, patch +from unittest.mock import patch import pytest +from mwclient import InvalidResponse +from requests import ConnectionError, HTTPError -from wikiget import USER_AGENT +from wikiget import DEFAULT_SITE, USER_AGENT from wikiget.client import connect_to_site, query_api from wikiget.wikiget import parse_args -class TestQueryApi: - @patch("mwclient.Site.__new__") - def test_connect_to_site( - self, mock_site: MagicMock, caplog: pytest.LogCaptureFixture - ) -> None: +class TestClient: + 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, f"Connecting to {DEFAULT_SITE}"), + ] + + def test_connect_to_site_connection_error( + self, caplog: pytest.LogCaptureFixture + ) -> None: + """ + The connect_to_site function should log an error if a ConnectionError exception + is raised. + """ + caplog.set_level(logging.DEBUG) + args = parse_args(["File:Example.jpg"]) + 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, f"Connecting to {DEFAULT_SITE}"), + ("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 connect_to_site function should log an error if an HTTPError 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 + 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, f"Connecting to {DEFAULT_SITE}"), + ( + "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" # TODO: don't hit the actual API when doing tests @pytest.mark.skip(reason="skip tests that query a live API") -- cgit v1.2.3 From 5b3fd4383462503c8d9eaae53692e68b159d5536 Mon Sep 17 00:00:00 2001 From: Cody Logan Date: Fri, 3 Nov 2023 16:37:59 -0700 Subject: Revise query_api test to use mock objects --- tests/test_client.py | 48 ++++++++++++++++++++++++++---------------------- 1 file changed, 26 insertions(+), 22 deletions(-) (limited to 'tests') diff --git a/tests/test_client.py b/tests/test_client.py index 18af3f9..fd47a35 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -16,18 +16,22 @@ # along with Wikiget. If not, see . import logging -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pytest -from mwclient import InvalidResponse +from mwclient import InvalidResponse, Site +from mwclient.image import Image from requests import ConnectionError, HTTPError -from wikiget import DEFAULT_SITE, USER_AGENT +from wikiget import DEFAULT_SITE from wikiget.client import connect_to_site, query_api from wikiget.wikiget import parse_args -class TestClient: +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 an info log message recording the @@ -38,15 +42,15 @@ class TestClient: with patch("wikiget.client.Site"): _ = connect_to_site(DEFAULT_SITE, args) assert caplog.record_tuples == [ - ("wikiget.client", logging.INFO, f"Connecting to {DEFAULT_SITE}"), + ("wikiget.client", logging.INFO, self.info_msg), ] def test_connect_to_site_connection_error( self, caplog: pytest.LogCaptureFixture ) -> None: """ - The connect_to_site function should log an error if a ConnectionError exception - is raised. + The connect_to_site function should log the correct messages if a + ConnectionError exception is raised. """ caplog.set_level(logging.DEBUG) args = parse_args(["File:Example.jpg"]) @@ -56,15 +60,15 @@ class TestClient: _ = connect_to_site(DEFAULT_SITE, args) assert "Could not connect to specified site" in caplog.text assert caplog.record_tuples == [ - ("wikiget.client", logging.INFO, f"Connecting to {DEFAULT_SITE}"), + ("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 connect_to_site function should log an error if an HTTPError exception - is raised. + 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"]) @@ -73,14 +77,14 @@ class TestClient: with pytest.raises(HTTPError): _ = connect_to_site(DEFAULT_SITE, args) assert caplog.record_tuples == [ - ("wikiget.client", logging.INFO, f"Connecting to {DEFAULT_SITE}"), + ("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, "") + ("wikiget.client", logging.DEBUG, ""), ] def test_connect_to_site_other_error( @@ -98,15 +102,15 @@ class TestClient: for record in caplog.records: assert record.levelname == "ERROR" - # TODO: don't hit the actual API when doing tests - @pytest.mark.skip(reason="skip tests that query a live API") - def test_query_api(self, caplog: pytest.LogCaptureFixture) -> None: + +class TestQueryApi: + def test_query_api(self) -> None: """ - The query_api function should create a debug log message containing the user - agent we're sending to the API. + The query_api function should return an Image object when given a name and a + valid Site. """ - 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 + mock_site = MagicMock(Site) + mock_image = MagicMock(Image) + mock_site.images = {"Example.jpg": mock_image} + image = query_api("Example.jpg", mock_site) + assert image == mock_image -- cgit v1.2.3 From 831fb088d6f902bf5da52a4da7f2d5d731d9f72e Mon Sep 17 00:00:00 2001 From: Cody Logan Date: Mon, 6 Nov 2023 11:34:51 -0800 Subject: Add explanatory comment to query_api test --- tests/test_client.py | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'tests') diff --git a/tests/test_client.py b/tests/test_client.py index fd47a35..45739d3 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -109,8 +109,13 @@ class TestQueryApi: 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(Site) mock_image = MagicMock(Image) mock_site.images = {"Example.jpg": mock_image} + image = query_api("Example.jpg", mock_site) assert image == mock_image -- cgit v1.2.3 From 56dbc899c6bec74cba25483768e855ad7953a432 Mon Sep 17 00:00:00 2001 From: Cody Logan Date: Mon, 6 Nov 2023 11:52:50 -0800 Subject: Test that prep_download returns the right File object --- tests/test_dl.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) (limited to 'tests') diff --git a/tests/test_dl.py b/tests/test_dl.py index 297c59d..6d0b484 100644 --- a/tests/test_dl.py +++ b/tests/test_dl.py @@ -16,8 +16,11 @@ # along with Wikiget. If not, see . from pathlib import Path +from unittest.mock import MagicMock, patch import pytest +from mwclient import Site +from mwclient.image import Image from wikiget.dl import prep_download, process_download from wikiget.file import File @@ -25,13 +28,24 @@ from wikiget.wikiget import parse_args class TestPrepDownload: - # TODO: don't hit the actual API when doing tests - @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 = MagicMock(Site) + mock_image = MagicMock(Image) + + 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: """ -- cgit v1.2.3 From 2d18a4cefff6f26a48966e66774c463dfbbe2492 Mon Sep 17 00:00:00 2001 From: Cody Logan Date: Mon, 6 Nov 2023 12:06:08 -0800 Subject: Be more consistent in use of mocking method --- tests/test_dl.py | 82 +++++++++++++++++++---------------------------- tests/test_wikiget_cli.py | 45 +++++++++++++++----------- 2 files changed, 59 insertions(+), 68 deletions(-) (limited to 'tests') diff --git a/tests/test_dl.py b/tests/test_dl.py index 6d0b484..f2a942f 100644 --- a/tests/test_dl.py +++ b/tests/test_dl.py @@ -60,68 +60,52 @@ class TestPrepDownload: class TestProcessDownload: - def test_batch_download(self, monkeypatch: pytest.MonkeyPatch) -> None: + @patch("wikiget.dl.batch_download") + def test_batch_download(self, mock_batch_download: MagicMock) -> None: """A successful batch download should not return any errors.""" + mock_batch_download.return_value = 0 - def mock_batch_download(*args, **kwargs): # noqa: ARG001 - return 0 - - with monkeypatch.context() as m: - m.setattr("wikiget.dl.batch_download", mock_batch_download) - - args = parse_args(["-a", "batch.txt"]) - exit_code = process_download(args) - assert exit_code == 0 + args = parse_args(["-a", "batch.txt"]) + exit_code = process_download(args) + assert exit_code == 0 + @patch("wikiget.dl.batch_download") def test_batch_download_with_errors( - self, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture + 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. """ + mock_batch_download.return_value = 4 - def mock_batch_download(*args, **kwargs): # noqa: ARG001 - return 4 - - with monkeypatch.context() as m: - m.setattr("wikiget.dl.batch_download", mock_batch_download) - - args = parse_args(["-a", "batch.txt"]) - exit_code = process_download(args) - assert exit_code == 1 - assert "4 problems encountered during batch processing" in caplog.text + args = parse_args(["-a", "batch.txt"]) + exit_code = process_download(args) + assert exit_code == 1 + assert "4 problems encountered during batch processing" in caplog.text - def test_single_download(self, monkeypatch: pytest.MonkeyPatch) -> None: + @patch("wikiget.dl.prep_download") + @patch("wikiget.dl.download") + def test_single_download( + self, mock_download: MagicMock, mock_prep_download: MagicMock + ) -> None: """A successful download should not return any errors.""" + mock_download.return_value = 0 + mock_prep_download.return_value = File("Example.jpg") - def mock_download(*args, **kwargs): # noqa: ARG001 - return 0 - - def mock_prep_download(*args, **kwargs): # noqa ARG001 - return File("Example.jpg") - - with monkeypatch.context() as m: - m.setattr("wikiget.dl.download", mock_download) - m.setattr("wikiget.dl.prep_download", mock_prep_download) - - args = parse_args(["File:Example.jpg"]) - exit_code = process_download(args) - assert exit_code == 0 + args = parse_args(["File:Example.jpg"]) + exit_code = process_download(args) + assert exit_code == 0 - def test_single_download_with_errors(self, monkeypatch: pytest.MonkeyPatch) -> None: + @patch("wikiget.dl.prep_download") + @patch("wikiget.dl.download") + def test_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") - def mock_download(*args, **kwargs): # noqa: ARG001 - return 1 - - def mock_prep_download(*args, **kwargs): # noqa ARG001 - return File("Example.jpg") - - with monkeypatch.context() as m: - m.setattr("wikiget.dl.download", mock_download) - m.setattr("wikiget.dl.prep_download", mock_prep_download) - - args = parse_args(["File:Example.jpg"]) - exit_code = process_download(args) - assert exit_code == 1 + args = parse_args(["File:Example.jpg"]) + exit_code = process_download(args) + assert exit_code == 1 diff --git a/tests/test_wikiget_cli.py b/tests/test_wikiget_cli.py index 86b3780..0306579 100644 --- a/tests/test_wikiget_cli.py +++ b/tests/test_wikiget_cli.py @@ -15,6 +15,8 @@ # You should have received a copy of the GNU General Public License # along with Wikiget. If not, see . +from unittest.mock import MagicMock, patch + import pytest from wikiget import USER_AGENT, __version__ @@ -23,47 +25,52 @@ from wikiget.wikiget import cli class TestCli: def test_cli_no_params(self, monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr("sys.argv", ["wikiget"]) - with pytest.raises(SystemExit) as e: - cli() - assert e.value.code == 2 + with monkeypatch.context() as m: + m.setattr("sys.argv", ["wikiget"]) + with pytest.raises(SystemExit) as e: + cli() + assert e.value.code == 2 - def test_cli_completed_successfully(self, monkeypatch: pytest.MonkeyPatch) -> None: - def mock_process_download(*args, **kwargs) -> int: # noqa: ARG001 - """A successful call to process_download returns 0.""" - return 0 + @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 with monkeypatch.context() as m: m.setattr("sys.argv", ["wikiget", "File:Example.jpg"]) - m.setattr("wikiget.wikiget.process_download", mock_process_download) with pytest.raises(SystemExit) as e: cli() assert e.value.code == 0 - def test_cli_completed_with_problems(self, monkeypatch: pytest.MonkeyPatch) -> None: - def mock_process_download(*args, **kwargs) -> int: # noqa: ARG001 - """An unsuccessful call to process_download returns 1.""" - return 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"]) - m.setattr("wikiget.wikiget.process_download", mock_process_download) with pytest.raises(SystemExit) as e: cli() assert e.value.code == 1 + @patch("wikiget.wikiget.process_download") def test_cli_logs( - self, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture + self, + mock_process_download: MagicMock, + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, ) -> None: - def mock_process_download(*args, **kwargs) -> int: # noqa: ARG001 - """A successful call to process_download returns 0.""" - return 0 + # 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"]) - m.setattr("wikiget.wikiget.process_download", mock_process_download) with pytest.raises(SystemExit): cli() -- cgit v1.2.3 From 3d34b09a361dadb50bb4e4ffa18c75928904c30d Mon Sep 17 00:00:00 2001 From: Cody Logan Date: Mon, 6 Nov 2023 12:18:46 -0800 Subject: Simplify mock usage in tests --- tests/test_client.py | 12 +++++------- tests/test_dl.py | 8 +++----- 2 files changed, 8 insertions(+), 12 deletions(-) (limited to 'tests') diff --git a/tests/test_client.py b/tests/test_client.py index 45739d3..207d9b2 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -16,11 +16,10 @@ # along with Wikiget. If not, see . import logging -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock, patch, sentinel import pytest -from mwclient import InvalidResponse, Site -from mwclient.image import Image +from mwclient import InvalidResponse from requests import ConnectionError, HTTPError from wikiget import DEFAULT_SITE @@ -113,9 +112,8 @@ class TestQueryApi: # 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(Site) - mock_image = MagicMock(Image) - mock_site.images = {"Example.jpg": mock_image} + mock_site = MagicMock() + mock_site.images = {"Example.jpg": sentinel.mock_image} image = query_api("Example.jpg", mock_site) - assert image == mock_image + assert image == sentinel.mock_image diff --git a/tests/test_dl.py b/tests/test_dl.py index f2a942f..08cf5b4 100644 --- a/tests/test_dl.py +++ b/tests/test_dl.py @@ -16,11 +16,9 @@ # along with Wikiget. If not, see . from pathlib import Path -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock, Mock, patch import pytest -from mwclient import Site -from mwclient.image import Image from wikiget.dl import prep_download, process_download from wikiget.file import File @@ -34,8 +32,8 @@ class TestPrepDownload: self, mock_connect_to_site: MagicMock, mock_query_api: MagicMock ) -> None: """The prep_download function should create the expected file object.""" - mock_site = MagicMock(Site) - mock_image = MagicMock(Image) + mock_site = Mock() + mock_image = Mock() mock_connect_to_site.return_value = mock_site mock_query_api.return_value = mock_image -- cgit v1.2.3 From 8c206d1e3857e17f5fd0f86a1ab6cfbc038c17ae Mon Sep 17 00:00:00 2001 From: Cody Logan Date: Mon, 6 Nov 2023 16:52:40 -0800 Subject: Additional download tests --- tests/test_dl.py | 274 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 269 insertions(+), 5 deletions(-) (limited to 'tests') diff --git a/tests/test_dl.py b/tests/test_dl.py index 08cf5b4..331b08f 100644 --- a/tests/test_dl.py +++ b/tests/test_dl.py @@ -15,12 +15,15 @@ # You should have received a copy of the GNU General Public License # along with Wikiget. If not, see . +import logging from pathlib import Path from unittest.mock import MagicMock, Mock, patch import pytest +from requests import ConnectionError -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 @@ -43,6 +46,7 @@ class TestPrepDownload: args = parse_args(["File:Example.jpg"]) file = prep_download(args.FILE, args) + assert file == expected_file def test_prep_download_with_existing_file(self, tmp_path: Path) -> None: @@ -59,16 +63,17 @@ class TestPrepDownload: class TestProcessDownload: @patch("wikiget.dl.batch_download") - def test_batch_download(self, mock_batch_download: MagicMock) -> None: + 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 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: """ @@ -79,12 +84,13 @@ class TestProcessDownload: args = parse_args(["-a", "batch.txt"]) exit_code = process_download(args) + 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.""" @@ -93,11 +99,12 @@ class TestProcessDownload: args = parse_args(["File:Example.jpg"]) exit_code = process_download(args) + 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.""" @@ -106,4 +113,261 @@ class TestProcessDownload: 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: + """ + 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 = ConnectionError + + args = parse_args(["File:Example.jpg"]) + exit_code = process_download(args) + + assert mock_prep_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 = 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 + + +class TestDownload: + @pytest.fixture + def mock_file(self) -> File: + file = File("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 = Mock() + file.image.site.host = "commons.wikimedia.org" + return file + + # TODO: test dry run option separately + def test_download(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) + + 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.WARNING, "[Example.jpg] Dry run; download skipped"), + ] + assert errors == 0 + + def test_download_with_output( + self, mock_file: File, caplog: pytest.LogCaptureFixture + ) -> None: + caplog.set_level(logging.INFO) + + mock_file.dest = "output.jpg" + args = parse_args(["-n", "-o", "output.jpg", "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 " + "commons.wikimedia.org to 'output.jpg'", + ) + assert errors == 0 + + def test_download_os_error( + self, mock_file: File, tmp_path: Path, caplog: pytest.LogCaptureFixture + ) -> None: + mock_file.dest = str(tmp_path / "Example.jpg") + + 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_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 -- cgit v1.2.3 From af02ca9a73807a2e59249e6c80af57d39d444e69 Mon Sep 17 00:00:00 2001 From: Cody Logan Date: Mon, 6 Nov 2023 17:11:01 -0800 Subject: Additional download tests using requests_mock --- tests/test_dl.py | 63 ++++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 54 insertions(+), 9 deletions(-) (limited to 'tests') diff --git a/tests/test_dl.py b/tests/test_dl.py index 331b08f..31964d9 100644 --- a/tests/test_dl.py +++ b/tests/test_dl.py @@ -20,7 +20,8 @@ from pathlib import Path from unittest.mock import MagicMock, Mock, patch import pytest -from requests import ConnectionError +import requests +from mwclient import Site from wikiget.dl import batch_download, download, prep_download, process_download from wikiget.exceptions import ParseError @@ -156,7 +157,7 @@ class TestProcessDownload: """ If process_download catches any other errors, it should return 1. """ - mock_prep_download.side_effect = ConnectionError + mock_prep_download.side_effect = requests.ConnectionError args = parse_args(["File:Example.jpg"]) exit_code = process_download(args) @@ -263,7 +264,7 @@ class TestBatchDownload: caplog: pytest.LogCaptureFixture, ) -> None: mock_read_batch_file.return_value = {1: "File:Example.jpg"} - mock_prep_download.side_effect = ConnectionError + mock_prep_download.side_effect = requests.ConnectionError args = parse_args(["-a", "batch.txt"]) errors = batch_download(args) @@ -291,16 +292,32 @@ class TestDownload: "size": 9022, "sha1": "d01b79a6781c72ac9bfff93e5e2cfbeef4efc840", } - file.image.site = Mock() + file.image.site = MagicMock(Site) file.image.site.host = "commons.wikimedia.org" + file.image.site.connection = requests.Session() return file - # TODO: test dry run option separately - def test_download(self, mock_file: File, caplog: pytest.LogCaptureFixture) -> None: + def test_download( + self, + mock_file: File, + requests_mock, + tmp_path: Path, + caplog: pytest.LogCaptureFixture, + ) -> None: caplog.set_level(logging.INFO) - args = parse_args(["-n", "File:Example.jpg"]) - errors = download(mock_file, args) + # fake the download request + requests_mock.get( + "https://upload.wikimedia.org/wikipedia/commons/a/a9/Example.jpg", + text="data", + ) + # save to a temp directory + mock_file.dest = str(tmp_path / "Example.jpg") + + 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 == [ ( @@ -315,7 +332,20 @@ class TestDownload: "[Example.jpg] " "https://upload.wikimedia.org/wikipedia/commons/a/a9/Example.jpg", ), - ("wikiget.dl", logging.WARNING, "[Example.jpg] Dry run; download skipped"), + ( + "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 @@ -325,6 +355,7 @@ class TestDownload: caplog.set_level(logging.INFO) mock_file.dest = "output.jpg" + # TODO: remove dry run option from this test args = parse_args(["-n", "-o", "output.jpg", "File:Example.jpg"]) errors = download(mock_file, args) @@ -336,6 +367,20 @@ class TestDownload: ) 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, tmp_path: Path, caplog: pytest.LogCaptureFixture ) -> None: -- cgit v1.2.3 From 749e5afaa1d85d9cf39cf96deb9b980a741f72da Mon Sep 17 00:00:00 2001 From: Cody Logan Date: Tue, 7 Nov 2023 10:36:25 -0800 Subject: Full coverage on dl module --- tests/test_dl.py | 91 +++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 84 insertions(+), 7 deletions(-) (limited to 'tests') diff --git a/tests/test_dl.py b/tests/test_dl.py index 31964d9..cfcefef 100644 --- a/tests/test_dl.py +++ b/tests/test_dl.py @@ -21,6 +21,7 @@ from unittest.mock import MagicMock, Mock, patch import pytest import requests +import requests_mock as rm from mwclient import Site from wikiget.dl import batch_download, download, prep_download, process_download @@ -300,7 +301,7 @@ class TestDownload: def test_download( self, mock_file: File, - requests_mock, + requests_mock: rm.Mocker, tmp_path: Path, caplog: pytest.LogCaptureFixture, ) -> None: @@ -350,20 +351,38 @@ class TestDownload: assert errors == 0 def test_download_with_output( - self, mock_file: File, caplog: pytest.LogCaptureFixture + self, + mock_file: File, + requests_mock: rm.Mocker, + tmp_path: Path, + caplog: pytest.LogCaptureFixture, ) -> None: caplog.set_level(logging.INFO) - mock_file.dest = "output.jpg" - # TODO: remove dry run option from this test - args = parse_args(["-n", "-o", "output.jpg", "File:Example.jpg"]) - errors = download(mock_file, args) + # fake the download request + requests_mock.get( + "https://upload.wikimedia.org/wikipedia/commons/a/a9/Example.jpg", + text="data", + ) + # save to a temp directory + tmp_file = str(tmp_path / "Example.jpg") + mock_file.dest = tmp_file + + 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 " - "commons.wikimedia.org to 'output.jpg'", + 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 @@ -400,6 +419,64 @@ class TestDownload: ] assert errors == 1 + def test_download_verify_os_error( + self, + mock_file: File, + requests_mock: rm.Mocker, + tmp_path: Path, + caplog: pytest.LogCaptureFixture, + ) -> None: + # fake the download request + requests_mock.get( + "https://upload.wikimedia.org/wikipedia/commons/a/a9/Example.jpg", + text="data", + ) + # save to a temp directory + mock_file.dest = str(tmp_path / "Example.jpg") + + 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, + requests_mock: rm.Mocker, + tmp_path: Path, + caplog: pytest.LogCaptureFixture, + ) -> None: + # fake the download request + requests_mock.get( + "https://upload.wikimedia.org/wikipedia/commons/a/a9/Example.jpg", + text="data", + ) + # save to a temp directory + mock_file.dest = str(tmp_path / "Example.jpg") + + 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: -- cgit v1.2.3 From 932869167e7b36c804ce42becb98a98a12b13340 Mon Sep 17 00:00:00 2001 From: Cody Logan Date: Tue, 7 Nov 2023 10:56:46 -0800 Subject: Reduce duplicated code in dl tests --- tests/conftest.py | 28 ++++++++++++++++++++++ tests/test_dl.py | 69 ++++++++----------------------------------------------- 2 files changed, 37 insertions(+), 60 deletions(-) create mode 100644 tests/conftest.py (limited to 'tests') 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 . + +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_dl.py b/tests/test_dl.py index cfcefef..d39c352 100644 --- a/tests/test_dl.py +++ b/tests/test_dl.py @@ -21,7 +21,6 @@ from unittest.mock import MagicMock, Mock, patch import pytest import requests -import requests_mock as rm from mwclient import Site from wikiget.dl import batch_download, download, prep_download, process_download @@ -282,10 +281,11 @@ class TestBatchDownload: assert errors == 1 +@pytest.mark.usefixtures("mock_get") class TestDownload: @pytest.fixture - def mock_file(self) -> File: - file = File("Example.jpg") + 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 = { @@ -298,23 +298,9 @@ class TestDownload: file.image.site.connection = requests.Session() return file - def test_download( - self, - mock_file: File, - requests_mock: rm.Mocker, - tmp_path: Path, - caplog: pytest.LogCaptureFixture, - ) -> None: + def test_download(self, mock_file: File, caplog: pytest.LogCaptureFixture) -> None: caplog.set_level(logging.INFO) - # fake the download request - requests_mock.get( - "https://upload.wikimedia.org/wikipedia/commons/a/a9/Example.jpg", - text="data", - ) - # save to a temp directory - mock_file.dest = str(tmp_path / "Example.jpg") - with patch("wikiget.dl.verify_hash") as mock_verify_hash: mock_verify_hash.return_value = "d01b79a6781c72ac9bfff93e5e2cfbeef4efc840" args = parse_args(["File:Example.jpg"]) @@ -351,22 +337,11 @@ class TestDownload: assert errors == 0 def test_download_with_output( - self, - mock_file: File, - requests_mock: rm.Mocker, - tmp_path: Path, - caplog: pytest.LogCaptureFixture, + self, mock_file: File, caplog: pytest.LogCaptureFixture ) -> None: caplog.set_level(logging.INFO) - # fake the download request - requests_mock.get( - "https://upload.wikimedia.org/wikipedia/commons/a/a9/Example.jpg", - text="data", - ) - # save to a temp directory - tmp_file = str(tmp_path / "Example.jpg") - mock_file.dest = tmp_file + tmp_file = mock_file.dest with patch("wikiget.dl.verify_hash") as mock_verify_hash: mock_verify_hash.return_value = "d01b79a6781c72ac9bfff93e5e2cfbeef4efc840" @@ -401,10 +376,8 @@ class TestDownload: assert errors == 0 def test_download_os_error( - self, mock_file: File, tmp_path: Path, caplog: pytest.LogCaptureFixture + self, mock_file: File, caplog: pytest.LogCaptureFixture ) -> None: - mock_file.dest = str(tmp_path / "Example.jpg") - with patch("wikiget.dl.open") as mock_open: mock_open.side_effect = OSError("write error") args = parse_args(["File:Example.jpg"]) @@ -420,20 +393,8 @@ class TestDownload: assert errors == 1 def test_download_verify_os_error( - self, - mock_file: File, - requests_mock: rm.Mocker, - tmp_path: Path, - caplog: pytest.LogCaptureFixture, + self, mock_file: File, caplog: pytest.LogCaptureFixture ) -> None: - # fake the download request - requests_mock.get( - "https://upload.wikimedia.org/wikipedia/commons/a/a9/Example.jpg", - text="data", - ) - # save to a temp directory - mock_file.dest = str(tmp_path / "Example.jpg") - with patch("wikiget.dl.verify_hash") as mock_verify_hash: mock_verify_hash.side_effect = OSError("read error") args = parse_args(["File:Example.jpg"]) @@ -449,20 +410,8 @@ class TestDownload: assert errors == 1 def test_download_verify_hash_mismatch( - self, - mock_file: File, - requests_mock: rm.Mocker, - tmp_path: Path, - caplog: pytest.LogCaptureFixture, + self, mock_file: File, caplog: pytest.LogCaptureFixture ) -> None: - # fake the download request - requests_mock.get( - "https://upload.wikimedia.org/wikipedia/commons/a/a9/Example.jpg", - text="data", - ) - # save to a temp directory - mock_file.dest = str(tmp_path / "Example.jpg") - with patch("wikiget.dl.verify_hash") as mock_verify_hash: mock_verify_hash.return_value = "mismatch" args = parse_args(["File:Example.jpg"]) -- cgit v1.2.3 From 4f0d86be186c290b199ee69b1f929c24de59c556 Mon Sep 17 00:00:00 2001 From: Cody Logan Date: Tue, 7 Nov 2023 11:07:10 -0800 Subject: Add tests for File equality --- tests/test_file_class.py | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'tests') diff --git a/tests/test_file_class.py b/tests/test_file_class.py index ede4be1..42f5667 100644 --- a/tests/test_file_class.py +++ b/tests/test_file_class.py @@ -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 -- cgit v1.2.3 From 14b3f3e4c48183776d3021fa596f30d2a3c1091f Mon Sep 17 00:00:00 2001 From: Cody Logan Date: Tue, 7 Nov 2023 11:24:35 -0800 Subject: Emit a log message when authenticating with a private wiki --- tests/test_client.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) (limited to 'tests') diff --git a/tests/test_client.py b/tests/test_client.py index 207d9b2..e17c17a 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -38,12 +38,33 @@ class TestConnectSite: """ 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: @@ -53,10 +74,12 @@ class TestConnectSite: """ caplog.set_level(logging.DEBUG) args = parse_args(["File:Example.jpg"]) + 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), @@ -71,10 +94,12 @@ class TestConnectSite: """ caplog.set_level(logging.DEBUG) args = parse_args(["File:Example.jpg"]) + 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), ( @@ -94,10 +119,12 @@ class TestConnectSite: 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" @@ -116,4 +143,5 @@ class TestQueryApi: mock_site.images = {"Example.jpg": sentinel.mock_image} image = query_api("Example.jpg", mock_site) + assert image == sentinel.mock_image -- cgit v1.2.3 From afd8bcae61290ed7025cbb6e6da4e8dcd1055e4f Mon Sep 17 00:00:00 2001 From: Cody Logan Date: Tue, 7 Nov 2023 11:35:39 -0800 Subject: Test query_api when an APIError is raised --- tests/test_client.py | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/test_client.py b/tests/test_client.py index e17c17a..4cbf702 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -19,7 +19,7 @@ import logging from unittest.mock import MagicMock, patch, sentinel import pytest -from mwclient import InvalidResponse +from mwclient import APIError, InvalidResponse from requests import ConnectionError, HTTPError from wikiget import DEFAULT_SITE @@ -145,3 +145,31 @@ class TestQueryApi: 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"), + ] -- cgit v1.2.3