jdk-24/test/jdk/java/nio/file/Files/SubstDrive.java
2024-02-29 16:47:14 +00:00

471 lines
16 KiB
Java

/*
* Copyright (c) 2020 Microsoft Corporation. All rights reserved.
* Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*
*/
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import static org.testng.Assert.*;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.AfterClass;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.Test;
import org.testng.SkipException;
import jdk.test.lib.process.ProcessTools;
import jdk.test.lib.process.OutputAnalyzer;
/* @test
* @summary Test Files' public APIs with drives created using the subst command on Windows.
* @requires (os.family == "windows")
* @library /test/lib ..
* @build SubstDrive
* @run testng SubstDrive
*/
public class SubstDrive {
private static Path SUBST_DRIVE;
private static Path TEST_TEMP_DIRECTORY;
/**
* Setup for the test:
* + Create a temporary directory where all subsequently created temp
* directories will be in. This directory and all of its contents will be
* deleted when the test finishes.
* + Find a drive that is available for use with subst.
*/
@BeforeClass
public void setup() throws IOException {
TEST_TEMP_DIRECTORY = Files.createTempDirectory("tmp");
System.out.printf("Test directory is at %s\n", TEST_TEMP_DIRECTORY);
Optional<Path> substDrive = findAvailableDrive(TEST_TEMP_DIRECTORY);
if (!substDrive.isPresent()) {
throw new SkipException(
"Could not find any available drive to use with subst, skipping the tests");
}
SUBST_DRIVE = substDrive.get();
System.out.printf("Using drive %s\n with subst", SUBST_DRIVE);
}
/**
* Delete the root temporary directory together with all of its contents
* when all tests finish.
*/
@AfterClass
public void removeRootTempDirectory() throws IOException {
TestUtil.removeAll(TEST_TEMP_DIRECTORY);
}
/**
* Each test method maps drive `SUBST_DRIVE` to a temporary directory,
* unmap the drive after every test so that subsequent ones can reuse
* the drive.
*/
@AfterMethod
public void deleteSubstDrive() throws IOException {
Stream<String> substitutedDrives = substFindMappedDrives();
// Only delete `SUBST_DRIVE` if it is currently being substituted
if (substitutedDrives.anyMatch(e -> e.contains(SUBST_DRIVE.toString()))) {
substDelete(SUBST_DRIVE);
}
}
/**
* Test whether files can be created in the substituted drive.
*/
@Test
public void testCreateAndDeleteFile() throws IOException {
Path tempDirectory = Files.createTempDirectory(TEST_TEMP_DIRECTORY, "tmp");
substCreate(SUBST_DRIVE, tempDirectory);
String fileContents = "Hello world!";
Path p = Path.of(SUBST_DRIVE.toString(), "testFile.txt");
Files.createFile(p);
assertTrue(Files.exists(p));
Files.writeString(p, fileContents);
assertEquals(Files.readString(p), fileContents);
}
/**
* Test if we can delete the substituted drive (essentially just a directory).
*/
@Test
public void testDeleteSubstitutedDrive() throws IOException {
Path tempDirectory = Files.createTempDirectory(TEST_TEMP_DIRECTORY, "tmp");
substCreate(SUBST_DRIVE, tempDirectory);
assertTrue(Files.exists(tempDirectory));
Files.delete(SUBST_DRIVE);
assertTrue(Files.notExists(tempDirectory));
}
/**
* Test if the attributes returned by the Files' APIs are consistent when
* using the actual path and the substituted path.
*/
@Test
public void testAttributes() throws IOException {
Path tempDirectory = Files.createTempDirectory(TEST_TEMP_DIRECTORY, "tmp");
substCreate(SUBST_DRIVE, tempDirectory);
assertTrue(Files.isSameFile(tempDirectory, SUBST_DRIVE));
assertEquals(
Files.isExecutable(tempDirectory),
Files.isExecutable(SUBST_DRIVE));
assertEquals(
Files.isReadable(tempDirectory),
Files.isReadable(SUBST_DRIVE));
assertEquals(
Files.isDirectory(tempDirectory),
Files.isDirectory(SUBST_DRIVE));
assertEquals(
Files.isHidden(tempDirectory),
Files.isHidden(SUBST_DRIVE));
assertEquals(
Files.isRegularFile(tempDirectory),
Files.isRegularFile(SUBST_DRIVE));
assertEquals(
Files.isSymbolicLink(tempDirectory),
Files.isSymbolicLink(SUBST_DRIVE));
assertEquals(
Files.getOwner(tempDirectory),
Files.getOwner(SUBST_DRIVE));
assertEquals(
Files.isWritable(tempDirectory),
Files.isWritable(SUBST_DRIVE));
}
/**
* Test if setting attributes for a substituted path works the same way
* as it would for a real path.
*/
@Test
public void testGetSetAttributes() throws IOException {
Path tempDirectory = Files.createTempDirectory(TEST_TEMP_DIRECTORY, "tmp");
substCreate(SUBST_DRIVE, tempDirectory);
Files.setAttribute(SUBST_DRIVE, "dos:hidden", true);
assertTrue(Files.isHidden(SUBST_DRIVE));
assertTrue(Files.isHidden(tempDirectory));
Files.setAttribute(tempDirectory, "dos:hidden", false);
assertFalse(Files.isHidden(SUBST_DRIVE));
assertFalse(Files.isHidden(tempDirectory));
Map<String, Object> attr1 = Files.readAttributes(SUBST_DRIVE, "*");
Map<String, Object> attr2 = Files.readAttributes(tempDirectory, "*");
assertEquals(attr1, attr2);
}
/**
* Test if the FileStores returned from using substituted path and real path
* are the same.
*/
@Test
public void testFileStore() throws IOException {
Path tempDirectory = Files.createTempDirectory(TEST_TEMP_DIRECTORY, "tmp");
substCreate(SUBST_DRIVE, tempDirectory);
FileStore fileStore1 = Files.getFileStore(tempDirectory);
FileStore fileStore2 = Files.getFileStore(SUBST_DRIVE);
assertEquals(
fileStore1.getTotalSpace(),
fileStore2.getTotalSpace());
assertEquals(
fileStore1.getBlockSize(),
fileStore2.getBlockSize());
assertEquals(
fileStore1.name(),
fileStore2.name());
assertEquals(
fileStore1.type(),
fileStore2.type());
assertEquals(
SUBST_DRIVE.getFileSystem().getRootDirectories(),
tempDirectory.getFileSystem().getRootDirectories());
}
/**
* Test if Files.copy works correctly on a substituted drive, and that
* all of the attributes are the same.
*/
@Test
public void testMoveAndCopySubstDrive() throws IOException {
Path tempDirectory = Files.createTempDirectory(TEST_TEMP_DIRECTORY, "tmp");
Path tempDirectoryCopy = Path.of(tempDirectory.toString() + "_copy");
substCreate(SUBST_DRIVE, tempDirectory);
Files.copy(SUBST_DRIVE, tempDirectoryCopy);
assertEquals(
Files.isExecutable(SUBST_DRIVE),
Files.isExecutable(tempDirectoryCopy));
assertEquals(
Files.isReadable(SUBST_DRIVE),
Files.isReadable(tempDirectoryCopy));
assertEquals(
Files.isDirectory(SUBST_DRIVE),
Files.isDirectory(tempDirectoryCopy));
assertEquals(
Files.isHidden(SUBST_DRIVE),
Files.isHidden(tempDirectoryCopy));
assertEquals(
Files.isRegularFile(SUBST_DRIVE),
Files.isRegularFile(tempDirectoryCopy));
assertEquals(
Files.isWritable(SUBST_DRIVE),
Files.isWritable(tempDirectoryCopy));
assertEquals(
Files.getOwner(SUBST_DRIVE),
Files.getOwner(tempDirectoryCopy));
}
/**
* Test if the attributes of a resolved symlink are the same as its target's
* Note: requires administrator privileges.
*/
@Test
public void testGetResolvedSymlinkAttribute() throws IOException {
if (!TestUtil.supportsSymbolicLinks(TEST_TEMP_DIRECTORY)) {
return;
}
Path tempDirectory = Files.createTempDirectory(TEST_TEMP_DIRECTORY, "tmp");
substCreate(SUBST_DRIVE, tempDirectory);
Path tempFile = Path.of(SUBST_DRIVE.toString(), "test.txt");
String contents = "Hello world!";
Files.writeString(tempFile, contents);
assertEquals(Files.readString(tempFile), contents);
Path link = Path.of(SUBST_DRIVE.toString(), "link");
Files.createSymbolicLink(link, tempFile);
assertEquals(Files.readString(link), contents);
assertEquals(Files.isExecutable(link), Files.isExecutable(tempFile));
assertEquals(Files.isReadable(link), Files.isReadable(tempFile));
assertEquals(Files.isDirectory(link), Files.isDirectory(tempFile));
assertEquals(Files.isHidden(link), Files.isHidden(tempFile));
assertEquals(Files.isRegularFile(link), Files.isRegularFile(tempFile));
assertEquals(Files.isWritable(link), Files.isWritable(tempFile));
assertEquals(Files.getOwner(link), Files.getOwner(tempFile));
}
/**
* Test if files and directories can be created, moved, and cut when the
* substituted drive is a symlink.
* Note: requires administrator privileges.
*/
@Test
public void testSubstWithSymlinkedDirectory() throws IOException {
if (!TestUtil.supportsSymbolicLinks(TEST_TEMP_DIRECTORY)) {
return;
}
Path tempDirectory = Files.createTempDirectory(TEST_TEMP_DIRECTORY, "tmp");
Path tempLink = Path.of(tempDirectory.toString() + "_link");
Files.createSymbolicLink(tempLink, tempDirectory);
substCreate(SUBST_DRIVE, tempLink);
assertEquals(
Files.readAttributes(SUBST_DRIVE, "*"),
Files.readAttributes(tempDirectory, "*"));
assertTrue(Files.isWritable(SUBST_DRIVE));
Path tempFile = Files.createTempFile(SUBST_DRIVE, "prefix", "suffix");
String contents = "Hello world!";
Files.writeString(tempFile, contents);
assertEquals(Files.readString(tempFile), contents);
Path tempDirectory2 = Files.createTempDirectory(TEST_TEMP_DIRECTORY, "tmp");
Path copy = Path.of(tempDirectory2.toString(), "copied");
Files.copy(tempFile, copy);
assertTrue(Files.exists(copy));
assertEquals(Files.readString(copy), contents);
Path cut = Path.of(tempDirectory2.toString(), "cut");
Files.move(tempFile, cut);
assertTrue(Files.notExists(tempFile));
assertTrue(Files.exists(cut));
assertEquals(Files.readString(cut), contents);
}
/**
* When the substituted drive is a symlink, test if it has the same
* attributes as its target.
* Note: requires administrator privileges.
*/
@Test
public void testMoveAndCopyFilesToSymlinkedDrive() throws IOException {
if (!TestUtil.supportsSymbolicLinks(TEST_TEMP_DIRECTORY)) {
return;
}
Path tempDirectory = Files.createTempDirectory(TEST_TEMP_DIRECTORY, "tmp");
Path tempLink = Path.of(tempDirectory.toString() + "_link");
Files.createSymbolicLink(tempLink, tempDirectory);
substCreate(SUBST_DRIVE, tempLink);
assertEquals(
Files.readAttributes(SUBST_DRIVE, "*"),
Files.readAttributes(tempDirectory, "*"));
assertTrue(Files.isWritable(SUBST_DRIVE));
}
/**
* Run a command and optionally prints stdout contents to
* `customOutputStream`.
*/
private void runCmd(ProcessBuilder pb, PrintStream customOutputStream) {
try {
PrintStream ps = customOutputStream != null ?
customOutputStream :
System.out;
OutputAnalyzer outputAnalyzer = ProcessTools.executeCommand(pb)
.outputTo(ps)
.errorTo(System.err);
int exitCode = outputAnalyzer.getExitValue();
assertEquals(
exitCode /* actual value */,
0 /* expected value */,
String.format(
"Command `%s` failed with exit code %d",
pb.command(),
exitCode
)
);
} catch (Throwable t) {
throw new RuntimeException(t);
}
}
/**
* Helper to map a path to a drive letter using subst.
* For reference, see:
* https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/subst
*/
private void substCreate(Path drive, Path path) {
runCmd(
new ProcessBuilder(
"cmd", "/c", "subst", drive.toString(), path.toString()),
null /* customOutputStream */);
}
/**
* Delete a drive mapping using subst.
* For reference, see:
* https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/subst
*/
private void substDelete(Path drive) throws IOException {
runCmd(
new ProcessBuilder(
"cmd", "/c", "subst", drive.toString(), "/D"),
null /* customOutputStream */);
}
/**
* Return a list of strings that represents all the currently mapped drives.
* For instance, with the following output of subst:
* A:\: => path1
* B:\: => path2
* T:\: => path3
* X:\: => path4
* The function returns: ["A:\", "B:\", "T:\", "X:\"]
* For reference, see:
* https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/subst
*/
private Stream<String> substFindMappedDrives() throws UnsupportedEncodingException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
String utf8 = StandardCharsets.UTF_8.name();
try (PrintStream ps = new PrintStream(baos, true, utf8)) {
// subst without any arguments returns a list of drives that
// are being substituted
runCmd(new ProcessBuilder("cmd", "/c", "subst"), ps);
String stdout = baos.toString(utf8);
return stdout
// split lines
.lines()
// only examine lines with "=>"
.filter(line -> line.contains("=>"))
// split each line into 2 components and take the first one
.map(line -> line.split("=>")[0].trim());
}
}
/**
* subst can fail if the drive to be mapped already exists. The method returns
* a drive that is available.
*/
private Optional<Path> findAvailableDrive(Path tempDirectory) {
for (char letter = 'Z'; letter >= 'A'; letter--) {
try {
Path p = Path.of(letter + ":");
substCreate(p, tempDirectory);
substDelete(p);
return Optional.of(p);
} catch (Throwable t) {
// fall through
}
}
return Optional.empty();
}
}