@@ -187,6 +187,7 @@ class PluginTests: XCTestCase {
187
187
}
188
188
189
189
func testCommandPluginInvocation( ) throws {
190
+ // FIXME: This test is getting quite long — we should add some support functionality for creating synthetic plugin tests and factor this out into separate tests.
190
191
try testWithTemporaryDirectory { tmpPath in
191
192
// Create a sample package with a library target and a plugin. It depends on a sample package.
192
193
let packageDir = tmpPath. appending ( components: " MyPackage " )
@@ -207,10 +208,22 @@ class PluginTests: XCTestCase {
207
208
]
208
209
),
209
210
.plugin(
210
- name: " MyPlugin " ,
211
+ name: " PluginPrintingInfo " ,
211
212
capability: .command(
212
- intent: .custom(verb: " mycmd " , description: " What is mycmd anyway? " ),
213
- permissions: [.writeToPackageDirectory(reason: " YOLO " )]
213
+ intent: .custom(verb: " print-info " , description: " Description of the command " ),
214
+ permissions: [.writeToPackageDirectory(reason: " Reason for wanting to write to package directory " )]
215
+ )
216
+ ),
217
+ .plugin(
218
+ name: " PluginFailingWithError " ,
219
+ capability: .command(
220
+ intent: .custom(verb: " fail-with-error " , description: " Sample plugin that throws an error " )
221
+ )
222
+ ),
223
+ .plugin(
224
+ name: " PluginFailingWithoutError " ,
225
+ capability: .command(
226
+ intent: .custom(verb: " fail-without-error " , description: " Sample plugin that exits without error " )
214
227
)
215
228
),
216
229
]
@@ -222,7 +235,7 @@ class PluginTests: XCTestCase {
222
235
public func Foo() { }
223
236
"""
224
237
}
225
- try localFileSystem. writeFileContents ( packageDir. appending ( components: " Plugins " , " MyPlugin " , " plugin.swift " ) ) {
238
+ try localFileSystem. writeFileContents ( packageDir. appending ( components: " Plugins " , " PluginPrintingInfo " , " plugin.swift " ) ) {
226
239
$0 <<< """
227
240
import PackagePlugin
228
241
@@ -243,6 +256,49 @@ class PluginTests: XCTestCase {
243
256
}
244
257
"""
245
258
}
259
+ try localFileSystem. writeFileContents ( packageDir. appending ( components: " Plugins " , " PluginFailingWithError " , " plugin.swift " ) ) {
260
+ $0 <<< """
261
+ import PackagePlugin
262
+
263
+ @main
264
+ struct MyCommandPlugin: CommandPlugin {
265
+ func performCommand(
266
+ context: PluginContext,
267
+ targets: [Target],
268
+ arguments: [String]
269
+ ) throws {
270
+ // Print some output that should appear before the error diagnostic.
271
+ print( " This text should appear before the error. " )
272
+
273
+ // Throw an uncaught error that should be reported as a diagnostics.
274
+ throw " Houston, we have a problem. "
275
+ }
276
+ }
277
+ extension String: Error { }
278
+ """
279
+ }
280
+ try localFileSystem. writeFileContents ( packageDir. appending ( components: " Plugins " , " PluginFailingWithoutError " , " plugin.swift " ) ) {
281
+ $0 <<< """
282
+ import PackagePlugin
283
+ import Foundation
284
+
285
+ @main
286
+ struct MyCommandPlugin: CommandPlugin {
287
+ func performCommand(
288
+ context: PluginContext,
289
+ targets: [Target],
290
+ arguments: [String]
291
+ ) throws {
292
+ // Print some output that should appear before we exit.
293
+ print( " This text should appear before we exit. " )
294
+
295
+ // Just exit with an error code without an emitting error.
296
+ exit(1)
297
+ }
298
+ }
299
+ extension String: Error { }
300
+ """
301
+ }
246
302
247
303
// Create the sample vendored dependency package.
248
304
try localFileSystem. writeFileContents ( packageDir. appending ( components: " VendoredDependencies " , " HelperPackage " , " Package.swift " ) ) {
@@ -302,62 +358,110 @@ class PluginTests: XCTestCase {
302
358
let libraryTarget = try XCTUnwrap ( package . targets. map ( \. underlyingTarget) . first { $0. name == " MyLibrary " } as? SwiftTarget )
303
359
XCTAssertEqual ( libraryTarget. type, . library)
304
360
305
- // Find the command plugin in our test package.
306
- let pluginTarget = try XCTUnwrap ( package . targets. map ( \. underlyingTarget) . first { $0. name == " MyPlugin " } as? PluginTarget )
307
- XCTAssertEqual ( pluginTarget. type, . plugin)
308
-
309
361
// Set up a delegate to handle callbacks from the command plugin.
310
362
let delegateQueue = DispatchQueue ( label: " plugin-invocation " )
311
363
class PluginDelegate : PluginInvocationDelegate {
312
364
let delegateQueue : DispatchQueue
313
- var outputData = Data ( )
365
+ var diagnostics : [ Basics . Diagnostic ] = [ ]
314
366
315
367
init ( delegateQueue: DispatchQueue ) {
316
368
self . delegateQueue = delegateQueue
317
369
}
318
370
319
371
func pluginEmittedOutput( _ data: Data ) {
372
+ // Add each line of emitted output as a `.info` diagnostic.
320
373
dispatchPrecondition ( condition: . onQueue( delegateQueue) )
321
- outputData. append ( contentsOf: data)
322
- print ( String ( decoding: data, as: UTF8 . self)
323
- . split ( separator: " \n " )
324
- . map { " 🧩 \( $0) " }
325
- . joined ( separator: " \n " )
326
- )
374
+ let textlines = String ( decoding: data, as: UTF8 . self) . split ( separator: " \n " )
375
+ print ( textlines. map { " 🧩 \( $0) " } . joined ( separator: " \n " ) )
376
+ diagnostics. append ( contentsOf: textlines. map {
377
+ Basics . Diagnostic ( severity: . info, message: String ( $0) , metadata: . none)
378
+ } )
327
379
}
328
380
329
381
func pluginEmittedDiagnostic( _ diagnostic: Basics . Diagnostic ) {
382
+ // Add the diagnostic as-is.
330
383
dispatchPrecondition ( condition: . onQueue( delegateQueue) )
384
+ diagnostics. append ( diagnostic)
331
385
}
332
386
}
333
- let pluginDelegate = PluginDelegate ( delegateQueue: delegateQueue)
334
387
335
- // Invoke the command plugin.
336
- let pluginCacheDir = tmpPath. appending ( component: " plugin-cache " )
337
- let pluginOutputDir = tmpPath. appending ( component: " plugin-output " )
338
- let pluginScriptRunner = DefaultPluginScriptRunner ( cacheDir: pluginCacheDir, toolchain: ToolchainConfiguration . default)
339
- let target = try XCTUnwrap ( package . targets. first { $0. underlyingTarget == libraryTarget } )
340
- let invocationSucceeded = try tsc_await { pluginTarget. invoke (
341
- action: . performCommand(
342
- targets: [ target ] ,
343
- arguments: [ " veni " , " vidi " , " vici " ] ) ,
344
- package : package ,
345
- buildEnvironment: BuildEnvironment ( platform: . macOS, configuration: . debug) ,
346
- scriptRunner: pluginScriptRunner,
347
- outputDirectory: pluginOutputDir,
348
- toolSearchDirectories: [ UserToolchain . default. swiftCompilerPath. parentDirectory] ,
349
- toolNamesToPaths: [ : ] ,
350
- fileSystem: localFileSystem,
351
- observabilityScope: observability. topScope,
352
- callbackQueue: delegateQueue,
353
- delegate: pluginDelegate,
354
- completion: $0) }
355
-
356
- // Check the results.
357
- XCTAssertTrue ( invocationSucceeded)
358
- let outputText = String ( decoding: pluginDelegate. outputData, as: UTF8 . self)
359
- XCTAssertTrue ( outputText. contains ( " Root package is MyPackage. " ) , outputText)
360
- XCTAssertTrue ( outputText. contains ( " Found the swiftc tool " ) , outputText)
388
+ // Helper function to invoke a plugin with given input and to check its outputs.
389
+ func testCommand(
390
+ package : ResolvedPackage ,
391
+ plugin pluginName: String ,
392
+ targets targetNames: [ String ] ,
393
+ arguments: [ String ] ,
394
+ toolSearchDirectories: [ AbsolutePath ] = [ UserToolchain . default. swiftCompilerPath. parentDirectory] ,
395
+ toolNamesToPaths: [ String : AbsolutePath ] = [ : ] ,
396
+ file: StaticString = #file,
397
+ line: UInt = #line,
398
+ expectFailure: Bool = false ,
399
+ diagnosticsChecker: ( DiagnosticsTestResult ) throws -> Void
400
+ ) {
401
+ // Find the named plugin.
402
+ let plugins = package . targets. compactMap { $0. underlyingTarget as? PluginTarget }
403
+ guard let plugin = plugins. first ( where: { $0. name == pluginName } ) else {
404
+ return XCTFail ( " There is no plugin target named ‘ \( pluginName) ’ " )
405
+ }
406
+ XCTAssertTrue ( plugin. type == . plugin, " Target \( plugin) isn’t a plugin " )
407
+
408
+ // Find the named input targets to the plugin.
409
+ var targets : [ ResolvedTarget ] = [ ]
410
+ for targetName in targetNames {
411
+ guard let target = package . targets. first ( where: { $0. underlyingTarget. name == targetName } ) else {
412
+ return XCTFail ( " There is no target named ‘ \( targetName) ’ " )
413
+ }
414
+ XCTAssertTrue ( target. type != . plugin, " Target \( target) is a plugin " )
415
+ targets. append ( target)
416
+ }
417
+
418
+ let pluginDir = tmpPath. appending ( components: package . identity. description, plugin. name)
419
+ let scriptRunner = DefaultPluginScriptRunner ( cacheDir: pluginDir. appending ( component: " cache " ) , toolchain: ToolchainConfiguration . default)
420
+ let delegate = PluginDelegate ( delegateQueue: delegateQueue)
421
+ do {
422
+ let success = try tsc_await { plugin. invoke (
423
+ action: . performCommand( targets: targets, arguments: arguments) ,
424
+ package : package ,
425
+ buildEnvironment: BuildEnvironment ( platform: . macOS, configuration: . debug) ,
426
+ scriptRunner: scriptRunner,
427
+ outputDirectory: pluginDir. appending ( component: " output " ) ,
428
+ toolSearchDirectories: [ UserToolchain . default. swiftCompilerPath. parentDirectory] ,
429
+ toolNamesToPaths: [ : ] ,
430
+ fileSystem: localFileSystem,
431
+ observabilityScope: observability. topScope,
432
+ callbackQueue: delegateQueue,
433
+ delegate: delegate,
434
+ completion: $0) }
435
+ if expectFailure {
436
+ XCTAssertFalse ( success, " expected command to fail, but it succeeded " , file: file, line: line)
437
+ }
438
+ else {
439
+ XCTAssertTrue ( success, " expected command to succeed, but it failed " , file: file, line: line)
440
+ }
441
+ }
442
+ catch {
443
+ XCTFail ( " error \( String ( describing: error) ) " , file: file, line: line)
444
+ }
445
+ testDiagnostics ( delegate. diagnostics, problemsOnly: false , file: file, line: line, handler: diagnosticsChecker)
446
+ }
447
+
448
+ // Invoke the command plugin that prints out various things it was given, and check them.
449
+ testCommand ( package : package , plugin: " PluginPrintingInfo " , targets: [ " MyLibrary " ] , arguments: [ " veni " , " vidi " , " vici " ] ) { output in
450
+ output. check ( diagnostic: . equal( " Root package is MyPackage. " ) , severity: . info)
451
+ output. check ( diagnostic: . and( . prefix( " Found the swiftc tool " ) , . suffix( " . " ) ) , severity: . info)
452
+ }
453
+
454
+ // Invoke the command plugin that throws an unhandled error at the top level.
455
+ testCommand ( package : package , plugin: " PluginFailingWithError " , targets: [ ] , arguments: [ ] , expectFailure: true ) { output in
456
+ output. check ( diagnostic: . equal( " This text should appear before the error. " ) , severity: . info)
457
+ output. check ( diagnostic: . equal( " Houston, we have a problem. " ) , severity: . error)
458
+
459
+ }
460
+ // Invoke the command plugin that exits with code 1 without returning an error.
461
+ testCommand ( package : package , plugin: " PluginFailingWithoutError " , targets: [ ] , arguments: [ ] , expectFailure: true ) { output in
462
+ output. check ( diagnostic: . equal( " This text should appear before we exit. " ) , severity: . info)
463
+ output. check ( diagnostic: . equal( " Plugin ended with exit code 1 " ) , severity: . error)
464
+ }
361
465
}
362
466
}
363
467
}
0 commit comments