Quellcodebibliothek Statistik Leitseite products/sources/formale Sprachen/GAP/lib/   (Algebra von RWTH Aachen Version 4.15.1©)  Datei vom 18.9.2025 mit Größe 41 kB image not shown  

Quellcode-Bibliothek test.gi   Sprache: unbekannt

 
#############################################################################
##
##  This file is part of GAP, a system for computational discrete algebra.
##  This file's authors include Frank Lübeck.
##
##  Copyright of GAP belongs to its developers, whose names are too numerous
##  to list here. Please refer to the COPYRIGHT file for details.
##
##  SPDX-License-Identifier: GPL-2.0-or-later
##
##  This file contains functions for test files.
##

##  FirstDiff := function(a, b)
##    return First([1..Length(a)],  i-> not IsBound(b[i]) or b[i] <> a[i]);
##  end;

InstallGlobalFunction(ParseTestInput, function(str, ignorecomments, fnam)
  local lines, inp, pos, outp, ign, commands, i, skipstate, checkifelsefi, foundcmd, testError;
  lines := SplitString(str, "\n", "");
  inp := [];
  pos := [];
  outp := [];
  ign := [];
  commands := [];
  i := 1;
  testError := function(s)
     if IsStream(fnam) then
        ErrorNoReturn(s, " in test stream, line ", i);
    else
        ErrorNoReturn(s, " at ", fnam, ":", i);
    fi;
  end;
  checkifelsefi := l -> ForAny(["#@if","#@elif","#@else","#@fi"],
                               x -> StartsWith(l, x));
  # Set to true if we find a #@if, #@elif, #@else or #@fi. Used to check these
  # do not occur in the middle of a single input/output test block.
  foundcmd := false;
  # skipstate represents the current status of '#@if/#@elif/#@else/#@fi'
  # 0: not in a '#@if'
  # 1: in the first #@if or #@elif with a true condition
  #-1: in an #@if or #@elif with a false condition before the first #@if or
  #    @elif with a true condition
  # 2: in the #@else when all #@if and #@elif had a false condition
  #-2: in the #@else after an #@if or #@elif with a true condition
  #-3: in an #@elif after an #@if or #@elif with a true condition
  # The transition table can be written as
  # state | keyword | condition | new state
  #  0    | #@if    | true      |  1
  #  0    | #@if    | false     | -1
  #  1    | #@elif  | true      | -3
  #  1    | #@elif  | false     | -3
  #  1    | #@else  |           | -2
  #  1    | #@fi    |           |  0
  # -1    | #@elif  | true      |  1
  # -1    | #@elif  | false     | -1
  # -1    | #@else  |           |  2
  # -1    | #@fi    |           |  0
  #  2    | #@fi    |           |  0
  # -2    | #@fi    |           |  0
  # -3    | #@elif  | true      | -3
  # -3    | #@elif  | false     | -3
  # -3    | #@else  |           | -2
  # -3    | #@fi    |           |  0
  # Any other transition is a syntax error.
  # Code is executed whenever skipstate >= 0
  skipstate := 0;
  while i <= Length(lines) do
    if checkifelsefi(lines[i]) then
        foundcmd := true;
        Add(ign, i);
        if StartsWith(lines[i], "#@if") then
            if skipstate <> 0 then
                testError("Invalid test file: Nested #@if");
            fi;
            if EvalString(lines[i]{[5..Length(lines[i])]}) then
                skipstate := 1;
            else
                skipstate := -1;
            fi;
        elif StartsWith(lines[i], "#@elif") then
            if skipstate = 0 then
                testError("Invalid test file: #@elif without #@if");
            elif skipstate = 2 or skipstate = -2 then
                testError("Invalid test file: #@elif after #@else");
            elif skipstate = -1 then
                # No ifs or elifs were true, check condition
                if EvalString(lines[i]{[7..Length(lines[i])]}) then
                    skipstate := 1;
                else
                    skipstate := -1;
                fi;
            else
                # Some if or elif was true, dont evaluate elif
                skipstate := -3;
            fi;
        elif StartsWith(lines[i], "#@else") then
            if skipstate = 0 then
                testError("Invalid test file: #@else without #@if");
            elif skipstate = 2 or skipstate = -2 then
                testError("Invalid test file: two #@else");
            elif skipstate = -1 then
                # No ifs or elifs were true, evaluate else
                skipstate := 2;
            else
                # Some if or elif was true, dont evaluate else
                skipstate := -2;
            fi;
        else # Must be #@fi
            if skipstate = 0 then
                testError("Invalid test file: #@fi without #@if");
            fi;
            skipstate := 0;
        fi;

        i := i + 1;
        continue;
    fi;

    if skipstate < 0 then
        Add(ign, i);
        i := i + 1;
        continue;
    fi;


    if Length(outp) = 0 and Length(inp) = 0 and (Length(lines[i]) = 0 or lines[i][1] = '#') then
      if ignorecomments = true then
        # ignore comment lines and empty lines at beginning of file
        Add(ign, i);
        # execute `#@exec` immediately so `#@if` can depend on it
        if StartsWith(lines[i], "#@exec") then
          Read(InputTextString(Concatenation(lines[i]{[7..Length(lines[i])]}, ";\n")));
        elif Length(lines[i]) > 3 and lines[i]{[1..2]} = "#@" then
          Add(commands, lines[i]);
        fi;
        i := i+1;
      else
        Add(inp, "\n");
        Add(pos, i);
        i := i+1;
        while i <= Length(lines) and (Length(lines[i]) = 0 or (StartsWith(lines[i], "#") and not StartsWith(lines[i], "#@"))) do
          i := i+1;
        od;
        Add(outp, JoinStringsWithSeparator(lines{[1..i-1]}, "\n"));
        Add(outp[1], '\n');
      fi;
    elif Length(lines[i]) = 0 and ignorecomments = true and i < Length(lines)
         and StartsWith(lines[i+1], "#") then
      # ignore an empty line followed by comment lines
      Add(ign, i);
      i := i+1;
      while i <= Length(lines) and StartsWith(lines[i], "#") and
            not StartsWith(lines[i], "#@") do
        Add(ign, i);
        i := i+1;
      od;
    elif StartsWith(lines[i], "gap> ") then
      foundcmd := false;
      Add(outp, "");
      Add(inp, lines[i]{[6..Length(lines[i])]});
      Add(Last(inp), '\n');
      Add(pos, i);
      i := i+1;
    elif StartsWith(lines[i], "> ") then
      if foundcmd then
        testError("Invalid test file: #@ command found in the middle of a single test");
      fi;
      Append(Last(inp), lines[i]{[3..Length(lines[i])]});
      Add(Last(inp), '\n');
      i := i+1;
    elif StartsWith(lines[i], ">\t") then
        testError("Invalid test file: Continuation prompt '> ' followed by a tab, expected a regular space");
    elif Length(outp) > 0 then
      if foundcmd and not ForAll(lines[i], c -> c = ' ' or c = '\t') then
        testError("Invalid test file: #@ command found in the middle of a single test");
      fi;
      Append(Last(outp), lines[i]);
      Add(Last(outp), '\n');
      i := i+1;
    else
      testError("Invalid test file");
      i := i+1;
    fi;
  od;

  if skipstate <> 0 then
    testError("Invalid test file: Unterminated #@if");
  fi;

  Add(pos, ign);
  return rec( inp := inp, outp := outp, pos := pos, commands := commands );
end);

