Skip to content

Commit

Permalink
Bring back the existing backwardCompatibilityCheck command
Browse files Browse the repository at this point in the history
Add deprecation notice to all the b/w compatibility related commands which will be eventually removed
  • Loading branch information
yogeshnikam671 committed Aug 27, 2024
1 parent b3f419f commit 59887e6
Show file tree
Hide file tree
Showing 9 changed files with 400 additions and 17 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,329 @@
package application

import application.BackwardCompatibilityCheckCommand.CompatibilityResult.*
import io.specmatic.conversions.OpenApiSpecification
import io.specmatic.core.*
import io.specmatic.core.git.GitCommand
import io.specmatic.core.git.SystemGit
import io.specmatic.core.log.logger
import io.specmatic.core.utilities.exitWithMessage
import io.specmatic.stub.isOpenAPI
import org.springframework.stereotype.Component
import picocli.CommandLine.Command
import java.io.File
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.util.concurrent.Callable
import java.util.regex.Pattern
import kotlin.io.path.extension
import kotlin.io.path.pathString
import kotlin.system.exitProcess

const val ONE_INDENT = " "
const val TWO_INDENTS = "${ONE_INDENT}${ONE_INDENT}"

@Component
@Command(
name = "backwardCompatibilityCheck",
mixinStandardHelpOptions = true,
description = [
"""
Checks backward compatibility of a directory across the current HEAD and the main branch.
DEPRECATED: This command will be removed in the next major release. Use 'backward-compatibility-check' command instead.
"""
]
)
class BackwardCompatibilityCheckCommand(
private val gitCommand: GitCommand = SystemGit(),
) : Callable<Unit> {

private val newLine = System.lineSeparator()

companion object {
private const val HEAD = "HEAD"
private const val MARGIN_SPACE = " "
}

override fun call() {
val filesChangedInCurrentBranch: Set<String> = getOpenAPISpecFilesChangedInCurrentBranch()

if (filesChangedInCurrentBranch.isEmpty()) exitWithMessage("${newLine}No OpenAPI spec files were changed, skipping the check.$newLine")

val filesReferringToChangedSchemaFiles = filesReferringToChangedSchemaFiles(filesChangedInCurrentBranch)

val specificationsOfChangedExternalisedExamples: Set<String> = getSpecificationsOfChangedExternalisedExamples(filesChangedInCurrentBranch)

logFilesToBeCheckedForBackwardCompatibility(
filesChangedInCurrentBranch,
filesReferringToChangedSchemaFiles,
specificationsOfChangedExternalisedExamples
)

val specificationsToCheck: Set<String> = filesChangedInCurrentBranch + filesReferringToChangedSchemaFiles + specificationsOfChangedExternalisedExamples

val result = try {
runBackwardCompatibilityCheckFor(specificationsToCheck)
} catch(e: Throwable) {
logger.newLine()
logger.newLine()
logger.log(e)
exitProcess(1)
}

println()
println(result.report)
exitProcess(result.exitCode)
}

private fun getSpecificationsOfChangedExternalisedExamples(filesChangedInCurrentBranch: Set<String>): Set<String> {
data class CollectedFiles(
val specifications: MutableSet<String> = mutableSetOf(),
val examplesMissingSpecifications: MutableList<String> = mutableListOf(),
val ignoredFiles: MutableList<String> = mutableListOf()
)

val collectedFiles = filesChangedInCurrentBranch.fold(CollectedFiles()) { acc, filePath ->
val path = Paths.get(filePath)
val examplesDir = path.find { it.toString().endsWith("_examples") || it.toString().endsWith("_tests") }

if (examplesDir == null) {
acc.ignoredFiles.add(filePath)
} else {
val parentPath = examplesDir.parent
val strippedPath = parentPath.resolve(examplesDir.fileName.toString().removeSuffix("_examples"))
val specFiles = findSpecFiles(strippedPath)

if (specFiles.isNotEmpty()) {
acc.specifications.addAll(specFiles.map { it.toString() })
} else {
acc.examplesMissingSpecifications.add(filePath)
}
}
acc
}

val result = collectedFiles.specifications.toMutableSet()

collectedFiles.examplesMissingSpecifications.forEach { filePath ->
val path = Paths.get(filePath)
val examplesDir = path.find { it.toString().endsWith("_examples") || it.toString().endsWith("_tests") }
if (examplesDir != null) {
val parentPath = examplesDir.parent
val strippedPath = parentPath.resolve(examplesDir.fileName.toString().removeSuffix("_examples"))
val specFiles = findSpecFiles(strippedPath)
if (specFiles.isNotEmpty()) {
result.addAll(specFiles.map { it.toString() })
} else {
result.add("${strippedPath}.yaml")
}
}
}

return result
}

private fun Path.find(predicate: (Path) -> Boolean): Path? {
var current: Path? = this
while (current != null) {
if (predicate(current)) {
return current
}
current = current.parent
}
return null
}

private fun findSpecFiles(path: Path): List<Path> {
val extensions = CONTRACT_EXTENSIONS
return extensions.map { path.resolveSibling(path.fileName.toString() + it) }
.filter { Files.exists(it) && (isOpenAPI(it.pathString) || it.extension in listOf(WSDL, CONTRACT_EXTENSION)) }
}

private fun runBackwardCompatibilityCheckFor(files: Set<String>): CompatibilityReport {
val branchWithChanges = gitCommand.currentBranch()
val treeishWithChanges = if (branchWithChanges == HEAD) gitCommand.detachedHEAD() else branchWithChanges

try {
val results = files.mapIndexed { index, specFilePath ->
try {
println("${index.inc()}. Running the check for $specFilePath:")

// newer => the file with changes on the branch
val (newer, unusedExamples) = OpenApiSpecification.fromFile(specFilePath).toFeature().loadExternalisedExamplesAndListUnloadableExamples()

val olderFile = gitCommand.getFileInTheBaseBranch(
specFilePath,
treeishWithChanges,
gitCommand.defaultBranch()
)
if (olderFile == null) {
println("$specFilePath is a new file.$newLine")
return@mapIndexed PASSED
}

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
)
)

println()

var errorsFound = false

if(!examplesAreValid(newer, "newer")) {
println(
"$newLine *** Examples in $specFilePath are not valid. ***$newLine".prependIndent(
MARGIN_SPACE
)
)

println()

errorsFound = true
}

if(unusedExamples.isNotEmpty()) {
println(
"$newLine *** Some examples for $specFilePath could not be loaded. ***$newLine".prependIndent(
MARGIN_SPACE
)
)

println()

errorsFound = true

}

if(errorsFound) {
FAILED
}
else
PASSED
} else {
println("$newLine ${backwardCompatibilityResult.report().prependIndent(MARGIN_SPACE)}")
println(
"$newLine *** The file $specFilePath is NOT backward compatible. ***$newLine".prependIndent(
MARGIN_SPACE
)
)

println()

FAILED
}
} finally {
gitCommand.checkout(treeishWithChanges)
}
}

