Skip to content

Commit

Permalink
Support test target for local Swift Package (#1169)
Browse files Browse the repository at this point in the history
* support local Swift Package test case into test scheme

* update test

* add test

* update CHABGELOG.md

* Update CHANGELOG.md

* revert resolved package test

* Update Sources/XcodeGenKit/SchemeGenerator.swift

Co-authored-by: Kohki Miki <[email protected]>

* make TargetReference convert from new JSON format

* add .package for location of target reference

* receive target reference format at target of scheme

* update test

* update XcodeProj

* add test and fix small bugs

* update docs

* support multiple style of coverageTargets

* add edge case of parsing test targets

* fix docs

* Update Docs/ProjectSpec.md

Co-authored-by: Yonas Kolb <[email protected]>

* create TestableTargetReference for not making API complex

* fix code format

* fix parameter name to Testable Target Reference

* support directly writing key of Testable Target Reference

* fix compile error in build

Co-authored-by: Kohki Miki <[email protected]>
Co-authored-by: Yonas Kolb <[email protected]>
  • Loading branch information
3 people committed Mar 20, 2022
1 parent 146eaad commit 245f171
Show file tree
Hide file tree
Showing 13 changed files with 284 additions and 28 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Change Log

## Next Version
#### Added
- Support test target for local Swift Package [#1074](https://github.com/yonaskolb/XcodeGen/pull/1074) @freddi-kit

### Fixed

Expand Down
22 changes: 20 additions & 2 deletions Docs/ProjectSpec.md
Original file line number Diff line number Diff line change
Expand Up @@ -828,21 +828,35 @@ A multiline script can be written using the various YAML multiline methods, for
### Test Action

- [ ] **gatherCoverageData**: **Bool** - a boolean that indicates if this scheme should gather coverage data. This defaults to false
- [ ] **coverageTargets**: **[String]** - a list of targets to gather code coverage. Each entry can either be a simple string, or a string using [Project Reference](#project-reference)
- [ ] **coverageTargets**: **[[Testable Target Reference](#testable-target-reference)]** - a list of targets to gather code coverage. Each entry can also either be a simple string, a string using [Project Reference](#project-reference) or [Testable Target Reference](#testable-target-reference)
- [ ] **targets**: **[[Test Target](#test-target)]** - a list of targets to test. Each entry can either be a simple string, or a [Test Target](#test-target)
- [ ] **customLLDBInit**: **String** - the absolute path to the custom `.lldbinit` file
- [ ] **captureScreenshotsAutomatically**: **Bool** - indicates whether screenshots should be captured automatically while UI Testing. This defaults to true.
- [ ] **deleteScreenshotsWhenEachTestSucceeds**: **Bool** - whether successful UI tests should cause automatically-captured screenshots to be deleted. If `captureScreenshotsAutomatically` is false, this value is ignored. This defaults to true.

#### Test Target
- [x] **name**: **String** - The name of the target
A target can be one of a 2 types:

- **name**: **String** - The name of the target.
- **target**: **[Testable Target Reference](#testable-target-reference)** - The information of the target. You can specify more detailed information than `name:`.

As syntax suger, you can also specify **[Testable Target Reference](#testable-target-reference)** without `target`.

#### Other Parameters

- [ ] **parallelizable**: **Bool** - Whether to run tests in parallel. Defaults to false
- [ ] **randomExecutionOrder**: **Bool** - Whether to run tests in a random order. Defaults to false
- [ ] **location**: **String** - GPX file or predefined value for simulating location. See [Simulate Location](#simulate-location) for location examples.
- [ ] **skipped**: **Bool** - Whether to skip all of the test target tests. Defaults to false
- [ ] **skippedTests**: **[String]** - List of tests in the test target to skip. Defaults to empty
- [ ] **selectedTests**: **[String]** - List of tests in the test target to whitelist and select. Defaults to empty. This will override `skippedTests` if provided

#### Testable Target Reference
A Testable Target Reference can be one of 3 types:
- `package: {local-swift-package-name}/{target-name}`: Name of local swift package and its target.
- `local: {target-name}`: Name of local target.
- `project: {project-reference-name}/{target-name}`: Name of local swift package and its target.

### Archive Action

- [ ] **customArchiveName**: **String** - the custom name to give to the archive
Expand Down Expand Up @@ -902,12 +916,16 @@ schemes:
coverageTargets:
- MyTarget1
- ExternalTarget/OtherTarget1
- package: LocalPackage/TestTarget
targets:
- Tester1
- name: Tester2
parallelizable: true
randomExecutionOrder: true
skippedTests: [Test/testExample()]
- package: APIClient/APIClientTests
parallelizable: true
randomExecutionOrder: true
environmentVariables:
- variable: TEST_ENV_VAR
value: VALUE
Expand Down
4 changes: 4 additions & 0 deletions Sources/ProjectSpec/Project.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ public struct Project: BuildSettingsContainer {
targetsMap[targetName]
}

public func getPackage(_ packageName: String) -> SwiftPackage? {
packages[packageName]
}

public func getAggregateTarget(_ targetName: String) -> AggregateTarget? {
aggregateTargetsMap[targetName]
}
Expand Down
47 changes: 36 additions & 11 deletions Sources/ProjectSpec/Scheme.swift
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ public struct Scheme: Equatable {

public var config: String?
public var gatherCoverageData: Bool
public var coverageTargets: [TargetReference]
public var coverageTargets: [TestableTargetReference]
public var disableMainThreadChecker: Bool
public var commandLineArguments: [String: Bool]
public var targets: [TestTarget]
Expand All @@ -189,7 +189,7 @@ public struct Scheme: Equatable {
public static let parallelizableDefault = false

public var name: String { targetReference.name }
public let targetReference: TargetReference
public let targetReference: TestableTargetReference
public var randomExecutionOrder: Bool
public var parallelizable: Bool
public var location: String?
Expand All @@ -198,7 +198,7 @@ public struct Scheme: Equatable {
public var selectedTests: [String]

public init(
targetReference: TargetReference,
targetReference: TestableTargetReference,
randomExecutionOrder: Bool = randomExecutionOrderDefault,
parallelizable: Bool = parallelizableDefault,
location: String? = nil,
Expand All @@ -217,7 +217,7 @@ public struct Scheme: Equatable {

public init(stringLiteral value: String) {
do {
targetReference = try TargetReference(value)
targetReference = try TestableTargetReference(value)
randomExecutionOrder = false
parallelizable = false
location = nil
Expand All @@ -233,7 +233,7 @@ public struct Scheme: Equatable {
public init(
config: String,
gatherCoverageData: Bool = gatherCoverageDataDefault,
coverageTargets: [TargetReference] = [],
coverageTargets: [TestableTargetReference] = [],
disableMainThreadChecker: Bool = disableMainThreadCheckerDefault,
randomExecutionOrder: Bool = false,
parallelizable: Bool = false,
Expand Down Expand Up @@ -331,10 +331,10 @@ public struct Scheme: Equatable {
}

public struct BuildTarget: Equatable, Hashable {
public var target: TargetReference
public var target: TestableTargetReference
public var buildTypes: [BuildType]

public init(target: TargetReference, buildTypes: [BuildType] = BuildType.all) {
public init(target: TestableTargetReference, buildTypes: [BuildType] = BuildType.all) {
self.target = target
self.buildTypes = buildTypes
}
Expand Down Expand Up @@ -465,13 +465,28 @@ extension Scheme.Test: JSONObjectConvertible {
public init(jsonDictionary: JSONDictionary) throws {
config = jsonDictionary.json(atKeyPath: "config")
gatherCoverageData = jsonDictionary.json(atKeyPath: "gatherCoverageData") ?? Scheme.Test.gatherCoverageDataDefault
coverageTargets = try (jsonDictionary.json(atKeyPath: "coverageTargets") ?? []).map { try TargetReference($0) }

if let coverages = jsonDictionary["coverageTargets"] as? [Any] {
coverageTargets = try coverages.compactMap { target in
if let string = target as? String {
return try TestableTargetReference(string)
} else if let dictionary = target as? JSONDictionary,
let target: TestableTargetReference = try? .init(jsonDictionary: dictionary) {
return target
} else {
return nil
}
}
} else {
coverageTargets = []
}

disableMainThreadChecker = jsonDictionary.json(atKeyPath: "disableMainThreadChecker") ?? Scheme.Test.disableMainThreadCheckerDefault
commandLineArguments = jsonDictionary.json(atKeyPath: "commandLineArguments") ?? [:]
if let targets = jsonDictionary["targets"] as? [Any] {
self.targets = try targets.compactMap { target in
if let string = target as? String {
return try TestTarget(targetReference: TargetReference(string))
return try TestTarget(targetReference: TestableTargetReference(string))
} else if let dictionary = target as? JSONDictionary {
return try TestTarget(jsonDictionary: dictionary)
} else {
Expand Down Expand Up @@ -538,7 +553,17 @@ extension Scheme.Test: JSONEncodable {
extension Scheme.Test.TestTarget: JSONObjectConvertible {

public init(jsonDictionary: JSONDictionary) throws {
targetReference = try TargetReference(jsonDictionary.json(atKeyPath: "name"))
if let name: String = jsonDictionary.json(atKeyPath: "name") {
targetReference = try TestableTargetReference(name)
} else if let local: String = jsonDictionary.json(atKeyPath: "local") {
self.targetReference = TestableTargetReference.local(local)
} else if let project: String = jsonDictionary.json(atKeyPath: "project") {
self.targetReference = TestableTargetReference.project(project)
} else if let package: String = jsonDictionary.json(atKeyPath: "package") {
self.targetReference = TestableTargetReference.package(package)
} else {
self.targetReference = try jsonDictionary.json(atKeyPath: "target")
}
randomExecutionOrder = jsonDictionary.json(atKeyPath: "randomExecutionOrder") ?? Scheme.Test.TestTarget.randomExecutionOrderDefault
parallelizable = jsonDictionary.json(atKeyPath: "parallelizable") ?? Scheme.Test.TestTarget.parallelizableDefault
location = jsonDictionary.json(atKeyPath: "location") ?? nil
Expand Down Expand Up @@ -694,7 +719,7 @@ extension Scheme.Build: JSONObjectConvertible {
} else {
buildTypes = BuildType.all
}
let target = try TargetReference(targetRepr)
let target = try TestableTargetReference(targetRepr)
targets.append(Scheme.BuildTarget(target: target, buildTypes: buildTypes))
}
self.targets = targets.sorted { $0.target.name < $1.target.name }
Expand Down
18 changes: 18 additions & 0 deletions Sources/ProjectSpec/SpecValidation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ extension Project {

for testTarget in scheme.testTargets {
if getTarget(testTarget.name) == nil {
// For test case of local Swift Package
if case .package(let name) = testTarget.targetReference.location, getPackage(name) != nil {
continue
}
errors.append(.invalidTargetSchemeTest(target: target.name, testTarget: testTarget.name))
}
}
Expand Down Expand Up @@ -243,4 +247,18 @@ extension Project {
return nil
}
}

/// Returns a descriptive error if the given target reference was invalid otherwise `nil`.
private func validationError(for testableTargetReference: TestableTargetReference, in scheme: Scheme, action: String) -> SpecValidationError.ValidationError? {
switch testableTargetReference.location {
case .local where getProjectTarget(testableTargetReference.name) == nil:
return .invalidSchemeTarget(scheme: scheme.name, target: testableTargetReference.name, action: action)
case .project(let project) where getProjectReference(project) == nil:
return .invalidProjectReference(scheme: scheme.name, reference: project)
case .package(let package) where getPackage(package) == nil:
return .invalidLocalPackage(package)
case .local, .project, .package:
return nil
}
}
}
3 changes: 3 additions & 0 deletions Sources/ProjectSpec/SpecValidationError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public struct SpecValidationError: Error, CustomStringConvertible {
case invalidSchemeTarget(scheme: String, target: String, action: String)
case invalidSchemeConfig(scheme: String, config: String)
case invalidSwiftPackage(name: String, target: String)
case invalidPackageDependencyReference(name: String)
case invalidLocalPackage(String)
case invalidConfigFile(configFile: String, config: String)
case invalidBuildSettingConfig(String)
Expand Down Expand Up @@ -69,6 +70,8 @@ public struct SpecValidationError: Error, CustomStringConvertible {
return "Target \(target.quoted) has an invalid package dependency \(name.quoted)"
case let .invalidLocalPackage(path):
return "Invalid local package \(path.quoted)"
case let .invalidPackageDependencyReference(name):
return "Package reference \(name) must be specified as package dependency, not target"
case let .missingConfigForTargetScheme(target, configType):
return "Target \(target.quoted) is missing a config of type \(configType.rawValue) to generate its scheme"
case let .missingDefaultConfig(name):
Expand Down
4 changes: 2 additions & 2 deletions Sources/ProjectSpec/TargetReference.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ extension TargetReference: CustomStringConvertible {
public var reference: String {
switch location {
case .local: return name
case .project(let projectPath):
return "\(projectPath)/\(name)"
case .project(let root):
return "\(root)/\(name)"
}
}

Expand Down
7 changes: 4 additions & 3 deletions Sources/ProjectSpec/TargetScheme.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,10 @@ extension TargetScheme: JSONObjectConvertible {
if let targets = jsonDictionary["testTargets"] as? [Any] {
testTargets = try targets.compactMap { target in
if let string = target as? String {
return .init(targetReference: try TargetReference(string))
} else if let dictionary = target as? JSONDictionary {
return try .init(jsonDictionary: dictionary)
return .init(targetReference: try TestableTargetReference(string))
} else if let dictionary = target as? JSONDictionary,
let target: Scheme.Test.TestTarget = try? .init(jsonDictionary: dictionary) {
return target
} else {
return nil
}
Expand Down
112 changes: 112 additions & 0 deletions Sources/ProjectSpec/TestTargeReference.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import Foundation
import JSONUtilities

public struct TestableTargetReference: Hashable {
public var name: String
public var location: Location

public var targetReference: TargetReference {
switch location {
case .local:
return TargetReference(name: name, location: .local)
case .project(let projectName):
return TargetReference(name: name, location: .project(projectName))
case .package:
fatalError("Package target is only available for testable")
}
}

public enum Location: Hashable {
case local
case project(String)
case package(String)
}

public init(name: String, location: Location) {
self.name = name
self.location = location
}
}

extension TestableTargetReference {
public init(_ string: String) throws {
let paths = string.split(separator: "/")
switch paths.count {
case 2:
location = .project(String(paths[0]))
name = String(paths[1])
case 1:
location = .local
name = String(paths[0])
default:
throw SpecParsingError.invalidTargetReference(string)
}
}

public static func local(_ name: String) -> TestableTargetReference {
TestableTargetReference(name: name, location: .local)
}

public static func project(_ name: String) -> TestableTargetReference {
let paths = name.split(separator: "/")
return TestableTargetReference(name: String(paths[1]), location: .project(String(paths[0])))
}

public static func package(_ name: String) -> TestableTargetReference {
let paths = name.split(separator: "/")
return TestableTargetReference(name: String(paths[1]), location: .package(String(paths[0])))
}
}

extension TestableTargetReference: ExpressibleByStringLiteral {
public init(stringLiteral value: String) {
try! self.init(value)
}
}

extension TestableTargetReference: CustomStringConvertible {
public var reference: String {
switch location {
case .local: return name
case .project(let root), .package(let root):
return "\(root)/\(name)"
}
}

public var description: String {
reference
}
}

extension TestableTargetReference: JSONObjectConvertible {

public init(jsonDictionary: JSONDictionary) throws {
if let project: String = jsonDictionary.json(atKeyPath: "project") {
let paths = project.split(separator: "/")
name = String(paths[1])
location = .project(String(paths[0]))
} else if let project: String = jsonDictionary.json(atKeyPath: "package") {
let paths = project.split(separator: "/")
name = String(paths[1])
location = .package(String(paths[0]))
} else {
name = try jsonDictionary.json(atKeyPath: "local")
location = .local
}
}
}

extension TestableTargetReference: JSONEncodable {
public func toJSONValue() -> Any {
var dictionary: JSONDictionary = [:]
switch self.location {
case .package(let packageName):
dictionary["package"] = "\(packageName)/\(name)"
case .project(let projectName):
dictionary["project"] = "\(projectName)/\(name)"
case .local:
dictionary["local"] = name
}
return dictionary
}
}
Loading

0 comments on commit 245f171

Please sign in to comment.