Copilot commented on code in PR #16639: URL: https://github.com/apache/grails-core/pull/16639#discussion_r4264431246
########## grails-bootstrap/src/test/groovy/org/grails/io/support/AntPathMatcherSpec.groovy: ########## @@ -0,0 +1,327 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.grails.io.support + +import java.util.regex.PatternSyntaxException + +import spock.lang.Specification +import spock.lang.Unroll + +class AntPathMatcherSpec extends Specification { + + private final AntPathMatcher matcher = new AntPathMatcher() Review Comment: The spec uses a shared `matcher` instance across all features, but this test mutates its configuration. If any assertion in the `expect:` block fails, the `when:` block (reset) won’t run and the modified separator can leak into subsequent tests, causing cascading failures. Prefer constructing a local `AntPathMatcher` inside this feature, or add a `cleanup:` (or `try/finally`) that always restores the separator regardless of assertion outcomes. ########## grails-bootstrap/src/test/groovy/org/grails/io/support/ByteArrayResourceSpec.groovy: ########## @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.grails.io.support + +import spock.lang.Specification +import spock.lang.Unroll + +class ByteArrayResourceSpec extends Specification { + + @Unroll + void 'byte array resource exposes its content and metadata for #content'() { + given: + byte[] bytes = content as byte[] + def resource = new ByteArrayResource(bytes) + + expect: + resource.exists() + resource.isReadable() + resource.contentLength() == bytes.length + resource.lastModified() == 0 + resource.getDescription() == 'resource loaded from byte array' + resource.getFilename() == resource.getDescription() + resource.getInputStream().withCloseable { it.readAllBytes() } == bytes + + where: + content << [[], [0, 1, 127, -128, -1]] + } + + void 'custom description is also used as filename'() { + given: + def resource = new ByteArrayResource([1, 2] as byte[], 'example.bin') + + expect: + resource.getDescription() == 'example.bin' + resource.getFilename() == 'example.bin' + } + + void 'each input stream has independent position and can be reopened after closing'() { + given: + def resource = new ByteArrayResource([1, 2, 3] as byte[]) + InputStream first = resource.getInputStream() + InputStream second = resource.getInputStream() + + expect: + !first.is(second) + first.read() == 1 + first.read() == 2 + second.read() == 1 + + when: + first.close() + + then: + resource.getInputStream().withCloseable { it.readAllBytes() } == [1, 2, 3] as byte[] + second.read() == 2 + + cleanup: + first.close() + second.close() + } + + @Unroll + void '#operation is unsupported for an in-memory resource'() { + given: + def resource = new ByteArrayResource(new byte[0]) + + when: + resource."$operation"(*arguments) + + then: + UnsupportedOperationException exception = thrown() + exception.message == "Method $operation not supported" + + where: + operation | arguments + 'getURL' | [] + 'getURI' | [] + 'getFile' | [] + 'createRelative' | ['other.bin'] + } Review Comment: Asserting the exact exception message makes the test brittle to harmless message wording changes (while behavior remains correct). Consider asserting only the exception type, or relaxing the message assertion (e.g., verify it contains the operation name) to keep the test resilient. ########## grails-bootstrap/src/test/groovy/org/grails/io/support/AntPathMatcherSpec.groovy: ########## @@ -0,0 +1,327 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.grails.io.support + +import java.util.regex.PatternSyntaxException + +import spock.lang.Specification +import spock.lang.Unroll + +class AntPathMatcherSpec extends Specification { + + private final AntPathMatcher matcher = new AntPathMatcher() + + @Unroll + void 'match #pattern against #path returns #expected'() { + expect: + matcher.match(pattern, path) == expected + + where: + pattern | path | expected + '/docs/file.txt' | '/docs/file.txt' | true + '/docs/file.txt' | '/docs/other.txt' | false + '/docs/file.txt' | '/docs/file.txt/more' | false + '' | '' | true + '' | '/' | false + '/' | '/' | true + '/' | '' | false + '/docs' | 'docs' | false + 'docs' | '/docs' | false + '/docs/' | '/docs' | false + '/docs' | '/docs/' | false + '/docs/' | '/docs/' | true + '/docs//file.txt' | '/docs/file.txt' | true + '/docs/file.txt' | '/docs//file.txt' | true + 'docs/file.txt' | 'docs/file.txt' | true + '/docs/t?st.txt' | '/docs/test.txt' | true + '/docs/t?st.txt' | '/docs/tst.txt' | false + '/docs/t?st.txt' | '/docs/teest.txt' | false + '/docs/?.txt' | '/docs/a.txt' | true + '/docs/?.txt' | '/docs/a/b.txt' | false + '/docs/*.txt' | '/docs/file.txt' | true + '/docs/*.txt' | '/docs/.txt' | true + '/docs/*.txt' | '/docs/file.csv' | false + '/docs/*.txt' | '/docs/sub/file.txt' | false + '/docs/*' | '/docs/' | true + '/docs/*' | '/docs' | false + '/docs/*/' | '/docs/file' | false + '/docs/*/' | '/docs/file/' | true + '/docs/**' | '/docs' | true + '/docs/**' | '/docs/' | true + '/docs/**' | '/docs/a/b/c.txt' | true + '/docs/**/file.txt' | '/docs/file.txt' | true + '/docs/**/file.txt' | '/docs/a/b/file.txt' | true + '/docs/**/file.txt' | '/docs/a/b/other.txt' | false + '/docs/**/a/file.txt' | '/docs/file.txt' | false + '/docs/**/a/**/file.txt' | '/docs/x/a/y/file.txt' | true + '/docs/**/a/b/**/file.txt' | '/docs/x/a/no/a/b/y/file.txt' | true + '/docs/**/a/b/**/file.txt' | '/docs/x/a/no/b/y/file.txt' | false + '/docs/**/a/b/**/file.txt' | '/docs/x/file.txt' | false + '/docs/**/**/file.txt' | '/docs/a/b/file.txt' | true + '/docs/**/**' | '/docs' | true + '/docs/**/a/*' | '/docs/a' | false + '**' | '' | true + '*' | '' | false + '/**' | '/' | true + '/?' | '/' | false + '/docs/a.b+(c)[d]$' | '/docs/a.b+(c)[d]$' | true + '/docs/a.b' | '/docs/axb' | false + '/Docs/*.TXT' | '/docs/file.txt' | false + '/docs/ file ' | '/docs/file' | false + '/docs/ file ' | '/docs/ file ' | true + '/docs/{name}' | '/docs/file' | true + '/docs/{id:[0-9]{2}}' | '/docs/42' | true + '/docs/{id:[0-9]{2}}' | '/docs/4' | false + '/docs/{id:[0-9]+}' | '/docs/name' | false + '/docs/{unclosed' | '/docs/{unclosed' | true + } + + @Unroll + void 'matchStart #pattern against #path differs from full matching when appropriate'() { + expect: + matcher.matchStart(pattern, path) == startMatch + matcher.match(pattern, path) == fullMatch + + where: + pattern | path | startMatch | fullMatch + '/docs/file.txt' | '/docs' | true | false + '/docs/*/file.txt' | '/docs/a' | true | false + '/docs/**/file.txt' | '/docs/a/other.txt' | true | false + '/docs/**/file.txt' | '/other/a/file.txt' | false | false + '/docs/file.txt' | '/docs/other.txt' | false | false + '/docs' | '/docs/a' | false | false + '/docs' | '/docs/' | false | false + '/docs/file.txt' | '/docs/file.txt' | true | true + 'docs/file.txt' | '/docs' | false | false + '**/file.txt' | '' | true | false + '' | '' | true | true + } + + @Unroll + void 'extract wildcard path from #pattern and #path'() { + expect: + matcher.extractPathWithinPattern(pattern, path) == expected + + where: + pattern | path | expected + '/docs/cvs/commit.html' | '/docs/cvs/commit.html' | '' + '/docs/*' | '/docs/cvs/commit' | 'cvs/commit' + '/docs/cvs/*.html' | '/docs/cvs/commit.html' | 'commit.html' + '/docs/**' | '/docs/cvs/commit' | 'cvs/commit' + '/docs/**/*.html' | '/docs/cvs/commit.html' | 'cvs/commit.html' + '/*.html' | '/docs/cvs/commit.html' | 'docs/cvs/commit.html' + '*.html' | '/docs/cvs/commit.html' | '/docs/cvs/commit.html' + '*' | '/docs/cvs/commit.html' | '/docs/cvs/commit.html' + '/docs/t?st/*' | '/docs/test/file' | 'test/file' + '/docs/{name}' | '/docs/file' | '' + '/docs/*/fixed/*' | '/docs/a/fixed/b' | 'a/b' + '/docs/*' | '/docs' | '' + '/docs' | '/docs/a/b' | '/a/b' + '' | 'a/b' | 'a/b' + '' | '' | '' + } + + @Unroll + void 'extract URI variables from #pattern and #path'() { + expect: + matcher.extractUriTemplateVariables(pattern, path) == expected + + where: + pattern | path | expected + '/users/{user}' | '/users/alice' | [user: 'alice'] + '/{first}/{second}' | '/one/two' | [first: 'one', second: 'two'] + '/files/{name}.{extension}' | '/files/report.pdf' | [name: 'report', extension: 'pdf'] + '/files/{name}.txt' | '/files/.txt' | [name: ''] + '/users/{id:[0-9]{2}}' | '/users/42' | [id: '42'] + '/users/{id:(?:ab|cd)}' | '/users/ab' | [id: 'ab'] + '/users/{id:[0-9]+}' | '/users/alice' | [:] + '/{name}/**/{file}.txt' | '/docs/a/b/readme.txt' | [name: 'docs', file: 'readme'] + '/{name}/{name}' | '/first/last' | [name: 'last'] + '/docs/file.txt' | '/docs/file.txt' | [:] + '/users/{user}' | '/other/alice' | [:] + '/{user}/required' | '/alice/other' | [user: 'alice'] + '' | '' | [:] + } + + void 'URI variables preserve insertion order and extraction results are independent'() { + when: + Map<String, String> first = matcher.extractUriTemplateVariables('/{one}/{two}', '/a/b') + Map<String, String> second = matcher.extractUriTemplateVariables('/{one}/{two}', '/c/d') + + then: + first.keySet().toList() == ['one', 'two'] + first == [one: 'a', two: 'b'] + second == [one: 'c', two: 'd'] + !first.is(second) + } + + @Unroll + void 'combine #first and #second using legacy pattern rules'() { + expect: + matcher.combine(first, second) == expected + + where: + first | second | expected + null | null | '' + '' | '' | '' + null | '/hotels' | '/hotels' + '/hotels' | null | '/hotels' + '' | '/hotels' | '/hotels' + '/hotels' | '' | '/hotels' + '/hotels' | '/hotels' | '/hotels' + '/hotels/*' | '/hotels/booking' | '/hotels/booking' + '/hotels' | '/booking' | '/hotels/booking' + '/hotels' | 'booking' | '/hotels/booking' + '/hotels/' | 'booking' | '/hotels/booking' + '/hotels/' | '/booking' | '/hotels//booking' + '/hotels/*' | '/booking' | '/hotels/booking' + '/hotels/*' | 'booking' | '/hotels/booking' + '/hotels/**' | '/booking' | '/hotels/**/booking' + '/hotels/**' | 'booking' | '/hotels/**/booking' + '/hotels/*' | '{hotel}' | '/hotels/{hotel}' + '/hotels/**' | '{hotel}' | '/hotels/**/{hotel}' + '/{hotel}' | '/booking' | '/{hotel}/booking' + '/*.html' | '/hotels.html' | '/hotels.html' + '/*.html' | '/hotels' | '/hotels.html' + '/*.html' | '/*.txt' | '/*.html' + '/report.html' | '/other.txt' | '/report.html' + '/users/{id:[}' | '/users/42' | '/users/{id:[}/users/42' + ' ' | 'booking' | ' /booking' + } + + @Unroll + void 'recognize wildcard patterns without treating URI templates as wildcards'() { + expect: + matcher.isPattern(path) == expected + + where: + path | expected + '' | false + '/docs/file.txt' | false + '/docs/{name}' | false + '/docs/*' | true + '/docs/**' | true + '/docs/?.txt' | true + } + + void 'custom path separator applies to matching and both extraction methods and null resets it'() { + given: + matcher.setPathSeparator('.') + + expect: + matcher.match('org.**.File', 'org.grails.io.File') + !matcher.match('org.*.File', 'org.grails.io.File') + !matcher.match('.org.*', 'org.grails') + matcher.matchStart('org.grails.File', 'org.grails') + matcher.extractPathWithinPattern('org.*', 'org.grails.io') == 'grails.io' + matcher.extractUriTemplateVariables('org.{name}.File', 'org.grails.File') == [name: 'grails'] + + when: + matcher.setPathSeparator(null) + + then: + matcher.match('/org/**/File', '/org/grails/io/File') + !matcher.match('org.grails.File', 'org/grails/File') + } Review Comment: The spec uses a shared `matcher` instance across all features, but this test mutates its configuration. If any assertion in the `expect:` block fails, the `when:` block (reset) won’t run and the modified separator can leak into subsequent tests, causing cascading failures. Prefer constructing a local `AntPathMatcher` inside this feature, or add a `cleanup:` (or `try/finally`) that always restores the separator regardless of assertion outcomes. ########## grails-bootstrap/src/test/groovy/org/grails/io/support/AntPathMatcherSpec.groovy: ########## @@ -0,0 +1,327 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.grails.io.support + +import java.util.regex.PatternSyntaxException + +import spock.lang.Specification +import spock.lang.Unroll + +class AntPathMatcherSpec extends Specification { + + private final AntPathMatcher matcher = new AntPathMatcher() + + @Unroll + void 'match #pattern against #path returns #expected'() { + expect: + matcher.match(pattern, path) == expected + + where: + pattern | path | expected + '/docs/file.txt' | '/docs/file.txt' | true + '/docs/file.txt' | '/docs/other.txt' | false + '/docs/file.txt' | '/docs/file.txt/more' | false + '' | '' | true + '' | '/' | false + '/' | '/' | true + '/' | '' | false + '/docs' | 'docs' | false + 'docs' | '/docs' | false + '/docs/' | '/docs' | false + '/docs' | '/docs/' | false + '/docs/' | '/docs/' | true + '/docs//file.txt' | '/docs/file.txt' | true + '/docs/file.txt' | '/docs//file.txt' | true + 'docs/file.txt' | 'docs/file.txt' | true + '/docs/t?st.txt' | '/docs/test.txt' | true + '/docs/t?st.txt' | '/docs/tst.txt' | false + '/docs/t?st.txt' | '/docs/teest.txt' | false + '/docs/?.txt' | '/docs/a.txt' | true + '/docs/?.txt' | '/docs/a/b.txt' | false + '/docs/*.txt' | '/docs/file.txt' | true + '/docs/*.txt' | '/docs/.txt' | true + '/docs/*.txt' | '/docs/file.csv' | false + '/docs/*.txt' | '/docs/sub/file.txt' | false + '/docs/*' | '/docs/' | true + '/docs/*' | '/docs' | false + '/docs/*/' | '/docs/file' | false + '/docs/*/' | '/docs/file/' | true + '/docs/**' | '/docs' | true + '/docs/**' | '/docs/' | true + '/docs/**' | '/docs/a/b/c.txt' | true + '/docs/**/file.txt' | '/docs/file.txt' | true + '/docs/**/file.txt' | '/docs/a/b/file.txt' | true + '/docs/**/file.txt' | '/docs/a/b/other.txt' | false + '/docs/**/a/file.txt' | '/docs/file.txt' | false + '/docs/**/a/**/file.txt' | '/docs/x/a/y/file.txt' | true + '/docs/**/a/b/**/file.txt' | '/docs/x/a/no/a/b/y/file.txt' | true + '/docs/**/a/b/**/file.txt' | '/docs/x/a/no/b/y/file.txt' | false + '/docs/**/a/b/**/file.txt' | '/docs/x/file.txt' | false + '/docs/**/**/file.txt' | '/docs/a/b/file.txt' | true + '/docs/**/**' | '/docs' | true + '/docs/**/a/*' | '/docs/a' | false + '**' | '' | true + '*' | '' | false + '/**' | '/' | true + '/?' | '/' | false + '/docs/a.b+(c)[d]$' | '/docs/a.b+(c)[d]$' | true + '/docs/a.b' | '/docs/axb' | false + '/Docs/*.TXT' | '/docs/file.txt' | false + '/docs/ file ' | '/docs/file' | false + '/docs/ file ' | '/docs/ file ' | true + '/docs/{name}' | '/docs/file' | true + '/docs/{id:[0-9]{2}}' | '/docs/42' | true + '/docs/{id:[0-9]{2}}' | '/docs/4' | false + '/docs/{id:[0-9]+}' | '/docs/name' | false + '/docs/{unclosed' | '/docs/{unclosed' | true + } + + @Unroll + void 'matchStart #pattern against #path differs from full matching when appropriate'() { + expect: + matcher.matchStart(pattern, path) == startMatch + matcher.match(pattern, path) == fullMatch + + where: + pattern | path | startMatch | fullMatch + '/docs/file.txt' | '/docs' | true | false + '/docs/*/file.txt' | '/docs/a' | true | false + '/docs/**/file.txt' | '/docs/a/other.txt' | true | false + '/docs/**/file.txt' | '/other/a/file.txt' | false | false + '/docs/file.txt' | '/docs/other.txt' | false | false + '/docs' | '/docs/a' | false | false + '/docs' | '/docs/' | false | false + '/docs/file.txt' | '/docs/file.txt' | true | true + 'docs/file.txt' | '/docs' | false | false + '**/file.txt' | '' | true | false + '' | '' | true | true + } + + @Unroll + void 'extract wildcard path from #pattern and #path'() { + expect: + matcher.extractPathWithinPattern(pattern, path) == expected + + where: + pattern | path | expected + '/docs/cvs/commit.html' | '/docs/cvs/commit.html' | '' + '/docs/*' | '/docs/cvs/commit' | 'cvs/commit' + '/docs/cvs/*.html' | '/docs/cvs/commit.html' | 'commit.html' + '/docs/**' | '/docs/cvs/commit' | 'cvs/commit' + '/docs/**/*.html' | '/docs/cvs/commit.html' | 'cvs/commit.html' + '/*.html' | '/docs/cvs/commit.html' | 'docs/cvs/commit.html' + '*.html' | '/docs/cvs/commit.html' | '/docs/cvs/commit.html' + '*' | '/docs/cvs/commit.html' | '/docs/cvs/commit.html' + '/docs/t?st/*' | '/docs/test/file' | 'test/file' + '/docs/{name}' | '/docs/file' | '' + '/docs/*/fixed/*' | '/docs/a/fixed/b' | 'a/b' + '/docs/*' | '/docs' | '' + '/docs' | '/docs/a/b' | '/a/b' + '' | 'a/b' | 'a/b' + '' | '' | '' + } + + @Unroll + void 'extract URI variables from #pattern and #path'() { + expect: + matcher.extractUriTemplateVariables(pattern, path) == expected + + where: + pattern | path | expected + '/users/{user}' | '/users/alice' | [user: 'alice'] + '/{first}/{second}' | '/one/two' | [first: 'one', second: 'two'] + '/files/{name}.{extension}' | '/files/report.pdf' | [name: 'report', extension: 'pdf'] + '/files/{name}.txt' | '/files/.txt' | [name: ''] + '/users/{id:[0-9]{2}}' | '/users/42' | [id: '42'] + '/users/{id:(?:ab|cd)}' | '/users/ab' | [id: 'ab'] + '/users/{id:[0-9]+}' | '/users/alice' | [:] + '/{name}/**/{file}.txt' | '/docs/a/b/readme.txt' | [name: 'docs', file: 'readme'] + '/{name}/{name}' | '/first/last' | [name: 'last'] + '/docs/file.txt' | '/docs/file.txt' | [:] + '/users/{user}' | '/other/alice' | [:] + '/{user}/required' | '/alice/other' | [user: 'alice'] + '' | '' | [:] + } + + void 'URI variables preserve insertion order and extraction results are independent'() { + when: + Map<String, String> first = matcher.extractUriTemplateVariables('/{one}/{two}', '/a/b') + Map<String, String> second = matcher.extractUriTemplateVariables('/{one}/{two}', '/c/d') + + then: + first.keySet().toList() == ['one', 'two'] + first == [one: 'a', two: 'b'] + second == [one: 'c', two: 'd'] + !first.is(second) + } + + @Unroll + void 'combine #first and #second using legacy pattern rules'() { + expect: + matcher.combine(first, second) == expected + + where: + first | second | expected + null | null | '' + '' | '' | '' + null | '/hotels' | '/hotels' + '/hotels' | null | '/hotels' + '' | '/hotels' | '/hotels' + '/hotels' | '' | '/hotels' + '/hotels' | '/hotels' | '/hotels' + '/hotels/*' | '/hotels/booking' | '/hotels/booking' + '/hotels' | '/booking' | '/hotels/booking' + '/hotels' | 'booking' | '/hotels/booking' + '/hotels/' | 'booking' | '/hotels/booking' + '/hotels/' | '/booking' | '/hotels//booking' + '/hotels/*' | '/booking' | '/hotels/booking' + '/hotels/*' | 'booking' | '/hotels/booking' + '/hotels/**' | '/booking' | '/hotels/**/booking' + '/hotels/**' | 'booking' | '/hotels/**/booking' + '/hotels/*' | '{hotel}' | '/hotels/{hotel}' + '/hotels/**' | '{hotel}' | '/hotels/**/{hotel}' + '/{hotel}' | '/booking' | '/{hotel}/booking' + '/*.html' | '/hotels.html' | '/hotels.html' + '/*.html' | '/hotels' | '/hotels.html' + '/*.html' | '/*.txt' | '/*.html' + '/report.html' | '/other.txt' | '/report.html' + '/users/{id:[}' | '/users/42' | '/users/{id:[}/users/42' + ' ' | 'booking' | ' /booking' + } + + @Unroll + void 'recognize wildcard patterns without treating URI templates as wildcards'() { + expect: + matcher.isPattern(path) == expected + + where: + path | expected + '' | false + '/docs/file.txt' | false + '/docs/{name}' | false + '/docs/*' | true + '/docs/**' | true + '/docs/?.txt' | true + } + + void 'custom path separator applies to matching and both extraction methods and null resets it'() { + given: + matcher.setPathSeparator('.') + + expect: + matcher.match('org.**.File', 'org.grails.io.File') + !matcher.match('org.*.File', 'org.grails.io.File') + !matcher.match('.org.*', 'org.grails') + matcher.matchStart('org.grails.File', 'org.grails') + matcher.extractPathWithinPattern('org.*', 'org.grails.io') == 'grails.io' + matcher.extractUriTemplateVariables('org.{name}.File', 'org.grails.File') == [name: 'grails'] + + when: + matcher.setPathSeparator(null) + + then: + matcher.match('/org/**/File', '/org/grails/io/File') + !matcher.match('org.grails.File', 'org/grails/File') + } + + @Unroll + void 'null input to #operation is rejected'() { + when: + matcher."$operation"(*arguments) + + then: + thrown(NullPointerException) + + where: + operation | arguments + 'match' | [null, '/docs'] + 'match' | ['/docs', null] + 'matchStart' | [null, '/docs'] + 'matchStart' | ['/docs', null] + 'isPattern' | [null] + 'extractPathWithinPattern' | [null, '/docs'] + 'extractPathWithinPattern' | ['/docs', null] + 'extractUriTemplateVariables' | [null, '/docs'] + 'extractUriTemplateVariables' | ['/docs', null] + } Review Comment: The feature name says inputs are \"rejected\", but the assertions encode a specific contract: `NullPointerException`. If the intent is to enforce that exact exception type, consider renaming the test to explicitly state it (e.g., \"null input throws NullPointerException\"). If the intent is only to assert rejection, consider asserting a more semantically meaningful exception type/message contract used by the project (to avoid over-coupling to NPE as an implementation detail). -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
