diff --git a/.github/workflows/ci-mac.yml b/.github/workflows/ci-mac.yml index 847a20c..fee7160 100644 --- a/.github/workflows/ci-mac.yml +++ b/.github/workflows/ci-mac.yml @@ -6,7 +6,7 @@ on: jobs: build: - runs-on: macos-13 + runs-on: macos-latest strategy: matrix: @@ -15,10 +15,10 @@ jobs: ] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} diff --git a/.gitignore b/.gitignore index c425004..ef33ba4 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ __pycache__/ # Fuzz testing files. **.mut.** + +.coverage diff --git a/html2pdf4doc/main.py b/html2pdf4doc/main.py index 3da31aa..e681a73 100644 --- a/html2pdf4doc/main.py +++ b/html2pdf4doc/main.py @@ -9,6 +9,7 @@ import sys import zipfile from datetime import datetime +from enum import IntEnum from pathlib import Path from time import sleep, time from typing import Any, Dict, Iterator, List, Optional, Tuple, Union @@ -58,6 +59,18 @@ def extract_page_count(logs: List[Dict[str, str]]) -> int: raise ValueError("No page count found in logs.") +class HPDExitCode(IntEnum): + GENERAL_ERROR = 1 + COULD_NOT_FIND_CHROME = 5 + DID_NOT_RECEIVE_SUCCESS_STATUS_FROM_HTML2PDF4DOC_JS = 6 + + +class HPDError(RuntimeError): + def __init__(self, message: str, exit_code: int): + super().__init__(message) + self.exit_code: int = exit_code + + class IntRange: def __init__(self, imin: int, imax: int) -> None: self.imin: int = imin @@ -87,9 +100,9 @@ def get_chrome_driver(self, path_to_cache_dir: str) -> str: # If Web Driver Manager cannot detect Chrome, it returns None. if chrome_version is None: - raise RuntimeError( - "html2pdf4doc: " - "Web Driver Manager could not detect an existing Chrome installation." + raise HPDError( + "Web Driver Manager could not detect an existing Chrome installation.", + exit_code=HPDExitCode.COULD_NOT_FIND_CHROME, ) chrome_major_version = chrome_version.split(".")[0] @@ -145,15 +158,16 @@ def get_chrome_driver(self, path_to_cache_dir: str) -> str: return path_to_downloaded_chrome_driver - @staticmethod + @classmethod def _download_chromedriver( + cls, chrome_major_version: str, os_type: str, path_to_driver_cache_dir: str, path_to_cached_chrome_driver: str, ) -> str: url = "https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json" - response = ChromeDriverManager.send_http_get_request(url) + response = cls.send_http_get_request(url) if response is None: raise RuntimeError( "Could not download known-good-versions-with-downloads.json" @@ -195,7 +209,7 @@ def _download_chromedriver( print( # noqa: T201 f"html2pdf4doc: downloading ChromeDriver from: {driver_url}" ) - response = ChromeDriverManager.send_http_get_request(driver_url) + response = cls.send_http_get_request(driver_url) if response is None: raise RuntimeError( @@ -360,7 +374,9 @@ def __init__(self, page_count: int): "error: html2pdf4doc: " "could not receive a successful completion status from HTML2PDF4Doc." ) - sys.exit(1) + sys.exit( + HPDExitCode.DID_NOT_RECEIVE_SUCCESS_STATUS_FROM_HTML2PDF4DOC_JS + ) bad_logs: List[Dict[str, str]] = [] @@ -402,6 +418,7 @@ def __init__(self, page_count: int): def create_webdriver( + chrome_driver_manager: ChromeDriverManager, chromedriver_argument: Optional[str], path_to_cache_dir: str, page_load_timeout: int, @@ -411,7 +428,7 @@ def create_webdriver( path_to_chrome_driver: str if chromedriver_argument is None: - path_to_chrome_driver = ChromeDriverManager().get_chrome_driver( + path_to_chrome_driver = chrome_driver_manager.get_chrome_driver( path_to_cache_dir ) else: @@ -475,7 +492,7 @@ def create_webdriver( return driver -def main() -> None: +def _main() -> None: if not os.path.isfile(PATH_TO_HTML2PDF4DOC_JS): raise RuntimeError( f"Corrupted html2pdf4doc package bundle. " @@ -573,13 +590,15 @@ def main() -> None: args = parser.parse_args() + chrome_driver_manager = ChromeDriverManager() + path_to_cache_dir: str if args.command == "get_driver": path_to_cache_dir = ( args.cache_dir if args.cache_dir is not None else DEFAULT_CACHE_DIR ) - path_to_chrome = ChromeDriverManager().get_chrome_driver( + path_to_chrome = chrome_driver_manager.get_chrome_driver( path_to_cache_dir ) print(f"html2pdf4doc: ChromeDriver available at path: {path_to_chrome}") # noqa: T201 @@ -594,6 +613,7 @@ def main() -> None: args.cache_dir if args.cache_dir is not None else DEFAULT_CACHE_DIR ) driver: webdriver.Chrome = create_webdriver( + chrome_driver_manager, args.chromedriver, path_to_cache_dir, page_load_timeout, @@ -645,7 +665,15 @@ def exit_handler() -> None: else: print("html2pdf4doc: unknown command.") # noqa: T201 - sys.exit(1) + sys.exit(HPDExitCode.GENERAL_ERROR) + + +def main() -> None: + try: + _main() + except HPDError as error_: + print("error: html2pdf4doc: " + str(error_), flush=True) # noqa: T201 + sys.exit(error_.exit_code) if __name__ == "__main__": diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..eb843dd --- /dev/null +++ b/mypy.ini @@ -0,0 +1,6 @@ +[mypy] + +# We ignore build folder but mypy still follows into the Python packages. +# The following is a way to skip it. +[mypy-_pytest.*] +follow_imports = skip diff --git a/requirements.development.txt b/requirements.development.txt index 0ed9e4f..85a66f1 100644 --- a/requirements.development.txt +++ b/requirements.development.txt @@ -15,6 +15,7 @@ ruff>=0.9 # Unit tests # pytest +coverage # # Integration tests diff --git a/tasks.py b/tasks.py index f5a33f2..80f264a 100644 --- a/tasks.py +++ b/tasks.py @@ -116,6 +116,7 @@ def lint_ruff_format(context): format *.py html2pdf4doc/ + tests/unit/ tests/integration/ """, ) @@ -131,7 +132,7 @@ def lint_ruff(context): run_invoke( context, """ - ruff check *.py html2pdf4doc/ --fix --cache-dir build/ruff + ruff check *.py html2pdf4doc/ tests/unit/ --fix --cache-dir build/ruff """, ) @@ -144,7 +145,7 @@ def lint_mypy(context): run_invoke( context, """ - mypy html2pdf4doc/ + mypy html2pdf4doc/ tests/unit/ --show-error-codes --disable-error-code=import --disable-error-code=misc @@ -162,6 +163,29 @@ def lint(context): lint_mypy(context) +@task(aliases=["tu"]) +def test_unit(context, focus=None, output=False): + focus_argument = f"-k {focus}" if focus is not None else "" + output_argument = "--capture=no" if output else "" + + cwd = os.getcwd() + + path_to_coverage_file = f"{cwd}/build/coverage/unit/.coverage" + run_invoke( + context, + f""" + coverage run + --data-file={path_to_coverage_file} + -m pytest + {focus_argument} + {output_argument} + -o cache_dir=build/pytest_unit + -o junit_suite_name="HTML2PDF4Doc unit tests" + tests/unit/ + """, + ) + + @task(aliases=["ti"]) def test_integration( context, diff --git a/tests/unit/test_chrome_driver_manager.py b/tests/unit/test_chrome_driver_manager.py new file mode 100644 index 0000000..f370124 --- /dev/null +++ b/tests/unit/test_chrome_driver_manager.py @@ -0,0 +1,27 @@ +import tempfile +from typing import Optional + +import pytest + +from html2pdf4doc.main import ChromeDriverManager, HPDError, HPDExitCode + + +class FailingChromeDriverManager(ChromeDriverManager): + @staticmethod + def get_chrome_version() -> Optional[str]: + return None + + +def test_raises_error_when_cannot_detect_chrome() -> None: + """ + This first unit test is not great but it is a good start anyway. + """ + + chrome_driver_manager = FailingChromeDriverManager() + + with tempfile.TemporaryDirectory() as tmpdir: + with pytest.raises(Exception) as exc_info: + _ = chrome_driver_manager.get_chrome_driver(tmpdir) + + assert exc_info.type is HPDError + assert exc_info.value.exit_code == HPDExitCode.COULD_NOT_FIND_CHROME