Skip to content

Commit 48c2340

Browse files
authored
Add daily test reporting (#8042)
Adds daily test status to the Test Reporting bug. Unlike unit and instrumentation tests, all runs are always shown, since we currently only have 1 type of daily test. Example output below. ----- ### Unit Tests | | [#8009](#8009) | [#7983](#7983) | [#8002](#8002) | [#8027](#8027) | [#8028](#8028) | [#8025](#8025) | Success Rate | | :--- | :---: | :---: | :---: | :---: | :---: | :---: | :--- | | firebase-dataconnect | | | | [✅](https://github.com/firebase/firebase-android-sdk/actions/runs/24202345424/job/70649528040) | | [⛔](https://github.com/firebase/firebase-android-sdk/actions/runs/24162429246/job/70516856982) | ⛔ 50% | *+8 passing SDKs ### Instrumentation Tests *All tests passing* ### Daily Tests | | Apr 15 | Apr 16 | Apr 17 | Apr 18 | Apr 19 | Apr 20 | Apr 21 | Success Rate | | :--- | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :--- | | AI Daily Tests | [✅](https://github.com/firebase/firebase-android-sdk/actions/runs/24442505713) | [✅](https://github.com/firebase/firebase-android-sdk/actions/runs/24498252859) | [✅](https://github.com/firebase/firebase-android-sdk/actions/runs/24553833385) | [✅](https://github.com/firebase/firebase-android-sdk/actions/runs/24599892175) | [✅](https://github.com/firebase/firebase-android-sdk/actions/runs/24623963356) | [✅](https://github.com/firebase/firebase-android-sdk/actions/runs/24654541730) | [✅](https://github.com/firebase/firebase-android-sdk/actions/runs/24710324995) | ✅ 100% |
1 parent 0761a2e commit 48c2340

3 files changed

Lines changed: 100 additions & 15 deletions

File tree

plugins/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ dependencies {
6363
implementation("org.ow2.asm:asm-tree:9.8")
6464
implementation("org.eclipse.jgit:org.eclipse.jgit:7.1.0.202411261347-r")
6565
implementation(libs.kotlinx.serialization.json)
66+
implementation(libs.kotlinx.datetime)
6667
implementation("com.google.code.gson:gson:2.13.2")
6768
implementation(libs.android.gradlePlugin.gradle)
6869
implementation(libs.android.gradlePlugin.builder.test.api)

plugins/src/main/java/com/google/firebase/gradle/plugins/report/TestReport.kt

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,32 +15,52 @@
1515
*/
1616
package com.google.firebase.gradle.plugins.report
1717

18+
import kotlinx.serialization.json.JsonObject
19+
import kotlinx.serialization.json.jsonPrimitive
20+
1821
/**
1922
* Represents a single run of a test in CI. One unit/instrumentation test workflow run creates many
2023
* `TestReport`s, one for each tested SDK.
2124
*
2225
* @param name SDK name of the associated test run.
23-
* @param type What type of test result this is, either unit or instrumentation test.
26+
* @param type What type of test result this is, either unit, instrumentation or daily test.
2427
* @param status Conclusion status of the test run, `SUCCESS`/`FAILURE` for typical results, `OTHER`
2528
* for ongoing runs and unexpected data.
2629
* @param commit Commit SHA this test was run on.
2730
* @param url Link to the GHA test run info, including logs.
31+
* @param date The ISO date of the run, only if the test is a daily test
2832
*/
2933
data class TestReport(
3034
val name: String,
3135
val type: Type,
3236
val status: Status,
3337
val commit: String,
3438
val url: String,
39+
val date: String? = null,
3540
) {
3641
enum class Type {
3742
UNIT_TEST,
3843
INSTRUMENTATION_TEST,
44+
DAILY_TEST,
3945
}
4046

4147
enum class Status {
4248
SUCCESS,
4349
FAILURE,
44-
OTHER,
50+
OTHER;
51+
52+
companion object {
53+
fun of(json: JsonObject): Status {
54+
if (json["status"]?.jsonPrimitive?.content == "completed") {
55+
if (json["conclusion"]?.jsonPrimitive?.content == "success") {
56+
return SUCCESS
57+
} else {
58+
return FAILURE
59+
}
60+
} else {
61+
return OTHER
62+
}
63+
}
64+
}
4565
}
4666
}

plugins/src/main/java/com/google/firebase/gradle/plugins/report/TestReportGenerator.kt

Lines changed: 77 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ import java.net.http.HttpClient
2222
import java.net.http.HttpRequest
2323
import java.net.http.HttpResponse
2424
import java.time.Duration
25+
import kotlinx.datetime.LocalDate
26+
import kotlinx.datetime.format.MonthNames
27+
import kotlinx.datetime.format.char
2528
import kotlinx.serialization.json.Json
2629
import kotlinx.serialization.json.JsonArray
2730
import kotlinx.serialization.json.JsonElement
@@ -75,10 +78,34 @@ class TestReportGenerator(private val apiToken: String) {
7578
?.int ?: throw RuntimeException("Couldn't find PR number for commit $obj"),
7679
)
7780
}
78-
outputReport(outputFile, commits)
81+
val dailyTests = arrayListOf<TestReport>()
82+
// GraphQL does not expose actions in an easy to index way, and the REST API does
83+
val dailyTestResponse = request("actions/workflows/ai-daily-tests.yml/runs?per_page=7")
84+
val arr =
85+
dailyTestResponse["workflow_runs"]?.jsonArray
86+
?: throw RuntimeException("Workflow request failed")
87+
for (test in arr) {
88+
val obj = test.jsonObject
89+
dailyTests.add(
90+
TestReport(
91+
"ai-daily-test",
92+
TestReport.Type.DAILY_TEST,
93+
TestReport.Status.of(obj),
94+
obj["head_commit"]?.jsonObject?.get("id")?.jsonPrimitive?.content
95+
?: throw RuntimeException("Missing head_commit"),
96+
obj["html_url"]?.jsonPrimitive?.content ?: throw RuntimeException("Missing url"),
97+
obj["created_at"]?.jsonPrimitive?.content?.substringBefore("T"),
98+
)
99+
)
100+
}
101+
outputReport(outputFile, commits, dailyTests)
79102
}
80103

