Skip to content

Commit 439f0f5

Browse files
authored
Modernize the interestingness tests (#92)
* Modernize the interestingness tests * Re-add module docstrings * Remove license from __init__.py * Raise parser error if command wasn't specified * Change to relative import * Re-add interesting text to log messages * Explicitly check list length * Add each test argument as a new list element
1 parent a0bf3a4 commit 439f0f5

File tree

11 files changed

+329
-198
lines changed

11 files changed

+329
-198
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ disable = [
3434
"duplicate-code",
3535
"fixme",
3636
"import-error",
37+
"logging-fstring-interpolation",
3738
"subprocess-run-check",
3839
"too-few-public-methods",
3940
"too-many-arguments",

setup.cfg

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ name = lithium-reducer
2525
url = https://github.com/MozillaSecurity/lithium
2626

2727
[options]
28+
install_requires =
29+
ffpuppet~=0.11.2
2830
package_dir =
2931
= src
3032
packages =

src/lithium/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
#
21
# This Source Code Form is subject to the terms of the Mozilla Public
32
# License, v. 2.0. If a copy of the MPL was not distributed with this
43
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +0,0 @@
1-
#
2-
# This Source Code Form is subject to the terms of the Mozilla Public
3-
# License, v. 2.0. If a copy of the MPL was not distributed with this
4-
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
5-
"""lithium built-in interestingness tests"""
6-
7-
from . import crashes, diff_test, hangs, outputs, repeat, timed_run, utils
Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
#
21
# This Source Code Form is subject to the terms of the Mozilla Public
32
# License, v. 2.0. If a copy of the MPL was not distributed with this
43
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
@@ -10,13 +9,19 @@
109
"""
1110

1211
import logging
13-
from typing import List
12+
import sys
13+
from typing import List, Optional
1414

15-
from . import timed_run
15+
from .timed_run import BaseParser, ExitStatus, timed_run
1616

17+
LOG = logging.getLogger(__name__)
1718

18-
def interesting(cli_args: List[str], temp_prefix: str) -> bool:
19-
"""Interesting if the binary causes a crash. (e.g. SIGKILL/SIGTERM/SIGTRAP etc.)
19+
20+
def interesting(
21+
cli_args: Optional[List[str]] = None,
22+
temp_prefix: Optional[str] = None,
23+
) -> bool:
24+
"""Interesting if the binary causes a crash.
2025
2126
Args:
2227
cli_args: List of input arguments.
@@ -25,20 +30,21 @@ def interesting(cli_args: List[str], temp_prefix: str) -> bool:
2530
Returns:
2631
True if binary crashes, False otherwise.
2732
"""
28-
parser = timed_run.ArgumentParser(
29-
prog="crashes",
30-
usage="python -m lithium %(prog)s [options] binary [flags] testcase.ext",
31-
)
33+
parser = BaseParser()
3234
args = parser.parse_args(cli_args)
35+
if not args.cmd_with_flags:
36+
parser.error("Must specify command to evaluate.")
3337

34-
log = logging.getLogger(__name__)
35-
# Run the program with desired flags and look out for crashes.
36-
runinfo = timed_run.timed_run(args.cmd_with_flags, args.timeout, temp_prefix)
38+
run_info = timed_run(args.cmd_with_flags, args.timeout, temp_prefix)
3739

38-
time_str = f" ({runinfo.elapsedtime:.3f} seconds)"
39-
if runinfo.sta == timed_run.CRASHED:
40-
log.info("Exit status: " + runinfo.msg + time_str)
40+
if run_info.status == ExitStatus.CRASH:
41+
LOG.info(f"[Interesting] Crash detected ({run_info.elapsed:.3f}s)")
4142
return True
4243

43-
log.info("[Uninteresting] It didn't crash: " + runinfo.msg + time_str)
44+
LOG.info(f"[Uninteresting] No crash detected ({run_info.elapsed:.3f}s)")
4445
return False
46+
47+
48+
if __name__ == "__main__":
49+
logging.basicConfig(format="%(message)s", level=logging.INFO)
50+
sys.exit(interesting())
Lines changed: 86 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
#
21
# This Source Code Form is subject to the terms of the Mozilla Public
32
# License, v. 2.0. If a copy of the MPL was not distributed with this
43
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
@@ -8,89 +7,118 @@
87
used to isolate and minimize differential behaviour test cases.
98
109
Example:
11-
python -m lithium diff_test -a "--fuzzing-safe" \
12-
-b "--fuzzing-safe --wasm-always-baseline" <binary> <testcase>
13-
14-
Example with autobisectjs, split into separate lines here for readability:
15-
python -u -m funfuzz.autobisectjs.autobisectjs \
16-
-b "--enable-debug --enable-more-deterministic" -p testcase.js \
17-
-i diff_test -a "--fuzzing-safe --no-threads --ion-eager" \
18-
-b "--fuzzing-safe --no-threads --ion-eager --no-wasm-baseline"
10+
python -m lithium diff_test \
11+
-a "--fuzzing-safe" \
12+
-b "--fuzzing-safe --wasm-always-baseline" \
13+
<binary> <testcase>
1914
"""
20-
21-
# This file came from nbp's GitHub PR #2 for adding new Lithium reduction strategies.
22-
# https://github.com/MozillaSecurity/lithium/pull/2
23-
15+
import argparse
2416
import filecmp
2517
import logging
26-
from typing import List
18+
import sys
19+
from typing import List, Optional, Union
2720

28-
from . import timed_run
21+
from .timed_run import BaseParser, ExitStatus, timed_run
2922

23+
LOG = logging.getLogger(__name__)
3024

31-
def interesting(cli_args: List[str], temp_prefix: str) -> bool:
32-
"""Interesting if the binary shows a difference in output when different command
33-
line arguments are passed in.
25+
26+
def parse_args(argv: Optional[List[str]] = None) -> argparse.Namespace:
27+
"""Parse args
3428
3529
Args:
36-
cli_args: List of input arguments.
37-
temp_prefix: Temporary directory prefix, e.g. tmp1/1 or tmp4/1
30+
argv: List of input arguments.
3831
3932
Returns:
40-
True if a difference in output appears, False otherwise.
33+
Parsed arguments
4134
"""
42-
parser = timed_run.ArgumentParser(
35+
parser = BaseParser(
4336
prog="diff_test",
44-
usage="python -m lithium %(prog)s [options] binary testcase.ext",
37+
usage="python -m lithium.interestingness.diff "
38+
"-a '--fuzzing-safe' -b='' binary testcase.js",
4539
)
4640
parser.add_argument(
4741
"-a",
48-
"--a-args",
4942
dest="a_args",
5043
help="Set of extra arguments given to first run.",
44+
required=True,
5145
)
5246
parser.add_argument(
5347
"-b",
54-
"--b-args",
5548
dest="b_args",
5649
help="Set of extra arguments given to second run.",
50+
required=True,
5751
)
58-
args = parser.parse_args(cli_args)
5952

60-
a_runinfo = timed_run.timed_run(
61-
args.cmd_with_flags[:1] + args.a_args.split() + args.cmd_with_flags[1:],
62-
args.timeout,
63-
temp_prefix + "-a",
64-
)
65-
b_runinfo = timed_run.timed_run(
66-
args.cmd_with_flags[:1] + args.b_args.split() + args.cmd_with_flags[1:],
67-
args.timeout,
68-
temp_prefix + "-b",
69-
)
70-
log = logging.getLogger(__name__)
71-
time_str = (
72-
f"(1st Run: {a_runinfo.elapsedtime:.3f} seconds)"
73-
f" (2nd Run: {b_runinfo.elapsedtime:.3f} seconds)"
74-
)
53+
args = parser.parse_args(argv)
54+
if not args.cmd_with_flags:
55+
parser.error("Must specify command to evaluate.")
7556

76-
if timed_run.TIMED_OUT not in (a_runinfo.sta, b_runinfo.sta):
77-
if a_runinfo.return_code != b_runinfo.return_code:
78-
log.info(
79-
"[Interesting] Different return code (%d, %d). %s",
80-
a_runinfo.return_code,
81-
b_runinfo.return_code,
82-
time_str,
83-
)
84-
return True
85-
if not filecmp.cmp(a_runinfo.out, b_runinfo.out):
86-
log.info("[Interesting] Different output. %s", time_str)
87-
return True
88-
if not filecmp.cmp(a_runinfo.err, b_runinfo.err):
89-
log.info("[Interesting] Different error output. %s", time_str)
57+
return args
58+
59+
60+
def interesting(
61+
cli_args: Optional[List[str]] = None,
62+
temp_prefix: Optional[str] = None,
63+
) -> bool:
64+
"""Check if there's a difference in output or return code with different args.
65+
66+
Args:
67+
cli_args: Input arguments.
68+
temp_prefix: Temporary directory prefix, e.g. tmp1/1.
69+
70+
Returns:
71+
True if a difference in output appears, False otherwise.
72+
"""
73+
args = parse_args(cli_args)
74+
75+
binary = args.cmd_with_flags[:1]
76+
testcase = args.cmd_with_flags[1:]
77+
78+
# Run with arguments set A
79+
command_a = binary + args.a_args.split() + testcase
80+
log_prefix_a = f"{temp_prefix}-a" if temp_prefix else None
81+
a_run = timed_run(command_a, args.timeout, log_prefix_a)
82+
if a_run.status == ExitStatus.TIMEOUT:
83+
LOG.warning("Command A timed out!")
84+
85+
# Run with arguments set B
86+
command_b = binary + args.b_args.split() + testcase
87+
log_prefix_b = f"{temp_prefix}-b" if temp_prefix else None
88+
b_run = timed_run(command_b, args.timeout, log_prefix_b)
89+
if b_run.status == ExitStatus.TIMEOUT:
90+
LOG.warning("Command B timed out!")
91+
92+
# Compare return codes
93+
a_ret = a_run.return_code
94+
b_ret = b_run.return_code
95+
if a_ret != b_ret:
96+
LOG.info(f"[Interesting] Different return codes: {a_ret} vs {b_ret}")
97+
return True
98+
99+
# Compare outputs
100+
def cmp_out(
101+
a_data: Union[str, bytes],
102+
b_run: Union[str, bytes],
103+
is_file: bool = False,
104+
) -> bool:
105+
if is_file:
106+
return not filecmp.cmp(a_data, b_run)
107+
return a_data != b_run
108+
109+
if temp_prefix:
110+
if cmp_out(a_run.out, b_run.out, True) or cmp_out(a_run.err, b_run.err, True):
111+
LOG.info("[Interesting] Differences in output detected")
90112
return True
91113
else:
92-
log.info("[Uninteresting] At least one test timed out. %s", time_str)
93-
return False
114+
if cmp_out(a_run.out, b_run.out) or cmp_out(a_run.err, b_run.err):
115+
LOG.info("[Interesting] Differences in output detected")
116+
return True
94117

95-
log.info("[Uninteresting] Identical behaviour. %s", time_str)
118+
LOG.info("[Uninteresting] No differences detected")
96119
return False
120+
121+
122+
if __name__ == "__main__":
123+
logging.basicConfig(format="%(message)s", level=logging.INFO)
124+
sys.exit(interesting())

src/lithium/interestingness/hangs.py

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
#
21
# This Source Code Form is subject to the terms of the Mozilla Public
32
# License, v. 2.0. If a copy of the MPL was not distributed with this
43
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
@@ -10,12 +9,18 @@
109
"""
1110

1211
import logging
13-
from typing import List
12+
import sys
13+
from typing import List, Optional
1414

15-
from . import timed_run
15+
from .timed_run import BaseParser, ExitStatus, timed_run
1616

17+
LOG = logging.getLogger(__name__)
1718

18-
def interesting(cli_args: List[str], temp_prefix: str) -> bool:
19+
20+
def interesting(
21+
cli_args: Optional[List[str]] = None,
22+
temp_prefix: Optional[str] = None,
23+
) -> bool:
1924
"""Interesting if the binary causes a hang.
2025
2126
Args:
@@ -25,18 +30,20 @@ def interesting(cli_args: List[str], temp_prefix: str) -> bool:
2530
Returns:
2631
True if binary causes a hang, False otherwise.
2732
"""
28-
parser = timed_run.ArgumentParser(
29-
prog="hangs",
30-
usage="python -m lithium %(prog)s [options] binary [flags] testcase.ext",
31-
)
33+
parser = BaseParser()
3234
args = parser.parse_args(cli_args)
35+
if not args.cmd_with_flags:
36+
parser.error("Must specify command to evaluate.")
3337

34-
log = logging.getLogger(__name__)
35-
runinfo = timed_run.timed_run(args.cmd_with_flags, args.timeout, temp_prefix)
36-
37-
if runinfo.sta == timed_run.TIMED_OUT:
38-
log.info("Timed out after %.3f seconds", args.timeout)
38+
run_info = timed_run(args.cmd_with_flags, args.timeout, temp_prefix)
39+
if run_info.status == ExitStatus.TIMEOUT:
40+
LOG.info(f"[Interesting] Timeout detected ({args.timeout:.3f}s)")
3941
return True
4042

41-
log.info("Exited in %.3f seconds", runinfo.elapsedtime)
43+
LOG.info(f"[Uninteresting] Program exited ({run_info.elapsed:.3f}s)")
4244
return False
45+
46+
47+
if __name__ == "__main__":
48+
logging.basicConfig(format="%(message)s", level=logging.INFO)
49+
sys.exit(interesting())

0 commit comments

Comments
 (0)