storage: end-to-end query test with synthetic data

Change-Id: Ie4f48245e1f223486557921a4196570c430e5feb
Reviewed-on: https://go-review.googlesource.com/34934
Reviewed-by: Russ Cox <rsc@golang.org>
diff --git a/storage/app/query_test.go b/storage/app/query_test.go
new file mode 100644
index 0000000..7c79e94
--- /dev/null
+++ b/storage/app/query_test.go
@@ -0,0 +1,85 @@
+// Copyright 2017 The Go Authors.  All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package app
+
+import (
+	"fmt"
+	"io"
+	"mime/multipart"
+	"net/http"
+	"net/url"
+	"testing"
+
+	"golang.org/x/perf/storage/benchfmt"
+)
+
+func TestQuery(t *testing.T) {
+	app := createTestApp(t)
+	defer app.Close()
+
+	// Write 1024 test results to the database.  These results
+	// have labels named label0, label1, etc. Each label's value
+	// is an integer whose value is (record number) / (1 << label
+	// number).  So 1 record has each value of label0, 2 records
+	// have each value of label1, 4 records have each value of
+	// label2, etc. This allows writing queries that match 2^n records.
+	app.uploadFiles(t, func(mpw *multipart.Writer) {
+		w, err := mpw.CreateFormFile("file", "1.txt")
+		if err != nil {
+			t.Errorf("CreateFormFile: %v", err)
+		}
+		bp := benchfmt.NewPrinter(w)
+		for i := 0; i < 1024; i++ {
+			r := &benchfmt.Result{Labels: make(map[string]string), NameLabels: make(map[string]string), Content: "BenchmarkName 1 ns/op"}
+			for j := uint(0); j < 10; j++ {
+				r.Labels[fmt.Sprintf("label%d", j)] = fmt.Sprintf("%d", i/(1<<j))
+			}
+			r.NameLabels["name"] = "Name"
+			if err := bp.Print(r); err != nil {
+				t.Fatalf("Print: %v", err)
+			}
+		}
+	})
+
+	tests := []struct {
+		q    string
+		want []int
+	}{
+		{"label0:0", []int{0}},
+		{"label1:0", []int{0, 1}},
+		{"label0:5 name:Name", []int{5}},
+		{"label0:0 label0:5", nil},
+	}
+	for _, test := range tests {
+		t.Run("query="+test.q, func(t *testing.T) {
+			u := app.srv.URL + "/search?" + url.Values{"q": []string{test.q}}.Encode()
+			resp, err := http.Get(u)
+			if err != nil {
+				t.Fatal(err)
+			}
+			defer resp.Body.Close()
+			if resp.StatusCode != 200 {
+				t.Fatalf("get /search: %v", resp.Status)
+			}
+			br := benchfmt.NewReader(resp.Body)
+			for i, num := range test.want {
+				r, err := br.Next()
+				if err != nil {
+					t.Fatalf("#%d: Next() = %v, want nil", i, err)
+				}
+				if r.Labels["label0"] != fmt.Sprintf("%d", num) {
+					t.Errorf("#%d: label0 = %q, want %d", i, r.Labels["label0"], num)
+				}
+				if r.NameLabels["name"] != "Name" {
+					t.Errorf("#%d: name = %q, want %q", i, r.NameLabels["name"], "Name")
+				}
+			}
+			_, err = br.Next()
+			if err != io.EOF {
+				t.Errorf("Next() = %v, want EOF", err)
+			}
+		})
+	}
+}
diff --git a/storage/app/upload_test.go b/storage/app/upload_test.go
index b2bc143..d4ef6aa 100644
--- a/storage/app/upload_test.go
+++ b/storage/app/upload_test.go
@@ -5,6 +5,7 @@
 package app
 
 import (
+	"encoding/json"
 	"fmt"
 	"io"
 	"io/ioutil"
@@ -18,46 +19,87 @@
 	"golang.org/x/perf/storage/fs"
 )
 
-func TestUpload(t *testing.T) {
+type testApp struct {
+	db  *db.DB
+	fs  *fs.MemFS
+	app *App
+	srv *httptest.Server
+}
+
+func (app *testApp) Close() {
+	app.db.Close()
+	app.srv.Close()
+}
+
+// createTestApp returns a testApp corresponding to a new app
+// serving from an in-memory database and file system on an
+// isolated test HTTP server.
+//
+// When finished with app, the caller must call app.Close().
+func createTestApp(t *testing.T) *testApp {
 	db, err := db.OpenSQL("sqlite3", ":memory:")
 	if err != nil {
 		t.Fatalf("open database: %v", err)
 	}
-	defer db.Close()
 
 	fs := fs.NewMemFS()
 
 	app := &App{DB: db, FS: fs}
 
-	srv := httptest.NewServer(http.HandlerFunc(app.upload))
-	defer srv.Close()
+	mux := http.NewServeMux()
+	app.RegisterOnMux(mux)
+
+	srv := httptest.NewServer(mux)
+
+	return &testApp{db, fs, app, srv}
+}
+
+// uploadFiles calls the /upload endpoint and executes f in a new
+// goroutine to write files to the POST request.
+func (app *testApp) uploadFiles(t *testing.T, f func(*multipart.Writer)) *uploadStatus {
 	pr, pw := io.Pipe()
 	mpw := multipart.NewWriter(pw)
+
 	go func() {
 		defer pw.Close()
 		defer mpw.Close()
-		// Write the parts here
+		f(mpw)
+	}()
+
+	resp, err := http.Post(app.srv.URL+"/upload", mpw.FormDataContentType(), pr)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer resp.Body.Close()
+	if resp.StatusCode != 200 {
+		t.Fatalf("post /upload: %v", resp.Status)
+	}
+	body, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		t.Fatalf("reading /upload response: %v", err)
+	}
+	t.Logf("/upload response:\n%s", body)
+
+	status := &uploadStatus{}
+	if err := json.Unmarshal(body, status); err != nil {
+		t.Fatalf("unmarshaling /upload response: %v", err)
+	}
+	return status
+}
+
+func TestUpload(t *testing.T) {
+	app := createTestApp(t)
+	defer app.Close()
+
+	app.uploadFiles(t, func(mpw *multipart.Writer) {
 		w, err := mpw.CreateFormFile("file", "1.txt")
 		if err != nil {
 			t.Errorf("CreateFormFile: %v", err)
 		}
 		fmt.Fprintf(w, "key: value\nBenchmarkOne 5 ns/op\nkey:value2\nBenchmarkTwo 10 ns/op\n")
-	}()
-	resp, err := http.Post(srv.URL, mpw.FormDataContentType(), pr)
-	if err != nil {
-		t.Fatalf("post /upload: %v", err)
-	}
-	defer resp.Body.Close()
-	if resp.StatusCode != 200 {
-		t.Errorf("post /upload: %v", resp.Status)
-	}
-	body, err := ioutil.ReadAll(resp.Body)
-	if err != nil {
-		t.Errorf("reading /upload response: %v", err)
-	}
-	t.Logf("/upload response:\n%s", body)
+	})
 
-	if len(fs.Files()) != 1 {
-		t.Errorf("/upload wrote %d files, want 1", len(fs.Files()))
+	if len(app.fs.Files()) != 1 {
+		t.Errorf("/upload wrote %d files, want 1", len(app.fs.Files()))
 	}
 }