diff options
| -rw-r--r-- | src/wikiget/validations.py | 23 | ||||
| -rw-r--r-- | src/wikiget/wikiget.py | 9 | ||||
| -rw-r--r-- | tests/conftest.py | 33 | ||||
| -rw-r--r-- | tests/test_client.py | 50 | ||||
| -rw-r--r-- | tests/test_dl.py | 90 | ||||
| -rw-r--r-- | tests/test_file_class.py | 40 | ||||
| -rw-r--r-- | tests/test_logging.py | 4 | ||||
| -rw-r--r-- | tests/test_parse.py | 117 | ||||
| -rw-r--r-- | tests/test_validations.py | 53 | ||||
| -rw-r--r-- | tests/test_wikiget_cli.py | 14 |
10 files changed, 321 insertions, 112 deletions
diff --git a/src/wikiget/validations.py b/src/wikiget/validations.py index ee73b87..18c1f86 100644 --- a/src/wikiget/validations.py +++ b/src/wikiget/validations.py @@ -25,10 +25,11 @@ from wikiget import BLOCKSIZE def valid_file(search_string: str) -> re.Match | None: - """ - Determines if the given string contains a valid file name, defined as a string - ending with a '.' and at least one character, beginning with 'File:' or 'Image:', - the standard file prefixes in MediaWiki. + """Determines if the given string contains a valid file name + + A valid file name is a string that begins with 'File:' or 'Image:' (the standard + file prefixes in MediaWiki), includes a period, and has at least one character + following the period, like 'File:Example.jpg' or 'Image:Example.svg'. :param search_string: string to validate :type search_string: str @@ -42,10 +43,10 @@ def valid_file(search_string: str) -> re.Match | None: def valid_site(search_string: str) -> re.Match | None: - """ - Determines if the given string contains a valid site name, defined as a string - ending with 'wikipedia.org' or 'wikimedia.org'. This covers all subdomains of those - domains. + """Determines if the given string contains a valid site name + + A valid site name is a string ending with 'wikipedia.org' or 'wikimedia.org'. This + covers all subdomains of those domains. Currently unused since any site is accepted as input, and we rely on the user to ensure the site has a compatible API. @@ -60,8 +61,10 @@ def valid_site(search_string: str) -> re.Match | None: def verify_hash(filename: str) -> str: - """ - Calculates the SHA1 hash of the given file for comparison with a known value. + """Calculates the SHA1 hash of the given file for comparison with a known value. + + Despite being insecure, SHA1 is used since that's what the MediaWiki API returns for + the file hash. :param filename: name of the file to calculate a hash for :type filename: str diff --git a/src/wikiget/wikiget.py b/src/wikiget/wikiget.py index 06dc458..ca211af 100644 --- a/src/wikiget/wikiget.py +++ b/src/wikiget/wikiget.py @@ -27,6 +27,13 @@ from wikiget.logging import configure_logging def parse_args(argv: list[str]) -> argparse.Namespace: + """Parse the given argument list. + + :param argv: a list of arguments in string form + :type argv: list[str] + :return: a Namespace containing the arguments and their values + :rtype: argparse.Namespace + """ parser = argparse.ArgumentParser( description=""" A tool for downloading files from MediaWiki sites using the file name or @@ -120,7 +127,7 @@ def parse_args(argv: list[str]) -> argparse.Namespace: def cli() -> None: - # setup our environment + """Set up the command-line environment and start the download process.""" args = parse_args(sys.argv[1:]) configure_logging(verbosity=args.verbose, logfile=args.logfile, quiet=args.quiet) diff --git a/tests/conftest.py b/tests/conftest.py index cda7dd3..5fccfc0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,13 +15,44 @@ # You should have received a copy of the GNU General Public License # along with Wikiget. If not, see <https://www.gnu.org/licenses/>. +"""Define fixtures used across all tests in this folder.""" + import pytest import requests_mock as rm +from wikiget.file import File + + +@pytest.fixture() +def file_with_name() -> File: + """Create a test File with only a filename. + + A File object created with only a name should set its destination property to + the same value and its site property to the program's default site. + + :return: File object created using a filename + :rtype: File + """ + return File("foobar.jpg") + + +@pytest.fixture() +def file_with_name_and_dest() -> File: + """Create a test File with a name and destination. + + :return: File object created with name and dest + :rtype: File + """ + return File(name="foobar.jpg", dest="bazqux.jpg") + @pytest.fixture() def _mock_get(requests_mock: rm.Mocker) -> None: - # fake the download request for File:Example.jpg + """Fake the download request for the true URL of File:Example.jpg. + + :param requests_mock: a requests_mock Mocker object + :type requests_mock: rm.Mocker + """ 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 4cbf702..dae63f5 100644 --- a/tests/test_client.py +++ b/tests/test_client.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/>. +"""Define tests related to the wikiget.client module.""" + import logging from unittest.mock import MagicMock, patch, sentinel @@ -28,14 +30,14 @@ from wikiget.wikiget import parse_args class TestConnectSite: - # this message is logged when the level is at INFO or below + """Define tests related to wikiget.client.connect_to_site.""" + + # this message is logged when the level is at INFO or below; + # defined here for ease of maintenance 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 - name of the site we're connecting to. - """ + """Test that an info log message is created with the name of the site.""" caplog.set_level(logging.INFO) args = parse_args(["File:Example.jpg"]) @@ -47,7 +49,8 @@ class TestConnectSite: ] def test_connect_to_site_with_creds(self, caplog: pytest.LogCaptureFixture) -> None: - """ + """Test that an info log message is created when credentials are used. + If a username and password are provided, connect_to_site should use them to log in to the site. """ @@ -68,9 +71,10 @@ class TestConnectSite: 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. + """Test that the correct log messages are created if ConnectionError is raised. + + In addition to the info-level site connection message, there should be error + and debug level messages with details about the problem. """ caplog.set_level(logging.DEBUG) args = parse_args(["File:Example.jpg"]) @@ -88,9 +92,10 @@ class TestConnectSite: ] def test_connect_to_site_http_error(self, caplog: pytest.LogCaptureFixture) -> None: - """ - The connect_to_site function should log the correct messages if an HTTPError - exception is raised. + """Test that the correct log messages are created if HTTPError is raised. + + In addition to the info-level site connection message, there should be error + and debug level messages with details about the problem. """ caplog.set_level(logging.DEBUG) args = parse_args(["File:Example.jpg"]) @@ -114,9 +119,10 @@ class TestConnectSite: 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. + """Test that log messages are created if other exceptions are raised. + + When an exception other than ConnectionError or HTTPError is raised, an + error-level log message should be created. """ args = parse_args(["File:Example.jpg"]) @@ -130,11 +136,10 @@ class TestConnectSite: class TestQueryApi: + """Define tests related to wikiget.client.query_api.""" + def test_query_api(self) -> None: - """ - The query_api function should return an Image object when given a name and a - valid Site. - """ + """Test that query_api returns the expected Image object.""" # 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 @@ -147,7 +152,8 @@ class TestQueryApi: assert image == sentinel.mock_image def test_query_api_error(self, caplog: pytest.LogCaptureFixture) -> None: - """ + """Test that the correct log messages are created when APIError is raised. + 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. """ @@ -155,6 +161,10 @@ class TestQueryApi: mock_site = MagicMock() mock_site.images = MagicMock() + # Normally, APIError is raised during the processing of the API call that + # creates the site.images attribute. Since we're faking all of that, the + # exception needs to be raised elsewhere, so that it's caught when query_api + # tries to read the items in site.images. mock_site.images.__getitem__.side_effect = APIError( "error code", "error info", "error kwargs" ) diff --git a/tests/test_dl.py b/tests/test_dl.py index c9f26dc..d7d5d77 100644 --- a/tests/test_dl.py +++ b/tests/test_dl.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/>. +"""Define tests related to the wikiget.dl module.""" + import logging from pathlib import Path from unittest.mock import MagicMock, Mock, patch @@ -30,6 +32,8 @@ from wikiget.wikiget import parse_args class TestPrepDownload: + """Define tests related to wikiget.dl.prep_download.""" + @patch("wikiget.dl.query_api") @patch("wikiget.dl.connect_to_site") def test_prep_download( @@ -51,7 +55,8 @@ class TestPrepDownload: assert file == expected_file def test_prep_download_with_existing_file(self, tmp_path: Path) -> None: - """ + """Test that an exception is raised when the download file already exists. + Attempting to download a file with the same destination name as an existing file should raise a FileExistsError. """ @@ -63,9 +68,11 @@ class TestPrepDownload: class TestProcessDownload: + """Define tests related to wikiget.dl.process_download.""" + @patch("wikiget.dl.batch_download") def test_process_batch_download(self, mock_batch_download: MagicMock) -> None: - """A successful batch download should not return any errors.""" + """A successful batch download should have an exit code of zero (no errors).""" mock_batch_download.return_value = 0 args = parse_args(["-a", "batch.txt"]) @@ -77,9 +84,9 @@ class TestProcessDownload: 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. + """A batch download with errors should have a non-zero exit code. + + Additionally, it should create a log message containing the number of errors. """ mock_batch_download.return_value = 4 @@ -94,7 +101,7 @@ class TestProcessDownload: def test_process_single_download( self, mock_download: MagicMock, mock_prep_download: MagicMock ) -> None: - """A successful download should not return any errors.""" + """A successful download should have an exit code of zero (no errors).""" mock_download.return_value = 0 mock_prep_download.return_value = File("Example.jpg") @@ -121,9 +128,7 @@ class TestProcessDownload: 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. - """ + """If ParseError is raised, it should create an error log message.""" mock_prep_download.side_effect = ParseError("error message") args = parse_args(["File:Example.jpg"]) @@ -136,10 +141,7 @@ class TestProcessDownload: 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. - """ + """If FileExistsError is raised, it should create a warning log message.""" mock_prep_download.side_effect = FileExistsError("warning message") args = parse_args(["File:Example.jpg"]) @@ -154,9 +156,7 @@ class TestProcessDownload: def test_process_single_download_other_error( self, mock_prep_download: MagicMock ) -> None: - """ - If process_download catches any other errors, it should return 1. - """ + """If any other errors occur, an exit code of 1 should be returned.""" mock_prep_download.side_effect = requests.ConnectionError args = parse_args(["File:Example.jpg"]) @@ -167,6 +167,8 @@ class TestProcessDownload: class TestBatchDownload: + """Define tests related to wikiget.dl.batch_download.""" + @patch("wikiget.dl.download") @patch("wikiget.dl.prep_download") @patch("wikiget.dl.read_batch_file") @@ -177,6 +179,11 @@ class TestBatchDownload: mock_download: MagicMock, caplog: pytest.LogCaptureFixture, ) -> None: + """Test that no errors are returned for a successful batch download. + + Additionally, a log message should be created for each line in the batch file + and should contain the line number and contents. + """ caplog.set_level(logging.INFO) # set dummy return values for read_batch_file() and download() @@ -198,10 +205,7 @@ class TestBatchDownload: 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. - """ + """Test that an OSError results in an error log message and program exit.""" mock_read_batch_file.side_effect = OSError("error message") args = parse_args(["-a", "batch.txt"]) @@ -221,6 +225,11 @@ class TestBatchDownload: mock_prep_download: MagicMock, caplog: pytest.LogCaptureFixture, ) -> None: + """Test that a warning log message is created if ParseError is raised. + + The resulting log message should contain the relevant line where the problem + ocurred. + """ mock_read_batch_file.return_value = {1: "File:Example.jpg"} mock_prep_download.side_effect = ParseError("warning message") @@ -242,6 +251,7 @@ class TestBatchDownload: mock_prep_download: MagicMock, caplog: pytest.LogCaptureFixture, ) -> None: + """Test that a warning log message is created if the download file exists.""" mock_read_batch_file.return_value = {1: "File:Example.jpg"} mock_prep_download.side_effect = FileExistsError("warning message") @@ -263,6 +273,11 @@ class TestBatchDownload: mock_prep_download: MagicMock, caplog: pytest.LogCaptureFixture, ) -> None: + """Test that a warning log message is created if there are problems downloading. + + The log message should also contain the line number and contents of the line + that caused the error. + """ mock_read_batch_file.return_value = {1: "File:Example.jpg"} mock_prep_download.side_effect = requests.ConnectionError @@ -283,8 +298,17 @@ class TestBatchDownload: @pytest.mark.usefixtures("_mock_get") class TestDownload: + """Define tests related to wikiget.dl.download.""" + @pytest.fixture() def mock_file(self, tmp_path: Path) -> File: + """Create a mock File object to test against. + + :param tmp_path: temporary path object + :type tmp_path: Path + :return: mock File object + :rtype: File + """ file = File(name="Example.jpg", dest=str(tmp_path / "Example.jpg")) file.image = Mock() file.image.exists = True @@ -299,6 +323,12 @@ class TestDownload: return file def test_download(self, mock_file: File, caplog: pytest.LogCaptureFixture) -> None: + """Test that the correct log messages are created when downloading a file. + + There should be a series of info-level messages containing the filename, size, + site name, actual URL, and SHA1 hash, along with a message noting the successful + download. + """ caplog.set_level(logging.INFO) with patch("wikiget.dl.verify_hash") as mock_verify_hash: @@ -339,6 +369,10 @@ class TestDownload: def test_download_with_output( self, mock_file: File, caplog: pytest.LogCaptureFixture ) -> None: + """Test that the correct log messages are created when downloading a file. + + When an output name is specified, the log messages should reflect that. + """ caplog.set_level(logging.INFO) tmp_file = mock_file.dest @@ -364,6 +398,7 @@ class TestDownload: def test_download_dry_run( self, mock_file: File, caplog: pytest.LogCaptureFixture ) -> None: + """Test that a dry run creates a log message saying so.""" caplog.set_level(logging.INFO) args = parse_args(["-n", "File:Example.jpg"]) @@ -378,6 +413,11 @@ class TestDownload: def test_download_os_error( self, mock_file: File, caplog: pytest.LogCaptureFixture ) -> None: + """Test what happens when an OSError is raised during download. + + If the downloaded file cannot be created, an error log message should be created + with details on the exception. + """ with patch("wikiget.dl.Path.open") as mock_open: mock_open.side_effect = OSError("write error") args = parse_args(["File:Example.jpg"]) @@ -395,6 +435,11 @@ class TestDownload: def test_download_verify_os_error( self, mock_file: File, caplog: pytest.LogCaptureFixture ) -> None: + """Test what happens when an OSError is raised during verification. + + If the downloaded file cannot be read in order to calculate its hash, an error + log message should be created with details on the exception. + """ with patch("wikiget.dl.verify_hash") as mock_verify_hash: mock_verify_hash.side_effect = OSError("read error") args = parse_args(["File:Example.jpg"]) @@ -412,6 +457,10 @@ class TestDownload: def test_download_verify_hash_mismatch( self, mock_file: File, caplog: pytest.LogCaptureFixture ) -> None: + """Test what happens when the downloaded file hash and server hash don't match. + + An error log message should be created if there's a hash mismatch. + """ with patch("wikiget.dl.verify_hash") as mock_verify_hash: mock_verify_hash.return_value = "mismatch" args = parse_args(["File:Example.jpg"]) @@ -429,6 +478,7 @@ class TestDownload: def test_download_nonexistent_file( self, mock_file: File, caplog: pytest.LogCaptureFixture ) -> None: + """Test that a warning message is logged if no file info was returned.""" mock_file.image.exists = False args = parse_args(["File:Example.jpg"]) diff --git a/tests/test_file_class.py b/tests/test_file_class.py index ee25f1c..8e68239 100644 --- a/tests/test_file_class.py +++ b/tests/test_file_class.py @@ -15,57 +15,61 @@ # 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 +"""Define tests related to the wikiget.file module.""" from wikiget import DEFAULT_SITE from wikiget.file import File class TestFileClass: - @pytest.fixture() - def file_with_name(self) -> File: - """ - A File object created with only a name should set its destination property to - the same value and its site property to the program's default site. - """ - return File("foobar.jpg") + """Define tests related to wikiget.file.File creation.""" def test_file_with_name(self, file_with_name: File) -> None: + """The file name attribute should equal what was passed in.""" assert file_with_name.name == "foobar.jpg" def test_file_with_name_dest(self, file_with_name: File) -> None: + """The file dest attribute should be the same as the name.""" assert file_with_name.dest == file_with_name.name def test_file_with_name_site(self, file_with_name: File) -> None: + """The file site attribute should equal the default site.""" assert file_with_name.site == DEFAULT_SITE - @pytest.fixture() - def file_with_name_and_dest(self) -> File: - """ - A File object created with a name and destination should set those properties - accordingly; they should not be the same value. - """ - return File("foobar.jpg", dest="bazqux.jpg") - def test_file_with_name_and_dest(self, file_with_name_and_dest: File) -> None: + """The file dest attribute should equal what was passed in.""" assert file_with_name_and_dest.dest == "bazqux.jpg" def test_name_and_dest_are_different(self, file_with_name_and_dest: File) -> None: + """The file name and dest attributes should not be the same.""" assert file_with_name_and_dest.dest != file_with_name_and_dest.name def test_file_with_name_and_site(self) -> None: - """ + """Test the attributes of a File created with a name and site. + A File object created with a name and site should set those properties accordingly and not use the program's default site. """ file = File("foobar.jpg", site="en.wikipedia.org") assert file.site == "en.wikipedia.org" + +class TestFileComparison: + """Define tests related to wikiget.file.File comparisons.""" + def test_file_equality(self, file_with_name: File) -> None: + """Test that two similar Files equal each other.""" assert File(name="foobar.jpg") == file_with_name def test_file_inequality(self, file_with_name: File) -> None: + """Test that two dissimilar Files do not equal each other.""" 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 + """Test what happens when a File is compared with a different object. + + The equality comparison should return NotImplemented when comparing non-Files + with Files. + """ + not_a_file = {"name": "foobar.jpg", "dest": "foobar.jpg"} + assert file_with_name.__eq__(not_a_file) == NotImplemented diff --git a/tests/test_logging.py b/tests/test_logging.py index 5d3aa4c..2fd95cd 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.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/>. +"""Define tests related to the wikiget.logging module.""" + import logging from pathlib import Path @@ -25,6 +27,8 @@ from wikiget.wikiget import parse_args class TestLogging: + """Define tests related to wikiget.logging.configure_logging and FileLogAdapter.""" + logger = logging.getLogger() def test_custom_log_adapter(self, caplog: pytest.LogCaptureFixture) -> None: diff --git a/tests/test_parse.py b/tests/test_parse.py index 7ef182c..fbbd1b7 100644 --- a/tests/test_parse.py +++ b/tests/test_parse.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/>. +"""Define tests related to the wikiget.parse module.""" + from __future__ import annotations import io @@ -34,46 +36,69 @@ if TYPE_CHECKING: class TestGetDest: + """Define tests related to wikiget.parse.get_dest.""" + @pytest.fixture() def file_with_filename(self) -> File: - """ - When a filename is passed to get_dest, it should create a File object with the - correct name and dest and the default site. + """Create a File object with a given filename. + + When only the filename is given as an argument, the dest attribute will be set + to the same value as the filename and the default site will be used. + + :return: a File object created using a filename + :rtype: File """ args = parse_args(["File:Example.jpg"]) return get_dest(args.FILE, args) def test_get_dest_name_with_filename(self, file_with_filename: File) -> None: + """Test that the file's name attribute is set correctly.""" assert file_with_filename.name == "Example.jpg" def test_get_dest_with_filename(self, file_with_filename: File) -> None: + """Test that the file's dest attribute is set correctly. + + Unless otherwise specified, it should match the filename. + """ assert file_with_filename.dest == "Example.jpg" def test_get_dest_site_with_filename(self, file_with_filename: File) -> None: + """Test that the file's site attribute is set correctly. + + Unless otherwise specified, it should be the default site. + """ assert file_with_filename.site == "commons.wikimedia.org" @pytest.fixture() def file_with_url(self) -> File: - """ - When a URL is passed to get_dest, it should create a File object with the - correct name and dest and the site from the URL. + """Create a File object with a given URL. + + When a URL is passed to get_dest, it will create a File object with the + filename and site parsed from the URL. + + :return: a File object created using a URL + :rtype: File """ args = parse_args(["https://en.wikipedia.org/wiki/File:Example.jpg"]) return get_dest(args.FILE, args) def test_get_dest_name_with_url(self, file_with_url: File) -> None: + """Test that the file's name attribute is set correctly.""" assert file_with_url.name == "Example.jpg" def test_get_dest_with_url(self, file_with_url: File) -> None: + """Test that the file's dest attribute is set correctly.""" assert file_with_url.dest == "Example.jpg" def test_get_dest_site_with_url(self, file_with_url: File) -> None: + """Test that the file's site attribute is set correctly. + + The site should be what was parsed from the URL, not the default site. + """ assert file_with_url.site == "en.wikipedia.org" def test_get_dest_with_bad_filename(self) -> None: - """ - The get_dest function should raise a ParseError if the filename is invalid. - """ + """Test that a ParseError exception is raised if the filename is invalid.""" args = parse_args(["Example.jpg"]) with pytest.raises(ParseError): _ = get_dest(args.FILE, args) @@ -81,7 +106,8 @@ class TestGetDest: def test_get_dest_with_different_site( self, caplog: pytest.LogCaptureFixture ) -> None: - """ + """Test that a warning log message is created. + If a URL is passed to get_dest and a site is also given on the command line, the site in the URL should be used and a warning log message created. """ @@ -97,10 +123,16 @@ class TestGetDest: class TestReadBatchFile: + """Define tests related to wikiget.parse.read_batch_file.""" + @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. + """Create and process a test batch file with three lines. + + :param tmp_path: temporary path object + :type tmp_path: Path + :return: dictionary representation of the input file + :rtype: dict[int, str] """ tmp_file = tmp_path / "batch.txt" tmp_file.write_text("File:Foo.jpg\nFile:Bar.jpg\nFile:Baz.jpg\n") @@ -109,7 +141,8 @@ class TestReadBatchFile: def test_batch_file_log( self, caplog: pytest.LogCaptureFixture, tmp_path: Path ) -> None: - """ + """Test that reading a batch file creates an info log message. + Reading in a batch file should create an info log message containing the name of the batch file. """ @@ -120,24 +153,26 @@ class TestReadBatchFile: assert f"Using file '{tmp_file}' for batch download" in caplog.text def test_batch_file_length(self, dl_dict: dict[int, str]) -> None: - """ - The processed batch dict should have the same number of items as lines in the - batch file. - """ + """Test that the batch dict has the same number of lines as the batch file.""" assert len(dl_dict) == 3 def test_batch_file_contents(self, dl_dict: dict[int, str]) -> None: + """Test that the batch dict has the correct line numbers and filenames. + + The processed batch dict should have the batch file's line numbers and filenames + as keys and values, respectively. """ - The processed batch dict should have the correct line numbers and filenames as - keys and values, respectively. - """ - expected_list = {1: "File:Foo.jpg", 2: "File:Bar.jpg", 3: "File:Baz.jpg"} - assert dl_dict == expected_list + expected_dict = {1: "File:Foo.jpg", 2: "File:Bar.jpg", 3: "File:Baz.jpg"} + assert dl_dict == expected_dict @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. + """Pass three lines of filenames from stdin to read_batch_file to create a dict. + + :param monkeypatch: Pytest monkeypatch helper + :type monkeypatch: pytest.MonkeyPatch + :return: dictionary representation of the input + :rtype: dict[int, str] """ monkeypatch.setattr( "sys.stdin", io.StringIO("File:Foo.jpg\nFile:Bar.jpg\nFile:Baz.jpg\n") @@ -147,34 +182,38 @@ class TestReadBatchFile: def test_batch_stdin_log( self, caplog: pytest.LogCaptureFixture, monkeypatch: pytest.MonkeyPatch ) -> None: - """ - Using stdin for batch processing should create an info log message saying so. - """ + """Test that using stdin for batch processing creates an info log message.""" caplog.set_level(logging.INFO) monkeypatch.setattr("sys.stdin", io.StringIO("File:Foo.jpg\n")) _ = read_batch_file("-") assert "Using stdin for batch download" in caplog.text def test_batch_stdin_length(self, dl_dict_stdin: dict[int, str]) -> None: - """ - The processed batch dict should have the same number of items as lines in the - input. + """Test that the batch dict has the correct number of items. + + The dict should contain the same number of items as lines in the input. """ assert len(dl_dict_stdin) == 3 def test_batch_stdin_contents(self, dl_dict_stdin: dict[int, str]) -> None: - """ - The processed batch dict should have the correct line numbers and filenames as - keys and values, respectively. + """Test that the batch dict has the correct keys and values. + + The line numbers and filenames from the input should be the keys and values, + respectively. """ expected_list = {1: "File:Foo.jpg", 2: "File:Bar.jpg", 3: "File:Baz.jpg"} assert dl_dict_stdin == expected_list @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 - commented out and another of which is blank, and return a dictionary. + """Create and process a test batch file with four lines. + + In addition to filenames, one line is commented out and another line is blank. + + :param tmp_path: temporary path object + :type tmp_path: Path + :return: dictionary representation of the input file + :rtype: dict[int, str] """ tmp_file = tmp_path / "batch.txt" tmp_file.write_text("File:Foo.jpg\n\n#File:Bar.jpg\nFile:Baz.jpg\n") @@ -183,7 +222,8 @@ class TestReadBatchFile: def test_batch_file_with_comment_length( self, dl_dict_with_comment: dict[int, str] ) -> None: - """ + """Test the length of the dict created from a file with comments. + The processed batch dict should contain the same number of items as uncommented and non-blank lines in the input. """ @@ -192,7 +232,8 @@ class TestReadBatchFile: def test_batch_file_with_comment_contents( self, dl_dict_with_comment: dict[int, str] ) -> None: - """ + """Test that the batch dict has the correct keys and values. + The processed batch dict should have the correct line numbers and filenames as keys and values, respectively, skipping any commented or blank lines. """ diff --git a/tests/test_validations.py b/tests/test_validations.py index 5263cdc..161d102 100644 --- a/tests/test_validations.py +++ b/tests/test_validations.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/>. +"""Define tests related to the wikiget.validations module.""" + from __future__ import annotations from typing import TYPE_CHECKING @@ -29,6 +31,8 @@ if TYPE_CHECKING: class TestSiteInput: + """Define tests related to wikiget.validations.valid_site.""" + @pytest.fixture( params=[ "example.com", @@ -38,6 +42,13 @@ class TestSiteInput: ], ) def invalid_input(self, request: pytest.FixtureRequest) -> Match | None: + """Return the results of checking various invalid site names. + + :param request: Pytest request object containing parameter values + :type request: pytest.FixtureRequest + :return: a Match object for the site or None if there was no match + :rtype: Match | None + """ return valid_site(request.param) @pytest.fixture( @@ -49,6 +60,13 @@ class TestSiteInput: ], ) def valid_input(self, request: pytest.FixtureRequest) -> Match | None: + """Return the results of checking various valid site names. + + :param request: Pytest request object containing parameter values + :type request: pytest.FixtureRequest + :return: a Match object for the site or None if there was no match + :rtype: Match | None + """ return valid_site(request.param) def test_invalid_site_input(self, invalid_input: None) -> None: @@ -61,28 +79,40 @@ class TestSiteInput: class TestFileRegex: + """Define tests related to the regex matching in wikiget.validations.valid_file.""" + @pytest.fixture() def file_match(self) -> Match | None: - """ - File regex should return a match object with match groups corresponding - to the file prefix and name. + """Return the results of processing a filename. + + The match object returned will have match groups corresponding to the file + prefix and name. + + :return: a Match object for the filename or None if there was no match + :rtype: Match | None """ return valid_file("File:Example.jpg") def test_file_match_exists(self, file_match: Match) -> None: + """Test that a Match object was returned.""" assert file_match is not None def test_file_match_entire_match(self, file_match: Match) -> None: + """Test that the the first match group equals the expected value.""" assert file_match.group(0) == "File:Example.jpg" def test_file_match_first_group(self, file_match: Match) -> None: + """Test that the second match group equals the expected value.""" assert file_match.group(1) == "File:" def test_file_match_second_group(self, file_match: Match) -> None: + """Test that the third match group equals the expected value.""" assert file_match.group(2) == "Example.jpg" class TestFileInput: + """Tests related to wikiget.validations.valid_site.""" + @pytest.fixture( params=[ "file:example", @@ -92,6 +122,13 @@ class TestFileInput: ], ) def invalid_input(self, request: pytest.FixtureRequest) -> Match | None: + """Return the results of checking various invalid filenames. + + :param request: Pytest request object containing parameter values + :type request: pytest.FixtureRequest + :return: a Match object for the filename or None if there was no match + :rtype: Match | None + """ return valid_file(request.param) @pytest.fixture( @@ -105,6 +142,13 @@ class TestFileInput: ], ) def valid_input(self, request: pytest.FixtureRequest) -> Match | None: + """Return the results of checking various valid filenames. + + :param request: Pytest request object containing parameter values + :type request: pytest.FixtureRequest + :return: a Match object for the filename or None if there was no match + :rtype: Match | None + """ return valid_file(request.param) def test_invalid_file_input(self, invalid_input: None) -> None: @@ -117,12 +161,15 @@ class TestFileInput: class TestVerifyHash: + """Define tests related to wikiget.validations.verify_hash.""" + def test_verify_hash(self, tmp_path: Path) -> None: """Confirm that verify_hash returns the proper SHA1 hash.""" file_name = "testfile" file_contents = "foobar" file_sha1 = "8843d7f92416211de9ebb963ff4ce28125932878" + # create a temporary file with known contents tmp_file = tmp_path / file_name tmp_file.write_text(file_contents) diff --git a/tests/test_wikiget_cli.py b/tests/test_wikiget_cli.py index 0306579..c0f09a9 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 <https://www.gnu.org/licenses/>. +"""Define tests related to the wikiget.wikiget module.""" + from unittest.mock import MagicMock, patch import pytest @@ -24,7 +26,10 @@ from wikiget.wikiget import cli class TestCli: - def test_cli_no_params(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Define tests related to wikiget.wikiget.cli.""" + + def test_cli_no_args(self, monkeypatch: pytest.MonkeyPatch) -> None: + """If no arguments are passed, the program should exit with code 2.""" with monkeypatch.context() as m: m.setattr("sys.argv", ["wikiget"]) with pytest.raises(SystemExit) as e: @@ -35,6 +40,7 @@ class TestCli: def test_cli_completed_successfully( self, mock_process_download: MagicMock, monkeypatch: pytest.MonkeyPatch ) -> None: + """If everything is successful, the program should exit with code 0.""" # a successful call to process_download returns 0 mock_process_download.return_value = 0 @@ -49,6 +55,7 @@ class TestCli: def test_cli_completed_with_problems( self, mock_process_download: MagicMock, monkeypatch: pytest.MonkeyPatch ) -> None: + """If there are problems during execution, the exit code should be 1.""" # an unsuccessful call to process_download returns 1 mock_process_download.return_value = 1 @@ -66,6 +73,11 @@ class TestCli: monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, ) -> None: + """When program execution starts, it should create the right log messages. + + There should be an info log record with the program version as well as a debug + record with the program's user agent. + """ # a successful call to process_download returns 0 mock_process_download.return_value = 0 |
