Skip to content

Commit

Permalink
Making things pretty
Browse files Browse the repository at this point in the history
  • Loading branch information
rockbruno committed Mar 25, 2019
1 parent 8b62bb1 commit f9b0c91
Show file tree
Hide file tree
Showing 16 changed files with 896 additions and 61 deletions.
674 changes: 674 additions & 0 deletions LICENSE.md

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
# 📊 SwiftInfo

SwiftInfo is a simple CLI tool that extracts and analyzes useful metrics of Swift apps such as number of dependencies, `.ipa` size, number of tests, code coverage and much more. Besides the tracking options that are provided by default, you can customize SwiftInfo to track pretty much anything that can be conveyed in a simple `.swift` script.
<img src="https://i.imgur.com/Y6z0xij.png" height="200">

SwiftInfo is a simple CLI tool that extracts and analyzes metrics that are useful for Swift apps. Besides the default tracking options that are shipped with the tool, you can also customize SwiftInfo to track pretty much anything that can be conveyed in a simple `.swift` script.

## Usage

SwiftInfo requires the raw logs of a succesful test/archive build combo to work, so it's better used as the last step of a CI pipeline.

--WIP--
56 changes: 38 additions & 18 deletions Sources/SwiftInfoCore/Core.swift
Original file line number Diff line number Diff line change
@@ -1,26 +1,46 @@
import Foundation

