├── .github └── workflows │ ├── ci.yml │ └── documentation.yml ├── .gitignore ├── .jazzy.yml ├── .ruby-version ├── CHANGELOG.md ├── CREATING_CUSTOM_PROVIDERS.md ├── ExampleProject ├── Gemfile ├── Gemfile.lock ├── Infofile.swift ├── Makefile ├── SwiftInfoExample.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ ├── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcuserdata │ │ │ └── bruno.rocha.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ ├── xcshareddata │ │ └── xcschemes │ │ │ └── SwiftInfoExample.xcscheme │ └── xcuserdata │ │ └── bruno.rocha.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist ├── SwiftInfoExample │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ ├── Contents.json │ │ └── iconCardGift.imageset │ │ │ ├── Contents.json │ │ │ ├── iconCardGift.png │ │ │ ├── iconCardGift@2x.png │ │ │ └── iconCardGift@3x.png │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Info.plist │ └── ViewController.swift ├── SwiftInfoExampleTests │ ├── Info.plist │ └── SwiftInfoExampleTests.swift └── fastlane │ ├── Fastfile │ └── README.md ├── Formula └── swiftinfo.rb ├── LICENSE ├── Makefile ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── Csourcekitd │ ├── include │ │ ├── module.modulemap │ │ └── sourcekitd_functions.h │ └── sourcekitd.c ├── SwiftInfo │ └── main.swift └── SwiftInfoCore │ ├── BuildSystem.swift │ ├── ExtractedInfo.swift │ ├── FileUtils │ ├── FileOpener.swift │ └── FileUtils.swift │ ├── HTTPClient.swift │ ├── InfoProvider.swift │ ├── Logger.swift │ ├── Output.swift │ ├── ProjectInfo │ ├── PlistExtractor.swift │ └── ProjectInfo.swift │ ├── Providers │ ├── ArchiveDurationProvider.swift │ ├── AssetCatalog.swift │ ├── CodeCoverageProvider.swift │ ├── IPASizeProvider.swift │ ├── LargestAssetCatalogProvider.swift │ ├── LargestAssetProvider.swift │ ├── LinesOfCodeProvider.swift │ ├── LongestTestDurationProvider.swift │ ├── OBJCFileCountProvider.swift │ ├── TargetCountProvider.swift │ ├── TestCountProvider.swift │ ├── TotalAssetCatalogsSizeProvider.swift │ ├── TotalTestDurationProvider.swift │ └── WarningCountProvider.swift │ ├── Regex.swift │ ├── Runner.swift │ ├── SlackFormatter.swift │ ├── SourceKitWrapper │ ├── DLHandle.swift │ └── SourceKit.swift │ ├── Summary.swift │ └── SwiftInfo.swift ├── SwiftInfo.podspec └── Tests ├── LinuxMain.swift └── SwiftInfoTests ├── CoreTests.swift ├── FileUtilsTests.swift ├── MockFileManager.swift ├── MockFileOpener.swift ├── MockPlistExtractor.swift ├── MockSourceKit.swift ├── ProjectInfoDataTests.swift ├── ProviderTests └── ProviderTests.swift ├── SlackFormatterTests.swift └── XCTestManifests.swift /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: macos-latest 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v2 16 | - name: Build 17 | run: make build 18 | env: 19 | DEVELOPER_DIR: /Applications/Xcode_12.3.app/Contents/Developer 20 | - name: Run tests 21 | run: swift test 22 | - name: Check the example project 23 | run: cd ./ExampleProject && make swiftinfo 24 | env: 25 | DEVELOPER_DIR: /Applications/Xcode_12.3.app/Contents/Developer 26 | - name: Package 27 | run: make package 28 | -------------------------------------------------------------------------------- /.github/workflows/documentation.yml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | deploy_docs: 10 | runs-on: macos-latest 11 | steps: 12 | - uses: actions/checkout@v1 13 | - name: Publish Jazzy Docs 14 | uses: steven0351/publish-jazzy-docs@v1 15 | with: 16 | personal_access_token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} 17 | config: .jazzy.yml -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.swiftpm 3 | SwiftInfo-bin/ 4 | /.build 5 | /.builda 6 | /Packages 7 | /*.xcodeproj 8 | swiftinfo 9 | /build 10 | /build1 11 | /build2 12 | /SwiftInfo-output 13 | /bin 14 | BuildTools/.build 15 | SwiftInfo-output 16 | Pods 17 | *.xcworkspace 18 | build 19 | ExampleProject/fastlane/report.xml 20 | ExampleProject/fastlane/test_output/report.html 21 | ExampleProject/fastlane/test_output/report.junit -------------------------------------------------------------------------------- /.jazzy.yml: -------------------------------------------------------------------------------- 1 | clean: true 2 | author: rockbruno 3 | author_url: https://github.com/rockbruno/SwiftInfo 4 | module: SwiftInfoCore 5 | output: Documentation -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.6.5 -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 10 | 11 | ## master 12 | 13 | ## 2.3.7 14 | * Fixed CodeCoverageProvider not working in Xcode 11 15 | 16 | ## 2.3.6 17 | * Recompiled for Xcode 11's toolchain (Swift 5.1) 18 | 19 | ## 2.3.5 20 | * Adding support for `buckLogFilePath` for the extraction of Buck Build related rules - Bruno Rocha 21 | * Adding Buck support to `TestCountProvider` - Bruno Rocha 22 | 23 | ## 2.3.4 24 | * Fixes version string and makes error methods public for custom providers - Bruno Rocha 25 | 26 | ## 2.3.3 27 | * Make `Summary.genericFor` public for custom providers - Bruno Rocha 28 | 29 | ## 2.3.2 30 | * ProjectInfo now allows you to manually specify a `versionNumber` and `buildNumber` in case your Info.plist doesn't have them (Buck iOS apps) - Bruno Rocha 31 | 32 | ## 2.3.1 33 | * Adding support to danger-SwiftInfo - Bruno Rocha 34 | * Making some failure messages better - Bruno Rocha 35 | 36 | ## 2.3.0 37 | * 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)) 38 | 39 | ## 2.2.0 40 | * Added LargestAssetProvider - [Bruno Rocha](https://github.com/rockbruno) 41 | * Changed how SwiftInfo generates summary results to allow custom providers to make use of SwiftInfo-Reader - [Bruno Rocha](https://github.com/rockbruno) 42 | * Small visual improvements to summaries - [Bruno Rocha](https://github.com/rockbruno) 43 | 44 | ## 2.1.0 45 | * Added ArchiveTimeProvider - [Bruno Rocha](https://github.com/rockbruno) 46 | * SwiftInfo will now continue executing even if a provider fails (the reasons are printed in the final summary) - [Bruno Rocha](https://github.com/rockbruno) 47 | * Improvements to the durability of many providers and fixing minor bugs related to them - [Bruno Rocha](https://github.com/rockbruno) 48 | * Fixed many providers silently failing if Xcode's new build system was active - [Bruno Rocha](https://github.com/rockbruno) 49 | * Fixed many providers reporting empty results when they should have failed - [Bruno Rocha](https://github.com/rockbruno) 50 | 51 | ## 2.0.2 52 | * Fixed some providers reporting wrong colors for the result - [Bruno Rocha](https://github.com/rockbruno) 53 | 54 | ## 2.0.1 55 | * Fixed LongestTest's provider not working with non-legacy workspaces - [Bruno Rocha](https://github.com/rockbruno) 56 | 57 | ## 2.0.0 58 | * Added support for arguments - [Bruno Rocha](https://github.com/rockbruno) 59 | * Updated to Swift 5 - [Bruno Rocha](https://github.com/rockbruno) 60 | * Improved CodeCoverageProvider and LinesOfCodeProvider - [Bruno Rocha](https://github.com/rockbruno) 61 | 62 | ## 1.2.0 63 | * Added LinesOfCodeProvider - [Bruno Rocha](https://github.com/rockbruno) 64 | * Slightly improved generic summary messages - [Bruno Rocha](https://github.com/rockbruno) 65 | 66 | ## 1.1.0 67 | * Linked sourcekitd to allow extraction of code related metrics - [Bruno Rocha](https://github.com/rockbruno) 68 | * Added OBJCFileCountProvider - [Bruno Rocha](https://github.com/rockbruno) 69 | * Added LongestTestDurationProvider - [Bruno Rocha](https://github.com/rockbruno) 70 | * Added TotalTestDurationProvider - [Bruno Rocha](https://github.com/rockbruno) 71 | * Added LargestAssetCatalogProvider - [Bruno Rocha](https://github.com/rockbruno) 72 | * Added TotalAssetCatalogsSizeProvider - [Bruno Rocha](https://github.com/rockbruno) 73 | 74 | ## 1.0.0 75 | * Added Unit Tests - [Bruno Rocha](https://github.com/rockbruno) 76 | * InfoProvider's `extract() -> Self` is now `extract(fromApi api: SwiftInfo) -> Self` - [Bruno Rocha](https://github.com/rockbruno) 77 | * Revamped logs - [Bruno Rocha](https://github.com/rockbruno) 78 | 79 | ## 0.1.1 80 | * Fixed dylib search paths by using `--driver-mode=swift` when running `swiftc` - [Bruno Rocha](https://github.com/rockbruno) 81 | 82 | ## 0.1.0 83 | (Initial Release) 84 | -------------------------------------------------------------------------------- /CREATING_CUSTOM_PROVIDERS.md: -------------------------------------------------------------------------------- 1 | # Creating your own providers 2 | 3 | If you wish to track something that's not handled by the default providers, you can create your own provider by creating a `struct` that inherits from `InfoProvider` inside your Infofile. Here's a simple provider that tracks the number of files in a project where adding new files is bad: 4 | 5 | ```swift 6 | struct FileCountProvider: InfoProvider { 7 | 8 | struct Args { 9 | let fromFolders: [String] 10 | } 11 | 12 | typealias Arguments = Args 13 | 14 | static let identifier = "file_count" 15 | let description = "Number of files" 16 | 17 | let fileCount: Int 18 | 19 | static func extract(fromApi api: SwiftInfo, args: Args?) throws -> FileCountProvider { 20 | let count = // get the number of files from the provided `args?.fromFolders` 21 | return FileCountProvider(fileCount: count) 22 | } 23 | 24 | // Given another instance of this provider, return a `Summary` that explains the difference between them. 25 | func summary(comparingWith other: FileCountProvider?, args: Args?) -> Summary { 26 | let prefix = "File Count" 27 | guard let other = other else { 28 | return Summary(text: prefix + ": \(fileCount)", style: .neutral) 29 | } 30 | guard count != other.count else { 31 | return Summary(text: prefix + ": Unchanged. (\(fileCount))", style: .neutral) 32 | } 33 | let modifier: String 34 | let style: Summary.Style 35 | if fileCount > other.fileCount { 36 | modifier = "*grew*" 37 | style = .negative 38 | } else { 39 | modifier = "was *reduced*" 40 | style = .positive 41 | } 42 | let difference = abs(other.fileCount - fileCount) 43 | let text = prefix + " \(modifier) by \(difference) (\(fileCount))" 44 | return Summary(text: text, style: style, numericValue: Float(fileCount), stringValue: "\(fileCount) files") 45 | } 46 | } 47 | ``` 48 | 49 | [Check our docs page](https://rockbruno.github.io/SwiftInfo/Structs/SwiftInfo.html) to see the capabilities of the `SwiftInfo` api. 50 | 51 | **If you end up creating a custom provider, consider submitting it here as a pull request to have it added as a default one!** 52 | -------------------------------------------------------------------------------- /ExampleProject/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "fastlane" 4 | gem "cocoapods" -------------------------------------------------------------------------------- /ExampleProject/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.2) 5 | activesupport (4.2.11.1) 6 | i18n (~> 0.7) 7 | minitest (~> 5.1) 8 | thread_safe (~> 0.3, >= 0.3.4) 9 | tzinfo (~> 1.1) 10 | addressable (2.7.0) 11 | public_suffix (>= 2.0.2, < 5.0) 12 | algoliasearch (1.27.2) 13 | httpclient (~> 2.8, >= 2.8.3) 14 | json (>= 1.5.1) 15 | atomos (0.1.3) 16 | aws-eventstream (1.1.0) 17 | aws-partitions (1.312.0) 18 | aws-sdk-core (3.95.0) 19 | aws-eventstream (~> 1, >= 1.0.2) 20 | aws-partitions (~> 1, >= 1.239.0) 21 | aws-sigv4 (~> 1.1) 22 | jmespath (~> 1.0) 23 | aws-sdk-kms (1.31.0) 24 | aws-sdk-core (~> 3, >= 3.71.0) 25 | aws-sigv4 (~> 1.1) 26 | aws-sdk-s3 (1.64.0) 27 | aws-sdk-core (~> 3, >= 3.83.0) 28 | aws-sdk-kms (~> 1) 29 | aws-sigv4 (~> 1.1) 30 | aws-sigv4 (1.1.3) 31 | aws-eventstream (~> 1.0, >= 1.0.2) 32 | babosa (1.0.3) 33 | claide (1.0.3) 34 | cocoapods (1.9.1) 35 | activesupport (>= 4.0.2, < 5) 36 | claide (>= 1.0.2, < 2.0) 37 | cocoapods-core (= 1.9.1) 38 | cocoapods-deintegrate (>= 1.0.3, < 2.0) 39 | cocoapods-downloader (>= 1.2.2, < 2.0) 40 | cocoapods-plugins (>= 1.0.0, < 2.0) 41 | cocoapods-search (>= 1.0.0, < 2.0) 42 | cocoapods-stats (>= 1.0.0, < 2.0) 43 | cocoapods-trunk (>= 1.4.0, < 2.0) 44 | cocoapods-try (>= 1.1.0, < 2.0) 45 | colored2 (~> 3.1) 46 | escape (~> 0.0.4) 47 | fourflusher (>= 2.3.0, < 3.0) 48 | gh_inspector (~> 1.0) 49 | molinillo (~> 0.6.6) 50 | nap (~> 1.0) 51 | ruby-macho (~> 1.4) 52 | xcodeproj (>= 1.14.0, < 2.0) 53 | cocoapods-core (1.9.1) 54 | activesupport (>= 4.0.2, < 6) 55 | algoliasearch (~> 1.0) 56 | concurrent-ruby (~> 1.1) 57 | fuzzy_match (~> 2.0.4) 58 | nap (~> 1.0) 59 | netrc (~> 0.11) 60 | typhoeus (~> 1.0) 61 | cocoapods-deintegrate (1.0.4) 62 | cocoapods-downloader (1.3.0) 63 | cocoapods-plugins (1.0.0) 64 | nap 65 | cocoapods-search (1.0.0) 66 | cocoapods-stats (1.1.0) 67 | cocoapods-trunk (1.5.0) 68 | nap (>= 0.8, < 2.0) 69 | netrc (~> 0.11) 70 | cocoapods-try (1.2.0) 71 | colored (1.2) 72 | colored2 (3.1.2) 73 | commander-fastlane (4.4.6) 74 | highline (~> 1.7.2) 75 | concurrent-ruby (1.1.6) 76 | declarative (0.0.10) 77 | declarative-option (0.1.0) 78 | digest-crc (0.5.1) 79 | domain_name (0.5.20190701) 80 | unf (>= 0.0.5, < 1.0.0) 81 | dotenv (2.7.5) 82 | emoji_regex (1.0.1) 83 | escape (0.0.4) 84 | ethon (0.12.0) 85 | ffi (>= 1.3.0) 86 | excon (0.73.0) 87 | faraday (0.17.3) 88 | multipart-post (>= 1.2, < 3) 89 | faraday-cookie_jar (0.0.6) 90 | faraday (>= 0.7.4) 91 | http-cookie (~> 1.0.0) 92 | faraday_middleware (0.13.1) 93 | faraday (>= 0.7.4, < 1.0) 94 | fastimage (2.1.7) 95 | fastlane (2.146.1) 96 | CFPropertyList (>= 2.3, < 4.0.0) 97 | addressable (>= 2.3, < 3.0.0) 98 | aws-sdk-s3 (~> 1.0) 99 | babosa (>= 1.0.2, < 2.0.0) 100 | bundler (>= 1.12.0, < 3.0.0) 101 | colored 102 | commander-fastlane (>= 4.4.6, < 5.0.0) 103 | dotenv (>= 2.1.1, < 3.0.0) 104 | emoji_regex (>= 0.1, < 2.0) 105 | excon (>= 0.71.0, < 1.0.0) 106 | faraday (~> 0.17) 107 | faraday-cookie_jar (~> 0.0.6) 108 | faraday_middleware (~> 0.13.1) 109 | fastimage (>= 2.1.0, < 3.0.0) 110 | gh_inspector (>= 1.1.2, < 2.0.0) 111 | google-api-client (>= 0.29.2, < 0.37.0) 112 | google-cloud-storage (>= 1.15.0, < 2.0.0) 113 | highline (>= 1.7.2, < 2.0.0) 114 | json (< 3.0.0) 115 | jwt (~> 2.1.0) 116 | mini_magick (>= 4.9.4, < 5.0.0) 117 | multi_xml (~> 0.5) 118 | multipart-post (~> 2.0.0) 119 | plist (>= 3.1.0, < 4.0.0) 120 | public_suffix (~> 2.0.0) 121 | rubyzip (>= 1.3.0, < 2.0.0) 122 | security (= 0.1.3) 123 | simctl (~> 1.6.3) 124 | slack-notifier (>= 2.0.0, < 3.0.0) 125 | terminal-notifier (>= 2.0.0, < 3.0.0) 126 | terminal-table (>= 1.4.5, < 2.0.0) 127 | tty-screen (>= 0.6.3, < 1.0.0) 128 | tty-spinner (>= 0.8.0, < 1.0.0) 129 | word_wrap (~> 1.0.0) 130 | xcodeproj (>= 1.13.0, < 2.0.0) 131 | xcpretty (~> 0.3.0) 132 | xcpretty-travis-formatter (>= 0.0.3) 133 | ffi (1.12.2) 134 | fourflusher (2.3.1) 135 | fuzzy_match (2.0.4) 136 | gh_inspector (1.1.3) 137 | google-api-client (0.36.4) 138 | addressable (~> 2.5, >= 2.5.1) 139 | googleauth (~> 0.9) 140 | httpclient (>= 2.8.1, < 3.0) 141 | mini_mime (~> 1.0) 142 | representable (~> 3.0) 143 | retriable (>= 2.0, < 4.0) 144 | signet (~> 0.12) 145 | google-cloud-core (1.5.0) 146 | google-cloud-env (~> 1.0) 147 | google-cloud-errors (~> 1.0) 148 | google-cloud-env (1.3.1) 149 | faraday (>= 0.17.3, < 2.0) 150 | google-cloud-errors (1.0.0) 151 | google-cloud-storage (1.26.1) 152 | addressable (~> 2.5) 153 | digest-crc (~> 0.4) 154 | google-api-client (~> 0.33) 155 | google-cloud-core (~> 1.2) 156 | googleauth (~> 0.9) 157 | mini_mime (~> 1.0) 158 | googleauth (0.12.0) 159 | faraday (>= 0.17.3, < 2.0) 160 | jwt (>= 1.4, < 3.0) 161 | memoist (~> 0.16) 162 | multi_json (~> 1.11) 163 | os (>= 0.9, < 2.0) 164 | signet (~> 0.14) 165 | highline (1.7.10) 166 | http-cookie (1.0.3) 167 | domain_name (~> 0.5) 168 | httpclient (2.8.3) 169 | i18n (0.9.5) 170 | concurrent-ruby (~> 1.0) 171 | jmespath (1.4.0) 172 | json (2.3.0) 173 | jwt (2.1.0) 174 | memoist (0.16.2) 175 | mini_magick (4.10.1) 176 | mini_mime (1.0.2) 177 | minitest (5.14.0) 178 | molinillo (0.6.6) 179 | multi_json (1.14.1) 180 | multi_xml (0.6.0) 181 | multipart-post (2.0.0) 182 | nanaimo (0.2.6) 183 | nap (1.1.0) 184 | naturally (2.2.0) 185 | netrc (0.11.0) 186 | os (1.1.0) 187 | plist (3.5.0) 188 | public_suffix (2.0.5) 189 | representable (3.0.4) 190 | declarative (< 0.1.0) 191 | declarative-option (< 0.2.0) 192 | uber (< 0.2.0) 193 | retriable (3.1.2) 194 | rouge (2.0.7) 195 | ruby-macho (1.4.0) 196 | rubyzip (1.3.0) 197 | security (0.1.3) 198 | signet (0.14.0) 199 | addressable (~> 2.3) 200 | faraday (>= 0.17.3, < 2.0) 201 | jwt (>= 1.5, < 3.0) 202 | multi_json (~> 1.10) 203 | simctl (1.6.8) 204 | CFPropertyList 205 | naturally 206 | slack-notifier (2.3.2) 207 | terminal-notifier (2.0.0) 208 | terminal-table (1.8.0) 209 | unicode-display_width (~> 1.1, >= 1.1.1) 210 | thread_safe (0.3.6) 211 | tty-cursor (0.7.1) 212 | tty-screen (0.7.1) 213 | tty-spinner (0.9.3) 214 | tty-cursor (~> 0.7) 215 | typhoeus (1.4.0) 216 | ethon (>= 0.9.0) 217 | tzinfo (1.2.7) 218 | thread_safe (~> 0.1) 219 | uber (0.1.0) 220 | unf (0.1.4) 221 | unf_ext 222 | unf_ext (0.0.7.7) 223 | unicode-display_width (1.7.0) 224 | word_wrap (1.0.0) 225 | xcodeproj (1.16.0) 226 | CFPropertyList (>= 2.3.3, < 4.0) 227 | atomos (~> 0.1.3) 228 | claide (>= 1.0.2, < 2.0) 229 | colored2 (~> 3.1) 230 | nanaimo (~> 0.2.6) 231 | xcpretty (0.3.0) 232 | rouge (~> 2.0.7) 233 | xcpretty-travis-formatter (1.0.0) 234 | xcpretty (~> 0.2, >= 0.0.7) 235 | 236 | PLATFORMS 237 | ruby 238 | 239 | DEPENDENCIES 240 | cocoapods 241 | fastlane 242 | 243 | BUNDLED WITH 244 | 2.1.4 245 | -------------------------------------------------------------------------------- /ExampleProject/Infofile.swift: -------------------------------------------------------------------------------- 1 | import SwiftInfoCore 2 | import Foundation 3 | 4 | FileUtils.buildLogFilePath = "./build/build_log/SwiftInfoExample-SwiftInfoExample.log" 5 | FileUtils.testLogFilePath = "./build/tests_log/SwiftInfoExample-SwiftInfoExample.log" 6 | 7 | let projectInfo = ProjectInfo(xcodeproj: "SwiftInfoExample.xcodeproj", 8 | target: "SwiftInfoExample", 9 | configuration: "Release") 10 | 11 | let api = SwiftInfo(projectInfo: projectInfo) 12 | 13 | let output = api.extract(TotalTestDurationProvider.self) + 14 | api.extract(TestCountProvider.self) + 15 | api.extract(CodeCoverageProvider.self) + 16 | api.extract(LongestTestDurationProvider.self) 17 | 18 | //api.sendToSlack(output: output, webhookUrl: "slackUrlHere") 19 | api.print(output: output) 20 | 21 | if ProcessInfo.processInfo.arguments.contains("--myCustomArgument") { 22 | print("Yay, custom arguments!") 23 | } else { 24 | preconditionFailure("The custom arguments test didn't work.") 25 | } 26 | 27 | api.save(output: output) 28 | -------------------------------------------------------------------------------- /ExampleProject/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY : swiftinfo 2 | 3 | swiftinfo: 4 | cd ../ && make build 5 | cd ../ && make package 6 | rm -rf SwiftInfo-bin 7 | mkdir SwiftInfo-bin 8 | cp ../.build/release/_PRODUCT/swiftinfo.zip ./SwiftInfo-bin 9 | cd ./SwiftInfo-bin && unzip swiftinfo.zip 10 | bundle install 11 | bundle exec fastlane beta 12 | ./SwiftInfo-bin/bin/swiftinfo --help 13 | ./SwiftInfo-bin/bin/swiftinfo -v --myCustomArgument 14 | echo "-------" 15 | echo "This bash script runs SwiftInfo outside of fastlane so you can see the output, but check out the Fastfile to see how you could use this in a real project." -------------------------------------------------------------------------------- /ExampleProject/SwiftInfoExample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | FDCD0CC722787F1D0090CB88 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCD0CC622787F1D0090CB88 /* AppDelegate.swift */; }; 11 | FDCD0CC922787F1D0090CB88 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCD0CC822787F1D0090CB88 /* ViewController.swift */; }; 12 | FDCD0CCC22787F1D0090CB88 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FDCD0CCA22787F1D0090CB88 /* Main.storyboard */; }; 13 | FDCD0CCE22787F1E0090CB88 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FDCD0CCD22787F1E0090CB88 /* Assets.xcassets */; }; 14 | FDCD0CD122787F1E0090CB88 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FDCD0CCF22787F1E0090CB88 /* LaunchScreen.storyboard */; }; 15 | FDCD0CDC22787F1E0090CB88 /* SwiftInfoExampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCD0CDB22787F1E0090CB88 /* SwiftInfoExampleTests.swift */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXContainerItemProxy section */ 19 | FDCD0CD822787F1E0090CB88 /* PBXContainerItemProxy */ = { 20 | isa = PBXContainerItemProxy; 21 | containerPortal = FDCD0CBB22787F1C0090CB88 /* Project object */; 22 | proxyType = 1; 23 | remoteGlobalIDString = FDCD0CC222787F1C0090CB88; 24 | remoteInfo = SwiftInfoExample; 25 | }; 26 | /* End PBXContainerItemProxy section */ 27 | 28 | /* Begin PBXFileReference section */ 29 | FDCD0CC322787F1D0090CB88 /* SwiftInfoExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftInfoExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 30 | FDCD0CC622787F1D0090CB88 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 31 | FDCD0CC822787F1D0090CB88 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 32 | FDCD0CCB22787F1D0090CB88 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 33 | FDCD0CCD22787F1E0090CB88 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 34 | FDCD0CD022787F1E0090CB88 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 35 | FDCD0CD222787F1E0090CB88 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 36 | FDCD0CD722787F1E0090CB88 /* SwiftInfoExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SwiftInfoExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 37 | FDCD0CDB22787F1E0090CB88 /* SwiftInfoExampleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftInfoExampleTests.swift; sourceTree = ""; }; 38 | FDCD0CDD22787F1E0090CB88 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 39 | /* End PBXFileReference section */ 40 | 41 | /* Begin PBXFrameworksBuildPhase section */ 42 | FDCD0CC022787F1C0090CB88 /* Frameworks */ = { 43 | isa = PBXFrameworksBuildPhase; 44 | buildActionMask = 2147483647; 45 | files = ( 46 | ); 47 | runOnlyForDeploymentPostprocessing = 0; 48 | }; 49 | FDCD0CD422787F1E0090CB88 /* Frameworks */ = { 50 | isa = PBXFrameworksBuildPhase; 51 | buildActionMask = 2147483647; 52 | files = ( 53 | ); 54 | runOnlyForDeploymentPostprocessing = 0; 55 | }; 56 | /* End PBXFrameworksBuildPhase section */ 57 | 58 | /* Begin PBXGroup section */ 59 | FDCD0CBA22787F1C0090CB88 = { 60 | isa = PBXGroup; 61 | children = ( 62 | FDCD0CC522787F1D0090CB88 /* SwiftInfoExample */, 63 | FDCD0CDA22787F1E0090CB88 /* SwiftInfoExampleTests */, 64 | FDCD0CC422787F1D0090CB88 /* Products */, 65 | ); 66 | sourceTree = ""; 67 | }; 68 | FDCD0CC422787F1D0090CB88 /* Products */ = { 69 | isa = PBXGroup; 70 | children = ( 71 | FDCD0CC322787F1D0090CB88 /* SwiftInfoExample.app */, 72 | FDCD0CD722787F1E0090CB88 /* SwiftInfoExampleTests.xctest */, 73 | ); 74 | name = Products; 75 | sourceTree = ""; 76 | }; 77 | FDCD0CC522787F1D0090CB88 /* SwiftInfoExample */ = { 78 | isa = PBXGroup; 79 | children = ( 80 | FDCD0CC622787F1D0090CB88 /* AppDelegate.swift */, 81 | FDCD0CC822787F1D0090CB88 /* ViewController.swift */, 82 | FDCD0CCA22787F1D0090CB88 /* Main.storyboard */, 83 | FDCD0CCD22787F1E0090CB88 /* Assets.xcassets */, 84 | FDCD0CCF22787F1E0090CB88 /* LaunchScreen.storyboard */, 85 | FDCD0CD222787F1E0090CB88 /* Info.plist */, 86 | ); 87 | path = SwiftInfoExample; 88 | sourceTree = ""; 89 | }; 90 | FDCD0CDA22787F1E0090CB88 /* SwiftInfoExampleTests */ = { 91 | isa = PBXGroup; 92 | children = ( 93 | FDCD0CDB22787F1E0090CB88 /* SwiftInfoExampleTests.swift */, 94 | FDCD0CDD22787F1E0090CB88 /* Info.plist */, 95 | ); 96 | path = SwiftInfoExampleTests; 97 | sourceTree = ""; 98 | }; 99 | /* End PBXGroup section */ 100 | 101 | /* Begin PBXNativeTarget section */ 102 | FDCD0CC222787F1C0090CB88 /* SwiftInfoExample */ = { 103 | isa = PBXNativeTarget; 104 | buildConfigurationList = FDCD0CE022787F1E0090CB88 /* Build configuration list for PBXNativeTarget "SwiftInfoExample" */; 105 | buildPhases = ( 106 | FDCD0CBF22787F1C0090CB88 /* Sources */, 107 | FDCD0CC022787F1C0090CB88 /* Frameworks */, 108 | FDCD0CC122787F1C0090CB88 /* Resources */, 109 | ); 110 | buildRules = ( 111 | ); 112 | dependencies = ( 113 | ); 114 | name = SwiftInfoExample; 115 | productName = SwiftInfoExample; 116 | productReference = FDCD0CC322787F1D0090CB88 /* SwiftInfoExample.app */; 117 | productType = "com.apple.product-type.application"; 118 | }; 119 | FDCD0CD622787F1E0090CB88 /* SwiftInfoExampleTests */ = { 120 | isa = PBXNativeTarget; 121 | buildConfigurationList = FDCD0CE322787F1E0090CB88 /* Build configuration list for PBXNativeTarget "SwiftInfoExampleTests" */; 122 | buildPhases = ( 123 | FDCD0CD322787F1E0090CB88 /* Sources */, 124 | FDCD0CD422787F1E0090CB88 /* Frameworks */, 125 | FDCD0CD522787F1E0090CB88 /* Resources */, 126 | ); 127 | buildRules = ( 128 | ); 129 | dependencies = ( 130 | FDCD0CD922787F1E0090CB88 /* PBXTargetDependency */, 131 | ); 132 | name = SwiftInfoExampleTests; 133 | productName = SwiftInfoExampleTests; 134 | productReference = FDCD0CD722787F1E0090CB88 /* SwiftInfoExampleTests.xctest */; 135 | productType = "com.apple.product-type.bundle.unit-test"; 136 | }; 137 | /* End PBXNativeTarget section */ 138 | 139 | /* Begin PBXProject section */ 140 | FDCD0CBB22787F1C0090CB88 /* Project object */ = { 141 | isa = PBXProject; 142 | attributes = { 143 | LastSwiftUpdateCheck = 1020; 144 | LastUpgradeCheck = 1020; 145 | ORGANIZATIONNAME = rockbruno; 146 | TargetAttributes = { 147 | FDCD0CC222787F1C0090CB88 = { 148 | CreatedOnToolsVersion = 10.2; 149 | }; 150 | FDCD0CD622787F1E0090CB88 = { 151 | CreatedOnToolsVersion = 10.2; 152 | TestTargetID = FDCD0CC222787F1C0090CB88; 153 | }; 154 | }; 155 | }; 156 | buildConfigurationList = FDCD0CBE22787F1C0090CB88 /* Build configuration list for PBXProject "SwiftInfoExample" */; 157 | compatibilityVersion = "Xcode 9.3"; 158 | developmentRegion = en; 159 | hasScannedForEncodings = 0; 160 | knownRegions = ( 161 | en, 162 | Base, 163 | ); 164 | mainGroup = FDCD0CBA22787F1C0090CB88; 165 | productRefGroup = FDCD0CC422787F1D0090CB88 /* Products */; 166 | projectDirPath = ""; 167 | projectRoot = ""; 168 | targets = ( 169 | FDCD0CC222787F1C0090CB88 /* SwiftInfoExample */, 170 | FDCD0CD622787F1E0090CB88 /* SwiftInfoExampleTests */, 171 | ); 172 | }; 173 | /* End PBXProject section */ 174 | 175 | /* Begin PBXResourcesBuildPhase section */ 176 | FDCD0CC122787F1C0090CB88 /* Resources */ = { 177 | isa = PBXResourcesBuildPhase; 178 | buildActionMask = 2147483647; 179 | files = ( 180 | FDCD0CD122787F1E0090CB88 /* LaunchScreen.storyboard in Resources */, 181 | FDCD0CCE22787F1E0090CB88 /* Assets.xcassets in Resources */, 182 | FDCD0CCC22787F1D0090CB88 /* Main.storyboard in Resources */, 183 | ); 184 | runOnlyForDeploymentPostprocessing = 0; 185 | }; 186 | FDCD0CD522787F1E0090CB88 /* Resources */ = { 187 | isa = PBXResourcesBuildPhase; 188 | buildActionMask = 2147483647; 189 | files = ( 190 | ); 191 | runOnlyForDeploymentPostprocessing = 0; 192 | }; 193 | /* End PBXResourcesBuildPhase section */ 194 | 195 | /* Begin PBXSourcesBuildPhase section */ 196 | FDCD0CBF22787F1C0090CB88 /* Sources */ = { 197 | isa = PBXSourcesBuildPhase; 198 | buildActionMask = 2147483647; 199 | files = ( 200 | FDCD0CC922787F1D0090CB88 /* ViewController.swift in Sources */, 201 | FDCD0CC722787F1D0090CB88 /* AppDelegate.swift in Sources */, 202 | ); 203 | runOnlyForDeploymentPostprocessing = 0; 204 | }; 205 | FDCD0CD322787F1E0090CB88 /* Sources */ = { 206 | isa = PBXSourcesBuildPhase; 207 | buildActionMask = 2147483647; 208 | files = ( 209 | FDCD0CDC22787F1E0090CB88 /* SwiftInfoExampleTests.swift in Sources */, 210 | ); 211 | runOnlyForDeploymentPostprocessing = 0; 212 | }; 213 | /* End PBXSourcesBuildPhase section */ 214 | 215 | /* Begin PBXTargetDependency section */ 216 | FDCD0CD922787F1E0090CB88 /* PBXTargetDependency */ = { 217 | isa = PBXTargetDependency; 218 | target = FDCD0CC222787F1C0090CB88 /* SwiftInfoExample */; 219 | targetProxy = FDCD0CD822787F1E0090CB88 /* PBXContainerItemProxy */; 220 | }; 221 | /* End PBXTargetDependency section */ 222 | 223 | /* Begin PBXVariantGroup section */ 224 | FDCD0CCA22787F1D0090CB88 /* Main.storyboard */ = { 225 | isa = PBXVariantGroup; 226 | children = ( 227 | FDCD0CCB22787F1D0090CB88 /* Base */, 228 | ); 229 | name = Main.storyboard; 230 | sourceTree = ""; 231 | }; 232 | FDCD0CCF22787F1E0090CB88 /* LaunchScreen.storyboard */ = { 233 | isa = PBXVariantGroup; 234 | children = ( 235 | FDCD0CD022787F1E0090CB88 /* Base */, 236 | ); 237 | name = LaunchScreen.storyboard; 238 | sourceTree = ""; 239 | }; 240 | /* End PBXVariantGroup section */ 241 | 242 | /* Begin XCBuildConfiguration section */ 243 | FDCD0CDE22787F1E0090CB88 /* Debug */ = { 244 | isa = XCBuildConfiguration; 245 | buildSettings = { 246 | ALWAYS_SEARCH_USER_PATHS = NO; 247 | CLANG_ANALYZER_NONNULL = YES; 248 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 249 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 250 | CLANG_CXX_LIBRARY = "libc++"; 251 | CLANG_ENABLE_MODULES = YES; 252 | CLANG_ENABLE_OBJC_ARC = YES; 253 | CLANG_ENABLE_OBJC_WEAK = YES; 254 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 255 | CLANG_WARN_BOOL_CONVERSION = YES; 256 | CLANG_WARN_COMMA = YES; 257 | CLANG_WARN_CONSTANT_CONVERSION = YES; 258 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 259 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 260 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 261 | CLANG_WARN_EMPTY_BODY = YES; 262 | CLANG_WARN_ENUM_CONVERSION = YES; 263 | CLANG_WARN_INFINITE_RECURSION = YES; 264 | CLANG_WARN_INT_CONVERSION = YES; 265 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 266 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 267 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 268 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 269 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 270 | CLANG_WARN_STRICT_PROTOTYPES = YES; 271 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 272 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 273 | CLANG_WARN_UNREACHABLE_CODE = YES; 274 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 275 | CODE_SIGN_IDENTITY = "iPhone Developer"; 276 | COPY_PHASE_STRIP = NO; 277 | DEBUG_INFORMATION_FORMAT = dwarf; 278 | ENABLE_STRICT_OBJC_MSGSEND = YES; 279 | ENABLE_TESTABILITY = YES; 280 | GCC_C_LANGUAGE_STANDARD = gnu11; 281 | GCC_DYNAMIC_NO_PIC = NO; 282 | GCC_NO_COMMON_BLOCKS = YES; 283 | GCC_OPTIMIZATION_LEVEL = 0; 284 | GCC_PREPROCESSOR_DEFINITIONS = ( 285 | "DEBUG=1", 286 | "$(inherited)", 287 | ); 288 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 289 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 290 | GCC_WARN_UNDECLARED_SELECTOR = YES; 291 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 292 | GCC_WARN_UNUSED_FUNCTION = YES; 293 | GCC_WARN_UNUSED_VARIABLE = YES; 294 | IPHONEOS_DEPLOYMENT_TARGET = 12.2; 295 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 296 | MTL_FAST_MATH = YES; 297 | ONLY_ACTIVE_ARCH = YES; 298 | SDKROOT = iphoneos; 299 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 300 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 301 | SWIFT_VERSION = 5.0; 302 | }; 303 | name = Debug; 304 | }; 305 | FDCD0CDF22787F1E0090CB88 /* Release */ = { 306 | isa = XCBuildConfiguration; 307 | buildSettings = { 308 | ALWAYS_SEARCH_USER_PATHS = NO; 309 | CLANG_ANALYZER_NONNULL = YES; 310 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 311 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 312 | CLANG_CXX_LIBRARY = "libc++"; 313 | CLANG_ENABLE_MODULES = YES; 314 | CLANG_ENABLE_OBJC_ARC = YES; 315 | CLANG_ENABLE_OBJC_WEAK = YES; 316 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 317 | CLANG_WARN_BOOL_CONVERSION = YES; 318 | CLANG_WARN_COMMA = YES; 319 | CLANG_WARN_CONSTANT_CONVERSION = YES; 320 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 321 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 322 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 323 | CLANG_WARN_EMPTY_BODY = YES; 324 | CLANG_WARN_ENUM_CONVERSION = YES; 325 | CLANG_WARN_INFINITE_RECURSION = YES; 326 | CLANG_WARN_INT_CONVERSION = YES; 327 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 328 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 329 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 330 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 331 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 332 | CLANG_WARN_STRICT_PROTOTYPES = YES; 333 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 334 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 335 | CLANG_WARN_UNREACHABLE_CODE = YES; 336 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 337 | CODE_SIGN_IDENTITY = "iPhone Developer"; 338 | COPY_PHASE_STRIP = NO; 339 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 340 | ENABLE_NS_ASSERTIONS = NO; 341 | ENABLE_STRICT_OBJC_MSGSEND = YES; 342 | GCC_C_LANGUAGE_STANDARD = gnu11; 343 | GCC_NO_COMMON_BLOCKS = YES; 344 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 345 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 346 | GCC_WARN_UNDECLARED_SELECTOR = YES; 347 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 348 | GCC_WARN_UNUSED_FUNCTION = YES; 349 | GCC_WARN_UNUSED_VARIABLE = YES; 350 | IPHONEOS_DEPLOYMENT_TARGET = 12.2; 351 | MTL_ENABLE_DEBUG_INFO = NO; 352 | MTL_FAST_MATH = YES; 353 | SDKROOT = iphoneos; 354 | SWIFT_COMPILATION_MODE = wholemodule; 355 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 356 | SWIFT_VERSION = 5.0; 357 | VALIDATE_PRODUCT = YES; 358 | }; 359 | name = Release; 360 | }; 361 | FDCD0CE122787F1E0090CB88 /* Debug */ = { 362 | isa = XCBuildConfiguration; 363 | buildSettings = { 364 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 365 | CODE_SIGN_STYLE = Automatic; 366 | DEVELOPMENT_TEAM = U7KHKH9L7R; 367 | INFOPLIST_FILE = SwiftInfoExample/Info.plist; 368 | LD_RUNPATH_SEARCH_PATHS = ( 369 | "$(inherited)", 370 | "@executable_path/Frameworks", 371 | ); 372 | PRODUCT_BUNDLE_IDENTIFIER = br.rockbruno.SwiftInfoExampleProject; 373 | PRODUCT_NAME = "$(TARGET_NAME)"; 374 | SWIFT_VERSION = 5.0; 375 | TARGETED_DEVICE_FAMILY = "1,2"; 376 | }; 377 | name = Debug; 378 | }; 379 | FDCD0CE222787F1E0090CB88 /* Release */ = { 380 | isa = XCBuildConfiguration; 381 | buildSettings = { 382 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 383 | CODE_SIGN_STYLE = Automatic; 384 | DEVELOPMENT_TEAM = U7KHKH9L7R; 385 | INFOPLIST_FILE = SwiftInfoExample/Info.plist; 386 | LD_RUNPATH_SEARCH_PATHS = ( 387 | "$(inherited)", 388 | "@executable_path/Frameworks", 389 | ); 390 | PRODUCT_BUNDLE_IDENTIFIER = br.rockbruno.SwiftInfoExampleProject; 391 | PRODUCT_NAME = "$(TARGET_NAME)"; 392 | SWIFT_VERSION = 5.0; 393 | TARGETED_DEVICE_FAMILY = "1,2"; 394 | }; 395 | name = Release; 396 | }; 397 | FDCD0CE422787F1E0090CB88 /* Debug */ = { 398 | isa = XCBuildConfiguration; 399 | buildSettings = { 400 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 401 | BUNDLE_LOADER = "$(TEST_HOST)"; 402 | CODE_SIGN_STYLE = Automatic; 403 | INFOPLIST_FILE = SwiftInfoExampleTests/Info.plist; 404 | LD_RUNPATH_SEARCH_PATHS = ( 405 | "$(inherited)", 406 | "@executable_path/Frameworks", 407 | "@loader_path/Frameworks", 408 | ); 409 | PRODUCT_BUNDLE_IDENTIFIER = br.rockbruno.SwiftInfoExampleTests; 410 | PRODUCT_NAME = "$(TARGET_NAME)"; 411 | SWIFT_VERSION = 5.0; 412 | TARGETED_DEVICE_FAMILY = "1,2"; 413 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftInfoExample.app/SwiftInfoExample"; 414 | }; 415 | name = Debug; 416 | }; 417 | FDCD0CE522787F1E0090CB88 /* Release */ = { 418 | isa = XCBuildConfiguration; 419 | buildSettings = { 420 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 421 | BUNDLE_LOADER = "$(TEST_HOST)"; 422 | CODE_SIGN_STYLE = Automatic; 423 | INFOPLIST_FILE = SwiftInfoExampleTests/Info.plist; 424 | LD_RUNPATH_SEARCH_PATHS = ( 425 | "$(inherited)", 426 | "@executable_path/Frameworks", 427 | "@loader_path/Frameworks", 428 | ); 429 | PRODUCT_BUNDLE_IDENTIFIER = br.rockbruno.SwiftInfoExampleTests; 430 | PRODUCT_NAME = "$(TARGET_NAME)"; 431 | SWIFT_VERSION = 5.0; 432 | TARGETED_DEVICE_FAMILY = "1,2"; 433 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftInfoExample.app/SwiftInfoExample"; 434 | }; 435 | name = Release; 436 | }; 437 | /* End XCBuildConfiguration section */ 438 | 439 | /* Begin XCConfigurationList section */ 440 | FDCD0CBE22787F1C0090CB88 /* Build configuration list for PBXProject "SwiftInfoExample" */ = { 441 | isa = XCConfigurationList; 442 | buildConfigurations = ( 443 | FDCD0CDE22787F1E0090CB88 /* Debug */, 444 | FDCD0CDF22787F1E0090CB88 /* Release */, 445 | ); 446 | defaultConfigurationIsVisible = 0; 447 | defaultConfigurationName = Release; 448 | }; 449 | FDCD0CE022787F1E0090CB88 /* Build configuration list for PBXNativeTarget "SwiftInfoExample" */ = { 450 | isa = XCConfigurationList; 451 | buildConfigurations = ( 452 | FDCD0CE122787F1E0090CB88 /* Debug */, 453 | FDCD0CE222787F1E0090CB88 /* Release */, 454 | ); 455 | defaultConfigurationIsVisible = 0; 456 | defaultConfigurationName = Release; 457 | }; 458 | FDCD0CE322787F1E0090CB88 /* Build configuration list for PBXNativeTarget "SwiftInfoExampleTests" */ = { 459 | isa = XCConfigurationList; 460 | buildConfigurations = ( 461 | FDCD0CE422787F1E0090CB88 /* Debug */, 462 | FDCD0CE522787F1E0090CB88 /* Release */, 463 | ); 464 | defaultConfigurationIsVisible = 0; 465 | defaultConfigurationName = Release; 466 | }; 467 | /* End XCConfigurationList section */ 468 | }; 469 | rootObject = FDCD0CBB22787F1C0090CB88 /* Project object */; 470 | } 471 | -------------------------------------------------------------------------------- /ExampleProject/SwiftInfoExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ExampleProject/SwiftInfoExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ExampleProject/SwiftInfoExample.xcodeproj/project.xcworkspace/xcuserdata/bruno.rocha.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rockbruno/SwiftInfo/7796d700c3589f88bcf7b4179f8315c0a4f43119/ExampleProject/SwiftInfoExample.xcodeproj/project.xcworkspace/xcuserdata/bruno.rocha.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /ExampleProject/SwiftInfoExample.xcodeproj/xcshareddata/xcschemes/SwiftInfoExample.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 71 | 73 | 79 | 80 | 81 | 82 | 84 | 85 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /ExampleProject/SwiftInfoExample.xcodeproj/xcuserdata/bruno.rocha.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | SwiftInfoExample.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 2 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | FDCD0CC222787F1C0090CB88 16 | 17 | primary 18 | 19 | 20 | FDCD0CD622787F1E0090CB88 21 | 22 | primary 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /ExampleProject/SwiftInfoExample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // SwiftInfoExample 4 | // 5 | // Created by Bruno Rocha on 4/30/19. 6 | // Copyright © 2019 rockbruno. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /ExampleProject/SwiftInfoExample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /ExampleProject/SwiftInfoExample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /ExampleProject/SwiftInfoExample/Assets.xcassets/iconCardGift.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "iconCardGift.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "iconCardGift@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "iconCardGift@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /ExampleProject/SwiftInfoExample/Assets.xcassets/iconCardGift.imageset/iconCardGift.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rockbruno/SwiftInfo/7796d700c3589f88bcf7b4179f8315c0a4f43119/ExampleProject/SwiftInfoExample/Assets.xcassets/iconCardGift.imageset/iconCardGift.png -------------------------------------------------------------------------------- /ExampleProject/SwiftInfoExample/Assets.xcassets/iconCardGift.imageset/iconCardGift@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rockbruno/SwiftInfo/7796d700c3589f88bcf7b4179f8315c0a4f43119/ExampleProject/SwiftInfoExample/Assets.xcassets/iconCardGift.imageset/iconCardGift@2x.png -------------------------------------------------------------------------------- /ExampleProject/SwiftInfoExample/Assets.xcassets/iconCardGift.imageset/iconCardGift@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rockbruno/SwiftInfo/7796d700c3589f88bcf7b4179f8315c0a4f43119/ExampleProject/SwiftInfoExample/Assets.xcassets/iconCardGift.imageset/iconCardGift@3x.png -------------------------------------------------------------------------------- /ExampleProject/SwiftInfoExample/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /ExampleProject/SwiftInfoExample/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /ExampleProject/SwiftInfoExample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /ExampleProject/SwiftInfoExample/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // SwiftInfoExample 4 | // 5 | // Created by Bruno Rocha on 4/30/19. 6 | // Copyright © 2019 rockbruno. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | // Do any additional setup after loading the view. 16 | } 17 | 18 | 19 | } 20 | 21 | -------------------------------------------------------------------------------- /ExampleProject/SwiftInfoExampleTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /ExampleProject/SwiftInfoExampleTests/SwiftInfoExampleTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftInfoExampleTests.swift 3 | // SwiftInfoExampleTests 4 | // 5 | // Created by Bruno Rocha on 4/30/19. 6 | // Copyright © 2019 rockbruno. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import SwiftInfoExample 11 | 12 | class SwiftInfoExampleTests: XCTestCase { 13 | 14 | override func setUp() { 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDown() { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | } 21 | 22 | func testExample() { 23 | // This is an example of a functional test case. 24 | // Use XCTAssert and related functions to verify your tests produce the correct results. 25 | } 26 | 27 | func testPerformanceExample() { 28 | // This is an example of a performance test case. 29 | self.measure { 30 | // Put the code you want to measure the time of here. 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /ExampleProject/fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | default_platform :ios 2 | 3 | platform :ios do 4 | desc "Submits a new beta build to TestFlight" 5 | lane :beta do 6 | # Run Tests, copying the raw logs to the project folder 7 | scan( 8 | scheme: "SwiftInfoExample", 9 | buildlog_path: "./build/tests_log" 10 | ) 11 | 12 | # Build the app without archiving, copying the raw logs to the project folder 13 | sh("mkdir -p ./build") 14 | sh("touch ./build/build_log") 15 | sh("xcodebuild -workspace ./SwiftInfoExample.xcworkspace -scheme SwiftInfoExample 2>&1 | tee ./build/build_log") 16 | 17 | # Archive the app, copying the raw logs to the project folder 18 | #gym( 19 | # workspace: "SwiftInfoExample.xcworkspace", 20 | # scheme: "SwiftInfoExample", 21 | # buildlog_path: "./build/build_log", 22 | # output_directory: "build", 23 | # # This is so you can run this with automatic provisioning 24 | # xcargs: "-allowProvisioningUpdates", 25 | # ) 26 | 27 | # Uncomment me if copying this to a real project 28 | # Run SwiftInfo 29 | # sh("../Pods/SwiftInfo/swiftinfo") 30 | 31 | # Commit and push SwiftInfo's result 32 | # sh("git add ../SwiftInfo-output/SwiftInfoOutput.json") 33 | # sh("git commit -m \"[ci skip] Updating SwiftInfo Output JSON\"") 34 | # push_to_git_remote 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /ExampleProject/fastlane/README.md: -------------------------------------------------------------------------------- 1 | fastlane documentation 2 | ================ 3 | # Installation 4 | 5 | Make sure you have the latest version of the Xcode command line tools installed: 6 | 7 | ``` 8 | xcode-select --install 9 | ``` 10 | 11 | Install _fastlane_ using 12 | ``` 13 | [sudo] gem install fastlane -NV 14 | ``` 15 | or alternatively using `brew cask install fastlane` 16 | 17 | # Available Actions 18 | ## iOS 19 | ### ios beta 20 | ``` 21 | fastlane ios beta 22 | ``` 23 | Submits a new beta build to TestFlight 24 | 25 | ---- 26 | 27 | This README.md is auto-generated and will be re-generated every time [fastlane](https://fastlane.tools) is run. 28 | More information about fastlane can be found on [fastlane.tools](https://fastlane.tools). 29 | The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools). 30 | -------------------------------------------------------------------------------- /Formula/swiftinfo.rb: -------------------------------------------------------------------------------- 1 | class Swiftinfo < Formula 2 | desc "📊 Extract and analyze the evolution of an iOS app's code." 3 | homepage "https://github.com/rockbruno/SwiftInfo" 4 | version "2.6.0" 5 | url "https://github.com/rockbruno/SwiftInfo/releases/download/#{version}/swiftinfo.zip" 6 | # TODO: Try something to provide a SHA automatically 7 | 8 | depends_on :xcode => ["12.5", :build] 9 | 10 | def install 11 | bin.install Dir["bin/*"] 12 | include.install Dir["include/*"] 13 | end 14 | 15 | test do 16 | system bin/"swiftinfo", "-version" 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 SwiftRocks. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL = /bin/bash 2 | 3 | REPODIR = $(shell pwd) 4 | BUILDDIR = $(REPODIR)/.build 5 | RELEASEBUILDDIR = $(BUILDDIR)/release 6 | TEMPPRODUCTDIR = $(BUILDDIR)/_PRODUCT 7 | PRODUCTDIR = $(RELEASEBUILDDIR)/_PRODUCT 8 | 9 | .DEFAULT_GOAL = all 10 | 11 | .PHONY: all 12 | all: build 13 | 14 | .PHONY: build 15 | build: 16 | @swift build \ 17 | -c release \ 18 | --disable-sandbox \ 19 | --build-path "$(BUILDDIR)" \ 20 | -Xswiftc \ 21 | -emit-module-interface \ 22 | -Xswiftc \ 23 | -enable-library-evolution \ 24 | -Xswiftc \ 25 | -swift-version \ 26 | -Xswiftc 5 27 | @rm -rf "$(PRODUCTDIR)" 28 | @rm -rf "$(TEMPPRODUCTDIR)" 29 | @mkdir -p "$(TEMPPRODUCTDIR)" 30 | @mkdir -p "$(TEMPPRODUCTDIR)/include/swiftinfo" 31 | 32 | @# Module Stability - Replace *.swiftmodule with *.swiftinterface 33 | @mv *.swiftinterface $(RELEASEBUILDDIR) 34 | @rm $(RELEASEBUILDDIR)/*.swiftmodule 35 | @# Workaround for types that have the same name as its module 36 | @# https://forums.swift.org/t/frameworkname-is-not-a-member-type-of-frameworkname-errors-inside-swiftinterface/28962 37 | @sed -i '' 's/XcodeProj\.//g' $(RELEASEBUILDDIR)/XcodeProj.swiftinterface 38 | 39 | @cp -a "$(RELEASEBUILDDIR)/." "$(TEMPPRODUCTDIR)/include/swiftinfo" 40 | @cp -a "$(TEMPPRODUCTDIR)/." "$(PRODUCTDIR)" 41 | @rm -rf "$(TEMPPRODUCTDIR)" 42 | @mkdir -p "$(PRODUCTDIR)/bin" 43 | @rm -rf $(PRODUCTDIR)/include/swiftinfo/ModuleCache 44 | @mv "$(PRODUCTDIR)/include/swiftinfo/swiftinfo" "$(PRODUCTDIR)/bin" 45 | @cp -a "$(REPODIR)/Sources/Csourcekitd/." "$(PRODUCTDIR)/include/swiftinfo/Csourcekitd" 46 | @rm -f "$(RELEASEBUILDDIR)/swiftinfo" 47 | @ln -s "$(PRODUCTDIR)/bin/swiftinfo" "$(RELEASEBUILDDIR)/swiftinfo" 48 | @cp "$(REPODIR)/LICENSE" "$(PRODUCTDIR)/LICENSE" 49 | 50 | .PHONY: package 51 | package: 52 | rm -f "$(PRODUCTDIR)/swiftinfo.zip" 53 | cd $(PRODUCTDIR) && zip -r ./swiftinfo.zip ./ 54 | echo "ZIP created at: $(PRODUCTDIR)/swiftinfo.zip" 55 | 56 | .PHONY: clean 57 | clean: 58 | @rm -rf "$(BUILDDIR)" -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "AEXML", 6 | "repositoryURL": "https://github.com/tadija/AEXML", 7 | "state": { 8 | "branch": null, 9 | "revision": "8623e73b193386909566a9ca20203e33a09af142", 10 | "version": "4.5.0" 11 | } 12 | }, 13 | { 14 | "package": "PathKit", 15 | "repositoryURL": "https://github.com/kylef/PathKit", 16 | "state": { 17 | "branch": null, 18 | "revision": "73f8e9dca9b7a3078cb79128217dc8f2e585a511", 19 | "version": "1.0.0" 20 | } 21 | }, 22 | { 23 | "package": "Spectre", 24 | "repositoryURL": "https://github.com/kylef/Spectre.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "f14ff47f45642aa5703900980b014c2e9394b6e5", 28 | "version": "0.9.0" 29 | } 30 | }, 31 | { 32 | "package": "swift-argument-parser", 33 | "repositoryURL": "https://github.com/apple/swift-argument-parser", 34 | "state": { 35 | "branch": null, 36 | "revision": "986d191f94cec88f6350056da59c2e59e83d1229", 37 | "version": "0.4.3" 38 | } 39 | }, 40 | { 41 | "package": "XcodeProj", 42 | "repositoryURL": "https://github.com/tuist/xcodeproj.git", 43 | "state": { 44 | "branch": null, 45 | "revision": "0b18c3e7a10c241323397a80cb445051f4494971", 46 | "version": "8.0.0" 47 | } 48 | } 49 | ] 50 | }, 51 | "version": 1 52 | } 53 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "SwiftInfo", 8 | products: [ 9 | .library(name: "SwiftInfoCore", type: .dynamic, targets: ["SwiftInfoCore"]), 10 | .executable(name: "swiftinfo", targets: ["SwiftInfo"]) 11 | ], 12 | dependencies: [ 13 | .package(url: "https://github.com/tuist/xcodeproj.git", .exact("8.0.0")), 14 | .package(url: "https://github.com/apple/swift-argument-parser", from: "0.4.0"), 15 | ], 16 | targets: [ 17 | // Csourcekitd: C modules wrapper for sourcekitd. 18 | .target( 19 | name: "Csourcekitd", 20 | dependencies: []), 21 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 22 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 23 | .target( 24 | name: "SwiftInfoCore", 25 | dependencies: ["Csourcekitd", "XcodeProj"]), 26 | .target( 27 | name: "SwiftInfo", 28 | dependencies: ["SwiftInfoCore", "ArgumentParser"]), 29 | .testTarget( 30 | name: "SwiftInfoTests", 31 | dependencies: ["SwiftInfo"]), 32 | ] 33 | ) 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 📊 SwiftInfo 2 | 3 | 4 | 5 | [![GitHub release](https://img.shields.io/github/tag/rockbruno/SwiftInfo.svg)](https://github.com/rockbruno/SwiftInfo/releases) 6 | 7 | **Note: I don't intend to ship further updates to this tool.** 8 | 9 | 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. 10 | 11 | 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. 12 | 13 | 14 | 15 | ## Available Providers 16 | 17 | | **Type Name** | **Description** | 18 | |---|:---:| 19 | | **📦 IPASizeProvider** | Size of the .ipa archive (not the App Store size!) | 20 | | **📊 CodeCoverageProvider** | Code coverage percentage | 21 | | **👶 TargetCountProvider** | Number of targets (dependencies) | 22 | | **🎯 TestCountProvider** | Sum of all test target's test count | 23 | | **⚠️ WarningCountProvider** | Number of warnings in a build | 24 | | **🧙‍♂️ OBJCFileCountProvider** | Number of OBJ-C files and headers (for mixed OBJ-C / Swift projects) | 25 | | **⏰ LongestTestDurationProvider** | The name and duration of the longest test | 26 | | **🛏 TotalTestDurationProvider** | Time it took to build and run all tests | 27 | | **🖼 LargestAssetCatalogProvider** | The name and size of the largest asset catalog | 28 | | **🎨 TotalAssetCatalogsSizeProvider** | The sum of the size of all asset catalogs | 29 | | **💻 LinesOfCodeProvider** | Executable lines of code | 30 | | **🚚 ArchiveDurationProvider** | Time it took to build and archive the app | 31 | | **📷 LargestAssetProvider** | The largest asset in the project. Only considers files inside asset catalogs. | 32 | 33 | Each provider may have a specific set of requirements in order for them to work. [Check their documentation to learn more](https://rockbruno.github.io/SwiftInfo/Structs.html). 34 | 35 | ## Usage 36 | 37 | SwiftInfo extracts information by analyzing the logs that your build system 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. 38 | 39 | We'll show how to get the logs first as you'll need them to configure SwiftInfo. 40 | 41 | **Note:** This repository contains an example project. Check it out to see the tool in action -- just go to the example project folder and run `make swiftinfo` in your terminal. 42 | 43 | ### Retrieving raw logs with [fastlane](https://github.com/fastlane/fastlane) 44 | 45 | If you use fastlane, you can expose raw logs to SwiftInfo by adding the `buildlog_path` argument to `scan` (test logs) and `gym` (build logs). Here's a simple example of a fastlane lane that runs tests, submits an archive to TestFlight and runs SwiftInfo (make sure to edit the folder paths to what's being used by your project): 46 | 47 | ```ruby 48 | desc "Submits a new beta build and runs SwiftInfo" 49 | lane :beta do 50 | # Run tests, copying the raw logs to the project folder 51 | scan( 52 | scheme: "MyScheme", 53 | buildlog_path: "./build/tests_log" 54 | ) 55 | 56 | # Archive the app, copying the raw logs to the project folder and the .ipa to the /build folder 57 | gym( 58 | workspace: "MyApp.xcworkspace", 59 | scheme: "Release", 60 | output_directory: "build", 61 | buildlog_path: "./build/build_log" 62 | ) 63 | 64 | # Send to TestFlight 65 | pilot( 66 | skip_waiting_for_build_processing: true 67 | ) 68 | 69 | # Run the CocoaPods version of SwiftInfo 70 | sh("../Pods/SwiftInfo/bin/swiftinfo") 71 | 72 | # Commit and push SwiftInfo's output 73 | sh("git add ../SwiftInfo-output/SwiftInfoOutput.json") 74 | sh("git commit -m \"[ci skip] Updating SwiftInfo Output JSON\"") 75 | push_to_git_remote 76 | end 77 | ``` 78 | 79 | ### Retrieving raw logs manually 80 | 81 | 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. 82 | 83 | ``` 84 | xcodebuild -workspace ./Example.xcworkspace -scheme Example 2>&1 | tee ./build/build_log/Example-Release.log 85 | ``` 86 | 87 | ## Configuring SwiftInfo 88 | 89 | SwiftInfo itself is configured by creating a `Infofile.swift` file in your project's root. Here's an example one with a detailed explanation: 90 | 91 | ```swift 92 | import SwiftInfoCore 93 | 94 | // Use `FileUtils` to configure the path of your logs. 95 | // If you're retrieving them with fastlane and don't know what the name of the log files are going to be, 96 | // just run it once to have it create them. 97 | 98 | FileUtils.buildLogFilePath = "./build/build_log/MyApp-MyConfig.log" 99 | FileUtils.testLogFilePath = "./build/tests_log/MyApp-MyConfig.log" 100 | 101 | // Now, create a `SwiftInfo` instance by passing your project's information. 102 | 103 | let projectInfo = ProjectInfo(xcodeproj: "MyApp.xcodeproj", 104 | target: "MyTarget", 105 | configuration: "MyConfig") 106 | 107 | let api = SwiftInfo(projectInfo: projectInfo) 108 | 109 | // Use SwiftInfo's `extract()` method to extract and append all the information you want into a single property. 110 | 111 | let output = api.extract(IPASizeProvider.self) + 112 | api.extract(WarningCountProvider.self) + 113 | api.extract(TestCountProvider.self) + 114 | api.extract(TargetCountProvider.self, args: .init(mode: .complainOnRemovals)) + 115 | api.extract(CodeCoverageProvider.self, args: .init(targets: ["NetworkModule", "MyApp"])) + 116 | api.extract(LinesOfCodeProvider.self, args: .init(targets: ["NetworkModule", "MyApp"])) 117 | 118 | // Lastly, process the output. 119 | 120 | if isInPullRequestMode { 121 | // If called from danger-SwiftInfo, print the results to the pull request 122 | api.print(output: output) 123 | } else { 124 | // If called manually, send the results to Slack... 125 | api.sendToSlack(output: output, webhookUrl: url) 126 | // ...and save the output to your repo so it serves as the basis for new comparisons. 127 | api.save(output: output) 128 | } 129 | ``` 130 | 131 | ## Saving and visualizing the data 132 | 133 | After successfully extracting data, you should 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. 134 | 135 | You can then use [SwiftInfo-Reader](https://github.com/rockbruno/SwiftInfo-Reader) to transform this output into a more visual static HTML page. 136 | 137 | 138 | 139 | ## Customizing Providers 140 | 141 | To be able to support different types of projects, SwiftInfo provides customization options to some providers. See the documentation for each provider to see what it supports. 142 | If you wish to track something that's not handled by the default providers, you can also create your own providers. [Click here to see how](CREATING_CUSTOM_PROVIDERS.md). 143 | 144 | ## Customizing Runs 145 | 146 | Any arguments you pass to SwiftInfo can be inspected inside your Infofile. This allows you to pass any custom information you want to the binary and use it to customize your runs. 147 | 148 | For example, if you run SwiftInfo by calling `swiftinfo --myCustomArgument`, you can use `ProcessInfo` to check for its presence inside your Infofile. 149 | 150 | ```swift 151 | if ProcessInfo.processInfo.arguments.contains("--myCustomArgument") { 152 | print("Yay, custom arguments!") 153 | } 154 | ``` 155 | 156 | If the argument has a value, you can also fetch that value with `UserDefaults`. 157 | 158 | ## Installation 159 | 160 | ### CocoaPods 161 | 162 | `pod 'SwiftInfo'` 163 | 164 | ### [Homebrew](https://brew.sh/) 165 | 166 | To install SwiftInfo with Homebrew the first time, simply run these commands: 167 | 168 | ```bash 169 | brew tap rockbruno/SwiftInfo https://github.com/rockbruno/SwiftInfo.git 170 | brew install rockbruno/SwiftInfo/swiftinfo 171 | ``` 172 | 173 | To **update** to the newest Homebrew version of SwiftInfo when you have an old version already installed, run: 174 | 175 | ```bash 176 | brew upgrade swiftinfo 177 | ``` 178 | 179 | ### Manually 180 | 181 | Download the [latest release](https://github.com/rockbruno/SwiftInfo/releases) and unzip the contents somewhere in your project's folder. 182 | 183 | ### Swift Package Manager 184 | 185 | SwiftPM is currently not supported due to the need of shipping additional files with the binary, which SwiftPM does not support. We might find a solution for this, but for now there's no way to use the tool with it. 186 | -------------------------------------------------------------------------------- /Sources/Csourcekitd/include/module.modulemap: -------------------------------------------------------------------------------- 1 | module sourcekitd { 2 | // header "sourcekitd.h" 3 | header "sourcekitd_functions.h" 4 | export * 5 | } 6 | -------------------------------------------------------------------------------- /Sources/Csourcekitd/include/sourcekitd_functions.h: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift.org open source project 4 | // 5 | // Copyright (c) 2014 - 2018 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors 10 | // 11 | //===----------------------------------------------------------------------===// 12 | 13 | #ifndef SOURCEKITDFUNCTIONS_H 14 | #define SOURCEKITDFUNCTIONS_H 15 | 16 | #include 17 | #include 18 | #include 19 | 20 | // Avoid including to make sure we don't call the functions directly. 21 | // But we need the types to form the function pointers. 22 | // These are supposed to stay stable across toolchains. 23 | 24 | typedef void *sourcekitd_object_t; 25 | typedef struct sourcekitd_uid_s *sourcekitd_uid_t; 26 | typedef void *sourcekitd_response_t; 27 | typedef void *sourcekitd_request_handle_t; 28 | 29 | typedef struct { 30 | uint64_t data[3]; 31 | } sourcekitd_variant_t; 32 | 33 | typedef enum { 34 | SOURCEKITD_VARIANT_TYPE_NULL = 0, 35 | SOURCEKITD_VARIANT_TYPE_DICTIONARY = 1, 36 | SOURCEKITD_VARIANT_TYPE_ARRAY = 2, 37 | SOURCEKITD_VARIANT_TYPE_INT64 = 3, 38 | SOURCEKITD_VARIANT_TYPE_STRING = 4, 39 | SOURCEKITD_VARIANT_TYPE_UID = 5, 40 | SOURCEKITD_VARIANT_TYPE_BOOL = 6, 41 | // Reserved for future addition 42 | // SOURCEKITD_VARIANT_TYPE_DOUBLE = 7, 43 | SOURCEKITD_VARIANT_TYPE_DATA = 8, 44 | } sourcekitd_variant_type_t; 45 | 46 | typedef enum { 47 | SOURCEKITD_ERROR_CONNECTION_INTERRUPTED = 1, 48 | SOURCEKITD_ERROR_REQUEST_INVALID = 2, 49 | SOURCEKITD_ERROR_REQUEST_FAILED = 3, 50 | SOURCEKITD_ERROR_REQUEST_CANCELLED = 4 51 | } sourcekitd_error_t; 52 | 53 | typedef bool (^sourcekitd_variant_dictionary_applier_t)(sourcekitd_uid_t key, 54 | sourcekitd_variant_t value); 55 | typedef void(^sourcekitd_interrupted_connection_handler_t)(void); 56 | typedef bool (*sourcekitd_variant_dictionary_applier_f_t)(sourcekitd_uid_t key, 57 | sourcekitd_variant_t value, 58 | void *context); 59 | typedef bool (^sourcekitd_variant_array_applier_t)(size_t index, 60 | sourcekitd_variant_t value); 61 | typedef bool (*sourcekitd_variant_array_applier_f_t)(size_t index, 62 | sourcekitd_variant_t value, 63 | void *context); 64 | typedef void (^sourcekitd_response_receiver_t)(sourcekitd_response_t resp); 65 | 66 | typedef sourcekitd_uid_t(^sourcekitd_uid_from_str_handler_t)(const char* uidStr); 67 | typedef const char *(^sourcekitd_str_from_uid_handler_t)(sourcekitd_uid_t uid); 68 | 69 | typedef struct { 70 | void (*initialize)(void); 71 | void (*shutdown)(void); 72 | 73 | sourcekitd_uid_t (*uid_get_from_cstr)(const char *string); 74 | sourcekitd_uid_t (*uid_get_from_buf)(const char *buf, size_t length); 75 | size_t (*uid_get_length)(sourcekitd_uid_t obj); 76 | const char * (*uid_get_string_ptr)(sourcekitd_uid_t obj); 77 | sourcekitd_object_t (*request_retain)(sourcekitd_object_t object); 78 | void (*request_release)(sourcekitd_object_t object); 79 | sourcekitd_object_t (*request_dictionary_create)(const sourcekitd_uid_t *keys, const sourcekitd_object_t *values, size_t count); 80 | void (*request_dictionary_set_value)(sourcekitd_object_t dict, sourcekitd_uid_t key, sourcekitd_object_t value); 81 | void (*request_dictionary_set_string)(sourcekitd_object_t dict, sourcekitd_uid_t key, const char *string); 82 | void (*request_dictionary_set_stringbuf)(sourcekitd_object_t dict, sourcekitd_uid_t key, const char *buf, size_t length); 83 | void (*request_dictionary_set_int64)(sourcekitd_object_t dict, sourcekitd_uid_t key, int64_t val); 84 | void (*request_dictionary_set_uid)(sourcekitd_object_t dict, sourcekitd_uid_t key, sourcekitd_uid_t uid); 85 | sourcekitd_object_t (*request_array_create)(const sourcekitd_object_t *objects, size_t count); 86 | void (*request_array_set_value)(sourcekitd_object_t array, size_t index, sourcekitd_object_t value); 87 | void (*request_array_set_string)(sourcekitd_object_t array, size_t index, const char *string); 88 | void (*request_array_set_stringbuf)(sourcekitd_object_t array, size_t index, const char *buf, size_t length); 89 | void (*request_array_set_int64)(sourcekitd_object_t array, size_t index, int64_t val); 90 | void (*request_array_set_uid)(sourcekitd_object_t array, size_t index, sourcekitd_uid_t uid); 91 | sourcekitd_object_t (*request_int64_create)(int64_t val); 92 | sourcekitd_object_t (*request_string_create)(const char *string); 93 | sourcekitd_object_t (*request_uid_create)(sourcekitd_uid_t uid); 94 | sourcekitd_object_t (*request_create_from_yaml)(const char *yaml, char **error); 95 | void (*request_description_dump)(sourcekitd_object_t obj); 96 | char *(*request_description_copy)(sourcekitd_object_t obj); 97 | void (*response_dispose)(sourcekitd_response_t obj); 98 | bool (*response_is_error)(sourcekitd_response_t obj); 99 | sourcekitd_error_t (*response_error_get_kind)(sourcekitd_response_t err); 100 | const char * (*response_error_get_description)(sourcekitd_response_t err); 101 | sourcekitd_variant_t (*response_get_value)(sourcekitd_response_t resp); 102 | sourcekitd_variant_type_t (*variant_get_type)(sourcekitd_variant_t obj); 103 | sourcekitd_variant_t (*variant_dictionary_get_value)(sourcekitd_variant_t dict, sourcekitd_uid_t key); 104 | const char * (*variant_dictionary_get_string)(sourcekitd_variant_t dict, sourcekitd_uid_t key); 105 | int64_t (*variant_dictionary_get_int64)(sourcekitd_variant_t dict, sourcekitd_uid_t key); 106 | bool (*variant_dictionary_get_bool)(sourcekitd_variant_t dict, sourcekitd_uid_t key); 107 | sourcekitd_uid_t (*variant_dictionary_get_uid)(sourcekitd_variant_t dict, sourcekitd_uid_t key); 108 | bool (*variant_dictionary_apply)(sourcekitd_variant_t dict, sourcekitd_variant_dictionary_applier_t applier); 109 | size_t (*variant_array_get_count)(sourcekitd_variant_t array); 110 | sourcekitd_variant_t (*variant_array_get_value)(sourcekitd_variant_t array, size_t index); 111 | const char * (*variant_array_get_string)(sourcekitd_variant_t array, size_t index); 112 | int64_t (*variant_array_get_int64)(sourcekitd_variant_t array, size_t index); 113 | bool (*variant_array_get_bool)(sourcekitd_variant_t array, size_t index); 114 | sourcekitd_uid_t (*variant_array_get_uid)(sourcekitd_variant_t array, size_t index); 115 | bool (*variant_array_apply)(sourcekitd_variant_t array, sourcekitd_variant_array_applier_t applier); 116 | int64_t (*variant_int64_get_value)(sourcekitd_variant_t obj); 117 | bool (*variant_bool_get_value)(sourcekitd_variant_t obj); 118 | size_t (*variant_string_get_length)(sourcekitd_variant_t obj); 119 | const char * (*variant_string_get_ptr)(sourcekitd_variant_t obj); 120 | size_t (*variant_data_get_size)(sourcekitd_variant_t obj); 121 | const void * (*variant_data_get_ptr)(sourcekitd_variant_t obj); 122 | sourcekitd_uid_t (*variant_uid_get_value)(sourcekitd_variant_t obj); 123 | void (*response_description_dump)(sourcekitd_response_t resp); 124 | void (*response_description_dump_filedesc)(sourcekitd_response_t resp, int fd); 125 | char *(*response_description_copy)(sourcekitd_response_t resp); 126 | void (*variant_description_dump)(sourcekitd_variant_t obj); 127 | void (*variant_description_dump_filedesc)(sourcekitd_variant_t obj, int fd); 128 | char * (*variant_description_copy)(sourcekitd_variant_t obj); 129 | sourcekitd_response_t (*send_request_sync)(sourcekitd_object_t req); 130 | void (*send_request)(sourcekitd_object_t req, sourcekitd_request_handle_t *out_handle, sourcekitd_response_receiver_t receiver); 131 | void (*cancel_request)(sourcekitd_request_handle_t handle); 132 | void (*set_notification_handler)(sourcekitd_response_receiver_t receiver); 133 | void (*set_uid_handlers)(sourcekitd_uid_from_str_handler_t uid_from_str, sourcekitd_str_from_uid_handler_t str_from_uid); 134 | } sourcekitd_functions_t; 135 | 136 | #endif 137 | -------------------------------------------------------------------------------- /Sources/Csourcekitd/sourcekitd.c: -------------------------------------------------------------------------------- 1 | // This file is here to prevent the package manager from warning about a target with no sources. 2 | -------------------------------------------------------------------------------- /Sources/SwiftInfo/main.swift: -------------------------------------------------------------------------------- 1 | import ArgumentParser 2 | import Foundation 3 | import SwiftInfoCore 4 | 5 | let task = Process() 6 | 7 | struct Swiftinfo: ParsableCommand { 8 | static var configuration = CommandConfiguration( 9 | abstract: "Swiftinfo 2.5.1", 10 | subcommands: [] 11 | ) 12 | 13 | /// Since we are using .unconditionalRemaining, --help doesnt work anymore. 14 | /// Thus, we added a manual flag here. 15 | @Flag(name: .shortAndLong, help: "Show help information.") 16 | var help = false 17 | 18 | @Flag(name: .shortAndLong, help: "Silences all logs.") 19 | var silent = false 20 | 21 | @Flag(name: .shortAndLong, help: "Logs additional details to the console.") 22 | var verbose = false 23 | 24 | @Flag(name: .shortAndLong, help: "Logs SourceKit requests to the console.") 25 | var printSourcekit = false 26 | 27 | @Argument(parsing: .unconditionalRemaining, help: "Any additional arguments that you would like your Infofile.swift to receive. These arguments can be retrieved in your Infofile through CommandLine's APIs to customize your runs.") 28 | var arguments: [String] = [] 29 | 30 | mutating func run() throws { 31 | guard !help else { 32 | print(Swiftinfo.helpMessage()) 33 | Swiftinfo.exit() 34 | } 35 | 36 | let fileUtils = FileUtils() 37 | let toolchainPath = getToolchainPath() 38 | 39 | log("Dylib Folder: \(fileUtils.toolFolder)", verbose: true) 40 | log("Infofile Path: \(try! fileUtils.infofileFolder())", verbose: true) 41 | log("Toolchain Path: \(toolchainPath)", verbose: true) 42 | 43 | let processInfoArgs = ProcessInfo.processInfo.arguments 44 | let args = Runner.getCoreSwiftCArguments(fileUtils: fileUtils, 45 | toolchainPath: toolchainPath, 46 | processInfoArgs: processInfoArgs) 47 | .joined(separator: " ") 48 | 49 | log("Swiftc Args: \(args)", verbose: true) 50 | 51 | task.launchPath = "/bin/bash" 52 | task.arguments = ["-c", args] 53 | task.standardOutput = FileHandle.standardOutput 54 | task.standardError = FileHandle.standardError 55 | 56 | task.terminationHandler = { t -> Void in 57 | Swiftinfo.exit() 58 | } 59 | 60 | task.launch() 61 | } 62 | 63 | private func getToolchainPath() -> String { 64 | let task = Process() 65 | task.launchPath = "/bin/bash" 66 | task.arguments = ["-c", "xcode-select -p"] 67 | let pipe = Pipe() 68 | task.standardOutput = pipe 69 | task.launch() 70 | task.waitUntilExit() 71 | let data = pipe.fileHandleForReading.readDataToEndOfFile() 72 | guard let developer = String(data: data, encoding: .utf8), developer.isEmpty == false else { 73 | fail("Xcode toolchain path not found. (xcode-select -p)") 74 | } 75 | let oneLined = developer.replacingOccurrences(of: "\n", with: "") 76 | return oneLined + "/Toolchains/XcodeDefault.xctoolchain/usr/lib/sourcekitd.framework/sourcekitd" 77 | } 78 | } 79 | 80 | ///////// 81 | // Detect interruptions and use it to interrupt the sub process. 82 | signal(SIGINT, SIG_IGN) 83 | let source = DispatchSource.makeSignalSource(signal: SIGINT) 84 | source.setEventHandler { 85 | task.interrupt() 86 | exit(SIGINT) 87 | } 88 | 89 | //////// 90 | 91 | source.resume() 92 | Swiftinfo.main() 93 | dispatchMain() 94 | -------------------------------------------------------------------------------- /Sources/SwiftInfoCore/BuildSystem.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum BuildSystem { 4 | case xcode 5 | case buck 6 | } 7 | -------------------------------------------------------------------------------- /Sources/SwiftInfoCore/ExtractedInfo.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct ExtractedInfo: Codable { 4 | let data: T 5 | let summary: Summary? 6 | 7 | init(data: T, summary: Summary?) { 8 | self.data = data 9 | self.summary = summary 10 | } 11 | 12 | init(from decoder: Decoder) throws { 13 | let values = try decoder.container(keyedBy: CodingKeys.self) 14 | data = try values.decode(T.self, forKey: .data) 15 | summary = try? values.decode(Summary.self, forKey: .summary) 16 | } 17 | 18 | func encoded() throws -> [String: Any] { 19 | let data = try JSONEncoder().encode(self) 20 | let json = try JSONSerialization.jsonObject(with: data, options: []) as! [String: Any] 21 | return [T.identifier: json] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/SwiftInfoCore/FileUtils/FileOpener.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | open class FileOpener { 4 | open func stringContents(ofUrl url: URL) throws -> String { 5 | return try String(contentsOf: url) 6 | } 7 | 8 | open func dataContents(ofUrl url: URL) throws -> Data { 9 | return try Data(contentsOf: url) 10 | } 11 | 12 | open func plistContents(ofPath path: String) -> NSDictionary? { 13 | return NSDictionary(contentsOfFile: path) 14 | } 15 | 16 | open func write(data: Data, toUrl url: URL) throws { 17 | try data.write(to: url) 18 | } 19 | 20 | public init() {} 21 | } 22 | -------------------------------------------------------------------------------- /Sources/SwiftInfoCore/FileUtils/FileUtils.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Wraps utilities for opening and saving files needed by SwiftInfo. 4 | public struct FileUtils { 5 | static let supportedInfofilePaths = ["./", "../", "../../", "../../../"] 6 | 7 | /// The path to the Xcode build log. 8 | public static var buildLogFilePath = "" 9 | 10 | /// The path to the Xcode test log. 11 | public static var testLogFilePath = "" 12 | 13 | /// The path to the Buck log. 14 | public static var buckLogFilePath = "" 15 | 16 | let outputFileName = "SwiftInfoOutput.json" 17 | let infofileName = "Infofile.swift" 18 | 19 | /// A file manager. 20 | public let fileManager: FileManager 21 | 22 | /// The utility that opens and saves files. 23 | public let fileOpener: FileOpener 24 | 25 | public init(fileManager: FileManager = .default, 26 | fileOpener: FileOpener = .init()) { 27 | self.fileManager = fileManager 28 | self.fileOpener = fileOpener 29 | } 30 | 31 | /// The working path that is containing the SwiftInfo binary. 32 | public var toolFolder: String { 33 | guard let executablePath = ProcessInfo.processInfo.arguments.first else { 34 | fail("Couldn't determine the folder that's running SwiftInfo.") 35 | } 36 | 37 | let executableUrl = URL(fileURLWithPath: executablePath) 38 | if let isAliasFile = try! executableUrl.resourceValues(forKeys: [URLResourceKey.isAliasFileKey]).isAliasFile, isAliasFile { 39 | return try! URL(resolvingAliasFileAt: executableUrl).deletingLastPathComponent().path 40 | } else { 41 | return executableUrl.deletingLastPathComponent().path 42 | } 43 | } 44 | 45 | /// The path where Infofile.swift is located. 46 | public func infofileFolder() throws -> String { 47 | guard let path = FileUtils.supportedInfofilePaths.first(where: { 48 | fileManager.fileExists(atPath: $0 + infofileName) 49 | }) else { 50 | throw SwiftInfoError.generic("Infofile.swift not found.") 51 | } 52 | return path 53 | } 54 | 55 | /// The contents of the test log located in the `testLogFilePath` path. 56 | public func testLog() throws -> String { 57 | let folder = try infofileFolder() 58 | let url = URL(fileURLWithPath: folder + FileUtils.testLogFilePath) 59 | do { 60 | return try fileOpener.stringContents(ofUrl: url) 61 | } catch { 62 | throw SwiftInfoError.generic(""" 63 | Test log not found! 64 | Expected path: \(FileUtils.testLogFilePath) 65 | Thrown error: \(error.localizedDescription) 66 | """) 67 | } 68 | } 69 | 70 | /// The contents of the buck log located in the `buckLogFilePath` path. 71 | public func buckLog() throws -> String { 72 | let folder = try infofileFolder() 73 | let url = URL(fileURLWithPath: folder + FileUtils.buckLogFilePath) 74 | do { 75 | return try fileOpener.stringContents(ofUrl: url) 76 | } catch { 77 | throw SwiftInfoError.generic(""" 78 | Buck's log not found! 79 | Expected path: \(FileUtils.buckLogFilePath) 80 | Thrown error: \(error.localizedDescription) 81 | """) 82 | } 83 | } 84 | 85 | /// The contents of the build log located in the `buildLogFilePath` path. 86 | public func buildLog() throws -> String { 87 | let folder = try infofileFolder() 88 | let url = URL(fileURLWithPath: folder + FileUtils.buildLogFilePath) 89 | do { 90 | return try fileOpener.stringContents(ofUrl: url) 91 | } catch { 92 | throw SwiftInfoError.generic(""" 93 | Build log not found! 94 | Expected path: \(FileUtils.buildLogFilePath) 95 | Thrown error: \(error.localizedDescription) 96 | """) 97 | } 98 | } 99 | 100 | /// The folder where the output should be stored. 101 | public func outputFileFolder() throws -> String { 102 | return (try infofileFolder()) + "SwiftInfo-output/" 103 | } 104 | 105 | /// The desired file path of the output. 106 | public func outputFileURL() throws -> URL { 107 | return URL(fileURLWithPath: (try outputFileFolder()) + outputFileName) 108 | } 109 | 110 | /// Opens the current output JSON in `outputFileURL()` and returns it as a dictionary. 111 | public func fullOutput() throws -> [String: Any] { 112 | guard let data = try? fileOpener.dataContents(ofUrl: try outputFileURL()) else { 113 | return [:] 114 | } 115 | let object = try JSONSerialization.jsonObject(with: data, options: []) 116 | return object as? [String: Any] ?? [:] 117 | } 118 | 119 | /// Opens the current output JSON in `outputFileURL()` and returns the array of SwiftInfo executions. 120 | public func outputArray() throws -> [[String: Any]] { 121 | return ((try fullOutput())["data"] as? [[String: Any]]) ?? [] 122 | } 123 | 124 | /// Opens the current output JSON in `outputFileURL()` and returns the latest SwiftInfo execution. 125 | public func lastOutput() throws -> Output { 126 | let array = try outputArray() 127 | return Output(rawDictionary: array.first ?? [:], summaries: [], errors: []) 128 | } 129 | 130 | /// Saves an output dictionary to the `outputFileURL()`. 131 | public func save(output: [[String: Any]]) throws { 132 | let path = try outputFileURL() 133 | log("Path to save: \(path.absoluteString)", verbose: true) 134 | let dictionary = ["data": output] 135 | let writingOptions: JSONSerialization.WritingOptions = { 136 | if #available(OSX 10.13, *) { 137 | return [.prettyPrinted, .sortedKeys] 138 | } else { 139 | return [.prettyPrinted] 140 | } 141 | }() 142 | let json = try JSONSerialization.data(withJSONObject: dictionary, options: writingOptions) 143 | try? fileManager.createDirectory(atPath: try outputFileFolder(), 144 | withIntermediateDirectories: true, 145 | attributes: nil) 146 | try fileOpener.write(data: json, toUrl: path) 147 | } 148 | } 149 | 150 | public enum SwiftInfoError: Error, LocalizedError { 151 | case generic(String) 152 | 153 | public var errorDescription: String? { 154 | switch self { 155 | case let .generic(message): 156 | return message 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /Sources/SwiftInfoCore/HTTPClient.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public final class HTTPClient { 4 | let client = URLSession.shared 5 | let group = DispatchGroup() 6 | 7 | public init() {} 8 | 9 | public func syncPost(urlString: String, json: [String: Any]) { 10 | guard let url = URL(string: urlString) else { 11 | return 12 | } 13 | var request = URLRequest(url: url) 14 | request.httpMethod = "POST" 15 | request.allHTTPHeaderFields = ["Content-Type": "application/json"] 16 | let data = try! JSONSerialization.data(withJSONObject: json, options: []) 17 | request.httpBody = data 18 | group.enter() 19 | let task = client.dataTask(with: request) { [weak self] _, _, _ in 20 | self?.group.leave() 21 | } 22 | task.resume() 23 | group.wait() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/SwiftInfoCore/InfoProvider.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// The base protocol that defines a struct capable of extracting and comparing pieces of information. 4 | public protocol InfoProvider: Codable { 5 | /// A structure that represents the arguments to use when extracting and comparing this provider. 6 | associatedtype Arguments 7 | 8 | /// The unique identifier of this provider. 9 | static var identifier: String { get } 10 | 11 | /// Executes this provider and returns an instance of it that contains the extracted info. 12 | static func extract(fromApi api: SwiftInfo, args: Arguments?) throws -> Self 13 | 14 | /// The descriptive name of this provider, for visual purposes. 15 | var description: String { get } 16 | 17 | /// Given another instance of this provider, return a `Summary` that explains the difference between them. 18 | func summary(comparingWith other: Self?, args: Arguments?) -> Summary 19 | } 20 | 21 | extension InfoProvider { 22 | static func extract(fromApi api: SwiftInfo) throws -> Self { 23 | return try extract(fromApi: api, args: nil) 24 | } 25 | 26 | func summary(comparingWith other: Self?) -> Summary { 27 | return summary(comparingWith: other, args: nil) 28 | } 29 | 30 | public static func error(_ message: String) -> SwiftInfoError { 31 | return .generic(message) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/SwiftInfoCore/Logger.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | func args() -> [String] { 4 | return ProcessInfo.processInfo.arguments 5 | } 6 | 7 | public var isInVerboseMode = args().contains("-v") || args().contains("--verbose") 8 | public var isInSilentMode = args().contains("-s") || args().contains("--silent") 9 | public var isInPullRequestMode = args().contains("--pullRequest") 10 | public var printSourceKitQueries = args().contains("-p") || args().contains("--print-sourcekit") 11 | 12 | public func log(_ message: String, verbose: Bool = false, sourceKit: Bool = false, hasPrefix: Bool = true) { 13 | guard isInSilentMode == false else { 14 | return 15 | } 16 | guard (sourceKit && printSourceKitQueries) || verbose == false || isInVerboseMode else { 17 | return 18 | } 19 | print("\(hasPrefix ? "* " : "")\(message)") 20 | } 21 | -------------------------------------------------------------------------------- /Sources/SwiftInfoCore/Output.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct Output { 4 | let rawDictionary: [String: Any] 5 | let summaries: [Summary] 6 | let errors: [String] 7 | 8 | init(info: ExtractedInfo) throws { 9 | rawDictionary = try info.encoded() 10 | summaries = [info.summary].compactMap { $0 } 11 | errors = [] 12 | } 13 | 14 | func extractedInfo(ofType type: T.Type) throws -> T? { 15 | let json = rawDictionary[type.identifier] as? [String: Any] ?? [:] 16 | guard let data = try? JSONSerialization.data(withJSONObject: json, options: []) else { 17 | return nil 18 | } 19 | let extractedInfo = try? JSONDecoder().decode(ExtractedInfo.self, from: data) 20 | return extractedInfo?.data 21 | } 22 | } 23 | 24 | extension Output { 25 | public static func + (lhs: Output, rhs: Output) -> Output { 26 | let lhsDict = lhs.rawDictionary 27 | let rhsDict = rhs.rawDictionary 28 | let dict = lhsDict.merging(rhsDict) { new, _ in 29 | new 30 | } 31 | return Output(rawDictionary: dict, 32 | summaries: lhs.summaries + rhs.summaries, 33 | errors: lhs.errors + rhs.errors) 34 | } 35 | 36 | public static func += (lhs: inout Output, rhs: Output) { 37 | lhs = lhs + rhs 38 | } 39 | 40 | public init(rawDictionary: [String: Any], summaries: [Summary], errors: [String]) { 41 | self.rawDictionary = rawDictionary 42 | self.summaries = summaries 43 | self.errors = errors 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/SwiftInfoCore/ProjectInfo/PlistExtractor.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import PathKit 3 | import XcodeProj 4 | 5 | public protocol PlistExtractor { 6 | func extractPlistPath(xcodeproj: String, 7 | target: String, 8 | configuration: String, 9 | fileUtils: FileUtils) -> String 10 | } 11 | 12 | public struct XcodeprojPlistExtractor: PlistExtractor { 13 | public init() {} 14 | 15 | public func extractPlistPath(xcodeproj: String, 16 | target: String, 17 | configuration: String, 18 | fileUtils: FileUtils) -> String { 19 | do { 20 | let projectFolder = try fileUtils.infofileFolder() + xcodeproj 21 | guard let xcodeproj = try? XcodeProj(path: Path(projectFolder)) else { 22 | fail("Failed to load .pbxproj! (\(projectFolder))") 23 | } 24 | guard let pbxTarget = xcodeproj.pbxproj.targets(named: target).first else { 25 | fail("The provided target was not found in the .pbxproj.") 26 | } 27 | let buildConfigs = pbxTarget.buildConfigurationList?.buildConfigurations 28 | let config = buildConfigs?.first { $0.name == configuration } 29 | guard let cfg = config else { 30 | fail("The provided configuration was not found in the .pbxproj!") 31 | } 32 | guard let plist = cfg.buildSettings["INFOPLIST_FILE"] as? String else { 33 | fail("The provided configuration has no plist. (INFOPLIST_FILE)") 34 | } 35 | return plist 36 | } catch { 37 | fail("ProjectInfo failed to resolve plist: \(error.localizedDescription)") 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/SwiftInfoCore/ProjectInfo/ProjectInfo.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct ProjectInfo: CustomStringConvertible { 4 | let xcodeproj: String 5 | let target: String 6 | let configuration: String 7 | let fileUtils: FileUtils 8 | let plistPath: String 9 | let versionString: String? 10 | let buildNumber: String? 11 | 12 | /// A visual description of this project. 13 | public var description: String { 14 | let version: String 15 | do { 16 | version = "\(try getVersionString()) (\(try getBuildNumber()))" 17 | } catch { 18 | version = "(Failed to retrieve version info)" 19 | } 20 | return "\(target) \(version) - \(configuration)" 21 | } 22 | 23 | public init(xcodeproj: String, 24 | target: String, 25 | configuration: String, 26 | plistPath: String? = nil, 27 | versionString: String? = nil, 28 | buildNumber: String? = nil, 29 | fileUtils: FileUtils = .init(), 30 | plistExtractor: PlistExtractor = XcodeprojPlistExtractor()) { 31 | self.xcodeproj = xcodeproj 32 | self.target = target 33 | self.configuration = configuration 34 | self.versionString = versionString 35 | self.buildNumber = buildNumber 36 | self.fileUtils = fileUtils 37 | self.plistPath = plistPath ?? plistExtractor.extractPlistPath(xcodeproj: xcodeproj, 38 | target: target, 39 | configuration: configuration, 40 | fileUtils: fileUtils) 41 | } 42 | 43 | func plistDict() throws -> NSDictionary { 44 | let folder = try fileUtils.infofileFolder() 45 | guard let dictionary = fileUtils.fileOpener.plistContents(ofPath: folder + plistPath) else { 46 | fail("Failed to load plist \(folder + plistPath)") 47 | } 48 | return dictionary 49 | } 50 | 51 | func getVersionString() throws -> String { 52 | if let versionString = versionString { 53 | return versionString 54 | } 55 | let plist = try plistDict() 56 | guard let version = plist["CFBundleShortVersionString"] as? String else { 57 | fail("Project version not found.") 58 | } 59 | return version 60 | } 61 | 62 | func getBuildNumber() throws -> String { 63 | if let buildNumber = buildNumber { 64 | return buildNumber 65 | } 66 | let plist = try plistDict() 67 | guard let version = plist["CFBundleVersion"] as? String else { 68 | fail("Project build number not found.") 69 | } 70 | return version 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Sources/SwiftInfoCore/Providers/ArchiveDurationProvider.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Time it took to build and archive the app. 4 | /// Requirements: Build logs of a successful xcodebuild archive. 5 | /// You must also have Xcode's ShowBuildOperationDuration enabled. 6 | /// (run in the Terminal: `defaults write com.apple.dt.Xcode ShowBuildOperationDuration YES` to enable it) 7 | public struct ArchiveDurationProvider: InfoProvider { 8 | public struct Args {} 9 | public typealias Arguments = Args 10 | 11 | public static let identifier: String = "archive_time" 12 | 13 | public let description: String = "🚚 Time to Build and Archive" 14 | public let timeInt: Int 15 | 16 | public init(timeInt: Int) { 17 | self.timeInt = timeInt 18 | } 19 | 20 | public static func extract(fromApi api: SwiftInfo, args _: Args?) throws -> ArchiveDurationProvider { 21 | let buildLog = try api.fileUtils.buildLog() 22 | let durationString = buildLog.match(regex: #"(?<=\*\* ARCHIVE SUCCEEDED \*\* \[).*?(?= sec)"#).first 23 | guard let duration = Float(durationString ?? "") else { 24 | throw error("Total archive time (ARCHIVE SUCCEEDED) not found in the logs. Did the archive fail?") 25 | } 26 | return ArchiveDurationProvider(timeInt: Int(duration * 1000)) 27 | } 28 | 29 | public func summary(comparingWith other: ArchiveDurationProvider?, args _: Args?) -> Summary { 30 | let prefix = description 31 | let numberFormatter: ((Int) -> Float) = { value in 32 | Float(value) / 1000 33 | } 34 | let stringFormatter: ((Int) -> String) = { value in 35 | "\(numberFormatter(value)) secs" 36 | } 37 | return Summary.genericFor(prefix: prefix, 38 | now: timeInt, 39 | old: other?.timeInt, 40 | increaseIsBad: true, 41 | stringValueFormatter: stringFormatter, 42 | numericValueFormatter: numberFormatter) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/SwiftInfoCore/Providers/AssetCatalog.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | protocol FileProtocol { 4 | var name: String { get } 5 | var size: Int { get } 6 | } 7 | 8 | struct AssetCatalog: FileProtocol { 9 | let name: String 10 | let size: Int 11 | let largestInnerFile: File? 12 | } 13 | 14 | struct File: FileProtocol { 15 | let name: String 16 | let size: Int 17 | } 18 | -------------------------------------------------------------------------------- /Sources/SwiftInfoCore/Providers/CodeCoverageProvider.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Code coverage percentage. 4 | /// Requirements: Test logs generated by test targets that have code coverage reports enabled in the scheme. 5 | public struct CodeCoverageProvider: InfoProvider { 6 | public struct Args { 7 | /// If provided, only these targets will be considered 8 | /// when calculating the result. The contents should be the name 9 | /// of the generated frameworks. For example, For MyLib.framework and MyApp.app, 10 | /// `targets` should be ["MyLib", "MyApp"]. 11 | /// If no args are provided, only the coverage of the main .app will be considered. 12 | public let targets: Set 13 | 14 | public init(targets: Set) { 15 | self.targets = targets 16 | } 17 | } 18 | 19 | public typealias Arguments = Args 20 | 21 | public static let identifier: String = "code_coverage" 22 | static let tempFileName = "swiftinfo_codecov.txt" 23 | 24 | static var tempFile: URL { 25 | return URL(fileURLWithPath: "./\(tempFileName)") 26 | } 27 | 28 | public let description: String = "📊 Code Coverage" 29 | public let percentageInt: Int 30 | 31 | public init(percentageInt: Int) { 32 | self.percentageInt = percentageInt 33 | } 34 | 35 | public static func extract(fromApi api: SwiftInfo, args: Args?) throws -> CodeCoverageProvider { 36 | if args == nil { 37 | log("No targets provided, getting code coverage of the main .apps", verbose: true) 38 | } 39 | let json = try getCodeCoverageJson(api: api) 40 | let targets = json["targets"] as? [[String: Any]] ?? [] 41 | let desiredTargets = try targets.filter { 42 | guard let rawName = $0["name"] as? String else { 43 | throw error("Failed to retrieve target name from xccov.") 44 | } 45 | if let targets = args?.targets, 46 | let name = rawName.components(separatedBy: ".").first { 47 | log("Getting code coverage of \(rawName)", verbose: true) 48 | let use = targets.contains(name) 49 | if use == false { 50 | log("Skipping, as \(rawName) was not included in the args.", verbose: true) 51 | } 52 | return use 53 | } else { 54 | return rawName.hasSuffix(".app") 55 | } 56 | } 57 | guard desiredTargets.isEmpty == false else { 58 | throw error("Couldn't find the desired targets in the code coverage report.") 59 | } 60 | let lineData = try desiredTargets.map { target -> (Int, Int) in 61 | guard let lines = target["executableLines"] as? Int, 62 | let covered = target["coveredLines"] as? Int else { 63 | throw error("One of the found targets was missing the coverage data!") 64 | } 65 | return (lines, covered) 66 | }.reduce((0, 0)) { 67 | ($0.0 + $1.0, $0.1 + $1.1) 68 | } 69 | let coverage = Double(lineData.1) / Double(lineData.0) 70 | let rounded = Int(1000 * coverage) 71 | return CodeCoverageProvider(percentageInt: rounded) 72 | } 73 | 74 | public static func getCodeCoverageJson(api: SwiftInfo) throws -> [String: Any] { 75 | let testLog = try api.fileUtils.testLog() 76 | if let xcode11CovPath = getCodeCoverageXcode11JsonPath(fromLogs: testLog) { 77 | let command = "xcrun xccov view --report \(xcode11CovPath) --json > \(tempFileName)" 78 | return try getCodeCoverageJson(reportFilePath: xcode11CovPath, command: command) 79 | } else if let legacyPath = getCodeCoverageLegacyJsonPath(fromLogs: testLog) { 80 | let command = "xcrun xccov view \(legacyPath) --json > \(tempFileName)" 81 | return try getCodeCoverageJson(reportFilePath: legacyPath, command: command) 82 | } else { 83 | throw error("Couldn't find code coverage report path in the logs, is it enabled?") 84 | } 85 | } 86 | 87 | private static func getCodeCoverageJson(reportFilePath _: String, command: String) throws -> [String: Any] { 88 | removeTemporaryFileIfNeeded() 89 | log("Processing code coverage report: \(command)", verbose: true) 90 | runShell(command) 91 | do { 92 | let data = try Data(contentsOf: tempFile) 93 | removeTemporaryFileIfNeeded() 94 | guard let object = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any] else { 95 | throw error("xccov failed to generate a coverage JSON. Was the report file deleted?") 96 | } 97 | return object 98 | } catch { 99 | let message = "Failed to read \(tempFile)! Error: \(error.localizedDescription)" 100 | throw self.error(message) 101 | } 102 | } 103 | 104 | public static func getCodeCoverageXcode11JsonPath(fromLogs logs: String) -> String? { 105 | let xcode11Regex = "(?<=Test session results, code coverage, and logs:\n).*" 106 | return getCodeCoverageJsonPath(fromLogs: logs, regex: xcode11Regex) 107 | } 108 | 109 | public static func getCodeCoverageLegacyJsonPath(fromLogs logs: String) -> String? { 110 | let legacyRegex = "(?<=Generated coverage report: ).*" 111 | return getCodeCoverageJsonPath(fromLogs: logs, regex: legacyRegex) 112 | } 113 | 114 | private static func getCodeCoverageJsonPath(fromLogs logs: String, regex: String) -> String? { 115 | guard let match = logs.match(regex: regex).first else { 116 | return nil 117 | } 118 | let characterSet = CharacterSet(charactersIn: " \t") 119 | return match.trimmingCharacters(in: characterSet) 120 | } 121 | 122 | static func removeTemporaryFileIfNeeded() { 123 | runShell("rm \(tempFile.path)") 124 | } 125 | 126 | static func runShell(_ command: String) { 127 | let task = Process() 128 | task.launchPath = "/bin/bash" 129 | task.arguments = ["-c", command] 130 | task.standardOutput = nil 131 | task.standardError = nil 132 | task.launch() 133 | task.waitUntilExit() 134 | } 135 | 136 | public func summary(comparingWith other: CodeCoverageProvider?, args _: Args?) -> Summary { 137 | let prefix = description 138 | let numberFormatter: ((Int) -> Float) = { value in 139 | Float(value) / 10 140 | } 141 | let stringFormatter: ((Int) -> String) = { value in 142 | "\(numberFormatter(value))%" 143 | } 144 | return Summary.genericFor(prefix: prefix, 145 | now: percentageInt, 146 | old: other?.percentageInt, 147 | increaseIsBad: false, 148 | stringValueFormatter: stringFormatter, 149 | numericValueFormatter: numberFormatter) 150 | } 151 | 152 | static func toPercentage(percentageInt: Int) -> Float { 153 | return Float(percentageInt / 10) 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /Sources/SwiftInfoCore/Providers/IPASizeProvider.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Size of the .ipa archive (not the App Store size!). 4 | /// Requirements: .ipa available in the `#{PROJECT_DIR}/build` folder. 5 | public struct IPASizeProvider: InfoProvider { 6 | public struct Args {} 7 | public typealias Arguments = Args 8 | 9 | public static let identifier: String = "ipa_size" 10 | 11 | public let description: String = "📦 Compressed App Size (.ipa)" 12 | public let size: Int 13 | 14 | public init(size: Int) { 15 | self.size = size 16 | } 17 | 18 | public static func extract(fromApi api: SwiftInfo, args _: Args?) throws -> IPASizeProvider { 19 | let fileUtils = api.fileUtils 20 | let infofileFolder = try fileUtils.infofileFolder() 21 | let buildFolder = infofileFolder + "build/" 22 | let contents = try fileUtils.fileManager.contentsOfDirectory(atPath: buildFolder) 23 | guard let ipa = contents.first(where: { $0.hasSuffix(".ipa") }) else { 24 | throw error(".ipa not found! Attempted to find .ipa at: \(buildFolder)") 25 | } 26 | let attributes = try fileUtils.fileManager.attributesOfItem(atPath: buildFolder + ipa) 27 | let fileSize = Int(attributes[.size] as? UInt64 ?? 0) 28 | return IPASizeProvider(size: fileSize) 29 | } 30 | 31 | public func summary(comparingWith other: IPASizeProvider?, args _: Args?) -> Summary { 32 | let prefix = description 33 | let stringFormatter: ((Int) -> String) = { value in 34 | let formatter = ByteCountFormatter() 35 | formatter.allowsNonnumericFormatting = false 36 | formatter.countStyle = .file 37 | return formatter.string(fromByteCount: Int64(value)) 38 | } 39 | return Summary.genericFor(prefix: prefix, 40 | now: size, 41 | old: other?.size, 42 | increaseIsBad: true, 43 | stringValueFormatter: stringFormatter) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/SwiftInfoCore/Providers/LargestAssetCatalogProvider.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// The name and size of the largest asset catalog. 4 | /// Requirements: Build logs. 5 | public struct LargestAssetCatalogProvider: InfoProvider { 6 | public struct Args {} 7 | public typealias Arguments = Args 8 | 9 | public static let identifier: String = "largest_asset_catalog_size" 10 | 11 | public let description: String = "🖼 Largest Asset Catalog" 12 | public let name: String 13 | public let size: Int 14 | 15 | public init(name: String, size: Int) { 16 | self.name = name 17 | self.size = size 18 | } 19 | 20 | public static func extract(fromApi api: SwiftInfo, args _: Args?) throws -> LargestAssetCatalogProvider { 21 | let catalogs = try TotalAssetCatalogsSizeProvider.allCatalogs(api: api) 22 | guard let largest = catalogs.max(by: { $0.size < $1.size }) else { 23 | throw error("No Asset Catalogs were found!") 24 | } 25 | return LargestAssetCatalogProvider(name: largest.name, size: largest.size) 26 | } 27 | 28 | public func summary(comparingWith other: LargestAssetCatalogProvider?, args _: Args?) -> Summary { 29 | let stringFormatter: ((Int) -> String) = { value in 30 | let formatter = ByteCountFormatter() 31 | formatter.allowsNonnumericFormatting = false 32 | formatter.countStyle = .file 33 | return formatter.string(fromByteCount: Int64(value)) 34 | } 35 | let formatted = stringFormatter(size) 36 | var prefix = "\(description): \(name) \(formatted)" 37 | let style: Summary.Style 38 | if let other = other, other.size != size { 39 | let otherFormatted = stringFormatter(other.size) 40 | prefix += " - previously \(other.name) (\(otherFormatted))" 41 | style = other.size > size ? .positive : .negative 42 | } else { 43 | style = .neutral 44 | } 45 | return Summary(text: prefix, 46 | style: style, 47 | numericValue: Float(size), 48 | stringValue: "\(formatted) (\(name))") 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/SwiftInfoCore/Providers/LargestAssetProvider.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// The largest asset in the project. Only considers files inside asset catalogs. 4 | /// Requirements: Build logs. 5 | public struct LargestAssetProvider: InfoProvider { 6 | public struct Args {} 7 | public typealias Arguments = Args 8 | 9 | public static let identifier: String = "largest_asset" 10 | 11 | public let description: String = "📷 Largest Asset" 12 | 13 | public let name: String 14 | public let size: Int 15 | 16 | public init(name: String, size: Int) { 17 | self.name = name 18 | self.size = size 19 | } 20 | 21 | public static func extract(fromApi api: SwiftInfo, args _: Args?) throws -> LargestAssetProvider { 22 | let catalogs = try TotalAssetCatalogsSizeProvider.allCatalogs(api: api) 23 | let files = catalogs.compactMap { $0.largestInnerFile } 24 | guard let maxFile = files.max(by: { $0.size < $1.size }) else { 25 | throw error("Can't find the largest asset because no assets were found.") 26 | } 27 | return LargestAssetProvider(name: maxFile.name, size: maxFile.size) 28 | } 29 | 30 | public func summary(comparingWith other: LargestAssetProvider?, args _: Args?) -> Summary { 31 | let stringFormatter: ((Int) -> String) = { value in 32 | let formatter = ByteCountFormatter() 33 | formatter.allowsNonnumericFormatting = false 34 | formatter.countStyle = .file 35 | return formatter.string(fromByteCount: Int64(value)) 36 | } 37 | let formatted = stringFormatter(size) 38 | var prefix = "\(description): \(name) \(formatted)" 39 | let style: Summary.Style 40 | if let other = other, other.size != size { 41 | let otherFormatted = stringFormatter(other.size) 42 | prefix += " - previously \(other.name) (\(otherFormatted))" 43 | style = size < other.size ? .positive : .negative 44 | } else { 45 | style = .neutral 46 | } 47 | return Summary(text: prefix, 48 | style: style, 49 | numericValue: Float(size), 50 | stringValue: "\(formatted) (\(name))") 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/SwiftInfoCore/Providers/LinesOfCodeProvider.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Number of executable lines of code. 4 | /// Requirements: Test logs generated by test targets that have code coverage reports enabled in the scheme. 5 | public struct LinesOfCodeProvider: InfoProvider { 6 | public struct Args { 7 | /// If provided, only these targets will be considered 8 | /// when calculating the result. The contents should be the name 9 | /// of the generated frameworks. For example, For MyLib.framework and MyApp.app, 10 | /// `targets` should be ["MyLib", "MyApp"]. 11 | /// If no args are provided, all targets are going to be considered. 12 | public let targets: Set 13 | 14 | public init(targets: Set) { 15 | self.targets = targets 16 | } 17 | } 18 | 19 | public typealias Arguments = Args 20 | 21 | public static let identifier: String = "lines_of_code" 22 | 23 | public let description: String = "💻 Executable Lines of Code" 24 | public let count: Int 25 | 26 | public init(count: Int) { 27 | self.count = count 28 | } 29 | 30 | public static func extract(fromApi api: SwiftInfo, args: Args?) throws -> LinesOfCodeProvider { 31 | let json = try CodeCoverageProvider.getCodeCoverageJson(api: api) 32 | let targets = json["targets"] as? [[String: Any]] ?? [] 33 | let count = try targets.reduce(0) { 34 | let current = $0 35 | let target = $1 36 | guard let rawName = target["name"] as? String, 37 | let name = rawName.components(separatedBy: ".").first else { 38 | throw error("Failed to retrieve target name from xccov.") 39 | } 40 | log("Getting lines of code for: \(rawName)", verbose: true) 41 | guard args?.targets.contains(name) != false else { 42 | log("Skipping, as \(rawName) was not included in the args.", verbose: true) 43 | return current 44 | } 45 | guard let count = target["executableLines"] as? Int else { 46 | throw error("Failed to retrieve the number of executable lines from xccov.") 47 | } 48 | return current + count 49 | } 50 | return LinesOfCodeProvider(count: count) 51 | } 52 | 53 | public func summary(comparingWith other: LinesOfCodeProvider?, args _: Args?) -> Summary { 54 | let text = description 55 | let summary = Summary.genericFor(prefix: text, now: count, old: other?.count, increaseIsBad: false) 56 | return Summary(text: summary.text, style: .neutral, numericValue: summary.numericValue, stringValue: summary.stringValue) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/SwiftInfoCore/Providers/LongestTestDurationProvider.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// The name and duration of the longest test. 4 | /// Requirements: Test logs. 5 | public struct LongestTestDurationProvider: InfoProvider { 6 | public struct Args {} 7 | public typealias Arguments = Args 8 | 9 | public static let identifier: String = "longest_test_duration" 10 | 11 | public let description: String = "⏰ Longest Test" 12 | public let name: String 13 | public let durationInt: Int 14 | 15 | public var duration: Float { 16 | return Float(durationInt) / 1000 17 | } 18 | 19 | public init(name: String, durationInt: Int) { 20 | self.name = name 21 | self.durationInt = durationInt 22 | } 23 | 24 | public static func extract(fromApi api: SwiftInfo, args _: Args?) throws -> LongestTestDurationProvider { 25 | let tests = try allTests(api: api) 26 | guard let longest = tests.max(by: { $0.1 < $1.1 }) else { 27 | throw error("Couldn't determine the longest test because no tests were found!") 28 | } 29 | return LongestTestDurationProvider(name: longest.0, 30 | durationInt: Int(longest.1 * 1000)) 31 | } 32 | 33 | public static func allTests(api: SwiftInfo) throws -> [(String, Float)] { 34 | let testLog = try api.fileUtils.testLog() 35 | let data = testLog.match(regex: #"Test.* seconds\)"#) 36 | return try data 37 | .filter { $0.contains(" passed ") } 38 | .map { str -> (String, Float) in 39 | let components = str.components(separatedBy: "'") 40 | let name = components[1] 41 | let secondsPart = components.last 42 | let timePart = secondsPart?.components(separatedBy: " seconds") ?? [] 43 | let time = timePart[timePart.count - 2].components(separatedBy: "(").last 44 | guard let duration = Float(time ?? "") else { 45 | throw error("Failed to extract test duration from this line: \(str).") 46 | } 47 | return (name, duration) 48 | } 49 | } 50 | 51 | public func summary(comparingWith other: LongestTestDurationProvider?, args _: Args?) -> Summary { 52 | var prefix = "\(description): \(name) (\(duration) secs)" 53 | let style: Summary.Style 54 | if let other = other, other.durationInt != durationInt { 55 | prefix += " - previously \(other.name) (\(other.duration) secs)" 56 | style = other.durationInt > durationInt ? .positive : .negative 57 | } else { 58 | style = .neutral 59 | } 60 | return Summary(text: prefix, 61 | style: style, 62 | numericValue: duration, 63 | stringValue: "\(duration) (\(name))") 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Sources/SwiftInfoCore/Providers/OBJCFileCountProvider.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Number of OBJ-C files and headers (for mixed OBJ-C / Swift projects). 4 | /// Requirements: Build logs. 5 | public struct OBJCFileCountProvider: InfoProvider { 6 | public struct Args {} 7 | public typealias Arguments = Args 8 | 9 | public static let identifier: String = "objc_file_count" 10 | 11 | public let description: String = "🧙‍♂️ OBJ-C .h/.m File Count" 12 | public let count: Int 13 | 14 | public init(count: Int) { 15 | self.count = count 16 | } 17 | 18 | public static func extract(fromApi api: SwiftInfo, args _: Args?) throws -> OBJCFileCountProvider { 19 | let buildLog = try api.fileUtils.buildLog() 20 | let impl = Set(buildLog.match(regex: #"CompileC.* (.*\.m)"#)) 21 | let headers = buildLog.match(regex: "CpHeader") 22 | return OBJCFileCountProvider(count: impl.count + headers.count) 23 | } 24 | 25 | public func summary(comparingWith other: OBJCFileCountProvider?, args _: Args?) -> Summary { 26 | let prefix = description 27 | return Summary.genericFor(prefix: prefix, now: count, old: other?.count, increaseIsBad: true) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/SwiftInfoCore/Providers/TargetCountProvider.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Number of targets in the build. 4 | /// Requirements: Build logs. 5 | public struct TargetCountProvider: InfoProvider { 6 | public struct Args { 7 | public enum Mode { 8 | case complainOnAdditions 9 | case complainOnRemovals 10 | case neutral 11 | } 12 | 13 | /// Determines how the summary message should treat target count changes. 14 | /// If no args are provided, .complainOnAdditions will be used. 15 | public let mode: Mode 16 | 17 | public init(mode: Mode) { 18 | self.mode = mode 19 | } 20 | } 21 | 22 | public typealias Arguments = Args 23 | 24 | public static let identifier: String = "target_count" 25 | 26 | public let description: String = "👶 Dependency Count" 27 | public let count: Int 28 | 29 | public init(count: Int) { 30 | self.count = count 31 | } 32 | 33 | public static func extract(fromApi api: SwiftInfo, args _: Args?) throws -> TargetCountProvider { 34 | let buildLog = try api.fileUtils.buildLog() 35 | let modules = Set(buildLog.match(regex: "(?<=-module-name ).*?(?= )")) 36 | return TargetCountProvider(count: modules.count) 37 | } 38 | 39 | public func summary(comparingWith other: TargetCountProvider?, args: Args?) -> Summary { 40 | let prefix = description 41 | let summary = Summary.genericFor(prefix: prefix, now: count, old: other?.count, increaseIsBad: false) 42 | guard let old = other?.count, old != count else { 43 | return summary 44 | } 45 | let mode = args?.mode ?? .complainOnAdditions 46 | switch mode { 47 | case .complainOnRemovals: 48 | return summary 49 | case .neutral: 50 | return Summary(text: summary.text, 51 | style: .neutral, 52 | numericValue: summary.numericValue, 53 | stringValue: summary.stringValue) 54 | case .complainOnAdditions: 55 | let style: Summary.Style = count > old ? .negative : .positive 56 | return Summary(text: summary.text, 57 | style: style, 58 | numericValue: summary.numericValue, 59 | stringValue: summary.stringValue) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/SwiftInfoCore/Providers/TestCountProvider.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Sum of all test target's test count. 4 | /// Requirements: Test logs (if building with Xcode) or Buck build log (if building with Buck) 5 | public struct TestCountProvider: InfoProvider { 6 | public struct Args { 7 | /// The build system that was used to test the app. 8 | /// If Xcode was used, SwiftInfo will parse the `testLogFilePath` file, 9 | /// but if Buck was used, `buckLogFilePath` will be used instead. 10 | public let buildSystem: BuildSystem 11 | 12 | public init(buildSystem: BuildSystem = .xcode) { 13 | self.buildSystem = buildSystem 14 | } 15 | } 16 | 17 | public typealias Arguments = Args 18 | 19 | public static let identifier: String = "test_count" 20 | 21 | public let description: String = "🎯 Test Count" 22 | public let count: Int 23 | 24 | public init(count: Int) { 25 | self.count = count 26 | } 27 | 28 | public static func extract(fromApi api: SwiftInfo, args: Args?) throws -> TestCountProvider { 29 | let count: Int 30 | if args?.buildSystem == .buck { 31 | count = try getCountFromBuck(api) 32 | } else { 33 | count = try getCountFromXcode(api) 34 | } 35 | guard count > 0 else { 36 | fail("Failing because 0 tests were found, and this is probably not intentional.") 37 | } 38 | return TestCountProvider(count: count) 39 | } 40 | 41 | static func getCountFromXcode(_ api: SwiftInfo) throws -> Int { 42 | let testLog = try api.fileUtils.testLog() 43 | return testLog.insensitiveMatch(regex: "Test Case '.*' passed").count + 44 | testLog.insensitiveMatch(regex: "Test Case '.*' failed").count 45 | } 46 | 47 | static func getCountFromBuck(_ api: SwiftInfo) throws -> Int { 48 | let buckLog = try api.fileUtils.buckLog() 49 | let regexString = "s *([0-9]*) Passed *([0-9]*) Skipped *([0-9]*) Failed" 50 | let results = buckLog.matchResults(regex: regexString) 51 | let passed = results.compactMap { Int($0.captureGroup(1, originalString: buckLog)) } 52 | let skipped = results.compactMap { Int($0.captureGroup(2, originalString: buckLog)) } 53 | let failed = results.compactMap { Int($0.captureGroup(3, originalString: buckLog)) } 54 | return (passed + skipped + failed).reduce(0, +) 55 | } 56 | 57 | public func summary(comparingWith other: TestCountProvider?, args _: Args?) -> Summary { 58 | let prefix = description 59 | return Summary.genericFor(prefix: prefix, now: count, old: other?.count, increaseIsBad: false) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/SwiftInfoCore/Providers/TotalAssetCatalogsSizeProvider.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// The sum of the size of all asset catalogs. 4 | /// Requirements: Build logs. 5 | public struct TotalAssetCatalogsSizeProvider: InfoProvider { 6 | public struct Args {} 7 | public typealias Arguments = Args 8 | 9 | public static let identifier: String = "total_asset_catalogs_size" 10 | 11 | public let description: String = "🎨 Total Asset Catalogs Size" 12 | public let size: Int 13 | 14 | public init(size: Int) { 15 | self.size = size 16 | } 17 | 18 | public static func extract(fromApi api: SwiftInfo, args _: Args?) throws -> TotalAssetCatalogsSizeProvider { 19 | let catalogs = try allCatalogs(api: api) 20 | let total = catalogs.map { $0.size }.reduce(0, +) 21 | return TotalAssetCatalogsSizeProvider(size: total) 22 | } 23 | 24 | static func allCatalogsPaths(api: SwiftInfo) throws -> [String] { 25 | let buildLog = try api.fileUtils.buildLog() 26 | let compileRows = buildLog.match(regex: "CompileAssetCatalog.*") 27 | let catalogs: [String] = compileRows.map { (row: String) -> [String] in 28 | let formatted: String = row.replacingEscapedSpaces 29 | let catalogs: [String] = formatted.components(separatedBy: " ").filter { $0.hasSuffix(".xcassets") } 30 | return catalogs.compactMap { $0.removingPlaceholder } 31 | }.flatMap { $0 } 32 | let uniqueCatalogs: [String] = Array(Set(catalogs)) 33 | return uniqueCatalogs 34 | } 35 | 36 | static func allCatalogs(api: SwiftInfo) throws -> [AssetCatalog] { 37 | let catalogs = try allCatalogsPaths(api: api) 38 | let sizes = try catalogs.map { try folderSize(ofCatalog: $0, api: api) } 39 | let result = zip(catalogs, sizes).map { ($0.0, $0.1) } 40 | return result.map { AssetCatalog(name: $0.0, size: $0.1.0, largestInnerFile: $0.1.1) } 41 | } 42 | 43 | static func folderSize(ofCatalog catalog: String, api: SwiftInfo) throws -> (size: Int, largestInnerFile: File?) { 44 | let fileManager = api.fileUtils.fileManager 45 | let enumerator: FileManager.DirectoryEnumerator? 46 | if fileManager.enumerator(atPath: catalog)?.nextObject() == nil { 47 | // Xcode's new build system 48 | let infofileFolder = try api.fileUtils.infofileFolder() 49 | enumerator = fileManager.enumerator(atPath: infofileFolder + catalog) 50 | } else { 51 | // Legacy build 52 | enumerator = fileManager.enumerator(atPath: catalog) 53 | } 54 | var fileSize = 0 55 | var largestInnerFile: File? 56 | while let next = enumerator?.nextObject() as? String { 57 | let name = catalog + "/" + next 58 | let attributes = try fileManager.attributesOfItem(atPath: name) 59 | let size = Int(attributes[.size] as? UInt64 ?? 0) 60 | fileSize += size 61 | if size > (largestInnerFile?.size ?? 0) { 62 | largestInnerFile = File(name: name, size: size) 63 | } 64 | } 65 | return (fileSize, largestInnerFile) 66 | } 67 | 68 | public func summary(comparingWith other: TotalAssetCatalogsSizeProvider?, args _: Args?) -> Summary { 69 | let prefix = description 70 | let stringFormatter: ((Int) -> String) = { value in 71 | let formatter = ByteCountFormatter() 72 | formatter.allowsNonnumericFormatting = false 73 | formatter.countStyle = .file 74 | return formatter.string(fromByteCount: Int64(value)) 75 | } 76 | return Summary.genericFor(prefix: prefix, now: size, old: other?.size, increaseIsBad: true, stringValueFormatter: stringFormatter) 77 | } 78 | } 79 | 80 | extension String { 81 | private var spacedFolderPlaceholder: String { 82 | return "\u{0}" 83 | } 84 | 85 | var replacingEscapedSpaces: String { 86 | return replacingOccurrences(of: "\\ ", with: spacedFolderPlaceholder) 87 | } 88 | 89 | var removingPlaceholder: String { 90 | return replacingOccurrences(of: spacedFolderPlaceholder, with: " ") 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Sources/SwiftInfoCore/Providers/TotalTestDurationProvider.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Time it took to build and run all tests. 4 | /// Requirements: Test logs. 5 | public struct TotalTestDurationProvider: InfoProvider { 6 | public struct Args {} 7 | public typealias Arguments = Args 8 | 9 | public static let identifier: String = "total_test_duration" 10 | 11 | public let description: String = "🛏 Time to Build and Run Tests" 12 | public let durationInt: Int 13 | 14 | public init(durationInt: Int) { 15 | self.durationInt = durationInt 16 | } 17 | 18 | public static func extract(fromApi api: SwiftInfo, args _: Args?) throws -> TotalTestDurationProvider { 19 | let testLog = try api.fileUtils.testLog() 20 | let durationString = testLog.match(regex: #"(?<=\*\* TEST SUCCEEDED \*\* \[).*?(?= sec)"#).first 21 | guard let duration = Float(durationString ?? "") else { 22 | throw error("Total test duration (TEST SUCCEEDED) not found in the logs. Did the tests fail?") 23 | } 24 | return TotalTestDurationProvider(durationInt: Int(duration * 1000)) 25 | } 26 | 27 | public func summary(comparingWith other: TotalTestDurationProvider?, args _: Args?) -> Summary { 28 | let prefix = description 29 | let numberFormatter: ((Int) -> Float) = { value in 30 | Float(value) / 1000 31 | } 32 | let stringFormatter: ((Int) -> String) = { value in 33 | "\(numberFormatter(value)) secs" 34 | } 35 | return Summary.genericFor(prefix: prefix, 36 | now: durationInt, 37 | old: other?.durationInt, 38 | increaseIsBad: true, 39 | stringValueFormatter: stringFormatter, 40 | numericValueFormatter: numberFormatter) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/SwiftInfoCore/Providers/WarningCountProvider.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Number of warnings in a build. 4 | /// Requirements: Build logs. 5 | public struct WarningCountProvider: InfoProvider { 6 | public struct Args {} 7 | public typealias Arguments = Args 8 | 9 | public static let identifier: String = "warning_count" 10 | 11 | public let description: String = "⚠️ Warning Count" 12 | public let count: Int 13 | 14 | public init(count: Int) { 15 | self.count = count 16 | } 17 | 18 | public static func extract(fromApi api: SwiftInfo, args _: Args?) throws -> WarningCountProvider { 19 | let buildLog = try api.fileUtils.buildLog() 20 | let results = buildLog.match(regex: "(: warning:.*\n)|((warning:.*\n))") 21 | let count = Set(results).count 22 | return WarningCountProvider(count: count) 23 | } 24 | 25 | public func summary(comparingWith other: WarningCountProvider?, args _: Args?) -> Summary { 26 | let prefix = description 27 | return Summary.genericFor(prefix: prefix, now: count, old: other?.count, increaseIsBad: true) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/SwiftInfoCore/Regex.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension String { 4 | func matchResults(regex: String, options: NSRegularExpression.Options = []) -> [NSTextCheckingResult] { 5 | let regex = try! NSRegularExpression(pattern: regex, options: options) 6 | let nsString = self as NSString 7 | return regex.matches(in: self, options: [], range: NSMakeRange(0, nsString.length)) 8 | } 9 | 10 | func match(regex: String, options: NSRegularExpression.Options = []) -> [String] { 11 | return matchResults(regex: regex, options: options).map { 12 | String(self[Range($0.range, in: self)!]) 13 | } 14 | } 15 | 16 | func insensitiveMatch(regex: String) -> [String] { 17 | return match(regex: regex, options: [.caseInsensitive]) 18 | } 19 | } 20 | 21 | extension NSTextCheckingResult { 22 | func captureGroup(_ index: Int, originalString: String) -> String { 23 | let groupRange = range(at: index) 24 | let groupStartIndex = originalString.index(originalString.startIndex, 25 | offsetBy: groupRange.location) 26 | let groupEndIndex = originalString.index(groupStartIndex, 27 | offsetBy: groupRange.length) 28 | let substring = originalString[groupStartIndex ..< groupEndIndex] 29 | return String(substring) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/SwiftInfoCore/Runner.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum Runner { 4 | public static func getCoreSwiftCArguments(fileUtils: FileUtils, 5 | toolchainPath: String, 6 | processInfoArgs: [String]) -> [String] { 7 | let include = fileUtils.toolFolder + "/../include/swiftinfo" 8 | return [ 9 | "swiftc", 10 | "--driver-mode=swift", // Don't generate a binary, just run directly. 11 | "-L", // Link with SwiftInfoCore manually. 12 | include, 13 | "-I", 14 | include, 15 | "-lSwiftInfoCore", 16 | "-Xcc", 17 | "-fmodule-map-file=\(include)/Csourcekitd/include/module.modulemap", 18 | "-I", 19 | "\(include)/Csourcekitd/include", 20 | (try! fileUtils.infofileFolder()) + "Infofile.swift", 21 | "-toolchain", 22 | "\(toolchainPath)", 23 | // Swift 5.5 (from Xcode 13+) uses the new swift-driver which doesn't support -toolchain arg 24 | // Disabling the driver for now as a workaround so it works with Swift 5.5 and older versions 25 | "-disallow-use-new-driver", 26 | ] + Array(processInfoArgs.dropFirst()) // Route SwiftInfo args to the sub process 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/SwiftInfoCore/SlackFormatter.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct SlackFormatter { 4 | public init() {} 5 | 6 | public func format(output: Output, titlePrefix: String?, projectInfo: ProjectInfo) -> (json: [String: Any], message: String) { 7 | let errors = output.errors 8 | let errorMessage = errors.isEmpty ? "" : "\nErrors:\n\(errors.joined(separator: "\n"))" 9 | 10 | let title = (titlePrefix ?? "SwiftInfo results for ") + projectInfo.description + ":" + errorMessage 11 | let description = output.summaries.map { $0.text }.joined(separator: "\n") 12 | let message = title + "\n" + description 13 | 14 | let json = output.summaries.map { $0.slackDictionary } 15 | return (["text": title, "attachments": json], message) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/SwiftInfoCore/SourceKitWrapper/DLHandle.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Swift.org open source project 4 | // 5 | // Copyright (c) 2014 - 2018 Apple Inc. and the Swift project authors 6 | // Licensed under Apache License v2.0 with Runtime Library Exception 7 | // 8 | // See https://swift.org/LICENSE.txt for license information 9 | // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors 10 | // 11 | //===----------------------------------------------------------------------===// 12 | 13 | import Foundation 14 | 15 | // Adapted from SourceKit-LSP. 16 | 17 | public final class DLHandle { 18 | var rawValue: UnsafeMutableRawPointer? 19 | 20 | init(rawValue: UnsafeMutableRawPointer) { 21 | self.rawValue = rawValue 22 | } 23 | 24 | deinit { 25 | precondition(rawValue == nil, "DLHandle must be closed or explicitly leaked before destroying") 26 | } 27 | 28 | public func close() throws { 29 | if let handle = rawValue { 30 | guard dlclose(handle) == 0 else { 31 | throw DLError.dlclose(dlerror() ?? "unknown error") 32 | } 33 | } 34 | rawValue = nil 35 | } 36 | 37 | public func leak() { 38 | rawValue = nil 39 | } 40 | } 41 | 42 | public struct DLOpenFlags: RawRepresentable, OptionSet { 43 | public static let lazy: DLOpenFlags = DLOpenFlags(rawValue: RTLD_LAZY) 44 | public static let now: DLOpenFlags = DLOpenFlags(rawValue: RTLD_NOW) 45 | public static let local: DLOpenFlags = DLOpenFlags(rawValue: RTLD_LOCAL) 46 | public static let global: DLOpenFlags = DLOpenFlags(rawValue: RTLD_GLOBAL) 47 | 48 | // Platform-specific flags. 49 | #if os(macOS) 50 | public static let first: DLOpenFlags = DLOpenFlags(rawValue: RTLD_FIRST) 51 | public static let deepBind: DLOpenFlags = DLOpenFlags(rawValue: 0) 52 | #else 53 | public static let first: DLOpenFlags = DLOpenFlags(rawValue: 0) 54 | public static let deepBind: DLOpenFlags = DLOpenFlags(rawValue: RTLD_DEEPBIND) 55 | #endif 56 | 57 | public var rawValue: Int32 58 | 59 | public init(rawValue: Int32) { 60 | self.rawValue = rawValue 61 | } 62 | } 63 | 64 | public enum DLError: Swift.Error { 65 | case dlopen(String) 66 | case dlclose(String) 67 | } 68 | 69 | public func dlopen(_ path: String?, mode: DLOpenFlags) throws -> DLHandle { 70 | guard let handle = dlopen(path, mode.rawValue) else { 71 | throw DLError.dlopen(dlerror() ?? "unknown error") 72 | } 73 | return DLHandle(rawValue: handle) 74 | } 75 | 76 | public func dlsym(_ handle: DLHandle, symbol: String) -> T? { 77 | guard let ptr = dlsym(handle.rawValue!, symbol) else { 78 | return nil 79 | } 80 | return unsafeBitCast(ptr, to: T.self) 81 | } 82 | 83 | public func dlclose(_ handle: DLHandle) throws { 84 | try handle.close() 85 | } 86 | 87 | public func dlerror() -> String? { 88 | if let err: UnsafeMutablePointer = dlerror() { 89 | return String(cString: err) 90 | } 91 | return nil 92 | } 93 | -------------------------------------------------------------------------------- /Sources/SwiftInfoCore/Summary.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A `Summary` is a wrapper struct that represents the result of comparing two instances of a provider. 4 | public struct Summary: Codable, Hashable { 5 | /// The sentimental result of a `Summary`. 6 | public enum Style { 7 | /// Indicates that the result was good. 8 | case positive 9 | /// Indicates that the result was neutral. 10 | case neutral 11 | /// Indicates that the result was bad. 12 | case negative 13 | 14 | /// The attributed hex color of this style. 15 | var hexColor: String { 16 | switch self { 17 | case .positive: 18 | return "#36a64f" 19 | case .neutral: 20 | return "#757575" 21 | case .negative: 22 | return "#c41919" 23 | } 24 | } 25 | } 26 | 27 | /// The descriptive result of this summary. 28 | let text: String 29 | /// A hex value that represents the result of this summary. 30 | let color: String 31 | /// The numeric value that represents this summary, to be used in tools like SwiftInfo-Reader. 32 | let numericValue: Float 33 | /// The string value that represents this summary, to be used in tools like SwiftInfo-Reader. 34 | let stringValue: String 35 | 36 | var slackDictionary: [String: Any] { 37 | return ["text": text, "color": color] 38 | } 39 | 40 | /// Creates a `Summary`. 41 | /// 42 | /// - Parameters: 43 | /// - text: The description of this summary. 44 | /// - style: The sentimental result of this summary. See `Summary.Style` for more information. 45 | /// - numericValue: The numeric value that represents this summary, to be used in tools like SwiftInfo-Reader. 46 | /// For example, if a build increased the number of tests by 3, `numericValue` should be 3. 47 | /// - stringValue: The string value that represents this summary. For example, 48 | /// if a build increased the number of tests by 3, `stringValue` can be either 3 or 'Three'. This is used only 49 | /// for visual purposes. 50 | public init(text: String, style: Style, numericValue: Float, stringValue: String) { 51 | self.text = text 52 | color = style.hexColor 53 | self.numericValue = numericValue 54 | self.stringValue = stringValue 55 | } 56 | 57 | /// Creates a basic `Summary` that differs depending if a provider's value increased, decreased or stayed the same. 58 | /// Here's an example: 59 | /// Build Time: *Increased* by 3 (100 seconds) 60 | /// 61 | /// - Parameters: 62 | /// - prefix: The prefix of the message. Ideally, this should be the description of your provider. 63 | /// - now: The current numeric value of the provider. 64 | /// - old: The previous numeric value of the provider. 65 | /// - increaseIsBad: If set to true, increases in value will use the `.negative` `Summary.Style`. 66 | /// - stringValueFormatter: (Optional) The closure that translates the numeric value to a visual string. 67 | /// By default, the behavior is to simply cast the number to a String. 68 | /// - numericValueFormatter: (Optional) The closure that translates the numeric value to a Float. 69 | /// Float is the type used by reader tools like SwiftInfo-Reader, and by default, the behavior is to simply 70 | /// convert the number to a Float. 71 | /// - difference: (Optional) The closure that shows how to calculate the numerical difference between the providers. 72 | /// The first argument is the **new** value, and the second is the **old** one. 73 | /// By default, this closure is { abs(old - new) }. 74 | public static func genericFor( 75 | prefix: String, 76 | now: T, 77 | old: T?, 78 | increaseIsBad: Bool, 79 | stringValueFormatter: ((T) -> String)? = nil, 80 | numericValueFormatter: ((T) -> Float)? = nil, 81 | difference: ((T, T) -> T)? = nil 82 | ) -> Summary { 83 | let stringFormatter = stringValueFormatter ?? { "\($0)" } 84 | let numberFormatter = numericValueFormatter ?? { Float($0) } 85 | let difference = difference ?? { abs($1 - $0) } 86 | func result(text: String, style: Style) -> Summary { 87 | return Summary(text: text, 88 | style: style, 89 | numericValue: numberFormatter(now), 90 | stringValue: stringFormatter(now)) 91 | } 92 | guard let old = old else { 93 | return result(text: prefix + ": \(stringFormatter(now))", style: .neutral) 94 | } 95 | guard now != old else { 96 | return result(text: prefix + ": Still at \(stringFormatter(now))", style: .neutral) 97 | } 98 | let modifier: String 99 | let style: Style 100 | if now > old { 101 | modifier = ": *Increased* by" 102 | style = increaseIsBad ? .negative : .positive 103 | } else if now < old { 104 | modifier = ": *Reduced* by" 105 | style = increaseIsBad ? .positive : .negative 106 | } else { 107 | modifier = ": Still at" 108 | style = .neutral 109 | } 110 | let diff = difference(now, old) 111 | let text = prefix + "\(modifier) \(stringFormatter(diff)) (\(stringFormatter(now)))" 112 | return result(text: text, style: style) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /Sources/SwiftInfoCore/SwiftInfo.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// The base API for SwiftInfo operations. 4 | public struct SwiftInfo { 5 | /// The information about the current project. 6 | public let projectInfo: ProjectInfo 7 | 8 | /// Utilities for opening and saving files. 9 | public let fileUtils: FileUtils 10 | 11 | /// A HTTP Client that sends synchronous POST requests. 12 | public let client: HTTPClient 13 | 14 | /// An instance of SourceKit. 15 | public let sourceKit: SourceKit 16 | 17 | let slackFormatter: SlackFormatter 18 | 19 | public init(projectInfo: ProjectInfo, 20 | fileUtils: FileUtils = .init(), 21 | slackFormatter: SlackFormatter = .init(), 22 | client: HTTPClient = .init(), 23 | sourceKit: SourceKit? = nil) { 24 | self.projectInfo = projectInfo 25 | self.fileUtils = fileUtils 26 | self.slackFormatter = slackFormatter 27 | self.client = client 28 | if let sourceKit = sourceKit { 29 | self.sourceKit = sourceKit 30 | } else { 31 | let toolchain = UserDefaults.standard.string(forKey: "toolchain") ?? "" 32 | self.sourceKit = SourceKit(path: toolchain) 33 | } 34 | } 35 | 36 | /// Executes a provider with an optional set of additional arguments. 37 | /// 38 | /// - Parameters: 39 | /// - provider: The provider metatype to execute. 40 | /// - args: (Optional) The arguments to send to the provider, if applicable. 41 | /// 42 | /// - Returns: The output of this provider. 43 | public func extract(_ provider: T.Type, 44 | args: T.Arguments? = nil) -> Output { 45 | do { 46 | log("Extracting \(provider.identifier)") 47 | let extracted = try provider.extract(fromApi: self, args: args) 48 | log("\(provider.identifier): Parsing previously extracted info", verbose: true) 49 | let other = try fileUtils.lastOutput().extractedInfo(ofType: provider) 50 | log("\(provider.identifier): Comparing with previously extracted info", verbose: true) 51 | let summary = extracted.summary(comparingWith: other, args: args) 52 | log("\(provider.identifier): Finishing", verbose: true) 53 | let info = ExtractedInfo(data: extracted, summary: summary) 54 | return try Output(info: info) 55 | } catch { 56 | let message = "**\(provider.identifier):** \(error.localizedDescription)" 57 | log(message) 58 | return Output(rawDictionary: [:], 59 | summaries: [], 60 | errors: [message]) 61 | } 62 | } 63 | 64 | /// Sends an output to slack. 65 | /// 66 | /// - Parameters: 67 | /// - output: The output to send. 68 | /// - webhookUrl: The Slack webhook URL used to send the message. 69 | /// - titlePrefix: (Optional) The string to prepend to the message title such as team-mention, etc. 70 | public func sendToSlack(output: Output, webhookUrl: String, titlePrefix: String? = nil) { 71 | log("Sending to Slack") 72 | log("Slack Webhook: \(webhookUrl)", verbose: true) 73 | let formatted = slackFormatter.format(output: output, titlePrefix: titlePrefix, projectInfo: projectInfo) 74 | client.syncPost(urlString: webhookUrl, json: formatted.json) 75 | } 76 | 77 | /// Uses the SlackFormatter to format the output and print it. 78 | /// 79 | /// - Parameters: 80 | /// - output: The output to print. 81 | public func print(output: Output) { 82 | let formatted = slackFormatter.format(output: output, titlePrefix: nil, projectInfo: projectInfo) 83 | // We print directly so that `log()`'s conditions don't interfere. 84 | // This is meant to be used with `danger-SwiftInfo` for printing to pull requests. 85 | Swift.print(formatted.message) 86 | } 87 | 88 | /// Saves the current output to the device. 89 | /// 90 | /// - Parameters: 91 | /// - output: The output to save. The file will be saved as {Infofile path}/SwiftInfo-output.json. 92 | /// - timestamp: (Optional) A custom timestamp for the output. 93 | public func save(output: Output, 94 | timestamp: TimeInterval = Date().timeIntervalSince1970) { 95 | log("Saving output to disk") 96 | var dict = output.rawDictionary 97 | dict["swiftinfo_run_project_info"] = [ 98 | "xcodeproj": projectInfo.xcodeproj, 99 | "target": projectInfo.target, 100 | "configuration": projectInfo.configuration, 101 | "versionString": (try? projectInfo.getVersionString()) ?? "(Failed to parse version)", 102 | "buildNumber": (try? projectInfo.getBuildNumber()) ?? "(Failed to parse build number)", 103 | "description": projectInfo.description, 104 | "timestamp": timestamp, 105 | ] 106 | do { 107 | let outputFile = try fileUtils.outputArray() 108 | try fileUtils.save(output: [dict] + outputFile) 109 | } catch { 110 | fail(error.localizedDescription) 111 | } 112 | } 113 | } 114 | 115 | /// Crashes SwiftInfo with a message. 116 | public func fail(_ message: String) -> Never { 117 | print("SwiftInfo crashed. Reason: \(message)") 118 | exit(-1) 119 | } 120 | -------------------------------------------------------------------------------- /SwiftInfo.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'SwiftInfo' 3 | s.module_name = 'SwiftInfo' 4 | s.version = '2.6.0' 5 | s.license = { type: 'MIT', file: 'LICENSE' } 6 | s.summary = 'Extract and analyze the evolution of an iOS app\'s code.' 7 | s.homepage = 'https://github.com/rockbruno/SwiftInfo' 8 | s.authors = { 'Bruno Rocha' => 'brunorochaesilva@gmail.com' } 9 | s.social_media_url = 'https://twitter.com/rockbruno_' 10 | s.source = { http: "https://github.com/rockbruno/SwiftInfo/releases/download/#{s.version}/SwiftInfo.zip" } 11 | s.preserve_paths = '*' 12 | s.exclude_files = '**/file.zip' 13 | end 14 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import SwiftInfoTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += SwiftInfoTests.__allTests() 7 | 8 | XCTMain(tests) 9 | -------------------------------------------------------------------------------- /Tests/SwiftInfoTests/CoreTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SwiftInfoCore 3 | 4 | final class CoreTests: XCTestCase { 5 | func testFullRun() { 6 | let api = SwiftInfo.mock() 7 | let outputPath = "SwiftInfo-output/SwiftInfoOutput.json" 8 | XCTAssertNil(api.mockFileManager.stringContents(atPath: outputPath)) 9 | var currentOutput = [String: Any]() 10 | currentOutput["data"] = [ 11 | [ 12 | "mock_provider": [ 13 | "data": [ 14 | "value": 5, 15 | "description": "Fake provider for testing purposes" 16 | ], 17 | "summary": [ 18 | "text": "-", 19 | "color": "#757575" 20 | ] 21 | ] 22 | ] 23 | ] 24 | let currentOutputData = try! JSONSerialization.data(withJSONObject: currentOutput, 25 | options: []) 26 | let currentOutputString = String(data: currentOutputData, encoding: .utf8)! 27 | api.mockFileManager.add(file: outputPath, contents: currentOutputString) 28 | XCTAssertEqual(api.mockFileManager.stringContents(atPath: outputPath), 29 | currentOutputString) 30 | let output = api.extract(MockInfoProvider.self) 31 | api.save(output: output, timestamp: 0) 32 | var expectedOutput = currentOutput 33 | let dataArray = expectedOutput["data"] as! [[String: Any]] 34 | let newDataArray: [[String: Any]] = [[ 35 | "swiftinfo_run_project_info": [ 36 | "buildNumber": "1", 37 | "configuration": "Mock-Debug", 38 | "description": "Mock 1.0 (1) - Mock-Debug", 39 | "target": "Mock", 40 | "versionString": "1.0", 41 | "xcodeproj": "Mock.xcproject", 42 | "timestamp": 0 43 | ], 44 | "mock_provider": [ 45 | "data": [ 46 | "value": 10, 47 | "description": "Fake provider for testing purposes" 48 | ], 49 | "summary": [ 50 | "text": "Old: 5, New: 10", 51 | "color": "#757575", 52 | "numericValue": 0, 53 | "stringValue": "a" 54 | ] 55 | ] 56 | ]] + dataArray 57 | expectedOutput["data"] = newDataArray 58 | let newOutputData = api.mockFileManager.dataContents(atPath: outputPath)! 59 | let newOutput = try! JSONSerialization.jsonObject(with: newOutputData, options: []) as! [String: Any] 60 | XCTAssertEqual(NSDictionary(dictionary: expectedOutput), 61 | NSDictionary(dictionary: newOutput)) 62 | } 63 | 64 | func testFullRunWithEmptyOutput() { 65 | let api = SwiftInfo.mock() 66 | let outputPath = "SwiftInfo-output/SwiftInfoOutput.json" 67 | XCTAssertNil(api.mockFileManager.stringContents(atPath: outputPath)) 68 | let output = api.extract(MockInfoProvider.self) 69 | api.save(output: output, timestamp: 0) 70 | let expectedOutput: [String: Any] = [ 71 | "data": [[ 72 | "swiftinfo_run_project_info": [ 73 | "buildNumber": "1", 74 | "configuration": "Mock-Debug", 75 | "description": "Mock 1.0 (1) - Mock-Debug", 76 | "target": "Mock", 77 | "versionString": "1.0", 78 | "xcodeproj": "Mock.xcproject", 79 | "timestamp": 0 80 | ], 81 | "mock_provider": [ 82 | "data": [ 83 | "value": 10, 84 | "description": "Fake provider for testing purposes" 85 | ], 86 | "summary": [ 87 | "text": "10", 88 | "color": "#757575", 89 | "numericValue": 0, 90 | "stringValue": "a" 91 | ] 92 | ] 93 | ]] 94 | ] 95 | let newOutputData = api.mockFileManager.dataContents(atPath: outputPath)! 96 | let newOutput = try! JSONSerialization.jsonObject(with: newOutputData, options: []) as! [String: Any] 97 | XCTAssertEqual(NSDictionary(dictionary: expectedOutput), 98 | NSDictionary(dictionary: newOutput)) 99 | } 100 | } 101 | 102 | struct MockInfoProvider: InfoProvider { 103 | 104 | public struct Args {} 105 | public typealias Arguments = Args 106 | 107 | static let identifier: String = "mock_provider" 108 | let description: String = "Fake provider for testing purposes" 109 | let value: Int 110 | 111 | static func extract(fromApi api: SwiftInfo, args: Args?) throws -> MockInfoProvider { 112 | return MockInfoProvider(value: 10) 113 | } 114 | 115 | func summary(comparingWith other: MockInfoProvider?, args: Args?) -> Summary { 116 | guard let other = other else { 117 | return Summary(text: "\(value)", style: .neutral, numericValue: 0, stringValue: "a") 118 | } 119 | return Summary(text: "Old: \(other.value), New: \(value)", style: .neutral, numericValue: 0, stringValue: "a") 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /Tests/SwiftInfoTests/FileUtilsTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SwiftInfoCore 3 | 4 | final class FileUtilsTests: XCTestCase { 5 | 6 | var fileManager: MockFileManager! 7 | var fileOpener: MockFileOpener! 8 | var fileUtils: FileUtils! 9 | 10 | override func setUp() { 11 | super.setUp() 12 | FileUtils.buildLogFilePath = "" 13 | FileUtils.testLogFilePath = "" 14 | fileManager = MockFileManager() 15 | fileOpener = MockFileOpener(mockFM: fileManager) 16 | fileUtils = FileUtils(fileManager: fileManager, fileOpener: fileOpener) 17 | } 18 | 19 | func testInfofileFinder() { 20 | XCTAssertNil(try? fileUtils.infofileFolder()) 21 | fileManager.add(file: "../../Infofile.swift", contents: "") 22 | XCTAssertNotNil(try? fileUtils.infofileFolder()) 23 | XCTAssertEqual(try! fileUtils.infofileFolder(), "../../") 24 | } 25 | 26 | func testBuildLogs() { 27 | fileManager.add(file: "./Infofile.swift", contents: "") 28 | let logPath = "builds/build.log" 29 | let contents = "MY LOG" 30 | XCTAssertNil((try? fileUtils.buildLog())) 31 | fileManager.add(file: logPath, contents: contents) 32 | XCTAssertNil((try? fileUtils.buildLog())) 33 | FileUtils.buildLogFilePath = logPath 34 | XCTAssertEqual((try? fileUtils.buildLog()), contents) 35 | } 36 | 37 | func testTestLogs() { 38 | fileManager.add(file: "./Infofile.swift", contents: "") 39 | let logPath = "builds/test.log" 40 | let contents = "MY TEST LOG" 41 | XCTAssertNil((try? fileUtils.testLog())) 42 | fileManager.add(file: logPath, contents: contents) 43 | XCTAssertNil((try? fileUtils.testLog())) 44 | FileUtils.testLogFilePath = logPath 45 | XCTAssertEqual((try? fileUtils.testLog()), contents) 46 | } 47 | 48 | func testLastOutput() { 49 | fileManager.add(file: "../../Infofile.swift", contents: "") 50 | XCTAssertEqual(try? fileUtils.outputFileFolder(), "../../SwiftInfo-output/") 51 | XCTAssertEqual(try? fileUtils.outputFileURL().relativePath, "../../SwiftInfo-output/SwiftInfoOutput.json") 52 | fileManager.add(file: "../../SwiftInfo-output/SwiftInfoOutput.json", contents: "{\"data\":[{\"a\": \"b\"}]}") 53 | let last = try? fileUtils.lastOutput() 54 | XCTAssertEqual((last?.rawDictionary["a"] as? String), "b") 55 | } 56 | 57 | func testSaveOutput() { 58 | fileManager.add(file: "../../Infofile.swift", contents: "") 59 | XCTAssertEqual(fileManager.createdDicts, []) 60 | let dict: [[String: Any]] = [["b": "c"]] 61 | try! fileUtils.save(output: dict) 62 | XCTAssertEqual(fileManager.createdDicts.contains(try! fileUtils.outputFileFolder()), true) 63 | let last = try! fileUtils.lastOutput() 64 | XCTAssertEqual((last.rawDictionary["b"] as? String), "c") 65 | XCTAssertNotNil(try! fileUtils.fullOutput()["data"]) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Tests/SwiftInfoTests/MockFileManager.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class MockFileManager: FileManager { 4 | 5 | private(set) var files = [String: String]() 6 | private(set) var plists = [String: NSDictionary]() 7 | private(set) var attributes = [String: [FileAttributeKey : Any]]() 8 | var createdDicts = Set() 9 | 10 | func dataContents(atPath path: String) -> Data? { 11 | guard let file = stringContents(atPath: path) else { 12 | return nil 13 | } 14 | return file.data(using: .utf8)! 15 | } 16 | 17 | override func contentsOfDirectory(atPath path: String) throws -> [String] { 18 | return files.filter { $0.key.hasPrefix(path) } 19 | .compactMap { $0.key.components(separatedBy: path).last } 20 | } 21 | 22 | override func attributesOfItem(atPath path: String) throws -> [FileAttributeKey : Any] { 23 | return attributes[path] ?? [:] 24 | } 25 | 26 | func stringContents(atPath path: String) -> String? { 27 | return files[path] 28 | } 29 | 30 | func plistContents(atPath path: String) -> NSDictionary? { 31 | return plists[path] 32 | } 33 | 34 | func add(file: String, contents: String) { 35 | files[file] = contents 36 | } 37 | 38 | func add(plist: NSDictionary, file: String) { 39 | plists[file] = plist 40 | } 41 | 42 | 43 | func add(attributes: [FileAttributeKey: Any], file: String) { 44 | self.attributes[file] = attributes 45 | } 46 | 47 | override func fileExists(atPath path: String) -> Bool { 48 | return files[path] != nil 49 | } 50 | 51 | override func createDirectory(atPath path: String, withIntermediateDirectories createIntermediates: Bool, attributes: [FileAttributeKey : Any]? = nil) throws { 52 | createdDicts.insert(path) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Tests/SwiftInfoTests/MockFileOpener.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftInfoCore 3 | 4 | final class MockFileOpener: FileOpener { 5 | 6 | enum Errors: Error { 7 | case noData 8 | } 9 | 10 | let mockFM: MockFileManager 11 | 12 | init(mockFM: MockFileManager) { 13 | self.mockFM = mockFM 14 | } 15 | 16 | override func stringContents(ofUrl url: URL) throws -> String { 17 | guard let file = mockFM.stringContents(atPath: url.relativePath) else { 18 | throw Errors.noData 19 | } 20 | return file 21 | } 22 | 23 | override func dataContents(ofUrl url: URL) throws -> Data { 24 | guard let file = mockFM.dataContents(atPath: url.relativePath) else { 25 | throw Errors.noData 26 | } 27 | return file 28 | } 29 | 30 | override func plistContents(ofPath path: String) -> NSDictionary? { 31 | guard let file = mockFM.plistContents(atPath: path) else { 32 | return nil 33 | } 34 | return file 35 | } 36 | 37 | override func write(data: Data, toUrl url: URL) throws { 38 | let contents = String(data: data, encoding: .utf8)! 39 | mockFM.add(file: url.relativePath, contents: contents) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Tests/SwiftInfoTests/MockPlistExtractor.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftInfoCore 3 | 4 | public struct MockPlistExtractor: PlistExtractor { 5 | 6 | public init() {} 7 | 8 | public func extractPlistPath(xcodeproj: String, 9 | target: String, 10 | configuration: String, 11 | fileUtils: FileUtils) -> String { 12 | return "Info.plist" 13 | } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /Tests/SwiftInfoTests/MockSourceKit.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftInfoCore 3 | 4 | class MockSourceKit: SourceKit { 5 | 6 | } 7 | -------------------------------------------------------------------------------- /Tests/SwiftInfoTests/ProjectInfoDataTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SwiftInfoCore 3 | 4 | final class ProjectInfoDataTests: XCTestCase { 5 | func testVersionComesFromPlist() { 6 | let fileManager = MockFileManager() 7 | let fileOpener = MockFileOpener(mockFM: fileManager) 8 | let fileUtils = FileUtils(fileManager: fileManager, fileOpener: fileOpener) 9 | fileManager.add(file: "./Infofile.swift", contents: "") 10 | let projectInfo = ProjectInfo(xcodeproj: "Mock.xcproject", 11 | target: "Mock", 12 | configuration: "Mock-Debug", 13 | fileUtils: fileUtils, 14 | plistExtractor: MockPlistExtractor()) 15 | let plist = NSDictionary(dictionary: ["CFBundleShortVersionString": "1.11", 16 | "CFBundleVersion": "123"]) 17 | fileManager.add(plist: plist, file: "./Info.plist") 18 | XCTAssertEqual(try? projectInfo.getVersionString(), "1.11") 19 | XCTAssertEqual(try? projectInfo.getBuildNumber(), "123") 20 | } 21 | 22 | func testVersionWasManuallyProvided() { 23 | let fileManager = MockFileManager() 24 | let fileOpener = MockFileOpener(mockFM: fileManager) 25 | let fileUtils = FileUtils(fileManager: fileManager, fileOpener: fileOpener) 26 | fileManager.add(file: "./Infofile.swift", contents: "") 27 | let projectInfo = ProjectInfo(xcodeproj: "Mock.xcproject", 28 | target: "Mock", 29 | configuration: "Mock-Debug", 30 | versionString: "2.3", 31 | buildNumber: "90", 32 | fileUtils: fileUtils, 33 | plistExtractor: MockPlistExtractor()) 34 | let plist = NSDictionary(dictionary: ["CFBundleShortVersionString": "1.11", 35 | "CFBundleVersion": "123"]) 36 | fileManager.add(plist: plist, file: "./Info.plist") 37 | XCTAssertEqual(try? projectInfo.getVersionString(), "2.3") 38 | XCTAssertEqual(try? projectInfo.getBuildNumber(), "90") 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Tests/SwiftInfoTests/ProviderTests/ProviderTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SwiftInfoCore 3 | 4 | extension SwiftInfo { 5 | static func mock() -> SwiftInfo { 6 | let fileManager = MockFileManager() 7 | let fileOpener = MockFileOpener(mockFM: fileManager) 8 | let fileUtils = FileUtils(fileManager: fileManager, fileOpener: fileOpener) 9 | fileManager.add(file: "./Infofile.swift", contents: "") 10 | let projectInfo = ProjectInfo(xcodeproj: "Mock.xcproject", 11 | target: "Mock", 12 | configuration: "Mock-Debug", 13 | fileUtils: fileUtils, 14 | plistExtractor: MockPlistExtractor()) 15 | let plist = NSDictionary(dictionary: ["CFBundleShortVersionString": "1.0", 16 | "CFBundleVersion": "1"]) 17 | fileManager.add(plist: plist, file: "./Info.plist") 18 | return SwiftInfo(projectInfo: projectInfo, 19 | fileUtils: fileUtils, 20 | slackFormatter: .init(), 21 | client: .init(), 22 | sourceKit: MockSourceKit()) 23 | } 24 | 25 | var mockFileManager: MockFileManager { 26 | return fileUtils.fileManager as! MockFileManager 27 | } 28 | } 29 | 30 | final class ProviderTests: XCTestCase { 31 | 32 | var api: SwiftInfo! 33 | 34 | override func setUp() { 35 | super.setUp() 36 | api = SwiftInfo.mock() 37 | FileUtils.buildLogFilePath = "" 38 | FileUtils.testLogFilePath = "" 39 | } 40 | 41 | func testIPASize() { 42 | let path = "./build/App.ipa" 43 | api.mockFileManager.add(file: path, contents: "") 44 | api.mockFileManager.add(attributes: [.size: UInt64(5000)], file: path) 45 | let extracted = try! IPASizeProvider.extract(fromApi: api) 46 | XCTAssertEqual(extracted.size, 5000) 47 | } 48 | 49 | func testWarningCount() { 50 | FileUtils.buildLogFilePath = "./build.log" 51 | let log = 52 | """ 53 | aaaaaaaa 54 | mockmockmock 55 | :global: warning: A warning 56 | :global: warning: Another warning 57 | warning: Also a warning warning: This one is not 58 | warning: Another one 59 | :global: warning: A warning 60 | :global: warning: A warning 61 | The above two are not warnings because they are repeated. 62 | """ 63 | api.mockFileManager.add(file: "build.log", contents: log) 64 | let extracted = try! WarningCountProvider.extract(fromApi: api) 65 | XCTAssertEqual(extracted.count, 4) 66 | } 67 | 68 | func testTargetCount() { 69 | FileUtils.buildLogFilePath = "./build.log" 70 | let log = 71 | """ 72 | ahfgvnvnrjrjjffj fjjnf nhfjfjfj nfnfnf -module-name MyMock nbvbfhuff fjjf ahfgvnvnrjrjjffj fjjnf nhfjfjfj nfnfnf -module-name MyMock nbvbfhuff fjjf 73 | ahfgvnvnrjrjjffj fjjnf nhfjfjfj nfnfnf -module-name MyMock2 nbvbfhuff fjjf 74 | -module-name MyMock3 fnbvvb 75 | """ 76 | api.mockFileManager.add(file: "build.log", contents: log) 77 | let extracted = try! TargetCountProvider.extract(fromApi: api) 78 | XCTAssertEqual(extracted.count, 3) 79 | XCTAssertEqual( 80 | extracted.summary(comparingWith: TargetCountProvider(count: 2), 81 | args: nil).color, 82 | Summary.Style.negative.hexColor 83 | ) 84 | XCTAssertEqual( 85 | extracted.summary(comparingWith: TargetCountProvider(count: 2), 86 | args: .init(mode: .complainOnAdditions)).color, 87 | Summary.Style.negative.hexColor 88 | ) 89 | XCTAssertEqual( 90 | extracted.summary(comparingWith: TargetCountProvider(count: 2), 91 | args: .init(mode: .complainOnRemovals)).color, 92 | Summary.Style.positive.hexColor 93 | ) 94 | XCTAssertEqual( 95 | extracted.summary(comparingWith: TargetCountProvider(count: 2), 96 | args: .init(mode: .neutral)).color, 97 | Summary.Style.neutral.hexColor 98 | ) 99 | XCTAssertEqual( 100 | extracted.summary(comparingWith: TargetCountProvider(count: 3), 101 | args: .init(mode: .complainOnAdditions)).color, 102 | Summary.Style.neutral.hexColor 103 | ) 104 | } 105 | 106 | func testTestCountXcode() { 107 | FileUtils.testLogFilePath = "./test.log" 108 | let log = 109 | """ 110 | aaa 111 | --- Test Case 'a' started --- 112 | --- Test Case 'a' passed --- 113 | --- Test Case 'b' failed --- 114 | --- test case 'c' passed --- 115 | --- Test CASE 'd' passed --- 116 | --- test Case 'e' failed --- 117 | aaa 118 | """ 119 | api.mockFileManager.add(file: "test.log", contents: log) 120 | let extracted = try! TestCountProvider.extract(fromApi: api) 121 | XCTAssertEqual(extracted.count, 5) 122 | } 123 | 124 | func testTestCountBuck() { 125 | FileUtils.buckLogFilePath = "./buck.log" 126 | let log = 127 | """ 128 | PASS <100ms 4 Passed 0 Skipped 0 Failed CorporatePaymentChoicesWorkerSpec 129 | PASS <100ms 3 Passed 0 Skipped 0 Failed CrossSellingAnalyticSpec 130 | PASS 525ms 12 Passed 0 Skipped 0 Failed CrossSellingCarouselCellSpec 131 | PASS <100ms 4 Passed 0 Skipped 0 Failed CrossSellingItemManagerProtocolSpec 132 | PASS <100ms 3 Passed 0 Skipped 0 Failed CrossSellingRemoteConfigSpec 133 | PASS <100ms 3 Passed 0 Skipped 0 Failed CrossSellingWokerSpec 134 | PASS <100ms 3 Passed 0 Skipped 0 Failed CustomerRequestModelSpec 135 | PASS <100ms 10 Passed 0 Skipped 0 Failed CustomizationManagerTests 136 | PASS <100ms 5 Passed 0 Skipped 0 Failed DateExtensionsSpec 137 | PASS <100ms 6 Passed 0 Skipped 0 Failed DateStrategyFactorySpec 138 | """ 139 | api.mockFileManager.add(file: "buck.log", contents: log) 140 | let extracted = try! TestCountProvider.extract(fromApi: api, args: .init(buildSystem: .buck)) 141 | XCTAssertEqual(extracted.count, 53) 142 | } 143 | 144 | func testObjcCount() { 145 | FileUtils.buildLogFilePath = "./build.log" 146 | let log = 147 | """ 148 | CompileC bla SDWebImage/SDWebImage/UIView+WebCacheOperation.m normal armv7 149 | CompileC bla SDWebImage/SDWebImage/UIView+WebCacheOperation.m normal armv7 150 | CompileC bla SDWebImage/SDWebImage/UIView+WebCacheOperation2.m normal armv7 151 | CompileC bla SDWebImage/SDWebImage/UIView+WebCacheOperation3.m normal armv7 152 | CpHeader SDWebImage/a.h normal armv7 153 | CpHeader SDWebImage/a2.h normal armv7 154 | CpHeader SDWebImage/a3.h normal armv7 155 | """ 156 | api.mockFileManager.add(file: "build.log", contents: log) 157 | let extracted = try! OBJCFileCountProvider.extract(fromApi: api) 158 | XCTAssertEqual(extracted.count, 6) 159 | } 160 | 161 | func testLongestTest() { 162 | FileUtils.testLogFilePath = "./test.log" 163 | let log = 164 | """ 165 | Test suite 'DeviceRequestTests' started on 'Clone 1 of iPhone 5s - Rapiddo.app (13627)' 166 | Test case 'DeviceRequestTests.testEventNameFormatter()' passed on 'Clone 1 of iPhone 5s - Rapiddo.app (13627)' (0.001 seconds) 167 | Test suite 'FareEstimateTests' started on 'Clone 1 of iPhone 5s - Rapiddo.app (13627)' 168 | Test case 'FareEstimateTests.testJsonDecoder()' passed on 'Clone 1 of iPhone 5s - Rapiddo.app (13627)' (0.464 seconds) 169 | Test case 'FareEstimateTests.testJsonDecoder()' failed on 'Clone 1 of iPhone 5s - Rapiddo.app (13627)' (1000.464 seconds) 170 | Test Case '-[UnitTests.AccountTagsWorkerTests Account_Tags_Worker__Fetching_tags__Try_to_get_tags_when_user_is_not_logged_in]' passed (0.013 seconds). 171 | Test suite 'CartItemTests' started on 'Clone 1 of iPhone 5s - Rapiddo.app (13627)' 172 | Test case 'CartItemTests.testPriceLogic()' passed on 'Clone 1 of iPhone 5s - Rapiddo.app (13627)' (0.001 seconds) 173 | """ 174 | api.mockFileManager.add(file: "test.log", contents: log) 175 | let extracted = try! LongestTestDurationProvider.extract(fromApi: api) 176 | XCTAssertEqual(extracted.durationInt, 464) 177 | } 178 | 179 | func testTotalTestDuration() { 180 | FileUtils.testLogFilePath = "./test.log" 181 | let log = 182 | """ 183 | Test suite 'DeviceRequestTests' started on 'Clone 1 of iPhone 5s - Rapiddo.app (13627)' 184 | Test case 'DeviceRequestTests.testEventNameFormatter()' passed on 'Clone 1 of iPhone 5s - Rapiddo.app (13627)' (0.001 seconds) 185 | Test suite 'FareEstimateTests' started on 'Clone 1 of iPhone 5s - Rapiddo.app (13627)' 186 | Test case 'FareEstimateTests.testJsonDecoder()' passed on 'Clone 1 of iPhone 5s - Rapiddo.app (13627)' (0.464 seconds) 187 | Test case 'FareEstimateTests.testJsonDecoder()' failed on 'Clone 1 of iPhone 5s - Rapiddo.app (13627)' (1000.464 seconds) 188 | Test Case '-[UnitTests.AccountTagsWorkerTests Account_Tags_Worker__Fetching_tags__Try_to_get_tags_when_user_is_not_logged_in]' passed (0.013 seconds). 189 | Test suite 'CartItemTests' started on 'Clone 1 of iPhone 5s - Rapiddo.app (13627)' 190 | Test case 'CartItemTests.testPriceLogic()' passed on 'Clone 1 of iPhone 5s - Rapiddo.app (13627)' (0.001 seconds) 191 | Generating coverage data... 192 | Generated coverage report: /Users/bruno.rocha/Library/Developer/Xcode/DerivedData/Rapiddo-cbobntbmchyaczezxxpesrevoisy/Logs/Test/Run-Marketplace-2019.03.25_12-55-36--0300.xcresult/2_Test/action.xccovreport 193 | ** TEST SUCCEEDED ** [37.368 sec] 194 | """ 195 | api.mockFileManager.add(file: "test.log", contents: log) 196 | let extracted = try! TotalTestDurationProvider.extract(fromApi: api) 197 | XCTAssertEqual(extracted.durationInt, 37368) 198 | } 199 | 200 | func testArchiveTime() { 201 | FileUtils.buildLogFilePath = "./build.log" 202 | let log = 203 | """ 204 | Touch /Users/bruno.rocha/Library/Developer/Xcode/DerivedData/Rapiddo-cbobntbmchyaczezxxpesrevoisy/Build/Intermediates.noindex/ArchiveIntermediates/Marketplace-AppStore/InstallationBuildProductsLocation/Applications/Rapiddo.app (in target: Rapiddo) 205 | cd /Users/bruno.rocha/Desktop/MovileRepos/rapiddo-vision-ios 206 | /usr/bin/touch -c /Users/bruno.rocha/Library/Developer/Xcode/DerivedData/Rapiddo-cbobntbmchyaczezxxpesrevoisy/Build/Intermediates.noindex/ArchiveIntermediates/Marketplace-AppStore/InstallationBuildProductsLocation/Applications/Rapiddo.app 207 | 208 | ** ARCHIVE SUCCEEDED ** [309.407 sec] 209 | """ 210 | api.mockFileManager.add(file: "build.log", contents: log) 211 | let extracted = try! ArchiveDurationProvider.extract(fromApi: api) 212 | XCTAssertEqual(extracted.timeInt, 309407) 213 | } 214 | 215 | func testCoverageExtraction() { 216 | var log = """ 217 | Generating coverage data... 218 | Generated coverage report: /Users/bla/action.xccovreport 219 | ** TEST SUCCEEDED ** [37.368 sec] 220 | """ 221 | XCTAssertEqual("/Users/bla/action.xccovreport", 222 | CodeCoverageProvider.getCodeCoverageLegacyJsonPath(fromLogs: log)) 223 | log = """ 224 | Test session results, code coverage, and logs: 225 | /Users/bla/action.xcresult 226 | 227 | ** TEST SUCCEEDED ** [9.756 sec] 228 | """ 229 | XCTAssertEqual("/Users/bla/action.xcresult", 230 | CodeCoverageProvider.getCodeCoverageXcode11JsonPath(fromLogs: log)) 231 | } 232 | 233 | func testAssetCatalogPaths() { 234 | FileUtils.buildLogFilePath = "./build.log" 235 | let log = 236 | """ 237 | CompileAssetCatalog /tmp/sandbox/58b30f5722d5c60100e34a03/ci/Build/Intermediates.noindex/ArchiveIntermediates/rogerluan-env/IntermediateBuildFilesPath/UninstalledProducts/iphoneos/Stripe_Stripe.bundle /tmp/sandbox/58b30f5722d5c60100e34a03/ci/SourcePackages/checkouts/stripe-ios/Stripe/Resources/Images/Stripe.xcassets (in target 'Stripe_Stripe' from project 'Stripe') 238 | CompileAssetCatalog /tmp/sandbox/58b30f5722d5c60100e34a03/ci/Build/Intermediates.noindex/ArchiveIntermediates/rogerluan-env/IntermediateBuildFilesPath/UninstalledProducts/iphoneos/rogerluan.appex /tmp/sandbox/workspace/rogerluan-sticker-pack/Stickers.xcassets (in target 'rogerluan-sticker-pack' from project 'rogerluans-product') 239 | CompileAssetCatalog /tmp/sandbox/58b30f5722d5c60100e34a03/ci/Build/Intermediates.noindex/ArchiveIntermediates/rogerluan-env/IntermediateBuildFilesPath/UninstalledProducts/iphoneos/rogerluanCore.framework /tmp/sandbox/workspace/rogerluan-core-ios/rogerluan-core/Colors.xcassets (in target 'rogerluanCore' from project 'Pods') 240 | CompileAssetCatalog /tmp/sandbox/58b30f5722d5c60100e34a03/ci/Build/Intermediates.noindex/ArchiveIntermediates/rogerluan-env/IntermediateBuildFilesPath/UninstalledProducts/iphoneos/rogerluan-function-widget.appex /tmp/sandbox/workspace/rogerluan-function-widget/Icons.xcassets /tmp/sandbox/workspace/rogerluans-product/Colors.xcassets (in target 'rogerluan-function-widget' from project 'rogerluans-product') 241 | CompileAssetCatalog /tmp/sandbox/58b30f5722d5c60100e34a03/ci/Build/Intermediates.noindex/ArchiveIntermediates/rogerluan-env/IntermediateBuildFilesPath/UninstalledProducts/iphoneos/rogerluan-share-plugin.appex /tmp/sandbox/workspace/rogerluans-product/Original Icons.xcassets /tmp/sandbox/workspace/rogerluan-share-plugin/Assets.xcassets /tmp/sandbox/workspace/rogerluans-product/Icons.xcassets /tmp/sandbox/workspace/rogerluans-product/SharedIcons.xcassets /tmp/sandbox/workspace/rogerluans-product/Colors.xcassets /tmp/sandbox/workspace/rogerluans-product/SharedImages.xcassets (in target 'rogerluan-share-plugin' from project 'rogerluans-product') 242 | CompileAssetCatalog /tmp/sandbox/58b30f5722d5c60100e34a03/ci/Build/Intermediates.noindex/ArchiveIntermediates/rogerluan-env/InstallationBuildProductsLocation/Applications/rogerluan.app /tmp/sandbox/workspace/rogerluans-product/AppIcon.xcassets /tmp/sandbox/workspace/rogerluans-product/Original Icons.xcassets /tmp/sandbox/workspace/rogerluans-product/Images.xcassets /tmp/sandbox/workspace/rogerluans-product/SharedImages.xcassets /tmp/sandbox/workspace/rogerluans-product/Icons.xcassets /tmp/sandbox/workspace/rogerluans-product/SharedIcons.xcassets /tmp/sandbox/workspace/rogerluans-product/Colors.xcassets (in target 'rogerluans-product' from project 'rogerluans-product') 243 | """ 244 | api.mockFileManager.add(file: "build.log", contents: log) 245 | let paths = try! TotalAssetCatalogsSizeProvider.allCatalogsPaths(api: api) 246 | XCTAssertEqual(paths.count, 12) 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /Tests/SwiftInfoTests/SlackFormatterTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SwiftInfoCore 3 | 4 | final class SlackFormatterTests: XCTestCase { 5 | func testFormatter() { 6 | let swiftInfo = SwiftInfo.mock() 7 | let summaries = [Summary(text: "A", style: .positive, numericValue: 0, stringValue: "a")] 8 | let output = Output(rawDictionary: [:], summaries: summaries, errors: []) 9 | let formatted = SlackFormatter().format(output: output, titlePrefix: nil, projectInfo: swiftInfo.projectInfo) 10 | let dictionary = NSDictionary(dictionary: formatted.json) 11 | let expected: [String: Any] = 12 | [ 13 | "attachments": [["color": "#36a64f","text": "A"]], 14 | "text": "SwiftInfo results for Mock 1.0 (1) - Mock-Debug:" 15 | ] 16 | XCTAssertEqual(dictionary, NSDictionary(dictionary: expected)) 17 | } 18 | 19 | func testFormatterWithTitlePrefix() { 20 | let swiftInfo = SwiftInfo.mock() 21 | let prefix = "" 22 | let summaries = [Summary(text: "A", style: .positive, numericValue: 0, stringValue: "a")] 23 | let output = Output(rawDictionary: [:], summaries: summaries, errors: []) 24 | let formatted = SlackFormatter().format(output: output, titlePrefix: prefix, projectInfo: swiftInfo.projectInfo) 25 | let dictionary = NSDictionary(dictionary: formatted.json) 26 | let text = prefix + "Mock 1.0 (1) - Mock-Debug:" 27 | let expected: [String: Any] = 28 | [ 29 | "attachments": [["color": "#36a64f","text": "A"]], 30 | "text": text 31 | ] 32 | XCTAssertEqual(dictionary, NSDictionary(dictionary: expected)) 33 | } 34 | 35 | func testFormatterWithError() { 36 | let swiftInfo = SwiftInfo.mock() 37 | let summaries = [Summary(text: "A", style: .positive, numericValue: 0, stringValue: "a")] 38 | let output = Output(rawDictionary: [:], summaries: summaries, errors: ["abc", "cde"]) 39 | let formatted = SlackFormatter().format(output: output, titlePrefix: nil, projectInfo: swiftInfo.projectInfo) 40 | let dictionary = NSDictionary(dictionary: formatted.json) 41 | let expected: [String: Any] = 42 | [ 43 | "attachments": [["color": "#36a64f","text": "A"]], 44 | "text": "SwiftInfo results for Mock 1.0 (1) - Mock-Debug:\nErrors:\nabc\ncde" 45 | ] 46 | XCTAssertEqual(dictionary, NSDictionary(dictionary: expected)) 47 | } 48 | 49 | func testRawPrint() { 50 | let swiftInfo = SwiftInfo.mock() 51 | let summaries = [Summary(text: "A", style: .positive, numericValue: 0, stringValue: "a")] 52 | let output = Output(rawDictionary: [:], summaries: summaries, errors: []) 53 | let formatted = SlackFormatter().format(output: output, titlePrefix: nil, projectInfo: swiftInfo.projectInfo) 54 | XCTAssertEqual(formatted.message, "SwiftInfo results for Mock 1.0 (1) - Mock-Debug:\nA") 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Tests/SwiftInfoTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | #if !canImport(ObjectiveC) 2 | import XCTest 3 | 4 | extension CoreTests { 5 | // DO NOT MODIFY: This is autogenerated, use: 6 | // `swift test --generate-linuxmain` 7 | // to regenerate. 8 | static let __allTests__CoreTests = [ 9 | ("testFullRun", testFullRun), 10 | ("testFullRunWithEmptyOutput", testFullRunWithEmptyOutput), 11 | ("testSwiftCArgs", testSwiftCArgs), 12 | ] 13 | } 14 | 15 | extension FileUtilsTests { 16 | // DO NOT MODIFY: This is autogenerated, use: 17 | // `swift test --generate-linuxmain` 18 | // to regenerate. 19 | static let __allTests__FileUtilsTests = [ 20 | ("testBuildLogs", testBuildLogs), 21 | ("testInfofileFinder", testInfofileFinder), 22 | ("testLastOutput", testLastOutput), 23 | ("testSaveOutput", testSaveOutput), 24 | ("testTestLogs", testTestLogs), 25 | ] 26 | } 27 | 28 | extension ProjectInfoDataTests { 29 | // DO NOT MODIFY: This is autogenerated, use: 30 | // `swift test --generate-linuxmain` 31 | // to regenerate. 32 | static let __allTests__ProjectInfoDataTests = [ 33 | ("testVersionComesFromPlist", testVersionComesFromPlist), 34 | ("testVersionWasManuallyProvided", testVersionWasManuallyProvided), 35 | ] 36 | } 37 | 38 | extension ProviderTests { 39 | // DO NOT MODIFY: This is autogenerated, use: 40 | // `swift test --generate-linuxmain` 41 | // to regenerate. 42 | static let __allTests__ProviderTests = [ 43 | ("testArchiveTime", testArchiveTime), 44 | ("testIPASize", testIPASize), 45 | ("testLongestTest", testLongestTest), 46 | ("testObjcCount", testObjcCount), 47 | ("testTargetCount", testTargetCount), 48 | ("testTestCountBuck", testTestCountBuck), 49 | ("testTestCountXcode", testTestCountXcode), 50 | ("testTotalTestDuration", testTotalTestDuration), 51 | ("testWarningCount", testWarningCount), 52 | ] 53 | } 54 | 55 | extension SlackFormatterTests { 56 | // DO NOT MODIFY: This is autogenerated, use: 57 | // `swift test --generate-linuxmain` 58 | // to regenerate. 59 | static let __allTests__SlackFormatterTests = [ 60 | ("testFormatter", testFormatter), 61 | ("testFormatterWithError", testFormatterWithError), 62 | ("testRawPrint", testRawPrint), 63 | ] 64 | } 65 | 66 | public func __allTests() -> [XCTestCaseEntry] { 67 | return [ 68 | testCase(CoreTests.__allTests__CoreTests), 69 | testCase(FileUtilsTests.__allTests__FileUtilsTests), 70 | testCase(ProjectInfoDataTests.__allTests__ProjectInfoDataTests), 71 | testCase(ProviderTests.__allTests__ProviderTests), 72 | testCase(SlackFormatterTests.__allTests__SlackFormatterTests), 73 | ] 74 | } 75 | #endif 76 | --------------------------------------------------------------------------------