#############################################################################
##
##  This file is part of GAP, a system for computational discrete algebra.
##
##  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 utilities for running tests.
##  It is not read with the library when {\GAP} is started.
##


#############################################################################
##
#F  ExtractManualExamples( <bookname> )
##
##  <bookname> is either ref or tut. 
##  The function should be called EXACTLY from the GAP root directory.
## 
LoadPackage("gapdoc");
ExtractManualExamples:=function( bookname )
local path, main, files, tst, i, s, name, output;
if bookname="tut" then
  files:=[];
elif bookname="ref" then
  Read( "doc/ref/makedocreldata.g" );
  files:= GAPInfo.ManualDataRef.files;
else
  Error( "RunManualsTest : the argument bust be \"ref\" or \"tut\"" );
fi;
path:=Directory( Concatenation( GAPInfo.RootPaths[1], "doc/", bookname ) );
main:="main.xml";
Print("===============================================================\n");
Print("Extracting manual examples from the book '", bookname, "'\n" );
Print("===============================================================\n");
tst:=ManualExamples( path, main, files, "Chapter" );
for i in [ 1 .. Length(tst) ] do 
  Print("Processing '", bookname, "' chapter number ", i, " of ", Length(tst), "\c" );
  if Length( tst[i] ) > 0 then
    s := String(i);
    if Length(s)=1 then 
      # works for <100 chapters
      s:=Concatenation("0",s); 
    fi;
    name := Filename( Directory( Concatenation( "doc/test/", bookname ) ), Concatenation( bookname, s, ".tst" ) );
    output := OutputTextFile( name, false ); # to empty the file first
    SetPrintFormattingStatus( output, false ); # to avoid line breaks
    PrintTo( output, tst[i] );
    CloseStream(output);
    # one superfluous check
    if tst[i] <> StringFile( name ) then
      Error("Saved file does not match original examples string!!!\n");  
    else
      Print(" - OK! \n" );
    fi;
  else
    Print(" - no examples to save! \n" );    
  fi;  
od;
Print("===============================================================\n");
end;

#############################################################################
##
#F  CreatePackageTestsInput( <scriptfile>, <outfile>, <gap>, <other> )
##
##  writes the file <scriptfile> that starts a new GAP session using the
##  command <gap> (including all command line options) for each test file
##  of a package (given by the component `TestFile' in the record stored in
##  its `PackageInfo.g' file) and reads this file. The output of all tests 
##  is collected in the files <outfile>.<packagename>
##  GAP} is started as <gap> (including all command line options).
##  <other> may be "true", "false" or "auto" to specify whether all available
##  packages are loaded, not loaded or only autoloaded packages. This mode
##  is actually managed in the Makefile, and is passed to this function 
##  just to be printed in the information messages.
##
BindGlobal( "CreatePackageTestsInput", function( scriptfile, outfile, gap, other, pkgname... )
    local result, name, entry, pair, testfile, packages;

    SizeScreen( [ 1000 ] );
    InitializePackagesInfoRecords( false );
    result:= "";
    
    Append( result, "TIMESTAMP=`date -u +_%Y-%m-%d-%H-%M`\n" );

    if Length(pkgname)=0 then
      packages := SortedList(ShallowCopy(RecNames(GAPInfo.PackagesInfo)));
    else
      packages := pkgname;
      if Length(packages) <> 1 or Length(packages[1]) = 0 then
         Error("Usage: make testpackage PKGNAME=pkgname");
      fi;
      if not pkgname[1] in RecNames(GAPInfo.PackagesInfo) then
         Error("There is no package with the name ", pkgname[1], " installed\n");
      fi;
    fi;

    for name in packages do
      for entry in GAPInfo.PackagesInfo.( name ) do
        if IsBound( entry.InstallationPath ) and IsBound( entry.TestFile ) then
          testfile := Filename( DirectoriesPackageLibrary( name, "" ), entry.TestFile );
          if testfile <> fail then
            Append( result, Concatenation(
                    "echo 'Testing ", name, " ", entry.Version, ", test=", 
		            testfile, ", all packages=", other, "'\n" ) );
            Append( result, Concatenation( "echo ",
                    "'============================OUTPUT START=============================='",
                    " > ", outfile, "$TIMESTAMP.", name, "\n" ) );
            Append( result, Concatenation(
                    "echo 'SetUserPreference(\"UseColorsInTerminal\",false); ",
                    "RunPackageTests( \"", name,
                    "\", \"", entry.Version, "\", \"", entry.TestFile,
                    "\", \"", other, "\" );' | ", gap, 
                    " >> ", outfile, "$TIMESTAMP.", name, "\n" ) );
            Append( result, Concatenation( "echo ",
                    "'============================OUTPUT END================================'",
                    " >> ", outfile, "$TIMESTAMP.", name, "\n" ) );
          else
            Append( result, Concatenation(
                    "echo 'failed to find test files for the ", name, " package'\n") );
          fi;            
        fi;
      od;
    od;

    PrintTo( scriptfile, result );
    end );


