View source with raw comments or as raw
    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).

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.

bug
- Relies heavily on SWI-Prolog internals. We have considered using a meta-interpreter for this purpose, but it is nearly impossible to do 100% complete meta-interpretation of Prolog. Example problem areas include handling cuts in control-structures and calls from non-interpreted meta-predicates. */
   82:- meta_predicate
   83    show_coverage(0),
   84    show_coverage(0,+).
 show_coverage(:Goal) is semidet
 show_coverage(:Goal, +Options) is semidet
show_coverage(:Goal, +Modules:list(atom)) is semidet
Report on coverage by Goal. Goal is executed as in once/1. Options processed:
modules(+Modules)
Provide a detailed report on Modules. For backwards compatibility this is the same as providing a list of modules in the second argument.
annotate(+Bool)
Create an annotated file for the detailed results. This is implied if the ext or dir option are specified.
ext(+Ext)
Extension to use for the annotated file. Default is `.cov`.
dir(+Dir)
Dump the annotations in the given directory. If not given, the annotated files are created in the same directory as the source file. Each clause that is related to a physical line in the file is annotated with one of:
###Clause was never executed.
++NClause was entered N times and always succeeded
--NClause was entered N times and never succeeded
+N-MClause has succeeded N times and failed M times
+N*MClause 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.

line_numbers(Boolean)
If true (default), add line numbers to the annotated file.
color(Boolean)
Controls using ANSI escape sequences to color the output in the annotated source. Default is 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'.
 covered(-Succeeded, -Failed) is det
Collect failed and succeeded clauses.
  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                 *******************************/
 file_coverage(+Succeeded, +Failed, +Options) is det
Write a report on the clauses covered organised by file to current output. Show detailed information about the non-coverered clauses defined in the modules Modules.
  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    ).
 clause_source(+Clause, -File, -Line) is semidet
clause_source(-Clause, +File, -Line) is semidet
  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)).
 list_details(+File, +Options) is semidet
  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    !.
 detailed_report(+Uncovered, +Covered, +File:atom, +Options) is det
Arguments:
Uncovered- is a list of uncovered clauses
Covered- is a list of covered clauses
  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)).
 clause_pi(+Clause, -Name) is det
Return the clause predicate indicator as Module:Name/Arity.
  352clause_pi(Clause, Name) :-
  353    clause(Module:Head, _, 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    ).
 annotate_file(+File, +Annotations, +Options) is det
Create an annotated copy of File. Annotations is a list of 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).
 report_hook(+Succeeded, +Failed) is semidet
This hook is called after the data collection. It is passed a list of objects that have succeeded as well as a list of objects that have failed. The objects are one of
ClauseRef
The specified clause
call_site(ClauseRef, PC, PI)
A call was make in ClauseRef at the given program counter to the predicate indicated by PI.
  540:- multifile
  541    report_hook/2.  542
  543
  544		 /*******************************
  545		 *             MESSAGES		*
  546		 *******************************/
  547
  548:- multifile
  549    prolog:message//1.  550
  551prolog:message(coverage(clause_info(ClauseRef))) -->
  552    [ 'Inconsistent clause info for '-[] ],
  553    clause_msg(ClauseRef).
  554prolog:message(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) ]