Skip to content

Commit

Permalink
Merge pull request #1131 from znsio/recursive_backward_compatibility_…
Browse files Browse the repository at this point in the history
…check

Recursive backward compatibility check
  • Loading branch information
joelrosario authored May 29, 2024
2 parents e03c262 + 7c94aab commit 0ef3f64
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<Unit> {

private val newLine = System.lineSeparator()
Expand All @@ -29,63 +32,82 @@ class BackwardCompatibilityCheckCommand(

override fun call() {
val filesChangedInCurrentBranch: Set<String> = 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<String> = filesChangedInCurrentBranch + filesReferringToChangedSchemaFiles


logFilesToBeCheckedForBackwardCompatibility(
filesChangedInCurrentBranch,
filesReferringToChangedSchemaFiles
)

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>): 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<String>, filesReferringToChangedFiles: Set<String>) {
private fun logFilesToBeCheckedForBackwardCompatibility(
changedFiles: Set<String>,
filesReferringToChangedFiles: Set<String>
) {
println("Checking backward compatibility of the following files: $newLine")
println("Files that have changed - ")
changedFiles.forEach { println(it) }
Expand All @@ -98,31 +120,35 @@ class BackwardCompatibilityCheckCommand(
println()
}

private fun filesReferringToChangedSchemaFiles(schemaFiles : Set<String>): Set<String> {
if(schemaFiles.isEmpty()) return emptySet()
internal fun filesReferringToChangedSchemaFiles(schemaFiles: Set<String>): Set<String> {
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<File> {
internal fun allOpenApiSpecFiles(): List<File> {
return File(".").walk().toList().filterNot {
".git" in it.path
}.filter { it.isFile && it.isOpenApiSpec() }
}

private fun getOpenAPISpecFilesChangedInCurrentBranch(): Set<String> {
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)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<BackwardCompatibilityCheckCommand>()
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<BackwardCompatibilityCheckCommand>()
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()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class CentralContractRepoReportCommandTestE2E {

@Test
fun `test generates report json file`() {
centralContractRepoReportCommand.baseDir = ""
centralContractRepoReportCommand.call()
val reportJson: CentralContractRepoReportJson = Json.decodeFromString(reportFile.readText())

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down

0 comments on commit 0ef3f64

Please sign in to comment.