Skip to content

Commit 81594b9

Browse files
committed
Generalize fuzz testing tools (#379)
This rearrangement allows us to fuzz test the hooks (which use the low level parser API) as well as the high level parser API.
1 parent 76839e3 commit 81594b9

File tree

4 files changed

+121
-37
lines changed

4 files changed

+121
-37
lines changed

Project.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ julia = "1.0"
99
[deps]
1010

1111
[extras]
12+
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
1213
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
1314

1415
[targets]
15-
test = ["Test"]
16+
test = ["Test", "Logging"]

test/diagnostics.jl

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -227,12 +227,11 @@ end
227227
\e[90m# └┘ ── \e[0;0m\e[91minvalid operator\e[0;0m"""
228228

229229
if Sys.isunix()
230-
mktempdir() do tempdirname
231-
cd(tempdirname) do
232-
rm(tempdirname)
233-
# Test _file_url doesn't fail with nonexistant directories
234-
@test isnothing(JuliaSyntax._file_url(joinpath("__nonexistant__", "test.jl")))
235-
end
230+
tempdirname = mktempdir()
231+
cd(tempdirname) do
232+
rm(tempdirname)
233+
# Test _file_url doesn't fail with nonexistant directories
234+
@test isnothing(JuliaSyntax._file_url(joinpath("__nonexistant__", "test.jl")))
236235
end
237236
end
238237
end

test/fuzz_test.jl

Lines changed: 66 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using JuliaSyntax
22
using JuliaSyntax: tokenize
3+
import Logging
4+
import Test
35

46
# Parser fuzz testing tools.
57

@@ -758,6 +760,7 @@ const cutdown_tokens = [
758760
"\t"
759761
"\n"
760762
"x"
763+
"β"
761764
"@"
762765
","
763766
";"
@@ -884,33 +887,36 @@ const cutdown_tokens = [
884887
]
885888

886889
#-------------------------------------------------------------------------------
887-
888-
# The parser should never throw an exception. To test whether this is true,
889-
# try passing randomly generated bad input data into it.
890-
function _fuzz_test(bad_input_iter)
891-
error_strings = []
892-
for str in bad_input_iter
893-
try
894-
JuliaSyntax.parseall(JuliaSyntax.SyntaxNode, str, ignore_errors=true);
895-
catch exc
896-
!(exc isa InterruptException) || rethrow()
897-
rstr = reduce_text(str, parser_throws_exception)
898-
@error "Parser threw exception" rstr exception=current_exceptions()
899-
push!(error_strings, rstr)
900-
end
890+
# Parsing functions for use with fuzz_test
891+
892+
function try_parseall_failure(str)
893+
try
894+
JuliaSyntax.parseall(JuliaSyntax.SyntaxNode, str, ignore_errors=true);
895+
return nothing
896+
catch exc
897+
!(exc isa InterruptException) || rethrow()
898+
rstr = reduce_text(str, parser_throws_exception)
899+
@error "Parser threw exception" rstr exception=current_exceptions()
900+
return rstr
901901
end
902-
return error_strings
903902
end
904903

905-
"""
906-
Fuzz test parser against all tuples of length `N` with elements taken from
907-
`tokens`.
908-
"""
909-
function fuzz_tokens(tokens, N)
910-
iter = (join(ts) for ts in Iterators.product([tokens for _ in 1:N]...))
911-
_fuzz_test(iter)
904+
function try_hook_failure(str)
905+
try
906+
test_logger = Test.TestLogger()
907+
Logging.with_logger(test_logger) do
908+
Meta_parseall(str)
909+
end
910+
if !isempty(test_logger.logs)
911+
return str
912+
end
913+
catch exc
914+
return str
915+
end
916+
return nothing
912917
end
913918

919+
#-------------------------------------------------------------------------------
914920
"""Delete `nlines` adjacent lines from code, at `niters` randomly chosen positions"""
915921
function delete_lines(lines, nlines, niters)
916922
selection = trues(length(lines))
@@ -953,29 +959,59 @@ function delete_tokens(code, tokens, ntokens, niters)
953959
end
954960

955961
#-------------------------------------------------------------------------------
956-
# Fuzzer functions
962+
# Generators for "potentially bad input"
963+
964+
"""
965+
Fuzz test parser against all tuples of length `N` with elements taken from
966+
`tokens`.
967+
"""
968+
function product_token_fuzz(tokens, N)
969+
(join(ts) for ts in Iterators.product([tokens for _ in 1:N]...))
970+
end
957971

958972
"""
959973
Fuzz test parser against randomly generated binary strings
960974
"""
961-
function fuzz_binary(nbytes, N)
962-
bad_strs = _fuzz_test(String(rand(UInt8, nbytes)) for _ in 1:N)
963-
reduce_text.(bad_strs, parser_throws_exception)
975+
function random_binary_fuzz(nbytes, N)
976+
(String(rand(UInt8, nbytes)) for _ in 1:N)
964977
end
965978

966979
"""
967980
Fuzz test by deleting random lines of some given source `code`
968981
"""
969-
function fuzz_lines(code, N; nlines=10, niters=10)
982+
function deleted_line_fuzz(code, N; nlines=10, niters=10)
970983
lines = split(code, '\n')
971-
_fuzz_test(delete_lines(lines, nlines, niters) for _=1:N)
984+
(delete_lines(lines, nlines, niters) for _=1:N)
972985
end
973986

974987
"""
975988
Fuzz test by deleting random tokens from given source `code`
976989
"""
977-
function fuzz_tokens(code, N; ntokens=10, niters=10)
990+
function deleted_token_fuzz(code, N; ntokens=10, niters=10)
978991
ts = tokenize(code)
979-
_fuzz_test(delete_tokens(code, ts, ntokens, niters) for _=1:N)
992+
(delete_tokens(code, ts, ntokens, niters) for _=1:N)
980993
end
981994

995+
"""
996+
Fuzz test a parsing function by trying it with many "bad" input strings.
997+
998+
`try_parsefail` should return `nothing` when the parser succeeds, and return a
999+
string (or reduced string) when parsing succeeds.
1000+
"""
1001+
function fuzz_test(try_parsefail::Function, bad_input_iter)
1002+
error_strings = []
1003+
for str in bad_input_iter
1004+
res = try_parsefail(str)
1005+
if !isnothing(res)
1006+
push!(error_strings, res)
1007+
end
1008+
end
1009+
return error_strings
1010+
end
1011+
1012+
1013+
# Examples
1014+
#
1015+
# fuzz_test(try_hook_failure, product_token_fuzz(cutdown_tokens, 2))
1016+
# fuzz_test(try_parseall_failure, product_token_fuzz(cutdown_tokens, 2))
1017+

test/test_utils.jl

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,3 +422,51 @@ function parse_sexpr(code)
422422
end
423423

424424

425+
#-------------------------------------------------------------------------------
426+
# Tools copied from Base.Meta which call core_parser_hook as if called by
427+
# Meta.parse(), but without installing the global hook.
428+
429+
function _Meta_parse_string(text::AbstractString, filename::AbstractString,
430+
lineno::Integer, index::Integer, options)
431+
if index < 1 || index > ncodeunits(text) + 1
432+
throw(BoundsError(text, index))
433+
end
434+
ex, offset::Int = JuliaSyntax.core_parser_hook(text, filename, lineno, index-1, options)
435+
ex, offset+1
436+
end
437+
438+
function Meta_parse(str::AbstractString, pos::Integer;
439+
filename="none", greedy::Bool=true, raise::Bool=true, depwarn::Bool=true)
440+
ex, pos = _Meta_parse_string(str, String(filename), 1, pos, greedy ? :statement : :atom)
441+
if raise && Meta.isexpr(ex, :error)
442+
err = ex.args[1]
443+
if err isa String
444+
err = Meta.ParseError(err) # For flisp parser
445+
end
446+
throw(err)
447+
end
448+
return ex, pos
449+
end
450+
451+
function Meta_parse(str::AbstractString;
452+
filename="none", raise::Bool=true, depwarn::Bool=true)
453+
ex, pos = Meta_parse(str, 1; filename=filename, greedy=true, raise=raise, depwarn=depwarn)
454+
if Meta.isexpr(ex, :error)
455+
return ex
456+
end
457+
if pos <= ncodeunits(str)
458+
raise && throw(Meta.ParseError("extra token after end of expression"))
459+
return Expr(:error, "extra token after end of expression")
460+
end
461+
return ex
462+
end
463+
464+
function Meta_parseatom(text::AbstractString, pos::Integer; filename="none", lineno=1)
465+
return _Meta_parse_string(text, String(filename), lineno, pos, :atom)
466+
end
467+
468+
function Meta_parseall(text::AbstractString; filename="none", lineno=1)
469+
ex,_ = _Meta_parse_string(text, String(filename), lineno, 1, :all)
470+
return ex
471+
end
472+

0 commit comments

Comments
 (0)