Skip to content
This repository has been archived by the owner on Aug 14, 2024. It is now read-only.

adding gradle reporter plugin #32

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
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: 7 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ plugins {
id 'kotlin-android'
}

apply plugin: ReporterPlugin

android {
compileSdkVersion 30
buildToolsVersion "30.0.3"
Expand Down Expand Up @@ -59,3 +61,8 @@ dependencies {
androidTestImplementation "androidx.test:core-ktx:1.4.0"
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

reporter {
returnPath 'https://developer.android.com/courses/pathways/android-basics-kotlin-four'
contentPath 'https://developer.android.com/codelabs/basic-android-kotlin-training-project-lemonade'
}
4 changes: 2 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ buildscript {
ext.kotlin_version = "1.5.20"
repositories {
google()
mavenCentral()
jcenter()
}
dependencies {
classpath "com.android.tools.build:gradle:4.1.3"
Expand All @@ -17,7 +17,7 @@ buildscript {
allprojects {
repositories {
google()
mavenCentral()
jcenter()
}
}

Expand Down
26 changes: 26 additions & 0 deletions buildSrc/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright (C) 2021 The Android Open Source Project.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
repositories {
mavenCentral()
}

plugins {
`kotlin-dsl`
}

dependencies {
testImplementation("junit:junit:4.+")
}
17 changes: 17 additions & 0 deletions buildSrc/settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Copyright (C) 2021 The Android Open Source Project.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

// empty file that allows gradle tests to be run within buildSrc
55 changes: 55 additions & 0 deletions buildSrc/src/main/kotlin/ReporterPlugin.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright (C) 2021 The Android Open Source Project.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.tasks.VerificationTask

open class ReporterPluginExtension {
var contentPath: String = ""
var returnPath: String = ""
}

class ReporterPlugin : Plugin<Project> {
override fun apply(project: Project) {
val extension = project.extensions.create("reporter", ReporterPluginExtension::class.java)

project.task("agpRunTests").doLast {
val containingDir = project.projectDir.toPath().resolve("build")
val endingSuffix = "debugAndroidTest.xml"
val result = readTestResult(containingDir, endingSuffix)

val testDir = project.projectDir.toPath().resolve("src/androidTest")
val projName = project.rootProject.name
val voucher = generateVoucherString(projName, testDir, result)

val url = generateReportUrl(
voucher,
extension.contentPath,
extension.returnPath,
result.passed,
result.total
)
openUrlInBrowser(url)
}.dependsOn("connectedAndroidTest")

project.tasks.all {
val isInstrumentationTask = this.name.matches(Regex("connected.*AndroidTest"))
if (isInstrumentationTask && this is VerificationTask) {
this.ignoreFailures = true
}
}
}
}
64 changes: 64 additions & 0 deletions buildSrc/src/main/kotlin/report.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright (C) 2021 The Android Open Source Project.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import java.awt.Desktop
import java.net.URI
import java.net.URLEncoder

fun generateReportUrl(
voucher: String,
contentPath: String,
returnPath: String,
passedCount: Int,
totalCount: Int
): String {
val params = HashMap<String, String>()
if (voucher.isNotEmpty()) params["vc"] = voucher
// continue query param is used to indicate the "go back" functionality on codelabs
if (returnPath.isNotEmpty()) params["continue"] = returnPath
params["pc"] = passedCount.toString()
params["tc"] = totalCount.toString()

val paramsStr = params
.map{ "${it.key}=${URLEncoder.encode(it.value, "utf-8")}" }
.sorted()
.joinToString("&")
return "$contentPath?$paramsStr"
}

fun openUrlInBrowser(url: String) {
if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
Desktop.getDesktop().browse(URI(url))
return
}

// Not handled by java.awt.Desktop
val os = System.getProperty("os.name").toLowerCase()
val command = if (os.contains("mac")) {
"open $url"
} else if (os.contains("nix") || os.contains("nux")) {
"xdg-open $url"
} else {
""
}

val runtime = Runtime.getRuntime()
try {
runtime.exec(command)
} catch (e: Exception) {
println(e)
println("Unable to open the browser. Please open this link to register your results: $url")
}
}
56 changes: 56 additions & 0 deletions buildSrc/src/main/kotlin/testResult.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright (C) 2021 The Android Open Source Project.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import java.nio.file.Files
import java.nio.file.Path
import java.util.stream.Collectors
import javax.xml.parsers.DocumentBuilderFactory

class FileNotFoundException : Exception("Cannot find instrumentation test results xml")
class InvalidXmlException : Exception("Test results xml is invalid")

data class Result(val passed: Int, val total: Int)

fun readTestResult(containingDir: Path, endingSuffix: String): Result {
val found = Files
.walk(containingDir)
.filter { f -> f.toString().endsWith(endingSuffix) }
.collect(Collectors.toList())

if (found.size != 1) {
throw FileNotFoundException()
}
val firstFile = found.first().toFile()

val xmlDoc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(firstFile)
xmlDoc.documentElement.normalize()

val tags = xmlDoc.getElementsByTagName("testsuite")
if (tags.length != 1) {
throw InvalidXmlException()
}

var total = 0
var notPassed = 0
for(i in 0 until tags.length) {
val attr = tags.item(i).attributes
total += attr.getNamedItem("tests").nodeValue.toInt()
notPassed += attr.getNamedItem("failures").nodeValue.toInt()
notPassed += attr.getNamedItem("errors").nodeValue.toInt()
notPassed += attr.getNamedItem("skipped").nodeValue.toInt()
}

return Result(total - notPassed, total)
}
85 changes: 85 additions & 0 deletions buildSrc/src/main/kotlin/voucher.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* Copyright (C) 2021 The Android Open Source Project.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import java.io.File
import java.nio.file.Path

fun generateVoucherString(projName: String, testDir: Path, testResult: Result): String {
val lines = getValidTestLinesCount(testDir)
val obfuscatedLines = rotateLeftInt(lines, lines)
val obfuscatedName = rot13(projName)
val obfuscatedResult = obfuscateResult(testResult)
return "${obfuscatedName}${obfuscatedLines}${obfuscatedResult}"
}

fun getValidTestLinesCount(testDir: Path): Int {
val testFiles = testDir.toFile().walk().filter { !it.isDirectory }.toList()
return testFiles.map { f -> countValidLines(f) }.reduce { acc, num -> acc + num }
}

fun countValidLines(file: File): Int {
var lineCount = 0
var multiStartCount = 0
file.forEachLine(action = fun(line: String) {
val trimmed = line.trim()
val isLineComment = trimmed.take(2) == "//"
if (isLineComment) {
return
}

var charCount = 0
for (i in trimmed.indices) {
if (i > 0 && trimmed[i - 1] == '/' && trimmed[i] == '*') {
if (multiStartCount == 0) {
charCount--
}
multiStartCount++
} else if (multiStartCount > 0 && i > 0 && trimmed[i - 1] == '*' && trimmed[i] == '/') {
multiStartCount--
} else if (multiStartCount == 0 && !trimmed[i].isWhitespace()) {
charCount++
}
}
if (charCount == 0) {
return
}

lineCount++
})

return lineCount
}

fun rot13(str: String): String {
return str.map{
if (it.isLetter()) {
val moved = it.toUpperCase().toInt() + 13
val rotated = if (moved > 90) {
moved - 26
} else moved
rotated.toChar().toLowerCase()
} else {
it
}
}.joinToString("")
}

fun rotateLeftInt(num: Int, bitCount: Int): Int = num.shl(bitCount) or num.ushr(32 - bitCount)

fun obfuscateResult(result: Result): String {
val rotPassed = rotateLeftInt(result.passed, result.passed).toString()
val rotTotal = rotateLeftInt(result.total, result.total).toString()
return "${rotPassed}:${rotTotal}"
}
40 changes: 40 additions & 0 deletions buildSrc/src/test/kotlin/ReportTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright (C) 2021 The Android Open Source Project.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import org.junit.Test
import org.junit.Assert

class ReportTest {
@Test
fun generateReportUrl_params_encoded() {
val want = "https://def.com?continue=https%3A%2F%2Fghi.com&pc=1&tc=3&vc=abc%3F"
val got = generateReportUrl("abc?", "https://def.com", "https://ghi.com", 1, 3)
Assert.assertEquals(want, got)
}

@Test
fun generateReportUrl_empty_voucher() {
val want = "https://def.com?continue=https%3A%2F%2Fghi.com&pc=1&tc=3"
val got = generateReportUrl("", "https://def.com", "https://ghi.com", 1, 3)
Assert.assertEquals(want, got)
}

@Test
fun generateReportUrl_empty_testResult() {
val want = "https://def.com?continue=https%3A%2F%2Fghi.com&pc=0&tc=0&vc=abc"
val got = generateReportUrl("abc", "https://def.com", "https://ghi.com", 0, 0)
Assert.assertEquals(want, got)
}
}
Loading