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
This commit is contained in:
committed by
Mikhail Glukhikh
parent
dae42f7c76
commit
f560677b15
@@ -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)
|
||||
|
||||
@@ -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<FileInputStream, Long>{
|
||||
val output = FileOutputStream(file)
|
||||
output.use<FileOutputStream, Long>{
|
||||
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<File>? = listFiles(
|
||||
object : FileFilter {
|
||||
override fun accept(file: File) = filter(file)
|
||||
}
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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<String> = 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<String> {
|
||||
val result = ArrayList<String>()
|
||||
@@ -140,16 +216,19 @@ public fun File.readLines(charset: Charset = Charsets.UTF_8): List<String> {
|
||||
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 <T> Reader.useLines(block: (Sequence<String>) -> 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<String>
|
||||
* 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<InputStream, ByteArray>{ 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 : Closeable, R> T.use(block: (T) -> R): R {
|
||||
var closed = false
|
||||
|
||||
@@ -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<File> {
|
||||
|
||||
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)
|
||||
|
||||
}
|
||||
@@ -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<File> {
|
||||
|
||||
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<File>? = 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<File>? = 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<DirectoryState>()
|
||||
|
||||
// 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<File> {
|
||||
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<File> {
|
||||
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)
|
||||
|
||||
@@ -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<FileInputStream, Long> {
|
||||
val output = FileOutputStream(dst)
|
||||
output.use<FileOutputStream, Long> {
|
||||
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<File>? = 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<File> = 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<String> = 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))
|
||||
@@ -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<String>()
|
||||
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<String>()
|
||||
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<String>()
|
||||
val namesTopDownLeave = HashSet<String>()
|
||||
val namesTopDown = HashSet<String>()
|
||||
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<String>()
|
||||
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<String>()
|
||||
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<String>()
|
||||
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<String>()
|
||||
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<String> = setOf()
|
||||
val namesTopDown = HashSet<String>()
|
||||
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<String>()
|
||||
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<String>()
|
||||
val dirs = HashSet<String>()
|
||||
val failed = HashSet<String>()
|
||||
val stack = ArrayList<File>()
|
||||
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<File>()
|
||||
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<File>()
|
||||
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<File>()
|
||||
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<String>) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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<String>()
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user