diff --git a/.gitignore b/.gitignore index e8802b5..87c0eaf 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,4 @@ Carthage # Pods/ Example/Pods/ +.build diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..34bf245 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,29 @@ +# Repository Guidelines + +## Project Structure & Module Organization +`Package.swift` defines four library targets: + +- `DBC-swift/`: core Swift API such as `require`, `check`, and `ensure` +- `DBC-objc/`: Objective-C implementation and public headers +- `DBC-bridged/`: bridge layer that combines Swift and Objective-C targets +- `DBC-testing/`: test helpers such as custom XCTest assertions + +SwiftPM tests live under `Example/Tests/swift` and `Example/Tests/objc`. The `Example/` directory also contains legacy iOS, tvOS, and macOS sample app targets plus the Xcode project/workspace used for manual verification. + +## Build, Test, and Development Commands +- `swift build`: builds all SwiftPM library targets. +- `swift test`: runs the Swift and Objective-C package test targets. +- `swift test --filter SwiftDBCTests`: runs a single XCTest class while iterating. +- `xcodebuild -project Example/DBC.xcodeproj -scheme DBC-Example test`: exercises the example project test flow when SwiftPM coverage is not enough. +- `cd Example && pod install`: refreshes CocoaPods dependencies for the example workspace if you need to open it in Xcode. + +Run commands from the repository root unless the command says otherwise. + +## Coding Style & Naming Conventions +Match the surrounding file before “cleaning up” style. Existing Swift and Objective-C sources use tabs in many files, `UpperCamelCase` for types, and descriptive `lowerCamelCase` for functions and variables. Keep assertion API names aligned with the library vocabulary (`require`, `check`, `ensure`, `inform`). There is no configured formatter or linter in this repo, so avoid unrelated whitespace churn. + +## Testing Guidelines +Tests use `XCTest` plus helpers from `DBC-testing`. Name new tests with the `test...` prefix and group them by behavior, as in `SwiftDBCTests`. Add regression tests for any assertion semantics, intensity handling, or bridging behavior you change. Prefer `swift test` first, then fall back to `xcodebuild` only for example-app-specific coverage. + +## Commit & Pull Request Guidelines +Recent history uses short, imperative commit subjects such as `Support Swift Package manager` and `Swift 5 update`. Keep commits focused and scoped to one concern. PRs should include a brief behavior summary, the test command(s) you ran, and links to any relevant issue. Include screenshots only when touching the sample apps’ UI. diff --git a/DBC-bridged/DBCIntensityBridged.swift b/DBC-bridged/DBCIntensityBridged.swift index 0d5dd81..1b59ed0 100644 --- a/DBC-bridged/DBCIntensityBridged.swift +++ b/DBC-bridged/DBCIntensityBridged.swift @@ -26,9 +26,7 @@ import DBC_objc @objc static public var intensityLevel: Int = 0 { didSet { dbcIntensityLevel = intensityLevel - #if DEBUG - DBC_SetDebugIntensityLevel(intensityLevel); - #endif + DBC_SetDebugIntensityLevel(intensityLevel) } } } diff --git a/DBC-objc/DBCIntensityLevel.h b/DBC-objc/DBCIntensityLevel.h index 236acb7..ff948d0 100644 --- a/DBC-objc/DBCIntensityLevel.h +++ b/DBC-objc/DBCIntensityLevel.h @@ -11,20 +11,18 @@ #define __DBCINTENSITYLEVEL__ /** - Set "DBC_DebugIntensityLevel" to some value to execute intense debugging/messaging code. - Allows you to enter intense debugging/messaging code at some level greater then zero. + Set "DBC_DebugIntensityLevel" to some value to execute intensity-gated messaging. + Allows you to enter intensity-gated messaging code at some level greater than zero. When you back off the intensity level, you can leave the code in place without execution until that intensity level is required again. "DBC_DebugIntensityLevel" defaults to zero. If a higher intensity level is required, it should be changed to the higher level in the debugger at runtime. - Setting "DBC_DebugIntensityLevel to a value less then zero effectively turns debugging/messaging off - for these calls + Setting "DBC_DebugIntensityLevel" to a value less than zero effectively turns intensity-gated messaging off + for these calls. */ -#ifdef DEBUG - @import Foundation; #ifdef __cplusplus @@ -34,7 +32,7 @@ extern "C" { extern NSInteger DBC_DebugIntensityLevel(void); extern void DBC_SetDebugIntensityLevel(NSInteger intensityLevel); - /// Utility function to perform a provided closure `block` if `DBC_DebugIntensityLevel` is at or greater then the target `intensity` level. + /// Utility function to perform a provided closure `block` if `DBC_DebugIntensityLevel` is at or greater than the target `intensity` level. /// See `DBC_DebugIntensityLevel`. extern void DBC_performIfDBCIntensity(NSInteger intensity, void (^ _Nonnull block)(void)); @@ -43,5 +41,3 @@ extern "C" { #endif #endif - -#endif diff --git a/DBC-objc/DBCIntensityLevel.m b/DBC-objc/DBCIntensityLevel.m index 5772a8c..01779cc 100644 --- a/DBC-objc/DBCIntensityLevel.m +++ b/DBC-objc/DBCIntensityLevel.m @@ -8,8 +8,6 @@ #import "DBCIntensityLevel.h" -#ifdef DEBUG - #ifndef vIntenseDebugging #define vIntenseDebugging 0 #endif @@ -37,5 +35,3 @@ void DBC_performIfDBCIntensity(NSInteger intensity, void (^ _Nonnull block)(void block(); } } - -#endif diff --git a/DBC-swift/DBC.swift b/DBC-swift/DBC.swift index 99b0ed8..b55a2b7 100644 --- a/DBC-swift/DBC.swift +++ b/DBC-swift/DBC.swift @@ -23,10 +23,10 @@ // documentation of what every component expects (precondition), what it guarantees // in return (postcondition) and what general conditions it maintains (invariant). // -// The following methods assist in "Design by contract". Each will check the "condition" -// passed in. If the condition fails, then each will display a message to the debugger -// console and throw a swift assertion error, an attempt will also be made to break in the -// debugger. +// The following methods assist in "Design by contract". Each checks the supplied condition. +// When a condition fails, the library either stops execution using the matching Swift +// assertion primitive or emits an `inform` message, depending on the assertion kind, +// build configuration, and active intensity level. // // Syntactically, these assertions are boolean expressions and although they perform // identical tasks, each is semantically different. @@ -95,7 +95,7 @@ import Foundation /// /// - SeeAlso: precondition() /// - SeeAlso: `DBCIntensityLevel.swift` -public func require(_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String = "", intensity: Int = 0, file: StaticString = #file, line: UInt = #line) { +public func require(_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String = "", intensity: Int = 0, file: StaticString = #fileID, line: UInt = #line) { if (intensity <= dbcIntensityLevel) { Assertions.precondition(condition(), "failed require : \(message())", file, line) } @@ -122,7 +122,7 @@ public func require(_ condition: @autoclosure () -> Bool, _ message: @autoclosu /// /// - SeeAlso: preconditionFailure() /// - SeeAlso: `DBCIntensityLevel.swift` -public func requireFailure(_ message: @autoclosure () -> String, intensity:Int = 0, file: StaticString = #file, line: UInt = #line) { +public func requireFailure(_ message: @autoclosure () -> String, intensity:Int = 0, file: StaticString = #fileID, line: UInt = #line) { if (intensity <= dbcIntensityLevel) { Assertions.preconditionFailure("failed require : \(message())", file, line) } @@ -131,110 +131,81 @@ public func requireFailure(_ message: @autoclosure () -> String, intensity:Int = } } - // MARK: - Postconditions , introduced by the keyword ensure /// Postconditions express conditions that the routine (the supplier) /// guarantees on return, if the preconditions where satisfied on entry. -/// Check a promised postcondition before leaving a roiutine. -///. -/// Use this function to validate postconditions active during testing -/// but will not impact performance of shipping code. +/// Check a promised postcondition before leaving a routine. +/// +/// Use this function to validate postconditions during testing. In release builds, +/// failed checks log through `inform` when `intensity <= dbcIntensityLevel`. /// -/// - Note: Active during testing/debuging but will not impact performance of shipping code. +/// - Note: In debug builds, disabled intensities still surface failures through `inform`. /// /// - SeeAlso: assert() /// - SeeAlso: `DBCIntensityLevel.swift` -public func ensure(_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String = "", intensity: Int = 0, file: StaticString = #file, line: UInt = #line) { -#if DEBUG - if (intensity <= dbcIntensityLevel) { - Assertions.assert(condition(), "failed ensure : \(message())", file, line) - } - else { - informIf(!condition(), "failed ensure(\(intensity)) : \(message())", intensity: Int.min, debuggerBreak: dbcBreakOnAssertionsFailures, file: file, line: line) - } -#endif +public func ensure(_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String = "", intensity: Int = 0, file: StaticString = #fileID, line: UInt = #line) { + AssertionSupport.performDebugAssertion(condition: condition, assertion: "ensure", message: message, intensity: intensity, file: file, line: line, debugAssert: Assertions.assert) } /// Indicate that a postcondition was violated. /// -/// Use this function to stop the program, without impacting the -/// performance of shipping code, when control flow is not expected to -/// reach the call. +/// Use this function to stop the program when control flow is not expected to +/// reach the call during testing. In release builds, failures log through `inform` +/// when `intensity <= dbcIntensityLevel`. /// -/// - Note: Active during testing/debuging but will not impact performance of shipping code. +/// - Note: In debug builds, disabled intensities still surface failures through `inform`. /// /// - SeeAlso: assertFailure() /// - SeeAlso: `DBCIntensityLevel.swift` -public func ensureFailure(_ message: @autoclosure () -> String, intensity: Int = 0, file: StaticString = #file, line: UInt = #line) { -#if DEBUG - if (intensity <= dbcIntensityLevel) { - Assertions.assertionFailure("failed ensure : \(message())", file, line) - } - else { - inform("failed ensure(\(intensity)): \(message())", intensity: Int.min, debuggerBreak: dbcBreakOnAssertionsFailures, file: file, line: line) - } -#endif +public func ensureFailure(_ message: @autoclosure () -> String, intensity: Int = 0, file: StaticString = #fileID, line: UInt = #line) { + AssertionSupport.performDebugAssertionFailure("ensure", message: message, intensity: intensity, file: file, line: line, debugAssertFailure: Assertions.assertionFailure) } -// MARK: - Runtime asssertions, introduced by the keyword check +// MARK: - Runtime assertions, introduced by the keyword check /// Runtime checks express/assert the expected values of (computed) variables /// and their relationships within the routine. -/// Use this function for internal sanity checks that are active -/// during testing but will not impact performance of shipping code. +/// Use this function for internal sanity checks during testing. In release builds, +/// failed checks log through `inform` when `intensity <= dbcIntensityLevel`. /// -/// - Note: Active during testing/debuging but will not impact performance of shipping code. +/// - Note: In debug builds, disabled intensities still surface failures through `inform`. /// /// - SeeAlso: assert() /// - SeeAlso: `DBCIntensityLevel.swift` -public func check(_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String = "", intensity: Int = 0, file: StaticString = #file, line: UInt = #line) { -#if DEBUG - if (intensity <= dbcIntensityLevel) { - Assertions.assert(condition(), "failed check : \(message())", file, line) - } - else { - informIf(!condition(), "failed check(\(intensity)) : \(message())", intensity: Int.min, debuggerBreak: dbcBreakOnAssertionsFailures, file: file, line: line) - } -#endif +public func check(_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String = "", intensity: Int = 0, file: StaticString = #fileID, line: UInt = #line) { + AssertionSupport.performDebugAssertion(condition: condition, assertion: "check", message: message, intensity: intensity, file: file, line: line, debugAssert: Assertions.assert) } /// Indicate that an internal sanity check failed. /// -/// Use this function to stop the program, without impacting the -/// performance of shipping code, when control flow is not expected to -/// reach the call. +/// Use this function to stop the program when control flow is not expected to +/// reach the call during testing. In release builds, failures log through `inform` +/// when `intensity <= dbcIntensityLevel`. /// -/// - Note: Active during testing/debuging but will not impact performance of shipping code. +/// - Note: In debug builds, disabled intensities still surface failures through `inform`. /// /// - SeeAlso: assertFailure() /// - SeeAlso: `DBCIntensityLevel.swift` -public func checkFailure(_ message: @autoclosure () -> String, intensity: Int = 0, file: StaticString = #file, line: UInt = #line) { -#if DEBUG - if (intensity <= dbcIntensityLevel) { - Assertions.assertionFailure("failed check : \(message())", file, line) - } - else { - inform("failed check(\(intensity)) : \(message())", intensity: Int.min, debuggerBreak: dbcBreakOnAssertionsFailures, file: file, line: line) - } -#endif +public func checkFailure(_ message: @autoclosure () -> String, intensity: Int = 0, file: StaticString = #fileID, line: UInt = #line) { + AssertionSupport.performDebugAssertionFailure("check", message: message, intensity: intensity, file: file, line: line, debugAssertFailure: Assertions.assertionFailure) } -/// Set to 'true' to break in the debugger when assertions fail yet are disabled due to intensity level. +/// Set to `true` to break in the debugger when assertion failures are reported through fallback `inform` logging. /// -/// DBC assertions that fail their condition but are silenced due to intensity level will still print the -/// failure to the debug console. When `dbcBreakOnAssertionsFailures` is `true` these conditions will also -/// break in the debugger. +/// This applies both when debug assertions are silenced by the intensity gate and when `check`/`ensure` +/// fall back to `inform` logging in release builds. When `dbcBreakOnAssertionsFailures` is `true` +/// these reported failures will also break in the debugger. /// /// Default is `false` -public var dbcBreakOnAssertionsFailures: Bool = false +public var dbcBreakOnAssertionsFailures: Bool { + get { DBCConfigurationStorage.dbcBreakOnAssertionsFailures } + set { DBCConfigurationStorage.dbcBreakOnAssertionsFailures = newValue } +} // MARK: - Assertions class, custom assertions closures -/// Stores custom assertions closures, by default each closure envolks Swift assertion functions, but test targets can override them. -/// -/// Also by default, in addition to envolking Swift assertion functions, each assertion closure throws an NSException which will -/// enable crash reporting tools like Crashlytics to pick up all the metadata of the crash. -/// See +/// Stores custom assertion closures. By default each closure delegates to the matching Swift assertion function, +/// but test targets can override them. /// /// - SeeAlso: XCTestCase+DBCAssertions.swift open class Assertions { @@ -242,93 +213,100 @@ open class Assertions { public typealias assertClosure = (@autoclosure () -> Bool, @autoclosure () -> String, StaticString, UInt) -> Void public typealias assertFailureClosure = (@autoclosure () -> String, StaticString, UInt) -> Void - public static var assert: assertClosure = swiftAssert - public static var assertionFailure: assertFailureClosure = swiftAssertionFailure - public static var precondition: assertClosure = swiftPrecondition - public static var preconditionFailure: assertFailureClosure = swiftPreconditionFailure - public static var fatalError: assertFailureClosure = swiftFatalError + public static var assert: assertClosure { + get { DBCConfigurationStorage.assert } + set { DBCConfigurationStorage.assert = newValue } + } + + public static var assertionFailure: assertFailureClosure { + get { DBCConfigurationStorage.assertionFailure } + set { DBCConfigurationStorage.assertionFailure = newValue } + } + + public static var precondition: assertClosure { + get { DBCConfigurationStorage.precondition } + set { DBCConfigurationStorage.precondition = newValue } + } + + public static var preconditionFailure: assertFailureClosure { + get { DBCConfigurationStorage.preconditionFailure } + set { DBCConfigurationStorage.preconditionFailure = newValue } + } + + public static var fatalError: assertFailureClosure { + get { DBCConfigurationStorage.fatalError } + set { DBCConfigurationStorage.fatalError = newValue } + } public static let swiftAssert: assertClosure = { (condition: @autoclosure () -> Bool, message: @autoclosure () -> String, file: StaticString, line: UInt) -> Void in - - #if !os(Linux) - if !condition() { - let exception = NSException( - name: .internalInconsistencyException, - reason: "\(message()) in \(file), at line \(line)", - userInfo: nil - ) - - exception.raise() - } - #endif - Swift.assert(condition(), message(), file: file, line: line) } public static let swiftAssertionFailure: assertFailureClosure = { (message: @autoclosure () -> String, file: StaticString, line: UInt) -> Void in - - #if !os(Linux) - let exception = NSException( - name: .internalInconsistencyException, - reason: "\(message()) in \(file), at line \(line)", - userInfo: nil - ) - - exception.raise() - #endif - Swift.assertionFailure(message(), file: file, line: line) } public static let swiftPrecondition: assertClosure = { (condition: @autoclosure () -> Bool, message: @autoclosure () -> String, file: StaticString, line: UInt) -> Void in - - #if !os(Linux) - if !condition() { - let exception = NSException( - name: .internalInconsistencyException, - reason: "\(message()) in \(file), at line \(line)", - userInfo: nil - ) - - exception.raise() - } - #endif - Swift.precondition(condition(), message(), file: file, line: line) } public static let swiftPreconditionFailure: assertFailureClosure = { (message: @autoclosure () -> String, file: StaticString, line: UInt) -> Void in - - #if !os(Linux) - let exception = NSException( - name: .internalInconsistencyException, - reason: "\(message()) in \(file), at line \(line)", - userInfo: nil - ) - - exception.raise() - #endif - Swift.preconditionFailure(message(), file: file, line: line) } public static let swiftFatalError: assertFailureClosure = { (message: @autoclosure () -> String, file: StaticString, line: UInt) -> Void in - - #if !os(Linux) - let exception = NSException( - name: .internalInconsistencyException, - reason: "\(message()) in \(file), at line \(line)", - userInfo: nil - ) - - exception.raise() - #endif - Swift.fatalError(message(), file: file, line: line) } } + +private enum AssertionSupport { + static func informAssertionIfFailed(condition: () -> Bool, assertion: String, message: () -> String, intensity: Int, file: StaticString, line: UInt, forceLogging: Bool) { + let informIntensity = forceLogging ? Int.min : intensity + + if !forceLogging && intensity > dbcIntensityLevel { + return + } + + let failed = !condition() + informIf(failed, "failed \(assertion)(\(intensity)): \(message())", intensity: informIntensity, debuggerBreak: dbcBreakOnAssertionsFailures, file: file, line: line) + } + + static func informAssertionFailure(_ assertion: String, message: () -> String, intensity: Int, file: StaticString, line: UInt, forceLogging: Bool) { + let informIntensity = forceLogging ? Int.min : intensity + inform("failed \(assertion)(\(intensity)): \(message())", intensity: informIntensity, debuggerBreak: dbcBreakOnAssertionsFailures, file: file, line: line) + } + + // `check` and `ensure` are debug assertions, but when they are disabled by intensity + // in a debug build we still surface the failure through `inform` so the signal is not lost. + static func performDebugAssertion(condition: () -> Bool, assertion: String, message: () -> String, intensity: Int, file: StaticString, line: UInt, debugAssert: Assertions.assertClosure) { +#if DEBUG + if intensity <= dbcIntensityLevel { + let assertionCondition = condition() + let failureMessage = assertionCondition ? "" : "failed \(assertion) : \(message())" + debugAssert(assertionCondition, failureMessage, file, line) + } else { + informAssertionIfFailed(condition: condition, assertion: assertion, message: message, intensity: intensity, file: file, line: line, forceLogging: true) + } +#else + informAssertionIfFailed(condition: condition, assertion: assertion, message: message, intensity: intensity, file: file, line: line, forceLogging: false) +#endif + } + + static func performDebugAssertionFailure(_ assertion: String, message: () -> String, intensity: Int, file: StaticString, line: UInt, debugAssertFailure: Assertions.assertFailureClosure) { +#if DEBUG + if intensity <= dbcIntensityLevel { + let failureMessage = "failed \(assertion) : \(message())" + debugAssertFailure(failureMessage, file, line) + } else { + informAssertionFailure(assertion, message: message, intensity: intensity, file: file, line: line, forceLogging: true) + } +#else + informAssertionFailure(assertion, message: message, intensity: intensity, file: file, line: line, forceLogging: false) +#endif + } +} diff --git a/DBC-swift/DBCConfigurationStorage.swift b/DBC-swift/DBCConfigurationStorage.swift new file mode 100644 index 0000000..85d43c1 --- /dev/null +++ b/DBC-swift/DBCConfigurationStorage.swift @@ -0,0 +1,68 @@ +// +// DBCConfigurationStorage.swift +// Pods +// +// Created by Jim Boyd on 7/22/16. +// +// + +import Foundation + +enum DBCConfigurationStorage { + private static let lock = NSLock() + + private static var _dbcIntensityLevel: Int = 0 + private static var _dbcLogger: DBCLogger = DBCDebugPrintLogger() + private static var _dbcBreakOnAssertionsFailures: Bool = false + private static var _assert: Assertions.assertClosure = Assertions.swiftAssert + private static var _assertionFailure: Assertions.assertFailureClosure = Assertions.swiftAssertionFailure + private static var _precondition: Assertions.assertClosure = Assertions.swiftPrecondition + private static var _preconditionFailure: Assertions.assertFailureClosure = Assertions.swiftPreconditionFailure + private static var _fatalError: Assertions.assertFailureClosure = Assertions.swiftFatalError + + static func withLock(_ body: () -> Result) -> Result { + lock.lock() + defer { lock.unlock() } + return body() + } + + static var dbcIntensityLevel: Int { + get { withLock { _dbcIntensityLevel } } + set { withLock { _dbcIntensityLevel = newValue } } + } + + static var dbcLogger: DBCLogger { + get { withLock { _dbcLogger } } + set { withLock { _dbcLogger = newValue } } + } + + static var dbcBreakOnAssertionsFailures: Bool { + get { withLock { _dbcBreakOnAssertionsFailures } } + set { withLock { _dbcBreakOnAssertionsFailures = newValue } } + } + + static var assert: Assertions.assertClosure { + get { withLock { _assert } } + set { withLock { _assert = newValue } } + } + + static var assertionFailure: Assertions.assertFailureClosure { + get { withLock { _assertionFailure } } + set { withLock { _assertionFailure = newValue } } + } + + static var precondition: Assertions.assertClosure { + get { withLock { _precondition } } + set { withLock { _precondition = newValue } } + } + + static var preconditionFailure: Assertions.assertFailureClosure { + get { withLock { _preconditionFailure } } + set { withLock { _preconditionFailure = newValue } } + } + + static var fatalError: Assertions.assertFailureClosure { + get { withLock { _fatalError } } + set { withLock { _fatalError = newValue } } + } +} diff --git a/DBC-swift/DBCInform.swift b/DBC-swift/DBCInform.swift index 0f2a12e..9908897 100644 --- a/DBC-swift/DBCInform.swift +++ b/DBC-swift/DBCInform.swift @@ -10,6 +10,23 @@ import Foundation // MARK: - Messaging, introduced by the keyword inform +public protocol DBCLogger { + func log(_ message: String, separator: String, terminator: String, file: StaticString, line: UInt) +} + +public struct DBCDebugPrintLogger: DBCLogger { + public init() {} + + public func log(_ message: String, separator: String, terminator: String, file: StaticString, line: UInt) { + Swift.debugPrint(message, file, line, separator: separator, terminator: terminator) + } +} + +public var dbcLogger: DBCLogger { + get { DBCConfigurationStorage.dbcLogger } + set { DBCConfigurationStorage.dbcLogger = newValue } +} + /// Writes the textual representations of `items`, separated by /// `separator` and terminated by `terminator`, into the standard /// output. @@ -22,20 +39,18 @@ import Foundation /// The textual representations are obtained for each `item` via /// the expression `String(item)`. /// -/// - Note: Active during testing/debuging but will not impact performance of shipping code. +/// - Note: Active in all builds when `intensity <= dbcIntensityLevel`. /// - Note: to print without a trailing newline, pass `terminator: ""` /// /// - SeeAlso: `print`, `debugPrint` /// - SeeAlso: `DBCIntensityLevel.swift`, `AmIBeingDebugged.swift` -public func inform(_ message: @autoclosure () -> String, separator: String = ", ", terminator: String = "\n", intensity: Int = 0, debuggerBreak: Bool = false, file: StaticString = #file, line: UInt = #line) { - #if DEBUG - if (intensity <= dbcIntensityLevel) { - Swift.debugPrint(message(), file, line, separator: separator, terminator: terminator) - if debuggerBreak && amIBeingDebugged() { - raise(SIGSTOP) - } +public func inform(_ message: @autoclosure () -> String, separator: String = ", ", terminator: String = "\n", intensity: Int = 0, debuggerBreak: Bool = false, file: StaticString = #fileID, line: UInt = #line) { + if (intensity <= dbcIntensityLevel) { + dbcLogger.log(message(), separator: separator, terminator: terminator, file: file, line: line) + if debuggerBreak && amIBeingDebugged() { + raise(SIGSTOP) } - #endif + } } /// If `condition` is true: writes the textual representations of `items`, @@ -50,18 +65,16 @@ public func inform(_ message: @autoclosure () -> String, separator: String = ", /// The textual representations are obtained for each `item` via /// the expression `String(item)`. /// -/// - Note: Active during testing/debuging but will not impact performance of shipping code. +/// - Note: Active in all builds when `intensity <= dbcIntensityLevel`. /// - Note: to print without a trailing newline, pass `terminator: ""` /// /// - SeeAlso: `print`, `debugPrint` /// - SeeAlso: `DBCIntensityLevel.swift`, `AmIBeingDebugged.swift` -public func informIf(_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String, separator: String = ", ", terminator: String = "\n", intensity: Int = 0, debuggerBreak: Bool = false, file: StaticString = #file, line: UInt = #line) { - #if DEBUG - if (intensity <= dbcIntensityLevel) && condition() { - Swift.debugPrint(message(), file, line, separator: separator, terminator: terminator) - if debuggerBreak && amIBeingDebugged() { - raise(SIGSTOP) - } +public func informIf(_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String, separator: String = ", ", terminator: String = "\n", intensity: Int = 0, debuggerBreak: Bool = false, file: StaticString = #fileID, line: UInt = #line) { + if (intensity <= dbcIntensityLevel) && condition() { + dbcLogger.log(message(), separator: separator, terminator: terminator, file: file, line: line) + if debuggerBreak && amIBeingDebugged() { + raise(SIGSTOP) } - #endif + } } diff --git a/DBC-swift/DBCIntensityLevel.swift b/DBC-swift/DBCIntensityLevel.swift index 51389ee..ae762a9 100644 --- a/DBC-swift/DBCIntensityLevel.swift +++ b/DBC-swift/DBCIntensityLevel.swift @@ -19,18 +19,21 @@ import Foundation /// /// `dbcIntensityLevel` defaults to zero. /// -/// Setting `dbcIntensityLevel` to a value less then zero effectively disables assertions/messaging. -public var dbcIntensityLevel: Int = 0 +/// Setting `dbcIntensityLevel` to a value less than zero disables normal intensity-gated +/// assertions and messaging. In debug builds, suppressed assertion failures may still emit +/// fallback `inform` logs so the signal is not lost while debugging. +public var dbcIntensityLevel: Int { + get { DBCConfigurationStorage.dbcIntensityLevel } + set { DBCConfigurationStorage.dbcIntensityLevel = newValue } +} /// Performs `block` closure if `intensity` is set at or below `dbcIntensityLevel`. -/// - Note: Active during testing/debuging but will not impact performance of shipping code. +/// - Note: Active in all builds when `intensity <= dbcIntensityLevel`. /// - SeeAlso: dbcIntensityLevel public func performIfDBCIntensity(_ intensity: Int, block: ()->Void) { -#if DEBUG if (intensity <= dbcIntensityLevel) { block() } -#endif } diff --git a/DBC-swift/Optional+DBC.swift b/DBC-swift/Optional+DBC.swift index 568b7d3..9523045 100644 --- a/DBC-swift/Optional+DBC.swift +++ b/DBC-swift/Optional+DBC.swift @@ -12,9 +12,55 @@ import Foundation +/// A typed Swift error emitted by the throwing optional helpers. +public struct DBCOptionalError: LocalizedError { + public enum Kind: Equatable { + case require + case check + case ensure + } + + public let kind: Kind + public let message: String + public let file: StaticString + public let function: StaticString + public let line: UInt + + public var errorDescription: String? { + return message + } + + public var nsError: NSError { + return NSError( + domain: legacyNSErrorDomain, + code: legacyNSErrorCode, + userInfo: [ + NSLocalizedDescriptionKey: message, + "file": String(describing: file), + "function": String(describing: function), + "line": Int(line) + ] + ) + } + + private var legacyNSErrorDomain: String { + return "DBC ERROR " + String(describing: kind).uppercased() + } + + private var legacyNSErrorCode: Int { + switch kind { + case .require: + return 99990 + case .check: + return 99991 + case .ensure: + return 99992 + } + } +} + public extension Optional { - /// Require this optional to contain a non-nil value /// /// This method will either return the value that this optional contains, or trigger @@ -26,17 +72,13 @@ public extension Optional { /// - SeeAlso: DBC.require() /// /// - return: The value this optional contains. - func require(_ message: String? = nil, file: StaticString = #file, line: UInt = #line, method: StaticString = #function) -> Wrapped { - var msg = "Required optional is nil." - - if let message = message, !message.isEmpty { - msg = message + func require(_ message: String? = nil, file: StaticString = #fileID, line: UInt = #line, method: StaticString = #function) -> Wrapped { + let msg = resolvedMessage("Required optional is nil.", customMessage: message, method: method) + guard let wrapped = self else { + return failRequire(msg, file: file, line: line) } - msg += " In \(method)." - - DBC.require(self != nil, msg, file: file, line: line) - return self.unsafelyUnwrapped + return wrapped } /// Require this optional to contain a non-nil value that can be cast to type CastType @@ -50,21 +92,11 @@ public extension Optional { /// - SeeAlso: DBC.require() /// /// - return: The value this optional contains cast to type CastType. - func requireCast(_ message: String? = nil, file: StaticString = #file, line: UInt = #line, method: StaticString = #function) -> CastType { - guard let castValue = self.require(message, file:file, line:line, method:method) as? CastType else { - var msg = "" - - if let message = message, !message.isEmpty { - msg = message - } - else { - msg = "Failed to cast value of type \(type(of: self)) to \(CastType.self)." - } - - msg += " In \(method)." - - requireFailure(msg, file: file, line: line) - return (self.unsafelyUnwrapped as! CastType) + func requireCast(_ message: String? = nil, file: StaticString = #fileID, line: UInt = #line, method: StaticString = #function) -> CastType { + let value = self.require(message, file: file, line: line, method: method) + guard let castValue = value as? CastType else { + let msg = resolvedMessage("Failed to cast value of type \(type(of: self)) to \(CastType.self).", customMessage: message, method: method) + return failRequire(msg, file: file, line: line) } return castValue @@ -82,16 +114,196 @@ public extension Optional { /// - SeeAlso: `DBCIntensityLevel.swift` /// /// - return: The value this optional contains. - func check(_ message: String? = nil, intensity: Int = 0, file: StaticString = #file, line: UInt = #line, method: StaticString = #function) -> Wrapped? { - var msg = "Checked optional is nil." - - if let message = message, !message.isEmpty { - msg = message - } - - msg += " In \(method)" - + func check(_ message: String? = nil, intensity: Int = 0, file: StaticString = #fileID, line: UInt = #line, method: StaticString = #function) -> Wrapped? { + let msg = resolvedMessage("Checked optional is nil.", customMessage: message, method: method) DBC.check(self != nil, msg, intensity:intensity, file: file, line: line) return self } + + // MARK: - Versions That Throw Errors + + /// Require that this optional wraps a non-nil value. + /// If nil, a `DBCOptionalError` is thrown. + /// + /// This method will either return the wrapped value, or throw a `DBCOptionalError` + /// containing debug information. + /// + /// On failure, this method emits an `inform` log before rethrowing the error. + /// + /// - parameter message: Optionally pass a message that will get included in any error + /// message generated in case nil was found. + /// + /// - SeeAlso: DBC.require() + /// + /// - return: The optional's wrapped value, or throws `DBCOptionalError`. + func required(_ message: String? = nil, file: StaticString = #fileID, line: UInt = #line, method: StaticString = #function) throws -> Wrapped { + do { + return try assertNonNil(.require, message: message, file: file, line: line, method: method) + } catch { + reportError(error, message: message) + throw error + } + } + + /// Require that this optional wraps a non-nil value that can be cast to `CastType`. + /// If nil, or the cast fails, a `DBCOptionalError` is thrown. + /// + /// This method will either return the wrapped value cast to CastType, + /// or throw a `DBCOptionalError` containing debug information. + /// + /// On failure, this method emits an `inform` log before rethrowing the error. + /// + /// - parameter message: Optionally pass a message that will get included in any error + /// message generated in case nil was found. + /// + /// - SeeAlso: DBC.require() + /// + /// - return: The optional's wrapped value cast to `CastType`, or throws `DBCOptionalError`. + func requiredCast(_ message: String? = nil, file: StaticString = #fileID, line: UInt = #line, method: StaticString = #function) throws -> CastType { + do { + return try assertCast(.require, message: message, file: file, line: line, method: method) + } catch { + reportError(error, message: message) + throw error + } + } + + func requiredCast(to: CastType.Type, message: String? = nil, file: StaticString = #fileID, line: UInt = #line, method: StaticString = #function) throws -> CastType { + do { + return try assertCast(.require, message: message, file: file, line: line, method: method, toType: to) + } catch { + reportError(error, message: message) + throw error + } + } + + /// Check that this optional wraps a non-nil value. + /// If nil, a `DBCOptionalError` is thrown. + /// + /// This method will either return the wrapped value, or throw a `DBCOptionalError` + /// containing debug information. + /// + /// On failure, this method emits an `inform` log before rethrowing the error. + /// + /// - parameter message: Optionally pass a message that will get included in any error + /// message generated in case nil was found. + /// + /// - SeeAlso: DBC.check() + /// + /// - return: The optional's wrapped value, or throws `DBCOptionalError`. + func checked(_ message: String? = nil, file: StaticString = #fileID, line: UInt = #line, method: StaticString = #function) throws -> Wrapped { + do { + return try assertNonNil(.check, message: message, file: file, line: line, method: method) + } catch { + reportError(error, message: message) + throw error + } + } + + /// Check that this optional wraps a non-nil value that can be cast to `CastType`. + /// If nil, or the cast fails, a `DBCOptionalError` is thrown. + /// + /// This method will either return the wrapped value cast to CastType, + /// or throw a `DBCOptionalError` containing debug information. + /// + /// On failure, this method emits an `inform` log before rethrowing the error. + /// + /// - parameter message: Optionally pass a message that will get included in any error + /// message generated in case nil was found. + /// + /// - SeeAlso: DBC.check() + /// + /// - return: The optional's wrapped value cast to `CastType`, or throws `DBCOptionalError`. + func checkedCast(_ message: String? = nil, file: StaticString = #fileID, line: UInt = #line, method: StaticString = #function) throws -> CastType { + do { + return try assertCast(.check, message: message, file: file, line: line, method: method) + } catch { + reportError(error, message: message) + throw error + } + } + + func checkedCast(to: CastType.Type, message: String? = nil, file: StaticString = #fileID, line: UInt = #line, method: StaticString = #function) throws -> CastType { + do { + return try assertCast(.check, message: message, file: file, line: line, method: method, toType: to) + } catch { + reportError(error, message: message) + throw error + } + } +} + +private extension Optional { + enum DBCAssertType: String { + case require + case check + case ensure + + var errorKind: DBCOptionalError.Kind { + switch self { + case .require: + return .require + case .check: + return .check + case .ensure: + return .ensure + } + } + + var errorStr: String { + return "Failed " + self.rawValue.uppercased() + } + + func error(_ message: String, _ file: StaticString, _ function: StaticString, _ line: UInt) -> DBCOptionalError { + return DBCOptionalError(kind: errorKind, message: message, file: file, function: function, line: line) + } + } + + func resolvedMessage(_ defaultMessage: String, customMessage: String?, method: StaticString) -> String { + let message = (customMessage?.isEmpty == false) ? customMessage! : defaultMessage + return "\(message) In \(method)." + } + + func reportError(_ error: Error, message: String?) { + let reportedError = (error as NSError).localizedDescription + if + let message = message, + !message.isEmpty, + !reportedError.hasSuffix(": \(message)") + { + inform("\(reportedError) : \(message)") + } else { + inform(reportedError) + } + } + + // `DBC.require(false, ...)` should terminate execution. If a custom precondition + // override returns, trap rather than fabricating an invalid `ReturnType` value. + func failRequire(_ message: @autoclosure () -> String, file: StaticString, line: UInt) -> ReturnType { + DBC.require(false, message(), intensity: Int.min, file: file, line: line) + fatalError("Unreachable failRequire returned for \(ReturnType.self)") + } + + func assertNonNil(_ assertType: DBCAssertType, message: String?, file: StaticString, line: UInt, method: StaticString) throws -> Wrapped { + guard let unwrapped = self else { + let msg = (message?.isEmpty == false) ? message! : "optional is nil." + let errorMsg = "\(assertType.errorStr) : \(msg)" + throw assertType.error(errorMsg, file, method, line) + } + + return unwrapped + } + + func assertCast(_ assertType: DBCAssertType, message: String?, file: StaticString, line: UInt, method: StaticString, toType: CastType.Type = CastType.self) throws -> CastType { + let value = try self.assertNonNil(assertType, message: message, file: file, line: line, method: method) + + guard let castValue = value as? CastType else { + let msg = (message?.isEmpty == false) ? message! : "Failed to cast value (\(String(describing: self))) of type \(type(of: self)) to \(toType.self)." + let errorMsg = "\(assertType.errorStr) : \(msg)" + + throw assertType.error(errorMsg, file, method, line) + } + + return castValue + } } diff --git a/DBC-testing/XCTestCase+DBCAssertions.swift b/DBC-testing/XCTestCase+DBCAssertions.swift index 80cfc12..f15c8a2 100755 --- a/DBC-testing/XCTestCase+DBCAssertions.swift +++ b/DBC-testing/XCTestCase+DBCAssertions.swift @@ -6,14 +6,10 @@ // Copyright © 2015 mohamede1945. All rights reserved. // -/// ### IMPORTANT HOW TO USE ### -/// 1. Drop `ProgrammerAssertions.swift` to the target of your app or framework under test. Just besides your source code. -/// 2. Drop `XCTestCase+ProgrammerAssertions.swift` to your test target. Just besides your test cases. -/// 3. Use `assert`, `assertionFailure`, `precondition`, `preconditionFailure` and `fatalError` normally as you always do. -/// 4. Unit test them with the new methods `expectAssert`, `expectAssertionFailure`, `expectPrecondition`, `expectPreconditionFailure` and `expectFatalError`. +/// XCTest helpers for verifying DBC assertion closures from Swift tests. /// -/// This file is the unit test assertions. -/// For a complete project example see +/// The implementation is adapted from +/// . import Foundation import XCTest @@ -35,9 +31,9 @@ public extension XCTestCase { - parameter line: The line number that called the method. - parameter testCase: The test case to be executed that expected to fire the assertion method. */ - func expectRequire(_ expectedMessage: String? = nil, file: StaticString = #file, line: UInt = #line, testCase: @escaping () -> Void) { - DBCType.require.expect(self, expectedMessage: expectedMessage, file: file, line: line, testCase: testCase) - } + func expectRequire(_ expectedMessage: String? = nil, file: StaticString = #fileID, line: UInt = #line, testCase: @escaping () -> Void) { + DBCType.require.expect(self, expectedMessage: expectedMessage, file: file, line: line, testCase: testCase) + } /** Expects an `requireFailure` to be called. @@ -48,7 +44,7 @@ public extension XCTestCase { - parameter line: The line number that called the method. - parameter testCase: The test case to be executed that expected to fire the assertion method. */ - func expectRequireFailure(_ expectedMessage: String, file: StaticString = #file, line: UInt = #line, testCase: @escaping () -> Void) { + func expectRequireFailure(_ expectedMessage: String, file: StaticString = #fileID, line: UInt = #line, testCase: @escaping () -> Void) { DBCFailureType.requireFailure.expect(self, expectedMessage: expectedMessage, file: file, line: line, testCase: testCase) } @@ -61,7 +57,7 @@ public extension XCTestCase { - parameter line: The line number that called the method. - parameter testCase: The test case to be executed that expected to fire the assertion method. */ - func expectCheck(_ expectedMessage: String? = nil, file: StaticString = #file, line: UInt = #line, testCase: @escaping () -> Void) { + func expectCheck(_ expectedMessage: String? = nil, file: StaticString = #fileID, line: UInt = #line, testCase: @escaping () -> Void) { DBCType.check.expect(self, expectedMessage: expectedMessage, file: file, line: line, testCase: testCase) } @@ -74,7 +70,7 @@ public extension XCTestCase { - parameter line: The line number that called the method. - parameter testCase: The test case to be executed that expected to fire the assertion method. */ - func expectCheckFailure(_ expectedMessage: String, file: StaticString = #file, line: UInt = #line, testCase: @escaping () -> Void) { + func expectCheckFailure(_ expectedMessage: String, file: StaticString = #fileID, line: UInt = #line, testCase: @escaping () -> Void) { DBCFailureType.checkFailure.expect(self, expectedMessage: expectedMessage, file: file, line: line, testCase: testCase) } @@ -87,7 +83,7 @@ public extension XCTestCase { - parameter line: The line number that called the method. - parameter testCase: The test case to be executed that expected to fire the assertion method. */ - func expectEnsure(_ expectedMessage: String? = nil, file: StaticString = #file, line: UInt = #line, testCase: @escaping () -> Void) { + func expectEnsure(_ expectedMessage: String? = nil, file: StaticString = #fileID, line: UInt = #line, testCase: @escaping () -> Void) { DBCType.ensure.expect(self, expectedMessage: expectedMessage, file: file, line: line, testCase: testCase) } @@ -100,7 +96,7 @@ public extension XCTestCase { - parameter line: The line number that called the method. - parameter testCase: The test case to be executed that expected to fire the assertion method. */ - func expectEnsureFailure(_ expectedMessage: String, file: StaticString = #file, line: UInt = #line, testCase: @escaping () -> Void) { + func expectEnsureFailure(_ expectedMessage: String, file: StaticString = #fileID, line: UInt = #line, testCase: @escaping () -> Void) { DBCFailureType.ensureFailure.expect(self, expectedMessage: expectedMessage, file: file, line: line, testCase: testCase) } } @@ -282,4 +278,3 @@ private enum DBCFailureType : String, DBCTestType { } } } - diff --git a/Example/Tests/objc/DBCBridgedTests.m b/Example/Tests/objc/DBCBridgedTests.m index 57eda0a..89c34cf 100644 --- a/Example/Tests/objc/DBCBridgedTests.m +++ b/Example/Tests/objc/DBCBridgedTests.m @@ -21,6 +21,15 @@ @interface DBCBridgedTests : XCTestCase @implementation DBCBridgedTests +- (BOOL)skipWhenAssertionsAreDisabled +{ +#ifndef DEBUG + return YES; +#else + return NO; +#endif +} + - (void)setUp { [super setUp]; @@ -52,6 +61,7 @@ - (void)testBridgedIntensity - (void)testDBCIntense { + if ([self skipWhenAssertionsAreDisabled]) { return; } NSInteger wasIntensity = [DBCBridge intensityLevel]; [DBCBridge setIntensityLevel:10]; @@ -111,6 +121,7 @@ - (void)testDBCIntense - (void)testDBCIntenseMessage { + if ([self skipWhenAssertionsAreDisabled]) { return; } NSInteger wasIntensity = [DBCBridge intensityLevel]; [DBCBridge setIntensityLevel:10]; diff --git a/Example/Tests/objc/DBCTests.m b/Example/Tests/objc/DBCTests.m index e37c044..7723571 100644 --- a/Example/Tests/objc/DBCTests.m +++ b/Example/Tests/objc/DBCTests.m @@ -23,6 +23,15 @@ @interface DBCTests : XCTestCase @implementation DBCTests +- (BOOL)skipWhenAssertionsAreDisabled +{ +#ifndef DEBUG + return YES; +#else + return NO; +#endif +} + - (void)setUp { [super setUp]; @@ -44,6 +53,7 @@ - (void)tearDown - (void)testDBC { + if ([self skipWhenAssertionsAreDisabled]) { return; } // This is an example of a functional test case. XCTAssertNoThrow(REQUIRE(true)); XCTAssertNoThrow(CHECK(true)); @@ -90,6 +100,7 @@ - (void)testDBC - (void)testDBCMessage { + if ([self skipWhenAssertionsAreDisabled]) { return; } // This is an example of a functional test case. XCTAssertNoThrow(REQUIRE_MSG(true, @"Test Message")); XCTAssertNoThrow(CHECK_MSG(true, @"Test Message")); @@ -136,6 +147,7 @@ - (void)testDBCMessage - (void)testDBCIntense { + if ([self skipWhenAssertionsAreDisabled]) { return; } NSInteger wasIntensity = DBC_DebugIntensityLevel(); DBC_SetDebugIntensityLevel(10); @@ -195,6 +207,7 @@ - (void)testDBCIntense - (void)testDBCIntenseMessage { + if ([self skipWhenAssertionsAreDisabled]) { return; } NSInteger wasIntensity = DBC_DebugIntensityLevel(); DBC_SetDebugIntensityLevel(10); diff --git a/Example/Tests/swift/DBCBridgedTests.swift b/Example/Tests/swift/DBCBridgedTests.swift index f782b92..36d27f6 100644 --- a/Example/Tests/swift/DBCBridgedTests.swift +++ b/Example/Tests/swift/DBCBridgedTests.swift @@ -29,13 +29,21 @@ import DBC_testing class SwiftDBCBridgedTests: XCTestCase { override func setUp() { super.setUp() - DBCBridge.intensityLevel = 0; + DBCBridge.intensityLevel = 0 } override func tearDown() { super.tearDown() } + private func assertionsAreEnabled() -> Bool { + #if DEBUG + return true + #else + return false + #endif + } + func testBridgedIntensity() { XCTAssertTrue(DBCBridge.intensityLevel == 0) XCTAssertTrue(dbcIntensityLevel == 0) @@ -58,7 +66,8 @@ class SwiftDBCBridgedTests: XCTestCase { } func testDBCIntense() { - let wasIntensity = DBCBridge.intensityLevel; + guard assertionsAreEnabled() else { return } + let wasIntensity = DBCBridge.intensityLevel XCTAssertTrue(wasIntensity == 0) DBCBridge.intensityLevel = 10 @@ -89,7 +98,7 @@ class SwiftDBCBridgedTests: XCTestCase { check(1 == 2, intensity: 15) ensure(1 == 2, intensity: 15) - let testStr: String? = "Test"; + let testStr: String? = "Test" require(testStr != nil, intensity: 5) check(testStr != nil, intensity: 5) ensure(testStr != nil, intensity: 5) @@ -102,7 +111,7 @@ class SwiftDBCBridgedTests: XCTestCase { check(testStr == nil, intensity: 15) ensure(testStr == nil, intensity: 15) - let nilStr: String? = nil; + let nilStr: String? = nil require(nilStr == nil, intensity: 5) check(nilStr == nil, intensity: 5) ensure(nilStr == nil, intensity: 5) @@ -120,7 +129,8 @@ class SwiftDBCBridgedTests: XCTestCase { } func testDBCIntenseMessage() { - let wasIntensity: Int = DBCBridge.intensityLevel; + guard assertionsAreEnabled() else { return } + let wasIntensity: Int = DBCBridge.intensityLevel XCTAssertTrue(wasIntensity == 0) DBCBridge.intensityLevel = 10 @@ -151,7 +161,7 @@ class SwiftDBCBridgedTests: XCTestCase { check(1 == 2, "Test Message", intensity: 15) ensure(1 == 2, "Test Message", intensity: 15) - let testStr: String? = "Test"; + let testStr: String? = "Test" require(testStr != nil, "Test Message", intensity: 5) check(testStr != nil, "Test Message", intensity: 5) ensure(testStr != nil, "Test Message", intensity: 5) @@ -164,7 +174,7 @@ class SwiftDBCBridgedTests: XCTestCase { check(testStr == nil, "Test Message", intensity: 15) ensure(testStr == nil, "Test Message", intensity: 15) - let nilStr: String? = nil; + let nilStr: String? = nil require(nilStr == nil, "Test Message", intensity: 5) check(nilStr == nil, "Test Message", intensity: 5) ensure(nilStr == nil, "Test Message", intensity: 5) @@ -182,7 +192,7 @@ class SwiftDBCBridgedTests: XCTestCase { } func testDBCOff() { - let wasIntensity: Int = DBCBridge.intensityLevel; + let wasIntensity: Int = DBCBridge.intensityLevel XCTAssertTrue(wasIntensity == 0) // Setting `DBCBridge.intensityLevel` to a value less then zero effectively turns assertions/messaging off. @@ -207,7 +217,7 @@ class SwiftDBCBridgedTests: XCTestCase { check(1 == 2) ensure(1 == 2) - let testStr: String? = "Test"; + let testStr: String? = "Test" require(testStr != nil) check(testStr != nil) ensure(testStr != nil) @@ -216,7 +226,7 @@ class SwiftDBCBridgedTests: XCTestCase { check(testStr == nil) ensure(testStr == nil) - let nilStr: String? = nil; + let nilStr: String? = nil require(nilStr != nil) check(nilStr != nil) ensure(nilStr != nil) @@ -231,7 +241,7 @@ class SwiftDBCBridgedTests: XCTestCase { func testDBCMessageOff() { - let wasIntensity: Int = DBCBridge.intensityLevel; + let wasIntensity: Int = DBCBridge.intensityLevel XCTAssertTrue(wasIntensity == 0) // Setting `DBCBridge.intensityLevel` to a value less then zero effectively turns assertions/messaging off. @@ -254,7 +264,7 @@ class SwiftDBCBridgedTests: XCTestCase { check(1 == 2, "Test Message") ensure(1 == 2, "Test Message") - let testStr: String? = "Test"; + let testStr: String? = "Test" require(testStr != nil, "Test Message") check(testStr != nil, "Test Message") ensure(testStr != nil, "Test Message") @@ -263,7 +273,7 @@ class SwiftDBCBridgedTests: XCTestCase { check(testStr == nil, "Test Message") ensure(testStr == nil, "Test Message") - let nilStr: String? = nil; + let nilStr: String? = nil require(nilStr != nil, "Test Message") check(nilStr != nil, "Test Message") ensure(nilStr != nil, "Test Message") @@ -277,7 +287,7 @@ class SwiftDBCBridgedTests: XCTestCase { } func testDBCIntenseOff() { - let wasIntensity: Int = DBCBridge.intensityLevel; + let wasIntensity: Int = DBCBridge.intensityLevel XCTAssertTrue(wasIntensity == 0) // Setting `DBCBridge.intensityLevel` to a value less then zero effectively turns assertions/messaging off. @@ -308,7 +318,7 @@ class SwiftDBCBridgedTests: XCTestCase { check(1 == 2, intensity: 15) ensure(1 == 2, intensity: 15) - let testStr: String? = "Test"; + let testStr: String? = "Test" require(testStr != nil, intensity: 5) check(testStr != nil, intensity: 5) ensure(testStr != nil, intensity: 5) @@ -321,7 +331,7 @@ class SwiftDBCBridgedTests: XCTestCase { check(testStr == nil, intensity: 15) ensure(testStr == nil, intensity: 15) - let nilStr: String? = nil; + let nilStr: String? = nil require(nilStr == nil, intensity: 5) check(nilStr == nil, intensity: 5) ensure(nilStr == nil, intensity: 5) @@ -339,7 +349,7 @@ class SwiftDBCBridgedTests: XCTestCase { } func testDBCIntenseMessageOff() { - let wasIntensity: Int = DBCBridge.intensityLevel; + let wasIntensity: Int = DBCBridge.intensityLevel XCTAssertTrue(wasIntensity == 0) // Setting `DBCBridge.intensityLevel` to a value less then zero effectively turns assertions/messaging off. @@ -370,7 +380,7 @@ class SwiftDBCBridgedTests: XCTestCase { check(1 == 2, "Test Message", intensity: 15) ensure(1 == 2, "Test Message", intensity: 15) - let testStr: String? = "Test"; + let testStr: String? = "Test" require(testStr != nil, "Test Message", intensity: 5) check(testStr != nil, "Test Message", intensity: 5) ensure(testStr != nil, "Test Message", intensity: 5) @@ -383,7 +393,7 @@ class SwiftDBCBridgedTests: XCTestCase { check(testStr == nil, "Test Message", intensity: 15) ensure(testStr == nil, "Test Message", intensity: 15) - let nilStr: String? = nil; + let nilStr: String? = nil require(nilStr == nil, "Test Message", intensity: 5) check(nilStr == nil, "Test Message", intensity: 5) ensure(nilStr == nil, "Test Message", intensity: 5) @@ -400,45 +410,45 @@ class SwiftDBCBridgedTests: XCTestCase { XCTAssertTrue(DBCBridge.intensityLevel == 0) } - func testPerfomIntenseBlock() + func testPerformIntenseBlock() { - let wasIntensity: Int = DBCBridge.intensityLevel; + let wasIntensity: Int = DBCBridge.intensityLevel XCTAssertTrue(wasIntensity == 0) - var intsity0 = false; - var intsity10 = false; + var intensity0 = false + var intensity10 = false performIfDBCIntensity(0) { - intsity0 = true; + intensity0 = true } performIfDBCIntensity(10) { - intsity10 = true; + intensity10 = true } - XCTAssertTrue(intsity0) - XCTAssertFalse(intsity10) + XCTAssertTrue(intensity0) + XCTAssertFalse(intensity10) - DBCBridge.intensityLevel = 10; + DBCBridge.intensityLevel = 10 XCTAssertTrue(DBCBridge.intensityLevel == 10) - intsity0 = false; - intsity10 = false; + intensity0 = false + intensity10 = false performIfDBCIntensity(0) { - intsity0 = true; + intensity0 = true } performIfDBCIntensity(10) { - intsity10 = true; + intensity10 = true } - XCTAssertTrue(intsity0) - XCTAssertTrue(intsity10) + XCTAssertTrue(intensity0) + XCTAssertTrue(intensity10) DBCBridge.intensityLevel = wasIntensity XCTAssertTrue(DBCBridge.intensityLevel == 0) diff --git a/Example/Tests/swift/DBCTests.swift b/Example/Tests/swift/DBCTests.swift index 69df425..065179c 100644 --- a/Example/Tests/swift/DBCTests.swift +++ b/Example/Tests/swift/DBCTests.swift @@ -22,16 +22,34 @@ import DBCTesting import DBC_testing #endif +private final class RecordingDBCLogger: DBCLogger { + private(set) var entries: [(message: String, file: StaticString, line: UInt)] = [] + + func log(_ message: String, separator: String, terminator: String, file: StaticString, line: UInt) { + entries.append((message, file, line)) + } +} + class SwiftDBCTests: XCTestCase { override func setUp() { super.setUp() - dbcIntensityLevel = 0; + dbcIntensityLevel = 0 + dbcLogger = DBCDebugPrintLogger() } override func tearDown() { + dbcLogger = DBCDebugPrintLogger() super.tearDown() } + private func assertionsAreEnabled() -> Bool { + #if DEBUG + return true + #else + return false + #endif + } + func testDBCRequire() { require(true) expectRequire() { require(false) } @@ -42,24 +60,163 @@ class SwiftDBCTests: XCTestCase { } func testDBCCheck() { + guard assertionsAreEnabled() else { return } check(true) expectCheck() { check(false) } } func testDBCCheckFailure() { + guard assertionsAreEnabled() else { return } expectCheckFailure("Some message") { checkFailure("Some message") } } func testDBCEnsure() { + guard assertionsAreEnabled() else { return } ensure(true) expectEnsure() { ensure(false) } } func testDBCEnsureFailure() { + guard assertionsAreEnabled() else { return } expectEnsureFailure("Some message") { ensureFailure("Some message") } } + + func testInformUsesRegisteredLogger() { + let logger = RecordingDBCLogger() + dbcLogger = logger + + inform("First Message") + informIf(true, "Second Message") + informIf(false, "Third Message") + + XCTAssertEqual(logger.entries.count, 2) + XCTAssertEqual(logger.entries[0].message, "First Message") + XCTAssertEqual(logger.entries[1].message, "Second Message") + } + + func testAssertionFallbackLoggingRespectsReleaseIntensity() { + let logger = RecordingDBCLogger() + dbcLogger = logger + dbcIntensityLevel = 0 + + check(false, intensity: 1) + ensure(false, intensity: 1) + checkFailure("Hidden", intensity: 1) + ensureFailure("Hidden", intensity: 1) + + if assertionsAreEnabled() { + XCTAssertEqual(logger.entries.count, 4) + } else { + XCTAssertEqual(logger.entries.count, 0) + } + } + + func testSuppressedCheckAndEnsureRemainLazyInReleaseBuilds() { + dbcIntensityLevel = 0 + + var checkConditionEvaluations = 0 + var checkMessageEvaluations = 0 + var ensureConditionEvaluations = 0 + var ensureMessageEvaluations = 0 + + check( + { + checkConditionEvaluations += 1 + return false + }(), + { + checkMessageEvaluations += 1 + return "check" + }(), + intensity: 1 + ) + + ensure( + { + ensureConditionEvaluations += 1 + return false + }(), + { + ensureMessageEvaluations += 1 + return "ensure" + }(), + intensity: 1 + ) + + #if DEBUG + XCTAssertEqual(checkConditionEvaluations, 1) + XCTAssertEqual(checkMessageEvaluations, 1) + XCTAssertEqual(ensureConditionEvaluations, 1) + XCTAssertEqual(ensureMessageEvaluations, 1) + #else + XCTAssertEqual(checkConditionEvaluations, 0) + XCTAssertEqual(checkMessageEvaluations, 0) + XCTAssertEqual(ensureConditionEvaluations, 0) + XCTAssertEqual(ensureMessageEvaluations, 0) + #endif + } + + func testSuppressedCheckFailureAndEnsureFailureRemainLazyInReleaseBuilds() { + dbcIntensityLevel = 0 + + var checkFailureMessageEvaluations = 0 + var ensureFailureMessageEvaluations = 0 + + checkFailure( + { + checkFailureMessageEvaluations += 1 + return "check failure" + }(), + intensity: 1 + ) + + ensureFailure( + { + ensureFailureMessageEvaluations += 1 + return "ensure failure" + }(), + intensity: 1 + ) + + #if DEBUG + XCTAssertEqual(checkFailureMessageEvaluations, 1) + XCTAssertEqual(ensureFailureMessageEvaluations, 1) + #else + XCTAssertEqual(checkFailureMessageEvaluations, 0) + XCTAssertEqual(ensureFailureMessageEvaluations, 0) + #endif + } + + func testPassingCheckAndEnsureDoNotEvaluateMessages() { + guard assertionsAreEnabled() else { return } + + var checkMessageEvaluations = 0 + var ensureMessageEvaluations = 0 + + check( + true, + { + checkMessageEvaluations += 1 + return "check message" + }(), + intensity: 0 + ) + + ensure( + true, + { + ensureMessageEvaluations += 1 + return "ensure message" + }(), + intensity: 0 + ) + + XCTAssertEqual(checkMessageEvaluations, 0) + XCTAssertEqual(ensureMessageEvaluations, 0) + } func testDBCAll() { + guard assertionsAreEnabled() else { return } require(true) check(true) ensure(true) @@ -76,7 +233,7 @@ class SwiftDBCTests: XCTestCase { expectCheck() { check(1 == 2) } expectEnsure() { ensure(1 == 2) } - let testStr: String? = "Test"; + let testStr: String? = "Test" require(testStr != nil) check(testStr != nil) ensure(testStr != nil) @@ -85,7 +242,7 @@ class SwiftDBCTests: XCTestCase { expectCheck() { check(testStr == nil) } expectEnsure() { ensure(testStr == nil) } - let nilStr: String? = nil; + let nilStr: String? = nil expectRequire() { require(nilStr != nil) } expectCheck() { check(nilStr != nil) } expectEnsure() { ensure(nilStr != nil) } @@ -96,6 +253,7 @@ class SwiftDBCTests: XCTestCase { } func testDBCMessage() { + guard assertionsAreEnabled() else { return } require(true, "Test Message") check(true, "Test Message") ensure(true, "Test Message") @@ -112,7 +270,7 @@ class SwiftDBCTests: XCTestCase { expectCheck("Test Message") { check(1 == 2, "Test Message") } expectEnsure("Test Message") { ensure(1 == 2, "Test Message") } - let testStr: String? = "Test"; + let testStr: String? = "Test" require(testStr != nil, "Test Message") check(testStr != nil, "Test Message") ensure(testStr != nil, "Test Message") @@ -121,7 +279,7 @@ class SwiftDBCTests: XCTestCase { expectCheck("Test Message") { check(testStr == nil, "Test Message") } expectEnsure("Test Message") { ensure(testStr == nil, "Test Message") } - let nilStr: String? = nil; + let nilStr: String? = nil expectRequire("Test Message") { require(nilStr != nil, "Test Message") } expectCheck("Test Message") { check(nilStr != nil, "Test Message") } expectEnsure("Test Message") { ensure(nilStr != nil, "Test Message") } @@ -132,7 +290,8 @@ class SwiftDBCTests: XCTestCase { } func testDBCIntense() { - let wasIntensity = dbcIntensityLevel; + guard assertionsAreEnabled() else { return } + let wasIntensity = dbcIntensityLevel XCTAssertTrue(wasIntensity == 0) dbcIntensityLevel = 10 @@ -163,7 +322,7 @@ class SwiftDBCTests: XCTestCase { check(1 == 2, intensity: 15) ensure(1 == 2, intensity: 15) - let testStr: String? = "Test"; + let testStr: String? = "Test" require(testStr != nil, intensity: 5) check(testStr != nil, intensity: 5) ensure(testStr != nil, intensity: 5) @@ -176,7 +335,7 @@ class SwiftDBCTests: XCTestCase { check(testStr == nil, intensity: 15) ensure(testStr == nil, intensity: 15) - let nilStr: String? = nil; + let nilStr: String? = nil require(nilStr == nil, intensity: 5) check(nilStr == nil, intensity: 5) ensure(nilStr == nil, intensity: 5) @@ -194,7 +353,8 @@ class SwiftDBCTests: XCTestCase { } func testDBCIntenseMessage() { - let wasIntensity: Int = dbcIntensityLevel; + guard assertionsAreEnabled() else { return } + let wasIntensity: Int = dbcIntensityLevel XCTAssertTrue(wasIntensity == 0) dbcIntensityLevel = 10 @@ -225,7 +385,7 @@ class SwiftDBCTests: XCTestCase { check(1 == 2, "Test Message", intensity: 15) ensure(1 == 2, "Test Message", intensity: 15) - let testStr: String? = "Test"; + let testStr: String? = "Test" require(testStr != nil, "Test Message", intensity: 5) check(testStr != nil, "Test Message", intensity: 5) ensure(testStr != nil, "Test Message", intensity: 5) @@ -238,7 +398,7 @@ class SwiftDBCTests: XCTestCase { check(testStr == nil, "Test Message", intensity: 15) ensure(testStr == nil, "Test Message", intensity: 15) - let nilStr: String? = nil; + let nilStr: String? = nil require(nilStr == nil, "Test Message", intensity: 5) check(nilStr == nil, "Test Message", intensity: 5) ensure(nilStr == nil, "Test Message", intensity: 5) @@ -256,7 +416,7 @@ class SwiftDBCTests: XCTestCase { } func testDBCOff() { - let wasIntensity: Int = dbcIntensityLevel; + let wasIntensity: Int = dbcIntensityLevel XCTAssertTrue(wasIntensity == 0) // Setting `dbcIntensityLevel` to a value less then zero effectively turns assertions/messaging off. @@ -279,7 +439,7 @@ class SwiftDBCTests: XCTestCase { check(1 == 2) ensure(1 == 2) - let testStr: String? = "Test"; + let testStr: String? = "Test" require(testStr != nil) check(testStr != nil) ensure(testStr != nil) @@ -288,7 +448,7 @@ class SwiftDBCTests: XCTestCase { check(testStr == nil) ensure(testStr == nil) - let nilStr: String? = nil; + let nilStr: String? = nil require(nilStr != nil) check(nilStr != nil) ensure(nilStr != nil) @@ -303,7 +463,7 @@ class SwiftDBCTests: XCTestCase { func testDBCMessageOff() { - let wasIntensity: Int = dbcIntensityLevel; + let wasIntensity: Int = dbcIntensityLevel XCTAssertTrue(wasIntensity == 0) // Setting `dbcIntensityLevel` to a value less then zero effectively turns assertions/messaging off. @@ -326,7 +486,7 @@ class SwiftDBCTests: XCTestCase { check(1 == 2, "Test Message") ensure(1 == 2, "Test Message") - let testStr: String? = "Test"; + let testStr: String? = "Test" require(testStr != nil, "Test Message") check(testStr != nil, "Test Message") ensure(testStr != nil, "Test Message") @@ -335,7 +495,7 @@ class SwiftDBCTests: XCTestCase { check(testStr == nil, "Test Message") ensure(testStr == nil, "Test Message") - let nilStr: String? = nil; + let nilStr: String? = nil require(nilStr != nil, "Test Message") check(nilStr != nil, "Test Message") ensure(nilStr != nil, "Test Message") @@ -349,7 +509,7 @@ class SwiftDBCTests: XCTestCase { } func testDBCIntenseOff() { - let wasIntensity: Int = dbcIntensityLevel; + let wasIntensity: Int = dbcIntensityLevel XCTAssertTrue(wasIntensity == 0) // Setting `dbcIntensityLevel` to a value less then zero effectively turns assertions/messaging off. @@ -380,7 +540,7 @@ class SwiftDBCTests: XCTestCase { check(1 == 2, intensity: 15) ensure(1 == 2, intensity: 15) - let testStr: String? = "Test"; + let testStr: String? = "Test" require(testStr != nil, intensity: 5) check(testStr != nil, intensity: 5) ensure(testStr != nil, intensity: 5) @@ -393,7 +553,7 @@ class SwiftDBCTests: XCTestCase { check(testStr == nil, intensity: 15) ensure(testStr == nil, intensity: 15) - let nilStr: String? = nil; + let nilStr: String? = nil require(nilStr == nil, intensity: 5) check(nilStr == nil, intensity: 5) ensure(nilStr == nil, intensity: 5) @@ -411,7 +571,7 @@ class SwiftDBCTests: XCTestCase { } func testDBCIntenseMessageOff() { - let wasIntensity: Int = dbcIntensityLevel; + let wasIntensity: Int = dbcIntensityLevel XCTAssertTrue(wasIntensity == 0) // Setting `dbcIntensityLevel` to a value less then zero effectively turns assertions/messaging off. @@ -442,7 +602,7 @@ class SwiftDBCTests: XCTestCase { check(1 == 2, "Test Message", intensity: 15) ensure(1 == 2, "Test Message", intensity: 15) - let testStr: String? = "Test"; + let testStr: String? = "Test" require(testStr != nil, "Test Message", intensity: 5) check(testStr != nil, "Test Message", intensity: 5) ensure(testStr != nil, "Test Message", intensity: 5) @@ -455,7 +615,7 @@ class SwiftDBCTests: XCTestCase { check(testStr == nil, "Test Message", intensity: 15) ensure(testStr == nil, "Test Message", intensity: 15) - let nilStr: String? = nil; + let nilStr: String? = nil require(nilStr == nil, "Test Message", intensity: 5) check(nilStr == nil, "Test Message", intensity: 5) ensure(nilStr == nil, "Test Message", intensity: 5) @@ -472,45 +632,45 @@ class SwiftDBCTests: XCTestCase { XCTAssertTrue(dbcIntensityLevel == 0) } - func testPerfomIntenseBlock() + func testPerformIntenseBlock() { - let wasIntensity: Int = dbcIntensityLevel; + let wasIntensity: Int = dbcIntensityLevel XCTAssertTrue(wasIntensity == 0) - var intsity0 = false; - var intsity10 = false; + var intensity0 = false + var intensity10 = false performIfDBCIntensity(0) { - intsity0 = true; + intensity0 = true } performIfDBCIntensity(10) { - intsity10 = true; + intensity10 = true } - XCTAssertTrue(intsity0) - XCTAssertFalse(intsity10) + XCTAssertTrue(intensity0) + XCTAssertFalse(intensity10) - dbcIntensityLevel = 10; + dbcIntensityLevel = 10 XCTAssertTrue(dbcIntensityLevel == 10) - intsity0 = false; - intsity10 = false; + intensity0 = false + intensity10 = false performIfDBCIntensity(0) { - intsity0 = true; + intensity0 = true } performIfDBCIntensity(10) { - intsity10 = true; + intensity10 = true } - XCTAssertTrue(intsity0) - XCTAssertTrue(intsity10) + XCTAssertTrue(intensity0) + XCTAssertTrue(intensity10) dbcIntensityLevel = wasIntensity XCTAssertTrue(dbcIntensityLevel == 0) diff --git a/Example/Tests/swift/RequiredOptionalTests.swift b/Example/Tests/swift/RequiredOptionalTests.swift index 7228719..51ae480 100644 --- a/Example/Tests/swift/RequiredOptionalTests.swift +++ b/Example/Tests/swift/RequiredOptionalTests.swift @@ -17,18 +17,51 @@ import DBCTesting import DBC_testing #endif +private final class RecordingDBCLogger: DBCLogger { + private(set) var entries: [String] = [] + + func log(_ message: String, separator: String, terminator: String, file: StaticString, line: UInt) { + entries.append(message) + } +} + class RequiredOptionalTests: XCTestCase { override func setUp() { super.setUp() - dbcIntensityLevel = 0; + dbcIntensityLevel = 0 + dbcLogger = DBCDebugPrintLogger() } override func tearDown() { + dbcLogger = DBCDebugPrintLogger() super.tearDown() } + + private func assertDBCOptionalError( + _ error: Error, + kind: DBCOptionalError.Kind, + message: String, + domain: String, + code: Int, + file: StaticString = #fileID, + line: UInt = #line + ) { + guard let error = error as? DBCOptionalError else { + XCTFail("Unexpected error type: \(error)", file: file, line: line) + return + } + + XCTAssertEqual(error.kind, kind, file: file, line: line) + XCTAssertEqual(error.message, message, file: file, line: line) + + let nsError = error.nsError + XCTAssertEqual(nsError.domain, domain, file: file, line: line) + XCTAssertEqual(nsError.code, code, file: file, line: line) + XCTAssertEqual(nsError.localizedDescription, message, file: file, line: line) + } func testDBCAll() { - let testStr: String? = "Test"; + let testStr: String? = "Test" _ = testStr.require() _ = testStr.check() @@ -47,7 +80,7 @@ class RequiredOptionalTests: XCTestCase { func testDBCMessage() { - let testStr: String? = "Test"; + let testStr: String? = "Test" _ = testStr.require("Test Message") _ = testStr.check("Test Message") @@ -56,4 +89,146 @@ class RequiredOptionalTests: XCTestCase { //expectRequire("Test Message") { _ = nilStr.require("Test Message") } //expectCheck("Test Message") { _ = nilStr.check("Test Message") } } + + func testRequiredThrowsTypedDBCOptionalError() { + let nilString: String? = nil + + do { + _ = try nilString.required("Typed Error") + XCTFail("Expected required() to throw") + } catch { + assertDBCOptionalError( + error, + kind: .require, + message: "Failed REQUIRE : Typed Error", + domain: "DBC ERROR REQUIRE", + code: 99990 + ) + } + } + + func testRequiredCastReturnsCastValue() throws { + let strings: [String]? = ["Test"] + let nsStrings: [NSString] = try strings.requiredCast() + + XCTAssertEqual(nsStrings, ["Test"]) + } + + func testRequiredCastThrowsTypedDBCOptionalErrorForBadCast() { + let ints: [Int]? = [1, 2, 3] + + do { + let _: [String] = try ints.requiredCast("Bad Cast") + XCTFail("Expected requiredCast() to throw") + } catch { + assertDBCOptionalError( + error, + kind: .require, + message: "Failed REQUIRE : Bad Cast", + domain: "DBC ERROR REQUIRE", + code: 99990 + ) + } + } + + func testCheckedThrowsTypedDBCOptionalError() { + let nilString: String? = nil + + do { + _ = try nilString.checked("Checked Error") + XCTFail("Expected checked() to throw") + } catch { + assertDBCOptionalError( + error, + kind: .check, + message: "Failed CHECK : Checked Error", + domain: "DBC ERROR CHECK", + code: 99991 + ) + } + } + + func testCheckedCastThrowsTypedDBCOptionalErrorForBadCast() { + let ints: [Int]? = [1, 2, 3] + + do { + let _: [String] = try ints.checkedCast("Checked Cast Error") + XCTFail("Expected checkedCast() to throw") + } catch { + assertDBCOptionalError( + error, + kind: .check, + message: "Failed CHECK : Checked Cast Error", + domain: "DBC ERROR CHECK", + code: 99991 + ) + } + } + + func testRequiredThrowsDefaultNilMessageWithoutExtraSeparator() { + let nilString: String? = nil + + do { + _ = try nilString.required() + XCTFail("Expected required() to throw") + } catch { + assertDBCOptionalError( + error, + kind: .require, + message: "Failed REQUIRE : optional is nil.", + domain: "DBC ERROR REQUIRE", + code: 99990 + ) + } + } + + func testRequiredCastThrowsDefaultCastMessageWithoutExtraSeparator() { + let ints: [Int]? = [1, 2, 3] + + do { + let _: [String] = try ints.requiredCast() + XCTFail("Expected requiredCast() to throw") + } catch { + guard let error = error as? DBCOptionalError else { + XCTFail("Unexpected error type: \(error)") + return + } + + XCTAssertTrue(error.message.hasPrefix("Failed REQUIRE : Failed to cast value")) + XCTAssertFalse(error.message.contains(": :")) + } + } + + func testRequiredLogsTypedErrorWithoutDuplicatingCustomMessage() { + let logger = RecordingDBCLogger() + let nilString: String? = nil + dbcLogger = logger + + do { + _ = try nilString.required("Typed Error") + XCTFail("Expected required() to throw") + } catch { + XCTAssertEqual(logger.entries, ["Failed REQUIRE : Typed Error"]) + } + } + + func testDBCOptionalNSErrorUsesObjectiveCBridgeFriendlyMetadataTypes() { + let nilString: String? = nil + let nsError: NSError + + do { + _ = try nilString.required("Bridge Metadata") + XCTFail("Expected required() to throw") + return + } catch let error as DBCOptionalError { + nsError = error.nsError + } catch { + XCTFail("Unexpected error type: \(error)") + return + } + + XCTAssertTrue(nsError.userInfo["file"] is String) + XCTAssertTrue(nsError.userInfo["function"] is String) + XCTAssertTrue(nsError.userInfo["line"] is Int) + } } diff --git a/README.md b/README.md index 50b7383..fafe496 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,63 @@ # DBC -[![CI Status](http://img.shields.io/travis/Jim Boyd/DBC.svg?style=flat)](https://travis-ci.org/Jim Boyd/DBC) -[![Version](https://img.shields.io/cocoapods/v/DBC.svg?style=flat)](http://cocoapods.org/pods/DBC) -[![License](https://img.shields.io/cocoapods/l/DBC.svg?style=flat)](http://cocoapods.org/pods/DBC) -[![Platform](https://img.shields.io/cocoapods/p/DBC.svg?style=flat)](http://cocoapods.org/pods/DBC) +DBC is a small Design by Contract library for Swift and Objective-C. It provides `require`, `check`, `ensure`, and `inform` helpers, plus bridged targets and XCTest helpers for verifying assertion behavior. -## Example +## Package Layout -To run the example project, clone the repo, and run `pod install` from the Example directory first. +- `DBC`: core Swift assertions, optional helpers, intensity handling, and logging +- `DBC-objc`: Objective-C implementation and headers +- `DBC-bridged`: mixed Swift/Objective-C bridge target +- `DBC-testing`: XCTest helpers for stubbing and asserting DBC failure paths -## Requirements +SwiftPM tests live in `Example/Tests/swift` and `Example/Tests/objc`. ## Installation -DBC is available through [CocoaPods](http://cocoapods.org). To install -it, simply add the following line to your Podfile: +### Swift Package Manager + +```swift +.package(name: "DBC", url: "git@github.com:alignops/DBC-Apple.git", from: "1.4.0") +``` + +Add one of these products to your target: + +```swift +.product(name: "DBC", package: "DBC") +.product(name: "DBC-objc", package: "DBC") +.product(name: "DBC-bridged", package: "DBC") +.product(name: "DBC-testing", package: "DBC") +``` + +### CocoaPods ```ruby pod "DBC" ``` -## Author +## Highlights -Jim Boyd, jim@busybusy.com +- `require` uses preconditions for failures that must stop execution. +- `check` and `ensure` use Swift assertions in debug builds when active for the current `dbcIntensityLevel`, and log through `inform` in release builds. When debug assertions are suppressed by the intensity gate, they emit fallback `inform` logs instead of trapping. +- `inform` and `informIf` are active in all build configurations when `intensity <= dbcIntensityLevel`. +- Swift logging is swappable through `dbcLogger`; the default logger delegates to `Swift.debugPrint`. +- The default Swift assertion closures delegate directly to Swift assertion primitives, so Apple-platform failures are Swift runtime traps/preconditions unless you override `Assertions.*`. +- Throwing optional helpers such as `required()`, `requiredCast()`, `checked()`, and `checkedCast()` now throw `DBCOptionalError`. + +## Development + +```sh +swift build +swift test +swift test -c release +``` + +To run the example Xcode project, install pods first: + +```sh +cd Example +pod install +``` ## License -Copyright (c) 2016 Busy, LLC. See the LICENSE file for more info. +Copyright (c) 2016 Busy, LLC. See [LICENSE](LICENSE).