Make ByteBufferView mutable (#1208)

Motivation:

ByteBufferView isn't a mutable collection, but it probably should be.

Modifications:

- Add `copyBytes(at:to:length)` to `ByteBuffer` to copy bytes from a
  readable region of a buffer to another part of the buffer
- Conform `ByteBufferView` to `MutableCollection`, `RangeReplaceableCollection`
  and `MutableDataProtocol`
- Add an allocation counting test

Result:

`ByteBufferView` is now mutable.
This commit is contained in:
George Barnett 2019-11-27 10:25:15 +00:00 committed by Cory Benfield
parent 7d8e6b2323
commit 302dee3e1a
11 changed files with 474 additions and 24 deletions

View File

@ -0,0 +1,40 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2017-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
func run(identifier: String) {
let allocator = ByteBufferAllocator()
let data = Array(repeating: UInt8(0), count: 1024)
measure(identifier: identifier) {
var count = 0
for _ in 0..<1_000 {
var buffer = allocator.buffer(capacity: data.count)
buffer.writeBytes(data)
var view = ByteBufferView(buffer)
// Unfortunately this CoWs: https://bugs.swift.org/browse/SR-11675
view[0] = 42
view.replaceSubrange(0..<4, with: [0x0, 0x1, 0x2, 0x3])
var modified = ByteBuffer(view)
modified.setBytes([0xa, 0xb, 0xc], at: modified.readerIndex)
count &+= modified.readableBytes
}
return count
}
}

View File

@ -284,7 +284,7 @@ public struct ByteBuffer {
} }
} }
private mutating func _copyStorageAndRebase(capacity: _Capacity, resetIndices: Bool = false) { @usableFromInline mutating func _copyStorageAndRebase(capacity: _Capacity, resetIndices: Bool = false) {
let indexRebaseAmount = resetIndices ? self._readerIndex : 0 let indexRebaseAmount = resetIndices ? self._readerIndex : 0
let storageRebaseAmount = self._slice.lowerBound + indexRebaseAmount let storageRebaseAmount = self._slice.lowerBound + indexRebaseAmount
let newSlice = storageRebaseAmount ..< min(storageRebaseAmount + _toCapacity(self._slice.count), self._slice.upperBound, storageRebaseAmount + capacity) let newSlice = storageRebaseAmount ..< min(storageRebaseAmount + _toCapacity(self._slice.count), self._slice.upperBound, storageRebaseAmount + capacity)
@ -359,11 +359,15 @@ public struct ByteBuffer {
let extraCapacity = newEndIndex > self._slice.upperBound ? newEndIndex - self._slice.upperBound : 0 let extraCapacity = newEndIndex > self._slice.upperBound ? newEndIndex - self._slice.upperBound : 0
self._copyStorageAndRebase(extraCapacity: extraCapacity) self._copyStorageAndRebase(extraCapacity: extraCapacity)
} }
self._ensureAvailableCapacity(_Capacity(bytesCount), at: index) self._ensureAvailableCapacity(_Capacity(bytesCount), at: index)
self._setBytesAssumingUniqueBufferAccess(bytes, at: index)
return _toCapacity(bytesCount)
}
@inlinable
mutating func _setBytesAssumingUniqueBufferAccess(_ bytes: UnsafeRawBufferPointer, at index: _Index) {
let targetPtr = UnsafeMutableRawBufferPointer(rebasing: self._slicedStorageBuffer.dropFirst(Int(index))) let targetPtr = UnsafeMutableRawBufferPointer(rebasing: self._slicedStorageBuffer.dropFirst(Int(index)))
targetPtr.copyMemory(from: bytes) targetPtr.copyMemory(from: bytes)
return _toCapacity(Int(bytesCount))
} }
@inline(never) @inline(never)
@ -733,14 +737,14 @@ extension ByteBuffer: CustomStringConvertible {
/* change types to the user visible `Int` */ /* change types to the user visible `Int` */
extension ByteBuffer { extension ByteBuffer {
/// Copy the collection of `bytes` into the `ByteBuffer` at `index`. /// Copy the collection of `bytes` into the `ByteBuffer` at `index`. Does not move the writer index.
@discardableResult @discardableResult
@inlinable @inlinable
public mutating func setBytes<Bytes: Sequence>(_ bytes: Bytes, at index: Int) -> Int where Bytes.Element == UInt8 { public mutating func setBytes<Bytes: Sequence>(_ bytes: Bytes, at index: Int) -> Int where Bytes.Element == UInt8 {
return Int(self._setBytes(bytes, at: _toIndex(index))) return Int(self._setBytes(bytes, at: _toIndex(index)))
} }
/// Copy `bytes` into the `ByteBuffer` at `index`. /// Copy `bytes` into the `ByteBuffer` at `index`. Does not move the writer index.
@discardableResult @discardableResult
@inlinable @inlinable
public mutating func setBytes(_ bytes: UnsafeRawBufferPointer, at index: Int) -> Int { public mutating func setBytes(_ bytes: UnsafeRawBufferPointer, at index: Int) -> Int {
@ -800,6 +804,69 @@ extension ByteBuffer {
} }
} }
extension ByteBuffer {
/// Copies `length` `bytes` starting at the `fromIndex` to `toIndex`. Does not move the writer index.
///
/// - Note: Overlapping ranges, for example `copyBytes(at: 1, to: 2, length: 5)` are allowed.
/// - Precondition: The range represented by `fromIndex` and `length` must be readable bytes,
/// that is: `fromIndex >= readerIndex` and `fromIndex + length <= writerIndex`.
/// - Parameter fromIndex: The index of the first byte to copy.
/// - Parameter toIndex: The index into to which the first byte will be copied.
/// - Parameter length: The number of bytes which should be copied.
@discardableResult
@inlinable
public mutating func copyBytes(at fromIndex: Int, to toIndex: Int, length: Int) throws -> Int {
switch length {
case ..<0:
throw CopyBytesError.negativeLength
case 0:
return 0
default:
()
}
guard self.readerIndex <= fromIndex && fromIndex + length <= self.writerIndex else {
throw CopyBytesError.unreadableSourceBytes
}
if !isKnownUniquelyReferenced(&self._storage) {
let newEndIndex = max(self._writerIndex, _toIndex(toIndex + length))
self._copyStorageAndRebase(capacity: newEndIndex)
}
self._ensureAvailableCapacity(_Capacity(length), at: _toIndex(toIndex))
self.withVeryUnsafeBytes { ptr in
let srcPtr = UnsafeRawBufferPointer(start: ptr.baseAddress!.advanced(by: fromIndex), count: length)
self._setBytesAssumingUniqueBufferAccess(srcPtr, at: _toIndex(toIndex))
}
return length
}
/// Errors thrown when calling `copyBytes`.
public struct CopyBytesError: Error {
private enum BaseError: Hashable {
case negativeLength
case unreadableSourceBytes
}
private var baseError: BaseError
/// The length of the bytes to copy was negative.
public static let negativeLength: CopyBytesError = .init(baseError: .negativeLength)
/// The bytes to copy are not readable.
public static let unreadableSourceBytes: CopyBytesError = .init(baseError: .unreadableSourceBytes)
}
}
extension ByteBuffer.CopyBytesError: Hashable { }
extension ByteBuffer.CopyBytesError: CustomDebugStringConvertible {
public var debugDescription: String {
return String(describing: self.baseError)
}
}
extension ByteBuffer: Equatable { extension ByteBuffer: Equatable {
// TODO: I don't think this makes sense. This should compare bytes 0..<writerIndex instead. // TODO: I don't think this makes sense. This should compare bytes 0..<writerIndex instead.

View File

@ -12,7 +12,7 @@
// //
//===----------------------------------------------------------------------===// //===----------------------------------------------------------------------===//
/// A read-only view into a portion of a `ByteBuffer`. /// A view into a portion of a `ByteBuffer`.
/// ///
/// A `ByteBufferView` is useful whenever a `Collection where Element == UInt8` representing a portion of a /// A `ByteBufferView` is useful whenever a `Collection where Element == UInt8` representing a portion of a
/// `ByteBuffer` is needed. /// `ByteBuffer` is needed.
@ -21,28 +21,33 @@ public struct ByteBufferView: RandomAccessCollection {
public typealias Index = Int public typealias Index = Int
public typealias SubSequence = ByteBufferView public typealias SubSequence = ByteBufferView
fileprivate let buffer: ByteBuffer /* private but usableFromInline */ @usableFromInline var _buffer: ByteBuffer
fileprivate let range: Range<Index> /* private but usableFromInline */ @usableFromInline var _range: Range<Index>
internal init(buffer: ByteBuffer, range: Range<Index>) { internal init(buffer: ByteBuffer, range: Range<Index>) {
precondition(range.lowerBound >= 0 && range.upperBound <= buffer.capacity) precondition(range.lowerBound >= 0 && range.upperBound <= buffer.capacity)
self.buffer = buffer self._buffer = buffer
self.range = range self._range = range
}
/// Creates a `ByteBufferView` from the readable bytes of the given `buffer`.
public init(_ buffer: ByteBuffer) {
self = ByteBufferView(buffer: buffer, range: buffer.readerIndex ..< buffer.writerIndex)
} }
public func withUnsafeBytes<R>(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { public func withUnsafeBytes<R>(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R {
return try self.buffer.withVeryUnsafeBytes { ptr in return try self._buffer.withVeryUnsafeBytes { ptr in
try body(UnsafeRawBufferPointer.init(start: ptr.baseAddress!.advanced(by: self.range.lowerBound), try body(UnsafeRawBufferPointer(start: ptr.baseAddress!.advanced(by: self._range.lowerBound),
count: self.range.count)) count: self._range.count))
} }
} }
public var startIndex: Index { public var startIndex: Index {
return self.range.lowerBound return self._range.lowerBound
} }
public var endIndex: Index { public var endIndex: Index {
return self.range.upperBound return self._range.upperBound
} }
public func index(after i: Index) -> Index { public func index(after i: Index) -> Index {
@ -50,14 +55,27 @@ public struct ByteBufferView: RandomAccessCollection {
} }
public subscript(position: Index) -> UInt8 { public subscript(position: Index) -> UInt8 {
guard position >= self.range.lowerBound && position < self.range.upperBound else { get {
preconditionFailure("index \(position) out of range") guard position >= self._range.lowerBound && position < self._range.upperBound else {
preconditionFailure("index \(position) out of range")
}
return self._buffer.getInteger(at: position)! // range check above
}
set {
guard position >= self._range.lowerBound && position < self._range.upperBound else {
preconditionFailure("index \(position) out of range")
}
self._buffer.setInteger(newValue, at: position)
} }
return self.buffer.getInteger(at: position)! // range check above
} }
public subscript(range: Range<Index>) -> ByteBufferView { public subscript(range: Range<Index>) -> ByteBufferView {
return ByteBufferView(buffer: self.buffer, range: range) get {
return ByteBufferView(buffer: self._buffer, range: range)
}
set {
self.replaceSubrange(range, with: newValue)
}
} }
@inlinable @inlinable
@ -68,10 +86,58 @@ public struct ByteBufferView: RandomAccessCollection {
} }
} }
extension ByteBufferView: MutableCollection {}
extension ByteBufferView: RangeReplaceableCollection {
// required by `RangeReplaceableCollection`
public init() {
self = ByteBufferView(ByteBufferAllocator().buffer(capacity: 0))
}
@inlinable
public mutating func replaceSubrange<C: Collection>(_ subrange: Range<Index>, with newElements: C) where ByteBufferView.Element == C.Element {
precondition(subrange.startIndex >= self.startIndex && subrange.endIndex <= self.endIndex,
"subrange out of bounds")
if newElements.count == subrange.count {
self._buffer.setBytes(newElements, at: subrange.startIndex)
} else if newElements.count < subrange.count {
// Replace the subrange.
self._buffer.setBytes(newElements, at: subrange.startIndex)
// Remove the unwanted bytes between the newly copied bytes and the end of the subrange.
// try! is fine here: the copied range is within the view and the length can't be negative.
try! self._buffer.copyBytes(at: subrange.endIndex,
to: subrange.startIndex.advanced(by: newElements.count),
length: subrange.endIndex.distance(to: self._buffer.writerIndex))
// Shorten the range.
let removedBytes = subrange.count - newElements.count
self._buffer.moveWriterIndex(to: self._buffer.writerIndex - removedBytes)
self._range = self._range.dropLast(removedBytes)
} else {
// Make space for the new elements.
// try! is fine here: the copied range is within the view and the length can't be negative.
try! self._buffer.copyBytes(at: subrange.endIndex,
to: subrange.startIndex.advanced(by: newElements.count),
length: subrange.endIndex.distance(to: self._buffer.writerIndex))
// Replace the bytes.
self._buffer.setBytes(newElements, at: subrange.startIndex)
// Widen the range.
let additionalByteCount = newElements.count - subrange.count
self._buffer.moveWriterIndex(forwardBy: additionalByteCount)
self._range = self._range.startIndex ..< self._range.endIndex.advanced(by: additionalByteCount)
}
}
}
extension ByteBuffer { extension ByteBuffer {
/// A view into the readable bytes of the `ByteBuffer`. /// A view into the readable bytes of the `ByteBuffer`.
public var readableBytesView: ByteBufferView { public var readableBytesView: ByteBufferView {
return ByteBufferView(buffer: self, range: self.readerIndex ..< self.readerIndex + self.readableBytes) return ByteBufferView(self)
} }
/// Returns a view into some portion of the readable bytes of a `ByteBuffer`. /// Returns a view into some portion of the readable bytes of a `ByteBuffer`.
@ -92,6 +158,6 @@ extension ByteBuffer {
/// ///
/// - parameter view: The `ByteBufferView` which you want to get a `ByteBuffer` from. /// - parameter view: The `ByteBufferView` which you want to get a `ByteBuffer` from.
public init(_ view: ByteBufferView) { public init(_ view: ByteBufferView) {
self = view.buffer.getSlice(at: view.range.startIndex, length: view.range.count)! self = view._buffer.getSlice(at: view.startIndex, length: view.count)!
} }
} }

View File

@ -210,3 +210,5 @@ extension ByteBufferView: DataProtocol {
return .init(self) return .init(self)
} }
} }
extension ByteBufferView: MutableDataProtocol {}

View File

@ -0,0 +1,35 @@
//===----------------------------------------------------------------------===//
//
// 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 Foundation
import XCTest
import NIO
import NIOFoundationCompat
class ByteBufferViewDataProtocolTests: XCTestCase {
func testResetBytes() {
var view = ByteBufferView()
view.resetBytes(in: view.indices)
XCTAssertTrue(view.elementsEqual([]))
view.replaceSubrange(view.indices, with: [1, 2, 3, 4, 5])
view.resetBytes(in: 0..<2)
XCTAssertTrue(view.elementsEqual([0, 0, 3, 4, 5]))
view.resetBytes(in: 2...4)
XCTAssertTrue(view.elementsEqual([0, 0, 0, 0, 0]))
}
}

View File

@ -85,6 +85,13 @@ extension ByteBufferTest {
("testWriteBuffer", testWriteBuffer), ("testWriteBuffer", testWriteBuffer),
("testMisalignedIntegerRead", testMisalignedIntegerRead), ("testMisalignedIntegerRead", testMisalignedIntegerRead),
("testSetAndWriteBytes", testSetAndWriteBytes), ("testSetAndWriteBytes", testSetAndWriteBytes),
("testCopyBytesWithNegativeLength", testCopyBytesWithNegativeLength),
("testCopyBytesNonReadable", testCopyBytesNonReadable),
("testCopyBytes", testCopyBytes),
("testCopyZeroBytesOutOfBoundsIsOk", testCopyZeroBytesOutOfBoundsIsOk),
("testCopyBytesBeyondWriterIndex", testCopyBytesBeyondWriterIndex),
("testCopyBytesOverSelf", testCopyBytesOverSelf),
("testCopyBytesCoWs", testCopyBytesCoWs),
("testWriteABunchOfCollections", testWriteABunchOfCollections), ("testWriteABunchOfCollections", testWriteABunchOfCollections),
("testSetABunchOfCollections", testSetABunchOfCollections), ("testSetABunchOfCollections", testSetABunchOfCollections),
("testTryStringTooLong", testTryStringTooLong), ("testTryStringTooLong", testTryStringTooLong),
@ -131,6 +138,15 @@ extension ByteBufferTest {
("testViewsStartIndexIsStable", testViewsStartIndexIsStable), ("testViewsStartIndexIsStable", testViewsStartIndexIsStable),
("testSlicesOfByteBufferViewsAreByteBufferViews", testSlicesOfByteBufferViewsAreByteBufferViews), ("testSlicesOfByteBufferViewsAreByteBufferViews", testSlicesOfByteBufferViewsAreByteBufferViews),
("testReadableBufferViewRangeEqualCapacity", testReadableBufferViewRangeEqualCapacity), ("testReadableBufferViewRangeEqualCapacity", testReadableBufferViewRangeEqualCapacity),
("testBufferViewCoWs", testBufferViewCoWs),
("testBufferViewMutationViaSubscriptIndex", testBufferViewMutationViaSubscriptIndex),
("testBufferViewReplaceBeyondEndOfRange", testBufferViewReplaceBeyondEndOfRange),
("testBufferViewReplaceWithSubrangeOfSelf", testBufferViewReplaceWithSubrangeOfSelf),
("testBufferViewMutationViaSubscriptRange", testBufferViewMutationViaSubscriptRange),
("testBufferViewReplaceSubrangeWithEqualLengthBytes", testBufferViewReplaceSubrangeWithEqualLengthBytes),
("testBufferViewReplaceSubrangeWithFewerBytes", testBufferViewReplaceSubrangeWithFewerBytes),
("testBufferViewReplaceSubrangeWithMoreBytes", testBufferViewReplaceSubrangeWithMoreBytes),
("testBufferViewEmpty", testBufferViewEmpty),
("testByteBuffersCanBeInitializedFromByteBufferViews", testByteBuffersCanBeInitializedFromByteBufferViews), ("testByteBuffersCanBeInitializedFromByteBufferViews", testByteBuffersCanBeInitializedFromByteBufferViews),
("testReserveCapacityWhenOversize", testReserveCapacityWhenOversize), ("testReserveCapacityWhenOversize", testReserveCapacityWhenOversize),
("testReserveCapacitySameCapacity", testReserveCapacitySameCapacity), ("testReserveCapacitySameCapacity", testReserveCapacitySameCapacity),

View File

@ -836,6 +836,86 @@ class ByteBufferTest: XCTestCase {
XCTAssertEqual(Array(repeating: str, count: 3).joined(), actualString) XCTAssertEqual(Array(repeating: str, count: 3).joined(), actualString)
} }
func testCopyBytesWithNegativeLength() {
self.buf.writeBytes([0x0, 0x1])
XCTAssertThrowsError(try self.buf.copyBytes(at: self.buf.readerIndex, to: self.buf.readerIndex + 1, length: -1)) {
XCTAssertEqual($0 as? ByteBuffer.CopyBytesError, .negativeLength)
}
}
func testCopyBytesNonReadable() {
let oldReaderIndex = self.buf.readerIndex
self.buf.writeBytes([0x0, 0x1, 0x2])
// Partially consume the bytes.
self.buf.moveReaderIndex(forwardBy: 2)
// Copy two read bytes.
XCTAssertThrowsError(try self.buf.copyBytes(at: oldReaderIndex, to: self.buf.writerIndex, length: 2)) {
XCTAssertEqual($0 as? ByteBuffer.CopyBytesError, .unreadableSourceBytes)
}
// Copy one read byte and one readable byte.
XCTAssertThrowsError(try self.buf.copyBytes(at: oldReaderIndex + 1, to: self.buf.writerIndex, length: 2)) {
XCTAssertEqual($0 as? ByteBuffer.CopyBytesError, .unreadableSourceBytes)
}
// Copy one readable byte and one uninitialized byte.
XCTAssertThrowsError(try self.buf.copyBytes(at: oldReaderIndex + 3, to: self.buf.writerIndex, length: 2)) {
XCTAssertEqual($0 as? ByteBuffer.CopyBytesError, .unreadableSourceBytes)
}
// Copy two uninitialized bytes.
XCTAssertThrowsError(try self.buf.copyBytes(at: self.buf.writerIndex, to: oldReaderIndex, length: 2)) {
XCTAssertEqual($0 as? ByteBuffer.CopyBytesError, .unreadableSourceBytes)
}
}
func testCopyBytes() throws {
self.buf.writeBytes([0, 1, 2, 3])
XCTAssertNoThrow(try self.buf.copyBytes(at: self.buf.readerIndex, to: self.buf.readerIndex + 2, length: 2))
XCTAssertEqual(self.buf.readableBytes, 4)
XCTAssertEqual(self.buf.readBytes(length: self.buf.readableBytes), [0, 1, 0, 1])
}
func testCopyZeroBytesOutOfBoundsIsOk() throws {
XCTAssertEqual(try self.buf.copyBytes(at: self.buf.writerIndex, to: self.buf.writerIndex + 42, length: 0), 0)
}
func testCopyBytesBeyondWriterIndex() throws {
self.buf.writeBytes([0, 1, 2, 3])
// Write beyond the writerIndex
XCTAssertNoThrow(try self.buf.copyBytes(at: self.buf.readerIndex, to: self.buf.readerIndex + 4, length: 2))
XCTAssertEqual(self.buf.readableBytes, 4)
XCTAssertEqual(self.buf.readBytes(length: 2), [0, 1])
XCTAssertNotNil(self.buf.readBytes(length: 2)) // could be anything!
self.buf.moveWriterIndex(forwardBy: 2) // We know these have been written.
XCTAssertEqual(self.buf.readBytes(length: 2), [0, 1])
}
func testCopyBytesOverSelf() throws {
self.buf.writeBytes([0, 1, 2, 3])
XCTAssertNoThrow(try self.buf.copyBytes(at: self.buf.readerIndex, to: self.buf.readerIndex + 1, length: 3))
XCTAssertEqual(self.buf.readableBytes, 4)
XCTAssertEqual(self.buf.readBytes(length: self.buf.readableBytes), [0, 0, 1, 2])
self.buf.writeBytes([0, 1, 2, 3])
XCTAssertNoThrow(try self.buf.copyBytes(at: self.buf.readerIndex + 1, to: self.buf.readerIndex, length: 3))
XCTAssertEqual(self.buf.readableBytes, 4)
XCTAssertEqual(self.buf.readBytes(length: self.buf.readableBytes), [1, 2, 3, 3])
}
func testCopyBytesCoWs() throws {
let bytes: [UInt8] = (0..<self.buf.writableBytes).map { UInt8($0 % Int(UInt8.max)) }
self.buf.writeBytes(bytes)
var otherBuf = self.buf!
XCTAssertEqual(self.buf.writableBytes, 0)
XCTAssertNoThrow(try self.buf.copyBytes(at: self.buf.readerIndex, to: self.buf.readerIndex + 2, length: 1))
XCTAssertNotEqual(self.buf.readBytes(length: self.buf.readableBytes),
otherBuf.readBytes(length: otherBuf.readableBytes))
}
func testWriteABunchOfCollections() throws { func testWriteABunchOfCollections() throws {
let overallData = "0123456789abcdef".data(using: .utf8)! let overallData = "0123456789abcdef".data(using: .utf8)!
buf.writeBytes("0123".utf8) buf.writeBytes("0123".utf8)
@ -1669,6 +1749,148 @@ class ByteBufferTest: XCTestCase {
XCTAssertEqual(buf.readableBytes, viewSlice.count) XCTAssertEqual(buf.readableBytes, viewSlice.count)
} }
func testBufferViewCoWs() throws {
self.buf.writeBytes([0x0, 0x1, 0x2])
var view = self.buf.readableBytesView
view.replaceSubrange(view.indices, with: [0xa, 0xb, 0xc])
XCTAssertEqual(self.buf.readBytes(length: 3), [0x0, 0x1, 0x2])
XCTAssertTrue(view.elementsEqual([0xa, 0xb, 0xc]))
self.buf.writeBytes([0x0, 0x1, 0x2])
view = self.buf.readableBytesView
view.replaceSubrange(view.indices, with: [0xa, 0xb])
XCTAssertEqual(self.buf.readBytes(length: 3), [0x0, 0x1, 0x2])
XCTAssertTrue(view.elementsEqual([0xa, 0xb]))
self.buf.writeBytes([0x0, 0x1, 0x2])
view = self.buf.readableBytesView
view.replaceSubrange(view.indices, with: [0xa, 0xb, 0xc, 0xd])
XCTAssertEqual(self.buf.readBytes(length: 3), [0x0, 0x1, 0x2])
XCTAssertTrue(view.elementsEqual([0xa, 0xb, 0xc, 0xd]))
}
func testBufferViewMutationViaSubscriptIndex() throws {
self.buf.writeBytes([0x0, 0x1, 0x2])
var view = self.buf.readableBytesView
view[0] = 0xa
view[1] = 0xb
view[2] = 0xc
XCTAssertTrue(view.elementsEqual([0xa, 0xb, 0xc]))
}
func testBufferViewReplaceBeyondEndOfRange() throws {
self.buf.writeBytes([1, 2, 3])
var view = self.buf.readableBytesView
view.replaceSubrange(2..<3, with: [2, 3, 4])
XCTAssertTrue(view.elementsEqual([1, 2, 2, 3, 4]))
}
func testBufferViewReplaceWithSubrangeOfSelf() throws {
let oneToNine: [UInt8] = (1...9).map { $0 }
self.buf.writeBytes(oneToNine)
var view = self.buf.readableBytesView
view[6..<9] = view[0..<3]
XCTAssertTrue(view.elementsEqual([1, 2, 3, 4, 5, 6, 1, 2, 3]))
view[0..<3] = view[1..<4]
XCTAssertTrue(view.elementsEqual([2, 3, 4, 4, 5, 6, 1, 2, 3]))
view[1..<4] = view[0..<3]
XCTAssertTrue(view.elementsEqual([2, 2, 3, 4, 5, 6, 1, 2, 3]))
}
func testBufferViewMutationViaSubscriptRange() throws {
let oneToNine: [UInt8] = (1...9).map { $0 }
var oneToNineBuffer = self.allocator.buffer(capacity: 9)
oneToNineBuffer.writeBytes(oneToNine)
let oneToNineView = oneToNineBuffer.readableBytesView
self.buf.writeBytes(Array(repeating: UInt8(0), count: 9))
var view = self.buf.readableBytesView
view[0..<3] = oneToNineView[0..<3]
XCTAssertTrue(view.elementsEqual([1, 2, 3, 0, 0, 0, 0, 0, 0]))
// Replace with shorter range
view[3..<6] = oneToNineView[3..<5]
XCTAssertTrue(view.elementsEqual([1, 2, 3, 4, 5, 0, 0, 0]))
// Replace with longer range
view[5..<8] = oneToNineView[5..<9]
XCTAssertTrue(view.elementsEqual(oneToNine))
}
func testBufferViewReplaceSubrangeWithEqualLengthBytes() throws {
self.buf.writeBytes([0x0, 0x1, 0x2, 0x3, 0x4])
var view = ByteBufferView(self.buf)
XCTAssertEqual(view.count, self.buf.readableBytes)
view.replaceSubrange(view.indices.suffix(3), with: [0xd, 0xe, 0xf])
var modifiedBuf = ByteBuffer(view)
XCTAssertEqual(self.buf.readerIndex, modifiedBuf.readerIndex)
XCTAssertEqual(self.buf.writerIndex, modifiedBuf.writerIndex)
XCTAssertEqual([0x0, 0x1, 0xd, 0xe, 0xf], modifiedBuf.readBytes(length: modifiedBuf.readableBytes)!)
}
func testBufferViewReplaceSubrangeWithFewerBytes() throws {
self.buf.writeBytes([0x0, 0x1, 0x2, 0x3, 0x4])
var view = ByteBufferView(self.buf)
view.replaceSubrange(view.indices.suffix(3), with: [0xd])
var modifiedBuf = ByteBuffer(view)
XCTAssertEqual(self.buf.readerIndex, modifiedBuf.readerIndex)
XCTAssertEqual(self.buf.writerIndex - 2, modifiedBuf.writerIndex)
XCTAssertEqual([0x0, 0x1, 0xd], modifiedBuf.readBytes(length: modifiedBuf.readableBytes)!)
}
func testBufferViewReplaceSubrangeWithMoreBytes() throws {
self.buf.writeBytes([0x0, 0x1, 0x2, 0x3])
var view = ByteBufferView(self.buf)
XCTAssertTrue(view.elementsEqual([0x0, 0x1, 0x2, 0x3]))
view.replaceSubrange(view.indices.suffix(1), with: [0xa, 0xb])
XCTAssertTrue(view.elementsEqual([0x0, 0x1, 0x2, 0xa, 0xb]))
var modifiedBuf = ByteBuffer(view)
XCTAssertEqual(self.buf.readerIndex, modifiedBuf.readerIndex)
XCTAssertEqual(self.buf.writerIndex + 1, modifiedBuf.writerIndex)
XCTAssertEqual([0x0, 0x1, 0x2, 0xa, 0xb], modifiedBuf.readBytes(length: modifiedBuf.readableBytes)!)
}
func testBufferViewEmpty() throws {
self.buf.writeBytes([0, 1, 2])
var view = ByteBufferView()
view[0..<0] = self.buf.readableBytesView
XCTAssertEqual(view.indices, 0..<3)
self.buf = ByteBuffer(view)
XCTAssertEqual(self.buf.readerIndex, 0)
XCTAssertEqual(self.buf.writerIndex, 3)
let anotherBuf = self.buf!
XCTAssertEqual([0, 1, 2], self.buf.readBytes(length: self.buf.readableBytes))
var anotherView = anotherBuf.readableBytesView
anotherView.replaceSubrange(0..<3, with: [])
XCTAssertTrue(anotherView.isEmpty)
self.buf = ByteBuffer(anotherView)
XCTAssertEqual(self.buf.readerIndex, 0)
XCTAssertEqual(self.buf.writerIndex, 0)
XCTAssertEqual([], self.buf.readBytes(length: self.buf.readableBytes))
}
func testByteBuffersCanBeInitializedFromByteBufferViews() throws { func testByteBuffersCanBeInitializedFromByteBufferViews() throws {
self.buf.writeString("hello") self.buf.writeString("hello")

View File

@ -414,7 +414,7 @@ public final class ByteToMessageDecoderTest: XCTestCase {
String(decoding: $0.readableBytesView, as: Unicode.UTF8.self) String(decoding: $0.readableBytesView, as: Unicode.UTF8.self)
})) }))
XCTAssertNoThrow(XCTAssertEqual("56", try channel.readInbound(as: ByteBuffer.self).map { XCTAssertNoThrow(XCTAssertEqual("56", try channel.readInbound(as: ByteBuffer.self).map {
String(decoding: $0.readableBytesView, as: Unicode.UTF8.self) String(decoding: $0.readableBytesView, as: Unicode.UTF8.self)
})) }))
XCTAssertNoThrow(XCTAssertEqual("78", try channel.readInbound(as: ByteBuffer.self).map { XCTAssertNoThrow(XCTAssertEqual("78", try channel.readInbound(as: ByteBuffer.self).map {
String(decoding: $0.readableBytesView, as: Unicode.UTF8.self) String(decoding: $0.readableBytesView, as: Unicode.UTF8.self)

View File

@ -83,12 +83,12 @@ public final class WebSocketFrameDecoderTest: XCTestCase {
while let d = try self.encoderChannel.readOutbound(as: ByteBuffer.self) { while let d = try self.encoderChannel.readOutbound(as: ByteBuffer.self) {
XCTAssertNoThrow(try self.decoderChannel.writeInbound(d)) XCTAssertNoThrow(try self.decoderChannel.writeInbound(d))
} }
guard let producedFrame: WebSocketFrame = try self.decoderChannel.readInbound() else { guard let producedFrame: WebSocketFrame = try self.decoderChannel.readInbound() else {
XCTFail("Did not produce a frame") XCTFail("Did not produce a frame")
return nil return nil
} }
// Should only have gotten one frame! // Should only have gotten one frame!
XCTAssertNoThrow(XCTAssertNil(try self.decoderChannel.readInbound(as: WebSocketFrame.self))) XCTAssertNoThrow(XCTAssertNil(try self.decoderChannel.readInbound(as: WebSocketFrame.self)))
return producedFrame return producedFrame

View File

@ -31,6 +31,7 @@ services:
- MAX_ALLOCS_ALLOWED_encode_1000_ws_frames_new_buffer=4010 - MAX_ALLOCS_ALLOWED_encode_1000_ws_frames_new_buffer=4010
- MAX_ALLOCS_ALLOWED_encode_1000_ws_frames_new_buffer_with_space=4010 - MAX_ALLOCS_ALLOWED_encode_1000_ws_frames_new_buffer_with_space=4010
- MAX_ALLOCS_ALLOWED_decode_1000_ws_frames=1000 - MAX_ALLOCS_ALLOWED_decode_1000_ws_frames=1000
- MAX_ALLOCS_ALLOWED_modifying_byte_buffer_view=6010
- SANITIZER_ARG=--sanitize=thread - SANITIZER_ARG=--sanitize=thread
performance-test: performance-test:

View File

@ -31,6 +31,7 @@ services:
- MAX_ALLOCS_ALLOWED_encode_1000_ws_frames_new_buffer=4010 - MAX_ALLOCS_ALLOWED_encode_1000_ws_frames_new_buffer=4010
- MAX_ALLOCS_ALLOWED_encode_1000_ws_frames_new_buffer_with_space=4010 - MAX_ALLOCS_ALLOWED_encode_1000_ws_frames_new_buffer_with_space=4010
- MAX_ALLOCS_ALLOWED_decode_1000_ws_frames=1000 - MAX_ALLOCS_ALLOWED_decode_1000_ws_frames=1000
- MAX_ALLOCS_ALLOWED_modifying_byte_buffer_view=6010
performance-test: performance-test:
image: swift-nio:18.04-5.0 image: swift-nio:18.04-5.0