| // This source file is part of the Swift.org open source project |
| // |
| // Copyright (c) 2016 Apple Inc. and the Swift project authors |
| // Licensed under Apache License v2.0 with Runtime Library Exception |
| // |
| // See http://swift.org/LICENSE.txt for license information |
| // See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors |
| // |
| // |
| // XCTestCase+Performance.swift |
| // Methods on XCTestCase for testing the performance of code blocks. |
| // |
| |
| public struct XCTPerformanceMetric : RawRepresentable, Equatable, Hashable { |
| public private(set) var rawValue: String |
| |
| public init(_ rawValue: String) { |
| self.rawValue = rawValue |
| } |
| |
| public init(rawValue: String) { |
| self.rawValue = rawValue |
| } |
| |
| public var hashValue: Int { |
| return rawValue.hashValue |
| } |
| |
| public static func ==(_ lhs: XCTPerformanceMetric, _ rhs: XCTPerformanceMetric) -> Bool { |
| return lhs.rawValue == rhs.rawValue |
| } |
| } |
| |
| public extension XCTPerformanceMetric { |
| /// Records wall clock time in seconds between `startMeasuring`/`stopMeasuring`. |
| public static let wallClockTime = XCTPerformanceMetric(rawValue: WallClockTimeMetric.name) |
| } |
| |
| /// The following methods are called from within a test method to carry out |
| /// performance testing on blocks of code. |
| public extension XCTestCase { |
| |
| /// The names of the performance metrics to measure when invoking `measure(block:)`. |
| /// Returns `XCTPerformanceMetric_WallClockTime` by default. Subclasses can |
| /// override this to change the behavior of `measure(block:)` |
| class var defaultPerformanceMetrics: [XCTPerformanceMetric] { |
| return [.wallClockTime] |
| } |
| |
| /// Call from a test method to measure resources (`defaultPerformanceMetrics`) |
| /// used by the block in the current process. |
| /// |
| /// func testPerformanceOfMyFunction() { |
| /// measure { |
| /// // Do that thing you want to measure. |
| /// MyFunction(); |
| /// } |
| /// } |
| /// |
| /// - Parameter block: A block whose performance to measure. |
| /// - Bug: The `block` param should have no external label, but there seems |
| /// to be a swiftc bug that causes issues when such a parameter comes |
| /// after a defaulted arg. See https://bugs.swift.org/browse/SR-1483 This |
| /// API incompatibility with Apple XCTest can be worked around in practice |
| /// by using trailing closure syntax when calling this method. |
| /// - ToDo: The `block` param should be marked @noescape once Apple XCTest |
| /// has been updated to do so rdar://26224596 |
| /// - Note: Whereas Apple XCTest determines the file and line number of |
| /// measurements by using symbolication, this implementation opts to take |
| /// `file` and `line` as parameters instead. As a result, the interface to |
| /// these methods are not exactly identical between these environments. To |
| /// ensure compatibility of tests between swift-corelibs-xctest and Apple |
| /// XCTest, it is not recommended to pass explicit values for `file` and `line`. |
| func measure(file: StaticString = #file, line: Int = #line, block: () -> Void) { |
| measureMetrics(type(of: self).defaultPerformanceMetrics, |
| automaticallyStartMeasuring: true, |
| file: file, |
| line: line, |
| for: block) |
| } |
| |
| /// Call from a test method to measure resources (XCTPerformanceMetrics) used |
| /// by the block in the current process. Each metric will be measured across |
| /// calls to the block. The number of times the block will be called is undefined |
| /// and may change in the future. For one example of why, as long as the requested |
| /// performance metrics do not interfere with each other the API will measure |
| /// all metrics across the same calls to the block. If the performance metrics |
| /// may interfere the API will measure them separately. |
| /// |
| /// func testMyFunction2_WallClockTime() { |
| /// measureMetrics(type(of: self).defaultPerformanceMetrics, automaticallyStartMeasuring: false) { |
| /// |
| /// // Do setup work that needs to be done for every iteration but |
| /// // you don't want to measure before the call to `startMeasuring()` |
| /// SetupSomething(); |
| /// self.startMeasuring() |
| /// |
| /// // Do that thing you want to measure. |
| /// MyFunction() |
| /// self.stopMeasuring() |
| /// |
| /// // Do teardown work that needs to be done for every iteration |
| /// // but you don't want to measure after the call to `stopMeasuring()` |
| /// TeardownSomething() |
| /// } |
| /// } |
| /// |
| /// Caveats: |
| /// * If `true` was passed for `automaticallyStartMeasuring` and `startMeasuring()` |
| /// is called anyway, the test will fail. |
| /// * If `false` was passed for `automaticallyStartMeasuring` then `startMeasuring()` |
| /// must be called once and only once before the end of the block or the test will fail. |
| /// * If `stopMeasuring()` is called multiple times during the block the test will fail. |
| /// |
| /// - Parameter metrics: An array of Strings (XCTPerformanceMetrics) to measure. |
| /// Providing an unrecognized string is a test failure. |
| /// - Parameter automaticallyStartMeasuring: If `false`, `XCTestCase` will |
| /// not take any measurements until -startMeasuring is called. |
| /// - Parameter block: A block whose performance to measure. |
| /// - ToDo: The `block` param should be marked @noescape once Apple XCTest |
| /// has been updated to do so. rdar://26224596 |
| /// - Note: Whereas Apple XCTest determines the file and line number of |
| /// measurements by using symbolication, this implementation opts to take |
| /// `file` and `line` as parameters instead. As a result, the interface to |
| /// these methods are not exactly identical between these environments. To |
| /// ensure compatibility of tests between swift-corelibs-xctest and Apple |
| /// XCTest, it is not recommended to pass explicit values for `file` and `line`. |
| func measureMetrics(_ metrics: [XCTPerformanceMetric], automaticallyStartMeasuring: Bool, file: StaticString = #file, line: Int = #line, for block: () -> Void) { |
| guard _performanceMeter == nil else { |
| return recordAPIViolation(description: "Can only record one set of metrics per test method.", file: file, line: line) |
| } |
| |
| PerformanceMeter.measureMetrics(metrics.map({ $0.rawValue }), delegate: self, file: file, line: line) { meter in |
| self._performanceMeter = meter |
| if automaticallyStartMeasuring { |
| meter.startMeasuring(file: file, line: line) |
| } |
| block() |
| } |
| } |
| |
| /// Call this from within a measure block to set the beginning of the critical |
| /// section. Measurement of metrics will start at this point. |
| /// - Note: Whereas Apple XCTest determines the file and line number of |
| /// measurements by using symbolication, this implementation opts to take |
| /// `file` and `line` as parameters instead. As a result, the interface to |
| /// these methods are not exactly identical between these environments. To |
| /// ensure compatibility of tests between swift-corelibs-xctest and Apple |
| /// XCTest, it is not recommended to pass explicit values for `file` and `line`. |
| func startMeasuring(file: StaticString = #file, line: Int = #line) { |
| guard let performanceMeter = _performanceMeter, !performanceMeter.didFinishMeasuring else { |
| return recordAPIViolation(description: "Cannot start measuring. startMeasuring() is only supported from a block passed to measureMetrics(...).", file: file, line: line) |
| } |
| performanceMeter.startMeasuring(file: file, line: line) |
| } |
| |
| /// Call this from within a measure block to set the ending of the critical |
| /// section. Measurement of metrics will stop at this point. |
| /// - Note: Whereas Apple XCTest determines the file and line number of |
| /// measurements by using symbolication, this implementation opts to take |
| /// `file` and `line` as parameters instead. As a result, the interface to |
| /// these methods are not exactly identical between these environments. To |
| /// ensure compatibility of tests between swift-corelibs-xctest and Apple |
| /// XCTest, it is not recommended to pass explicit values for `file` and `line`. |
| func stopMeasuring(file: StaticString = #file, line: Int = #line) { |
| guard let performanceMeter = _performanceMeter, !performanceMeter.didFinishMeasuring else { |
| return recordAPIViolation(description: "Cannot stop measuring. stopMeasuring() is only supported from a block passed to measureMetrics(...).", file: file, line: line) |
| } |
| performanceMeter.stopMeasuring(file: file, line: line) |
| } |
| } |
| |
| extension XCTestCase: PerformanceMeterDelegate { |
| internal func recordAPIViolation(description: String, file: StaticString, line: Int) { |
| recordFailure(withDescription: "API violation - \(description)", |
| inFile: String(describing: file), |
| atLine: line, |
| expected: false) |
| } |
| |
| internal func recordMeasurements(results: String, file: StaticString, line: Int) { |
| XCTestObservationCenter.shared.testCase(self, didMeasurePerformanceResults: results, file: file, line: line) |
| } |
| |
| internal func recordFailure(description: String, file: StaticString, line: Int) { |
| recordFailure(withDescription: "failed: " + description, inFile: String(describing: file), atLine: line, expected: true) |
| } |
| } |