From cebb34b1549947d3f31144be20fe20e62fa06e98 Mon Sep 17 00:00:00 2001 From: "a.durymanov" Date: Sat, 25 Oct 2025 13:48:00 +0200 Subject: [PATCH 1/5] feat: Add template parameter support for test_target_path - Add ConfigPathResolver utility with clean parameter interface - Support {{target}} and {{testTarget}} placeholders - Apply to test_target_path configuration only - Comprehensive unit tests (18 test cases) and integration tests - Backward compatible with existing configurations Useful for multi-target projects and monorepos. Also: Add .serena/ to .gitignore --- .gitignore | 1 + Configuration.md | 16 +- .../Commands/Tests/GenerateTestsCommand.swift | 10 +- .../prefire/Config/ConfigPathResolver.swift | 40 +++++ .../ConfigPathResolverTests.swift | 152 ++++++++++++++++++ .../GenerateTestsCommandTest.swift | 122 ++++++++++++++ README.md | 1 + 7 files changed, 340 insertions(+), 2 deletions(-) create mode 100644 PrefireExecutable/Sources/prefire/Config/ConfigPathResolver.swift create mode 100644 PrefireExecutable/Tests/PrefireTests/ConfigPathResolverTests.swift 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..6b2615f 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 `{{testTarget}}` 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/Tests/GenerateTestsCommand.swift b/PrefireExecutable/Sources/prefire/Commands/Tests/GenerateTestsCommand.swift index 7aa2667..a33e372 100644 --- a/PrefireExecutable/Sources/prefire/Commands/Tests/GenerateTestsCommand.swift +++ b/PrefireExecutable/Sources/prefire/Commands/Tests/GenerateTestsCommand.swift @@ -37,7 +37,15 @@ struct GeneratedTestsOptions { ) throws { self.target = config?.tests.target ?? target self.testTarget = testTarget - self.testTargetPath = (config?.tests.testTargetPath ?? testTargetPath).flatMap({ Path($0) }) + + if let testTargetPath = config?.tests.testTargetPath ?? testTargetPath { + let resolvedPath = ConfigPathResolver.resolve( + testTargetPath, + target: self.target, + testTarget: self.testTarget + ) + self.testTargetPath = Path(resolvedPath) + } if let template = config?.tests.template, let testTargetPath = self.testTargetPath { let testTargetURL = URL(filePath: testTargetPath.string) diff --git a/PrefireExecutable/Sources/prefire/Config/ConfigPathResolver.swift b/PrefireExecutable/Sources/prefire/Config/ConfigPathResolver.swift new file mode 100644 index 0000000..0f73a61 --- /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 +/// - `{{testTarget}}` - 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 {{testTarget}} + /// - 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: "{{testTarget}}", with: testTarget) + } + + return result + } +} diff --git a/PrefireExecutable/Tests/PrefireTests/ConfigPathResolverTests.swift b/PrefireExecutable/Tests/PrefireTests/ConfigPathResolverTests.swift new file mode 100644 index 0000000..7834294 --- /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/{{testTarget}}" + let result = ConfigPathResolver.resolve(path, testTarget: "MyAppTests") + + XCTAssertEqual(result, "${PROJECT_DIR}/Tests/MyAppTests") + } + + func test_resolve_replacesTestTargetPlaceholder_multipleOccurrences() { + let path = "{{testTarget}}/Snapshots/{{testTarget}}" + let result = ConfigPathResolver.resolve(path, testTarget: "UnitTests") + + XCTAssertEqual(result, "UnitTests/Snapshots/UnitTests") + } + + func test_resolve_doesNotReplaceTestTargetPlaceholder_whenTestTargetIsNil() { + let path = "${PROJECT_DIR}/Tests/{{testTarget}}" + let result = ConfigPathResolver.resolve(path, testTarget: nil) + + XCTAssertEqual(result, "${PROJECT_DIR}/Tests/{{testTarget}}") + } + + // MARK: - Combined Placeholder Tests + + func test_resolve_replacesBothPlaceholders() { + let path = "${PROJECT_DIR}/{{target}}/Tests/{{testTarget}}" + 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/{{testTarget}}/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/{{testTarget}}" + let result = ConfigPathResolver.resolve(path, target: nil, testTarget: nil) + + XCTAssertEqual(result, "${PROJECT_DIR}/{{target}}/Tests/{{testTarget}}") + } + + // 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/{{testTarget}}" + 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/{{testTarget}}.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/{{testTarget}}" + let result = ConfigPathResolver.resolve(path) + + XCTAssertEqual(result, "${PROJECT_DIR}/{{target}}/Tests/{{testTarget}}") + } + + func test_resolve_onlyTarget_leavesTestTargetPlaceholder() { + let path = "{{target}}/Tests/{{testTarget}}" + let result = ConfigPathResolver.resolve(path, target: "MyApp") + + XCTAssertEqual(result, "MyApp/Tests/{{testTarget}}") + } + + func test_resolve_onlyTestTarget_leavesTargetPlaceholder() { + let path = "{{target}}/Tests/{{testTarget}}" + 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..9799371 100644 --- a/PrefireExecutable/Tests/PrefireTests/GenerateTestsCommandTest.swift +++ b/PrefireExecutable/Tests/PrefireTests/GenerateTestsCommandTest.swift @@ -48,4 +48,126 @@ class GenerateTestsCommandTests: XCTestCase { XCTAssertEqual(YAMLParser().string(from: arguments), YAMLParser().string(from: expectedArguments)) } + + // MARK: - Template Parameter Tests + + func test_init_resolvesTargetPlaceholder() throws { + let configData = """ + test_configuration: + target: MyAppTarget + test_target_path: "${PROJECT_DIR}/{{target}}/Tests" + """ + let config = ConfigDecoder().decode(from: configData, env: [:]) + + let options = try GeneratedTestsOptions( + target: nil, + testTarget: nil, + template: nil, + sources: [], + output: nil, + testTargetPath: nil, + cacheBasePath: nil, + device: nil, + osVersion: nil, + config: config + ) + + XCTAssertEqual(options.testTargetPath?.string, "${PROJECT_DIR}/MyAppTarget/Tests") + } + + func test_init_resolvesTestTargetPlaceholder() throws { + let configData = """ + test_configuration: + target: MyApp + test_target_path: "${PROJECT_DIR}/Tests/{{testTarget}}" + """ + let config = ConfigDecoder().decode(from: configData, env: [:]) + + let options = try GeneratedTestsOptions( + target: nil, + testTarget: "MyAppTests", + template: nil, + sources: [], + output: nil, + testTargetPath: nil, + cacheBasePath: nil, + device: nil, + osVersion: nil, + config: config + ) + + XCTAssertEqual(options.testTargetPath?.string, "${PROJECT_DIR}/Tests/MyAppTests") + } + + func test_init_resolvesBothPlaceholders() throws { + let configData = """ + test_configuration: + target: FeatureA + test_target_path: "${PROJECT_DIR}/{{target}}/Tests/{{testTarget}}" + """ + let config = ConfigDecoder().decode(from: configData, env: [:]) + + let options = try GeneratedTestsOptions( + target: nil, + testTarget: "FeatureATests", + template: nil, + sources: [], + output: nil, + testTargetPath: nil, + cacheBasePath: nil, + device: nil, + osVersion: nil, + config: config + ) + + XCTAssertEqual(options.testTargetPath?.string, "${PROJECT_DIR}/FeatureA/Tests/FeatureATests") + } + + func test_init_noPlaceholders_keepsOriginalPath() throws { + let configData = """ + test_configuration: + target: MyApp + test_target_path: "${PROJECT_DIR}/Tests" + """ + let config = ConfigDecoder().decode(from: configData, env: [:]) + + let options = try GeneratedTestsOptions( + target: nil, + testTarget: "MyAppTests", + template: nil, + sources: [], + output: nil, + testTargetPath: nil, + cacheBasePath: nil, + device: nil, + osVersion: nil, + config: config + ) + + XCTAssertEqual(options.testTargetPath?.string, "${PROJECT_DIR}/Tests") + } + + func test_init_pathWithSpaces_resolvesCorrectly() throws { + 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 = try GeneratedTestsOptions( + target: nil, + testTarget: nil, + template: nil, + sources: [], + output: nil, + testTargetPath: nil, + cacheBasePath: nil, + device: nil, + osVersion: nil, + config: config + ) + + XCTAssertEqual(options.testTargetPath?.string, "${PROJECT_DIR}/My Project/My App/Tests") + } } diff --git a/README.md b/README.md index 682aa7a..1937042 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 From 0ed84d5c4b262736ec65c07014bcbabe97a6c2a8 Mon Sep 17 00:00:00 2001 From: "a.durymanov" Date: Sat, 3 Jan 2026 14:37:51 +0100 Subject: [PATCH 2/5] Update targets variables names --- Configuration.md | 4 +- .../prefire/Config/ConfigPathResolver.swift | 10 ++--- .../ConfigPathResolverTests.swift | 44 +++++++++---------- .../GenerateTestsCommandTest.swift | 8 ++-- README.md | 2 +- 5 files changed, 34 insertions(+), 34 deletions(-) diff --git a/Configuration.md b/Configuration.md index 6b2615f..e45e1c8 100644 --- a/Configuration.md +++ b/Configuration.md @@ -58,13 +58,13 @@ playbook_configuration: ### 🎯 Template Parameters -The `test_target_path` configuration supports template placeholders `{{target}}` and `{{testTarget}}` that are dynamically replaced at runtime. Useful for multi-target projects and monorepos. +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" + test_target_path: "${PROJECT_DIR}/${TARGET}/Tests" # Resolves to: ${PROJECT_DIR}/MyApp/Tests ``` diff --git a/PrefireExecutable/Sources/prefire/Config/ConfigPathResolver.swift b/PrefireExecutable/Sources/prefire/Config/ConfigPathResolver.swift index 0f73a61..55271b7 100644 --- a/PrefireExecutable/Sources/prefire/Config/ConfigPathResolver.swift +++ b/PrefireExecutable/Sources/prefire/Config/ConfigPathResolver.swift @@ -3,12 +3,12 @@ import Foundation /// Utility for resolving template placeholders in configuration paths /// /// Supports the following placeholders: -/// - `{{target}}` - Replaced with the target name -/// - `{{testTarget}}` - Replaced with the test target name +/// - `${TARGET}` - Replaced with the target name +/// - `${TEST_TARGET}` - Replaced with the test target name /// /// Example: /// ```swift -/// let path = "${PROJECT_DIR}/{{target}}/Tests" +/// let path = "${PROJECT_DIR}/${TARGET}/Tests" /// let resolved = ConfigPathResolver.resolve(path, target: "MyApp") /// // Result: "${PROJECT_DIR}/MyApp/Tests" /// ``` @@ -17,8 +17,8 @@ enum ConfigPathResolver { /// /// - Parameters: /// - path: The path string containing placeholders - /// - target: Optional target name to replace {{target}} - /// - testTarget: Optional test target name to replace {{testTarget}} + /// - 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, diff --git a/PrefireExecutable/Tests/PrefireTests/ConfigPathResolverTests.swift b/PrefireExecutable/Tests/PrefireTests/ConfigPathResolverTests.swift index 7834294..11ca5ad 100644 --- a/PrefireExecutable/Tests/PrefireTests/ConfigPathResolverTests.swift +++ b/PrefireExecutable/Tests/PrefireTests/ConfigPathResolverTests.swift @@ -6,53 +6,53 @@ class ConfigPathResolverTests: XCTestCase { // MARK: - Target Placeholder Tests func test_resolve_replacesTargetPlaceholder() { - let path = "${PROJECT_DIR}/{{target}}/Tests" + 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 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 path = "${PROJECT_DIR}/${TARGET}/Tests" let result = ConfigPathResolver.resolve(path, target: nil) - XCTAssertEqual(result, "${PROJECT_DIR}/{{target}}/Tests") + XCTAssertEqual(result, "${PROJECT_DIR}/${TARGET}/Tests") } // MARK: - TestTarget Placeholder Tests func test_resolve_replacesTestTargetPlaceholder() { - let path = "${PROJECT_DIR}/Tests/{{testTarget}}" + 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 = "{{testTarget}}/Snapshots/{{testTarget}}" + 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/{{testTarget}}" + let path = "${PROJECT_DIR}/Tests/${TEST_TARGET}" let result = ConfigPathResolver.resolve(path, testTarget: nil) - XCTAssertEqual(result, "${PROJECT_DIR}/Tests/{{testTarget}}") + XCTAssertEqual(result, "${PROJECT_DIR}/Tests/${TEST_TARGET}") } // MARK: - Combined Placeholder Tests func test_resolve_replacesBothPlaceholders() { - let path = "${PROJECT_DIR}/{{target}}/Tests/{{testTarget}}" + let path = "${PROJECT_DIR}/${TARGET}/Tests/${TEST_TARGET}" let result = ConfigPathResolver.resolve( path, target: "FeatureA", @@ -63,7 +63,7 @@ class ConfigPathResolverTests: XCTestCase { } func test_resolve_replacesBothPlaceholders_complexPath() { - let path = "Modules/{{target}}/Testing/{{testTarget}}/Snapshots/{{target}}" + let path = "Modules/${TARGET}/Testing/${TEST_TARGET}/Snapshots/${TARGET}" let result = ConfigPathResolver.resolve( path, target: "ShoppingCart", @@ -74,10 +74,10 @@ class ConfigPathResolverTests: XCTestCase { } func test_resolve_doesNotReplacePlaceholders_whenBothParametersAreNil() { - let path = "${PROJECT_DIR}/{{target}}/Tests/{{testTarget}}" + let path = "${PROJECT_DIR}/${TARGET}/Tests/${TEST_TARGET}" let result = ConfigPathResolver.resolve(path, target: nil, testTarget: nil) - XCTAssertEqual(result, "${PROJECT_DIR}/{{target}}/Tests/{{testTarget}}") + XCTAssertEqual(result, "${PROJECT_DIR}/${TARGET}/Tests/${TEST_TARGET}") } // MARK: - Edge Cases @@ -96,28 +96,28 @@ class ConfigPathResolverTests: XCTestCase { } func test_resolve_emptyTargetValue_doesNotReplace() { - let path = "{{target}}/Tests" + let path = "${TARGET}/Tests" let result = ConfigPathResolver.resolve(path, target: "") XCTAssertEqual(result, "/Tests") } func test_resolve_emptyTestTargetValue_doesNotReplace() { - let path = "Tests/{{testTarget}}" + 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 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/{{testTarget}}.snapshot" + let path = "Project-${TARGET}_Tests/${TEST_TARGET}.snapshot" let result = ConfigPathResolver.resolve( path, target: "Core-Module", @@ -130,23 +130,23 @@ class ConfigPathResolverTests: XCTestCase { // MARK: - No Parameters func test_resolve_withNoParameters_returnsOriginalPath() { - let path = "${PROJECT_DIR}/{{target}}/Tests/{{testTarget}}" + let path = "${PROJECT_DIR}/${TARGET}/Tests/${TEST_TARGET}" let result = ConfigPathResolver.resolve(path) - XCTAssertEqual(result, "${PROJECT_DIR}/{{target}}/Tests/{{testTarget}}") + XCTAssertEqual(result, "${PROJECT_DIR}/${TARGET}/Tests/${TEST_TARGET}") } func test_resolve_onlyTarget_leavesTestTargetPlaceholder() { - let path = "{{target}}/Tests/{{testTarget}}" + let path = "${TARGET}/Tests/${TEST_TARGET}" let result = ConfigPathResolver.resolve(path, target: "MyApp") - XCTAssertEqual(result, "MyApp/Tests/{{testTarget}}") + XCTAssertEqual(result, "MyApp/Tests/${TEST_TARGET}") } func test_resolve_onlyTestTarget_leavesTargetPlaceholder() { - let path = "{{target}}/Tests/{{testTarget}}" + let path = "${TARGET}/Tests/${TEST_TARGET}" let result = ConfigPathResolver.resolve(path, testTarget: "MyTests") - XCTAssertEqual(result, "{{target}}/Tests/MyTests") + XCTAssertEqual(result, "${TARGET}/Tests/MyTests") } } diff --git a/PrefireExecutable/Tests/PrefireTests/GenerateTestsCommandTest.swift b/PrefireExecutable/Tests/PrefireTests/GenerateTestsCommandTest.swift index 9799371..1bf5842 100644 --- a/PrefireExecutable/Tests/PrefireTests/GenerateTestsCommandTest.swift +++ b/PrefireExecutable/Tests/PrefireTests/GenerateTestsCommandTest.swift @@ -55,7 +55,7 @@ class GenerateTestsCommandTests: XCTestCase { let configData = """ test_configuration: target: MyAppTarget - test_target_path: "${PROJECT_DIR}/{{target}}/Tests" + test_target_path: "${PROJECT_DIR}/${TARGET}/Tests" """ let config = ConfigDecoder().decode(from: configData, env: [:]) @@ -79,7 +79,7 @@ class GenerateTestsCommandTests: XCTestCase { let configData = """ test_configuration: target: MyApp - test_target_path: "${PROJECT_DIR}/Tests/{{testTarget}}" + test_target_path: "${PROJECT_DIR}/Tests/${TEST_TARGET}" """ let config = ConfigDecoder().decode(from: configData, env: [:]) @@ -103,7 +103,7 @@ class GenerateTestsCommandTests: XCTestCase { let configData = """ test_configuration: target: FeatureA - test_target_path: "${PROJECT_DIR}/{{target}}/Tests/{{testTarget}}" + test_target_path: "${PROJECT_DIR}/${TARGET}/Tests/${TEST_TARGET}" """ let config = ConfigDecoder().decode(from: configData, env: [:]) @@ -151,7 +151,7 @@ class GenerateTestsCommandTests: XCTestCase { let configData = """ test_configuration: target: My App - test_target_path: "${PROJECT_DIR}/My Project/{{target}}/Tests" + test_target_path: "${PROJECT_DIR}/My Project/${TARGET}/Tests" """ let config = ConfigDecoder().decode(from: configData, env: [:]) diff --git a/README.md b/README.md index 1937042..1ba6f9c 100644 --- a/README.md +++ b/README.md @@ -259,7 +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 + test_target_path: "${PROJECT_DIR}/${TARGET}/Tests" # Template parameters supported playbook_configuration: preview_default_enabled: true From 031c378b16ccb8c23dac9c7983188414ed1f5c73 Mon Sep 17 00:00:00 2001 From: "a.durymanov" Date: Sat, 3 Jan 2026 14:40:31 +0100 Subject: [PATCH 3/5] Change names for targets variables --- .../Sources/prefire/Config/ConfigPathResolver.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PrefireExecutable/Sources/prefire/Config/ConfigPathResolver.swift b/PrefireExecutable/Sources/prefire/Config/ConfigPathResolver.swift index 55271b7..2d08be4 100644 --- a/PrefireExecutable/Sources/prefire/Config/ConfigPathResolver.swift +++ b/PrefireExecutable/Sources/prefire/Config/ConfigPathResolver.swift @@ -28,11 +28,11 @@ enum ConfigPathResolver { var result = path if let target { - result = result.replacingOccurrences(of: "{{target}}", with: target) + result = result.replacingOccurrences(of: "${TARGET}", with: target) } if let testTarget { - result = result.replacingOccurrences(of: "{{testTarget}}", with: testTarget) + result = result.replacingOccurrences(of: "${TEST_TARGET}", with: testTarget) } return result From e07bea44f388cb2a3d9f9c4b77e008ea82a4203b Mon Sep 17 00:00:00 2001 From: "a.durymanov" Date: Sat, 3 Jan 2026 17:22:16 +0100 Subject: [PATCH 4/5] refactor: Separate CLI options from config merging logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add OptionsResolver for shared path/template resolution - Create CLITestsOptions and CLIPlaybookOptions structs for raw CLI input - Refactor GeneratedTestsOptions and GeneratedPlaybookOptions with factory methods - Move ConfigPathResolver.resolve() call to factory after CLI+config merge - Fix variable shadowing (config -> loadedConfig) - Change options properties from var to let (immutable) - Update tests to use new factory pattern 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../Playbook/GeneratePlaybookCommand.swift | 60 +++--- .../prefire/Commands/Playbook/Playbook.swift | 31 ++-- .../Commands/Tests/GenerateTestsCommand.swift | 110 +++++------ .../prefire/Commands/Tests/Tests.swift | 39 ++-- .../prefire/Options/CLIPlaybookOptions.swift | 10 + .../prefire/Options/CLITestsOptions.swift | 14 ++ .../prefire/Options/OptionsResolver.swift | 66 +++++++ .../GenerateTestsCommandTest.swift | 173 +++++++++--------- 8 files changed, 317 insertions(+), 186 deletions(-) create mode 100644 PrefireExecutable/Sources/prefire/Options/CLIPlaybookOptions.swift create mode 100644 PrefireExecutable/Sources/prefire/Options/CLITestsOptions.swift create mode 100644 PrefireExecutable/Sources/prefire/Options/OptionsResolver.swift 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 35503e2..453b52b 100644 --- a/PrefireExecutable/Sources/prefire/Commands/Tests/GenerateTestsCommand.swift +++ b/PrefireExecutable/Sources/prefire/Commands/Tests/GenerateTestsCommand.swift @@ -8,63 +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 +} - 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 +// MARK: - Factory - if let testTargetPath = config?.tests.testTargetPath ?? testTargetPath { - let resolvedPath = ConfigPathResolver.resolve( - testTargetPath, - target: self.target, - testTarget: self.testTarget - ) - self.testTargetPath = Path(resolvedPath) - } +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 - 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 rawTestTargetPath = config?.tests.testTargetPath ?? cli.testTargetPath + let testTargetPath = OptionsResolver.resolvePath( + rawTestTargetPath, + target: target, + testTarget: testTarget + ) - 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 + let template = OptionsResolver.resolveTemplate( + cliTemplate: cli.template, + configTemplate: config?.tests.template, + targetPath: testTargetPath + ) + + 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/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..8d7029d --- /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()) + } + 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/GenerateTestsCommandTest.swift b/PrefireExecutable/Tests/PrefireTests/GenerateTestsCommandTest.swift index 1bf5842..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] @@ -51,7 +83,7 @@ class GenerateTestsCommandTests: XCTestCase { // MARK: - Template Parameter Tests - func test_init_resolvesTargetPlaceholder() throws { + func test_from_resolvesTargetPlaceholder() { let configData = """ test_configuration: target: MyAppTarget @@ -59,23 +91,12 @@ class GenerateTestsCommandTests: XCTestCase { """ let config = ConfigDecoder().decode(from: configData, env: [:]) - let options = try GeneratedTestsOptions( - target: nil, - testTarget: nil, - template: nil, - sources: [], - output: nil, - testTargetPath: nil, - cacheBasePath: nil, - device: nil, - osVersion: nil, - config: config - ) + let options = makeOptions(config: config) XCTAssertEqual(options.testTargetPath?.string, "${PROJECT_DIR}/MyAppTarget/Tests") } - func test_init_resolvesTestTargetPlaceholder() throws { + func test_from_resolvesTestTargetPlaceholder() { let configData = """ test_configuration: target: MyApp @@ -83,23 +104,12 @@ class GenerateTestsCommandTests: XCTestCase { """ let config = ConfigDecoder().decode(from: configData, env: [:]) - let options = try GeneratedTestsOptions( - target: nil, - testTarget: "MyAppTests", - template: nil, - sources: [], - output: nil, - testTargetPath: nil, - cacheBasePath: nil, - device: nil, - osVersion: nil, - config: config - ) + let options = makeOptions(testTarget: "MyAppTests", config: config) XCTAssertEqual(options.testTargetPath?.string, "${PROJECT_DIR}/Tests/MyAppTests") } - func test_init_resolvesBothPlaceholders() throws { + func test_from_resolvesBothPlaceholders() { let configData = """ test_configuration: target: FeatureA @@ -107,23 +117,12 @@ class GenerateTestsCommandTests: XCTestCase { """ let config = ConfigDecoder().decode(from: configData, env: [:]) - let options = try GeneratedTestsOptions( - target: nil, - testTarget: "FeatureATests", - template: nil, - sources: [], - output: nil, - testTargetPath: nil, - cacheBasePath: nil, - device: nil, - osVersion: nil, - config: config - ) + let options = makeOptions(testTarget: "FeatureATests", config: config) XCTAssertEqual(options.testTargetPath?.string, "${PROJECT_DIR}/FeatureA/Tests/FeatureATests") } - func test_init_noPlaceholders_keepsOriginalPath() throws { + func test_from_noPlaceholders_keepsOriginalPath() { let configData = """ test_configuration: target: MyApp @@ -131,23 +130,12 @@ class GenerateTestsCommandTests: XCTestCase { """ let config = ConfigDecoder().decode(from: configData, env: [:]) - let options = try GeneratedTestsOptions( - target: nil, - testTarget: "MyAppTests", - template: nil, - sources: [], - output: nil, - testTargetPath: nil, - cacheBasePath: nil, - device: nil, - osVersion: nil, - config: config - ) + let options = makeOptions(testTarget: "MyAppTests", config: config) XCTAssertEqual(options.testTargetPath?.string, "${PROJECT_DIR}/Tests") } - func test_init_pathWithSpaces_resolvesCorrectly() throws { + func test_from_pathWithSpaces_resolvesCorrectly() { let configData = """ test_configuration: target: My App @@ -155,19 +143,38 @@ class GenerateTestsCommandTests: XCTestCase { """ let config = ConfigDecoder().decode(from: configData, env: [:]) - let options = try GeneratedTestsOptions( - target: nil, - testTarget: nil, - template: nil, - sources: [], - output: nil, - testTargetPath: nil, - cacheBasePath: nil, - device: nil, - osVersion: nil, + 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.testTargetPath?.string, "${PROJECT_DIR}/My Project/My App/Tests") + 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") } } From 7c897d297a6db67a310c55d1e30dd0fd914daa30 Mon Sep 17 00:00:00 2001 From: "a.durymanov" Date: Sat, 3 Jan 2026 17:28:34 +0100 Subject: [PATCH 5/5] Fix percent encoded paths --- PrefireExecutable/Sources/prefire/Options/OptionsResolver.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PrefireExecutable/Sources/prefire/Options/OptionsResolver.swift b/PrefireExecutable/Sources/prefire/Options/OptionsResolver.swift index 8d7029d..d5ebe4f 100644 --- a/PrefireExecutable/Sources/prefire/Options/OptionsResolver.swift +++ b/PrefireExecutable/Sources/prefire/Options/OptionsResolver.swift @@ -17,7 +17,7 @@ enum OptionsResolver { if let configTemplate, let targetPath { let targetURL = URL(filePath: targetPath.string) let templateURL = targetURL.appending(path: configTemplate) - return Path(templateURL.absoluteURL.path()) + return Path(templateURL.absoluteURL.path(percentEncoded: false)) } return cliTemplate.map { Path($0) } }