Skip to content

Commit 2ed483e

Browse files
committed
Allow unit tests to import and link any main modules of executables that are implemented in Swift. This uses a new Swift compiler flag to set the name of the entry point when emitting object code, and then uses linker flags to rename the main executable module's entry point back to _main again when actually linking the executable.
This is guarded by a tools version check, since packages written this way won't be testable on older toolchains.
1 parent 42bafb2 commit 2ed483e

File tree

10 files changed

+181
-31
lines changed

10 files changed

+181
-31
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// swift-tools-version:5.3
2+
import PackageDescription
3+
4+
let package = Package(
5+
name: "TestableExe",
6+
targets: [
7+
.target(
8+
name: "TestableExe1"
9+
),
10+
.target(
11+
name: "TestableExe2"
12+
),
13+
.target(
14+
name: "TestableExe3"
15+
),
16+
.testTarget(
17+
name: "TestableExeTests",
18+
dependencies: [
19+
"TestableExe1",
20+
"TestableExe2",
21+
"TestableExe3",
22+
]
23+
),
24+
]
25+
)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
public func GetGreeting1() -> String {
2+
return "Hello, world"
3+
}
4+
5+
print("\(GetGreeting1())!")
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
public func GetGreeting2() -> String {
2+
return "Hello, planet"
3+
}
4+
5+
print("\(GetGreeting2())!")
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
const char * GetGreeting3();
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#include <stdio.h>
2+
#include "include/TestableExe3.h"
3+
4+
const char * GetGreeting3() {
5+
return "Hello, universe";
6+
}
7+
8+
int main() {
9+
printf("%s!\n", GetGreeting3());
10+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import XCTest
2+
import TestableExe1
3+
import TestableExe2
4+
// import TestableExe3
5+
import class Foundation.Bundle
6+
7+
final class TestableExeTests: XCTestCase {
8+
func testExample() throws {
9+
// This is an example of a functional test case.
10+
// Use XCTAssert and related functions to verify your tests produce the correct
11+
// results.
12+
13+
print(GetGreeting1())
14+
XCTAssertEqual(GetGreeting1(), "Hello, world")
15+
print(GetGreeting2())
16+
XCTAssertEqual(GetGreeting2(), "Hello, planet")
17+
// XCTAssertEqual(String(cString: GetGreeting3()), "Hello, universe")
18+
19+
// Some of the APIs that we use below are available in macOS 10.13 and above.
20+
guard #available(macOS 10.13, *) else {
21+
return
22+
}
23+
24+
var execPath = productsDirectory.appendingPathComponent("TestableExe1")
25+
var process = Process()
26+
process.executableURL = execPath
27+
var pipe = Pipe()
28+
process.standardOutput = pipe
29+
try process.run()
30+
process.waitUntilExit()
31+
var data = pipe.fileHandleForReading.readDataToEndOfFile()
32+
var output = String(data: data, encoding: .utf8)
33+
XCTAssertEqual(output, "Hello, world!\n")
34+
35+
execPath = productsDirectory.appendingPathComponent("TestableExe2")
36+
process = Process()
37+
process.executableURL = execPath
38+
pipe = Pipe()
39+
process.standardOutput = pipe
40+
try process.run()
41+
process.waitUntilExit()
42+
data = pipe.fileHandleForReading.readDataToEndOfFile()
43+
output = String(data: data, encoding: .utf8)
44+
XCTAssertEqual(output, "Hello, planet!\n")
45+
46+
execPath = productsDirectory.appendingPathComponent("TestableExe3")
47+
process = Process()
48+
process.executableURL = execPath
49+
pipe = Pipe()
50+
process.standardOutput = pipe
51+
try process.run()
52+
process.waitUntilExit()
53+
data = pipe.fileHandleForReading.readDataToEndOfFile()
54+
output = String(data: data, encoding: .utf8)
55+
XCTAssertEqual(output, "Hello, universe!\n")
56+
}
57+
58+
/// Returns path to the built products directory.
59+
var productsDirectory: URL {
60+
#if os(macOS)
61+
for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") {
62+
return bundle.bundleURL.deletingLastPathComponent()
63+
}
64+
fatalError("couldn't find the products directory")
65+
#else
66+
return Bundle.main.bundleURL
67+
#endif
68+
}
69+
70+
static var allTests = [
71+
("testExample", testExample),
72+
]
73+
}

