From 7396103d6a6f92b9b0dda7164a8abd2bc6e9e5ef Mon Sep 17 00:00:00 2001 From: Sergey Kanzhelev Date: Thu, 21 Mar 2019 06:53:53 -0700 Subject: [PATCH] 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 --- plugin/ochttp/testdata/download-test-cases.sh | 5 + .../ochttp/testdata/http-out-test-cases.json | 274 ++++++++++++++++++ plugin/ochttp/trace.go | 14 +- plugin/ochttp/trace_test.go | 149 +++++++++- 4 files changed, 437 insertions(+), 5 deletions(-) create mode 100755 plugin/ochttp/testdata/download-test-cases.sh create mode 100644 plugin/ochttp/testdata/http-out-test-cases.json diff --git a/plugin/ochttp/testdata/download-test-cases.sh b/plugin/ochttp/testdata/download-test-cases.sh new file mode 100755 index 000000000..3baa11438 --- /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 000000000..039a73eb5 --- /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 fdf65fc80..c23b97fb1 100644 --- a/plugin/ochttp/trace.go +++ b/plugin/ochttp/trace.go @@ -34,6 +34,7 @@ const ( 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 spanNameFromURL(req *http.Request) string { } 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 33df4d730..13ef30cab 100644 --- a/plugin/ochttp/trace_test.go +++ b/plugin/ochttp/trace_test.go @@ -18,13 +18,16 @@ import ( "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 @@ func TestEndToEnd(t *testing.T) { 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 TestEndToEnd(t *testing.T) { } } -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 @@ func TestRequestAttributes(t *testing.T) { }, 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 @@ func TestResponseAttributes(t *testing.T) { } } +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