diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 1372b34..bf2191a 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -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
\ No newline at end of file
+ ./gradlew publishReleasePublicationToMavenRepository -DLIBRARY_VERSION=${{ github.event.release.tag_name }}
+ cd include-build
+ ../gradlew publishGradlePluginPublicationToMavenRepository -DLIBRARY_VERSION=${{ github.event.release.tag_name }}
diff --git a/README.md b/README.md
index ff44bcd..6d8bba9 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,142 @@
-# android-loggerazzi
\ No newline at end of file
+
+
+
+
+
+# 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 {
+
+ private val logs = mutableListOf()
+
+ override fun clear() {
+ logs.clear()
+ }
+
+ override fun getRecordedLogs(): List =
+ 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.
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 5535540..b25c2b9 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,5 +1,5 @@
[versions]
-agp = "8.3.2"
+agp = "8.4.1"
constraintlayout = "2.1.4"
min-sdk = "23"
target-sdk = "34"
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 380614d..8342a64 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -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
diff --git a/include-build/gradle-plugin/src/main/java/com/telefonica/loggerazzi/LoggerazziPlugin.kt b/include-build/gradle-plugin/src/main/java/com/telefonica/loggerazzi/LoggerazziPlugin.kt
index bb4b972..cd5665d 100644
--- a/include-build/gradle-plugin/src/main/java/com/telefonica/loggerazzi/LoggerazziPlugin.kt
+++ b/include-build/gradle-plugin/src/main/java/com/telefonica/loggerazzi/LoggerazziPlugin.kt
@@ -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)
}
}
}
@@ -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() }
+ }
}
diff --git a/include-build/gradle-plugin/src/main/java/com/telefonica/loggerazzi/LoggerazziReportUtils.kt b/include-build/gradle-plugin/src/main/java/com/telefonica/loggerazzi/LoggerazziReportUtils.kt
index 1811479..cd8547b 100644
--- a/include-build/gradle-plugin/src/main/java/com/telefonica/loggerazzi/LoggerazziReportUtils.kt
+++ b/include-build/gradle-plugin/src/main/java/com/telefonica/loggerazzi/LoggerazziReportUtils.kt
@@ -4,6 +4,7 @@ import java.io.File
fun getFailuresReport(
failureEntries: List,
+ reportsDir: File,
): String {
return buildString {
append("Failures Report
")
@@ -24,9 +25,9 @@ fun getFailuresReport(
failureEntries.forEach { entry ->
append("")
append("${entry.failure.name} | ")
- append(" | ")
- append(" | ")
- append(" | ")
+ append(" | ")
+ append(" | ")
+ append(" | ")
append("
")
}
append("")
@@ -36,6 +37,7 @@ fun getFailuresReport(
fun getRecordedReport(
recordedFiles: List,
+ reportsDir: File,
): String {
return buildString {
append("Recorded Logs Report
")
@@ -54,7 +56,7 @@ fun getRecordedReport(
recordedFiles.forEach { recorded ->
append("")
append("${recorded.name} | ")
- append(" | ")
+ append(" | ")
append("
")
}
append("")
diff --git a/include-build/gradle/libs.versions.toml b/include-build/gradle/libs.versions.toml
index c82fd34..3d96fb1 100644
--- a/include-build/gradle/libs.versions.toml
+++ b/include-build/gradle/libs.versions.toml
@@ -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" }
diff --git a/include-build/mavencentral.gradle b/include-build/mavencentral.gradle
index 40bb96a..562fca3 100644
--- a/include-build/mavencentral.gradle
+++ b/include-build/mavencentral.gradle
@@ -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'
@@ -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")
}
diff --git a/mavencentral.gradle b/mavencentral.gradle
index 6de424a..ab4cd7b 100644
--- a/mavencentral.gradle
+++ b/mavencentral.gradle
@@ -11,6 +11,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 {
release(MavenPublication) {
groupId 'com.telefonica'
@@ -59,6 +68,8 @@ publishing {
afterEvaluate {
tasks.getByName("publishReleasePublicationToMavenLocal").dependsOn("assembleRelease")
+ tasks.getByName("publishReleasePublicationToSonatypeRepository").dependsOn("assembleRelease")
+ tasks.getByName("signReleasePublication").dependsOn("assembleRelease")
}
signing {