1/* Part of SWI-Prolog 2 3 Author: Jan Wielemaker 4 E-mail: J.Wielemaker@vu.nl 5 WWW: https://www.swi-prolog.org 6 Copyright (c) 2006-2022, University of Amsterdam 7 VU University Amsterdam 8 SWI-Prolog Solutions b.v. 9 All rights reserved. 10 11 Redistribution and use in source and binary forms, with or without 12 modification, are permitted provided that the following conditions 13 are met: 14 15 1. Redistributions of source code must retain the above copyright 16 notice, this list of conditions and the following disclaimer. 17 18 2. Redistributions in binary form must reproduce the above copyright 19 notice, this list of conditions and the following disclaimer in 20 the documentation and/or other materials provided with the 21 distribution. 22 23 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 26 FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 27 COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 28 INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 29 BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 30 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 31 CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 32 LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 33 ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 34 POSSIBILITY OF SUCH DAMAGE. 35*/ 36 37:- module(prolog_cover, 38 [ show_coverage/1, % :Goal 39 show_coverage/2 % :Goal, +Modules 40 ]). 41:- autoload(library(apply), [exclude/3, maplist/2, convlist/3]). 42:- autoload(library(ordsets), 43 [ord_intersect/2, ord_intersection/3, ord_subtract/3]). 44:- autoload(library(pairs), [group_pairs_by_key/2]). 45:- autoload(library(ansi_term), [ansi_format/3]). 46:- autoload(library(filesex), [directory_file_path/3, make_directory_path/1]). 47:- autoload(library(lists), [append/3]). 48:- autoload(library(option), [option/2, option/3]). 49:- autoload(library(readutil), [read_line_to_string/2]). 50:- use_module(prolog_breakpoints, []). 51 52:- set_prolog_flag(generate_debug_info, false).
82:- meta_predicate
83 show_coverage( ),
84 show_coverage( , ).
ext
or dir
option are
specified.### | Clause was never executed. |
++N | Clause was entered N times and always succeeded |
--N | Clause was entered N times and never succeeded |
+N-M | Clause has succeeded N times and failed M times |
+N*M | Clause was entered N times and succeeded M times |
All call sites are annotated using the same conventions,
except that ---
is used to annotate subgoals that were
never called.
true
(default), add line numbers to the annotated file.true
.126show_coverage(Goal) :- 127 show_coverage(Goal, []). 128show_coverage(Goal, Modules) :- 129 maplist(atom, Modules), 130 !, 131 show_coverage(Goal, [modules(Modules)]). 132show_coverage(Goal, Options) :- 133 clean_output(Options), 134 setup_call_cleanup( 135 '$cov_start', 136 once(Goal), 137 cleanup_trace(Options)). 138 139cleanup_trace(Options) :- 140 '$cov_stop', 141 covered(Succeeded, Failed), 142 ( report_hook(Succeeded, Failed) 143 -> true 144 ; file_coverage(Succeeded, Failed, Options) 145 ), 146 '$cov_reset'.
152covered(Succeeded, Failed) :- 153 findall(Cl, ('$cov_data'(clause(Cl), Enter, 0), Enter > 0), Failed0), 154 findall(Cl, ('$cov_data'(clause(Cl), _, Exit), Exit > 0), Succeeded0), 155 sort(Failed0, Failed), 156 sort(Succeeded0, Succeeded). 157 158 159 /******************************* 160 * REPORTING * 161 *******************************/
169file_coverage(Succeeded, Failed, Options) :- 170 format('~N~n~`=t~78|~n'), 171 format('~tCoverage by File~t~78|~n'), 172 format('~`=t~78|~n'), 173 format('~w~t~w~64|~t~w~72|~t~w~78|~n', 174 ['File', 'Clauses', '%Cov', '%Fail']), 175 format('~`=t~78|~n'), 176 forall(source_file(File), 177 file_coverage(File, Succeeded, Failed, Options)), 178 format('~`=t~78|~n'). 179 180file_coverage(File, Succeeded, Failed, Options) :- 181 findall(Cl, clause_source(Cl, File, _), Clauses), 182 sort(Clauses, All), 183 ( ord_intersect(All, Succeeded) 184 -> true 185 ; ord_intersect(All, Failed) 186 ), % Clauses from this file are touched 187 !, 188 ord_intersection(All, Failed, FailedInFile), 189 ord_intersection(All, Succeeded, SucceededInFile), 190 ord_subtract(All, SucceededInFile, UnCov1), 191 ord_subtract(UnCov1, FailedInFile, Uncovered), 192 193 clean_set(All, All_wo_system), 194 clean_set(Uncovered, Uncovered_wo_system), 195 clean_set(FailedInFile, Failed_wo_system), 196 197 length(All_wo_system, AC), 198 length(Uncovered_wo_system, UC), 199 length(Failed_wo_system, FC), 200 201 CP is 100-100*UC/AC, 202 FCP is 100*FC/AC, 203 summary(File, 56, SFile), 204 format('~w~t ~D~64| ~t~1f~72| ~t~1f~78|~n', [SFile, AC, CP, FCP]), 205 ( list_details(File, Options), 206 clean_set(SucceededInFile, Succeeded_wo_system), 207 ord_union(Failed_wo_system, Succeeded_wo_system, Covered) 208 -> detailed_report(Uncovered_wo_system, Covered, File, Options) 209 ; true 210 ). 211file_coverage(_,_,_,_). 212 213clean_set(Clauses, UserClauses) :- 214 exclude(is_pldoc, Clauses, Clauses_wo_pldoc), 215 exclude(is_system_clause, Clauses_wo_pldoc, UserClauses). 216 217is_system_clause(Clause) :- 218 clause_pi(Clause, Name), 219 Name = system:_. 220 221is_pldoc(Clause) :- 222 clause_pi(Clause, _Module:Name2/_Arity), 223 pldoc_predicate(Name2). 224 225pldoc_predicate('$pldoc'). 226pldoc_predicate('$mode'). 227pldoc_predicate('$pred_option'). 228pldoc_predicate('$exported_op'). % not really PlDoc ... 229 230summary(String, MaxLen, Summary) :- 231 string_length(String, Len), 232 ( Len < MaxLen 233 -> Summary = String 234 ; SLen is MaxLen - 5, 235 sub_string(String, _, SLen, 0, End), 236 string_concat('...', End, Summary) 237 ).
243clause_source(Clause, File, Line) :- 244 nonvar(Clause), 245 !, 246 clause_property(Clause, file(File)), 247 clause_property(Clause, line_count(Line)). 248clause_source(Clause, File, Line) :- 249 Pred = _:_, 250 source_file(Pred, File), 251 \+ predicate_property(Pred, multifile), 252 nth_clause(Pred, _Index, Clause), 253 clause_property(Clause, line_count(Line)). 254clause_source(Clause, File, Line) :- 255 Pred = _:_, 256 predicate_property(Pred, multifile), 257 nth_clause(Pred, _Index, Clause), 258 clause_property(Clause, file(File)), 259 clause_property(Clause, line_count(Line)).
263list_details(File, Options) :- 264 option(modules(Modules), Options), 265 source_file_property(File, module(M)), 266 memberchk(M, Modules), 267 !. 268list_details(File, Options) :- 269 ( source_file_property(File, module(M)), 270 module_property(M, class(user)) 271 -> true 272 ; forall(source_file_property(File, module(M)), 273 module_property(M, class(test))) 274 ), 275 annotate_file(Options). 276 277annotate_file(Options) :- 278 ( option(annotate(true), Options) 279 ; option(dir(_), Options) 280 ; option(ext(_), Options) 281 ), 282 !.
289detailed_report(Uncovered, Covered, File, Options):- 290 annotate_file(Options), 291 !, 292 convlist(line_annotation(File, uncovered), Uncovered, Annot1), 293 convlist(line_annotation(File, covered), Covered, Annot20), 294 flatten(Annot20, Annot2), 295 append(Annot1, Annot2, AnnotationsLen), 296 pairs_keys_values(AnnotationsLen, Annotations, Lens), 297 max_list(Lens, MaxLen), 298 Margin is MaxLen+1, 299 annotate_file(File, Annotations, [margin(Margin)|Options]). 300detailed_report(Uncovered, _, File, _Options):- 301 convlist(uncovered_clause_line(File), Uncovered, Pairs), 302 sort(Pairs, Pairs_sorted), 303 group_pairs_by_key(Pairs_sorted, Compact_pairs), 304 nl, 305 file_base_name(File, Base), 306 format('~2|Clauses not covered from file ~p~n', [Base]), 307 format('~4|Predicate ~59|Clauses at lines ~n', []), 308 maplist(print_clause_line, Compact_pairs), 309 nl. 310 311line_annotation(File, uncovered, Clause, Annotation) :- 312 !, 313 clause_property(Clause, file(File)), 314 clause_property(Clause, line_count(Line)), 315 Annotation = (Line-ansi(error,###))-3. 316line_annotation(File, covered, Clause, [(Line-Annot)-Len|CallSites]) :- 317 clause_property(Clause, file(File)), 318 clause_property(Clause, line_count(Line)), 319 '$cov_data'(clause(Clause), Entered, Exited), 320 counts_annotation(Entered, Exited, Annot, Len), 321 findall(((CSLine-CSAnnot)-CSLen)-PC, 322 clause_call_site_annotation(Clause, PC, CSLine, CSAnnot, CSLen), 323 CallSitesPC), 324 pairs_keys_values(CallSitesPC, CallSites, PCs), 325 check_covered_call_sites(Clause, PCs). 326 327counts_annotation(Entered, Exited, Annot, Len) :- 328 ( Exited == Entered 329 -> format(string(Text), '++~D', [Entered]), 330 Annot = ansi(comment, Text) 331 ; Exited == 0 332 -> format(string(Text), '--~D', [Entered]), 333 Annot = ansi(warning, Text) 334 ; Exited < Entered 335 -> Failed is Entered - Exited, 336 format(string(Text), '+~D-~D', [Exited, Failed]), 337 Annot = ansi(comment, Text) 338 ; format(string(Text), '+~D*~D', [Entered, Exited]), 339 Annot = ansi(fg(cyan), Text) 340 ), 341 string_length(Text, Len). 342 343uncovered_clause_line(File, Clause, Name-Line) :- 344 clause_property(Clause, file(File)), 345 clause_pi(Clause, Name), 346 clause_property(Clause, line_count(Line)).
352clause_pi(Clause, Name) :- 353 clause(Module:, _, Clause), 354 functor(Head,F,A), 355 Name=Module:F/A. 356 357print_clause_line((Module:Name/Arity)-Lines):- 358 term_string(Module:Name, Complete_name), 359 summary(Complete_name, 54, SName), 360 format('~4|~w~t~59|~p~n', [SName/Arity, Lines]). 361 362 363 /******************************* 364 * LINE LEVEL CALL SITES * 365 *******************************/ 366 367clause_call_site_annotation(ClauseRef, NextPC, Line, Annot, Len) :- 368 clause_call_site(ClauseRef, PC-NextPC, Line:_LPos), 369 ( '$cov_data'(call_site(ClauseRef, NextPC, _PI), Entered, Exited) 370 -> counts_annotation(Entered, Exited, Annot, Len) 371 ; '$fetch_vm'(ClauseRef, PC, _, VMI), 372 \+ no_annotate_call_site(VMI) 373 -> Annot = ansi(error, ---), 374 Len = 3 375 ). 376 377no_annotate_call_site(i_enter). 378no_annotate_call_site(i_exit). 379no_annotate_call_site(i_cut). 380 381 382clause_call_site(ClauseRef, PC-NextPC, Pos) :- 383 clause_info(ClauseRef, File, TermPos, _NameOffset), 384 '$break_pc'(ClauseRef, PC, NextPC), 385 '$clause_term_position'(ClauseRef, NextPC, List), 386 catch(prolog_breakpoints:range(List, TermPos, SubPos), E, true), 387 ( var(E) 388 -> arg(1, SubPos, A), 389 file_offset_pos(File, A, Pos) 390 ; print_message(warning, coverage(clause_info(ClauseRef))), 391 fail 392 ). 393 394file_offset_pos(File, A, Line:LPos) :- 395 file_text(File, String), 396 State = start(1, 0), 397 call_nth(sub_string(String, S, _, _, "\n"), NLine), 398 ( S >= A 399 -> !, 400 State = start(Line, SLine), 401 LPos is A-SLine 402 ; NS is S+1, 403 NLine1 is NLine+1, 404 nb_setarg(1, State, NLine1), 405 nb_setarg(2, State, NS), 406 fail 407 ). 408 409file_text(File, String) :- 410 setup_call_cleanup( 411 open(File, read, In), 412 read_string(In, _, String), 413 close(In)). 414 415check_covered_call_sites(Clause, Reported) :- 416 findall(PC, ('$cov_data'(call_site(Clause,PC,_), Enter, _), Enter > 0), Seen), 417 sort(Reported, SReported), 418 sort(Seen, SSeen), 419 ord_subtract(SSeen, SReported, Missed), 420 ( Missed == [] 421 -> true 422 ; print_message(warning, coverage(unreported_call_sites(Clause, Missed))) 423 ). 424 425 426 /******************************* 427 * ANNOTATE * 428 *******************************/ 429 430clean_output(Options) :- 431 option(dir(Dir), Options), 432 !, 433 option(ext(Ext), Options, cov), 434 format(atom(Pattern), '~w/*.~w', [Dir, Ext]), 435 expand_file_name(Pattern, Files), 436 maplist(delete_file, Files). 437clean_output(Options) :- 438 forall(source_file(File), 439 clean_output(File, Options)). 440 441clean_output(File, Options) :- 442 option(ext(Ext), Options, cov), 443 file_name_extension(File, Ext, CovFile), 444 ( exists_file(CovFile) 445 -> E = error(_,_), 446 catch(delete_file(CovFile), E, 447 print_message(warning, E)) 448 ; true 449 ).
LineNo-Annotation
, where Annotation is atomic or a term
Format-Args, optionally embedded in ansi(Code, Annotation)
.458annotate_file(Source, Annotations, Options) :- 459 option(ext(Ext), Options, cov), 460 ( option(dir(Dir), Options) 461 -> file_base_name(Source, Base), 462 file_name_extension(Base, Ext, CovFile), 463 directory_file_path(Dir, CovFile, CovPath), 464 make_directory_path(Dir) 465 ; file_name_extension(Source, Ext, CovPath) 466 ), 467 keysort(Annotations, SortedAnnotations), 468 setup_call_cleanup( 469 open(Source, read, In), 470 setup_call_cleanup( 471 open(CovPath, write, Out), 472 annotate(In, Out, SortedAnnotations, Options), 473 close(Out)), 474 close(In)). 475 476annotate(In, Out, Annotations, Options) :- 477 ( option(color(true), Options, true) 478 -> set_stream(Out, tty(true)) 479 ; true 480 ), 481 annotate(In, Out, Annotations, 0, Options). 482 483annotate(In, Out, Annotations, LineNo0, Options) :- 484 read_line_to_string(In, Line), 485 ( Line == end_of_file 486 -> true 487 ; succ(LineNo0, LineNo), 488 margins(LMargin, CMargin, Options), 489 line_no(LineNo, Out, LMargin), 490 annotations(LineNo, Out, LMargin, Annotations, Annotations1), 491 format(Out, '~t~*|~s~n', [CMargin, Line]), 492 annotate(In, Out, Annotations1, LineNo, Options) 493 ). 494 495annotations(Line, Out, LMargin, [Line-Annot|T0], T) :- 496 !, 497 write_annotation(Out, Annot), 498 ( T0 = [Line-_|_] 499 -> with_output_to(Out, ansi_format(bold, ' \u2bb0~n~t~*|', [LMargin])), 500 annotations(Line, Out, LMargin, T0, T) 501 ; T = T0 502 ). 503annotations(_, _, _, Annots, Annots). 504 505write_annotation(Out, ansi(Code, Fmt-Args)) => 506 with_output_to(Out, ansi_format(Code, Fmt, Args)). 507write_annotation(Out, ansi(Code, Fmt)) => 508 with_output_to(Out, ansi_format(Code, Fmt, [])). 509write_annotation(Out, Fmt-Args) => 510 format(Out, Fmt, Args). 511write_annotation(Out, Fmt) => 512 format(Out, Fmt, []). 513 514line_no(_, _, 0) :- !. 515line_no(Line, Out, LMargin) :- 516 with_output_to(Out, ansi_format(fg(127,127,127), '~t~d ~*|', 517 [Line, LMargin])). 518 519margins(LMargin, Margin, Options) :- 520 option(line_numbers(true), Options, true), 521 !, 522 option(line_number_margin(LMargin), Options, 6), 523 option(margin(AMargin), Options, 4), 524 Margin is LMargin+AMargin. 525margins(0, Margin, Options) :- 526 option(margin(Margin), Options, 4).
540:- multifile 541 report_hook/2. 542 543 544 /******************************* 545 * MESSAGES * 546 *******************************/ 547 548:- multifile 549 prolog:message//1. 550 551prologmessage(coverage(clause_info(ClauseRef))) --> 552 [ 'Inconsistent clause info for '-[] ], 553 clause_msg(ClauseRef). 554prologmessage(coverage(unreported_call_sites(ClauseRef, PCList))) --> 555 [ 'Failed to report call sites for '-[] ], 556 clause_msg(ClauseRef), 557 [ nl, ' Missed at these PC offsets: ~p'-[PCList] ]. 558 559clause_msg(ClauseRef) --> 560 { clause_pi(ClauseRef, PI), 561 clause_property(ClauseRef, file(File)), 562 clause_property(ClauseRef, line_count(Line)) 563 }, 564 [ '~p at'-[PI], nl, ' ', url(File:Line) ]
Clause coverage analysis
The purpose of this module is to find which part of the program has been used by a certain goal. Usage is defined in terms of clauses for which the head unification succeeded. For each clause we count how often it succeeded and how often it failed. In addition we track all call sites, creating goal-by-goal annotated clauses.
This module relies on the SWI-Prolog tracer hooks. It modifies these hooks and collects the results, after which it restores the debugging environment. This has some limitations:
The result is represented as a list of clause-references. As the references to clauses of dynamic predicates cannot be guaranteed, these are omitted from the result.