// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package org.jetbrains.idea.devkit.commit import com.intellij.openapi.application.PathManager import org.junit.Test import org.junit.experimental.runners.Enclosed import org.junit.runner.RunWith import org.junit.runners.Parameterized import org.junit.runners.Parameterized.Parameter import java.nio.file.Path import java.nio.file.Paths import kotlin.io.path.div import kotlin.io.path.exists import kotlin.io.path.readText @RunWith(Enclosed::class) class IntelliJMonorepoPrePushHandlerTest { @RunWith(Parameterized::class) class FilesBelongToPlatform { companion object { @JvmStatic @Parameterized.Parameters(name = "{0}") fun provideTestParameters(): Iterable> { return listOf( listOf("${COMMUNITY_PLATFORM}/File.kt"), listOf( "${COMMUNITY_PLATFORM}/File.kt", "root.txt", ), listOf( "${COMMUNITY_PLATFORM}/File.kt", "${COMMUNITY}/plugins/stuff.xml", "not-important-dir-A/whatever", "not-important-dir-B/whatever" ), listOf("unrelated/$COMMUNITY_PLATFORM/File.kt"), ) } } @Parameter lateinit var files: List @Test fun testThatChecksForFileSet() { val filesSet = files.asSequence().map { pathAt(it) } assert(prePushHandler.containSources(filesSet)) { "The following set of files doesn't trigger the check: $filesSet" } } } @RunWith(Parameterized::class) class FilesDoNotBelongToPlatform { companion object { @JvmStatic @Parameterized.Parameters(name = "{0}") fun provideTestParameters(): Iterable> { return listOf( listOf("${COMMUNITY_PLATFORM}/README.md"), listOf("${COMMUNITY_PLATFORM}/sub/sub.iml"), listOf("${COMMUNITY}/File.kt"), listOf("${COMMUNITY}/README.md"), listOf("${COMMUNITY}/sub/sub.iml"), listOf( "${COMMUNITY}/plugins/stuff.xml", "not-important-dir-A/whatever", "not-important-dir-B/whatever", ), ) } } @Parameter lateinit var files: List @Test fun testThatIgnoresFileSet() { val filesSet = files.asSequence().map { pathAt(it) } assert(!prePushHandler.containSources(filesSet)) { "The following set of files triggered the check: $filesSet" } } } @RunWith(Parameterized::class) class ValidCommitMessages { companion object { @JvmStatic @Parameterized.Parameters(name = "{0}") fun provideTestParameters(): Iterable { return listOf( "IJPL-1", "A-1", "IJPL-123", "(IJPL-123)", "[IJPL-123]", "{IJPL-123}", "'IJPL-123'", "`IJPL-123`", "\"IJPL-123\"", "IDEA-123", "(IDEA-123)", "[IDEA-123]", "{IDEA-123}", "'IDEA-123'", "`IDEA-123`", "\"IDEA-123\"", "IJPL-42", "KTIJ-123 ", " KTIJ-123", " KTIJ-123 ", "IDEA-123 ", " IDEA-123", " IDEA-123 ", "IJPL-123 header", "Header IJPL-123", "[sub system 123] docs: fix typo", "Header IJPL-123 header", "IJPL-123 header", "Header IJPL-123", "Header IJPL-123 header", """ Header IJPL-123 header Body-line-1 Body-line-N """.trimIndent(), """ Header IJPL-123 header Body-line-1 Body-line-N """.trimIndent(), """ Header IJPL-123 header Body-line-1 Body-line-N ^IJPL-123 fixed """.trimIndent(), """ Header IJPL-123 header Body-line-1 Body-line-N ^IDEA-123 fixed """.trimIndent(), """ Header IJPL-42 header Body-line-1 Body-line-N ^IDEA-123 fixed """.trimIndent(), """ Header Body-line-1 Body-line-N ^IJPL-123 fixed """.trimIndent(), """ Header Body-line-1 Body-line-N ^IDEA-123 fixed """.trimIndent(), """ Header Body-line-1 Body-line-N #KTIJ-123 fixed """.trimIndent(), """ Header Body-line-1 Body-line-N #IDEA-123 fixed """.trimIndent(), """ Header Body-line-1 Body-line-N Relates to #KTIJ-123 """.trimIndent(), """ Header Body-line-1 Body-line-N Relates to #IDEA-123 """.trimIndent(), """ Header Body-line-1 Body-line-N ^WHATEVER-123 fixed """.trimIndent(), """ Header Body-line-1 Body-line-N #WHATEVER-123 fixed """.trimIndent(), """ Header Body-line-1 Body-line-N Relates to #WHATEVER-123 """.trimIndent(), """ [subsystem] refactoring: rename a to b Explains why it is needed. """.trimIndent(), "test thing", "test: thing", "[test] thing", "Test thing", "tests thing", "tests: thing", "[tests] thing", "cleanup thing", "cleanup: thing", "[cleanup] thing", "Cleanup stuff", "clean up thing", "clean up: thing", "[clean up] thing", "Clean Up stuff", "docs thing", "docs: thing", "[docs] thing", "Docs thing", "doc thing", "doc: thing", "[doc] thing", "Doc very much", "typo thing", "typo: thing", "[typo] thing", "format thing", "format: thing", "[format] thing", "style thing", "style: thing", "[style] thing", "refactor this thing", "refactor: this thing", "WIP", "[WIP] do stuff", "Add thingies WIP", "wip", "(wip) hoho", "wip: haha", """ WIP Some additional remarks. """.trimIndent(), "Cleanup (reason)", "[subsystem][tests] new tests", "[subsystem] docs: fix typo", "[smth][refactoring] do stuff", "[testFramework] whatever", "[test framework] whatever", "test framework", "Rename .java to .kt" ) } } @Parameter lateinit var commitMessage: String @Test fun testThatCommitMessageIsValid() { assert(prePushHandler.isCommitMessageCorrect(commitMessage)) { "The following commit message was considered invalid: $commitMessage" } } } @RunWith(Parameterized::class) class InvalidCommitMessages { companion object { @JvmStatic @Parameterized.Parameters(name = "{0}") fun provideTestParameters(): Iterable { return listOf( "", "IJPL", "IJPL-", "IJPL-one", "IJPL 1", "IJPL111", "Text IJPL Text", "something-something", "2-fold", "AB-RA-CA-DA-BRA-1", """" Header Body-line-1 Body-line-N """.trimIndent(), """ No explanation Test added """.trimIndent(), """ No explanation WIP """.trimIndent(), "test", "test:", "[test]", "test ", "add test", "drop test", "tests", "tests:", "[tests]", "tests ", "add tests", "drop tests", "cleanup", "cleanup:", "[cleanup]", "cleanup ", "do cleanup", "clean up", "clean up:", "[clean up]", "clean up ", "do clean up", "docs", "docs:", "[docs]", "docs ", "add docs", "doc", "doc:", "[doc]", "doc ", "add doc", "typo", "typo:", "[typo]", "typo ", "fix typo", "format", "format:", "[format]", "fix format", "fix formatting", "style", "style:", "[style]", "fix style", "stuff", "stuff:", "[stuff]", "very important stuff", "refactor", "refactor:", "[refactor]", "refactoring", "refactoring:", "[refactoring]", "wipe", "swipe", "wipe the db", "[subsystem] Do very important stuff very very important", "platform: add 'thing' here and there", "subsystem tests: use junit5 instead of junit4", "subsystem: cleanup", "cool: 5 new tests", "Fix test", "[subsystem] docs", "null", "IJ-CR-160532 do not highlight parser errors twice for java", "IJ-MR-123", "IJ-CR-0", ) } } @Parameter lateinit var commitMessage: String @Test fun testThatCommitMessageIsValid() { assert(!prePushHandler.isCommitMessageCorrect(commitMessage)) { "The following commit message was considered as valid: $commitMessage" } } } class ContributingMdCommitMessagesAreValid { @Test fun testCommitMessagesFromContributingMd() { val contributingFile = Path.of(PathManager.getCommunityHomePath()) / "CONTRIBUTING.md" assert(contributingFile.exists()) { "CONTRIBUTING.md file not found at expected location" } val content = contributingFile.readText() val commitMessages = extractCommitMessagesFromMarkdown(content) assert(commitMessages.isNotEmpty()) { "No commit messages found in CONTRIBUTING.md" } commitMessages.forEach { commitMessage -> assert(prePushHandler.isCommitMessageCorrect(commitMessage)) { "Commit message from CONTRIBUTING.md is invalid: '$commitMessage'" } } } private fun extractCommitMessagesFromMarkdown(content: String): List { val codeBlockRegex = Regex("```(.*?)```", RegexOption.DOT_MATCHES_ALL) val commitMessages = mutableListOf() codeBlockRegex.findAll(content).forEach { match -> val codeBlock = match.groupValues[1].trim() if (codeBlock.contains("[<") && codeBlock.contains(">]")) { return@forEach } if (codeBlock.isNotEmpty()) { commitMessages.add(codeBlock) } } return commitMessages } } } private val prePushHandler = IntelliJMonorepoPrePushHandler() private val tempDir: String = System.getProperty("java.io.tmpdir") private const val COMMUNITY_PLATFORM = "community/platform/" private const val COMMUNITY = "community/" private fun pathAt(path: String): Path { return Paths.get(tempDir, *path.split("/").filterNot { it.isEmpty() }.toTypedArray()) }