@@ -47,6 +47,12 @@ final class IncrementalCompilationTests: XCTestCase {
47
47
var masterSwiftDepsPath : AbsolutePath {
48
48
derivedDataPath. appending ( component: " \( module) -master.swiftdeps " )
49
49
}
50
+ var priorsPath : AbsolutePath {
51
+ derivedDataPath. appending ( component: " \( module) -master.priors " )
52
+ }
53
+ func swiftDepsPath( basename: String ) -> AbsolutePath {
54
+ derivedDataPath. appending ( component: " \( basename) .swiftdeps " )
55
+ }
50
56
var autolinkIncrementalExpectations : [ String ] {
51
57
[
52
58
" Incremental compilation: Queuing Extracting autolink information for module \( module) " ,
@@ -239,7 +245,7 @@ fileprivate extension Driver {
239
245
}
240
246
}
241
247
242
- // MARK: - Actual incremental tests
248
+ // MARK: - Simpler incremental tests
243
249
extension IncrementalCompilationTests {
244
250
245
251
// FIXME: why does it fail on Linux in CI?
@@ -271,7 +277,6 @@ extension IncrementalCompilationTests {
271
277
#endif
272
278
}
273
279
274
-
275
280
/// Ensure that the mod date of the input comes back exactly the same via the build-record.
276
281
/// Otherwise the up-to-date calculation in `IncrementalCompilationState` will fail.
277
282
func testBuildRecordDateAccuracy( ) throws {
@@ -313,6 +318,120 @@ extension IncrementalCompilationTests {
313
318
}
314
319
}
315
320
321
+ // MARK: - Incremental file removal tests
322
+ /// In order to ensure robustness, test what happens under various conditions when a source file is
323
+ /// removed.
324
+ /// The following is a lot of work to get something that prints nicely. Need an enum with both a string and an int value.
325
+ fileprivate enum RemovalTestOption : String , CaseIterable , Comparable , Hashable , CustomStringConvertible {
326
+ case
327
+ removeInputFromInvocation,
328
+ removeSourceFile,
329
+ removeEntryFromOutputFileMap,
330
+ removeSwiftDepsFile,
331
+ restoreBadPriors
332
+
333
+ private static let byInt = [ Int: Self] ( uniqueKeysWithValues: allCases. enumerated ( ) . map { ( $0, $1) } )
334
+ private static let intFor = [ Self: Int] ( uniqueKeysWithValues: allCases. enumerated ( ) . map { ( $1, $0) } )
335
+
336
+ var intValue : Int { Self . intFor [ self ] !}
337
+ init ( fromInt i: Int ) { self = Self . byInt [ i] !}
338
+
339
+ static func < ( lhs: RemovalTestOption , rhs: RemovalTestOption ) -> Bool {
340
+ lhs. intValue < rhs. intValue
341
+ }
342
+ var mask : Int { 1 << intValue}
343
+ static let maxIntValue = allCases. map { $0. intValue} . max ( ) !
344
+ static let maxCombinedValue = ( 1 << maxIntValue) - 1
345
+
346
+ var description : String { rawValue }
347
+ }
348
+
349
+ /// Only 5 elements, an array is fine
350
+ fileprivate typealias RemovalTestOptions = [ RemovalTestOption ]
351
+
352
+ extension RemovalTestOptions {
353
+ fileprivate static let allCombinations : [ RemovalTestOptions ] =
354
+ ( 0 ... RemovalTestOption . maxCombinedValue) . map ( decoding)
355
+
356
+ fileprivate static func decoding( _ bits: Int ) -> Self {
357
+ RemovalTestOption . allCases. filter { opt in
358
+ ( 1 << opt. intValue) & bits != 0
359
+ }
360
+ }
361
+ }
362
+
363
+ extension IncrementalCompilationTests {
364
+ /// While all cases are being made to work, just test for now in known good cases
365
+ func testRemovalOfPassingCases( ) throws {
366
+ try testRemoval ( includeFailingCombos: false )
367
+ }
368
+
369
+ /// Someday, turn this test on and test all cases
370
+ func testRemovalOfAllCases( ) throws {
371
+ throw XCTSkip ( " unimplemented " )
372
+ try testRemoval ( includeFailingCombos: true )
373
+ }
374
+
375
+ func testRemoval( includeFailingCombos: Bool ) throws {
376
+ #if !os(Linux)
377
+ let knownGoodCombos : [ [ RemovalTestOption ] ] = [
378
+ [ . removeInputFromInvocation] ,
379
+ // next up:
380
+ // [.removeInputFromInvocation, .restoreBadPriors],
381
+ ]
382
+ for optionsToTest in RemovalTestOptions . allCombinations {
383
+ if knownGoodCombos. contains ( optionsToTest) {
384
+ try testRemoval ( optionsToTest)
385
+ }
386
+ else if includeFailingCombos {
387
+ try XCTExpectFailure ( " \( optionsToTest) should fail " ) {
388
+ try testRemoval ( optionsToTest)
389
+ }
390
+ }
391
+ }
392
+ #endif
393
+ }
394
+
395
+ private func testRemoval( _ options: RemovalTestOptions ) throws {
396
+ guard !options. isEmpty else { return }
397
+ print ( " *** testRemoval \( options) *** " , to: & stderrStream) ; stderrStream. flush ( )
398
+
399
+ let newInput = " another "
400
+ let topLevelName = " nameInAnother "
401
+ try testAddingInput ( newInput: newInput, defining: topLevelName)
402
+ if options. contains ( . removeSourceFile) {
403
+ removeInput ( newInput)
404
+ }
405
+ if options. contains ( . removeSwiftDepsFile) {
406
+ removeSwiftDeps ( newInput)
407
+ }
408
+ if options. contains ( . removeEntryFromOutputFileMap) {
409
+ // FACTOR
410
+ OutputFileMapCreator . write ( module: module,
411
+ inputPaths: inputPathsAndContents. map { $0. 0 } ,
412
+ derivedData: derivedDataPath,
413
+ to: OFM)
414
+ }
415
+ let includeInputInInvocation = !options. contains ( . removeInputFromInvocation)
416
+ do {
417
+ let wrapperFn = options. contains ( . restoreBadPriors)
418
+ ? preservingPriorsDo
419
+ : { try $0 ( ) }
420
+ try wrapperFn {
421
+ try self . checkNonincrementalAfterRemoving (
422
+ removedInput: newInput,
423
+ defining: topLevelName,
424
+ includeInputInInvocation: includeInputInInvocation)
425
+ }
426
+ }
427
+ try checkRestorationOfIncrementalityAfterRemoval (
428
+ removedInput: newInput,
429
+ defining: topLevelName,
430
+ includeInputInInvocation: includeInputInInvocation,
431
+ afterRestoringBadPriors: options. contains ( . restoreBadPriors) )
432
+ }
433
+ }
434
+
316
435
// MARK: - Incremental test stages
317
436
extension IncrementalCompilationTests {
318
437
/// Setup the initial post-build state.
@@ -622,6 +741,95 @@ extension IncrementalCompilationTests {
622
741
XCTAssert ( graph. contains ( sourceBasenameWithoutExt: newInput) )
623
742
XCTAssert ( graph. contains ( name: topLevelName) )
624
743
}
744
+
745
+ /// Check fallback to nonincremental build after a removal.
746
+ ///
747
+ /// - Parameters:
748
+ /// - newInput: The basename without extension of the removed input
749
+ /// - defining: A top level name defined by the removed file
750
+ /// - includeInputInInvocation: include the removed input in the invocation
751
+ private func checkNonincrementalAfterRemoving(
752
+ removedInput: String ,
753
+ defining topLevelName: String ,
754
+ includeInputInInvocation: Bool
755
+ ) throws {
756
+ let extraArguments = includeInputInInvocation
757
+ ? [ inputPath ( basename: removedInput) . pathString]
758
+ : [ ]
759
+ try doABuild (
760
+ " after removal of \( removedInput) " ,
761
+ checkDiagnostics: true ,
762
+ extraArguments: extraArguments,
763
+ expectingRemarks: [
764
+ " Incremental compilation: Incremental compilation has been disabled, because the following inputs were used in the previous compilation but not in this one: \( removedInput) .swift " ,
765
+ " Found 2 batchable jobs " ,
766
+ " Forming into 1 batch " ,
767
+ " Adding {compile: main.swift} to batch 0 " ,
768
+ " Adding {compile: other.swift} to batch 0 " ,
769
+ " Forming batch job from 2 constituents: main.swift, other.swift " ,
770
+ " Starting Compiling main.swift, other.swift " ,
771
+ " Finished Compiling main.swift, other.swift " ,
772
+ " Starting Linking theModule " ,
773
+ " Finished Linking theModule " ,
774
+ ] ,
775
+ whenAutolinking: autolinkLifecycleExpectations)
776
+ . verifyNoGraph ( )
777
+
778
+ verifyNoPriors ( )
779
+ }
780
+
781
+ /// Ensure that incremental builds happen after a removal.
782
+ ///
783
+ /// - Parameters:
784
+ /// - newInput: The basename without extension of the new file
785
+ /// - topLevelName: The top-level decl name added by the new file
786
+ @discardableResult
787
+ private func checkRestorationOfIncrementalityAfterRemoval(
788
+ removedInput: String ,
789
+ defining topLevelName: String ,
790
+ includeInputInInvocation: Bool ,
791
+ afterRestoringBadPriors: Bool
792
+ ) throws -> ModuleDependencyGraph {
793
+ let extraArguments = includeInputInInvocation
794
+ ? [ inputPath ( basename: removedInput) . pathString]
795
+ : [ ]
796
+ let expectations = afterRestoringBadPriors
797
+ ? [
798
+ " Incremental compilation: Read dependency graph " ,
799
+ " Incremental compilation: Enabling incremental cross-module building " ,
800
+ " Incremental compilation: May skip current input: {compile: main.o <= main.swift} " ,
801
+ " Incremental compilation: May skip current input: {compile: other.o <= other.swift} " ,
802
+ " Incremental compilation: Skipping input: {compile: main.o <= main.swift} " ,
803
+ " Incremental compilation: Skipping input: {compile: other.o <= other.swift} " ,
804
+ " Incremental compilation: Skipping job: Linking theModule; oldest output is current " ,
805
+ " Skipped Compiling main.swift " ,
806
+ " Skipped Compiling other.swift " ,
807
+ ] . map ( Diagnostic . Message. remark)
808
+ : [
809
+ " Incremental compilation: Created dependency graph from swiftdeps files " ,
810
+ " Incremental compilation: Enabling incremental cross-module building " ,
811
+ " Incremental compilation: May skip current input: {compile: main.o <= main.swift} " ,
812
+ " Incremental compilation: May skip current input: {compile: other.o <= other.swift} " ,
813
+ " Incremental compilation: Skipping input: {compile: main.o <= main.swift} " ,
814
+ " Incremental compilation: Skipping input: {compile: other.o <= other.swift} " ,
815
+ " Incremental compilation: Skipping job: Linking theModule; oldest output is current " ,
816
+ " Skipped Compiling main.swift " ,
817
+ " Skipped Compiling other.swift " ,
818
+ ] . map ( Diagnostic . Message. remark)
819
+ let graph = try doABuild (
820
+ " after after removal of \( removedInput) " ,
821
+ checkDiagnostics: true ,
822
+ extraArguments: extraArguments,
823
+ expecting: expectations,
824
+ expectingWhenAutolinking: autolinkLifecycleExpectations. map ( Diagnostic . Message. remark) )
825
+ . moduleDependencyGraph ( )
826
+
827
+ graph. verifyGraph ( )
828
+ graph. ensureOmits ( sourceBasenameWithoutExt: removedInput)
829
+ graph. ensureOmits ( name: topLevelName)
830
+
831
+ return graph
832
+ }
625
833
}
626
834
627
835
// MARK: - Incremental test perturbation helpers
@@ -632,6 +840,18 @@ extension IncrementalCompilationTests {
632
840
try ! localFileSystem. writeFileContents ( path) { $0 <<< contents }
633
841
}
634
842
843
+ private func removeInput( _ name: String ) {
844
+ print ( " *** removing input \( name) *** " , to: & stderrStream) ; stderrStream. flush ( )
845
+ try ! localFileSystem. removeFileTree ( inputPath ( basename: name) )
846
+ }
847
+
848
+ private func removeSwiftDeps( _ name: String ) {
849
+ print ( " *** removing swiftdeps \( name) *** " , to: & stderrStream) ; stderrStream. flush ( )
850
+ let swiftDepsPath = swiftDepsPath ( basename: name)
851
+ XCTAssert ( localFileSystem. exists ( swiftDepsPath) )
852
+ try ! localFileSystem. removeFileTree ( swiftDepsPath)
853
+ }
854
+
635
855
private func replace( contentsOf name: String , with replacement: String ) {
636
856
print ( " *** replacing \( name) *** " , to: & stderrStream) ; stderrStream. flush ( )
637
857
let path = inputPath ( basename: name)
@@ -648,10 +868,27 @@ extension IncrementalCompilationTests {
648
868
$0 <<< contents
649
869
}
650
870
}
871
+
872
+ private func readPriors( ) -> ByteString ? {
873
+ try ? localFileSystem. readFileContents ( priorsPath)
874
+ }
875
+
876
+ private func writePriors( _ contents: ByteString ) {
877
+ try ! localFileSystem. writeFileContents ( priorsPath, bytes: contents)
878
+ }
879
+
880
+ private func preservingPriorsDo( _ fn: ( ) throws -> Void ) throws {
881
+ let contents = try XCTUnwrap ( readPriors ( ) )
882
+ try fn ( )
883
+ writePriors ( contents)
884
+ }
885
+
886
+ private func verifyNoPriors( ) {
887
+ XCTAssertNil ( readPriors ( ) . map { " \( $0. count) bytes " } , " Should not have found priors " )
888
+ }
651
889
}
652
890
653
891
// MARK: - Graph inspection
654
-
655
892
fileprivate extension Driver {
656
893
func moduleDependencyGraph( ) throws -> ModuleDependencyGraph {
657
894
do { return try XCTUnwrap ( incrementalCompilationState? . moduleDependencyGraph) }
@@ -660,6 +897,9 @@ fileprivate extension Driver {
660
897
throw error
661
898
}
662
899
}
900
+ func verifyNoGraph( ) {
901
+ XCTAssertNil ( incrementalCompilationState)
902
+ }
663
903
}
664
904
665
905
fileprivate extension ModuleDependencyGraph {
@@ -677,12 +917,14 @@ fileprivate extension ModuleDependencyGraph {
677
917
}
678
918
func ensureOmits( sourceBasenameWithoutExt target: String ) {
679
919
nodeFinder. forEachNode { node in
680
- XCTAssertFalse ( node. contains ( sourceBasenameWithoutExt: target) )
920
+ XCTAssertFalse ( node. contains ( sourceBasenameWithoutExt: target) ,
921
+ " graph should omit source: \( target) " )
681
922
}
682
923
}
683
924
func ensureOmits( name: String ) {
684
925
nodeFinder. forEachNode { node in
685
- XCTAssertFalse ( node. contains ( name: name) )
926
+ XCTAssertFalse ( node. contains ( name: name) ,
927
+ " graph should omit decl named: \( name) " )
686
928
}
687
929
}
688
930
}
0 commit comments