diff --git a/scripts/BUILD.bazel b/scripts/BUILD.bazel index 689cf6e53d2..23587533fa0 100644 --- a/scripts/BUILD.bazel +++ b/scripts/BUILD.bazel @@ -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. diff --git a/scripts/src/java/org/oppia/android/scripts/common/BazelClient.kt b/scripts/src/java/org/oppia/android/scripts/common/BazelClient.kt index 9aede2fd16f..706adbba03c 100644 --- a/scripts/src/java/org/oppia/android/scripts/common/BazelClient.kt +++ b/scripts/src/java/org/oppia/android/scripts/common/BazelClient.kt @@ -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 { + 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. diff --git a/scripts/src/java/org/oppia/android/scripts/common/CommandExecutorImpl.kt b/scripts/src/java/org/oppia/android/scripts/common/CommandExecutorImpl.kt index 01476cbf3cd..05a90b02110 100644 --- a/scripts/src/java/org/oppia/android/scripts/common/CommandExecutorImpl.kt +++ b/scripts/src/java/org/oppia/android/scripts/common/CommandExecutorImpl.kt @@ -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( diff --git a/scripts/src/java/org/oppia/android/scripts/coverage/BUILD.bazel b/scripts/src/java/org/oppia/android/scripts/coverage/BUILD.bazel new file mode 100644 index 00000000000..53f09dbb98c --- /dev/null +++ b/scripts/src/java/org/oppia/android/scripts/coverage/BUILD.bazel @@ -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", + ], +) diff --git a/scripts/src/java/org/oppia/android/scripts/coverage/CoverageRunner.kt b/scripts/src/java/org/oppia/android/scripts/coverage/CoverageRunner.kt new file mode 100644 index 00000000000..a88e4b6f0a5 --- /dev/null +++ b/scripts/src/java/org/oppia/android/scripts/coverage/CoverageRunner.kt @@ -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 { + 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 { + 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? { + 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 + } +} diff --git a/scripts/src/java/org/oppia/android/scripts/coverage/RunCoverageForTestTarget.kt b/scripts/src/java/org/oppia/android/scripts/coverage/RunCoverageForTestTarget.kt new file mode 100644 index 00000000000..9ddf3a04450 --- /dev/null +++ b/scripts/src/java/org/oppia/android/scripts/coverage/RunCoverageForTestTarget.kt @@ -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 -- + * + * 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 + } + } + } +} diff --git a/scripts/src/java/org/oppia/android/scripts/testing/TestBazelWorkspace.kt b/scripts/src/java/org/oppia/android/scripts/testing/TestBazelWorkspace.kt index 7c57cf0d10c..30fde89364d 100644 --- a/scripts/src/java/org/oppia/android/scripts/testing/TestBazelWorkspace.kt +++ b/scripts/src/java/org/oppia/android/scripts/testing/TestBazelWorkspace.kt @@ -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 diff --git a/scripts/src/javatests/org/oppia/android/scripts/common/BazelClientTest.kt b/scripts/src/javatests/org/oppia/android/scripts/common/BazelClientTest.kt index 3f451d354b8..9740c1acc3d 100644 --- a/scripts/src/javatests/org/oppia/android/scripts/common/BazelClientTest.kt +++ b/scripts/src/javatests/org/oppia/android/scripts/common/BazelClientTest.kt @@ -379,6 +379,79 @@ class BazelClientTest { assertThat(thirdPartyDependenciesList).doesNotContain("@maven//:androidx_annotation_annotation") } + @Test + fun testRunCodeCoverage_forSampleTestTarget_returnsCoverageResult() { + val bazelClient = BazelClient(tempFolder.root, commandExecutor) + testBazelWorkspace.initEmptyWorkspace() + + val sourceContent = + """ + package com.example + + class TwoSum { + + companion object { + fun sumNumbers(a: Int, b: Int): Any { + return if (a == 0 && b == 0) { + "Both numbers are zero" + } else { + a + b + } + } + } + } + """.trimIndent() + + val testContent = + """ + package com.example + + import org.junit.Assert.assertEquals + import org.junit.Test + + class TwoSumTest { + + @Test + fun testSumNumbers() { + assertEquals(TwoSum.sumNumbers(0, 1), 1) + assertEquals(TwoSum.sumNumbers(3, 4), 7) + assertEquals(TwoSum.sumNumbers(0, 0), "Both numbers are zero") + } + } + """.trimIndent() + + testBazelWorkspace.addSourceAndTestFileWithContent( + filename = "TwoSum", + sourceContent = sourceContent, + testContent = testContent, + subpackage = "coverage" + ) + + val result = bazelClient.runCoverageForTestTarget("//coverage/test/java/com/example:test") + + // Check that the test has "PASSED" + val containsPassedValue = result.any { it.contains("PASSED") } + assert(containsPassedValue) { "The test is not 'PASSED'" } + + // Check if the coverage.dat file is generated + val containsCoverageData = result.any { it.contains("coverage.dat") } + assert(containsCoverageData) { "The coverage.dat is not generated" } + } + + @Test + fun testRunCodeCoverage_forNonTestTarget_fails() { + val bazelClient = BazelClient(tempFolder.root, commandExecutor) + testBazelWorkspace.initEmptyWorkspace() + + val exception = assertThrows() { + bazelClient.runCoverageForTestTarget("//coverage/test/java/com/example:test") + } + + // Verify that the underlying Bazel command failed since the test target was not available. + assertThat(exception).hasMessageThat().contains("Expected non-zero exit code") + assertThat(exception).hasMessageThat().contains("no such package") + } + private fun fakeCommandExecutorWithResult(singleLine: String) { // Fake a Bazel command's results to return jumbled results. This has been observed to happen // sometimes in CI, but doesn't have a known cause. The utility is meant to de-jumble these in diff --git a/scripts/src/javatests/org/oppia/android/scripts/coverage/BUILD.bazel b/scripts/src/javatests/org/oppia/android/scripts/coverage/BUILD.bazel new file mode 100644 index 00000000000..cb20129dd61 --- /dev/null +++ b/scripts/src/javatests/org/oppia/android/scripts/coverage/BUILD.bazel @@ -0,0 +1,29 @@ +""" +Tests corresponding to developer scripts that help with obtaining coverage data for test targets. +""" + +load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_test") + +kt_jvm_test( + name = "CoverageRunnerTest", + srcs = ["CoverageRunnerTest.kt"], + deps = [ + "//scripts/src/java/org/oppia/android/scripts/coverage:coverage_runner", + "//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", + ], +) + +kt_jvm_test( + name = "RunCoverageForTestTargetTest", + srcs = ["RunCoverageForTestTargetTest.kt"], + deps = [ + "//scripts/src/java/org/oppia/android/scripts/coverage:run_coverage_for_test_target_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", + ], +) diff --git a/scripts/src/javatests/org/oppia/android/scripts/coverage/CoverageRunnerTest.kt b/scripts/src/javatests/org/oppia/android/scripts/coverage/CoverageRunnerTest.kt new file mode 100644 index 00000000000..7d3ced9b557 --- /dev/null +++ b/scripts/src/javatests/org/oppia/android/scripts/coverage/CoverageRunnerTest.kt @@ -0,0 +1,138 @@ +package org.oppia.android.scripts.coverage + +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import org.oppia.android.scripts.common.ScriptBackgroundCoroutineDispatcher +import org.oppia.android.scripts.testing.TestBazelWorkspace +import org.oppia.android.testing.assertThrows + +/** Tests for [CoverageRunner] */ +class CoverageRunnerTest { + @field:[Rule JvmField] val tempFolder = TemporaryFolder() + + private val scriptBgDispatcher by lazy { ScriptBackgroundCoroutineDispatcher() } + + private lateinit var coverageRunner: CoverageRunner + private lateinit var testBazelWorkspace: TestBazelWorkspace + private lateinit var bazelTestTarget: String + + @Before + fun setUp() { + coverageRunner = CoverageRunner(tempFolder.root, scriptBgDispatcher) + bazelTestTarget = "//:testTarget" + testBazelWorkspace = TestBazelWorkspace(tempFolder) + } + + @After + fun tearDown() { + scriptBgDispatcher.close() + } + + @Test + fun testParseCoverageDataFile_invalidData_returnsNull() { + // Result data from coverage execution that doesn't contain path to coverage data file [coverage.dat] + val invalidResultData = listOf("data1", "data2", "data3") + + val parsedData = coverageRunner.parseCoverageDataFile(invalidResultData) + // Return Null when the coverage data file path is not found + assertThat(parsedData).isNull() + } + + @Test + fun testParseCoverageDataFile_validData_returnsNull() { + // Result data from coverage execution that contains path to coverage data file [coverage.dat] + val validResultData = listOf( + "//package/test/example:test PASSED in 0.4s", + "/path/.cache/bazel/4654367352564/sandbox/__main__/__tmp/coverage/package/test/coverage.dat", + "Executed 1 out of 1 test: 1 test passes." + ) + val expectedResultParsedData = + "/path/.cache/bazel/4654367352564/sandbox/__main__/__tmp/coverage/package/test/coverage.dat" + + val parsedData = coverageRunner.parseCoverageDataFile(validResultData) + assertThat(parsedData).isEqualTo(expectedResultParsedData) + } + + @Test + fun testRunCoverage_emptyDirectory_throwsException() { + val exception = assertThrows() { + coverageRunner.getCoverage(bazelTestTarget) + } + + assertThat(exception).hasMessageThat().contains("not invoked from within a workspace") + } + + @Test + fun testRunCoverage_invalidTestTarget_throwsException() { + testBazelWorkspace.initEmptyWorkspace() + + val exception = assertThrows() { + coverageRunner.getCoverage(bazelTestTarget) + } + + assertThat(exception).hasMessageThat().contains("Expected non-zero exit code") + assertThat(exception).hasMessageThat().contains("no such package") + } + + @Test + fun testRunCoverage_validSampleTestTarget_returnsCoverageData() { + testBazelWorkspace.initEmptyWorkspace() + + val sourceContent = + """ + package com.example + + class TwoSum { + + companion object { + fun sumNumbers(a: Int, b: Int): Any { + return if (a ==0 && b == 0) { + "Both numbers are zero" + } else { + a + b + } + } + } + } + """.trimIndent() + + val testContent = + """ + package com.example + + import org.junit.Assert.assertEquals + import org.junit.Test + + class TwoSumTest { + + @Test + fun testSumNumbers() { + assertEquals(TwoSum.sumNumbers(0, 1), 1) + assertEquals(TwoSum.sumNumbers(3, 4), 7) + assertEquals(TwoSum.sumNumbers(0, 0), "Both numbers are zero") + } + } + """.trimIndent() + + testBazelWorkspace.addSourceAndTestFileWithContent( + filename = "TwoSum", + sourceContent = sourceContent, + testContent = testContent, + subpackage = "coverage" + ) + + val result = coverageRunner.getCoverage("//coverage/test/java/com/example:test") + + // Check that the test has "PASSED" + val containsPassedValue = result.any { it.contains("PASSED") } + assert(containsPassedValue) { "The test is not 'PASSED'" } + + // Check if the coverage.dat file is generated + val containsCoverageData = result.any { it.contains("coverage.dat") } + assert(containsCoverageData) { "The coverage.dat is not generated" } + } +} diff --git a/scripts/src/javatests/org/oppia/android/scripts/coverage/RunCoverageForTestTargetTest.kt b/scripts/src/javatests/org/oppia/android/scripts/coverage/RunCoverageForTestTargetTest.kt new file mode 100644 index 00000000000..2c1d579168f --- /dev/null +++ b/scripts/src/javatests/org/oppia/android/scripts/coverage/RunCoverageForTestTargetTest.kt @@ -0,0 +1,116 @@ +package org.oppia.android.scripts.coverage + +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import org.oppia.android.scripts.common.ScriptBackgroundCoroutineDispatcher +import org.oppia.android.scripts.testing.TestBazelWorkspace +import org.oppia.android.testing.assertThrows + +/** Tests for [RunCoverageForTestTarget] */ +class RunCoverageForTestTargetTest { + @field:[Rule JvmField] val tempFolder = TemporaryFolder() + + private val scriptBgDispatcher by lazy { ScriptBackgroundCoroutineDispatcher() } + + private lateinit var testBazelWorkspace: TestBazelWorkspace + private lateinit var bazelTestTarget: String + + @Before + fun setUp() { + bazelTestTarget = "//:testTarget" + testBazelWorkspace = TestBazelWorkspace(tempFolder) + } + + @After + fun tearDown() { + scriptBgDispatcher.close() + } + + @Test + fun testRunCoverageForTestTarget_emptyDirectory_throwsException() { + val exception = assertThrows() { + RunCoverageForTestTarget( + tempFolder.root, + bazelTestTarget + ).runCoverage() + } + + assertThat(exception).hasMessageThat().contains("not invoked from within a workspace") + } + + @Test + fun testRunCoverageForTestTarget_invalidTestTarget_throwsException() { + testBazelWorkspace.initEmptyWorkspace() + + val exception = assertThrows() { + RunCoverageForTestTarget( + tempFolder.root, + bazelTestTarget + ).runCoverage() + } + + assertThat(exception).hasMessageThat().contains("Expected non-zero exit code") + assertThat(exception).hasMessageThat().contains("no such package") + } + + @Test + fun testRunCoverageForTestTarget_validSampleTestTarget_returnsCoverageDataPath() { + testBazelWorkspace.initEmptyWorkspace() + + val sourceContent = + """ + package com.example + + class TwoSum { + + companion object { + fun sumNumbers(a: Int, b: Int): Any { + return if (a ==0 && b == 0) { + "Both numbers are zero" + } else { + a + b + } + } + } + } + """.trimIndent() + + val testContent = + """ + package com.example + + import org.junit.Assert.assertEquals + import org.junit.Test + + class TwoSumTest { + + @Test + fun testSumNumbers() { + assertEquals(TwoSum.sumNumbers(0, 1), 1) + assertEquals(TwoSum.sumNumbers(3, 4), 7) + assertEquals(TwoSum.sumNumbers(0, 0), "Both numbers are zero") + } + } + """.trimIndent() + + testBazelWorkspace.addSourceAndTestFileWithContent( + filename = "TwoSum", + sourceContent = sourceContent, + testContent = testContent, + subpackage = "coverage" + ) + + val result = RunCoverageForTestTarget( + tempFolder.root, + "//coverage/test/java/com/example:test" + ).runCoverage() + + // Check if the coverage.dat file is generated and parsed as result + val parsedCoverageDataPath = result?.endsWith("coverage.dat") + assert(parsedCoverageDataPath!!) { "The coverage.dat is not generated" } + } +}