Skip to content

Commit

Permalink
ForwardingFilesystem (square#851)
Browse files Browse the repository at this point in the history
* ForwardingFilesystem

* Update okio/src/commonMain/kotlin/okio/ForwardingFilesystem.kt

Co-authored-by: Egor Andreevich <egor@squareup.com>

* Update okio/src/commonMain/kotlin/okio/ForwardingFilesystem.kt

Co-authored-by: Egor Andreevich <egor@squareup.com>

Co-authored-by: Egor Andreevich <egor@squareup.com>
  • Loading branch information
swankjesse and Egorand authored Dec 23, 2020
1 parent 0a4337e commit 94b73fb
Show file tree
Hide file tree
Showing 7 changed files with 334 additions and 1 deletion.
2 changes: 2 additions & 0 deletions okio-testing/src/commonMain/kotlin/okio/FakeFilesystem.kt
Original file line number Diff line number Diff line change
Expand Up @@ -290,4 +290,6 @@ class FakeFilesystem(
openPathsMutable -= path
}
}

override fun toString() = "FakeFilesystem"
}
203 changes: 203 additions & 0 deletions okio/src/commonMain/kotlin/okio/ForwardingFilesystem.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
/*
* Copyright (C) 2020 Square, Inc.
*
* 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 okio

import kotlin.jvm.JvmName

/**
* A [Filesystem] that forwards calls to another, intended for subclassing.
*
* ### Fault Injection
*
* You can use this to deterministically trigger filesystem failures in tests. This is useful to
* confirm that your program behaves correctly even if its filesystem operations fail. For example,
* this subclass fails every access of files named `unlucky.txt`:
*
* ```
* val faultyFilesystem = object : ForwardingFilesystem(FileSystem.SYSTEM) {
* override fun onPathParameter(path: Path, functionName: String, parameterName: String): Path {
* if (path.name == "unlucky.txt") throw IOException("synthetic failure!")
* return path
* }
* }
* ```
*
* You can fail specific operations by overriding them directly:
*
* ```
* val faultyFilesystem = object : ForwardingFilesystem(FileSystem.SYSTEM) {
* override fun delete(path: Path) {
* throw IOException("synthetic failure!")
* }
* }
* ```
*
* ### Observability
*
* You can extend this to verify which files your program accesses. This is a testing filesystem
* that records accesses as they happen:
*
* ```
* class LoggingFilesystem : ForwardingFilesystem(Filesystem.SYSTEM) {
* val log = mutableListOf<String>()
*
* override fun onPathParameter(path: Path, functionName: String, parameterName: String): Path {
* log += "$functionName($parameterName=$path)"
* return path
* }
* }
* ```
*
* This makes it easy for tests to assert exactly which files were accessed.
*
* ```
* @Test
* fun testMergeJsonReports() {
* createSampleJsonReports()
* loggingFilesystem.log.clear()
*
* mergeJsonReports()
*
* assertThat(loggingFilesystem.log).containsExactly(
* "list(dir=json_reports)",
* "source(file=json_reports/2020-10.json)",
* "source(file=json_reports/2020-12.json)",
* "source(file=json_reports/2020-11.json)",
* "sink(file=json_reports/2020-all.json)"
* )
* }
* ```
*
* ### Transformations
*
* Subclasses can transform file names and content.
*
* For example, your program may be written to operate on a well-known directory like `/etc/` or
* `/System`. You can rewrite paths to make such operations safer to test.
*
* You may also transform file content to apply application-layer encryption or compression. This
* is particularly useful in situations where it's difficult or impossible to enable those features
* in the underlying filesystem.
*
* ### Abstract Functions Only
*
* Some filesystem functions like [copy] are implemented by using other features. These are the
* non-abstract functions in the [Filesystem] interface.
*
* **This class forwards only the abstract functions;** non-abstract functions delegate to the
* other functions of this class.
*/
@ExperimentalFilesystem
abstract class ForwardingFilesystem(
/** [Filesystem] to which this instance is delegating. */
@get:JvmName("delegate")
val delegate: Filesystem
) : Filesystem() {

/**
* Invoked each time a path is passed as a parameter to this filesystem. This returns the path to
* pass to [delegate], which should be [path] itself or a path on [delegate] that corresponds to
* it.
*
* Subclasses may override this to log accesses, fail on unexpected accesses, or map paths across
* filesystems.
*
* The base implementation returns [path].
*
* Note that this function will be called twice for calls to [atomicMove]; once for the source
* file and once for the target file.
*
* @param path the path passed to any of the functions of this.
* @param functionName a string like "canonicalize", "metadata", or "appendingSink".
* @param parameterName a string like "path", "file", "source", or "target".
* @return the path to pass to [delegate] for the same parameter.
*/
open fun onPathParameter(path: Path, functionName: String, parameterName: String): Path = path

/**
* Invoked each time a path is returned by [delegate]. This returns the path to return to the
* caller, which should be [path] itself or a path on this that corresponds to it.
*
* Subclasses may override this to log accesses, fail on unexpected path accesses, or map
* directories or path names.
*
* The base implementation returns [path].
*
* @param path the path returned by any of the functions of this.
* @param functionName a string like "canonicalize" or "list".
* @return the path to return to the caller.
*/
open fun onPathResult(path: Path, functionName: String): Path = path

@Throws(IOException::class)
override fun canonicalize(path: Path): Path {
val path = onPathParameter(path, "canonicalize", "path")
val result = delegate.canonicalize(path)
return onPathResult(result, "canonicalize")
}

@Throws(IOException::class)
override fun metadata(path: Path): FileMetadata {
val path = onPathParameter(path, "metadata", "path")
return delegate.metadata(path)
}

@Throws(IOException::class)
override fun list(dir: Path): List<Path> {
val dir = onPathParameter(dir, "list", "dir")
val result = delegate.list(dir)
return result.map { onPathResult(it, "list") }
}

@Throws(IOException::class)
override fun source(file: Path): Source {
val file = onPathParameter(file, "source", "file")
return delegate.source(file)
}

@Throws(IOException::class)
override fun sink(file: Path): Sink {
val file = onPathParameter(file, "sink", "file")
return delegate.sink(file)
}

@Throws(IOException::class)
override fun appendingSink(file: Path): Sink {
val file = onPathParameter(file, "appendingSink", "file")
return delegate.appendingSink(file)
}

@Throws(IOException::class)
override fun createDirectory(dir: Path) {
val dir = onPathParameter(dir, "createDirectory", "dir")
delegate.createDirectory(dir)
}

@Throws(IOException::class)
override fun atomicMove(source: Path, target: Path) {
val source = onPathParameter(source, "atomicMove", "source")
val target = onPathParameter(target, "atomicMove", "target")
delegate.atomicMove(source, target)
}

@Throws(IOException::class)
override fun delete(path: Path) {
val path = onPathParameter(path, "delete", "path")
delegate.delete(path)
}

override fun toString() = "${this::class.simpleName}($delegate)"
}
2 changes: 1 addition & 1 deletion okio/src/commonTest/kotlin/okio/AbstractFilesystemTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ abstract class AbstractFilesystemTest(

@Test
fun canonicalizeDotReturnsCurrentWorkingDirectory() {
if (filesystem is FakeFilesystem) return
if (filesystem is FakeFilesystem || filesystem is ForwardingFilesystem) return
val cwd = filesystem.canonicalize(".".toPath())
assertTrue(cwd.toString()) {
cwd.toString().endsWith("okio${Path.directorySeparator}okio") ||
Expand Down
122 changes: 122 additions & 0 deletions okio/src/commonTest/kotlin/okio/ForwardingFilesystemTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
* Copyright (C) 2020 Square, Inc.
*
* 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 okio

import kotlinx.datetime.Clock
import okio.Path.Companion.toPath
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
import kotlin.time.ExperimentalTime

@ExperimentalTime
@ExperimentalFilesystem
class ForwardingFilesystemTest : AbstractFilesystemTest(
clock = Clock.System,
filesystem = object : ForwardingFilesystem(FakeFilesystem()) {},
windowsLimitations = false,
temporaryDirectory = "/".toPath()
) {
@Test
fun pathBlocking() {
val forwardingFilesystem = object : ForwardingFilesystem(filesystem) {
override fun delete(path: Path) {
throw IOException("synthetic failure!")
}

override fun onPathParameter(path: Path, functionName: String, parameterName: String): Path {
if (path.name.contains("blocked")) throw IOException("blocked path!")
return path
}
}

forwardingFilesystem.createDirectory(base / "okay")
assertFailsWith<IOException> {
forwardingFilesystem.createDirectory(base / "blocked")
}
}

@Test
fun operationBlocking() {
val forwardingFilesystem = object : ForwardingFilesystem(filesystem) {
override fun onPathParameter(path: Path, functionName: String, parameterName: String): Path {
if (functionName == "delete") throw IOException("blocked operation!")
return path
}
}

forwardingFilesystem.createDirectory(base / "operation-blocking")
assertFailsWith<IOException> {
forwardingFilesystem.delete(base / "operation-blocking")
}
}

@Test
fun pathMapping() {
val prefix = "/mapped"
val source = base / "source"
val mappedSource = (prefix + source).toPath()
val target = base / "target"
val mappedTarget = (prefix + target).toPath()

source.writeUtf8("hello, world!")

val forwardingFilesystem = object : ForwardingFilesystem(filesystem) {
override fun onPathParameter(path: Path, functionName: String, parameterName: String): Path {
return path.toString().removePrefix(prefix).toPath()
}

override fun onPathResult(path: Path, functionName: String): Path {
return (prefix + path).toPath()
}
}

forwardingFilesystem.copy(mappedSource, mappedTarget)
assertTrue(target in filesystem.list(base))
assertTrue(mappedTarget in forwardingFilesystem.list(base))
assertEquals("hello, world!", source.readUtf8())
assertEquals("hello, world!", target.readUtf8())
}

@Test
fun copyIsNotForwarded() {
val log = mutableListOf<String>()

val delegate = object : ForwardingFilesystem(filesystem) {
override fun copy(source: Path, target: Path) {
throw AssertionError("unexpected call to copy()")
}
}

val forwardingFilesystem = object : ForwardingFilesystem(delegate) {
override fun onPathParameter(path: Path, functionName: String, parameterName: String): Path {
log += "$functionName($parameterName=$path)"
return path
}
}

val source = base / "source"
source.writeUtf8("hello, world!")
val target = base / "target"
forwardingFilesystem.copy(source, target)
assertTrue(target in filesystem.list(base))
assertEquals("hello, world!", source.readUtf8())
assertEquals("hello, world!", target.readUtf8())

assertEquals(log, listOf("source(file=$source)", "sink(file=$target)"))
}
}
2 changes: 2 additions & 0 deletions okio/src/jsMain/kotlin/okio/NodeJsSystemFilesystem.kt
Original file line number Diff line number Diff line change
Expand Up @@ -142,4 +142,6 @@ internal object NodeJsSystemFilesystem : Filesystem() {
throw IOException(e.message)
}
}

override fun toString() = "NodeJsSystemFilesystem"
}
2 changes: 2 additions & 0 deletions okio/src/jvmMain/kotlin/okio/JvmSystemFilesystem.kt
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,6 @@ internal object JvmSystemFilesystem : Filesystem() {
val deleted = path.toFile().delete()
if (!deleted) throw IOException("failed to delete $path")
}

override fun toString() = "JvmSystemFilesystem"
}
2 changes: 2 additions & 0 deletions okio/src/nativeMain/kotlin/okio/PosixSystemFilesystem.kt
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,6 @@ internal object PosixSystemFilesystem : Filesystem() {
override fun delete(path: Path) {
variantDelete(path)
}

override fun toString() = "PosixSystemFilesystem"
}

0 comments on commit 94b73fb

Please sign in to comment.