Skip to content

Commit

Permalink
Merge pull request #183 from ndtp/increase-code-coverage
Browse files Browse the repository at this point in the history
Add Jacoco for code coverage and add unit tests
  • Loading branch information
DanielJette authored Oct 24, 2023
2 parents f7bd617 + 5cd78c6 commit 51953ca
Show file tree
Hide file tree
Showing 53 changed files with 2,485 additions and 197 deletions.
41 changes: 41 additions & 0 deletions Library/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ plugins {
id 'com.android.library'
id 'kotlin-android'
id 'org.jetbrains.dokka'
id 'jacoco'
}

ext {
Expand All @@ -22,6 +23,8 @@ version = "$project.versions.testify"
group = pom.publishedGroupId
archivesBaseName = pom.artifact

jacoco { toolVersion = "0.8.10" }

android {
compileSdkVersion coreVersions.compileSdk

Expand Down Expand Up @@ -69,6 +72,7 @@ android {
testImplementation "io.mockk:mockk:${versions.mockk}"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:${versions.kotlinx}"
testImplementation "org.slf4j:slf4j-jdk14:${versions.slf4j}"
testImplementation "com.google.truth:truth:${versions.truth}"

androidTestImplementation "androidx.test.ext:junit:${versions.androidx.test.junit}"
androidTestImplementation "androidx.test:runner:${versions.androidx.test.runner}"
Expand All @@ -84,6 +88,7 @@ android {
abortOnError true
textOutput file('stdout')
textReport true
htmlReport true
warningsAsErrors true
xmlReport false
}
Expand All @@ -109,3 +114,39 @@ afterEvaluate {
}

apply from: '../ktlint.gradle'

tasks.withType(Test) {
jacoco.includeNoLocationClasses = true
jacoco.excludes = ['jdk.internal.*']
}

project.afterEvaluate {
android.buildTypes.each { buildType ->
def testTaskName = "test${buildType.name.capitalize()}UnitTest"
task "${testTaskName}Coverage"(type: JacocoReport, dependsOn: ["$testTaskName"]) {
group = "Reporting"
description = "Generate Jacoco coverage reports for the $testTaskName"
reports {
html.required = true
xml.required = true
}
def excludes = [
'**/dev/testify/core/processor/capture/*',
'**/dev/testify/extensions/ViewExtensionsKt*',
'**/dev/testify/internal/extensions/LocaleExtensionsKt*',
'**/dev/testify/internal/helpers/AssetLoaderKt*',
'**/dev/testify/internal/helpers/OrientationHelperKt*',
'**/dev/testify/internal/helpers/WrappedFontScaleKt*',
'**/dev/testify/core/processor/compare/SameAsCompare*',
'**/dev/testify/output/*',
'**/dev/testify/resources/TestifyResourcesOverride*',
'**/dev/testify/ScreenshotUtilityKt*',
]
def javaClasses = fileTree(dir: "${buildDir}/intermediates/javac/debug/classes", excludes: excludes)
def kotlinClasses = fileTree(dir: "${buildDir}/tmp/kotlin-classes/debug", excludes: excludes)
classDirectories.setFrom(files([javaClasses, kotlinClasses]))
sourceDirectories.setFrom(files(["$project.projectDir/src/main/java"]))
executionData(files("${project.buildDir}/jacoco/${testTaskName}.exec"))
}
}
}
41 changes: 41 additions & 0 deletions Library/src/main/java/dev/testify/ActivityLaunchCycle.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2023 ndtp
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package dev.testify

interface ActivityLaunchCycle {

/**
* Invoked before the Activity under test has been launched.
* Invoked after @Before methods.
* Invoked after beforeAssertSame()
*/
fun beforeActivityLaunched()

/**
* Invoked after the Activity under test has been launched.
* Invoked after @Before methods.
* Invoked after beforeAssertSame()
*/
fun afterActivityLaunched()
}
24 changes: 13 additions & 11 deletions Library/src/main/java/dev/testify/ScreenshotRule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import dev.testify.core.logic.AssertionState
import dev.testify.core.logic.ScreenshotLifecycleHost
import dev.testify.core.logic.ScreenshotLifecycleObserver
import dev.testify.core.logic.assertSame
import dev.testify.internal.annotation.ExcludeFromJacocoGeneratedReport
import dev.testify.internal.extensions.isInvokedFromPlugin
import dev.testify.internal.helpers.ActivityProvider
import dev.testify.internal.helpers.EspressoActions
Expand All @@ -76,10 +77,12 @@ open class ScreenshotRule<T : Activity> @JvmOverloads constructor(
TestRule,
ActivityProvider<T>,
ScreenshotLifecycle,
ActivityLaunchCycle,
AssertionState,
ScreenshotLifecycleHost by ScreenshotLifecycleObserver(),
CompatibilityMethods<ScreenshotRule<T>, T> by ScreenshotRuleCompatibilityMethods() {

@ExcludeFromJacocoGeneratedReport
@Deprecated(
message = "Parameter launchActivity is deprecated and no longer required",
replaceWith = ReplaceWith("ScreenshotRule(activityClass = activityClass, rootViewId = rootViewId, initialTouchMode = initialTouchMode, enableReporter = enableReporter, configuration = TestifyConfiguration())") // ktlint-disable max-line-length
Expand All @@ -106,7 +109,7 @@ open class ScreenshotRule<T : Activity> @JvmOverloads constructor(
override var screenshotViewProvider: ViewProvider? = null
override var throwable: Throwable? = null
override var viewModification: ViewModification? = null
private var extrasProvider: ExtrasProvider? = null
@VisibleForTesting internal var extrasProvider: ExtrasProvider? = null

@VisibleForTesting
internal var reporter: Reporter? = null
Expand Down Expand Up @@ -281,21 +284,26 @@ open class ScreenshotRule<T : Activity> @JvmOverloads constructor(
return this
}

public final override fun getActivityIntent(): Intent {
public final override fun getActivityIntent(): Intent? {
var intent: Intent? = super.getActivityIntent()
if (intent == null) {
intent = getIntent()
}

extrasProvider?.let {
val bundle = Bundle()
it(bundle)
val bundle = invokeExtrasProvider()
intent.extras?.putAll(bundle) ?: intent.replaceExtras(bundle)
}

return intent
}

@ExcludeFromJacocoGeneratedReport
@VisibleForTesting
internal fun invokeExtrasProvider(): Bundle =
Bundle().apply { extrasProvider?.invoke(this) }

@ExcludeFromJacocoGeneratedReport
@VisibleForTesting
internal fun getIntent(): Intent {
var intent = super.getActivityIntent()
Expand All @@ -309,6 +317,7 @@ open class ScreenshotRule<T : Activity> @JvmOverloads constructor(
launchActivity(intent)
}

@ExcludeFromJacocoGeneratedReport
override fun launchActivity(startIntent: Intent?): T {
try {
return super.launchActivity(startIntent)
Expand Down Expand Up @@ -389,11 +398,4 @@ open class ScreenshotRule<T : Activity> @JvmOverloads constructor(
reporter?.fail(throwable)
throw throwable
}

@VisibleForTesting
var isDebugMode: Boolean = false
set(value) {
field = value
assertSameInvoked = value
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ package dev.testify.core
import android.content.Context
import android.content.res.AssetManager
import dev.testify.core.exception.UnexpectedDeviceException
import dev.testify.internal.annotation.ExcludeFromJacocoGeneratedReport
import dev.testify.output.SCREENSHOT_DIR
import java.io.File
import dev.testify.internal.extensions.TestInstrumentationRegistry.Companion.isRecordMode as recordMode
Expand All @@ -42,6 +43,7 @@ import dev.testify.internal.extensions.TestInstrumentationRegistry.Companion.isR
* @param context - [Context] of the test instrumentation's package
* @param testName - The name of the currently running test
*/
@ExcludeFromJacocoGeneratedReport
fun assertExpectedDevice(context: Context, testName: String, isRecordMode: Boolean) {
if (isRecordMode || recordMode) return

Expand Down
18 changes: 17 additions & 1 deletion Library/src/main/java/dev/testify/core/ConfigurationBuilder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,11 @@
package dev.testify.core

import android.app.Activity
import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
import android.view.View
import androidx.annotation.IdRes
import androidx.annotation.VisibleForTesting
import dev.testify.CaptureMethod
import dev.testify.CompareMethod
import dev.testify.ScreenshotRule
Expand Down Expand Up @@ -101,6 +104,9 @@ class ConfigurationBuilder<T : Activity> internal constructor(private val rule:
}

fun setOrientation(requestedOrientation: Int): ConfigurationBuilder<T> {
require(
requestedOrientation in SCREEN_ORIENTATION_LANDSCAPE..SCREEN_ORIENTATION_PORTRAIT
)
innerConfiguration.orientation = requestedOrientation
return this
}
Expand Down Expand Up @@ -132,7 +138,16 @@ class ConfigurationBuilder<T : Activity> internal constructor(private val rule:
return this
}

private fun build(): TestifyConfiguration.() -> Unit = {
/**
* Record a new baseline when running the test
*/
fun setRecordModeEnabled(isRecordMode: Boolean): ConfigurationBuilder<T> {
innerConfiguration.isRecordMode = isRecordMode
return this
}

@VisibleForTesting
internal fun build(): TestifyConfiguration.() -> Unit = {
this.exactness = innerConfiguration.exactness
this.exclusionRectProvider = innerConfiguration.exclusionRectProvider
this.exclusionRects.addAll(innerConfiguration.exclusionRects)
Expand All @@ -149,6 +164,7 @@ class ConfigurationBuilder<T : Activity> internal constructor(private val rule:
this.useSoftwareRenderer = innerConfiguration.useSoftwareRenderer
this.captureMethod = innerConfiguration.captureMethod
this.compareMethod = innerConfiguration.compareMethod
this.isRecordMode = innerConfiguration.isRecordMode
}

fun assertSame() {
Expand Down
3 changes: 2 additions & 1 deletion Library/src/main/java/dev/testify/core/DeviceIdentifier.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import android.content.Context
import android.util.DisplayMetrics
import android.view.WindowManager
import dev.testify.internal.extensions.languageTag
import dev.testify.internal.helpers.buildVersionSdkInt
import java.util.Locale

typealias TestName = Pair<String, String>
Expand Down Expand Up @@ -94,7 +95,7 @@ open class DeviceStringFormatter(private val context: Context, private val testN
get() = getDeviceDimensions(context)

internal open val androidVersion: String
get() = android.os.Build.VERSION.SDK_INT.toString()
get() = buildVersionSdkInt().toString()

internal open val deviceWidth: String
get() = dimensions.first.toString()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import android.view.ViewGroup
import androidx.annotation.FloatRange
import androidx.annotation.IdRes
import androidx.annotation.UiThread
import androidx.annotation.VisibleForTesting
import androidx.annotation.WorkerThread
import dev.testify.CaptureMethod
import dev.testify.CompareMethod
Expand Down Expand Up @@ -100,10 +101,12 @@ data class TestifyConfiguration(
val hasExactness: Boolean
get() = exactness != null

private val orientationHelper: OrientationHelper?
@VisibleForTesting
internal val orientationHelper: OrientationHelper?
get() = orientation?.let { OrientationHelper(it) }

private var ignoreAnnotation: IgnoreScreenshot? = null
@VisibleForTesting
internal var ignoreAnnotation: IgnoreScreenshot? = null

/**
* Update the internal configuration values based on any annotations that may be present on the test method
Expand Down
20 changes: 12 additions & 8 deletions Library/src/main/java/dev/testify/core/logic/AssertSame.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import android.app.Activity
import android.content.Context
import android.content.Intent
import android.view.View
import androidx.annotation.VisibleForTesting
import androidx.test.platform.app.InstrumentationRegistry
import dev.testify.TestifyFeatures
import dev.testify.core.DEFAULT_FOLDER_FORMAT
Expand Down Expand Up @@ -99,7 +100,6 @@ internal fun <TActivity : Activity> assertSame(
} catch (e: ScreenshotTestIgnoredException) {
// Exit gracefully; mark test as ignored
Assume.assumeTrue(false)
return
}

var activity: TActivity? = null
Expand Down Expand Up @@ -129,9 +129,8 @@ internal fun <TActivity : Activity> assertSame(

screenshotLifecycleHost.notifyObservers { it.afterScreenshot(activity, currentBitmap) }

if (configuration.pauseForInspection) {
Thread.sleep(LAYOUT_INSPECTION_TIME_MS)
}
if (configuration.pauseForInspection)
pauseForInspection()

val isRecordMode = isRecordMode()

Expand All @@ -140,7 +139,7 @@ internal fun <TActivity : Activity> assertSame(
val destination = getDestination(activity, outputFileName)

val baselineBitmap = loadBaselineBitmapForComparison(testContext, description.name)
?: if (isRecordMode()) {
?: if (isRecordMode) {
TestInstrumentationRegistry.instrumentationPrintln(
"\n\t" + "Recording baseline for ${description.name}".cyan()
)
Expand Down Expand Up @@ -172,15 +171,16 @@ internal fun <TActivity : Activity> assertSame(
throw FinalizeDestinationException(destination.description)

if (TestifyFeatures.GenerateDiffs.isEnabled(activity)) {
HighContrastDiff(configuration.exclusionRects)
HighContrastDiff
.create(configuration.exclusionRects) // TODO: Test me
.name(outputFileName)
.baseline(baselineBitmap)
.current(currentBitmap)
.exactness(configuration.exactness)
.generate(context = activity)
}
if (TestInstrumentationRegistry.isRecordMode) {
TestInstrumentationRegistry.instrumentationPrintln(
if (isRecordMode) {
TestInstrumentationRegistry.instrumentationPrintln( // TODO: Test me
"\n\t" + "Recording baseline for ${description.name}".cyan()
)
} else {
Expand All @@ -202,3 +202,7 @@ internal fun <TActivity : Activity> assertSame(
}

private const val LAYOUT_INSPECTION_TIME_MS = 60000L

@VisibleForTesting
internal fun pauseForInspection() =
Thread.sleep(LAYOUT_INSPECTION_TIME_MS)
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package dev.testify.core.processor

import android.graphics.Bitmap
import androidx.annotation.VisibleForTesting
import androidx.annotation.VisibleForTesting.PACKAGE_PRIVATE
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.asCoroutineDispatcher
import java.util.concurrent.Executors
Expand All @@ -17,11 +18,12 @@ fun ParallelPixelProcessor.TransformResult.createBitmap(): Bitmap {

private val numberOfAvailableCores = Runtime.getRuntime().availableProcessors()

@VisibleForTesting
var maxNumberOfChunkThreads = numberOfAvailableCores
@VisibleForTesting(otherwise = PACKAGE_PRIVATE)
internal var maxNumberOfChunkThreads = numberOfAvailableCores

@Suppress("ObjectPropertyName")
@VisibleForTesting
var _executorDispatcher: CoroutineDispatcher? = null
internal var _executorDispatcher: CoroutineDispatcher? = null

val executorDispatcher by lazy {
if (_executorDispatcher == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class ParallelPixelProcessor private constructor() {

private fun getChunkData(width: Int, height: Int): ChunkData {
val size = width * height
val chunkSize = size / maxNumberOfChunkThreads
val chunkSize = (size / maxNumberOfChunkThreads).coerceAtLeast(1)
val chunks = ceil(size.toFloat() / chunkSize.toFloat()).toInt()
return ChunkData(size, chunks, chunkSize)
}
Expand Down
Loading

0 comments on commit 51953ca

Please sign in to comment.