#############################################################################
##
#W  Examples.gi                  GAPDoc                          Frank Lübeck
##
##
#Y  Copyright (C)  2007,  Frank Lübeck,  Lehrstuhl D für Mathematik,  
#Y  RWTH Aachen
##  
##  The files Examples.g{d,i} contain functions for extracting and checking
##  GAP examples in GAPDoc manuals.
##  


##  <#GAPDoc Label="ExtractExamples">
##  <ManSection >
##  <Func Arg="path, main, files, units[, withLog]" Name="ExtractExamples" />
##  <Returns>a list of lists</Returns>
##  <Func Arg="tree, units[, withLog]" Name="ExtractExamplesXMLTree" />
##  <Returns>a list of lists</Returns>
##  <Description>
##  The  argument   <A>tree</A>  must   be  a   parse tree of a
##  &GAPDoc; document, see <Ref Func="ParseTreeXMLFile"/>. 
##  The function <Ref Func="ExtractExamplesXMLTree"/> returns a data
##  structure representing the <C>&lt;Example></C> elements of the document. 
##  The return value can be used with <Ref Func="RunExamples"/> to check and
##  optionally update the examples of the document.<P/> 
##  Depending 
##  on the argument <A>units</A> several examples are collected in one list.
##  Recognized values for <A>units</A> are <C>"Chapter"</C>, <C>"Section"</C>,
##  <C>"Subsection"</C> or <C>"Single"</C>. The latter means that each example
##  is in a separate list. For all other value of <A>units</A> just one list
##  with all examples is returned.<P/>
##  
##  The arguments <A>path</A>, <A>main</A> and <A>files</A> of <Ref
##  Func="ExtractExamples"/> are the same as for <Ref Func="ComposedDocument"/>.
##  This function first contructs and parses the &GAPDoc; document and then
##  applies <Ref Func="ExtractExamplesXMLTree"/>.<P/>
##  
##  If the optional argument <A>withLog</A> is given and <K>true</K> then
##  <C>&lt;Log></C> elements are handled like <C>&lt;Example></C> elements.
##  This allows to put examples which can only run under certain conditions,
##  e.g., when certain external programs are available, into <C>&lt;Log></C>
##  elements. (Put example code which should also not be included by this
##  variant into <C>&lt;Listing></C> elements.)
##  </Description>
##  </ManSection>
##  <#/GAPDoc>

if not GAPInfo.CommandLineOptions.O and 
       UserPreference( "ReadObsolete" ) <> false then
  MANEXreadobs := true;
else
  MANEXreadobs := false;
fi;

# obsolete
# Extract examples units-wise from a GAPDoc document as XML tree, 
# 'units' can either be: "Chapter" or "Section" or "Subsection" or "Single"
#     then a list of strings is returned
# For all other values of 'units' one string with all examples is returned.
# Before each extracted example there is its paragraph number in a comment:
#  [ chapter, section, subsection, paragraph ]
if MANEXreadobs then
InstallGlobalFunction(ManualExamplesXMLTree, function( tree, units )
  local secelts, sec, exelts, res, str, a, ex;
  if units = "Chapter" then
    secelts := ["Chapter", "Appendix"];
  elif units = "Section" then
    secelts := ["Section"];
  elif units = "Subsection" then
    secelts := ["Subsection", "ManSection"];
  elif units = "Single" then
    secelts := ["Example"];
  else
    secelts := 0;
  fi;
  if secelts <> 0 then
    sec := XMLElements(tree, secelts);
  else
    sec := [tree];
  fi;
  # want to put section numbers in comments
  AddParagraphNumbersGapDocTree(tree);
  exelts := List(sec, a-> XMLElements(a, ["Example"]));
  res := [];
  for a in exelts do
    str := "";
    for ex in a do
      Append(str, "# from paragraph ");
      if IsBound(ex.count) then
        Append(str, String(ex.count));
      else
        Append(str, "in Ignore?");
      fi;
      if IsBound(tree.inputorigins) then
        Append(str, String(OriginalPositionDocument(
                                           tree.inputorigins, ex.start)));
      fi;
      Append(str, "\n");
      Append(str, GetTextXMLTree(ex));
      Append(str, "\n");
    od;
    Add(res, str);
  od;
  if secelts = 0 then
    res := res[1];
  fi;
  return res;
end);
fi;

