Skip to content

Commit 641ce09

Browse files
authored
Preserve unknown test plan keys (#83)
* Preserve unknown test plan keys * Remove unused import of ArgumentParser
1 parent 258c430 commit 641ce09

6 files changed

Lines changed: 241 additions & 152 deletions

File tree

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ let targets: [PackageDescription.Target] = [
6161
),
6262
.testTarget(
6363
name: "SelectiveTestingTests",
64-
dependencies: ["xcode-selective-test", "PathKit"],
64+
dependencies: ["xcode-selective-test", "PathKit", "Workspace"],
6565
resources: [.copy("ExampleProject")],
6666
swiftSettings: flags
6767
),

Sources/TestConfigurator/TestConfigurator.swift

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,10 @@ extension TestPlanHelper {
3636
packagesToTest.contains(target.target.name)
3737

3838
guard enabled else { return nil }
39-
40-
return TestTarget(parallelizable: target.parallelizable,
41-
skippedTests: target.skippedTests,
42-
selectedTests: target.selectedTests,
43-
target: target.target,
44-
enabled: enabled)
39+
40+
var updatedTarget = target
41+
updatedTarget.enabled = true
42+
return updatedTarget
4543
}
4644
}
4745
}
Lines changed: 144 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,111 +1,174 @@
11
import Foundation
22

3-
// MARK: - Welcome
3+
typealias JSONObject = [String: Any]
44

