Skip to content
Permalink
Browse files

Create NIOTestUtils & add B2MD verifier (#939)

Motivation:

When writing B2MDs, there are a couple of scenarios that always need to
be tested: firehose feeding, drip feeding, many messages, ...

It's tedious writing those tests over and over again for every B2MD.

Modifications:

- Add a simple B2MD verifier that users can use in unit tests.
- Add a new, public `NIOTestUtils` module which contains utilities
  mostly useful for testing. Crucially however, it does not depend on
  `XCTest` so it can be used it all targets.

Result:

Hopefully fewer bugs in B2MDs.
  • Loading branch information...
weissi committed May 10, 2019
1 parent d1c7cd0 commit 5513bb202a16d22ee1a6c994acbd6ed71c29e78c
@@ -59,16 +59,20 @@ var targets: [PackageDescription.Target] = [
dependencies: ["NIO"]),
.target(name: "NIOUDPEchoClient",
dependencies: ["NIO"]),
.target(name: "NIOTestUtils",
dependencies: ["NIO"]),
.testTarget(name: "NIOTests",
dependencies: ["NIO", "NIOFoundationCompat"]),
.testTarget(name: "NIOConcurrencyHelpersTests",
dependencies: ["NIOConcurrencyHelpers"]),
.testTarget(name: "NIOHTTP1Tests",
dependencies: ["NIOHTTP1", "NIOFoundationCompat"]),
dependencies: ["NIOHTTP1", "NIOFoundationCompat", "NIOTestUtils"]),
.testTarget(name: "NIOTLSTests",
dependencies: ["NIO", "NIOTLS", "NIOFoundationCompat"]),
.testTarget(name: "NIOWebSocketTests",
dependencies: ["NIO", "NIOWebSocket"]),
.testTarget(name: "NIOTestUtilsTests",
dependencies: ["NIOTestUtils"]),
]

