This is the mail archive of the
mauve-patches@sourceware.org
mailing list for the Mauve project.
FYI/RFC: Interactive GUI tests for Mauve
- From: Roman Kennke <roman dot kennke at aicas dot com>
- To: mauve-patches at sources dot redhat dot com, classpath at gnu dot org
- Date: Fri, 13 Oct 2006 13:52:23 +0200
- Subject: FYI/RFC: Interactive GUI tests for Mauve
Hi,
(I'm CC'ing classpath@gnu.org as this might be of more general interest)
This is a first implementation for visual interactive tests in Mauve.
This allows to write testcases that require human interaction, for
example:
- to test specific rendering issues of Swing components
- for complex issues that are not easily transformed in automated
testcases, but are quite easy to check interactivly
- quickly transform Swing testprograms from the bugdatabase into
regression tests
- and many more
In order to write such an interactive test, subclass from
gnu.testlet.VisualTestlet and implement the abstract methods.
To run interactive tests:
java Harness -interactive <tests>
The -interactive flag tells the TestRunner to only perform interactive
tests, leaving this flag away only performs only non-interactive tests.
The test asks the tester on the console if the test passed or failed. I
think that this is better than additional GUIness, because we are
testing the GUI here, and need to run with a minimum of additional GUI
stuff. This is slightly inconvenient, as it requires the tester to
change between a Terminal and the test window.
This adds an example test that I've written for a bug that I fixed this
morning.
Comments and improvements are welcome.
2006-10-03 Roman Kennke <kennke@aicas.com>
* Harness.java
(InputPiperThread): New inner class. Forwards the input from
the outside process to the inside (test) process.
(initProcess): Set up piping.
(printHelpMessage): Added description of -interactive option.
* RunnerProcess.java
(interactive): New field. This is set to true when we are running
interactive tests only.
(main): Interpret -interactive option.
(runtest): Filter tests based on the -interactive flag.
* gnu/testlet/VisualTestlet.java: New class. This is the
base class for visual (interactive) tests.
* gnu/testlet/javax/swing/JMenuItem/DragSelectTest.java: New
interactive test.
/Roman
Index: Harness.java
===================================================================
RCS file: /cvs/mauve/mauve/Harness.java,v
retrieving revision 1.25
diff -u -1 -5 -r1.25 Harness.java
--- Harness.java 16 Aug 2006 19:00:18 -0000 1.25
+++ Harness.java 13 Oct 2006 11:44:53 -0000
@@ -571,32 +571,33 @@
"tests before running them. This" + align + "overrides the configure" +
"option --disable-auto-compilation but requires an ecj jar" + align +
"to be in /usr/share/java/eclipse-ecj.jar or specified via the " +
"--with-ecj-jar" + align + "option to configure. See the README" +
" file for more details.\n" +
" -timeout [millis]: specifies a timeout value for the tests " +
"(default is 60000 milliseconds)" +
// Testcase Selection Options.
"\n\nTestcase Selection Options:\n" +
" -exclude [test|folder]: specifies a test or a folder to exclude " +
"from the run\n" +
" -norecursion: if a folder is specified to be run, don't " +
"run the tests in its subfolders\n" +
" -file [filename]: specifies a file that contains the names " +
- "of tests to be run (one per line)" +
-
+ "of tests to be run (one per line)\n" +
+ " -interactive: only run interavtice tests, if not set, " +
+ "only run non-interactive tests\n" +
// Output Options.
"\n\nOutput Options:\n" +
" -showpasses: display passing tests as well as failing " +
"ones\n" +
" -hidecompilefails: hide errors from the compiler when " +
"tests fail to compile\n" +
" -noexceptions: suppress stack traces for uncaught " +
"exceptions\n" +
" -verbose: run in noisy mode, displaying extra " +
"information\n" +
" -debug: displays some extra information for " +
"failing tests that " +
"use the" + align + "harness.check(Object, Object) method\n" +
" -xmlout [filename]: specifies a file to use for xml output\n" +
"\nOther Options:\n" +
@@ -632,30 +633,33 @@
{
StringBuffer sb = new StringBuffer(" RunnerProcess");
for (int i = 0; i < args.length; i++)
sb.append(" " + args[i]);
sb.insert(0, vmCommand + vmArgs);
try
{
// Exec the process and set up in/out communications with it.
runnerProcess = Runtime.getRuntime().exec(sb.toString());
runner_out = new PrintWriter(runnerProcess.getOutputStream(), true);
runner_in =
new BufferedReader
(new InputStreamReader(runnerProcess.getInputStream()));
runner_esp = new ErrorStreamPrinter(runnerProcess.getErrorStream());
+ InputPiperThread pipe = new InputPiperThread(System.in,
+ runnerProcess.getOutputStream());
+ pipe.start();
runner_esp.start();
}
catch (IOException e)
{
System.err.println("Problems invoking RunnerProcess: " + e);
System.exit(1);
}
// Create a timer to watch this new process. After confirming the
// RunnerProcess started properly, we create a new runner_watcher
// because it may be a while before we run the next test (due to
// preprocessing and compilation) and we don't want the runner_watcher
// to time out.
if (runner_watcher != null)
@@ -1563,16 +1567,59 @@
* printed out and also so that if the output is verbose we print
* our own summary.
*/
public void print(String x)
{
if (isCompileSummary(x))
{
if (verbose)
super.println("TEST FAILED: compile failed for "
+ lastFailingCompile);
}
else
super.print(x);
}
}
+
+ /**
+ * Reads from one stream and writes this to another. This is used to pipe
+ * the input (System.in) from the outside process to the test process.
+ */
+ private static class InputPiperThread
+ extends Thread
+ {
+ InputStream in;
+ OutputStream out;
+ InputPiperThread(InputStream i, OutputStream o)
+ {
+ in = i;
+ out = o;
+ }
+ public void run()
+ {
+ int ch = 0;
+ do
+ {
+ try
+ {
+ if (in.available() > 0)
+ {
+ ch = in.read();
+ if (ch != '\n') // Skip the trailing newline.
+ out.write(ch);
+ out.flush();
+ }
+ else
+ Thread.sleep(200);
+ }
+ catch (IOException ex)
+ {
+ ex.printStackTrace();
+ }
+ catch (InterruptedException ex)
+ {
+ ch = -1; // Jump outside.
+ }
+ } while (ch != -1);
+ }
+ }
}
Index: RunnerProcess.java
===================================================================
RCS file: /cvs/mauve/mauve/RunnerProcess.java,v
retrieving revision 1.14
diff -u -1 -5 -r1.14 RunnerProcess.java
--- RunnerProcess.java 4 Aug 2006 20:30:09 -0000 1.14
+++ RunnerProcess.java 13 Oct 2006 11:44:53 -0000
@@ -17,53 +17,54 @@
// You should have received a copy of the GNU General Public License
// along with Mauve; see the file COPYING. If not, write to
// the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
// Boston, MA 02110-1301 USA.
// This file is used by Harness.java to run the tests in a separate process
// so that the process can be killed by the Harness if it is hung.
import gnu.testlet.ResourceNotFoundException;
import gnu.testlet.TestHarness;
import gnu.testlet.TestReport;
import gnu.testlet.TestResult;
import gnu.testlet.TestSecurityManager;
import gnu.testlet.Testlet;
+import gnu.testlet.VisualTestlet;
import gnu.testlet.config;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Vector;
public class RunnerProcess
extends TestHarness
{
// A description of files that are not tests
private static final String NOT_A_TEST_DESCRIPTION = "not-a-test";
-
+
// A description of files that fail to load
private static final String FAIL_TO_LOAD_DESCRIPTION = "failed-to-load";
// A description of a test that throws an uncaught exception
private static final String UNCAUGHT_EXCEPTION_DESCRIPTION = "uncaught-exception";
// Total number of harness.check calls since the last checkpoint
private int count = 0;
// The location of the emma.jar file
private static String emmaJarLocation = null;
// Whether or not to use EMMA
private static boolean useEMMA = true;
@@ -100,31 +101,36 @@
// The TestReport if a report is necessary
private static TestReport report = null;
// The xmlfile for the report
private static String xmlfile = null;
// The result of the current test
private TestResult currentResult = null;
// The EMMA forced data dump method
private static Method emmaMethod = null;
// The failure message for the last failing check()
private String lastFailureMessage = null;
-
+
+ /**
+ * Should we run interactive or non-interactive tests ?
+ */
+ private static boolean interactive;
+
protected RunnerProcess()
{
try
{
BufferedReader xfile = new BufferedReader(new FileReader("xfails"));
String str;
while ((str = xfile.readLine()) != null)
{
expected_xfails.addElement(str);
}
}
catch (FileNotFoundException ex)
{
// Nothing.
}
@@ -159,30 +165,32 @@
{
// User wants a report.
if (++i >= args.length)
throw new RuntimeException("No file path after '-xmlout'.");
xmlfile = args[i];
}
else if (args[i].equalsIgnoreCase("-emma"))
{
// User is specifying the location of the eclipse-ecj.jar file
// to use for compilation.
if (++i >= args.length)
throw new RuntimeException("No file path " +
"after '-emma'. Exit");
emmaJarLocation = args[i];
}
+ else if (args[i].equals("-interactive"))
+ interactive = true;
}
// If the user wants an xml report, create a new TestReport.
if (xmlfile != null)
{
report = new TestReport(System.getProperties());
}
// Setup the data coverage dumping mechanism. The default configuration
// is to auto-detect EMMA, meaning if the emma classes are found on the
// classpath then we should force a dump of coverage data. Also, the user
// can configure with -with-emma=JARLOCATION or can specify -emma
// JARLOCATION on the command line to explicitly specify an emma.jar to use
// to dump coverage data.
if (emmaJarLocation == null)
emmaJarLocation = config.emmaString;
@@ -256,31 +264,46 @@
{
Class k = Class.forName(name);
int mods = k.getModifiers();
if (Modifier.isAbstract(mods))
{
description = NOT_A_TEST_DESCRIPTION;
return;
}
Object o = k.newInstance();
if (! (o instanceof Testlet))
{
description = NOT_A_TEST_DESCRIPTION;
return;
}
-
+ if (o instanceof VisualTestlet)
+ {
+ if (! interactive)
+ {
+ description = NOT_A_TEST_DESCRIPTION;
+ return;
+ }
+ }
+ else
+ {
+ if (interactive)
+ {
+ description = NOT_A_TEST_DESCRIPTION;
+ return;
+ }
+ }
t = (Testlet) o;
}
catch (Throwable ex)
{
description = FAIL_TO_LOAD_DESCRIPTION;
// Maybe the file was marked not-a-test, check that before we report
// it as an error
try
{
File f = new File(name.replace('.', File.separatorChar) + ".java");
BufferedReader r = new BufferedReader(new FileReader(f));
String firstLine = r.readLine();
// Since some people mistakenly put not-a-test not as the first line
// we have to check through the file.
while (firstLine != null)
Index: gnu/testlet/VisualTestlet.java
===================================================================
RCS file: gnu/testlet/VisualTestlet.java
diff -N gnu/testlet/VisualTestlet.java
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ gnu/testlet/VisualTestlet.java 13 Oct 2006 11:44:54 -0000
@@ -0,0 +1,131 @@
+/* VisualTestlet.java -- Abstract superclass for visual tests
+ Copyright (C) 2006 Roman Kennke (kennke@aicas.com)
+This file is part of Mauve.
+
+Mauve is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2, or (at your option)
+any later version.
+
+Mauve is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with Mauve; see the file COPYING. If not, write to the
+Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+02110-1301 USA.
+
+*/
+
+package gnu.testlet;
+
+import java.awt.Component;
+import java.awt.Frame;
+import java.io.IOException;
+
+import javax.swing.JComponent;
+import javax.swing.JFrame;
+
+/**
+ * Provides an environment for visual tests. Visual tests must provide a
+ * component, instructions and the expected results. The harness provides
+ * all three to the tester and ask if the test passed or not.
+ *
+ * The test component is displayed inside a AWT Frame or a Swing JFrame
+ * (depending on the type of the component). This means that the tested
+ * Java environment needs to have some basic AWT or Swing functionality. This
+ * should be covered by other tests (possibly by java.awt.Robot or so).
+ */
+public abstract class VisualTestlet
+ implements Testlet
+{
+
+ /**
+ * Starts the test.
+ *
+ * @param h the test harness
+ */
+ public void test(TestHarness h)
+ {
+ // Initialize and show test component.
+ Component c = getTestComponent();
+ Frame f;
+ if (c instanceof JComponent)
+ {
+ JFrame jFrame = new JFrame();
+ jFrame.setContentPane((JComponent) c);
+ f = jFrame;
+ }
+ else
+ {
+ f = new Frame();
+ f.add(c);
+ }
+ f.pack();
+ f.setVisible(true);
+
+ // Print instructions and expected results on console.
+ System.out.println("====================================================");
+ System.out.print("This is a test that needs human interaction. Please ");
+ System.out.print("read the instructions carefully and follow them. ");
+ System.out.print("Then check if your results match the expected results. ");
+ System.out.print("Type p <ENTER> if the test showed the expected results,");
+ System.out.println(" f <ENTER> otherwise.");
+ System.out.println("====================================================");
+ System.out.println("INSTRUCTIONS:");
+ System.out.println(getInstructions());
+ System.out.println("====================================================");
+ System.out.println("EXPECTED RESULTS:");
+ System.out.println(getExpectedResults());
+ System.out.println("====================================================");
+
+ // Ask the tester whether the test passes or fails.
+ System.out.println("(P)ASS or (F)AIL ?");
+ while (true)
+ {
+ int ch;
+ try
+ {
+ ch = System.in.read();
+ if (ch == 'P' || ch == 'p')
+ {
+ h.check(true);
+ break;
+ }
+ else if (ch == 'f' || ch == 'F')
+ {
+ h.check(false);
+ break;
+ }
+ }
+ catch (IOException ex)
+ {
+ h.debug(ex);
+ h.fail("Unexpected IO problem on console");
+ }
+ }
+ }
+
+ /**
+ * Provides the instructions for the test.
+ *
+ * @return the instructions for the test
+ */
+ public abstract String getInstructions();
+
+ /**
+ * Describes the expected results.
+ *
+ * @return the expected results
+ */
+ public abstract String getExpectedResults();
+
+ /**
+ * Provides the test component.
+ *
+ * @return the test component
+ */
+ public abstract Component getTestComponent();
+}
Index: gnu/testlet/javax/swing/JMenuItem/DragSelectTest.java
===================================================================
RCS file: gnu/testlet/javax/swing/JMenuItem/DragSelectTest.java
diff -N gnu/testlet/javax/swing/JMenuItem/DragSelectTest.java
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ gnu/testlet/javax/swing/JMenuItem/DragSelectTest.java 13 Oct 2006 11:44:54 -0000
@@ -0,0 +1,84 @@
+/* DragSelectTest.java -- Tests if drag selection works
+ Copyright (C) 2006 Roman Kennke (kennke@aicas.com)
+This file is part of Mauve.
+
+Mauve is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2, or (at your option)
+any later version.
+
+Mauve is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with Mauve; see the file COPYING. If not, write to the
+Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+02110-1301 USA.
+
+*/
+
+// Tags: JDK1.2 manual
+
+package gnu.testlet.javax.swing.JMenuItem;
+
+import java.awt.Component;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+import javax.swing.JLabel;
+import javax.swing.JMenu;
+import javax.swing.JMenuBar;
+import javax.swing.JMenuItem;
+import javax.swing.JRootPane;
+
+import gnu.testlet.VisualTestlet;
+
+public class DragSelectTest extends VisualTestlet
+{
+ public String getInstructions()
+ {
+ return "Press the mouse on the 'Menu' menu, hold the button pressed and "
+ + "drag it to one of the menu items. Then release the mouse "
+ + "button";
+ }
+
+ public String getExpectedResults()
+ {
+ return "The menu should be closed and the name of the menu item shown in "
+ + "the panel below";
+ }
+
+ public Component getTestComponent()
+ {
+ JRootPane rp = new JRootPane();
+ JMenuBar mb = new JMenuBar();
+ JMenu menu = new JMenu("Menu");
+ final JLabel label =
+ new JLabel("The selected menu item should show up here");
+ ActionListener l = new ActionListener()
+ {
+ public void actionPerformed(ActionEvent ev)
+ {
+ JMenuItem i = (JMenuItem) ev.getSource();
+ label.setText(i.getText());
+ }
+ };
+
+ JMenuItem item1 = new JMenuItem("MenuItem 1");
+ item1.addActionListener(l);
+ JMenuItem item2 = new JMenuItem("MenuItem 2");
+ item2.addActionListener(l);
+ JMenuItem item3 = new JMenuItem("MenuItem 3");
+ item3.addActionListener(l);
+ menu.add(item1);
+ menu.add(item2);
+ menu.add(item3);
+ mb.add(menu);
+ rp.setJMenuBar(mb);
+ rp.getContentPane().add(label);
+ return rp;
+ }
+
+}