Pudu programming language
Menu
Package

@chrismichaelps / pudu-lang-environment

Environment files for Pudu: load .env variables safely with layered files, discovery, expansion, typed reads, and secrets kept out of every message

0.1.0Apache-2.01

InstallClose

ParserTest.pudu

Pudu102 lines8.9 KB

GitHub ↗
1/** @Test.Domain.Parser.Suite — lines of environment files to entries */2module PuduLangEnvironment.Domain.ParserTest34import Std.Io as Io5import Std.Map as Map6import Std.Result as Result7import Std.Test as Test8import PuduLangEnvironment.Domain.Layers as Layers9import PuduLangEnvironment.Domain.Parser as Parser1011/// The default reading rules.12fn plain() -> Parser.Rules {13  Parser.Rules{trimValues: false, exportSyntax: false, inlineComments: true, expansion: false, overwrite: true}14}1516/// The pairs the text assigns under the rules, over the given process values.17fn read(text: Str, rules: Parser.Rules, process: Array[(Str, Str)]) -> Result[Array[(Str, Str)], Parser.Unclosed] {18  let parsed = Parser.parse(&Parser.lines(text), &rules, Map.empty(), &Map.fromPairs(&process)) ?19  Ok(parsed.entries.map(|entry: Layers.Entry| (entry.key, entry.value)))20}2122/// The pairs the text assigns under the default rules.23fn simple(text: Str) -> Result[Array[(Str, Str)], Parser.Unclosed] { read(text, plain(), []) }2425/// Runs the suite.26fn main() -> Int {27  let expanding = Parser.Rules{..plain(), expansion: true}28  let keeping = Parser.Rules{..plain(), overwrite: false}29  let known = Parser.parse(&["A=1", "A=2", "B=3"], &keeping, Map.empty(), &Map.empty())30  let threaded = Parser.parse(&["B=$A"], &expanding, Map.fromPairs(&[("A", "earlier")]), &Map.empty())31  let checks = Test.suite("Domain.Parser", &[32      Test.equals("a plain assignment is read", &simple("KEY=value"), &Ok([("KEY", "value")])),33      Test.equals("the key is trimmed and the value kept as written", &simple("  KEY  = value "), &Ok([("KEY", " value ")])),34      Test.equals("blank lines and comments are skipped", &simple("\n   \n# A=1\n   # B=2\nC=3"), &Ok([("C", "3")])),35      Test.equals("a line without = is skipped", &simple("NOTHING\nK=v"), &Ok([("K", "v")])),36      Test.equals("a line starting with = is skipped", &simple("=v\nK=v"), &Ok([("K", "v")])),37      Test.equals("a key of one character is read", &simple("K=v"), &Ok([("K", "v")])),38      Test.equals("an empty key is skipped", &simple("   =v"), &Ok([])),39      Test.equals("the value runs to the end after the first =", &simple("URL=a=b=c"), &Ok([("URL", "a=b=c")])),40      Test.equals("an empty value is kept", &simple("EMPTY="), &Ok([("EMPTY", "")])),41      Test.equals("an inline comment after a space is dropped", &simple("K=value # note"), &Ok([("K", "value")])),42      Test.equals("an inline comment after a tab is dropped", &simple("K=value\t# note"), &Ok([("K", "value")])),43      Test.equals("a # without space before it is kept", &simple("K=a#b"), &Ok([("K", "a#b")])),44      Test.equals("a # after the value with a space after it is kept", &simple("K=a# b"), &Ok([("K", "a# b")])),45      Test.equals("a # after a space with text right after it is a comment", &simple("K=a #b"), &Ok([("K", "a")])),46      Test.equals("an empty double-quoted value is read", &simple("K=\"\"\nNEXT=1"), &Ok([("K", ""), ("NEXT", "1")])),47      Test.equals("an empty single-quoted value is read", &simple("K=''"), &Ok([("K", "")])),48      Test.equals("a quote after three backslashes is escaped", &simple("K=\"a\\\\\\\"b\""), &Ok([("K", "a\\\"b")])),49      Test.equals("a leading # is kept", &simple("K=#b"), &Ok([("K", "#b")])),50      Test.equals("a # right after the value's first character is a comment", &simple("K= # c"), &Ok([("K", "")])),51      Test.equals("inline comments may be turned off", &read("K=value # note", Parser.Rules{..plain(), inlineComments: false}, []), &Ok([("K", "value # note")])),52      Test.equals("a double-quoted value loses its quotes", &simple("K=\"quoted value\""), &Ok([("K", "quoted value")])),53      Test.equals("a single-quoted value loses its quotes", &simple("K='quoted'"), &Ok([("K", "quoted")])),54      Test.equals("space before the opening quote is ignored", &simple("K=   'q'"), &Ok([("K", "q")])),55      Test.equals("text after the closing quote is ignored", &simple("K=\"q\" # comment"), &Ok([("K", "q")])),56      Test.equals("a # inside quotes is kept", &simple("K=\"a # b\""), &Ok([("K", "a # b")])),57      Test.equals("an escaped quote is kept", &simple("K=\"say \\\"hi\\\"\""), &Ok([("K", "say \"hi\"")])),58      Test.equals("an escaped single quote is kept", &simple("K='it\\'s'"), &Ok([("K", "it's")])),59      Test.equals("a doubled backslash is one", &simple("K=\"a\\\\b\""), &Ok([("K", "a\\b")])),60      Test.equals("a quote after an escaped backslash closes", &simple("K=\"a\\\\\" tail"), &Ok([("K", "a\\")])),61      Test.equals("other backslashes are kept", &simple("K=\"C:\\new\\temp\""), &Ok([("K", "C:\\new\\temp")])),62      Test.equals("the other quote is literal", &simple("K=\"it's\""), &Ok([("K", "it's")])),63      Test.equals("a quoted value spans lines", &simple("K=\"first\nsecond\nthird\"\nNEXT=1"), &Ok([("K", "first\nsecond\nthird"), ("NEXT", "1")])),64      Test.equals("a single-quoted value spans lines", &simple("K='a\nb'"), &Ok([("K", "a\nb")])),65      Test.equals("a quote closing on the last line is read", &simple("K=\"a\nb\""), &Ok([("K", "a\nb")])),66      Test.equals("an unclosed quote names its line and key", &simple("A=1\nKEY=\"never\nclosed"), &Err(Parser.Unclosed{line: 2, key: "KEY"})),67      Test.equals("an unclosed quote on the last line is refused", &simple("KEY='open"), &Err(Parser.Unclosed{line: 1, key: "KEY"})),68      Test.equals("an unquoted value is not trimmed by default", &simple("K=  v  "), &Ok([("K", "  v  ")])),69      Test.equals("trimming applies to unquoted values", &read("K=  v  ", Parser.Rules{..plain(), trimValues: true}, []), &Ok([("K", "v")])),70      Test.equals("trimming applies to quoted values", &read("K=\"  v  \"", Parser.Rules{..plain(), trimValues: true}, []), &Ok([("K", "v")])),71      Test.equals("export is part of the key by default", &simple("export K=v"), &Ok([("export K", "v")])),72      Test.equals("export syntax drops the word", &read("export K=v", Parser.Rules{..plain(), exportSyntax: true}, []), &Ok([("K", "v")])),73      Test.equals("export syntax drops a word followed by a tab", &read("export\tK=v", Parser.Rules{..plain(), exportSyntax: true}, []), &Ok([("K", "v")])),74      Test.equals("export syntax keeps keys that begin with the word", &read("exporter=v\nexport=w", Parser.Rules{..plain(), exportSyntax: true}, []), &Ok([("exporter", "v"), ("export", "w")])),75      Test.equals("references stay as written without expansion", &read("A=1\nB=$A", plain(), []), &Ok([("A", "1"), ("B", "$A")])),76      Test.equals("an unquoted value expands", &read("A=1\nB=$A-x", expanding, []), &Ok([("A", "1"), ("B", "1-x")])),77      Test.equals("a double-quoted value expands", &read("A=1\nB=\"$\{A\}x\"", expanding, []), &Ok([("A", "1"), ("B", "1x")])),78      Test.equals("a single-quoted value never expands", &read("A=1\nB='$A'", expanding, []), &Ok([("A", "1"), ("B", "$A")])),79      Test.equals("a literal value stays literal when referenced", &read("A='$HOME'\nB=$A", expanding, [("HOME", "/root")]), &Ok([("A", "$HOME"), ("B", "$HOME")])),80      Test.equals("an escaped dollar in double quotes is literal", &read("B=\"\\$A\"", expanding, [("A", "x")]), &Ok([("B", "$A")])),81      Test.equals("the process answers names the file did not set", &read("B=$HOME", expanding, [("HOME", "/root")]), &Ok([("B", "/root")])),82      Test.equals("an earlier assignment hides the process value", &read("HOME=/mine\nB=$HOME", expanding, [("HOME", "/root")]), &Ok([("HOME", "/mine"), ("B", "/mine")])),83      Test.equals("an inline comment is dropped before expansion", &read("A=1\nB=$A # $A", expanding, []), &Ok([("A", "1"), ("B", "1")])),84      Test.equals("without overwriting the first value is what later expansions see", &read("A=1\nA=2\nB=$A", Parser.Rules{..expanding, overwrite: false}, []), &Ok([("A", "1"), ("A", "2"), ("B", "1")])),85      Test.equals("with overwriting the latest value is what later expansions see", &read("A=1\nA=2\nB=$A", expanding, []), &Ok([("A", "1"), ("A", "2"), ("B", "2")])),86      Test.equals("known values grow by the overwrite rule", &Result.map(known, |parsed: Parser.Parsed| parsed.known), &Ok(Map.fromPairs(&[("A", "1"), ("B", "3")]))),87      Test.equals("known values from an earlier source are seen", &Result.map(threaded, |parsed: Parser.Parsed| parsed.entries), &Ok([Layers.Entry{key: "B", value: "earlier"}])),88      Test.equals("CRLF lines are read", &simple("A=1\r\nB=2\r\n"), &Ok([("A", "1"), ("B", "2")])),89      Test.equals("lines split on LF and CRLF", &Parser.lines("a\r\nb\nc\r"), &["a", "b", "c"]),90      Test.equals("a carriage return inside a line is kept", &Parser.lines("a\rb"), &["a\rb"]),91      Test.equals("a trailing line break leaves an empty last line", &Parser.lines("a\n"), &["a", ""]),92      Test.equals("a leading byte-order mark is dropped", &Parser.lines("\u{FEFF}A=1\nB=2"), &["A=1", "B=2"]),93      Test.equals("a byte-order mark later in the text is kept", &Parser.lines("A=\u{FEFF}"), &["A=\u{FEFF}"]),94      Test.equals("empty text is one empty line", &Parser.lines(""), &[""])95    ])96  let ran = Test.run(&checks)97  for failure in Test.failuresOf(&ran) {98    let _reported = Io.writeErrorLine(failure)99  }100  Test.report(&ran)101}102