#############################################################################
##
#F  RunPackageTests( <pkgname>, <version>, <testfile>, <other> )
##
##  loads the package <pkgname> in version <version>,
##  and reads the file <testfile> (a path relative to the package directory).
##  If <other> is "true" then all other available packages are also loaded.
##
##  The file <testfile> can either be a file that contains 
##  `Test' statements and therefore must be read with `Read',
##  or it can be a file that itself must be read with `Test';
##  the latter is detected from the occurrence of a substring
##  `"START_TEST"' in the file.
##
BindGlobal( "RunPackageTests", function( pkgname, version, testfile, other )
    local file, PKGTSTHDR, str;

    if LoadPackage( pkgname, Concatenation( "=", version ) ) = fail then
      Print( "#I  RunPackageTests: package `",
             pkgname, "' (version ", version, ") not loadable\n" );
      return;
    fi;
    if other = "true" then
      LoadAllPackages();
    fi;
    PKGTSTHDR := Concatenation( "\"", pkgname, "\", \"", version, "\", \"",
           testfile, "\", \"", other, "\"" );
    Print( "#I  RunPackageTests(", PKGTSTHDR, ");\n" );
    ShowSystemInformation();
    file:= Filename( DirectoriesPackageLibrary( pkgname, "" ), testfile );
    str:= StringFile( file );
    if not IsString( str ) then
      Print( "#I  RunPackageTests: file `", testfile, "' for package `",
             pkgname, "' (version ", version, ") not readable\n" );
      return;
    fi;
    if PositionSublist( str, "gap> START_TEST(" ) = fail then
      if not READ( file ) then
        Print( "#I  RunPackageTests: file `", testfile, "' for package `",
               pkgname, "' (version ", version, ") not readable\n" );
      fi;
    else
      if not Test( file, rec(compareFunction := "uptowhitespace") ) then
        Print( "#I  Errors detected while testing package ", pkgname, " ", version, "\n",
               "#I  using the test file `", testfile, "'\n");
      else
        Print( "#I  No errors detected while testing package ", pkgname, " ", version, "\n",
               "#I  using the test file `", testfile, "'\n");
      fi;
    fi;

    Print( "#I  RunPackageTests(", PKGTSTHDR, "): runtime ", Runtime(), "\n" );
    end );


