提交 5ddf1aff 编写于 作者: C Christian Noon

Removed majority of AFResult extensions and migrated to Result equivalents

上级 ce428e40
......@@ -243,6 +243,9 @@
4C743D321C22772F00BCB23E /* signed-by-ca2.cer in Resources */ = {isa = PBXBuildFile; fileRef = 4C812C511B535F540017E0BF /* signed-by-ca2.cer */; };
4C743D331C22772F00BCB23E /* valid-dns-name.cer in Resources */ = {isa = PBXBuildFile; fileRef = 4C812C521B535F540017E0BF /* valid-dns-name.cer */; };
4C743D341C22772F00BCB23E /* valid-uri.cer in Resources */ = {isa = PBXBuildFile; fileRef = 4C812C531B535F540017E0BF /* valid-uri.cer */; };
4C7DD7EB224C627300249836 /* AFResult+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7DD7EA224C627300249836 /* AFResult+Alamofire.swift */; };
4C7DD7EC224C627300249836 /* AFResult+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7DD7EA224C627300249836 /* AFResult+Alamofire.swift */; };
4C7DD7ED224C627300249836 /* AFResult+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7DD7EA224C627300249836 /* AFResult+Alamofire.swift */; };
4C811F8D1B51856D00E0F59A /* ServerTrustEvaluation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C811F8C1B51856D00E0F59A /* ServerTrustEvaluation.swift */; };
4C811F8E1B51856D00E0F59A /* ServerTrustEvaluation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C811F8C1B51856D00E0F59A /* ServerTrustEvaluation.swift */; };
4CB928291C66BFBC00CE5F08 /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB928281C66BFBC00CE5F08 /* Notifications.swift */; };
......@@ -408,6 +411,7 @@
4C3D00571C66A8B900D1F709 /* NetworkReachabilityManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkReachabilityManagerTests.swift; sourceTree = "<group>"; };
4C43669A1D7BB93D00C38AAD /* DispatchQueue+Alamofire.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DispatchQueue+Alamofire.swift"; sourceTree = "<group>"; };
4C4466EA21F8F5D800AC9703 /* CachedResponseHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedResponseHandler.swift; sourceTree = "<group>"; };
4C7DD7EA224C627300249836 /* AFResult+Alamofire.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AFResult+Alamofire.swift"; sourceTree = "<group>"; };
4C811F8C1B51856D00E0F59A /* ServerTrustEvaluation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServerTrustEvaluation.swift; sourceTree = "<group>"; };
4C812C3A1B535F220017E0BF /* alamofire-root-ca.cer */ = {isa = PBXFileReference; lastKnownFileType = file; name = "alamofire-root-ca.cer"; path = "alamofire.org/alamofire-root-ca.cer"; sourceTree = "<group>"; };
4C812C3D1B535F2E0017E0BF /* alamofire-signing-ca1.cer */ = {isa = PBXFileReference; lastKnownFileType = file; name = "alamofire-signing-ca1.cer"; path = "alamofire.org/alamofire-signing-ca1.cer"; sourceTree = "<group>"; };
......@@ -675,6 +679,14 @@
name = Extensions;
sourceTree = "<group>";
};
4C7DD7E9224C625500249836 /* Helpers */ = {
isa = PBXGroup;
children = (
4C7DD7EA224C627300249836 /* AFResult+Alamofire.swift */,
);
name = Helpers;
sourceTree = "<group>";
};
4C812C391B535F060017E0BF /* alamofire.org */ = {
isa = PBXGroup;
children = (
......@@ -886,6 +898,7 @@
4C256A4E1B09656A0065714F /* Core */,
4C7C8D201B9D0D7300948136 /* Extensions */,
4C256A4F1B09656E0065714F /* Features */,
4C7DD7E9224C625500249836 /* Helpers */,
4C3238E91B3617A600FE04AE /* Resources */,
F8111E4019A95C8B0040E7D1 /* Supporting Files */,
);
......@@ -1366,6 +1379,7 @@
3107EA3A20A11F9700445260 /* ResponseTests.swift in Sources */,
4CFB02921D7CF28F0056F249 /* FileManager+AlamofireTests.swift in Sources */,
4CF627141BA7CC240011A099 /* BaseTestCase.swift in Sources */,
4C7DD7ED224C627300249836 /* AFResult+Alamofire.swift in Sources */,
31727424218BB9A50039FFCC /* HTTPBin.swift in Sources */,
31EBD9C320D1D89D00D1FF34 /* ValidationTests.swift in Sources */,
3111CE8620A76370008315E2 /* SessionTests.swift in Sources */,
......@@ -1514,6 +1528,7 @@
3107EA3820A11F9600445260 /* ResponseTests.swift in Sources */,
F8858DDD19A96B4300F55F93 /* RequestTests.swift in Sources */,
4C256A531B096C770065714F /* BaseTestCase.swift in Sources */,
4C7DD7EB224C627300249836 /* AFResult+Alamofire.swift in Sources */,
31727422218BB9A50039FFCC /* HTTPBin.swift in Sources */,
31EBD9C120D1D89C00D1FF34 /* ValidationTests.swift in Sources */,
3111CE8420A7636E008315E2 /* SessionTests.swift in Sources */,
......@@ -1548,6 +1563,7 @@
3107EA3920A11F9600445260 /* ResponseTests.swift in Sources */,
F829C6BE1A7A950600A2CD59 /* ParameterEncodingTests.swift in Sources */,
F829C6BF1A7A950600A2CD59 /* RequestTests.swift in Sources */,
4C7DD7EC224C627300249836 /* AFResult+Alamofire.swift in Sources */,
31727423218BB9A50039FFCC /* HTTPBin.swift in Sources */,
31EBD9C220D1D89C00D1FF34 /* ValidationTests.swift in Sources */,
3111CE8520A7636F008315E2 /* SessionTests.swift in Sources */,
......
......@@ -26,39 +26,19 @@ import Foundation
public typealias AFResult<T> = Result<T, Error>
// MARK: - CustomStringConvertible
extension AFResult: CustomStringConvertible {
/// The textual representation used when written to an output stream, which includes whether the result was a
/// success or failure.
public var description: String {
switch self {
case .success:
return "SUCCESS"
case .failure:
return "FAILURE"
}
extension AFResult {
/// Returns the associated value if the result is a success, `nil` otherwise.
var value: Success? {
guard case .success(let value) = self else { return nil }
return value
}
}
// MARK: - CustomDebugStringConvertible
extension AFResult: CustomDebugStringConvertible {
/// The debug textual representation used when written to an output stream, which includes whether the result was a
/// success or failure in addition to the value or error.
public var debugDescription: String {
switch self {
case .success(let value):
return "SUCCESS: \(value)"
case .failure(let error):
return "FAILURE: \(error)"
}
/// Returns the associated error value if the result is a failure, `nil` otherwise.
var error: Error? {
guard case .failure(let error) = self else { return nil }
return error
}
}
// MARK: - Functional APIs
extension AFResult {
/// Initializes an `AFResult` from value or error. Returns `.failure` if the error is non-nil, `.success` otherwise.
///
/// - Parameters:
......@@ -72,39 +52,6 @@ extension AFResult {
}
}
/// Returns `true` if the result is a success, `false` otherwise.
var isSuccess: Bool {
switch self {
case .success:
return true
case .failure:
return false
}
}
/// Returns `true` if the result is a failure, `false` otherwise.
var isFailure: Bool {
return !isSuccess
}
/// Returns the success value, or throws the failure error.
///
/// let possibleString: AFResult<String> = .success("success")
/// try print(possibleString.unwrap())
/// // Prints "success"
///
/// let noString: AFResult<String> = .failure(error)
/// try print(noString.unwrap())
/// // Throws error
func unwrap() throws -> Success {
switch self {
case .success(let value):
return value
case .failure(let error):
throw error
}
}
/// Evaluates the specified closure when the `AFResult` is a success, passing the unwrapped value as a parameter.
///
/// Use the `flatMap` method with a closure that may throw an error. For example:
......@@ -156,92 +103,4 @@ extension AFResult {
return .success(value)
}
}
/// Evaluates the specified closure when the `AFResult` is a success, passing the unwrapped value as a parameter.
///
/// Use the `withValue` function to evaluate the passed closure.
///
/// - Parameter closure: A closure that takes the success value of this instance.
/// - Returns: An `AFResult` instance, unmodified.
@discardableResult
func withValue(_ closure: (Success) throws -> Void) rethrows -> AFResult<Success> {
switch self {
case .success(let value):
try closure(value)
return .success(value)
case .failure(let error):
return .failure(error)
}
}
/// Evaluates the specified closure when the `AFResult` is a failure, passing the unwrapped error as a parameter.
///
/// Use the `withError` function to evaluate the passed closure.
///
/// - Parameter closure: A closure that takes the success value of this instance.
/// - Returns: An `AFResult` instance, unmodified.
@discardableResult
func withError(_ closure: (Failure) throws -> Void) rethrows -> AFResult<Success> {
switch self {
case .failure(let error):
try closure(error)
return .failure(error)
case .success(let value):
return .success(value)
}
}
/// Evaluates the specified closure when the `AFResult` is a success.
///
/// Use the `ifSuccess` function to evaluate the passed closure without modifying the `AFResult` instance.
///
/// - Parameter closure: A `Void` closure.
/// - Returns: This `AFResult` instance, unmodified.
@discardableResult
func ifSuccess(_ closure: () throws -> Void) rethrows -> AFResult<Success> {
switch self {
case .success(let value):
try closure()
return .success(value)
case .failure(let error):
return .failure(error)
}
}
/// Evaluates the specified closure when the `AFResult` is a failure.
///
/// Use the `ifFailure` function to evaluate the passed closure without modifying the `AFResult` instance.
///
/// - Parameter closure: A `Void` closure.
/// - Returns: This `AFResult` instance, unmodified.
@discardableResult
func ifFailure(_ closure: () throws -> Void) rethrows -> AFResult<Success> {
switch self {
case .success(let value):
return .success(value)
case .failure(let error):
try closure()
return .failure(error)
}
}
/// Returns the associated value if the result is a success, `nil` otherwise.
var value: Success? {
switch self {
case .success(let value):
return value
case .failure:
return nil
}
}
/// Returns the associated error value if the result is a failure, `nil` otherwise.
var error: Error? {
switch self {
case .success:
return nil
case .failure(let error):
return error
}
}
}
......@@ -80,10 +80,10 @@ open class MultipartUpload {
extension MultipartUpload: UploadConvertible {
public func asURLRequest() throws -> URLRequest {
return try result.unwrap().request
return try result.get().request
}
public func createUploadable() throws -> UploadRequest.Uploadable {
return try result.unwrap().uploadable
return try result.get().uploadable
}
}
......@@ -165,7 +165,7 @@ open class URLEncodedFormParameterEncoder: ParameterEncoder {
if destination.encodesParametersInURL(for: method),
var components = URLComponents(url: url, resolvingAgainstBaseURL: false) {
let query: String = try AFResult<String> { try encoder.encode(parameters) }
.mapError { AFError.parameterEncoderFailed(reason: .encoderFailed(error: $0)) }.unwrap()
.mapError { AFError.parameterEncoderFailed(reason: .encoderFailed(error: $0)) }.get()
let newQueryString = [components.percentEncodedQuery, query].compactMap { $0 }.joinedWithAmpersands()
components.percentEncodedQuery = newQueryString
......@@ -180,7 +180,7 @@ open class URLEncodedFormParameterEncoder: ParameterEncoder {
}
request.httpBody = try AFResult<Data> { try encoder.encode(parameters) }
.mapError { AFError.parameterEncoderFailed(reason: .encoderFailed(error: $0)) }.unwrap()
.mapError { AFError.parameterEncoderFailed(reason: .encoderFailed(error: $0)) }.get()
}
return request
......
......@@ -781,7 +781,7 @@ open class DataRequest: Request {
let result = validation(self.request, response, self.data)
result.withError { self.error = $0 }
if case .failure(let error) = result { self.error = error }
self.eventMonitor?.request(self,
didValidateRequest: self.request,
......@@ -905,8 +905,10 @@ open class DownloadRequest: Request {
func didFinishDownloading(using task: URLSessionTask, with result: AFResult<URL>) {
eventMonitor?.request(self, didFinishDownloadingUsing: task, with: result)
result.withValue { url in protectedMutableState.write { $0.fileURL = url } }
.withError { self.error = $0 }
switch result {
case .success(let url): protectedMutableState.write { $0.fileURL = url }
case .failure(let error): self.error = error
}
}
func updateDownloadProgress(bytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
......@@ -953,7 +955,7 @@ open class DownloadRequest: Request {
let result = validation(self.request, response, self.fileURL)
result.withError { self.error = $0 }
if case .failure(let error) = result { self.error = error }
self.eventMonitor?.request(self,
didValidateRequest: self.request,
......
......@@ -80,7 +80,7 @@ extension DataResponse: CustomStringConvertible, CustomDebugStringConvertible {
/// The textual representation used when written to an output stream, which includes whether the result was a
/// success or failure.
public var description: String {
return result.debugDescription
return "\(result)"
}
/// The debug textual representation used when written to an output stream, which includes the URL request, the URL
......@@ -109,7 +109,7 @@ extension DataResponse: CustomStringConvertible, CustomDebugStringConvertible {
[Data]: \(data?.description ?? "None")
[Network Duration]: \(metricsDescription)
[Serialization Duration]: \(serializationDuration)s
[Result]: \(result.debugDescription)
[Result]: \(result)
"""
}
}
......@@ -268,7 +268,7 @@ extension DownloadResponse: CustomStringConvertible, CustomDebugStringConvertibl
/// The textual representation used when written to an output stream, which includes whether the result was a
/// success or failure.
public var description: String {
return result.debugDescription
return "\(result)"
}
/// The debug textual representation used when written to an output stream, which includes the URL request, the URL
......@@ -297,7 +297,7 @@ extension DownloadResponse: CustomStringConvertible, CustomDebugStringConvertibl
[ResumeData]: \(resumeDataDescription)
[Network Duration]: \(metricsDescription)
[Serialization Duration]: \(serializationDuration)s
[Result]: \(result.debugDescription)
[Result]: \(result)
"""
}
}
......
......@@ -429,7 +429,7 @@ open class Session {
if let adapter = adapter(for: request) {
adapter.adapt(initialRequest, for: self) { result in
do {
let adaptedRequest = try result.unwrap()
let adaptedRequest = try result.get()
self.rootQueue.async {
request.didAdaptInitialRequest(initialRequest, to: adaptedRequest)
......
//
// AFResult+Alamofire.swift
//
// Copyright (c) 2019 Alamofire Software Foundation (http://alamofire.org/)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import Alamofire
import Foundation
extension AFResult {
var isSuccess: Bool {
guard case .success = self else { return false }
return true
}
var isFailure: Bool { return !isSuccess }
}
......@@ -29,42 +29,6 @@ import XCTest
class AFResultTestCase: BaseTestCase {
let error = AFError.responseValidationFailed(reason: .unacceptableStatusCode(code: 404))
// MARK: - Is Success Tests
func testThatIsSuccessPropertyReturnsTrueForSuccessCase() {
// Given, When
let result = AFResult<String>.success("success")
// Then
XCTAssertTrue(result.isSuccess, "result is success should be true for success case")
}
func testThatIsSuccessPropertyReturnsFalseForFailureCase() {
// Given, When
let result = AFResult<String>.failure(error)
// Then
XCTAssertFalse(result.isSuccess, "result is success should be false for failure case")
}
// MARK: - Is Failure Tests
func testThatIsFailurePropertyReturnsFalseForSuccessCase() {
// Given, When
let result = AFResult<String>.success("success")
// Then
XCTAssertFalse(result.isFailure, "result is failure should be false for success case")
}
func testThatIsFailurePropertyReturnsTrueForFailureCase() {
// Given, When
let result = AFResult<String>.failure(error)
// Then
XCTAssertTrue(result.isFailure, "result is failure should be true for failure case")
}
// MARK: - Value Tests
func testThatValuePropertyReturnsValueForSuccessCase() {
......@@ -101,50 +65,6 @@ class AFResultTestCase: BaseTestCase {
XCTAssertNotNil(result.error, "result error should not be nil for failure case")
}
// MARK: - Description Tests
func testThatDescriptionStringMatchesExpectedValueForSuccessCase() {
// Given, When
let result = AFResult<String>.success("success")
// Then
XCTAssertEqual(result.description, "SUCCESS", "result description should match expected value for success case")
}
func testThatDescriptionStringMatchesExpectedValueForFailureCase() {
// Given, When
let result = AFResult<String>.failure(error)
// Then
XCTAssertEqual(result.description, "FAILURE", "result description should match expected value for failure case")
}
// MARK: - Debug Description Tests
func testThatDebugDescriptionStringMatchesExpectedValueForSuccessCase() {
// Given, When
let result = AFResult<String>.success("success value")
// Then
XCTAssertEqual(
result.debugDescription,
"SUCCESS: success value",
"result debug description should match expected value for success case"
)
}
func testThatDebugDescriptionStringMatchesExpectedValueForFailureCase() {
// Given, When
let result = AFResult<String>.failure(error)
// Then
XCTAssertEqual(
result.debugDescription,
"FAILURE: \(error)",
"result debug description should match expected value for failure case"
)
}
// MARK: - Initializer Tests
func testThatInitializerFromThrowingClosureStoresResultAsASuccess() {
......@@ -177,64 +97,6 @@ class AFResultTestCase: BaseTestCase {
}
}
// MARK: - Unwrap Tests
func testThatUnwrapReturnsSuccessValue() {
// Given
let result = AFResult<String>.success("success value")
// When
let unwrappedValue = try? result.unwrap()
// Then
XCTAssertEqual(unwrappedValue, "success value")
}
func testThatUnwrapThrowsFailureError() {
// Given
struct ResultError: Error {}
// When
let result = AFResult<String>.failure(ResultError())
// Then
do {
_ = try result.unwrap()
XCTFail("result unwrapping should throw the failure error")
} catch {
XCTAssertTrue(error is ResultError)
}
}
// MARK: - Map Tests
func testThatMapTransformsSuccessValue() {
// Given
let result = AFResult<String>.success("success value")
// When
let mappedResult = result.map { $0.count }
// Then
XCTAssertEqual(mappedResult.value, 13)
}
func testThatMapPreservesFailureError() {
// Given
struct ResultError: Error {}
let result = AFResult<String>.failure(ResultError())
// When
let mappedResult = result.map { $0.count }
// Then
if let error = mappedResult.error {
XCTAssertTrue(error is ResultError)
} else {
XCTFail("map should preserve the failure error")
}
}
// MARK: - FlatMap Tests
func testThatFlatMapTransformsSuccessValue() {
......@@ -281,37 +143,7 @@ class AFResultTestCase: BaseTestCase {
}
}
// MARK: - Error Mapping Tests
func testMapErrorTransformsErrorValue() {
// Given
struct ResultError: Error {}
struct OtherError: Error { let error: Error }
let result: AFResult<String> = .failure(ResultError())
// When
let mappedResult = result.mapError { OtherError(error: $0) }
// Then
if let error = mappedResult.error {
XCTAssertTrue(error is OtherError)
} else {
XCTFail("mapError should transform error value")
}
}
func testMapErrorPreservesSuccessError() {
// Given
struct ResultError: Error {}
struct OtherError: Error { let error: Error }
let result: AFResult<String> = .success("success")
// When
let mappedResult = result.mapError { OtherError(error: $0) }
// Then
XCTAssertEqual(mappedResult.value, "success")
}
// MARK: - FlatMapError Tests
func testFlatMapErrorTransformsErrorValue() {
// Given
......@@ -326,7 +158,7 @@ class AFResultTestCase: BaseTestCase {
if let error = mappedResult.error {
XCTAssertTrue(error is OtherError)
} else {
XCTFail("mapError should transform error value")
XCTFail("flatMapError should transform error value")
}
}
......@@ -347,133 +179,7 @@ class AFResultTestCase: BaseTestCase {
if let error = mappedResult.error {
XCTAssertTrue(error is ThrownError)
} else {
XCTFail("mapError should capture thrown error value")
XCTFail("flatMapError should capture thrown error value")
}
}
// MARK: - With Value or Error Tests
func testWithValueExecutesWhenSuccess() {
// Given
let result: AFResult<String> = .success("success")
var string = "failure"
// When
result.withValue { string = $0 }
// Then
XCTAssertEqual(string, "success")
}
func testWithValueDoesNotExecutesWhenFailure() {
// Given
struct ResultError: Error {}
let result: AFResult<String> = .failure(ResultError())
var string = "failure"
// When
result.withValue { string = $0 }
// Then
XCTAssertEqual(string, "failure")
}
func testWithErrorExecutesWhenFailure() {
// Given
struct ResultError: Error {}
let result: AFResult<String> = .failure(ResultError())
var string = "success"
// When
result.withError { string = "\(type(of: $0))" }
// Then
XCTAssertEqual(string, "ResultError")
}
func testWithErrorDoesNotExecuteWhenSuccess() {
// Given
let result: AFResult<String> = .success("success")
var string = "success"
// When
result.withError { string = "\(type(of: $0))" }
// Then
XCTAssertEqual(string, "success")
}
// MARK: - If Success or Failure Tests
func testIfSuccessExecutesWhenSuccess() {
// Given
let result: AFResult<String> = .success("success")
var string = "failure"
// When
result.ifSuccess { string = "success" }
// Then
XCTAssertEqual(string, "success")
}
func testIfSuccessDoesNotExecutesWhenFailure() {
// Given
struct ResultError: Error {}
let result: AFResult<String> = .failure(ResultError())
var string = "failure"
// When
result.ifSuccess { string = "success" }
// Then
XCTAssertEqual(string, "failure")
}
func testIfFailureExecutesWhenFailure() {
// Given
struct ResultError: Error {}
let result: AFResult<String> = .failure(ResultError())
var string = "success"
// When
result.ifFailure { string = "failure" }
// Then
XCTAssertEqual(string, "failure")
}
func testIfFailureDoesNotExecuteWhenSuccess() {
// Given
let result: AFResult<String> = .success("success")
var string = "success"
// When
result.ifFailure { string = "failure" }
// Then
XCTAssertEqual(string, "success")
}
// MARK: - Functional Chaining Tests
func testFunctionalMethodsCanBeChained() {
// Given
struct ResultError: Error {}
let result: AFResult<String> = .success("first")
var string = "first"
var success = false
// When
let endResult = result
.map { _ in "second" }
.flatMap { _ in "third" }
.withValue { if $0 == "third" { string = "fourth" } }
.ifSuccess { success = true }
// Then
XCTAssertEqual(endResult.value, "third")
XCTAssertEqual(string, "fourth")
XCTAssertTrue(success)
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册