Skip to content

Commit 4343bed

Browse files
authored
Merge pull request #541 from CodaFi/i-was-framed
Some Improvements to Cross-Module Incremental Build Testing
2 parents 5396beb + b2e02cc commit 4343bed

File tree

6 files changed

+347
-51
lines changed

6 files changed

+347
-51
lines changed
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
//===---------------- Antisymmetry.swift - Swift Testing ------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import XCTest
14+
import IncrementalTestFramework
15+
16+
// This test establishes an "antisymmetric" chain of modules that import one
17+
// another and ensures that rebuilds propagate in one direction.
18+
//
19+
// Module B Module A
20+
// -------- -> --------
21+
// ^ x
22+
// | |
23+
// ----XXX-----
24+
class AntisymmetryTest: XCTestCase {
25+
private let defAdditions = [
26+
"b-add-struct",
27+
"b-add-class",
28+
"b-add-protocol",
29+
"b-add-extension",
30+
]
31+
32+
private let useAdditions = [
33+
"use-struct",
34+
"use-class",
35+
"use-protocol",
36+
"use-extension",
37+
]
38+
39+
func testAntisymmetricTopLevelDefs() throws {
40+
try IncrementalTest.perform([
41+
// The baseline step is special, we want everything to get built first.
42+
Step(adding: defAdditions,
43+
building: [ .B, .A ],
44+
.expecting(.init(always: [ .main, .B ])))
45+
] + defAdditions.dropFirst().indices.map { idx in
46+
// Make sure the addition of defs without users only causes cascading
47+
// rebuilds when incremental imports are disabled.
48+
Step(adding: Array(defAdditions[0..<idx]),
49+
building: [ .B, .A ],
50+
.expecting(.init(always: [ .B ], andWhenDisabled: [ .main ])))
51+
})
52+
}
53+
54+
func testAntisymmetricTopLevelUses() throws {
55+
try IncrementalTest.perform([
56+
// The baseline step is special, we want everything to get built first.
57+
Step(adding: defAdditions,
58+
building: [ .B, .A ],
59+
.expecting(.init(always: [ .main, .B ])))
60+
] + useAdditions.indices.dropFirst().map { idx in
61+
// Make sure the addition of uses causes only users to recompile.
62+
Step(adding: defAdditions + Array(useAdditions[0..<idx]),
63+
building: [ .B, .A ],
64+
.expecting(.init(always: [ .main ])))
65+
})
66+
}
67+
}
68+
69+
fileprivate extension Module {
70+
static var A = Module(named: "A", containing: [
71+
.main,
72+
], importing: [
73+
.B,
74+
], producing: .executable)
75+
76+
static var B = Module(named: "B", containing: [
77+
.B,
78+
], producing: .library)
79+
}
80+
81+
fileprivate extension Source {
82+
static var main: Source {
83+
Self(
84+
named: "main",
85+
containing: """
86+
import B
87+
88+
//# use-struct _ = BStruct()
89+
//# use-class _ = BClass()
90+
//# use-protocol extension BStruct: BProtocol { public func foo(parameter: Int = 0) {} }
91+
//# use-extension BStruct().foo()
92+
""")
93+
}
94+
}
95+
96+
fileprivate extension Source {
97+
static var B: Source {
98+
Self(
99+
named: "B",
100+
containing: """
101+
//# b-add-struct public struct BStruct { public init() {} }
102+
//# b-add-class public class BClass { public init() {} }
103+
//# b-add-protocol public protocol BProtocol {}
104+
//# b-add-extension extension BStruct { public func foo() {} }
105+
""")
106+
}
107+
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
//===---------------- Transitivity.swift - Swift Testing ------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import XCTest
14+
import IncrementalTestFramework
15+
16+
// This test establishes a "transitive" chain of modules that import one another
17+
// and ensures that a cross-module incremental build rebuilds all modules
18+
// involved in the chain.
19+
//
20+
// Module C Module B Module A
21+
// -------- -> -------- -> --------
22+
// | ^
23+
// | |
24+
// -------------------------
25+
class TransitivityTest: XCTestCase {
26+
func testTransitiveTopLevelUses() throws {
27+
try IncrementalTest.perform([
28+
// Build a baseline
29+
Step(adding: "transitive-baseline",
30+
building: [.C, .B, .A],
31+
.expecting([.C, .B, .A].allSourcesToCompile)),
32+
// Swap in a new default argument: B needs to rebuild `fromB` and
33+
// relink against fromC, but A doesn't import `C` so only non-incremental
34+
// imports rebuilds it.
35+
Step(adding: "transitive-add-default",
36+
building: [.C, .B, .A],
37+
.expecting(.init(always: [.C, .B], andWhenDisabled: [.A]))),
38+
// Now change C back to the old baseline. We edit A in the process to
39+
// introduce a dependency on C, so it needs to rebuild.
40+
Step(adding: "transitive-baseline", "transitive-add-use-in-A",
41+
building: [.C, .B, .A],
42+
.expecting(.init(always: [.C, .B, .A]))),
43+
// Same as before - the addition of a default argument requires B rebuild,
44+
// but A doesn't use anything from C, so it doesn't rebuild unless
45+
// incremental imports are disabled.
46+
Step(adding: "transitive-add-default", "transitive-add-use-in-A",
47+
building: [.C, .B, .A],
48+
.expecting(.init(always: [.C, .B], andWhenDisabled: [.A]))),
49+
])
50+
}
51+
52+
func testTransitiveStructMember() throws {
53+
try IncrementalTest.perform([
54+
// Establish the baseline build
55+
Step(adding: "transitive-baseline",
56+
building: [.C, .B, .A],
57+
.expecting(.init(always: [ .A, .B, .C ]))),
58+
// Add the def of a struct to C, which B imports and has a use of so
59+
// B rebuilds but A does not unless incremental imports are disabled.
60+
Step(adding: "transitive-baseline", "transitive-struct-def-in-C",
61+
building: [.C, .B, .A],
62+
.expecting(.init(always: [ .B, .C ], andWhenDisabled: [ .A ]))),
63+
// Now add a use in B, make sure C doesn't rebuild.
64+
Step(adding: "transitive-baseline", "transitive-struct-def-in-C", "transitive-struct-def-in-B",
65+
building: [.C, .B, .A],
66+
.expecting(.init(always: [ .B, ], andWhenDisabled: [ .A ]))),
67+
// Now add a use in A and make sure only A rebuilds.
68+
Step(adding: "transitive-baseline", "transitive-struct-def-in-C", "transitive-struct-def-in-B", "transitive-struct-def-in-A",
69+
building: [.C, .B, .A],
70+
.expecting(.init(always: [ .A ]))),
71+
// Finally, add a member to a struct in C, which influences the layout of
72+
// the struct in B, which influences the layout of the struct in A.
73+
// Everything rebuilds!
74+
Step(adding: "transitive-baseline", "transitive-struct-add-member-in-C", "transitive-struct-def-in-B", "transitive-struct-def-in-A",
75+
building: [.C, .B, .A],
76+
.expecting(.init(always: [ .A, .B, .C ]))),
77+
])
78+
}
79+
}
80+
81+
fileprivate extension Module {
82+
static var A = Module(named: "A", containing: [
83+
.A,
84+
], importing: [
85+
.B, .C,
86+
], producing: .executable)
87+
88+
static var B = Module(named: "B", containing: [
89+
.B,
90+
], importing: [
91+
.C
92+
], producing: .library)
93+
94+
static var C = Module(named: "C", containing: [
95+
.C,
96+
], producing: .library)
97+
}
98+
99+
fileprivate extension Source {
100+
static var A: Source {
101+
Self(
102+
named: "A",
103+
containing: """
104+
import B
105+
106+
//# transitive-add-use-in-A import C
107+
//# transitive-add-use-in-A public func fromA() {
108+
//# transitive-add-use-in-A return fromB()
109+
//# transitive-add-use-in-A }
110+
111+
//# transitive-struct-def-in-A import C
112+
//# transitive-struct-def-in-A struct AStruct { var b: BStruct }
113+
""")
114+
}
115+
}
116+
117+
fileprivate extension Source {
118+
static var B: Source {
119+
Self(
120+
named: "B",
121+
containing: """
122+
import C
123+
124+
public func fromB() {
125+
return fromC()
126+
}
127+
128+
//# transitive-struct-def-in-B public struct BStruct { var c: CStruct }
129+
""")
130+
}
131+
}
132+
133+
fileprivate extension Source {
134+
static var C: Source {
135+
Self(
136+
named: "C",
137+
containing: """
138+
//# transitive-baseline public func fromC() {}
139+
//# transitive-add-default public func fromC(parameter: Int = 0) {}
140+
141+
//# transitive-struct-def-in-C public struct CStruct { }
142+
//# transitive-struct-add-member-in-C public struct CStruct { var x: Int }
143+
""")
144+
}
145+
}

Tests/IncrementalTestFramework/Context.swift

Lines changed: 71 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -42,47 +42,90 @@ struct Context: CustomStringConvertible {
4242
file: file, line: line)
4343
}
4444