InstallGlobalFunction(ParseTestFile, function(arg)
  local fnam, ignorecomments, str;
  fnam := arg[1];
  if Length(arg) > 1 then
    ignorecomments := arg[2];
  else
    ignorecomments := true;
  fi;
  str := StringFile(fnam);
  if str = fail then
    ErrorNoReturn("Cannot read file ",fnam,"\n");
  fi;
  return ParseTestInput(str, ignorecomments, fnam);
end);

InstallGlobalFunction(RunTests, function(tests, inopts, fnam)
  local opts, breakOnError, alwaysPrintTracebackOnError, inp, outp,
        pos, cmp, times, ttime, nrlines, s, res, fres, t, f, i,
        localbag, failures, startsize, size;
  # don't enter break loop in case of error during test
  opts := rec( breakOnError := false, alwaysPrintTracebackOnError:= false,
               showProgress := "some", localdef := false );

  if not IS_OUTPUT_TTY() then
    opts.showProgress := false;
  fi;
  for f in RecNames(inopts) do
    opts.(f) := inopts.(f);
  od;

  # now start the work
  startsize := SizeScreen();
  SizeScreen([opts.width, startsize[2]]);

  # we collect outputs and add them to 'tests.cmp'
  # also collect timings and add them to 'tests.times'
  inp := tests.inp;
  outp := tests.outp;
  pos := tests.pos;
  cmp := [];
  times := [];
  tests.cmp := cmp;
  tests.times := times;
  failures := 0;

  if Length(inp) = 0 then
    return 0;
  fi;

  breakOnError := BreakOnError;
  BreakOnError := opts.breakOnError;
  alwaysPrintTracebackOnError:= AlwaysPrintTracebackOnError;
  AlwaysPrintTracebackOnError:= opts.alwaysPrintTracebackOnError;

  localbag := false;
  if opts.localdef <> false then
    # Create a local variables bag for the variables listed in
    # #@local (if it exists). We run the test in this context
    # so it does not create/overwrite global variables
    localbag := CREATE_LOCAL_VARIABLES_BAG(opts.localdef);
  fi;
  ttime := Runtime();
  nrlines := pos[Length(pos) - 1];
  for i in [1..Length(inp)] do
    if opts.showProgress = true then
      Print("# line ", pos[i], ", input:\n",inp[i]);
    elif opts.showProgress = "some" then
      Print("\r# line ", pos[i],
            " of ", nrlines,
            " (", Int(pos[i] / nrlines * 100), "%)",
            "\c");
    fi;
    s := InputTextString(inp[i]);
    res := "";
    fres := OutputTextString(res, false);
    t := Runtime();
    READ_STREAM_LOOP(s, fres, localbag);
    t := Runtime() - t;
    CloseStream(fres);
    CloseStream(s);
    # check whether the user aborted by pressing ctrl-C
    if StartsWith(res, "Error, user interrupt") then
        BreakOnError := breakOnError;
        AlwaysPrintTracebackOnError:= alwaysPrintTracebackOnError;
        Error("user interrupt");
        BreakOnError := opts.breakOnError;
        AlwaysPrintTracebackOnError:= opts.alwaysPrintTracebackOnError;
    fi;
    Add(cmp, res);
    Add(times, t);
      # check for and report differences
    if opts.compareFunction(opts.transformFunction(tests.outp[i]),
                            opts.transformFunction(tests.cmp[i])) <> true then
      if opts.showProgress = "some" then
        Print("\r                                    \c\r"); # clear the line
      fi;
      size := SizeScreen();
      SizeScreen(startsize);
      if not opts.ignoreSTOP_TEST or
        PositionSublist(tests.inp[i], "STOP_TEST") <> 1 then
        failures := failures + 1;
        opts.reportDiff(tests.inp[i], tests.outp[i], tests.cmp[i], fnam, tests.pos[i], tests.times[i]);
      else
        # print output of STOP_TEST
        Print(tests.cmp[i]);
      fi;
      SizeScreen(size);
    fi;
  od;
  if opts.showProgress = "some" then
    Print("\r                                    \c\r"); # clear the line
  fi;
  # add total time to 'times'
  Add(times, Runtime() - ttime);
  # reset
  BreakOnError := breakOnError;
  AlwaysPrintTracebackOnError:= alwaysPrintTracebackOnError;
  SizeScreen(startsize);
  return failures;
end);

