Skip to content

Commit 4a79e94

Browse files
committed
tests(fuzz): switch to Pytest for running multiple fuzz tests in a test suite
1 parent 988f34d commit 4a79e94

10 files changed

Lines changed: 169 additions & 53 deletions

File tree

.github/workflows/ci_fuzz_linux.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ jobs:
5555
run: |
5656
if [ "${{ github.event_name }}" = "schedule" ]; then
5757
echo "🕒 Running long fuzzing..."
58-
invoke test-fuzz --long
58+
invoke test-fuzz --total-mutations 25
5959
else
6060
echo "🚀 Running short fuzzing..."
6161
invoke test-fuzz
@@ -66,6 +66,6 @@ jobs:
6666
if: failure() || always()
6767
uses: actions/upload-artifact@v4
6868
with:
69-
name: broken-pdfs
70-
path: output/
69+
name: tests_fuzz_broken_pdfs
70+
path: build/tests_fuzz
7171
retention-days: 30

html2pdf4doc/__init__.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import os
2+
from pathlib import Path
3+
4+
__version__ = "0.0.22"
5+
6+
PATH_TO_HTML2PDF4DOC_PY = os.path.join(
7+
os.path.dirname(os.path.join(__file__)),
8+
"main.py",
9+
)
10+
PATH_TO_HTML2PDF4DOC_JS = os.path.join(
11+
os.path.dirname(os.path.join(__file__)),
12+
"html2pdf4doc_js",
13+
"html2pdf4doc.min.js",
14+
)
15+
16+
DEFAULT_CACHE_DIR = os.path.join(Path.home(), ".html2pdf4doc", "chromedriver")
17+
18+
PATH_TO_CHROME_DRIVER_DEBUG_LOG = "/tmp/chromedriver.log"
Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,13 @@
2121
from selenium.webdriver.chrome.service import Service
2222
from webdriver_manager.core.os_manager import ChromeType, OperationSystemManager
2323