45-
/// Each module has its own directory under the root
46-
private func modulePath(for module: Module) -> AbsolutePath {
47-
rootDir.appending(component: module.name)
48-
}
49-
func derivedDataPath(for module: Module) -> AbsolutePath {
50-
modulePath(for: module).appending(component: "\(module.name)DD")
51-
}
52-
func sourceDir(for module: Module) -> AbsolutePath {
53-
modulePath(for: module)
45+
var description: String {
46+
"Incremental imports \(incrementalImports)"
5447
}
55-
func swiftFilePath(for source: Source, in module: Module) -> AbsolutePath {
56-
sourceDir(for: module).appending(component: "\(source.name).swift")
48+
49+
func failMessage(_ step: Step) -> String {
50+
"\(description), in step \(stepIndex), \(step.whatIsBuilt)"
5751
}
58-
func objFilePath(for source: Source, in module: Module) -> AbsolutePath {
59-
derivedDataPath(for: module).appending(component: "\(source.name).o")
52+
53+
func fail(_ msg: String, _ step: Step) {
54+
XCTFail("\(msg) \(failMessage(step))")
6055
}
61-
func allObjFilePaths(in module: Module) -> [AbsolutePath] {
62-
module.sources.map {objFilePath(for: $0, in: module)}
56+
}
57+
58+
// MARK: Paths
59+
60+
extension Context {
61+
/// Computes the directory containing the given module's build products.
62+
///
63+
/// - Parameter module: The module.
64+
/// - Returns: An absolute path to the build root - relative to the root
65+
/// directory of this test context.
66+
func buildRoot(for module: Module) -> AbsolutePath {
67+
self.rootDir.appending(component: "\(module.name)-buildroot")
6368
}
64-
func allImportedObjFilePaths(in module: Module) -> [AbsolutePath] {
65-
module.imports.flatMap(allObjFilePaths(in:))
69+
70+
/// Computes the directory containing the given module's source files.
71+
///
72+
/// - Parameter module: The module.
73+
/// - Returns: An absolute path to the build root - relative to the root
74+
/// directory of this test context.
75+
func sourceRoot(for module: Module) -> AbsolutePath {
76+
self.rootDir.appending(component: "\(module.name)-srcroot")
6677
}
78+
79+
/// Computes the path to the output file map for the given module.
80+
///
81+
/// - Parameter module: The module.
82+
/// - Returns: An absolute path to the output file map - relative to the root
83+
/// directory of this test context.
6784
func outputFileMapPath(for module: Module) -> AbsolutePath {
68-
derivedDataPath(for: module).appending(component: "OFM.json")
85+
self.buildRoot(for: module).appending(component: "OFM")
6986
}
87+
88+
/// Computes the path to the `.swiftmodule` file for the given module.
89+
///
90+
/// - Parameter module: The module.
91+
/// - Returns: An absolute path to the swiftmodule file - relative to the root
92+
/// directory of this test context.
7093
func swiftmodulePath(for module: Module) -> AbsolutePath {
71-
derivedDataPath(for: module).appending(component: "\(module.name).swiftmodule")
72-
}
73-
func executablePath(for module: Module) -> AbsolutePath {
74-
derivedDataPath(for: module).appending(component: "a.out")
94+
self.buildRoot(for: module).appending(component: "\(module.name).swiftmodule")
7595
}
7696

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

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

85-
func fail(_ msg: String, _ step: Step) {
86-
XCTFail("\(msg) \(failMessage(step))")
117+
/// Computes the path to the executable file for the given module.
118+
///
119+
/// - Parameter module: The module.
120+
/// - Returns: An absolute path to the executable file - relative to the root
121+
/// directory of this test context.
122+
func executablePath(for module: Module) -> AbsolutePath {
123+
#if os(Windows)
124+
return self.buildRoot(for: module).appending(component: "a.exe")
125+
#else
126+
return self.buildRoot(for: module).appending(component: "a.out")
127+
#endif
87128
}
88129
}
130+
131+

Tests/IncrementalTestFramework/IncrementalTest.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,12 @@ public struct IncrementalTest {
7474
}
7575
private func performSteps() throws {
7676
for (index, step) in steps.enumerated() {
77-
if !context.verbose {
77+
if context.verbose {
7878
print("\(index)", terminator: " ")
7979
}
8080
try step.perform(stepIndex: index, in: context)
8181
}
82-
if !context.verbose {
82+
if context.verbose {
8383
print("")
8484
}
8585
}

0 commit comments

Comments
 (0)