Skip to content

FileManagerSmokeTest.kt

src/jvmMain/kotlin/afield/examples/stdlib/FileManagerSmokeTest.kt · 274 lines

package afield.examples.stdlib

import afield.document.graph.Box
import afield.document.history.HistoryManager
import afield.app.server.SessionInformation
import afield.document.persistence.SavingContext
import afield.document.workspace.FILE_MANAGER_ACCESS_HISTORY_PROPERTY
import afield.document.workspace.FileManager
import afield.document.workspace.absolutePath
import afield.document.workspace.abslutePath
import afield.document.workspace.file
import afield.document.workspace.path
import java.io.File
import java.nio.file.Files
import java.util.concurrent.ConcurrentHashMap

object FileManagerSmokeTest

FIELD_TEST id: stdlib.file-manager.smoke runner: jvm-main entrypoint: afield.examples.stdlib.FileManagerSmokeTest tags: [smoke, stdlib, files, persistence] timeoutSec: 20 expect: exitCode: 0 stdoutContains: - "FileManager smoke tests passed"

object FileManagerSmokeTest {

fun main(args: Array<String>)

    @JvmStatic
    fun main(args: Array<String>) {
        check(runTests()) { "FileManager smoke tests failed" }
    }

fun runTests(): Boolean

body — 238 lines, from line 36
    fun runTests(): Boolean {
        return try {
            testDocumentDefaultAndExplicitPaths()
            testProjectLookupAndGlob()
            testNamedDirectoryLookup()
            testSearchListLookup()
            testAccessHistoryPersistsOnRoot()
            testAccessHistorySurfacesAsSyntheticHistoryItem()
            println("FileManager smoke tests passed")
            true
        } catch (t: Throwable) {
            t.printStackTrace()
            false
        }
    }

    private fun testDocumentDefaultAndExplicitPaths() {
        val fixture = fixture("file-manager-document")
        Box.withBoxContextNow(fixture.box) {
            val implicit = "foo.json".file
            val expected = File(fixture.documentDir, "files/foo.json")
            require(sameFilePath(implicit, expected)) {
                "Expected implicit relative file in document files/, got ${implicit.absolutePath}"
            }
            require(implicit.parentFile.isDirectory) { "Expected document files/ directory to be created" }

            val explicit = "%document/foo.json"
            require(sameFilePath(explicit.file, implicit)) { "Expected %document/foo.json to match implicit current directory" }
            require(sameFilePath(explicit.path.toFile(), implicit)) { "Expected .path to return a Path" }
            require(explicit.absolutePath == implicit.absolutePath) { "Expected .absolutePath to return absolute string" }
            @Suppress("DEPRECATION") // the point of the assertion is that the deprecated typo alias still works
            require(explicit.abslutePath == implicit.absolutePath) { "Expected .abslutePath typo alias to work" }
        }
    }

    private fun testProjectLookupAndGlob() {
        val fixture = fixture("file-manager-project")
        val projectFiles = File(fixture.workspaceDir, "_projects/projectA/files")
        val nested = File(projectFiles, "deep/dir/bar.json").also {
            it.parentFile.mkdirs()
            it.writeText("""{"ok":true}""")
        }

        Box.withBoxContextNow(fixture.box) {
            val implicit = "deep/dir/bar.json".file
            require(sameFilePath(implicit, nested)) {
                "Expected implicit lookup to find project file, got ${implicit.absolutePath}, expected ${nested.absolutePath}"
            }

            val glob = "%project/**/bar.json"
            val globFile = glob.file
            require(sameFilePath(globFile, nested)) {
                "Expected project glob to find nested project file, got ${globFile.absolutePath}, expected ${nested.absolutePath}"
            }
        }
    }

    private fun testNamedDirectoryLookup() {
        val fixture = fixture("file-manager-named")
        val namedRoot = Files.createTempDirectory("file-manager-named-root").toFile()
        val namedFile = File(namedRoot, "a/b/bar.json").also {
            it.parentFile.mkdirs()
            it.writeText("""{"named":true}""")
        }
        FileManager.add("fmtest_${fixture.documentDir.name}", namedRoot)

        Box.withBoxContextNow(fixture.box) {
            val direct = "%fmtest_${fixture.documentDir.name}/a/b/bar.json"
            val glob = "%fmtest_${fixture.documentDir.name}/**/bar.json"
            require(sameFilePath(direct.file, namedFile)) { "Expected direct named-root lookup" }
            require(sameFilePath(glob.file, namedFile)) { "Expected named-root glob lookup" }
        }
    }

