From c5a646f9d77a00aa66f711706770be454d310a8c Mon Sep 17 00:00:00 2001 From: Andrey Shcheglov Date: Mon, 6 Jun 2022 20:18:59 +0300 Subject: [PATCH] Add unit tests for mis-indented expression body functions ### What's done: * Added unit tests for mis-indented expression body functions. * See #1330. --- .../chapter3/spaces/IndentationRuleFixTest.kt | 126 +++++++++++- .../spaces/IndentationRuleTestMixin.kt | 185 +++++++++++++++++ .../spaces/IndentationRuleWarnTest.kt | 193 +++++++++++++++++- .../org/cqfn/diktat/util/FixTestBase.kt | 43 ++++ .../org/cqfn/diktat/util/LintTestBase.kt | 30 ++- .../kotlin/org/cqfn/diktat/util/TestUtils.kt | 6 +- .../framework/processing/FileComparator.kt | 22 +- .../processing/TestComparatorUnit.kt | 78 +++++-- 8 files changed, 643 insertions(+), 40 deletions(-) create mode 100644 diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/IndentationRuleTestMixin.kt diff --git a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/IndentationRuleFixTest.kt b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/IndentationRuleFixTest.kt index 2b4c4b7fd1..400ef5bc85 100644 --- a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/IndentationRuleFixTest.kt +++ b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/IndentationRuleFixTest.kt @@ -6,8 +6,14 @@ import org.cqfn.diktat.ruleset.rules.chapter3.files.IndentationRule import org.cqfn.diktat.util.FixTestBase import generated.WarningNames +import org.assertj.core.api.SoftAssertions.assertSoftly +import org.intellij.lang.annotations.Language +import org.junit.jupiter.api.Assumptions.assumeTrue import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir + +import java.nio.file.Path class IndentationRuleFixTest : FixTestBase("test/paragraph3/indentation", ::IndentationRule, @@ -22,7 +28,7 @@ class IndentationRuleFixTest : FixTestBase("test/paragraph3/indentation", ) ) ) -) { +), IndentationRuleTestMixin { @Test @Tag(WarningNames.WRONG_INDENTATION) fun `parameters should be properly aligned`() { @@ -46,4 +52,122 @@ class IndentationRuleFixTest : FixTestBase("test/paragraph3/indentation", fun `regression - incorrect fixing in constructor parameter list`() { fixAndCompare("ConstructorExpected.kt", "ConstructorTest.kt") } + + /** + * This test has a counterpart under [IndentationRuleWarnTest]. + * + * See [#1330](https://github.com/saveourtool/diktat/issues/1330). + */ + @Test + @Tag(WarningNames.WRONG_INDENTATION) + fun `expression body functions should remain unchanged if properly indented (extendedIndentAfterOperators = true)`(@TempDir tempDir: Path) { + val defaultConfig = IndentationConfig("newlineAtEnd" to false) + val customConfig = defaultConfig.withCustomParameters("extendedIndentAfterOperators" to true) + + lintMultipleMethods( + expressionBodyFunctionsContinuationIndent, + tempDir = tempDir, + rulesConfigList = customConfig.asRulesConfigList()) + } + + /** + * This test has a counterpart under [IndentationRuleWarnTest]. + * + * See [#1330](https://github.com/saveourtool/diktat/issues/1330). + */ + @Test + @Tag(WarningNames.WRONG_INDENTATION) + fun `expression body functions should remain unchanged if properly indented (extendedIndentAfterOperators = false)`(@TempDir tempDir: Path) { + val defaultConfig = IndentationConfig("newlineAtEnd" to false) + val customConfig = defaultConfig.withCustomParameters("extendedIndentAfterOperators" to false) + + lintMultipleMethods( + expressionBodyFunctionsSingleIndent, + tempDir = tempDir, + rulesConfigList = customConfig.asRulesConfigList()) + } + + /** + * This test has a counterpart under [IndentationRuleWarnTest]. + * + * See [#1330](https://github.com/saveourtool/diktat/issues/1330). + */ + @Test + @Tag(WarningNames.WRONG_INDENTATION) + fun `expression body functions should be reformatted if mis-indented (extendedIndentAfterOperators = true)`(@TempDir tempDir: Path) { + assumeTrue(testsCanBeMuted()) { + "Skipping a known-to-fail test" + } + + val defaultConfig = IndentationConfig("newlineAtEnd" to false) + val customConfig = defaultConfig.withCustomParameters("extendedIndentAfterOperators" to true) + + lintMultipleMethods( + actualContent = expressionBodyFunctionsSingleIndent, + expectedContent = expressionBodyFunctionsContinuationIndent, + tempDir = tempDir, + rulesConfigList = customConfig.asRulesConfigList()) + } + + /** + * This test has a counterpart under [IndentationRuleWarnTest]. + * + * See [#1330](https://github.com/saveourtool/diktat/issues/1330). + */ + @Test + @Tag(WarningNames.WRONG_INDENTATION) + fun `expression body functions should be reformatted if mis-indented (extendedIndentAfterOperators = false)`(@TempDir tempDir: Path) { + assumeTrue(testsCanBeMuted()) { + "Skipping a known-to-fail test" + } + + val defaultConfig = IndentationConfig("newlineAtEnd" to false) + val customConfig = defaultConfig.withCustomParameters("extendedIndentAfterOperators" to false) + + lintMultipleMethods( + actualContent = expressionBodyFunctionsContinuationIndent, + expectedContent = expressionBodyFunctionsSingleIndent, + tempDir = tempDir, + rulesConfigList = customConfig.asRulesConfigList()) + } + + /** + * @param actualContent the original file content (may well be modified as + * fixes are applied). + * @param expectedContent the content the file is expected to have after the + * fixes are applied. + */ + private fun lintMultipleMethods( + @Language("kotlin") actualContent: Array, + @Language("kotlin") expectedContent: Array = actualContent, + tempDir: Path, + rulesConfigList: List? = null + ) { + require(actualContent.isNotEmpty()) { + "code fragments is an empty array" + } + + require(actualContent.size == expectedContent.size) { + "The actual and expected code fragments are arrays of different size: ${actualContent.size} != ${expectedContent.size}" + } + + assertSoftly { softly -> + sequence { + yieldAll(actualContent.asSequence() zip expectedContent.asSequence()) + + /* + * All fragments concatenated. + */ + yield(actualContent.concatenated() to expectedContent.concatenated()) + }.forEach { (actual, expected) -> + val lintResult = fixAndCompareContent( + actual, + expected, + tempDir, + rulesConfigList) + + softly.assertThat(lintResult).describedAs("lint result for \"$actual\"").isTrue + } + } + } } diff --git a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/IndentationRuleTestMixin.kt b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/IndentationRuleTestMixin.kt new file mode 100644 index 0000000000..7251636bf7 --- /dev/null +++ b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/IndentationRuleTestMixin.kt @@ -0,0 +1,185 @@ +package org.cqfn.diktat.ruleset.chapter3.spaces + +import org.cqfn.diktat.common.config.rules.RulesConfig +import org.cqfn.diktat.ruleset.constants.Warnings.WRONG_INDENTATION +import org.cqfn.diktat.ruleset.utils.indentation.IndentationConfig +import org.intellij.lang.annotations.Language + +import java.lang.Boolean.getBoolean as getBooleanProperty + +/** + * Code shared by [IndentationRuleWarnTest] and [IndentationRuleFixTest]. + * + * @see IndentationRuleWarnTest + * @see IndentationRuleFixTest + */ +internal interface IndentationRuleTestMixin { + /** + * See [#1330](https://github.com/saveourtool/diktat/issues/1330). + * + * @see expressionBodyFunctionsContinuationIndent + */ + @Suppress("CUSTOM_GETTERS_SETTERS") + val expressionBodyFunctionsSingleIndent: Array + @Language("kotlin") + get() = + arrayOf( + """ + |@Test + |fun `checking that suppression with ignore everything works`() { + | val code = + | ""${'"'} + | @Suppress("diktat") + | fun foo() { + | val a = 1 + | } + | ""${'"'}.trimIndent() + | lintMethod(code) + |} + """.trimMargin(), + + """ + |val currentTime: Time + | get() = + | with(currentDateTime) { + | Time(hour = hour, minute = minute, second = second) + | } + """.trimMargin(), + + """ + |fun formatDateByPattern(date: String, pattern: String = "ddMMyy"): String = + | DateTimeFormatter.ofPattern(pattern).format(LocalDate.parse(date)) + """.trimMargin(), + + """ + |private fun createLayoutParams(): WindowManager.LayoutParams = + | WindowManager.LayoutParams().apply { /* ... */ } + """.trimMargin(), + + """ + |val offsetDelta = + | if (shimmerAnimationType != ShimmerAnimationType.FADE) translateAnim.dp + | else 2000.dp + """.trimMargin(), + + """ + |private fun lerp(start: Float, stop: Float, fraction: Float): Float = + | (1 - fraction) * start + fraction * stop + """.trimMargin() + ) + + /** + * See [#1330](https://github.com/saveourtool/diktat/issues/1330). + * + * @see expressionBodyFunctionsSingleIndent + */ + @Suppress("CUSTOM_GETTERS_SETTERS") + val expressionBodyFunctionsContinuationIndent: Array + @Language("kotlin") + get() = + arrayOf( + """ + |@Test + |fun `checking that suppression with ignore everything works`() { + | val code = + | ""${'"'} + | @Suppress("diktat") + | fun foo() { + | val a = 1 + | } + | ""${'"'}.trimIndent() + | lintMethod(code) + |} + """.trimMargin(), + + """ + |val currentTime: Time + | get() = + | with(currentDateTime) { + | Time(hour = hour, minute = minute, second = second) + | } + """.trimMargin(), + + """ + |fun formatDateByPattern(date: String, pattern: String = "ddMMyy"): String = + | DateTimeFormatter.ofPattern(pattern).format(LocalDate.parse(date)) + """.trimMargin(), + + """ + |private fun createLayoutParams(): WindowManager.LayoutParams = + | WindowManager.LayoutParams().apply { /* ... */ } + """.trimMargin(), + + """ + |val offsetDelta = + | if (shimmerAnimationType != ShimmerAnimationType.FADE) translateAnim.dp + | else 2000.dp + """.trimMargin(), + + """ + |private fun lerp(start: Float, stop: Float, fraction: Float): Float = + | (1 - fraction) * start + fraction * stop + """.trimMargin(), + ) + + /** + * Creates an `IndentationConfig` from zero or more + * [config entries][configEntries]. Invoke without arguments to create a + * default `IndentationConfig`. + * + * @param configEntries the configuration entries to create this instance from. + * @see [IndentationConfig] + */ + @Suppress("TestFunctionName", "FUNCTION_NAME_INCORRECT_CASE") + fun IndentationConfig(vararg configEntries: Pair): IndentationConfig = + IndentationConfig(mapOf(*configEntries).mapValues(Any::toString)) + + /** + * @param configEntries the optional values which override the state of this + * [IndentationConfig]. + * @return the content of this [IndentationConfig] as a map, with some + * configuration entries overridden via [configEntries]. + */ + @Suppress("STRING_TEMPLATE_QUOTES") + fun IndentationConfig.withCustomParameters(vararg configEntries: Pair): Map = + mutableMapOf( + "alignedParameters" to "$alignedParameters", + "indentationSize" to "$indentationSize", + "newlineAtEnd" to "$newlineAtEnd", + "extendedIndentOfParameters" to "$extendedIndentOfParameters", + "extendedIndentAfterOperators" to "$extendedIndentAfterOperators", + "extendedIndentBeforeDot" to "$extendedIndentBeforeDot", + ).apply { + configEntries.forEach { (key, value) -> + this[key] = value.toString() + } + } + + /** + * Converts this map to a list containing a single [RulesConfig]. + * + * @return the list containing a single [RulesConfig] entry. + */ + fun Map.asRulesConfigList(): List = + listOf( + RulesConfig( + name = WRONG_INDENTATION.name, + enabled = true, + configuration = this + ) + ) + + /** + * @return the concatenated content of this array (elements separated with + * blank lines). + */ + fun Array.concatenated(): String = + joinToString(separator = "\n\n") + + /** + * @return `true` if known-to-fail unit tests can be muted on the CI server. + */ + @Suppress("FUNCTION_BOOLEAN_PREFIX") + fun testsCanBeMuted(): Boolean = + getBooleanProperty("tests.can.be.muted") +} diff --git a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/IndentationRuleWarnTest.kt b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/IndentationRuleWarnTest.kt index 92bc9dd498..a50da08e14 100644 --- a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/IndentationRuleWarnTest.kt +++ b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/IndentationRuleWarnTest.kt @@ -8,11 +8,18 @@ import org.cqfn.diktat.util.LintTestBase import com.pinterest.ktlint.core.LintError import generated.WarningNames +import org.assertj.core.api.AbstractSoftAssertions +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.SoftAssertions.assertSoftly +import org.intellij.lang.annotations.Language import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test +import org.opentest4j.MultipleFailuresError + +import java.util.function.Consumer @Suppress("LargeClass") -class IndentationRuleWarnTest : LintTestBase(::IndentationRule) { +class IndentationRuleWarnTest : LintTestBase(::IndentationRule), IndentationRuleTestMixin { private val ruleId = "$DIKTAT_RULE_SET_ID:${IndentationRule.NAME_ID}" private val rulesConfigList = listOf( RulesConfig(WRONG_INDENTATION.name, true, @@ -697,5 +704,189 @@ class IndentationRuleWarnTest : LintTestBase(::IndentationRule) { ) } + /** + * This test has a counterpart under [IndentationRuleFixTest]. + * + * See [#1330](https://github.com/saveourtool/diktat/issues/1330). + */ + @Test + @Tag(WarningNames.WRONG_INDENTATION) + fun `expression body functions should be properly indented (extendedIndentAfterOperators = true)`() { + val defaultConfig = IndentationConfig("newlineAtEnd" to false) + val customConfig = defaultConfig.withCustomParameters("extendedIndentAfterOperators" to true) + + lintMultipleMethods( + expressionBodyFunctionsContinuationIndent, + lintErrors = emptyArray(), + customConfig.asRulesConfigList() + ) + } + + /** + * This test has a counterpart under [IndentationRuleFixTest]. + * + * See [#1330](https://github.com/saveourtool/diktat/issues/1330). + */ + @Test + @Tag(WarningNames.WRONG_INDENTATION) + fun `expression body functions should be properly indented (extendedIndentAfterOperators = false)`() { + val defaultConfig = IndentationConfig("newlineAtEnd" to false) + val customConfig = defaultConfig.withCustomParameters("extendedIndentAfterOperators" to false) + + lintMultipleMethods( + expressionBodyFunctionsSingleIndent, + lintErrors = emptyArray(), + customConfig.asRulesConfigList() + ) + } + + /** + * This test has a counterpart under [IndentationRuleFixTest]. + * + * See [#1330](https://github.com/saveourtool/diktat/issues/1330). + */ + @Test + @Tag(WarningNames.WRONG_INDENTATION) + fun `expression body functions should be reported if mis-indented (extendedIndentAfterOperators = true)`() { + val defaultConfig = IndentationConfig("newlineAtEnd" to false) + val customConfig = defaultConfig.withCustomParameters("extendedIndentAfterOperators" to true) + + assertSoftly { softly -> + expressionBodyFunctionsSingleIndent.forEach { code -> + softly.assertThat(lintResult(code, customConfig.asRulesConfigList())) + .describedAs("lint result for \"$code\"") + .isNotEmpty + .hasSizeBetween(1, 3).allSatisfy(Consumer { lintError -> + assertThat(lintError.ruleId).describedAs("ruleId").isEqualTo(ruleId) + assertThat(lintError.canBeAutoCorrected).describedAs("canBeAutoCorrected").isTrue + assertThat(lintError.detail).matches(warnTextRegex) + }) + } + } + } + + /** + * This test has a counterpart under [IndentationRuleFixTest]. + * + * See [#1330](https://github.com/saveourtool/diktat/issues/1330). + */ + @Test + @Tag(WarningNames.WRONG_INDENTATION) + fun `expression body functions should be reported if mis-indented (extendedIndentAfterOperators = false)`() { + val defaultConfig = IndentationConfig("newlineAtEnd" to false) + val customConfig = defaultConfig.withCustomParameters("extendedIndentAfterOperators" to false) + + assertSoftly { softly -> + expressionBodyFunctionsContinuationIndent.forEach { code -> + softly.assertThat(lintResult(code, customConfig.asRulesConfigList())) + .describedAs("lint result for \"$code\"") + .isNotEmpty + .hasSizeBetween(1, 3).allSatisfy(Consumer { lintError -> + assertThat(lintError.ruleId).describedAs("ruleId").isEqualTo(ruleId) + assertThat(lintError.canBeAutoCorrected).describedAs("canBeAutoCorrected").isTrue + assertThat(lintError.detail).matches(warnTextRegex) + }) + } + } + } + + /** + * @see warnTextRegex + */ private fun warnText(expected: Int, actual: Int) = "${WRONG_INDENTATION.warnText()} expected $expected but was $actual" + + /** + * When within a scope of an `AbstractSoftAssertions`, collects failures + * thrown by [block], correctly accumulating multiple failures from nested + * soft assertions (if any). + * + * @see org.assertj.core.api.AssertionErrorCollector.collectAssertionError + */ + private fun AbstractSoftAssertions.collectAssertionErrors(block: () -> Unit) = + try { + block() + } catch (mfe: MultipleFailuresError) { + mfe.failures.forEach { failure -> + when (failure) { + is AssertionError -> collectAssertionError(failure) + else -> fail(failure.toString(), failure) + } + } + } catch (ae: AssertionError) { + collectAssertionError(ae) + } catch (th: Throwable) { + fail(th.toString(), th) + } + + /** + * Similar to [lintMethod], but can be invoked from a scope of + * `AbstractSoftAssertions` in order to accumulate test results from linting + * _multiple_ code fragments. + * + * @param rulesConfigList the list of rules which can optionally override + * the [default value][LintTestBase.rulesConfigList]. + * @see lintMethod + */ + private fun AbstractSoftAssertions.lintMethodSoftly( + @Language("kotlin") code: String, + vararg lintErrors: LintError, + rulesConfigList: List? = null, + fileName: String? = null + ) { + require(code.isNotBlank()) { + "code is blank" + } + + collectAssertionErrors { + lintMethod(code, lintErrors = lintErrors, rulesConfigList, fileName) + } + } + + /** + * Tests multiple code [fragments] using the same + * [rule configuration][rulesConfigList]. + * + * All code fragments get concatenated together and the resulting, bigger + * fragment gets tested, too. + * + * @param rulesConfigList the list of rules which can optionally override + * the [default value][LintTestBase.rulesConfigList]. + * @see lintMethod + */ + private fun lintMultipleMethods( + @Language("kotlin") fragments: Array, + vararg lintErrors: LintError, + rulesConfigList: List? = null, + fileName: String? = null + ) { + require(fragments.isNotEmpty()) { + "code fragments is an empty array" + } + + assertSoftly { softly -> + sequence { + yieldAll(fragments.asSequence()) + + /* + * All fragments concatenated. + */ + yield(fragments.concatenated()) + }.forEach { fragment -> + softly.lintMethodSoftly( + fragment, + lintErrors = lintErrors, + rulesConfigList, + fileName + ) + } + } + } + + companion object { + /** + * @see warnText + */ + @Language("RegExp") + private val warnTextRegex = "^\\Q${WRONG_INDENTATION.warnText()}\\E expected \\d+ but was \\d+$" + } } diff --git a/diktat-rules/src/test/kotlin/org/cqfn/diktat/util/FixTestBase.kt b/diktat-rules/src/test/kotlin/org/cqfn/diktat/util/FixTestBase.kt index ea538ffcdc..24298eab2f 100644 --- a/diktat-rules/src/test/kotlin/org/cqfn/diktat/util/FixTestBase.kt +++ b/diktat-rules/src/test/kotlin/org/cqfn/diktat/util/FixTestBase.kt @@ -5,8 +5,13 @@ import org.cqfn.diktat.test.framework.processing.TestComparatorUnit import com.pinterest.ktlint.core.Rule import com.pinterest.ktlint.core.RuleSetProvider +import org.intellij.lang.annotations.Language import org.junit.jupiter.api.Assertions +import java.nio.file.Path +import kotlin.io.path.bufferedWriter +import kotlin.io.path.div + /** * @property resourceFilePath path to files which will be compared in tests */ @@ -56,6 +61,7 @@ open class FixTestBase(protected val resourceFilePath: String, * @param expectedPath path to file with expected result, relative to [resourceFilePath] * @param testPath path to file with code that will be transformed by formatter, relative to [resourceFilePath] * @param overrideRulesConfigList optional override to [rulesConfigList] + * @see fixAndCompareContent */ protected fun fixAndCompare(expectedPath: String, testPath: String, @@ -69,4 +75,41 @@ open class FixTestBase(protected val resourceFilePath: String, .compareFilesFromResources(expectedPath, testPath) ) } + + /** + * Unlike [fixAndCompare], this method doesn't perform any assertions. + * + * @param actualContent the original file content (may well be modified as + * fixes are applied). + * @param expectedContent the content the file is expected to have after the + * fixes are applied. + * @param tempDir the temporary directory (usually injected by _JUnit_). + * @param overrideRulesConfigList an optional override for [rulesConfigList] + * (the class-wide configuration). + * @return the result of file content comparison. + * @see fixAndCompare + */ + @Suppress("FUNCTION_BOOLEAN_PREFIX") + protected fun fixAndCompareContent( + @Language("kotlin") actualContent: String, + @Language("kotlin") expectedContent: String = actualContent, + tempDir: Path, + overrideRulesConfigList: List? = null + ): Boolean { + val actual = tempDir / "actual.kt" + actual.bufferedWriter().use { out -> + out.write(actualContent) + } + + val expected = tempDir / "expected.kt" + expected.bufferedWriter().use { out -> + out.write(expectedContent) + } + + val testComparatorUnit = TestComparatorUnit(tempDir.toString()) { text, fileName -> + format(ruleSetProviderRef, text, fileName, overrideRulesConfigList ?: rulesConfigList, cb) + } + + return testComparatorUnit.compareFilesFromFileSystem(expected, actual) + } } diff --git a/diktat-rules/src/test/kotlin/org/cqfn/diktat/util/LintTestBase.kt b/diktat-rules/src/test/kotlin/org/cqfn/diktat/util/LintTestBase.kt index 512e8c17cf..9c56f17992 100644 --- a/diktat-rules/src/test/kotlin/org/cqfn/diktat/util/LintTestBase.kt +++ b/diktat-rules/src/test/kotlin/org/cqfn/diktat/util/LintTestBase.kt @@ -22,15 +22,36 @@ open class LintTestBase(private val ruleSupplier: (rulesConfigList: List? = null, fileName: String? = null ) { + lintResult(code, rulesConfigList, fileName) + .assertEquals(*lintErrors) + } + + /** + * Lints the [code] and returns the errors collected, but (unlike + * [lintMethod]) doesn't make any assertions. + * + * @param code the code to check. + * @param rulesConfigList an optional override for `this.rulesConfigList`. + * @param fileName an optional override for the file name. + * @return the list of lint errors. + * @see lintMethod + */ + @OptIn(FeatureInAlphaState::class) + protected fun lintResult( + @Language("kotlin") code: String, + rulesConfigList: List? = null, + fileName: String? = null + ): List { val actualFileName = fileName ?: TEST_FILE_NAME - val res: MutableList = mutableListOf() + val lintErrors: MutableList = mutableListOf() + KtLint.lint( KtLint.ExperimentalParams( fileName = actualFileName, @@ -38,10 +59,11 @@ open class LintTestBase(private val ruleSupplier: (rulesConfigList: List res.add(lintError) }, + cb = { lintError, _ -> lintErrors += lintError }, userData = mapOf("file_path" to actualFileName) ) ) - res.assertEquals(*lintErrors) + + return lintErrors } } diff --git a/diktat-rules/src/test/kotlin/org/cqfn/diktat/util/TestUtils.kt b/diktat-rules/src/test/kotlin/org/cqfn/diktat/util/TestUtils.kt index aff5745ae0..f46bea499a 100644 --- a/diktat-rules/src/test/kotlin/org/cqfn/diktat/util/TestUtils.kt +++ b/diktat-rules/src/test/kotlin/org/cqfn/diktat/util/TestUtils.kt @@ -16,6 +16,7 @@ import com.pinterest.ktlint.core.VisitorProvider import com.pinterest.ktlint.core.api.FeatureInAlphaState import org.assertj.core.api.Assertions import org.assertj.core.api.SoftAssertions +import org.intellij.lang.annotations.Language import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.slf4j.LoggerFactory @@ -24,7 +25,8 @@ import java.util.function.Consumer internal const val TEST_FILE_NAME = "TestFileName.kt" -private val log = LoggerFactory.getLogger("TestUtils") +@Suppress("WRONG_WHITESPACE") +private val log = LoggerFactory.getLogger({}.javaClass) @Suppress("TYPE_ALIAS") internal val defaultCallback: (lintError: LintError, corrected: Boolean) -> Unit = { lintError, _ -> @@ -82,7 +84,7 @@ internal fun List.assertEquals(vararg expectedLintErrors: LintError) @OptIn(FeatureInAlphaState::class) @Suppress("LAMBDA_IS_NOT_LAST_PARAMETER") internal fun format(ruleSetProviderRef: (rulesConfigList: List?) -> RuleSetProvider, - text: String, + @Language("kotlin") text: String, fileName: String, rulesConfigList: List? = null, cb: LintErrorCallback = defaultCallback diff --git a/diktat-test-framework/src/main/kotlin/org/cqfn/diktat/test/framework/processing/FileComparator.kt b/diktat-test-framework/src/main/kotlin/org/cqfn/diktat/test/framework/processing/FileComparator.kt index e179c39557..227686b016 100644 --- a/diktat-test-framework/src/main/kotlin/org/cqfn/diktat/test/framework/processing/FileComparator.kt +++ b/diktat-test-framework/src/main/kotlin/org/cqfn/diktat/test/framework/processing/FileComparator.kt @@ -7,10 +7,9 @@ import org.slf4j.LoggerFactory import java.io.File import java.io.IOException -import java.nio.file.Files -import java.nio.file.Paths -import java.util.ArrayList -import java.util.stream.Collectors +import java.nio.file.Path +import kotlin.io.path.Path +import kotlin.io.path.readLines /** * A class that is capable of comparing files content @@ -36,6 +35,8 @@ class FileComparator { this.actualResultList = readFile(actualResultFile.absolutePath) } + constructor(expectedResultFile: Path, actualResultList: List) : this(expectedResultFile.toFile(), actualResultList) + /** * @return true in case files are different * false - in case they are equals @@ -87,18 +88,17 @@ class FileComparator { } /** - * @param fileName - file where to write these list to, separated with newlines - * @return a list of lines from the file + * @param fileName file where to write these list to, separated with newlines. + * @return a list of lines from the file, or an empty list if an I/O error + * has occurred. */ - private fun readFile(fileName: String): List { - var list: List = ArrayList() + private fun readFile(fileName: String): List = try { - Files.newBufferedReader(Paths.get(fileName)).use { list = it.lines().collect(Collectors.toList()) } + Path(fileName).readLines() } catch (e: IOException) { log.error("Not able to read file: $fileName") + emptyList() } - return list - } companion object { private val log = LoggerFactory.getLogger(FileComparator::class.java) diff --git a/diktat-test-framework/src/main/kotlin/org/cqfn/diktat/test/framework/processing/TestComparatorUnit.kt b/diktat-test-framework/src/main/kotlin/org/cqfn/diktat/test/framework/processing/TestComparatorUnit.kt index a77519727c..8e2e1ffd70 100644 --- a/diktat-test-framework/src/main/kotlin/org/cqfn/diktat/test/framework/processing/TestComparatorUnit.kt +++ b/diktat-test-framework/src/main/kotlin/org/cqfn/diktat/test/framework/processing/TestComparatorUnit.kt @@ -1,28 +1,39 @@ package org.cqfn.diktat.test.framework.processing -import org.apache.commons.io.FileUtils import org.slf4j.Logger import org.slf4j.LoggerFactory -import java.io.File import java.io.IOException -import java.nio.file.Files +import java.nio.file.Path import java.nio.file.Paths -import java.util.ArrayList -import java.util.stream.Collectors +import kotlin.io.path.Path +import kotlin.io.path.absolutePathString +import kotlin.io.path.copyTo +import kotlin.io.path.isRegularFile +import kotlin.io.path.readLines /** * Class that can apply transformation to an input file and then compare with expected result and output difference. + * + * @property resourceFilePath only used when the files are loaded as resources, + * via [compareFilesFromResources]. * @property function a transformation that will be applied to the file */ @Suppress("ForbiddenComment", "TYPE_ALIAS") class TestComparatorUnit(private val resourceFilePath: String, private val function: (expectedText: String, testFilePath: String) -> String) { /** - * @param expectedResult - * @param testFileStr - * @param trimLastEmptyLine - * @return true if transformed file equals expected result, false otherwise + * @param expectedResult the name of the resource which has the expected + * content. The trailing newline, if any, **won't be read** as a separate + * empty string. So, if the content to be read from this file is expected + * to be terminated with an empty string (which is the case if + * `newlineAtEnd` is `true`), then the file should end with **two** + * consecutive linebreaks. + * @param testFileStr the name of the resource which has the original content. + * @param trimLastEmptyLine whether the last (empty) line should be + * discarded when reading the content of [testFileStr]. + * @return `true` if transformed file equals expected result, `false` otherwise. + * @see compareFilesFromFileSystem */ @Suppress("FUNCTION_BOOLEAN_PREFIX") fun compareFilesFromResources( @@ -37,15 +48,41 @@ class TestComparatorUnit(private val resourceFilePath: String, return false } - val expectedFile = File(expectedPath.file) - val testFile = File(testPath.file) + return compareFilesFromFileSystem( + Paths.get(expectedPath.toURI()), + Paths.get(testPath.toURI()), + trimLastEmptyLine) + } + + /** + * @param expectedFile the file which has the expected content. The trailing + * newline, if any, **won't be read** as a separate empty string. So, if + * the content to be read from this file is expected to be terminated with + * an empty string (which is the case if `newlineAtEnd` is `true`), then + * the file should end with **two** consecutive linebreaks. + * @param testFile the file which has the original content. + * @param trimLastEmptyLine whether the last (empty) line should be + * discarded when reading the content of [testFile]. + * @return `true` if transformed file equals expected result, `false` otherwise. + * @see compareFilesFromResources + */ + @Suppress("FUNCTION_BOOLEAN_PREFIX") + fun compareFilesFromFileSystem( + expectedFile: Path, + testFile: Path, + trimLastEmptyLine: Boolean = false + ): Boolean { + if (!testFile.isRegularFile() || !expectedFile.isRegularFile()) { + log.error("Not able to find files for running test: $expectedFile and $testFile") + return false + } - val copyTestFile = File("${testFile.absolutePath}_copy") - FileUtils.copyFile(testFile, copyTestFile) + val copyTestFile = Path("${testFile.absolutePathString()}_copy") + testFile.copyTo(copyTestFile, overwrite = true) val actualResult = function( - readFile(copyTestFile.absolutePath).joinToString("\n"), - copyTestFile.absolutePath + readFile(copyTestFile.absolutePathString()).joinToString("\n"), + copyTestFile.absolutePathString() ) if (trimLastEmptyLine) { @@ -60,17 +97,16 @@ class TestComparatorUnit(private val resourceFilePath: String, /** * @param fileName - * @return file content as a list of lines + * @return file content as a list of lines, or an empty list if an I/O error + * has occurred. */ - private fun readFile(fileName: String): List { - var list: List = ArrayList() + private fun readFile(fileName: String): List = try { - Files.newBufferedReader(Paths.get(fileName)).use { list = it.lines().collect(Collectors.toList()) } + Path(fileName).readLines() } catch (e: IOException) { log.error("Not able to read file: $fileName") + emptyList() } - return list - } companion object { val log: Logger = LoggerFactory.getLogger(TestComparatorUnit::class.java)