aboutsummaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/compiler/objective_c_generator.cc3
-rw-r--r--src/compiler/objective_c_plugin.cc5
-rw-r--r--src/cpp/client/channel_arguments.cc2
-rw-r--r--src/cpp/client/client_context.cc2
-rw-r--r--src/csharp/Grpc.Auth/GoogleCredential.cs16
-rw-r--r--src/csharp/Grpc.Core.Tests/Internal/MetadataArraySafeHandleTest.cs3
-rw-r--r--src/csharp/Grpc.IntegrationTesting/Grpc.IntegrationTesting.csproj32
-rw-r--r--src/csharp/Grpc.IntegrationTesting/InteropClient.cs53
-rw-r--r--src/node/README.md4
-rw-r--r--src/node/examples/math_server.js18
-rw-r--r--src/node/examples/route_guide_server.js16
-rw-r--r--src/node/examples/stock_server.js15
-rw-r--r--src/node/index.js6
-rw-r--r--src/node/interop/interop_server.js20
-rw-r--r--src/node/src/server.js255
-rw-r--r--src/node/test/health_test.js9
-rw-r--r--src/node/test/interop_sanity_test.js2
-rw-r--r--src/node/test/math_client_test.js2
-rw-r--r--src/node/test/surface_test.js286
-rw-r--r--src/objective-c/GRPCClient/GRPCCall.m32
-rw-r--r--src/objective-c/GRPCClient/private/GRPCCompletionQueue.m3
-rw-r--r--src/objective-c/GRPCClient/private/GRPCWrappedCall.m7
-rw-r--r--src/objective-c/RxLibrary/GRXConcurrentWriteable.h (renamed from src/objective-c/GRPCClient/private/GRPCDelegateWrapper.h)52
-rw-r--r--src/objective-c/RxLibrary/GRXConcurrentWriteable.m (renamed from src/objective-c/GRPCClient/private/GRPCDelegateWrapper.m)50
-rw-r--r--src/objective-c/RxLibrary/GRXImmediateWriter.m19
-rw-r--r--src/objective-c/RxLibrary/private/GRXNSFastEnumerator.m1
-rw-r--r--src/objective-c/generated_libraries/RemoteTestClient/RemoteTest.podspec8
-rw-r--r--src/objective-c/generated_libraries/RouteGuideClient/RouteGuide.podspec8
-rw-r--r--src/objective-c/tests/Tests.xcodeproj/project.pbxproj2
-rw-r--r--src/objective-c/tests/Tests.xcodeproj/xcshareddata/xcschemes/AllTests.xcscheme104
-rwxr-xr-xsrc/objective-c/tests/run_tests.sh49
31 files changed, 665 insertions, 419 deletions
diff --git a/src/compiler/objective_c_generator.cc b/src/compiler/objective_c_generator.cc
index 711d0d5870..69b3805bb1 100644
--- a/src/compiler/objective_c_generator.cc
+++ b/src/compiler/objective_c_generator.cc
@@ -186,9 +186,6 @@ string GetHeader(const ServiceDescriptor *service) {
grpc::protobuf::io::StringOutputStream output_stream(&output);
Printer printer(&output_stream, '$');
- printer.Print("@protocol GRXWriteable;\n");
- printer.Print("@protocol GRXWriter;\n\n");
-
map<string, string> vars = {{"service_class", ServiceClassName(service)}};
printer.Print(vars, "@protocol $service_class$ <NSObject>\n\n");
diff --git a/src/compiler/objective_c_plugin.cc b/src/compiler/objective_c_plugin.cc
index 2b5ab758fc..10f06ad4df 100644
--- a/src/compiler/objective_c_plugin.cc
+++ b/src/compiler/objective_c_plugin.cc
@@ -63,7 +63,9 @@ class ObjectiveCGrpcGenerator : public grpc::protobuf::compiler::CodeGenerator {
// Generate .pbrpc.h
string imports = string("#import \"") + file_name + ".pbobjc.h\"\n\n"
- "#import <ProtoRPC/ProtoService.h>\n";
+ "#import <ProtoRPC/ProtoService.h>\n"
+ "#import <RxLibrary/GRXWriteable.h>\n"
+ "#import <RxLibrary/GRXWriter.h>\n";
// TODO(jcanizales): Instead forward-declare the input and output types
// and import the files in the .pbrpc.m
@@ -89,7 +91,6 @@ class ObjectiveCGrpcGenerator : public grpc::protobuf::compiler::CodeGenerator {
string imports = string("#import \"") + file_name + ".pbrpc.h\"\n\n"
"#import <ProtoRPC/ProtoRPC.h>\n"
- "#import <RxLibrary/GRXWriteable.h>\n"
"#import <RxLibrary/GRXWriter+Immediate.h>\n";
string definitions;
diff --git a/src/cpp/client/channel_arguments.cc b/src/cpp/client/channel_arguments.cc
index 3a151c70c8..da6602e7af 100644
--- a/src/cpp/client/channel_arguments.cc
+++ b/src/cpp/client/channel_arguments.cc
@@ -75,7 +75,7 @@ void ChannelArguments::Swap(ChannelArguments& other) {
strings_.swap(other.strings_);
}
-void ChannelArguments::_Experimental_SetCompressionAlgorithm(
+void ChannelArguments::SetCompressionAlgorithm(
grpc_compression_algorithm algorithm) {
SetInt(GRPC_COMPRESSION_ALGORITHM_ARG, algorithm);
}
diff --git a/src/cpp/client/client_context.cc b/src/cpp/client/client_context.cc
index 69216d2030..14ab772e50 100644
--- a/src/cpp/client/client_context.cc
+++ b/src/cpp/client/client_context.cc
@@ -79,7 +79,7 @@ void ClientContext::set_call(grpc_call* call,
}
}
-void ClientContext::_experimental_set_compression_algorithm(
+void ClientContext::set_compression_algorithm(
grpc_compression_algorithm algorithm) {
char* algorithm_name = NULL;
if (!grpc_compression_algorithm_name(algorithm, &algorithm_name)) {
diff --git a/src/csharp/Grpc.Auth/GoogleCredential.cs b/src/csharp/Grpc.Auth/GoogleCredential.cs
index 8d5e543a21..7edf19ed67 100644
--- a/src/csharp/Grpc.Auth/GoogleCredential.cs
+++ b/src/csharp/Grpc.Auth/GoogleCredential.cs
@@ -35,8 +35,11 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
+using System.Threading;
+using System.Threading.Tasks;
using Google.Apis.Auth.OAuth2;
+using Google.Apis.Auth.OAuth2.Responses;
using Newtonsoft.Json.Linq;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Security;
@@ -100,6 +103,19 @@ namespace Grpc.Auth
return new GoogleCredential(serviceCredential);
}
+ public Task<bool> RequestAccessTokenAsync(CancellationToken taskCancellationToken)
+ {
+ return credential.RequestAccessTokenAsync(taskCancellationToken);
+ }
+
+ public TokenResponse Token
+ {
+ get
+ {
+ return credential.Token;
+ }
+ }
+
internal ServiceCredential InternalCredential
{
get
diff --git a/src/csharp/Grpc.Core.Tests/Internal/MetadataArraySafeHandleTest.cs b/src/csharp/Grpc.Core.Tests/Internal/MetadataArraySafeHandleTest.cs
index 320423b245..e03e20c4f7 100644
--- a/src/csharp/Grpc.Core.Tests/Internal/MetadataArraySafeHandleTest.cs
+++ b/src/csharp/Grpc.Core.Tests/Internal/MetadataArraySafeHandleTest.cs
@@ -51,7 +51,8 @@ namespace Grpc.Core.Internal.Tests
[Test]
public void CreateAndDestroy()
{
- var metadata = new Metadata {
+ var metadata = new Metadata
+ {
new Metadata.Entry("host", "somehost"),
new Metadata.Entry("header2", "header value"),
};
diff --git a/src/csharp/Grpc.IntegrationTesting/Grpc.IntegrationTesting.csproj b/src/csharp/Grpc.IntegrationTesting/Grpc.IntegrationTesting.csproj
index af4a75a034..d3c69ab9eb 100644
--- a/src/csharp/Grpc.IntegrationTesting/Grpc.IntegrationTesting.csproj
+++ b/src/csharp/Grpc.IntegrationTesting/Grpc.IntegrationTesting.csproj
@@ -32,21 +32,34 @@
<PlatformTarget>x86</PlatformTarget>
</PropertyGroup>
<ItemGroup>
- <Reference Include="Google.Apis.Auth.PlatformServices">
+ <Reference Include="Google.Apis.Auth, Version=1.9.1.12395, Culture=neutral, processorArchitecture=MSIL">
+ <SpecificVersion>False</SpecificVersion>
+ <HintPath>..\packages\Google.Apis.Auth.1.9.1\lib\net40\Google.Apis.Auth.dll</HintPath>
+ </Reference>
+ <Reference Include="Google.Apis.Auth.PlatformServices, Version=1.9.1.12399, Culture=neutral, processorArchitecture=MSIL">
+ <SpecificVersion>False</SpecificVersion>
<HintPath>..\packages\Google.Apis.Auth.1.9.1\lib\net40\Google.Apis.Auth.PlatformServices.dll</HintPath>
</Reference>
- <Reference Include="Google.Apis.Core">
+ <Reference Include="Google.Apis.Core, Version=1.9.1.12394, Culture=neutral, processorArchitecture=MSIL">
+ <SpecificVersion>False</SpecificVersion>
<HintPath>..\packages\Google.Apis.Core.1.9.1\lib\portable-net40+sl50+win+wpa81+wp80\Google.Apis.Core.dll</HintPath>
</Reference>
- <Reference Include="Microsoft.Threading.Tasks">
+ <Reference Include="Microsoft.Threading.Tasks, Version=1.0.12.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
+ <SpecificVersion>False</SpecificVersion>
<HintPath>..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.dll</HintPath>
</Reference>
- <Reference Include="Microsoft.Threading.Tasks.Extensions">
+ <Reference Include="Microsoft.Threading.Tasks.Extensions, Version=1.0.12.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
+ <SpecificVersion>False</SpecificVersion>
<HintPath>..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.Extensions.dll</HintPath>
</Reference>
- <Reference Include="Microsoft.Threading.Tasks.Extensions.Desktop">
+ <Reference Include="Microsoft.Threading.Tasks.Extensions.Desktop, Version=1.0.168.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
+ <SpecificVersion>False</SpecificVersion>
<HintPath>..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.Extensions.Desktop.dll</HintPath>
</Reference>
+ <Reference Include="Newtonsoft.Json, Version=6.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
+ <SpecificVersion>False</SpecificVersion>
+ <HintPath>..\packages\Newtonsoft.Json.6.0.6\lib\net45\Newtonsoft.Json.dll</HintPath>
+ </Reference>
<Reference Include="nunit.framework">
<HintPath>..\packages\NUnit.2.6.4\lib\nunit.framework.dll</HintPath>
</Reference>
@@ -63,16 +76,15 @@
</Reference>
<Reference Include="System.Net" />
<Reference Include="System.Net.Http" />
- <Reference Include="System.Net.Http.Extensions">
+ <Reference Include="System.Net.Http.Extensions, Version=2.2.28.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
+ <SpecificVersion>False</SpecificVersion>
<HintPath>..\packages\Microsoft.Net.Http.2.2.28\lib\net45\System.Net.Http.Extensions.dll</HintPath>
</Reference>
- <Reference Include="System.Net.Http.Primitives">
+ <Reference Include="System.Net.Http.Primitives, Version=4.2.28.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
+ <SpecificVersion>False</SpecificVersion>
<HintPath>..\packages\Microsoft.Net.Http.2.2.28\lib\net45\System.Net.Http.Primitives.dll</HintPath>
</Reference>
<Reference Include="System.Net.Http.WebRequest" />
- <Reference Include="Newtonsoft.Json">
- <HintPath>..\packages\Newtonsoft.Json.6.0.6\lib\net45\Newtonsoft.Json.dll</HintPath>
- </Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="..\Grpc.Core\Version.cs">
diff --git a/src/csharp/Grpc.IntegrationTesting/InteropClient.cs b/src/csharp/Grpc.IntegrationTesting/InteropClient.cs
index 05e732dbd4..ea83aaf2c1 100644
--- a/src/csharp/Grpc.IntegrationTesting/InteropClient.cs
+++ b/src/csharp/Grpc.IntegrationTesting/InteropClient.cs
@@ -135,7 +135,7 @@ namespace Grpc.IntegrationTesting
GrpcEnvironment.Shutdown();
}
- private void RunTestCase(string testCase, TestService.ITestServiceClient client)
+ private void RunTestCase(string testCase, TestService.TestServiceClient client)
{
switch (testCase)
{
@@ -163,6 +163,12 @@ namespace Grpc.IntegrationTesting
case "compute_engine_creds":
RunComputeEngineCreds(client);
break;
+ case "oauth2_auth_token":
+ RunOAuth2AuthToken(client);
+ break;
+ case "per_rpc_creds":
+ RunPerRpcCreds(client);
+ break;
case "cancel_after_begin":
RunCancelAfterBegin(client);
break;
@@ -355,6 +361,51 @@ namespace Grpc.IntegrationTesting
Console.WriteLine("Passed!");
}
+ public static void RunOAuth2AuthToken(TestService.TestServiceClient client)
+ {
+ Console.WriteLine("running oauth2_auth_token");
+ var credential = GoogleCredential.GetApplicationDefault().CreateScoped(new[] { AuthScope });
+ Assert.IsTrue(credential.RequestAccessTokenAsync(CancellationToken.None).Result);
+ string oauth2Token = credential.Token.AccessToken;
+
+ // Intercept calls with an OAuth2 token obtained out-of-band.
+ client.HeaderInterceptor = new MetadataInterceptorDelegate((metadata) =>
+ {
+ metadata.Add(new Metadata.Entry("Authorization", "Bearer " + oauth2Token));
+ });
+
+ var request = SimpleRequest.CreateBuilder()
+ .SetFillUsername(true)
+ .SetFillOauthScope(true)
+ .Build();
+
+ var response = client.UnaryCall(request);
+
+ Assert.AreEqual(AuthScopeResponse, response.OauthScope);
+ Assert.AreEqual(ServiceAccountUser, response.Username);
+ Console.WriteLine("Passed!");
+ }
+
+ public static void RunPerRpcCreds(TestService.TestServiceClient client)
+ {
+ Console.WriteLine("running per_rpc_creds");
+
+ var credential = GoogleCredential.GetApplicationDefault().CreateScoped(new[] { AuthScope });
+ Assert.IsTrue(credential.RequestAccessTokenAsync(CancellationToken.None).Result);
+ string oauth2Token = credential.Token.AccessToken;
+
+ var request = SimpleRequest.CreateBuilder()
+ .SetFillUsername(true)
+ .SetFillOauthScope(true)
+ .Build();
+
+ var response = client.UnaryCall(request, headers: new Metadata { new Metadata.Entry("Authorization", "Bearer " + oauth2Token) } );
+
+ Assert.AreEqual(AuthScopeResponse, response.OauthScope);
+ Assert.AreEqual(ServiceAccountUser, response.Username);
+ Console.WriteLine("Passed!");
+ }
+
public static void RunCancelAfterBegin(TestService.ITestServiceClient client)
{
Task.Run(async () =>
diff --git a/src/node/README.md b/src/node/README.md
index 2f4c49096d..78781dab14 100644
--- a/src/node/README.md
+++ b/src/node/README.md
@@ -54,10 +54,10 @@ function loadObject(reflectionObject)
Returns the same structure that `load` returns, but takes a reflection object from `ProtoBuf.js` instead of a file name.
```javascript
-function buildServer(serviceArray)
+function Server([serverOpions])
```
-Takes an array of service objects and returns a constructor for a server that handles requests to all of those services.
+Constructs a server to which service/implementation pairs can be added.
```javascript
diff --git a/src/node/examples/math_server.js b/src/node/examples/math_server.js
index 0a86e7eaff..b1f8a6323f 100644
--- a/src/node/examples/math_server.js
+++ b/src/node/examples/math_server.js
@@ -36,8 +36,6 @@
var grpc = require('..');
var math = grpc.load(__dirname + '/math.proto').math;
-var Server = grpc.buildServer([math.Math.service]);
-
/**
* Server function for division. Provides the /Math/DivMany and /Math/Div
* functions (Div is just DivMany with only one stream element). For each
@@ -108,19 +106,17 @@ function mathDivMany(stream) {
stream.end();
});
}
-
-var server = new Server({
- 'math.Math' : {
- div: mathDiv,
- fib: mathFib,
- sum: mathSum,
- divMany: mathDivMany
- }
+var server = new grpc.Server();
+server.addProtoService(math.Math.service, {
+ div: mathDiv,
+ fib: mathFib,
+ sum: mathSum,
+ divMany: mathDivMany
});
if (require.main === module) {
server.bind('0.0.0.0:50051');
- server.listen();
+ server.start();
}
/**
diff --git a/src/node/examples/route_guide_server.js b/src/node/examples/route_guide_server.js
index c777eab7bc..70044a322c 100644
--- a/src/node/examples/route_guide_server.js
+++ b/src/node/examples/route_guide_server.js
@@ -40,8 +40,6 @@ var _ = require('lodash');
var grpc = require('..');
var examples = grpc.load(__dirname + '/route_guide.proto').examples;
-var Server = grpc.buildServer([examples.RouteGuide.service]);
-
var COORD_FACTOR = 1e7;
/**
@@ -228,14 +226,14 @@ function routeChat(call) {
* @return {Server} The new server object
*/
function getServer() {
- return new Server({
- 'examples.RouteGuide' : {
- getFeature: getFeature,
- listFeatures: listFeatures,
- recordRoute: recordRoute,
- routeChat: routeChat
- }
+ var server = new grpc.Server();
+ server.addProtoService(examples.RouteGuide.service, {
+ getFeature: getFeature,
+ listFeatures: listFeatures,
+ recordRoute: recordRoute,
+ routeChat: routeChat
});
+ return server;
}
if (require.main === module) {
diff --git a/src/node/examples/stock_server.js b/src/node/examples/stock_server.js
index caaf9f99ba..f2eb6ad4ab 100644
--- a/src/node/examples/stock_server.js
+++ b/src/node/examples/stock_server.js
@@ -37,8 +37,6 @@ var _ = require('lodash');
var grpc = require('..');
var examples = grpc.load(__dirname + '/stock.proto').examples;
-var StockServer = grpc.buildServer([examples.Stock.service]);
-
function getLastTradePrice(call, callback) {
callback(null, {symbol: call.request.symbol, price: 88});
}
@@ -73,13 +71,12 @@ function getLastTradePriceMultiple(call) {
});
}
-var stockServer = new StockServer({
- 'examples.Stock' : {
- getLastTradePrice: getLastTradePrice,
- getLastTradePriceMultiple: getLastTradePriceMultiple,
- watchFutureTrades: watchFutureTrades,
- getHighestTradePrice: getHighestTradePrice
- }
+var stockServer = new grpc.Server();
+stockServer.addProtoService(examples.Stock.service, {
+ getLastTradePrice: getLastTradePrice,
+ getLastTradePriceMultiple: getLastTradePriceMultiple,
+ watchFutureTrades: watchFutureTrades,
+ getHighestTradePrice: getHighestTradePrice
});
if (require.main === module) {
diff --git a/src/node/index.js b/src/node/index.js
index b6a4e2d0ee..d81e780443 100644
--- a/src/node/index.js
+++ b/src/node/index.js
@@ -133,9 +133,9 @@ exports.loadObject = loadObject;
exports.load = load;
/**
- * See docs for server.makeServerConstructor
+ * See docs for Server
*/
-exports.buildServer = server.makeProtobufServerConstructor;
+exports.Server = server.Server;
/**
* Status name to code number mapping
@@ -159,5 +159,3 @@ exports.ServerCredentials = grpc.ServerCredentials;
exports.getGoogleAuthDelegate = getGoogleAuthDelegate;
exports.makeGenericClientConstructor = client.makeClientConstructor;
-
-exports.makeGenericServerConstructor = server.makeServerConstructor;
diff --git a/src/node/interop/interop_server.js b/src/node/interop/interop_server.js
index 0baa78a094..505c6bb537 100644
--- a/src/node/interop/interop_server.js
+++ b/src/node/interop/interop_server.js
@@ -38,7 +38,6 @@ var path = require('path');
var _ = require('lodash');
var grpc = require('..');
var testProto = grpc.load(__dirname + '/test.proto').grpc.testing;
-var Server = grpc.buildServer([testProto.TestService.service]);
/**
* Create a buffer filled with size zeroes
@@ -173,16 +172,15 @@ function getServer(port, tls) {
key_data,
pem_data);
}
- var server = new Server({
- 'grpc.testing.TestService' : {
- emptyCall: handleEmpty,
- unaryCall: handleUnary,
- streamingOutputCall: handleStreamingOutput,
- streamingInputCall: handleStreamingInput,
- fullDuplexCall: handleFullDuplex,
- halfDuplexCall: handleHalfDuplex
- }
- }, null, options);
+ var server = new grpc.Server(options);
+ server.addProtoService(testProto.TestService.service, {
+ emptyCall: handleEmpty,
+ unaryCall: handleUnary,
+ streamingOutputCall: handleStreamingOutput,
+ streamingInputCall: handleStreamingInput,
+ fullDuplexCall: handleFullDuplex,
+ halfDuplexCall: handleHalfDuplex
+ });
var port_num = server.bind('0.0.0.0:' + port, server_creds);
return {server: server, port: port_num};
}
diff --git a/src/node/src/server.js b/src/node/src/server.js
index 00be400e61..0a3a0031bd 100644
--- a/src/node/src/server.js
+++ b/src/node/src/server.js
@@ -72,6 +72,9 @@ function handleError(call, error) {
status.metadata = error.metadata;
}
var error_batch = {};
+ if (!call.metadataSent) {
+ error_batch[grpc.opType.SEND_INITIAL_METADATA] = {};
+ }
error_batch[grpc.opType.SEND_STATUS_FROM_SERVER] = status;
call.startBatch(error_batch, function(){});
}
@@ -115,6 +118,10 @@ function sendUnaryResponse(call, value, serialize, metadata) {
if (metadata) {
status.metadata = metadata;
}
+ if (!call.metadataSent) {
+ end_batch[grpc.opType.SEND_INITIAL_METADATA] = {};
+ call.metadataSent = true;
+ }
end_batch[grpc.opType.SEND_MESSAGE] = serialize(value);
end_batch[grpc.opType.SEND_STATUS_FROM_SERVER] = status;
call.startBatch(end_batch, function (){});
@@ -136,6 +143,10 @@ function setUpWritable(stream, serialize) {
stream.serialize = common.wrapIgnoreNull(serialize);
function sendStatus() {
var batch = {};
+ if (!stream.call.metadataSent) {
+ stream.call.metadataSent = true;
+ batch[grpc.opType.SEND_INITIAL_METADATA] = {};
+ }
batch[grpc.opType.SEND_STATUS_FROM_SERVER] = stream.status;
stream.call.startBatch(batch, function(){});
}
@@ -239,6 +250,10 @@ function ServerWritableStream(call, serialize) {
function _write(chunk, encoding, callback) {
/* jshint validthis: true */
var batch = {};
+ if (!this.call.metadataSent) {
+ batch[grpc.opType.SEND_INITIAL_METADATA] = {};
+ this.call.metadataSent = true;
+ }
batch[grpc.opType.SEND_MESSAGE] = this.serialize(chunk);
this.call.startBatch(batch, function(err, value) {
if (err) {
@@ -251,6 +266,23 @@ function _write(chunk, encoding, callback) {
ServerWritableStream.prototype._write = _write;
+function sendMetadata(responseMetadata) {
+ /* jshint validthis: true */
+ if (!this.call.metadataSent) {
+ this.call.metadataSent = true;
+ var batch = [];
+ batch[grpc.opType.SEND_INITIAL_METADATA] = responseMetadata;
+ this.call.startBatch(batch, function(err) {
+ if (err) {
+ this.emit('error', err);
+ return;
+ }
+ });
+ }
+}
+
+ServerWritableStream.prototype.sendMetadata = sendMetadata;
+
util.inherits(ServerReadableStream, Readable);
/**
@@ -339,6 +371,7 @@ function ServerDuplexStream(call, serialize, deserialize) {
ServerDuplexStream.prototype._read = _read;
ServerDuplexStream.prototype._write = _write;
+ServerDuplexStream.prototype.sendMetadata = sendMetadata;
/**
* Fully handle a unary call
@@ -348,12 +381,20 @@ ServerDuplexStream.prototype._write = _write;
*/
function handleUnary(call, handler, metadata) {
var emitter = new EventEmitter();
+ emitter.sendMetadata = function(responseMetadata) {
+ if (!call.metadataSent) {
+ call.metadataSent = true;
+ var batch = {};
+ batch[grpc.opType.SEND_INITIAL_METADATA] = responseMetadata;
+ call.startBatch(batch, function() {});
+ }
+ };
emitter.on('error', function(error) {
handleError(call, error);
});
+ emitter.metadata = metadata;
waitForCancel(call, emitter);
var batch = {};
- batch[grpc.opType.SEND_INITIAL_METADATA] = metadata;
batch[grpc.opType.RECV_MESSAGE] = true;
call.startBatch(batch, function(err, result) {
if (err) {
@@ -392,8 +433,8 @@ function handleUnary(call, handler, metadata) {
function handleServerStreaming(call, handler, metadata) {
var stream = new ServerWritableStream(call, handler.serialize);
waitForCancel(call, stream);
+ stream.metadata = metadata;
var batch = {};
- batch[grpc.opType.SEND_INITIAL_METADATA] = metadata;
batch[grpc.opType.RECV_MESSAGE] = true;
call.startBatch(batch, function(err, result) {
if (err) {
@@ -419,13 +460,19 @@ function handleServerStreaming(call, handler, metadata) {
*/
function handleClientStreaming(call, handler, metadata) {
var stream = new ServerReadableStream(call, handler.deserialize);
+ stream.sendMetadata = function(responseMetadata) {
+ if (!call.metadataSent) {
+ call.metadataSent = true;
+ var batch = {};
+ batch[grpc.opType.SEND_INITIAL_METADATA] = responseMetadata;
+ call.startBatch(batch, function() {});
+ }
+ };
stream.on('error', function(error) {
handleError(call, error);
});
waitForCancel(call, stream);
- var metadata_batch = {};
- metadata_batch[grpc.opType.SEND_INITIAL_METADATA] = metadata;
- call.startBatch(metadata_batch, function() {});
+ stream.metadata = metadata;
handler.func(stream, function(err, value, trailer) {
stream.terminate();
if (err) {
@@ -449,9 +496,7 @@ function handleBidiStreaming(call, handler, metadata) {
var stream = new ServerDuplexStream(call, handler.serialize,
handler.deserialize);
waitForCancel(call, stream);
- var metadata_batch = {};
- metadata_batch[grpc.opType.SEND_INITIAL_METADATA] = metadata;
- call.startBatch(metadata_batch, function() {});
+ stream.metadata = metadata;
handler.func(stream);
}
@@ -466,29 +511,28 @@ var streamHandlers = {
* Constructs a server object that stores request handlers and delegates
* incoming requests to those handlers
* @constructor
- * @param {function(string, Object<string, Array<Buffer>>):
- Object<string, Array<Buffer|string>>=} getMetadata Callback that gets
- * metatada for a given method
* @param {Object=} options Options that should be passed to the internal server
* implementation
*/
-function Server(getMetadata, options) {
+function Server(options) {
this.handlers = {};
var handlers = this.handlers;
var server = new grpc.Server(options);
this._server = server;
+ this.started = false;
/**
* Start the server and begin handling requests
* @this Server
*/
- this.listen = function() {
+ this.start = function() {
+ if (this.started) {
+ throw new Error('Server is already running');
+ }
+ this.started = true;
console.log('Server starting');
_.each(handlers, function(handler, handler_name) {
console.log('Serving', handler_name);
});
- if (this.started) {
- throw 'Server is already running';
- }
server.start();
/**
* Handles the SERVER_RPC_NEW event. If there is a handler associated with
@@ -523,11 +567,7 @@ function Server(getMetadata, options) {
call.startBatch(batch, function() {});
return;
}
- var response_metadata = {};
- if (getMetadata) {
- response_metadata = getMetadata(method, metadata);
- }
- streamHandlers[handler.type](call, handler, response_metadata);
+ streamHandlers[handler.type](call, handler, metadata);
}
server.requestCall(handleNewCall);
};
@@ -565,6 +605,47 @@ Server.prototype.register = function(name, handler, serialize, deserialize,
return true;
};
+Server.prototype.addService = function(service, implementation) {
+ if (this.started) {
+ throw new Error('Can\'t add a service to a started server.');
+ }
+ var self = this;
+ _.each(service, function(attrs, name) {
+ var method_type;
+ if (attrs.requestStream) {
+ if (attrs.responseStream) {
+ method_type = 'bidi';
+ } else {
+ method_type = 'client_stream';
+ }
+ } else {
+ if (attrs.responseStream) {
+ method_type = 'server_stream';
+ } else {
+ method_type = 'unary';
+ }
+ }
+ if (implementation[name] === undefined) {
+ throw new Error('Method handler for ' + attrs.path +
+ ' not provided.');
+ }
+ var serialize = attrs.responseSerialize;
+ var deserialize = attrs.requestDeserialize;
+ var register_success = self.register(attrs.path,
+ _.bind(implementation[name],
+ implementation),
+ serialize, deserialize, method_type);
+ if (!register_success) {
+ throw new Error('Method handler for ' + attrs.path +
+ ' already provided.');
+ }
+ });
+};
+
+Server.prototype.addProtoService = function(service, implementation) {
+ this.addService(common.getProtobufServiceAttrs(service), implementation);
+};
+
/**
* Binds the server to the given port, with SSL enabled if creds is given
* @param {string} port The port that the server should bind on, in the format
@@ -573,6 +654,9 @@ Server.prototype.register = function(name, handler, serialize, deserialize,
* nothing for an insecure port
*/
Server.prototype.bind = function(port, creds) {
+ if (this.started) {
+ throw new Error('Can\'t bind an already running server to an address');
+ }
if (creds) {
return this._server.addSecureHttp2Port(port, creds);
} else {
@@ -581,131 +665,6 @@ Server.prototype.bind = function(port, creds) {
};
/**
- * Create a constructor for servers with services defined by service_attr_map.
- * That is an object that maps (namespaced) service names to objects that in
- * turn map method names to objects with the following keys:
- * path: The path on the server for accessing the method. For example, for
- * protocol buffers, we use "/service_name/method_name"
- * requestStream: bool indicating whether the client sends a stream
- * resonseStream: bool indicating whether the server sends a stream
- * requestDeserialize: function to deserialize request objects
- * responseSerialize: function to serialize response objects
- * @param {Object} service_attr_map An object mapping service names to method
- * attribute map objects
- * @return {function(Object, function, Object=)} New server constructor
- */
-function makeServerConstructor(service_attr_map) {
- /**
- * Create a server with the given handlers for all of the methods.
- * @constructor
- * @param {Object} service_handlers Map from service names to map from method
- * names to handlers
- * @param {function(string, Object<string, Array<Buffer>>):
- Object<string, Array<Buffer|string>>=} getMetadata Callback that
- * gets metatada for a given method
- * @param {Object=} options Options to pass to the underlying server
- */
- function SurfaceServer(service_handlers, getMetadata, options) {
- var server = new Server(getMetadata, options);
- this.inner_server = server;
- _.each(service_attr_map, function(service_attrs, service_name) {
- if (service_handlers[service_name] === undefined) {
- throw new Error('Handlers for service ' +
- service_name + ' not provided.');
- }
- _.each(service_attrs, function(attrs, name) {
- var method_type;
- if (attrs.requestStream) {
- if (attrs.responseStream) {
- method_type = 'bidi';
- } else {
- method_type = 'client_stream';
- }
- } else {
- if (attrs.responseStream) {
- method_type = 'server_stream';
- } else {
- method_type = 'unary';
- }
- }
- if (service_handlers[service_name][name] === undefined) {
- throw new Error('Method handler for ' + attrs.path +
- ' not provided.');
- }
- var serialize = attrs.responseSerialize;
- var deserialize = attrs.requestDeserialize;
- server.register(attrs.path, _.bind(service_handlers[service_name][name],
- service_handlers[service_name]),
- serialize, deserialize, method_type);
- });
- }, this);
- }
-
- /**
- * Binds the server to the given port, with SSL enabled if creds is supplied
- * @param {string} port The port that the server should bind on, in the format
- * "address:port"
- * @param {boolean=} creds Credentials to use for SSL
- * @return {SurfaceServer} this
- */
- SurfaceServer.prototype.bind = function(port, creds) {
- return this.inner_server.bind(port, creds);
- };
-
- /**
- * Starts the server listening on any bound ports
- * @return {SurfaceServer} this
- */
- SurfaceServer.prototype.listen = function() {
- this.inner_server.listen();
- return this;
- };
-
- /**
- * Shuts the server down; tells it to stop listening for new requests and to
- * kill old requests.
- */
- SurfaceServer.prototype.shutdown = function() {
- this.inner_server.shutdown();
- };
-
- return SurfaceServer;
-}
-
-/**
- * Create a constructor for servers that serve the given services.
- * @param {Array<ProtoBuf.Reflect.Service>} services The services that the
- * servers will serve
- * @return {function(Object, function, Object=)} New server constructor
- */
-function makeProtobufServerConstructor(services) {
- var qual_names = [];
- var service_attr_map = {};
- _.each(services, function(service) {
- var service_name = common.fullyQualifiedName(service);
- _.each(service.children, function(method) {
- var name = common.fullyQualifiedName(method);
- if (_.indexOf(qual_names, name) !== -1) {
- throw new Error('Method ' + name + ' exposed by more than one service');
- }
- qual_names.push(name);
- });
- var method_attrs = common.getProtobufServiceAttrs(service);
- if (!service_attr_map.hasOwnProperty(service_name)) {
- service_attr_map[service_name] = {};
- }
- service_attr_map[service_name] = _.extend(service_attr_map[service_name],
- method_attrs);
- });
- return makeServerConstructor(service_attr_map);
-}
-
-/**
- * See documentation for makeServerConstructor
- */
-exports.makeServerConstructor = makeServerConstructor;
-
-/**
- * See documentation for makeProtobufServerConstructor
+ * See documentation for Server
*/
-exports.makeProtobufServerConstructor = makeProtobufServerConstructor;
+exports.Server = Server;
diff --git a/src/node/test/health_test.js b/src/node/test/health_test.js
index 4d1a5082e0..bb700cc46c 100644
--- a/src/node/test/health_test.js
+++ b/src/node/test/health_test.js
@@ -49,14 +49,13 @@ describe('Health Checking', function() {
'grpc.test.TestService': 'SERVING'
}
};
- var HealthServer = grpc.buildServer([health.service]);
- var healthServer = new HealthServer({
- 'grpc.health.v1alpha.Health': new health.Implementation(statusMap)
- });
+ var healthServer = new grpc.Server();
+ healthServer.addProtoService(health.service,
+ new health.Implementation(statusMap));
var healthClient;
before(function() {
var port_num = healthServer.bind('0.0.0.0:0');
- healthServer.listen();
+ healthServer.start();
healthClient = new health.Client('localhost:' + port_num);
});
after(function() {
diff --git a/src/node/test/interop_sanity_test.js b/src/node/test/interop_sanity_test.js
index fcd8eb6403..0a5eb29c0c 100644
--- a/src/node/test/interop_sanity_test.js
+++ b/src/node/test/interop_sanity_test.js
@@ -46,7 +46,7 @@ describe('Interop tests', function() {
before(function(done) {
var server_obj = interop_server.getServer(0, true);
server = server_obj.server;
- server.listen();
+ server.start();
port = 'localhost:' + server_obj.port;
done();
});
diff --git a/src/node/test/math_client_test.js b/src/node/test/math_client_test.js
index 3461922e66..f2751857ff 100644
--- a/src/node/test/math_client_test.js
+++ b/src/node/test/math_client_test.js
@@ -52,7 +52,7 @@ var server = require('../examples/math_server.js');
describe('Math client', function() {
before(function(done) {
var port_num = server.bind('0.0.0.0:0');
- server.listen();
+ server.start();
math_client = new math.Math('localhost:' + port_num);
done();
});
diff --git a/src/node/test/surface_test.js b/src/node/test/surface_test.js
index 125957277f..18178e49e4 100644
--- a/src/node/test/surface_test.js
+++ b/src/node/test/surface_test.js
@@ -69,34 +69,45 @@ describe('File loader', function() {
});
});
});
-describe('Surface server constructor', function() {
- it('Should fail with conflicting method names', function() {
- assert.throws(function() {
- grpc.buildServer([mathService, mathService]);
- });
+describe('Server.prototype.addProtoService', function() {
+ var server;
+ var dummyImpls = {
+ 'div': function() {},
+ 'divMany': function() {},
+ 'fib': function() {},
+ 'sum': function() {}
+ };
+ beforeEach(function() {
+ server = new grpc.Server();
+ });
+ afterEach(function() {
+ server.shutdown();
});
it('Should succeed with a single service', function() {
assert.doesNotThrow(function() {
- grpc.buildServer([mathService]);
+ server.addProtoService(mathService, dummyImpls);
+ });
+ });
+ it('Should fail with conflicting method names', function() {
+ server.addProtoService(mathService, dummyImpls);
+ assert.throws(function() {
+ server.addProtoService(mathService, dummyImpls);
});
});
it('Should fail with missing handlers', function() {
- var Server = grpc.buildServer([mathService]);
assert.throws(function() {
- new Server({
- 'math.Math': {
- 'div': function() {},
- 'divMany': function() {},
- 'fib': function() {}
- }
+ server.addProtoService(mathService, {
+ 'div': function() {},
+ 'divMany': function() {},
+ 'fib': function() {}
});
}, /math.Math.Sum/);
});
- it('Should fail with no handlers for the service', function() {
- var Server = grpc.buildServer([mathService]);
+ it('Should fail if the server has been started', function() {
+ server.start();
assert.throws(function() {
- new Server({});
- }, /math.Math/);
+ server.addProtoService(mathService, dummyImpls);
+ });
});
});
describe('Echo service', function() {
@@ -105,18 +116,16 @@ describe('Echo service', function() {
before(function() {
var test_proto = ProtoBuf.loadProtoFile(__dirname + '/echo_service.proto');
var echo_service = test_proto.lookup('EchoService');
- var Server = grpc.buildServer([echo_service]);
- server = new Server({
- 'EchoService': {
- echo: function(call, callback) {
- callback(null, call.request);
- }
+ server = new grpc.Server();
+ server.addProtoService(echo_service, {
+ echo: function(call, callback) {
+ callback(null, call.request);
}
});
var port = server.bind('localhost:0');
var Client = surface_client.makeProtobufClientConstructor(echo_service);
client = new Client('localhost:' + port);
- server.listen();
+ server.start();
});
after(function() {
server.shutdown();
@@ -151,18 +160,14 @@ describe('Generic client and server', function() {
var client;
var server;
before(function() {
- var Server = grpc.makeGenericServerConstructor({
- string: string_service_attrs
- });
- server = new Server({
- string: {
- capitalize: function(call, callback) {
- callback(null, _.capitalize(call.request));
- }
+ server = new grpc.Server();
+ server.addService(string_service_attrs, {
+ capitalize: function(call, callback) {
+ callback(null, _.capitalize(call.request));
}
});
var port = server.bind('localhost:0');
- server.listen();
+ server.start();
var Client = grpc.makeGenericClientConstructor(string_service_attrs);
client = new Client('localhost:' + port);
});
@@ -178,6 +183,82 @@ describe('Generic client and server', function() {
});
});
});
+describe('Echo metadata', function() {
+ var client;
+ var server;
+ before(function() {
+ var test_proto = ProtoBuf.loadProtoFile(__dirname + '/test_service.proto');
+ var test_service = test_proto.lookup('TestService');
+ server = new grpc.Server();
+ server.addProtoService(test_service, {
+ unary: function(call, cb) {
+ call.sendMetadata(call.metadata);
+ cb(null, {});
+ },
+ clientStream: function(stream, cb){
+ stream.on('data', function(data) {});
+ stream.on('end', function() {
+ stream.sendMetadata(stream.metadata);
+ cb(null, {});
+ });
+ },
+ serverStream: function(stream) {
+ stream.sendMetadata(stream.metadata);
+ stream.end();
+ },
+ bidiStream: function(stream) {
+ stream.on('data', function(data) {});
+ stream.on('end', function() {
+ stream.sendMetadata(stream.metadata);
+ stream.end();
+ });
+ }
+ });
+ var port = server.bind('localhost:0');
+ var Client = surface_client.makeProtobufClientConstructor(test_service);
+ client = new Client('localhost:' + port);
+ server.start();
+ });
+ after(function() {
+ server.shutdown();
+ });
+ it('with unary call', function(done) {
+ var call = client.unary({}, function(err, data) {
+ assert.ifError(err);
+ }, {key: ['value']});
+ call.on('metadata', function(metadata) {
+ assert.deepEqual(metadata.key, ['value']);
+ done();
+ });
+ });
+ it('with client stream call', function(done) {
+ var call = client.clientStream(function(err, data) {
+ assert.ifError(err);
+ }, {key: ['value']});
+ call.on('metadata', function(metadata) {
+ assert.deepEqual(metadata.key, ['value']);
+ done();
+ });
+ call.end();
+ });
+ it('with server stream call', function(done) {
+ var call = client.serverStream({}, {key: ['value']});
+ call.on('data', function() {});
+ call.on('metadata', function(metadata) {
+ assert.deepEqual(metadata.key, ['value']);
+ done();
+ });
+ });
+ it('with bidi stream call', function(done) {
+ var call = client.bidiStream({key: ['value']});
+ call.on('data', function() {});
+ call.on('metadata', function(metadata) {
+ assert.deepEqual(metadata.key, ['value']);
+ done();
+ });
+ call.end();
+ });
+});
describe('Other conditions', function() {
var client;
var server;
@@ -185,72 +266,70 @@ describe('Other conditions', function() {
before(function() {
var test_proto = ProtoBuf.loadProtoFile(__dirname + '/test_service.proto');
var test_service = test_proto.lookup('TestService');
- var Server = grpc.buildServer([test_service]);
- server = new Server({
- TestService: {
- unary: function(call, cb) {
- var req = call.request;
- if (req.error) {
- cb(new Error('Requested error'), null, {metadata: ['yes']});
+ server = new grpc.Server();
+ server.addProtoService(test_service, {
+ unary: function(call, cb) {
+ var req = call.request;
+ if (req.error) {
+ cb(new Error('Requested error'), null, {trailer_present: ['yes']});
+ } else {
+ cb(null, {count: 1}, {trailer_present: ['yes']});
+ }
+ },
+ clientStream: function(stream, cb){
+ var count = 0;
+ var errored;
+ stream.on('data', function(data) {
+ if (data.error) {
+ errored = true;
+ cb(new Error('Requested error'), null, {trailer_present: ['yes']});
} else {
- cb(null, {count: 1}, {metadata: ['yes']});
+ count += 1;
}
- },
- clientStream: function(stream, cb){
- var count = 0;
- var errored;
- stream.on('data', function(data) {
- if (data.error) {
- errored = true;
- cb(new Error('Requested error'), null, {metadata: ['yes']});
- } else {
- count += 1;
- }
- });
- stream.on('end', function() {
- if (!errored) {
- cb(null, {count: count}, {metadata: ['yes']});
- }
- });
- },
- serverStream: function(stream) {
- var req = stream.request;
- if (req.error) {
+ });
+ stream.on('end', function() {
+ if (!errored) {
+ cb(null, {count: count}, {trailer_present: ['yes']});
+ }
+ });
+ },
+ serverStream: function(stream) {
+ var req = stream.request;
+ if (req.error) {
+ var err = new Error('Requested error');
+ err.metadata = {trailer_present: ['yes']};
+ stream.emit('error', err);
+ } else {
+ for (var i = 0; i < 5; i++) {
+ stream.write({count: i});
+ }
+ stream.end({trailer_present: ['yes']});
+ }
+ },
+ bidiStream: function(stream) {
+ var count = 0;
+ stream.on('data', function(data) {
+ if (data.error) {
var err = new Error('Requested error');
- err.metadata = {metadata: ['yes']};
+ err.metadata = {
+ trailer_present: ['yes'],
+ count: ['' + count]
+ };
stream.emit('error', err);
} else {
- for (var i = 0; i < 5; i++) {
- stream.write({count: i});
- }
- stream.end({metadata: ['yes']});
+ stream.write({count: count});
+ count += 1;
}
- },
- bidiStream: function(stream) {
- var count = 0;
- stream.on('data', function(data) {
- if (data.error) {
- var err = new Error('Requested error');
- err.metadata = {
- metadata: ['yes'],
- count: ['' + count]
- };
- stream.emit('error', err);
- } else {
- stream.write({count: count});
- count += 1;
- }
- });
- stream.on('end', function() {
- stream.end({metadata: ['yes']});
- });
- }
+ });
+ stream.on('end', function() {
+ stream.end({trailer_present: ['yes']});
+ });
}
});
port = server.bind('localhost:0');
var Client = surface_client.makeProtobufClientConstructor(test_service);
client = new Client('localhost:' + port);
- server.listen();
+ server.start();
});
after(function() {
server.shutdown();
@@ -340,7 +419,7 @@ describe('Other conditions', function() {
assert.ifError(err);
});
call.on('status', function(status) {
- assert.deepEqual(status.metadata.metadata, ['yes']);
+ assert.deepEqual(status.metadata.trailer_present, ['yes']);
done();
});
});
@@ -349,7 +428,7 @@ describe('Other conditions', function() {
assert(err);
});
call.on('status', function(status) {
- assert.deepEqual(status.metadata.metadata, ['yes']);
+ assert.deepEqual(status.metadata.trailer_present, ['yes']);
done();
});
});
@@ -361,7 +440,7 @@ describe('Other conditions', function() {
call.write({error: false});
call.end();
call.on('status', function(status) {
- assert.deepEqual(status.metadata.metadata, ['yes']);
+ assert.deepEqual(status.metadata.trailer_present, ['yes']);
done();
});
});
@@ -373,7 +452,7 @@ describe('Other conditions', function() {
call.write({error: true});
call.end();
call.on('status', function(status) {
- assert.deepEqual(status.metadata.metadata, ['yes']);
+ assert.deepEqual(status.metadata.trailer_present, ['yes']);
done();
});
});
@@ -382,7 +461,7 @@ describe('Other conditions', function() {
call.on('data', function(){});
call.on('status', function(status) {
assert.strictEqual(status.code, grpc.status.OK);
- assert.deepEqual(status.metadata.metadata, ['yes']);
+ assert.deepEqual(status.metadata.trailer_present, ['yes']);
done();
});
});
@@ -390,7 +469,7 @@ describe('Other conditions', function() {
var call = client.serverStream({error: true});
call.on('data', function(){});
call.on('error', function(error) {
- assert.deepEqual(error.metadata.metadata, ['yes']);
+ assert.deepEqual(error.metadata.trailer_present, ['yes']);
done();
});
});
@@ -402,7 +481,7 @@ describe('Other conditions', function() {
call.on('data', function(){});
call.on('status', function(status) {
assert.strictEqual(status.code, grpc.status.OK);
- assert.deepEqual(status.metadata.metadata, ['yes']);
+ assert.deepEqual(status.metadata.trailer_present, ['yes']);
done();
});
});
@@ -413,7 +492,7 @@ describe('Other conditions', function() {
call.end();
call.on('data', function(){});
call.on('error', function(error) {
- assert.deepEqual(error.metadata.metadata, ['yes']);
+ assert.deepEqual(error.metadata.trailer_present, ['yes']);
done();
});
});
@@ -465,18 +544,17 @@ describe('Cancelling surface client', function() {
var client;
var server;
before(function() {
- var Server = grpc.buildServer([mathService]);
- server = new Server({
- 'math.Math': {
- 'div': function(stream) {},
- 'divMany': function(stream) {},
- 'fib': function(stream) {},
- 'sum': function(stream) {}
- }
+ server = new grpc.Server();
+ server.addProtoService(mathService, {
+ 'div': function(stream) {},
+ 'divMany': function(stream) {},
+ 'fib': function(stream) {},
+ 'sum': function(stream) {}
});
var port = server.bind('localhost:0');
var Client = surface_client.makeProtobufClientConstructor(mathService);
client = new Client('localhost:' + port);
+ server.start();
});
after(function() {
server.shutdown();
diff --git a/src/objective-c/GRPCClient/GRPCCall.m b/src/objective-c/GRPCClient/GRPCCall.m
index 53e5abe177..9435bf2b35 100644
--- a/src/objective-c/GRPCClient/GRPCCall.m
+++ b/src/objective-c/GRPCClient/GRPCCall.m
@@ -35,10 +35,10 @@
#include <grpc/grpc.h>
#include <grpc/support/time.h>
+#import <RxLibrary/GRXConcurrentWriteable.h>
#import "private/GRPCChannel.h"
#import "private/GRPCCompletionQueue.h"
-#import "private/GRPCDelegateWrapper.h"
#import "private/GRPCWrappedCall.h"
#import "private/NSData+GRPC.h"
#import "private/NSDictionary+GRPC.h"
@@ -78,9 +78,13 @@ NSString * const kGRPCStatusMetadataKey = @"io.grpc.StatusMetadataKey";
// do. Particularly, in the face of errors, there's no ordering guarantee at
// all. This wrapper over our actual writeable ensures thread-safety and
// correct ordering.
- GRPCDelegateWrapper *_responseWriteable;
+ GRXConcurrentWriteable *_responseWriteable;
GRXWriter *_requestWriter;
+ // To create a retain cycle when a call is started, up until it finishes. See
+ // |startWithWriteable:| and |finishWithError:|.
+ GRPCCall *_self;
+
NSMutableDictionary *_requestMetadata;
NSMutableDictionary *_responseMetadata;
}
@@ -143,8 +147,13 @@ NSString * const kGRPCStatusMetadataKey = @"io.grpc.StatusMetadataKey";
#pragma mark Finish
- (void)finishWithError:(NSError *)errorOrNil {
+ // If the call isn't retained anywhere else, it can be deallocated now.
+ _self = nil;
+
+ // If there were still request messages coming, stop them.
_requestWriter.state = GRXWriterStateFinished;
_requestWriter = nil;
+
if (errorOrNil) {
[_responseWriteable cancelWithError:errorOrNil];
} else {
@@ -191,7 +200,7 @@ NSString * const kGRPCStatusMetadataKey = @"io.grpc.StatusMetadataKey";
return;
}
__weak GRPCCall *weakSelf = self;
- __weak GRPCDelegateWrapper *weakWriteable = _responseWriteable;
+ __weak GRXConcurrentWriteable *weakWriteable = _responseWriteable;
dispatch_async(_callQueue, ^{
[weakSelf startReadWithHandler:^(grpc_byte_buffer *message) {
@@ -216,7 +225,7 @@ NSString * const kGRPCStatusMetadataKey = @"io.grpc.StatusMetadataKey";
[weakSelf cancelCall];
return;
}
- [weakWriteable enqueueMessage:data completionHandler:^{
+ [weakWriteable enqueueValue:data completionHandler:^{
[weakSelf startNextRead];
}];
}];
@@ -276,6 +285,7 @@ NSString * const kGRPCStatusMetadataKey = @"io.grpc.StatusMetadataKey";
}
- (void)writesFinishedWithError:(NSError *)errorOrNil {
+ _requestWriter = nil;
if (errorOrNil) {
[self cancel];
} else {
@@ -335,12 +345,14 @@ NSString * const kGRPCStatusMetadataKey = @"io.grpc.StatusMetadataKey";
#pragma mark GRXWriter implementation
- (void)startWithWriteable:(id<GRXWriteable>)writeable {
- // The following produces a retain cycle self:_responseWriteable:self, which is only
- // broken when writesFinishedWithError: is sent to the wrapped writeable.
- // Care is taken not to retain self strongly in any of the blocks used in
- // the implementation of GRPCCall, so that the life of the instance is
- // determined by this retain cycle.
- _responseWriteable = [[GRPCDelegateWrapper alloc] initWithWriteable:writeable writer:self];
+ // Create a retain cycle so that this instance lives until the RPC finishes (or is cancelled).
+ // This makes RPCs in which the call isn't externally retained possible (as long as it is started
+ // before being autoreleased).
+ // Care is taken not to retain self strongly in any of the blocks used in this implementation, so
+ // that the life of the instance is determined by this retain cycle.
+ _self = self;
+
+ _responseWriteable = [[GRXConcurrentWriteable alloc] initWithWriteable:writeable];
[self sendHeaders:_requestMetadata];
[self invokeCall];
}
diff --git a/src/objective-c/GRPCClient/private/GRPCCompletionQueue.m b/src/objective-c/GRPCClient/private/GRPCCompletionQueue.m
index 40aade4f9a..12535c9616 100644
--- a/src/objective-c/GRPCClient/private/GRPCCompletionQueue.m
+++ b/src/objective-c/GRPCClient/private/GRPCCompletionQueue.m
@@ -65,7 +65,8 @@
dispatch_async(gDefaultConcurrentQueue, ^{
while (YES) {
// The following call blocks until an event is available.
- grpc_event event = grpc_completion_queue_next(unmanagedQueue, gpr_inf_future);
+ grpc_event event = grpc_completion_queue_next(unmanagedQueue,
+ gpr_inf_future(GPR_CLOCK_REALTIME));
GRPCQueueCompletionHandler handler;
switch (event.type) {
case GRPC_OP_COMPLETE:
diff --git a/src/objective-c/GRPCClient/private/GRPCWrappedCall.m b/src/objective-c/GRPCClient/private/GRPCWrappedCall.m
index 45f10f5d63..1db63df77f 100644
--- a/src/objective-c/GRPCClient/private/GRPCWrappedCall.m
+++ b/src/objective-c/GRPCClient/private/GRPCWrappedCall.m
@@ -246,8 +246,11 @@
if (!_queue) {
return nil;
}
- _call = grpc_channel_create_call(channel.unmanagedChannel, _queue.unmanagedQueue,
- path.UTF8String, host.UTF8String, gpr_inf_future);
+ _call = grpc_channel_create_call(channel.unmanagedChannel,
+ _queue.unmanagedQueue,
+ path.UTF8String,
+ host.UTF8String,
+ gpr_inf_future(GPR_CLOCK_REALTIME));
if (_call == NULL) {
return nil;
}
diff --git a/src/objective-c/GRPCClient/private/GRPCDelegateWrapper.h b/src/objective-c/RxLibrary/GRXConcurrentWriteable.h
index 9a30a2f966..1080001905 100644
--- a/src/objective-c/GRPCClient/private/GRPCDelegateWrapper.h
+++ b/src/objective-c/RxLibrary/GRXConcurrentWriteable.h
@@ -33,49 +33,39 @@
#import <Foundation/Foundation.h>
-#import <RxLibrary/GRXWriter.h>
+#import "GRXWriter.h"
+#import "GRXWriteable.h"
-@protocol GRXWriteable;
-
-// This is a thread-safe wrapper over a GRXWriteable instance. It lets one
-// enqueue calls to a GRXWriteable instance for the main thread, guaranteeing
-// that writesFinishedWithError: is the last message sent to it (no matter what
-// messages are sent to the wrapper, in what order, nor from which thread). It
-// also guarantees that, if cancelWithError: is called from the main thread
-// (e.g. by the app cancelling the writes), no further messages are sent to the
-// writeable except writesFinishedWithError:.
+// This is a thread-safe wrapper over a GRXWriteable instance. It lets one enqueue calls to a
+// GRXWriteable instance for the main thread, guaranteeing that writesFinishedWithError: is the last
+// message sent to it (no matter what messages are sent to the wrapper, in what order, nor from
+// which thread). It also guarantees that, if cancelWithError: is called from the main thread (e.g.
+// by the app cancelling the writes), no further messages are sent to the writeable except
+// writesFinishedWithError:.
//
-// TODO(jcanizales): Let the user specify another queue for the writeable
-// callbacks.
-// TODO(jcanizales): Rename to GRXWriteableWrapper and move to the Rx library.
-@interface GRPCDelegateWrapper : NSObject
+// TODO(jcanizales): Let the user specify another queue for the writeable callbacks.
+@interface GRXConcurrentWriteable : NSObject
// The GRXWriteable passed is the wrapped writeable.
-// Both the GRXWriter instance and the GRXWriteable instance are retained until
-// writesFinishedWithError: is sent to the writeable, and released after that.
-// This is used to create a retain cycle that keeps both objects alive until the
-// writing is explicitly finished.
-- (instancetype)initWithWriteable:(id<GRXWriteable>)writeable writer:(GRXWriter *)writer
- NS_DESIGNATED_INITIALIZER;
+// The GRXWriteable instance is retained until writesFinishedWithError: is sent to it, and released
+// after that.
+- (instancetype)initWithWriteable:(id<GRXWriteable>)writeable NS_DESIGNATED_INITIALIZER;
// Enqueues writeValue: to be sent to the writeable in the main thread.
// The passed handler is invoked from the main thread after writeValue: returns.
-- (void)enqueueMessage:(NSData *)message completionHandler:(void (^)())handler;
+- (void)enqueueValue:(id)value completionHandler:(void (^)())handler;
-// Enqueues writesFinishedWithError:nil to be sent to the writeable in the main
-// thread. After that message is sent to the writeable, all other methods of
-// this object are effectively noops.
+// Enqueues writesFinishedWithError:nil to be sent to the writeable in the main thread. After that
+// message is sent to the writeable, all other methods of this object are effectively noops.
- (void)enqueueSuccessfulCompletion;
-// If the writeable has not yet received a writesFinishedWithError: message, this
-// will enqueue one to be sent to it in the main thread, and cancel all other
-// pending messages to the writeable enqueued by this object (both past and
-// future).
+// If the writeable has not yet received a writesFinishedWithError: message, this will enqueue one
+// to be sent to it in the main thread, and cancel all other pending messages to the writeable
+// enqueued by this object (both past and future).
// The error argument cannot be nil.
- (void)cancelWithError:(NSError *)error;
-// Cancels all pending messages to the writeable enqueued by this object (both
-// past and future). Because the writeable won't receive writesFinishedWithError:,
-// this also releases the writeable and the writer.
+// Cancels all pending messages to the writeable enqueued by this object (both past and future).
+// Because the writeable won't receive writesFinishedWithError:, this also releases the writeable.
- (void)cancelSilently;
@end
diff --git a/src/objective-c/GRPCClient/private/GRPCDelegateWrapper.m b/src/objective-c/RxLibrary/GRXConcurrentWriteable.m
index 294cfb7e23..08bd079aea 100644
--- a/src/objective-c/GRPCClient/private/GRPCDelegateWrapper.m
+++ b/src/objective-c/RxLibrary/GRXConcurrentWriteable.m
@@ -31,45 +31,42 @@
*
*/
-#import "GRPCDelegateWrapper.h"
+#import "GRXConcurrentWriteable.h"
#import <RxLibrary/GRXWriteable.h>
-@interface GRPCDelegateWrapper ()
-// These are atomic so that cancellation can nillify them from any thread.
+@interface GRXConcurrentWriteable ()
+// This is atomic so that cancellation can nillify it from any thread.
@property(atomic, strong) id<GRXWriteable> writeable;
-@property(atomic, strong) GRXWriter *writer;
@end
-@implementation GRPCDelegateWrapper {
+@implementation GRXConcurrentWriteable {
dispatch_queue_t _writeableQueue;
// This ensures that writesFinishedWithError: is only sent once to the writeable.
dispatch_once_t _alreadyFinished;
}
- (instancetype)init {
- return [self initWithWriteable:nil writer:nil];
+ return [self initWithWriteable:nil];
}
// Designated initializer
-- (instancetype)initWithWriteable:(id<GRXWriteable>)writeable writer:(GRXWriter *)writer {
+- (instancetype)initWithWriteable:(id<GRXWriteable>)writeable {
if (self = [super init]) {
_writeableQueue = dispatch_get_main_queue();
_writeable = writeable;
- _writer = writer;
}
return self;
}
-- (void)enqueueMessage:(NSData *)message completionHandler:(void (^)())handler {
+- (void)enqueueValue:(id)value completionHandler:(void (^)())handler {
dispatch_async(_writeableQueue, ^{
- // We're racing a possible cancellation performed by another thread. To turn
- // all already-enqueued messages into noops, cancellation nillifies the
- // writeable property. If we get it before it's nil, we won
- // the race.
+ // We're racing a possible cancellation performed by another thread. To turn all already-
+ // enqueued messages into noops, cancellation nillifies the writeable property. If we get it
+ // before it's nil, we won the race.
id<GRXWriteable> writeable = self.writeable;
if (writeable) {
- [writeable writeValue:message];
+ [writeable writeValue:value];
handler();
}
});
@@ -78,13 +75,11 @@
- (void)enqueueSuccessfulCompletion {
dispatch_async(_writeableQueue, ^{
dispatch_once(&_alreadyFinished, ^{
- // Cancellation is now impossible. None of the other three blocks can run
- // concurrently with this one.
+ // Cancellation is now impossible. None of the other three blocks can run concurrently with
+ // this one.
[self.writeable writesFinishedWithError:nil];
- // Break the retain cycle with writer, and skip any possible message to the
- // wrapped writeable enqueued after this one.
+ // Skip any possible message to the wrapped writeable enqueued after this one.
self.writeable = nil;
- self.writer = nil;
});
});
}
@@ -92,29 +87,24 @@
- (void)cancelWithError:(NSError *)error {
NSAssert(error, @"For a successful completion, use enqueueSuccessfulCompletion.");
dispatch_once(&_alreadyFinished, ^{
- // Skip any of the still-enqueued messages to the wrapped writeable. We use
- // the atomic setter to nillify writer and writeable because we might be
- // running concurrently with the blocks in _writeableQueue, and assignment
- // with ARC isn't atomic.
+ // Skip any of the still-enqueued messages to the wrapped writeable. We use the atomic setter to
+ // nillify writeable because we might be running concurrently with the blocks in
+ // _writeableQueue, and assignment with ARC isn't atomic.
id<GRXWriteable> writeable = self.writeable;
self.writeable = nil;
dispatch_async(_writeableQueue, ^{
[writeable writesFinishedWithError:error];
- // Break the retain cycle with writer.
- self.writer = nil;
});
});
}
- (void)cancelSilently {
dispatch_once(&_alreadyFinished, ^{
- // Skip any of the still-enqueued messages to the wrapped writeable. We use
- // the atomic setter to nillify writer and writeable because we might be
- // running concurrently with the blocks in _writeableQueue, and assignment
- // with ARC isn't atomic.
+ // Skip any of the still-enqueued messages to the wrapped writeable. We use the atomic setter to
+ // nillify writeable because we might be running concurrently with the blocks in
+ // _writeableQueue, and assignment with ARC isn't atomic.
self.writeable = nil;
- self.writer = nil;
});
}
@end
diff --git a/src/objective-c/RxLibrary/GRXImmediateWriter.m b/src/objective-c/RxLibrary/GRXImmediateWriter.m
index b6d2b2cac0..3edae788ab 100644
--- a/src/objective-c/RxLibrary/GRXImmediateWriter.m
+++ b/src/objective-c/RxLibrary/GRXImmediateWriter.m
@@ -76,28 +76,15 @@
}
+ (GRXWriter *)writerWithValue:(id)value {
- if (value) {
- return [self writerWithEnumerator:[NSEnumerator grx_enumeratorWithSingleValue:value]];
- } else {
- return [self emptyWriter];
- }
+ return [self writerWithEnumerator:[NSEnumerator grx_enumeratorWithSingleValue:value]];
}
+ (GRXWriter *)writerWithError:(NSError *)error {
- if (error) {
- return [self writerWithEnumerator:nil error:error];
- } else {
- return [self emptyWriter];
- }
+ return [self writerWithEnumerator:nil error:error];
}
+ (GRXWriter *)emptyWriter {
- static GRXImmediateWriter *emptyWriter;
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- emptyWriter = [self writerWithEnumerator:nil error:nil];
- });
- return emptyWriter;
+ return [self writerWithEnumerator:nil error:nil];
}
#pragma mark Conformance with GRXWriter
diff --git a/src/objective-c/RxLibrary/private/GRXNSFastEnumerator.m b/src/objective-c/RxLibrary/private/GRXNSFastEnumerator.m
index 2050fa98ec..0387c9954e 100644
--- a/src/objective-c/RxLibrary/private/GRXNSFastEnumerator.m
+++ b/src/objective-c/RxLibrary/private/GRXNSFastEnumerator.m
@@ -59,7 +59,6 @@
// Designated initializer.
- (instancetype)initWithContainer:(id<NSFastEnumeration>)container {
- NSAssert(container, @"container can't be nil");
if ((self = [super init])) {
_container = container;
}
diff --git a/src/objective-c/generated_libraries/RemoteTestClient/RemoteTest.podspec b/src/objective-c/generated_libraries/RemoteTestClient/RemoteTest.podspec
index dd0dab352d..7cc9a040fe 100644
--- a/src/objective-c/generated_libraries/RemoteTestClient/RemoteTest.podspec
+++ b/src/objective-c/generated_libraries/RemoteTestClient/RemoteTest.podspec
@@ -7,7 +7,13 @@ Pod::Spec.new do |s|
s.osx.deployment_target = "10.8"
# Run protoc with the Objective-C and gRPC plugins to generate protocol messages and gRPC clients.
- s.prepare_command = "protoc --objc_out=. --objcgrpc_out=. *.proto"
+ s.prepare_command = <<-CMD
+ cd ../../../..
+ # TODO(jcanizales): Make only Objective-C plugin.
+ make plugins
+ cd -
+ protoc --plugin=protoc-gen-grpc=../../../../bins/opt/grpc_objective_c_plugin --objc_out=. --grpc_out=. *.proto
+ CMD
s.subspec "Messages" do |ms|
ms.source_files = "*.pbobjc.{h,m}"
diff --git a/src/objective-c/generated_libraries/RouteGuideClient/RouteGuide.podspec b/src/objective-c/generated_libraries/RouteGuideClient/RouteGuide.podspec
index e26e62f5bb..0e8dacd1c4 100644
--- a/src/objective-c/generated_libraries/RouteGuideClient/RouteGuide.podspec
+++ b/src/objective-c/generated_libraries/RouteGuideClient/RouteGuide.podspec
@@ -7,7 +7,13 @@ Pod::Spec.new do |s|
s.osx.deployment_target = "10.8"
# Run protoc with the Objective-C and gRPC plugins to generate protocol messages and gRPC clients.
- s.prepare_command = "protoc --objc_out=. --objcgrpc_out=. *.proto"
+ s.prepare_command = <<-CMD
+ cd ../../../..
+ # TODO(jcanizales): Make only Objective-C plugin.
+ make plugins
+ cd -
+ protoc --plugin=protoc-gen-grpc=../../../../bins/opt/grpc_objective_c_plugin --objc_out=. --grpc_out=. *.proto
+ CMD
s.subspec "Messages" do |ms|
ms.source_files = "*.pbobjc.{h,m}"
diff --git a/src/objective-c/tests/Tests.xcodeproj/project.pbxproj b/src/objective-c/tests/Tests.xcodeproj/project.pbxproj
index 34be705db2..f13fb8288b 100644
--- a/src/objective-c/tests/Tests.xcodeproj/project.pbxproj
+++ b/src/objective-c/tests/Tests.xcodeproj/project.pbxproj
@@ -391,7 +391,6 @@
635697DC1B14FC11007A7283 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
- OTHER_LDFLAGS = "-ObjC";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
};
@@ -400,7 +399,6 @@
635697DD1B14FC11007A7283 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
- OTHER_LDFLAGS = "-ObjC";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
};
diff --git a/src/objective-c/tests/Tests.xcodeproj/xcshareddata/xcschemes/AllTests.xcscheme b/src/objective-c/tests/Tests.xcodeproj/xcshareddata/xcschemes/AllTests.xcscheme
new file mode 100644
index 0000000000..3a6e2c3591
--- /dev/null
+++ b/src/objective-c/tests/Tests.xcodeproj/xcshareddata/xcschemes/AllTests.xcscheme
@@ -0,0 +1,104 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+ LastUpgradeVersion = "0630"
+ version = "1.3">
+ <BuildAction
+ parallelizeBuildables = "YES"
+ buildImplicitDependencies = "YES">
+ <BuildActionEntries>
+ <BuildActionEntry
+ buildForTesting = "YES"
+ buildForRunning = "YES"
+ buildForProfiling = "NO"
+ buildForArchiving = "NO"
+ buildForAnalyzing = "YES">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "63423F431B150A5F006CF63C"
+ BuildableName = "AllTests.xctest"
+ BlueprintName = "AllTests"
+ ReferencedContainer = "container:Tests.xcodeproj">
+ </BuildableReference>
+ </BuildActionEntry>
+ </BuildActionEntries>
+ </BuildAction>
+ <TestAction
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ buildConfiguration = "Debug">
+ <Testables>
+ <TestableReference
+ skipped = "NO">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "63423F431B150A5F006CF63C"
+ BuildableName = "AllTests.xctest"
+ BlueprintName = "AllTests"
+ ReferencedContainer = "container:Tests.xcodeproj">
+ </BuildableReference>
+ <SkippedTests>
+ <Test
+ Identifier = "LocalClearTextTests">
+ </Test>
+ <Test
+ Identifier = "LocalClearTextTests/testConnectionToLocalServer">
+ </Test>
+ </SkippedTests>
+ </TestableReference>
+ </Testables>
+ <MacroExpansion>
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "63423F431B150A5F006CF63C"
+ BuildableName = "AllTests.xctest"
+ BlueprintName = "AllTests"
+ ReferencedContainer = "container:Tests.xcodeproj">
+ </BuildableReference>
+ </MacroExpansion>
+ </TestAction>
+ <LaunchAction
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ launchStyle = "0"
+ useCustomWorkingDirectory = "NO"
+ buildConfiguration = "Debug"
+ ignoresPersistentStateOnLaunch = "NO"
+ debugDocumentVersioning = "YES"
+ allowLocationSimulation = "YES">
+ <MacroExpansion>
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "63423F431B150A5F006CF63C"
+ BuildableName = "AllTests.xctest"
+ BlueprintName = "AllTests"
+ ReferencedContainer = "container:Tests.xcodeproj">
+ </BuildableReference>
+ </MacroExpansion>
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </LaunchAction>
+ <ProfileAction
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ savedToolIdentifier = ""
+ useCustomWorkingDirectory = "NO"
+ buildConfiguration = "Release"
+ debugDocumentVersioning = "YES">
+ <MacroExpansion>
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "63423F431B150A5F006CF63C"
+ BuildableName = "AllTests.xctest"
+ BlueprintName = "AllTests"
+ ReferencedContainer = "container:Tests.xcodeproj">
+ </BuildableReference>
+ </MacroExpansion>
+ </ProfileAction>
+ <AnalyzeAction
+ buildConfiguration = "Debug">
+ </AnalyzeAction>
+ <ArchiveAction
+ buildConfiguration = "Release"
+ revealArchiveInOrganizer = "YES">
+ </ArchiveAction>
+</Scheme>
diff --git a/src/objective-c/tests/run_tests.sh b/src/objective-c/tests/run_tests.sh
new file mode 100755
index 0000000000..37fced3a62
--- /dev/null
+++ b/src/objective-c/tests/run_tests.sh
@@ -0,0 +1,49 @@
+#!/bin/bash
+# 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.
+
+set -e
+
+cd $(dirname $0)
+
+# TODO(jcanizales): Remove when Cocoapods issue #3823 is resolved.
+export COCOAPODS_DISABLE_DETERMINISTIC_UUIDS=YES
+pod install
+
+# xcodebuild is very verbose. We filter its output and tell Bash to fail if any
+# element of the pipe fails.
+# TODO(jcanizales): Use xctool instead? Issue #2540.
+set -o pipefail
+XCODEBUILD_FILTER='(^===|^\*\*|\bfatal\b|\berror\b|\bwarning\b|\bfail)'
+xcodebuild \
+ -workspace Tests.xcworkspace \
+ -scheme AllTests \
+ -destination name="iPhone 6" \
+ test \
+ | egrep "$XCODEBUILD_FILTER" -