Skip to content
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

Allow unit test targets to import and link executable targets #3316

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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ Swift v.Next
* Improvements

Adding a dependency requirement can now be done with the convenience initializer `.package(url: String, branch: String)`.

Test targets can now link against executable targets as if they were libraries, so that they can test any data strutures or algorithms in them. All the code in the executable except for the main entry point itself is available to the unit test. Separate executables are still linked, and can be tested as a subprocess in the same way as before. This feature is available to tests defined in packages that have a tools version of `vNext` or newer.



Expand Down
4 changes: 2 additions & 2 deletions Fixtures/Miscellaneous/ExeTest/Package.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
// swift-tools-version:5.3
// swift-tools-version: 999.0
abertelrud marked this conversation as resolved.
Show resolved Hide resolved
import PackageDescription

let package = Package(
name: "ExeTest",
targets: [
.target(
.executableTarget(
name: "Exe",
dependencies: []
),
Expand Down
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: 999.0
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 {
Copy link
Contributor

Choose a reason for hiding this comment

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

@abertelrud does this stuff really not work on any platform but macOS?

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),
]
}
108 changes: 100 additions & 8 deletions Sources/Build/BuildPlan.swift
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,18 @@ extension BuildParameters {
return args
}

/// Computes the linker flags to use in order to rename a module-named main function to 'main' for the target platform, or nil if the linker doesn't support it for the platform.
fileprivate func linkerFlagsForRenamingMainFunction(of target: ResolvedTarget) -> [String]? {
var args: [String] = []
if self.triple.isDarwin() {
args = ["-alias", "_\(target.c99name)_main", "_main"]
}
else if self.triple.isLinux() {
args = ["--defsym", "main=\(target.c99name)_main"]
}
return args.flatMap { ["-Xlinker", $0] }
}

/// Returns the scoped view of build settings for a given target.
fileprivate func createScope(for target: ResolvedTarget) -> BuildSettings.Scope {
return BuildSettings.Scope(target.underlyingTarget.buildSettings, environment: buildEnvironment)
Expand Down Expand Up @@ -195,6 +207,11 @@ public final class ClangTargetBuildDescription {
public var clangTarget: ClangTarget {
return target.underlyingTarget as! ClangTarget
}

/// The tools version of the package that declared the target. This can
/// can be used to conditionalize semantically significant changes in how
/// a target is built.
public let toolsVersion: ToolsVersion

/// The build parameters.
let buildParameters: BuildParameters
Expand Down Expand Up @@ -249,11 +266,12 @@ public final class ClangTargetBuildDescription {
}

/// Create a new target description with target and build parameters.
init(target: ResolvedTarget, buildParameters: BuildParameters, fileSystem: FileSystem = localFileSystem, diagnostics: DiagnosticsEngine) throws {
init(target: ResolvedTarget, toolsVersion: ToolsVersion, buildParameters: BuildParameters, fileSystem: FileSystem = localFileSystem, diagnostics: DiagnosticsEngine) throws {
assert(target.underlyingTarget is ClangTarget, "underlying target type mismatch \(target)")
self.fileSystem = fileSystem
self.diagnostics = diagnostics
self.target = target
self.toolsVersion = toolsVersion
self.buildParameters = buildParameters
self.tempsPath = buildParameters.buildPath.appending(component: target.c99name + ".build")
self.derivedSources = Sources(paths: [], root: tempsPath.appending(component: "DerivedSources"))
Expand Down Expand Up @@ -472,6 +490,11 @@ public final class SwiftTargetBuildDescription {
/// The target described by this target.
public let target: ResolvedTarget

/// The tools version of the package that declared the target. This can
/// can be used to conditionalize semantically significant changes in how
/// a target is built.
public let toolsVersion: ToolsVersion

/// The build parameters.
let buildParameters: BuildParameters

Expand Down Expand Up @@ -504,7 +527,9 @@ public final class SwiftTargetBuildDescription {

/// The path to the swiftmodule file after compilation.
var moduleOutputPath: AbsolutePath {
let dirPath = (target.type == .executable) ? tempsPath : buildParameters.buildPath
// If we're an executable and we're not allowing test targets to link against us, we hide the module.
let allowLinkingAgainstExecutables = (buildParameters.triple.isDarwin() || buildParameters.triple.isLinux()) && toolsVersion >= .vNext
let dirPath = (target.type == .executable && !allowLinkingAgainstExecutables) ? tempsPath : buildParameters.buildPath
return dirPath.appending(component: target.c99name + ".swiftmodule")
}

Expand Down Expand Up @@ -555,6 +580,7 @@ public final class SwiftTargetBuildDescription {
/// Create a new target description with target and build parameters.
init(
target: ResolvedTarget,
toolsVersion: ToolsVersion,
buildParameters: BuildParameters,
pluginInvocationResults: [PluginInvocationResult] = [],
prebuildCommandResults: [PrebuildCommandResult] = [],
Expand All @@ -564,6 +590,7 @@ public final class SwiftTargetBuildDescription {
) throws {
assert(target.underlyingTarget is SwiftTarget, "underlying target type mismatch \(target)")
self.target = target
self.toolsVersion = toolsVersion
self.buildParameters = buildParameters
// Unless mentioned explicitly, use the target type to determine if this is a test target.
self.isTestTarget = isTestTarget ?? (target.type == .test)
Expand Down Expand Up @@ -677,6 +704,24 @@ public final class SwiftTargetBuildDescription {
args += buildParameters.sanitizers.compileSwiftFlags()
args += ["-parseable-output"]

// If we're compiling the main module of an executable other than the one that
// implements a test suite, and if the package tools version indicates that we
// should, we rename the `_main` entry point to `_<modulename>_main`.
//
// This will allow tests to link against the module without any conflicts. And
// when we link the executable, we will ask the linker to rename the entry point
// symbol to just `_main` again (or if the linker doesn't support it, we'll
// generate a source containing a redirect).
if target.type == .executable && !isTestTarget && toolsVersion >= .vNext {
// We only do this if the linker supports it, as indicated by whether we
// can construct the linker flags. In the future we will use a generated
// code stub for the cases in which the linker doesn't support it, so that
// we can rename the symbol unconditionally.
if buildParameters.linkerFlagsForRenamingMainFunction(of: target) != nil {
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 @@ -1018,6 +1063,11 @@ public final class ProductBuildDescription {
/// The reference to the product.
public let product: ResolvedProduct

/// The tools version of the package that declared the product. This can
/// can be used to conditionalize semantically significant changes in how
/// a target is built.
public let toolsVersion: ToolsVersion

/// The build parameters.
let buildParameters: BuildParameters

Expand All @@ -1029,7 +1079,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 @@ -1067,9 +1117,10 @@ public final class ProductBuildDescription {
let diagnostics: DiagnosticsEngine

/// Create a build description for a product.
init(product: ResolvedProduct, buildParameters: BuildParameters, fs: FileSystem, diagnostics: DiagnosticsEngine) {
init(product: ResolvedProduct, toolsVersion: ToolsVersion, buildParameters: BuildParameters, fs: FileSystem, diagnostics: DiagnosticsEngine) {
assert(product.type != .library(.automatic), "Automatic type libraries should not be described.")
self.product = product
self.toolsVersion = toolsVersion
self.buildParameters = buildParameters
self.fs = fs
self.diagnostics = diagnostics
Expand Down Expand Up @@ -1148,6 +1199,20 @@ 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,
// we will instead have generated a source file containing the redirect.
// Support for linking tests againsts executables is conditional on the tools
// version of the package that defines the executable product.
if product.executableModule.underlyingTarget is SwiftTarget, toolsVersion >= .vNext {
if let flags = buildParameters.linkerFlagsForRenamingMainFunction(of: product.executableModule) {
args += flags
}
}
case .plugin:
throw InternalError("unexpectedly asked to generate linker arguments for a plugin product")
}
Expand Down Expand Up @@ -1327,9 +1392,11 @@ public class BuildPlan {
// if test manifest exists, prefer that over test detection,
// this is designed as an escape hatch when test discovery is not appropriate
// and for backwards compatibility for projects that have existing test manifests (LinuxMain.swift)
let toolsVersion = graph.package(for: testProduct)?.manifest.toolsVersion ?? .vNext
if let testManifestTarget = testProduct.testManifestTarget, !generate {
let desc = try SwiftTargetBuildDescription(
target: testManifestTarget,
toolsVersion: toolsVersion,
buildParameters: buildParameters,
isTestTarget: true
)
Expand Down Expand Up @@ -1361,6 +1428,7 @@ public class BuildPlan {

let target = try SwiftTargetBuildDescription(
target: testManifestTarget,
toolsVersion: toolsVersion,
buildParameters: buildParameters,
isTestTarget: true,
testDiscoveryTarget: true
Expand Down Expand Up @@ -1403,18 +1471,24 @@ public class BuildPlan {
}
}
}

// Determine the appropriate tools version to use for the target.
// This can affect what flags to pass and other semantics.
let toolsVersion = graph.package(for: target)?.manifest.toolsVersion ?? .vNext

switch target.underlyingTarget {
case is SwiftTarget:
targetMap[target] = try .swift(SwiftTargetBuildDescription(
target: target,
toolsVersion: toolsVersion,
buildParameters: buildParameters,
pluginInvocationResults: pluginInvocationResults[target] ?? [],
prebuildCommandResults: prebuildCommandResults[target] ?? [],
fs: fileSystem))
case is ClangTarget:
targetMap[target] = try .clang(ClangTargetBuildDescription(
target: target,
toolsVersion: toolsVersion,
buildParameters: buildParameters,
fileSystem: fileSystem,
diagnostics: diagnostics))
Expand Down Expand Up @@ -1448,8 +1522,14 @@ public class BuildPlan {
// Create product description for each product we have in the package graph except
// for automatic libraries and plugins, because they don't produce any output.
for product in graph.allProducts where product.type != .library(.automatic) && product.type != .plugin {

// Determine the appropriate tools version to use for the product.
// This can affect what flags to pass and other semantics.
let toolsVersion = graph.package(for: product)?.manifest.toolsVersion ?? .vNext
productMap[product] = ProductBuildDescription(
product: product, buildParameters: buildParameters,
product: product,
toolsVersion: toolsVersion,
buildParameters: buildParameters,
fs: fileSystem,
diagnostics: diagnostics
)
Expand Down Expand Up @@ -1635,9 +1715,21 @@ public class BuildPlan {
switch dependency {
case .target(let target, _):
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:
// Executable target have historically only been included if they are directly in the product's
// target list. Otherwise they have always been just build-time dependencies.
// In tool version .vNext or greater, we also include executable modules implemented in Swift in
// any test products... this is to allow testing of executables. Note that they are also still
// built as separate products that the test can invoke as subprocesses.
case .executable:
if product.targets.contains(target) {
staticTargets.append(target)
} else if product.type == .test && target.underlyingTarget is SwiftTarget {
if let toolsVersion = graph.package(for: product)?.manifest.toolsVersion, toolsVersion >= .vNext {
staticTargets.append(target)
}
}
// Test targets should be included only if they are directly in the product's target list.
case .test:
if product.targets.contains(target) {
staticTargets.append(target)
}
Expand Down
Loading