package tester;

import java.lang.reflect.*;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.*;
import java.io.*;

/**
 * Copyright 2007, 2008 Viera K. Proulx
 * This program is distributed under the terms of the 
 * GNU Lesser General Public License (LGPL)
 */

/**
 * <P>A test harness that compares arbitrary objects for 
 * extensional equality. </P>
 * <P>It catches exceptions in tests - the remaining tests are omitted.</P>
 * <P>It uses the visitor pattern to accept test cases.</P>
 * <P>Displays the data even if no tests are present.</P>
 * 
 * @author Viera K. Proulx
 * @since 21 March 2008, 11 June 2008, 24 June 2008, 16 October 2008
 */
public class Tester{

  /** A <code>String</code> that records the results for all failed tests */
  protected String testResults = "Test results: \n--------------\n";

  /** A <code>String</code> that records all test results */
  protected String fullTestResults = "Full test results: \n-------------------\n";

  /** the total number of tests */
  protected int n;			        
  
  /** the total number of errors */
  protected int errors;
  
  /** the name of the current test */
  protected String testname;       

  /** An instance of the Inspector to use throughout */
  protected Inspector inspector = new Inspector();
  
  /** start with no tests and no failures */
  protected Tester() {
    this.n = 0; 
    this.errors = 0;
    this.testname = "";
  }

  /*--------------------------------------------------------------------*/
  /*------------- TEST SELECTION AND REPORTING SECTION -----------------*/
  /*--------------------------------------------------------------------*/
  /**
   * <p>Super magic happens here....</p>
   * <p>Report only the results of all failed tests in the 
   * class whose instance is given as the parameter.</p>
   * 
   * @param f the instance of a class that defines all tests 
   * and the data for them
   */
  protected void runAnyTests(Object f){
    runAnyTests(f, false);
  }

  /**
   * <p>Super magic happens here....</p>
   * <p>Report only the results of tests in the class whose instance 
   * is given as the parameter.</p>
   * <p>If the class extends the <code>IExamples</code> all tests within 
   * the <code>test</code> method are performed. Otherwise we locate all 
   * methods with names that start with <code>test</code> and consume one
   * parameter of the type <code>Tester</code> and perform the desired
   * tests.</p>
   * 
   * @param f the instance of a class that defines all tests 
   * and the data for them
   * @param full true if full test report is desired
   */
  protected void runAnyTests(Object f, boolean full){
    this.n = 0;               // number of tests run
    boolean failed = false;   // any tests failed?

    // pretty-print the 'Examples' class data
    System.out.println(f.getClass().getName() + ":\n---------------");
    System.out.println(Printer.produceString(f) + "\n---------------");

    // check if the 'Examples' class extends 'IExamples'
    // if yes, run its 'tests' method
    Class[] interfaces = f.getClass().getInterfaces();
    boolean foundInterface = false;
    for (int i =0; i < Array.getLength(interfaces); i++){
      if (interfaces[i].getName().equals("tester.IExamples")){
        runTests((IExamples)f);
        foundInterface = true;
      }
    }

    // otherwise use reflection to find all test methods 
    if (!foundInterface){

      // find all methods that start with the String 'test...'
      // and consume one argument of the type Tester
      Class[] classparams = new Class[]{this.getClass()};
      ArrayList<Method> testMethods= this.findTestMethods(f, 
          "Your class does not define any method with the header\n" +
      " boolean test...(Tester t)");

      // make sure there are tests to run
      if (!(testMethods == null)){
	      // invoke every method that starts with 'test' 
	      // and accepts one Tester argument (ignore the resulting boolean)
	      Object[] args = new Object[]{this};
	      
	      try {      
	        for (Method testMethod: testMethods){
	          testMethod.invoke(f, args);
	        }
	      }
	
	      // catch all exceptions
	      catch (Throwable e) {  
	        this.errors = this.errors + 1;
	        System.out.println("Threw exception during test " + this.n);
	        e.printStackTrace();
	        failed = true;
	      }
	
	      // print the test results at the end
	      finally {
	        if (full)
	          this.fullTestReport();
	        else
	          this.testReport();
	        done(failed);
	      }
      }
    }
  }

 
  /**
   * <P>Run the tests, accept the class to be tested as a visitor</P>
   * <P>An alternative method for running all tests.</p>
   * 
   * @param f the class to be tested -- it defines the method 
   * <code>void tests(Tester t)</code> that invokes all test cases.
   * @return true if all tests succeeded
   */
  protected void runTests(IExamples f) {
    runTests(f, false);
  }
  
