Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -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) 성능으로 결합하도록 작성해야 합니다.
15 changes: 14 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ buildscript {

apply plugin: 'kotlin'
apply plugin: 'application'
apply plugin: 'jacoco'

mainClassName = 'html4tree.MainKt'

Expand All @@ -33,4 +34,16 @@ jar {
}

from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
}
}

jacocoTestReport {
reports {
xml.enabled false
csv.enabled false
html.enabled true
}
}

test {
finalizedBy jacocoTestReport
}
10 changes: 5 additions & 5 deletions src/main/kotlin/html4tree/main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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<File> = curr_dir.listFiles().toMutableList()
dir_files.sortWith(compareBy ({it.name}) )
dir_files.forEach {
if((it.getName() !in exclude) && (it != curr_dir)) {
l += """ <li><a style="display:block; width:100%" href="${if (it.isDirectory()) { "./${it.getName().urlEncodePath()}/" } else { "./${it.getName().urlEncodePath()}" }}">${if (it.isDirectory()) { "&#128193;" } else { "&rtrif;" }} ${it.getName().escapeHtml()}</a></li>"""+"\n"
if((it.getName() !in exclude)) {
sb.append(""" <li><a style="display:block; width:100%" href="${if (it.isDirectory()) { "./${it.getName().urlEncodePath()}/" } else { "./${it.getName().urlEncodePath()}" }}">${if (it.isDirectory()) { "&#128193;" } else { "&rtrif;" }} ${it.getName().escapeHtml()}</a></li>""").append("\n")
}
}

return l;
return sb.toString()
}

val index_bottom="""
Expand Down
59 changes: 59 additions & 0 deletions src/test/kotlin/html4tree/LinkedListTest.kt
Original file line number Diff line number Diff line change
@@ -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))
}
}
147 changes: 147 additions & 0 deletions src/test/kotlin/html4tree/MainTest.kt
Original file line number Diff line number Diff line change
@@ -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("&amp;&lt;&gt;&quot;&#x27;", "&<>\"'".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()
}
}