return CompatibilityReport(results)
} finally {
gitCommand.checkout(treeishWithChanges)
}
}

private fun examplesAreValid(feature: Feature, which: String): Boolean {
return try {
feature.validateExamplesOrException()
true
} catch (t: Throwable) {
println()
false
}
}

private fun logFilesToBeCheckedForBackwardCompatibility(
changedFiles: Set<String>,
filesReferringToChangedFiles: Set<String>,
specificationsOfChangedExternalisedExamples: Set<String>
) {

println("Checking backward compatibility of the following files: $newLine")
println("${ONE_INDENT}Files that have changed:")
changedFiles.forEach { println(it.prependIndent(TWO_INDENTS)) }
println()

if(filesReferringToChangedFiles.isNotEmpty()) {
println("${ONE_INDENT}Files referring to the changed files - ")
filesReferringToChangedFiles.forEach { println(it.prependIndent(TWO_INDENTS)) }
println()
}

if(specificationsOfChangedExternalisedExamples.isNotEmpty()) {
println("${ONE_INDENT}Specifications whose externalised examples were changed - ")
filesReferringToChangedFiles.forEach { println(it.prependIndent(TWO_INDENTS)) }
println()
}

println("-".repeat(20))
println()
}

internal fun filesReferringToChangedSchemaFiles(inputFiles: Set<String>): Set<String> {
if (inputFiles.isEmpty()) return emptySet()

val inputFileNames = inputFiles.map { File(it).name }
val result = allOpenApiSpecFiles().filter {
it.readText().trim().let { specContent ->
inputFileNames.any { inputFileName ->
val pattern = Pattern.compile("\\b$inputFileName\\b")
val matcher = pattern.matcher(specContent)
matcher.find()
}
}
}.map { it.path }.toSet()

return result.flatMap {
filesReferringToChangedSchemaFiles(setOf(it)).ifEmpty { setOf(it) }
}.toSet()
}

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.getFilesChangedInCurrentBranch(
gitCommand.defaultBranch()
).filter {
File(it).exists() && File(it).isOpenApiSpec()
}.toSet()
}

