Skip to content

Some Improvements to Cross-Module Incremental Build Testing #541

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Mar 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions Tests/IncrementalImportTests/Antisymmetry.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
//===---------------- Antisymmetry.swift - Swift Testing ------------------===//
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This whole file is beautiful! Well-done!! I wonder if, in the future, we'll want to add other sorts of uses and definitions, and/or test removals as well?

//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import XCTest
import IncrementalTestFramework

// This test establishes an "antisymmetric" chain of modules that import one
// another and ensures that rebuilds propagate in one direction.
//
// Module B Module A
// -------- -> --------
// ^ x
// | |
// ----XXX-----
class AntisymmetryTest: XCTestCase {
private let defAdditions = [
"b-add-struct",
"b-add-class",
"b-add-protocol",
"b-add-extension",
]

private let useAdditions = [
"use-struct",
"use-class",
"use-protocol",
"use-extension",
]

func testAntisymmetricTopLevelDefs() throws {
try IncrementalTest.perform([
// The baseline step is special, we want everything to get built first.
Step(adding: defAdditions,
building: [ .B, .A ],
.expecting(.init(always: [ .main, .B ])))
] + defAdditions.dropFirst().indices.map { idx in
// Make sure the addition of defs without users only causes cascading
// rebuilds when incremental imports are disabled.
Step(adding: Array(defAdditions[0..<idx]),
building: [ .B, .A ],
.expecting(.init(always: [ .B ], andWhenDisabled: [ .main ])))
})
}

func testAntisymmetricTopLevelUses() throws {
try IncrementalTest.perform([
// The baseline step is special, we want everything to get built first.
Step(adding: defAdditions,
building: [ .B, .A ],
.expecting(.init(always: [ .main, .B ])))
] + useAdditions.indices.dropFirst().map { idx in
// Make sure the addition of uses causes only users to recompile.
Step(adding: defAdditions + Array(useAdditions[0..<idx]),
building: [ .B, .A ],
.expecting(.init(always: [ .main ])))
})
}
}

fileprivate extension Module {
static var A = Module(named: "A", containing: [
.main,
], importing: [
.B,
], producing: .executable)

static var B = Module(named: "B", containing: [
.B,
], producing: .library)
}

fileprivate extension Source {
static var main: Source {
Self(
named: "main",
containing: """
import B

//# use-struct _ = BStruct()
//# use-class _ = BClass()
//# use-protocol extension BStruct: BProtocol { public func foo(parameter: Int = 0) {} }
//# use-extension BStruct().foo()
""")
}
}

fileprivate extension Source {
static var B: Source {
Self(
named: "B",
containing: """
//# b-add-struct public struct BStruct { public init() {} }
//# b-add-class public class BClass { public init() {} }
//# b-add-protocol public protocol BProtocol {}
//# b-add-extension extension BStruct { public func foo() {} }
""")
}
}
145 changes: 145 additions & 0 deletions Tests/IncrementalImportTests/Transitivity.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
//===---------------- Transitivity.swift - Swift Testing ------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import XCTest
import IncrementalTestFramework

