Browse Source

Capture logs in tests (#903)

Motivation:

In #902 we changed behaviour such that gRPC will not log unless you
explicitly pass in a logger. When  tests fail it's useful to have the
logs to triage them, especially when they fail under CI, for example.

Modifications:

- Add a capturing log handler factory to the test module which captures
  all logs emitted by handlers it makes
- Wire up loggers in tests
- Hook up `GRPCTestCase` to print the captured logs only if a test fails
  (or an environment variable was set)
- Add a bunch of missing `super.setUp` and `super.tearDown` calls

Result:

- If a test fails we get all `.trace` level logs emitted during that
  test
George Barnett 5 years ago
parent
commit
d27c29834f

+ 4 - 1
Tests/GRPCTests/AnyServiceClientTests.swift

@@ -20,7 +20,10 @@ import XCTest
 
 class AnyServiceClientTests: EchoTestCaseBase {
   var anyServiceClient: AnyServiceClient {
-    return AnyServiceClient(channel: self.client.channel)
+    return AnyServiceClient(
+      channel: self.client.channel,
+      defaultCallOptions: self.callOptionsWithLogger
+    )
   }
 
   func testUnary() throws {

+ 8 - 5
Tests/GRPCTests/BasicEchoTestCase.swift

@@ -100,15 +100,15 @@ class EchoTestCaseBase: GRPCTestCase {
     return try self.serverBuilder()
       .withErrorDelegate(makeErrorDelegate())
       .withServiceProviders([makeEchoProvider()])
+      .withLogger(self.serverLogger)
       .bind(host: "localhost", port: 0)
       .wait()
   }
 
   func makeClientConnection(port: Int) throws -> ClientConnection {
-    return self.connectionBuilder().connect(
-      host: "localhost",
-      port: port
-    )
+    return self.connectionBuilder()
+      .withBackgroundActivityLogger(self.clientLogger)
+      .connect(host: "localhost", port: port)
   }
 
   func makeEchoProvider() -> Echo_EchoProvider { return EchoProvider() }
@@ -116,7 +116,10 @@ class EchoTestCaseBase: GRPCTestCase {
   func makeErrorDelegate() -> ServerErrorDelegate? { return nil }
 
   func makeEchoClient(port: Int) throws -> Echo_EchoClient {
-    return Echo_EchoClient(channel: try self.makeClientConnection(port: port))
+    return Echo_EchoClient(
+      channel: try self.makeClientConnection(port: port),
+      defaultCallOptions: self.callOptionsWithLogger
+    )
   }
 
   override func setUp() {

+ 2 - 1
Tests/GRPCTests/CallStartBehaviorTests.swift

@@ -29,12 +29,13 @@ class CallStartBehaviorTests: GRPCTestCase {
     // and the RPC wouldn't complete until we call shutdown (because we're not setting a timeout).
     let channel = ClientConnection.insecure(group: group)
       .withCallStartBehavior(.fastFailure)
+      .withBackgroundActivityLogger(self.clientLogger)
       .connect(host: "http://unreachable.invalid", port: 0)
     defer {
       XCTAssertNoThrow(try channel.close().wait())
     }
 
-    let echo = Echo_EchoClient(channel: channel)
+    let echo = Echo_EchoClient(channel: channel, defaultCallOptions: self.callOptionsWithLogger)
     let get = echo.get(.with { $0.text = "Is anyone out there?" })
 
     XCTAssertThrowsError(try get.response.wait())

+ 107 - 0
Tests/GRPCTests/CapturingLogHandler.swift

@@ -0,0 +1,107 @@
+/*
+ * Copyright 2020, gRPC Authors All rights reserved.
+ *
+ * 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.
+ */
+import struct Foundation.Date
+import Logging
+import NIOConcurrencyHelpers
+
+/// A `LogHandler` factory which captures all logs emitted by the handlers it makes.
+internal class CapturingLogHandlerFactory {
+  private var lock = Lock()
+  private var _logs: [CapturedLog] = []
+
+  /// Returns all captured logs and empties the store of captured logs.
+  func clearCapturedLogs() -> [CapturedLog] {
+    return self.lock.withLock {
+      let logs = self._logs
+      self._logs.removeAll()
+      return logs
+    }
+  }
+
+  /// Make a `LogHandler` whose logs will be recorded by this factory.
+  func make(_ label: String) -> LogHandler {
+    return CapturingLogHandler(label: label) { log in
+      self.lock.withLockVoid {
+        self._logs.append(log)
+      }
+    }
+  }
+}
+
+/// A captured log.
+internal struct CapturedLog {
+  var label: String
+  var level: Logger.Level
+  var message: Logger.Message
+  var metadata: Logger.Metadata
+  var file: String
+  var function: String
+  var line: UInt
+  var date: Date
+}
+
+/// A log handler which captures all logs it records.
+internal struct CapturingLogHandler: LogHandler {
+  private let capture: (CapturedLog) -> Void
+
+  internal let label: String
+  internal var metadata: Logger.Metadata = [:]
+  internal var logLevel: Logger.Level = .trace
+
+  fileprivate init(label: String, capture: @escaping (CapturedLog) -> ()) {
+    self.label = label
+    self.capture = capture
+  }
+
+  internal func log(
+    level: Logger.Level,
+    message: Logger.Message,
+    metadata: Logger.Metadata?,
+    file: String,
+    function: String,
+    line: UInt
+  ) {
+    let merged: Logger.Metadata
+
+    if let metadata = metadata {
+      merged = self.metadata.merging(metadata, uniquingKeysWith: { old, new in return new })
+    } else {
+      merged = self.metadata
+    }
+
+    let log = CapturedLog(
+      label: self.label,
+      level: level,
+      message: message,
+      metadata: merged,
+      file: file,
+      function: function,
+      line: line,
+      date: Date()
+    )
+
+    self.capture(log)
+  }
+
+  internal subscript(metadataKey metadataKey: String) -> Logger.Metadata.Value? {
+    get {
+      return self.metadata[metadataKey]
+    }
+    set {
+      self.metadata[metadataKey] = newValue
+    }
+  }
+}

+ 9 - 4
Tests/GRPCTests/ClientConnectionBackoffTests.swift

@@ -33,6 +33,7 @@ class ClientConnectionBackoffTests: GRPCTestCase {
   var connectionStateRecorder = RecordingConnectivityDelegate()
 
   override func setUp() {
+    super.setUp()
     self.serverGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
     self.clientGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
   }
@@ -55,11 +56,14 @@ class ClientConnectionBackoffTests: GRPCTestCase {
     XCTAssertNoThrow(try self.clientGroup.syncShutdownGracefully())
     self.client = nil
     self.clientGroup = nil
+
+    super.tearDown()
   }
 
   func makeServer() -> EventLoopFuture<Server> {
     return Server.insecure(group: self.serverGroup)
       .withServiceProviders([EchoProvider()])
+      .withLogger(self.serverLogger)
       .bind(host: "localhost", port: self.port)
   }
 
@@ -68,6 +72,7 @@ class ClientConnectionBackoffTests: GRPCTestCase {
       .withConnectivityStateDelegate(self.connectionStateRecorder)
       .withConnectionBackoff(maximum: .milliseconds(100))
       .withConnectionTimeout(minimum: .milliseconds(100))
+      .withBackgroundActivityLogger(self.clientLogger)
   }
 
   func testClientConnectionFailsWithNoBackoff() throws {
@@ -83,7 +88,7 @@ class ClientConnectionBackoffTests: GRPCTestCase {
       .connect(host: "localhost", port: self.port)
 
     // Start an RPC to trigger creating a channel.
-    let echo = Echo_EchoClient(channel: self.client)
+    let echo = Echo_EchoClient(channel: self.client, defaultCallOptions: self.callOptionsWithLogger)
     _ = echo.get(.with { $0.text = "foo" })
 
     self.connectionStateRecorder.waitForExpectedChanges(timeout: .seconds(5))
@@ -104,7 +109,7 @@ class ClientConnectionBackoffTests: GRPCTestCase {
       .connect(host: "localhost", port: self.port)
 
     // Start an RPC to trigger creating a channel.
-    let echo = Echo_EchoClient(channel: self.client)
+    let echo = Echo_EchoClient(channel: self.client, defaultCallOptions: self.callOptionsWithLogger)
     _ = echo.get(.with { $0.text = "foo" })
 
     self.connectionStateRecorder.waitForExpectedChanges(timeout: .seconds(5))
@@ -123,7 +128,7 @@ class ClientConnectionBackoffTests: GRPCTestCase {
       .connect(host: "localhost", port: self.port)
 
     // Start an RPC to trigger creating a channel.
-    let echo = Echo_EchoClient(channel: self.client)
+    let echo = Echo_EchoClient(channel: self.client, defaultCallOptions: self.callOptionsWithLogger)
     _ = echo.get(.with { $0.text = "foo" })
 
     self.connectionStateRecorder.waitForExpectedChanges(timeout: .seconds(5))
@@ -162,7 +167,7 @@ class ClientConnectionBackoffTests: GRPCTestCase {
       .connect(host: "localhost", port: self.port)
 
     // Start an RPC to trigger creating a channel.
-    let echo = Echo_EchoClient(channel: self.client)
+    let echo = Echo_EchoClient(channel: self.client, defaultCallOptions: self.callOptionsWithLogger)
     _ = echo.get(.with { $0.text = "foo" })
 
     // Wait for the connection to be ready.

+ 7 - 1
Tests/GRPCTests/ClientTLSFailureTests.swift

@@ -62,7 +62,8 @@ class ClientTLSFailureTests: GRPCTestCase {
       eventLoopGroup: self.clientEventLoopGroup,
       tls: tls,
       // No need to retry connecting.
-      connectionBackoff: nil
+      connectionBackoff: nil,
+      backgroundActivityLogger: self.clientLogger
     )
   }
 
@@ -71,6 +72,8 @@ class ClientTLSFailureTests: GRPCTestCase {
   }
 
   override func setUp() {
+    super.setUp()
+
     self.serverEventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
 
     self.server = try! Server.secure(
@@ -78,6 +81,7 @@ class ClientTLSFailureTests: GRPCTestCase {
       certificateChain: [SampleCertificate.server.certificate],
       privateKey: SamplePrivateKey.server
     ).withServiceProviders([EchoProvider()])
+      .withLogger(self.serverLogger)
       .bind(host: "localhost", port: 0)
       .wait()
 
@@ -97,6 +101,8 @@ class ClientTLSFailureTests: GRPCTestCase {
     XCTAssertNoThrow(try self.serverEventLoopGroup.syncShutdownGracefully())
     self.server = nil
     self.serverEventLoopGroup = nil
+
+    super.tearDown()
   }
 
   func testClientConnectionFailsWhenServerIsUnknown() throws {

+ 6 - 3
Tests/GRPCTests/ClientTLSTests.swift

@@ -33,15 +33,14 @@ class ClientTLSHostnameOverrideTests: GRPCTestCase {
   }
 
   override func tearDown() {
-    super.tearDown()
     XCTAssertNoThrow(try self.server.close().wait())
     XCTAssertNoThrow(try connection.close().wait())
     XCTAssertNoThrow(try self.eventLoopGroup.syncShutdownGracefully())
+    super.tearDown()
   }
 
-
   func doTestUnary() throws {
-    let client = Echo_EchoClient(channel: self.connection)
+    let client = Echo_EchoClient(channel: self.connection, defaultCallOptions: self.callOptionsWithLogger)
     let get = client.get(.with { $0.text = "foo" })
 
     let response = try get.response.wait()
@@ -59,6 +58,7 @@ class ClientTLSHostnameOverrideTests: GRPCTestCase {
     self.server = try Server.secure(group: self.eventLoopGroup, certificateChain: [cert], privateKey: key)
       .withTLS(trustRoots: .certificates([SampleCertificate.ca.certificate]))
       .withServiceProviders([EchoProvider()])
+      .withLogger(self.serverLogger)
       .bind(host: "localhost", port: 0)
       .wait()
 
@@ -70,6 +70,7 @@ class ClientTLSHostnameOverrideTests: GRPCTestCase {
     self.connection = ClientConnection.secure(group: self.eventLoopGroup)
       .withTLS(trustRoots: .certificates([SampleCertificate.ca.certificate]))
       .withTLS(serverHostnameOverride: "example.com")
+      .withBackgroundActivityLogger(self.clientLogger)
       .connect(host: "localhost", port: port)
 
     try self.doTestUnary()
@@ -83,6 +84,7 @@ class ClientTLSHostnameOverrideTests: GRPCTestCase {
     self.server = try Server.secure(group: self.eventLoopGroup, certificateChain: [cert], privateKey: key)
       .withTLS(trustRoots: .certificates([SampleCertificate.ca.certificate]))
       .withServiceProviders([EchoProvider()])
+      .withLogger(self.serverLogger)
       .bind(host: "localhost", port: 0)
       .wait()
 
@@ -93,6 +95,7 @@ class ClientTLSHostnameOverrideTests: GRPCTestCase {
 
     self.connection = ClientConnection.secure(group: self.eventLoopGroup)
       .withTLS(trustRoots: .certificates([SampleCertificate.ca.certificate]))
+      .withBackgroundActivityLogger(self.clientLogger)
       .connect(host: "localhost", port: port)
 
     try self.doTestUnary()

+ 5 - 2
Tests/GRPCTests/ClientTimeoutTests.swift

@@ -28,7 +28,9 @@ class ClientTimeoutTests: GRPCTestCase {
     // We use a deadline here because internally we convert timeouts into deadlines by diffing
     // with `DispatchTime.now()`. We therefore need the deadline to be known in advance. Note we
     // use zero because `EmbeddedEventLoop`s time starts at zero.
-    return CallOptions(timeLimit: .deadline(.uptimeNanoseconds(0) + timeout))
+    var options = self.callOptionsWithLogger
+    options.timeLimit = .deadline(.uptimeNanoseconds(0) + timeout)
+    return options
   }
 
   // Note: this is not related to the call timeout since we're using an EmbeddedChannel. We require
@@ -38,7 +40,7 @@ class ClientTimeoutTests: GRPCTestCase {
   override func setUp() {
     super.setUp()
 
-    let connection = EmbeddedGRPCChannel()
+    let connection = EmbeddedGRPCChannel(logger: self.clientLogger)
     XCTAssertNoThrow(try connection.embeddedChannel.connect(to: SocketAddress(unixDomainSocketPath: "/foo")))
     let client = Echo_EchoClient(channel: connection, defaultCallOptions: self.callOptions)
 
@@ -48,6 +50,7 @@ class ClientTimeoutTests: GRPCTestCase {
 
   override func tearDown() {
     XCTAssertNoThrow(try self.channel.finish())
+    super.tearDown()
   }
 
   func assertRPCTimedOut(_ response: EventLoopFuture<Echo_EchoResponse>, expectation: XCTestExpectation) {

+ 5 - 1
Tests/GRPCTests/CompressionTests.swift

@@ -29,6 +29,7 @@ class MessageCompressionTests: GRPCTestCase {
   var echo: Echo_EchoClient!
 
   override func setUp() {
+    super.setUp()
     self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
   }
 
@@ -36,23 +37,26 @@ class MessageCompressionTests: GRPCTestCase {
     XCTAssertNoThrow(try self.client.close().wait())
     XCTAssertNoThrow(try self.server.close().wait())
     XCTAssertNoThrow(try self.group.syncShutdownGracefully())
+    super.tearDown()
   }
 
   func setupServer(encoding: ServerMessageEncoding) throws {
     self.server = try Server.insecure(group: self.group)
       .withServiceProviders([EchoProvider()])
       .withMessageCompression(encoding)
+      .withLogger(self.serverLogger)
       .bind(host: "localhost", port: 0)
       .wait()
   }
 
   func setupClient(encoding: ClientMessageEncoding) {
     self.client = ClientConnection.insecure(group: self.group)
+      .withBackgroundActivityLogger(self.clientLogger)
       .connect(host: "localhost", port: self.server.channel.localAddress!.port!)
 
     self.echo = Echo_EchoClient(
       channel: self.client,
-      defaultCallOptions: CallOptions(messageEncoding: encoding)
+      defaultCallOptions: CallOptions(messageEncoding: encoding, logger: self.clientLogger)
     )
   }
 

+ 2 - 1
Tests/GRPCTests/ConnectionManagerTests.swift

@@ -27,7 +27,8 @@ class ConnectionManagerTests: GRPCTestCase {
       target: .unixDomainSocket("/ignored"),
       eventLoopGroup: self.loop,
       connectivityStateDelegate: self.recorder,
-      connectionBackoff: nil
+      connectionBackoff: nil,
+      backgroundActivityLogger: self.clientLogger
     )
   }
 

+ 6 - 4
Tests/GRPCTests/EchoTestClientTests.swift

@@ -74,12 +74,14 @@ class EchoTestClientTests: GRPCTestCase {
 
     let server = try Server.insecure(group: group)
       .withServiceProviders([EchoProvider()])
+      .withLogger(self.serverLogger)
       .bind(host: "127.0.0.1", port: 0)
       .wait()
 
     self.server = server
 
     let channel = ClientConnection.insecure(group: group)
+      .withBackgroundActivityLogger(self.clientLogger)
       .connect(host: "127.0.0.1", port: server.channel.localAddress!.port!)
 
     self.channel = channel
@@ -102,7 +104,7 @@ class EchoTestClientTests: GRPCTestCase {
   }
 
   func testGetWithTestClient() {
-    let client = Echo_EchoTestClient()
+    let client = Echo_EchoTestClient(defaultCallOptions: self.callOptionsWithLogger)
     let model = EchoModel(client: client)
 
     let completed = self.expectation(description: "'Get' completed")
@@ -126,7 +128,7 @@ class EchoTestClientTests: GRPCTestCase {
 
   func testGetWithRealClientAndServer() throws {
     let channel = try self.setUpServerAndChannel()
-    let client = Echo_EchoClient(channel: channel)
+    let client = Echo_EchoClient(channel: channel, defaultCallOptions: self.callOptionsWithLogger)
     let model = EchoModel(client: client)
 
     let completed = self.expectation(description: "'Get' completed")
@@ -146,7 +148,7 @@ class EchoTestClientTests: GRPCTestCase {
   }
 
   func testUpdateWithTestClient() {
-    let client = Echo_EchoTestClient()
+    let client = Echo_EchoTestClient(defaultCallOptions: self.callOptionsWithLogger)
     let model = EchoModel(client: client)
 
     let completed = self.expectation(description: "'Update' completed")
@@ -175,7 +177,7 @@ class EchoTestClientTests: GRPCTestCase {
 
   func testUpdateWithRealClientAndServer() throws {
     let channel = try self.setUpServerAndChannel()
-    let client = Echo_EchoClient(channel: channel)
+    let client = Echo_EchoClient(channel: channel, defaultCallOptions: self.callOptionsWithLogger)
     let model = EchoModel(client: client)
 
     let completed = self.expectation(description: "'Update' completed")

+ 2 - 1
Tests/GRPCTests/FakeChannelTests.swift

@@ -25,7 +25,8 @@ class FakeChannelTests: GRPCTestCase {
   var channel: FakeChannel!
 
   override func setUp() {
-    self.channel = FakeChannel()
+    super.setUp()
+    self.channel = FakeChannel(logger: self.clientLogger)
   }
 
   private func makeUnaryResponse(

+ 4 - 1
Tests/GRPCTests/GRPCCustomPayloadTests.swift

@@ -30,19 +30,22 @@ class GRPCCustomPayloadTests: GRPCTestCase {
 
     self.server = try! Server.insecure(group: self.group)
       .withServiceProviders([CustomPayloadProvider()])
+      .withLogger(self.serverLogger)
       .bind(host: "localhost", port: 0)
       .wait()
 
     let channel = ClientConnection.insecure(group: self.group)
+      .withBackgroundActivityLogger(self.clientLogger)
       .connect(host: "localhost", port: server.channel.localAddress!.port!)
 
-    self.client = AnyServiceClient(channel: channel)
+    self.client = AnyServiceClient(channel: channel, defaultCallOptions: self.callOptionsWithLogger)
   }
 
   override func tearDown() {
     XCTAssertNoThrow(try self.server.close().wait())
     XCTAssertNoThrow(try self.client.channel.close().wait())
     XCTAssertNoThrow(try self.group.syncShutdownGracefully())
+    super.tearDown()
   }
 
   func testCustomPayload() throws {

+ 2 - 0
Tests/GRPCTests/GRPCIdleTests.swift

@@ -42,6 +42,7 @@ class GRPCIdleTests: GRPCTestCase {
     let server = try Server.insecure(group: group)
       .withServiceProviders([EchoProvider()])
       .withConnectionIdleTimeout(serverIdle)
+      .withLogger(self.serverLogger)
       .bind(host: "localhost", port: 0)
       .wait()
     defer {
@@ -62,6 +63,7 @@ class GRPCIdleTests: GRPCTestCase {
     let connection = ClientConnection.insecure(group: group)
       .withConnectivityStateDelegate(stateRecorder)
       .withConnectionIdleTimeout(clientIdle)
+      .withBackgroundActivityLogger(self.clientLogger)
       .connect(host: "localhost", port: server.channel.localAddress!.port!)
     defer {
       XCTAssertNoThrow(try connection.close().wait())

+ 3 - 2
Tests/GRPCTests/GRPCInteroperabilityTests.swift

@@ -38,7 +38,8 @@ class GRPCInsecureInteroperabilityTests: GRPCTestCase {
       host: "localhost",
       port: 0,
       eventLoopGroup: self.serverEventLoopGroup!,
-      useTLS: self.useTLS
+      useTLS: self.useTLS,
+      logger: self.serverLogger
     ).wait()
 
     guard let serverPort = self.server.channel.localAddress?.port else {
@@ -80,7 +81,7 @@ class GRPCInsecureInteroperabilityTests: GRPCTestCase {
     let builder = makeInteroperabilityTestClientBuilder(
       group: self.clientEventLoopGroup,
       useTLS: self.useTLS
-    )
+    ).withBackgroundActivityLogger(self.clientLogger)
     test.configure(builder: builder)
     self.clientConnection = builder.connect(host: "localhost", port: self.serverPort)
     XCTAssertNoThrow(try test.run(using: self.clientConnection), line: line)

+ 1 - 0
Tests/GRPCTests/GRPCServerRequestRoutingHandlerTests.swift

@@ -41,6 +41,7 @@ class GRPCServerRequestRoutingHandlerTests: GRPCTestCase {
 
   override func tearDown() {
     XCTAssertNoThrow(try self.channel.finish())
+    super.tearDown()
   }
 
   func testInvalidGRPCContentTypeReturnsUnsupportedMediaType() throws {

+ 92 - 46
Tests/GRPCTests/GRPCTestCase.swift

@@ -13,47 +13,34 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import GRPC
 import XCTest
 import Logging
 
-/// A test case which initializes the logging system once.
-///
 /// This should be used instead of `XCTestCase`.
 class GRPCTestCase: XCTestCase {
-  // Travis will fail the CI if there is too much logging, but it can be useful when running
-  // locally; conditionally enable it based on the environment.
-  //
-  // https://docs.travis-ci.com/user/environment-variables/#default-environment-variables
-  private static let isCI = Bool(
-      fromTruthLike: ProcessInfo.processInfo.environment["CI"],
-      defaultingTo: false
+  /// Unless `GRPC_ALWAYS_LOG` is set, logs will only be printed if a test case fails.
+  private static let alwaysLog = Bool(
+    fromTruthLike: ProcessInfo.processInfo.environment["GRPC_ALWAYS_LOG"],
+    defaultingTo: false
   )
-  private static let isLoggingEnabled = !isCI
 
   private static let runTimeSensitiveTests = Bool(
-      fromTruthLike: ProcessInfo.processInfo.environment["ENABLE_TIMING_TESTS"],
-      defaultingTo: true
+    fromTruthLike: ProcessInfo.processInfo.environment["ENABLE_TIMING_TESTS"],
+    defaultingTo: true
   )
 
-  // `LoggingSystem.bootstrap` must be called once per process. This is the suggested approach to
-  // workaround this for XCTestCase.
-  //
-  // See: https://github.com/apple/swift-log/issues/77
-  private static let isLoggingConfigured: Bool = {
-    LoggingSystem.bootstrap { label in
-      guard isLoggingEnabled else {
-        return BlackHole()
-      }
-      var handler = StreamLogHandler.standardOutput(label: label)
-      handler.logLevel = .debug
-      return handler
+  override func setUp() {
+    super.setUp()
+    self.logFactory = CapturingLogHandlerFactory()
+  }
+
+  override func tearDown() {
+    if GRPCTestCase.alwaysLog || (self.testRun.map { $0.totalFailureCount > 0 } ?? false) {
+      self.printCapturedLogs()
     }
-    return true
-  }()
 
-  override class func setUp() {
-    super.setUp()
-    XCTAssertTrue(GRPCTestCase.isLoggingConfigured)
+    super.tearDown()
   }
 
   func runTimeSensitiveTests() -> Bool {
@@ -64,32 +51,70 @@ class GRPCTestCase: XCTestCase {
     return shouldRun
   }
 
+  private(set) var logFactory: CapturingLogHandlerFactory!
+
+  /// A general-use logger.
   var logger: Logger {
-    return Logger(label: "io.grpc.testing")
+    return Logger(label: "grpc", factory: self.logFactory.make)
   }
-}
 
-/// A `LogHandler` which does nothing with log messages.
-struct BlackHole: LogHandler {
-  func log(level: Logger.Level, message: Logger.Message, metadata: Logger.Metadata?, file: String, function: String, line: UInt) {
-    ()
+  /// A logger for clients to use.
+  var clientLogger: Logger {
+    // Label is ignored; we already have a handler.
+    return Logger(label: "client", factory: self.logFactory.make)
   }
 
-  subscript(metadataKey key: String) -> Logger.Metadata.Value? {
-    get {
-      return metadata[key]
-    }
-    set(newValue) {
-      self.metadata[key] = newValue
-    }
+  /// A logger for servers to use.
+  var serverLogger: Logger {
+    // Label is ignored; we already have a handler.
+    return Logger(label: "server", factory: self.logFactory.make)
+  }
+
+  /// The default client call options using `self.clientLogger`.
+  var callOptionsWithLogger: CallOptions {
+    return CallOptions(logger: self.clientLogger)
+  }
+
+  /// Returns all captured logs sorted by date.
+  private func capturedLogs() -> [CapturedLog] {
+    assert(self.logFactory != nil, "Missing call to super.setUp()")
+
+    var logs = self.logFactory.clearCapturedLogs()
+    logs.sort(by: { $0.date < $1.date })
+
+    return logs
   }
 
-  var metadata: Logger.Metadata = [:]
-  var logLevel: Logger.Level = .critical
+  /// Prints all captured logs.
+  private func printCapturedLogs() {
+    let logs = self.capturedLogs()
+
+    let formatter = DateFormatter()
+    // We don't care about the date.
+    formatter.dateFormat = "HH:mm:ss.SSS"
+
+    print("Test Case '\(self.name)' logs started")
+
+    // The logs are already sorted by date.
+    for log in logs {
+      let date = formatter.string(from: log.date)
+      let level = log.level.short
+
+      // Format the metadata.
+      let formattedMetadata = log.metadata
+        .sorted(by: { $0.key < $1.key })
+        .map { key, value in "\(key)=\(value)" }
+        .joined(separator: " ")
+
+      print("\(date) \(log.label) \(level):", log.message, formattedMetadata)
+    }
+
+    print("Test Case '\(self.name)' logs finished")
+  }
 }
 
-fileprivate extension Bool {
-  init(fromTruthLike value: String?, defaultingTo defaultValue: Bool) {
+extension Bool {
+  fileprivate init(fromTruthLike value: String?, defaultingTo defaultValue: Bool) {
     switch value?.lowercased() {
     case "0", "false", "no":
       self = false
@@ -100,3 +125,24 @@ fileprivate extension Bool {
     }
   }
 }
+
+extension Logger.Level {
+  fileprivate var short: String {
+    switch self {
+    case .info:
+      return "I"
+    case .debug:
+      return "D"
+    case .warning:
+      return "W"
+    case .error:
+      return "E"
+    case .critical:
+      return "C"
+    case .trace:
+      return "T"
+    case .notice:
+      return "N"
+    }
+  }
+}

+ 1 - 1
Tests/GRPCTests/HTTP1ToGRPCServerCodecTests.swift

@@ -32,8 +32,8 @@ class HTTP1ToGRPCServerCodecTests: GRPCTestCase {
   }
 
   override func tearDown() {
-    super.tearDown()
     XCTAssertNoThrow(try self.channel.finish())
+    super.tearDown()
   }
 
   func makeRequestHead() -> HTTPRequestHead {

+ 1 - 0
Tests/GRPCTests/HeaderNormalizationTests.swift

@@ -106,6 +106,7 @@ class HeaderNormalizationTests: GRPCTestCase {
     XCTAssertNoThrow(try self.channel.close().wait())
     XCTAssertNoThrow(try self.server.close().wait())
     XCTAssertNoThrow(try self.group.syncShutdownGracefully())
+    super.tearDown()
   }
 
   private func assertCustomMetadataIsLowercased(

+ 1 - 0
Tests/GRPCTests/PlatformSupportTests.swift

@@ -24,6 +24,7 @@ class PlatformSupportTests: GRPCTestCase {
 
   override func tearDown() {
     XCTAssertNoThrow(try self.group?.syncShutdownGracefully())
+    super.tearDown()
   }
 
   func testMakeEventLoopGroupReturnsMultiThreadedGroupForPosix() {

+ 2 - 0
Tests/GRPCTests/ServerTLSErrorTests.swift

@@ -66,6 +66,7 @@ class ServerTLSErrorTests: GRPCTestCase {
   }
 
   override func setUp() {
+    super.setUp()
     self.serverEventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
     self.clientEventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
   }
@@ -76,6 +77,7 @@ class ServerTLSErrorTests: GRPCTestCase {
 
     XCTAssertNoThrow(try self.serverEventLoopGroup.syncShutdownGracefully())
     self.serverEventLoopGroup = nil
+    super.tearDown()
   }
 
   func testErrorIsLoggedWhenSSLContextErrors() throws {

+ 1 - 1
Tests/GRPCTests/TestClientExample.swift

@@ -24,7 +24,7 @@ class FakeResponseStreamExampleTests: GRPCTestCase {
 
   override func setUp() {
     super.setUp()
-    self.client = Echo_EchoTestClient()
+    self.client = Echo_EchoTestClient(defaultCallOptions: self.callOptionsWithLogger)
   }
 
   func testUnary() {