Skip to content

Commit cb2127f

Browse files
[MCLEAN-124] Leverage Files.delete(Path) API to provide more accurate reason in case of failure (#60)
* MCLEAN-124 Leverage Files.delete(Path) API to provide more accurate reason in case of failure * Use Mockito + fix unit tests for Windows * Renamed variable error -> failure * Removed the resetting of permissions in tests, relying on JUnit @tempdir to take care of permissions issues when clearing the temporary directory * Fixed typo a -> an * Simplified the setting of permissions in tests
1 parent bf21c55 commit cb2127f

File tree

3 files changed

+163
-4
lines changed

3 files changed

+163
-4
lines changed

pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,12 @@ under the License.
108108
<artifactId>junit-jupiter-api</artifactId>
109109
<scope>test</scope>
110110
</dependency>
111+
<dependency>
112+
<groupId>org.mockito</groupId>
113+
<artifactId>mockito-core</artifactId>
114+
<version>4.11.0</version>
115+
<scope>test</scope>
116+
</dependency>
111117
<dependency>
112118
<groupId>org.codehaus.plexus</groupId>
113119
<artifactId>plexus-testing</artifactId>

src/main/java/org/apache/maven/plugins/clean/Cleaner.java

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,8 @@ private boolean isSymbolicLink(Path path) throws IOException {
273273
* @throws IOException If a file/directory could not be deleted and <code>failOnError</code> is <code>true</code>.
274274
*/
275275
private int delete(File file, boolean failOnError, boolean retryOnError) throws IOException {
276-
if (!file.delete()) {
276+
IOException failure = delete(file);
277+
if (failure != null) {
277278
boolean deleted = false;
278279

279280
if (retryOnError) {
@@ -289,18 +290,18 @@ private int delete(File file, boolean failOnError, boolean retryOnError) throws
289290
} catch (InterruptedException e) {
290291
// ignore
291292
}
292-
deleted = file.delete() || !file.exists();
293+
deleted = delete(file) == null || !file.exists();
293294
}
294295
} else {
295296
deleted = !file.exists();
296297
}
297298

298299
if (!deleted) {
299300
if (failOnError) {
300-
throw new IOException("Failed to delete " + file);
301+
throw new IOException("Failed to delete " + file, failure);
301302
} else {
302303
if (log.isWarnEnabled()) {
303-
log.warn("Failed to delete " + file);
304+
log.warn("Failed to delete " + file, failure);
304305
}
305306
return 1;
306307
}
@@ -310,6 +311,15 @@ private int delete(File file, boolean failOnError, boolean retryOnError) throws
310311
return 0;
311312
}
312313

314+
private static IOException delete(File file) {
315+
try {
316+
Files.delete(file.toPath());
317+
} catch (IOException e) {
318+
return e;
319+
}
320+
return null;
321+
}
322+
313323
private static class Result {
314324

315325
private int failures;
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.maven.plugins.clean;
20+
21+
import java.io.IOException;
22+
import java.nio.file.AccessDeniedException;
23+
import java.nio.file.DirectoryNotEmptyException;
24+
import java.nio.file.FileSystems;
25+
import java.nio.file.Path;
26+
import java.nio.file.attribute.PosixFilePermission;
27+
import java.nio.file.attribute.PosixFilePermissions;
28+
import java.util.Set;
29+
30+
import org.apache.maven.plugin.logging.Log;
31+
import org.junit.jupiter.api.Test;
32+
import org.junit.jupiter.api.io.TempDir;
33+
import org.mockito.ArgumentCaptor;
34+
import org.mockito.InOrder;
35+
36+
import static java.nio.file.Files.createDirectory;
37+
import static java.nio.file.Files.createFile;
38+
import static java.nio.file.Files.exists;
39+
import static java.nio.file.Files.setPosixFilePermissions;
40+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
41+
import static org.junit.jupiter.api.Assertions.assertEquals;
42+
import static org.junit.jupiter.api.Assertions.assertFalse;
43+
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
44+
import static org.junit.jupiter.api.Assertions.assertThrows;
45+
import static org.junit.jupiter.api.Assumptions.assumeTrue;
46+
import static org.mockito.ArgumentMatchers.any;
47+
import static org.mockito.ArgumentMatchers.eq;
48+
import static org.mockito.Mockito.inOrder;
49+
import static org.mockito.Mockito.mock;
50+
import static org.mockito.Mockito.never;
51+
import static org.mockito.Mockito.times;
52+
import static org.mockito.Mockito.verify;
53+
import static org.mockito.Mockito.when;
54+
55+
class CleanerTest {
56+
57+
private static final boolean POSIX_COMPLIANT =
58+
FileSystems.getDefault().supportedFileAttributeViews().contains("posix");
59+
60+
private final Log log = mock();
61+
62+
@Test
63+
void deleteSucceedsDeeply(@TempDir Path tempDir) throws Exception {
64+
assumeTrue(POSIX_COMPLIANT);
65+
final Path basedir = createDirectory(tempDir.resolve("target")).toRealPath();
66+
final Path file = createFile(basedir.resolve("file"));
67+
final Cleaner cleaner = new Cleaner(null, log, false, null, null);
68+
cleaner.delete(basedir.toFile(), null, false, true, false);
69+
assertFalse(exists(basedir));
70+
assertFalse(exists(file));
71+
}
72+
73+
@Test
74+
void deleteFailsWithoutRetryWhenNoPermission(@TempDir Path tempDir) throws Exception {
75+
assumeTrue(POSIX_COMPLIANT);
76+
when(log.isWarnEnabled()).thenReturn(true);
77+
final Path basedir = createDirectory(tempDir.resolve("target")).toRealPath();
78+
createFile(basedir.resolve("file"));
79+
// Remove the executable flag to prevent directory listing, which will result in a DirectoryNotEmptyException.
80+
final Set<PosixFilePermission> permissions = PosixFilePermissions.fromString("rw-rw-r--");
81+
setPosixFilePermissions(basedir, permissions);
82+
final Cleaner cleaner = new Cleaner(null, log, false, null, null);
83+
final IOException exception =
84+
assertThrows(IOException.class, () -> cleaner.delete(basedir.toFile(), null, false, true, false));
85+
verify(log, never()).warn(any(CharSequence.class), any(Throwable.class));
86+
assertEquals("Failed to delete " + basedir, exception.getMessage());
87+
final DirectoryNotEmptyException cause =
88+
assertInstanceOf(DirectoryNotEmptyException.class, exception.getCause());
89+
assertEquals(basedir.toString(), cause.getMessage());
90+
}
91+
92+
@Test
93+
void deleteFailsAfterRetryWhenNoPermission(@TempDir Path tempDir) throws Exception {
94+
assumeTrue(POSIX_COMPLIANT);
95+
final Path basedir = createDirectory(tempDir.resolve("target")).toRealPath();
96+
createFile(basedir.resolve("file"));
97+
// Remove the executable flag to prevent directory listing, which will result in a DirectoryNotEmptyException.
98+
final Set<PosixFilePermission> permissions = PosixFilePermissions.fromString("rw-rw-r--");
99+
setPosixFilePermissions(basedir, permissions);
100+
final Cleaner cleaner = new Cleaner(null, log, false, null, null);
101+
final IOException exception =
102+
assertThrows(IOException.class, () -> cleaner.delete(basedir.toFile(), null, false, true, true));
103+
assertEquals("Failed to delete " + basedir, exception.getMessage());
104+
final DirectoryNotEmptyException cause =
105+
assertInstanceOf(DirectoryNotEmptyException.class, exception.getCause());
106+
assertEquals(basedir.toString(), cause.getMessage());
107+
}
108+
109+
@Test
110+
void deleteLogsWarningWithoutRetryWhenNoPermission(@TempDir Path tempDir) throws Exception {
111+
assumeTrue(POSIX_COMPLIANT);
112+
when(log.isWarnEnabled()).thenReturn(true);
113+
final Path basedir = createDirectory(tempDir.resolve("target")).toRealPath();
114+
final Path file = createFile(basedir.resolve("file"));
115+
// Remove the writable flag to prevent deletion of the file, which will result in an AccessDeniedException.
116+
final Set<PosixFilePermission> permissions = PosixFilePermissions.fromString("r-xr-xr-x");
117+
setPosixFilePermissions(basedir, permissions);
118+
final Cleaner cleaner = new Cleaner(null, log, false, null, null);
119+
assertDoesNotThrow(() -> cleaner.delete(basedir.toFile(), null, false, false, false));
120+
verify(log, times(2)).warn(any(CharSequence.class), any(Throwable.class));
121+
InOrder inOrder = inOrder(log);
122+
ArgumentCaptor<AccessDeniedException> cause1 = ArgumentCaptor.forClass(AccessDeniedException.class);
123+
inOrder.verify(log).warn(eq("Failed to delete " + file), cause1.capture());
124+
assertEquals(file.toString(), cause1.getValue().getMessage());
125+
ArgumentCaptor<DirectoryNotEmptyException> cause2 = ArgumentCaptor.forClass(DirectoryNotEmptyException.class);
126+
inOrder.verify(log).warn(eq("Failed to delete " + basedir), cause2.capture());
127+
assertEquals(basedir.toString(), cause2.getValue().getMessage());
128+
}
129+
130+
@Test
131+
void deleteDoesNotLogAnythingWhenNoPermissionAndWarnDisabled(@TempDir Path tempDir) throws Exception {
132+
assumeTrue(POSIX_COMPLIANT);
133+
when(log.isWarnEnabled()).thenReturn(false);
134+
final Path basedir = createDirectory(tempDir.resolve("target")).toRealPath();
135+
createFile(basedir.resolve("file"));
136+
// Remove the writable flag to prevent deletion of the file, which will result in an AccessDeniedException.
137+
final Set<PosixFilePermission> permissions = PosixFilePermissions.fromString("r-xr-xr-x");
138+
setPosixFilePermissions(basedir, permissions);
139+
final Cleaner cleaner = new Cleaner(null, log, false, null, null);
140+
assertDoesNotThrow(() -> cleaner.delete(basedir.toFile(), null, false, false, false));
141+
verify(log, never()).warn(any(CharSequence.class), any(Throwable.class));
142+
}
143+
}

0 commit comments

Comments
 (0)