You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by jd...@apache.org on 2026/10/16 17:02:07 UTC

(grails-core) 03/06: Test File values for null instead of truth in the file system interaction

This is an automated email from the ASF dual-hosted git repository.

jdaugherty pushed a commit to branch 7.0.x
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit 81e4dc83731d28dd05ed56ac5e5b7106fdc80024
Author: James Daugherty <jd...@jdresources.net>
AuthorDate: Fri Oct 16 12:55:58 2026 -0400

    Test File values for null instead of truth in the file system interaction
    
    Groovy 5 and later evaluate a java.io.File that does not exist as false,
    where Groovy 4 treated every File as true. The guards in both copies of
    FileSystemInteractionImpl relied on the old meaning, so on Grails 8 and later
    projectPath returned an empty string for a file that is not there yet, which
    is exactly the state a template destination is in after a failed render has
    deleted it: the TemplateRendererImpl error message then read "Error rendering
    template to destination : ..." with no path. The same guards decided the base
    directory in file(), the copy spec destination and the empty-path check in
    resource(). Each one now tests for null (or an empty path) explicitly.
    
    Covered by a feature on the shell-cli FileSystemInteractionImplSpec and a new
    spec for the grails.dev.commands.io copy, both using files that do not exist.
---
 .../commands/io/FileSystemInteractionImpl.groovy   |  9 +--
 .../io/FileSystemInteractionImplSpec.groovy        | 75 ++++++++++++++++++++++
 .../commands/io/FileSystemInteractionImpl.groovy   |  9 +--
 .../io/FileSystemInteractionImplSpec.groovy        | 22 +++++++
 4 files changed, 107 insertions(+), 8 deletions(-)