#############################################################################
##
#F  CreatePackageLoadTestsInput( <scriptfile>, <outfileprefix>, <gap>, 
##                               <autoload>, <onlyneeded> )
##
##  Writes the file <scriptfile> that tests loading each package
##
BindGlobal( "CreatePackageLoadTestsInput", 

	function( scriptfile, outfileprefix, gap, autoload, onlyneeded )

    local mode, smode, PKGLOADTSTOPT, result, name, entry, packagenames;

    SizeScreen( [ 1000 ] );
    InitializePackagesInfoRecords( false );
    result:= "";
    
    mode:="";
    PKGLOADTSTOPT:="";
    if autoload then 
		Append( mode, " with autoloaded" );
	else	
		Append( mode, "                " );
	fi;	
    if onlyneeded then 
		Append( mode, ", only needed" );
		PKGLOADTSTOPT:=":OnlyNeeded";
	fi;	
	smode := NormalizedWhitespace(mode);
	if Length(smode) > 0 and smode[1] <> ' ' then
	  smode:=Concatenation( " ", smode );
    fi;
    
    packagenames := ShallowCopy( RecNames( GAPInfo.PackagesInfo ) );
    Sort( packagenames );
    
    Append( result, "TIMESTAMP=`date -u +_%Y-%m-%d-%H-%M`\n" );
    
    for name in packagenames do
        for entry in GAPInfo.PackagesInfo.( name ) do
            Append( result, "echo '==========================================='\n" );
            Append( result, 
              Concatenation( "echo '%%% Loading ", name, " ", entry.Version, smode, "'\n" ) );
            Append( result, 
              Concatenation( 
                "echo 'SetUserPreference(\"UseColorsInTerminal\",false); ",
                "PKGLOADTSTRES:=LoadPackage(\"", name, "\"", PKGLOADTSTOPT, ");;",
                "Filtered(NamesUserGVars(),x->IsLowerAlphaChar(x[1]) or Length(x)<=3);",
                "if PKGLOADTSTRES=true then PKGLOADTSTRES:=\"### Loaded\"; ",
                "else PKGLOADTSTRES:=\"### Not loaded\"; fi;",
                "Print( PKGLOADTSTRES, \" \",\"", name, "\",\" \",\"", 
                 entry.Version, smode, "\");", 
                "Print([CHAR_INT(10)]);' | ", 
                gap, " > ", outfileprefix, "$TIMESTAMP.", name, " 2>&1 \n" ) );
            Append( result, 
                Concatenation( "cat ", outfileprefix, "$TIMESTAMP.", name, "\n" ) );
         od;
    od;
    Append( result, "echo '==========================================='\n" );
    
    # do not test LoadAllPackages with OnlyNeeded option
    if not onlyneeded then    
        Append( result, Concatenation("echo '\n======OUTPUT START: LoadAllPackages", mode, "'\n" ) );
        Append( result, 
            Concatenation( "echo 'SetUserPreference(\"UseColorsInTerminal\",false); ",
                           "if CompareVersionNumbers( GAPInfo.Version, \"4.5.0\") then ",
                           "SetInfoLevel(InfoPackageLoading,4);",
                           "fi;LoadAllPackages(", PKGLOADTSTOPT, "); ",
                           "Print([CHAR_INT(10)]); ",
                           "Print(\"### all packages loaded                 ", mode, "\"); ' | ",  
                           gap, " > ", outfileprefix, "$TIMESTAMP.all 2>&1 \n" ) );
        Append( result, 
            Concatenation( "cat ", outfileprefix, "$TIMESTAMP.all\n" ) );
        Append( result, 
            Concatenation("echo '\n======OUTPUT END: LoadAllPackages", mode, "'\n" ) );
        Append( result, "echo '==========================================='\n" );
        Append( result, 
            Concatenation("echo '\n======OUTPUT START: LoadAllPackages ",
                          "in the reverse order", mode, "'\n" ) );
        if PKGLOADTSTOPT="" then
      	    PKGLOADTSTOPT:=":reversed";
        else
      	    PKGLOADTSTOPT:=":OnlyNeeded,reversed";
        fi;
        Append( result, 
            Concatenation( "echo 'SetUserPreference(\"UseColorsInTerminal\",false); ",
                           "if CompareVersionNumbers( GAPInfo.Version, \"4.5.0\") then ",
                           "SetInfoLevel(InfoPackageLoading,4);",
                           "fi;LoadAllPackages(", PKGLOADTSTOPT, "); ",
                           "Print([CHAR_INT(10)]); ",
                           "Print(\"### all packages loaded in reverse order", mode, "\"); ' | ", 
                           gap, " > ", outfileprefix, "$TIMESTAMP.all 2>&1 \n" ) );
        Append( result, 
            Concatenation( "cat ", outfileprefix, "$TIMESTAMP.all\n" ) );
        Append( result, 
            Concatenation("echo '\n======OUTPUT END: LoadAllPackages ",
                          "in the reverse order", mode, "'\n" ) );
    fi;         
    
    Append( result, Concatenation( "rm ", outfileprefix, "$TIMESTAMP.*\n" ) );
    PrintTo( scriptfile, result );
    end );
    
    
