Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 13 additions & 9 deletions .github/actions/tests/action.yml
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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.
Expand All @@ -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
Expand All @@ -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
Expand Down
3 changes: 2 additions & 1 deletion Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions Sources/SPMGraphExecutable/Subcommands/Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -69,6 +76,7 @@ struct Tests: AsyncParsableCommand {
changedFiles: arguments.changedFiles,
baseBranch: arguments.baseBranch,
outputMode: arguments.outputMode,
experimentalUITestTargets: arguments.experimentalUITestTargets,
verbose: arguments.common.verbose
)
)
Expand Down
74 changes: 65 additions & 9 deletions Sources/SPMGraphTests/SPMGraphTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
Expand All @@ -56,6 +62,7 @@ public struct SPMGraphTestsInput {
self.changedFiles = changedFiles
self.baseBranch = baseBranch
self.outputMode = outputMode
self.experimentalUITestTargets = experimentalUITestTargets
self.verbose = verbose
}
}
Expand Down Expand Up @@ -184,25 +191,76 @@ 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
}
}

// 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
///
Expand Down Expand Up @@ -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)"
)
Expand Down