diff --git a/.gitignore b/.gitignore index 2b4de2e..67f35b8 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ DerivedData/ .swiftpm/config/registries.json .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata .netrc +.serena/ diff --git a/Configuration.md b/Configuration.md index ec982b5..e45e1c8 100644 --- a/Configuration.md +++ b/Configuration.md @@ -42,7 +42,7 @@ playbook_configuration: | Key | Description | | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | `target` | Target name used for snapshot generation. Default: *FirstTarget* | -| `test_target_path` | Path to unit test directory. Snapshots will be written to its `__Snapshots__` folder. Default: target name folder | +| `test_target_path` | Path to unit test directory. Snapshots will be written to its `__Snapshots__` folder. Supports template parameters (see below). Default: target name folder | | `test_file_path` | Output file path for generated tests. Default: DerivedData or resolved via plugin | | `template_file_path` | Custom template path relative to target. Optional. Defaults:‣ *PreviewTests.stencil* for test plugin‣ *PreviewModels.stencil* for playbook plugin| | `simulator_device` | Device identifier used to run tests (e.g. `iPhone15,2`). Optional | @@ -56,6 +56,20 @@ playbook_configuration: --- +### 🎯 Template Parameters + +The `test_target_path` configuration supports template placeholders `${TARGET}` and `${TEST_TARGET}` that are dynamically replaced at runtime. Useful for multi-target projects and monorepos. + +**Example:** +```yaml +test_configuration: + target: MyApp + test_target_path: "${PROJECT_DIR}/${TARGET}/Tests" + # Resolves to: ${PROJECT_DIR}/MyApp/Tests +``` + +--- + 📌 You can define both `test_configuration` and `playbook_configuration` at once. Prefire will use these settings when generating files either via: diff --git a/PrefireExecutable/Sources/prefire/Commands/Playbook/GeneratePlaybookCommand.swift b/PrefireExecutable/Sources/prefire/Commands/Playbook/GeneratePlaybookCommand.swift index 48cfe03..364d5b5 100644 --- a/PrefireExecutable/Sources/prefire/Commands/Playbook/GeneratePlaybookCommand.swift +++ b/PrefireExecutable/Sources/prefire/Commands/Playbook/GeneratePlaybookCommand.swift @@ -7,34 +7,46 @@ private enum Constants { } struct GeneratedPlaybookOptions { - var targetPath: String? - var sources: [Path] - var output: Path - var previewDefaultEnabled: Bool - var template: Path? - var cacheBasePath: Path? - var imports: [String]? - var testableImports: [String]? - - init(targetPath: String?, sources: [String], output: String?, template: String?, cacheBasePath: String?, config: Config?) throws { - self.targetPath = config?.playbook.targetPath ?? targetPath - self.sources = sources.isEmpty ? [.current] : sources.compactMap({ Path($0) }) + let targetPath: String? + let sources: [Path] + let output: Path + let previewDefaultEnabled: Bool + let template: Path? + let cacheBasePath: Path? + let imports: [String]? + let testableImports: [String]? +} - self.output = (output.flatMap({ Path($0) }) ?? .current) + Constants.defaultOutputName +// MARK: - Factory - previewDefaultEnabled = config?.playbook.previewDefaultEnabled ?? true +extension GeneratedPlaybookOptions { + /// Creates GeneratedPlaybookOptions by merging CLI options with Config + /// - Parameters: + /// - cli: Raw CLI options + /// - config: Loaded config (optional) + /// - Returns: Fully resolved GeneratedPlaybookOptions + static func from( + cli: CLIPlaybookOptions, + config: Config? + ) -> GeneratedPlaybookOptions { + let targetPath = config?.playbook.targetPath ?? cli.targetPath - if let template = config?.playbook.template, let targetPath { - let targetURL = URL(filePath: targetPath) - let templateURL = targetURL.appending(path: template) - self.template = Path(templateURL.absoluteURL.path(percentEncoded: false)) - } else if let template { - self.template = Path(template) - } + let template = OptionsResolver.resolveTemplate( + cliTemplate: cli.template, + configTemplate: config?.playbook.template, + targetPath: targetPath.flatMap { Path($0) } + ) - self.cacheBasePath = cacheBasePath.flatMap({ Path($0) }) - imports = config?.playbook.imports - testableImports = config?.playbook.testableImports + return GeneratedPlaybookOptions( + targetPath: targetPath, + sources: OptionsResolver.resolveSources(cli.sources), + output: OptionsResolver.resolveOutput(cli.output, defaultFileName: Constants.defaultOutputName), + previewDefaultEnabled: config?.playbook.previewDefaultEnabled ?? true, + template: template, + cacheBasePath: cli.cacheBasePath.flatMap { Path($0) }, + imports: config?.playbook.imports, + testableImports: config?.playbook.testableImports + ) } } diff --git a/PrefireExecutable/Sources/prefire/Commands/Playbook/Playbook.swift b/PrefireExecutable/Sources/prefire/Commands/Playbook/Playbook.swift index 776b21a..526f545 100644 --- a/PrefireExecutable/Sources/prefire/Commands/Playbook/Playbook.swift +++ b/PrefireExecutable/Sources/prefire/Commands/Playbook/Playbook.swift @@ -26,18 +26,27 @@ extension Prefire { func run() async throws { Logger.level = verbose ? .verbose : .warnings - let config = Config.load(from: config, testTargetPath: nil, env: ProcessInfo.processInfo.environment) - - try await GeneratePlaybookCommand.run( - GeneratedPlaybookOptions( - targetPath: targetPath, - sources: sources, - output: output, - template: template, - cacheBasePath: cacheBasePath, - config: config - ) + + let cliOptions = CLIPlaybookOptions( + targetPath: targetPath, + template: template, + sources: sources, + output: output, + cacheBasePath: cacheBasePath + ) + + let loadedConfig = Config.load( + from: config, + testTargetPath: nil, + env: ProcessInfo.processInfo.environment ) + + let options = GeneratedPlaybookOptions.from( + cli: cliOptions, + config: loadedConfig + ) + + try await GeneratePlaybookCommand.run(options) } } } diff --git a/PrefireExecutable/Sources/prefire/Commands/Tests/GenerateTestsCommand.swift b/PrefireExecutable/Sources/prefire/Commands/Tests/GenerateTestsCommand.swift index 8a9f5ba..453b52b 100644 --- a/PrefireExecutable/Sources/prefire/Commands/Tests/GenerateTestsCommand.swift +++ b/PrefireExecutable/Sources/prefire/Commands/Tests/GenerateTestsCommand.swift @@ -8,55 +8,67 @@ private enum Constants { } struct GeneratedTestsOptions { - var target: String? - var testTarget: String? - var template: Path? - var sources: [Path] - var output: Path - var prefireEnabledMarker: Bool - var testTargetPath: Path? - var cacheBasePath: Path? - var device: String? - var osVersion: String? - var snapshotDevices: [String]? - var imports: [String]? - var testableImports: [String]? - var useGroupedSnapshots: Bool + let target: String? + let testTarget: String? + let template: Path? + let sources: [Path] + let output: Path + let prefireEnabledMarker: Bool + let testTargetPath: Path? + let cacheBasePath: Path? + let device: String? + let osVersion: String? + let snapshotDevices: [String]? + let imports: [String]? + let testableImports: [String]? + let useGroupedSnapshots: Bool +} + +// MARK: - Factory + +extension GeneratedTestsOptions { + /// Creates GeneratedTestsOptions by merging CLI options with Config + /// - Parameters: + /// - cli: Raw CLI options + /// - config: Loaded config (optional) + /// - Returns: Fully resolved GeneratedTestsOptions + static func from(cli: CLITestsOptions, config: Config?) -> GeneratedTestsOptions { + let target = config?.tests.target ?? cli.target + let testTarget = cli.testTarget + let sources = config?.tests.sources ?? cli.sources + let output = config?.tests.testFilePath ?? cli.output + let device = config?.tests.device ?? cli.device + let osVersion = config?.tests.osVersion ?? cli.osVersion - init( - target: String?, - testTarget: String?, - template: String?, - sources: [String], - output: String?, - testTargetPath: String?, - cacheBasePath: String?, - device: String?, - osVersion: String?, - config: Config? - ) throws { - self.target = config?.tests.target ?? target - self.testTarget = testTarget - self.testTargetPath = (config?.tests.testTargetPath ?? testTargetPath).flatMap({ Path($0) }) + let rawTestTargetPath = config?.tests.testTargetPath ?? cli.testTargetPath + let testTargetPath = OptionsResolver.resolvePath( + rawTestTargetPath, + target: target, + testTarget: testTarget + ) - if let template = config?.tests.template, let testTargetPath = self.testTargetPath { - let testTargetURL = URL(filePath: testTargetPath.string) - let templateURL = testTargetURL.appending(path: template) - self.template = Path(templateURL.absoluteURL.path(percentEncoded: false)) - } else if let template { - self.template = Path(template) - } + let template = OptionsResolver.resolveTemplate( + cliTemplate: cli.template, + configTemplate: config?.tests.template, + targetPath: testTargetPath + ) - self.sources = (config?.tests.sources ?? sources).compactMap({ Path($0) }) - self.output = (config?.tests.testFilePath ?? output).flatMap({ Path($0) }) ?? .current - prefireEnabledMarker = config?.tests.previewDefaultEnabled ?? true - self.cacheBasePath = cacheBasePath.flatMap({ Path($0) }) - self.device = config?.tests.device ?? device - self.osVersion = config?.tests.osVersion ?? osVersion - useGroupedSnapshots = config?.tests.useGroupedSnapshots ?? true - snapshotDevices = config?.tests.snapshotDevices - imports = config?.tests.imports - testableImports = config?.tests.testableImports + return GeneratedTestsOptions( + target: target, + testTarget: testTarget, + template: template, + sources: OptionsResolver.resolveSources(sources), + output: output.flatMap { Path($0) } ?? .current, + prefireEnabledMarker: config?.tests.previewDefaultEnabled ?? true, + testTargetPath: testTargetPath, + cacheBasePath: cli.cacheBasePath.flatMap { Path($0) }, + device: device, + osVersion: osVersion, + snapshotDevices: config?.tests.snapshotDevices, + imports: config?.tests.imports, + testableImports: config?.tests.testableImports, + useGroupedSnapshots: config?.tests.useGroupedSnapshots ?? true + ) } } diff --git a/PrefireExecutable/Sources/prefire/Commands/Tests/Tests.swift b/PrefireExecutable/Sources/prefire/Commands/Tests/Tests.swift index 4a0d385..eca28ed 100644 --- a/PrefireExecutable/Sources/prefire/Commands/Tests/Tests.swift +++ b/PrefireExecutable/Sources/prefire/Commands/Tests/Tests.swift @@ -35,22 +35,31 @@ extension Prefire { func run() async throws { Logger.level = verbose ? .verbose : .warnings - let config = Config.load(from: config, testTargetPath: testTargetPath, env: ProcessInfo.processInfo.environment) - - try await GenerateTestsCommand.run( - GeneratedTestsOptions( - target: target, - testTarget: testTarget, - template: template, - sources: sources, - output: output, - testTargetPath: testTargetPath, - cacheBasePath: cacheBasePath, - device: device, - osVersion: osVersion, - config: config - ) + + let cliOptions = CLITestsOptions( + target: target, + testTarget: testTarget, + template: template, + sources: sources, + output: output, + testTargetPath: testTargetPath, + cacheBasePath: cacheBasePath, + device: device, + osVersion: osVersion + ) + + let loadedConfig = Config.load( + from: config, + testTargetPath: testTargetPath, + env: ProcessInfo.processInfo.environment ) + + let options = GeneratedTestsOptions.from( + cli: cliOptions, + config: loadedConfig + ) + + try await GenerateTestsCommand.run(options) } } } diff --git a/PrefireExecutable/Sources/prefire/Config/ConfigPathResolver.swift b/PrefireExecutable/Sources/prefire/Config/ConfigPathResolver.swift new file mode 100644 index 0000000..2d08be4 --- /dev/null +++ b/PrefireExecutable/Sources/prefire/Config/ConfigPathResolver.swift @@ -0,0 +1,40 @@ +import Foundation + +/// Utility for resolving template placeholders in configuration paths +/// +/// Supports the following placeholders: +/// - `${TARGET}` - Replaced with the target name +/// - `${TEST_TARGET}` - Replaced with the test target name +/// +/// Example: +/// ```swift +/// let path = "${PROJECT_DIR}/${TARGET}/Tests" +/// let resolved = ConfigPathResolver.resolve(path, target: "MyApp") +/// // Result: "${PROJECT_DIR}/MyApp/Tests" +/// ``` +enum ConfigPathResolver { + /// Resolves template placeholders in a configuration path + /// + /// - Parameters: + /// - path: The path string containing placeholders + /// - target: Optional target name to replace ${TARGET} + /// - testTarget: Optional test target name to replace ${TEST_TARGET} + /// - Returns: Resolved path with placeholders replaced by their values + static func resolve( + _ path: String, + target: String? = nil, + testTarget: String? = nil + ) -> String { + var result = path + + if let target { + result = result.replacingOccurrences(of: "${TARGET}", with: target) + } + + if let testTarget { + result = result.replacingOccurrences(of: "${TEST_TARGET}", with: testTarget) + } + + return result + } +} diff --git a/PrefireExecutable/Sources/prefire/Options/CLIPlaybookOptions.swift b/PrefireExecutable/Sources/prefire/Options/CLIPlaybookOptions.swift new file mode 100644 index 0000000..689991e --- /dev/null +++ b/PrefireExecutable/Sources/prefire/Options/CLIPlaybookOptions.swift @@ -0,0 +1,10 @@ +import Foundation + +/// Raw CLI options for Playbook command - no merging or resolution logic +struct CLIPlaybookOptions { + let targetPath: String? + let template: String? + let sources: [String] + let output: String? + let cacheBasePath: String? +} diff --git a/PrefireExecutable/Sources/prefire/Options/CLITestsOptions.swift b/PrefireExecutable/Sources/prefire/Options/CLITestsOptions.swift new file mode 100644 index 0000000..fe507c4 --- /dev/null +++ b/PrefireExecutable/Sources/prefire/Options/CLITestsOptions.swift @@ -0,0 +1,14 @@ +import Foundation + +/// Raw CLI options for Tests command - no merging or resolution logic +struct CLITestsOptions { + let target: String? + let testTarget: String? + let template: String? + let sources: [String] + let output: String? + let testTargetPath: String? + let cacheBasePath: String? + let device: String? + let osVersion: String? +} diff --git a/PrefireExecutable/Sources/prefire/Options/OptionsResolver.swift b/PrefireExecutable/Sources/prefire/Options/OptionsResolver.swift new file mode 100644 index 0000000..d5ebe4f --- /dev/null +++ b/PrefireExecutable/Sources/prefire/Options/OptionsResolver.swift @@ -0,0 +1,66 @@ +import Foundation +import PathKit + +/// Shared resolution logic for command options +enum OptionsResolver { + /// Resolves template path - if config template exists, resolves relative to target path + /// - Parameters: + /// - cliTemplate: Template path from CLI argument + /// - configTemplate: Template path from config file (relative to targetPath) + /// - targetPath: Base path for resolving config template + /// - Returns: Resolved template path or nil + static func resolveTemplate( + cliTemplate: String?, + configTemplate: String?, + targetPath: Path? + ) -> Path? { + if let configTemplate, let targetPath { + let targetURL = URL(filePath: targetPath.string) + let templateURL = targetURL.appending(path: configTemplate) + return Path(templateURL.absoluteURL.path(percentEncoded: false)) + } + return cliTemplate.map { Path($0) } + } + + /// Resolves path string with template placeholders (${TARGET}, ${TEST_TARGET}) + /// - Parameters: + /// - path: Path string potentially containing placeholders + /// - target: Target name to replace ${TARGET} + /// - testTarget: Test target name to replace ${TEST_TARGET} + /// - Returns: Resolved Path or nil + static func resolvePath( + _ path: String?, + target: String?, + testTarget: String? + ) -> Path? { + guard let path else { return nil } + let resolved = ConfigPathResolver.resolve(path, target: target, testTarget: testTarget) + return Path(resolved) + } + + /// Converts string array to Path array + /// - Parameters: + /// - sources: Array of path strings + /// - default: Default value if sources is nil or empty + /// - Returns: Array of Path objects + static func resolveSources( + _ sources: [String]?, + default defaultValue: [Path] = [.current] + ) -> [Path] { + guard let sources, !sources.isEmpty else { return defaultValue } + return sources.compactMap { Path($0) } + } + + /// Resolves output path with default filename + /// - Parameters: + /// - output: Output path string + /// - defaultFileName: Default filename to append if output is directory + /// - Returns: Resolved output Path + static func resolveOutput( + _ output: String?, + defaultFileName: String + ) -> Path { + let basePath = output.flatMap { Path($0) } ?? .current + return basePath + defaultFileName + } +} diff --git a/PrefireExecutable/Tests/PrefireTests/ConfigPathResolverTests.swift b/PrefireExecutable/Tests/PrefireTests/ConfigPathResolverTests.swift new file mode 100644 index 0000000..11ca5ad --- /dev/null +++ b/PrefireExecutable/Tests/PrefireTests/ConfigPathResolverTests.swift @@ -0,0 +1,152 @@ +import Foundation +@testable import prefire +import XCTest + +class ConfigPathResolverTests: XCTestCase { + // MARK: - Target Placeholder Tests + + func test_resolve_replacesTargetPlaceholder() { + let path = "${PROJECT_DIR}/${TARGET}/Tests" + let result = ConfigPathResolver.resolve(path, target: "MyApp") + + XCTAssertEqual(result, "${PROJECT_DIR}/MyApp/Tests") + } + + func test_resolve_replacesTargetPlaceholder_multipleOccurrences() { + let path = "${TARGET}/Sources/${TARGET}/Tests" + let result = ConfigPathResolver.resolve(path, target: "CoreModule") + + XCTAssertEqual(result, "CoreModule/Sources/CoreModule/Tests") + } + + func test_resolve_doesNotReplaceTargetPlaceholder_whenTargetIsNil() { + let path = "${PROJECT_DIR}/${TARGET}/Tests" + let result = ConfigPathResolver.resolve(path, target: nil) + + XCTAssertEqual(result, "${PROJECT_DIR}/${TARGET}/Tests") + } + + // MARK: - TestTarget Placeholder Tests + + func test_resolve_replacesTestTargetPlaceholder() { + let path = "${PROJECT_DIR}/Tests/${TEST_TARGET}" + let result = ConfigPathResolver.resolve(path, testTarget: "MyAppTests") + + XCTAssertEqual(result, "${PROJECT_DIR}/Tests/MyAppTests") + } + + func test_resolve_replacesTestTargetPlaceholder_multipleOccurrences() { + let path = "${TEST_TARGET}/Snapshots/${TEST_TARGET}" + let result = ConfigPathResolver.resolve(path, testTarget: "UnitTests") + + XCTAssertEqual(result, "UnitTests/Snapshots/UnitTests") + } + + func test_resolve_doesNotReplaceTestTargetPlaceholder_whenTestTargetIsNil() { + let path = "${PROJECT_DIR}/Tests/${TEST_TARGET}" + let result = ConfigPathResolver.resolve(path, testTarget: nil) + + XCTAssertEqual(result, "${PROJECT_DIR}/Tests/${TEST_TARGET}") + } + + // MARK: - Combined Placeholder Tests + + func test_resolve_replacesBothPlaceholders() { + let path = "${PROJECT_DIR}/${TARGET}/Tests/${TEST_TARGET}" + let result = ConfigPathResolver.resolve( + path, + target: "FeatureA", + testTarget: "FeatureATests" + ) + + XCTAssertEqual(result, "${PROJECT_DIR}/FeatureA/Tests/FeatureATests") + } + + func test_resolve_replacesBothPlaceholders_complexPath() { + let path = "Modules/${TARGET}/Testing/${TEST_TARGET}/Snapshots/${TARGET}" + let result = ConfigPathResolver.resolve( + path, + target: "ShoppingCart", + testTarget: "ShoppingCartUnitTests" + ) + + XCTAssertEqual(result, "Modules/ShoppingCart/Testing/ShoppingCartUnitTests/Snapshots/ShoppingCart") + } + + func test_resolve_doesNotReplacePlaceholders_whenBothParametersAreNil() { + let path = "${PROJECT_DIR}/${TARGET}/Tests/${TEST_TARGET}" + let result = ConfigPathResolver.resolve(path, target: nil, testTarget: nil) + + XCTAssertEqual(result, "${PROJECT_DIR}/${TARGET}/Tests/${TEST_TARGET}") + } + + // MARK: - Edge Cases + + func test_resolve_noPlaceholders_returnsOriginalPath() { + let path = "${PROJECT_DIR}/Tests/Snapshots" + let result = ConfigPathResolver.resolve(path, target: "MyApp", testTarget: "MyAppTests") + + XCTAssertEqual(result, "${PROJECT_DIR}/Tests/Snapshots") + } + + func test_resolve_emptyPath_returnsEmptyPath() { + let result = ConfigPathResolver.resolve("", target: "MyApp") + + XCTAssertEqual(result, "") + } + + func test_resolve_emptyTargetValue_doesNotReplace() { + let path = "${TARGET}/Tests" + let result = ConfigPathResolver.resolve(path, target: "") + + XCTAssertEqual(result, "/Tests") + } + + func test_resolve_emptyTestTargetValue_doesNotReplace() { + let path = "Tests/${TEST_TARGET}" + let result = ConfigPathResolver.resolve(path, testTarget: "") + + XCTAssertEqual(result, "Tests/") + } + + func test_resolve_pathWithSpaces() { + let path = "My Project/${TARGET}/My Tests" + let result = ConfigPathResolver.resolve(path, target: "My App") + + XCTAssertEqual(result, "My Project/My App/My Tests") + } + + func test_resolve_pathWithSpecialCharacters() { + let path = "Project-${TARGET}_Tests/${TEST_TARGET}.snapshot" + let result = ConfigPathResolver.resolve( + path, + target: "Core-Module", + testTarget: "Core_Tests" + ) + + XCTAssertEqual(result, "Project-Core-Module_Tests/Core_Tests.snapshot") + } + + // MARK: - No Parameters + + func test_resolve_withNoParameters_returnsOriginalPath() { + let path = "${PROJECT_DIR}/${TARGET}/Tests/${TEST_TARGET}" + let result = ConfigPathResolver.resolve(path) + + XCTAssertEqual(result, "${PROJECT_DIR}/${TARGET}/Tests/${TEST_TARGET}") + } + + func test_resolve_onlyTarget_leavesTestTargetPlaceholder() { + let path = "${TARGET}/Tests/${TEST_TARGET}" + let result = ConfigPathResolver.resolve(path, target: "MyApp") + + XCTAssertEqual(result, "MyApp/Tests/${TEST_TARGET}") + } + + func test_resolve_onlyTestTarget_leavesTargetPlaceholder() { + let path = "${TARGET}/Tests/${TEST_TARGET}" + let result = ConfigPathResolver.resolve(path, testTarget: "MyTests") + + XCTAssertEqual(result, "${TARGET}/Tests/MyTests") + } +} diff --git a/PrefireExecutable/Tests/PrefireTests/GenerateTestsCommandTest.swift b/PrefireExecutable/Tests/PrefireTests/GenerateTestsCommandTest.swift index 1290962..27d3ca8 100644 --- a/PrefireExecutable/Tests/PrefireTests/GenerateTestsCommandTest.swift +++ b/PrefireExecutable/Tests/PrefireTests/GenerateTestsCommandTest.swift @@ -4,26 +4,45 @@ import XCTest import PathKit class GenerateTestsCommandTests: XCTestCase { - var options: GeneratedTestsOptions! - override func setUp() { - super.setUp() - options = try? GeneratedTestsOptions( - target: "GenerateTestsCommand", - testTarget: "GenerateTestsCommandTests", - template: nil, - sources: [], - output: "User/Tests/PreviewTests.generated.swift", - testTargetPath: "User/Tests", - cacheBasePath: nil, - device: nil, - osVersion: nil, - config: nil + // MARK: - Helper Methods + + private func makeOptions( + target: String? = nil, + testTarget: String? = nil, + template: String? = nil, + sources: [String] = [], + output: String? = nil, + testTargetPath: String? = nil, + cacheBasePath: String? = nil, + device: String? = nil, + osVersion: String? = nil, + config: Config? = nil + ) -> GeneratedTestsOptions { + let cli = CLITestsOptions( + target: target, + testTarget: testTarget, + template: template, + sources: sources, + output: output, + testTargetPath: testTargetPath, + cacheBasePath: cacheBasePath, + device: device, + osVersion: osVersion ) + return GeneratedTestsOptions.from(cli: cli, config: config) } + // MARK: - makeArguments Tests + func test_makeArguments_sources() async { - options.sources = ["some/sources"] + let options = makeOptions( + target: "GenerateTestsCommand", + testTarget: "GenerateTestsCommandTests", + sources: ["some/sources"], + testTargetPath: "User/Tests" + ) + let expectedArguments = [ "mainTarget": options.target! as NSString, "file": options.testTargetPath.flatMap({ $0 + "PreviewTests.generated.swift"})!.string as NSString, @@ -33,13 +52,26 @@ class GenerateTestsCommandTests: XCTestCase { XCTAssertEqual(YAMLParser().string(from: arguments), YAMLParser().string(from: expectedArguments)) } - + func test_makeArguments_snapshot_devices() async { - options.snapshotDevices = ["iPhone 15", "iPad"] - options.sources = ["some/sources", "some/other/sources"] + let configData = """ + test_configuration: + snapshot_devices: + - iPhone 15 + - iPad + """ + let config = ConfigDecoder().decode(from: configData, env: [:]) + + let options = makeOptions( + target: "GenerateTestsCommand", + testTarget: "GenerateTestsCommandTests", + sources: ["some/sources", "some/other/sources"], + testTargetPath: "User/Tests", + config: config + ) let expectedArguments = [ - "mainTarget": "\(options.target ?? "")" as NSString, + "mainTarget": options.target! as NSString, "file": options.testTargetPath.flatMap({ $0 + "PreviewTests.generated.swift"})!.string as NSString, "snapshotDevices": "iPhone 15|iPad" as NSString, ] as [String: NSObject] @@ -48,4 +80,101 @@ class GenerateTestsCommandTests: XCTestCase { XCTAssertEqual(YAMLParser().string(from: arguments), YAMLParser().string(from: expectedArguments)) } + + // MARK: - Template Parameter Tests + + func test_from_resolvesTargetPlaceholder() { + let configData = """ + test_configuration: + target: MyAppTarget + test_target_path: "${PROJECT_DIR}/${TARGET}/Tests" + """ + let config = ConfigDecoder().decode(from: configData, env: [:]) + + let options = makeOptions(config: config) + + XCTAssertEqual(options.testTargetPath?.string, "${PROJECT_DIR}/MyAppTarget/Tests") + } + + func test_from_resolvesTestTargetPlaceholder() { + let configData = """ + test_configuration: + target: MyApp + test_target_path: "${PROJECT_DIR}/Tests/${TEST_TARGET}" + """ + let config = ConfigDecoder().decode(from: configData, env: [:]) + + let options = makeOptions(testTarget: "MyAppTests", config: config) + + XCTAssertEqual(options.testTargetPath?.string, "${PROJECT_DIR}/Tests/MyAppTests") + } + + func test_from_resolvesBothPlaceholders() { + let configData = """ + test_configuration: + target: FeatureA + test_target_path: "${PROJECT_DIR}/${TARGET}/Tests/${TEST_TARGET}" + """ + let config = ConfigDecoder().decode(from: configData, env: [:]) + + let options = makeOptions(testTarget: "FeatureATests", config: config) + + XCTAssertEqual(options.testTargetPath?.string, "${PROJECT_DIR}/FeatureA/Tests/FeatureATests") + } + + func test_from_noPlaceholders_keepsOriginalPath() { + let configData = """ + test_configuration: + target: MyApp + test_target_path: "${PROJECT_DIR}/Tests" + """ + let config = ConfigDecoder().decode(from: configData, env: [:]) + + let options = makeOptions(testTarget: "MyAppTests", config: config) + + XCTAssertEqual(options.testTargetPath?.string, "${PROJECT_DIR}/Tests") + } + + func test_from_pathWithSpaces_resolvesCorrectly() { + let configData = """ + test_configuration: + target: My App + test_target_path: "${PROJECT_DIR}/My Project/${TARGET}/Tests" + """ + let config = ConfigDecoder().decode(from: configData, env: [:]) + + let options = makeOptions(config: config) + + XCTAssertEqual(options.testTargetPath?.string, "${PROJECT_DIR}/My Project/My App/Tests") + } + + // MARK: - CLI and Config Merging Tests + + func test_from_configTakesPrecedenceOverCLI_forTarget() { + let configData = """ + test_configuration: + target: ConfigTarget + """ + let config = ConfigDecoder().decode(from: configData, env: [:]) + + let options = makeOptions( + target: "CLITarget", + testTarget: "CLITestTarget", + config: config + ) + + XCTAssertEqual(options.target, "ConfigTarget") + // testTarget only comes from CLI (not in config) + XCTAssertEqual(options.testTarget, "CLITestTarget") + } + + func test_from_usesCliWhenConfigIsNil() { + let options = makeOptions( + target: "CLITarget", + testTarget: "CLITestTarget" + ) + + XCTAssertEqual(options.target, "CLITarget") + XCTAssertEqual(options.testTarget, "CLITestTarget") + } } diff --git a/README.md b/README.md index 682aa7a..1ba6f9c 100644 --- a/README.md +++ b/README.md @@ -259,6 +259,7 @@ See detailed configuration in the [Configuration guide](https://github.com/Barre ```yaml test_configuration: target: MyApp + test_target_path: "${PROJECT_DIR}/${TARGET}/Tests" # Template parameters supported playbook_configuration: preview_default_enabled: true