Skip to content

Commit f2a4eec

Browse files
author
David Ungar
authored
Merge pull request #694 from davidungar/add-remove-2
2 parents 14fedb3 + 6919457 commit f2a4eec

File tree

5 files changed

+258
-6
lines changed

5 files changed

+258
-6
lines changed

Sources/SwiftDriver/IncrementalCompilation/IncrementalCompilationState.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,7 @@ extension IncrementalCompilationState {
500500
}
501501
}
502502
}
503+
503504
@_spi(Testing) public func writeDependencyGraph() throws {
504505
// If the cross-module build is not enabled, the status quo dictates we
505506
// not emit this file.
@@ -515,6 +516,12 @@ extension IncrementalCompilationState {
515516
on: self.driver.fileSystem,
516517
compilerVersion: recordInfo.actualSwiftVersion)
517518
}
519+
520+
@_spi(Testing) public static func removeDependencyGraphFile(_ driver: Driver) {
521+
if let path = driver.buildRecordInfo?.dependencyGraphPath {
522+
try? driver.fileSystem.removeFileTree(path)
523+
}
524+
}
518525
}
519526

520527
// MARK: - OutputFileMap

Sources/SwiftDriver/IncrementalCompilation/IncrementalDependencyAndInputSetup.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ extension IncrementalCompilationState {
5959
driver.diagnosticEngine
6060
).compute()
6161
else {
62+
Self.removeDependencyGraphFile(driver)
6263
return nil
6364
}
6465

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
}

Sources/SwiftDriver/Utilities/VirtualPath.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -707,6 +707,12 @@ extension TSCBasic.FileSystem {
707707
}
708708
}
709709

710+
func removeFileTree(_ path: VirtualPath) throws {
711+
try resolvingVirtualPath(path) { absolutePath in
712+
try self.removeFileTree(absolutePath)
713+
}
714+
}
715+
710716
func getFileInfo(_ path: VirtualPath) throws -> TSCBasic.FileInfo {
711717
try resolvingVirtualPath(path, apply: getFileInfo)
712718
}

Tests/SwiftDriverTests/IncrementalCompilationTests.swift

Lines changed: 243 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,116 @@ 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+
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+
316431
// MARK: - Incremental test stages
317432
extension IncrementalCompilationTests {
318433
/// Setup the initial post-build state.
@@ -622,6 +737,95 @@ extension IncrementalCompilationTests {
622737
XCTAssert(graph.contains(sourceBasenameWithoutExt: newInput))
623738
XCTAssert(graph.contains(name: topLevelName))
624739
}
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+
}
625829
}
626830

627831
// MARK: - Incremental test perturbation helpers
@@ -632,6 +836,18 @@ extension IncrementalCompilationTests {
632836
try! localFileSystem.writeFileContents(path) { $0 <<< contents }
633837
}
634838

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+
635851
private func replace(contentsOf name: String, with replacement: String ) {
636852
print("*** replacing \(name) ***", to: &stderrStream); stderrStream.flush()
637853
let path = inputPath(basename: name)
@@ -648,10 +864,27 @@ extension IncrementalCompilationTests {
648864
$0 <<< contents
649865
}
650866
}
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+
}
651885
}
652886

653887
// MARK: - Graph inspection
654-
655888
fileprivate extension Driver {
656889
func moduleDependencyGraph() throws -> ModuleDependencyGraph {
657890
do {return try XCTUnwrap(incrementalCompilationState?.moduleDependencyGraph) }
@@ -660,6 +893,9 @@ fileprivate extension Driver {
660893
throw error
661894
}
662895
}
896+
func verifyNoGraph() {
897+
XCTAssertNil(incrementalCompilationState)
898+
}
663899
}
664900

665901
fileprivate extension ModuleDependencyGraph {
@@ -677,12 +913,14 @@ fileprivate extension ModuleDependencyGraph {
677913
}
678914
func ensureOmits(sourceBasenameWithoutExt target: String) {
679915
nodeFinder.forEachNode { node in
680-
XCTAssertFalse(node.contains(sourceBasenameWithoutExt: target))
916+
XCTAssertFalse(node.contains(sourceBasenameWithoutExt: target),
917+
"graph should omit source: \(target)")
681918
}
682919
}
683920
func ensureOmits(name: String) {
684921
nodeFinder.forEachNode { node in
685-
XCTAssertFalse(node.contains(name: name))
922+
XCTAssertFalse(node.contains(name: name),
923+
"graph should omit decl named: \(name)")
686924
}
687925
}
688926
}

0 commit comments

Comments
 (0)