diff options
Diffstat (limited to 'tools')
-rw-r--r-- | tools/buildgen/plugins/transitive_dependencies.py | 63 | ||||
-rw-r--r-- | tools/doxygen/Doxyfile.c++ | 2 | ||||
-rw-r--r-- | tools/doxygen/Doxyfile.c++.internal | 2 | ||||
-rw-r--r-- | tools/doxygen/Doxyfile.core | 2 | ||||
-rw-r--r-- | tools/doxygen/Doxyfile.core.internal | 2 | ||||
-rwxr-xr-x | tools/gke/kubernetes_api.py | 216 | ||||
-rw-r--r-- | tools/http2_interop/goaway.go | 72 | ||||
-rw-r--r-- | tools/http2_interop/http2interop.go | 52 | ||||
-rw-r--r-- | tools/http2_interop/http2interop_test.go | 46 | ||||
-rw-r--r-- | tools/run_tests/report_utils.py | 168 | ||||
-rwxr-xr-x | tools/run_tests/run_interop_tests.py | 4 | ||||
-rwxr-xr-x | tools/run_tests/run_tests.py | 2 |
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 ►</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 ►<br/>' - 'Test Cases ▼</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 ►<br/>' - 'Server languages ▼</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, |