http out test cases (#928)
* http spec test example
* support for local-server based spec tests
* read test cases from file:
* the same test cases file as for C#
* moved test cases file to testdata
* updated to the lateat spec
* attributes
* fixed linter errors
* fix sync issue
* use already defined map instead
diff --git a/plugin/ochttp/testdata/download-test-cases.sh b/plugin/ochttp/testdata/download-test-cases.sh
new file mode 100755
index 0000000..3baa114
--- /dev/null
+++ b/plugin/ochttp/testdata/download-test-cases.sh
@@ -0,0 +1,5 @@
+# This script downloads latest test cases from specs
+
+# TODO: change the link to when test cases are merged to specs repo
+
+curl https://raw.githubusercontent.com/census-instrumentation/opencensus-specs/master/trace/http-out-test-cases.json -o http-out-test-cases.json
\ No newline at end of file
diff --git a/plugin/ochttp/testdata/http-out-test-cases.json b/plugin/ochttp/testdata/http-out-test-cases.json
new file mode 100644
index 0000000..039a73e
--- /dev/null
+++ b/plugin/ochttp/testdata/http-out-test-cases.json
@@ -0,0 +1,274 @@
+[
+ {
+ "name": "Successful GET call to https://example.com",
+ "method": "GET",
+ "url": "https://example.com/",
+ "spanName": "/",
+ "spanStatus": "OK",
+ "spanKind": "Client",
+ "spanAttributes": {
+ "http.path": "/",
+ "http.method": "GET",
+ "http.host": "example.com",
+ "http.status_code": "200",
+ "http.url": "https://example.com/"
+ }
+ },
+ {
+ "name": "Successfully POST call to https://example.com",
+ "method": "POST",
+ "url": "https://example.com/",
+ "spanName": "/",
+ "spanStatus": "OK",
+ "spanKind": "Client",
+ "spanAttributes": {
+ "http.path": "/",
+ "http.method": "POST",
+ "http.host": "example.com",
+ "http.status_code": "200",
+ "http.url": "https://example.com/"
+ }
+ },
+ {
+ "name": "Name is populated as a path",
+ "method": "GET",
+ "url": "http://{host}:{port}/path/to/resource/",
+ "responseCode": 200,
+ "spanName": "/path/to/resource/",
+ "spanStatus": "OK",
+ "spanKind": "Client",
+ "spanAttributes": {
+ "http.path": "/path/to/resource/",
+ "http.method": "GET",
+ "http.host": "{host}:{port}",
+ "http.status_code": "200",
+ "http.url": "http://{host}:{port}/path/to/resource/"
+ }
+ },
+ {
+ "name": "Call that cannot resolve DNS will be reported as error span",
+ "method": "GET",
+ "url": "https://sdlfaldfjalkdfjlkajdflkajlsdjf.sdlkjafsdjfalfadslkf.com/",
+ "spanName": "/",
+ "spanStatus": "UNKNOWN",
+ "spanKind": "Client",
+ "spanAttributes": {
+ "http.path": "/",
+ "http.method": "GET",
+ "http.host": "sdlfaldfjalkdfjlkajdflkajlsdjf.sdlkjafsdjfalfadslkf.com",
+ "http.url": "https://sdlfaldfjalkdfjlkajdflkajlsdjf.sdlkjafsdjfalfadslkf.com/"
+ }
+ },
+ {
+ "name": "Response code: 199. This test case is not possible to implement on some platforms as they don't allow to return this status code. Keeping this test case for visibility, but it actually simply a fallback into 200 test case",
+ "method": "GET",
+ "url": "http://{host}:{port}/",
+ "responseCode": 200,
+ "spanName": "/",
+ "spanStatus": "OK",
+ "spanKind": "Client",
+ "spanAttributes": {
+ "http.path": "/",
+ "http.method": "GET",
+ "http.host": "{host}:{port}",
+ "http.status_code": "200",
+ "http.url": "http://{host}:{port}/"
+ }
+ },
+ {
+ "name": "Response code: 200",
+ "method": "GET",
+ "url": "http://{host}:{port}/",
+ "responseCode": 200,
+ "spanName": "/",
+ "spanStatus": "OK",
+ "spanKind": "Client",
+ "spanAttributes": {
+ "http.path": "/",
+ "http.method": "GET",
+ "http.host": "{host}:{port}",
+ "http.status_code": "200",
+ "http.url": "http://{host}:{port}/"
+ }
+ },
+ {
+ "name": "Response code: 399",
+ "method": "GET",
+ "url": "http://{host}:{port}/",
+ "responseCode": 399,
+ "spanName": "/",
+ "spanStatus": "OK",
+ "spanKind": "Client",
+ "spanAttributes": {
+ "http.path": "/",
+ "http.method": "GET",
+ "http.host": "{host}:{port}",
+ "http.status_code": "399",
+ "http.url": "http://{host}:{port}/"
+ }
+ },
+ {
+ "name": "Response code: 400",
+ "method": "GET",
+ "url": "http://{host}:{port}/",
+ "responseCode": 400,
+ "spanName": "/",
+ "spanStatus": "INVALID_ARGUMENT",
+ "spanKind": "Client",
+ "spanAttributes": {
+ "http.path": "/",
+ "http.method": "GET",
+ "http.host": "{host}:{port}",
+ "http.status_code": "400",
+ "http.url": "http://{host}:{port}/"
+ }
+ },
+ {
+ "name": "Response code: 401",
+ "method": "GET",
+ "url": "http://{host}:{port}/",
+ "responseCode": 401,
+ "spanName": "/",
+ "spanStatus": "UNAUTHENTICATED",
+ "spanKind": "Client",
+ "spanAttributes": {
+ "http.path": "/",
+ "http.method": "GET",
+ "http.host": "{host}:{port}",
+ "http.status_code": "401",
+ "http.url": "http://{host}:{port}/"
+ }
+ },
+ {
+ "name": "Response code: 403",
+ "method": "GET",
+ "url": "http://{host}:{port}/",
+ "responseCode": 403,
+ "spanName": "/",
+ "spanStatus": "PERMISSION_DENIED",
+ "spanKind": "Client",
+ "spanAttributes": {
+ "http.path": "/",
+ "http.method": "GET",
+ "http.host": "{host}:{port}",
+ "http.status_code": "403",
+ "http.url": "http://{host}:{port}/"
+ }
+ },
+ {
+ "name": "Response code: 404",
+ "method": "GET",
+ "url": "http://{host}:{port}/",
+ "responseCode": 404,
+ "spanName": "/",
+ "spanStatus": "NOT_FOUND",
+ "spanKind": "Client",
+ "spanAttributes": {
+ "http.path": "/",
+ "http.method": "GET",
+ "http.host": "{host}:{port}",
+ "http.status_code": "404",
+ "http.url": "http://{host}:{port}/"
+ }
+ },
+ {
+ "name": "Response code: 429",
+ "method": "GET",
+ "url": "http://{host}:{port}/",
+ "responseCode": 429,
+ "spanName": "/",
+ "spanStatus": "RESOURCE_EXHAUSTED",
+ "spanKind": "Client",
+ "spanAttributes": {
+ "http.path": "/",
+ "http.method": "GET",
+ "http.host": "{host}:{port}",
+ "http.status_code": "429",
+ "http.url": "http://{host}:{port}/"
+ }
+ },
+ {
+ "name": "Response code: 501",
+ "method": "GET",
+ "url": "http://{host}:{port}/",
+ "responseCode": 501,
+ "spanName": "/",
+ "spanStatus": "UNIMPLEMENTED",
+ "spanKind": "Client",
+ "spanAttributes": {
+ "http.path": "/",
+ "http.method": "GET",
+ "http.host": "{host}:{port}",
+ "http.status_code": "501",
+ "http.url": "http://{host}:{port}/"
+ }
+ },
+ {
+ "name": "Response code: 503",
+ "method": "GET",
+ "url": "http://{host}:{port}/",
+ "responseCode": 503,
+ "spanName": "/",
+ "spanStatus": "UNAVAILABLE",
+ "spanKind": "Client",
+ "spanAttributes": {
+ "http.path": "/",
+ "http.method": "GET",
+ "http.host": "{host}:{port}",
+ "http.status_code": "503",
+ "http.url": "http://{host}:{port}/"
+ }
+ },
+ {
+ "name": "Response code: 504",
+ "method": "GET",
+ "url": "http://{host}:{port}/",
+ "responseCode": 504,
+ "spanName": "/",
+ "spanStatus": "DEADLINE_EXCEEDED",
+ "spanKind": "Client",
+ "spanAttributes": {
+ "http.path": "/",
+ "http.method": "GET",
+ "http.host": "{host}:{port}",
+ "http.status_code": "504",
+ "http.url": "http://{host}:{port}/"
+ }
+ },
+ {
+ "name": "Response code: 600",
+ "method": "GET",
+ "url": "http://{host}:{port}/",
+ "responseCode": 600,
+ "spanName": "/",
+ "spanStatus": "UNKNOWN",
+ "spanKind": "Client",
+ "spanAttributes": {
+ "http.path": "/",
+ "http.method": "GET",
+ "http.host": "{host}:{port}",
+ "http.status_code": "600",
+ "http.url": "http://{host}:{port}/"
+ }
+ },
+ {
+ "name": "User agent attribute populated",
+ "method": "GET",
+ "url": "http://{host}:{port}/",
+ "headers": {
+ "User-Agent": "test-user-agent"
+ },
+ "responseCode": 200,
+ "spanName": "/",
+ "spanStatus": "OK",
+ "spanKind": "Client",
+ "spanAttributes": {
+ "http.path": "/",
+ "http.method": "GET",
+ "http.host": "{host}:{port}",
+ "http.status_code": "200",
+ "http.user_agent": "test-user-agent",
+ "http.url": "http://{host}:{port}/"
+ }
+ }
+ ]
\ No newline at end of file
diff --git a/plugin/ochttp/trace.go b/plugin/ochttp/trace.go
index fdf65fc..c23b97f 100644
--- a/plugin/ochttp/trace.go
+++ b/plugin/ochttp/trace.go
@@ -34,6 +34,7 @@
HostAttribute = "http.host"
MethodAttribute = "http.method"
PathAttribute = "http.path"
+ URLAttribute = "http.url"
UserAgentAttribute = "http.user_agent"
StatusCodeAttribute = "http.status_code"
)
@@ -150,12 +151,21 @@
}
func requestAttrs(r *http.Request) []trace.Attribute {
- return []trace.Attribute{
+ userAgent := r.UserAgent()
+
+ attrs := make([]trace.Attribute, 0, 5)
+ attrs = append(attrs,
trace.StringAttribute(PathAttribute, r.URL.Path),
+ trace.StringAttribute(URLAttribute, r.URL.String()),
trace.StringAttribute(HostAttribute, r.Host),
trace.StringAttribute(MethodAttribute, r.Method),
- trace.StringAttribute(UserAgentAttribute, r.UserAgent()),
+ )
+
+ if userAgent != "" {
+ attrs = append(attrs, trace.StringAttribute(UserAgentAttribute, userAgent))
}
+
+ return attrs
}
func responseAttrs(resp *http.Response) []trace.Attribute {
diff --git a/plugin/ochttp/trace_test.go b/plugin/ochttp/trace_test.go
index 33df4d7..13ef30c 100644
--- a/plugin/ochttp/trace_test.go
+++ b/plugin/ochttp/trace_test.go
@@ -18,13 +18,16 @@
"bytes"
"context"
"encoding/hex"
+ "encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
+ "net"
"net/http"
"net/http/httptest"
+ "net/url"
"reflect"
"strings"
"testing"
@@ -244,7 +247,7 @@
serverDone := make(chan struct{})
serverReturn := make(chan time.Time)
tt.handler.StartOptions.Sampler = trace.AlwaysSample()
- url := serveHTTP(tt.handler, serverDone, serverReturn)
+ url := serveHTTP(tt.handler, serverDone, serverReturn, 200)
ctx := context.Background()
// Make the request.
@@ -342,9 +345,9 @@
}
}
-func serveHTTP(handler *Handler, done chan struct{}, wait chan time.Time) string {
+func serveHTTP(handler *Handler, done chan struct{}, wait chan time.Time, statusCode int) string {
handler.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- w.WriteHeader(200)
+ w.WriteHeader(statusCode)
w.(http.Flusher).Flush()
// Simulate a slow-responding server.
@@ -467,12 +470,14 @@
},
wantAttrs: []trace.Attribute{
trace.StringAttribute("http.path", "/hello"),
+ trace.StringAttribute("http.url", "http://example.com:779/hello"),
trace.StringAttribute("http.host", "example.com:779"),
trace.StringAttribute("http.method", "GET"),
trace.StringAttribute("http.user_agent", "ua"),
},
},
}
+
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := tt.makeReq()
@@ -516,6 +521,144 @@
}
}
+type TestCase struct {
+ Name string
+ Method string
+ URL string
+ Headers map[string]string
+ ResponseCode int
+ SpanName string
+ SpanStatus string
+ SpanKind string
+ SpanAttributes map[string]string
+}
+
+func TestAgainstSpecs(t *testing.T) {
+
+ fmt.Println("start")
+
+ dat, err := ioutil.ReadFile("testdata/http-out-test-cases.json")
+ if err != nil {
+ t.Fatalf("error reading file: %v", err)
+ }
+
+ tests := make([]TestCase, 0)
+ err = json.Unmarshal(dat, &tests)
+ if err != nil {
+ t.Fatalf("error parsing json: %v", err)
+ }
+
+ trace.ApplyConfig(trace.Config{DefaultSampler: trace.AlwaysSample()})
+
+ for _, tt := range tests {
+ t.Run(tt.Name, func(t *testing.T) {
+ var spans collector
+ trace.RegisterExporter(&spans)
+ defer trace.UnregisterExporter(&spans)
+
+ handler := &Handler{}
+ transport := &Transport{}
+
+ serverDone := make(chan struct{})
+ serverReturn := make(chan time.Time)
+ host := ""
+ port := ""
+ serverRequired := strings.Contains(tt.URL, "{")
+ if serverRequired {
+ // Start the server.
+ localServerURL := serveHTTP(handler, serverDone, serverReturn, tt.ResponseCode)
+ u, _ := url.Parse(localServerURL)
+ host, port, _ = net.SplitHostPort(u.Host)
+
+ tt.URL = strings.Replace(tt.URL, "{host}", host, 1)
+ tt.URL = strings.Replace(tt.URL, "{port}", port, 1)
+ }
+
+ // Start a root Span in the client.
+ ctx, _ := trace.StartSpan(
+ context.Background(),
+ "top-level")
+ // Make the request.
+ req, err := http.NewRequest(
+ tt.Method,
+ tt.URL,
+ nil)
+ for headerName, headerValue := range tt.Headers {
+ req.Header.Add(headerName, headerValue)
+ }
+ if err != nil {
+ t.Fatal(err)
+ }
+ req = req.WithContext(ctx)
+ resp, err := transport.RoundTrip(req)
+ if err != nil {
+ // do not fail. We want to validate DNS issues
+ //t.Fatal(err)
+ }
+
+ if serverRequired {
+ // Tell the server to return from request handling.
+ serverReturn <- time.Now().Add(time.Millisecond)
+ }
+
+ if resp != nil {
+ // If it simply closes body without reading
+ // synchronization problem may happen for spans slice.
+ // Server span and client span will write themselves
+ // at the same time
+ ioutil.ReadAll(resp.Body)
+ resp.Body.Close()
+ if serverRequired {
+ <-serverDone
+ }
+ }
+ trace.UnregisterExporter(&spans)
+
+ var client *trace.SpanData
+ for _, sp := range spans {
+ if sp.SpanKind == trace.SpanKindClient {
+ client = sp
+ }
+ }
+
+ if client.Name != tt.SpanName {
+ t.Errorf("span names don't match: expected: %s, actual: %s", tt.SpanName, client.Name)
+ }
+
+ spanKindToStr := map[int]string{
+ trace.SpanKindClient: "Client",
+ trace.SpanKindServer: "Server",
+ }
+
+ if !strings.EqualFold(codeToStr[client.Status.Code], tt.SpanStatus) {
+ t.Errorf("span status don't match: expected: %s, actual: %d (%s)", tt.SpanStatus, client.Status.Code, codeToStr[client.Status.Code])
+ }
+
+ if !strings.EqualFold(spanKindToStr[client.SpanKind], tt.SpanKind) {
+ t.Errorf("span kind don't match: expected: %s, actual: %d (%s)", tt.SpanKind, client.SpanKind, spanKindToStr[client.SpanKind])
+ }
+
+ normalizedActualAttributes := map[string]string{}
+ for k, v := range client.Attributes {
+ normalizedActualAttributes[k] = fmt.Sprintf("%v", v)
+ }
+
+ normalizedExpectedAttributes := map[string]string{}
+ for k, v := range tt.SpanAttributes {
+ normalizedValue := v
+ normalizedValue = strings.Replace(normalizedValue, "{host}", host, 1)
+ normalizedValue = strings.Replace(normalizedValue, "{port}", port, 1)
+
+ normalizedExpectedAttributes[k] = normalizedValue
+ }
+
+ if got, want := normalizedActualAttributes, normalizedExpectedAttributes; !reflect.DeepEqual(got, want) {
+ t.Errorf("Request attributes = %#v; want %#v", got, want)
+ }
+ })
+ }
+}
+
func TestStatusUnitTest(t *testing.T) {
tests := []struct {
in int