From f560677b1520ed6182b7416d0e83e7c4b4fd4906 Mon Sep 17 00:00:00 2001 From: Denis Mekhanikov Date: Thu, 23 Oct 2014 17:17:01 +0400 Subject: [PATCH] Kotlin I/O new features: relativeTo, copyTo, copyRecursively, deleteRecursively, file tree walkers, file component iterators, file roots, startsWith, endsWith, subPath, normalize, replaceBytes, replaceTest, additional tests and comments --- .../jps/build/AbstractIncrementalJpsTest.kt | 1 + libraries/stdlib/src/kotlin/io/Exceptions.kt | 45 + libraries/stdlib/src/kotlin/io/Files.kt | 95 -- libraries/stdlib/src/kotlin/io/IOStreams.kt | 18 +- libraries/stdlib/src/kotlin/io/ReadWrite.kt | 134 ++- .../src/kotlin/io/files/FileIterator.kt | 138 +++ .../src/kotlin/io/files/FileTreeWalk.kt | 251 ++++++ libraries/stdlib/src/kotlin/io/files/Utils.kt | 514 +++++++++++ libraries/stdlib/test/io/Files.kt | 845 +++++++++++++++++- libraries/stdlib/test/io/IOStreams.kt | 29 + libraries/stdlib/test/io/ReadWrite.kt | 34 +- 11 files changed, 1949 insertions(+), 155 deletions(-) create mode 100644 libraries/stdlib/src/kotlin/io/Exceptions.kt delete mode 100644 libraries/stdlib/src/kotlin/io/Files.kt create mode 100644 libraries/stdlib/src/kotlin/io/files/FileIterator.kt create mode 100644 libraries/stdlib/src/kotlin/io/files/FileTreeWalk.kt create mode 100644 libraries/stdlib/src/kotlin/io/files/Utils.kt create mode 100644 libraries/stdlib/test/io/IOStreams.kt diff --git a/jps-plugin/test/org/jetbrains/kotlin/jps/build/AbstractIncrementalJpsTest.kt b/jps-plugin/test/org/jetbrains/kotlin/jps/build/AbstractIncrementalJpsTest.kt index 61f75320692..779125bf233 100644 --- a/jps-plugin/test/org/jetbrains/kotlin/jps/build/AbstractIncrementalJpsTest.kt +++ b/jps-plugin/test/org/jetbrains/kotlin/jps/build/AbstractIncrementalJpsTest.kt @@ -374,6 +374,7 @@ public abstract class AbstractIncrementalJpsTest : JpsBuildTestCase() { val file = File(workDir, path) val oldLastModified = file.lastModified() + file.delete() dataFile.copyTo(file) val newLastModified = file.lastModified() diff --git a/libraries/stdlib/src/kotlin/io/Exceptions.kt b/libraries/stdlib/src/kotlin/io/Exceptions.kt new file mode 100644 index 00000000000..f8533d868f4 --- /dev/null +++ b/libraries/stdlib/src/kotlin/io/Exceptions.kt @@ -0,0 +1,45 @@ +package kotlin.io + +import java.io.File +import java.io.IOException + +private fun constructMessage(file: File, other: File?, reason: String?): String { + val sb = StringBuilder(file.toString()) + if (other != null) { + sb.append(" -> $other") + } + if (reason != null) { + sb.append(": $reason") + } + return sb.toString() +} + +/** + * A base exception class for file system exceptions + */ +open public class FileSystemException(public val file: File, + public val other: File? = null, + public val reason: String? = null + ) : IOException(constructMessage(file, other, reason)) + +/** + * An exception class which is used when some file to create or copy to already exists + */ +public class FileAlreadyExistsException(file: File, + other: File? = null, + reason: String? = null) : FileSystemException(file, other, reason) + +/** + * An exception class which is used when we have not enough access for some operation + */ +public class AccessDeniedException(file: File, + other: File? = null, + reason: String? = null) : FileSystemException(file, other, reason) + +/** + * An exception class which is used when file to copy does not exist + */ +public class NoSuchFileException(file: File, + other: File? = null, + reason: String? = null) : FileSystemException(file, other, reason) + diff --git a/libraries/stdlib/src/kotlin/io/Files.kt b/libraries/stdlib/src/kotlin/io/Files.kt deleted file mode 100644 index d7c4769b929..00000000000 --- a/libraries/stdlib/src/kotlin/io/Files.kt +++ /dev/null @@ -1,95 +0,0 @@ -package kotlin.io - -import java.io.* -import java.nio.charset.* -import java.util.* - -/** - * Recursively process this file and all children with the given block. - * Note that if this file doesn't exist, then the block will be executed on it anyway. - */ -public fun File.recurse(block: (File) -> Unit): Unit { - block(this) - listFiles()?.forEach { it.recurse(block) } -} - -/** - * Returns `this` if this file is a directory, or the parent if it is a file inside a directory - */ -public val File.directory: File - get() = if (isDirectory()) this else getParentFile()!! - -/** - * Returns the canonical path of this file. - */ -public val File.canonicalPath: String - get() = getCanonicalPath() - -/** - * Returns the file name or "" for an empty name - */ -public val File.name: String - get() = getName() - -/** - * Returns the file path or "" for an empty name - */ -public val File.path: String - get() = getPath() - -/** - * Returns the extension of this file (not including the dot), or an empty string if it doesn't have one. - */ -public val File.extension: String - get() { - return name.substringAfterLast('.', "") - } - -/** - * Returns true if the given [file] is in the same directory as this file or a descendant directory. - */ -public fun File.isDescendant(file: File): Boolean { - return file.directory.canonicalPath.startsWith(directory.canonicalPath) -} - -/** - * Returns the relative path of the given descendant of this file if it is a descendant, or an empty string otherwise. - */ -public fun File.relativePath(descendant: File): String { - val prefix = directory.canonicalPath - val answer = descendant.canonicalPath - return if (answer.startsWith(prefix)) { - val prefixSize = prefix.length() - if (answer.length() > prefixSize) { - answer.substring(prefixSize + 1) - } else "" - } else { - answer - } -} - -/** - * Copies this file to the given output [file]. - * @param bufferSize the buffer size to use when copying. - * @return the number of bytes copied - */ -public fun File.copyTo(file: File, bufferSize: Int = defaultBufferSize): Long { - file.directory.mkdirs() - val input = FileInputStream(this) - return input.use{ - val output = FileOutputStream(file) - output.use{ - input.copyTo(output, bufferSize) - } - } -} - -/** - * Returns an array of files and directories in this directory that satisfy the specified [filter], - * or null if this file does not denote a directory. - */ -public fun File.listFiles(filter: (file: File) -> Boolean): Array? = listFiles( - object : FileFilter { - override fun accept(file: File) = filter(file) - } - ) diff --git a/libraries/stdlib/src/kotlin/io/IOStreams.kt b/libraries/stdlib/src/kotlin/io/IOStreams.kt index 6401ee97879..e65114fecfb 100644 --- a/libraries/stdlib/src/kotlin/io/IOStreams.kt +++ b/libraries/stdlib/src/kotlin/io/IOStreams.kt @@ -5,9 +5,11 @@ import java.nio.charset.Charset import java.nio.charset.CharsetDecoder import java.nio.charset.CharsetEncoder +// Note: hasNext() is incorrect because available() means "number of available bytes WITHOUT BLOCKING" + /** Returns an [Iterator] of bytes in this input stream. */ public fun InputStream.iterator(): ByteIterator = - object: ByteIterator() { + object : ByteIterator() { override fun hasNext(): Boolean = available() > 0 public override fun nextByte(): Byte = read().toByte() @@ -75,3 +77,17 @@ public fun InputStream.readBytes(estimatedSize: Int = defaultBufferSize): ByteAr this.copyTo(buffer) return buffer.toByteArray() } + +/** + * Constructs a new FileInputStream of this file and returns it as a result. + */ +public fun File.inputStream(): InputStream { + return FileInputStream(this) +} + +/** + * Constructs a new FileOutputStream of this file and returns it as a result. + */ +public fun File.outputStream(): OutputStream { + return FileOutputStream(this) +} diff --git a/libraries/stdlib/src/kotlin/io/ReadWrite.kt b/libraries/stdlib/src/kotlin/io/ReadWrite.kt index d441139b617..e2acf39122c 100644 --- a/libraries/stdlib/src/kotlin/io/ReadWrite.kt +++ b/libraries/stdlib/src/kotlin/io/ReadWrite.kt @@ -7,14 +7,16 @@ import java.util.ArrayList import java.util.NoSuchElementException /** - * Creates a new [FileReader] for reading the contents of this file. + * @return a new [FileReader] for reading the contents of this file. */ public fun File.reader(): FileReader = FileReader(this) /** * Reads the entire content of this file as a byte array. * - * This method is not recommended on huge files. + * This method is not recommended on huge files. It has an internal limitation of 2 GB byte array size + * + * @return the entire content of this file as a byte array */ public fun File.readBytes(): ByteArray { return FileInputStream(this).use { it.readBytes(length().toInt()) } @@ -22,13 +24,27 @@ public fun File.readBytes(): ByteArray { /** * Replaces the contents of this file with [data]. + * + * @data byte array to write into this file */ +deprecated("Use replaceBytes instead") public fun File.writeBytes(data: ByteArray): Unit { return FileOutputStream(this).use { it.write(data) } } +/** + * Replaces the contents of this file with [data]. + * + * @data byte array to write into this file + */ +public fun File.replaceBytes(data: ByteArray): Unit { + return FileOutputStream(this).use { it.write(data) } +} + /** * Appends [data] to the contents of this file. + * + * @data byte array to append to this file */ public fun File.appendBytes(data: ByteArray): Unit { return FileOutputStream(this, true).use { it.write(data) } @@ -37,33 +53,72 @@ public fun File.appendBytes(data: ByteArray): Unit { /** * Reads the entire content of this file as a String using specified [charset]. * - * This method is not recommended on huge files. + * This method is not recommended on huge files. It has an internal limitation of 2 GB file size + * + * @param charset character set to use + * @return the entire content of this file as a String */ +deprecated("There is a same function with default parameter value") public fun File.readText(charset: String): String = readBytes().toString(charset) /** * Reads the entire content of this file as a String using UTF-8 or specified [charset]. * - * This method is not recommended on huge files. + * This method is not recommended on huge files. It has an internal limitation of 2 GB file size + * + * @param charset character set to use + * @return the entire content of this file as a String */ public fun File.readText(charset: Charset = Charsets.UTF_8): String = readBytes().toString(charset) /** * Replaces the contents of this file with [text] encoded using the specified [charset]. + * + * @param text text to write into file + * @param charset character set to use */ +deprecated("Use replaceText instead") public fun File.writeText(text: String, charset: String): Unit { - writeBytes(text.toByteArray(charset)) + replaceBytes(text.toByteArray(charset)) +} + +/** + * Replaces the contents of this file with [text] encoded using the specified [charset]. + * + * @param text text to write into file + * @param charset character set to use + */ +deprecated("There is a same function with default parameter value") +public fun File.replaceText(text: String, charset: String): Unit { + replaceBytes(text.toByteArray(charset)) } /** * Replaces the contents of this file with [text] encoded using UTF-8 or specified [charset]. + * + * @param text text to write into file + * @param charset character set to use */ +deprecated("Use replaceText instead") public fun File.writeText(text: String, charset: Charset = Charsets.UTF_8): Unit { - writeBytes(text.toByteArray(charset)) + replaceBytes(text.toByteArray(charset)) +} + +/** + * Replaces the contents of this file with [text] encoded using UTF-8 or specified [charset]. + * + * @param text text to write into file + * @param charset character set to use + */ +public fun File.replaceText(text: String, charset: Charset = Charsets.UTF_8): Unit { + replaceBytes(text.toByteArray(charset)) } /** * Appends [text] to the contents of this file using UTF-8 or the specified [charset]. + * + * @param text text to append to file + * @param charset character set to use */ public fun File.appendText(text: String, charset: Charset = Charsets.UTF_8): Unit { appendBytes(text.toByteArray(charset)) @@ -71,7 +126,11 @@ public fun File.appendText(text: String, charset: Charset = Charsets.UTF_8): Uni /** * Appends [text] to the contents of the file using the specified [charset]. + * + * @param text text to append to file + * @param charset character set to use */ +deprecated("There is a same function with default parameter value") public fun File.appendText(text: String, charset: String): Unit { appendBytes(text.toByteArray(charset)) } @@ -79,9 +138,11 @@ public fun File.appendText(text: String, charset: String): Unit { /** * Reads file by byte blocks and calls [closure] for each block read. Block size depends on implementation * but is never less than 512 bytes. - * This functions passes the byte array and amount of bytes in this buffer to the [closure] function. + * This functions passes the byte array and amount of bytes in the array to the [closure] function. * * You can use this function for huge files. + * + * @param closure function to proceed file blocks */ public fun File.forEachBlock(closure: (ByteArray, Int) -> Unit): Unit { val arr = ByteArray(4096) @@ -102,9 +163,13 @@ public fun File.forEachBlock(closure: (ByteArray, Int) -> Unit): Unit { } /** - * Reads this file line by line using the specified [charset]. Default charset is UTF-8. + * Reads this file line by line using the specified [charset] and calls [closure] for each line. + * Default charset is UTF-8. * * You may use this function on huge files. + * + * @param charset character set to use + * @param closure function to proceed file lines */ public fun File.forEachLine(charset: Charset = Charsets.UTF_8, closure: (line: String) -> Unit): Unit { val reader = BufferedReader(InputStreamReader(FileInputStream(this), charset)) @@ -116,23 +181,34 @@ public fun File.forEachLine(charset: Charset = Charsets.UTF_8, closure: (line: S } /** - * Reads this file line by line using the specified [charset]. + * Reads this file line by line using the specified [charset] and calls [closure] for each line. * * You may use this function on huge files. + * + * @param charset character set to use + * @param closure function to proceed file lines */ +deprecated("There is a same function with default parameter value") public fun File.forEachLine(charset: String, closure: (line: String) -> Unit): Unit = forEachLine(Charset.forName(charset), closure) /** * Reads the file content as a list of lines, using the specified [charset]. * * Do not use this function for huge files. + * + * @param charset character set to use + * @return list of file lines */ +deprecated("There is a same function with default parameter value") public fun File.readLines(charset: String): List = readLines(Charset.forName(charset)) /** - * Reads the file content as a list of strings. By default uses UTF-8 charset. + * Reads the file content as a list of lines. By default uses UTF-8 charset. * * Do not use this function for huge files. + * + * @param charset character set to use + * @return list of file lines */ public fun File.readLines(charset: Charset = Charsets.UTF_8): List { val result = ArrayList() @@ -140,16 +216,19 @@ public fun File.readLines(charset: Charset = Charsets.UTF_8): List { return result } -/** Creates a buffered reader wrapping this Reader, or returns this Reader if it is already buffered. */ +/** @return a buffered reader wrapping this Reader, or this Reader itself if it is already buffered. */ public fun Reader.buffered(bufferSize: Int = defaultBufferSize): BufferedReader = if (this is BufferedReader) this else BufferedReader(this, bufferSize) -/** Creates a buffered writer wrapping this Writer, or returns this Writer if it is already buffered. */ +/** @return a buffered reader wrapping this Writer, or this Writer itself if it is already buffered. */ public fun Writer.buffered(bufferSize: Int = defaultBufferSize): BufferedWriter = if (this is BufferedWriter) this else BufferedWriter(this, bufferSize) /** - * Iterates through each line of this reader and closes the [Reader] when it's completed + * Iterates through each line of this reader, calls [block] for each line read + * and closes the [Reader] when it's completed + * + * @param block function to proceed file lines */ public fun Reader.forEachLine(block: (String) -> Unit): Unit = useLines { lines -> lines.forEach(block) } @@ -162,7 +241,8 @@ public inline fun Reader.useLines(block: (Sequence) -> T): T = this.buffered().use { block(it.lines()) } /** - * Returns an iterator over each line. + * @return a stream of corresponding file lines + * * *Note*: the caller must close the underlying `BufferedReader` * when the iteration is finished; as the user may not complete the iteration loop (e.g. using a method like find() or any() on the iterator * may terminate the iteration early. @@ -204,6 +284,8 @@ private class LinesStream(private val reader: BufferedReader) : Sequence * Reads this reader completely as a String * * *Note*: It is the caller's responsibility to close this resource. + * + * @return the string with corresponding file contents */ public fun Reader.readText(): String { val buffer = StringWriter() @@ -212,9 +294,13 @@ public fun Reader.readText(): String { } /** - * Copies this reader to the given output writer, returning the number of bytes copied. + * Copies this reader to the given [out] writer, returning the number of bytes copied. * * **Note** it is the caller's responsibility to close both of these resources + * + * @param out writer to write to + * @param bufferSize size of character buffer to use in process + * @return number of bytes copies */ public fun Reader.copyTo(out: Writer, bufferSize: Int = defaultBufferSize): Long { var charsCopied: Long = 0 @@ -229,16 +315,23 @@ public fun Reader.copyTo(out: Writer, bufferSize: Int = defaultBufferSize): Long } /** - * Reads the entire content of the URL as a String using the specified [charset]. + * Reads the entire content of this URL as a String using the specified [charset]. * * This method is not recommended on huge files. + * + * @param charset a character set to use + * @return a string with this URL entire content */ +deprecated("There is a same function with default parameter value") public fun URL.readText(charset: String): String = readBytes().toString(charset) /** - * Reads the entire content of the URL as a String using UTF-8 or the specified [charset]. + * Reads the entire content of this URL as a String using UTF-8 or the specified [charset]. * * This method is not recommended on huge files. + * + * @param charset a character set to use + * @return a string with this URL entire content */ public fun URL.readText(charset: Charset = Charsets.UTF_8): String = readBytes().toString(charset) @@ -246,12 +339,17 @@ public fun URL.readText(charset: Charset = Charsets.UTF_8): String = readBytes() * Reads the entire content of the URL as bytes * * This method is not recommended on huge files. + * + * @return a byte array with this URL entire content */ public fun URL.readBytes(): ByteArray = this.openStream()!!.use{ it.readBytes() } /** - * Executes the given [block] on this resource and then closes it down correctly whether an exception + * Executes the given [block] function on this resource and then closes it down correctly whether an exception * is thrown or not + * + * @block a function to proceed this closable resource + * @return the result of [block] function on this closable resource */ public inline fun T.use(block: (T) -> R): R { var closed = false diff --git a/libraries/stdlib/src/kotlin/io/files/FileIterator.kt b/libraries/stdlib/src/kotlin/io/files/FileIterator.kt new file mode 100644 index 00000000000..b05ff14d788 --- /dev/null +++ b/libraries/stdlib/src/kotlin/io/files/FileIterator.kt @@ -0,0 +1,138 @@ +package kotlin.io + +import java.io.File +import java.util.NoSuchElementException + +fun String.getRootName(): String { + var first = this.indexOf(File.separatorChar, 0) + if (first==0) { + if (length() > 1 && this[1]==File.separatorChar) { + // Network names like //my.host/home/something => //my.host/home/ should be root + first = this.indexOf(File.separatorChar, 2) + if (first>=0) { + first = this.indexOf(File.separatorChar, first+1) + if (first >= 0) + return this.substring(0, first+1) + } + } + return this.substring(0, 1) + } + // C:\ + if (first > 0 && this[first-1]==':') { + first++ + return this.substring(0, first) + } + // C: + if (first==-1 && this.endsWith(':')) + return this + return "" + +} + +val File.rootName: String + get() = separatorsToSystem().getRootName() + +/** + * Returns root component of this abstract name, like / from /home/user, or C:\ from C:\file.tmp, + * or //my.host/home for //my.host/home/user, + * or null if this name is relative, like bar/gav + */ +public val File.root: File? + get() { + val name = rootName + return if (name.length() > 0) File(name) else null + } + +/** + * Iterates over elements of file, e.g. for /foo/bar/gav + * we will have foo, bar and gav + */ +class FileIterator(private val f: File): Iterator { + + private val path = f.separatorsToSystem() + + val root = path.getRootName() + + // Begin from the last root character + private var separatorIndex = root.length()-1 + + private var nextIndex = separatorIndex + + /** + * Returns the next element for this file iterator + * @throws NoSuchElementException if there is no next element + */ + override fun next(): File { + if (!hasNext()) { + throw NoSuchElementException() + } + val prevIndex = separatorIndex + separatorIndex = nextIndex + return File(path.substring(prevIndex+1, nextIndex)) + } + + /** + * Returns true if the next element is available for this file iterator, + * or false otherwise + */ + override fun hasNext(): Boolean { + if (nextIndex == separatorIndex) { + // Consider special "" case + if (separatorIndex==-1 && path.length()==0) { + nextIndex = 0 + return true + } + // /foo/bar/gav/, next points to last / + if (nextIndex >= path.length()-1) + return false + nextIndex = path.indexOf(File.separatorChar, separatorIndex+1) + // To remove all "" like in the middle of /home//something/ + if (nextIndex==separatorIndex+1) { + separatorIndex = nextIndex + return hasNext() + } + } + if (nextIndex == -1) + nextIndex = path.length() + return true + } +} + +/** + * Contains number of components in this abstract path name, + * e.g. 3 in /home/user/tmp (home, user, tmp) or 0 in /. + * Empty path name has one component which is the empty name itself. + */ +public val File.nameCount: Int + get() { + var i = 0 + for (elem in this) i++ + return i + } + +/** + * Returns a relative pathname which is a subsequence of this pathname, + * beginning from component [beginIndex], inclusive, + * ending at component [endIndex], exclusive. + * Number 0 belongs to a component closest to the root, + * number count-1 belongs to a component farthest from the root + * @throws IllegalArgumentException if [beginIndex] is negative, +* or [endIndex] is greater than existing number of components, +* or [beginIndex] is greater than or equals to [endIndex] + */ +public fun File.subPath(beginIndex: Int, endIndex: Int): File { + if (beginIndex < 0 || beginIndex >= endIndex || endIndex > nameCount) + throw IllegalArgumentException() + var res = "" + var i = 0 + for (elem in this) { + if (i >= beginIndex && i < endIndex) { + res += elem.toString() + if (i != endIndex-1) + res += File.separatorChar + } + i++ + } + return File(res) + +} diff --git a/libraries/stdlib/src/kotlin/io/files/FileTreeWalk.kt b/libraries/stdlib/src/kotlin/io/files/FileTreeWalk.kt new file mode 100644 index 00000000000..717157e6e59 --- /dev/null +++ b/libraries/stdlib/src/kotlin/io/files/FileTreeWalk.kt @@ -0,0 +1,251 @@ +package kotlin.io.files + +import java.io.File +import java.io.FileNotFoundException +import java.io.IOException +import java.util.ArrayList +import java.util.NoSuchElementException +import java.util.Stack + +/** + * Created by Mikhail.Glukhikh on 13/03/2015. + * + * An alternative version of file walker + */ + +enum class FileWalkDirection { + /** Depth-first search, directory is visited BEFORE its files */ + TOP_DOWN + /** Depth-first search, directory is visited AFTER its files */ + BOTTOM_UP + // Do we want also breadth-first search? +} + +class FileTreeWalk(private val start: File, + private val direction: FileWalkDirection = FileWalkDirection.TOP_DOWN, + private val enter: (File) -> Unit = {}, + private val leave: (File) -> Unit = {}, + private val fail: (f: File, e: IOException) -> Unit = { f, e -> Unit }, + private val filter: (File) -> Boolean = { it -> true }, + private val maxDepth: Int = Int.MAX_VALUE + ): Stream { + + private abstract class DirectoryState(val rootDir: File) { + init { + if (!rootDir.isDirectory()) + throw IllegalArgumentException("Directory is needed") + } + abstract public fun step(): File? + } + + private inner class BottomUpDirectoryState(rootDir: File): DirectoryState(rootDir) { + + private var rootVisited = false + + private var fileList: Array? = null + + private var fileIndex = 0 + + private var failed = false + + override public fun step(): File? { + if (!failed && fileList == null) { + enter(rootDir) + fileList = rootDir.listFiles() + if (fileList == null) { + fail(rootDir, AccessDeniedException(file = rootDir, reason = "Cannot list files in a directory")) + failed = true + } + } + if (fileList != null && fileIndex < fileList!!.size()) { + // First visit all files + return fileList!![fileIndex++] + } else if (!rootVisited) { + // Then visit root + rootVisited = true + return rootDir + } else { + // That's all + leave(rootDir) + return null + } + } + } + + private inner class TopDownDirectoryState(rootDir: File): DirectoryState(rootDir) { + + private var rootVisited = false + + private var fileList: Array? = null + + private var fileIndex = 0 + + override public fun step(): File? { + if (!rootVisited) { + // First visit root + enter(rootDir) + rootVisited = true + return rootDir + } else if (fileList == null || fileIndex < fileList!!.size()) { + if (fileList == null) { + // Then read an array of files, if any + fileList = rootDir.listFiles() + if (fileList == null) { + fail(rootDir, AccessDeniedException(file = rootDir, reason = "Cannot list files in a directory")) + } + if (fileList == null || fileList!!.size() == 0) { + leave(rootDir) + return null + } + } + // Then visit all files + return fileList!![fileIndex++] + } else { + // That's all + leave(rootDir) + return null + } + } + } + + // Stack of directory states, beginning from the start directory + private val state = Stack() + + // We are already at the end or not? + private var end = false + + // A future result of next() call + private var nextFile: File? = null + + init { + if (!start.exists()) { + throw FileNotFoundException("The start file doesn't exist: $start") + } + if (start.isDirectory() && filter(start)) { + pushState(start) + } + } + + private fun pushState(root: File) { + state.push(when (direction) { + FileWalkDirection.TOP_DOWN -> TopDownDirectoryState(root) + FileWalkDirection.BOTTOM_UP -> BottomUpDirectoryState(root) + }) + } + + private fun gotoNext(): File? { + if (end) { + // We are already at the end + return null + } else if (state.empty()) { + // There is nothing in the state + // We must visit "start" if it's a file and matches the filter + end = true + return if (!start.isDirectory() && filter(start)) start else null + } + // Take next file from the top of the stack + val topState = state.peek() + val file = topState.step() + if (file == null) { + // There is nothing more on the top of the stack, go back + state.pop() + return gotoNext() + } else { + // Check that file/directory matches the filter + if (!filter(file)) + return gotoNext() + if (file == topState.rootDir || !file.isDirectory() || state.size() >= maxDepth) { + // Proceed a root directory or a simple file + return file + } else { + // Proceed a sub-directory + pushState(file) + return gotoNext() + } + } + } + + /** + * Sets enter directory function. + * Enter function is called BEFORE the corresponding directory and its files are visited + */ + public fun enter(f: (File) -> Unit): FileTreeWalk { + return FileTreeWalk(start, direction, f, leave, fail, filter, maxDepth) + } + + /** + * Sets leave directory function. + * Leave function is called AFTER the corresponding directory and its files are visited + */ + public fun leave(f: (File) -> Unit): FileTreeWalk { + return FileTreeWalk(start, direction, enter, f, fail, filter, maxDepth) + } + + /** + * Set fail entering directory function. + * Fail function is called when walker is unable to get list of directory files. + * Enter and leave functions are called even in this case. + */ + public fun fail(f: (File, IOException) -> Unit): FileTreeWalk { + return FileTreeWalk(start, direction, enter, leave, f, filter, maxDepth) + } + + /** + * Sets filter function. + * Filter function is called before visiting files and entering directories. + * If it returns false, file is not visited, directory is not entered and all its content is also not visited. + * If it returns true, everything goes in the regular way + */ + public fun filter(f: (File) -> Boolean): FileTreeWalk { + return FileTreeWalk(start, direction, enter, leave, fail, f, maxDepth) + } + + /** + * Sets maximum depth of walk. Int.MAX_VALUE is used for unlimited. + * Negative and zero values are not allowed + */ + public fun maxDepth(depth: Int): FileTreeWalk { + if (depth <= 0) + throw IllegalArgumentException("Use positive depth value") + return FileTreeWalk(start, direction, enter, leave, fail, filter, depth) + } + + private val it = object: Iterator { + override fun hasNext(): Boolean { + if (nextFile == null) + nextFile = gotoNext() + return (nextFile != null) + } + override fun next(): File { + if (nextFile == null) + nextFile = gotoNext() + val res = nextFile + if (res == null) + throw NoSuchElementException() + // With nextFile = gotoNext() here some enter() / leave() can be called BEFORE visiting res + nextFile = null + // Visit this directory or file + return res + } + } + + override fun iterator(): Iterator { + return it + } +} + +fun File.walk(direction: FileWalkDirection = FileWalkDirection.TOP_DOWN): FileTreeWalk = + FileTreeWalk(this, direction) + +/** + * Gets a stream for visiting this directory and all its content in top-down order. + * Depth-first search is used and directories are visited before all their files + */ +public fun File.walkTopDown(): FileTreeWalk = walk(FileWalkDirection.TOP_DOWN) + +/** + * Gets a stream for visiting this directory and all its content in bottom-up order. + * Depth-first search is used and directories are visited after all their files + */ +public fun File.walkBottomUp(): FileTreeWalk = walk(FileWalkDirection.BOTTOM_UP) + diff --git a/libraries/stdlib/src/kotlin/io/files/Utils.kt b/libraries/stdlib/src/kotlin/io/files/Utils.kt new file mode 100644 index 00000000000..bdfd69fcd2d --- /dev/null +++ b/libraries/stdlib/src/kotlin/io/files/Utils.kt @@ -0,0 +1,514 @@ +package kotlin.io + +import java.io.* +import java.util.ArrayList + +/** + * Creates an empty directory in the specified [directory], using the given [prefix] and [suffix] to generate its name. + * + * If [prefix] is not specified then some unspecified name will be used. + * If [suffix] is not specified then ".tmp" will be used. + * If [directory] is not specified then the default temporary-file directory will be used. + * + * @return a file object corresponding to a newly-created directory. + * + * @throws IOException in case of input/output error + * @throws IllegalArgumentException if [prefix] is shorter than three symbols + */ +public fun createTempDir(prefix: String = "tmp", suffix: String? = null, directory: File? = null): File { + val dir = File.createTempFile(prefix, suffix, directory) + dir.delete() + if (dir.mkdir()) { + return dir + } else { + throw IOException("Unable to create temporary directory") + } +} + +/** + * Creates a new empty file in the specified [directory], using the given [prefix] and [suffix] to generate its name. + * + * If [prefix] is not specified then some unspecified name will be used. + * If [suffix] is not specified then ".tmp" will be used. + * If [directory] is not specified then the default temporary-file directory will be used. + * + * @return a file object corresponding to a newly-created file. + * + * @throws IOException in case of input/output error + * @throws IllegalArgumentException if [prefix] is shorter than three symbols + */ +public fun createTempFile(prefix: String = "tmp", suffix: String? = null, directory: File? = null): File { + return File.createTempFile(prefix, suffix, directory) +} + +/** + * @return this if this file is a directory, or the parent if it is a file inside a directory + * + * Note: this property looks strange + */ +public val File.directory: File + get() = if (isDirectory()) this else parent!! + +/** + * @return parent of this abstract path name, or null if it has no parent + */ +public val File.parent: File? + get() = getParentFile() + +/** + * @return the canonical path of this file. + */ +public val File.canonicalPath: String + get() = getCanonicalPath() + +/** + * @return the file name or "" for an empty name + */ +public val File.name: String + get() = getName() + +/** + * @return the file path or "" for an empty name + */ +public val File.path: String + get() = getPath() + +/** + * @return the extension of this file (not including the dot), or an empty string if it doesn't have one. + */ +public val File.extension: String + get() { + return name.substringAfterLast('.', "") + } + +/** + * Replaces all separators in the string used to separate directories with system ones and returns the resulting string. + * + * @return the pathname with system separators + */ +public fun String.separatorsToSystem(): String { + val otherSep = if (File.separator == "/") "\\" else "/" + return replace(otherSep, File.separator) +} + +/** + * Replaces all path separators in the string with system ones and returns the resulting string. + * + * @return the pathname with system separators + */ +public fun String.pathSeparatorsToSystem(): String { + val otherSep = if (File.pathSeparator == ":") ";" else ":" + return replace(otherSep, File.pathSeparator) +} + +/** + * Replaces path and directories separators with corresponding system ones and returns the resulting string. + * + * @return the pathname with system separators + */ +public fun String.allSeparatorsToSystem(): String { + return separatorsToSystem().pathSeparatorsToSystem() +} + +/** + * Returns a pathname of this file with all path separators replaced with File.pathSeparator + * + * @return the pathname with system separators + */ +public fun File.separatorsToSystem(): String { + return toString().separatorsToSystem() +} + +/** + * @return file's name without an extension. + */ +public val File.nameWithoutExtension: String + get() = name.substringBeforeLast(".") + +/** + * Calculates the relative path for this file from [base] file. + * Note that the [base] file is treated as a directory. + * If this file matches the [base] file, then an empty string will be returned. + * + * @return relative path from [base] to this + * + * @throws IllegalArgumentException if child and parent have different roots. + */ +public fun File.relativeTo(base: File): String { + // Check roots + if (root != base.root) + throw IllegalArgumentException("this and base files have different roots") + var it = iterator() + var baseIt = base.iterator() + var same = true + var sameComponents = 0 + var baseComponents = 0 + // Determine number of same components for this and base + // and total number of components for base + while (baseIt.hasNext()) { + baseComponents++ + val baseName = baseIt.next() + if (same && it.hasNext()) { + if(it.next() == baseName) { + sameComponents++ + } else { + same = false + } + } else { + same = false + } + } + // Add all .. + var res = "" + for (i in sameComponents+1..baseComponents-1) + res += (".." + File.separator) + val components = nameCount + // If .. is the last element, no separator should present + if (baseComponents > sameComponents) { + res += if (sameComponents < components) ".." + File.separator else ".." + } + // Add remaining this components + if (sameComponents < components-1) + res += (subPath(sameComponents, components-1).toString() + File.separator) + // The last one should be without separator + if (sameComponents < components) + res += subPath(components-1, components).toString() + return res +} + +deprecated("Use relativeTo() function instead") +public fun File.relativePath(descendant: File): String { + val prefix = directory.canonicalPath + val answer = descendant.canonicalPath + return if (answer.startsWith(prefix)) { + val prefixSize = prefix.length() + if (answer.length() > prefixSize) { + answer.substring(prefixSize + 1) + } else "" + } else { + answer + } +} + +/** + * Copies this file to the given output [dst], returning the number of bytes copied. + * + * If some directories on a way to the [dst] are missing, then they will be created. + * If the [dst] file already exists, then this function will fail unless [overwrite] argument is set to true and + * the [dst] file is not a non-empty directory. + * + * Note: this function fails if you call it on a directory. + * If you want to copy directories, use 'copyRecursively' function instead. + * + * @param overwrite true if destination overwrite is allowed + * @param bufferSize the buffer size to use when copying. + * @return the number of bytes copied + * @throws NoSuchFileException if the source file doesn't exist + * @throws FileAlreadyExistsException if the destination file already exists and 'rewrite' argument is set to false + * @throws IOException if any errors occur while copying + */ +public fun File.copyTo(dst: File, overwrite: Boolean = false, bufferSize: Int = defaultBufferSize): Long { + if (!exists()) { + throw NoSuchFileException(file = this, reason = "The source file doesn't exist") + } else if (isDirectory()) { + throw IllegalArgumentException("Use copyRecursively to copy a directory") + } else if (dst.exists()) { + if (!overwrite) { + throw FileAlreadyExistsException(file = this, + other = dst, + reason = "The destination file already exists") + } else if (dst.isDirectory() && dst.listFiles().any()) { + // In this case file should be copied *into* this directory, + // no matter whether it is empty or not + return copyTo(dst.resolve(name), overwrite, bufferSize) + } + } + dst.getParentFile().mkdirs() + dst.delete() + val input = FileInputStream(this) + return input.use { + val output = FileOutputStream(dst) + output.use { + input.copyTo(output, bufferSize) + } + } +} + +/** + * Enum that can be used to specify behaviour of the `copyRecursively()` function + * in exceptional conditions. + */ +public enum class OnErrorAction { + /** Skip this file and go to the next. */ + SKIP + + /** Terminate the evaluation of the function. */ + TERMINATE +} + +/** + * Copies this file with all its children to the specified destination [dst] path. + * If some directories on the way to the destination are missing, then they will be created. + * + * If any errors occur during the copying, then further actions will depend on the result of the call + * to `onError(File, IOException)` function, that will be called with arguments, + * specifying the file that caused the error and the exception itself. + * By default this function rethrows exceptions. + * Exceptions that can be passed to the `onError` function: + * NoSuchFileException - if there was an attempt to copy a non-existent file + * FileAlreadyExistsException - if there is a conflict + * AccessDeniedException - if there was an attempt to open a directory that didn't succeed. + * IOException - if some problems occur when copying. + * + * @return false if the copying was terminated, true otherwise. + * + * Note that if this function fails, then partial copying may have taken place. + */ +public fun File.copyRecursively(dst: File, + onError: (File, IOException) -> OnErrorAction = + {(file: File, e: IOException) -> throw e } + ): Boolean { + fun copy(src: File): OnErrorAction? { + if (!src.exists()) { + return onError(this, NoSuchFileException(file = this, reason = "The source file doesn't exist")) + } + val relPath = src.relativeTo(this@copyRecursively) + val dstFile = File(dst, relPath) + if (dstFile.exists() && !(src.isDirectory() && dstFile.isDirectory())) { + return onError(dstFile, FileAlreadyExistsException(file = src, + other = dstFile, + reason = "The destination file already exists")) + } + try { + if (src.isDirectory()) { + dstFile.mkdirs() + val children = src.listFiles() + if (children == null) { + return onError(src, AccessDeniedException(file = src, + reason = "Cannot list files in a directory")) + } + for (child in children) { + val result = copy(child) + if (result == OnErrorAction.TERMINATE) { + return result + } + } + } else { + if (src.copyTo(dstFile, true) != src.length()) { + return onError(src, IOException("src.length() != dst.length()")) + } + } + } catch (e: IOException) { + return onError(src, e) + } + return null + } + + val result = copy(this) + + return result != OnErrorAction.TERMINATE +} + +/** + * Delete this file with all its children. + * + * @return true if the file or directory is successfully deleted, false otherwise. + * + * Note that if this operation fails then partial deletion may have taken place. + */ +public fun File.deleteRecursively(): Boolean { + if (isDirectory()) { + listFiles()?.forEach { it.deleteRecursively() } + } + return delete() +} + +/** + * @return an array of files and directories in the directory that match the specified [filter] + * or null if this file does not denote a directory. + */ +public fun File.listFiles(filter: (file: File) -> Boolean): Array? = listFiles( + object : FileFilter { + override fun accept(file: File) = filter(file) + } + ) + +/** + * @return an iterator to go through pathname components, e.g. + * /foo/bar/gav has components foo, bar and gav + */ +public fun File.iterator(): Iterator = FileIterator(this) + +/** + * Determines whether this file belongs to the same root as [o] + * and starts with all components of [o] in the same order. + * So if [o] has N components, first N components of `this` must be the same as in [o]. + * For relative [o], `this` can belong to any root. + * + * @return true if this path starts with [o] path, false otherwise + */ +public fun File.startsWith(o: File): Boolean { + val it = FileIterator(this) + val otherIt = FileIterator(o) + // Roots must be same OR other path can have no root + if (it.root != otherIt.root && otherIt.root != "") + return false + // Other path is empty + if (!otherIt.hasNext()) + return true + val count = this.nameCount + val otherCount = o.nameCount + // This path is shorted than the other + if (count < otherCount) + return false + // Compare first elements until other ends + while (otherIt.hasNext()) { + if (it.next() != otherIt.next()) + return false + } + return true +} + +/** + * Determines whether this file belongs to the same root as [o] + * and starts with all components of [o] in the same order. + * So if [o] has N components, first N components of `this` must be the same as in [o]. + * For relative [o], `this` can belong to any root. + * + * @return true if this path starts with [o] path, false otherwise + */ +public fun File.startsWith(o: String): Boolean = startsWith(File(o)) + +/** + * Determines whether this file belongs to the same root as [o] + * and ends with all components of [o] in the same order. + * So if [o] has N components, last N components of `this` must be the same as in [o]. + * For relative [o], `this` can belong to any root. + * + * @return true if this path ends with [o] path, false otherwise + */ +public fun File.endsWith(o: File): Boolean { + val it = FileIterator(this) + val otherIt = FileIterator(o) + // Roots must be same OR other path can have no root + if (it.root != otherIt.root && otherIt.root != "") + return false + // Other path is empty + if (!otherIt.hasNext()) + return true + var theirs = otherIt.next() + val count = this.nameCount + val otherCount = o.nameCount + // This path is shorted than the other + if (count < otherCount) + return false + var ours = it.next() + // Move forward to have same number of elements remaining + for (i in 0..count-otherCount-1) { + ours = it.next() + } + // Check all next elements are same, until the end + while (ours == theirs && it.hasNext() && otherIt.hasNext()) { + ours = it.next() + theirs = otherIt.next() + } + if (ours != theirs || it.hasNext() || otherIt.hasNext()) + return false + return true +} + +/** + * Determines whether this file belongs to the same root as [o] + * and ends with all components of [o] in the same order. + * So if [o] has N components, last N components of `this` must be the same as in [o]. + * For relative [o], `this` can belong to any root. + * + * @return true if this path ends with [o] path, false otherwise + */ +public fun File.endsWith(o: String): Boolean = endsWith(File(o)) + +/** + * Removes all . and resolves all possible .. in this file name. + * For instance, File("/foo/./bar/gav/../baaz").normalize is File("/foo/bar/baaz") + * + * @return normalized pathname with . and possibly .. removed + */ +public fun File.normalize(): File { + val rootName = rootName + val list: MutableList = ArrayList() + for (elem in this) + if (elem.toString() != ".") + list.add(elem.toString()) + var first = 0 + var dots = list.subList(first, list.size()).indexOf("..") + while (dots != -1) { + if (dots > 0) { + list.remove(dots+first) + list.remove(dots+first-1) + } else { + first++ + } + dots = list.subList(first, list.size()).indexOf("..") + } + var res = rootName + var addSeparator = rootName != "" && !rootName.endsWith(File.separatorChar) + for (elem in list) { + if (addSeparator) + res += File.separatorChar + res += elem + addSeparator = true + } + return File(res) +} + +/** + * Adds relative [o] to this, considering this as a directory. + * If [o] has a root, [o] is returned back. + * For instance, File("/foo/bar").resolve(File("gav")) is File("/foo/bar/gav"). + * This function is complementary with (File.relativeTo), + * so f.resolve(g.relativeTo(f)) == g should be always true except for different roots case. + * + * @return concatenated this and [o] paths, or just [o] if it's absolute + */ +public fun File.resolve(o: File): File { + if (o.root != null) + return o + val ourName = toString() + if (ourName.endsWith(File.separatorChar)) + return File(ourName + o) + else + return File(ourName + File.separatorChar + o) +} + +/** + * Adds relative [o] to this, considering this as a directory. + * If [o] has a root, [o] is returned back. + * For instance, File("/foo/bar").resolve("gav") is File("/foo/bar/gav") + * + * @return concatenated this and [o] paths, or just [o] if it's absolute + */ +public fun File.resolve(o: String): File = resolve(File(o)) + +/** + * Adds relative [o] to this parent directory. + * If [o] has a root or this has no parent directory, [o] is returned back. + * For instance, File("/foo/bar").resolveSibling(File("gav")) is File("/foo/gav") + * + * @return concatenated this.parent and [o] paths, or just [o] if it's absolute or this has no parent + */ +public fun File.resolveSibling(o: File): File { + val parentFile = parent + if (parentFile != null) + return parentFile.resolve(o) + else + return o +} + +/** + * Adds relative [o] to this parent directory. + * If [o] has a root or this has no parent directory, [o] is returned back + * For instance, File("/foo/bar").resolveSibling("gav") is File("/foo/gav") + * + * @return concatenated this.parent and [o] paths, or just [o] if it's absolute or this has no parent + */ +public fun File.resolveSibling(o: String): File = resolveSibling(File(o)) diff --git a/libraries/stdlib/test/io/Files.kt b/libraries/stdlib/test/io/Files.kt index 1d8289874ac..5b1c09cdb78 100644 --- a/libraries/stdlib/test/io/Files.kt +++ b/libraries/stdlib/test/io/Files.kt @@ -1,52 +1,582 @@ package test.io import org.junit.Test as test -import java.io.File import kotlin.test.assertEquals +import java.io.File +import java.io.IOException +import java.io.FileNotFoundException +import java.util.NoSuchElementException +import java.util.HashSet +import java.util.ArrayList +import kotlin.io.files.walkBottomUp +import kotlin.io.files.walkTopDown +import kotlin.test.assertFalse +import kotlin.test.assertNull +import kotlin.test.assertTrue class FilesTest { - test fun listFilesWithFilter() { - val dir = File.createTempFile("temp", System.nanoTime().toString()) - dir.delete() - dir.mkdir() - File.createTempFile("temp1", ".kt", dir) - File.createTempFile("temp2", ".java", dir) - File.createTempFile("temp3", ".kt", dir) + test fun testPath() { + val fileSuf = System.currentTimeMillis().toString() + val file1 = createTempFile("temp", fileSuf) + assertTrue(file1.path.endsWith(fileSuf), file1.path) + } + + test fun testCreateTempDir() { + val dirSuf = System.currentTimeMillis().toString() + val dir1 = createTempDir("temp", dirSuf) + assert(dir1.exists() && dir1.isDirectory() && dir1.name.startsWith("temp") && dir1.name.endsWith(dirSuf)) + try { + createTempDir("a") + assert(false) + } catch(e: IllegalArgumentException) {} + + val dir2 = createTempDir("temp") + assert(dir2.exists() && dir2.isDirectory() && dir2.name.endsWith(".tmp")) + + val dir3 = createTempDir() + assert(dir3.exists() && dir3.isDirectory()) + + dir1.delete() + dir2.delete() + dir3.delete() + } + + test fun testCreateTempFile() { + val fileSuf = System.currentTimeMillis().toString() + val file1 = createTempFile("temp", fileSuf) + assert(file1.exists() && file1.name.startsWith("temp") && file1.name.endsWith(fileSuf)) + try { + createTempFile("a") + assert(false) + } catch(e: IllegalArgumentException) {} + val file2 = createTempFile("temp") + assert(file2.exists() && file2.name.endsWith(".tmp")) + + val file3 = createTempFile() + assert(file3.exists()) + + file1.delete() + file2.delete() + file3.delete() + } + + class Walks { + fun createTestFiles(): File { + val basedir = createTempDir() + File(basedir, "1").mkdir() + File(basedir, "1/2".separatorsToSystem()).mkdir() + File(basedir, "1/3".separatorsToSystem()).mkdir() + File(basedir, "1/3/4.txt".separatorsToSystem()).createNewFile() + File(basedir, "1/3/5.txt".separatorsToSystem()).createNewFile() + File(basedir, "6").mkdir() + File(basedir, "7.txt").createNewFile() + File(basedir, "8").mkdir() + File(basedir, "8/9.txt".separatorsToSystem()).createNewFile() + return basedir + } + + test fun withSimple() { + val basedir = createTestFiles() + try { + val referenceNames = + listOf("", "1", "1/2", "1/3", "1/3/4.txt", "1/3/5.txt", "6", "7.txt", "8", "8/9.txt").map( + {it -> it.separatorsToSystem()}).toHashSet() + val namesTopDown = HashSet() + for (file in basedir.walkTopDown()) { + val name = file.relativeTo(basedir) + assertFalse(namesTopDown.contains(name), "$name is visited twice") + namesTopDown.add(name) + } + assertEquals(referenceNames, namesTopDown) + val namesBottomUp = HashSet() + for (file in basedir.walkBottomUp()) { + val name = file.relativeTo(basedir) + assertFalse(namesBottomUp.contains(name), "$name is visited twice") + namesBottomUp.add(name) + } + assertEquals(referenceNames, namesBottomUp) + } finally { + basedir.deleteRecursively() + } + } + + test fun withEnterLeave() { + val basedir = createTestFiles() + try { + val referenceNames = + listOf("", "1", "1/2", "1/3", "6", "8").map( + {it -> it.separatorsToSystem()}).toHashSet() + val namesTopDownEnter = HashSet() + val namesTopDownLeave = HashSet() + val namesTopDown = HashSet() + fun enter(file: File) { + val name = file.relativeTo(basedir) + assertFalse(namesTopDownEnter.contains(name), "$name is entered twice") + namesTopDownEnter.add(name) + assertFalse(namesTopDownLeave.contains(name), "$name is left before entrance") + } + fun leave(file: File) { + val name = file.relativeTo(basedir) + assertFalse(namesTopDownLeave.contains(name), "$name is left twice") + namesTopDownLeave.add(name) + assertTrue(namesTopDownEnter.contains(name), "$name is left before entrance") + } + fun visit(file: File) { + val name = file.relativeTo(basedir) + if (file.isDirectory()) { + assertTrue(namesTopDownEnter.contains(name), "$name is visited before entrance") + namesTopDown.add(name) + assertFalse(namesTopDownLeave.contains(name), "$name is visited after leaving") + } + if (file == basedir) + return + val parent = file.getParentFile() + if (parent != null) { + val parentName = parent.relativeTo(basedir) + assertTrue(namesTopDownEnter.contains(parentName), + "$name is visited before entering its parent $parentName") + assertFalse(namesTopDownLeave.contains(parentName), + "$name is visited after leaving its parent $parentName") + } + } + for (file in basedir.walkTopDown().enter(::enter).leave(::leave)) { + visit(file) + } + assertEquals(referenceNames, namesTopDownEnter) + assertEquals(referenceNames, namesTopDownLeave) + namesTopDownEnter.clear() + namesTopDownLeave.clear() + namesTopDown.clear() + for (file in basedir.walkBottomUp().enter(::enter).leave(::leave)) { + visit(file) + } + assertEquals(referenceNames, namesTopDownEnter) + assertEquals(referenceNames, namesTopDownLeave) + } finally { + basedir.deleteRecursively() + } + } + + test fun withFilterAndMap() { + val basedir = createTestFiles() + try { + val referenceNames = + listOf("", "1", "1/2", "1/3", "6", "8").map( + {it -> it.separatorsToSystem()}).toHashSet() + assertEquals(referenceNames, basedir.walkTopDown().filter{ it.isDirectory() }.map{ + it.relativeTo(basedir) }.toHashSet()) + } finally { + basedir.deleteRecursively() + } + + } + + test fun withDeleteTxtTopDown() { + val basedir = createTestFiles() + try { + val referenceNames = + listOf("", "1", "1/2", "1/3", "6", "8").map( + {it -> it.separatorsToSystem()}).toHashSet() + val namesTopDown = HashSet() + fun enter(file: File) { + assertTrue(file.isDirectory()) + for (child in file.listFiles()) { + if (child.name.endsWith("txt")) + child.delete() + } + } + for (file in basedir.walkTopDown().enter(::enter)) { + val name = file.relativeTo(basedir) + assertFalse(namesTopDown.contains(name), "$name is visited twice") + namesTopDown.add(name) + } + assertEquals(referenceNames, namesTopDown) + } finally { + basedir.deleteRecursively() + } + } + + test fun withDeleteTxtBottomUp() { + val basedir = createTestFiles() + try { + val referenceNames = + listOf("", "1", "1/2", "1/3", "6", "8").map( + {it -> it.separatorsToSystem()}).toHashSet() + val namesTopDown = HashSet() + fun enter(file: File) { + assertTrue(file.isDirectory()) + for (child in file.listFiles()) { + if (child.name.endsWith("txt")) + child.delete() + } + } + for (file in basedir.walkBottomUp().enter(::enter)) { + val name = file.relativeTo(basedir) + assertFalse(namesTopDown.contains(name), "$name is visited twice") + namesTopDown.add(name) + } + assertEquals(referenceNames, namesTopDown) + } finally { + basedir.deleteRecursively() + } + } + + test fun withFilter() { + val basedir = createTestFiles() + try { + fun filter(file: File): Boolean { + // Everything ended with 3 is filtered + return (!file.name.endsWith("3")); + } + val referenceNames = + listOf("", "1", "1/2", "6", "7.txt", "8", "8/9.txt").map( + {it -> it.separatorsToSystem()}).toHashSet() + val namesTopDown = HashSet() + for (file in basedir.walkTopDown().filter(::filter)) { + val name = file.relativeTo(basedir) + assertFalse(namesTopDown.contains(name), "$name is visited twice") + namesTopDown.add(name) + } + assertEquals(referenceNames, namesTopDown) + val namesBottomUp = HashSet() + for (file in basedir.walkBottomUp().filter(::filter)) { + val name = file.relativeTo(basedir) + assertFalse(namesBottomUp.contains(name), "$name is visited twice") + namesBottomUp.add(name) + } + assertEquals(referenceNames, namesBottomUp) + } finally { + basedir.deleteRecursively() + } + } + + test fun withTotalFilter() { + val basedir = createTestFiles() + try { + // Everything is filtered + fun filter(file: File) = false + val referenceNames: Set = setOf() + val namesTopDown = HashSet() + for (file in basedir.walkTopDown().filter(::filter)) { + val name = file.relativeTo(basedir) + assertFalse(namesTopDown.contains(name), "$name is visited twice") + namesTopDown.add(name) + } + assertEquals(referenceNames, namesTopDown) + val namesBottomUp = HashSet() + for (file in basedir.walkBottomUp().filter(::filter)) { + val name = file.relativeTo(basedir) + assertFalse(namesBottomUp.contains(name), "$name is visited twice") + namesBottomUp.add(name) + } + assertEquals(referenceNames, namesBottomUp) + } finally { + basedir.deleteRecursively() + } + } + + test fun withForEach() { + val basedir = createTestFiles() + try { + var i = 0 + basedir.walkTopDown().forEach { it -> i++ } + assertEquals(10, i); + i = 0 + basedir.walkBottomUp().forEach { it -> i++ } + assertEquals(10, i); + } finally { + basedir.deleteRecursively() + } + } + + test fun withCount() { + val basedir = createTestFiles() + try { + assertEquals(10, basedir.walkTopDown().count()); + assertEquals(10, basedir.walkBottomUp().count()); + } finally { + basedir.deleteRecursively() + } + } + + test fun withReduce() { + val basedir = createTestFiles() + try { + val res = basedir.walkTopDown().reduce { (a, b) -> if (a.canonicalPath > b.canonicalPath) a else b } + assertTrue(res.endsWith("9.txt"), "Expected end with 9.txt actual: ${res.name}") + } finally { + basedir.deleteRecursively() + } + } + + test fun withVisitorAndDepth() { + val basedir = createTestFiles() + try { + val files = HashSet() + val dirs = HashSet() + val failed = HashSet() + val stack = ArrayList() + fun beforeVisitDirectory(dir: File) { + stack.add(dir) + dirs.add(dir.relativeTo(basedir)) + } + fun afterVisitDirectory(dir: File) { + assertEquals(stack.last(), dir) + stack.remove(stack.lastIndex) + } + fun visitFile(file: File) { + assert(stack.last().listFiles().contains(file), file) + files.add(file.relativeTo(basedir)) + } + fun visitDirectoryFailed(dir: File, e: IOException) { + assertEquals(stack.last(), dir) + stack.remove(stack.lastIndex) + failed.add(dir.name) + } + basedir.walkTopDown().enter(::beforeVisitDirectory).leave(::afterVisitDirectory). + fail(::visitDirectoryFailed).forEach{ it -> if (!it.isDirectory()) visitFile(it) } + assert(stack.isEmpty()) + val sep = File.separator + for (fileName in array("", "1", "1${sep}2", "1${sep}3", "6", "8")) { + assert(dirs.contains(fileName), fileName) + } + for (fileName in array("1${sep}3${sep}4.txt", "1${sep}3${sep}4.txt", "7.txt", "8${sep}9.txt")) { + assert(files.contains(fileName), fileName) + } + + //limit maxDepth + files.clear() + dirs.clear() + basedir.walkTopDown().enter(::beforeVisitDirectory).leave(::afterVisitDirectory).maxDepth(1). + forEach{ it -> if (it != basedir) visitFile(it) } + assert(stack.isEmpty()) + assert(dirs.size() == 1 && dirs.contains(""), dirs.size()) + for (file in array("1", "6", "7.txt", "8")) { + assert(files.contains(file), file) + } + + //restrict access + if (File(basedir, "1").setReadable(false)) { + try { + files.clear() + dirs.clear() + basedir.walkTopDown().enter(::beforeVisitDirectory).leave(::afterVisitDirectory). + fail(::visitDirectoryFailed).forEach{ it -> if (!it.isDirectory()) visitFile(it) } + assert(stack.isEmpty()) + assert(failed.size() == 1 && failed.contains("1"), failed.size()) + assert(dirs.size() == 4, dirs.size()) + for (dir in array("", "1", "6", "8")) { + assert(dirs.contains(dir), dir) + } + assert(files.size() == 2, files.size()) + for (file in array("7.txt", "8${sep}9.txt")) { + assert(files.contains(file), file) + } + } finally { + File(basedir, "1").setReadable(true) + } + } else { + System.err.println("cannot restrict access") + } + } finally { + basedir.deleteRecursively() + } + } + + test fun topDown() { + val basedir = createTestFiles() + try { + val visited = HashSet() + val block: (File) -> Unit = { + assert(!visited.contains(it), it) + assert(it == basedir && visited.isEmpty() || visited.contains(it.getParentFile()), it) + visited.add(it) + } + basedir.walkTopDown().forEach( block ) + assert(visited.size() == 10, visited.size()) + + } finally { + basedir.deleteRecursively() + } + } + + test fun restrictedAccess() { + val basedir = createTestFiles() + val restricted = File(basedir, "1") + try { + if (restricted.setReadable(false)) { + val visited = HashSet() + val block: (File) -> Unit = { + assert(!visited.contains(it), it) + assert(it == basedir && visited.isEmpty() || visited.contains(it.getParentFile()), it) + visited.add(it) + } + basedir.walkTopDown().forEach( block ) + assert(visited.size() == 6, visited.size()) + } + } finally { + restricted.setReadable(true) + basedir.deleteRecursively() + } + } + + test fun backup() { + var count = 0 + fun makeBackup(file: File) { + count++ + val bakFile = File(file.toString() + ".bak") + file.copyTo(bakFile) + } + + val basedir1 = createTestFiles() + try { + basedir1.walkTopDown().forEach { + if (it.isFile()) { + makeBackup(it) + } + } + assert(count == 4) + } finally { + basedir1.deleteRecursively() + } + + count = 0 + val basedir2 = createTestFiles() + try { + basedir2.walkTopDown().forEach { + if (it.isFile()) { + makeBackup(it) + } + } + assert(count == 4) + } finally { + basedir2.deleteRecursively() + } + } + + test fun find() { + val basedir = createTestFiles() + try { + File(basedir, "8/4.txt".separatorsToSystem()).createNewFile() + var count = 0 + basedir.walkTopDown().takeWhile{ it -> count == 0 }.forEach { + if (it.name == "4.txt") { + count++ + } + } + assert(count == 1) + } finally { + basedir.deleteRecursively() + } + } + + test fun findGits() { + val basedir = createTestFiles() + try { + File(basedir, "1/3/.git").mkdir() + File(basedir, "1/2/.git").mkdir() + File(basedir, "6/.git").mkdir() + val found = HashSet() + for (file in basedir.walkTopDown()) { + if (file.name == ".git") { + found.add(file.getParentFile()) + } + } + assert(found.size() == 3) + } finally { + basedir.deleteRecursively() + } + } + + test fun streamFileTree() { + val dir = createTempDir() + try { + val subDir1 = createTempDir(prefix = "d1_", directory = dir) + val subDir2 = createTempDir(prefix = "d2_", directory = dir) + createTempDir(prefix = "d1_", directory = subDir1) + createTempFile(prefix = "f1_", directory = subDir1) + createTempDir(prefix = "d1_", directory = subDir2) + assertEquals(6, dir.walkTopDown().count()) + } finally { + dir.deleteRecursively() + } + dir.mkdir() + try { + val it = dir.walkTopDown().iterator() + it.next() + it.next() + assert(false) + } catch(e: NoSuchElementException) { + } finally { + dir.delete() + } + try { + dir.walkTopDown() + assert(false) + } catch(e: FileNotFoundException) {} + } + } + + test fun listFilesWithFilter() { + val dir = createTempDir("temp") + + createTempFile("temp1", ".kt", dir) + createTempFile("temp2", ".java", dir) + createTempFile("temp3", ".kt", dir) val result = dir.listFiles { it.getName().endsWith(".kt") } assertEquals(2, result!!.size()) } - test fun recurse() { - val dir = File.createTempFile("temp", System.nanoTime().toString()) - dir.delete() + test fun relativeToTest() { + val file1 = File("/foo/bar/baz") + val file2 = File("/foo/baa/ghoo") + assertEquals("../../bar/baz".separatorsToSystem(), file1.relativeTo(file2)) + val file3 = File("/foo/bar") + assertEquals("baz", file1.relativeTo(file3)) + assertEquals("..", file3.relativeTo(file1)) + val file4 = File("/foo/bar/") + assertEquals("baz", file1.relativeTo(file4)) + assertEquals("..", file4.relativeTo(file1)) + assertEquals("", file3.relativeTo(file4)) + assertEquals("", file4.relativeTo(file3)) + val file5 = File("/foo/baran") + assertEquals("../bar".separatorsToSystem(), file3.relativeTo(file5)) + assertEquals("../baran".separatorsToSystem(), file5.relativeTo(file3)) + assertEquals("../bar".separatorsToSystem(), file4.relativeTo(file5)) + assertEquals("../baran".separatorsToSystem(), file5.relativeTo(file4)) + val file6 = File("C:\\Users\\Me") + val file7 = File("C:\\Users\\Me\\Documents") + assertEquals("..", file6.relativeTo(file7)) + assertEquals("Documents", file7.relativeTo(file6)) + val file8 = File("//my.host/home/user/documents/vip") + val file9 = File("//my.host/home/other/images/nice") + assertEquals("../../../user/documents/vip".separatorsToSystem(), file8.relativeTo(file9)) + assertEquals("../../../other/images/nice".separatorsToSystem(), file9.relativeTo(file8)) + val file10 = File("foo/bar") + val file11 = File("foo") + assertEquals("bar", file10.relativeTo(file11)) + assertEquals("..", file11.relativeTo(file10)) + } - var totalFiles = 0 - dir.recurse { totalFiles++ } - assertEquals(1, totalFiles) + test fun relativeTo() { + assertEquals("kotlin", File("src/kotlin".separatorsToSystem()).relativeTo(File("src"))) + assertEquals("", File("dir").relativeTo(File("dir"))) + assertEquals("..", File("dir").relativeTo(File("dir/subdir".separatorsToSystem()))) + assertEquals("../../test".separatorsToSystem(), File("test").relativeTo(File("dir/dir".separatorsToSystem()))) - dir.mkdir() - - File.createTempFile("temp", "1.kt", dir) - File.createTempFile("temp", "2.java", dir) - - val subdir = File(dir, "subdir") - subdir.mkdir() - - File(subdir, "3.txt").createNewFile() - - totalFiles = 0 - dir.recurse { totalFiles++ } - - assertEquals(5, totalFiles) - - if (subdir.setReadable(false)) { - // On Windows, we can't make directory not readable, and setReadable() will return false - - totalFiles = 0 - dir.recurse { totalFiles++ } - assertEquals(4, totalFiles) + // This test operates correctly only at Windows PCs with C & D drives + val file1 = File("C:/dir1".separatorsToSystem()) + val file2 = File("D:/dir2".separatorsToSystem()) + try { + val winRelPath = file1.relativeTo(file2) + assert(file1.canonicalPath.charAt(0) == '/') + assertEquals("../../C:/dir1", winRelPath) + } catch (e: IllegalArgumentException) { + assert(Character.isLetter(file1.canonicalPath.charAt(0))) + } catch (e: IOException) { + // The device is not ready (D) ==> DO NOTHING } } @@ -59,4 +589,249 @@ class FilesTest { assertEquals("", file1.relativePath(file1)) assertEquals(file3.canonicalPath, file1.relativePath(file3)) } + + private fun checkFileElements(f: File, root: File?, elements: List) { + var i = 0 + assertEquals(root, f.root) + for (elem in f) { + assertTrue(i < elements.size()) + assertEquals(elements[i++], elem.toString()) + } + assertEquals(elements.size(), i) + } + + test fun fileIterator() { + checkFileElements(File("/foo/bar"), File("/"), listOf("foo", "bar")) + checkFileElements(File("/foo/bar/gav"), File("/"), listOf("foo", "bar", "gav")) + checkFileElements(File("/foo/bar/gav/"), File("/"), listOf("foo", "bar", "gav")) + checkFileElements(File("bar/gav"), null, listOf("bar", "gav")) + checkFileElements(File("C:\\bar\\gav"), File("C:\\"), listOf("bar", "gav")) + checkFileElements(File("C:\\"), File("C:\\"), listOf()) + checkFileElements(File("C:"), File("C:"), listOf()) + checkFileElements(File("//host.ru/home/mike"), File("//host.ru/home"), listOf("mike")) + checkFileElements(File(""), null, listOf("")) + checkFileElements(File("."), null, listOf(".")) + checkFileElements(File(".."), null, listOf("..")) + } + + test fun startsWith() { + assertTrue(File("C:\\Users\\Me\\Temp\\Game").startsWith("C:\\Users\\Me")) + assertFalse(File("C:\\Users\\Me\\Temp\\Game").startsWith("C:\\Users\\He")) + assertTrue(File("C:\\Users\\Me").startsWith("C:\\")) + } + + test fun endsWith() { + assertTrue(File("/foo/bar").endsWith("bar")) + assertTrue(File("/foo/bar").endsWith("/bar")) + assertTrue(File("/foo/bar/gav/bar").endsWith("/bar")) + assertTrue(File("/foo/bar/gav/bar").endsWith("/gav/bar")) + assertFalse(File("/foo/bar/gav").endsWith("/bar")) + assertFalse(File("foo/bar").endsWith("/bar")) + } + + test fun subPath() { + assertEquals(File("mike"), File("//my.host.net/home/mike/temp").subPath(0, 1)) + assertEquals(File("bar/gav"), File("/foo/bar/gav/hi").subPath(1, 3)) + } + + test fun normalize() { + assertEquals(File("/foo/bar/baaz"), File("/foo/./bar/gav/../baaz").normalize()) + assertEquals(File("../../bar"), File("../foo/../../bar").normalize()) + assertEquals(File("C:\\windows"), File("C:\\home\\..\\documents\\..\\windows").normalize()) + assertEquals(File("foo"), File("gav/bar/../../foo").normalize()) + } + + test fun resolve() { + assertEquals(File("/foo/bar/gav"), File("/foo/bar").resolve("gav")) + assertEquals(File("/foo/bar/gav"), File("/foo/bar/").resolve("gav")) + assertEquals(File("/gav"), File("/foo/bar").resolve("/gav")) + assertEquals(File("C:\\Users\\Me\\Documents\\important.doc"), + File("C:\\Users\\Me").resolve("Documents\\important.doc")) + } + + test fun resolveSibling() { + assertEquals(File("/foo/gav"), File("/foo/bar").resolveSibling("gav")) + assertEquals(File("/foo/gav"), File("/foo/bar/").resolveSibling("gav")) + assertEquals(File("/gav"), File("/foo/bar").resolveSibling("/gav")) + assertEquals(File("C:\\Users\\Me\\Documents\\important.doc"), + File("C:\\Users\\Me\\profile.ini").resolveSibling("Documents\\important.doc")) + } + + test fun extension() { + assertEquals("bbb", File("aaa.bbb").extension) + assertEquals("", File("aaa").extension) + assertEquals("", File("aaa.").extension) + // maybe we should think that such files have name .bbb and no extension + assertEquals("bbb", File(".bbb").extension) + assertEquals("", File("/my.dir/log").extension) + } + + test fun nameWithoutExtension() { + assertEquals("aaa", File("aaa.bbb").nameWithoutExtension) + assertEquals("aaa", File("aaa").nameWithoutExtension) + assertEquals("aaa", File("aaa.").nameWithoutExtension) + assertEquals("", File(".bbb").nameWithoutExtension) + assertEquals("log", File("/my.dir/log").nameWithoutExtension) + } + + test fun separatorsToSystem() { + var path = "/aaa/bbb/ccc" + assertEquals(path.replace("/", File.separator), File(path).separatorsToSystem()) + + path = "C:\\Program Files\\My Awesome Program" + assertEquals(path.replace("\\", File.separator), File(path).separatorsToSystem()) + + path = "/Libraries\\Java:/Libraries/Python:/Libraries/Ruby" + assertEquals(path.replace(":", File.pathSeparator), path.pathSeparatorsToSystem()) + + path = "/Libraries\\Java;/Libraries/Python;/Libraries/Ruby" + assertEquals(path.replace(";", File.pathSeparator), path.pathSeparatorsToSystem()) + + path = "/Libraries\\Java;/Libraries/Python:\\Libraries/Ruby" + assertEquals(path.replace("/", File.separator).replace("\\", File.separator) + .replace(":", File.pathSeparator).replace(";", File.pathSeparator), path.allSeparatorsToSystem()) + + assertEquals("test", "test".allSeparatorsToSystem()) + } + + test fun testCopyTo() { + val srcFile = createTempFile() + val dstFile = createTempFile() + srcFile.replaceText("Hello, World!") + try { + srcFile.copyTo(dstFile) + assert(false) + } catch (e: FileAlreadyExistsException) {} + + var len = srcFile.copyTo(dstFile, overwrite = true) + assertEquals(13L, len) + assertEquals(srcFile.readText(), dstFile.readText()) + + assert(dstFile.delete()) + len = srcFile.copyTo(dstFile) + assertEquals(13L, len) + assertEquals(srcFile.readText(), dstFile.readText()) + + assert(dstFile.delete()) + dstFile.mkdir() + val child = File(dstFile, "child") + child.createNewFile() + srcFile.copyTo(dstFile, overwrite = true) + assertEquals(13L, len) + val copy = dstFile.resolve(srcFile.name) + assertEquals(srcFile.readText(), copy.readText()) + + assert(srcFile.delete()) + assert(child.delete() && copy.delete() && dstFile.delete()) + + try { + srcFile.copyTo(dstFile) + assert(false) + } catch (e: NoSuchFileException) {} + + srcFile.mkdir() + try { + srcFile.copyTo(dstFile) + assert(false) + } catch (e: IllegalArgumentException) {} + srcFile.delete() + } + + test fun deleteRecursively() { + val dir = createTempDir() + dir.delete() + dir.mkdir() + val subDir = File(dir, "subdir"); + subDir.mkdir() + File(dir, "test1.txt").createNewFile() + File(subDir, "test2.txt").createNewFile() + + assert(dir.deleteRecursively()) + assert(!dir.exists()) + assert(!dir.deleteRecursively()) + } + + test fun copyRecursively() { + val src = createTempDir() + val dst = createTempDir() + dst.delete() + fun check() { + for (file in src.walkTopDown()) { + val dstFile = File(dst, file.relativeTo(src)) + assert(dstFile.exists()) + if (dstFile.isFile()) { + assertEquals(file.readText(), dstFile.readText()) + } + + } + } + + try { + val subDir1 = createTempDir(prefix = "d1_", directory = src) + val subDir2 = createTempDir(prefix = "d2_", directory = src) + createTempDir(prefix = "d1_", directory = subDir1) + val file1 = createTempFile(prefix = "f1_", directory = src) + val file2 = createTempFile(prefix = "f2_", directory = subDir1) + file1.replaceText("hello") + file2.replaceText("wazzup") + createTempDir(prefix = "d1_", directory = subDir2) + + assert(src.copyRecursively(dst)) + check() + + try { + src.copyRecursively(dst) + assert(false) + } catch (e: FileAlreadyExistsException) { + } + + var conflicts = 0 + src.copyRecursively(dst) { + (file: File, e: IOException) -> + if (e is FileAlreadyExistsException) { + conflicts++ + OnErrorAction.SKIP + } else { + throw e + } + } + assert(conflicts == 2) + + if (subDir1.setReadable(false)) { + try { + dst.deleteRecursively() + var caught = false + assert(src.copyRecursively(dst) { + (file: File, e: IOException) -> + if (e is AccessDeniedException) { + caught = true + OnErrorAction.SKIP + } else { + throw e + } + }) + assert(caught) + check() + } finally { + subDir1.setReadable(true) + } + } + + src.deleteRecursively() + dst.deleteRecursively() + try { + src.copyRecursively(dst) + assert(false) + } catch (e: NoSuchFileException) { + } + + assert(!src.copyRecursively(dst) { + (file: File, e: IOException) -> + OnErrorAction.TERMINATE + }) + } finally { + src.deleteRecursively() + dst.deleteRecursively() + } + } } diff --git a/libraries/stdlib/test/io/IOStreams.kt b/libraries/stdlib/test/io/IOStreams.kt new file mode 100644 index 00000000000..7cd552af3f0 --- /dev/null +++ b/libraries/stdlib/test/io/IOStreams.kt @@ -0,0 +1,29 @@ +package test.io + +import org.junit.Test as test +import java.io.Writer +import java.io.BufferedReader +import java.io.File +import kotlinhack.test.assertEquals + +class IOStreamsTest { + test fun testGetStreamOfFile() { + val tmpFile = createTempFile() + var writer: Writer? = null + try { + writer = tmpFile.outputStream().writer() + writer!!.write("Hello, World!") + } finally { + writer?.close() + } + var act: String? + var reader: BufferedReader? = null + try { + reader = tmpFile.inputStream().reader().buffered() + act = reader!!.readLine() + } finally { + reader?.close() + } + assertEquals("Hello, World!", act) + } +} diff --git a/libraries/stdlib/test/io/ReadWrite.kt b/libraries/stdlib/test/io/ReadWrite.kt index f171bf880fe..be5f2595ba4 100644 --- a/libraries/stdlib/test/io/ReadWrite.kt +++ b/libraries/stdlib/test/io/ReadWrite.kt @@ -5,14 +5,17 @@ import java.io.File import kotlin.test.assertEquals import java.io.Reader import java.io.StringReader +import java.net.URL import java.util.ArrayList +import kotlin.test.assertFalse +import kotlin.test.assertTrue fun sample(): Reader = StringReader("Hello\nWorld"); class ReadWriteTest { test fun testAppendText() { val file = File.createTempFile("temp", System.nanoTime().toString()) - file.writeText("Hello\n") + file.replaceText("Hello\n") file.appendText("World") assertEquals("Hello\nWorld", file.readText()) @@ -60,29 +63,42 @@ class ReadWriteTest { test fun file() { val file = File.createTempFile("temp", System.nanoTime().toString()) + val writer = file.outputStream().writer().buffered() - file.writeText("Hello\nWorld") + writer.write("Hello") + writer.newLine() + writer.write("World") + writer.close() + + //file.replaceText("Hello\nWorld") + file.forEachBlock{ (arr: ByteArray, size: Int) -> + assertTrue(size >= 11 && size <= 12, size.toString()) + assertTrue(arr.contains('W'.toByte())) + } val list = ArrayList() file.forEachLine{ list.add(it) } assertEquals(arrayListOf("Hello", "World"), list) + val text = file.inputStream().reader().readText() + assertTrue(text.contains("Hello")) + assertTrue(text.contains("World")) - file.writeText("") + file.replaceText("") var c = 0 file.forEachLine { c++ } assertEquals(0, c) - file.writeText(" ") + file.replaceText(" ") file.forEachLine { c++ } assertEquals(1, c) - file.writeText(" \n") + file.replaceText(" \n") c = 0 file.forEachLine { c++ } assertEquals(1, c) - file.writeText(" \n ") + file.replaceText(" \n ") c = 0 file.forEachLine { c++ } assertEquals(2, c) @@ -146,4 +162,10 @@ class ReadWriteTest { assertEquals(arrayListOf("Hello", "World"), list) } + + test fun testURL() { + val url = URL("http://kotlinlang.org") + val text = url.readText() + assertFalse(text.isEmpty()) + } }