diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..b459e1c --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,34 @@ +# iOS CircleCI 2.0 configuration file +# +# Check https://circleci.com/docs/2.0/ios-migrating-from-1-2/ for more details +# +version: 2 +jobs: + build: + + # Specify the Xcode version to use + macos: + xcode: "9.3.0" + + steps: + - checkout + + # Build the app and run tests + - run: + name: Build and run tests + command: fastlane scan -p ./DemoCodeCoverage/DemoCodeCoverage.xcodeproj/ + environment: + SCAN_DEVICE: iPhone 6 + SCAN_SCHEME: DemoCodeCoverage + + # Collect XML test results data to show in the UI, + # and save the same XML files under test-results folder + # in the Artifacts tab + - store_test_results: + path: test_output/report.xml + - store_artifacts: + path: /tmp/test-results + destination: scan-test-results + - store_artifacts: + path: ~/Library/Logs/scan + destination: scan-logs diff --git a/.travis.yml b/.travis.yml new file mode 100755 index 0000000..63f32a7 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,4 @@ +language: objective-c +xcode_sdk: iphonesimulator +xcode_project: DemoCodeCoverage/DemoCodeCoverage.xcodeproj +xcode_scheme: DemoCodeCoverage \ No newline at end of file diff --git a/DemoCodeCoverage/DemoCodeCoverage.xcodeproj/project.pbxproj b/DemoCodeCoverage/DemoCodeCoverage.xcodeproj/project.pbxproj index d9ef94d..304f280 100644 --- a/DemoCodeCoverage/DemoCodeCoverage.xcodeproj/project.pbxproj +++ b/DemoCodeCoverage/DemoCodeCoverage.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ C5711D7E1AFB3C1A00F2BA8D /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = C5711D7C1AFB3C1A00F2BA8D /* LaunchScreen.xib */; }; C5711D971AFB441E00F2BA8D /* CalculatorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = C5711D961AFB441E00F2BA8D /* CalculatorTests.m */; }; C5711D9B1AFB62B300F2BA8D /* Calculator.m in Sources */ = {isa = PBXBuildFile; fileRef = C5711D9A1AFB62B300F2BA8D /* Calculator.m */; }; + C59D54FA1BD62281004EB692 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = C59D54F91BD62281004EB692 /* Settings.bundle */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -43,6 +44,7 @@ C5711D961AFB441E00F2BA8D /* CalculatorTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CalculatorTests.m; sourceTree = ""; }; C5711D991AFB62B300F2BA8D /* Calculator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Calculator.h; sourceTree = ""; }; C5711D9A1AFB62B300F2BA8D /* Calculator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Calculator.m; sourceTree = ""; }; + C59D54F91BD62281004EB692 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = SOURCE_ROOT; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -84,6 +86,7 @@ C5711D6C1AFB3C1A00F2BA8D /* DemoCodeCoverage */ = { isa = PBXGroup; children = ( + C59D54F91BD62281004EB692 /* Settings.bundle */, C5711D981AFB62B300F2BA8D /* Classes */, C5711D711AFB3C1A00F2BA8D /* AppDelegate.h */, C5711D721AFB3C1A00F2BA8D /* AppDelegate.m */, @@ -142,6 +145,7 @@ C5711D661AFB3C1A00F2BA8D /* Sources */, C5711D671AFB3C1A00F2BA8D /* Frameworks */, C5711D681AFB3C1A00F2BA8D /* Resources */, + C59D54F81BD62261004EB692 /* Setup build number */, ); buildRules = ( ); @@ -181,6 +185,7 @@ TargetAttributes = { C5711D691AFB3C1A00F2BA8D = { CreatedOnToolsVersion = 6.3; + DevelopmentTeam = AT989KJ27T; }; C5711D821AFB3C1A00F2BA8D = { CreatedOnToolsVersion = 6.3; @@ -212,6 +217,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + C59D54FA1BD62281004EB692 /* Settings.bundle in Resources */, C5711D791AFB3C1A00F2BA8D /* Main.storyboard in Resources */, C5711D7E1AFB3C1A00F2BA8D /* LaunchScreen.xib in Resources */, C5711D7B1AFB3C1A00F2BA8D /* Images.xcassets in Resources */, @@ -227,6 +233,23 @@ }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + C59D54F81BD62261004EB692 /* Setup build number */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Setup build number"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if [ \"${CONFIGURATION}\" = \"Release\" ]; then\n#!/bin/bash\n#get path to the BUILT .plist, NOT the packaged one! this fixes the off-by-one bug\nbuiltInfoPlistPath=${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\necho \"using plist at $builtInfoPlistPath\"\n\n#increment the build number when to archieve new build\nbuildNumber=$(/usr/libexec/PlistBuddy -c \"Print :CFBundleVersion\" \"$builtInfoPlistPath\")\necho \"retrieved current build number: $buildNumber\"\nbuildNumber=$(($buildNumber + 1))\n/usr/libexec/PlistBuddy -c \"Set :CFBundleVersion $buildNumber\" \"$builtInfoPlistPath\"\n/usr/libexec/PlistBuddy -c \"Set :CFBundleVersion $buildNumber\" \"$INFOPLIST_FILE\"\n\n#compose the version number string\nversionString=$(/usr/libexec/PlistBuddy -c \"Print :CFBundleShortVersionString\" \"$builtInfoPlistPath\")\nversionString+=\" (\"\nversionString+=$(/usr/libexec/PlistBuddy -c \"Print :CFBundleVersion\" \"$builtInfoPlistPath\")\nversionString+=\")\"\n\n#write the version number string to the settings bundle\n#IMPORTANT: this assumes the version number is the second property in the settings bundle!\n/usr/libexec/PlistBuddy -c \"Set :PreferenceSpecifiers:1:DefaultValue $versionString\" \"Settings.bundle/Root.plist\"\nfi"; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ C5711D661AFB3C1A00F2BA8D /* Sources */ = { isa = PBXSourcesBuildPhase; @@ -361,9 +384,11 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + DEVELOPMENT_TEAM = AT989KJ27T; GCC_GENERATE_TEST_COVERAGE_FILES = YES; GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = YES; INFOPLIST_FILE = DemoCodeCoverage/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 8.1; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_NAME = "$(TARGET_NAME)"; }; @@ -373,9 +398,11 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + DEVELOPMENT_TEAM = AT989KJ27T; GCC_GENERATE_TEST_COVERAGE_FILES = NO; GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = NO; INFOPLIST_FILE = DemoCodeCoverage/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 8.1; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_NAME = "$(TARGET_NAME)"; }; diff --git a/DemoCodeCoverage/DemoCodeCoverage.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/DemoCodeCoverage/DemoCodeCoverage.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/DemoCodeCoverage/DemoCodeCoverage.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/DemoCodeCoverage/DemoCodeCoverage.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/DemoCodeCoverage/DemoCodeCoverage.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/DemoCodeCoverage/DemoCodeCoverage.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/DemoCodeCoverage/DemoCodeCoverage.xcodeproj/xcshareddata/xcschemes/DemoCodeCoverage.xcscheme b/DemoCodeCoverage/DemoCodeCoverage.xcodeproj/xcshareddata/xcschemes/DemoCodeCoverage.xcscheme new file mode 100644 index 0000000..3aa0a0a --- /dev/null +++ b/DemoCodeCoverage/DemoCodeCoverage.xcodeproj/xcshareddata/xcschemes/DemoCodeCoverage.xcscheme @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DemoCodeCoverage/DemoCodeCoverage/Info.plist b/DemoCodeCoverage/DemoCodeCoverage/Info.plist index cc3cd78..8ab63db 100644 --- a/DemoCodeCoverage/DemoCodeCoverage/Info.plist +++ b/DemoCodeCoverage/DemoCodeCoverage/Info.plist @@ -15,11 +15,11 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.0 + 1.0.0 CFBundleSignature ???? CFBundleVersion - 1 + 4 LSRequiresIPhoneOS UILaunchStoryboardName diff --git a/DemoCodeCoverage/Gemfile b/DemoCodeCoverage/Gemfile new file mode 100644 index 0000000..7a118b4 --- /dev/null +++ b/DemoCodeCoverage/Gemfile @@ -0,0 +1,3 @@ +source "https://rubygems.org" + +gem "fastlane" diff --git a/DemoCodeCoverage/Settings.bundle/Root.plist b/DemoCodeCoverage/Settings.bundle/Root.plist new file mode 100644 index 0000000..92050d3 --- /dev/null +++ b/DemoCodeCoverage/Settings.bundle/Root.plist @@ -0,0 +1,27 @@ + + + + + PreferenceSpecifiers + + + Title + Group + Type + Application information + + + DefaultValue + 1.0.0 (4) + Key + versionString + Title + Version number + Type + PSTitleValueSpecifier + + + StringsTable + Root + + diff --git a/DemoCodeCoverage/Settings.bundle/en.lproj/Root.strings b/DemoCodeCoverage/Settings.bundle/en.lproj/Root.strings new file mode 100644 index 0000000..7ff165f Binary files /dev/null and b/DemoCodeCoverage/Settings.bundle/en.lproj/Root.strings differ diff --git a/DemoCodeCoverage/build.sh b/DemoCodeCoverage/build.sh index 1eedc07..9f80ba7 100644 --- a/DemoCodeCoverage/build.sh +++ b/DemoCodeCoverage/build.sh @@ -1,10 +1,10 @@ #!/bin/sh # First Run Tests -xcodebuild test -project DemoCodeCoverage.xcodeproj/ -scheme 'DemoCodeCoverage' -configuration Debug -destination 'platform=iOS Simulator,name=iPhone 6,OS=8.3' | xcpretty -c --report junit +xcodebuild test -project DemoCodeCoverage.xcodeproj/ -scheme 'DemoCodeCoverage' -configuration Debug -destination 'platform=iOS Simulator,name=iPhone 6' | xcpretty -c --report junit #We set required coverage to 80% - build fails if coverage falls below this value. # Now Produce Test Coverage Report -groovy http://frankencover.it/with -source-dir "DemoCodeCoverage/Classes" -required-coverage 80 +#groovy http://frankencover.it/with -source-dir "DemoCodeCoverage/Classes" -required-coverage 80 # Use for local -#groovy frankencover -source-dir "DemoCodeCoverage/Classes" -required-coverage 80 +groovy frankencover -source-dir "DemoCodeCoverage/Classes" -required-coverage 80 diff --git a/DemoCodeCoverage/fastlane/Appfile b/DemoCodeCoverage/fastlane/Appfile new file mode 100644 index 0000000..6ccd6a1 --- /dev/null +++ b/DemoCodeCoverage/fastlane/Appfile @@ -0,0 +1,6 @@ +app_identifier("congpc.DemoCodeCoverage") # The bundle identifier of your app +apple_id("chicong7891@gmail.com") # Your Apple email address + + +# For more information about the Appfile, see: +# https://docs.fastlane.tools/advanced/#appfile diff --git a/DemoCodeCoverage/fastlane/Fastfile b/DemoCodeCoverage/fastlane/Fastfile new file mode 100644 index 0000000..9809bf8 --- /dev/null +++ b/DemoCodeCoverage/fastlane/Fastfile @@ -0,0 +1,29 @@ +# This file contains the fastlane.tools configuration +# You can find the documentation at https://docs.fastlane.tools +# +# For a list of all available actions, check out +# +# https://docs.fastlane.tools/actions +# +# For a list of all available plugins, check out +# +# https://docs.fastlane.tools/plugins/available-plugins +# + +# Uncomment the line if you want fastlane to automatically update itself +# update_fastlane + +default_platform(:ios) + +platform :ios do + desc "Generate new localized screenshots" + #lane :screenshots do + # capture_screenshots(scheme: "DemoCodeCoverage") + #end + + lane :tests do + run_tests(project:"DemoCodeCoverage.xcodeproj", + devices: ["iPhone 6s"], + scheme: "MyAppTests") + end +end diff --git a/DemoCodeCoverage/fastlane/README.md b/DemoCodeCoverage/fastlane/README.md new file mode 100644 index 0000000..9a99026 --- /dev/null +++ b/DemoCodeCoverage/fastlane/README.md @@ -0,0 +1,29 @@ +fastlane documentation +================ +# Installation + +Make sure you have the latest version of the Xcode command line tools installed: + +``` +xcode-select --install +``` + +Install _fastlane_ using +``` +[sudo] gem install fastlane -NV +``` +or alternatively using `brew cask install fastlane` + +# Available Actions +## iOS +### ios tests +``` +fastlane ios tests +``` +Generate new localized screenshots + +---- + +This README.md is auto-generated and will be re-generated every time [fastlane](https://fastlane.tools) is run. +More information about fastlane can be found on [fastlane.tools](https://fastlane.tools). +The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools). diff --git a/DemoCodeCoverage/fastlane/Snapfile b/DemoCodeCoverage/fastlane/Snapfile new file mode 100644 index 0000000..bdd8a86 --- /dev/null +++ b/DemoCodeCoverage/fastlane/Snapfile @@ -0,0 +1,34 @@ +# Uncomment the lines below you want to change by removing the # in the beginning + +# A list of devices you want to take the screenshots from +# devices([ +# "iPhone 8", +# "iPhone 8 Plus", +# "iPhone SE", +# "iPhone X", +# "iPad Pro (12.9-inch)", +# "iPad Pro (9.7-inch)", +# "Apple TV 1080p" +# ]) + +# languages([ +# "en-US", +# "de-DE", +# "it-IT", +# ["pt", "pt_BR"] # Portuguese with Brazilian locale +# ]) + +# The name of the scheme which contains the UI Tests +# scheme("SchemeName") + +# Where should the resulting screenshots be stored? +# output_directory("./screenshots") + +# remove the '#' to clear all previously generated screenshots before creating new ones +# clear_previous_screenshots(true) + +# Arguments to pass to the app on launch. See https://docs.fastlane.tools/actions/snapshot/#launch-arguments +# launch_arguments(["-favColor red"]) + +# For more information about all available options run +# fastlane action snapshot diff --git a/DemoCodeCoverage/fastlane/SnapshotHelper.swift b/DemoCodeCoverage/fastlane/SnapshotHelper.swift new file mode 100644 index 0000000..8c1e8e8 --- /dev/null +++ b/DemoCodeCoverage/fastlane/SnapshotHelper.swift @@ -0,0 +1,281 @@ +// +// SnapshotHelper.swift +// Example +// +// Created by Felix Krause on 10/8/15. +// Copyright Ā© 2015 Felix Krause. All rights reserved. +// + +// ----------------------------------------------------- +// IMPORTANT: When modifying this file, make sure to +// increment the version number at the very +// bottom of the file to notify users about +// the new SnapshotHelper.swift +// ----------------------------------------------------- + +import Foundation +import XCTest + +var deviceLanguage = "" +var locale = "" + +func setupSnapshot(_ app: XCUIApplication) { + Snapshot.setupSnapshot(app) +} + +func snapshot(_ name: String, waitForLoadingIndicator: Bool) { + if waitForLoadingIndicator { + Snapshot.snapshot(name) + } else { + Snapshot.snapshot(name, timeWaitingForIdle: 0) + } +} + +/// - Parameters: +/// - name: The name of the snapshot +/// - timeout: Amount of seconds to wait until the network loading indicator disappears. Pass `0` if you don't want to wait. +func snapshot(_ name: String, timeWaitingForIdle timeout: TimeInterval = 20) { + Snapshot.snapshot(name, timeWaitingForIdle: timeout) +} + +enum SnapshotError: Error, CustomDebugStringConvertible { + case cannotDetectUser + case cannotFindHomeDirectory + case cannotFindSimulatorHomeDirectory + case cannotAccessSimulatorHomeDirectory(String) + case cannotRunOnPhysicalDevice + + var debugDescription: String { + switch self { + case .cannotDetectUser: + return "Couldn't find Snapshot configuration files - can't detect current user " + case .cannotFindHomeDirectory: + return "Couldn't find Snapshot configuration files - can't detect `Users` dir" + case .cannotFindSimulatorHomeDirectory: + return "Couldn't find simulator home location. Please, check SIMULATOR_HOST_HOME env variable." + case .cannotAccessSimulatorHomeDirectory(let simulatorHostHome): + return "Can't prepare environment. Simulator home location is inaccessible. Does \(simulatorHostHome) exist?" + case .cannotRunOnPhysicalDevice: + return "Can't use Snapshot on a physical device." + } + } +} + +@objcMembers +open class Snapshot: NSObject { + static var app: XCUIApplication? + static var cacheDirectory: URL? + static var screenshotsDirectory: URL? { + return cacheDirectory?.appendingPathComponent("screenshots", isDirectory: true) + } + + open class func setupSnapshot(_ app: XCUIApplication) { + + Snapshot.app = app + + do { + let cacheDir = try pathPrefix() + Snapshot.cacheDirectory = cacheDir + setLanguage(app) + setLocale(app) + setLaunchArguments(app) + } catch let error { + print(error) + } + } + + class func setLanguage(_ app: XCUIApplication) { + guard let cacheDirectory = self.cacheDirectory else { + print("CacheDirectory is not set - probably running on a physical device?") + return + } + + let path = cacheDirectory.appendingPathComponent("language.txt") + + do { + let trimCharacterSet = CharacterSet.whitespacesAndNewlines + deviceLanguage = try String(contentsOf: path, encoding: .utf8).trimmingCharacters(in: trimCharacterSet) + app.launchArguments += ["-AppleLanguages", "(\(deviceLanguage))"] + } catch { + print("Couldn't detect/set language...") + } + } + + class func setLocale(_ app: XCUIApplication) { + guard let cacheDirectory = self.cacheDirectory else { + print("CacheDirectory is not set - probably running on a physical device?") + return + } + + let path = cacheDirectory.appendingPathComponent("locale.txt") + + do { + let trimCharacterSet = CharacterSet.whitespacesAndNewlines + locale = try String(contentsOf: path, encoding: .utf8).trimmingCharacters(in: trimCharacterSet) + } catch { + print("Couldn't detect/set locale...") + } + if locale.isEmpty { + locale = Locale(identifier: deviceLanguage).identifier + } + app.launchArguments += ["-AppleLocale", "\"\(locale)\""] + } + + class func setLaunchArguments(_ app: XCUIApplication) { + guard let cacheDirectory = self.cacheDirectory else { + print("CacheDirectory is not set - probably running on a physical device?") + return + } + + let path = cacheDirectory.appendingPathComponent("snapshot-launch_arguments.txt") + app.launchArguments += ["-FASTLANE_SNAPSHOT", "YES", "-ui_testing"] + + do { + let launchArguments = try String(contentsOf: path, encoding: String.Encoding.utf8) + let regex = try NSRegularExpression(pattern: "(\\\".+?\\\"|\\S+)", options: []) + let matches = regex.matches(in: launchArguments, options: [], range: NSRange(location: 0, length: launchArguments.count)) + let results = matches.map { result -> String in + (launchArguments as NSString).substring(with: result.range) + } + app.launchArguments += results + } catch { + print("Couldn't detect/set launch_arguments...") + } + } + + open class func snapshot(_ name: String, timeWaitingForIdle timeout: TimeInterval = 20) { + if timeout > 0 { + waitForLoadingIndicatorToDisappear(within: timeout) + } + + print("snapshot: \(name)") // more information about this, check out https://docs.fastlane.tools/actions/snapshot/#how-does-it-work + + sleep(1) // Waiting for the animation to be finished (kind of) + + #if os(OSX) + XCUIApplication().typeKey(XCUIKeyboardKeySecondaryFn, modifierFlags: []) + #else + + guard let app = self.app else { + print("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().") + return + } + + guard let window = app.windows.allElementsBoundByIndex.first(where: { $0.frame.isEmpty == false }) else { + print("Couldn't find an element window in XCUIApplication with a non-empty frame.") + return + } + + let screenshot = window.screenshot() + guard let simulator = ProcessInfo().environment["SIMULATOR_DEVICE_NAME"], let screenshotsDir = screenshotsDirectory else { return } + let path = screenshotsDir.appendingPathComponent("\(simulator)-\(name).png") + do { + try screenshot.pngRepresentation.write(to: path) + } catch let error { + print("Problem writing screenshot: \(name) to \(path)") + print(error) + } + #endif + } + + class func waitForLoadingIndicatorToDisappear(within timeout: TimeInterval) { + #if os(tvOS) + return + #endif + + let networkLoadingIndicator = XCUIApplication().otherElements.deviceStatusBars.networkLoadingIndicators.element + let networkLoadingIndicatorDisappeared = XCTNSPredicateExpectation(predicate: NSPredicate(format: "exists == false"), object: networkLoadingIndicator) + _ = XCTWaiter.wait(for: [networkLoadingIndicatorDisappeared], timeout: timeout) + } + + class func pathPrefix() throws -> URL? { + let homeDir: URL + // on OSX config is stored in /Users//Library + // and on iOS/tvOS/WatchOS it's in simulator's home dir + #if os(OSX) + guard let user = ProcessInfo().environment["USER"] else { + throw SnapshotError.cannotDetectUser + } + + guard let usersDir = FileManager.default.urls(for: .userDirectory, in: .localDomainMask).first else { + throw SnapshotError.cannotFindHomeDirectory + } + + homeDir = usersDir.appendingPathComponent(user) + #else + #if arch(i386) || arch(x86_64) + guard let simulatorHostHome = ProcessInfo().environment["SIMULATOR_HOST_HOME"] else { + throw SnapshotError.cannotFindSimulatorHomeDirectory + } + guard let homeDirUrl = URL(string: simulatorHostHome) else { + throw SnapshotError.cannotAccessSimulatorHomeDirectory(simulatorHostHome) + } + homeDir = URL(fileURLWithPath: homeDirUrl.path) + #else + throw SnapshotError.cannotRunOnPhysicalDevice + #endif + #endif + return homeDir.appendingPathComponent("Library/Caches/tools.fastlane") + } +} + +private extension XCUIElementAttributes { + var isNetworkLoadingIndicator: Bool { + if hasWhiteListedIdentifier { return false } + + let hasOldLoadingIndicatorSize = frame.size == CGSize(width: 10, height: 20) + let hasNewLoadingIndicatorSize = frame.size.width.isBetween(46, and: 47) && frame.size.height.isBetween(2, and: 3) + + return hasOldLoadingIndicatorSize || hasNewLoadingIndicatorSize + } + + var hasWhiteListedIdentifier: Bool { + let whiteListedIdentifiers = ["GeofenceLocationTrackingOn", "StandardLocationTrackingOn"] + + return whiteListedIdentifiers.contains(identifier) + } + + func isStatusBar(_ deviceWidth: CGFloat) -> Bool { + if elementType == .statusBar { return true } + guard frame.origin == .zero else { return false } + + let oldStatusBarSize = CGSize(width: deviceWidth, height: 20) + let newStatusBarSize = CGSize(width: deviceWidth, height: 44) + + return [oldStatusBarSize, newStatusBarSize].contains(frame.size) + } +} + +private extension XCUIElementQuery { + var networkLoadingIndicators: XCUIElementQuery { + let isNetworkLoadingIndicator = NSPredicate { (evaluatedObject, _) in + guard let element = evaluatedObject as? XCUIElementAttributes else { return false } + + return element.isNetworkLoadingIndicator + } + + return self.containing(isNetworkLoadingIndicator) + } + + var deviceStatusBars: XCUIElementQuery { + let deviceWidth = XCUIApplication().frame.width + + let isStatusBar = NSPredicate { (evaluatedObject, _) in + guard let element = evaluatedObject as? XCUIElementAttributes else { return false } + + return element.isStatusBar(deviceWidth) + } + + return self.containing(isStatusBar) + } +} + +private extension CGFloat { + func isBetween(_ numberA: CGFloat, and numberB: CGFloat) -> Bool { + return numberA...numberB ~= self + } +} + +// Please don't remove the lines below +// They are used to detect outdated configuration files +// SnapshotHelperVersion [1.10] diff --git a/DemoCodeCoverage/fastlane/report.xml b/DemoCodeCoverage/fastlane/report.xml new file mode 100644 index 0000000..3a9172d --- /dev/null +++ b/DemoCodeCoverage/fastlane/report.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/DemoCodeCoverage/fastlane/screenshots/screenshots.html b/DemoCodeCoverage/fastlane/screenshots/screenshots.html new file mode 100644 index 0000000..6e0504f --- /dev/null +++ b/DemoCodeCoverage/fastlane/screenshots/screenshots.html @@ -0,0 +1,174 @@ + + + + fastlane/snapshot + + + + +
+ +
+
+ + + diff --git a/DemoCodeCoverage/fastlane/test_output/report.html b/DemoCodeCoverage/fastlane/test_output/report.html new file mode 100644 index 0000000..a09447f --- /dev/null +++ b/DemoCodeCoverage/fastlane/test_output/report.html @@ -0,0 +1,204 @@ + + + + + Test Results | xcpretty + + + + +
+
+

