-
-
Notifications
You must be signed in to change notification settings - Fork 516
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add test plan support within Tuist test #5135
Add test plan support within Tuist test #5135
Conversation
db0c71b
to
3d346e2
Compare
Sources/TuistCache/Mappers/Graph/FocusTargetsGraphMappers.swift
Outdated
Show resolved
Hide resolved
Sources/TuistCache/Mappers/Graph/FocusTargetsGraphMappers.swift
Outdated
Show resolved
Hide resolved
basedOn targets: GraphTargets, | ||
testPlan: String?, | ||
includedTargets: Set<String>, | ||
excludedTargets: Set<String>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a need to check for an intersection between includedTargets
& excludedTargets
so that we can throw an early error about misconfiguration?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The way it works right now, is that includeTargets
takes precedence over excludedTargets
in that case.
This is on purpose, I could update the behavior to throw instead but that could complicate the code in other places. What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As commented elsewhere, we should catch the error in the command, and map the input to an enum to only allow valid combinations
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let me know if you should we should still do that considering my comments below.
There could still be errors caught there for configurations that don't make sense, for example if includedTargets
& excludedTargets
's intersection is not empty, as it in this case the user would to include and exclude the same targets. Down to implement this check if we want it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yep, I would implement it in the command
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@danyf90 I've added validation of that to the command
Sources/TuistCache/Mappers/Graph/FocusTargetsGraphMappers.swift
Outdated
Show resolved
Hide resolved
Sources/TuistCache/Mappers/Graph/FocusTargetsGraphMappers.swift
Outdated
Show resolved
Hide resolved
testPlan: String?, | ||
testTargets: [TestIdentifier], | ||
skipTestTargets: [TestIdentifier], | ||
testConfigurations: [String], | ||
skipTestConfigurations: [String] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aren't these all related (meaning if testPlan is nil the others are ignored)? If yes, we should model it as an enum with associated values
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
testTargets
& skipTestTargets
can be used without a test plan, however testConfigurations
& skipTestConfigurations
can only be used when a test plan is specified. I can perhaps refactor them as a struct
in this case? Like TestPlanConfiguration
with the 3 fields
basedOn targets: GraphTargets, | ||
testPlan: String?, | ||
includedTargets: Set<String>, | ||
excludedTargets: Set<String>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As commented elsewhere, we should catch the error in the command, and map the input to an enum to only allow valid combinations
@@ -52,6 +52,41 @@ public class GraphTraverser: GraphTraversing { | |||
allTargets(excludingExternalTargets: true) | |||
} | |||
|
|||
public func filterIncludedTargets<GraphTargets: Collection>( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This logic is related to caching, so if possible I would define it in TuistCache
module, possibly as an extension of the GraphTraverser
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Works for me, will move it in the module.
3d346e2
to
f4fbb11
Compare
public init( | ||
testPlan: String? = nil, | ||
includedTargets: Set<String>, | ||
excludedTargets: Set<String> = [] | ||
) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
avoid defaults in internal methods, they just risk you forget passing parameters
public init( | |
testPlan: String? = nil, | |
includedTargets: Set<String>, | |
excludedTargets: Set<String> = [] | |
) { | |
public init(testPlan: String?, includedTargets: Set<String>, excludedTargets: Set<String>) { |
@Option( | ||
name: .long, | ||
parsing: .upToNextOption, | ||
help: "The list of test identifiers you want to test. Takes precedence over --skip-testing", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
help: "The list of test identifiers you want to test. Takes precedence over --skip-testing", | |
help: "The list of test identifiers you want to test. Expected format is TestTarget[/TestClass[/TestMethod]]. It is applied before --skip-testing", |
@Option( | ||
name: .long, | ||
parsing: .upToNextOption, | ||
help: "The list of test identifiers you want to skip testing.", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
help: "The list of test identifiers you want to skip testing.", | |
help: "The list of test identifiers you want to skip testing. Expected format is TestTarget[/TestClass[/TestMethod]].", |
@Option( | ||
name: .long, | ||
parsing: .upToNextOption, | ||
help: "The list of configurations you want to test. Takes precedence over --skip-test-configuration" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
help: "The list of configurations you want to test. Takes precedence over --skip-test-configuration" | |
help: "The list of configurations you want to test. It is applied before --skip-test-configuration" |
@Option( | ||
name: .long, | ||
parsing: .upToNextOption, | ||
help: "The list of configurations you want to test. Takes precedence over --skip-test-configuration" | ||
) | ||
var testConfigurations: [String] = [] | ||
|
||
@Option( | ||
name: .long, | ||
parsing: .upToNextOption, | ||
help: "The list of configurations you want to skip testing." | ||
) | ||
var skipTestConfigurations: [String] = [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How does it work if you specify both of these?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure, I'll double check
func validate() throws { | ||
let intersection = Set(testTargets).intersection(skipTestTargets) | ||
if !intersection.isEmpty { | ||
throw ValidationError.invalidTestTargetsOptions(intersection) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are also other invalid scenario. For example, if testTargets is [TargetName
] it is invalid/wrong to have skipTestTarget as [AnotherTargetName/ClassName
], as it doesn't skip anything from the selected set
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed, though I'm not sure this would be considered an error as technically AnotherTargetName/ClassName
will be skipped. Maybe we could output some some of warning instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's cleary an error in passing the parameter, better to let the user know by making the command fail
projects/docs/docs/commands/test.md
Outdated
| `--result-bundle-path` | `-T` | `Path where test result bundle will be saved` | | No | | ||
| `--retry-count` | n/a | `Tests will retry <number> of times until they succeed.` | 0 | | | ||
| `--test-plan` | n/a | `When passed, the test plan to run. The scheme parameter is required when a test plan is passed.` | | No | | ||
| `--test-targets` | n/a | `Specify which tests should run in the test-identifier* format. Takes precedence over --skip-testing` | | No | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| `--test-targets` | n/a | `Specify which tests should run in the test-identifier* format. Takes precedence over --skip-testing` | | No | | |
| `--test-targets` | n/a | `Specify which tests should run in the test-identifier* format. It is applied before --skip-testing` | | No | |
projects/docs/docs/commands/test.md
Outdated
| `--test-plan` | n/a | `When passed, the test plan to run. The scheme parameter is required when a test plan is passed.` | | No | | ||
| `--test-targets` | n/a | `Specify which tests should run in the test-identifier* format. Takes precedence over --skip-testing` | | No | | ||
| `--skip-test-targets` | n/a | `Specify which tests should not run be passed in the test-identifier* format.` | | No | | ||
| `--test-configurations` | n/a | `When a test plan is specified, specify which test configurations to use for the run. Takes precedence over --skip-test-configuration` | | No | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| `--test-configurations` | n/a | `When a test plan is specified, specify which test configurations to use for the run. Takes precedence over --skip-test-configuration` | | No | | |
| `--test-configurations` | n/a | `When a test plan is specified, specify which test configurations to use for the run. It is applied before --skip-test-configuration` | | No | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for contributing this one @stephanecopin ππΌ. I went through it and left a handful of suggestions. Let me know when they are address and I'll give the PR another pass.
@Option( | ||
name: .long, | ||
parsing: .upToNextOption, | ||
help: "The list of test identifiers you want to test. Expected format is TestTarget[/TestClass[/TestMethod]]. It is applied before --skip-testing", | ||
transform: TestIdentifier.init(string:) | ||
) | ||
var testTargets: [TestIdentifier] = [] | ||
|
||
@Option( | ||
name: .long, | ||
parsing: .upToNextOption, | ||
help: "The list of test identifiers you want to skip testing. Expected format is TestTarget[/TestClass[/TestMethod]].", | ||
transform: TestIdentifier.init(string:) | ||
) | ||
var skipTestTargets: [TestIdentifier] = [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about merging these two options into one using the glob convention? Something along the lines of:
tuist test --filter-targets Target*,!OtherTarget
If it's technically possible, I'd go down this path. Otherwise we have to explain people through the help of the flag in which order the filters are applied, which will most likely go unnoticed for many users.
If it's technically possible, I'd do the same with test-configuration:
tuist test --filter-targets ... --filter-test-configurations ...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@pepicrft this would be ideal, but unfortunately is not actually possible with the current granularity. The value of the options is passed through xcodebuild
as-is which handles running the right tests.
In order to use globs, we would need to know 3 pieces of info:
- The list of targets, which we have access to through the project file (and test plan if provided)
- The list of test cases in the target, which is not easy to figure out.
We could:- Approximate that by assuming that all files ending in
Tests.swift
have a test case of the same name, and use the project file to enumerate all tests - Use
SwiftSyntax
to parse all swift files in the test target and infer the test cases from there
- Approximate that by assuming that all files ending in
- The list of individual tests in a test case. The only way I could see would be to use SwiftSyntax there as well.
TL;DR using globs would basically have us recreate functionality build-in xcodebuild.
parsing: .upToNextOption, | ||
help: "The list of configurations you want to test. It is applied before --skip-test-configuration" | ||
) | ||
var testConfigurations: [String] = [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this flag is used as part of the test
command, would it make sense to remove some redundancy in the naming?
tuist test --filter-configurations
# instead of
tuist test --filter-test-configurations
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That works for me!
case let .testPlanNotFound(scheme, testPlan, existing): | ||
let existingMessage: String | ||
if existing.isEmpty { | ||
existingMessage = "No test plans are defined for this scheme" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpick: For someone writing or reading this code it's obvious what "this" refers to, but it might not be for the user receiving the following error:
No test plans are defined for this scheme
Which scheme? Since you have it already in the enum case, I'd include it:
We could not execute the test plan X because the scheme Y doesn't have test plans defined.
if existing.isEmpty { | ||
existingMessage = "No test plans are defined for this scheme" | ||
} else { | ||
existingMessage = "The available test plans are: \(existing.joined(separator: ","))" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And same here, add as much context as possible for the user to understand the error. What scheme are we talking about? What if we match the testPlan
against all the existing and suggest one of them if it's very close to the one given by the user? It's possible they made a typo while invoking the CLI.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Works for me, I'll add the info about the scheme.
As for matching test plan against existing ones using some sort a fuzzy search, I'd rather keep that as a separate improvement as the same logic could be applied in other places (e.g. the scheme above, but probably other places within Tuist), let me know what you think! We can add an improvement issue so we don't forgot about it.
logger.log(level: .info, "There are no tests to run, finishing early") | ||
return | ||
case (_?, _, true), (_?, _, nil): | ||
logger.log(level: .info, "There are no test plans to run, finishing early") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This and the above case are not the same so I'd expect the message to be different. Assuming I read the code well, this one should be something more like:
The scheme X's test action Y has no test plans to run, finishing early.
public func testableTarget( | ||
scheme: Scheme, | ||
testPlan: String?, | ||
testTargets: [TestIdentifier], | ||
skipTestTargets: [TestIdentifier], | ||
graphTraverser: GraphTraversing | ||
) -> GraphTarget? { | ||
func isIncluded(_ testTarget: TestPlan.TestTarget) -> Bool { | ||
if !testTarget.isEnabled { | ||
return false | ||
} else if testTargets.isEmpty { | ||
return !skipTestTargets.contains { $0.target == testTarget.target.name } | ||
} else { | ||
return testTargets.contains { $0.target == testTarget.target.name } | ||
} | ||
} | ||
|
||
if let testPlanName = testPlan, | ||
let testPlan = scheme.testAction?.testPlans?.first(where: { $0.name == testPlanName }), | ||
let target = testPlan.testTargets.first(where: { isIncluded($0) })?.target | ||
{ | ||
return graphTraverser.target(path: target.projectPath, name: target.name) | ||
} else if let testTarget = scheme.testAction?.targets.first { | ||
return graphTraverser.target(path: testTarget.target.projectPath, name: testTarget.target.name) | ||
} else { | ||
return nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd add unit tests that cover the various scenarios in this method.
@@ -122,7 +153,7 @@ public final class BuildGraphInspector: BuildGraphInspecting { | |||
|
|||
public func testableSchemes(graphTraverser: GraphTraversing) -> [Scheme] { | |||
graphTraverser.schemes() | |||
.filter { $0.testAction?.targets.isEmpty == false } | |||
.filter { $0.testAction?.targets.isEmpty == false || $0.testAction?.testPlans?.isEmpty == false } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd also add a unit test that covers this one.
@@ -52,6 +52,10 @@ public class GraphTraverser: GraphTraversing { | |||
allTargets(excludingExternalTargets: true) | |||
} | |||
|
|||
public func allTestPlans() -> Set<TestPlan> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd add tests for the new functions in this class.
@@ -0,0 +1,49 @@ | |||
struct XCTestPlan: Decodable { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd import Foundation
here. Is there a reason why this file is not in the TuistGraph
target where all the other modules live?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd also add coddle tests like we are doing with the other models.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an internal object representing the JSON schema for .xctestplan
files. It's used exclusively within TuistLoader
(which loads it from the disk), and then TuistLoader
uses TuistGraph
's TestPlan
to build a Tuist-representation of the test plan (see the file right above).
Let me know if that makes sense to you, I can add some comments to clarify if needed!
There is discussion to improve that so Test Plan are generated by Tuist directly (so it wouldn't need to be loaded anymore) here, and I think this should be done eventually but also think it's out of scope for this PR.
@@ -89,3 +89,26 @@ extension TuistGraph.TestAction { | |||
) | |||
} | |||
} | |||
|
|||
extension TestPlan { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's be consistent with the other {Model}+ManifestMapper.swift
and extract this one into a TestPlan+ManifestMapper.swift
file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @pepicrft for your review! I've answer to the relevant comments, and will address your feedback. Not sure when I'll be able to, I'll keep you posted!
var description: String { | ||
switch self { | ||
case let .invalidTestIdentifier(value): | ||
return "Invalid test identifiers \(value). The expected format is TestTarget[/TestClass[/TestMethod]]." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is indeed
@Option( | ||
name: .long, | ||
parsing: .upToNextOption, | ||
help: "The list of test identifiers you want to test. Expected format is TestTarget[/TestClass[/TestMethod]]. It is applied before --skip-testing", | ||
transform: TestIdentifier.init(string:) | ||
) | ||
var testTargets: [TestIdentifier] = [] | ||
|
||
@Option( | ||
name: .long, | ||
parsing: .upToNextOption, | ||
help: "The list of test identifiers you want to skip testing. Expected format is TestTarget[/TestClass[/TestMethod]].", | ||
transform: TestIdentifier.init(string:) | ||
) | ||
var skipTestTargets: [TestIdentifier] = [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@pepicrft this would be ideal, but unfortunately is not actually possible with the current granularity. The value of the options is passed through xcodebuild
as-is which handles running the right tests.
In order to use globs, we would need to know 3 pieces of info:
- The list of targets, which we have access to through the project file (and test plan if provided)
- The list of test cases in the target, which is not easy to figure out.
We could:- Approximate that by assuming that all files ending in
Tests.swift
have a test case of the same name, and use the project file to enumerate all tests - Use
SwiftSyntax
to parse all swift files in the test target and infer the test cases from there
- Approximate that by assuming that all files ending in
- The list of individual tests in a test case. The only way I could see would be to use SwiftSyntax there as well.
TL;DR using globs would basically have us recreate functionality build-in xcodebuild.
parsing: .upToNextOption, | ||
help: "The list of configurations you want to test. It is applied before --skip-test-configuration" | ||
) | ||
var testConfigurations: [String] = [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That works for me!
func validate() throws { | ||
let targetsIntersection = Set(testTargets) | ||
.intersection(skipTestTargets) | ||
if !targetsIntersection.isEmpty { | ||
throw ValidationError.duplicatedTestTargets(targetsIntersection) | ||
} | ||
if !testTargets.isEmpty { | ||
// --test-targets Test --skip-test-targets AnotherTest | ||
let skipTestTargetsOnly = Set(skipTestTargets.map { TestIdentifier(target: $0.target) }) | ||
let testTargetsOnly = testTargets.map { TestIdentifier(target: $0.target) } | ||
let targetsOnlyIntersection = skipTestTargetsOnly.intersection(testTargetsOnly) | ||
if targetsOnlyIntersection.isEmpty { | ||
throw ValidationError.nothingToSkip(skipped: skipTestTargets.filter { skipTarget in !testTargetsOnly.contains(TestIdentifier(target: skipTarget.target)) }, included: testTargets) | ||
} | ||
|
||
// --test-targets Test/MyTest --skip-test-targets Test/AnotherTest | ||
let skipTestTargetsClasses = Set(skipTestTargets.map { TestIdentifier(target: $0.target, class: $0.class) }) | ||
let testTargetsClasses = testTargets.map { TestIdentifier(target: $0.target, class: $0.class) } | ||
let targetsClassesIntersection = skipTestTargetsClasses.intersection(testTargetsClasses) | ||
.intersection(testTargetsClasses.map { TestIdentifier(target: $0.target, class: $0.class) }) | ||
if targetsClassesIntersection.isEmpty { | ||
throw ValidationError.nothingToSkip(skipped: skipTestTargets.filter { skipTarget in !testTargetsClasses.contains { $0 == TestIdentifier(target: skipTarget.target, class: skipTarget.class) } }, included: testTargets) | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can extract it to another place indeed
@@ -5,19 +5,33 @@ import TuistGraph | |||
|
|||
/// `FocusTargetsGraphMappers` is used to filter out some targets and their dependencies and tests targets. | |||
public final class FocusTargetsGraphMappers: GraphMapping { | |||
// When specified, if includedTargets is empty it will automatically include all targets in the test plan |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed, why does that make you wary?
In this context includedTargets
is a filter onto either the entire test suite (if no test plans are available) or a filter onto the test plan (if provided), a test plan being itself a subset of the entire test suite.
Does that make sense to you?
return Set( | ||
targets.filter { target in | ||
if !includedTargets.isEmpty { | ||
return includedTargets.contains(target.target.name) | ||
} | ||
if excludedTargets.contains(target.target.name) { | ||
return false | ||
} | ||
if let allTestPlansTargetNames = allTestPlansTargetNames { | ||
return allTestPlansTargetNames.contains(target.target.name) | ||
} | ||
return excludingExternalTargets ? allInternalTargets.contains(target.target.name) : true | ||
} | ||
) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, nice catch! Indeed, the logic should be:
1. If a test plan is specified, if the target target is not in the test plan, exclude it
2. Include the target target if it's included in in includedTargets.
3. Exclude it if it's included in excludedTargets.
4. If we're excluding external targets, only include the target if internal. If we're including external targets, include the target.
Does that make more sense?
As for the name of the function, is it fine with that new logic, and otherwise what would you suggest? That's the best I had π
if existing.isEmpty { | ||
existingMessage = "No test plans are defined for this scheme" | ||
} else { | ||
existingMessage = "The available test plans are: \(existing.joined(separator: ","))" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Works for me, I'll add the info about the scheme.
As for matching test plan against existing ones using some sort a fuzzy search, I'd rather keep that as a separate improvement as the same logic could be applied in other places (e.g. the scheme above, but probably other places within Tuist), let me know what you think! We can add an improvement issue so we don't forgot about it.
@@ -0,0 +1,49 @@ | |||
struct XCTestPlan: Decodable { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an internal object representing the JSON schema for .xctestplan
files. It's used exclusively within TuistLoader
(which loads it from the disk), and then TuistLoader
uses TuistGraph
's TestPlan
to build a Tuist-representation of the test plan (see the file right above).
Let me know if that makes sense to you, I can add some comments to clarify if needed!
There is discussion to improve that so Test Plan are generated by Tuist directly (so it wouldn't need to be loaded anymore) here, and I think this should be done eventually but also think it's out of scope for this PR.
@testable import TuistCache | ||
|
||
class GraphTraverserExtrasTests: XCTestCase { | ||
func test_filterIncludedTargets_includeExcluded() throws { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, my intent here was to test all the possible scenarii involved using filterIncludedTargets
. I can split them up
// When | ||
filteredTargets = subject.filterIncludedTargets( | ||
basedOn: subject.allTargets(), | ||
testPlan: nil, | ||
includedTargets: [tests1.name], | ||
excludedTargets: [] | ||
) | ||
|
||
// Then | ||
XCTAssertEqual(filteredTargets.map(\.target), [tests1]) | ||
|
||
// When | ||
filteredTargets = subject.filterIncludedTargets( | ||
basedOn: subject.allTargets(), | ||
testPlan: nil, | ||
includedTargets: [tests1.name], | ||
excludedTargets: [tests1.name] | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Works for me! My intent initially was to avoid duplicating the inputs of tests too much. I'll see if I can replace them with properties and not make it too confusing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks a lot for this PR π It's a great contribution
@@ -1,12 +1,26 @@ | |||
import Foundation | |||
import TSCBasic | |||
|
|||
public struct TestPlan: Equatable, Codable { | |||
public struct TestPlan: Hashable, Codable { | |||
public struct TestTarget: Hashable, Codable { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we reuse the TestableTarget model instead of creating a new one?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm fine with using it, reversing isEnabled
with !isSkipped
. I'll do the change
func validate() throws { | ||
let targetsIntersection = Set(testTargets) | ||
.intersection(skipTestTargets) | ||
if !targetsIntersection.isEmpty { | ||
throw ValidationError.duplicatedTestTargets(targetsIntersection) | ||
} | ||
if !testTargets.isEmpty { | ||
// --test-targets Test --skip-test-targets AnotherTest | ||
let skipTestTargetsOnly = Set(skipTestTargets.map { TestIdentifier(target: $0.target) }) | ||
let testTargetsOnly = testTargets.map { TestIdentifier(target: $0.target) } | ||
let targetsOnlyIntersection = skipTestTargetsOnly.intersection(testTargetsOnly) | ||
if targetsOnlyIntersection.isEmpty { | ||
throw ValidationError.nothingToSkip(skipped: skipTestTargets.filter { skipTarget in !testTargetsOnly.contains(TestIdentifier(target: skipTarget.target)) }, included: testTargets) | ||
} | ||
|
||
// --test-targets Test/MyTest --skip-test-targets Test/AnotherTest | ||
let skipTestTargetsClasses = Set(skipTestTargets.map { TestIdentifier(target: $0.target, class: $0.class) }) | ||
let testTargetsClasses = testTargets.map { TestIdentifier(target: $0.target, class: $0.class) } | ||
let targetsClassesIntersection = skipTestTargetsClasses.intersection(testTargetsClasses) | ||
.intersection(testTargetsClasses.map { TestIdentifier(target: $0.target, class: $0.class) }) | ||
if targetsClassesIntersection.isEmpty { | ||
throw ValidationError.nothingToSkip(skipped: skipTestTargets.filter { skipTarget in !testTargetsClasses.contains { $0 == TestIdentifier(target: skipTarget.target, class: skipTarget.class) } }, included: testTargets) | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TestService
would most likely be the right place. Or a new utility class.
allTestPlansTargetNames = nil | ||
} | ||
|
||
lazy var allInternalTargets = allInternalTargets().map(\.target.name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this var lazy
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is lazy as it is not used in all code path, so it's initialized only when used.
} | ||
|
||
public let target: String | ||
public let `class`: String? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we should rename this to className
, so we don't have to escape it everywhere?
self.method = method | ||
} | ||
|
||
public init(string: String) throws { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be worth testing this init
@@ -1,47 +1,54 @@ | |||
# frozen_string_literal: true | |||
|
|||
require "xcodeproj" | |||
require 'xcodeproj' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I don't see a reason for the change of quotation marks? Makes it harder to follow the actual changes here. In the other step_definitions
files, we tend to use "
instead of '
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@fortmarek ah sorry about that, I have rubocop installed globally which do these changes automatically. I'll revert them. As an improvement, it may be good to such rules for Tuist so the style is kept consistency across all Ruby files?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this something we expect developers to be able to generate themselves? Does this PR provide value to tuist users until we deliver on generating the test plan functionality (#5214)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@fortmarek these will be generated by the user via Xcode. The PR allows to use test plans when using tuist test
(which is not possible currently), so this is the value the PR provide. This is independent from being able to create test plans using Tuist.
5a19642
to
2cee88d
Compare
Hey @pepicrft @fortmarek @danyf90, I've finally got around to updating the PR, and should have addressed all of your comments. Please let me know if I missed anything! There was a bunch of additions, especially related to unit tests π |
37194a8
to
479b41e
Compare
Resolves #5080
Short description π
Add 5 new flags to
tuist test
:--test-plan
)--only-test-configuration
,--skip-test-configuration
)--only-testing
,--skip-testing
)The changes are integrated with the Tuist graph & cache system, and will be optimized to build only what's changed.
Open to any suggestions regarding to naming the flags, or if they should be handled differently that I've implemented in the PR.
Implementation-wide:
How to test the changes locally π§
Changes are unit tested and can also be tested using
./fourier test tuist acceptance projects/tuist/features/test-plan.feature
Contributor checklist β
./fourier lint tuist --fix
Reviewer checklist β
changelog:added
,changelog:fixed
, orchangelog:changed
, and the title is usable as a changelog entry