Skip to content
Permalink
Browse files

Making things pretty

  • Loading branch information...
rockbruno committed Mar 24, 2019
1 parent 8b62bb1 commit 0ebf88109697d1c935cfe6c6c2581883ed746260

Large diffs are not rendered by default.

Oops, something went wrong.
@@ -0,0 +1,52 @@
{
"object": {
"pins": [
{
"package": "AEXML",
"repositoryURL": "https://github.com/tadija/AEXML",
"state": {
"branch": null,
"revision": "54bb8ea6fb693dd3f92a89e5fcc19e199fdeedd0",
"version": "4.3.3"
}
},
{
"package": "PathKit",
"repositoryURL": "https://github.com/kylef/PathKit",
"state": {
"branch": null,
"revision": "e2f5be30e4c8f531c9c1e8765aa7b71c0a45d7a0",
"version": "0.9.2"
}
},
{
"package": "Spectre",
"repositoryURL": "https://github.com/kylef/Spectre.git",
"state": {
"branch": null,
"revision": "f14ff47f45642aa5703900980b014c2e9394b6e5",
"version": "0.9.0"
}
},
{
"package": "SwiftShell",
"repositoryURL": "https://github.com/kareman/SwiftShell",
"state": {
"branch": null,
"revision": "beebe43c986d89ea5359ac3adcb42dac94e5e08a",
"version": "4.1.2"
}
},
{
"package": "xcodeproj",
"repositoryURL": "https://github.com/tuist/xcodeproj.git",
"state": {
"branch": null,
"revision": "3fe1bd763072c81050b867d34db56d11cb9085bb",
"version": "6.6.0"
}
}
]
},
"version": 1
}
@@ -10,6 +10,7 @@ let package = Package(
.executable(name: "swiftinfo", targets: ["SwiftInfo"])
],
dependencies: [
.package(url: "https://github.com/tuist/xcodeproj.git", .exact("6.6.0"))
],
targets: [
// Csourcekitd: C modules wrapper for sourcekitd.
@@ -20,7 +21,7 @@ let package = Package(
// Targets can depend on other targets in this package, and on products in packages which this package depends on.
.target(
name: "SwiftInfoCore",
dependencies: ["Csourcekitd"]),
dependencies: ["Csourcekitd", "xcodeproj"]),
.target(
name: "SwiftInfo",
dependencies: ["SwiftInfoCore"]),
131 README.md
@@ -1,7 +1,134 @@
# πŸ“Š 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.
--WIP--

<img src="https://i.imgur.com/Y6z0xij.png" height="200">

SwiftInfo is a simple CLI tool that extracts, tracks 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.
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. If you use Fastlane, you can easily expose the raw logs by adding a `buildlog_path` to `scan` and `gym`. Here's a simple example of a Fastlane step that runs tests, submits an archive to TestFlight and runs SwiftInfo (be sure to edit the folder paths to what's being used by your project):

```ruby
desc "Submits a new beta build and runs SwiftInfo"
lane :beta do
# Run tests, copying the raw logs to the project folder
scan(
scheme: "MyScheme",
buildlog_path: "./build/tests_log"
)
# Archive the app, copying the raw logs to the project folder
gym(
workspace: "MyApp.xcworkspace",
scheme: "Release",
buildlog_path: "./build/build_log"
)
# Send to TestFlight
pilot(
skip_waiting_for_build_processing: true
)
# Run SwiftInfo
sh("../Pods/SwiftInfo/swiftinfo")
# Commit and push SwiftInfo's result
sh("git add ../SwiftInfo-output/SwiftInfoOutput.json")
sh("git commit -m \"[ci skip] Updating SwiftInfo Output JSON\"")
push_to_git_remote
end
```

SwiftInfo itself is configured by creating a `Infofile.swift` file in your project's root. Here's an example Infofile that retrieves some data and sends it to Slack:

