diff --git a/diktat-maven-plugin/src/main/kotlin/org/cqfn/diktat/plugin/maven/DiktatBaseMojo.kt b/diktat-maven-plugin/src/main/kotlin/org/cqfn/diktat/plugin/maven/DiktatBaseMojo.kt index 34f4bcb622..274a1d00fc 100644 --- a/diktat-maven-plugin/src/main/kotlin/org/cqfn/diktat/plugin/maven/DiktatBaseMojo.kt +++ b/diktat-maven-plugin/src/main/kotlin/org/cqfn/diktat/plugin/maven/DiktatBaseMojo.kt @@ -5,6 +5,7 @@ package org.cqfn.diktat.plugin.maven import org.cqfn.diktat.ruleset.rules.DiktatRuleSetProvider +import org.cqfn.diktat.ruleset.utils.isKotlinCodeOrScript import com.pinterest.ktlint.core.KtLint import com.pinterest.ktlint.core.LintError @@ -207,7 +208,7 @@ abstract class DiktatBaseMojo : AbstractMojo() { directory .walk() .filter { file -> - file.isDirectory || file.extension.let { it == "kt" || it == "kts" } + file.isDirectory || file.toPath().isKotlinCodeOrScript() } .filter { it.isFile } .filterNot { file -> file in excludedFiles || excludedDirs.any { file.startsWith(it) } } diff --git a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter3/files/IndentationRule.kt b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter3/files/IndentationRule.kt index 36e731a0e3..eda4d21bc7 100644 --- a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter3/files/IndentationRule.kt +++ b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter3/files/IndentationRule.kt @@ -121,7 +121,7 @@ class IndentationRule(configRules: List) : DiktatRule( ::KdocIndentationChecker, ::CustomGettersAndSettersChecker, ::ArrowInWhenChecker - ).map { it.invoke(configuration) } + ).map { it(configuration) } if (checkIsIndentedWithSpaces(node)) { checkIndentation(node) diff --git a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/FileUtils.kt b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/FileUtils.kt index 0e349bbeb6..2eaa177b93 100644 --- a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/FileUtils.kt +++ b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/FileUtils.kt @@ -4,7 +4,12 @@ package org.cqfn.diktat.ruleset.utils +import java.nio.file.Path +import kotlin.io.path.extension + internal const val SRC_DIRECTORY_NAME = "src" +private const val KOTLIN_EXTENSION = "kt" +private const val KOTLIN_SCRIPT_EXTENSION = KOTLIN_EXTENSION + "s" /** * Splits [this] string by file path separator. @@ -17,11 +22,25 @@ fun String.splitPathToDirs(): List = .split("/") /** - * Checks if [this] String is a name of a kotlin script file by checking whether file extension equals 'kts' + * Checks if [this] [String] is a name of a kotlin script file by checking whether file extension equals 'kts' + * + * @return true if this is a kotlin script file name, false otherwise + */ +fun String.isKotlinScript() = endsWith(".$KOTLIN_SCRIPT_EXTENSION") + +/** + * Check if [this] [Path] is a kotlin script by checking whether an extension equals to 'kts' * * @return true if this is a kotlin script file name, false otherwise */ -fun String.isKotlinScript() = endsWith(".kts") +fun Path.isKotlinScript() = this.extension.lowercase() == KOTLIN_SCRIPT_EXTENSION + +/** + * Check if [this] [Path] is a kotlin code or script by checking whether an extension equals to `kt` or 'kts' + * + * @return true if this is a kotlin code or script file name, false otherwise + */ +fun Path.isKotlinCodeOrScript() = this.extension.lowercase() in setOf(KOTLIN_EXTENSION, KOTLIN_SCRIPT_EXTENSION) /** * Checks if [this] String is a name of a gradle kotlin script file by checking whether file extension equals 'gradle.kts' diff --git a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter2/HeaderCommentRuleTest.kt b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter2/HeaderCommentRuleTest.kt index e96b4b614d..4c57aa6cee 100644 --- a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter2/HeaderCommentRuleTest.kt +++ b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter2/HeaderCommentRuleTest.kt @@ -221,7 +221,7 @@ class HeaderCommentRuleTest : LintTestBase(::HeaderCommentRule) { lintMethod( """ /* - * Copyright (c) 2023 My Company, Ltd. All rights reserved. + * Copyright (c) $curYear My Company, Ltd. All rights reserved. */ /** * Very useful description, why this file has two classes @@ -244,7 +244,7 @@ class HeaderCommentRuleTest : LintTestBase(::HeaderCommentRule) { lintMethod( """ /* - * Copyright (c) My Company, Ltd. 2012-2023. All rights reserved. + * Copyright (c) My Company, Ltd. 2012-$curYear. All rights reserved. */ /** * Very useful description, why this file has two classes @@ -267,7 +267,7 @@ class HeaderCommentRuleTest : LintTestBase(::HeaderCommentRule) { lintMethod( """ /* - Copyright (c) My Company, Ltd. 2021-2023. All rights reserved. + Copyright (c) My Company, Ltd. 2021-$curYear. All rights reserved. */ /** * Very useful description, why this file has two classes @@ -290,7 +290,7 @@ class HeaderCommentRuleTest : LintTestBase(::HeaderCommentRule) { lintMethod( """ /* - * Copyright (c) My Company, Ltd. 2002-2023. All rights reserved. + * Copyright (c) My Company, Ltd. 2002-$curYear. All rights reserved. */ /** * Very useful description, why this file has two classes diff --git a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/junit/CloseablePath.kt b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/junit/CloseablePath.kt index 8141cefc64..a0fc9fa6c0 100644 --- a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/junit/CloseablePath.kt +++ b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/junit/CloseablePath.kt @@ -1,5 +1,7 @@ package org.cqfn.diktat.ruleset.junit +import org.cqfn.diktat.test.framework.util.resetPermissions +import org.cqfn.diktat.test.framework.util.tryToDeleteOnExit import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource import java.io.IOException import java.nio.file.DirectoryNotEmptyException @@ -15,6 +17,7 @@ import kotlin.io.path.absolute import kotlin.io.path.deleteExisting import kotlin.io.path.isDirectory import kotlin.io.path.notExists +import kotlin.io.path.relativeToOrSelf /** * @property directory the temporary directory (will be recursively deleted once @@ -102,11 +105,11 @@ data class CloseablePath(val directory: Path) : CloseableResource { @Suppress("WRONG_NEWLINES") // False positives, see #1495. val joinedPaths = keys .asSequence() + .map(Path::tryToDeleteOnExit) .map { path -> - path.tryToDeleteOnExit() - }.map { path -> - path.relativizeSafely() - }.map(Any::toString) + path.relativeToOrSelf(directory) + } + .map(Any::toString) .joinToString() return IOException("Failed to delete temp directory ${directory.absolute()}. " + @@ -114,36 +117,4 @@ data class CloseablePath(val directory: Path) : CloseableResource { values.forEach(this::addSuppressed) } } - - private fun Path.tryToDeleteOnExit(): Path { - try { - toFile().deleteOnExit() - } catch (_: UnsupportedOperationException) { - /* - * Ignore. - */ - } - - return this - } - - private fun Path.relativizeSafely(): Path = - try { - directory.relativize(this) - } catch (_: IllegalArgumentException) { - this - } - - private companion object { - private fun Path.resetPermissions() { - toFile().apply { - setReadable(true) - setWritable(true) - - if (isDirectory) { - setExecutable(true) - } - } - } - } } diff --git a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/utils/AstNodeUtilsTest.kt b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/utils/AstNodeUtilsTest.kt index db62fe1d78..d26cbc4c00 100644 --- a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/utils/AstNodeUtilsTest.kt +++ b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/utils/AstNodeUtilsTest.kt @@ -31,6 +31,7 @@ import com.pinterest.ktlint.core.ast.ElementType.WHITE_SPACE import com.pinterest.ktlint.core.ast.isLeaf import com.pinterest.ktlint.core.ast.nextCodeSibling import com.pinterest.ktlint.core.ast.nextSibling +import org.intellij.lang.annotations.Language import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl import org.jetbrains.kotlin.com.intellij.psi.tree.IElementType @@ -807,7 +808,7 @@ private class PrettyPrintingVisitor(private val elementType: IElementType, companion object { fun assertStringRepr( elementType: IElementType, - code: String, + @Language("kotlin") code: String, level: Int = 0, maxLevel: Int = -1, expected: String diff --git a/diktat-rules/src/test/kotlin/org/cqfn/diktat/util/DiktatRuleSetProvider4Test.kt b/diktat-rules/src/test/kotlin/org/cqfn/diktat/util/DiktatRuleSetProvider4Test.kt index 6e9918cff8..52ca3eea1f 100644 --- a/diktat-rules/src/test/kotlin/org/cqfn/diktat/util/DiktatRuleSetProvider4Test.kt +++ b/diktat-rules/src/test/kotlin/org/cqfn/diktat/util/DiktatRuleSetProvider4Test.kt @@ -13,6 +13,7 @@ import org.cqfn.diktat.common.config.rules.RulesConfig import org.cqfn.diktat.common.config.rules.RulesConfigReader import org.cqfn.diktat.ruleset.rules.DiktatRuleSetProvider import org.cqfn.diktat.ruleset.rules.OrderedRuleSet.Companion.delegatee +import org.cqfn.diktat.test.framework.util.filterContentMatches import com.pinterest.ktlint.core.Rule import com.pinterest.ktlint.core.RuleSet @@ -21,7 +22,12 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test -import java.io.File +import java.nio.file.Path +import kotlin.io.path.ExperimentalPathApi +import kotlin.io.path.Path +import kotlin.io.path.isRegularFile +import kotlin.io.path.nameWithoutExtension +import kotlin.io.path.walk /** * simple class for emulating RuleSetProvider to inject .yml rule configuration and mock this part of code @@ -38,28 +44,19 @@ class DiktatRuleSetProvider4Test(private val ruleSupplier: (rulesConfigList: Lis } class DiktatRuleSetProviderTest { + @OptIn(ExperimentalPathApi::class) @Suppress("UnsafeCallOnNullableType") @Test fun `check DiktatRuleSetProviderTest contain all rules`() { val path = "${System.getProperty("user.dir")}/src/main/kotlin/org/cqfn/diktat/ruleset/rules" - val filesName = File(path) + val fileNames = Path(path) .walk() - .filter { it.isFile } - .filter { file -> - /* - * Include only those files which contain `Rule` or `DiktatRule` - * descendants (any of the 1st 150 lines contains a superclass - * constructor call). - */ - val constructorCall = Regex(""":\s*(?:Diktat)?Rule\s*\(""") - file.bufferedReader().lineSequence().take(150) - .any { line -> - line.contains(constructorCall) - } - } - .map { it.nameWithoutExtension } - .filterNot { it in ignoreFile } - val rulesName = DiktatRuleSetProvider().get() + .filter(Path::isRegularFile) + .filterContentMatches(linesToRead = 150, Regex(""":\s*(?:Diktat)?Rule\s*\(""")) + .map(Path::nameWithoutExtension) + .filterNot { it in ignoredFileNames } + .toList() + val ruleNames = DiktatRuleSetProvider().get() .asSequence() .onEachIndexed { index, rule -> if (index != 0) { @@ -70,16 +67,22 @@ class DiktatRuleSetProviderTest { } } .map { it.delegatee() } - .map { it::class.simpleName!! } - .filterNot { it == "DummyWarning" } + .map { it::class.simpleName } + .filterNotNull() + .filterNot { it in ignoredRuleNames } .toList() - assertThat(rulesName.sorted()).containsExactlyElementsOf(filesName.sorted().toList()) + assertThat(fileNames).isNotEmpty + assertThat(ruleNames).isNotEmpty + assertThat(ruleNames.sorted()).containsExactlyElementsOf(fileNames.sorted()) } companion object { - private val ignoreFile = listOf( + private val ignoredFileNames = listOf( "DiktatRule", "OrderedRuleSet", ) + private val ignoredRuleNames = listOf( + "DummyWarning", + ) } } 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 7e5386948b..a23cb96a90 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 @@ -15,12 +15,22 @@ import com.pinterest.ktlint.core.Rule import com.pinterest.ktlint.core.RuleSet import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.fail +import org.intellij.lang.annotations.Language import org.jetbrains.kotlin.com.intellij.lang.ASTNode +import java.io.Reader import java.util.concurrent.atomic.AtomicInteger +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind.EXACTLY_ONCE +import kotlin.contracts.contract internal const val TEST_FILE_NAME = "TestFileName.kt" +private val debuggerPromptPrefixes: Array = arrayOf( + "Listening for transport dt_socket at address: ", + "Listening for transport dt_shmem at address: ", +) + /** * Casts a nullable value to a non-`null` one, similarly to the `!!` * operator. @@ -28,8 +38,67 @@ internal const val TEST_FILE_NAME = "TestFileName.kt" * @param lazyFailureMessage the message to evaluate in case of a failure. * @return a non-`null` value. */ -internal fun T?.assertNotNull(lazyFailureMessage: () -> String = { "Expecting actual not to be null" }): T = - this ?: fail(lazyFailureMessage()) +@OptIn(ExperimentalContracts::class) +internal fun T?.assertNotNull(lazyFailureMessage: () -> String = { "Expecting actual not to be null" }): T { + contract { + returns() implies (this@assertNotNull != null) + } + + return this ?: fail(lazyFailureMessage()) +} + +/** + * Calls the [block] callback giving it a sequence of all the lines in this file + * and closes the reader once the processing is complete. + * + * If [filterDebuggerPrompt] is `true`, the JVM debugger prompt is filtered out + * from the sequence of lines before it is consumed by [block]. + * + * If [filterDebuggerPrompt] is `false`, this function behaves exactly as the + * overloaded function from the standard library. + * + * @param filterDebuggerPrompt whether the JVM debugger prompt should be + * filtered out. + * @param block the callback which consumes the lines produced by this [Reader]. + * @return the value returned by [block]. + */ +@OptIn(ExperimentalContracts::class) +internal fun Reader.useLines( + filterDebuggerPrompt: Boolean, + block: (Sequence) -> T, +): T { + contract { + callsInPlace(block, EXACTLY_ONCE) + } + + return when { + filterDebuggerPrompt -> { + /* + * Transform the line consumer. + */ + { lines -> + lines.filterNot(String::isDebuggerPrompt).let(block) + } + } + + else -> block + }.let(this::useLines) +} + +private fun String.isDebuggerPrompt(printIfTrue: Boolean = true): Boolean { + val isDebuggerPrompt = debuggerPromptPrefixes.any { prefix -> + this.startsWith(prefix) + } + if (isDebuggerPrompt && printIfTrue) { + /* + * Print the prompt to the standard out, + * so that the IDE can attach to the debugger. + */ + @Suppress("DEBUG_PRINT") + println(this) + } + return isDebuggerPrompt +} /** * This utility function lets you run arbitrary code on every node of given [code]. @@ -41,7 +110,7 @@ internal fun T?.assertNotNull(lazyFailureMessage: () -> String = { "Expectin * @param applyToNode Function to be called on each AST node, should increment counter if assert is called */ @Suppress("TYPE_ALIAS") -internal fun applyToCode(code: String, +internal fun applyToCode(@Language("kotlin") code: String, expectedAsserts: Int, applyToNode: (node: ASTNode, counter: AtomicInteger) -> Unit ) { diff --git a/diktat-ruleset/src/test/kotlin/org/cqfn/diktat/ruleset/smoke/DiktatSaveSmokeTest.kt b/diktat-ruleset/src/test/kotlin/org/cqfn/diktat/ruleset/smoke/DiktatSaveSmokeTest.kt index 005a17077a..3b82f5dcc9 100644 --- a/diktat-ruleset/src/test/kotlin/org/cqfn/diktat/ruleset/smoke/DiktatSaveSmokeTest.kt +++ b/diktat-ruleset/src/test/kotlin/org/cqfn/diktat/ruleset/smoke/DiktatSaveSmokeTest.kt @@ -1,15 +1,16 @@ package org.cqfn.diktat.ruleset.smoke import org.cqfn.diktat.common.utils.loggerWithKtlintConfig +import org.cqfn.diktat.test.framework.processing.TestComparatorUnit +import org.cqfn.diktat.test.framework.util.checkForkedJavaHome import org.cqfn.diktat.test.framework.util.deleteIfExistsRecursively import org.cqfn.diktat.test.framework.util.deleteIfExistsSilently -import org.cqfn.diktat.test.framework.util.retry -import org.cqfn.diktat.util.isSameJavaHomeAs -import org.cqfn.diktat.util.prependPath +import org.cqfn.diktat.test.framework.util.inheritJavaHome +import org.cqfn.diktat.test.framework.util.isWindows +import org.cqfn.diktat.test.framework.util.temporaryDirectory import com.pinterest.ktlint.core.LintError import mu.KotlinLogging -import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.fail import org.assertj.core.api.SoftAssertions.assertSoftly import org.junit.jupiter.api.AfterAll @@ -21,17 +22,12 @@ import java.net.URL import java.nio.file.Path import kotlin.io.path.Path import kotlin.io.path.absolute -import kotlin.io.path.absolutePathString import kotlin.io.path.copyTo import kotlin.io.path.createDirectories import kotlin.io.path.div import kotlin.io.path.exists import kotlin.io.path.listDirectoryEntries -import kotlin.io.path.name -import kotlin.io.path.outputStream import kotlin.io.path.readText -import kotlin.io.path.relativeTo -import kotlin.system.measureNanoTime @DisabledOnOs(OS.MAC) class DiktatSaveSmokeTest : DiktatSmokeTestBase() { @@ -48,7 +44,7 @@ class DiktatSaveSmokeTest : DiktatSmokeTestBase() { override fun assertUnfixedLintErrors(lintErrorsConsumer: (List) -> Unit) = Unit /** - * @param testPath path to file with code that will be transformed by formatter, relative to [resourceFilePath] + * @param testPath path to file with code that will be transformed by formatter, relative to [TestComparatorUnit.resourceFilePath] * @param configFilePath path of diktat-analysis file */ @Suppress("TOO_LONG_FUNCTION") @@ -77,9 +73,7 @@ class DiktatSaveSmokeTest : DiktatSmokeTestBase() { /* * Inherit JAVA_HOME for the child process. */ - val javaHome = System.getProperty("java.home") - environment()["JAVA_HOME"] = javaHome - prependPath(Path(javaHome) / "bin") + inheritJavaHome() /* * On Windows, ktlint is often unable to relativize paths @@ -88,12 +82,8 @@ class DiktatSaveSmokeTest : DiktatSmokeTestBase() { * So let's force the temporary directory to be the * sub-directory of the project root. */ - if (System.getProperty("os.name").startsWith("Windows")) { - val tempDirectory = baseDirectoryPath / ".save-cli" - tempDirectory.createDirectories() - val tempDirectoryPath = tempDirectory.absolutePathString() - environment()["TMP"] = tempDirectoryPath - environment()["TEMP"] = tempDirectoryPath + if (System.getProperty("os.name").isWindows()) { + temporaryDirectory(baseDirectoryPath / WINDOWS_TEMP_DIRECTORY) } } @@ -118,29 +108,32 @@ class DiktatSaveSmokeTest : DiktatSmokeTestBase() { } /** - * @param testPath path to file with code that will be transformed by formatter, relative to [resourceFilePath] + * @param testPath path to file with code that will be transformed by formatter, relative to [TestComparatorUnit.resourceFilePath] * @return ProcessBuilder */ private fun createProcessBuilderWithCmd(testPath: String): ProcessBuilder { val savePath = "$BASE_DIRECTORY/${getSaveForCurrentOs()}" - - val systemName = System.getProperty("os.name") - val result = when { - systemName.startsWith("Linux", ignoreCase = true) || systemName.startsWith("Mac", ignoreCase = true) -> - ProcessBuilder("sh", "-c", "chmod 777 $savePath ; ./$savePath $BASE_DIRECTORY/src/main/kotlin $testPath --log all") - else -> ProcessBuilder(savePath, "$BASE_DIRECTORY/src/main/kotlin", testPath, "--log", "all") + val saveArgs = arrayOf( + "$BASE_DIRECTORY/src/main/kotlin", + testPath, + "--log", + "all" + ) + + return when { + System.getProperty("os.name").isWindows() -> arrayOf(savePath, *saveArgs) + else -> arrayOf("sh", "-c", "chmod 777 $savePath ; ./$savePath ${saveArgs.joinToString(" ")}") + }.let { args -> + ProcessBuilder(*args) } - return result } companion object { @Suppress("EMPTY_BLOCK_STRUCTURE_ERROR") private val logger = KotlinLogging.loggerWithKtlintConfig { } private const val BASE_DIRECTORY = "src/test/resources/test/smoke" - private const val BUILD_DIRECTORY = "build/libs" - private const val FAT_JAR_GLOB = "diktat-*.jar" - private const val KTLINT_VERSION = "0.46.1" private const val SAVE_VERSION: String = "0.3.4" + private const val WINDOWS_TEMP_DIRECTORY = ".save-cli" private val baseDirectoryPath = Path(BASE_DIRECTORY).absolute() private fun getSaveForCurrentOs(): String { @@ -149,95 +142,66 @@ class DiktatSaveSmokeTest : DiktatSmokeTestBase() { return when { osName.startsWith("Linux", ignoreCase = true) -> "save-$SAVE_VERSION-linuxX64.kexe" osName.startsWith("Mac", ignoreCase = true) -> "save-$SAVE_VERSION-macosX64.kexe" - osName.startsWith("Windows", ignoreCase = true) -> "save-$SAVE_VERSION-mingwX64.exe" + osName.isWindows() -> "save-$SAVE_VERSION-mingwX64.exe" else -> fail("SAVE doesn't support $osName (version ${System.getProperty("os.version")})") } } - @Suppress("FLOAT_IN_ACCURATE_CALCULATIONS") - private fun downloadFile(from: URL, to: Path) { - logger.info { - "Downloading $from to ${to.relativeTo(baseDirectoryPath)}..." - } - - val attempts = 5 - - val lazyDefault: (Throwable) -> Unit = { error -> - fail("Failure downloading $from after $attempts attempt(s)", error) - } - - retry(attempts, lazyDefault = lazyDefault) { - from.openStream().use { source -> - to.outputStream().use { target -> - val bytesCopied: Long - val timeNanos = measureNanoTime { - bytesCopied = source.copyTo(target) - } - logger.info { - "$bytesCopied byte(s) copied in ${timeNanos / 1000 / 1e3} ms." - } - } - } - } - } + private fun downloadFile(from: URL, to: Path) = downloadFile(from, to, baseDirectoryPath) @BeforeAll @JvmStatic - @Suppress("AVOID_NULL_CHECKS") internal fun beforeAll() { - val forkedJavaHome = System.getenv("JAVA_HOME") - if (forkedJavaHome != null) { - val javaHome = System.getProperty("java.home") - if (javaHome != null && !Path(javaHome).isSameJavaHomeAs(Path(forkedJavaHome))) { - logger.warn { - "Current JDK home is $javaHome. Forked tests may use a different JDK at $forkedJavaHome." - } - } - logger.warn { - "Make sure JAVA_HOME ($forkedJavaHome) points to a Java 8 or Java 11 home. Java 17 is not yet supported." + assertSoftly { softly -> + checkForkedJavaHome() + + logger.info { + "The base directory for the smoke test is $baseDirectoryPath." } - } - logger.info { - "The base directory for the smoke test is $baseDirectoryPath." + /* + * The fat JAR should reside in the same directory as `ktlint` and + * `save*` and be named `diktat.jar` + * (see `diktat-rules/src/test/resources/test/smoke/save.toml`). + */ + val buildDirectory = Path(BUILD_DIRECTORY) + softly.assertThat(buildDirectory) + .isDirectory + val diktatFrom = buildDirectory + .takeIf(Path::exists) + ?.listDirectoryEntries(DIKTAT_FAT_JAR_GLOB) + ?.singleOrNull() + softly.assertThat(diktatFrom) + .describedAs(diktatFrom?.toString() ?: "$BUILD_DIRECTORY/$DIKTAT_FAT_JAR_GLOB") + .isNotNull + .isRegularFile + + val diktat = baseDirectoryPath / DIKTAT_FAT_JAR + val save = baseDirectoryPath / getSaveForCurrentOs() + val ktlint = baseDirectoryPath / KTLINT_FAT_JAR + + downloadFile(URL("https://github.com/saveourtool/save-cli/releases/download/v$SAVE_VERSION/${getSaveForCurrentOs()}"), save) + downloadFile(URL("https://github.com/pinterest/ktlint/releases/download/$KTLINT_VERSION/ktlint"), ktlint) + + diktatFrom?.copyTo(diktat, overwrite = true) } - - /* - * The fat JAR should reside in the same directory as `ktlint` and - * `save*` and be named `diktat.jar` - * (see `diktat-rules/src/test/resources/test/smoke/save.toml`). - */ - val diktatFrom = Path(BUILD_DIRECTORY) - .takeIf(Path::exists) - ?.listDirectoryEntries(FAT_JAR_GLOB) - ?.singleOrNull() - assertThat(diktatFrom) - .describedAs(diktatFrom?.toString() ?: "$BUILD_DIRECTORY/$FAT_JAR_GLOB") - .isNotNull - .isRegularFile - - val diktat = baseDirectoryPath / "diktat.jar" - val save = baseDirectoryPath / getSaveForCurrentOs() - val ktlint = baseDirectoryPath / "ktlint" - - downloadFile(URL("https://github.com/saveourtool/save-cli/releases/download/v$SAVE_VERSION/${getSaveForCurrentOs()}"), save) - downloadFile(URL("https://github.com/pinterest/ktlint/releases/download/$KTLINT_VERSION/ktlint"), ktlint) - - diktatFrom?.copyTo(diktat, overwrite = true) } @AfterAll @JvmStatic internal fun afterAll() { - val diktat = baseDirectoryPath / "diktat.jar" + val diktat = baseDirectoryPath / DIKTAT_FAT_JAR val save = baseDirectoryPath / getSaveForCurrentOs() - val ktlint = baseDirectoryPath / "ktlint" - val tempDirectory = baseDirectoryPath / ".save-cli" + val ktlint = baseDirectoryPath / KTLINT_FAT_JAR diktat.deleteIfExistsSilently() ktlint.deleteIfExistsSilently() save.deleteIfExistsSilently() - tempDirectory.deleteIfExistsRecursively() + + if (System.getProperty("os.name").isWindows()) { + val tempDirectory = baseDirectoryPath / WINDOWS_TEMP_DIRECTORY + tempDirectory.deleteIfExistsRecursively() + } } } } diff --git a/diktat-ruleset/src/test/kotlin/org/cqfn/diktat/ruleset/smoke/DiktatSmokeTestUtils.kt b/diktat-ruleset/src/test/kotlin/org/cqfn/diktat/ruleset/smoke/DiktatSmokeTestUtils.kt new file mode 100644 index 0000000000..a4c1397768 --- /dev/null +++ b/diktat-ruleset/src/test/kotlin/org/cqfn/diktat/ruleset/smoke/DiktatSmokeTestUtils.kt @@ -0,0 +1,62 @@ +@file:Suppress("HEADER_MISSING_IN_NON_SINGLE_CLASS_FILE") + +package org.cqfn.diktat.ruleset.smoke + +import org.cqfn.diktat.common.utils.loggerWithKtlintConfig +import org.cqfn.diktat.test.framework.util.retry +import mu.KotlinLogging +import org.assertj.core.api.Assertions.fail +import java.net.URL +import java.nio.file.Path +import kotlin.io.path.outputStream +import kotlin.io.path.relativeToOrSelf +import kotlin.system.measureNanoTime + +internal const val BUILD_DIRECTORY = "build/libs" +internal const val DIKTAT_FAT_JAR = "diktat.jar" +internal const val DIKTAT_FAT_JAR_GLOB = "diktat-*.jar" +internal const val KTLINT_FAT_JAR = "ktlint" +internal const val KTLINT_VERSION = "0.46.1" + +@Suppress("EMPTY_BLOCK_STRUCTURE_ERROR") +private val logger = KotlinLogging.loggerWithKtlintConfig { } + +/** + * Downloads the file from a remote URL, retrying if necessary. + * + * @param from the remote URL to download from. + * @param to the target path. + * @param baseDirectory the directory against which [to] should be relativized + * if it's absolute. + */ +@Suppress("FLOAT_IN_ACCURATE_CALCULATIONS") +internal fun downloadFile( + from: URL, + to: Path, + baseDirectory: Path, +) { + logger.info { + "Downloading $from to ${to.relativeToOrSelf(baseDirectory)}..." + } + + @Suppress("MAGIC_NUMBER") + val attempts = 5 + + val lazyDefault: (Throwable) -> Unit = { error -> + fail("Failure downloading $from after $attempts attempt(s)", error) + } + + retry(attempts, lazyDefault = lazyDefault) { + from.openStream().use { source -> + to.outputStream().use { target -> + val bytesCopied: Long + val timeNanos = measureNanoTime { + bytesCopied = source.copyTo(target) + } + logger.info { + "$bytesCopied byte(s) copied in ${timeNanos / 1000 / 1e3} ms." + } + } + } + } +} diff --git a/diktat-test-framework/src/main/kotlin/org/cqfn/diktat/test/framework/util/TestUtils.kt b/diktat-test-framework/src/main/kotlin/org/cqfn/diktat/test/framework/util/TestUtils.kt index 564297cf38..bd0f40ad2e 100644 --- a/diktat-test-framework/src/main/kotlin/org/cqfn/diktat/test/framework/util/TestUtils.kt +++ b/diktat-test-framework/src/main/kotlin/org/cqfn/diktat/test/framework/util/TestUtils.kt @@ -8,21 +8,32 @@ import org.cqfn.diktat.common.utils.loggerWithKtlintConfig import mu.KotlinLogging +import java.io.File import java.io.IOException import java.nio.file.FileVisitResult import java.nio.file.FileVisitResult.CONTINUE +import java.nio.file.Files import java.nio.file.Files.walkFileTree import java.nio.file.NoSuchFileException import java.nio.file.Path import java.nio.file.SimpleFileVisitor import java.nio.file.attribute.BasicFileAttributes +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract +import kotlin.io.path.Path import kotlin.io.path.absolute +import kotlin.io.path.absolutePathString +import kotlin.io.path.bufferedReader +import kotlin.io.path.createDirectories import kotlin.io.path.deleteExisting import kotlin.io.path.deleteIfExists +import kotlin.io.path.div +import kotlin.io.path.isDirectory +import kotlin.io.path.isSameFileAs @Suppress("EMPTY_BLOCK_STRUCTURE_ERROR") -private val log = KotlinLogging.loggerWithKtlintConfig {} +private val logger = KotlinLogging.loggerWithKtlintConfig {} /** * Deletes the file if it exists, retrying as necessary if the file is @@ -49,7 +60,7 @@ fun Path.deleteIfExistsSilently() { } if (!deleted) { - log.warn { + logger.warn { "File \"${absolute()}\" not deleted after $attempts attempt(s)." } } @@ -91,6 +102,179 @@ fun Path.deleteIfExistsRecursively(): Boolean = false } +/** + * @receiver the 1st operand. + * @param other the 2nd operand. + * @return `true` if, and only if, the two paths locate the same `JAVA_HOME`. + */ +fun Path.isSameJavaHomeAs(other: Path): Boolean = + isDirectory() && + (isSameFileAsSafe(other) || + resolve("jre").isSameFileAsSafe(other) || + other.resolve("jre").isSameFileAsSafe(this)) + +/** + * The same as [Path.isSameFileAs], but doesn't throw any [NoSuchFileException] + * if either of the operands doesn't exist. + * + * @receiver the 1st operand. + * @param other the 2nd operand. + * @return `true` if, and only if, the two paths locate the same file. + * @see Path.isSameFileAs + */ +fun Path.isSameFileAsSafe(other: Path): Boolean = + try { + isSameFileAs(other) + } catch (_: NoSuchFileException) { + false + } + +/** + * Requests that this file or directory be deleted when the JVM terminates. + * + * Does nothing if this [Path] is not associated with the default provider. + * + * @receiver a regular file or a directory. + * @return this [Path]. + */ +fun Path.tryToDeleteOnExit(): Path { + try { + toFile().deleteOnExit() + } catch (_: UnsupportedOperationException) { + /* + * Ignore. + */ + } + + return this +} + +/** + * Resets any permissions which might otherwise prevent from reading or writing + * this file or directory, or traversing this directory. + * + * @receiver a regular file or a directory. + */ +fun Path.resetPermissions() { + toFile().apply { + setReadable(true) + setWritable(true) + + if (isDirectory) { + setExecutable(true) + } + } +} + +/** + * Returns a sequence containing only files whose content (the first + * [linesToRead] lines) matches [lineRegex]. + * + * The operation is _intermediate_ and _stateless_. + * + * @receiver a sequence of regular files. + * @param linesToRead the number of lines to read (at most). + * @param lineRegex the regular expression to be applied to each line until a + * match is found (i.e. the line is found which _contains_ [lineRegex]). + * @return the filtered sequence. + */ +fun Sequence.filterContentMatches(linesToRead: Int, lineRegex: Regex): Sequence = + filter { file -> + file.bufferedReader().useLines { lines -> + lines.take(linesToRead).any { line -> + line.contains(lineRegex) + } + } + } + +/** + * Prepends the `PATH` of this process builder with [pathEntry]. + * + * @param pathEntry the entry to be prepended to the `PATH`. + */ +fun ProcessBuilder.prependPath(pathEntry: Path) { + require(pathEntry.isDirectory()) { + "$pathEntry is not a directory" + } + + val environment = environment() + + val defaultPathKey = "PATH" + val defaultWindowsPathKey = "Path" + + val pathKey = when { + /*- + * Keys of the Windows environment are case-insensitive ("PATH" == "Path"). + * Keys of the Java interface to the environment are not ("PATH" != "Path"). + * This is an attempt to work around the inconsistency. + */ + System.getProperty("os.name").isWindows() -> environment.keys.firstOrNull { key -> + key.equals(defaultPathKey, ignoreCase = true) + } ?: defaultWindowsPathKey + + else -> defaultPathKey + } + + val pathSeparator = File.pathSeparatorChar + val oldPath = environment[pathKey] + + val newPath = when { + oldPath.isNullOrEmpty() -> pathEntry.toString() + else -> "$pathEntry$pathSeparator$oldPath" + } + + environment[pathKey] = newPath +} + +/** + * Inherits the home of the current JVM (by setting `JAVA_HOME` and adding it to + * the `PATH`) for the children of this process builder. + */ +fun ProcessBuilder.inheritJavaHome() { + val javaHome = System.getProperty("java.home") + environment()["JAVA_HOME"] = javaHome + prependPath(Path(javaHome) / "bin") +} + +/** + * Changes the temporary directory for the children of this process builder. + * + * @param temporaryDirectory the new temporary directory (created automatically, + * scheduled for removal at JVM exit). + */ +fun ProcessBuilder.temporaryDirectory(temporaryDirectory: Path) { + temporaryDirectory.createDirectories().tryToDeleteOnExit() + + /* + * On UNIX, TMPDIR is the canonical name + */ + val environmentVariables: Sequence = when { + System.getProperty("os.name").isWindows() -> sequenceOf("TMP", "TEMP") + else -> sequenceOf("TMPDIR") + } + + val environment = environment() + + val value = temporaryDirectory.absolutePathString() + environmentVariables.forEach { name -> + environment[name] = value + } +} + +/** + * @receiver the value of `os.name` system property. + * @return `true` if the value of `os.name` system property starts with + * "Windows", `false` otherwise. + */ +@OptIn(ExperimentalContracts::class) +fun String?.isWindows(): Boolean { + contract { + returns(true) implies (this@isWindows != null) + } + + return this != null && startsWith("Windows") +} + /** * Retries the execution of the [block]. * @@ -129,3 +313,23 @@ fun retry( return lazyDefault(lastError ?: Exception("The block was never executed")) } + +/** + * Checks whether the current JVM's home matches the `JAVA_HOME` environment + * variable. + */ +@Suppress("AVOID_NULL_CHECKS") +fun checkForkedJavaHome() { + val forkedJavaHome = System.getenv("JAVA_HOME") + if (forkedJavaHome != null) { + val javaHome = System.getProperty("java.home") + if (javaHome != null && !Path(javaHome).isSameJavaHomeAs(Path(forkedJavaHome))) { + logger.warn { + "Current JDK home is $javaHome. Forked tests may use a different JDK at $forkedJavaHome." + } + } + logger.warn { + "Make sure JAVA_HOME ($forkedJavaHome) points to a Java 8 or Java 11 home. Java 17 is not yet supported." + } + } +} diff --git a/info/buildSrc/src/main/kotlin/org/cqfn/diktat/generation/docs/GenerationDocs.kt b/info/buildSrc/src/main/kotlin/org/cqfn/diktat/generation/docs/GenerationDocs.kt index 483d8973d8..8ba319eb8e 100644 --- a/info/buildSrc/src/main/kotlin/org/cqfn/diktat/generation/docs/GenerationDocs.kt +++ b/info/buildSrc/src/main/kotlin/org/cqfn/diktat/generation/docs/GenerationDocs.kt @@ -15,7 +15,6 @@ import java.io.PrintWriter "MagicNumber", "ComplexMethod", "NestedBlockDepth", - "WRONG_INDENTATION", "TOO_LONG_FUNCTION") fun generateCodeStyle(guideDir: File, wpDir: File) { val file = File(guideDir, "diktat-coding-convention.md") @@ -207,7 +206,6 @@ private fun handleHyperlinks(line: String): String { return correctedString } -@Suppress("WRONG_INDENTATION") private fun findBoldOrItalicText(regex: Regex, line: String, type: FindType): String { @@ -228,4 +226,4 @@ private fun findBoldOrItalicText(regex: Regex, return correctedLine } -private fun String.getFirstNumber() = trimStart().takeWhile { it.isDigit() || it == '.' } \ No newline at end of file +private fun String.getFirstNumber() = trimStart().takeWhile { it.isDigit() || it == '.' }