Skip to content

Commit d312da5

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

9 files changed

Lines changed: 166 additions & 47 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 html2pdf4doc 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: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ def mutate_and_print(path_to_input_file: str, path_to_root: str) -> bool:
8080
cmd.append(path_to_print_[0])
8181
cmd.append(path_to_print_[1])
8282

83-
relative_path_to_mut_html = Path(path_to_root).relative_to(".")
83+
relative_path_to_mut_html = Path(path_to_mut_html).relative_to(path_to_root)
8484
path_to_mut_output = f"output/{relative_path_to_mut_html}"
8585

8686
def copy_files_if_needed() -> None:
@@ -143,30 +143,16 @@ def copy_mutated_file() -> None:
143143
return True
144144

145145

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-
146+
def fuzz_test(
147+
*, path_to_input_file: str, path_to_root: str, total_mutations: int = 20
148+
) -> None:
162149
shutil.rmtree("output", ignore_errors=True)
163150
Path("output").mkdir(parents=True, exist_ok=True)
164151

165-
total_runs = 200 if args.long else 20
166152
success_count, failure_count = 0, 0
167-
for i in range(1, total_runs + 1):
153+
for i in range(1, total_mutations + 1):
168154
print( # noqa: T201
169-
f"html2pdf4doc_fuzzer print cycle #{i}/{total_runs} — "
155+
f"html2pdf4doc_fuzzer print cycle #{i}/{total_mutations} — "
170156
f"So far: 🟢{success_count} / 🔴{failure_count}",
171157
flush=True,
172158
)
@@ -176,18 +162,44 @@ def main() -> None:
176162
else:
177163
failure_count += 1
178164

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

182168
print( # noqa: T201
183169
f"html2pdf4doc_fuzzer: finished {'✅' if failure_count == 0 else '❌'} — "
184-
f"Success rate: {success_count}/{total_runs} ({success_rate_percent}%)",
170+
f"Success rate: {success_count}/{total_mutations} ({success_rate_percent}%)",
185171
flush=True,
186172
)
187173

188174
if failure_count > 0:
189175
sys.exit(1)
190176

191177

178+
def main() -> None:
179+
parser = argparse.ArgumentParser()
180+
181+
parser.add_argument("input_file", type=str, help="TODO")
182+
parser.add_argument("root_path", type=str, help="TODO")
183+
parser.add_argument(
184+
"--total-mutations",
185+
type=int,
186+
choices=range(1, 1001),
187+
required=True,
188+
help="An integer between 1 and 1000",
189+
)
190+
191+
args = parser.parse_args()
192+
193+
path_to_input_file = args.input_file
194+
path_to_root = args.root_path
195+
total_mutations = args.total_mutations
196+
197+
fuzz_test(
198+
path_to_input_file=path_to_input_file,
199+
path_to_root=path_to_root,
200+
total_mutations=total_mutations,
201+
)
202+
203+
192204
if __name__ == "__main__":
193205
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 & 8 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 html2pdf4doc/main.py get_driver
106106
""",
107107
)
108108

@@ -175,7 +175,7 @@ def test_integration(
175175

176176
cwd = os.getcwd()
177177

178-
html2pdf_exec = f'python3 \\"{cwd}/html2pdf4doc/html2pdf4doc.py\\"'
178+
html2pdf_exec = f'python3 \\"{cwd}/html2pdf4doc/main.py\\"'
179179

180180
focus_or_none = f"--filter {focus}" if focus else ""
181181
debug_opts = "-vv --show-all" if debug else ""
@@ -201,16 +201,37 @@ def test_integration(
201201

202202

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

207226
run_invoke(
208227
context,
209228
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}
229+
pytest
230+
{focus_argument}
231+
{long_argument}
232+
{output_argument}
233+
-o cache_dir=build/tests_fuzz_cache
234+
tests/fuzz/
214235
""",
215236
)
216237

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: 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 %project_root/html2pdf4doc/main_fuzzer.py %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)