Skip to content

Commit

Permalink
Test cleanup WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
karwa committed Jan 29, 2023
1 parent 461bd3c commit 6ac78af
Show file tree
Hide file tree
Showing 7 changed files with 2,319 additions and 1,459 deletions.
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ let package = Package(
.testTarget(
name: "WebURLTests",
dependencies: ["WebURL", "WebURLTestSupport", "Checkit"],
exclude: ["KeyValuePairsTests.swift"]
exclude: ["KeyValuePairs"]
),
.testTarget(
name: "WebURLDeprecatedAPITests",
Expand Down
2 changes: 1 addition & 1 deletion [email protected]
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ let package = Package(
.testTarget(
name: "WebURLTests",
dependencies: ["WebURL", "WebURLTestSupport", "Checkit"],
exclude: ["KeyValuePairsTests.swift"]
exclude: ["KeyValuePairs"]
),
.testTarget(
name: "WebURLDeprecatedAPITests",
Expand Down
148 changes: 109 additions & 39 deletions Sources/WebURL/WebURL+KeyValuePairs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,36 @@ extension KeyValueStringSchema {
}
}

internal enum KeyValueStringSchemaVerificationFailure: Error, CustomStringConvertible {

case preferredKeyValueDelimiterIsInvalid
case preferredKeyValueDelimiterNotRecognized
case preferredPairDelimiterIsInvalid
case preferredPairDelimiterNotRecognized
case invalidKeyValueDelimiterIsRecognized
case invalidPairDelimiterIsRecognized
case inconsistentSpaceEncoding

public var description: String {
switch self {
case .preferredKeyValueDelimiterIsInvalid:
return "Schema's preferred key-value delimiter is invalid"
case .preferredKeyValueDelimiterNotRecognized:
return "Schema does not recognize its preferred key-value delimiter as a key-value delimiter"
case .preferredPairDelimiterIsInvalid:
return "Schema's preferred pair delimiter is invalid"
case .preferredPairDelimiterNotRecognized:
return "Schema does not recognize its preferred pair delimiter as a pair delimiter"
case .invalidKeyValueDelimiterIsRecognized:
return "isKeyValueDelimiter recognizes an invalid delimiter"
case .invalidPairDelimiterIsRecognized:
return "isPairDelimiter recognizes an invalid delimiter"
case .inconsistentSpaceEncoding:
return "encodeSpaceAsPlus is true, so decodePlusAsSpace must also be true"
}
}
}

extension KeyValueStringSchema {

/// Checks this schema for consistency.
Expand Down Expand Up @@ -658,19 +688,47 @@ extension KeyValueStringSchema {
/// > For those that do, it is recommended to run this verification
/// > as part of your regular unit tests.
///
public func verify(for component: KeyValuePairsSupportedComponent) {
public func verify(for component: KeyValuePairsSupportedComponent) throws {

// Preferred delimiters must not require escaping.

let delimiters = verifyDelimitersDoNotNeedEscaping(in: component)
let preferredDelimiters = verifyDelimitersDoNotNeedEscaping(in: component)

if !isKeyValueDelimiter(delimiters.keyValue) {
fatalError("Inconsistent schema: preferred key-value delimiter is not recognized as a key-value delimiter")
if preferredDelimiters.keyValue == .max {
throw KeyValueStringSchemaVerificationFailure.preferredKeyValueDelimiterIsInvalid
}
if !isPairDelimiter(delimiters.pair) {
fatalError("Inconsistent schema: preferred pair delimiter is not recognized as a pair delimiter")
if preferredDelimiters.pair == .max {
throw KeyValueStringSchemaVerificationFailure.preferredPairDelimiterIsInvalid
}

// isKeyValueDelimiter/isPairDelimiter must recognize preferred delimiters,
// and must not recognize other reserved characters (e.g. %, ASCII hex digits, +).

if !isKeyValueDelimiter(preferredDelimiters.keyValue) {
throw KeyValueStringSchemaVerificationFailure.preferredKeyValueDelimiterNotRecognized
}
if !isPairDelimiter(preferredDelimiters.pair) {
throw KeyValueStringSchemaVerificationFailure.preferredPairDelimiterNotRecognized
}

func delimiterPredicateIsInvalid(_ isDelimiter: (UInt8) -> Bool) -> Bool {
"0123456789abcdefABCDEF%+".utf8.contains(where: isDelimiter)
}

if delimiterPredicateIsInvalid(isKeyValueDelimiter) {
throw KeyValueStringSchemaVerificationFailure.invalidKeyValueDelimiterIsRecognized
}
if delimiterPredicateIsInvalid(isPairDelimiter) {
throw KeyValueStringSchemaVerificationFailure.invalidPairDelimiterIsRecognized
}

// Space encoding must be consistent.

if encodeSpaceAsPlus, !decodePlusAsSpace {
fatalError("Inconsistent schema: encodeSpaceAsPlus is true, so decodePlusAsSpace must also be true")
throw KeyValueStringSchemaVerificationFailure.inconsistentSpaceEncoding
}

// All checks passed.
}
}

Expand Down Expand Up @@ -1064,7 +1122,7 @@ extension WebURL.KeyValuePairs: CustomStringConvertible {


// --------------------------------------------
// MARK: - Reading: Collection
// MARK: - Reading: By Location.
// --------------------------------------------


Expand Down Expand Up @@ -1218,7 +1276,7 @@ extension WebURL.KeyValuePairs: Collection {
}


// MARK: - TODO: BidirectionalCollection.
// MARK: TODO: BidirectionalCollection.


extension WebURL.KeyValuePairs {
Expand Down Expand Up @@ -2522,41 +2580,40 @@ extension KeyValueStringSchema {
// - Must not be the percent sign (`%`), plus sign (`+`), space, or a hex digit, and
// - Must not require escaping in the URL component(s) used with this schema.

precondition(
ASCII(keyValueDelimiter)?.isHexDigit == false
&& keyValueDelimiter != ASCII.percentSign.codePoint
&& keyValueDelimiter != ASCII.plus.codePoint,
"Schema's preferred key-value delimiter is invalid"
)
precondition(
ASCII(pairDelimiter)?.isHexDigit == false
&& pairDelimiter != ASCII.percentSign.codePoint
&& pairDelimiter != ASCII.plus.codePoint,
"Schema's preferred pair delimiter is invalid"
)
guard
ASCII(keyValueDelimiter)?.isHexDigit == false,
keyValueDelimiter != ASCII.percentSign.codePoint,
keyValueDelimiter != ASCII.plus.codePoint
else {
return (.max, 0)
}
guard
ASCII(pairDelimiter)?.isHexDigit == false,
pairDelimiter != ASCII.percentSign.codePoint,
pairDelimiter != ASCII.plus.codePoint
else {
return (0, .max)
}

switch component.value {
case .query:
let encodeSet = URLEncodeSet.SpecialQuery()
precondition(
!encodeSet.shouldPercentEncode(ascii: keyValueDelimiter),
"Schema's preferred key-value delimiter may not be used in the query"
)
precondition(
!encodeSet.shouldPercentEncode(ascii: pairDelimiter),
"Schema's preferred pair delimiter may not be used in the query"
)
guard !encodeSet.shouldPercentEncode(ascii: keyValueDelimiter) else {
return (.max, 0)
}
guard !encodeSet.shouldPercentEncode(ascii: pairDelimiter) else {
return (0, .max)
}
case .fragment:
let encodeSet = URLEncodeSet.Fragment()
precondition(
!encodeSet.shouldPercentEncode(ascii: keyValueDelimiter),
"Schema's preferred key-value delimiter may not be used in the fragment"
)
precondition(
!encodeSet.shouldPercentEncode(ascii: pairDelimiter),
"Schema's preferred pair delimiter may not be used in the fragment"
)
guard !encodeSet.shouldPercentEncode(ascii: keyValueDelimiter) else {
return (.max, 0)
}
guard !encodeSet.shouldPercentEncode(ascii: pairDelimiter) else {
return (0, .max)
}
}

return (keyValueDelimiter, pairDelimiter)
}

Expand Down Expand Up @@ -2587,6 +2644,17 @@ extension KeyValuePairsSupportedComponent {
}
}

@inlinable
internal func _trapOnInvalidDelimiters(
_ delimiters: (keyValue: UInt8, pair: UInt8)
) -> (keyValue: UInt8, pair: UInt8) {
precondition(
delimiters.keyValue != .max && delimiters.pair != .max,
"Schema has invalid delimiters"
)
return delimiters
}

extension URLStorage {

/// Replaces the given range of key-value pairs with a new collection of key-value pairs.
Expand Down Expand Up @@ -2642,7 +2710,9 @@ extension URLStorage {
return .success(newUpper..<newUpper)
}

let (keyValueDelimiter, pairDelimiter) = schema.verifyDelimitersDoNotNeedEscaping(in: component)
let (keyValueDelimiter, pairDelimiter) = _trapOnInvalidDelimiters(
schema.verifyDelimitersDoNotNeedEscaping(in: component)
)

// Measure the replacement string.

Expand Down Expand Up @@ -2971,7 +3041,7 @@ extension URLStorage {

if insertDelimiter {
precondition(!buffer.isEmpty)
buffer[0] = schema.verifyDelimitersDoNotNeedEscaping(in: component).keyValue
buffer[0] = _trapOnInvalidDelimiters(schema.verifyDelimitersDoNotNeedEscaping(in: component)).keyValue
buffer = UnsafeMutableBufferPointer(
start: buffer.baseAddress! + 1,
count: buffer.count &- 1
Expand Down
136 changes: 136 additions & 0 deletions Tests/WebURLTests/KeyValuePairs/Helpers.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// Copyright The swift-url Contributors.
//
// 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 XCTest

@testable import WebURL

#if swift(<5.7)
#error("WebURL.KeyValuePairs requires Swift 5.7 or newer")
#endif

/// A key-value pair.
///
/// The `Equatable` conformance for this type checks exact code-unit/code-point equality
/// rather than Unicode canonical equivalence. In other words:
///
/// ```swift
/// let nfc = KeyValuePair(key: "caf\u{00E9}", value: "")
/// let nfd = KeyValuePair(key: "cafe\u{0301}", value: "")
///
/// nfc == nfd // false
/// ```
///
struct KeyValuePair: Equatable {
var key: String
var value: String

init(key: String, value: String) {
self.key = key
self.value = value
}

init(_ kvp: WebURL.KeyValuePairs<some KeyValueStringSchema>.Element) {
self.init(key: kvp.key, value: kvp.value)
}

static func == (lhs: Self, rhs: Self) -> Bool {
lhs.key.utf8.elementsEqual(rhs.key.utf8) && lhs.value.utf8.elementsEqual(rhs.value.utf8)
}
}

/// Asserts that the given `WebURL.KeyValuePairs` (or slice thereof)
/// contains the same pairs as the given list.
///
/// Keys and values from the `WebURL.KeyValuePairs` are decoded before checking
/// for equality. The lists must match at the code-unit/code-point level.
///
func XCTAssertEqualKeyValuePairs(
_ left: some Collection<WebURL.KeyValuePairs<some KeyValueStringSchema>.Element>,
_ right: some Collection<KeyValuePair>,
file: StaticString = #fileID,
line: UInt = #line
) {
XCTAssertEqualElements(left.map { KeyValuePair($0) }, right, file: file, line: line)
}

/// Asserts that the given `WebURL.KeyValuePairs` (or slice thereof)
/// contains the same pairs as the given Array.
///
/// Keys and values from the `WebURL.KeyValuePairs` are decoded before checking
/// for equality. The lists must match at the code-unit/code-point level.
///
func XCTAssertEqualKeyValuePairs(
_ left: some Collection<WebURL.KeyValuePairs<some KeyValueStringSchema>.Element>,
_ right: [(key: String, value: String)],
file: StaticString = #fileID,
line: UInt = #line
) {
XCTAssertEqualKeyValuePairs(left, right.map { KeyValuePair(key: $0.key, value: $0.value) }, file: file, line: line)
}

/// A key-value string schema with non-standard delimiters.
///
/// ```
/// key1:value1,key2:value2
/// ```
///
/// Other than delimiters, it should match `PercentEncodedKeyValueString`.
///
struct CommaSeparated: KeyValueStringSchema {

var preferredPairDelimiter: UInt8 { UInt8(ascii: ",") }
var preferredKeyValueDelimiter: UInt8 { UInt8(ascii: ":") }

var decodePlusAsSpace: Bool { false }

func shouldPercentEncode(ascii codePoint: UInt8) -> Bool {
PercentEncodedKeyValueString().shouldPercentEncode(ascii: codePoint)
}
}

/// A key-value string schema which supports additional options for form-encoding.
///
struct ExtendedForm: KeyValueStringSchema {

var semicolonIsPairDelimiter = false
var encodeSpaceAsPlus = false

func isPairDelimiter(_ codePoint: UInt8) -> Bool {
codePoint == UInt8(ascii: "&") || (semicolonIsPairDelimiter && codePoint == UInt8(ascii: ";"))
}

var preferredPairDelimiter: UInt8 { UInt8(ascii: "&") }
var preferredKeyValueDelimiter: UInt8 { UInt8(ascii: "=") }

var decodePlusAsSpace: Bool { true }

func shouldPercentEncode(ascii codePoint: UInt8) -> Bool {
FormCompatibleKeyValueString().shouldPercentEncode(ascii: codePoint)
}
}

/// Asserts that information in the `WebURL.KeyValuePairs` cache
/// is consistent with a freshly-recalculated cache.
///
func XCTAssertKeyValuePairCacheIsAccurate(_ kvps: WebURL.KeyValuePairs<some KeyValueStringSchema>) {

let expectedCache = type(of: kvps).Cache.calculate(
storage: kvps.storage,
component: kvps.component,
schema: kvps.schema
)
XCTAssertEqual(kvps.cache.startIndex, expectedCache.startIndex)
XCTAssertEqual(kvps.cache.componentContents, expectedCache.componentContents)
}
Loading

0 comments on commit 6ac78af

Please sign in to comment.