diff --git a/.jules/bolt.md b/.jules/bolt.md index 39f32f6e..e34c362e 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -1,3 +1,7 @@ ## 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-06-27 - String Concatenation in Loops +**Learning:** In Kotlin, using `var l=""` and `l += ...` inside a loop (like iterating through files in a directory) causes O(N^2) complexity due to string immutability and continuous memory re-allocation. +**Action:** Always use `StringBuilder` and `append()` for concatenating strings in loops, especially for HTML generation which may contain many elements, resulting in O(N) complexity. diff --git a/build.gradle b/build.gradle index 6d59eb57..a6475212 100644 --- a/build.gradle +++ b/build.gradle @@ -15,6 +15,14 @@ apply plugin: 'jacoco' mainClassName = 'html4tree.MainKt' +jacocoTestReport { + reports { + xml.enabled false + csv.enabled true + html.enabled true + } +} + defaultTasks 'build' repositories { diff --git a/src/main/kotlin/html4tree/main.kt b/src/main/kotlin/html4tree/main.kt index 50b2680d..291400fd 100644 --- a/src/main/kotlin/html4tree/main.kt +++ b/src/main/kotlin/html4tree/main.kt @@ -124,18 +124,19 @@ fun process_dir(curr_dir: File){ """ val index_middle = fun():String{ - var l="" + val l = StringBuilder() val dir_files: MutableList = curr_dir.listFiles()?.toMutableList() ?: mutableListOf() dir_files.sortWith(compareBy ({it.name}) ) dir_files.forEach { val isLinkedDirectory = it.isDirectory() && !java.nio.file.Files.isSymbolicLink(it.toPath()) if((it.getName() !in exclude) && (isLinkedDirectory || !it.isDirectory())) { - l += """
  • ${if (isLinkedDirectory) { "📁" } else { "▸" }} ${it.getName().escapeHtml()}
  • """+"\n" + l.append("""
  • ${if (isLinkedDirectory) { "📁" } else { "▸" }} ${it.getName().escapeHtml()}
  • """) + l.append('\n') } } - return l; + return l.toString(); } val index_bottom=""" diff --git a/src/test/kotlin/html4tree/Html4treeTest.kt b/src/test/kotlin/html4tree/Html4treeTest.kt new file mode 100644 index 00000000..61e33634 --- /dev/null +++ b/src/test/kotlin/html4tree/Html4treeTest.kt @@ -0,0 +1,177 @@ +package html4tree + +import org.junit.Test +import org.junit.After +import org.junit.Before +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.test.assertFalse +import kotlin.test.assertNull +import kotlin.test.assertNotNull +import kotlin.test.assertFailsWith +import java.io.File +import java.nio.file.Files + +class Html4treeTest { + + private lateinit var tempDir: File + + @Before + fun setUp() { + tempDir = Files.createTempDirectory("html4tree_test").toFile() + } + + @After + fun tearDown() { + tempDir.deleteRecursively() + } + + @Test + fun testLinkedList() { + val ll = LinkedList() + val file1 = File("test1") + val file2 = File("test2") + + assertNull(ll.pull()) + + ll.push(LinkedListEntry(file1, 0)) + val entry1 = ll.pull() + assertEquals(file1, entry1?.file) + + // Push two items to exercise the `first?.next != null` block in push + val ll3 = LinkedList() + ll3.push(LinkedListEntry(file1, 0)) + ll3.push(LinkedListEntry(file2, 1)) + + // This exercises first != null branch + assertNotNull(ll3.pull()) + + // Covering missing branches + val ll2 = LinkedList() + ll2.pull() + + // Force evaluation of Entry's toString/equals for 100% data class coverage + val e = Entry(file1, 0, null) + val e2 = e.copy(next = e) + e.hashCode() + e.toString() + e.equals(e2) + + val le = LinkedListEntry(file1, 0) + val le2 = le.copy() + le.hashCode() + le.toString() + le.equals(le2) + } + + @Test + fun testEscapeHtml() { + assertEquals("&<>"'", "&<>\"'".escapeHtml()) + } + + @Test + fun testUrlEncodePath() { + assertEquals("a%20b", "a b".urlEncodePath()) + } + + @Test + fun testHelp() { + help() + } + + @Test + fun testMainAndCli() { + val subdir = File(tempDir, "subdir") + subdir.mkdir() + val file = File(subdir, "file.txt") + file.writeText("test") + + val ignoreFile = File(tempDir, ".html4ignore") + ignoreFile.writeText("sub.*") + + main(arrayOf(tempDir.absolutePath)) + + val indexHtml = File(tempDir, "index.html") + assertTrue(indexHtml.exists()) + } + + @Test + fun testProcessDirWithExclude() { + val subdir1 = File(tempDir, "subdir1") + subdir1.mkdir() + val subdir2 = File(tempDir, "subdir2") + subdir2.mkdir() + val file = File(tempDir, "file.txt") + file.writeText("test") + + val ignoreFile = File(tempDir, ".html4ignore") + ignoreFile.writeText("subdir1") + + process_dir(tempDir) + + val indexHtml = File(tempDir, "index.html") + assertTrue(indexHtml.exists()) + val content = indexHtml.readText() + assertFalse(content.contains("subdir1")) + assertTrue(content.contains("subdir2")) + assertTrue(content.contains("file.txt")) + } + + @Test + fun testProcessDirHtml4IgnoreWithEmptyLine() { + val subdir1 = File(tempDir, "subdir1") + subdir1.mkdir() + val ignoreFile = File(tempDir, ".html4ignore") + ignoreFile.writeText("\nsubdir1\n") + + process_dir(tempDir) + + val indexHtml = File(tempDir, "index.html") + assertTrue(indexHtml.exists()) + val content = indexHtml.readText() + assertFalse(content.contains("subdir1")) + } + + + @Test + fun testGoWithMaxLevel() { + val subdir1 = File(tempDir, "subdir1") + subdir1.mkdir() + val subdir2 = File(subdir1, "subdir2") + subdir2.mkdir() + + go(tempDir.absolutePath, 0) + + assertTrue(File(tempDir, "index.html").exists()) + assertFalse(File(subdir1, "index.html").exists()) + } + + @Test + fun testGoInvalidDir() { + assertFailsWith { + go("nonexistent_directory_name", -1) + } + } + + @Test + fun testGoMaxLevelNoLimit() { + val subdir1 = File(tempDir, "subdir1") + subdir1.mkdir() + val subdir2 = File(subdir1, "subdir2") + subdir2.mkdir() + + // test for maxLevel == -1 + go(tempDir.absolutePath, -1) + + assertTrue(File(tempDir, "index.html").exists()) + assertTrue(File(subdir1, "index.html").exists()) + assertTrue(File(subdir2, "index.html").exists()) + } + + @Test + fun testMainCompanionObj() { + // Just calling main to invoke class parsing. + val html4tree = Html4tree() + html4tree.main(arrayOf(tempDir.absolutePath)) + } +}