Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Project adaptation to work with external instrumentation runners #2

Merged
merged 28 commits into from
Jul 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
722ee38
Fix publication
dpastor May 30, 2024
7783788
Hardcode version for testing
dpastor May 30, 2024
e3b7cde
code to test publishing before integration
dpastor May 30, 2024
19cbb5f
Add more required task dependencies
dpastor May 30, 2024
d09be90
close staging repository at the end
dpastor May 30, 2024
c859270
Check closing staging repository on each publication
dpastor May 30, 2024
b7b1760
Raise artifact version to test
dpastor May 30, 2024
be19613
Increase publishing timeout
dpastor May 30, 2024
91ebfa5
Check unified publication
dpastor May 30, 2024
ca62167
Point to correct project
dpastor May 30, 2024
0b6e725
Test plugin publication
dpastor May 31, 2024
92c4f1b
Check changing current directory
dpastor May 31, 2024
2936499
Add info logs
dpastor May 31, 2024
7fd6ad2
Check nexus publishing
dpastor May 31, 2024
4f3f156
Using default nexus credentials
dpastor May 31, 2024
ddbce18
Declare correct dependency
dpastor May 31, 2024
9dc0173
More dependencies
dpastor May 31, 2024
3ff5e58
Just upload to nexus
dpastor May 31, 2024
dcf2abc
Make report paths relative
dpastor Jun 7, 2024
57b9de8
Copy required golden files to report
dpastor Jun 7, 2024
7d873e0
Cleanup and update dependencies
dpastor Jun 7, 2024
d802a79
Revert changes in publish config
dpastor Jun 7, 2024
a015223
Provide before and after calls for non standard test runners
dpastor Jun 21, 2024
d469978
Revert preintegration action
dpastor Jul 12, 2024
04cf478
Revert correctly action
dpastor Jul 12, 2024
c0213a6
Revert file to avoid change in file
dpastor Jul 12, 2024
dd1bab0
Update README.md
dpastor Jul 12, 2024
a7bfaf3
Fix variant name capitalizing
dpastor Jul 12, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ jobs:
ORG_GRADLE_PROJECT_signingKey: ${{ secrets.ORG_GRADLE_PROJECT_SIGNINGKEY }}
ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.ORG_GRADLE_PROJECT_SIGNINGPASSWORD }}
ORG_GRADLE_PROJECT_signingKeyId: ${{ secrets.ORG_GRADLE_PROJECT_SIGNINGKEYID }}
NEXUS_USER: ${{ secrets.NEXUS_RELEASE_USER }}
NEXUS_PASS: ${{ secrets.NEXUS_RELEASE_PASSWORD }}
run: |
./gradlew clean publishReleasePublicationToSonatypeRepository -DLIBRARY_VERSION=${{ github.event.release.tag_name }} --max-workers 1 closeAndReleaseStagingRepository
./gradlew -p include-build clean publishGradlePluginPublicationToSonatypeRepository -DLIBRARY_VERSION=${{ github.event.release.tag_name }} --max-workers 1 closeAndReleaseStagingRepository
./gradlew publishReleasePublicationToMavenRepository -DLIBRARY_VERSION=${{ github.event.release.tag_name }}
cd include-build
../gradlew publishGradlePluginPublicationToMavenRepository -DLIBRARY_VERSION=${{ github.event.release.tag_name }}
143 changes: 142 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,142 @@
# android-loggerazzi
<p>
<img src="https://img.shields.io/badge/Platform-Android-brightgreen" />
<img src="https://img.shields.io/badge/Support-%3E%3D%20Android%206.0-brightgreen" />
</p>

# Android Loggerazzi

Logs snapshot testing for Android Instrumentation tests.

## Introduction

Similarly to screenshot testing, which is an easy and mantenible approach to ensure your application UI does not get broken, loggerazzi brings the same "snapshoting" idea, but for your analytics or any other application logs.

## Usage

You just need to include the loggerazzi plugin in yout project, and the rule in your test class (configuring it properly).

In order to universally include all your existing application tests, rule can be added to your tests base class.

To include the plugin, add plugin classpath to your project build.gradle buildscript dependencies block:

```gradle
buildscript {
repositories {
...
maven { url 'https://nexusng.tuenti.io/repository/maven-group/' }
}
dependencies {
classpath "com.telefonica:loggerazzi-gradle-plugin:$loggerazzi_version"
}
}
```

