From e15760eeaceef6ea45421ae283007bb2838253e5 Mon Sep 17 00:00:00 2001 From: Andrey Shcheglov Date: Wed, 19 Jul 2023 16:39:09 +0300 Subject: [PATCH] Add support for JSON serialization via Jackson --- README.md | 9 +- buildSrc/build.gradle.kts | 5 + gradle/libs.versions.toml | 3 + kompiledb-jackson/.gitignore | 5 + kompiledb-jackson/build.gradle.kts | 24 ++ .../kompiledb/core/JsonIoExtensionsJackson.kt | 27 ++ .../jackson/CompilationCommandDeserializer.kt | 65 +++++ .../CompilationDatabaseDeserializer.kt | 15 ++ .../kompiledb/jackson/EnvPathDeserializer.kt | 12 + .../kompiledb/jackson/JacksonIo.kt | 82 ++++++ .../jackson/CompilationCommandJacksonTest.kt | 250 ++++++++++++++++++ .../jackson/CompilationDatabaseJacksonTest.kt | 185 +++++++++++++ settings.gradle.kts | 1 + 13 files changed, 679 insertions(+), 4 deletions(-) create mode 100644 kompiledb-jackson/.gitignore create mode 100644 kompiledb-jackson/build.gradle.kts create mode 100644 kompiledb-jackson/src/main/kotlin/com/saveourtool/kompiledb/core/JsonIoExtensionsJackson.kt create mode 100644 kompiledb-jackson/src/main/kotlin/com/saveourtool/kompiledb/jackson/CompilationCommandDeserializer.kt create mode 100644 kompiledb-jackson/src/main/kotlin/com/saveourtool/kompiledb/jackson/CompilationDatabaseDeserializer.kt create mode 100644 kompiledb-jackson/src/main/kotlin/com/saveourtool/kompiledb/jackson/EnvPathDeserializer.kt create mode 100644 kompiledb-jackson/src/main/kotlin/com/saveourtool/kompiledb/jackson/JacksonIo.kt create mode 100644 kompiledb-jackson/src/test/kotlin/com/saveourtool/kompiledb/jackson/CompilationCommandJacksonTest.kt create mode 100644 kompiledb-jackson/src/test/kotlin/com/saveourtool/kompiledb/jackson/CompilationDatabaseJacksonTest.kt diff --git a/README.md b/README.md index d1fe039..fa2d2b4 100644 --- a/README.md +++ b/README.md @@ -17,10 +17,11 @@ See the [project website](https://saveourtool.github.io/kompiledb/) for document - Compatible with Java 8. - Can read and write `compile_commands.json`. - - No dependencies (except for [google/gson](https://github.com/google/gson)). + - No dependencies (except for [google/gson](https://github.com/google/gson) or + [FasterXML/jackson](https://github.com/FasterXML/jackson)). The core library is JSON engine agnostic, so it's easy to add support for a - different JSON back-end, such as [FasterXML/jackson](https://github.com/FasterXML/jackson) - or [Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization). + different JSON back-end, such as + [Kotlin/kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization). - Support for pluggable path translation between the local and the build environments (such as [_Cygwin_](https://www.cygwin.com) or [_WSL_](https://github.com/Microsoft/WSL)). @@ -82,7 +83,7 @@ Then add the dependency as usual: ```kotlin dependencies { - implementation("com.saveourtool.kompiledb:kompiledb-gson:1.0.0") + implementation("com.saveourtool.kompiledb:kompiledb-gson:1.0.1") } ``` diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 85bbb60..6905134 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -16,6 +16,11 @@ dependencies { implementation(libs.nexus.publish.gradle.plugin) implementation(libs.reckon.gradle.plugin) + constraints { + implementation(libs.jackson.databind) + implementation(libs.jackson.module.kotlin) + } + /* * Workaround for https://github.com/gradle/gradle/issues/15383: * Make version catalogs accessible from precompiled script plugins. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 15c0428..67c38ef 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,6 @@ [versions] gson = "2.10.1" +jackson = "2.15.2" kotlin = "1.9.0" kotest = "5.6.2" dokka = "1.8.20" @@ -11,6 +12,8 @@ reckon-gradle-plugin = "0.13.2" [libraries] google-gson = { module = "com.google.code.gson:gson", version.ref = "gson" } +jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson"} +jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson"} gradle-kotlin-dsl-plugins = { module = "org.gradle.kotlin:gradle-kotlin-dsl-plugins", version = "4.0.15" } kotest-assertions-core = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest" } kotest-assertions-json = { module = "io.kotest:kotest-assertions-json", version.ref = "kotest" } diff --git a/kompiledb-jackson/.gitignore b/kompiledb-jackson/.gitignore new file mode 100644 index 0000000..8cc8211 --- /dev/null +++ b/kompiledb-jackson/.gitignore @@ -0,0 +1,5 @@ +/.classpath +/.project +/.settings/ +/build/ +/target/ diff --git a/kompiledb-jackson/build.gradle.kts b/kompiledb-jackson/build.gradle.kts new file mode 100644 index 0000000..963dc1b --- /dev/null +++ b/kompiledb-jackson/build.gradle.kts @@ -0,0 +1,24 @@ +plugins { + id("com.saveourtool.kompiledb.maven-repo-configuration") + id("com.saveourtool.kompiledb.kotlin-configuration") + id("com.saveourtool.kompiledb.testing-configuration") + id("com.saveourtool.kompiledb.publishing-configuration") +} + +dependencies { + api(project(":kompiledb-core")) + implementation(libs.jackson.module.kotlin) + + constraints { + implementation(libs.jackson.databind) + } + + testImplementation(testFixtures(project(":kompiledb-core"))) +} + +tasks.withType { + filter { + includeTestsMatching("com.saveourtool.kompiledb.jackson.*") + isFailOnNoMatchingTests = true + } +} diff --git a/kompiledb-jackson/src/main/kotlin/com/saveourtool/kompiledb/core/JsonIoExtensionsJackson.kt b/kompiledb-jackson/src/main/kotlin/com/saveourtool/kompiledb/core/JsonIoExtensionsJackson.kt new file mode 100644 index 0000000..627d381 --- /dev/null +++ b/kompiledb-jackson/src/main/kotlin/com/saveourtool/kompiledb/core/JsonIoExtensionsJackson.kt @@ -0,0 +1,27 @@ +@file:JvmName("JsonIoExtensionsJackson") + +package com.saveourtool.kompiledb.core + +import com.saveourtool.kompiledb.core.JsonIo.Factory +import com.saveourtool.kompiledb.jackson.JacksonIo +import com.fasterxml.jackson.databind.json.JsonMapper +import com.fasterxml.jackson.databind.json.JsonMapper.Builder + +/** + * Creates a [JsonIo] instance which uses _Jackson_ to read and write JSON. + */ +val Factory.jackson: JsonIo + get() = + jackson() + +/** + * Creates a [JsonIo] instance which uses _Jackson_ to read and write JSON. + * + * @param initBuilder the optional configuration for [Builder]. + * @param initMapper the optional configuration for [JsonMapper]. + */ +fun Factory.jackson( + initBuilder: Builder.() -> Unit = {}, + initMapper: JsonMapper.() -> Unit = {}, +): JsonIo = + JacksonIo(initBuilder, initMapper) diff --git a/kompiledb-jackson/src/main/kotlin/com/saveourtool/kompiledb/jackson/CompilationCommandDeserializer.kt b/kompiledb-jackson/src/main/kotlin/com/saveourtool/kompiledb/jackson/CompilationCommandDeserializer.kt new file mode 100644 index 0000000..5453cdb --- /dev/null +++ b/kompiledb-jackson/src/main/kotlin/com/saveourtool/kompiledb/jackson/CompilationCommandDeserializer.kt @@ -0,0 +1,65 @@ +package com.saveourtool.kompiledb.jackson + +import com.saveourtool.kompiledb.core.CompilationCommand +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JsonDeserializer +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.exc.MismatchedInputException +import com.fasterxml.jackson.databind.node.ArrayNode +import com.fasterxml.jackson.databind.node.ObjectNode +import com.fasterxml.jackson.databind.node.TextNode +import kotlin.collections.Map.Entry + +internal object CompilationCommandDeserializer : JsonDeserializer() { + private const val DIRECTORY = "directory" + + private const val FILE = "file" + + private const val ARGUMENTS = "arguments" + + private const val COMMAND = "command" + + private const val OUTPUT = "output" + + private val FIELDS: Array = arrayOf( + DIRECTORY, + FILE, + ARGUMENTS, + COMMAND, + OUTPUT, + ) + + override fun deserialize( + p: JsonParser, + ctxt: DeserializationContext, + ): CompilationCommand { + val json = p.readValueAsTree() + + val props: Set> = json.properties() + + val unexpectedFields = props.asSequence().map(Entry::key).toSet() - FIELDS + return when { + unexpectedFields.isEmpty() -> { + val directory: TextNode = json[DIRECTORY] as TextNode? ?: throwMismatchedInput(p, DIRECTORY) + val file: TextNode = json[FILE] as TextNode? ?: throwMismatchedInput(p, FILE) + val arguments: ArrayNode? = json[ARGUMENTS] as ArrayNode? + val command: TextNode? = json[COMMAND] as TextNode? + val output: TextNode? = json[OUTPUT] as TextNode? + TODO("Not implemented") + } + + else -> throwMismatchedInput( + p, + "${unexpectedFields.size} unexpected field(s) encountered (${unexpectedFields.joinToString()})", + ) + } + } + + private fun throwMismatchedInput(p: JsonParser, message: String): Nothing = + throw MismatchedInputException.from( + p, + CompilationCommand::class.java, + message, + ) +} diff --git a/kompiledb-jackson/src/main/kotlin/com/saveourtool/kompiledb/jackson/CompilationDatabaseDeserializer.kt b/kompiledb-jackson/src/main/kotlin/com/saveourtool/kompiledb/jackson/CompilationDatabaseDeserializer.kt new file mode 100644 index 0000000..7aca72d --- /dev/null +++ b/kompiledb-jackson/src/main/kotlin/com/saveourtool/kompiledb/jackson/CompilationDatabaseDeserializer.kt @@ -0,0 +1,15 @@ +package com.saveourtool.kompiledb.jackson + +import com.saveourtool.kompiledb.core.CompilationDatabase +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JsonDeserializer + +internal object CompilationDatabaseDeserializer : JsonDeserializer() { + override fun deserialize( + p: JsonParser, + ctxt: DeserializationContext, + ): CompilationDatabase { + TODO("") + } +} diff --git a/kompiledb-jackson/src/main/kotlin/com/saveourtool/kompiledb/jackson/EnvPathDeserializer.kt b/kompiledb-jackson/src/main/kotlin/com/saveourtool/kompiledb/jackson/EnvPathDeserializer.kt new file mode 100644 index 0000000..c1884bc --- /dev/null +++ b/kompiledb-jackson/src/main/kotlin/com/saveourtool/kompiledb/jackson/EnvPathDeserializer.kt @@ -0,0 +1,12 @@ +package com.saveourtool.kompiledb.jackson + +import com.saveourtool.kompiledb.core.EnvPath +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JsonDeserializer +import com.fasterxml.jackson.databind.node.TextNode + +internal object EnvPathDeserializer : JsonDeserializer() { + override fun deserialize(p: JsonParser, ctxt: DeserializationContext): EnvPath = + p.readValueAsTree().textValue().let(::EnvPath) +} diff --git a/kompiledb-jackson/src/main/kotlin/com/saveourtool/kompiledb/jackson/JacksonIo.kt b/kompiledb-jackson/src/main/kotlin/com/saveourtool/kompiledb/jackson/JacksonIo.kt new file mode 100644 index 0000000..04b692d --- /dev/null +++ b/kompiledb-jackson/src/main/kotlin/com/saveourtool/kompiledb/jackson/JacksonIo.kt @@ -0,0 +1,82 @@ +package com.saveourtool.kompiledb.jackson + +import com.saveourtool.kompiledb.core.CompilationCommand +import com.saveourtool.kompiledb.core.CompilationDatabase +import com.saveourtool.kompiledb.core.CompilationDatabase.Companion.COMPILE_COMMANDS_JSON +import com.saveourtool.kompiledb.core.EnvPath +import com.saveourtool.kompiledb.core.JsonIo +import com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL +import com.fasterxml.jackson.core.JacksonException +import com.fasterxml.jackson.databind.json.JsonMapper +import com.fasterxml.jackson.databind.json.JsonMapper.Builder +import com.fasterxml.jackson.module.kotlin.KotlinFeature.StrictNullChecks +import com.fasterxml.jackson.module.kotlin.addDeserializer +import com.fasterxml.jackson.module.kotlin.jsonMapper +import com.fasterxml.jackson.module.kotlin.kotlinModule +import com.fasterxml.jackson.module.kotlin.readValue +import java.io.IOException +import java.io.Reader +import java.nio.charset.Charset +import java.nio.file.Path +import kotlin.Result.Companion.failure +import kotlin.Result.Companion.success +import kotlin.io.path.bufferedReader +import kotlin.io.path.div +import kotlin.io.path.isDirectory +import kotlin.io.path.name + +internal class JacksonIo( + initBuilder: Builder.() -> Unit, + initMapper: JsonMapper.() -> Unit, +) : JsonIo { + private val mapper = jsonMapper { + initBuilder() + + kotlinModule { + enable(StrictNullChecks) + } + .addDeserializer(EnvPath::class, EnvPathDeserializer) + .addDeserializer(CompilationCommand::class, CompilationCommandDeserializer) + .addDeserializer(CompilationDatabase::class, CompilationDatabaseDeserializer) + .let(this::addModule) + } + .apply(initMapper) + .setSerializationInclusion(NON_NULL) + + override fun CompilationDatabase.toJson(): String = + mapper.writeValueAsString(this) + + override fun CompilationCommand.toJson(): String = + mapper.writeValueAsString(this) + + override fun String.toCompilationCommand(): Result = + runCatching { + mapper.readValue(this) + } + + override fun String.toCompilationDatabase(): Result = + runCatching { + mapper.readValue(this) + } + + override fun Reader.readCompilationDatabase(): Result = + try { + success(mapper.readValue(this)) + } catch (je: JacksonException) { + when (val cause = je.cause) { + is IOException -> throw cause + else -> failure(je) + } + } catch (ioe: IOException) { + throw ioe + } + + override fun Path.readCompilationDatabase(charset: Charset): Result = + when { + isDirectory() && name != COMPILE_COMMANDS_JSON -> (this / COMPILE_COMMANDS_JSON).readCompilationDatabase(charset) + + else -> bufferedReader(charset).use { reader -> + reader.readCompilationDatabase() + } + } +} diff --git a/kompiledb-jackson/src/test/kotlin/com/saveourtool/kompiledb/jackson/CompilationCommandJacksonTest.kt b/kompiledb-jackson/src/test/kotlin/com/saveourtool/kompiledb/jackson/CompilationCommandJacksonTest.kt new file mode 100644 index 0000000..c4a3c80 --- /dev/null +++ b/kompiledb-jackson/src/test/kotlin/com/saveourtool/kompiledb/jackson/CompilationCommandJacksonTest.kt @@ -0,0 +1,250 @@ +package com.saveourtool.kompiledb.jackson + +import com.saveourtool.kompiledb.core.CompilationCommand +import com.saveourtool.kompiledb.core.JsonIo +import com.saveourtool.kompiledb.core.jackson +import com.saveourtool.kompiledb.core.matchers.shouldBeCommand +import com.fasterxml.jackson.core.JacksonException +import com.fasterxml.jackson.databind.SerializationFeature.INDENT_OUTPUT +import com.fasterxml.jackson.databind.exc.MismatchedInputException +import io.kotest.assertions.json.shouldEqualJson +import io.kotest.matchers.equals.shouldBeEqual +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.result.shouldBeFailure +import io.kotest.matchers.result.shouldBeSuccess +import io.kotest.matchers.string.shouldStartWith +import org.intellij.lang.annotations.Language +import kotlin.test.Test +import com.saveourtool.kompiledb.core.EnvPath as Path + +/** + * @see CompilationCommand + */ +class CompilationCommandJacksonTest { + private val jsonIo = JsonIo.jackson { + configure(INDENT_OUTPUT, true) + } + + @Test + fun `single command should serialize successfully`() { + val command = CompilationCommand(Path(""), Path("file.c"), listOf("clang", "-c", "file.c")) + + val actualJson = with(jsonIo) { + command.toJson() + } + + @Language("JSON") + val expectedJson = """ + { + "directory": "", + "file": "file.c", + "arguments": [ + "clang", + "-c", + "file.c" + ] + } + """.trimIndent() + + actualJson shouldEqualJson expectedJson + } + + @Test + fun `single command with arguments should be read successfully`() { + @Language("JSON") + val json = """ + { + "directory": "", + "file": "file.c", + "arguments": [ + "clang", + "-c", + "file.c" + ] + } + """.trimIndent() + + val command = with(jsonIo) { + json.toCompilationCommand() + }.shouldBeSuccess() + + command.shouldBeCommand( + file = "file.c", + arguments = listOf("clang", "-c", "file.c"), + ) + } + + @Test + fun `single command should be read successfully`() { + @Language("JSON") + val json = """ + { + "directory": "C:/Users/alice/cmake-3.26.4/Source", + "command": "/C/Program_Files/msys64/mingw64/bin/clang++.exe -DCURL_STATICLIB -DLIBARCHIVE_STATIC -DUNICODE -DWIN32_LEAN_AND_MEAN -D_UNICODE @CMakeFiles/CMakeLib.dir/includes_CXX.rsp -O3 -DNDEBUG -std=c++17 -o CMakeFiles/CMakeLib.dir/cmInstalledFile.cxx.obj -c /C/Users/alice/cmake-3.26.4/Source/cmInstalledFile.cxx", + "file": "C:/Users/alice/cmake-3.26.4/Source/cmInstalledFile.cxx", + "output": "Source/CMakeFiles/CMakeLib.dir/cmInstalledFile.cxx.obj" + } + """.trimIndent() + + val command = with(jsonIo) { + json.toCompilationCommand() + }.shouldBeSuccess() + + command.shouldBeCommand( + directory = "C:/Users/alice/cmake-3.26.4/Source", + file = "C:/Users/alice/cmake-3.26.4/Source/cmInstalledFile.cxx", + command = "/C/Program_Files/msys64/mingw64/bin/clang++.exe -DCURL_STATICLIB -DLIBARCHIVE_STATIC -DUNICODE -DWIN32_LEAN_AND_MEAN -D_UNICODE @CMakeFiles/CMakeLib.dir/includes_CXX.rsp -O3 -DNDEBUG -std=c++17 -o CMakeFiles/CMakeLib.dir/cmInstalledFile.cxx.obj -c /C/Users/alice/cmake-3.26.4/Source/cmInstalledFile.cxx", + output = "Source/CMakeFiles/CMakeLib.dir/cmInstalledFile.cxx.obj", + ) + } + + @Test + fun `null argument entries when reading`() { + @Language("JSON") + val json = """ + { + "directory": "", + "file": "file.c", + "arguments": [ + null, + "file.c" + ] + } + """.trimIndent() + + val failure = with(jsonIo) { + json.toCompilationCommand() + }.shouldBeFailure() + + failure.message.shouldNotBeNull() shouldBeEqual + """`arguments[0]` is null: {"directory":"","file":"file.c","arguments":[null,"file.c"]}""" + } + + @Test + fun `non-string argument entries when reading`() { + @Language("JSON") + val json = """ + { + "directory": "", + "file": "file.c", + "arguments": [ + 41, + 42 + ] + } + """.trimIndent() + + val failure = with(jsonIo) { + json.toCompilationCommand() + }.shouldBeFailure() + + failure.message.shouldNotBeNull() shouldBeEqual + """Expected `arguments[0]` to be a string but was a JsonPrimitive: {"directory":"","file":"file.c","arguments":[41,42]}""" + } + + @Test + fun `null fields should be read successfully`() { + @Language("JSON") + val json = """ + { + "directory": "", + "file": "file.c", + "arguments": [ + "clang", + "-c", + "file.c" + ], + "command": null, + "output": null + } + """.trimIndent() + + val command = with(jsonIo) { + json.toCompilationCommand() + }.shouldBeSuccess() + + command.shouldBeCommand( + file = "file.c", + arguments = listOf("clang", "-c", "file.c"), + ) + } + + @Test + fun `null directory when reading`() { + @Language("JSON") + val json = """ + { + "directory": null, + "file": "file.c", + "arguments": [ + "clang", + "-c", + "file.c" + ] + } + """.trimIndent() + + val failure = with(jsonIo) { + json.toCompilationCommand() + }.shouldBeFailure() + + failure.message.shouldNotBeNull() shouldBeEqual + """`directory` is null: {"directory":null,"file":"file.c","arguments":["clang","-c","file.c"]}""" + } + + @Test + fun `extra fields when reading`() { + @Language("JSON") + val json = """ + { + "directory": "", + "file": "file.c", + "arguments": [ + "clang", + "-c", + "file.c" + ], + "extra1": null, + "extra2": "foo", + "extra3": ["bar"], + "extra4": {} + } + """.trimIndent() + + val failure = with(jsonIo) { + json.toCompilationCommand() + }.shouldBeFailure() + + failure.message.shouldNotBeNull() shouldStartWith + """4 unexpected field(s) encountered (extra1, extra2, extra3, extra4)""" + } + + @Test + fun `empty object when reading`() { + @Language("JSON") + val json = """ + { + } + """.trimIndent() + + val failure = with(jsonIo) { + json.toCompilationCommand() + }.shouldBeFailure() + + failure.message.shouldNotBeNull() shouldBeEqual + """`directory` is missing: {}""" + } + + @Test + fun `not an object when reading`() { + @Language("JSON") + val json = "[]" + + val failure = with(jsonIo) { + json.toCompilationCommand() + }.shouldBeFailure() + + failure.message.shouldNotBeNull() shouldBeEqual + "Expected a JSON object but was a JsonArray: []" + } +} diff --git a/kompiledb-jackson/src/test/kotlin/com/saveourtool/kompiledb/jackson/CompilationDatabaseJacksonTest.kt b/kompiledb-jackson/src/test/kotlin/com/saveourtool/kompiledb/jackson/CompilationDatabaseJacksonTest.kt new file mode 100644 index 0000000..365487f --- /dev/null +++ b/kompiledb-jackson/src/test/kotlin/com/saveourtool/kompiledb/jackson/CompilationDatabaseJacksonTest.kt @@ -0,0 +1,185 @@ +package com.saveourtool.kompiledb.jackson + +import com.saveourtool.kompiledb.core.CompilationCommand +import com.saveourtool.kompiledb.core.CompilationDatabase +import com.saveourtool.kompiledb.core.CompilationDatabase.Companion.COMPILE_COMMANDS_JSON +import com.saveourtool.kompiledb.core.EnvPath +import com.saveourtool.kompiledb.core.JsonIo +import com.saveourtool.kompiledb.core.jackson +import com.fasterxml.jackson.core.JacksonException +import com.fasterxml.jackson.databind.SerializationFeature.INDENT_OUTPUT +import io.kotest.assertions.json.shouldBeEmptyJsonArray +import io.kotest.assertions.json.shouldEqualJson +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.equals.shouldBeEqual +import io.kotest.matchers.maps.shouldHaveSize +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.result.shouldBeFailure +import io.kotest.matchers.result.shouldBeSuccess +import org.intellij.lang.annotations.Language +import org.junit.jupiter.api.io.TempDir +import java.io.IOException +import java.nio.file.Path +import kotlin.io.path.div +import kotlin.test.Test + +/** + * @see CompilationDatabase + */ +class CompilationDatabaseJacksonTest { + private val jsonIo = JsonIo.jackson { + configure(INDENT_OUTPUT, true) + } + + @Test + fun `simple database should serialize successfully`() { + val command0 = CompilationCommand(EnvPath(""), EnvPath("file1.c"), listOf("clang", "-c", "file1.c")) + val command1 = CompilationCommand(EnvPath(""), EnvPath("file2.c"), listOf("clang", "-c", "file2.c")) + + val database = CompilationDatabase(command0, command1) + + val actualJson = with(jsonIo) { + database.toJson() + } + + @Language("JSON") + val expectedJson = """ + [ + { + "directory": "", + "file": "file1.c", + "arguments": [ + "clang", + "-c", + "file1.c" + ] + }, + { + "directory": "", + "file": "file2.c", + "arguments": [ + "clang", + "-c", + "file2.c" + ] + } + ] + """.trimIndent() + + actualJson shouldEqualJson expectedJson + } + + @Test + fun `empty database should serialize successfully`() { + with(jsonIo) { + CompilationDatabase().toJson() + }.shouldBeEmptyJsonArray() + } + + @Test + fun `empty database should be read successfully`() { + @Language("JSON") + val json = "[]" + + val database = with(jsonIo) { + json.toCompilationDatabase() + }.shouldBeSuccess() + + database.commands.shouldBeEmpty() + } + + @Test + fun `not an array when reading a database`() { + @Language("JSON") + val json = """ + { + "foo": 42, + "bar": null + } + """.trimIndent() + + val failure = with(jsonIo) { + json.toCompilationDatabase() + }.shouldBeFailure() + + failure.message.shouldNotBeNull() shouldBeEqual + """When reading a database, expected a JSON array but was a JsonObject: {"foo":42,"bar":null}""" + } + + @Test + fun `mixed database should be read partially`() { + @Language("JSON") + val json = """ + [ + { + "directory": "C:/Users/alice/cmake-3.26.4/Source", + "file": "C:/Users/alice/cmake-3.26.4/Source/cmInstalledFile.cxx", + "command": "/C/Program_Files/msys64/mingw64/bin/g++.exe -DCURL_STATICLIB -DLIBARCHIVE_STATIC -DUNICODE -DWIN32_LEAN_AND_MEAN -D_UNICODE @CMakeFiles/CMakeLib.dir/includes_CXX.rsp -O3 -DNDEBUG -std=c++17 -o CMakeFiles/CMakeLib.dir/cmInstalledFile.cxx.obj -c /C/Users/alice/cmake-3.26.4/Source/cmInstalledFile.cxx", + "output": "Source/CMakeFiles/CMakeLib.dir/cmInstalledFile.cxx.obj" + }, + { + "directory": "C:/Users/alice/cmake-3.26.4/Source", + "file": "C:/Users/alice/cmake-3.26.4/Source/cmMarkAsAdvancedCommand.cxx", + "output": "Source/CMakeFiles/CMakeLib.dir/cmMarkAsAdvancedCommand.cxx.obj" + }, + { + "directory": "C:/Users/alice/cmake-3.26.4/Source", + "file": "C:/Users/alice/cmake-3.26.4/Source/cmFileAPIToolchains.cxx", + "arguments": [ + "/C/Program_Files/msys64/mingw64/bin/g++.exe", + 41, + 42, + null + ], + "output": "Source/CMakeFiles/CMakeLib.dir/cmFileAPIToolchains.cxx.obj" + }, + { + "directory": "C:/Users/alice/cmake-3.26.4/Source", + "file": "C:/Users/alice/cmake-3.26.4/Source/cmInstallFileSetGenerator.cxx", + "command": "/C/Program_Files/msys64/mingw64/bin/g++.exe -DCURL_STATICLIB -DLIBARCHIVE_STATIC -DUNICODE -DWIN32_LEAN_AND_MEAN -D_UNICODE @CMakeFiles/CMakeLib.dir/includes_CXX.rsp -O3 -DNDEBUG -std=c++17 -o CMakeFiles/CMakeLib.dir/cmInstallFileSetGenerator.cxx.obj -c /C/Users/alice/cmake-3.26.4/Source/cmInstallFileSetGenerator.cxx", + "output": "Source/CMakeFiles/CMakeLib.dir/cmInstallFileSetGenerator.cxx.obj" + } + ] + """.trimIndent() + + val database = with(jsonIo) { + json.toCompilationDatabase() + }.shouldBeSuccess() + + database.commands shouldHaveSize 2 + database.errors shouldHaveSize 2 + + database.errors[1].shouldNotBeNull() shouldBeEqual + "Either `arguments` or `command` is required" + database.errors[2].shouldNotBeNull() shouldBeEqual + """Expected `arguments[1]` to be a string but was a JsonPrimitive: {"directory":"C:/Users/alice/cmake-3.26.4/Source","file":"C:/Users/alice/cmake-3.26.4/Source/cmFileAPIToolchains.cxx","arguments":["/C/Program_Files/msys64/mingw64/bin/g++.exe",41,42,null],"output":"Source/CMakeFiles/CMakeLib.dir/cmFileAPIToolchains.cxx.obj"}""" + } + + /** + * An attempt to read a database from a non-regular or an inaccessible file + * should result in an I/O exception. + */ + @Test + fun `i-o error when reading a non-regular file`(@TempDir projectDirectory: Path) { + shouldThrow { + with(jsonIo) { + projectDirectory.readCompilationDatabase() + } + } + } + + /** + * An attempt to read a database from a nonexistent file should result in an + * I/O exception. + */ + @Test + fun `i-o error when reading a missing file`(@TempDir projectDirectory: Path) { + shouldThrow { + with(jsonIo) { + (projectDirectory / COMPILE_COMMANDS_JSON).readCompilationDatabase() + } + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index dc039e5..26a8d45 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -13,6 +13,7 @@ plugins { include("kompiledb-core") include("kompiledb-gson") +include("kompiledb-jackson") gradleEnterprise { if (System.getenv("CI") != null) {