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..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,19 +16,16 @@
*/
package nl.basjes.codeowners
-import java.util.regex.Pattern
-
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
*/
- val filePattern: Pattern
+ val filePattern: Regex
var verbose: Boolean = false
@@ -63,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("/**", "(/.*)?")
@@ -93,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-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..6f53bb7 100644
--- a/codeowners-validator/pom.xml
+++ b/codeowners-validator/pom.xml
@@ -111,15 +111,14 @@
- org.junit.jupiter
- junit-jupiter-engine
+ org.jetbrains.kotlin
+ kotlin-test-junit5
test
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 fe85e9b..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
@@ -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()
@@ -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/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/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 8b83295..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
@@ -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 {
@@ -72,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")
@@ -103,7 +104,7 @@ class ProblemTable : StringTable() {
fun toLog(logger: Logger) {
- clearCaches();
+ clearCaches()
logger.info(writeSeparator())
logger.info(writeHeaders())
logger.info(writeSeparator())
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/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/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..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
@@ -83,10 +83,17 @@
+
+ org.jetbrains.kotlin
+ kotlin-test-junit5
+ ${kotlin.version}
+ test
+
+
org.junit
junit-bom
- ${junit5.version}
+ ${junit.version}
pom
import