Skip to content

Commit d70401a

Browse files
committed
Test cleanup WIP
1 parent 461bd3c commit d70401a

File tree

7 files changed

+1639
-975
lines changed

7 files changed

+1639
-975
lines changed

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ let package = Package(
9393
.testTarget(
9494
name: "WebURLTests",
9595
dependencies: ["WebURL", "WebURLTestSupport", "Checkit"],
96-
exclude: ["KeyValuePairsTests.swift"]
96+
exclude: ["KeyValuePairs"]
9797
),
9898
.testTarget(
9999
name: "WebURLDeprecatedAPITests",

[email protected]

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ let package = Package(
9393
.testTarget(
9494
name: "WebURLTests",
9595
dependencies: ["WebURL", "WebURLTestSupport", "Checkit"],
96-
exclude: ["KeyValuePairsTests.swift"]
96+
exclude: ["KeyValuePairs"]
9797
),
9898
.testTarget(
9999
name: "WebURLDeprecatedAPITests",

Sources/WebURL/WebURL+KeyValuePairs.swift

Lines changed: 109 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,36 @@ extension KeyValueStringSchema {
627627
}
628628
}
629629

630+
internal enum KeyValueStringSchemaVerificationFailure: Error, CustomStringConvertible {
631+
632+
case preferredKeyValueDelimiterIsInvalid
633+
case preferredKeyValueDelimiterNotRecognized
634+
case preferredPairDelimiterIsInvalid
635+
case preferredPairDelimiterNotRecognized
636+
case invalidKeyValueDelimiterIsRecognized
637+
case invalidPairDelimiterIsRecognized
638+
case inconsistentSpaceEncoding
639+
640+
public var description: String {
641+
switch self {
642+
case .preferredKeyValueDelimiterIsInvalid:
643+
return "Schema's preferred key-value delimiter is invalid"
644+
case .preferredKeyValueDelimiterNotRecognized:
645+
return "Schema does not recognize its preferred key-value delimiter as a key-value delimiter"
646+
case .preferredPairDelimiterIsInvalid:
647+
return "Schema's preferred pair delimiter is invalid"
648+
case .preferredPairDelimiterNotRecognized:
649+
return "Schema does not recognize its preferred pair delimiter as a pair delimiter"
650+
case .invalidKeyValueDelimiterIsRecognized:
651+
return "isKeyValueDelimiter recognizes an invalid delimiter"
652+
case .invalidPairDelimiterIsRecognized:
653+
return "isPairDelimiter recognizes an invalid delimiter"
654+
case .inconsistentSpaceEncoding:
655+
return "encodeSpaceAsPlus is true, so decodePlusAsSpace must also be true"
656+
}
657+
}
658+
}
659+
630660
extension KeyValueStringSchema {
631661

632662
/// Checks this schema for consistency.
@@ -658,19 +688,47 @@ extension KeyValueStringSchema {
658688
/// > For those that do, it is recommended to run this verification
659689
/// > as part of your regular unit tests.
660690
///
661-
public func verify(for component: KeyValuePairsSupportedComponent) {
691+
public func verify(for component: KeyValuePairsSupportedComponent) throws {
692+
693+
// Preferred delimiters must not require escaping.
662694

663-
let delimiters = verifyDelimitersDoNotNeedEscaping(in: component)
695+
let preferredDelimiters = verifyDelimitersDoNotNeedEscaping(in: component)
664696

665-
if !isKeyValueDelimiter(delimiters.keyValue) {
666-
fatalError("Inconsistent schema: preferred key-value delimiter is not recognized as a key-value delimiter")
697+
if preferredDelimiters.keyValue == .max {
698+
throw KeyValueStringSchemaVerificationFailure.preferredKeyValueDelimiterIsInvalid
667699
}
668-
if !isPairDelimiter(delimiters.pair) {
669-
fatalError("Inconsistent schema: preferred pair delimiter is not recognized as a pair delimiter")
700+
if preferredDelimiters.pair == .max {
701+
throw KeyValueStringSchemaVerificationFailure.preferredPairDelimiterIsInvalid
670702
}
703+
704+
// isKeyValueDelimiter/isPairDelimiter must recognize preferred delimiters,
705+
// and must not recognize other reserved characters (e.g. %, ASCII hex digits, +).
706+
707+
if !isKeyValueDelimiter(preferredDelimiters.keyValue) {
708+
throw KeyValueStringSchemaVerificationFailure.preferredKeyValueDelimiterNotRecognized
709+
}
710+
if !isPairDelimiter(preferredDelimiters.pair) {
711+
throw KeyValueStringSchemaVerificationFailure.preferredPairDelimiterNotRecognized
712+
}
713+
714+
func delimiterPredicateIsInvalid(_ isDelimiter: (UInt8) -> Bool) -> Bool {
715+
"0123456789abcdefABCDEF%+".utf8.contains(where: isDelimiter)
716+
}
717+
718+
if delimiterPredicateIsInvalid(isKeyValueDelimiter) {
719+
throw KeyValueStringSchemaVerificationFailure.invalidKeyValueDelimiterIsRecognized
720+
}
721+
if delimiterPredicateIsInvalid(isPairDelimiter) {
722+
throw KeyValueStringSchemaVerificationFailure.invalidPairDelimiterIsRecognized
723+
}
724+
725+
// Space encoding must be consistent.
726+
671727
if encodeSpaceAsPlus, !decodePlusAsSpace {
672-
fatalError("Inconsistent schema: encodeSpaceAsPlus is true, so decodePlusAsSpace must also be true")
728+
throw KeyValueStringSchemaVerificationFailure.inconsistentSpaceEncoding
673729
}
730+
731+
// All checks passed.
674732
}
675733
}
676734

@@ -1064,7 +1122,7 @@ extension WebURL.KeyValuePairs: CustomStringConvertible {
10641122

10651123

10661124
// --------------------------------------------
1067-
// MARK: - Reading: Collection
1125+
// MARK: - Reading: By Location.
10681126
// --------------------------------------------
10691127

10701128

@@ -1218,7 +1276,7 @@ extension WebURL.KeyValuePairs: Collection {
12181276
}
12191277

12201278

1221-
// MARK: - TODO: BidirectionalCollection.
1279+
// MARK: TODO: BidirectionalCollection.
12221280

12231281

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

2525-
precondition(
2526-
ASCII(keyValueDelimiter)?.isHexDigit == false
2527-
&& keyValueDelimiter != ASCII.percentSign.codePoint
2528-
&& keyValueDelimiter != ASCII.plus.codePoint,
2529-
"Schema's preferred key-value delimiter is invalid"
2530-
)
2531-
precondition(
2532-
ASCII(pairDelimiter)?.isHexDigit == false
2533-
&& pairDelimiter != ASCII.percentSign.codePoint
2534-
&& pairDelimiter != ASCII.plus.codePoint,
2535-
"Schema's preferred pair delimiter is invalid"
2536-
)
2583+
guard
2584+
ASCII(keyValueDelimiter)?.isHexDigit == false,
2585+
keyValueDelimiter != ASCII.percentSign.codePoint,
2586+
keyValueDelimiter != ASCII.plus.codePoint
2587+
else {
2588+
return (.max, 0)
2589+
}
2590+
guard
2591+
ASCII(pairDelimiter)?.isHexDigit == false,
2592+
pairDelimiter != ASCII.percentSign.codePoint,
2593+
pairDelimiter != ASCII.plus.codePoint
2594+
else {
2595+
return (0, .max)
2596+
}
25372597

25382598
switch component.value {
25392599
case .query:
25402600
let encodeSet = URLEncodeSet.SpecialQuery()
2541-
precondition(
2542-
!encodeSet.shouldPercentEncode(ascii: keyValueDelimiter),
2543-
"Schema's preferred key-value delimiter may not be used in the query"
2544-
)
2545-
precondition(
2546-
!encodeSet.shouldPercentEncode(ascii: pairDelimiter),
2547-
"Schema's preferred pair delimiter may not be used in the query"
2548-
)
2601+
guard !encodeSet.shouldPercentEncode(ascii: keyValueDelimiter) else {
2602+
return (.max, 0)
2603+
}
2604+
guard !encodeSet.shouldPercentEncode(ascii: pairDelimiter) else {
2605+
return (0, .max)
2606+
}
25492607
case .fragment:
25502608
let encodeSet = URLEncodeSet.Fragment()
2551-
precondition(
2552-
!encodeSet.shouldPercentEncode(ascii: keyValueDelimiter),
2553-
"Schema's preferred key-value delimiter may not be used in the fragment"
2554-
)
2555-
precondition(
2556-
!encodeSet.shouldPercentEncode(ascii: pairDelimiter),
2557-
"Schema's preferred pair delimiter may not be used in the fragment"
2558-
)
2609+
guard !encodeSet.shouldPercentEncode(ascii: keyValueDelimiter) else {
2610+
return (.max, 0)
2611+
}
2612+
guard !encodeSet.shouldPercentEncode(ascii: pairDelimiter) else {
2613+
return (0, .max)
2614+
}
25592615
}
2616+
25602617
return (keyValueDelimiter, pairDelimiter)
25612618
}
25622619

@@ -2587,6 +2644,17 @@ extension KeyValuePairsSupportedComponent {
25872644
}
25882645
}
25892646

2647+
@inlinable
2648+
internal func _trapOnInvalidDelimiters(
2649+
_ delimiters: (keyValue: UInt8, pair: UInt8)
2650+
) -> (keyValue: UInt8, pair: UInt8) {
2651+
precondition(
2652+
delimiters.keyValue != .max && delimiters.pair != .max,
2653+
"Schema has invalid delimiters"
2654+
)
2655+
return delimiters
2656+
}
2657+
25902658
extension URLStorage {
25912659

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

2645-
let (keyValueDelimiter, pairDelimiter) = schema.verifyDelimitersDoNotNeedEscaping(in: component)
2713+
let (keyValueDelimiter, pairDelimiter) = _trapOnInvalidDelimiters(
2714+
schema.verifyDelimitersDoNotNeedEscaping(in: component)
2715+
)
26462716

26472717
// Measure the replacement string.
26482718

@@ -2971,7 +3041,7 @@ extension URLStorage {
29713041

29723042
if insertDelimiter {
29733043
precondition(!buffer.isEmpty)
2974-
buffer[0] = schema.verifyDelimitersDoNotNeedEscaping(in: component).keyValue
3044+
buffer[0] = _trapOnInvalidDelimiters(schema.verifyDelimitersDoNotNeedEscaping(in: component)).keyValue
29753045
buffer = UnsafeMutableBufferPointer(
29763046
start: buffer.baseAddress! + 1,
29773047
count: buffer.count &- 1
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
// Copyright The swift-url Contributors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import XCTest
16+
17+
@testable import WebURL
18+
19+
#if swift(<5.7)
20+
#error("WebURL.KeyValuePairs requires Swift 5.7 or newer")
21+
#endif
22+
23+
/// A key-value pair.
24+
///
25+
/// The `Equatable` conformance for this type checks exact code-unit/code-point equality
26+
/// rather than Unicode canonical equivalence. In other words:
27+
///
28+
/// ```swift
29+
/// let nfc = KeyValuePair(key: "caf\u{00E9}", value: "")
30+
/// let nfd = KeyValuePair(key: "cafe\u{0301}", value: "")
31+
///
32+
/// nfc == nfd // false
33+
/// ```
34+
///
35+
struct KeyValuePair: Equatable {
36+
var key: String
37+
var value: String
38+
39+
init(key: String, value: String) {
40+
self.key = key
41+
self.value = value
42+
}
43+
44+
init(_ kvp: WebURL.KeyValuePairs<some KeyValueStringSchema>.Element) {
45+
self.init(key: kvp.key, value: kvp.value)
46+
}
47+
48+
static func == (lhs: Self, rhs: Self) -> Bool {
49+
lhs.key.utf8.elementsEqual(rhs.key.utf8) && lhs.value.utf8.elementsEqual(rhs.value.utf8)
50+
}
51+
}
52+
53+
/// Asserts that the given `WebURL.KeyValuePairs` (or slice thereof)
54+
/// contains the same pairs as the given list.
55+
///
56+
/// Keys and values from the `WebURL.KeyValuePairs` are decoded before checking
57+
/// for equality. The lists must match at the code-unit/code-point level.
58+
///
59+
func XCTAssertEqualKeyValuePairs(
60+
_ left: some Collection<WebURL.KeyValuePairs<some KeyValueStringSchema>.Element>,
61+
_ right: some Collection<KeyValuePair>,
62+
file: StaticString = #fileID,
63+
line: UInt = #line
64+
) {
65+
XCTAssertEqualElements(left.map { KeyValuePair($0) }, right, file: file, line: line)
66+
}
67+
68+
/// Asserts that the given `WebURL.KeyValuePairs` (or slice thereof)
69+
/// contains the same pairs as the given Array.
70+
///
71+
/// Keys and values from the `WebURL.KeyValuePairs` are decoded before checking
72+
/// for equality. The lists must match at the code-unit/code-point level.
73+
///
74+
func XCTAssertEqualKeyValuePairs(
75+
_ left: some Collection<WebURL.KeyValuePairs<some KeyValueStringSchema>.Element>,
76+
_ right: [(key: String, value: String)],
77+
file: StaticString = #fileID,
78+
line: UInt = #line
79+
) {
80+
XCTAssertEqualKeyValuePairs(left, right.map { KeyValuePair(key: $0.key, value: $0.value) }, file: file, line: line)
81+
}
82+
83+
/// A key-value string schema with non-standard delimiters.
84+
///
85+
/// ```
86+
/// key1:value1,key2:value2
87+
/// ```
88+
///
89+
/// Other than delimiters, it should match `PercentEncodedKeyValueString`.
90+
///
91+
struct CommaSeparated: KeyValueStringSchema {
92+
93+
var preferredPairDelimiter: UInt8 { UInt8(ascii: ",") }
94+
var preferredKeyValueDelimiter: UInt8 { UInt8(ascii: ":") }
95+
96+
var decodePlusAsSpace: Bool { false }
97+
98+
func shouldPercentEncode(ascii codePoint: UInt8) -> Bool {
99+
PercentEncodedKeyValueString().shouldPercentEncode(ascii: codePoint)
100+
}
101+
}
102+
103+
/// A key-value string schema which supports additional options for form-encoding.
104+
///
105+
struct ExtendedForm: KeyValueStringSchema {
106+
107+
var semicolonIsPairDelimiter = false
108+
var encodeSpaceAsPlus = false
109+
110+
func isPairDelimiter(_ codePoint: UInt8) -> Bool {
111+
codePoint == UInt8(ascii: "&") || (semicolonIsPairDelimiter && codePoint == UInt8(ascii: ";"))
112+
}
113+
114+
var preferredPairDelimiter: UInt8 { UInt8(ascii: "&") }
115+
var preferredKeyValueDelimiter: UInt8 { UInt8(ascii: "=") }
116+
117+
var decodePlusAsSpace: Bool { true }
118+
119+
func shouldPercentEncode(ascii codePoint: UInt8) -> Bool {
120+
FormCompatibleKeyValueString().shouldPercentEncode(ascii: codePoint)
121+
}
122+
}
123+
124+
/// Asserts that information in the `WebURL.KeyValuePairs` cache
125+
/// is consistent with a freshly-recalculated cache.
126+
///
127+
func XCTAssertKeyValuePairCacheIsAccurate(_ kvps: WebURL.KeyValuePairs<some KeyValueStringSchema>) {
128+
129+
let expectedCache = type(of: kvps).Cache.calculate(
130+
storage: kvps.storage,
131+
component: kvps.component,
132+
schema: kvps.schema
133+
)
134+
XCTAssertEqual(kvps.cache.startIndex, expectedCache.startIndex)
135+
XCTAssertEqual(kvps.cache.componentContents, expectedCache.componentContents)
136+
}

0 commit comments

Comments
 (0)