// This test establishes a "transitive" chain of modules that import one another
// and ensures that a cross-module incremental build rebuilds all modules
// involved in the chain.
//
// Module C Module B Module A
// -------- -> -------- -> --------
// | ^
// | |
// -------------------------
class TransitivityTest: XCTestCase {
func testTransitiveTopLevelUses() throws {
try IncrementalTest.perform([
// Build a baseline
Step(adding: "transitive-baseline",
building: [.C, .B, .A],
.expecting([.C, .B, .A].allSourcesToCompile)),
// Swap in a new default argument: B needs to rebuild `fromB` and
// relink against fromC, but A doesn't import `C` so only non-incremental
// imports rebuilds it.
Step(adding: "transitive-add-default",
building: [.C, .B, .A],
.expecting(.init(always: [.C, .B], andWhenDisabled: [.A]))),
// Now change C back to the old baseline. We edit A in the process to
// introduce a dependency on C, so it needs to rebuild.
Step(adding: "transitive-baseline", "transitive-add-use-in-A",
building: [.C, .B, .A],
.expecting(.init(always: [.C, .B, .A]))),
// Same as before - the addition of a default argument requires B rebuild,
// but A doesn't use anything from C, so it doesn't rebuild unless
// incremental imports are disabled.
Step(adding: "transitive-add-default", "transitive-add-use-in-A",
building: [.C, .B, .A],
.expecting(.init(always: [.C, .B], andWhenDisabled: [.A]))),
])
}

func testTransitiveStructMember() throws {
try IncrementalTest.perform([
// Establish the baseline build
Step(adding: "transitive-baseline",
building: [.C, .B, .A],
.expecting(.init(always: [ .A, .B, .C ]))),
// Add the def of a struct to C, which B imports and has a use of so
// B rebuilds but A does not unless incremental imports are disabled.
Step(adding: "transitive-baseline", "transitive-struct-def-in-C",
building: [.C, .B, .A],
.expecting(.init(always: [ .B, .C ], andWhenDisabled: [ .A ]))),
// Now add a use in B, make sure C doesn't rebuild.
Step(adding: "transitive-baseline", "transitive-struct-def-in-C", "transitive-struct-def-in-B",
building: [.C, .B, .A],
.expecting(.init(always: [ .B, ], andWhenDisabled: [ .A ]))),
// Now add a use in A and make sure only A rebuilds.
Step(adding: "transitive-baseline", "transitive-struct-def-in-C", "transitive-struct-def-in-B", "transitive-struct-def-in-A",
building: [.C, .B, .A],
.expecting(.init(always: [ .A ]))),
// Finally, add a member to a struct in C, which influences the layout of
// the struct in B, which influences the layout of the struct in A.
// Everything rebuilds!
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great to see this test!

Step(adding: "transitive-baseline", "transitive-struct-add-member-in-C", "transitive-struct-def-in-B", "transitive-struct-def-in-A",
building: [.C, .B, .A],
.expecting(.init(always: [ .A, .B, .C ]))),
])
}
}

fileprivate extension Module {
static var A = Module(named: "A", containing: [
.A,
], importing: [
.B, .C,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I read this--haven't absorbed the whole thing yet--I'm wondering why A also imports C? Or would that be a different test we might need?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm... if the import is optional, maybe there should be different Module A objects, one with- and another without the import.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All imports does is configure search paths, right? I'm not worried about having to state this conditional dependency edge all the time.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a slightly tighter test without the imports because then it relies a bit less on our assumptions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'll also be noisier because the change in search paths will force us to discard the incremental build state. We need separate tests for that case if we don't have them already.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent point! Thank you for thinking of it.

], producing: .executable)

static var B = Module(named: "B", containing: [
.B,
], importing: [
.C
], producing: .library)

static var C = Module(named: "C", containing: [
.C,
], producing: .library)
}

fileprivate extension Source {
static var A: Source {
Self(
named: "A",
containing: """
import B

//# transitive-add-use-in-A import C
//# transitive-add-use-in-A public func fromA() {
//# transitive-add-use-in-A return fromB()
//# transitive-add-use-in-A }

//# transitive-struct-def-in-A import C
//# transitive-struct-def-in-A struct AStruct { var b: BStruct }
""")
}
}

fileprivate extension Source {
static var B: Source {
Self(
named: "B",
containing: """
import C

public func fromB() {
return fromC()
}

//# transitive-struct-def-in-B public struct BStruct { var c: CStruct }
""")
}
}

fileprivate extension Source {
static var C: Source {
Self(
named: "C",
containing: """
//# transitive-baseline public func fromC() {}
//# transitive-add-default public func fromC(parameter: Int = 0) {}

//# transitive-struct-def-in-C public struct CStruct { }
//# transitive-struct-add-member-in-C public struct CStruct { var x: Int }
""")
}
}
99 changes: 71 additions & 28 deletions Tests/IncrementalTestFramework/Context.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,47 +42,90 @@ struct Context: CustomStringConvertible {
file: file, line: line)
}

