From 6f1ef96f7d99169d317a5464b857e8dd2f303459 Mon Sep 17 00:00:00 2001 From: Niels Basjes Date: Mon, 18 May 2026 21:24:10 +0200 Subject: [PATCH 1/3] test: All tests are in Kotlin --- README.md | 4 +- codeowners-reader/pom.xml | 4 +- .../main/kotlin/nl/basjes/codeowners/Rule.kt | 3 +- .../kotlin/nl/basjes/codeowners/Section.kt | 3 +- .../nl/basjes/codeowners/TestCodeOwners.java | 187 ----- .../nl/basjes/codeowners/TestCodeOwners.kt | 208 ++++++ .../codeowners/TestCodeOwnersEdgeCases.java | 58 -- .../codeowners/TestCodeOwnersEdgeCases.kt | 57 ++ .../codeowners/TestCodeOwnersEmail.java | 99 --- .../basjes/codeowners/TestCodeOwnersEmail.kt | 101 +++ .../codeowners/TestCodeOwnersGetters.java | 158 ----- .../codeowners/TestCodeOwnersGetters.kt | 149 ++++ ...ersGithub.java => TestCodeOwnersGithub.kt} | 224 +++--- .../codeowners/TestCodeOwnersGitlab.java | 527 -------------- .../basjes/codeowners/TestCodeOwnersGitlab.kt | 577 +++++++++++++++ .../codeowners/TestCodeOwnersOrdering.java | 83 --- .../codeowners/TestCodeOwnersOrdering.kt | 89 +++ .../TestTrailingGlobstarPattern.java | 81 --- .../codeowners/TestTrailingGlobstarPattern.kt | 78 ++ .../nl/basjes/codeowners/TestUtils.java | 96 --- .../kotlin/nl/basjes/codeowners/TestUtils.kt | 99 +++ codeowners-validator/pom.xml | 4 +- .../validator/gitlab/GitlabConfiguration.kt | 2 +- .../validator/gitlab/GitlabProjectMembers.kt | 27 +- .../validator/utils/ProblemTable.kt | 19 +- .../validator/TestCodeOwnersValidator.java | 174 ----- .../validator/TestCodeOwnersValidator.kt | 178 +++++ .../gitlab/TestGitlabConfiguration.java | 248 ------- .../gitlab/TestGitlabConfiguration.kt | 287 ++++++++ .../TestGitlabConfigurationAccessToken.java | 125 ---- .../TestGitlabConfigurationAccessToken.kt | 138 ++++ .../TestGitlabConfigurationProjectId.java | 208 ------ .../TestGitlabConfigurationProjectId.kt | 220 ++++++ .../TestGitlabConfigurationServerUrl.java | 209 ------ .../TestGitlabConfigurationServerUrl.kt | 221 ++++++ .../validator/gitlab/TestGitlabUsers.java | 523 -------------- .../validator/gitlab/TestGitlabUsers.kt | 617 ++++++++++++++++ .../validator/utils/{Line.java => Line.kt} | 25 +- .../validator/utils/TestLogger.java | 152 ---- .../codeowners/validator/utils/TestLogger.kt | 140 ++++ .../validator/utils/TestProblemTable.java | 64 -- .../validator/utils/TestProblemTable.kt | 140 ++++ .../validator/utils/TestStringTable.java | 55 -- .../validator/utils/TestStringTable.kt | 55 ++ enforcer/pom.xml | 4 +- .../codeowners/CodeOwnersEnforcerRule.kt | 14 +- gitignore-reader/pom.xml | 4 +- ...{TestBugreports.java => TestBugreports.kt} | 102 +-- .../nl/basjes/gitignore/TestGitIgnore.java | 637 ----------------- .../nl/basjes/gitignore/TestGitIgnore.kt | 610 ++++++++++++++++ .../basjes/gitignore/TestGitIgnoreFiles.java | 582 --------------- .../nl/basjes/gitignore/TestGitIgnoreFiles.kt | 664 ++++++++++++++++++ .../nl/basjes/gitignore/TestJetBrains.java | 228 ------ .../nl/basjes/gitignore/TestJetBrains.kt | 230 ++++++ .../kotlin/nl/basjes/gitignore/TestUtils.java | 121 ---- .../kotlin/nl/basjes/gitignore/TestUtils.kt | 141 ++++ ...java => EnvironmentVariableUtilsFacade.kt} | 14 +- pom.xml | 9 +- 58 files changed, 5219 insertions(+), 4857 deletions(-) delete mode 100644 codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwners.java create mode 100644 codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwners.kt delete mode 100644 codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwnersEdgeCases.java create mode 100644 codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwnersEdgeCases.kt delete mode 100644 codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwnersEmail.java create mode 100644 codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwnersEmail.kt delete mode 100644 codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwnersGetters.java create mode 100644 codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwnersGetters.kt rename codeowners-reader/src/test/kotlin/nl/basjes/codeowners/{TestCodeOwnersGithub.java => TestCodeOwnersGithub.kt} (56%) delete mode 100644 codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwnersGitlab.java create mode 100644 codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwnersGitlab.kt delete mode 100644 codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwnersOrdering.java create mode 100644 codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwnersOrdering.kt delete mode 100644 codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestTrailingGlobstarPattern.java create mode 100644 codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestTrailingGlobstarPattern.kt delete mode 100644 codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestUtils.java create mode 100644 codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestUtils.kt delete mode 100644 codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/TestCodeOwnersValidator.java create mode 100644 codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/TestCodeOwnersValidator.kt delete mode 100644 codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/gitlab/TestGitlabConfiguration.java create mode 100644 codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/gitlab/TestGitlabConfiguration.kt delete mode 100644 codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/gitlab/TestGitlabConfigurationAccessToken.java create mode 100644 codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/gitlab/TestGitlabConfigurationAccessToken.kt delete mode 100644 codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/gitlab/TestGitlabConfigurationProjectId.java create mode 100644 codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/gitlab/TestGitlabConfigurationProjectId.kt delete mode 100644 codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/gitlab/TestGitlabConfigurationServerUrl.java create mode 100644 codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/gitlab/TestGitlabConfigurationServerUrl.kt delete mode 100644 codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/gitlab/TestGitlabUsers.java create mode 100644 codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/gitlab/TestGitlabUsers.kt rename codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/utils/{Line.java => Line.kt} (62%) delete mode 100644 codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/utils/TestLogger.java create mode 100644 codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/utils/TestLogger.kt delete mode 100644 codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/utils/TestProblemTable.java create mode 100644 codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/utils/TestProblemTable.kt delete mode 100644 codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/utils/TestStringTable.java create mode 100644 codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/utils/TestStringTable.kt rename gitignore-reader/src/test/kotlin/nl/basjes/gitignore/{TestBugreports.java => TestBugreports.kt} (56%) delete mode 100644 gitignore-reader/src/test/kotlin/nl/basjes/gitignore/TestGitIgnore.java create mode 100644 gitignore-reader/src/test/kotlin/nl/basjes/gitignore/TestGitIgnore.kt delete mode 100644 gitignore-reader/src/test/kotlin/nl/basjes/gitignore/TestGitIgnoreFiles.java create mode 100644 gitignore-reader/src/test/kotlin/nl/basjes/gitignore/TestGitIgnoreFiles.kt delete mode 100644 gitignore-reader/src/test/kotlin/nl/basjes/gitignore/TestJetBrains.java create mode 100644 gitignore-reader/src/test/kotlin/nl/basjes/gitignore/TestJetBrains.kt delete mode 100644 gitignore-reader/src/test/kotlin/nl/basjes/gitignore/TestUtils.java create mode 100644 gitignore-reader/src/test/kotlin/nl/basjes/gitignore/TestUtils.kt rename gitignore-reader/src/test/kotlin/org/junitpioneer/jupiter/{EnvironmentVariableUtilsFacade.java => EnvironmentVariableUtilsFacade.kt} (73%) diff --git a/README.md b/README.md index ea2ec16..609eac5 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Reality: The syntax of these files can be tricky, and it is quite easy to write # Major update The upcoming 2.0.0 release has been rewritten into Kotlin. This allows for more exact checks on nullability and improves code quality. -Most of the API has not been changed so using this from Java should be a simple migration. +Most of the API has not been changed so using this from Java should be a simple migration. This new version needs JVM 11 or newer. # What is this 1) A Kotlin/Java library to read a CODEOWNERS file. @@ -27,7 +27,7 @@ Most of the API has not been changed so using this from Java should be a simple The intended goal is to make the build fail if the codeowners file does not cover all files and directories in the project. -- The libraries for the CODEOWNERS and gitignore files are usable in Java 8 and newer. +- The libraries for the CODEOWNERS and gitignore files are usable in Java 11 and newer. - The Maven Enforcer rule needs Java 11 or newer. # CodeOwners Enforcer rule diff --git a/codeowners-reader/pom.xml b/codeowners-reader/pom.xml index 086c274..edb60d5 100644 --- a/codeowners-reader/pom.xml +++ b/codeowners-reader/pom.xml @@ -52,8 +52,8 @@ - org.junit.jupiter - junit-jupiter-engine + org.jetbrains.kotlin + kotlin-test-junit5 test diff --git a/codeowners-reader/src/main/kotlin/nl/basjes/codeowners/Rule.kt b/codeowners-reader/src/main/kotlin/nl/basjes/codeowners/Rule.kt index 2f274e7..dd335a2 100644 --- a/codeowners-reader/src/main/kotlin/nl/basjes/codeowners/Rule.kt +++ b/codeowners-reader/src/main/kotlin/nl/basjes/codeowners/Rule.kt @@ -22,9 +22,8 @@ abstract class Rule( /** * @return The provided file expression used to build this rule */ - @JvmField val fileExpression: String + val fileExpression: String ) { - fun getFileExpression() = fileExpression /** * The Pattern which was constructed from the provided fileExpression */ diff --git a/codeowners-reader/src/main/kotlin/nl/basjes/codeowners/Section.kt b/codeowners-reader/src/main/kotlin/nl/basjes/codeowners/Section.kt index 5f27033..487377d 100644 --- a/codeowners-reader/src/main/kotlin/nl/basjes/codeowners/Section.kt +++ b/codeowners-reader/src/main/kotlin/nl/basjes/codeowners/Section.kt @@ -16,8 +16,7 @@ */ package nl.basjes.codeowners -class Section(@JvmField val name: String) { - fun getName() = name +class Section(val name: String) { var verbose = false set(verbose) { diff --git a/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwners.java b/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwners.java deleted file mode 100644 index 22b95e6..0000000 --- a/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwners.java +++ /dev/null @@ -1,187 +0,0 @@ -/* - * CodeOwners Tools - * Copyright (C) 2023-2026 Niels Basjes - * - * 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 - * - * https://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 nl.basjes.codeowners; - -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.File; -import java.io.IOException; -import java.net.URL; - -import static nl.basjes.codeowners.TestUtils.assertOwners; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -class TestCodeOwners { - - private static final Logger LOG = LoggerFactory.getLogger(TestCodeOwners.class); - - private void runChecks(CodeOwners codeOwners) { - assertOwners(codeOwners, "Foo.txt","@code","@multiple","@owners", "@dev-team"); - assertOwners(codeOwners, "Foo.rb","@ruby-owner", "@dev-team"); - assertOwners(codeOwners, "#file_with_pound.rb","@owner-file-with-pound", "@dev-team"); - } - - @Test - void testDotStarCase() { - CodeOwners codeOwners = new CodeOwners( - "/foo/.* @user1\n" + // Intended to match '/foo/.bar' NOT '/foo/' and NOT '/foo/foo/.bar' - "*.xml @user2\n" - ); -// codeOwners.setVerbose(true); -// LOG.info("CODEOWNERS:\n{}", codeOwners); - assertOwners(codeOwners, "/foo/.foo", "@user1"); - assertOwners(codeOwners, "/foo/.foo/bar", "@user1"); - assertOwners(codeOwners, "/foo/foo/.bar"); // No users - assertOwners(codeOwners, "/foo/xfoo"); // No users - assertOwners(codeOwners, "/foo/xfoo/bar"); - assertOwners(codeOwners, "/foo/.foo/bar.xml", "@user2"); - assertOwners(codeOwners, "/foo/foo"); // No users - assertOwners(codeOwners, "/foo/foo/bar.xml", "@user2"); - } - - @Test - void testMidStarCase() { - CodeOwners codeOwners = new CodeOwners( - "/tool-*/ @user1\n" + // Intended to match '/tool-library/bar.txt' - "*.xml @user2\n" - ); - codeOwners.setVerbose(true); - LOG.info("CODEOWNERS:\n{}", codeOwners); - assertOwners(codeOwners, "/tool-app/bar.txt", "@user1"); - assertOwners(codeOwners, "/tool-app/foo/bar.txt", "@user1"); - assertOwners(codeOwners, "/tool-app/bar.xml", "@user2"); - assertOwners(codeOwners, "/tool-app/foo/bar.xml", "@user2"); - assertOwners(codeOwners, "/bar.txt"); - assertOwners(codeOwners, "/bar.xml", "@user2"); - } - - @Test - void testSubDirectoriesCase() { - // # The `docs/*` pattern will match files like - // # `docs/getting-started.md` but not further nested files like - // # `docs/build-app/troubleshooting.md`. - CodeOwners codeOwners = new CodeOwners( - "/dir1/* @user1\n" + - "/dir2/*/* @user2\n" + - "/dir3/*/*/* @user3\n" + - "/dir4/**/* @user4\n" - ); - codeOwners.setVerbose(true); - LOG.info("CODEOWNERS:\n{}", codeOwners); - // NO Subdirs - assertOwners(codeOwners, "/dir1/bar.txt", "@user1"); - assertOwners(codeOwners, "/dir1//bar.txt"); - assertOwners(codeOwners, "/dir1/foo/bar.txt"); - assertOwners(codeOwners, "/dir1/foo/foo/bar.txt"); - assertOwners(codeOwners, "/dir1/foo/foo/foo/bar.txt"); - - // Exactly 1 Subdir - assertOwners(codeOwners, "/dir2/bar.txt"); - assertOwners(codeOwners, "/dir2//bar.txt"); - assertOwners(codeOwners, "/dir2/foo/bar.txt", "@user2"); - assertOwners(codeOwners, "/dir2/foo/foo/bar.txt"); - assertOwners(codeOwners, "/dir2/foo/foo/foo/bar.txt"); - - // Exactly 2 Subdirs - assertOwners(codeOwners, "/dir3/bar.txt"); - assertOwners(codeOwners, "/dir3//bar.txt"); - assertOwners(codeOwners, "/dir3/foo/bar.txt"); - assertOwners(codeOwners, "/dir3/foo/foo/bar.txt", "@user3"); - assertOwners(codeOwners, "/dir3///bar.txt"); - assertOwners(codeOwners, "/dir3/a//bar.txt"); - assertOwners(codeOwners, "/dir3//b/bar.txt"); - assertOwners(codeOwners, "/dir3/foo/foo/foo/bar.txt"); - - // Any Subdirs - assertOwners(codeOwners, "/dir4/bar.txt", "@user4"); - assertOwners(codeOwners, "/dir4//bar.txt", "@user4"); - assertOwners(codeOwners, "/dir4/foo/bar.txt", "@user4"); - assertOwners(codeOwners, "/dir4/foo/foo/bar.txt", "@user4"); - assertOwners(codeOwners, "/dir4/foo/foo/foo/bar.txt", "@user4"); - } - - - @Test - void testCodeOwnersToStringRoundTrip() throws IOException { - URL url = this.getClass() - .getClassLoader() - .getResource("CODEOWNERS_base"); - - assertNotNull(url); - - CodeOwners codeOwners = new CodeOwners(new File(url.getFile())); -// LOG.info("\n{}", codeOwners); - runChecks(codeOwners); - // Now reparse the toString output... (NORMAL) - codeOwners.setVerbose(false); - CodeOwners codeOwners2 = new CodeOwners(codeOwners.toString()); - runChecks(codeOwners2); - - // Now reparse the toString output... (VERBOSE) - codeOwners.setVerbose(true); - CodeOwners codeOwners3 = new CodeOwners(codeOwners.toString()); - runChecks(codeOwners3); - - } - - @Test - void testToString() { - CodeOwners codeOwners = new CodeOwners( - "/tool-*/ @user1\n" + - "*.xml @user2\n" - ); - codeOwners.setVerbose(true); - assertEquals( - "# CODEOWNERS file:\n" + - "# Regex used for the next rule: ^/tool-.*/\n" + - "/tool-*/ @user1\n" + - "# Regex used for the next rule: .*\\.xml(/|$)\n" + - "*.xml @user2\n", - codeOwners.toString()); - - codeOwners.setVerbose(false); - assertEquals( - "# CODEOWNERS file:\n" + - "/tool-*/ @user1\n" + - "*.xml @user2\n", - codeOwners.toString()); - } - - @Test - void testToStringEmpty() { - CodeOwners codeOwners = new CodeOwners( - "# Nothing here, only comments\n" - ); - codeOwners.setVerbose(true); - assertEquals( - "# CODEOWNERS file:\n" + - "# No CODEOWNER rules were defined.\n", - codeOwners.toString()); - - codeOwners.setVerbose(false); - assertEquals( - "# CODEOWNERS file:\n" + - "# No CODEOWNER rules were defined.\n", - codeOwners.toString()); - } - - -} diff --git a/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwners.kt b/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwners.kt new file mode 100644 index 0000000..fbf9068 --- /dev/null +++ b/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwners.kt @@ -0,0 +1,208 @@ +/* + * CodeOwners Tools + * Copyright (C) 2023-2026 Niels Basjes + * + * 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 + * + * https://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 nl.basjes.codeowners + +import kotlin.test.Test +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.io.File +import java.io.IOException +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +internal class TestCodeOwners { + private fun runChecks(codeOwners: CodeOwners) { + TestUtils.assertOwners(codeOwners, "Foo.txt", "@code", "@multiple", "@owners", "@dev-team") + TestUtils.assertOwners(codeOwners, "Foo.rb", "@ruby-owner", "@dev-team") + TestUtils.assertOwners(codeOwners, "#file_with_pound.rb", "@owner-file-with-pound", "@dev-team") + } + + @Test + fun testDotStarCase() { + val codeOwners = CodeOwners( + // Intended to match '/foo/.bar' NOT '/foo/' and NOT '/foo/foo/.bar' + """ + /foo/.* @user1 + *.xml @user2 + """.trimIndent() + ) + // codeOwners.setVerbose(true); +// LOG.info("CODEOWNERS:\n{}", codeOwners); + TestUtils.assertOwners(codeOwners, "/foo/.foo", "@user1") + TestUtils.assertOwners(codeOwners, "/foo/.foo/bar", "@user1") + TestUtils.assertOwners(codeOwners, "/foo/foo/.bar") // No users + TestUtils.assertOwners(codeOwners, "/foo/xfoo") // No users + TestUtils.assertOwners(codeOwners, "/foo/xfoo/bar") + TestUtils.assertOwners(codeOwners, "/foo/.foo/bar.xml", "@user2") + TestUtils.assertOwners(codeOwners, "/foo/foo") // No users + TestUtils.assertOwners(codeOwners, "/foo/foo/bar.xml", "@user2") + } + + @Test + fun testMidStarCase() { + val codeOwners = CodeOwners( + // Intended to match '/tool-library/bar.txt' + """ + /tool-*/ @user1 + *.xml @user2 + """.trimIndent() + ) + codeOwners.verbose = true + LOG.info("CODEOWNERS:\n{}", codeOwners) + TestUtils.assertOwners(codeOwners, "/tool-app/bar.txt", "@user1") + TestUtils.assertOwners(codeOwners, "/tool-app/foo/bar.txt", "@user1") + TestUtils.assertOwners(codeOwners, "/tool-app/bar.xml", "@user2") + TestUtils.assertOwners(codeOwners, "/tool-app/foo/bar.xml", "@user2") + TestUtils.assertOwners(codeOwners, "/bar.txt") + TestUtils.assertOwners(codeOwners, "/bar.xml", "@user2") + } + + @Test + fun testSubDirectoriesCase() { + // # The `docs/*` pattern will match files like + // # `docs/getting-started.md` but not further nested files like + // # `docs/build-app/troubleshooting.md`. + val codeOwners = CodeOwners( + "/dir1/* @user1\n" + + "/dir2/*/* @user2\n" + + "/dir3/*/*/* @user3\n" + + "/dir4/**/* @user4\n" + ) + codeOwners.verbose = true + LOG.info("CODEOWNERS:\n{}", codeOwners) + // NO Subdirs + TestUtils.assertOwners(codeOwners, "/dir1/bar.txt", "@user1") + TestUtils.assertOwners(codeOwners, "/dir1//bar.txt") + TestUtils.assertOwners(codeOwners, "/dir1/foo/bar.txt") + TestUtils.assertOwners(codeOwners, "/dir1/foo/foo/bar.txt") + TestUtils.assertOwners(codeOwners, "/dir1/foo/foo/foo/bar.txt") + + // Exactly 1 Subdir + TestUtils.assertOwners(codeOwners, "/dir2/bar.txt") + TestUtils.assertOwners(codeOwners, "/dir2//bar.txt") + TestUtils.assertOwners(codeOwners, "/dir2/foo/bar.txt", "@user2") + TestUtils.assertOwners(codeOwners, "/dir2/foo/foo/bar.txt") + TestUtils.assertOwners(codeOwners, "/dir2/foo/foo/foo/bar.txt") + + // Exactly 2 Subdirs + TestUtils.assertOwners(codeOwners, "/dir3/bar.txt") + TestUtils.assertOwners(codeOwners, "/dir3//bar.txt") + TestUtils.assertOwners(codeOwners, "/dir3/foo/bar.txt") + TestUtils.assertOwners(codeOwners, "/dir3/foo/foo/bar.txt", "@user3") + TestUtils.assertOwners(codeOwners, "/dir3///bar.txt") + TestUtils.assertOwners(codeOwners, "/dir3/a//bar.txt") + TestUtils.assertOwners(codeOwners, "/dir3//b/bar.txt") + TestUtils.assertOwners(codeOwners, "/dir3/foo/foo/foo/bar.txt") + + // Any Subdirs + TestUtils.assertOwners(codeOwners, "/dir4/bar.txt", "@user4") + TestUtils.assertOwners(codeOwners, "/dir4//bar.txt", "@user4") + TestUtils.assertOwners(codeOwners, "/dir4/foo/bar.txt", "@user4") + TestUtils.assertOwners(codeOwners, "/dir4/foo/foo/bar.txt", "@user4") + TestUtils.assertOwners(codeOwners, "/dir4/foo/foo/foo/bar.txt", "@user4") + } + + + @Test + @Throws(IOException::class) + fun testCodeOwnersToStringRoundTrip() { + val url = this.javaClass + .classLoader + .getResource("CODEOWNERS_base") + + assertNotNull(url) + + val codeOwners = CodeOwners(File(url.file)) + // LOG.info("\n{}", codeOwners); + runChecks(codeOwners) + // Now reparse the toString output... (NORMAL) + codeOwners.verbose = false + val codeOwners2 = CodeOwners(codeOwners.toString()) + runChecks(codeOwners2) + + // Now reparse the toString output... (VERBOSE) + codeOwners.verbose = true + val codeOwners3 = CodeOwners(codeOwners.toString()) + runChecks(codeOwners3) + } + + @Test + fun testToString() { + val codeOwners = CodeOwners( + """ + /tool-*/ @user1 + *.xml @user2 + """.trimIndent() + ) + codeOwners.verbose = true + assertEquals( + """ + # CODEOWNERS file: + # Regex used for the next rule: ^/tool-.*/ + /tool-*/ @user1 + # Regex used for the next rule: .*\.xml(/|$) + *.xml @user2 + + """.trimIndent(), + codeOwners.toString() + ) + + codeOwners.verbose = false + assertEquals( + """ + # CODEOWNERS file: + /tool-*/ @user1 + *.xml @user2 + + """.trimIndent(), + codeOwners.toString() + ) + } + + @Test + fun testToStringEmpty() { + val codeOwners = CodeOwners( + """ + # Nothing here, only comments + """.trimIndent() + ) + codeOwners.verbose = true + assertEquals( + """ + # CODEOWNERS file: + # No CODEOWNER rules were defined. + + """.trimIndent(), + codeOwners.toString() + ) + + codeOwners.verbose = false + assertEquals( + """ + # CODEOWNERS file: + # No CODEOWNER rules were defined. + + """.trimIndent(), + codeOwners.toString() + ) + } + + + companion object { + private val LOG: Logger = LoggerFactory.getLogger(TestCodeOwners::class.java) + } +} diff --git a/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwnersEdgeCases.java b/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwnersEdgeCases.java deleted file mode 100644 index ab8ba0a..0000000 --- a/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwnersEdgeCases.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * CodeOwners Tools - * Copyright (C) 2023-2026 Niels Basjes - * - * 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 - * - * https://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 nl.basjes.codeowners; - -import org.junit.jupiter.api.Test; - -import static nl.basjes.codeowners.TestUtils.assertOwners; - -public class TestCodeOwnersEdgeCases { - - @Test - // https://gitlab.com/gitlab-org/gitlab/-/work_items/585698 - void testDuplicatesSamePattern() { - CodeOwners codeOwners = new CodeOwners( - "packages/client/docker/ @team-a\n"+ - "packages/client/docker/ @team-b\n" - ); - assertOwners(codeOwners, "/packages/client/docker/something.txt", "@team-b"); - assertOwners(codeOwners, "/somewhere/packages/client/docker/something.txt", "@team-b"); - } - - @Test - void testDuplicatesDifferentPattern() { - CodeOwners codeOwners = new CodeOwners( - "# Global\n" + - "* @tech\n" + - "\n" + - "# Excludes\n" + - "!Data/\n" + - "\n" + - "# Teams\n" + - "/Engine/ @tech/core\n" + - "/App/ @tech/core\n" + - "\n" + - "# Individuals\n" + - "/Engine/Something/ @person\n" + - "\n" + - "# Critical\n" + - "/ThirdParty/Attributions/ @cto # CTO must approve\n" - ); - assertOwners(codeOwners, "/ThirdParty/Attributions/Licenses/SOME_FILE.txt", "@cto"); - } -} diff --git a/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwnersEdgeCases.kt b/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwnersEdgeCases.kt new file mode 100644 index 0000000..fd10e44 --- /dev/null +++ b/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwnersEdgeCases.kt @@ -0,0 +1,57 @@ +/* + * CodeOwners Tools + * Copyright (C) 2023-2026 Niels Basjes + * + * 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 + * + * https://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 nl.basjes.codeowners + +import kotlin.test.Test + +class TestCodeOwnersEdgeCases { + @Test + fun testDuplicatesSamePattern() { + val codeOwners = CodeOwners( + """ + packages/client/docker/ @team-a + packages/client/docker/ @team-b + """.trimIndent() + ) + TestUtils.assertOwners(codeOwners, "/packages/client/docker/something.txt", "@team-b") + TestUtils.assertOwners(codeOwners, "/somewhere/packages/client/docker/something.txt", "@team-b") + } + + @Test + fun testDuplicatesDifferentPattern() { + val codeOwners = CodeOwners( + """ + # Global + * @tech + + # Excludes + !Data/ + + # Teams + /Engine/ @tech/core + /App/ @tech/core + + # Individuals + /Engine/Something/ @person + + # Critical + /ThirdParty/Attributions/ @cto # CTO must approve + """.trimIndent() + ) + TestUtils.assertOwners(codeOwners, "/ThirdParty/Attributions/Licenses/SOME_FILE.txt", "@cto") + } +} diff --git a/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwnersEmail.java b/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwnersEmail.java deleted file mode 100644 index 8a512f2..0000000 --- a/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwnersEmail.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * CodeOwners Tools - * Copyright (C) 2023-2026 Niels Basjes - * - * 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 - * - * https://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 nl.basjes.codeowners; - -import org.junit.jupiter.api.Test; - -import static nl.basjes.codeowners.TestUtils.assertOwners; - -class TestCodeOwnersEmail { - - @Test - void testEmailOwners() { - CodeOwners codeOwners = new CodeOwners( - "file0.txt @someone\n" + - "file1.txt someone@example.nl\n" + - "file2.txt some.one@example.nl\n"+ - "file3.txt some+one@example.nl\n"+ - "file4.txt s.o{m}e-o_n|e@example.nl\n"+ - "fileall.txt @someone someone@example.nl some.one@example.nl some+one@example.nl s.o{m}e-o_n|e@example.nl" - ); - assertOwners(codeOwners, "file1.txt", "someone@example.nl"); - assertOwners(codeOwners, "file2.txt", "some.one@example.nl"); - assertOwners(codeOwners, "file3.txt", "some+one@example.nl"); - assertOwners(codeOwners, "file4.txt", "s.o{m}e-o_n|e@example.nl"); - assertOwners(codeOwners, "fileall.txt", "@someone", "someone@example.nl", "some.one@example.nl", "some+one@example.nl", "s.o{m}e-o_n|e@example.nl"); - } - - @Test - void testEmailOwnersCommentsBefore() { - CodeOwners codeOwners = new CodeOwners( - "file0.txt @someone\n" + - "file1.txt (before)someone@example.nl\n" + - "file2.txt (before)some.one@example.nl\n"+ - "file3.txt (before)some+one@example.nl\n"+ - "file4.txt (before)s.o{m}e-o_n|e@example.nl\n"+ - "fileall.txt @someone (before)someone@example.nl (before)some.one@example.nl (before)some+one@example.nl (before)s.o{m}e-o_n|e@example.nl" - ); - - // The comments will be stripped automatically - assertOwners(codeOwners, "file1.txt", "someone@example.nl"); - assertOwners(codeOwners, "file2.txt", "some.one@example.nl"); - assertOwners(codeOwners, "file3.txt", "some+one@example.nl"); - assertOwners(codeOwners, "file4.txt", "s.o{m}e-o_n|e@example.nl"); - assertOwners(codeOwners, "fileall.txt", "@someone", "someone@example.nl", "some.one@example.nl", "some+one@example.nl", "s.o{m}e-o_n|e@example.nl"); - } - - @Test - void testEmailOwnersCommentsAfter() { - CodeOwners codeOwners = new CodeOwners( - "file0.txt @someone\n" + - "file1.txt someone(after)@example.nl\n" + - "file2.txt some.one(after)@example.nl\n"+ - "file3.txt some+one(after)@example.nl\n"+ - "file4.txt s.o{m}e-o_n|e(after)@example.nl\n"+ - "fileall.txt @someone someone(after)@example.nl some.one(after)@example.nl some+one(after)@example.nl s.o{m}e-o_n|e(after)@example.nl" - ); - - // The comments will be stripped automatically - assertOwners(codeOwners, "file1.txt", "someone@example.nl"); - assertOwners(codeOwners, "file2.txt", "some.one@example.nl"); - assertOwners(codeOwners, "file3.txt", "some+one@example.nl"); - assertOwners(codeOwners, "file4.txt", "s.o{m}e-o_n|e@example.nl"); - assertOwners(codeOwners, "fileall.txt", "@someone", "someone@example.nl", "some.one@example.nl", "some+one@example.nl", "s.o{m}e-o_n|e@example.nl"); - } - - @Test - void testEmailOwnersCommentsBeforeAndAfter() { - CodeOwners codeOwners = new CodeOwners( - "file0.txt @someone\n" + - "file1.txt (before)someone(after)@example.nl\n" + - "file2.txt (before)some.one(after)@example.nl\n"+ - "file3.txt (before)some+one(after)@example.nl\n"+ - "file4.txt (before)s.o{m}e-o_n|e(after)@example.nl\n"+ - "fileall.txt @someone (before)someone(after)@example.nl (before)some.one(after)@example.nl (before)some+one(after)@example.nl (before)s.o{m}e-o_n|e(after)@example.nl" - ); - - // The comments will be stripped automatically - assertOwners(codeOwners, "file1.txt", "someone@example.nl"); - assertOwners(codeOwners, "file2.txt", "some.one@example.nl"); - assertOwners(codeOwners, "file3.txt", "some+one@example.nl"); - assertOwners(codeOwners, "file4.txt", "s.o{m}e-o_n|e@example.nl"); - assertOwners(codeOwners, "fileall.txt", "@someone", "someone@example.nl", "some.one@example.nl", "some+one@example.nl", "s.o{m}e-o_n|e@example.nl"); - } -} diff --git a/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwnersEmail.kt b/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwnersEmail.kt new file mode 100644 index 0000000..f96a986 --- /dev/null +++ b/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwnersEmail.kt @@ -0,0 +1,101 @@ +/* + * CodeOwners Tools + * Copyright (C) 2023-2026 Niels Basjes + * + * 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 + * + * https://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 nl.basjes.codeowners + +import kotlin.test.Test + +internal class TestCodeOwnersEmail { + @Test + fun testEmailOwners() { + val codeOwners = CodeOwners( + """ + file0.txt @someone + file1.txt someone@example.nl + file2.txt some.one@example.nl + file3.txt some+one@example.nl + file4.txt s.o{m}e-o_n|e@example.nl + fileall.txt @someone someone@example.nl some.one@example.nl some+one@example.nl s.o{m}e-o_n|e@example.nl + """.trimIndent() + ) + TestUtils.assertOwners(codeOwners, "file1.txt", "someone@example.nl") + TestUtils.assertOwners(codeOwners, "file2.txt", "some.one@example.nl") + TestUtils.assertOwners(codeOwners, "file3.txt", "some+one@example.nl") + TestUtils.assertOwners(codeOwners, "file4.txt", "s.o{m}e-o_n|e@example.nl") + TestUtils.assertOwners(codeOwners, "fileall.txt", "@someone", "someone@example.nl", "some.one@example.nl", "some+one@example.nl", "s.o{m}e-o_n|e@example.nl") + } + + @Test + fun testEmailOwnersCommentsBefore() { + val codeOwners = CodeOwners( + "file0.txt @someone\n" + + "file1.txt (before)someone@example.nl\n" + + "file2.txt (before)some.one@example.nl\n" + + "file3.txt (before)some+one@example.nl\n" + + "file4.txt (before)s.o{m}e-o_n|e@example.nl\n" + + "fileall.txt @someone (before)someone@example.nl (before)some.one@example.nl (before)some+one@example.nl (before)s.o{m}e-o_n|e@example.nl" + ) + + // The comments will be stripped automatically + TestUtils.assertOwners(codeOwners, "file1.txt", "someone@example.nl") + TestUtils.assertOwners(codeOwners, "file2.txt", "some.one@example.nl") + TestUtils.assertOwners(codeOwners, "file3.txt", "some+one@example.nl") + TestUtils.assertOwners(codeOwners, "file4.txt", "s.o{m}e-o_n|e@example.nl") + TestUtils.assertOwners(codeOwners, "fileall.txt", "@someone", "someone@example.nl", "some.one@example.nl", "some+one@example.nl", "s.o{m}e-o_n|e@example.nl") + } + + @Test + fun testEmailOwnersCommentsAfter() { + val codeOwners = CodeOwners( + """ + file0.txt @someone + file1.txt someone(after)@example.nl + file2.txt some.one(after)@example.nl + file3.txt some+one(after)@example.nl + file4.txt s.o{m}e-o_n|e(after)@example.nl + fileall.txt @someone someone(after)@example.nl some.one(after)@example.nl some+one(after)@example.nl s.o{m}e-o_n|e(after)@example.nl + """.trimIndent() + ) + + // The comments will be stripped automatically + TestUtils.assertOwners(codeOwners, "file1.txt", "someone@example.nl") + TestUtils.assertOwners(codeOwners, "file2.txt", "some.one@example.nl") + TestUtils.assertOwners(codeOwners, "file3.txt", "some+one@example.nl") + TestUtils.assertOwners(codeOwners, "file4.txt", "s.o{m}e-o_n|e@example.nl") + TestUtils.assertOwners(codeOwners, "fileall.txt", "@someone", "someone@example.nl", "some.one@example.nl", "some+one@example.nl", "s.o{m}e-o_n|e@example.nl") + } + + @Test + fun testEmailOwnersCommentsBeforeAndAfter() { + val codeOwners = CodeOwners( + """ + file0.txt @someone + file1.txt (before)someone(after)@example.nl + file2.txt (before)some.one(after)@example.nl + file3.txt (before)some+one(after)@example.nl + file4.txt (before)s.o{m}e-o_n|e(after)@example.nl + fileall.txt @someone (before)someone(after)@example.nl (before)some.one(after)@example.nl (before)some+one(after)@example.nl (before)s.o{m}e-o_n|e(after)@example.nl + """.trimIndent() + ) + + // The comments will be stripped automatically + TestUtils.assertOwners(codeOwners, "file1.txt", "someone@example.nl") + TestUtils.assertOwners(codeOwners, "file2.txt", "some.one@example.nl") + TestUtils.assertOwners(codeOwners, "file3.txt", "some+one@example.nl") + TestUtils.assertOwners(codeOwners, "file4.txt", "s.o{m}e-o_n|e@example.nl") + TestUtils.assertOwners(codeOwners, "fileall.txt", "@someone", "someone@example.nl", "some.one@example.nl", "some+one@example.nl", "s.o{m}e-o_n|e@example.nl") + } +} diff --git a/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwnersGetters.java b/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwnersGetters.java deleted file mode 100644 index ac497e9..0000000 --- a/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwnersGetters.java +++ /dev/null @@ -1,158 +0,0 @@ -/* - * CodeOwners Tools - * Copyright (C) 2023-2026 Niels Basjes - * - * 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 - * - * https://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 nl.basjes.codeowners; - -import org.junit.jupiter.api.Test; - -import java.util.Collections; -import java.util.List; -import java.util.Set; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; - -class TestCodeOwnersGetters { - - @Test - void verifyVerbose() { - CodeOwners codeOwners = new CodeOwners("docs/"); - codeOwners.setVerbose(true); - assertTrue(codeOwners.getVerbose()); - codeOwners.setVerbose(false); - assertFalse(codeOwners.getVerbose()); - - Section section = new Section("foo"); - section.setVerbose(true); - assertTrue(section.getVerbose()); - section.setVerbose(false); - assertFalse(section.getVerbose()); - } - - @Test - void verifyGetters() { - CodeOwners codeOwners = new CodeOwners( - "[One][11] @docs-team\n" + - "docs/\n" + - "*.md\n" + - "\n" + - "[Two][22] @database-team\n" + - "model/db/\n" + - "config/db/database-setup.md @docs-team\n" + - "\n" + - "[Three]\n" + - "three1/ \n" + - "\n" + - "^[Four]\n" + // Optional - "four/\n" + - "\n" + - "[ tHrEe ]\n" + - "three2/ @docs-team\n" + - "\n" - ); - - Set
allDefinedSections = codeOwners.getAllDefinedSections(); - assertEquals(4, allDefinedSections.size()); - Rule rule0; - Rule rule1; - for (Section section : allDefinedSections) { - List rules; - switch (section.getName()) { - case "One": - assertFalse(section.isOptional()); - assertEquals(11, section.getMinimalNumberOfApprovers()); - assertEquals(1, section.getDefaultApprovers().size()); - assertEquals("@docs-team", section.getDefaultApprovers().get(0)); - - rules = section.getRules(); - assertEquals(2, rules.size()); - - rule0 = rules.get(0); - assertEquals("docs/", rule0.getFileExpression()); - assertEquals("(/.*)?/docs/", rule0.getFilePattern().pattern()); - assertInstanceOf(ApprovalRule.class, rule0); - assertEquals(Collections.emptyList(), ((ApprovalRule) rule0).getApprovers()); - - rule1 = rules.get(1); - assertEquals("*.md", rule1.getFileExpression()); - assertEquals(".*\\.md(/|$)", rule1.getFilePattern().pattern()); - assertEquals(Collections.emptyList(), ((ApprovalRule) rule1).getApprovers()); - break; - - case "Two": - assertFalse(section.isOptional()); - assertEquals(22, section.getMinimalNumberOfApprovers()); - assertEquals(1, section.getDefaultApprovers().size()); - assertEquals("@database-team", section.getDefaultApprovers().get(0)); - - rules = section.getRules(); - assertEquals(2, rules.size()); - - rule0 = rules.get(0); - assertEquals("model/db/", rule0.getFileExpression()); - assertEquals("(/.*)?/model/db/", rule0.getFilePattern().pattern()); - assertEquals(Collections.emptyList(), ((ApprovalRule)rule0).getApprovers()); - - rule1 = rules.get(1); - assertEquals("config/db/database-setup.md", rule1.getFileExpression()); - assertEquals("(/.*)?/config/db/database-setup\\.md(/|$)", rule1.getFilePattern().pattern()); - assertEquals(Collections.singletonList("@docs-team"), ((ApprovalRule)rule1).getApprovers()); - break; - - case "Three": - assertFalse(section.isOptional()); - assertEquals(0, section.getMinimalNumberOfApprovers()); - assertEquals(0, section.getDefaultApprovers().size()); - - rules = section.getRules(); - assertEquals(2, rules.size()); - - rule0 = rules.get(0); - assertEquals("three1/", rule0.getFileExpression()); - assertEquals("(/.*)?/three1/", rule0.getFilePattern().pattern()); - assertEquals(Collections.emptyList(), ((ApprovalRule)rule0).getApprovers()); - - rule1 = rules.get(1); - assertEquals("three2/", rule1.getFileExpression()); - assertEquals("(/.*)?/three2/", rule1.getFilePattern().pattern()); - assertEquals(Collections.singletonList("@docs-team"), ((ApprovalRule)rule1).getApprovers()); - break; - - case "Four": - assertTrue(section.isOptional()); - assertEquals(0, section.getMinimalNumberOfApprovers()); - assertEquals(0, section.getDefaultApprovers().size()); - - rules = section.getRules(); - assertEquals(1, rules.size()); - - rule0 = rules.get(0); - assertEquals("four/", rule0.getFileExpression()); - assertEquals("(/.*)?/four/", rule0.getFilePattern().pattern()); - assertEquals(Collections.emptyList(), ((ApprovalRule)rule0).getApprovers()); - break; - - default: - fail("The section name " + section.getName() + " is not supported"); - } - } - } - -} diff --git a/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwnersGetters.kt b/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwnersGetters.kt new file mode 100644 index 0000000..7615103 --- /dev/null +++ b/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwnersGetters.kt @@ -0,0 +1,149 @@ +/* + * CodeOwners Tools + * Copyright (C) 2023-2026 Niels Basjes + * + * 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 + * + * https://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 nl.basjes.codeowners + +import org.junit.jupiter.api.Assertions.assertInstanceOf +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import kotlin.test.fail + +internal class TestCodeOwnersGetters { + @Test + fun verifyVerbose() { + val codeOwners = CodeOwners("docs/") + codeOwners.verbose = true + assertTrue(codeOwners.verbose) + codeOwners.verbose = false + assertFalse(codeOwners.verbose) + + val section = Section("foo") + section.verbose = true + assertTrue(section.verbose) + section.verbose = false + assertFalse(section.verbose) + } + + @Test + fun verifyGetters() { + val codeOwners = CodeOwners( + "[One][11] @docs-team\n" + + "docs/\n" + + "*.md\n" + + "\n" + + "[Two][22] @database-team\n" + + "model/db/\n" + + "config/db/database-setup.md @docs-team\n" + + "\n" + + "[Three]\n" + + "three1/ \n" + + "\n" + + "^[Four]\n" + // Optional + "four/\n" + + "\n" + + "[ tHrEe ]\n" + + "three2/ @docs-team\n" + + "\n" + ) + + val allDefinedSections: Set
= codeOwners.allDefinedSections + assertEquals(4, allDefinedSections.size) + var rule0: Rule + var rule1: Rule + for (section in allDefinedSections) { + val rules: MutableList + when (section.name) { + "One" -> { + assertFalse(section.isOptional) + assertEquals(11, section.minimalNumberOfApprovers) + assertEquals(1, section.defaultApprovers.size) + assertEquals("@docs-team", section.defaultApprovers[0]) + + rules = section.rules + assertEquals(2, rules.size) + + rule0 = rules[0] + assertEquals("docs/", rule0.fileExpression) + assertEquals("(/.*)?/docs/", rule0.filePattern.pattern) + assertInstanceOf(ApprovalRule::class.java, rule0) + assertEquals(mutableListOf(), (rule0 as ApprovalRule).approvers) + + rule1 = rules[1] + assertEquals("*.md", rule1.fileExpression) + assertEquals(".*\\.md(/|$)", rule1.filePattern.pattern) + assertEquals(mutableListOf(), (rule1 as ApprovalRule).approvers) + } + + "Two" -> { + assertFalse(section.isOptional) + assertEquals(22, section.minimalNumberOfApprovers) + assertEquals(1, section.defaultApprovers.size) + assertEquals("@database-team", section.defaultApprovers[0]) + + rules = section.rules + assertEquals(2, rules.size) + + rule0 = rules[0] + assertEquals("model/db/", rule0.fileExpression) + assertEquals("(/.*)?/model/db/", rule0.filePattern.pattern) + assertEquals(mutableListOf(), (rule0 as ApprovalRule).approvers) + + rule1 = rules[1] + assertEquals("config/db/database-setup.md", rule1.fileExpression) + assertEquals("(/.*)?/config/db/database-setup\\.md(/|$)", rule1.filePattern.pattern) + assertEquals(mutableListOf("@docs-team"), (rule1 as ApprovalRule).approvers) + } + + "Three" -> { + assertFalse(section.isOptional) + assertEquals(0, section.minimalNumberOfApprovers) + assertEquals(0, section.defaultApprovers.size) + + rules = section.rules + assertEquals(2, rules.size) + + rule0 = rules[0] + assertEquals("three1/", rule0.fileExpression) + assertEquals("(/.*)?/three1/", rule0.filePattern.pattern) + assertEquals(mutableListOf(), (rule0 as ApprovalRule).approvers) + + rule1 = rules[1] + assertEquals("three2/", rule1.fileExpression) + assertEquals("(/.*)?/three2/", rule1.filePattern.pattern) + assertEquals(mutableListOf("@docs-team"), (rule1 as ApprovalRule).approvers) + } + + "Four" -> { + assertTrue(section.isOptional) + assertEquals(0, section.minimalNumberOfApprovers) + assertEquals(0, section.defaultApprovers.size) + + rules = section.rules + assertEquals(1, rules.size) + + rule0 = rules[0] + assertEquals("four/", rule0.fileExpression) + assertEquals("(/.*)?/four/", rule0.filePattern.pattern) + assertEquals(mutableListOf(), (rule0 as ApprovalRule).approvers) + } + + else -> fail("The section name ${section.name} is not supported") + } + } + } +} diff --git a/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwnersGithub.java b/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwnersGithub.kt similarity index 56% rename from codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwnersGithub.java rename to codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwnersGithub.kt index dc6bb7b..67cdab7 100644 --- a/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwnersGithub.java +++ b/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwnersGithub.kt @@ -14,184 +14,182 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package nl.basjes.codeowners -package nl.basjes.codeowners; - -import org.junit.jupiter.api.Test; - -import static nl.basjes.codeowners.TestUtils.assertOwners; - -class TestCodeOwnersGithub { - - // https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners#codeowners-syntax - private static final String GITHUB_CODEOWNERS_DOC_EXAMPLE = - "# This is a comment.\n" + - "# Each line is a file pattern followed by one or more owners.\n" + - "\n" + - "# These owners will be the default owners for everything in\n" + - "# the repo. Unless a later match takes precedence,\n" + - "# @global-owner1 and @global-owner2 will be requested for\n" + - "# review when someone opens a pull request.\n" + - "* @global-owner1 @global-owner2\n" + - "\n" + - "# Order is important; the last matching pattern takes the most\n" + - "# precedence. When someone opens a pull request that only\n" + - "# modifies JS files, only @js-owner and not the global\n" + - "# owner(s) will be requested for a review.\n" + - "*.js @js-owner #This is an inline comment.\n" + - "\n" + - "# You can also use email addresses if you prefer. They'll be\n" + - "# used to look up users just like we do for commit author\n" + - "# emails.\n" + - "*.go docs@example.com\n" + - "\n" + - "# Teams can be specified as code owners as well. Teams should\n" + - "# be identified in the format @org/team-name. Teams must have\n" + - "# explicit write access to the repository. In this example,\n" + - "# the octocats team in the octo-org organization owns all .txt files.\n" + - "*.txt @octo-org/octocats\n" + - "\n" + - "# In this example, @doctocat owns any files in the build/output\n" + - "# directory at the root of the repository and any of its\n" + - "# subdirectories.\n" + - // Changed /logs to /output for better testing - "/build/output/ @doctocat\n" + - "\n" + - "# The `docs/*` pattern will match files like\n" + - "# `docs/getting-started.md` but not further nested files like\n" + - "# `docs/build-app/troubleshooting.md`.\n" + - "docs/* docs@example.com\n" + - "\n" + - "# In this example, @octocat owns any file in an apps directory\n" + - "# anywhere in your repository.\n" + - "apps/ @octocat\n" + - "\n" + - "# In this example, @doctocat owns any file in the `/docs`\n" + - "# directory in the root of your repository and any of its\n" + - "# subdirectories.\n" + - "/docs/ @doctocat\n" + - "\n" + - "# In this example, any change inside the `/scripts` directory\n" + - "# will require approval from @doctocat or @octocat.\n" + - "/scripts/ @doctocat @octocat\n" + - "\n" + - "# In this example, @octocat owns any file in a `/logs` directory such as\n" + - "# `/build/logs`, `/scripts/logs`, and `/deeply/nested/logs`. Any changes\n" + - "# in a `/logs` directory will require approval from @octocat.\n" + - "**/logs @octocat\n" + - "\n" + - "# In this example, @octocat owns any file in the `/apps`\n" + - "# directory in the root of your repository except for the `/apps/github`\n" + - "# subdirectory, as its owners are left empty.\n" + - "/apps/ @octocat\n" + - "/apps/github\n"; +import nl.basjes.codeowners.TestUtils.assertOwners +import kotlin.test.Test +internal class TestCodeOwnersGithub { @Test - void githubCodeOwnersExample() { - CodeOwners codeOwners = new CodeOwners(GITHUB_CODEOWNERS_DOC_EXAMPLE); + fun githubCodeOwnersExample() { + val codeOwners = CodeOwners(GITHUB_CODEOWNERS_DOC_EXAMPLE) // # These owners will be the default owners for everything in // # the repo. Unless a later match takes precedence, // # @global-owner1 and @global-owner2 will be requested for // # review when someone opens a pull request. // * @global-owner1 @global-owner2 - assertOwners(codeOwners, "RandomName.docx", "@global-owner1", "@global-owner2"); + assertOwners(codeOwners, "RandomName.docx", "@global-owner1", "@global-owner2") // # Order is important; the last matching pattern takes the most // # precedence. When someone opens a pull request that only // # modifies JS files, only @js-owner and not the global // # owner(s) will be requested for a review. // *.js @js-owner #This is an inline comment. - assertOwners(codeOwners, "File.js", "@js-owner"); - assertOwners(codeOwners, "/File.js", "@js-owner"); - assertOwners(codeOwners, "/foo/File.js", "@js-owner"); - assertOwners(codeOwners, "/one/two/three/File.js", "@js-owner"); + assertOwners(codeOwners, "File.js", "@js-owner") + assertOwners(codeOwners, "/File.js", "@js-owner") + assertOwners(codeOwners, "/foo/File.js", "@js-owner") + assertOwners(codeOwners, "/one/two/three/File.js", "@js-owner") // # You can also use email addresses if you prefer. They'll be // # used to look up users just like we do for commit author // # emails. // *.go docs@example.com - assertOwners(codeOwners, "File.go", "docs@example.com"); - assertOwners(codeOwners, "/File.go", "docs@example.com"); - assertOwners(codeOwners, "/foo/File.go", "docs@example.com"); - assertOwners(codeOwners, "/one/two/three/File.go", "docs@example.com"); + assertOwners(codeOwners, "File.go", "docs@example.com") + assertOwners(codeOwners, "/File.go", "docs@example.com") + assertOwners(codeOwners, "/foo/File.go", "docs@example.com") + assertOwners(codeOwners, "/one/two/three/File.go", "docs@example.com") // # Teams can be specified as code owners as well. Teams should // # be identified in the format @org/team-name. Teams must have // # explicit write access to the repository. In this example, // # the octocats team in the octo-org organization owns all .txt files. // *.txt @octo-org/octocats - assertOwners(codeOwners, "File.txt", "@octo-org/octocats"); - assertOwners(codeOwners, "/File.txt", "@octo-org/octocats"); - assertOwners(codeOwners, "/foo/File.txt", "@octo-org/octocats"); - assertOwners(codeOwners, "/one/two/three/File.txt", "@octo-org/octocats"); + assertOwners(codeOwners, "File.txt", "@octo-org/octocats") + assertOwners(codeOwners, "/File.txt", "@octo-org/octocats") + assertOwners(codeOwners, "/foo/File.txt", "@octo-org/octocats") + assertOwners(codeOwners, "/one/two/three/File.txt", "@octo-org/octocats") // Changed /logs to /output for testing !!!!!!!!!!!!! // # In this example, @doctocat owns any files in the build/output // # directory at the root of the repository and any of its // # subdirectories. // /build/output/ @doctocat - assertOwners(codeOwners, "/build/RandomName.docx", "@global-owner1", "@global-owner2"); - assertOwners(codeOwners, "/build/outputx/RandomName.docx", "@global-owner1", "@global-owner2"); - assertOwners(codeOwners, "/build/outputx/RandomName.js", "@js-owner"); - assertOwners(codeOwners, "/build/output/RandomName.docx", "@doctocat"); - assertOwners(codeOwners, "/build/output/File.js", "@doctocat"); - assertOwners(codeOwners, "/build/output/foo/File.js", "@doctocat"); + assertOwners(codeOwners, "/build/RandomName.docx", "@global-owner1", "@global-owner2") + assertOwners(codeOwners, "/build/outputx/RandomName.docx", "@global-owner1", "@global-owner2") + assertOwners(codeOwners, "/build/outputx/RandomName.js", "@js-owner") + assertOwners(codeOwners, "/build/output/RandomName.docx", "@doctocat") + assertOwners(codeOwners, "/build/output/File.js", "@doctocat") + assertOwners(codeOwners, "/build/output/foo/File.js", "@doctocat") // # The `docs/*` pattern will match files like // # `docs/getting-started.md` but not further nested files like // # `docs/build-app/troubleshooting.md`. // docs/* docs@example.com - assertOwners(codeOwners, "/foo/docs/getting-started.md", "docs@example.com"); - assertOwners(codeOwners, "/foo/docs/build-app/troubleshooting.md", "@global-owner1", "@global-owner2"); + assertOwners(codeOwners, "/foo/docs/getting-started.md", "docs@example.com") + assertOwners(codeOwners, "/foo/docs/build-app/troubleshooting.md", "@global-owner1", "@global-owner2") // # In this example, @octocat owns any file in an apps directory // # anywhere in your repository. // apps/ @octocat - assertOwners(codeOwners, "/apps/RandomName.docx", "@octocat"); - assertOwners(codeOwners, "/foo/apps/RandomName.docx", "@octocat"); + assertOwners(codeOwners, "/apps/RandomName.docx", "@octocat") + assertOwners(codeOwners, "/foo/apps/RandomName.docx", "@octocat") // # In this example, @doctocat owns any file in the `/docs` // # directory in the root of your repository and any of its // # subdirectories. // /docs/ @doctocat - assertOwners(codeOwners, "/apps/RandomName.docx", "@octocat"); - assertOwners(codeOwners, "/foo/apps/RandomName.docx", "@octocat"); + assertOwners(codeOwners, "/apps/RandomName.docx", "@octocat") + assertOwners(codeOwners, "/foo/apps/RandomName.docx", "@octocat") // # In this example, any change inside the `/scripts` directory // # will require approval from @doctocat or @octocat. // /scripts/ @doctocat @octocat - assertOwners(codeOwners, "/scriptsx/RandomName.docx", "@global-owner1", "@global-owner2"); - assertOwners(codeOwners, "/scriptsx/RandomName.js", "@js-owner"); - assertOwners(codeOwners, "/scripts/RandomName.docx", "@doctocat", "@octocat"); - assertOwners(codeOwners, "/scripts/File.js", "@doctocat", "@octocat"); - assertOwners(codeOwners, "/scripts/foo/File.js", "@doctocat", "@octocat"); + assertOwners(codeOwners, "/scriptsx/RandomName.docx", "@global-owner1", "@global-owner2") + assertOwners(codeOwners, "/scriptsx/RandomName.js", "@js-owner") + assertOwners(codeOwners, "/scripts/RandomName.docx", "@doctocat", "@octocat") + assertOwners(codeOwners, "/scripts/File.js", "@doctocat", "@octocat") + assertOwners(codeOwners, "/scripts/foo/File.js", "@doctocat", "@octocat") // # In this example, @octocat owns any file in a `/logs` directory such as // # `/build/logs`, `/scripts/logs`, and `/deeply/nested/logs`. Any changes // # in a `/logs` directory will require approval from @octocat. // **/logs @octocat - assertOwners(codeOwners, "/logssss/access.log", "@global-owner1", "@global-owner2"); - assertOwners(codeOwners, "/build/logssss/access.log", "@global-owner1", "@global-owner2"); - assertOwners(codeOwners, "/scripts/logssss/access.log", "@doctocat", "@octocat"); - assertOwners(codeOwners, "/deeply/nested/logssss/access.log", "@global-owner1", "@global-owner2"); + assertOwners(codeOwners, "/logssss/access.log", "@global-owner1", "@global-owner2") + assertOwners(codeOwners, "/build/logssss/access.log", "@global-owner1", "@global-owner2") + assertOwners(codeOwners, "/scripts/logssss/access.log", "@doctocat", "@octocat") + assertOwners(codeOwners, "/deeply/nested/logssss/access.log", "@global-owner1", "@global-owner2") - assertOwners(codeOwners, "/logs/access.log", "@octocat"); + assertOwners(codeOwners, "/logs/access.log", "@octocat") // !!!!!!!!!!! NOTE THE OVERLAP WITH THE ABOVE '/build/logs/' rule !!!!!!!!!! - assertOwners(codeOwners, "/build/logs/access.log", "@octocat"); - assertOwners(codeOwners, "/scripts/logs/access.log", "@octocat"); - assertOwners(codeOwners, "/deeply/nested/logs/access.log", "@octocat"); + assertOwners(codeOwners, "/build/logs/access.log", "@octocat") + assertOwners(codeOwners, "/scripts/logs/access.log", "@octocat") + assertOwners(codeOwners, "/deeply/nested/logs/access.log", "@octocat") // # In this example, @octocat owns any file in the `/apps` // # directory in the root of your repository except for the `/apps/github` // # subdirectory, as its owners are left empty. // /apps/ @octocat // /apps/github - - assertOwners(codeOwners, "/apps/RandomName.txt", "@octocat"); + assertOwners(codeOwners, "/apps/RandomName.txt", "@octocat") // No owners for this file - assertOwners(codeOwners, "/apps/github/RandomName.txt"); + assertOwners(codeOwners, "/apps/github/RandomName.txt") } + companion object { + // https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners#codeowners-syntax + private val GITHUB_CODEOWNERS_DOC_EXAMPLE = + """ + # This is a comment. + # Each line is a file pattern followed by one or more owners. + + # These owners will be the default owners for everything in + # the repo. Unless a later match takes precedence, + # @global-owner1 and @global-owner2 will be requested for + # review when someone opens a pull request. + * @global-owner1 @global-owner2 + + # Order is important; the last matching pattern takes the most + # precedence. When someone opens a pull request that only + # modifies JS files, only @js-owner and not the global + # owner(s) will be requested for a review. + *.js @js-owner #This is an inline comment. + + # You can also use email addresses if you prefer. They'll be + # used to look up users just like we do for commit author + # emails. + *.go docs@example.com + + # Teams can be specified as code owners as well. Teams should + # be identified in the format @org/team-name. Teams must have + # explicit write access to the repository. In this example, + # the octocats team in the octo-org organization owns all .txt files. + *.txt @octo-org/octocats + + # In this example, @doctocat owns any files in the build/output + # directory at the root of the repository and any of its + # subdirectories. + /build/output/ @doctocat + + # The `docs/*` pattern will match files like + # `docs/getting-started.md` but not further nested files like + # `docs/build-app/troubleshooting.md`. + docs/* docs@example.com + + # In this example, @octocat owns any file in an apps directory + # anywhere in your repository. + apps/ @octocat + + # In this example, @doctocat owns any file in the `/docs` + # directory in the root of your repository and any of its + # subdirectories. + /docs/ @doctocat + + # In this example, any change inside the `/scripts` directory + # will require approval from @doctocat or @octocat. + /scripts/ @doctocat @octocat + + # In this example, @octocat owns any file in a `/logs` directory such as + # `/build/logs`, `/scripts/logs`, and `/deeply/nested/logs`. Any changes + # in a `/logs` directory will require approval from @octocat. + **/logs @octocat + + # In this example, @octocat owns any file in the `/apps` + # directory in the root of your repository except for the `/apps/github` + # subdirectory, as its owners are left empty. + /apps/ @octocat + /apps/github + """.trimIndent() + } } diff --git a/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwnersGitlab.java b/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwnersGitlab.java deleted file mode 100644 index ad0c35f..0000000 --- a/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwnersGitlab.java +++ /dev/null @@ -1,527 +0,0 @@ -/* - * CodeOwners Tools - * Copyright (C) 2023-2026 Niels Basjes - * - * 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 - * - * https://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 nl.basjes.codeowners; - -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import static nl.basjes.codeowners.TestUtils.assertMandatoryOwners; -import static nl.basjes.codeowners.TestUtils.assertOwners; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -class TestCodeOwnersGitlab { - - private static final Logger LOG = LoggerFactory.getLogger(TestCodeOwnersGitlab.class); - - @Test - void gitlabPathsWithSpaces() { - // Paths containing whitespace must be escaped with backslashes: path\ with\ spaces/*.md. - CodeOwners codeOwners = new CodeOwners( - "internalstuff/README.md @user1\n" + - "internal\\ stuff/README.md @user2\n" - ); - - assertOwners(codeOwners, "internalstuff/README.md", "@user1"); - assertOwners(codeOwners, "internal stuff/README.md", "@user2"); - assertOwners(codeOwners, "internal stuff/README.md"); // No owners - } - - @Test - void gitlabSections() { - CodeOwners codeOwners = new CodeOwners( - "[README Owners]\n" + - "README.md @user1 @user2\n" + - "internal/README.md @user4\n" + - "\n" + - "[README other owners]\n" + - "README.md @user3 \n" + - "\n" + - "[README default] @user5\n" + - "*.md\n" + - "SomethingElse.md @user3" - ); - - codeOwners.setVerbose(true); - // The Code Owners for the README.md in the root directory are @user1, @user2, and @user3 + @user5 because of the extra rule for all *.md files. - assertOwners(codeOwners, "README.md", "@user1", "@user2", "@user3", "@user5"); - // The Code Owners for internal/README.md are @user4 and @user3 + @user5 because of the extra rule for all *.md files. - assertOwners(codeOwners, "internal/README.md", "@user3", "@user4", "@user5"); - } - - @Test - void gitlabRelativePaths() { - // If a path does not start with a /, the path is treated as if it starts with a globstar. - // README.md is treated the same way as /**/README.md: - - // This will match /README.md, /internal/README.md, /app/lib/README.md - CodeOwners codeOwnersRoot = new CodeOwners("README.md @username"); - assertOwners(codeOwnersRoot, "/README.md", "@username"); - assertOwners(codeOwnersRoot, "/internal/README.md", "@username"); - assertOwners(codeOwnersRoot, "/app/lib/README.md", "@username"); - - CodeOwners codeOwnersSubdir = new CodeOwners("internal/README.md @username"); - assertOwners(codeOwnersSubdir, "/internal/README.md", "@username"); - assertOwners(codeOwnersSubdir, "/docs/internal/README.md", "@username"); - assertOwners(codeOwnersSubdir, "/docs/api/internal/README.md", "@username"); - } - - @Test - void gitlabWildcardPaths() { - CodeOwners codeOwners = new CodeOwners( - "# Any markdown files in the docs directory\n" + - "/docs/*.md @user1\n" + - "\n" + - "# /docs/index file of any filetype\n" + - "# For example: /docs/index.md, /docs/index.html, /docs/index.xml\n" + - "/docs/index.* @user2\n" + - "\n" + - "# Any file in the docs directory with 'spec' in the name.\n" + - "# For example: /docs/qa_specs.rb, /docs/spec_helpers.rb, /docs/runtime.spec\n" + - "/docs/*spec* @user3\n" + - "\n" + - "# README.md files one level deep within the docs directory\n" + - "# For example: /docs/api/README.md\n" + - "/docs/*/README.md @user4"); - - assertOwners(codeOwners, "/docs/test.md", "@user1"); - - assertOwners(codeOwners, "/docs/index.md", "@user2"); - assertOwners(codeOwners, "/docs/index.html", "@user2"); - assertOwners(codeOwners, "/docs/index.xml", "@user2"); - - assertOwners(codeOwners, "/docs/qa_specs.rb", "@user3"); - assertOwners(codeOwners, "/docs/spec_helpers.rb", "@user3"); - assertOwners(codeOwners, "/docs/runtime.spec", "@user3"); - - assertOwners(codeOwners, "/docs/api/README.md", "@user4"); - } - - @Test - void gitlabGlobstarPaths() { - //This will match /docs/index.md, /docs/api/index.md, /docs/api/graphql/index.md - CodeOwners codeOwners = new CodeOwners("/docs/**/index.md @username"); - assertOwners(codeOwners, "/docs/index.md", "@username"); - assertOwners(codeOwners, "/docs/api/index.md", "@username"); - assertOwners(codeOwners, "/docs/api/graphql/index.md", "@username"); - } - - @Test - void gitlabSectionsDefaults() { - CodeOwners codeOwners = new CodeOwners( - "[Documentation] @docs-team\n" + - "docs/\n" + - "README.md\n" + - "\n" + - "[Database] @database-team\n" + - "model/db/\n" + - "config/db/database-setup.md @docs-team"); - - assertOwners(codeOwners, "docs/api/graphql/index.md", "@docs-team"); - assertOwners(codeOwners, "/something/README.md", "@docs-team"); - - assertOwners(codeOwners, "/model/db/README.md", "@docs-team", "@database-team"); - } - - @Test - void gitlabMergeSectionsProblems() { - CodeOwners codeOwners = new CodeOwners( - "[ Documentation ] @default-user1\n" + - "*\n" + - "docs/ @docs-team1\n" + - "README.md @docs-team1\n" + - "\n" + - "[DoCuMeNtAtIoN] @default-user2\n" + // According to Gitlab this should merge with the previous section. - "docs/ @docs-team2\n" + - "README.md @docs-team2\n"); - - LOG.info("Merged codeowners:\n{}", codeOwners); - assertOwners(codeOwners, "docs/api/graphql/index.md", "@docs-team2"); - assertOwners(codeOwners, "/something/README.md", "@docs-team2"); - assertOwners(codeOwners, "README.md", "@docs-team2"); - assertOwners(codeOwners, "MatchWildCard.txt", "@default-user1", "@default-user2"); - assertTrue(codeOwners.getHasStructuralProblems()); - } - - @Test - void gitlabMergeSectionsNoProblems() { - CodeOwners codeOwners = new CodeOwners( - "[Documentation] @default-user1\n" + - "*\n" + - "docs/ @docs-team1\n" + - "\n" + - "[DoCuMeNtAtIoN] @default-user2\n" + // According to Gitlab this should merge with the previous section. - "README.md @docs-team2\n"); - - LOG.info("Merged codeowners:\n{}", codeOwners); - assertOwners(codeOwners, "docs/api/graphql/index.md", "@docs-team1"); - assertOwners(codeOwners, "/something/README.md", "@docs-team2"); - assertOwners(codeOwners, "README.md", "@docs-team2"); - assertOwners(codeOwners, "MatchWildCard.txt", "@default-user1", "@default-user2"); - assertFalse(codeOwners.getHasStructuralProblems()); - } - - @Test - void gitlabMergeSectionsOptionalMix() { - CodeOwners codeOwners = new CodeOwners( - "[Documentation] @default-user1\n" + - "*\n" + - "docs/ @docs-team1\n" + - "\n" + - "^[DoCuMeNtAtIoN] @default-user2\n" + - "README.md @docs-team2\n" + - "^[Documentation] @default-user1\n" + // Deliberate duplication! - "INSTALL.md @docs-team3\n" - ); - - LOG.info("Merged codeowners:\n------------------\n{}\n------------------", codeOwners); - assertOwners(codeOwners, "docs/api/graphql/index.md", "@docs-team1"); - assertOwners(codeOwners, "/something/README.md", "@docs-team2"); - assertOwners(codeOwners, "README.md", "@docs-team2"); - assertOwners(codeOwners, "MatchWildCard.txt", "@default-user1", "@default-user2"); - assertTrue(codeOwners.getHasStructuralProblems()); - - assertEquals( - "# CODEOWNERS file:\n" + - "[Documentation] @default-user1 @default-user2\n" + - "* \n" + - "docs/ @docs-team1\n" + - "README.md @docs-team2\n" + - "INSTALL.md @docs-team3\n" + - "\n", codeOwners.toString()); - } - - // From https://docs.gitlab.com/ee/user/project/codeowners/reference.html#example-codeowners-file - private static final String GITLAB_CODEOWNERS_DOC_EXAMPLE = - "# This is an example of a CODEOWNERS file.\n" + - "# Lines that start with `#` are ignored.\n" + - "\n" + - "# app/ @commented-rule\n" + - "\n" + - "# Specify a default Code Owner by using a wildcard:\n" + - "* @default-codeowner\n" + - "\n" + - "# Specify multiple Code Owners by using a tab or space:\n" + - "* @multiple @code @owners\n" + - "\n" + - "# Rules defined later in the file take precedence over the rules\n" + - "# defined before.\n" + - "# For example, for all files with a filename ending in `.rb`:\n" + - "*.rb @ruby-owner\n" + - "\n" + - "# Files with a `#` can still be accessed by escaping the pound sign:\n" + - "\\#file_with_pound.rb @owner-file-with-pound\n" + - "\n" + - "# Specify multiple Code Owners separated by spaces or tabs.\n" + - "# In the following case the CODEOWNERS file from the root of the repo\n" + - "# has 3 Code Owners (@multiple @code @owners):\n" + - "CODEOWNERS @multiple @code @owners\n" + - "\n" + - "# You can use both usernames or email addresses to match\n" + - "# users. Everything else is ignored. For example, this code\n" + - "# specifies the `@legal` and a user with email `janedoe@gitlab.com` as the\n" + - "# owner for the LICENSE file:\n" + - "LICENSE @legal this_does_not_match janedoe@gitlab.com\n" + - "\n" + - "# Use group names to match groups, and nested groups to specify\n" + - "# them as owners for a file:\n" + - "README @group @group/with-nested/subgroup\n" + - "\n" + - "# End a path in a `/` to specify the Code Owners for every file\n" + - "# nested in that directory, on any level:\n" + - "/docs/ @all-docs\n" + - "\n" + - "# End a path in `/*` to specify Code Owners for every file in\n" + - "# a directory, but not nested deeper. This code matches\n" + - "# `docs/index.md` but not `docs/projects/index.md`:\n" + - "/docs/* @root-docs\n" + - "\n" + - "# Include `/**` to specify Code Owners for all subdirectories\n" + - "# in a directory. This rule matches `docs/projects/index.md` or\n" + - "# `docs/development/index.md`\n" + - "/docs/**/*.md @root-docs\n" + - "\n" + - "# This code makes matches a `lib` directory nested anywhere in the repository:\n" + - "lib/ @lib-owner\n" + - "\n" + - "# This code match only a `config` directory in the root of the repository:\n" + - "/config/ @config-owner\n" + - "\n" + - "# If the path contains spaces, escape them like this:\n" + - "path\\ with\\ spaces/ @space-owner\n" + - "\n" + - "# Code Owners section:\n" + - "[Documentation]\n" + - "ee/docs @docs\n" + - "docs @docs\n" + - "\n" + - "# Use of default owners for a section. In this case, all files (*) are owned by\n" + - "# the dev team except the README.md and data-models which are owned by other teams.\n" + - "[Development] @dev-team\n" + - "*\n" + - "README.md @docs-team\n" + - "data-models/ @data-science-team\n" + - "\n" + - "# This section is combined with the previously defined [Documentation] section:\n" + - "[DOCUMENTATION]\n" + - "README.md @docs"; - - @Test - void gitlabCodeOwnersExample() { - CodeOwners codeOwners = new CodeOwners(GITLAB_CODEOWNERS_DOC_EXAMPLE); - assertOwners(codeOwners, "Foo.txt","@code","@multiple","@owners", "@dev-team"); - assertOwners(codeOwners, "Foo.rb","@ruby-owner", "@dev-team"); - assertOwners(codeOwners, "#file_with_pound.rb","@owner-file-with-pound", "@dev-team"); - - assertOwners(codeOwners, "README.md", "@docs", "@docs-team", "@code","@multiple","@owners"); - assertOwners(codeOwners, "docs/README.md", "@docs","@docs-team","@root-docs"); - } - - @Test - void allowElaborateSectionNames() { - CodeOwners codeOwners = new CodeOwners( - "[ Some Thing | And & Some $ Thing @ More ]\n" + - "README.md @docs-team\n"); - String codeOwnersString = codeOwners.toString(); - LOG.info("\n{}", codeOwnersString); - assertOwners(codeOwners, "README.md", "@docs-team"); - } - - @Test - void gitlabOptional() { - CodeOwners codeOwners = new CodeOwners( - "^[One][11] @docs-team\n" + // Optional + minimal --> Warning message - "docs/\n" + - "*.md\n" + - "\n" + - "[Two][22] @database-team\n" + - "model/db/\n" + - "config/db/database-setup.md @docs-team\n" + - "\n" + - "[Three]\n" + - "three1/ \n" + - "\n" + - "^[Four]\n" + - "four/\n" + - "\n" + - "[Three]\n" + - "three2/\n" + - "\n" - ); - - codeOwners.setVerbose(false); - assertOwners(codeOwners, "docs/api/graphql/index.md", "@docs-team"); - assertMandatoryOwners(codeOwners, "docs/api/graphql/index.md"); - - codeOwners.setVerbose(true); - assertOwners(codeOwners, "/something/README.md", "@docs-team"); - assertMandatoryOwners(codeOwners, "/something/README.md"); - - assertOwners(codeOwners, "/model/db/README.md", "@docs-team", "@database-team"); - - codeOwners.setVerbose(false); - assertEquals( - "# CODEOWNERS file:\n" + - "^[One][11] @docs-team\n" + - "docs/ \n" + - "*.md \n" + - "\n" + - "[Two][22] @database-team\n" + - "model/db/ \n" + - "config/db/database-setup.md @docs-team\n" + - "\n" + - "[Three]\n" + - "three1/ \n" + - "three2/ \n" + - "\n" + - "^[Four]\n" + - "four/ \n" + - "\n", codeOwners.toString()); - } - - @Test - void gitlabRoles() { - CodeOwners codeOwners = new CodeOwners( - "^[One][11] @docs-team @@optionalsection some-1@example.nl\n" + // Optional + minimal --> Warning message - "docs/\n" + - "*.md\n" + - "\n" + - "[Two][22] @database-team @@developer user_1_foo@example.nl\n" + - "model/db/\n" + - "config/db/database-setup.md @docs-team @@maintainer other-2_user@example.nl\n" + - "\n" - ); - - codeOwners.setVerbose(false); - assertOwners(codeOwners, "docs/api/graphql/index.md", "@docs-team", "@@optionalsection", "some-1@example.nl"); - assertMandatoryOwners(codeOwners, "docs/api/graphql/index.md"); - - codeOwners.setVerbose(true); - assertOwners(codeOwners, "/something/README.md", "@docs-team", "@@optionalsection", "some-1@example.nl"); - assertMandatoryOwners(codeOwners, "/something/README.md"); - - assertOwners(codeOwners, "/model/db/README.md", "@docs-team", "@database-team", "@@developer", "@@optionalsection", "some-1@example.nl", "user_1_foo@example.nl"); - assertMandatoryOwners(codeOwners, "/model/db/README.md", "@database-team", "@@developer", "user_1_foo@example.nl"); - - assertOwners(codeOwners, "/config/db/database-setup.md", "@docs-team", "@@maintainer", "@@optionalsection", "some-1@example.nl", "other-2_user@example.nl"); - assertMandatoryOwners(codeOwners, "/config/db/database-setup.md", "@docs-team", "@@maintainer", "other-2_user@example.nl"); - - codeOwners.setVerbose(false); - assertEquals( - "# CODEOWNERS file:\n" + - "^[One][11] @docs-team @@optionalsection some-1@example.nl\n" + - "docs/ \n" + - "*.md \n" + - "\n" + - "[Two][22] @database-team @@developer user_1_foo@example.nl\n" + - "model/db/ \n" + - "config/db/database-setup.md @docs-team @@maintainer other-2_user@example.nl\n" + - "\n", - codeOwners.toString()); - } - - - // https://docs.gitlab.com/user/project/codeowners/reference/#exclusion-patterns - @Test - void gitlabExclusionExample() { - CodeOwners codeOwners = new CodeOwners( - "# All files require approval from @username\n" + - "* @username\n" + - "\n" + - "# Except pom.xml which needs no approval\n" + - "!pom.xml\n" + - "\n" + - "[Ruby]\n" + - "# All ruby files require approval from @ruby-team\n" + - "*.rb @ruby-team\n" + - "\n" + - "# Except Ruby files in the config directory\n" + - "!/config/**/*.rb" - ); - - codeOwners.setVerbose(true); - assertOwners(codeOwners, "README.md", "@username"); - assertOwners(codeOwners, "pom.xml"); - assertOwners(codeOwners, "something.rb", "@username", "@ruby-team"); - assertOwners(codeOwners, "/somedir/something.rb", "@username", "@ruby-team"); - assertOwners(codeOwners, "/config/something.rb", "@username"); - - // Test the toString also - codeOwners.setVerbose(false); - LOG.info("CodeOwners were parsed as (non-verbose):\n{}", codeOwners); - codeOwners.setVerbose(true); - LOG.info("CodeOwners were parsed as (verbose):\n{}", codeOwners); - } - - @Test - void gitlabExclusionInOrder() { - CodeOwners codeOwners = new CodeOwners( - "* @default-owner\n" + - "!*.rb # Excludes all Ruby files.\n" + - "/special/*.rb @ruby-owner # This won't take effect as *.rb is already excluded." - ); - - codeOwners.setVerbose(true); - assertOwners(codeOwners, "README.md", "@default-owner"); - assertOwners(codeOwners, "something.rb"); - assertOwners(codeOwners, "/special/something.rb"); // << NOT @ruby-owner - } - - @Test - void gitlabExclusionNoReincludeWithinSection() { - CodeOwners codeOwners = new CodeOwners( - "[Ruby]\n" + - "*.rb @ruby-team # All Ruby files need Ruby team approval.\n" + - "!/config/**/*.rb # Ruby files in config don't need Ruby team approval.\n" + - "/config/routes.rb @ops # This won't take effect as config Ruby files are excluded." - ); - - codeOwners.setVerbose(true); - assertOwners(codeOwners, "something.rb", "@ruby-team"); - assertOwners(codeOwners, "/config/something.rb"); - assertOwners(codeOwners, "/config/subdir/something.rb"); - assertOwners(codeOwners, "/config/routes.rb"); // << NOT @ops - } - - @Test - void gitlabExclusionMultipleSections() { - CodeOwners codeOwners = new CodeOwners( - "[Ruby]\n" + - "*.rb @ruby-team\n" + - "!/config/**/*.rb # Config Ruby files don't need Ruby team approval.\n" + - "\n" + - "[Config]\n" + - "/config/ @ops-team # Config files still require ops-team approval." - ); - - codeOwners.setVerbose(true); - assertOwners(codeOwners, "something.rb", "@ruby-team"); - assertOwners(codeOwners, "/config/something.rb", "@ops-team"); - assertOwners(codeOwners, "/config/something.conf", "@ops-team"); - } - - @Test - void gitlabExclusionRealUsecase() { - CodeOwners codeOwners = new CodeOwners( - "[Code Quality][3] @quality\n" + - "*\n" + - "\n" + - "[Change Management Process][1] @changemanagement\n" + - "!/docs/\n" + - "!*.md\n" + - "!*.example\n" + - "!.gitignore\n" + - "!.prettierignore\n" + - "*" - ); - - codeOwners.setVerbose(true); - assertOwners(codeOwners, "README.md", "@quality"); // Not for *.md - assertOwners(codeOwners, "docs/README.md", "@quality"); // Not for *.md - assertOwners(codeOwners, "subdir/README.md", "@quality"); // Not for *.md - assertOwners(codeOwners, "Something.rb", "@quality", "@changemanagement"); - assertOwners(codeOwners, "docs/Something.rb", "@quality"); // Not in /docs/ - assertOwners(codeOwners, "subdir/Something.rb", "@quality", "@changemanagement"); - assertOwners(codeOwners, "Foo.gitignore", "@quality", "@changemanagement"); - assertOwners(codeOwners, ".gitignore", "@quality"); // Not .gitignore - assertOwners(codeOwners, "docs/Foo.gitignore", "@quality"); // Not in /docs/ - assertOwners(codeOwners, "docs/.gitignore", "@quality"); // Not in /docs/ AND Not .gitignore - assertOwners(codeOwners, "subdir/Foo.gitignore", "@quality", "@changemanagement"); - assertOwners(codeOwners, "subdir/.gitignore", "@quality"); // Not .gitignore - } - - - @Test - void gitlabFileMatchingBugReproduction() { - // The bug: The second line also matches "foo.gitignore" - CodeOwners codeOwners = new CodeOwners( - "*.gitignore @one\n" + - ".gitignore @two\n" - ); - codeOwners.setVerbose(true); - assertOwners(codeOwners, ".gitignore", "@two"); // Last match in a section is used - assertOwners(codeOwners, "foo.gitignore", "@one"); - assertOwners(codeOwners, "/.gitignore", "@two"); // Last match in a section is used - assertOwners(codeOwners, "/foo.gitignore", "@one"); - assertOwners(codeOwners, "/subdir/.gitignore", "@two"); - assertOwners(codeOwners, "/subdir/foo.gitignore", "@one"); - } - -} diff --git a/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwnersGitlab.kt b/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwnersGitlab.kt new file mode 100644 index 0000000..e151dbb --- /dev/null +++ b/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwnersGitlab.kt @@ -0,0 +1,577 @@ +/* + * CodeOwners Tools + * Copyright (C) 2023-2026 Niels Basjes + * + * 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 + * + * https://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 nl.basjes.codeowners + +import kotlin.test.Test +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +internal class TestCodeOwnersGitlab { + @Test + fun gitlabPathsWithSpaces() { + // Paths containing whitespace must be escaped with backslashes: path\ with\ spaces/*.md. + val codeOwners = CodeOwners( + "internalstuff/README.md @user1\n" + + "internal\\ stuff/README.md @user2\n" + ) + + TestUtils.assertOwners(codeOwners, "internalstuff/README.md", "@user1") + TestUtils.assertOwners(codeOwners, "internal stuff/README.md", "@user2") + TestUtils.assertOwners(codeOwners, "internal stuff/README.md") // No owners + } + + @Test + fun gitlabSections() { + val codeOwners = CodeOwners( + "[README Owners]\n" + + "README.md @user1 @user2\n" + + "internal/README.md @user4\n" + + "\n" + + "[README other owners]\n" + + "README.md @user3 \n" + + "\n" + + "[README default] @user5\n" + + "*.md\n" + + "SomethingElse.md @user3" + ) + + codeOwners.verbose = true + // The Code Owners for the README.md in the root directory are @user1, @user2, and @user3 + @user5 because of the extra rule for all *.md files. + TestUtils.assertOwners(codeOwners, "README.md", "@user1", "@user2", "@user3", "@user5") + // The Code Owners for internal/README.md are @user4 and @user3 + @user5 because of the extra rule for all *.md files. + TestUtils.assertOwners(codeOwners, "internal/README.md", "@user3", "@user4", "@user5") + } + + @Test + fun gitlabRelativePaths() { + // If a path does not start with a /, the path is treated as if it starts with a globstar. + // README.md is treated the same way as /**/README.md: + + // This will match /README.md, /internal/README.md, /app/lib/README.md + + val codeOwnersRoot = CodeOwners("README.md @username") + TestUtils.assertOwners(codeOwnersRoot, "/README.md", "@username") + TestUtils.assertOwners(codeOwnersRoot, "/internal/README.md", "@username") + TestUtils.assertOwners(codeOwnersRoot, "/app/lib/README.md", "@username") + + val codeOwnersSubdir = CodeOwners("internal/README.md @username") + TestUtils.assertOwners(codeOwnersSubdir, "/internal/README.md", "@username") + TestUtils.assertOwners(codeOwnersSubdir, "/docs/internal/README.md", "@username") + TestUtils.assertOwners(codeOwnersSubdir, "/docs/api/internal/README.md", "@username") + } + + @Test + fun gitlabWildcardPaths() { + val codeOwners = CodeOwners( + "# Any markdown files in the docs directory\n" + + "/docs/*.md @user1\n" + + "\n" + + "# /docs/index file of any filetype\n" + + "# For example: /docs/index.md, /docs/index.html, /docs/index.xml\n" + + "/docs/index.* @user2\n" + + "\n" + + "# Any file in the docs directory with 'spec' in the name.\n" + + "# For example: /docs/qa_specs.rb, /docs/spec_helpers.rb, /docs/runtime.spec\n" + + "/docs/*spec* @user3\n" + + "\n" + + "# README.md files one level deep within the docs directory\n" + + "# For example: /docs/api/README.md\n" + + "/docs/*/README.md @user4" + ) + + TestUtils.assertOwners(codeOwners, "/docs/test.md", "@user1") + + TestUtils.assertOwners(codeOwners, "/docs/index.md", "@user2") + TestUtils.assertOwners(codeOwners, "/docs/index.html", "@user2") + TestUtils.assertOwners(codeOwners, "/docs/index.xml", "@user2") + + TestUtils.assertOwners(codeOwners, "/docs/qa_specs.rb", "@user3") + TestUtils.assertOwners(codeOwners, "/docs/spec_helpers.rb", "@user3") + TestUtils.assertOwners(codeOwners, "/docs/runtime.spec", "@user3") + + TestUtils.assertOwners(codeOwners, "/docs/api/README.md", "@user4") + } + + @Test + fun gitlabGlobstarPaths() { + //This will match /docs/index.md, /docs/api/index.md, /docs/api/graphql/index.md + val codeOwners = CodeOwners("/docs/**/index.md @username") + TestUtils.assertOwners(codeOwners, "/docs/index.md", "@username") + TestUtils.assertOwners(codeOwners, "/docs/api/index.md", "@username") + TestUtils.assertOwners(codeOwners, "/docs/api/graphql/index.md", "@username") + } + + @Test + fun gitlabSectionsDefaults() { + val codeOwners = CodeOwners( + "[Documentation] @docs-team\n" + + "docs/\n" + + "README.md\n" + + "\n" + + "[Database] @database-team\n" + + "model/db/\n" + + "config/db/database-setup.md @docs-team" + ) + + TestUtils.assertOwners(codeOwners, "docs/api/graphql/index.md", "@docs-team") + TestUtils.assertOwners(codeOwners, "/something/README.md", "@docs-team") + + TestUtils.assertOwners(codeOwners, "/model/db/README.md", "@docs-team", "@database-team") + } + + @Test + fun gitlabMergeSectionsProblems() { + val codeOwners = CodeOwners( + "[ Documentation ] @default-user1\n" + + "*\n" + + "docs/ @docs-team1\n" + + "README.md @docs-team1\n" + + "\n" + + "[DoCuMeNtAtIoN] @default-user2\n" + // According to Gitlab this should merge with the previous section. + "docs/ @docs-team2\n" + + "README.md @docs-team2\n" + ) + + LOG.info("Merged codeowners:\n{}", codeOwners) + TestUtils.assertOwners(codeOwners, "docs/api/graphql/index.md", "@docs-team2") + TestUtils.assertOwners(codeOwners, "/something/README.md", "@docs-team2") + TestUtils.assertOwners(codeOwners, "README.md", "@docs-team2") + TestUtils.assertOwners(codeOwners, "MatchWildCard.txt", "@default-user1", "@default-user2") + assertTrue(codeOwners.hasStructuralProblems) + } + + @Test + fun gitlabMergeSectionsNoProblems() { + val codeOwners = CodeOwners( + "[Documentation] @default-user1\n" + + "*\n" + + "docs/ @docs-team1\n" + + "\n" + + "[DoCuMeNtAtIoN] @default-user2\n" + // According to Gitlab this should merge with the previous section. + "README.md @docs-team2\n" + ) + + LOG.info("Merged codeowners:\n{}", codeOwners) + TestUtils.assertOwners(codeOwners, "docs/api/graphql/index.md", "@docs-team1") + TestUtils.assertOwners(codeOwners, "/something/README.md", "@docs-team2") + TestUtils.assertOwners(codeOwners, "README.md", "@docs-team2") + TestUtils.assertOwners(codeOwners, "MatchWildCard.txt", "@default-user1", "@default-user2") + assertFalse(codeOwners.hasStructuralProblems) + } + + @Test + fun gitlabMergeSectionsOptionalMix() { + val codeOwners = CodeOwners( + "[Documentation] @default-user1\n" + + "*\n" + + "docs/ @docs-team1\n" + + "\n" + + "^[DoCuMeNtAtIoN] @default-user2\n" + + "README.md @docs-team2\n" + + "^[Documentation] @default-user1\n" + // Deliberate duplication! + "INSTALL.md @docs-team3\n" + ) + + LOG.info("Merged codeowners:\n------------------\n{}\n------------------", codeOwners) + TestUtils.assertOwners(codeOwners, "docs/api/graphql/index.md", "@docs-team1") + TestUtils.assertOwners(codeOwners, "/something/README.md", "@docs-team2") + TestUtils.assertOwners(codeOwners, "README.md", "@docs-team2") + TestUtils.assertOwners(codeOwners, "MatchWildCard.txt", "@default-user1", "@default-user2") + assertTrue(codeOwners.hasStructuralProblems) + + assertEquals( + "# CODEOWNERS file:\n" + + "[Documentation] @default-user1 @default-user2\n" + + "* \n" + + "docs/ @docs-team1\n" + + "README.md @docs-team2\n" + + "INSTALL.md @docs-team3\n" + + "\n", codeOwners.toString() + ) + } + + @Test + fun gitlabCodeOwnersExample() { + val codeOwners = CodeOwners(GITLAB_CODEOWNERS_DOC_EXAMPLE) + TestUtils.assertOwners(codeOwners, "Foo.txt", "@code", "@multiple", "@owners", "@dev-team") + TestUtils.assertOwners(codeOwners, "Foo.rb", "@ruby-owner", "@dev-team") + TestUtils.assertOwners(codeOwners, "#file_with_pound.rb", "@owner-file-with-pound", "@dev-team") + + TestUtils.assertOwners(codeOwners, "README.md", "@docs", "@docs-team", "@code", "@multiple", "@owners") + TestUtils.assertOwners(codeOwners, "docs/README.md", "@docs", "@docs-team", "@root-docs") + } + + @Test + fun allowElaborateSectionNames() { + val codeOwners = CodeOwners( + "[ Some Thing | And & Some $ Thing @ More ]\n" + + "README.md @docs-team\n" + ) + val codeOwnersString = codeOwners.toString() + LOG.info("\n{}", codeOwnersString) + TestUtils.assertOwners(codeOwners, "README.md", "@docs-team") + } + + @Test + fun gitlabOptional() { + val codeOwners = CodeOwners( + "^[One][11] @docs-team\n" + // Optional + minimal --> Warning message + "docs/\n" + + "*.md\n" + + "\n" + + "[Two][22] @database-team\n" + + "model/db/\n" + + "config/db/database-setup.md @docs-team\n" + + "\n" + + "[Three]\n" + + "three1/ \n" + + "\n" + + "^[Four]\n" + + "four/\n" + + "\n" + + "[Three]\n" + + "three2/\n" + + "\n" + ) + + codeOwners.verbose = false + TestUtils.assertOwners(codeOwners, "docs/api/graphql/index.md", "@docs-team") + TestUtils.assertMandatoryOwners(codeOwners, "docs/api/graphql/index.md") + + codeOwners.verbose = true + TestUtils.assertOwners(codeOwners, "/something/README.md", "@docs-team") + TestUtils.assertMandatoryOwners(codeOwners, "/something/README.md") + + TestUtils.assertOwners(codeOwners, "/model/db/README.md", "@docs-team", "@database-team") + + codeOwners.verbose = false + assertEquals( + "# CODEOWNERS file:\n" + + "^[One][11] @docs-team\n" + + "docs/ \n" + + "*.md \n" + + "\n" + + "[Two][22] @database-team\n" + + "model/db/ \n" + + "config/db/database-setup.md @docs-team\n" + + "\n" + + "[Three]\n" + + "three1/ \n" + + "three2/ \n" + + "\n" + + "^[Four]\n" + + "four/ \n" + + "\n", codeOwners.toString() + ) + } + + @Test + fun gitlabRoles() { + val codeOwners = CodeOwners( + "^[One][11] @docs-team @@optionalsection some-1@example.nl\n" + // Optional + minimal --> Warning message + "docs/\n" + + "*.md\n" + + "\n" + + "[Two][22] @database-team @@developer user_1_foo@example.nl\n" + + "model/db/\n" + + "config/db/database-setup.md @docs-team @@maintainer other-2_user@example.nl\n" + + "\n" + ) + + codeOwners.verbose = false + TestUtils.assertOwners( + codeOwners, + "docs/api/graphql/index.md", + "@docs-team", + "@@optionalsection", + "some-1@example.nl" + ) + TestUtils.assertMandatoryOwners(codeOwners, "docs/api/graphql/index.md") + + codeOwners.verbose = true + TestUtils.assertOwners( + codeOwners, + "/something/README.md", + "@docs-team", + "@@optionalsection", + "some-1@example.nl" + ) + TestUtils.assertMandatoryOwners(codeOwners, "/something/README.md") + + TestUtils.assertOwners( + codeOwners, + "/model/db/README.md", + "@docs-team", + "@database-team", + "@@developer", + "@@optionalsection", + "some-1@example.nl", + "user_1_foo@example.nl" + ) + TestUtils.assertMandatoryOwners( + codeOwners, + "/model/db/README.md", + "@database-team", + "@@developer", + "user_1_foo@example.nl" + ) + + TestUtils.assertOwners( + codeOwners, + "/config/db/database-setup.md", + "@docs-team", + "@@maintainer", + "@@optionalsection", + "some-1@example.nl", + "other-2_user@example.nl" + ) + TestUtils.assertMandatoryOwners( + codeOwners, + "/config/db/database-setup.md", + "@docs-team", + "@@maintainer", + "other-2_user@example.nl" + ) + + codeOwners.verbose = false + assertEquals( + "# CODEOWNERS file:\n" + + "^[One][11] @docs-team @@optionalsection some-1@example.nl\n" + + "docs/ \n" + + "*.md \n" + + "\n" + + "[Two][22] @database-team @@developer user_1_foo@example.nl\n" + + "model/db/ \n" + + "config/db/database-setup.md @docs-team @@maintainer other-2_user@example.nl\n" + + "\n", + codeOwners.toString() + ) + } + + + // https://docs.gitlab.com/user/project/codeowners/reference/#exclusion-patterns + @Test + fun gitlabExclusionExample() { + val codeOwners = CodeOwners( + "# All files require approval from @username\n" + + "* @username\n" + + "\n" + + "# Except pom.xml which needs no approval\n" + + "!pom.xml\n" + + "\n" + + "[Ruby]\n" + + "# All ruby files require approval from @ruby-team\n" + + "*.rb @ruby-team\n" + + "\n" + + "# Except Ruby files in the config directory\n" + + "!/config/**/*.rb" + ) + + codeOwners.verbose = true + TestUtils.assertOwners(codeOwners, "README.md", "@username") + TestUtils.assertOwners(codeOwners, "pom.xml") + TestUtils.assertOwners(codeOwners, "something.rb", "@username", "@ruby-team") + TestUtils.assertOwners(codeOwners, "/somedir/something.rb", "@username", "@ruby-team") + TestUtils.assertOwners(codeOwners, "/config/something.rb", "@username") + + // Test the toString also + codeOwners.verbose = false + LOG.info("CodeOwners were parsed as (non-verbose):\n{}", codeOwners) + codeOwners.verbose = true + LOG.info("CodeOwners were parsed as (verbose):\n{}", codeOwners) + } + + @Test + fun gitlabExclusionInOrder() { + val codeOwners = CodeOwners( + """ + * @default-owner + # Excludes all Ruby files. + !*.rb + # This won't take effect as *.rb is already excluded. + /special/*.rb @ruby-owner + """.trimIndent() + ) + + codeOwners.verbose = true + TestUtils.assertOwners(codeOwners, "README.md", "@default-owner") + TestUtils.assertOwners(codeOwners, "something.rb") + TestUtils.assertOwners(codeOwners, "/special/something.rb") // << NOT @ruby-owner + } + + @Test + fun gitlabExclusionNoReincludeWithinSection() { + val codeOwners = CodeOwners( + "[Ruby]\n" + + "*.rb @ruby-team # All Ruby files need Ruby team approval.\n" + + "!/config/**/*.rb # Ruby files in config don't need Ruby team approval.\n" + + "/config/routes.rb @ops # This won't take effect as config Ruby files are excluded." + ) + + codeOwners.verbose = true + TestUtils.assertOwners(codeOwners, "something.rb", "@ruby-team") + TestUtils.assertOwners(codeOwners, "/config/something.rb") + TestUtils.assertOwners(codeOwners, "/config/subdir/something.rb") + TestUtils.assertOwners(codeOwners, "/config/routes.rb") // << NOT @ops + } + + @Test + fun gitlabExclusionMultipleSections() { + val codeOwners = CodeOwners( + "[Ruby]\n" + + "*.rb @ruby-team\n" + + "!/config/**/*.rb # Config Ruby files don't need Ruby team approval.\n" + + "\n" + + "[Config]\n" + + "/config/ @ops-team # Config files still require ops-team approval." + ) + + codeOwners.verbose = true + TestUtils.assertOwners(codeOwners, "something.rb", "@ruby-team") + TestUtils.assertOwners(codeOwners, "/config/something.rb", "@ops-team") + TestUtils.assertOwners(codeOwners, "/config/something.conf", "@ops-team") + } + + @Test + fun gitlabExclusionRealUsecase() { + val codeOwners = CodeOwners( + "[Code Quality][3] @quality\n" + + "*\n" + + "\n" + + "[Change Management Process][1] @changemanagement\n" + + "!/docs/\n" + + "!*.md\n" + + "!*.example\n" + + "!.gitignore\n" + + "!.prettierignore\n" + + "*" + ) + + codeOwners.verbose = true + TestUtils.assertOwners(codeOwners, "README.md", "@quality") // Not for *.md + TestUtils.assertOwners(codeOwners, "docs/README.md", "@quality") // Not for *.md + TestUtils.assertOwners(codeOwners, "subdir/README.md", "@quality") // Not for *.md + TestUtils.assertOwners(codeOwners, "Something.rb", "@quality", "@changemanagement") + TestUtils.assertOwners(codeOwners, "docs/Something.rb", "@quality") // Not in /docs/ + TestUtils.assertOwners(codeOwners, "subdir/Something.rb", "@quality", "@changemanagement") + TestUtils.assertOwners(codeOwners, "Foo.gitignore", "@quality", "@changemanagement") + TestUtils.assertOwners(codeOwners, ".gitignore", "@quality") // Not .gitignore + TestUtils.assertOwners(codeOwners, "docs/Foo.gitignore", "@quality") // Not in /docs/ + TestUtils.assertOwners(codeOwners, "docs/.gitignore", "@quality") // Not in /docs/ AND Not .gitignore + TestUtils.assertOwners(codeOwners, "subdir/Foo.gitignore", "@quality", "@changemanagement") + TestUtils.assertOwners(codeOwners, "subdir/.gitignore", "@quality") // Not .gitignore + } + + + @Test + fun gitlabFileMatchingBugReproduction() { + // The bug: The second line also matches "foo.gitignore" + val codeOwners = CodeOwners( + "*.gitignore @one\n" + + ".gitignore @two\n" + ) + codeOwners.verbose = true + TestUtils.assertOwners(codeOwners, ".gitignore", "@two") // Last match in a section is used + TestUtils.assertOwners(codeOwners, "foo.gitignore", "@one") + TestUtils.assertOwners(codeOwners, "/.gitignore", "@two") // Last match in a section is used + TestUtils.assertOwners(codeOwners, "/foo.gitignore", "@one") + TestUtils.assertOwners(codeOwners, "/subdir/.gitignore", "@two") + TestUtils.assertOwners(codeOwners, "/subdir/foo.gitignore", "@one") + } + + companion object { + private val LOG: Logger = LoggerFactory.getLogger(TestCodeOwnersGitlab::class.java) + + // From https://docs.gitlab.com/ee/user/project/codeowners/reference.html#example-codeowners-file + private val GITLAB_CODEOWNERS_DOC_EXAMPLE = """ + # This is an example of a CODEOWNERS file. + # Lines that start with `#` are ignored. + + # app/ @commented-rule + + # Specify a default Code Owner by using a wildcard: + * @default-codeowner + + # Specify multiple Code Owners by using a tab or space: + * @multiple @code @owners + + # Rules defined later in the file take precedence over the rules + # defined before. + # For example, for all files with a filename ending in `.rb`: + *.rb @ruby-owner + + # Files with a `#` can still be accessed by escaping the pound sign: + \#file_with_pound.rb @owner-file-with-pound + + # Specify multiple Code Owners separated by spaces or tabs. + # In the following case the CODEOWNERS file from the root of the repo + # has 3 Code Owners (@multiple @code @owners): + CODEOWNERS @multiple @code @owners + + # You can use both usernames or email addresses to match + # users. Everything else is ignored. For example, this code + # specifies the `@legal` and a user with email `janedoe@gitlab.com` as the + # owner for the LICENSE file: + LICENSE @legal this_does_not_match janedoe@gitlab.com + + # Use group names to match groups, and nested groups to specify + # them as owners for a file: + README @group @group/with-nested/subgroup + + # End a path in a `/` to specify the Code Owners for every file + # nested in that directory, on any level: + /docs/ @all-docs + + # End a path in `/*` to specify Code Owners for every file in + # a directory, but not nested deeper. This code matches + # `docs/index.md` but not `docs/projects/index.md`: + /docs/* @root-docs + + # Include `/**` to specify Code Owners for all subdirectories + # in a directory. This rule matches `docs/projects/index.md` or + # `docs/development/index.md` + /docs/**/*.md @root-docs + + # This code makes matches a `lib` directory nested anywhere in the repository: + lib/ @lib-owner + + # This code match only a `config` directory in the root of the repository: + /config/ @config-owner + + # If the path contains spaces, escape them like this: + path\ with\ spaces/ @space-owner + + # Code Owners section: + [Documentation] + ee/docs @docs + docs @docs + + # Use of default owners for a section. In this case, all files (*) are owned by + # the dev team except the README.md and data-models which are owned by other teams. + [Development] @dev-team + * + README.md @docs-team + data-models/ @data-science-team + + # This section is combined with the previously defined [Documentation] section: + [DOCUMENTATION] + README.md @docs""".trimIndent() + } +} diff --git a/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwnersOrdering.java b/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwnersOrdering.java deleted file mode 100644 index aaf9497..0000000 --- a/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwnersOrdering.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * CodeOwners Tools - * Copyright (C) 2023-2026 Niels Basjes - * - * 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 - * - * https://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 nl.basjes.codeowners; - -import org.junit.jupiter.api.Test; - -import static nl.basjes.codeowners.TestUtils.assertMandatoryOwnersCheckOrdering; -import static nl.basjes.codeowners.TestUtils.assertOwnersCheckOrdering; - -class TestCodeOwnersOrdering { - - @Test - void ensureMaintainingOrdering() { - CodeOwners codeOwners = new CodeOwners( - "[README Owners]\n" + - "README.md @user5 @user2 @user5\n" + // extra user5 to test deduplication - "\n" + - "^[README other owners]\n" + // Optional section - "README.md @user3 \n" + - "\n" + - "[README default] @user2 @user1 @user2\n" + // extra user2 to test deduplication - "*.md\n" + - "SomethingElse.md @user3\n" + - "\n" + - "[README Owners]\n" + - // Because this rule is ADDED to an existing section (i.e. earlier in the sections list) - // the result will be earlier in the source sorted output. - "internal/README.md @user4\n" - ); - - assertOwnersCheckOrdering(codeOwners, - "README.md", "@user5", "@user2", "@user3", "@user1"); - assertOwnersCheckOrdering(codeOwners, - // Note that the user4 at the front is correct - "internal/README.md", "@user4", "@user3", "@user2", "@user1" ); - - // Mandatory owners means we skip the optional sections --> No more user3 for these testcases - assertMandatoryOwnersCheckOrdering(codeOwners, - "README.md", "@user5", "@user2", "@user1"); - assertMandatoryOwnersCheckOrdering(codeOwners, - // Note that the user4 at the front is correct - "internal/README.md", "@user4", "@user2", "@user1" ); - } - - @Test - void ensureMaintainingOrderingOptionalDeduplicateSwitch() { - CodeOwners codeOwners = new CodeOwners( - "[README Owners]\n" + - "README.md @user1\n" + - "\n" + - "^[README other owners]\n" + // Optional section - "README.md @user3 \n" + - "\n" + - "[README default] @user2 @user3\n" + - "*.md\n" + - "\n" - ); - - assertOwnersCheckOrdering(codeOwners, - "README.md", "@user1", "@user3", "@user2"); - - // Mandatory owners means we skip the optional sections - // Because the user3 is no longer present in the optional section and - // it is present in a later section it will have changed place in the final result list. - assertMandatoryOwnersCheckOrdering(codeOwners, - "README.md", "@user1", "@user2", "@user3"); - } -} diff --git a/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwnersOrdering.kt b/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwnersOrdering.kt new file mode 100644 index 0000000..146e32b --- /dev/null +++ b/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestCodeOwnersOrdering.kt @@ -0,0 +1,89 @@ +/* + * CodeOwners Tools + * Copyright (C) 2023-2026 Niels Basjes + * + * 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 + * + * https://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 nl.basjes.codeowners + +import kotlin.test.Test + +internal class TestCodeOwnersOrdering { + @Test + fun ensureMaintainingOrdering() { + val codeOwners = CodeOwners( + """ + [README Owners] + README.md @user5 @user2 @user5 + + ^[README other owners] + README.md @user3 + + [README default] @user2 @user1 @user2 + *.md + SomethingElse.md @user3 + + [README Owners] + internal/README.md @user4 + """.trimIndent() + ) + + TestUtils.assertOwnersCheckOrdering( + codeOwners, + "README.md", "@user5", "@user2", "@user3", "@user1" + ) + TestUtils.assertOwnersCheckOrdering( + codeOwners, // Note that the user4 at the front is correct + "internal/README.md", "@user4", "@user3", "@user2", "@user1" + ) + + // Mandatory owners means we skip the optional sections --> No more user3 for these testcases + TestUtils.assertMandatoryOwnersCheckOrdering( + codeOwners, + "README.md", "@user5", "@user2", "@user1" + ) + TestUtils.assertMandatoryOwnersCheckOrdering( + codeOwners, // Note that the user4 at the front is correct + "internal/README.md", "@user4", "@user2", "@user1" + ) + } + + @Test + fun ensureMaintainingOrderingOptionalDeduplicateSwitch() { + val codeOwners = CodeOwners( + """ + [README Owners] + README.md @user1 + + ^[README other owners] + README.md @user3 + + [README default] @user2 @user3 + *.md + """.trimIndent() + ) + + TestUtils.assertOwnersCheckOrdering( + codeOwners, + "README.md", "@user1", "@user3", "@user2" + ) + + // Mandatory owners means we skip the optional sections + // Because the user3 is no longer present in the optional section and + // it is present in a later section it will have changed place in the final result list. + TestUtils.assertMandatoryOwnersCheckOrdering( + codeOwners, + "README.md", "@user1", "@user2", "@user3" + ) + } +} diff --git a/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestTrailingGlobstarPattern.java b/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestTrailingGlobstarPattern.java deleted file mode 100644 index 9d9280e..0000000 --- a/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestTrailingGlobstarPattern.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * CodeOwners Tools - * Copyright (C) 2023-2026 Niels Basjes - * - * 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 - * - * https://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 nl.basjes.codeowners; - -import org.junit.jupiter.api.Test; - -import static nl.basjes.codeowners.TestUtils.assertOwners; - -class TestTrailingGlobstarPattern { - - /** - * Test case for patterns ending with /** (trailing globstar). - * - * The pattern "dirname/**" should match in the root and in a subdirectory: - * - dirname/ (directory itself) - * - dirname/file (files inside the directory) - * - dirname/subdir/file (files in subdirectories) - * - * But should NOT match in the root and in a subdirectory: - * - dirname (file at root level with the same name) - * - dirname.properties (file at root level starting with dirname) - * - dirname-something (file at root level starting with dirname) - * - * This ensures that earlier patterns like "*" can match files that merely - * start with the directory name, following the "last match wins" rule. - */ - - @Test - void testTrailingGlobstarMatching() { - CodeOwners codeOwners = new CodeOwners( - "* @mismatch\n" + // Fall back to ensure we always get a response - "dirname/** @match\n" - ); - - // Match in root - assertOwners(codeOwners, "dirname/", "@match"); // directory itself - assertOwners(codeOwners, "dirname/file", "@match"); // files inside the directory - assertOwners(codeOwners, "dirname/subdir/file", "@match"); // files in subdirectories - - // Match in subdir/ - assertOwners(codeOwners, "subdir/dirname/", "@match"); // directory itself - assertOwners(codeOwners, "subdir/dirname/file", "@match"); // files inside the directory - assertOwners(codeOwners, "subdir/dirname/subdir/file", "@match"); // files in subdirectories - - // Match in subdir/subdir/ - assertOwners(codeOwners, "subdir/subdir/dirname/", "@match"); // directory itself - assertOwners(codeOwners, "subdir/subdir/dirname/file", "@match"); // files inside the directory - assertOwners(codeOwners, "subdir/subdir/dirname/subdir/file", "@match"); // files in subdirectories - - // Do NOT match files with the name in root - assertOwners(codeOwners, "dirname", "@mismatch"); // file with the same name - assertOwners(codeOwners, "dirname.properties", "@mismatch"); // file starting with dirname - assertOwners(codeOwners, "dirname-something", "@mismatch"); // file starting with dirname - - // Do NOT match files with the name in subdir/ - assertOwners(codeOwners, "subdir/dirname", "@mismatch"); // file with the same name - assertOwners(codeOwners, "subdir/dirname.properties", "@mismatch"); // file starting with dirname - assertOwners(codeOwners, "subdir/dirname-something", "@mismatch"); // file starting with dirname - - // Do NOT match files with the name in subdir/subdir/ - assertOwners(codeOwners, "subdir/subdir/dirname", "@mismatch"); // file with the same name - assertOwners(codeOwners, "subdir/subdir/dirname.properties", "@mismatch"); // file starting with dirname - assertOwners(codeOwners, "subdir/subdir/dirname-something", "@mismatch"); // file starting with dirname - } - -} diff --git a/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestTrailingGlobstarPattern.kt b/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestTrailingGlobstarPattern.kt new file mode 100644 index 0000000..b883214 --- /dev/null +++ b/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestTrailingGlobstarPattern.kt @@ -0,0 +1,78 @@ +/* + * CodeOwners Tools + * Copyright (C) 2023-2026 Niels Basjes + * + * 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 + * + * https://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 nl.basjes.codeowners + +import kotlin.test.Test + +internal class TestTrailingGlobstarPattern { + /** + * Test case for patterns ending with / ** (trailing globstar). + * + * The pattern "dirname/ **" should match in the root and in a subdirectory: + * - dirname/ (directory itself) + * - dirname/file (files inside the directory) + * - dirname/subdir/file (files in subdirectories) + * + * But should NOT match in the root and in a subdirectory: + * - dirname (file at root level with the same name) + * - dirname.properties (file at root level starting with dirname) + * - dirname-something (file at root level starting with dirname) + * + * This ensures that earlier patterns like "*" can match files that merely + * start with the directory name, following the "last match wins" rule. + */ + @Test + fun testTrailingGlobstarMatching() { + val codeOwners = CodeOwners( + """ + * @mismatch + dirname/** @match + + """.trimIndent() + ) + + // Match in root + TestUtils.assertOwners(codeOwners, "dirname/", "@match") // directory itself + TestUtils.assertOwners(codeOwners, "dirname/file", "@match") // files inside the directory + TestUtils.assertOwners(codeOwners, "dirname/subdir/file", "@match") // files in subdirectories + + // Match in subdir/ + TestUtils.assertOwners(codeOwners, "subdir/dirname/", "@match") // directory itself + TestUtils.assertOwners(codeOwners, "subdir/dirname/file", "@match") // files inside the directory + TestUtils.assertOwners(codeOwners, "subdir/dirname/subdir/file", "@match") // files in subdirectories + + // Match in subdir/subdir/ + TestUtils.assertOwners(codeOwners, "subdir/subdir/dirname/", "@match") // directory itself + TestUtils.assertOwners(codeOwners, "subdir/subdir/dirname/file", "@match") // files inside the directory + TestUtils.assertOwners(codeOwners, "subdir/subdir/dirname/subdir/file", "@match") // files in subdirectories + + // Do NOT match files with the name in root + TestUtils.assertOwners(codeOwners, "dirname", "@mismatch") // file with the same name + TestUtils.assertOwners(codeOwners, "dirname.properties", "@mismatch") // file starting with dirname + TestUtils.assertOwners(codeOwners, "dirname-something", "@mismatch") // file starting with dirname + + // Do NOT match files with the name in subdir/ + TestUtils.assertOwners(codeOwners, "subdir/dirname", "@mismatch") // file with the same name + TestUtils.assertOwners(codeOwners, "subdir/dirname.properties", "@mismatch") // file starting with dirname + TestUtils.assertOwners(codeOwners, "subdir/dirname-something", "@mismatch") // file starting with dirname + + // Do NOT match files with the name in subdir/subdir/ + TestUtils.assertOwners(codeOwners, "subdir/subdir/dirname", "@mismatch") // file with the same name + TestUtils.assertOwners(codeOwners, "subdir/subdir/dirname.properties", "@mismatch") // file starting with dirname + TestUtils.assertOwners(codeOwners, "subdir/subdir/dirname-something", "@mismatch") // file starting with dirname + } +} diff --git a/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestUtils.java b/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestUtils.java deleted file mode 100644 index 3b7d764..0000000 --- a/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestUtils.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * CodeOwners Tools - * Copyright (C) 2023-2026 Niels Basjes - * - * 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 - * - * https://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 nl.basjes.codeowners; - -import org.opentest4j.AssertionFailedError; - -import java.util.Arrays; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -public class TestUtils { - public static void assertOwners(CodeOwners codeOwners, String filename, String... expectedOwners) { - assertOwnersInternal(codeOwners, filename, true, expectedOwners); - assertOwnersInternal(codeOwners, windowsFileName(filename), true, expectedOwners); - } - - public static void assertOwnersCheckOrdering(CodeOwners codeOwners, String filename, String... expectedOwners) { - assertOwnersInternal(codeOwners, filename, false, expectedOwners); - assertOwnersInternal(codeOwners, windowsFileName(filename), false, expectedOwners); - } - - private static void assertOwnersInternal(CodeOwners codeOwners, String filename, boolean anyOrderIsValid, String... expectedApproversParam) { - List expectedApprovers = Arrays.asList(expectedApproversParam); - List actualApprovers = codeOwners.getAllApprovers(filename); - if (anyOrderIsValid) { - expectedApprovers.sort(String::compareTo); - actualApprovers.sort(String::compareTo); - } - try { - assertEquals( - expectedApprovers, - actualApprovers, - "Filename \"" + filename + "\" should have approvers " + expectedApprovers + " but got " + actualApprovers); - } catch (AssertionFailedError afe) { - codeOwners.setVerbose(true); - codeOwners.getAllApprovers(filename); - codeOwners.setVerbose(false); - throw afe; - } - } - - public static void assertMandatoryOwners(CodeOwners codeOwners, String filename, String... expectedOwners) { - assertMandatoryOwnersInternal(codeOwners, filename, true, expectedOwners); - assertMandatoryOwnersInternal(codeOwners, windowsFileName(filename), true, expectedOwners); - } - public static void assertMandatoryOwnersCheckOrdering(CodeOwners codeOwners, String filename, String... expectedOwners) { - assertMandatoryOwnersInternal(codeOwners, filename, false, expectedOwners); - assertMandatoryOwnersInternal(codeOwners, windowsFileName(filename), false, expectedOwners); - } - - - private static void assertMandatoryOwnersInternal(CodeOwners codeOwners, String filename, boolean anyOrderIsValid, String... expectedApproversParam) { - List expectedApprovers = Arrays.asList(expectedApproversParam); - List actualApprovers = codeOwners.getMandatoryApprovers(filename); - if (anyOrderIsValid) { - expectedApprovers.sort(String::compareTo); - actualApprovers.sort(String::compareTo); - } - try { - assertEquals( - expectedApprovers, - actualApprovers, - "Filename \"" + filename + "\" should have mandatory approvers " + expectedApprovers + " but got " + actualApprovers); - } catch (AssertionFailedError afe) { - codeOwners.setVerbose(true); - codeOwners.getMandatoryApprovers(filename); - codeOwners.setVerbose(false); - throw afe; - } - } - - public static void assertOwners(String codeOwners, String filename, String... expectedOwners) { - assertOwners(new CodeOwners(codeOwners), filename, expectedOwners); - } - - public static String windowsFileName(String filename) { - return filename.replace("/", "\\"); - } - -} diff --git a/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestUtils.kt b/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestUtils.kt new file mode 100644 index 0000000..d7aa6f7 --- /dev/null +++ b/codeowners-reader/src/test/kotlin/nl/basjes/codeowners/TestUtils.kt @@ -0,0 +1,99 @@ +/* + * CodeOwners Tools + * Copyright (C) 2023-2026 Niels Basjes + * + * 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 + * + * https://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 nl.basjes.codeowners + +import org.opentest4j.AssertionFailedError +import kotlin.test.assertEquals + +object TestUtils { + fun assertOwners(codeOwners: CodeOwners, filename: String, vararg expectedOwners: String) { + assertOwnersInternal(codeOwners, filename, true, *expectedOwners) + assertOwnersInternal(codeOwners, windowsFileName(filename), true, *expectedOwners) + } + + fun assertOwnersCheckOrdering(codeOwners: CodeOwners, filename: String, vararg expectedOwners: String) { + assertOwnersInternal(codeOwners, filename, false, *expectedOwners) + assertOwnersInternal(codeOwners, windowsFileName(filename), false, *expectedOwners) + } + + private fun assertOwnersInternal( + codeOwners: CodeOwners, + filename: String, + anyOrderIsValid: Boolean, + vararg expectedApproversParam: String + ) { + var expectedApprovers = listOf(*expectedApproversParam) + var actualApprovers = codeOwners.getAllApprovers(filename) + if (anyOrderIsValid) { + expectedApprovers = expectedApprovers.sorted() + actualApprovers = actualApprovers.sorted() + } + try { + assertEquals( + expectedApprovers, + actualApprovers, + "Filename \"$filename\" should have approvers $expectedApprovers but got $actualApprovers" + ) + } catch (afe: AssertionFailedError) { + codeOwners.verbose = true + codeOwners.getAllApprovers(filename) + codeOwners.verbose = false + throw afe + } + } + + fun assertMandatoryOwners(codeOwners: CodeOwners, filename: String, vararg expectedOwners: String) { + assertMandatoryOwnersInternal(codeOwners, filename, true, *expectedOwners) + assertMandatoryOwnersInternal(codeOwners, windowsFileName(filename), true, *expectedOwners) + } + + fun assertMandatoryOwnersCheckOrdering(codeOwners: CodeOwners, filename: String, vararg expectedOwners: String) { + assertMandatoryOwnersInternal(codeOwners, filename, false, *expectedOwners) + assertMandatoryOwnersInternal(codeOwners, windowsFileName(filename), false, *expectedOwners) + } + + + private fun assertMandatoryOwnersInternal( + codeOwners: CodeOwners, + filename: String, + anyOrderIsValid: Boolean, + vararg expectedApproversParam: String + ) { + var expectedApprovers = mutableListOf(*expectedApproversParam) + var actualApprovers: MutableList = codeOwners.getMandatoryApprovers(filename).toMutableList() + if (anyOrderIsValid) { + expectedApprovers = expectedApprovers.sorted().toMutableList() + actualApprovers = actualApprovers.sorted().toMutableList() + } + try { + assertEquals( + expectedApprovers, + actualApprovers, + "Filename \"$filename\" should have mandatory approvers $expectedApprovers but got $actualApprovers" + ) + } catch (afe: AssertionFailedError) { + codeOwners.verbose = true + codeOwners.getMandatoryApprovers(filename) + codeOwners.verbose = false + throw afe + } + } + + fun windowsFileName(filename: String): String { + return filename.replace("/", "\\") + } +} diff --git a/codeowners-validator/pom.xml b/codeowners-validator/pom.xml index e625786..ac0d450 100644 --- a/codeowners-validator/pom.xml +++ b/codeowners-validator/pom.xml @@ -111,8 +111,8 @@ - org.junit.jupiter - junit-jupiter-engine + org.jetbrains.kotlin + kotlin-test-junit5 test diff --git a/codeowners-validator/src/main/kotlin/nl/basjes/codeowners/validator/gitlab/GitlabConfiguration.kt b/codeowners-validator/src/main/kotlin/nl/basjes/codeowners/validator/gitlab/GitlabConfiguration.kt index fe85e9b..4df3308 100644 --- a/codeowners-validator/src/main/kotlin/nl/basjes/codeowners/validator/gitlab/GitlabConfiguration.kt +++ b/codeowners-validator/src/main/kotlin/nl/basjes/codeowners/validator/gitlab/GitlabConfiguration.kt @@ -139,7 +139,7 @@ ${problemLevels.toString().prependIndent(" ")} return value != null && NON_SPACE_STRING.matcher(value).matches() } - protected abstract val sanitizedValue: String + abstract val sanitizedValue: String fun toString(name: String): String { load() diff --git a/codeowners-validator/src/main/kotlin/nl/basjes/codeowners/validator/gitlab/GitlabProjectMembers.kt b/codeowners-validator/src/main/kotlin/nl/basjes/codeowners/validator/gitlab/GitlabProjectMembers.kt index 45db5d1..331bcba 100644 --- a/codeowners-validator/src/main/kotlin/nl/basjes/codeowners/validator/gitlab/GitlabProjectMembers.kt +++ b/codeowners-validator/src/main/kotlin/nl/basjes/codeowners/validator/gitlab/GitlabProjectMembers.kt @@ -148,18 +148,13 @@ class GitlabProjectMembers(configuration: GitlabConfiguration) : AutoCloseable { ) { when (level) { Level.FATAL -> table.addProblem( - Fatal( - section.getName(), - approvalRule.getFileExpression(), - approver, - message - ) + Fatal(section.name,approvalRule.fileExpression, approver, message) ) Level.ERROR -> table.addProblem( Problem.Error( - section.getName(), - approvalRule.getFileExpression(), + section.name, + approvalRule.fileExpression, approver, message ) @@ -167,8 +162,8 @@ class GitlabProjectMembers(configuration: GitlabConfiguration) : AutoCloseable { Level.WARNING -> table.addProblem( Problem.Warning( - section.getName(), - approvalRule.getFileExpression(), + section.name, + approvalRule.fileExpression, approver, message ) @@ -176,8 +171,8 @@ class GitlabProjectMembers(configuration: GitlabConfiguration) : AutoCloseable { Level.INFO -> table.addProblem( Problem.Info( - section.getName(), - approvalRule.getFileExpression(), + section.name, + approvalRule.fileExpression, approver, message ) @@ -360,8 +355,8 @@ class GitlabProjectMembers(configuration: GitlabConfiguration) : AutoCloseable { } // Is this a Member? - if (allProjectMembers.containsKey(approver)) { - val member: Member = allProjectMembers[approver]!! + val member = allProjectMembers[approver] + if (member != null) { if (!usableAccount(member)) { report( userDisabled, results, definedSection, rule, rawApprover, @@ -393,8 +388,8 @@ class GitlabProjectMembers(configuration: GitlabConfiguration) : AutoCloseable { } // Is this a Shared Group? - if (sharedGroups.containsKey(approver)) { - val sharedGroup: SharedGroup = sharedGroups[approver]!! + val sharedGroup = sharedGroups[approver] + if (sharedGroup != null) { val groupAccessLevel = sharedGroup.groupAccessLevel if (accessLevelCanApprove(groupAccessLevel)) { // Success, we have found this valid approver to be the name of a shared group. diff --git a/codeowners-validator/src/main/kotlin/nl/basjes/codeowners/validator/utils/ProblemTable.kt b/codeowners-validator/src/main/kotlin/nl/basjes/codeowners/validator/utils/ProblemTable.kt index 8b83295..0339ed9 100644 --- a/codeowners-validator/src/main/kotlin/nl/basjes/codeowners/validator/utils/ProblemTable.kt +++ b/codeowners-validator/src/main/kotlin/nl/basjes/codeowners/validator/utils/ProblemTable.kt @@ -24,6 +24,7 @@ import nl.basjes.codeowners.validator.utils.Problem.ProblemColor.COLOR_INFO_TEXT import nl.basjes.codeowners.validator.utils.Problem.Tags.INFO_TAG import nl.basjes.codeowners.validator.utils.Problem.Warning import org.slf4j.Logger +import kotlin.collections.filterIsInstance class ProblemTable : StringTable() { @@ -49,19 +50,19 @@ class ProblemTable : StringTable() { val numberOfProblems: Int get() = theProblems.size - val numberOfFatalErrors: Long - get() = theProblems.stream() - .filter { it is Fatal } + val numberOfFatalErrors: Int + get() = theProblems + .filterIsInstance() .count() - val numberOfErrors: Long - get() = theProblems.stream() - .filter { it is Error } + val numberOfErrors: Int + get() = theProblems + .filterIsInstance() .count() - val numberOfWarnings: Long - get() = theProblems.stream() - .filter { it is Warning } + val numberOfWarnings: Int + get() = theProblems + .filterIsInstance() .count() fun contains(problem: Problem?): Boolean { diff --git a/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/TestCodeOwnersValidator.java b/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/TestCodeOwnersValidator.java deleted file mode 100644 index 23aa8a2..0000000 --- a/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/TestCodeOwnersValidator.java +++ /dev/null @@ -1,174 +0,0 @@ -/* - * CodeOwners Tools - * Copyright (C) 2023-2026 Niels Basjes - * - * 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 - * - * https://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 nl.basjes.codeowners.validator; - -import nl.basjes.codeowners.validator.CodeOwnersValidator.DirectoryOwners; -import nl.basjes.codeowners.validator.CodeOwnersValidator.FileOwners; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.File; -import java.nio.file.Path; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; - -public class TestCodeOwnersValidator { - - private static final Logger log = LoggerFactory.getLogger(TestCodeOwnersValidator.class); - - private DirectoryOwners analyze(String directoryName, boolean verbose) throws CodeOwnersValidationException { - CodeOwnersValidator validator = new CodeOwnersValidator(null, null, verbose); - - DirectoryOwners directoryOwners = validator.analyzeDirectory(new File("src/test/resources/testdirectories/"+directoryName)); - - log.info("\n{}", directoryOwners.toTable()); - log.info("\n{}", directoryOwners.toJson()); - return directoryOwners; - } - - private static void assertMandatoryApprovers(DirectoryOwners directoryOwners, String path, String... expected) { - Path asPath = Path.of(path); - assertNotNull(asPath, "Unable to convert " +path+ " into a Path."); - FileOwners fileOwners = directoryOwners.get(asPath); - assertNotNull(fileOwners, "No FileOwners found for " + asPath); - assertEquals(List.of(expected), fileOwners.mandatoryApprovers); - } - - private static void assertIgnored(DirectoryOwners directoryOwners, String path) { - Path asPath = Path.of(path); - assertNotNull(asPath, "Unable to convert " +path+ " into a Path."); - FileOwners fileOwners = directoryOwners.get(asPath); - assertNull(fileOwners, "No FileOwners should have been found for " + asPath); - } - - @Test - void testCovered() throws CodeOwnersValidationException { - DirectoryOwners directoryOwners = analyze("covered", false); - - assertMandatoryApprovers(directoryOwners, "", "@integrationtest"); - assertMandatoryApprovers(directoryOwners, ".gitlab/", "@integrationtest"); - assertMandatoryApprovers(directoryOwners, ".gitlab/CODEOWNERS", "@nielsbasjes"); - assertMandatoryApprovers(directoryOwners, "pom.xml", "@integrationtest"); - assertMandatoryApprovers(directoryOwners, "src/", "@integrationtest"); - assertMandatoryApprovers(directoryOwners, "src/main/", "@projectteam"); - assertMandatoryApprovers(directoryOwners, "src/main/README.java", "@projectteam"); - assertMandatoryApprovers(directoryOwners, "src/main/README.md", "@username"); - assertMandatoryApprovers(directoryOwners, "src/main/README.txt", "@projectteam"); - } - - @Test - void testGitignore() throws CodeOwnersValidationException { - DirectoryOwners directoryOwners = analyze("gitignore", false); - - assertMandatoryApprovers(directoryOwners, "", "@integrationtest" ); - assertMandatoryApprovers(directoryOwners, ".gitignore", "@integrationtest" ); - assertMandatoryApprovers(directoryOwners, ".gitlab/", "@integrationtest" ); - assertMandatoryApprovers(directoryOwners, ".gitlab/CODEOWNERS", "@nielsbasjes" ); - assertMandatoryApprovers(directoryOwners, "pom.xml", "@integrationtest" ); - assertMandatoryApprovers(directoryOwners, "src/", "@integrationtest" ); - assertMandatoryApprovers(directoryOwners, "src/.gitignore", "@nielsbasjes" ); - assertMandatoryApprovers(directoryOwners, "src/main/", "@projectteam" ); - assertMandatoryApprovers(directoryOwners, "src/main/README.md", "@username" ); - - assertIgnored(directoryOwners, "build.log"); - assertIgnored(directoryOwners, "src/main/README.java"); - assertIgnored(directoryOwners, "src/main/README.txt"); - } - - @Test - void testMissing() throws CodeOwnersValidationException { - DirectoryOwners directoryOwners = analyze("missing", false); - - assertMandatoryApprovers(directoryOwners, "", "@integrationtest" ); - assertMandatoryApprovers(directoryOwners, ".gitlab/", "@integrationtest" ); - assertMandatoryApprovers(directoryOwners, ".gitlab/CODEOWNERS", "@nielsbasjes" ); - assertMandatoryApprovers(directoryOwners, "pom.xml", "@integrationtest" ); - assertMandatoryApprovers(directoryOwners, "src/", "@integrationtest" ); - assertMandatoryApprovers(directoryOwners, "src/main/" ); // <-- NO APPROVERS! - assertMandatoryApprovers(directoryOwners, "src/main/README.java" ); // <-- NO APPROVERS! - assertMandatoryApprovers(directoryOwners, "src/main/README.md", "@username" ); - assertMandatoryApprovers(directoryOwners, "src/main/README.txt" ); // <-- NO APPROVERS! - } - - @Test - void testOutSideGitlab() throws CodeOwnersValidationException { - DirectoryOwners directoryOwners = analyze("OutsideGitlab", false); - - assertMandatoryApprovers(directoryOwners, "", "@integrationtest" ); - assertMandatoryApprovers(directoryOwners, ".gitlab/", "@integrationtest" ); - assertMandatoryApprovers(directoryOwners, ".gitlab/CODEOWNERS", "@nielsbasjes" ); - assertMandatoryApprovers(directoryOwners, "pom.xml", "@integrationtest" ); - assertMandatoryApprovers(directoryOwners, "src/", "@integrationtest" ); - assertMandatoryApprovers(directoryOwners, "src/main/" ); // <-- NO APPROVERS! - assertMandatoryApprovers(directoryOwners, "src/main/README.java" ); // <-- NO APPROVERS! - assertMandatoryApprovers(directoryOwners, "src/main/README.md", "@username" ); - assertMandatoryApprovers(directoryOwners, "src/main/README.txt" ); // <-- NO APPROVERS! - -// assert text.contains("[ERROR] Unable to load projectId from Gitlab: GitlabConfiguration: {"); -// assert text.contains("[ERROR] ServerUrl='http://localhost:0' found via gitlab.serverUrl.url is valid."); -// assert text.contains("[ERROR] ProjectId='niels/project' found via gitlab.projectId.id is valid."); -// assert text.contains("[ERROR] AccessToken= 'gltst-*****ue' found via environment variable CHECK_USERS_TOKEN is valid."); - } - - @Test - void testTestTree() throws CodeOwnersValidationException { - DirectoryOwners directoryOwners = analyze("testtree", true); -// assert text.contains("[INFO] Using GitIgnore : \${baseDir}/.gitignore") -// assert text.contains("[INFO] Using GitIgnore : \${baseDir}/dir1/.gitignore") -// assert text.contains("[INFO] Using GitIgnore : \${baseDir}/dir3/.gitignore") -// -//// These exist but are ignored so they may not be read. -// assert !text.contains("[INFO] Using GitIgnore : \${baseDir}/dir4/.gitignore") -// assert !text.contains("[INFO] Using GitIgnore : \${baseDir}/dir4/subdir/.gitignore") -// assert !text.contains("[INFO] Using GitIgnore : \${baseDir}/dir4/subdir/subdir/.gitignore") - -// assert text.contains("[INFO] Using CODEOWNERS: \${baseDir}/docs/CODEOWNERS") - - assertMandatoryApprovers(directoryOwners,"" , "@integrationtest" ); - assertMandatoryApprovers(directoryOwners,".gitignore" , "@integrationtest" ); - assertMandatoryApprovers(directoryOwners,"docs/CODEOWNERS" , "@documentation" ); - assertMandatoryApprovers(directoryOwners,"dir1/" , "@integrationtest" ); - assertMandatoryApprovers(directoryOwners,"dir1/.gitignore" , "@dir1project" ); - assertMandatoryApprovers(directoryOwners,"dir1/dir1.md" , "@dir1project" ); - assertMandatoryApprovers(directoryOwners,"dir2/" , "@integrationtest" ); - assertMandatoryApprovers(directoryOwners,"dir2/dir2.txt" , "@txt" ); - assertMandatoryApprovers(directoryOwners,"dir2/file2.log" , "@log" ); - assertMandatoryApprovers(directoryOwners,"dir3/" , "@integrationtest" ); - assertMandatoryApprovers(directoryOwners,"dir3/.gitignore" , "@dir3project" ); - assertMandatoryApprovers(directoryOwners,"dir3/dir3.txt" , "@dir3project" ); - assertMandatoryApprovers(directoryOwners,"dir3/file3.log" , "@dir3project" ); - assertMandatoryApprovers(directoryOwners,"pom.xml" , "@integrationtest" ); - - // Make sure NONE of the ignored files get an approver - - assertIgnored(directoryOwners, "dir1/dir1.log"); - assertIgnored(directoryOwners, "dir1/dir1.txt"); - assertIgnored(directoryOwners, "dir1/file1.log"); - assertIgnored(directoryOwners, "dir2/dir2.log"); - assertIgnored(directoryOwners, "dir2/dir2.md"); - assertIgnored(directoryOwners, "dir3/dir3.log"); - assertIgnored(directoryOwners, "dir3/dir3.md"); - assertIgnored(directoryOwners, "dir4/"); // Which matches everything under dir4 - assertIgnored(directoryOwners, "README.md"); - assertIgnored(directoryOwners, "root.md"); - } -} diff --git a/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/TestCodeOwnersValidator.kt b/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/TestCodeOwnersValidator.kt new file mode 100644 index 0000000..dfe22d9 --- /dev/null +++ b/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/TestCodeOwnersValidator.kt @@ -0,0 +1,178 @@ +/* + * CodeOwners Tools + * Copyright (C) 2023-2026 Niels Basjes + * + * 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 + * + * https://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 nl.basjes.codeowners.validator + +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.io.File +import java.nio.file.Path +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +class TestCodeOwnersValidator { + @Throws(CodeOwnersValidationException::class) + private fun analyze(directoryName: String?, verbose: Boolean): CodeOwnersValidator.DirectoryOwners { + val validator = CodeOwnersValidator(null, null, verbose) + + val directoryOwners = validator.analyzeDirectory(File("src/test/resources/testdirectories/$directoryName")) + + log.info("\n{}", directoryOwners.toTable()) + log.info("\n{}", directoryOwners.toJson()) + return directoryOwners + } + + @Test + @Throws(CodeOwnersValidationException::class) + fun testCovered() { + val directoryOwners = analyze("covered", false) + + assertMandatoryApprovers(directoryOwners, "", "@integrationtest") + assertMandatoryApprovers(directoryOwners, ".gitlab/", "@integrationtest") + assertMandatoryApprovers(directoryOwners, ".gitlab/CODEOWNERS", "@nielsbasjes") + assertMandatoryApprovers(directoryOwners, "pom.xml", "@integrationtest") + assertMandatoryApprovers(directoryOwners, "src/", "@integrationtest") + assertMandatoryApprovers(directoryOwners, "src/main/", "@projectteam") + assertMandatoryApprovers(directoryOwners, "src/main/README.java", "@projectteam") + assertMandatoryApprovers(directoryOwners, "src/main/README.md", "@username") + assertMandatoryApprovers(directoryOwners, "src/main/README.txt", "@projectteam") + } + + @Test + @Throws(CodeOwnersValidationException::class) + fun testGitignore() { + val directoryOwners = analyze("gitignore", false) + + assertMandatoryApprovers(directoryOwners, "", "@integrationtest") + assertMandatoryApprovers(directoryOwners, ".gitignore", "@integrationtest") + assertMandatoryApprovers(directoryOwners, ".gitlab/", "@integrationtest") + assertMandatoryApprovers(directoryOwners, ".gitlab/CODEOWNERS", "@nielsbasjes") + assertMandatoryApprovers(directoryOwners, "pom.xml", "@integrationtest") + assertMandatoryApprovers(directoryOwners, "src/", "@integrationtest") + assertMandatoryApprovers(directoryOwners, "src/.gitignore", "@nielsbasjes") + assertMandatoryApprovers(directoryOwners, "src/main/", "@projectteam") + assertMandatoryApprovers(directoryOwners, "src/main/README.md", "@username") + + assertIgnored(directoryOwners, "build.log") + assertIgnored(directoryOwners, "src/main/README.java") + assertIgnored(directoryOwners, "src/main/README.txt") + } + + @Test + @Throws(CodeOwnersValidationException::class) + fun testMissing() { + val directoryOwners = analyze("missing", false) + + assertMandatoryApprovers(directoryOwners, "", "@integrationtest") + assertMandatoryApprovers(directoryOwners, ".gitlab/", "@integrationtest") + assertMandatoryApprovers(directoryOwners, ".gitlab/CODEOWNERS", "@nielsbasjes") + assertMandatoryApprovers(directoryOwners, "pom.xml", "@integrationtest") + assertMandatoryApprovers(directoryOwners, "src/", "@integrationtest") + assertMandatoryApprovers(directoryOwners, "src/main/") // <-- NO APPROVERS! + assertMandatoryApprovers(directoryOwners, "src/main/README.java") // <-- NO APPROVERS! + assertMandatoryApprovers(directoryOwners, "src/main/README.md", "@username") + assertMandatoryApprovers(directoryOwners, "src/main/README.txt") // <-- NO APPROVERS! + } + + @Test + @Throws(CodeOwnersValidationException::class) + fun testOutSideGitlab() { + val directoryOwners = analyze("OutsideGitlab", false) + + assertMandatoryApprovers(directoryOwners, "", "@integrationtest") + assertMandatoryApprovers(directoryOwners, ".gitlab/", "@integrationtest") + assertMandatoryApprovers(directoryOwners, ".gitlab/CODEOWNERS", "@nielsbasjes") + assertMandatoryApprovers(directoryOwners, "pom.xml", "@integrationtest") + assertMandatoryApprovers(directoryOwners, "src/", "@integrationtest") + assertMandatoryApprovers(directoryOwners, "src/main/") // <-- NO APPROVERS! + assertMandatoryApprovers(directoryOwners, "src/main/README.java") // <-- NO APPROVERS! + assertMandatoryApprovers(directoryOwners, "src/main/README.md", "@username") + assertMandatoryApprovers(directoryOwners, "src/main/README.txt") // <-- NO APPROVERS! + + // assert text.contains("[ERROR] Unable to load projectId from Gitlab: GitlabConfiguration: {"); +// assert text.contains("[ERROR] ServerUrl='http://localhost:0' found via gitlab.serverUrl.url is valid."); +// assert text.contains("[ERROR] ProjectId='niels/project' found via gitlab.projectId.id is valid."); +// assert text.contains("[ERROR] AccessToken= 'gltst-*****ue' found via environment variable CHECK_USERS_TOKEN is valid."); + } + + @Test + @Throws(CodeOwnersValidationException::class) + fun testTestTree() { + val directoryOwners = analyze("testtree", true) + +// assert text.contains("[INFO] Using GitIgnore : \${baseDir}/.gitignore") +// assert text.contains("[INFO] Using GitIgnore : \${baseDir}/dir1/.gitignore") +// assert text.contains("[INFO] Using GitIgnore : \${baseDir}/dir3/.gitignore") +// +//// These exist but are ignored so they may not be read. +// assert !text.contains("[INFO] Using GitIgnore : \${baseDir}/dir4/.gitignore") +// assert !text.contains("[INFO] Using GitIgnore : \${baseDir}/dir4/subdir/.gitignore") +// assert !text.contains("[INFO] Using GitIgnore : \${baseDir}/dir4/subdir/subdir/.gitignore") + +// assert text.contains("[INFO] Using CODEOWNERS: \${baseDir}/docs/CODEOWNERS") + assertMandatoryApprovers(directoryOwners, "", "@integrationtest") + assertMandatoryApprovers(directoryOwners, ".gitignore", "@integrationtest") + assertMandatoryApprovers(directoryOwners, "docs/CODEOWNERS", "@documentation") + assertMandatoryApprovers(directoryOwners, "dir1/", "@integrationtest") + assertMandatoryApprovers(directoryOwners, "dir1/.gitignore", "@dir1project") + assertMandatoryApprovers(directoryOwners, "dir1/dir1.md", "@dir1project") + assertMandatoryApprovers(directoryOwners, "dir2/", "@integrationtest") + assertMandatoryApprovers(directoryOwners, "dir2/dir2.txt", "@txt") + assertMandatoryApprovers(directoryOwners, "dir2/file2.log", "@log") + assertMandatoryApprovers(directoryOwners, "dir3/", "@integrationtest") + assertMandatoryApprovers(directoryOwners, "dir3/.gitignore", "@dir3project") + assertMandatoryApprovers(directoryOwners, "dir3/dir3.txt", "@dir3project") + assertMandatoryApprovers(directoryOwners, "dir3/file3.log", "@dir3project") + assertMandatoryApprovers(directoryOwners, "pom.xml", "@integrationtest") + + // Make sure NONE of the ignored files get an approver + assertIgnored(directoryOwners, "dir1/dir1.log") + assertIgnored(directoryOwners, "dir1/dir1.txt") + assertIgnored(directoryOwners, "dir1/file1.log") + assertIgnored(directoryOwners, "dir2/dir2.log") + assertIgnored(directoryOwners, "dir2/dir2.md") + assertIgnored(directoryOwners, "dir3/dir3.log") + assertIgnored(directoryOwners, "dir3/dir3.md") + assertIgnored(directoryOwners, "dir4/") // Which matches everything under dir4 + assertIgnored(directoryOwners, "README.md") + assertIgnored(directoryOwners, "root.md") + } + + companion object { + private val log: Logger = LoggerFactory.getLogger(TestCodeOwnersValidator::class.java) + + private fun assertMandatoryApprovers( + directoryOwners: CodeOwnersValidator.DirectoryOwners, + path: String, + vararg expected: String? + ) { + val asPath = Path.of(path) + assertNotNull(asPath, "Unable to convert $path into a Path.") + val fileOwners = directoryOwners[asPath] + assertNotNull(fileOwners, "No FileOwners found for $asPath") + assertEquals(listOf(*expected), fileOwners.mandatoryApprovers) + } + + private fun assertIgnored(directoryOwners: CodeOwnersValidator.DirectoryOwners, path: String) { + val asPath = Path.of(path) + assertNotNull(asPath, "Unable to convert $path into a Path.") + val fileOwners = directoryOwners[asPath] + assertNull(fileOwners, "No FileOwners should have been found for $asPath") + } + } +} diff --git a/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/gitlab/TestGitlabConfiguration.java b/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/gitlab/TestGitlabConfiguration.java deleted file mode 100644 index 98bfe7c..0000000 --- a/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/gitlab/TestGitlabConfiguration.java +++ /dev/null @@ -1,248 +0,0 @@ -/* - * CodeOwners Tools - * Copyright (C) 2023-2026 Niels Basjes - * - * 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 - * - * https://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 nl.basjes.codeowners.validator.gitlab; - -import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; -import com.github.tomakehurst.wiremock.junit5.WireMockTest; -import nl.basjes.codeowners.validator.CodeOwnersValidationException; -import org.junit.jupiter.api.Test; -import org.junitpioneer.jupiter.SetEnvironmentVariable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import static nl.basjes.codeowners.validator.gitlab.TestGitlabUsers.makeConfig; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; -import static wiremock.org.hamcrest.MatcherAssert.assertThat; -import static wiremock.org.hamcrest.core.StringContains.containsString; - -@WireMockTest -public class TestGitlabConfiguration { - - private static final Logger log = LoggerFactory.getLogger(TestGitlabConfiguration.class); - - @Test -// @SetEnvironmentVariable(key = "CI_SERVER_URL", value = "foobar") // <<-- Is bad - @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels/project") - @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "gltst-validtoken") - public void testServerUrlInvalid() { - GitlabConfiguration configuration = makeConfig(null, null, "FETCH_USER_ACCESS_TOKEN"); - - IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { - try (GitlabProjectMembers ignored = new GitlabProjectMembers(configuration)) { - fail("Should never get here:" + ignored); - } - } - ); - - assertThat(exception.getMessage(), containsString("the environment variable \"CI_SERVER_URL\" does not exist")); - assertThat(exception.getMessage(), containsString("(via environment variable \"CI_PROJECT_ID\")")); - assertThat(exception.getMessage(), containsString("(via environment variable \"FETCH_USER_ACCESS_TOKEN\")")); - assertFalse(configuration.isDefaultCIConfigRunningOutsideCI()); - } - - @Test - @SetEnvironmentVariable(key = "CI_SERVER_URL", value = "http://localhost:0") // <<-- Is bad - @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels/project") - @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "gltst-validtoken") - public void testServerUrlUnreachable() { - GitlabConfiguration configuration = makeConfig(null, null, "FETCH_USER_ACCESS_TOKEN"); - - CodeOwnersValidationException exception = assertThrows(CodeOwnersValidationException.class, () -> { - try (GitlabProjectMembers ignored = new GitlabProjectMembers(configuration)) { - fail("Should never get here:" + ignored); - } - } - ); - - assertThat(exception.getMessage(), containsString("(via environment variable \"CI_SERVER_URL\")")); - assertThat(exception.getMessage(), containsString("(via environment variable \"CI_PROJECT_ID\")")); - assertThat(exception.getMessage(), containsString("(via environment variable \"FETCH_USER_ACCESS_TOKEN\")")); - assertFalse(configuration.isDefaultCIConfigRunningOutsideCI()); - } - - - @Test - @SetEnvironmentVariable(key = "CI_SERVER_URL", value = "https://git.example.nl") - @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels project") // <<-- Is bad - @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "gltst-validtoken") - public void testProjectIdInvalid() { - GitlabConfiguration configuration = makeConfig(null, null, "FETCH_USER_ACCESS_TOKEN"); - - IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { - try (GitlabProjectMembers ignored = new GitlabProjectMembers(configuration)) { - fail("Should never get here:" + ignored); - } - } - ); - - assertThat(exception.getMessage(), containsString("(via environment variable \"CI_SERVER_URL\")")); - assertThat(exception.getMessage(), containsString("the value from environment variable \"CI_PROJECT_ID\" is NOT valid")); - assertThat(exception.getMessage(), containsString("(via environment variable \"FETCH_USER_ACCESS_TOKEN\")")); - assertFalse(configuration.isDefaultCIConfigRunningOutsideCI()); - } - - - @Test - @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "doesnotexist") - @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "gltst-validtoken") - public void testProjectIdDoesNotExist(WireMockRuntimeInfo wmRuntimeInfo) { - GitlabConfiguration configuration = makeConfig(wmRuntimeInfo, null, "FETCH_USER_ACCESS_TOKEN"); - log.info("Test with config:\n{}", configuration); - CodeOwnersValidationException exception = assertThrows(CodeOwnersValidationException.class, () -> { - try (GitlabProjectMembers ignored = new GitlabProjectMembers(configuration)) { - fail("Should never get here:" + ignored); - } - } - ); - - assertThat(exception.getMessage(), containsString("Unable to load projectId from Gitlab")); - assertFalse(configuration.isDefaultCIConfigRunningOutsideCI()); - } - - - @Test - @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels/project") - @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "") - public void testAccessTokenInvalid(WireMockRuntimeInfo wmRuntimeInfo) { - GitlabConfiguration configuration = makeConfig(wmRuntimeInfo, null, "FETCH_USER_ACCESS_TOKEN"); - - IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { - try (GitlabProjectMembers ignored = new GitlabProjectMembers(configuration)) { - fail("Should never get here:" + ignored); - } - } - ); - - assertThat(exception.getMessage(), containsString("(via property \"gitlab.serverUrl.url\")")); - assertThat(exception.getMessage(), containsString("(via environment variable \"CI_PROJECT_ID\")")); - assertThat(exception.getMessage(), containsString("the value from environment variable \"FETCH_USER_ACCESS_TOKEN\" is NOT valid")); - assertFalse(configuration.isDefaultCIConfigRunningOutsideCI()); - } - - @Test - @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels/project") - @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "gltst-badtoken") - public void testAccessTokenBad(WireMockRuntimeInfo wmRuntimeInfo) { - GitlabConfiguration configuration = makeConfig(wmRuntimeInfo, null, "FETCH_USER_ACCESS_TOKEN"); - - CodeOwnersValidationException exception = assertThrows(CodeOwnersValidationException.class, () -> { - try (GitlabProjectMembers ignored = new GitlabProjectMembers(configuration)) { - fail("Should never get here:" + ignored); - } - } - ); - - assertThat(exception.getMessage(), containsString("Unable to load projectId from Gitlab")); - assertFalse(configuration.isDefaultCIConfigRunningOutsideCI()); - } - - // Check if the config IN CI is picked up - @Test - @SetEnvironmentVariable(key = "CI_SERVER_URL", value = "https://gitlab.example.nl") - @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels/project") - @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "gltst-validtoken") - public void testCIConfig() { - GitlabConfiguration configuration = new GitlabConfiguration( - new GitlabConfiguration.ServerUrl(null, null), - new GitlabConfiguration.ProjectId(null, null), - new GitlabConfiguration.AccessToken("FETCH_USER_ACCESS_TOKEN") - ); - assertFalse(configuration.isDefaultCIConfigRunningOutsideCI(), "This is the default config INSIDE the pipeline"); - assertTrue(configuration.isValid(), "Config should be valid"); - } - - private GitlabConfiguration getBaseCIConfig() { - return new GitlabConfiguration( - new GitlabConfiguration.ServerUrl(null, null), - new GitlabConfiguration.ProjectId(null, null), - new GitlabConfiguration.AccessToken("FETCH_USER_ACCESS_TOKEN") - ); - } - - @Test -// @SetEnvironmentVariable(key = "CI_SERVER_URL", value = "https://gitlab.example.nl") -// @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels/project") -// @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "gltst-validtoken") - public void testCIConfigOutsideOfCI1() { - GitlabConfiguration configuration = getBaseCIConfig(); - assertTrue(configuration.isDefaultCIConfigRunningOutsideCI(), "Config should be seen as the default CI config"); - assertFalse(configuration.isValid(), "Config should be invalid"); - } - - @Test -// @SetEnvironmentVariable(key = "CI_SERVER_URL", value = "https://gitlab.example.nl") -// @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels/project") -// @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "gltst-validtoken") - public void testCIConfigOutsideOfCI2() { - GitlabConfiguration configuration = getBaseCIConfig(); - assertTrue(configuration.isDefaultCIConfigRunningOutsideCI(), "Config should be seen as the default CI config"); - assertFalse(configuration.isValid(), "Config should be invalid"); - } - - @Test - @SetEnvironmentVariable(key = "CI_SERVER_URL", value = "https://gitlab.example.nl") -// @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels/project") -// @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "gltst-validtoken") - public void testCIConfig1() { - GitlabConfiguration configuration = getBaseCIConfig(); - assertFalse(configuration.isDefaultCIConfigRunningOutsideCI(), "Partial CI parameters should be invalid"); - assertFalse(configuration.isValid(), "Config should be invalid"); - } - - @Test -// @SetEnvironmentVariable(key = "CI_SERVER_URL", value = "https://gitlab.example.nl") - @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels/project") -// @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "gltst-validtoken") - public void testCIConfig2() { - GitlabConfiguration configuration = getBaseCIConfig(); - assertFalse(configuration.isDefaultCIConfigRunningOutsideCI(), "Partial CI parameters should be invalid"); - assertFalse(configuration.isValid(), "Config should be invalid"); - } - - @Test - @SetEnvironmentVariable(key = "CI_SERVER_URL", value = "https://gitlab.example.nl") - @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels/project") -// @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "gltst-validtoken") - public void testCIConfig3() { - GitlabConfiguration configuration = getBaseCIConfig(); - assertFalse(configuration.isDefaultCIConfigRunningOutsideCI(), "Partial CI parameters should be invalid"); - assertFalse(configuration.isValid(), "Config should be invalid"); - } - @Test -// @SetEnvironmentVariable(key = "CI_SERVER_URL", value = "https://gitlab.example.nl") - @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels/project") - @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "gltst-validtoken") - public void testCIConfig4() { - GitlabConfiguration configuration = getBaseCIConfig(); - assertFalse(configuration.isDefaultCIConfigRunningOutsideCI(), "Partial CI parameters should be invalid"); - assertFalse(configuration.isValid(), "Config should be invalid"); - } - - @Test - @SetEnvironmentVariable(key = "CI_SERVER_URL", value = "https://gitlab.example.nl") -// @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels/project") - @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "gltst-validtoken") - public void testCIConfig5() { - GitlabConfiguration configuration = getBaseCIConfig(); - assertFalse(configuration.isDefaultCIConfigRunningOutsideCI(), "Partial CI parameters should be invalid"); - assertFalse(configuration.isValid(), "Config should be invalid"); - } - -} diff --git a/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/gitlab/TestGitlabConfiguration.kt b/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/gitlab/TestGitlabConfiguration.kt new file mode 100644 index 0000000..63dfcfc --- /dev/null +++ b/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/gitlab/TestGitlabConfiguration.kt @@ -0,0 +1,287 @@ +/* + * CodeOwners Tools + * Copyright (C) 2023-2026 Niels Basjes + * + * 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 + * + * https://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 nl.basjes.codeowners.validator.gitlab + +import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo +import com.github.tomakehurst.wiremock.junit5.WireMockTest +import nl.basjes.codeowners.validator.CodeOwnersValidationException +import org.junitpioneer.jupiter.SetEnvironmentVariable +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import wiremock.org.hamcrest.MatcherAssert.assertThat +import wiremock.org.hamcrest.core.StringContains.containsString +import kotlin.test.Test +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +@WireMockTest +class TestGitlabConfiguration { + @Test // @SetEnvironmentVariable(key = "CI_SERVER_URL", value = "foobar") // <<-- Is bad + @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels/project") + @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "gltst-validtoken") + fun testServerUrlInvalid() { + val configuration = TestGitlabUsers.makeConfig(null, null, "FETCH_USER_ACCESS_TOKEN") + + val exception = assertFailsWith { GitlabProjectMembers(configuration) } + + assertThat( + exception.message, + containsString("the environment variable \"CI_SERVER_URL\" does not exist") + ) + assertThat( + exception.message, + containsString("(via environment variable \"CI_PROJECT_ID\")") + ) + assertThat( + exception.message, + containsString("(via environment variable \"FETCH_USER_ACCESS_TOKEN\")") + ) + assertFalse(configuration.isDefaultCIConfigRunningOutsideCI) + } + + @Test + @SetEnvironmentVariable(key = "CI_SERVER_URL", value = "http://localhost:0") // <<-- Is bad + @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels/project") + @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "gltst-validtoken") + fun testServerUrlUnreachable() { + val configuration = TestGitlabUsers.makeConfig(null, null, "FETCH_USER_ACCESS_TOKEN") + + val exception = assertFailsWith { GitlabProjectMembers(configuration) } + + assertThat( + exception.message, + containsString("(via environment variable \"CI_SERVER_URL\")") + ) + assertThat( + exception.message, + containsString("(via environment variable \"CI_PROJECT_ID\")") + ) + assertThat( + exception.message, + containsString("(via environment variable \"FETCH_USER_ACCESS_TOKEN\")") + ) + assertFalse(configuration.isDefaultCIConfigRunningOutsideCI) + } + + + @Test + @SetEnvironmentVariable(key = "CI_SERVER_URL", value = "https://git.example.nl") + @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels project") // <<-- Is bad + @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "gltst-validtoken") + fun testProjectIdInvalid() { + val configuration = TestGitlabUsers.makeConfig(null, null, "FETCH_USER_ACCESS_TOKEN") + + val exception = assertFailsWith { GitlabProjectMembers(configuration) } + + assertThat( + exception.message, + containsString("(via environment variable \"CI_SERVER_URL\")") + ) + assertThat( + exception.message, + containsString("the value from environment variable \"CI_PROJECT_ID\" is NOT valid") + ) + assertThat( + exception.message, + containsString("(via environment variable \"FETCH_USER_ACCESS_TOKEN\")") + ) + assertFalse(configuration.isDefaultCIConfigRunningOutsideCI) + } + + + @Test + @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "doesnotexist") + @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "gltst-validtoken") + fun testProjectIdDoesNotExist(wmRuntimeInfo: WireMockRuntimeInfo?) { + val configuration = TestGitlabUsers.makeConfig(wmRuntimeInfo, null, "FETCH_USER_ACCESS_TOKEN") + log.info("Test with config:\n{}", configuration) + val exception = assertFailsWith { GitlabProjectMembers(configuration) } + + assertThat( + exception.message, + containsString("Unable to load projectId from Gitlab") + ) + assertFalse(configuration.isDefaultCIConfigRunningOutsideCI) + } + + + @Test + @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels/project") + @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "") + fun testAccessTokenInvalid(wmRuntimeInfo: WireMockRuntimeInfo?) { + val configuration = TestGitlabUsers.makeConfig(wmRuntimeInfo, null, "FETCH_USER_ACCESS_TOKEN") + + val exception = + assertFailsWith { + GitlabProjectMembers(configuration) + } + + assertThat( + exception.message, + containsString("(via property \"gitlab.serverUrl.url\")") + ) + assertThat( + exception.message, + containsString("(via environment variable \"CI_PROJECT_ID\")") + ) + assertThat( + exception.message, + containsString("the value from environment variable \"FETCH_USER_ACCESS_TOKEN\" is NOT valid") + ) + assertFalse(configuration.isDefaultCIConfigRunningOutsideCI) + } + + @Test + @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels/project") + @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "gltst-badtoken") + fun testAccessTokenBad(wmRuntimeInfo: WireMockRuntimeInfo?) { + val configuration = TestGitlabUsers.makeConfig(wmRuntimeInfo, null, "FETCH_USER_ACCESS_TOKEN") + + val exception = assertFailsWith { GitlabProjectMembers(configuration) } + + assertThat( + exception.message, + containsString("Unable to load projectId from Gitlab") + ) + assertFalse(configuration.isDefaultCIConfigRunningOutsideCI) + } + + // Check if the config IN CI is picked up + @Test + @SetEnvironmentVariable(key = "CI_SERVER_URL", value = "https://gitlab.example.nl") + @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels/project") + @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "gltst-validtoken") + fun testCIConfig() { + val configuration = GitlabConfiguration( + GitlabConfiguration.ServerUrl(null, null), + GitlabConfiguration.ProjectId(null, null), + GitlabConfiguration.AccessToken("FETCH_USER_ACCESS_TOKEN") + ) + assertFalse( + configuration.isDefaultCIConfigRunningOutsideCI, + "This is the default config INSIDE the pipeline" + ) + assertTrue(configuration.isValid(), "Config should be valid") + } + + private val baseCIConfig: GitlabConfiguration + get() = GitlabConfiguration( + GitlabConfiguration.ServerUrl(null, null), + GitlabConfiguration.ProjectId(null, null), + GitlabConfiguration.AccessToken("FETCH_USER_ACCESS_TOKEN") + ) + + @Test // @SetEnvironmentVariable(key = "CI_SERVER_URL", value = "https://gitlab.example.nl") + // @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels/project") + // @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "gltst-validtoken") + fun testCIConfigOutsideOfCI1() { + val configuration = this.baseCIConfig + assertTrue( + configuration.isDefaultCIConfigRunningOutsideCI, + "Config should be seen as the default CI config" + ) + assertFalse(configuration.isValid(), "Config should be invalid") + } + + @Test // @SetEnvironmentVariable(key = "CI_SERVER_URL", value = "https://gitlab.example.nl") + // @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels/project") + // @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "gltst-validtoken") + fun testCIConfigOutsideOfCI2() { + val configuration = this.baseCIConfig + assertTrue( + configuration.isDefaultCIConfigRunningOutsideCI, + "Config should be seen as the default CI config" + ) + assertFalse(configuration.isValid(), "Config should be invalid") + } + + @Test + @SetEnvironmentVariable( + key = "CI_SERVER_URL", + value = "https://gitlab.example.nl" + ) // @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels/project") + // @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "gltst-validtoken") + fun testCIConfig1() { + val configuration = this.baseCIConfig + assertFalse( + configuration.isDefaultCIConfigRunningOutsideCI, + "Partial CI parameters should be invalid" + ) + assertFalse(configuration.isValid(), "Config should be invalid") + } + + @Test // @SetEnvironmentVariable(key = "CI_SERVER_URL", value = "https://gitlab.example.nl") + @SetEnvironmentVariable( + key = "CI_PROJECT_ID", + value = "niels/project" + ) // @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "gltst-validtoken") + fun testCIConfig2() { + val configuration = this.baseCIConfig + assertFalse( + configuration.isDefaultCIConfigRunningOutsideCI, + "Partial CI parameters should be invalid" + ) + assertFalse(configuration.isValid(), "Config should be invalid") + } + + @Test + @SetEnvironmentVariable(key = "CI_SERVER_URL", value = "https://gitlab.example.nl") + @SetEnvironmentVariable( + key = "CI_PROJECT_ID", + value = "niels/project" + ) // @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "gltst-validtoken") + fun testCIConfig3() { + val configuration = this.baseCIConfig + assertFalse( + configuration.isDefaultCIConfigRunningOutsideCI, + "Partial CI parameters should be invalid" + ) + assertFalse(configuration.isValid(), "Config should be invalid") + } + + @Test // @SetEnvironmentVariable(key = "CI_SERVER_URL", value = "https://gitlab.example.nl") + @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels/project") + @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "gltst-validtoken") + fun testCIConfig4() { + val configuration = this.baseCIConfig + assertFalse( + configuration.isDefaultCIConfigRunningOutsideCI, + "Partial CI parameters should be invalid" + ) + assertFalse(configuration.isValid(), "Config should be invalid") + } + + @Test + @SetEnvironmentVariable( + key = "CI_SERVER_URL", + value = "https://gitlab.example.nl" + ) // @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels/project") + @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "gltst-validtoken") + fun testCIConfig5() { + val configuration = this.baseCIConfig + assertFalse( + configuration.isDefaultCIConfigRunningOutsideCI, + "Partial CI parameters should be invalid" + ) + assertFalse(configuration.isValid(), "Config should be invalid") + } + + companion object { + private val log: Logger = LoggerFactory.getLogger(TestGitlabConfiguration::class.java) + } +} diff --git a/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/gitlab/TestGitlabConfigurationAccessToken.java b/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/gitlab/TestGitlabConfigurationAccessToken.java deleted file mode 100644 index 23d7353..0000000 --- a/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/gitlab/TestGitlabConfigurationAccessToken.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * CodeOwners Tools - * Copyright (C) 2023-2026 Niels Basjes - * - * 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 - * - * https://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 nl.basjes.codeowners.validator.gitlab; - -import nl.basjes.codeowners.validator.gitlab.GitlabConfiguration.AccessToken; -import org.junit.jupiter.api.Test; -import org.junitpioneer.jupiter.SetEnvironmentVariable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class TestGitlabConfigurationAccessToken { - - private static final Logger log = LoggerFactory.getLogger(TestGitlabConfigurationAccessToken.class); - - @Test - @SetEnvironmentVariable(key = "MY_SECRET_TOKEN", value = "gltst-mySecretToken") // <<-- Is good - public void testAccessTokenEnvValidValue() { - AccessToken accessToken = new AccessToken("MY_SECRET_TOKEN"); - assertTrue(accessToken.isValid()); - assertEquals("gltst-mySecretToken", accessToken.getValue()); - assertEquals("gltst-*****en", accessToken.getSanitizedValue()); - - // Ensure the actual token value is hidden in a toString() - log.info("{}", accessToken); - assertFalse(accessToken.toString().contains("gltst-mySecretToken")); - assertTrue(accessToken.toString().contains("gltst-*****en")); - } - - @Test - @SetEnvironmentVariable(key = "MY_SECRET_TOKEN", value = "short") // <<-- Is good - public void testAccessTokenEnvValidValueShortToken() { - AccessToken accessToken = new AccessToken("MY_SECRET_TOKEN"); - assertTrue(accessToken.isValid()); - assertEquals("short", accessToken.getValue()); - assertEquals("***", accessToken.getSanitizedValue()); - - // Ensure the actual token value is hidden in a toString() - log.info("{}", accessToken); - assertFalse(accessToken.toString().contains("short")); - assertTrue(accessToken.toString().contains("***")); - } - - @Test - @SetEnvironmentVariable(key = "MY_SECRET_TOKEN", value = "foo bar") // <<-- Is bad - public void testAccessTokenEnvBadValue() { - AccessToken accessToken = new AccessToken("MY_SECRET_TOKEN"); - assertFalse(accessToken.isValid()); - assertNull(accessToken.getValue()); - log.info("{}", accessToken); - assertEquals("<<>>", accessToken.getSanitizedValue()); - assertTrue(accessToken.toString().contains("AccessToken could not be loaded: the value from environment variable \"MY_SECRET_TOKEN\" is NOT valid.")); - } - - @Test - @SetEnvironmentVariable(key = "MY_SECRET_TOKEN", value = "") // <<-- Is bad - public void testAccessTokenEnvEmptyValue() { - AccessToken accessToken = new AccessToken("MY_SECRET_TOKEN"); - assertFalse(accessToken.isValid()); - assertNull(accessToken.getValue()); - log.info("{}", accessToken); - assertEquals("<<>>", accessToken.getSanitizedValue()); - assertTrue(accessToken.toString().contains("AccessToken could not be loaded: the value from environment variable \"MY_SECRET_TOKEN\" is NOT valid.")); - } - - @Test -// @SetEnvironmentVariable(key = "MY_SECRET_TOKEN", value = "") // <<-- Is bad - public void testAccessTokenEnvMissing() { - AccessToken accessToken = new AccessToken("MY_SECRET_TOKEN"); - assertFalse(accessToken.isValid()); - assertNull(accessToken.getValue()); - log.info("{}", accessToken); - assertEquals("<<>>", accessToken.getSanitizedValue()); - assertTrue(accessToken.toString().contains("AccessToken could not be loaded: the environment variable \"MY_SECRET_TOKEN\" does not exist.")); - } - - @Test - public void testAccessTokenEnvNameEmpty() { - AccessToken accessToken = new AccessToken(""); - assertFalse(accessToken.isValid()); - assertNull(accessToken.getValue()); - log.info("{}", accessToken); - assertEquals("<<>>", accessToken.getSanitizedValue()); - assertTrue(accessToken.toString().contains("AccessToken could not be loaded: no environment variable was specified.")); - } - - @Test - public void testAccessTokenEnvNameBlank() { - AccessToken accessToken = new AccessToken(" "); - assertFalse(accessToken.isValid()); - assertNull(accessToken.getValue()); - log.info("{}", accessToken); - assertEquals("<<>>", accessToken.getSanitizedValue()); - assertTrue(accessToken.toString().contains("AccessToken could not be loaded: the environment variable name \" \" is blank.")); - } - - @Test - public void testAccessTokenEnvNameNull() { - AccessToken accessToken = new AccessToken(null); - assertFalse(accessToken.isValid()); - assertNull(accessToken.getValue()); - log.info("{}", accessToken); - assertEquals("<<>>", accessToken.getSanitizedValue()); - assertTrue(accessToken.toString().contains("AccessToken could not be loaded: no environment variable was specified.")); - } - -} diff --git a/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/gitlab/TestGitlabConfigurationAccessToken.kt b/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/gitlab/TestGitlabConfigurationAccessToken.kt new file mode 100644 index 0000000..1142718 --- /dev/null +++ b/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/gitlab/TestGitlabConfigurationAccessToken.kt @@ -0,0 +1,138 @@ +/* + * CodeOwners Tools + * Copyright (C) 2023-2026 Niels Basjes + * + * 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 + * + * https://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 nl.basjes.codeowners.validator.gitlab + +import org.junitpioneer.jupiter.SetEnvironmentVariable +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class TestGitlabConfigurationAccessToken { + @Test + @SetEnvironmentVariable(key = "MY_SECRET_TOKEN", value = "gltst-mySecretToken") // <<-- Is good + fun testAccessTokenEnvValidValue() { + val accessToken = GitlabConfiguration.AccessToken("MY_SECRET_TOKEN") + assertTrue(accessToken.isValid()) + assertEquals("gltst-mySecretToken", accessToken.value) + assertEquals("gltst-*****en", accessToken.sanitizedValue) + + // Ensure the actual token value is hidden in a toString() + log.info("{}", accessToken) + assertFalse(accessToken.toString().contains("gltst-mySecretToken")) + assertTrue(accessToken.toString().contains("gltst-*****en")) + } + + @Test + @SetEnvironmentVariable(key = "MY_SECRET_TOKEN", value = "short") // <<-- Is good + fun testAccessTokenEnvValidValueShortToken() { + val accessToken = GitlabConfiguration.AccessToken("MY_SECRET_TOKEN") + assertTrue(accessToken.isValid()) + assertEquals("short", accessToken.value) + assertEquals("***", accessToken.sanitizedValue) + + // Ensure the actual token value is hidden in a toString() + log.info("{}", accessToken) + assertFalse(accessToken.toString().contains("short")) + assertTrue(accessToken.toString().contains("***")) + } + + @Test + @SetEnvironmentVariable(key = "MY_SECRET_TOKEN", value = "foo bar") // <<-- Is bad + fun testAccessTokenEnvBadValue() { + val accessToken = GitlabConfiguration.AccessToken("MY_SECRET_TOKEN") + assertFalse(accessToken.isValid()) + assertNull(accessToken.value) + log.info("{}", accessToken) + assertEquals("<<>>", accessToken.sanitizedValue) + assertTrue( + accessToken.toString() + .contains("AccessToken could not be loaded: the value from environment variable \"MY_SECRET_TOKEN\" is NOT valid.") + ) + } + + @Test + @SetEnvironmentVariable(key = "MY_SECRET_TOKEN", value = "") // <<-- Is bad + fun testAccessTokenEnvEmptyValue() { + val accessToken = GitlabConfiguration.AccessToken("MY_SECRET_TOKEN") + assertFalse(accessToken.isValid()) + assertNull(accessToken.value) + log.info("{}", accessToken) + assertEquals("<<>>", accessToken.sanitizedValue) + assertTrue( + accessToken.toString() + .contains("AccessToken could not be loaded: the value from environment variable \"MY_SECRET_TOKEN\" is NOT valid.") + ) + } + + @Test // @SetEnvironmentVariable(key = "MY_SECRET_TOKEN", value = "") // <<-- Is bad + fun testAccessTokenEnvMissing() { + val accessToken = GitlabConfiguration.AccessToken("MY_SECRET_TOKEN") + assertFalse(accessToken.isValid()) + assertNull(accessToken.value) + log.info("{}", accessToken) + assertEquals("<<>>", accessToken.sanitizedValue) + assertTrue( + accessToken.toString() + .contains("AccessToken could not be loaded: the environment variable \"MY_SECRET_TOKEN\" does not exist.") + ) + } + + @Test + fun testAccessTokenEnvNameEmpty() { + val accessToken = GitlabConfiguration.AccessToken("") + assertFalse(accessToken.isValid()) + assertNull(accessToken.value) + log.info("{}", accessToken) + assertEquals("<<>>", accessToken.sanitizedValue) + assertTrue( + accessToken.toString().contains("AccessToken could not be loaded: no environment variable was specified.") + ) + } + + @Test + fun testAccessTokenEnvNameBlank() { + val accessToken = GitlabConfiguration.AccessToken(" ") + assertFalse(accessToken.isValid()) + assertNull(accessToken.value) + log.info("{}", accessToken) + assertEquals("<<>>", accessToken.sanitizedValue) + assertTrue( + accessToken.toString() + .contains("AccessToken could not be loaded: the environment variable name \" \" is blank.") + ) + } + + @Test + fun testAccessTokenEnvNameNull() { + val accessToken = GitlabConfiguration.AccessToken(null) + assertFalse(accessToken.isValid()) + assertNull(accessToken.value) + log.info("{}", accessToken) + assertEquals("<<>>", accessToken.sanitizedValue) + assertTrue( + accessToken.toString().contains("AccessToken could not be loaded: no environment variable was specified.") + ) + } + + companion object { + private val log: Logger = LoggerFactory.getLogger(TestGitlabConfigurationAccessToken::class.java) + } +} diff --git a/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/gitlab/TestGitlabConfigurationProjectId.java b/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/gitlab/TestGitlabConfigurationProjectId.java deleted file mode 100644 index 8ce458a..0000000 --- a/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/gitlab/TestGitlabConfigurationProjectId.java +++ /dev/null @@ -1,208 +0,0 @@ -/* - * CodeOwners Tools - * Copyright (C) 2023-2026 Niels Basjes - * - * 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 - * - * https://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 nl.basjes.codeowners.validator.gitlab; - -import nl.basjes.codeowners.validator.gitlab.GitlabConfiguration.ProjectId; -import org.junit.jupiter.api.Test; -import org.junitpioneer.jupiter.SetEnvironmentVariable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class TestGitlabConfigurationProjectId { - - private static final Logger log = LoggerFactory.getLogger(TestGitlabConfigurationProjectId.class); - - @Test - @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels/project") // <<-- Is good - public void testProjectIdDefaultEnvValidValue() { - ProjectId projectId = new ProjectId(null, null); - assertTrue(projectId.isValid()); - assertEquals("niels/project", projectId.getValue()); - log.info("{}", projectId); - assertEquals("niels/project", projectId.getSanitizedValue()); - assertTrue(projectId.toString().contains("(via environment variable \"CI_PROJECT_ID\")")); - } - - @Test - @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "foo bar") // <<-- Is bad - public void testProjectIdDefaultEnvBadValue() { - ProjectId projectId = new ProjectId(null, null); - assertFalse(projectId.isValid()); - assertNull(projectId.getValue()); - log.info("{}", projectId); - assertEquals("<<>>", projectId.getSanitizedValue()); - assertTrue(projectId.toString().contains("the value from environment variable \"CI_PROJECT_ID\" is NOT valid")); - } - - @Test - @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "") // <<-- Is bad - public void testProjectIdDefaultEnvEmptyValue() { - ProjectId projectId = new ProjectId(null, null); - assertFalse(projectId.isValid()); - assertNull(projectId.getValue()); - log.info("{}", projectId); - assertEquals("<<>>", projectId.getSanitizedValue()); - assertTrue(projectId.toString().contains("the value from environment variable \"CI_PROJECT_ID\" is NOT valid")); - } - - @Test -// @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "") // <<-- Is bad - public void testProjectIdDefaultEnvMissing() { - ProjectId projectId = new ProjectId(null, null); - assertFalse(projectId.isValid()); - assertNull(projectId.getValue()); - log.info("{}", projectId); - assertEquals("<<>>", projectId.getSanitizedValue()); - assertTrue(projectId.toString().contains("the environment variable \"CI_PROJECT_ID\" does not exist")); - } - - @Test - @SetEnvironmentVariable(key = "MY_SPECIAL_PROJECT_ID", value = "niels/project") // <<-- Is good - public void testProjectIdCustomEnvValidValue() { - ProjectId projectId = new ProjectId(null, "MY_SPECIAL_PROJECT_ID"); - assertTrue(projectId.isValid()); - assertEquals("niels/project", projectId.getValue()); - log.info("{}", projectId); - assertEquals("niels/project", projectId.getSanitizedValue()); - assertTrue(projectId.toString().contains("(via environment variable \"MY_SPECIAL_PROJECT_ID\")")); - } - - @Test - @SetEnvironmentVariable(key = "MY_SPECIAL_PROJECT_ID", value = "foo bar") // <<-- Is bad - public void testProjectIdCustomEnvBadValue() { - ProjectId projectId = new ProjectId(null, "MY_SPECIAL_PROJECT_ID"); - assertFalse(projectId.isValid()); - assertNull(projectId.getValue()); - log.info("{}", projectId); - assertEquals("<<>>", projectId.getSanitizedValue()); - assertTrue(projectId.toString().contains("the value from environment variable \"MY_SPECIAL_PROJECT_ID\" is NOT valid")); - } - - @Test - @SetEnvironmentVariable(key = "MY_SPECIAL_PROJECT_ID", value = "") // <<-- Is bad - public void testProjectIdCustomEnvEmptyValue() { - ProjectId projectId = new ProjectId(null, "MY_SPECIAL_PROJECT_ID"); - assertFalse(projectId.isValid()); - assertNull(projectId.getValue()); - log.info("{}", projectId); - assertEquals("<<>>", projectId.getSanitizedValue()); - assertTrue(projectId.toString().contains("the value from environment variable \"MY_SPECIAL_PROJECT_ID\" is NOT valid")); - } - - @Test -// @SetEnvironmentVariable(key = "MY_SPECIAL_PROJECT_ID", value = "") // <<-- Is bad - public void testProjectIdCustomEnvMissing() { - ProjectId projectId = new ProjectId(null, "MY_SPECIAL_PROJECT_ID"); - assertFalse(projectId.isValid()); - assertNull(projectId.getValue()); - log.info("{}", projectId); - assertEquals("<<>>", projectId.getSanitizedValue()); - assertTrue(projectId.toString().contains("the environment variable \"MY_SPECIAL_PROJECT_ID\" does not exist")); - } - - @Test - public void testProjectIdCustomEnvNameEmpty() { - ProjectId projectId = new ProjectId(null, ""); - assertFalse(projectId.isValid()); - assertNull(projectId.getValue()); - log.info("{}", projectId); - assertTrue(projectId.toString().contains("the environment variable \"CI_PROJECT_ID\" does not exist")); - } - - @Test - public void testProjectIdCustomEnvNameBlank() { - ProjectId projectId = new ProjectId(null, " "); - assertFalse(projectId.isValid()); - assertNull(projectId.getValue()); - log.info("{}", projectId); - assertEquals("<<>>", projectId.getSanitizedValue()); - assertTrue(projectId.toString().contains("the environment variable name \" \" is blank.")); - } - - @Test - @SetEnvironmentVariable(key = "MY_SPECIAL_PROJECT_ID", value = "niels/project") // <<-- Fallback - public void testProjectIdDirectValidValue() { - ProjectId projectId = new ProjectId("otherproject", "MY_SPECIAL_PROJECT_ID"); - assertTrue(projectId.isValid()); - assertEquals("otherproject", projectId.getValue()); - log.info("{}", projectId); - assertEquals("otherproject", projectId.getSanitizedValue()); - assertTrue(projectId.toString().contains("(via property \"gitlab.projectId.id\")")); - } - - @Test - @SetEnvironmentVariable(key = "MY_SPECIAL_PROJECT_ID", value = "niels/project") // <<-- Fallback - public void testProjectIdDirectBadValue() { - ProjectId projectId = new ProjectId("foo bar", "MY_SPECIAL_PROJECT_ID"); - // Do NOT use the fallback because that would cause confusion with the person configuring it. - assertFalse(projectId.isValid()); - assertNull(projectId.getValue()); - log.info("{}", projectId); - assertEquals("<<>>", projectId.getSanitizedValue()); - assertTrue(projectId.toString().contains("the value found using property \"gitlab.projectId.id\" is not valid")); - } - - @Test - @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels/project") // <<-- Fallback - public void testProjectIdDirectEmptyValueFallBackDefault() { - ProjectId projectId = new ProjectId("", null); - assertTrue(projectId.isValid()); - assertEquals("niels/project", projectId.getValue()); - log.info("{}", projectId); - assertEquals("niels/project", projectId.getSanitizedValue()); - assertTrue(projectId.toString().contains("(via environment variable \"CI_PROJECT_ID\")")); - } - - @Test - @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels/project") // <<-- Fallback - public void testProjectIdDirectMissingFallBackDefault() { - ProjectId projectId = new ProjectId(null, null); - assertTrue(projectId.isValid()); - assertEquals("niels/project", projectId.getValue()); - log.info("{}", projectId); - assertEquals("niels/project", projectId.getSanitizedValue()); - assertTrue(projectId.toString().contains("(via environment variable \"CI_PROJECT_ID\")")); - } - - @Test - @SetEnvironmentVariable(key = "MY_SPECIAL_PROJECT_ID", value = "niels/project") // <<-- Fallback - public void testProjectIdDirectEmptyValueFallBackCustom() { - ProjectId projectId = new ProjectId("", "MY_SPECIAL_PROJECT_ID"); - assertTrue(projectId.isValid()); - assertEquals("niels/project", projectId.getValue()); - log.info("{}", projectId); - assertEquals("niels/project", projectId.getSanitizedValue()); - assertTrue(projectId.toString().contains("(via environment variable \"MY_SPECIAL_PROJECT_ID\")")); - } - - @Test - @SetEnvironmentVariable(key = "MY_SPECIAL_PROJECT_ID", value = "niels/project") // <<-- Fallback - public void testProjectIdDirectMissingFallBackCustom() { - ProjectId projectId = new ProjectId(null, "MY_SPECIAL_PROJECT_ID"); - assertTrue(projectId.isValid()); - assertEquals("niels/project", projectId.getValue()); - log.info("{}", projectId); - assertEquals("niels/project", projectId.getSanitizedValue()); - assertTrue(projectId.toString().contains("(via environment variable \"MY_SPECIAL_PROJECT_ID\")")); - } - -} diff --git a/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/gitlab/TestGitlabConfigurationProjectId.kt b/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/gitlab/TestGitlabConfigurationProjectId.kt new file mode 100644 index 0000000..db3e680 --- /dev/null +++ b/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/gitlab/TestGitlabConfigurationProjectId.kt @@ -0,0 +1,220 @@ +/* + * CodeOwners Tools + * Copyright (C) 2023-2026 Niels Basjes + * + * 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 + * + * https://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 nl.basjes.codeowners.validator.gitlab + +import org.junitpioneer.jupiter.SetEnvironmentVariable +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class TestGitlabConfigurationProjectId { + @Test + @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels/project") // <<-- Is good + fun testProjectIdDefaultEnvValidValue() { + val projectId = GitlabConfiguration.ProjectId(null, null) + assertTrue(projectId.isValid()) + assertEquals("niels/project", projectId.value) + log.info("{}", projectId) + assertEquals("niels/project", projectId.sanitizedValue) + assertTrue(projectId.toString().contains("(via environment variable \"CI_PROJECT_ID\")")) + } + + @Test + @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "foo bar") // <<-- Is bad + fun testProjectIdDefaultEnvBadValue() { + val projectId = GitlabConfiguration.ProjectId(null, null) + assertFalse(projectId.isValid()) + assertNull(projectId.value) + log.info("{}", projectId) + assertEquals("<<>>", projectId.sanitizedValue) + assertTrue( + projectId.toString().contains("the value from environment variable \"CI_PROJECT_ID\" is NOT valid") + ) + } + + @Test + @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "") // <<-- Is bad + fun testProjectIdDefaultEnvEmptyValue() { + val projectId = GitlabConfiguration.ProjectId(null, null) + assertFalse(projectId.isValid()) + assertNull(projectId.value) + log.info("{}", projectId) + assertEquals("<<>>", projectId.sanitizedValue) + assertTrue( + projectId.toString().contains("the value from environment variable \"CI_PROJECT_ID\" is NOT valid") + ) + } + + @Test // @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "") // <<-- Is bad + fun testProjectIdDefaultEnvMissing() { + val projectId = GitlabConfiguration.ProjectId(null, null) + assertFalse(projectId.isValid()) + assertNull(projectId.value) + log.info("{}", projectId) + assertEquals("<<>>", projectId.sanitizedValue) + assertTrue( + projectId.toString().contains("the environment variable \"CI_PROJECT_ID\" does not exist") + ) + } + + @Test + @SetEnvironmentVariable(key = "MY_SPECIAL_PROJECT_ID", value = "niels/project") // <<-- Is good + fun testProjectIdCustomEnvValidValue() { + val projectId = GitlabConfiguration.ProjectId(null, "MY_SPECIAL_PROJECT_ID") + assertTrue(projectId.isValid()) + assertEquals("niels/project", projectId.value) + log.info("{}", projectId) + assertEquals("niels/project", projectId.sanitizedValue) + assertTrue(projectId.toString().contains("(via environment variable \"MY_SPECIAL_PROJECT_ID\")")) + } + + @Test + @SetEnvironmentVariable(key = "MY_SPECIAL_PROJECT_ID", value = "foo bar") // <<-- Is bad + fun testProjectIdCustomEnvBadValue() { + val projectId = GitlabConfiguration.ProjectId(null, "MY_SPECIAL_PROJECT_ID") + assertFalse(projectId.isValid()) + assertNull(projectId.value) + log.info("{}", projectId) + assertEquals("<<>>", projectId.sanitizedValue) + assertTrue( + projectId.toString().contains("the value from environment variable \"MY_SPECIAL_PROJECT_ID\" is NOT valid") + ) + } + + @Test + @SetEnvironmentVariable(key = "MY_SPECIAL_PROJECT_ID", value = "") // <<-- Is bad + fun testProjectIdCustomEnvEmptyValue() { + val projectId = GitlabConfiguration.ProjectId(null, "MY_SPECIAL_PROJECT_ID") + assertFalse(projectId.isValid()) + assertNull(projectId.value) + log.info("{}", projectId) + assertEquals("<<>>", projectId.sanitizedValue) + assertTrue( + projectId.toString().contains("the value from environment variable \"MY_SPECIAL_PROJECT_ID\" is NOT valid") + ) + } + + @Test // @SetEnvironmentVariable(key = "MY_SPECIAL_PROJECT_ID", value = "") // <<-- Is bad + fun testProjectIdCustomEnvMissing() { + val projectId = GitlabConfiguration.ProjectId(null, "MY_SPECIAL_PROJECT_ID") + assertFalse(projectId.isValid()) + assertNull(projectId.value) + log.info("{}", projectId) + assertEquals("<<>>", projectId.sanitizedValue) + assertTrue( + projectId.toString().contains("the environment variable \"MY_SPECIAL_PROJECT_ID\" does not exist") + ) + } + + @Test + fun testProjectIdCustomEnvNameEmpty() { + val projectId = GitlabConfiguration.ProjectId(null, "") + assertFalse(projectId.isValid()) + assertNull(projectId.value) + log.info("{}", projectId) + assertTrue( + projectId.toString().contains("the environment variable \"CI_PROJECT_ID\" does not exist") + ) + } + + @Test + fun testProjectIdCustomEnvNameBlank() { + val projectId = GitlabConfiguration.ProjectId(null, " ") + assertFalse(projectId.isValid()) + assertNull(projectId.value) + log.info("{}", projectId) + assertEquals("<<>>", projectId.sanitizedValue) + assertTrue(projectId.toString().contains("the environment variable name \" \" is blank.")) + } + + @Test + @SetEnvironmentVariable(key = "MY_SPECIAL_PROJECT_ID", value = "niels/project") // <<-- Fallback + fun testProjectIdDirectValidValue() { + val projectId = GitlabConfiguration.ProjectId("otherproject", "MY_SPECIAL_PROJECT_ID") + assertTrue(projectId.isValid()) + assertEquals("otherproject", projectId.value) + log.info("{}", projectId) + assertEquals("otherproject", projectId.sanitizedValue) + assertTrue(projectId.toString().contains("(via property \"gitlab.projectId.id\")")) + } + + @Test + @SetEnvironmentVariable(key = "MY_SPECIAL_PROJECT_ID", value = "niels/project") // <<-- Fallback + fun testProjectIdDirectBadValue() { + val projectId = GitlabConfiguration.ProjectId("foo bar", "MY_SPECIAL_PROJECT_ID") + // Do NOT use the fallback because that would cause confusion with the person configuring it. + assertFalse(projectId.isValid()) + assertNull(projectId.value) + log.info("{}", projectId) + assertEquals("<<>>", projectId.sanitizedValue) + assertTrue( + projectId.toString().contains("the value found using property \"gitlab.projectId.id\" is not valid") + ) + } + + @Test + @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels/project") // <<-- Fallback + fun testProjectIdDirectEmptyValueFallBackDefault() { + val projectId = GitlabConfiguration.ProjectId("", null) + assertTrue(projectId.isValid()) + assertEquals("niels/project", projectId.value) + log.info("{}", projectId) + assertEquals("niels/project", projectId.sanitizedValue) + assertTrue(projectId.toString().contains("(via environment variable \"CI_PROJECT_ID\")")) + } + + @Test + @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels/project") // <<-- Fallback + fun testProjectIdDirectMissingFallBackDefault() { + val projectId = GitlabConfiguration.ProjectId(null, null) + assertTrue(projectId.isValid()) + assertEquals("niels/project", projectId.value) + log.info("{}", projectId) + assertEquals("niels/project", projectId.sanitizedValue) + assertTrue(projectId.toString().contains("(via environment variable \"CI_PROJECT_ID\")")) + } + + @Test + @SetEnvironmentVariable(key = "MY_SPECIAL_PROJECT_ID", value = "niels/project") // <<-- Fallback + fun testProjectIdDirectEmptyValueFallBackCustom() { + val projectId = GitlabConfiguration.ProjectId("", "MY_SPECIAL_PROJECT_ID") + assertTrue(projectId.isValid()) + assertEquals("niels/project", projectId.value) + log.info("{}", projectId) + assertEquals("niels/project", projectId.sanitizedValue) + assertTrue(projectId.toString().contains("(via environment variable \"MY_SPECIAL_PROJECT_ID\")")) + } + + @Test + @SetEnvironmentVariable(key = "MY_SPECIAL_PROJECT_ID", value = "niels/project") // <<-- Fallback + fun testProjectIdDirectMissingFallBackCustom() { + val projectId = GitlabConfiguration.ProjectId(null, "MY_SPECIAL_PROJECT_ID") + assertTrue(projectId.isValid()) + assertEquals("niels/project", projectId.value) + log.info("{}", projectId) + assertEquals("niels/project", projectId.sanitizedValue) + assertTrue(projectId.toString().contains("(via environment variable \"MY_SPECIAL_PROJECT_ID\")")) + } + + companion object { + private val log: Logger = LoggerFactory.getLogger(TestGitlabConfigurationProjectId::class.java) + } +} diff --git a/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/gitlab/TestGitlabConfigurationServerUrl.java b/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/gitlab/TestGitlabConfigurationServerUrl.java deleted file mode 100644 index 2853757..0000000 --- a/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/gitlab/TestGitlabConfigurationServerUrl.java +++ /dev/null @@ -1,209 +0,0 @@ -/* - * CodeOwners Tools - * Copyright (C) 2023-2026 Niels Basjes - * - * 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 - * - * https://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 nl.basjes.codeowners.validator.gitlab; - -import nl.basjes.codeowners.validator.gitlab.GitlabConfiguration.ServerUrl; -import org.junit.jupiter.api.Test; -import org.junitpioneer.jupiter.SetEnvironmentVariable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class TestGitlabConfigurationServerUrl { - - private static final Logger log = LoggerFactory.getLogger(TestGitlabConfigurationServerUrl.class); - - @Test - @SetEnvironmentVariable(key = "CI_SERVER_URL", value = "https://git.example.nl") // <<-- Is good - public void testServerUrlDefaultEnvValidValue() { - ServerUrl serverUrl = new ServerUrl(null, null); - assertTrue(serverUrl.isValid()); - assertEquals("https://git.example.nl", serverUrl.getValue()); - log.info("{}", serverUrl); - assertEquals("https://git.example.nl", serverUrl.getSanitizedValue()); - assertTrue(serverUrl.toString().contains("(via environment variable \"CI_SERVER_URL\")")); - } - - @Test - @SetEnvironmentVariable(key = "CI_SERVER_URL", value = "foobar") // <<-- Is bad - public void testServerUrlDefaultEnvBadValue() { - ServerUrl serverUrl = new ServerUrl(null, null); - assertFalse(serverUrl.isValid()); - assertNull(serverUrl.getValue()); - log.info("{}", serverUrl); - assertEquals("<<>>", serverUrl.getSanitizedValue()); - assertTrue(serverUrl.toString().contains("the value from environment variable \"CI_SERVER_URL\" is NOT valid")); - } - - @Test - @SetEnvironmentVariable(key = "CI_SERVER_URL", value = "") // <<-- Is bad - public void testServerUrlDefaultEnvEmptyValue() { - ServerUrl serverUrl = new ServerUrl(null, null); - assertFalse(serverUrl.isValid()); - assertNull(serverUrl.getValue()); - log.info("{}", serverUrl); - assertEquals("<<>>", serverUrl.getSanitizedValue()); - assertTrue(serverUrl.toString().contains("the value from environment variable \"CI_SERVER_URL\" is NOT valid")); - } - - @Test -// @SetEnvironmentVariable(key = "CI_SERVER_URL", value = "") // <<-- Is bad - public void testServerUrlDefaultEnvMissing() { - ServerUrl serverUrl = new ServerUrl(null, null); - assertFalse(serverUrl.isValid()); - assertNull(serverUrl.getValue()); - log.info("{}", serverUrl); - assertEquals("<<>>", serverUrl.getSanitizedValue()); - assertTrue(serverUrl.toString().contains("the environment variable \"CI_SERVER_URL\" does not exist")); - } - - @Test - @SetEnvironmentVariable(key = "MY_SPECIAL_SERVER_URL", value = "https://git.example.nl") // <<-- Is good - public void testServerUrlCustomEnvValidValue() { - ServerUrl serverUrl = new ServerUrl(null, "MY_SPECIAL_SERVER_URL"); - assertTrue(serverUrl.isValid()); - assertEquals("https://git.example.nl", serverUrl.getValue()); - log.info("{}", serverUrl); - assertEquals("https://git.example.nl", serverUrl.getSanitizedValue()); - assertTrue(serverUrl.toString().contains("(via environment variable \"MY_SPECIAL_SERVER_URL\")")); - } - - @Test - @SetEnvironmentVariable(key = "MY_SPECIAL_SERVER_URL", value = "foobar") // <<-- Is bad - public void testServerUrlCustomEnvBadValue() { - ServerUrl serverUrl = new ServerUrl(null, "MY_SPECIAL_SERVER_URL"); - assertFalse(serverUrl.isValid()); - assertNull(serverUrl.getValue()); - log.info("{}", serverUrl); - assertEquals("<<>>", serverUrl.getSanitizedValue()); - assertTrue(serverUrl.toString().contains("the value from environment variable \"MY_SPECIAL_SERVER_URL\" is NOT valid")); - } - - @Test - @SetEnvironmentVariable(key = "MY_SPECIAL_SERVER_URL", value = "") // <<-- Is bad - public void testServerUrlCustomEnvEmptyValue() { - ServerUrl serverUrl = new ServerUrl(null, "MY_SPECIAL_SERVER_URL"); - assertFalse(serverUrl.isValid()); - assertNull(serverUrl.getValue()); - log.info("{}", serverUrl); - assertEquals("<<>>", serverUrl.getSanitizedValue()); - assertTrue(serverUrl.toString().contains("the value from environment variable \"MY_SPECIAL_SERVER_URL\" is NOT valid")); - } - - @Test -// @SetEnvironmentVariable(key = "MY_SPECIAL_SERVER_URL", value = "") // <<-- Is bad - public void testServerUrlCustomEnvMissing() { - ServerUrl serverUrl = new ServerUrl(null, "MY_SPECIAL_SERVER_URL"); - assertFalse(serverUrl.isValid()); - assertNull(serverUrl.getValue()); - log.info("{}", serverUrl); - assertEquals("<<>>", serverUrl.getSanitizedValue()); - assertTrue(serverUrl.toString().contains("the environment variable \"MY_SPECIAL_SERVER_URL\" does not exist")); - } - - @Test - public void testServerUrlCustomEnvNameEmpty() { - ServerUrl serverUrl = new ServerUrl(null, ""); - assertFalse(serverUrl.isValid()); - assertNull(serverUrl.getValue()); - log.info("{}", serverUrl); - assertEquals("<<>>", serverUrl.getSanitizedValue()); - assertTrue(serverUrl.toString().contains("the environment variable \"CI_SERVER_URL\" does not exist")); - } - - @Test - public void testServerUrlCustomEnvNameBlank() { - ServerUrl serverUrl = new ServerUrl(null, " "); - assertFalse(serverUrl.isValid()); - assertNull(serverUrl.getValue()); - log.info("{}", serverUrl); - assertEquals("<<>>", serverUrl.getSanitizedValue()); - assertTrue(serverUrl.toString().contains("the environment variable name \" \" is blank.")); - } - - @Test - @SetEnvironmentVariable(key = "MY_SPECIAL_SERVER_URL", value = "https://git.example.nl") // <<-- Fallback - public void testServerUrlDirectValidValue() { - ServerUrl serverUrl = new ServerUrl("https://git.example.com", "MY_SPECIAL_SERVER_URL"); - assertTrue(serverUrl.isValid()); - assertEquals("https://git.example.com", serverUrl.getValue()); - log.info("{}", serverUrl); - assertEquals("https://git.example.com", serverUrl.getSanitizedValue()); - assertTrue(serverUrl.toString().contains("(via property \"gitlab.serverUrl.url\")")); - } - - @Test - @SetEnvironmentVariable(key = "MY_SPECIAL_SERVER_URL", value = "https://git.example.nl") // <<-- Fallback - public void testServerUrlDirectBadValue() { - ServerUrl serverUrl = new ServerUrl("foobar", "MY_SPECIAL_SERVER_URL"); - // Do NOT use the fallback because that would cause confusion with the person configuring it. - assertFalse(serverUrl.isValid()); - assertNull(serverUrl.getValue()); - log.info("{}", serverUrl); - assertEquals("<<>>", serverUrl.getSanitizedValue()); - assertTrue(serverUrl.toString().contains("the value found using property \"gitlab.serverUrl.url\" is not valid")); - } - - @Test - @SetEnvironmentVariable(key = "CI_SERVER_URL", value = "https://git.example.nl") // <<-- Fallback - public void testServerUrlDirectEmptyValueFallBackDefault() { - ServerUrl serverUrl = new ServerUrl("", null); - assertTrue(serverUrl.isValid()); - assertEquals("https://git.example.nl", serverUrl.getValue()); - log.info("{}", serverUrl); - assertEquals("https://git.example.nl", serverUrl.getSanitizedValue()); - assertTrue(serverUrl.toString().contains("(via environment variable \"CI_SERVER_URL\")")); - } - - @Test - @SetEnvironmentVariable(key = "CI_SERVER_URL", value = "https://git.example.nl") // <<-- Fallback - public void testServerUrlDirectMissingFallBackDefault() { - ServerUrl serverUrl = new ServerUrl(null, null); - assertTrue(serverUrl.isValid()); - assertEquals("https://git.example.nl", serverUrl.getValue()); - log.info("{}", serverUrl); - assertEquals("https://git.example.nl", serverUrl.getSanitizedValue()); - assertTrue(serverUrl.toString().contains("(via environment variable \"CI_SERVER_URL\")")); - } - - @Test - @SetEnvironmentVariable(key = "MY_SPECIAL_SERVER_URL", value = "https://git.example.nl") // <<-- Fallback - public void testServerUrlDirectEmptyValueFallBackCustom() { - ServerUrl serverUrl = new ServerUrl("", "MY_SPECIAL_SERVER_URL"); - assertTrue(serverUrl.isValid()); - assertEquals("https://git.example.nl", serverUrl.getValue()); - log.info("{}", serverUrl); - assertEquals("https://git.example.nl", serverUrl.getSanitizedValue()); - assertTrue(serverUrl.toString().contains("(via environment variable \"MY_SPECIAL_SERVER_URL\")")); - } - - @Test - @SetEnvironmentVariable(key = "MY_SPECIAL_SERVER_URL", value = "https://git.example.nl") // <<-- Fallback - public void testServerUrlDirectMissingFallBackCustom() { - ServerUrl serverUrl = new ServerUrl(null, "MY_SPECIAL_SERVER_URL"); - assertTrue(serverUrl.isValid()); - assertEquals("https://git.example.nl", serverUrl.getValue()); - log.info("{}", serverUrl); - assertEquals("https://git.example.nl", serverUrl.getSanitizedValue()); - assertTrue(serverUrl.toString().contains("(via environment variable \"MY_SPECIAL_SERVER_URL\")")); - } - -} diff --git a/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/gitlab/TestGitlabConfigurationServerUrl.kt b/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/gitlab/TestGitlabConfigurationServerUrl.kt new file mode 100644 index 0000000..a8bdee2 --- /dev/null +++ b/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/gitlab/TestGitlabConfigurationServerUrl.kt @@ -0,0 +1,221 @@ +/* + * CodeOwners Tools + * Copyright (C) 2023-2026 Niels Basjes + * + * 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 + * + * https://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 nl.basjes.codeowners.validator.gitlab + +import org.junitpioneer.jupiter.SetEnvironmentVariable +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class TestGitlabConfigurationServerUrl { + @Test + @SetEnvironmentVariable(key = "CI_SERVER_URL", value = "https://git.example.nl") // <<-- Is good + fun testServerUrlDefaultEnvValidValue() { + val serverUrl = GitlabConfiguration.ServerUrl(null, null) + assertTrue(serverUrl.isValid()) + assertEquals("https://git.example.nl", serverUrl.value) + log.info("{}", serverUrl) + assertEquals("https://git.example.nl", serverUrl.sanitizedValue) + assertTrue(serverUrl.toString().contains("(via environment variable \"CI_SERVER_URL\")")) + } + + @Test + @SetEnvironmentVariable(key = "CI_SERVER_URL", value = "foobar") // <<-- Is bad + fun testServerUrlDefaultEnvBadValue() { + val serverUrl = GitlabConfiguration.ServerUrl(null, null) + assertFalse(serverUrl.isValid()) + assertNull(serverUrl.value) + log.info("{}", serverUrl) + assertEquals("<<>>", serverUrl.sanitizedValue) + assertTrue( + serverUrl.toString().contains("the value from environment variable \"CI_SERVER_URL\" is NOT valid") + ) + } + + @Test + @SetEnvironmentVariable(key = "CI_SERVER_URL", value = "") // <<-- Is bad + fun testServerUrlDefaultEnvEmptyValue() { + val serverUrl = GitlabConfiguration.ServerUrl(null, null) + assertFalse(serverUrl.isValid()) + assertNull(serverUrl.value) + log.info("{}", serverUrl) + assertEquals("<<>>", serverUrl.sanitizedValue) + assertTrue( + serverUrl.toString().contains("the value from environment variable \"CI_SERVER_URL\" is NOT valid") + ) + } + + @Test // @SetEnvironmentVariable(key = "CI_SERVER_URL", value = "") // <<-- Is bad + fun testServerUrlDefaultEnvMissing() { + val serverUrl = GitlabConfiguration.ServerUrl(null, null) + assertFalse(serverUrl.isValid()) + assertNull(serverUrl.value) + log.info("{}", serverUrl) + assertEquals("<<>>", serverUrl.sanitizedValue) + assertTrue( + serverUrl.toString().contains("the environment variable \"CI_SERVER_URL\" does not exist") + ) + } + + @Test + @SetEnvironmentVariable(key = "MY_SPECIAL_SERVER_URL", value = "https://git.example.nl") // <<-- Is good + fun testServerUrlCustomEnvValidValue() { + val serverUrl = GitlabConfiguration.ServerUrl(null, "MY_SPECIAL_SERVER_URL") + assertTrue(serverUrl.isValid()) + assertEquals("https://git.example.nl", serverUrl.value) + log.info("{}", serverUrl) + assertEquals("https://git.example.nl", serverUrl.sanitizedValue) + assertTrue(serverUrl.toString().contains("(via environment variable \"MY_SPECIAL_SERVER_URL\")")) + } + + @Test + @SetEnvironmentVariable(key = "MY_SPECIAL_SERVER_URL", value = "foobar") // <<-- Is bad + fun testServerUrlCustomEnvBadValue() { + val serverUrl = GitlabConfiguration.ServerUrl(null, "MY_SPECIAL_SERVER_URL") + assertFalse(serverUrl.isValid()) + assertNull(serverUrl.value) + log.info("{}", serverUrl) + assertEquals("<<>>", serverUrl.sanitizedValue) + assertTrue( + serverUrl.toString().contains("the value from environment variable \"MY_SPECIAL_SERVER_URL\" is NOT valid") + ) + } + + @Test + @SetEnvironmentVariable(key = "MY_SPECIAL_SERVER_URL", value = "") // <<-- Is bad + fun testServerUrlCustomEnvEmptyValue() { + val serverUrl = GitlabConfiguration.ServerUrl(null, "MY_SPECIAL_SERVER_URL") + assertFalse(serverUrl.isValid()) + assertNull(serverUrl.value) + log.info("{}", serverUrl) + assertEquals("<<>>", serverUrl.sanitizedValue) + assertTrue( + serverUrl.toString().contains("the value from environment variable \"MY_SPECIAL_SERVER_URL\" is NOT valid") + ) + } + + @Test // @SetEnvironmentVariable(key = "MY_SPECIAL_SERVER_URL", value = "") // <<-- Is bad + fun testServerUrlCustomEnvMissing() { + val serverUrl = GitlabConfiguration.ServerUrl(null, "MY_SPECIAL_SERVER_URL") + assertFalse(serverUrl.isValid()) + assertNull(serverUrl.value) + log.info("{}", serverUrl) + assertEquals("<<>>", serverUrl.sanitizedValue) + assertTrue( + serverUrl.toString().contains("the environment variable \"MY_SPECIAL_SERVER_URL\" does not exist") + ) + } + + @Test + fun testServerUrlCustomEnvNameEmpty() { + val serverUrl = GitlabConfiguration.ServerUrl(null, "") + assertFalse(serverUrl.isValid()) + assertNull(serverUrl.value) + log.info("{}", serverUrl) + assertEquals("<<>>", serverUrl.sanitizedValue) + assertTrue( + serverUrl.toString().contains("the environment variable \"CI_SERVER_URL\" does not exist") + ) + } + + @Test + fun testServerUrlCustomEnvNameBlank() { + val serverUrl = GitlabConfiguration.ServerUrl(null, " ") + assertFalse(serverUrl.isValid()) + assertNull(serverUrl.value) + log.info("{}", serverUrl) + assertEquals("<<>>", serverUrl.sanitizedValue) + assertTrue(serverUrl.toString().contains("the environment variable name \" \" is blank.")) + } + + @Test + @SetEnvironmentVariable(key = "MY_SPECIAL_SERVER_URL", value = "https://git.example.nl") // <<-- Fallback + fun testServerUrlDirectValidValue() { + val serverUrl = GitlabConfiguration.ServerUrl("https://git.example.com", "MY_SPECIAL_SERVER_URL") + assertTrue(serverUrl.isValid()) + assertEquals("https://git.example.com", serverUrl.value) + log.info("{}", serverUrl) + assertEquals("https://git.example.com", serverUrl.sanitizedValue) + assertTrue(serverUrl.toString().contains("(via property \"gitlab.serverUrl.url\")")) + } + + @Test + @SetEnvironmentVariable(key = "MY_SPECIAL_SERVER_URL", value = "https://git.example.nl") // <<-- Fallback + fun testServerUrlDirectBadValue() { + val serverUrl = GitlabConfiguration.ServerUrl("foobar", "MY_SPECIAL_SERVER_URL") + // Do NOT use the fallback because that would cause confusion with the person configuring it. + assertFalse(serverUrl.isValid()) + assertNull(serverUrl.value) + log.info("{}", serverUrl) + assertEquals("<<>>", serverUrl.sanitizedValue) + assertTrue( + serverUrl.toString().contains("the value found using property \"gitlab.serverUrl.url\" is not valid") + ) + } + + @Test + @SetEnvironmentVariable(key = "CI_SERVER_URL", value = "https://git.example.nl") // <<-- Fallback + fun testServerUrlDirectEmptyValueFallBackDefault() { + val serverUrl = GitlabConfiguration.ServerUrl("", null) + assertTrue(serverUrl.isValid()) + assertEquals("https://git.example.nl", serverUrl.value) + log.info("{}", serverUrl) + assertEquals("https://git.example.nl", serverUrl.sanitizedValue) + assertTrue(serverUrl.toString().contains("(via environment variable \"CI_SERVER_URL\")")) + } + + @Test + @SetEnvironmentVariable(key = "CI_SERVER_URL", value = "https://git.example.nl") // <<-- Fallback + fun testServerUrlDirectMissingFallBackDefault() { + val serverUrl = GitlabConfiguration.ServerUrl(null, null) + assertTrue(serverUrl.isValid()) + assertEquals("https://git.example.nl", serverUrl.value) + log.info("{}", serverUrl) + assertEquals("https://git.example.nl", serverUrl.sanitizedValue) + assertTrue(serverUrl.toString().contains("(via environment variable \"CI_SERVER_URL\")")) + } + + @Test + @SetEnvironmentVariable(key = "MY_SPECIAL_SERVER_URL", value = "https://git.example.nl") // <<-- Fallback + fun testServerUrlDirectEmptyValueFallBackCustom() { + val serverUrl = GitlabConfiguration.ServerUrl("", "MY_SPECIAL_SERVER_URL") + assertTrue(serverUrl.isValid()) + assertEquals("https://git.example.nl", serverUrl.value) + log.info("{}", serverUrl) + assertEquals("https://git.example.nl", serverUrl.sanitizedValue) + assertTrue(serverUrl.toString().contains("(via environment variable \"MY_SPECIAL_SERVER_URL\")")) + } + + @Test + @SetEnvironmentVariable(key = "MY_SPECIAL_SERVER_URL", value = "https://git.example.nl") // <<-- Fallback + fun testServerUrlDirectMissingFallBackCustom() { + val serverUrl = GitlabConfiguration.ServerUrl(null, "MY_SPECIAL_SERVER_URL") + assertTrue(serverUrl.isValid()) + assertEquals("https://git.example.nl", serverUrl.value) + log.info("{}", serverUrl) + assertEquals("https://git.example.nl", serverUrl.sanitizedValue) + assertTrue(serverUrl.toString().contains("(via environment variable \"MY_SPECIAL_SERVER_URL\")")) + } + + companion object { + private val log: Logger = LoggerFactory.getLogger(TestGitlabConfigurationServerUrl::class.java) + } +} diff --git a/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/gitlab/TestGitlabUsers.java b/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/gitlab/TestGitlabUsers.java deleted file mode 100644 index 8f08dc3..0000000 --- a/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/gitlab/TestGitlabUsers.java +++ /dev/null @@ -1,523 +0,0 @@ -/* - * CodeOwners Tools - * Copyright (C) 2023-2026 Niels Basjes - * - * 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 - * - * https://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 nl.basjes.codeowners.validator.gitlab; - -import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; -import com.github.tomakehurst.wiremock.junit5.WireMockTest; -import nl.basjes.codeowners.CodeOwners; -import nl.basjes.codeowners.validator.CodeOwnersValidationException; -import nl.basjes.codeowners.validator.gitlab.GitlabConfiguration.AccessToken; -import nl.basjes.codeowners.validator.gitlab.GitlabConfiguration.FailLevel; -import nl.basjes.codeowners.validator.gitlab.GitlabConfiguration.ProjectId; -import nl.basjes.codeowners.validator.gitlab.GitlabConfiguration.ServerUrl; -import nl.basjes.codeowners.validator.utils.Problem; -import nl.basjes.codeowners.validator.utils.ProblemTable; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInfo; -import org.junitpioneer.jupiter.SetEnvironmentVariable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.lang.reflect.Method; -import java.util.stream.Collectors; - -import static nl.basjes.codeowners.validator.gitlab.GitlabConfiguration.FailLevel.ERROR; -import static nl.basjes.codeowners.validator.gitlab.GitlabConfiguration.FailLevel.FATAL; -import static nl.basjes.codeowners.validator.gitlab.GitlabConfiguration.FailLevel.NEVER; -import static nl.basjes.codeowners.validator.gitlab.GitlabConfiguration.FailLevel.WARNING; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -@WireMockTest -public class TestGitlabUsers { - - static GitlabConfiguration makeConfig(WireMockRuntimeInfo wmRuntimeInfo, String projectId, String tokenEnvVariableName) { - return new GitlabConfiguration( - new ServerUrl(wmRuntimeInfo==null ? null : wmRuntimeInfo.getHttpBaseUrl(), null), - new ProjectId(projectId, null), - new AccessToken(tokenEnvVariableName) - ); - } - - @Test - @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels/project") - @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "gltst-validtoken") - public void testValidCodeOwners(WireMockRuntimeInfo wmRuntimeInfo) throws CodeOwnersValidationException { - GitlabConfiguration configuration = makeConfig(wmRuntimeInfo, null, "FETCH_USER_ACCESS_TOKEN"); - - try(GitlabProjectMembers gitlabProjectMembers = new GitlabProjectMembers(configuration)) { - CodeOwners codeOwners = new CodeOwners( - "[README Owners]\n" + - "README.md @niels @SomeUser @isguest\n" + - "README_owner.md @@owner\n" + - "README_owners.md @@owners\n" + - "README_maintainer.md @@maintainer\n" + - "README_maintainers.md @@maintainers\n" + - "README_developer.md @@developer\n" + - "README_developers.md @@developers\n" + - "README_group_dev.md @codeowners/developers\n" + - "README_group_main.md @codeowners/maintainers\n" + - "README_user_public_email.md public@example.nl\n" - ); - - Logger logger = LoggerFactory.getLogger("testValidCodeOwners"); - gitlabProjectMembers.setShowAllApprovers(true); - logger.info(gitlabProjectMembers.verifyAllCodeowners(logger, codeOwners).toString()); - logger.info(gitlabProjectMembers.verifyAllCodeowners(logger, codeOwners).toProblemMessageGroupedString()); - gitlabProjectMembers.setShowAllApprovers(false); - logger.info(gitlabProjectMembers.verifyAllCodeowners(logger, codeOwners).toString()); - logger.info(gitlabProjectMembers.verifyAllCodeowners(logger, codeOwners).toProblemMessageGroupedString()); - } - } - - @Test - @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels/project") - @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "gltst-validtoken") - public void testRoles(WireMockRuntimeInfo wmRuntimeInfo) throws CodeOwnersValidationException { - GitlabConfiguration configuration = makeConfig(wmRuntimeInfo, null, "FETCH_USER_ACCESS_TOKEN"); - - try(GitlabProjectMembers gitlabProjectMembers = new GitlabProjectMembers(configuration)) { - CodeOwners codeOwners = new CodeOwners( - "[README Owners]\n" + - "README_niels.md @niels\n" + - "README_owner.md @@owner\n" + - "README_owners.md @@owners\n" + - "README_maintainer.md @@maintainer\n" + - "README_maintainers.md @@maintainers\n" + - "README_developer.md @@developer\n" + - "README_developers.md @@developers\n" - ); - - Logger logger = LoggerFactory.getLogger("testRoles"); - gitlabProjectMembers.setShowAllApprovers(true); - gitlabProjectMembers.verifyAllCodeowners(logger, codeOwners); - gitlabProjectMembers.setShowAllApprovers(false); - gitlabProjectMembers.verifyAllCodeowners(logger, codeOwners); - } - } - - @Test - @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels/project") - @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "gltst-validtoken") - public void testEmpty(WireMockRuntimeInfo wmRuntimeInfo, TestInfo testInfo) throws CodeOwnersValidationException { - GitlabConfiguration configuration = makeConfig(wmRuntimeInfo, null, "FETCH_USER_ACCESS_TOKEN"); - configuration.setFailLevel(WARNING); - - CodeOwnersValidationException exception = runCodeownersValidation(testInfo, configuration, - new CodeOwners( - "[README Owners]\n" - ) - ); - if (exception != null) { - throw exception; - } - } - - @Test - @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels/project") - @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "gltst-validtoken") - public void testSharedGroups(WireMockRuntimeInfo wmRuntimeInfo, TestInfo testInfo) throws CodeOwnersValidationException { - GitlabConfiguration configuration = makeConfig(wmRuntimeInfo, null, "FETCH_USER_ACCESS_TOKEN"); - configuration.setFailLevel(FATAL); - - CodeOwnersValidationException exception = runCodeownersValidation(testInfo, configuration, - new CodeOwners( - "[README Owners]\n" + - "README_shared_group_guests.md @codeowners/guests\n" + - "README_shared_group_developers.md @codeowners/developers\n" + - "README_shared_group_maintainers.md @codeowners/maintainers\n" - ), - new Problem.Warning("README Owners", "README_shared_group_guests.md", "@codeowners/guests", "Shared group does not have sufficient permissions to approve: AccessLevel=10 (=GUEST)"), - new Problem.Error ("README Owners", "README_shared_group_guests.md", "", "NO Valid Approvers for rule") - ); - if (exception != null) { - throw exception; - } - } - - @Test - @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels/project") - @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "gltst-validtoken") - public void testUnknownEmailAssumptions(WireMockRuntimeInfo wmRuntimeInfo, TestInfo testInfo) throws CodeOwnersValidationException { - GitlabConfiguration configuration = makeConfig(wmRuntimeInfo, null, "FETCH_USER_ACCESS_TOKEN"); - - CodeOwners codeOwners = new CodeOwners( - "[README Owners]\n" + - "README_niels_private_email.md private@example.nl\n" // WARNING: niels --> is member --> Cannot find --> Warning only - ); - - configuration.setAssumeUncheckableEmailExistsAndCanApprove(true); - assertNull(runCodeownersValidation(testInfo, configuration, codeOwners, - new Problem.Warning("README Owners", "README_niels_private_email.md", "private@example.nl", "Unable to verify email address: Assuming the user exists and can approve") - )); - - configuration.setAssumeUncheckableEmailExistsAndCanApprove(false); - configuration.getProblemLevels().setUserUnknownEmail(GitlabConfiguration.Level.ERROR); - configuration.getProblemLevels().setNoValidApprovers(GitlabConfiguration.Level.FATAL); - assertNotNull(runCodeownersValidation(testInfo, configuration, codeOwners, - new Problem.Error("README Owners", "README_niels_private_email.md", "private@example.nl", "Unable to verify email address: Assuming the user does not exist and/or cannot approve"), - new Problem.Fatal("README Owners", "README_niels_private_email.md", "", "NO Valid Approvers for rule") - )); - } - - - @Test - @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "nomembers") - @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "gltst-validtoken") - public void testBadRoles(WireMockRuntimeInfo wmRuntimeInfo, TestInfo testInfo) throws CodeOwnersValidationException { - GitlabConfiguration configuration = makeConfig(wmRuntimeInfo, null, "FETCH_USER_ACCESS_TOKEN"); - - CodeOwnersValidationException exception = runCodeownersValidation(testInfo, configuration, - new CodeOwners( - "[README Owners]\n" + - "README_1.md @niels @dummy\n" + - "README_2.md @niels @dummy\n" + - "README_owner.md @@owner\n" + - "README_owners.md @@owners\n" + - "README_maintainer.md @@maintainer\n" + - "README_maintainers.md @@maintainers\n" + - "README_developer.md @@developer\n" + - "README_developers.md @@developers\n" + - "README_nosuchrole.md @@nosuchrole\n" + - "README_disabled_1.md @disabledowner @disabledmaintainer @disableddeveloper\n" + - "README_locked_1.md @lockedowner @lockedmaintainer @lockeddeveloper\n" + - "README_disabled_2.md @disabledowner @disabledmaintainer @disableddeveloper\n" + - "README_locked_2.md @lockedowner @lockedmaintainer @lockeddeveloper\n" + - "README_dummy_1.md @dummy\n" + - "README_dummy_2.md @dummy\n" + - "README_dummy_3.md @dummy\n" + - "README_nonsharedgroup_1.md @opensource \n" + - "README_nonsharedgroup_2.md @opensource \n" + - "README_nonsharedgroup_3.md @opensource \n" + - "README_sharedgroup_guests_1.md @codeowners/guests \n" + - "README_sharedgroup_guests_2.md @codeowners/guests \n" + - "README_sharedgroup_guests_3.md @codeowners/guests \n" + - "README_nosuchgroup_1.md @codeowners/nosuchgroup \n" + - "README_nosuchgroup_2.md @codeowners/nosuchgroup \n" + - "README_nosuchgroup_3.md @codeowners/nosuchgroup \n" - ), - - new Problem.Error ("README Owners", "README_1.md", "@niels", "User exists but is not a member of with this project: Niels Basjes"), - new Problem.Error ("README Owners", "README_1.md", "@dummy", "User exists but is not a member of with this project: Dummy User"), - new Problem.Error ("README Owners", "README_1.md", "", "NO Valid Approvers for rule"), - new Problem.Error ("README Owners", "README_2.md", "@niels", "User exists but is not a member of with this project: Niels Basjes"), - new Problem.Error ("README Owners", "README_2.md", "@dummy", "User exists but is not a member of with this project: Dummy User"), - new Problem.Error ("README Owners", "README_2.md", "", "NO Valid Approvers for rule"), - new Problem.Warning ("README Owners", "README_owner.md", "@@owner", "No direct project members have the \"owner\" role"), - new Problem.Error ("README Owners", "README_owner.md", "", "NO Valid Approvers for rule"), - new Problem.Warning ("README Owners", "README_owners.md", "@@owners", "No direct project members have the \"owner\" role"), - new Problem.Error ("README Owners", "README_owners.md", "", "NO Valid Approvers for rule"), - new Problem.Warning ("README Owners", "README_maintainer.md", "@@maintainer", "No direct project members have the \"maintainer\" role"), - new Problem.Error ("README Owners", "README_maintainer.md", "", "NO Valid Approvers for rule"), - new Problem.Warning ("README Owners", "README_maintainers.md", "@@maintainers", "No direct project members have the \"maintainer\" role"), - new Problem.Error ("README Owners", "README_maintainers.md", "", "NO Valid Approvers for rule"), - new Problem.Warning ("README Owners", "README_developer.md", "@@developer", "No direct project members have the \"developer\" role"), - new Problem.Error ("README Owners", "README_developer.md", "", "NO Valid Approvers for rule"), - new Problem.Warning ("README Owners", "README_developers.md", "@@developers", "No direct project members have the \"developer\" role"), - new Problem.Error ("README Owners", "README_developers.md", "", "NO Valid Approvers for rule"), - new Problem.Fatal ("README Owners", "README_nosuchrole.md", "@@nosuchrole", "Illegal role was specified"), - new Problem.Error ("README Owners", "README_nosuchrole.md", "", "NO Valid Approvers for rule"), - new Problem.Warning ("README Owners", "README_disabled_1.md", "@disabledowner", "Disabled account: State=disabled; Locked=false"), - new Problem.Warning ("README Owners", "README_disabled_1.md", "@disabledmaintainer", "Disabled account: State=disabled; Locked=false"), - new Problem.Warning ("README Owners", "README_disabled_1.md", "@disableddeveloper", "Disabled account: State=disabled; Locked=false"), - new Problem.Error ("README Owners", "README_disabled_1.md", "", "NO Valid Approvers for rule"), - new Problem.Warning ("README Owners", "README_locked_1.md", "@lockedowner", "Disabled account: State=active; Locked=true"), - new Problem.Warning ("README Owners", "README_locked_1.md", "@lockedmaintainer", "Disabled account: State=active; Locked=true"), - new Problem.Warning ("README Owners", "README_locked_1.md", "@lockeddeveloper", "Disabled account: State=active; Locked=true"), - new Problem.Error ("README Owners", "README_locked_1.md", "", "NO Valid Approvers for rule"), - new Problem.Warning ("README Owners", "README_disabled_2.md", "@disabledowner", "Disabled account: State=disabled; Locked=false"), - new Problem.Warning ("README Owners", "README_disabled_2.md", "@disabledmaintainer", "Disabled account: State=disabled; Locked=false"), - new Problem.Warning ("README Owners", "README_disabled_2.md", "@disableddeveloper", "Disabled account: State=disabled; Locked=false"), - new Problem.Error ("README Owners", "README_disabled_2.md", "", "NO Valid Approvers for rule"), - new Problem.Warning ("README Owners", "README_locked_2.md", "@lockedowner", "Disabled account: State=active; Locked=true"), - new Problem.Warning ("README Owners", "README_locked_2.md", "@lockedmaintainer", "Disabled account: State=active; Locked=true"), - new Problem.Warning ("README Owners", "README_locked_2.md", "@lockeddeveloper", "Disabled account: State=active; Locked=true"), - new Problem.Error ("README Owners", "README_locked_2.md", "", "NO Valid Approvers for rule"), - new Problem.Error ("README Owners", "README_dummy_1.md", "@dummy", "User exists but is not a member of with this project: Dummy User"), - new Problem.Error ("README Owners", "README_dummy_1.md", "", "NO Valid Approvers for rule"), - new Problem.Error ("README Owners", "README_dummy_2.md", "@dummy", "User exists but is not a member of with this project: Dummy User"), - new Problem.Error ("README Owners", "README_dummy_2.md", "", "NO Valid Approvers for rule"), - new Problem.Error ("README Owners", "README_dummy_3.md", "@dummy", "User exists but is not a member of with this project: Dummy User"), - new Problem.Error ("README Owners", "README_dummy_3.md", "", "NO Valid Approvers for rule"), - new Problem.Error ("README Owners", "README_nonsharedgroup_1.md", "@opensource", "Group exists and is not shared with this project"), - new Problem.Error ("README Owners", "README_nonsharedgroup_1.md", "", "NO Valid Approvers for rule"), - new Problem.Error ("README Owners", "README_nonsharedgroup_2.md", "@opensource", "Group exists and is not shared with this project"), - new Problem.Error ("README Owners", "README_nonsharedgroup_2.md", "", "NO Valid Approvers for rule"), - new Problem.Error ("README Owners", "README_nonsharedgroup_3.md", "@opensource", "Group exists and is not shared with this project"), - new Problem.Error ("README Owners", "README_nonsharedgroup_3.md", "", "NO Valid Approvers for rule"), - new Problem.Warning ("README Owners", "README_sharedgroup_guests_1.md", "@codeowners/guests", "Shared group does not have sufficient permissions to approve: AccessLevel=10 (=GUEST)"), - new Problem.Error ("README Owners", "README_sharedgroup_guests_1.md", "", "NO Valid Approvers for rule"), - new Problem.Warning ("README Owners", "README_sharedgroup_guests_2.md", "@codeowners/guests", "Shared group does not have sufficient permissions to approve: AccessLevel=10 (=GUEST)"), - new Problem.Error ("README Owners", "README_sharedgroup_guests_2.md", "", "NO Valid Approvers for rule"), - new Problem.Warning ("README Owners", "README_sharedgroup_guests_3.md", "@codeowners/guests", "Shared group does not have sufficient permissions to approve: AccessLevel=10 (=GUEST)"), - new Problem.Error ("README Owners", "README_sharedgroup_guests_3.md", "", "NO Valid Approvers for rule"), - new Problem.Warning ("README Owners", "README_nosuchgroup_1.md", "@codeowners/nosuchgroup", "Approver does not exist in Gitlab (not as user and not as group)"), - new Problem.Error ("README Owners", "README_nosuchgroup_1.md", "", "NO Valid Approvers for rule"), - new Problem.Warning ("README Owners", "README_nosuchgroup_2.md", "@codeowners/nosuchgroup", "Approver does not exist in Gitlab (not as user and not as group)"), - new Problem.Error ("README Owners", "README_nosuchgroup_2.md", "", "NO Valid Approvers for rule"), - new Problem.Warning ("README Owners", "README_nosuchgroup_3.md", "@codeowners/nosuchgroup", "Approver does not exist in Gitlab (not as user and not as group)"), - new Problem.Error ("README Owners", "README_nosuchgroup_3.md", "", "NO Valid Approvers for rule") - ); - assertNotNull(exception, "There should be an exception because we have a Fatal Error and the FailLevel is ERROR"); - } - - @Test - @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels/project") - @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "gltst-validtoken") - public void testOwnerHandling(WireMockRuntimeInfo wmRuntimeInfo, TestInfo testInfo) throws CodeOwnersValidationException { - GitlabConfiguration configuration = makeConfig(wmRuntimeInfo, null, "FETCH_USER_ACCESS_TOKEN"); - configuration.setFailLevel(FATAL); - - CodeOwnersValidationException exception = runCodeownersValidation(testInfo, configuration, - new CodeOwners( - "[README Owners]\n" + - "README_niels.md @niels\n" + // niels --> is member - "README_dummy.md @dummy\n" + // dummy --> is NOT member --> Error - "README_niels_public_email.md public@example.nl\n" + // niels --> is member - "README_niels_private_email.md private@example.nl\n" + // niels --> is member --> Cannot find --> Warning only - "README_dummy_public_email.md public@dummy.example.nl\n" + // dummy --> is NOT member --> Error - "README_dummy_private_email.md private@dummy.example.nl\n" // niels --> is NOT member --> Cannot find --> Warning only - ), - new Problem.Info ("README Owners", "README_niels.md", "@niels", "Valid approver: Member with username \"niels\" can approve (AccessLevel:50=OWNER)"), - new Problem.Error ("README Owners", "README_dummy.md", "@dummy", "User exists but is not a member of with this project: Dummy User"), - new Problem.Error ("README Owners", "README_dummy.md", "", "NO Valid Approvers for rule"), - new Problem.Info ("README Owners", "README_niels_public_email.md", "public@example.nl", "Valid approver: Member with username \"niels\" can approve (AccessLevel:50=OWNER)"), - new Problem.Warning ("README Owners", "README_niels_private_email.md", "private@example.nl", "Unable to verify email address: Assuming the user exists and can approve"), - new Problem.Error ("README Owners", "README_dummy_public_email.md", "public@dummy.example.nl", "User exists but is not a member of with this project: Dummy User"), - new Problem.Error ("README Owners", "README_dummy_public_email.md", "", "NO Valid Approvers for rule"), - new Problem.Warning ("README Owners", "README_dummy_private_email.md", "private@dummy.example.nl", "Unable to verify email address: Assuming the user exists and can approve") - ); - if (exception != null) { - throw exception; - } - } - - @Test - @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels/project") - @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "gltst-validtoken") - public void testShowMixedErrorReport(WireMockRuntimeInfo wmRuntimeInfo, TestInfo testInfo) throws CodeOwnersValidationException { - GitlabConfiguration configuration = new GitlabConfiguration( - new ServerUrl(wmRuntimeInfo.getHttpBaseUrl(), null), - new ProjectId(null, null), - new AccessToken("FETCH_USER_ACCESS_TOKEN") - ); - configuration.setShowAllApprovers(true); - CodeOwnersValidationException exception = runCodeownersValidation(testInfo, configuration, - true, - new CodeOwners( - "[README Owners]\n" + - "README_niels.md @niels\n" + // INFO: niels --> is member - "README_niels_private_email.md private@example.nl private@dummy.example.nl\n" + // WARNING: niels --> is member --> Cannot find --> Warning only - "README_dummy.md @dummy\n" + // ERROR: dummy --> is NOT member --> Error - "README_badrole.md @@badrole\n" // ERROR: dummy --> is NOT member --> Error - ), - new Problem.Info ("README Owners", "README_niels.md", "@niels", "Valid approver: Member with username \"niels\" can approve (AccessLevel:50=OWNER)"), - new Problem.Warning ("README Owners", "README_niels_private_email.md", "private@example.nl", "Unable to verify email address: Assuming the user exists and can approve"), - new Problem.Warning ("README Owners", "README_niels_private_email.md", "private@dummy.example.nl", "Unable to verify email address: Assuming the user exists and can approve"), - new Problem.Error ("README Owners", "README_dummy.md", "@dummy", "User exists but is not a member of with this project: Dummy User"), - new Problem.Error ("README Owners", "README_dummy.md", "", "NO Valid Approvers for rule"), - new Problem.Fatal ("README Owners", "README_badrole.md", "@@badrole", "Illegal role was specified") - ); - assertNotNull(exception); - } - - @Test - @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels/project") - @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "gltst-validtoken") - public void testFailLevelNever(WireMockRuntimeInfo wmRuntimeInfo, TestInfo testInfo) throws CodeOwnersValidationException { - GitlabConfiguration configuration = new GitlabConfiguration( - new ServerUrl(wmRuntimeInfo.getHttpBaseUrl(), null), - new ProjectId(null, null), - new AccessToken("FETCH_USER_ACCESS_TOKEN") - ); - runFailLevelTests(testInfo, configuration, NEVER); - } - - @Test - @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels/project") - @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "gltst-validtoken") - public void testFailLevelFatal(WireMockRuntimeInfo wmRuntimeInfo, TestInfo testInfo) throws CodeOwnersValidationException { - GitlabConfiguration configuration = new GitlabConfiguration( - new ServerUrl(wmRuntimeInfo.getHttpBaseUrl(), null), - new ProjectId(null, null), - new AccessToken("FETCH_USER_ACCESS_TOKEN") - ); - runFailLevelTests(testInfo, configuration, FATAL); - } - - @Test - @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels/project") - @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "gltst-validtoken") - public void testFailLevelError(WireMockRuntimeInfo wmRuntimeInfo, TestInfo testInfo) throws CodeOwnersValidationException { - GitlabConfiguration configuration = new GitlabConfiguration( - new ServerUrl(wmRuntimeInfo.getHttpBaseUrl(), null), - new ProjectId(null, null), - new AccessToken("FETCH_USER_ACCESS_TOKEN") - ); - runFailLevelTests(testInfo, configuration, ERROR); - } - - @Test - @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels/project") - @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "gltst-validtoken") - public void testFailLevelWarning(WireMockRuntimeInfo wmRuntimeInfo, TestInfo testInfo) throws CodeOwnersValidationException { - GitlabConfiguration configuration = new GitlabConfiguration( - new ServerUrl(wmRuntimeInfo.getHttpBaseUrl(), null), - new ProjectId(null, null), - new AccessToken("FETCH_USER_ACCESS_TOKEN") - ); - configuration.setFailLevel(WARNING); - - CodeOwnersValidationException exception; - exception = runFailLevelTestInfo(testInfo, configuration); - assertNull(exception, "No exception should have been thrown"); - exception = runFailLevelTestWarning(testInfo, configuration); - assertNotNull(exception, "An exception should have been thrown"); - exception = runFailLevelTestError(testInfo, configuration); - assertNotNull(exception, "An exception should have been thrown"); - exception = runFailLevelTestFatal(testInfo, configuration); - assertNotNull(exception, "An exception should have been thrown"); - } - - private void runFailLevelTests(TestInfo testInfo, - GitlabConfiguration configuration, - FailLevel failLevel - ) throws CodeOwnersValidationException { - - boolean infoShouldFail = false; - boolean warningShouldFail = false; - boolean errorShouldFail = false; - boolean fatalShouldFail = false; - switch (failLevel) { - case NEVER: - break; - case FATAL: - fatalShouldFail = true; - break; - case ERROR: - errorShouldFail = true; - fatalShouldFail = true; - break; - case WARNING: - warningShouldFail = true; - errorShouldFail = true; - fatalShouldFail = true; - break; - } - configuration.setFailLevel(failLevel); - CodeOwnersValidationException exception; - exception = runFailLevelTestInfo(testInfo, configuration); - assertFail(infoShouldFail, exception); - exception = runFailLevelTestWarning(testInfo, configuration); - assertFail(warningShouldFail, exception); - exception = runFailLevelTestError(testInfo, configuration); - assertFail(errorShouldFail, exception); - exception = runFailLevelTestFatal(testInfo, configuration); - assertFail(fatalShouldFail, exception); - } - - private void assertFail(boolean shouldFail, CodeOwnersValidationException exception) { - if (shouldFail) { - assertNotNull(exception, "An exception should have been thrown"); - } else { - assertNull(exception, "No exception should have been thrown"); - } - } - - private CodeOwnersValidationException runFailLevelTestFatal(TestInfo testInfo, GitlabConfiguration configuration) { - return runCodeownersValidation(testInfo, configuration, - new CodeOwners( - "[README Owners]\n" + - "README_niels.md @niels\n" + // INFO: niels --> is member - "README_niels_private_email.md private@example.nl\n" + // WARNING: niels --> is member --> Cannot find --> Warning only - "README_dummy.md @dummy\n" + // ERROR: dummy --> is NOT member --> Error - "README_badrole.md @@badrole\n" // ERROR: dummy --> is NOT member --> Error - ), - new Problem.Info ("README Owners", "README_niels.md", "@niels", "Valid approver: Member with username \"niels\" can approve (AccessLevel:50=OWNER)"), - new Problem.Warning ("README Owners", "README_niels_private_email.md", "private@example.nl", "Unable to verify email address: Assuming the user exists and can approve"), - new Problem.Error ("README Owners", "README_dummy.md", "@dummy", "User exists but is not a member of with this project: Dummy User"), - new Problem.Error ("README Owners", "README_dummy.md", "", "NO Valid Approvers for rule"), - new Problem.Fatal ("README Owners", "README_badrole.md", "@@badrole", "Illegal role was specified") - ); - } - - private CodeOwnersValidationException runFailLevelTestError(TestInfo testInfo, GitlabConfiguration configuration) { - return runCodeownersValidation(testInfo, configuration, - new CodeOwners( - "[README Owners]\n" + - "README_niels.md @niels\n" + // INFO: niels --> is member - "README_niels_private_email.md private@example.nl\n" + // WARNING: niels --> is member --> Cannot find --> Warning only - "README_dummy.md @dummy\n" // ERROR: dummy --> is NOT member --> Error - ), - new Problem.Info ("README Owners", "README_niels.md", "@niels", "Valid approver: Member with username \"niels\" can approve (AccessLevel:50=OWNER)"), - new Problem.Warning ("README Owners", "README_niels_private_email.md", "private@example.nl", "Unable to verify email address: Assuming the user exists and can approve"), - new Problem.Error ("README Owners", "README_dummy.md", "@dummy", "User exists but is not a member of with this project: Dummy User"), - new Problem.Error ("README Owners", "README_dummy.md", "", "NO Valid Approvers for rule") - ); - } - - private CodeOwnersValidationException runFailLevelTestWarning(TestInfo testInfo, GitlabConfiguration configuration) { - return runCodeownersValidation(testInfo, configuration, - new CodeOwners( - "[README Owners]\n" + - "README_niels.md @niels\n" + // INFO: niels --> is member - "README_niels_private_email.md private@example.nl\n" // WARNING: niels --> is member --> Cannot find --> Warning only - ), - new Problem.Warning("README Owners", "README_niels_private_email.md", "private@example.nl","Unable to verify email address: Assuming the user exists and can approve") - ); - } - - private CodeOwnersValidationException runFailLevelTestInfo(TestInfo testInfo, GitlabConfiguration configuration) { - return runCodeownersValidation(testInfo, configuration, - new CodeOwners( - "[README Owners]\n" + - "README_niels.md @niels\n" // INFO: niels --> is member - ), - new Problem.Info("README Owners", "README_niels.md", "@niels", "Valid approver: Member with username \"niels\" can approve (AccessLevel:50=OWNER)") - ); - } - - private CodeOwnersValidationException runCodeownersValidation(TestInfo testInfo, GitlabConfiguration configuration, CodeOwners codeOwners, Problem...expectedProblems) { - return runCodeownersValidation(testInfo, configuration, false, codeOwners, expectedProblems); - } - - private CodeOwnersValidationException runCodeownersValidation(TestInfo testInfo, GitlabConfiguration configuration, boolean logResult, CodeOwners codeOwners, Problem...expectedProblems) { - try(GitlabProjectMembers gitlabProjectMembers = new GitlabProjectMembers(configuration)) { - Logger logger = LoggerFactory.getLogger(testInfo.getTestMethod().map(Method::getName).orElse("runCodeownersValidation")); - gitlabProjectMembers.setShowAllApprovers(true); - CodeOwnersValidationException exception = null; - ProblemTable problemTable = null; - try { - problemTable = gitlabProjectMembers.verifyAllCodeowners(logger, codeOwners); - gitlabProjectMembers.failIfExceededFailLevel(problemTable); - } catch (CodeOwnersValidationException e) { - exception = e; - } - - assertNotNull(problemTable); - for (Problem expectedProblem : expectedProblems) { - assertTrue(problemTable.contains(expectedProblem), "The problem table \n" + problemTable.getProblems().stream().map(Problem::toString).collect(Collectors.joining("\n")) + "\n should contain \n" + expectedProblem); - } - if (logResult) { - logger.info(problemTable.toString()); - logger.info(problemTable.toProblemMessageGroupedString()); - } - return exception; - } - } -} diff --git a/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/gitlab/TestGitlabUsers.kt b/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/gitlab/TestGitlabUsers.kt new file mode 100644 index 0000000..104d4cd --- /dev/null +++ b/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/gitlab/TestGitlabUsers.kt @@ -0,0 +1,617 @@ +/* + * CodeOwners Tools + * Copyright (C) 2023-2026 Niels Basjes + * + * 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 + * + * https://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 nl.basjes.codeowners.validator.gitlab + +import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo +import com.github.tomakehurst.wiremock.junit5.WireMockTest +import nl.basjes.codeowners.CodeOwners +import nl.basjes.codeowners.validator.CodeOwnersValidationException +import nl.basjes.codeowners.validator.gitlab.GitlabConfiguration.AccessToken +import nl.basjes.codeowners.validator.gitlab.GitlabConfiguration.ProjectId +import nl.basjes.codeowners.validator.gitlab.GitlabConfiguration.ServerUrl +import nl.basjes.codeowners.validator.utils.Problem +import nl.basjes.codeowners.validator.utils.ProblemTable +import org.junit.jupiter.api.TestInfo +import org.junitpioneer.jupiter.SetEnvironmentVariable +import org.slf4j.LoggerFactory +import java.lang.reflect.Method +import java.util.function.Function +import kotlin.test.Test +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +@WireMockTest +class TestGitlabUsers { + @Test + @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels/project") + @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "gltst-validtoken") + @Throws(CodeOwnersValidationException::class) + fun testValidCodeOwners(wmRuntimeInfo: WireMockRuntimeInfo?) { + val configuration: GitlabConfiguration = makeConfig(wmRuntimeInfo, null, "FETCH_USER_ACCESS_TOKEN") + + GitlabProjectMembers(configuration).use { gitlabProjectMembers -> + val codeOwners = CodeOwners( + """ + [README Owners] + README.md @niels @SomeUser @isguest + README_owner.md @@owner + README_owners.md @@owners + README_maintainer.md @@maintainer + README_maintainers.md @@maintainers + README_developer.md @@developer + README_developers.md @@developers + README_group_dev.md @codeowners/developers + README_group_main.md @codeowners/maintainers + README_user_public_email.md public@example.nl + """.trimIndent() + ) + val logger = LoggerFactory.getLogger("testValidCodeOwners") + gitlabProjectMembers.showAllApprovers = true + logger.info(gitlabProjectMembers.verifyAllCodeowners(logger, codeOwners).toString()) + logger.info(gitlabProjectMembers.verifyAllCodeowners(logger, codeOwners).toProblemMessageGroupedString()) + gitlabProjectMembers.showAllApprovers = false + logger.info(gitlabProjectMembers.verifyAllCodeowners(logger, codeOwners).toString()) + logger.info(gitlabProjectMembers.verifyAllCodeowners(logger, codeOwners).toProblemMessageGroupedString()) + } + } + + @Test + @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels/project") + @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "gltst-validtoken") + @Throws(CodeOwnersValidationException::class) + fun testRoles(wmRuntimeInfo: WireMockRuntimeInfo?) { + val configuration: GitlabConfiguration = makeConfig(wmRuntimeInfo, null, "FETCH_USER_ACCESS_TOKEN") + + GitlabProjectMembers(configuration).use { gitlabProjectMembers -> + val codeOwners = CodeOwners( + """ + [README Owners] + README_niels.md @niels + README_owner.md @@owner + README_owners.md @@owners + README_maintainer.md @@maintainer + README_maintainers.md @@maintainers + README_developer.md @@developer + README_developers.md @@developers + """.trimIndent() + ) + val logger = LoggerFactory.getLogger("testRoles") + gitlabProjectMembers.showAllApprovers = true + gitlabProjectMembers.verifyAllCodeowners(logger, codeOwners) + gitlabProjectMembers.showAllApprovers = false + gitlabProjectMembers.verifyAllCodeowners(logger, codeOwners) + } + } + + @Test + @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels/project") + @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "gltst-validtoken") + @Throws(CodeOwnersValidationException::class) + fun testEmpty(wmRuntimeInfo: WireMockRuntimeInfo?, testInfo: TestInfo) { + val configuration: GitlabConfiguration = makeConfig(wmRuntimeInfo, null, "FETCH_USER_ACCESS_TOKEN") + configuration.failLevel = GitlabConfiguration.FailLevel.WARNING + + val exception = runCodeownersValidation( + testInfo, configuration, + CodeOwners( + """ + [README Owners] + """.trimIndent() + ) + ) + if (exception != null) { + throw exception + } + } + + @Test + @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels/project") + @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "gltst-validtoken") + @Throws(CodeOwnersValidationException::class) + fun testSharedGroups(wmRuntimeInfo: WireMockRuntimeInfo?, testInfo: TestInfo) { + val configuration: GitlabConfiguration = makeConfig(wmRuntimeInfo, null, "FETCH_USER_ACCESS_TOKEN") + configuration.failLevel = GitlabConfiguration.FailLevel.FATAL + + val exception = runCodeownersValidation( + testInfo, configuration, + CodeOwners( + """ + [README Owners] + README_shared_group_guests.md @codeowners/guests + README_shared_group_developers.md @codeowners/developers + README_shared_group_maintainers.md @codeowners/maintainers + """.trimIndent() + ), + Problem.Warning( + "README Owners", + "README_shared_group_guests.md", + "@codeowners/guests", + "Shared group does not have sufficient permissions to approve: AccessLevel=10 (=GUEST)" + ), + Problem.Error("README Owners", "README_shared_group_guests.md", "", "NO Valid Approvers for rule") + ) + if (exception != null) { + throw exception + } + } + + @Test + @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels/project") + @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "gltst-validtoken") + @Throws(CodeOwnersValidationException::class) + fun testUnknownEmailAssumptions(wmRuntimeInfo: WireMockRuntimeInfo?, testInfo: TestInfo) { + val configuration: GitlabConfiguration = makeConfig(wmRuntimeInfo, null, "FETCH_USER_ACCESS_TOKEN") + + val codeOwners = CodeOwners( + """ + [README Owners] + README_niels_private_email.md private@example.nl + """.trimIndent() + ) + + configuration.assumeUncheckableEmailExistsAndCanApprove = true + assertNull( + runCodeownersValidation( + testInfo, configuration, codeOwners, + Problem.Warning( + "README Owners", + "README_niels_private_email.md", + "private@example.nl", + "Unable to verify email address: Assuming the user exists and can approve" + ) + ) + ) + + configuration.assumeUncheckableEmailExistsAndCanApprove = false + configuration.problemLevels.userUnknownEmail = GitlabConfiguration.Level.ERROR + configuration.problemLevels.noValidApprovers = GitlabConfiguration.Level.FATAL + assertNotNull( + runCodeownersValidation( + testInfo, configuration, codeOwners, + Problem.Error( + "README Owners", + "README_niels_private_email.md", + "private@example.nl", + "Unable to verify email address: Assuming the user does not exist and/or cannot approve" + ), + Problem.Fatal("README Owners", "README_niels_private_email.md", "", "NO Valid Approvers for rule") + ) + ) + } + + + @Test + @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "nomembers") + @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "gltst-validtoken") + @Throws(CodeOwnersValidationException::class) + fun testBadRoles(wmRuntimeInfo: WireMockRuntimeInfo?, testInfo: TestInfo) { + val configuration: GitlabConfiguration = makeConfig(wmRuntimeInfo, null, "FETCH_USER_ACCESS_TOKEN") + + val exception = runCodeownersValidation( + testInfo, configuration, + CodeOwners( + """ + [README Owners] + README_1.md @niels @dummy + README_2.md @niels @dummy + README_owner.md @@owner + README_owners.md @@owners + README_maintainer.md @@maintainer + README_maintainers.md @@maintainers + README_developer.md @@developer + README_developers.md @@developers + README_nosuchrole.md @@nosuchrole + README_disabled_1.md @disabledowner @disabledmaintainer @disableddeveloper + README_locked_1.md @lockedowner @lockedmaintainer @lockeddeveloper + README_disabled_2.md @disabledowner @disabledmaintainer @disableddeveloper + README_locked_2.md @lockedowner @lockedmaintainer @lockeddeveloper + README_dummy_1.md @dummy + README_dummy_2.md @dummy + README_dummy_3.md @dummy + README_nonsharedgroup_1.md @opensource + README_nonsharedgroup_2.md @opensource + README_nonsharedgroup_3.md @opensource + README_sharedgroup_guests_1.md @codeowners/guests + README_sharedgroup_guests_2.md @codeowners/guests + README_sharedgroup_guests_3.md @codeowners/guests + README_nosuchgroup_1.md @codeowners/nosuchgroup + README_nosuchgroup_2.md @codeowners/nosuchgroup + README_nosuchgroup_3.md @codeowners/nosuchgroup + """.trimIndent() + ), + + Problem.Error( "README Owners", "README_1.md", "@niels", "User exists but is not a member of with this project: Niels Basjes" ), + Problem.Error( "README Owners", "README_1.md", "@dummy", "User exists but is not a member of with this project: Dummy User" ), + Problem.Error( "README Owners", "README_1.md", "", "NO Valid Approvers for rule" ), + Problem.Error( "README Owners", "README_2.md", "@niels", "User exists but is not a member of with this project: Niels Basjes" ), + Problem.Error( "README Owners", "README_2.md", "@dummy", "User exists but is not a member of with this project: Dummy User" ), + Problem.Error( "README Owners", "README_2.md", "", "NO Valid Approvers for rule" ), + Problem.Warning( "README Owners", "README_owner.md", "@@owner", "No direct project members have the \"owner\" role" ), + Problem.Error( "README Owners", "README_owner.md", "", "NO Valid Approvers for rule" ), + Problem.Warning( "README Owners", "README_owners.md", "@@owners", "No direct project members have the \"owner\" role" ), + Problem.Error( "README Owners", "README_owners.md", "", "NO Valid Approvers for rule" ), + Problem.Warning( "README Owners", "README_maintainer.md", "@@maintainer", "No direct project members have the \"maintainer\" role" ), + Problem.Error( "README Owners", "README_maintainer.md", "", "NO Valid Approvers for rule" ), + Problem.Warning( "README Owners", "README_maintainers.md", "@@maintainers", "No direct project members have the \"maintainer\" role" ), + Problem.Error( "README Owners", "README_maintainers.md", "", "NO Valid Approvers for rule" ), + Problem.Warning( "README Owners", "README_developer.md", "@@developer", "No direct project members have the \"developer\" role" ), + Problem.Error( "README Owners", "README_developer.md", "", "NO Valid Approvers for rule" ), + Problem.Warning( "README Owners", "README_developers.md", "@@developers", "No direct project members have the \"developer\" role" ), + Problem.Error( "README Owners", "README_developers.md", "", "NO Valid Approvers for rule" ), + Problem.Fatal( "README Owners", "README_nosuchrole.md", "@@nosuchrole", "Illegal role was specified" ), + Problem.Error( "README Owners", "README_nosuchrole.md", "", "NO Valid Approvers for rule" ), + Problem.Warning( "README Owners", "README_disabled_1.md", "@disabledowner", "Disabled account: State=disabled; Locked=false" ), + Problem.Warning( "README Owners", "README_disabled_1.md", "@disabledmaintainer", "Disabled account: State=disabled; Locked=false" ), + Problem.Warning( "README Owners", "README_disabled_1.md", "@disableddeveloper", "Disabled account: State=disabled; Locked=false" ), + Problem.Error( "README Owners", "README_disabled_1.md", "", "NO Valid Approvers for rule" ), + Problem.Warning( "README Owners", "README_locked_1.md", "@lockedowner", "Disabled account: State=active; Locked=true" ), + Problem.Warning( "README Owners", "README_locked_1.md", "@lockedmaintainer", "Disabled account: State=active; Locked=true" ), + Problem.Warning( "README Owners", "README_locked_1.md", "@lockeddeveloper", "Disabled account: State=active; Locked=true" ), + Problem.Error( "README Owners", "README_locked_1.md", "", "NO Valid Approvers for rule" ), + Problem.Warning( "README Owners", "README_disabled_2.md", "@disabledowner", "Disabled account: State=disabled; Locked=false" ), + Problem.Warning( "README Owners", "README_disabled_2.md", "@disabledmaintainer", "Disabled account: State=disabled; Locked=false" ), + Problem.Warning( "README Owners", "README_disabled_2.md", "@disableddeveloper", "Disabled account: State=disabled; Locked=false" ), + Problem.Error( "README Owners", "README_disabled_2.md", "", "NO Valid Approvers for rule" ), + Problem.Warning( "README Owners", "README_locked_2.md", "@lockedowner", "Disabled account: State=active; Locked=true" ), + Problem.Warning( "README Owners", "README_locked_2.md", "@lockedmaintainer", "Disabled account: State=active; Locked=true" ), + Problem.Warning( "README Owners", "README_locked_2.md", "@lockeddeveloper", "Disabled account: State=active; Locked=true" ), + Problem.Error( "README Owners", "README_locked_2.md", "", "NO Valid Approvers for rule" ), + Problem.Error( "README Owners", "README_dummy_1.md", "@dummy", "User exists but is not a member of with this project: Dummy User" ), + Problem.Error( "README Owners", "README_dummy_1.md", "", "NO Valid Approvers for rule" ), + Problem.Error( "README Owners", "README_dummy_2.md", "@dummy", "User exists but is not a member of with this project: Dummy User" ), + Problem.Error( "README Owners", "README_dummy_2.md", "", "NO Valid Approvers for rule" ), + Problem.Error( "README Owners", "README_dummy_3.md", "@dummy", "User exists but is not a member of with this project: Dummy User" ), + Problem.Error( "README Owners", "README_dummy_3.md", "", "NO Valid Approvers for rule" ), + Problem.Error( "README Owners", "README_nonsharedgroup_1.md", "@opensource", "Group exists and is not shared with this project" ), + Problem.Error( "README Owners", "README_nonsharedgroup_1.md", "", "NO Valid Approvers for rule" ), + Problem.Error( "README Owners", "README_nonsharedgroup_2.md", "@opensource", "Group exists and is not shared with this project" ), + Problem.Error( "README Owners", "README_nonsharedgroup_2.md", "", "NO Valid Approvers for rule" ), + Problem.Error( "README Owners", "README_nonsharedgroup_3.md", "@opensource", "Group exists and is not shared with this project" ), + Problem.Error( "README Owners", "README_nonsharedgroup_3.md", "", "NO Valid Approvers for rule" ), + Problem.Warning( "README Owners", "README_sharedgroup_guests_1.md", "@codeowners/guests", "Shared group does not have sufficient permissions to approve: AccessLevel=10 (=GUEST)" ), + Problem.Error( "README Owners", "README_sharedgroup_guests_1.md", "", "NO Valid Approvers for rule" ), + Problem.Warning( "README Owners", "README_sharedgroup_guests_2.md", "@codeowners/guests", "Shared group does not have sufficient permissions to approve: AccessLevel=10 (=GUEST)" ), + Problem.Error( "README Owners", "README_sharedgroup_guests_2.md", "", "NO Valid Approvers for rule" ), + Problem.Warning( "README Owners", "README_sharedgroup_guests_3.md", "@codeowners/guests", "Shared group does not have sufficient permissions to approve: AccessLevel=10 (=GUEST)" ), + Problem.Error( "README Owners", "README_sharedgroup_guests_3.md", "", "NO Valid Approvers for rule" ), + Problem.Warning( "README Owners", "README_nosuchgroup_1.md", "@codeowners/nosuchgroup", "Approver does not exist in Gitlab (not as user and not as group)" ), + Problem.Error( "README Owners", "README_nosuchgroup_1.md", "", "NO Valid Approvers for rule" ), + Problem.Warning( "README Owners", "README_nosuchgroup_2.md", "@codeowners/nosuchgroup", "Approver does not exist in Gitlab (not as user and not as group)" ), + Problem.Error( "README Owners", "README_nosuchgroup_2.md", "", "NO Valid Approvers for rule" ), + Problem.Warning( "README Owners", "README_nosuchgroup_3.md", "@codeowners/nosuchgroup", "Approver does not exist in Gitlab (not as user and not as group)" ), + Problem.Error( "README Owners", "README_nosuchgroup_3.md", "", "NO Valid Approvers for rule" ), + ) + + assertNotNull( + exception, + "There should be an exception because we have a Fatal Error and the FailLevel is ERROR" + ) + } + + @Test + @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels/project") + @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "gltst-validtoken") + @Throws(CodeOwnersValidationException::class) + fun testOwnerHandling(wmRuntimeInfo: WireMockRuntimeInfo?, testInfo: TestInfo) { + val configuration: GitlabConfiguration = makeConfig(wmRuntimeInfo, null, "FETCH_USER_ACCESS_TOKEN") + configuration.failLevel = GitlabConfiguration.FailLevel.FATAL + + val exception = runCodeownersValidation( + testInfo, configuration, + CodeOwners( + """ + [README Owners] + README_niels.md @niels + README_dummy.md @dummy + README_niels_public_email.md public@example.nl + README_niels_private_email.md private@example.nl + README_dummy_public_email.md public@dummy.example.nl + README_dummy_private_email.md private@dummy.example.nl + """.trimIndent() + ), + + Problem.Info( "README Owners", "README_niels.md", "@niels", "Valid approver: Member with username \"niels\" can approve (AccessLevel:50=OWNER)" ), + Problem.Error( "README Owners", "README_dummy.md", "@dummy", "User exists but is not a member of with this project: Dummy User" ), + Problem.Error( "README Owners", "README_dummy.md", "", "NO Valid Approvers for rule" ), + Problem.Info( "README Owners", "README_niels_public_email.md", "public@example.nl", "Valid approver: Member with username \"niels\" can approve (AccessLevel:50=OWNER)" ), + Problem.Warning( "README Owners", "README_niels_private_email.md", "private@example.nl", "Unable to verify email address: Assuming the user exists and can approve" ), + Problem.Error( "README Owners", "README_dummy_public_email.md", "public@dummy.example.nl", "User exists but is not a member of with this project: Dummy User" ), + Problem.Error( "README Owners", "README_dummy_public_email.md", "", "NO Valid Approvers for rule" ), + Problem.Warning( "README Owners", "README_dummy_private_email.md", "private@dummy.example.nl", "Unable to verify email address: Assuming the user exists and can approve" ), + ) + if (exception != null) { + throw exception + } + } + + @Test + @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels/project") + @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "gltst-validtoken") + @Throws(CodeOwnersValidationException::class) + fun testShowMixedErrorReport(wmRuntimeInfo: WireMockRuntimeInfo, testInfo: TestInfo) { + val configuration = GitlabConfiguration( + ServerUrl(wmRuntimeInfo.httpBaseUrl, null), + ProjectId(null, null), + AccessToken("FETCH_USER_ACCESS_TOKEN") + ) + configuration.showAllApprovers = true + val exception = runCodeownersValidation( + testInfo, configuration, + true, + CodeOwners( + """ + [README Owners] + # INFO: niels --> is member + README_niels.md @niels + # WARNING: niels --> is member --> Cannot find --> Warning only + README_niels_private_email.md private@example.nl private@dummy.example.nl + # ERROR: dummy --> is NOT member --> Error + README_dummy.md @dummy + # ERROR: dummy --> is NOT member --> Error + README_badrole.md @@badrole + """.trimIndent() + ), + Problem.Info( "README Owners", "README_niels.md", "@niels", "Valid approver: Member with username \"niels\" can approve (AccessLevel:50=OWNER)" ), + Problem.Warning( "README Owners", "README_niels_private_email.md", "private@example.nl", "Unable to verify email address: Assuming the user exists and can approve" ), + Problem.Warning( "README Owners", "README_niels_private_email.md", "private@dummy.example.nl", "Unable to verify email address: Assuming the user exists and can approve" ), + Problem.Error( "README Owners", "README_dummy.md", "@dummy", "User exists but is not a member of with this project: Dummy User" ), + Problem.Error( "README Owners", "README_dummy.md", "", "NO Valid Approvers for rule" ), + Problem.Fatal( "README Owners", "README_badrole.md", "@@badrole", "Illegal role was specified" ), + ) + assertNotNull(exception) + } + + @Test + @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels/project") + @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "gltst-validtoken") + @Throws(CodeOwnersValidationException::class) + fun testFailLevelNever(wmRuntimeInfo: WireMockRuntimeInfo, testInfo: TestInfo) { + val configuration = GitlabConfiguration( + ServerUrl(wmRuntimeInfo.httpBaseUrl, null), + ProjectId(null, null), + AccessToken("FETCH_USER_ACCESS_TOKEN") + ) + runFailLevelTests(testInfo, configuration, GitlabConfiguration.FailLevel.NEVER) + } + + @Test + @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels/project") + @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "gltst-validtoken") + @Throws(CodeOwnersValidationException::class) + fun testFailLevelFatal(wmRuntimeInfo: WireMockRuntimeInfo, testInfo: TestInfo) { + val configuration = GitlabConfiguration( + ServerUrl(wmRuntimeInfo.httpBaseUrl, null), + ProjectId(null, null), + AccessToken("FETCH_USER_ACCESS_TOKEN") + ) + runFailLevelTests(testInfo, configuration, GitlabConfiguration.FailLevel.FATAL) + } + + @Test + @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels/project") + @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "gltst-validtoken") + @Throws(CodeOwnersValidationException::class) + fun testFailLevelError(wmRuntimeInfo: WireMockRuntimeInfo, testInfo: TestInfo) { + val configuration = GitlabConfiguration( + ServerUrl(wmRuntimeInfo.httpBaseUrl, null), + ProjectId(null, null), + AccessToken("FETCH_USER_ACCESS_TOKEN") + ) + runFailLevelTests(testInfo, configuration, GitlabConfiguration.FailLevel.ERROR) + } + + @Test + @SetEnvironmentVariable(key = "CI_PROJECT_ID", value = "niels/project") + @SetEnvironmentVariable(key = "FETCH_USER_ACCESS_TOKEN", value = "gltst-validtoken") + @Throws(CodeOwnersValidationException::class) + fun testFailLevelWarning(wmRuntimeInfo: WireMockRuntimeInfo, testInfo: TestInfo) { + val configuration = GitlabConfiguration( + ServerUrl(wmRuntimeInfo.httpBaseUrl, null), + ProjectId(null, null), + AccessToken("FETCH_USER_ACCESS_TOKEN") + ) + configuration.failLevel = GitlabConfiguration.FailLevel.WARNING + + assertNull( runFailLevelTestInfo(testInfo, configuration), "No exception should have been thrown") + assertNotNull( runFailLevelTestWarning(testInfo, configuration), "An exception should have been thrown") + assertNotNull( runFailLevelTestError(testInfo, configuration), "An exception should have been thrown") + assertNotNull( runFailLevelTestFatal(testInfo, configuration), "An exception should have been thrown") + } + + @Throws(CodeOwnersValidationException::class) + private fun runFailLevelTests( + testInfo: TestInfo, + configuration: GitlabConfiguration, + failLevel: GitlabConfiguration.FailLevel + ) { + val infoShouldFail = false + var warningShouldFail = false + var errorShouldFail = false + var fatalShouldFail = false + when (failLevel) { + GitlabConfiguration.FailLevel.NEVER -> {} + GitlabConfiguration.FailLevel.FATAL -> fatalShouldFail = true + GitlabConfiguration.FailLevel.ERROR -> { + errorShouldFail = true + fatalShouldFail = true + } + + GitlabConfiguration.FailLevel.WARNING -> { + warningShouldFail = true + errorShouldFail = true + fatalShouldFail = true + } + } + configuration.failLevel = failLevel + var exception: CodeOwnersValidationException? + exception = runFailLevelTestInfo(testInfo, configuration) + assertFail(infoShouldFail, exception) + exception = runFailLevelTestWarning(testInfo, configuration) + assertFail(warningShouldFail, exception) + exception = runFailLevelTestError(testInfo, configuration) + assertFail(errorShouldFail, exception) + exception = runFailLevelTestFatal(testInfo, configuration) + assertFail(fatalShouldFail, exception) + } + + private fun assertFail(shouldFail: Boolean, exception: CodeOwnersValidationException?) { + if (shouldFail) { + assertNotNull(exception, "An exception should have been thrown") + } else { + assertNull(exception, "No exception should have been thrown") + } + } + + private fun runFailLevelTestFatal( + testInfo: TestInfo, + configuration: GitlabConfiguration + ): CodeOwnersValidationException? { + return runCodeownersValidation( + testInfo, configuration, + CodeOwners( + """ + [README Owners] + README_niels.md @niels + README_niels_private_email.md private@example.nl + README_dummy.md @dummy + README_badrole.md @@badrole + """.trimIndent() + ), + Problem.Info( "README Owners", "README_niels.md", "@niels", "Valid approver: Member with username \"niels\" can approve (AccessLevel:50=OWNER)" ), + Problem.Warning( "README Owners", "README_niels_private_email.md", "private@example.nl", "Unable to verify email address: Assuming the user exists and can approve" ), + Problem.Error( "README Owners", "README_dummy.md", "@dummy", "User exists but is not a member of with this project: Dummy User" ), + Problem.Error( "README Owners", "README_dummy.md", "", "NO Valid Approvers for rule" ), + Problem.Fatal( "README Owners", "README_badrole.md", "@@badrole", "Illegal role was specified" ), + ) + } + + private fun runFailLevelTestError( + testInfo: TestInfo, + configuration: GitlabConfiguration + ): CodeOwnersValidationException? { + return runCodeownersValidation( + testInfo, configuration, + CodeOwners( + """ + [README Owners] + README_niels.md @niels + README_niels_private_email.md private@example.nl + README_dummy.md @dummy + """.trimIndent() + ), + Problem.Info( "README Owners", "README_niels.md", "@niels", "Valid approver: Member with username \"niels\" can approve (AccessLevel:50=OWNER)" ), + Problem.Warning( "README Owners", "README_niels_private_email.md", "private@example.nl", "Unable to verify email address: Assuming the user exists and can approve" ), + Problem.Error( "README Owners", "README_dummy.md", "@dummy", "User exists but is not a member of with this project: Dummy User" ), + Problem.Error( "README Owners", "README_dummy.md", "", "NO Valid Approvers for rule" ), + ) + } + + private fun runFailLevelTestWarning( + testInfo: TestInfo, + configuration: GitlabConfiguration + ): CodeOwnersValidationException? { + return runCodeownersValidation( + testInfo, configuration, + CodeOwners( + """ + [README Owners] + README_niels.md @niels + README_niels_private_email.md private@example.nl + """.trimIndent() + ), + Problem.Warning("README Owners", "README_niels_private_email.md", "private@example.nl", "Unable to verify email address: Assuming the user exists and can approve"), + ) + } + + private fun runFailLevelTestInfo( + testInfo: TestInfo, + configuration: GitlabConfiguration + ): CodeOwnersValidationException? { + return runCodeownersValidation( + testInfo, configuration, + CodeOwners( + """ + [README Owners] + README_niels.md @niels + """.trimIndent() + ), + Problem.Info("README Owners", "README_niels.md", "@niels", "Valid approver: Member with username \"niels\" can approve (AccessLevel:50=OWNER)") + ) + } + + private fun runCodeownersValidation( + testInfo: TestInfo, + configuration: GitlabConfiguration, + codeOwners: CodeOwners, + vararg expectedProblems: Problem? + ): CodeOwnersValidationException? { + return runCodeownersValidation(testInfo, configuration, false, codeOwners, *expectedProblems) + } + + private fun runCodeownersValidation( + testInfo: TestInfo, + configuration: GitlabConfiguration, + logResult: Boolean, + codeOwners: CodeOwners, + vararg expectedProblems: Problem? + ): CodeOwnersValidationException? { + GitlabProjectMembers(configuration).use { gitlabProjectMembers -> + val logger = LoggerFactory.getLogger( + testInfo.testMethod.map(Function { obj: Method? -> obj!!.name }) + .orElse("runCodeownersValidation") + ) + gitlabProjectMembers.showAllApprovers = true + var exception: CodeOwnersValidationException? = null + var problemTable: ProblemTable? = null + try { + problemTable = gitlabProjectMembers.verifyAllCodeowners(logger, codeOwners) + gitlabProjectMembers.failIfExceededFailLevel(problemTable) + } catch (e: CodeOwnersValidationException) { + exception = e + } + + assertNotNull(problemTable) + for (expectedProblem in expectedProblems) { + assertTrue( + problemTable.contains(expectedProblem), + "The problem table \n" + + problemTable.problems.joinToString(separator = "\n") { it.toString() } + + "\n should contain \n" + expectedProblem) + } + if (logResult) { + logger.info(problemTable.toString()) + logger.info(problemTable.toProblemMessageGroupedString()) + } + return exception + } + } + + companion object { + fun makeConfig( + wmRuntimeInfo: WireMockRuntimeInfo?, + projectId: String?, + tokenEnvVariableName: String? + ): GitlabConfiguration { + return GitlabConfiguration( + ServerUrl(wmRuntimeInfo?.httpBaseUrl), + ProjectId(projectId), + AccessToken(tokenEnvVariableName) + ) + } + } +} diff --git a/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/utils/Line.java b/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/utils/Line.kt similarity index 62% rename from codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/utils/Line.java rename to codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/utils/Line.kt index dfcbc6c..c304bf9 100644 --- a/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/utils/Line.java +++ b/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/utils/Line.kt @@ -14,26 +14,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package nl.basjes.codeowners.validator.utils -package nl.basjes.codeowners.validator.utils; +import org.slf4j.event.Level -import org.slf4j.event.Level; - - -public class Line { - public Line(Level level, String message) { - this.level = level; - this.message = message; - } - - public Level level; - public String message; - - @Override - public String toString() { +class Line(@JvmField var level: Level, @JvmField var message: String) { + override fun toString(): String { return "Line {" + - "level=" + level + - ", message='" + message + '\'' + - '}'; + "level=" + level + + ", message='" + message + '\'' + + '}' } } diff --git a/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/utils/TestLogger.java b/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/utils/TestLogger.java deleted file mode 100644 index f8d2675..0000000 --- a/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/utils/TestLogger.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * CodeOwners Tools - * Copyright (C) 2023-2026 Niels Basjes - * - * 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 - * - * https://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 nl.basjes.codeowners.validator.utils; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.slf4j.Marker; -import org.slf4j.event.Level; -import org.slf4j.helpers.AbstractLogger; -import org.slf4j.helpers.FormattingTuple; -import org.slf4j.helpers.MessageFormatter; - -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -import static org.junit.jupiter.api.Assertions.fail; - -public class TestLogger extends AbstractLogger { - - public TestLogger(String className) { - logger = LoggerFactory.getLogger(className); - } - - final Logger logger; - - @Override - protected String getFullyQualifiedCallerName() { - return null; - } - - - @Override - protected void handleNormalizedLoggingCall(Level level, Marker marker, String messagePattern, Object[] arguments, Throwable throwable) { - FormattingTuple tuple = MessageFormatter.format(messagePattern, arguments, throwable); - loggedLines.add(new Line(level, tuple.getMessage())); - - switch (level) { - case ERROR: - logger.error(tuple.getMessage()); - break; - case WARN: - logger.warn(tuple.getMessage()); - break; - case INFO: - logger.info(tuple.getMessage()); - break; - case DEBUG: - case TRACE: - logger.debug(tuple.getMessage()); - break; - } - } - - - final List loggedLines = new ArrayList<>(); - - private void assertContainsMsg(Level level, String expectedSubstring) { - for (Line loggedLine : loggedLines) { - if (loggedLine.level == level) { - if (loggedLine.message.contains(expectedSubstring)) { - return; - } - } - } - fail("A message of level "+level+" containing '" + expectedSubstring + "' was not found.\n"+ "Known lines are: \n" + loggedLines.stream().map(line -> "---> " + line.level + " | " + line.message + "\n").collect(Collectors.joining())); - } - - public void assertContainsDebug(String message) { - assertContainsMsg(Level.DEBUG, message); - } - public void assertContainsInfo(String message) { - assertContainsMsg(Level.INFO ,message); - } - public void assertContainsWarn(String message) { - assertContainsMsg(Level.WARN, message); - } - public void assertContainsError(String message) { - assertContainsMsg(Level.ERROR, message); - } - - public long countDebug() { return loggedLines.stream().filter(l -> l.level == Level.DEBUG) .count(); } - public long countInfo() { return loggedLines.stream().filter(l -> l.level == Level.INFO) .count(); } - public long countWarn() { return loggedLines.stream().filter(l -> l.level == Level.WARN) .count(); } - public long countError() { return loggedLines.stream().filter(l -> l.level == Level.ERROR) .count(); } - - @Override - public boolean isTraceEnabled() { - return false; - } - - @Override - public boolean isTraceEnabled(Marker marker) { - return false; - } - - @Override - public boolean isDebugEnabled() { - return true; - } - - @Override - public boolean isDebugEnabled(Marker marker) { - return false; - } - - @Override - public boolean isInfoEnabled() { - return true; - } - - @Override - public boolean isInfoEnabled(Marker marker) { - return false; - } - - @Override - public boolean isWarnEnabled() { - return true; - } - - @Override - public boolean isWarnEnabled(Marker marker) { - return false; - } - - @Override - public boolean isErrorEnabled() { - return true; - } - - @Override - public boolean isErrorEnabled(Marker marker) { - return false; - } - -} diff --git a/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/utils/TestLogger.kt b/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/utils/TestLogger.kt new file mode 100644 index 0000000..2eb5043 --- /dev/null +++ b/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/utils/TestLogger.kt @@ -0,0 +1,140 @@ +/* + * CodeOwners Tools + * Copyright (C) 2023-2026 Niels Basjes + * + * 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 + * + * https://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 nl.basjes.codeowners.validator.utils + +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.slf4j.Marker +import org.slf4j.event.Level +import org.slf4j.helpers.AbstractLogger +import org.slf4j.helpers.MessageFormatter +import kotlin.test.fail + +class TestLogger(className: String?) : AbstractLogger() { + val logger: Logger = LoggerFactory.getLogger(className) + + override fun getFullyQualifiedCallerName(): String? { + return null + } + + override fun handleNormalizedLoggingCall( + level: Level, + marker: Marker?, + messagePattern: String?, + arguments: Array?, + throwable: Throwable? + ) { + val tuple = MessageFormatter.format(messagePattern, arguments, throwable) + loggedLines.add(Line(level, tuple.message)) + + when (level) { + Level.ERROR -> logger.error(tuple.message) + Level.WARN -> logger.warn(tuple.message) + Level.INFO -> logger.info(tuple.message) + Level.DEBUG, Level.TRACE -> logger.debug(tuple.message) + } + } + + val loggedLines: MutableList = mutableListOf() + + private fun assertContainsMsg(level: Level?, expectedSubstring: String) { + for (loggedLine in loggedLines) { + if (loggedLine.level == level) { + if (loggedLine.message.contains(expectedSubstring)) { + return + } + } + } + fail( + "A message of level $level containing '$expectedSubstring' was not found.\n" + + "Known lines are: \n" + + loggedLines.map { "---> ${it.level} | ${it.message}\n" }.toList() + ) + } + + fun assertContainsDebug(message: String) { + assertContainsMsg(Level.DEBUG, message) + } + + fun assertContainsInfo(message: String) { + assertContainsMsg(Level.INFO, message) + } + + fun assertContainsWarn(message: String) { + assertContainsMsg(Level.WARN, message) + } + + fun assertContainsError(message: String) { + assertContainsMsg(Level.ERROR, message) + } + + fun countDebug(): Int { + return loggedLines.count { it.level == Level.DEBUG } + } + + fun countInfo(): Int { + return loggedLines.count { it.level == Level.INFO } + } + + fun countWarn(): Int { + return loggedLines.count { it.level == Level.WARN } + } + + fun countError(): Int { + return loggedLines.count { it.level == Level.ERROR } + } + + override fun isTraceEnabled(): Boolean { + return false + } + + override fun isTraceEnabled(marker: Marker?): Boolean { + return false + } + + override fun isDebugEnabled(): Boolean { + return true + } + + override fun isDebugEnabled(marker: Marker?): Boolean { + return false + } + + override fun isInfoEnabled(): Boolean { + return true + } + + override fun isInfoEnabled(marker: Marker?): Boolean { + return false + } + + override fun isWarnEnabled(): Boolean { + return true + } + + override fun isWarnEnabled(marker: Marker?): Boolean { + return false + } + + override fun isErrorEnabled(): Boolean { + return true + } + + override fun isErrorEnabled(marker: Marker?): Boolean { + return false + } +} diff --git a/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/utils/TestProblemTable.java b/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/utils/TestProblemTable.java deleted file mode 100644 index 7c0414c..0000000 --- a/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/utils/TestProblemTable.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * CodeOwners Tools - * Copyright (C) 2023-2026 Niels Basjes - * - * 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 - * - * https://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 nl.basjes.codeowners.validator.utils; - -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class TestProblemTable { - - @Test - void testProblemTable() { - TestLogger logger = new TestLogger("testProblemTable"); - - ProblemTable table = new ProblemTable(); - table.addProblem(new Problem.Info ("Section Info", "Expression Info", "Approver Info", "Problem Info")); - table.toLogAsBlock(logger); - assertEquals(1, table.getNumberOfProblems()); - table.addProblem(new Problem.Warning ("Section Warning", "Expression Warning", "Approver Warning", "Problem Warning")); - table.toLogAsBlock(logger); - assertEquals(2, table.getNumberOfProblems()); - table.addProblem(new Problem.Error ("Section Error", "Expression Error", "Approver Error", "Problem Error")); - table.toLogAsBlock(logger); - assertEquals(3, table.getNumberOfProblems()); - table.addProblem(new Problem.Fatal ("Section Fatal", "Expression Fatal", "Approver Fatal", "Problem Fatal")); - table.toLogAsBlock(logger); - assertEquals(4, table.getNumberOfProblems()); - - assertTrue(table.contains(new Problem.Info ("Section Info", "Expression Info", "Approver Info", "Problem Info"))); - assertTrue(table.contains(new Problem.Warning ("Section Warning", "Expression Warning", "Approver Warning", "Problem Warning"))); - assertTrue(table.contains(new Problem.Error ("Section Error", "Expression Error", "Approver Error", "Problem Error"))); - assertTrue(table.contains(new Problem.Fatal ("Section Fatal", "Expression Fatal", "Approver Fatal", "Problem Fatal"))); - - assertFalse(table.contains(new Problem.Info ("x Section Info", "x Expression Info", "x Approver Info", "x Problem Info"))); - assertFalse(table.contains(new Problem.Warning ("x Section Warning", "x Expression Warning", "x Approver Warning", "x Problem Warning"))); - assertFalse(table.contains(new Problem.Error ("x Section Error", "x Expression Error", "x Approver Error", "x Problem Error"))); - assertFalse(table.contains(new Problem.Fatal ("x Section Fatal", "x Expression Fatal", "x Approver Fatal", "x Problem Fatal"))); - - table.toLog(logger); - logger.assertContainsInfo ("| Section Info | Expression Info | Approver Info | Problem Info |"); - logger.assertContainsWarn ("| Section Warning | Expression Warning | Approver Warning | Problem Warning |"); - logger.assertContainsError("| Section Error | Expression Error | Approver Error | Problem Error |"); - logger.assertContainsError("| Section Fatal | Expression Fatal | Approver Fatal | Problem Fatal |"); - - table.toLogAsBlock(logger); - } - -} diff --git a/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/utils/TestProblemTable.kt b/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/utils/TestProblemTable.kt new file mode 100644 index 0000000..ef71b8a --- /dev/null +++ b/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/utils/TestProblemTable.kt @@ -0,0 +1,140 @@ +/* + * CodeOwners Tools + * Copyright (C) 2023-2026 Niels Basjes + * + * 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 + * + * https://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 nl.basjes.codeowners.validator.utils + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class TestProblemTable { + @Test + fun testProblemTable() { + val logger = TestLogger("testProblemTable") + + val table = ProblemTable() + table.addProblem(Problem.Info("Section Info", "Expression Info", "Approver Info", "Problem Info")) + table.toLogAsBlock(logger) + assertEquals(1, table.numberOfProblems) + table.addProblem( + Problem.Warning( + "Section Warning", + "Expression Warning", + "Approver Warning", + "Problem Warning" + ) + ) + table.toLogAsBlock(logger) + assertEquals(2, table.numberOfProblems) + table.addProblem(Problem.Error("Section Error", "Expression Error", "Approver Error", "Problem Error")) + table.toLogAsBlock(logger) + assertEquals(3, table.numberOfProblems) + table.addProblem(Problem.Fatal("Section Fatal", "Expression Fatal", "Approver Fatal", "Problem Fatal")) + table.toLogAsBlock(logger) + assertEquals(4, table.numberOfProblems) + + assertTrue( + table.contains( + Problem.Info( + "Section Info", + "Expression Info", + "Approver Info", + "Problem Info" + ) + ) + ) + assertTrue( + table.contains( + Problem.Warning( + "Section Warning", + "Expression Warning", + "Approver Warning", + "Problem Warning" + ) + ) + ) + assertTrue( + table.contains( + Problem.Error( + "Section Error", + "Expression Error", + "Approver Error", + "Problem Error" + ) + ) + ) + assertTrue( + table.contains( + Problem.Fatal( + "Section Fatal", + "Expression Fatal", + "Approver Fatal", + "Problem Fatal" + ) + ) + ) + + assertFalse( + table.contains( + Problem.Info( + "x Section Info", + "x Expression Info", + "x Approver Info", + "x Problem Info" + ) + ) + ) + assertFalse( + table.contains( + Problem.Warning( + "x Section Warning", + "x Expression Warning", + "x Approver Warning", + "x Problem Warning" + ) + ) + ) + assertFalse( + table.contains( + Problem.Error( + "x Section Error", + "x Expression Error", + "x Approver Error", + "x Problem Error" + ) + ) + ) + assertFalse( + table.contains( + Problem.Fatal( + "x Section Fatal", + "x Expression Fatal", + "x Approver Fatal", + "x Problem Fatal" + ) + ) + ) + + table.toLog(logger) + logger.assertContainsInfo("| Section Info | Expression Info | Approver Info | Problem Info |") + logger.assertContainsWarn("| Section Warning | Expression Warning | Approver Warning | Problem Warning |") + logger.assertContainsError("| Section Error | Expression Error | Approver Error | Problem Error |") + logger.assertContainsError("| Section Fatal | Expression Fatal | Approver Fatal | Problem Fatal |") + + table.toLogAsBlock(logger) + } +} diff --git a/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/utils/TestStringTable.java b/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/utils/TestStringTable.java deleted file mode 100644 index 772884b..0000000 --- a/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/utils/TestStringTable.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * CodeOwners Tools - * Copyright (C) 2023-2026 Niels Basjes - * - * 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 - * - * https://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 nl.basjes.codeowners.validator.utils; - -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.List; - -public class TestStringTable { - Logger log = LoggerFactory.getLogger("TestStringTable"); - - @Test - void testStringTable() { - StringTable table = new StringTable(); - log.info("\n{}", table - .withHeaders("One", "Two", "Three") - .addRow("1", "2", "3", "4") - .addRowSeparator() - .addRow("11", "22") - .addRow("1111", "2222", "33") - .addRow("111111", "222222", "3333", "444444444") - .addRow("11111111", "22222222", "333333", "4444", "55")); - } - - @Test - void testStringTable1() { - StringTable table = new StringTable(); - log.info("\n{}", table - .withHeaders(List.of("One", "Two", "Three")) - .addRow("1", "2", "3", "4") - .addRowSeparator() - .addRow("11", "22") - .addRow("1111", "2222", "33") - .addRow("111111", "222222", "3333", "444444444") - .addRow("11111111", "22222222", "333333", "4444", "55")); - } - - -} diff --git a/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/utils/TestStringTable.kt b/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/utils/TestStringTable.kt new file mode 100644 index 0000000..5d3537c --- /dev/null +++ b/codeowners-validator/src/test/kotlin/nl/basjes/codeowners/validator/utils/TestStringTable.kt @@ -0,0 +1,55 @@ +/* + * CodeOwners Tools + * Copyright (C) 2023-2026 Niels Basjes + * + * 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 + * + * https://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 nl.basjes.codeowners.validator.utils + +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import kotlin.test.Test + +class TestStringTable { + var log: Logger = LoggerFactory.getLogger("TestStringTable") + + @Test + fun testStringTable() { + val table = StringTable() + log.info( + "\n{}", table + .withHeaders("One", "Two", "Three") + .addRow("1", "2", "3", "4") + .addRowSeparator() + .addRow("11", "22") + .addRow("1111", "2222", "33") + .addRow("111111", "222222", "3333", "444444444") + .addRow("11111111", "22222222", "333333", "4444", "55") + ) + } + + @Test + fun testStringTable1() { + val table = StringTable() + log.info( + "\n{}", table + .withHeaders(mutableListOf("One", "Two", "Three")) + .addRow("1", "2", "3", "4") + .addRowSeparator() + .addRow("11", "22") + .addRow("1111", "2222", "33") + .addRow("111111", "222222", "3333", "444444444") + .addRow("11111111", "22222222", "333333", "4444", "55") + ) + } +} diff --git a/enforcer/pom.xml b/enforcer/pom.xml index 8f625a6..43b0c75 100644 --- a/enforcer/pom.xml +++ b/enforcer/pom.xml @@ -70,8 +70,8 @@ - org.junit.jupiter - junit-jupiter-engine + org.jetbrains.kotlin + kotlin-test-junit5 test diff --git a/enforcer/src/main/kotlin/nl/basjes/maven/enforcer/codeowners/CodeOwnersEnforcerRule.kt b/enforcer/src/main/kotlin/nl/basjes/maven/enforcer/codeowners/CodeOwnersEnforcerRule.kt index 8d31711..2df6f3a 100644 --- a/enforcer/src/main/kotlin/nl/basjes/maven/enforcer/codeowners/CodeOwnersEnforcerRule.kt +++ b/enforcer/src/main/kotlin/nl/basjes/maven/enforcer/codeowners/CodeOwnersEnforcerRule.kt @@ -56,13 +56,15 @@ class CodeOwnersEnforcerRule @Inject constructor(private val project: MavenProje baseDir = project.basedir } + val cleanBaseDir = baseDir ?: throw IllegalStateException("Unable to determine the basedir") + val directoryOwners: DirectoryOwners? var pass = true try { val validator = CodeOwnersValidator(gitlab, EnforcerLoggerSlf4j(log), verbose) - directoryOwners = validator.analyzeDirectory(baseDir!!, codeOwnersFile) + directoryOwners = validator.analyzeDirectory(cleanBaseDir, codeOwnersFile) if (showApprovers) { log.info("Approvers:\n" + directoryOwners.toTable()) @@ -112,15 +114,11 @@ class CodeOwnersEnforcerRule @Inject constructor(private val project: MavenProje * * @return rule description */ - override fun toString(): String { - return String.format( - "CodeOwnersEnforcerRule[codeOwnersFile=%s ; allFilesMustHaveCodeOwner=%b]", - codeOwnersFile, allFilesMustHaveCodeOwner - ) - } + override fun toString() = + "CodeOwnersEnforcerRule[codeOwnersFile=${codeOwnersFile} ; allFilesMustHaveCodeOwner=${allFilesMustHaveCodeOwner}]" companion object { - private val log: Logger? = LoggerFactory.getLogger(CodeOwnersEnforcerRule::class.java) + private val log: Logger = LoggerFactory.getLogger(CodeOwnersEnforcerRule::class.java) } } diff --git a/gitignore-reader/pom.xml b/gitignore-reader/pom.xml index e2d9b83..57eb7a0 100644 --- a/gitignore-reader/pom.xml +++ b/gitignore-reader/pom.xml @@ -47,8 +47,8 @@ - org.junit.jupiter - junit-jupiter-engine + org.jetbrains.kotlin + kotlin-test-junit5 test diff --git a/gitignore-reader/src/test/kotlin/nl/basjes/gitignore/TestBugreports.java b/gitignore-reader/src/test/kotlin/nl/basjes/gitignore/TestBugreports.kt similarity index 56% rename from gitignore-reader/src/test/kotlin/nl/basjes/gitignore/TestBugreports.java rename to gitignore-reader/src/test/kotlin/nl/basjes/gitignore/TestBugreports.kt index 92207db..6651252 100644 --- a/gitignore-reader/src/test/kotlin/nl/basjes/gitignore/TestBugreports.java +++ b/gitignore-reader/src/test/kotlin/nl/basjes/gitignore/TestBugreports.kt @@ -14,23 +14,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package nl.basjes.gitignore -package nl.basjes.gitignore; - -import org.junit.jupiter.api.Test; - -import java.io.File; - -import static nl.basjes.gitignore.TestUtils.assertIgnore; -import static nl.basjes.gitignore.TestUtils.assertNotIgnore; -import static nl.basjes.gitignore.TestUtils.verifyGeneratedRegex; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -class TestBugreports { +import kotlin.test.Test +import java.io.File +import kotlin.test.assertEquals +import kotlin.test.assertTrue +internal class TestBugreports { @Test - void testRat362() { + fun testRat362() { // https://issues.apache.org/jira/browse/RAT-362 // Bugreport summary // In the root of the file system @@ -42,46 +35,49 @@ void testRat362() { // Rootcause: This library expects absolute paths and was provided with a relative path. - GitIgnoreFileSet gitIgnoreFileSet = new GitIgnoreFileSet(new File("/foo"), false); - gitIgnoreFileSet.add(new GitIgnore("/", "/foo.md")); + val gitIgnoreFileSet = GitIgnoreFileSet(File("/foo"), false) + gitIgnoreFileSet.add(GitIgnore("/", "/foo.md")) // Absolute path is the default (for backwards compatibility) - assertTrue(gitIgnoreFileSet.ignoreFile("/foo/foo.md")); - assertTrue(gitIgnoreFileSet.keepFile("/foo/foo/foo.md")); + assertTrue(gitIgnoreFileSet.ignoreFile("/foo/foo.md")) + assertTrue(gitIgnoreFileSet.keepFile("/foo/foo/foo.md")) // Project relative (explicit) - assertTrue(gitIgnoreFileSet.ignoreFile("foo.md", true)); - assertTrue(gitIgnoreFileSet.keepFile("/foo/foo.md", true)); + assertTrue(gitIgnoreFileSet.ignoreFile("foo.md", true)) + assertTrue(gitIgnoreFileSet.keepFile("/foo/foo.md", true)) // Project relative (assumption) - gitIgnoreFileSet.assumeQueriesAreProjectRelative(); - assertTrue(gitIgnoreFileSet.ignoreFile("foo.md")); - assertTrue(gitIgnoreFileSet.keepFile("/foo/foo.md")); + gitIgnoreFileSet.assumeQueriesAreProjectRelative() + assertTrue(gitIgnoreFileSet.ignoreFile("foo.md")) + assertTrue(gitIgnoreFileSet.keepFile("/foo/foo.md")) // Absolute path (explicit) - assertTrue(gitIgnoreFileSet.ignoreFile("/foo/foo.md", false)); - assertTrue(gitIgnoreFileSet.keepFile("/foo/foo/foo.md", false)); + assertTrue(gitIgnoreFileSet.ignoreFile("/foo/foo.md", false)) + assertTrue(gitIgnoreFileSet.keepFile("/foo/foo/foo.md", false)) // Absolute path (assumption) - gitIgnoreFileSet.assumeQueriesIncludeProjectBaseDir(); - assertTrue(gitIgnoreFileSet.ignoreFile("/foo/foo.md")); - assertTrue(gitIgnoreFileSet.keepFile("/foo/foo/foo.md")); + gitIgnoreFileSet.assumeQueriesIncludeProjectBaseDir() + assertTrue(gitIgnoreFileSet.ignoreFile("/foo/foo.md")) + assertTrue(gitIgnoreFileSet.keepFile("/foo/foo/foo.md")) } @Test - void testStarInIgnoreRule() { + fun testStarInIgnoreRule() { // https://github.com/nielsbasjes/codeowners/issues/77 // foo\*txt should match foo*txt (and also not match foo.txt) - verifyGeneratedRegex("/", "foo\\*txt", "^/?(.*/)?foo\\Q*\\Etxt(/|$)"); + TestUtils.verifyGeneratedRegex("/", "foo\\*txt", "^/?(.*/)?foo\\Q*\\Etxt(/|$)") - GitIgnore gitIgnore = new GitIgnore("/", "foo\\*txt"); + val gitIgnore = GitIgnore("/", "foo\\*txt") - assertIgnore( gitIgnore, + TestUtils.assertIgnore( + gitIgnore, "/foo*txt", "/foo*txt/foo.txt", - "/foo.txt/foo*txt"); + "/foo.txt/foo*txt" + ) - assertNotIgnore( gitIgnore, + TestUtils.assertNotIgnore( + gitIgnore, "/foo.txt", "/footxt", "/foootxt", @@ -90,22 +86,26 @@ void testStarInIgnoreRule() { "/foo.txt/foo.txt", "/footxt/footxt", "/foootxt/foootxt", - "/foo_txt/foo_txt"); + "/foo_txt/foo_txt" + ) } @Test - void testQuestionMarkInIgnoreRule() { - verifyGeneratedRegex("/", "foo\\?txt", "^/?(.*/)?foo\\Q?\\Etxt(/|$)"); + fun testQuestionMarkInIgnoreRule() { + TestUtils.verifyGeneratedRegex("/", "foo\\?txt", "^/?(.*/)?foo\\Q?\\Etxt(/|$)") - GitIgnore gitIgnore = new GitIgnore("/", "foo\\?txt"); + val gitIgnore = GitIgnore("/", "foo\\?txt") - assertIgnore( gitIgnore, + TestUtils.assertIgnore( + gitIgnore, "/foo?txt", "/foo?txt/foo.txt", - "/foo.txt/foo?txt"); + "/foo.txt/foo?txt" + ) - assertNotIgnore( gitIgnore, + TestUtils.assertNotIgnore( + gitIgnore, "/foo.txt", "/footxt", "/foo/txt", @@ -113,23 +113,23 @@ void testQuestionMarkInIgnoreRule() { "/foo.txt/foo.txt", "/footxt/footxt", "/foootxt/foootxt", - "/foo_txt/foo_txt"); + "/foo_txt/foo_txt" + ) } @Test - void extraRulesForExistingDirectory() { + fun extraRulesForExistingDirectory() { // Old situation: If you specify a second set of rules for a directory one of them is lost. // New situation: There are evaluated in the order they were added - GitIgnore gitIgnore1 = new GitIgnore(".git/"); - GitIgnore gitIgnore2 = new GitIgnore(".svn/"); + val gitIgnore1 = GitIgnore(".git/") + val gitIgnore2 = GitIgnore(".svn/") - GitIgnoreFileSet gitIgnoreFileSet = new GitIgnoreFileSet(new File("/tmp/foo"), false); - gitIgnoreFileSet.add(gitIgnore1); - gitIgnoreFileSet.add(gitIgnore2); + val gitIgnoreFileSet = GitIgnoreFileSet(File("/tmp/foo"), false) + gitIgnoreFileSet.add(gitIgnore1) + gitIgnoreFileSet.add(gitIgnore2) - assertEquals(true, gitIgnoreFileSet.isIgnoredFile("/tmp/foo/.git/foo")); - assertEquals(true, gitIgnoreFileSet.isIgnoredFile("/tmp/foo/.svn/bar")); + assertEquals(true, gitIgnoreFileSet.isIgnoredFile("/tmp/foo/.git/foo")) + assertEquals(true, gitIgnoreFileSet.isIgnoredFile("/tmp/foo/.svn/bar")) } - } diff --git a/gitignore-reader/src/test/kotlin/nl/basjes/gitignore/TestGitIgnore.java b/gitignore-reader/src/test/kotlin/nl/basjes/gitignore/TestGitIgnore.java deleted file mode 100644 index a27eeff..0000000 --- a/gitignore-reader/src/test/kotlin/nl/basjes/gitignore/TestGitIgnore.java +++ /dev/null @@ -1,637 +0,0 @@ -/* - * CodeOwners Tools - * Copyright (C) 2023-2026 Niels Basjes - * - * 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 - * - * https://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 nl.basjes.gitignore; - -import nl.basjes.gitignore.GitIgnore.IgnoreRule; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.File; -import java.io.IOException; -import java.net.URL; -import java.util.regex.PatternSyntaxException; - -import static nl.basjes.gitignore.TestUtils.assertIgnore; -import static nl.basjes.gitignore.TestUtils.assertNotIgnore; -import static nl.basjes.gitignore.TestUtils.assertNullMatch; -import static nl.basjes.gitignore.TestUtils.verifyGeneratedRegex; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; - -class TestGitIgnore { - - private static final Logger LOG = LoggerFactory.getLogger(TestGitIgnore.class); - - @Test - void testFullRangeExpression() { - // This expression contains all features described in the examples below to ensure the parser can handle it - String input = - "*.log\n" + - "!\\#important?/debug[0-9]/debug[!01]/**/*debug[a-z]/*.log"; - GitIgnore gitIgnore = new GitIgnore(input); - assertIgnore(gitIgnore, "logs/debug.log"); - assertNullMatch(gitIgnore, "#important_/debug4/debug4/something/something/local_debugb/Something.logxxx"); - LOG.info("Input:\n{}\nParsed:\n{}\n", input, gitIgnore); - } - - // ------------------------------------------ - - // I used the examples provided by Atlassian in their gitignore tutorial as test cases - // https://www.atlassian.com/git/tutorials/saving-changes/gitignore - // This documentation is licensed via https://creativecommons.org/licenses/by/2.5/au/ - - @Test - void testPrependGlobstar1() { - // You can prepend a pattern with a double asterisk to match directories anywhere in the repository. - GitIgnore gitIgnore = new GitIgnore("**/logs"); - - assertIgnore(gitIgnore, "logs/debug.log"); - assertIgnore(gitIgnore, "logs/monday/foo.bar"); - assertIgnore(gitIgnore, "build/logs/debug.log"); - } - - // ------------------------------------------ - - @Test - void testPrependGlobstar2() { - //You can also use a double asterisk to match files based on their name and the name of their parent directory. - GitIgnore gitIgnore = new GitIgnore("**/logs/debug.log"); - - assertIgnore(gitIgnore, "logs/debug.log"); - assertIgnore(gitIgnore, "build/logs/debug.log"); - - assertNotIgnore(gitIgnore, "logs/build/debug.log"); - } - - // ------------------------------------------ - - @Test - void testWildcard() { - //An asterisk is a wildcard that matches zero or more characters. - GitIgnore gitIgnore = new GitIgnore("*.log"); - - assertIgnore(gitIgnore, "debug.log"); - assertIgnore(gitIgnore, "foo.log"); - assertIgnore(gitIgnore, ".log"); - assertIgnore(gitIgnore, "logs/debug.log"); - } - - // ------------------------------------------ - - @Test - void testNegate() { - // Prepending an exclamation mark to a pattern negates it. - // If a file matches a pattern, but also matches a negating pattern defined later in the file, it will not be ignored. - GitIgnore gitIgnore = new GitIgnore( - "*.log\n" + - "!important.log"); - - assertIgnore(gitIgnore, "debug.log"); - assertIgnore(gitIgnore, "trace.log"); - - assertNotIgnore(gitIgnore, "important.log"); - assertNotIgnore(gitIgnore, "logs/important.log"); - } - - // ------------------------------------------ - - @Test - void testNegateAndReignore() { - // Patterns defined after a negating pattern will re-ignore any previously negated files. - GitIgnore gitIgnore = new GitIgnore( - "*.log\n" + - "!important/*.log\n" + - "trace.* "); - assertIgnore(gitIgnore, "debug.log"); - assertIgnore(gitIgnore, "important/trace.log"); - - assertNotIgnore(gitIgnore, "important/debug.log"); - } - - // ------------------------------------------ - - @Test - void testPinRoot() { - // Prepending a slash matches files only in the repository root. - GitIgnore gitIgnore = new GitIgnore("/debug.log "); - assertIgnore(gitIgnore, "debug.log"); - - assertNotIgnore(gitIgnore, "logs/debug.log"); - } - - // ------------------------------------------ - - @Test - void testDefaultAnyDirectory() { - // By default, patterns match files in any directory - GitIgnore gitIgnore = new GitIgnore("debug.log"); - assertIgnore(gitIgnore, "debug.log"); - assertIgnore(gitIgnore, "logs/debug.log"); - } - - // ------------------------------------------ - - @Test - void testSingleCharWildcard() { - // A question mark matches exactly one character. - GitIgnore gitIgnore = new GitIgnore("debug?.log "); - assertIgnore(gitIgnore, "debug0.log"); - assertIgnore(gitIgnore, "debugg.log"); - - assertNotIgnore(gitIgnore, "debug10.log"); - } - - // ------------------------------------------ - - @Test - void testCharRange() { - // Square brackets can also be used to match a single character from a specified range. - GitIgnore gitIgnore = new GitIgnore("debug[0-9].log "); - - assertIgnore(gitIgnore, "debug0.log"); - assertIgnore(gitIgnore, "debug1.log"); - - assertNotIgnore(gitIgnore, "debug10.log"); - } - - // ------------------------------------------ - - @Test - void testCharSet() { - // Square brackets match a single character form the specified set. - GitIgnore gitIgnore = new GitIgnore("debug[01].log"); - - assertIgnore(gitIgnore, "debug0.log"); - assertIgnore(gitIgnore, "debug1.log"); - - assertNotIgnore(gitIgnore, "debug2.log"); - assertNotIgnore(gitIgnore, "debug01.log"); - } - - // ------------------------------------------ - - @Test - void testNotCharSet() { - // An exclamation mark can be used to match any character except one from the specified set. - GitIgnore gitIgnore = new GitIgnore("debug[!01].log "); - - assertIgnore(gitIgnore, "debug2.log"); - - assertNotIgnore(gitIgnore, "debug0.log"); - assertNotIgnore(gitIgnore, "debug1.log"); - assertNotIgnore(gitIgnore, "debug01.log"); - } - - // ------------------------------------------ - - @Test - void testCharRangeAlpabetical() { - // Ranges can be numeric or alphabetic. - GitIgnore gitIgnore = new GitIgnore("debug[a-z].log "); - - assertIgnore(gitIgnore, "debuga.log"); - assertIgnore(gitIgnore, "debugb.log"); - - assertNotIgnore(gitIgnore, "debug1.log"); - } - - // ------------------------------------------ - - @Test - void testNoSlashMatchDirsAndFiles() { - // If you don't append a slash, the pattern will match both files and the contents of directories with that name. - // In the example matches on the left, both directories and files named logs are ignored - GitIgnore gitIgnore = new GitIgnore("logs"); - assertIgnore(gitIgnore, "logs"); - assertIgnore(gitIgnore, "logs/debug.log"); - assertIgnore(gitIgnore, "logs/latest/foo.bar"); - assertIgnore(gitIgnore, "build/logs"); - assertIgnore(gitIgnore, "build/logs/debug.log"); - } - - // ------------------------------------------ - - @Test - void testAppendSlash() { - // Appending a slash indicates the pattern is a directory. - // The entire contents of any directory in the repository matching that name – - // including all of its files and subdirectories – will be ignored - GitIgnore gitIgnore = new GitIgnore("logs/"); - - assertIgnore(gitIgnore, "logs/"); - assertIgnore(gitIgnore, "logs/debug.log"); - assertIgnore(gitIgnore, "logs/latest/foo.bar"); - assertIgnore(gitIgnore, "build/logs/foo.bar"); - assertIgnore(gitIgnore, "build/logs/latest/debug.log"); - } - - // ------------------------------------------ - - @Test - void testEdgeCase() { - // From https://www.atlassian.com/git/tutorials/saving-changes/gitignore - // Wait a minute! Shouldn't logs/important.log be negated in the example on the left? - // Nope! Due to a performance-related quirk in Git, you can not negate a file - // that is ignored due to a pattern matching a directory - GitIgnore gitIgnore = new GitIgnore( - "logs/\n" + - "!logs/important.log\n"); - - assertIgnore(gitIgnore, "logs/debug.log"); - assertIgnore(gitIgnore, "logs/important.log"); - } - - @Test - void testEdgeCaseRuleOrdering() { - // Same as above but now the rules in a different order - GitIgnore gitIgnore = new GitIgnore( - "!logs/important.log\n" + - "logs/\n"); - - assertIgnore(gitIgnore, "logs/debug.log"); - assertIgnore(gitIgnore, "logs/important.log"); - } - - @Test - void testEdgeCase2() { - // As generated by the Initializer on https://jmonkeyengine.org/start/ - // Apparently unaware of this issue in git. - GitIgnore gitIgnore = new GitIgnore( - "#Although most of the .idea directory should not be committed there is a legitimate purpose for committing run configurations\n" + - "/.idea/\n" + - "!/.idea/runConfigurations/\n"); - - assertIgnore(gitIgnore, ".idea/ignore.txt"); - assertIgnore(gitIgnore, ".idea/runConfigurations/important.txt"); - } - - // ------------------------------------------ - - @Test - void testGlobStarDirectories() { - // A double asterisk matches zero or more directories. - GitIgnore gitIgnore = new GitIgnore("logs/**/debug.log"); - - assertIgnore(gitIgnore, "logs/debug.log"); - assertIgnore(gitIgnore, "logs/monday/debug.log"); - assertIgnore(gitIgnore, "logs/monday/pm/debug.log"); - } - - // ------------------------------------------ - - @Test - void testDirNameWildcard() { - // Wildcards can be used in directory names as well. - GitIgnore gitIgnore = new GitIgnore("logs/*day/debug.log"); - - assertIgnore(gitIgnore, "logs/monday/debug.log"); - assertIgnore(gitIgnore, "logs/tuesday/debug.log"); - - assertNotIgnore(gitIgnore, "logs/latest/debug.log"); - } - - @Test - void testSubDirectoriesCase() { - GitIgnore gitIgnore = new GitIgnore( - "/dir1/*\n" + - "/dir2/*/*\n" + - "/dir3/*/*/*\n" + - "/dir4/**/*\n" - ); - gitIgnore.setVerbose(true); - assertTrue(gitIgnore.getVerbose()); - LOG.info("GitIgnore:\n{}", gitIgnore); - // NO Subdirs - assertIgnore(gitIgnore, "/dir1/bar.txt"); - assertIgnore(gitIgnore, "/dir1//bar.txt"); // Duplicate / are simplified - assertNotIgnore(gitIgnore, "/dir1/foo/bar.txt"); - assertNotIgnore(gitIgnore, "/dir1/foo/foo/bar.txt"); - assertNotIgnore(gitIgnore, "/dir1/foo/foo/foo/bar.txt"); - - // Exactly 1 Subdir - assertNotIgnore(gitIgnore, "/dir2/bar.txt"); - assertNotIgnore(gitIgnore, "/dir2//bar.txt"); - assertIgnore(gitIgnore, "/dir2/foo/bar.txt"); - assertNotIgnore(gitIgnore, "/dir2/foo/foo/bar.txt"); - assertNotIgnore(gitIgnore, "/dir2/foo/foo/foo/bar.txt"); - - // Exactly 2 Subdirs - assertNotIgnore(gitIgnore, "/dir3/bar.txt"); - assertNotIgnore(gitIgnore, "/dir3//bar.txt"); - assertNotIgnore(gitIgnore, "/dir3/foo/bar.txt"); - assertIgnore(gitIgnore, "/dir3/foo/foo/bar.txt"); - assertIgnore(gitIgnore, "/dir3///foo///foo////bar.txt"); // Duplicate / are simplified - assertNotIgnore(gitIgnore, "/dir3///bar.txt"); - assertNotIgnore(gitIgnore, "/dir3/a//bar.txt"); - assertNotIgnore(gitIgnore, "/dir3//b/bar.txt"); - assertNotIgnore(gitIgnore, "/dir3/foo/foo/foo/bar.txt"); - - // Any Subdirs - assertIgnore(gitIgnore, "/dir4/bar.txt"); - assertIgnore(gitIgnore, "/dir4//bar.txt"); - assertIgnore(gitIgnore, "/dir4/foo/bar.txt"); - assertIgnore(gitIgnore, "/dir4/foo/foo/bar.txt"); - assertIgnore(gitIgnore, "/dir4/foo/foo/foo/bar.txt"); - } - - // ------------------------------------------ - - @Test - void testRootRelative() { - // Patterns specifying a file in a particular directory are relative to the repository root. - // (You can prepend a slash if you like, but it doesn't do anything special.) - GitIgnore gitIgnore = new GitIgnore("logs/debug.log"); - - assertIgnore(gitIgnore, "logs/debug.log"); - - assertNotIgnore(gitIgnore, "debug.log"); - assertNotIgnore(gitIgnore, "build/logs/debug.log"); - - // (You can prepend a slash if you like, but it doesn't do anything special.) - assertIgnore(gitIgnore, "/logs/debug.log"); - - assertNotIgnore(gitIgnore, "/debug.log"); - assertNotIgnore(gitIgnore, "/build/logs/debug.log"); - } - - // ------------------------------------------ - - @Test - void testIgnoreEscapedSpecials() { - GitIgnore gitIgnore = new GitIgnore("foo\\[01\\].txt "); - - assertIgnore(gitIgnore, "foo[01].txt"); - assertNotIgnore(gitIgnore, "foo01.txt"); - assertNotIgnore(gitIgnore, "foo0.txt"); - assertNotIgnore(gitIgnore, "foo1.txt"); - } - - @Test - void testIgnoreBaseDir() { - // To ensure all variants work as expected. - testIgnoreBaseDir("src/test"); - testIgnoreBaseDir("/src/test"); - testIgnoreBaseDir("src/test/"); - testIgnoreBaseDir("/src/test/"); - } - - void testIgnoreBaseDir(String baseDir) { - GitIgnore gitIgnore = new GitIgnore(baseDir, "*.properties"); - gitIgnore.setVerbose(true); - assertIgnore(gitIgnore, "src/test/test.properties"); - assertIgnore(gitIgnore, "/src/test/test.properties"); - - // Note: This actually contains the '/src/test/' !! - assertNotIgnore(gitIgnore, "/somethingelse/src/test/test.properties"); - - // Note: This actually contains the 'src/test' !! - assertNotIgnore(gitIgnore, "src/test.properties"); - - // Note: This actually contains the 'src/test' !! - assertNotIgnore(gitIgnore, "foo/src/test/something.properties"); - - assertNotIgnore(gitIgnore, "src/main/test.properties"); - assertNotIgnore(gitIgnore, "test.properties"); - - // To ensure more code coverage - gitIgnore.setVerbose(false); - assertNotIgnore(gitIgnore, "foo/src/test/something.properties"); - } - - // ------------------------------------------ - - @Test - void testIgnoreFromFile() throws IOException { - URL url = this.getClass() - .getClassLoader() - .getResource("gitignore_fileread"); - - assertNotNull(url); - - GitIgnore gitIgnore = new GitIgnore(new File(url.getFile())); - - gitIgnore.setVerbose(true); - - assertIgnore(gitIgnore, "foo[01].txt"); - assertNotIgnore(gitIgnore, "foo01.txt"); - assertNotIgnore(gitIgnore, "foo0.txt"); - assertNotIgnore(gitIgnore, "foo1.txt"); - assertIgnore(gitIgnore, "src/test/test.properties"); - assertIgnore(gitIgnore, "/src/test/test.properties"); - assertNotIgnore(gitIgnore, "/src/test/bla.properties"); - } - - // ------------------------------------------ - - @Test - void testGeneratedRegexesBasedir() { - // Patterns defined after a negating pattern will re-ignore any previously negated files. - GitIgnore gitIgnore = new GitIgnore( - "*.log\n" + - "!important/*.log\n" + - "trace.* "); - assertIgnore(gitIgnore, "debug.log"); - assertIgnore(gitIgnore, "trace.txt"); - assertIgnore(gitIgnore, "important/trace.log"); - assertNotIgnore(gitIgnore, "important/debug.log"); - - // Verify the created ignore rules. - assertEquals(3, gitIgnore.getIgnoreRules$gitignore_reader().size()); - for (IgnoreRule ignoreRule : gitIgnore.getIgnoreRules$gitignore_reader()) { - switch (ignoreRule.getIgnoreExpression()) { - case "*.log": - assertEquals("^/?.*\\.log(/|$)", ignoreRule.getIgnorePattern().getPattern()); - break; - case "!important/*.log": - assertEquals("^/?important/[^/]*\\.log(/|$)", ignoreRule.getIgnorePattern().getPattern()); - break; - case "trace.*": - assertEquals("^/?(.*/)?trace\\.[^/]*", ignoreRule.getIgnorePattern().getPattern()); - break; - default: - fail("Unexpected expression:" + ignoreRule.getIgnoreExpression()); - } - } - } - - @Test - void testSubdirs() { - assertIgnore( "", "/*.log", "/test.log"); - assertIgnore( "", "/*.log", "/docs/test.log"); - assertIgnore( "", "/*.log", "/src/test.log"); - assertIgnore( "", "/*.log", "/src/main/test.log"); - assertIgnore( "", "/*.log", "/src/test/test.log"); - - assertNotIgnore( "src", "/*.log", "/test.log"); - assertNotIgnore( "src", "/*.log", "/docs/test.log"); - assertIgnore( "src", "/*.log", "/src/test.log"); - assertIgnore( "src", "/*.log", "/src/main/test.log"); - assertIgnore( "src", "/*.log", "/src/test/test.log"); - - assertNotIgnore( "src/main", "/*.log", "/test.log"); - assertNotIgnore( "src/main", "/*.log", "/docs/test.log"); - assertNotIgnore( "src/main", "/*.log", "/src/test.log"); - assertIgnore( "src/main", "/*.log", "/src/main/test.log"); - assertNotIgnore( "src/main", "/*.log", "/src/test/test.log"); - } - - - private void verifyBaseDir(GitIgnore gitIgnore, String baseDir, String matchingFilename) { - assertEquals(baseDir, gitIgnore.projectRelativeBaseDir, "Wrong basedir in GitIgnore"); - for (IgnoreRule ignoreRule : gitIgnore.getIgnoreRules$gitignore_reader()) { - assertEquals(baseDir, ignoreRule.getIgnoreBasedir(), "Wrong basedir in rule"); - } - assertEquals(true, gitIgnore.isIgnoredFile(matchingFilename), "The filename " + matchingFilename + " should have matched("+gitIgnore+")."); - } - - @Test - void testBaseDir() { - verifyBaseDir(new GitIgnore("", "test.md"), "/", "test.md"); - verifyBaseDir(new GitIgnore("/", "test.md"), "/", "test.md"); - - verifyBaseDir(new GitIgnore("foo", "test.md"), "/foo/", "foo/test.md"); - verifyBaseDir(new GitIgnore("/foo", "test.md"), "/foo/", "foo/test.md"); - verifyBaseDir(new GitIgnore("foo/", "test.md"), "/foo/", "foo/test.md"); - verifyBaseDir(new GitIgnore("/foo/", "test.md"), "/foo/", "foo/test.md"); - - verifyBaseDir(new GitIgnore("foo/bar", "test.md"), "/foo/bar/", "foo/bar/test.md"); - verifyBaseDir(new GitIgnore("/foo/bar", "test.md"), "/foo/bar/", "foo/bar/test.md"); - verifyBaseDir(new GitIgnore("foo/bar/", "test.md"), "/foo/bar/", "foo/bar/test.md"); - verifyBaseDir(new GitIgnore("/foo/bar/", "test.md"), "/foo/bar/", "foo/bar/test.md"); - } - - @Test - void testGeneratedRegexesSubdir() { - verifyGeneratedRegex("", "*.txt", "^/?.*\\.txt(/|$)"); - verifyGeneratedRegex("src/", "*.log", "^/?\\Qsrc/\\E.*\\.log(/|$)"); - verifyGeneratedRegex("src/main/", "*.md", "^/?\\Qsrc/main/\\E.*\\.md(/|$)"); - } - - // ------------------------------------------ - - @Test - void testGitDocumentation() { - // From the git documentation: - - // If there is a separator at the beginning or middle (or both) of the pattern, - // then the pattern is relative to the directory level of the particular .gitignore file itself. - // Otherwise, the pattern may also match at any level below the .gitignore level. - GitIgnore gitIgnore; - // a pattern doc/frotz/ matches doc/frotz directory, but not a/doc/frotz directory; - gitIgnore = new GitIgnore("doc/frotz/"); - assertIgnore(gitIgnore, "doc/frotz/"); - assertIgnore(gitIgnore, "doc/frotz/file.txt"); - assertNotIgnore(gitIgnore, "a/doc/frotz/"); - assertNotIgnore(gitIgnore, "a/doc/frotz/file.txt"); - - // however frotz/ matches frotz and a/frotz that is a directory (all paths are relative from the .gitignore file). - gitIgnore = new GitIgnore("frotz/"); - assertIgnore(gitIgnore, "frotz/"); - assertIgnore(gitIgnore, "frotz/file.txt"); - assertIgnore(gitIgnore, "a/frotz/"); - assertIgnore(gitIgnore, "a/frotz/file.txt"); - - // Beginning - gitIgnore = new GitIgnore("/frotz"); - assertIgnore(gitIgnore, "frotz/"); - assertIgnore(gitIgnore, "frotz/file.txt"); - assertNotIgnore(gitIgnore, "a/frotz/"); - assertNotIgnore(gitIgnore, "a/frotz/file.txt"); - - // Both - gitIgnore = new GitIgnore("/doc/frotz/"); - assertIgnore(gitIgnore, "doc/frotz/"); - assertIgnore(gitIgnore, "doc/frotz/file.txt"); - assertNotIgnore(gitIgnore, "a/doc/frotz/"); - assertNotIgnore(gitIgnore, "a/doc/frotz/file.txt"); - } - - @Test - void testGitDocumentationSlashesAndStars() { - // From the git documentation: - GitIgnore gitIgnore; - - // Put a backslash ("\") in front of the first "!" for patterns that - // begin with a literal "!", for example, "\!important!.txt". - gitIgnore = new GitIgnore("\\!important!.txt"); - assertIgnore(gitIgnore, "!important!.txt"); - - // An asterisk "*" matches anything except a slash. - gitIgnore = new GitIgnore("foo*bar"); - assertIgnore(gitIgnore, "foo_bar"); - assertNotIgnore(gitIgnore, "foo/bar"); - assertIgnore(gitIgnore, "foo_____bar"); - assertNotIgnore(gitIgnore, "foo__/__bar"); - - // The character "?" matches any one character except "/". - gitIgnore = new GitIgnore("foo?bar"); - assertIgnore(gitIgnore, "foo_bar"); - assertIgnore(gitIgnore, "foo.bar"); - assertNotIgnore(gitIgnore, "foo/bar"); - } - - @Test - void testEmptyIgnoreRules() { - GitIgnore gitIgnore = new GitIgnore(""); - assertNotIgnore(gitIgnore, "debug.log"); - assertNotIgnore(gitIgnore, "logs/debug.log"); - } - - @Test - void testNullIgnoreRules() { - assertThrows(NullPointerException.class, () -> { - new GitIgnore((String)null); // Deliberately passing a null value which should fail. - }); - } - - @Test - void testBaseDirNull() { - IgnoreRule rule = new IgnoreRule(null, false, "debug.log", true); - assertTrueAndNotNull(rule.isIgnoredFile("debug.log")); - assertTrueAndNotNull(rule.isIgnoredFile("logs/debug.log")); - } - - @Test - void testBaseDirEmpty() { - IgnoreRule rule = new IgnoreRule("", false, "debug.log", true); - assertTrueAndNotNull(rule.isIgnoredFile("debug.log")); - assertTrueAndNotNull(rule.isIgnoredFile("logs/debug.log")); - } - - @Test - void testBaseDirRoot() { - IgnoreRule rule = new IgnoreRule("/", false, "debug.log", true); - assertTrueAndNotNull(rule.isIgnoredFile("debug.log")); - assertTrueAndNotNull(rule.isIgnoredFile("logs/debug.log")); - } - - @Test - void testBadExpression() { - assertThrows(PatternSyntaxException.class, () -> new GitIgnore("[[[[[***+++")); - } - - private void assertTrueAndNotNull(Boolean condition) { - assertNotNull(condition); - assertTrue(condition); - } - -} diff --git a/gitignore-reader/src/test/kotlin/nl/basjes/gitignore/TestGitIgnore.kt b/gitignore-reader/src/test/kotlin/nl/basjes/gitignore/TestGitIgnore.kt new file mode 100644 index 0000000..6826e6a --- /dev/null +++ b/gitignore-reader/src/test/kotlin/nl/basjes/gitignore/TestGitIgnore.kt @@ -0,0 +1,610 @@ +/* + * CodeOwners Tools + * Copyright (C) 2023-2026 Niels Basjes + * + * 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 + * + * https://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 nl.basjes.gitignore + +import nl.basjes.gitignore.TestUtils.assertIgnore +import kotlin.test.Test +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.io.File +import java.io.IOException +import java.util.regex.PatternSyntaxException +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertNotNull +import kotlin.test.assertTrue +import kotlin.test.fail + +internal class TestGitIgnore { + @Test + fun testFullRangeExpression() { + // This expression contains all features described in the examples below to ensure the parser can handle it + val input = + "*.log\n" + + "!\\#important?/debug[0-9]/debug[!01]/**/*debug[a-z]/*.log" + val gitIgnore = GitIgnore(input) + assertIgnore(gitIgnore, "logs/debug.log") + TestUtils.assertNullMatch( + gitIgnore, + "#important_/debug4/debug4/something/something/local_debugb/Something.logxxx" + ) + LOG.info("Input:\n{}\nParsed:\n{}\n", input, gitIgnore) + } + + // ------------------------------------------ + // I used the examples provided by Atlassian in their gitignore tutorial as test cases + // https://www.atlassian.com/git/tutorials/saving-changes/gitignore + // This documentation is licensed via https://creativecommons.org/licenses/by/2.5/au/ + @Test + fun testPrependGlobstar1() { + // You can prepend a pattern with a double asterisk to match directories anywhere in the repository. + val gitIgnore = GitIgnore("**/logs") + + assertIgnore(gitIgnore, "logs/debug.log") + assertIgnore(gitIgnore, "logs/monday/foo.bar") + assertIgnore(gitIgnore, "build/logs/debug.log") + } + + // ------------------------------------------ + @Test + fun testPrependGlobstar2() { + //You can also use a double asterisk to match files based on their name and the name of their parent directory. + val gitIgnore = GitIgnore("**/logs/debug.log") + + assertIgnore(gitIgnore, "logs/debug.log") + assertIgnore(gitIgnore, "build/logs/debug.log") + + TestUtils.assertNotIgnore(gitIgnore, "logs/build/debug.log") + } + + // ------------------------------------------ + @Test + fun testWildcard() { + //An asterisk is a wildcard that matches zero or more characters. + val gitIgnore = GitIgnore("*.log") + + assertIgnore(gitIgnore, "debug.log") + assertIgnore(gitIgnore, "foo.log") + assertIgnore(gitIgnore, ".log") + assertIgnore(gitIgnore, "logs/debug.log") + } + + // ------------------------------------------ + @Test + fun testNegate() { + // Prepending an exclamation mark to a pattern negates it. + // If a file matches a pattern, but also matches a negating pattern defined later in the file, it will not be ignored. + val gitIgnore = GitIgnore( + "*.log\n" + + "!important.log" + ) + + assertIgnore(gitIgnore, "debug.log") + assertIgnore(gitIgnore, "trace.log") + + TestUtils.assertNotIgnore(gitIgnore, "important.log") + TestUtils.assertNotIgnore(gitIgnore, "logs/important.log") + } + + // ------------------------------------------ + @Test + fun testNegateAndReignore() { + // Patterns defined after a negating pattern will re-ignore any previously negated files. + val gitIgnore = GitIgnore( + "*.log\n" + + "!important/*.log\n" + + "trace.* " + ) + assertIgnore(gitIgnore, "debug.log") + assertIgnore(gitIgnore, "important/trace.log") + + TestUtils.assertNotIgnore(gitIgnore, "important/debug.log") + } + + // ------------------------------------------ + @Test + fun testPinRoot() { + // Prepending a slash matches files only in the repository root. + val gitIgnore = GitIgnore("/debug.log ") + assertIgnore(gitIgnore, "debug.log") + + TestUtils.assertNotIgnore(gitIgnore, "logs/debug.log") + } + + // ------------------------------------------ + @Test + fun testDefaultAnyDirectory() { + // By default, patterns match files in any directory + val gitIgnore = GitIgnore("debug.log") + assertIgnore(gitIgnore, "debug.log") + assertIgnore(gitIgnore, "logs/debug.log") + } + + // ------------------------------------------ + @Test + fun testSingleCharWildcard() { + // A question mark matches exactly one character. + val gitIgnore = GitIgnore("debug?.log ") + assertIgnore(gitIgnore, "debug0.log") + assertIgnore(gitIgnore, "debugg.log") + + TestUtils.assertNotIgnore(gitIgnore, "debug10.log") + } + + // ------------------------------------------ + @Test + fun testCharRange() { + // Square brackets can also be used to match a single character from a specified range. + val gitIgnore = GitIgnore("debug[0-9].log ") + + assertIgnore(gitIgnore, "debug0.log") + assertIgnore(gitIgnore, "debug1.log") + + TestUtils.assertNotIgnore(gitIgnore, "debug10.log") + } + + // ------------------------------------------ + @Test + fun testCharSet() { + // Square brackets match a single character form the specified set. + val gitIgnore = GitIgnore("debug[01].log") + + assertIgnore(gitIgnore, "debug0.log") + assertIgnore(gitIgnore, "debug1.log") + + TestUtils.assertNotIgnore(gitIgnore, "debug2.log") + TestUtils.assertNotIgnore(gitIgnore, "debug01.log") + } + + // ------------------------------------------ + @Test + fun testNotCharSet() { + // An exclamation mark can be used to match any character except one from the specified set. + val gitIgnore = GitIgnore("debug[!01].log ") + + assertIgnore(gitIgnore, "debug2.log") + + TestUtils.assertNotIgnore(gitIgnore, "debug0.log") + TestUtils.assertNotIgnore(gitIgnore, "debug1.log") + TestUtils.assertNotIgnore(gitIgnore, "debug01.log") + } + + // ------------------------------------------ + @Test + fun testCharRangeAlphabetical() { + // Ranges can be numeric or alphabetic. + val gitIgnore = GitIgnore("debug[a-z].log ") + + assertIgnore(gitIgnore, "debuga.log") + assertIgnore(gitIgnore, "debugb.log") + + TestUtils.assertNotIgnore(gitIgnore, "debug1.log") + } + + // ------------------------------------------ + @Test + fun testNoSlashMatchDirsAndFiles() { + // If you don't append a slash, the pattern will match both files and the contents of directories with that name. + // In the example matches on the left, both directories and files named logs are ignored + val gitIgnore = GitIgnore("logs") + assertIgnore(gitIgnore, "logs") + assertIgnore(gitIgnore, "logs/debug.log") + assertIgnore(gitIgnore, "logs/latest/foo.bar") + assertIgnore(gitIgnore, "build/logs") + assertIgnore(gitIgnore, "build/logs/debug.log") + } + + // ------------------------------------------ + @Test + fun testAppendSlash() { + // Appending a slash indicates the pattern is a directory. + // The entire contents of any directory in the repository matching that name – + // including all of its files and subdirectories – will be ignored + val gitIgnore = GitIgnore("logs/") + + assertIgnore(gitIgnore, "logs/") + assertIgnore(gitIgnore, "logs/debug.log") + assertIgnore(gitIgnore, "logs/latest/foo.bar") + assertIgnore(gitIgnore, "build/logs/foo.bar") + assertIgnore(gitIgnore, "build/logs/latest/debug.log") + } + + // ------------------------------------------ + @Test + fun testEdgeCase() { + // From https://www.atlassian.com/git/tutorials/saving-changes/gitignore + // Wait a minute! Shouldn't logs/important.log be negated in the example on the left? + // Nope! Due to a performance-related quirk in Git, you can not negate a file + // that is ignored due to a pattern matching a directory + val gitIgnore = GitIgnore( + "logs/\n" + + "!logs/important.log\n" + ) + + assertIgnore(gitIgnore, "logs/debug.log") + assertIgnore(gitIgnore, "logs/important.log") + } + + @Test + fun testEdgeCaseRuleOrdering() { + // Same as above but now the rules in a different order + val gitIgnore = GitIgnore( + "!logs/important.log\n" + + "logs/\n" + ) + + assertIgnore(gitIgnore, "logs/debug.log") + assertIgnore(gitIgnore, "logs/important.log") + } + + @Test + fun testEdgeCase2() { + // As generated by the Initializer on https://jmonkeyengine.org/start/ + // Apparently unaware of this issue in git. + val gitIgnore = GitIgnore( + "#Although most of the .idea directory should not be committed there is a legitimate purpose for committing run configurations\n" + + "/.idea/\n" + + "!/.idea/runConfigurations/\n" + ) + + assertIgnore(gitIgnore, ".idea/ignore.txt") + assertIgnore(gitIgnore, ".idea/runConfigurations/important.txt") + } + + // ------------------------------------------ + @Test + fun testGlobStarDirectories() { + // A double asterisk matches zero or more directories. + val gitIgnore = GitIgnore("logs/**/debug.log") + + assertIgnore(gitIgnore, "logs/debug.log") + assertIgnore(gitIgnore, "logs/monday/debug.log") + assertIgnore(gitIgnore, "logs/monday/pm/debug.log") + } + + // ------------------------------------------ + @Test + fun testDirNameWildcard() { + // Wildcards can be used in directory names as well. + val gitIgnore = GitIgnore("logs/*day/debug.log") + + assertIgnore(gitIgnore, "logs/monday/debug.log") + assertIgnore(gitIgnore, "logs/tuesday/debug.log") + + TestUtils.assertNotIgnore(gitIgnore, "logs/latest/debug.log") + } + + @Test + fun testSubDirectoriesCase() { + val gitIgnore = GitIgnore( + "/dir1/*\n" + + "/dir2/*/*\n" + + "/dir3/*/*/*\n" + + "/dir4/**/*\n" + ) + gitIgnore.verbose = true + assertTrue(gitIgnore.verbose) + LOG.info("GitIgnore:\n{}", gitIgnore) + // NO Subdirs + assertIgnore(gitIgnore, "/dir1/bar.txt") + assertIgnore(gitIgnore, "/dir1//bar.txt") // Duplicate / are simplified + TestUtils.assertNotIgnore(gitIgnore, "/dir1/foo/bar.txt") + TestUtils.assertNotIgnore(gitIgnore, "/dir1/foo/foo/bar.txt") + TestUtils.assertNotIgnore(gitIgnore, "/dir1/foo/foo/foo/bar.txt") + + // Exactly 1 Subdir + TestUtils.assertNotIgnore(gitIgnore, "/dir2/bar.txt") + TestUtils.assertNotIgnore(gitIgnore, "/dir2//bar.txt") + assertIgnore(gitIgnore, "/dir2/foo/bar.txt") + TestUtils.assertNotIgnore(gitIgnore, "/dir2/foo/foo/bar.txt") + TestUtils.assertNotIgnore(gitIgnore, "/dir2/foo/foo/foo/bar.txt") + + // Exactly 2 Subdirs + TestUtils.assertNotIgnore(gitIgnore, "/dir3/bar.txt") + TestUtils.assertNotIgnore(gitIgnore, "/dir3//bar.txt") + TestUtils.assertNotIgnore(gitIgnore, "/dir3/foo/bar.txt") + assertIgnore(gitIgnore, "/dir3/foo/foo/bar.txt") + assertIgnore(gitIgnore, "/dir3///foo///foo////bar.txt") // Duplicate / are simplified + TestUtils.assertNotIgnore(gitIgnore, "/dir3///bar.txt") + TestUtils.assertNotIgnore(gitIgnore, "/dir3/a//bar.txt") + TestUtils.assertNotIgnore(gitIgnore, "/dir3//b/bar.txt") + TestUtils.assertNotIgnore(gitIgnore, "/dir3/foo/foo/foo/bar.txt") + + // Any Subdirs + assertIgnore(gitIgnore, "/dir4/bar.txt") + assertIgnore(gitIgnore, "/dir4//bar.txt") + assertIgnore(gitIgnore, "/dir4/foo/bar.txt") + assertIgnore(gitIgnore, "/dir4/foo/foo/bar.txt") + assertIgnore(gitIgnore, "/dir4/foo/foo/foo/bar.txt") + } + + // ------------------------------------------ + @Test + fun testRootRelative() { + // Patterns specifying a file in a particular directory are relative to the repository root. + // (You can prepend a slash if you like, but it doesn't do anything special.) + val gitIgnore = GitIgnore("logs/debug.log") + + assertIgnore(gitIgnore, "logs/debug.log") + + TestUtils.assertNotIgnore(gitIgnore, "debug.log") + TestUtils.assertNotIgnore(gitIgnore, "build/logs/debug.log") + + // (You can prepend a slash if you like, but it doesn't do anything special.) + assertIgnore(gitIgnore, "/logs/debug.log") + + TestUtils.assertNotIgnore(gitIgnore, "/debug.log") + TestUtils.assertNotIgnore(gitIgnore, "/build/logs/debug.log") + } + + // ------------------------------------------ + @Test + fun testIgnoreEscapedSpecials() { + val gitIgnore = GitIgnore("foo\\[01\\].txt ") + + assertIgnore(gitIgnore, "foo[01].txt") + TestUtils.assertNotIgnore(gitIgnore, "foo01.txt") + TestUtils.assertNotIgnore(gitIgnore, "foo0.txt") + TestUtils.assertNotIgnore(gitIgnore, "foo1.txt") + } + + @Test + fun testIgnoreBaseDir() { + // To ensure all variants work as expected. + testIgnoreBaseDir("src/test") + testIgnoreBaseDir("/src/test") + testIgnoreBaseDir("src/test/") + testIgnoreBaseDir("/src/test/") + } + + fun testIgnoreBaseDir(baseDir: String) { + val gitIgnore = GitIgnore(baseDir, "*.properties") + gitIgnore.verbose = true + assertIgnore(gitIgnore, "src/test/test.properties") + assertIgnore(gitIgnore, "/src/test/test.properties") + + // Note: This actually contains the '/src/test/' + TestUtils.assertNotIgnore(gitIgnore, "/somethingelse/src/test/test.properties") + + // Note: This actually contains the 'src/test' + TestUtils.assertNotIgnore(gitIgnore, "src/test.properties") + + // Note: This actually contains the 'src/test' + TestUtils.assertNotIgnore(gitIgnore, "foo/src/test/something.properties") + + TestUtils.assertNotIgnore(gitIgnore, "src/main/test.properties") + TestUtils.assertNotIgnore(gitIgnore, "test.properties") + + // To ensure more code coverage + gitIgnore.verbose = false + TestUtils.assertNotIgnore(gitIgnore, "foo/src/test/something.properties") + } + + // ------------------------------------------ + @Test + @Throws(IOException::class) + fun testIgnoreFromFile() { + val url = this.javaClass + .classLoader + .getResource("gitignore_fileread") + + assertNotNull(url) + + val gitIgnore = GitIgnore(File(url.file)) + + gitIgnore.verbose = true + + assertIgnore(gitIgnore, "foo[01].txt") + TestUtils.assertNotIgnore(gitIgnore, "foo01.txt") + TestUtils.assertNotIgnore(gitIgnore, "foo0.txt") + TestUtils.assertNotIgnore(gitIgnore, "foo1.txt") + assertIgnore(gitIgnore, "src/test/test.properties") + assertIgnore(gitIgnore, "/src/test/test.properties") + TestUtils.assertNotIgnore(gitIgnore, "/src/test/bla.properties") + } + + // ------------------------------------------ + @Test + fun testGeneratedRegexesBasedir() { + // Patterns defined after a negating pattern will re-ignore any previously negated files. + val gitIgnore = GitIgnore( + "*.log\n" + + "!important/*.log\n" + + "trace.* " + ) + assertIgnore(gitIgnore, "debug.log") + assertIgnore(gitIgnore, "trace.txt") + assertIgnore(gitIgnore, "important/trace.log") + TestUtils.assertNotIgnore(gitIgnore, "important/debug.log") + + // Verify the created ignore rules. + assertEquals(3, gitIgnore.ignoreRules.size) + for (ignoreRule in gitIgnore.ignoreRules) { + when (ignoreRule.ignoreExpression) { + "*.log" -> + assertEquals("^/?.*\\.log(/|$)", ignoreRule.ignorePattern.pattern) + "!important/*.log" -> + assertEquals("^/?important/[^/]*\\.log(/|$)", ignoreRule.ignorePattern.pattern) + + "trace.*" -> assertEquals("^/?(.*/)?trace\\.[^/]*", ignoreRule.ignorePattern.pattern) + else -> fail("Unexpected expression:${ignoreRule.ignoreExpression}") + } + } + } + + @Test + fun testSubdirs() { + assertIgnore("", "/*.log", "/test.log") + assertIgnore("", "/*.log", "/docs/test.log") + assertIgnore("", "/*.log", "/src/test.log") + assertIgnore("", "/*.log", "/src/main/test.log") + assertIgnore("", "/*.log", "/src/test/test.log") + + TestUtils.assertNotIgnore("src", "/*.log", "/test.log") + TestUtils.assertNotIgnore("src", "/*.log", "/docs/test.log") + assertIgnore("src", "/*.log", "/src/test.log") + assertIgnore("src", "/*.log", "/src/main/test.log") + assertIgnore("src", "/*.log", "/src/test/test.log") + + TestUtils.assertNotIgnore("src/main", "/*.log", "/test.log") + TestUtils.assertNotIgnore("src/main", "/*.log", "/docs/test.log") + TestUtils.assertNotIgnore("src/main", "/*.log", "/src/test.log") + assertIgnore("src/main", "/*.log", "/src/main/test.log") + TestUtils.assertNotIgnore("src/main", "/*.log", "/src/test/test.log") + } + + + private fun verifyBaseDir(gitIgnore: GitIgnore, baseDir: String?, matchingFilename: String) { + assertEquals(baseDir, gitIgnore.projectRelativeBaseDir, "Wrong basedir in GitIgnore") + for (ignoreRule in gitIgnore.ignoreRules) { + assertEquals(baseDir, ignoreRule.ignoreBasedir, "Wrong basedir in rule") + } + assertEquals( + true, + gitIgnore.isIgnoredFile(matchingFilename), + "The filename $matchingFilename should have matched($gitIgnore)." + ) + } + + @Test + fun testBaseDir() { + verifyBaseDir(GitIgnore("", "test.md"), "/", "test.md") + verifyBaseDir(GitIgnore("/", "test.md"), "/", "test.md") + + verifyBaseDir(GitIgnore("foo", "test.md"), "/foo/", "foo/test.md") + verifyBaseDir(GitIgnore("/foo", "test.md"), "/foo/", "foo/test.md") + verifyBaseDir(GitIgnore("foo/", "test.md"), "/foo/", "foo/test.md") + verifyBaseDir(GitIgnore("/foo/", "test.md"), "/foo/", "foo/test.md") + + verifyBaseDir(GitIgnore("foo/bar", "test.md"), "/foo/bar/", "foo/bar/test.md") + verifyBaseDir(GitIgnore("/foo/bar", "test.md"), "/foo/bar/", "foo/bar/test.md") + verifyBaseDir(GitIgnore("foo/bar/", "test.md"), "/foo/bar/", "foo/bar/test.md") + verifyBaseDir(GitIgnore("/foo/bar/", "test.md"), "/foo/bar/", "foo/bar/test.md") + } + + @Test + fun testGeneratedRegexesSubdir() { + TestUtils.verifyGeneratedRegex("", "*.txt", "^/?.*\\.txt(/|$)") + TestUtils.verifyGeneratedRegex("src/", "*.log", "^/?\\Qsrc/\\E.*\\.log(/|$)") + TestUtils.verifyGeneratedRegex("src/main/", "*.md", "^/?\\Qsrc/main/\\E.*\\.md(/|$)") + } + + // ------------------------------------------ + @Test + fun testGitDocumentation() { + // From the git documentation: + + // If there is a separator at the beginning or middle (or both) of the pattern, + // then the pattern is relative to the directory level of the particular .gitignore file itself. + // Otherwise, the pattern may also match at any level below the .gitignore level. + + var gitIgnore: GitIgnore? + // a pattern doc/frotz/ matches doc/frotz directory, but not a/doc/frotz directory; + gitIgnore = GitIgnore("doc/frotz/") + assertIgnore(gitIgnore, "doc/frotz/") + assertIgnore(gitIgnore, "doc/frotz/file.txt") + TestUtils.assertNotIgnore(gitIgnore, "a/doc/frotz/") + TestUtils.assertNotIgnore(gitIgnore, "a/doc/frotz/file.txt") + + // however frotz/ matches frotz and a/frotz that is a directory (all paths are relative from the .gitignore file). + gitIgnore = GitIgnore("frotz/") + assertIgnore(gitIgnore, "frotz/") + assertIgnore(gitIgnore, "frotz/file.txt") + assertIgnore(gitIgnore, "a/frotz/") + assertIgnore(gitIgnore, "a/frotz/file.txt") + + // Beginning + gitIgnore = GitIgnore("/frotz") + assertIgnore(gitIgnore, "frotz/") + assertIgnore(gitIgnore, "frotz/file.txt") + TestUtils.assertNotIgnore(gitIgnore, "a/frotz/") + TestUtils.assertNotIgnore(gitIgnore, "a/frotz/file.txt") + + // Both + gitIgnore = GitIgnore("/doc/frotz/") + assertIgnore(gitIgnore, "doc/frotz/") + assertIgnore(gitIgnore, "doc/frotz/file.txt") + TestUtils.assertNotIgnore(gitIgnore, "a/doc/frotz/") + TestUtils.assertNotIgnore(gitIgnore, "a/doc/frotz/file.txt") + } + + @Test + fun testGitDocumentationSlashesAndStars() { + // From the git documentation: + var gitIgnore: GitIgnore + + // Put a backslash ("\") in front of the first "!" for patterns that + // begin with a literal "!", for example, "\!important!.txt". + gitIgnore = GitIgnore("\\!important!.txt") + assertIgnore(gitIgnore, "!important!.txt") + + // An asterisk "*" matches anything except a slash. + gitIgnore = GitIgnore("foo*bar") + assertIgnore(gitIgnore, "foo_bar") + TestUtils.assertNotIgnore(gitIgnore, "foo/bar") + assertIgnore(gitIgnore, "foo_____bar") + TestUtils.assertNotIgnore(gitIgnore, "foo__/__bar") + + // The character "?" matches any one character except "/". + gitIgnore = GitIgnore("foo?bar") + assertIgnore(gitIgnore, "foo_bar") + assertIgnore(gitIgnore, "foo.bar") + TestUtils.assertNotIgnore(gitIgnore, "foo/bar") + } + + @Test + fun testEmptyIgnoreRules() { + val gitIgnore = GitIgnore("") + TestUtils.assertNotIgnore(gitIgnore, "debug.log") + TestUtils.assertNotIgnore(gitIgnore, "logs/debug.log") + } + + @Test + fun testBaseDirNull() { + val rule = GitIgnore.IgnoreRule(null, false, "debug.log", true) + assertTrueAndNotNull(rule.isIgnoredFile("debug.log")) + assertTrueAndNotNull(rule.isIgnoredFile("logs/debug.log")) + } + + @Test + fun testBaseDirEmpty() { + val rule = GitIgnore.IgnoreRule("", false, "debug.log", true) + assertTrueAndNotNull(rule.isIgnoredFile("debug.log")) + assertTrueAndNotNull(rule.isIgnoredFile("logs/debug.log")) + } + + @Test + fun testBaseDirRoot() { + val rule = GitIgnore.IgnoreRule("/", false, "debug.log", true) + assertTrueAndNotNull(rule.isIgnoredFile("debug.log")) + assertTrueAndNotNull(rule.isIgnoredFile("logs/debug.log")) + } + + @Test + fun testBadExpression() { + assertFailsWith { GitIgnore("[[[[[***+++") } + } + + private fun assertTrueAndNotNull(condition: Boolean?) { + assertNotNull(condition) + assertTrue(condition) + } + + companion object { + private val LOG: Logger = LoggerFactory.getLogger(TestGitIgnore::class.java) + } +} diff --git a/gitignore-reader/src/test/kotlin/nl/basjes/gitignore/TestGitIgnoreFiles.java b/gitignore-reader/src/test/kotlin/nl/basjes/gitignore/TestGitIgnoreFiles.java deleted file mode 100644 index fe8e3d9..0000000 --- a/gitignore-reader/src/test/kotlin/nl/basjes/gitignore/TestGitIgnoreFiles.java +++ /dev/null @@ -1,582 +0,0 @@ -/* - * CodeOwners Tools - * Copyright (C) 2023-2026 Niels Basjes - * - * 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 - * - * https://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 nl.basjes.gitignore; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.DisabledOnOs; -import org.junitpioneer.jupiter.ClearEnvironmentVariable; -import org.junitpioneer.jupiter.EnvironmentVariableUtilsFacade; -import org.junitpioneer.jupiter.SetEnvironmentVariable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.File; -import java.io.IOException; -import java.lang.reflect.Method; -import java.net.URL; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static nl.basjes.gitignore.GitIgnore.standardizeFilename; -import static nl.basjes.gitignore.Utils.findAllNonIgnored; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; -import static org.junit.jupiter.api.condition.OS.WINDOWS; - -class TestGitIgnoreFiles { - - private static final Logger LOG = LoggerFactory.getLogger(TestGitIgnoreFiles.class); - - public static String separatorsToWindows(String path) { - return path.replace("/", "\\"); - } - final List expectedKeepFiles = Stream.of( - "/", - "/dir1", - "/dir1/dir1.md", - "/dir1/.gitignore", - "/dir2", - "/dir2/dir2.txt", - "/dir2/file2.log", - "/dir3", - "/dir3/dir3.txt", - "/dir3/file3.log", - "/dir3/.gitignore", - "/dir5", - "/dir5/file5.log", - "/.gitignore" - ).sorted().collect(Collectors.toList()); - - final List expectedIgnoredFiles = Stream.of( - "/dir1/dir1.log", - "/dir1/dir1.txt", - "/dir1/file1.log", - "/dir2/dir2.log", - "/dir2/dir2.md", - "/dir3/dir3.log", - "/dir3/dir3.md", - "/dir4/.gitignore", - "/dir4/dir4.log", - "/dir4/dir4.md", - "/dir4/dir4.txt", - "/dir4/file4.log", - "/dir4/subdir/.gitignore", - "/dir4/subdir/dir1.log", - "/dir4/subdir/dir1.md", - "/dir4/subdir/dir1.txt", - "/dir4/subdir/file1.log", - "/dir4/subdir/subdir/.gitignore", - "/dir4/subdir/subdir/dir1.log", - "/dir4/subdir/subdir/dir1.md", - "/dir4/subdir/subdir/dir1.txt", - "/dir4/subdir/subdir/file1.log", - "/dir5/ignored_from_global_gitignore", - "/README.md", - "/root.md").sorted().collect(Collectors.toList()); - - private List stripTestTreeBaseDir(List input) { - return input - .stream() - .map(Path::toString) - .map(GitIgnore::standardizeFilename) // Needed for testing on Windows. - .map(path -> path.replaceAll("^\\Q/"+testTreeDir+"\\E", "/").replace("//", "/")) - .sorted() - .collect(Collectors.toList()); - } - - private String stripTestTreeBaseDir(Path input) { - return standardizeFilename(input.toString()).replaceAll("^\\Q/"+testTreeDir+"\\E", "/").replace("//", "/"); - } - - static final String testTreeDir = "src/test/resources/testtree"; - static final File testTree = new File(testTreeDir); - static final File targetDirTestTree = new File(Objects.requireNonNull(TestGitIgnoreFiles.class.getClassLoader().getResource("testtree")).getFile()); - - @Test - void ensureExpectationsDoNotOverlap() { - for (String expectedKeepFile : expectedKeepFiles) { - assertFalse(expectedIgnoredFiles.contains(expectedKeepFile), "Expected Ignores has " + expectedKeepFile); - } - for (String expectedIgnoreFile : expectedIgnoredFiles) { - assertFalse(expectedKeepFiles.contains(expectedIgnoreFile), "Expected Keeps has " + expectedIgnoreFile); - } - } - - @Test - void ensureAllFilesInTestTreeAreEitherInKeepOrIgnore() throws IOException { - try (Stream projectFiles = Files.find(testTree.toPath(), 128, (filePath, fileAttr) -> fileAttr.isRegularFile())){ - for (Path path : projectFiles.sorted().collect(Collectors.toList())) { - String name = stripTestTreeBaseDir(path); - assertTrue(expectedIgnoredFiles.contains(name) || expectedKeepFiles.contains(name), "Missing entry for " + name); - } - } - } - - private void checkIgnoredList(List ignore) { - checkIgnoredList(expectedIgnoredFiles, ignore); - } - private void checkIgnoredList(List expectedIgnore, List ignore) { - List ignored = ignore - .stream() - .map(filename -> standardizeFilename(new File(filename).getAbsolutePath())) // Needed for testing on Windows. - .map(filename -> filename.replaceAll("^\\Q"+ standardizeFilename(testTree.getAbsolutePath()) + "\\E", "")) - .sorted() - .collect(Collectors.toList()); - assertEquals(expectedIgnore, ignored); - } - - @Test - void testGetGlobalGitIgnore() throws Exception { - assertNull(GitIgnoreFileSet.getGlobalGitIgnore(null, null)); - assertNull(GitIgnoreFileSet.getGlobalGitIgnore(null, "")); - assertNull(GitIgnoreFileSet.getGlobalGitIgnore("", null)); - assertNull(GitIgnoreFileSet.getGlobalGitIgnore("", "")); - - URL dirWithGitIgnoreURL = this.getClass() - .getClassLoader() - .getResource("xdg_config_home"); - assertNotNull(dirWithGitIgnoreURL); - String dirWithGitIgnore = dirWithGitIgnoreURL.getFile(); - - URL dirWithConfigGitIgnoreURL = this.getClass() - .getClassLoader() - .getResource("home"); - assertNotNull(dirWithConfigGitIgnoreURL); - String dirWithConfigGitIgnore = dirWithConfigGitIgnoreURL.getFile(); - - assertNull(GitIgnoreFileSet.getGlobalGitIgnore(dirWithConfigGitIgnore, "")); - assertNull(GitIgnoreFileSet.getGlobalGitIgnore("", dirWithGitIgnore)); - - assertEquals(Paths.get(dirWithGitIgnoreURL.toURI()).resolve("git").resolve("ignore"), GitIgnoreFileSet.getGlobalGitIgnore(dirWithGitIgnore, "")); - assertEquals(Paths.get(dirWithGitIgnoreURL.toURI()).resolve("git").resolve("ignore"), GitIgnoreFileSet.getGlobalGitIgnore(dirWithGitIgnore, dirWithConfigGitIgnore)); - - assertEquals(Paths.get(dirWithConfigGitIgnoreURL.toURI()).resolve(".config").resolve("git").resolve("ignore"), GitIgnoreFileSet.getGlobalGitIgnore(null, dirWithConfigGitIgnore)); - assertNull(GitIgnoreFileSet.getGlobalGitIgnore(dirWithConfigGitIgnore, dirWithConfigGitIgnore)); - } - - @Test - @DisabledOnOs(WINDOWS) - void testGetGlobalGitIgnoreUnreadableFiles() throws Exception { - URL dirWithGitIgnoreURL = this.getClass() - .getClassLoader() - .getResource("unreadable/.config"); - assertNotNull(dirWithGitIgnoreURL); - String dirWithGitIgnore = dirWithGitIgnoreURL.getFile(); - - URL dirWithConfigGitIgnoreURL = this.getClass() - .getClassLoader() - .getResource("unreadable"); - assertNotNull(dirWithConfigGitIgnoreURL); - String dirWithConfigGitIgnore = dirWithConfigGitIgnoreURL.getFile(); - - File ignoreFileToMakeUnreadable = new File(dirWithGitIgnore + "/git/ignore"); - - Path expectedIgnorePath = Paths.get(dirWithGitIgnoreURL.toURI()).resolve("git").resolve("ignore"); - - assertTrue(ignoreFileToMakeUnreadable.isFile(), "This " + ignoreFileToMakeUnreadable + " should be a File."); - assertTrue(ignoreFileToMakeUnreadable.canRead(), "This " + ignoreFileToMakeUnreadable + " should be readable."); - - // Use Pioneer's underlying utility to inject it programmatically - EnvironmentVariableUtilsFacade.set("XDG_CONFIG_HOME", dirWithGitIgnore); - EnvironmentVariableUtilsFacade.set("HOME", dirWithConfigGitIgnore); - - try { - LOG.info("Making {} unreadable", ignoreFileToMakeUnreadable); - assertTrue(ignoreFileToMakeUnreadable.setReadable(false, false)); - assertTrue(ignoreFileToMakeUnreadable.isFile(), "This " + ignoreFileToMakeUnreadable + " should be a File."); - assertFalse(ignoreFileToMakeUnreadable.canRead(), "This " + ignoreFileToMakeUnreadable + " should NO LONGER be readable."); - - // Check 1: Even if the file is not readable it should be found - assertEquals(expectedIgnorePath, GitIgnoreFileSet.getGlobalGitIgnore(null, dirWithConfigGitIgnore)); - assertEquals(expectedIgnorePath, GitIgnoreFileSet.getGlobalGitIgnore(dirWithGitIgnore, null)); - - // Check 2: When trying to use it in a GitIgnoreFileSet it should all fail (for code coverage). - GitIgnoreFileSet gitIgnoreFileSet = new GitIgnoreFileSet(targetDirTestTree, false); - gitIgnoreFileSet.addAllGitIgnoreFiles(true); - } - finally { - assertTrue(ignoreFileToMakeUnreadable.setReadable(true, false)); - assertTrue(ignoreFileToMakeUnreadable.canRead(), "This " + ignoreFileToMakeUnreadable + " should be readable."); - } - } - - - @Test - @DisabledOnOs(WINDOWS) - void testUnreadableDirectory() throws Exception { - URL testTreeURL = this.getClass() - .getClassLoader() - .getResource("testtree"); - assertNotNull(testTreeURL); - - Path directoryToMakeUnreadable = Paths.get(testTreeURL.toURI()).resolve("dir1"); - File directoryToMakeUnreadableAsFile = directoryToMakeUnreadable.toFile(); - - assertTrue(directoryToMakeUnreadableAsFile.isDirectory(), "This " + directoryToMakeUnreadableAsFile + " should be a Directory."); - assertTrue(directoryToMakeUnreadableAsFile.canRead(), "This " + directoryToMakeUnreadableAsFile + " should be readable."); - assertTrue(directoryToMakeUnreadableAsFile.canExecute(), "This " + directoryToMakeUnreadableAsFile + " should be executable."); - - try { - LOG.info("Making {} unreadable", directoryToMakeUnreadableAsFile); - assertTrue(directoryToMakeUnreadableAsFile.setReadable(false, false)); - assertTrue(directoryToMakeUnreadableAsFile.setExecutable(false, false)); - - assertTrue(directoryToMakeUnreadableAsFile.isDirectory(), "This " + directoryToMakeUnreadableAsFile + " should be a Directory."); - assertFalse(directoryToMakeUnreadableAsFile.canRead(), "This " + directoryToMakeUnreadableAsFile + " should NO LONGER be readable."); - assertFalse(directoryToMakeUnreadableAsFile.canExecute(), "This " + directoryToMakeUnreadableAsFile + " should NO LONGER be executable."); - - // Check 2: When trying to use it in a GitIgnoreFileSet it should all fail (for code coverage). - IOException ioException = assertThrows(IOException.class, ()-> new GitIgnoreFileSet(targetDirTestTree, true)); - assertTrue(ioException.getMessage().contains("Unable to load .gitignore files")); - } - finally { - LOG.info("Making {} readable", directoryToMakeUnreadableAsFile); - assertTrue(directoryToMakeUnreadableAsFile.setReadable(true, false)); - assertTrue(directoryToMakeUnreadableAsFile.setExecutable(true, false)); - - assertTrue(directoryToMakeUnreadableAsFile.isDirectory(), "This " + directoryToMakeUnreadableAsFile + " should be a Directory."); - assertTrue(directoryToMakeUnreadableAsFile.canRead(), "This " + directoryToMakeUnreadableAsFile + " should be readable."); - assertTrue(directoryToMakeUnreadableAsFile.canExecute(), "This " + directoryToMakeUnreadableAsFile + " should be executable."); - } - } - - @Test - void testIsIgnoredFile() throws IOException { - GitIgnoreFileSet gitIgnoreFileSet = new GitIgnoreFileSet(testTree) -// .setVerbose(true) - .assumeQueriesIncludeProjectBaseDir(); - - assertFalse(gitIgnoreFileSet.isEmpty(), "Unable to load any .gitignore files"); - - LOG.info("LOADED: {}", gitIgnoreFileSet); - - List ignore = new ArrayList<>(); - - try (Stream projectFiles = Files.find(testTree.toPath(), 128, (filePath, fileAttr) -> fileAttr.isRegularFile())) { - for (Path path : projectFiles.sorted().collect(Collectors.toList())) { - Boolean ignoredFile = gitIgnoreFileSet.isIgnoredFile(path.toString()); - if (ignoredFile == null) { - LOG.info("Path {} : Keep (No match)", path); - } else { - if (ignoredFile) { - LOG.error("Path {} : Ignore (Match) --------------- ", path); - ignore.add(path.toString()); - } else { - LOG.warn("Path {} : Keep (Match) ++++++++++++++ ", path); - } - } - } - } - - checkIgnoredList(ignore); - } - - @Test - void testVerboseFlag() { - GitIgnoreFileSet gitIgnoreFileSet = new GitIgnoreFileSet(testTree, false); - gitIgnoreFileSet.setVerbose(false); - assertFalse(gitIgnoreFileSet.getVerbose()); - gitIgnoreFileSet.setVerbose(true); - assertTrue(gitIgnoreFileSet.getVerbose()); - } - - @Test - @ClearEnvironmentVariable(key = "XDG_CONFIG_HOME") - @ClearEnvironmentVariable(key = "HOME") - void testIgnoreFile() throws IOException { - GitIgnoreFileSet gitIgnoreFileSet = new GitIgnoreFileSet(testTree); - gitIgnoreFileSet.setVerbose(true); - gitIgnoreFileSet.assumeQueriesIncludeProjectBaseDir(); - - assertFalse(gitIgnoreFileSet.isEmpty(), "Unable to load any .gitignore files"); - - try (Stream projectFiles = Files.find(testTree.toPath(), 128, (filePath, fileAttr) -> fileAttr.isRegularFile())) { - List ignore = new ArrayList<>(); - - for (Path path : projectFiles.sorted().collect(Collectors.toList())) { - if (gitIgnoreFileSet.ignoreFile(path.toString())) { - LOG.info("Path {} : Ignore --------------- ", path); - ignore.add(path.toString()); - } else { - LOG.info("Path {} : Keep", path); - } - } - - // Because we have excluded the global ignore file; the list of expected ignores must be updated. - List adjustedExpectedIgnoredFiles = new ArrayList<>(expectedIgnoredFiles); - // This file is no longer ignored! - adjustedExpectedIgnoredFiles.remove("/dir5/ignored_from_global_gitignore"); - checkIgnoredList(adjustedExpectedIgnoredFiles, ignore); - } - } - - @Test - void testRecursiveIncrementalLoading() { - GitIgnoreFileSet gitIgnoreFileSet = new GitIgnoreFileSet(testTree, false); - gitIgnoreFileSet.setVerbose(true); - - List files = recursivelyFindFiles(gitIgnoreFileSet, testTree); - - // Check we got it right - files - .stream() - .map(File::getPath) - .map(GitIgnore::standardizeFilename) // Needed for testing on Windows. - .map(f -> f.replace(testTree.getPath(), "")) - .forEach(file -> assertFalse(expectedIgnoredFiles.contains(file))); - } - - private List recursivelyFindFiles(GitIgnoreFileSet gitIgnoreFileSet, File directory) { - if (directory == null) { - return Collections.emptyList(); - } - - // Load the new gitignore files (if any). - File[] newGitIgnoreFiles = directory.listFiles(pathname -> ".gitignore".equals(pathname.getName())); - if (newGitIgnoreFiles != null) { - Arrays - .asList(newGitIgnoreFiles) - .forEach(gitIgnoreFileSet::addGitIgnoreFile); - } - - // Only keep the non ignored files - File[] normalFiles = directory.listFiles(gitIgnoreFileSet); // << Using it as a FileFilter - - if (normalFiles == null) { - return Collections.emptyList(); - } - - List result = new ArrayList<>(); - for (File file : normalFiles) { - if (file.isFile()) { - if (!gitIgnoreFileSet.ignoreFile(file.getPath())) { - result.add(file); - } - } - - if (file.isDirectory()) { - if (!gitIgnoreFileSet.ignoreFile(file.getPath())) { - result.addAll(recursivelyFindFiles(gitIgnoreFileSet, file)); - } - } - } - return result; - } - - @Test - void testFileFilter() throws IOException { - GitIgnoreFileSet gitIgnoreFileSet = new GitIgnoreFileSet(testTree).assumeQueriesIncludeProjectBaseDir(); - - try (Stream projectFiles = Files.find(testTree.toPath(), 128, (filePath, fileAttr) -> fileAttr.isRegularFile() && gitIgnoreFileSet.ignoreFile(filePath.toString()))) { - List ignored = projectFiles - .map(Path::toString) - .sorted() - .collect(Collectors.toList()); - checkIgnoredList(ignored); - } - } - - @Test - void testWindowsPaths() { - GitIgnoreFileSet gitIgnoreFileSet = new GitIgnoreFileSet(new File(separatorsToWindows(testTree.getPath())), false).assumeQueriesAreProjectRelative(); - - assertFalse(gitIgnoreFileSet.isAssumeQueriesIncludeProjectBaseDir()); - assertTrue(gitIgnoreFileSet.isAssumeQueriesAreProjectRelative()); - - gitIgnoreFileSet.add(new GitIgnore(separatorsToWindows("/"), "*.txt")); - gitIgnoreFileSet.add(new GitIgnore(separatorsToWindows("/dir1/"), "*.md")); - gitIgnoreFileSet.add(new GitIgnore(separatorsToWindows("/dir2/"), "!foo.txt")); - - assertFalse(gitIgnoreFileSet.isEmpty()); - - assertTrue(gitIgnoreFileSet.ignoreFile("\\foo.txt")); - assertTrue(gitIgnoreFileSet.ignoreFile("\\dir1\\foo.txt")); - assertFalse(gitIgnoreFileSet.ignoreFile("\\dir2\\foo.txt")); - - assertFalse(gitIgnoreFileSet.ignoreFile("\\foo.md")); - assertTrue(gitIgnoreFileSet.ignoreFile("\\dir1\\foo.md")); - assertFalse(gitIgnoreFileSet.ignoreFile("\\dir2\\foo.md")); - } - - @Test - void testWindowsPathsWithDriveLetter() { - GitIgnoreFileSet gitIgnoreFileSet = new GitIgnoreFileSet(new File("A:\\MyProject\\src\\test\\resources\\"), false).assumeQueriesIncludeProjectBaseDir(); - - assertTrue(gitIgnoreFileSet.isAssumeQueriesIncludeProjectBaseDir()); - assertFalse(gitIgnoreFileSet.isAssumeQueriesAreProjectRelative()); - - gitIgnoreFileSet.add(new GitIgnore("\\", "*.txt")); - gitIgnoreFileSet.add(new GitIgnore("\\dir1\\", "*.md")); - gitIgnoreFileSet.add(new GitIgnore("\\dir2\\", "!foo.txt")); - gitIgnoreFileSet.setVerbose(true); - - assertFalse(gitIgnoreFileSet.isEmpty()); - - // Correctly handle absolute paths (assume) - gitIgnoreFileSet.assumeQueriesIncludeProjectBaseDir(); - assertTrue(gitIgnoreFileSet.ignoreFile("A:\\MyProject\\src\\test\\resources\\foo.txt")); - assertTrue(gitIgnoreFileSet.ignoreFile("A:\\MyProject\\src\\test\\resources\\dir1\\foo.txt")); - assertFalse(gitIgnoreFileSet.ignoreFile("A:\\MyProject\\src\\test\\resources\\dir2\\foo.txt")); - - assertFalse(gitIgnoreFileSet.ignoreFile("A:\\MyProject\\src\\test\\resources\\foo.md")); - assertTrue(gitIgnoreFileSet.ignoreFile("A:\\MyProject\\src\\test\\resources\\dir1\\foo.md")); - assertFalse(gitIgnoreFileSet.ignoreFile("A:\\MyProject\\src\\test\\resources\\dir2\\foo.md")); - - // Correctly handle absolute paths (explicit) - gitIgnoreFileSet.assumeQueriesAreProjectRelative(); - assertTrue(gitIgnoreFileSet.ignoreFile("A:\\MyProject\\src\\test\\resources\\foo.txt", false)); - assertTrue(gitIgnoreFileSet.ignoreFile("A:\\MyProject\\src\\test\\resources\\dir1\\foo.txt", false)); - assertFalse(gitIgnoreFileSet.ignoreFile("A:\\MyProject\\src\\test\\resources\\dir2\\foo.txt", false)); - - assertFalse(gitIgnoreFileSet.ignoreFile("A:\\MyProject\\src\\test\\resources\\foo.md", false)); - assertTrue(gitIgnoreFileSet.ignoreFile("A:\\MyProject\\src\\test\\resources\\dir1\\foo.md", false)); - assertFalse(gitIgnoreFileSet.ignoreFile("A:\\MyProject\\src\\test\\resources\\dir2\\foo.md", false)); - - // Correctly handle project relative paths (assume) - gitIgnoreFileSet.assumeQueriesAreProjectRelative(); - assertTrue(gitIgnoreFileSet.ignoreFile("\\foo.txt")); - assertTrue(gitIgnoreFileSet.ignoreFile("\\dir1\\foo.txt")); - assertFalse(gitIgnoreFileSet.ignoreFile("\\dir2\\foo.txt")); - - assertFalse(gitIgnoreFileSet.ignoreFile("\\foo.md")); - assertTrue(gitIgnoreFileSet.ignoreFile("\\dir1\\foo.md")); - assertFalse(gitIgnoreFileSet.ignoreFile("\\dir2\\foo.md")); - - // Correctly handle project relative paths (explicit) - gitIgnoreFileSet.assumeQueriesIncludeProjectBaseDir(); - assertTrue(gitIgnoreFileSet.ignoreFile("\\foo.txt", true)); - assertTrue(gitIgnoreFileSet.ignoreFile("\\dir1\\foo.txt", true)); - assertFalse(gitIgnoreFileSet.ignoreFile("\\dir2\\foo.txt", true)); - - assertFalse(gitIgnoreFileSet.ignoreFile("\\foo.md", true)); - assertTrue(gitIgnoreFileSet.ignoreFile("\\dir1\\foo.md", true)); - assertFalse(gitIgnoreFileSet.ignoreFile("\\dir2\\foo.md", true)); - } - - @Test - void testErrorHandlingNosuchDirectory() { - GitIgnoreFileSet gitIgnoreFileSet = new GitIgnoreFileSet(new File("/no-such-file-really")); - assertTrue(gitIgnoreFileSet.isEmpty()); - } - - @Test - void testErrorHandlingNosuchFile() { - GitIgnoreFileSet gitIgnoreFileSet = new GitIgnoreFileSet(new File("src/test/resources/"), false); - gitIgnoreFileSet.addGitIgnoreFile(new File("src/test/resources/no-such-file-really")); - assertTrue(gitIgnoreFileSet.isEmpty()); - } - - @Test - void ignoreDirectoriesWithAndWithoutSlash() { - GitIgnoreFileSet gitIgnoreFileSet = new GitIgnoreFileSet(testTree, false); - List addedGitIgnoreFiles = gitIgnoreFileSet.addAllGitIgnoreFiles(false); - - LOG.info("Added gitignore files: {}", addedGitIgnoreFiles); - - List expected = Arrays.asList( - "/src/test/resources/testtree/.gitignore", - "/src/test/resources/testtree/dir1/.gitignore", - "/src/test/resources/testtree/dir3/.gitignore" - ); - - assertEquals(expected, addedGitIgnoreFiles - .stream() - .map(Path::toString) - .map(GitIgnore::standardizeFilename) // Needed for testing on Windows. - .sorted() - .collect(Collectors.toList())); - } - - @Test - void listNonIgnoredFilesAndDirectories() { - GitIgnoreFileSet gitIgnoreFileSet = new GitIgnoreFileSet(testTree).assumeQueriesIncludeProjectBaseDir(); - List allNonIgnored = findAllNonIgnored(gitIgnoreFileSet); - - LOG.info("All non ignored files: {}", allNonIgnored); - - assertEquals(expectedKeepFiles, stripTestTreeBaseDir(allNonIgnored)); - - // Correct use: Relative path - assertEquals(true, gitIgnoreFileSet.isIgnoredFile("/README.md", true)); - // Correct use: Absolute path - assertEquals(true, gitIgnoreFileSet.isIgnoredFile(testTree.getAbsolutePath() + "/README.md", false)); - // Wrong use: Relative path when absolute is needed. - assertThrows(IllegalArgumentException.class, () -> gitIgnoreFileSet.isIgnoredFile("/README.md", false)); - } - - @Test - void triggerNotExist() { - GitIgnoreFileSet gitIgnoreFileSet = new GitIgnoreFileSet(testTree).assumeQueriesIncludeProjectBaseDir(); - List allNonIgnored = findAllNonIgnored(gitIgnoreFileSet, new File("/no-such-file-really").toPath()); - assertTrue(allNonIgnored.isEmpty()); - } - - @Test - void triggerNotADirectory() { - GitIgnoreFileSet gitIgnoreFileSet = new GitIgnoreFileSet(testTree).assumeQueriesIncludeProjectBaseDir(); - List allNonIgnored = findAllNonIgnored(gitIgnoreFileSet, new File(gitIgnoreFileSet.projectBaseDir.toPath() + "/README.md").toPath()); - assertTrue(allNonIgnored.isEmpty()); - } - - @Test - void triggerEverythingIsIgnored() { - GitIgnoreFileSet gitIgnoreFileSet = new GitIgnoreFileSet(testTree).assumeQueriesIncludeProjectBaseDir(); - GitIgnore gitIgnore = new GitIgnore("*"); - gitIgnoreFileSet.add(gitIgnore); - List allNonIgnored = findAllNonIgnored(gitIgnoreFileSet); - assertTrue(allNonIgnored.isEmpty()); - } - - @Test - @DisabledOnOs(WINDOWS) - void triggerIOErrorDirectory() { - GitIgnoreFileSet gitIgnoreFileSet = new GitIgnoreFileSet(testTree).assumeQueriesIncludeProjectBaseDir(); - - File testDir = new File(gitIgnoreFileSet.projectBaseDir.toPath() + "/dir5"); - assertTrue(testDir.isDirectory()); - assertTrue(testDir.canExecute()); - assertTrue(testDir.canRead()); - List allNonIgnored = findAllNonIgnored(gitIgnoreFileSet); - assertTrue(allNonIgnored.contains(testDir.toPath()), "The list " + allNonIgnored + " is missing " + testDir); - - try { - assertTrue(testDir.setExecutable(false, false)); - assertTrue(testDir.setReadable(false, false)); - List allNonIgnored2 = findAllNonIgnored(gitIgnoreFileSet); - assertFalse(allNonIgnored2.contains(testDir.toPath()), "The list " + allNonIgnored + " SHOULD NOT contain " + testDir); - } - finally { - assertTrue(testDir.setExecutable(true, false)); - assertTrue(testDir.setReadable(true, false)); - } - } - -} diff --git a/gitignore-reader/src/test/kotlin/nl/basjes/gitignore/TestGitIgnoreFiles.kt b/gitignore-reader/src/test/kotlin/nl/basjes/gitignore/TestGitIgnoreFiles.kt new file mode 100644 index 0000000..a8c98be --- /dev/null +++ b/gitignore-reader/src/test/kotlin/nl/basjes/gitignore/TestGitIgnoreFiles.kt @@ -0,0 +1,664 @@ +/* + * CodeOwners Tools + * Copyright (C) 2023-2026 Niels Basjes + * + * 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 + * + * https://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 nl.basjes.gitignore + +import nl.basjes.gitignore.GitIgnore.Companion.standardizeFilename +import nl.basjes.gitignore.GitIgnoreFileSet.Companion.getGlobalGitIgnore +import nl.basjes.gitignore.Utils.findAllNonIgnored +import kotlin.test.Test +import org.junit.jupiter.api.condition.DisabledOnOs +import org.junit.jupiter.api.condition.OS +import org.junitpioneer.jupiter.ClearEnvironmentVariable +import org.junitpioneer.jupiter.EnvironmentVariableUtilsFacade +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.nio.file.attribute.BasicFileAttributes +import kotlin.streams.asSequence + +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +internal class TestGitIgnoreFiles { + val expectedKeepFiles = listOf( + "/", + "/dir1", + "/dir1/dir1.md", + "/dir1/.gitignore", + "/dir2", + "/dir2/dir2.txt", + "/dir2/file2.log", + "/dir3", + "/dir3/dir3.txt", + "/dir3/file3.log", + "/dir3/.gitignore", + "/dir5", + "/dir5/file5.log", + "/.gitignore" + ).sorted() + + val expectedIgnoredFiles = listOf( + "/dir1/dir1.log", + "/dir1/dir1.txt", + "/dir1/file1.log", + "/dir2/dir2.log", + "/dir2/dir2.md", + "/dir3/dir3.log", + "/dir3/dir3.md", + "/dir4/.gitignore", + "/dir4/dir4.log", + "/dir4/dir4.md", + "/dir4/dir4.txt", + "/dir4/file4.log", + "/dir4/subdir/.gitignore", + "/dir4/subdir/dir1.log", + "/dir4/subdir/dir1.md", + "/dir4/subdir/dir1.txt", + "/dir4/subdir/file1.log", + "/dir4/subdir/subdir/.gitignore", + "/dir4/subdir/subdir/dir1.log", + "/dir4/subdir/subdir/dir1.md", + "/dir4/subdir/subdir/dir1.txt", + "/dir4/subdir/subdir/file1.log", + "/dir5/ignored_from_global_gitignore", + "/README.md", + "/root.md" + ).sorted() + + private fun stripTestTreeBaseDir(input: List): List { + return input + .map { it.toString() } + .map { standardizeFilename(it) } // Needed for testing on Windows. + .map { it.replace(("^\\Q/$TEST_TREE_DIR\\E").toRegex(), "/").replace("//", "/") } + .sorted() + .toList() + + } + + private fun stripTestTreeBaseDir(input: Path): String { + return standardizeFilename(input.toString()) + .replace(("^\\Q/$TEST_TREE_DIR\\E").toRegex(), "/") + .replace("//", "/") + } + + @Test + fun ensureExpectationsDoNotOverlap() { + for (expectedKeepFile in expectedKeepFiles) { + assertFalse( + expectedIgnoredFiles.contains(expectedKeepFile), + "Expected Ignores has $expectedKeepFile" + ) + } + for (expectedIgnoreFile in expectedIgnoredFiles) { + assertFalse( + expectedKeepFiles.contains(expectedIgnoreFile), + "Expected Keeps has $expectedIgnoreFile" + ) + } + } + + @Test + @Throws(IOException::class) + fun ensureAllFilesInTestTreeAreEitherInKeepOrIgnore() { + Files + .find( + testTree.toPath(), + 128, + { _: Path, fileAttr: BasicFileAttributes -> fileAttr.isRegularFile } + ) + .use { + it + .sorted() + .map { path -> stripTestTreeBaseDir(path) } + .forEach { + name -> + assertTrue( + expectedIgnoredFiles.contains(name) || expectedKeepFiles.contains(name), + "Missing entry for $name" + ) + } + } + } + + private fun checkIgnoredList(ignore: List) { + checkIgnoredList(expectedIgnoredFiles, ignore) + } + + private fun checkIgnoredList(expectedIgnore: List?, ignore: List) { + val ignored = ignore + .map { standardizeFilename(File(it).absolutePath) } // Needed for testing on Windows. + .map { it.replace( ("^\\Q" + standardizeFilename(testTree.absolutePath) + "\\E").toRegex(), "") } + .sorted() + .toList() + assertEquals(expectedIgnore, ignored) + } + + @Test + @Throws(Exception::class) + fun testGetGlobalGitIgnore() { + assertNull(getGlobalGitIgnore(null, null)) + assertNull(getGlobalGitIgnore(null, "")) + assertNull(getGlobalGitIgnore("", null)) + assertNull(getGlobalGitIgnore("", "")) + + val dirWithGitIgnoreURL = this.javaClass + .classLoader + .getResource("xdg_config_home") + assertNotNull(dirWithGitIgnoreURL) + val dirWithGitIgnore = dirWithGitIgnoreURL.file + + val dirWithConfigGitIgnoreURL = this.javaClass + .classLoader + .getResource("home") + assertNotNull(dirWithConfigGitIgnoreURL) + val dirWithConfigGitIgnore = dirWithConfigGitIgnoreURL.file + + assertNull(getGlobalGitIgnore(dirWithConfigGitIgnore, "")) + assertNull(getGlobalGitIgnore("", dirWithGitIgnore)) + + assertEquals( + Paths.get(dirWithGitIgnoreURL.toURI()).resolve("git").resolve("ignore"), + getGlobalGitIgnore(dirWithGitIgnore, "") + ) + assertEquals( + Paths.get(dirWithGitIgnoreURL.toURI()).resolve("git").resolve("ignore"), + getGlobalGitIgnore(dirWithGitIgnore, dirWithConfigGitIgnore) + ) + + assertEquals( + Paths.get(dirWithConfigGitIgnoreURL.toURI()).resolve(".config").resolve("git").resolve("ignore"), + getGlobalGitIgnore(null, dirWithConfigGitIgnore) + ) + assertNull(getGlobalGitIgnore(dirWithConfigGitIgnore, dirWithConfigGitIgnore)) + } + + @Test + @DisabledOnOs(OS.WINDOWS) + @Throws(Exception::class) + fun testGetGlobalGitIgnoreUnreadableFiles() { + val dirWithGitIgnoreURL = this.javaClass + .classLoader + .getResource("unreadable/.config") + assertNotNull(dirWithGitIgnoreURL) + val dirWithGitIgnore = dirWithGitIgnoreURL.file + + val dirWithConfigGitIgnoreURL = this.javaClass + .classLoader + .getResource("unreadable") + assertNotNull(dirWithConfigGitIgnoreURL) + val dirWithConfigGitIgnore = dirWithConfigGitIgnoreURL.file + + val ignoreFileToMakeUnreadable = File("$dirWithGitIgnore/git/ignore") + + val expectedIgnorePath = Paths.get(dirWithGitIgnoreURL.toURI()).resolve("git").resolve("ignore") + + assertTrue( + ignoreFileToMakeUnreadable.isFile, + "This $ignoreFileToMakeUnreadable should be a File." + ) + assertTrue( + ignoreFileToMakeUnreadable.canRead(), + "This $ignoreFileToMakeUnreadable should be readable." + ) + + // Use Pioneer's underlying utility to inject it programmatically + EnvironmentVariableUtilsFacade.set("XDG_CONFIG_HOME", dirWithGitIgnore) + EnvironmentVariableUtilsFacade.set("HOME", dirWithConfigGitIgnore) + + try { + LOG.info("Making {} unreadable", ignoreFileToMakeUnreadable) + assertTrue(ignoreFileToMakeUnreadable.setReadable(false, false)) + assertTrue( + ignoreFileToMakeUnreadable.isFile, + "This $ignoreFileToMakeUnreadable should be a File." + ) + assertFalse( + ignoreFileToMakeUnreadable.canRead(), + "This $ignoreFileToMakeUnreadable should NO LONGER be readable." + ) + + // Check 1: Even if the file is not readable it should be found + assertEquals(expectedIgnorePath, getGlobalGitIgnore(null, dirWithConfigGitIgnore)) + assertEquals(expectedIgnorePath, getGlobalGitIgnore(dirWithGitIgnore, null)) + + // Check 2: When trying to use it in a GitIgnoreFileSet it should all fail (for code coverage). + val gitIgnoreFileSet = GitIgnoreFileSet(targetDirTestTree, false) + gitIgnoreFileSet.addAllGitIgnoreFiles(true) + } finally { + assertTrue(ignoreFileToMakeUnreadable.setReadable(true, false)) + assertTrue( + ignoreFileToMakeUnreadable.canRead(), + "This $ignoreFileToMakeUnreadable should be readable." + ) + } + } + + + @Test + @DisabledOnOs(OS.WINDOWS) + @Throws(Exception::class) + fun testUnreadableDirectory() { + val testTreeURL = this.javaClass + .classLoader + .getResource("testtree") + assertNotNull(testTreeURL) + + val directoryToMakeUnreadable = Paths.get(testTreeURL.toURI()).resolve("dir1") + val directoryToMakeUnreadableAsFile = directoryToMakeUnreadable.toFile() + + assertTrue( + directoryToMakeUnreadableAsFile.isDirectory, + "This $directoryToMakeUnreadableAsFile should be a Directory." + ) + assertTrue( + directoryToMakeUnreadableAsFile.canRead(), + "This $directoryToMakeUnreadableAsFile should be readable." + ) + assertTrue( + directoryToMakeUnreadableAsFile.canExecute(), + "This $directoryToMakeUnreadableAsFile should be executable." + ) + + try { + LOG.info("Making {} unreadable", directoryToMakeUnreadableAsFile) + assertTrue(directoryToMakeUnreadableAsFile.setReadable(false, false)) + assertTrue(directoryToMakeUnreadableAsFile.setExecutable(false, false)) + + assertTrue( + directoryToMakeUnreadableAsFile.isDirectory, + "This $directoryToMakeUnreadableAsFile should be a Directory." + ) + assertFalse( + directoryToMakeUnreadableAsFile.canRead(), + "This $directoryToMakeUnreadableAsFile should NO LONGER be readable." + ) + assertFalse( + directoryToMakeUnreadableAsFile.canExecute(), + "This $directoryToMakeUnreadableAsFile should NO LONGER be executable." + ) + + // Check 2: When trying to use it in a GitIgnoreFileSet it should all fail (for code coverage). + val ioException = assertFailsWith { GitIgnoreFileSet(targetDirTestTree, true) } + assertTrue(ioException.message!!.contains("Unable to load .gitignore files")) + } finally { + LOG.info("Making {} readable", directoryToMakeUnreadableAsFile) + assertTrue(directoryToMakeUnreadableAsFile.setReadable(true, false)) + assertTrue(directoryToMakeUnreadableAsFile.setExecutable(true, false)) + + assertTrue( + directoryToMakeUnreadableAsFile.isDirectory, + "This $directoryToMakeUnreadableAsFile should be a Directory." + ) + assertTrue( + directoryToMakeUnreadableAsFile.canRead(), + "This $directoryToMakeUnreadableAsFile should be readable." + ) + assertTrue( + directoryToMakeUnreadableAsFile.canExecute(), + "This $directoryToMakeUnreadableAsFile should be executable." + ) + } + } + + @Test + @Throws(IOException::class) + fun testIsIgnoredFile() { + val gitIgnoreFileSet = GitIgnoreFileSet(testTree) // .setVerbose(true) + .assumeQueriesIncludeProjectBaseDir() + + assertFalse(gitIgnoreFileSet.isEmpty, "Unable to load any .gitignore files") + + LOG.info("LOADED: {}", gitIgnoreFileSet) + + val ignore = mutableListOf() + + Files.find( + testTree.toPath(), + 128, + { _: Path, fileAttr: BasicFileAttributes -> fileAttr.isRegularFile }) + .use { projectFiles -> + for (path in projectFiles.sorted()) { + val ignoredFile = gitIgnoreFileSet.isIgnoredFile(path.toString()) + if (ignoredFile == null) { + LOG.info("Path {} : Keep (No match)", path) + } else { + if (ignoredFile) { + LOG.error("Path {} : Ignore (Match) --------------- ", path) + ignore.add(path.toString()) + } else { + LOG.warn("Path {} : Keep (Match) ++++++++++++++ ", path) + } + } + } + } + checkIgnoredList(ignore) + } + + @Test + fun testVerboseFlag() { + val gitIgnoreFileSet = GitIgnoreFileSet(testTree, false) + gitIgnoreFileSet.verbose = false + assertFalse(gitIgnoreFileSet.verbose) + gitIgnoreFileSet.verbose = true + assertTrue(gitIgnoreFileSet.verbose) + } + + @Test + @ClearEnvironmentVariable(key = "XDG_CONFIG_HOME") + @ClearEnvironmentVariable(key = "HOME") + @Throws(IOException::class) + fun testIgnoreFile() { + val gitIgnoreFileSet = GitIgnoreFileSet(testTree) + gitIgnoreFileSet.verbose = true + gitIgnoreFileSet.assumeQueriesIncludeProjectBaseDir() + + assertFalse(gitIgnoreFileSet.isEmpty, "Unable to load any .gitignore files") + + Files.find( + testTree.toPath(), + 128, + { _: Path, fileAttr: BasicFileAttributes -> fileAttr.isRegularFile }) + .use { projectFiles -> + val ignore = mutableListOf() + for (path in projectFiles.sorted()) { + if (gitIgnoreFileSet.ignoreFile(path.toString())) { + LOG.info("Path {} : Ignore --------------- ", path) + ignore.add(path.toString()) + } else { + LOG.info("Path {} : Keep", path) + } + } + + // Because we have excluded the global ignore file; the list of expected ignores must be updated. + val adjustedExpectedIgnoredFiles = expectedIgnoredFiles.toMutableList() + // This file is no longer ignored! + adjustedExpectedIgnoredFiles.remove("/dir5/ignored_from_global_gitignore") + checkIgnoredList(adjustedExpectedIgnoredFiles, ignore) + } + } + + @Test + fun testRecursiveIncrementalLoading() { + val gitIgnoreFileSet = GitIgnoreFileSet(testTree, false) + gitIgnoreFileSet.verbose = true + + val files = recursivelyFindFiles(gitIgnoreFileSet, testTree) + + // Check we got it right + files + .map { it.path } + .map { standardizeFilename(it) } // Needed for testing on Windows. + .map { it.replace(testTree.path, "") } + .forEach { assertFalse(expectedIgnoredFiles.contains(it)) } + } + + private fun recursivelyFindFiles(gitIgnoreFileSet: GitIgnoreFileSet, directory: File): List { + // Load the new gitignore files (if any). + val newGitIgnoreFiles = + directory.listFiles { it.name == ".gitignore" }?.toList()?:listOf() + newGitIgnoreFiles.forEach { gitIgnoreFileSet.addGitIgnoreFile(it) } + + // Only keep the non ignored files + val normalFiles = + directory.listFiles(gitIgnoreFileSet)?.toList() ?: listOf() // << Using it as a FileFilter + + val result = mutableListOf() + for (file in normalFiles) { + if (file.isFile) { + if (!gitIgnoreFileSet.ignoreFile(file.path)) { + result.add(file) + } + } + + if (file.isDirectory) { + if (!gitIgnoreFileSet.ignoreFile(file.path)) { + result.addAll(recursivelyFindFiles(gitIgnoreFileSet, file)) + } + } + } + return result + } + + @Test + @Throws(IOException::class) + fun testFileFilter() { + val gitIgnoreFileSet = GitIgnoreFileSet(testTree).assumeQueriesIncludeProjectBaseDir() + + Files + .find( + testTree.toPath(), + 128, + { filePath: Path, fileAttr: BasicFileAttributes -> + fileAttr.isRegularFile && gitIgnoreFileSet.ignoreFile(filePath.toString()) } + ) + .use { + projectFiles -> + val ignored = projectFiles + .asSequence() + .map { it.toString() } + .sorted() + .toList() + checkIgnoredList(ignored) + } + } + + @Test + fun testWindowsPaths() { + val gitIgnoreFileSet = + GitIgnoreFileSet(File(separatorsToWindows(testTree.path)), false).assumeQueriesAreProjectRelative() + + assertFalse(gitIgnoreFileSet.isAssumeQueriesIncludeProjectBaseDir) + assertTrue(gitIgnoreFileSet.isAssumeQueriesAreProjectRelative) + + gitIgnoreFileSet.add(GitIgnore(separatorsToWindows("/"), "*.txt")) + gitIgnoreFileSet.add(GitIgnore(separatorsToWindows("/dir1/"), "*.md")) + gitIgnoreFileSet.add(GitIgnore(separatorsToWindows("/dir2/"), "!foo.txt")) + + assertFalse(gitIgnoreFileSet.isEmpty) + + assertTrue(gitIgnoreFileSet.ignoreFile("\\foo.txt")) + assertTrue(gitIgnoreFileSet.ignoreFile("\\dir1\\foo.txt")) + assertFalse(gitIgnoreFileSet.ignoreFile("\\dir2\\foo.txt")) + + assertFalse(gitIgnoreFileSet.ignoreFile("\\foo.md")) + assertTrue(gitIgnoreFileSet.ignoreFile("\\dir1\\foo.md")) + assertFalse(gitIgnoreFileSet.ignoreFile("\\dir2\\foo.md")) + } + + @Test + fun testWindowsPathsWithDriveLetter() { + val gitIgnoreFileSet = + GitIgnoreFileSet(File("A:\\MyProject\\src\\test\\resources\\"), false).assumeQueriesIncludeProjectBaseDir() + + assertTrue(gitIgnoreFileSet.isAssumeQueriesIncludeProjectBaseDir) + assertFalse(gitIgnoreFileSet.isAssumeQueriesAreProjectRelative) + + gitIgnoreFileSet.add(GitIgnore("\\", "*.txt")) + gitIgnoreFileSet.add(GitIgnore("\\dir1\\", "*.md")) + gitIgnoreFileSet.add(GitIgnore("\\dir2\\", "!foo.txt")) + gitIgnoreFileSet.verbose = true + + assertFalse(gitIgnoreFileSet.isEmpty) + + // Correctly handle absolute paths (assume) + gitIgnoreFileSet.assumeQueriesIncludeProjectBaseDir() + assertTrue(gitIgnoreFileSet.ignoreFile("A:\\MyProject\\src\\test\\resources\\foo.txt")) + assertTrue(gitIgnoreFileSet.ignoreFile("A:\\MyProject\\src\\test\\resources\\dir1\\foo.txt")) + assertFalse(gitIgnoreFileSet.ignoreFile("A:\\MyProject\\src\\test\\resources\\dir2\\foo.txt")) + + assertFalse(gitIgnoreFileSet.ignoreFile("A:\\MyProject\\src\\test\\resources\\foo.md")) + assertTrue(gitIgnoreFileSet.ignoreFile("A:\\MyProject\\src\\test\\resources\\dir1\\foo.md")) + assertFalse(gitIgnoreFileSet.ignoreFile("A:\\MyProject\\src\\test\\resources\\dir2\\foo.md")) + + // Correctly handle absolute paths (explicit) + gitIgnoreFileSet.assumeQueriesAreProjectRelative() + assertTrue(gitIgnoreFileSet.ignoreFile("A:\\MyProject\\src\\test\\resources\\foo.txt", false)) + assertTrue(gitIgnoreFileSet.ignoreFile("A:\\MyProject\\src\\test\\resources\\dir1\\foo.txt", false)) + assertFalse(gitIgnoreFileSet.ignoreFile("A:\\MyProject\\src\\test\\resources\\dir2\\foo.txt", false)) + + assertFalse(gitIgnoreFileSet.ignoreFile("A:\\MyProject\\src\\test\\resources\\foo.md", false)) + assertTrue(gitIgnoreFileSet.ignoreFile("A:\\MyProject\\src\\test\\resources\\dir1\\foo.md", false)) + assertFalse(gitIgnoreFileSet.ignoreFile("A:\\MyProject\\src\\test\\resources\\dir2\\foo.md", false)) + + // Correctly handle project relative paths (assume) + gitIgnoreFileSet.assumeQueriesAreProjectRelative() + assertTrue(gitIgnoreFileSet.ignoreFile("\\foo.txt")) + assertTrue(gitIgnoreFileSet.ignoreFile("\\dir1\\foo.txt")) + assertFalse(gitIgnoreFileSet.ignoreFile("\\dir2\\foo.txt")) + + assertFalse(gitIgnoreFileSet.ignoreFile("\\foo.md")) + assertTrue(gitIgnoreFileSet.ignoreFile("\\dir1\\foo.md")) + assertFalse(gitIgnoreFileSet.ignoreFile("\\dir2\\foo.md")) + + // Correctly handle project relative paths (explicit) + gitIgnoreFileSet.assumeQueriesIncludeProjectBaseDir() + assertTrue(gitIgnoreFileSet.ignoreFile("\\foo.txt", true)) + assertTrue(gitIgnoreFileSet.ignoreFile("\\dir1\\foo.txt", true)) + assertFalse(gitIgnoreFileSet.ignoreFile("\\dir2\\foo.txt", true)) + + assertFalse(gitIgnoreFileSet.ignoreFile("\\foo.md", true)) + assertTrue(gitIgnoreFileSet.ignoreFile("\\dir1\\foo.md", true)) + assertFalse(gitIgnoreFileSet.ignoreFile("\\dir2\\foo.md", true)) + } + + @Test + fun testErrorHandlingNosuchDirectory() { + val gitIgnoreFileSet = GitIgnoreFileSet(File("/no-such-file-really")) + assertTrue(gitIgnoreFileSet.isEmpty) + } + + @Test + fun testErrorHandlingNosuchFile() { + val gitIgnoreFileSet = GitIgnoreFileSet(File("src/test/resources/"), false) + gitIgnoreFileSet.addGitIgnoreFile(File("src/test/resources/no-such-file-really")) + assertTrue(gitIgnoreFileSet.isEmpty) + } + + @Test + fun ignoreDirectoriesWithAndWithoutSlash() { + val gitIgnoreFileSet = GitIgnoreFileSet(testTree, false) + val addedGitIgnoreFiles: List = gitIgnoreFileSet.addAllGitIgnoreFiles(false) + + LOG.info("Added gitignore files: {}", addedGitIgnoreFiles) + + val expected = mutableListOf( + "/src/test/resources/testtree/.gitignore", + "/src/test/resources/testtree/dir1/.gitignore", + "/src/test/resources/testtree/dir3/.gitignore" + ) + + assertEquals( + expected, addedGitIgnoreFiles + .map { it.toString() } + .map { standardizeFilename(it) } // Needed for testing on Windows. + .sorted() + .toList()) + } + + @Test + fun listNonIgnoredFilesAndDirectories() { + val gitIgnoreFileSet = GitIgnoreFileSet(testTree).assumeQueriesIncludeProjectBaseDir() + val allNonIgnored: List = findAllNonIgnored(gitIgnoreFileSet) + + LOG.info("All non ignored files: {}", allNonIgnored) + + assertEquals(expectedKeepFiles, stripTestTreeBaseDir(allNonIgnored)) + + // Correct use: Relative path + assertEquals(true, gitIgnoreFileSet.isIgnoredFile("/README.md", true)) + // Correct use: Absolute path + assertEquals(true, gitIgnoreFileSet.isIgnoredFile(testTree.absolutePath + "/README.md", false)) + // Wrong use: Relative path when absolute is needed. + assertFailsWith { gitIgnoreFileSet.isIgnoredFile("/README.md", false) } + } + + @Test + fun triggerNotExist() { + val gitIgnoreFileSet = GitIgnoreFileSet(testTree).assumeQueriesIncludeProjectBaseDir() + val allNonIgnored: List = + findAllNonIgnored(gitIgnoreFileSet, File("/no-such-file-really").toPath()) + assertTrue(allNonIgnored.isEmpty()) + } + + @Test + fun triggerNotADirectory() { + val gitIgnoreFileSet = GitIgnoreFileSet(testTree).assumeQueriesIncludeProjectBaseDir() + val allNonIgnored: List = findAllNonIgnored( + gitIgnoreFileSet, + File(gitIgnoreFileSet.projectBaseDir.toPath().toString() + "/README.md").toPath() + ) + assertTrue(allNonIgnored.isEmpty()) + } + + @Test + fun triggerEverythingIsIgnored() { + val gitIgnoreFileSet = GitIgnoreFileSet(testTree).assumeQueriesIncludeProjectBaseDir() + val gitIgnore = GitIgnore("*") + gitIgnoreFileSet.add(gitIgnore) + val allNonIgnored: List = findAllNonIgnored(gitIgnoreFileSet) + assertTrue(allNonIgnored.isEmpty()) + } + + @Test + @DisabledOnOs(OS.WINDOWS) + fun triggerIOErrorDirectory() { + val gitIgnoreFileSet = GitIgnoreFileSet(testTree).assumeQueriesIncludeProjectBaseDir() + + val testDir = File(gitIgnoreFileSet.projectBaseDir.toPath().toString() + "/dir5") + assertTrue(testDir.isDirectory) + assertTrue(testDir.canExecute()) + assertTrue(testDir.canRead()) + val allNonIgnored: List = findAllNonIgnored(gitIgnoreFileSet) + assertTrue( + allNonIgnored.contains(testDir.toPath()), + "The list $allNonIgnored is missing $testDir" + ) + + try { + assertTrue(testDir.setExecutable(false, false)) + assertTrue(testDir.setReadable(false, false)) + val allNonIgnored2: List = findAllNonIgnored(gitIgnoreFileSet) + assertFalse( + allNonIgnored2.contains(testDir.toPath()), + "The list $allNonIgnored SHOULD NOT contain $testDir" + ) + } finally { + assertTrue(testDir.setExecutable(true, false)) + assertTrue(testDir.setReadable(true, false)) + } + } + + companion object { + private val LOG: Logger = LoggerFactory.getLogger(TestGitIgnoreFiles::class.java) + + fun separatorsToWindows(path: String): String { + return path.replace("/", "\\") + } + + const val TEST_TREE_DIR: String = "src/test/resources/testtree" + val testTree: File = File(TEST_TREE_DIR) + val targetDirTestTree: File = File(TestGitIgnoreFiles::class.java.classLoader.getResource("testtree")?.file ?: throw IllegalStateException("This should not occur")) + } +} diff --git a/gitignore-reader/src/test/kotlin/nl/basjes/gitignore/TestJetBrains.java b/gitignore-reader/src/test/kotlin/nl/basjes/gitignore/TestJetBrains.java deleted file mode 100644 index c999dca..0000000 --- a/gitignore-reader/src/test/kotlin/nl/basjes/gitignore/TestJetBrains.java +++ /dev/null @@ -1,228 +0,0 @@ -/* - * CodeOwners Tools - * Copyright (C) 2023-2026 Niels Basjes - * - * 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 - * - * https://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 nl.basjes.gitignore; - -import org.junit.jupiter.api.Test; - -import java.io.File; -import java.util.regex.PatternSyntaxException; - -import static nl.basjes.gitignore.TestUtils.assertIgnore; -import static nl.basjes.gitignore.TestUtils.assertNotIgnore; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.fail; - -class TestJetBrains { - - @Test - void testJetBrainsIgnoreRules() { - // The JetBrains ignore plugin for IntelliJ has a lot of prebuilt rules to choose from. - // This simply loads all of them and tests a few edge cases. - // https://plugins.jetbrains.com/plugin/7495--ignore - try { - GitIgnoreFileSet ignoreFileSet = new GitIgnoreFileSet(new File("src/test/resources/JetBrains")); - ignoreFileSet.addGitIgnoreFile(new File("src/test/resources/JetBrains/JetBrains.gitignore")); - } catch (Exception e) { - fail("Should not throw anything"); - } - } - - // ------------------------------------------ - - @Test - void testCharacterRange() { - GitIgnore gitIgnore = new GitIgnore("*.sw[mnop]", true); - assertIgnore(gitIgnore, "test.swm"); - assertIgnore(gitIgnore, "logs/test.swp"); - assertNotIgnore(gitIgnore, "test.swa"); - assertNotIgnore(gitIgnore, "test.swma"); - } - - @Test - void testExtensionsRange() { - GitIgnore gitIgnore = new GitIgnore("coverage*[.json, .xml, .info]"); - assertIgnore(gitIgnore, "coverage.json"); - assertIgnore(gitIgnore, "coverage-001.json"); - assertIgnore(gitIgnore, "dir1/coverage-001.json"); - assertIgnore(gitIgnore, "dir2/coverage-001.json"); - assertNotIgnore(gitIgnore, "foo_coverage-001.json"); - - assertIgnore(gitIgnore, "coverage.xml"); - assertIgnore(gitIgnore, "coverage-001.xml"); - assertIgnore(gitIgnore, "dir1/coverage-001.xml"); - assertIgnore(gitIgnore, "dir2/coverage-001.xml"); - assertNotIgnore(gitIgnore, "foo_coverage-001.xml"); - - assertIgnore(gitIgnore, "coverage.info"); - assertIgnore(gitIgnore, "coverage-001.info"); - assertIgnore(gitIgnore, "dir1/coverage-001.info"); - assertIgnore(gitIgnore, "dir2/coverage-001.info"); - assertNotIgnore(gitIgnore, "foo_coverage-001.info"); - - assertNotIgnore(gitIgnore, "coverage.j"); - assertNotIgnore(gitIgnore, "coverage.x"); - assertNotIgnore(gitIgnore, "coverage.i"); - assertNotIgnore(gitIgnore, "coverage.,"); - assertNotIgnore(gitIgnore, "coverage."); - } - - @Test - void testIncorrectExtensionsRange() { - GitIgnore gitIgnore = new GitIgnore("coverage*[.json,]"); // << This becomes a normal regex ... - // This actually means: starts with "coverage" and ends with any of those characters ".json," - assertIgnore(gitIgnore, "coverage.json"); - assertIgnore(gitIgnore, "coverage-001.json"); - assertIgnore(gitIgnore, "dir1/coverage-001.json"); - assertIgnore(gitIgnore, "dir2/coverage-001.json"); - assertNotIgnore(gitIgnore, "foo_coverage-001.json"); - - // Yes, this bad expression actually matches this garbage also - assertIgnore(gitIgnore, "coverage.....jsnosnosjjjnn"); - assertIgnore(gitIgnore, "coverage-001.....jsnosnosjjjnn"); - assertIgnore(gitIgnore, "dir1/coverage-001.....jsnosnosjjjnn"); - assertIgnore(gitIgnore, "dir2/coverage-001.....jsnosnosjjjnn"); - assertNotIgnore(gitIgnore, "foo_coverage-001.....jsnosnosjjjnn"); - - assertNotIgnore(gitIgnore, "coverage.xml"); - assertNotIgnore(gitIgnore, "coverage-001.xml"); - assertNotIgnore(gitIgnore, "dir1/coverage-001.xml"); - assertNotIgnore(gitIgnore, "dir2/coverage-001.xml"); - assertNotIgnore(gitIgnore, "foo_coverage-001.xml"); - - assertIgnore(gitIgnore, "coverage.xml,"); - assertIgnore(gitIgnore, "coverage-001.xml,"); - assertIgnore(gitIgnore, "dir1/coverage-001.xml,"); - assertIgnore(gitIgnore, "dir2/coverage-001.xml,"); - assertNotIgnore(gitIgnore, "foo_coverage-001.xml,"); - - assertIgnore(gitIgnore, "coverage.info"); - assertIgnore(gitIgnore, "coverage-001.info"); - assertIgnore(gitIgnore, "dir1/coverage-001.info"); - assertIgnore(gitIgnore, "dir2/coverage-001.info"); - assertNotIgnore(gitIgnore, "foo_coverage-001.info"); - - assertIgnore(gitIgnore, "coverage.j"); - assertNotIgnore(gitIgnore, "coverage.x"); - assertNotIgnore(gitIgnore, "coverage.i"); - assertIgnore(gitIgnore, "coverage.,"); - assertIgnore(gitIgnore, "coverage."); - } - - @Test - void testSpecialCharactersTilde() { - GitIgnore gitIgnore = new GitIgnore( - "foo*\n" + - "foo\n" + - ".~lock.*\n" + - ".log.*\n" + - ".log\n", - true); - assertIgnore(gitIgnore, "foo"); - assertIgnore(gitIgnore, "/dir/foo"); - - assertIgnore(gitIgnore, "foobar"); - assertIgnore(gitIgnore, "/dir/foobar"); - - assertIgnore(gitIgnore, ".log.something1234"); - assertIgnore(gitIgnore, "/dir/.log.something1234"); - - assertIgnore(gitIgnore, ".~lock.something1234"); - assertIgnore(gitIgnore, "/dir/.~lock.something1234"); - - // Ensure the trailing . is not a wildcard. - assertNotIgnore(gitIgnore, ".logger"); - assertNotIgnore(gitIgnore, "/dir/.logger"); - assertNotIgnore(gitIgnore, ".~locker"); - assertNotIgnore(gitIgnore, "/dir/.~locker"); - } - - - @Test - void testSpecials() { - assertIgnore("", "*.project.~u", "dummy.project.~u", "/dir/dummy.project.~u"); - assertIgnore("", ".idea/**/workspace.xml", ".idea/something/workspace.xml"); - assertIgnore("", "[Bb]uild/", "build/foo.txt", "Build/foo.txt", "/dir/build/foo.txt", "/dir/Build/foo.txt"); - assertIgnore("", "[Ww][Ii][Nn]32/", "wIn32/", "wIn32/foo.txt", "WiN32/", "WiN32/foo.txt"); - assertIgnore("", "Generated\\ Files/", "Generated Files/", "Generated Files/foo.txt"); - assertIgnore("", "*~", "foo.txt~", "/dir/foo.txt~"); - assertIgnore("", "*~.nib", "foo.txt~.nib", "/dir/foo.txt~.nib"); - assertNotIgnore("", "*~.nib", "foo.txt~xnib", "/dir/foo.txt~xnib"); - assertIgnore("", "._*", "._foo.txt", "/dir/._bar.txt"); - assertIgnore("", "@eaDir", "@eaDir", "/dir/@eaDir"); - assertIgnore("", "\\#recycle", "#recycle", "/dir/#recycle"); - assertIgnore("", "*.py[cod]", "foo.pyc", "/dir/bar.pyo"); - assertIgnore("", "*$py.class", "foo.$py.class", "/dir/bar.$py.class"); - assertIgnore("", "*- [Bb]ackup ([0-9][0-9]).rdl", "/dir/foo - Backup (42).rdl"); - assertIgnore("", "*.lyx~", "foo.lyx~", "/dir/bar.lyx~"); - assertIgnore("", "~$*.doc*", "~$foo.docxx", "/dir/~$bar.doc"); - assertIgnore("", "*.l$?", "foo.l$1", "/dir/bar.l$b"); - assertIgnore("", "*.$$$", "foo.$$$", "/dir/bar.$$$"); - } - - - @Test - void testSpecialCharactersHash() { - assertIgnore("", ".#*", ".#foo", "/dir/.#foo"); - assertIgnore("", "\\#*\\#", "#foo#", "/dir/#foo#"); - assertIgnore("", ".\\#*", ".#foo", "/dir/.#foo"); - assertIgnore("", ".~lock.*#", ".~lock.foo#", "/dir/.~lock.foo#"); - assertIgnore("", "*.s#?", "foo.s#b", "/dir/foo.s#b"); - assertIgnore("", "*.b#?", "foo.b#b", "/dir/foo.b#b"); - assertIgnore("", "*.l#?", "foo.l#b", "/dir/foo.l#b"); - assertIgnore("", "*.b$?", "foo.b$b", "/dir/foo.b$b"); - assertIgnore("", "*.s$?", "foo.s$b", "/dir/foo.s$b"); - assertIgnore("", "*.l$?", "foo.l$b", "/dir/foo.l$b"); - assertIgnore("", "*.s#*", "foo.s#bar", "/dir/foo.s#bar"); - assertIgnore("", "*.b#*", "foo.b#bar", "/dir/foo.b#bar"); - assertIgnore("", "*#", "foo.#", "/dir/foo.#"); - assertIgnore("", "*#*#", "foo.#bar#", "/dir/foo.#bar#"); - assertIgnore("", "*.#*", "foo.#bar", "/dir/foo.#bar"); - assertIgnore("", "*.ss#*", "foo.ss#bar", "/dir/foo.ss#bar"); - assertIgnore("", ".#*.ss", ".#foo.ss", "/dir/.#foo.ss"); - - assertIgnore("", "*.lyx#", "foo.lyx#", "/dir/bar.lyx#"); - assertIgnore("", "*.l#?", "foo.l#2", "/dir/bar.l#a"); - assertIgnore("", "\\#*.rkt#", "#foo.rkt#", "/dir/#foo.rkt#"); - assertIgnore("", "\\#*.rkt#*#", "#foo.rkt#bar#", "/dir/#foo.rkt#bar#"); - } - - @Test - void testMultiStarsAndBraces() { - GitIgnore gitIgnore = new GitIgnore("*- Copy (*).*"); - assertIgnore(gitIgnore, "Something - Copy (1).docx"); - assertIgnore(gitIgnore, "/dir/Something - Copy (1).docx"); - } - - @Test - void testSpecialCharactersUnderscore() { - GitIgnore gitIgnore = new GitIgnore("_"); - assertIgnore(gitIgnore, "_"); - assertIgnore(gitIgnore, "/dir/_"); - } - - @Test - void testBadExpression() { - assertThrows(PatternSyntaxException.class, () -> new GitIgnore("[")); - } - - @Test - void testBadNegateExpression() { - assertThrows(PatternSyntaxException.class, () -> new GitIgnore("![")); - } - -} diff --git a/gitignore-reader/src/test/kotlin/nl/basjes/gitignore/TestJetBrains.kt b/gitignore-reader/src/test/kotlin/nl/basjes/gitignore/TestJetBrains.kt new file mode 100644 index 0000000..efd05d1 --- /dev/null +++ b/gitignore-reader/src/test/kotlin/nl/basjes/gitignore/TestJetBrains.kt @@ -0,0 +1,230 @@ +/* + * CodeOwners Tools + * Copyright (C) 2023-2026 Niels Basjes + * + * 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 + * + * https://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 nl.basjes.gitignore + +import nl.basjes.gitignore.TestUtils.assertIgnore +import nl.basjes.gitignore.TestUtils.assertNotIgnore +import kotlin.test.Test +import java.io.File +import java.util.regex.PatternSyntaxException +import kotlin.test.assertFailsWith +import kotlin.test.fail + +internal class TestJetBrains { + @Test + fun testJetBrainsIgnoreRules() { + // The JetBrains ignore plugin for IntelliJ has a lot of prebuilt rules to choose from. + // This simply loads all of them and tests a few edge cases. + // https://plugins.jetbrains.com/plugin/7495--ignore + try { + val ignoreFileSet = GitIgnoreFileSet(File("src/test/resources/JetBrains")) + ignoreFileSet.addGitIgnoreFile(File("src/test/resources/JetBrains/JetBrains.gitignore")) + } catch (e: Exception) { + fail("Should not throw anything") + } + } + + // ------------------------------------------ + @Test + fun testCharacterRange() { + val gitIgnore = GitIgnore("*.sw[mnop]", true) + assertIgnore(gitIgnore, "test.swm") + assertIgnore(gitIgnore, "logs/test.swp") + assertNotIgnore(gitIgnore, "test.swa") + assertNotIgnore(gitIgnore, "test.swma") + } + + @Test + fun testExtensionsRange() { + val gitIgnore = GitIgnore("coverage*[.json, .xml, .info]") + assertIgnore(gitIgnore, "coverage.json") + assertIgnore(gitIgnore, "coverage-001.json") + assertIgnore(gitIgnore, "dir1/coverage-001.json") + assertIgnore(gitIgnore, "dir2/coverage-001.json") + assertNotIgnore(gitIgnore, "foo_coverage-001.json") + + assertIgnore(gitIgnore, "coverage.xml") + assertIgnore(gitIgnore, "coverage-001.xml") + assertIgnore(gitIgnore, "dir1/coverage-001.xml") + assertIgnore(gitIgnore, "dir2/coverage-001.xml") + assertNotIgnore(gitIgnore, "foo_coverage-001.xml") + + assertIgnore(gitIgnore, "coverage.info") + assertIgnore(gitIgnore, "coverage-001.info") + assertIgnore(gitIgnore, "dir1/coverage-001.info") + assertIgnore(gitIgnore, "dir2/coverage-001.info") + assertNotIgnore(gitIgnore, "foo_coverage-001.info") + + assertNotIgnore(gitIgnore, "coverage.j") + assertNotIgnore(gitIgnore, "coverage.x") + assertNotIgnore(gitIgnore, "coverage.i") + assertNotIgnore(gitIgnore, "coverage.,") + assertNotIgnore(gitIgnore, "coverage.") + } + + @Test + fun testIncorrectExtensionsRange() { + val gitIgnore = GitIgnore("coverage*[.json,]") // << This becomes a normal regex ... + // This actually means: starts with "coverage" and ends with any of those characters ".json," + assertIgnore(gitIgnore, "coverage.json") + assertIgnore(gitIgnore, "coverage-001.json") + assertIgnore(gitIgnore, "dir1/coverage-001.json") + assertIgnore(gitIgnore, "dir2/coverage-001.json") + assertNotIgnore(gitIgnore, "foo_coverage-001.json") + + // Yes, this bad expression actually matches this garbage also + assertIgnore(gitIgnore, "coverage.....jsnosnosjjjnn") + assertIgnore(gitIgnore, "coverage-001.....jsnosnosjjjnn") + assertIgnore(gitIgnore, "dir1/coverage-001.....jsnosnosjjjnn") + assertIgnore(gitIgnore, "dir2/coverage-001.....jsnosnosjjjnn") + assertNotIgnore(gitIgnore, "foo_coverage-001.....jsnosnosjjjnn") + + assertNotIgnore(gitIgnore, "coverage.xml") + assertNotIgnore(gitIgnore, "coverage-001.xml") + assertNotIgnore(gitIgnore, "dir1/coverage-001.xml") + assertNotIgnore(gitIgnore, "dir2/coverage-001.xml") + assertNotIgnore(gitIgnore, "foo_coverage-001.xml") + + assertIgnore(gitIgnore, "coverage.xml,") + assertIgnore(gitIgnore, "coverage-001.xml,") + assertIgnore(gitIgnore, "dir1/coverage-001.xml,") + assertIgnore(gitIgnore, "dir2/coverage-001.xml,") + assertNotIgnore(gitIgnore, "foo_coverage-001.xml,") + + assertIgnore(gitIgnore, "coverage.info") + assertIgnore(gitIgnore, "coverage-001.info") + assertIgnore(gitIgnore, "dir1/coverage-001.info") + assertIgnore(gitIgnore, "dir2/coverage-001.info") + assertNotIgnore(gitIgnore, "foo_coverage-001.info") + + assertIgnore(gitIgnore, "coverage.j") + assertNotIgnore(gitIgnore, "coverage.x") + assertNotIgnore(gitIgnore, "coverage.i") + assertIgnore(gitIgnore, "coverage.,") + assertIgnore(gitIgnore, "coverage.") + } + + @Test + fun testSpecialCharactersTilde() { + val gitIgnore = GitIgnore( + "foo*\n" + + "foo\n" + + ".~lock.*\n" + + ".log.*\n" + + ".log\n", + true + ) + assertIgnore(gitIgnore, "foo") + assertIgnore(gitIgnore, "/dir/foo") + + assertIgnore(gitIgnore, "foobar") + assertIgnore(gitIgnore, "/dir/foobar") + + assertIgnore(gitIgnore, ".log.something1234") + assertIgnore(gitIgnore, "/dir/.log.something1234") + + assertIgnore(gitIgnore, ".~lock.something1234") + assertIgnore(gitIgnore, "/dir/.~lock.something1234") + + // Ensure the trailing . is not a wildcard. + assertNotIgnore(gitIgnore, ".logger") + assertNotIgnore(gitIgnore, "/dir/.logger") + assertNotIgnore(gitIgnore, ".~locker") + assertNotIgnore(gitIgnore, "/dir/.~locker") + } + + + @Test + fun testSpecials() { + assertIgnore("", "*.project.~u", "dummy.project.~u", "/dir/dummy.project.~u") + assertIgnore("", ".idea/**/workspace.xml", ".idea/something/workspace.xml") + assertIgnore( + "", + "[Bb]uild/", + "build/foo.txt", + "Build/foo.txt", + "/dir/build/foo.txt", + "/dir/Build/foo.txt" + ) + assertIgnore("", "[Ww][Ii][Nn]32/", "wIn32/", "wIn32/foo.txt", "WiN32/", "WiN32/foo.txt") + assertIgnore("", "Generated\\ Files/", "Generated Files/", "Generated Files/foo.txt") + assertIgnore("", "*~", "foo.txt~", "/dir/foo.txt~") + assertIgnore("", "*~.nib", "foo.txt~.nib", "/dir/foo.txt~.nib") + assertNotIgnore("", "*~.nib", "foo.txt~xnib", "/dir/foo.txt~xnib") + assertIgnore("", "._*", "._foo.txt", "/dir/._bar.txt") + assertIgnore("", "@eaDir", "@eaDir", "/dir/@eaDir") + assertIgnore("", "\\#recycle", "#recycle", "/dir/#recycle") + assertIgnore("", "*.py[cod]", "foo.pyc", "/dir/bar.pyo") + assertIgnore("", $$"*$py.class", $$"foo.$py.class", $$"/dir/bar.$py.class") + assertIgnore("", "*- [Bb]ackup ([0-9][0-9]).rdl", "/dir/foo - Backup (42).rdl") + assertIgnore("", "*.lyx~", "foo.lyx~", "/dir/bar.lyx~") + assertIgnore("", "~$*.doc*", $$"~$foo.docxx", $$"/dir/~$bar.doc") + assertIgnore("", "*.l$?", "foo.l$1", $$"/dir/bar.l$b") + assertIgnore("", "*.$$$", "foo.$$$", "/dir/bar.$$$") + } + + + @Test + fun testSpecialCharactersHash() { + assertIgnore("", ".#*", ".#foo", "/dir/.#foo") + assertIgnore("", "\\#*\\#", "#foo#", "/dir/#foo#") + assertIgnore("", ".\\#*", ".#foo", "/dir/.#foo") + assertIgnore("", ".~lock.*#", ".~lock.foo#", "/dir/.~lock.foo#") + assertIgnore("", "*.s#?", "foo.s#b", "/dir/foo.s#b") + assertIgnore("", "*.b#?", "foo.b#b", "/dir/foo.b#b") + assertIgnore("", "*.l#?", "foo.l#b", "/dir/foo.l#b") + assertIgnore("", "*.b$?", $$"foo.b$b", $$"/dir/foo.b$b") + assertIgnore("", "*.s$?", $$"foo.s$b", $$"/dir/foo.s$b") + assertIgnore("", "*.l$?", $$"foo.l$b", $$"/dir/foo.l$b") + assertIgnore("", "*.s#*", "foo.s#bar", "/dir/foo.s#bar") + assertIgnore("", "*.b#*", "foo.b#bar", "/dir/foo.b#bar") + assertIgnore("", "*#", "foo.#", "/dir/foo.#") + assertIgnore("", "*#*#", "foo.#bar#", "/dir/foo.#bar#") + assertIgnore("", "*.#*", "foo.#bar", "/dir/foo.#bar") + assertIgnore("", "*.ss#*", "foo.ss#bar", "/dir/foo.ss#bar") + assertIgnore("", ".#*.ss", ".#foo.ss", "/dir/.#foo.ss") + + assertIgnore("", "*.lyx#", "foo.lyx#", "/dir/bar.lyx#") + assertIgnore("", "*.l#?", "foo.l#2", "/dir/bar.l#a") + assertIgnore("", "\\#*.rkt#", "#foo.rkt#", "/dir/#foo.rkt#") + assertIgnore("", "\\#*.rkt#*#", "#foo.rkt#bar#", "/dir/#foo.rkt#bar#") + } + + @Test + fun testMultiStarsAndBraces() { + val gitIgnore = GitIgnore("*- Copy (*).*") + assertIgnore(gitIgnore, "Something - Copy (1).docx") + assertIgnore(gitIgnore, "/dir/Something - Copy (1).docx") + } + + @Test + fun testSpecialCharactersUnderscore() { + val gitIgnore = GitIgnore("_") + assertIgnore(gitIgnore, "_") + assertIgnore(gitIgnore, "/dir/_") + } + + @Test + fun testBadExpression() { + assertFailsWith { GitIgnore("[") } + } + + @Test + fun testBadNegateExpression() { + assertFailsWith { GitIgnore("![") } + } +} diff --git a/gitignore-reader/src/test/kotlin/nl/basjes/gitignore/TestUtils.java b/gitignore-reader/src/test/kotlin/nl/basjes/gitignore/TestUtils.java deleted file mode 100644 index e9b3442..0000000 --- a/gitignore-reader/src/test/kotlin/nl/basjes/gitignore/TestUtils.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * CodeOwners Tools - * Copyright (C) 2023-2026 Niels Basjes - * - * 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 - * - * https://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 nl.basjes.gitignore; - -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class TestUtils { - - public static void assertIgnore(String baseDir, String gitIgnore, String... filenames) { - assertIgnore(new GitIgnore(baseDir, gitIgnore), filenames); - } - - public static void assertIgnore(GitIgnore gitIgnore, String ... filenames) { - for (String filename : filenames) { - __assertIgnore(gitIgnore, filename); - } - } - - private static void __assertIgnore(GitIgnore gitIgnore, String filename) { - assertSame(true, - gitIgnore.isIgnoredFile(filename), - "Filename \"" + filename + "\" should match but did not.\n" + gitIgnore); - assertTrue(gitIgnore.ignoreFile(filename), - "Filename \"" + filename + "\" should match but did not.\n" + gitIgnore); - assertFalse(gitIgnore.keepFile(filename), - "Filename \"" + filename + "\" should match but did not.\n" + gitIgnore); - - // Same but now with a windows path separator - String wFilename = windowsFileName(filename); - assertSame(true, - gitIgnore.isIgnoredFile(wFilename), - "Filename \"" + wFilename + "\" should match but did not.\n" + gitIgnore); - assertTrue(gitIgnore.ignoreFile(wFilename), - "Filename \"" + wFilename + "\" should match but did not.\n" + gitIgnore); - assertFalse(gitIgnore.keepFile(wFilename), - "Filename \"" + wFilename + "\" should match but did not.\n" + gitIgnore); - } - - public static void assertNotIgnore(String baseDir, String gitIgnore, String... filenames) { - assertNotIgnore(new GitIgnore(baseDir, gitIgnore), filenames); - } - - public static void assertNotIgnore(GitIgnore gitIgnore, String ... filenames) { - for (String filename : filenames) { - __assertNotIgnore(gitIgnore, filename); - } - } - - private static void __assertNotIgnore(GitIgnore gitIgnore, String filename) { - Boolean isIgnoredFile = gitIgnore.isIgnoredFile(filename); - assertTrue( - isIgnoredFile == null || !isIgnoredFile, - "Filename \""+filename+"\" should NOT match but did.\n" + gitIgnore); - assertFalse(gitIgnore.ignoreFile(filename), - "Filename \""+filename+"\" should NOT match but did.\n" + gitIgnore); - assertTrue(gitIgnore.keepFile(filename), - "Filename \""+filename+"\" should NOT match but did.\n" + gitIgnore); - - // Same but now with a windows path separator - String wFilename = windowsFileName(filename); - Boolean wIsIgnoredFile = gitIgnore.isIgnoredFile(wFilename); - assertTrue( - wIsIgnoredFile == null || !wIsIgnoredFile, - "Filename \""+wFilename+"\" should NOT match but did.\n" + gitIgnore); - assertFalse(gitIgnore.ignoreFile(wFilename), - "Filename \""+wFilename+"\" should NOT match but did.\n" + gitIgnore); - assertTrue(gitIgnore.keepFile(wFilename), - "Filename \""+wFilename+"\" should NOT match but did.\n" + gitIgnore); - } - - - public static void assertNullMatch(GitIgnore gitIgnore, String filename) { - assertNull( - gitIgnore.isIgnoredFile(filename), - "Filename \""+filename+"\" should NOT match but did."); - - // Same but now with a windows path separator - String wFilename = windowsFileName(filename); - assertNull( - gitIgnore.isIgnoredFile(wFilename), - "Filename \""+wFilename+"\" should NOT match but did."); - } - - public static void assertNullMatch(String baseDir, String gitIgnore, String filename) { - assertNullMatch(new GitIgnore(baseDir, gitIgnore), filename); - } - - public static String windowsFileName(String filename) { - return filename.replace("/", "\\"); - } - - public static void verifyGeneratedRegex(String baseDir, String gitIgnoreContent, String expectedRegex) { - GitIgnore gitIgnore = new GitIgnore(baseDir, gitIgnoreContent); - List ignoreRules = gitIgnore.getIgnoreRules$gitignore_reader(); - assertEquals(1, ignoreRules.size()); - GitIgnore.IgnoreRule ignoreRule = ignoreRules.get(0); - assertEquals(expectedRegex, ignoreRule.getIgnorePattern().getPattern(), "Incorrect regex generated"); - } - -} diff --git a/gitignore-reader/src/test/kotlin/nl/basjes/gitignore/TestUtils.kt b/gitignore-reader/src/test/kotlin/nl/basjes/gitignore/TestUtils.kt new file mode 100644 index 0000000..c4f2dca --- /dev/null +++ b/gitignore-reader/src/test/kotlin/nl/basjes/gitignore/TestUtils.kt @@ -0,0 +1,141 @@ +/* + * CodeOwners Tools + * Copyright (C) 2023-2026 Niels Basjes + * + * 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 + * + * https://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 nl.basjes.gitignore + +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNull +import kotlin.test.assertSame +import kotlin.test.assertTrue + + +object TestUtils { + fun assertIgnore(baseDir: String, gitIgnore: String, vararg filenames: String) { + assertIgnore(GitIgnore(baseDir, gitIgnore), *filenames) + } + + fun assertIgnore(gitIgnore: GitIgnore, vararg filenames: String) { + for (filename in filenames) { + interalAssertIgnore(gitIgnore, filename) + } + } + + private fun interalAssertIgnore(gitIgnore: GitIgnore, filename: String) { + assertSame( + true, + gitIgnore.isIgnoredFile(filename), + "Filename \"$filename\" should match but did not.\n$gitIgnore" + ) + assertTrue( + gitIgnore.ignoreFile(filename), + "Filename \"$filename\" should match but did not.\n$gitIgnore" + ) + assertFalse( + gitIgnore.keepFile(filename), + "Filename \"$filename\" should match but did not.\n$gitIgnore" + ) + + // Same but now with a windows path separator + val wFilename = windowsFileName(filename) + assertSame( + true, + gitIgnore.isIgnoredFile(wFilename), + "Filename \"$wFilename\" should match but did not.\n$gitIgnore" + ) + assertTrue( + gitIgnore.ignoreFile(wFilename), + "Filename \"$wFilename\" should match but did not.\n$gitIgnore" + ) + assertFalse( + gitIgnore.keepFile(wFilename), + "Filename \"$wFilename\" should match but did not.\n$gitIgnore" + ) + } + + fun assertNotIgnore(baseDir: String, gitIgnore: String, vararg filenames: String) { + assertNotIgnore(GitIgnore(baseDir, gitIgnore), *filenames) + } + + fun assertNotIgnore(gitIgnore: GitIgnore, vararg filenames: String) { + for (filename in filenames) { + internalAssertNotIgnore(gitIgnore, filename) + } + } + + private fun internalAssertNotIgnore(gitIgnore: GitIgnore, filename: String) { + val isIgnoredFile = gitIgnore.isIgnoredFile(filename) + assertTrue( + isIgnoredFile == null || !isIgnoredFile, + "Filename \"$filename\" should NOT match but did.\n$gitIgnore" + ) + assertFalse( + gitIgnore.ignoreFile(filename), + "Filename \"$filename\" should NOT match but did.\n$gitIgnore" + ) + assertTrue( + gitIgnore.keepFile(filename), + "Filename \"$filename\" should NOT match but did.\n$gitIgnore" + ) + + // Same but now with a windows path separator + val wFilename = windowsFileName(filename) + val wIsIgnoredFile = gitIgnore.isIgnoredFile(wFilename) + assertTrue( + wIsIgnoredFile == null || !wIsIgnoredFile, + "Filename \"$wFilename\" should NOT match but did.\n$gitIgnore" + ) + assertFalse( + gitIgnore.ignoreFile(wFilename), + "Filename \"$wFilename\" should NOT match but did.\n$gitIgnore" + ) + assertTrue( + gitIgnore.keepFile(wFilename), + "Filename \"$wFilename\" should NOT match but did.\n$gitIgnore" + ) + } + + + fun assertNullMatch(gitIgnore: GitIgnore, filename: String) { + assertNull( + gitIgnore.isIgnoredFile(filename), + "Filename \"$filename\" should NOT match but did." + ) + + // Same but now with a windows path separator + val wFilename = windowsFileName(filename) + assertNull( + gitIgnore.isIgnoredFile(wFilename), + "Filename \"$wFilename\" should NOT match but did." + ) + } + +// fun assertNullMatch(baseDir: String, gitIgnore: String, filename: String) { +// assertNullMatch(GitIgnore(baseDir, gitIgnore), filename) +// } + + fun windowsFileName(filename: String): String { + return filename.replace("/", "\\") + } + + fun verifyGeneratedRegex(baseDir: String, gitIgnoreContent: String, expectedRegex: String?) { + val gitIgnore = GitIgnore(baseDir, gitIgnoreContent) + val ignoreRules: List = gitIgnore.ignoreRules + assertEquals(1, ignoreRules.size) + val ignoreRule = ignoreRules[0] + assertEquals(expectedRegex, ignoreRule.ignorePattern.pattern, "Incorrect regex generated") + } +} diff --git a/gitignore-reader/src/test/kotlin/org/junitpioneer/jupiter/EnvironmentVariableUtilsFacade.java b/gitignore-reader/src/test/kotlin/org/junitpioneer/jupiter/EnvironmentVariableUtilsFacade.kt similarity index 73% rename from gitignore-reader/src/test/kotlin/org/junitpioneer/jupiter/EnvironmentVariableUtilsFacade.java rename to gitignore-reader/src/test/kotlin/org/junitpioneer/jupiter/EnvironmentVariableUtilsFacade.kt index 397008c..b7f1601 100644 --- a/gitignore-reader/src/test/kotlin/org/junitpioneer/jupiter/EnvironmentVariableUtilsFacade.java +++ b/gitignore-reader/src/test/kotlin/org/junitpioneer/jupiter/EnvironmentVariableUtilsFacade.kt @@ -14,17 +14,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.junitpioneer.jupiter; +package org.junitpioneer.jupiter /** * By putting my own class in the same package as the package private class I want to access ... I can use it. */ -public class EnvironmentVariableUtilsFacade { - public static void set(String name, String value) { - EnvironmentVariableUtils.set(name, value); - } - public static void clear(String name) { - EnvironmentVariableUtils.clear(name); +object EnvironmentVariableUtilsFacade { + fun set(name: String?, value: String?) { + EnvironmentVariableUtils.set(name, value) } + fun clear(name: String?) { + EnvironmentVariableUtils.clear(name) + } } diff --git a/pom.xml b/pom.xml index aaf7755..df82451 100644 --- a/pom.xml +++ b/pom.xml @@ -84,11 +84,10 @@ - org.junit - junit-bom - ${junit5.version} - pom - import + org.jetbrains.kotlin + kotlin-test-junit5 + ${kotlin.version} + test From 6ba3fa53df7e6847d89d14013a5a4344b2212b47 Mon Sep 17 00:00:00 2001 From: Niels Basjes Date: Sun, 24 May 2026 17:44:09 +0200 Subject: [PATCH 2/3] fix: Code cleanup, less Java library use --- .../src/main/kotlin/nl/basjes/codeowners/Rule.kt | 10 ++++------ codeowners-validator/pom.xml | 1 - .../validator/gitlab/GitlabConfiguration.kt | 15 ++++++++------- .../basjes/codeowners/validator/utils/Problem.kt | 3 +-- .../codeowners/validator/utils/ProblemTable.kt | 4 ++-- .../main/kotlin/nl/basjes/gitignore/GitIgnore.kt | 3 +-- pom.xml | 8 ++++++++ 7 files changed, 24 insertions(+), 20 deletions(-) diff --git a/codeowners-reader/src/main/kotlin/nl/basjes/codeowners/Rule.kt b/codeowners-reader/src/main/kotlin/nl/basjes/codeowners/Rule.kt index dd335a2..8b0f5ae 100644 --- a/codeowners-reader/src/main/kotlin/nl/basjes/codeowners/Rule.kt +++ b/codeowners-reader/src/main/kotlin/nl/basjes/codeowners/Rule.kt @@ -16,8 +16,6 @@ */ package nl.basjes.codeowners -import java.util.regex.Pattern - abstract class Rule( /** * @return The provided file expression used to build this rule @@ -27,7 +25,7 @@ abstract class Rule( /** * The Pattern which was constructed from the provided fileExpression */ - val filePattern: Pattern + val filePattern: Regex var verbose: Boolean = false @@ -62,7 +60,7 @@ abstract class Rule( // The Globstar "foo/**/bar" must also match "foo/bar" // Process trailing /** before middle /** to handle them correctly: // 1. Trailing: "foo/**" matches "foo/" and its contents, but NOT "foo" or "foobar" - .replace( "/\\*\\*$".toRegex(), "/.*" ) + .replace("/\\*\\*$".toRegex(), "/.*") // 2. Middle: "foo/**/bar" matches both "foo/bar" and "foo/anything/bar" .replace("/**", "(/.*)?") @@ -92,14 +90,14 @@ abstract class Rule( // Remove duplication .replace("/+".toRegex(), "/") - filePattern = Pattern.compile(fileRegex) + filePattern = fileRegex.toRegex() } /** * @return True if the provided file matches the configured fileExpression. If not it returns false. */ fun matches(filename: String): Boolean { - val matches = filePattern.matcher(filename).find() + val matches = filePattern.containsMatchIn(filename) if (verbose) { val result = if(matches) "MATCH " else "NO MATCH" LOG.info("$result |{}| ~ |{}| --> {}", fileExpression, filePattern, filename) diff --git a/codeowners-validator/pom.xml b/codeowners-validator/pom.xml index ac0d450..6f53bb7 100644 --- a/codeowners-validator/pom.xml +++ b/codeowners-validator/pom.xml @@ -119,7 +119,6 @@ org.junit-pioneer junit-pioneer - 2.3.0 test diff --git a/codeowners-validator/src/main/kotlin/nl/basjes/codeowners/validator/gitlab/GitlabConfiguration.kt b/codeowners-validator/src/main/kotlin/nl/basjes/codeowners/validator/gitlab/GitlabConfiguration.kt index 4df3308..46cc132 100644 --- a/codeowners-validator/src/main/kotlin/nl/basjes/codeowners/validator/gitlab/GitlabConfiguration.kt +++ b/codeowners-validator/src/main/kotlin/nl/basjes/codeowners/validator/gitlab/GitlabConfiguration.kt @@ -204,15 +204,16 @@ ${problemLevels.toString().prependIndent(" ")} internalValue = System.getenv(usedEnvVariableName) ?.trim() internalValid = checkValidity(internalValue) - if (internalValid) { - sourceOrErrorMessage = "(via environment variable \"$usedEnvVariableName\")" - } else { - if (internalValue == null) { - sourceOrErrorMessage = "the environment variable \"$usedEnvVariableName\" does not exist" + sourceOrErrorMessage = + if (internalValid) { + "(via environment variable \"$usedEnvVariableName\")" } else { - sourceOrErrorMessage = "the value from environment variable \"$usedEnvVariableName\" is NOT valid" + if (internalValue == null) { + "the environment variable \"$usedEnvVariableName\" does not exist" + } else { + "the value from environment variable \"$usedEnvVariableName\" is NOT valid" + } } - } } companion object { diff --git a/codeowners-validator/src/main/kotlin/nl/basjes/codeowners/validator/utils/Problem.kt b/codeowners-validator/src/main/kotlin/nl/basjes/codeowners/validator/utils/Problem.kt index 367f83a..1f0ddcf 100644 --- a/codeowners-validator/src/main/kotlin/nl/basjes/codeowners/validator/utils/Problem.kt +++ b/codeowners-validator/src/main/kotlin/nl/basjes/codeowners/validator/utils/Problem.kt @@ -16,7 +16,6 @@ */ package nl.basjes.codeowners.validator.utils -import nl.basjes.codeowners.validator.utils.LogColor.RESET import nl.basjes.codeowners.validator.utils.Problem.ProblemColor.COLOR_ERROR_TAG import nl.basjes.codeowners.validator.utils.Problem.ProblemColor.COLOR_ERROR_TEXT import nl.basjes.codeowners.validator.utils.Problem.ProblemColor.COLOR_FATAL_TAG @@ -44,7 +43,7 @@ sealed class Problem private constructor( ERROR_TAG ("ERROR", COLOR_ERROR_TAG), FATAL_TAG ("FATAL", COLOR_FATAL_TAG); override fun toString(): String { - return "[${color}${text}${RESET}]" + return "[${color}${text}${LogColor.RESET}]" } } diff --git a/codeowners-validator/src/main/kotlin/nl/basjes/codeowners/validator/utils/ProblemTable.kt b/codeowners-validator/src/main/kotlin/nl/basjes/codeowners/validator/utils/ProblemTable.kt index 0339ed9..d33140d 100644 --- a/codeowners-validator/src/main/kotlin/nl/basjes/codeowners/validator/utils/ProblemTable.kt +++ b/codeowners-validator/src/main/kotlin/nl/basjes/codeowners/validator/utils/ProblemTable.kt @@ -73,7 +73,7 @@ class ProblemTable : StringTable() { get() = theProblems.isEmpty() override fun toString(): String { - clearCaches(); + clearCaches() val buffer = StringBuilder("\n") fun addLineInfo(line: String) { buffer.append("$INFO_TAG : ${COLOR_INFO_TEXT}${line}${RESET}\n") @@ -104,7 +104,7 @@ class ProblemTable : StringTable() { fun toLog(logger: Logger) { - clearCaches(); + clearCaches() logger.info(writeSeparator()) logger.info(writeHeaders()) logger.info(writeSeparator()) diff --git a/gitignore-reader/src/main/kotlin/nl/basjes/gitignore/GitIgnore.kt b/gitignore-reader/src/main/kotlin/nl/basjes/gitignore/GitIgnore.kt index ba10401..5a429f0 100644 --- a/gitignore-reader/src/main/kotlin/nl/basjes/gitignore/GitIgnore.kt +++ b/gitignore-reader/src/main/kotlin/nl/basjes/gitignore/GitIgnore.kt @@ -33,7 +33,7 @@ class GitIgnore @JvmOverloads constructor( verbose: Boolean = false ) { @JvmField - val projectRelativeBaseDir: String + val projectRelativeBaseDir: String = standardizeFilename(projectRelativeBaseDir + GITIGNORE_PATH_SEPARATOR) internal val ignoreRules: List var verbose = false @@ -52,7 +52,6 @@ class GitIgnore @JvmOverloads constructor( constructor(gitIgnoreContent: String, verbose: Boolean) : this("", gitIgnoreContent, verbose) init { - this.projectRelativeBaseDir = standardizeFilename(projectRelativeBaseDir + GITIGNORE_PATH_SEPARATOR) val allIgnoreRules: MutableList = mutableListOf() diff --git a/pom.xml b/pom.xml index df82451..3da4462 100644 --- a/pom.xml +++ b/pom.xml @@ -90,6 +90,14 @@ test + + org.junit + junit-bom + 5.14.3 + pom + import + + org.antlr antlr4-runtime From b1fc184f85513d5f6225c65a31899892211beb32 Mon Sep 17 00:00:00 2001 From: Niels Basjes Date: Sun, 24 May 2026 17:52:04 +0200 Subject: [PATCH 3/3] fix: Use Junit 6 with kotlin --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 3da4462..fe6c3d1 100644 --- a/pom.xml +++ b/pom.xml @@ -70,7 +70,7 @@ 2.9.1 4.13.2 - 6.1.0 + 6.1.0 2.0.18 1.5.32 2.21.3 @@ -93,7 +93,7 @@ org.junit junit-bom - 5.14.3 + ${junit.version} pom import