diff --git a/allure-ktorm/build.gradle.kts b/allure-ktorm/build.gradle.kts new file mode 100644 index 000000000..819846089 --- /dev/null +++ b/allure-ktorm/build.gradle.kts @@ -0,0 +1,46 @@ +description = "Allure Ktorm Integration" + +plugins { + id("org.jetbrains.kotlin.jvm") version "1.4.31" +} +apply(plugin = "kotlin") + +val agent: Configuration by configurations.creating + +val ktormVersion = "3.3.0" +val assertK = "0.23.1" +val h2 = "1.4.197" + +dependencies { + agent("org.aspectj:aspectjweaver") + api(project(":allure-attachments")) + + implementation("org.ktorm:ktorm-core:$ktormVersion") + + testImplementation("com.h2database:h2:$h2") + + testImplementation("com.willowtreeapps.assertk:assertk-jvm:$assertK") + testImplementation("org.junit.jupiter:junit-jupiter-api") + testImplementation("org.mockito:mockito-core") + testImplementation("org.slf4j:slf4j-simple") + testImplementation(project(":allure-java-commons-test")) + testImplementation(project(":allure-junit-platform")) + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") +} + +tasks.jar { + manifest { + attributes( + mapOf( + "Automatic-Module-Name" to "io.qameta.allure.ktorm" + ) + ) + } +} + +tasks.test { + useJUnitPlatform() + doFirst { + jvmArgs("-javaagent:${agent.singleFile}") + } +} diff --git a/allure-ktorm/src/main/kotlin/io/qameta/allure/ktorm/AllureKtormLogger.kt b/allure-ktorm/src/main/kotlin/io/qameta/allure/ktorm/AllureKtormLogger.kt new file mode 100644 index 000000000..c8830a6f3 --- /dev/null +++ b/allure-ktorm/src/main/kotlin/io/qameta/allure/ktorm/AllureKtormLogger.kt @@ -0,0 +1,125 @@ +/* + * Copyright 2021 Qameta Software OÜ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.qameta.allure.ktorm + +import io.qameta.allure.Allure +import io.qameta.allure.model.Status +import io.qameta.allure.model.StepResult +import org.ktorm.logging.Logger +import java.util.UUID + +/** + * Allure logger for Ktorm. + * + * Create attachments with sql, parameters and results. + * Unfortunately now we can't add parameters and results to created step. + * + * @author crazyMoonkin(Sergey Frolov) + * + * @property createSqlSteps enable creating steps + */ +class AllureKtormLogger( + private val createSqlSteps: Boolean = false, + private val attachParams: Boolean = true, + private val attachResults: Boolean = true, +) : Logger { + + override fun isTraceEnabled(): Boolean = false + + override fun trace(msg: String, e: Throwable?) { + log(msg, e) + } + + override fun isDebugEnabled(): Boolean = true + + override fun debug(msg: String, e: Throwable?) { + log(msg, e) + } + + override fun isInfoEnabled(): Boolean = false + + override fun info(msg: String, e: Throwable?) { + log(msg, e) + } + + override fun isWarnEnabled(): Boolean = false + + override fun warn(msg: String, e: Throwable?) { + log(msg, e) + } + + override fun isErrorEnabled(): Boolean = true + + override fun error(msg: String, e: Throwable?) { + log(msg, e) + } + + private fun log(msg: String, e: Throwable?) { + val typedMessage = msg.toTypedMessage() + lateinit var stepUUID: String + + if (createSqlSteps && typedMessage?.type == MessageType.SQL) { + stepUUID = UUID.randomUUID().toString() + createStep(stepUUID = stepUUID) + } + try { + typedMessage?.let { + when { + typedMessage.type == MessageType.SQL + || typedMessage.type == MessageType.PARAMETERS && attachParams + || typedMessage.type == MessageType.RESULTS && attachResults -> { + Allure.addAttachment(typedMessage.type.name, typedMessage.msg) + } + } + } + + } finally { + if (createSqlSteps && typedMessage?.type == MessageType.SQL) { + e?.let { Allure.getLifecycle().updateStep(stepUUID) { it.status = Status.FAILED } } + Allure.getLifecycle().stopStep(stepUUID) + } + } + } + + private fun createStep(stepUUID: String) = Allure.getLifecycle().startStep( + stepUUID, + StepResult().apply { + name = "Executed SQL query" + status = Status.PASSED + } + ) + + /** + * Split logged messages and convert it to TypedMessage + * Logged messages is type and message separated by colon + */ + private fun String.toTypedMessage() = split(": ").takeIf { it.size == 2 }?.let { msgParts -> + MessageType.values().firstOrNull { it.name == msgParts[0].toUpperCase() }?.let { + TypedMessage( + type = it, + msg = msgParts[1] + ) + } + } + + internal enum class MessageType { + SQL, + RESULTS, + PARAMETERS, + } + + internal data class TypedMessage(val type: MessageType, val msg: String) +} diff --git a/allure-ktorm/src/test/kotlin/io/qameta/allure/ktorm/AllureKtormLoggerTest.kt b/allure-ktorm/src/test/kotlin/io/qameta/allure/ktorm/AllureKtormLoggerTest.kt new file mode 100644 index 000000000..32ebde6e2 --- /dev/null +++ b/allure-ktorm/src/test/kotlin/io/qameta/allure/ktorm/AllureKtormLoggerTest.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2021 Qameta Software OÜ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.qameta.allure.ktorm + +import assertk.assertThat +import assertk.assertions.containsExactly +import assertk.assertions.extracting +import io.qameta.allure.model.Attachment +import io.qameta.allure.model.TestResult +import org.junit.jupiter.api.Test +import org.ktorm.dsl.from +import org.ktorm.dsl.insert +import org.ktorm.dsl.select + +internal class AllureKtormLoggerTest : BaseTest() { + + @Test + fun selectShouldCreateAttachments() { + val results = execute(database.from(Departments).select()::rowSet) + + assertThat(results.testResults.flatMap(TestResult::getAttachments)) + .extracting(Attachment::getName) + .containsExactly("SQL", "PARAMETERS", "RESULTS") + } + + @Test + fun insertShouldCreateAttachments() { + val results = execute { + database.insert(Departments) { + set(it.name, "John") + set(it.location, "Moscow") + } + } + + assertThat(results.testResults.flatMap(TestResult::getAttachments)) + .extracting(Attachment::getName) + .containsExactly("SQL", "PARAMETERS") + } +} diff --git a/allure-ktorm/src/test/kotlin/io/qameta/allure/ktorm/AllureKtormLoggerWithCreateStepsTest.kt b/allure-ktorm/src/test/kotlin/io/qameta/allure/ktorm/AllureKtormLoggerWithCreateStepsTest.kt new file mode 100644 index 000000000..52001b4cf --- /dev/null +++ b/allure-ktorm/src/test/kotlin/io/qameta/allure/ktorm/AllureKtormLoggerWithCreateStepsTest.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2021 Qameta Software OÜ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.qameta.allure.ktorm + +import assertk.assertThat +import assertk.assertions.containsExactly +import assertk.assertions.extracting +import io.qameta.allure.model.Attachment +import io.qameta.allure.model.StepResult +import io.qameta.allure.model.TestResult +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.ktorm.database.Database +import org.ktorm.dsl.from +import org.ktorm.dsl.insert +import org.ktorm.dsl.select + +internal class AllureKtormLoggerWithCreateStepsTest : BaseTest() { + + @BeforeEach + override fun setup() { + database = Database.connect( + url = "jdbc:h2:mem:ktorm;DB_CLOSE_DELAY=-1", + driver = "org.h2.Driver", + logger = AllureKtormLogger(createSqlSteps = true), + alwaysQuoteIdentifiers = true + ) + + execSqlScript("init-data.sql") + } + + @Test + fun selectShouldCreateStepAndAttachments() { + val results = execute(database.from(Departments).select()::rowSet) + + assertThat(results.testResults.flatMap(TestResult::getSteps)) + .extracting(StepResult::getName) + .containsExactly("Executed SQL query") + + assertThat(results.testResults.flatMap(TestResult::getSteps).flatMap(StepResult::getAttachments)) + .extracting(Attachment::getName) + .containsExactly("SQL") + + assertThat(results.testResults.flatMap(TestResult::getAttachments)) + .extracting(Attachment::getName) + .containsExactly("PARAMETERS", "RESULTS") + } + + @Test + fun insertShouldCreateStepAndAttachments() { + val results = execute { + database.insert(Departments) { + set(it.name, "John") + set(it.location, "Moscow") + } + } + + assertThat(results.testResults.flatMap(TestResult::getSteps)) + .extracting(StepResult::getName) + .containsExactly("Executed SQL query") + + assertThat(results.testResults.flatMap(TestResult::getSteps).flatMap(StepResult::getAttachments)) + .extracting(Attachment::getName) + .containsExactly("SQL") + + assertThat(results.testResults.flatMap(TestResult::getAttachments)) + .extracting(Attachment::getName) + .containsExactly("PARAMETERS") + } +} diff --git a/allure-ktorm/src/test/kotlin/io/qameta/allure/ktorm/BaseTest.kt b/allure-ktorm/src/test/kotlin/io/qameta/allure/ktorm/BaseTest.kt new file mode 100644 index 000000000..0dd841d26 --- /dev/null +++ b/allure-ktorm/src/test/kotlin/io/qameta/allure/ktorm/BaseTest.kt @@ -0,0 +1,108 @@ +/* + * Copyright 2021 Qameta Software OÜ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.qameta.allure.ktorm + +import io.qameta.allure.test.AllureResults +import io.qameta.allure.test.RunUtils +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.BeforeEach +import org.ktorm.database.Database +import org.ktorm.database.use +import org.ktorm.dsl.Query +import org.ktorm.entity.Entity +import org.ktorm.entity.sequenceOf +import org.ktorm.schema.Table +import org.ktorm.schema.date +import org.ktorm.schema.int +import org.ktorm.schema.long +import org.ktorm.schema.varchar +import java.io.IOException +import java.io.Serializable +import java.time.LocalDate + +open class BaseTest { + lateinit var database: Database + + @BeforeEach + open fun setup() { + database = Database.connect( + url = "jdbc:h2:mem:ktorm;DB_CLOSE_DELAY=-1", + driver = "org.h2.Driver", + logger = AllureKtormLogger(), + alwaysQuoteIdentifiers = true + ) + + execSqlScript("init-data.sql") + } + + @AfterEach + open fun destroy() { + execSqlScript("drop-data.sql") + } + + protected fun execSqlScript(filename: String) { + database.useConnection { conn -> + conn.createStatement().use { statement -> + javaClass.classLoader + ?.getResourceAsStream(filename) + ?.bufferedReader() + ?.use { reader -> + for (sql in reader.readText().split(';')) { + if (sql.any { it.isLetterOrDigit() }) { + statement.executeUpdate(sql) + } + } + } + } + } + } + + protected fun execute(query: () -> Any): AllureResults { + return RunUtils.runWithinTestContext { + try { + query() + } catch (e: IOException) { + throw RuntimeException("Could not execute query", e) + } + } + } + + data class LocationWrapper(val underlying: String = "") : Serializable + + interface Department : Entity { + companion object : Entity.Factory() + + val id: Int + var name: String + var location: String + var mixedCase: String? + } + + open class Departments(alias: String?) : Table("t_department", alias) { + companion object : Departments(null) + + override fun aliased(alias: String) = Departments(alias) + + val id = int("id").primaryKey().bindTo { it.id } + val name = varchar("name").bindTo { it.name } + val location = varchar("location").bindTo { it.location } + val mixedCase = varchar("mixedCase").bindTo { it.mixedCase } + } + + val Database.departments get() = this.sequenceOf(Departments) +} diff --git a/allure-ktorm/src/test/resources/drop-data.sql b/allure-ktorm/src/test/resources/drop-data.sql new file mode 100644 index 000000000..22569ada6 --- /dev/null +++ b/allure-ktorm/src/test/resources/drop-data.sql @@ -0,0 +1,2 @@ + +drop table if exists "t_department"; diff --git a/allure-ktorm/src/test/resources/init-data.sql b/allure-ktorm/src/test/resources/init-data.sql new file mode 100644 index 000000000..de8d96acd --- /dev/null +++ b/allure-ktorm/src/test/resources/init-data.sql @@ -0,0 +1,9 @@ +create table "t_department"( + "id" int not null primary key auto_increment, + "name" varchar(128) not null, + "location" varchar(128) not null, + "mixedCase" varchar(128) +); + +insert into "t_department"("name", "location") values ('tech', 'Guangzhou'); +insert into "t_department"("name", "location") values ('finance', 'Beijing'); diff --git a/settings.gradle.kts b/settings.gradle.kts index d2093d40a..dc91ec55c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -33,4 +33,4 @@ include("allure-spock") include("allure-spring-web") include("allure-test-filter") include("allure-testng") - +include("allure-ktorm")