BindGlobal("TEST", AtomicRecord( rec(Timings := rec())));

TEST.transformFunctions := AtomicRecord(rec());
TEST.transformFunctions.removenl := function(a)
  a := ShallowCopy(a);
  while Length(a) > 0 and Last(a) = '\n' do
    Remove(a);
  od;
  return a;
end;
TEST.transformFunctions.removewhitespace := function(a)
  a := ReplacedString(ShallowCopy(a), "\\\n", "");
  RemoveCharacters(a, " \n\t\r");
  return a;
end;

TEST.compareFunctions := AtomicRecord(rec());
TEST.compareFunctions.uptonl := function(a, b)
  a := TEST.transformFunctions.removenl(a);
  b := TEST.transformFunctions.removenl(b);
  return a=b;
end;
TEST.compareFunctions.uptowhitespace := function(a, b)
  a := TEST.transformFunctions.removewhitespace(a);
  b := TEST.transformFunctions.removewhitespace(b);
  return a=b;
end;


##
## CREATE_LOCAL_VARIABLES_BAG(namelist)
##
## Given a (possibly empty) comma separated string 'namelist',
## create a local variable bag which contains the names in 'namelist'.
##
InstallGlobalFunction(CREATE_LOCAL_VARIABLES_BAG, function(namelist)
    local localvars, func;
    NormalizeWhitespace(namelist);
    if IsEmpty(namelist) then
        localvars := "";
    else
        localvars := Concatenation("local ", namelist, ";");
    fi;
    func := Concatenation("(function() ", localvars,
                          "return GetCurrentLVars(); end)()");
    return EvalString(func);
end);

