| 1 | :- module(fuzzing,[fuzz/3,fuzz/4,fuzz/5, | |
| 2 | reproduce_test/4,reproduce_test/5, | |
| 3 | generate/2, | |
| 4 | error_occurred/0]). | |
| 5 | ||
| 6 | :- use_module(library(lists)). | |
| 7 | :- use_module(library(timeout),[time_out/3]). | |
| 8 | :- use_module(library(random),[getrand/1,setrand/1]). | |
| 9 | :- use_module(library(file_systems),[file_members_of_directory/3]). | |
| 10 | ||
| 11 | %% generate(+Type, -Value). | |
| 12 | % | |
| 13 | % True if Value is a randomly generated Prolog term of Type. | |
| 14 | % For a list of available types see files in 'extensions/prolog_fuzzer/types'. | |
| 15 | :- multifile generate/2. | |
| 16 | :- multifile shrink/3. | |
| 17 | ||
| 18 | %% error_occurred. | |
| 19 | % | |
| 20 | % True if fuzzer has detected an errorneous Prolog predicate. | |
| 21 | :- dynamic error_occurred/0. | |
| 22 | ||
| 23 | include_type_definition(_-FullPath) :- consult(FullPath). | |
| 24 | ||
| 25 | % set true to use extension to generate B ASTs | |
| 26 | b_extension(true). | |
| 27 | ||
| 28 | include_b_extension :- | |
| 29 | b_extension(true) , | |
| 30 | file_members_of_directory('./types/b_extension/','*.pl',FileList), | |
| 31 | maplist(include_type_definition,FileList). | |
| 32 | include_b_extension :- | |
| 33 | b_extension(false). | |
| 34 | ||
| 35 | :- file_members_of_directory('./types/','*.pl',FileList), | |
| 36 | maplist(include_type_definition,FileList) , | |
| 37 | include_b_extension. | |
| 38 | ||
| 39 | :- meta_predicate fuzz(1,+,+). | |
| 40 | :- meta_predicate fuzz(1,+,+,+). | |
| 41 | :- meta_predicate fuzz(1,+,+,+,+). | |
| 42 | ||
| 43 | %% fuzz(+Module:Predicate, +Arity, +Arguments). | |
| 44 | % | |
| 45 | % Generate 20000 random tests for a predicate with given argument types. | |
| 46 | fuzz(Module:Predicate,Arity,Arguments) :- | |
| 47 | fuzz(Module:Predicate,Arity,20000,Arguments). | |
| 48 | ||
| 49 | %% fuzz(+Module:Predicate, +Arity, +Testcount, +Arguments). | |
| 50 | % | |
| 51 | % Same as fuzz/2 but using a custom amount of tests. | |
| 52 | fuzz(Module:Predicate,Arity,Testcount,Arguments) :- | |
| 53 | fuzz(Module:Predicate,Arity,Testcount,5000,Arguments). | |
| 54 | ||
| 55 | %% fuzz(+Module:Predicate, +Arity, +Testcount, +Arguments). | |
| 56 | % | |
| 57 | % Same as fuzz/3 but using a custom timeout in milliseconds. | |
| 58 | fuzz(Module:Predicate,Arity,Testcount,Timeout,Arguments) :- | |
| 59 | is_valid_timeout(Timeout), | |
| 60 | (current_predicate(Module:Predicate/Arity) | |
| 61 | -> true | |
| 62 | ; error_process(existence_error,Predicate,_,_,_) , fail) , | |
| 63 | % split arguments by ':' | |
| 64 | get_types(Arguments,Types) , | |
| 65 | length(Types,Arity) , | |
| 66 | getrand(Seed) , | |
| 67 | format('Start fuzzing predicate ~w/~w~n',[Predicate,Arity]) , | |
| 68 | format('First state is ~w~n',[Seed]) , | |
| 69 | % run randomized tests | |
| 70 | (run_tests(Predicate,Types,Module,Testcount,Timeout,Result) | |
| 71 | -> fuzz_aux(Result) | |
| 72 | ; % run tests failed, error in the code | |
| 73 | error_process(generation_error,Predicate,_,Types,_)). | |
| 74 | fuzz(_:Predicate,_,_,_,_) :- | |
| 75 | error_process(not_enough_arguments,Predicate,_,_,_). | |
| 76 | ||
| 77 | fuzz_aux(true) :- | |
| 78 | format('~nAll tests passed~n',[]). | |
| 79 | fuzz_aux(_). | |
| 80 | ||
| 81 | is_valid_timeout(Timeout) :- | |
| 82 | integer(Timeout), | |
| 83 | Timeout > 0. | |
| 84 | ||
| 85 | :- meta_predicate reproduce_test(1,+,+,+). | |
| 86 | :- meta_predicate reproduce_test(1,+,+,+,+). | |
| 87 | ||
| 88 | %% reproduce_test(+Module:Predicate, +Arity, +Arguments, +Seed). | |
| 89 | % | |
| 90 | % Reproduce test case from fuzzing by using a custom seed. | |
| 91 | reproduce_test(Module:Predicate,Arity,Arguments,Seed) :- | |
| 92 | reproduce_test(Module:Predicate,Arity,2500,Arguments,Seed). | |
| 93 | ||
| 94 | %% reproduce_test(+Module:Predicate, +Arity, +Timeout, +Arguments, +Seed). | |
| 95 | % | |
| 96 | % Same as reproduce_test/3 but using a custom timeout in milliseconds. | |
| 97 | reproduce_test(Module:Predicate,Arity,Timeout,Arguments,Seed) :- | |
| 98 | is_valid_timeout(Timeout), | |
| 99 | get_types(Arguments,Types) , | |
| 100 | length(Types,Arity) , | |
| 101 | format('Start fuzzing predicate ~w/~w for given seed~n',[Predicate,Arity]) , | |
| 102 | setrand(Seed) , | |
| 103 | % run single test | |
| 104 | (run_tests(Predicate,Types,Module,1,Timeout,Result) | |
| 105 | -> reproduce_test_aux(Result,Seed) | |
| 106 | ; % run tests failed | |
| 107 | error_process(generation_error,Predicate,_,Types,_)). | |
| 108 | reproduce_test(_,_,Timeout,_,_) :- | |
| 109 | \+is_valid_timeout(Timeout), | |
| 110 | !, | |
| 111 | format("Invalid timeout.~n",[]). | |
| 112 | reproduce_test(_:Predicate,_,_,_,_) :- | |
| 113 | error_process(not_enough_arguments,Predicate,_,_,_). | |
| 114 | ||
| 115 | reproduce_test_aux(true,Seed) :- | |
| 116 | format('Test passed for seed ~w~n',[Seed]). | |
| 117 | % pass, because error has already been printed in run_tests | |
| 118 | reproduce_test_aux(_,_). | |
| 119 | ||
| 120 | run_tests(_,_,_,0,_,true). | |
| 121 | % Execute predicate with randomly generated arguments. | |
| 122 | run_tests(Predicate,Types,Module,Testcount,Timeout,Result) :- | |
| 123 | getrand(Seed) , | |
| 124 | random_arguments(Types,Values) , | |
| 125 | Term =.. [Predicate|Values] , | |
| 126 | call_term(Module,Term,Timeout,Error) , | |
| 127 | % write '.' every thousandth testcase | |
| 128 | (0 is mod(Testcount,1000) | |
| 129 | -> write('.') | |
| 130 | ; true) , | |
| 131 | run_tests_aux(Predicate,Types,Module,Testcount,Timeout,Seed,Values,Error,Result). | |
| 132 | ||
| 133 | run_tests_aux(Predicate,Types,Module,Testcount,Timeout,_Seed,_Values,Error,Result) :- | |
| 134 | % go on with testing if no error detected | |
| 135 | Error = none , | |
| 136 | NTestcount is Testcount - 1 , | |
| 137 | run_tests(Predicate,Types,Module,NTestcount,Timeout,Result). | |
| 138 | run_tests_aux(Predicate,Types,Module,_Testcount,_Timeout,Seed,Values,Error,Result) :- | |
| 139 | Error \= none , | |
| 140 | Result = false , | |
| 141 | % try shrinking arguments and print error | |
| 142 | format('~nError detected, shrink arguments~n',[]) , | |
| 143 | % don't print input from mutation(Input:Type) for user readability | |
| 144 | minimum_typelist(Types,NTypes) , | |
| 145 | shrink_values(Predicate,Module,Types,Values,Shrunken) , nl , | |
| 146 | assert(error_occurred) , | |
| 147 | error_process(Error,Predicate,Shrunken,NTypes,Seed) , nl. | |
| 148 | ||
| 149 | % call_term(+Module,+Term,+Timeout,-Error). | |
| 150 | % | |
| 151 | % Calls a term within its given module with error and timeout exception. | |
| 152 | call_term(Module,Term,Timeout,Error) :- | |
| 153 | Timeout1 is Timeout + 10000, | |
| 154 | time_out(on_exception(_,Module:call(Term),fail),Timeout1,Result) , | |
| 155 | call_term_aux(Result,Error). | |
| 156 | call_term(_Module,_Timeout,_Term,Error) :- | |
| 157 | % predicate failed | |
| 158 | Error = false. | |
| 159 | ||
| 160 | call_term_aux(success,none). | |
| 161 | call_term_aux(_,timeout). | |
| 162 | ||
| 163 | % shrink arguments | |
| 164 | shrink_values(Predicate,Module,Types,Values,Result) :- | |
| 165 | % write sth at every shrinking step | |
| 166 | write('.') , | |
| 167 | maplist(shrink_arguments,Types,Values,Shrunken) , | |
| 168 | % termination condition | |
| 169 | Values \= Shrunken , | |
| 170 | Term =.. [Predicate|Shrunken] , | |
| 171 | % catch timeout and error exception | |
| 172 | \+ time_out(on_exception(_,Module:call(Term),fail),1000,success) , | |
| 173 | shrink_values(Predicate,Module,Types,Shrunken,Result). | |
| 174 | shrink_values(_,_,_,Result,Result). | |
| 175 | ||
| 176 | shrink_arguments(Type,Value,Shrunken) :- | |
| 177 | shrink(Type,Value,Shrunken). | |
| 178 | shrink_arguments(_,Value,Value). | |
| 179 | ||
| 180 | % convert types divided by ':' to a list | |
| 181 | get_types(Type:T,[Type|NT]) :- | |
| 182 | get_types(T,NT) , !. | |
| 183 | get_types(Type,[Type]). | |
| 184 | ||
| 185 | % generate random arguments from a list of types | |
| 186 | random_arguments([],[]). | |
| 187 | random_arguments([Type|T1],[Value|T2]) :- | |
| 188 | generate(Type,Value) , | |
| 189 | random_arguments(T1,T2). | |
| 190 | ||
| 191 | % make typelist readable, i.e. don't print input from mutation(Input:Type) | |
| 192 | minimum_typelist([],[]). | |
| 193 | minimum_typelist([mutation(_:Type)|T],[mutation(Type)|NT]) :- ! , | |
| 194 | minimum_typelist(T,NT). | |
| 195 | minimum_typelist([Type|T],[Type|NT]) :- | |
| 196 | minimum_typelist(T,NT). | |
| 197 | ||
| 198 | % error prints | |
| 199 | error_process(existence_error,Predicate,_,_,_) :- | |
| 200 | format('Predicate ~q does not exist.~n',[Predicate]). | |
| 201 | error_process(not_enough_arguments,Predicate,_,_,_) :- | |
| 202 | format('Wrong amount of arguments for predicate ~q~n',[Predicate]). | |
| 203 | error_process(error(type_error(_,_),_),Predicate,_,Types,_) :- | |
| 204 | format('Wrong type of arguments in predicate ~q of type ~q~n',[Predicate,Types]). | |
| 205 | error_process(generation_error,_,_,Types,_) :- % error in generate/2 | |
| 206 | format('Either the type is not defined or there is an implementation error in a prolog file for a type of ~q~n',[Types]). | |
| 207 | error_process(timeout,Predicate,Values,Types,Seed) :- | |
| 208 | length(Types,Arity) , | |
| 209 | format('Timeout in ~q/~w for input ~q of type ~q~nSeed for reproducing test case: ~w~n',[Predicate,Arity,Values,Types,Seed]). | |
| 210 | error_process(false,Predicate,Values,Types,Seed) :- | |
| 211 | length(Types,Arity) , | |
| 212 | format('Predicate ~q/~w false for input ~q of type ~q~nSeed for reproducing test case: ~w~n',[Predicate,Arity,Values,Types,Seed]). |