@@ -1126,12 +1126,12 @@ final class PluginDelegate: PluginInvocationDelegate {
1126
1126
// Run the build in the background and call the completion handler when done.
1127
1127
DispatchQueue . sharedConcurrent. async {
1128
1128
completion ( Result {
1129
- return try self . doBuild ( subset: subset, parameters: parameters)
1129
+ return try self . performBuildForPlugin ( subset: subset, parameters: parameters)
1130
1130
} )
1131
1131
}
1132
1132
}
1133
1133
1134
- private func doBuild ( subset: PluginInvocationBuildSubset , parameters: PluginInvocationBuildParameters ) throws -> PluginInvocationBuildResult {
1134
+ private func performBuildForPlugin ( subset: PluginInvocationBuildSubset , parameters: PluginInvocationBuildParameters ) throws -> PluginInvocationBuildResult {
1135
1135
// Configure the build parameters.
1136
1136
var buildParameters = try self . swiftTool. buildParameters ( )
1137
1137
switch parameters. configuration {
@@ -1201,26 +1201,139 @@ final class PluginDelegate: PluginInvocationDelegate {
1201
1201
// Run the test in the background and call the completion handler when done.
1202
1202
DispatchQueue . sharedConcurrent. async {
1203
1203
completion ( Result {
1204
- return try self . doTest ( subset: subset, parameters: parameters)
1204
+ return try self . performTestsForPlugin ( subset: subset, parameters: parameters)
1205
1205
} )
1206
1206
}
1207
1207
}
1208
1208
1209
- private func doTest( subset: PluginInvocationTestSubset , parameters: PluginInvocationTestParameters ) throws -> PluginInvocationTestResult {
1210
- // FIXME: To implement this we should factor out a lot of the code in SwiftTestTool.
1211
- throw StringError ( " Running tests from a plugin is not yet implemented " )
1209
+ func performTestsForPlugin( subset: PluginInvocationTestSubset , parameters: PluginInvocationTestParameters ) throws -> PluginInvocationTestResult {
1210
+ // 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.
1211
+ let toolchain = try swiftTool. getToolchain ( )
1212
+ var buildParameters = try swiftTool. buildParameters ( )
1213
+ buildParameters. enableTestability = true
1214
+ buildParameters. enableCodeCoverage = parameters. enableCodeCoverage
1215
+ let buildSystem = try swiftTool. createBuildSystem ( buildParameters: buildParameters)
1216
+ try buildSystem. build ( subset: . allIncludingTests)
1217
+
1218
+ // Clean out the code coverage directory that may contain stale `profraw` files from a previous run of the code coverage tool.
1219
+ if parameters. enableCodeCoverage {
1220
+ try localFileSystem. removeFileTree ( buildParameters. codeCovPath)
1221
+ }
1222
+
1223
+ // Construct the environment we'll pass down to the tests.
1224
+ var environmentOptions = swiftTool. options
1225
+ environmentOptions. shouldEnableCodeCoverage = parameters. enableCodeCoverage
1226
+ let testEnvironment = try TestingSupport . constructTestEnvironment (
1227
+ toolchain: toolchain,
1228
+ options: environmentOptions,
1229
+ buildParameters: buildParameters)
1230
+
1231
+ // Iterate over the tests and run those that match the filter.
1232
+ var testTargetResults : [ PluginInvocationTestResult . TestTarget ] = [ ]
1233
+ var numFailedTests = 0
1234
+ for testProduct in buildSystem. builtTestProducts {
1235
+ // Get the test suites in the bundle. Each is just a container for test cases.
1236
+ let testSuites = try TestingSupport . getTestSuites ( fromTestAt: testProduct. bundlePath, swiftTool: swiftTool, swiftOptions: swiftTool. options)
1237
+ for testSuite in testSuites {
1238
+ // Each test suite is just a container for test cases (confusingly called "tests", though they are test cases).
1239
+ for testCase in testSuite. tests {
1240
+ // Each test case corresponds to a combination of target and a XCTestCase, and is a collection of tests that can actually be run.
1241
+ var testResults : [ PluginInvocationTestResult . TestTarget . TestCase . Test ] = [ ]
1242
+ for testName in testCase. tests {
1243
+ // Check if we should filter out this test.
1244
+ let testSpecifier = testCase. name + " / " + testName
1245
+ if case . filtered( let regexes) = subset {
1246
+ guard regexes. contains ( where: { testSpecifier. range ( of: $0, options: . regularExpression) != nil } ) else {
1247
+ continue
1248
+ }
1249
+ }
1250
+
1251
+ // Configure a test runner.
1252
+ let testRunner = TestRunner (
1253
+ bundlePaths: [ testProduct. bundlePath] ,
1254
+ xctestArg: testSpecifier,
1255
+ processSet: swiftTool. processSet,
1256
+ toolchain: toolchain,
1257
+ testEnv: testEnvironment,
1258
+ outputStream: swiftTool. outputStream,
1259
+ observabilityScope: swiftTool. observabilityScope)
1260
+
1261
+ // Run the test — for now we run the sequentially so we can capture accurate timing results.
1262
+ let startTime = DispatchTime . now ( )
1263
+ let ( success, _) = testRunner. test ( writeToOutputStream: false )
1264
+ let duration = Double ( startTime. distance ( to: . now( ) ) . milliseconds ( ) ?? 0 ) / 1000.0
1265
+ numFailedTests += success ? 0 : 1
1266
+ testResults. append ( . init( name: testName, result: success ? . succeeded : . failed, duration: duration) )
1267
+ }
1268
+
1269
+ // Don't add any results if we didn't run any tests.
1270
+ if testResults. isEmpty { continue }
1271
+
1272
+ // 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.
1273
+ let testTargetName = testCase. name. prefix ( while: { $0 != " . " } )
1274
+ if let lastTestTargetName = testTargetResults. last? . name, testTargetName == lastTestTargetName {
1275
+ // Same as last one, just extend its list of cases. We know we have a last one at this point.
1276
+ testTargetResults [ testTargetResults. count- 1 ] . testCases. append ( . init( name: testCase. name, tests: testResults) )
1277
+ }
1278
+ else {
1279
+ // Not the same, so start a new target result.
1280
+ testTargetResults. append ( . init( name: String ( testTargetName) , testCases: [ . init( name: testCase. name, tests: testResults) ] ) )
1281
+ }
1282
+ }
1283
+ }
1284
+ }
1285
+
1286
+ // Deal with code coverage, if enabled.
1287
+ let codeCoverageDataFile : AbsolutePath ?
1288
+ if parameters. enableCodeCoverage {
1289
+ // Use `llvm-prof` to merge all the `.profraw` files into a single `.profdata` file.
1290
+ let mergedCovFile = buildParameters. codeCovDataFile
1291
+ let codeCovFileNames = try localFileSystem. getDirectoryContents ( buildParameters. codeCovPath)
1292
+ var llvmProfCommand = [ try toolchain. getLLVMProf ( ) . pathString]
1293
+ llvmProfCommand += [ " merge " , " -sparse " ]
1294
+ for fileName in codeCovFileNames where fileName. hasSuffix ( " .profraw " ) {
1295
+ let filePath = buildParameters. codeCovPath. appending ( component: fileName)
1296
+ llvmProfCommand. append ( filePath. pathString)
1297
+ }
1298
+ llvmProfCommand += [ " -o " , mergedCovFile. pathString]
1299
+ try Process . checkNonZeroExit ( arguments: llvmProfCommand)
1300
+
1301
+ // Use `llvm-cov` to export the merged `.profdata` file contents in JSON form.
1302
+ var llvmCovCommand = [ try toolchain. getLLVMCov ( ) . pathString]
1303
+ llvmCovCommand += [ " export " , " -instr-profile= \( mergedCovFile. pathString) " ]
1304
+ for product in buildSystem. builtTestProducts {
1305
+ llvmCovCommand. append ( " -object " )
1306
+ llvmCovCommand. append ( product. binaryPath. pathString)
1307
+ }
1308
+ // We get the output on stdout, and have to write it to a JSON ourselves.
1309
+ let jsonOutput = try Process . checkNonZeroExit ( arguments: llvmCovCommand)
1310
+ let jsonCovFile = buildParameters. codeCovDataFile. parentDirectory. appending ( component: buildParameters. codeCovDataFile. basenameWithoutExt + " .json " )
1311
+ try localFileSystem. writeFileContents ( jsonCovFile, string: jsonOutput)
1312
+
1313
+ // Return the path of the exported code coverage data file.
1314
+ codeCoverageDataFile = jsonCovFile
1315
+ }
1316
+ else {
1317
+ codeCoverageDataFile = nil
1318
+ }
1319
+
1320
+ // Return the results to the plugin. We only consider the test run a success if no test failed.
1321
+ return PluginInvocationTestResult (
1322
+ succeeded: ( numFailedTests == 0 ) ,
1323
+ testTargets: testTargetResults,
1324
+ codeCoverageDataFile: codeCoverageDataFile? . pathString)
1212
1325
}
1213
1326
1214
1327
func pluginRequestedSymbolGraph( forTarget targetName: String , options: PluginInvocationSymbolGraphOptions , completion: @escaping ( Result < PluginInvocationSymbolGraphResult , Error > ) -> Void ) {
1215
1328
// Extract the symbol graph in the background and call the completion handler when done.
1216
1329
DispatchQueue . sharedConcurrent. async {
1217
1330
completion ( Result {
1218
- return try self . createSymbolGraph ( forTarget: targetName, options: options)
1331
+ return try self . createSymbolGraphForPlugin ( forTarget: targetName, options: options)
1219
1332
} )
1220
1333
}
1221
1334
}
1222
1335
1223
- private func createSymbolGraph ( forTarget targetName: String , options: PluginInvocationSymbolGraphOptions ) throws -> PluginInvocationSymbolGraphResult {
1336
+ private func createSymbolGraphForPlugin ( forTarget targetName: String , options: PluginInvocationSymbolGraphOptions ) throws -> PluginInvocationSymbolGraphResult {
1224
1337
// Current implementation uses `SymbolGraphExtract()` but in the future we should emit the symbol graph while building.
1225
1338
1226
1339
// Create a build operation for building the target., skipping the the cache because we need the build plan.
0 commit comments