Then, include plugin at the beggining of your application or library module build.gradle:

```gradle
apply plugin: "com.telefonica.loggerazzi"
```

Also, include the rule dependency in your application or library dependencies block:

```gradle
dependencies {
...
androidTestImplementation "com.telefonica:loggerazzi:$loggerazzi_version"
}
```

Finally, add Loggerazzi rule to your test class (or base instrumentation tests class), where a logs recorded must be provided (Check configuration section):

```kotlin
open class BaseInstrumentationTest {
@get:Rule
val loggerazziRule: LoggerazziRule = LoggerazziRule(
recorder = fakeAnalyticsTracker
)
}
```

For more details, check included [application example](app).

## Execution

### Verification mode

Regular `connectedXXXXAndroidTest` target invocation is enough for verifications against previosuly generated logs baselines. Android Studio executions should also work seamlessly.

```bash
./gradlew :app:connectedDebugAndroidTest
```

In case of any failures due logs verifications, regular junit reports include failed tests and comparation failure reason.

Additionally, an specific loggerazzi report is generated at --> `build/reports/androidTests/connected/debug/loggerazzi/failures.html`

### Recording mode

When the logs baseline needs to be updated, it's enough to include `-Pandroid.testInstrumentationRunnerArguments.record=true`.

```bash
./gradlew :app:connectedDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.record=true
```

This execution won't perform any logs verification, instead, it will execute tests to generate new logs, placing them in the corresponding tests baseline directory.

A loggerazzi report with all recorded logs is generated at --> `build/reports/androidTests/connected/debug/loggerazzi/recorded.html`

## Execution from external runners

In situations where the regular `connectedXXXXAndroidTest` target is not used because execution is performed by a different external test runner (such as composer or marathon), two loggerazzi gradle tasks are provided which should be executed manually before and after external test runner execution:
- `loggerazziBefore[VariantName]AndroidTest`
- `loggerazziAfter[VariantName]AndroidTest`

In case test execution is triggered from any gradle task, here's an example on how to configure dependencies with loggerazzi tasks:

```gradle
project.afterEvaluate {
project.tasks.findByName("externalTestRunner[VariantName]Execution")
.dependsOn("loggerazziBefore[VariantName]AndroidTest")
.finalizedBy("loggerazziAfter[VariantName]AndroidTest")
}
```

## Configuration

### Logs recorder

Loggerazzi rule must be configured with a [LogsRecorder](loggerazzi/src/main/java/com/telefonica/loggerazzi/LogsRecorder.kt) implementation which will be used by loggerazzi to obtain logs recorded at the end of the test. This should be usually implemented as the replacement of the original application tracker in tests.

Example:

```kotlin
class FakeAnalyticsTracker : AnalyticsTracker, LogsRecorder<String> {

private val logs = mutableListOf<String>()

override fun clear() {
logs.clear()
}

override fun getRecordedLogs(): List<String> =
logs.mapIndexed { index, s ->
"$index: $s"
}

override fun init() {}

override fun trackScreenView(screen: AnalyticsScreen) {
logs.add("trackScreenView: $screen")
}

override fun trackEvent(event: Event.GenericEvent) {
logs.add("trackEvent: $event")
}
}
```

### Logs comparator

By default, loggerazzi rule compares recorded logs by ensuring these are equal and in same order than the baseline logs.

In case a different comparation mechanism is needed (such as ignoring the order of the events, or ignoring certain logs), you can implement an specific [LogComparator](loggerazzi/src/main/java/com/telefonica/loggerazzi/LogComparator.kt), which can be provided to the LoggerazziRule on its creation.
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[versions]
agp = "8.3.2"
agp = "8.4.1"
constraintlayout = "2.1.4"
min-sdk = "23"
target-sdk = "34"
Expand Down
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#Fri Mar 22 10:54:28 CET 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Original file line number Diff line number Diff line change
Expand Up @@ -18,61 +18,82 @@ class LoggerazziPlugin @Inject constructor(
project.afterEvaluate {
project.tasks
.withType(DeviceProviderInstrumentTestTask::class.java)
.configureEach { it.configure() }
.forEach { deviceProviderTask ->
val capitalizedVariant = deviceProviderTask.variantName.capitalizeFirstLetter()
val beforeTaskName = "loggerazziBefore$capitalizedVariant"
project.tasks.register(beforeTaskName, Task::class.java) { task ->
task.doFirst {
deviceProviderTask.deviceFileManager().clearAllLogs()
}
}
deviceProviderTask.dependsOn(beforeTaskName)

val afterTaskName = "loggerazziAfter$capitalizedVariant"
project.tasks.register(afterTaskName, Task::class.java) { task ->
task.doLast {
deviceProviderTask.afterExecution()
}
}
deviceProviderTask.onTaskCompleted {
deviceProviderTask.afterExecution()
}
}
}
}

