Skip to content

Commit e5a38dd

Browse files
author
David Ungar
committed
Add removal test.
1 parent b076b9e commit e5a38dd

File tree

2 files changed

+248
-6
lines changed

2 files changed

+248
-6
lines changed

Sources/SwiftDriver/IncrementalCompilation/ModuleDependencyGraph.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -466,7 +466,7 @@ extension ModuleDependencyGraph {
466466
// MARK: - verification
467467
extension ModuleDependencyGraph {
468468
@discardableResult
469-
func verifyGraph() -> Bool {
469+
@_spi(Testing) public func verifyGraph() -> Bool {
470470
nodeFinder.verify()
471471
}
472472
}

Tests/SwiftDriverTests/IncrementalCompilationTests.swift

Lines changed: 247 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ final class IncrementalCompilationTests: XCTestCase {
4747
var masterSwiftDepsPath: AbsolutePath {
4848
derivedDataPath.appending(component: "\(module)-master.swiftdeps")
4949
}
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+
}
5056
var autolinkIncrementalExpectations: [String] {
5157
[
5258
"Incremental compilation: Queuing Extracting autolink information for module \(module)",
@@ -239,7 +245,7 @@ fileprivate extension Driver {
239245
}
240246
}
241247

242-
// MARK: - Actual incremental tests
248+
// MARK: - Simpler incremental tests
243249
extension IncrementalCompilationTests {
244250

245251
// FIXME: why does it fail on Linux in CI?
@@ -271,7 +277,6 @@ extension IncrementalCompilationTests {
271277
#endif
272278
}
273279

274-
275280
/// Ensure that the mod date of the input comes back exactly the same via the build-record.
276281
/// Otherwise the up-to-date calculation in `IncrementalCompilationState` will fail.
277282
func testBuildRecordDateAccuracy() throws {
@@ -313,6 +318,120 @@ extension IncrementalCompilationTests {
313318
}
314319
}
315320

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+
316435
// MARK: - Incremental test stages
317436
extension IncrementalCompilationTests {
318437
/// Setup the initial post-build state.
@@ -622,6 +741,95 @@ extension IncrementalCompilationTests {
622741
XCTAssert(graph.contains(sourceBasenameWithoutExt: newInput))
623742
XCTAssert(graph.contains(name: topLevelName))
624743
}
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+
}
625833
}
626834

627835
// MARK: - Incremental test perturbation helpers
@@ -632,6 +840,18 @@ extension IncrementalCompilationTests {
632840
try! localFileSystem.writeFileContents(path) { $0 <<< contents }
633841
}
634842

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+
635855
private func replace(contentsOf name: String, with replacement: String ) {
636856
print("*** replacing \(name) ***", to: &stderrStream); stderrStream.flush()
637857
let path = inputPath(basename: name)
@@ -648,10 +868,27 @@ extension IncrementalCompilationTests {
648868
$0 <<< contents
649869
}
650870
}
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+
}
651889
}
652890

653891
// MARK: - Graph inspection
654-
655892
fileprivate extension Driver {
656893
func moduleDependencyGraph() throws -> ModuleDependencyGraph {
657894
do {return try XCTUnwrap(incrementalCompilationState?.moduleDependencyGraph) }
@@ -660,6 +897,9 @@ fileprivate extension Driver {
660897
throw error
661898
}
662899
}
900+
func verifyNoGraph() {
901+
XCTAssertNil(incrementalCompilationState)
902+
}
663903
}
664904

665905
fileprivate extension ModuleDependencyGraph {
@@ -677,12 +917,14 @@ fileprivate extension ModuleDependencyGraph {
677917
}
678918
func ensureOmits(sourceBasenameWithoutExt target: String) {
679919
nodeFinder.forEachNode { node in
680-
XCTAssertFalse(node.contains(sourceBasenameWithoutExt: target))
920+
XCTAssertFalse(node.contains(sourceBasenameWithoutExt: target),
921+
"graph should omit source: \(target)")
681922
}
682923
}
683924
func ensureOmits(name: String) {
684925
nodeFinder.forEachNode { node in
685-
XCTAssertFalse(node.contains(name: name))
926+
XCTAssertFalse(node.contains(name: name),
927+
"graph should omit decl named: \(name)")
686928
}
687929
}
688930
}

0 commit comments

Comments
 (0)