Add GenerationOption to enable code coverage (#2020)

Resolves [no ticket]

### Short description 📝

Code coverage is not enabled when schemes are automatically generated. To enable it, one needs to define a scheme manually to enable `Coverage` and set `codeCoverageTargets`. 

This change introduces a new `generationOption` case: `enableCodeCoverage` that will enable code coverage for automatically generated schemes.

### Solution 📦

I started by attempting to update Project.swift with a manually-defined scheme to enable code coverage, but there are a lot of projects in our workspace and it seemed like a broader solution would be more useful than one that would require touching each project (and potentially ensuring the stand-in schemes are kept up-to-date).

I started with my colleague @kalkwarf's PR #1782 and used that as a guide to make similar changes in what seemed like appropriate places including the generator code, tests, and documentation.

This change doesn't introduce any breaking changes and hopefully avoids any unnecessary complexity, while making it possible to enable code coverage with one line: 
```
let config = Config(
  generationOptions: [
    // your options here
    .enableCodeCoverage,     <-- this one :-)
  ]
)
```

### Implementation 👩‍💻👨‍💻

- [x] Extend `GenerationOptions` enum with `enableCodeCoverage` case.
- [x] Update `AutogeneratedSchemesProjectMapper` to initialize with `TuistCore.Config` .
- [x] Check config to to enable code coverage and generate code coverage targets.
- [x] Update `AutogeneratedSchemesProjectMapperTests` to verify new behavior.
- [x] Checked that existing tests would fail if the they were run with the new option.
- [x] Added new case to `docs/usage/config.mdx`.


### **One more thing™** 
~This is a draft since I am not sure the test configuration this produces is correct, but I wanted to get eyes on the changes to the project in general.~ Looks like the coverage config is good to go! Thanks for reading!!
This commit is contained in:
kodiakhq[bot] 2020-11-12 16:40:16 +00:00 committed by GitHub
commit eead4911f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 92 additions and 9 deletions

View File

@ -11,6 +11,7 @@ Please, check out guidelines: https://keepachangelog.com/en/1.0.0/
- Synthesize accessors for stringsdict [#1993](https://github.com/tuist/tuist/pull/1993) by [@fortmarek](https://githubl.com/fortmarek)
- Add support for `StencilSwiftKit`'s additional filters. [#1994](https://github.com/tuist/tuist/pull/1994) by [@svastven](https://github.com/svastven).
- Add `migration list-targets` command to show all targets sorted by number of dependencies [#1732](https://github.com/tuist/tuist/pull/1732) of a given project by [@andreacipriani](https://github.com/andreacipriani).
- Add `enableCodeCoverage` generation option to enable code coverage in automatically generated schemes [#ZZZZ](https://github.com/tuist/tuist/pull/ZZZZ) by [@frijole](https://github.com/frijole).)
### Fixed

View File

@ -12,6 +12,7 @@ public struct Config: Codable, Equatable {
/// - disableAutogeneratedSchemes: When passed, Tuist generates the project only with custom specified schemes, autogenerated default schemes are skipped
/// - disableSynthesizedResourceAccessors: When passed, Tuist does not synthesize resource accessors
/// - disableShowEnvironmentVarsInScriptPhases: When passed, Tuist disables echoing the ENV in shell script build phases
/// - enableCodeCoverage: When passed, Tuist will enable code coverage for autogenerated default schemes
public enum GenerationOptions: Encodable, Decodable, Equatable {
case xcodeProjectName(TemplateString)
case organizationName(String)
@ -19,6 +20,7 @@ public struct Config: Codable, Equatable {
case disableAutogeneratedSchemes
case disableSynthesizedResourceAccessors
case disableShowEnvironmentVarsInScriptPhases
case enableCodeCoverage
}
/// Generation options.
@ -55,6 +57,7 @@ extension Config.GenerationOptions {
case disableAutogeneratedSchemes
case disableSynthesizedResourceAccessors
case disableShowEnvironmentVarsInScriptPhases
case enableCodeCoverage
}
public init(from decoder: Decoder) throws {
@ -90,6 +93,10 @@ extension Config.GenerationOptions {
self = .disableShowEnvironmentVarsInScriptPhases
return
}
if container.allKeys.contains(.enableCodeCoverage), try container.decode(Bool.self, forKey: .enableCodeCoverage) {
self = .enableCodeCoverage
return
}
throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, debugDescription: "Unknown enum case"))
}
@ -113,6 +120,8 @@ extension Config.GenerationOptions {
try container.encode(true, forKey: .disableSynthesizedResourceAccessors)
case .disableShowEnvironmentVarsInScriptPhases:
try container.encode(true, forKey: .disableShowEnvironmentVarsInScriptPhases)
case .enableCodeCoverage:
try container.encode(true, forKey: .enableCodeCoverage)
}
}
}
@ -136,6 +145,8 @@ public func == (lhs: TuistConfig.GenerationOptions, rhs: TuistConfig.GenerationO
return true
case (.disableShowEnvironmentVarsInScriptPhases, .disableShowEnvironmentVarsInScriptPhases):
return true
case (.enableCodeCoverage, .enableCodeCoverage):
return true
default:
return false
}

View File

@ -14,6 +14,7 @@ public struct Config: Equatable, Hashable {
case disableAutogeneratedSchemes
case disableSynthesizedResourceAccessors
case disableShowEnvironmentVarsInScriptPhases
case enableCodeCoverage
}
/// Generation options.

View File

@ -4,9 +4,13 @@ import TuistCore
/// A project mapper that auto-generates schemes for each of the targets of the `Project`
/// if the user hasn't already defined schemes for those.
public final class AutogeneratedSchemesProjectMapper: ProjectMapping {
private let enableCodeCoverage: Bool
// MARK: - Init
public init() {}
public init(enableCodeCoverage: Bool) {
self.enableCodeCoverage = enableCodeCoverage
}
// MARK: - ProjectMapping
@ -17,6 +21,7 @@ public final class AutogeneratedSchemesProjectMapper: ProjectMapping {
let autogeneratedSchemes = project.targets.compactMap { (target: Target) -> Scheme? in
let scheme = self.createDefaultScheme(target: target,
project: project,
codeCoverage: enableCodeCoverage,
buildConfiguration: project.defaultDebugBuildConfigurationName)
// The user has already defined a scheme with that name.
if schemeNames.contains(scheme.name) { return nil }
@ -28,21 +33,23 @@ public final class AutogeneratedSchemesProjectMapper: ProjectMapping {
// MARK: - Private
private func createDefaultScheme(target: Target, project: Project, buildConfiguration: String) -> Scheme {
private func createDefaultScheme(target: Target, project: Project, codeCoverage: Bool, buildConfiguration: String) -> Scheme {
let targetReference = TargetReference(projectPath: project.path, name: target.name)
let buildTargets = buildableTargets(targetReference: targetReference, target: target, project: project)
let testTargets = testableTargets(targetReference: targetReference, target: target, project: project)
let executable = runnableExecutable(targetReference: targetReference, target: target, project: project)
let codeCoverageTargets = codeCoverage ? [targetReference] : []
return Scheme(name: target.name,
shared: true,
buildAction: BuildAction(targets: buildTargets),
testAction: TestAction(targets: testTargets,
arguments: nil,
configurationName: buildConfiguration,
coverage: false,
codeCoverageTargets: [],
coverage: enableCodeCoverage,
codeCoverageTargets: codeCoverageTargets,
preActions: [],
postActions: [],
diagnosticsOptions: Set()),

View File

@ -27,7 +27,7 @@ final class ProjectMapperProvider: ProjectMapperProviding {
// Auto-generation of schemes
if !config.generationOptions.contains(.disableAutogeneratedSchemes) {
mappers.append(AutogeneratedSchemesProjectMapper())
mappers.append(AutogeneratedSchemesProjectMapper(enableCodeCoverage: config.generationOptions.contains(.enableCodeCoverage)))
}
// Delete current derived

View File

@ -71,7 +71,7 @@ final class ProjectEditor: ProjectEditing {
templatesDirectoryLocator: TemplatesDirectoryLocating = TemplatesDirectoryLocator(),
projectMapper: ProjectMapping = SequentialProjectMapper(
mappers: [
AutogeneratedSchemesProjectMapper(),
AutogeneratedSchemesProjectMapper(enableCodeCoverage: false),
]
),
sideEffectDescriptorExecutor: SideEffectDescriptorExecuting = SideEffectDescriptorExecutor()

View File

@ -42,6 +42,8 @@ extension TuistCore.Config.GenerationOption {
return .disableSynthesizedResourceAccessors
case .disableShowEnvironmentVarsInScriptPhases:
return .disableShowEnvironmentVarsInScriptPhases
case .enableCodeCoverage:
return .enableCodeCoverage
}
}
}

View File

@ -12,6 +12,7 @@ final class ConfigTests: XCTestCase {
.disableAutogeneratedSchemes,
.disableSynthesizedResourceAccessors,
.disableShowEnvironmentVarsInScriptPhases,
.enableCodeCoverage,
])
XCTAssertCodable(config)

View File

@ -12,7 +12,7 @@ final class AutogeneratedSchemesProjectMapperTests: TuistUnitTestCase {
override func setUp() {
super.setUp()
subject = AutogeneratedSchemesProjectMapper()
subject = AutogeneratedSchemesProjectMapper(enableCodeCoverage: false)
}
override func tearDown() {
@ -142,13 +142,50 @@ final class AutogeneratedSchemesProjectMapperTests: TuistUnitTestCase {
])
}
func test_map_enables_test_coverage_on_generated_schemes() throws {
// Given
subject = AutogeneratedSchemesProjectMapper(enableCodeCoverage: true)
let targetA = Target.test(
name: "A",
dependencies: [
.target(name: "B"),
]
)
let targetATests = Target.test(
name: "ATests",
product: .unitTests,
dependencies: [.target(name: "A")]
)
let projectPath = try temporaryPath()
let project = Project.test(
path: projectPath,
targets: [
targetA,
targetATests,
]
)
// When
let (got, sideEffects) = try subject.map(project: project)
// Then
XCTAssertEmpty(sideEffects)
XCTAssertEqual(got.schemes.count, 2)
// Then: A Tests
let gotAScheme = got.schemes.first!
XCTAssertTrue(gotAScheme.testAction?.coverage != nil)
XCTAssertEqual(gotAScheme.testAction?.codeCoverageTargets.count, 1)
}
// MARK: - Helpers
private func testScheme(
target: Target,
target: TuistCore.Target,
projectPath: AbsolutePath,
testTargetName: String
) -> Scheme {
) -> TuistCore.Scheme {
Scheme(
name: target.name,
shared: true,

View File

@ -95,4 +95,22 @@ final class ProjectMapperProviderTests: TuistUnitTestCase {
let sequentialProjectMapper = try XCTUnwrap(got as? SequentialProjectMapper)
XCTAssertEqual(sequentialProjectMapper.mappers.filter { $0 is TargetProjectMapper }.count, 1)
}
func test_mappers_does_enable_code_coverage() throws {
// Given
subject = ProjectMapperProvider(contentHasher: CacheContentHasher())
// When
let got = subject.mapper(
config: Config.test(
generationOptions: [
.enableCodeCoverage,
]
)
)
// Then
let sequentialProjectMapper = try XCTUnwrap(got as? SequentialProjectMapper)
XCTAssertEqual(sequentialProjectMapper.mappers.filter { $0 is AutogeneratedSchemesProjectMapper }.count, 1)
}
}

View File

@ -125,6 +125,11 @@ Generation options allow customizing the generation of Xcode projects.
description:
'Do not automatically synthesize resource accessors (assets, localized strings, etc.)',
},
{
case: '.enableCodeCoverage',
description:
'Enable code coverage for auto generated schemes.',
},
]}
/>