Skip to content

Commit

Permalink
Add simulated location support on testable target configuration (#6187)
Browse files Browse the repository at this point in the history
* Support simulated location for testable targets

* Fix typo in default location

* Add simulated location to `ios_app_with_custom_scheme` fixture

* Add test for finding GPX files

* Avoid breaking compatibility

* Add assertions for simulated location

* Make assertion method clear
  • Loading branch information
woin2ee committed Apr 22, 2024
1 parent 15af7d8 commit afcd2dc
Show file tree
Hide file tree
Showing 16 changed files with 395 additions and 143 deletions.
81 changes: 8 additions & 73 deletions Sources/ProjectDescription/RunActionOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public struct RunActionOptions: Equatable, Codable {
public var storeKitConfigurationPath: Path?

/// A simulated GPS location to use when running the app.
public var simulatedLocation: SimulatedLocation?
public var simulatedLocation: ProjectDescription.SimulatedLocation?

/// Configure your project to work with the Metal frame debugger.
public var enableGPUFrameCaptureMode: GPUFrameCaptureMode
Expand All @@ -40,7 +40,7 @@ public struct RunActionOptions: Equatable, Codable {
language: SchemeLanguage? = nil,
region: String? = nil,
storeKitConfigurationPath: Path? = nil,
simulatedLocation: SimulatedLocation? = nil,
simulatedLocation: ProjectDescription.SimulatedLocation? = nil,
enableGPUFrameCaptureMode: GPUFrameCaptureMode = GPUFrameCaptureMode.default
) {
self.language = language
Expand Down Expand Up @@ -74,7 +74,7 @@ public struct RunActionOptions: Equatable, Codable {
language: SchemeLanguage? = nil,
region: String? = nil,
storeKitConfigurationPath: Path? = nil,
simulatedLocation: SimulatedLocation? = nil,
simulatedLocation: ProjectDescription.SimulatedLocation? = nil,
enableGPUFrameCaptureMode: GPUFrameCaptureMode = GPUFrameCaptureMode.default
) -> Self {
self.init(
Expand All @@ -87,76 +87,6 @@ public struct RunActionOptions: Equatable, Codable {
}
}

extension RunActionOptions {
/// Simulated location represents a GPS location that is used when running an app on the simulator.
public struct SimulatedLocation: Codable, Equatable {
/// The identifier of the location (e.g. London, England)
public var identifier: String?
/// Path to a .gpx file that indicates the location
public var gpxFile: Path?

private init(
identifier: String? = nil,
gpxFile: Path? = nil
) {
self.identifier = identifier
self.gpxFile = gpxFile
}

public static func custom(gpxFile: Path) -> SimulatedLocation {
.init(gpxFile: gpxFile)
}

public static var london: SimulatedLocation {
.init(identifier: "London, England")
}

public static var johannesburg: SimulatedLocation {
.init(identifier: "Johannesburg, South Africa")
}

public static var moscow: SimulatedLocation {
.init(identifier: "Moscow, Russia")
}

public static var mumbai: SimulatedLocation {
.init(identifier: "Mumbai, India")
}

public static var tokyo: SimulatedLocation {
.init(identifier: "Tokyo, Japan")
}

public static var sydney: SimulatedLocation {
.init(identifier: "Sydney, Australia")
}

public static var hongKong: SimulatedLocation {
.init(identifier: "Hong Kong, China")
}

public static var honolulu: SimulatedLocation {
.init(identifier: "Honolulu, HI, USA")
}

public static var sanFrancisco: SimulatedLocation {
.init(identifier: "San Francisco, CA, USA")
}

public static var mexicoCity: SimulatedLocation {
.init(identifier: "Mexico City, Mexico")
}

public static var newYork: SimulatedLocation {
.init(identifier: "New York, NY, USA")
}

public static var rioDeJaneiro: SimulatedLocation {
.init(identifier: "Rio De Janeiro, Brazil")
}
}
}

extension RunActionOptions {
public enum GPUFrameCaptureMode: String, Codable, Equatable {
case autoEnabled
Expand All @@ -169,3 +99,8 @@ extension RunActionOptions {
}
}
}

extension RunActionOptions {
@available(*, deprecated, message: "Use ProjectDescription.SimulatedLocation directly instead.")
public typealias SimulatedLocation = ProjectDescription.SimulatedLocation
}
69 changes: 69 additions & 0 deletions Sources/ProjectDescription/SimulatedLocation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import Foundation

/// Simulated location represents a GPS location that is used when running an app on the simulator.
public struct SimulatedLocation: Codable, Equatable {
/// The identifier of the location (e.g. London, England)
public var identifier: String?
/// Path to a .gpx file that indicates the location
public var gpxFile: Path?

private init(
identifier: String? = nil,
gpxFile: Path? = nil
) {
self.identifier = identifier
self.gpxFile = gpxFile
}

public static func custom(gpxFile: Path) -> SimulatedLocation {
.init(gpxFile: gpxFile)
}

public static var london: SimulatedLocation {
.init(identifier: "London, England")
}

public static var johannesburg: SimulatedLocation {
.init(identifier: "Johannesburg, South Africa")
}

public static var moscow: SimulatedLocation {
.init(identifier: "Moscow, Russia")
}

public static var mumbai: SimulatedLocation {
.init(identifier: "Mumbai, India")
}

public static var tokyo: SimulatedLocation {
.init(identifier: "Tokyo, Japan")
}

public static var sydney: SimulatedLocation {
.init(identifier: "Sydney, Australia")
}

public static var hongKong: SimulatedLocation {
.init(identifier: "Hong Kong, China")
}

public static var honolulu: SimulatedLocation {
.init(identifier: "Honolulu, HI, USA")
}

public static var sanFrancisco: SimulatedLocation {
.init(identifier: "San Francisco, CA, USA")
}

public static var mexicoCity: SimulatedLocation {
.init(identifier: "Mexico City, Mexico")
}

public static var newYork: SimulatedLocation {
.init(identifier: "New York, NY, USA")
}

public static var rioDeJaneiro: SimulatedLocation {
.init(identifier: "Rio de Janeiro, Brazil")
}
}
23 changes: 19 additions & 4 deletions Sources/ProjectDescription/TestableTarget.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,44 @@ public struct TestableTarget: Equatable, Codable, ExpressibleByStringInterpolati
public var isSkipped: Bool
public var isParallelizable: Bool
public var isRandomExecutionOrdering: Bool
public var simulatedLocation: SimulatedLocation?

init(
target: TargetReference,
isSkipped: Bool,
isParallelizable: Bool,
isRandomExecutionOrdering: Bool
isRandomExecutionOrdering: Bool,
simulatedLocation: SimulatedLocation? = nil
) {
self.target = target
self.isSkipped = isSkipped
self.isParallelizable = isParallelizable
self.isRandomExecutionOrdering = isRandomExecutionOrdering
self.simulatedLocation = simulatedLocation
}

/// Returns a testable target.
///
/// - Parameters:
/// - target: The name or reference of target to test.
/// - isSkipped: Whether to skip this test target. If true, the test target is disabled.
/// - isParallelizable: Whether to run in parallel.
/// - isRandomExecutionOrdering: Whether to test in random order.
/// - simulatedLocation: The simulated GPS location to use when testing this target.
/// Please note that the `.custom(gpxPath:)` case must refer to a valid GPX file in your project’s resources.
public static func testableTarget(
target: TargetReference,
isSkipped: Bool = false,
isParallelizable: Bool = false,
isRandomExecutionOrdering: Bool = false
isRandomExecutionOrdering: Bool = false,
simulatedLocation: SimulatedLocation? = nil
) -> Self {
self.init(
target: target,
isSkipped: isSkipped,
isParallelizable: isParallelizable,
isRandomExecutionOrdering: isRandomExecutionOrdering
isRandomExecutionOrdering: isRandomExecutionOrdering,
simulatedLocation: simulatedLocation
)
}

Expand All @@ -37,7 +51,8 @@ public struct TestableTarget: Equatable, Codable, ExpressibleByStringInterpolati
target: TargetReference(projectPath: nil, target: value),
isSkipped: false,
isParallelizable: false,
isRandomExecutionOrdering: false
isRandomExecutionOrdering: false,
simulatedLocation: nil
)
}
}
50 changes: 50 additions & 0 deletions Sources/TuistAcceptanceTesting/TuistAcceptanceTestCase+Extra.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,54 @@ extension TuistAcceptanceTestCase {
return
}
}