24-
__version__ = "0.0.22"
25-
26-
PATH_TO_HTML2PDF4DOC_PY = __file__
27-
PATH_TO_HTML2PDF4DOC_JS = os.path.join(
28-
os.path.dirname(os.path.join(__file__)),
29-
"html2pdf4doc_js",
30-
"html2pdf4doc.min.js",
24+
from . import (
25+
DEFAULT_CACHE_DIR,
26+
PATH_TO_CHROME_DRIVER_DEBUG_LOG,
27+
PATH_TO_HTML2PDF4DOC_JS,
28+
__version__,
3129
)
3230

33-
DEFAULT_CACHE_DIR = os.path.join(Path.home(), ".html2pdf4doc", "chromedriver")
34-
35-
PATH_TO_CHROME_DRIVER_DEBUG_LOG = "/tmp/chromedriver.log"
36-
3731
# HTML2PDF4Doc.js prints unicode symbols to console. The following makes it work on
3832
# Windows which otherwise complains:
3933
# UnicodeEncodeError: 'charmap' codec can't encode characters in position 129-130: character maps to <undefined>
Lines changed: 37 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@
1313
from faker import Faker
1414
from lxml import etree, html
1515

16-
from html2pdf4doc import PATH_TO_HTML2PDF4DOC_PY
17-
1816

1917
@contextlib.contextmanager
2018
def measure_performance(title: str) -> Iterator[None]:
@@ -71,7 +69,8 @@ def mutate_and_print(path_to_input_file: str, path_to_root: str) -> bool:
7169

7270
cmd: List[str] = [
7371
sys.executable,
74-
PATH_TO_HTML2PDF4DOC_PY,
72+
"-m",
73+
"html2pdf4doc.main",
7574
"print",
7675
"--strict",
7776
]
@@ -80,7 +79,7 @@ def mutate_and_print(path_to_input_file: str, path_to_root: str) -> bool:
8079
cmd.append(path_to_print_[0])
8180
cmd.append(path_to_print_[1])
8281

83-
relative_path_to_mut_html = Path(path_to_root).relative_to(".")
82+
relative_path_to_mut_html = Path(path_to_mut_html).relative_to(path_to_root)
8483
path_to_mut_output = f"output/{relative_path_to_mut_html}"
8584

8685
def copy_files_if_needed() -> None:
@@ -143,30 +142,16 @@ def copy_mutated_file() -> None:
143142
return True
144143

145144

146-
def main() -> None:
147-
parser = argparse.ArgumentParser()
148-
149-
parser.add_argument("input_file", type=str, help="TODO")
150-
parser.add_argument("root_path", type=str, help="TODO")
151-
parser.add_argument(
152-
"--long",
153-
action="store_true",
154-
help="Run the fuzzer in long mode (more iterations).",
155-
)
156-
157-
args = parser.parse_args()
158-
159-
path_to_input_file = args.input_file
160-
path_to_root = args.root_path
161-
145+
def fuzz_test(
146+
*, path_to_input_file: str, path_to_root: str, total_mutations: int = 20
147+
) -> None:
162148
shutil.rmtree("output", ignore_errors=True)
163149
Path("output").mkdir(parents=True, exist_ok=True)
164150

165-
total_runs = 200 if args.long else 20
166151
success_count, failure_count = 0, 0
167-
for i in range(1, total_runs + 1):
152+
for i in range(1, total_mutations + 1):
168153
print( # noqa: T201
169-
f"html2pdf4doc_fuzzer print cycle #{i}/{total_runs} — "
154+
f"html2pdf4doc_fuzzer print cycle #{i}/{total_mutations} — "
170155
f"So far: 🟢{success_count} / 🔴{failure_count}",
171156
flush=True,
172157
)
@@ -176,18 +161,44 @@ def main() -> None:
176161
else:
177162
failure_count += 1
178163

179-
assert total_runs > 0
180-
success_rate_percent = (success_count / total_runs) * 100
164+
assert total_mutations > 0
165+
success_rate_percent = (success_count / total_mutations) * 100
181166

182167
print( # noqa: T201
183168
f"html2pdf4doc_fuzzer: finished {'✅' if failure_count == 0 else '❌'} — "
184-
f"Success rate: {success_count}/{total_runs} ({success_rate_percent}%)",
169+
f"Success rate: {success_count}/{total_mutations} ({success_rate_percent}%)",
185170
flush=True,
186171
)
187172

188173
if failure_count > 0:
189174
sys.exit(1)
190175

191176

177+
def main() -> None:
178+
parser = argparse.ArgumentParser()
179+
180+
parser.add_argument("input_file", type=str, help="TODO")
181+
parser.add_argument("root_path", type=str, help="TODO")
182+
parser.add_argument(
183+
"--total-mutations",
184+
type=int,
185+
choices=range(1, 1001),
186+
required=True,
187+
help="An integer between 1 and 1000",
188+
)
189+
190+
args = parser.parse_args()
191+
192+
path_to_input_file = args.input_file
193+
path_to_root = args.root_path
194+
total_mutations = args.total_mutations
195+
196+
fuzz_test(
197+
path_to_input_file=path_to_input_file,
198+
path_to_root=path_to_root,
199+
total_mutations=total_mutations,
200+
)
201+
202+
192203
if __name__ == "__main__":
193204
main()

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,8 @@ development = [
6969
]
7070

7171
[project.scripts]
72-
html2pdf4doc = "html2pdf4doc.html2pdf4doc:main"
73-
html2pdf4doc_fuzzer = "html2pdf4doc.html2pdf4doc_fuzzer:main"
72+
html2pdf4doc = "html2pdf4doc.main:main"
73+
html2pdf4doc_fuzzer = "html2pdf4doc.main_fuzzer:main"
7474

7575
[project.urls]
7676
Changelog = "https://github.com/mettta/html2pdf_python/releases/"

tasks.py

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ def get_chrome_driver(
102102
run_invoke(
103103
context,
104104
"""
105-
python html2pdf4doc/html2pdf4doc.py get_driver
105+
python -m html2pdf4doc.main get_driver
106106
""",
107107
)
108108

@@ -173,9 +173,7 @@ def test_integration(
173173

174174
get_chrome_driver(context)
175175

176-
cwd = os.getcwd()
177-
178-
html2pdf_exec = f'python3 \\"{cwd}/html2pdf4doc/html2pdf4doc.py\\"'
176+
html2pdf_exec = "python3 -m html2pdf4doc.main"
179177

180178
focus_or_none = f"--filter {focus}" if focus else ""
181179
debug_opts = "-vv --show-all" if debug else ""
@@ -201,16 +199,37 @@ def test_integration(
201199

202200

203201
@task(aliases=["tf"])
204-
def test_fuzz(context, long: bool = False):
205-
arg_long = "--long" if long else ""
202+
def test_fuzz(context, focus=None, total_mutations: int = 10, output=False):
203+
"""
204+
@relation(SDOC-SRS-44, scope=function)
205+
"""
206+
207+
test_reports_dir = "build/test_reports"
208+
209+
Path(test_reports_dir).mkdir(parents=True, exist_ok=True)
210+
211+
focus_argument = f"-k {focus}" if focus is not None else ""
212+
long_argument = (
213+
f"--fuzz-total-mutations {total_mutations}" if total_mutations else ""
214+
)
215+
output_argument = "--capture=no" if output else ""
216+
217+
run_invoke(
218+
context,
219+
"""
220+
rm -rf build/tests_fuzz
221+
""",
222+
)
206223

207224
run_invoke(
208225
context,
209226
f"""
210-
python html2pdf4doc/html2pdf4doc_fuzzer.py
211-
tests/fuzz/01_strictdoc_guide_202510/strictdoc/docs/strictdoc_01_user_guide-PDF.html
212-
tests/fuzz/01_strictdoc_guide_202510/
213-
{arg_long}
227+
pytest
228+
{focus_argument}
229+
{long_argument}
230+
{output_argument}
231+
-o cache_dir=build/tests_fuzz_cache
232+
tests/fuzz/
214233
""",
215234
)
216235

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import os
2+
3+
from html2pdf4doc.main_fuzzer import fuzz_test
4+
from tests.fuzz.conftest import create_build_folder, FuzzConfig
5+
6+
PATH_TO_THIS_FOLDER = os.path.dirname(__file__)
7+
8+
def test(fuzz_config: FuzzConfig):
9+
build_folder = create_build_folder(PATH_TO_THIS_FOLDER)
10+
11+
fuzz_test(
12+
path_to_input_file=os.path.join(
13+
build_folder,
14+
"strictdoc/docs/strictdoc_01_user_guide-PDF.html"
15+
),
16+
path_to_root=build_folder,
17+
total_mutations=fuzz_config.total_mutations
18+
)

tests/fuzz/conftest.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import os
2+
import shutil
3+
from dataclasses import dataclass
4+
from pathlib import Path
5+
6+
import pytest
7+
8+
PATH_TO_TESTS_FUZZ_FOLDER = os.path.dirname(__file__)
9+
10+
11+
@dataclass
12+
class FuzzConfig:
13+
total_mutations: bool
14+
15+
16+
def pytest_addoption(parser):
17+
parser.addoption(
18+
"--fuzz-total-mutations",
19+
action="store",
20+
type=int,
21+
choices=range(1, 1001),
22+
default=10,
23+
help="Total number of mutations to perform (1-1000)"
24+
)
25+
26+
@pytest.fixture
27+
def fuzz_config(request):
28+
return FuzzConfig(total_mutations=request.config.getoption("--fuzz-total-mutations"))
29+
30+
31+
def create_build_folder(test_folder: str) -> str:
32+
assert os.path.isdir(test_folder), test_folder
33+
assert os.path.isabs(test_folder), test_folder
34+
35+
relative_path_to_test_folder = Path(test_folder).relative_to(PATH_TO_TESTS_FUZZ_FOLDER)
36+
37+
# IMPORTANT: The number of nested folders matches the number of nesting
38+
# in the tests/fuzz/* test folders. Otherwise, the html2pdf4doc.js
39+
# will not be found in either of tests/fuzz/* or build/tests_fuzz/*.
40+
build_folder = os.path.join(
41+
"build",
42+
"tests_fuzz",
43+
relative_path_to_test_folder
44+
)
45+
46+
shutil.copytree(test_folder, build_folder)
47+
48+
return build_folder
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
RUN: %expect_exit 2 %html2pdf print --page-load-timeout 1000000 %S/index1.html %S/Output/index1.pdf 2>&1 | filecheck %s
22

3-
CHECK: html2pdf4doc.py print: error: argument --page-load-timeout: Must be an integer in the range [0, 600].
3+
CHECK: main.py print: error: argument --page-load-timeout: Must be an integer in the range [0, 600].
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# This test verifies that the main_fuzzer.py script works as a standalone
2+
# command-line program.
3+
4+
RUN: mkdir -p %project_root/build/tests_integration_fuzz/
5+
RUN: cp -rv %project_root/tests/fuzz/01_strictdoc_guide_202510 %project_root/build/tests_integration_fuzz/
6+
7+
RUN: PYTHONPATH=%project_root python -m html2pdf4doc.main_fuzzer %project_root/build/tests_integration_fuzz/01_strictdoc_guide_202510/strictdoc/docs/strictdoc_01_user_guide-PDF.html %project_root/build/tests_integration_fuzz/01_strictdoc_guide_202510 --total-mutations 1 | filecheck %s --dump-input=fail
8+
CHECK: html2pdf4doc_fuzzer: finished ✅ — Success rate: 1/1 (100.0%)

0 commit comments

Comments
 (0)