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: Introduce RunCoverageForTestTarget and CoverageRunner utilities to run code coverage for a specific bazel test target [Blocked: #4886]. #5420

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

kt_jvm_binary(
name = "run_coverage_for_test_target",
testonly = True,
main_class = "org.oppia.android.scripts.coverage.RunCoverageForTestTargetKt",
runtime_deps = [
"//scripts/src/java/org/oppia/android/scripts/coverage:run_coverage_for_test_target_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
13 changes: 13 additions & 0 deletions scripts/src/java/org/oppia/android/scripts/common/BazelClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,19 @@ class BazelClient(private val rootDirectory: File, private val commandExecutor:
return correctedTargets
}

/**
* Runs code coverage for the specified Bazel test target.
*
* @param bazelTestTarget Bazel test target for which code coverage will be run.
* @return generated coverageResult output
*/
fun runCoverageForTestTarget(bazelTestTarget: String): List<String> {
return executeBazelCommand(
"coverage",
bazelTestTarget
)
}

/**
* Returns the results of a query command with a potentially large list of [values] that will be
* split up into multiple commands to avoid overflow the system's maximum argument limit.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import java.util.concurrent.TimeUnit
* The default amount of time that should be waited before considering a process as 'hung', in
* milliseconds.
*/
const val WAIT_PROCESS_TIMEOUT_MS = 60_000L
const val WAIT_PROCESS_TIMEOUT_MS = 600_000L

/** Default implementation of [CommandExecutor]. */
class CommandExecutorImpl(
Expand Down
32 changes: 32 additions & 0 deletions scripts/src/java/org/oppia/android/scripts/coverage/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""
Libraries corresponding to developer scripts that obtain coverage data for test targets.
"""

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

kt_jvm_library(
name = "run_coverage_for_test_target_lib",
testonly = True,
srcs = [
"RunCoverageForTestTarget.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/common:git_client",
"//scripts/src/java/org/oppia/android/scripts/coverage:coverage_runner",
],
)

kt_jvm_library(
name = "coverage_runner",
testonly = True,
srcs = [
"CoverageRunner.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/common:git_client",
],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package org.oppia.android.scripts.coverage

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
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 java.io.File

/**
* Class responsible for running coverage analysis asynchronously.
*
* @param repoRoot the absolute path to the working root directory
* @param targetFile Path to the target file to analyze coverage.
*/
class CoverageRunner(
private val repoRoot: File,
private val scriptBgDispatcher: ScriptBackgroundCoroutineDispatcher
) {

/**
* Runs coverage analysis asynchronously for the Bazel test target.
*
* @param repoRoot the absolute path to the working root directory
* @param scriptBgDispatcher the [ScriptBackgroundCoroutineDispatcher] to be used for running the coverage command
* @param bazelTestTarget Bazel test target to analyze coverage.
* @return a deferred value that contains the path of the coverage data file [will contain the proto for the coverage data].
*/
suspend fun runWithCoverageAsync(
bazelTestTarget: String
): Deferred<String?> {
return CoroutineScope(scriptBgDispatcher).async {
val coverageData = getCoverage(bazelTestTarget)
val data = coverageData
parseCoverageDataFile(data)
}
}

/**
* Runs coverage command for the Bazel test target.
*
* @param repoRoot the absolute path to the working root directory
* @param scriptBgDispatcher the [ScriptBackgroundCoroutineDispatcher] to be used for running the coverage command
* @param bazelTestTarget Bazel test target to analyze coverage.
* @return a lisf of string that contains the result of the coverage execution.
*/
fun getCoverage(
bazelTestTarget: String
): List<String> {
val commandExecutor: CommandExecutor = CommandExecutorImpl(scriptBgDispatcher)
val bazelClient = BazelClient(repoRoot, commandExecutor)
val coverageData = bazelClient.runCoverageForTestTarget(bazelTestTarget)
return coverageData
}

/**
* Parse the coverage command result to extract the path of the coverage data file.
*
* @param data the result from the execution of the coverage command
* @return the extracted path of the coverage data file.
*/
fun parseCoverageDataFile(data: List<String>): String? {
val regex = ".*coverage\\.dat$".toRegex()
for (line in data) {
val match = regex.find(line)
val extractedPath = match?.value?.substringAfterLast(",")?.trim()
if (extractedPath != null) {
println("Parsed Coverage Data File: $extractedPath")
return extractedPath
}
}
return null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package org.oppia.android.scripts.coverage

import kotlinx.coroutines.runBlocking
import org.oppia.android.scripts.common.ScriptBackgroundCoroutineDispatcher
import java.io.File

/**
* Entry point function for running coverage analysis for a single test target.
*
* Usage:
* bazel run //scripts:run_coverage_for_test_target -- <path_to_root> <//:test_targetname>
*
* Arguments:
* - path_to_root: directory path to the root of the Oppia Android repository.
* - test_targetname: bazel target name of the test
*
* Example:
* bazel run //scripts:run_coverage_for_test_target -- $(pwd)
* //utility/src/test/java/org/oppia/android/util/parser/math:MathModelTest
*/
fun main(vararg args: String) {
val repoRoot = File(args[0]).absoluteFile.normalize()
val targetPath = args[1]

RunCoverageForTestTarget(repoRoot, targetPath).runCoverage()
}

/**
* Class responsible for analyzing target files for coverage and generating reports.
*/
class RunCoverageForTestTarget(
private val repoRoot: File,
private val targetPath: String
) {

/**
* Analyzes target file for coverage, generates chosen reports accordingly.
*/
fun runCoverage(): String? {
return runWithCoverageAnalysis()
}

/**
* Runs coverage analysis on the specified target file asynchronously.
*
* @return [Path of the coverage data file].
*/
fun runWithCoverageAnalysis(): String? {
return ScriptBackgroundCoroutineDispatcher().use { scriptBgDispatcher ->
runBlocking {
val result =
CoverageRunner(repoRoot, scriptBgDispatcher).runWithCoverageAsync(targetPath).await()
result
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,133 @@ class TestBazelWorkspace(private val temporaryRootFolder: TemporaryFolder) {
assertThat(bazelRcFile.exists()).isTrue()
}

/**
* Adds a source file and test file with the specified name and content,
* and updates the corresponding build configuration.
*
* @param filename the name of the source file (without the .kt extension)
* @param sourceContent the content of the source file
* @param testContent the content of the test file
* @param subpackage the subpackage under which the source and test files should be added
*/
fun addSourceAndTestFileWithContent(
filename: String,
sourceContent: String,
testContent: String,
subpackage: String
) {
val sourceSubpackage = "$subpackage/main/java/com/example"
addSourceContentAndBuildFile(filename, sourceContent, sourceSubpackage)

val testSubpackage = "$subpackage/test/java/com/example"
val testFileName = "${filename}Test"
addTestContentAndBuildFile(filename, testFileName, testContent, testSubpackage)
}

/**
* Adds a source file with the specified name and content to the specified subpackage,
* and updates the corresponding build configuration.
*
* @param filename the name of the source file (without the .kt extension)
* @param sourceContent the content of the source file
* @param sourceSubpackage the subpackage under which the source file should be added
* @return the target name of the added source file
*/
fun addSourceContentAndBuildFile(
filename: String,
sourceContent: String,
sourceSubpackage: String
) {
initEmptyWorkspace()
ensureWorkspaceIsConfiguredForKotlin()
setUpWorkspaceForRulesJvmExternal(
listOf("junit:junit:4.12")
)

// Create the source subpackage directory if it doesn't exist
if (!File(temporaryRootFolder.root, sourceSubpackage.replace(".", "/")).exists()) {
temporaryRootFolder.newFolder(*(sourceSubpackage.split(".")).toTypedArray())
}

// Create the source file
val sourceFile = temporaryRootFolder.newFile(
"${sourceSubpackage.replace(".", "/")}/$filename.kt"
)
sourceFile.writeText(sourceContent)

// Create or update the BUILD file for the source file
val buildFileRelativePath = "${sourceSubpackage.replace(".", "/")}/BUILD.bazel"
val buildFile = File(temporaryRootFolder.root, buildFileRelativePath)
if (!buildFile.exists()) {
temporaryRootFolder.newFile(buildFileRelativePath)
}

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

kt_jvm_library(
name = "${filename.lowercase()}",
srcs = ["$filename.kt"],
visibility = ["//visibility:public"],
deps = [],
)
""".trimIndent() + "\n"
)
}

/**
* Adds a test file with the specified name and content to the specified subpackage,
* and updates the corresponding build configuration.
*
* @param filename the name of the source file (without the .kt extension)
* @param testName the name of the test file (without the .kt extension)
* @param testContent the content of the test file
* @param testSubpackage the subpackage for the test file
*/
fun addTestContentAndBuildFile(
filename: String,
testName: String,
testContent: String,
testSubpackage: String
) {
initEmptyWorkspace()

// Create the test subpackage directory for the test file if it doesn't exist
if (!File(temporaryRootFolder.root, testSubpackage.replace(".", "/")).exists()) {
temporaryRootFolder.newFolder(*(testSubpackage.split(".")).toTypedArray())
}

// Create the test file
val testFile = temporaryRootFolder.newFile("${testSubpackage.replace(".", "/")}/$testName.kt")
testFile.writeText(testContent)

// Create or update the BUILD file for the test file
val testBuildFileRelativePath = "${testSubpackage.replace(".", "/")}/BUILD.bazel"
val testBuildFile = File(temporaryRootFolder.root, testBuildFileRelativePath)
if (!testBuildFile.exists()) {
temporaryRootFolder.newFile(testBuildFileRelativePath)
}

// Add the test file to the BUILD file with appropriate dependencies
testBuildFile.appendText(
"""
load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_test")

kt_jvm_test(
name = "test",
srcs = ["$testName.kt"],
deps = [
"//coverage/main/java/com/example:${filename.lowercase()}",
"@maven//:junit_junit",
],
visibility = ["//visibility:public"],
test_class = "com.example.$testName",
)
""".trimIndent() + "\n"
)
}

/**
* Generates and adds a new kt_jvm_test target with the target name [testName] and test file
* [testFile]. This can be used to add multiple tests to the same build file, and will
Expand Down
Loading
Loading