diff --git a/.gitignore b/.gitignore index 95c4320..0b90dbe 100755 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ /Packages /*.xcodeproj xcuserdata/ +.swiftpm diff --git a/Docs/run_this_from_post_action.md b/Docs/run_this_from_post_action.md new file mode 100644 index 0000000..cd8bcbd --- /dev/null +++ b/Docs/run_this_from_post_action.md @@ -0,0 +1,79 @@ +## Add the following script as Post-action to your Target's scheme in Test section + +```sh +# Log file path where script actions should be written for debug purposes. +# Add as User-Defined Setting in Build Settings. +if [[ -z "$SUITCASE_LOG_FILE_PATH" ]]; then + SUITCASE_LOG_FILE_PATH="/dev/null 2>&1" +fi + +rm "$SUITCASE_LOG_FILE_PATH" + +echo "Started at $(date)" >> "$SUITCASE_LOG_FILE_PATH" + +if [[ $PLATFORM_NAME != "iphoneos" ]]; then + echo "Not running on iPhone, exiting" >> "$SUITCASE_LOG_FILE_PATH" + exit +fi + +# SUITCASE_ENABLE_DEVICE_IMAGES_SYNC +# Add as User-Defined Setting in Build Settings. Set to YES to explicitely enable sychronization of test images saved on device. +# As this script is intended to be placed in Post-action phase it will be executed at every launch +# despite of test kind (even if not related to images), that is not always desirable. +# Do not add this setting or set it to NO to disable images synchronization. +if [[ -z $SUITCASE_ENABLE_DEVICE_IMAGES_SYNC ]]; then + echo "Running on device, but SUITCASE_ENABLE_DEVICE_IMAGES_SYNC environment variable is not set, exiting" >> "$SUITCASE_LOG_FILE_PATH" + exit +fi + +if [[ -z $SUITCASE_ENABLE_DEVICE_IMAGES_SYNC || $SUITCASE_ENABLE_DEVICE_IMAGES_SYNC = "NO" ]]; then + echo "Testing on device is explicitely disabled, exiting" >> "$SUITCASE_LOG_FILE_PATH" + exit +fi + +# remote path - this path is valid if you install SUITCase as a remote package +SWIFT_PACKAGES_PATH="${BUILD_DIR%Build/*}SourcePackages/checkouts" +SCRIPT_PATH="$SWIFT_PACKAGES_PATH/suitcase/Scripts/get_device_screenshots.sh" + +if test -f "$SCRIPT_PATH"; then + echo "Script found at '$SCRIPT_PATH'" >> "$SUITCASE_LOG_FILE_PATH" +else + echo "Script is not found at '$SCRIPT_PATH'" >> "$SUITCASE_LOG_FILE_PATH" + + # Try local path if it is set, can be used if you install SUITCase as a local package. + # Add as User-Defined Setting in Build Settings. + if [[ -z $SUITCASE_LOCAL_SCRIPT_PATH ]]; then + echo "Local path is not set too, exiting" >> "$SUITCASE_LOG_FILE_PATH" + exit + fi + + SCRIPT_PATH=$SUITCASE_LOCAL_SCRIPT_PATH + + if test -f "$SCRIPT_PATH"; then + echo "Script found at '$SCRIPT_PATH'" >> "$SUITCASE_LOG_FILE_PATH" + else + echo "Script is not found at '$SCRIPT_PATH', exiting" >> "$SUITCASE_LOG_FILE_PATH" + exit + fi +fi + +# Path where to save test images retrieved from device. +# Add as User-Defined Setting in Build Settings. +if [[ -z "$SUITCASE_IMAGES_DIR" ]]; then + echo "SUITCASE_IMAGES_DIR environment variable is not set, exiting" >> "$SUITCASE_LOG_FILE_PATH" + exit +fi + +# Images relative path inside application container on device without leading slash. +# Add as User-Defined Setting in Build Settings. +if [[ -z "$SUITCASE_DEVICE_IMAGES_DIR" ]]; then + echo "SUITCASE_DEVICE_IMAGES_DIR environment variable is not set, exiting" >> "$SUITCASE_LOG_FILE_PATH" + exit +fi + +# Tests target bundle id. This will be a runner app where images will be saved +TESTS_TARGET_BUNDLE_ID="$PRODUCT_BUNDLE_IDENTIFIER" + +# Run configured script +"$SCRIPT_PATH" "$TESTS_TARGET_BUNDLE_ID" "$SUITCASE_DEVICE_IMAGES_DIR" "$SUITCASE_IMAGES_DIR" "$SUITCASE_LOG_FILE_PATH" +``` diff --git a/README.md b/README.md index 77fcc22..1db2604 100644 --- a/README.md +++ b/README.md @@ -130,5 +130,74 @@ Compares the average colors of screenshots. * You can also verify the average color without the reference screenshot by using `averageColorIs(_ uiColor: UIColor, tolerance: Double = 0.1)` \ `XCTAssert(app.buttons["Red Button"].averageColorIs(.red))` +## Experimental support for testing on real devices using `ifuse` library +Currently SUITCase is intended to be used mainly with iOS Simulator, because the latter allows seamless access to macOS filesystem (test screenshots can be saved directly to your Mac). This is not as on real devices, because tests are being run on device filesystem which has no direct access to macOS filesystem. + +However we can opt-in saving all screenshots during tests on device, mount xctrunner application container in macOS and this way copy screenshots from the testable device to Mac. Now, treating mounted container as an ordinary directory we can also copy images from Mac to device too (if needed). This enables two-way synchronization between device and Mac. This needs additional setup as follows + +### Setup + +1. Install [ifuse](https://github.com/libimobiledevice/ifuse) using Homebrew (based on [this article](https://habr.com/ru/post/459888/)) +* Install `osxfuse` +``` +brew install osxfuse +``` +* Install dependencies +``` +brew uninstall --ignore-dependencies libimobiledevice +brew uninstall --ignore-dependencies usbmuxd +#If you never installed libimobiledevice and usbmuxd before +#skip above commands +brew install --HEAD usbmuxd +brew unlink usbmuxd +brew link usbmuxd +brew install --HEAD libimobiledevice +``` +**Important**: If you've already installed stable `libimobiledevice` and `usbmuxd` versions remove them and install `dev` versions with `--HEAD` instead to avoid connection issues with iOS 12 +* Install `ifuse` +``` +brew install ifuse +``` + +2. Add [this script](Docs/run_this_from_post_action.md) as Post-action to your Target's scheme in Test section and configure it properly + +3. Make sure you selected a real device for testing and use the following code snippet to enable the feature +``` +class AppearanceTests: SUITCase { + override func setUp() { + super.setUp() + + deviceTestingEnabled = true + let imagesFolder = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent("Images").path + SUITCase.screenshotComparisonImagesFolder = imagesFolder + + XCUIApplication().launch() + } +} +``` + +4. Add the following User-Defined Settings in Build Settings of your UITests target. These settings are used in Post-action script. + + **SUITCASE_DEVICE_IMAGES_DIR** + + Images relative path inside application container on device without leading slash. +Make sure this relative path is the same as last path components retrieved from `FileManager`. For instance for `FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent("Images").path` this would be `Documents/Images` + + **SUITCASE_ENABLE_DEVICE_IMAGES_SYNC** + + Set to `YES` to explicitely enable sychronization of test images saved on device. As this script is intended to be placed in Post-action phase it will be executed at every launch despite of test kind (even if not related to images), that is not always desirable. Do not add this setting or set it to NO to disable images synchronization. + + **SUITCASE_IMAGES_DIR** + + Path where to save test images retrieved from device. This setting is similar to the `IMAGES_DIR` environment variable added at [Installation](#installation) step and you may assign the same value to it, it's been introduced to follow the common `SUITCASE_` prefix naming pattern and to distinct use cases, because `IMAGES_DIR` when set in Test scheme cannot be accessed inside Post-action script while a User-Defined Settings in Build Settings can. + + **SUITCASE_LOCAL_SCRIPT_PATH** + + Optional setting used if you install SUITCase as a local package, because in this case we cannot grab script path from standard build variables like `BUILD_DIR` + + **SUITCASE_LOG_FILE_PATH** + + Optional log file path where script actions should be written for debug purposes. + ## License SUITCase is the open-source software under [the MPL 2.0 license.](LICENSE) diff --git a/Scripts/get_device_screenshots.sh b/Scripts/get_device_screenshots.sh new file mode 100755 index 0000000..e430444 --- /dev/null +++ b/Scripts/get_device_screenshots.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +# +# See Docs/run_this_from_post_action.md for configuration script +# + +LOG_FILE_PATH="$4" + +MOUNT_DIR="$HOME/fuse_mount_point" +echo "Mount point $MOUNT_DIR" >> "$LOG_FILE_PATH" + +mkdir "$MOUNT_DIR" + +# get input + +TESTS_TARGET_BUNDLE_ID="$1.xctrunner" +TEST_IMAGES_SOURCE_PATH="$MOUNT_DIR/$2" +TEST_IMAGES_DESTINATION_PATH="$3" + +echo "Got following data:" >> "$LOG_FILE_PATH" +echo "-- Bundle identifier '$TESTS_TARGET_BUNDLE_ID'" >> "$LOG_FILE_PATH" +echo "-- Images source path '$TEST_IMAGES_SOURCE_PATH'" >> "$LOG_FILE_PATH" +echo "-- Images destination path '$TEST_IMAGES_DESTINATION_PATH'" >> "$LOG_FILE_PATH" + +# mount app container + +echo "(Re)mounting '$TESTS_TARGET_BUNDLE_ID'" >> "$LOG_FILE_PATH" +# unmount container (if already mounted) to avoid copying error, +# make it forcibly (with -f flag) to get updated mounted container +umount -f -v "$MOUNT_DIR" >> "$LOG_FILE_PATH" 2>&1 +# mount container +ifuse --debug --container $TESTS_TARGET_BUNDLE_ID "$MOUNT_DIR" >> "$LOG_FILE_PATH" 2>&1 + +# sync images + +echo "Syncing images between $TEST_IMAGES_SOURCE_PATH and $TEST_IMAGES_DESTINATION_PATH" >> "$LOG_FILE_PATH" +echo "-- source -> target" >> "$LOG_FILE_PATH" +rsync -rtuv "$TEST_IMAGES_SOURCE_PATH/" "$TEST_IMAGES_DESTINATION_PATH" >> "$LOG_FILE_PATH" 2>&1 +echo "-- target -> source" >> "$LOG_FILE_PATH" +rsync -rtuv "$TEST_IMAGES_DESTINATION_PATH/" "$TEST_IMAGES_SOURCE_PATH" >> "$LOG_FILE_PATH" 2>&1 + + + diff --git a/Sources/SUITCase/RGBAImage.swift b/Sources/SUITCase/RGBAImage.swift index 74662c1..cb7daff 100644 --- a/Sources/SUITCase/RGBAImage.swift +++ b/Sources/SUITCase/RGBAImage.swift @@ -8,7 +8,6 @@ If a copy of the MPL was not distributed with this file, You can obtain one at h See https://code.devexperts.com for more open source projects */ - import XCTest /// A structure describing image built with RGBA pixels. diff --git a/Sources/SUITCase/SUITCase+getImagePaths.swift b/Sources/SUITCase/SUITCase+getImagePaths.swift index a036a32..bd5990a 100755 --- a/Sources/SUITCase/SUITCase+getImagePaths.swift +++ b/Sources/SUITCase/SUITCase+getImagePaths.swift @@ -11,7 +11,7 @@ import XCTest @available(iOS 12.0, *) @available(tvOS 10.0, *) extension SUITCase { - static var screenshotComparisonImagesFolder = ProcessInfo.processInfo.environment["IMAGES_DIR"] + public static var screenshotComparisonImagesFolder = ProcessInfo.processInfo.environment["IMAGES_DIR"] /// The enumeration of possible reference screenshots naming strategies. public enum ScreenshotComparisonNamingStrategies { diff --git a/Sources/SUITCase/SUITCase+verifyScreenshot.swift b/Sources/SUITCase/SUITCase+verifyScreenshot.swift index 85a25ca..5fd651a 100644 --- a/Sources/SUITCase/SUITCase+verifyScreenshot.swift +++ b/Sources/SUITCase/SUITCase+verifyScreenshot.swift @@ -101,7 +101,7 @@ extension SUITCase { withThreshold customThreshold: Double? = nil, withMethod method: SUITCaseMethod = SUITCaseMethodWithTolerance(), withLabel label: String? = nil) throws { - guard UIDevice.isSimulator else { + guard UIDevice.isSimulator || deviceTestingEnabled else { throw VerifyScreenshotError.notSimulator } diff --git a/Sources/SUITCase/SUITCase.swift b/Sources/SUITCase/SUITCase.swift index 439b8e8..949845f 100755 --- a/Sources/SUITCase/SUITCase.swift +++ b/Sources/SUITCase/SUITCase.swift @@ -27,4 +27,6 @@ open class SUITCase: XCTestCase { public var screenshotComparisonGlobalThreshold = 0.01 /// Changes current reference images naming strategy. public var screenshotComparisonNamingStrategy = ScreenshotComparisonNamingStrategies.imageSize + /// Enables testing on real devices, is ignored when run on Simulator. + public var deviceTestingEnabled = false } diff --git a/Sources/SUITCase/UIDevice+modelName.swift b/Sources/SUITCase/UIDevice+modelName.swift index ef8ebc4..e114263 100755 --- a/Sources/SUITCase/UIDevice+modelName.swift +++ b/Sources/SUITCase/UIDevice+modelName.swift @@ -87,8 +87,8 @@ extension UIDevice { }() #if targetEnvironment(simulator) - static let isSimulator = true + public static let isSimulator = true #else - static let isSimulator = false + public static let isSimulator = false #endif }