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/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); 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/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, 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); + } +}