diff --git a/application/src/main/kotlin/application/BackwardCompatibilityCheckCommand.kt b/application/src/main/kotlin/application/BackwardCompatibilityCheckCommand.kt index 5d194a570..3268c626e 100644 --- a/application/src/main/kotlin/application/BackwardCompatibilityCheckCommand.kt +++ b/application/src/main/kotlin/application/BackwardCompatibilityCheckCommand.kt @@ -3,6 +3,7 @@ package application import `in`.specmatic.conversions.OpenApiSpecification import `in`.specmatic.core.CONTRACT_EXTENSIONS import `in`.specmatic.core.git.GitCommand +import `in`.specmatic.core.git.SystemGit import `in`.specmatic.core.testBackwardCompatibility import `in`.specmatic.core.utilities.exitWithMessage import org.springframework.stereotype.Component @@ -11,11 +12,13 @@ import java.io.File import java.util.concurrent.Callable @Component -@Command(name = "backwardCompatibilityCheck", - mixinStandardHelpOptions = true, - description = ["Checks backward compatibility of a directory across the current HEAD and the main branch"]) -class BackwardCompatibilityCheckCommand( - private val gitCommand: GitCommand, +@Command( + name = "backwardCompatibilityCheck", + mixinStandardHelpOptions = true, + description = ["Checks backward compatibility of a directory across the current HEAD and the main branch"] +) +open class BackwardCompatibilityCheckCommand( + private val gitCommand: GitCommand = SystemGit(), ) : Callable { private val newLine = System.lineSeparator() @@ -29,12 +32,13 @@ class BackwardCompatibilityCheckCommand( override fun call() { val filesChangedInCurrentBranch: Set = getOpenAPISpecFilesChangedInCurrentBranch() - if(filesChangedInCurrentBranch.isEmpty()) exitWithMessage("$newLine OpenAPI spec files were changed, skipping the check.$newLine") + if (filesChangedInCurrentBranch.isEmpty()) exitWithMessage("${newLine}No OpenAPI spec files were changed, skipping the check.$newLine") val filesReferringToChangedSchemaFiles = filesReferringToChangedSchemaFiles(filesChangedInCurrentBranch) val filesToCheck: Set = filesChangedInCurrentBranch + filesReferringToChangedSchemaFiles + logFilesToBeCheckedForBackwardCompatibility( filesChangedInCurrentBranch, filesReferringToChangedSchemaFiles @@ -42,50 +46,68 @@ class BackwardCompatibilityCheckCommand( val result = runBackwardCompatibilityCheckFor(filesToCheck) - if(result == FAILED) { + if (result == FAILED) { exitWithMessage("$newLine Verdict: FAIL, backward incompatible changes were found.") } println("$newLine Verdict: PASS, all changes were backward compatible") } private fun runBackwardCompatibilityCheckFor(files: Set): String { - val currentBranch = gitCommand.currentBranch() - val currentTreeish = if(currentBranch == HEAD) gitCommand.detachedHEAD() else currentBranch + val branchWithChanges = gitCommand.currentBranch() + val treeishWithChanges = if (branchWithChanges == HEAD) gitCommand.detachedHEAD() else branchWithChanges try { val failures = files.mapIndexed { index, specFilePath -> - println("${index.inc()}. Running the check for $specFilePath:") - - // newer => the file with changes on the branch - val newer = OpenApiSpecification.fromFile(specFilePath).toFeature() - - val olderFile = gitCommand.getFileInTheDefaultBranch(specFilePath, currentTreeish) - if (olderFile == null) { - println("$specFilePath is a new file.$newLine") - return@mapIndexed SUCCESS - } - // older => the same file on the default (e.g. main) branch - val older = OpenApiSpecification.fromFile(olderFile.path).toFeature() - - val backwardCompatibilityResult = testBackwardCompatibility(older, newer) - - if (backwardCompatibilityResult.success()) { - println("$newLine The file $specFilePath is backward compatible.$newLine".prependIndent(MARGIN_SPACE)) - SUCCESS - } else { - println("$newLine ${backwardCompatibilityResult.report().prependIndent(MARGIN_SPACE)}") - println("$newLine *** The file $specFilePath is NOT backward compatible. ***$newLine".prependIndent(MARGIN_SPACE)) - FAILED + try { + println("${index.inc()}. Running the check for $specFilePath:") + + // newer => the file with changes on the branch + val newer = OpenApiSpecification.fromFile(specFilePath).toFeature() + + val olderFile = gitCommand.getFileInTheDefaultBranch(specFilePath, treeishWithChanges) + if (olderFile == null) { + println("$specFilePath is a new file.$newLine") + return@mapIndexed SUCCESS + } + + gitCommand.checkout(gitCommand.defaultBranch()) + + // older => the same file on the default (e.g. main) branch + val older = OpenApiSpecification.fromFile(olderFile.path).toFeature() + + val backwardCompatibilityResult = testBackwardCompatibility(older, newer) + + if (backwardCompatibilityResult.success()) { + println( + "$newLine The file $specFilePath is backward compatible.$newLine".prependIndent( + MARGIN_SPACE + ) + ) + SUCCESS + } else { + println("$newLine ${backwardCompatibilityResult.report().prependIndent(MARGIN_SPACE)}") + println( + "$newLine *** The file $specFilePath is NOT backward compatible. ***$newLine".prependIndent( + MARGIN_SPACE + ) + ) + FAILED + } + } finally { + gitCommand.checkout(treeishWithChanges) } }.filter { it == FAILED } return if (failures.isNotEmpty()) FAILED else SUCCESS } finally { - gitCommand.checkout(currentTreeish) + gitCommand.checkout(treeishWithChanges) } } - private fun logFilesToBeCheckedForBackwardCompatibility(changedFiles : Set, filesReferringToChangedFiles: Set) { + private fun logFilesToBeCheckedForBackwardCompatibility( + changedFiles: Set, + filesReferringToChangedFiles: Set + ) { println("Checking backward compatibility of the following files: $newLine") println("Files that have changed - ") changedFiles.forEach { println(it) } @@ -98,18 +120,22 @@ class BackwardCompatibilityCheckCommand( println() } - private fun filesReferringToChangedSchemaFiles(schemaFiles : Set): Set { - if(schemaFiles.isEmpty()) return emptySet() + internal fun filesReferringToChangedSchemaFiles(schemaFiles: Set): Set { + if (schemaFiles.isEmpty()) return emptySet() val schemaFileBaseNames = schemaFiles.map { File(it).name } - return allOpenApiSpecFiles().filter { + val result = allOpenApiSpecFiles().filter { it.readText().let { specContent -> schemaFileBaseNames.any { schemaFileBaseName -> schemaFileBaseName in specContent } } }.map { it.path }.toSet() + + return result.flatMap { + filesReferringToChangedSchemaFiles(setOf(it)).ifEmpty { setOf(it) } + }.toSet() } - private fun allOpenApiSpecFiles(): List { + internal fun allOpenApiSpecFiles(): List { return File(".").walk().toList().filterNot { ".git" in it.path }.filter { it.isFile && it.isOpenApiSpec() } @@ -117,12 +143,12 @@ class BackwardCompatibilityCheckCommand( private fun getOpenAPISpecFilesChangedInCurrentBranch(): Set { return gitCommand.getFilesChangeInCurrentBranch().filter { - File(it).exists() && File(it).isOpenApiSpec() + File(it).exists() && File(it).isOpenApiSpec() }.toSet() } private fun File.isOpenApiSpec(): Boolean { - if(this.extension !in CONTRACT_EXTENSIONS) return false + if (this.extension !in CONTRACT_EXTENSIONS) return false return OpenApiSpecification.isParsable(this.path) } } diff --git a/application/src/test/kotlin/application/BackwardCompatibilityCheckCommandTest.kt b/application/src/test/kotlin/application/BackwardCompatibilityCheckCommandTest.kt new file mode 100644 index 000000000..71bc9b104 --- /dev/null +++ b/application/src/test/kotlin/application/BackwardCompatibilityCheckCommandTest.kt @@ -0,0 +1,49 @@ +package application + +import io.mockk.every +import io.mockk.mockk +import io.mockk.spyk +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import java.io.File + +class BackwardCompatibilityCheckCommandTest { + + @Test + fun `filesReferringToChangedSchemaFiles returns empty set when input is empty`() { + val command = BackwardCompatibilityCheckCommand(mockk()) + val result = command.filesReferringToChangedSchemaFiles(emptySet()) + assertTrue(result.isEmpty()) + } + + @Test + fun `filesReferringToChangedSchemaFiles returns empty set when no files refer to changed schema files`() { + val command = spyk() + every { command.allOpenApiSpecFiles() } returns listOf( + File("file1.yaml").apply { writeText("content1") }, + File("file2.yaml").apply { writeText("content2") } + ) + val result = command.filesReferringToChangedSchemaFiles(setOf("file3.yaml")) + assertTrue(result.isEmpty()) + } + + @Test + fun `filesReferringToChangedSchemaFiles returns set of files that refer to changed schema files`() { + val command = spyk() + every { command.allOpenApiSpecFiles() } returns listOf( + File("file1.yaml").apply { writeText("file3.yaml") }, + File("file2.yaml").apply { writeText("file4.yaml") } + ) + val result = command.filesReferringToChangedSchemaFiles(setOf("file3.yaml")) + assertEquals(setOf("file1.yaml"), result) + } + + @AfterEach + fun `cleanup files`() { + listOf(File("file1.yaml"), File("file2.yaml")).forEach { + it.delete() + } + } +} \ No newline at end of file diff --git a/application/src/test/kotlin/application/CentralContractRepoReportCommandTestE2E.kt b/application/src/test/kotlin/application/CentralContractRepoReportCommandTestE2E.kt index 978368d7a..cfac1918f 100644 --- a/application/src/test/kotlin/application/CentralContractRepoReportCommandTestE2E.kt +++ b/application/src/test/kotlin/application/CentralContractRepoReportCommandTestE2E.kt @@ -23,6 +23,7 @@ class CentralContractRepoReportCommandTestE2E { @Test fun `test generates report json file`() { + centralContractRepoReportCommand.baseDir = "" centralContractRepoReportCommand.call() val reportJson: CentralContractRepoReportJson = Json.decodeFromString(reportFile.readText()) diff --git a/core/src/main/kotlin/in/specmatic/reports/CentralContractRepoReport.kt b/core/src/main/kotlin/in/specmatic/reports/CentralContractRepoReport.kt index d07efc52c..14c3da330 100644 --- a/core/src/main/kotlin/in/specmatic/reports/CentralContractRepoReport.kt +++ b/core/src/main/kotlin/in/specmatic/reports/CentralContractRepoReport.kt @@ -10,7 +10,7 @@ import java.io.File class CentralContractRepoReport { fun generate(currentWorkingDir: String = ""): CentralContractRepoReportJson { - val searchPath = currentWorkingDir.takeIf { it.isNotEmpty() }.let { File(it).canonicalPath } ?: File("").canonicalPath + val searchPath = File(currentWorkingDir).canonicalPath logger.log("Searching for specification files at: $searchPath") val specifications = findSpecifications(searchPath) return CentralContractRepoReportJson(getSpecificationRows(specifications.sorted(), searchPath))