diff --git a/pom.xml b/pom.xml
index 73c3c72..40bb82e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -108,6 +108,12 @@ under the License.
junit-jupiter-api
test
+
+ org.mockito
+ mockito-core
+ 4.11.0
+ test
+
org.codehaus.plexus
plexus-testing
diff --git a/src/main/java/org/apache/maven/plugins/clean/Cleaner.java b/src/main/java/org/apache/maven/plugins/clean/Cleaner.java
index f5c25d2..52f1e25 100644
--- a/src/main/java/org/apache/maven/plugins/clean/Cleaner.java
+++ b/src/main/java/org/apache/maven/plugins/clean/Cleaner.java
@@ -273,7 +273,8 @@ private boolean isSymbolicLink(Path path) throws IOException {
* @throws IOException If a file/directory could not be deleted and failOnError
is true
.
*/
private int delete(File file, boolean failOnError, boolean retryOnError) throws IOException {
- if (!file.delete()) {
+ IOException failure = delete(file);
+ if (failure != null) {
boolean deleted = false;
if (retryOnError) {
@@ -289,7 +290,7 @@ private int delete(File file, boolean failOnError, boolean retryOnError) throws
} catch (InterruptedException e) {
// ignore
}
- deleted = file.delete() || !file.exists();
+ deleted = delete(file) == null || !file.exists();
}
} else {
deleted = !file.exists();
@@ -297,10 +298,10 @@ private int delete(File file, boolean failOnError, boolean retryOnError) throws
if (!deleted) {
if (failOnError) {
- throw new IOException("Failed to delete " + file);
+ throw new IOException("Failed to delete " + file, failure);
} else {
if (log.isWarnEnabled()) {
- log.warn("Failed to delete " + file);
+ log.warn("Failed to delete " + file, failure);
}
return 1;
}
@@ -310,6 +311,15 @@ private int delete(File file, boolean failOnError, boolean retryOnError) throws
return 0;
}
+ private static IOException delete(File file) {
+ try {
+ Files.delete(file.toPath());
+ } catch (IOException e) {
+ return e;
+ }
+ return null;
+ }
+
private static class Result {
private int failures;
diff --git a/src/test/java/org/apache/maven/plugins/clean/CleanerTest.java b/src/test/java/org/apache/maven/plugins/clean/CleanerTest.java
new file mode 100644
index 0000000..e958664
--- /dev/null
+++ b/src/test/java/org/apache/maven/plugins/clean/CleanerTest.java
@@ -0,0 +1,143 @@
+/*
+ * 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
+ *
+ * http://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.apache.maven.plugins.clean;
+
+import java.io.IOException;
+import java.nio.file.AccessDeniedException;
+import java.nio.file.DirectoryNotEmptyException;
+import java.nio.file.FileSystems;
+import java.nio.file.Path;
+import java.nio.file.attribute.PosixFilePermission;
+import java.nio.file.attribute.PosixFilePermissions;
+import java.util.Set;
+
+import org.apache.maven.plugin.logging.Log;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
+
+import static java.nio.file.Files.createDirectory;
+import static java.nio.file.Files.createFile;
+import static java.nio.file.Files.exists;
+import static java.nio.file.Files.setPosixFilePermissions;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+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.assertThrows;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class CleanerTest {
+
+ private static final boolean POSIX_COMPLIANT =
+ FileSystems.getDefault().supportedFileAttributeViews().contains("posix");
+
+ private final Log log = mock();
+
+ @Test
+ void deleteSucceedsDeeply(@TempDir Path tempDir) throws Exception {
+ assumeTrue(POSIX_COMPLIANT);
+ final Path basedir = createDirectory(tempDir.resolve("target")).toRealPath();
+ final Path file = createFile(basedir.resolve("file"));
+ final Cleaner cleaner = new Cleaner(null, log, false, null, null);
+ cleaner.delete(basedir.toFile(), null, false, true, false);
+ assertFalse(exists(basedir));
+ assertFalse(exists(file));
+ }
+
+ @Test
+ void deleteFailsWithoutRetryWhenNoPermission(@TempDir Path tempDir) throws Exception {
+ assumeTrue(POSIX_COMPLIANT);
+ when(log.isWarnEnabled()).thenReturn(true);
+ final Path basedir = createDirectory(tempDir.resolve("target")).toRealPath();
+ createFile(basedir.resolve("file"));
+ // Remove the executable flag to prevent directory listing, which will result in a DirectoryNotEmptyException.
+ final Set permissions = PosixFilePermissions.fromString("rw-rw-r--");
+ setPosixFilePermissions(basedir, permissions);
+ final Cleaner cleaner = new Cleaner(null, log, false, null, null);
+ final IOException exception =
+ assertThrows(IOException.class, () -> cleaner.delete(basedir.toFile(), null, false, true, false));
+ verify(log, never()).warn(any(CharSequence.class), any(Throwable.class));
+ assertEquals("Failed to delete " + basedir, exception.getMessage());
+ final DirectoryNotEmptyException cause =
+ assertInstanceOf(DirectoryNotEmptyException.class, exception.getCause());
+ assertEquals(basedir.toString(), cause.getMessage());
+ }
+
+ @Test
+ void deleteFailsAfterRetryWhenNoPermission(@TempDir Path tempDir) throws Exception {
+ assumeTrue(POSIX_COMPLIANT);
+ final Path basedir = createDirectory(tempDir.resolve("target")).toRealPath();
+ createFile(basedir.resolve("file"));
+ // Remove the executable flag to prevent directory listing, which will result in a DirectoryNotEmptyException.
+ final Set permissions = PosixFilePermissions.fromString("rw-rw-r--");
+ setPosixFilePermissions(basedir, permissions);
+ final Cleaner cleaner = new Cleaner(null, log, false, null, null);
+ final IOException exception =
+ assertThrows(IOException.class, () -> cleaner.delete(basedir.toFile(), null, false, true, true));
+ assertEquals("Failed to delete " + basedir, exception.getMessage());
+ final DirectoryNotEmptyException cause =
+ assertInstanceOf(DirectoryNotEmptyException.class, exception.getCause());
+ assertEquals(basedir.toString(), cause.getMessage());
+ }
+
+ @Test
+ void deleteLogsWarningWithoutRetryWhenNoPermission(@TempDir Path tempDir) throws Exception {
+ assumeTrue(POSIX_COMPLIANT);
+ when(log.isWarnEnabled()).thenReturn(true);
+ final Path basedir = createDirectory(tempDir.resolve("target")).toRealPath();
+ final Path file = createFile(basedir.resolve("file"));
+ // Remove the writable flag to prevent deletion of the file, which will result in an AccessDeniedException.
+ final Set permissions = PosixFilePermissions.fromString("r-xr-xr-x");
+ setPosixFilePermissions(basedir, permissions);
+ final Cleaner cleaner = new Cleaner(null, log, false, null, null);
+ assertDoesNotThrow(() -> cleaner.delete(basedir.toFile(), null, false, false, false));
+ verify(log, times(2)).warn(any(CharSequence.class), any(Throwable.class));
+ InOrder inOrder = inOrder(log);
+ ArgumentCaptor cause1 = ArgumentCaptor.forClass(AccessDeniedException.class);
+ inOrder.verify(log).warn(eq("Failed to delete " + file), cause1.capture());
+ assertEquals(file.toString(), cause1.getValue().getMessage());
+ ArgumentCaptor cause2 = ArgumentCaptor.forClass(DirectoryNotEmptyException.class);
+ inOrder.verify(log).warn(eq("Failed to delete " + basedir), cause2.capture());
+ assertEquals(basedir.toString(), cause2.getValue().getMessage());
+ }
+
+ @Test
+ void deleteDoesNotLogAnythingWhenNoPermissionAndWarnDisabled(@TempDir Path tempDir) throws Exception {
+ assumeTrue(POSIX_COMPLIANT);
+ when(log.isWarnEnabled()).thenReturn(false);
+ final Path basedir = createDirectory(tempDir.resolve("target")).toRealPath();
+ createFile(basedir.resolve("file"));
+ // Remove the writable flag to prevent deletion of the file, which will result in an AccessDeniedException.
+ final Set permissions = PosixFilePermissions.fromString("r-xr-xr-x");
+ setPosixFilePermissions(basedir, permissions);
+ final Cleaner cleaner = new Cleaner(null, log, false, null, null);
+ assertDoesNotThrow(() -> cleaner.delete(basedir.toFile(), null, false, false, false));
+ verify(log, never()).warn(any(CharSequence.class), any(Throwable.class));
+ }
+}