Skip to content

Commit

Permalink
[Commands] Add LinuxMainGenerator
Browse files Browse the repository at this point in the history
This adds a new option "--generate-linuxmain" to 'swift test' to
generate test entries that can be used for XCTest on Linux.  Since only
macOS supports automatic test discovery, this option will only work on
macOS. Using it on Linux will result in a "null" generation.

<rdar://problem/37259907> Use macOS's test disovery to generate LinuxMain.swift
  • Loading branch information
aciidgh committed Feb 9, 2018
1 parent 31e5aff commit 3a12883
Show file tree
Hide file tree
Showing 8 changed files with 294 additions and 25 deletions.
11 changes: 10 additions & 1 deletion Fixtures/Miscellaneous/ParallelTestsPkg/Package.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
// swift-tools-version:4.0
import PackageDescription

let package = Package(
name: "ParallelTestsPkg"
name: "ParallelTestsPkg",
targets: [
.target(
name: "ParallelTestsPkg",
dependencies: []),
.testTarget(
name: "ParallelTestsPkgTests",
dependencies: ["ParallelTestsPkg"]),
]
)
6 changes: 0 additions & 6 deletions Fixtures/Miscellaneous/ParallelTestsPkg/Tests/LinuxMain.swift

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,4 @@ class ParallelTestsFailureTests: XCTestCase {
func testSureFailure() {
XCTFail("Giving up is the only sure way to fail.")
}

static var allTests : [(String, (ParallelTestsFailureTests) -> () throws -> Void)] {
return [
("testSureFailure", testSureFailure),
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,4 @@ class ParallelTestsTests: XCTestCase {
func testExample2() {
XCTAssertEqual(ParallelTests().bool, false)
}

static var allTests : [(String, (ParallelTestsTests) -> () throws -> Void)] {
return [
("testExample1", testExample1),
("testExample2", testExample2),
]
}
}
202 changes: 202 additions & 0 deletions Sources/Commands/GenerateLinuxMain.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
/*
This source file is part of the Swift.org open source project

Copyright 2015 - 2018 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
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
*/

import Basic
import PackageGraph
import PackageModel

/// A utility for generating test entries on linux.
///
/// This uses input from macOS's test discovery and generates
/// corelibs-xctest compatible test manifests.
final class LinuxMainGenerator {

enum Error: Swift.Error {
case noTestTargets
}

/// The package graph we're working on.
let graph: PackageGraph

/// The test suites that we need to write.
let testSuites: [TestSuite]

init(graph: PackageGraph, testSuites: [TestSuite]) {
self.graph = graph
self.testSuites = testSuites
}

/// Generate the XCTestManifests.swift and LinuxMain.swift for the package.
func generate() throws {
// Create the module struct from input.
//
// This converts the input test suite into a structure that
// is more suitable for generating linux test entries.
let modulesBuilder = ModulesBuilder()
for suite in testSuites {
modulesBuilder.add(suite.tests)
}
let modules = modulesBuilder.build()

// Generate manifest file for each test module we got from XCTest discovery.
for module in modules.lazy.sorted(by: { $0.name < $1.name }) {
guard let target = graph.reachableTargets.first(where: { $0.c99name == module.name }) else {
print("warning: did not file target '\(module.name)'")
continue
}
assert(target.type == .test, "Unexpected target type \(target.type) for \(target)")

// Write the manifest file for this module.
let testManifest = target.sources.root.appending(component: "XCTestManifests.swift")
let stream = try LocalFileOutputByteStream(testManifest)

stream <<< "import XCTest" <<< "\n"
for klass in module.classes.lazy.sorted(by: { $0.name < $1.name }) {
stream <<< "\n"
stream <<< "extension " <<< klass.name <<< " {" <<< "\n"
stream <<< indent(4) <<< "static let __allTests = [" <<< "\n"
for method in klass.methods {
stream <<< indent(8) <<< "(\"\(method)\", \(method))," <<< "\n"
}
stream <<< indent(4) <<< "]" <<< "\n"
stream <<< "}" <<< "\n"
}

stream <<<
"""
#if !os(macOS)
public func __allTests() -> [XCTestCaseEntry] {
return [
"""

for klass in module.classes {
stream <<< indent(8) <<< "testCase(" <<< klass.name <<< ".__allTests)," <<< "\n"
}

stream <<< """
]
}
#endif
"""
stream.flush()
}

/// Write LinuxMain.swift file.
guard let testTarget = graph.reachableProducts.first(where: { $0.type == .test })?.targets.first else {
throw Error.noTestTargets
}
let linuxMain = testTarget.sources.root.parentDirectory.appending(components: SwiftTarget.linuxMainBasename)

let stream = try LocalFileOutputByteStream(linuxMain)
stream <<< "import XCTest" <<< "\n\n"
for module in modules {
stream <<< "import " <<< module.name <<< "\n"
}
stream <<< "\n"
stream <<< "var tests = [XCTestCaseEntry]()" <<< "\n"
for module in modules {
stream <<< "tests += \(module.name).__allTests()" <<< "\n"
}
stream <<< "\n"
stream <<< "XCTMain(tests)" <<< "\n"
stream.flush()
}

private func indent(_ spaces: Int) -> ByteStreamable {
return Format.asRepeating(string: " ", count: spaces)
}
}

// MARK: - Internal data structure for LinuxMainGenerator.

private struct Module {
struct Class {
let name: String
let methods: [String]
}
let name: String
let classes: [Class]
}

private final class ModulesBuilder {

final class ModuleBuilder {
let name: String
var classes: [ClassBuilder]

init(_ name: String) {
self.name = name
self.classes = []
}

func build() -> Module {
return Module(name: name, classes: classes.map({ $0.build() }))
}
}

final class ClassBuilder {
let name: String
var methods: [String]

init(_ name: String) {
self.name = name
self.methods = []
}

func build() -> Module.Class {
return .init(name: name, methods: methods)
}
}

/// The built modules.
private var modules: [ModuleBuilder] = []

func add(_ cases: [TestSuite.TestCase]) {
for testCase in cases {
let (module, theKlass) = testCase.name.split(around: ".")
guard let klass = theKlass else {
fatalError("Unsupported test case name \(testCase.name)")
}
for method in testCase.tests {
add(module, klass, method)
}
}
}

private func add(_ moduleName: String, _ klassName: String, _ methodName: String) {
// Find or create the module.
let module: ModuleBuilder
if let theModule = modules.first(where: { $0.name == moduleName }) {
module = theModule
} else {
module = ModuleBuilder(moduleName)
modules.append(module)
}

// Find or create the class.
let klass: ClassBuilder
if let theKlass = module.classes.first(where: { $0.name == klassName }) {
klass = theKlass
} else {
klass = ClassBuilder(klassName)
module.classes.append(klass)
}

// Finally, append the method to the class.
klass.methods.append(methodName)
}

func build() -> [Module] {
return modules.map({ $0.build() })
}
}
34 changes: 29 additions & 5 deletions Sources/Commands/SwiftTestTool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import class Foundation.ProcessInfo
import Basic
import Build
import Utility
import PackageGraph

import func POSIX.exit

Expand Down Expand Up @@ -73,6 +74,10 @@ public class TestToolOptions: ToolOptions {
return .listTests
}

if shouldGenerateLinuxMain {
return .generateLinuxMain
}

return .runSerial
}

Expand All @@ -85,6 +90,9 @@ public class TestToolOptions: ToolOptions {
/// List the tests and exit.
var shouldListTests = false

/// Generate LinuxMain entries and exit.
var shouldGenerateLinuxMain = false

var testCaseSpecifier: TestCaseSpecifier = .none
}

Expand All @@ -103,6 +111,7 @@ public enum TestCaseSpecifier {
public enum TestMode {
case version
case listTests
case generateLinuxMain
case runSerial
case runParallel
}
Expand All @@ -121,13 +130,14 @@ public class SwiftTestTool: SwiftTool<TestToolOptions> {
}

override func runImpl() throws {
let graph = try loadPackageGraph()

switch options.mode {
case .version:
print(Versioning.currentVersion.completeDisplayString)

case .listTests:
let testPath = try buildTestsIfNeeded(options)
let testPath = try buildTestsIfNeeded(options, graph: graph)
let testSuites = try getTestSuites(path: testPath)
let tests = testSuites.filteredTests(specifier: options.testCaseSpecifier)

Expand All @@ -136,8 +146,17 @@ public class SwiftTestTool: SwiftTool<TestToolOptions> {
print(test.specifier)
}

case .generateLinuxMain:
#if os(Linux)
warning(message: "can't discover new tests on Linux; please use this option on macOS instead")
#endif
let testPath = try buildTestsIfNeeded(options, graph: graph)
let testSuites = try getTestSuites(path: testPath)
let generator = LinuxMainGenerator(graph: graph, testSuites: testSuites)
try generator.generate()

case .runSerial:
let testPath = try buildTestsIfNeeded(options)
let testPath = try buildTestsIfNeeded(options, graph: graph)
var ranSuccessfully = true

switch options.testCaseSpecifier {
Expand Down Expand Up @@ -172,7 +191,7 @@ public class SwiftTestTool: SwiftTool<TestToolOptions> {
}

case .runParallel:
let testPath = try buildTestsIfNeeded(options)
let testPath = try buildTestsIfNeeded(options, graph: graph)
let testSuites = try getTestSuites(path: testPath)
let tests = testSuites.filteredTests(specifier: options.testCaseSpecifier)

Expand All @@ -195,8 +214,8 @@ public class SwiftTestTool: SwiftTool<TestToolOptions> {
/// Builds the "test" target if enabled in options.
///
/// - Returns: The path to the test binary.
private func buildTestsIfNeeded(_ options: TestToolOptions) throws -> AbsolutePath {
let buildPlan = try BuildPlan(buildParameters: self.buildParameters(), graph: loadPackageGraph())
private func buildTestsIfNeeded(_ options: TestToolOptions, graph: PackageGraph) throws -> AbsolutePath {
let buildPlan = try BuildPlan(buildParameters: self.buildParameters(), graph: graph)
if options.shouldBuildTests {
try build(plan: buildPlan, subset: .allIncludingTests)
}
Expand Down Expand Up @@ -225,6 +244,11 @@ public class SwiftTestTool: SwiftTool<TestToolOptions> {
usage: "Lists test methods in specifier format"),
to: { $0.shouldListTests = $1 })

binder.bind(
option: parser.add(option: "--generate-linuxmain", kind: Bool.self,
usage: "Generate LinuxMain.swift entries for the package"),
to: { $0.shouldGenerateLinuxMain = $1 })

binder.bind(
option: parser.add(option: "--parallel", kind: Bool.self,
usage: "Run the tests in parallel."),
Expand Down
Loading

0 comments on commit 3a12883

Please sign in to comment.