From d4b6c4f624646b1cc62461096966749260be47ef Mon Sep 17 00:00:00 2001 From: tangcent Date: Sat, 8 Jul 2023 22:26:32 +0800 Subject: [PATCH] chore: add test cases --- build.gradle.kts | 18 ++- .../itangcent/common/constant/HttpMethod.kt | 2 +- .../common/constant/HttpMethodTest.kt | 56 +++++---- .../com/itangcent/utils/AnyKitKtTest.kt | 15 ++- .../com/itangcent/utils/FuncKitKtTest.kt | 56 +++++++++ .../org/apache/http/util/EntityKitsKtTest.kt | 87 ++++++++++++++ gradle.properties | 2 +- .../idea/plugin/api/cache/FileApiCache.kt | 2 +- .../plugin/api/export/rule/RequestRuleWrap.kt | 2 +- .../com/itangcent/idea/utils/ExtensibleKit.kt | 9 -- .../com/itangcent/utils/ExtensibleKit.kt | 22 ++-- .../idea/utils/ExtensibleKitKtTest.kt | 26 ---- .../com/itangcent/utils/ExtensibleKitTest.kt | 111 ++++++++++++------ 13 files changed, 297 insertions(+), 111 deletions(-) create mode 100644 common-api/src/test/kotlin/com/itangcent/utils/FuncKitKtTest.kt create mode 100644 common-api/src/test/kotlin/org/apache/http/util/EntityKitsKtTest.kt delete mode 100644 idea-plugin/src/main/kotlin/com/itangcent/idea/utils/ExtensibleKit.kt delete mode 100644 idea-plugin/src/test/kotlin/com/itangcent/idea/utils/ExtensibleKitKtTest.kt diff --git a/build.gradle.kts b/build.gradle.kts index 31277379b..41b53c9f3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -46,10 +46,22 @@ tasks.create("codeCoverageReport", JacocoReport::class) { executionData( fileTree(project.rootDir.absolutePath).include("**/build/jacoco/*.exec") ) - subprojects.forEach { - sourceDirectories.from(it.file("src/main/kotlin")) - classDirectories.from(it.file("build/classes/kotlin/main")) + + val exclusiveDirectories = listOf("**/common/model/**") + + subprojects.forEach { project -> + sourceDirectories.from(project.files("src/main/kotlin").map { + fileTree(it).matching { + exclude(exclusiveDirectories) + } + }) + classDirectories.from(project.files("build/classes/kotlin/main").map { + fileTree(it).matching { + exclude(exclusiveDirectories) + } + }) } + reports { xml.required.set(true) xml.outputLocation.set(file("${buildDir}/reports/jacoco/report.xml").apply { parentFile.mkdirs() }) diff --git a/common-api/src/main/kotlin/com/itangcent/common/constant/HttpMethod.kt b/common-api/src/main/kotlin/com/itangcent/common/constant/HttpMethod.kt index 807ce812b..ff6b099a3 100644 --- a/common-api/src/main/kotlin/com/itangcent/common/constant/HttpMethod.kt +++ b/common-api/src/main/kotlin/com/itangcent/common/constant/HttpMethod.kt @@ -21,7 +21,7 @@ object HttpMethod { if (method.isBlank()) { return NO_METHOD } - val standardMethod = method.toUpperCase() + val standardMethod = method.uppercase() if (ALL_METHODS.contains(standardMethod)) { return standardMethod } diff --git a/common-api/src/test/kotlin/com/itangcent/common/constant/HttpMethodTest.kt b/common-api/src/test/kotlin/com/itangcent/common/constant/HttpMethodTest.kt index 0d53f33c5..5ad2dc040 100644 --- a/common-api/src/test/kotlin/com/itangcent/common/constant/HttpMethodTest.kt +++ b/common-api/src/test/kotlin/com/itangcent/common/constant/HttpMethodTest.kt @@ -1,7 +1,7 @@ package com.itangcent.common.constant -import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test +import kotlin.test.assertEquals /** * Test case for [HttpMethod] @@ -9,24 +9,40 @@ import org.junit.jupiter.api.Test internal class HttpMethodTest { @Test - fun preferMethod() { - - Assertions.assertEquals("ALL", HttpMethod.preferMethod("")) - - Assertions.assertEquals("GET", HttpMethod.preferMethod("GET")) - Assertions.assertEquals("POST", HttpMethod.preferMethod("POST")) - - Assertions.assertEquals("GET", HttpMethod.preferMethod("XXX.GET")) - Assertions.assertEquals("POST", HttpMethod.preferMethod("XXX.POST")) - Assertions.assertEquals("GET", HttpMethod.preferMethod("POST.GET")) - Assertions.assertEquals("POST", HttpMethod.preferMethod("GET.POST")) - - Assertions.assertEquals("GET", HttpMethod.preferMethod("POST_GET")) - Assertions.assertEquals("GET", HttpMethod.preferMethod("GET_POST")) - - Assertions.assertEquals("GET", HttpMethod.preferMethod("[GET]")) - Assertions.assertEquals("POST", HttpMethod.preferMethod("[POST]")) - Assertions.assertEquals("GET", HttpMethod.preferMethod("[GET][POST]")) - Assertions.assertEquals("GET", HttpMethod.preferMethod("[POST][GET]")) + fun `test preferMethod function`() { + + assertEquals("ALL", HttpMethod.preferMethod("")) + assertEquals("ALL", HttpMethod.preferMethod("foo")) + + assertEquals("GET", HttpMethod.preferMethod("GET")) + assertEquals("POST", HttpMethod.preferMethod("POST")) + assertEquals("DELETE", HttpMethod.preferMethod("DELETE")) + assertEquals("PUT", HttpMethod.preferMethod("PUT")) + assertEquals("PATCH", HttpMethod.preferMethod("PATCH")) + assertEquals("OPTIONS", HttpMethod.preferMethod("OPTIONS")) + assertEquals("TRACE", HttpMethod.preferMethod("TRACE")) + assertEquals("HEAD", HttpMethod.preferMethod("HEAD")) + + assertEquals("GET", HttpMethod.preferMethod("get")) + assertEquals("POST", HttpMethod.preferMethod("post")) + assertEquals("DELETE", HttpMethod.preferMethod("delete")) + assertEquals("PUT", HttpMethod.preferMethod("put")) + assertEquals("PATCH", HttpMethod.preferMethod("patch")) + assertEquals("OPTIONS", HttpMethod.preferMethod("options")) + assertEquals("TRACE", HttpMethod.preferMethod("trace")) + assertEquals("HEAD", HttpMethod.preferMethod("head")) + + assertEquals("GET", HttpMethod.preferMethod("XXX.GET")) + assertEquals("POST", HttpMethod.preferMethod("XXX.POST")) + assertEquals("GET", HttpMethod.preferMethod("POST.GET")) + assertEquals("POST", HttpMethod.preferMethod("GET.POST")) + + assertEquals("GET", HttpMethod.preferMethod("POST_GET")) + assertEquals("GET", HttpMethod.preferMethod("GET_POST")) + + assertEquals("GET", HttpMethod.preferMethod("[GET]")) + assertEquals("POST", HttpMethod.preferMethod("[POST]")) + assertEquals("GET", HttpMethod.preferMethod("[GET][POST]")) + assertEquals("GET", HttpMethod.preferMethod("[POST][GET]")) } } \ No newline at end of file diff --git a/common-api/src/test/kotlin/com/itangcent/utils/AnyKitKtTest.kt b/common-api/src/test/kotlin/com/itangcent/utils/AnyKitKtTest.kt index 3ba513ee3..fff153f7b 100644 --- a/common-api/src/test/kotlin/com/itangcent/utils/AnyKitKtTest.kt +++ b/common-api/src/test/kotlin/com/itangcent/utils/AnyKitKtTest.kt @@ -3,7 +3,6 @@ package com.itangcent.utils import com.itangcent.common.kit.toJson import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Test -import kotlin.test.assertEquals class AnyKitKtTest { @@ -62,5 +61,19 @@ class AnyKitKtTest { it.subMutable("x")!!["b"] = "c" }.toJson() ) + + assertNull( + ImmutableMap(hashMapOf("a" to mapOf("x" to "y"))).subMutable("a") + ) + assertNull( + ImmutableMap(hashMapOf("a" to mapOf("x" to "y"))).subMutable("b") + ) + } + + private class ImmutableMap(m: MutableMap?) : HashMap(m) { + + override fun put(key: K, value: V): V? { + throw UnsupportedOperationException() + } } } \ No newline at end of file diff --git a/common-api/src/test/kotlin/com/itangcent/utils/FuncKitKtTest.kt b/common-api/src/test/kotlin/com/itangcent/utils/FuncKitKtTest.kt new file mode 100644 index 000000000..434820f88 --- /dev/null +++ b/common-api/src/test/kotlin/com/itangcent/utils/FuncKitKtTest.kt @@ -0,0 +1,56 @@ +package com.itangcent.utils + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class FuncKitKtTest { + + @Test + fun `test and function`() { + val isEven = { num: Int -> num % 2 == 0 } + val isPositive = { num: Int -> num > 0 } + + val isEvenAndPositive = isEven.and(isPositive) + + assertEquals(true, isEvenAndPositive(4)) + assertEquals(false, isEvenAndPositive(3)) + assertEquals(false, isEvenAndPositive(-4)) + assertEquals(false, isEvenAndPositive(0)) + } + + @Test + fun `test then function with one parameter`() { + var result = "" + + val addHello = { str: String -> result = "$result $str Hello" } + val addWorld = { str: String -> result = "$result $str World" } + + val addHelloThenWorld = addHello.then(addWorld) + + result = "" + addHelloThenWorld("Hi") + assertEquals(" Hi Hello Hi World", result) + + result = "" + addHelloThenWorld("") + assertEquals(" Hello World", result) + } + + @Test + fun `test then function with three parameters`() { + var result = "" + + val addHello = { str: String, num: Int, flag: Boolean -> result = "$result $str Hello $num $flag" } + val addWorld = { str: String, num: Int, flag: Boolean -> result = "$result $str World $num $flag" } + + val addHelloThenWorld = addHello.then(addWorld) + + result = "" + addHelloThenWorld("Hi", 42, true) + assertEquals(" Hi Hello 42 true Hi World 42 true", result) + + result = "" + addHelloThenWorld("", 0, false) + assertEquals(" Hello 0 false World 0 false", result) + } +} \ No newline at end of file diff --git a/common-api/src/test/kotlin/org/apache/http/util/EntityKitsKtTest.kt b/common-api/src/test/kotlin/org/apache/http/util/EntityKitsKtTest.kt new file mode 100644 index 000000000..3644d46a6 --- /dev/null +++ b/common-api/src/test/kotlin/org/apache/http/util/EntityKitsKtTest.kt @@ -0,0 +1,87 @@ +package org.apache.http.util + +import org.apache.http.HttpEntity +import org.apache.http.entity.ContentType +import org.apache.http.entity.StringEntity +import org.junit.jupiter.api.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals + +class EntityKitsKtTest { + + @Test + fun `test HttpEntity toByteArray`() { + val content = "Hello, world!" + val entity = StringEntity(content, ContentType.TEXT_PLAIN) + val bytes = entity.toByteArray() + assertContentEquals(content.toByteArray(), bytes) + } + + @Test + fun `test HttpEntity consume`() { + object : HttpEntity { + override fun getContent() = null + override fun getContentLength() = 0L + override fun getContentType() = null + override fun isChunked() = false + override fun isRepeatable() = false + override fun isStreaming() = false + override fun writeTo(output: java.io.OutputStream?) {} + override fun getContentEncoding() = null + override fun consumeContent() {} + }.consume() + object : HttpEntity { + override fun getContent() = "content".byteInputStream(Charsets.UTF_8) + override fun getContentLength() = 0L + override fun getContentType() = null + override fun isChunked() = false + override fun isRepeatable() = false + override fun isStreaming() = true + override fun writeTo(output: java.io.OutputStream?) {} + override fun getContentEncoding() = null + override fun consumeContent() {} + }.consume() + } + + @Test + fun `test HttpEntity getContentCharSet`() { + val contentType = ContentType.create("text/plain", "UTF-8") + val entity = StringEntity("Hello, world!", contentType) + val charset = entity.getContentCharSet() + assertEquals("UTF-8", charset) + } + + @Test + fun `test HttpEntity getContentMimeType`() { + val contentType = ContentType.create("text/plain", "UTF-8") + val entity = StringEntity("Hello, world!", contentType) + val mimeType = entity.getContentMimeType() + assertEquals("text/plain", mimeType) + } + + @Test + fun `test HttpEntity readString`() { + val content = "Hello, world!" + val entity = StringEntity(content, ContentType.TEXT_PLAIN) + assertEquals(content, entity.readString()) + + val wildcardEntity = StringEntity(content, ContentType.WILDCARD) + assertEquals(content, wildcardEntity.readString()) + assertEquals(content, wildcardEntity.readString(Charsets.ISO_8859_1)) + } + + @Test + fun `test HttpEntity readString with default charset`() { + val content = "Hello, world!" + val entity = StringEntity(content, ContentType.TEXT_PLAIN.withCharset("ISO-8859-1")) + assertEquals(content, entity.readString()) + } + + @Test + fun `test HttpEntity readString with specified charset`() { + val content = "Hello, world!" + val entity = StringEntity(content, ContentType.TEXT_PLAIN.withCharset("ISO-8859-1")) + val result = entity.readString("ISO-8859-1") + assertEquals(content, result) + } +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 959fa7343..6e5e8a31a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,4 +3,4 @@ plugin_version=2.5.9.212.0 kotlin.code.style=official kotlin_version=1.8.0 junit_version=5.9.2 -itangcent_intellij_version=1.5.13-SNAPSHOT \ No newline at end of file +itangcent_intellij_version=1.5.2 \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/cache/FileApiCache.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/cache/FileApiCache.kt index bb90289a0..d6bca6016 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/cache/FileApiCache.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/cache/FileApiCache.kt @@ -2,7 +2,7 @@ package com.itangcent.idea.plugin.api.cache import com.itangcent.common.constant.HttpMethod import com.itangcent.common.model.* -import com.itangcent.idea.utils.setExts +import com.itangcent.utils.setExts class FileApiCache { /** diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/rule/RequestRuleWrap.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/rule/RequestRuleWrap.kt index 1aa769a95..1aa268b3d 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/rule/RequestRuleWrap.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/rule/RequestRuleWrap.kt @@ -8,7 +8,7 @@ import com.itangcent.common.utils.* import com.itangcent.idea.plugin.api.export.* import com.itangcent.idea.plugin.api.export.core.* import com.itangcent.idea.psi.resource -import com.itangcent.idea.utils.setExts +import com.itangcent.utils.setExts import com.itangcent.intellij.context.ActionContext import com.itangcent.intellij.jvm.DuckTypeHelper import com.itangcent.intellij.jvm.JsonOption diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/utils/ExtensibleKit.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/utils/ExtensibleKit.kt deleted file mode 100644 index d9416ff2d..000000000 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/utils/ExtensibleKit.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.itangcent.idea.utils - -import com.itangcent.common.utils.Extensible - -fun Extensible.setExts(exts: Map) { - exts.forEach { (t, u) -> - this.setExt(t, u) - } -} \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/com/itangcent/utils/ExtensibleKit.kt b/idea-plugin/src/main/kotlin/com/itangcent/utils/ExtensibleKit.kt index d9df15b5c..3b59bc3b2 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/utils/ExtensibleKit.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/utils/ExtensibleKit.kt @@ -15,18 +15,20 @@ object ExtensibleKit { val jsonElement = GsonUtils.parseToJsonTree(json)!! val t = GSON.fromJson(jsonElement, this.java) jsonElement.asJsonObject.entrySet() - .filter { it.key.startsWith(Attrs.PREFIX) } - .forEach { t.setExt(it.key, it.value.unbox()) } + .filter { it.key.startsWith(Attrs.PREFIX) } + .forEach { t.setExt(it.key, it.value.unbox()) } return t } fun KClass.fromJson(json: String, vararg exts: String): T { - val extNames = exts.removePrefix(Attrs.PREFIX) + val extNames = exts.toSet() + + exts.map { it.removePrefix(Attrs.PREFIX) } + + exts.map { it.addPrefix(Attrs.PREFIX) } val jsonElement = GsonUtils.parseToJsonTree(json)!! val t = GSON.fromJson(jsonElement, this.java) jsonElement.asJsonObject.entrySet() - .filter { extNames.contains(it.key) } - .forEach { t.setExt(it.key.addPrefix(Attrs.PREFIX), it.value.unbox()) } + .filter { extNames.contains(it.key) } + .forEach { t.setExt(it.key.addPrefix(Attrs.PREFIX), it.value.unbox()) } return t } @@ -41,10 +43,10 @@ object ExtensibleKit { return this } - /** - * Remove [prefix] from each string in the original array. - */ - private fun Array.removePrefix(prefix: CharSequence): Array { - return this.mapToTypedArray { it.removePrefix(prefix) } +} + +fun Extensible.setExts(exts: Map) { + exts.forEach { (t, u) -> + this.setExt(t, u) } } diff --git a/idea-plugin/src/test/kotlin/com/itangcent/idea/utils/ExtensibleKitKtTest.kt b/idea-plugin/src/test/kotlin/com/itangcent/idea/utils/ExtensibleKitKtTest.kt deleted file mode 100644 index 5d3285474..000000000 --- a/idea-plugin/src/test/kotlin/com/itangcent/idea/utils/ExtensibleKitKtTest.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.itangcent.idea.utils - -import com.itangcent.common.model.* -import com.itangcent.common.utils.Extensible -import org.junit.jupiter.api.Assertions.* -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.ValueSource - -/** - * Test case of [com.itangcent.idea.utils.ExtensibleKit] - */ -internal class ExtensibleKitKtTest { - - @ParameterizedTest - @ValueSource(classes = [Doc::class, FormParam::class, - Header::class, MethodDoc::class, - Param::class, PathParam::class, - Request::class, Response::class - ]) - fun testSetExts(cls: Class) { - val extensible = cls.newInstance() as Extensible//{} - extensible.setExts(mapOf("a" to 1, "b" to 2)) - kotlin.test.assertEquals(mapOf("a" to 1, "b" to 2), extensible.exts()) - } - -} \ No newline at end of file diff --git a/idea-plugin/src/test/kotlin/com/itangcent/utils/ExtensibleKitTest.kt b/idea-plugin/src/test/kotlin/com/itangcent/utils/ExtensibleKitTest.kt index fd94abb3a..0eefb9886 100644 --- a/idea-plugin/src/test/kotlin/com/itangcent/utils/ExtensibleKitTest.kt +++ b/idea-plugin/src/test/kotlin/com/itangcent/utils/ExtensibleKitTest.kt @@ -1,52 +1,87 @@ package com.itangcent.utils -import com.itangcent.common.constant.Attrs -import com.itangcent.common.model.Header -import com.itangcent.idea.plugin.api.export.yapi.setExample +import com.itangcent.common.model.* +import com.itangcent.common.utils.Extensible +import com.itangcent.common.utils.SimpleExtensible import com.itangcent.utils.ExtensibleKit.fromJson import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertNotEquals import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource /** * Test case for [ExtensibleKit] */ -class ExtensibleKitTest { +class ExtensibleTest { + + class Person( + val name: String, + val age: Int + ) : SimpleExtensible() { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Person + + if (name != other.name) return false + if (age != other.age) return false + return exts() == other.exts() + } + + override fun hashCode(): Int { + var result = name.hashCode() + result = 31 * result + age + result = 31 * result + exts().hashCode() + return result + } + } @Test - fun testFromJson() { - val acceptHeader = Header() - - acceptHeader.name = "Accept" - acceptHeader.value = "*/*" - acceptHeader.desc = "authentication" - acceptHeader.required = true - - assertEquals(acceptHeader, - Header::class.fromJson("{name: \"Accept\",value: \"*/*\",desc: \"authentication\",required:true}")) - assertEquals(acceptHeader, - Header::class.fromJson("{name: \"Accept\",value: \"*/*\",desc: \"authentication\",required:true, demo:\"token123\"}")) - - assertEquals(acceptHeader, - Header::class.fromJson("{name: \"Accept\",value: \"*/*\",desc: \"authentication\",required:true}", Attrs.DEMO_ATTR)) - assertNotEquals(acceptHeader, - Header::class.fromJson("{name: \"Accept\",value: \"*/*\",desc: \"authentication\",required:true, demo:\"token123\"}", Attrs.DEMO_ATTR)) - - acceptHeader.setExample("token123") - - assertNotEquals(acceptHeader, - Header::class.fromJson("{name: \"Accept\",value: \"*/*\",desc: \"authentication\",required:true}")) - assertNotEquals(acceptHeader, - Header::class.fromJson("{name: \"Accept\",value: \"*/*\",desc: \"authentication\",required:true, demo:\"token123\"}")) - - assertNotEquals(acceptHeader, - Header::class.fromJson("{name: \"Accept\",value: \"*/*\",desc: \"authentication\",required:true}", Attrs.DEMO_ATTR)) - assertEquals(acceptHeader, - Header::class.fromJson("{name: \"Accept\",value: \"*/*\",desc: \"authentication\",required:true, demo:\"token123\"}", Attrs.DEMO_ATTR)) - - //ext with '@' - assertEquals(acceptHeader, - Header::class.fromJson("{name: \"Accept\",value: \"*/*\",desc: \"authentication\",required:true, \"@demo\":\"token123\"}")) + fun `fromJson deserializes JSON into an Extensible object`() { + val json = """{ + "name": "John", + "age": 30, + "@ext1": "value1", + "@ext2": "value2" + }""" + val expected = Person("John", 30).apply { + setExt("@ext1", "value1") + setExt("@ext2", "value2") + } + val result = Person::class.fromJson(json) + assertEquals(expected, result) + } + + @Test + fun `fromJson deserializes JSON into an Extensible object with selected attributes`() { + val json = """{ + "name": "John", + "age": 30, + "@ext1": "value1", + "@ext2": "value2", + "ext3": "value3" + }""" + val expected = Person("John", 30).apply { + setExt("@ext1", "value1") + setExt("@ext3", "value3") + } + val result = Person::class.fromJson(json, "@ext1", "ext3") + assertEquals(expected, result) + } + + @ParameterizedTest + @ValueSource( + classes = [Doc::class, FormParam::class, + Header::class, MethodDoc::class, + Param::class, PathParam::class, + Request::class, Response::class + ] + ) + fun testSetExts(cls: Class) { + val extensible = cls.newInstance() as Extensible//{} + extensible.setExts(mapOf("a" to 1, "b" to 2)) + kotlin.test.assertEquals(mapOf("a" to 1, "b" to 2), extensible.exts()) } } \ No newline at end of file