  /**
   * <P>Run the tests, accept the class to be tested as a visitor</P>
   * <P>An alternative method for running all tests.</p>
   * 
   * @param f the class to be tested -- it defines the method 
   * <code>void tests(Tester t)</code> that invokes all test cases.
   * @param full true if full test report is desired
   * @return true if all tests succeeded
   */
  protected void runTests(IExamples f, boolean full) {
    this.n = 0;
    boolean failed = false;

    System.out.println("Examples class:\n---------------");
    System.out.println(Printer.produceString(f) + "\n---------------");
    
    try {
      f.tests(this);
    }
    catch (Throwable e) {  // catch all exceptions
      this.errors = this.errors + 1;
      System.out.println("Threw exception during test " + this.n);
      e.printStackTrace();
      failed = true;
    }
    finally {
      if (full)
        this.fullTestReport();
      else
        this.testReport();
      done(failed);
    }
  }

  /**
   * On completion of all tests print the count of number of tests 
   * and the number of failures.
   */
  protected void done(boolean failed){
    if (failed){
      this.reportErrors(testname, "caused RuntimeException", "unkown");
   }
    /*
    if (this.errors > 0)
      System.out.print("Test summary:\nFailed " + this.errors + " out of ");
    else
      System.out.print("Test summary:\nPassed all ");
    System.out.println (this.n + " tests.");
    */
  }

  /*--------------------------------------------------------------------*/
  /*------------- TEST EVALUATION SECTION: Public API ------------------*/
  /*--------------------------------------------------------------------*/

  /*--------------- Tolerance for comparing inexact numbers  -----------*/

  /**
   * Set the relative tolerance for the comparison of inexact numbers
   * @param epsilon the desired tolerance
   */
  public boolean setTolerance(double epsilon){
    Inspector.TOLERANCE = epsilon;
    return epsilon > 0;
  }


  /*-------- Delegate the 'same' method to the Inspector class ---------*/

  /**
   * <P>Provide a general extensional equality comparison for 
   * arbitrary pair of objects</P>
   * <P>Inspector invokes the user-defined <code>same</code> method, if the 
   * corresponding classes implement the <code>ISame</code> interface.</P>
   * 
   * @param obj1 the first object
   * @param obj2 the second object
   * @return true if the two objects represent the same data
   */
  public boolean same(Object obj1, Object obj2){
    return inspector.isSame(obj1, obj2);
  }

  /*------------------------ Test definitions  -------------------------*/

  /**
   * Test that only reports success or failure
   * 
   * @param result the test result
   */
  public boolean checkExpect(boolean result){
    return checkExpect(result, "");
  }

  /**
   * Test that only reports success or failure
   * 
   * @param testname the name of this test
   * @param result the test result
   */
  public boolean checkExpect(boolean result, String testname){
    this.testname = testname;
    if (!result){
      // add the report of failure
      return this.report(false,
               testname + ": error -- no blame -- \n",
               false, true);
    }
    // add the report of success
    else{
      return this.addSuccess(
               testname + ": success \n");
    }
  }
  
  /**
   * Test that compares two objects of any kind
   * 
   * @param <T> the type of the objects being compared
   * @param actual the computed value of the type T
   * @param expected the expected value of the type T
   */
  public <T>  boolean checkExpect(T actual, T expected){ 
    return checkExpect(actual, expected, "");
  }
  /**
   * Test that compares two objects of any kind
   * 
   * @param <T> the type of the objects being compared
   * @param testname the name of this test
   * @param actual the computed value of the type T
   * @param expected the expected value of the type T
   */
  public <T>  boolean checkExpect(T actual, T expected, String testname){ 
    this.testname = testname;
    return this.report(inspector.isSame(actual, expected), 
             testname, actual, expected);
  }
 
  /**
   * Test that verifies that when the given object invokes the given
   * method with the given arguments, it throws the expected exception
   * with the expected message.
   * 
   * @param <T> The type of the object that invokes the given method
   * @param object the object that invokes the method to be tested
   * @param method the name of the method to test
   * @param args an array of arguments for the method - use Array of length 0
   * if no arguments are needed
   * @param e an instance of the expected exception -- including the 
   * expected message
   */
  public <T>  boolean checkExpect(T object, String method, 
                          Object[] args, Exception e){
    return checkExpect(object, method, args, e, "");
   }
  