#############################################################################
##
#F  CreatePackageVarsTestsInput( <scriptfile>, <outfileprefix>, <gap> )
##
##  Writes the file <scriptfile> that calls ShowPackageVariables 
##  for each package in a new GAP session
##
BindGlobal( "CreatePackageVarsTestsInput", 

	function( scriptfile, outfileprefix, gap )

    local result, name, entry, packagenames;

    SizeScreen( [ 1000 ] );
    InitializePackagesInfoRecords( false );
    result:= "";
        
    packagenames := ShallowCopy( RecNames( GAPInfo.PackagesInfo ) );
    Sort( packagenames );
    
    Append( result, "TIMESTAMP=`date -u +_%Y-%m-%d-%H-%M`\n" );
    
    for name in packagenames do
        for entry in GAPInfo.PackagesInfo.( name ) do
            Append( result, "echo '==========================================='\n" );
            Append( result, 
              Concatenation( "echo '### Checking variables in \"", name, "\", ver. ", 
                             entry.Version, "'\n" ) );
            Append( result, 
              Concatenation( 
                "echo 'ShowPackageVariables(\"", name, "\"", ");",
                "Print(\"### Variables checked for \\\"\",\"", name, "\\\"\",\", ver. \",\"", 
                 entry.Version, "\" );", 
                "Print([CHAR_INT(10)]);' | ", 
                gap, " > ", outfileprefix, "$TIMESTAMP.", name, " 2>&1 \n" ) );                
            Append( result, 
                Concatenation( "cat ", outfileprefix, "$TIMESTAMP.", name, "\n" ) );
         od;
    od;
    Append( result, "echo '==========================================='\n" );
    Append( result, Concatenation( "rm ", outfileprefix, "$TIMESTAMP.*\n" ) );
    PrintTo( scriptfile, result );
    end );
    

#############################################################################
##
#F  CreateDevUpdateTestInput( )
#F  RunDevUpdateTests( )
##
##  RunDevUpdateTests() extracts test from dev/Update files and runs them
##  in the current GAP session. CreateDevUpdateTestInput() is an auxiliary
##  function which returns the string with the tests. It may be used to
##  view the tests or print them to a file.
##
BindGlobal( "CreateDevUpdateTestInput",
    function()
    local dirname, file, f, filename, content, alltests, output, nr, line, teststart, testlines;
    dirname:= DirectoriesLibrary( "dev/Updates" );
    if dirname = fail then
      Error("Can not find the 'dev/Updates' directory. Note that it is a part of the\n",
            "development version of GAP and is not included in the GAP distribution\n");
    fi;

    alltests := [ ];
   
    # Exclude hidden files and directories. Sort to ensure the order is not system-dependent
    for file in SortedList( Filtered( DirectoryContents(dirname[1]), f -> f[1] <> '.' ) ) do
    filename := Filename( dirname, file );
    content := SplitString( StringFile( filename ), "\r\n");
    output := [ ];
    nr:=0;
    repeat
        nr := nr+1;
        if nr > Length(content) then
            break;
        fi;
        line := content[nr];
        if Length(line) > 0 then
            if line[1]='!' then
                if LowercaseString( ReplacedString( line, " ","")) = "!testcode" then
                    teststart := nr;
                    testlines := [];
                    repeat
                        nr := nr+1;
                        line := content[nr];
                        if Length(line) > 0 then
                            if line[1]='!' then
                                break;
                            elif not line[1]='%' then
                                Add( testlines, Concatenation(line,"\n") );
                            fi;
                        fi;
                    until false;
                    if Length( testlines ) > 0 then
                        Add(output, Concatenation( "# ", filename, ", line ", String(teststart), "\n") );
                        Append(output, testlines );
                        Add( output, "\n" );
                    fi;
                fi;
            fi;
        fi;
    until false;
    if Length(output) > 0 then 
      Add( output, "#######################\n#END\n");
      Add( alltests, [ filename, Concatenation( output ) ] );
    fi;  
    od;
    return alltests;
end);


