Skip to content

Commit 9a0abac

Browse files
authored
Project adaptation to work with external instrumentation runners (#2)
* Fix publication * Provide before and after calls for non standard test runners * Update README.md
1 parent 1de8749 commit 9a0abac

File tree

9 files changed

+249
-54
lines changed

9 files changed

+249
-54
lines changed

.github/workflows/release.yml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ jobs:
1818
ORG_GRADLE_PROJECT_signingKey: ${{ secrets.ORG_GRADLE_PROJECT_SIGNINGKEY }}
1919
ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.ORG_GRADLE_PROJECT_SIGNINGPASSWORD }}
2020
ORG_GRADLE_PROJECT_signingKeyId: ${{ secrets.ORG_GRADLE_PROJECT_SIGNINGKEYID }}
21+
NEXUS_USER: ${{ secrets.NEXUS_RELEASE_USER }}
22+
NEXUS_PASS: ${{ secrets.NEXUS_RELEASE_PASSWORD }}
2123
run: |
22-
./gradlew clean publishReleasePublicationToSonatypeRepository -DLIBRARY_VERSION=${{ github.event.release.tag_name }} --max-workers 1 closeAndReleaseStagingRepository
23-
./gradlew -p include-build clean publishGradlePluginPublicationToSonatypeRepository -DLIBRARY_VERSION=${{ github.event.release.tag_name }} --max-workers 1 closeAndReleaseStagingRepository
24+
./gradlew publishReleasePublicationToMavenRepository -DLIBRARY_VERSION=${{ github.event.release.tag_name }}
25+
cd include-build
26+
../gradlew publishGradlePluginPublicationToMavenRepository -DLIBRARY_VERSION=${{ github.event.release.tag_name }}

README.md

Lines changed: 142 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,142 @@
1-
# android-loggerazzi
1+
<p>
2+
<img src="https://img.shields.io/badge/Platform-Android-brightgreen" />
3+
<img src="https://img.shields.io/badge/Support-%3E%3D%20Android%206.0-brightgreen" />
4+
</p>
5+
6+
# Android Loggerazzi
7+
8+
Logs snapshot testing for Android Instrumentation tests.
9+
10+
## Introduction
11+
12+
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.
13+
14+
## Usage
15+
16+
You just need to include the loggerazzi plugin in yout project, and the rule in your test class (configuring it properly).
17+
18+
In order to universally include all your existing application tests, rule can be added to your tests base class.
19+
20+
To include the plugin, add plugin classpath to your project build.gradle buildscript dependencies block:
21+
22+
```gradle
23+
buildscript {
24+
repositories {
25+
...
26+
maven { url 'https://nexusng.tuenti.io/repository/maven-group/' }
27+
}
28+
dependencies {
29+
classpath "com.telefonica:loggerazzi-gradle-plugin:$loggerazzi_version"
30+
}
31+
}
32+
```
33+
34+
Then, include plugin at the beggining of your application or library module build.gradle:
35+
36+
```gradle
37+
apply plugin: "com.telefonica.loggerazzi"
38+
```
39+
40+
Also, include the rule dependency in your application or library dependencies block:
41+
42+
```gradle
43+
dependencies {
44+
...
45+
androidTestImplementation "com.telefonica:loggerazzi:$loggerazzi_version"
46+
}
47+
```
48+
49+
Finally, add Loggerazzi rule to your test class (or base instrumentation tests class), where a logs recorded must be provided (Check configuration section):
50+
51+
```kotlin
52+
open class BaseInstrumentationTest {
53+
@get:Rule
54+
val loggerazziRule: LoggerazziRule = LoggerazziRule(
55+
recorder = fakeAnalyticsTracker
56+
)
57+
}
58+
```
59+
60+
For more details, check included [application example](app).
61+
62+
## Execution
63+
64+
### Verification mode
65+
66+
Regular `connectedXXXXAndroidTest` target invocation is enough for verifications against previosuly generated logs baselines. Android Studio executions should also work seamlessly.
67+
68+
```bash
69+
./gradlew :app:connectedDebugAndroidTest
70+
```
71+
72+
In case of any failures due logs verifications, regular junit reports include failed tests and comparation failure reason.
73+
74+
Additionally, an specific loggerazzi report is generated at --> `build/reports/androidTests/connected/debug/loggerazzi/failures.html`
75+
76+
### Recording mode
77+
78+
When the logs baseline needs to be updated, it's enough to include `-Pandroid.testInstrumentationRunnerArguments.record=true`.
79+
80+
```bash
81+
./gradlew :app:connectedDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.record=true
82+
```
83+
84+
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.
85+
86+
A loggerazzi report with all recorded logs is generated at --> `build/reports/androidTests/connected/debug/loggerazzi/recorded.html`
87+
88+
## Execution from external runners
89+
90+
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:
91+
- `loggerazziBefore[VariantName]AndroidTest`
92+
- `loggerazziAfter[VariantName]AndroidTest`
93+
94+
In case test execution is triggered from any gradle task, here's an example on how to configure dependencies with loggerazzi tasks:
95+
96+
```gradle
97+
project.afterEvaluate {
98+
project.tasks.findByName("externalTestRunner[VariantName]Execution")
99+
.dependsOn("loggerazziBefore[VariantName]AndroidTest")
100+
.finalizedBy("loggerazziAfter[VariantName]AndroidTest")
101+
}
102+
```
103+
104+
## Configuration
105+
106+
### Logs recorder
107+
108+
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.
109+
110+
Example:
111+
112+
```kotlin
113+
class FakeAnalyticsTracker : AnalyticsTracker, LogsRecorder<String> {
114+
115+
private val logs = mutableListOf<String>()
116+
117+
override fun clear() {
118+
logs.clear()
119+
}
120+
121+
override fun getRecordedLogs(): List<String> =
122+
logs.mapIndexed { index, s ->
123+
"$index: $s"
124+
}
125+
126+
override fun init() {}
127+
128+
override fun trackScreenView(screen: AnalyticsScreen) {
129+
logs.add("trackScreenView: $screen")
130+
}
131+
132+
override fun trackEvent(event: Event.GenericEvent) {
133+
logs.add("trackEvent: $event")
134+
}
135+
}
136+
```
137+
138+
### Logs comparator
139+
140+
By default, loggerazzi rule compares recorded logs by ensuring these are equal and in same order than the baseline logs.
141+
142+
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.

gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[versions]
2-
agp = "8.3.2"
2+
agp = "8.4.1"
33
constraintlayout = "2.1.4"
44
min-sdk = "23"
55
target-sdk = "34"
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#Fri Mar 22 10:54:28 CET 2024
22
distributionBase=GRADLE_USER_HOME
33
distributionPath=wrapper/dists
4-
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
4+
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
55
zipStoreBase=GRADLE_USER_HOME
66
zipStorePath=wrapper/dists

include-build/gradle-plugin/src/main/java/com/telefonica/loggerazzi/LoggerazziPlugin.kt

Lines changed: 67 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -18,61 +18,82 @@ class LoggerazziPlugin @Inject constructor(
1818
project.afterEvaluate {
1919
project.tasks
2020
.withType(DeviceProviderInstrumentTestTask::class.java)
21-
.configureEach { it.configure() }
21+
.forEach { deviceProviderTask ->
22+
val capitalizedVariant = deviceProviderTask.variantName.capitalizeFirstLetter()
23+
val beforeTaskName = "loggerazziBefore$capitalizedVariant"
24+
project.tasks.register(beforeTaskName, Task::class.java) { task ->
25+
task.doFirst {
26+
deviceProviderTask.deviceFileManager().clearAllLogs()
27+
}
28+
}
29+
deviceProviderTask.dependsOn(beforeTaskName)
30+
31+
val afterTaskName = "loggerazziAfter$capitalizedVariant"
32+
project.tasks.register(afterTaskName, Task::class.java) { task ->
33+
task.doLast {
34+
deviceProviderTask.afterExecution()
35+
}
36+
}
37+
deviceProviderTask.onTaskCompleted {
38+
deviceProviderTask.afterExecution()
39+
}
40+
}
2241
}
2342
}
2443

