Skip to content

Generate boilerplate code for XCTest on linux using AST #164

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

Closed
wants to merge 10 commits into from
Closed
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
5 changes: 4 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,15 @@ let package = Package(
/** Common components of both executables */
name: "Multitool",
dependencies: ["PackageType"]),
Target(
name: "ASTParser",
dependencies: ["POSIX", "Utility"]),
Target(
name: "swift-build",
dependencies: ["Get", "Transmute", "Build", "Multitool"]),
Target(
name: "swift-test",
dependencies: ["Multitool"]),
dependencies: ["Multitool", "ASTParser"]),
])


Expand Down
91 changes: 91 additions & 0 deletions Sources/ASTParser/generate().swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
This source file is part of the Swift.org open source project

Copyright 2015 - 2016 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 Utility
import POSIX
import func libc.fclose

public func generate(testModules: [TestModule], prefix: String) throws {

let testManifestFolder = Path.join(prefix, "XCTestGen")
try mkdir(testManifestFolder)

for module in testModules {
let path = Path.join(testManifestFolder, "\(module.name)-XCTestManifest.swift")
try writeXCTestManifest(module, path: path)
}

let main = Path.join(testManifestFolder, "XCTestMain.swift")
try writeXCTestMain(testModules, path: main)
}

private func writeXCTestManifest(module: TestModule, path: String) throws {

let file = try fopen(path, mode: .Write)
defer {
fclose(file)
}

//imports
try fputs("import XCTest\n", file)
try fputs("\n", file)

try fputs("#if os(Linux)\n", file)

//for each class
try module.classes.sort { $0.name < $1.name }.forEach { moduleClass in
try fputs("extension \(moduleClass.name) {\n", file)
try fputs(" static var allTests : [(String, \(moduleClass.name) -> () throws -> Void)] {\n", file)
try fputs(" return [\n", file)

try moduleClass.testMethods.sort().forEach {
let methodName = $0[$0.startIndex..<$0.endIndex.advancedBy(-2)]
try fputs(" (\"\(methodName)\", \(methodName)),\n", file)
}

try fputs(" ]\n", file)
try fputs(" }\n", file)
try fputs("}\n", file)
}

try fputs("#endif\n\n", file)
}

private func writeXCTestMain(testModules: [TestModule], path: String) throws {

//don't write anything if no classes are available
guard testModules.count > 0 else { return }

let file = try fopen(path, mode: .Write)
defer {
fclose(file)
}

//imports
try fputs("import XCTest\n", file)
try testModules.flatMap { $0.name }.sort().forEach {
try fputs("@testable import \($0)\n", file)
}
try fputs("\n", file)

try fputs("XCTMain([\n", file)

//for each class
for module in testModules {
try module
.classes
.sort { $0.name < $1.name }
.forEach { moduleClass in
try fputs(" testCase(\(module.name).\(moduleClass.name).allTests),\n", file)
}
}

try fputs("])\n\n", file)
}
35 changes: 35 additions & 0 deletions Sources/ASTParser/parseAST().swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
This source file is part of the Swift.org open source project

Copyright 2015 - 2016 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 Utility

public struct TestModule {
struct Class {
let name: String
let testMethods: [String]
}
let name: String
let classes: [Class]
}

public func parseAST(dir: String) throws -> [TestModule] {
var testModules: [TestModule] = []

try walk(dir, recursively: false).filter{$0.isFile}.forEach { file in
let fp = File(path: file)
let astString = try fp.enumerate().reduce("") { $0 + $1 }
let fileName = file.basename
let moduleName = fileName[fileName.startIndex..<fileName.endIndex.advancedBy(-4)]
print("Processing \(moduleName) AST")
testModules += [parseASTString(astString, module: moduleName)]
}

return testModules
}
108 changes: 108 additions & 0 deletions Sources/ASTParser/parser.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
This source file is part of the Swift.org open source project

Copyright 2015 - 2016 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
*/

func parseASTString(astString: String, module: String) -> TestModule {
let sourceNodes = parseASTString(astString)
var classes: [TestModule.Class] = []
for source in sourceNodes {
for node in source.nodes {
guard case let .Class(isXCTestCaseSubClass) = node.type where isXCTestCaseSubClass else { continue }
var testMethods: [String] = []
for classNode in node.nodes {
guard case let .Fn(signature) = classNode.type else { continue }
if classNode.name.hasPrefix("test") && signature == "(\(node.name)) -> () -> ()" {
testMethods += [classNode.name]
}
}
classes += [TestModule.Class(name: node.name, testMethods: testMethods)]
}
}
return TestModule(name: module, classes: classes)
}