    private fun testSearchListLookup() {
        val fixture = fixture("file-manager-search")
        val name = "fmsearch_${fixture.documentDir.name}"
        val searchRoot = Files.createTempDirectory("file-manager-search-root").toFile()
        val onlyThere = File(searchRoot, "kit/only-there.json").also {
            it.parentFile.mkdirs()
            it.writeText("""{"search":true}""")
        }
        File(searchRoot, "shadowed.json").writeText("""{"from":"search"}""")
        val documentCopy = File(fixture.documentDir, "files/shadowed.json").also {
            it.parentFile.mkdirs()
            it.writeText("""{"from":"document"}""")
        }

        try {
            Box.withBoxContextNow(fixture.box) {
                FileManager.add(name, searchRoot)
                require(!sameFilePath("kit/only-there.json".file, onlyThere)) {
                    "A root added without search = true must not answer a plain name"
                }

                FileManager.add(name, searchRoot, search = true)
                require(FileManager.searchNames.contains(name)) { "Expected $name on FileManager.searchNames" }
                require(sameFilePath("kit/only-there.json".file, onlyThere)) {
                    "Expected a plain name to fall through to the search list"
                }
                val history = fixture.box.root.getPropertyRaw(FILE_MANAGER_ACCESS_HISTORY_PROPERTY) as List<*>
                val source = (history.last() as Map<*, *>)["source"]
                require(source == "search:$name") { "Expected source search:$name, got $source" }

                require(sameFilePath("**/only-there.json".file, onlyThere)) { "Expected a glob to search the search list" }
                require(sameFilePath("shadowed.json".file, documentCopy)) {
                    "Expected the document's files/ to win over a search directory"
                }
                val missing = "not-anywhere.json".file
                require(sameFilePath(missing, File(fixture.documentDir, "files/not-anywhere.json"))) {
                    "Expected a missing name to resolve into the document's files/, got ${missing.absolutePath}"
                }

                FileManager[name] = searchRoot
                require(!FileManager.searchNames.contains(name)) { "Re-registering with set should leave the search list" }
                require(!sameFilePath("kit/only-there.json".file, onlyThere)) {
                    "Expected the cached search resolution to be dropped once the root left the search list"
                }
            }
        } finally {
            FileManager.remove(name)
        }
        require(!FileManager.searchNames.contains(name)) { "Expected remove to clear the search list entry" }
    }

    private fun testAccessHistoryPersistsOnRoot() {
        val fixture = fixture("file-manager-history")
        val savedContext = SavingContext()

        Box.withBoxContextNow(fixture.box) {
            "history.json".file
            val history = fixture.box.root.getPropertyRaw(FILE_MANAGER_ACCESS_HISTORY_PROPERTY) as? List<*>
                ?: error("Expected root.$FILE_MANAGER_ACCESS_HISTORY_PROPERTY")
            require(history.size == 1) { "Expected one access entry, got ${history.size}" }

            val entry = history.single() as? Map<*, *> ?: error("Expected access entry map")
            require(entry["request"] == "history.json") { "Wrong request in access entry: $entry" }
            require(entry["access"] == "file") { "Wrong access kind in access entry: $entry" }
            require(entry["source"] == "document") { "Wrong source in access entry: $entry" }
            require(entry["box"] == fixture.box.id) { "Wrong box id in access entry: $entry" }

            val pending = fixture.box.inside.hasPendingSidecarSave(fixture.box.root.id, FILE_MANAGER_ACCESS_HISTORY_PROPERTY)
            require(pending) { "Expected FileManager access history to mark document pending for save" }
        }

        val edn = savedContext.exportEdnPackage(setOf(fixture.box.root, fixture.box), fixture.documentDir.absolutePath)
        val rootTransfer = savedContext.readEdnTransfersFromString(edn).first { it.id == "root" }
        val persisted = rootTransfer.properties[FILE_MANAGER_ACCESS_HISTORY_PROPERTY] as? List<*>
            ?: error("Expected exported root.$FILE_MANAGER_ACCESS_HISTORY_PROPERTY")
        require(persisted.size == 1) { "Expected exported access history to contain one entry" }
    }

