From fc1a5a882b5c33aa3bf53002919535e9c6a1b086 Mon Sep 17 00:00:00 2001 From: Patrick Butkiewicz Date: Sun, 13 Nov 2016 17:44:44 -0500 Subject: [PATCH 1/5] Add Coverage report model --- Sources/APIClient.swift | 1 - Sources/Coverage.swift | 133 ++++++++++++++++++++++++++++++++++++++++ Sources/JSON.swift | 2 + 3 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 Sources/Coverage.swift create mode 100644 Sources/JSON.swift diff --git a/Sources/APIClient.swift b/Sources/APIClient.swift index a0232d8..1dc74c4 100644 --- a/Sources/APIClient.swift +++ b/Sources/APIClient.swift @@ -8,7 +8,6 @@ import Foundation -typealias JSON = [String: AnyObject] private let ApiClientTimeout: TimeInterval = 30 internal enum APIError: Error { diff --git a/Sources/Coverage.swift b/Sources/Coverage.swift new file mode 100644 index 0000000..c118e44 --- /dev/null +++ b/Sources/Coverage.swift @@ -0,0 +1,133 @@ +// +// Coverage.swift +// Jenkins +// +// Created by Patrick Butkiewicz on 11/13/16. +// +// + +import Foundation + +public enum CodeCoverageProvider { + case Cobertura + case Jacoco + case Other(path: String) + + func path() -> String { + switch self { + case .Cobertura: return "Cobertura" + case .Jacoco: return "jacaco" + case .Other(let path): return path + } + } +} + +public enum CodeCoverageElementType: String { + case Classes = "Classes" + case Conditionals = "Conditionals" + case Files = "Files" + case Lines = "Lines" + case Packages = "Packages" + case Unknown = "Unknown" + + init(_ rawValue: String) { + switch rawValue { + case "Classes": self = .Classes + case "Conditionals": self = .Conditionals + case "Files": self = .Files + case "Lines": self = .Lines + case "Packages": self = .Packages + default: self = .Unknown + } + } +} + +public struct CodeCoverageElement { + private(set) var elementType: CodeCoverageElementType = .Unknown + private(set) var numerator: Int = 0 + private(set) var denominator: Int = 0 + + init(json: JSON) { + let coverageElementName: String = json["name"] as? String ?? "" + elementType = CodeCoverageElementType(coverageElementName) + numerator = json["numerator"] as? Int ?? 0 + denominator = json["denominator"] as? Int ?? 0 + } + + func ratio() -> Double { + return Double(numerator) / Double(denominator) + } +} + +public struct CodeCoverageReport { + private(set) var name: String + private(set) var childReports: [CodeCoverageReport] + private(set) var coverageElements: [CodeCoverageElement] + + init(json: JSON) { + name = json["name"] as? String ?? "" + + // for each child, init self + let childrenJSON: [JSON] = json["children"] as? [JSON] ?? [] + childReports = childrenJSON + .filter({ child in + let elements = child["elements"] as? [JSON] ?? [] + return elements.filter({ $0.count > 0 }).count > 0 + }) + .map({ + CodeCoverageReport(json: $0) + }) + + // for each element, init element + let elementJSON: [JSON] = json["elements"] as? [JSON] ?? [] + coverageElements = elementJSON.map({ CodeCoverageElement(json: $0) }) + } + + func lineRatio() -> Double { + return coverageElements.filter({$0.elementType == .Lines}).first?.ratio() ?? 0 + } +} + +/* + * Jenkins Extension + */ + +extension Jenkins { + func codeCoverage(_ job: String, + build: Int = 0, + depth: Int = 2, + provider: CodeCoverageProvider = .Cobertura, + handler: @escaping (_ coverageReport: CodeCoverageReport?) -> Void) + { + let buildPath = (build == 0) ? "lastSuccessfulBuild" : "\(build)" + guard let url = URL(string: jobURL)? + .appendingPathComponent(job) + .appendingPathComponent(buildPath) + .appendingPathComponent(provider.path()) + .appendingPathComponent("api") + .appendingPathComponent("json") else { + return handler(nil) + } + + client?.get(path: url) { response, error in + guard let json = response as? JSON else { + return handler(nil) + } + + guard let results = json["results"] as? JSON else { + return handler(nil) + } + + handler(CodeCoverageReport(json: results)) + } + } + + func codeCoverage(_ job: Job, + build: Int = 0, + depth: Int = 2, + provider: CodeCoverageProvider = .Cobertura, + handler: @escaping (_ coverageReport: CodeCoverageReport?) -> Void) + { + codeCoverage(job.name, build: build, depth: depth, provider: provider, handler: handler) + } +} diff --git a/Sources/JSON.swift b/Sources/JSON.swift new file mode 100644 index 0000000..1fdacaf --- /dev/null +++ b/Sources/JSON.swift @@ -0,0 +1,2 @@ +import Foundation +typealias JSON = [String: AnyObject] From 789465225416e299f0b237b3763cac80ceed5e5b Mon Sep 17 00:00:00 2001 From: Patrick Butkiewicz Date: Sun, 13 Nov 2016 17:45:02 -0500 Subject: [PATCH 2/5] Add tests for coverage model parsing --- JenkinsTests/CoverageTests.swift | 71 +++++++++++++++++++++ JenkinsTests/Info.plist | 22 +++++++ JenkinsTests/JSON/CoverageReportDepth2.json | 1 + JenkinsTests/JSON/CoverageReportDepth3.json | 2 + JenkinsTests/JenkinsTests.swift | 13 ++++ Tests/Jenkins/JenkinsTests.swift | 17 ----- Tests/LinuxMain.swift | 6 -- 7 files changed, 109 insertions(+), 23 deletions(-) create mode 100644 JenkinsTests/CoverageTests.swift create mode 100644 JenkinsTests/Info.plist create mode 100644 JenkinsTests/JSON/CoverageReportDepth2.json create mode 100644 JenkinsTests/JSON/CoverageReportDepth3.json create mode 100644 JenkinsTests/JenkinsTests.swift delete mode 100644 Tests/Jenkins/JenkinsTests.swift delete mode 100644 Tests/LinuxMain.swift diff --git a/JenkinsTests/CoverageTests.swift b/JenkinsTests/CoverageTests.swift new file mode 100644 index 0000000..528498a --- /dev/null +++ b/JenkinsTests/CoverageTests.swift @@ -0,0 +1,71 @@ +// +// CoverageTests.swift +// Jenkins +// +// Created by Patrick Butkiewicz on 11/13/16. +// +// + +import Foundation +import XCTest +@testable import Jenkins + +class CoverageTests: XCTestCase { + + func testCoberturaCodeCoverageDepth2() { + + guard let path = Bundle(for: type(of: self)).path(forResource: "CoverageReportDepth2", ofType: "json") else { + return XCTFail("Missing Coverage Report JSON") + } + + guard + let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .alwaysMapped), + let json: JSON = try! JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.allowFragments) as? JSON + else { + return XCTFail("Failed mapping Coverage Report JSON") + } + + let c = CodeCoverageReport(json: json["results"] as! JSON) + + let validChildren = 0 + XCTAssert(c.childReports.count == validChildren, "Report has \(c.childReports.count) children, but should have \(validChildren)") + + let validName = "Cobertura Coverage Report" + XCTAssert(c.name.compare(validName) == ComparisonResult.orderedSame, "Coverage report name is '\(c.name)' but should be \(validName)") + + let validCovElements = 5 + XCTAssert(c.coverageElements.count == validCovElements, "Report has \(c.coverageElements.count) coverage elements but should have \(validCovElements)") + + let validLineRatio = 0.2 + XCTAssert(c.lineRatio() == validLineRatio, "Ratio is \(c.lineRatio()) but should be \(validLineRatio)") + } + + func testCoberturaCodeCoverageDepth3() { + guard let path = Bundle(for: type(of: self)).path(forResource: "CoverageReportDepth3", ofType: "json") else { + return XCTFail("Missing Coverage Report JSON") + } + + guard + let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .alwaysMapped), + let json: JSON = try! JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.allowFragments) as? JSON + else { + return XCTFail("Failed mapping Coverage Report JSON") + } + + let c = CodeCoverageReport(json: json["results"] as! JSON) + + let validChildren = 27 + XCTAssert(c.childReports.count == validChildren, "Report has \(c.childReports.count) children, but should have \(validChildren)") + + let validName = "Cobertura Coverage Report" + XCTAssert(c.name.compare(validName) == ComparisonResult.orderedSame, "Coverage report name is '\(c.name)' but should be \(validName)") + + let validCovElements = 5 + XCTAssert(c.coverageElements.count == validCovElements, "Report has \(c.coverageElements.count) coverage elements but should have \(validCovElements)") + + let validLineRatio = 0.20 + XCTAssertEqualWithAccuracy(c.lineRatio(), validLineRatio, accuracy: 0.05, "Ratio is \(c.lineRatio()) but should be \(validLineRatio)") + } + +} + diff --git a/JenkinsTests/Info.plist b/JenkinsTests/Info.plist new file mode 100644 index 0000000..6c6c23c --- /dev/null +++ b/JenkinsTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/JenkinsTests/JSON/CoverageReportDepth2.json b/JenkinsTests/JSON/CoverageReportDepth2.json new file mode 100644 index 0000000..a4032cc --- /dev/null +++ b/JenkinsTests/JSON/CoverageReportDepth2.json @@ -0,0 +1 @@ +{"_class":"hudson.plugins.cobertura.targets.CoverageResult","results":{"children":[{"children":[{}],"elements":[{},{},{},{}],"name":"Pods.Crashlytics.iOS.Crashlytics.framework.Headers"},{"children":[{}],"elements":[{},{},{},{}],"name":"bose-connect-ios.Analytics"},{"children":[{},{},{},{},{}],"elements":[{},{},{},{}],"name":"bose-connect-ios.Controllers"},{"children":[{},{}],"elements":[{},{},{},{}],"name":"bose-connect-ios.Controllers.About"},{"children":[{},{},{},{}],"elements":[{},{},{},{}],"name":"bose-connect-ios.Controllers.CarouselSubControllers"},{"children":[{},{},{}],"elements":[{},{},{},{}],"name":"bose-connect-ios.Controllers.FirmwareUpdate"},{"children":[{}],"elements":[{},{},{},{}],"name":"bose-connect-ios.Controllers.FirmwareUpdate.UpdateSuccessOverlay"},{"children":[{}],"elements":[{},{},{},{}],"name":"bose-connect-ios.Controllers.FullScreenAlert"},{"children":[{}],"elements":[{},{},{},{}],"name":"bose-connect-ios.Controllers.HeartRate"},{"children":[{}],"elements":[{},{},{},{}],"name":"bose-connect-ios.Controllers.Help"},{"children":[{},{},{},{}],"elements":[{},{},{},{}],"name":"bose-connect-ios.Controllers.MusicShare"},{"children":[{},{},{},{}],"elements":[{},{},{},{}],"name":"bose-connect-ios.Controllers.NowPlaying"},{"children":[{}],"elements":[{},{},{},{}],"name":"bose-connect-ios.Controllers.PairingMode"},{"children":[{},{},{},{},{},{},{}],"elements":[{},{},{},{}],"name":"bose-connect-ios.Controllers.Settings"},{"children":[{},{}],"elements":[{},{},{},{}],"name":"bose-connect-ios.Controllers.VideoPlayer"},{"children":[{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}],"elements":[{},{},{},{}],"name":"bose-connect-ios.Extensions"},{"children":[{},{}],"elements":[{},{},{},{}],"name":"bose-connect-ios.Extensions.CrashlyticsLog"},{"children":[{}],"elements":[{},{},{},{}],"name":"bose-connect-ios.Mocks"},{"children":[{},{}],"elements":[{},{},{},{}],"name":"bose-connect-ios.Models"},{"children":[{},{},{},{}],"elements":[{},{},{},{}],"name":"bose-connect-ios.Models.UserDefaults"},{"children":[{}],"elements":[{},{},{},{}],"name":"bose-connect-ios.Utilities"},{"children":[{},{}],"elements":[{},{},{},{}],"name":"bose-connect-ios.ViewModels"},{"children":[{},{},{},{},{}],"elements":[{},{},{},{}],"name":"bose-connect-ios.ViewModels.Carousel"},{"children":[{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}],"elements":[{},{},{},{}],"name":"bose-connect-ios.Views"},{"children":[{},{},{},{},{}],"elements":[{},{},{},{}],"name":"bose-connect-ios.Views.CNC"},{"children":[{},{},{},{},{}],"elements":[{},{},{},{}],"name":"bose-connect-ios.Views.HeartRate"},{"children":[{},{}],"elements":[{},{},{},{}],"name":"bose-connect-ios.Views.MusicShareOnboarding"}],"elements":[{"denominator":10.0,"name":"Packages","numerator":2.0,"ratio":20.0},{"denominator":20.0,"name":"Files","numerator":4.0,"ratio":20.0},{"denominator":20.0,"name":"Classes","numerator":5.0,"ratio":25.0},{"denominator":5.0,"name":"Lines","numerator":1.0,"ratio":20.0},{"denominator":0.0,"name":"Conditionals","numerator":0.0,"ratio":100.0}],"name":"Cobertura Coverage Report"}} diff --git a/JenkinsTests/JSON/CoverageReportDepth3.json b/JenkinsTests/JSON/CoverageReportDepth3.json new file mode 100644 index 0000000..ff85a9a --- /dev/null +++ b/JenkinsTests/JSON/CoverageReportDepth3.json @@ -0,0 +1,2 @@ + +{"_class":"hudson.plugins.cobertura.targets.CoverageResult","results":{"children":[{"children":[{"children":[{}],"elements":[{},{},{}],"name":"Pods/Crashlytics/iOS/Crashlytics.framework/Headers/CLSLogging.h"}],"elements":[{"denominator":1.0,"name":"Files","numerator":0.0,"ratio":0.0},{"denominator":1.0,"name":"Classes","numerator":0.0,"ratio":0.0},{"denominator":1.0,"name":"Lines","numerator":0.0,"ratio":0.0},{"denominator":0.0,"name":"Conditionals","numerator":0.0,"ratio":100.0}],"name":"Pods.Crashlytics.iOS.Crashlytics.framework.Headers"},{"children":[{"children":[{}],"elements":[{},{},{}],"name":"Analytics.swift"}],"elements":[{"denominator":1.0,"name":"Files","numerator":1.0,"ratio":100.0},{"denominator":1.0,"name":"Classes","numerator":1.0,"ratio":100.0},{"denominator":215.0,"name":"Lines","numerator":64.0,"ratio":29.767443},{"denominator":0.0,"name":"Conditionals","numerator":0.0,"ratio":100.0}],"name":"bose-connect-ios.Analytics"},{"children":[{"children":[{}],"elements":[{},{},{}],"name":"AppDelegate.swift"},{"children":[{}],"elements":[{},{},{}],"name":"CarouselViewController.swift"},{"children":[{}],"elements":[{},{},{}],"name":"MainCarouselViewController.swift"},{"children":[{}],"elements":[{},{},{}],"name":"MonetNavigationController.swift"},{"children":[{}],"elements":[{},{},{}],"name":"Router.swift"}],"elements":[{"denominator":5.0,"name":"Files","numerator":4.0,"ratio":80.0},{"denominator":5.0,"name":"Classes","numerator":4.0,"ratio":80.0},{"denominator":918.0,"name":"Lines","numerator":383.0,"ratio":41.721134},{"denominator":0.0,"name":"Conditionals","numerator":0.0,"ratio":100.0}],"name":"bose-connect-ios.Controllers"},{"children":[{"children":[{}],"elements":[{},{},{}],"name":"AboutViewController.swift"},{"children":[{}],"elements":[{},{},{}],"name":"TextFilledViewController.swift"}],"elements":[{"denominator":2.0,"name":"Files","numerator":0.0,"ratio":0.0},{"denominator":2.0,"name":"Classes","numerator":0.0,"ratio":0.0},{"denominator":273.0,"name":"Lines","numerator":0.0,"ratio":0.0},{"denominator":0.0,"name":"Conditionals","numerator":0.0,"ratio":100.0}],"name":"bose-connect-ios.Controllers.About"},{"children":[{"children":[{}],"elements":[{},{},{}],"name":"CarouselHelpViewController.swift"},{"children":[{}],"elements":[{},{},{}],"name":"ConnectedDeviceViewController.swift"},{"children":[{}],"elements":[{},{},{}],"name":"ConnectingDeviceViewController.swift"},{"children":[{}],"elements":[{},{},{}],"name":"SearchingViewController.swift"}],"elements":[{"denominator":4.0,"name":"Files","numerator":2.0,"ratio":50.0},{"denominator":4.0,"name":"Classes","numerator":2.0,"ratio":50.0},{"denominator":977.0,"name":"Lines","numerator":39.0,"ratio":3.9918118},{"denominator":0.0,"name":"Conditionals","numerator":0.0,"ratio":100.0}],"name":"bose-connect-ios.Controllers.CarouselSubControllers"},{"children":[{"children":[{}],"elements":[{},{},{}],"name":"FirmwareUpdateAnimationCircle.swift"},{"children":[{}],"elements":[{},{},{}],"name":"FirmwareUpdateController.swift"},{"children":[{}],"elements":[{},{},{}],"name":"ForemanInstructionsViewController.swift"}],"elements":[{"denominator":3.0,"name":"Files","numerator":0.0,"ratio":0.0},{"denominator":3.0,"name":"Classes","numerator":0.0,"ratio":0.0},{"denominator":319.0,"name":"Lines","numerator":0.0,"ratio":0.0},{"denominator":0.0,"name":"Conditionals","numerator":0.0,"ratio":100.0}],"name":"bose-connect-ios.Controllers.FirmwareUpdate"},{"children":[{"children":[{}],"elements":[{},{},{}],"name":"FirmwareUpdateSuccessOverlay.swift"}],"elements":[{"denominator":1.0,"name":"Files","numerator":0.0,"ratio":0.0},{"denominator":1.0,"name":"Classes","numerator":0.0,"ratio":0.0},{"denominator":22.0,"name":"Lines","numerator":0.0,"ratio":0.0},{"denominator":0.0,"name":"Conditionals","numerator":0.0,"ratio":100.0}],"name":"bose-connect-ios.Controllers.FirmwareUpdate.UpdateSuccessOverlay"},{"children":[{"children":[{}],"elements":[{},{},{}],"name":"FullScreenAlertController.swift"}],"elements":[{"denominator":1.0,"name":"Files","numerator":0.0,"ratio":0.0},{"denominator":1.0,"name":"Classes","numerator":0.0,"ratio":0.0},{"denominator":67.0,"name":"Lines","numerator":0.0,"ratio":0.0},{"denominator":0.0,"name":"Conditionals","numerator":0.0,"ratio":100.0}],"name":"bose-connect-ios.Controllers.FullScreenAlert"},{"children":[{"children":[{}],"elements":[{},{},{}],"name":"HeartRateInformationViewController.swift"}],"elements":[{"denominator":1.0,"name":"Files","numerator":0.0,"ratio":0.0},{"denominator":1.0,"name":"Classes","numerator":0.0,"ratio":0.0},{"denominator":222.0,"name":"Lines","numerator":0.0,"ratio":0.0},{"denominator":0.0,"name":"Conditionals","numerator":0.0,"ratio":100.0}],"name":"bose-connect-ios.Controllers.HeartRate"},{"children":[{"children":[{}],"elements":[{},{},{}],"name":"HelpWebviewController.swift"}],"elements":[{"denominator":1.0,"name":"Files","numerator":0.0,"ratio":0.0},{"denominator":1.0,"name":"Classes","numerator":0.0,"ratio":0.0},{"denominator":50.0,"name":"Lines","numerator":0.0,"ratio":0.0},{"denominator":0.0,"name":"Conditionals","numerator":0.0,"ratio":100.0}],"name":"bose-connect-ios.Controllers.Help"},{"children":[{"children":[{}],"elements":[{},{},{}],"name":"MusicShareCarouselViewController.swift"},{"children":[{}],"elements":[{},{},{}],"name":"MusicShareConnectingDeviceViewController.swift"},{"children":[{}],"elements":[{},{},{}],"name":"MusicShareHistoryViewController.swift"},{"children":[{}],"elements":[{},{},{}],"name":"MusicShareOnboardingViewController.swift"}],"elements":[{"denominator":4.0,"name":"Files","numerator":0.0,"ratio":0.0},{"denominator":4.0,"name":"Classes","numerator":0.0,"ratio":0.0},{"denominator":698.0,"name":"Lines","numerator":0.0,"ratio":0.0},{"denominator":0.0,"name":"Conditionals","numerator":0.0,"ratio":100.0}],"name":"bose-connect-ios.Controllers.MusicShare"},{"children":[{"children":[{}],"elements":[{},{},{}],"name":"NowPlayingBar.swift"},{"children":[{}],"elements":[{},{},{}],"name":"NowPlayingModel.swift"},{"children":[{}],"elements":[{},{},{}],"name":"NowPlayingViewController.swift"},{"children":[{}],"elements":[{},{},{}],"name":"PlayPauseButton.swift"}],"elements":[{"denominator":4.0,"name":"Files","numerator":4.0,"ratio":100.0},{"denominator":4.0,"name":"Classes","numerator":4.0,"ratio":100.0},{"denominator":901.0,"name":"Lines","numerator":405.0,"ratio":44.950054},{"denominator":0.0,"name":"Conditionals","numerator":0.0,"ratio":100.0}],"name":"bose-connect-ios.Controllers.NowPlaying"},{"children":[{"children":[{}],"elements":[{},{},{}],"name":"PairingModeViewController.swift"}],"elements":[{"denominator":1.0,"name":"Files","numerator":0.0,"ratio":0.0},{"denominator":1.0,"name":"Classes","numerator":0.0,"ratio":0.0},{"denominator":111.0,"name":"Lines","numerator":0.0,"ratio":0.0},{"denominator":0.0,"name":"Conditionals","numerator":0.0,"ratio":100.0}],"name":"bose-connect-ios.Controllers.PairingMode"},{"children":[{"children":[{}],"elements":[{},{},{}],"name":"ManageDevicesViewController.swift"},{"children":[{}],"elements":[{},{},{}],"name":"ProductDetailsViewController.swift"},{"children":[{}],"elements":[{},{},{}],"name":"RenameHeadsetViewController.swift"},{"children":[{}],"elements":[{},{},{}],"name":"SettingsBaseController.swift"},{"children":[{}],"elements":[{},{},{}],"name":"SettingsViewController.swift"},{"children":[{}],"elements":[{},{},{}],"name":"SleepTimerViewController.swift"},{"children":[{}],"elements":[{},{},{}],"name":"VoiceLanguageViewController.swift"}],"elements":[{"denominator":7.0,"name":"Files","numerator":0.0,"ratio":0.0},{"denominator":7.0,"name":"Classes","numerator":0.0,"ratio":0.0},{"denominator":1163.0,"name":"Lines","numerator":0.0,"ratio":0.0},{"denominator":0.0,"name":"Conditionals","numerator":0.0,"ratio":100.0}],"name":"bose-connect-ios.Controllers.Settings"},{"children":[{"children":[{}],"elements":[{},{},{}],"name":"VideoCompletionController.swift"},{"children":[{}],"elements":[{},{},{}],"name":"VideoPlayerController.swift"}],"elements":[{"denominator":2.0,"name":"Files","numerator":2.0,"ratio":100.0},{"denominator":2.0,"name":"Classes","numerator":2.0,"ratio":100.0},{"denominator":425.0,"name":"Lines","numerator":19.0,"ratio":4.470588},{"denominator":0.0,"name":"Conditionals","numerator":0.0,"ratio":100.0}],"name":"bose-connect-ios.Controllers.VideoPlayer"},{"children":[{"children":[{}],"elements":[{},{},{}],"name":"AllUpdatesReceiverType.swift"},{"children":[{}],"elements":[{},{},{}],"name":"AnyTrue.swift"},{"children":[{}],"elements":[{},{},{}],"name":"Array+Extensions.swift"},{"children":[{}],"elements":[{},{},{}],"name":"BMAP+Extensions.swift"},{"children":[{}],"elements":[{},{},{}],"name":"Eat.swift"},{"children":[{}],"elements":[{},{},{}],"name":"Event.swift"},{"children":[{}],"elements":[{},{},{}],"name":"Geometry+Extensions.swift"},{"children":[{}],"elements":[{},{},{}],"name":"Log.swift"},{"children":[{}],"elements":[{},{},{}],"name":"NSBundle+Extensions.swift"},{"children":[{}],"elements":[{},{},{}],"name":"NSDate+Extensions.swift"},{"children":[{}],"elements":[{},{},{}],"name":"NSMutableAttributedString+Extensions.swift"},{"children":[{}],"elements":[{},{},{}],"name":"String+Extensions.swift"},{"children":[{}],"elements":[{},{},{}],"name":"Theme.swift"},{"children":[{}],"elements":[{},{},{}],"name":"UIButton+Extensions.swift"},{"children":[{}],"elements":[{},{},{}],"name":"UILabel+Extensions.swift"},{"children":[{}],"elements":[{},{},{}],"name":"UILocalNotification+Extensions.swift"},{"children":[{}],"elements":[{},{},{}],"name":"UINavigationController+Extensions.swift"},{"children":[{}],"elements":[{},{},{}],"name":"UITableView+Extensions.swift"},{"children":[{}],"elements":[{},{},{}],"name":"UIView+Extensions.swift"},{"children":[{}],"elements":[{},{},{}],"name":"UIViewController+Extensions.swift"}],"elements":[{"denominator":20.0,"name":"Files","numerator":11.0,"ratio":55.0},{"denominator":20.0,"name":"Classes","numerator":11.0,"ratio":55.0},{"denominator":991.0,"name":"Lines","numerator":463.0,"ratio":46.720486},{"denominator":0.0,"name":"Conditionals","numerator":0.0,"ratio":100.0}],"name":"bose-connect-ios.Extensions"},{"children":[{"children":[{}],"elements":[{},{},{}],"name":"Crashlytics+Extensions.swift"},{"children":[{}],"elements":[{},{},{}],"name":"CrashlyticsBridge.m"}],"elements":[{"denominator":2.0,"name":"Files","numerator":1.0,"ratio":50.0},{"denominator":2.0,"name":"Classes","numerator":1.0,"ratio":50.0},{"denominator":32.0,"name":"Lines","numerator":5.0,"ratio":15.625},{"denominator":0.0,"name":"Conditionals","numerator":0.0,"ratio":100.0}],"name":"bose-connect-ios.Extensions.CrashlyticsLog"},{"children":[{"children":[{}],"elements":[{},{},{}],"name":"Mocks.swift"}],"elements":[{"denominator":1.0,"name":"Files","numerator":1.0,"ratio":100.0},{"denominator":1.0,"name":"Classes","numerator":1.0,"ratio":100.0},{"denominator":180.0,"name":"Lines","numerator":61.0,"ratio":33.88889},{"denominator":0.0,"name":"Conditionals","numerator":0.0,"ratio":100.0}],"name":"bose-connect-ios.Mocks"},{"children":[{"children":[{}],"elements":[{},{},{}],"name":"Debug.swift"},{"children":[{}],"elements":[{},{},{}],"name":"NameGenerator.swift"}],"elements":[{"denominator":2.0,"name":"Files","numerator":2.0,"ratio":100.0},{"denominator":2.0,"name":"Classes","numerator":2.0,"ratio":100.0},{"denominator":47.0,"name":"Lines","numerator":39.0,"ratio":82.97872},{"denominator":0.0,"name":"Conditionals","numerator":0.0,"ratio":100.0}],"name":"bose-connect-ios.Models"},{"children":[{"children":[{}],"elements":[{},{},{}],"name":"LastConnectedProduct.swift"},{"children":[{}],"elements":[{},{},{}],"name":"NotificationPreferences.swift"},{"children":[{}],"elements":[{},{},{}],"name":"PreviouslyMusicShared.swift"},{"children":[{}],"elements":[{},{},{}],"name":"PreviouslyPairedBoseDevices.swift"}],"elements":[{"denominator":4.0,"name":"Files","numerator":3.0,"ratio":75.0},{"denominator":4.0,"name":"Classes","numerator":3.0,"ratio":75.0},{"denominator":182.0,"name":"Lines","numerator":163.0,"ratio":89.56044},{"denominator":0.0,"name":"Conditionals","numerator":0.0,"ratio":100.0}],"name":"bose-connect-ios.Models.UserDefaults"},{"children":[{"children":[{}],"elements":[{},{},{}],"name":"LevelChangeThrottler.swift"}],"elements":[{"denominator":1.0,"name":"Files","numerator":1.0,"ratio":100.0},{"denominator":1.0,"name":"Classes","numerator":1.0,"ratio":100.0},{"denominator":24.0,"name":"Lines","numerator":4.0,"ratio":16.666666},{"denominator":0.0,"name":"Conditionals","numerator":0.0,"ratio":100.0}],"name":"bose-connect-ios.Utilities"},{"children":[{"children":[{}],"elements":[{},{},{}],"name":"HeartRateInformationViewModel.swift"},{"children":[{}],"elements":[{},{},{}],"name":"MusicShareHistoryViewModel.swift"}],"elements":[{"denominator":2.0,"name":"Files","numerator":2.0,"ratio":100.0},{"denominator":2.0,"name":"Classes","numerator":2.0,"ratio":100.0},{"denominator":45.0,"name":"Lines","numerator":33.0,"ratio":73.333336},{"denominator":0.0,"name":"Conditionals","numerator":0.0,"ratio":100.0}],"name":"bose-connect-ios.ViewModels"},{"children":[{"children":[{}],"elements":[{},{},{}],"name":"BoseDeviceViewModel.swift"},{"children":[{}],"elements":[{},{},{}],"name":"CarouselViewModel.swift"},{"children":[{}],"elements":[{},{},{}],"name":"MusicShareBoseDeviceViewModel.swift"},{"children":[{}],"elements":[{},{},{}],"name":"MusicShareCarouselViewModel.swift"},{"children":[{}],"elements":[{},{},{}],"name":"MusicShareOnboardingViewModel.swift"}],"elements":[{"denominator":5.0,"name":"Files","numerator":5.0,"ratio":100.0},{"denominator":5.0,"name":"Classes","numerator":5.0,"ratio":100.0},{"denominator":1246.0,"name":"Lines","numerator":506.0,"ratio":40.60995},{"denominator":0.0,"name":"Conditionals","numerator":0.0,"ratio":100.0}],"name":"bose-connect-ios.ViewModels.Carousel"},{"children":[{"children":[{}],"elements":[{},{},{}],"name":"AboutTableViewCell.swift"},{"children":[{}],"elements":[{},{},{}],"name":"BatteryView.swift"},{"children":[{}],"elements":[{},{},{}],"name":"BoseBMAPVolumeSlider.swift"},{"children":[{}],"elements":[{},{},{}],"name":"BoseNativeVolumeSlider.swift"},{"children":[{}],"elements":[{},{},{}],"name":"ButtonFooter.swift"},{"children":[{}],"elements":[{},{},{}],"name":"ConnectingArrowsView.swift"},{"children":[{}],"elements":[{},{},{}],"name":"DeviceControlsView.swift"},{"children":[{}],"elements":[{},{},{}],"name":"EAPInstructionView.swift"},{"children":[{}],"elements":[{},{},{}],"name":"FirmwareStatusHeaderView.swift"},{"children":[{}],"elements":[{},{},{}],"name":"FirmwareTransferProgressView.swift"},{"children":[{}],"elements":[{},{},{}],"name":"IndependentButton.swift"},{"children":[{}],"elements":[{},{},{}],"name":"IndependentSwitch.swift"},{"children":[{}],"elements":[{},{},{}],"name":"MarginLabel.swift"},{"children":[{}],"elements":[{},{},{}],"name":"MusicShareHistoryTableViewCell.swift"},{"children":[{}],"elements":[{},{},{}],"name":"PagedCarouselView.swift"},{"children":[{}],"elements":[{},{},{}],"name":"PulsingView.swift"},{"children":[{}],"elements":[{},{},{}],"name":"RadioButton.swift"},{"children":[{}],"elements":[{},{},{}],"name":"RemoveButton.swift"},{"children":[{}],"elements":[{},{},{}],"name":"RoundedSolidButton.swift"},{"children":[{}],"elements":[{},{},{}],"name":"SettingsTableViewCell.swift"},{"children":[{}],"elements":[{},{},{}],"name":"SlideUpOverlay.swift"},{"children":[{}],"elements":[{},{},{}],"name":"TappablePuppetMusicShareDeviceView.swift"},{"children":[{}],"elements":[{},{},{}],"name":"Tooltip.swift"},{"children":[{}],"elements":[{},{},{}],"name":"VerticalPanGestureRecognizer.swift"}],"elements":[{"denominator":24.0,"name":"Files","numerator":4.0,"ratio":16.666666},{"denominator":24.0,"name":"Classes","numerator":4.0,"ratio":16.666666},{"denominator":1306.0,"name":"Lines","numerator":148.0,"ratio":11.332313},{"denominator":0.0,"name":"Conditionals","numerator":0.0,"ratio":100.0}],"name":"bose-connect-ios.Views"},{"children":[{"children":[{}],"elements":[{},{},{}],"name":"CNCSliderDeviceContentView.swift"},{"children":[{}],"elements":[{},{},{}],"name":"CNCTooltipView.swift"},{"children":[{}],"elements":[{},{},{}],"name":"CNCViewController.swift"},{"children":[{}],"elements":[{},{},{}],"name":"CircularSlider.swift"},{"children":[{}],"elements":[{},{},{}],"name":"CircularSliderView.swift"}],"elements":[{"denominator":5.0,"name":"Files","numerator":0.0,"ratio":0.0},{"denominator":5.0,"name":"Classes","numerator":0.0,"ratio":0.0},{"denominator":624.0,"name":"Lines","numerator":0.0,"ratio":0.0},{"denominator":0.0,"name":"Conditionals","numerator":0.0,"ratio":100.0}],"name":"bose-connect-ios.Views.CNC"},{"children":[{"children":[{}],"elements":[{},{},{}],"name":"EarbudInstructionCell.swift"},{"children":[{}],"elements":[{},{},{}],"name":"HeartRateAcquiredView.swift"},{"children":[{}],"elements":[{},{},{}],"name":"HeartRateEarbudView.swift"},{"children":[{}],"elements":[{},{},{}],"name":"HeartRateViewController.swift"},{"children":[{}],"elements":[{},{},{}],"name":"VectorLoadingView.swift"}],"elements":[{"denominator":5.0,"name":"Files","numerator":0.0,"ratio":0.0},{"denominator":5.0,"name":"Classes","numerator":0.0,"ratio":0.0},{"denominator":354.0,"name":"Lines","numerator":0.0,"ratio":0.0},{"denominator":0.0,"name":"Conditionals","numerator":0.0,"ratio":100.0}],"name":"bose-connect-ios.Views.HeartRate"},{"children":[{"children":[{}],"elements":[{},{},{}],"name":"MusicShareOboardingOverviewContentView.swift"},{"children":[{}],"elements":[{},{},{}],"name":"MusicShareOnboardGetStartedContentView.swift"}],"elements":[{"denominator":2.0,"name":"Files","numerator":1.0,"ratio":50.0},{"denominator":2.0,"name":"Classes","numerator":1.0,"ratio":50.0},{"denominator":88.0,"name":"Lines","numerator":30.0,"ratio":34.090908},{"denominator":0.0,"name":"Conditionals","numerator":0.0,"ratio":100.0}],"name":"bose-connect-ios.Views.MusicShareOnboarding"}],"elements":[{"denominator":27.0,"name":"Packages","numerator":15.0,"ratio":55.555557},{"denominator":111.0,"name":"Files","numerator":44.0,"ratio":39.63964},{"denominator":111.0,"name":"Classes","numerator":44.0,"ratio":39.63964},{"denominator":11481.0,"name":"Lines","numerator":2362.0,"ratio":20.57312},{"denominator":0.0,"name":"Conditionals","numerator":0.0,"ratio":100.0}],"name":"Cobertura Coverage Report"}} diff --git a/JenkinsTests/JenkinsTests.swift b/JenkinsTests/JenkinsTests.swift new file mode 100644 index 0000000..bc19845 --- /dev/null +++ b/JenkinsTests/JenkinsTests.swift @@ -0,0 +1,13 @@ +// +// JenkinsTests.swift +// JenkinsTests +// +// Created by Patrick Butkiewicz on 11/13/16. +// +// + +import XCTest + +class JenkinsTests: XCTestCase { + +} diff --git a/Tests/Jenkins/JenkinsTests.swift b/Tests/Jenkins/JenkinsTests.swift deleted file mode 100644 index 2326ff4..0000000 --- a/Tests/Jenkins/JenkinsTests.swift +++ /dev/null @@ -1,17 +0,0 @@ -import XCTest -@testable import Jenkins - -class JenkinsTests: XCTestCase { - func testExample() { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. - XCTAssertEqual(Jenkins().text, "Hello, World!") - } - - - static var allTests : [(String, (JenkinsTests) -> () throws -> Void)] { - return [ - ("testExample", testExample), - ] - } -} diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift deleted file mode 100644 index 27aa45f..0000000 --- a/Tests/LinuxMain.swift +++ /dev/null @@ -1,6 +0,0 @@ -import XCTest -@testable import JenkinsTestSuite - -XCTMain([ - testCase(JenkinsTests.allTests), -]) From f2dd3f109639f769bb99a1f7987cee8aee605f1f Mon Sep 17 00:00:00 2001 From: Patrick Butkiewicz Date: Sun, 13 Nov 2016 20:48:08 -0500 Subject: [PATCH 3/5] Change code coverage to only support cobertura for now --- ...sts.swift => CoberturaCoverageTests.swift} | 16 ++- Sources/CoberturaCoverage.swift | 116 +++++++++++++++ Sources/Coverage.swift | 133 ------------------ 3 files changed, 125 insertions(+), 140 deletions(-) rename JenkinsTests/{CoverageTests.swift => CoberturaCoverageTests.swift} (79%) create mode 100644 Sources/CoberturaCoverage.swift delete mode 100644 Sources/Coverage.swift diff --git a/JenkinsTests/CoverageTests.swift b/JenkinsTests/CoberturaCoverageTests.swift similarity index 79% rename from JenkinsTests/CoverageTests.swift rename to JenkinsTests/CoberturaCoverageTests.swift index 528498a..ea00eba 100644 --- a/JenkinsTests/CoverageTests.swift +++ b/JenkinsTests/CoberturaCoverageTests.swift @@ -1,5 +1,5 @@ // -// CoverageTests.swift +// CoberturaCoverageTests.swift // Jenkins // // Created by Patrick Butkiewicz on 11/13/16. @@ -10,7 +10,7 @@ import Foundation import XCTest @testable import Jenkins -class CoverageTests: XCTestCase { +class CoberturaCoverageTests: XCTestCase { func testCoberturaCodeCoverageDepth2() { @@ -25,7 +25,7 @@ class CoverageTests: XCTestCase { return XCTFail("Failed mapping Coverage Report JSON") } - let c = CodeCoverageReport(json: json["results"] as! JSON) + let c = CoberturaCodeCoverageReport(json: json["results"] as! JSON) let validChildren = 0 XCTAssert(c.childReports.count == validChildren, "Report has \(c.childReports.count) children, but should have \(validChildren)") @@ -37,7 +37,8 @@ class CoverageTests: XCTestCase { XCTAssert(c.coverageElements.count == validCovElements, "Report has \(c.coverageElements.count) coverage elements but should have \(validCovElements)") let validLineRatio = 0.2 - XCTAssert(c.lineRatio() == validLineRatio, "Ratio is \(c.lineRatio()) but should be \(validLineRatio)") + let actualLineRatio = c.ratio(of: .Lines) + XCTAssertEqualWithAccuracy(actualLineRatio, validLineRatio, accuracy: 0.05, "Ratio is \(actualLineRatio) but should be \(validLineRatio)") } func testCoberturaCodeCoverageDepth3() { @@ -52,7 +53,7 @@ class CoverageTests: XCTestCase { return XCTFail("Failed mapping Coverage Report JSON") } - let c = CodeCoverageReport(json: json["results"] as! JSON) + let c = CoberturaCodeCoverageReport(json: json["results"] as! JSON) let validChildren = 27 XCTAssert(c.childReports.count == validChildren, "Report has \(c.childReports.count) children, but should have \(validChildren)") @@ -63,8 +64,9 @@ class CoverageTests: XCTestCase { let validCovElements = 5 XCTAssert(c.coverageElements.count == validCovElements, "Report has \(c.coverageElements.count) coverage elements but should have \(validCovElements)") - let validLineRatio = 0.20 - XCTAssertEqualWithAccuracy(c.lineRatio(), validLineRatio, accuracy: 0.05, "Ratio is \(c.lineRatio()) but should be \(validLineRatio)") + let validLineRatio = 0.2 + let actualLineRatio = c.ratio(of: .Lines) + XCTAssertEqualWithAccuracy(actualLineRatio, validLineRatio, accuracy: 0.05, "Ratio is \(actualLineRatio) but should be \(validLineRatio)") } } diff --git a/Sources/CoberturaCoverage.swift b/Sources/CoberturaCoverage.swift new file mode 100644 index 0000000..bbec5af --- /dev/null +++ b/Sources/CoberturaCoverage.swift @@ -0,0 +1,116 @@ +// +// Coverage.swift +// Jenkins +// +// Created by Patrick Butkiewicz on 11/13/16. +// +// + +import Foundation + +public enum CoberturaCodeCoverageElementType: String { + case Classes = "Classes" + case Conditionals = "Conditionals" + case Files = "Files" + case Lines = "Lines" + case Packages = "Packages" + case Unknown = "Unknown" + + init(_ rawValue: String) { + switch rawValue { + case "Classes": self = .Classes + case "Conditionals": self = .Conditionals + case "Files": self = .Files + case "Lines": self = .Lines + case "Packages": self = .Packages + default: self = .Unknown + } + } +} + +public struct CoberturaCodeCoverageElement { + private(set) var elementType: CoberturaCodeCoverageElementType = .Unknown + private(set) var covered: Int = 0 + private(set) var total: Int = 0 + + init(json: JSON) { + let coverageElementName: String = json["name"] as? String ?? "" + elementType = CoberturaCodeCoverageElementType(coverageElementName) + covered = json["numerator"] as? Int ?? 0 + total = json["denominator"] as? Int ?? 0 + } + + func ratio() -> Double { + return Double(covered) / Double(total) + } +} + +public struct CoberturaCodeCoverageReport { + private(set) var name: String + private(set) var childReports: [CoberturaCodeCoverageReport] + private(set) var coverageElements: [CoberturaCodeCoverageElement] + + init(json: JSON) { + name = json["name"] as? String ?? "" + + // for each child, init self + let childrenJSON: [JSON] = json["children"] as? [JSON] ?? [] + childReports = childrenJSON + .filter({ child in + let elements = child["elements"] as? [JSON] ?? [] + return elements.filter({ $0.count > 0 }).count > 0 + }) + .map({ + CoberturaCodeCoverageReport(json: $0) + }) + + // for each element, init element + let elementJSON: [JSON] = json["elements"] as? [JSON] ?? [] + coverageElements = elementJSON.map({ CoberturaCodeCoverageElement(json: $0) }) + } + + func ratio(of element: CoberturaCodeCoverageElementType) -> Double { + return coverageElements.filter({$0.elementType == element}).first?.ratio() ?? 0 + } +} + +/* + * Jenkins Cobertura Extension + */ + +extension Jenkins { + func coberturaCoverage(_ job: String, + build: Int = 0, + depth: Int = 2, + handler: @escaping (_ coverageReport: CoberturaCodeCoverageReport?) -> Void) + { + let buildPath = (build == 0) ? "lastSuccessfulBuild" : String(build) + + guard let url: URL = URL(string: jobURL)? + .appendingPathComponent(job) + .appendingPathComponent(buildPath) + .appendingPathComponent("cobertura") + .appendingPathComponent("api") + .appendingPathComponent("json") else { + return handler(nil) + } + + let parameters: [String : AnyObject] = ["depth" : depth as AnyObject] + client?.get(path: url, params: parameters) { response, error in + guard let json = response as? JSON, + let results = json["results"] as? JSON else { + return handler(nil) + } + + handler(CoberturaCodeCoverageReport(json: results)) + } + } + + func coberturaCoverage(_ job: Job, + build: Int = 0, + depth: Int = 2, + handler: @escaping (_ coverageReport: CoberturaCodeCoverageReport?) -> Void) + { + coberturaCoverage(job.name, build: build, depth: depth, handler: handler) + } +} diff --git a/Sources/Coverage.swift b/Sources/Coverage.swift deleted file mode 100644 index c118e44..0000000 --- a/Sources/Coverage.swift +++ /dev/null @@ -1,133 +0,0 @@ -// -// Coverage.swift -// Jenkins -// -// Created by Patrick Butkiewicz on 11/13/16. -// -// - -import Foundation - -public enum CodeCoverageProvider { - case Cobertura - case Jacoco - case Other(path: String) - - func path() -> String { - switch self { - case .Cobertura: return "Cobertura" - case .Jacoco: return "jacaco" - case .Other(let path): return path - } - } -} - -public enum CodeCoverageElementType: String { - case Classes = "Classes" - case Conditionals = "Conditionals" - case Files = "Files" - case Lines = "Lines" - case Packages = "Packages" - case Unknown = "Unknown" - - init(_ rawValue: String) { - switch rawValue { - case "Classes": self = .Classes - case "Conditionals": self = .Conditionals - case "Files": self = .Files - case "Lines": self = .Lines - case "Packages": self = .Packages - default: self = .Unknown - } - } -} - -public struct CodeCoverageElement { - private(set) var elementType: CodeCoverageElementType = .Unknown - private(set) var numerator: Int = 0 - private(set) var denominator: Int = 0 - - init(json: JSON) { - let coverageElementName: String = json["name"] as? String ?? "" - elementType = CodeCoverageElementType(coverageElementName) - numerator = json["numerator"] as? Int ?? 0 - denominator = json["denominator"] as? Int ?? 0 - } - - func ratio() -> Double { - return Double(numerator) / Double(denominator) - } -} - -public struct CodeCoverageReport { - private(set) var name: String - private(set) var childReports: [CodeCoverageReport] - private(set) var coverageElements: [CodeCoverageElement] - - init(json: JSON) { - name = json["name"] as? String ?? "" - - // for each child, init self - let childrenJSON: [JSON] = json["children"] as? [JSON] ?? [] - childReports = childrenJSON - .filter({ child in - let elements = child["elements"] as? [JSON] ?? [] - return elements.filter({ $0.count > 0 }).count > 0 - }) - .map({ - CodeCoverageReport(json: $0) - }) - - // for each element, init element - let elementJSON: [JSON] = json["elements"] as? [JSON] ?? [] - coverageElements = elementJSON.map({ CodeCoverageElement(json: $0) }) - } - - func lineRatio() -> Double { - return coverageElements.filter({$0.elementType == .Lines}).first?.ratio() ?? 0 - } -} - -/* - * Jenkins Extension - */ - -extension Jenkins { - func codeCoverage(_ job: String, - build: Int = 0, - depth: Int = 2, - provider: CodeCoverageProvider = .Cobertura, - handler: @escaping (_ coverageReport: CodeCoverageReport?) -> Void) - { - let buildPath = (build == 0) ? "lastSuccessfulBuild" : "\(build)" - guard let url = URL(string: jobURL)? - .appendingPathComponent(job) - .appendingPathComponent(buildPath) - .appendingPathComponent(provider.path()) - .appendingPathComponent("api") - .appendingPathComponent("json") else { - return handler(nil) - } - - client?.get(path: url) { response, error in - guard let json = response as? JSON else { - return handler(nil) - } - - guard let results = json["results"] as? JSON else { - return handler(nil) - } - - handler(CodeCoverageReport(json: results)) - } - } - - func codeCoverage(_ job: Job, - build: Int = 0, - depth: Int = 2, - provider: CodeCoverageProvider = .Cobertura, - handler: @escaping (_ coverageReport: CodeCoverageReport?) -> Void) - { - codeCoverage(job.name, build: build, depth: depth, provider: provider, handler: handler) - } -} From b9ee02141b5636c0305ccbbb65c5ecfc9242ef85 Mon Sep 17 00:00:00 2001 From: Patrick Butkiewicz Date: Sun, 13 Nov 2016 22:58:13 -0500 Subject: [PATCH 4/5] Create CoverageReport protocols and Jacoco Coverage model. Create associated tests. --- JenkinsTests/CoberturaCoverageTests.swift | 12 +- ...son => CoberturaCoverageReportDepth2.json} | 0 ...son => CoberturaCoverageReportDepth3.json} | 0 .../JSON/JacocoCoverageReportDepth.json | 1 + JenkinsTests/JacocoCoverageTests.swift | 53 +++++++++ Sources/CoberturaCoverage.swift | 44 +++---- Sources/Coverage.swift | 38 +++++++ Sources/JacacoCoverage.swift | 107 ++++++++++++++++++ 8 files changed, 227 insertions(+), 28 deletions(-) rename JenkinsTests/JSON/{CoverageReportDepth2.json => CoberturaCoverageReportDepth2.json} (100%) rename JenkinsTests/JSON/{CoverageReportDepth3.json => CoberturaCoverageReportDepth3.json} (100%) create mode 100644 JenkinsTests/JSON/JacocoCoverageReportDepth.json create mode 100644 JenkinsTests/JacocoCoverageTests.swift create mode 100644 Sources/Coverage.swift create mode 100644 Sources/JacacoCoverage.swift diff --git a/JenkinsTests/CoberturaCoverageTests.swift b/JenkinsTests/CoberturaCoverageTests.swift index ea00eba..dacbcc9 100644 --- a/JenkinsTests/CoberturaCoverageTests.swift +++ b/JenkinsTests/CoberturaCoverageTests.swift @@ -14,7 +14,7 @@ class CoberturaCoverageTests: XCTestCase { func testCoberturaCodeCoverageDepth2() { - guard let path = Bundle(for: type(of: self)).path(forResource: "CoverageReportDepth2", ofType: "json") else { + guard let path = Bundle(for: type(of: self)).path(forResource: "CoberturaCoverageReportDepth2", ofType: "json") else { return XCTFail("Missing Coverage Report JSON") } @@ -37,12 +37,12 @@ class CoberturaCoverageTests: XCTestCase { XCTAssert(c.coverageElements.count == validCovElements, "Report has \(c.coverageElements.count) coverage elements but should have \(validCovElements)") let validLineRatio = 0.2 - let actualLineRatio = c.ratio(of: .Lines) - XCTAssertEqualWithAccuracy(actualLineRatio, validLineRatio, accuracy: 0.05, "Ratio is \(actualLineRatio) but should be \(validLineRatio)") + let actualLineRatio = c.ratio(of: CoberturaCodeCoverageElementType.Lines) + XCTAssertEqualWithAccuracy(actualLineRatio, validLineRatio, accuracy: 0.05) } func testCoberturaCodeCoverageDepth3() { - guard let path = Bundle(for: type(of: self)).path(forResource: "CoverageReportDepth3", ofType: "json") else { + guard let path = Bundle(for: type(of: self)).path(forResource: "CoberturaCoverageReportDepth3", ofType: "json") else { return XCTFail("Missing Coverage Report JSON") } @@ -65,8 +65,8 @@ class CoberturaCoverageTests: XCTestCase { XCTAssert(c.coverageElements.count == validCovElements, "Report has \(c.coverageElements.count) coverage elements but should have \(validCovElements)") let validLineRatio = 0.2 - let actualLineRatio = c.ratio(of: .Lines) - XCTAssertEqualWithAccuracy(actualLineRatio, validLineRatio, accuracy: 0.05, "Ratio is \(actualLineRatio) but should be \(validLineRatio)") + let actualLineRatio = c.ratio(of: CoberturaCodeCoverageElementType.Lines) + XCTAssertEqualWithAccuracy(actualLineRatio, validLineRatio, accuracy: 0.05) } } diff --git a/JenkinsTests/JSON/CoverageReportDepth2.json b/JenkinsTests/JSON/CoberturaCoverageReportDepth2.json similarity index 100% rename from JenkinsTests/JSON/CoverageReportDepth2.json rename to JenkinsTests/JSON/CoberturaCoverageReportDepth2.json diff --git a/JenkinsTests/JSON/CoverageReportDepth3.json b/JenkinsTests/JSON/CoberturaCoverageReportDepth3.json similarity index 100% rename from JenkinsTests/JSON/CoverageReportDepth3.json rename to JenkinsTests/JSON/CoberturaCoverageReportDepth3.json diff --git a/JenkinsTests/JSON/JacocoCoverageReportDepth.json b/JenkinsTests/JSON/JacocoCoverageReportDepth.json new file mode 100644 index 0000000..716b2d6 --- /dev/null +++ b/JenkinsTests/JSON/JacocoCoverageReportDepth.json @@ -0,0 +1 @@ +{"_class":"hudson.plugins.jacoco.report.CoverageReport","branchCoverage":{"covered":731,"missed":454,"percentage":62,"percentageFloat":61.687763,"total":1185},"classCoverage":{"covered":61,"missed":23,"percentage":73,"percentageFloat":72.61904,"total":84},"complexityScore":{"covered":854,"missed":578,"percentage":60,"percentageFloat":59.63687,"total":1432},"instructionCoverage":{"covered":8244,"missed":2896,"percentage":74,"percentageFloat":74.003586,"total":11140},"lineCoverage":{"covered":2107,"missed":764,"percentage":73,"percentageFloat":73.38907,"total":2871},"methodCoverage":{"covered":567,"missed":247,"percentage":70,"percentageFloat":69.65602,"total":814},"previousResult":{"_class":"hudson.plugins.jacoco.report.CoverageReport"}} diff --git a/JenkinsTests/JacocoCoverageTests.swift b/JenkinsTests/JacocoCoverageTests.swift new file mode 100644 index 0000000..f03ac44 --- /dev/null +++ b/JenkinsTests/JacocoCoverageTests.swift @@ -0,0 +1,53 @@ +// +// JacocoCoverageTests.swift +// Jenkins +// +// Created by Patrick Butkiewicz on 11/13/16. +// +// + +import Foundation +import XCTest +@testable import Jenkins + +class JacocoCoverageTests: XCTestCase { + + func testJacocoCodeCoverageDepth2() { + guard let path = Bundle(for: type(of: self)).path(forResource: "JacocoCoverageReportDepth", ofType: "json") else { + return XCTFail("Missing Jacoco Coverage Report JSON") + } + + guard + let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .alwaysMapped), + let json: JSON = try! JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.allowFragments) as? JSON + else { + return XCTFail("Failed mapping Coverage Report JSON") + } + + let coverage = JacocoCodeCoverageReport(json: json) + + let validBranchRatio = 0.61 + let actualBranchRatio = coverage.ratio(of: JacocoCodeCoverageElementType.BranchCoverage) + XCTAssertEqualWithAccuracy(actualBranchRatio, validBranchRatio, accuracy: 0.05) + + let validClassRatio = 0.72 + let actualClassRatio = coverage.ratio(of: JacocoCodeCoverageElementType.ClassCoverage) + XCTAssertEqualWithAccuracy(actualClassRatio, validClassRatio, accuracy: 0.05) + + let validComplexityRatio = 0.59 + let actualComplexityRatio = coverage.ratio(of: JacocoCodeCoverageElementType.ComplexityCoverage) + XCTAssertEqualWithAccuracy(actualComplexityRatio, validComplexityRatio, accuracy: 0.05) + + let validInstructionRatio = 0.74 + let actualInstructionRatio = coverage.ratio(of: JacocoCodeCoverageElementType.InstructionCoverage) + XCTAssertEqualWithAccuracy(actualInstructionRatio, validInstructionRatio, accuracy: 0.05) + + let validLineRatio = 0.73 + let actualLineRatio = coverage.ratio(of: JacocoCodeCoverageElementType.LineCoverage) + XCTAssertEqualWithAccuracy(actualLineRatio, validLineRatio, accuracy: 0.05) + + let validMethodRatio = 0.69 + let actualMethodRatio = coverage.ratio(of: JacocoCodeCoverageElementType.MethodCoverage) + XCTAssertEqualWithAccuracy(actualMethodRatio, validMethodRatio, accuracy: 0.05) + } +} diff --git a/Sources/CoberturaCoverage.swift b/Sources/CoberturaCoverage.swift index bbec5af..f2414c4 100644 --- a/Sources/CoberturaCoverage.swift +++ b/Sources/CoberturaCoverage.swift @@ -8,7 +8,7 @@ import Foundation -public enum CoberturaCodeCoverageElementType: String { +public enum CoberturaCodeCoverageElementType: String, CoverageElementType { case Classes = "Classes" case Conditionals = "Conditionals" case Files = "Files" @@ -28,24 +28,13 @@ public enum CoberturaCodeCoverageElementType: String { } } -public struct CoberturaCodeCoverageElement { - private(set) var elementType: CoberturaCodeCoverageElementType = .Unknown - private(set) var covered: Int = 0 - private(set) var total: Int = 0 - - init(json: JSON) { - let coverageElementName: String = json["name"] as? String ?? "" - elementType = CoberturaCodeCoverageElementType(coverageElementName) - covered = json["numerator"] as? Int ?? 0 - total = json["denominator"] as? Int ?? 0 - } - - func ratio() -> Double { - return Double(covered) / Double(total) - } +public struct CoberturaCodeCoverageElement: CoverageElement { + public var elementType: CoverageElementType = CoberturaCodeCoverageElementType.Unknown + public var covered: Int = 0 + public var total: Int = 0 } -public struct CoberturaCodeCoverageReport { +public struct CoberturaCodeCoverageReport: CoverageReport { private(set) var name: String private(set) var childReports: [CoberturaCodeCoverageReport] private(set) var coverageElements: [CoberturaCodeCoverageElement] @@ -66,11 +55,22 @@ public struct CoberturaCodeCoverageReport { // for each element, init element let elementJSON: [JSON] = json["elements"] as? [JSON] ?? [] - coverageElements = elementJSON.map({ CoberturaCodeCoverageElement(json: $0) }) + coverageElements = elementJSON.map({ json in + let coverageElementName: String = json["name"] as? String ?? "" + let elementType = CoberturaCodeCoverageElementType(coverageElementName) + let covered = json["numerator"] as? Int ?? 0 + let total = json["denominator"] as? Int ?? 0 + return CoberturaCodeCoverageElement(elementType: elementType, covered: covered, total: total) + }) } - func ratio(of element: CoberturaCodeCoverageElementType) -> Double { - return coverageElements.filter({$0.elementType == element}).first?.ratio() ?? 0 + public func ratio(of element: CoverageElementType) -> Double { + if let e = element as? CoberturaCodeCoverageElementType { + return coverageElements.filter({ + return ($0.elementType as? CoberturaCodeCoverageElementType) == e + }).first?.ratio() ?? 0 + } + return 0 } } @@ -79,7 +79,7 @@ public struct CoberturaCodeCoverageReport { */ extension Jenkins { - func coberturaCoverage(_ job: String, + public func coberturaCoverage(_ job: String, build: Int = 0, depth: Int = 2, handler: @escaping (_ coverageReport: CoberturaCodeCoverageReport?) -> Void) @@ -106,7 +106,7 @@ extension Jenkins { } } - func coberturaCoverage(_ job: Job, + public func coberturaCoverage(_ job: Job, build: Int = 0, depth: Int = 2, handler: @escaping (_ coverageReport: CoberturaCodeCoverageReport?) -> Void) diff --git a/Sources/Coverage.swift b/Sources/Coverage.swift new file mode 100644 index 0000000..2ed73e7 --- /dev/null +++ b/Sources/Coverage.swift @@ -0,0 +1,38 @@ +// +// Coverage.swift +// Jenkins +// +// Created by Patrick Butkiewicz on 11/13/16. +// +// + +import Foundation + +public protocol CoverageReport { + func ratio(of element: CoverageElementType) -> Double +} + +/* + * Coverage Element + */ + +public protocol CoverageElementType {} + +public protocol CoverageElement { + var elementType: CoverageElementType { get } + var covered: Int { get } + var total: Int { get } + + func missed() -> Int + func ratio() -> Double +} + +extension CoverageElement { + public func missed() -> Int { + return max(0, (total - covered)) + } + + public func ratio() -> Double { + return fmax(0, (Double(covered) / Double(total))) + } +} diff --git a/Sources/JacacoCoverage.swift b/Sources/JacacoCoverage.swift new file mode 100644 index 0000000..b818672 --- /dev/null +++ b/Sources/JacacoCoverage.swift @@ -0,0 +1,107 @@ +// +// JacacoCoverage.swift +// Jenkins +// +// Created by Patrick Butkiewicz on 11/13/16. +// +// + +import Foundation + +public enum JacocoCodeCoverageElementType: CoverageElementType { + case BranchCoverage + case ClassCoverage + case ComplexityCoverage + case InstructionCoverage + case LineCoverage + case MethodCoverage + case Unknown + + init(_ rawValue: String) { + switch rawValue { + case "branchCoverage": self = .BranchCoverage + case "classCoverage": self = .ClassCoverage + case "complexityScore": self = .ComplexityCoverage + case "instructionCoverage": self = .InstructionCoverage + case "lineCoverage": self = .LineCoverage + case "methodCoverage": self = .MethodCoverage + default: self = .Unknown + } + } +} + +public struct JacocoCodeCoverageElement: CoverageElement { + public var elementType: CoverageElementType + public var covered: Int + public var total: Int +} + +public struct JacocoCodeCoverageReport { + private(set) var name: String + private(set) var coverageElements: [JacocoCodeCoverageElement] + + init(json: JSON) { + let elementCoveredKey = "covered" + let elementTotalKey = "total" + + name = json["_class"] as? String ?? "Jacoco Report" + var elements: [JacocoCodeCoverageElement] = [] + + for (key, coverage) in json { + let elemType = JacocoCodeCoverageElementType(key) + if elemType != .Unknown { + let covered = coverage[elementCoveredKey] as? Int ?? 0 + let total = coverage[elementTotalKey] as? Int ?? 0 + let elem = JacocoCodeCoverageElement(elementType: elemType, covered: covered, total: total) + elements.append(elem) + } + } + + self.coverageElements = elements + } + + public func ratio(of element: CoverageElementType) -> Double { + if let e = element as? JacocoCodeCoverageElementType { + return coverageElements.filter({$0.elementType as? JacocoCodeCoverageElementType == e}).first?.ratio() ?? 0 + } + return 0 + } +} + +/* + * Jenkins Cobertura Extension + */ + +extension Jenkins { + public func jacocoCoverage(_ job: String, + build: Int = 0, + handler: @escaping (_ coverageReport: JacocoCodeCoverageReport?) -> Void) + { + let buildPath = (build == 0) ? "lastSuccessfulBuild" : String(build) + + guard let url: URL = URL(string: jobURL)? + .appendingPathComponent(job) + .appendingPathComponent(buildPath) + .appendingPathComponent("jacoco") + .appendingPathComponent("api") + .appendingPathComponent("json") else { + return handler(nil) + } + + client?.get(path: url) { response, error in + guard let json = response as? JSON else { + return handler(nil) + } + + handler(JacocoCodeCoverageReport(json: json)) + } + } + + public func jacocoCoverage(_ job: Job, + build: Int = 0, + depth: Int = 0, + handler: @escaping (_ coverageReport: JacocoCodeCoverageReport?) -> Void) + { + jacocoCoverage(job.name, build: build, handler: handler) + } +} From 509c92dee90c4588a3239def5577a5bcb8b1135f Mon Sep 17 00:00:00 2001 From: Patrick Butkiewicz Date: Mon, 14 Nov 2016 02:53:03 -0500 Subject: [PATCH 5/5] Update Readme --- README.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e5f8c83..10ec900 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Jenkins-Swift --- -![Latest](http://img.shields.io/badge/Latest-0.0.3-brightgreen.svg) +![Latest](http://img.shields.io/badge/Latest-0.0.6-brightgreen.svg) ![Swift](http://img.shields.io/badge/swift-3.0-brightgreen.svg) [![Build Status](https://travis-ci.org/IntrepidPursuits/Jenkins-swift.svg?branch=master)](https://travis-ci.org/IntrepidPursuits/Jenkins-swift) @@ -198,6 +198,24 @@ Building a job requires using 1 of 2 methods, depending on whether or not your p print("Building Job With Paramaters: \(parameters)") } +___ +#### Code Coverage + +This client supports retrieving code coverage reports from Jacoco and Cobertura plugins. + + jenkins.coberturaCoverage(project, handler: { report in + if let report = report { + print(report.ratio(of: CoberturaCodeCoverageElementType.Lines)) + } + }) + + jenkins.jacocoCoverage(project, handler: { report in + if let report = report { + report.ratio(of: JacocoCodeCoverageElementType.LineCoverage) + } + }) + + ___ ## Contributing