/// Asserts that a simulated location is contained in a specific testable target.
/// - Parameters:
/// - xcodeprojPath: A specific `.xcodeproj` file path.
/// - scheme: A specific scheme name.
/// - testTarget: A specific test target name.
/// - simulatedLocation: A simulated location. This value can be passed a `location string` or a `GPX filename`.
/// For example, "Rio de Janeiro, Brazil" or "Grand Canyon.gpx".
public func XCTAssertContainsSimulatedLocation(
xcodeprojPath: AbsolutePath,
scheme: String,
testTarget: String,
simulatedLocation: String,
file: StaticString = #file,
line: UInt = #line
) throws {
let xcodeproj = try XcodeProj(pathString: xcodeprojPath.pathString)

guard let scheme = xcodeproj.sharedData?.schemes
.filter({ $0.name == scheme })
.first
else {
XCTFail(
"The '\(scheme)' scheme doesn't exist.",
file: file,
line: line
)
return
}

guard let testableTarget = scheme.testAction?.testables
.filter({ $0.buildableReference.blueprintName == testTarget })
.first
else {
XCTFail(
"The '\(testTarget)' testable target doesn't exist.",
file: file,
line: line
)
return
}

XCTAssertEqual(
testableTarget.locationScenarioReference?.identifier.contains(simulatedLocation),
true,
"The '\(testableTarget)' testable target doesn't have simulated location set.",
file: file,
line: line
)
}
}
45 changes: 37 additions & 8 deletions Sources/TuistGenerator/Generator/ProjectFileElements.swift
Original file line number Diff line number Diff line change
Expand Up @@ -127,15 +127,11 @@ class ProjectFileElements {
// Add the .gpx files if needed. GPS Exchange files must be added to the
// project/workspace so that the scheme can correctly reference them.
// In case the configuration already contains such file, we should avoid adding it twice
let gpxFiles = project.schemes.compactMap { scheme -> GroupFileElement? in
guard case let .gpxFile(path) = scheme.runAction?.options.simulatedLocation else {
return nil
}

return GroupFileElement(path: path, group: project.filesGroup)
}
let runActionGPXFiles = gpxFilesForRunAction(in: project.schemes, filesGroup: project.filesGroup)
fileElements.formUnion(runActionGPXFiles)

fileElements.formUnion(gpxFiles)
let testActionGPXFiles = gpxFilesForTestAction(in: project.schemes, filesGroup: project.filesGroup)
fileElements.formUnion(testActionGPXFiles)

return fileElements
}
Expand Down Expand Up @@ -739,4 +735,37 @@ class ProjectFileElements {
return nil
}
}