81-
private fun outputReport(outputFile: File, commits: List<ReportCommit>) {
104+
private fun outputReport(
105+
outputFile: File,
106+
commits: List<ReportCommit>,
107+
dailyTests: List<TestReport>,
108+
) {
82109
val reports = commits.flatMap { commit -> parseTestReports(commit.sha) }
83110
val output = StringBuilder()
84111
output.append("### Unit Tests\n\n")
@@ -97,6 +124,11 @@ class TestReportGenerator(private val apiToken: String) {
97124
)
98125
)
99126
output.append("\n")
127+
if (dailyTests.isNotEmpty()) {
128+
output.append("### Daily Tests\n\n")
129+
output.append(generateDailyTable(dailyTests.reversed()))
130+
output.append("\n")
131+
}
100132

101133
try {
102134
outputFile.writeText(output.toString())
@@ -169,7 +201,7 @@ class TestReportGenerator(private val apiToken: String) {
169201
TestReport.Status.FAILURE -> ""
170202
TestReport.Status.OTHER -> ""
171203
}
172-
val link: String = " [%s](%s)".format(icon, report.url)
204+
val link: String = " [$icon](${report.url})"
173205
output.append(link)
174206
}
175207
output.append(" |")
@@ -192,6 +224,41 @@ class TestReportGenerator(private val apiToken: String) {
192224
return output.toString()
193225
}
194226

227+
private fun generateDailyTable(reports: List<TestReport>): String {
228+
val output = StringBuilder("| |")
229+
for (report in reports) {
230+
val time = LocalDate.parse(report.date!!)
231+
output.append(" ${DATE_FORMAT.format(time)} |")
232+
}
233+
output.append(" Success Rate |\n|")
234+
output.append(" :--- |")
235+
output.append(" :---: |".repeat(reports.size))
236+
output.append(" :--- |")
237+
output.append("\n| AI Daily Tests |")
238+
for (report in reports) {
239+
val icon =
240+
when (report.status) {
241+
TestReport.Status.SUCCESS -> ""
242+
TestReport.Status.FAILURE -> ""
243+
TestReport.Status.OTHER -> ""
244+
}
245+
val link: String = " [$icon](${report.url})"
246+
output.append(link)
247+
output.append(" |")
248+
}
249+
output.append(" ")
250+
val successChance: Int =
251+
reports.count { r -> r.status == TestReport.Status.SUCCESS } * 100 / reports.size
252+
if (successChance == 100) {
253+
output.append("✅ 100%")
254+
} else {
255+
output.append("$successChance%")
256+
}
257+
output.append(" |")
258+
output.append("\n")
259+
return output.toString()
260+
}
261+
195262
private fun parseTestReports(commit: String): List<TestReport> {
196263
val runs = request("actions/runs?head_sha=$commit")
197264
for (el in runs["workflow_runs"] as JsonArray) {
@@ -234,16 +301,7 @@ class TestReportGenerator(private val apiToken: String) {
234301
.dropLastWhile { it.isEmpty() }
235302
.toTypedArray()[1]
236303
name = name.substring(0, name.length - 1) // Remove trailing ")"
237-
val status =
238-
if (job["status"]?.jsonPrimitive?.content == "completed") {
239-
if (job["conclusion"]?.jsonPrimitive?.content == "success") {
240-
TestReport.Status.SUCCESS
241-
} else {
242-
TestReport.Status.FAILURE
243-
}
244-
} else {
245-
TestReport.Status.OTHER
246-
}
304+
val status = TestReport.Status.of(job)
247305
val url = job["html_url"]?.jsonPrimitive?.content ?: throw RuntimeException("PR missing URL")
248306
return TestReport(name, type, status, commit, url)
249307
}
@@ -371,5 +429,11 @@ class TestReportGenerator(private val apiToken: String) {
371429
)
372430
private val CLIENT: HttpClient =
373431
HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build()
432+
private val DATE_FORMAT =
433+
LocalDate.Format {
434+
monthName(MonthNames.ENGLISH_ABBREVIATED)
435+
char(' ')
436+
dayOfMonth()
437+
}
374438
}
375439
}

0 commit comments

Comments
 (0)