Skip to content

Commit

Permalink
Merge branch 'maven-clean-plugin-3.x' into master-tmp-merge-3.x
Browse files Browse the repository at this point in the history
~ Conflicts:
~	.github/workflows/release-drafter.yml
~	pom.xml
~	src/it/dangling-symlinks/setup.groovy
~	src/it/symlink-dont-follow/setup.groovy
~	src/main/java/org/apache/maven/plugins/clean/CleanMojo.java
~	src/main/java/org/apache/maven/plugins/clean/Cleaner.java
  • Loading branch information
peterdemaeyer committed Jan 31, 2025
2 parents 2d7979c + 5e797fc commit ca474c6
Show file tree
Hide file tree
Showing 5 changed files with 177 additions and 25 deletions.
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,12 @@ under the License.
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.11.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-api-impl</artifactId>
Expand Down
18 changes: 7 additions & 11 deletions src/it/dangling-symlinks/setup.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,24 @@
* under the License.
*/

import java.io.*;
import java.nio.file.*;
import java.util.*;
import java.util.jar.*;
import java.util.regex.*;
import org.apache.maven.plugins.clean.*;
import java.nio.file.attribute.*;

try
{
File targetDir = new File( basedir, "target" );
File link = new File( targetDir, "link" );
File target = new File( targetDir, "link-target.txt" );
Path targetDir = basedir.toPath().resolve( "target" );
Path link = targetDir.resolve( "link" );
Path target = targetDir.resolve( "link-target.txt" );

System.out.println( "Creating symlink " + link + " -> " + target );
Files.createSymbolicLink( link.toPath(), target.toPath() );
if ( !link.exists() )
Files.createSymbolicLink( link, target, new FileAttribute[0] );
if ( !Files.exists( link, new LinkOption[0] ) )
{
System.out.println( "Platform does not support symlinks, skipping test." );
}

System.out.println( "Deleting symlink target " + target );
target.delete();
Files.delete( target );
}
catch( Exception ex )
{
Expand Down
16 changes: 5 additions & 11 deletions src/it/symlink-dont-follow/setup.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,8 @@
* under the License.
*/

import java.io.*;
import java.nio.file.*;
import java.util.*;
import java.util.jar.*;
import java.util.regex.*;
import org.apache.maven.plugins.clean.*;
import java.nio.file.attribute.*;

def pairs =
[
Expand All @@ -34,13 +30,11 @@ def pairs =

for ( pair : pairs )
{
File target = new File( basedir, pair[0] );
File link = new File( basedir, pair[1] );
Path target = basedir.toPath().resolve( pair[0] );
Path link = basedir.toPath().resolve( pair[1] );
println "Creating symlink " + link + " -> " + target;
Path targetPath = target.toPath();
Path linkPath = link.toPath();
Files.createSymbolicLink( linkPath, targetPath );
if ( !link.exists() )
Files.createSymbolicLink( link, target, new FileAttribute[0] );
if ( !Files.exists( link, new LinkOption[0] ) )
{
println "Platform does not support symlinks, skipping test.";
return;
Expand Down
6 changes: 3 additions & 3 deletions src/main/java/org/apache/maven/plugins/clean/CleanMojo.java
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ public class CleanMojo implements org.apache.maven.api.plugin.Mojo {
* <code>${maven.multiModuleProjectDirectory}/target/.clean</code> directory will be used. If the
* <code>${build.directory}</code> has been modified, you'll have to adjust this property explicitly.
* In order for fast clean to work correctly, this directory and the various directories that will be deleted
* should usually reside on the same volume. The exact conditions are system dependant though, but if an atomic
* should usually reside on the same volume. The exact conditions are system-dependent though, but if an atomic
* move is not supported, the standard deletion mechanism will be used.
*
* @since 3.2
Expand All @@ -201,8 +201,8 @@ public class CleanMojo implements org.apache.maven.api.plugin.Mojo {
* Mode to use when using fast clean. Values are: <code>background</code> to start deletion immediately and
* waiting for all files to be deleted when the session ends, <code>at-end</code> to indicate that the actual
* deletion should be performed synchronously when the session ends, or <code>defer</code> to specify that
* the actual file deletion should be started in the background when the session ends (this should only be used
* when maven is embedded in a long running process).
* the actual file deletion should be started in the background when the session ends. This should only be used
* when maven is embedded in a long-running process.
*
* @since 3.2
* @see #fast
Expand Down
156 changes: 156 additions & 0 deletions src/test/java/org/apache/maven/plugins/clean/CleanerTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/*
* 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.Path;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.Set;

import org.apache.maven.api.plugin.Log;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledOnOs;
import org.junit.jupiter.api.condition.OS;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.ArgumentCaptor;
import org.mockito.InOrder;

import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
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.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 final Log log = mock();

@Test
@DisabledOnOs(OS.WINDOWS)
void deleteSucceedsDeeply(@TempDir Path tempDir) throws Exception {
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, null, false, true, false);
assertFalse(exists(basedir));
assertFalse(exists(file));
}

@Test
@DisabledOnOs(OS.WINDOWS)
void deleteFailsWithoutRetryWhenNoPermission(@TempDir Path tempDir) throws Exception {
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<PosixFilePermission> 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, null, false, true, false));
verify(log, never()).warn(any(CharSequence.class), any(Throwable.class));
assertEquals("Failed to delete " + basedir, exception.getMessage());
// MCLEAN-124 Fixed on the 3.x branch: wrapper IOException has cause DirectoryNotEmptyException, the latter
// being the accurate reason of failure.
// On the 4.x branch it behaves differently: wrapper IOException has cause which is another IOException which
// has suppressed DirectoryNotEmptyException.
// So on 3.x one needed to get the cause to get the accurate reason of failure. Simple.
// final DirectoryNotEmptyException cause =
// assertInstanceOf(DirectoryNotEmptyException.class, exception.getCause());
// On 4.x, one now needs to get the suppressed exception from the cause for that. Slightly more complicated.
final DirectoryNotEmptyException suppressed = assertInstanceOf(
DirectoryNotEmptyException.class, exception.getCause().getSuppressed()[0]);
assertEquals(basedir.toString(), suppressed.getMessage());
}

@Test
@DisabledOnOs(OS.WINDOWS)
void deleteFailsAfterRetryWhenNoPermission(@TempDir Path tempDir) throws Exception {
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<PosixFilePermission> 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, null, false, true, true));
assertEquals("Failed to delete " + basedir, exception.getMessage());
// MCLEAN-124 Similar different in 3.x versus 4.x behavior as explained above.
final DirectoryNotEmptyException suppressed = assertInstanceOf(
DirectoryNotEmptyException.class, exception.getCause().getSuppressed()[0]);
assertEquals(basedir.toString(), suppressed.getMessage());
}

@Test
@DisabledOnOs(OS.WINDOWS)
void deleteLogsWarningWithoutRetryWhenNoPermission(@TempDir Path tempDir) throws Exception {
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<PosixFilePermission> permissions = PosixFilePermissions.fromString("r-xr-xr-x");
setPosixFilePermissions(basedir, permissions);
final Cleaner cleaner = new Cleaner(null, log, false, null, null);
assertDoesNotThrow(() -> cleaner.delete(basedir, null, false, false, false));
verify(log, times(2)).warn(any(CharSequence.class), any(Throwable.class));
InOrder inOrder = inOrder(log);
// MCLEAN-124 Similar different in 3.x versus 4.x behavior as explained above.
ArgumentCaptor<IOException> captor1 = ArgumentCaptor.forClass(IOException.class);
inOrder.verify(log).warn(eq("Failed to delete " + file), captor1.capture());
final AccessDeniedException cause1 =
assertInstanceOf(AccessDeniedException.class, captor1.getValue().getSuppressed()[0]);
assertEquals(file.toString(), cause1.getMessage());
ArgumentCaptor<IOException> captor2 = ArgumentCaptor.forClass(IOException.class);
inOrder.verify(log).warn(eq("Failed to delete " + basedir), captor2.capture());
final DirectoryNotEmptyException cause2 = assertInstanceOf(
DirectoryNotEmptyException.class, captor2.getValue().getSuppressed()[0]);
assertEquals(basedir.toString(), cause2.getMessage());
}

@Test
@DisabledOnOs(OS.WINDOWS)
void deleteDoesNotLogAnythingWhenNoPermissionAndWarnDisabled(@TempDir Path tempDir) throws Exception {
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<PosixFilePermission> permissions = PosixFilePermissions.fromString("r-xr-xr-x");
setPosixFilePermissions(basedir, permissions);
final Cleaner cleaner = new Cleaner(null, log, false, null, null);
assertDoesNotThrow(() -> cleaner.delete(basedir, null, false, false, false));
verify(log, never()).warn(any(CharSequence.class), any(Throwable.class));
}
}

0 comments on commit ca474c6

Please sign in to comment.