Add tests for dash
Time-traveling test-driven-development.
--
MOS_MIGRATED_REVID=98399070
diff --git a/WORKSPACE b/WORKSPACE
index 4cd9877..33a42b6 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -36,3 +36,8 @@
name = "javax/servlet/api",
actual = "//tools/build_rules/appengine:javax.servlet.api",
)
+
+maven_jar(
+ name = "easymock",
+ artifact = "org.easymock:easymock:3.1",
+)
diff --git a/src/tools/dash/README.md b/src/tools/dash/README.md
deleted file mode 100644
index c68de45..0000000
--- a/src/tools/dash/README.md
+++ /dev/null
@@ -1,52 +0,0 @@
-# A Dashboard for Bazel
-
-This is a self-hosted dashboard for Bazel. In particular, this runs a server
-that turns build results and logs into webpages.
-
-## Running the server
-
-Build and run the server:
-
-```bash
-$ bazel build //src/tools/dash:dash
-$ bazel-bin/src/tools/dash
-```
-
-Once you see the log message `INFO: Dev App Server is now running`, you
-can visit [http://localhost:8080] to see the main page (which should say "No
-builds, yet!").
-
-This builds a .war file that can be deployed to AppEngine (although this
-doc assumes you'll run it locally).
-
-_Note: as of this writing, there is no authentication, rate limiting, or other
-protection for the dashboard. Anyone who can access the URL can read and write
-data to it. You may want to specify the `--address` or `--host` option
-(depending on AppEngine SDK version) when you run `dash` to bind the server to
-an internal network address._
-
-## Configuring Bazel to write results to the dashboard
-
-You will need to tell Bazel where to send build results. Run `bazel` with the
-`--use_dash` and `--dash_url=http://localhost:8080` flags, for
-example:
-
-```bash
-$ bazel build --use_dash --dash_url=http://localhost:8080 //foo:bar
-```
-
-If you don't want to have to specify the flags for every build and test, add
-the following lines to your .bazelrc (either in your home directory,
-_~/.bazelrc_, or on a per-project basis):
-
-```
-build --use_dash
-build --dash_url=http://localhost:8080
-```
-
-Then build results will be sent to the dashboard by default. You can specify
-`--use_dash=false` for a particular build if you don't want it sent.
-
-Please email the
-[mailing list](https://groups.google.com/forum/#!forum/bazel-discuss)
-with any questions or concerns.
diff --git a/src/tools/dash/src/main/java/BUILD b/src/tools/dash/src/main/java/BUILD
index 285b686..7c317d1 100644
--- a/src/tools/dash/src/main/java/BUILD
+++ b/src/tools/dash/src/main/java/BUILD
@@ -1,8 +1,14 @@
java_binary(
name = "java-bin",
- srcs = glob(["**/*.java"]),
main_class = "does.not.exist",
visibility = ["//src/tools/dash:__pkg__"],
+ deps = [":servlets"],
+)
+
+java_library(
+ name = "servlets",
+ srcs = glob(["**/*.java"]),
+ visibility = ["//src/tools/dash/src/test/java/com/google/devtools/dash:__pkg__"],
deps = [
"@appengine-java//:api",
"//external:javax/servlet/api",
diff --git a/src/tools/dash/src/main/java/com/google/devtools/dash/BuildViewServlet.java b/src/tools/dash/src/main/java/com/google/devtools/dash/BuildViewServlet.java
index e7faf2c..c10193b 100644
--- a/src/tools/dash/src/main/java/com/google/devtools/dash/BuildViewServlet.java
+++ b/src/tools/dash/src/main/java/com/google/devtools/dash/BuildViewServlet.java
@@ -18,8 +18,6 @@
import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
-import com.google.appengine.api.datastore.Key;
-import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.datastore.PreparedQuery;
import com.google.appengine.api.datastore.Query;
import com.google.appengine.api.datastore.Query.FilterOperator;
@@ -42,8 +40,6 @@
* Handles HTTP gets of builds/tests.
*/
public class BuildViewServlet extends HttpServlet {
- private static final String BUILD_ID =
- "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}";
private DatastoreService datastore;
public BuildViewServlet() {
@@ -56,13 +52,12 @@
DashRequest request;
try {
request = new DashRequest(req);
- } catch (IllegalArgumentException e) {
+ } catch (DashRequest.DashRequestException e) {
// TODO(kchodorow): make an error page.
response.setContentType("text/html");
response.getWriter().println("Error: " + HtmlEscapers.htmlEscaper().escape(e.getMessage()));
return;
}
- Key buildKey = KeyFactory.createKey(DashRequest.KEY_KIND, request.getBuildId());
BuildData.Builder data = BuildData.newBuilder();
Query query = new Query(DashRequest.KEY_KIND).setFilter(new FilterPredicate(
diff --git a/src/tools/dash/src/main/java/com/google/devtools/dash/DashRequest.java b/src/tools/dash/src/main/java/com/google/devtools/dash/DashRequest.java
index c4af2ff..7eab485 100644
--- a/src/tools/dash/src/main/java/com/google/devtools/dash/DashRequest.java
+++ b/src/tools/dash/src/main/java/com/google/devtools/dash/DashRequest.java
@@ -42,20 +42,20 @@
private final String buildId;
private final Blob blob;
- DashRequest(HttpServletRequest request) {
+ DashRequest(HttpServletRequest request) throws DashRequestException {
Matcher matcher = URI_REGEX.matcher(request.getRequestURI());
if (matcher.find()) {
- pageName = matcher.group(0);
- buildId = matcher.group(1);
+ pageName = matcher.group(1);
+ buildId = matcher.group(2);
} else {
- throw new IllegalArgumentException("Invalid URI pattern: " + request.getRequestURI());
+ throw new DashRequestException("Invalid URI pattern: " + request.getRequestURI());
}
try {
// Requests are capped at 32MB (see
// https://cloud.google.com/appengine/docs/quotas?csw=1#Requests).
blob = new Blob(ByteStreams.toByteArray(request.getInputStream()));
} catch (IOException e) {
- throw new IllegalArgumentException("Could not read request body: " + e.getMessage());
+ throw new DashRequestException("Could not read request body: " + e.getMessage());
}
}
@@ -70,4 +70,10 @@
entity.setProperty(BUILD_DATA, blob);
return entity;
}
+
+ static class DashRequestException extends Exception {
+ public DashRequestException(String message) {
+ super(message);
+ }
+ }
}
diff --git a/src/tools/dash/src/main/java/com/google/devtools/dash/StoreServlet.java b/src/tools/dash/src/main/java/com/google/devtools/dash/StoreServlet.java
index 2cb9fce..05a0efc 100644
--- a/src/tools/dash/src/main/java/com/google/devtools/dash/StoreServlet.java
+++ b/src/tools/dash/src/main/java/com/google/devtools/dash/StoreServlet.java
@@ -39,7 +39,7 @@
DashRequest request;
try {
request = new DashRequest(req);
- } catch (IllegalArgumentException e) {
+ } catch (DashRequest.DashRequestException e) {
response.setContentType("text/json");
response.getWriter().println(
"{ \"error\": \"" + e.getMessage().replaceAll("\"", "") + "\" }");
diff --git a/src/tools/dash/src/main/webapp/WEB-INF/appengine-web.xml b/src/tools/dash/src/main/webapp/WEB-INF/appengine-web.xml
deleted file mode 100644
index 7118a0e..0000000
--- a/src/tools/dash/src/main/webapp/WEB-INF/appengine-web.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
- <application>dash-of-bazel</application>
- <version>1</version>
- <threadsafe>true</threadsafe>
-</appengine-web-app>
diff --git a/src/tools/dash/src/main/webapp/WEB-INF/web.xml b/src/tools/dash/src/main/webapp/WEB-INF/web.xml
deleted file mode 100644
index a4b6daf..0000000
--- a/src/tools/dash/src/main/webapp/WEB-INF/web.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!DOCTYPE web-app PUBLIC
-"-//Oracle Corporation//DTD Web Application 2.3//EN"
-"http://java.sun.com/dtd/web-app_2_3.dtd">
-
-<web-app xmlns="http://java.sun.com/xml/ns/javaee" version="2.5">
- <servlet>
- <servlet-name>build</servlet-name>
- <servlet-class>com.google.devtools.dash.BuildViewServlet</servlet-class>
- </servlet>
- <servlet>
- <servlet-name>store</servlet-name>
- <servlet-class>com.google.devtools.dash.StoreServlet</servlet-class>
- </servlet>
-
- <servlet-mapping>
- <servlet-name>build</servlet-name>
- <url-pattern>/result/*</url-pattern>
- </servlet-mapping>
- <servlet-mapping>
- <servlet-name>store</servlet-name>
- <url-pattern>/start/*</url-pattern>
- </servlet-mapping>
- <servlet-mapping>
- <servlet-name>store</servlet-name>
- <url-pattern>/options/*</url-pattern>
- </servlet-mapping>
- <servlet-mapping>
- <servlet-name>store</servlet-name>
- <url-pattern>/targets/*</url-pattern>
- </servlet-mapping>
- <servlet-mapping>
- <servlet-name>store</servlet-name>
- <url-pattern>/test/*</url-pattern>
- </servlet-mapping>
-
- <welcome-file-list>
- <welcome-file>index.html</welcome-file>
- </welcome-file-list>
-</web-app>
diff --git a/src/tools/dash/src/main/webapp/dashboard.css b/src/tools/dash/src/main/webapp/dashboard.css
deleted file mode 100644
index d3d3904..0000000
--- a/src/tools/dash/src/main/webapp/dashboard.css
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * Base structure
- */
-
-/* Move down content because we have a fixed navbar that is 50px tall */
-body {
- padding-top: 50px;
-}
-
-
-/*
- * Global add-ons
- */
-
-.sub-header {
- padding-bottom: 10px;
- border-bottom: 1px solid #eee;
-}
-
-/*
- * Top navigation
- * Hide default border to remove 1px line.
- */
-.navbar-fixed-top {
- border: 0;
-}
-
-/*
- * Sidebar
- */
-
-/* Hide for mobile, show later */
-.sidebar {
- display: none;
-}
-@media (min-width: 768px) {
- .sidebar {
- position: fixed;
- top: 51px;
- bottom: 0;
- left: 0;
- z-index: 1000;
- display: block;
- padding: 20px;
- overflow-x: hidden;
- overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
- background-color: #f5f5f5;
- border-right: 1px solid #eee;
- }
-}
-
-/* Sidebar navigation */
-.nav-sidebar {
- margin-right: -21px; /* 20px padding + 1px border */
- margin-bottom: 20px;
- margin-left: -20px;
-}
-.nav-sidebar > li > a {
- padding-right: 20px;
- padding-left: 20px;
-}
-.nav-sidebar > .active > a,
-.nav-sidebar > .active > a:hover,
-.nav-sidebar > .active > a:focus {
- color: #fff;
- background-color: #428bca;
-}
-
-
-/*
- * Main content
- */
-
-.main {
- padding: 20px;
-}
-@media (min-width: 768px) {
- .main {
- padding-right: 40px;
- padding-left: 40px;
- }
-}
-.main .page-header {
- margin-top: 0;
-}
-
-
-/*
- * Placeholder dashboard ideas
- */
-
-.placeholders {
- margin-bottom: 30px;
- text-align: center;
-}
-.placeholders h4 {
- margin-bottom: 0;
-}
-.placeholder {
- margin-bottom: 20px;
-}
-.placeholder img {
- display: inline-block;
- border-radius: 50%;
-}
-
-code {
- color: #00A388;
- background-color: transparent;
-}
diff --git a/src/tools/dash/src/main/webapp/favicon.ico b/src/tools/dash/src/main/webapp/favicon.ico
deleted file mode 100644
index a37c760..0000000
--- a/src/tools/dash/src/main/webapp/favicon.ico
+++ /dev/null
Binary files differ
diff --git a/src/tools/dash/src/main/webapp/index.html b/src/tools/dash/src/main/webapp/index.html
deleted file mode 100644
index db1b6af..0000000
--- a/src/tools/dash/src/main/webapp/index.html
+++ /dev/null
@@ -1,69 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
- <!-- TODO(kchodorow): make this and result.html inherit from a parent template. -->
- <head>
- <meta charset="utf-8">
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <!-- The above 3 meta tags *must* come first in the head; any other head content must come
- *after* these tags -->
- <meta name="description" content="">
- <meta name="author" content="">
- <link rel="icon" href="/favicon.ico">
-
- <title>Build $build_data.getBuildId()</title>
-
- <!-- Bootstrap core CSS -->
- <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css"
- rel="stylesheet">
-
- <!-- Custom styles for this template -->
- <link href="/dashboard.css" rel="stylesheet">
-
- <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
- <!--[if lt IE 9]>
- <script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
- <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
- <![endif]-->
- </head>
-
- <body>
-
- <nav class="navbar navbar-inverse navbar-fixed-top">
- <div class="container-fluid">
- <div class="navbar-header">
- <button type="button" class="navbar-toggle collapsed" data-toggle="collapse"
- data-target="#navbar" aria-expanded="false" aria-controls="navbar">
- <span class="sr-only">Toggle navigation</span>
- <span class="icon-bar"></span>
- <span class="icon-bar"></span>
- <span class="icon-bar"></span>
- </button>
- <a class="navbar-brand" href="#">Dash of Bazel</a>
- </div>
- <div id="navbar" class="navbar-collapse collapse">
- <ul class="nav navbar-nav navbar-right">
- <li><a href="/">Dashboard</a></li>
- <!-- TODO(kchodorow): link to a bazel.io documentation. -->
- <li><a href="#todo">Help</a></li>
- </ul>
- <form class="navbar-form navbar-right">
- <!-- TODO(kchodorow): add fulltext search. -->
- <input type="text" class="form-control" placeholder="Search...">
- </form>
- </div>
- </div>
- </nav>
-
- <div class="container-fluid">
- <!-- TODO(kchodorow): add a list of existing builds. -->
- <h2>No builds, yet!</h2>
- </div>
-
- <!-- Bootstrap core JavaScript
- ================================================== -->
- <!-- Placed at the end of the document so the pages load faster -->
- <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
- <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
- </body>
-</html>
diff --git a/src/tools/dash/src/main/webapp/result.html b/src/tools/dash/src/main/webapp/result.html
deleted file mode 100644
index 106fec6..0000000
--- a/src/tools/dash/src/main/webapp/result.html
+++ /dev/null
@@ -1,156 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
- <meta charset="utf-8">
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <!-- The above 3 meta tags *must* come first in the head; any other head content must come
- *after* these tags -->
- <meta name="description" content="">
- <meta name="author" content="">
- <link rel="icon" href="/favicon.ico">
-
- <title>Build $build_data.getBuildId()</title>
-
- <!-- Bootstrap core CSS -->
- <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css"
- rel="stylesheet">
-
- <!-- Custom styles for this template -->
- <link href="/dashboard.css" rel="stylesheet">
-
- <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
- <!--[if lt IE 9]>
- <script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
- <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
- <![endif]-->
-</head>
-
-<body>
-
-<nav class="navbar navbar-inverse navbar-fixed-top">
- <div class="container-fluid">
- <div class="navbar-header">
- <button type="button" class="navbar-toggle collapsed" data-toggle="collapse"
- data-target="#navbar" aria-expanded="false" aria-controls="navbar">
- <span class="sr-only">Toggle navigation</span>
- <span class="icon-bar"></span>
- <span class="icon-bar"></span>
- <span class="icon-bar"></span>
- </button>
- <a class="navbar-brand" href="#">$build_data.getBuildId()</a>
- </div>
- <div id="navbar" class="navbar-collapse collapse">
- <ul class="nav navbar-nav navbar-right">
- <li><a href="/">Dashboard</a></li>
- <!-- TODO(kchodorow): link to a bazel.io documentation. -->
- <li><a href="#">Help</a></li>
- </ul>
- <form class="navbar-form navbar-right">
- <!-- TODO(kchodorow): add fulltext search. -->
- <input type="text" class="form-control" placeholder="Search...">
- </form>
- </div>
- </div>
-</nav>
-
-<div class="container-fluid">
- <div class="row">
- <div class="col-sm-3 col-md-2 sidebar">
- <ul class="nav nav-sidebar">
- <li class="active">
- <a href="#results">
- Results <span class="sr-only">(current)</span>
- </a>
- </li>
- <li><a href="#command-line">Command line</a></li>
- <li><a href="#env">Environment</a></li>
- </ul>
- </div>
-
- <div id="results" class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
- <h1 class="page-header">bazel $build_data.getCommandName() results</h1>
- <div class="table-responsive">
- <table class="table table-striped">
- <thead>
- <tr>
- <th>Target</th>
- <th>Status</th>
- </tr>
- </thead>
- <tbody>
- #if ($build_data.getCommandName() == "test")
- #foreach( $result in $build_data.getTestDataList() )
- <tr>
- #if ( $result.getPassed() )
- <td>
- <div>
- <code style="color:#79BD8F; background-color: transparent;">$result.getLabel()</code>
- </div>
- </td>
- <td style="color:#79BD8F;">Passed</td>
- #else
- <td>
- <div onclick="$('#log-$velocityCount').toggle();">
- <code style="color:#FF6138;">$result.getLabel()</code>
- </div>
- <pre id="log-$velocityCount" style="display: none;">
-$result.getLog().toStringUtf8()
- </pre>
- #if ( $result.getTruncated() )
- <div>Truncated after 1MB, see local log for full output.</div>
- #end
- </td>
- <td style="color:#FF6138;">Failed</td>
- #end
- </tr>
- #end
- #else
- #foreach( $result in $build_data.getTargetsList() )
- <tr>
- <td><code>$result.getLabel()</code></td>
- <td>Built</td>
- </tr>
- #end
- #end
- </tbody>
- </table>
- </div>
- </div>
-
- <div id="command-line" class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
- <h1>bazel command line</h1>
- <div>
- <pre>
-bazel #foreach( $option in $build_data.getCommandLine().getStartupOptionsList())--$option #end $build_data.getCommandName() #foreach( $option in $build_data.getCommandLine().getOptionsList())--$option #end #foreach( $residue in $build_data.getCommandLine().getResidueList())$residue #end
- </pre>
- </div>
- </div>
-
- <div id="env" class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
- <h1>bazel environment</h1>
- <div>
- <table>
- <tr>
- <th>Name</th>
- <th>Value</th>
- </tr>
- #foreach( $env in $build_data.getClientEnvList())
- <tr>
- <td><code>$env.getName()</code></td>
- <td><code>$env.getValue()</code></td>
- </tr>
- #end
- </pre>
- </div>
- </div>
- </div>
-</div>
-
-<!-- Bootstrap core JavaScript
-================================================== -->
-<!-- Placed at the end of the document so the pages load faster -->
-<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
-<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
-</body>
-</html>
diff --git a/src/tools/dash/src/test/java/com/google/devtools/dash/AllTests.java b/src/tools/dash/src/test/java/com/google/devtools/dash/AllTests.java
new file mode 100644
index 0000000..4d92ffb
--- /dev/null
+++ b/src/tools/dash/src/test/java/com/google/devtools/dash/AllTests.java
@@ -0,0 +1,26 @@
+// Copyright 2015 Google Inc. All rights reserved.
+//
+// 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.google.devtools.dash;
+
+import com.google.devtools.build.lib.testutil.ClasspathSuite;
+
+import org.junit.runner.RunWith;
+
+/**
+ * Runs all tests.
+ */
+@RunWith(ClasspathSuite.class)
+public class AllTests {
+}
diff --git a/src/tools/dash/src/test/java/com/google/devtools/dash/BUILD b/src/tools/dash/src/test/java/com/google/devtools/dash/BUILD
new file mode 100644
index 0000000..d65b06c
--- /dev/null
+++ b/src/tools/dash/src/test/java/com/google/devtools/dash/BUILD
@@ -0,0 +1,34 @@
+java_library(
+ name = "AllTests",
+ srcs = ["AllTests.java"],
+ deps = [
+ "//src/test/java:testutil",
+ "//third_party:junit4",
+ ],
+)
+
+java_library(
+ name = "util",
+ srcs = ["ProtoInputStream.java"],
+ deps = [
+ "//external:javax/servlet/api",
+ "//third_party:protobuf",
+ ],
+)
+
+java_test(
+ name = "dash",
+ srcs = [
+ "DashRequestTest.java",
+ ],
+ runtime_deps = [":AllTests"],
+ deps = [
+ "@appengine-java//:jars",
+ "@easymock//jar",
+ ":util",
+ "//src/main/protobuf:proto_dash",
+ "//src/tools/dash/src/main/java:servlets",
+ "//third_party:junit4",
+ "//third_party:truth",
+ ],
+)
diff --git a/src/tools/dash/src/test/java/com/google/devtools/dash/DashRequestTest.java b/src/tools/dash/src/test/java/com/google/devtools/dash/DashRequestTest.java
new file mode 100644
index 0000000..02adc72
--- /dev/null
+++ b/src/tools/dash/src/test/java/com/google/devtools/dash/DashRequestTest.java
@@ -0,0 +1,98 @@
+// Copyright 2015 Google Inc. All rights reserved.
+//
+// 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.google.devtools.dash;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import com.google.appengine.api.datastore.Blob;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
+import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
+import com.google.devtools.build.lib.bazel.dash.DashProtos;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Tests for {@link DashRequest}.
+ */
+@RunWith(JUnit4.class)
+public class DashRequestTest {
+
+ private final LocalServiceTestHelper helper =
+ new LocalServiceTestHelper(new LocalDatastoreServiceTestConfig());
+ private HttpServletRequest request;
+
+ @Before
+ public void setUp() {
+ helper.setUp();
+ request = createMock(HttpServletRequest.class);
+ }
+
+ @After
+ public void tearDown() {
+ helper.tearDown();
+ }
+
+ @Test
+ public void testUriParsing() throws Exception {
+ final String buildId = "3b9a81d9-0ed3-48d2-84c6-6296aecc21e6";
+ final DashProtos.BuildData data = DashProtos.BuildData.newBuilder().setBuildId(buildId).build();
+ expect(request.getRequestURI()).andReturn("/result/3b9a81d9-0ed3-48d2-84c6-6296aecc21e6");
+ expect(request.getInputStream()).andReturn(new ProtoInputStream(data));
+ replay(request);
+
+ DashRequest dashRequest = new DashRequest(request);
+ assertEquals(buildId, dashRequest.getBuildId());
+ Entity entity = dashRequest.getEntity();
+ assertEquals(buildId, entity.getProperty(DashRequest.BUILD_ID));
+ assertEquals("result", entity.getProperty(DashRequest.PAGE_NAME));
+ assertEquals(data, DashProtos.BuildData.parseFrom(
+ ((Blob) entity.getProperty(DashRequest.BUILD_DATA)).getBytes()));
+ }
+
+ private void uriError(String uri) {
+ expect(request.getRequestURI()).andReturn(uri).times(2);
+ replay(request);
+
+ try {
+ new DashRequest(request);
+ fail("Should have thrown");
+ } catch (DashRequest.DashRequestException e) {
+ assertThat(e.getMessage()).contains("Invalid URI pattern: " + uri);
+ }
+ }
+
+ @Test
+ public void testInvalidUuid() {
+ uriError("/result/3b9a81d9-0ed3-48d2");
+ }
+
+ @Test
+ public void testMissingPageName() {
+ uriError("/3b9a81d9-0ed3-48d2-84c6-6296aecc21e6");
+ }
+
+}
diff --git a/src/tools/dash/src/test/java/com/google/devtools/dash/ProtoInputStream.java b/src/tools/dash/src/test/java/com/google/devtools/dash/ProtoInputStream.java
new file mode 100644
index 0000000..35cc7c0
--- /dev/null
+++ b/src/tools/dash/src/test/java/com/google/devtools/dash/ProtoInputStream.java
@@ -0,0 +1,39 @@
+// Copyright 2015 Google Inc. All rights reserved.
+//
+// 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.google.devtools.dash;
+
+import com.google.protobuf.Message;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import javax.servlet.ServletInputStream;
+
+/**
+ * Input stream for testing.
+ */
+public class ProtoInputStream extends ServletInputStream {
+ InputStream stream;
+
+ ProtoInputStream(Message message) {
+ stream = new ByteArrayInputStream(message.toByteArray());
+ }
+
+ @Override
+ public int read() throws IOException {
+ return stream.read();
+ }
+
+}