Test (#1935)
* Add test command and service * Fix warnings. * Run test with device and ios specified. * Prefer booted device. * Recognize host application. * Add AppCore framework for tests fixture. * Add Derived folder to .gitignore. * Error when no available device was found. * Change input arguments. * Add TestServiceTests. * Add XcodeBuildController + SimulatorController tests. * Run only tests scheme with single test target. * Add test command documentation. * Add test acceptance tests. * Run swiftformat. * Rename --os-version argument to --os. * Add fixture for test command. * Fix compilation issues. Co-authored-by: Pedro Piñera <pedro@ppinera.es>
This commit is contained in:
parent
8f6fa475af
commit
c3de526717
|
@ -85,6 +85,7 @@ jobs:
|
|||
'scaffold',
|
||||
'up',
|
||||
'build',
|
||||
'test',
|
||||
]
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
"repositoryURL": "https://github.com/rnine/Checksum.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "cd1ae53384dd578a84a0afef492a4f5d6202b068",
|
||||
"revision": "9dde3d1d898a5074608a1420791ef0a80c2399f2",
|
||||
"version": "1.0.2"
|
||||
}
|
||||
},
|
||||
|
|
|
@ -19,6 +19,12 @@ public protocol BuildGraphInspecting {
|
|||
/// - graph: Dependency graph.
|
||||
func buildableTarget(scheme: Scheme, graph: Graph) -> Target?
|
||||
|
||||
/// From the list of testable targets of the given scheme, it returns the first one.
|
||||
/// - Parameters:
|
||||
/// - scheme: Scheme in which to look up the target.
|
||||
/// - graph: Dependency graph.
|
||||
func testableTarget(scheme: Scheme, graph: Graph) -> Target?
|
||||
|
||||
/// Given a graph, it returns a list of buildable schemes.
|
||||
/// - Parameter graph: Dependency graph.
|
||||
func buildableSchemes(graph: Graph) -> [Scheme]
|
||||
|
@ -27,6 +33,14 @@ public protocol BuildGraphInspecting {
|
|||
/// - Parameters:
|
||||
/// - graph: Dependency graph
|
||||
func buildableEntrySchemes(graph: Graph) -> [Scheme]
|
||||
|
||||
/// Given a graph, it returns a list of test schemes (those that include only one test target).
|
||||
/// - Parameter graph: Dependency graph.
|
||||
func testSchemes(graph: Graph) -> [Scheme]
|
||||
|
||||
/// Given a graph, it returns a list of testable schemes.
|
||||
/// - Parameter graph: Dependency graph.
|
||||
func testableSchemes(graph: Graph) -> [Scheme]
|
||||
}
|
||||
|
||||
public class BuildGraphInspector: BuildGraphInspecting {
|
||||
|
@ -53,11 +67,22 @@ public class BuildGraphInspector: BuildGraphInspecting {
|
|||
}
|
||||
|
||||
public func buildableTarget(scheme: Scheme, graph: Graph) -> Target? {
|
||||
if scheme.buildAction?.targets.count == 0 {
|
||||
guard
|
||||
scheme.buildAction?.targets.isEmpty == false,
|
||||
let buildTarget = scheme.buildAction?.targets.first
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
let buildTarget = scheme.buildAction!.targets.first!
|
||||
return graph.target(path: buildTarget.projectPath, name: buildTarget.name)!.target
|
||||
|
||||
return graph.target(path: buildTarget.projectPath, name: buildTarget.name)?.target
|
||||
}
|
||||
|
||||
public func testableTarget(scheme: Scheme, graph: Graph) -> Target? {
|
||||
if scheme.testAction?.targets.count == 0 {
|
||||
return nil
|
||||
}
|
||||
let testTarget = scheme.testAction!.targets.first!
|
||||
return graph.target(path: testTarget.target.projectPath, name: testTarget.target.name)!.target
|
||||
}
|
||||
|
||||
public func buildableSchemes(graph: Graph) -> [Scheme] {
|
||||
|
@ -74,6 +99,25 @@ public class BuildGraphInspector: BuildGraphInspecting {
|
|||
.sorted(by: { $0.name < $1.name })
|
||||
}
|
||||
|
||||
public func testableSchemes(graph: Graph) -> [Scheme] {
|
||||
graph.schemes
|
||||
.filter { $0.testAction?.targets.isEmpty == false }
|
||||
.sorted(by: { $0.name < $1.name })
|
||||
}
|
||||
|
||||
public func testSchemes(graph: Graph) -> [Scheme] {
|
||||
graph.targets.values.flatMap { target -> [Scheme] in
|
||||
target
|
||||
.filter { $0.target.product == .unitTests || $0.target.product == .uiTests }
|
||||
.flatMap { target -> [Scheme] in
|
||||
target.project.schemes
|
||||
.filter { $0.targetDependencies().map(\.name) == [target.name] }
|
||||
}
|
||||
}
|
||||
.filter { $0.testAction?.targets.isEmpty == false }
|
||||
.sorted(by: { $0.name < $1.name })
|
||||
}
|
||||
|
||||
public func workspacePath(directory: AbsolutePath) throws -> AbsolutePath? {
|
||||
try directory.glob("**/*.xcworkspace")
|
||||
.filter {
|
||||
|
|
|
@ -53,6 +53,40 @@ public final class XcodeBuildController: XcodeBuildControlling {
|
|||
return run(command: command)
|
||||
}
|
||||
|
||||
public func test(
|
||||
_ target: XcodeBuildTarget,
|
||||
scheme: String,
|
||||
clean: Bool = false,
|
||||
destination: XcodeBuildDestination,
|
||||
arguments: [XcodeBuildArgument]
|
||||
) -> Observable<SystemEvent<XcodeBuildOutput>> {
|
||||
var command = ["/usr/bin/xcrun", "xcodebuild"]
|
||||
|
||||
// Action
|
||||
if clean {
|
||||
command.append("clean")
|
||||
}
|
||||
command.append("test")
|
||||
|
||||
// Scheme
|
||||
command.append(contentsOf: ["-scheme", scheme])
|
||||
|
||||
// Target
|
||||
command.append(contentsOf: target.xcodebuildArguments)
|
||||
|
||||
// Arguments
|
||||
command.append(contentsOf: arguments.flatMap { $0.arguments })
|
||||
|
||||
switch destination {
|
||||
case let .device(udid):
|
||||
command.append(contentsOf: ["-destination", "id=\(udid)"])
|
||||
case .mac:
|
||||
break
|
||||
}
|
||||
|
||||
return run(command: command)
|
||||
}
|
||||
|
||||
public func archive(_ target: XcodeBuildTarget,
|
||||
scheme: String,
|
||||
clean: Bool,
|
||||
|
|
|
@ -43,4 +43,27 @@ public final class MockBuildGraphInspector: BuildGraphInspecting {
|
|||
return []
|
||||
}
|
||||
}
|
||||
|
||||
public var testableTargetStub: ((Scheme, Graph) -> Target?)?
|
||||
public func testableTarget(scheme: Scheme, graph: Graph) -> Target? {
|
||||
if let testableTargetStub = testableTargetStub {
|
||||
return testableTargetStub(scheme, graph)
|
||||
} else {
|
||||
return Target.test()
|
||||
}
|
||||
}
|
||||
|
||||
public var testableSchemesStub: ((Graph) -> [Scheme])?
|
||||
public func testableSchemes(graph: Graph) -> [Scheme] {
|
||||
if let testableSchemesStub = testableSchemesStub {
|
||||
return testableSchemesStub(graph)
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
public var testSchemesStub: ((Graph) -> [Scheme])?
|
||||
public func testSchemes(graph: Graph) -> [Scheme] {
|
||||
testSchemesStub?(graph) ?? []
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,11 @@ import RxSwift
|
|||
import TSCBasic
|
||||
import TuistSupport
|
||||
|
||||
public enum XcodeBuildDestination: Equatable {
|
||||
case device(String)
|
||||
case mac
|
||||
}
|
||||
|
||||
public protocol XcodeBuildControlling {
|
||||
/// Returns an observable to build the given project using xcodebuild.
|
||||
/// - Parameters:
|
||||
|
@ -15,6 +20,20 @@ public protocol XcodeBuildControlling {
|
|||
clean: Bool,
|
||||
arguments: [XcodeBuildArgument]) -> Observable<SystemEvent<XcodeBuildOutput>>
|
||||
|
||||
/// Returns an observable to test the given project using xcodebuild.
|
||||
/// - Parameters:
|
||||
/// - target: The project or workspace to be built.
|
||||
/// - scheme: The scheme of the project that should be built.
|
||||
/// - clean: True if xcodebuild should clean the project before building.
|
||||
/// - arguments: Extra xcodebuild arguments.
|
||||
func test(
|
||||
_ target: XcodeBuildTarget,
|
||||
scheme: String,
|
||||
clean: Bool,
|
||||
destination: XcodeBuildDestination,
|
||||
arguments: [XcodeBuildArgument]
|
||||
) -> Observable<SystemEvent<XcodeBuildOutput>>
|
||||
|
||||
/// Returns an observable that archives the given project using xcodebuild.
|
||||
/// - Parameters:
|
||||
/// - target: The project or workspace to be archived.
|
||||
|
|
|
@ -551,6 +551,12 @@ public class Graph: Encodable, Equatable {
|
|||
} ?? nil
|
||||
}
|
||||
|
||||
/// - Returns: Host application for a given `targetNode`, if it exists
|
||||
public func hostApplication(for targetNode: TargetNode) -> TargetNode? {
|
||||
targetDependencies(path: targetNode.path, name: targetNode.name)
|
||||
.first(where: { $0.target.product == .app })
|
||||
}
|
||||
|
||||
/// Returns a copy of the graph with the given projects set.
|
||||
/// - Parameter projects: Projects to be set to the copy.
|
||||
public func with(projects: [Project]) -> Graph {
|
||||
|
@ -643,11 +649,6 @@ public class Graph: Encodable, Equatable {
|
|||
.product(target: targetNode.target.name, productName: targetNode.target.productNameWithExtension)
|
||||
}
|
||||
|
||||
fileprivate func hostApplication(for targetNode: TargetNode) -> TargetNode? {
|
||||
targetDependencies(path: targetNode.path, name: targetNode.name)
|
||||
.first(where: { $0.target.product == .app })
|
||||
}
|
||||
|
||||
fileprivate func isStaticLibrary(targetNode: TargetNode) -> Bool {
|
||||
targetNode.target.product.isStatic
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import Foundation
|
||||
import RxSwift
|
||||
import struct TSCUtility.Version
|
||||
import TuistSupport
|
||||
|
||||
public protocol SimulatorControlling {
|
||||
|
@ -9,22 +10,43 @@ public protocol SimulatorControlling {
|
|||
/// Returns the list of simulator runtimes that are available in the system.
|
||||
func runtimes() -> Single<[SimulatorRuntime]>
|
||||
|
||||
/// Returns the list of simulator devices and runtimes.
|
||||
/// - Parameters:
|
||||
/// - platform: Optionally filter by platform
|
||||
/// - deviceName: Optionally filter by device name
|
||||
/// - Returns: the list of simulator devices and runtimes.
|
||||
func devicesAndRuntimes() -> Single<[SimulatorDeviceAndRuntime]>
|
||||
|
||||
/// Finds first available device defined by given parameters
|
||||
/// - Parameters:
|
||||
/// - platform: Given platform
|
||||
/// - version: Specific version, ignored if nil
|
||||
/// - minVersion: Minimum version of the OS
|
||||
/// - deviceName: Specific device name (eg. iPhone X)
|
||||
func findAvailableDevice(
|
||||
platform: Platform,
|
||||
version: Version?,
|
||||
minVersion: Version?,
|
||||
deviceName: String?
|
||||
) -> Single<SimulatorDevice>
|
||||
}
|
||||
|
||||
public enum SimulatorControllerError: FatalError {
|
||||
case simctlError(String)
|
||||
case deviceNotFound(Platform, Version?, String?, [SimulatorDeviceAndRuntime])
|
||||
|
||||
public var type: ErrorType {
|
||||
switch self {
|
||||
case .simctlError: return .abort
|
||||
case .simctlError, .deviceNotFound:
|
||||
return .abort
|
||||
}
|
||||
}
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case let .simctlError(output): return output
|
||||
case let .simctlError(output):
|
||||
return output
|
||||
case let .deviceNotFound(platform, version, deviceName, devices):
|
||||
return "Could not find a suitable device for \(platform.caseValue)\(version.map { " \($0)" } ?? "")\(deviceName.map { ", device name \($0)" } ?? ""). Did find \(devices.map { "\($0.device.name) (\($0.runtime.description))" }.joined(separator: ", "))"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -101,4 +123,36 @@ public final class SimulatorController: SimulatorControlling {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func findAvailableDevice(
|
||||
platform: Platform,
|
||||
version: Version?,
|
||||
minVersion: Version?,
|
||||
deviceName: String?
|
||||
) -> Single<SimulatorDevice> {
|
||||
devicesAndRuntimes()
|
||||
.flatMap { devicesAndRuntimes in
|
||||
let availableDevices = devicesAndRuntimes
|
||||
.filter { simulatorDeviceAndRuntime in
|
||||
let nameComponents = simulatorDeviceAndRuntime.runtime.name.components(separatedBy: " ")
|
||||
guard nameComponents.first == platform.caseValue else { return false }
|
||||
let deviceVersion = nameComponents.last?.version()
|
||||
if let version = version {
|
||||
guard deviceVersion == version else { return false }
|
||||
} else if let minVersion = minVersion, let deviceVersion = deviceVersion {
|
||||
guard deviceVersion >= minVersion else { return false }
|
||||
}
|
||||
if let deviceName = deviceName {
|
||||
guard simulatorDeviceAndRuntime.device.name == deviceName else { return false }
|
||||
}
|
||||
return true
|
||||
}
|
||||
.map(\.device)
|
||||
guard
|
||||
let device = availableDevices.first(where: { !$0.isShutdown }) ?? availableDevices.first
|
||||
else { return .error(SimulatorControllerError.deviceNotFound(platform, version, deviceName, devicesAndRuntimes)) }
|
||||
|
||||
return .just(device)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,16 +39,17 @@ public struct SimulatorDevice: Decodable, Hashable, CustomStringConvertible {
|
|||
name
|
||||
}
|
||||
|
||||
public init(dataPath: AbsolutePath,
|
||||
logPath: AbsolutePath,
|
||||
udid: String,
|
||||
isAvailable: Bool,
|
||||
deviceTypeIdentifier: String,
|
||||
state: String,
|
||||
name: String,
|
||||
availabilityError: String?,
|
||||
runtimeIdentifier: String)
|
||||
{
|
||||
public init(
|
||||
dataPath: AbsolutePath,
|
||||
logPath: AbsolutePath,
|
||||
udid: String,
|
||||
isAvailable: Bool,
|
||||
deviceTypeIdentifier: String,
|
||||
state: String,
|
||||
name: String,
|
||||
availabilityError: String?,
|
||||
runtimeIdentifier: String
|
||||
) {
|
||||
self.dataPath = dataPath
|
||||
self.logPath = logPath
|
||||
self.udid = udid
|
||||
|
|
|
@ -25,14 +25,15 @@ public struct SimulatorRuntime: Decodable, Hashable, CustomStringConvertible {
|
|||
// Name of the runtime (e.g. iOS 13.5)
|
||||
public let name: String
|
||||
|
||||
init(bundlePath: AbsolutePath,
|
||||
buildVersion: String,
|
||||
runtimeRoot: AbsolutePath,
|
||||
identifier: String,
|
||||
version: SimulatorRuntimeVersion,
|
||||
isAvailable: Bool,
|
||||
name: String)
|
||||
{
|
||||
public init(
|
||||
bundlePath: AbsolutePath,
|
||||
buildVersion: String,
|
||||
runtimeRoot: AbsolutePath,
|
||||
identifier: String,
|
||||
version: SimulatorRuntimeVersion,
|
||||
isAvailable: Bool,
|
||||
name: String
|
||||
) {
|
||||
self.bundlePath = bundlePath
|
||||
self.buildVersion = buildVersion
|
||||
self.runtimeRoot = runtimeRoot
|
||||
|
|
|
@ -19,6 +19,21 @@ final class MockXcodeBuildController: XcodeBuildControlling {
|
|||
}
|
||||
}
|
||||
|
||||
var testStub: ((XcodeBuildTarget, String, Bool, XcodeBuildDestination, [XcodeBuildArgument]) -> Observable<SystemEvent<XcodeBuildOutput>>)?
|
||||
func test(
|
||||
_ target: XcodeBuildTarget,
|
||||
scheme: String,
|
||||
clean: Bool,
|
||||
destination: XcodeBuildDestination,
|
||||
arguments: [XcodeBuildArgument]
|
||||
) -> Observable<SystemEvent<XcodeBuildOutput>> {
|
||||
if let testStub = testStub {
|
||||
return testStub(target, scheme, clean, destination, arguments)
|
||||
} else {
|
||||
return Observable.error(TestError("\(String(describing: MockXcodeBuildController.self)) received an unexpected call to test"))
|
||||
}
|
||||
}
|
||||
|
||||
var archiveStub: ((XcodeBuildTarget, String, Bool, AbsolutePath, [XcodeBuildArgument]) -> Observable<SystemEvent<XcodeBuildOutput>>)?
|
||||
func archive(_ target: XcodeBuildTarget,
|
||||
scheme: String,
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import Foundation
|
||||
import RxSwift
|
||||
import struct TSCUtility.Version
|
||||
import TuistCore
|
||||
import TuistSupport
|
||||
|
||||
@testable import TuistCore
|
||||
|
@ -43,4 +45,9 @@ public final class MockSimulatorController: SimulatorControlling {
|
|||
return .error(TestError("call to non-stubbed method runtimesAndDevices"))
|
||||
}
|
||||
}
|
||||
|
||||
public var findAvailableDeviceStub: ((Platform, Version?, Version?, String?) -> Single<SimulatorDevice>)?
|
||||
public func findAvailableDevice(platform: Platform, version: Version?, minVersion: Version?, deviceName: String?) -> Single<SimulatorDevice> {
|
||||
findAvailableDeviceStub?(platform, version, minVersion, deviceName) ?? .just(SimulatorDevice.test())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
import ArgumentParser
|
||||
import Foundation
|
||||
import TSCBasic
|
||||
import TuistSupport
|
||||
|
||||
/// Command that tests a target from the project in the current directory.
|
||||
struct TestCommand: ParsableCommand {
|
||||
static var configuration: CommandConfiguration {
|
||||
CommandConfiguration(commandName: "test",
|
||||
abstract: "Tests a project")
|
||||
}
|
||||
|
||||
@Argument(
|
||||
help: "The scheme to be tested. By default it tests all the testable targets of the project in the current directory."
|
||||
)
|
||||
var scheme: String?
|
||||
|
||||
@Flag(
|
||||
help: "Force the generation of the project before testing."
|
||||
)
|
||||
var generate: Bool = false
|
||||
|
||||
@Flag(
|
||||
help: "When passed, it cleans the project before testing it."
|
||||
)
|
||||
var clean: Bool = false
|
||||
|
||||
@Option(
|
||||
name: .shortAndLong,
|
||||
help: "The path to the directory that contains the project to be tested."
|
||||
)
|
||||
var path: String?
|
||||
|
||||
@Option(
|
||||
name: .shortAndLong,
|
||||
help: "Test on a specific device."
|
||||
)
|
||||
var device: String?
|
||||
|
||||
@Option(
|
||||
name: .shortAndLong,
|
||||
help: "Test with a specific version of the OS."
|
||||
)
|
||||
var os: String?
|
||||
|
||||
@Option(
|
||||
name: [.long, .customShort("C")],
|
||||
help: "The configuration to be used when testing the scheme."
|
||||
)
|
||||
var configuration: String?
|
||||
|
||||
func run() throws {
|
||||
let absolutePath: AbsolutePath
|
||||
if let path = path {
|
||||
absolutePath = AbsolutePath(path)
|
||||
} else {
|
||||
absolutePath = FileHandler.shared.currentPath
|
||||
}
|
||||
try TestService().run(
|
||||
schemeName: scheme,
|
||||
generate: generate,
|
||||
clean: clean,
|
||||
configuration: configuration,
|
||||
path: absolutePath,
|
||||
deviceName: device,
|
||||
osVersion: os
|
||||
)
|
||||
}
|
||||
}
|
|
@ -19,6 +19,7 @@ public struct TuistCommand: ParsableCommand {
|
|||
LintCommand.self,
|
||||
VersionCommand.self,
|
||||
BuildCommand.self,
|
||||
TestCommand.self,
|
||||
CreateIssueCommand.self,
|
||||
ScaffoldCommand.self,
|
||||
InitCommand.self,
|
||||
|
|
|
@ -0,0 +1,171 @@
|
|||
import Foundation
|
||||
import RxBlocking
|
||||
import TSCBasic
|
||||
import struct TSCUtility.Version
|
||||
import TuistAutomation
|
||||
import TuistCore
|
||||
import TuistSupport
|
||||
|
||||
enum TestServiceError: FatalError {
|
||||
case schemeNotFound(scheme: String, existing: [String])
|
||||
case schemeWithoutTestableTargets(scheme: String)
|
||||
|
||||
// Error description
|
||||
var description: String {
|
||||
switch self {
|
||||
case let .schemeNotFound(scheme, existing):
|
||||
return "Couldn't find scheme \(scheme). The available schemes are: \(existing.joined(separator: ", "))."
|
||||
case let .schemeWithoutTestableTargets(scheme):
|
||||
return "The scheme \(scheme) cannot be built because it contains no buildable targets."
|
||||
}
|
||||
}
|
||||
|
||||
// Error type
|
||||
var type: ErrorType {
|
||||
switch self {
|
||||
case .schemeNotFound:
|
||||
return .abort
|
||||
case .schemeWithoutTestableTargets:
|
||||
return .abort
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class TestService {
|
||||
/// Project generator
|
||||
let generator: Generating
|
||||
|
||||
/// Xcode build controller.
|
||||
let xcodebuildController: XcodeBuildControlling
|
||||
|
||||
/// Build graph inspector.
|
||||
let buildGraphInspector: BuildGraphInspecting
|
||||
|
||||
/// Simulator controller
|
||||
let simulatorController: SimulatorControlling
|
||||
|
||||
init(
|
||||
generator: Generating = Generator(),
|
||||
xcodebuildController: XcodeBuildControlling = XcodeBuildController(),
|
||||
buildGraphInspector: BuildGraphInspecting = BuildGraphInspector(),
|
||||
simulatorController: SimulatorControlling = SimulatorController()
|
||||
) {
|
||||
self.generator = generator
|
||||
self.xcodebuildController = xcodebuildController
|
||||
self.buildGraphInspector = buildGraphInspector
|
||||
self.simulatorController = simulatorController
|
||||
}
|
||||
|
||||
func run(
|
||||
schemeName: String?,
|
||||
generate: Bool,
|
||||
clean: Bool,
|
||||
configuration: String?,
|
||||
path: AbsolutePath,
|
||||
deviceName: String?,
|
||||
osVersion: String?
|
||||
) throws {
|
||||
let graph: Graph
|
||||
if try (generate || buildGraphInspector.workspacePath(directory: path) == nil) {
|
||||
graph = try generator.generateWithGraph(path: path, projectOnly: false).1
|
||||
} else {
|
||||
graph = try generator.load(path: path)
|
||||
}
|
||||
|
||||
let version = osVersion?.version()
|
||||
|
||||
let testableSchemes = buildGraphInspector.testableSchemes(graph: graph)
|
||||
logger.log(
|
||||
level: .debug,
|
||||
"Found the following testable schemes: \(testableSchemes.map(\.name).joined(separator: ", "))"
|
||||
)
|
||||
|
||||
if let schemeName = schemeName {
|
||||
guard let scheme = testableSchemes.first(where: { $0.name == schemeName }) else {
|
||||
throw TestServiceError.schemeNotFound(scheme: schemeName, existing: testableSchemes.map(\.name))
|
||||
}
|
||||
try testScheme(
|
||||
scheme: scheme,
|
||||
graph: graph,
|
||||
path: path,
|
||||
clean: clean,
|
||||
configuration: configuration,
|
||||
version: version,
|
||||
deviceName: deviceName
|
||||
)
|
||||
} else {
|
||||
var cleaned: Bool = false
|
||||
let testSchemes = buildGraphInspector.testSchemes(graph: graph)
|
||||
try testSchemes.forEach {
|
||||
try testScheme(
|
||||
scheme: $0,
|
||||
graph: graph,
|
||||
path: path,
|
||||
clean: !cleaned && clean,
|
||||
configuration: configuration,
|
||||
version: version,
|
||||
deviceName: deviceName
|
||||
)
|
||||
cleaned = true
|
||||
}
|
||||
}
|
||||
|
||||
logger.log(level: .notice, "The project tests ran successfully", metadata: .success)
|
||||
}
|
||||
|
||||
// MARK: - private
|
||||
|
||||
private func testScheme(
|
||||
scheme: Scheme,
|
||||
graph: Graph,
|
||||
path: AbsolutePath,
|
||||
clean: Bool,
|
||||
configuration: String?,
|
||||
version: Version?,
|
||||
deviceName: String?
|
||||
) throws {
|
||||
logger.log(level: .notice, "Testing scheme \(scheme.name)", metadata: .section)
|
||||
guard let buildableTarget = buildGraphInspector.testableTarget(scheme: scheme, graph: graph) else {
|
||||
throw TestServiceError.schemeWithoutTestableTargets(scheme: scheme.name)
|
||||
}
|
||||
|
||||
let destination: XcodeBuildDestination
|
||||
switch buildableTarget.platform {
|
||||
case .iOS, .tvOS, .watchOS:
|
||||
let minVersion: Version?
|
||||
if let deploymentTarget = buildableTarget.deploymentTarget {
|
||||
minVersion = deploymentTarget.version.version()
|
||||
} else {
|
||||
minVersion = scheme.targetDependencies()
|
||||
.compactMap { graph.findTargetNode(path: $0.projectPath, name: $0.name) }
|
||||
.flatMap { $0.targetDependencies.compactMap { $0.target.deploymentTarget?.version } }
|
||||
.compactMap { $0.version() }
|
||||
.sorted()
|
||||
.first
|
||||
}
|
||||
let device = try simulatorController.findAvailableDevice(
|
||||
platform: buildableTarget.platform,
|
||||
version: version,
|
||||
minVersion: minVersion,
|
||||
deviceName: deviceName
|
||||
)
|
||||
.toBlocking()
|
||||
.single()
|
||||
destination = .device(device.udid)
|
||||
case .macOS:
|
||||
destination = .mac
|
||||
}
|
||||
|
||||
let workspacePath = try buildGraphInspector.workspacePath(directory: path)!
|
||||
_ = try xcodebuildController.test(
|
||||
.workspace(workspacePath),
|
||||
scheme: scheme.name,
|
||||
clean: clean,
|
||||
destination: destination,
|
||||
arguments: buildGraphInspector.buildArguments(target: buildableTarget, configuration: configuration)
|
||||
)
|
||||
.printFormattedOutput()
|
||||
.toBlocking()
|
||||
.last()
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import Foundation
|
||||
import struct TSCUtility.Version
|
||||
|
||||
extension String {
|
||||
// swiftlint:disable:next force_try
|
||||
|
@ -69,6 +70,14 @@ extension String {
|
|||
length: utf16.distance(from: from, to: to))
|
||||
}
|
||||
|
||||
public func version() -> Version? {
|
||||
if components(separatedBy: ".").count == 2 {
|
||||
return Version(string: self + ".0")
|
||||
} else {
|
||||
return Version(string: self)
|
||||
}
|
||||
}
|
||||
|
||||
public func capitalizingFirstLetter() -> String {
|
||||
prefix(1).capitalized + dropFirst()
|
||||
}
|
||||
|
|
|
@ -48,4 +48,19 @@ final class SimulatorControllerIntegrationTests: TuistTestCase {
|
|||
let runtimes = try XCTUnwrap(got)
|
||||
XCTAssertNotEmpty(runtimes)
|
||||
}
|
||||
|
||||
func test_findAvailableDevice() throws {
|
||||
// When
|
||||
let got = try subject.findAvailableDevice(
|
||||
platform: .iOS,
|
||||
version: nil,
|
||||
minVersion: nil,
|
||||
deviceName: nil
|
||||
)
|
||||
.toBlocking()
|
||||
.single()
|
||||
|
||||
// Then
|
||||
XCTAssertTrue(got.isAvailable)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -156,6 +156,138 @@ final class BuildGraphInspectorTests: TuistUnitTestCase {
|
|||
)
|
||||
}
|
||||
|
||||
func test_testSchemes() throws {
|
||||
// Given
|
||||
let path = try temporaryPath()
|
||||
let projectPath = path.appending(component: "Project.xcodeproj")
|
||||
let coreProjectPath = path.appending(component: "CoreProject.xcodeproj")
|
||||
let coreScheme = Scheme.test(
|
||||
name: "Core",
|
||||
testAction: .test(
|
||||
targets: [.init(target: .init(projectPath: projectPath, name: "CoreTests"))]
|
||||
)
|
||||
)
|
||||
let coreTestsScheme = Scheme(
|
||||
name: "CoreTests",
|
||||
testAction: .test(
|
||||
targets: [.init(target: .init(projectPath: projectPath, name: "CoreTests"))]
|
||||
)
|
||||
)
|
||||
let kitScheme = Scheme.test(
|
||||
name: "Kit",
|
||||
testAction: .test(
|
||||
targets: [.init(target: .init(projectPath: projectPath, name: "KitTests"))]
|
||||
)
|
||||
)
|
||||
let kitTestsScheme = Scheme(
|
||||
name: "KitTests",
|
||||
testAction: .test(
|
||||
targets: [.init(target: .init(projectPath: projectPath, name: "KitTests"))]
|
||||
)
|
||||
)
|
||||
let coreTarget = Target.test(name: "Core")
|
||||
let coreProject = Project.test(
|
||||
path: coreProjectPath,
|
||||
schemes: [coreScheme, coreTestsScheme]
|
||||
)
|
||||
let coreTargetNode = TargetNode.test(
|
||||
project: coreProject,
|
||||
target: coreTarget
|
||||
)
|
||||
let coreTestsTarget = Target.test(
|
||||
name: "CoreTests",
|
||||
product: .unitTests,
|
||||
dependencies: [.target(name: "Core")]
|
||||
)
|
||||
let coreTestsTargetNode = TargetNode.test(
|
||||
project: coreProject,
|
||||
target: coreTestsTarget
|
||||
)
|
||||
let kitTarget = Target.test(name: "Kit", dependencies: [.target(name: "Core")])
|
||||
let kitProject = Project.test(
|
||||
path: projectPath,
|
||||
schemes: [kitScheme, kitTestsScheme]
|
||||
)
|
||||
let kitTargetNode = TargetNode.test(
|
||||
project: kitProject,
|
||||
target: kitTarget
|
||||
)
|
||||
let kitTestsTarget = Target.test(
|
||||
name: "KitTests",
|
||||
product: .unitTests,
|
||||
dependencies: [.target(name: "Kit")]
|
||||
)
|
||||
let kitTestsTargetNode = TargetNode.test(
|
||||
project: kitProject,
|
||||
target: kitTestsTarget
|
||||
)
|
||||
let graph = Graph.test(
|
||||
entryNodes: [kitTargetNode],
|
||||
targets: [
|
||||
projectPath: [kitTargetNode, kitTestsTargetNode],
|
||||
coreProjectPath: [coreTargetNode, coreTestsTargetNode],
|
||||
]
|
||||
)
|
||||
|
||||
// When
|
||||
let got = subject.testSchemes(graph: graph)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(
|
||||
got,
|
||||
[
|
||||
coreTestsScheme,
|
||||
kitTestsScheme,
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
func test_testableSchemes() throws {
|
||||
// Given
|
||||
let path = try temporaryPath()
|
||||
let projectPath = path.appending(component: "Project.xcodeproj")
|
||||
let coreProjectPath = path.appending(component: "CoreProject.xcodeproj")
|
||||
let coreScheme = Scheme.test(
|
||||
name: "Core",
|
||||
testAction: .test(
|
||||
targets: [.init(target: .init(projectPath: projectPath, name: "CoreTests"))]
|
||||
)
|
||||
)
|
||||
let coreTestsScheme = Scheme(
|
||||
name: "CoreTests",
|
||||
testAction: .test(
|
||||
targets: [.init(target: .init(projectPath: projectPath, name: "CoreTests"))]
|
||||
)
|
||||
)
|
||||
let coreTarget = Target.test(name: "Core")
|
||||
let coreProject = Project.test(
|
||||
path: coreProjectPath,
|
||||
schemes: [coreScheme, coreTestsScheme]
|
||||
)
|
||||
let coreTargetNode = TargetNode.test(
|
||||
project: coreProject,
|
||||
target: coreTarget
|
||||
)
|
||||
let graph = Graph.test(
|
||||
entryNodes: [coreTargetNode],
|
||||
projects: [
|
||||
coreProject,
|
||||
]
|
||||
)
|
||||
|
||||
// When
|
||||
let got = subject.testableSchemes(graph: graph)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(
|
||||
got,
|
||||
[
|
||||
coreScheme,
|
||||
coreTestsScheme,
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
func test_buildableEntrySchemes_only_includes_entryTargets() throws {
|
||||
// Given
|
||||
let path = try temporaryPath()
|
||||
|
|
|
@ -68,4 +68,105 @@ final class XcodeBuildControllerTests: TuistUnitTestCase {
|
|||
XCTFail("The command was not expected to fail")
|
||||
}
|
||||
}
|
||||
|
||||
func test_test_when_device() throws {
|
||||
// Given
|
||||
let path = try temporaryPath()
|
||||
let xcworkspacePath = path.appending(component: "Project.xcworkspace")
|
||||
let target = XcodeBuildTarget.workspace(xcworkspacePath)
|
||||
let scheme = "Scheme"
|
||||
let shouldOutputBeColoured = true
|
||||
environment.shouldOutputBeColoured = shouldOutputBeColoured
|
||||
|
||||
var command = [
|
||||
"/usr/bin/xcrun",
|
||||
"xcodebuild",
|
||||
"clean",
|
||||
"test",
|
||||
"-scheme",
|
||||
scheme,
|
||||
]
|
||||
command.append(contentsOf: target.xcodebuildArguments)
|
||||
command.append(contentsOf: ["-destination", "id=device-id"])
|
||||
|
||||
system.succeedCommand(command, output: "output")
|
||||
var parseCalls: [(String, Bool)] = []
|
||||
parser.parseStub = { output, colored in
|
||||
parseCalls.append((output, colored))
|
||||
return "formated-output"
|
||||
}
|
||||
|
||||
// When
|
||||
let events = subject.test(
|
||||
target,
|
||||
scheme: scheme,
|
||||
clean: true,
|
||||
destination: .device("device-id"),
|
||||
arguments: []
|
||||
)
|
||||
.toBlocking()
|
||||
.materialize()
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(parseCalls.count, 1)
|
||||
XCTAssertEqual(parseCalls.first?.0, "output")
|
||||
XCTAssertEqual(parseCalls.first?.1, shouldOutputBeColoured)
|
||||
|
||||
switch events {
|
||||
case let .completed(output):
|
||||
XCTAssertEqual(output, [.standardOutput(XcodeBuildOutput(raw: "output\n", formatted: "formated-output\n"))])
|
||||
case .failed:
|
||||
XCTFail("The command was not expected to fail")
|
||||
}
|
||||
}
|
||||
|
||||
func test_test_when_mac() throws {
|
||||
// Given
|
||||
let path = try temporaryPath()
|
||||
let xcworkspacePath = path.appending(component: "Project.xcworkspace")
|
||||
let target = XcodeBuildTarget.workspace(xcworkspacePath)
|
||||
let scheme = "Scheme"
|
||||
let shouldOutputBeColoured = true
|
||||
environment.shouldOutputBeColoured = shouldOutputBeColoured
|
||||
|
||||
var command = [
|
||||
"/usr/bin/xcrun",
|
||||
"xcodebuild",
|
||||
"clean",
|
||||
"test",
|
||||
"-scheme",
|
||||
scheme,
|
||||
]
|
||||
command.append(contentsOf: target.xcodebuildArguments)
|
||||
|
||||
system.succeedCommand(command, output: "output")
|
||||
var parseCalls: [(String, Bool)] = []
|
||||
parser.parseStub = { output, colored in
|
||||
parseCalls.append((output, colored))
|
||||
return "formated-output"
|
||||
}
|
||||
|
||||
// When
|
||||
let events = subject.test(
|
||||
target,
|
||||
scheme: scheme,
|
||||
clean: true,
|
||||
destination: .mac,
|
||||
arguments: []
|
||||
)
|
||||
.toBlocking()
|
||||
.materialize()
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(parseCalls.count, 1)
|
||||
XCTAssertEqual(parseCalls.first?.0, "output")
|
||||
XCTAssertEqual(parseCalls.first?.1, shouldOutputBeColoured)
|
||||
|
||||
switch events {
|
||||
case let .completed(output):
|
||||
XCTAssertEqual(output, [.standardOutput(XcodeBuildOutput(raw: "output\n", formatted: "formated-output\n"))])
|
||||
case .failed:
|
||||
XCTFail("The command was not expected to fail")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,204 @@
|
|||
import Foundation
|
||||
import RxSwift
|
||||
import TSCBasic
|
||||
import TuistAutomation
|
||||
import TuistCore
|
||||
import XCTest
|
||||
|
||||
@testable import TuistAutomationTesting
|
||||
@testable import TuistCoreTesting
|
||||
@testable import TuistKit
|
||||
@testable import TuistSupportTesting
|
||||
|
||||
final class TestServiceTests: TuistUnitTestCase {
|
||||
private var subject: TestService!
|
||||
private var generator: MockGenerator!
|
||||
private var xcodebuildController: MockXcodeBuildController!
|
||||
private var buildGraphInspector: MockBuildGraphInspector!
|
||||
private var simulatorController: MockSimulatorController!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
generator = .init()
|
||||
xcodebuildController = .init()
|
||||
buildGraphInspector = .init()
|
||||
simulatorController = .init()
|
||||
|
||||
subject = TestService(
|
||||
generator: generator,
|
||||
xcodebuildController: xcodebuildController,
|
||||
buildGraphInspector: buildGraphInspector,
|
||||
simulatorController: simulatorController
|
||||
)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
generator = nil
|
||||
xcodebuildController = nil
|
||||
buildGraphInspector = nil
|
||||
simulatorController = nil
|
||||
subject = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func test_run_when_the_project_is_already_generated() throws {
|
||||
// Given
|
||||
let path = try temporaryPath()
|
||||
let workspacePath = path.appending(component: "App.xcworkspace")
|
||||
let graph = Graph.test()
|
||||
let scheme = Scheme.test()
|
||||
let target = Target.test()
|
||||
let buildArguments: [XcodeBuildArgument] = [.sdk("iphoneos")]
|
||||
|
||||
generator.loadStub = { _path in
|
||||
XCTAssertEqual(_path, path)
|
||||
return graph
|
||||
}
|
||||
buildGraphInspector.testableSchemesStub = { _ in
|
||||
[scheme]
|
||||
}
|
||||
buildGraphInspector.testableTargetStub = { _scheme, _ in
|
||||
XCTAssertEqual(_scheme, scheme)
|
||||
return target
|
||||
}
|
||||
buildGraphInspector.workspacePathStub = { _path in
|
||||
XCTAssertEqual(_path, path)
|
||||
return workspacePath
|
||||
}
|
||||
buildGraphInspector.buildArgumentsStub = { _target, _ in
|
||||
XCTAssertEqual(_target, target)
|
||||
return buildArguments
|
||||
}
|
||||
|
||||
let availableDevice: SimulatorDevice = .test()
|
||||
simulatorController.findAvailableDeviceStub = { _, _, _, _ in
|
||||
.just(availableDevice)
|
||||
}
|
||||
xcodebuildController.testStub = { _target, _scheme, _clean, _destination, _arguments in
|
||||
XCTAssertEqual(_target, .workspace(workspacePath))
|
||||
XCTAssertEqual(_scheme, scheme.name)
|
||||
XCTAssertTrue(_clean)
|
||||
XCTAssertEqual(_arguments, buildArguments)
|
||||
XCTAssertEqual(_destination, .device(availableDevice.udid))
|
||||
return Observable.just(.standardOutput(.init(raw: "success", formatted: nil)))
|
||||
}
|
||||
|
||||
// Then
|
||||
try subject.testRun(
|
||||
schemeName: scheme.name,
|
||||
clean: true,
|
||||
path: path
|
||||
)
|
||||
}
|
||||
|
||||
func test_run_only_cleans_the_first_time() throws {
|
||||
// Given
|
||||
let path = try temporaryPath()
|
||||
let workspacePath = path.appending(component: "App.xcworkspace")
|
||||
let graph = Graph.test()
|
||||
let schemeA = Scheme.test(name: "A")
|
||||
let schemeB = Scheme.test(name: "B")
|
||||
let targetA = Target.test(name: "A")
|
||||
let targetB = Target.test(name: "B")
|
||||
let buildArguments: [XcodeBuildArgument] = [.sdk("iphoneos")]
|
||||
|
||||
generator.loadStub = { _path in
|
||||
XCTAssertEqual(_path, path)
|
||||
return graph
|
||||
}
|
||||
buildGraphInspector.buildableSchemesStub = { _ in
|
||||
[schemeA, schemeB]
|
||||
}
|
||||
buildGraphInspector.buildableTargetStub = { _scheme, _ in
|
||||
if _scheme == schemeA { return targetA }
|
||||
else if _scheme == schemeB { return targetB }
|
||||
else { XCTFail("unexpected scheme"); return targetA }
|
||||
}
|
||||
buildGraphInspector.workspacePathStub = { _path in
|
||||
XCTAssertEqual(_path, path)
|
||||
return workspacePath
|
||||
}
|
||||
buildGraphInspector.buildArgumentsStub = { _, _ in
|
||||
buildArguments
|
||||
}
|
||||
xcodebuildController.testStub = { _target, _scheme, _clean, _, _arguments in
|
||||
XCTAssertEqual(_target, .workspace(workspacePath))
|
||||
XCTAssertEqual(_arguments, buildArguments)
|
||||
|
||||
if _scheme == "A" {
|
||||
XCTAssertEqual(_scheme, "A")
|
||||
XCTAssertTrue(_clean)
|
||||
} else if _scheme == "B" {
|
||||
// When running the second scheme clean should be false
|
||||
XCTAssertEqual(_scheme, "B")
|
||||
XCTAssertFalse(_clean)
|
||||
} else {
|
||||
XCTFail("unexpected scheme \(_scheme)")
|
||||
}
|
||||
return Observable.just(.standardOutput(.init(raw: "success", formatted: nil)))
|
||||
}
|
||||
|
||||
// Then
|
||||
try subject.testRun(
|
||||
path: path
|
||||
)
|
||||
}
|
||||
|
||||
func test_run_lists_schemes() throws {
|
||||
// Given
|
||||
let path = try temporaryPath()
|
||||
let workspacePath = path.appending(component: "App.xcworkspace")
|
||||
let graph = Graph.test()
|
||||
let schemeA = Scheme.test(name: "A")
|
||||
let schemeB = Scheme.test(name: "B")
|
||||
generator.loadStub = { _path in
|
||||
XCTAssertEqual(_path, path)
|
||||
return graph
|
||||
}
|
||||
buildGraphInspector.workspacePathStub = { _path in
|
||||
XCTAssertEqual(_path, path)
|
||||
return workspacePath
|
||||
}
|
||||
buildGraphInspector.testableSchemesStub = { _ in
|
||||
[
|
||||
schemeA,
|
||||
schemeB,
|
||||
]
|
||||
}
|
||||
xcodebuildController.testStub = { _, _, _, _, _ in
|
||||
.just(.standardOutput(.init(raw: "success", formatted: nil)))
|
||||
}
|
||||
|
||||
// When
|
||||
try subject.testRun(
|
||||
path: path
|
||||
)
|
||||
|
||||
// Then
|
||||
XCTAssertPrinterContains("Found the following testable schemes: A, B", at: .debug, ==)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private extension TestService {
|
||||
func testRun(
|
||||
schemeName: String? = nil,
|
||||
generate: Bool = false,
|
||||
clean: Bool = false,
|
||||
configuration: String? = nil,
|
||||
path: AbsolutePath,
|
||||
deviceName: String? = nil,
|
||||
osVersion: String? = nil
|
||||
) throws {
|
||||
try run(
|
||||
schemeName: schemeName,
|
||||
generate: generate,
|
||||
clean: clean,
|
||||
configuration: configuration,
|
||||
path: path,
|
||||
deviceName: deviceName,
|
||||
osVersion: osVersion
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
Then(/^tuist tests the project$/) do
|
||||
system("swift", "run", "tuist", "test", "--path", @dir)
|
||||
end
|
||||
Then(/^tuist tests the scheme ([a-zA-Z]+) from the project$/) do |scheme|
|
||||
system("swift", "run", "tuist", "test", scheme, "--path", @dir)
|
||||
end
|
||||
|
||||
Then(/^tuist tests the scheme ([a-zA-Z]+) and configuration ([a-zA-Z]+) from the project$/) do |scheme, configuration|
|
||||
system("swift", "run", "tuist", "test", scheme, "--path", @dir, "--configuration", configuration)
|
||||
end
|
||||
|
||||
Then(/^tuist tests the project at (.+)$/) do |path|
|
||||
system("swift", "run", "tuist", "test", "--path", File.join(@dir, path))
|
||||
end
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
Feature: Tests projects using Tuist test
|
||||
Scenario: The project is an application with tests (app_with_tests)
|
||||
Given that tuist is available
|
||||
And I have a working directory
|
||||
Then I copy the fixture app_with_tests into the working directory
|
||||
Then tuist generates the project
|
||||
Then tuist tests the project
|
||||
Then tuist tests the scheme AppTests from the project
|
||||
Then tuist tests the scheme MacFrameworkTests from the project
|
||||
Then tuist tests the scheme App and configuration Debug from the project
|
|
@ -0,0 +1,67 @@
|
|||
### macOS ###
|
||||
# General
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two
|
||||
Icon
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
### Xcode ###
|
||||
# Xcode
|
||||
#
|
||||
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
|
||||
|
||||
## User settings
|
||||
xcuserdata/
|
||||
|
||||
## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
|
||||
*.xcscmblueprint
|
||||
*.xccheckout
|
||||
|
||||
## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
|
||||
build/
|
||||
DerivedData/
|
||||
*.moved-aside
|
||||
*.pbxuser
|
||||
!default.pbxuser
|
||||
*.mode1v3
|
||||
!default.mode1v3
|
||||
*.mode2v3
|
||||
!default.mode2v3
|
||||
*.perspectivev3
|
||||
!default.perspectivev3
|
||||
|
||||
### Xcode Patch ###
|
||||
*.xcodeproj/*
|
||||
!*.xcodeproj/project.pbxproj
|
||||
!*.xcodeproj/xcshareddata/
|
||||
!*.xcworkspace/contents.xcworkspacedata
|
||||
/*.gcno
|
||||
|
||||
### Projects ###
|
||||
*.xcodeproj
|
||||
*.xcworkspace
|
||||
|
||||
### Tuist derived files ###
|
||||
graph.dot
|
||||
Derived/
|
|
@ -0,0 +1,78 @@
|
|||
import ProjectDescription
|
||||
|
||||
let project = Project(
|
||||
name: "App",
|
||||
organizationName: "tuist.io",
|
||||
targets: [
|
||||
Target(
|
||||
name: "App",
|
||||
platform: .iOS,
|
||||
product: .app,
|
||||
bundleId: "io.tuist.app",
|
||||
deploymentTarget: .iOS(targetVersion: "13.0", devices: .iphone),
|
||||
infoPlist: .default,
|
||||
sources: ["Targets/App/Sources/**"]
|
||||
),
|
||||
Target(
|
||||
name: "AppTests",
|
||||
platform: .iOS,
|
||||
product: .unitTests,
|
||||
bundleId: "io.tuist.AppTests",
|
||||
infoPlist: .default,
|
||||
sources: ["Targets/App/Tests/**"],
|
||||
dependencies: [
|
||||
.target(name: "App")
|
||||
]
|
||||
),
|
||||
Target(
|
||||
name: "tvOSFramework",
|
||||
platform: .tvOS,
|
||||
product: .framework,
|
||||
bundleId: "io.tuist.tvOSFramework",
|
||||
infoPlist: .default,
|
||||
sources: "Targets/tvOSFramework/Sources/**"
|
||||
),
|
||||
Target(
|
||||
name: "tvOSFrameworkTests",
|
||||
platform: .tvOS,
|
||||
product: .unitTests,
|
||||
bundleId: "io.tuist.tvOSFrameworkTests",
|
||||
infoPlist: .default,
|
||||
sources: "Targets/tvOSFramework/Tests/**",
|
||||
dependencies: [
|
||||
.target(name: "tvOSFramework"),
|
||||
]
|
||||
),
|
||||
Target(
|
||||
name: "MacFramework",
|
||||
platform: .macOS,
|
||||
product: .framework,
|
||||
bundleId: "io.tuist.MacFramework",
|
||||
infoPlist: .default,
|
||||
sources: "Targets/MacFramework/Sources/**",
|
||||
settings: Settings(
|
||||
base: [
|
||||
"CODE_SIGN_IDENTITY": "",
|
||||
"CODE_SIGNING_REQUIRED": "NO"
|
||||
]
|
||||
)
|
||||
),
|
||||
Target(
|
||||
name: "MacFrameworkTests",
|
||||
platform: .macOS,
|
||||
product: .unitTests,
|
||||
bundleId: "io.tuist.MacFrameworkTests",
|
||||
infoPlist: .default,
|
||||
sources: "Targets/MacFramework/Tests/**",
|
||||
dependencies: [
|
||||
.target(name: "MacFramework"),
|
||||
],
|
||||
settings: Settings(
|
||||
base: [
|
||||
"CODE_SIGN_IDENTITY": "",
|
||||
"CODE_SIGNING_REQUIRED": "NO"
|
||||
]
|
||||
)
|
||||
),
|
||||
]
|
||||
)
|
|
@ -0,0 +1,21 @@
|
|||
import UIKit
|
||||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
var window: UIWindow?
|
||||
|
||||
func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil
|
||||
) -> Bool {
|
||||
window = UIWindow(frame: UIScreen.main.bounds)
|
||||
let viewController = UIViewController()
|
||||
viewController.view.backgroundColor = .white
|
||||
window?.rootViewController = viewController
|
||||
window?.makeKeyAndVisible()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import Foundation
|
||||
import XCTest
|
||||
|
||||
final class AppTests: XCTestCase {
|
||||
func test_twoPlusTwo_isFour() {
|
||||
XCTAssertEqual(2+2, 4)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import Foundation
|
||||
|
||||
final class MacFramework {
|
||||
func hello() -> String {
|
||||
return "MacFramework.hello()"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@testable import MacFramework
|
||||
|
||||
final class MacFrameworkTests: XCTestCase {
|
||||
func testHello() {
|
||||
let sut = MacFramework()
|
||||
|
||||
XCTAssertEqual("MacFramework.hello()", sut.hello())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import Foundation
|
||||
|
||||
public final class tvOSFramework {
|
||||
func hello() -> String {
|
||||
return "tvOSFramework.hello()"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@testable import tvOSFramework
|
||||
|
||||
final class tvOSFrameworkTests: XCTestCase {
|
||||
func testHello() {
|
||||
let sut = tvOSFramework()
|
||||
|
||||
XCTAssertEqual("tvOSFramework.hello()", sut.hello())
|
||||
}
|
||||
}
|
|
@ -60,4 +60,6 @@ DerivedData/
|
|||
|
||||
### Projects ###
|
||||
*.xcodeproj
|
||||
*.xcworkspace
|
||||
*.xcworkspace
|
||||
|
||||
Derived
|
||||
|
|
|
@ -4,11 +4,9 @@ import XCTest
|
|||
@testable import App
|
||||
|
||||
final class AppTests: XCTestCase {
|
||||
|
||||
func testHello() {
|
||||
let sut = AppDelegate()
|
||||
|
||||
XCTAssertEqual("AppDelegate.hello()", sut.hello())
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import Foundation
|
||||
|
||||
final class AppCore {
|
||||
func hello() -> String {
|
||||
return "AppCore.hello()"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@testable import AppCore
|
||||
|
||||
final class AppCoreTests: XCTestCase {
|
||||
func testHello() {
|
||||
let sut = AppCore()
|
||||
|
||||
XCTAssertEqual("AppCore.hello()", sut.hello())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import Foundation
|
||||
|
||||
final class MacFramework {
|
||||
func hello() -> String {
|
||||
return "MacFramework.hello()"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@testable import MacFramework
|
||||
|
||||
final class MacFrameworkTests: XCTestCase {
|
||||
func testHello() {
|
||||
let sut = MacFramework()
|
||||
|
||||
XCTAssertEqual("MacFramework.hello()", sut.hello())
|
||||
}
|
||||
}
|
|
@ -1,60 +1,113 @@
|
|||
import ProjectDescription
|
||||
|
||||
let project = Project(name: "App",
|
||||
targets: [
|
||||
Target(name: "App",
|
||||
platform: .iOS,
|
||||
product: .app,
|
||||
bundleId: "io.tuist.App",
|
||||
infoPlist: .file(path: .relativeToManifest("Info.plist")),
|
||||
sources: .paths([.relativeToManifest("Sources/**")]),
|
||||
dependencies: [
|
||||
/* Target dependencies can be defined here */
|
||||
/* .framework(path: "framework") */
|
||||
],
|
||||
settings: Settings(base: ["CODE_SIGN_IDENTITY": "",
|
||||
"CODE_SIGNING_REQUIRED": "NO"])),
|
||||
Target(name: "AppTests",
|
||||
platform: .iOS,
|
||||
product: .unitTests,
|
||||
bundleId: "io.tuist.AppTests",
|
||||
infoPlist: "Tests.plist",
|
||||
sources: "Tests/**",
|
||||
dependencies: [
|
||||
.target(name: "App"),
|
||||
],
|
||||
settings: Settings(base: ["CODE_SIGN_IDENTITY": "",
|
||||
"CODE_SIGNING_REQUIRED": "NO"])),
|
||||
Target(name: "AppUITests",
|
||||
platform: .iOS,
|
||||
product: .uiTests,
|
||||
bundleId: "io.tuist.AppUITests",
|
||||
infoPlist: "Tests.plist",
|
||||
sources: "UITests/**",
|
||||
dependencies: [
|
||||
.target(name: "App"),
|
||||
]),
|
||||
|
||||
|
||||
Target(name: "App-dash",
|
||||
platform: .iOS,
|
||||
product: .app,
|
||||
bundleId: "io.tuist.AppDash",
|
||||
infoPlist: "Info.plist",
|
||||
sources: .paths([.relativeToManifest("Sources/**")]),
|
||||
dependencies: [
|
||||
/* Target dependencies can be defined here */
|
||||
/* .framework(path: "framework") */
|
||||
],
|
||||
settings: Settings(base: ["CODE_SIGN_IDENTITY": "",
|
||||
"CODE_SIGNING_REQUIRED": "NO"])),
|
||||
Target(name: "App-dashUITests",
|
||||
platform: .iOS,
|
||||
product: .uiTests,
|
||||
bundleId: "io.tuist.AppDashUITests",
|
||||
infoPlist: "Tests.plist",
|
||||
sources: "UITests/**",
|
||||
dependencies: [
|
||||
.target(name: "App-dash"),
|
||||
]),
|
||||
])
|
||||
let project = Project(
|
||||
name: "App",
|
||||
targets: [
|
||||
Target(
|
||||
name: "AppCore",
|
||||
platform: .iOS,
|
||||
product: .framework,
|
||||
bundleId: "io.tuist.AppCore",
|
||||
deploymentTarget: .iOS(targetVersion: "12.0", devices: .iphone),
|
||||
infoPlist: .default,
|
||||
sources: .paths([.relativeToManifest("AppCore/Sources/**")])
|
||||
),
|
||||
Target(
|
||||
name: "AppCoreTests",
|
||||
platform: .iOS,
|
||||
product: .unitTests,
|
||||
bundleId: "io.tuist.AppCoreTests",
|
||||
deploymentTarget: .iOS(targetVersion: "12.0", devices: .iphone),
|
||||
infoPlist: "Tests.plist",
|
||||
sources: "AppCore/Tests/**",
|
||||
dependencies: [
|
||||
.target(name: "AppCore"),
|
||||
]
|
||||
),
|
||||
Target(
|
||||
name: "App",
|
||||
platform: .iOS,
|
||||
product: .app,
|
||||
bundleId: "io.tuist.App",
|
||||
infoPlist: .file(path: .relativeToManifest("Info.plist")),
|
||||
sources: .paths([.relativeToManifest("App/Sources/**")]),
|
||||
dependencies: [
|
||||
.target(name: "AppCore")
|
||||
],
|
||||
settings: Settings(base: ["CODE_SIGN_IDENTITY": "",
|
||||
"CODE_SIGNING_REQUIRED": "NO"])
|
||||
),
|
||||
Target(
|
||||
name: "AppTests",
|
||||
platform: .iOS,
|
||||
product: .unitTests,
|
||||
bundleId: "io.tuist.AppTests",
|
||||
infoPlist: "Tests.plist",
|
||||
sources: "App/Tests/**",
|
||||
dependencies: [
|
||||
.target(name: "App"),
|
||||
],
|
||||
settings: Settings(base: ["CODE_SIGN_IDENTITY": "",
|
||||
"CODE_SIGNING_REQUIRED": "NO"])
|
||||
),
|
||||
Target(
|
||||
name: "MacFramework",
|
||||
platform: .macOS,
|
||||
product: .framework,
|
||||
bundleId: "io.tuist.MacFramework",
|
||||
infoPlist: .file(path: .relativeToManifest("Info.plist")),
|
||||
sources: .paths([.relativeToManifest("MacFramework/Sources/**")]),
|
||||
settings: Settings(base: ["CODE_SIGN_IDENTITY": "",
|
||||
"CODE_SIGNING_REQUIRED": "NO"])
|
||||
),
|
||||
Target(
|
||||
name: "MacFrameworkTests",
|
||||
platform: .macOS,
|
||||
product: .unitTests,
|
||||
bundleId: "io.tuist.MacFrameworkTests",
|
||||
infoPlist: "Tests.plist",
|
||||
sources: "MacFramework/Tests/**",
|
||||
dependencies: [
|
||||
.target(name: "MacFramework"),
|
||||
],
|
||||
settings: Settings(base: ["CODE_SIGN_IDENTITY": "",
|
||||
"CODE_SIGNING_REQUIRED": "NO"])
|
||||
),
|
||||
Target(
|
||||
name: "AppUITests",
|
||||
platform: .iOS,
|
||||
product: .uiTests,
|
||||
bundleId: "io.tuist.AppUITests",
|
||||
infoPlist: "Tests.plist",
|
||||
sources: "App/UITests/**",
|
||||
dependencies: [
|
||||
.target(name: "App"),
|
||||
]
|
||||
),
|
||||
Target(
|
||||
name: "App-dash",
|
||||
platform: .iOS,
|
||||
product: .app,
|
||||
bundleId: "io.tuist.AppDash",
|
||||
infoPlist: "Info.plist",
|
||||
sources: .paths([.relativeToManifest("App/Sources/**")]),
|
||||
dependencies: [
|
||||
/* Target dependencies can be defined here */
|
||||
/* .framework(path: "framework") */
|
||||
],
|
||||
settings: Settings(base: ["CODE_SIGN_IDENTITY": "",
|
||||
"CODE_SIGNING_REQUIRED": "NO"])
|
||||
),
|
||||
Target(
|
||||
name: "App-dashUITests",
|
||||
platform: .iOS,
|
||||
product: .uiTests,
|
||||
bundleId: "io.tuist.AppDashUITests",
|
||||
infoPlist: "Tests.plist",
|
||||
sources: "App/UITests/**",
|
||||
dependencies: [
|
||||
.target(name: "App-dash"),
|
||||
]
|
||||
),
|
||||
]
|
||||
)
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
---
|
||||
name: Test
|
||||
excerpt: 'Learn how to test your Tuist projects by simply using the test command that is optimized for minimal configuration.'
|
||||
---
|
||||
|
||||
# Test your App
|
||||
|
||||
Whenever you don't want to run tests from Xcode, for whatever reason, you have to resort to `xcodebuild`.
|
||||
While being a fine piece of software, it's really hard to get all its arguments **just right**
|
||||
when you want to do something simple.
|
||||
|
||||
This is why we think we can do better - just by running `tuist test` we will run *all* test targets in your app.
|
||||
But not only that, we will automatically choose the right device for you - preferring the device you have already booted
|
||||
or choosing one with the correct iOS version and boot it for you. Easy!
|
||||
|
||||
## Command
|
||||
|
||||
As we said, we strive for the test command being really simple - but it should be powerful enough to be useful for all your
|
||||
test-related wishes. Let's see it in more detail below.
|
||||
|
||||
**Test the project in the current directory**
|
||||
|
||||
```bash
|
||||
tuist test
|
||||
```
|
||||
|
||||
**Test a specific scheme**
|
||||
|
||||
```bash
|
||||
tuist test MyScheme
|
||||
```
|
||||
|
||||
**Test on a specific device and OS version**
|
||||
|
||||
```bash
|
||||
tuist test --device "iPhone X" --os 14.0
|
||||
```
|
||||
|
||||
<Message
|
||||
info
|
||||
title="Standard commands"
|
||||
description="One of the benefits of using Tuist over other automation tools is that developers can get familiar with a set of commands that they can use in any Tuist project."
|
||||
/>
|
||||
|
||||
### Arguments
|
||||
|
||||
<ArgumentsTable
|
||||
args={[
|
||||
{
|
||||
long: '`--generate`',
|
||||
description: 'Force the generation of the project before testing.',
|
||||
},
|
||||
{
|
||||
long: '`--clean`',
|
||||
description: 'When passed, it cleans the project before testing it.',
|
||||
},
|
||||
{
|
||||
long: '`--path`',
|
||||
short: '`-p`',
|
||||
description: ' The path to the directory that contains the project to be tested.',
|
||||
},
|
||||
{
|
||||
long: '`--device`',
|
||||
short: '`-d`',
|
||||
description: 'Test on a specific device.',
|
||||
},
|
||||
{
|
||||
long: '`--os`',
|
||||
short: '`-o`',
|
||||
description: 'Test with a specific version of the OS.',
|
||||
},
|
||||
{
|
||||
long: '`--configuration`',
|
||||
short: '`-C`',
|
||||
description: 'The configuration to be used when building the scheme.',
|
||||
},
|
||||
]}
|
||||
/>
|
Loading…
Reference in New Issue