Skip to content

Commit

Permalink
Version 0.3
Browse files Browse the repository at this point in the history
Add reges pattern support
Add tests
  • Loading branch information
Alex da Franca committed Aug 2, 2024
1 parent c39a25f commit 99e0925
Show file tree
Hide file tree
Showing 9 changed files with 832 additions and 85 deletions.
27 changes: 26 additions & 1 deletion .swiftpm/xcode/xcshareddata/xcschemes/findsimulator.xcscheme
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,19 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
shouldUseLaunchSchemeArgsEnv = "YES"
codeCoverageEnabled = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "findsimulatorTests"
BuildableName = "findsimulatorTests"
BlueprintName = "findsimulatorTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
Expand All @@ -50,6 +61,20 @@
ReferencedContainer = "container:">
</BuildableReference>
</BuildableProductRunnable>
<CommandLineArguments>
<CommandLineArgument
argument = "-h"
isEnabled = "YES">
</CommandLineArgument>
<CommandLineArgument
argument = "-l"
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "-r &quot;^iPhone\\s1\\d Pro$&quot;"
isEnabled = "NO">
</CommandLineArgument>
</CommandLineArguments>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
Expand Down
24 changes: 11 additions & 13 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
{
"object": {
"pins": [
{
"package": "swift-argument-parser",
"repositoryURL": "https://github.com/apple/swift-argument-parser.git",
"state": {
"branch": null,
"revision": "6b2aa2748a7881eebb9f84fb10c01293e15b52ca",
"version": "0.5.0"
}
"pins" : [
{
"identity" : "swift-argument-parser",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-argument-parser.git",
"state" : {
"revision" : "41982a3656a71c768319979febd796c6fd111d5c",
"version" : "1.5.0"
}
]
},
"version": 1
}
],
"version" : 2
}
11 changes: 7 additions & 4 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
// swift-tools-version:5.4
// swift-tools-version:5.9
import PackageDescription

let package = Package(
name: "findsimulator",
platforms: [
.macOS(.v11),
.macOS(.v13),
],
products: [
.executable(
Expand All @@ -14,9 +14,8 @@ let package = Package(
],
dependencies: [
.package(
name: "swift-argument-parser",
url: "https://github.com/apple/swift-argument-parser.git",
.upToNextMajor(from: "0.4.3")
.upToNextMajor(from: "1.5.0")
)
],
targets: [
Expand All @@ -29,6 +28,10 @@ let package = Package(
)
],
path: "Sources"
),
.testTarget(
name: "findsimulatorTests",
dependencies: ["findsimulator"]
)
]
)
2 changes: 2 additions & 0 deletions Sources/Models/OsVersion.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ struct OsVersion {
let majorVersion: Int
let minorVersion: Int
let simulators: [SimulatorInfo]
}

extension OsVersion {
init?(string identifier: String, simulators: [SimulatorInfo]) {
guard let ident = identifier.split(separator: ".").last else { return nil }
let parts = ident.split(separator: "-")
Expand Down
54 changes: 54 additions & 0 deletions Sources/Shell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,57 @@
//

import Foundation

protocol Shell {
func execute(program: String, with arguments: [String]) -> Result<Data, Error>
}

struct ErrorResponse: Codable {
let message: String
let status: Int
}

struct ShellCommand: Shell {
private let decoder = JSONDecoder()

func execute(program: String, with arguments: [String]) -> Result<Data, Error> {
#if os(macOS)
let task = Process()
task.launchPath = program
task.arguments = arguments

// // comment out for debugging purposes:
// print("executing now:\n\(program) \(arguments.joined(separator: " "))")

let outPipe = Pipe()
task.standardOutput = outPipe // to capture standard error, use task.standardError = outPipe
let errorPipe = Pipe()
task.standardError = errorPipe
task.launch()
let fileHandle = outPipe.fileHandleForReading
let data = fileHandle.readDataToEndOfFile()
let errorHandle = errorPipe.fileHandleForReading
let errorData = errorHandle.readDataToEndOfFile()
task.waitUntilExit()
let status = task.terminationStatus
if status != 0 {
return .failure(NSError(message: String(data: errorData, encoding: .utf8) ?? "", status: Int(status)))
} else {
if let error = try? decoder.decode(ErrorResponse.self, from: data) {
return .failure(NSError(message: error.message, status: error.status))
}
// print(String(decoding: data, as: UTF8.self))
return .success(data)
}
#else
return .failure(NSError(message: "Works only on MacOS", status: -17))
#endif
}
}

private extension NSError {
convenience init(message: String, status: Int = 1) {
let domain = Bundle.main.object(forInfoDictionaryKey: "CFBundleIdentifier") as? String ?? "com.farbflash"
self.init(domain: "\(domain).error", code: status, userInfo: [NSLocalizedDescriptionKey: message])
}
}
96 changes: 47 additions & 49 deletions Sources/SimulatorControl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,6 @@ import Foundation
/// Interface to the simctl command line tool, which is part of the Xcode tools suite

struct SimulatorControl {
struct ErrorResponse: Codable {
let message: String
let status: Int
}

/// The os type. It can be either 'ios', 'watchos' or 'tvos'.
/// Note: Does only apply to filterSimulators().
let osFilter: String
Expand All @@ -27,21 +22,43 @@ struct SimulatorControl {
/// Note,: If 'majorOSVersion' is set to 'latest', then minor version will also be 'latest'. Does only apply to filterSimulators().
let minorVersionFilter: String

/// A string contains check on the name of the simulator.
/// A more flexible regex check on the name of the simulator.
let regexPattern: String

/// A simple "string.contains" check on the name of the simulator.
let nameFilter: String

private let shell: Shell
private let decoder = JSONDecoder()

// MARK: - Public interface

init(
osFilter: String,
majorVersionFilter: String,
minorVersionFilter: String,
nameFilter: String,
regexPattern: String,
shell: Shell = ShellCommand()
) {
self.osFilter = osFilter
self.majorVersionFilter = majorVersionFilter
self.minorVersionFilter = minorVersionFilter
self.nameFilter = nameFilter
self.regexPattern = regexPattern
self.shell = shell
}

/// Find simulators
/// - Returns: Array of OsVersions objects containing available simulators, which match the specified filters.
func filterSimulators() throws -> [OsVersion] {
let rslt = simulatorList(pattern: nameFilter)
switch rslt {
case .success(let pairs):
let (majorVersion, subVersion) = computeVersions(in: pairs, os: osFilter)
return pairs.enabledOSVersions
return pairs
.enabledOSVersions
.filteredByRegex(pattern: regexPattern)
.filter { $0.isEligible(for: osFilter, major: majorVersion, minor: subVersion)}

case .failure(let error):
Expand Down Expand Up @@ -107,42 +124,12 @@ struct SimulatorControl {
}

private func executeJSONTask<T: Decodable>(with arguments: [String]) -> Result<T, Error> {
let rslt = execute(program: "/usr/bin/xcrun", with: arguments)
let rslt = shell.execute(program: "/usr/bin/xcrun", with: arguments)
return rslt.flatMap { data in
let rslt = Result { try decoder.decode(T.self, from: data) }
return rslt
}
}

private func execute(program: String, with arguments: [String]) -> Result<Data, Error> {
let task = Process()
task.launchPath = program
task.arguments = arguments

// // comment out for debugging purposes:
// print("executing now:\n\(program) \(arguments.joined(separator: " "))")

let outPipe = Pipe()
task.standardOutput = outPipe // to capture standard error, use task.standardError = outPipe
let errorPipe = Pipe()
task.standardError = errorPipe
task.launch()
let fileHandle = outPipe.fileHandleForReading
let data = fileHandle.readDataToEndOfFile()
let errorHandle = errorPipe.fileHandleForReading
let errorData = errorHandle.readDataToEndOfFile()
task.waitUntilExit()
let status = task.terminationStatus
if status != 0 {
return .failure(NSError(message: String(data: errorData, encoding: .utf8) ?? "", status: Int(status)))
} else {
if let error = try? decoder.decode(ErrorResponse.self, from: data) {
return .failure(NSError(message: error.message, status: error.status))
}
// print(String(decoding: data, as: UTF8.self))
return .success(data)
}
}
}

/// Map result from simctl call
Expand All @@ -168,6 +155,28 @@ private extension SimulatorListResult {
}
}

extension Array where Element == OsVersion {
func filteredByRegex(pattern regexPattern: String) -> [OsVersion] {
guard let regex = try? Regex(regexPattern) else {
return self
}
return compactMap { osversion in
let filteredSimulators = osversion.simulators.filter { simulator in
return !simulator.name.ranges(of: regex).isEmpty
}
if !filteredSimulators.isEmpty {
return OsVersion(
name: osversion.name,
majorVersion: osversion.majorVersion,
minorVersion: osversion.minorVersion,
simulators: filteredSimulators
)
}
return nil
}
}
}

private extension OsVersion {
var containsEnabledSimulators: Bool {
return !simulators.filter({ $0.isAvailable == true }).isEmpty
Expand All @@ -178,14 +187,3 @@ private extension OsVersion {
(minor < 1 || minorVersion == minor)
}
}

private extension NSError {
convenience init(message: String, status: Int = 1) {
let domain = Bundle.main.object(forInfoDictionaryKey: "CFBundleIdentifier") as? String ?? "com.farbflash"
self.init(domain: "\(domain).error", code: status, userInfo: [NSLocalizedDescriptionKey: message])
}
static let noMajorVersionProvided: NSError = {
let domain = Bundle.main.object(forInfoDictionaryKey: "CFBundleIdentifier") as? String ?? "com.farbflash"
return NSError(domain: "\(domain).error", code: 2, userInfo: [NSLocalizedDescriptionKey: "When specifying 'latest' for the minor OS version, you must provide a majorVersion."])
}()
}
17 changes: 11 additions & 6 deletions Sources/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,20 @@
import Foundation
import ArgumentParser

private let marketingVersion = "0.2"
private let marketingVersion = "0.3"

struct findsimulator: ParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Interface to simctl in order to get suitable strings for destinations for the xcodebuild command."
)

@Option(name: .shortAndLong, help: "The os type. It can be either 'ios', 'watchos' or 'tvos'. Does only apply without '-pairs' option.")
@Option(name: .shortAndLong, help: "The os type. It can be either 'ios', 'watchos' or 'tvos'. Does only apply without '--pairs' option.")
var osType = "ios"

@Option(name: .shortAndLong, help: "The major OS version. Can be something like '12' or '14', 'all' or 'latest', which is the latest installed major version. Does only apply without '-pairs' option.")
@Option(name: .shortAndLong, help: "A regex pattern to match the device name. Does only apply without '--pairs' option.")
var regexPattern = ""

@Option(name: .shortAndLong, help: "The major OS version. Can be something like '12' or '14', 'all' or 'latest', which is the latest installed major version. Does only apply without '--pairs' option.")
var majorOSVersion = "all"

@Option(name: .shortAndLong, help: "The minor OS version. Can be something like '2' or '4', 'all' or 'latest', which is the latest installed minor version of a given major version. Note, if 'majorOSVersion' is set to 'latest', then minor version will also be 'latest'. Does only apply without '-pairs' option.")
Expand All @@ -33,8 +36,8 @@ struct findsimulator: ParsableCommand {
@Flag(name: .shortAndLong, help: "Print version of this tool.")
var version: Int

@Argument(help: "A string contains check on the name of the simulator.")
var name_contains = ""
@Argument(help: "A simple 'string contains' check on the name of the simulator. Use the [-r | --regex-pattern] option for more finegrained searches instead.")
var nameContains = ""

mutating func run() throws {
guard version != 1 else {
Expand All @@ -45,7 +48,8 @@ struct findsimulator: ParsableCommand {
osFilter: osType,
majorVersionFilter: majorOSVersion,
minorVersionFilter: subOSVersion,
nameFilter: name_contains
nameFilter: nameContains,
regexPattern: regexPattern
)
if pairs == 1 {
let sims = (try controller.filterSimulatorPairs()).sorted(by: { $0.name > $1.name})
Expand All @@ -69,6 +73,7 @@ struct findsimulator: ParsableCommand {
}
}
} else {

if let firstVersion = versions.first,
let first = firstVersion.simulators.sorted(by: { $0.name > $1.name}).first {
print("platform=\(firstVersion.platform),id=\(first.udid)")
Expand Down
4 changes: 2 additions & 2 deletions Tests/LinuxMain.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
// Created by Alex da Franca on 26.12.21.
//

import XcresultparserTests
import findsimulatorTests
import XCTest

var tests = [XCTestCaseEntry]()
tests += XcresultparserTests.allTests()
tests += findsimulatorTests.allTests()
XCTMain(tests)
Loading

0 comments on commit 99e0925

Please sign in to comment.