From 2fea8f3eb1dc73d123e28e80fa191ade0d1004f5 Mon Sep 17 00:00:00 2001 From: Ashley Scopes <73482956+ascopes@users.noreply.github.com> Date: Sun, 29 Jan 2023 15:36:17 +0000 Subject: [PATCH 1/3] Fix some bugs in PathFileObject, add test pack - Mark methods in PathFileObject as raising NoSuchFileException rather than FileNotFoundException which was previously incorrect information. - Fix bug where the 'name' of a PathFileObject could previously have been an absolute path if the PathFileObject was initialised with an absolute file path. Now it will match the string representation of the relative path on the object. - Rename getRoot to the clearer 'getRootPath' which is consistent with other method naming in this class. - Update IllegalArgumentException that is thrown if a root path is not absolute so that it conveys the value of the erroneous parameter in the error message. - Implement tests for PathFileObject. --- .../jct/filemanagers/PathFileObject.java | 27 +- .../ascopes/jct/tests/helpers/Fixtures.java | 26 +- .../unit/filemanagers/PathFileObjectTest.java | 763 ++++++++++++++++++ 3 files changed, 801 insertions(+), 15 deletions(-) create mode 100644 java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/PathFileObjectTest.java diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/PathFileObject.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/PathFileObject.java index 5c8f4865d..6b77a18a0 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/PathFileObject.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/PathFileObject.java @@ -23,7 +23,6 @@ import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.BufferedWriter; -import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -37,6 +36,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.FileSystem; import java.nio.file.Files; +import java.nio.file.NoSuchFileException; import java.nio.file.Path; import javax.annotation.Nullable; import javax.annotation.WillNotClose; @@ -90,7 +90,7 @@ public PathFileObject(Location location, Path rootPath, Path relativePath) { requireNonNull(relativePath, "relativePath"); if (!rootPath.isAbsolute()) { - throw new IllegalArgumentException("rootPath must be absolute"); + throw new IllegalArgumentException("Expected rootPath to be absolute, but got " + rootPath); } this.location = location; @@ -101,7 +101,7 @@ public PathFileObject(Location location, Path rootPath, Path relativePath) { : relativePath; fullPath = rootPath.resolve(relativePath); - name = relativePath.toString(); + name = this.relativePath.toString(); uri = fullPath.toUri(); kind = FileUtils.pathToKind(relativePath); } @@ -138,8 +138,8 @@ public boolean equals(@Nullable Object other) { * Get the class access level, where appropriate. * *

In this implementation, this class will always return {@code null}, since this - * information is not readily available without preloading the file in question and - * parsing it first. + * information is not readily available without preloading the file in question and parsing it + * first. * *

At the time of writing, the OpenJDK implementations of the JavaFileObject class * do not provide an implementation for this method either. @@ -227,8 +227,8 @@ public String getName() { * Determine the class nesting kind, where appropriate. * *

In this implementation, this class will always return {@code null}, since this - * information is not readily available without preloading the file in question and - * parsing it first. + * information is not readily available without preloading the file in question and parsing it + * first. * *

At the time of writing, the OpenJDK implementations of the JavaFileObject class * do not provide an implementation for this method either. @@ -255,7 +255,7 @@ public Path getRelativePath() { * * @return the root path. */ - public Path getRoot() { + public Path getRootPath() { return rootPath; } @@ -295,8 +295,8 @@ public boolean isNameCompatible(String simpleName, Kind kind) { *

The returned implementation will always be buffered. * * @return a buffered input stream. - * @throws FileNotFoundException if the file does not exist. - * @throws IOException if an IO error occurs. + * @throws NoSuchFileException if the file does not exist. + * @throws IOException if an IO error occurs. */ @Override @WillNotClose @@ -315,7 +315,6 @@ public BufferedInputStream openInputStream() throws IOException { * *

The returned implementation will always be buffered. * - * * @return a buffered output stream. * @throws IOException if an IO error occurs. */ @@ -336,8 +335,8 @@ public BufferedOutputStream openOutputStream() throws IOException { * @param ignoreEncodingErrors {@code true} to suppress encoding errors, or {@code false} to throw * them to the caller. * @return a buffered reader. - * @throws FileNotFoundException if the file does not exist. - * @throws IOException if an IO error occurs. + * @throws NoSuchFileException if the file does not exist. + * @throws IOException if an IO error occurs. */ @Override @WillNotClose @@ -351,7 +350,7 @@ public BufferedReader openReader(boolean ignoreEncodingErrors) throws IOExceptio /** * Open a writer to this file using the default charset (UTF-8). * - *

This will Ccreate the file first if it does not already exist. If it does exist, + *

This will create the file first if it does not already exist. If it does exist, * this will first overwrite the file and truncate it. * *

This input stream must be closed once finished with, otherwise diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/helpers/Fixtures.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/helpers/Fixtures.java index 6b7bc03ee..f4f5840f4 100644 --- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/helpers/Fixtures.java +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/helpers/Fixtures.java @@ -418,6 +418,31 @@ public static Path somePath() { return root.resolve("some-dummy-path"); } + /** + * Get some dummy absolute path. + * + * @return some dummy absolute path. + */ + public static Path someAbsolutePath() { + return somePath().resolve("some-absolute-path").toAbsolutePath(); + } + + /** + * Get some relative path. + * + * @return some dummy relative path. + */ + public static Path someRelativePath() { + var absolutePath = someAbsolutePath(); + var relativePath = absolutePath; + + for (var i = 0; i < someInt(1, 4); ++i) { + relativePath = absolutePath.resolve(someText()); + } + + return absolutePath.relativize(relativePath.resolve("some-relative-path")); + } + /** * Get some module reference. * @@ -521,7 +546,6 @@ private TempFileSystem() { .setAttributeViews("basic", "posix") .setRoots("/") .setWorkingDirectory("/") - .setPathEqualityUsesCanonicalForm(true) .build(); fs = Jimfs.newFileSystem(name, config); diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/PathFileObjectTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/PathFileObjectTest.java new file mode 100644 index 000000000..9db0e4f06 --- /dev/null +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/PathFileObjectTest.java @@ -0,0 +1,763 @@ +/* + * Copyright (C) 2022 - 2023, the original author or authors. + * + * Licensed 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 io.github.ascopes.jct.tests.unit.filemanagers; + +import static io.github.ascopes.jct.tests.helpers.Fixtures.someAbsolutePath; +import static io.github.ascopes.jct.tests.helpers.Fixtures.someBinaryData; +import static io.github.ascopes.jct.tests.helpers.Fixtures.someLinesOfText; +import static io.github.ascopes.jct.tests.helpers.Fixtures.someLocation; +import static io.github.ascopes.jct.tests.helpers.Fixtures.someRelativePath; +import static io.github.ascopes.jct.tests.helpers.Fixtures.someTemporaryFileSystem; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; + +import io.github.ascopes.jct.filemanagers.PathFileObject; +import java.io.IOException; +import java.io.StringWriter; +import java.nio.charset.MalformedInputException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.attribute.FileTime; +import javax.tools.FileObject; +import javax.tools.JavaFileObject; +import javax.tools.JavaFileObject.Kind; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +/** + * {@link PathFileObject} tests. + * + * @author Ashley Scopes + */ +@DisplayName("PathFileObject tests") +class PathFileObjectTest { + + @DisplayName("Passing a null location to the constructor raises an exception") + @SuppressWarnings("DataFlowIssue") + @Test + void passingNullLocationToConstructorRaisesException() { + // Then + assertThatThrownBy(() -> new PathFileObject(null, someAbsolutePath(), someRelativePath())) + .isInstanceOf(NullPointerException.class) + .hasMessage("location"); + } + + @DisplayName("Passing a null root path to the constructor raises an exception") + @SuppressWarnings("DataFlowIssue") + @Test + void passingNullRootPathToConstructorRaisesException() { + // Then + assertThatThrownBy(() -> new PathFileObject(someLocation(), null, someRelativePath())) + .isInstanceOf(NullPointerException.class) + .hasMessage("rootPath"); + } + + @DisplayName("Passing a null relative path to the constructor raises an exception") + @SuppressWarnings("DataFlowIssue") + @Test + void passingNullRelativePathToConstructorRaisesException() { + // Then + assertThatThrownBy(() -> new PathFileObject(someLocation(), someAbsolutePath(), null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("relativePath"); + } + + @DisplayName("Passing a relative path to the constructor for the root path raises an exception") + @Test + void passingRelativePathToConstructorForRootPathRaisesException() { + // Given + var rootPath = someRelativePath(); + + // Then + assertThatThrownBy(() -> new PathFileObject(someLocation(), rootPath, someRelativePath())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Expected rootPath to be absolute, but got " + rootPath); + } + + @DisplayName(".delete() will delete an existing file") + @Test + void deleteWillDeleteAnExistingFile() throws IOException { + // Given + try (var fs = someTemporaryFileSystem()) { + var rootDir = fs.getRootPath().resolve("root-a").resolve("root-sub-a"); + var dir = rootDir.resolve("foo").resolve("bar"); + var file = dir.resolve("Baz.txt"); + var relativeFile = rootDir.relativize(file); + + Files.createDirectories(dir); + Files.createFile(file); + var fileObject = new PathFileObject(someLocation(), rootDir, relativeFile); + + // When + var result = fileObject.delete(); + + // Then + assertThat(file).doesNotExist(); + assertThat(result).isTrue(); + } + } + + @DisplayName(".delete() will not delete a missing file") + @Test + void deleteWillNotDeleteMissingFile() throws IOException { + // Given + try (var fs = someTemporaryFileSystem()) { + var rootDir = fs.getRootPath().resolve("root-a").resolve("root-sub-a"); + var dir = rootDir.resolve("foo").resolve("bar"); + var file = dir.resolve("Baz.txt"); + var relativeFile = rootDir.relativize(file); + + Files.createDirectories(dir); + + var fileObject = new PathFileObject(someLocation(), rootDir, relativeFile); + + // When + var result = fileObject.delete(); + + // Then + assertThat(file).doesNotExist(); + assertThat(result).isFalse(); + } + } + + @DisplayName(".delete() will ignore internal errors") + @Test + void deleteWillIgnoreInternalErrors() throws IOException { + // Given + try (var fs = someTemporaryFileSystem()) { + var rootDir = fs.getRootPath().resolve("root-a").resolve("root-sub-a"); + var dir = rootDir.resolve("foo").resolve("bar"); + var file = dir.resolve("Baz.txt"); + var relativeDir = rootDir.relativize(dir); + + Files.createDirectories(dir); + Files.createFile(file); + + // Purposely using a directory rather than a file here to trigger a deletion error. + var fileObject = new PathFileObject(someLocation(), rootDir, relativeDir); + + // When + var result = fileObject.delete(); + + // Then + assertThat(dir).exists().isDirectory(); + assertThat(result).isFalse(); + } + } + + @DisplayName(".equals(null) returns false") + @Test + void equalsNullReturnsFalse() { + // Given + var fileObject = new PathFileObject(someLocation(), someAbsolutePath(), someRelativePath()); + + // Then + assertThat(fileObject) + .isNotEqualTo(null); + } + + @DisplayName(".equals(Object) returns false if the object is not a path file object") + @Test + void equalsReturnsFalseIfNotPathFileObject() { + // Given + var fileObject = new PathFileObject(someLocation(), someAbsolutePath(), someRelativePath()); + + // Then + assertThat(fileObject) + .isNotEqualTo(mock(FileObject.class)) + .isNotEqualTo(mock(JavaFileObject.class)) + .isNotEqualTo("foobar") + .isNotEqualTo(1234); + } + + @DisplayName(".equals(PathFileObject) returns true if the file object has the same URI") + @Test + void equalsReturnsTrueIfTheFileObjectHasTheSameUri() { + // Given + var rootPath = someAbsolutePath(); + var relativePath = someRelativePath(); + + var fileObject1 = new PathFileObject(someLocation(), rootPath, relativePath); + var fileObject2 = new PathFileObject(someLocation(), rootPath, relativePath); + + // Then + assertThat(fileObject1).isEqualTo(fileObject2); + } + + @DisplayName(".equals(PathFileObject) returns true if the file object is the same instance") + @Test + void equalsReturnsTrueIfTheFileObjectIsTheSameInstance() { + // Given + var location = someLocation(); + var rootPath = someAbsolutePath(); + var relativePath = someRelativePath(); + + var fileObject = new PathFileObject(location, rootPath, relativePath); + + // Then + assertThat(fileObject).isEqualTo(fileObject); + } + + @DisplayName(".getAccessLevel() returns null") + @SuppressWarnings("DataFlowIssue") + @Test + void getAccessLevelReturnsNull() { + // Given + var fileObject = new PathFileObject(someLocation(), someAbsolutePath(), someRelativePath()); + + // Then + assertThat(fileObject.getAccessLevel()).isNull(); + } + + @DisplayName(".getCharContent(...) returns the character content") + @ValueSource(booleans = {true, false}) + @ParameterizedTest(name = "for ignoreEncodingErrors={0}") + void getCharContentReturnsCharacterContent(boolean ignoreEncodingErrors) throws IOException { + // Given + try (var fs = someTemporaryFileSystem()) { + var rootDir = fs.getRootPath().resolve("root-a").resolve("root-sub-a"); + var dir = rootDir.resolve("foo").resolve("bar"); + var file = dir.resolve("Baz.txt"); + var relativeFile = rootDir.relativize(file); + + var text = someLinesOfText(); + + Files.createDirectories(dir); + Files.writeString(file, text, StandardCharsets.UTF_8); + + var fileObject = new PathFileObject(someLocation(), rootDir, relativeFile); + + // Then + assertThat(fileObject.getCharContent(ignoreEncodingErrors)) + .isEqualTo(text); + } + } + + @DisplayName(".getCharContent(...) ignores encoding errors when instructed") + @Test + void getCharContentIgnoresEncodingErrors() throws IOException { + // Given + try (var fs = someTemporaryFileSystem()) { + var rootDir = fs.getRootPath().resolve("root-a").resolve("root-sub-a"); + var dir = rootDir.resolve("foo").resolve("bar"); + var file = dir.resolve("Baz.txt"); + var relativeFile = rootDir.relativize(file); + + var badlyEncodedBytes = new byte[]{(byte) 0xC0, (byte) 0xC1, (byte) 0xF5, (byte) 0xFF}; + + Files.createDirectories(dir); + Files.write(file, badlyEncodedBytes); + + var fileObject = new PathFileObject(someLocation(), rootDir, relativeFile); + + // Then + assertThatCode(() -> fileObject.getCharContent(true)) + .doesNotThrowAnyException(); + } + } + + @DisplayName(".getCharContent(...) propagates encoding errors when instructed") + @Test + void getCharContentPropagatesEncodingErrors() throws IOException { + // Given + try (var fs = someTemporaryFileSystem()) { + var rootDir = fs.getRootPath().resolve("root-a").resolve("root-sub-a"); + var dir = rootDir.resolve("foo").resolve("bar"); + var file = dir.resolve("Baz.txt"); + var relativeFile = rootDir.relativize(file); + + var badlyEncodedBytes = new byte[]{(byte) 0xC0, (byte) 0xC1, (byte) 0xF5, (byte) 0xFF}; + + Files.createDirectories(dir); + Files.write(file, badlyEncodedBytes); + + var fileObject = new PathFileObject(someLocation(), rootDir, relativeFile); + + // Then + assertThatThrownBy(() -> fileObject.getCharContent(false)) + .isInstanceOf(MalformedInputException.class); + } + } + + @DisplayName(".getFullPath() returns the full path") + @Test + void getFullPathReturnsTheFullPath() { + // Given + var rootPath = someAbsolutePath(); + var relativePath = someRelativePath(); + var fileObject = new PathFileObject(someLocation(), rootPath, relativePath); + + // Then + assertThat(fileObject.getFullPath()) + .isEqualTo(rootPath.resolve(relativePath)); + } + + @DisplayName(".getLastModified() returns the last modified timestamp") + @Test + void getLastModifiedReturnsTheLastModifiedTimestamp() throws Exception { + // Given + try (var fs = someTemporaryFileSystem()) { + var rootDir = fs.getRootPath().resolve("root-a").resolve("root-sub-a"); + var dir = rootDir.resolve("foo").resolve("bar"); + var file = dir.resolve("Baz.txt"); + var relativeFile = rootDir.relativize(file); + + // Create the file first to separate the creation and modification timestamps. + Files.createDirectories(dir); + Files.createFile(file); + // Write something to it first. + Files.writeString(file, "foobar"); + // Wait a moment or two + Thread.sleep(500); + // Write again to change the timestamp for modification + Files.writeString(file, "bazbork"); + + var fileObject = new PathFileObject(someLocation(), rootDir, relativeFile); + + // Then + var creationTime = ((FileTime) Files.getAttribute(file, "creationTime")).toMillis(); + var lastModified = fileObject.getLastModified(); + + assertThat(lastModified) + .isNotEqualTo(creationTime) + .isEqualTo(Files.getLastModifiedTime(file).toMillis()); + } + } + + @DisplayName(".getLastModified() returns 0 if the file does not exist") + @Test + void getLastModifiedReturnsZeroIfTheFileDoesNotExist() throws Exception { + // Given + try (var fs = someTemporaryFileSystem()) { + var rootDir = fs.getRootPath().resolve("root-a").resolve("root-sub-a"); + var dir = rootDir.resolve("foo").resolve("bar"); + var file = dir.resolve("Baz.txt"); + var relativeFile = rootDir.relativize(file); + Files.createDirectories(dir); + // Purposely do not create the file. + + var fileObject = new PathFileObject(someLocation(), rootDir, relativeFile); + + // Then + var lastModified = fileObject.getLastModified(); + + assertThat(lastModified).isZero(); + } + } + + @DisplayName(".getLocation() returns the location") + @Test + void getLocationReturnsTheLocation() { + // Given + var location = someLocation(); + var fileObject = new PathFileObject(location, someAbsolutePath(), someRelativePath()); + + // Then + assertThat(fileObject.getLocation()) + .isSameAs(location); + } + + @DisplayName( + ".getName() returns the file name when the relative path is initialised from a relative path" + ) + @Test + void getNameReturnsTheFileNameWhenRelativePathIsRelative() { + // Given + var relativePath = someRelativePath(); + var fileObject = new PathFileObject(someLocation(), someAbsolutePath(), relativePath); + + // Then + assertThat(fileObject.getName()) + .isEqualTo(relativePath.toString()); + } + + @DisplayName( + ".getName() returns the file name when the relative path is initialised from an absolute path" + ) + @Test + void getNameReturnsTheFileNameWhenRelativePathIsInitialisedFromAnAbsolutePath() { + // Given + var absolutePath = someAbsolutePath(); + var relativePath = someRelativePath(); + var fileObject = new PathFileObject( + someLocation(), + absolutePath, + absolutePath.resolve(relativePath) + ); + + // Then + assertThat(fileObject.getName()) + .isEqualTo(relativePath.toString()); + } + + @DisplayName(".getNestingKind() returns null") + @SuppressWarnings("DataFlowIssue") + @Test + void getNestingKindReturnsNull() { + // Given + var fileObject = new PathFileObject(someLocation(), someAbsolutePath(), someRelativePath()); + + // Then + assertThat(fileObject.getNestingKind()).isNull(); + } + + @DisplayName(".getRelativePath() returns the relative path if initialised from a relative path") + @Test + void getRelativePathReturnsRelativePathIfInitialisedFromRelativePath() { + // Given + var rootPath = someAbsolutePath(); + var relativePath = someRelativePath(); + var fileObject = new PathFileObject(someLocation(), rootPath, rootPath.resolve(relativePath)); + + // Then + assertThat(fileObject.getRelativePath()).isEqualTo(relativePath); + } + + @DisplayName(".getRelativePath() returns the relative path if initialised from an absolute path") + @Test + void getRelativePathReturnsRelativePathIfInitialisedFromAbsolutePath() { + // Given + var rootPath = someAbsolutePath(); + var relativePath = someRelativePath(); + var fileObject = new PathFileObject(someLocation(), rootPath, relativePath); + + // Then + assertThat(fileObject.getRelativePath()).isEqualTo(relativePath); + } + + @DisplayName(".getRootPath() returns the root path") + @Test + void getRootPathReturnsTheRootPath() { + // Given + var rootPath = someAbsolutePath(); + var fileObject = new PathFileObject(someLocation(), rootPath, someRelativePath()); + + // Then + assertThat(fileObject.getRootPath()).isEqualTo(rootPath); + } + + @DisplayName(".hashCode() returns the URI hash code") + @Test + void hashCodeUsesUriHashCode() { + // Given + var rootPath = someAbsolutePath(); + var relativePath = someRelativePath(); + var uri = rootPath.resolve(relativePath).toUri(); + var fileObject = new PathFileObject(someLocation(), rootPath, relativePath); + + // Then + assertThat(fileObject).hasSameHashCodeAs(uri); + } + + @DisplayName(".isNameCompatible(...) returns the expected value") + @CsvSource({ + // Valid cases + " Foo.java, Foo, SOURCE, true", + " Bar.class, Bar, CLASS, true", + " Baz.html, Baz, HTML, true", + " Bork, Bork, OTHER, true", + // Simple name case-insensitive matches that should fail. + " Foo.java, foo, SOURCE, false", + " Bar.class, bar, CLASS, false", + " Baz.html, baz, HTML, false", + " Bork, bork, OTHER, false", + // Different simple names + " Foo.java, Bar, SOURCE, false", + " Bar.class, Baz, CLASS, false", + " Baz.html, Bork, HTML, false", + " Bork, Foo, OTHER, false", + // Different kinds + " Foo.java, Foo, CLASS, false", + " Bar.class, Bar, SOURCE, false", + " Baz.html, Baz, OTHER, false", + " Bork, Bork, HTML, false", + }) + @ParameterizedTest(name = "expect {3} when fileName={0}, simpleName={1}, kind={2}") + void isNameCompatibleReturnsTheExpectedValue( + String fileName, + String simpleName, + Kind kind, + boolean expectCompatible + ) { + // Given + var rootPath = someAbsolutePath(); + var relativePath = someRelativePath().resolve(fileName); + var fileObject = new PathFileObject(someLocation(), rootPath, relativePath); + + assertThat(fileObject.isNameCompatible(simpleName, kind)) + .isEqualTo(expectCompatible); + } + + @DisplayName(".openInputStream() reads the correct file contents") + @Test + void openInputStreamReadsTheCorrectFileContents() throws IOException { + // Given + try (var fs = someTemporaryFileSystem()) { + var rootDir = fs.getRootPath().resolve("root-a").resolve("root-sub-a"); + var dir = rootDir.resolve("foo").resolve("bar"); + var file = dir.resolve("Baz.txt"); + var relativeFile = rootDir.relativize(file); + var fileObject = new PathFileObject(someLocation(), rootDir, relativeFile); + var data = someBinaryData(); + + Files.createDirectories(dir); + Files.write(file, data); + + // Then + assertThat(fileObject.openInputStream()) + .hasBinaryContent(data); + } + } + + @DisplayName(".openInputStream() throws NoSuchFileException if the file does not exist") + @Test + void openInputStreamThrowsNoSuchFileExceptionIfFileDoesNotExist() throws IOException { + // Given + try (var fs = someTemporaryFileSystem()) { + var rootDir = fs.getRootPath().resolve("root-a").resolve("root-sub-a"); + var dir = rootDir.resolve("foo").resolve("bar"); + var file = dir.resolve("Baz.txt"); + var relativeFile = rootDir.relativize(file); + var fileObject = new PathFileObject(someLocation(), rootDir, relativeFile); + Files.createDirectories(dir); + + // Then + assertThatThrownBy(fileObject::openInputStream) + .isInstanceOf(NoSuchFileException.class); + } + } + + @DisplayName( + ".openOutputStream() enables writing to the expected file when it does yet not exist" + ) + @Test + void openOutputStreamEnablesWritingToTheExpectedFileWhenItDoesNotYetExist() throws IOException { + // Given + try (var fs = someTemporaryFileSystem()) { + var rootDir = fs.getRootPath().resolve("root-a").resolve("root-sub-a"); + var dir = rootDir.resolve("foo").resolve("bar"); + var file = dir.resolve("Baz.txt"); + var relativeFile = rootDir.relativize(file); + var fileObject = new PathFileObject(someLocation(), rootDir, relativeFile); + var data = someBinaryData(); + + Files.createDirectories(dir); + + // When + try (var os = fileObject.openOutputStream()) { + os.write(data); + } + + // Then + assertThat(file).hasBinaryContent(data); + } + } + + @DisplayName(".openOutputStream() overwrites any existing file") + @Test + void openOutputStreamOverwritesAnyExistingFile() throws IOException { + // Given + try (var fs = someTemporaryFileSystem()) { + var rootDir = fs.getRootPath().resolve("root-a").resolve("root-sub-a"); + var dir = rootDir.resolve("foo").resolve("bar"); + var file = dir.resolve("Baz.txt"); + var relativeFile = rootDir.relativize(file); + var fileObject = new PathFileObject(someLocation(), rootDir, relativeFile); + var data = someBinaryData(); + + Files.createDirectories(dir); + Files.write(file, new byte[]{1, 2, 3, 4, 5, 6}); + + // When + try (var os = fileObject.openOutputStream()) { + os.write(data); + } + + // Then + assertThat(file).hasBinaryContent(data); + } + } + + @DisplayName(".openReader(...) returns the character content") + @ValueSource(booleans = {true, false}) + @ParameterizedTest(name = "for ignoreEncodingErrors={0}") + void openReaderReturnsCharacterContent(boolean ignoreEncodingErrors) throws IOException { + // Given + try (var fs = someTemporaryFileSystem()) { + var rootDir = fs.getRootPath().resolve("root-a").resolve("root-sub-a"); + var dir = rootDir.resolve("foo").resolve("bar"); + var file = dir.resolve("Baz.txt"); + var relativeFile = rootDir.relativize(file); + + var text = someLinesOfText(); + + Files.createDirectories(dir); + Files.writeString(file, text, StandardCharsets.UTF_8); + + var fileObject = new PathFileObject(someLocation(), rootDir, relativeFile); + + // Then + try (var reader = fileObject.openReader(ignoreEncodingErrors)) { + var stringWriter = new StringWriter(); + reader.transferTo(stringWriter); + assertThat(stringWriter.toString()).isEqualTo(text); + } + } + } + + @DisplayName(".openReader(...) ignores encoding errors when instructed") + @SuppressWarnings("StatementWithEmptyBody") + @Test + void openReaderIgnoresEncodingErrors() throws IOException { + // Given + try (var fs = someTemporaryFileSystem()) { + var rootDir = fs.getRootPath().resolve("root-a").resolve("root-sub-a"); + var dir = rootDir.resolve("foo").resolve("bar"); + var file = dir.resolve("Baz.txt"); + var relativeFile = rootDir.relativize(file); + + var badlyEncodedBytes = new byte[]{(byte) 0xC0, (byte) 0xC1, (byte) 0xF5, (byte) 0xFF}; + + Files.createDirectories(dir); + Files.write(file, badlyEncodedBytes); + + var fileObject = new PathFileObject(someLocation(), rootDir, relativeFile); + + // Then + assertThatCode(() -> { + // Then + try (var reader = fileObject.openReader(true)) { + while (reader.readLine() != null) { + // discard the line. + } + } + }).doesNotThrowAnyException(); + } + } + + @DisplayName(".openReader(...) propagates encoding errors when instructed") + @SuppressWarnings("StatementWithEmptyBody") + @Test + void openReaderPropagatesEncodingErrors() throws IOException { + // Given + try (var fs = someTemporaryFileSystem()) { + var rootDir = fs.getRootPath().resolve("root-a").resolve("root-sub-a"); + var dir = rootDir.resolve("foo").resolve("bar"); + var file = dir.resolve("Baz.txt"); + var relativeFile = rootDir.relativize(file); + + var badlyEncodedBytes = new byte[]{(byte) 0xC0, (byte) 0xC1, (byte) 0xF5, (byte) 0xFF}; + + Files.createDirectories(dir); + Files.write(file, badlyEncodedBytes); + + var fileObject = new PathFileObject(someLocation(), rootDir, relativeFile); + + // Then + assertThatThrownBy(() -> { + try (var reader = fileObject.openReader(false)) { + while (reader.readLine() != null) { + // discard the line. + } + } + }).isInstanceOf(MalformedInputException.class); + } + } + + @DisplayName( + ".openWriter() enables writing to the expected file when it does yet not exist" + ) + @Test + void openWriterEnablesWritingToTheExpectedFileWhenItDoesNotYetExist() throws IOException { + // Given + try (var fs = someTemporaryFileSystem()) { + var rootDir = fs.getRootPath().resolve("root-a").resolve("root-sub-a"); + var dir = rootDir.resolve("foo").resolve("bar"); + var file = dir.resolve("Baz.txt"); + var relativeFile = rootDir.relativize(file); + var fileObject = new PathFileObject(someLocation(), rootDir, relativeFile); + var text = someLinesOfText(); + + Files.createDirectories(dir); + + // When + try (var writer = fileObject.openWriter()) { + writer.write(text); + } + + // Then + assertThat(file).hasContent(text); + } + } + + @DisplayName(".openWriter() overwrites any existing file") + @Test + void openWriterOverwritesAnyExistingFile() throws IOException { + // Given + try (var fs = someTemporaryFileSystem()) { + var rootDir = fs.getRootPath().resolve("root-a").resolve("root-sub-a"); + var dir = rootDir.resolve("foo").resolve("bar"); + var file = dir.resolve("Baz.txt"); + var relativeFile = rootDir.relativize(file); + var fileObject = new PathFileObject(someLocation(), rootDir, relativeFile); + var text = someLinesOfText(); + + Files.createDirectories(dir); + Files.write(file, new byte[]{1, 2, 3, 4, 5, 6}); + + // When + try (var writer = fileObject.openWriter()) { + writer.write(text); + } + + // Then + assertThat(file).hasContent(text); + } + } + + @DisplayName(".toUri() returns the URI") + @Test + void toUriReturnsTheUri() { + // Given + var rootPath = someAbsolutePath(); + var relativePath = someRelativePath(); + var uri = rootPath.resolve(relativePath).toUri(); + var fileObject = new PathFileObject(someLocation(), rootPath, relativePath); + + // Then + assertThat(fileObject.toUri()) + .isEqualTo(uri); + } + + @DisplayName(".toString() returns the expected value") + @Test + void toStringReturnsTheExpectedValue() { + var rootPath = someAbsolutePath(); + var relativePath = someRelativePath(); + var uri = rootPath.resolve(relativePath).toUri(); + var fileObject = new PathFileObject(someLocation(), rootPath, relativePath); + + // Then + assertThat(fileObject.toString()) + .isEqualTo("PathFileObject{uri=\"%s\"}", uri); + } +} From b31cd1ff429f3737e5683a5a0f2eff42e65e7f09 Mon Sep 17 00:00:00 2001 From: Ashley Scopes <73482956+ascopes@users.noreply.github.com> Date: Sun, 29 Jan 2023 15:39:05 +0000 Subject: [PATCH 2/3] Replace FileNotFoundException in FileBuilderImpl with NoSuchFileException This keeps the exception handling consistent with the handling used elsewhere, since this API relies on the NIO API rather than the IO API for file system access. --- .../github/ascopes/jct/workspaces/impl/FileBuilderImpl.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/workspaces/impl/FileBuilderImpl.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/workspaces/impl/FileBuilderImpl.java index 98eebad9a..78bb7af1d 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/workspaces/impl/FileBuilderImpl.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/workspaces/impl/FileBuilderImpl.java @@ -22,13 +22,13 @@ import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.File; -import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.NoSuchFileException; import java.nio.file.OpenOption; import java.nio.file.Path; import java.nio.file.StandardOpenOption; @@ -101,7 +101,7 @@ public ManagedDirectory copiedFromClassPath(ClassLoader classLoader, String reso return uncheckedIo(() -> { try (var input = classLoader.getResourceAsStream(resource)) { if (input == null) { - throw new FileNotFoundException("classpath:" + resource); + throw new NoSuchFileException("classpath:" + resource); } return createFile(input); From 2c97913149eb9bf459d0f72174d005fed6098161 Mon Sep 17 00:00:00 2001 From: Ashley Scopes <73482956+ascopes@users.noreply.github.com> Date: Sun, 29 Jan 2023 15:39:30 +0000 Subject: [PATCH 3/3] Remove a reference to FileNotFoundException in a test, use NoSuchFileException instead. Enables consistency with the rest of the API. --- .../jct/tests/unit/compilers/AbstractJctCompilerTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/compilers/AbstractJctCompilerTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/compilers/AbstractJctCompilerTest.java index bf14663e9..d8cca187d 100644 --- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/compilers/AbstractJctCompilerTest.java +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/compilers/AbstractJctCompilerTest.java @@ -50,12 +50,12 @@ import io.github.ascopes.jct.filemanagers.JctFileManagerFactory; import io.github.ascopes.jct.filemanagers.LoggingMode; import io.github.ascopes.jct.workspaces.Workspace; -import java.io.FileNotFoundException; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.FileSystemException; +import java.nio.file.NoSuchFileException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -511,7 +511,7 @@ void configureInvokesConfigurerOnTheCompiler() throws Throwable { RuntimeException.class, IOException.class, FileSystemException.class, - FileNotFoundException.class, + NoSuchFileException.class, UnsupportedEncodingException.class, IndexOutOfBoundsException.class, SecurityException.class,