@@ -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,116 @@ 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
+ removePreviouslyAddedInputFromOutputFileMap,
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 testRemovalInPassingCases( ) throws {
366
+ try testRemoval ( includeFailingCombos: false )
367
+ }
368
+
369
+ /// Someday, turn this test on and test all cases
370
+ func testRemovalInAllCases( ) 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
+ ]
380
+ for optionsToTest in RemovalTestOptions . allCombinations {
381
+ if knownGoodCombos. contains ( optionsToTest) {
382
+ try testRemoval ( optionsToTest)
383
+ }
384
+ else if includeFailingCombos {
385
+ try testRemoval ( optionsToTest)
386
+ }
387
+ }
388
+ #endif
389
+ }
390
+
391
+ private func testRemoval( _ options: RemovalTestOptions ) throws {
392
+ guard !options. isEmpty else { return }
393
+ print ( " *** testRemoval \( options) *** " , to: & stderrStream) ; stderrStream. flush ( )
394
+
395
+ let newInput = " another "
396
+ let topLevelName = " nameInAnother "
397
+ try testAddingInput ( newInput: newInput, defining: topLevelName)
398
+ if options. contains ( . removeSourceFile) {
399
+ removeInput ( newInput)
400
+ }
401
+ if options. contains ( . removeSwiftDepsFile) {
402
+ removeSwiftDeps ( newInput)
403
+ }
404
+ if options. contains ( . removePreviouslyAddedInputFromOutputFileMap) {
405
+ // FACTOR
406
+ OutputFileMapCreator . write ( module: module,
407
+ inputPaths: inputPathsAndContents. map { $0. 0 } ,
408
+ derivedData: derivedDataPath,
409
+ to: OFM)
410
+ }
411
+ let includeInputInInvocation = !options. contains ( . removeInputFromInvocation)
412
+ do {
413
+ let wrapperFn = options. contains ( . restoreBadPriors)
414
+ ? preservingPriorsDo
415
+ : { try $0 ( ) }
416
+ try wrapperFn {
417
+ try self . checkNonincrementalAfterRemoving (
418
+ removedInput: newInput,
419
+ defining: topLevelName,
420
+ includeInputInInvocation: includeInputInInvocation)
421
+ }
422
+ }
423
+ try checkRestorationOfIncrementalityAfterRemoval (
424
+ removedInput: newInput,
425
+ defining: topLevelName,
426
+ includeInputInInvocation: includeInputInInvocation,
427
+ afterRestoringBadPriors: options. contains ( . restoreBadPriors) )
428
+ }
429
+ }
430
+
316
431
// MARK: - Incremental test stages
317
432
extension IncrementalCompilationTests {
318
433
/// Setup the initial post-build state.
@@ -622,6 +737,95 @@ extension IncrementalCompilationTests {
622
737
XCTAssert ( graph. contains ( sourceBasenameWithoutExt: newInput) )
623
738
XCTAssert ( graph. contains ( name: topLevelName) )
624
739
}
740
+
741
+ /// Check fallback to nonincremental build after a removal.
742
+ ///
743
+ /// - Parameters:
744
+ /// - newInput: The basename without extension of the removed input
745
+ /// - defining: A top level name defined by the removed file
746
+ /// - includeInputInInvocation: include the removed input in the invocation
747
+ private func checkNonincrementalAfterRemoving(
748
+ removedInput: String ,
749
+ defining topLevelName: String ,
750
+ includeInputInInvocation: Bool
751
+ ) throws {
752
+ let extraArguments = includeInputInInvocation
753
+ ? [ inputPath ( basename: removedInput) . pathString]
754
+ : [ ]
755
+ try doABuild (
756
+ " after removal of \( removedInput) " ,
757
+ checkDiagnostics: true ,
758
+ extraArguments: extraArguments,
759
+ expectingRemarks: [
760
+ " Incremental compilation: Incremental compilation has been disabled, because the following inputs were used in the previous compilation but not in this one: \( removedInput) .swift " ,
761
+ " Found 2 batchable jobs " ,
762
+ " Forming into 1 batch " ,
763
+ " Adding {compile: main.swift} to batch 0 " ,
764
+ " Adding {compile: other.swift} to batch 0 " ,
765
+ " Forming batch job from 2 constituents: main.swift, other.swift " ,
766
+ " Starting Compiling main.swift, other.swift " ,
767
+ " Finished Compiling main.swift, other.swift " ,
768
+ " Starting Linking theModule " ,
769
+ " Finished Linking theModule " ,
770
+ ] ,
771
+ whenAutolinking: autolinkLifecycleExpectations)
772
+ . verifyNoGraph ( )
773
+
774
+ verifyNoPriors ( )
775
+ }
776
+
777
+ /// Ensure that incremental builds happen after a removal.
778
+ ///
779
+ /// - Parameters:
780
+ /// - newInput: The basename without extension of the new file
781
+ /// - topLevelName: The top-level decl name added by the new file
782
+ @discardableResult
783
+ private func checkRestorationOfIncrementalityAfterRemoval(
784
+ removedInput: String ,
785
+ defining topLevelName: String ,
786
+ includeInputInInvocation: Bool ,
787
+ afterRestoringBadPriors: Bool
788
+ ) throws -> ModuleDependencyGraph {
789
+ let extraArguments = includeInputInInvocation
790
+ ? [ inputPath ( basename: removedInput) . pathString]
791
+ : [ ]
792
+ let expectations = afterRestoringBadPriors
793
+ ? [
794
+ " Incremental compilation: Read dependency graph " ,
795
+ " Incremental compilation: Enabling incremental cross-module building " ,
796
+ " Incremental compilation: May skip current input: {compile: main.o <= main.swift} " ,
797
+ " Incremental compilation: May skip current input: {compile: other.o <= other.swift} " ,
798
+ " Incremental compilation: Skipping input: {compile: main.o <= main.swift} " ,
799
+ " Incremental compilation: Skipping input: {compile: other.o <= other.swift} " ,
800
+ " Incremental compilation: Skipping job: Linking theModule; oldest output is current " ,
801
+ " Skipped Compiling main.swift " ,
802
+ " Skipped Compiling other.swift " ,
803
+ ] . map ( Diagnostic . Message. remark)
804
+ : [
805
+ " Incremental compilation: Created dependency graph from swiftdeps files " ,
806
+ " Incremental compilation: Enabling incremental cross-module building " ,
807
+ " Incremental compilation: May skip current input: {compile: main.o <= main.swift} " ,
808
+ " Incremental compilation: May skip current input: {compile: other.o <= other.swift} " ,
809
+ " Incremental compilation: Skipping input: {compile: main.o <= main.swift} " ,
810
+ " Incremental compilation: Skipping input: {compile: other.o <= other.swift} " ,
811
+ " Incremental compilation: Skipping job: Linking theModule; oldest output is current " ,
812
+ " Skipped Compiling main.swift " ,
813
+ " Skipped Compiling other.swift " ,
814
+ ] . map ( Diagnostic . Message. remark)
815
+ let graph = try doABuild (
816
+ " after after removal of \( removedInput) " ,
817
+ checkDiagnostics: true ,
818
+ extraArguments: extraArguments,
819
+ expecting: expectations,
820
+ expectingWhenAutolinking: autolinkLifecycleExpectations. map ( Diagnostic . Message. remark) )
821
+ . moduleDependencyGraph ( )
822
+
823
+ graph. verifyGraph ( )
824
+ graph. ensureOmits ( sourceBasenameWithoutExt: removedInput)
825
+ graph. ensureOmits ( name: topLevelName)
826
+
827
+ return graph
828
+ }
625
829
}
626
830
627
831
// MARK: - Incremental test perturbation helpers
@@ -632,6 +836,18 @@ extension IncrementalCompilationTests {
632
836
try ! localFileSystem. writeFileContents ( path) { $0 <<< contents }
633
837
}
634
838
839
+ private func removeInput( _ name: String ) {
840
+ print ( " *** removing input \( name) *** " , to: & stderrStream) ; stderrStream. flush ( )
841
+ try ! localFileSystem. removeFileTree ( inputPath ( basename: name) )
842
+ }
843
+
844
+ private func removeSwiftDeps( _ name: String ) {
845
+ print ( " *** removing swiftdeps \( name) *** " , to: & stderrStream) ; stderrStream. flush ( )
846
+ let swiftDepsPath = swiftDepsPath ( basename: name)
847
+ XCTAssert ( localFileSystem. exists ( swiftDepsPath) )
848
+ try ! localFileSystem. removeFileTree ( swiftDepsPath)
849
+ }
850
+
635
851
private func replace( contentsOf name: String , with replacement: String ) {
636
852
print ( " *** replacing \( name) *** " , to: & stderrStream) ; stderrStream. flush ( )
637
853
let path = inputPath ( basename: name)
@@ -648,10 +864,27 @@ extension IncrementalCompilationTests {
648
864
$0 <<< contents
649
865
}
650
866
}
867
+
868
+ private func readPriors( ) -> ByteString ? {
869
+ try ? localFileSystem. readFileContents ( priorsPath)
870
+ }
871
+
872
+ private func writePriors( _ contents: ByteString ) {
873
+ try ! localFileSystem. writeFileContents ( priorsPath, bytes: contents)
874
+ }
875
+
876
+ private func preservingPriorsDo( _ fn: ( ) throws -> Void ) throws {
877
+ let contents = try XCTUnwrap ( readPriors ( ) )
878
+ try fn ( )
879
+ writePriors ( contents)
880
+ }
881
+
882
+ private func verifyNoPriors( ) {
883
+ XCTAssertNil ( readPriors ( ) . map { " \( $0. count) bytes " } , " Should not have found priors " )
884
+ }
651
885
}
652
886
653
887
// MARK: - Graph inspection
654
-
655
888
fileprivate extension Driver {
656
889
func moduleDependencyGraph( ) throws -> ModuleDependencyGraph {
657
890
do { return try XCTUnwrap ( incrementalCompilationState? . moduleDependencyGraph) }
@@ -660,6 +893,9 @@ fileprivate extension Driver {
660
893
throw error
661
894
}
662
895
}
896
+ func verifyNoGraph( ) {
897
+ XCTAssertNil ( incrementalCompilationState)
898
+ }
663
899
}
664
900
665
901
fileprivate extension ModuleDependencyGraph {
@@ -677,12 +913,14 @@ fileprivate extension ModuleDependencyGraph {
677
913
}
678
914
func ensureOmits( sourceBasenameWithoutExt target: String ) {
679
915
nodeFinder. forEachNode { node in
680
- XCTAssertFalse ( node. contains ( sourceBasenameWithoutExt: target) )
916
+ XCTAssertFalse ( node. contains ( sourceBasenameWithoutExt: target) ,
917
+ " graph should omit source: \( target) " )
681
918
}
682
919
}
683
920
func ensureOmits( name: String ) {
684
921
nodeFinder. forEachNode { node in
685
- XCTAssertFalse ( node. contains ( name: name) )
922
+ XCTAssertFalse ( node. contains ( name: name) ,
923
+ " graph should omit decl named: \( name) " )
686
924
}
687
925
}
688
926
}
0 commit comments