##
##  <#GAPDoc Label="Test">
##  <ManSection>
##  <Func Name="Test" Arg='fname[, optrec]'/>
##  <Returns><K>true</K> or <K>false</K>.</Returns>
##  <Description>
##  The argument <Arg>fname</Arg> must be the name of a file or an
##  open input stream. The content of this file or stream should contain
##  &GAP; input and output. The function <Ref Func="Test" /> runs the input
##  lines, compares the actual output with the output stored in
##  <Arg>fname</Arg> and reports differences. With an optional record as
##  argument <Arg>optrec</Arg> details of this process can be adjusted.
##  Note that the <C>rewriteToFile</C> option is especially useful for
##  generating test files.
##  <P/>
##  More precisely, the content of <Arg>fname</Arg> must have the following
##  format. <Br/>
##  Lines starting with <C>"gap> "</C> are considered as &GAP; input,
##  they can be followed by lines starting with <C>"> "</C> if the input is
##  continued over several lines. <Br/>
##  To allow for comments in <Arg>fname</Arg> the following lines are ignored
##  by default: lines at the beginning of <Arg>fname</Arg> that start with
##  <C>"#"</C> or are empty, and one empty line together with one or more
##  lines starting with <C>"#"</C>.<Br/>
##  All other lines are considered as &GAP; output from the
##  preceding &GAP; input.
##  <P/>
##  Lines which begin "#@" define special configuration options for tests.
##  The <C>#@local</C> and <C>#@exec</C> options can only be used before
##  any &GAP; input, and the other commands can only be used between
##  individual tests (just before a line starting <C>gap></C>, or at end
##  of the file).
##  Currently defined options are:
##  <List>
##  <Mark>#@local identifierlist</Mark>
##  <Item>Run all the tests in the input as if it is in a function with local variable list
##  <C>identifierlist</C>, which is a comma-separated list of
##  identifiers. Multiple #@local lines may be used.
##  These lines should <E>not</E> end with a comma or semicolon.
##  If this option is used then an error will occur unless <E>all</E>
##  the variables used are included in the local list.
##  <P/>
##  As an example, the <Package>Utils</Package> package has a test file
##  <C>tst/iterator.tst</C> which starts with the lines:
##  <Log><![CDATA[
##  #@local  c3c3, cart, G, h, it1, it2, iter, iter0, iter4, iterL
##  #@local  L, n, pairs0, pairs4, pairsL, s3, s4
##  ]]></Log>
##  </Item>
##  <Mark>#@exec gapcode</Mark>
##  <Item>Execute the code <C>gapcode</C> before any test in the input is run.
##  This allows defining global variables when using <C>#@local</C>.
##  </Item>
##  <Mark>#@if EXPR ... {#@elif EXPR ...} [#@else ...] #@fi</Mark>
##  <Item>A <C>#@if</C> allows to conditionally skip parts of the test input depending on
##  the value of a boolean expression. The exact behavior is done as follows:
##  <P/>
##  If the first &GAP; expression <C>EXPR</C> evaluates to <K>true</K>, then the lines after
##  the <C>#@if</C> are used until either a <C>#@elif</C>, <C>#@else</C> or <C>#@fi</C> is
##  reached.
##  If an <C>#@elif</C> is present, then the lines after <C>#@elif</C> are used if and
##  only if its <C>EXPR</C> evaluates to <K>true</K> and all previous <C>#@if</C> and
##  <C>#@elif</C> clauses had expressions evaluating to <K>false</K>. In this case
##  the lines after <C>#@elif</C> are used until either a <C>#@elif</C>, <C>#@else</C> or
##  <C>#@fi</C> is reached.
##  If an <C>#@else</C> is present then the lines after the <C>#@else</C>
##  are used if and only if <C>EXPR</C> evaluated to <K>false</K> in all <C>#@if</C> and
##  <C>#@elif</C> clauses. Finally, once <C>#fi</C> is reached, evaluation continues
##  normally.
##  <P/>
##  Note that each <C>EXPR</C> is evaluated after all <C>#@exec</C> lines have been
##  executed but before any tests are run. Thus, it cannot depend on test
##  results or packages loaded in tests, but it can depend on packages loaded
##  via <C>#@exec</C>.
##  <P/>
##  In addition <C>#@if</C> clauses cannot be nested within each other.
##  <P/>
##  As an example, the &GAP; test suite contains the test file
##  <C>tst/testinstall/pperm.tst</C> which contains the lines:
##  <Log><![CDATA[
##  #@if GAPInfo.BytesPerVariable = 8
##  gap> HASH_FUNC_FOR_PPERM(f, 10 ^ 6) in [260581, 402746];
##  true
##  #@else
##  gap> HASH_FUNC_FOR_PPERM(f, 10 ^ 6);
##  953600
##  #@fi
##  ]]></Log>
##  </Item>
##  </List>
##  By default the actual &GAP; output is compared exactly with the
##  stored output, and if these are different some information about the
##  differences is printed.
##  <P/>
##  If any differences are found then <Ref Func="Test" /> returns <K>false</K>,
##  otherwise <K>true</K>.
##  <P/>
##  If the optional argument <Arg>optrec</Arg> is given it must be a record.
##  The following components of <Arg>optrec</Arg> are recognized and can change
##  the default behaviour of <Ref Func="Test" />:
##  <List>
##  <Mark><C>ignoreComments</C></Mark>
##  <Item>If set to <K>false</K> then no lines in <Arg>fname</Arg>
##  are ignored as explained above (default is <K>true</K>).</Item>
##  <Mark><C>width</C></Mark>
##  <Item>The screen width used for the new output (default is <C>80</C>).
##  </Item>
##  <Mark><C>compareFunction</C></Mark>
##  <Item>This must be a function that gets two strings as input, the newly
##  generated and the stored output of some &GAP; input. The function must
##  return <K>true</K> or <K>false</K>, indicating if the strings should
##  be considered equivalent or not. By default <Ref Oper="\=" /> is used.
##  <Br/>
##  Two strings are recognized as abbreviations in this component:
##  <C>"uptowhitespace"</C> checks if the two strings become equal after
##  removing all white space. And <C>"uptonl"</C> compares the string up
##  to trailing newline characters.
##  </Item>
##  <Mark><C>transformFunction</C></Mark>
##  <Item>This must be a function that gets one string as input, either the newly
##  generated or the stored output of some &GAP; input. The function must
##  return a new string which will be used to compare the actual and the expected
##  output. By default <Ref Func="IdFunc" /> is used.
##  <Br/>
##  Two strings are recognized as abbreviations in this component:
##  <C>"removewhitespace"</C> removes all white space.
##  And <C>"removenl"</C> removes all trailing newline characters.
##  </Item>
##  <Mark><C>reportDiff</C></Mark>
##  <Item>A function that gets six arguments and reports a difference in the
##  output: the &GAP; input, the expected &GAP; output, the newly generated
##  output, the name of tested file, the line number of the input, the
##  time to run the input. (The default is demonstrated in the example
##  below.)</Item>
##  <Mark><C>rewriteToFile</C></Mark>
##  <Item>If this is bound to a string it is considered as a file name
##  and that file is written with the same input and comment lines as
##  <Arg>fname</Arg> but the output substituted by the newly generated
##  version; if it is bound to <K>true</K>, then this is treated as if
##  it was bound to <Arg>fname</Arg> (default is <K>false</K>). This is
##  especially useful for generating test files because it ensures that
##  the test files are formatted exactly as <Ref Func="Test" /> expects
##  them to be.
##  </Item>
##  <Mark><C>writeTimings</C></Mark>
##  <Item>If this is bound to a string it is considered as a file name,
##  that file is written and contains timing information for each input
##  in <Arg>fname</Arg>. </Item>
##  <Mark><C>compareTimings</C></Mark>
##  <Item>If this is bound to a string it is considered as name of a file to
##  which timing information was stored via <C>writeTimings</C> in a previous
##  call. The new timings are compared to the stored ones.
##  By default only commands which take more than a threshold of
##  100 milliseconds are considered, and only differences of more than 20% are
##  considered significant. These defaults can be overwritten by assigning a
##  list <C>[timingfile, threshold, percentage]</C> to this component.
##  (The default of <C>compareTimings</C> is <K>false</K>.)</Item>
##  <Mark><C>reportTimeDiff</C></Mark>
##  <Item>This component can be used to overwrite the default function to
##  display timing differences. It must be a function with 5 arguments:
##  &GAP; input, name of test file, line number, stored time, new time.
##  </Item>
##  <Mark><C>ignoreSTOP_TEST</C></Mark>
##  <Item>By default set to <K>true</K>, in that case the output of &GAP;
##  input starting with <C>"STOP_TEST"</C> is not checked.</Item>
##  <!--  don't document now, needs some work to become useful
##  <Mark><C>breakOnError</C></Mark>
##  <Item>If this is <K>true</K> then &GAP; enters a break loop in case of
##  an error (default is <K>false</K>).</Item>
##  -->
##  <Mark><C>showProgress</C></Mark>
##  <Item>If this is <K>true</K> then &GAP; prints position information
##  and the input line before it is processed; if set to <C>"some"</C>,
##  then GAP shows the current line number of the test being processed;
##  if set to <K>false</K>, no progress updates are displayed
##  (default is <C>"some"</C> if GAP's output goes to a terminal, otherwise
##  <K>false</K>). </Item>
##  <Mark><C>subsWindowsLineBreaks</C></Mark>
##  <Item>If this is <K>true</K> then &GAP; substitutes DOS/Windows style
##  line breaks "\r\n" by UNIX style line breaks "\n" after reading the test
##  file. (default is <K>true</K>).</Item>
##  <Mark><C>returnNumFailures</C></Mark>
##  <Item>If this is <K>true</K> then &GAP; returns the number of input
##  lines of the test file which had differences in their output, instead
##  of returning <K>true</K> or <K>false</K>.</Item>
##  </List>
##
##  <Log><![CDATA[
##  gap> tnam := Filename(DirectoriesLibrary(), "../doc/ref/demo.tst");;
##  gap> mask := function(str) return Concatenation("| ",
##  >          JoinStringsWithSeparator(SplitString(str, "\n", ""), "\n| "),
##  >          "\n"); end;;
##  gap> Print(mask(StringFile(tnam)));
##  | # this is a demo file for the 'Test' function
##  | #
##  | gap> g := Group((1,2), (1,2,3));
##  | Group([ (1,2), (1,2,3) ])
##  |
##  | # another comment following an empty line
##  | # the following fails:
##  | gap> a := 13+29;
##  | 41
##  gap> ss := InputTextString(StringFile(tnam));;
##  gap> Test(ss);
##  ########> Diff in test stream, line 8:
##  # Input is:
##  a := 13+29;
##  # Expected output:
##  41
##  # But found:
##  42
##  ########
##  false
##  gap> RewindStream(ss);
##  true
##  gap> dtmp := DirectoryTemporary();;
##  gap> ftmp := Filename(dtmp,"demo.tst");;
##  gap> Test(ss, rec(reportDiff := Ignore, rewriteToFile := ftmp));
##  false
##  gap> Test(ftmp);
##  true
##  gap> Print(mask(StringFile(ftmp)));
##  | # this is a demo file for the 'Test' function
##  | #
##  | gap> g := Group((1,2), (1,2,3));
##  | Group([ (1,2), (1,2,3) ])
##  |
##  | # another comment following an empty line
##  | # the following fails:
##  | gap> a := 13+29;
##  | 42
##  ]]></Log>
##  </Description>
##  </ManSection>
##  <#/GAPDoc>
##
DeclareGlobalName("TextAttr"); # from GAPDoc
DeclareGlobalName("DefaultReportDiffColors"); # initialized in Test() or by the user
BindGlobal("DefaultReportDiff", function(inp, expout, found, fnam, line, time)
  if UserPreference("UseColorsInTerminal") = true then
    Print(DefaultReportDiffColors.message);
    Print("########> Diff in ");
    if IsStream(fnam) then
      Print("test stream, line ",line,":");
    else
      Print(fnam,":",line);
    fi;
    Print(TextAttr.reset, "\n", DefaultReportDiffColors.message);
    Print("# Input is:", TextAttr.reset, "\n");
    Print(DefaultReportDiffColors.input);
    Print(inp);
    Print(TextAttr.reset, TextAttr.delline, DefaultReportDiffColors.message);
    Print("# Expected output:", TextAttr.reset, "\n");
    Print(DefaultReportDiffColors.expected);
    Print(expout);
    Print(TextAttr.reset, TextAttr.delline, DefaultReportDiffColors.message);
    Print("# But found:", TextAttr.reset, "\n");
    Print(DefaultReportDiffColors.actual);
    Print(found);
    Print(TextAttr.reset, TextAttr.delline, DefaultReportDiffColors.message);
    Print("########", TextAttr.reset, "\n");
  else
    Print("########> Diff in ");
    if IsStream(fnam) then
      Print("test stream, line ",line,":\n");
    else
      Print(fnam,":",line,"\n");
    fi;
    Print("# Input is:\n", inp);
    Print("# Expected output:\n", expout);
    Print("# But found:\n", found);
    Print("########\n");  fi;
end);

