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

Fix part of #5343: Implement script to run code coverage for a specific file #5432

10 changes: 10 additions & 0 deletions scripts/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,16 @@ kt_jvm_binary(
],
)

kt_jvm_binary(
name = "run_coverage",
testonly = True,
data = TEST_FILE_EXEMPTION_ASSETS,
main_class = "org.oppia.android.scripts.coverage.RunCoverageKt",
runtime_deps = [
"//scripts/src/java/org/oppia/android/scripts/coverage:run_coverage_lib",
],
)

# Note that this is intentionally not test-only since it's used by the app build pipeline. Also,
# this apparently needs to be a java_binary to set up runfiles correctly when executed within a
# Starlark rule as a tool.
Expand Down
15 changes: 14 additions & 1 deletion scripts/src/java/org/oppia/android/scripts/coverage/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,20 @@ Libraries corresponding to developer scripts that obtain coverage data for test

load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_jvm_library")

kt_jvm_library(
name = "run_coverage_lib",
testonly = True,
srcs = [
"RunCoverage.kt",
],
visibility = ["//scripts:oppia_script_binary_visibility"],
deps = [
"//scripts/src/java/org/oppia/android/scripts/common:bazel_client",
"//scripts/src/java/org/oppia/android/scripts/proto:script_exemptions_java_proto",
"//scripts/src/java/org/oppia/android/scripts/coverage:run_coverage_for_test_target_lib",
],
)

