aboutsummaryrefslogtreecommitdiffhomepage
path: root/tools
diff options
context:
space:
mode:
Diffstat (limited to 'tools')
-rw-r--r--tools/buildgen/plugins/transitive_dependencies.py63
-rw-r--r--tools/doxygen/Doxyfile.c++2
-rw-r--r--tools/doxygen/Doxyfile.c++.internal2
-rw-r--r--tools/doxygen/Doxyfile.core2
-rw-r--r--tools/doxygen/Doxyfile.core.internal2
-rwxr-xr-xtools/gke/kubernetes_api.py216
-rw-r--r--tools/http2_interop/goaway.go72
-rw-r--r--tools/http2_interop/http2interop.go52
-rw-r--r--tools/http2_interop/http2interop_test.go46
-rw-r--r--tools/run_tests/report_utils.py168
-rwxr-xr-xtools/run_tests/run_interop_tests.py4
-rwxr-xr-xtools/run_tests/run_tests.py2
12 files changed, 472 insertions, 159 deletions
diff --git a/tools/buildgen/plugins/transitive_dependencies.py b/tools/buildgen/plugins/transitive_dependencies.py
new file mode 100644
index 0000000000..c2d3da3a3b
--- /dev/null
+++ b/tools/buildgen/plugins/transitive_dependencies.py
@@ -0,0 +1,63 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Buildgen transitive dependencies
+
+This takes the list of libs, node_modules, and targets from our
+yaml dictionary, and adds to each the transitive closure
+of the list of dependencies.
+
+"""
+
+def get_lib(libs, name):
+ return next(lib for lib in libs if lib['name']==name)
+
+def transitive_deps(lib, libs):
+ if 'deps' in lib:
+ # Recursively call transitive_deps on each dependency, and take the union
+ return set.union(set(lib['deps']),
+ *[set(transitive_deps(get_lib(libs, dep), libs))
+ for dep in lib['deps']])
+ else:
+ return set()
+
+def mako_plugin(dictionary):
+ """The exported plugin code for transitive_dependencies.
+
+ Each item in libs, node_modules, and targets can have a deps list.
+ We add a transitive_deps property to each with the transitive closure
+ of those dependency lists.
+ """
+ libs = dictionary.get('libs')
+ node_modules = dictionary.get('node_modules')
+ targets = dictionary.get('targets')
+
+ for target_list in (libs, node_modules, targets):
+ for target in target_list:
+ target['transitive_deps'] = transitive_deps(target, libs)
diff --git a/tools/doxygen/Doxyfile.c++ b/tools/doxygen/Doxyfile.c++
index 5d592c8e0a..f07718515a 100644
--- a/tools/doxygen/Doxyfile.c++
+++ b/tools/doxygen/Doxyfile.c++
@@ -40,7 +40,7 @@ PROJECT_NAME = "GRPC C++"
# could be handy for archiving the generated documentation or if some version
# control system is used.
-PROJECT_NUMBER = 0.11.0.0
+PROJECT_NUMBER = 0.12.0.0
# Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a
diff --git a/tools/doxygen/Doxyfile.c++.internal b/tools/doxygen/Doxyfile.c++.internal
index bbd1706fb0..11aaa379ce 100644
--- a/tools/doxygen/Doxyfile.c++.internal
+++ b/tools/doxygen/Doxyfile.c++.internal
@@ -40,7 +40,7 @@ PROJECT_NAME = "GRPC C++"
# could be handy for archiving the generated documentation or if some version
# control system is used.
-PROJECT_NUMBER = 0.11.0.0
+PROJECT_NUMBER = 0.12.0.0
# Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a
diff --git a/tools/doxygen/Doxyfile.core b/tools/doxygen/Doxyfile.core
index beb0128e41..e411abf300 100644
--- a/tools/doxygen/Doxyfile.core
+++ b/tools/doxygen/Doxyfile.core
@@ -40,7 +40,7 @@ PROJECT_NAME = "GRPC Core"
# could be handy for archiving the generated documentation or if some version
# control system is used.
-PROJECT_NUMBER = 0.11.0.0
+PROJECT_NUMBER = 0.12.0.0
# Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a
diff --git a/tools/doxygen/Doxyfile.core.internal b/tools/doxygen/Doxyfile.core.internal
index e3dc181511..fbabae4238 100644
--- a/tools/doxygen/Doxyfile.core.internal
+++ b/tools/doxygen/Doxyfile.core.internal
@@ -40,7 +40,7 @@ PROJECT_NAME = "GRPC Core"
# could be handy for archiving the generated documentation or if some version
# control system is used.
-PROJECT_NUMBER = 0.11.0.0
+PROJECT_NUMBER = 0.12.0.0
# Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a
diff --git a/tools/gke/kubernetes_api.py b/tools/gke/kubernetes_api.py
new file mode 100755
index 0000000000..7dd3015365
--- /dev/null
+++ b/tools/gke/kubernetes_api.py
@@ -0,0 +1,216 @@
+#!/usr/bin/env python2.7
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import requests
+import json
+
+_REQUEST_TIMEOUT_SECS = 10
+
+def _make_pod_config(pod_name, image_name, container_port_list, cmd_list,
+ arg_list):
+ """Creates a string containing the Pod defintion as required by the Kubernetes API"""
+ body = {
+ 'kind': 'Pod',
+ 'apiVersion': 'v1',
+ 'metadata': {
+ 'name': pod_name,
+ 'labels': {'name': pod_name}
+ },
+ 'spec': {
+ 'containers': [
+ {
+ 'name': pod_name,
+ 'image': image_name,
+ 'ports': []
+ }
+ ]
+ }
+ }
+ # Populate the 'ports' list
+ for port in container_port_list:
+ port_entry = {'containerPort': port, 'protocol': 'TCP'}
+ body['spec']['containers'][0]['ports'].append(port_entry)
+
+ # Add the 'Command' and 'Args' attributes if they are passed.
+ # Note:
+ # - 'Command' overrides the ENTRYPOINT in the Docker Image
+ # - 'Args' override the COMMAND in Docker image (yes, it is confusing!)
+ if len(cmd_list) > 0:
+ body['spec']['containers'][0]['command'] = cmd_list
+ if len(arg_list) > 0:
+ body['spec']['containers'][0]['args'] = arg_list
+ return json.dumps(body)
+
+
+def _make_service_config(service_name, pod_name, service_port_list,
+ container_port_list, is_headless):
+ """Creates a string containing the Service definition as required by the Kubernetes API.
+
+ NOTE:
+ This creates either a Headless Service or 'LoadBalancer' service depending on
+ the is_headless parameter. For Headless services, there is no 'type' attribute
+ and the 'clusterIP' attribute is set to 'None'. Also, if the service is
+ Headless, Kubernetes creates DNS entries for Pods - i.e creates DNS A-records
+ mapping the service's name to the Pods' IPs
+ """
+ if len(container_port_list) != len(service_port_list):
+ print(
+ 'ERROR: container_port_list and service_port_list must be of same size')
+ return ''
+ body = {
+ 'kind': 'Service',
+ 'apiVersion': 'v1',
+ 'metadata': {
+ 'name': service_name,
+ 'labels': {
+ 'name': service_name
+ }
+ },
+ 'spec': {
+ 'ports': [],
+ 'selector': {
+ 'name': pod_name
+ }
+ }
+ }
+ # Populate the 'ports' list in the 'spec' section. This maps service ports
+ # (port numbers that are exposed by Kubernetes) to container ports (i.e port
+ # numbers that are exposed by your Docker image)
+ for idx in range(len(container_port_list)):
+ port_entry = {
+ 'port': service_port_list[idx],
+ 'targetPort': container_port_list[idx],
+ 'protocol': 'TCP'
+ }
+ body['spec']['ports'].append(port_entry)
+
+ # Make this either a LoadBalancer service or a headless service depending on
+ # the is_headless parameter
+ if is_headless:
+ body['spec']['clusterIP'] = 'None'
+ else:
+ body['spec']['type'] = 'LoadBalancer'
+ return json.dumps(body)
+
+
+def _print_connection_error(msg):
+ print('ERROR: Connection failed. Did you remember to run Kubenetes proxy on '
+ 'localhost (i.e kubectl proxy --port=<proxy_port>) ?. Error: %s' % msg)
+
+def _do_post(post_url, api_name, request_body):
+ """Helper to do HTTP POST.
+
+ Note:
+ 1) On success, Kubernetes returns a success code of 201(CREATED) not 200(OK)
+ 2) A response code of 509(CONFLICT) is interpreted as a success code (since
+ the error is most likely due to the resource already existing). This makes
+ _do_post() idempotent which is semantically desirable.
+ """
+ is_success = True
+ try:
+ r = requests.post(post_url, data=request_body, timeout=_REQUEST_TIMEOUT_SECS)
+ if r.status_code == requests.codes.conflict:
+ print('WARN: Looks like the resource already exists. Api: %s, url: %s' %
+ (api_name, post_url))
+ elif r.status_code != requests.codes.created:
+ print('ERROR: %s API returned error. HTTP response: (%d) %s' %
+ (api_name, r.status_code, r.text))
+ is_success = False
+ except(requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e:
+ is_success = False
+ _print_connection_error(str(e))
+ return is_success
+
+
+def _do_delete(del_url, api_name):
+ """Helper to do HTTP DELETE.
+
+ Note: A response code of 404(NOT_FOUND) is treated as success to keep
+ _do_delete() idempotent.
+ """
+ is_success = True
+ try:
+ r = requests.delete(del_url, timeout=_REQUEST_TIMEOUT_SECS)
+ if r.status_code == requests.codes.not_found:
+ print('WARN: The resource does not exist. Api: %s, url: %s' %
+ (api_name, del_url))
+ elif r.status_code != requests.codes.ok:
+ print('ERROR: %s API returned error. HTTP response: %s' %
+ (api_name, r.text))
+ is_success = False
+ except(requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e:
+ is_success = False
+ _print_connection_error(str(e))
+ return is_success
+
+
+def create_service(kube_host, kube_port, namespace, service_name, pod_name,
+ service_port_list, container_port_list, is_headless):
+ """Creates either a Headless Service or a LoadBalancer Service depending
+ on the is_headless parameter.
+ """
+ post_url = 'http://%s:%d/api/v1/namespaces/%s/services' % (
+ kube_host, kube_port, namespace)
+ request_body = _make_service_config(service_name, pod_name, service_port_list,
+ container_port_list, is_headless)
+ return _do_post(post_url, 'Create Service', request_body)
+
+
+def create_pod(kube_host, kube_port, namespace, pod_name, image_name,
+ container_port_list, cmd_list, arg_list):
+ """Creates a Kubernetes Pod.
+
+ Note that it is generally NOT considered a good practice to directly create
+ Pods. Typically, the recommendation is to create 'Controllers' to create and
+ manage Pods' lifecycle. Currently Kubernetes only supports 'Replication
+ Controller' which creates a configurable number of 'identical Replicas' of
+ Pods and automatically restarts any Pods in case of failures (for eg: Machine
+ failures in Kubernetes). This makes it less flexible for our test use cases
+ where we might want slightly different set of args to each Pod. Hence we
+ directly create Pods and not care much about Kubernetes failures since those
+ are very rare.
+ """
+ post_url = 'http://%s:%d/api/v1/namespaces/%s/pods' % (kube_host, kube_port,
+ namespace)
+ request_body = _make_pod_config(pod_name, image_name, container_port_list,
+ cmd_list, arg_list)
+ return _do_post(post_url, 'Create Pod', request_body)
+
+
+def delete_service(kube_host, kube_port, namespace, service_name):
+ del_url = 'http://%s:%d/api/v1/namespaces/%s/services/%s' % (
+ kube_host, kube_port, namespace, service_name)
+ return _do_delete(del_url, 'Delete Service')
+
+
+def delete_pod(kube_host, kube_port, namespace, pod_name):
+ del_url = 'http://%s:%d/api/v1/namespaces/%s/pods/%s' % (kube_host, kube_port,
+ namespace, pod_name)
+ return _do_delete(del_url, 'Delete Pod')
diff --git a/tools/http2_interop/goaway.go b/tools/http2_interop/goaway.go
new file mode 100644
index 0000000000..289442d615
--- /dev/null
+++ b/tools/http2_interop/goaway.go
@@ -0,0 +1,72 @@
+package http2interop
+
+import (
+ "encoding/binary"
+ "fmt"
+ "io"
+)
+
+type GoAwayFrame struct {
+ Header FrameHeader
+ Reserved
+ StreamID
+ // TODO(carl-mastrangelo): make an enum out of this.
+ Code uint32
+ Data []byte
+}
+
+func (f *GoAwayFrame) GetHeader() *FrameHeader {
+ return &f.Header
+}
+
+func (f *GoAwayFrame) ParsePayload(r io.Reader) error {
+ raw := make([]byte, f.Header.Length)
+ if _, err := io.ReadFull(r, raw); err != nil {
+ return err
+ }
+ return f.UnmarshalPayload(raw)
+}
+
+func (f *GoAwayFrame) UnmarshalPayload(raw []byte) error {
+ if f.Header.Length != len(raw) {
+ return fmt.Errorf("Invalid Payload length %d != %d", f.Header.Length, len(raw))
+ }
+ if f.Header.Length < 8 {
+ return fmt.Errorf("Invalid Payload length %d", f.Header.Length)
+ }
+ *f = GoAwayFrame{
+ Reserved: Reserved(raw[0]>>7 == 1),
+ StreamID: StreamID(binary.BigEndian.Uint32(raw[0:4]) & 0x7fffffff),
+ Code: binary.BigEndian.Uint32(raw[4:8]),
+ Data: []byte(string(raw[8:])),
+ }
+
+ return nil
+}
+
+func (f *GoAwayFrame) MarshalPayload() ([]byte, error) {
+ raw := make([]byte, 8, 8+len(f.Data))
+ binary.BigEndian.PutUint32(raw[:4], uint32(f.StreamID))
+ binary.BigEndian.PutUint32(raw[4:8], f.Code)
+ raw = append(raw, f.Data...)
+
+ return raw, nil
+}
+
+func (f *GoAwayFrame) MarshalBinary() ([]byte, error) {
+ payload, err := f.MarshalPayload()
+ if err != nil {
+ return nil, err
+ }
+
+ f.Header.Length = len(payload)
+ f.Header.Type = GoAwayFrameType
+ header, err := f.Header.MarshalBinary()
+ if err != nil {
+ return nil, err
+ }
+
+ header = append(header, payload...)
+
+ return header, nil
+}
diff --git a/tools/http2_interop/http2interop.go b/tools/http2_interop/http2interop.go
index 8585a044e5..bef8b0b656 100644
--- a/tools/http2_interop/http2interop.go
+++ b/tools/http2_interop/http2interop.go
@@ -252,6 +252,58 @@ func testTLSApplicationProtocol(ctx *HTTP2InteropCtx) error {
return nil
}
+func testTLSBadCipherSuites(ctx *HTTP2InteropCtx) error {
+ config := buildTlsConfig(ctx)
+ // These are the suites that Go supports, but are forbidden by http2.
+ config.CipherSuites = []uint16{
+ tls.TLS_RSA_WITH_RC4_128_SHA,
+ tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
+ tls.TLS_RSA_WITH_AES_128_CBC_SHA,
+ tls.TLS_RSA_WITH_AES_256_CBC_SHA,
+ tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA,
+ tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
+ tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
+ tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA,
+ tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
+ tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
+ tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
+ }
+ conn, err := connectWithTls(ctx, config)
+ if err != nil {
+ return err
+ }
+ defer conn.Close()
+ conn.SetDeadline(time.Now().Add(defaultTimeout))
+
+ if err := http2Connect(conn, nil); err != nil {
+ return err
+ }
+
+ for {
+ f, err := parseFrame(conn)
+ if err != nil {
+ return err
+ }
+ if gf, ok := f.(*GoAwayFrame); ok {
+ return fmt.Errorf("Got goaway frame %d", gf.Code)
+ }
+ }
+ return nil
+}
+
+func http2Connect(c net.Conn, sf *SettingsFrame) error {
+ if _, err := c.Write([]byte(Preface)); err != nil {
+ return err
+ }
+ if sf == nil {
+ sf = &SettingsFrame{}
+ }
+ if err := streamFrame(c, sf); err != nil {
+ return err
+ }
+ return nil
+}
+
func connect(ctx *HTTP2InteropCtx) (net.Conn, error) {
var conn net.Conn
var err error
diff --git a/tools/http2_interop/http2interop_test.go b/tools/http2_interop/http2interop_test.go
index dc2960048f..8fd838422b 100644
--- a/tools/http2_interop/http2interop_test.go
+++ b/tools/http2_interop/http2interop_test.go
@@ -3,13 +3,13 @@ package http2interop
import (
"crypto/tls"
"crypto/x509"
- "strings"
"flag"
"fmt"
"io"
"io/ioutil"
"os"
"strconv"
+ "strings"
"testing"
)
@@ -17,8 +17,7 @@ var (
serverHost = flag.String("server_host", "", "The host to test")
serverPort = flag.Int("server_port", 443, "The port to test")
useTls = flag.Bool("use_tls", true, "Should TLS tests be run")
- // TODO: implement
- testCase = flag.String("test_case", "", "What test cases to run")
+ testCase = flag.String("test_case", "", "What test cases to run")
// The rest of these are unused, but present to fulfill the client interface
serverHostOverride = flag.String("server_host_override", "", "Unused")
@@ -86,33 +85,50 @@ func TestUnknownFrameType(t *testing.T) {
}
func TestTLSApplicationProtocol(t *testing.T) {
+ if *testCase != "tls" {
+ return
+ }
ctx := InteropCtx(t)
- err := testTLSApplicationProtocol(ctx);
+ err := testTLSApplicationProtocol(ctx)
matchError(t, err, "EOF")
}
func TestTLSMaxVersion(t *testing.T) {
+ if *testCase != "tls" {
+ return
+ }
ctx := InteropCtx(t)
- err := testTLSMaxVersion(ctx, tls.VersionTLS11);
+ err := testTLSMaxVersion(ctx, tls.VersionTLS11)
+ // TODO(carl-mastrangelo): maybe this should be some other error. If the server picks
+ // the wrong protocol version, thats bad too.
matchError(t, err, "EOF", "server selected unsupported protocol")
}
+func TestTLSBadCipherSuites(t *testing.T) {
+ if *testCase != "tls" {
+ return
+ }
+ ctx := InteropCtx(t)
+ err := testTLSBadCipherSuites(ctx)
+ matchError(t, err, "EOF", "Got goaway frame")
+}
+
func TestClientPrefaceWithStreamId(t *testing.T) {
ctx := InteropCtx(t)
err := testClientPrefaceWithStreamId(ctx)
matchError(t, err, "EOF")
}
-func matchError(t *testing.T, err error, matches ... string) {
- if err == nil {
- t.Fatal("Expected an error")
- }
- for _, s := range matches {
- if strings.Contains(err.Error(), s) {
- return
- }
- }
- t.Fatalf("Error %v not in %+v", err, matches)
+func matchError(t *testing.T, err error, matches ...string) {
+ if err == nil {
+ t.Fatal("Expected an error")
+ }
+ for _, s := range matches {
+ if strings.Contains(err.Error(), s) {
+ return
+ }
+ }
+ t.Fatalf("Error %v not in %+v", err, matches)
}
func TestMain(m *testing.M) {
diff --git a/tools/run_tests/report_utils.py b/tools/run_tests/report_utils.py
index bb9eca4254..adeb707a07 100644
--- a/tools/run_tests/report_utils.py
+++ b/tools/run_tests/report_utils.py
@@ -29,6 +29,11 @@
"""Generate XML and HTML test reports."""
+try:
+ from mako.runtime import Context
+ from mako.template import Template
+except (ImportError):
+ pass # Mako not installed but it is ok.
import os
import string
import xml.etree.cElementTree as ET
@@ -49,7 +54,7 @@ def _filter_msg(msg, output_format):
return msg
-def render_xml_report(resultset, xml_report):
+def render_junit_xml_report(resultset, xml_report):
"""Generate JUnit-like XML report."""
root = ET.Element('testsuites')
testsuite = ET.SubElement(root, 'testsuite', id='1', package='grpc',
@@ -69,147 +74,36 @@ def render_xml_report(resultset, xml_report):
tree.write(xml_report, encoding='UTF-8')
-# TODO(adelez): Use mako template.
-def fill_one_test_result(shortname, resultset, html_str):
- if shortname in resultset:
- # Because interop tests does not have runs_per_test flag, each test is run
- # once. So there should only be one element for each result.
- result = resultset[shortname][0]
- if result.state == 'PASSED':
- html_str = '%s<td bgcolor=\"green\">PASS</td>\n' % html_str
- else:
- tooltip = ''
- if result.returncode > 0 or result.message:
- if result.returncode > 0:
- tooltip = 'returncode: %d ' % result.returncode
- if result.message:
- escaped_msg = _filter_msg(result.message, 'HTML')
- tooltip = '%smessage: %s' % (tooltip, escaped_msg)
- if result.state == 'FAILED':
- html_str = '%s<td bgcolor=\"red\">' % html_str
- if tooltip:
- html_str = ('%s<a href=\"#\" data-toggle=\"tooltip\" '
- 'data-placement=\"auto\" title=\"%s\">FAIL</a></td>\n' %
- (html_str, tooltip))
- else:
- html_str = '%sFAIL</td>\n' % html_str
- elif result.state == 'TIMEOUT':
- html_str = '%s<td bgcolor=\"yellow\">' % html_str
- if tooltip:
- html_str = ('%s<a href=\"#\" data-toggle=\"tooltip\" '
- 'data-placement=\"auto\" title=\"%s\">TIMEOUT</a></td>\n'
- % (html_str, tooltip))
- else:
- html_str = '%sTIMEOUT</td>\n' % html_str
- else:
- html_str = '%s<td bgcolor=\"magenta\">Not implemented</td>\n' % html_str
-
- return html_str
-
+def render_interop_html_report(
+ client_langs, server_langs, test_cases, auth_test_cases, http2_cases,
+ resultset, num_failures, cloud_to_prod, http2_interop):
+ """Generate HTML report for interop tests."""
+ html_report_dir = 'reports'
+ template_file = os.path.join(html_report_dir, 'interop_html_report.template')
+ try:
+ mytemplate = Template(filename=template_file, format_exceptions=True)
+ except NameError:
+ print 'Mako template is not installed. Skipping HTML report generation.'
+ return
+ except IOError as e:
+ print 'Failed to find the template %s: %s' % (template_file, e)
+ return
-def render_html_report(client_langs, server_langs, test_cases, auth_test_cases,
- http2_cases, resultset, num_failures, cloud_to_prod,
- http2_interop):
- """Generate html report."""
sorted_test_cases = sorted(test_cases)
sorted_auth_test_cases = sorted(auth_test_cases)
sorted_http2_cases = sorted(http2_cases)
sorted_client_langs = sorted(client_langs)
sorted_server_langs = sorted(server_langs)
- html_str = ('<!DOCTYPE html>\n'
- '<html lang=\"en\">\n'
- '<head><title>Interop Test Result</title></head>\n'
- '<body>\n')
- if num_failures > 1:
- html_str = (
- '%s<p><h2><font color=\"red\">%d tests failed!</font></h2></p>\n' %
- (html_str, num_failures))
- elif num_failures:
- html_str = (
- '%s<p><h2><font color=\"red\">%d test failed!</font></h2></p>\n' %
- (html_str, num_failures))
- else:
- html_str = (
- '%s<p><h2><font color=\"green\">All tests passed!</font></h2></p>\n' %
- html_str)
- if cloud_to_prod:
- # Each column header is the client language.
- html_str = ('%s<h2>Cloud to Prod</h2>\n'
- '<table style=\"width:100%%\" border=\"1\">\n'
- '<tr bgcolor=\"#00BFFF\">\n'
- '<th>Client languages &#9658;</th>\n') % html_str
- for client_lang in sorted_client_langs:
- html_str = '%s<th>%s\n' % (html_str, client_lang)
- html_str = '%s</tr>\n' % html_str
- for test_case in sorted_test_cases + sorted_auth_test_cases:
- html_str = '%s<tr><td><b>%s</b></td>\n' % (html_str, test_case)
- for client_lang in sorted_client_langs:
- if not test_case in sorted_auth_test_cases:
- shortname = 'cloud_to_prod:%s:%s' % (client_lang, test_case)
- else:
- shortname = 'cloud_to_prod_auth:%s:%s' % (client_lang, test_case)
- html_str = fill_one_test_result(shortname, resultset, html_str)
- html_str = '%s</tr>\n' % html_str
- html_str = '%s</table>\n' % html_str
- if http2_interop:
- # Each column header is the server language.
- html_str = ('%s<h2>HTTP/2 Interop</h2>\n'
- '<table style=\"width:100%%\" border=\"1\">\n'
- '<tr bgcolor=\"#00BFFF\">\n'
- '<th>Servers &#9658;<br/>'
- 'Test Cases &#9660;</th>\n') % html_str
- for server_lang in sorted_server_langs:
- html_str = '%s<th>%s\n' % (html_str, server_lang)
- if cloud_to_prod:
- html_str = '%s<th>%s\n' % (html_str, "prod")
- html_str = '%s</tr>\n' % html_str
- for test_case in sorted_http2_cases:
- html_str = '%s<tr><td><b>%s</b></td>\n' % (html_str, test_case)
- # Fill up the cells with test result.
- for server_lang in sorted_server_langs:
- shortname = 'cloud_to_cloud:%s:%s_server:%s' % (
- "http2", server_lang, test_case)
- html_str = fill_one_test_result(shortname, resultset, html_str)
- if cloud_to_prod:
- shortname = 'cloud_to_prod:%s:%s' % ("http2", test_case)
- html_str = fill_one_test_result(shortname, resultset, html_str)
- html_str = '%s</tr>\n' % html_str
- html_str = '%s</table>\n' % html_str
- if server_langs:
- for test_case in sorted_test_cases:
- # Each column header is the client language.
- html_str = ('%s<h2>%s</h2>\n'
- '<table style=\"width:100%%\" border=\"1\">\n'
- '<tr bgcolor=\"#00BFFF\">\n'
- '<th>Client languages &#9658;<br/>'
- 'Server languages &#9660;</th>\n') % (html_str, test_case)
- for client_lang in sorted_client_langs:
- html_str = '%s<th>%s\n' % (html_str, client_lang)
- html_str = '%s</tr>\n' % html_str
- # Each row head is the server language.
- for server_lang in sorted_server_langs:
- html_str = '%s<tr><td><b>%s</b></td>\n' % (html_str, server_lang)
- # Fill up the cells with test result.
- for client_lang in sorted_client_langs:
- shortname = 'cloud_to_cloud:%s:%s_server:%s' % (
- client_lang, server_lang, test_case)
- html_str = fill_one_test_result(shortname, resultset, html_str)
- html_str = '%s</tr>\n' % html_str
- html_str = '%s</table>\n' % html_str
- html_str = ('%s\n'
- '<script>\n'
- '$(document).ready(function(){'
- '$(\'[data-toggle=\"tooltip\"]\').tooltip();\n'
- '});\n'
- '</script>\n'
- '</body>\n'
- '</html>') % html_str
-
- # Write to reports/index.html as set up in Jenkins plugin.
- html_report_dir = 'reports'
- if not os.path.exists(html_report_dir):
- os.mkdir(html_report_dir)
+ args = {'client_langs': sorted_client_langs,
+ 'server_langs': sorted_server_langs,
+ 'test_cases': sorted_test_cases,
+ 'auth_test_cases': sorted_auth_test_cases,
+ 'http2_cases': sorted_http2_cases,
+ 'resultset': resultset,
+ 'num_failures': num_failures,
+ 'cloud_to_prod': cloud_to_prod,
+ 'http2_interop': http2_interop}
html_file_path = os.path.join(html_report_dir, 'index.html')
- with open(html_file_path, 'w') as f:
- f.write(html_str)
+ with open(html_file_path, 'w') as output_file:
+ mytemplate.render_context(Context(output_file, **args))
diff --git a/tools/run_tests/run_interop_tests.py b/tools/run_tests/run_interop_tests.py
index 2634164a21..ee3cddddd9 100755
--- a/tools/run_tests/run_interop_tests.py
+++ b/tools/run_tests/run_interop_tests.py
@@ -686,9 +686,9 @@ try:
else:
jobset.message('SUCCESS', 'All tests passed', do_newline=True)
- report_utils.render_xml_report(resultset, 'report.xml')
+ report_utils.render_junit_xml_report(resultset, 'report.xml')
- report_utils.render_html_report(
+ report_utils.render_interop_html_report(
set([str(l) for l in languages]), servers, _TEST_CASES, _AUTH_TEST_CASES,
_HTTP2_TEST_CASES, resultset, num_failures,
args.cloud_to_prod_auth or args.cloud_to_prod, args.http2_interop)
diff --git a/tools/run_tests/run_tests.py b/tools/run_tests/run_tests.py
index ab2b71b80e..aa43337263 100755
--- a/tools/run_tests/run_tests.py
+++ b/tools/run_tests/run_tests.py
@@ -902,7 +902,7 @@ def _build_and_run(
for antagonist in antagonists:
antagonist.kill()
if xml_report and resultset:
- report_utils.render_xml_report(resultset, xml_report)
+ report_utils.render_junit_xml_report(resultset, xml_report)
number_failures, _ = jobset.run(
post_tests_steps, maxjobs=1, stop_on_failure=True,