InstallGlobalFunction("Test", function(arg)
  local fnam, nopts, opts, full, pf, failures, lines, ign, new,
        cT, ok, oldtimes, thr, delta, len, c, i, j, d, localdef, line;

  # get arguments and set options
  fnam := arg[1];
  if Length(arg) > 1 and IsRecord(arg[2]) then
    nopts := arg[2];
  else
    nopts := rec();
  fi;
  if not IsBound(DefaultReportDiffColors) then
    BindGlobal("DefaultReportDiffColors", rec(
        message := TextAttr.4,  # blue text
        input := "",
        expected := Concatenation(TextAttr.0, TextAttr.b2), # black text on green background
        actual := Concatenation(TextAttr.7, TextAttr.b1),   # white text on red background
        ));
  fi;
  opts := rec(
           ignoreComments := true,
           isStream := IsStream(fnam),
           width := 80,
           ignoreSTOP_TEST := true,
           compareFunction := EQ,
           transformFunction := IdFunc,
           showProgress := "some",
           writeTimings := false,
           compareTimings := false,
           reportTimeDiff := function(inp, fnam, line, oldt, newt)
             local d;
             d := String(Int(100*(newt-oldt)/oldt));
             if d[1] <> '-' then
               d := Concatenation("+", d);
             fi;
             Print("########> Time diff in ");
             if IsStream(fnam) then
               Print("test stream, line ",line,":\n");
             else
               Print(fnam,":",line,"\n");
             fi;
             Print("# Input:\n", inp);
             Print("# Old time: ", oldt,"   New time: ", newt,
             "    (", d, "%)\n");
           end,
           rewriteToFile := false,
           breakOnError := false,
           reportDiff := DefaultReportDiff,
           subsWindowsLineBreaks := true,
           returnNumFailures := false,
           localdef := false,
         );
  if not IS_OUTPUT_TTY() then
    opts.showProgress := false;
  fi;

  if IsHPCGAP then
    # HPCGAP's window size varies in different threads
    opts.transformFunction := "removewhitespace";
    # HPC-GAP's output is not compatible with changing lines
    opts.showProgress := false;
  fi;

  for c in RecNames(nopts) do
    opts.(c) := nopts.(c);
  od;
  # check shortcuts
  if IsString(opts.compareFunction) then
    if IsBound(TEST.compareFunctions.(opts.compareFunction)) then
      opts.compareFunction := TEST.compareFunctions.(opts.compareFunction);
    else
      Error("Unknown compareFunction '", opts.compareFunction, "'");
    fi;
  fi;
  if IsString(opts.transformFunction) then
    if IsBound(TEST.transformFunctions.(opts.transformFunction)) then
      opts.transformFunction := TEST.transformFunctions.(opts.transformFunction);
    else
      Error("Unknown transformFunction '", opts.transformFunction, "'");
    fi;
  fi;

  # remember the full input
  if not opts.isStream then
    full := StringFile(fnam);
    if full = fail then
      ErrorNoReturn("Cannot read file ",fnam,"\n");
    fi;
  else
    full := ReadAll(fnam);
  fi;
  # change Windows to UNIX line breaks
  if opts.subsWindowsLineBreaks = true then
    full := ReplacedString(full, "\r\n", "\n");
  fi;

  # split input into GAP input, GAP output and comments
  pf := ParseTestInput(full, opts.ignoreComments, fnam);

  # Warn if we have not found any tests in the file
  if IsEmpty(pf.inp) then
    Info(InfoWarning, 1, "Test: File does not contain any tests!");
  fi;
  for line in pf.commands do
    if StartsWith(line, "#@local") then
      line := line{[8..Length(line)]};
      if opts.localdef = false then
        opts.localdef := line;
      else
        opts.localdef := Concatenation(opts.localdef, ", ", line);
      fi;
    else
      ErrorNoReturn("Invalid #@ test option: ", line);
    fi;
  od;

  # run the GAP inputs and collect the outputs and the timings
  failures := RunTests(pf, opts, fnam);

  # maybe rewrite the input into a file
  if opts.rewriteToFile = true then
    opts.rewriteToFile := fnam;
  fi;
  if IsString(opts.rewriteToFile) then
    lines := SplitString(full, "\n", "");
    ign := Last(pf.pos);
    new := [];
    for i in ign do
      new[i] := lines[i];
      Add(new[i], '\n');
    od;
    for i in [1..Length(pf.inp)] do
      new[pf.pos[i]] := "";
      for j in [1 .. Number(pf.inp[i], c -> c = '\n') +
          Number(pf.outp[i], c -> c = '\n')] do
        if (j = 1 and StartsWith(lines[pf.pos[i] + j - 1], "gap> ")) or
            (j > 1 and StartsWith(lines[pf.pos[i] + j - 1], "> ")) then
          Append(new[pf.pos[i]], lines[pf.pos[i] + j - 1]);
          Add(new[pf.pos[i]], '\n');
        fi;
      od;
      if PositionSublist(pf.inp[i], "STOP_TEST") <> 1 then
        Append(new[pf.pos[i]], pf.cmp[i]);
        if pf.cmp[i] <> "" and Last(pf.cmp[i]) <> '\n' then
            Info(InfoWarning, 1, "An output in the .tst file does not end with a newline. GAP does not support this.");
            Info(InfoWarning, 1, "A newline will be inserted to make the file valid, but the test will fail.");
            Info(InfoWarning, 1, "The location of this problem is marked with '# Newline inserted here'.");
            Append(new[pf.pos[i]], "# Newline inserted here by 'rewriteToFile'\n");
        fi;
      fi;
    od;
    new := Concatenation(Compacted(new));
    FileString(opts.rewriteToFile, new);
  fi;

  # maybe store the timings into a file
  if IsString(opts.writeTimings) then
    PrintTo(opts.writeTimings, "TEST.Timings.(\"", opts.writeTimings,
            "\") := \n", pf.times, ";\n");
  fi;

  # maybe compare timings
  cT := opts.compareTimings;
  if IsList(cT) and IsString(cT[1]) then
    ok := READ(cT[1]);
    if not ok then
      Info(InfoWarning, 1, "Could not read timings from ", cT[1]);
    else
      oldtimes := TEST.Timings.(cT[1]);
      if Length(cT) > 1 and IsInt(cT[2]) then
        thr := cT[2];
      else
        thr := 100;
      fi;
      if Length(cT) > 2 and IsInt(cT[3]) then
        delta := cT[3];
      else
        delta := 10;
      fi;
      for i in [1..Length(pf.inp)] do
        if oldtimes[i] >= thr and
           AbsInt(oldtimes[i] - pf.times[i])/oldtimes[i] > delta/100 then
          opts.reportTimeDiff(pf.inp[i], fnam, pf.pos[i], oldtimes[i], pf.times[i]);
        fi;
      od;
      # compare total times
      len := Length(oldtimes);
      if oldtimes[len] >= thr and
         AbsInt(oldtimes[len] - pf.times[len])/oldtimes[len] > delta/100 then
         d := String(Int(100*(pf.times[len] - oldtimes[len])/oldtimes[len]));
         if d[1] <> '-' then
           d := Concatenation("+", d);
         fi;
         Print("########> Total time for ", fnam, ":\n");
         Print("# Old time: ", oldtimes[len],"   New time: ", pf.times[len],
         "    (", d, "%)\n");
      fi;
    fi;
  fi;

  # store internal test data in TEST, in old list format
  TEST.lastTestData := [pf.inp, pf.outp, pf.pos, pf.cmp, pf.times];
  # And also new record format
  TEST.lastTestDataRec := pf;

  # if requested, return number of failures
  if opts.returnNumFailures then
    return failures;
  fi;

  # return true/false
  return (failures = 0);
end);