5-
public struct TestPlanModel: Codable {
6-
public var configurations: [Configuration]
7-
public var defaultOptions: DefaultOptions
5+
public struct TestPlanModel {
6+
private var rawJSON: JSONObject
87
public var testTargets: [TestTarget]
9-
public var version: Int
10-
}
11-
12-
// MARK: - Configuration
13-
14-
public struct Configuration: Codable {
15-
public var id, name: String
16-
public var options: Options
8+
9+
public init(data: Data) throws {
10+
let object = try JSONSerialization.jsonObject(with: data, options: [])
11+
guard let dictionary = object as? JSONObject else {
12+
throw TestPlanModelError.invalidFormat
13+
}
14+
15+
self.rawJSON = dictionary
16+
self.testTargets = try Self.decodeTargets(from: dictionary["testTargets"])
17+
}
18+
19+
func encodedData() throws -> Data {
20+
var json = rawJSON
21+
json["testTargets"] = try Self.encodeTargets(testTargets)
22+
return try JSONSerialization.data(withJSONObject: json,
23+
options: [.prettyPrinted, .sortedKeys])
24+
}
25+
26+
private static func decodeTargets(from value: Any?) throws -> [TestTarget] {
27+
guard let value, !(value is NSNull) else { return [] }
28+
guard let array = value as? [Any] else {
29+
throw TestPlanModelError.invalidTargets
30+
}
31+
32+
return try array.map { element in
33+
guard let dictionary = element as? JSONObject else {
34+
throw TestPlanModelError.invalidTargets
35+
}
36+
return try TestTarget(json: dictionary)
37+
}
38+
}
39+
40+
private static func encodeTargets(_ targets: [TestTarget]) throws -> [Any] {
41+
return try targets.map { try $0.encodeJSON() }
42+
}
1743
}
1844

19-
// MARK: - Options
20-
21-
public struct Options: Codable {
22-
public var targetForVariableExpansion: Target?
45+
public enum TestPlanModelError: Error {
46+
case invalidFormat
47+
case invalidTargets
48+
case invalidTargetObject
2349
}
2450

2551
// MARK: - Target
2652

27-
public struct Target: Codable {
28-
public var containerPath, identifier, name: String
29-
}
30-
31-
// MARK: - DefaultOptions
32-
33-
public struct DefaultOptions: Codable {
34-
public var commandLineArgumentEntries: [CommandLineArgumentEntry]?
35-
public var environmentVariableEntries: [EnvironmentVariableEntry]?
36-
public var language: String?
37-
public var region: String?
38-
public var locationScenario: LocationScenario?
39-
public var testTimeoutsEnabled: Bool?
40-
public var testRepetitionMode: String?
41-
public var maximumTestRepetitions: Int?
42-
public var maximumTestExecutionTimeAllowance: Int?
43-
public var targetForVariableExpansion: Target?
44-
}
45-
46-
// MARK: - CommandLineArgumentEntry
47-
48-
public struct CommandLineArgumentEntry: Codable {
49-
public let argument: String
50-
public let enabled: Bool?
51-
}
52-
53-
// MARK: - EnvironmentVariableEntry
54-
55-
public struct EnvironmentVariableEntry: Codable {
56-
public var key, value: String
57-
public let enabled: Bool?
58-
}
59-
60-
// MARK: - LocationScenario
61-
62-
public struct LocationScenario: Codable {
53+
public struct Target {
54+
private var rawJSON: JSONObject
55+
public var containerPath: String
6356
public var identifier: String
57+
public var name: String
58+
59+
init(json: JSONObject) throws {
60+
guard let containerPath = json["containerPath"] as? String,
61+
let identifier = json["identifier"] as? String,
62+
let name = json["name"] as? String else {
63+
throw TestPlanModelError.invalidTargetObject
64+
}
65+
66+
self.rawJSON = json
67+
self.containerPath = containerPath
68+
self.identifier = identifier
69+
self.name = name
70+
}
71+
72+
func encodeJSON() -> JSONObject {
73+
var json = rawJSON
74+
json["containerPath"] = containerPath
75+
json["identifier"] = identifier
76+
json["name"] = name
77+
return json
78+
}
6479
}
6580

6681
// MARK: - TestTarget
6782

68-
public struct TestTarget: Codable {
83+
public struct TestTarget {
84+
private var rawJSON: JSONObject
6985
public var parallelizable: Bool?
7086
public var skippedTests: Tests?
7187
public var selectedTests: Tests?
7288
public var target: Target
7389
public var enabled: Bool?
90+
91+
init(json: JSONObject) throws {
92+
guard let targetJSON = json["target"] as? JSONObject else {
93+
throw TestPlanModelError.invalidTargetObject
94+
}
95+
self.rawJSON = json
96+
self.parallelizable = json["parallelizable"] as? Bool
97+
self.enabled = json["enabled"] as? Bool
98+
self.target = try Target(json: targetJSON)
99+
self.skippedTests = try Tests.fromJSONValue(json["skippedTests"])
100+
self.selectedTests = try Tests.fromJSONValue(json["selectedTests"])
101+
}
102+
103+
func encodeJSON() throws -> JSONObject {
104+
var json = rawJSON
105+
json.setJSONValue(parallelizable, forKey: "parallelizable")
106+
json.setJSONValue(enabled, forKey: "enabled")
107+
json.setJSONValue(try skippedTests?.jsonValue(), forKey: "skippedTests")
108+
json.setJSONValue(try selectedTests?.jsonValue(), forKey: "selectedTests")
109+
json["target"] = target.encodeJSON()
110+
return json
111+
}
74112
}
75113

76114
public enum Tests: Codable {
77-
case array([String])
78-
case dictionary(Suites)
79-
80-
public struct Suites: Codable {
81-
let suites: [Suite]
82-
83-
public struct Suite: Codable {
84-
let name: String
115+
case array([String])
116+
case dictionary(Suites)
117+
118+
public struct Suites: Codable {
119+
let suites: [Suite]
120+
121+
public struct Suite: Codable {
122+
let name: String
123+
}
85124
}
86-
}
87-
88-
public init(from decoder: Decoder) throws {
89-
let container = try decoder.singleValueContainer()
90-
if let array = try? container.decode([String].self) {
91-
self = .array(array)
92-
return
125+
126+
public init(from decoder: Decoder) throws {
127+
let container = try decoder.singleValueContainer()
128+
if let array = try? container.decode([String].self) {
129+
self = .array(array)
130+
return
131+
}
132+
133+
if let dict = try? container.decode(Suites.self) {
134+
self = .dictionary(dict)
135+
return
136+
}
137+
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid type for skippedTests")
138+
}
139+
140+
public func encode(to encoder: Encoder) throws {
141+
var container = encoder.singleValueContainer()
142+
switch self {
143+
case .array(let array):
144+
try container.encode(array)
145+
case .dictionary(let dict):
146+
try container.encode(dict)
147+
}
93148
}
149+
}
94150

95-
if let dict = try? container.decode(Suites.self) {
96-
self = .dictionary(dict)
97-
return
151+
extension Tests {
152+
static func fromJSONValue(_ value: Any?) throws -> Tests? {
153+
guard let value, !(value is NSNull) else { return nil }
154+
let data = try JSONSerialization.data(withJSONObject: value)
155+
let decoder = JSONDecoder()
156+
return try decoder.decode(Tests.self, from: data)
157+
}
158+
159+
func jsonValue() throws -> Any {
160+
let encoder = JSONEncoder()
161+
let data = try encoder.encode(self)
162+
return try JSONSerialization.jsonObject(with: data, options: [])
98163
}
99-
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid type for skippedTests")
100-
}
164+
}
101165

102-
public func encode(to encoder: Encoder) throws {
103-
var container = encoder.singleValueContainer()
104-
switch self {
105-
case .array(let array):
106-
try container.encode(array)
107-
case .dictionary(let dict):
108-
try container.encode(dict)
166+
extension Dictionary where Key == String, Value == Any {
167+
mutating func setJSONValue(_ value: Any?, forKey key: String) {
168+
if let value {
169+
self[key] = value
170+
} else {
171+
self.removeValue(forKey: key)
172+
}
109173
}
110-
}
111174
}

Sources/TestConfigurator/xctestplanner/Core/Extensions/Array+Extensions.swift

Lines changed: 0 additions & 13 deletions
This file was deleted.

Sources/TestConfigurator/xctestplanner/Core/Helper/TestPlanHelper.swift

Lines changed: 2 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
// Created by Atakan Karslı on 20/12/2022.
66
//
77

8-
import ArgumentParser
98
import Foundation
109
import Logging
1110

@@ -16,68 +15,20 @@ public class TestPlanHelper {
1615
logger.info("Reading test plan from file: \(filePath)")
1716
let url = URL(fileURLWithPath: filePath)
1817
let data = try Data(contentsOf: url)
19-
20-
let decoder = JSONDecoder()
21-
return try decoder.decode(TestPlanModel.self, from: data)
18+
return try TestPlanModel(data: data)
2219
}
2320

2421
static func writeTestPlan(_ testPlan: TestPlanModel, filePath: String) throws {
2522
logger.info("Writing updated test plan to file: \(filePath)")
26-
let encoder = JSONEncoder()
27-
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
28-
let updatedData = try encoder.encode(testPlan)
29-
23+
let updatedData = try testPlan.encodedData()
3024
let url = URL(fileURLWithPath: filePath)
3125
try updatedData.write(to: url)
3226
}
3327

34-
static func updateRerunCount(testPlan: inout TestPlanModel, to count: Int) {
35-
logger.info("Updating rerun count in test plan to: \(count)")
36-
if testPlan.defaultOptions.testRepetitionMode == nil {
37-
testPlan.defaultOptions.testRepetitionMode = TestPlanValue.retryOnFailure.rawValue
38-
}
39-
testPlan.defaultOptions.maximumTestRepetitions = count
40-
}
41-
42-
static func updateLanguage(testPlan: inout TestPlanModel, to language: String) {
43-
logger.info("Updating language in test plan to: \(language)")
44-
testPlan.defaultOptions.language = language.lowercased()
45-
}
46-
47-
static func updateRegion(testPlan: inout TestPlanModel, to region: String) {
48-
logger.info("Updating region in test plan to: \(region)")
49-
testPlan.defaultOptions.region = region.uppercased()
50-
}
51-
52-
static func setEnvironmentVariable(testPlan: inout TestPlanModel, key: String, value: String, enabled: Bool? = true) {
53-
logger.info("Setting environment variable with key '\(key)' and value '\(value)' in test plan")
54-
if testPlan.defaultOptions.environmentVariableEntries == nil {
55-
testPlan.defaultOptions.environmentVariableEntries = []
56-
}
57-
testPlan.defaultOptions.environmentVariableEntries?.append(EnvironmentVariableEntry(key: key, value: value, enabled: enabled))
58-
}
59-
60-
static func setArgument(testPlan: inout TestPlanModel, key: String, disabled: Bool) {
61-
if testPlan.defaultOptions.commandLineArgumentEntries == nil {
62-
testPlan.defaultOptions.commandLineArgumentEntries = []
63-
}
64-
if disabled {
65-
logger.info("Setting command line argument with key '\(key)' in test plan as disabled")
66-
testPlan.defaultOptions.commandLineArgumentEntries?.append(CommandLineArgumentEntry(argument: key, enabled: !disabled))
67-
} else {
68-
logger.info("Setting command line argument with key '\(key)', enabled by default")
69-
testPlan.defaultOptions.commandLineArgumentEntries?.append(CommandLineArgumentEntry(argument: key, enabled: nil))
70-
}
71-
}
72-
7328
static func checkForTestTargets(testPlan: TestPlanModel) {
7429
if testPlan.testTargets.isEmpty {
7530
logger.error("Test plan does not have any test targets. Add a test target before attempting to update the selected or skipped tests.")
7631
exit(1)
7732
}
7833
}
7934
}
80-
81-
enum TestPlanValue: String {
82-
case retryOnFailure
83-
}

0 commit comments

Comments
 (0)