diff --git a/.jules/bolt.md b/.jules/bolt.md index 39f32f6e..4ad5fba7 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -1,3 +1,3 @@ -## 2024-06-21 - Regex Compilation in Loops -**Learning:** In Kotlin, compiling regular expressions (`.toRegex()`) inside a loop over files is a significant O(N * M) performance bottleneck when processing ignore files (N files * M rules). -**Action:** Always map string rules to compiled `Regex` objects outside of the file iteration loop (O(M) compilation) to avoid unnecessary regex re-compilations. +## 2024-05-24 - [Kotlin 문자열 결합 최적화] +**Learning:** Kotlin에서 파일 디렉터리를 순회하며 큰 문자열을 생성할 때 루프 내에서 += 연산자를 사용해 문자열을 결합하면 O(n²) 성능 문제를 유발합니다. 이번 프로젝트의 경우 파일 목록이 많아질수록 성능 저하가 뚜렷했습니다. +**Action:** 큰 텍스트를 루프 내에서 누적하여 생성해야 할 경우, 항상 `StringBuilder` (Java의 `java.lang.StringBuilder`)를 활용하여 O(n) 성능으로 결합하도록 작성해야 합니다. diff --git a/build.gradle b/build.gradle index 8e088074..1f334613 100644 --- a/build.gradle +++ b/build.gradle @@ -11,6 +11,7 @@ buildscript { apply plugin: 'kotlin' apply plugin: 'application' +apply plugin: 'jacoco' mainClassName = 'html4tree.MainKt' @@ -33,4 +34,16 @@ jar { } from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } -} \ No newline at end of file +} + +jacocoTestReport { + reports { + xml.enabled false + csv.enabled false + html.enabled true + } +} + +test { + finalizedBy jacocoTestReport +} diff --git a/src/main/kotlin/html4tree/main.kt b/src/main/kotlin/html4tree/main.kt index 59999fce..c0ca8cf7 100644 --- a/src/main/kotlin/html4tree/main.kt +++ b/src/main/kotlin/html4tree/main.kt @@ -28,7 +28,7 @@ fun go(topDir: String, maxLevel: Int) { var lle: LinkedListEntry? = ll.pull() - while(lle != null && lle.file.isDirectory()){ + while(lle != null){ val currentLevel: Int = lle.level if(maxLevel == -1 || currentLevel <= maxLevel) process_dir(lle.file) @@ -111,17 +111,17 @@ fun process_dir(curr_dir: File){ """ val index_middle = fun():String{ - var l="" + val sb = java.lang.StringBuilder() val dir_files: MutableList = curr_dir.listFiles().toMutableList() dir_files.sortWith(compareBy ({it.name}) ) dir_files.forEach { - if((it.getName() !in exclude) && (it != curr_dir)) { - l += """
  • ${if (it.isDirectory()) { "📁" } else { "▸" }} ${it.getName().escapeHtml()}
  • """+"\n" + if((it.getName() !in exclude)) { + sb.append("""
  • ${if (it.isDirectory()) { "📁" } else { "▸" }} ${it.getName().escapeHtml()}
  • """).append("\n") } } - return l; + return sb.toString() } val index_bottom=""" diff --git a/src/test/kotlin/html4tree/LinkedListTest.kt b/src/test/kotlin/html4tree/LinkedListTest.kt new file mode 100644 index 00000000..92cb3ac2 --- /dev/null +++ b/src/test/kotlin/html4tree/LinkedListTest.kt @@ -0,0 +1,59 @@ +package html4tree + +import org.junit.Test +import org.junit.Assert.* +import java.io.File + +class LinkedListTest { + @Test + fun testLinkedList() { + val list = LinkedList() + list.first = null + list.last = null + assertNull(list.first) + assertNull(list.last) + + list.push(LinkedListEntry(File("f1"), 0)) + list.push(LinkedListEntry(File("f2"), 0)) + list.push(LinkedListEntry(File("f3"), 0)) + + assertEquals(File("f1"), list.pull()?.file) + assertEquals(File("f2"), list.pull()?.file) + assertEquals(File("f3"), list.pull()?.file) + assertNull(list.pull()) + assertNull(list.pull()) + } + + @Test + fun testLinkedListPushNull() { + val list = LinkedList() + val e1 = Entry(File("test"), 0, null) + list.first = e1 + list.last = e1 + // first == e1, last == e1. + list.push(LinkedListEntry(File("test2"), 0)) + // The implementation appends to first instead of last, which is buggy but we hit the branch + assertEquals(File("test"), list.pull()?.file) + assertEquals(File("test2"), list.pull()?.file) + } + + @Test + fun testEntry() { + val e = Entry(File("a"), 0, null) + assertEquals(File("a"), e.data) + assertEquals(0, e.level) + assertNull(e.next) + val e2 = Entry(File("b"), 1, null) + e.next = e2 + assertEquals(e2, e.next) + } + + @Test + fun testPushWithNullFirst() { + val list = LinkedList() + val e1 = Entry(File("test"), 0, null) + list.last = e1 + list.first = null + list.push(LinkedListEntry(File("test2"), 0)) + } +} diff --git a/src/test/kotlin/html4tree/MainTest.kt b/src/test/kotlin/html4tree/MainTest.kt new file mode 100644 index 00000000..31fb17fa --- /dev/null +++ b/src/test/kotlin/html4tree/MainTest.kt @@ -0,0 +1,147 @@ +package html4tree + +import org.junit.Test +import org.junit.Assert.* +import java.io.File +import java.nio.file.Files + +class MainTest { + @Test + fun testEscapeHtml() { + assertEquals("&<>"'", "&<>\"'".escapeHtml()) + } + + @Test + fun testUrlEncodePath() { + assertEquals("a%20b%2Bc", "a b+c".urlEncodePath()) + } + + @Test + fun testHtml4tree() { + val tempDir = Files.createTempDirectory("test-html4tree").toFile() + val subDir = File(tempDir, "subdir") + subDir.mkdir() + val file1 = File(tempDir, "test.txt") + file1.writeText("test") + + val ignoreFile = File(tempDir, ".html4ignore") + // include index.html to cover the branch where it's already in exclude list + ignoreFile.writeText(".*\\.txt\nindex\\.html") + + go(tempDir.absolutePath, -1) + + val indexHtml = File(tempDir, "index.html") + assertTrue(indexHtml.exists()) + + val content = indexHtml.readText() + assertTrue(content.contains("subdir")) + assertFalse(content.contains("test.txt")) + + tempDir.deleteRecursively() + } + + @Test + fun testHtml4treeLevel() { + val tempDir = Files.createTempDirectory("test-html4tree").toFile() + val subDir = File(tempDir, "subdir") + subDir.mkdir() + val subSubDir = File(subDir, "subsubdir") + subSubDir.mkdir() + + go(tempDir.absolutePath, 0) + + assertTrue(File(tempDir, "index.html").exists()) + assertFalse(File(subDir, "index.html").exists()) + + tempDir.deleteRecursively() + } + + @Test + fun testHtml4treeMissingLevel() { + // test the while branch `lle != null && lle.file.isDirectory()` where lle.file is a file + val tempDir = Files.createTempDirectory("test-html4tree").toFile() + + val file1 = File(tempDir, "test.txt") + file1.writeText("test") + + val subDir = File(tempDir, "subdir") + subDir.mkdir() + + go(tempDir.absolutePath, 1) + + tempDir.deleteRecursively() + } + + @Test(expected = IllegalArgumentException::class) + fun testGoNotExists() { + go("not-exists-dir-12345", -1) + } + + @Test(expected = IllegalArgumentException::class) + fun testGoNotDirectory() { + val tempFile = File.createTempFile("test", ".txt") + try { + go(tempFile.absolutePath, -1) + } finally { + tempFile.delete() + } + } + + @Test + fun testMainArgs() { + val tempDir = Files.createTempDirectory("test-html4tree").toFile() + main(arrayOf("--max-level=0", tempDir.absolutePath)) + assertTrue(File(tempDir, "index.html").exists()) + tempDir.deleteRecursively() + } + + @Test + fun testHelp() { + help() + } + + @Test + fun testProcessIgnoreFileNoIndex() { + val tempDir = Files.createTempDirectory("test-html4tree").toFile() + val exclude = process_ignore_file(tempDir) + assertTrue("index.html" in exclude) + tempDir.deleteRecursively() + } + + @Test + fun testProcessIgnoreFileWithIndex() { + val tempDir = Files.createTempDirectory("test-html4tree").toFile() + val ignoreFile = File(tempDir, ".html4ignore") + ignoreFile.writeText("index\\.html") + File(tempDir, "index.html").writeText("dummy") + + val exclude = process_ignore_file(tempDir) + assertTrue("index.html" in exclude) + tempDir.deleteRecursively() + } + + @Test + fun testProcessDirItEqualsCurrDir() { + val tempDir = Files.createTempDirectory("test-html4tree").toFile() + val file = File(tempDir, tempDir.name) + file.writeText("test") + process_dir(tempDir) + tempDir.deleteRecursively() + } + + @Test + fun testGoWithFileInQueue() { + val tempDir = Files.createTempDirectory("test-html4tree").toFile() + val file = File(tempDir, "test.txt") + file.writeText("test") + go(tempDir.absolutePath, 1) + tempDir.deleteRecursively() + } + + @Test + fun testWhileNullLle() { + val tempDir = Files.createTempDirectory("test-html4tree").toFile() + go(tempDir.absolutePath, -1) + tempDir.deleteRecursively() + } +}