private class Node {
enum NodeType {
case Class(isXCTestCaseSubClass: Bool)
case Fn(signature: String) // would be like : `(ClassName) -> () -> ()`
case Unknown
}
var contents: String = "" {
didSet {
guard let index = contents.characters.indexOf(" ") else {
return
}
let decl = contents[contents.startIndex..<index]
name = contents.substringBetween("\"") ?? ""
if decl == "class_decl" {
type = .Class(isXCTestCaseSubClass: contents.hasSuffix("XCTestCase"))
} else if decl == "func_decl", let signature = contents.substringBetween("'") {
type = .Fn(signature: signature)
}
}
}
var nodes: [Node] = []
var type: NodeType = .Unknown
var name: String = ""
}

private func parseASTString(astString: String) -> [Node] {
var stack = Array<Node>()
var data = ""
var quoteStarted = false
var quoteChar: Character? = nil
var sources: [Node] = []

for char in astString.characters {

if char == "(" && !quoteStarted {
let node = Node()
if data.characters.count > 0, let lastNode = stack.last, let chuzzledData = data.chuzzle() {
lastNode.contents = chuzzledData
if lastNode.contents == "source_file" { sources += [lastNode] }
}
stack.append(node)
data = ""
} else if char == ")" && !quoteStarted {
if case let poppedNode = stack.removeLast() where stack.count > 0 {
if data.characters.count > 0, let chuzzledData = data.chuzzle() {
poppedNode.contents = chuzzledData
}
stack.last!.nodes += [poppedNode]

}
data = ""
} else {
data = data + String(char)
if char == "\"" || char == "'" {
if quoteChar == nil {
quoteChar = char
quoteStarted = true
} else if char == quoteChar {
quoteChar = nil
quoteStarted = false
}
}
}

}
return sources
}