##  <#GAPDoc Label="TestDirectory">
##  <ManSection>
##  <Func Name="TestDirectory" Arg='inlist[, optrec]'/>
##  <Returns><K>true</K> or <K>false</K>.</Returns>
##  <Description>
##  The argument <Arg>inlist</Arg> must be either a single filename
##  or directory name, or a list of filenames and directories.
##  The function <Ref Func="TestDirectory" /> will create a list of files
##  to be tested by taking any files in <Arg>inlist</Arg>, and recursively searching
##  any directories in <Arg>inlist</Arg> for files ending in <C>.tst</C>.
##  Each of these files is then run through <Ref Func="Test" />, and the results
##  printed, and <K>true</K> returned if all tests passed.
##  <P/>
##  If the optional argument <Arg>optrec</Arg> is given it must be a record.
##  Note that the <C>rewriteToFile</C> option is especially useful for
##  generating test files.
##  The following components of <Arg>optrec</Arg> are recognized and can change
##  the default behaviour of <Ref Func="TestDirectory" />:
##  <List>
##  <Mark><C>testOptions</C></Mark>
##  <Item>A record which will be passed on as the second argument of <Ref Func="Test" />
##  if present.</Item>
##  <Mark><C>earlyStop</C></Mark>
##  <Item>If <K>true</K>, stop as soon as any <Ref Func="Test" /> fails (defaults to <K>false</K>).
##  </Item>
##  <Mark><C>showProgress</C></Mark>
##  <Item>Print information about how tests are progressing (defaults to <C>"some"</C>
##  if GAP's output goes to a terminal, otherwise <K>false</K>).
##  </Item>
##  <Mark><C>suppressStatusMessage</C></Mark>
##  <Item>suppress displaying status messages <C>#I  Errors detected while testing</C> and
##  <C>#I  No errors detected while testing</C> after the test (defaults to <K>false</K>).
##  </Item>
##  <Mark><C>rewriteToFile</C></Mark>
##  <Item>If <K>true</K>, then rewrite each test file to disc, with the output substituted
##  by the results of running the test (defaults to <K>false</K>).
##  This is especially useful for generating test files because it ensures that
##  the test files are formatted exactly as <Ref Func="Test" /> expects
##  them to be.
##  </Item>
##  <Mark><C>exclude</C></Mark>
##  <Item>A list of file and directory names which will be excluded from
##  testing (defaults to <K>[]</K>).
##  </Item>
##  <Mark><C>exitGAP</C></Mark>
##  <Item>Rather than returning <K>true</K> or <K>false</K>, exit GAP with the return value
##  of GAP set to success or fail, depending on if all tests passed (defaults to <K>false</K>).
##  </Item>
##  </List>
##
##  </Description>
##  </ManSection>
##  <#/GAPDoc>
InstallGlobalFunction( "TestDirectory", function(arg)
    local  testTotalFailures, testFailedFiles, totalTime, totalMem, STOP_TEST_CPY,
           basedirs, nopts, opts, testOptions, earlyStop,
           showProgress, suppressStatusMessage, exitGAP, c, files,
           filetimes, filemems, recurseFiles, f, i, startTime,
           startMem, testResult, time, mem, startGcTime, gctime,
           totalGcTime, filegctimes;

  testTotalFailures := 0;
  testFailedFiles := 0;
  totalTime := 0;
  totalMem := 0;
  totalGcTime := 0;

  STOP_TEST_CPY := STOP_TEST;
  # wrap STOP_TEST_QUIET to drop the return value so it does not get printed below
  STOP_TEST := function(arg) CallFuncList( STOP_TEST_QUIET, arg ); end;

  if IsString(arg[1]) or IsDirectory(arg[1]) then
    basedirs := [arg[1]];
  else
    basedirs := arg[1];
  fi;

  if Length(arg) > 1 and IsRecord(arg[2]) then
    nopts := arg[2];
  else
    nopts := rec();
  fi;

  opts := rec(
    testOptions := rec(),
    earlyStop := false,
    showProgress := true,
    suppressStatusMessage := false,
    rewriteToFile := false,
    exclude := [],
    exitGAP := false,
  );

  for c in RecNames(nopts) do
    opts.(c) := nopts.(c);
  od;
  opts.exclude := Set(opts.exclude);
  opts.testOptions.returnNumFailures := true;

  if opts.exitGAP then
    GapExitCode(1);
  fi;

  files := [];
  filetimes := [];
  filemems := [];
  filegctimes := [];

  recurseFiles := function(dirs, prefix)
    local dircontents, testfiles, t, testrecs, shortName, recursedirs, d, subdirs;
    if Length(dirs) = 0 then return; fi;
    if prefix in opts.exclude then return; fi;
    dircontents := Union(List(dirs, DirectoryContents));
    testfiles := Filtered(dircontents, x -> EndsWith(x, ".tst"));
    testrecs := [];
    for t in testfiles do
      shortName := Concatenation(prefix, t);
      if shortName[1] = '/' then
        shortName := shortName{[2..Length(shortName)]};
      fi;
      if not shortName in opts.exclude then
        Add(testrecs, rec(name := Filename(dirs, t), shortName := shortName));
      fi;
    od;
    Append(files, testrecs);

    recursedirs := Difference(dircontents, testfiles);
    RemoveSet(recursedirs, ".");
    RemoveSet(recursedirs, "..");
    for d in recursedirs do
      subdirs := List(dirs, x -> Directory(Filename(x, d)));
      subdirs := Filtered(subdirs, IsDirectoryPath);
      recurseFiles(subdirs, Concatenation(prefix,d,"/"));
    od;
  end;

  files := [];
  for f in basedirs do
    if not IsString(f) and IsList(f) and ForAll(f, IsDirectoryPath) then
      recurseFiles(List(f, Directory), "");
    elif IsDirectoryPath(f) then
      recurseFiles( [ Directory(f) ], "" );
    else
      Add(files, rec(name := f, shortName := f));
    fi;
  od;

  SortBy(files, f -> [f.shortName, f.name]);

  if opts.showProgress then
    Print( "Architecture: ", GAPInfo.Architecture, "\n\n" );
  fi;

  for i in [1..Length(files)] do
    if opts.showProgress then
      Print("testing: ", files[i].name, "\n");
    fi;

    startTime := Runtime();
    startMem := TotalMemoryAllocated();
    startGcTime := TOTAL_GC_TIME();

    if opts.rewriteToFile then
      opts.testOptions.rewriteToFile := files[i].name;
    fi;
    testResult := Test(files[i].name, opts.testOptions);
    if (testResult <> 0) and opts.earlyStop then
      STOP_TEST := STOP_TEST_CPY;
      if not opts.suppressStatusMessage then
        # Do not change the next line - it is needed for testing scripts
        Print( "#I  Errors detected while testing\n\n" );
      fi;
      if opts.exitGAP then
        QuitGap(1);
      fi;
      return false;
    fi;
    if testResult <> 0 then
      testFailedFiles := testFailedFiles + 1;
    fi;
    testTotalFailures := testTotalFailures + testResult;

    time := Runtime() - startTime;
    mem := TotalMemoryAllocated() - startMem;
    gctime := TOTAL_GC_TIME() - startGcTime;
    filetimes[i] := time;
    filemems[i] := mem;
    filegctimes[i] := gctime;
    totalTime := totalTime + time;
    totalMem := totalMem + mem;
    totalGcTime := totalGcTime + gctime;

    if opts.showProgress then
        Print( String( time, 8 ), " ms (",String(gctime)," ms GC) and ",
               StringOfMemoryAmount( mem ),
               " allocated for ", files[i].shortName, "\n" );
    fi;
  od;

  STOP_TEST := STOP_TEST_CPY;

  Print("-----------------------------------\n");
  Print( "total",
         String( totalTime, 10 ), " ms (",String( totalGcTime )," ms GC) and ",
         StringOfMemoryAmount( totalMem )," allocated\n" );
  Print( "     ", String( testTotalFailures, 10 ), " failures in " );
  if testTotalFailures > 0 then
    Print( testFailedFiles, " of " );
  fi;
  Print( Length(files), " files\n\n" );

  if not opts.suppressStatusMessage then
    if testTotalFailures = 0 then
      # Do not change the next line - it is needed for testing scripts
      Print( "#I  No errors detected while testing\n\n" );
    else
      # Do not change the next line - it is needed for testing scripts
      Print( "#I  Errors detected while testing\n\n" );
    fi;
  fi;

  if opts.exitGAP then
    if testTotalFailures = 0 then
      QuitGap(0);
    else
      QuitGap(1);
    fi;
  fi;

  return testTotalFailures = 0;
end);

