/* $Id$ */ /** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.manifoldcf.core.tests; import java.io.*; import java.nio.charset.StandardCharsets; import java.util.*; import org.junit.*; /** This tester sets up a virtual browser and allows a sequence of testing to take place. It's set * up to allow this to be done in Java, even though the tester itself may well be running in Python. * The eventual goal is to replace the Python browser emulator with a Java one, but we can't get there * all in one goal. * * The paradigm used is one of a "virtual browser", which basically handles multiple windows and can * emulate user activities, such as clicking a link or a button, filling in a field, etc. Identification * of each of these elements may have a language dependence, so I would anticipate that there would * need to be a new set of tests for each localization we have. Presumably it should be possible * to come up with a structure at a level above this one in order to meet the goal of having the * same test in a different language, so I'm not going to worry about that here. * * The tester works by basically accumulating a set of "instructions", and then firing them off at the * end. This set of instructions is then executed in an appropriate environment, and test feedback is * returned. */ public class HTMLTester { protected File currentTestFile = null; protected OutputStream currentOutputStream = null; protected BufferedWriter currentWriter = null; protected int variableCounter; protected int currentIndentLevel; protected String virtualBrowserVarName; /** Constructor. Create a test sequence object. */ public HTMLTester() { } /** Set up for all tests. Basically this grabs the necessary stuff out of resources * and writes it to the current directory. */ @Before public void setup() throws Exception { copyResource("VirtualBrowser.py"); copyResource("Javascript.py"); // Delete any test files hanging around from before new File("test.py").delete(); } protected void copyResource(String resName) throws Exception { OutputStream os = new FileOutputStream(new File(resName)); try { InputStream is = getClass().getResourceAsStream(resName); try { BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os, StandardCharsets.UTF_8)); BufferedReader br = new BufferedReader(new InputStreamReader(is,StandardCharsets.UTF_8)); while (true) { String line = br.readLine(); if (line == null) break; bw.write(line); bw.newLine(); } bw.flush(); } finally { is.close(); } } finally { os.close(); } } /** Clean up the files we created. */ @After public void teardown() throws Exception { closeAll(); new File("Javascript.py").delete(); new File("VirtualBrowser.py").delete(); } /** Test to test the tester. */ @Test public void TesterTest() throws Exception { newTest(Locale.US); executeTest(); } /** Close the current output. */ protected void closeAll() throws Exception { if (currentWriter != null) { currentWriter.flush(); currentWriter = null; } if (currentOutputStream != null) { currentOutputStream.close(); currentOutputStream = null; } } /** Begin a new test. Call this when we're ready to start building a new UI test. */ public void newTest(Locale desiredLocale) throws Exception { currentTestFile = new File("test.py"); currentOutputStream = new FileOutputStream(currentTestFile); currentWriter = new BufferedWriter(new OutputStreamWriter(currentOutputStream,"ASCII")); variableCounter = 0; currentIndentLevel = 0; virtualBrowserVarName = getNextVariableName(); emitLine("import time"); emitLine("import sys"); emitLine("sys.path.append(\".\")"); emitLine("import VirtualBrowser"); emitLine("if __name__ == '__main__':"); currentIndentLevel++; emitLine("print 'Starting test'"); emitLine(virtualBrowserVarName + " = VirtualBrowser.VirtualBrowser("+quotePythonString(desiredLocale.toString().replace("_","-"))+")"); } /** Execute the test. The virtual browser will be called and will perform the sequence of * activity described by the test. If at any point an error occurs, an appropriate exception * will be thrown, with sufficient description to (hopefully) permit the problem to be tracked down. */ public void executeTest() throws Exception { emitLine("print 'Test complete'"); closeAll(); // Now, execute the python command. Process p = Runtime.getRuntime().exec(new String[]{"python","test.py"}); // Read from streams StreamConnector mStdOut = new StreamConnector( p.getErrorStream(), "Stderr: ", System.err ); StreamConnector mErrOut = new StreamConnector( p.getInputStream(), "Stdout: ", System.out ); mStdOut.start(); mErrOut.start(); int exitCode = p.waitFor(); mStdOut.abort(); mErrOut.abort(); mStdOut.join(); mErrOut.join(); if (exitCode != 0) throw new Exception("UI test failed; error code: "+exitCode); // After successful execution, remove the test file. if (currentTestFile != null) { currentTestFile.delete(); currentTestFile = null; } } /** Create a string description for use later in the test. *@param value is the intended value of the string description. *@return the string description. */ public StringDescription createStringDescription(String value) throws Exception { String variableName = getNextVariableName(); if (value != null) emitLine(variableName + " = " + quotePythonString(value)); else emitLine(variableName + " = None"); return new StringDescription(variableName); } /** Create a string description for use later in the test. *@param values are the intended values of the string description, concatenated together. *@return the string description. */ public StringDescription createStringDescription(StringDescription[] values) throws Exception { String variableName = getNextVariableName(); if (values.length == 0) emitLine(variableName + " = " + quotePythonString("")); else { StringBuilder sb = new StringBuilder(variableName); sb.append(" = "); for (int i = 0; i < values.length ; i++) { if (i > 0) sb.append(" + "); sb.append(values[i].getVarName()); } emitLine(sb.toString()); } return new StringDescription(variableName); } /** Print a value. */ public void printValue(StringDescription value) throws Exception { emitLine("print >> sys.stderr, "+value.getVarName()); } /** Begin a loop. */ public Loop beginLoop(int maxSeconds) throws Exception { String variableName = getNextVariableName(); emitLine(variableName+" = time.time() + "+maxSeconds); emitLine("while True:"); currentIndentLevel++; return new Loop(variableName); } /** Open virtual browser window, and send it to a specified URL. *@param url is the desired URL. *@return the window handle. Use this whenever a window argument is required later. */ public Window openMainWindow(String url) throws Exception { emitLine(virtualBrowserVarName + ".load_main_window(" + quotePythonString(url) + ")"); return findWindow(null); } /** Find a window of a specific name, or null for the main window. *@param windowName is the name of the window, or null. *@return the window handle. */ public Window findWindow(StringDescription windowName) throws Exception { String windowVar = getNextVariableName(); if (windowName != null) emitLine(windowVar + " = " + virtualBrowserVarName + ".find_window("+windowName.getVarName()+")"); else emitLine(windowVar + " = " + virtualBrowserVarName + ".find_window("+quotePythonString("")+")"); return new Window(windowVar); } /** Calculate the next variable name */ protected String getNextVariableName() { String rval = "var"+variableCounter; variableCounter++; return rval; } /** Quote a python string */ protected String quotePythonString(String value) { StringBuilder sb = new StringBuilder("\""); for (int i = 0 ; i < value.length() ; i++) { char c = value.charAt(i); if (c == '"') sb.append("\\").append(c); else sb.append(c); } sb.append("\""); return sb.toString(); } /** Emit a python line. */ protected void emitLine(String line) throws IOException { // Append to file with current indent. StringBuilder fullLine = new StringBuilder(); for (int i = 0 ; i < currentIndentLevel ; i++) { fullLine.append(" "); } fullLine.append(line); currentWriter.write(fullLine.toString()); currentWriter.newLine(); } /** Window handle */ public class Window { protected String windowVar; /** Create a window instance. */ public Window(String windowVar) { this.windowVar = windowVar; } /** Check if a pattern is present or not. *@return a StringDescription that in fact describes a boolean condition; true if present. */ public StringDescription isPresent(StringDescription regularExpression) throws Exception { String varName = getNextVariableName(); emitLine(varName + " = "+windowVar+".is_present("+regularExpression.getVarName()+")"); return new StringDescription(varName); } /** Check if a pattern is present or not. *@return a StringDescription that in fact describes a boolean condition; true if not present. */ public StringDescription isNotPresent(StringDescription regularExpression) throws Exception { String varName = getNextVariableName(); emitLine(varName + " = not "+windowVar+".is_present("+regularExpression.getVarName()+")"); return new StringDescription(varName); } /** Look for a specific match in the current page data, and return the value of the specified group. *@return a description of the string found. This can be used later in other commands to assess * correctness of the page, or allow form data to be filled in. */ public StringDescription findMatch(StringDescription regularExpression, int group) throws Exception { String varName = getNextVariableName(); emitLine(varName + " = "+windowVar+".find_match("+regularExpression.getVarName()+",group="+group+")"); return new StringDescription(varName); } /** Same as findMatch, but strips out newlines before it looks. */ public StringDescription findMatchNoNewlines(StringDescription regularExpression, int group) throws Exception { String varName = getNextVariableName(); emitLine(varName + " = "+windowVar+".find_match_no_newlines("+regularExpression.getVarName()+",group="+group+")"); return new StringDescription(varName); } /** If the match is not found, the test will error out. */ public void checkMatch(StringDescription regularExpression) throws Exception { emitLine(windowVar+".find_match("+regularExpression.getVarName()+")"); } /** If the match is found, the test will error out. */ public void checkNoMatch(StringDescription regularExpression) throws Exception { emitLine(windowVar+".check_no_match("+regularExpression.getVarName()+")"); } /** Find a link. */ public Link findLink(StringDescription altText) throws Exception { String linkVarName = getNextVariableName(); emitLine(linkVarName + " = " + windowVar + ".find_link("+altText.getVarName()+")"); return new Link(linkVarName); } /** Find a form. */ public Form findForm(StringDescription formName) throws Exception { String formVarName = getNextVariableName(); emitLine(formVarName + " = " + windowVar + ".find_form("+formName.getVarName()+")"); return new Form(formVarName); } /** Find a button. */ public Button findButton(StringDescription altText) throws Exception { String buttonVarName = getNextVariableName(); emitLine(buttonVarName + " = " + windowVar + ".find_button("+altText.getVarName()+")"); return new Button(buttonVarName); } /** Close this window. */ public void closeWindow() throws Exception { emitLine(windowVar+".close_window()"); } } /** Loop object. */ public class Loop { protected String loopVarName; public Loop(String loopVarName) { this.loopVarName = loopVarName; } /** Break on condition being true. */ public void breakWhenTrue(StringDescription condition) throws Exception { emitLine("if "+condition.getVarName()+":"); currentIndentLevel++; emitLine("break"); currentIndentLevel--; } /** End the loop. */ public void endLoop() throws Exception { emitLine("time.sleep(1)"); emitLine("if time.time() >= "+loopVarName+":"); currentIndentLevel++; emitLine("raise Exception('Loop timed out')"); currentIndentLevel--; currentIndentLevel--; } } /** Object representative of a virtual browser link. */ public class Link { protected String linkVarName; public Link(String linkVarName) { this.linkVarName = linkVarName; } /** Click the link */ public void click() throws Exception { emitLine(linkVarName + ".click()"); } } /** Object representative of a virtual browser form. */ public class Form { protected String formVarName; public Form(String formVarName) { this.formVarName = formVarName; } /** Find a file browser element, by data variable name. */ public FileBrowser findFileBrowser(StringDescription dataName) throws Exception { String fileBrowserVarName = getNextVariableName(); emitLine(fileBrowserVarName + " = " + formVarName + ".find_filebrowser("+dataName.getVarName()+")"); return new FileBrowser(fileBrowserVarName); } /** Find a checkbox element, by data variable name and value. */ public Checkbox findCheckbox(StringDescription dataName, StringDescription value) throws Exception { String checkboxVarName = getNextVariableName(); emitLine(checkboxVarName + " = " + formVarName + ".find_checkbox("+dataName.getVarName()+","+value.getVarName()+")"); return new Checkbox(checkboxVarName); } /** Find a radio button by variable name and value. */ public Radiobutton findRadiobutton(StringDescription dataName, StringDescription value) throws Exception { String radiobuttonVarName = getNextVariableName(); emitLine(radiobuttonVarName + " = " + formVarName + ".find_radiobutton("+dataName.getVarName()+","+value.getVarName()+")"); return new Radiobutton(radiobuttonVarName); } /** Find a select box by data variable name. */ public Selectbox findSelectbox(StringDescription dataName) throws Exception { String selectboxVarName = getNextVariableName(); emitLine(selectboxVarName + " = " + formVarName + ".find_selectbox("+dataName.getVarName()+")"); return new Selectbox(selectboxVarName); } /** Find a textarea/password field by data variable name. */ public Textarea findTextarea(StringDescription dataName) throws Exception { String textareaVarName = getNextVariableName(); emitLine(textareaVarName + " = " + formVarName + ".find_textarea("+dataName.getVarName()+")"); return new Textarea(textareaVarName); } } /** Object representative of a file browser. */ public class FileBrowser { protected String fileBrowserVarName; public FileBrowser(String fileBrowserVarName) { this.fileBrowserVarName = fileBrowserVarName; } public void setFile(StringDescription fileName, StringDescription contentType) throws Exception { emitLine(fileBrowserVarName + ".set_file("+fileName.getVarName()+","+contentType.getVarName()+")"); } } /** Object representative of a checkbox. */ public class Checkbox { protected String checkBoxVarName; public Checkbox(String checkBoxVarName) { this.checkBoxVarName = checkBoxVarName; } /** Select this checkbox */ public void select() throws Exception { emitLine(checkBoxVarName + ".select()"); } /** Deselect this checkbox */ public void deselect() throws Exception { emitLine(checkBoxVarName + ".deselect()"); } } /** Object representative of a radio button. */ public class Radiobutton { protected String radioButtonVarName; public Radiobutton(String radioButtonVarName) { this.radioButtonVarName = radioButtonVarName; } /** Select this radio button */ public void select() throws Exception { emitLine(radioButtonVarName + ".select()"); } } /** Object representative of a select box. */ public class Selectbox { protected String selectBoxVarName; public Selectbox(String selectBoxVarName) { this.selectBoxVarName = selectBoxVarName; } /** Select a value (without CTRL button). * This works like a browser in that selecting in this way turns * off all other current selections. */ public void selectValue(StringDescription selectedValue) throws Exception { emitLine(selectBoxVarName + ".select_value(" + selectedValue.getVarName() + ")"); } /** Select a value using a regular expression (without CTRL button) */ public void selectValueRegexp(StringDescription selectedValueRegexp) throws Exception { emitLine(selectBoxVarName + ".select_value_regexp(" + selectedValueRegexp.getVarName() + ")"); } /** CTRL-select a value. * For multiselect boxes, this adds a new selection to those already * chosen. For non-multi boxes, it works just like select_value. */ public void multiSelectValue(StringDescription selectedValue) throws Exception { emitLine(selectBoxVarName + ".multi_select_value(" + selectedValue.getVarName() + ")"); } } /** Object representative of a text area. */ public class Textarea { protected String textAreaVarName; public Textarea(String textAreaVarName) { this.textAreaVarName = textAreaVarName; } /** Set the value. */ public void setValue(StringDescription textValue) throws Exception { emitLine(textAreaVarName + ".set_value(" + textValue.getVarName() + ")"); } } /** Object representative of a virtual browser button. */ public class Button { protected String buttonVarName; public Button(String buttonVarName) { this.buttonVarName = buttonVarName; } public void click() throws Exception { emitLine(buttonVarName + ".click()"); } } /** String description. An instance of this class represents a string that will be located as the * browser emulator functions. It can be used at various places as the test description is built. */ public class StringDescription { protected String variableName; public StringDescription(String variableName) { this.variableName = variableName; } public String getVarName() { return variableName; } } /** Connector thread that allows for exec */ protected static class StreamConnector extends Thread { protected InputStream inputStream; protected OutputStream outputStream; protected String prefix; protected boolean abortSignal; public StreamConnector(InputStream inputStream, String prefix, OutputStream outputStream) { this.inputStream = inputStream; this.prefix = prefix; this.outputStream = outputStream; abortSignal = false; } public void abort() { abortSignal = true; } public void run() { try { byte[] buffer = new byte[63356]; while (true) { int amt = inputStream.read(buffer); if (amt == -1) { if (abortSignal) break; Thread.yield(); continue; } outputStream.write(buffer,0,amt); } } catch (IOException e) { e.printStackTrace(System.err); } } } }