InstallGlobalFunction(ExtractExamplesXMLTree, 
function( tree, units, withLog... )
  local secelts, sec, eltnames, exelts, orig, res, l, b, e, a, ex;
  if units = "Chapter" then
    secelts := ["Chapter", "Appendix"];
  elif units = "Section" then
    secelts := ["Section"];
  elif units = "Subsection" then
    secelts := ["Subsection", "ManSection"];
  elif units = "Single" then
    secelts := ["Example"];
  else
    secelts := 0;
  fi;
  if secelts <> 0 then
    sec := XMLElements(tree, secelts);
  else
    sec := [tree];
  fi;
  if Length(withLog) > 0 and withLog[1] = true then
    eltnames := ["Example", "Log"];
  else
    eltnames := ["Example"];
  fi;
  exelts := List(sec, a-> XMLElements(a, eltnames));
  if IsBound(tree.inputorigins) then
    orig := tree.inputorigins;
  elif IsBound(tree.root) and IsBound(tree.root.inputorigins) then
    orig := tree.inputorigins;
  else
    orig := fail;
  fi;
  res := [];
  for a in exelts do
    l := [];
    for ex in a do
      if orig <> fail then
        b := OriginalPositionDocument(orig, ex.start);
        e := OriginalPositionDocument(orig, ex.stop);
        Add(b, e[2]);
      else
        b := [ex.start, ex.stop];
      fi;
      Add(l, [GetTextXMLTree(ex), b]);
    od;
    Add(res, l);
  od;
  return res;
end);

# obsolete
# compose and parse document, then extract examples units-wise
if MANEXreadobs then
InstallGlobalFunction(ManualExamples, function( path, main, files, units )
  local str, xmltree;
  str:= ComposedDocument( "GAPDoc", path, main, files, true );
  xmltree:= ParseTreeXMLString( str[1], str[2] );
  return ManualExamplesXMLTree(xmltree, units);
end);
fi;

# compose and parse document, then extract examples units-wise
InstallGlobalFunction(ExtractExamples, 
function( path, main, files, units, opt... )
  local str, xmltree, withLog;
  if Length(opt) > 0 and opt[1] = true then
    withLog := true;
  else
    withLog := false;
  fi;
  str:= ComposedDocument( "GAPDoc", path, main, files, true );
  xmltree:= ParseTreeXMLString( str[1], str[2] );
  return ExtractExamplesXMLTree(xmltree, units, withLog);
end);