private extension String {
func substringBetween(char: Character) -> String? {
guard let firstIndex = self.characters.indexOf(char) where firstIndex != self.endIndex else {
return nil
}
let choppedString = self[firstIndex.successor()..<self.endIndex]
guard let secondIndex = choppedString.characters.indexOf(char) else { return nil }
return choppedString[choppedString.startIndex..<secondIndex]
}
}
70 changes: 61 additions & 9 deletions Sources/Build/describe().swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ public func describe(prefix: String, _ conf: Configuration, _ modules: [Module],
let (buildableTests, buildableNonTests) = (modules.map{$0 as Buildable} + products.map{$0 as Buildable}).partition{$0.isTest}
let (tests, nontests) = (buildableTests.map{$0.targetName}, buildableNonTests.map{$0.targetName})


var testsAst: [String] = []
for case let x as TestModule in modules {
testsAst.append("<\(x.name).ast.module>")
}
let xcTestGenPath = Path.join(prefix, "XCTestGen")

defer { yaml.close() }

try write("client:")
Expand All @@ -39,6 +46,7 @@ public func describe(prefix: String, _ conf: Configuration, _ modules: [Module],
try write("targets:")
try write(" default: ", nontests)
try write(" test: ", tests)
try write(" tests-ast: ", testsAst)
try write("commands: ")

var mkdirs = Set<String>()
Expand Down Expand Up @@ -66,18 +74,23 @@ public func describe(prefix: String, _ conf: Configuration, _ modules: [Module],

let node = IncrementalNode(module: module, prefix: prefix)

var testGenFile: String? = nil
if module is TestModule {
testGenFile = Path.join(xcTestGenPath, "\(module.c99name)-XCTestManifest")
}

try write(" ", module.targetName, ":")
try write(" tool: swift-compiler")
try write(" executable: ", Resources.path.swiftc)
try write(" module-name: ", module.c99name)
try write(" module-output-path: ", node.moduleOutputPath)
try write(" inputs: ", node.inputs)
try write(" outputs: ", node.outputs)
try write(" outputs: ", node.outputs + (testGenFile != nil ? ["\(testGenFile!).o"] : []))
try write(" import-paths: ", prefix)
try write(" temps-path: ", node.tempsPath)
try write(" objects: ", node.objectPaths)
try write(" objects: ", node.objectPaths + (testGenFile != nil ? ["\(testGenFile!).o"] : []))
try write(" other-args: ", args + otherArgs)
try write(" sources: ", module.sources.paths)
try write(" sources: ", module.sources.paths + (testGenFile != nil ? ["\(testGenFile!).swift"] : []))
Copy link
Member

Choose a reason for hiding this comment

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

Are you assuming that tests will never be compiled in Release mode? Otherwise you'd need to add the new generated files down in the .Release branch as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this was totally a work around just to see if it was working, as mentioned in description this file needs refactoring.... I don't even like what I did here 😆


// this must be set or swiftc compiles single source file
// modules with a main() for some reason
Expand All @@ -104,6 +117,43 @@ public func describe(prefix: String, _ conf: Configuration, _ modules: [Module],
try write(" args: ", [Resources.path.swiftc, "-o", productPath] + args + module.sources.paths + otherArgs)
}
}


// MARK:- AST stuff

for case let testModule as TestModule in modules {
let name = "<\(testModule.name).ast.module>"

var args = [Resources.path.swiftc, "-dump-ast"]
args += ["-module-name", testModule.c99name]
args += ["-I", prefix]

#if os(OSX)
if let platformPath = Resources.path.platformPath {
let path = Path.join(platformPath, "Developer/Library/Frameworks")
args += ["-F", path]
} else {
throw Error.InvalidPlatformPath
}
#endif
args += platformArgs()
args += testModule.sources.paths

args += ["2>"]

let testsAstDir = Path.join(prefix, "TestsAST")
mkdirs.insert(testsAstDir)
args += [Path.join(testsAstDir, "\(testModule.c99name).ast")]

let realArgs = ["/bin/sh", "-c", args.reduce(""){$0 + " " + $1}]

try write(" ", name, ":")
try write(" tool: shell")
try write(" description: Generating AST for \(name)")
try write(" outputs: ", [name])
try write(" args: ", realArgs)
}


// make eg .build/debug/foo.build/subdir for eg. Sources/foo/subdir/bar.swift
// TODO swift-build-tool should do this
Expand Down Expand Up @@ -147,11 +197,8 @@ public func describe(prefix: String, _ conf: Configuration, _ modules: [Module],
}
}
#else
// HACK: To get a path to LinuxMain.swift, we just grab the
// parent directory of the first test module we can find.
let firstTestModule = product.modules.flatMap{ $0 as? TestModule }.first!
let testDirectory = firstTestModule.sources.root.parentDirectory
let main = Path.join(testDirectory, "LinuxMain.swift")

let main = Path.join(xcTestGenPath, "XCTestMain.swift")
args.append(main)
args.append("-emit-executable")
args += ["-I", prefix]
Expand All @@ -171,7 +218,12 @@ public func describe(prefix: String, _ conf: Configuration, _ modules: [Module],
args += platformArgs() //TODO don't need all these here or above: split outname
args += Xld
args += ["-o", outpath]
args += objects

var genObjs: [String] = []
if case .Test = product.type {
genObjs = product.modules.filter{$0 is TestModule}.flatMap { Path.join(xcTestGenPath, "\($0.c99name)-XCTestManifest.o") }
}
args += objects + genObjs

let inputs = product.modules.flatMap{ [$0.targetName] + IncrementalNode(module: $0, prefix: prefix).inputs }

Expand Down
8 changes: 7 additions & 1 deletion Sources/swift-test/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import func libc.setenv
import func libc.exit
import Multitool
import Utility
import ASTParser

// Initialize the resource support.
public var globalSymbolInMainBinary = 0
Expand All @@ -25,7 +26,12 @@ do {

let yamlPath = Path.join(dir.build, "debug.yaml")
guard yamlPath.exists else { throw Error.DebugYAMLNotFound }


try build(YAMLPath: yamlPath, target: "tests-ast")
let testModules = try parseAST(Path.join(dir.build, "debug", "TestsAST"))

try generate(testModules, prefix: Path.join(dir.build, "debug"))

try build(YAMLPath: yamlPath, target: "test")
let success = test(dir.build, "debug")
exit(success ? 0 : 1)
Expand Down
Loading