Skip to content

Commit

Permalink
Merge pull request #23 from rockbruno/danger
Browse files Browse the repository at this point in the history
2.3.1 - Danger Support
  • Loading branch information
rockbruno committed May 20, 2019
2 parents 0fbba4e + 382a98d commit cfdc199
Show file tree
Hide file tree
Showing 13 changed files with 75 additions and 32 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Expand Up @@ -10,6 +10,10 @@

## master

## 2.3.1
* Adding support to danger-SwiftInfo - Bruno Rocha
* Making some failure messages better - Bruno Rocha

## 2.3.0
* Added support for installation via Homebrew. - [Cihat G眉nd眉z](https://github.com/Dschee) (Issue [#17](https://github.com/rockbruno/SwiftInfo/issues/17), PR [#20](https://github.com/rockbruno/SwiftInfo/pull/20))

Expand Down
2 changes: 1 addition & 1 deletion Formula/swiftinfo.rb
@@ -1,7 +1,7 @@
class Swiftinfo < Formula
desc "馃搳 Extract and analyze the evolution of an iOS app's code."
homepage "https://github.com/rockbruno/SwiftInfo"
version "2.3.0"
version "2.3.1"
url "https://github.com/rockbruno/SwiftInfo/releases/download/#{version}/swiftinfo.zip"
# TODO: Try something to provide a SHA automatically

Expand Down
50 changes: 35 additions & 15 deletions README.md
Expand Up @@ -4,7 +4,11 @@

[![GitHub release](https://img.shields.io/github/tag/rockbruno/SwiftInfo.svg)](https://github.com/rockbruno/SwiftInfo/releases)

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 customize SwiftInfo to track pretty much anything that can be conveyed in a simple `.swift` script.
SwiftInfo is a 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.

By default SwiftInfo will assume you're extracting info from a release build and send the final results to Slack, but it can be used to extract info from individual pull requests as well with the [danger-SwiftInfo](https://github.com/rockbruno/danger-SwiftInfo) [danger](https://github.com/danger/danger) plugin.

<img src="https://i.imgur.com/8kvEx5O.png">

## Available Providers

Expand All @@ -26,11 +30,15 @@ SwiftInfo is a simple CLI tool that extracts, tracks and analyzes metrics that a

## 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 extracts information by analyzing the logs that logs that Xcode generates when you build and/or test your app. Because it requires these logs to work, SwiftInfo is meant to be used alongside a build automation tool like [fastlane](https://github.com/fastlane/fastlane). The following topics describe how you can retrieve these logs and setup SwiftInfo itself.

We'll show how to get the logs first as you'll need them to configure SwiftInfo.

**Note:** This repository contains an example project. Check it out to see the tool in action!

### Retrieving raw logs with Fastlane
### Retrieving raw logs with [fastlane](https://github.com/fastlane/fastlane)

If you use Fastlane, you can expose the raw logs after building by adding `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):
If you use fastlane, you can expose the raw logs by adding the `buildlog_path` argument to `scan` (test logs) and `gym` (build logs). 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"
Expand Down Expand Up @@ -66,44 +74,56 @@ end

### Retrieving raw logs manually

An alternative that doesn't require Fastlane is to simply manually run `xcodebuild` / `xctest` and pipe the output to a file. We don't recommend doing this in a real project, but it can be useful if you just want to test the tool without having to download other tools.
An alternative that doesn't require fastlane is to simply manually run `xcodebuild` / `xctest` and pipe the output to a file. We don't recommend doing this in a real project, but it can be useful if you just want to test the tool without having to setup fastlane.

```
xcodebuild -workspace ./Example.xcworkspace -scheme Example &> ./build/build_log/Example-Release.log
```

## Configuring
## Configuring SwiftInfo

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:

**Note:** This repository contains an example project. Check it out to see the tool in action!
SwiftInfo itself is configured by creating a `Infofile.swift` file in your project's root. Here's an example one:

```swift
import SwiftInfoCore

// 1
FileUtils.buildLogFilePath = "./build/build_log/MyApp-MyConfig.log"
FileUtils.testLogFilePath = "./build/tests_log/MyApp-MyConfig.log"

// 2
let projectInfo = ProjectInfo(xcodeproj: "MyApp.xcodeproj",
target: "MyTarget",
configuration: "MyConfig")

let api = SwiftInfo(projectInfo: projectInfo)

// 3
let output = api.extract(IPASizeProvider.self) +
api.extract(WarningCountProvider.self) +
api.extract(TestCountProvider.self) +
api.extract(TargetCountProvider.self, args: .init(mode: .complainOnRemovals)) +
api.extract(CodeCoverageProvider.self, args: .init(targets: ["NetworkModule", "MyApp"])) +
api.extract(LinesOfCodeProvider.self, args: .init(targets: ["NetworkModule", "MyApp"]))

// Send the results to Slack.
api.sendToSlack(output: output, webhookUrl: "YOUR_SLACK_WEBHOOK_HERE")
// 4

// Save the output to disk.
api.save(output: output)
if isInPullRequestMode {
// If called from danger-SwiftInfo, print the results to the pull request
api.print(output: output)
} else {
// If called manually, send the results to Slack...
api.sendToSlack(output: output, webhookUrl: url)
// ...and save the output to your repo so it serves as the basis for new comparisons.
api.save(output: output)
}
```

- 1: Use `FileUtils` to configure the path of your logs. If you're using fastlane and don't know what the name of the log files are going to be, just run it once to have it create them.
- 2: Create a `SwiftInfo` instance by passing your project's information.
- 3: Use `SwiftInfo`'s `extract()` to extract and append all the information you want into a single property.
- 4: Lastly, you can act upon this output. Here, I print the results to a pull request if [danger-SwiftInfo](https://github.com/rockbruno/danger-SwiftInfo) is being used, or send it to Slack / save it to the repo if this is the result of a release build.

You can see `SwiftInfo`'s properties and methods [here.](Sources/SwiftInfoCore/SwiftInfo.swift)

## Available Arguments
Expand All @@ -118,9 +138,9 @@ To be able to support different types of projects, SwiftInfo provides customizat

## Output

After successfully extracting data, SwiftInfo will add/update a json file in the `{Infofile path}/SwiftInfo-output` folder. It's important to add this file to version control after the running the tool as this is what SwiftInfo uses to compare new pieces of information.
After successfully extracting data, you can call `api.save(output: output)` to have SwiftInfo add/update a json file in the `{Infofile path}/SwiftInfo-output` folder. It's important to add this file to version control after the running the tool as this is what SwiftInfo uses to compare new pieces of information.

[SwiftInfo-Reader](https://github.com/rockbruno/SwiftInfo-Reader) can be used to transform this output into a more visual static HTML page:
[SwiftInfo-Reader](https://github.com/rockbruno/SwiftInfo-Reader) can be used to transform this output into a more visual static HTML page.

<img src="https://i.imgur.com/62jNGdh.png">

Expand Down
2 changes: 1 addition & 1 deletion Sources/SwiftInfo/main.swift
Expand Up @@ -7,7 +7,7 @@ public struct Main {
static func run() {
let fileUtils = FileUtils()
let toolchainPath = getToolchainPath()
log("SwiftInfo 2.3.0")
log("SwiftInfo 2.3.1")
if ProcessInfo.processInfo.arguments.contains("-version") {
exit(0)
}
Expand Down
1 change: 1 addition & 0 deletions Sources/SwiftInfoCore/Logger.swift
Expand Up @@ -2,6 +2,7 @@ import Foundation

public var isInVerboseMode = ProcessInfo.processInfo.arguments.contains("-v")
public var isInSilentMode = ProcessInfo.processInfo.arguments.contains("-s")
public var isInPullRequestMode = ProcessInfo.processInfo.arguments.contains("-pullRequest")
public var printSourceKitQueries = ProcessInfo.processInfo.arguments.contains("-print-sourcekit")

public func log(_ message: String, verbose: Bool = false, sourceKit: Bool = false, hasPrefix: Bool = true) {
Expand Down
6 changes: 3 additions & 3 deletions Sources/SwiftInfoCore/ProjectInfo/PlistExtractor.swift
Expand Up @@ -28,15 +28,15 @@ public struct XcodeprojPlistExtractor: PlistExtractor {
fail("Failed to load .pbxproj! (\(projectFolder + project))")
}
guard let pbxTarget = xcodeproj.pbxproj.targets(named: target).first else {
fail("Target not found.")
fail("The provided target was not found in the .pbxproj.")
}
let buildConfigs = pbxTarget.buildConfigurationList?.buildConfigurations
let config = buildConfigs?.first { $0.name == configuration }
guard let cfg = config else {
fail("Config not found in .pbjproj!")
fail("The provided configuration was not found in the .pbxproj!")
}
guard let plist = cfg.buildSettings["INFOPLIST_FILE"] as? String else {
fail("Plist not found.")
fail("The provided configuration has no plist. (INFOPLIST_FILE)")
}
return plist
} catch {
Expand Down
Expand Up @@ -18,7 +18,7 @@ public struct ArchiveDurationProvider: InfoProvider {
let buildLog = try api.fileUtils.buildLog()
let durationString = buildLog.match(regex: #"(?<=\*\* ARCHIVE SUCCEEDED \*\* \[).*?(?= sec)"#).first
guard let duration = Float(durationString ?? "") else {
throw error("Total archive time (** ARCHIVE SUCCEEDED **) not found in the logs. Did the archive fail?")
throw error("Total archive time (ARCHIVE SUCCEEDED) not found in the logs. Did the archive fail?")
}
return ArchiveDurationProvider(timeInt: Int(duration * 1000))
}
Expand Down
3 changes: 3 additions & 0 deletions Sources/SwiftInfoCore/Providers/TestCountProvider.swift
Expand Up @@ -18,6 +18,9 @@ public struct TestCountProvider: InfoProvider {
let testLog = try api.fileUtils.testLog()
let count = testLog.insensitiveMatch(regex: "Test Case '.*' passed").count +
testLog.insensitiveMatch(regex: "Test Case '.*' failed").count
guard count > 0 else {
fail("Failing because 0 tests were found, and this is probably not intentional.")
}
return TestCountProvider(count: count)
}

Expand Down
Expand Up @@ -18,7 +18,7 @@ public struct TotalTestDurationProvider: InfoProvider {
let testLog = try api.fileUtils.testLog()
let durationString = testLog.match(regex: #"(?<=\*\* TEST SUCCEEDED \*\* \[).*?(?= sec)"#).first
guard let duration = Float(durationString ?? "") else {
throw error("Total test duration (** TEST SUCCEEDED **) not found in the logs. Did the tests fail?")
throw error("Total test duration (TEST SUCCEEDED) not found in the logs. Did the tests fail?")
}
return TotalTestDurationProvider(durationInt: Int(duration * 1000))
}
Expand Down
10 changes: 5 additions & 5 deletions Sources/SwiftInfoCore/SlackFormatter.swift
Expand Up @@ -4,13 +4,13 @@ public struct SlackFormatter {

public init() {}

public func format(output: Output, projectInfo: ProjectInfo) -> [String: Any] {
public func format(output: Output, projectInfo: ProjectInfo) -> (json: [String: Any], message: String) {
let json = output.summaries.map { $0.slackDictionary }
let prefix = "SwiftInfo results for \(projectInfo.description):"
let errors = "\nErrors:\n\(output.errors.joined(separator: "\n"))"
let text = prefix + (output.errors.isEmpty ? "" : errors)
log(text, hasPrefix: false)
log(output.summaries.map { $0.text }.joined(separator: "\n"), hasPrefix: false)
return ["text": text, "attachments": json]
let title = prefix + (output.errors.isEmpty ? "" : errors)
let description = output.summaries.map { $0.text }.joined(separator: "\n")
let message = title + "\n" + description
return (["text": title, "attachments": json], message)
}
}
11 changes: 9 additions & 2 deletions Sources/SwiftInfoCore/SwiftInfo.swift
Expand Up @@ -49,7 +49,14 @@ public struct SwiftInfo {
log("Sending to Slack")
log("Slack Webhook: \(webhookUrl)", verbose: true)
let formatted = slackFormatter.format(output: output, projectInfo: projectInfo)
client.syncPost(urlString: webhookUrl, json: formatted)
client.syncPost(urlString: webhookUrl, json: formatted.json)
}

public func print(output: Output) {
let formatted = slackFormatter.format(output: output, projectInfo: projectInfo)
// We print directly so that `log()`'s conditions don't interfere.
// This is meant to be used with `danger-SwiftInfo` for printing to pull requests.
Swift.print(formatted.message)
}

public func save(output: Output,
Expand All @@ -75,6 +82,6 @@ public struct SwiftInfo {
}

public func fail(_ message: String) -> Never {
log("Fatal error: \(message)")
print("SwiftInfo crashed. Reason: \(message)")
exit(-1)
}
2 changes: 1 addition & 1 deletion SwiftInfo.podspec
@@ -1,7 +1,7 @@
Pod::Spec.new do |s|
s.name = 'SwiftInfo'
s.module_name = 'SwiftInfo'
s.version = '2.3.0'
s.version = '2.3.1'
s.license = { type: 'MIT', file: 'LICENSE' }
s.summary = 'Extract and analyze the evolution of an iOS app\'s code.'
s.homepage = 'https://github.com/rockbruno/SwiftInfo'
Expand Down
12 changes: 10 additions & 2 deletions Tests/SwiftInfoTests/SlackFormatterTests.swift
Expand Up @@ -7,7 +7,7 @@ final class SlackFormatterTests: XCTestCase {
let summaries = [Summary(text: "A", style: .positive, numericValue: 0, stringValue: "a")]
let output = Output(rawDictionary: [:], summaries: summaries, errors: [])
let formatted = SlackFormatter().format(output: output, projectInfo: swiftInfo.projectInfo)
let dictionary = NSDictionary(dictionary: formatted)
let dictionary = NSDictionary(dictionary: formatted.json)
let expected: [String: Any] =
[
"attachments": [["color": "#36a64f","text": "A"]],
Expand All @@ -21,12 +21,20 @@ final class SlackFormatterTests: XCTestCase {
let summaries = [Summary(text: "A", style: .positive, numericValue: 0, stringValue: "a")]
let output = Output(rawDictionary: [:], summaries: summaries, errors: ["abc", "cde"])
let formatted = SlackFormatter().format(output: output, projectInfo: swiftInfo.projectInfo)
let dictionary = NSDictionary(dictionary: formatted)
let dictionary = NSDictionary(dictionary: formatted.json)
let expected: [String: Any] =
[
"attachments": [["color": "#36a64f","text": "A"]],
"text": "SwiftInfo results for Mock 1.0 (1) - Mock-Debug:\nErrors:\nabc\ncde"
]
XCTAssertEqual(dictionary, NSDictionary(dictionary: expected))
}

func testRawPrint() {
let swiftInfo = SwiftInfo.mock()
let summaries = [Summary(text: "A", style: .positive, numericValue: 0, stringValue: "a")]
let output = Output(rawDictionary: [:], summaries: summaries, errors: [])
let formatted = SlackFormatter().format(output: output, projectInfo: swiftInfo.projectInfo)
XCTAssertEqual(formatted.message, "SwiftInfo results for Mock 1.0 (1) - Mock-Debug:\nA")
}
}

0 comments on commit cfdc199

Please sign in to comment.