:- module(test_differential, []).

%% The enumeration differential as a test. With every profile in force,
%% Hornguard must never admit a predicate SWI's own sandbox refuses, and
%% every sandbox-safe predicate Hornguard refuses must be explained by a
%% pin or by the generator's exclusion table. A new engine version that
%% changes a builtin's character shows up here first.

:- use_module(library(plunit)).
:- use_module(library(lists)).
:- use_module(library(apply)).
:- use_module('../prolog/hornguard').
:- use_module('../tools/differential').
:- use_module('../tools/gen_swi_profiles').

:- dynamic report/1.

ensure_report :-
    (   report(_)
    ->  true
    ;   all_profiles(Ps),
        differential_report(Ps, R),
        assertz(report(R))
    ).

of_kind(Kind, K-_-_) :- K == Kind.

explained(N/A) :- hornguard:hg_pinned(_, N/A), !.
explained(Ind)  :- hornguard_gen_swi_profiles:exclude(Ind, _), !.

:- begin_tests(differential).

test(no_hole_candidates, [true(Holes == [])]) :-
    ensure_report, report(R),
    include(of_kind(hole_candidate), R, Holes).

%   A sandbox-safe predicate Hornguard refuses with no pin and no exclusion
%   to explain it. On the attested engine that is the generator and the
%   profile disagreeing, and it fails. On any other engine it is a predicate
%   that did not exist, or was not sandbox-safe, when the profile was
%   generated: the profile lists everything that was, allowed or dropped,
%   so anything outside both is new since attestation. Hornguard refuses it
%   either way, so it is reported as the notice it is and does not fail.
test(every_refusal_of_a_sandbox_safe_predicate_is_explained, [true(Unexplained == [])]) :-
    ensure_report, report(R),
    include(of_kind(profile_todo), R, Todo),
    findall(Ind, ( member(_-Ind-_, Todo), \+ explained(Ind) ), Unexplained0),
    (   generated_against_this_engine
    ->  Unexplained = Unexplained0
    ;   Unexplained = [],
        (   Unexplained0 == []
        ->  true
        ;   format(user_error, "~nsandbox-safe on this engine and new since the attested one, refused as unknown: ~q~n", [Unexplained0])
        )
    ).

test(undecided_are_only_closure_arity, [true(Other == [])]) :-
    ensure_report, report(R),
    include(of_kind(undecided), R, Und),
    findall(Ind-D, ( member(_-Ind-D, Und), D \== closure_arity_above_2 ), Other).

%   The committed profile is generated against one engine version and its
%   header records which. On that version, regenerating must reproduce it. On
%   any other, a difference is the engine having changed rather than the file
%   having gone stale, and the differential above is the check that matters â
%   so this one reports and passes instead of going red for every benign
%   addition in a development build.
test(generated_profiles_are_current, [condition(generated_against_this_engine), true(Same == true)]) :-
    tmp_file(swi_profile, Tmp),
    gen_swi_profiles(Tmp),
    read_file_to_string('profiles/swi.pl', S0, []),
    read_file_to_string(Tmp, S1, []),
    delete_file(Tmp),
    strip_date(S0, T0), strip_date(S1, T1),
    ( T0 == T1 -> Same = true ; Same = false ).

generated_against_this_engine :-
    read_file_to_string('profiles/swi.pl', S, []),
    current_prolog_flag(version_data, swi(Ma, Mi, Pa, _)),
    format(atom(Running), "~w.~w.~w", [Ma, Mi, Pa]),
    (   sub_string(S, _, _, _, Running)
    ->  true
    ;   format(user_error,
               "~Nnote: profiles/swi.pl was generated against a different engine than ~w;~n\c
                      skipping the currency check, the differential above is the real one.~n",
               [Running]),
        fail
    ).

:- end_tests(differential).

strip_date(S, T) :-
    split_string(S, "\n", "", Lines),
    exclude([L]>>sub_string(L, _, _, _, "GENERATED by"), Lines, Kept),
    atomic_list_concat(Kept, '\n', T).