```swift
import SwiftInfoCore
FileUtils.buildLogFilePath = "./build/build_log/MyApp-MyConfig.log"
FileUtils.testLogFilePath = "./build/tests_log/MyApp-MyConfig.log"
let projectInfo = ProjectInfo(xcodeproj: "MyApp.xcodeproj",
target: "MyTarget",
configuration: "MyConfig")
let api = SwiftInfo(projectInfo: projectInfo)
let output = api.extract(IPASizeProvider.self) +
api.extract(WarningCountProvider.self) +
api.extract(TestCountProvider.self) +
api.extract(TargetCountProvider.self) +
api.extract(CodeCoverageProvider.self)
// Send the results to Slack.
api.sendToSlack(output: output, webhookUrl: "YOUR_SLACK_WEBHOOK_HERE")
// Save the output to disk.
api.save(output: output)
```

The full list of providers you can use and the documentation for the `SwiftInfo` api is available here. --WIP--

## Tracking custom info

If you wish to track something that's not handled by the default providers, you can do so by creating your own provider by creating a `struct` that [inherits from InfoProvider](https://github.com/rockbruno/SwiftInfo/blob/master/Sources/SwiftInfoCore/InfoProvider.swift) and adding it to your Infofile. Here's a simple provider that tracks the number of files in a project where adding new files is bad:

```swift
struct FileCountProvider: InfoProvider {
static let identifier = "file_count"
let description = "Number of files"
let fileCount: Int
static func extract() throws -> FileCountProvider {
let count = // get the number of files in the project folder
return FileCountProvider(fileCount: count)
}
// Given another instance of this provider, return a `Summary` that explains the difference between them.
func summary(comparingWith other: FileCountProvider?) -> Summary {
let prefix = "File Count"
guard let other = other else {
return Summary(text: prefix + ": \(count)", style: .neutral)
}
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 text = prefix + " \(modifier) by \(difference) (\(count))"
return Summary(text: text, style: style)
}
}
```

**If you end up creating a custom provider, consider submitting it here as a pull request to have it added as a default one!**

## Output

After successfully extracting data, SwiftInfo will add/update a json file in the `{Infofile path}/SwiftInfo-output` folder. It's important to commit this file after the running the tool as this is what SwiftInfo uses to compare new pieces of information.

Although for now you can't do anything with the output besides sending it to Slack, we'll develop tools in the future that allow you to convert this JSON to graphs inside a HTML page.

## Installation

### CocoaPods

`pod 'SwiftInfo'`

### Swift Package Manager

`.package(url: "https://github.com/rockbruno/SwiftInfo.git", from: .upToNextMajor(from: "0.1.0"))`

## License

SwiftInfo is released under the GNU GPL v3.0 license. See LICENSE for details.
@@ -1,7 +1,7 @@
import Foundation
import SwiftInfoCore

struct SwiftInfo {
struct Main {
static func run() {
let utils = FileUtils()
guard let path = utils.infofileFolder() else {
@@ -12,6 +12,7 @@ struct SwiftInfo {
}
print("SwiftInfo")
let shell = Shell()
//FIXME: Shutting down SwiftInfo should force the sub processes to shut down as well.
shell.run("swiftc",
path + "Infofile.swift",
"-I",
@@ -24,4 +25,4 @@ struct SwiftInfo {
}
}

SwiftInfo.run()
Main.run()

This file was deleted.

Oops, something went wrong.
@@ -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)
@@ -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() {}
@@ -1,8 +1,12 @@
import Foundation

public protocol InfoProvider: Codable {
// The identifier of this provider.
static var identifier: String { get }
// Run this provider and return an instance of it, containing the extracted info.
static func extract() throws -> Self
// The descriptive name of this provider, for visual purposes.
var description: String { get }
func summary(comparingWith other: Self?) -> String
// Given another instance of this provider, return a `Summary` that explains the difference between them.
func summary(comparingWith other: Self?) -> Summary
}
@@ -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()
}
}
@@ -2,24 +2,39 @@ import Foundation

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

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) {
lhs = lhs + rhs
}

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

0 comments on commit 0ebf881

Please sign in to comment.
You can’t perform that action at this time.