Test Results

+
+
+
+

6 tests

+ +
+
+ AllFailingPassing +
+
+
+
+ + +
+
+

CalculatorTests

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +

0.001s

+ +

testAddition_ShouldReturnRight_WhenInputParam1AndParam2AreValidValues

+ +

0.001s

+ +

testAddition_ShouldReturnZero_WhenInputParam1IsNilAndParam2IsEmpty

+ +

0.001s

+ +

testAddition_ShouldReturnZero_WhenInputParam1IsNilOrEmptyAndParam2IsRightValue

+ +

0.001s

+ +

testAddition_ShouldReturnZero_WhenInputParam1IsNotNumbericAndParam2IsRightValue

+ +

0.001s

+ +

testAddition_ShouldReturnZero_WhenInputParam2IsNilOrEmptyAndParam1IsRightValue

+ +

0.001s

+ +

testAddition_ShouldReturnZero_WhenInputParam2IsNotNumbericAndParam1IsRightValue

+
+
+ +
+ + + diff --git a/DemoCodeCoverage/fastlane/test_output/report.junit b/DemoCodeCoverage/fastlane/test_output/report.junit new file mode 100644 index 0000000..c368686 --- /dev/null +++ b/DemoCodeCoverage/fastlane/test_output/report.junit @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/DemoCodeCoverage/frankencover b/DemoCodeCoverage/frankencover new file mode 100755 index 0000000..cec38b4 --- /dev/null +++ b/DemoCodeCoverage/frankencover @@ -0,0 +1,380 @@ +#!/usr/bin/env groovy +import groovy.io.FileType +import org.apache.tools.ant.util.FileUtils + +import org.apache.commons.cli.Option + +import static org.fusesource.jansi.Ansi.Attribute.INTENSITY_BOLD +import static org.fusesource.jansi.Ansi.Attribute.INTENSITY_BOLD_OFF +import static org.fusesource.jansi.Ansi.Color.RED +import static org.fusesource.jansi.Ansi.Color.GREEN +import static org.fusesource.jansi.Ansi.Color.YELLOW +import static org.fusesource.jansi.Ansi.Color.DEFAULT +import static org.fusesource.jansi.Ansi.ansi + +new FrankenCliReader(args).execute() + +class FrankenCliReader +{ + + public String[] args + public CliBuilder cli + + public FrankenCliReader(args) + { + super() + this.args = args + this.cli = initCliBuilder() + } + + public void execute() + { + def opt = cli.parse(args) + if (!opt) { + System.exit(-1) + } + if (opt.h) { + cli.usage() + } + + + String outPutDir = opt.getProperty("o") ?: "build/reports" + String sourceDir = opt.getProperty("s") + List excludedFileNames = opt.es ?: ["main.m","queue.h","once.h","CGGeometry.h","MKGeometry.h","NSRange.h"] + + //Stop some non-source tree symbols leaking into the report + def reportConfig = new ReportConfig(directory: "${sourceDir}", excludeSymbols:excludedFileNames) + + def ideConfig = new IDEConfig(withXcode: true, withAppCodeIfAvailable: true) + if (opt.getProperty("x") || opt.getProperty("a")) { + ideConfig.withAppCodeIfAvailable = opt.getProperty("a") + ideConfig.withXcode = opt.getProperty("x") + } + if (opt.getProperty("p")) { + ideConfig.customDirectory = opt.getProperty("p") + } + + Double requiredCoverage = opt.getProperty("r") ? new Double(opt.getProperty("r").replaceAll("%", "")) : 0 + def generator = new ReportGenerator(outPutDir, requiredCoverage, reportConfig, ideConfig, new AntBuilder(), opt.getProperty("d"),opt.getProperty("n")) + generator.generate() + System.exit(generator.failed ? -1 : 0) + } + + private CliBuilder initCliBuilder() + { + def usageInstructions = """groovy http://frankencover.it/with -s \n\n + |For instructions on configuring Xcode to output coverage data, visit http://frankencover.it\n\n + """.stripMargin().stripIndent() + CliBuilder builder = new CliBuilder(usage: usageInstructions) + builder.with + { + h(longOpt: 'help', 'Help - Usage Information') + s(longOpt: 'source-dir', 'The source directory to generate coverage for', args: 1, type: String, required: true) + o(longOpt: 'output-dir', 'The output directory to write coverage report', args: 1, type: String, required: false) + p(longOpt: 'products-dir', 'Use custom directory to search for code coverage files, instead of the IDE default', args: 1, type: String, required: false) + r(longOpt: 'required-coverage', 'Required line coverage', args: 1, type: String, required: false) + a(longOpt: 'appcode', 'Search in AppCode\'s output directory. (default is both Xcode and AppCode\'s output dirs)', args: 0, type: boolean, required: false) + x(longOpt: 'xcode', 'Search in Xcode\'s output directory. (default is both Xcode and AppCode\'s output dirs)', args: 0, type: boolean, required: false) + d(longOpt: 'debug', 'Print debugging output.', type: boolean, required: false) + n(longOpt: 'no-html', 'Only output lcov data. No html report', args: 0, type: boolean, required: false) + e(longOpt: 'exclude', 'Comma separated list of symbols to exclude from report', args: Option.UNLIMITED_VALUES, valueSeparator: ',', type: String, required: false) + } + return builder + } +} + + +class ReportGenerator +{ + String outputDir + Double requiredCoverage + ReportConfig reportConfig + IDEConfig ideConfig + AntBuilder ant + boolean debug + boolean noHtml + boolean failed + + private File reportLocation + + // ================================================================ // + // Constructors + + ReportGenerator(outputDir, requiredCoverage, reportConfig, ideConfig, ant, debug, noHtml) + { + this.outputDir = outputDir + this.requiredCoverage = requiredCoverage + this.reportConfig = reportConfig + this.ideConfig = ideConfig + this.ant = ant + this.reportLocation = new File("${outputDir}/coverage") + this.debug = debug + this.noHtml = noHtml + + print ansi().a(INTENSITY_BOLD) + print ansi().fg(YELLOW).a("\nā–ø ") + print ansi().fg(DEFAULT).a("Code Coverage ").reset().a("${reportConfig.directory}\n") + } + + // ================================================================ // + // Public + + public void generate() + { + def tempDir = "${outputDir}/temp" + def collationDir = "${tempDir}/coverage-data-collate" + def coverageData = "${tempDir}/coverage-data" + def coverageInfoFile = "${tempDir}/coverage.info" + def genHtmlCmd = "genhtml -o ${escapeSpecialCharacters(reportLocation.absolutePath)} --prefix ${escapeSpecialCharacters(reportConfig.prefix())} ${escapeSpecialCharacters(coverageInfoFile)}" + def excludeSymbols = reportConfig.excludeSymbols.join(" ") + + if(this.noHtml) + { + genHtmlCmd = "" + } + + def script = """\ + |mkdir -p ${escapeSpecialCharacters(collationDir)} + |mkdir -p ${escapeSpecialCharacters(coverageData)} + |mkdir -p ${escapeSpecialCharacters(reportLocation.absolutePath)}/data + |find ${escapeSpecialCharacters(ideConfig.searchDirectories())} ${reportConfig.dataFileNames()} | rsync --files-from=- / ${escapeSpecialCharacters(collationDir)} + |find ${escapeSpecialCharacters(collationDir)} -type file -exec cp -fr {} ${escapeSpecialCharacters(coverageData)} \\; + |rm -fr ${escapeSpecialCharacters(collationDir)} + |geninfo ${escapeSpecialCharacters(coverageData)}/*.gcno --no-recursion --output-filename ${escapeSpecialCharacters(coverageInfoFile)}.temp + |lcov -r ${escapeSpecialCharacters(coverageInfoFile)}.temp ${excludeSymbols} > ${escapeSpecialCharacters(coverageInfoFile)} + |${genHtmlCmd} + |cp -R ${escapeSpecialCharacters(tempDir)}/* ${escapeSpecialCharacters(reportLocation.absolutePath)}/data + """.stripMargin() + + def commands = script.split("\n") + + for(command in commands) + { + if (debug) println(command) + Process process = "bash".execute(); + process.outputStream.write(command.getBytes()) + process.outputStream.close() + process.waitFor() + } + + printSummary(coverageInfoFile) + if(!debug) + "rm -fr ${escapeSpecialCharacters(tempDir)}".execute().waitFor() + + } + + private void printSummary(coverageInfoFile) + { + OutputStream os = new ByteArrayOutputStream() + Process summary = "lcov --summary ${escapeSpecialCharacters(coverageInfoFile)}".execute() + summary.consumeProcessOutput(os, os) + summary.waitFor() + + + def parser = new ReportParser(os.toString()) + parser.print() + + if (parser.lineCoveragePercent < requiredCoverage) { + print ansi().a(INTENSITY_BOLD).fg(RED).a(" <----- Required coverage is ${requiredCoverage}%").reset() + failed = true + } + print ansi().a("\n").reset() + if(this.noHtml == false) + { + print ansi().a(" šŸ“Š full report..: ${outputDir}/coverage/index.html") + } + print ansi().a("\n\n").reset() + } + + private String escapeSpecialCharacters(toEscape) + { + String escaped = toEscape.replace("(","\\("); + escaped = escaped.replace(")","\\)"); + } + + +} + + +class ReportParser +{ + + Double lineCoveragePercent; + String lineCoverageDetail; + + // ================================================================ // + // Constructors + + ReportParser(String summary) + { + boolean linesFound = false + summary.eachLine { + if (it.contains("lines.....")) { + def lineSummaries = it.split("%") + lineCoveragePercent = lineSummaries[0].split()[1].toDouble() + lineCoverageDetail = lineSummaries[1].trim() + linesFound = true + } + } + if (!linesFound) { + throw new RuntimeException(ansi().a(INTENSITY_BOLD).fg(RED).a("\n\nāœ— Coverage data not found\n").reset() + .a(" ☐ To see how to set build flags, visit http://frankencover.it\n") + .a(" ☐ Exercise your code with tests or otherwise.\n") + .a(" ☐ Run this script again.\n\n").toString()) + }; + } + + // ================================================================ // + // Public + + void print() + { + + print ansi().a(INTENSITY_BOLD) + if (lineCoveragePercent < 60) { + print ansi().fg(RED).a(" āœ—").reset() + print ansi().a(" lines........: ") + print ansi().fg(RED) + } + else if (lineCoveragePercent < 79) { + print ansi().fg(YELLOW).a(" ⚠").reset() + print ansi().a(" lines........: ") + print ansi().fg(YELLOW) + } + else { + print ansi().fg(GREEN).a(" āœ“").reset() + print ansi().a(" lines........: ") + print ansi().fg(GREEN) + } + + print ansi().a(INTENSITY_BOLD) + print ansi().a("${lineCoveragePercent}% ").reset() + print ansi().a("${lineCoverageDetail}") + } + + +} + +class ReportConfig +{ + + String directory + List excludeSymbols + + //Private + private List fileNames; + + // ================================================================ // + // Public + + String prefix() + { + def prefix + if (directory.startsWith("/")) { + prefix = directory + } + else { + def baseDir = new File(".").getAbsolutePath() + prefix = baseDir.substring(0, baseDir.length() - 1) + "${directory}" + } + return prefix + } + + String dataFileNames() + { + def dataFiles = new ArrayList(); + cachedFileNames().each { fileName -> + dataFiles.add("-name ${fileName}.gcno") + dataFiles.add("-name ${fileName}.gcda") + } + dataFiles.join(' -o ') + } + + public void setDirectory(String directory) + { + this.directory = directory.endsWith("/") ? directory : directory + "/" + } + + // ================================================================ // + // Private + + private List cachedFileNames() + { + if (!fileNames) { + fileNames = new ArrayList() + + def searchDir = directory.startsWith("/") ? new File(directory) : new File("./${directory}") + + searchDir.eachFileRecurse(FileType.FILES) { file -> + def fileName = file.name; + if (fileName.endsWith(".h")) { + fileNames << fileName.substring(0, fileName.length() - 2) + } + } + } + + //println ("Filenames: " + fileNames) + fileNames + } + + +} + +class IDEConfig +{ + + public boolean withAppCodeIfAvailable + public boolean withXcode + public String customDirectory + + def cachedAppCodeDir = null + + public String xcodeDir() + { + return "~/Library/Developer/Xcode/DerivedData" + } + + public String appCodeDirOrNull() + { + if (cachedAppCodeDir == null) { + def highestAppCodeInstalled = 0 + new File("${System.getProperty("user.home")}/Library/Caches").eachFile(FileType.DIRECTORIES) { directory -> + def dir = directory.name; + if (dir.startsWith("appCode")) { + def appCodeVersion = dir.replaceAll("[^\\d.]", "").toInteger() + highestAppCodeInstalled = appCodeVersion > highestAppCodeInstalled ? appCodeVersion : highestAppCodeInstalled + } + } + if (highestAppCodeInstalled) { + cachedAppCodeDir = "~/Library/Caches/appCode${highestAppCodeInstalled}" + } + } + return cachedAppCodeDir + } + + public String searchDirectories() + { + return customDirectory ? customDirectory : ideDirectories() + } + + public void setCustomDirectory(String customDirectory) { + println "setting custom dir to " + customDirectory + this.customDirectory = customDirectory + this.withAppCodeIfAvailable = false + this.withXcode = false + } + + private String ideDirectories() + { + def searchDirectories = new StringBuilder() + if (withXcode) { + searchDirectories.append(xcodeDir()) + searchDirectories.append(" ") + } + if (withAppCodeIfAvailable && appCodeDirOrNull() != null) { + searchDirectories.append(appCodeDirOrNull()) + } + return searchDirectories.toString() + } + +} \ No newline at end of file diff --git a/DemoCodeCoverage/output.xml b/DemoCodeCoverage/output.xml new file mode 100644 index 0000000..a581e21 --- /dev/null +++ b/DemoCodeCoverage/output.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/DemoCodeCoverage/xctool.sh b/DemoCodeCoverage/xctool.sh new file mode 100644 index 0000000..2401e1d --- /dev/null +++ b/DemoCodeCoverage/xctool.sh @@ -0,0 +1,8 @@ +#!/bin/sh +xctool test -project DemoCodeCoverage.xcodeproj \ +-scheme DemoCodeCoverage \ +-reporter plain \ +-sdk iphonesimulator | xcpretty -c --report junit + +#run frankencover +groovy frankencover -source-dir "DemoCodeCoverage/Classes" -required-coverage 80 \ No newline at end of file diff --git a/README.md b/README.md index cbee9d8..82a478c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,5 @@ # iOS-Code-Coverage -Code Coverage for iOS +[![Build Status](https://travis-ci.org/congpc/iOS-Code-Coverage.svg?branch=master)](https://travis-ci.org/congpc/iOS-Code-Coverage) ![Build Status](https://circleci.com/gh/congpc/iOS-Code-Coverage/tree/master.svg?style=shield&circle-token=a7ba955eeae027a1445789937eade51e20bbb958)
+ +- Source code of the tutorial at http://phamchicong.blogspot.com/2015/05/unit-test-huong-dan-config-frankencover.html +- Demo setup Travis-CI and Circle-CI diff --git a/images/XCode7_01.png b/images/XCode7_01.png new file mode 100644 index 0000000..cc29278 Binary files /dev/null and b/images/XCode7_01.png differ diff --git a/images/XCode7_02.png b/images/XCode7_02.png new file mode 100644 index 0000000..dde22f5 Binary files /dev/null and b/images/XCode7_02.png differ diff --git a/images/XCode7_03.png b/images/XCode7_03.png new file mode 100644 index 0000000..b0bbd1a Binary files /dev/null and b/images/XCode7_03.png differ diff --git a/images/XCode7_04.png b/images/XCode7_04.png new file mode 100644 index 0000000..7178edb Binary files /dev/null and b/images/XCode7_04.png differ diff --git a/images/XCode7_05.png b/images/XCode7_05.png new file mode 100644 index 0000000..37a6f12 Binary files /dev/null and b/images/XCode7_05.png differ diff --git a/images/XCode7_06.png b/images/XCode7_06.png new file mode 100644 index 0000000..068cdb0 Binary files /dev/null and b/images/XCode7_06.png differ diff --git a/images/XCode7_07.png b/images/XCode7_07.png new file mode 100644 index 0000000..b796752 Binary files /dev/null and b/images/XCode7_07.png differ diff --git a/images/XCode7_08.png b/images/XCode7_08.png new file mode 100644 index 0000000..f993e6b Binary files /dev/null and b/images/XCode7_08.png differ diff --git a/images/XCode7_09.png b/images/XCode7_09.png new file mode 100644 index 0000000..6cfb4e2 Binary files /dev/null and b/images/XCode7_09.png differ