Sources/Build/BuildPlan.swift

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -510,7 +510,7 @@ public final class SwiftTargetBuildDescription {
510510

511511
/// The path to the swiftmodule file after compilation.
512512
var moduleOutputPath: AbsolutePath {
513-
let dirPath = (target.type == .executable) ? tempsPath : buildParameters.buildPath
513+
let dirPath = (target.type == .executable && toolsVersion < .vNext) ? tempsPath : buildParameters.buildPath
514514
return dirPath.appending(component: target.c99name + ".swiftmodule")
515515
}
516516

@@ -675,6 +675,14 @@ public final class SwiftTargetBuildDescription {
675675
args += buildParameters.sanitizers.compileSwiftFlags()
676676
args += ["-parseable-output"]
677677

678+
// If we're compiling the main module of an executable, we rename the `_main`
679+
// entry point to `_<modulename>_main`. This will allow tests to link against
680+
// them without conflicts. When we link the executable we will ask the linker
681+
// to rename the entry point symbol to just `_main` again.
682+
if target.type == .executable && !isTestTarget && toolsVersion >= .vNext {
683+
args += ["-Xfrontend", "-entry-point-function-name", "-Xfrontend", "\(target.c99name)_main"]
684+
}
685+
678686
// Only add the build path to the framework search path if there are binary frameworks to link against.
679687
if !libraryBinaryPaths.isEmpty {
680688
args += ["-F", buildParameters.buildPath.pathString]
@@ -1032,7 +1040,7 @@ public final class ProductBuildDescription {
10321040
return buildParameters.binaryPath(for: product)
10331041
}
10341042

1035-
/// The objects in this product.
1043+
/// All object files to link into this product.
10361044
///
10371045
// Computed during build planning.
10381046
public fileprivate(set) var objects = SortedArray<AbsolutePath>()
@@ -1152,6 +1160,23 @@ public final class ProductBuildDescription {
11521160
}
11531161
}
11541162
args += ["-emit-executable"]
1163+
1164+
// If we're linking an executable whose main module is implemented in Swift,
1165+
// we rename the `_<modulename>_main` entry point symbol to `_main` again.
1166+
// This is because executable modules implemented in Swift are compiled with
1167+
// a main symbol named that way to allow tests to link against it without
1168+
// conflicts. If we're using a linker that doesn't support symbol renaming,
1169+
// an alternate implementation could use a generated source file with a stub
1170+
// implementation of `_main` to call the renamed main symbol.
1171+
let execModule = product.executableModule
1172+
if execModule.underlyingTarget is SwiftTarget {
1173+
if buildParameters.triple.isDarwin() {
1174+
args += ["-Xlinker", "-alias", "-Xlinker", "_\(execModule.c99name)_main", "-Xlinker", "_main"]
1175+
}
1176+
else {
1177+
args += ["-Xlinker", "--defsym", "-Xlinker", "main=\(execModule.c99name)_main"]
1178+
}
1179+
}
11551180
case .plugin:
11561181
throw InternalError("unexpectedly asked to generate linker arguments for a plugin product")
11571182
}
@@ -1649,7 +1674,11 @@ public class BuildPlan {
16491674
switch target.type {
16501675
// Include executable and tests only if they're top level contents
16511676
// of the product. Otherwise they are just build time dependency.
1652-
case .executable, .test:
1677+
case .executable:
1678+
if product.targets.contains(target) || (product.type == .test && target.underlyingTarget is SwiftTarget) {
1679+
staticTargets.append(target)
1680+
}
1681+
case .test:
16531682
if product.targets.contains(target) {
16541683
staticTargets.append(target)
16551684
}

Tests/BuildTests/BuildPlanTests.swift

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
This source file is part of the Swift.org open source project
33

4-
Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
4+
Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors
55
Licensed under Apache License v2.0 with Runtime Library Exception
66

77
See http://swift.org/LICENSE.txt for license information
@@ -138,18 +138,20 @@ final class BuildPlanTests: XCTestCase {
138138
"/fake/path/to/swiftc", "-L", "/path/to/build/debug",
139139
"-o", "/path/to/build/debug/exe", "-module-name", "exe",
140140
"-emit-executable",
141+
"-Xlinker", "-alias", "-Xlinker", "_exe_main", "-Xlinker", "_main",
141142
"-Xlinker", "-rpath", "-Xlinker", "@loader_path",
142143
"@/path/to/build/debug/exe.product/Objects.LinkFileList",
143144
"-Xlinker", "-rpath", "-Xlinker", "/fake/path/lib/swift/macosx",
144145
"-target", "x86_64-apple-macosx10.10", "-Xlinker", "-add_ast_path",
145-
"-Xlinker", "/path/to/build/debug/exe.build/exe.swiftmodule", "-Xlinker", "-add_ast_path",
146+
"-Xlinker", "/path/to/build/debug/exe.swiftmodule", "-Xlinker", "-add_ast_path",
146147
"-Xlinker", "/path/to/build/debug/lib.swiftmodule",
147148
]
148149
#else
149150
let linkArguments = [
150151
"/fake/path/to/swiftc", "-L", "/path/to/build/debug",
151152
"-o", "/path/to/build/debug/exe", "-module-name", "exe",
152153
"-static-stdlib", "-emit-executable",
154+
"-Xlinker", "--defsym", "-Xlinker", "main=exe_main",
153155
"-Xlinker", "-rpath=$ORIGIN",
154156
"@/path/to/build/debug/exe.product/Objects.LinkFileList",
155157
"-target", defaultTargetTriple,
@@ -450,6 +452,7 @@ final class BuildPlanTests: XCTestCase {
450452
XCTAssertEqual(try result.buildProduct(for: "exe").linkArguments(), [
451453
"/fake/path/to/swiftc", "-g", "-L", "/path/to/build/release",
452454
"-o", "/path/to/build/release/exe", "-module-name", "exe", "-emit-executable",
455+
"-Xlinker", "-alias", "-Xlinker", "_exe_main", "-Xlinker", "_main",
453456
"-Xlinker", "-rpath", "-Xlinker", "@loader_path",
454457
"@/path/to/build/release/exe.product/Objects.LinkFileList",
455458
"-Xlinker", "-rpath", "-Xlinker", "/fake/path/lib/swift/macosx",
@@ -459,6 +462,7 @@ final class BuildPlanTests: XCTestCase {
459462
XCTAssertEqual(try result.buildProduct(for: "exe").linkArguments(), [
460463
"/fake/path/to/swiftc", "-g", "-L", "/path/to/build/release",
461464
"-o", "/path/to/build/release/exe", "-module-name", "exe", "-emit-executable",
465+
"-Xlinker", "--defsym", "-Xlinker", "main=exe_main",
462466
"-Xlinker", "-rpath=$ORIGIN",
463467
"@/path/to/build/release/exe.product/Objects.LinkFileList",
464468
"-target", defaultTargetTriple,
@@ -779,16 +783,18 @@ final class BuildPlanTests: XCTestCase {
779783
XCTAssertEqual(try result.buildProduct(for: "exe").linkArguments(), [
780784
"/fake/path/to/swiftc", "-L", "/path/to/build/debug",
781785
"-o", "/path/to/build/debug/exe", "-module-name", "exe", "-emit-executable",
786+
"-Xlinker", "-alias", "-Xlinker", "_exe_main", "-Xlinker", "_main",
782787
"-Xlinker", "-rpath", "-Xlinker", "@loader_path",
783788
"@/path/to/build/debug/exe.product/Objects.LinkFileList",
784789
"-Xlinker", "-rpath", "-Xlinker", "/fake/path/lib/swift/macosx",
785790
"-target", "x86_64-apple-macosx10.10",
786-
"-Xlinker", "-add_ast_path", "-Xlinker", "/path/to/build/debug/exe.build/exe.swiftmodule",
791+
"-Xlinker", "-add_ast_path", "-Xlinker", "/path/to/build/debug/exe.swiftmodule",
787792
])
788793
#else
789794
XCTAssertEqual(try result.buildProduct(for: "exe").linkArguments(), [
790795
"/fake/path/to/swiftc", "-L", "/path/to/build/debug",
791796
"-o", "/path/to/build/debug/exe", "-module-name", "exe", "-emit-executable",
797+
"-Xlinker", "--defsym", "-Xlinker", "main=exe_main",
792798
"-Xlinker", "-rpath=$ORIGIN",
793799
"@/path/to/build/debug/exe.product/Objects.LinkFileList",
794800
"-target", defaultTargetTriple,
@@ -989,16 +995,18 @@ final class BuildPlanTests: XCTestCase {
989995
XCTAssertEqual(try result.buildProduct(for: "exe").linkArguments(), [
990996
"/fake/path/to/swiftc", "-L", "/path/to/build/debug",
991997
"-o", "/path/to/build/debug/exe", "-module-name", "exe", "-emit-executable",
998+
"-Xlinker", "-alias", "-Xlinker", "_exe_main", "-Xlinker", "_main",
992999
"-Xlinker", "-rpath", "-Xlinker", "@loader_path",
9931000
"@/path/to/build/debug/exe.product/Objects.LinkFileList",
9941001
"-Xlinker", "-rpath", "-Xlinker", "/fake/path/lib/swift/macosx",
9951002
"-target", "x86_64-apple-macosx10.10",
996-
"-Xlinker", "-add_ast_path", "-Xlinker", "/path/to/build/debug/exe.build/exe.swiftmodule",
1003+
"-Xlinker", "-add_ast_path", "-Xlinker", "/path/to/build/debug/exe.swiftmodule",
9971004
])
9981005
#else
9991006
XCTAssertEqual(try result.buildProduct(for: "exe").linkArguments(), [
10001007
"/fake/path/to/swiftc", "-L", "/path/to/build/debug",
10011008
"-o", "/path/to/build/debug/exe", "-module-name", "exe", "-emit-executable",
1009+
"-Xlinker", "--defsym", "-Xlinker", "main=exe_main",
10021010
"-Xlinker", "-rpath=$ORIGIN",
10031011
"@/path/to/build/debug/exe.product/Objects.LinkFileList",
10041012
"-target", defaultTargetTriple,
@@ -1086,12 +1094,13 @@ final class BuildPlanTests: XCTestCase {
10861094
#if os(macOS)
10871095
XCTAssertEqual(fooLinkArgs, [
10881096
"/fake/path/to/swiftc", "-L", "/path/to/build/debug",
1089-
"-o", "/path/to/build/debug/Foo", "-module-name", "Foo", "-lBar-Baz", "-emit-executable",
1090-
"-Xlinker", "-rpath", "-Xlinker", "@loader_path",
1097+
"-o", "/path/to/build/debug/Foo", "-module-name", "Foo", "-lBar-Baz", "-emit-executable",
1098+
"-Xlinker", "-alias", "-Xlinker", "_Foo_main", "-Xlinker", "_main",
1099+
"-Xlinker", "-rpath", "-Xlinker", "@loader_path",
10911100
"@/path/to/build/debug/Foo.product/Objects.LinkFileList",
10921101
"-Xlinker", "-rpath", "-Xlinker", "/fake/path/lib/swift/macosx",
10931102
"-target", "x86_64-apple-macosx10.10",
1094-
"-Xlinker", "-add_ast_path", "-Xlinker", "/path/to/build/debug/Foo.build/Foo.swiftmodule"
1103+
"-Xlinker", "-add_ast_path", "-Xlinker", "/path/to/build/debug/Foo.swiftmodule"
10951104
])
10961105

10971106
XCTAssertEqual(barLinkArgs, [
@@ -1108,6 +1117,7 @@ final class BuildPlanTests: XCTestCase {
11081117
XCTAssertEqual(fooLinkArgs, [
11091118
"/fake/path/to/swiftc", "-L", "/path/to/build/debug",
11101119
"-o", "/path/to/build/debug/Foo", "-module-name", "Foo", "-lBar-Baz", "-emit-executable",
1120+
"-Xlinker", "--defsym", "-Xlinker", "main=Foo_main",
11111121
"-Xlinker", "-rpath=$ORIGIN",
11121122
"@/path/to/build/debug/Foo.product/Objects.LinkFileList",
11131123
"-target", defaultTargetTriple,
@@ -1615,6 +1625,7 @@ final class BuildPlanTests: XCTestCase {
16151625
"/fake/path/to/swiftc",
16161626
"-L", "/path/to/build/debug", "-o", "/path/to/build/debug/exe.exe",
16171627
"-module-name", "exe", "-emit-executable",
1628+
"-Xlinker", "--defsym", "-Xlinker", "main=exe_main",
16181629
"@/path/to/build/debug/exe.product/Objects.LinkFileList",
16191630
"-target", "x86_64-unknown-windows-msvc",
16201631
])
@@ -1692,6 +1703,7 @@ final class BuildPlanTests: XCTestCase {
16921703
"/fake/path/to/swiftc", "-L", "/path/to/build/debug",
16931704
"-o", "/path/to/build/debug/app.wasm",
16941705
"-module-name", "app", "-static-stdlib", "-emit-executable",
1706+
"-Xlinker", "--defsym", "-Xlinker", "main=app_main",
16951707
"@/path/to/build/debug/app.product/Objects.LinkFileList",
16961708
"-target", "wasm32-unknown-wasi"
16971709
]
@@ -2340,10 +2352,10 @@ final class BuildPlanTests: XCTestCase {
23402352
XCTAssertMatch(contents, .contains("""
23412353
"/path/to/build/debug/exe.build/exe.swiftmodule.o":
23422354
tool: shell
2343-
inputs: ["/path/to/build/debug/exe.build/exe.swiftmodule"]
2355+
inputs: ["/path/to/build/debug/exe.swiftmodule"]
23442356
outputs: ["/path/to/build/debug/exe.build/exe.swiftmodule.o"]
23452357
description: "Wrapping AST for exe for debugging"
2346-
args: ["/fake/path/to/swiftc","-modulewrap","/path/to/build/debug/exe.build/exe.swiftmodule","-o","/path/to/build/debug/exe.build/exe.swiftmodule.o","-target","x86_64-unknown-linux-gnu"]
2358+
args: ["/fake/path/to/swiftc","-modulewrap","/path/to/build/debug/exe.swiftmodule","-o","/path/to/build/debug/exe.build/exe.swiftmodule.o","-target","x86_64-unknown-linux-gnu"]
23472359
"""))
23482360
XCTAssertMatch(contents, .contains("""
23492361
"/path/to/build/debug/lib.build/lib.swiftmodule.o":

Tests/FunctionalTests/MiscellaneousTests.swift

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
This source file is part of the Swift.org open source project
33

4-
Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
4+
Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors
55
Licensed under Apache License v2.0 with Runtime Library Exception
66

77
See http://swift.org/LICENSE.txt for license information
@@ -578,25 +578,15 @@ class MiscellaneousTestCase: XCTestCase {
578578
#endif
579579
}
580580

581-
func testErrorMessageWhenTestLinksExecutable() {
581+
func testTestsCanLinkAgainstExecutable() {
582582
fixture(name: "Miscellaneous/ExeTest") { prefix in
583583
do {
584-
try executeSwiftTest(prefix)
585-
XCTFail()
586-
} catch SwiftPMProductError.executionFailure(let error, let output, let stderr) {
587-
XCTAssertMatch(stderr + output, .contains("Compiling Exe main.swift"))
588-
XCTAssertMatch(stderr + output, .contains("Compiling ExeTests ExeTests.swift"))
589-
XCTAssertMatch(stderr + output, .regex("error: no such module 'Exe'"))
590-
XCTAssertMatch(stderr + output, .regex("note: module 'Exe' is the main module of an executable, and cannot be imported by tests and other targets"))
591-
592-
if case ProcessResult.Error.nonZeroExit(let result) = error {
593-
// if our code crashes we'll get an exit code of 256
594-
XCTAssertEqual(result.exitStatus, .terminated(code: 1))
595-
} else {
596-
XCTFail("\(stderr + output)")
597-
}
584+
let (stdout, _) = try executeSwiftTest(prefix)
585+
XCTAssertMatch(stdout, .contains("Compiling Exe main.swift"))
586+
XCTAssertMatch(stdout, .contains("Compiling ExeTests ExeTests.swift"))
587+
XCTAssertMatch(stdout, .contains("Linking ExeTestPackageTests"))
598588
} catch {
599-
XCTFail()
589+
XCTFail("\(error)")
600590
}
601591
}
602592
}

Tests/WorkspaceTests/InitTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
This source file is part of the Swift.org open source project
33

4-
Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
4+
Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors
55
Licensed under Apache License v2.0 with Runtime Library Exception
66

77
See http://swift.org/LICENSE.txt for license information
@@ -90,7 +90,7 @@ class InitTests: XCTestCase {
9090
let triple = Resources.default.toolchain.triple
9191
let binPath = path.appending(components: ".build", triple.tripleString, "debug")
9292
XCTAssertFileExists(binPath.appending(component: "Foo"))
93-
XCTAssertFileExists(binPath.appending(components: "Foo.build", "Foo.swiftmodule"))
93+
XCTAssertFileExists(binPath.appending(components: "Foo.swiftmodule"))
9494
}
9595
}
9696

0 commit comments

Comments
 (0)