Skip to content
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

DRAFT : Adding functionality to post code coverage #5

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
109 changes: 76 additions & 33 deletions Sources/DangerSwiftKantoku/Kantoku+Coverage.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//
// Kantoku+Coverage.swift
//
//
//
// Created by 史 翔新 on 2022/03/03.
//
Expand All @@ -9,79 +9,122 @@ import Foundation
import XCResultKit

extension Kantoku {

var coverageNumberFormatter: NumberFormatter {

let formatter = NumberFormatter()
formatter.numberStyle = .percent
formatter.maximumFractionDigits = 2
return formatter

}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nits]
Please don't remove these blank lines. I made them for better readability.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cool, would it possible to have a swift format file to the repo, would make it easier to make sure that we stick to convention without any misses

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will add it later this week 👍

}

extension Kantoku {

enum CoverageAcceptance {
case good
case acceptable
case reject
}

private struct CoverageCommentItem {
var title: String
var coverage: Double
var acceptance: CoverageAcceptance
}

func post(_ coverage: CodeCoverage, as acceptanceDecision: (Double) -> CoverageAcceptance) {


func post(_ coverage: CodeCoverage, for files: [XCResultParsingConfiguration.RelativeFilePath], as acceptanceDecision: (Double) -> CoverageAcceptance) {
let formatter = coverageNumberFormatter
let title = "Overall"
let overallCoverage = coverage.lineCoverage
let overallAcceptance = acceptanceDecision(overallCoverage)
let coverageText = formatter.string(from: overallCoverage as NSNumber) ?? {
fail("Failed to extract overall coverage from \(overallCoverage)")
let overallCoverageText = formatter.string(from: coverage.lineCoverage as NSNumber) ?? {
warn("Failed to extract overall coverage, line coverage \(coverage.lineCoverage)")
return "NaN"
}()

let markdownLines: [String] = [
"| | Coverage | Acceptance |",
"|:---:|:---:|:---:|",
"| \(title) | \(coverageText) | \(overallAcceptance.markdownDescription) |",
// TODO: Coverage of diff files
]
let overallAcceptance = acceptanceDecision(coverage.lineCoverage)
var markdownLines: [String] = ["##Overall Coverage",
"\(overallCoverageText) \(overallAcceptance.markdownDescription)"]

markdownLines.append("")
coverage.targets.forEach { target in
let filtered = target.filteringFiles(on: files)
if filtered.files.count > 0 {
markdownLines.append("")
let acceptance = acceptanceDecision(target.lineCoverage)
let coverageText = formatter.string(from: target.lineCoverage as NSNumber) ?? "NaN"
markdownLines.append(contentsOf: ["| ###\(target.name) | \(coverageText) | \(acceptance.markdownDescription) |",
""])
markdownLines.append(contentsOf: ["| File | Coverage | Acceptance |",
"|:---:|:---:|:---:|"])
filtered.files.forEach {
let acceptance = acceptanceDecision($0.lineCoverage)
let coverageText = formatter.string(from: $0.lineCoverage as NSNumber) ?? "NaN"
markdownLines.append("| \($0.name) | \(coverageText) | \(acceptance.markdownDescription) |")
}
}
}

markdown(markdownLines.joined(separator: "\n"))

switch overallAcceptance {
case .good:
break

case .acceptable:
warn("Overall coverage is \(overallCoverage)")
warn("Overall coverage is \(coverage.lineCoverage)")

case .reject:
fail("Overall coverage is \(overallCoverage), which is not enough")
fail("Overall coverage is \(coverage.lineCoverage), which is not enough")
}

}

}

extension CodeCoverage {
func filterTargets(excludedTargets: [ExcludedTarget]) -> CodeCoverage {
let targets = self.targets.filter { target in
!target.files.isEmpty && !excludedTargets.contains {
$0.matches(string: target.name)
}
}
return CodeCoverage(targets: targets)
}

func filterTarget(excludeFiles: (XCResultParsingConfiguration.RelativeFilePath) -> Bool) -> CodeCoverage {
let targets = self.targets
.map { $0.filteringFiles(excludeFiles: excludeFiles) }
.filter { target in
!target.files.isEmpty
}
return CodeCoverage(targets: targets)
}

func filteringTargets(on files: [String]) -> CodeCoverage {
let targets = self.targets
.map { $0.filteringFiles(on: files) }
.filter { target in
!target.files.isEmpty
}
return CodeCoverage(targets: targets)
}
}

extension CodeCoverageTarget {
func filteringFiles(on files: [String]) -> CodeCoverageTarget {
let files = self.files.filter { files.contains($0.path) }
return CodeCoverageTarget(name: name, buildProductPath: buildProductPath, files: files)
}

func filteringFiles(excludeFiles: (XCResultParsingConfiguration.RelativeFilePath) -> Bool) -> CodeCoverageTarget {
let files = self.files.filter { excludeFiles($0.path) }
return CodeCoverageTarget(name: name, buildProductPath: buildProductPath, files: files)
}
}

extension Kantoku.CoverageAcceptance {

var markdownDescription: String {
switch self {
case .good:
return ":white_flower:"

case .acceptable:
return ":thinking:"

case .reject:
return ":no_good:"
}
}

}
102 changes: 61 additions & 41 deletions Sources/DangerSwiftKantoku/Kantoku.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//
// Kantoku.swift
//
//
//
// Created by 史 翔新 on 2022/02/10.
//
Expand All @@ -9,22 +9,21 @@ import Foundation
import XCResultKit

public struct Kantoku {

let workingDirectoryPath: String
let modifiedFiles: [String]
let createdFiles: [String]

private let markdownCommentExecutor: (_ comment: String) -> Void

private let inlineCommentExecutor: (_ comment: String, _ filePath: String, _ lineNumber: Int) -> Void
private let normalCommentExecutor: (_ comment: String) -> Void

private let inlineWarningExecutor: (_ comment: String, _ filePath: String, _ lineNumber: Int) -> Void
private let normalWarningExecutor: (_ comment: String) -> Void

private let inlineFailureExecutor: (_ comment: String, _ filePath: String, _ lineNumber: Int) -> Void
private let normalFailureExecutor: (_ comment: String) -> Void

init(
workingDirectoryPath: String,
modifiedFiles: [String],
Expand All @@ -48,108 +47,106 @@ public struct Kantoku {
self.inlineFailureExecutor = inlineFailureExecutor
self.normalFailureExecutor = normalFailureExecutor
}

}

extension Kantoku {

func markdown(_ comment: String) {
markdownCommentExecutor(comment)
}

func comment(_ comment: String, to filePath: String, at lineNumber: Int) {
inlineCommentExecutor(comment, filePath, lineNumber)
}

func comment(_ comment: String) {
normalCommentExecutor(comment)
}

func warn(_ warning: String, to filePath: String, at lineNumber: Int) {
inlineWarningExecutor(warning, filePath, lineNumber)
}

func warn(_ warning: String) {
normalWarningExecutor(warning)
}

func fail(_ failure: String, to filePath: String, at lineNumber: Int) {
inlineFailureExecutor(failure, filePath, lineNumber)
}

func fail(_ failure: String) {
normalFailureExecutor(failure)
}

}



extension Kantoku {

private func postIssuesIfNeeded(from resultFile: XCResultFile, configuration: XCResultParsingConfiguration) {

if configuration.needsIssues {

guard let issues = resultFile.getInvocationRecord()?.issues else {
warn("Failed to get invocation record from \(resultFile.url.absoluteString)")
return
}

if configuration.parseBuildWarnings {
let filteredSummaries = summaries(of: issues.warningSummaries, filteredBy: configuration.reportingFileType)
let filteredSummaries = summaries(of: issues.warningSummaries, filteredBy: configuration.reportingFileType)
post(filteredSummaries, as: .warning)
}

if configuration.parseBuildErrors {
post(issues.errorSummaries, as: .failure)
}

if configuration.parseAnalyzerWarnings {
post(issues.analyzerWarningSummaries, as: .warning)
}

if configuration.parseTestFailures {
post(issues.testFailureSummaries, as: .failure)
}

}

}

private func postCoverageIfNeeded(from resultFile: XCResultFile, configuration: XCResultParsingConfiguration) {

if let coverageAcceptanceDecision = configuration.codeCoverageRequirement.acceptanceDecision {

guard let coverage = resultFile.getCodeCoverage() else {
warn("Failed to get coverage from \(resultFile.url.absoluteString)")
if configuration.failIfCoverageUnavailable {
fail("Failed to get coverage from \(resultFile.url.absoluteString)")
} else {
warn("Failed to get coverage from \(resultFile.url.absoluteString)")
}

return
}

post(coverage, as: coverageAcceptanceDecision)

let excludeCoverageTarget = configuration.excludeCoverageTarget + [.regex("*Tests$")]
var relaventTestCoverage = coverage.filterTargets(excludedTargets: excludeCoverageTarget)
relaventTestCoverage = coverage.filterTarget(excludeFiles: configuration.excludeCoverageForFiles)
let files: [XCResultParsingConfiguration.RelativeFilePath]
if configuration.showCoverageForChangedFiles {
files = (createdFiles + modifiedFiles)

} else {
files = []
}
post(relaventTestCoverage, for: files, as: coverageAcceptanceDecision)
}

}

public func parseXCResultFile(at filePath: String, configuration: XCResultParsingConfiguration) {

let resultFile = XCResultFile(url: .init(fileURLWithPath: filePath))

postIssuesIfNeeded(from: resultFile, configuration: configuration)
postCoverageIfNeeded(from: resultFile, configuration: configuration)

}

}

extension XCResultParsingConfiguration.CodeCoverageRequirement {

var acceptanceDecision: ((Double) -> Kantoku.CoverageAcceptance)? {
switch self {
case .none:
return nil

case .required(let threshold):
return { coverage in
if coverage >= threshold.recommended {
Expand All @@ -162,7 +159,30 @@ extension XCResultParsingConfiguration.CodeCoverageRequirement {
}
}
}

}

extension Kantoku {
private func summaries<T: PostableIssueSummary>(of summaries: [T], filteredBy fileType: XCResultParsingConfiguration.ReportingFileType) -> [T] {
let filteringPredicate: (XCResultParsingConfiguration.RelativeFilePath) -> Bool

switch fileType {
case .all:
return summaries

case .modifiedAndCreatedFiles:
filteringPredicate = { (modifiedFiles + createdFiles).contains($0) }

case .custom(predicate: let predicate):
filteringPredicate = predicate
}

return summaries.filter { summary in
guard let relativePath = summary.documentLocation?.relativePath(against: workingDirectoryPath) else {
return false
}
return filteringPredicate(relativePath.filePath)
}
}
}

extension Kantoku {
Expand Down
Loading