private fun File.isOpenApiSpec(): Boolean {
if (this.extension !in CONTRACT_EXTENSIONS) return false
return OpenApiSpecification.isParsable(this.path)
}

class CompatibilityReport(results: List<CompatibilityResult>) {
val report: String
val exitCode: Int

init {
val failed: Boolean = results.any { it == FAILED }
val failedCount = results.count { it == FAILED }
val passedCount = results.count { it == PASSED }

report = "Files checked: ${results.size} (Passed: ${passedCount}, Failed: $failedCount)"
exitCode = if(failed) 1 else 0
}

}

enum class CompatibilityResult {
PASSED, FAILED
}
}
7 changes: 6 additions & 1 deletion application/src/main/kotlin/application/CompareCommand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ import kotlin.system.exitProcess

@Command(name = "compare",
mixinStandardHelpOptions = true,
description = ["Checks if two contracts are equivalent"])
description = [
"""
Checks if two contracts are equivalent.
DEPRECATED: This command will be removed in the next major release. Use 'backward-compatibility-check' command instead.
"""
])
class CompareCommand : Callable<Unit> {
@Parameters(index = "0", description = ["Older contract file path"])
lateinit var olderContractFilePath: String
Expand Down
7 changes: 6 additions & 1 deletion application/src/main/kotlin/application/CompatibleCommand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,12 @@ class GitCompatibleCommand : Callable<Int> {

@Command(name = "compatible",
mixinStandardHelpOptions = true,
description = ["Checks if the newer contract is backward compatible with the older one"],
description = [
"""
Checks if the newer contract is backward compatible with the older one
DEPRECATED: This command will be removed in the next major release. Use 'backward-compatibility-check' command instead.
"""
],
subcommands = [ GitCompatibleCommand::class ])
internal class CompatibleCommand : Callable<Unit> {
override fun call() {
Expand Down
8 changes: 7 additions & 1 deletion application/src/main/kotlin/application/DifferenceCommand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@ import kotlin.system.exitProcess

@Command(name = "similar",
mixinStandardHelpOptions = true,
description = ["Show the difference between two contracts"])
description = [
"""
Show the difference between two contracts.
DEPRECATED: This command will be removed in the next major release. Use 'backward-compatibility-check' command instead.
"""
]
)
class DifferenceCommand : Callable<Unit> {
@Parameters(index = "0", description = ["Older contract file path"])
lateinit var olderContractFilePath: String
Expand Down
13 changes: 11 additions & 2 deletions application/src/main/kotlin/application/PushCommand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,16 @@ import kotlin.system.exitProcess

private const val pipelineKeyInSpecmaticConfig = "pipeline"

@CommandLine.Command(name = "push", description = ["Check the new contract for backward compatibility with the specified version, then overwrite the old one with it."], mixinStandardHelpOptions = true)
@CommandLine.Command(
name = "push",
description = [
"""
Check the new contract for backward compatibility with the specified version, then overwrite the old one with it.
DEPRECATED: This command will be removed in the next major release. Use 'backward-compatibility-check' command instead.
"""
],
mixinStandardHelpOptions = true
)
class PushCommand: Callable<Unit> {
override fun call() {
val userHome = File(System.getProperty("user.home"))
Expand Down Expand Up @@ -160,4 +169,4 @@ fun registerPipelineCredentials(manifestData: JSONObjectValue, contractPath: Str
sourceGit.add()
}
}
}
}
Loading

0 comments on commit 59887e6

Please sign in to comment.