
ServicesTest.pudu
Pudu119 lines8.3 KB
1/** @Test.Services.ServicesTest.Suite — processes, discovery, and module files */2module PuduLangMcp.Services.ServicesTest34import Std.Bytes as ByteSeq5import Std.Env as Env6import Std.Fs as Fs7import Std.Io as Io8import Std.Path as Path9import Std.Result as Result10import Std.Test as Test11import PuduLangMcp.Services.Process.Bounded as Bounded12import PuduLangMcp.Services.Reference as Reference13import PuduLangMcp.Services.Toolchain as Toolchain14import PuduLangMcp.Services.Workspace as Workspace151617fn shell(script: Str, input: Str, millis: Int, cap: Int) -> Bounded.Finished {18 Result.unwrapOr(Bounded.run("/bin/sh", &["-c", script], input, "", millis, cap), Bounded.Finished{status: -99, output: "", errors: "", timedOut: false, truncated: false, millis: 0})19}202122fn place(path: Str, text: Str) -> () {23 let _made = Io.makeDirectory(Path.directoryOf(path))24 let _written = Io.write(path, text)25}262728fn main() -> Int {29 let temporary = Env.temporaryDirectory()30 let mixed = shell("echo out; echo err >&2; exit 3", "", 5000, 4096)31 let echoed = shell("cat", "line one\nline two", 5000, 4096)32 let slow = shell("sleep 5", "", 200, 4096)33 let held = shell("echo early; sleep 5; true", "", 200, 4096)34 let quiet = shell("sleep 5 2>/dev/null; true", "", 200, 4096)35 let late = shell("echo first; (sleep 0.05; echo second) &", "", 5000, 4096)36 let flood = shell("yes x | head -c 100000", "", 5000, 1000)37 let missing = Bounded.run("/nonexistent/program", &[], "", "", 1000, 1000)38 let accent = ByteSeq.fromText("é")39 let euro = ByteSeq.fromText("€")40 let emoji = ByteSeq.fromText("😀")41 let mixedText = ByteSeq.fromText("a😀")42 let exactCap = shell("head -c 1000 /dev/zero | tr '\\000' x", "", 5000, 1000)43 let pastCapLater = shell("head -c 1000 /dev/zero | tr '\\000' x; sleep 0.3; printf y", "", 5000, 1000)44 let archive = Result.unwrapOr(Fs.temporaryDirectoryIn(temporary, "pudu-mcp-archive-"), "/nonexistent")45 place(archive + "/bin/pudu", "")46 place(archive + "/lib/pudu/Std/Io.pudu", "module Std.Io\n")47 let checkout = Result.unwrapOr(Fs.temporaryDirectoryIn(temporary, "pudu-mcp-checkout-"), "/nonexistent")48 place(checkout + "/dist/build/x/pudu", "")49 place(checkout + "/packages/pudu/v0.1/lib/Std/Io.pudu", "module Std.Io\n")50 place(checkout + "/packages/pudu/v0.2/lib/Std/Io.pudu", "module Std.Io\n")51 let lonely = Result.unwrapOr(Fs.temporaryDirectoryIn(temporary, "pudu-mcp-lonely-"), "/nonexistent")52 place(lonely + "/bin/pudu", "")53 let workspace = Result.unwrapOr(Fs.temporaryDirectoryIn(temporary, "pudu-mcp-deps-"), "/nonexistent")54 place(workspace + "/deps/shapes/src/Shapes.pudu", "module Shapes\n")55 place(workspace + "/deps/shapes/src/Shapes/Area.pudu", "module Shapes.Area\n")56 place(workspace + "/deps/shapes/README.md", "no module")57 place(workspace + "/deps/flat/Flat.pudu", "module Flat\n")58 place(workspace + "/deps/clash/src/Std/Io.pudu", "module Std.Io\n")59 place(workspace + "/deps/deep/src/" + "D/".repeat(12) + "Found.pudu", "module Found\n")60 place(workspace + "/deps/deeper/src/" + "E/".repeat(13) + "Lost.pudu", "module Lost\n")61 let files = Reference.moduleFiles(Some(archive + "/lib/pudu"), workspace)62 let names = files.map(|held: Reference.ModuleFile| held.moduleName)63 let stdIo = Reference.fileOf(&files, "Std.Io")64 let located = Workspace.at(workspace)65 let checks = Test.suite("Services", &[66 Test.equals("a run answers its exit status", &mixed.status, &3),67 Test.equals("a run answers its standard output", &mixed.output.trim(), &"out"),68 Test.equals("a run answers its standard error", &mixed.errors.trim(), &"err"),69 Test.not("a finished run did not time out", mixed.timedOut),70 Test.equals("input reaches the child", &echoed.output, &"line one\nline two"),71 Test.that("a slow child is stopped at its deadline", slow.timedOut && slow.status == -1),72 Test.that("a stopped child costs about its deadline", slow.millis < 3000),73 Test.that("a grandchild holding the streams does not outlast the deadline", held.timedOut && held.millis < 3000),74 Test.equals("output written before the deadline is kept", &held.output.trim(), &"early"),75 Test.that("a stream left open past the grace is marked cut", held.truncated),76 Test.not("a run whose streams closed is not marked cut", mixed.truncated),77 Test.that("a silent stream held open alone still ends near the deadline", quiet.timedOut && quiet.millis < 3000),78 Test.that("one stream left open is enough to mark the run cut", quiet.truncated),79 Test.equals("a stream that closes within the grace is read to its end", &late.output.trim(), &"first\nsecond"),80 Test.not("a stream that closes within the grace is not marked cut", late.truncated),81 Test.that("output past the cap is cut and marked", flood.truncated && flood.output.length() == 1000),82 Test.errored("a program that cannot start is an error", &missing),83 Test.equals("a cut inside a character backs off to its start", &Bounded.textWithin(&accent, 1), &("", true)),84 Test.equals("a cut inside a three-byte character backs off", &[Bounded.textWithin(&euro, 1), Bounded.textWithin(&euro, 2)], &[("", true), ("", true)]),85 Test.equals("a cut inside a four-byte character backs off", &Bounded.textWithin(&emoji, 3), &("", true)),86 Test.equals("a cut after text keeps the text before the broken character", &Bounded.textWithin(&mixedText, 4), &("a", true)),87 Test.equals("a four-byte character within the cap is whole", &Bounded.textWithin(&emoji, 4), &("😀", false)),88 Test.that("output exactly at the cap is whole", !exactCap.truncated && exactCap.output.length() == 1000),89 Test.that("output past the cap in a later write is still cut", pastCapLater.truncated && pastCapLater.output.length() == 1000),90 Test.equals("text within the cap is whole", &Bounded.textWithin(&accent, 2), &("é", false)),91 Test.equals("bytes that are not text are marked", &Bounded.textWithin(&ByteSeq.fromArray(&[255u8, 254u8, 253u8, 252u8, 251u8]), 8), &("[output that is not UTF-8 text]", false)),92 Test.equals("one invalid byte is not a truncated character", &Bounded.textWithin(&ByteSeq.fromArray(&[255u8]), 8), &("[output that is not UTF-8 text]", false)),93 Test.not("a child has time before its deadline", Bounded.deadlineReached(99, 100)),94 Test.that("a child expires at and after its deadline", Bounded.deadlineReached(100, 100) && Bounded.deadlineReached(101, 100)),95 Test.not("scripted finished output is not truncated", Toolchain.finished(0, "x").truncated),96 Test.equals("an archive's library is beside its bin directory", &Toolchain.libraryFrom(archive + "/bin/pudu"), &Some(archive + "/lib/pudu")),97 Test.equals("a checkout's newest series is chosen", &Toolchain.libraryFrom(checkout + "/dist/build/x/pudu"), &Some(checkout + "/packages/pudu/v0.2/lib")),98 Test.equals("no library anywhere above is none", &Toolchain.libraryFrom(lonely + "/bin/pudu"), &None),99 Test.that("installed packages are importable modules", names.contains("Shapes") && names.contains("Shapes.Area")),100 Test.that("a package without src is read from its root", names.contains("Flat")),101 Test.equals("the library wins a duplicate module name", &match stdIo { case Some(held) => held.origin case None => "" }, &"Std"),102 Test.equals("module files are sorted by name", &names.filter(|name: Str| !name.startsWith("D.") && !name.startsWith("E.")), &["Flat", "Shapes", "Shapes.Area", "Std.Io"]),103 Test.that("a module twelve directories deep is found", names.contains("D.D.D.D.D.D.D.D.D.D.D.D.Found")),104 Test.not("a module thirteen directories deep is not", names.contains("E.E.E.E.E.E.E.E.E.E.E.E.E.Lost")),105 Test.succeeded("a workspace path resolves inside it", &Workspace.resolve(&located, "deps/flat/Flat.pudu")),106 Test.equals("a relative path is shown from the root", &Workspace.relative(&located, located.root + "/deps/flat/Flat.pudu"), &"deps/flat/Flat.pudu"),107 Test.equals("a path outside is shown whole", &Workspace.relative(&located, "/etc/hosts"), &"/etc/hosts")108 ])109 let _a = Fs.removeTree(archive)110 let _b = Fs.removeTree(checkout)111 let _c = Fs.removeTree(lonely)112 let _d = Fs.removeTree(workspace)113 let report = Test.run(&checks)114 for failure in Test.failuresOf(&report) {115 let _reported = Io.writeErrorLine(failure)116 }117 Test.report(&report)118}119