Skip to content

Commit 8288cb0

Browse files
committed
Implement SE-0129 to update the test naming conventions. Instead of
automatically and silently appending a `TestSuite` suffix, tests are now expected to have a `Tests` suffix in the module name. Right now we make sure there are no tests under `Sources` and no non-tests under `Tests`, but we expect to lift that restriction in the future to allow test support libraries etc.
1 parent 38caecb commit 8288cb0

File tree

8 files changed

+82
-20
lines changed

8 files changed

+82
-20
lines changed

Fixtures/Miscellaneous/SwiftPMXCTestHelper/Tests/ObjcTest/Objc.m renamed to Fixtures/Miscellaneous/SwiftPMXCTestHelper/Tests/ObjCTests/ObjCTests.m

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
@import XCTest;
22

3-
@interface Objc: XCTestCase
3+
@interface ObjCTests: XCTestCase
44
@end
55

6-
@implementation Objc
6+
@implementation ObjCTests
77

88
- (void)testThisThing {
99
}

Sources/Commands/init.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ final class InitPackage {
114114
guard exists(sources) == false else {
115115
return
116116
}
117-
print("Creating Sources/")
117+
print("Creating \(sources.relative(to: rootd).asString)/")
118118
try makeDirectories(sources)
119119

120120
let sourceFileName = (mode == .executable) ? "main.swift" : "\(typeName).swift"
@@ -160,7 +160,7 @@ final class InitPackage {
160160
guard exists(tests) == false else {
161161
return
162162
}
163-
print("Creating Tests/")
163+
print("Creating \(tests.relative(to: rootd).asString)/")
164164
try makeDirectories(tests)
165165

166166
// Only libraries are testable for now.
@@ -173,16 +173,16 @@ final class InitPackage {
173173
private func writeLinuxMain(testsPath: AbsolutePath) throws {
174174
try writePackageFile(testsPath.appending(component: "LinuxMain.swift")) { stream in
175175
stream <<< "import XCTest\n"
176-
stream <<< "@testable import \(moduleName)TestSuite\n\n"
176+
stream <<< "@testable import \(moduleName)Tests\n\n"
177177
stream <<< "XCTMain([\n"
178178
stream <<< " testCase(\(typeName)Tests.allTests),\n"
179179
stream <<< "])\n"
180180
}
181181
}
182182

183183
private func writeTestFileStubs(testsPath: AbsolutePath) throws {
184-
let testModule = testsPath.appending(RelativePath(pkgname))
185-
print("Creating Tests/\(pkgname)/")
184+
let testModule = testsPath.appending(RelativePath(pkgname + Module.testModuleNameSuffix))
185+
print("Creating \(testModule.relative(to: rootd).asString)/")
186186
try makeDirectories(testModule)
187187

188188
try writePackageFile(testModule.appending(RelativePath("\(moduleName)Tests.swift"))) { stream in

Sources/PackageLoading/PackageBuilder.swift

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,25 +78,45 @@ extension ModuleError.InvalidLayoutType: FixableError {
7878
}
7979

8080
extension Module {
81-
/// An error in the organization of an individual module.
81+
82+
/// An error in the organization or configuration of an individual module.
8283
enum Error: Swift.Error {
84+
85+
/// The module's name is invalid.
86+
case invalidName(path: String, name: String, problem: ModuleNameProblem)
87+
enum ModuleNameProblem {
88+
/// Empty module name.
89+
case emptyName
90+
/// Test module doesn't have a "Tests" suffix.
91+
case noTestSuffix
92+
/// Non-test module does have a "Tests" suffix.
93+
case hasTestSuffix
94+
}
95+
96+
/// The module contains no source code at all.
8397
case noSources(String)
98+
99+
/// The module contains an invalid mix of languages (e.g. both Swift and C).
84100
case mixedSources(String)
85101
}
86102
}
87103

88104
extension Module.Error: FixableError {
89105
var error: String {
90106
switch self {
91-
case .noSources(let path):
107+
case .invalidName(let path, let name, let problem):
108+
return "the module at \(path) has an invalid name ('\(name)'): \(problem.error)"
109+
case .noSources(let path):
92110
return "the module at \(path) does not contain any source files"
93-
case .mixedSources(let path):
111+
case .mixedSources(let path):
94112
return "the module at \(path) contains mixed language source files"
95113
}
96114
}
97115

98116
var fix: String? {
99117
switch self {
118+
case .invalidName(_, _, let problem):
119+
return "rename the module (\(problem.fix))"
100120
case .noSources(_):
101121
return "either remove the module folder, or add a source file to the module"
102122
case .mixedSources(_):
@@ -105,6 +125,30 @@ extension Module.Error: FixableError {
105125
}
106126
}
107127

128+
extension Module.Error.ModuleNameProblem : FixableError {
129+
var error: String {
130+
switch self {
131+
case .emptyName:
132+
return "the module name is empty"
133+
case .noTestSuffix:
134+
return "the name of a test module has no ‘Tests’ suffix"
135+
case .hasTestSuffix:
136+
return "the name of a non-test module has a ‘Tests’ suffix"
137+
}
138+
}
139+
var fix: String? {
140+
switch self {
141+
case .emptyName:
142+
return "give the module a name"
143+
case .noTestSuffix:
144+
return "add a ‘Tests’ suffix"
145+
case .hasTestSuffix:
146+
return "remove the ‘Tests’ suffix"
147+
}
148+
}
149+
}
150+
151+
108152
extension Product {
109153
/// An error in a product definition.
110154
enum Error: Swift.Error {
@@ -349,8 +393,26 @@ public struct PackageBuilder {
349393
return modules
350394
}
351395

396+
/// Private function that checks whether a module name is valid. This method doesn't return anything, but rather, if there's a problem, it throws an error describing what the problem is.
397+
// FIXME: We will eventually be loosening this restriction to allow test-only libraries etc
398+
private func validateModuleName(_ path: AbsolutePath, _ name: String, isTest: Bool) throws {
399+
if name.isEmpty {
400+
throw Module.Error.invalidName(path: path.asString, name: name, problem: .emptyName)
401+
}
402+
if name.hasSuffix(Module.testModuleNameSuffix) && !isTest {
403+
throw Module.Error.invalidName(path: path.asString, name: name, problem: .hasTestSuffix)
404+
}
405+
if !name.hasSuffix(Module.testModuleNameSuffix) && isTest {
406+
throw Module.Error.invalidName(path: path.asString, name: name, problem: .noTestSuffix)
407+
}
408+
}
409+
352410
/// Private function that constructs a single Module object for the moduel at `path`, having the name `name`. If `isTest` is true, the module is constructed as a test module; if false, it is a regular module.
353411
private func createModule(_ path: AbsolutePath, name: String, isTest: Bool) throws -> Module {
412+
413+
// Validate the module name. This function will throw an error if it detects a problem.
414+
try validateModuleName(path, name, isTest: isTest)
415+
354416
// Find all the files under the module path.
355417
let walked = try walk(path, fileSystem: fileSystem, recursing: shouldConsiderDirectory).map{ $0 }
356418

Sources/PackageModel/Module.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ public class Module: ModuleProtocol {
4949
// FIXME: This should probably be rolled into the type.
5050
public let isTest: Bool
5151

52-
private let testModuleNameSuffix = "TestSuite"
52+
/// Suffix that's expected for test modules.
53+
public static let testModuleNameSuffix = "Tests"
5354

5455
/// The "type" of module.
5556
public let type: ModuleType
@@ -58,8 +59,7 @@ public class Module: ModuleProtocol {
5859
public let sources: Sources
5960

6061
public init(name: String, type: ModuleType, sources: Sources, isTest: Bool = false) throws {
61-
// Append TestSuite to name if its a test module.
62-
self.name = name + (isTest ? testModuleNameSuffix : "")
62+
self.name = name
6363
self.type = type
6464
self.sources = sources
6565
self.dependencies = []
@@ -79,8 +79,8 @@ public class Module: ModuleProtocol {
7979
guard isTest else {
8080
fatalError("\(self.dynamicType) should be a test module to access basename.")
8181
}
82-
precondition(name.hasSuffix(testModuleNameSuffix))
83-
return name[name.startIndex..<name.index(name.endIndex, offsetBy: -testModuleNameSuffix.characters.count)]
82+
precondition(name.hasSuffix(Module.testModuleNameSuffix))
83+
return name[name.startIndex..<name.index(name.endIndex, offsetBy: -Module.testModuleNameSuffix.characters.count)]
8484
}
8585
}
8686

Tests/Functional/SwiftPMXCTestHelperTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ class SwiftPMXCTestHelperTests: XCTestCase {
2626
let testCases = ["name": "All Tests", "tests": [["name" : "SwiftPMXCTestHelperTests.xctest",
2727
"tests": [
2828
[
29-
"name": "Objc",
29+
"name": "ObjCTests",
3030
"tests": [["name": "test_example"], ["name": "testThisThing"]]
3131
],
3232
[
33-
"name": "SwiftPMXCTestHelperTestSuite.SwiftPMXCTestHelperTests1",
33+
"name": "SwiftPMXCTestHelperTests.SwiftPMXCTestHelperTests1",
3434
"tests": [["name": "test_Example2"], ["name": "testExample1"]]
3535
]
3636
]]]

Tests/PackageLoading/ConventionTests.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,17 +99,17 @@ class ConventionTests: XCTestCase {
9999
func testCInTests() throws {
100100
var fs = InMemoryFileSystem()
101101
try fs.createEmptyFiles("/Sources/main.swift",
102-
"/Tests/MyPackage/abc.c")
102+
"/Tests/MyPackageTests/abc.c")
103103

104104
PackageBuilderTester("MyPackage", in: fs) { result in
105105
result.checkModule("MyPackage") { moduleResult in
106106
moduleResult.check(type: .executable, isTest: false)
107107
moduleResult.checkSources(root: "/Sources", paths: "main.swift")
108108
}
109109

110-
result.checkModule("MyPackageTestSuite") { moduleResult in
110+
result.checkModule("MyPackageTests") { moduleResult in
111111
moduleResult.check(type: .library, isTest: true)
112-
moduleResult.checkSources(root: "/Tests/MyPackage", paths: "abc.c")
112+
moduleResult.checkSources(root: "/Tests/MyPackageTests", paths: "abc.c")
113113
}
114114

115115
#if os(Linux)

0 commit comments

Comments
 (0)