#############################################################################
##
## TestPackage( <pkgname> )
##
##  <#GAPDoc Label="TestPackage">
##  <ManSection>
##  <Func Name="TestPackage" Arg='pkgname'/>
##  <Description>
##  It is recommended that a &GAP; package specifies a standard test in its
##  <F>PackageInfo.g</F> file. If <A>pkgname</A> is a string with the name of
##  a &GAP; package, then <C>TestPackage(pkgname)</C> will check if this
##  package is loadable and has the standard test, and will run this test in
##  the current &GAP; session.<P/>
##
##  The output of the test depends on the particular package, and it also
##  may depend on the current &GAP; session (loaded packages, state of the
##  random sources, defined global variables etc.).
##  </Description>
##  </ManSection>
##  <#/GAPDoc>
##
InstallGlobalFunction( "TestPackage", function(pkgname)
local testfile, str;
pkgname := LowercaseString(pkgname);
if not IsBound( GAPInfo.PackagesInfo.(pkgname) ) then
    Print("#I  No package with the name ", pkgname, " is available\n");
    return fail;
elif LoadPackage( pkgname ) = fail then
    Print( "#I ", pkgname, " package cannot be loaded\n" );
    return fail;
elif not IsBound( GAPInfo.PackagesInfo.(pkgname)[1].TestFile ) then
    Print("#I No standard tests specified in ", pkgname, " package, version ",
          GAPInfo.PackagesInfo.(pkgname)[1].Version,  "\n");
    # Since a TestFile is not required, technically we passed "all" tests
    return true;
else
    testfile := Filename( DirectoriesPackageLibrary( pkgname, "" ),
                          GAPInfo.PackagesInfo.(pkgname)[1].TestFile );
    str:= StringFile( testfile );
    if not IsString( str ) then
        Print( "#I Test file `", testfile, "' for package `", pkgname,
        " version ", GAPInfo.PackagesInfo.(pkgname)[1].Version, " is not readable\n" );
        return fail;
    fi;
    if EndsWith(testfile,".tst") then
        if Test( testfile, rec(compareFunction := "uptowhitespace") ) then
            Print( "#I  No errors detected while testing package ", pkgname,
                   " version ", GAPInfo.PackagesInfo.(pkgname)[1].Version,
                   "\n#I  using the test file `", testfile, "'\n");
            return true;
        else
            Print( "#I  Errors detected while testing package ", pkgname,
                   " version ", GAPInfo.PackagesInfo.(pkgname)[1].Version,
                   "\n#I  using the test file `", testfile, "'\n");
            return false;
        fi;
    elif not READ( testfile ) then
        Print( "#I Test file `", testfile, "' for package `", pkgname,
        " version ", GAPInfo.PackagesInfo.(pkgname)[1].Version, " is not readable\n" );
        return fail;
    else
        # At this point, the READ succeeded, but we have no idea what the
        # outcome of that test was. Hopefully, that file printed a message of
        # its own and then terminated GAP with a suitable error code (e.g. by
        # using TestDirectory with exitGAP:=true); in that case we never get
        # here and all is fine.
        return fail;
    fi;
fi;
end);

[ 0.71Quellennavigators  Projekt   ]