// Copyright © 2014-2019 the Surge contributors // // 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 Foundation import XCTest @testable import Surge private struct ValueError: Swift.Error, CustomStringConvertible { let message: String var description: String { return self.message } } private enum ArrayError: Swift.Error, CustomStringConvertible { case size(message: String) case content(index: Int, content: ValueError) var description: String { switch self { case let .size(message): return message case let .content(index, error): return "Failure at index [\(index)]: \(error)" } } } private enum GridError: Swift.Error, CustomStringConvertible { case size(message: String) case content(index: Int, content: ArrayError) var description: String { switch self { case let .size(message): return message case let .content(gridIndex, arrayError): switch arrayError { case let .size(message): return "Failure at index [\(gridIndex), ..]: \(message)" case let .content(arrayIndex, valueError): return "Failure at index [\(gridIndex), \(arrayIndex)]: \(valueError)" } } } } private func checkValue( _ actualValue: T, _ expectedValue: T, accuracy: T ) -> Result<(), ValueError> where T: FloatingPoint { guard abs(actualValue - expectedValue) <= abs(accuracy) else { let (actual, expected) = (actualValue, expectedValue) let message = "(\(actual)) is not equal to (\(expected)) +/- (\(accuracy))" return .failure(ValueError(message: message)) } return .success(()) } private func checkArray( _ actualArray: T, _ expectedArray: T, accuracy: U ) -> Result<(), ArrayError> where T: Collection, T.Element == U, U: FloatingPoint { guard actualArray.count == expectedArray.count else { let (actual, expected) = (actualArray.count, expectedArray.count) let message = "Values have different size: (\(actual)) is not equal to (\(expected))" return .failure(.size(message: message)) } for (index, (actualValue, expectedValue)) in zip(actualArray, expectedArray).enumerated() { switch checkValue(actualValue, expectedValue, accuracy: accuracy) { case .success: continue case .failure(let error): return .failure(.content(index: index, content: error)) } } return .success(()) } private func checkGrid( _ actualGrid: T, _ expectedGrid: T, accuracy: V ) -> Result<(), GridError> where T: Collection, U: Collection, T.Element == U, U.Element == V, V: FloatingPoint { guard actualGrid.count == expectedGrid.count else { let (actual, expected) = (actualGrid.count, expectedGrid.count) let message = "Values have different size: (\(actual) × _) is not equal to (\(expected) × _)" return .failure(.size(message: message)) } for (index, (actualArray, expectedArray)) in zip(actualGrid, expectedGrid).enumerated() { switch checkArray(actualArray, expectedArray, accuracy: accuracy) { case .success: continue case .failure(let error): return .failure(.content(index: index, content: error)) } } return .success(()) } private enum Prefix: String { case assertEqual = "XCTAssertEqual" case assertEqualWithAccuracy = "XCTAssertEqualWithAccuracy" } private func fail( prefix: Prefix, failureMessage: String, userMessage: String? = nil, file: StaticString, line: UInt ) { let prefix = "\(prefix.rawValue) failed: " let suffix = userMessage.map { " - \($0)" } ?? "" let message = "\(prefix)\(failureMessage)\(suffix)" XCTFail(message, file: file, line: line) } /// Asserts that two values are equal within a certain accuracy. /// /// - Parameters: /// - expression1: An expression of type `T: Collection`, where `T.Element` conforms `FloatingPoint`. /// - expression2: An expression of type `T: Collection`, where `T.Element` conforms `FloatingPoint`. /// - accuracy: An expression of type `T.Element`, where `T.Element` conforms to `FloatingPoint`. /// Describes the maximum difference between `expression1` and `expression2` for these values to be considered equal. /// - message: An optional description of the failure. /// - file: The file in which failure occurred. Defaults to the file name of the test case in which this function was called. /// - line: The line number on which failure occurred. Defaults to the line number on which this function was called. func XCTAssertEqual( _ expression1: @autoclosure () throws -> T, _ expression2: @autoclosure () throws -> T, accuracy: T.Element? = nil, _ message: @autoclosure () -> String = "", file: StaticString = #file, line: UInt = #line ) where T: Collection, T.Element: FloatingPoint & ExpressibleByFloatLiteral { XCTAssertEqual1D( try expression1(), try expression2(), accuracy: accuracy, message(), file: file, line: line ) } /// Asserts that two values are equal within a certain accuracy. /// /// Semantically the same as its `XCTAssertEqual(_:_:accuracy:_:file:line:)` counterpart /// (i.e. without the `…1D` suffix), but with improved error messages. /// /// - Parameters: /// - expression1: An expression of type `T: Collection`, where `T.Element` conforms `FloatingPoint`. /// - expression2: An expression of type `T: Collection`, where `T.Element` conforms `FloatingPoint`. /// - accuracy: An expression of type `T.Element`, where `T.Element` conforms to `FloatingPoint`. /// Describes the maximum difference between `expression1` and `expression2` for these values to be considered equal. /// - message: An optional description of the failure. /// - file: The file in which failure occurred. Defaults to the file name of the test case in which this function was called. /// - line: The line number on which failure occurred. Defaults to the line number on which this function was called. func XCTAssertEqual1D( _ expression1: @autoclosure () throws -> T, _ expression2: @autoclosure () throws -> T, accuracy: T.Element? = nil, _ message: @autoclosure () -> String = "", file: StaticString = #file, line: UInt = #line ) where T: Collection, T.Element: FloatingPoint & ExpressibleByFloatLiteral { let prefix: Prefix = (accuracy == nil) ? .assertEqual : .assertEqualWithAccuracy let (actual, expected): (T, T) do { (actual, expected) = (try expression1(), try expression2()) } catch let error { let message = String(describing: error) return fail(prefix: prefix, failureMessage: message, file: file, line: line) } let result = checkArray(actual, expected, accuracy: accuracy ?? 0.0) guard case .failure(let error) = result else { return } return fail(prefix: prefix, failureMessage: error.description, file: file, line: line) } /// Asserts that two values are equal within a certain accuracy. /// /// - Parameters: /// - expression1: An expression of type `T: Collection`, `T.Element == U`, where `U` is `FloatingPoint`. /// - expression2: An expression of type `T: Collection`, `T.Element == U`, where `U` is `FloatingPoint`. /// - accuracy: An expression of type `U.Element`, where `U.Element` conforms to `FloatingPoint`. /// Describes the maximum difference between `expression1` and `expression2` for these values to be considered equal. /// - message: An optional description of the failure. /// - file: The file in which failure occurred. Defaults to the file name of the test case in which this function was called. /// - line: The line number on which failure occurred. Defaults to the line number on which this function was called. func XCTAssertEqual( _ expression1: @autoclosure () throws -> T, _ expression2: @autoclosure () throws -> T, accuracy: U.Element? = nil, _ message: @autoclosure () -> String = "", file: StaticString = #file, line: UInt = #line ) where T: Collection, U: Collection, T.Element == U, U.Element: FloatingPoint & ExpressibleByFloatLiteral { XCTAssertEqual2D( try expression1(), try expression2(), accuracy: accuracy, message(), file: file, line: line ) } /// Asserts that two values are equal within a certain accuracy. /// /// Semantically the same as its `XCTAssertEqual(_:_:accuracy:_:file:line:)` counterpart /// (i.e. without the `…2D` suffix), but with improved error messages. /// /// - Parameters: /// - expression1: An expression of type `T: Collection`, `T.Element == U`, where `U` is `FloatingPoint`. /// - expression2: An expression of type `T: Collection`, `T.Element == U`, where `U` is `FloatingPoint`. /// - accuracy: An expression of type `U.Element`, where `U.Element` conforms to `FloatingPoint`. /// Describes the maximum difference between `expression1` and `expression2` for these values to be considered equal. /// - message: An optional description of the failure. /// - file: The file in which failure occurred. Defaults to the file name of the test case in which this function was called. /// - line: The line number on which failure occurred. Defaults to the line number on which this function was called. func XCTAssertEqual2D( _ expression1: @autoclosure () throws -> T, _ expression2: @autoclosure () throws -> T, accuracy: U.Element? = nil, _ message: @autoclosure () -> String = "", file: StaticString = #file, line: UInt = #line ) where T: Collection, U: Collection, T.Element == U, U.Element: FloatingPoint & ExpressibleByFloatLiteral { let prefix: Prefix = (accuracy == nil) ? .assertEqual : .assertEqualWithAccuracy let (actual, expected): (T, T) do { (actual, expected) = (try expression1(), try expression2()) } catch let error { let message = String(describing: error) return fail(prefix: prefix, failureMessage: message, file: file, line: line) } let result = checkGrid(actual, expected, accuracy: accuracy ?? 0.0) guard case .failure(let error) = result else { return } return fail(prefix: prefix, failureMessage: error.description, file: file, line: line) }