diff --git a/grails-core/src/main/groovy/grails/dev/commands/io/FileSystemInteractionImpl.groovy b/grails-core/src/main/groovy/grails/dev/commands/io/FileSystemInteractionImpl.groovy
index f57e4ffa81..0f37ff555d 100644
--- a/grails-core/src/main/groovy/grails/dev/commands/io/FileSystemInteractionImpl.groovy
+++ b/grails-core/src/main/groovy/grails/dev/commands/io/FileSystemInteractionImpl.groovy
@@ -85,7 +85,7 @@ class FileSystemInteractionImpl implements FileSystemInteraction {
         FileSystemInteraction.CopySpec spec = new FileSystemInteraction.CopySpec()
         callable.delegate = spec
         callable.call()
-        if (spec.from && spec.into) {
+        if (spec.from && spec.into != null) {
             if (spec.from instanceof Iterable) {
                 copyAll((Iterable) spec.from, spec.into)
             }
@@ -158,7 +158,7 @@ class FileSystemInteractionImpl implements FileSystemInteraction {
         if (path instanceof File) return (File) path
         else if (path instanceof Resource) return ((Resource) path).file
         else {
-            new File(baseDir ?: new File('.'), path.toString())
+            new File(baseDir != null ? baseDir : new File('.'), path.toString())
         }
     }
 
@@ -213,7 +213,7 @@ class FileSystemInteractionImpl implements FileSystemInteraction {
      */
     @Override
     Resource resource(Object path) {
-        if (!path) return null
+        if (path == null || path.toString().isEmpty()) return null
         if (path instanceof Resource) return (Resource) path
         def f = file(path)
         if (f?.exists() && f.isFile()) {
@@ -261,7 +261,8 @@ class FileSystemInteractionImpl implements FileSystemInteraction {
     @Override
     String projectPath(Object path) {
         def file = file(path)
-        if (file) {
+        // a File that does not exist is false on Groovy 5 and later, so test for null explicitly
+        if (file != null) {
             def basePath = baseDir.canonicalPath
             return (file.canonicalPath - basePath).substring(1)
         }
diff --git a/grails-core/src/test/groovy/grails/dev/commands/io/FileSystemInteractionImplSpec.groovy b/grails-core/src/test/groovy/grails/dev/commands/io/FileSystemInteractionImplSpec.groovy
new file mode 100644
index 0000000000..ddd22a28f0
--- /dev/null
+++ b/grails-core/src/test/groovy/grails/dev/commands/io/FileSystemInteractionImplSpec.groovy
@@ -0,0 +1,75 @@
+/*
+ *  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 grails.dev.commands.io
+
+import org.grails.io.support.FileSystemResource
+import spock.lang.Specification
+import spock.lang.TempDir
+
+class FileSystemInteractionImplSpec extends Specification {
+
+    @TempDir
+    File directory
+
+    FileSystemInteractionImpl interaction
+
+    void setup() {
+        interaction = new FileSystemInteractionImpl(directory)
+    }
+
+    def 'files and resources are resolved relative to the project and existing objects are preserved'() {
+        given:
+        def file = new File(directory, 'example.txt')
+        file.text = 'example'
+        def resource = new FileSystemResource(file)
+
+        expect:
+        interaction.file('example.txt') == file
+        interaction.file(file).is(file)
+        interaction.file(resource) == file
+        interaction.resource(resource).is(resource)
+        interaction.resource('example.txt').file == file
+        interaction.resource(null) == null
+        interaction.resource('') == null
+        !interaction.resource('missing.txt').exists()
+        interaction.projectPath(file) == 'example.txt'
+    }
+
+    def 'files that do not exist yet are resolved like existing ones'() {
+        given: 'a project directory that has not been created and files below it'
+        def pending = new File(directory, 'pending')
+        def pendingInteraction = new FileSystemInteractionImpl(pending)
+        def missing = new File(directory, 'nested/missing.txt')
+        def existing = interaction.file('present.txt')
+        existing.text = 'present'
+
+        when:
+        interaction.copy {
+            from(existing)
+            into(new File(directory, 'target'))
+        }
+
+        then:
+        interaction.projectPath(missing) == 'nested/missing.txt'
+        interaction.projectPath('missing.txt') == 'missing.txt'
+        !interaction.resource(missing).exists()
+        pendingInteraction.file('build.gradle') == new File(pending, 'build.gradle')
+        interaction.file('target/present.txt').text == 'present'
+    }
+}
diff --git a/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/io/FileSystemInteractionImpl.groovy b/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/io/FileSystemInteractionImpl.groovy
index 6dbd0aadb2..53f8b5f6b0 100644
--- a/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/io/FileSystemInteractionImpl.groovy
+++ b/grails-shell-cli/src/main/groovy/org/grails/cli/profile/commands/io/FileSystemInteractionImpl.groovy
@@ -87,7 +87,7 @@ class FileSystemInteractionImpl implements FileSystemInteraction {
         FileSystemInteraction.CopySpec spec = new FileSystemInteraction.CopySpec()
         callable.delegate = spec
         callable.call()
-        if (spec.from && spec.into) {
+        if (spec.from && spec.into != null) {
             if (spec.from instanceof Iterable) {
                 copyAll((Iterable) spec.from, spec.into)
             }
@@ -161,7 +161,7 @@ class FileSystemInteractionImpl implements FileSystemInteraction {
         else if (path instanceof Resource) return ((Resource) path).file
         else {
             def baseDir = executionContext.baseDir
-            new File(baseDir ?: new File('.'), path.toString())
+            new File(baseDir != null ? baseDir : new File('.'), path.toString())
         }
     }
 
@@ -216,7 +216,7 @@ class FileSystemInteractionImpl implements FileSystemInteraction {
      */
     @Override
     Resource resource(Object path) {
-        if (!path) return null
+        if (path == null || path.toString().isEmpty()) return null
         if (path instanceof Resource) return (Resource) path
         def f = file(path)
         if (f?.exists() && f.isFile()) {
@@ -264,7 +264,8 @@ class FileSystemInteractionImpl implements FileSystemInteraction {
     @Override
     String projectPath(Object path) {
         def file = file(path)
-        if (file) {
+        // a File that does not exist is false on Groovy 5 and later, so test for null explicitly
+        if (file != null) {
             def basePath = executionContext.baseDir.canonicalPath
             return (file.canonicalPath - basePath).substring(1)
         }
diff --git a/grails-shell-cli/src/test/groovy/org/grails/cli/profile/commands/io/FileSystemInteractionImplSpec.groovy b/grails-shell-cli/src/test/groovy/org/grails/cli/profile/commands/io/FileSystemInteractionImplSpec.groovy
index 953b976bee..c7acbb161d 100644
--- a/grails-shell-cli/src/test/groovy/org/grails/cli/profile/commands/io/FileSystemInteractionImplSpec.groovy
+++ b/grails-shell-cli/src/test/groovy/org/grails/cli/profile/commands/io/FileSystemInteractionImplSpec.groovy
@@ -65,6 +65,28 @@ class FileSystemInteractionImplSpec extends Specification {
         interaction.projectPath(file) == 'example.txt'
     }
 
+    def 'files that do not exist yet are resolved like existing ones'() {
+        given: 'a project directory that has not been created and files below it'
+        def pending = new File(directory, 'pending')
+        def pendingInteraction = new FileSystemInteractionImpl(Stub(ExecutionContext) { getBaseDir() >> pending })
+        def missing = new File(directory, 'nested/missing.txt')
+        def existing = interaction.file('present.txt')
+        existing.text = 'present'
+
+        when:
+        interaction.copy {
+            from(existing)
+            into(new File(directory, 'target'))
+        }
+
+        then:
+        interaction.projectPath(missing) == 'nested/missing.txt'
+        interaction.projectPath('missing.txt') == 'missing.txt'
+        !interaction.resource(missing).exists()
+        pendingInteraction.file('build.gradle') == new File(pending, 'build.gradle')
+        interaction.file('target/present.txt').text == 'present'
+    }
+
     def 'mkdir and delete operate on project files and return the interaction for chaining'() {
         when:
         def result = interaction.mkdir('nested/child')