public func extract<T: InfoProvider>(_ provider: T.Type) throws -> Output {
do {
let extracted = try provider.extract()
let other = try FileUtils().lastOutput.extractedInfo(ofType: provider)
let summary = extracted.summary(comparingWith: other)
let info = ExtractedInfo(data: extracted, summary: summary)
let dictionary = try info.encoded()
return Output(rawDictionary: dictionary)
} catch {
print(error)
throw error
public struct SwiftInfo {
public let fileUtils: FileUtils
public let slackFormatter: SlackFormatter
public let network: Network

public init(fileUtils: FileUtils = .init(),
slackFormatter: SlackFormatter = .init(),
network: Network = Network.shared) {
self.fileUtils = fileUtils
self.slackFormatter = slackFormatter
self.network = network
}
}

public func sendToSlack(output: Output) {
print("slack")
}
public func extract<T: InfoProvider>(_ provider: T.Type) -> Output {
do {
print("Extracting \(provider.identifier)")
let extracted = try provider.extract()
let other = try fileUtils.lastOutput.extractedInfo(ofType: provider)
let summary = extracted.summary(comparingWith: other)
let info = ExtractedInfo(data: extracted, summary: summary)
return try Output(info: info)
} catch {
fail(error.localizedDescription)
}
}

public func save(output: Output) throws {
let outputFile = FileUtils().outputJson
try FileUtils().save(output: [output.rawDictionary] + outputFile)
public func sendToSlack(output: Output, webhookUrl: String) {
print("Sending to slack...")
let formatted = slackFormatter.format(output: output)
network.syncPost(urlString: webhookUrl, json: formatted)
}

public func save(output: Output) {
print("Saving output to disk...")
let outputFile = fileUtils.outputJson
do {
try fileUtils.save(output: [output.rawDictionary] + outputFile)
} catch {
fail(error.localizedDescription)
}
}
}

public func fail(_ message: String) -> Never {
Expand Down
2 changes: 1 addition & 1 deletion Sources/SwiftInfoCore/ExtractedInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Foundation

struct ExtractedInfo<T: InfoProvider>: Codable {
let data: T
let summary: String
let summary: Summary

func encoded() throws -> [String: Any] {
let data = try JSONEncoder().encode(self)
Expand Down
2 changes: 1 addition & 1 deletion Sources/SwiftInfoCore/FileUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public struct FileUtils {

public var lastOutput: Output {
let last = outputJson.first ?? [:]
return Output(rawDictionary: last)
return Output(rawDictionary: last, summaries: [])
}

public init() {}
Expand Down
2 changes: 1 addition & 1 deletion Sources/SwiftInfoCore/InfoProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ public protocol InfoProvider: Codable {
static var identifier: String { get }
static func extract() throws -> Self
var description: String { get }
func summary(comparingWith other: Self?) -> String
func summary(comparingWith other: Self?) -> Summary
}
24 changes: 24 additions & 0 deletions Sources/SwiftInfoCore/Network.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Foundation

public final class Network {
public static let shared = Network()
let client = URLSession.shared
let group = DispatchGroup()

func syncPost(urlString: String, json: [String: Any]) {
guard let url = URL(string: urlString) else {
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.allHTTPHeaderFields = ["Content-Type": "application/json"]
let data = try! JSONSerialization.data(withJSONObject: json, options: [])
request.httpBody = data
group.enter()
let task = client.dataTask(with: request) { [weak self] _, _, _ in
self?.group.leave()
}
task.resume()
group.wait()
}
}
23 changes: 19 additions & 4 deletions Sources/SwiftInfoCore/Output.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,36 @@ import Foundation

public struct Output {
let rawDictionary: [String: Any]
let summaries: [Summary]

public init(rawDictionary: [String: Any], summaries: [Summary]) {
self.rawDictionary = rawDictionary
self.summaries = summaries
}

init<T: InfoProvider>(info: ExtractedInfo<T>) throws {
self.rawDictionary = try info.encoded()
self.summaries = [info.summary]
}

func extractedInfo<T: InfoProvider>(ofType type: T.Type) throws -> T? {
let json = rawDictionary[type.identifier] as? [String: Any] ?? [:]
let data = try JSONSerialization.data(withJSONObject: json, options: [])
let extractedInfo = try JSONDecoder().decode(ExtractedInfo<T>.self, from: data)
return extractedInfo.data
guard let data = try? JSONSerialization.data(withJSONObject: json, options: []) else {
return nil
}
let extractedInfo = try? JSONDecoder().decode(ExtractedInfo<T>.self, from: data)
return extractedInfo?.data
}
}

extension Output {
public static func +(lhs: Output, rhs: Output) -> Output {
let lhsDict = lhs.rawDictionary
let rhsDict = rhs.rawDictionary
let dict = lhsDict.merging(rhsDict) { new, _ in
return new
}
return Output(rawDictionary: dict)
return Output(rawDictionary: dict, summaries: lhs.summaries + rhs.summaries)
}

public static func +=(lhs: inout Output, rhs: Output) {
Expand Down
23 changes: 16 additions & 7 deletions Sources/SwiftInfoCore/Providers/CodeCoverageProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,25 @@ public struct CodeCoverageProvider: InfoProvider {
_ = Shell().run(supressOutput: true, "rm \(tempFile.path)")
}

public func summary(comparingWith other: CodeCoverageProvider?) -> String {
let regularMessage = "Code Coverage: \(percentage)"
public func summary(comparingWith other: CodeCoverageProvider?) -> Summary {
let prefix = "📊 Code Coverage"
guard let other = other else {
return regularMessage
return Summary(text: prefix + ": \(percentage)", style: .neutral)
}
if percentage == other.percentage {
return regularMessage
guard percentage != other.percentage else {
return Summary(text: prefix + ": Unchanged. (\(percentage))", style: .neutral)
}
let modifier: String
let style: Summary.Style
if percentage > other.percentage {
modifier = "*grew*"
style = .positive
} else {
modifier = "was *reduced*"
style = .negative
}
let difference = abs(other.percentage - percentage)
let modifier = percentage > other.percentage ? "*grew*" : "was *reduced*"
return "Test count \(modifier) by \(Double(difference) / 10) (\(Double(percentage) / 10))"
let text = prefix + " \(modifier) by \(Double(difference) / 10) (\(Double(percentage) / 10))"
return Summary(text: text, style: style)
}
}
23 changes: 16 additions & 7 deletions Sources/SwiftInfoCore/Providers/IPASizeProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,26 @@ public struct IPASizeProvider: InfoProvider {
return String(format: "%4.2f %@", convertedValue, tokens[multiplyFactor])
}

public func summary(comparingWith other: IPASizeProvider?) -> String {
let regularMessage = ".ipa size: \(friendlySize)"
public func summary(comparingWith other: IPASizeProvider?) -> Summary {
let prefix = "📦 .ipa size"
guard let other = other else {
return regularMessage
return Summary(text: prefix + ": \(friendlySize)", style: .neutral)
}
if size == other.size {
return regularMessage
guard size != other.size else {
return Summary(text: prefix + ": Unchanged. (\(friendlySize))", style: .neutral)
}
let modifier: String
let style: Summary.Style
if size > other.size {
modifier = "*grew*"
style = .negative
} else {
modifier = "was *reduced*"
style = .positive
}
let difference = abs(other.size - size)
let modifier = size > other.size ? "*grew*" : "was *reduced*"
let friendlyDifference = IPASizeProvider.convertToFileString(with: difference)
return ".ipa size \(modifier) by \(friendlyDifference) (\(friendlySize))"
let text = prefix + " \(modifier) by \(friendlyDifference) (\(friendlySize))"
return Summary(text: text, style: style)
}
}
23 changes: 16 additions & 7 deletions Sources/SwiftInfoCore/Providers/TargetCountProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,25 @@ public struct TargetCountProvider: InfoProvider {
return TargetCountProvider(count: modules.count)
}

public func summary(comparingWith other: TargetCountProvider?) -> String {
let regularMessage = "Dependency Count: \(count)"
public func summary(comparingWith other: TargetCountProvider?) -> Summary {
let prefix = "👶 Dependency Count"
guard let other = other else {
return regularMessage
return Summary(text: prefix + ": \(count)", style: .neutral)
}
if count == other.count {
return regularMessage
guard count != other.count else {
return Summary(text: prefix + ": Unchanged. (\(count))", style: .neutral)
}
let modifier: String
let style: Summary.Style
if count > other.count {
modifier = "*grew*"
style = .negative
} else {
modifier = "was *reduced*"
style = .positive
}
let difference = abs(other.count - count)
let modifier = count > other.count ? "*grew*" : "was *reduced*"
return "Dependency count \(modifier) by \(difference) (\(count))"
let text = prefix + " \(modifier) by \(difference) (\(count))"
return Summary(text: text, style: style)
}
}
23 changes: 16 additions & 7 deletions Sources/SwiftInfoCore/Providers/TestCountProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,25 @@ public struct TestCountProvider: InfoProvider {
return TestCountProvider(count: count)
}

public func summary(comparingWith other: TestCountProvider?) -> String {
let regularMessage = "Test Count: \(count)"
public func summary(comparingWith other: TestCountProvider?) -> Summary {
let prefix = "🎯 Test Count"
guard let other = other else {
return regularMessage
return Summary(text: prefix + ": \(count)", style: .neutral)
}
if count == other.count {
return regularMessage
guard count != other.count else {
return Summary(text: prefix + ": Unchanged. (\(count))", style: .neutral)
}
let modifier: String
let style: Summary.Style
if count > other.count {
modifier = "*grew*"
style = .positive
} else {
modifier = "was *reduced*"
style = .negative
}
let difference = abs(other.count - count)
let modifier = count > other.count ? "*grew*" : "was *reduced*"
return "Test count \(modifier) by \(difference) (\(count))"
let text = prefix + " \(modifier) by \(difference) (\(count))"
return Summary(text: text, style: style)
}
}
23 changes: 16 additions & 7 deletions Sources/SwiftInfoCore/Providers/WarningCountProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,25 @@ public struct WarningCountProvider: InfoProvider {
return WarningCountProvider(count: count)
}

public func summary(comparingWith other: WarningCountProvider?) -> String {
let regularMessage = "Warning count: \(count)"
public func summary(comparingWith other: WarningCountProvider?) -> Summary {
let prefix = "⚠️ Warning count"
guard let other = other else {
return regularMessage
return Summary(text: prefix + ": \(count)", style: .neutral)
}
if count == other.count {
return regularMessage
guard count != other.count else {
return Summary(text: prefix + ": Unchanged. (\(count))", style: .neutral)
}
let modifier: String
let style: Summary.Style
if count > other.count {
modifier = "*grew*"
style = .negative
} else {
modifier = "was *reduced*"
style = .positive
}
let difference = abs(other.count - count)
let modifier = count > other.count ? "*grew*" : "was *reduced*"
return "Warning count \(modifier) by \(difference) (\(count))"
let text = prefix + " \(modifier) by \(difference) (\(count))"
return Summary(text: text, style: style)
}
}
13 changes: 13 additions & 0 deletions Sources/SwiftInfoCore/SlackFormatter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import Foundation

public struct SlackFormatter {

public init() {}

public func format(output: Output) -> [String: Any] {
print(output.summaries.map { $0.text }.joined(separator: "\n"))
let data = try! JSONEncoder().encode(output.summaries)
let json = try! JSONSerialization.jsonObject(with: data, options: []) as! [[String: Any]]
return ["text": "SwiftInfo results for MyApp 1.10.11:", "attachments": json]
}
}
29 changes: 29 additions & 0 deletions Sources/SwiftInfoCore/Summary.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import Foundation

public struct Summary: Codable, Hashable {

public enum Style {
case positive
case neutral
case negative

var hexColor: String {
switch self {
case .positive:
return "#36a64f"
case .neutral:
return "#757575"
case .negative:
return "#c41919"
}
}
}

let text: String
let color: String

public init(text: String, style: Style) {
self.text = text
self.color = style.hexColor
}
}
11 changes: 11 additions & 0 deletions SwiftInfo.podspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Pod::Spec.new do |s|
s.name = 'SwiftInfo'
s.module_name = 'SwiftInfo'
s.version = '0.0.1'
s.license = { type: 'GNU GPL v3.0', file: 'LICENSE.md' }
s.summary = 'Extract and analyze the evolution of an iOS app\'s code.'
s.homepage = 'https://github.com/rockbruno/SwiftInfo'
s.authors = { 'Bruno Rocha' => '[email protected]' }
s.social_media_url = 'https://twitter.com/rockthebruno'
s.source = { http: "https://github.com/rockbruno/SwiftInfo/releases/download/#{s.version}/SwiftInfo.zip" }
end

0 comments on commit f9b0c91

Please sign in to comment.