storage: add Client for interacting with storage server

Change-Id: I1bf6c01e5ad08b6bc780cd69ea0d1a2877691f7a
Reviewed-on: https://go-review.googlesource.com/35451
Reviewed-by: Russ Cox <rsc@golang.org>
diff --git a/storage/client.go b/storage/client.go
new file mode 100644
index 0000000..09f5925
--- /dev/null
+++ b/storage/client.go
@@ -0,0 +1,100 @@
+// 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 storage contains a client for the performance data storage server.
+package storage
+
+import (
+	"io"
+	"net/http"
+	"net/url"
+
+	"golang.org/x/perf/storage/benchfmt"
+)
+
+// A Client issues queries to a performance data storage server.
+// It is safe to use from multiple goroutines simultaneously.
+type Client struct {
+	// BaseURL is the base URL of the storage server.
+	BaseURL string
+	// HTTPClient is the HTTP client for sending requests. If nil, http.DefaultClient will be used.
+	HTTPClient *http.Client
+}
+
+// httpClient returns the http.Client to use for requests.
+func (c *Client) httpClient() *http.Client {
+	if c.HTTPClient != nil {
+		return c.HTTPClient
+	}
+	return http.DefaultClient
+}
+
+// Query searches for results matching the given query string.
+//
+// The query string is first parsed into quoted words (as in the shell)
+// and then each word must be formatted as one of the following:
+// key:value - exact match on label "key" = "value"
+// key>value - value greater than (useful for dates)
+// key<value - value less than (also useful for dates)
+func (c *Client) Query(q string) *Query {
+	hc := c.httpClient()
+
+	resp, err := hc.Get(c.BaseURL + "/search?" + url.Values{"q": []string{q}}.Encode())
+	if err != nil {
+		return &Query{err: err}
+	}
+
+	br := benchfmt.NewReader(resp.Body)
+
+	return &Query{br: br, body: resp.Body}
+}
+
+// A Query allows iteration over the results of a search query.
+// Use Next to advance through the results, making sure to call Close when done:
+//
+//   q := client.Query("key:value")
+//   defer q.Close()
+//   for q.Next() {
+//     res := q.Result()
+//     ...
+//   }
+//   if err = q.Err(); err != nil {
+//     // handle error encountered during query
+//   }
+type Query struct {
+	br   *benchfmt.Reader
+	body io.ReadCloser
+	err  error
+}
+
+// Next prepares the next result for reading with the Result
+// method. It returns false when there are no more results, either by
+// reaching the end of the input or an error.
+func (q *Query) Next() bool {
+	if q.err != nil {
+		return false
+	}
+	return q.br.Next()
+}
+
+// Result returns the most recent result generated by a call to Next.
+func (q *Query) Result() *benchfmt.Result {
+	return q.br.Result()
+}
+
+// Err returns the first error encountered during the query.
+func (q *Query) Err() error {
+	if q.err != nil {
+		return q.err
+	}
+	return q.br.Err()
+}
+
+// Close frees resources associated with the query.
+func (q *Query) Close() error {
+	q.body.Close()
+	return q.err
+}
+
+// TODO(quentin): Move upload code here from cmd/benchsave?
diff --git a/storage/client_test.go b/storage/client_test.go
new file mode 100644
index 0000000..c05ad20
--- /dev/null
+++ b/storage/client_test.go
@@ -0,0 +1,82 @@
+// 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 storage
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"net/http/httptest"
+	"os"
+	"os/exec"
+	"testing"
+
+	"golang.org/x/perf/storage/benchfmt"
+)
+
+func TestQuery(t *testing.T) {
+	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		if have, want := r.URL.RequestURI(), "/search?q=key1%3Avalue+key2%3Avalue"; have != want {
+			t.Errorf("RequestURI = %q, want %q", have, want)
+		}
+		fmt.Fprintf(w, "key: value\nBenchmarkOne 5 ns/op\nkey: value2\nBenchmarkTwo 10 ns/op\n")
+	}))
+	defer ts.Close()
+
+	c := &Client{BaseURL: ts.URL}
+
+	q := c.Query("key1:value key2:value")
+	defer q.Close()
+
+	var buf bytes.Buffer
+	bp := benchfmt.NewPrinter(&buf)
+
+	for q.Next() {
+		if err := bp.Print(q.Result()); err != nil {
+			t.Fatalf("Print: %v", err)
+		}
+	}
+	if err := q.Err(); err != nil {
+		t.Fatalf("Err: %v", err)
+	}
+	want := "key: value\nBenchmarkOne 5 ns/op\nkey: value2\nBenchmarkTwo 10 ns/op\n"
+	if diff := diff(buf.String(), want); diff != "" {
+		t.Errorf("wrong results: (- have/+ want)\n%s", diff)
+	}
+}
+
+// diff returns the output of unified diff on s1 and s2. If the result
+// is non-empty, the strings differ or the diff command failed.
+func diff(s1, s2 string) string {
+	f1, err := ioutil.TempFile("", "benchfmt_test")
+	if err != nil {
+		return err.Error()
+	}
+	defer os.Remove(f1.Name())
+	defer f1.Close()
+
+	f2, err := ioutil.TempFile("", "benchfmt_test")
+	if err != nil {
+		return err.Error()
+	}
+	defer os.Remove(f2.Name())
+	defer f2.Close()
+
+	f1.Write([]byte(s1))
+	f2.Write([]byte(s2))
+
+	data, err := exec.Command("diff", "-u", f1.Name(), f2.Name()).CombinedOutput()
+	if len(data) > 0 {
+		// diff exits with a non-zero status when the files don't match.
+		// Ignore that failure as long as we get output.
+		err = nil
+	}
+	if err != nil {
+		data = append(data, []byte(err.Error())...)
+	}
+	return string(data)
+
+}