private fun DeviceProviderInstrumentTestTask.configure() {
private fun DeviceProviderInstrumentTestTask.afterExecution() {
val deviceFileManager = deviceFileManager()

doFirst {
deviceFileManager.clearAllLogs()
val reportsFolder = reportsDir.get().dir("loggerazzi")
val recordedFolderFile = reportsFolder.dir("recorded").asFile.apply {
mkdirs()
deviceFileManager.pullRecordedLogs(absolutePath)
}
val failuresFolderFile = reportsFolder.dir("failures").asFile.apply {
mkdirs()
deviceFileManager.pullFailuresLogs(absolutePath)
}
val goldenForFailuresReportFolderFile = reportsFolder.dir("golden").asFile.apply {
mkdirs()
}
val goldenFolderFile = File(getAbsoluteGoldenLogsSourcePath())

onTaskCompleted {
val reportsFolder = reportsDir.get().dir("loggerazzi")
val recordedFolderFile = reportsFolder.dir("recorded").asFile.apply {
mkdirs()
deviceFileManager.pullRecordedLogs(absolutePath)
}
val failuresFolderFile = reportsFolder.dir("failures").asFile.apply {
mkdirs()
deviceFileManager.pullFailuresLogs(absolutePath)
}
val goldenFolderFile = File(getAbsoluteGoldenLogsSourcePath())
File("${reportsFolder.asFile.absolutePath}/recorded.html").apply {
createNewFile()
val recordedFiles = recordedFolderFile.listFiles()?.asList() ?: emptyList()
val report = LoggerazziReportConst.reportHtml.replace(
oldValue = "REPORT_TEMPLATE_BODY",
newValue = getRecordedReport(recordedFiles, reportsFolder.asFile)
)
writeText(report)
}

File("${reportsFolder.asFile.absolutePath}/recorded.html").apply {
if (project.properties["android.testInstrumentationRunnerArguments.record"] != "true") {
File("${reportsFolder.asFile.absolutePath}/failures.html").apply {
createNewFile()
val recordedFiles = recordedFolderFile.listFiles()?.asList() ?: emptyList()
val failuresFiles = failuresFolderFile.listFiles()?.asList() ?: emptyList()
val failuresEntries = failuresFiles.map { failureFile ->
FailureEntry(
failure = failureFile,
recorded = File(recordedFolderFile, failureFile.name),
golden = File(goldenFolderFile, failureFile.name).let {
it.copyTo(
File(goldenForFailuresReportFolderFile, it.name),
true
)
}
)
}
val report = LoggerazziReportConst.reportHtml.replace(
oldValue = "REPORT_TEMPLATE_BODY",
newValue = getRecordedReport(recordedFiles)
newValue = getFailuresReport(failuresEntries, reportsFolder.asFile)
)
writeText(report)
}

if (project.properties["android.testInstrumentationRunnerArguments.record"] != "true") {
File("${reportsFolder.asFile.absolutePath}/failures.html").apply {
createNewFile()
val failuresFiles = failuresFolderFile.listFiles()?.asList() ?: emptyList()
val failuresEntries = failuresFiles.map { failureFile ->
FailureEntry(
failure = failureFile,
recorded = File(recordedFolderFile, failureFile.name),
golden = File(goldenFolderFile, failureFile.name)
)
}
val report = LoggerazziReportConst.reportHtml.replace(
oldValue = "REPORT_TEMPLATE_BODY",
newValue = getFailuresReport(failuresEntries)
)
writeText(report)
}
} else {
File(getAbsoluteGoldenLogsSourcePath()).apply {
mkdirs()
deviceFileManager.pullRecordedLogs(absolutePath)
}
} else {
File(getAbsoluteGoldenLogsSourcePath()).apply {
mkdirs()
deviceFileManager.pullRecordedLogs(absolutePath)
}
}
}
Expand All @@ -94,8 +115,12 @@ class LoggerazziPlugin @Inject constructor(
val variantSourceFolder = this
.variantName
.replace("AndroidTest", "")
.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }
.capitalizeFirstLetter()
.let { "androidTest$it" }
return "${project.projectDir}/src/$variantSourceFolder/assets/loggerazzi-golden-files"
}

private fun String.capitalizeFirstLetter(): String {
return replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import java.io.File

fun getFailuresReport(
failureEntries: List<FailureEntry>,
reportsDir: File,
): String {
return buildString {
append("<h3>Failures Report</h3>")
Expand All @@ -24,9 +25,9 @@ fun getFailuresReport(
failureEntries.forEach { entry ->
append("<tr class=\"row\">")
append("<td class=\"$fileNameClass\" style=\"$fileNameStyle\">${entry.failure.name}</td>")
append("<td class=\"$imgClass\"><iframe $imgAttributes src=\"${entry.golden.absolutePath}\"></iframe></td>")
append("<td class=\"$imgClass\"><iframe $imgAttributes src=\"${entry.failure.absolutePath}\"></iframe></td>")
append("<td class=\"$imgClass\"><iframe $imgAttributes src=\"${entry.recorded.absolutePath}\"></iframe></td>")
append("<td class=\"$imgClass\"><iframe $imgAttributes src=\"${entry.golden.relativeTo(reportsDir)}\"></iframe></td>")
append("<td class=\"$imgClass\"><iframe $imgAttributes src=\"${entry.failure.relativeTo(reportsDir)}\"></iframe></td>")
append("<td class=\"$imgClass\"><iframe $imgAttributes src=\"${entry.recorded.relativeTo(reportsDir)}\"></iframe></td>")
append("</tr>")
}
append("</tbody>")
Expand All @@ -36,6 +37,7 @@ fun getFailuresReport(

fun getRecordedReport(
recordedFiles: List<File>,
reportsDir: File,
): String {
return buildString {
append("<h3>Recorded Logs Report</h3>")
Expand All @@ -54,7 +56,7 @@ fun getRecordedReport(
recordedFiles.forEach { recorded ->
append("<tr class=\"row\">")
append("<td class=\"$fileNameClass\" style=\"$fileNameStyle\">${recorded.name}</td>")
append("<td class=\"$imgClass\"><iframe $imgAttributes src=\"${recorded.absolutePath}\"></iframe></td>")
append("<td class=\"$imgClass\"><iframe $imgAttributes src=\"${recorded.relativeTo(reportsDir)}\"></iframe></td>")
append("</tr>")
}
append("</tbody>")
Expand Down
6 changes: 3 additions & 3 deletions include-build/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
[versions]
agp = "8.4.1"
common = "31.4.1"
ddmlib = "31.4.1"
gradle = "8.3.2"
kotlin = "1.9.23"
detekt = "1.23.6"
publish = "1.1.0"

[libraries]
android-builder-test-api = { module = "com.android.tools.build:builder-test-api", version.ref = "gradle" }
android-builder-test-api = { module = "com.android.tools.build:builder-test-api", version.ref = "agp" }
android-common = { module = "com.android.tools:common", version.ref = "common" }
android-ddmlib = { module = "com.android.tools.ddms:ddmlib", version.ref = "ddmlib" }
android-gradle = { module = "com.android.tools.build:gradle", version.ref = "gradle" }
android-gradle = { module = "com.android.tools.build:gradle", version.ref = "agp" }

[plugins]
detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
Expand Down
13 changes: 13 additions & 0 deletions include-build/mavencentral.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@ apply plugin: 'maven-publish'
apply plugin: 'signing'

publishing {
repositories {
maven {
credentials {
username System.env.NEXUS_USER
password System.env.NEXUS_PASS
}
url "https://nexusng.tuenti.io/repository/maven-release-private/"
}
}
publications {
gradlePlugin(MavenPublication) {
groupId 'com.telefonica'
Expand Down Expand Up @@ -49,6 +58,10 @@ publishing {

afterEvaluate {
tasks.getByName("publishGradlePluginPublicationToMavenLocal").dependsOn("jar")
tasks.getByName("publishGradlePluginPublicationToSonatypeRepository").dependsOn("jar")
tasks.getByName("publishGradlePluginPublicationToMavenRepository").dependsOn("jar")
tasks.getByName("signGradlePluginPublication").dependsOn("jar")
tasks.getByName("signPluginMavenPublication").dependsOn("jar")
}


Expand Down
Loading