Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ let targets: [PackageDescription.Target] = [
),
.testTarget(
name: "SelectiveTestingTests",
dependencies: ["xcode-selective-test", "PathKit"],
dependencies: ["xcode-selective-test", "PathKit", "Workspace"],
resources: [.copy("ExampleProject")],
swiftSettings: flags
),
Expand Down
10 changes: 4 additions & 6 deletions Sources/TestConfigurator/TestConfigurator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,10 @@ extension TestPlanHelper {
packagesToTest.contains(target.target.name)

guard enabled else { return nil }

return TestTarget(parallelizable: target.parallelizable,
skippedTests: target.skippedTests,
selectedTests: target.selectedTests,
target: target.target,
enabled: enabled)

var updatedTarget = target
updatedTarget.enabled = true
return updatedTarget
}
}
}
Expand Down
225 changes: 144 additions & 81 deletions Sources/TestConfigurator/xctestplanner/Core/Entity/TestPlanModel.swift
Original file line number Diff line number Diff line change
@@ -1,111 +1,174 @@
import Foundation

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

public struct TestPlanModel: Codable {
public var configurations: [Configuration]
public var defaultOptions: DefaultOptions
public struct TestPlanModel {
private var rawJSON: JSONObject
public var testTargets: [TestTarget]
public var version: Int
}

// MARK: - Configuration

public struct Configuration: Codable {
public var id, name: String
public var options: Options

public init(data: Data) throws {
let object = try JSONSerialization.jsonObject(with: data, options: [])
guard let dictionary = object as? JSONObject else {
throw TestPlanModelError.invalidFormat
}

self.rawJSON = dictionary
self.testTargets = try Self.decodeTargets(from: dictionary["testTargets"])
}

func encodedData() throws -> Data {
var json = rawJSON
json["testTargets"] = try Self.encodeTargets(testTargets)
return try JSONSerialization.data(withJSONObject: json,
options: [.prettyPrinted, .sortedKeys])
}

private static func decodeTargets(from value: Any?) throws -> [TestTarget] {
guard let value, !(value is NSNull) else { return [] }
guard let array = value as? [Any] else {
throw TestPlanModelError.invalidTargets
}

return try array.map { element in
guard let dictionary = element as? JSONObject else {
throw TestPlanModelError.invalidTargets
}
return try TestTarget(json: dictionary)
}
}

private static func encodeTargets(_ targets: [TestTarget]) throws -> [Any] {
return try targets.map { try $0.encodeJSON() }
}
}

// MARK: - Options

public struct Options: Codable {
public var targetForVariableExpansion: Target?
public enum TestPlanModelError: Error {
case invalidFormat
case invalidTargets
case invalidTargetObject
}

// MARK: - Target

public struct Target: Codable {
public var containerPath, identifier, name: String
}

// MARK: - DefaultOptions

public struct DefaultOptions: Codable {
public var commandLineArgumentEntries: [CommandLineArgumentEntry]?
public var environmentVariableEntries: [EnvironmentVariableEntry]?
public var language: String?
public var region: String?
public var locationScenario: LocationScenario?
public var testTimeoutsEnabled: Bool?
public var testRepetitionMode: String?
public var maximumTestRepetitions: Int?
public var maximumTestExecutionTimeAllowance: Int?
public var targetForVariableExpansion: Target?
}

// MARK: - CommandLineArgumentEntry

public struct CommandLineArgumentEntry: Codable {
public let argument: String
public let enabled: Bool?
}

// MARK: - EnvironmentVariableEntry

public struct EnvironmentVariableEntry: Codable {
public var key, value: String
public let enabled: Bool?
}

// MARK: - LocationScenario

public struct LocationScenario: Codable {
public struct Target {
private var rawJSON: JSONObject
public var containerPath: String
public var identifier: String
public var name: String

init(json: JSONObject) throws {
guard let containerPath = json["containerPath"] as? String,
let identifier = json["identifier"] as? String,
let name = json["name"] as? String else {
throw TestPlanModelError.invalidTargetObject
}

self.rawJSON = json
self.containerPath = containerPath
self.identifier = identifier
self.name = name
}

func encodeJSON() -> JSONObject {
var json = rawJSON
json["containerPath"] = containerPath
json["identifier"] = identifier
json["name"] = name
return json
}
}

// MARK: - TestTarget

public struct TestTarget: Codable {
public struct TestTarget {
private var rawJSON: JSONObject
public var parallelizable: Bool?
public var skippedTests: Tests?
public var selectedTests: Tests?
public var target: Target
public var enabled: Bool?

init(json: JSONObject) throws {
guard let targetJSON = json["target"] as? JSONObject else {
throw TestPlanModelError.invalidTargetObject
}
self.rawJSON = json
self.parallelizable = json["parallelizable"] as? Bool
self.enabled = json["enabled"] as? Bool
self.target = try Target(json: targetJSON)
self.skippedTests = try Tests.fromJSONValue(json["skippedTests"])
self.selectedTests = try Tests.fromJSONValue(json["selectedTests"])
}

func encodeJSON() throws -> JSONObject {
var json = rawJSON
json.setJSONValue(parallelizable, forKey: "parallelizable")
json.setJSONValue(enabled, forKey: "enabled")
json.setJSONValue(try skippedTests?.jsonValue(), forKey: "skippedTests")
json.setJSONValue(try selectedTests?.jsonValue(), forKey: "selectedTests")
json["target"] = target.encodeJSON()
return json
}
}

public enum Tests: Codable {
case array([String])
case dictionary(Suites)

public struct Suites: Codable {
let suites: [Suite]

public struct Suite: Codable {
let name: String
case array([String])
case dictionary(Suites)

public struct Suites: Codable {
let suites: [Suite]

public struct Suite: Codable {
let name: String
}
}
}

public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let array = try? container.decode([String].self) {
self = .array(array)
return

public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let array = try? container.decode([String].self) {
self = .array(array)
return
}

if let dict = try? container.decode(Suites.self) {
self = .dictionary(dict)
return
}
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid type for skippedTests")
}

public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .array(let array):
try container.encode(array)
case .dictionary(let dict):
try container.encode(dict)
}
}
}

if let dict = try? container.decode(Suites.self) {
self = .dictionary(dict)
return
extension Tests {
static func fromJSONValue(_ value: Any?) throws -> Tests? {
guard let value, !(value is NSNull) else { return nil }
let data = try JSONSerialization.data(withJSONObject: value)
let decoder = JSONDecoder()
return try decoder.decode(Tests.self, from: data)
}

func jsonValue() throws -> Any {
let encoder = JSONEncoder()
let data = try encoder.encode(self)
return try JSONSerialization.jsonObject(with: data, options: [])
}
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid type for skippedTests")
}
}

public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .array(let array):
try container.encode(array)
case .dictionary(let dict):
try container.encode(dict)
extension Dictionary where Key == String, Value == Any {
mutating func setJSONValue(_ value: Any?, forKey key: String) {
if let value {
self[key] = value
} else {
self.removeValue(forKey: key)
}
}
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
// Created by Atakan Karslı on 20/12/2022.
//

import ArgumentParser
import Foundation
import Logging

Expand All @@ -16,68 +15,20 @@ public class TestPlanHelper {
logger.info("Reading test plan from file: \(filePath)")
let url = URL(fileURLWithPath: filePath)
let data = try Data(contentsOf: url)

let decoder = JSONDecoder()
return try decoder.decode(TestPlanModel.self, from: data)
return try TestPlanModel(data: data)
}

static func writeTestPlan(_ testPlan: TestPlanModel, filePath: String) throws {
logger.info("Writing updated test plan to file: \(filePath)")
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
let updatedData = try encoder.encode(testPlan)

let updatedData = try testPlan.encodedData()
let url = URL(fileURLWithPath: filePath)
try updatedData.write(to: url)
}

static func updateRerunCount(testPlan: inout TestPlanModel, to count: Int) {
logger.info("Updating rerun count in test plan to: \(count)")
if testPlan.defaultOptions.testRepetitionMode == nil {
testPlan.defaultOptions.testRepetitionMode = TestPlanValue.retryOnFailure.rawValue
}
testPlan.defaultOptions.maximumTestRepetitions = count
}

static func updateLanguage(testPlan: inout TestPlanModel, to language: String) {
logger.info("Updating language in test plan to: \(language)")
testPlan.defaultOptions.language = language.lowercased()
}

static func updateRegion(testPlan: inout TestPlanModel, to region: String) {
logger.info("Updating region in test plan to: \(region)")
testPlan.defaultOptions.region = region.uppercased()
}

static func setEnvironmentVariable(testPlan: inout TestPlanModel, key: String, value: String, enabled: Bool? = true) {
logger.info("Setting environment variable with key '\(key)' and value '\(value)' in test plan")
if testPlan.defaultOptions.environmentVariableEntries == nil {
testPlan.defaultOptions.environmentVariableEntries = []
}
testPlan.defaultOptions.environmentVariableEntries?.append(EnvironmentVariableEntry(key: key, value: value, enabled: enabled))
}

static func setArgument(testPlan: inout TestPlanModel, key: String, disabled: Bool) {
if testPlan.defaultOptions.commandLineArgumentEntries == nil {
testPlan.defaultOptions.commandLineArgumentEntries = []
}
if disabled {
logger.info("Setting command line argument with key '\(key)' in test plan as disabled")
testPlan.defaultOptions.commandLineArgumentEntries?.append(CommandLineArgumentEntry(argument: key, enabled: !disabled))
} else {
logger.info("Setting command line argument with key '\(key)', enabled by default")
testPlan.defaultOptions.commandLineArgumentEntries?.append(CommandLineArgumentEntry(argument: key, enabled: nil))
}
}

static func checkForTestTargets(testPlan: TestPlanModel) {
if testPlan.testTargets.isEmpty {
logger.error("Test plan does not have any test targets. Add a test target before attempting to update the selected or skipped tests.")
exit(1)
}
}
}

enum TestPlanValue: String {
case retryOnFailure
}
Loading