   /**
   * Test that verifies that when the given object invokes the given
   * method with the given arguments, it throws the expected exception
   * with the expected message.
   * 
   * @param <T> The type of the object that invokes the given method
   * @param object the object that invokes the method to be tested
   * @param method the name of the method to test
   * @param args an array of arguments for the method - use Array of length 0
   * if no arguments are needed
   * @param e an instance of the expected exception -- including the 
   * expected message
   * @param testname the description of this test
   */
  public <T>  boolean checkExpect(T object, String method, 
                          Object[] args, Exception e, String testname){ 
    this.testname = testname;
    
    // create an Array of the argument types
    int length = Array.getLength(args);
    Class[] parameters = new Class[length];

    for (int i = 0; i < length; i++){
      parameters[i] = args[i].getClass();
    }
    
    // get the type of the expected exception
    Class exceptClass = e.getClass();
    String exceptName = exceptClass.getName();
    String exceptMessage = e.getMessage();
    try {  
      // find the method
      Method meth = this.findMethod(object, method, parameters, testname);
      
       // allow access to protected and private methods
      meth.setAccessible(true);
      
      Object result = meth.invoke(object, args);
      
      // if the invocation succeeds, the test fails because
      // it does not throw the expected exception
     if (result == null)
        return this.report(false, testname + 
            "\n invocation did not throw any exception " +
              "\n  method name: " + meth.getName() +
              "\n  object class: " + object.getClass().getName() + 
              "\n  result: void" +
              ": " + Printer.produceString(result) + 
              "\n  expected exception was: \n    class: " 
              + exceptName + 
              "\n    message: " + exceptMessage + "\n",
              result, "exception expected");
      else
      return this.report(false, testname + 
          "\n invocation did not throw any exception " +
            "\n  method name: " + meth.getName() +
            "\n  object class: " + object.getClass().getName() + 
            "\n  result: " + result.getClass().getName()  +
            ": " + Printer.produceString(result) + 
            "\n  expected exception was: \n    class: " 
            + exceptName + 
            "\n    message: " + exceptMessage + "\n",
            result, "exception expected");
    }
    catch (Throwable exception){
      
      String excName;
      String excMessage;
      if (exception.getCause() != null){
        excName = exception.getCause().getClass().getName();
        excMessage = exception.getCause().getMessage();
      }
      else{
        excName = exception.getClass().getName();
        excMessage = exception.getMessage();
      }
      
      // check that the method threw an exception of the desired type
      if (excName.equals(exceptName)){
        
        // check whether the message is correct
        if (excMessage.equals(exceptMessage)){
          
          // report correct exception, correct message -- test succeeds
          return this.addSuccess(testname + 
              "\n correct exception: \n" + "" +
                " class: " + exceptName +
              "\n correct message: " + exceptMessage + "\n");
        }
        else{
          
          // report correct exception, incorrect message -- test fails
          return this.report(false, testname + 
              "\n correct exception: \n" + "" +
              " class: " + exceptName +
              "\n incorrect message: " + excMessage + "\n",
              "message produced: " + excMessage,
              "message expected: " + exceptMessage);
        }
      }
      else{
        // report that method invocation threw an exception of the wrong type
        return this.report(false, testname + 
            "\n incorrect exception was thrown: ",
            "exception thrown:   " + excName,
            "exception expected: " + exceptName);
      }
    }
  }

  //--------- METHOD INVOCATION TESTS -------------------------------------
  /**
   * Invoke the method with the given name on a given object
   * with the given arguments - check if it produces the 
   * expected value
   * 
   * @param <T> The type of the object that invokes the given method
   * @param object the object that invokes the method to be tested
   * @param method the name of the method to test
   * @param args an array of arguments for the method - use Array of length 0
   * if no arguments are needed
   * @param expected 
   */
  public <T>  boolean checkExpect(T object, String method, 
      Object[] args, Object expected){ 
    return checkExpect(object, method, args, expected, "");
  }
  