BindGlobal( "RunDevUpdateTests",
    function()
    local tests, t, resfile, str;
    tests := CreateDevUpdateTestInput();
    SaveWorkspace("testdevupdate.wsp");    
    for t in tests do
        Print("Checking " , t[1],"\n");
        resfile := "TESTDEVUPDATEOUTPUT";
        FileString( "TESTDEVUPDATE", t[2] );
        Exec( Concatenation(
        "echo 'Test(\"TESTDEVUPDATE\");' | bin/gap.sh -b -r -A -q -L testdevupdate.wsp > ", resfile ));
        str := StringFile(resfile);
        Print(str);
    od;
    RemoveFile("testdevupdate.wsp");
    RemoveFile("TESTDEVUPDATE");
    RemoveFile(resfile);
end);

#############################################################################
##
#F  CheckOutputDelegations
##
##  A method to output an object may only delegate to another operation
##  which appears further right in the following list: Display, ViewObj,
##  PrintObj, DisplayString, ViewString, PrintString, String.
##
##  This function parses the code of all installed methods for these
##  operations and checks whether this rule is followed, and shortlists
##  methods that require further inspection. Since it may still report
##  some cases where it is safe to call a predecessor of an operations
##  for a subobject of the original object, the check cannot be fully
##  automated.
##
BindGlobal( "CheckOutputDelegations",
function()
local rules, name, f, str, ots, met, pos, nargs, r, i, tmp,
      report, line, m, n, illegal_delegations, checklist;

rules := [ "Display", "ViewObj", "PrintObj", "DisplayString",
           "ViewString", "PrintString", "String" ];

for pos in [1..Length(rules)] do
  name := rules[pos];
  report:=[];

  for nargs in [1..2] do
    f:=MethodsOperation( ValueGlobal(name), nargs );
    for m in f do
      met := m.func;
      str := "";
      ots := OutputTextString(str,true);;
      PrintTo( ots, met );
      CloseStream(ots);
      illegal_delegations:=[];
      checklist:=rules{[1..pos-1]};
      for r in checklist do
        # check for all occurrences of the string, but
        # ignore those which are preceded or followed by a letter
        n := 0;
        while true do
          n := POSITION_SUBSTRING(str, r, n);
          if n = fail then break; fi;
          if n > 1 and str[n-1] in LETTERS then continue; fi;
          if Length(str) >= n + Length(r) then
            if not str[n + Length(r)] in LETTERS then
              Add( illegal_delegations, r );
              break;
            fi;
          fi;
        od;
      od;
      if Length(illegal_delegations) > 0 then
        tmp := [];
        if IsBound(m.location) then
          Add(tmp, m.location);
        else
          Add(tmp, [ FILENAME_FUNC( met ), STARTLINE_FUNC( met ) ]);
        fi;
        Append(tmp, [ m.info, illegal_delegations, met ]);
        Add(report, tmp);
      fi;
    od;
  od;

  if Length(report) > 0 then
    Print("\nDetected incorrect delegations for ", name, "\n");
    for line in report do
      Print("---------------------------------------------------------------\n");
      Print( line[2], "\n", " delegates to ", line[3], "\n",
             "Filename: ", line[1][1], ":", line[1][2], "\n", line[4], "\n");
    od;
    Print("---------------------------------------------------------------\n");
  else
    Print("All delegations correct for ", name, "\n");
  fi;

od;
end);