##  <#GAPDoc Label="TestExamples">
##  <ManSection >
##  <Func Arg="str" Name="ReadTestExamplesString" />
##  <Returns><K>true</K> or <K>false</K></Returns>
##  <Func Arg="str[, print]" Name="TestExamplesString" />
##  <Returns><K>true</K> or a list of records</Returns>
##  <Func Arg="[tree][,][path, main, files]" Name="TestManualExamples" />
##  <Returns><K>true</K> or a list of records</Returns>
##  <Description>
##  The argument <A>str</A> must be a string containing lines for the test mode
##  of &GAP;. The function <Ref Func="ReadTestExamplesString"/> just runs 
##  <Ref BookName="Reference" Oper="ReadTest"/> on this code. <P/>
##  
##  The function <Ref Func="TestExamplesString"/> returns <K>true</K> if <Ref
##  BookName="Reference" Oper="ReadTest"/> does not find differences. In the
##  other case it returns a list of records, where each record describes one
##  difference. The records have fields <C>.line</C> with the line number of the
##  relevant input line of <A>str</A>, <C>.input</C> with the input line and
##  <C>.diff</C> with the differences as displayed by <Ref BookName="Reference"
##  Oper="ReadTest"/>. If the optional argument <A>print</A> is given and set 
##  to <K>true</K> then the differences are also printed before the function
##  returns.<P/>
##  
##  The arguments of the function <Ref Func="TestManualExamples"/> is either
##  a parse tree of a &GAPDoc; document or the information to build and parse
##  such a document. The function extracts all examples in <C>"Single"</C>
##  units and applies <Ref Func="TestExamplesString"/> to them.<P/>
##  
##  <Example>
##  gap> TestExamplesString("gap> 1+1;\n2\n");
##  true
##  gap> TestExamplesString("gap> 1+1;\n2\ngap> 2+3;\n4\n");
##  [ rec( diff := "+ 5\n- 4\n", input := "gap> 2+3;", line := 3 ) ]
##  gap> TestExamplesString("gap> 1+1;\n2\ngap> 2+3;\n4\n", true);
##  -----------  bad example --------
##  line: 3
##  input: gap> 2+3;
##  differences:
##  + 5
##  - 4
##  [ rec( diff := "+ 5\n- 4\n", input := "gap> 2+3;", line := 3 ) ]
##  </Example>
##  </Description>
##  </ManSection>
##  <#/GAPDoc>

# obsolete
# test a string with examples 
if MANEXreadobs then
InstallGlobalFunction(ReadTestExamplesString, function(str)
  local res, file;
  file := InputTextString(str);
  res := ReadTest(file);
  CloseStream(file);
  return res;
end);
fi;

# obsolete
# args:  str, print
if MANEXreadobs then
InstallGlobalFunction(TestExamplesString, function(arg)
  local l, s, z, inp, out, f, lout, pos, bad, i, n, diffs, str;
  str := arg[1];
  l := SplitString(str, "\n", "");
  s := "";
  for i in [1..Length(l)] do
    z := l[i];
    if Length(z) > 4 and z{[1..5]} = "gap> " or
       Length(z) > 1 and z{[1,2]} = "> " then
      Append(s, " #IPL");
      Append(s, String(i));
      Append(s, "--->");
      Append(s, z);
      Add(s, '\n');
    fi;
    Append(s, z);
    Add(s, '\n');
  od;
  inp := InputTextString(s);
  out := "";
  f := OutputTextString(out, false);
  PrintTo1(f, function()
##      READ_TEST_STREAM(inp);
    ReadTest(inp);
  end);
  if not IsClosedStream(inp) then
    CloseStream(inp);
  fi;
  if not IsClosedStream(f) then
    CloseStream(f);
  fi;
  lout := SplitString(out, "\n", "");
  pos := First([1..Length(lout)], i-> Length(lout[i]) > 0 and lout[i][1] = '+');
  if pos = fail then
    return true;
  fi;
  bad := [];
  while pos <> fail do
    i := pos-1;
    while Length(lout[i]) < 7 or lout[i]{[1..7]} <> "-  #IPL" do
      i := i-1;
    od;
    n := lout[i]{[8..Length(lout[i])]};
    n := Int(n{[1..Position(n, '-')-1]});
    diffs := "";
    while IsBound(lout[pos]) and 
           (Length(lout[pos]) < 7 or lout[pos]{[1..7]} <> "-  #IPL") do
      Append(diffs, lout[pos]);
      Add(diffs, '\n');
      pos := pos+1;
    od;
    Add(bad, rec(line := n, input := l[n], diff := diffs));
    pos := First([pos..Length(lout)], i-> Length(lout[i]) > 0 and
                  lout[i][1] = '+');
  od;
  if Length(arg) > 1 and arg[2] = true then
    for z in bad do
      Print("-----------  bad example --------\n",
            "line: ", z.line, "\ninput: ");
      PrintFormattedString(z.input);
      Print("\n");
      Print("differences:\n");
      PrintFormattedString(z.diff);
    od;
  fi;
  return bad;
end);
fi;