  /**
   * Invoke the method with the given name on a given object
   * with the given arguments - check if it produces the 
   * expected value
   * 
   * @param <T> The type of the object that invokes the given method
   * @param testname the description of this test
   * @param object the object that invokes the method to be tested
   * @param method the name of the method to test
   * @param args an array of arguments for the method - use Array of length 0
   * if no arguments are needed
   * @param expected 
   */
  public <T>  boolean checkExpect(T object, String method, 
      Object[] args, Object expected, String testname){ 
    this.testname = testname;
    // create an Array of the argument types
    int length = Array.getLength(args);
    Class[] parameters = new Class[length];
    String[] paraNames = new String[length];

    for (int i = 0; i < length; i++){
      parameters[i] = args[i].getClass();
    }
    
    String parlist = "(";
    for (int i = 0; i < length; i++){
      parlist = parlist + (parameters[i].getName()) + ","; 
    }
    parlist = parlist.substring(0, parlist.length()-1) + ")";
   
    
    try {
      // find the method to be invoked by 'object' with the name 'method'
      // and parameters in the classes given by 'parameters'
      Method meth = findMethod(object, method, parameters, testname);
      
      if (meth == null){
        return report(false, 
               testname + "\nNo method with the name " + method + " found\n",
               "Failed to invoke method " + object.getClass().getName() + 
                   "." + method + parlist,
               expected);
      }
      else{
      // allow access to protected and private methods
      meth.setAccessible(true);

      String testmessage = testname + "\n" + 
        Printer.produceString(object) + "\n invoked method " + method +
        " in the class " + object.getClass().getName() + 
        "\n with arguments " + Printer.produceString(args) +"\n";
      
      return checkExpect(meth.invoke(object, args), expected, testmessage);
      }
    }
    catch (Throwable exception){
      //String testmessage = testname + "\n" + 
      String testmessage = testname + "\n" + 
      Printer.produceString(object) + "\n invoked method " + method +
      " in the class " + object.getClass().getName() + 
      "\n with arguments " + Printer.produceString(args) + "\n";

      // report that the method threw an exception 
      boolean result = this.report(false, 
          testmessage + "\nthrew an excception ", 
          object, Printer.produceString(args));
      exception.printStackTrace();
      return result;
    }       
  }
 
  /**
   * Test that determines whether the value of the given object 
   * (of any kind) is the same as one of the expected values
   * 
   * @param <T> the type of the objects being compared
   * @param actual the computed value of the type T
   * @param expected the expected values of the type T
   */
  public <T>  boolean checkOneOf(T actual, T[] expected){ 
    return checkOneOf(actual, expected, "");
  }
  
  /**
   * Test that determines whether the value of the given object 
   * (of any kind) is the same as one of the expected values
   * 
   * @param <T> the type of the objects being compared
   * @param testname the name of this test
   * @param actual the computed value of the type T
   * @param expected the expected values of the type T
   */
  public <T>  boolean checkOneOf(T actual, T[] expected, String testname){ 
    this.testname = testname;
    for (int i = 0; i < Array.getLength(expected); i++){
      if (inspector.isSame(actual, expected[i]))
        return this.report(true, testname, actual, expected[i]);
    }
    return this.report(false, 
        testname + 
            "\nNo matching value found among the list of expected values", 
        actual, expected);
  }

 
  /*--------------------------------------------------------------------*/
  /*---------- HELPERS FOR THE TEST EVALUATION SECTION -----------------*/
  /*--------------------------------------------------------------------*/
  
  /**
   * Find the method with the given name for the type of the given object,
   * that consumes parameters of the given types. 
   * Account for any autoboxing of primitive types
   * 
   * @param object the object expected to invoke the method
   * @param method the name of the method to invoke
   * @param parameters the parameter types for the method invocation
   * @return the instance of the declared method
   */
  private <T> Method findMethod(T object, String method, Class[] parameters, 
                               String testname){
    
      Method[] allMethods = object.getClass().getDeclaredMethods();
      ArrayList<Method> allNamed = new ArrayList<Method>();
      
      // make a list of all methods with the given name
      for (int i = 0; i< allMethods.length; i++){
        if (allMethods[i].getName().equals(method))
          allNamed.add(allMethods[i]);
      }
      
      /// add the test to compare parameters -- invocation with int works!!
      if (allNamed.size() > 0){
        for (Method m: allNamed){
          if (this.matchParams(m.getParameterTypes(), parameters))
            return m;                     
        }  
        // no methods matched the given parameter list
        testname = testname + "\nNo method with the name " + method + 
                              " had a matching argument list\n";
        return null;
      }
      // no methods found with the given name
      else{
        testname = testname + "\nNo method with the name " + method + " found\n";
        return null;
      }
  }
  
