From efe10dab663dbb296f62a8258baa4c036376811d Mon Sep 17 00:00:00 2001 From: Aleksey Berezka Date: Mon, 18 Dec 2023 22:58:56 +0500 Subject: [PATCH] Preparations for open source (#18) * Minor changes * Added comments * Fixed unit tests workflow * Fixed icon * Added LICENSE * Updated CODEOWNERS * Added README.md * Minor change * Filter is configurable * Actualized tests --- .github/workflows/unittest.yml | 2 +- CODEOWNERS | 2 +- LICENSE | 201 ++++++++++++++ Package.swift | 19 +- README.md | 135 +++++++++- .../{Shell.swift => DBShell.swift} | 6 +- ...ormatter.swift => DBXCTextFormatter.swift} | 35 ++- .../Formatters/FormatterProtocol.swift | 15 -- .../Models/DBXCReportModel+Convenience.swift | 44 +++ ...eportModel.swift => DBXCReportModel.swift} | 81 +++--- .../Models/DTO/DTO+Helpers.swift | 6 +- .../Models/ReportModel+Convenience.swift | 25 -- Sources/DBXCResultParser/XCResultSeeker.swift | 23 -- .../DBXCReportModel+TestHelpers.swift | 106 ++++++++ .../Measurement+TestHelpers.swift | 25 ++ .../{ShellTests.swift => DBShellTests.swift} | 4 +- .../DBXCTextFormatterTests.swift | 127 +++++++++ Tests/DBXCResultParserTests/SeekerTests.swift | 26 -- .../TextFormatterTests.swift | 250 ------------------ 19 files changed, 733 insertions(+), 399 deletions(-) create mode 100644 LICENSE rename Sources/DBXCResultParser/{Shell.swift => DBShell.swift} (84%) rename Sources/DBXCResultParser/Formatters/{TextFormatter.swift => DBXCTextFormatter.swift} (72%) delete mode 100644 Sources/DBXCResultParser/Formatters/FormatterProtocol.swift create mode 100644 Sources/DBXCResultParser/Models/DBXCReportModel+Convenience.swift rename Sources/DBXCResultParser/Models/{ReportModel.swift => DBXCReportModel.swift} (76%) delete mode 100644 Sources/DBXCResultParser/Models/ReportModel+Convenience.swift delete mode 100644 Sources/DBXCResultParser/XCResultSeeker.swift create mode 100644 Sources/DBXCResultParserTestHelpers/DBXCReportModel+TestHelpers.swift create mode 100644 Sources/DBXCResultParserTestHelpers/Measurement+TestHelpers.swift rename Tests/DBXCResultParserTests/{ShellTests.swift => DBShellTests.swift} (77%) create mode 100644 Tests/DBXCResultParserTests/DBXCTextFormatterTests.swift delete mode 100644 Tests/DBXCResultParserTests/SeekerTests.swift delete mode 100644 Tests/DBXCResultParserTests/TextFormatterTests.swift diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index 1dd678d..6c8f8a5 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -13,7 +13,7 @@ concurrency: cancel-in-progress: true env: - SCHEME: "DBXCResultParser" + SCHEME: "DBXCResultParser-Package" DESTINATION: "platform=OS X" jobs: diff --git a/CODEOWNERS b/CODEOWNERS index f134edb..7a9bc99 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1 +1 @@ -* @dodobrands/mobile-ios-devs \ No newline at end of file +* @alldmeat diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b6dcec1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2023 - present © Dodo Engineering + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Package.swift b/Package.swift index cd76931..9b6e9d4 100644 --- a/Package.swift +++ b/Package.swift @@ -5,6 +5,7 @@ import PackageDescription let packageName = "DBXCResultParser" let packageTestsName = packageName + "Tests" +let packageTestHelpersName = packageName + "TestHelpers" let package = Package( name: packageName, @@ -15,7 +16,12 @@ let package = Package( // Products define the executables and libraries a package produces, and make them visible to other packages. .library( name: packageName, - targets: [packageName]), + targets: [packageName] + ), + .library( + name: packageTestHelpersName, + targets: [packageTestHelpersName] + ), ], dependencies: [ @@ -25,13 +31,20 @@ let package = Package( // Targets can depend on other targets in this package, and on products in packages this package depends on. .target( name: packageName, + dependencies: [] + ), + .target( + name: packageTestHelpersName, dependencies: [ - + .init(stringLiteral: packageName) ] ), .testTarget( name: packageTestsName, - dependencies: [.init(stringLiteral: packageName)], + dependencies: [ + .init(stringLiteral: packageName), + .init(stringLiteral: packageTestHelpersName) + ], resources: [ .process("Resources/AllTests.xcresult"), .process("Resources/AllTests_coverage.xcresult"), diff --git a/README.md b/README.md index 7248d11..8c6c278 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,134 @@ -# DBXCResultParser +# DBXCReportModel -A description of this package. +The `DBXCReportModel` package provides a Swift module for parsing `.xcresult` files generated by Xcode to produce a structured report of build and test results. It allows developers to programmatically access detailed information about test cases, code coverage, and warnings from their CI/CD pipelines or automated scripts. + +## Features + +- Parses `.xcresult` files to create a typed model of the test results and code coverage. +- Filters out coverage data related to test helpers and test cases. +- Provides a detailed breakdown of modules, files, and repeatable tests. +- Calculates total and average test durations, as well as combined test statuses. +- Supports identifying slow tests based on average duration. +- Includes utility functions for filtering tests based on status. + +## Installation + +To use `DBXCReportModel` in your Swift package, add it to the dependencies for your `Package.swift` file: + +```swift +let package = Package( + name: "YourPackageName", + dependencies: [ + .package(url: "https://github.com/dodobrands/DBXCResultParser", from: "1.0.0") + ], + targets: [ + .target( + name: "YourTargetName", + dependencies: ["DBXCResultParser"] + ) + ] +) +``` + +## Usage + +To parse an `.xcresult` file and access the report data, initialize a `DBXCReportModel` with the path to the `.xcresult` file: + +```swift +import DBXCReportModel + +let xcresultPath = URL(fileURLWithPath: "/path/to/your.xcresult") +do { + let reportModel = try DBXCReportModel(xcresultPath: xcresultPath) + + // Access different parts of the report: + let modules = reportModel.modules + let warningCount = reportModel.warningCount + let totalCoverage = reportModel.totalCoverage + + // Iterate over modules, files, and tests: + for module in modules { + print("Module: \(module.name)") + for file in module.files { + print(" File: \(file.name)") + for repeatableTest in file.repeatableTests { + print(" Repeatable Test: \(repeatableTest.name)") + for test in repeatableTest.tests { + print(" Test: \(test.status.icon) - Duration: \(test.duration)") + } + } + } + } + +} catch { + print("An error occurred while parsing the .xcresult file: \(error)") +} +``` + +## Formatting Test Reports with DBXCTextFormatter + +The `DBXCTextFormatter` class provides a way to format the data from a `DBXCReportModel` into a human-readable string. It supports two output formats: a detailed list of test results and a summary count of test results. + +### Usage + +To format your test report data, create an instance of `DBXCTextFormatter` with the desired output format and optionally a locale for number and measurement formatting: + +```swift +import DBXCReportModel + +// Assuming you have already created a `DBXCReportModel` instance as `reportModel` +let reportModel: DBXCReportModel = ... + +// Create a text formatter for detailed list output +let listFormatter = DBXCTextFormatter(format: .list) + +// Create a text formatter for summary count output with a specific locale +let countFormatter = DBXCTextFormatter(format: .count, locale: Locale(identifier: "en_US")) + +// Format the report data into a string +let detailedListOutput = listFormatter.format(reportModel) +let summaryCountOutput = countFormatter.format(reportModel) + +// Print the formatted outputs +print("Detailed List Output:\n\(detailedListOutput)") +print("Summary Count Output:\n\(summaryCountOutput)") +``` + +The `format` method can also take an array of `DBXCReportModel.Module.File.RepeatableTest.Test.Status` to filter which test results are included in the output. By default, it includes all test statuses. + +### Output Formats + +- **List Format**: Outputs a detailed list of test results, including the name of each file and the status of each test. +- **Count Format**: Outputs a summary count of test results, including the total number of tests and their combined duration. + +### Example Outputs + +#### List Format +``` +FileA.swift +✅ TestA1 +❌ TestA2 + +FileB.swift +✅ TestB1 +⚠️ TestB2 +``` + +#### Count Format +``` +12 tests (1m 23s) +``` + +### Customizing Number and Measurement Formatting + +The `DBXCTextFormatter` allows you to specify a locale when initializing it. This locale is used to format numbers and measurements according to the provided locale's conventions. + +```swift +let formatter = DBXCTextFormatter(format: .count, locale: Locale(identifier: "fr_FR")) +let output = formatter.format(reportModel) +print(output) // Will output numbers and durations formatted in French +``` + +## License + +This code is released under the Apache License. See [LICENSE](LICENSE) for more information. \ No newline at end of file diff --git a/Sources/DBXCResultParser/Shell.swift b/Sources/DBXCResultParser/DBShell.swift similarity index 84% rename from Sources/DBXCResultParser/Shell.swift rename to Sources/DBXCResultParser/DBShell.swift index e425d5f..1f8473b 100644 --- a/Sources/DBXCResultParser/Shell.swift +++ b/Sources/DBXCResultParser/DBShell.swift @@ -1,5 +1,5 @@ // -// Shell.swift +// DBShell.swift // // // Created by Алексей Берёзка on 28.12.2021. @@ -7,9 +7,9 @@ import Foundation -class Shell { +public class DBShell { @discardableResult - static func execute(_ command: String) throws -> String { + public static func execute(_ command: String) throws -> String { let task = Process() let pipe = Pipe() diff --git a/Sources/DBXCResultParser/Formatters/TextFormatter.swift b/Sources/DBXCResultParser/Formatters/DBXCTextFormatter.swift similarity index 72% rename from Sources/DBXCResultParser/Formatters/TextFormatter.swift rename to Sources/DBXCResultParser/Formatters/DBXCTextFormatter.swift index 4ce5666..5fc8583 100644 --- a/Sources/DBXCResultParser/Formatters/TextFormatter.swift +++ b/Sources/DBXCResultParser/Formatters/DBXCTextFormatter.swift @@ -1,5 +1,5 @@ // -// TextFormatter.swift +// DBXCTextFormatter.swift // // // Created by Алексей Берёзка on 31.12.2021. @@ -7,17 +7,26 @@ import Foundation -extension TextFormatter { +extension DBXCTextFormatter { + /// Output format options public enum Format { - case list - case count + case list // Outputs detailed list of test results + case count // // Outputs a summary count of test results } } -class TextFormatter: FormatterProtocol { +public class DBXCTextFormatter { + /// The format style to use for output public let format: Format + + /// /// The locale to use for formatting numbers and measurements public let locale: Locale? + /// Initializes a new text formatter with the specified format and locale. + /// + /// - Parameters: + /// - format: The output format to use. + /// - locale: The locale for number and measurement formatting. Defaults to `nil`. public init( format: Format, locale: Locale? = nil @@ -26,9 +35,15 @@ class TextFormatter: FormatterProtocol { self.locale = locale } + /// Formats the test report data into a string based on the specified format. + /// + /// - Parameters: + /// - report: The `DBXCReportModel` containing the test report data. + /// - testResults: The test result statuses to include in the output. Defaults to all test statuses. + /// - Returns: A formatted string representation of the report data. public func format( - _ report: ReportModel, - testResults: [ReportModel.Module.File.RepeatableTest.Test.Status] = .allCases + _ report: DBXCReportModel, + testResults: [DBXCReportModel.Module.File.RepeatableTest.Test.Status] = .allCases ) -> String { let files = report.modules .flatMap { Array($0.files) } @@ -62,8 +77,8 @@ class TextFormatter: FormatterProtocol { } } -extension ReportModel.Module.File { - func report(testResults: [ReportModel.Module.File.RepeatableTest.Test.Status], +extension DBXCReportModel.Module.File { + func report(testResults: [DBXCReportModel.Module.File.RepeatableTest.Test.Status], formatter: MeasurementFormatter) -> String? { let tests = repeatableTests.filtered(testResults: testResults).sorted { $0.name < $1.name } @@ -83,7 +98,7 @@ extension ReportModel.Module.File { } } -fileprivate extension ReportModel.Module.File.RepeatableTest { +fileprivate extension DBXCReportModel.Module.File.RepeatableTest { func report(formatter: MeasurementFormatter) -> String { [ combinedStatus.icon, diff --git a/Sources/DBXCResultParser/Formatters/FormatterProtocol.swift b/Sources/DBXCResultParser/Formatters/FormatterProtocol.swift deleted file mode 100644 index 7f1d643..0000000 --- a/Sources/DBXCResultParser/Formatters/FormatterProtocol.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// FormatterProtocol.swift -// -// -// Created by Aleksey Berezka on 15.12.2023. -// - -import Foundation - -public protocol FormatterProtocol { - func format( - _ report: ReportModel, - testResults: [ReportModel.Module.File.RepeatableTest.Test.Status] - ) -> String -} diff --git a/Sources/DBXCResultParser/Models/DBXCReportModel+Convenience.swift b/Sources/DBXCResultParser/Models/DBXCReportModel+Convenience.swift new file mode 100644 index 0000000..38236da --- /dev/null +++ b/Sources/DBXCResultParser/Models/DBXCReportModel+Convenience.swift @@ -0,0 +1,44 @@ +// +// DBXCReportModel+Convenience.swift +// +// +// Created by Aleksey Berezka on 15.12.2023. +// + +import Foundation + +extension DBXCReportModel { + /// Initializes a new instance of the `DBXCReportModel` using the provided `xcresultPath`. + /// The initialization process involves parsing the `.xcresult` file to extract various reports. + /// Coverage data for targets specified in `excludingCoverageNames` will be excluded from the report. + /// + /// - Parameters: + /// - xcresultPath: The file URL of the `.xcresult` file to parse. + /// - excludingCoverageNames: An array of strings representing the names of the targets to be excluded + /// from the code coverage report. Defaults to an empty array, meaning no + /// targets will be excluded. + /// - Throws: An error if the `.xcresult` file cannot be parsed or if required data is missing. + public init( + xcresultPath: URL, + excludingCoverageNames: [String] = [] + ) throws { + // Parse the overview report from the xcresult file, which contains general test execution information. + let overviewReport = try OverviewReportDTO(from: xcresultPath) + + // Parse the detailed report using the reference ID obtained from the overview report. + // This report provides a more granular look at the test results, including individual test cases. + let detailedReport = try DetailedReportDTO(from: xcresultPath, + refId: overviewReport.testsRefId) + + // Attempt to parse the code coverage data from the xcresult file, excluding specified targets. + let coverageDTOs = try? Array(from: xcresultPath) + .filter { !excludingCoverageNames.contains($0.name) } + + // Initialize the DBXCReportModel with the parsed report data, including the filtered coverage data. + self = try DBXCReportModel( + overviewReportDTO: overviewReport, + detailedReportDTO: detailedReport, + coverageDTOs: coverageDTOs ?? [] + ) + } +} diff --git a/Sources/DBXCResultParser/Models/ReportModel.swift b/Sources/DBXCResultParser/Models/DBXCReportModel.swift similarity index 76% rename from Sources/DBXCResultParser/Models/ReportModel.swift rename to Sources/DBXCResultParser/Models/DBXCReportModel.swift index 45206c8..3e4fe7b 100644 --- a/Sources/DBXCResultParser/Models/ReportModel.swift +++ b/Sources/DBXCResultParser/Models/DBXCReportModel.swift @@ -1,5 +1,5 @@ // -// ReportModel.swift +// DBXCReportModel.swift // // // Created by Алексей Берёзка on 31.12.2021. @@ -7,14 +7,12 @@ import Foundation -public typealias Duration = Measurement - -public struct ReportModel { +public struct DBXCReportModel { public let modules: Set - public private(set) var warningCount: Int? + public let warningCount: Int? } -extension ReportModel { +extension DBXCReportModel { public struct Module: Hashable { public let name: String public internal(set) var files: Set @@ -30,7 +28,7 @@ extension ReportModel { } } -extension ReportModel.Module { +extension DBXCReportModel.Module { public struct Coverage: Equatable { public let name: String public let coveredLines: Int @@ -56,7 +54,7 @@ extension ReportModel.Module { } } -extension ReportModel.Module { +extension DBXCReportModel.Module { public struct File: Hashable { public let name: String public internal(set) var repeatableTests: Set @@ -70,7 +68,7 @@ extension ReportModel.Module { } } } -extension ReportModel.Module.File { +extension DBXCReportModel.Module.File { public struct RepeatableTest: Hashable { public let name: String public internal(set) var tests: [Test] @@ -85,13 +83,13 @@ extension ReportModel.Module.File { } } -extension ReportModel.Module.File.RepeatableTest { +extension DBXCReportModel.Module.File.RepeatableTest { public struct Test { public let status: Status - public let duration: Duration + public let duration: Measurement } - var combinedStatus: Test.Status { + public var combinedStatus: Test.Status { let statuses = tests.map { $0.status } if statuses.elementsAreEqual { return statuses.first ?? .success @@ -100,7 +98,7 @@ extension ReportModel.Module.File.RepeatableTest { } } - var averageDuration: Duration { + public var averageDuration: Measurement { assert(tests.map { $0.duration.unit }.elementsAreEqual) let unit = tests.first?.duration.unit ?? Test.defaultDurationUnit @@ -111,21 +109,21 @@ extension ReportModel.Module.File.RepeatableTest { ) } - var totalDuration: Duration { + public var totalDuration: Measurement { assert(tests.map { $0.duration.unit }.elementsAreEqual) let value = tests.map { $0.duration.value }.sum() let unit = tests.first?.duration.unit ?? Test.defaultDurationUnit return .init(value: value, unit: unit) } - func isSlow(_ duration: Duration) -> Bool { + public func isSlow(_ duration: Measurement) -> Bool { let averageDuration = averageDuration let duration = duration.converted(to: averageDuration.unit) return averageDuration >= duration } } -extension ReportModel.Module.File.RepeatableTest.Test { +extension DBXCReportModel.Module.File.RepeatableTest.Test { public enum Status: Equatable, CaseIterable { case success case failure @@ -136,22 +134,24 @@ extension ReportModel.Module.File.RepeatableTest.Test { } } -extension Array where Element == ReportModel.Module.File.RepeatableTest.Test.Status { - static let allCases = ReportModel.Module.File.RepeatableTest.Test.Status.allCases +public extension Array where Element == DBXCReportModel.Module.File.RepeatableTest.Test.Status { + static let allCases = DBXCReportModel.Module.File.RepeatableTest.Test.Status.allCases } -extension ReportModel { +extension DBXCReportModel { init(overviewReportDTO: OverviewReportDTO, detailedReportDTO: DetailedReportDTO, coverageDTOs: [CoverageDTO]) throws { if let warningCount = overviewReportDTO.metrics.warningCount?._value { self.warningCount = Int(warningCount) + } else { + self.warningCount = nil } - let filteredCoverages = coverageDTOs + let coverages = coverageDTOs .map { Module.Coverage(from: $0)} - .filter { !$0.name.contains("TestHelpers") && !$0.name.contains("Tests") } + var modules = Set() func findCoverage(for moduleName: String, coverageModels: [Module.Coverage]) -> Module.Coverage? { @@ -164,7 +164,7 @@ extension ReportModel { var module = modules[modulename] ?? .init(name: modulename, files: [], coverage: findCoverage(for: modulename, - coverageModels: filteredCoverages)) + coverageModels: coverages)) try value2.tests._values.forEach { value3 in try value3.subtests?._values.forEach { value4 in try value4.subtests?._values.forEach { value5 in @@ -175,7 +175,7 @@ extension ReportModel { let testname = value6.name._value var repeatableTest = file.repeatableTests[testname] ?? .init(name: testname, tests: []) - let test = try ReportModel.Module.File.RepeatableTest.Test(value6) + let test = try DBXCReportModel.Module.File.RepeatableTest.Test(value6) repeatableTest.tests.append(test) file.repeatableTests.update(with: repeatableTest) } @@ -203,7 +203,7 @@ extension ReportModel { } } -extension ReportModel { +extension DBXCReportModel { enum Error: Swift.Error { case missingFilename(testName: String) } @@ -215,8 +215,11 @@ fileprivate extension String { } } -extension Set where Element == ReportModel.Module.File.RepeatableTest { - public func filtered(testResults: [ReportModel.Module.File.RepeatableTest.Test.Status]) -> Set { +extension Set where Element == DBXCReportModel.Module.File.RepeatableTest { + /// Filters tests based on statis + /// - Parameter testResults: statuses to leave in result + /// - Returns: set of elements matching any of the specified statuses + public func filtered(testResults: [DBXCReportModel.Module.File.RepeatableTest.Test.Status]) -> Set { guard !testResults.isEmpty else { return self } @@ -242,32 +245,40 @@ extension Set where Element == ReportModel.Module.File.RepeatableTest { return Set(results) } + // Property that filters the collection to include only elements whose status is `.success`. var succeeded: Self { filter { $0.combinedStatus == .success } } + // Property that filters the collection to include only elements whose status is `.failure`. var failed: Self { filter { $0.combinedStatus == .failure } } + // Property that filters the collection to include only elements whose status is `.expectedFailure`. var expectedFailed: Self { filter { $0.combinedStatus == .expectedFailure } } + // Property that filters the collection to include only elements whose status is `.skipped`. var skipped: Self { filter { $0.combinedStatus == .skipped } } + // Property that filters the collection to include only elements whose status is `.mixed`. + // This might indicate a combination of success and failure statuses or an intermediate state. var mixed: Self { filter { $0.combinedStatus == .mixed } } + // Property that filters the collection to include only elements whose status is `.unknown`. + // This status might be used when the status of an element has not been determined or is not applicable. var unknown: Self { filter { $0.combinedStatus == .unknown } } } -extension ReportModel.Module.File.RepeatableTest.Test { +extension DBXCReportModel.Module.File.RepeatableTest.Test { init(_ test: DetailedReportDTO.Summaries.Value.TestableSummaries.Value.Tests.Value.Subtests.Value.Subtests.Value.Subtests.Value) throws { switch test.testStatus._value { case "Success": @@ -302,34 +313,34 @@ extension Array where Element: Equatable { } } -extension Set where Element == ReportModel.Module.File { +extension Set where Element == DBXCReportModel.Module.File { subscript(_ name: String) -> Element? { first { $0.name == name } } } -extension Set where Element == ReportModel.Module.File.RepeatableTest { +extension Set where Element == DBXCReportModel.Module.File.RepeatableTest { subscript(_ name: String) -> Element? { first { $0.name == name } } } -extension Set where Element == ReportModel.Module { +extension Set where Element == DBXCReportModel.Module { subscript(_ name: String) -> Element? { first { $0.name == name } } } -extension Array where Element == ReportModel.Module.File.RepeatableTest { - var totalDuration: Duration { +extension Array where Element == DBXCReportModel.Module.File.RepeatableTest { + var totalDuration: Measurement { assert(map { $0.totalDuration.unit }.elementsAreEqual) let value = map { $0.totalDuration.value }.sum() - let unit = first?.totalDuration.unit ?? ReportModel.Module.File.RepeatableTest.Test.defaultDurationUnit + let unit = first?.totalDuration.unit ?? DBXCReportModel.Module.File.RepeatableTest.Test.defaultDurationUnit return .init(value: value, unit: unit) } } -extension ReportModel.Module.File.RepeatableTest.Test.Status { +extension DBXCReportModel.Module.File.RepeatableTest.Test.Status { var icon: String { switch self { case .success: @@ -337,7 +348,7 @@ extension ReportModel.Module.File.RepeatableTest.Test.Status { case .failure: return "❌" case .skipped: - return "⏭" + return "⏭️" case .mixed: return "⚠️" case .expectedFailure: diff --git a/Sources/DBXCResultParser/Models/DTO/DTO+Helpers.swift b/Sources/DBXCResultParser/Models/DTO/DTO+Helpers.swift index 32a9462..c1d5ac1 100644 --- a/Sources/DBXCResultParser/Models/DTO/DTO+Helpers.swift +++ b/Sources/DBXCResultParser/Models/DTO/DTO+Helpers.swift @@ -10,7 +10,7 @@ import Foundation extension OverviewReportDTO { init(from xcresultPath: URL) throws { let tempFilePath = try Constants.tempFilePath - try Shell.execute("xcrun xcresulttool get --path \(xcresultPath.relativePath) --format json > \(tempFilePath.relativePath)") + try DBShell.execute("xcrun xcresulttool get --path \(xcresultPath.relativePath) --format json > \(tempFilePath.relativePath)") let data = try Data(contentsOf: tempFilePath) try FileManager.default.removeItem(atPath: tempFilePath.relativePath) self = try JSONDecoder().decode(OverviewReportDTO.self, from: data) @@ -21,7 +21,7 @@ extension DetailedReportDTO { init(from xcresultPath: URL, refId: String? = nil) throws { let refId = try (refId ?? OverviewReportDTO(from: xcresultPath).testsRefId) let tempFilePath = try Constants.tempFilePath - try Shell.execute("xcrun xcresulttool get --path \(xcresultPath.relativePath) --format json --id \(refId) > \(tempFilePath.relativePath)") + try DBShell.execute("xcrun xcresulttool get --path \(xcresultPath.relativePath) --format json --id \(refId) > \(tempFilePath.relativePath)") let data = try Data(contentsOf: tempFilePath) try FileManager.default.removeItem(atPath: tempFilePath.relativePath) self = try JSONDecoder().decode(DetailedReportDTO.self, from: data) @@ -31,7 +31,7 @@ extension DetailedReportDTO { extension Array where Element == CoverageDTO { init(from xcresultPath: URL) throws { let tempFilePath = try Constants.tempFilePath - try Shell.execute("xcrun xccov view --report --only-targets --json \(xcresultPath.relativePath) > \(tempFilePath.relativePath)") + try DBShell.execute("xcrun xccov view --report --only-targets --json \(xcresultPath.relativePath) > \(tempFilePath.relativePath)") let data = try Data(contentsOf: tempFilePath) try FileManager.default.removeItem(atPath: tempFilePath.relativePath) self = try JSONDecoder().decode(Array.self, from: data) diff --git a/Sources/DBXCResultParser/Models/ReportModel+Convenience.swift b/Sources/DBXCResultParser/Models/ReportModel+Convenience.swift deleted file mode 100644 index ec59b32..0000000 --- a/Sources/DBXCResultParser/Models/ReportModel+Convenience.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// ReportModel+Convenience.swift -// -// -// Created by Aleksey Berezka on 15.12.2023. -// - -import Foundation - -extension ReportModel { - public init(xcresultPath: URL) throws { - let overviewReport = try OverviewReportDTO(from: xcresultPath) - let detailedReport = try DetailedReportDTO(from: xcresultPath, - refId: overviewReport.testsRefId) - - let coverageDTOs = try? Array(from: xcresultPath) - .filter { !$0.name.contains("TestHelpers") && !$0.name.contains("Tests") } - - self = try ReportModel( - overviewReportDTO: overviewReport, - detailedReportDTO: detailedReport, - coverageDTOs: coverageDTOs ?? [] - ) - } -} diff --git a/Sources/DBXCResultParser/XCResultSeeker.swift b/Sources/DBXCResultParser/XCResultSeeker.swift deleted file mode 100644 index 55f11ba..0000000 --- a/Sources/DBXCResultParser/XCResultSeeker.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// XCResultSeeker.swift -// -// -// Created by Алексей Берёзка on 30.12.2021. -// - -import Foundation - -public class XCResultSeeker { - public init() { } - - public func seek(in path: URL) throws -> [URL] { - guard FileManager.default.fileExists(atPath: path.path) else { - return [] - } - - return try FileManager - .default - .contentsOfDirectory(at: path, includingPropertiesForKeys: nil) - .filter { $0.pathExtension == "xcresult" } - } -} diff --git a/Sources/DBXCResultParserTestHelpers/DBXCReportModel+TestHelpers.swift b/Sources/DBXCResultParserTestHelpers/DBXCReportModel+TestHelpers.swift new file mode 100644 index 0000000..c1a2d5c --- /dev/null +++ b/Sources/DBXCResultParserTestHelpers/DBXCReportModel+TestHelpers.swift @@ -0,0 +1,106 @@ +// +// DBXCReportModel+TestHelpers.swift +// +// +// Created by Aleksey Berezka on 18.12.2023. +// + +import Foundation +@testable import DBXCResultParser + +extension DBXCReportModel { + public static func testMake( + modules: Set = [], + warningCount: Int? = nil + ) -> Self { + .init( + modules: modules, + warningCount: warningCount + ) + } +} + +extension DBXCReportModel.Module { + public static func testMake( + name: String = "", + files: Set = [], + coverage: Coverage = .testMake() + ) -> Self { + .init(name: name, files: files, coverage: coverage) + } +} + +extension DBXCReportModel.Module.Coverage { + public static func testMake( + name: String = "", + coveredLines: Int = 0, + totalLines: Int = 0, + coverage: Double = 0.0 + ) -> Self { + Self(name: name, + coveredLines: coveredLines, + totalLines: totalLines, + coverage: coverage) + } +} + +extension DBXCReportModel.Module.File { + public static func testMake( + name: String = "", + repeatableTests: Set = [] + ) -> Self { + .init(name: name, repeatableTests: repeatableTests) + } +} + +extension DBXCReportModel.Module.File.RepeatableTest { + public static func testMake( + name: String = "", + tests: [Test] = [] + ) -> Self { + .init(name: name, tests: tests) + } + + public static func failed( + named name: String, + times: Int = 1 + ) -> Self { + let tests = Array( + repeating: DBXCReportModel.Module.File.RepeatableTest.Test.testMake(status: .failure), + count: times + ) + return .testMake(name: name, tests: tests) + } + + public static func succeeded( + named name: String + ) -> Self { + .testMake(name: name, tests: [.testMake(status: .success)]) + } + + public static func skipped( + named name: String + ) -> Self { + .testMake(name: name, tests: [.testMake(status: .skipped)]) + } + + public static func mixedFailedSucceeded( + named name: String, + failedTimes: Int = 1 + ) -> Self { + let failedTests = Array( + repeating: DBXCReportModel.Module.File.RepeatableTest.Test.testMake(status: .failure), + count: failedTimes + ) + return .testMake(name: name, tests: failedTests + [.testMake(status: .success)]) + } +} + +extension DBXCReportModel.Module.File.RepeatableTest.Test { + public static func testMake( + status: Status = .success, + duration: Measurement = .testMake() + ) -> Self { + .init(status: status, duration: duration) + } +} diff --git a/Sources/DBXCResultParserTestHelpers/Measurement+TestHelpers.swift b/Sources/DBXCResultParserTestHelpers/Measurement+TestHelpers.swift new file mode 100644 index 0000000..eac49ff --- /dev/null +++ b/Sources/DBXCResultParserTestHelpers/Measurement+TestHelpers.swift @@ -0,0 +1,25 @@ +// +// Measurement+TestHelpers.swift +// +// +// Created by Aleksey Berezka on 18.12.2023. +// + +import Foundation + +extension Measurement where UnitType: UnitDuration { + public static func testMake( + unit: UnitDuration = .milliseconds, + value: Double = 0 + ) -> Measurement { + .init(value: value, unit: unit) + } + + public static func * (left: Self, right: Int) -> Self { + .init(value: left.value * Double(right), unit: left.unit) + } + + public static func / (left: Self, right: Int) -> Self { + .init(value: left.value / Double(right), unit: left.unit) + } +} diff --git a/Tests/DBXCResultParserTests/ShellTests.swift b/Tests/DBXCResultParserTests/DBShellTests.swift similarity index 77% rename from Tests/DBXCResultParserTests/ShellTests.swift rename to Tests/DBXCResultParserTests/DBShellTests.swift index 138d8f9..3c22dbd 100644 --- a/Tests/DBXCResultParserTests/ShellTests.swift +++ b/Tests/DBXCResultParserTests/DBShellTests.swift @@ -9,7 +9,7 @@ import Foundation import XCTest @testable import DBXCResultParser -class ShellTests: XCTestCase { +class DBShellTests: XCTestCase { override func setUpWithError() throws { try super.setUpWithError() } @@ -19,6 +19,6 @@ class ShellTests: XCTestCase { } func test() throws { - try XCTAssertEqual(Shell.execute("which swift"), "/usr/bin/swift") + try XCTAssertEqual(DBShell.execute("which swift"), "/usr/bin/swift") } } diff --git a/Tests/DBXCResultParserTests/DBXCTextFormatterTests.swift b/Tests/DBXCResultParserTests/DBXCTextFormatterTests.swift new file mode 100644 index 0000000..2643340 --- /dev/null +++ b/Tests/DBXCResultParserTests/DBXCTextFormatterTests.swift @@ -0,0 +1,127 @@ +import XCTest +@testable import DBXCResultParser +import DBXCResultParserTestHelpers + +final class DBXCTextFormatterTests: XCTestCase { + var locale: Locale! + override func setUpWithError() throws { + try super.setUpWithError() + locale = Locale(identifier: "en-US") + } + + override func tearDownWithError() throws { + locale = nil + try super.tearDownWithError() + } + + func test_testResult_any_list() { + let formatter = DBXCTextFormatter(format: .list, locale: locale) + let result = formatter.format(.genericReport) + + XCTAssertEqual(result, + """ +AuthSpec +✅ login +❌ logout +⚠️ openSettings + +CaptchaSpec +❌ Another Handle Request +❌ Handle Request + +NetworkSpec +✅ MakeRequest + +NotificationsSetupServiceTests +⏭️ enabledNotifications +""") + } + + func test_testResult_success_list() { + let formatter = DBXCTextFormatter(format: .list, locale: locale) + let result = formatter.format(.genericReport, testResults: [.success]) + + XCTAssertEqual(result, + """ +AuthSpec +✅ login + +NetworkSpec +✅ MakeRequest +""") + } + + func test_testResult_any_count() { + let formatter = DBXCTextFormatter(format: .count, locale: locale) + let result = formatter.format(.genericReport) + + XCTAssertEqual(result, "7 (0 sec)") + } + + func test_testResult_failure_count() { + let formatter = DBXCTextFormatter(format: .count, locale: locale) + let result = formatter.format(.genericReport, testResults: [.failure]) + + XCTAssertEqual(result, "3 (0 sec)") + } +} + +extension DBXCReportModel { + static var genericReport: DBXCReportModel { + // Module with all possible tests + let profileModule = DBXCReportModel.Module.testMake( + name: "Profile", + files: [ + .testMake( + name: "AuthSpec", + repeatableTests: [ + .failed(named: "logout"), + .succeeded(named: "login"), + .mixedFailedSucceeded(named: "openSettings") + ] + ) + ] + ) + + // Module with repeated tests + let networkModule = DBXCReportModel.Module.testMake( + name: "Network", + files: [ + .testMake( + name: "NetworkSpec", + repeatableTests: [ + .succeeded(named: "MakeRequest") + ] + ), + .testMake( + name: "CaptchaSpec", + repeatableTests: [ + .failed(named: "Handle Request", times: 2), + .failed(named: "Another Handle Request", times: 2) + ] + ) + ] + ) + + // Module with skipped tests + let notificationsModule = DBXCReportModel.Module.testMake( + name: "Notifications", + files: [ + .testMake( + name: "NotificationsSetupServiceTests", + repeatableTests: [ + .skipped(named: "enabledNotifications") + ] + ) + ] + ) + + return .testMake( + modules: [ + profileModule, + networkModule, + notificationsModule + ] + ) + } +} diff --git a/Tests/DBXCResultParserTests/SeekerTests.swift b/Tests/DBXCResultParserTests/SeekerTests.swift deleted file mode 100644 index c40bebb..0000000 --- a/Tests/DBXCResultParserTests/SeekerTests.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// ReportSeekerTests.swift -// -// -// Created by Алексей Берёзка on 30.12.2021. -// - -import Foundation -import XCTest -@testable import DBXCResultParser - -class SeekerTests: XCTestCase { - override func setUpWithError() throws { - try super.setUpWithError() - } - - override func tearDownWithError() throws { - try super.tearDownWithError() - } - - func test_xcresultIsFound() throws { - let resourcesPath = try Constants.resourcesPath - let result = try XCTUnwrap(XCResultSeeker().seek(in: resourcesPath).first) - XCTAssertEqual(result.pathExtension, "xcresult") - } -} diff --git a/Tests/DBXCResultParserTests/TextFormatterTests.swift b/Tests/DBXCResultParserTests/TextFormatterTests.swift deleted file mode 100644 index ad54761..0000000 --- a/Tests/DBXCResultParserTests/TextFormatterTests.swift +++ /dev/null @@ -1,250 +0,0 @@ -import XCTest -@testable import DBXCResultParser - -final class FormatterTests: XCTestCase { - var locale: Locale! - override func setUpWithError() throws { - try super.setUpWithError() - locale = Locale(identifier: "en-US") - } - - override func tearDownWithError() throws { - locale = nil - try super.tearDownWithError() - } - - func test_testResult_any_list() { - let formatter = TextFormatter(format: .list, locale: locale) - let result = formatter.format(generalReport) - - XCTAssertEqual(result, - """ -AuthSpec -✅ login -❌ logout -⚠️ openSettings - -CaptchaSpec -❌ Another Handle Request -❌ Handle Request - -NetworkSpec -✅ MakeRequest - -NotificationsSetupServiceTests -⏭ enabledNotifications -""") - } - - func test_testResult_success_list() { - let formatter = TextFormatter(format: .list, locale: locale) - let result = formatter.format(generalReport, testResults: [.success]) - - XCTAssertEqual(result, - """ -AuthSpec -✅ login - -NetworkSpec -✅ MakeRequest -""") - } - - func test_testResult_any_count() { - let formatter = TextFormatter(format: .count, locale: locale) - let result = formatter.format(generalReport) - - XCTAssertEqual(result, "7 (0 sec)") - } - - func test_testResult_failure_count() { - let formatter = TextFormatter(format: .count, locale: locale) - let result = formatter.format(generalReport, testResults: [.failure]) - - XCTAssertEqual(result, "3 (0 sec)") - } -} - -extension FormatterTests { - var generalReport: ReportModel { - // Module with all possible tests - let profileModule = ReportModel.Module.testMake( - name: "Profile", - files: [ - .testMake( - name: "AuthSpec", - repeatableTests: [ - .failed(named: "logout"), - .succeeded(named: "login"), - .mixedFailedSucceeded(named: "openSettings") - ] - ) - ] - ) - - // Module with repeated tests - let networkModule = ReportModel.Module.testMake( - name: "Network", - files: [ - .testMake( - name: "NetworkSpec", - repeatableTests: [ - .succeeded(named: "MakeRequest") - ] - ), - .testMake( - name: "CaptchaSpec", - repeatableTests: [ - .failed(named: "Handle Request", times: 2), - .failed(named: "Another Handle Request", times: 2) - ] - ) - ] - ) - - // Module with skipped tests - let notificationsModule = ReportModel.Module.testMake( - name: "Notifications", - files: [ - .testMake( - name: "NotificationsSetupServiceTests", - repeatableTests: [ - .skipped(named: "enabledNotifications") - ] - ) - ] - ) - - return .testMake( - modules: [ - profileModule, - networkModule, - notificationsModule - ] - ) - } - - func slowReport(duration: Duration) -> ReportModel { - .testMake( - modules: [ - .testMake( - name: "FSModule", - files: [ - .testMake( - name: "WriterSpec", - repeatableTests: [ - .testMake( - name: "Check folder exists", - tests: [ - .testMake(status: .failure, duration: duration / 2) - ] - ), - .testMake( - name: "Check file exists", - tests: [ - .testMake(status: .success, duration: duration) - ] - ), - .testMake( - name: "Read from file", - tests: [ - .testMake(status: .success, duration: duration * 2) - ] - ), - .testMake( - name: "Write to file", - tests: [ - .testMake(status: .failure, duration: duration / 2), - .testMake(status: .success, duration: duration * 2), - ] - ) - ] - ) - ] - ) - ] - ) - } -} - -extension ReportModel { - static func testMake(modules: Set = []) -> Self { - .init(modules: modules) - } -} - -extension ReportModel.Module { - static func testMake(name: String = "", files: Set = [], coverage: Coverage = .testMake()) -> Self { - .init(name: name, files: files, coverage: coverage) - } -} - -extension ReportModel.Module.Coverage { - static func testMake(name: String = "", - coveredLines: Int = 0, - totalLines: Int = 0, - coverage: Double = 0.0) -> Self { - Self(name: name, - coveredLines: coveredLines, - totalLines: totalLines, - coverage: coverage) - } -} - -extension ReportModel.Module.File { - static func testMake(name: String = "", repeatableTests: Set = []) -> Self { - .init(name: name, repeatableTests: repeatableTests) - } -} - -extension ReportModel.Module.File.RepeatableTest { - static func testMake(name: String = "", tests: [Test] = []) -> Self { - .init(name: name, tests: tests) - } - - static func failed(named name: String, times: Int = 1) -> Self { - let tests = Array( - repeating: ReportModel.Module.File.RepeatableTest.Test.testMake(status: .failure), - count: times - ) - return .testMake(name: name, tests: tests) - } - - static func succeeded(named name: String) -> Self { - .testMake(name: name, tests: [.testMake(status: .success)]) - } - - static func skipped(named name: String) -> Self { - .testMake(name: name, tests: [.testMake(status: .skipped)]) - } - - static func mixedFailedSucceeded(named name: String, failedTimes: Int = 1) -> Self { - let failedTests = Array( - repeating: ReportModel.Module.File.RepeatableTest.Test.testMake(status: .failure), - count: failedTimes - ) - return .testMake(name: name, tests: failedTests + [.testMake(status: .success)]) - } -} - -extension ReportModel.Module.File.RepeatableTest.Test { - static func testMake(status: Status = .success, - duration: Duration = .testMake()) -> Self { - .init(status: status, duration: duration) - } -} - -extension Measurement where UnitType: UnitDuration { - static func testMake(unit: UnitDuration = .milliseconds, - value: Double = 0) -> Duration { - .init(value: value, unit: unit) - } - - static func * (left: Self, right: Int) -> Self { - .init(value: left.value * Double(right), unit: left.unit) - } - - static func / (left: Self, right: Int) -> Self { - .init(value: left.value / Double(right), unit: left.unit) - } -}