Skip to content

Commit aef8781

Browse files
authored
Create a swift utility for uploading project health metrics from Travis (#2612)
* Create a swift utility for collecting code coverage and uploading to a database * Update the readme * Code clean up * Fix line length * Change to a still supported commandline kit * Run swift formatter * Add copyright * Move example report under the Tests dir * Add a TODO for Firestore, Functions, and InAppMessaging * Change todo name to GitHub name * Put line breaks in the example report
1 parent bddf94e commit aef8781

File tree

10 files changed

+42364
-0
lines changed

10 files changed

+42364
-0
lines changed

Metrics/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.DS_Store
2+
/.build
3+
/Packages
4+
/*.xcodeproj

Metrics/Package.resolved

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Metrics/Package.swift

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// swift-tools-version:4.2
2+
// The swift-tools-version declares the minimum version of Swift required to build this package.
3+
/*
4+
* Copyright 2019 Google
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
import PackageDescription
20+
21+
let package = Package(
22+
name: "Metrics",
23+
dependencies: [
24+
.package(url: "https://github.com/objecthub/swift-commandlinekit", from: "0.2.5"),
25+
],
26+
targets: [
27+
.target(
28+
name: "MetricsLib"
29+
),
30+
.target(
31+
name: "Metrics",
32+
dependencies: ["CommandLineKit", "MetricsLib"]
33+
),
34+
.testTarget(
35+
name: "MetricsTests",
36+
dependencies: ["MetricsLib"]
37+
),
38+
]
39+
)

Metrics/README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Metrics
2+
3+
A swift utility for collecting project health metrics on Travis and uploading to a database. It
4+
currently only supports parsing a Code Coverage report generated from XCov.
5+
6+
## Run the coverage parser
7+
8+
```
9+
swift build
10+
.build/debug/Metrics -c=example_report.json -o=database.json -p=99
11+
```
12+
13+
This generates a database.json file that can be executed through a pre-existing Java uploader that
14+
will push the data to a Cloud SQL database.
15+
16+
## Run the unit tests
17+
18+
```
19+
swift test
20+
```

Metrics/Sources/Metrics/main.swift

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright 2019 Google
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import CommandLineKit
18+
import Foundation
19+
import MetricsLib
20+
21+
var flags = Flags()
22+
let coveragePath = flags.string("c",
23+
"coverage",
24+
description: "Required - The path of the JSON coverage report generated by XCov.")
25+
let outputPath = flags.string("o",
26+
"output",
27+
description: "Required - The path to write the database JSON info.")
28+
let pullRequest = flags.int("p",
29+
"pull_request",
30+
description: "Required - The number of the pull request that corresponds to this coverage run.")
31+
do {
32+
try flags.parse()
33+
if !coveragePath.wasSet {
34+
print("Please specify the path of the JSON coverage report from XCov. -c or --coverage")
35+
exit(1)
36+
}
37+
if !outputPath.wasSet {
38+
print("Please specify output location for the database JSON file. -o or --output")
39+
exit(1)
40+
}
41+
if !pullRequest.wasSet {
42+
print("Please specify the corresponding pull request number. -p or --pull_request")
43+
exit(1)
44+
}
45+
let pullRequestTable = TableUpdate(table_name: "PullRequests",
46+
column_names: ["pull_request_id"],
47+
replace_measurements: [[Double(pullRequest.value!)]])
48+
let coverageReport = try CoverageReport.load(path: coveragePath.value!)
49+
let coverageTable = TableUpdate.createFrom(coverage: coverageReport,
50+
pullRequest: pullRequest.value!)
51+
let json = try UploadMetrics(tables: [pullRequestTable, coverageTable]).json()
52+
try json.write(to: NSURL(fileURLWithPath: outputPath.value!) as URL,
53+
atomically: false,
54+
encoding: .utf8)
55+
print("Successfully created \(outputPath.value!)")
56+
} catch {
57+
print("Error occurred: \(error)")
58+
exit(1)
59+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Copyright 2019 Google
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import Foundation
18+
19+
/// Represents a code coverage report generated by XCov (maps to the JSON report one to one).
20+
public struct CoverageReport: Decodable {
21+
public var targets: [Target]
22+
public var coverage: Double
23+
24+
public init(targets: [Target], coverage: Double) {
25+
self.targets = targets
26+
self.coverage = coverage
27+
}
28+
29+
/// Creates a CoverageReport from a JSON file generated by XCov.
30+
public static func load(path: String) throws -> CoverageReport {
31+
let data = try Data(contentsOf: NSURL(fileURLWithPath: path) as URL)
32+
let decoder = JSONDecoder()
33+
return try decoder.decode(CoverageReport.self, from: data)
34+
}
35+
}
36+
37+
/// An XCov target.
38+
public struct Target: Decodable {
39+
public var name: String
40+
public var files: [File]
41+
public var coverage: Double
42+
43+
public init(name: String, coverage: Double) {
44+
self.name = name
45+
self.coverage = coverage
46+
files = []
47+
}
48+
}
49+
50+
/// An XCov file.
51+
public struct File: Decodable {
52+
public var name: String
53+
public var coverage: Double
54+
public var type: String
55+
public var functions: [Function]
56+
}
57+
58+
/// An XCov function.
59+
public struct Function: Decodable {
60+
public var name: String
61+
public var coverage: Double
62+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Copyright 2019 Google
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import Foundation
18+
19+
/// A mapping of SDKs to an ID that will represent that SDK in the database.
20+
let TARGET_TO_SDK_INDEX = [
21+
"Auth_Example_iOS.app": 0.0,
22+
"Core_Example_iOS.app": 1.0,
23+
"Database_Example_iOS.app": 2.0,
24+
"DynamicLinks_Example_iOS.app": 3.0,
25+
"InstanceID_Example_iOS.app": 4.0,
26+
"Messaging_Example_iOS.app": 5.0,
27+
"Storage_Example_iOS.app": 6.0,
28+
// TODO(Corrob): Add support for Firestore, Functions, and InAppMessaging.
29+
]
30+
31+
/// Represents a set of metric table updates to upload to the database.
32+
public struct UploadMetrics: Encodable {
33+
public var tables: [TableUpdate]
34+
35+
public init(tables: [TableUpdate]) {
36+
self.tables = tables
37+
}
38+
39+
/// Converts the metric table updates to a JSON format this is compatible with the Java uploader.
40+
public func json() throws -> String {
41+
let json = try JSONEncoder().encode(self)
42+
return String(data: json, encoding: .utf8)!
43+
}
44+
}
45+
46+
/// An update to a metrics table with the new data to uplaod to the database.
47+
public struct TableUpdate: Encodable {
48+
public var table_name: String
49+
public var column_names: [String]
50+
public var replace_measurements: [[Double]]
51+
52+
public init(table_name: String, column_names: [String], replace_measurements: [[Double]]) {
53+
self.table_name = table_name
54+
self.column_names = column_names
55+
self.replace_measurements = replace_measurements
56+
}
57+
58+
/// Creates a table update for code coverage by parsing a coverage report from XCov.
59+
public static func createFrom(coverage: CoverageReport, pullRequest: Int) -> TableUpdate {
60+
var metrics = [[Double]]()
61+
for target in coverage.targets {
62+
let sdkKey = TARGET_TO_SDK_INDEX[target.name]
63+
if sdkKey == nil {
64+
print("WARNING - target \(target.name) has no mapping to an SDK id. Skipping...")
65+
} else {
66+
var row = [Double]()
67+
row.append(Double(pullRequest))
68+
row.append(sdkKey!)
69+
row.append(target.coverage)
70+
metrics.append(row)
71+
}
72+
}
73+
let columnNames = ["pull_request_id", "sdk_id", "coverage_percent"]
74+
return TableUpdate(table_name: "Coverage1", column_names: columnNames, replace_measurements: metrics)
75+
}
76+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright 2019 Google
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import MetricsLib
18+
import XCTest
19+
20+
let EXAMPLE_REPORT = "Tests/MetricsTests/example_report.json"
21+
22+
final class CoverageReportTests: XCTestCase {
23+
func testShouldParseTotalCoverage() throws {
24+
let report = try CoverageReport.load(path: EXAMPLE_REPORT)
25+
XCTAssertEqual(report.coverage, 0.5490569575543673)
26+
}
27+
28+
func testShouldParseTargets() throws {
29+
let report = try CoverageReport.load(path: EXAMPLE_REPORT)
30+
XCTAssertEqual(report.targets.count, 14)
31+
XCTAssertEqual(report.targets[0].name, "Auth_Example_iOS.app")
32+
XCTAssertEqual(report.targets[0].coverage, 0.8241201927002532)
33+
XCTAssertEqual(report.targets[0].files.count, 97)
34+
}
35+
36+
func testShouldFailOnMissingFile() throws {
37+
XCTAssertThrowsError(try CoverageReport.load(path: "IDontExist"))
38+
}
39+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2019 Google
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import MetricsLib
18+
import XCTest
19+
20+
let PULL_REQUEST = 777
21+
22+
final class UploadMetricsTests: XCTestCase {
23+
func testShouldCreateTableUpdateFromCoverageReport() {
24+
let target_one = Target(name: "Auth_Example_iOS.app", coverage: 0.1)
25+
let target_two = Target(name: "Core_Example_iOS.app", coverage: 0.2)
26+
let report = CoverageReport(targets: [target_one, target_two], coverage: 0.15)
27+
let metricsUpdate = TableUpdate.createFrom(coverage: report, pullRequest: PULL_REQUEST)
28+
XCTAssertEqual(metricsUpdate.table_name, "Coverage1")
29+
XCTAssertEqual(metricsUpdate.replace_measurements.count, 2)
30+
XCTAssertEqual(metricsUpdate.replace_measurements[0],
31+
[Double(PULL_REQUEST), 0, target_one.coverage])
32+
XCTAssertEqual(metricsUpdate.replace_measurements[1],
33+
[Double(PULL_REQUEST), 1, target_two.coverage])
34+
}
35+
36+
func testShouldIgnoreUnkownTargets() {
37+
let target = Target(name: "Unknown_Target", coverage: 0.3)
38+
let report = CoverageReport(targets: [target], coverage: 0.15)
39+
let metrics = TableUpdate.createFrom(coverage: report, pullRequest: PULL_REQUEST)
40+
XCTAssertEqual(metrics.table_name, "Coverage1")
41+
XCTAssertEqual(metrics.replace_measurements.count, 0)
42+
}
43+
44+
func testShouldConvertToJson() throws {
45+
let table = TableUpdate(table_name: "name",
46+
column_names: ["col"],
47+
replace_measurements: [[0], [2]])
48+
let metrics = UploadMetrics(tables: [table])
49+
let json = try metrics.json()
50+
XCTAssertEqual(json, "{\"tables\":[{\"replace_measurements\":[[0],[2]],\"column_names\":[\"col\"],\"table_name\":\"name\"}]}")
51+
}
52+
}

0 commit comments

Comments
 (0)