  /**
   * Find all test methods (with the name that starts with test...)
   * in the class determined by the instance of the given object,
   * that consumes a <code>Tester</code> argument. 
   * 
   * @param object the object expected to invoke the method
   * @param testname the <code>String</code> that records test diagnosis info
   * @return an <code>ArrayList</code> of all test methods
   */
  private <T> ArrayList<Method> findTestMethods(T object, String testname){
    
      Method[] allMethods = object.getClass().getDeclaredMethods();
      ArrayList<Method> allNamed = new ArrayList<Method>();
      Class[] testerParam = new Class[]{this.getClass()};
           
      // make a list of all methods with the given name
      for (int i = 0; i< allMethods.length; i++){
        if (allMethods[i].getName().startsWith("test") &&
            this.matchParams(allMethods[i].getParameterTypes(), testerParam)){
          allNamed.add(allMethods[i]);
          allMethods[i].setAccessible(true);
        }
      }
      System.out.println("Found " + allNamed.size() + " test methods");

      if (allNamed.size() >0){
        // found test methods that matched the given parameter list
        testname = testname + "Found " + allNamed.size() + " test methods";

        return allNamed;
      }
      // no test methods that matched the given parameter list
      else{
        testname = testname + "\nNo method with the name test..." +  
        " found in the class " + object.getClass().getName() + "\n";
        return null;
      }
  }
  

  /**
   * See if the list of <CODE>Class</CODE instances
   *   that represent the parameter list for the method to invoke
   *   matches the method definition with the same name
   * 
   * @param parInput <CODE>Array</CODE> of <CODE>Class</CODE instances
   *   that represent the input parameter list
   * @param parDefined <CODE>Array</CODE> of <CODE>Class</CODE instances
   *   that represent the parameter list for a method with matching name
   * @return true if the parameter lists represent the same classes
   *   allowing for primitive types to match their wrapper classes.
   */
  private boolean matchParams(Class[] parInput, Class[] parDefined){
    if (Array.getLength(parInput) != Array.getLength(parDefined))
      return false;
    else {
      for (int i = 0; i < Array.getLength(parInput); i++){
        String in = parInput[i].getName();
        String def = parDefined[i].getName();
        if (!in.equals(def)){
          if (Inspector.isWrapperClass(def)){
            if (!isWrapperMatch(in, def))
              return false;
            else
              return true;
          }
          return false;
        }
      }
    }
    return true;
  }
  
  /**
   * Check whether the primitive type with the name <CODE>in</CODE>
   * matches the wrapper type with the name <CODE>def</CODE>.
   * 
   * @param in the name of the primitive type to match
   * @param def the name of the wrapper class to match
   * @return true if the primitive type matches its wrapper class
   */
  private boolean isWrapperMatch(String in, String def){
    if (def.equals("java.lang.Integer") && in.equals("int"))
      return true;
    else if (def.equals("java.lang.Short") && in.equals("short"))
      return true;
    else if (def.equals("java.lang.Long") && in.equals("long"))
      return true;
    else if (def.equals("java.lang.Byte") && in.equals("byte"))
      return true;
    else if (def.equals("java.lang.Character") && in.equals("char"))
      return true;
    else if (def.equals("java.lang.Double") && in.equals("double"))
      return true;
    else if (def.equals("java.lang.Float") && in.equals("float"))
      return true;
    else if (def.equals("java.lang.Double") && in.equals("double"))
      return true;
    else if (def.equals("java.lang.Boolean") && in.equals("boolean"))
      return true;
    else
      return false;
  }
  
  

  /*--------------------------------------------------------------------*/
  /*--------------------- TEST REPORTING SECTION -----------------------*/
  /*--------------------------------------------------------------------*/

  /**
   * General contractor to report test results
   * 
   * @param success did the test succeed?
   * @param testname the name of this test
   * @param actual the computed value of the result
   * @param expected the expected value of the result
   */
  private boolean report(boolean success, String testname, 
      Object actual, Object expected){
    if (success)
      return this.reportSuccess(testname, actual, expected);
    else{
      // get the relevant stack trace for this test case
      String trace = this.getStackTrace();
      return this.reportErrors(testname + "\n" + trace, actual, expected);
      
    }
  }

  /**
   * Add a test to the list of failed tests
   * 
   * @param testname the name of the failed test
   * @param actual the computed value of the test
   * @param expected the expected value of the test
   */
  private boolean reportErrors(String testname, 
      Object actual, 
      Object expected){

    // add test report to the error report and the full test report
    return this.addError(
        "Error in test number " + (n + 1) + "\n" + testname + "\n" +
        "actual:     " + Printer.produceString(actual) + "\n" +
        "expected:   " + Printer.produceString(expected) + "\n");
  }

