1
1
/*
2
2
This source file is part of the Swift.org open source project
3
3
4
- Copyright 2015 - 2016 Apple Inc. and the Swift project authors
4
+ Copyright 2015 - 2022 Apple Inc. and the Swift project authors
5
5
Licensed under Apache License v2.0 with Runtime Library Exception
6
6
7
7
See http://swift.org/LICENSE.txt for license information
@@ -11,6 +11,7 @@ See http://swift.org/CONTRIBUTORS.txt for Swift project authors
11
11
import ArgumentParser
12
12
import Basics
13
13
import Build
14
+ import Dispatch
14
15
import class Foundation. ProcessInfo
15
16
import PackageGraph
16
17
import SPMBuildCore
@@ -209,7 +210,7 @@ public struct SwiftTestTool: SwiftCommand {
209
210
switch options. mode {
210
211
case . listTests:
211
212
let testProducts = try buildTestsIfNeeded ( swiftTool: swiftTool)
212
- let testSuites = try getTestSuites ( in: testProducts, swiftTool: swiftTool)
213
+ let testSuites = try Self . getTestSuites ( in: testProducts, swiftTool: swiftTool, swiftOptions : swiftOptions )
213
214
let tests = try testSuites
214
215
. filteredTests ( specifier: options. testCaseSpecifier)
215
216
. skippedTests ( specifier: options. testCaseSkip)
@@ -245,7 +246,7 @@ public struct SwiftTestTool: SwiftCommand {
245
246
#endif
246
247
let graph = try swiftTool. loadPackageGraph ( )
247
248
let testProducts = try buildTestsIfNeeded ( swiftTool: swiftTool)
248
- let testSuites = try getTestSuites ( in: testProducts, swiftTool: swiftTool)
249
+ let testSuites = try Self . getTestSuites ( in: testProducts, swiftTool: swiftTool, swiftOptions : swiftOptions )
249
250
let allTestSuites = testSuites. values. flatMap { $0 }
250
251
let generator = LinuxMainGenerator ( graph: graph, testSuites: allTestSuites)
251
252
try generator. generate ( )
@@ -278,7 +279,7 @@ public struct SwiftTestTool: SwiftCommand {
278
279
}
279
280
280
281
// Find the tests we need to run.
281
- let testSuites = try getTestSuites ( in: testProducts, swiftTool: swiftTool)
282
+ let testSuites = try Self . getTestSuites ( in: testProducts, swiftTool: swiftTool, swiftOptions : swiftOptions )
282
283
let tests = try testSuites
283
284
. filteredTests ( specifier: options. testCaseSpecifier)
284
285
. skippedTests ( specifier: options. testCaseSkip)
@@ -317,7 +318,7 @@ public struct SwiftTestTool: SwiftCommand {
317
318
case . runParallel:
318
319
let toolchain = try swiftTool. getToolchain ( )
319
320
let testProducts = try buildTestsIfNeeded ( swiftTool: swiftTool)
320
- let testSuites = try getTestSuites ( in: testProducts, swiftTool: swiftTool)
321
+ let testSuites = try Self . getTestSuites ( in: testProducts, swiftTool: swiftTool, swiftOptions : swiftOptions )
321
322
let tests = try testSuites
322
323
. filteredTests ( specifier: options. testCaseSpecifier)
323
324
. skippedTests ( specifier: options. testCaseSkip)
@@ -465,7 +466,7 @@ public struct SwiftTestTool: SwiftCommand {
465
466
/// Note: It is a fatalError if we are not able to locate the tool.
466
467
///
467
468
/// - Returns: Path to XCTestHelper tool.
468
- private func xctestHelperPath( swiftTool: SwiftTool ) throws -> AbsolutePath {
469
+ private static func xctestHelperPath( swiftTool: SwiftTool ) throws -> AbsolutePath {
469
470
let xctestHelperBin = " swiftpm-xctest-helper "
470
471
let binDirectory = AbsolutePath ( CommandLine . arguments. first!,
471
472
relativeTo: swiftTool. originalWorkingDirectory) . parentDirectory
@@ -483,9 +484,9 @@ public struct SwiftTestTool: SwiftCommand {
483
484
throw InternalError ( " XCTestHelper binary not found. " )
484
485
}
485
486
486
- fileprivate func getTestSuites( in testProducts: [ BuiltTestProduct ] , swiftTool: SwiftTool ) throws -> [ AbsolutePath : [ TestSuite ] ] {
487
+ fileprivate static func getTestSuites( in testProducts: [ BuiltTestProduct ] , swiftTool: SwiftTool , swiftOptions : SwiftToolOptions ) throws -> [ AbsolutePath : [ TestSuite ] ] {
487
488
let testSuitesByProduct = try testProducts
488
- . map { try ( $0. bundlePath, self . getTestSuites ( fromTestAt: $0. bundlePath, swiftTool: swiftTool) ) }
489
+ . map { try ( $0. bundlePath, Self . getTestSuites ( fromTestAt: $0. bundlePath, swiftTool: swiftTool, swiftOptions : swiftOptions ) ) }
489
490
return Dictionary ( uniqueKeysWithValues: testSuitesByProduct)
490
491
}
491
492
@@ -499,7 +500,7 @@ public struct SwiftTestTool: SwiftCommand {
499
500
/// - Throws: TestError, SystemError, TSCUtility.Error
500
501
///
501
502
/// - Returns: Array of TestSuite
502
- fileprivate func getTestSuites( fromTestAt path: AbsolutePath , swiftTool: SwiftTool ) throws -> [ TestSuite ] {
503
+ fileprivate static func getTestSuites( fromTestAt path: AbsolutePath , swiftTool: SwiftTool , swiftOptions : SwiftToolOptions ) throws -> [ TestSuite ] {
503
504
// Run the correct tool.
504
505
#if os(macOS)
505
506
let data : String = try withTemporaryFile { tempFile in
@@ -1139,3 +1140,141 @@ private extension SwiftTool {
1139
1140
return parameters
1140
1141
}
1141
1142
}
1143
+
1144
+ extension SwiftTestTool {
1145
+
1146
+ public static func doTest( subset: PluginInvocationTestSubset , parameters: PluginInvocationTestParameters , swiftTool: SwiftTool ) throws -> PluginInvocationTestResult {
1147
+ // Build the tests. Ideally we should only build those that match the subset, but we don't have a way to know which ones they are until we've built them and can examine the binaries.
1148
+ let toolchain = try swiftTool. getToolchain ( )
1149
+ var buildParameters = try swiftTool. buildParameters ( )
1150
+ buildParameters. enableTestability = true
1151
+ buildParameters. enableCodeCoverage = parameters. enableCodeCoverage
1152
+ let buildSystem = try swiftTool. createBuildSystem ( buildParameters: buildParameters)
1153
+ try buildSystem. build ( subset: . allIncludingTests)
1154
+
1155
+ // Clean out the code coverage directory that may contain stale `profraw` files from a previous run of the code coverage tool.
1156
+ if parameters. enableCodeCoverage {
1157
+ try localFileSystem. removeFileTree ( buildParameters. codeCovPath)
1158
+ }
1159
+
1160
+ // Construct the environment we'll pass down to the tests.
1161
+ var environmentOptions = swiftTool. options
1162
+ environmentOptions. shouldEnableCodeCoverage = parameters. enableCodeCoverage
1163
+ let testEnvironment = try constructTestEnvironment (
1164
+ toolchain: toolchain,
1165
+ options: environmentOptions,
1166
+ buildParameters: buildParameters)
1167
+
1168
+ // Iterate over the tests and run those that match the filter.
1169
+ var testTargetResults : [ PluginInvocationTestResult . TestTarget ] = [ ]
1170
+ var numFailedTests = 0
1171
+ for testProduct in buildSystem. builtTestProducts {
1172
+ // Get the test suites in the bundle. Each is just a container for test cases.
1173
+ let testSuites = try Self . getTestSuites ( fromTestAt: testProduct. bundlePath, swiftTool: swiftTool, swiftOptions: swiftTool. options)
1174
+ for testSuite in testSuites {
1175
+ // Each test suite is just a container for test cases (confusingly called "tests", though they are test cases).
1176
+ for testCase in testSuite. tests {
1177
+ // Each test case corresponds to a combination of target and a XCTestCase, and is a collection of tests that can actually be run.
1178
+ var testResults : [ PluginInvocationTestResult . TestTarget . TestCase . Test ] = [ ]
1179
+ for testName in testCase. tests {
1180
+ // Check if we should filter out this test.
1181
+ let testSpecifier = testCase. name + " / " + testName
1182
+ if case . filtered( let regexes) = subset {
1183
+ guard regexes. contains ( where: { testSpecifier. range ( of: $0, options: . regularExpression) != nil } ) else {
1184
+ continue
1185
+ }
1186
+ }
1187
+
1188
+ // Configure a test runner.
1189
+ let testRunner = TestRunner (
1190
+ bundlePaths: [ testProduct. bundlePath] ,
1191
+ xctestArg: testSpecifier,
1192
+ processSet: swiftTool. processSet,
1193
+ toolchain: toolchain,
1194
+ testEnv: testEnvironment,
1195
+ outputStream: swiftTool. outputStream,
1196
+ observabilityScope: swiftTool. observabilityScope)
1197
+
1198
+ // Run the test — for now we run the sequentially so we can capture accurate timing results.
1199
+ let startTime = DispatchTime . now ( )
1200
+ let ( success, _) = testRunner. test ( writeToOutputStream: false )
1201
+ let duration = startTime. distance ( to: . now( ) ) . asSeconds ?? 0.0
1202
+ numFailedTests += success ? 0 : 1
1203
+ testResults. append ( . init( name: testName, result: success ? . succeeded : . failed, duration: duration) )
1204
+ }
1205
+
1206
+ // Don't add any results if we didn't run any tests.
1207
+ if testResults. isEmpty { continue }
1208
+
1209
+ // Otherwise we either create a new create a new target result or add to the previous one, depending on whether the target name is the same.
1210
+ let testTargetName = testCase. name. prefix ( while: { $0 != " . " } )
1211
+ if let lastTestTargetName = testTargetResults. last? . name, testTargetName == lastTestTargetName {
1212
+ // Same as last one, just extend its list of cases. We know we have a last one at this point.
1213
+ testTargetResults [ testTargetResults. count- 1 ] . testCases. append ( . init( name: testCase. name, tests: testResults) )
1214
+ }
1215
+ else {
1216
+ // Not the same, so start a new target result.
1217
+ testTargetResults. append ( . init( name: String ( testTargetName) , testCases: [ . init( name: testCase. name, tests: testResults) ] ) )
1218
+ }
1219
+ }
1220
+ }
1221
+ }
1222
+
1223
+ // Deal with code coverage, if enabled.
1224
+ let codeCoverageDataFile : AbsolutePath ?
1225
+ if parameters. enableCodeCoverage {
1226
+ // Use `llvm-prof` to merge all the `.profraw` files into a single `.profdata` file.
1227
+ let mergedCovFile = buildParameters. codeCovDataFile
1228
+ let codeCovFileNames = try localFileSystem. getDirectoryContents ( buildParameters. codeCovPath)
1229
+ var llvmProfCommand = [ try toolchain. getLLVMProf ( ) . pathString]
1230
+ llvmProfCommand += [ " merge " , " -sparse " ]
1231
+ for fileName in codeCovFileNames where fileName. hasSuffix ( " .profraw " ) {
1232
+ let filePath = buildParameters. codeCovPath. appending ( component: fileName)
1233
+ llvmProfCommand. append ( filePath. pathString)
1234
+ }
1235
+ llvmProfCommand += [ " -o " , mergedCovFile. pathString]
1236
+ try Process . checkNonZeroExit ( arguments: llvmProfCommand)
1237
+
1238
+ // Use `llvm-cov` to export the merged `.profdata` file contents in JSON form.
1239
+ var llvmCovCommand = [ try toolchain. getLLVMCov ( ) . pathString]
1240
+ llvmCovCommand += [ " export " , " -instr-profile= \( mergedCovFile. pathString) " ]
1241
+ for product in buildSystem. builtTestProducts {
1242
+ llvmCovCommand. append ( " -object " )
1243
+ llvmCovCommand. append ( product. binaryPath. pathString)
1244
+ }
1245
+ // We get the output on stdout, and have to write it to a JSON ourselves.
1246
+ let jsonOutput = try Process . checkNonZeroExit ( arguments: llvmCovCommand)
1247
+ let jsonCovFile = buildParameters. codeCovDataFile. parentDirectory. appending ( component: buildParameters. codeCovDataFile. basenameWithoutExt + " .json " )
1248
+ try localFileSystem. writeFileContents ( jsonCovFile, string: jsonOutput)
1249
+
1250
+ // Return the path of the exported code coverage data file.
1251
+ codeCoverageDataFile = jsonCovFile
1252
+ }
1253
+ else {
1254
+ codeCoverageDataFile = nil
1255
+ }
1256
+
1257
+ // Return the results to the plugin. We only consider the test run a success if no test failed.
1258
+ return PluginInvocationTestResult (
1259
+ succeeded: ( numFailedTests == 0 ) ,
1260
+ testTargets: testTargetResults,
1261
+ codeCoverageDataFile: codeCoverageDataFile? . pathString)
1262
+ }
1263
+ }
1264
+
1265
+ fileprivate extension DispatchTimeInterval {
1266
+ var asSeconds : Double ? {
1267
+ switch self {
1268
+ case . seconds( let value) :
1269
+ return Double ( value)
1270
+ case . milliseconds( let value) :
1271
+ return Double ( value) / 1000
1272
+ case . microseconds( let value) :
1273
+ return Double ( value) / 1_000_000
1274
+ case . nanoseconds( let value) :
1275
+ return Double ( value) / 1_000_000_000
1276
+ default :
1277
+ return . none
1278
+ }
1279
+ }
1280
+ }
0 commit comments