
LoaderTest.pudu
Pudu85 lines7.2 KB
1/** @Test.Loader.Suite — reading, merging, and settling sources */2module PuduLangEnvironment.LoaderTest34import Std.Bytes as Bytes5import Std.Env as Env6import Std.Fs as Fs7import Std.Io as Io8import Std.Map as Map9import Std.Path as Path10import Std.Result as Result11import Std.Test as Test12import PuduLangEnvironment.Domain.Encoding as Encoding13import PuduLangEnvironment as Environment14import PuduLangEnvironment.Loader as Loader15import PuduLangEnvironment.Options as Options16import PuduLangEnvironment.Source as Source17import PuduLangEnvironment.Variables as Variables181920const SECRET: Str = "postgres://admin:s3cr3t@db/prod"212223fn readOver(options: Options.Options) -> Result[Map[Str, Str], Environment.Problem] {24 Loader.readOver(&options, &Map.fromPairs(&[("HOME", "/home/me"), ("HOST", "shell")]))25}262728fn pairs(options: Options.Options) -> Result[Array[(Str, Str)], Environment.Problem] {29 Ok(readOver(options) ?.entries())30}313233fn main() -> Int {34 let made = Result.unwrapOr(Fs.temporaryDirectoryIn(Env.temporaryDirectory(), "pudu-env-loader-"), ".")35 let root = Result.unwrapOr(Fs.canonical(made), made)36 let _base = Io.write(Path.join(root, ".env"), "# shared\nHOST=file\nPORT=8080\nURL=" + SECRET + "\n")37 let _local = Io.write(Path.join(root, ".env.local"), "PORT=9090\nLOCAL=$HOST:$PORT\n")38 let _broken = Io.write(Path.join(root, "broken.env"), "OK=1\nKEY=\"never closed\n")39 let _latin = Fs.writeAtomically(Path.join(root, "latin.env"), &Result.unwrapOr(Bytes.decodeHex("4e414d453de9"), Bytes.empty()))40 let base = Options.defaults().inDirectory(root)41 let layered = base.withFiles(&[".env", ".env.local"])42 let process = Map.fromPairs(&[("HOST", "shell"), ("ONLY_SHELL", "1")])43 let lenient = Loader.loadOver(&base.withFiles(&[".env", "absent.env"]), &process)44 let kept = Loader.loadOver(&layered.withoutOverwrite(), &process)45 let checks = Test.suite("Loader", &[46 Test.equals("the default file is read", &pairs(base), &Ok([("HOST", "file"), ("PORT", "8080"), ("URL", SECRET)])),47 Test.equals("later files win when overwriting", &Result.map(readOver(layered), |values: Map[Str, Str]| values.get("PORT")), &Ok(Some("9090"))),48 Test.equals("earlier files win without overwriting", &Result.map(readOver(layered.withoutOverwrite()), |values: Map[Str, Str]| values.get("PORT")), &Ok(Some("8080"))),49 Test.equals("expansion sees earlier files", &Result.map(readOver(layered.withExpansion()), |values: Map[Str, Str]| values.get("LOCAL")), &Ok(Some("file:9090"))),50 Test.equals("without expansion references stay", &Result.map(readOver(layered), |values: Map[Str, Str]| values.get("LOCAL")), &Ok(Some("$HOST:$PORT"))),51 Test.equals("a lenient load skips a missing file and records it", &Result.map(lenient, |view: Variables.Variables| (view.sources, view.skipped)), &Ok(([Path.join(root, ".env")], [Environment.MissingFile(Path.join(root, "absent.env"))]))),52 Test.equals("a strict load refuses a missing file", &pairs(base.withFiles(&["absent.env"]).withStrict()), &Err(Environment.MissingFile(Path.join(root, "absent.env")))),53 Test.equals("a lenient load of nothing answers nothing", &pairs(base.inDirectory(Path.join(root, "nowhere"))), &Ok([])),54 Test.equals("an unclosed quote refuses even a lenient load", &pairs(base.withFiles(&["broken.env"])), &Err(Environment.UnclosedQuote(Path.join(root, "broken.env"), 2, "KEY"))),55 Test.equals("invalid options are refused with every problem", &pairs(base.withFiles(&["a"]).withProbeLevels(-1)), &Err(Environment.InvalidOptions(["files cannot be given while probing", "probe levels must not be negative"]))),56 Test.equals("an encoding is honoured", &pairs(base.withFiles(&["latin.env"]).withEncoding(Encoding.Latin1)), &Ok([("NAME", "é")])),57 Test.equals("undecodable bytes are skipped when lenient", &pairs(base.withFiles(&["latin.env"])), &Ok([])),58 Test.equals("undecodable bytes are refused when strict", &pairs(base.withFiles(&["latin.env"]).withStrict()), &Err(Environment.Undecodable(Path.join(root, "latin.env"), "UTF-8"))),59 Test.equals("text sources are read in order", &pairs(base.withSources(&[Source.Text("one", "A=1\nB=1"), Source.Text("two", "B=2")])), &Ok([("A", "1"), ("B", "2")])),60 Test.equals("text sources expand over the process", &pairs(base.withSources(&[Source.Text("one", "D=$HOME/data")]).withExpansion()), &Ok([("D", "/home/me/data")])),61 Test.equals("a file source is located against the directory", &pairs(base.withSources(&[Source.File(".env.local")])), &Ok([("LOCAL", "$HOST:$PORT"), ("PORT", "9090")])),62 Test.equals("an unclosed quote in text names its label", &pairs(base.withSources(&[Source.Text("inline", "K='x")])), &Err(Environment.UnclosedQuote("inline", 1, "K"))),63 Test.equals("the cascade loads its layers", &Result.map(readOver(base.withCascade()), |values: Map[Str, Str]| values.get("PORT")), &Ok(Some("9090"))),64 Test.equals("loading settles over the process when overwriting", &Result.map(Loader.loadOver(&layered, &process), |view: Variables.Variables| Variables.find(&view, "HOST")), &Ok(Some("file"))),65 Test.equals("loading keeps the process without overwriting", &Result.map(kept, |view: Variables.Variables| (Variables.find(&view, "HOST"), Variables.find(&view, "PORT"), Variables.find(&view, "ONLY_SHELL"))), &Ok((Some("shell"), Some("8080"), Some("1")))),66 Test.equals("without overwriting expansion sees the process value that is kept", &Result.map(Loader.readOver(&base.withSources(&[Source.Text("t", "HOST=file\nURL=$HOST")]).withExpansion().withoutOverwrite(), &process), |values: Map[Str, Str]| values.get("URL")), &Ok(Some("shell"))),67 Test.equals("with overwriting expansion sees the file's value", &Result.map(Loader.readOver(&base.withSources(&[Source.Text("t", "HOST=file\nURL=$HOST")]).withExpansion(), &process), |values: Map[Str, Str]| values.get("URL")), &Ok(Some("file"))),68 Test.equals("loading records what it applied and read", &Result.map(kept, |view: Variables.Variables| (view.applied, view.sources)), &Ok((["LOCAL", "PORT", "URL"], [Path.join(root, ".env"), Path.join(root, ".env.local")]))),69 Test.that("a loaded view never renders a value", match Loader.loadOver(&base, &process) {70 case Ok(view) => !show(view).contains(SECRET) && !Variables.describe(&view).contains(SECRET)71 case Err(_) => false72 }),73 Test.equals("read takes the process environment", &Result.map(Loader.read(&base.withSources(&[Source.Text("t", "P=$PATH")]).withExpansion()), |values: Map[Str, Str]| values.get("P") == Env.variable("PATH")), &Ok(true)),74 Test.equals("load takes the process environment", &Result.map(Loader.load(&base), |view: Variables.Variables| Variables.has(&view, "PATH")), &Ok(true)),75 Test.equals("the chain ends in read", &Result.map(base.withFiles(&[".env.local"]).read(), |values: Map[Str, Str]| values.size()), &Ok(2)),76 Test.equals("the chain ends in load", &Result.map(base.withStrict().load(), |view: Variables.Variables| Variables.find(&view, "PORT")), &Ok(Some("8080")))77 ])78 let _cleared = Fs.removeTree(root)79 let ran = Test.run(&checks)80 for failure in Test.failuresOf(&ran) {81 let _reported = Io.writeErrorLine(failure)82 }83 Test.report(&ran)84}85