    private fun testAccessHistorySurfacesAsSyntheticHistoryItem() {
        val fixture = fixture("file-manager-history-panel")
        val savingContext = SavingContext()
        val boxes = setOf(fixture.box.root, fixture.box)
        val backups = File(fixture.documentDir, "backups").also { it.mkdirs() }

        fixture.box.root.setPropertyRaw(FILE_MANAGER_ACCESS_HISTORY_PROPERTY, emptyList<Map<String, Any?>>())
        File(backups, "metadata_backup_00000.edn").writeText(
            savingContext.exportEdnPackage(boxes, fixture.documentDir.absolutePath)
        )

        fixture.box.root.setPropertyRaw(
            FILE_MANAGER_ACCESS_HISTORY_PROPERTY,
            listOf(
                linkedMapOf<String, Any?>(
                    "index" to 1L,
                    "time" to 1234L,
                    "access" to "file",
                    "request" to "history.json",
                    "resolvedPath" to File(fixture.documentDir, "files/history.json").absolutePath,
                    "source" to "document",
                    "exists" to false,
                    "box" to fixture.box.id,
                    "boxName" to "boxA"
                )
            )
        )
        File(fixture.documentDir, SavingContext.METADATA_EDN).writeText(
            savingContext.exportEdnPackage(boxes, fixture.documentDir.absolutePath)
        )

        val diff = HistoryManager.diffVersionAgainstPrevious(fixture.documentDir.absolutePath, 1)
            ?: error("Expected history diff")
        val synthetic = diff.boxes.singleOrNull { it.uid == "root" }
            ?: error("Expected synthetic root history item in diff: ${diff.boxes}")
        require(synthetic.name == "Document file access") { "Wrong synthetic history item name: ${synthetic.name}" }
        require(synthetic.currentFrame == null && synthetic.targetFrame == null) { "Synthetic item should not expose frames" }
        require(synthetic.currentChildren.isEmpty() && synthetic.targetChildren.isEmpty()) {
            "Synthetic item should not expose children"
        }
        require(synthetic.currentParents.isEmpty() && synthetic.targetParents.isEmpty()) {
            "Synthetic item should not expose parents"
        }

        val property = synthetic.properties.singleOrNull { it.property == FILE_MANAGER_ACCESS_HISTORY_PROPERTY }
            ?: error("Expected $FILE_MANAGER_ACCESS_HISTORY_PROPERTY change on synthetic item")
        require(property.kind == "text") { "Expected access history to open as text, got ${property.kind}" }
        require(property.targetText.contains("history.json")) {
            "Expected formatted target text to mention requested file: ${property.targetText}"
        }
        require(property.lineStats.added >= 1) { "Expected added line stats for access history" }
    }

    private fun sameFilePath(a: File, b: File): Boolean = a.canonicalFile == b.canonicalFile

    private data class Fixture(
        val workspaceDir: File,
        val documentDir: File,
        val box: Box
    )

    private fun fixture(prefix: String): Fixture {
        val workspace = Files.createTempDirectory(prefix).toFile()
        val document = File(workspace, "docA").also { it.mkdirs() }
        File(document, SavingContext.METADATA_EDN).writeText("{:version 2 :boxes []}\n")

        val projectDir = File(workspace, "_projects/projectA").also { it.mkdirs() }
        File(projectDir, "contents.edn").writeText("{:documents [\"docA\"]}\n")

        val root = Box("root")
        val session = SessionInformation(
            filename = document.absolutePath,
            documentName = document.name,
            boxes = ConcurrentHashMap(),
            root = root
        )
        root.inside = session
        session.boxes[root.id] = root

        val box = Box("boxA")
        box.inside = session
        session.boxes[box.id] = box

        return Fixture(workspace, document, box)
    }
}