/// Each module has its own directory under the root
private func modulePath(for module: Module) -> AbsolutePath {
rootDir.appending(component: module.name)
}
func derivedDataPath(for module: Module) -> AbsolutePath {
modulePath(for: module).appending(component: "\(module.name)DD")
}
func sourceDir(for module: Module) -> AbsolutePath {
modulePath(for: module)
var description: String {
"Incremental imports \(incrementalImports)"
}
func swiftFilePath(for source: Source, in module: Module) -> AbsolutePath {
sourceDir(for: module).appending(component: "\(source.name).swift")

func failMessage(_ step: Step) -> String {
"\(description), in step \(stepIndex), \(step.whatIsBuilt)"
}
func objFilePath(for source: Source, in module: Module) -> AbsolutePath {
derivedDataPath(for: module).appending(component: "\(source.name).o")

func fail(_ msg: String, _ step: Step) {
XCTFail("\(msg) \(failMessage(step))")
}
func allObjFilePaths(in module: Module) -> [AbsolutePath] {
module.sources.map {objFilePath(for: $0, in: module)}
}

// MARK: Paths

extension Context {
/// Computes the directory containing the given module's build products.
///
/// - Parameter module: The module.
/// - Returns: An absolute path to the build root - relative to the root
/// directory of this test context.
func buildRoot(for module: Module) -> AbsolutePath {
self.rootDir.appending(component: "\(module.name)-buildroot")
}
func allImportedObjFilePaths(in module: Module) -> [AbsolutePath] {
module.imports.flatMap(allObjFilePaths(in:))

/// Computes the directory containing the given module's source files.
///
/// - Parameter module: The module.
/// - Returns: An absolute path to the build root - relative to the root
/// directory of this test context.
func sourceRoot(for module: Module) -> AbsolutePath {
self.rootDir.appending(component: "\(module.name)-srcroot")
}

/// Computes the path to the output file map for the given module.
///
/// - Parameter module: The module.
/// - Returns: An absolute path to the output file map - relative to the root
/// directory of this test context.
func outputFileMapPath(for module: Module) -> AbsolutePath {
derivedDataPath(for: module).appending(component: "OFM.json")
self.buildRoot(for: module).appending(component: "OFM")
}

/// Computes the path to the `.swiftmodule` file for the given module.
///
/// - Parameter module: The module.
/// - Returns: An absolute path to the swiftmodule file - relative to the root
/// directory of this test context.
func swiftmodulePath(for module: Module) -> AbsolutePath {
derivedDataPath(for: module).appending(component: "\(module.name).swiftmodule")
}
func executablePath(for module: Module) -> AbsolutePath {
derivedDataPath(for: module).appending(component: "a.out")
self.buildRoot(for: module).appending(component: "\(module.name).swiftmodule")
}

var description: String {
"Incremental imports \(incrementalImports)"
/// Computes the path to the `.swift` file for the given module.
///
/// - Parameter source: The name of the swift file.
/// - Parameter module: The module.
/// - Returns: An absolute path to the swift file - relative to the root
/// directory of this test context.
func swiftFilePath(for source: Source, in module: Module) -> AbsolutePath {
self.sourceRoot(for: module).appending(component: "\(source.name).swift")
}

func failMessage(_ step: Step) -> String {
"\(description), in step \(stepIndex), \(step.whatIsBuilt)"
/// Computes the path to the `.o` file for the given module.
///
/// - Parameter source: The name of the swift file.
/// - Parameter module: The module.
/// - Returns: An absolute path to the object file - relative to the root
/// directory of this test context.
func objectFilePath(for source: Source, in module: Module) -> AbsolutePath {
self.buildRoot(for: module).appending(component: "\(source.name).o")
}

func fail(_ msg: String, _ step: Step) {
XCTFail("\(msg) \(failMessage(step))")
/// Computes the path to the executable file for the given module.
///
/// - Parameter module: The module.
/// - Returns: An absolute path to the executable file - relative to the root
/// directory of this test context.
func executablePath(for module: Module) -> AbsolutePath {
#if os(Windows)
return self.buildRoot(for: module).appending(component: "a.exe")
#else
return self.buildRoot(for: module).appending(component: "a.out")
#endif
}
}


4 changes: 2 additions & 2 deletions Tests/IncrementalTestFramework/IncrementalTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,12 @@ public struct IncrementalTest {
}
private func performSteps() throws {
for (index, step) in steps.enumerated() {
if !context.verbose {
if context.verbose {
print("\(index)", terminator: " ")
}
try step.perform(stepIndex: index, in: context)
}
if !context.verbose {
if context.verbose {
print("")
}
}
Expand Down
Loading