Skip to content

Commit

Permalink
Allow unit tests to import and link any main modules of executables t…
Browse files Browse the repository at this point in the history
…hat 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 should possibly be guarded by a tools version check, since packages written this way won't be testable on older toolchains.
  • Loading branch information
abertelrud committed Mar 1, 2021
1 parent f73c0c7 commit 798d7d9
Show file tree
Hide file tree
Showing 10 changed files with 167 additions and 29 deletions.
25 changes: 25 additions & 0 deletions Fixtures/Miscellaneous/TestableExe/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// swift-tools-version:5.3
import PackageDescription

let package = Package(
name: "TestableExe",
targets: [
.target(
name: "TestableExe1"
),
.target(
name: "TestableExe2"
),
.target(
name: "TestableExe3"
),
.testTarget(
name: "TestableExeTests",
dependencies: [
"TestableExe1",
"TestableExe2",
"TestableExe3",
]
),
]
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
public func GetGreeting1() -> String {
return "Hello, world"
}

print("\(GetGreeting1())!")
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
public func GetGreeting2() -> String {
return "Hello, planet"
}

print("\(GetGreeting2())!")
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
const char * GetGreeting3();
10 changes: 10 additions & 0 deletions Fixtures/Miscellaneous/TestableExe/Sources/TestableExe3/main.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#include <stdio.h>
#include "include/TestableExe3.h"

const char * GetGreeting3() {
return "Hello, universe";
}

int main() {
printf("%s!\n", GetGreeting3());
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import XCTest
import TestableExe1
import TestableExe2
// import TestableExe3
import class Foundation.Bundle

final class TestableExeTests: XCTestCase {
func testExample() throws {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct
// results.

print(GetGreeting1())
XCTAssertEqual(GetGreeting1(), "Hello, world")
print(GetGreeting2())
XCTAssertEqual(GetGreeting2(), "Hello, planet")
// XCTAssertEqual(String(cString: GetGreeting3()), "Hello, universe")

// Some of the APIs that we use below are available in macOS 10.13 and above.
guard #available(macOS 10.13, *) else {
return
}

var execPath = productsDirectory.appendingPathComponent("TestableExe1")
var process = Process()
process.executableURL = execPath
var pipe = Pipe()
process.standardOutput = pipe
try process.run()
process.waitUntilExit()
var data = pipe.fileHandleForReading.readDataToEndOfFile()
var output = String(data: data, encoding: .utf8)
XCTAssertEqual(output, "Hello, world!\n")

execPath = productsDirectory.appendingPathComponent("TestableExe2")
process = Process()
process.executableURL = execPath
pipe = Pipe()
process.standardOutput = pipe
try process.run()
process.waitUntilExit()
data = pipe.fileHandleForReading.readDataToEndOfFile()
output = String(data: data, encoding: .utf8)
XCTAssertEqual(output, "Hello, planet!\n")

execPath = productsDirectory.appendingPathComponent("TestableExe3")
process = Process()
process.executableURL = execPath
pipe = Pipe()
process.standardOutput = pipe
try process.run()
process.waitUntilExit()
data = pipe.fileHandleForReading.readDataToEndOfFile()
output = String(data: data, encoding: .utf8)
XCTAssertEqual(output, "Hello, universe!\n")
}

/// Returns path to the built products directory.
var productsDirectory: URL {
#if os(macOS)
for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") {
return bundle.bundleURL.deletingLastPathComponent()
}
fatalError("couldn't find the products directory")
#else
return Bundle.main.bundleURL
#endif
}

static var allTests = [
("testExample", testExample),
]
}
36 changes: 32 additions & 4 deletions Sources/Build/BuildPlan.swift
Original file line number Diff line number Diff line change
Expand Up @@ -499,8 +499,7 @@ public final class SwiftTargetBuildDescription {

/// The path to the swiftmodule file after compilation.
var moduleOutputPath: AbsolutePath {
let dirPath = (target.type == .executable) ? tempsPath : buildParameters.buildPath
return dirPath.appending(component: target.c99name + ".swiftmodule")
return buildParameters.buildPath.appending(component: target.c99name + ".swiftmodule")
}

/// The path to the wrapped swift module which is created using the modulewrap tool. This is required
Expand Down Expand Up @@ -662,6 +661,14 @@ public final class SwiftTargetBuildDescription {
args += buildParameters.sanitizers.compileSwiftFlags()
args += ["-parseable-output"]

// If we're compiling the main module of an executable, we rename the `_main`
// entry point to `_<modulename>_main`. This will allow tests to link against
// them without conflicts. When we link the executable we will ask the linker
// to rename the entry point symbol to just `_main` again.
if target.type == .executable {
args += ["-Xfrontend", "-entry-point-function-name", "-Xfrontend", "\(target.c99name)_main"]
}

// Only add the build path to the framework search path if there are binary frameworks to link against.
if !libraryBinaryPaths.isEmpty {
args += ["-F", buildParameters.buildPath.pathString]
Expand Down Expand Up @@ -1014,7 +1021,7 @@ public final class ProductBuildDescription {
return buildParameters.binaryPath(for: product)
}

/// The objects in this product.
/// All object files to link into this product.
///
// Computed during build planning.
public fileprivate(set) var objects = SortedArray<AbsolutePath>()
Expand Down Expand Up @@ -1133,6 +1140,23 @@ public final class ProductBuildDescription {
}
}
args += ["-emit-executable"]

// If we're linking an executable whose main module is implemented in Swift,
// we rename the `_<modulename>_main` entry point symbol to `_main` again.
// This is because executable modules implemented in Swift are compiled with
// a main symbol named that way to allow tests to link against it without
// conflicts. If we're using a linker that doesn't support symbol renaming,
// an alternate implementation could use a generated source file with a stub
// implementation of `_main` to call the renamed main symbol.
let execModule = product.executableModule
if execModule.underlyingTarget is SwiftTarget {
if buildParameters.triple.isDarwin() {
args += ["-Xlinker", "-alias", "-Xlinker", "_\(execModule.c99name)_main", "-Xlinker", "_main"]
}
else {
args += ["-Xlinker", "-symdef", "-Xlinker", "_main=_\(execModule.c99name)_main"]
}
}
case .plugin:
throw InternalError("unexpectedly asked to generate linker arguments for a plugin product")
}
Expand Down Expand Up @@ -1615,7 +1639,11 @@ public class BuildPlan {
switch target.type {
// Include executable and tests only if they're top level contents
// of the product. Otherwise they are just build time dependency.
case .executable, .test:
case .executable:
if product.targets.contains(target) || (product.type == .test && target.underlyingTarget is SwiftTarget) {
staticTargets.append(target)
}
case .test:
if product.targets.contains(target) {
staticTargets.append(target)
}
Expand Down
12 changes: 6 additions & 6 deletions Tests/BuildTests/BuildPlanTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ final class BuildPlanTests: XCTestCase {
"@/path/to/build/debug/exe.product/Objects.LinkFileList",
"-Xlinker", "-rpath", "-Xlinker", "/fake/path/lib/swift/macosx",
"-target", "x86_64-apple-macosx10.10", "-Xlinker", "-add_ast_path",
"-Xlinker", "/path/to/build/debug/exe.build/exe.swiftmodule", "-Xlinker", "-add_ast_path",
"-Xlinker", "/path/to/build/debug/exe.swiftmodule", "-Xlinker", "-add_ast_path",
"-Xlinker", "/path/to/build/debug/lib.swiftmodule",
]
#else
Expand Down Expand Up @@ -783,7 +783,7 @@ final class BuildPlanTests: XCTestCase {
"@/path/to/build/debug/exe.product/Objects.LinkFileList",
"-Xlinker", "-rpath", "-Xlinker", "/fake/path/lib/swift/macosx",
"-target", "x86_64-apple-macosx10.10",
"-Xlinker", "-add_ast_path", "-Xlinker", "/path/to/build/debug/exe.build/exe.swiftmodule",
"-Xlinker", "-add_ast_path", "-Xlinker", "/path/to/build/debug/exe.swiftmodule",
])
#else
XCTAssertEqual(try result.buildProduct(for: "exe").linkArguments(), [
Expand Down Expand Up @@ -993,7 +993,7 @@ final class BuildPlanTests: XCTestCase {
"@/path/to/build/debug/exe.product/Objects.LinkFileList",
"-Xlinker", "-rpath", "-Xlinker", "/fake/path/lib/swift/macosx",
"-target", "x86_64-apple-macosx10.10",
"-Xlinker", "-add_ast_path", "-Xlinker", "/path/to/build/debug/exe.build/exe.swiftmodule",
"-Xlinker", "-add_ast_path", "-Xlinker", "/path/to/build/debug/exe.swiftmodule",
])
#else
XCTAssertEqual(try result.buildProduct(for: "exe").linkArguments(), [
Expand Down Expand Up @@ -1091,7 +1091,7 @@ final class BuildPlanTests: XCTestCase {
"@/path/to/build/debug/Foo.product/Objects.LinkFileList",
"-Xlinker", "-rpath", "-Xlinker", "/fake/path/lib/swift/macosx",
"-target", "x86_64-apple-macosx10.10",
"-Xlinker", "-add_ast_path", "-Xlinker", "/path/to/build/debug/Foo.build/Foo.swiftmodule"
"-Xlinker", "-add_ast_path", "-Xlinker", "/path/to/build/debug/Foo.swiftmodule"
])

XCTAssertEqual(barLinkArgs, [
Expand Down Expand Up @@ -2340,10 +2340,10 @@ final class BuildPlanTests: XCTestCase {
XCTAssertMatch(contents, .contains("""
"/path/to/build/debug/exe.build/exe.swiftmodule.o":
tool: shell
inputs: ["/path/to/build/debug/exe.build/exe.swiftmodule"]
inputs: ["/path/to/build/debug/exe.swiftmodule"]
outputs: ["/path/to/build/debug/exe.build/exe.swiftmodule.o"]
description: "Wrapping AST for exe for debugging"
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"]
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"]
"""))
XCTAssertMatch(contents, .contains("""
"/path/to/build/debug/lib.build/lib.swiftmodule.o":
Expand Down
25 changes: 8 additions & 17 deletions Tests/FunctionalTests/MiscellaneousTests.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
This source file is part of the Swift.org open source project
Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors
Licensed under Apache License v2.0 with Runtime Library Exception
See http://swift.org/LICENSE.txt for license information
Expand Down Expand Up @@ -578,25 +578,16 @@ class MiscellaneousTestCase: XCTestCase {
#endif
}

func testErrorMessageWhenTestLinksExecutable() {
func testTestsCanLinkAgainstExecutable() {
fixture(name: "Miscellaneous/ExeTest") { prefix in
do {
try executeSwiftTest(prefix)
XCTFail()
} catch SwiftPMProductError.executionFailure(let error, let output, let stderr) {
XCTAssertMatch(stderr + output, .contains("Compiling Exe main.swift"))
XCTAssertMatch(stderr + output, .contains("Compiling ExeTests ExeTests.swift"))
XCTAssertMatch(stderr + output, .regex("error: no such module 'Exe'"))
XCTAssertMatch(stderr + output, .regex("note: module 'Exe' is the main module of an executable, and cannot be imported by tests and other targets"))

if case ProcessResult.Error.nonZeroExit(let result) = error {
// if our code crashes we'll get an exit code of 256
XCTAssertEqual(result.exitStatus, .terminated(code: 1))
} else {
XCTFail("\(stderr + output)")
}
let (stdout, _) = try executeSwiftTest(prefix)
XCTAssertMatch(stdout, .contains("Compiling Exe main.swift"))
XCTAssertMatch(stdout, .contains("Compiling ExeTests ExeTests.swift"))
XCTAssertMatch(stdout, .contains("Eliding symbols from Exe_main.swift.o"))
XCTAssertMatch(stdout, .contains("Linking ExeTestPackageTests"))
} catch {
XCTFail()
XCTFail("\(error)")
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions Tests/WorkspaceTests/InitTests.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
This source file is part of the Swift.org open source project
Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors
Licensed under Apache License v2.0 with Runtime Library Exception
See http://swift.org/LICENSE.txt for license information
Expand Down Expand Up @@ -90,7 +90,7 @@ class InitTests: XCTestCase {
let triple = Resources.default.toolchain.triple
let binPath = path.appending(components: ".build", triple.tripleString, "debug")
XCTAssertFileExists(binPath.appending(component: "Foo"))
XCTAssertFileExists(binPath.appending(components: "Foo.build", "Foo.swiftmodule"))
XCTAssertFileExists(binPath.appending(components: "Foo.swiftmodule"))
}
}

Expand Down

0 comments on commit 798d7d9

Please sign in to comment.