From 5e0d34a7b79f8f8d5d5f5459d4a42e398d2cc6d7 Mon Sep 17 00:00:00 2001 From: murgatroid99 Date: Thu, 8 Mar 2018 16:42:01 -0800 Subject: [PATCH] Make interop tests use new proto loader, run them with pure js client --- packages/grpc-js-core/src/channel.ts | 11 ++- packages/grpc-js-core/src/make-client.ts | 2 +- packages/grpc-native-core/index.js | 2 +- packages/grpc-protobufjs/package.json | 3 +- packages/grpc-protobufjs/src/index.ts | 57 +++++++++----- test/any_grpc.js | 26 ++----- test/api/interop_sanity_test.js | 94 ++++++++++++++++++++++++ test/fixtures/native_js.js | 4 +- test/gulpfile.js | 27 ++++++- test/interop/interop_client.js | 14 +++- test/interop/interop_server.js | 14 +++- 11 files changed, 201 insertions(+), 53 deletions(-) create mode 100644 test/api/interop_sanity_test.js diff --git a/packages/grpc-js-core/src/channel.ts b/packages/grpc-js-core/src/channel.ts index 193ae9a5..8fcce521 100644 --- a/packages/grpc-js-core/src/channel.ts +++ b/packages/grpc-js-core/src/channel.ts @@ -158,6 +158,7 @@ export class Http2Channel extends EventEmitter implements Channel { connectionOptions.checkServerIdentity = (host: string, cert: PeerCertificate): Error | undefined => { return checkServerIdentity(sslTargetNameOverride, cert); } + connectionOptions.servername = sslTargetNameOverride; } subChannel = http2.connect(this.authority, connectionOptions); } @@ -224,7 +225,14 @@ export class Http2Channel extends EventEmitter implements Channel { Promise.all([finalMetadata, this.connect()]) .then(([metadataValue]) => { let headers = metadataValue.toHttp2Headers(); - headers[HTTP2_HEADER_AUTHORITY] = this.authority.hostname; + let host: string; + // TODO(murgatroid99): Add more centralized handling of channel options + if (this.options['grpc.default_authority']) { + host = this.options['grpc.default_authority'] as string; + } else { + host = this.authority.hostname; + } + headers[HTTP2_HEADER_AUTHORITY] = host; headers[HTTP2_HEADER_USER_AGENT] = this.userAgent; headers[HTTP2_HEADER_CONTENT_TYPE] = 'application/grpc'; headers[HTTP2_HEADER_METHOD] = 'POST'; @@ -234,6 +242,7 @@ export class Http2Channel extends EventEmitter implements Channel { if (this.connectivityState === ConnectivityState.READY) { const session: http2.ClientHttp2Session = this.subChannel!; // Prevent the HTTP/2 session from keeping the process alive. + // Note: this function is only available in Node 9 session.unref(); stream.attachHttp2Stream(session.request(headers)); } else { diff --git a/packages/grpc-js-core/src/make-client.ts b/packages/grpc-js-core/src/make-client.ts index ea176a5f..bc3299e8 100644 --- a/packages/grpc-js-core/src/make-client.ts +++ b/packages/grpc-js-core/src/make-client.ts @@ -137,7 +137,7 @@ export function loadPackageDefinition(packageDef: PackageDefinition) { const nameComponents = serviceFqn.split('.'); const serviceName = nameComponents[nameComponents.length-1]; let current = result; - for (const packageName in nameComponents.slice(0, -1)) { + for (const packageName of nameComponents.slice(0, -1)) { if (!current[packageName]) { current[packageName] = {}; } diff --git a/packages/grpc-native-core/index.js b/packages/grpc-native-core/index.js index 4e4289fa..30a6ff3c 100644 --- a/packages/grpc-native-core/index.js +++ b/packages/grpc-native-core/index.js @@ -158,7 +158,7 @@ exports.loadPackageDefinition = function loadPackageDefintion(packageDef) { const nameComponents = serviceFqn.split('.'); const serviceName = nameComponents[nameComponents.length-1]; let current = result; - for (const packageName in nameComponents.slice(0, -1)) { + for (const packageName of nameComponents.slice(0, -1)) { if (!current[packageName]) { current[packageName] = {}; } diff --git a/packages/grpc-protobufjs/package.json b/packages/grpc-protobufjs/package.json index a6fecc1b..bcf53e5f 100644 --- a/packages/grpc-protobufjs/package.json +++ b/packages/grpc-protobufjs/package.json @@ -30,10 +30,11 @@ "build/src/*.js" ], "dependencies": { + "@types/lodash": "^4.14.104", "@types/node": "^9.4.6", "clang-format": "^1.2.2", "gts": "^0.5.3", - "lodash": "^4.17.4", + "lodash": "^4.17.5", "protobufjs": "^6.8.5", "typescript": "~2.7.2" } diff --git a/packages/grpc-protobufjs/src/index.ts b/packages/grpc-protobufjs/src/index.ts index 22083f98..85c2c941 100644 --- a/packages/grpc-protobufjs/src/index.ts +++ b/packages/grpc-protobufjs/src/index.ts @@ -18,6 +18,7 @@ import * as Protobuf from 'protobufjs'; import * as fs from 'fs'; import * as path from 'path'; +import * as _ from 'lodash'; export interface Serialize { (value: T): Buffer; @@ -35,6 +36,7 @@ export interface MethodDefinition { responseSerialize: Serialize; requestDeserialize: Deserialize; responseDeserialize: Deserialize; + originalName?: string; } export interface ServiceDefinition { @@ -88,12 +90,14 @@ function createSerializer(cls: Protobuf.Type): Serialize { function createMethodDefinition(method: Protobuf.Method, serviceName: string, options: Options): MethodDefinition { return { path: '/' + serviceName + '/' + method.name, - requestStream: !!method.requestStream, - responseStream: !!method.responseStream, - requestSerialize: createSerializer(method.resolvedRequestType as Protobuf.Type), - requestDeserialize: createDeserializer(method.resolvedRequestType as Protobuf.Type, options), - responseSerialize: createSerializer(method.resolvedResponseType as Protobuf.Type), - responseDeserialize: createDeserializer(method.resolvedResponseType as Protobuf.Type, options) + requestStream: !!method.requestStream, + responseStream: !!method.responseStream, + requestSerialize: createSerializer(method.resolvedRequestType as Protobuf.Type), + requestDeserialize: createDeserializer(method.resolvedRequestType as Protobuf.Type, options), + responseSerialize: createSerializer(method.resolvedResponseType as Protobuf.Type), + responseDeserialize: createDeserializer(method.resolvedResponseType as Protobuf.Type, options), + // TODO(murgatroid99): Find a better way to handle this + originalName: _.camelCase(method.name) }; } @@ -113,6 +117,21 @@ function createPackageDefinition(root: Protobuf.Root, options: Options): Package return def; } +function addIncludePathResolver(root: Protobuf.Root, includePaths: string[]) { + root.resolvePath = (origin: string, target: string) => { + for (const directory of includePaths) { + const fullPath: string = path.join(directory, target); + try { + fs.accessSync(fullPath, fs.constants.R_OK); + return fullPath; + } catch (err) { + continue; + } + } + return null; + }; +} + /** * Load a .proto file with the specified options. * @param filename The file path to load. Can be an absolute path or relative to @@ -143,21 +162,23 @@ export function load(filename: string, options: Options): Promise { - for (const directory of options.include as string[]) { - const fullPath: string = path.join(directory, target); - try { - fs.accessSync(fullPath, fs.constants.R_OK); - return fullPath; - } catch (err) { - continue; - } - } - return null; - }; + addIncludePathResolver(root, options.include as string[]); } return root.load(filename, options).then((loadedRoot) => { loadedRoot.resolveAll(); return createPackageDefinition(root, options); }); } + +export function loadSync(filename: string, options: Options): PackageDefinition { + const root: Protobuf.Root = new Protobuf.Root(); + if (!!options.include) { + if (!(options.include instanceof Array)) { + throw new Error('The include option must be an array'); + } + addIncludePathResolver(root, options.include as string[]); + } + const loadedRoot = root.loadSync(filename, options); + loadedRoot.resolveAll(); + return createPackageDefinition(root, options); +} diff --git a/test/any_grpc.js b/test/any_grpc.js index d6d008c2..a434d41b 100644 --- a/test/any_grpc.js +++ b/test/any_grpc.js @@ -11,29 +11,15 @@ function getImplementation(globalField) { 'If running from the command line, please --require a fixture first.' ].join(' ')); } + console.error(globalField, global[globalField]); const impl = global[globalField]; - return { - surface: require(`../packages/grpc-${impl}`), - pjson: require(`../packages/grpc-${impl}/package.json`), - core: require(`../packages/grpc-${impl}-core`), - corePjson: require(`../packages/grpc-${impl}-core/package.json`) - }; + return require(`../packages/grpc-${impl}-core`); } const clientImpl = getImplementation('_client_implementation'); const serverImpl = getImplementation('_server_implementation'); -// We export a "merged" gRPC API by merging client and server specified -// APIs together. Any function that is unspecific to client/server defaults -// to client-side implementation. -// This object also has a test-only field from which details about the -// modules may be read. -module.exports = Object.assign({ - '$implementationInfo': { - client: clientImpl, - server: serverImpl - } -}, clientImpl.surface, _.pick(serverImpl.surface, [ - 'Server', - 'ServerCredentials' -])); +module.exports = { + client: clientImpl, + server: serverImpl +}; diff --git a/test/api/interop_sanity_test.js b/test/api/interop_sanity_test.js new file mode 100644 index 00000000..b3f65a03 --- /dev/null +++ b/test/api/interop_sanity_test.js @@ -0,0 +1,94 @@ +/* + * + * Copyright 2015 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +'use strict'; + +var interop_server = require('../interop/interop_server.js'); +var interop_client = require('../interop/interop_client.js'); + +var server; + +var port; + +var name_override = 'foo.test.google.fr'; + +describe('Interop tests', function() { + before(function(done) { + var server_obj = interop_server.getServer(0, true); + server = server_obj.server; + server.start(); + port = 'localhost:' + server_obj.port; + done(); + }); + after(function() { + server.forceShutdown(); + }); + // This depends on not using a binary stream + it('should pass empty_unary', function(done) { + interop_client.runTest(port, name_override, 'empty_unary', true, true, + done); + }); + // This fails due to an unknown bug + it('should pass large_unary', function(done) { + interop_client.runTest(port, name_override, 'large_unary', true, true, + done); + }); + it('should pass client_streaming', function(done) { + interop_client.runTest(port, name_override, 'client_streaming', true, true, + done); + }); + it('should pass server_streaming', function(done) { + interop_client.runTest(port, name_override, 'server_streaming', true, true, + done); + }); + it('should pass ping_pong', function(done) { + interop_client.runTest(port, name_override, 'ping_pong', true, true, done); + }); + it('should pass empty_stream', function(done) { + interop_client.runTest(port, name_override, 'empty_stream', true, true, + done); + }); + it('should pass cancel_after_begin', function(done) { + interop_client.runTest(port, name_override, 'cancel_after_begin', true, + true, done); + }); + it('should pass cancel_after_first_response', function(done) { + interop_client.runTest(port, name_override, 'cancel_after_first_response', + true, true, done); + }); + it('should pass timeout_on_sleeping_server', function(done) { + interop_client.runTest(port, name_override, 'timeout_on_sleeping_server', + true, true, done); + }); + it('should pass custom_metadata', function(done) { + interop_client.runTest(port, name_override, 'custom_metadata', + true, true, done); + }); + it('should pass status_code_and_message', function(done) { + interop_client.runTest(port, name_override, 'status_code_and_message', + true, true, done); + }); + it('should pass unimplemented_service', function(done) { + interop_client.runTest(port, name_override, 'unimplemented_service', + true, true, done); + }); + it('should pass unimplemented_method', function(done) { + interop_client.runTest(port, name_override, 'unimplemented_method', + true, true, done); + }); +}); diff --git a/test/fixtures/native_js.js b/test/fixtures/native_js.js index 5088df96..3da4106e 100644 --- a/test/fixtures/native_js.js +++ b/test/fixtures/native_js.js @@ -1,2 +1,2 @@ -global._server_implementation = 'js'; -global._client_implementation = 'native'; \ No newline at end of file +global._server_implementation = 'native'; +global._client_implementation = 'js'; diff --git a/test/gulpfile.js b/test/gulpfile.js index 41c4b33c..61ff1658 100644 --- a/test/gulpfile.js +++ b/test/gulpfile.js @@ -35,4 +35,29 @@ gulp.task('install', 'Install test dependencies', () => { gulp.task('clean.all', 'Delete all files created by tasks', () => {}); -gulp.task('test', 'Run API-level tests', () => {}); +gulp.task('test', 'Run API-level tests', () => { + // run mocha tests matching a glob with a pre-required fixture, + // returning the associated gulp stream + const apiTestGlob = `${apiTestDir}/*.js`; + const runTestsWithFixture = (server, client) => new Promise((resolve, reject) => { + const fixture = `${server}_${client}`; + console.log(`Running ${apiTestGlob} with ${server} server + ${client} client`); + gulp.src(apiTestGlob) + .pipe(mocha({ + reporter: 'mocha-jenkins-reporter', + require: `${testDir}/fixtures/${fixture}.js` + })) + .resume() // put the stream in flowing mode + .on('end', resolve) + .on('error', reject); + }); + const runTestsArgPairs = [ + ['native', 'native'], + ['native', 'js'], + // ['js', 'native'], + // ['js', 'js'] + ]; + return runTestsArgPairs.reduce((previousPromise, argPair) => { + return previousPromise.then(runTestsWithFixture.bind(null, argPair[0], argPair[1])); + }, Promise.resolve()); +}); diff --git a/test/interop/interop_client.js b/test/interop/interop_client.js index 83890bca..da3f6c33 100644 --- a/test/interop/interop_client.js +++ b/test/interop/interop_client.js @@ -20,12 +20,18 @@ var fs = require('fs'); var path = require('path'); -var grpc = require('../any_grpc')['$implementationInfo'].client.surface; -var testProto = grpc.load({ - root: __dirname + '/../../packages/grpc-native-core/deps/grpc', - file: 'src/proto/grpc/testing/test.proto'}).grpc.testing; +var grpc = require('../any_grpc').client; +var protoLoader = require('../../packages/grpc-protobufjs'); var GoogleAuth = require('google-auth-library'); +var protoPackage = protoLoader.loadSync( + 'src/proto/grpc/testing/test.proto', + {keepCase: true, + defaults: true, + enums: String, + include: [__dirname + '/../../packages/grpc-native-core/deps/grpc']}); +var testProto = grpc.loadPackageDefinition(protoPackage).grpc.testing; + var assert = require('assert'); var SERVICE_ACCOUNT_EMAIL; diff --git a/test/interop/interop_server.js b/test/interop/interop_server.js index fe1222dd..a0b80cc9 100644 --- a/test/interop/interop_server.js +++ b/test/interop/interop_server.js @@ -22,10 +22,16 @@ var fs = require('fs'); var path = require('path'); var _ = require('lodash'); var AsyncDelayQueue = require('./async_delay_queue'); -var grpc = require('../any_grpc')['$implementationInfo'].server.surface; -var testProto = grpc.load({ - root: __dirname + '/../../packages/grpc-native-core/deps/grpc', - file: 'src/proto/grpc/testing/test.proto'}).grpc.testing; +var grpc = require('../any_grpc').server; +// TODO(murgatroid99): do this import more cleanly +var protoLoader = require('../../packages/grpc-protobufjs'); +var protoPackage = protoLoader.loadSync( + 'src/proto/grpc/testing/test.proto', + {keepCase: true, + defaults: true, + enums: String, + include: [__dirname + '/../../packages/grpc-native-core/deps/grpc']}); +var testProto = grpc.loadPackageDefinition(protoPackage).grpc.testing; var ECHO_INITIAL_KEY = 'x-grpc-test-echo-initial'; var ECHO_TRAILING_KEY = 'x-grpc-test-echo-trailing-bin';