25-
private fun DeviceProviderInstrumentTestTask.configure() {
44+
private fun DeviceProviderInstrumentTestTask.afterExecution() {
2645
val deviceFileManager = deviceFileManager()
2746

28-
doFirst {
29-
deviceFileManager.clearAllLogs()
47+
val reportsFolder = reportsDir.get().dir("loggerazzi")
48+
val recordedFolderFile = reportsFolder.dir("recorded").asFile.apply {
49+
mkdirs()
50+
deviceFileManager.pullRecordedLogs(absolutePath)
51+
}
52+
val failuresFolderFile = reportsFolder.dir("failures").asFile.apply {
53+
mkdirs()
54+
deviceFileManager.pullFailuresLogs(absolutePath)
3055
}
56+
val goldenForFailuresReportFolderFile = reportsFolder.dir("golden").asFile.apply {
57+
mkdirs()
58+
}
59+
val goldenFolderFile = File(getAbsoluteGoldenLogsSourcePath())
3160

32-
onTaskCompleted {
33-
val reportsFolder = reportsDir.get().dir("loggerazzi")
34-
val recordedFolderFile = reportsFolder.dir("recorded").asFile.apply {
35-
mkdirs()
36-
deviceFileManager.pullRecordedLogs(absolutePath)
37-
}
38-
val failuresFolderFile = reportsFolder.dir("failures").asFile.apply {
39-
mkdirs()
40-
deviceFileManager.pullFailuresLogs(absolutePath)
41-
}
42-
val goldenFolderFile = File(getAbsoluteGoldenLogsSourcePath())
61+
File("${reportsFolder.asFile.absolutePath}/recorded.html").apply {
62+
createNewFile()
63+
val recordedFiles = recordedFolderFile.listFiles()?.asList() ?: emptyList()
64+
val report = LoggerazziReportConst.reportHtml.replace(
65+
oldValue = "REPORT_TEMPLATE_BODY",
66+
newValue = getRecordedReport(recordedFiles, reportsFolder.asFile)
67+
)
68+
writeText(report)
69+
}
4370

44-
File("${reportsFolder.asFile.absolutePath}/recorded.html").apply {
71+
if (project.properties["android.testInstrumentationRunnerArguments.record"] != "true") {
72+
File("${reportsFolder.asFile.absolutePath}/failures.html").apply {
4573
createNewFile()
46-
val recordedFiles = recordedFolderFile.listFiles()?.asList() ?: emptyList()
74+
val failuresFiles = failuresFolderFile.listFiles()?.asList() ?: emptyList()
75+
val failuresEntries = failuresFiles.map { failureFile ->
76+
FailureEntry(
77+
failure = failureFile,
78+
recorded = File(recordedFolderFile, failureFile.name),
79+
golden = File(goldenFolderFile, failureFile.name).let {
80+
it.copyTo(
81+
File(goldenForFailuresReportFolderFile, it.name),
82+
true
83+
)
84+
}
85+
)
86+
}
4787
val report = LoggerazziReportConst.reportHtml.replace(
4888
oldValue = "REPORT_TEMPLATE_BODY",
49-
newValue = getRecordedReport(recordedFiles)
89+
newValue = getFailuresReport(failuresEntries, reportsFolder.asFile)
5090
)
5191
writeText(report)
5292
}
53-
54-
if (project.properties["android.testInstrumentationRunnerArguments.record"] != "true") {
55-
File("${reportsFolder.asFile.absolutePath}/failures.html").apply {
56-
createNewFile()
57-
val failuresFiles = failuresFolderFile.listFiles()?.asList() ?: emptyList()
58-
val failuresEntries = failuresFiles.map { failureFile ->
59-
FailureEntry(
60-
failure = failureFile,
61-
recorded = File(recordedFolderFile, failureFile.name),
62-
golden = File(goldenFolderFile, failureFile.name)
63-
)
64-
}
65-
val report = LoggerazziReportConst.reportHtml.replace(
66-
oldValue = "REPORT_TEMPLATE_BODY",
67-
newValue = getFailuresReport(failuresEntries)
68-
)
69-
writeText(report)
70-
}
71-
} else {
72-
File(getAbsoluteGoldenLogsSourcePath()).apply {
73-
mkdirs()
74-
deviceFileManager.pullRecordedLogs(absolutePath)
75-
}
93+
} else {
94+
File(getAbsoluteGoldenLogsSourcePath()).apply {
95+
mkdirs()
96+
deviceFileManager.pullRecordedLogs(absolutePath)
7697
}
7798
}
7899
}
@@ -94,8 +115,12 @@ class LoggerazziPlugin @Inject constructor(
94115
val variantSourceFolder = this
95116
.variantName
96117
.replace("AndroidTest", "")
97-
.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }
118+
.capitalizeFirstLetter()
98119
.let { "androidTest$it" }
99120
return "${project.projectDir}/src/$variantSourceFolder/assets/loggerazzi-golden-files"
100121
}
122+
123+
private fun String.capitalizeFirstLetter(): String {
124+
return replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }
125+
}
101126
}

include-build/gradle-plugin/src/main/java/com/telefonica/loggerazzi/LoggerazziReportUtils.kt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import java.io.File
44

55
fun getFailuresReport(
66
failureEntries: List<FailureEntry>,
7+
reportsDir: File,
78
): String {
89
return buildString {
910
append("<h3>Failures Report</h3>")
@@ -24,9 +25,9 @@ fun getFailuresReport(
2425
failureEntries.forEach { entry ->
2526
append("<tr class=\"row\">")
2627
append("<td class=\"$fileNameClass\" style=\"$fileNameStyle\">${entry.failure.name}</td>")
27-
append("<td class=\"$imgClass\"><iframe $imgAttributes src=\"${entry.golden.absolutePath}\"></iframe></td>")
28-
append("<td class=\"$imgClass\"><iframe $imgAttributes src=\"${entry.failure.absolutePath}\"></iframe></td>")
29-
append("<td class=\"$imgClass\"><iframe $imgAttributes src=\"${entry.recorded.absolutePath}\"></iframe></td>")
28+
append("<td class=\"$imgClass\"><iframe $imgAttributes src=\"${entry.golden.relativeTo(reportsDir)}\"></iframe></td>")
29+
append("<td class=\"$imgClass\"><iframe $imgAttributes src=\"${entry.failure.relativeTo(reportsDir)}\"></iframe></td>")
30+
append("<td class=\"$imgClass\"><iframe $imgAttributes src=\"${entry.recorded.relativeTo(reportsDir)}\"></iframe></td>")
3031
append("</tr>")
3132
}
3233
append("</tbody>")
@@ -36,6 +37,7 @@ fun getFailuresReport(
3637

3738
fun getRecordedReport(
3839
recordedFiles: List<File>,
40+
reportsDir: File,
3941
): String {
4042
return buildString {
4143
append("<h3>Recorded Logs Report</h3>")
@@ -54,7 +56,7 @@ fun getRecordedReport(
5456
recordedFiles.forEach { recorded ->
5557
append("<tr class=\"row\">")
5658
append("<td class=\"$fileNameClass\" style=\"$fileNameStyle\">${recorded.name}</td>")
57-
append("<td class=\"$imgClass\"><iframe $imgAttributes src=\"${recorded.absolutePath}\"></iframe></td>")
59+
append("<td class=\"$imgClass\"><iframe $imgAttributes src=\"${recorded.relativeTo(reportsDir)}\"></iframe></td>")
5860
append("</tr>")
5961
}
6062
append("</tbody>")

include-build/gradle/libs.versions.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
[versions]
2+
agp = "8.4.1"
23
common = "31.4.1"
34
ddmlib = "31.4.1"
4-
gradle = "8.3.2"
55
kotlin = "1.9.23"
66
detekt = "1.23.6"
77
publish = "1.1.0"
88

99
[libraries]
10-
android-builder-test-api = { module = "com.android.tools.build:builder-test-api", version.ref = "gradle" }
10+
android-builder-test-api = { module = "com.android.tools.build:builder-test-api", version.ref = "agp" }
1111
android-common = { module = "com.android.tools:common", version.ref = "common" }
1212
android-ddmlib = { module = "com.android.tools.ddms:ddmlib", version.ref = "ddmlib" }
13-
android-gradle = { module = "com.android.tools.build:gradle", version.ref = "gradle" }
13+
android-gradle = { module = "com.android.tools.build:gradle", version.ref = "agp" }
1414

1515
[plugins]
1616
detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }

include-build/mavencentral.gradle

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@ apply plugin: 'maven-publish'
22
apply plugin: 'signing'
33

44
publishing {
5+
repositories {
6+
maven {
7+
credentials {
8+
username System.env.NEXUS_USER
9+
password System.env.NEXUS_PASS
10+
}
11+
url "https://nexusng.tuenti.io/repository/maven-release-private/"
12+
}
13+
}
514
publications {
615
gradlePlugin(MavenPublication) {
716
groupId 'com.telefonica'
@@ -49,6 +58,10 @@ publishing {
4958

5059
afterEvaluate {
5160
tasks.getByName("publishGradlePluginPublicationToMavenLocal").dependsOn("jar")
61+
tasks.getByName("publishGradlePluginPublicationToSonatypeRepository").dependsOn("jar")
62+
tasks.getByName("publishGradlePluginPublicationToMavenRepository").dependsOn("jar")
63+
tasks.getByName("signGradlePluginPublication").dependsOn("jar")
64+
tasks.getByName("signPluginMavenPublication").dependsOn("jar")
5265
}
5366

5467

0 commit comments

Comments
 (0)