diff --git a/.github/actions/tests/action.yml b/.github/actions/tests/action.yml index baeeb13..7f8a0d4 100644 --- a/.github/actions/tests/action.yml +++ b/.github/actions/tests/action.yml @@ -1,30 +1,30 @@ name: 'spmgraph tests' description: 'Map tests to run based on changed files of a given dependency graph' inputs: - package_directory: + package-directory: description: The directory where the Package.swift file is located required: true verbose: description: Show extra logging for troubleshooting purposes. type: boolean default: false - excluded_suffixes: + excluded-suffixes: description: Comma separated suffixes to exclude from the graph e.g. 'Tests','Live','TestSupport' required: false default: '' - base_branch: + base-branch: description: Base branch to compare the changes against required: false default: 'main' - changed_files: + changed-files: description: Optional list of changed files. Otherwise git versioning is used required: false default: '' - output_format: + output-format: description: "The output mode. Options are: textDump, textFile. The first dumps the list of test modules to run in a single line, while the latter Saves the list of test modules into an `output.txt` file in the `current dir`. Both follow the following the xcodebuild/fastlane scan expected format." required: false default: 'textDump' - adds_to_summary: + adds-to-summary: description: List the filtered tests in the action summary. type: boolean default: false @@ -36,6 +36,10 @@ inputs: - **Warning**: Ensure this is consistent across commands, otherwise your configuration won't be correctly loaded! required: false + experimental-ui-test-targets: + description: "Warning: This is an experimental flag, use it with caution! Enables support for including UITest targets on selecting testing. It looks for a `uiTestsDependencies.json` in the temporary directory, reads it, and checks if any of the UITest targets dependencies are affected, if so, it includes them in the list of test targets to run." + type: boolean + default: false outputs: test_targets: description: A comma separated list of test targets to run. It can be passed as is to xcodebuild or fastlane scan. @@ -50,8 +54,8 @@ runs: - id: spmgraph_test name: Run spmgraph tests run: | - # note: changed_files take precedence over baseBranch - spmgraph tests ${{ inputs.package_directory }} ${{ inputs.verbose == 'true' && '-v' || '' }} --baseBranch ${{ github.base_ref || 'main' }} --output ${{ inputs.output_format }} ${{ inputs.excluded_suffixes && '--excludedSuffixes ${{ inputs.excluded_suffixes }}' || '' }} ${{ inputs.changed_files && '--changed_files ${{ inputs.changed_files }}' || '' }} ${{ inputs.config-build-directory && '--config-build-directory ${{ inputs.config-build-directory }}' || '' }} + # note: changed-files takes precedence over baseBranch + spmgraph tests ${{ inputs.package-directory }} ${{ inputs.verbose == 'true' && '-v' || '' }} --baseBranch ${{ github.base_ref || 'main' }} --output ${{ inputs.output-format }} ${{ inputs.excluded-suffixes && '--excludedSuffixes ${{ inputs.excluded-suffixes }}' || '' }} ${{ inputs.changed-files && '--changed-files ${{ inputs.changed-files }}' || '' }} ${{ inputs.config-build-directory && '--config-build-directory ${{ inputs.config-build-directory }}' || '' }} ${{ inputs.experimental-ui-test-targets == 'true' && '--experimentalUITest' || '' }} if [ ! -s "$GITHUB_WORKSPACE/output.txt" ]; then echo "tests_needed=false" >> $GITHUB_OUTPUT else @@ -61,7 +65,7 @@ runs: shell: /bin/bash -e {0} - name: Write to workflow job summary - if: ${{ inputs.adds_to_summary == 'true' }} + if: ${{ inputs.adds-to-summary == 'true' }} run: | tests_summary=$'# spmgraph tests\n## Tests to run\n' echo "$tests_summary" >> $GITHUB_STEP_SUMMARY diff --git a/Package.resolved b/Package.resolved index 358a693..9e21d07 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "a6a226de3b06cc20dcca8a35c4d9d79870436d050b919e889fe62edb8e7de24e", + "originHash" : "df5ee5f652940741893fd22b7f6d1e6dbb16327ac851be2defc389abf69960bb", "pins" : [ { "identity" : "filemonitor", @@ -15,6 +15,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/tuist/GraphViz.git", "state" : { + "branch" : "main", "revision" : "083bccf9e492fd5731dd288a46741ea80148f508" } }, diff --git a/Sources/SPMGraphExecutable/Subcommands/Tests.swift b/Sources/SPMGraphExecutable/Subcommands/Tests.swift index 1c52736..7f9c9a9 100644 --- a/Sources/SPMGraphExecutable/Subcommands/Tests.swift +++ b/Sources/SPMGraphExecutable/Subcommands/Tests.swift @@ -41,6 +41,13 @@ struct TestsArguments: ParsableArguments { ) var outputMode: SPMGraphTests.OutputMode = .textDump + @Flag( + name: [.customLong("experimentalUITest"), .long], + help: + "Warning: This is an experimental flag, use it with caution! Enables support for including UITest targets on selecting testing. It looks for a `uiTestsDependencies.json` in the temporary directory, reads it, and checks if any of the UITest targets dependencies are affected, if so, it includes them in the list of test targets to run." + ) + var experimentalUITestTargets: Bool = false + @OptionGroup var config: SPMGraphConfigArguments @@ -69,6 +76,7 @@ struct Tests: AsyncParsableCommand { changedFiles: arguments.changedFiles, baseBranch: arguments.baseBranch, outputMode: arguments.outputMode, + experimentalUITestTargets: arguments.experimentalUITestTargets, verbose: arguments.common.verbose ) ) diff --git a/Sources/SPMGraphTests/SPMGraphTests.swift b/Sources/SPMGraphTests/SPMGraphTests.swift index b433f56..cf3beb9 100644 --- a/Sources/SPMGraphTests/SPMGraphTests.swift +++ b/Sources/SPMGraphTests/SPMGraphTests.swift @@ -37,6 +37,11 @@ public struct SPMGraphTestsInput { let baseBranch: String /// The output mode let outputMode: SPMGraphTests.OutputMode // TODO: Check if it make sense in the SPMGraphConfig fle + /// Enables support for including UITest targets on selecting testing. It looks for a `uiTestsDependencies.json` in the temporary directory, + /// reads it, and checks if any of the UITest targets dependencies are affected, if so, it includes them in the list of test targets to run. + /// + /// - warning: This is an experimental flag, use it with caution! + let experimentalUITestTargets: Bool /// Show extra logging for troubleshooting purposes let verbose: Bool @@ -48,6 +53,7 @@ public struct SPMGraphTestsInput { changedFiles: [String], baseBranch: String, outputMode: SPMGraphTests.OutputMode, + experimentalUITestTargets: Bool, verbose: Bool ) throws { self.spmPackageDirectory = try AbsolutePath.packagePath(spmPackageDirectory) @@ -56,6 +62,7 @@ public struct SPMGraphTestsInput { self.changedFiles = changedFiles self.baseBranch = baseBranch self.outputMode = outputMode + self.experimentalUITestTargets = experimentalUITestTargets self.verbose = verbose } } @@ -184,18 +191,55 @@ public final class SPMGraphTests: SSPMGraphTestsProtocol { // map tests modules that should run let testModulesToRun = mapTestmodules(for: affectedModules, package: package) + var inlineTestModulesNames = testModulesToRun.map(\.name).joined(separator: ",") + + if input.experimentalUITestTargets { + let uiTestsDependenciesFilePath = try localFileSystem.tempDirectory + .appending(component: "uiTestsDependencies") + .appending(extension: "json") + + if input.verbose { + try system.echo("Looking for the UI tests dependencies file at: \(uiTestsDependenciesFilePath)") + } + + if localFileSystem.exists(uiTestsDependenciesFilePath) { + if input.verbose { + try system.echo("Found the UI tests dependencies file at: \(uiTestsDependenciesFilePath)") + } + + try localFileSystem.readFileContents(uiTestsDependenciesFilePath) + .withData { data in + let uiTestModules = try JSONDecoder().decode(UITestsModulesMap.self, from: data) + + let uiTestModulesToRun = uiTestModules.modules + .filter { target in + let allAffectedModules = (affectedModules + testModulesToRun).map(\.name) + return allAffectedModules.contains { moduleName in + target.dependencies.contains(moduleName) + } + } + .map(\.name) + + if !uiTestModulesToRun.isEmpty { + inlineTestModulesNames.append(",") + inlineTestModulesNames.append(uiTestModulesToRun.joined(separator: ",")) + } + } + } + } - if testModulesToRun.isEmpty { + if inlineTestModulesNames.isEmpty { try system.echo( "No test modules to run" ) } else { + let lineByLineModulesToRun = inlineTestModulesNames.replacingOccurrences(of: ",", with: "\n") try system.echo( - "The test modules to run are: \(testModulesToRun.map(\.name).joined(separator: "\n"))" + "The test modules to run are: \(lineByLineModulesToRun)" ) } - try generateOutput(testModulesToRun: testModulesToRun, outputMode: input.outputMode) + try generateOutput(inlineTestModulesToRun: inlineTestModulesNames, outputMode: input.outputMode) return testModulesToRun } @@ -203,6 +247,20 @@ public final class SPMGraphTests: SSPMGraphTestsProtocol { // MARK: - Private +private struct UITestsModulesMap: Decodable { + struct Module: Decodable { + let name: String + let dependencies: [String] + + private enum Keys: String, CodingKey { + case name = "targetName" + case dependencies + } + } + + let modules: [Module] +} + private extension SPMGraphTests { /// Maps and returns the modules that were affected by a set of changed files /// @@ -288,20 +346,18 @@ private extension SPMGraphTests { /// A function that generates the output with tests to run /// - Parameters: - /// - testModulesToRun: All modules which tests need to run + /// - inlineTestModulesToRun: The name of all modules which tests need to run /// - outputMode: Specifies the output mode - func generateOutput(testModulesToRun: [Module], outputMode: OutputMode) throws { - let inlineModuleNames = testModulesToRun.map(\.name).joined(separator: ",") - + func generateOutput(inlineTestModulesToRun: String, outputMode: OutputMode) throws { switch outputMode { case .textDump: - try system.echo(inlineModuleNames) + try system.echo(inlineTestModulesToRun) case .textFile: let url = AbsolutePath.currentDir.asURL var fileURL = url.appendingPathComponent("output") fileURL = fileURL.appendingPathExtension("txt") do { - try inlineModuleNames.write(to: fileURL, atomically: true, encoding: .utf8) + try inlineTestModulesToRun.write(to: fileURL, atomically: true, encoding: .utf8) try system.echo( "✅ Successfully saved the formatted list of test modules to \(fileURL)" )