diff --git a/scripts/BUILD.bazel b/scripts/BUILD.bazel index 689cf6e53d2..2fba670d34e 100644 --- a/scripts/BUILD.bazel +++ b/scripts/BUILD.bazel @@ -237,6 +237,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. 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 62ce53bfb82..8c3c546f026 100644 --- a/scripts/src/java/org/oppia/android/scripts/common/BazelClient.kt +++ b/scripts/src/java/org/oppia/android/scripts/common/BazelClient.kt @@ -142,9 +142,11 @@ class BazelClient(private val rootDirectory: File, private val commandExecutor: * or null if the coverage data file could not be parsed */ fun runCoverageForTestTarget(bazelTestTarget: String): List? { + val computeInstrumentation = bazelTestTarget.split("/").let { "//${it[2]}/..." } val coverageCommandOutputLines = executeBazelCommand( "coverage", - bazelTestTarget + bazelTestTarget, + "--instrumentation_filter=$computeInstrumentation" ) return parseCoverageDataFilePath(coverageCommandOutputLines)?.let { path -> File(path).readLines() diff --git a/scripts/src/java/org/oppia/android/scripts/coverage/BUILD.bazel b/scripts/src/java/org/oppia/android/scripts/coverage/BUILD.bazel index 4c7ab41a5fa..aa0c56dd7c3 100644 --- a/scripts/src/java/org/oppia/android/scripts/coverage/BUILD.bazel +++ b/scripts/src/java/org/oppia/android/scripts/coverage/BUILD.bazel @@ -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 = [ + ":coverage_runner", + "//scripts/src/java/org/oppia/android/scripts/common:bazel_client", + "//scripts/src/java/org/oppia/android/scripts/proto:script_exemptions_java_proto", + ], +) + kt_jvm_library( name = "coverage_runner", testonly = True, diff --git a/scripts/src/java/org/oppia/android/scripts/coverage/RunCoverage.kt b/scripts/src/java/org/oppia/android/scripts/coverage/RunCoverage.kt new file mode 100644 index 00000000000..35b27a4b95e --- /dev/null +++ b/scripts/src/java/org/oppia/android/scripts/coverage/RunCoverage.kt @@ -0,0 +1,148 @@ +package org.oppia.android.scripts.coverage + +import kotlinx.coroutines.runBlocking +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.io.File +import java.util.concurrent.TimeUnit + +/** + * Entry point function for running coverage analysis for a source file. + * + * Usage: + * bazel run //scripts:run_coverage_for_test_target -- + * + * 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] + + if (!File(repoRoot, filePath).exists()) { + error("File doesn't exist: $filePath.") + } + + 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" + + /** + * 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. + * + * @return a list of lists containing coverage data for each requested test target, if + * the file is exempted from having a test file, an empty list is returned + */ + fun execute(): List> { + val testFileExemptionList = loadTestFileExemptionsProto(testFileExemptionTextProto) + .testFileExemptionList + .filter { it.testFileNotRequired } + .map { it.exemptedFilePath } + + if (filePath in testFileExemptionList) { + println("This file is exempted from having a test file; skipping coverage check.") + return emptyList() + } + + val testFilePaths = findTestFile(repoRoot, filePath) + if (testFilePaths.isEmpty()) { + error("No appropriate test file found for $filePath") + } + + val testTargets = bazelClient.retrieveBazelTargets(testFilePaths) + + return testTargets.mapNotNull { testTarget -> + val coverageData = runCoverageForTarget(testTarget) + if (coverageData == null) { + println("Coverage data for $testTarget is null") + } + coverageData + } + } + + private fun runCoverageForTarget(testTarget: String): List? { + return runBlocking { + CoverageRunner(rootDirectory, scriptBgDispatcher, commandExecutor) + .runWithCoverageAsync(testTarget.removeSuffix(".kt")) + .await() + } + } +} + +private fun findTestFile(repoRoot: String, filePath: String): List { + val possibleTestFilePaths = when { + filePath.startsWith("scripts/") -> { + listOf(filePath.replace("/java/", "/javatests/").replace(".kt", "Test.kt")) + } + filePath.startsWith("app/") -> { + listOf( + filePath.replace("/main/", "/sharedTest/").replace(".kt", "Test.kt"), + filePath.replace("/main/", "/test/").replace(".kt", "Test.kt"), + filePath.replace("/main/", "/test/").replace(".kt", "LocalTest.kt") + ) + } + else -> { + listOf(filePath.replace("/main/", "/test/").replace(".kt", "Test.kt")) + } + } + + val repoRootFile = File(repoRoot).absoluteFile + + return possibleTestFilePaths + .map { File(repoRootFile, it) } + .filter(File::exists) + .map { it.relativeTo(repoRootFile).path } +} + +private fun loadTestFileExemptionsProto(testFileExemptiontextProto: String): TestFileExemptions { + return File("$testFileExemptiontextProto.pb").inputStream().use { stream -> + TestFileExemptions.newBuilder().also { builder -> + builder.mergeFrom(stream) + }.build() + } +} 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 71ee2eb542a..2e4f1b20c0f 100644 --- a/scripts/src/java/org/oppia/android/scripts/testing/TestBazelWorkspace.kt +++ b/scripts/src/java/org/oppia/android/scripts/testing/TestBazelWorkspace.kt @@ -57,22 +57,22 @@ class TestBazelWorkspace(private val temporaryRootFolder: TemporaryFolder) { * @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 + * @param sourceSubpackage the subpackage under which the source files should be added + * @param testSubpackage the subpackage under which the test files should be added */ fun addSourceAndTestFileWithContent( filename: String, sourceContent: String, testContent: String, - subpackage: String + sourceSubpackage: String, + testSubpackage: 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, @@ -83,6 +83,51 @@ class TestBazelWorkspace(private val temporaryRootFolder: TemporaryFolder) { ) } + /** + * Adds a source file and 2 test files 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 testContentShared the content of the test file for SharedTest Package + * @param testContentLocal the content of the test file for Test Package + * @param subpackage the subpackage under which the source and test files should be added + */ + fun addMultiLevelSourceAndTestFileWithContent( + filename: String, + sourceContent: String, + testContentShared: String, + testContentLocal: String, + subpackage: String + ) { + val sourceSubpackage = "$subpackage/main/java/com/example" + addSourceContentAndBuildFile( + filename, + sourceContent, + sourceSubpackage + ) + + val testSubpackageShared = "$subpackage/sharedTest/java/com/example" + val testFileNameShared = "${filename}Test" + addTestContentAndBuildFile( + filename, + testFileNameShared, + testContentShared, + sourceSubpackage, + testSubpackageShared + ) + + val testSubpackageLocal = "$subpackage/test/java/com/example" + val testFileNameLocal = "${filename}LocalTest" + addTestContentAndBuildFile( + filename, + testFileNameLocal, + testContentLocal, + sourceSubpackage, + testSubpackageLocal + ) + } + /** * Adds a source file with the specified name and content to the specified subpackage, * and updates the corresponding build configuration. @@ -172,7 +217,7 @@ class TestBazelWorkspace(private val temporaryRootFolder: TemporaryFolder) { testBuildFile.appendText( """ kt_jvm_test( - name = "test", + name = "$testName", srcs = ["$testName.kt"], deps = [ "//$sourceSubpackage:${filename.lowercase()}", 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 075feeab3fe..db725861d16 100644 --- a/scripts/src/javatests/org/oppia/android/scripts/common/BazelClientTest.kt +++ b/scripts/src/javatests/org/oppia/android/scripts/common/BazelClientTest.kt @@ -424,10 +424,11 @@ class BazelClientTest { filename = "TwoSum", sourceContent = sourceContent, testContent = testContent, - subpackage = "coverage" + sourceSubpackage = "coverage/main/java/com/example", + testSubpackage = "coverage/test/java/com/example" ) - val result = bazelClient.runCoverageForTestTarget("//coverage/test/java/com/example:test") + val result = bazelClient.runCoverageForTestTarget("//coverage/test/java/com/example:TwoSumTest") val expectedResult = listOf( "SF:coverage/main/java/com/example/TwoSum.kt", "FN:7,com/example/TwoSum${'$'}Companion::sumNumbers (II)Ljava/lang/Object;", diff --git a/scripts/src/javatests/org/oppia/android/scripts/coverage/BUILD.bazel b/scripts/src/javatests/org/oppia/android/scripts/coverage/BUILD.bazel index f2e5c80564b..6c200e83d8a 100644 --- a/scripts/src/javatests/org/oppia/android/scripts/coverage/BUILD.bazel +++ b/scripts/src/javatests/org/oppia/android/scripts/coverage/BUILD.bazel @@ -15,3 +15,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", + ], +) diff --git a/scripts/src/javatests/org/oppia/android/scripts/coverage/CoverageRunnerTest.kt b/scripts/src/javatests/org/oppia/android/scripts/coverage/CoverageRunnerTest.kt index bae58d98feb..6a89e9db798 100644 --- a/scripts/src/javatests/org/oppia/android/scripts/coverage/CoverageRunnerTest.kt +++ b/scripts/src/javatests/org/oppia/android/scripts/coverage/CoverageRunnerTest.kt @@ -37,7 +37,7 @@ class CoverageRunnerTest { } @Test - fun testRunCoverage_emptyDirectory_throwsException() { + fun testRunWithCoverageAsync_emptyDirectory_throwsException() { val exception = assertThrows() { runBlocking { coverageRunner.runWithCoverageAsync(bazelTestTarget).await() @@ -48,7 +48,7 @@ class CoverageRunnerTest { } @Test - fun testRunCoverage_invalidTestTarget_throwsException() { + fun testRunWithCoverageAsync_invalidTestTarget_throwsException() { testBazelWorkspace.initEmptyWorkspace() val exception = assertThrows() { @@ -62,7 +62,7 @@ class CoverageRunnerTest { } @Test - fun testRunCoverage_validSampleTestTarget_returnsCoverageData() { + fun testRunWithCoverageAsync_validSampleTestTarget_returnsCoverageData() { testBazelWorkspace.initEmptyWorkspace() val sourceContent = @@ -105,12 +105,13 @@ class CoverageRunnerTest { filename = "TwoSum", sourceContent = sourceContent, testContent = testContent, - subpackage = "coverage" + sourceSubpackage = "coverage/main/java/com/example", + testSubpackage = "coverage/test/java/com/example" ) val result = runBlocking { coverageRunner.runWithCoverageAsync( - "//coverage/test/java/com/example:test" + "//coverage/test/java/com/example:TwoSumTest" ).await() } val expectedResult = listOf( diff --git a/scripts/src/javatests/org/oppia/android/scripts/coverage/RunCoverageTest.kt b/scripts/src/javatests/org/oppia/android/scripts/coverage/RunCoverageTest.kt new file mode 100644 index 00000000000..fb9db5d14b0 --- /dev/null +++ b/scripts/src/javatests/org/oppia/android/scripts/coverage/RunCoverageTest.kt @@ -0,0 +1,459 @@ +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.CommandExecutorImpl +import org.oppia.android.scripts.common.ScriptBackgroundCoroutineDispatcher +import org.oppia.android.scripts.testing.TestBazelWorkspace +import org.oppia.android.testing.assertThrows +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.PrintStream +import java.util.concurrent.TimeUnit + +/** Tests for [RunCoverage]. */ +class RunCoverageTest { + @field:[Rule JvmField] val tempFolder = TemporaryFolder() + + private val outContent: ByteArrayOutputStream = ByteArrayOutputStream() + private val originalOut: PrintStream = System.out + + private val scriptBgDispatcher by lazy { ScriptBackgroundCoroutineDispatcher() } + private val commandExecutor by lazy { CommandExecutorImpl(scriptBgDispatcher) } + private val longCommandExecutor by lazy { initializeCommandExecutorWithLongProcessWaitTime() } + + private lateinit var testBazelWorkspace: TestBazelWorkspace + private lateinit var sampleFilePath: String + + @Before + fun setUp() { + sampleFilePath = "/path/to/Sample.kt" + testBazelWorkspace = TestBazelWorkspace(tempFolder) + System.setOut(PrintStream(outContent)) + } + + @After + fun tearDown() { + System.setOut(originalOut) + scriptBgDispatcher.close() + } + + @Test + fun testRunCoverage_invalidFile_throwsException() { + testBazelWorkspace.initEmptyWorkspace() + val exception = assertThrows() { + main(tempFolder.root.absolutePath, "file.kt") + } + + assertThat(exception).hasMessageThat().contains("File doesn't exist") + } + + @Test + fun testRunCoverage_missingTestFileNotExempted_throwsException() { + testBazelWorkspace.initEmptyWorkspace() + val exception = assertThrows() { + val sampleFile = File(tempFolder.root.absolutePath, "file.kt") + sampleFile.createNewFile() + main(tempFolder.root.absolutePath, "file.kt") + } + + assertThat(exception).hasMessageThat().contains("No appropriate test file found") + } + + @Test + fun testRunCoverage_testFileExempted_noCoverage() { + val exemptedFilePath = "app/src/main/java/org/oppia/android/app/activity/ActivityComponent.kt" + + RunCoverage( + "${tempFolder.root}", + exemptedFilePath, + commandExecutor, + scriptBgDispatcher + ).execute() + + assertThat(outContent.toString()) + .isEqualTo("This file is exempted from having a test file; skipping coverage check.\n") + } + + @Test + fun testRunCoverage_sampleTests_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, + sourceSubpackage = "coverage/main/java/com/example", + testSubpackage = "coverage/test/java/com/example" + ) + + val result = RunCoverage( + "${tempFolder.root}", + "coverage/main/java/com/example/TwoSum.kt", + longCommandExecutor, + scriptBgDispatcher + ).execute() + + val expectedResultList = listOf( + listOf( + "SF:coverage/main/java/com/example/TwoSum.kt", + "FN:7,com/example/TwoSum${'$'}Companion::sumNumbers (II)Ljava/lang/Object;", + "FN:3,com/example/TwoSum:: ()V", + "FNDA:1,com/example/TwoSum${'$'}Companion::sumNumbers (II)Ljava/lang/Object;", + "FNDA:0,com/example/TwoSum:: ()V", + "FNF:2", + "FNH:1", + "BRDA:7,0,0,1", + "BRDA:7,0,1,1", + "BRDA:7,0,2,1", + "BRDA:7,0,3,1", + "BRF:4", + "BRH:4", + "DA:3,0", + "DA:7,1", + "DA:8,1", + "DA:10,1", + "LH:3", + "LF:4", + "end_of_record" + ) + ) + + assertThat(result).isEqualTo(expectedResultList) + } + + @Test + fun testRunCoverage_scriptTests_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, + sourceSubpackage = "scripts/java/com/example", + testSubpackage = "scripts/javatests/com/example" + ) + + val result = RunCoverage( + "${tempFolder.root}", + "scripts/java/com/example/TwoSum.kt", + longCommandExecutor, + scriptBgDispatcher + ).execute() + + val expectedResultList = listOf( + listOf( + "SF:scripts/java/com/example/TwoSum.kt", + "FN:7,com/example/TwoSum${'$'}Companion::sumNumbers (II)Ljava/lang/Object;", + "FN:3,com/example/TwoSum:: ()V", + "FNDA:1,com/example/TwoSum${'$'}Companion::sumNumbers (II)Ljava/lang/Object;", + "FNDA:0,com/example/TwoSum:: ()V", + "FNF:2", + "FNH:1", + "BRDA:7,0,0,1", + "BRDA:7,0,1,1", + "BRDA:7,0,2,1", + "BRDA:7,0,3,1", + "BRF:4", + "BRH:4", + "DA:3,0", + "DA:7,1", + "DA:8,1", + "DA:10,1", + "LH:3", + "LF:4", + "end_of_record" + ) + ) + + assertThat(result).isEqualTo(expectedResultList) + } + + @Test + fun testRunCoverage_appTests_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, + sourceSubpackage = "app/main/java/com/example", + testSubpackage = "app/test/java/com/example" + ) + + val result = RunCoverage( + "${tempFolder.root}", + "app/main/java/com/example/TwoSum.kt", + longCommandExecutor, + scriptBgDispatcher + ).execute() + + val expectedResultList = listOf( + listOf( + "SF:app/main/java/com/example/TwoSum.kt", + "FN:7,com/example/TwoSum${'$'}Companion::sumNumbers (II)Ljava/lang/Object;", + "FN:3,com/example/TwoSum:: ()V", + "FNDA:1,com/example/TwoSum${'$'}Companion::sumNumbers (II)Ljava/lang/Object;", + "FNDA:0,com/example/TwoSum:: ()V", + "FNF:2", + "FNH:1", + "BRDA:7,0,0,1", + "BRDA:7,0,1,1", + "BRDA:7,0,2,1", + "BRDA:7,0,3,1", + "BRF:4", + "BRH:4", + "DA:3,0", + "DA:7,1", + "DA:8,1", + "DA:10,1", + "LH:3", + "LF:4", + "end_of_record" + ) + ) + + assertThat(result).isEqualTo(expectedResultList) + } + + @Test + fun testRunCoverage_sharedAndLocalTests_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 testContentShared = + """ + 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() + + val testContentLocal = + """ + package com.example + + import org.junit.Assert.assertEquals + import org.junit.Test + + class TwoSumLocalTest { + + @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.addMultiLevelSourceAndTestFileWithContent( + filename = "TwoSum", + sourceContent = sourceContent, + testContentShared = testContentShared, + testContentLocal = testContentLocal, + subpackage = "app" + ) + + val result = RunCoverage( + "${tempFolder.root}", + "app/main/java/com/example/TwoSum.kt", + longCommandExecutor, + scriptBgDispatcher + ).execute() + + val expectedResultList = listOf( + listOf( + "SF:app/main/java/com/example/TwoSum.kt", + "FN:7,com/example/TwoSum${'$'}Companion::sumNumbers (II)Ljava/lang/Object;", + "FN:3,com/example/TwoSum:: ()V", + "FNDA:1,com/example/TwoSum${'$'}Companion::sumNumbers (II)Ljava/lang/Object;", + "FNDA:0,com/example/TwoSum:: ()V", + "FNF:2", + "FNH:1", + "BRDA:7,0,0,1", + "BRDA:7,0,1,1", + "BRDA:7,0,2,1", + "BRDA:7,0,3,1", + "BRF:4", + "BRH:4", + "DA:3,0", + "DA:7,1", + "DA:8,1", + "DA:10,1", + "LH:3", + "LF:4", + "end_of_record" + ), + listOf( + "SF:app/main/java/com/example/TwoSum.kt", + "FN:7,com/example/TwoSum${'$'}Companion::sumNumbers (II)Ljava/lang/Object;", + "FN:3,com/example/TwoSum:: ()V", + "FNDA:1,com/example/TwoSum${'$'}Companion::sumNumbers (II)Ljava/lang/Object;", + "FNDA:0,com/example/TwoSum:: ()V", + "FNF:2", + "FNH:1", + "BRDA:7,0,0,1", + "BRDA:7,0,1,1", + "BRDA:7,0,2,1", + "BRDA:7,0,3,1", + "BRF:4", + "BRH:4", + "DA:3,0", + "DA:7,1", + "DA:8,1", + "DA:10,1", + "LH:3", + "LF:4", + "end_of_record" + ) + ) + + assertThat(result).isEqualTo(expectedResultList) + } + + private fun initializeCommandExecutorWithLongProcessWaitTime(): CommandExecutorImpl { + return CommandExecutorImpl( + scriptBgDispatcher, processTimeout = 5, processTimeoutUnit = TimeUnit.MINUTES + ) + } +} diff --git a/scripts/src/javatests/org/oppia/android/scripts/testing/TestBazelWorkspaceTest.kt b/scripts/src/javatests/org/oppia/android/scripts/testing/TestBazelWorkspaceTest.kt index 9d6a33378d6..b52113e817d 100644 --- a/scripts/src/javatests/org/oppia/android/scripts/testing/TestBazelWorkspaceTest.kt +++ b/scripts/src/javatests/org/oppia/android/scripts/testing/TestBazelWorkspaceTest.kt @@ -282,7 +282,8 @@ class TestBazelWorkspaceTest { "Main", sourceContent, testContent, - "coverage" + sourceSubpackage = "coverage/main/java/com/example", + testSubpackage = "coverage/test/java/com/example" ) val sourceFile = File(tempFolder.root, "coverage/main/java/com/example/Main.kt") @@ -316,7 +317,8 @@ class TestBazelWorkspaceTest { "Main", sourceContent, testContent, - "coverage" + sourceSubpackage = "coverage/main/java/com/example", + testSubpackage = "coverage/test/java/com/example" ) val sourceBuildFile = File(tempFolder.root, "coverage/main/java/com/example/BUILD.bazel") @@ -338,7 +340,7 @@ class TestBazelWorkspaceTest { """ load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_test") kt_jvm_test( - name = "test", + name = "MainTest", srcs = ["MainTest.kt"], deps = [ "//coverage/main/java/com/example:main", @@ -351,6 +353,168 @@ class TestBazelWorkspaceTest { ) } + @Test + fun testAddMultiLevelSourceAndTestFileWithContent_createsSourceAndTestFiles() { + val testBazelWorkspace = TestBazelWorkspace(tempFolder) + val sourceContent = + """ + fun main() { + println("Hello, World!") + } + """ + + val testContentShared = + """ + import org.junit.Test + import kotlin.test.assertEquals + + class MainTest { + + @Test + fun testMain() { + assertEquals(1, 1) + } + } + """ + + val testContentLocal = + """ + import org.junit.Test + import kotlin.test.assertEquals + + class MainTestLocal { + + @Test + fun testMain() { + assertEquals(1, 2) + } + } + """ + + testBazelWorkspace.addMultiLevelSourceAndTestFileWithContent( + filename = "Main", + sourceContent = sourceContent, + testContentShared = testContentShared, + testContentLocal = testContentLocal, + subpackage = "coverage" + ) + + val sourceFile = File(tempFolder.root, "coverage/main/java/com/example/Main.kt") + val testFileShared = File(tempFolder.root, "coverage/sharedTest/java/com/example/MainTest.kt") + val testFileLocal = File(tempFolder.root, "coverage/test/java/com/example/MainLocalTest.kt") + + assertThat(sourceFile.exists()).isTrue() + assertThat(sourceFile.readText()).isEqualTo(sourceContent) + + assertThat(testFileShared.exists()).isTrue() + assertThat(testFileShared.readText()).isEqualTo(testContentShared) + + assertThat(testFileLocal.exists()).isTrue() + assertThat(testFileLocal.readText()).isEqualTo(testContentLocal) + } + + @Test + fun testAddMultiLevelSourceAndTestFileWithContent_updatesBuildFiles() { + val testBazelWorkspace = TestBazelWorkspace(tempFolder) + val sourceContent = + """ + fun main() { + println("Hello, World!") + } + """ + + val testContentShared = + """ + import org.junit.Test + import kotlin.test.assertEquals + + class MainTest { + + @Test + fun testMain() { + assertEquals(1, 1) + } + } + """ + + val testContentLocal = + """ + import org.junit.Test + import kotlin.test.assertEquals + + class MainTestLocal { + + @Test + fun testMain() { + assertEquals(1, 2) + } + } + """ + + testBazelWorkspace.addMultiLevelSourceAndTestFileWithContent( + filename = "Main", + sourceContent = sourceContent, + testContentShared = testContentShared, + testContentLocal = testContentLocal, + subpackage = "coverage" + ) + + val sourceBuildFile = File( + tempFolder.root, "coverage/main/java/com/example/BUILD.bazel" + ) + val testBuildFileShared = File( + tempFolder.root, "coverage/sharedTest/java/com/example/BUILD.bazel" + ) + val testBuildFileLocal = File( + tempFolder.root, "coverage/test/java/com/example/BUILD.bazel" + ) + + assertThat(sourceBuildFile.exists()).isTrue() + assertThat(sourceBuildFile.readText()).contains( + """ + kt_jvm_library( + name = "main", + srcs = ["Main.kt"], + visibility = ["//visibility:public"] + ) + """.trimIndent() + ) + + assertThat(testBuildFileShared.exists()).isTrue() + assertThat(testBuildFileShared.readText()).contains( + """ + load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_test") + kt_jvm_test( + name = "MainTest", + srcs = ["MainTest.kt"], + deps = [ + "//coverage/main/java/com/example:main", + "@maven//:junit_junit", + ], + visibility = ["//visibility:public"], + test_class = "com.example.MainTest", + ) + """.trimIndent() + ) + + assertThat(testBuildFileLocal.exists()).isTrue() + assertThat(testBuildFileLocal.readText()).contains( + """ + load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_test") + kt_jvm_test( + name = "MainLocalTest", + srcs = ["MainLocalTest.kt"], + deps = [ + "//coverage/main/java/com/example:main", + "@maven//:junit_junit", + ], + visibility = ["//visibility:public"], + test_class = "com.example.MainLocalTest", + ) + """.trimIndent() + ) + } + @Test fun testAddSourceContentAndBuildFile_createsSourceFileAndBuildFile() { val testBazelWorkspace = TestBazelWorkspace(tempFolder) @@ -406,7 +570,7 @@ class TestBazelWorkspaceTest { """ load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_test") kt_jvm_test( - name = "test", + name = "MainTest", srcs = ["MainTest.kt"], deps = [ "//coverage/main/java/com/example:main",