kt_jvm_library(
name = "run_coverage_for_test_target_lib",
testonly = True,
Expand All @@ -13,7 +27,6 @@ kt_jvm_library(
visibility = ["//scripts:oppia_script_binary_visibility"],
deps = [
"//scripts/src/java/org/oppia/android/scripts/common:bazel_client",
"//scripts/src/java/org/oppia/android/scripts/common:git_client",
"//scripts/src/java/org/oppia/android/scripts/coverage:coverage_runner",
],
)
Expand Down
147 changes: 147 additions & 0 deletions scripts/src/java/org/oppia/android/scripts/coverage/RunCoverage.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package org.oppia.android.scripts.coverage

import org.oppia.android.scripts.common.BazelClient
import org.oppia.android.scripts.common.CommandExecutor
import org.oppia.android.scripts.common.CommandExecutorImpl
import org.oppia.android.scripts.common.ScriptBackgroundCoroutineDispatcher
import org.oppia.android.scripts.proto.TestFileExemptions
import java.util.concurrent.TimeUnit
import java.io.File
import java.io.FileInputStream

/**
* Entry point function for running coverage analysis for a source file.
*
* Usage:
* bazel run //scripts:run_coverage_for_test_target -- <path_to_root> <relative_path_to_file>
*
* Arguments:
* - path_to_root: directory path to the root of the Oppia Android repository.
* - relative_path_to_file: the relative path to the file to analyse coverage
*
* Example:
* bazel run //scripts:run_coverage -- $(pwd)
* utility/src/main/java/org/oppia/android/util/parser/math/MathModel.kt
* Example with custom process timeout:
* bazel run //scripts:run_coverage -- $(pwd)
* utility/src/main/java/org/oppia/android/util/parser/math/MathModel.kt processTimeout=10
*
*/
fun main(vararg args: String) {
val repoRoot = args[0]
val filePath = args[1]

ScriptBackgroundCoroutineDispatcher().use { scriptBgDispatcher ->
val processTimeout: Long = args.find { it.startsWith("processTimeout=") }
?.substringAfter("=")
?.toLongOrNull() ?: 5

val commandExecutor: CommandExecutor = CommandExecutorImpl(
scriptBgDispatcher, processTimeout = processTimeout, processTimeoutUnit = TimeUnit.MINUTES
)

RunCoverage(repoRoot, filePath, commandExecutor, scriptBgDispatcher).execute()
}
}

/**
* Class responsible for executing coverage on a given file.
*
* @param repoRoot the root directory of the repository
* @param filePath the relative path to the file to analyse coverage
* @param commandExecutor Executes the specified command in the specified working directory
* @param scriptBgDispatcher the [ScriptBackgroundCoroutineDispatcher] to be used for running the coverage command
*/
class RunCoverage(
private val repoRoot: String,
private val filePath: String,
private val commandExecutor: CommandExecutor,
private val scriptBgDispatcher: ScriptBackgroundCoroutineDispatcher
) {
private val bazelClient by lazy { BazelClient(File(repoRoot), commandExecutor) }

private val rootDirectory = File(repoRoot).absoluteFile
private val testFileExemptionTextProto = "scripts/assets/test_file_exemptions"
var coverageDataList = mutableListOf<String>()
// var covdat: String = ""
Rd4dev marked this conversation as resolved.
Show resolved Hide resolved

/**
* Executes coverage analysis for the specified file.
*
* Loads test file exemptions and checks if the specified file is exempted. If exempted,
* prints a message indicating no coverage analysis is performed. Otherwise, initializes
* a Bazel client, finds potential test file paths, retrieves Bazel targets, and initiates
* coverage analysis for each test target found.
*/
fun execute(): List<String?> {
val testFileExemptionList = loadTestFileExemptionsProto(testFileExemptionTextProto)
.getExemptedFilePathList()

val isExempted = testFileExemptionList.contains(filePath)
if (isExempted) {
println("This file is exempted from having a test file. Hence No coverage!")
return emptyList()
}

val testFilePaths = findTestFile(repoRoot, filePath)
val testTargets = bazelClient.retrieveBazelTargets(testFilePaths)

for (testTarget in testTargets) {
val coverageData = RunCoverageForTestTarget(
rootDirectory,
testTarget.substringBeforeLast(".kt"),
commandExecutor,
scriptBgDispatcher
).runCoverage()!!
coverageDataList.add(coverageData)
}
return coverageDataList
}

fun findTestFile(repoRoot: String, filePath: String): List<String> {
Rd4dev marked this conversation as resolved.
Show resolved Hide resolved
val file = File(filePath)
val parts = file.parent.split(File.separator)
val testFiles = mutableListOf<String>()

if (parts.isNotEmpty() && parts[0] == "scripts") {
val testFilePath = filePath.replace("/java/", "/javatests/").replace(".kt", "Test.kt")
if (File(repoRoot, testFilePath).exists()) {
testFiles.add(testFilePath)
}
} else if (parts.isNotEmpty() && parts[0] == "app") {
val sharedTestFilePath = filePath.replace("/main/", "/sharedTest/").replace(".kt", "Test.kt")
val testFilePath = filePath.replace("/main/", "/test/").replace(".kt", "Test.kt")
val localTestFilePath = filePath.replace("/main/", "/test/").replace(".kt", "LocalTest.kt")

if (File(repoRoot, sharedTestFilePath).exists()) {
testFiles.add(sharedTestFilePath)
}
if (File(repoRoot, testFilePath).exists()) {
testFiles.add(testFilePath)
}
if (File(repoRoot, localTestFilePath).exists()) {
testFiles.add(localTestFilePath)
}
} else {
val defaultTestFilePath = filePath.replace("/main/", "/test/").replace(".kt", "Test.kt")
if (File(repoRoot, defaultTestFilePath).exists()) {
testFiles.add(defaultTestFilePath)
}
}
return testFiles
}

private fun loadTestFileExemptionsProto(testFileExemptiontextProto: String): TestFileExemptions {
val protoBinaryFile = File("$testFileExemptiontextProto.pb")
val builder = TestFileExemptions.getDefaultInstance().newBuilderForType()

// This cast is type-safe since proto guarantees type consistency from mergeFrom(),
// and this method is bounded by the generic type T.
@Suppress("UNCHECKED_CAST")
val protoObj: TestFileExemptions =
FileInputStream(protoBinaryFile).use {
builder.mergeFrom(it)
}.build() as TestFileExemptions
return protoObj
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ class TestBazelWorkspace(private val temporaryRootFolder: TemporaryFolder) {
testBuildFile.appendText(
"""
kt_jvm_test(
name = "test",
name = "$testName",
srcs = ["$testName.kt"],
deps = [
"//$sourceSubpackage:${filename.lowercase()}",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -512,7 +512,7 @@ class BazelClientTest {
subpackage = "coverage"
)

val result = bazelClient.runCoverageForTestTarget("//coverage/test/java/com/example:test")
val result = bazelClient.runCoverageForTestTarget("//coverage/test/java/com/example:TwoSumTest")

// Check if ByteArray is returned from executing coverage command
assertThat(result).isInstanceOf(ByteArray::class.java)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,16 @@ kt_jvm_test(
"//third_party:org_jetbrains_kotlin_kotlin-test-junit",
],
)

kt_jvm_test(
name = "RunCoverageTest",
srcs = ["RunCoverageTest.kt"],
deps = [
"//scripts:test_file_check_assets",
"//scripts/src/java/org/oppia/android/scripts/coverage:run_coverage_lib",
"//scripts/src/java/org/oppia/android/scripts/testing:test_bazel_workspace",
"//testing:assertion_helpers",
"//third_party:com_google_truth_truth",
"//third_party:org_jetbrains_kotlin_kotlin-test-junit",
],
)
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class CoverageRunnerTest {
}

@Test
fun testRunCoverage_emptyDirectory_throwsException() {
fun testCoverageRunner_emptyDirectory_throwsException() {
val exception = assertThrows<IllegalStateException>() {
coverageRunner.getCoverage(bazelTestTarget)
}
Expand All @@ -45,7 +45,7 @@ class CoverageRunnerTest {
}

@Test
fun testRunCoverage_invalidTestTarget_throwsException() {
fun testCoverageRunner_invalidTestTarget_throwsException() {
testBazelWorkspace.initEmptyWorkspace()

val exception = assertThrows<IllegalStateException>() {
Expand All @@ -57,7 +57,7 @@ class CoverageRunnerTest {
}

@Test
fun testRunCoverage_validSampleTestTarget_returnsCoverageData() {
fun testCoverageRunner_validSampleTestTarget_returnsCoverageData() {
testBazelWorkspace.initEmptyWorkspace()

val sourceContent =
Expand Down Expand Up @@ -103,7 +103,7 @@ class CoverageRunnerTest {
subpackage = "coverage"
)

val result = coverageRunner.getCoverage("//coverage/test/java/com/example:test")
val result = coverageRunner.getCoverage("//coverage/test/java/com/example:TwoSumTest")
val expectedResult =
"SF:coverage/main/java/com/example/TwoSum.kt\n" +
"FN:7,com/example/TwoSum${'$'}Companion::sumNumbers (II)Ljava/lang/Object;\n" +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ class RunCoverageForTestTargetTest {

val result = RunCoverageForTestTarget(
tempFolder.root,
"//coverage/test/java/com/example:test",
"//coverage/test/java/com/example:TwoSumTest",
longCommandExecutor,
scriptBgDispatcher
).runCoverage()
Expand Down
Loading
Loading