# obsolete
if MANEXreadobs then
InstallGlobalFunction(TestManualExamples, function(arg)
  local ex, bad, res, a;
  if IsRecord(arg[1]) then
    ex := ManualExamplesXMLTree(arg[1], "Single");
  else
    ex := ManualExamples(arg[1], arg[2], arg[3], "Single");
  fi;
  bad := Filtered(ex, a-> TestExamplesString(a) <> true);
  res := [];
  for a in bad do 
    Print("===========================\n");
    PrintFormattedString(a); 
    Add(res, TestExamplesString(a, true));
  od; 
  return res;
end);
fi;


##  <#GAPDoc Label="RunExamples">
##  <ManSection >
##  <Func Arg="exmpls[, optrec]" Name="RunExamples" />
##  <Returns><K>true</K> or <K>false</K></Returns>
##  <Description>
##  The argument <A>exmpls</A> must be the output of a call to 
##  <Ref Func="ExtractExamples"/> or <Ref Func="ExtractExamplesXMLTree"/>.
##  The optional argument <A>optrec</A> must be a record, its components
##  can change the default behaviour of this function.
##  <P/>
##  By default this function runs the &GAP; input of all examples and compares
##  the actual output with the output given in the examples. If differences
##  occur these are displayed together with information on the location of the 
##  source code of that example. Before running the examples in each unit (entry
##  of <A>exmpls</A>) the function <Ref BookName="Reference" Func="START_TEST"/>
##  is called and the screen width is set to 72 characters.
##  <P/>
##  This function returns <K>true</K> if no differences are found and
##  <K>false</K> otherwise.
##  <P/>
##  If the argument <A>optrec</A> is given, the following components are
##  recognized:
##  <List>
##  <Mark><C>showDiffs</C></Mark>
##  <Item>
##  The default value is <K>true</K>, if set to something else found differences
##  in the examples are not displayed. 
##  </Item>
##  <Mark><C>width</C></Mark>
##  <Item>
##  The value must be a positive integer which is used as screen width when
##  running the examples. As mentioned above, the default is 72 which is a
##  sensible value for the text version of the &GAPDoc; document used
##  in a 80 character wide terminal.
##  </Item>
##  <Mark><C>ignoreComments</C></Mark>
##  <Item>
##  The default is <K>false</K>.<Br/>
##  If set to <K>true</K> comments in the input will be ignored (as in the
##  default behaviour of the <Ref Func="Test" BookName="reference"/> function).
##  </Item>
##  <Mark><C>changeSources</C></Mark>
##  <Item>
##  If this is set to <K>true</K> then the source code of all manual
##  examples which show differences is adjusted to the current outputs.
##  The default is <K>false</K>.<Br/>
##  Use this feature with care.
##  Note that sometimes differences can indicate a bug, and in such a case
##  it is more appropriate to fix the bug instead of changing the example
##  output. 
##  </Item>
##  <Mark><C>compareFunction</C></Mark>
##  <Item>
##  The function used to compare the output shown in the example and the
##  current output. See <Ref BookName="Reference" Func="Test"/> for more
##  details.
##  </Item>
##  <Mark><C>checkWidth</C></Mark>
##  <Item>
##  If this option is a positive integer <C>n</C> the function prints warnings
##  if an example contains any line with more than <C>n</C> characters (input
##  and output lines are considered). By default this option is set to
##  <K>false</K>.
##  </Item>
##  </List>
##  
##  </Description>
##  </ManSection>
##  <#/GAPDoc>

