| /* | |
| * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. | |
| * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. | |
| */ | |
| package kotlin.jdk7.test | |
| import java.nio.file.FileSystemLoopException | |
| import java.nio.file.Path | |
| import kotlin.io.path.* | |
| import kotlin.test.* | |
| class PathTreeWalkTest : AbstractPathTest() { | |
| companion object { | |
| val referenceFilenames = listOf("1", "1/2", "1/3", "1/3/4.txt", "1/3/5.txt", "6", "7.txt", "8", "8/9.txt") | |
| val referenceFilesOnly = listOf("1/3/4.txt", "1/3/5.txt", "7.txt", "8/9.txt") | |
| fun createTestFiles(): Path { | |
| val basedir = createTempDirectory() | |
| for (name in referenceFilenames) { | |
| val file = basedir.resolve(name) | |
| if (file.extension.isEmpty()) | |
| file.createDirectories() | |
| else | |
| file.createFile() | |
| } | |
| return basedir | |
| } | |
| fun testVisitedFiles(expected: List<String>, walk: Sequence<Path>, basedir: Path, message: (() -> String)? = null) { | |
| val actual = walk.map { it.relativeToOrSelf(basedir).invariantSeparatorsPathString } | |
| assertEquals(expected.sorted(), actual.toList().sorted(), message?.invoke()) | |
| } | |
| } | |
| @Test | |
| fun visitOnce() { | |
| val basedir = createTestFiles().cleanupRecursively() | |
| testVisitedFiles(referenceFilesOnly, basedir.walk(), basedir) | |
| val expected = listOf("") + referenceFilenames | |
| testVisitedFiles(expected, basedir.walk(PathWalkOption.INCLUDE_DIRECTORIES), basedir) | |
| } | |
| @Test | |
| fun singleFile() { | |
| val testFile = createTempFile().cleanup() | |
| val nonExistentFile = testFile.resolve("foo") | |
| assertEquals(testFile, testFile.walk().single()) | |
| assertEquals(testFile, testFile.walk(PathWalkOption.INCLUDE_DIRECTORIES).single()) | |
| assertTrue(nonExistentFile.walk().none()) | |
| assertTrue(nonExistentFile.walk(PathWalkOption.INCLUDE_DIRECTORIES).none()) | |
| } | |
| @Test | |
| fun singleEmptyDirectory() { | |
| val testDir = createTempDirectory().cleanup() | |
| assertTrue(testDir.walk().none()) | |
| assertEquals(testDir, testDir.walk(PathWalkOption.INCLUDE_DIRECTORIES).single()) | |
| } | |
| @Test | |
| fun filterAndMap() { | |
| val basedir = createTestFiles().cleanupRecursively() | |
| testVisitedFiles(referenceFilesOnly, basedir.walk().filterNot { it.isDirectory() }, basedir) | |
| } | |
| @Test | |
| fun deleteTxtChildrenOnVisit() { | |
| fun visit(path: Path) { | |
| if (!path.isDirectory()) return | |
| for (child in path.listDirectoryEntries()) { | |
| if (child.name.endsWith("txt")) | |
| child.deleteExisting() | |
| } | |
| } | |
| val basedir = createTestFiles().cleanupRecursively() | |
| val walk = basedir.walk(PathWalkOption.INCLUDE_DIRECTORIES).onEach { visit(it) } | |
| val expected = listOf("", "1", "1/2", "1/3", "6", "8") | |
| testVisitedFiles(expected, walk, basedir) | |
| } | |
| @Test | |
| fun deleteSubtreeOnVisit() { | |
| val basedir = createTestFiles().cleanupRecursively() | |
| val walk = basedir.walk(PathWalkOption.INCLUDE_DIRECTORIES).onEach { path -> | |
| if (path.name == "1") { | |
| path.toFile().deleteRecursively() | |
| } | |
| } | |
| val expected = listOf("", "1", "6", "7.txt", "8", "8/9.txt") | |
| testVisitedFiles(expected, walk, basedir) | |
| } | |
| @Test | |
| fun addChildOnVisit() { | |
| val basedir = createTestFiles().cleanupRecursively() | |
| val walk = basedir.walk(PathWalkOption.INCLUDE_DIRECTORIES).onEach { path -> | |
| if (path.isDirectory()) { | |
| path.resolve("a.txt").createFile() | |
| } | |
| } | |
| val expected = referenceFilenames + listOf("", "a.txt", "1/a.txt", "1/2/a.txt", "1/3/a.txt", "6/a.txt", "8/a.txt") | |
| testVisitedFiles(expected, walk, basedir) | |
| } | |
| @Test | |
| fun exceptionOnVisit() { | |
| val basedir = createTestFiles().cleanupRecursively() | |
| val walk = basedir.walk(PathWalkOption.INCLUDE_DIRECTORIES).onEach { path -> | |
| if (path.name == "3") { | |
| throw RuntimeException("Test error") | |
| } | |
| } | |
| val error = assertFailsWith<RuntimeException> { | |
| walk.toList() | |
| } | |
| assertEquals("Test error", error.message) | |
| } | |
| @Test | |
| fun restrictedRead() { | |
| val basedir = createTestFiles().cleanupRecursively() | |
| val restrictedDir = basedir.resolve("1/3") | |
| withRestrictedRead(restrictedDir) { | |
| val walk = basedir.walk(PathWalkOption.INCLUDE_DIRECTORIES) | |
| val error = assertFailsWith<java.nio.file.AccessDeniedException> { | |
| walk.toList() | |
| } | |
| assertEquals(restrictedDir.toString(), error.file) | |
| } | |
| } | |
| @Test | |
| fun depthFirstOrder() { | |
| val basedir = createTestFiles().cleanupRecursively() | |
| val visited = HashSet<Path>() | |
| fun visit(path: Path) { | |
| if (path == basedir) { | |
| assertTrue(visited.isEmpty()) | |
| } else { | |
| assertTrue(visited.contains(path.parent)) | |
| } | |
| visited.add(path) | |
| } | |
| val walk = basedir.walk(PathWalkOption.INCLUDE_DIRECTORIES).onEach(::visit) | |
| val expected = referenceFilenames + listOf("") | |
| testVisitedFiles(expected, walk, basedir) | |
| assertEquals(expected.sorted(), visited.map { it.relativeToOrSelf(basedir).invariantSeparatorsPathString }.sorted()) | |
| } | |
| @Test | |
| fun addSiblingOnVisit() { | |
| fun makeBackup(file: Path) { | |
| val bakFile = Path("$file.bak") | |
| file.copyTo(bakFile) | |
| } | |
| val basedir = createTestFiles().cleanupRecursively() | |
| // added siblings do not appear during iteration | |
| testVisitedFiles(referenceFilesOnly, basedir.walk().onEach(::makeBackup), basedir) | |
| val expected = referenceFilenames + referenceFilesOnly.map { "$it.bak" } + listOf("") | |
| testVisitedFiles(expected, basedir.walk(PathWalkOption.INCLUDE_DIRECTORIES), basedir) | |
| } | |
| @Test | |
| fun find() { | |
| val basedir = createTestFiles().cleanupRecursively() | |
| basedir.resolve("8/4.txt").createFile() | |
| val count = basedir.walk().count { it.name == "4.txt" } | |
| assertEquals(2, count) | |
| } | |
| @Test | |
| fun symlinkToFile() { | |
| val basedir = createTestFiles().cleanupRecursively() | |
| val original = basedir.resolve("8/9.txt") | |
| basedir.resolve("1/3/link").tryCreateSymbolicLinkTo(original) ?: return | |
| for (followLinks in listOf(emptyArray(), arrayOf(PathWalkOption.FOLLOW_LINKS))) { | |
| val walk = basedir.walk(*followLinks) | |
| testVisitedFiles(referenceFilesOnly + listOf("1/3/link"), walk, basedir) | |
| } | |
| original.deleteExisting() | |
| for (followLinks in listOf(emptyArray(), arrayOf(PathWalkOption.FOLLOW_LINKS))) { | |
| val walk = basedir.walk(*followLinks) | |
| testVisitedFiles(referenceFilesOnly - listOf("8/9.txt") + listOf("1/3/link"), walk, basedir) | |
| } | |
| } | |
| @Test | |
| fun symlinkToDirectory() { | |
| val basedir = createTestFiles().cleanupRecursively() | |
| val original = basedir.resolve("8") | |
| basedir.resolve("1/3/link").tryCreateSymbolicLinkTo(original) ?: return | |
| // directory "8" contains "9.txt" file | |
| val followWalk = basedir.walk(PathWalkOption.INCLUDE_DIRECTORIES, PathWalkOption.FOLLOW_LINKS) | |
| testVisitedFiles(referenceFilenames + listOf("", "1/3/link", "1/3/link/9.txt"), followWalk, basedir) | |
| val nofollowWalk = basedir.walk(PathWalkOption.INCLUDE_DIRECTORIES) | |
| testVisitedFiles(referenceFilenames + listOf("", "1/3/link"), nofollowWalk, basedir) | |
| original.toFile().deleteRecursively() | |
| for (followLinks in listOf(emptyArray(), arrayOf(PathWalkOption.FOLLOW_LINKS))) { | |
| val walk = basedir.walk(PathWalkOption.INCLUDE_DIRECTORIES, *followLinks) | |
| testVisitedFiles(referenceFilenames - listOf("8", "8/9.txt") + listOf("", "1/3/link"), walk, basedir) | |
| } | |
| } | |
| @Test | |
| fun symlinkTwoPointingToEachOther() { | |
| val basedir = createTempDirectory().cleanupRecursively() | |
| val link1 = basedir.resolve("link1") | |
| val link2 = basedir.resolve("link2").tryCreateSymbolicLinkTo(link1) ?: return | |
| link1.tryCreateSymbolicLinkTo(link2) ?: return | |
| val walk = basedir.walk(PathWalkOption.FOLLOW_LINKS) | |
| testVisitedFiles(listOf("link1", "link2"), walk, basedir) | |
| } | |
| @Test | |
| fun symlinkPointingToItself() { | |
| val basedir = createTempDirectory().cleanupRecursively() | |
| val link = basedir.resolve("link") | |
| link.tryCreateSymbolicLinkTo(link) ?: return | |
| val walk = basedir.walk(PathWalkOption.FOLLOW_LINKS) | |
| testVisitedFiles(listOf("link"), walk, basedir) | |
| } | |
| @Test | |
| fun symlinkToSymlink() { | |
| val basedir = createTestFiles().cleanupRecursively() | |
| val original = basedir.resolve("8") | |
| val link = basedir.resolve("1/3/link").tryCreateSymbolicLinkTo(original) ?: return | |
| basedir.resolve("1/linkToLink").tryCreateSymbolicLinkTo(link) ?: return | |
| val walk = basedir.walk(PathWalkOption.INCLUDE_DIRECTORIES, PathWalkOption.FOLLOW_LINKS) | |
| val depth2ExpectedNames = | |
| listOf("", "1", "1/2", "1/3", "1/linkToLink", "6", "7.txt", "8", "8/9.txt") // linkToLink is visited | |
| val depth3ExpectedNames = depth2ExpectedNames + | |
| listOf("1/3/4.txt", "1/3/5.txt", "1/3/link", "1/linkToLink/9.txt") // "9.txt" is visited once more through linkToLink | |
| val depth4ExpectedNames = depth3ExpectedNames + | |
| listOf("1/3/link/9.txt") // "9.txt" is visited once more through link | |
| testVisitedFiles(depth4ExpectedNames, walk, basedir) // no depth limit | |
| } | |
| @Test | |
| fun symlinkBasedir() { | |
| val basedir = createTestFiles().cleanupRecursively() | |
| val link = createTempDirectory().cleanupRecursively().resolve("link").tryCreateSymbolicLinkTo(basedir) ?: return | |
| run { | |
| val followWalk = link.walk(PathWalkOption.INCLUDE_DIRECTORIES, PathWalkOption.FOLLOW_LINKS) | |
| testVisitedFiles(referenceFilenames + listOf(""), followWalk, link) | |
| testVisitedFiles(referenceFilesOnly, link.walk(PathWalkOption.FOLLOW_LINKS), link) | |
| val nofollowWalk = link.walk(PathWalkOption.INCLUDE_DIRECTORIES) | |
| assertEquals(link, nofollowWalk.single()) | |
| assertEquals(link, link.walk().single()) | |
| } | |
| run { | |
| basedir.toFile().deleteRecursively() | |
| val followWalk = link.walk(PathWalkOption.INCLUDE_DIRECTORIES, PathWalkOption.FOLLOW_LINKS) | |
| assertEquals(link, followWalk.single()) | |
| val nofollowWalk = link.walk(PathWalkOption.INCLUDE_DIRECTORIES) | |
| assertEquals(link, nofollowWalk.single()) | |
| } | |
| } | |
| @Test | |
| fun symlinkCyclic() { | |
| val basedir = createTestFiles().cleanupRecursively() | |
| val original = basedir.resolve("1") | |
| val link = original.resolve("2/link").tryCreateSymbolicLinkTo(original) ?: return | |
| for (order in listOf(arrayOf(), arrayOf(PathWalkOption.BREADTH_FIRST))) { | |
| val walk = basedir.walk(PathWalkOption.FOLLOW_LINKS, *order) | |
| val error = assertFailsWith<FileSystemLoopException> { | |
| walk.toList() | |
| } | |
| assertEquals(link.toString(), error.file) | |
| } | |
| } | |
| @Test | |
| fun symlinkCyclicWithTwo() { | |
| val basedir = createTestFiles().cleanupRecursively() | |
| val dir8 = basedir.resolve("8") | |
| val dir2 = basedir.resolve("1/2") | |
| dir8.resolve("linkTo2").tryCreateSymbolicLinkTo(dir2) ?: return | |
| dir2.resolve("linkTo8").tryCreateSymbolicLinkTo(dir8) ?: return | |
| for (order in listOf(arrayOf(), arrayOf(PathWalkOption.BREADTH_FIRST))) { | |
| val walk = basedir.walk(PathWalkOption.FOLLOW_LINKS, *order) | |
| assertFailsWith<FileSystemLoopException> { | |
| walk.toList() | |
| } | |
| } | |
| } | |
| @Test | |
| fun breadthFirstOrder() { | |
| val basedir = createTestFiles().cleanupRecursively() | |
| val walk = basedir.walk(PathWalkOption.BREADTH_FIRST, PathWalkOption.INCLUDE_DIRECTORIES) | |
| val depth0 = mutableListOf("") | |
| val depth1 = mutableListOf("1", "6", "7.txt", "8") | |
| val depth2 = mutableListOf("1/2", "1/3", "8/9.txt") | |
| val depth3 = mutableListOf("1/3/4.txt", "1/3/5.txt") | |
| for (file in walk) { | |
| when (val pathString = file.relativeToOrSelf(basedir).invariantSeparatorsPathString) { | |
| in depth0 -> { | |
| depth0.remove(pathString) | |
| } | |
| in depth1 -> { | |
| assertTrue(depth0.isEmpty()) | |
| depth1.remove(pathString) | |
| } | |
| in depth2 -> { | |
| assertTrue(depth1.isEmpty()) | |
| depth2.remove(pathString) | |
| } | |
| in depth3 -> { | |
| assertTrue(depth2.isEmpty()) | |
| depth3.remove(pathString) | |
| } | |
| else -> { | |
| fail("Unexpected file: $file. It might have appeared for the second time.") | |
| } | |
| } | |
| } | |
| assertTrue( | |
| depth0.isEmpty() && depth1.isEmpty() && depth2.isEmpty() && depth3.isEmpty(), | |
| "The following files were not visited: $depth0, $depth1, $depth2 $depth3" | |
| ) | |
| } | |
| @Test | |
| fun breadthFirstOnlyFiles() { | |
| val basedir = createTestFiles().cleanupRecursively() | |
| val walk = basedir.walk(PathWalkOption.BREADTH_FIRST) | |
| val depth1 = mutableListOf("7.txt") | |
| val depth2 = mutableListOf("8/9.txt") | |
| val depth3 = mutableListOf("1/3/4.txt", "1/3/5.txt") | |
| for (file in walk) { | |
| when (val pathString = file.relativeToOrSelf(basedir).invariantSeparatorsPathString) { | |
| in depth1 -> { | |
| depth1.remove(pathString) | |
| } | |
| in depth2 -> { | |
| assertTrue(depth1.isEmpty()) | |
| depth2.remove(pathString) | |
| } | |
| in depth3 -> { | |
| assertTrue(depth2.isEmpty()) | |
| depth3.remove(pathString) | |
| } | |
| else -> { | |
| fail("Unexpected file: $file. It might have appeared for the second time.") | |
| } | |
| } | |
| } | |
| assertTrue( | |
| depth1.isEmpty() && depth2.isEmpty() && depth3.isEmpty(), | |
| "The following files were not visited: $depth1, $depth2 $depth3" | |
| ) | |
| } | |
| } |