  /**
   * Produce a formatted String that represent the relevant entries
   * of the <CODE>StackTrace</CODE> that provides a link to the 
   * test case that produced the error.
   * 
   * @return a formatted String representation of the relevant stack trace
   */
  private String getStackTrace(){
    try {
      // force an exception so you can inspect the stack trace
      throw new ErrorReport("Error trace:");
    }
    catch (ErrorReport e) {            // catch the exception
      // record the original stack trace
      StackTraceElement[] ste = e.getStackTrace();
      
      // copy only the relevant entries
      int length = Array.getLength(ste);
      StackTraceElement[] tmpSTE = new StackTraceElement[length];
      int ui = 0;  // index for the stripped stack trace
      for (int i = 3; i < length; i++){
        String cname = ste[i].getClassName();
        if (!((cname.startsWith("tester.")) ||
             (cname.startsWith("sun.reflect")) ||
             (cname.startsWith("java.lang")) ||
             (cname.startsWith("bluej")) ||
             (cname.startsWith("__SHELL")))){
          tmpSTE[ui] = ste[i];
          ui = ui + 1;
        }
      }  
      
      // we cannot have null entries at the end
      StackTraceElement[] userSTE = new StackTraceElement[ui];
      for (int i = 0; i < ui; i++){
        userSTE[i] = tmpSTE[i];
      }
      
      // now set the stack trace so it can be converted to a String
      e.setStackTrace(userSTE);
      
      StringWriter writer = new StringWriter();      
      PrintWriter printwriter = new PrintWriter(writer);     
      e.printStackTrace(printwriter);
      
      return writer.toString();
    }
  }

  /**
   * Add a test to the list of successful tests
   * 
   * @param testname the name of the successful test
   * @param actual the computed value of the test
   * @param expected the expected value of the test
   */
  private boolean reportSuccess(String testname, 
      Object actual, 
      Object expected){

    // add test report to the full test report
    return this.addSuccess(
        "Success in the test number " + (n + 1) + "\n" + testname + "\n" +
        "actual:     " + Printer.produceString(actual) + "\n" +
        "expected:   " + Printer.produceString(expected) + "\n");
  }

  /**
   * Add the given test successful test result to the full report 
   * @param testResult
   */
  private boolean addSuccess(String testResult){
    this.n = this.n + 1;
    this.fullTestResults = this.fullTestResults + "\n" + testResult;
    // update the count of all tests
    return true;
  }

  /**
   * Add the given test failed test result to the full report and 
   * to the error report
   * @param testResult
   */
  private boolean addError(String testResult){
//  update the count of all tests
    this.n = this.n + 1;
    //  update the count of the errors tests
    this.errors = this.errors + 1; 
    this.testResults = this.testResults + "\n" + testResult;
    this.fullTestResults = this.fullTestResults + "\n" + testResult;
    return false;
  }
  
  /**
   * Produce a <code>String</code> describing the number of tests that
   * were run and that failed.
   */
  private String testCount(){
    String tCount = "";
    // report test totals
    if (this.n == 1){
      tCount = "\nRan 1 test.\n";
    }
    else if (this.n > 1){
      tCount = "\nRan " + n + " tests.\n";
    }
    
    // report error totals
    if (this.errors == 0){
      tCount = tCount + "All tests passed.\n\n";
    }
    if (this.errors == 1){
      tCount = tCount + "1 test failed.\n\n";
      
    }
    else if (this.errors > 1){
      tCount = tCount + this.errors + " tests failed.\n\n";
    }
    
    return tCount;
  }


  /**
   * Report on the number and nature of failed tests
   */
  protected void testReport(){
    System.out.println(testCount() + this.testResults + 
        "\n--- END OF TEST RESULTS ---");
  }

  /**
   * Produce test names and values compared for all tests
   */
  protected void fullTestReport(){
    System.out.println(testCount() + this.fullTestResults + 
    "\n--- END OF FULL TEST RESULTS ---");
  }
  
  /*--------------------------------------------------------------------*/
  /*-------------- TESTER INVOCATION HELPERS SECTION -------------------*/
  /*--------------------------------------------------------------------*/
  
  /**
   * A hook to run the tester for any object 
   * --- needed to run from BlueJ
   * @param obj the 'Examples' class instance where the tests are defined
   */
  public static void run(Object obj){
    Tester t = new Tester();
    t.runAnyTests(obj);
  }
  
  /**
   * A hook to run the tester for any object and produce a full test report
   *
   * @param obj the 'Examples' class instance where the tests are defined
   */
  public static void runFullReport(Object obj){
    Tester t = new Tester();
    t.runAnyTests(obj, true);
  }
}