let package = Package(
@@ -94,6 +98,7 @@ let package = Package(
.library(name: "NIOConcurrencyHelpers", targets: ["NIOConcurrencyHelpers"]),
.library(name: "NIOFoundationCompat", targets: ["NIOFoundationCompat"]),
.library(name: "NIOWebSocket", targets: ["NIOWebSocket"]),
.library(name: "NIOTestUtils", targets: ["NIOTestUtils"]),
],
dependencies: [
],
@@ -0,0 +1,211 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2019 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import NIO

public enum ByteToMessageDecoderVerifier {
/// - seealso: verifyDecoder(inputOutputPairs:decoderFactory:)
///
/// Verify `ByteToMessageDecoder`s with `String` inputs
public static func verifyDecoder<Decoder: ByteToMessageDecoder>(stringInputOutputPairs: [(String, [Decoder.InboundOut])],
decoderFactory: @escaping () -> Decoder) throws where Decoder.InboundOut: Equatable {
let alloc = ByteBufferAllocator()
let ioPairs = stringInputOutputPairs.map { (ioPair: (String, [Decoder.InboundOut])) -> (ByteBuffer, [Decoder.InboundOut]) in
var buffer = alloc.buffer(capacity: ioPair.0.utf8.count)
buffer.writeString(ioPair.0)
return (buffer, ioPair.1)
}
return try ByteToMessageDecoderVerifier.verifyDecoder(inputOutputPairs: ioPairs, decoderFactory: decoderFactory)
}

/// Verifies a `ByteToMessageDecoder` by performing a number of tests.
///
/// This method is mostly useful in unit tests for `ByteToMessageDecoder`s. It feeds the inputs from
/// `inputOutputPairs` into the decoder in various ways and expects the decoder to produce the outputs from
/// `inputOutputPairs`.
///
/// The verification performs various tests, for example:
///
/// - drip feeding the bytes, one by one
/// - sending many messages in one `ByteBuffer`
/// - sending each complete message in one `ByteBuffer`
///
/// For `ExampleDecoder` that produces `ExampleDecoderOutput`s you would use this method the following way:
///
/// var exampleInput1 = channel.allocator.buffer(capacity: 16)
/// exampleInput1.writeString("example-in1")
/// var exampleInput2 = channel.allocator.buffer(capacity: 16)
/// exampleInput2.writeString("example-in2")
/// let expectedInOuts = [(exampleInput1, ExampleDecoderOutput("1")),
/// (exampleInput2, ExampleDecoderOutput("2"))
/// ]
/// XCTAssertNoThrow(try ByteToMessageDecoderVerifier.verifyDecoder(inputOutputPairs: expectedInOuts,
/// decoderFactory: { ExampleDecoder() }))
public static func verifyDecoder<Decoder: ByteToMessageDecoder>(inputOutputPairs: [(ByteBuffer, [Decoder.InboundOut])],
decoderFactory: @escaping () -> Decoder) throws where Decoder.InboundOut: Equatable {
typealias Out = Decoder.InboundOut

func verifySimple(channel: RecordingChannel) throws {
for (input, expectedOutputs) in inputOutputPairs.shuffled() {
try channel.writeInbound(input)
for expectedOutput in expectedOutputs {
guard let actualOutput = try channel.readInbound(as: Out.self) else {
throw VerificationError<Out>(inputs: channel.inboundWrites,
errorCode: .underProduction(expectedOutput))
}
guard actualOutput == expectedOutput else {
throw VerificationError<Out>(inputs: channel.inboundWrites,
errorCode: .wrongProduction(actual: actualOutput,
expected: expectedOutput))
}
}
let actualExtraOutput = try channel.readInbound(as: Out.self)
guard actualExtraOutput == nil else {
throw VerificationError<Out>(inputs: channel.inboundWrites,
errorCode: .overProduction(actualExtraOutput!))
}
}
}

func verifyDripFeed(channel: RecordingChannel) throws {
for _ in 0..<10 {
for (input, expectedOutputs) in inputOutputPairs.shuffled() {
for c in input.readableBytesView {
var buffer = channel.allocator.buffer(capacity: 12)
buffer.writeString("BEFORE")
buffer.writeInteger(c)
buffer.writeString("AFTER")
buffer.moveReaderIndex(forwardBy: 6)
buffer.moveWriterIndex(to: buffer.readerIndex + 1)
try channel.writeInbound(buffer)
}
for expectedOutput in expectedOutputs {
guard let actualOutput = try channel.readInbound(as: Out.self) else {
throw VerificationError<Out>(inputs: channel.inboundWrites,
errorCode: .underProduction(expectedOutput))
}
guard actualOutput == expectedOutput else {
throw VerificationError<Out>(inputs: channel.inboundWrites,
errorCode: .wrongProduction(actual: actualOutput,
expected: expectedOutput))
}
}
let actualExtraOutput = try channel.readInbound(as: Out.self)
guard actualExtraOutput == nil else {
throw VerificationError<Out>(inputs: channel.inboundWrites,
errorCode: .overProduction(actualExtraOutput!))
}
}
}
}

func verifyManyAtOnce(channel: RecordingChannel) throws {
var overallBuffer = channel.allocator.buffer(capacity: 1024)
var overallExpecteds: [Out] = []

for _ in 0..<10 {
for (var input, expectedOutputs) in inputOutputPairs.shuffled() {
overallBuffer.writeBuffer(&input)
overallExpecteds.append(contentsOf: expectedOutputs)
}
}

try channel.writeInbound(overallBuffer)
for expectedOutput in overallExpecteds {
guard let actualOutput = try channel.readInbound(as: Out.self) else {
throw VerificationError<Out>(inputs: channel.inboundWrites,
errorCode: .underProduction(expectedOutput))
}
guard actualOutput == expectedOutput else {
throw VerificationError<Out>(inputs: channel.inboundWrites,
errorCode: .wrongProduction(actual: actualOutput,
expected: expectedOutput))
}
}
}

let decoder: Decoder = decoderFactory()
let channel = RecordingChannel(EmbeddedChannel(handler: ByteToMessageHandler<Decoder>(decoder)))

try verifySimple(channel: channel)
try verifyDripFeed(channel: channel)
try verifyManyAtOnce(channel: channel)

if case .leftOvers(inbound: let ib, outbound: let ob, pendingOutbound: let pob) = try channel.finish() {
throw VerificationError<Out>(inputs: channel.inboundWrites,
errorCode: .leftOversOnDeconstructingChannel(inbound: ib,
outbound: ob,
pendingOutbound: pob))
}
}
}

extension ByteToMessageDecoderVerifier {
private class RecordingChannel {
private let actualChannel: EmbeddedChannel
private(set) var inboundWrites: [ByteBuffer] = []

init(_ actualChannel: EmbeddedChannel) {
self.actualChannel = actualChannel
}

func readInbound<T>(as type: T.Type = T.self) throws -> T? {
return try self.actualChannel.readInbound()
}

@discardableResult public func writeInbound(_ data: ByteBuffer) throws -> EmbeddedChannel.BufferState {
self.inboundWrites.append(data)
return try self.actualChannel.writeInbound(data)
}

var allocator: ByteBufferAllocator {
return self.actualChannel.allocator
}

func finish() throws -> EmbeddedChannel.LeftOverState {
return try self.actualChannel.finish()
}
}
}

extension ByteToMessageDecoderVerifier {
/// A `VerificationError` is thrown when the verification of a `ByteToMessageDecoder` failed.
public struct VerificationError<OutputType: Equatable>: Error {
/// Contains the `inputs` that were passed to the `ByteToMessageDecoder` at the point where it failed
/// verification.
public var inputs: [ByteBuffer]

/// `errorCode` describes the concrete problem that was detected.
public var errorCode: ErrorCode

public enum ErrorCode {
/// The `errorCode` will be `wrongProduction` when the `expected` output didn't match the `actual`
/// output.
case wrongProduction(actual: OutputType, expected: OutputType)

/// The `errorCode` will be set to `overProduction` when a decoding result was yielded where
/// nothing was expected.
case overProduction(OutputType)

/// The `errorCode` will be set to `underProduction` when a decoder didn't yield output when output was
/// expected. The expected output is delivered as the associated value.
case underProduction(OutputType)

/// The `errorCode` will be set to `leftOversOnDeconstructionChannel` if there were left-over items
/// in the `Channel` on deconstruction. This usually means that your `ByteToMessageDecoder` did not process
/// certain items.
case leftOversOnDeconstructingChannel(inbound: [NIOAny], outbound: [NIOAny], pendingOutbound: [NIOAny])
}
}
}
@@ -26,6 +26,7 @@ import XCTest
@testable import NIOConcurrencyHelpersTests
@testable import NIOHTTP1Tests
@testable import NIOTLSTests
@testable import NIOTestUtilsTests
@testable import NIOTests
@testable import NIOWebSocketTests

@@ -40,6 +41,7 @@ import XCTest
testCase(ByteBufferTest.allTests),
testCase(ByteBufferUtilsTest.allTests),
testCase(ByteToMessageDecoderTest.allTests),
testCase(ByteToMessageDecoderVerifierTest.allTests),
testCase(ChannelNotificationTest.allTests),
testCase(ChannelOptionStorageTest.allTests),
testCase(ChannelPipelineTest.allTests),
@@ -45,6 +45,7 @@ extension HTTPDecoderTest {
("testNonASCIIWorksAsHeaderValue", testNonASCIIWorksAsHeaderValue),
("testDoesNotDeliverLeftoversUnnecessarily", testDoesNotDeliverLeftoversUnnecessarily),
("testHTTPResponseWithoutHeaders", testHTTPResponseWithoutHeaders),
("testBasicVerifications", testBasicVerifications),
]
}
}
@@ -15,6 +15,7 @@
import XCTest
import NIO
import NIOHTTP1
import NIOTestUtils

