1+ package org.ntqqrev.yogurt.fs
2+
3+ import kotlinx.io.buffered
4+ import kotlinx.io.files.Path
5+ import kotlinx.io.files.SystemTemporaryDirectory
6+ import kotlinx.io.readByteArray
7+ import kotlinx.io.readString
8+ import kotlinx.io.writeString
9+ import kotlin.random.Random
10+ import kotlin.test.*
11+
12+ class FileSystemTest {
13+ private val fs = defaultFileSystem
14+
15+ @Test
16+ fun cjkDirectoryAndTextRoundTrip () = withTemporaryRoot(" 目录读写" ) { root ->
17+ val topLevel = Path (root, " 中文目录" )
18+ val nested = Path (Path (topLevel, " 二级_日本語" ), " 세번째_한글" )
19+ val textFile = Path (nested, " 数据-混合.txt" )
20+ val content = """
21+ 第一行:你好,世界
22+ 第二行:日本語の文章
23+ 第三行:한글 문장
24+ """ .trimIndent()
25+
26+ fs.createDirectories(nested)
27+ fs.sink(textFile).buffered().use { it.writeString(content) }
28+
29+ assertTrue(fs.exists(topLevel))
30+ assertTrue(fs.exists(nested))
31+ assertTrue(fs.exists(textFile))
32+
33+ val metadata = fs.metadataOrNull(textFile)
34+ assertNotNull(metadata)
35+ assertTrue(metadata.isRegularFile)
36+ assertFalse(metadata.isDirectory)
37+ assertEquals(content.encodeToByteArray().size.toLong(), metadata.size)
38+
39+ val listedEntries = fs.list(topLevel)
40+ val listedNames = listedEntries.map(::semanticLeafName).toSet()
41+ assertEquals(setOf (" 二级_日本語" ), listedNames)
42+ val listedPath = listedEntries.single()
43+ assertEquals(normalizedPath(Path (topLevel, " 二级_日本語" )), normalizedPath(listedPath))
44+ assertEquals(listOf (" 中文目录" , " 二级_日本語" ), semanticTail(listedPath, 2 ))
45+
46+ val readBack = fs.source(textFile).buffered().use { it.readString() }
47+ assertEquals(content, readBack)
48+
49+ val resolved = fs.resolve(textFile)
50+ assertTrue(resolved.isAbsolute)
51+ assertEquals(listOf (" 二级_日本語" , " 세번째_한글" , " 数据-混合.txt" ), semanticTail(resolved, 3 ))
52+ }
53+
54+ @Test
55+ fun appendAndBinaryRoundTripOnCjkPaths () = withTemporaryRoot(" 追加与二进制" ) { root ->
56+ val dataDir = Path (root, " 附件_资料" )
57+ val textFile = Path (dataDir, " 日志.txt" )
58+ val binaryFile = Path (dataDir, " 图像_样本.bin" )
59+ val expectedText = " 第一行\n 第二行_追加\n 第三行_終わり\n "
60+ val expectedBinary = byteArrayOf(0 , 1 , 2 , 3 , 127 , (- 1 ).toByte()) +
61+ " 中文ABCかな한글" .encodeToByteArray()
62+
63+ fs.createDirectories(dataDir)
64+
65+ fs.sink(textFile).buffered().use { it.writeString(" 第一行\n " ) }
66+ fs.sink(textFile, append = true ).buffered().use { it.writeString(" 第二行_追加\n 第三行_終わり\n " ) }
67+ fs.sink(binaryFile).buffered().use { it.write(expectedBinary) }
68+
69+ val actualText = fs.source(textFile).buffered().use { it.readString() }
70+ val actualBinary = fs.source(binaryFile).buffered().use { it.readByteArray() }
71+
72+ assertEquals(expectedText, actualText)
73+ assertContentEquals(expectedBinary, actualBinary)
74+ }
75+
76+ @Test
77+ fun atomicMoveKeepsCjkNamesAndContent () = withTemporaryRoot(" 原子移动" ) { root ->
78+ val sourceDir = Path (root, " 原目录" )
79+ val destinationDir = Path (root, " 目标目录" )
80+ val sourceFile = Path (sourceDir, " 待移动_文件.txt" )
81+ val destinationFile = Path (destinationDir, " 已移动_结果.txt" )
82+ val content = " 迁移内容_日本語_한글"
83+
84+ fs.createDirectories(sourceDir)
85+ fs.createDirectories(destinationDir)
86+ fs.sink(sourceFile).buffered().use { it.writeString(content) }
87+
88+ fs.atomicMove(sourceFile, destinationFile)
89+
90+ assertFalse(fs.exists(sourceFile))
91+ assertTrue(fs.exists(destinationFile))
92+ assertEquals(content, fs.source(destinationFile).buffered().use { it.readString() })
93+
94+ val destinationEntries = fs.list(destinationDir)
95+ val destinationListing = destinationEntries.map(::semanticLeafName).toSet()
96+ assertEquals(setOf (destinationFile.name), destinationListing)
97+ assertEquals(normalizedPath(destinationFile), normalizedPath(destinationEntries.single()))
98+ }
99+
100+ private inline fun withTemporaryRoot (label : String , block : (Path ) -> Unit ) {
101+ val root = Path (
102+ SystemTemporaryDirectory ,
103+ " ktfs-${Random .nextLong().toString(16 )} -$label -中文_日本語_한글" ,
104+ )
105+ fs.createDirectories(root, mustCreate = true )
106+ try {
107+ block(root)
108+ } finally {
109+ deleteRecursively(root)
110+ }
111+ }
112+
113+ private fun deleteRecursively (path : Path ) {
114+ val metadata = fs.metadataOrNull(path) ? : return
115+ if (metadata.isDirectory) {
116+ fs.list(path).forEach(::deleteRecursively)
117+ }
118+ fs.delete(path, mustExist = true )
119+ }
120+
121+ private fun semanticLeafName (path : Path ): String {
122+ return semanticSegments(path).last()
123+ }
124+
125+ private fun semanticTail (path : Path , count : Int ): List <String > {
126+ return semanticSegments(path).takeLast(count)
127+ }
128+
129+ private fun normalizedPath (path : Path ): String {
130+ return path.toString().replace(' \\ ' , ' /' ).removeSuffix(" /." )
131+ }
132+
133+ private fun semanticSegments (path : Path ): List <String > {
134+ return normalizedPath(path)
135+ .split(' /' )
136+ .filter { it.isNotEmpty() && it != " ." }
137+ }
138+ }
0 commit comments