InstallGlobalFunction(RunExamples, function(arg)
  local exlists, opts, oldscr, l, sp, bad, s, test, pex, new, inp, ch, 
        fnams, str, fch, pos, pre, a, j, ex, i, attedStrin, f, nodiffs;
  exlists := arg[1];
  opts := rec(
          showDiffs := true,
          changeSources := false,
          ignoreComments := false,
          width := 72,
          compareFunction := EQ,
          checkWidth := false,
  );                 
  nodiffs := true;
  if Length(arg) > 1 and IsRecord(arg[2]) then
    for a in RecNames(arg[2]) do
      opts.(a) := arg[2].(a);
    od;
  fi;
  if IsString(opts.compareFunction) then
    if IsBound(TEST.compareFunctions.(opts.compareFunction)) then
      opts.compareFunction := TEST.compareFunctions.(opts.compareFunction);
    else
      opts.compareFunction := EQ;
    fi;
  fi;
  oldscr := SizeScreen();
  SizeScreen([opts.width, oldscr[2]]);
  for j in [1..Length(exlists)] do
    l := exlists[j];
    Print("# Running list ",j," . . .\n");
    START_TEST("");
    for ex in l do
      if IsInt(opts.checkWidth) then
        sp := SplitString(ex[1], "\n", "");
        bad := Filtered([1..Length(sp)], i-> Length(sp[i]) > opts.checkWidth);
        if Length(bad) > 0 then
          Print("# WARNING: Overlong lines ", bad, 
                " in ", ex[2]{[1..3]}, "\n");
        fi;
      fi;
      s := InputTextString(ex[1]);
      test := Test(s, rec(ignoreComments := opts.ignoreComments,
                   width := opts.width,
                   compareFunction := opts.compareFunction,
                   reportDiff := Ignore
                                 ) );
      CloseStream(s);
      pex := TEST.lastTestData;
      if test = false then
        for i in [1..Length(pex[1])] do
          if opts.compareFunction(pex[2][i], pex[4][i]) <> true then
            nodiffs := false;
            if opts.showDiffs = true then
              Print("########> Diff in ", ex[2]{[1..3]}, "\n# Input is:\n");
              PrintFormattedString(pex[1][i]);
              Print("# Expected output:\n");
              PrintFormattedString(pex[2][i]);
              Print("# But found:\n");
              PrintFormattedString(pex[4][i]);
              Print("########\n");
            fi;
          fi;
        od;
      fi;
      if test = false then
        new := "";
        for i in [1..Length(pex[1])] do
          inp := Concatenation("gap> ", JoinStringsWithSeparator(
                    SplitString(pex[1][i], "\n", ""), "\n> "), "\n");
          Append(new, inp);
          Append(new, pex[4][i]);
        od;
        ex[2][4] := new;
      fi;
    od;
  od;
  if opts.changeSources = true then
    ch := [];
    for l in exlists do
      for ex in l do
        if IsString(ex[2][1]) and Length(ex[2]) > 3 then
          Add(ch, ex[2]);
        fi;
      od;
    od;
    if Length(ch) > 0 then
      Print("# Diffs found, changing source files ...\n");
      fnams := Set(List(ch, a-> a[1]));
      for f in fnams do
        Print("# Changing ",f,"\n");
        str := StringFile(f);
        if str = fail then
          Print("# WARNING: Cannot read file ",f,", skipping\n");
        else
          str := SplitString(str, "\n", "");
          for a in str do
            Add(a, '\n');
          od;
          fch := Filtered(ch, a-> a[1] = f);
          for ex in fch do
            # change first line to everything new and empty the remaining ones
            pos := PositionSublist(str[ex[2]], "<Example");
            pre := str[ex[2]]{[1..pos-1]};
            l := SplitString(ex[4], "\n", "");
            new := "";
            for a in l do
              Append(new, pre);
              Append(new, a);
              Add(new, '\n');
            od;
            # maybe escape & and <
            if PositionSublist(str[ex[2]], "<![CDATA[") = fail then
              new := SubstitutionSublist(new, "&", "&amp;");
              new := SubstitutionSublist(new, "<", "&lt;");
            fi;
            str[ex[2]+1] := new;
            for i in [ex[2]+2..ex[3]-1] do
              str[i] := "";
            od;
            Print("        changed lines ", ex[2]+1, "..", ex[3]-1, "\n");
          od;
          str := Concatenation(str);
          FileString(f, str);
        fi;
      od;
    fi;
  fi;
  SizeScreen(oldscr);
  return nodiffs;
end);

Unbind(MANEXreadobs);

