Skip to content

Commit

Permalink
[ISSUE-78] Test orchestrator support (#273)
Browse files Browse the repository at this point in the history
* [ISSUE-78] Updated facebook screenshot plugin, migrated from xml metadata parser to json, initial solution to gather and pull images

* Assembling images but not worked for flavors

* Clear orchestrated folder from screenshots too

* Changed screenshot paths

* Uncommented code

* Working screenshot testing for compose

* Check if orchestrator connected by test options

* Fixed tests

* Fixed code style

* Fixed code style

* Fixed broken compose screenshotting without orchestrator

* Fixed names and reverted version of shot in config and consumer

* Fixed code style

* Reverted empty line

* Fixed code style

* Fixed tests

* Fixed issues, doesn't work with composer

* Fixed compose screenshot testing

* Fixed tests

* Rename all metadata json files

* Fixed code style

* Fixed code style, tests

* Add information about orchestrator in composer section

* Added additional steps for running CI checks with enabled orchestrator

* Fixed syntax

* Removed logging
  • Loading branch information
mariuszmarzec authored Jan 24, 2022
1 parent 121affa commit 6288ac3
Show file tree
Hide file tree
Showing 29 changed files with 1,001 additions and 610 deletions.
17 changes: 17 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,23 @@ jobs:
- name: Execute screenshot tests for shot-consumer-flavors blue flavor
run: ./gradlew blueCustomBuildTypeExecuteScreenshotTests
working-directory: shot-consumer-flavors
- name: Set orchestrator enabled
run: echo "orchestrated=true" >> $GITHUB_ENV
- name: Execute screenshot tests with orchestrator for shot-consumer-library-no-tests
run: ./gradlew executeScreenshotTests
working-directory: shot-consumer-library-no-tests
- name: Execute screenshot tests with orchestrator for shot-consumer-compose
run: ./gradlew executeScreenshotTests
working-directory: shot-consumer-compose
- name: Execute screenshot tests with orchestrator for shot-consumer
run: ./gradlew executeScreenshotTests
working-directory: shot-consumer
- name: Execute screenshot tests with orchestrator for shot-consumer-flavors green flavor
run: ./gradlew greenCustomBuildTypeExecuteScreenshotTests
working-directory: shot-consumer-flavors
- name: Execute screenshot tests with orchestrator for shot-consumer-flavors blue flavor
run: ./gradlew blueCustomBuildTypeExecuteScreenshotTests
working-directory: shot-consumer-flavors
- uses: actions/upload-artifact@v2
if: always()
with:
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,16 @@ shot {
}
```
If you are using orchestrator remember to enable it in composer configuration:
```groovy
composer {
// ...
withOrchestrator true
// ...
}
```
## Tolerance
Shot provides a simple mechanism to be able to configure a threshold value when comparing recorded images with the new ones during the verification stage. You may need to use tolerance in your tests when testing compose components because the API Shot uses to record screenshots depending on the device where your tests are executed. There are other scenarios where you may need to configure a tolerance value, but these are not so common. If you want to configure it you can use this config in your ``build.gradle`` file.
Expand Down
80 changes: 55 additions & 25 deletions core/src/main/scala/com/karumi/shot/Shot.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,17 @@ import com.karumi.shot.screenshots.{
}
import com.karumi.shot.system.EnvVars
import com.karumi.shot.ui.Console
import com.karumi.shot.xml.ScreenshotsSuiteXmlParser._
import com.karumi.shot.xml.ScreenshotsSuiteJsonParser._
import org.apache.commons.io.FileUtils
import org.tinyzip.TinyZip

import java.io.File
import java.nio.file.Paths
import scala.collection.convert.ImplicitConversions.{
`collection AsScalaIterable`,
`collection asJava`
}
import scala.collection.immutable.Stream.Empty

class Shot(
adb: Adb,
Expand All @@ -34,14 +39,14 @@ class Shot(
Adb.adbBinaryPath = adbPath
}

def downloadScreenshots(appId: AppId, shotFolder: ShotFolder): Unit = {
def downloadScreenshots(appId: AppId, shotFolder: ShotFolder, orchestrated: Boolean): Unit = {
console.show("⬇️ Pulling screenshots from your connected devices!")
pullScreenshots(appId, shotFolder)
pullScreenshots(appId, shotFolder, orchestrated)
}

def recordScreenshots(appId: AppId, shotFolder: ShotFolder): Unit = {
def recordScreenshots(appId: AppId, shotFolder: ShotFolder, orchestrated: Boolean): Unit = {
console.show("💾 Saving screenshots.")
moveComposeScreenshotsToRegularScreenshotsFolder(shotFolder)
moveComposeScreenshotsToRegularScreenshotsFolder(shotFolder, orchestrated)
val composeScreenshotSuite = recordComposeScreenshots(shotFolder)
val regularScreenshotSuite = recordRegularScreenshots(shotFolder)
if (regularScreenshotSuite.isEmpty && composeScreenshotSuite.isEmpty) {
Expand All @@ -65,10 +70,11 @@ class Shot(
projectName: String,
shouldPrintBase64Error: Boolean,
tolerance: Double,
showOnlyFailingTestsInReports: Boolean
showOnlyFailingTestsInReports: Boolean,
orchestrated: Boolean
): ScreenshotsComparisionResult = {
console.show("🔎 Comparing screenshots with previous ones.")
moveComposeScreenshotsToRegularScreenshotsFolder(shotFolder)
moveComposeScreenshotsToRegularScreenshotsFolder(shotFolder, orchestrated)
val regularScreenshots = readScreenshotsMetadata(shotFolder)
val composeScreenshots = readComposeScreenshotsMetadata(shotFolder)
if (regularScreenshots.isEmpty && composeScreenshots.isEmpty) {
Expand Down Expand Up @@ -132,20 +138,25 @@ class Shot(
}
}

def removeScreenshots(appId: AppId): Unit =
clearScreenshots(appId)
def removeScreenshots(appId: AppId, orchestrated: Boolean): Unit =
clearScreenshots(appId, orchestrated)

private def moveComposeScreenshotsToRegularScreenshotsFolder(
shotFolder: ShotFolder
shotFolder: ShotFolder,
orchestrated: Boolean
): Unit = {
val composeFolder = shotFolder.pulledComposeScreenshotsFolder()
files.listFilesInFolder(composeFolder).forEach { file: File =>
val composeFolder = shotFolder.pulledComposeScreenshotsFolder()
var fileList: Iterable[File] = Empty
if (orchestrated) {
val orchestratedComposeFolder = shotFolder.pulledComposeOrchestratedScreenshotsFolder()
fileList =
files.listFilesInFolder(composeFolder) ++ files.listFilesInFolder(orchestratedComposeFolder)
} else {
fileList = files.listFilesInFolder(composeFolder)
}
fileList.forEach { file: File =>
val rawFilePath = file.getAbsolutePath
val newFilePath =
rawFilePath.replace(
shotFolder.pulledComposeScreenshotsFolder(),
shotFolder.pulledScreenshotsFolder()
)
val newFilePath = shotFolder.pulledScreenshotsFolder() + file.getName
files.rename(rawFilePath, newFilePath)
}
}
Expand Down Expand Up @@ -175,8 +186,9 @@ class Shot(
}
}

private def clearScreenshots(appId: AppId): Unit = forEachDevice { device =>
adb.clearScreenshots(device, appId)
private def clearScreenshots(appId: AppId, orchestrated: Boolean): Unit = forEachDevice {
device =>
adb.clearScreenshots(device, appId, orchestrated)
}

private def forEachDevice[T](f: String => T): Unit = devices().foreach(f)
Expand All @@ -197,17 +209,32 @@ class Shot(

private def pullScreenshots(
appId: AppId,
shotFolder: ShotFolder
shotFolder: ShotFolder,
orchestrated: Boolean
): Unit =
forEachDevice { device =>
val screenshotsFolder = shotFolder.screenshotsFolder()
createScreenshotsFolderIfDoesNotExist(screenshotsFolder)
removeProjectTemporalScreenshotsFolder(shotFolder)
adb.pullScreenshots(device, screenshotsFolder, appId)
adb.pullScreenshots(device, screenshotsFolder, appId, orchestrated)

extractPicturesFromBundle(shotFolder.pulledScreenshotsFolder())
files.rename(shotFolder.metadataFile(), s"${shotFolder.metadataFile()}_$device")
files.rename(shotFolder.composeMetadataFile(), s"${shotFolder.composeMetadataFile()}_$device")

files
.listFilesInFolder(shotFolder.pulledScreenshotsFolder())
.filter(file => file.getAbsolutePath.contains(shotFolder.metadataFileName()))
.foreach(file => {
val filePath = shotFolder.pulledScreenshotsFolder() + file.getName
files.rename(filePath, s"${filePath}_$device")
})

files
.listFilesInFolder(shotFolder.pulledComposeOrchestratedScreenshotsFolder())
.filter(file => file.getAbsolutePath.contains(shotFolder.composeMetadataFileName()))
.foreach(file => {
val filePath = shotFolder.pulledComposeOrchestratedScreenshotsFolder() + file.getName
files.rename(filePath, s"${filePath}_$device")
})
}

private def readScreenshotsMetadata(
Expand All @@ -218,7 +245,7 @@ class Shot(
if (folder.exists()) {
val filesInScreenshotFolder = folder.listFiles
val metadataFiles =
filesInScreenshotFolder.filter(file => file.getAbsolutePath.contains("metadata.xml"))
filesInScreenshotFolder.filter(file => file.getAbsolutePath.contains("metadata.json"))
val screenshotSuite = metadataFiles.flatMap { metadataFilePath =>
val metadataFileContent = files.read(metadataFilePath.getAbsolutePath)
parseScreenshots(
Expand Down Expand Up @@ -247,7 +274,9 @@ class Shot(
if (folder.exists()) {
val filesInScreenshotFolder = folder.listFiles
val metadataFiles =
filesInScreenshotFolder.filter(file => file.getAbsolutePath.contains("metadata.json"))
filesInScreenshotFolder.filter(file =>
file.getAbsolutePath.contains(shotFolder.composeMetadataFileName())
)
val screenshotSuite = metadataFiles.flatMap { metadataFilePath =>
val metadataFileContent = files.read(metadataFilePath.getAbsolutePath)
ScreenshotsComposeSuiteJsonParser.parseScreenshots(
Expand All @@ -271,6 +300,7 @@ class Shot(
private def removeProjectTemporalScreenshotsFolder(shotFolder: ShotFolder): Unit = {
FileUtils.deleteDirectory(new File(shotFolder.pulledScreenshotsFolder()))
FileUtils.deleteDirectory(new File(shotFolder.pulledComposeScreenshotsFolder()))
FileUtils.deleteDirectory(new File(shotFolder.pulledComposeOrchestratedScreenshotsFolder()))
}

private def extractPicturesFromBundle(screenshotsFolder: String): Unit = {
Expand Down
43 changes: 38 additions & 5 deletions core/src/main/scala/com/karumi/shot/android/Adb.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.karumi.shot.android

import com.karumi.shot.android.Adb.baseStoragePath
import com.karumi.shot.android.Adb.{baseStoragePath}
import com.karumi.shot.domain.model.{AppId, Folder}

import scala.sys.process._
Expand Down Expand Up @@ -32,14 +32,44 @@ class Adb {
.filter(device => !isCarriageReturnASCII(device))
}

def pullScreenshots(device: String, screenshotsFolder: Folder, appId: AppId): Unit = {
pullFolder("screenshots-default", device, screenshotsFolder, appId)
def pullScreenshots(
device: String,
screenshotsFolder: Folder,
appId: AppId,
orchestrated: Boolean
): Unit = {
pullFolder(
s"screenshots-default${orchestratedSuffix(orchestrated)}",
device,
screenshotsFolder,
appId
)
pullFolder("screenshots-compose-default", device, screenshotsFolder, appId)
if (orchestrated) {
pullFolder(
s"screenshots-compose-default${orchestratedSuffix(orchestrated)}",
device,
screenshotsFolder,
appId
)
}
}

def clearScreenshots(device: String, appId: AppId): Unit = {
def clearScreenshots(device: String, appId: AppId, orchestrated: Boolean): Unit = {
clearScreenshotsFromFolder(device, appId, "screenshots-default")
clearScreenshotsFromFolder(device, appId, "screenshots-compose-default")
if (orchestrated) {
clearScreenshotsFromFolder(
device,
appId,
s"screenshots-default${orchestratedSuffix(orchestrated)}"
)
clearScreenshotsFromFolder(
device,
appId,
s"screenshots-compose-default${orchestratedSuffix(orchestrated)}"
)
}
}

private def pullFolder(
Expand All @@ -59,8 +89,9 @@ class Adb {
}
}

private def clearScreenshotsFromFolder(device: String, appId: AppId, folder: AppId) =
private def clearScreenshotsFromFolder(device: String, appId: AppId, folder: AppId): Unit = {
executeAdbCommand(s"-s $device shell rm -r $baseStoragePath/screenshots/$appId/$folder/")
}

private def executeAdbCommand(command: String): Int =
s"${Adb.adbBinaryPath} $command" ! logger
Expand All @@ -70,4 +101,6 @@ class Adb {

private def isCarriageReturnASCII(device: String): Boolean =
device.charAt(0) == CR_ASCII_DECIMAL

private def orchestratedSuffix(orchestrated: Boolean) = if (orchestrated) "-orchestrated" else ""
}
23 changes: 19 additions & 4 deletions core/src/main/scala/com/karumi/shot/domain/ShotFolder.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@ case class ShotFolder(
private val buildType: String,
private val flavor: Option[String],
private val directorySuffix: Option[String],
private val separator: String
private val separator: String,
private val orchestrated: Boolean
) {

private val orchestratedSuffix = if (orchestrated) "-orchestrated" else ""

private def pathSuffix(): String = {
s"${flavor.fold("") { s => s"$s$separator" }}" +
s"$buildType$separator" +
Expand All @@ -22,19 +25,31 @@ case class ShotFolder(
}

def pulledScreenshotsFolder(): FilePath = {
s"${screenshotsFolder()}screenshots-default$separator"
s"${screenshotsFolder()}screenshots-default$orchestratedSuffix$separator"
}

def pulledComposeScreenshotsFolder(): FilePath = {
s"${screenshotsFolder()}screenshots-compose-default$separator"
}

def pulledComposeOrchestratedScreenshotsFolder(): FilePath = {
s"${screenshotsFolder()}screenshots-compose-default$orchestratedSuffix$separator"
}

def metadataFile(): FilePath = {
pulledScreenshotsFolder() + s"metadata.xml"
pulledScreenshotsFolder() + s"metadata.json"
}

def metadataFileName(): FilePath = {
"metadata.json"
}

def composeMetadataFile(): FilePath = {
pulledComposeScreenshotsFolder() + s"metadata.json"
pulledComposeScreenshotsFolder() + composeMetadataFileName()
}

def composeMetadataFileName(): FilePath = {
"metadata_compose.json"
}

def reportFolder(): FilePath = {
Expand Down
Loading

0 comments on commit 6288ac3

Please sign in to comment.