class HTTPDecoderTest: XCTestCase {
private var channel: EmbeddedChannel!
@@ -547,4 +548,49 @@ class HTTPDecoderTest: XCTestCase {
XCTAssertNoThrow(XCTAssertEqual(HTTPClientResponsePart.head(.init(version: .init(major: 1, minor: 0),
status: .ok)), try channel.readInbound()))
}

func testBasicVerifications() {
let byteBufferContainingJustAnX: ByteBuffer = {
var buffer = ByteBufferAllocator().buffer(capacity: 1)
buffer.writeString("X")
return buffer
}()
let expectedInOuts: [(String, [HTTPServerRequestPart])] = [
("GET / HTTP/1.1\r\n\r\n",
[.head(.init(version: .init(major: 1, minor: 1), method: .GET, uri: "/")),
.end(nil)]),
("POST /foo HTTP/1.1\r\n\r\n",
[.head(.init(version: .init(major: 1, minor: 1), method: .POST, uri: "/foo")),
.end(nil)]),
("POST / HTTP/1.1\r\ncontent-length: 1\r\n\r\nX",
[.head(.init(version: .init(major: 1, minor: 1),
method: .POST,
uri: "/",
headers: .init([("content-length", "1")]))),
.body(byteBufferContainingJustAnX),
.end(nil)]),
("POST / HTTP/1.1\r\ntransfer-encoding: chunked\r\n\r\n1\r\nX\r\n0\r\n\r\n",
[.head(.init(version: .init(major: 1, minor: 1),
method: .POST,
uri: "/",
headers: .init([("transfer-encoding", "chunked")]))),
.body(byteBufferContainingJustAnX),
.end(nil)]),
("POST / HTTP/1.1\r\ntransfer-encoding: chunked\r\none: two\r\n\r\n1\r\nX\r\n0\r\nfoo: bar\r\n\r\n",
[.head(.init(version: .init(major: 1, minor: 1),
method: .POST,
uri: "/",
headers: .init([("transfer-encoding", "chunked"), ("one", "two")]))),
.body(byteBufferContainingJustAnX),
.end(.init([("foo", "bar")]))]),
]

let expectedInOutsBB: [(ByteBuffer, [HTTPServerRequestPart])] = expectedInOuts.map { io in
var buffer = ByteBufferAllocator().buffer(capacity: io.0.utf8.count)
buffer.writeString(io.0)
return (buffer, io.1)
}
XCTAssertNoThrow(try ByteToMessageDecoderVerifier.verifyDecoder(inputOutputPairs: expectedInOutsBB,
decoderFactory: { HTTPRequestDecoder() }))
}
}
@@ -0,0 +1,36 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
//
// ByteToMessageDecoderVerifierTest+XCTest.swift
//
import XCTest

///
/// NOTE: This file was generated by generate_linux_tests.rb
///
/// Do NOT edit this file directly as it will be regenerated automatically when needed.
///
extension ByteToMessageDecoderVerifierTest {

static var allTests : [(String, (ByteToMessageDecoderVerifierTest) -> () throws -> Void)] {
return [
("testWrongResults", testWrongResults),
("testNoOutputWhenWeShouldHaveOutput", testNoOutputWhenWeShouldHaveOutput),
("testOutputWhenWeShouldNotProduceOutput", testOutputWhenWeShouldNotProduceOutput),
("testLeftovers", testLeftovers),
]
}
}

Oops, something went wrong.

0 comments on commit 5513bb2

Please sign in to comment.
You can’t perform that action at this time.