diff options
author | Rogan Creswick <creswick@gmail.com> | 2013-06-07 16:00:22 -0700 |
---|---|---|
committer | Rogan Creswick <creswick@gmail.com> | 2013-06-07 16:00:22 -0700 |
commit | 88c95d18a81e4f107cc4e5967bfa45d1bf4882a1 (patch) | |
tree | f1b8f5bb1bffd8ea84078d829248dddbdc2f3544 /src/batchtools | |
parent | 04d3c6e96ed4dd528418fe71a85e72316ae5bba4 (diff) |
cleaned up some test files that broke during a merge
Diffstat (limited to 'src/batchtools')
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 © 2001,2005-2012 Jarno Elonen (elonen@iki.fi, http://iki.fi/elonen/) + * and Copyright © 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 += " <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 Binary files differnew file mode 100755 index 0000000..b0a0e4a --- /dev/null +++ b/src/batchtools/rsTester/src/main/resources/seleniumDrivers/linux64/chromedriver 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; + } + +} |