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