/// Finds and returns the gpx files used by the Run Action for all schemes.
func gpxFilesForRunAction(in schemes: [Scheme], filesGroup: ProjectGroup) -> [GroupFileElement] {
let gpxFiles = schemes.compactMap { scheme -> GroupFileElement? in
guard case let .gpxFile(path) = scheme.runAction?.options.simulatedLocation else {
return nil
}

return GroupFileElement(path: path, group: filesGroup)
}

return gpxFiles
}

/// Finds and returns the gpx files used by the Test Action for all schemes.
func gpxFilesForTestAction(in schemes: [Scheme], filesGroup: ProjectGroup) -> [GroupFileElement] {
let gpxFiles = schemes.compactMap { scheme -> [GroupFileElement] in
guard let testAction = scheme.testAction else { return [] }

let elements = testAction.targets.compactMap { target -> GroupFileElement? in
guard case let .gpxFile(path) = target.simulatedLocation else {
return nil
}

return GroupFileElement(path: path, group: filesGroup)
}

return elements
}
.flatMap { $0 }

return gpxFiles
}
}
18 changes: 18 additions & 0 deletions Sources/TuistGenerator/Generator/SchemeDescriptorsGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -298,11 +298,29 @@ final class SchemeDescriptorsGenerator: SchemeDescriptorsGenerating {
else {
continue
}

var locationScenarioReference: XCScheme.LocationScenarioReference?

if let locationScenario = testableTarget.simulatedLocation {
var identifier = locationScenario.identifier

if case let .gpxFile(gpxPath) = locationScenario {
let fileRelativePath = gpxPath.relative(to: graphTraverser.workspace.xcWorkspacePath)
identifier = fileRelativePath.pathString
}

locationScenarioReference = .init(
identifier: identifier,
referenceType: locationScenario.referenceType
)
}

let testable = XCScheme.TestableReference(
skipped: testableTarget.isSkipped,
parallelizable: testableTarget.isParallelizable,
randomExecutionOrdering: testableTarget.isRandomExecutionOrdering,
buildableReference: reference,
locationScenarioReference: locationScenarioReference,
skippedTests: skippedTests
)
testables.append(testable)
Expand Down

0 comments on commit afcd2dc

Please sign in to comment.