aboutsummaryrefslogtreecommitdiff
path: root/src/batchtools
diff options
context:
space:
mode:
authorGravatar Rogan Creswick <creswick@gmail.com>2013-06-07 16:00:22 -0700
committerGravatar Rogan Creswick <creswick@gmail.com>2013-06-07 16:00:22 -0700
commit88c95d18a81e4f107cc4e5967bfa45d1bf4882a1 (patch)
treef1b8f5bb1bffd8ea84078d829248dddbdc2f3544 /src/batchtools
parent04d3c6e96ed4dd528418fe71a85e72316ae5bba4 (diff)
cleaned up some test files that broke during a merge
Diffstat (limited to 'src/batchtools')
-rw-r--r--src/batchtools/headless/.classpath31
-rw-r--r--src/batchtools/headless/.gitignore2
-rw-r--r--src/batchtools/headless/.settings/org.eclipse.core.resources.prefs5
-rw-r--r--src/batchtools/headless/.settings/org.eclipse.jdt.core.prefs5
-rw-r--r--src/batchtools/headless/.settings/org.eclipse.m2e.core.prefs4
-rwxr-xr-xsrc/batchtools/headless/bin/runHeadless.sh8
-rw-r--r--src/batchtools/headless/pom.xml121
-rw-r--r--src/batchtools/headless/programs.properties.example2
-rw-r--r--src/batchtools/headless/src/main/java/com/galois/fiveui/BasicCrawler.java90
-rw-r--r--src/batchtools/headless/src/main/java/com/galois/fiveui/BasicCrawlerController.java150
-rw-r--r--src/batchtools/headless/src/main/java/com/galois/fiveui/BatchRunner.java312
-rw-r--r--src/batchtools/headless/src/main/java/com/galois/fiveui/CrawlParameters.java84
-rw-r--r--src/batchtools/headless/src/main/java/com/galois/fiveui/HeadlessAtom.java95
-rw-r--r--src/batchtools/headless/src/main/java/com/galois/fiveui/HeadlessRunDescription.java177
-rw-r--r--src/batchtools/headless/src/main/java/com/galois/fiveui/HeadlessRunner.java181
-rw-r--r--src/batchtools/headless/src/main/java/com/galois/fiveui/Reporter.java360
-rw-r--r--src/batchtools/headless/src/test/java/com/galois/fiveui/BatchExecutorTest.java122
-rw-r--r--src/batchtools/headless/src/test/java/com/galois/fiveui/CrawlParametersTest.java107
-rw-r--r--src/batchtools/headless/src/test/java/com/galois/fiveui/CrawlTest.java136
-rw-r--r--src/batchtools/headless/src/test/java/com/galois/fiveui/HeadlessTest.java114
-rw-r--r--src/batchtools/headless/src/test/java/com/galois/fiveui/NanoHTTPD.java1122
-rw-r--r--src/batchtools/headless/src/test/java/com/galois/fiveui/ReporterTest.java71
-rw-r--r--src/batchtools/headless/src/test/resources/crawlTest/four.html7
-rw-r--r--src/batchtools/headless/src/test/resources/crawlTest/one.html12
-rw-r--r--src/batchtools/headless/src/test/resources/crawlTest/one_a.html6
-rw-r--r--src/batchtools/headless/src/test/resources/crawlTest/one_b.html6
-rw-r--r--src/batchtools/headless/src/test/resources/crawlTest/one_c.html6
-rw-r--r--src/batchtools/headless/src/test/resources/crawlTest/one_d.html6
-rw-r--r--src/batchtools/headless/src/test/resources/crawlTest/one_e.html6
-rw-r--r--src/batchtools/headless/src/test/resources/crawlTest/three.html7
-rw-r--r--src/batchtools/headless/src/test/resources/crawlTest/two.html7
-rw-r--r--src/batchtools/headless/src/test/resources/ruleSets/emptyRuleSet.json4
-rw-r--r--src/batchtools/headless/src/test/resources/runDescriptions/headlessRunTest0.json4
-rw-r--r--src/batchtools/headless/src/test/resources/runDescriptions/headlessRunTestCNN.json4
-rw-r--r--src/batchtools/headless/src/test/resources/runDescriptions/headlessRunTestGalois.json10
-rw-r--r--src/batchtools/headless/src/test/resources/runDescriptions/headlessSample0.json4
-rw-r--r--src/batchtools/headless/src/test/resources/runDescriptions/headlessSample1.json4
-rw-r--r--src/batchtools/headless/src/test/resources/runDescriptions/headlessSample2.json9
-rw-r--r--src/batchtools/headless/test.html0
-rw-r--r--src/batchtools/pom.xml36
-rw-r--r--src/batchtools/rsTester/.buildpath10
-rw-r--r--src/batchtools/rsTester/.classpath36
-rw-r--r--src/batchtools/rsTester/.gitignore2
-rw-r--r--src/batchtools/rsTester/.settings/org.ebayopensource.vjet.eclipse.core.prefs2
-rw-r--r--src/batchtools/rsTester/.settings/org.eclipse.jdt.core.prefs5
-rw-r--r--src/batchtools/rsTester/.settings/org.eclipse.m2e.core.prefs4
-rw-r--r--src/batchtools/rsTester/.settings/org.maven.ide.eclipse.prefs8
-rw-r--r--src/batchtools/rsTester/pom.xml109
-rw-r--r--src/batchtools/rsTester/src/main/java/com/galois/fiveui/BatchRunner.java235
-rw-r--r--src/batchtools/rsTester/src/main/java/com/galois/fiveui/RSTestDescription.java321
-rw-r--r--src/batchtools/rsTester/src/main/java/com/galois/fiveui/ResType.java32
-rw-r--r--src/batchtools/rsTester/src/main/java/com/galois/fiveui/Result.java174
-rw-r--r--src/batchtools/rsTester/src/main/java/com/galois/fiveui/Rule.java122
-rw-r--r--src/batchtools/rsTester/src/main/java/com/galois/fiveui/RuleSet.java154
-rw-r--r--src/batchtools/rsTester/src/main/java/com/galois/fiveui/RuleSetTester.java91
-rw-r--r--src/batchtools/rsTester/src/main/java/com/galois/fiveui/RuleTest.java76
-rw-r--r--src/batchtools/rsTester/src/main/java/com/galois/fiveui/Utils.java44
-rw-r--r--src/batchtools/rsTester/src/main/resources/javascript/ruleEval.js74
-rwxr-xr-xsrc/batchtools/rsTester/src/main/resources/seleniumDrivers/linux64/chromedriverbin0 -> 16328112 bytes
-rw-r--r--src/batchtools/rsTester/src/test/java/com/galois/fiveui/BasicRuleSetParseTest.java115
-rw-r--r--src/batchtools/rsTester/src/test/java/com/galois/fiveui/BasicRunDescriptionParseTest.java126
-rw-r--r--src/batchtools/rsTester/src/test/java/com/galois/fiveui/RunDescriptionTest.java118
-rw-r--r--src/batchtools/rsTester/src/test/resources/ruleSets/emptyCheck.js6
-rw-r--r--src/batchtools/rsTester/src/test/resources/ruleSets/emptyRuleSet.json4
-rw-r--r--src/batchtools/rsTester/src/test/resources/ruleSets/headingGuidelines-caps.js19
-rw-r--r--src/batchtools/rsTester/src/test/resources/ruleSets/headingGuidelines-noEmptyHdrs.js12
-rw-r--r--src/batchtools/rsTester/src/test/resources/ruleSets/headingGuidelines.json6
-rw-r--r--src/batchtools/rsTester/src/test/resources/ruleSets/simpleRuleSet1.json4
-rw-r--r--src/batchtools/rsTester/src/test/resources/runDescriptions/headingSample-1-fails.json13
-rw-r--r--src/batchtools/rsTester/src/test/resources/runDescriptions/headingSample-2-fails.json13
-rw-r--r--src/batchtools/rsTester/src/test/resources/runDescriptions/headingSample.json13
-rw-r--r--src/batchtools/rsTester/src/test/resources/runDescriptions/sample0.json4
-rw-r--r--src/batchtools/rsTester/src/test/resources/runDescriptions/sample1.json4
-rw-r--r--src/batchtools/rsTester/src/test/resources/runDescriptions/sample2-fails.json10
-rw-r--r--src/batchtools/rsTester/src/test/resources/runDescriptions/sample2.json10
-rw-r--r--src/batchtools/rsTester/src/test/resources/runDescriptions/sample3.json7
-rw-r--r--src/batchtools/rsTester/src/test/resources/runDescriptions/sample4.json10
-rw-r--r--src/batchtools/rsTester/src/test/resources/runDescriptions/sample5.json13
-rw-r--r--src/batchtools/rsTester/test.html0
-rw-r--r--src/batchtools/webdrivers/.classpath36
-rw-r--r--src/batchtools/webdrivers/.gitignore1
-rw-r--r--src/batchtools/webdrivers/.settings/org.eclipse.jdt.core.prefs5
-rw-r--r--src/batchtools/webdrivers/.settings/org.eclipse.m2e.core.prefs4
-rw-r--r--src/batchtools/webdrivers/pom.xml43
-rw-r--r--src/batchtools/webdrivers/src/main/java/com/galois/fiveui/drivers/Drivers.java149
85 files changed, 5681 insertions, 0 deletions
diff --git a/src/batchtools/headless/.classpath b/src/batchtools/headless/.classpath
new file mode 100644
index 0000000..beba987
--- /dev/null
+++ b/src/batchtools/headless/.classpath
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+ <classpathentry kind="src" output="target/classes" path="src/main/java">
+ <attributes>
+ <attribute name="optional" value="true"/>
+ <attribute name="maven.pomderived" value="true"/>
+ </attributes>
+ </classpathentry>
+ <classpathentry kind="src" output="target/test-classes" path="src/test/java">
+ <attributes>
+ <attribute name="optional" value="true"/>
+ <attribute name="maven.pomderived" value="true"/>
+ </attributes>
+ </classpathentry>
+ <classpathentry excluding="**" kind="src" output="target/test-classes" path="src/test/resources">
+ <attributes>
+ <attribute name="maven.pomderived" value="true"/>
+ </attributes>
+ </classpathentry>
+ <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/J2SE-1.5">
+ <attributes>
+ <attribute name="maven.pomderived" value="true"/>
+ </attributes>
+ </classpathentry>
+ <classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
+ <attributes>
+ <attribute name="maven.pomderived" value="true"/>
+ </attributes>
+ </classpathentry>
+ <classpathentry kind="output" path="target/classes"/>
+</classpath>
diff --git a/src/batchtools/headless/.gitignore b/src/batchtools/headless/.gitignore
new file mode 100644
index 0000000..24c4165
--- /dev/null
+++ b/src/batchtools/headless/.gitignore
@@ -0,0 +1,2 @@
+target
+programs.properties
diff --git a/src/batchtools/headless/.settings/org.eclipse.core.resources.prefs b/src/batchtools/headless/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 0000000..cdfe4f1
--- /dev/null
+++ b/src/batchtools/headless/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,5 @@
+eclipse.preferences.version=1
+encoding//src/main/java=UTF-8
+encoding//src/test/java=UTF-8
+encoding//src/test/resources=UTF-8
+encoding/<project>=UTF-8
diff --git a/src/batchtools/headless/.settings/org.eclipse.jdt.core.prefs b/src/batchtools/headless/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 0000000..abec6ca
--- /dev/null
+++ b/src/batchtools/headless/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,5 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.5
+org.eclipse.jdt.core.compiler.compliance=1.5
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
+org.eclipse.jdt.core.compiler.source=1.5
diff --git a/src/batchtools/headless/.settings/org.eclipse.m2e.core.prefs b/src/batchtools/headless/.settings/org.eclipse.m2e.core.prefs
new file mode 100644
index 0000000..f897a7f
--- /dev/null
+++ b/src/batchtools/headless/.settings/org.eclipse.m2e.core.prefs
@@ -0,0 +1,4 @@
+activeProfiles=
+eclipse.preferences.version=1
+resolveWorkspaceProjects=true
+version=1
diff --git a/src/batchtools/headless/bin/runHeadless.sh b/src/batchtools/headless/bin/runHeadless.sh
new file mode 100755
index 0000000..4c4d863
--- /dev/null
+++ b/src/batchtools/headless/bin/runHeadless.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+export FIVEUI_ROOT_PATH=$HOME/FiveUI
+export FIREFOX_BIN_PATH=/Users/bjones/myapps/Firefox_17_esr.app/Contents/MacOS/firefox
+
+java -DFIVEUI_ROOT_PATH=$FIVEUI_ROOT_PATH \
+ -DFIREFOX_BIN_PATH=$FIREFOX_BIN_PATH \
+ -jar $FIVEUI_ROOT_PATH/headless/bin/HeadlessRunner-0.0.1-SNAPSHOT.one-jar.jar \
+ $*
diff --git a/src/batchtools/headless/pom.xml b/src/batchtools/headless/pom.xml
new file mode 100644
index 0000000..da5b348
--- /dev/null
+++ b/src/batchtools/headless/pom.xml
@@ -0,0 +1,121 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <groupId>com.galois.fiveui</groupId>
+ <artifactId>HeadlessRunner</artifactId>
+ <version>0.0.1-SNAPSHOT</version>
+ <packaging>jar</packaging>
+
+
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ </properties>
+
+ <pluginRepositories>
+ <pluginRepository>
+ <id>onejar-maven-plugin.googlecode.com</id>
+ <url>http://onejar-maven-plugin.googlecode.com/svn/mavenrepo</url>
+ </pluginRepository>
+ </pluginRepositories>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <version>2.11</version>
+ <configuration>
+ <systemPropertiesFile>programs.properties</systemPropertiesFile>
+ </configuration>
+ </plugin>
+ <plugin>
+ <!-- package with 'mvn assembly:single' -->
+ <artifactId>maven-assembly-plugin</artifactId>
+ <configuration>
+ <archive>
+ <manifest>
+ <mainClass>com.galois.fiveui.HeadlessRunner</mainClass>
+ </manifest>
+ </archive>
+ <descriptorRefs>
+ <descriptorRef>jar-with-dependencies</descriptorRef>
+ </descriptorRefs>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.dstovall</groupId>
+ <artifactId>onejar-maven-plugin</artifactId>
+ <version>1.3.0</version>
+ <executions>
+ <execution>
+ <configuration>
+ <mainClass>com.galois.fiveui.HeadlessRunner</mainClass>
+ </configuration>
+ <goals>
+ <goal>one-jar</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.seleniumhq.selenium</groupId>
+ <artifactId>selenium-java</artifactId>
+ <version>2.26.0</version>
+ </dependency>
+ <dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava</artifactId>
+ <version>13.0.1</version>
+ </dependency>
+ <dependency>
+ <groupId>com.google.code.gson</groupId>
+ <artifactId>gson</artifactId>
+ <version>2.2.2</version>
+ </dependency>
+ <dependency>
+ <groupId>com.galois.fiveui</groupId>
+ <artifactId>webdrivers</artifactId>
+ <version>0.0.1-SNAPSHOT</version>
+ </dependency>
+ <dependency>
+ <groupId>com.galois.fiveui</groupId>
+ <artifactId>RuleSetTester</artifactId>
+ <version>1.0</version>
+ </dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <version>4.9</version>
+ </dependency>
+ <dependency>
+ <groupId>edu.uci.ics</groupId>
+ <artifactId>crawler4j</artifactId>
+ <version>3.3</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>httpclient</artifactId>
+ <version>4.2.1</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>httpcore</artifactId>
+ <version>4.2.1</version>
+ </dependency>
+ <dependency>
+ <groupId>commons-cli</groupId>
+ <artifactId>commons-cli</artifactId>
+ <version>1.2</version>
+ </dependency>
+ <dependency>
+ <groupId>com.googlecode.jatl</groupId>
+ <artifactId>jatl</artifactId>
+ <version>0.2.2</version>
+ </dependency>
+ </dependencies>
+</project>
diff --git a/src/batchtools/headless/programs.properties.example b/src/batchtools/headless/programs.properties.example
new file mode 100644
index 0000000..97bd0c6
--- /dev/null
+++ b/src/batchtools/headless/programs.properties.example
@@ -0,0 +1,2 @@
+FIREFOX_BIN_PATH=/home/creswick/myapps/firefox/firefox-17.0.2-x86_64/firefox
+LOG_LEVEL=ERROR
diff --git a/src/batchtools/headless/src/main/java/com/galois/fiveui/BasicCrawler.java b/src/batchtools/headless/src/main/java/com/galois/fiveui/BasicCrawler.java
new file mode 100644
index 0000000..7f64b1a
--- /dev/null
+++ b/src/batchtools/headless/src/main/java/com/galois/fiveui/BasicCrawler.java
@@ -0,0 +1,90 @@
+package com.galois.fiveui;
+
+import edu.uci.ics.crawler4j.crawler.Page;
+import edu.uci.ics.crawler4j.crawler.WebCrawler;
+import edu.uci.ics.crawler4j.parser.HtmlParseData;
+import edu.uci.ics.crawler4j.url.WebURL;
+
+import java.util.List;
+import java.util.regex.Pattern;
+
+import org.apache.log4j.Logger;
+
+import com.google.common.base.Function;
+
+/**
+ * @author bjones
+ */
+public class BasicCrawler extends WebCrawler {
+
+ private static Logger logger = Logger.getLogger("com.galois.fiveui.BasicCrawler");
+ private final static Pattern FILTERS = Pattern.compile(
+ ".*(\\.(css|js|bmp|gif|jpe?g"
+ + "|png|tiff?|mid|mp2|mp3|mp4"
+ + "|wav|avi|mov|mpeg|ram|m4v|pdf"
+ + "|rm|smil|wmv|swf|wma|zip|rar|gz))$");
+ public static Function<String, Boolean> _predicate;
+ public static List<String> _urls;
+
+ /**
+ * Configure static properties of the class before a crawl.
+ *
+ * @param pred URLs will be crawled only if pred.apply(URL) is
+ * true
+ * @param urls reference to a list of strings which the crawler will
+ * append URLs to as it works
+ */
+ public static void configure(Function<String, Boolean> pred, List<String> urls) {
+ _predicate = pred;
+ _urls = urls;
+ }
+
+ /**
+ * specify whether the given url should be crawled or not
+ */
+ @Override
+ public boolean shouldVisit(WebURL url) {
+ String href = url.getURL();
+ Boolean yesno = !FILTERS.matcher(href).matches() && _predicate.apply(href);
+ logger.debug("saying " + (yesno ? "yes" : "no") + " to " + href);
+ return yesno;
+ }
+
+ /**
+ * This function is called when a page is fetched and ready to be processed
+ * by the program.
+ */
+ @Override
+ public void visit(Page page) {
+ int docid = page.getWebURL().getDocid();
+ String url = page.getWebURL().getURL();
+ String domain = page.getWebURL().getDomain();
+ String path = page.getWebURL().getPath();
+ String subDomain = page.getWebURL().getSubDomain();
+ String parentUrl = page.getWebURL().getParentUrl();
+
+ logger.debug(" - Docid: " + docid);
+ logger.debug(" - URL: " + url);
+ logger.debug(" - Domain: '" + domain + "'");
+ logger.debug(" - Sub-domain: '" + subDomain + "'");
+ logger.debug(" - Path: '" + path + "'");
+ logger.debug(" - Parent page: " + parentUrl);
+
+ if (page.getParseData() instanceof HtmlParseData) {
+ HtmlParseData htmlParseData = (HtmlParseData) page.getParseData();
+ String text = htmlParseData.getText();
+ String html = htmlParseData.getHtml();
+ List<WebURL> links = htmlParseData.getOutgoingUrls();
+
+ logger.debug(" -- Text length: " + text.length());
+ logger.debug(" -- Html length: " + html.length());
+ logger.debug(" -- Number of outgoing links: " + links.size());
+ }
+ logger.debug(" - =============");
+
+ // append to URLs list
+ if (null != _urls) {
+ _urls.add(url);
+ }
+ }
+}
diff --git a/src/batchtools/headless/src/main/java/com/galois/fiveui/BasicCrawlerController.java b/src/batchtools/headless/src/main/java/com/galois/fiveui/BasicCrawlerController.java
new file mode 100644
index 0000000..79338ec
--- /dev/null
+++ b/src/batchtools/headless/src/main/java/com/galois/fiveui/BasicCrawlerController.java
@@ -0,0 +1,150 @@
+package com.galois.fiveui;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.google.common.base.Function;
+
+import edu.uci.ics.crawler4j.crawler.CrawlConfig;
+import edu.uci.ics.crawler4j.crawler.CrawlController;
+import edu.uci.ics.crawler4j.fetcher.PageFetcher;
+import edu.uci.ics.crawler4j.robotstxt.RobotstxtConfig;
+import edu.uci.ics.crawler4j.robotstxt.RobotstxtServer;
+
+/**
+ * @author bjones
+ */
+public class BasicCrawlerController {
+
+ private String seed;
+ private String tmpDir;
+ private int depth;
+ private int maxFetch;
+ private int politeness;
+ private int threads;
+ private Function<String, Boolean> predicate;
+
+ /**
+ * Initialize a basic web crawler controller.
+ *
+ * @param seed URL to start the crawl
+ * @param domain string that all crawled page URLs must start with
+ * @param depth maximum depth to crawl
+ * @param maxFetch maximum number of pages to crawl
+ * @param politeness time in milliseconds to wait before making requests on same domain
+ * @param threads number of concurrent threads to use while crawling
+ * @param tmpDir temporary directory to store intermediate crawl data
+ * (must exist and be read/write before crawl starts)
+ */
+ public BasicCrawlerController (String seed, final String domain, int depth, int maxFetch,
+ int politeness, int threads, String tmpDir) {
+ this.seed = seed;
+ this.predicate = new Function<String, Boolean>() {
+ public Boolean apply(String s) {
+ return s.startsWith(domain);
+ }
+ };
+ this.depth = depth;
+ this.maxFetch = maxFetch;
+ this.politeness = politeness;
+ this.threads = threads;
+ this.tmpDir = tmpDir;
+ }
+
+ /**
+ * Initialize a basic web crawler controller.
+ *
+ * @param seed URL to start the crawl
+ * @param pred a Function<String, Boolean> to be used as a predicate that all crawled URLs must pass
+ * @param depth maximum depth to crawl
+ * @param maxFetch maximum number of pages to crawl
+ * @param politeness time in milliseconds to wait before making requests on same domain
+ * @param threads number of concurrent threads to use while crawling
+ * @param tmpDir temporary directory to store intermediate crawl data
+ * (must exist and be read/write before crawl starts)
+ */
+ public BasicCrawlerController (String seed, Function<String, Boolean> pred, int depth, int maxFetch,
+ int politeness, int threads, String tmpDir) {
+ this.seed = seed;
+ this.predicate = pred;
+ this.depth = depth;
+ this.maxFetch = maxFetch;
+ this.politeness = politeness;
+ this.threads = threads;
+ this.tmpDir = tmpDir;
+ }
+
+ public List<String> go() throws Exception {
+
+ /*
+ * crawlStorageFolder is a folder where intermediate crawl data is
+ * stored.
+ */
+ String crawlStorageFolder = this.tmpDir;
+
+ /*
+ * numberOfCrawlers shows the number of concurrent threads that should
+ * be initiated for crawling.
+ */
+ int numberOfCrawlers = this.threads;
+
+ CrawlConfig config = new CrawlConfig();
+
+ config.setCrawlStorageFolder(crawlStorageFolder);
+
+ /*
+ * Be polite: Make sure that we don't send more than 1 request per
+ * second (1000 milliseconds between requests).
+ */
+ config.setPolitenessDelay(this.politeness);
+
+ /*
+ * You can set the maximum crawl depth here. The default value is -1 for
+ * unlimited depth
+ */
+ config.setMaxDepthOfCrawling(this.depth);
+
+ /*
+ * You can set the maximum number of pages to crawl. The default value
+ * is -1 for unlimited number of pages
+ */
+ config.setMaxPagesToFetch(this.maxFetch);
+
+ /*
+ * Delete the temporary crawl storage after we're done.
+ */
+ config.setResumableCrawling(false);
+
+ /*
+ * Instantiate the controller for this crawl.
+ */
+ PageFetcher pageFetcher = new PageFetcher(config);
+ RobotstxtConfig robotstxtConfig = new RobotstxtConfig();
+ //robotstxtConfig.setEnabled(false); // uncomment if you want to ignore robots.txt
+ RobotstxtServer robotstxtServer = new RobotstxtServer(robotstxtConfig, pageFetcher);
+ CrawlController controller = new CrawlController(config, pageFetcher, robotstxtServer);
+
+ // add a seed URL
+ controller.addSeed(this.seed);
+
+ /*
+ * Setup storage for data collection by the BasicCrawler class
+ */
+ List<String> store = new ArrayList<String>();
+ BasicCrawler.configure(this.predicate, store);
+
+ /*
+ * Start the crawl. This is a blocking operation.
+ */
+ try {
+ controller.start(BasicCrawler.class, numberOfCrawlers);
+ } finally {
+ controller.Shutdown();
+ }
+
+ /*
+ * Extract and return data collected by BasicCrawler
+ */
+ return store;
+ }
+}
diff --git a/src/batchtools/headless/src/main/java/com/galois/fiveui/BatchRunner.java b/src/batchtools/headless/src/main/java/com/galois/fiveui/BatchRunner.java
new file mode 100644
index 0000000..9f3f301
--- /dev/null
+++ b/src/batchtools/headless/src/main/java/com/galois/fiveui/BatchRunner.java
@@ -0,0 +1,312 @@
+/**
+ * Module : BatchRunner.java Copyright : (c) 2012, Galois, Inc.
+ *
+ * Maintainer : Stability : Provisional Portability: Portable
+ *
+ * Licensed 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 com.galois.fiveui;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.openqa.selenium.JavascriptExecutor;
+import org.openqa.selenium.WebDriver;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableList.Builder;
+import com.google.common.io.Files;
+import com.galois.fiveui.Result;
+import com.galois.fiveui.RuleSet;
+import com.galois.fiveui.Utils;
+import com.galois.fiveui.drivers.Drivers;
+
+import edu.uci.ics.crawler4j.util.IO;
+
+import org.apache.log4j.Logger; // System.out.* is old fashioned
+
+
+/**
+ * BatchRunner is initialized with a WebDriver object. It provides an interface
+ * for running {@code RuleSet}s and {@code RuleTest}s with the WebDriver.
+ *
+ * @author bjones
+ */
+public class BatchRunner {
+
+ private WebDriver _driver;
+ private JavascriptExecutor _exe;
+ private String _root; // FiveUI root directory
+
+ // Hard coded JS files, relative to the FiveUI root directory.
+ private static final String DATA_DIR = "contexts/data/";
+ private static final String J_QUERY_JS = DATA_DIR
+ + "lib/jquery/jquery.js";
+ private static final String PRELUDE_JS = DATA_DIR
+ + "fiveui/injected/prelude.js";
+ private static final String MD5_JS = DATA_DIR
+ + "lib/jshash/md5.js";
+ private static final String JQUERY_PLUGIN_JS = DATA_DIR
+ + "fiveui/injected/jquery-plugins.js";
+ private static final String SEL_INJECTED_COMPUTE_JS = DATA_DIR +
+ "/fiveui/selenium/selenium-injected-compute.js";
+ private static final String INJECTED_COMPUTE_JS = DATA_DIR +
+ "/fiveui/injected/fiveui-injected-compute.js";
+
+ private static Logger logger = Logger.getLogger("com.galois.fiveui.BatchRunner");
+
+ private void registerDriver(WebDriver driver) {
+ logger.debug("registering new webdriver...");
+ this._driver = driver;
+ this._exe = (JavascriptExecutor) driver;
+ this._root = Drivers.getRootPath();
+ logger.debug("root path for webdriver is " + _root);
+ }
+
+ public BatchRunner() {
+ logger.debug("initializing BatchRunner ...");
+ }
+
+ /**
+ * Run a headless run description, returning the raw results: 'PASS' if
+ * no inconsistencies were found, 'ERROR' for each inconsistency found,
+ * 'EXCEPTION' for each uncaught exception.
+ * <p>
+ * The run.getURL() is loaded using the WebDriver and the rule set returned
+ * by {@code run.getRule()} is run.
+ *
+ * @param run a headless run description object
+ */
+ public ImmutableList<Result> runHeadless(HeadlessRunDescription run) {
+ String seedUrl;
+ Builder<Result> builder = ImmutableList.builder(); // for results
+ ImmutableList<Result> rawResults;
+ CrawlParameters params = new CrawlParameters(run.getCrawlType());
+ int politeness = params.toString().equals("none") ? 1000 : params.politeness;
+ List<String> urls;
+ Map<String, Map<String, List<String>>> urlCache;
+ //- URL, params, urls
+ urlCache = new HashMap<String, Map<String, List<String>>>();
+
+ for (HeadlessAtom a: run.getAtoms()) {
+ RuleSet rs = a.getRuleSet();
+ seedUrl = a.getURL();
+ logger.debug("setting seed URL for crawl: " + seedUrl);
+ urls = null;
+
+ /**************
+ * Gather URLs
+ **************/
+
+ if (params.isNone()) {
+ urls = ImmutableList.of(seedUrl);
+ logger.debug("skipping webcrawl");
+ } else if (urlCache.containsKey(seedUrl) &&
+ urlCache.get(seedUrl).containsKey(params.toString())) {
+ logger.debug("retreiving urls list from cache");
+ urls = urlCache.get(seedUrl).get(params.toString());
+ } else {
+ File tmpPath = Files.createTempDir();
+ logger.debug("tmp directory for crawl data: " + tmpPath.toString());
+ logger.debug("starting webcrawl controller ...");
+ BasicCrawlerController con =
+ new BasicCrawlerController(seedUrl,
+ params.matchFcn,
+ params.depth, params.maxFetch,
+ params.politeness,
+ 1, // TODO only one thread is currently supported
+ tmpPath.getAbsolutePath());
+ try {
+ urls = con.go();
+ logger.debug("adding urls list to cache");
+ logger.debug("URLs: " + urls.toString());
+ Map<String, List<String>> entry = (Map<String, List<String>>) new HashMap<String, List<String>>();
+ entry.put(params.toString(), urls);
+ urlCache.put(seedUrl, entry);
+ } catch (Exception e) {
+ String errStr = "failed to complete webcrawl of" + seedUrl + "\n";
+ errStr += e.toString();
+ builder.add(new Result(ResType.Exception, _driver, errStr, seedUrl,
+ rs.getName(), rs.getDescription(), ""));
+ logger.error(errStr);
+ continue;
+ } finally {
+ IO.deleteFolder(tmpPath); // does its own logging
+ }
+ }
+
+ /***********************
+ * Drive the browser(s)
+ ***********************/
+
+ for (WebDriver driver: getDrivers()) {
+ registerDriver(driver);
+ for (String url: urls) {
+ logger.info("loading " + url + " for ruleset run ...");
+ loadURL(url); // set state of the WebDriver (blocking)
+ try {
+ logger.info("running ruleset \"" + rs.getName() + "\"");
+ rawResults = runRule(rs); // run the ruleset, collect results
+ builder.addAll(rawResults);
+ } catch (Exception e) {
+ String errStr = "exception during runRule: " + rs.getName() + "\n";
+ errStr += e.toString();
+ builder.add(new Result(ResType.Exception, _driver, errStr, url,
+ rs.getName(), rs.getDescription(), ""));
+ logger.error(errStr);
+ }
+ try {
+ logger.debug("being polite for " + politeness + " millis...");
+ Thread.sleep(politeness);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ driver.quit();
+ }
+ }
+ return builder.build();
+ }
+
+ /**
+ * Run a rule set on the currently loaded page.
+ * <p>
+ * This method uses the web driver instance to run a rule set on the currently
+ * loaded page. The webdriver injects javascript that
+ * includes all the dependencies (JQuery, etc..) as well as the function which
+ * executes the rule check. The method sleeps the thread for 1 second and
+ * queries the results, which are then parsed and returned as a list of
+ * Result objects.
+ *
+ * @param ruleSet the rule set to be run
+ * @return results of running the rule set
+ * @throws IOException
+ */
+ private ImmutableList<Result> runRule(final RuleSet ruleSet) throws IOException {
+ String contentScript = wrapRule(ruleSet);
+ Builder<Result> builder = ImmutableList.builder();
+ String state = "url=" + _driver.getCurrentUrl() +
+ ", ruleSet=\"" + ruleSet.getName() + "\"";
+ logger.debug("runRule: " + state);
+
+ _exe.executeScript(contentScript);
+
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e1) {
+ logger.error(e1.toString());
+ }
+
+ Object res = _exe.executeScript("return fiveui.selPort.query(type='ReportProblem')");
+
+ if (res.getClass() == String.class) {
+ // we received an error via the expected mechanisms:
+ logger.error("exception running rule: " + res);
+ builder.add(new Result(ResType.Exception, _driver, "", _driver.getCurrentUrl(),
+ ruleSet.getName(), ruleSet.getDescription(), ""));
+ return builder.build();
+ } else {
+ try {
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ List<Map<String, Map<String, String>>> results = (List) res;
+
+ if (0 == results.size()) {
+ builder.add(new Result(ResType.Pass, _driver,
+ "passed " + ruleSet.getRules().size() + " tests",
+ _driver.getCurrentUrl(), ruleSet.getName(), ruleSet.getDescription(), ""));
+ }
+
+ for (Map<String, Map<String, String>> r : results) {
+ Map<String, String> problem = r.get("payload");
+ // TODO decide what to extract from problem object and what
+ // to do with it.
+ //
+ // Probably we should just pass along the Map<String, String>
+ // and let the reporter deal with it.
+ String problemAsHTML = "Rule Name: " + problem.get("name") + " / "
+ + "Rule Desc: " + problem.get("descr") + " / "
+ + "XPath: " + problem.get("xpath");
+ builder.add(new Result(ResType.Error, _driver, "",
+ _driver.getCurrentUrl(),
+ ruleSet.getName(),
+ ruleSet.getDescription(),
+ problemAsHTML));
+ }
+
+ } catch (ClassCastException e) {
+ // An unexpected error happened:
+ logger.error("unexpected object returned: " + e.toString());
+ builder.add(new Result(ResType.Exception, _driver,
+ "Unexpected object returned: " + res + ", state: " + state,
+ _driver.getCurrentUrl(),
+ ruleSet.getName(),
+ ruleSet.getDescription(),
+ ""));
+ }
+ }
+ return builder.build();
+ }
+
+ /**
+ * Build up the complete content script needed to run the rule.
+ * <p>
+ * The string returned contains all the javascript dependencies required
+ * to run a rule set and the function that is injected into the page which
+ * executes the rule set.
+ *
+ * TODO DRY
+ *
+ * @param ruleSet a RuleSet object
+ * @throws IOException
+ */
+ private String wrapRule(RuleSet ruleSet) throws IOException {
+ String injected = "";
+ injected += Utils.readFile(_root + SEL_INJECTED_COMPUTE_JS);
+ injected += Utils.readFile(_root + J_QUERY_JS);
+ injected += Utils.readFile(_root + PRELUDE_JS);
+ injected += Utils.readFile(_root + MD5_JS);
+ injected += Utils.readFile(_root + JQUERY_PLUGIN_JS);
+ injected += Utils.readFile(_root + INJECTED_COMPUTE_JS);
+
+ injected += "return fiveui.selPort.send('SetRules', " + ruleSet + ");";
+
+ return injected;
+ }
+
+ /**
+ * Build a list of webdrivers with which to run each ruleset.
+ *
+ * @return list of initialized WebDriver objects
+ */
+ private static ImmutableList<WebDriver> getDrivers() {
+ logger.debug("building webdrivers ...");
+ ImmutableList<WebDriver> r = ImmutableList.<WebDriver>of(
+ Drivers.buildFFDriver()
+ // , Drivers.buildChromeDriver()
+ );
+ logger.debug("built: " + r.toString());
+ return r;
+ }
+
+ /**
+ * Sets the state of the WebDriver by loading a given URL.
+ *
+ * @param url URL to load
+ */
+ private void loadURL(String url) {
+ _driver.get(url);
+ }
+}
diff --git a/src/batchtools/headless/src/main/java/com/galois/fiveui/CrawlParameters.java b/src/batchtools/headless/src/main/java/com/galois/fiveui/CrawlParameters.java
new file mode 100644
index 0000000..a07d43a
--- /dev/null
+++ b/src/batchtools/headless/src/main/java/com/galois/fiveui/CrawlParameters.java
@@ -0,0 +1,84 @@
+package com.galois.fiveui;
+
+import com.google.common.base.Function;
+import java.util.regex.Pattern;
+import java.util.regex.Matcher;
+
+import org.apache.log4j.Logger;
+
+public class CrawlParameters {
+
+ private static Logger logger = Logger.getLogger("com.galois.fiveui.CrawlParameters");
+
+ public int depth;
+ public int maxFetch;
+ public int politeness;
+ public String match;
+ public Function<String, Boolean> matchFcn;
+
+ private Boolean _doNotCrawl;
+ private String _str;
+
+ /**
+ * Construct (parse) a crawl type object from a string description
+ *
+ * A valid description is a whitespace separated list as follows:
+ * "<depth> <maxFetch> <politeness> <start>"
+ * where:
+ * <ol>
+ * <li> (depth :: int) depth of the crawl </li>
+ * <li> (maxFetch :: int) maximum number of pages to crawl </li>
+ * <li> (politeness :: int) number of milliseconds between hits on same domain </li>
+ * <li> (match :: String) glob pattern to match URLs </li>
+ * </ol>
+ * or the string "none" which is, in spirit, equivalent to "0 1 1000 *",
+ * but in practice the webcrawl is skipped entirely in this case.
+ *
+ * @param desc a string description of the crawl type
+ * @throws Exception
+ */
+ public CrawlParameters(String desc) {
+ String[] l = desc.split("\\s+");
+ if (desc == "none" || l.length != 4) {
+ this._doNotCrawl = true;
+ this._str = desc;
+ logger.debug("setting doNotCrawl = True");
+ return;
+ } else {
+ this.depth = Integer.parseInt(l[0]);
+ this.maxFetch = Integer.parseInt(l[1]);
+ this.politeness = Integer.parseInt(l[2]);
+ this.match = l[3];
+ this._doNotCrawl = false;
+ this._str = desc;
+ this.matchFcn = compileMatchFcn(this.match);
+ logger.debug("setting depth: " + this.depth);
+ logger.debug("setting maxFetch: " + this.maxFetch);
+ logger.debug("setting politeness: " + this.politeness);
+ logger.debug("setting match: " + this.match);
+ }
+ }
+
+ public static Function<String, Boolean> compileMatchFcn(String glob) {
+ String reg = glob.replaceAll("\\.", "\\.").replaceAll("\\*", ".*");
+ final Pattern pat = Pattern.compile(reg);
+ return new Function<String, Boolean>() {
+ public Boolean apply(String input) {
+ Matcher m = pat.matcher(input);
+ return m.matches();
+ }
+ };
+ }
+
+ public static CrawlParameters none() {
+ return new CrawlParameters("none");
+ }
+
+ public Boolean isNone() {
+ return this._doNotCrawl;
+ }
+
+ public String toString() {
+ return _str;
+ }
+}
diff --git a/src/batchtools/headless/src/main/java/com/galois/fiveui/HeadlessAtom.java b/src/batchtools/headless/src/main/java/com/galois/fiveui/HeadlessAtom.java
new file mode 100644
index 0000000..01ffae0
--- /dev/null
+++ b/src/batchtools/headless/src/main/java/com/galois/fiveui/HeadlessAtom.java
@@ -0,0 +1,95 @@
+/**
+ *
+ */
+package com.galois.fiveui;
+
+import java.io.File;
+import java.io.IOException;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+
+/**
+ * A singular url to test with a RuleSet.
+ *
+ * @author bjones
+ */
+public class HeadlessAtom {
+
+ private String _url;
+ private RuleSet _ruleSet;
+
+ public HeadlessAtom(String url, RuleSet ruleSet) {
+ _url = url;
+ _ruleSet = ruleSet;
+ }
+
+ public String getURL() {
+ return _url;
+ }
+
+ public RuleSet getRuleSet() {
+ return _ruleSet;
+ }
+
+ public String toString() {
+ Gson gson = new Gson();
+ return gson.toJson(this);
+ }
+
+ /**
+ * Construct a HeadlessAtom from a JsonObject
+ *
+ * @param obj JsonObject to convert from
+ * @param dir parent directory of the filenames referenced in the ruleSet field
+ * @return a HeadlessAtom POJO
+ * @throws IOException
+ * @throws JsonParseException
+ */
+ public static HeadlessAtom fromJsonObject(JsonObject obj, String dir) throws IOException {
+ String url = obj.get("url").getAsString();
+ String ruleSet = obj.get("ruleSet").getAsString();
+
+ if (url == null || ruleSet == null) {
+ throw new JsonParseException("could get either 'url' or 'ruleSet' properties");
+ }
+
+ String rsPath = dir + File.separator + ruleSet;
+ RuleSet parsed = RuleSet.parseFile(rsPath);
+
+ return new HeadlessAtom(url, parsed);
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((_url == null) ? 0 : _url.hashCode());
+ result = prime * result + ((_ruleSet == null) ? 0 : _ruleSet.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ HeadlessAtom other = (HeadlessAtom) obj;
+ if (_url == null) {
+ if (other._url != null)
+ return false;
+ } else if (!_url.equals(other._url))
+ return false;
+ if (_ruleSet == null) {
+ if (other._ruleSet != null)
+ return false;
+ } else if (!_ruleSet.equals(other._ruleSet))
+ return false;
+ return true;
+ }
+
+}
diff --git a/src/batchtools/headless/src/main/java/com/galois/fiveui/HeadlessRunDescription.java b/src/batchtools/headless/src/main/java/com/galois/fiveui/HeadlessRunDescription.java
new file mode 100644
index 0000000..92f2ef1
--- /dev/null
+++ b/src/batchtools/headless/src/main/java/com/galois/fiveui/HeadlessRunDescription.java
@@ -0,0 +1,177 @@
+/**
+ *
+ */
+package com.galois.fiveui;
+
+
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.lang.reflect.Type;
+import java.util.List;
+
+import org.apache.log4j.Logger;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableList.Builder;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+
+/**
+ * @author bjones
+ *
+ */
+public class HeadlessRunDescription {
+
+ private static Logger logger = Logger.getLogger("com.galois.fiveui.HeadlessRunDescription");
+ private static String _crawlType;
+ private List<HeadlessAtom> _atoms;
+
+ public HeadlessRunDescription (List<HeadlessAtom> atoms) {
+ _atoms = atoms;
+ }
+
+ public List<HeadlessAtom> getAtoms() {
+ return _atoms;
+ }
+
+ public int size() {
+ return _atoms.size();
+ }
+
+ /**
+ * Parse a JSON file into a HeadlessRunDescription
+ *
+ * @param runDescFileName The file to load.
+ * @return A populated HeadlessRunDescription object.
+ * @throws FileNotFoundException if runDescFile can't be found.
+ */
+ public static HeadlessRunDescription parse(String runDescFileName)
+ throws FileNotFoundException {
+ GsonBuilder gsonBuilder = new GsonBuilder();
+ gsonBuilder.registerTypeAdapter(HeadlessRunDescription.class,
+ new HeadlessRunDescription.Deserializer(runDescFileName));
+ Gson gson = gsonBuilder.create();
+ Reader in = new InputStreamReader(new FileInputStream(runDescFileName));
+ return gson.fromJson(in, HeadlessRunDescription.class);
+ }
+
+ public static class Deserializer implements JsonDeserializer<HeadlessRunDescription> {
+
+ private String _fn; // stores the filename on disk being parsed
+ private String _ctxDir; // stores the parent dir of _fn
+
+ public Deserializer(String fn) {
+ _fn = fn;
+ _ctxDir = new File(_fn).getParent();
+ if (null == _ctxDir) {
+ _ctxDir = ".";
+ }
+ }
+
+ public static void reportError(JsonElement json) {
+ logger.error("HeadlessRunDescription.parse: ran into unexpected jsonElement type:");
+ logger.error(" " + json.getAsString());
+ }
+
+ /**
+ * Gracefully lookup property 'prop' in JsonObject 'obj'.
+ *
+ * @param obj a JSON object
+ * @param prop a key string to lookup in the JSON object
+ * @return string property or null if the prop doesn't resolve
+ */
+ public static String objGetString(JsonObject obj, String prop) {
+ try {
+ return obj.get(prop).getAsString();
+ } catch (NullPointerException e) {
+ logger.error("HeadlessRunDescription.parse: failed to lookup JSON property: " + prop);
+ logger.error(e.toString());
+ return null;
+ }
+ }
+
+ public HeadlessRunDescription deserialize(JsonElement json, Type typeOfT,
+ JsonDeserializationContext context) throws JsonParseException {
+
+ String ruleSetDir;
+ JsonArray arr;
+
+ if (json.isJsonObject()) { // check if the description is an extended one
+ JsonObject obj = json.getAsJsonObject();
+ ruleSetDir = objGetString(obj, "rulePath");
+ _crawlType = objGetString(obj, "crawlType");
+ arr = obj.get("runs").getAsJsonArray();
+ } else if (json.isJsonArray()) {
+ ruleSetDir = _ctxDir;
+ _crawlType = "none";
+ arr = json.getAsJsonArray();
+ } else {
+ reportError(json);
+ return new HeadlessRunDescription(null);
+ }
+
+ Builder<HeadlessAtom> atoms = ImmutableList.builder();
+ for (JsonElement jsonElement : arr) {
+ try {
+ JsonObject obj = jsonElement.getAsJsonObject();
+ atoms.add(HeadlessAtom.fromJsonObject(obj, ruleSetDir));
+ } catch (IOException e) {
+ logger.error("HeadlessAtom.parse: error parsing ruleSet file: " + e.getMessage());
+ System.exit(1);
+ } catch (IllegalStateException e) {
+ reportError(jsonElement);
+ }
+ }
+
+ return new HeadlessRunDescription(atoms.build());
+ }
+ }
+
+ public String getCrawlType() {
+ return _crawlType;
+ }
+
+ public String toString() {
+ Gson gson = new Gson();
+ return gson.toJson(this);
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ for (HeadlessAtom a: _atoms) {
+ result = prime * result + a.hashCode();
+ }
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ HeadlessRunDescription other = (HeadlessRunDescription) obj;
+ if (_atoms == null) {
+ if (other._atoms != null)
+ return false;
+ } else if (!_atoms.equals(other._atoms))
+ return false;
+
+ return true;
+ }
+}
diff --git a/src/batchtools/headless/src/main/java/com/galois/fiveui/HeadlessRunner.java b/src/batchtools/headless/src/main/java/com/galois/fiveui/HeadlessRunner.java
new file mode 100644
index 0000000..aba0ad7
--- /dev/null
+++ b/src/batchtools/headless/src/main/java/com/galois/fiveui/HeadlessRunner.java
@@ -0,0 +1,181 @@
+/**
+ * Module : HeadlessRunner.java Copyright : (c) 2012, Galois, Inc.
+ *
+ * Maintainer : Stability : Provisional Portability: Portable
+ *
+ * Licensed 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 com.galois.fiveui;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.net.URISyntaxException;
+import com.galois.fiveui.Result;
+import com.google.common.collect.ImmutableList;
+
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.GnuParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.OptionBuilder;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+import org.apache.log4j.BasicConfigurator;
+
+/**
+ * The main entry point for running headless rule set runs.
+ * <p>
+ * The {@link #main(String[])} method of this class sets up a WebDriver, loads
+ * a headless run description from disk, and executes the run which includes
+ * loading seed URL's, performing a webcrawl, and running rule sets on each of
+ * the crawled pages.
+ *
+ * @author bjones
+ *
+ */
+public class HeadlessRunner {
+
+ private static Logger logger = Logger.getLogger("com.galois.fiveui.HeadlessRunner");
+
+ /**
+ * @param args list of headless run description filenames
+ * @throws IOException
+ * @throws URISyntaxException
+ * @throws ParseException
+ */
+ @SuppressWarnings("static-access")
+ public static void main(final String[] args)
+ throws IOException, URISyntaxException, ParseException {
+
+ // Setup command line options
+ Options options = new Options();
+ Option help = new Option( "h", "print this help message" );
+ Option output = OptionBuilder.withArgName("outfile")
+ .hasArg()
+ .withDescription("write output to file")
+ .create("o");
+ Option report = OptionBuilder.withArgName("report directory")
+ .hasArg()
+ .withDescription("write HTML reports to given directory")
+ .create("r");
+ options.addOption(output);
+ options.addOption(report);
+ options.addOption("v", false, "verbose output");
+ options.addOption("vv", false, "VERY verbose output");
+ options.addOption(help);
+
+ // Parse command line options
+ CommandLineParser parser = new GnuParser();
+ CommandLine cmd = null;
+ try {
+ cmd = parser.parse( options, args);
+ } catch (ParseException e) {
+ System.err.println( "Command line option parsing failed. Reason: " + e.getMessage() );
+ System.exit(1);
+ }
+
+ // Display help if requested
+ if (cmd.hasOption("h")) {
+ HelpFormatter formatter = new HelpFormatter();
+ formatter.printHelp("headless <input file 1> [<input file 2> ...]", options);
+ System.exit(1);
+ }
+
+ // Set logging levels
+ BasicConfigurator.configure();
+ Logger fiveuiLogger = Logger.getLogger("com.galois.fiveui");
+ Logger rootLogger = Logger.getRootLogger();
+ if (cmd.hasOption("v")) {
+ fiveuiLogger.setLevel(Level.DEBUG);
+ rootLogger.setLevel(Level.ERROR);
+ } else if (cmd.hasOption("vv")) {
+ fiveuiLogger.setLevel(Level.DEBUG);
+ rootLogger.setLevel(Level.DEBUG);
+ } else {
+ fiveuiLogger.setLevel(Level.ERROR);
+ rootLogger.setLevel(Level.ERROR);
+ }
+
+ // Setup output file if requested
+ PrintWriter outStream = null;
+ if (cmd.hasOption("o")) {
+ String outfile = cmd.getOptionValue("o");
+ try {
+ outStream = new PrintWriter(new BufferedWriter(new FileWriter(outfile)));
+ } catch (IOException e) {
+ System.err.println("Could not open outfile for writing: " + cmd.getOptionValue("outfile"));
+ System.exit(1);
+ }
+ } else {
+ outStream = new PrintWriter(new BufferedWriter(new PrintWriter(System.out)));
+ }
+
+ // Setup HTML reports directory before the major work happens in case we
+ // have to throw an exception.
+ PrintWriter summaryFile = null;
+ PrintWriter byURLFile = null;
+ PrintWriter byRuleFile = null;
+ if (cmd.hasOption("r")) {
+ String repDir = cmd.getOptionValue("r");
+ try {
+ File file = new File(repDir);
+ if (!file.exists()) {
+ file.mkdir();
+ logger.info("report directory created: " + repDir);
+ } else {
+ logger.info("report directory already exists!");
+ }
+ summaryFile = new PrintWriter(new FileWriter(repDir + File.separator + "summary.html"));
+ byURLFile = new PrintWriter(new FileWriter(repDir + File.separator + "byURL.html"));
+ byRuleFile = new PrintWriter(new FileWriter(repDir + File.separator + "byRule.html"));
+ } catch (IOException e) {
+ System.err.println("could not open report directory / files for writing");
+ System.exit(1);
+ }
+ }
+
+ // Major work: process input files
+ ImmutableList<Result> results = null;
+ for (String in: cmd.getArgs()) {
+ HeadlessRunDescription descr = HeadlessRunDescription.parse(in);
+ logger.debug("invoking headless run...");
+ BatchRunner runner = new BatchRunner();
+ results = runner.runHeadless(descr);
+ logger.debug("runHeadless returned " + results.size() + " results");
+ // write results to the output stream as we go
+ for (Result result : results) {
+ outStream.println(result.toString());
+ }
+ outStream.flush();
+ }
+ outStream.close();
+
+ // Write report files if requested
+ if (cmd.hasOption("r") && results != null) {
+ Reporter kermit = new Reporter(results);
+ summaryFile.write(kermit.getSummary());
+ summaryFile.close();
+ byURLFile.write(kermit.getByURL());
+ byURLFile.close();
+ byRuleFile.write(kermit.getByRule());
+ byRuleFile.close();
+ }
+ }
+}
+
diff --git a/src/batchtools/headless/src/main/java/com/galois/fiveui/Reporter.java b/src/batchtools/headless/src/main/java/com/galois/fiveui/Reporter.java
new file mode 100644
index 0000000..5c0b233
--- /dev/null
+++ b/src/batchtools/headless/src/main/java/com/galois/fiveui/Reporter.java
@@ -0,0 +1,360 @@
+/**
+ * Module : Reporter.java Copyright : (c) 2012, Galois, Inc.
+ *
+ * Maintainer : Stability : Provisional Portability: Portable
+ *
+ * Licensed 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.
+ *
+ * @author Benjamin Jones <bjones@galois.com>
+ */
+package com.galois.fiveui;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import com.googlecode.jatl.Html;
+
+/**
+ * Reporter is responsible for turning a list of results from a FiveUI run and
+ * and generating various reports which summarize the results. The reports are
+ * generated in HTML and returned as human readable strings by getSummaryPage(),
+ * getURLPage(), and getRulePage(). The client is responsible for writing them
+ * to .html files (or whatever).
+ *
+ * @author bjones
+ *
+ */
+public class Reporter {
+
+ /** see sortBy* and computeSummaryStats methods for map semantics */
+ private final Map<String, List<Result>> _byURLMap;
+ private final Map<String, List<Result>> _byRuleMap;
+ private final Map<String, int[]> _passFailMap;
+ private Map<String, String> _ruleNameToDesc;
+ private static String GLOBAL_CSS =
+ "h1 { font-size: 150%; }" +
+ "h2 { font-size: 110%; }" +
+ "li { margin: 5 0 0 0; }" +
+ "table { border: 1px solid grey; cellpadding: 5%; width: 200px; }\n" +
+ "td.pass-number{ text-align: right;color: green; }\n" +
+ "td.fail-number{ text-align: right;color: red; }\n" +
+ "td.text{ text-align: left; }\n" +
+ "th { font-weight: bold; }\n" +
+ "td, th { border: 1px solid grey; }\n" +
+ ".hlRow { background: #EEEEEE; }\n" +
+ ".regRow { background: #FFFFFF; }\n";
+
+ /**
+ * Construct a Reporter object. The constructor takes a list of results
+ * and uses them to populate various maps used in reporting.
+ *
+ * @param results a list of Result objects
+ */
+ public Reporter(List<Result> results) {
+ this._byURLMap = sortByURL(results);
+ this._byRuleMap = sortByRule(results);
+ this._passFailMap = computeSummaryStats(results);
+ this._ruleNameToDesc = extractRuleDesc(results);
+ }
+
+ /**
+ * Build the HTML markup for a summary page based on the precomputed map
+ * this._passFailMap.
+ *
+ * @return String containing human-readable HTML representing a summary page
+ */
+ public String getSummary() {
+ StringWriter summaryPage = new StringWriter();
+ final Map<String, int[]> scopedMap = this._passFailMap;
+ Html page = new Html(summaryPage);
+ page = makeHead(page, "Summary of Results");
+ page = new Html(page) {{
+ h1().text("Headless Run Summary").end();
+ p();
+ ul();
+ li().a().href("byUrl.html").text("Results organized by URL").end().end();
+ li().a().href("byRule.html").text("Results organized by Rule").end().end();
+ end();
+ end();
+ p();
+ div().id ("stats");
+ makeSummaryStats(scopedMap);
+ end();
+ end();
+ endAll();
+ done();
+ }
+ /**
+ * Report statistics on the headless run. Note, "pass"
+ * means the URL passed all tests in the ruleset, but "fail" can be
+ * reported for the same test on multiple offenders in the page.
+ */
+ Html makeSummaryStats(Map<String, int[]> passFailMap) {
+ int uniqueURLs = passFailMap.size();
+ int[] passFailList;
+
+ p();
+ h3().text("Unique URLs: ");
+ span().classAttr("number").text(String.valueOf(uniqueURLs)).end();
+ end().end();
+
+ p();
+ table().id("stats-table");
+ tr();
+ th().text("URL").end();
+ th().text("Pass").end();
+ th().text("Fail").end();
+ end();
+ int i = 0; // index for **alternate row highlighting**
+ List<String> sortedKeys = new ArrayList<String>();
+ sortedKeys.addAll(passFailMap.keySet());
+ Collections.sort(sortedKeys);
+ for (String key: sortedKeys) {
+ passFailList = passFailMap.get(key);
+ tr().classAttr(i % 2 == 0 ? "hlRow" : "regRow");
+ td().classAttr("text").a().href(key).text(key).end().end();
+ td().classAttr("pass-number").text(String.valueOf(passFailList[0])).end();
+ td().classAttr("fail-number").text(String.valueOf(passFailList[1])).end();
+ end();
+ i++;
+ }
+ end(); // end <table>
+ return end(); // end <p>
+ }
+ };
+ return summaryPage.getBuffer().toString();
+ }
+
+ /**
+ * Build the HTML markup for a report page sorted by URL.
+ *
+ * @return String containing human-readable HTML representing a report page
+ */
+ public String getByURL() {
+ StringWriter byURLPage = new StringWriter();
+ final Map<String, List<Result>> scopedMap = this._byURLMap;
+ Html page = new Html(byURLPage);
+ page = makeHead(page, "Results sorted by URL");
+ page = new Html(page) {{
+ body();
+ h1().text("Results by URL").end();
+ ol();
+ List<String> sortedKeys = new ArrayList<String>();
+ sortedKeys.addAll(scopedMap.keySet());
+ Collections.sort(sortedKeys);
+ for (String url: sortedKeys) {
+ li().h2().a().href(url).text(url).end().end();
+ ul();
+ int i = 0;
+ for (Result r: scopedMap.get(url)) {
+ li().classAttr(i % 2 == 0 ? "regRow" : "hlRow");
+ text(r.getRuleName() + ": " + r.getRuleDesc()).br();
+ text(r.getProblem()).end();
+ i++;
+ }
+ end();
+ end();
+ }
+ endAll();
+ done();
+ }};
+ return byURLPage.getBuffer().toString();
+ }
+
+ /**
+ * Build the HTML markup for a report page sorted by rule name.
+ *
+ * @return String containing human-readable HTML representing a report page
+ */
+ public String getByRule() {
+ StringWriter byRulePage = new StringWriter();
+ final Map<String, List<Result>> scopedMap = this._byRuleMap;
+ final Map<String, String> scopedRuleNameToDesc = this._ruleNameToDesc;
+ Html page = new Html(byRulePage);
+ page = makeHead(page, "Results sorted by rule");
+ page = new Html(page) {{
+ h1().text("Results by Rule").end();
+ ul();
+ List<String> sortedKeys = new ArrayList<String>();
+ sortedKeys.addAll(scopedMap.keySet());
+ Collections.sort(sortedKeys);
+ for (String rule: sortedKeys) {
+ li();
+ b().text(rule).end().text(": " + scopedRuleNameToDesc.get(rule));
+ ul();
+ int i = 0;
+ for (Result r: scopedMap.get(rule)) {
+ li().classAttr(i % 2 == 0 ? "regRow" : "hlRow");
+ text("Problem: " + r.getProblem()).br();
+ text("URL: ").a().href(r.getURL()).text(r.getURL()).end().end();
+ i++;
+ }
+ end();
+ end();
+ }
+ endAll();
+ done();
+ }};
+ return byRulePage.getBuffer().toString();
+ }
+
+ /**
+ * Utility method to take all the reports and write them to standard file
+ * names under a given directory.
+ *
+ * @param dirName name of the directory where the reports should be written
+ * @throws IOException
+ */
+ public void writeReportsToDir(String dirName) throws IOException {
+ PrintWriter summaryFile = new PrintWriter(new FileWriter(dirName + File.separator + "summary.html"));
+ PrintWriter byURLFile = new PrintWriter(new FileWriter(dirName + File.separator + "byURL.html"));
+ PrintWriter byRuleFile = new PrintWriter(new FileWriter(dirName + File.separator + "byRule.html"));
+ summaryFile.write(this.getSummary());
+ summaryFile.close();
+ byURLFile.write(this.getByURL());
+ byURLFile.close();
+ byRuleFile.write(this.getByRule());
+ byRuleFile.close();
+ }
+
+ /** Private fields **/
+
+ private Html makeHead(Html page, final String title) {
+ return new Html(page) {{
+ html();
+ head();
+ title().text(title).end();
+ style().type("text/css").text(GLOBAL_CSS).end();
+ end();
+ body();
+ }};
+ }
+
+ /**
+ * Populate a Map<String, List<Result>> representing the results sorted by
+ * URL.
+ *
+ * @param results a list of results
+ * @return a map representing the results sorted by URL.
+ */
+ private Map<String, List<Result>>sortByURL(List<Result> results) {
+ /** map semantics: Map< url, [rule1, rule2, ...] > */
+ Map<String, List<Result>> map = new HashMap<String, List<Result>>();
+ String url;
+ List<Result> list;
+ for (Result r: results) {
+ url = r.getURL();
+ if (map.containsKey(url)) {
+ list = map.get(url);
+ list.add(r);
+ } else {
+ list = new ArrayList<Result>();
+ list.add(r);
+ map.put(url, list);
+ }
+ }
+ return map;
+ }
+
+ /**
+ * Populate a Map<String, List<Result>> representing the results sorted by
+ * rule name.
+ *
+ * @param results a list of results
+ * @return a map representing the results sorted by rule name.
+ */
+ private Map<String, List<Result>>sortByRule(List<Result> results) {
+ /** map semantics: Map< rule, [url1, url2, ...] > */
+ Map<String, List<Result>> map = new HashMap<String, List<Result>>();
+ String rule;
+ List<Result> list;
+ for (Result r: results) {
+ rule = r.getRuleName();
+ if (map.containsKey(rule)) {
+ list = map.get(rule);
+ list.add(r);
+ } else {
+ list = new ArrayList<Result>();
+ list.add(r);
+ map.put(rule, list);
+ }
+ }
+ return map;
+ }
+
+ /**
+ * Build a map of ruleName -> ruleDesc entries occurring in the given list of
+ * results.
+ *
+ * @param results a list of results
+ * @return a map associating rule names to rule descriptions
+ */
+ private Map<String, String> extractRuleDesc(List<Result> results) {
+ Map<String, String> assoc = new HashMap<String, String>();
+ for (Result r: results)
+ if (!assoc.containsKey(r.getRuleName()))
+ assoc.put(r.getRuleName(), r.getRuleDesc());
+ return assoc;
+ }
+
+ /**
+ * Compute summary statistics from the results list. This includes number of
+ * passes and fails for each URL checked.
+ *
+ * @param results a list of results
+ * @return a map representing the results sorted by rule name.
+ */
+ private Map<String, int[]> computeSummaryStats(List<Result> results) {
+ /** passFailMap semantics: Map<url, {#pass, #fail}> */
+ Map<String, int[]> passFailMap = new HashMap<String, int[]>();
+ String url;
+ int pass, fail;
+ int[] passFailList;
+ Pattern numberPassedPattern = Pattern.compile("passed ([0-9]+) tests");
+ Matcher matcher;
+
+ for (Result result : results) {
+ pass = fail = 0;
+ url = result.getURL();
+ if (result.getType() == ResType.Pass) {
+ // now we have to parse out how many tests passed
+ matcher = numberPassedPattern.matcher(result.getMsg());
+ if (matcher.find())
+ pass = Integer.parseInt(matcher.group(1)); // throws exception if parse fails
+ } else if (result.getType() == ResType.Error) {
+ // each error result corresponds to one test
+ fail = 1;
+ }
+
+ if (passFailMap.containsKey(url)) {
+ passFailList = passFailMap.get(url);
+ } else {
+ passFailList = new int[] { 0, 0};
+ passFailMap.put(url, passFailList);
+ }
+ passFailList[0] += pass;
+ passFailList[1] += fail;
+ }
+ return passFailMap;
+ }
+}
diff --git a/src/batchtools/headless/src/test/java/com/galois/fiveui/BatchExecutorTest.java b/src/batchtools/headless/src/test/java/com/galois/fiveui/BatchExecutorTest.java
new file mode 100644
index 0000000..f7f593f
--- /dev/null
+++ b/src/batchtools/headless/src/test/java/com/galois/fiveui/BatchExecutorTest.java
@@ -0,0 +1,122 @@
+/**
+ * Module : BatchExecutorTest.java
+ * Copyright : (c) 2011-2012, Galois, Inc.
+ *
+ * Maintainer :
+ * Stability : Provisional
+ * Portability: Portable
+ *
+ * Licensed 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 com.galois.fiveui;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.net.BindException;
+
+import junit.framework.Assert;
+
+import org.apache.log4j.BasicConfigurator;
+import org.apache.log4j.Level;
+import org.apache.log4j.LogManager;
+import org.apache.log4j.Logger;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+
+
+/**
+ * @author bjones
+ *
+ */
+public class BatchExecutorTest {
+
+ private static final String RUN_DESCRIPTION_DIR = "src/test/resources/runDescriptions/";
+ private static Logger logger = Logger.getLogger("com.galois.fiveui.BatchExecutorTest");
+ private static NanoHTTPD httpServer;
+
+ @BeforeClass
+ public static void setupTests() {
+ BasicConfigurator.configure();
+ logger.setLevel(Level.DEBUG);
+ Logger root = Logger.getRootLogger();
+ root.setLevel(Level.ERROR);
+ // start up local web server for crawl tests
+ File dir = new File(".");
+ logger.info("Starting NanoHTTPD webserver in " + dir.getAbsolutePath() + " on port 8000 ...");
+ try {
+ httpServer = new NanoHTTPD(8000, dir);
+ } catch (BindException e) {
+ logger.debug("assuming that local web server is already running");
+ } catch (IOException e1) {
+ e1.printStackTrace();
+ Assert.assertTrue("failed to start NanoHTTPD in current directory " + dir.getAbsolutePath(), false);
+ }
+ }
+
+ @AfterClass
+ public static void teardown() {
+ LogManager.shutdown();
+ httpServer.stop();
+ }
+
+ /**
+ * This unit test requires that a webserver be running locally on port 8000,
+ * what it serves does not matter.
+ *
+ * @throws IOException
+ * @throws FileNotFoundException
+ */
+ @Test
+ public void headlessRunTest0() throws IOException {
+ String jsonFileName = RUN_DESCRIPTION_DIR + "headlessRunTest0.json";
+ testHeadlessRun(jsonFileName);
+ }
+
+ /**
+ * This unit test requires Internet access to http://www.cnn.com
+ *
+ * @throws IOException
+ * @throws FileNotFoundException
+ */
+ @Ignore
+ public void headlessRunTestCNN() throws IOException {
+ String jsonFileName = RUN_DESCRIPTION_DIR + "headlessRunTestCNN.json";
+ testHeadlessRun(jsonFileName);
+ }
+
+ /**
+ * Helper method for headless run unit tests.
+ *
+ * @param fn filename of a .json file containing a headless run description
+ */
+ private static void testHeadlessRun(String fn) {
+ boolean flag = true;
+ try {
+ HeadlessRunDescription descr = HeadlessRunDescription.parse(fn);
+ BatchRunner runner = new BatchRunner();
+ ImmutableList<Result> results = runner.runHeadless(descr);
+ logger.info(results.toString());
+ } catch (Exception e) {
+ logger.error("testHeadlessRun: exception caught while running a headless run description");
+ logger.error(e.toString());
+ flag = false;
+ }
+ Assert.assertTrue(flag);
+ }
+
+}
diff --git a/src/batchtools/headless/src/test/java/com/galois/fiveui/CrawlParametersTest.java b/src/batchtools/headless/src/test/java/com/galois/fiveui/CrawlParametersTest.java
new file mode 100644
index 0000000..f5ffa35
--- /dev/null
+++ b/src/batchtools/headless/src/test/java/com/galois/fiveui/CrawlParametersTest.java
@@ -0,0 +1,107 @@
+package com.galois.fiveui;
+
+import org.apache.log4j.BasicConfigurator;
+import org.apache.log4j.LogManager;
+import org.apache.log4j.Logger;
+import org.junit.AfterClass;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import com.galois.fiveui.CrawlParameters;
+
+
+public class CrawlParametersTest {
+
+ private static Logger logger = Logger.getLogger("com.galois.fiveui.CrawlParameters");
+
+ @BeforeClass
+ public static void before() {
+ //if (!Logger.getRootLogger().getAllAppenders().hasMoreElements())
+ BasicConfigurator.configure();
+ logger.info("running unit tests...");
+ }
+
+ @AfterClass
+ public static void after() {
+ LogManager.shutdown();
+ }
+
+ @Test
+ public void testConstructorFields() {
+ CrawlParameters c;
+ try {
+ c = new CrawlParameters("0 1 100 foo");
+ } catch (Exception e) {
+ Assert.assertTrue("failed to parse crawl type string", false);
+ return;
+ }
+ Assert.assertArrayEquals(new int[]{0, 1, 100}, new int[]{c.depth, c.maxFetch, c.politeness});
+ Assert.assertEquals("foo", c.match);
+ }
+
+ @Test
+ public void testNone() {
+ CrawlParameters c = CrawlParameters.none();
+ Assert.assertTrue(c.isNone());
+ }
+
+ @Test
+ public void testMatchFcn() {
+ CrawlParameters c;
+ try {
+ c = new CrawlParameters("0 1 100 foo");
+ } catch (Exception e) {
+ Assert.assertTrue("failed to parse crawl type string", false);
+ return;
+ }
+ Assert.assertTrue("failed to match foo", c.matchFcn.apply("foo"));
+ Assert.assertFalse("failed to not match bar", c.matchFcn.apply("bar"));
+ }
+
+ @Test
+ public void testMatchFcnGlob1() {
+ CrawlParameters c;
+ try {
+ c = new CrawlParameters("0 1 100 foo*");
+ } catch (Exception e) {
+ Assert.assertTrue("failed to parse crawl type string", false);
+ return;
+ }
+ Assert.assertTrue("failed to match foo", c.matchFcn.apply("foo"));
+ Assert.assertFalse("failed to not match bar", c.matchFcn.apply("bar"));
+ Assert.assertTrue("failed to match foobar", c.matchFcn.apply("foobar"));
+ Assert.assertFalse("failed to not match barfoobar", c.matchFcn.apply("barfoobar"));
+ }
+
+ @Test
+ public void testMatchFcnGlob2() {
+ CrawlParameters c;
+ try {
+ c = new CrawlParameters("0 1 100 http://foo.com/*.html");
+ } catch (Exception e) {
+ Assert.assertTrue("failed to parse crawl type string", false);
+ return;
+ }
+ Assert.assertTrue("failed to match http://foo.com/index.html", c.matchFcn.apply("http://foo.com/index.html"));
+ Assert.assertTrue("failed to match http://foo.com/test/index.html", c.matchFcn.apply("http://foo.com/test/index.html"));
+ Assert.assertFalse("failed to not match http://bar.com/index.html", c.matchFcn.apply("http://bar.com/index.html"));
+ }
+
+ @Test
+ public void testMatchFcnGlob3() {
+ CrawlParameters c;
+ try {
+ c = new CrawlParameters("0 1 100 *foo.com*");
+ } catch (Exception e) {
+ Assert.assertTrue("failed to parse crawl type string", false);
+ return;
+ }
+ Assert.assertTrue("failed to match http://foo.com/index.html", c.matchFcn.apply("http://foo.com/index.html"));
+ Assert.assertTrue("failed to match http://foo.com/test/index.html", c.matchFcn.apply("http://foo.com/test/index.html"));
+ Assert.assertTrue("failed to match http://bar.foo.com", c.matchFcn.apply("http://bar.foo.com"));
+ Assert.assertFalse("failed to not match http://foobar.com", c.matchFcn.apply("http://foobar.com"));
+ }
+
+}
+
diff --git a/src/batchtools/headless/src/test/java/com/galois/fiveui/CrawlTest.java b/src/batchtools/headless/src/test/java/com/galois/fiveui/CrawlTest.java
new file mode 100644
index 0000000..0f932c2
--- /dev/null
+++ b/src/batchtools/headless/src/test/java/com/galois/fiveui/CrawlTest.java
@@ -0,0 +1,136 @@
+package com.galois.fiveui;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.BindException;
+import java.util.List;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import junit.framework.Assert;
+
+import org.apache.log4j.Level;
+import org.apache.log4j.LogManager;
+import org.apache.log4j.Logger;
+import org.apache.log4j.BasicConfigurator;
+
+import com.google.common.base.Function;
+import com.google.common.io.Files;
+
+import edu.uci.ics.crawler4j.util.IO;
+
+public class CrawlTest {
+
+ // TODO need a system independent way of getting the resources path
+ private static String resourceDir = "src/test/resources/crawlTest/";
+ private static Logger logger = Logger.getLogger("com.galois.fiveui.CrawlTest");
+ private static NanoHTTPD httpServer = null;
+
+ @BeforeClass
+ public static void setupCrawlTests() {
+ // Set up a simple configuration that logs on the console.
+ BasicConfigurator.configure();
+ logger.setLevel(Level.DEBUG);
+ Logger root = Logger.getRootLogger();
+ root.setLevel(Level.ERROR);
+
+ // start up local web server for crawl tests
+
+ logger.info("Starting NanoHTTPD webserver in " + resourceDir + " on port 8080 ...");
+ try {
+ httpServer = new NanoHTTPD(8080, new File(resourceDir));
+ } catch (BindException e) {
+ logger.info("assuming that local web server is already running");
+ } catch (IOException e1) {
+ e1.printStackTrace();
+ Assert.assertTrue("failed to start NanoHTTPD in resource directory", false);
+ }
+ }
+
+ @AfterClass
+ public static void teardown() {
+ LogManager.shutdown();
+ httpServer.stop();
+ }
+
+ // Requires Internet access
+ // @Test
+ public void corpDotGaloisCrawlTest() {
+ File tmpPath = Files.createTempDir();
+ BasicCrawlerController con =
+ new BasicCrawlerController("http://corp.galois.com",
+ "http://corp.galois.com",
+ 2, 5, 1000, 1,
+ tmpPath.getAbsolutePath());
+ List<String> urls = null;
+ try {
+ urls = con.go();
+ System.out.println(urls.toString());
+ } catch (Exception e) {
+ Assert.assertTrue("failed to complete webcrawl", false);
+ e.printStackTrace();
+ } finally {
+ IO.deleteFolder(tmpPath);
+ }
+
+ Assert.assertEquals((urls != null) && (urls.size() == 5), true);
+ }
+
+ @Test
+ public void testLocalCrawlDepth3one() {
+ doLocalCrawlTest("http://localhost:8080/one.html", 3, 10, 9);
+ }
+
+ @Test
+ public void testLocalCrawlDepth3two() {
+ doLocalCrawlTest("http://localhost:8080/two.html", 3, 10, 3);
+ }
+
+ @Test
+ public void testLocalCrawlDepth0one() {
+ doLocalCrawlTest("http://localhost:8080/one.html", 0, 10, 1);
+ }
+
+ @Test
+ public void testCrawlWithPredicate() {
+ CrawlParameters c = new CrawlParameters("5 5 100 *one.html");
+ doLocalCrawlTest("http://localhost:8080/one.html", c.matchFcn, c.depth, c.maxFetch, 1);
+ }
+
+ public void doLocalCrawlTest(String seed, int depth, int maxFetch, int oracle) {
+ Function<String, Boolean> pred = new Function<String, Boolean>() {
+ public Boolean apply(String s) {
+ return s.startsWith("http");
+ }
+ };
+ doLocalCrawlTest(seed, pred, depth, maxFetch, oracle);
+ }
+
+ public void doLocalCrawlTest(String seed, Function<String, Boolean> pred, int depth, int maxFetch, int oracle) {
+
+ logger.info("Starting localCrawlTest ...");
+ logger.info(" seed " + seed + ", depth " + depth);
+
+ File tmpPath = Files.createTempDir();
+ BasicCrawlerController con =
+ new BasicCrawlerController(seed, pred, depth, maxFetch, 100, 1,
+ tmpPath.getAbsolutePath());
+ List<String> urls = null;
+ try {
+ logger.info("Starting webcrawl ...");
+ urls = con.go();
+ logger.info("RETURN -- " + urls.toString());
+ } catch (Exception e) {
+ e.printStackTrace();
+ Assert.assertTrue("failed to run webcrawler", false);
+ } finally {
+ IO.deleteFolder(tmpPath);
+ }
+
+ // assert that we got oracle number of URLs
+ Assert.assertTrue("got " + urls.size() + " URLs, expected " + oracle,
+ (urls != null) && (urls.size() == oracle));
+ }
+
+}
diff --git a/src/batchtools/headless/src/test/java/com/galois/fiveui/HeadlessTest.java b/src/batchtools/headless/src/test/java/com/galois/fiveui/HeadlessTest.java
new file mode 100644
index 0000000..804d9f6
--- /dev/null
+++ b/src/batchtools/headless/src/test/java/com/galois/fiveui/HeadlessTest.java
@@ -0,0 +1,114 @@
+/**
+ *
+ */
+package com.galois.fiveui;
+
+import java.io.IOException;
+
+import junit.framework.Assert;
+
+import org.apache.log4j.BasicConfigurator;
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+
+/**
+ * @author bjones
+ *
+ */
+public class HeadlessTest {
+ private static final String RUN_DESCRIPTION_DIR = "src/test/resources/runDescriptions/";
+ private static Logger logger = Logger.getLogger("com.galois.fiveui.HeadlessTest");
+
+ @BeforeClass
+ public static void beforeClass() {
+ BasicConfigurator.configure();
+ Logger root = Logger.getRootLogger();
+ root.setLevel(Level.ERROR);
+ logger.setLevel(Level.DEBUG);
+ logger.debug("running headless tests...");
+ }
+ /**
+ * Test method for {@link com.galois.com.galois.fiveui.HeadlessRunDescription}, parses
+ * 'src/test/resources/runDescriptions/headlessSample0.json'.
+ *
+ * @throws IOException
+ */
+ @Test
+ public final void testDeserialize_headlessSample0() throws IOException {
+
+ String jsonFileName = RUN_DESCRIPTION_DIR + "headlessSample0.json";
+ String ruleSetLoc =
+ RUN_DESCRIPTION_DIR + "../ruleSets/emptyRuleSet.json";
+ RuleSet ruleSetOracle = RuleSet.parseFile(ruleSetLoc);
+ HeadlessAtom headlessAtomOracle =
+ new HeadlessAtom("http://testhost", ruleSetOracle);
+ HeadlessRunDescription oracle =
+ new HeadlessRunDescription(ImmutableList.of(headlessAtomOracle));
+
+ HeadlessRunDescription actual = HeadlessRunDescription.parse(jsonFileName);
+ assertObjEqual("Object deserialized incorrectly.", oracle, actual);
+ }
+
+ /**
+ * Test method for {@link com.galois.com.galois.fiveui.HeadlessRunDescription}, parses
+ * 'src/test/resources/runDescriptions/headlessSample1.json'.
+ *
+ * @throws IOException
+ */
+ @Test
+ public final void testDeserialize_headlessSample1() throws IOException {
+
+ String jsonFileName = RUN_DESCRIPTION_DIR + "headlessSample1.json";
+ String ruleSetLoc = RUN_DESCRIPTION_DIR +
+ "../../../../../rsTester/src/test/resources/ruleSets/headingGuidelines.json";
+ RuleSet ruleSetOracle = RuleSet.parseFile(ruleSetLoc);
+ HeadlessAtom headlessAtomOracle =
+ new HeadlessAtom("http://testhost", ruleSetOracle);
+ HeadlessRunDescription oracle =
+ new HeadlessRunDescription(ImmutableList.of(headlessAtomOracle));
+
+ HeadlessRunDescription actual = HeadlessRunDescription.parse(jsonFileName);
+ assertObjEqual("Object deserialized incorrectly.", oracle, actual);
+ }
+
+ /**
+ * Test method for {@link com.galois.com.galois.fiveui.HeadlessRunDescription}, parses
+ * 'src/test/resources/runDescriptions/headlessSample2.json'.
+ *
+ * @throws IOException
+ */
+ @Test
+ public final void testDeserialize_headlessSample2() throws IOException {
+
+ String jsonFileName = RUN_DESCRIPTION_DIR + "headlessSample2.json";
+ // manually build first HeadlessAtom
+ String ruleSetLoc1 =
+ RUN_DESCRIPTION_DIR + "../ruleSets/emptyRuleSet.json";
+ RuleSet ruleSetOracle1 = RuleSet.parseFile(ruleSetLoc1);
+ HeadlessAtom headlessAtomOracle1 =
+ new HeadlessAtom("http://testhost1", ruleSetOracle1);
+ // manually build second HeadlessAtom
+ String ruleSetLoc2 = RUN_DESCRIPTION_DIR +
+ "../../../../../rsTester/src/test/resources/ruleSets/headingGuidelines.json";
+
+ RuleSet ruleSetOracle2 = RuleSet.parseFile(ruleSetLoc2);
+ HeadlessAtom headlessAtomOracle2 =
+ new HeadlessAtom("http://testhost2", ruleSetOracle2);
+
+ HeadlessRunDescription oracle =
+ new HeadlessRunDescription(ImmutableList.of(headlessAtomOracle1,
+ headlessAtomOracle2));
+
+ HeadlessRunDescription actual = HeadlessRunDescription.parse(jsonFileName);
+ assertObjEqual("Object deserialized incorrectly.", oracle, actual);
+ }
+
+ private void assertObjEqual(String msg, Object oracle, Object actual) {
+ Assert.assertTrue(msg + ";\n expected: "+oracle+"\n actual: "+actual,
+ oracle.equals(actual));
+ }
+}
diff --git a/src/batchtools/headless/src/test/java/com/galois/fiveui/NanoHTTPD.java b/src/batchtools/headless/src/test/java/com/galois/fiveui/NanoHTTPD.java
new file mode 100644
index 0000000..53434be
--- /dev/null
+++ b/src/batchtools/headless/src/test/java/com/galois/fiveui/NanoHTTPD.java
@@ -0,0 +1,1122 @@
+package com.galois.fiveui;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.io.PrintWriter;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.URLEncoder;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.Vector;
+import java.util.Hashtable;
+import java.util.Locale;
+import java.util.Properties;
+import java.util.StringTokenizer;
+import java.util.TimeZone;
+
+import java.io.ByteArrayOutputStream;
+import java.io.FileOutputStream;
+
+/**
+ * A simple, tiny, nicely embeddable HTTP 1.0 (partially 1.1) server in Java
+ *
+ * <p> NanoHTTPD version 1.25,
+ * Copyright &copy; 2001,2005-2012 Jarno Elonen (elonen@iki.fi, http://iki.fi/elonen/)
+ * and Copyright &copy; 2010 Konstantinos Togias (info@ktogias.gr, http://ktogias.gr)
+ *
+ * <p><b>Features + limitations: </b><ul>
+ *
+ * <li> Only one Java file </li>
+ * <li> Java 1.1 compatible </li>
+ * <li> Released as open source, Modified BSD licence </li>
+ * <li> No fixed config files, logging, authorization etc. (Implement yourself if you need them.) </li>
+ * <li> Supports parameter parsing of GET and POST methods (+ rudimentary PUT support in 1.25) </li>
+ * <li> Supports both dynamic content and file serving </li>
+ * <li> Supports file upload (since version 1.2, 2010) </li>
+ * <li> Supports partial content (streaming)</li>
+ * <li> Supports ETags</li>
+ * <li> Never caches anything </li>
+ * <li> Doesn't limit bandwidth, request time or simultaneous connections </li>
+ * <li> Default code serves files and shows all HTTP parameters and headers</li>
+ * <li> File server supports directory listing, index.html and index.htm</li>
+ * <li> File server supports partial content (streaming)</li>
+ * <li> File server supports ETags</li>
+ * <li> File server does the 301 redirection trick for directories without '/'</li>
+ * <li> File server supports simple skipping for files (continue download) </li>
+ * <li> File server serves also very long files without memory overhead </li>
+ * <li> Contains a built-in list of most common mime types </li>
+ * <li> All header names are converted lowercase so they don't vary between browsers/clients </li>
+ *
+ * </ul>
+ *
+ * <p><b>Ways to use: </b><ul>
+ *
+ * <li> Run as a standalone app, serves files and shows requests</li>
+ * <li> Subclass serve() and embed to your own program </li>
+ * <li> Call serveFile() from serve() with your own base directory </li>
+ *
+ * </ul>
+ *
+ * See the end of the source file for distribution license
+ * (Modified BSD licence)
+ */
+public class NanoHTTPD
+{
+ // ==================================================
+ // API parts
+ // ==================================================
+
+ /**
+ * Override this to customize the server.<p>
+ *
+ * (By default, this delegates to serveFile() and allows directory listing.)
+ *
+ * @param uri Percent-decoded URI without parameters, for example "/index.cgi"
+ * @param method "GET", "POST" etc.
+ * @param parms Parsed, percent decoded parameters from URI and, in case of POST, data.
+ * @param header Header entries, percent decoded
+ * @return HTTP response, see class Response for details
+ */
+ public Response serve( String uri, String method, Properties header, Properties parms, Properties files )
+ {
+ myOut.println( method + " '" + uri + "' " );
+
+ Enumeration<?> e = header.propertyNames();
+ while ( e.hasMoreElements())
+ {
+ String value = (String)e.nextElement();
+ myOut.println( " HDR: '" + value + "' = '" +
+ header.getProperty( value ) + "'" );
+ }
+ e = parms.propertyNames();
+ while ( e.hasMoreElements())
+ {
+ String value = (String)e.nextElement();
+ myOut.println( " PRM: '" + value + "' = '" +
+ parms.getProperty( value ) + "'" );
+ }
+ e = files.propertyNames();
+ while ( e.hasMoreElements())
+ {
+ String value = (String)e.nextElement();
+ myOut.println( " UPLOADED: '" + value + "' = '" +
+ files.getProperty( value ) + "'" );
+ }
+
+ return serveFile( uri, header, myRootDir, true );
+ }
+
+ /**
+ * HTTP response.
+ * Return one of these from serve().
+ */
+ public class Response
+ {
+ /**
+ * Default constructor: response = HTTP_OK, data = mime = 'null'
+ */
+ public Response()
+ {
+ this.status = HTTP_OK;
+ }
+
+ /**
+ * Basic constructor.
+ */
+ public Response( String status, String mimeType, InputStream data )
+ {
+ this.status = status;
+ this.mimeType = mimeType;
+ this.data = data;
+ }
+
+ /**
+ * Convenience method that makes an InputStream out of
+ * given text.
+ */
+ public Response( String status, String mimeType, String txt )
+ {
+ this.status = status;
+ this.mimeType = mimeType;
+ try
+ {
+ this.data = new ByteArrayInputStream( txt.getBytes("UTF-8"));
+ }
+ catch ( java.io.UnsupportedEncodingException uee )
+ {
+ uee.printStackTrace();
+ }
+ }
+
+ /**
+ * Adds given line to the header.
+ */
+ public void addHeader( String name, String value )
+ {
+ header.put( name, value );
+ }
+
+ /**
+ * HTTP status code after processing, e.g. "200 OK", HTTP_OK
+ */
+ public String status;
+
+ /**
+ * MIME type of content, e.g. "text/html"
+ */
+ public String mimeType;
+
+ /**
+ * Data of the response, may be null.
+ */
+ public InputStream data;
+
+ /**
+ * Headers for the HTTP response. Use addHeader()
+ * to add lines.
+ */
+ public Properties header = new Properties();
+ }
+
+ /**
+ * Some HTTP response status codes
+ */
+ public static final String
+ HTTP_OK = "200 OK",
+ HTTP_PARTIALCONTENT = "206 Partial Content",
+ HTTP_RANGE_NOT_SATISFIABLE = "416 Requested Range Not Satisfiable",
+ HTTP_REDIRECT = "301 Moved Permanently",
+ HTTP_NOTMODIFIED = "304 Not Modified",
+ HTTP_FORBIDDEN = "403 Forbidden",
+ HTTP_NOTFOUND = "404 Not Found",
+ HTTP_BADREQUEST = "400 Bad Request",
+ HTTP_INTERNALERROR = "500 Internal Server Error",
+ HTTP_NOTIMPLEMENTED = "501 Not Implemented";
+
+ /**
+ * Common mime types for dynamic content
+ */
+ public static final String
+ MIME_PLAINTEXT = "text/plain",
+ MIME_HTML = "text/html",
+ MIME_DEFAULT_BINARY = "application/octet-stream",
+ MIME_XML = "text/xml";
+
+ // ==================================================
+ // Socket & server code
+ // ==================================================
+
+ /**
+ * Starts a HTTP server to given port.<p>
+ * Throws an IOException if the socket is already in use
+ */
+ public NanoHTTPD( int port, File wwwroot ) throws IOException
+ {
+ myTcpPort = port;
+ this.myRootDir = wwwroot;
+ myServerSocket = new ServerSocket( myTcpPort );
+ myThread = new Thread( new Runnable()
+ {
+ public void run()
+ {
+ try
+ {
+ while( true )
+ new HTTPSession( myServerSocket.accept());
+ }
+ catch ( IOException ioe )
+ {}
+ }
+ });
+ myThread.setDaemon( true );
+ myThread.start();
+ }
+
+ /**
+ * Stops the server.
+ */
+ public void stop()
+ {
+ try
+ {
+ myServerSocket.close();
+ myThread.join();
+ }
+ catch ( IOException ioe ) {}
+ catch ( InterruptedException e ) {}
+ }
+
+
+ /**
+ * Starts as a standalone file server and waits for Enter.
+ */
+ public static void main( String[] args )
+ {
+ myOut.println( "NanoHTTPD 1.25 (C) 2001,2005-2011 Jarno Elonen and (C) 2010 Konstantinos Togias\n" +
+ "(Command line options: [-p port] [-d root-dir] [--licence])\n" );
+
+ // Defaults
+ int port = 80;
+ File wwwroot = new File(".").getAbsoluteFile();
+
+ // Show licence if requested
+ for ( int i=0; i<args.length; ++i )
+ if(args[i].equalsIgnoreCase("-p"))
+ port = Integer.parseInt( args[i+1] );
+ else if(args[i].equalsIgnoreCase("-d"))
+ wwwroot = new File( args[i+1] ).getAbsoluteFile();
+ else if ( args[i].toLowerCase().endsWith( "licence" ))
+ {
+ myOut.println( LICENCE + "\n" );
+ break;
+ }
+
+ try
+ {
+ new NanoHTTPD( port, wwwroot );
+ }
+ catch( IOException ioe )
+ {
+ myErr.println( "Couldn't start server:\n" + ioe );
+ System.exit( -1 );
+ }
+
+ myOut.println( "Now serving files in port " + port + " from \"" + wwwroot + "\"" );
+ myOut.println( "Hit Enter to stop.\n" );
+
+ try { System.in.read(); } catch( Throwable t ) {}
+ }
+
+ /**
+ * Handles one session, i.e. parses the HTTP request
+ * and returns the response.
+ */
+ private class HTTPSession implements Runnable
+ {
+ public HTTPSession( Socket s )
+ {
+ mySocket = s;
+ Thread t = new Thread( this );
+ t.setDaemon( true );
+ t.start();
+ }
+
+ public void run()
+ {
+ try
+ {
+ InputStream is = mySocket.getInputStream();
+ if ( is == null) return;
+
+ // Read the first 8192 bytes.
+ // The full header should fit in here.
+ // Apache's default header limit is 8KB.
+ int bufsize = 8192;
+ byte[] buf = new byte[bufsize];
+ int rlen = is.read(buf, 0, bufsize);
+ if (rlen <= 0) return;
+
+ // Create a BufferedReader for parsing the header.
+ ByteArrayInputStream hbis = new ByteArrayInputStream(buf, 0, rlen);
+ BufferedReader hin = new BufferedReader( new InputStreamReader( hbis ));
+ Properties pre = new Properties();
+ Properties parms = new Properties();
+ Properties header = new Properties();
+ Properties files = new Properties();
+
+ // Decode the header into parms and header java properties
+ decodeHeader(hin, pre, parms, header);
+ String method = pre.getProperty("method");
+ String uri = pre.getProperty("uri");
+
+ long size = 0x7FFFFFFFFFFFFFFFl;
+ String contentLength = header.getProperty("content-length");
+ if (contentLength != null)
+ {
+ try { size = Integer.parseInt(contentLength); }
+ catch (NumberFormatException ex) {}
+ }
+
+ // We are looking for the byte separating header from body.
+ // It must be the last byte of the first two sequential new lines.
+ int splitbyte = 0;
+ boolean sbfound = false;
+ while (splitbyte < rlen)
+ {
+ if (buf[splitbyte] == '\r' && buf[++splitbyte] == '\n' && buf[++splitbyte] == '\r' && buf[++splitbyte] == '\n') {
+ sbfound = true;
+ break;
+ }
+ splitbyte++;
+ }
+ splitbyte++;
+
+ // Write the part of body already read to ByteArrayOutputStream f
+ ByteArrayOutputStream f = new ByteArrayOutputStream();
+ if (splitbyte < rlen) f.write(buf, splitbyte, rlen-splitbyte);
+
+ // While Firefox sends on the first read all the data fitting
+ // our buffer, Chrome and Opera sends only the headers even if
+ // there is data for the body. So we do some magic here to find
+ // out whether we have already consumed part of body, if we
+ // have reached the end of the data to be sent or we should
+ // expect the first byte of the body at the next read.
+ if (splitbyte < rlen)
+ size -= rlen - splitbyte +1;
+ else if (!sbfound || size == 0x7FFFFFFFFFFFFFFFl)
+ size = 0;
+
+ // Now read all the body and write it to f
+ buf = new byte[512];
+ while ( rlen >= 0 && size > 0 )
+ {
+ rlen = is.read(buf, 0, 512);
+ size -= rlen;
+ if (rlen > 0)
+ f.write(buf, 0, rlen);
+ }
+
+ // Get the raw body as a byte []
+ byte [] fbuf = f.toByteArray();
+
+ // Create a BufferedReader for easily reading it as string.
+ ByteArrayInputStream bin = new ByteArrayInputStream(fbuf);
+ BufferedReader in = new BufferedReader( new InputStreamReader(bin));
+
+ // If the method is POST, there may be parameters
+ // in data section, too, read it:
+ if ( method.equalsIgnoreCase( "POST" ))
+ {
+ String contentType = "";
+ String contentTypeHeader = header.getProperty("content-type");
+ StringTokenizer st = new StringTokenizer( contentTypeHeader , "; " );
+ if ( st.hasMoreTokens()) {
+ contentType = st.nextToken();
+ }
+
+ if (contentType.equalsIgnoreCase("multipart/form-data"))
+ {
+ // Handle multipart/form-data
+ if ( !st.hasMoreTokens())
+ sendError( HTTP_BADREQUEST, "BAD REQUEST: Content type is multipart/form-data but boundary missing. Usage: GET /example/file.html" );
+ String boundaryExp = st.nextToken();
+ st = new StringTokenizer( boundaryExp , "=" );
+ if (st.countTokens() != 2)
+ sendError( HTTP_BADREQUEST, "BAD REQUEST: Content type is multipart/form-data but boundary syntax error. Usage: GET /example/file.html" );
+ st.nextToken();
+ String boundary = st.nextToken();
+
+ decodeMultipartData(boundary, fbuf, in, parms, files);
+ }
+ else
+ {
+ // Handle application/x-www-form-urlencoded
+ String postLine = "";
+ char pbuf[] = new char[512];
+ int read = in.read(pbuf);
+ while ( read >= 0 && !postLine.endsWith("\r\n") )
+ {
+ postLine += String.valueOf(pbuf, 0, read);
+ read = in.read(pbuf);
+ }
+ postLine = postLine.trim();
+ decodeParms( postLine, parms );
+ }
+ }
+
+ if ( method.equalsIgnoreCase( "PUT" ))
+ files.put("content", saveTmpFile( fbuf, 0, f.size()));
+
+ // Ok, now do the serve()
+ Response r = serve( uri, method, header, parms, files );
+ if ( r == null )
+ sendError( HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: Serve() returned a null response." );
+ else
+ sendResponse( r.status, r.mimeType, r.header, r.data );
+
+ in.close();
+ is.close();
+ }
+ catch ( IOException ioe )
+ {
+ try
+ {
+ sendError( HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
+ }
+ catch ( Throwable t ) {}
+ }
+ catch ( InterruptedException ie )
+ {
+ // Thrown by sendError, ignore and exit the thread.
+ }
+ }
+
+ /**
+ * Decodes the sent headers and loads the data into
+ * java Properties' key - value pairs
+ **/
+ private void decodeHeader(BufferedReader in, Properties pre, Properties parms, Properties header)
+ throws InterruptedException
+ {
+ try {
+ // Read the request line
+ String inLine = in.readLine();
+ if (inLine == null) return;
+ StringTokenizer st = new StringTokenizer( inLine );
+ if ( !st.hasMoreTokens())
+ sendError( HTTP_BADREQUEST, "BAD REQUEST: Syntax error. Usage: GET /example/file.html" );
+
+ String method = st.nextToken();
+ pre.put("method", method);
+
+ if ( !st.hasMoreTokens())
+ sendError( HTTP_BADREQUEST, "BAD REQUEST: Missing URI. Usage: GET /example/file.html" );
+
+ String uri = st.nextToken();
+
+ // Decode parameters from the URI
+ int qmi = uri.indexOf( '?' );
+ if ( qmi >= 0 )
+ {
+ decodeParms( uri.substring( qmi+1 ), parms );
+ uri = decodePercent( uri.substring( 0, qmi ));
+ }
+ else uri = decodePercent(uri);
+
+ // If there's another token, it's protocol version,
+ // followed by HTTP headers. Ignore version but parse headers.
+ // NOTE: this now forces header names lowercase since they are
+ // case insensitive and vary by client.
+ if ( st.hasMoreTokens())
+ {
+ String line = in.readLine();
+ while ( line != null && line.trim().length() > 0 )
+ {
+ int p = line.indexOf( ':' );
+ if ( p >= 0 )
+ header.put( line.substring(0,p).trim().toLowerCase(), line.substring(p+1).trim());
+ line = in.readLine();
+ }
+ }
+
+ pre.put("uri", uri);
+ }
+ catch ( IOException ioe )
+ {
+ sendError( HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
+ }
+ }
+
+ /**
+ * Decodes the Multipart Body data and put it
+ * into java Properties' key - value pairs.
+ **/
+ private void decodeMultipartData(String boundary, byte[] fbuf, BufferedReader in, Properties parms, Properties files)
+ throws InterruptedException
+ {
+ try
+ {
+ int[] bpositions = getBoundaryPositions(fbuf,boundary.getBytes());
+ int boundarycount = 1;
+ String mpline = in.readLine();
+ while ( mpline != null )
+ {
+ if (mpline.indexOf(boundary) == -1)
+ sendError( HTTP_BADREQUEST, "BAD REQUEST: Content type is multipart/form-data but next chunk does not start with boundary. Usage: GET /example/file.html" );
+ boundarycount++;
+ Properties item = new Properties();
+ mpline = in.readLine();
+ while (mpline != null && mpline.trim().length() > 0)
+ {
+ int p = mpline.indexOf( ':' );
+ if (p != -1)
+ item.put( mpline.substring(0,p).trim().toLowerCase(), mpline.substring(p+1).trim());
+ mpline = in.readLine();
+ }
+ if (mpline != null)
+ {
+ String contentDisposition = item.getProperty("content-disposition");
+ if (contentDisposition == null)
+ {
+ sendError( HTTP_BADREQUEST, "BAD REQUEST: Content type is multipart/form-data but no content-disposition info found. Usage: GET /example/file.html" );
+ }
+ StringTokenizer st = new StringTokenizer( contentDisposition , "; " );
+ Properties disposition = new Properties();
+ while ( st.hasMoreTokens())
+ {
+ String token = st.nextToken();
+ int p = token.indexOf( '=' );
+ if (p!=-1)
+ disposition.put( token.substring(0,p).trim().toLowerCase(), token.substring(p+1).trim());
+ }
+ String pname = disposition.getProperty("name");
+ pname = pname.substring(1,pname.length()-1);
+
+ String value = "";
+ if (item.getProperty("content-type") == null) {
+ while (mpline != null && mpline.indexOf(boundary) == -1)
+ {
+ mpline = in.readLine();
+ if ( mpline != null)
+ {
+ int d = mpline.indexOf(boundary);
+ if (d == -1)
+ value+=mpline;
+ else
+ value+=mpline.substring(0,d-2);
+ }
+ }
+ }
+ else
+ {
+ if (boundarycount> bpositions.length)
+ sendError( HTTP_INTERNALERROR, "Error processing request" );
+ int offset = stripMultipartHeaders(fbuf, bpositions[boundarycount-2]);
+ String path = saveTmpFile(fbuf, offset, bpositions[boundarycount-1]-offset-4);
+ files.put(pname, path);
+ value = disposition.getProperty("filename");
+ value = value.substring(1,value.length()-1);
+ do {
+ mpline = in.readLine();
+ } while (mpline != null && mpline.indexOf(boundary) == -1);
+ }
+ parms.put(pname, value);
+ }
+ }
+ }
+ catch ( IOException ioe )
+ {
+ sendError( HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
+ }
+ }
+
+ /**
+ * Find the byte positions where multipart boundaries start.
+ **/
+ public int[] getBoundaryPositions(byte[] b, byte[] boundary)
+ {
+ int matchcount = 0;
+ int matchbyte = -1;
+ Vector<Integer> matchbytes = new Vector<Integer>();
+ for (int i=0; i<b.length; i++)
+ {
+ if (b[i] == boundary[matchcount])
+ {
+ if (matchcount == 0)
+ matchbyte = i;
+ matchcount++;
+ if (matchcount==boundary.length)
+ {
+ matchbytes.addElement(new Integer(matchbyte));
+ matchcount = 0;
+ matchbyte = -1;
+ }
+ }
+ else
+ {
+ i -= matchcount;
+ matchcount = 0;
+ matchbyte = -1;
+ }
+ }
+ int[] ret = new int[matchbytes.size()];
+ for (int i=0; i < ret.length; i++)
+ {
+ ret[i] = ((Integer)matchbytes.elementAt(i)).intValue();
+ }
+ return ret;
+ }
+
+ /**
+ * Retrieves the content of a sent file and saves it
+ * to a temporary file.
+ * The full path to the saved file is returned.
+ **/
+ private String saveTmpFile(byte[] b, int offset, int len)
+ {
+ String path = "";
+ if (len > 0)
+ {
+ String tmpdir = System.getProperty("java.io.tmpdir");
+ try {
+ File temp = File.createTempFile("NanoHTTPD", "", new File(tmpdir));
+ OutputStream fstream = new FileOutputStream(temp);
+ fstream.write(b, offset, len);
+ fstream.close();
+ path = temp.getAbsolutePath();
+ } catch (Exception e) { // Catch exception if any
+ myErr.println("Error: " + e.getMessage());
+ }
+ }
+ return path;
+ }
+
+
+ /**
+ * It returns the offset separating multipart file headers
+ * from the file's data.
+ **/
+ private int stripMultipartHeaders(byte[] b, int offset)
+ {
+ int i = 0;
+ for (i=offset; i<b.length; i++)
+ {
+ if (b[i] == '\r' && b[++i] == '\n' && b[++i] == '\r' && b[++i] == '\n')
+ break;
+ }
+ return i+1;
+ }
+
+ /**
+ * Decodes the percent encoding scheme. <br/>
+ * For example: "an+example%20string" -> "an example string"
+ */
+ private String decodePercent( String str ) throws InterruptedException
+ {
+ try
+ {
+ StringBuffer sb = new StringBuffer();
+ for( int i=0; i<str.length(); i++ )
+ {
+ char c = str.charAt( i );
+ switch ( c )
+ {
+ case '+':
+ sb.append( ' ' );
+ break;
+ case '%':
+ sb.append((char)Integer.parseInt( str.substring(i+1,i+3), 16 ));
+ i += 2;
+ break;
+ default:
+ sb.append( c );
+ break;
+ }
+ }
+ return sb.toString();
+ }
+ catch( Exception e )
+ {
+ sendError( HTTP_BADREQUEST, "BAD REQUEST: Bad percent-encoding." );
+ return null;
+ }
+ }
+
+ /**
+ * Decodes parameters in percent-encoded URI-format
+ * ( e.g. "name=Jack%20Daniels&pass=Single%20Malt" ) and
+ * adds them to given Properties. NOTE: this doesn't support multiple
+ * identical keys due to the simplicity of Properties -- if you need multiples,
+ * you might want to replace the Properties with a Hashtable of Vectors or such.
+ */
+ private void decodeParms( String parms, Properties p )
+ throws InterruptedException
+ {
+ if ( parms == null )
+ return;
+
+ StringTokenizer st = new StringTokenizer( parms, "&" );
+ while ( st.hasMoreTokens())
+ {
+ String e = st.nextToken();
+ int sep = e.indexOf( '=' );
+ if ( sep >= 0 )
+ p.put( decodePercent( e.substring( 0, sep )).trim(),
+ decodePercent( e.substring( sep+1 )));
+ }
+ }
+
+ /**
+ * Returns an error message as a HTTP response and
+ * throws InterruptedException to stop further request processing.
+ */
+ private void sendError( String status, String msg ) throws InterruptedException
+ {
+ sendResponse( status, MIME_PLAINTEXT, null, new ByteArrayInputStream( msg.getBytes()));
+ throw new InterruptedException();
+ }
+
+ /**
+ * Sends given response to the socket.
+ */
+ private void sendResponse( String status, String mime, Properties header, InputStream data )
+ {
+ try
+ {
+ if ( status == null )
+ throw new Error( "sendResponse(): Status can't be null." );
+
+ OutputStream out = mySocket.getOutputStream();
+ PrintWriter pw = new PrintWriter( out );
+ pw.print("HTTP/1.0 " + status + " \r\n");
+
+ if ( mime != null )
+ pw.print("Content-Type: " + mime + "\r\n");
+
+ if ( header == null || header.getProperty( "Date" ) == null )
+ pw.print( "Date: " + gmtFrmt.format( new Date()) + "\r\n");
+
+ if ( header != null )
+ {
+ Enumeration<Object> e = header.keys();
+ while ( e.hasMoreElements())
+ {
+ String key = (String)e.nextElement();
+ String value = header.getProperty( key );
+ pw.print( key + ": " + value + "\r\n");
+ }
+ }
+
+ pw.print("\r\n");
+ pw.flush();
+
+ if ( data != null )
+ {
+ int pending = data.available(); // This is to support partial sends, see serveFile()
+ byte[] buff = new byte[theBufferSize];
+ while (pending>0)
+ {
+ int read = data.read( buff, 0, ( (pending>theBufferSize) ? theBufferSize : pending ));
+ if (read <= 0) break;
+ out.write( buff, 0, read );
+ pending -= read;
+ }
+ }
+ out.flush();
+ out.close();
+ if ( data != null )
+ data.close();
+ }
+ catch( IOException ioe )
+ {
+ // Couldn't write? No can do.
+ try { mySocket.close(); } catch( Throwable t ) {}
+ }
+ }
+
+ private Socket mySocket;
+ }
+
+ /**
+ * URL-encodes everything between "/"-characters.
+ * Encodes spaces as '%20' instead of '+'.
+ */
+ @SuppressWarnings("deprecation")
+ private String encodeUri( String uri )
+ {
+ String newUri = "";
+ StringTokenizer st = new StringTokenizer( uri, "/ ", true );
+ while ( st.hasMoreTokens())
+ {
+ String tok = st.nextToken();
+ if ( tok.equals( "/" ))
+ newUri += "/";
+ else if ( tok.equals( " " ))
+ newUri += "%20";
+ else
+ {
+ newUri += URLEncoder.encode( tok );
+ // For Java 1.4 you'll want to use this instead:
+ // try { newUri += URLEncoder.encode( tok, "UTF-8" ); } catch ( java.io.UnsupportedEncodingException uee ) {}
+ }
+ }
+ return newUri;
+ }
+
+ private int myTcpPort;
+ private final ServerSocket myServerSocket;
+ private Thread myThread;
+ private File myRootDir;
+
+ // ==================================================
+ // File server code
+ // ==================================================
+
+ /**
+ * Serves file from homeDir and its' subdirectories (only).
+ * Uses only URI, ignores all headers and HTTP parameters.
+ */
+ public Response serveFile( String uri, Properties header, File homeDir,
+ boolean allowDirectoryListing )
+ {
+ Response res = null;
+
+ // Make sure we won't die of an exception later
+ if ( !homeDir.isDirectory())
+ res = new Response( HTTP_INTERNALERROR, MIME_PLAINTEXT,
+ "INTERNAL ERRROR: serveFile(): given homeDir is not a directory." );
+
+ if ( res == null )
+ {
+ // Remove URL arguments
+ uri = uri.trim().replace( File.separatorChar, '/' );
+ if ( uri.indexOf( '?' ) >= 0 )
+ uri = uri.substring(0, uri.indexOf( '?' ));
+
+ // Prohibit getting out of current directory
+ if ( uri.startsWith( ".." ) || uri.endsWith( ".." ) || uri.indexOf( "../" ) >= 0 )
+ res = new Response( HTTP_FORBIDDEN, MIME_PLAINTEXT,
+ "FORBIDDEN: Won't serve ../ for security reasons." );
+ }
+
+ File f = new File( homeDir, uri );
+ if ( res == null && !f.exists())
+ res = new Response( HTTP_NOTFOUND, MIME_PLAINTEXT,
+ "Error 404, file not found." );
+
+ // List the directory, if necessary
+ if ( res == null && f.isDirectory())
+ {
+ // Browsers get confused without '/' after the
+ // directory, send a redirect.
+ if ( !uri.endsWith( "/" ))
+ {
+ uri += "/";
+ res = new Response( HTTP_REDIRECT, MIME_HTML,
+ "<html><body>Redirected: <a href=\"" + uri + "\">" +
+ uri + "</a></body></html>");
+ res.addHeader( "Location", uri );
+ }
+
+ if ( res == null )
+ {
+ // First try index.html and index.htm
+ if ( new File( f, "index.html" ).exists())
+ f = new File( homeDir, uri + "/index.html" );
+ else if ( new File( f, "index.htm" ).exists())
+ f = new File( homeDir, uri + "/index.htm" );
+ // No index file, list the directory if it is readable
+ else if ( allowDirectoryListing && f.canRead() )
+ {
+ String[] files = f.list();
+ String msg = "<html><body><h1>Directory " + uri + "</h1><br/>";
+
+ if ( uri.length() > 1 )
+ {
+ String u = uri.substring( 0, uri.length()-1 );
+ int slash = u.lastIndexOf( '/' );
+ if ( slash >= 0 && slash < u.length())
+ msg += "<b><a href=\"" + uri.substring(0, slash+1) + "\">..</a></b><br/>";
+ }
+
+ if (files!=null)
+ {
+ for ( int i=0; i<files.length; ++i )
+ {
+ File curFile = new File( f, files[i] );
+ boolean dir = curFile.isDirectory();
+ if ( dir )
+ {
+ msg += "<b>";
+ files[i] += "/";
+ }
+
+ msg += "<a href=\"" + encodeUri( uri + files[i] ) + "\">" +
+ files[i] + "</a>";
+
+ // Show file size
+ if ( curFile.isFile())
+ {
+ long len = curFile.length();
+ msg += " &nbsp;<font size=2>(";
+ if ( len < 1024 )
+ msg += len + " bytes";
+ else if ( len < 1024 * 1024 )
+ msg += len/1024 + "." + (len%1024/10%100) + " KB";
+ else
+ msg += len/(1024*1024) + "." + len%(1024*1024)/10%100 + " MB";
+
+ msg += ")</font>";
+ }
+ msg += "<br/>";
+ if ( dir ) msg += "</b>";
+ }
+ }
+ msg += "</body></html>";
+ res = new Response( HTTP_OK, MIME_HTML, msg );
+ }
+ else
+ {
+ res = new Response( HTTP_FORBIDDEN, MIME_PLAINTEXT,
+ "FORBIDDEN: No directory listing." );
+ }
+ }
+ }
+
+ try
+ {
+ if ( res == null )
+ {
+ // Get MIME type from file name extension, if possible
+ String mime = null;
+ int dot = f.getCanonicalPath().lastIndexOf( '.' );
+ if ( dot >= 0 )
+ mime = (String)theMimeTypes.get( f.getCanonicalPath().substring( dot + 1 ).toLowerCase());
+ if ( mime == null )
+ mime = MIME_DEFAULT_BINARY;
+
+ // Calculate etag
+ String etag = Integer.toHexString((f.getAbsolutePath() + f.lastModified() + "" + f.length()).hashCode());
+
+ // Support (simple) skipping:
+ long startFrom = 0;
+ long endAt = -1;
+ String range = header.getProperty( "range" );
+ if ( range != null )
+ {
+ if ( range.startsWith( "bytes=" ))
+ {
+ range = range.substring( "bytes=".length());
+ int minus = range.indexOf( '-' );
+ try {
+ if ( minus > 0 )
+ {
+ startFrom = Long.parseLong( range.substring( 0, minus ));
+ endAt = Long.parseLong( range.substring( minus+1 ));
+ }
+ }
+ catch ( NumberFormatException nfe ) {}
+ }
+ }
+
+ // Change return code and add Content-Range header when skipping is requested
+ long fileLen = f.length();
+ if (range != null && startFrom >= 0)
+ {
+ if ( startFrom >= fileLen)
+ {
+ res = new Response( HTTP_RANGE_NOT_SATISFIABLE, MIME_PLAINTEXT, "" );
+ res.addHeader( "Content-Range", "bytes 0-0/" + fileLen);
+ res.addHeader( "ETag", etag);
+ }
+ else
+ {
+ if ( endAt < 0 )
+ endAt = fileLen-1;
+ long newLen = endAt - startFrom + 1;
+ if ( newLen < 0 ) newLen = 0;
+
+ final long dataLen = newLen;
+ FileInputStream fis = new FileInputStream( f ) {
+ public int available() throws IOException { return (int)dataLen; }
+ };
+ fis.skip( startFrom );
+
+ res = new Response( HTTP_PARTIALCONTENT, mime, fis );
+ res.addHeader( "Content-Length", "" + dataLen);
+ res.addHeader( "Content-Range", "bytes " + startFrom + "-" + endAt + "/" + fileLen);
+ res.addHeader( "ETag", etag);
+ }
+ }
+ else
+ {
+ if (etag.equals(header.getProperty("if-none-match")))
+ res = new Response( HTTP_NOTMODIFIED, mime, "");
+ else
+ {
+ res = new Response( HTTP_OK, mime, new FileInputStream( f ));
+ res.addHeader( "Content-Length", "" + fileLen);
+ res.addHeader( "ETag", etag);
+ }
+ }
+ }
+ }
+ catch( IOException ioe )
+ {
+ res = new Response( HTTP_FORBIDDEN, MIME_PLAINTEXT, "FORBIDDEN: Reading file failed." );
+ }
+
+ res.addHeader( "Accept-Ranges", "bytes"); // Announce that the file server accepts partial content requestes
+ return res;
+ }
+
+ /**
+ * Hashtable mapping (String)FILENAME_EXTENSION -> (String)MIME_TYPE
+ */
+ private static Hashtable<String, String> theMimeTypes = new Hashtable<String, String>();
+ static
+ {
+ StringTokenizer st = new StringTokenizer(
+ "css text/css "+
+ "htm text/html "+
+ "html text/html "+
+ "xml text/xml "+
+ "txt text/plain "+
+ "asc text/plain "+
+ "gif image/gif "+
+ "jpg image/jpeg "+
+ "jpeg image/jpeg "+
+ "png image/png "+
+ "mp3 audio/mpeg "+
+ "m3u audio/mpeg-url " +
+ "mp4 video/mp4 " +
+ "ogv video/ogg " +
+ "flv video/x-flv " +
+ "mov video/quicktime " +
+ "swf application/x-shockwave-flash " +
+ "js application/javascript "+
+ "pdf application/pdf "+
+ "doc application/msword "+
+ "ogg application/x-ogg "+
+ "zip application/octet-stream "+
+ "exe application/octet-stream "+
+ "class application/octet-stream " );
+ while ( st.hasMoreTokens())
+ theMimeTypes.put( st.nextToken(), st.nextToken());
+ }
+
+ private static int theBufferSize = 16 * 1024;
+
+ // Change these if you want to log to somewhere else than stdout
+ protected static PrintStream myOut = System.out;
+ protected static PrintStream myErr = System.err;
+
+ /**
+ * GMT date formatter
+ */
+ private static java.text.SimpleDateFormat gmtFrmt;
+ static
+ {
+ gmtFrmt = new java.text.SimpleDateFormat( "E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
+ gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT"));
+ }
+
+ /**
+ * The distribution licence
+ */
+ private static final String LICENCE =
+ "Copyright (C) 2001,2005-2011 by Jarno Elonen <elonen@iki.fi>\n"+
+ "and Copyright (C) 2010 by Konstantinos Togias <info@ktogias.gr>\n"+
+ "\n"+
+ "Redistribution and use in source and binary forms, with or without\n"+
+ "modification, are permitted provided that the following conditions\n"+
+ "are met:\n"+
+ "\n"+
+ "Redistributions of source code must retain the above copyright notice,\n"+
+ "this list of conditions and the following disclaimer. Redistributions in\n"+
+ "binary form must reproduce the above copyright notice, this list of\n"+
+ "conditions and the following disclaimer in the documentation and/or other\n"+
+ "materials provided with the distribution. The name of the author may not\n"+
+ "be used to endorse or promote products derived from this software without\n"+
+ "specific prior written permission. \n"+
+ " \n"+
+ "THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\n"+
+ "IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\n"+
+ "OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.\n"+
+ "IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,\n"+
+ "INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT\n"+
+ "NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n"+
+ "DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n"+
+ "THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n"+
+ "(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n"+
+ "OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.";
+}
+
diff --git a/src/batchtools/headless/src/test/java/com/galois/fiveui/ReporterTest.java b/src/batchtools/headless/src/test/java/com/galois/fiveui/ReporterTest.java
new file mode 100644
index 0000000..39a8e5c
--- /dev/null
+++ b/src/batchtools/headless/src/test/java/com/galois/fiveui/ReporterTest.java
@@ -0,0 +1,71 @@
+/**
+ * Module : Reporter.java Copyright : (c) 2012, Galois, Inc.
+ *
+ * Maintainer : Stability : Provisional Portability: Portable
+ *
+ * Licensed 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.
+ *
+ * @author Benjamin Jones <bjones@galois.com>
+ */
+package com.galois.fiveui;
+
+import static org.junit.Assert.*;
+
+import java.io.File;
+import java.io.IOException;
+import org.junit.Test;
+import org.openqa.selenium.WebDriver;
+
+import com.google.common.collect.ImmutableList;
+
+public class ReporterTest {
+
+ @Test
+ public void testConstructor() {
+ ImmutableList<Result> r = ImmutableList.of(
+ new Result(ResType.Pass, (WebDriver) null, "OK", "http://nonexistant", "test rule 1", "a desc or test rule 1", "problem!"),
+ new Result(ResType.Pass, (WebDriver) null, "OK", "http://intransigent", "test rule 1", "a desc or test rule 1", "problem!"),
+ new Result(ResType.Error, (WebDriver) null, "ERROR", "http://nonexistant", "test rule 2", "a desc or test rule 1", "problem!"));
+ Reporter kermit = new Reporter(r);
+ System.out.println("Summary page size: " + kermit.getSummary().length() + " bytes");
+ System.out.println("Summary page size: " + kermit.getByURL().length() + " bytes");
+ System.out.println("Summary page size: " + kermit.getByRule().length() + " bytes");
+ assertTrue(kermit.getSummary().length() > 0 &&
+ kermit.getByURL().length() > 0 &&
+ kermit.getByRule().length() > 0);
+ }
+
+ @Test
+ public void testSummaryPage() throws IOException {
+ //File tmpPath = Files.createTempDir();
+ File tmpPath = new File("/tmp/");
+ System.out.println("Writing test summary page to: " + tmpPath.toString() + File.separator);
+ ImmutableList<Result> r = ImmutableList.of(
+ new Result(ResType.Pass, (WebDriver) null, "OK", "http://nonexistant", "test rule 1", "a desc or test rule 1", "problem!"),
+ new Result(ResType.Pass, (WebDriver) null, "OK", "http://intransigent", "test rule 1", "a desc or test rule 1", "problem!"),
+ new Result(ResType.Pass, (WebDriver) null, "OK", "http://intransigent", "test rule 3", "a desc or test rule 1", "problem!"),
+ new Result(ResType.Pass, (WebDriver) null, "OK", "http://intransigent", "test rule 4", "a desc or test rule 1", "problem!"),
+ new Result(ResType.Pass, (WebDriver) null, "OK", "http://intransigent", "test rule 5", "a desc or test rule 1", "problem!"),
+ new Result(ResType.Pass, (WebDriver) null, "OK", "http://foo.com", "test rule 1", "a desc or test rule 1", "problem!"),
+ new Result(ResType.Error, (WebDriver) null, "ERROR", "http://foo.com", "test rule 5", "a desc or test rule 1", "problem!"),
+ new Result(ResType.Error, (WebDriver) null, "ERROR", "http://foo.com", "test rule 2", "a desc or test rule 1", "problem!"),
+ new Result(ResType.Error, (WebDriver) null, "ERROR", "http://bar.com", "test rule 3", "a desc or test rule 1", "problem!"),
+ new Result(ResType.Error, (WebDriver) null, "ERROR", "http://bar.com", "test rule 3", "a desc or test rule 1", "problem!"), // multiple fails for same url+rule combo
+ new Result(ResType.Error, (WebDriver) null, "ERROR", "http://bar.com", "test rule 3", "a desc or test rule 1", "problem!"),
+ new Result(ResType.Error, (WebDriver) null, "ERROR", "http://bar.com", "test rule 3", "a desc or test rule 1", "problem!"),
+ new Result(ResType.Error, (WebDriver) null, "ERROR", "http://nonexistant", "test rule 2", "a desc or test rule 1", "problem!"));
+ Reporter kermit = new Reporter(r);
+ kermit.writeReportsToDir(tmpPath.toString());
+ assertTrue("made it!", true);
+ }
+}
diff --git a/src/batchtools/headless/src/test/resources/crawlTest/four.html b/src/batchtools/headless/src/test/resources/crawlTest/four.html
new file mode 100644
index 0000000..a25405b
--- /dev/null
+++ b/src/batchtools/headless/src/test/resources/crawlTest/four.html
@@ -0,0 +1,7 @@
+<html>
+ <head><title>four</title></head>
+ <body>
+ END
+ </body>
+</html>
+
diff --git a/src/batchtools/headless/src/test/resources/crawlTest/one.html b/src/batchtools/headless/src/test/resources/crawlTest/one.html
new file mode 100644
index 0000000..d5fae55
--- /dev/null
+++ b/src/batchtools/headless/src/test/resources/crawlTest/one.html
@@ -0,0 +1,12 @@
+<html>
+ <head><title>one</title></head>
+ <body>
+ <a href="two.html">two</a>
+ <a href="one_a.html">one_a</a>
+ <a href="one_b.html">one_b</a>
+ <a href="one_c.html">one_c</a>
+ <a href="one_d.html">one_d</a>
+ <a href="one_e.html">one_e</a>
+ </body>
+</html>
+
diff --git a/src/batchtools/headless/src/test/resources/crawlTest/one_a.html b/src/batchtools/headless/src/test/resources/crawlTest/one_a.html
new file mode 100644
index 0000000..3dd01ae
--- /dev/null
+++ b/src/batchtools/headless/src/test/resources/crawlTest/one_a.html
@@ -0,0 +1,6 @@
+<html>
+ <head><title>one_a</title></head>
+ <body>
+ </body>
+</html>
+
diff --git a/src/batchtools/headless/src/test/resources/crawlTest/one_b.html b/src/batchtools/headless/src/test/resources/crawlTest/one_b.html
new file mode 100644
index 0000000..dbbf635
--- /dev/null
+++ b/src/batchtools/headless/src/test/resources/crawlTest/one_b.html
@@ -0,0 +1,6 @@
+<html>
+ <head><title>one_b</title></head>
+ <body>
+ </body>
+</html>
+
diff --git a/src/batchtools/headless/src/test/resources/crawlTest/one_c.html b/src/batchtools/headless/src/test/resources/crawlTest/one_c.html
new file mode 100644
index 0000000..f1ef9bc
--- /dev/null
+++ b/src/batchtools/headless/src/test/resources/crawlTest/one_c.html
@@ -0,0 +1,6 @@
+<html>
+ <head><title>one_c</title></head>
+ <body>
+ </body>
+</html>
+
diff --git a/src/batchtools/headless/src/test/resources/crawlTest/one_d.html b/src/batchtools/headless/src/test/resources/crawlTest/one_d.html
new file mode 100644
index 0000000..2bfb0fd
--- /dev/null
+++ b/src/batchtools/headless/src/test/resources/crawlTest/one_d.html
@@ -0,0 +1,6 @@
+<html>
+ <head><title>one_d</title></head>
+ <body>
+ </body>
+</html>
+
diff --git a/src/batchtools/headless/src/test/resources/crawlTest/one_e.html b/src/batchtools/headless/src/test/resources/crawlTest/one_e.html
new file mode 100644
index 0000000..3016b17
--- /dev/null
+++ b/src/batchtools/headless/src/test/resources/crawlTest/one_e.html
@@ -0,0 +1,6 @@
+<html>
+ <head><title>one_e</title></head>
+ <body>
+ </body>
+</html>
+
diff --git a/src/batchtools/headless/src/test/resources/crawlTest/three.html b/src/batchtools/headless/src/test/resources/crawlTest/three.html
new file mode 100644
index 0000000..a6f1c32
--- /dev/null
+++ b/src/batchtools/headless/src/test/resources/crawlTest/three.html
@@ -0,0 +1,7 @@
+<html>
+ <head><title>three</title></head>
+ <body>
+ <a href="four.html">four</a>
+ </body>
+</html>
+
diff --git a/src/batchtools/headless/src/test/resources/crawlTest/two.html b/src/batchtools/headless/src/test/resources/crawlTest/two.html
new file mode 100644
index 0000000..9fe3c68
--- /dev/null
+++ b/src/batchtools/headless/src/test/resources/crawlTest/two.html
@@ -0,0 +1,7 @@
+<html>
+ <head><title>two</title></head>
+ <body>
+ <a href="three.html">three</a>
+ </body>
+</html>
+
diff --git a/src/batchtools/headless/src/test/resources/ruleSets/emptyRuleSet.json b/src/batchtools/headless/src/test/resources/ruleSets/emptyRuleSet.json
new file mode 100644
index 0000000..a01bc68
--- /dev/null
+++ b/src/batchtools/headless/src/test/resources/ruleSets/emptyRuleSet.json
@@ -0,0 +1,4 @@
+{ "name": "emptyRuleSet"
+, "description": ""
+, "rules": []
+}
diff --git a/src/batchtools/headless/src/test/resources/runDescriptions/headlessRunTest0.json b/src/batchtools/headless/src/test/resources/runDescriptions/headlessRunTest0.json
new file mode 100644
index 0000000..e6c4c3b
--- /dev/null
+++ b/src/batchtools/headless/src/test/resources/runDescriptions/headlessRunTest0.json
@@ -0,0 +1,4 @@
+[{
+ 'url': 'http://localhost:8000',
+ 'ruleSet': '../ruleSets/emptyRuleSet.json'
+}]
diff --git a/src/batchtools/headless/src/test/resources/runDescriptions/headlessRunTestCNN.json b/src/batchtools/headless/src/test/resources/runDescriptions/headlessRunTestCNN.json
new file mode 100644
index 0000000..07aaab8
--- /dev/null
+++ b/src/batchtools/headless/src/test/resources/runDescriptions/headlessRunTestCNN.json
@@ -0,0 +1,4 @@
+[{
+ 'url': 'http://www.cnn.com',
+ 'ruleSet': '../ruleSets/headingGuidelines.json'
+}]
diff --git a/src/batchtools/headless/src/test/resources/runDescriptions/headlessRunTestGalois.json b/src/batchtools/headless/src/test/resources/runDescriptions/headlessRunTestGalois.json
new file mode 100644
index 0000000..5e1e5e6
--- /dev/null
+++ b/src/batchtools/headless/src/test/resources/runDescriptions/headlessRunTestGalois.json
@@ -0,0 +1,10 @@
+{
+ 'rulePath' : '/Users/bjones/galois/FiveUI/exampleData/ruleSets',
+ 'crawlType' : 'none',
+ 'runs': [
+ { 'url': 'http://corp.galois.com', 'ruleSet': 'colorRulesRF.json' },
+ { 'url': 'http://corp.galois.com', 'ruleSet': 'fontRules.json' },
+ { 'url': 'http://corp.galois.com', 'ruleSet': 'headingGuidelines.json' },
+ { 'url': 'http://corp.galois.com', 'ruleSet': 'imageRules.json' }
+ ]
+}
diff --git a/src/batchtools/headless/src/test/resources/runDescriptions/headlessSample0.json b/src/batchtools/headless/src/test/resources/runDescriptions/headlessSample0.json
new file mode 100644
index 0000000..2b3a51a
--- /dev/null
+++ b/src/batchtools/headless/src/test/resources/runDescriptions/headlessSample0.json
@@ -0,0 +1,4 @@
+[{
+ 'url': 'http://testhost',
+ 'ruleSet': '../ruleSets/emptyRuleSet.json'
+}]
diff --git a/src/batchtools/headless/src/test/resources/runDescriptions/headlessSample1.json b/src/batchtools/headless/src/test/resources/runDescriptions/headlessSample1.json
new file mode 100644
index 0000000..8e4d6c6
--- /dev/null
+++ b/src/batchtools/headless/src/test/resources/runDescriptions/headlessSample1.json
@@ -0,0 +1,4 @@
+[{
+ 'url': 'http://testhost',
+ 'ruleSet': '../../../../../rsTester/src/test/resources/ruleSets/headingGuidelines.json'
+}]
diff --git a/src/batchtools/headless/src/test/resources/runDescriptions/headlessSample2.json b/src/batchtools/headless/src/test/resources/runDescriptions/headlessSample2.json
new file mode 100644
index 0000000..b5ea2ee
--- /dev/null
+++ b/src/batchtools/headless/src/test/resources/runDescriptions/headlessSample2.json
@@ -0,0 +1,9 @@
+[
+{
+ 'url': 'http://testhost1',
+ 'ruleSet': '../ruleSets/emptyRuleSet.json'
+},
+{
+ 'url': 'http://testhost2',
+ 'ruleSet': '../../../../../rsTester/src/test/resources/ruleSets/headingGuidelines.json'
+}]
diff --git a/src/batchtools/headless/test.html b/src/batchtools/headless/test.html
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/batchtools/headless/test.html
diff --git a/src/batchtools/pom.xml b/src/batchtools/pom.xml
new file mode 100644
index 0000000..548cc0f
--- /dev/null
+++ b/src/batchtools/pom.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Module : pom.xml -->
+<!-- Copyright : (c) 2011-2012, Galois, Inc. -->
+<!-- -->
+<!-- Maintainer : -->
+<!-- Stability : Provisional -->
+<!-- Portability: Portable -->
+<!-- -->
+<!-- Licensed 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. -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>com.galois.fiveui</groupId>
+ <artifactId>BatchTools</artifactId>
+ <version>1.0</version>
+ <packaging>pom</packaging>
+
+ <modules>
+ <module>webdrivers</module>
+ <module>rsTester</module>
+ <module>headless</module>
+ </modules>
+
+</project>
diff --git a/src/batchtools/rsTester/.buildpath b/src/batchtools/rsTester/.buildpath
new file mode 100644
index 0000000..24e3b93
--- /dev/null
+++ b/src/batchtools/rsTester/.buildpath
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<buildpath>
+ <buildpathentry kind="src" path="src/test/java"/>
+ <buildpathentry kind="src" path="src/main/java"/>
+ <buildpathentry kind="src" path="src/main/resources"/>
+ <buildpathentry kind="con" path="org.eclipse.dltk.mod.launching.INTERPRETER_CONTAINER"/>
+ <buildpathentry kind="con" path="org.ebayopensource.vjet.eclipse.core.JSNATIVE_CONTAINER/JS Native Types"/>
+ <buildpathentry kind="con" path="org.ebayopensource.vjet.eclipse.core.BROWSER_CONTAINER/Browser SDK"/>
+ <buildpathentry external="true" kind="lib" path="/home/creswick/.m2/repository/org/seleniumhq/selenium/selenium-java/2.16.0/selenium-java-2.16.0.jar"/>
+</buildpath>
diff --git a/src/batchtools/rsTester/.classpath b/src/batchtools/rsTester/.classpath
new file mode 100644
index 0000000..9fc2de7
--- /dev/null
+++ b/src/batchtools/rsTester/.classpath
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+ <classpathentry kind="src" output="target/classes" path="src/main/java">
+ <attributes>
+ <attribute name="optional" value="true"/>
+ <attribute name="maven.pomderived" value="true"/>
+ </attributes>
+ </classpathentry>
+ <classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
+ <attributes>
+ <attribute name="maven.pomderived" value="true"/>
+ </attributes>
+ </classpathentry>
+ <classpathentry kind="src" output="target/test-classes" path="src/test/java">
+ <attributes>
+ <attribute name="optional" value="true"/>
+ <attribute name="maven.pomderived" value="true"/>
+ </attributes>
+ </classpathentry>
+ <classpathentry excluding="**" kind="src" output="target/test-classes" path="src/test/resources">
+ <attributes>
+ <attribute name="maven.pomderived" value="true"/>
+ </attributes>
+ </classpathentry>
+ <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/J2SE-1.5">
+ <attributes>
+ <attribute name="maven.pomderived" value="true"/>
+ </attributes>
+ </classpathentry>
+ <classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
+ <attributes>
+ <attribute name="maven.pomderived" value="true"/>
+ </attributes>
+ </classpathentry>
+ <classpathentry kind="output" path="target/classes"/>
+</classpath>
diff --git a/src/batchtools/rsTester/.gitignore b/src/batchtools/rsTester/.gitignore
new file mode 100644
index 0000000..7e7291c
--- /dev/null
+++ b/src/batchtools/rsTester/.gitignore
@@ -0,0 +1,2 @@
+target
+doc
diff --git a/src/batchtools/rsTester/.settings/org.ebayopensource.vjet.eclipse.core.prefs b/src/batchtools/rsTester/.settings/org.ebayopensource.vjet.eclipse.core.prefs
new file mode 100644
index 0000000..6e8a644
--- /dev/null
+++ b/src/batchtools/rsTester/.settings/org.ebayopensource.vjet.eclipse.core.prefs
@@ -0,0 +1,2 @@
+eclipse.preferences.version=1
+initialized_project_from_v4classpath=true
diff --git a/src/batchtools/rsTester/.settings/org.eclipse.jdt.core.prefs b/src/batchtools/rsTester/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 0000000..abec6ca
--- /dev/null
+++ b/src/batchtools/rsTester/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,5 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.5
+org.eclipse.jdt.core.compiler.compliance=1.5
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
+org.eclipse.jdt.core.compiler.source=1.5
diff --git a/src/batchtools/rsTester/.settings/org.eclipse.m2e.core.prefs b/src/batchtools/rsTester/.settings/org.eclipse.m2e.core.prefs
new file mode 100644
index 0000000..f897a7f
--- /dev/null
+++ b/src/batchtools/rsTester/.settings/org.eclipse.m2e.core.prefs
@@ -0,0 +1,4 @@
+activeProfiles=
+eclipse.preferences.version=1
+resolveWorkspaceProjects=true
+version=1
diff --git a/src/batchtools/rsTester/.settings/org.maven.ide.eclipse.prefs b/src/batchtools/rsTester/.settings/org.maven.ide.eclipse.prefs
new file mode 100644
index 0000000..0512605
--- /dev/null
+++ b/src/batchtools/rsTester/.settings/org.maven.ide.eclipse.prefs
@@ -0,0 +1,8 @@
+#Tue Jan 10 18:30:41 PST 2012
+activeProfiles=
+eclipse.preferences.version=1
+fullBuildGoals=process-test-resources
+resolveWorkspaceProjects=true
+resourceFilterGoals=process-resources resources\:testResources
+skipCompilerPlugin=true
+version=1
diff --git a/src/batchtools/rsTester/pom.xml b/src/batchtools/rsTester/pom.xml
new file mode 100644
index 0000000..b3d637f
--- /dev/null
+++ b/src/batchtools/rsTester/pom.xml
@@ -0,0 +1,109 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Module : pom.xml -->
+<!-- Copyright : (c) 2011-2012, Galois, Inc. -->
+<!-- -->
+<!-- Maintainer : -->
+<!-- Stability : Provisional -->
+<!-- Portability: Portable -->
+<!-- -->
+<!-- Licensed 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. -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>com.galois.fiveui</groupId>
+ <artifactId>RuleSetTester</artifactId>
+ <version>1.0</version>
+
+ <pluginRepositories>
+ <pluginRepository>
+ <id>onejar-maven-plugin.googlecode.com</id>
+ <url>http://onejar-maven-plugin.googlecode.com/svn/mavenrepo</url>
+ </pluginRepository>
+ </pluginRepositories>
+
+ <build>
+ <plugins>
+ <plugin>
+ <!-- package with 'mvn assembly:single' -->
+ <artifactId>maven-assembly-plugin</artifactId>
+ <configuration>
+ <archive>
+ <manifest>
+ <mainClass>com.galois.fiveui.RuleSetTester</mainClass>
+ </manifest>
+ </archive>
+ <descriptorRefs>
+ <descriptorRef>jar-with-dependencies</descriptorRef>
+ </descriptorRefs>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.dstovall</groupId>
+ <artifactId>onejar-maven-plugin</artifactId>
+ <version>1.3.0</version>
+ <executions>
+ <execution>
+ <configuration>
+ <mainClass>com.galois.fiveui.RuleSetTester</mainClass>
+ </configuration>
+ <goals>
+ <goal>one-jar</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+
+ <reporting>
+ <plugins>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>findbugs-maven-plugin</artifactId>
+ <version>2.4.0-SNAPSHOT</version>
+ </plugin>
+ </plugins>
+ </reporting>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.seleniumhq.selenium</groupId>
+ <artifactId>selenium-java</artifactId>
+ <version>2.25.0</version>
+ </dependency>
+ <dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava</artifactId>
+ <version>10.0.1</version>
+ </dependency>
+ <dependency>
+ <groupId>com.google.code.gson</groupId>
+ <artifactId>gson</artifactId>
+ <version>2.1</version>
+ </dependency>
+ <dependency>
+ <groupId>com.galois.fiveui</groupId>
+ <artifactId>webdrivers</artifactId>
+ <version>0.0.1-SNAPSHOT</version>
+ </dependency>
+
+ <!-- Test dependencies -->
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <version>4.11</version>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+</project>
diff --git a/src/batchtools/rsTester/src/main/java/com/galois/fiveui/BatchRunner.java b/src/batchtools/rsTester/src/main/java/com/galois/fiveui/BatchRunner.java
new file mode 100644
index 0000000..e065128
--- /dev/null
+++ b/src/batchtools/rsTester/src/main/java/com/galois/fiveui/BatchRunner.java
@@ -0,0 +1,235 @@
+/**
+ * Module : BatchRunner.java Copyright : (c) 2011-2012, Galois, Inc.
+ *
+ * Maintainer : Stability : Provisional Portability: Portable
+ *
+ * Licensed 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 com.galois.fiveui;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+import org.openqa.selenium.JavascriptExecutor;
+import org.openqa.selenium.WebDriver;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableList.Builder;
+import com.google.common.collect.Lists;
+
+/**
+ * BatchRunner is initialized with a WebDriver object. It provides an interface
+ * for running {@code RuleSet}s and {@code RuleTest}s with the WebDriver.
+ *
+ * @see #runTest
+ * @see #runRule
+ * @author creswick
+ */
+public class BatchRunner {
+
+ private final WebDriver _driver;
+ private final JavascriptExecutor _exe;
+
+ // Relative to the batch directory.
+ private static final String DATA_DIR = "../contexts/data/";
+ private static final String J_QUERY_JS = DATA_DIR
+ + "lib/jquery/jquery-1.7.1.min.js";
+ private static final String PRELUDE_JS = DATA_DIR
+ + "fiveui/injected/prelude.js";
+ private static final String MD5_JS = DATA_DIR
+ + "lib/jshash/md5.js";
+ private static final String JQUERY_PLUGIN_JS = DATA_DIR
+ + "fiveui/injected/jquery-plugins.js";
+ private static final String SEL_INJECTED_COMPUTE_JS = DATA_DIR +
+ "/fiveui/selenium/selenium-injected-compute.js";
+
+ private static final String INJECTED_COMPUTE_JS = DATA_DIR +
+ "/fiveui/injected/fiveui-injected-compute.js";
+
+ /**
+ * BatchRunner constructor, stores the given WebDriver.
+ *
+ * @param driver the WebDriver object to run tests with
+ */
+ public BatchRunner(WebDriver driver) {
+ _driver = driver;
+ _exe = (JavascriptExecutor) _driver;
+ }
+
+ /**
+ * Run a collection of RuleTests and return the concatenated results.
+ *
+ * @param tests a list of RuleTest objects
+ * @return a list of Result objects
+ * @see #runTest
+ */
+ public ImmutableList<Result> runTests(ImmutableList<RuleTest> tests) {
+ Builder<Result> resBuilder = ImmutableList.builder();
+ for (RuleTest test : tests) {
+ resBuilder.addAll(runTest(test));
+ }
+ return resBuilder.build();
+ }
+
+ /**
+ * Run a RuleTest, returning the result (success, failure details, or
+ * indicator of exceptional conditions.)
+ * <p>
+ * The test URI is loaded using the WebDriver and the rule set contained in
+ * {@code test} is run.
+ * <p>
+ * For each result, look in the oracle collection for a corresponding ResType
+ * (exception, pass, error, ...). If one is found, remove it from the oracle
+ * and report that we got an expected result. If one is not found, report that
+ * we got an unexpected result.
+ * <p>
+ * Finally, for each ResType left over in the oracle, report that we missed
+ * an expected result.
+ * <p>
+ * Note: This method catches all exceptions generated during the rule run and
+ * reports them as "exception" ResType results in its return value.
+ *
+ * @param test a RuleTest to run
+ */
+ public ImmutableList<Result> runTest(final RuleTest test) {
+ Rule rule = test.getRule();
+
+ ImmutableList<Result> rawResults;
+ Builder<Result> builder = ImmutableList.builder();
+ try {
+ loadURL(test.getUri().toString()); // set state of the WebDriver
+ rawResults = runRule(rule); // run the ruleset, collect results
+
+ List<ResType> oracle = Lists.newArrayList(test.getOracle());
+ for (Result result : rawResults) {
+ Result res;
+ if ( oracle.remove(result.getType()) ) {
+ res = Result.pass(_driver,
+ test.getRuleName() + ": Got expected result: "+result.getType());
+ } else {
+ res = Result.error(_driver,
+ test.getRuleName() + ": Unexpected result: "+result);
+ }
+ builder.add(res);
+ }
+ for (ResType o : oracle) {
+ Result res = Result.error(_driver,
+ test.getRuleName() + ": missing expected result: "+o);
+ builder.add(res);
+ }
+ } catch (Exception e) {
+ String errStr = "Could not run rule: " + rule.getName() + "\n";
+ errStr += e.toString();
+ rawResults = ImmutableList.of(
+ Result.exception(_driver, "Could not run rule: "+errStr));
+
+ e.printStackTrace();
+ }
+
+ return builder.build();
+ }
+
+ /**
+ * Run a rule set on a page.
+ * <p>
+ * This method uses the web driver instance to run a rule set on the currently
+ * loaded page. The webdriver injects javascript that
+ * includes all the dependencies (JQuery, etc..) as well as the function which
+ * executes the rule check. The method sleeps the thread for 1 second and
+ * queries the results, which are then parsed and returned as a list of
+ * Result objects.
+ *
+ * @param rule the rule to be run
+ * @return results of running the rule set
+ * @throws IOException
+ */
+ private ImmutableList<Result> runRule(final Rule rule) throws IOException {
+ String contentScript = wrapRule(rule);
+ Builder<Result> builder = ImmutableList.builder();
+
+ _exe.executeScript(contentScript);
+
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e1) {
+ e1.printStackTrace();
+ }
+
+ Object res = _exe.executeScript("return fiveui.selPort.query(type='ReportProblem')");
+
+ if (res.getClass() == String.class) {
+ // we received an error via the expected mechanisms:
+ System.err.println("Exception running rule: " + res);
+ builder.add(Result.exception(_driver, (String) res));
+ return builder.build();
+ } else {
+
+ try {
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ List<Map<String, Map<String, String>>> results = (List) res;
+
+ if (0 == results.size()) {
+ builder.add(Result.pass(_driver, "passed"));
+ }
+
+ for (Map<String, Map<String, String>> r : results) {
+ Map<String, String> problem = r.get("payload");
+
+ builder.add(Result.error(_driver, problem.get("descr")));
+ }
+
+ } catch (ClassCastException e) {
+ // An unexpected error happened:
+ builder.add(Result.exception(_driver, "Unexpected object returned: "
+ + res));
+ e.printStackTrace();
+ }
+ }
+ return builder.build();
+ }
+
+ /**
+ * Build up the complete content script needed to run the rule.
+ * <p>
+ * The string returned contains all the javascript dependencies required
+ * to run a rule set and the function that is injected into the page which
+ * executes the rule set.
+ *
+ * @param rule a Rule object
+ * @throws IOException
+ */
+ private String wrapRule(Rule rule) throws IOException {
+ String injected = "";
+ injected += Utils.readFile(SEL_INJECTED_COMPUTE_JS);
+ injected += Utils.readFile(J_QUERY_JS);
+ injected += Utils.readFile(PRELUDE_JS);
+ injected += Utils.readFile(MD5_JS);
+ injected += Utils.readFile(JQUERY_PLUGIN_JS);
+ injected += Utils.readFile(INJECTED_COMPUTE_JS);
+
+ injected += "return fiveui.selPort.send('SetRules', " + rule + ");";
+
+ return injected;
+ }
+
+ /**
+ * Sets the state of the WebDriver by loading a given URL.
+ *
+ * @param url URL to load
+ */
+ private void loadURL(String url) {
+ _driver.get(url);
+ }
+
+}
diff --git a/src/batchtools/rsTester/src/main/java/com/galois/fiveui/RSTestDescription.java b/src/batchtools/rsTester/src/main/java/com/galois/fiveui/RSTestDescription.java
new file mode 100644
index 0000000..8341c6f
--- /dev/null
+++ b/src/batchtools/rsTester/src/main/java/com/galois/fiveui/RSTestDescription.java
@@ -0,0 +1,321 @@
+package com.galois.fiveui;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.lang.reflect.Type;
+import java.net.URI;
+import java.util.List;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableList.Builder;
+import com.google.common.collect.Lists;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+
+/**
+ * RSTestDescriptions represent a set of tests for a specific RuleSet.
+ *
+ * RSTestDescriptions are also JSON-serializiable (and deserializable).
+ *
+ * An example of the JSON is given below:
+ *
+ * <pre>
+ * {@code
+ * { 'ruleSet': '../../exampleData/ruleSets/headingGuidelines.json',
+ * 'tests': [ { 'url': 'http://localhost:8000/exampleData/basic/headings.html',
+ * 'oracle': [ { 'ruleName': "Headings are capitalized"
+ * , 'results': ['Error', 'Error']
+ * },
+ * { 'ruleName': "Disallow Empty Headers"
+ * , 'results': ['Error']
+ * }
+ * ]
+ * }
+ * ]
+ * }
+ * }
+ * </pre>
+ *
+ * {@code tests} is as list that may contain a number of urls and sets of
+ * expected Problems for the specified rule set and url combination.
+ *
+ * {@code ruleSet} is a string file path that is relative to the rule set
+ * description json file.
+ *
+ * @author creswick
+ *
+ */
+public class RSTestDescription {
+
+ /**
+ * Parse a JSON file into a RSTestDescription
+ *
+ * @param runDescFileName The file to load.
+ * @return A populated RunDescription object.
+ * @throws FileNotFoundException if runDescFile can't be found.
+ */
+ public static RSTestDescription parse(String runDescFileName)
+ throws FileNotFoundException {
+
+ GsonBuilder gsonBuilder = new GsonBuilder();
+ gsonBuilder.registerTypeAdapter(RSTestDescription.class,
+ new RSTestDescription.Deserializer(runDescFileName));
+ Gson gson = gsonBuilder.create();
+
+ Reader in = new InputStreamReader(new FileInputStream(runDescFileName));
+
+ return gson.fromJson(in, RSTestDescription.class);
+ }
+
+ public static class Deserializer implements JsonDeserializer<RSTestDescription> {
+
+ private final String _descFile;
+
+ public Deserializer(String descFile) {
+ _descFile = descFile;
+ }
+
+ public RSTestDescription deserialize(JsonElement json, Type typeOfT,
+ JsonDeserializationContext context) throws JsonParseException {
+
+ JsonObject obj = json.getAsJsonObject();
+
+ String ruleSet = obj.get("ruleSet").getAsString();
+ JsonArray testArray = obj.get("tests").getAsJsonArray();
+
+ List<URIMap> tests = Lists.newArrayList();
+ for (JsonElement jsonElement : testArray) {
+ tests.add((URIMap)context.deserialize(jsonElement, URIMap.class));
+ }
+
+ String descDir = new File(_descFile).getParent();
+ if (null == descDir) {
+ descDir = ".";
+ }
+
+ // This probably is not portable, because the ruleSet path separator
+ // may not match that of the file system.
+ // TODO if File.separator is not "/", then replace "\" with File.separator.
+ String rsPath = descDir + File.separator + ruleSet;
+
+ RuleSet parsed;
+ try {
+ parsed = RuleSet.parseFile(rsPath);
+ } catch (IOException e) {
+ e.printStackTrace();
+ throw new JsonParseException(e.getMessage());
+ }
+ //RuleSet parsed = gson.fromJson(rsPath, RuleSet.class);
+
+ return new RSTestDescription(rsPath, tests, parsed);
+ }
+ }
+
+ /**
+ * The path to the selected rule set.
+ */
+ private final String _ruleSetLoc;
+
+ /**
+ * A list of urls and oracles that define tests for the specified RuleSet.
+ */
+ private final List<URIMap> _tests;
+
+ private final RuleSet _ruleSet;
+
+ public RSTestDescription(String ruleSetLoc, List<URIMap> tests, RuleSet ruleSet) {
+ _ruleSetLoc = ruleSetLoc;
+ _tests = tests;
+ _ruleSet = ruleSet;
+ }
+
+ public RuleSet getRuleSet() {
+ return _ruleSet;
+ }
+
+ public String getRuleSetLoc() {
+ return _ruleSetLoc;
+ }
+
+ /**
+ * Combines the URI in each test from the rule set test description with
+ * each rule that accompanies it.
+ * <p>
+ * The result is a list of RuleTest objects
+ * that each contain: a URI and rule set (to be run), a rule ID and list
+ * of results the we expect from the run.
+ *
+ * @return a list of RuleTest objects
+ * @throws IOException
+ */
+ public ImmutableList<RuleTest> getTests() throws IOException {
+ Builder<RuleTest> builder = ImmutableList.builder();
+ for (URIMap uriMap : _tests) {
+ for (RuleMap rMap : uriMap.getOracle()) {
+ builder.add(
+ new RuleTest(uriMap.getUrl(), getRuleSet(), rMap.getRuleName(), rMap.getResults()));
+ }
+ }
+ return builder.build();
+ }
+
+ public String toString() {
+ Gson gson = new Gson();
+ return gson.toJson(this);
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result
+ + ((_ruleSet == null) ? 0 : _ruleSet.hashCode());
+ result = prime * result
+ + ((_ruleSetLoc == null) ? 0 : _ruleSetLoc.hashCode());
+ result = prime * result + ((_tests == null) ? 0 : _tests.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ RSTestDescription other = (RSTestDescription) obj;
+ if (_ruleSet == null) {
+ if (other._ruleSet != null)
+ return false;
+ } else if (!_ruleSet.equals(other._ruleSet))
+ return false;
+ if (_ruleSetLoc == null) {
+ if (other._ruleSetLoc != null)
+ return false;
+ } else if (!_ruleSetLoc.equals(other._ruleSetLoc))
+ return false;
+ if (_tests == null) {
+ if (other._tests != null)
+ return false;
+ } else if (!_tests.equals(other._tests))
+ return false;
+ return true;
+ }
+
+ public static class URIMap {
+ private URI url;
+ private List<RuleMap> oracle;
+
+ URIMap(){}
+
+ public URIMap(URI url, List<RuleMap> oracle) {
+ super();
+ this.url = url;
+ this.oracle = oracle;
+ }
+
+ public URI getUrl() {
+ return url;
+ }
+
+ public List<RuleMap> getOracle() {
+ return oracle;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result
+ + ((oracle == null) ? 0 : oracle.hashCode());
+ result = prime * result + ((url == null) ? 0 : url.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ URIMap other = (URIMap) obj;
+ if (oracle == null) {
+ if (other.oracle != null)
+ return false;
+ } else if (!oracle.equals(other.oracle))
+ return false;
+ if (url == null) {
+ if (other.url != null)
+ return false;
+ } else if (!url.equals(other.url))
+ return false;
+ return true;
+ }
+ }
+
+ public static class RuleMap {
+ private String ruleName;
+ private List<ResType> results;
+
+ public RuleMap(String ruleName, List<ResType> results) {
+ super();
+ this.ruleName = ruleName;
+ this.results = results;
+ }
+
+ public String getRuleName() {
+ return ruleName;
+ }
+
+ public List<ResType> getResults() {
+ return results;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result
+ + ((results == null) ? 0 : results.hashCode());
+ result = prime * result
+ + ((ruleName == null) ? 0 : ruleName.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ RuleMap other = (RuleMap) obj;
+ if (results == null) {
+ if (other.results != null)
+ return false;
+ } else if (!results.equals(other.results))
+ return false;
+ if (ruleName == null) {
+ if (other.ruleName != null)
+ return false;
+ } else if (!ruleName.equals(other.ruleName))
+ return false;
+ return true;
+ }
+ }
+
+}
diff --git a/src/batchtools/rsTester/src/main/java/com/galois/fiveui/ResType.java b/src/batchtools/rsTester/src/main/java/com/galois/fiveui/ResType.java
new file mode 100644
index 0000000..ef14a60
--- /dev/null
+++ b/src/batchtools/rsTester/src/main/java/com/galois/fiveui/ResType.java
@@ -0,0 +1,32 @@
+/**
+ * Module : ResType.java Copyright : (c) 2011-2012, Galois, Inc.
+ *
+ * Maintainer : Stability : Provisional Portability: Portable
+ *
+ * Licensed 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 com.galois.fiveui;
+
+/**
+* Results are organized into categories as follows:
+* <ol>
+* <li> exception: an uncaught exception occurred while running the rule set</li>
+* <li> pass: the expected result was returned</li>
+* <li> error: an unexpected result was returned</li>
+* <li> warning: a warning was returned, currently this type is unused</li>
+* </ol>
+*/
+public enum ResType {
+ Pass, Error, Warning, Exception
+} \ No newline at end of file
diff --git a/src/batchtools/rsTester/src/main/java/com/galois/fiveui/Result.java b/src/batchtools/rsTester/src/main/java/com/galois/fiveui/Result.java
new file mode 100644
index 0000000..6543476
--- /dev/null
+++ b/src/batchtools/rsTester/src/main/java/com/galois/fiveui/Result.java
@@ -0,0 +1,174 @@
+/**
+ * Module : Result.java Copyright : (c) 2011-2012, Galois, Inc.
+ *
+ * Maintainer : Stability : Provisional Portability: Portable
+ *
+ * Licensed 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 com.galois.fiveui;
+
+import org.openqa.selenium.WebDriver;
+
+/**
+ * A Result object encapsulates the result of running a rule set check on a URL
+ * with a WebDriver. Results are organized into four categories:
+ * <ol>
+ * <li> exception: an uncaught exception occurred while running the rule set</li>
+ * <li> pass: the expected result was returned</li>
+ * <li> error: an unexpected result was returned</li>
+ * <li> warning: a warning was returned, currently this type is unused</li>
+ * </ol>
+ */
+public class Result {
+
+ private ResType _type;
+ private String _msg;
+ private WebDriver _driver;
+ private String _url;
+ private String _ruleName;
+ private String _ruleDesc;
+ private String _prob;
+
+ /**
+ * Construct a new result of one of the four types.
+ *
+ * @param type type of the result
+ * @param driver WebDriver that the result comes from
+ * @param msg any additional information from the rule runner
+ * @param url url
+ * @param ruleName rule name
+ * @param ruleDesc rule description
+ * @param prob problem description
+ */
+ public Result(ResType type, WebDriver driver, String msg, String url,
+ String ruleName, String ruleDesc, String prob) {
+ super();
+ _type = type;
+ _msg = msg;
+ _driver = driver;
+ _url = url;
+ _ruleName = ruleName;
+ _ruleDesc = ruleDesc;
+ _prob = prob;
+ }
+
+ /**
+ * An information restricted version of the other public constructor. This
+ * constructor does not include URL, rule, or problem information.
+ */
+ public Result(ResType type, WebDriver driver, String msg) {
+ super();
+ _type = type;
+ _msg = msg;
+ _driver = driver;
+ _url = "";
+ _ruleName = "";
+ _ruleDesc = "";
+ _prob = "";
+ }
+
+ /**********************************************************************************
+ * Factory methods: these provide easy construction of restricted Result
+ * types.
+ */
+
+ /**
+ * Result constructor, returns an "exception" type result.
+ *
+ * @param driver WebDriver the result came from
+ * @param res description of the result
+ * @return a Result object
+ */
+ public static Result exception(WebDriver driver, String res) {
+ return new Result(ResType.Exception, driver, res);
+ }
+
+ /**
+ * Result constructor, returns a "pass" type result.
+ *
+ * @param driver WebDriver the result came from
+ * @param res description of the result
+ * @return a Result object
+ */
+ public static Result pass(WebDriver driver, String res) {
+ return new Result(ResType.Pass, driver, res);
+ }
+
+ /**
+ * Result constructor, returns an "error" type result.
+ *
+ * @param driver WebDriver the result came from
+ * @param res description of the result
+ * @return a Result object
+ */
+ public static Result error(WebDriver driver, String res) {
+ return new Result(ResType.Error, driver, res);
+ }
+
+ /**
+ * Result constructor, returns a "warning" type result.
+ *
+ * @param driver WebDriver the result came from
+ * @param res description of the result
+ * @return a Result object
+ */
+ public static Result warning(WebDriver driver, String res) {
+ return new Result(ResType.Warning, driver, res);
+ }
+
+ /**********************************************************************************
+ * Extractor methods
+ */
+
+ public ResType getType() {
+ return _type;
+ }
+
+ public String getMsg() {
+ return _msg;
+ }
+
+ public WebDriver getDriver() {
+ return _driver;
+ }
+
+ public String getURL() {
+ return _url;
+ }
+
+ public String getRuleName() {
+ return _ruleName;
+ }
+
+ public String getRuleDesc() {
+ return _ruleDesc;
+ }
+
+ public String getProblem() {
+ return _prob;
+ }
+
+ /**
+ * Stringify the result, returning the type, driver name, and
+ * full description.
+ */
+ @Override
+ public String toString() {
+ return getType() + " - " + _driver.toString().split(":")[0] + ": "
+ + _msg + "\n"
+ + " |\\- " + _url + "\n"
+ + " |\\_ " + _ruleName + "\n"
+ + " |\\_ " + _ruleDesc + "\n"
+ + " \\_ " + _prob;
+ }
+}
diff --git a/src/batchtools/rsTester/src/main/java/com/galois/fiveui/Rule.java b/src/batchtools/rsTester/src/main/java/com/galois/fiveui/Rule.java
new file mode 100644
index 0000000..3f420cb
--- /dev/null
+++ b/src/batchtools/rsTester/src/main/java/com/galois/fiveui/Rule.java
@@ -0,0 +1,122 @@
+/**
+ * Module : Rule.java Copyright : (c) 2011-2012, Galois, Inc.
+ *
+ * Maintainer : Stability : Provisional Portability: Portable
+ *
+ * Licensed 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 com.galois.fiveui;
+
+import java.util.HashMap;
+
+import org.openqa.selenium.htmlunit.HtmlUnitDriver;
+
+import com.google.gson.Gson;
+
+public class Rule {
+
+ /**
+ * Parse a string representation of a Rule into a Java POJO.
+ *
+ * @param str string representing a rule set
+ * @return a RuleSet object
+ */
+ @SuppressWarnings("unchecked")
+ public static final Rule parse(String str) {
+ HtmlUnitDriver driver = new HtmlUnitDriver(true);
+ String name = "";
+ String desc = "";
+ String ruleStr = "";
+ HashMap<String, Object> res = null;
+ String stmt = "exports = {};\n"+str + "; return exports;";
+ try {
+ driver.get("http://localhost:8000/test.html");
+ res = (HashMap<String, Object>) driver.executeScript(stmt);
+ name = (String) res.get("name");
+ desc = (String) res.get("description");
+ ruleStr = res.get("rule").toString();
+ } finally {
+ driver.quit();
+ }
+
+ return new Rule(name, desc, ruleStr);
+ }
+
+ private final String _name;
+ private final String _desc;
+ private final String _rule;
+
+ public Rule(final String name, final String desc, final String rule) {
+ this._name = name;
+ this._desc = desc;
+ this._rule = rule;
+ }
+
+ public String getName() {
+ return _name;
+ }
+
+ public String getDescription() {
+ return _desc;
+ }
+
+ public String getRule() {
+ return _rule;
+ }
+
+ @Override
+ public String toString() {
+ Gson gson = new Gson();
+
+ return "exports.name = " + gson.toJson(getName()) + ";\n" +
+ "exports.description = " + gson.toJson(getDescription()) + ";\n" +
+ "exports.rule = " + gson.toJson(getRule()) + ";\n";
+ }
+
+ /**
+ * Equals and hashCode ignore the rule function text when performing comparisons.
+ */
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((_desc == null) ? 0 : _desc.hashCode());
+ result = prime * result + ((_name == null) ? 0 : _name.hashCode());
+ return result;
+ }
+
+ /**
+ * Equals and hashCode ignore the rule function text when performing comparisons.
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ Rule other = (Rule) obj;
+ if (_desc == null) {
+ if (other._desc != null)
+ return false;
+ } else if (!_desc.equals(other._desc))
+ return false;
+ if (_name == null) {
+ if (other._name != null)
+ return false;
+ } else if (!_name.equals(other._name))
+ return false;
+ return true;
+ }
+}
diff --git a/src/batchtools/rsTester/src/main/java/com/galois/fiveui/RuleSet.java b/src/batchtools/rsTester/src/main/java/com/galois/fiveui/RuleSet.java
new file mode 100644
index 0000000..ed9bc99
--- /dev/null
+++ b/src/batchtools/rsTester/src/main/java/com/galois/fiveui/RuleSet.java
@@ -0,0 +1,154 @@
+/**
+ * Module : RuleSet.java Copyright : (c) 2011-2012, Galois, Inc.
+ *
+ * Maintainer : Stability : Provisional Portability: Portable
+ *
+ * Licensed 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 com.galois.fiveui;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.gson.Gson;
+import com.google.gson.JsonSyntaxException;
+
+public class RuleSet {
+
+ public static RuleSet parseFile(String rsFileName) throws JsonSyntaxException, IOException {
+ String descDir = new File(rsFileName).getParent();
+
+ Gson gson = new Gson();
+ RuleSet rs = gson.fromJson(Utils.readFile(rsFileName), RuleSet.class);
+
+ rs.setDirectory(descDir);
+ return rs;
+ }
+
+ public void setDirectory(String descDir) {
+ this.descDir = descDir;
+ }
+
+ private final String name;
+ private final String description;
+ private final List<String> rules;
+
+ private transient ImmutableMap<String, Rule> _evaledRules = null;
+
+ private transient String descDir = ".";
+
+ public RuleSet(String name, String description, List<String> ruleFiles) {
+ this.name = name;
+ this.description = description;
+ this.rules = ruleFiles;
+ }
+
+ private void parseRules() {
+ ImmutableMap.Builder<String, Rule> builder = ImmutableMap.builder();
+
+ // Parse all the rules from files:
+ for (String r : rules) {
+ String adjustedPath = descDir + File.separator + r;
+ try {
+ Rule evRule = Rule.parse(
+ Utils.readFile(adjustedPath));
+
+ builder.put(evRule.getName(), evRule);
+ } catch (IOException e) {
+ System.err.println("Could not load rule from file: "+adjustedPath);
+ System.err.println(" error: "+e);
+ }
+ }
+
+ _evaledRules = builder.build();
+ }
+
+ /**
+ * Construct a new empty rule set.
+ *
+ * @return a new empty RuleSet object
+ */
+ public static RuleSet empty() {
+ List<String> rules = Lists.newArrayList();
+ return new RuleSet("", "", ImmutableList.copyOf(rules));
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public ImmutableCollection<Rule> getRules() {
+ if (null == _evaledRules) {
+ parseRules();
+ }
+
+ return _evaledRules.values();
+ }
+
+ public Rule getRule(String ruleName) {
+ return _evaledRules.get(ruleName);
+ }
+
+ @Override
+ public String toString() {
+ Gson gson = new Gson();
+ return gson.toJson(this);
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result
+ + ((description == null) ? 0 : description.hashCode());
+ result = prime * result + ((name == null) ? 0 : name.hashCode());
+ result = prime * result + ((rules == null) ? 0 : rules.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ RuleSet other = (RuleSet) obj;
+ if (description == null) {
+ if (other.description != null)
+ return false;
+ } else if (!description.equals(other.description))
+ return false;
+ if (name == null) {
+ if (other.name != null)
+ return false;
+ } else if (!name.equals(other.name))
+ return false;
+ if (rules == null) {
+ if (other.rules != null)
+ return false;
+ } else if (!rules.equals(other.rules))
+ return false;
+ return true;
+ }
+}
diff --git a/src/batchtools/rsTester/src/main/java/com/galois/fiveui/RuleSetTester.java b/src/batchtools/rsTester/src/main/java/com/galois/fiveui/RuleSetTester.java
new file mode 100644
index 0000000..3c85b22
--- /dev/null
+++ b/src/batchtools/rsTester/src/main/java/com/galois/fiveui/RuleSetTester.java
@@ -0,0 +1,91 @@
+/**
+ * Module : BatchExecutor.java Copyright : (c) 2011-2012, Galois, Inc.
+ *
+ * Maintainer : Stability : Provisional Portability: Portable
+ *
+ * Licensed 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 com.galois.fiveui;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+
+import org.openqa.selenium.WebDriver;
+
+import com.galois.fiveui.drivers.Drivers;
+import com.google.common.collect.ImmutableList;
+
+/**
+ * The main entry point for testing Rule Sets.
+ * <p>
+ * The {@link #main(String[])} method of this class sets up a WebDriver, loads
+ * a test run description from disk, and executes the test run which includes
+ * loading URL's and running rule sets on their content.
+ *
+ * @author creswick
+ *
+ */
+public class RuleSetTester {
+
+ /**
+ * @param args
+ * @throws IOException
+ * @throws URISyntaxException
+ */
+ public static void main(final String[] args) throws IOException,
+ URISyntaxException {
+
+ if (0 == args.length) {
+ printHelp();
+ System.exit(1);
+ }
+
+ for (int i = 0; i < args.length; i++) {
+ String runDescFileName = args[i];
+ RSTestDescription descr = RSTestDescription.parse(runDescFileName);
+
+ for (WebDriver driver : getDrivers()) {
+ try {
+ ImmutableList<Result> results = invokeTest(descr, driver);
+
+ for (Result result : results) {
+ System.out.println(result);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ } finally {
+ driver.quit();
+ }
+ }
+ }
+ }
+
+ private static ImmutableList<WebDriver> getDrivers() {
+ return ImmutableList.<WebDriver>of(
+ Drivers.buildFFDriver()
+ // , Drivers.buildChromeDriver()
+ );
+ }
+
+ private static void printHelp() {
+ System.out.println(
+ "Usage: RuleSetTester [<ruleSetTestDescirption.json>]");
+ }
+
+ private static ImmutableList<Result> invokeTest(RSTestDescription descr,
+ WebDriver driver) throws IOException {
+ BatchRunner runner = new BatchRunner(driver);
+
+ return runner.runTests(descr.getTests());
+ }
+}
diff --git a/src/batchtools/rsTester/src/main/java/com/galois/fiveui/RuleTest.java b/src/batchtools/rsTester/src/main/java/com/galois/fiveui/RuleTest.java
new file mode 100644
index 0000000..acf6743
--- /dev/null
+++ b/src/batchtools/rsTester/src/main/java/com/galois/fiveui/RuleTest.java
@@ -0,0 +1,76 @@
+/**
+ * Module : RuleTest.java
+ * Copyright : (c) 2011-2012, Galois, Inc.
+ *
+ * Maintainer :
+ * Stability : Provisional
+ * Portability: Portable
+ *
+ * Licensed 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 com.galois.fiveui;
+
+import java.net.URI;
+import java.util.Collection;
+
+import com.google.common.collect.ImmutableMultiset;
+
+/**
+ * A RuleTest object encapsulates the following data:
+ * <ol>
+ * <li> a URI on which to run rules, accessed using {@code getUri}</li>
+ * <li> a RuleSet object encapsulating the rule to run on the URI</li>
+ * <li> a rule ID, signifying we expect results for the corresponding rule
+ * (defined in the rule set) of the type(s) given in the oracle</li>
+ * <li> an oracle, the expected results of the test</li>
+ * </ol>
+ *
+ * @author creswick
+ *
+ */
+public class RuleTest {
+
+ private final URI _uri;
+ private final RuleSet _ruleSet;
+ private final String _ruleName;
+ private final ImmutableMultiset<ResType> _oracle;
+
+ public RuleTest(URI uri, RuleSet ruleSet, String ruleName, Collection<ResType> oracle) {
+ _uri = uri;
+ _ruleSet = ruleSet;
+ _ruleName = ruleName;
+ _oracle = ImmutableMultiset.copyOf(oracle);
+ }
+
+ public Rule getRule() {
+ return _ruleSet.getRule(_ruleName);
+ }
+
+ public URI getUri() {
+ return _uri;
+ }
+
+ public RuleSet getRuelSet() {
+ return _ruleSet;
+ }
+
+ public String getRuleName() {
+ return _ruleName;
+ }
+
+ public ImmutableMultiset<ResType> getOracle() {
+ return _oracle;
+ }
+
+
+}
diff --git a/src/batchtools/rsTester/src/main/java/com/galois/fiveui/Utils.java b/src/batchtools/rsTester/src/main/java/com/galois/fiveui/Utils.java
new file mode 100644
index 0000000..429997f
--- /dev/null
+++ b/src/batchtools/rsTester/src/main/java/com/galois/fiveui/Utils.java
@@ -0,0 +1,44 @@
+/**
+ * Module : Utils.java Copyright : (c) 2011-2012, Galois, Inc.
+ *
+ * Maintainer : Stability : Provisional Portability: Portable
+ *
+ * Licensed 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 com.galois.fiveui;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
+import com.google.common.io.CharStreams;
+
+/**
+ * Provides helper functions for reading files from disk.
+ *
+ * @author creswick
+ */
+public class Utils {
+
+ public static String readFile(String fileName) throws IOException {
+ InputStream in = new FileInputStream(new File(fileName));
+ return readStream(in);
+ }
+
+ public static String readStream(final InputStream stream)
+ throws IOException {
+ return CharStreams.toString(new InputStreamReader(stream));
+ }
+}
diff --git a/src/batchtools/rsTester/src/main/resources/javascript/ruleEval.js b/src/batchtools/rsTester/src/main/resources/javascript/ruleEval.js
new file mode 100644
index 0000000..04e81ba
--- /dev/null
+++ b/src/batchtools/rsTester/src/main/resources/javascript/ruleEval.js
@@ -0,0 +1,74 @@
+/**
+ * Module : ruleEval.js
+ * Copyright : (c) 2011-2012, Galois, Inc.
+ *
+ * Maintainer :
+ * Stability : Provisional
+ * Portability: Portable
+ *
+ * Licensed 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.
+ */
+/* Evaluate a rule.
+ *
+ * @param {!string} ruleStr The string representation of the rule (as
+ * a JavaScript Object literal).
+ * @return {?Array<Problem>} Empty if no problems were found or a string
+ * with an error if an exception occurred.
+ */
+var evaluate = function(ruleName, description, ruleStr) {
+ var theRule = null;
+ var results = [];
+
+// return [{ 'name': 'some rule',
+// 'descr': 'some description',
+// 'url': 'http:\/\/localhost:8000',
+// 'severity': '1'
+// }];
+//};
+
+ var report = function(name, node) {
+ var prob = {
+ 'name': name,
+ 'descr': description,
+ 'url': window.location.href,
+ 'severity': 1
+ };
+
+ results.push(prob);
+ };
+
+ try {
+ eval('var theRule = '+ruleStr);
+ return theRule.toString();
+ } catch (x) {
+// console.log('could not load rule: '+ruleStr);
+// console.log(x);
+ return "Error: "+x;
+ }
+
+ var scope = {
+ name : ruleName,
+ description : description
+ };
+
+ if (theRule) {
+ try {
+ theRule.apply(scope);
+ } catch (x) {
+// console.log('exception running rule: '+theRule.name);
+// console.log(x);
+ return "Error: "+x;
+ }
+ }
+ return results;
+};
diff --git a/src/batchtools/rsTester/src/main/resources/seleniumDrivers/linux64/chromedriver b/src/batchtools/rsTester/src/main/resources/seleniumDrivers/linux64/chromedriver
new file mode 100755
index 0000000..b0a0e4a
--- /dev/null
+++ b/src/batchtools/rsTester/src/main/resources/seleniumDrivers/linux64/chromedriver
Binary files differ
diff --git a/src/batchtools/rsTester/src/test/java/com/galois/fiveui/BasicRuleSetParseTest.java b/src/batchtools/rsTester/src/test/java/com/galois/fiveui/BasicRuleSetParseTest.java
new file mode 100644
index 0000000..5db53f8
--- /dev/null
+++ b/src/batchtools/rsTester/src/test/java/com/galois/fiveui/BasicRuleSetParseTest.java
@@ -0,0 +1,115 @@
+/**
+ *
+ * Copyright (c) 2009-2013,
+ *
+ * Galois, Inc. (creswick)
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * 3. The names of the contributors may not be used to endorse or promote
+ * products derived from this software without specific prior written
+ * permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ *
+ */
+package com.galois.fiveui;
+
+import java.util.Collection;
+import java.util.List;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+import com.google.common.collect.Lists;
+
+/**
+ * @author creswick
+ *
+ */
+@RunWith(Parameterized.class)
+public class BasicRuleSetParseTest {
+
+ /**
+ * Set up the tests via the parameterized runner:
+ *
+ * @return
+ * @throws Throwable
+ */
+ @Parameters(name = "{0} {1}")
+ public static Collection<Object[]> setup() throws Throwable {
+ List<Object[]> tests = Lists.newArrayList();
+
+ Object[][] rawTests = new Object[][] {
+ { "ruleSets/emptyRuleSet.json", true },
+ { "ruleSets/simpleRuleSet1.json", true },
+ { "ruleSets/headingGuidelines.json", true },
+ };
+
+ for (Object[] descr : rawTests) {
+ tests.add(descr);
+ }
+
+ return tests;
+ }
+
+ /**
+ * The path (relative to test resources) to the file to test.
+ */
+ private final String _filePath;
+
+ private final boolean _parses;
+
+ public BasicRuleSetParseTest(String path, boolean parses) {
+ this._filePath = path;
+ this._parses = parses;
+ }
+
+ /**
+ * Try and parse the file, no oracle other than the expected success/fail.
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testParse() throws Exception{
+ boolean actual = false;
+ String msg = "";
+ try {
+ RuleSet desc = RuleSet.parseFile("src/test/resources/"+_filePath);
+ // if desc is not null, we assume the parse worked:
+ actual = (desc != null);
+ } catch (Exception e) {
+ // if an exception was thrown, then we assume the parse failed:
+ msg = e.getMessage();
+ actual = false;
+ } finally {
+ // test to see if the parse result matched the expectation.
+ Assert.assertEquals("Parse did not complete as expected: "+msg,
+ _parses, actual);
+ }
+ }
+}
diff --git a/src/batchtools/rsTester/src/test/java/com/galois/fiveui/BasicRunDescriptionParseTest.java b/src/batchtools/rsTester/src/test/java/com/galois/fiveui/BasicRunDescriptionParseTest.java
new file mode 100644
index 0000000..57cf772
--- /dev/null
+++ b/src/batchtools/rsTester/src/test/java/com/galois/fiveui/BasicRunDescriptionParseTest.java
@@ -0,0 +1,126 @@
+/**
+ *
+ * Copyright (c) 2009-2013,
+ *
+ * Galois, Inc. (creswick)
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * 3. The names of the contributors may not be used to endorse or promote
+ * products derived from this software without specific prior written
+ * permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ *
+ */
+package com.galois.fiveui;
+
+import java.util.Collection;
+import java.util.List;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+import com.google.common.collect.Lists;
+
+/**
+ * @author creswick
+ *
+ */
+@RunWith(Parameterized.class)
+public class BasicRunDescriptionParseTest {
+
+ /**
+ * Set up the tests via the parameterized runner:
+ *
+ * @return
+ * @throws Throwable
+ */
+ @Parameters(name = "{0} {1}")
+ public static Collection<Object[]> setup() throws Throwable {
+ List<Object[]> tests = Lists.newArrayList();
+
+ Object[][] rawTests = new Object[][] {
+ { "runDescriptions/sample0.json", true },
+ { "runDescriptions/sample1.json", true },
+ { "runDescriptions/sample2.json", true },
+
+ // This should test that tests must have run ids,
+ // but that can't be enforced with gson's standard deserializers.
+ // { "runDescriptions/sample2-fails.json", false },
+
+ { "runDescriptions/sample3.json", true },
+ { "runDescriptions/sample4.json", true },
+ { "runDescriptions/sample5.json", true },
+ { "runDescriptions/headingSample.json", true },
+ { "runDescriptions/headingSample-1-fails.json", true },
+ { "runDescriptions/headingSample-2-fails.json", true },
+ };
+
+ for (Object[] descr : rawTests) {
+ tests.add(descr);
+ }
+
+ return tests;
+ }
+
+ /**
+ * The path (relative to test resources) to the file to test.
+ */
+ private final String _filePath;
+
+ private final boolean _parses;
+
+ public BasicRunDescriptionParseTest(String path, boolean parses) {
+ this._filePath = path;
+ this._parses = parses;
+ }
+
+ /**
+ * Try and parse the file, no oracle other than the expected success/fail.
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testParse() throws Exception{
+ boolean actual = false;
+ String msg = "";
+ try {
+ RSTestDescription desc = RSTestDescription.parse("src/test/resources/"+_filePath);
+ // if desc is not null, we assume the parse worked:
+ actual = (desc != null);
+ } catch (Exception e) {
+ // if an exception was thrown, then we assume the parse failed:
+ msg = e.getMessage();
+ actual = false;
+ } finally {
+ // test to see if the parse result matched the expectation.
+ Assert.assertEquals("Parse did not complete as expected: "+msg,
+ _parses, actual);
+ }
+ }
+}
diff --git a/src/batchtools/rsTester/src/test/java/com/galois/fiveui/RunDescriptionTest.java b/src/batchtools/rsTester/src/test/java/com/galois/fiveui/RunDescriptionTest.java
new file mode 100644
index 0000000..0d5d4c4
--- /dev/null
+++ b/src/batchtools/rsTester/src/test/java/com/galois/fiveui/RunDescriptionTest.java
@@ -0,0 +1,118 @@
+/**
+ * RunDescriptionTest.java
+ *
+ * Copyright (c) 2012 Galois, Inc.
+ */
+package com.galois.fiveui;
+
+import java.io.FileNotFoundException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+
+/**
+ * @author creswick
+ *
+ */
+public class RunDescriptionTest {
+
+ private static final String RUN_DESCRIPTION_DIR = "src/test/resources/runDescriptions/";
+
+ /**
+ * Test method for {@link com.galois.fiveui.RSTestDescription}.
+ * @throws FileNotFoundException
+ */
+ @Test
+ public final void testDeserialize_sample0() throws FileNotFoundException {
+
+ String jsonFileName = RUN_DESCRIPTION_DIR + "sample0.json";
+ String ruleSetLoc =
+ RUN_DESCRIPTION_DIR + "../ruleSets/emptyRuleSet.json";
+
+ ImmutableList<String> emptyRuleList = ImmutableList.of();
+ RuleSet rsOracle = new RuleSet("emptyRuleSet", "", emptyRuleList);
+
+ RSTestDescription oracle =
+ new RSTestDescription(ruleSetLoc,
+ new ArrayList<RSTestDescription.URIMap>(), rsOracle);
+
+
+ RSTestDescription actual = RSTestDescription.parse(jsonFileName);
+ assertObjEqual("Object deserialized incorrectly.", oracle, actual);
+ }
+
+ /**
+ * Test method for {@link com.galois.fiveui.RSTestDescription}.
+ * @throws FileNotFoundException
+ */
+ @Test
+ public final void testDeserialize_sample1() throws FileNotFoundException {
+
+ String jsonFileName = RUN_DESCRIPTION_DIR + "sample1.json";
+ String ruleSetLoc =
+ RUN_DESCRIPTION_DIR + "../ruleSets/simpleRuleSet1.json";
+
+ RuleSet rsOracle =
+ new RuleSet("simpleRuleSet1", "", ImmutableList.of("emptyCheck.js"));
+ rsOracle.setDirectory(RUN_DESCRIPTION_DIR + "../ruleSets/");
+
+ RSTestDescription oracle =
+ new RSTestDescription(ruleSetLoc,
+ new ArrayList<RSTestDescription.URIMap>(), rsOracle);
+
+
+ RSTestDescription actual = RSTestDescription.parse(jsonFileName);
+ assertObjEqual("Object deserialized incorrectly.", oracle, actual);
+ }
+
+ /**
+ * Test method for {@link com.galois.fiveui.RSTestDescription}.
+ * @throws FileNotFoundException
+ * @throws URISyntaxException
+ */
+ @Test
+ public final void testDeserialize_headingGuidelines() throws FileNotFoundException, URISyntaxException {
+
+ String jsonFileName = RUN_DESCRIPTION_DIR + "headingSample.json";
+ String ruleSetLoc =
+ RUN_DESCRIPTION_DIR + "../ruleSets/headingGuidelines.json";
+
+ RuleSet rsOracle = new RuleSet(
+ "Heading Guidelines",
+ "Guidelines pertaining to the formatting and content of headings.",
+ ImmutableList.of("headingGuidelines-caps.js",
+ "headingGuidelines-noEmptyHdrs.js"));
+ rsOracle.setDirectory(RUN_DESCRIPTION_DIR + "../ruleSets/");
+ rsOracle.getRules(); // force the rules to parse
+
+ RSTestDescription.URIMap uriMapOracle =
+ new RSTestDescription.URIMap(
+ new URI("http://localhost:8000/exampleData/basic/headings.html"),
+
+ Lists.newArrayList(
+ new RSTestDescription.RuleMap("Headings are capitalized",
+ Lists.newArrayList(ResType.Error, ResType.Error)),
+ new RSTestDescription.RuleMap("Disallow Empty Headers",
+ Lists.newArrayList(ResType.Error))));
+
+
+ RSTestDescription oracle =
+ new RSTestDescription(ruleSetLoc, Lists.newArrayList(uriMapOracle), rsOracle);
+
+
+ RSTestDescription actual = RSTestDescription.parse(jsonFileName);
+ assertObjEqual("Object deserialized incorrectly.", oracle, actual);
+ }
+ private void assertObjEqual(String msg, Object oracle, Object actual) {
+ Assert.assertTrue(msg + "; expected: "+oracle+" actual: "+actual,
+ oracle.equals(actual));
+ }
+
+
+}
diff --git a/src/batchtools/rsTester/src/test/resources/ruleSets/emptyCheck.js b/src/batchtools/rsTester/src/test/resources/ruleSets/emptyCheck.js
new file mode 100644
index 0000000..6588e5b
--- /dev/null
+++ b/src/batchtools/rsTester/src/test/resources/ruleSets/emptyCheck.js
@@ -0,0 +1,6 @@
+exports.name = 'Empty Rule';
+
+exports.description = 'A rule with no body, used for testing.';
+
+exports.rule = function() { };
+
diff --git a/src/batchtools/rsTester/src/test/resources/ruleSets/emptyRuleSet.json b/src/batchtools/rsTester/src/test/resources/ruleSets/emptyRuleSet.json
new file mode 100644
index 0000000..a01bc68
--- /dev/null
+++ b/src/batchtools/rsTester/src/test/resources/ruleSets/emptyRuleSet.json
@@ -0,0 +1,4 @@
+{ "name": "emptyRuleSet"
+, "description": ""
+, "rules": []
+}
diff --git a/src/batchtools/rsTester/src/test/resources/ruleSets/headingGuidelines-caps.js b/src/batchtools/rsTester/src/test/resources/ruleSets/headingGuidelines-caps.js
new file mode 100644
index 0000000..f6c1cb6
--- /dev/null
+++ b/src/batchtools/rsTester/src/test/resources/ruleSets/headingGuidelines-caps.js
@@ -0,0 +1,19 @@
+exports.name = "Headings are capitalized";
+
+exports.description = "Check to see if all headings use leading capital letters.";
+
+exports.rule = function() {
+ var badHeadings =
+ fiveui.query(':header').filter(
+ function(idx) {
+ var ch = $(this).text()[0];
+ if (ch) {
+ return (ch == ch.toLowerCase() );
+ } else {
+ return false;
+ }
+ });
+ $(badHeadings).map(function(idx, elt){
+ report('Heading does not start with a capitol letter.', elt);
+ });
+};
diff --git a/src/batchtools/rsTester/src/test/resources/ruleSets/headingGuidelines-noEmptyHdrs.js b/src/batchtools/rsTester/src/test/resources/ruleSets/headingGuidelines-noEmptyHdrs.js
new file mode 100644
index 0000000..a70f71c
--- /dev/null
+++ b/src/batchtools/rsTester/src/test/resources/ruleSets/headingGuidelines-noEmptyHdrs.js
@@ -0,0 +1,12 @@
+exports.name = "Disallow Empty Headers";
+
+exports.description = "Heading elements should contain text.";
+
+exports.rule = function() {
+ fiveui.query(':header').each(
+ function(ix, elt) {
+ if($(elt).text() == '') {
+ report('Heading does not contain text', elt);
+ }
+ });
+ };
diff --git a/src/batchtools/rsTester/src/test/resources/ruleSets/headingGuidelines.json b/src/batchtools/rsTester/src/test/resources/ruleSets/headingGuidelines.json
new file mode 100644
index 0000000..330f389
--- /dev/null
+++ b/src/batchtools/rsTester/src/test/resources/ruleSets/headingGuidelines.json
@@ -0,0 +1,6 @@
+{ "name": "Heading Guidelines"
+, "description": "Guidelines pertaining to the formatting and content of headings."
+, "rules": [ "headingGuidelines-caps.js",
+ "headingGuidelines-noEmptyHdrs.js"
+ ]
+} \ No newline at end of file
diff --git a/src/batchtools/rsTester/src/test/resources/ruleSets/simpleRuleSet1.json b/src/batchtools/rsTester/src/test/resources/ruleSets/simpleRuleSet1.json
new file mode 100644
index 0000000..71905cd
--- /dev/null
+++ b/src/batchtools/rsTester/src/test/resources/ruleSets/simpleRuleSet1.json
@@ -0,0 +1,4 @@
+{ "name": "simpleRuleSet1"
+, "description": ""
+, "rules": [ "emptyCheck.js" ]
+}
diff --git a/src/batchtools/rsTester/src/test/resources/runDescriptions/headingSample-1-fails.json b/src/batchtools/rsTester/src/test/resources/runDescriptions/headingSample-1-fails.json
new file mode 100644
index 0000000..23860b4
--- /dev/null
+++ b/src/batchtools/rsTester/src/test/resources/runDescriptions/headingSample-1-fails.json
@@ -0,0 +1,13 @@
+{
+ 'ruleSet': '../ruleSets/headingGuidelines.json',
+ 'tests': [ { 'url': 'http://localhost:8000/exampleData/basic/headings.html',
+ 'oracle': [ { 'ruleName': "Headings are capitalized"
+ , 'results': ['Error'] /* actually errors twice */
+ },
+ { 'ruleName': "Disallow Empty Headers"
+ , 'results': ['Error']
+ }
+ ]
+ }
+ ]
+}
diff --git a/src/batchtools/rsTester/src/test/resources/runDescriptions/headingSample-2-fails.json b/src/batchtools/rsTester/src/test/resources/runDescriptions/headingSample-2-fails.json
new file mode 100644
index 0000000..8a1cb38
--- /dev/null
+++ b/src/batchtools/rsTester/src/test/resources/runDescriptions/headingSample-2-fails.json
@@ -0,0 +1,13 @@
+{
+ 'ruleSet': '../ruleSets/headingGuidelines.json',
+ 'tests': [ { 'url': 'http://localhost:8000/exampleData/basic/headings.html',
+ 'oracle': [ { 'ruleName': "Headings are capitalized"
+ , 'results': ['Error', 'Error', 'Error'] /* actually only errors twice */
+ },
+ { 'ruleName': "Disallow Empty Headers"
+ , 'results': ['Error']
+ }
+ ]
+ }
+ ]
+}
diff --git a/src/batchtools/rsTester/src/test/resources/runDescriptions/headingSample.json b/src/batchtools/rsTester/src/test/resources/runDescriptions/headingSample.json
new file mode 100644
index 0000000..69deb49
--- /dev/null
+++ b/src/batchtools/rsTester/src/test/resources/runDescriptions/headingSample.json
@@ -0,0 +1,13 @@
+{
+ 'ruleSet': '../ruleSets/headingGuidelines.json',
+ 'tests': [ { 'url': 'http://localhost:8000/exampleData/basic/headings.html',
+ 'oracle': [ { 'ruleName': "Headings are capitalized"
+ , 'results': ['Error', 'Error']
+ },
+ { 'ruleName': "Disallow Empty Headers"
+ , 'results': ['Error']
+ }
+ ]
+ }
+ ]
+}
diff --git a/src/batchtools/rsTester/src/test/resources/runDescriptions/sample0.json b/src/batchtools/rsTester/src/test/resources/runDescriptions/sample0.json
new file mode 100644
index 0000000..0c2bbb7
--- /dev/null
+++ b/src/batchtools/rsTester/src/test/resources/runDescriptions/sample0.json
@@ -0,0 +1,4 @@
+{
+ 'ruleSet': '../ruleSets/emptyRuleSet.json',
+ 'tests': []
+}
diff --git a/src/batchtools/rsTester/src/test/resources/runDescriptions/sample1.json b/src/batchtools/rsTester/src/test/resources/runDescriptions/sample1.json
new file mode 100644
index 0000000..91f8f34
--- /dev/null
+++ b/src/batchtools/rsTester/src/test/resources/runDescriptions/sample1.json
@@ -0,0 +1,4 @@
+{
+ 'ruleSet': '../ruleSets/simpleRuleSet1.json',
+ 'tests': []
+}
diff --git a/src/batchtools/rsTester/src/test/resources/runDescriptions/sample2-fails.json b/src/batchtools/rsTester/src/test/resources/runDescriptions/sample2-fails.json
new file mode 100644
index 0000000..a8ea9ec
--- /dev/null
+++ b/src/batchtools/rsTester/src/test/resources/runDescriptions/sample2-fails.json
@@ -0,0 +1,10 @@
+{
+ 'ruleSet': '../ruleSets/simpleRuleSet1.json',
+ 'tests': [ { 'url': 'http://localhost:8000/',
+ 'oracle': [ { 'ruleId': 42
+ , 'results': ['Error', 'Error']
+ }
+ ]
+ }
+ ]
+}
diff --git a/src/batchtools/rsTester/src/test/resources/runDescriptions/sample2.json b/src/batchtools/rsTester/src/test/resources/runDescriptions/sample2.json
new file mode 100644
index 0000000..acff2d7
--- /dev/null
+++ b/src/batchtools/rsTester/src/test/resources/runDescriptions/sample2.json
@@ -0,0 +1,10 @@
+{
+ 'ruleSet': '../ruleSets/simpleRuleSet1.json',
+ 'tests': [ { 'url': 'http://localhost:8000/',
+ 'oracle': [ { 'ruleName': 'Empty Rule'
+ , 'results': []
+ }
+ ]
+ }
+ ]
+}
diff --git a/src/batchtools/rsTester/src/test/resources/runDescriptions/sample3.json b/src/batchtools/rsTester/src/test/resources/runDescriptions/sample3.json
new file mode 100644
index 0000000..e4d1cea
--- /dev/null
+++ b/src/batchtools/rsTester/src/test/resources/runDescriptions/sample3.json
@@ -0,0 +1,7 @@
+{
+ 'ruleSet': '../ruleSets/simpleRuleSet1.json',
+ 'tests': [ { 'url': 'http://localhost:8000/',
+ 'oracle': [ ]
+ }
+ ]
+}
diff --git a/src/batchtools/rsTester/src/test/resources/runDescriptions/sample4.json b/src/batchtools/rsTester/src/test/resources/runDescriptions/sample4.json
new file mode 100644
index 0000000..c4f46cd
--- /dev/null
+++ b/src/batchtools/rsTester/src/test/resources/runDescriptions/sample4.json
@@ -0,0 +1,10 @@
+{
+ 'ruleSet': '../ruleSets/simpleRuleSet1.json',
+ 'tests': [ { 'url': 'http://localhost:8000/',
+ 'oracle': [ ]
+ },
+ { 'url': 'http://localhost:8000/',
+ 'oracle': [ ]
+ }
+ ]
+}
diff --git a/src/batchtools/rsTester/src/test/resources/runDescriptions/sample5.json b/src/batchtools/rsTester/src/test/resources/runDescriptions/sample5.json
new file mode 100644
index 0000000..11ca957
--- /dev/null
+++ b/src/batchtools/rsTester/src/test/resources/runDescriptions/sample5.json
@@ -0,0 +1,13 @@
+{
+ 'ruleSet': '../ruleSets/simpleRuleSet1.json',
+ 'tests': [ { 'url': 'http://localhost:8000/',
+ 'oracle': [ { 'ruleId': 42
+ , 'results': ['Error']
+ }
+ ]
+ },
+ { 'url': 'http://localhost:8000/',
+ 'oracle': [ ]
+ }
+ ]
+}
diff --git a/src/batchtools/rsTester/test.html b/src/batchtools/rsTester/test.html
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/batchtools/rsTester/test.html
diff --git a/src/batchtools/webdrivers/.classpath b/src/batchtools/webdrivers/.classpath
new file mode 100644
index 0000000..75e85b7
--- /dev/null
+++ b/src/batchtools/webdrivers/.classpath
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+ <classpathentry kind="src" output="target/classes" path="src/main/java">
+ <attributes>
+ <attribute name="optional" value="true"/>
+ <attribute name="maven.pomderived" value="true"/>
+ </attributes>
+ </classpathentry>
+ <classpathentry kind="src" output="target/test-classes" path="src/test/java">
+ <attributes>
+ <attribute name="optional" value="true"/>
+ <attribute name="maven.pomderived" value="true"/>
+ </attributes>
+ </classpathentry>
+ <classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
+ <attributes>
+ <attribute name="maven.pomderived" value="true"/>
+ </attributes>
+ </classpathentry>
+ <classpathentry excluding="**" kind="src" output="target/test-classes" path="src/test/resources">
+ <attributes>
+ <attribute name="maven.pomderived" value="true"/>
+ </attributes>
+ </classpathentry>
+ <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/J2SE-1.5">
+ <attributes>
+ <attribute name="maven.pomderived" value="true"/>
+ </attributes>
+ </classpathentry>
+ <classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
+ <attributes>
+ <attribute name="maven.pomderived" value="true"/>
+ </attributes>
+ </classpathentry>
+ <classpathentry kind="output" path="target/classes"/>
+</classpath>
diff --git a/src/batchtools/webdrivers/.gitignore b/src/batchtools/webdrivers/.gitignore
new file mode 100644
index 0000000..eb5a316
--- /dev/null
+++ b/src/batchtools/webdrivers/.gitignore
@@ -0,0 +1 @@
+target
diff --git a/src/batchtools/webdrivers/.settings/org.eclipse.jdt.core.prefs b/src/batchtools/webdrivers/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 0000000..abec6ca
--- /dev/null
+++ b/src/batchtools/webdrivers/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,5 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.5
+org.eclipse.jdt.core.compiler.compliance=1.5
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
+org.eclipse.jdt.core.compiler.source=1.5
diff --git a/src/batchtools/webdrivers/.settings/org.eclipse.m2e.core.prefs b/src/batchtools/webdrivers/.settings/org.eclipse.m2e.core.prefs
new file mode 100644
index 0000000..f897a7f
--- /dev/null
+++ b/src/batchtools/webdrivers/.settings/org.eclipse.m2e.core.prefs
@@ -0,0 +1,4 @@
+activeProfiles=
+eclipse.preferences.version=1
+resolveWorkspaceProjects=true
+version=1
diff --git a/src/batchtools/webdrivers/pom.xml b/src/batchtools/webdrivers/pom.xml
new file mode 100644
index 0000000..4d8847f
--- /dev/null
+++ b/src/batchtools/webdrivers/pom.xml
@@ -0,0 +1,43 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>com.galois.fiveui</groupId>
+ <artifactId>webdrivers</artifactId>
+ <version>0.0.1-SNAPSHOT</version>
+ <name>Web Drivers</name>
+ <description>Wrapper around Selenium Web Driver creation to handle configuration details that we wish to be uniform across all FiveUI applications.</description>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <version>2.3.2</version>
+ <configuration>
+ <source>1.5</source>
+ <target>1.5</target>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+ <dependencies>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <version>4.9</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.seleniumhq.selenium</groupId>
+ <artifactId>selenium-java</artifactId>
+ <version>2.25.0</version>
+ </dependency>
+
+ <dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava</artifactId>
+ <version>10.0.1</version>
+ </dependency>
+ </dependencies>
+</project>
diff --git a/src/batchtools/webdrivers/src/main/java/com/galois/fiveui/drivers/Drivers.java b/src/batchtools/webdrivers/src/main/java/com/galois/fiveui/drivers/Drivers.java
new file mode 100644
index 0000000..db73fec
--- /dev/null
+++ b/src/batchtools/webdrivers/src/main/java/com/galois/fiveui/drivers/Drivers.java
@@ -0,0 +1,149 @@
+/**
+ * Module : Drivers.java Copyright : (c) 2011-2012, Galois, Inc.
+ *
+ * Maintainer : Stability : Provisional Portability: Portable
+ *
+ * Licensed 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 com.galois.fiveui.drivers;
+
+import java.io.File;
+import java.io.IOException;
+
+import org.openqa.selenium.chrome.ChromeDriver;
+import org.openqa.selenium.chrome.ChromeOptions;
+import org.openqa.selenium.firefox.FirefoxBinary;
+import org.openqa.selenium.firefox.FirefoxDriver;
+import org.openqa.selenium.firefox.FirefoxProfile;
+
+/**
+ * @author creswick
+ *
+ */
+public class Drivers {
+ private static final String FIREFOX_BIN_PATH = "FIREFOX_BIN_PATH";
+ private static final String CHROME_BIN_PATH = "CHROME_BIN_PATH";
+
+ private static final String CD_BINARY_NAME = "chromedriver";
+ private static final String CD_BASE_PATH = mkPath("..", "tools",
+ "seleniumChromeDrivers");
+
+ private static final String FIVEUI_ROOT_PATH = "FIVEUI_ROOT_PATH";
+ private static final String defaultFiveuiRootPath = "../../../";
+ private static final String firefoxProfilePath = "profiles/firefox";
+ private static final String chromeProfilePath = "profiles/chrome";
+ private static final String firefoxExtensionPath = "binaries/fiveui.xpi";
+ private static final String chromeExtensionPath = "binaries/fiveui.crx";
+
+ /**
+ * Query the OS environment for the FiveUI root path, or return a default.
+ */
+ public static String getRootPath() {
+ String rootPath = System.getProperty(FIVEUI_ROOT_PATH);
+ return (null != rootPath) ? rootPath + File.separator : defaultFiveuiRootPath;
+ }
+
+ public static FirefoxDriver buildFFDriver() {
+ // Extracted into a method so we can set up profiles
+
+ String rootPath = getRootPath();
+
+ File profileDir = new File(rootPath+firefoxProfilePath);
+ FirefoxProfile profile = new FirefoxProfile(profileDir);
+
+ File fiveuiXpi = new File(rootPath+firefoxExtensionPath);
+ try {
+ profile.addExtension(fiveuiXpi);
+ } catch (IOException e) {
+ System.err.println("could not load firefox with FiveUI");
+ e.printStackTrace();
+ }
+
+ String ffBinaryPath = System.getProperty(FIREFOX_BIN_PATH);
+
+ FirefoxDriver driver;
+ if (null == ffBinaryPath) {
+ System.err
+ .println("WARNING: Running essentially random version of FireFox!");
+ System.err.println(" set a path to firefox with -D"
+ + FIREFOX_BIN_PATH + "=<path to firefox>");
+ driver = new FirefoxDriver(profile);
+ } else {
+ FirefoxBinary binary = new FirefoxBinary(new File(ffBinaryPath));
+ driver = new FirefoxDriver(binary, profile);
+ }
+
+ return driver;
+ }
+
+ public static ChromeDriver buildChromeDriver() {
+
+ String rootPath = getRootPath();
+
+ // set the chrome driver path:
+ String chromeDriverPth =
+ mkPath(CD_BASE_PATH, osNameArch(), CD_BINARY_NAME);
+ System.setProperty("webdriver.chrome.driver", chromeDriverPth);
+
+ // setting the path to chrome also seems to cause issues:
+ ChromeOptions options = new ChromeOptions();
+ options.addArguments("--user-data-dir=" + rootPath + chromeProfilePath); // ,
+ // "--enable-logging",
+ // "--v=1");
+ options.addExtensions(new File(rootPath + chromeExtensionPath));
+
+ String chromeBinaryPath = System.getProperty(CHROME_BIN_PATH);
+ if (null == chromeBinaryPath) {
+ System.err
+ .println("WARNING: Running essentially random version of Chrome!");
+ System.err.println(" set a path to Chrome with -D"
+ + CHROME_BIN_PATH + "=<path to chrome>");
+ } else {
+ options.setBinary(new File(chromeBinaryPath));
+ }
+ // For use with ChromeDriver:
+ return new ChromeDriver(options);
+ }
+
+ private static String mkPath(String... components) {
+ StringBuilder path = new StringBuilder();
+ int remaining = components.length;
+ for (String c : components) {
+ path.append(c);
+ remaining--;
+ if (remaining != 0) {
+ path.append(File.separator);
+ }
+ }
+
+ return path.toString();
+ }
+
+ /**
+ * Determine the name of the directory that the chromedriver is in, based on
+ * os.name and os.arch.
+ *
+ * @return The name of the directory containing 'chromedriver'
+ */
+ private static String osNameArch() {
+ String rawOsName = System.getProperty("os.name").toLowerCase();
+ String osName = rawOsName.substring(0, 3);
+ boolean is64bit = System.getProperty("os.arch").indexOf("64") >= 0;
+
+ if (osName.equals("lin")) {
+ osName += is64bit ? "64" : "32";
+ }
+ return osName;
+ }
+
+}