A structured-data shell in Gleam, inspired by Nushell
0

Configure Feed

Select the types of activity you want to include in your feed.

gleshell / test / gleshell_test.gleam
55 kB 1615 lines
1import gleam/int 2import gleam/list 3import gleam/option.{None, Some} 4import gleam/string 5import gleeunit 6import gleshell/builtins 7import gleshell/color 8import gleshell/display 9import gleshell/env 10import gleshell/eval 11import gleshell/highlight 12import gleshell/lexer 13import gleshell/pager 14import gleshell/parser 15import gleshell/syntax 16import gleshell/sys 17import gleshell/value.{Bool, Int, List, Nothing, Record, String, Table} 18import simplifile 19 20pub fn main() -> Nil { 21 gleeunit.main() 22} 23 24// --- lexer --- 25 26pub fn lexer_pipeline_test() { 27 let assert Ok(tokens) = lexer.tokenize("ls | where type == file") 28 let assert [lexer.Ident("ls"), lexer.Pipe, lexer.Ident("where"), ..] = tokens 29 Nil 30} 31 32pub fn lexer_string_and_number_test() { 33 let assert Ok(tokens) = lexer.tokenize("echo \"hi\" 42 3.14 true") 34 let assert [ 35 lexer.Ident("echo"), 36 lexer.StringLit("hi"), 37 lexer.IntLit(42), 38 lexer.FloatLit(3.14), 39 lexer.BoolLit(True), 40 lexer.Eof, 41 ] = tokens 42 Nil 43} 44 45pub fn lexer_path_idents_test() { 46 // Dotfiles, relative/absolute paths, home — must be bare words for `ls .jj` etc. 47 let assert Ok(tokens) = lexer.tokenize("ls .jj ./src ../foo /tmp ~ ~/code") 48 let assert [ 49 lexer.Ident("ls"), 50 lexer.Ident(".jj"), 51 lexer.Ident("./src"), 52 lexer.Ident("../foo"), 53 lexer.Ident("/tmp"), 54 lexer.Ident("~"), 55 lexer.Ident("~/code"), 56 lexer.Eof, 57 ] = tokens 58 Nil 59} 60 61pub fn lexer_bare_double_dash_test() { 62 // POSIX end-of-options: `nix run . -- args` must not lex-error on bare `--`. 63 let assert Ok(tokens) = lexer.tokenize("nix run . -- chadfowler.com yolo") 64 let assert [ 65 lexer.Ident("nix"), 66 lexer.Ident("run"), 67 lexer.Ident("."), 68 lexer.Flag(""), 69 lexer.Ident("chadfowler.com"), 70 lexer.Ident("yolo"), 71 lexer.Eof, 72 ] = tokens 73 // Named long flags still work 74 let assert Ok(tokens2) = lexer.tokenize("cmd --think --scale 3") 75 let assert [ 76 lexer.Ident("cmd"), 77 lexer.Flag("think"), 78 lexer.Flag("scale"), 79 lexer.IntLit(3), 80 lexer.Eof, 81 ] = tokens2 82 Nil 83} 84 85pub fn lexer_flake_ref_hash_test() { 86 // Mid-token `#` is part of the bareword (nix flake refs), not a comment. 87 let assert Ok(tokens) = lexer.tokenize("nix run nixpkgs#hello .#package") 88 let assert [ 89 lexer.Ident("nix"), 90 lexer.Ident("run"), 91 lexer.Ident("nixpkgs#hello"), 92 lexer.Ident(".#package"), 93 lexer.Eof, 94 ] = tokens 95 // Leading `#` at a word boundary still starts a line comment. 96 let assert Ok(tokens2) = lexer.tokenize("echo hi # not parsed") 97 let assert [ 98 lexer.Ident("echo"), 99 lexer.Ident("hi"), 100 lexer.Eof, 101 ] = tokens2 102 Nil 103} 104 105pub fn parse_flake_ref_hash_test() { 106 let assert Ok(parser.Expr(parser.Pipeline([ 107 parser.Command("nix", args, False), 108 ]))) = parser.parse("nix shell nixpkgs#cowsay") 109 let assert [ 110 parser.ValueArg(parser.Lit(String("shell"))), 111 parser.ValueArg(parser.Lit(String("nixpkgs#cowsay"))), 112 ] = args 113 Nil 114} 115 116pub fn parse_bare_double_dash_test() { 117 let assert Ok(parser.Expr(parser.Pipeline([ 118 parser.Command("nix", args, False), 119 ]))) = parser.parse("nix run . -- chadfowler.com yolo") 120 let assert [ 121 parser.ValueArg(parser.Lit(String("run"))), 122 parser.ValueArg(parser.Lit(String("."))), 123 parser.ValueArg(parser.Lit(String("--"))), 124 parser.ValueArg(parser.Lit(String("chadfowler.com"))), 125 parser.ValueArg(parser.Lit(String("yolo"))), 126 ] = args 127 Nil 128} 129 130pub fn parse_ls_dotfile_test() { 131 let assert Ok(parser.Expr(parser.Pipeline([ 132 parser.Command("ls", args, False), 133 ]))) = parser.parse("ls .jj") 134 let assert [parser.ValueArg(parser.Lit(String(".jj")))] = args 135 Nil 136} 137 138pub fn parse_port_spec_arg_test() { 139 // `lsof -i :4004` — colon is a record token but must be a bare argv word here. 140 let assert Ok(parser.Expr(parser.Pipeline([ 141 parser.Command("lsof", args, False), 142 ]))) = parser.parse("lsof -i :4004") 143 let assert [ 144 parser.FlagArg("i", parser.None), 145 parser.ValueArg(parser.Lit(String(":4004"))), 146 ] = args 147 // No space: `lsof -i:4004` 148 let assert Ok(parser.Expr(parser.Pipeline([ 149 parser.Command("lsof", args2, False), 150 ]))) = parser.parse("lsof -i:4004") 151 let assert [ 152 parser.FlagArg("i", parser.None), 153 parser.ValueArg(parser.Lit(String(":4004"))), 154 ] = args2 155 // Glued host:port and URL-shaped words 156 let assert Ok(parser.Expr(parser.Pipeline([ 157 parser.Command("echo", args3, False), 158 ]))) = parser.parse("echo host:4004 http://example.com") 159 let assert [ 160 parser.ValueArg(parser.Lit(String("host:4004"))), 161 parser.ValueArg(parser.Lit(String("http://example.com"))), 162 ] = args3 163 Nil 164} 165 166// --- parser --- 167 168pub fn parse_pipeline_test() { 169 let assert Ok(parser.Expr(parser.Pipeline(cmds))) = 170 parser.parse("ls | first 3") 171 let assert [ 172 parser.Command("ls", [], False), 173 parser.Command("first", args, False), 174 ] = cmds 175 let assert [parser.ValueArg(parser.Lit(Int(3)))] = args 176 Nil 177} 178 179pub fn parse_let_test() { 180 let assert Ok(parser.Let( 181 "x", 182 parser.Pipeline([parser.Command("echo", args, False)]), 183 )) = parser.parse("let x = echo 1") 184 let assert [parser.ValueArg(parser.Lit(Int(1)))] = args 185 Nil 186} 187 188pub fn parse_where_ops_test() { 189 let assert Ok(parser.Expr(parser.Pipeline([ 190 parser.Command("where", args, False), 191 ]))) = parser.parse("where type == file") 192 let assert [ 193 parser.ValueArg(parser.Lit(String("type"))), 194 parser.ValueArg(parser.Lit(String("=="))), 195 parser.ValueArg(parser.Lit(String("file"))), 196 ] = args 197 Nil 198} 199 200pub fn parse_list_and_record_test() { 201 let assert Ok(parser.Expr(parser.Pipeline([ 202 parser.Command("echo", args, False), 203 ]))) = parser.parse("echo [1 2] {a: true}") 204 let assert [ 205 parser.ValueArg(parser.ListExpr([parser.Lit(Int(1)), parser.Lit(Int(2))])), 206 parser.ValueArg(parser.RecordExpr([#("a", parser.Lit(Bool(True)))])), 207 ] = args 208 Nil 209} 210 211// --- value helpers --- 212 213pub fn table_from_records_test() { 214 let rows = [ 215 Record([#("name", String("a")), #("n", Int(1))]), 216 Record([#("name", String("b")), #("n", Int(2))]), 217 ] 218 let assert Table( 219 ["name", "n"], 220 [[String("a"), Int(1)], [String("b"), Int(2)]], 221 ) = value.table_from_records(rows) 222 Nil 223} 224 225// --- eval --- 226 227pub fn eval_echo_and_range_test() { 228 let env = env.new() 229 let assert eval.Continue(_, List([Int(0), Int(1), Int(2)])) = 230 eval.eval_source(env, "range 3") 231 let assert eval.Continue(_, String("hello")) = 232 eval.eval_source(env, "echo hello") 233 Nil 234} 235 236pub fn eval_pipeline_reverse_first_test() { 237 let env = env.new() 238 let assert eval.Continue(_, Int(2)) = 239 eval.eval_source(env, "range 3 | reverse | first") 240 Nil 241} 242 243/// Pipeline input must become the external's stdin (`cat f | less`, `echo hi | wc`). 244/// Use `let` so the last stage is capture mode even on a TTY (bare expressions 245/// may inherit the terminal and return an empty string value). 246pub fn eval_pipeline_stdin_to_external_test() { 247 let env = env.new() 248 // `echo` is a builtin; `wc` is external. Count bytes of "hello" (no trailing NL). 249 let assert eval.Continue(_, String(out)) = 250 eval.eval_source(env, "let n = echo hello | ^wc -c") 251 let assert True = string.contains(out, "5") 252 // Tight `cmd|cmd` form (no spaces around pipe) 253 let assert eval.Continue(_, String(out2)) = 254 eval.eval_source(env, "let m = echo ab|^wc -c") 255 let assert True = string.contains(out2, "2") 256 Nil 257} 258 259pub fn eval_let_and_var_test() { 260 let env = env.new() 261 let assert eval.Continue(env2, Int(7)) = 262 eval.eval_source(env, "let n = echo 7") 263 let assert eval.Continue(_, Int(7)) = eval.eval_source(env2, "echo $n") 264 Nil 265} 266 267pub fn eval_env_var_get_test() { 268 // `$env.HOME` and bare `$env.HOME` as pipeline source 269 let env = env.new() 270 let assert Ok(home) = sys.getenv("HOME") 271 let assert eval.Continue(_, String(got)) = 272 eval.eval_source(env, "echo $env.HOME") 273 let assert True = got == home 274 let assert eval.Continue(_, String(got2)) = eval.eval_source(env, "$env.HOME") 275 let assert True = got2 == home 276 Nil 277} 278 279pub fn eval_get_dotted_path_test() { 280 let env = env.new() 281 // Nested record: get a.b 282 let assert eval.Continue(_, String("ada")) = 283 eval.eval_source(env, "echo {user: {name: \"ada\"}} | get user.name") 284 // Deeper path 285 let assert eval.Continue(_, Int(42)) = 286 eval.eval_source(env, "echo {a: {b: {c: 42}}} | get a.b.c") 287 // Mid-path list: collect nested field from each item 288 let assert eval.Continue(_, List([String("x"), String("y")])) = 289 eval.eval_source( 290 env, 291 "echo {items: [{n: \"x\"}, {n: \"y\"}]} | get items.n", 292 ) 293 // Missing path errors on records 294 let assert eval.Continue(_, value.Fail(msg)) = 295 eval.eval_source(env, "echo {a: 1} | get a.b") 296 let assert True = string.contains(msg, "get:") 297 Nil 298} 299 300pub fn eval_env_record_test() { 301 let env = env.new() 302 let assert eval.Continue(_, Record(fields)) = eval.eval_source(env, "$env") 303 let assert True = list_has_string_field(fields, "HOME") 304 let assert True = list_has_string_field(fields, "PATH") 305 // `$env | get HOME` 306 let assert eval.Continue(_, String(home)) = 307 eval.eval_source(env, "$env | get HOME") 308 let assert True = string.length(home) > 0 309 Nil 310} 311 312pub fn eval_env_assign_test() { 313 let env = env.new() 314 let assert eval.Continue(env2, String("gleshell-test-val")) = 315 eval.eval_source(env, "$env.GLESHELL_TEST_VAR = gleshell-test-val") 316 let assert eval.Continue(_, String("gleshell-test-val")) = 317 eval.eval_source(env2, "$env.GLESHELL_TEST_VAR") 318 let assert Ok("gleshell-test-val") = sys.getenv("GLESHELL_TEST_VAR") 319 Nil 320} 321 322pub fn parse_env_assign_test() { 323 let assert Ok(parser.EnvAssign("FOO", parser.Pipeline([cmd]))) = 324 parser.parse("$env.FOO = hello") 325 let assert parser.Command( 326 "__value__", 327 [parser.ValueArg(parser.Lit(String("hello")))], 328 False, 329 ) = cmd 330 Nil 331} 332 333fn list_has_string_field( 334 fields: List(#(String, value.Value)), 335 key: String, 336) -> Bool { 337 case fields { 338 [] -> False 339 [#(k, String(_)), ..] if k == key -> True 340 [_, ..rest] -> list_has_string_field(rest, key) 341 } 342} 343 344pub fn eval_where_select_test() { 345 let env = env.new() 346 // build table via records in a list, convert with table 347 let assert eval.Continue(_, result) = 348 eval.eval_source( 349 env, 350 "echo [{name: a, n: 1} {name: b, n: 2} {name: c, n: 3}] | table | where n > 1 | select name", 351 ) 352 let assert Table(["name"], rows) = result 353 let assert [[String("b")], [String("c")]] = rows 354 Nil 355} 356 357pub fn about_command_test() { 358 let env = env.new() 359 let assert eval.Continue(_, String(text)) = eval.eval_source(env, "about") 360 let assert True = string.contains(text, "gleshell") 361 let assert True = string.contains(text, "nandi.uk") 362 let assert True = string.contains(text, "NaNdi") 363 let assert True = string.contains(text, "did:plc:ngokl2gnmpbvuvrfckja3g7p") 364 let assert True = string.contains(text, "latha.org") 365 let assert eval.Continue(_, String(which_out)) = 366 eval.eval_source(env, "which about") 367 let assert "builtin: about" = which_out 368 Nil 369} 370 371pub fn help_covers_all_builtins_test() { 372 // Every registered builtin must have a dedicated help_text entry. 373 let assert [] = builtins.missing_help() 374 375 let env = env.new() 376 // which finds builtins; help must too (not "unknown command") 377 let assert eval.Continue(_, String(which_out)) = 378 eval.eval_source(env, "which table") 379 let assert "builtin: table" = which_out 380 let assert eval.Continue(_, String(help_out)) = 381 eval.eval_source(env, "help table") 382 let assert True = string.contains(help_out, "table") 383 let assert True = string.contains(help_out, "coerce") 384 385 // Parent commands with subcommands: `help to` / `help from` / `help http`. 386 let assert eval.Continue(_, String(to_help)) = 387 eval.eval_source(env, "help to") 388 let assert True = string.contains(to_help, "json") 389 let assert eval.Continue(_, String(from_help)) = 390 eval.eval_source(env, "help from") 391 let assert True = string.contains(from_help, "json") 392 let assert eval.Continue(_, String(http_help)) = 393 eval.eval_source(env, "help http") 394 let assert True = string.contains(http_help, "get") 395 let assert True = string.contains(http_help, "post") 396 397 // Bare help lists every command with its one-line description. 398 let assert eval.Continue(_, String(all)) = eval.eval_source(env, "help") 399 list.each(builtins.names(), fn(name) { 400 let assert True = string.contains(all, name) 401 }) 402 403 let assert eval.Continue(env2, value.Fail(msg)) = 404 eval.eval_source(env, "help not-a-real-cmd") 405 let assert True = string.contains(msg, "unknown command") 406 let assert 1 = env2.last_exit 407 Nil 408} 409 410pub fn eval_from_json_test() { 411 let env = env.new() 412 let assert eval.Continue(_, Record(fields)) = 413 eval.eval_source(env, "echo \"{\\\"x\\\": 1}\" | from json") 414 let assert True = list_has_field(fields, "x", Int(1)) 415 Nil 416} 417 418pub fn eval_from_jwt_test() { 419 let env = env.new() 420 // Classic jwt.io sample (HS256); parse-only — signature not verified. 421 let token = 422 "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" 423 424 let assert eval.Continue(_, Record(fields)) = 425 eval.eval_source(env, "echo \"" <> token <> "\" | from jwt") 426 427 // header 428 let assert Ok(Record(header_fields)) = list_find_field(fields, "header") 429 let assert True = list_has_field(header_fields, "alg", String("HS256")) 430 let assert True = list_has_field(header_fields, "typ", String("JWT")) 431 432 // payload claims 433 let assert Ok(Record(payload_fields)) = list_find_field(fields, "payload") 434 let assert True = 435 list_has_field(payload_fields, "sub", String("1234567890")) 436 let assert True = 437 list_has_field(payload_fields, "name", String("John Doe")) 438 let assert True = list_has_field(payload_fields, "iat", Int(1_516_239_022)) 439 440 // signature segment preserved as base64url text 441 let assert Ok(String(sig)) = list_find_field(fields, "signature") 442 let assert "SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" = sig 443 444 // Argument form + Bearer prefix 445 let assert eval.Continue(_, Record(_)) = 446 eval.eval_source(env, "from jwt \"Bearer " <> token <> "\"") 447 448 // Nested get (pipeline and dotted path) 449 let assert eval.Continue(_, String("HS256")) = 450 eval.eval_source( 451 env, 452 "echo \"" <> token <> "\" | from jwt | get header | get alg", 453 ) 454 let assert eval.Continue(_, String("HS256")) = 455 eval.eval_source( 456 env, 457 "echo \"" <> token <> "\" | from jwt | get header.alg", 458 ) 459 460 // Errors 461 let assert eval.Continue(env2, value.Fail(msg)) = 462 eval.eval_source(env, "echo not-a-jwt | from jwt") 463 let assert True = 464 string.contains(msg, "segments") || string.contains(msg, "base64") 465 let assert 1 = env2.last_exit 466 467 let assert eval.Continue(_, value.Fail(empty_msg)) = 468 eval.eval_source(env, "echo \"\" | from jwt") 469 let assert True = string.contains(empty_msg, "empty") 470 471 let assert eval.Continue(_, String(help_out)) = 472 eval.eval_source(env, "help from") 473 let assert True = string.contains(help_out, "jwt") 474 Nil 475} 476 477pub fn eval_to_json_pretty_test() { 478 let env = env.new() 479 // `to` command + `json` subcommand — pretty by default 480 let assert eval.Continue(_, String(pretty)) = 481 eval.eval_source(env, "echo [1 2 3] | to json") 482 let assert True = string.contains(pretty, "\n") 483 let assert True = string.contains(pretty, "1") 484 // `--raw` matches Nu: compact, no trailing newline 485 let assert eval.Continue(_, String(raw)) = 486 eval.eval_source(env, "echo [1 2 3] | to json --raw") 487 let assert "[1,2,3]" = raw 488 // Missing / unknown subcommand 489 let assert eval.Continue(env2, value.Fail(msg)) = 490 eval.eval_source(env, "echo 1 | to") 491 let assert True = string.contains(msg, "subcommand") 492 let assert 1 = env2.last_exit 493 let assert eval.Continue(_, value.Fail(msg2)) = 494 eval.eval_source(env, "echo 1 | to yaml") 495 let assert True = string.contains(msg2, "unknown subcommand") 496 Nil 497} 498 499pub fn eval_to_json_record_test() { 500 let env = env.new() 501 let assert eval.Continue(_, String(raw)) = 502 eval.eval_source(env, "echo {a: 1, b: true} | to json -r") 503 let assert True = string.contains(raw, "\"a\":1") 504 let assert True = string.contains(raw, "\"b\":true") 505 Nil 506} 507 508pub fn http_subcommand_errors_test() { 509 let env = env.new() 510 // Missing subcommand 511 let assert eval.Continue(env2, value.Fail(msg)) = 512 eval.eval_source(env, "http") 513 let assert True = string.contains(msg, "subcommand") 514 let assert 1 = env2.last_exit 515 // Unknown subcommand 516 let assert eval.Continue(_, value.Fail(msg2)) = 517 eval.eval_source(env, "http foo") 518 let assert True = string.contains(msg2, "unknown subcommand") 519 // Missing URL 520 let assert eval.Continue(_, value.Fail(msg3)) = 521 eval.eval_source(env, "http get") 522 let assert True = string.contains(msg3, "URL") || string.contains(msg3, "url") 523 // Invalid URL 524 let assert eval.Continue(_, value.Fail(msg4)) = 525 eval.eval_source(env, "http get not-a-url") 526 let assert True = string.contains(msg4, "invalid URL") 527 // which / help 528 let assert eval.Continue(_, String(which_out)) = 529 eval.eval_source(env, "which http") 530 let assert "builtin: http" = which_out 531 Nil 532} 533 534pub fn eval_ps_test() { 535 let env = env.new() 536 // Default columns match Nushell short `ps`. 537 let assert eval.Continue(_, Table(cols, rows)) = eval.eval_source(env, "ps") 538 let assert [ 539 "pid", 540 "ppid", 541 "name", 542 "status", 543 "cpu", 544 "mem", 545 "virtual", 546 ] = cols 547 let assert True = rows != [] 548 549 // pid 1 exists on Linux 550 let assert eval.Continue(_, Table(_, pid1_rows)) = 551 eval.eval_source(env, "ps | where pid == 1") 552 let assert True = pid1_rows != [] 553 554 // --long adds the extra Nu columns 555 let assert eval.Continue(_, Table(long_cols, _)) = 556 eval.eval_source(env, "ps --long") 557 let assert True = list.contains(long_cols, "command") 558 let assert True = list.contains(long_cols, "start_time") 559 let assert True = list.contains(long_cols, "cwd") 560 let assert True = list.length(long_cols) > list.length(cols) 561 562 // which / help 563 let assert eval.Continue(_, String("builtin: ps")) = 564 eval.eval_source(env, "which ps") 565 let assert eval.Continue(_, String(help_out)) = 566 eval.eval_source(env, "help ps") 567 let assert True = string.contains(help_out, "--long") 568 Nil 569} 570 571pub fn eval_whyport_test() { 572 let env = env.new() 573 // Default: short listener columns only (no command / remote spam). 574 let assert eval.Continue(_, Table(cols, _)) = 575 eval.eval_source(env, "whyport 1") 576 let assert ["protocol", "local_address", "local_port", "pid", "name"] = cols 577 578 // --all adds peer/state; --long adds command (and more). 579 let assert eval.Continue(_, Table(all_cols, _)) = 580 eval.eval_source(env, "whyport --all 1") 581 let assert True = list.contains(all_cols, "state") 582 let assert True = list.contains(all_cols, "remote_port") 583 let assert False = list.contains(all_cols, "command") 584 585 let assert eval.Continue(_, Table(long_cols, _)) = 586 eval.eval_source(env, "whyport --long 1") 587 let assert True = list.contains(long_cols, "command") 588 let assert True = list.contains(long_cols, "family") 589 let assert False = list.contains(long_cols, "state") 590 591 // Port with nothing listening: empty table with schema. 592 let assert eval.Continue(_, Table(_, unused_rows)) = 593 eval.eval_source(env, "whyport 1") 594 let assert [] = unused_rows 595 596 // epmd listens on 4369 when present — default is listeners only. 597 let assert eval.Continue(_, Table(_, epmd_rows)) = 598 eval.eval_source(env, "whyport 4369") 599 case epmd_rows { 600 [] -> Nil 601 _ -> { 602 // Every default row should be a listener (local_port 4369). 603 let assert eval.Continue(_, Int(n)) = 604 eval.eval_source(env, "whyport 4369 | length") 605 let assert True = n >= 1 606 // --all should not be smaller than listeners. 607 let assert eval.Continue(_, Int(all_n)) = 608 eval.eval_source(env, "whyport --all 4369 | length") 609 let assert True = all_n >= n 610 Nil 611 } 612 } 613 614 // Leading colon + pipeline + flag that steals the port value 615 let assert eval.Continue(_, Table(_, _)) = 616 eval.eval_source(env, "whyport :22") 617 let assert eval.Continue(_, Table(_, _)) = 618 eval.eval_source(env, "echo 22 | whyport") 619 let assert eval.Continue(_, Table(_, _)) = 620 eval.eval_source(env, "whyport --all 22") 621 622 // Errors 623 let assert eval.Continue(_, value.Fail(missing)) = 624 eval.eval_source(env, "whyport") 625 let assert True = string.contains(missing, "port") 626 let assert eval.Continue(_, value.Fail(bad)) = 627 eval.eval_source(env, "whyport notaport") 628 let assert True = string.contains(bad, "invalid") || string.contains(bad, "port") 629 let assert eval.Continue(_, value.Fail(range)) = 630 eval.eval_source(env, "whyport 99999") 631 let assert True = string.contains(range, "range") 632 633 let assert eval.Continue(_, String("builtin: whyport")) = 634 eval.eval_source(env, "which whyport") 635 let assert eval.Continue(_, String(help_out)) = 636 eval.eval_source(env, "help whyport") 637 let assert True = string.contains(help_out, "--all") 638 let assert True = string.contains(help_out, "--long") 639 Nil 640} 641 642pub fn eval_now_test() { 643 let env = env.new() 644 // Data is Unix epoch seconds (same as ls modified); near wall clock. 645 let assert eval.Continue(_, Int(secs)) = eval.eval_source(env, "now") 646 let wall = sys.unix_now() 647 let assert True = secs > 1_700_000_000 648 let assert True = secs <= wall + 2 && secs >= wall - 5 649 650 // typeof is int 651 let assert eval.Continue(_, String("int")) = 652 eval.eval_source(env, "now | typeof") 653 654 // Display formats bare epoch ints like ls modified (local 12-hour). 655 let text = display.render_with(False, Int(secs)) 656 let assert False = string.contains(text, int.to_string(secs)) 657 let assert True = string.contains(text, ":") 658 let assert True = 659 string.contains(text, " AM") || string.contains(text, " PM") 660 661 // Small ints still print as numbers 662 let assert True = string.contains(display.render_with(False, Int(42)), "42") 663 664 let assert eval.Continue(_, String("builtin: now")) = 665 eval.eval_source(env, "which now") 666 let assert eval.Continue(_, String(help_out)) = 667 eval.eval_source(env, "help now") 668 let assert True = string.contains(help_out, "epoch") 669 Nil 670} 671 672pub fn eval_input_help_test() { 673 let env = env.new() 674 let assert eval.Continue(_, String("builtin: input")) = 675 eval.eval_source(env, "which input") 676 let assert eval.Continue(_, String(help_out)) = 677 eval.eval_source(env, "help input") 678 let assert True = string.contains(help_out, "Ctrl+D") 679 let assert True = string.contains(help_out, "from json") 680 // Too many args without reading stdin 681 let assert eval.Continue(_, value.Fail(msg)) = 682 eval.eval_source(env, "input too many args") 683 let assert True = string.contains(msg, "prompt") 684 Nil 685} 686 687 688pub fn http_get_live_test() { 689 // Live request against postman-echo (JSON). Skip gracefully if offline. 690 let env = env.new() 691 case eval.eval_source(env, "http get --full https://postman-echo.com/get") { 692 eval.Continue(_, Record(fields)) -> { 693 let assert True = list_has_field(fields, "status", Int(200)) 694 let assert True = list_has_key(fields, "body") 695 let assert True = list_has_key(fields, "headers") 696 let assert True = 697 list_has_field(fields, "url", String("https://postman-echo.com/get")) 698 // Default path (no --full) parses JSON body into a record 699 let assert eval.Continue(_, Record(body_fields)) = 700 eval.eval_source(env, "http get https://postman-echo.com/get") 701 let assert True = list_has_key(body_fields, "url") 702 Nil 703 } 704 eval.Continue(_, value.Fail(msg)) -> { 705 // Network unavailable — still assert the error is from http, not parse 706 let assert True = 707 string.contains(msg, "http:") || string.contains(msg, "failed") 708 Nil 709 } 710 _ -> panic as "http get --full: unexpected eval result" 711 } 712} 713 714pub fn http_post_json_live_test() { 715 let env = env.new() 716 case 717 eval.eval_source( 718 env, 719 "http post --full https://postman-echo.com/post {name: gleshell}", 720 ) 721 { 722 eval.Continue(_, Record(fields)) -> { 723 let assert True = list_has_field(fields, "status", Int(200)) 724 // JSON body should be parsed; postman-echo echoes under `json` 725 case list_find_field(fields, "body") { 726 Ok(Record(body)) -> { 727 case list_find_field(body, "json") { 728 Ok(Record(json_fields)) -> { 729 let assert True = 730 list_has_field(json_fields, "name", String("gleshell")) 731 Nil 732 } 733 _ -> Nil 734 } 735 } 736 _ -> Nil 737 } 738 } 739 eval.Continue(_, value.Fail(_)) -> Nil 740 _ -> panic as "http post --full: unexpected eval result" 741 } 742} 743 744fn list_has_key(fields: List(#(String, value.Value)), key: String) -> Bool { 745 case fields { 746 [] -> False 747 [#(k, _), ..rest] -> 748 case k == key { 749 True -> True 750 False -> list_has_key(rest, key) 751 } 752 } 753} 754 755fn list_find_field( 756 fields: List(#(String, value.Value)), 757 key: String, 758) -> Result(value.Value, Nil) { 759 case fields { 760 [] -> Error(Nil) 761 [#(k, v), ..rest] -> 762 case k == key { 763 True -> Ok(v) 764 False -> list_find_field(rest, key) 765 } 766 } 767} 768 769fn list_has_field( 770 fields: List(#(String, value.Value)), 771 key: String, 772 expected: value.Value, 773) -> Bool { 774 case fields { 775 [] -> False 776 [#(k, v), ..rest] -> 777 case k == key { 778 True -> value.equals(v, expected) 779 False -> list_has_field(rest, key, expected) 780 } 781 } 782} 783 784pub fn eval_length_test() { 785 let env = env.new() 786 let assert eval.Continue(_, Int(4)) = 787 eval.eval_source(env, "range 4 | length") 788 Nil 789} 790 791pub fn eval_which_builtin_test() { 792 let env = env.new() 793 let assert eval.Continue(_, String("builtin: ls")) = 794 eval.eval_source(env, "which ls") 795 Nil 796} 797 798pub fn eval_find_list_test() { 799 let env = env.new() 800 // Substring match (OR across terms) 801 let assert eval.Continue(_, List(items)) = 802 eval.eval_source(env, "echo [moe larry curly] | find l") 803 let assert [String("larry"), String("curly")] = items 804 // Multiple terms 805 let assert eval.Continue(_, List(items2)) = 806 eval.eval_source(env, "echo [a.toml b.md c.rs] | find toml md") 807 let assert [String("a.toml"), String("b.md")] = items2 808 // Exact number match 809 let assert eval.Continue(_, List([Int(5)])) = 810 eval.eval_source(env, "echo [1 5 3 4 35] | find 5") 811 Nil 812} 813 814pub fn eval_find_ignore_case_invert_test() { 815 let env = env.new() 816 let assert eval.Continue(_, List(items)) = 817 eval.eval_source(env, "echo [Hello world HELLO] | find hello -i") 818 let assert [String("Hello"), String("HELLO")] = items 819 // `-i term` may attach term to the flag; still works 820 let assert eval.Continue(_, List(items2)) = 821 eval.eval_source(env, "echo [Hello world HELLO] | find -i hello") 822 let assert [String("Hello"), String("HELLO")] = items2 823 let assert eval.Continue(_, List([String("cd")])) = 824 eval.eval_source(env, "echo [ab cd] | find --invert a") 825 Nil 826} 827 828pub fn eval_find_table_and_string_test() { 829 let env = env.new() 830 let assert eval.Continue(_, Table(["name", "type"], rows)) = 831 eval.eval_source( 832 env, 833 "echo [{name: a, type: file} {name: b, type: dir}] | table | find file", 834 ) 835 let assert [[String("a"), String("file")]] = rows 836 // Single-line string: return the string if match, else nothing 837 let assert eval.Continue(_, String("Cargo.toml")) = 838 eval.eval_source(env, "echo Cargo.toml | find Cargo") 839 let assert eval.Continue(_, Nothing) = 840 eval.eval_source(env, "echo Cargo.toml | find zz") 841 // Multi-line string → list of matching lines 842 let assert eval.Continue(_, List(lines)) = 843 eval.eval_source(env, "echo \"hi\nbye\nhi there\" | find hi") 844 let assert [String("hi"), String("hi there")] = lines 845 Nil 846} 847 848pub fn eval_find_regex_test() { 849 let env = env.new() 850 let assert eval.Continue(_, List(items)) = 851 eval.eval_source(env, "echo [abc odb arc abf] | find --regex \"b.\"") 852 let assert [String("abc"), String("abf")] = items 853 // Regex + terms is rejected (Nu-compatible) 854 let assert eval.Continue(env2, value.Fail(msg)) = 855 eval.eval_source(env, "echo [abc] | find --regex \"b.\" x") 856 let assert True = string.contains(msg, "regex") 857 let assert 1 = env2.last_exit 858 Nil 859} 860 861pub fn eval_which_all_test() { 862 let env = env.new() 863 // `-a` includes the builtin first, then any PATH copies of `ls`. 864 let assert eval.Continue(_, result) = eval.eval_source(env, "which -a ls") 865 case result { 866 String("builtin: ls") -> Nil 867 List([String("builtin: ls"), ..]) -> Nil 868 other -> { 869 let _ = other 870 panic as "which -a ls should start with builtin: ls" 871 } 872 } 873} 874 875pub fn eval_which_external_test() { 876 let env = env.new() 877 // `sh` is not a gleshell builtin; should resolve via PATH. 878 let assert eval.Continue(_, String(path)) = eval.eval_source(env, "which sh") 879 let assert True = string.contains(path, "sh") 880 Nil 881} 882 883pub fn eval_which_follow_test() { 884 let env = env.new() 885 // `-f` follows symlinks to a canonical path (NixOS: sh → bash store path). 886 let assert eval.Continue(_, String(plain)) = eval.eval_source(env, "which sh") 887 let assert eval.Continue(_, String(followed)) = 888 eval.eval_source(env, "which -f sh") 889 // Followed path is absolute and exists; if plain was a symlink, they differ. 890 let assert True = string.starts_with(followed, "/") 891 let assert True = string.length(followed) > 0 892 // Combined with -a: first entry for a non-builtin is still a real path. 893 let assert eval.Continue(_, result) = eval.eval_source(env, "which -a -f sh") 894 case result { 895 String(p) -> { 896 let assert True = string.starts_with(p, "/") 897 Nil 898 } 899 List([String(p), ..]) -> { 900 let assert True = string.starts_with(p, "/") 901 Nil 902 } 903 other -> { 904 let _ = other 905 panic as "which -a -f sh should yield a path" 906 } 907 } 908 let _ = plain 909 let _ = followed 910 Nil 911} 912 913pub fn value_nothing_falsey_test() { 914 let assert False = value.is_truthy(Nothing) 915 let assert True = value.is_truthy(Int(1)) 916 Nil 917} 918 919// --- display / color --- 920 921pub fn display_plain_no_ansi_test() { 922 let text = display.render_with(False, Bool(True)) 923 let assert "true" = text 924 let assert False = string_contains(text, "\u{001b}") 925 Nil 926} 927 928pub fn display_colored_has_ansi_test() { 929 let text = display.render_with(True, Bool(True)) 930 let assert True = string_contains(text, "\u{001b}") 931 let assert True = string_contains(text, "true") 932 Nil 933} 934 935pub fn display_preserves_external_ansi_test() { 936 // Pre-colored tool output (jj, git, …) must not be re-wrapped in string green. 937 let app = "\u{001b}[1;31merror\u{001b}[0m: boom" 938 let text = display.render_with(True, String(app)) 939 let assert True = text == app 940 Nil 941} 942 943pub fn display_multiline_string_not_recolored_test() { 944 let multi = "line1\nline2" 945 let text = display.render_with(True, String(multi)) 946 let assert True = text == multi 947 Nil 948} 949 950pub fn display_table_headers_colored_test() { 951 let text = 952 display.render_with( 953 True, 954 Table(["name", "type"], [[String("src"), String("dir")]]), 955 ) 956 // bold green header + bright-blue dir name 957 let assert True = string_contains(text, "\u{001b}[1;32m") 958 let assert True = string_contains(text, "\u{001b}[94m") 959 let assert True = string_contains(text, "src") 960 Nil 961} 962 963pub fn format_filesize_units_test() { 964 let assert "0 B" = display.format_filesize(0) 965 let assert "512 B" = display.format_filesize(512) 966 let assert "1023 B" = display.format_filesize(1023) 967 let assert "1 KB" = display.format_filesize(1024) 968 let assert "1.5 KB" = display.format_filesize(1536) 969 let assert "1 MB" = display.format_filesize(1_048_576) 970 let assert "1.5 MB" = display.format_filesize(1_048_576 + 524_288) 971 let assert "1 GB" = display.format_filesize(1_073_741_824) 972 Nil 973} 974 975pub fn display_size_column_humanized_test() { 976 // Data stays as raw bytes (Int); only display shows KB/MB. 977 let text = 978 display.render_with( 979 False, 980 Table(["name", "size"], [[String("a"), Int(2048)]]), 981 ) 982 let assert True = string_contains(text, "2 KB") 983 let assert False = string_contains(text, "2048") 984 Nil 985} 986 987pub fn format_datetime_shape_test() { 988 // Local 12-hour with abbr month: "Nov 14 2023 2:13:20 PM" (example shape). 989 let text = display.format_datetime(1_700_000_000) 990 let assert True = string_contains(text, ":") 991 let assert True = 992 string_contains(text, " AM") || string_contains(text, " PM") 993 let assert True = 994 string_contains(text, "Jan") 995 || string_contains(text, "Feb") 996 || string_contains(text, "Mar") 997 || string_contains(text, "Apr") 998 || string_contains(text, "May") 999 || string_contains(text, "Jun") 1000 || string_contains(text, "Jul") 1001 || string_contains(text, "Aug") 1002 || string_contains(text, "Sep") 1003 || string_contains(text, "Oct") 1004 || string_contains(text, "Nov") 1005 || string_contains(text, "Dec") 1006 Nil 1007} 1008 1009pub fn display_modified_column_humanized_test() { 1010 // Data stays as Unix seconds; only display shows a local 12-hour datetime. 1011 let text = 1012 display.render_with( 1013 False, 1014 Table(["name", "modified"], [[String("a"), Int(1_700_000_000)]]), 1015 ) 1016 let assert False = string_contains(text, "1700000000") 1017 let assert True = string_contains(text, ":") 1018 let assert True = 1019 string_contains(text, " AM") || string_contains(text, " PM") 1020 Nil 1021} 1022 1023pub fn ls_includes_modified_column_test() { 1024 let env = env.new() 1025 // `first` collapses a single row to a record. 1026 let assert eval.Continue(_, Record(fields)) = 1027 eval.eval_source(env, "ls | first 1") 1028 let assert True = list_has_key(fields, "name") 1029 let assert True = list_has_key(fields, "type") 1030 let assert True = list_has_key(fields, "size") 1031 let assert True = list_has_key(fields, "modified") 1032 // Pipeline data stays as Int (epoch seconds); display formats it. 1033 case list_find_field(fields, "modified") { 1034 Ok(Int(n)) if n >= 0 -> Nil 1035 _ -> panic as "ls modified should be non-negative Unix epoch seconds" 1036 } 1037} 1038 1039pub fn color_visible_length_strips_ansi_test() { 1040 let painted = color.paint(True, "\u{001b}[32m", "hi") 1041 let assert 2 = color.visible_length(painted) 1042 let assert 2 = color.visible_length("hi") 1043 Nil 1044} 1045 1046// --- less / pager --- 1047 1048pub fn pager_wrap_respects_ansi_width_test() { 1049 // 10 visible chars of content; wrap at 4 → three physical lines. 1050 let painted = color.paint(True, "\u{001b}[32m", "abcdefghij") 1051 let lines = pager.wrap_line(painted, 4) 1052 let assert 3 = list.length(lines) 1053 // Each physical line still carries / continues color; visible width ≤ 4. 1054 list.each(lines, fn(line) { 1055 let assert True = color.visible_length(line) <= 4 1056 }) 1057 // Joining without separators reconstructs the original SGR + text. 1058 let joined = string.join(lines, "") 1059 let assert True = string.contains(joined, "abcdefghij") 1060 let assert True = string.contains(joined, "\u{001b}[32m") 1061 Nil 1062} 1063 1064pub fn pager_display_lines_splits_newlines_test() { 1065 let lines = pager.display_lines("a\nb\nc", 80) 1066 let assert ["a", "b", "c"] = lines 1067 Nil 1068} 1069 1070pub fn strip_ansi_removes_csi_test() { 1071 let painted = color.paint(True, "\u{001b}[32m", "hello") 1072 let assert "hello" = color.strip_ansi(painted) 1073 let assert "plain" = color.strip_ansi("plain") 1074 let assert "ab" = color.strip_ansi("a\u{001b}[1;31mb\u{001b}[0m") 1075 Nil 1076} 1077 1078pub fn pager_line_matches_strips_ansi_test() { 1079 let painted = color.paint(True, "\u{001b}[31m", "needle") 1080 let assert True = pager.line_matches(painted, "needle") 1081 let assert True = pager.line_matches(painted, "eed") 1082 // Case-insensitive: mixed / upper pattern still hits. 1083 let assert True = pager.line_matches(painted, "NEEDLE") 1084 let assert True = pager.line_matches(painted, "NeEd") 1085 let assert False = pager.line_matches(painted, "") 1086 // Pattern must not match inside CSI itself. 1087 let assert False = pager.line_matches(painted, "[31m") 1088 Nil 1089} 1090 1091pub fn pager_find_after_and_before_test() { 1092 let lines = ["alpha", "bravo", "alpha", "charlie"] 1093 let assert Ok(#(0, False)) = pager.find_after(lines, "alpha", -1) 1094 let assert Ok(#(2, False)) = pager.find_after(lines, "alpha", 0) 1095 // Wrap from end back to first match. 1096 let assert Ok(#(0, True)) = pager.find_after(lines, "alpha", 2) 1097 let assert Error(Nil) = pager.find_after(lines, "zzz", -1) 1098 // Case-insensitive search. 1099 let assert Ok(#(0, False)) = pager.find_after(lines, "ALPHA", -1) 1100 let assert Ok(#(2, False)) = pager.find_after(lines, "AlPhA", 0) 1101 1102 let assert Ok(#(2, False)) = pager.find_before(lines, "alpha", 3) 1103 let assert Ok(#(0, False)) = pager.find_before(lines, "alpha", 2) 1104 // Wrap from start back to last match. 1105 let assert Ok(#(2, True)) = pager.find_before(lines, "alpha", 0) 1106 let assert Ok(#(2, False)) = pager.find_before(lines, "ALPHA", 3) 1107 Nil 1108} 1109 1110pub fn pager_find_empty_pattern_test() { 1111 let lines = ["a", "b"] 1112 let assert Error(Nil) = pager.find_after(lines, "", -1) 1113 let assert Error(Nil) = pager.find_before(lines, "", 2) 1114 Nil 1115} 1116 1117pub fn pager_live_search_preview_test() { 1118 let lines = ["alpha", "bravo", "charlie", "alpha again"] 1119 // Empty query: stay put, no highlight. 1120 let assert #(2, None, None) = pager.live_search_preview(lines, "", 2) 1121 // Inclusive of start_offset (match on current top line). 1122 let assert #(0, Some("alpha"), None) = 1123 pager.live_search_preview(lines, "alpha", 0) 1124 // From mid-buffer: first match at or after start. 1125 let assert #(3, Some("alpha"), None) = 1126 pager.live_search_preview(lines, "alpha", 1) 1127 // Not found: keep start offset, still paint pattern, status suffix. 1128 let assert #(1, Some("zzz"), Some("not found")) = 1129 pager.live_search_preview(lines, "zzz", 1) 1130 // Progressive typing narrows: "ch" → charlie. 1131 let assert #(2, Some("ch"), None) = pager.live_search_preview(lines, "ch", 0) 1132 // Case-insensitive: upper pattern still finds lower content. 1133 let assert #(0, Some("ALPHA"), None) = 1134 pager.live_search_preview(lines, "ALPHA", 0) 1135 let assert #(3, Some("Alpha"), None) = 1136 pager.live_search_preview(lines, "Alpha", 1) 1137 Nil 1138} 1139 1140pub fn pager_highlight_matches_test() { 1141 let out = pager.highlight_matches("hello world", "world") 1142 // Black on bright yellow accent. 1143 let assert True = string.contains(out, "\u{001b}[30;103m") 1144 let assert True = string.contains(out, "world") 1145 let assert True = string.contains(out, "\u{001b}[39;49m") 1146 let assert "hello world" = color.strip_ansi(out) 1147 1148 // Case-insensitive highlight preserves original casing in the line. 1149 let ci = pager.highlight_matches("Hello World", "WORLD") 1150 let assert True = string.contains(ci, "\u{001b}[30;103m") 1151 let assert True = string.contains(ci, "World") 1152 let assert "Hello World" = color.strip_ansi(ci) 1153 1154 // No match / empty pattern: unchanged. 1155 let assert "nope" = pager.highlight_matches("nope", "zzz") 1156 let assert "x" = pager.highlight_matches("x", "") 1157 1158 // Multiple non-overlapping hits. 1159 let multi = pager.highlight_matches("aa x aa", "aa") 1160 let parts = string.split(multi, "\u{001b}[30;103m") 1161 let assert 3 = list.length(parts) 1162 1163 // ANSI around the match is preserved; highlight still finds visible text. 1164 let painted = color.paint(True, "\u{001b}[32m", "needle here") 1165 let hi = pager.highlight_matches(painted, "needle") 1166 let assert True = string.contains(hi, "\u{001b}[32m") 1167 let assert True = string.contains(hi, "\u{001b}[30;103m") 1168 let assert "needle here" = color.strip_ansi(hi) 1169 let assert True = string.contains(hi, "\u{001b}[39;49m") 1170 Nil 1171} 1172 1173pub fn pager_highlight_match_spanning_sgr_test() { 1174 // Match crosses a mid-string color change: black-on-yellow re-opens after SGR. 1175 let line = "ab\u{001b}[31mcd\u{001b}[0mef" 1176 let hi = pager.highlight_matches(line, "bcde") 1177 let assert True = string.contains(hi, "\u{001b}[30;103m") 1178 let assert "abcdef" = color.strip_ansi(hi) 1179 // After the red open, accent should be re-applied inside the match. 1180 let assert True = string.contains(hi, "\u{001b}[31m\u{001b}[30;103m") 1181 Nil 1182} 1183 1184pub fn less_short_output_passthrough_test() { 1185 // Non-TTY test runner: needs_paging is false → less returns the text. 1186 let env = env.new() 1187 let assert eval.Continue(_, String(out)) = 1188 eval.eval_source(env, "echo hello | less") 1189 let assert True = string.contains(out, "hello") 1190 let assert eval.Continue(_, String(which_out)) = 1191 eval.eval_source(env, "which less") 1192 let assert "builtin: less" = which_out 1193 Nil 1194} 1195 1196pub fn less_preserves_ansi_in_string_test() { 1197 let env = env.new() 1198 // Pre-colored multi-line text must survive less (no re-paint / strip). 1199 let colored = "\u{001b}[31mred\u{001b}[0m\n\u{001b}[32mgreen\u{001b}[0m" 1200 let assert eval.Continue(_, String(out)) = 1201 eval.eval_source(env, "echo \"" <> escape_for_source(colored) <> "\" | less") 1202 let assert True = string.contains(out, "\u{001b}[31m") 1203 let assert True = string.contains(out, "red") 1204 let assert True = string.contains(out, "\u{001b}[32m") 1205 let assert True = string.contains(out, "green") 1206 Nil 1207} 1208 1209pub fn less_no_input_errors_test() { 1210 let env = env.new() 1211 let assert eval.Continue(env2, value.Fail(msg)) = eval.eval_source(env, "less") 1212 let assert True = string.contains(msg, "no input") 1213 let assert 1 = env2.last_exit 1214 Nil 1215} 1216 1217pub fn less_help_test() { 1218 let env = env.new() 1219 let assert eval.Continue(_, String(help_out)) = 1220 eval.eval_source(env, "help less") 1221 let assert True = string.contains(help_out, "ANSI") 1222 let assert True = string.contains(help_out, "q") 1223 let assert True = string.contains(help_out, "/pattern") 1224 let assert True = string.contains(help_out, "n / N") 1225 Nil 1226} 1227 1228pub fn git_log_pipeline_emits_ansi_and_decorate_test() { 1229 // Capture uses a throwaway PTY when color is wanted, so git colorizes and 1230 // keeps ref decorations without GIT_CONFIG_* hacks. FORCE_COLOR makes 1231 // want_child_color true in non-TTY test runners. 1232 let prev = sys.getenv("FORCE_COLOR") 1233 let assert Ok(_) = sys.setenv("FORCE_COLOR", "1") 1234 let env = env.new() 1235 // Full log (not --oneline): decorations appear on the commit line. 1236 let result = eval.eval_source(env, "git log -1 | identity") 1237 case prev { 1238 Ok(v) -> { 1239 let assert Ok(_) = sys.setenv("FORCE_COLOR", v) 1240 Nil 1241 } 1242 Error(Nil) -> { 1243 let assert Ok(_) = sys.setenv("FORCE_COLOR", "") 1244 Nil 1245 } 1246 } 1247 let assert eval.Continue(_, String(out)) = result 1248 // Real git colors, not gleshell string-green on plain text. 1249 let assert True = string.contains(out, "\u{001b}[") 1250 // TTY-style decorate: ref names like HEAD / main. 1251 let assert True = string.contains(out, "HEAD") || string.contains(out, "main") 1252 Nil 1253} 1254 1255pub fn jj_log_pipeline_emits_ansi_test() { 1256 // jj ignores FORCE_COLOR; PTY capture is what keeps colors for `jj log | less`. 1257 let prev = sys.getenv("FORCE_COLOR") 1258 let assert Ok(_) = sys.setenv("FORCE_COLOR", "1") 1259 let env = env.new() 1260 let result = eval.eval_source(env, "jj log -n 1 | identity") 1261 case prev { 1262 Ok(v) -> { 1263 let assert Ok(_) = sys.setenv("FORCE_COLOR", v) 1264 Nil 1265 } 1266 Error(Nil) -> { 1267 let assert Ok(_) = sys.setenv("FORCE_COLOR", "") 1268 Nil 1269 } 1270 } 1271 let assert eval.Continue(_, String(out)) = result 1272 let assert True = string.contains(out, "\u{001b}[") 1273 Nil 1274} 1275 1276/// Embed a string in double-quoted source: escape `\` and `"`. 1277fn escape_for_source(s: String) -> String { 1278 s 1279 |> string.replace("\\", "\\\\") 1280 |> string.replace("\"", "\\\"") 1281} 1282 1283// --- input syntax highlighting --- 1284 1285pub fn highlight_plain_when_off_test() { 1286 let src = "ls | where type == file" 1287 let assert True = highlight.highlight(False, src) == src 1288 Nil 1289} 1290 1291pub fn highlight_pipeline_has_shapes_test() { 1292 let text = highlight.highlight(True, "ls | first 3") 1293 // bold cyan internalcall, bold purple pipe, bold purple int 1294 let assert True = string_contains(text, "\u{001b}[1;36m") 1295 let assert True = string_contains(text, "\u{001b}[1;35m") 1296 let assert True = string_contains(text, "ls") 1297 let assert True = string_contains(text, "first") 1298 let assert True = string_contains(text, "3") 1299 // visible text unchanged 1300 let assert 12 = color.visible_length(text) 1301 Nil 1302} 1303 1304pub fn highlight_string_and_flag_test() { 1305 let text = highlight.highlight(True, "echo \"hi\" --raw") 1306 let assert True = string_contains(text, "\u{001b}[32m") 1307 let assert True = string_contains(text, "\u{001b}[1;34m") 1308 let assert True = string_contains(text, "hi") 1309 let assert True = string_contains(text, "--raw") 1310 Nil 1311} 1312 1313pub fn highlight_variable_and_let_test() { 1314 let text = highlight.highlight(True, "let x = $in") 1315 let assert True = string_contains(text, "\u{001b}[1;36m") 1316 let assert True = string_contains(text, "\u{001b}[35m") 1317 let assert True = string_contains(text, "let") 1318 let assert True = string_contains(text, "$in") 1319 Nil 1320} 1321 1322pub fn highlight_incomplete_string_test() { 1323 // Unterminated string while typing should not crash 1324 let text = highlight.highlight(True, "echo \"hel") 1325 let assert True = string_contains(text, "hel") 1326 let assert True = string_contains(text, "\u{001b}[32m") 1327 Nil 1328} 1329 1330pub fn highlight_to_command_test() { 1331 // `to` is a builtin; `json` is a subcommand arg 1332 let text = highlight.highlight(True, "range 3 | to json") 1333 let assert True = string_contains(text, "to") 1334 let assert True = string_contains(text, "json") 1335 let assert True = string_contains(text, "\u{001b}[1;36m") 1336 Nil 1337} 1338 1339pub fn highlight_path_arg_not_garbage_test() { 1340 // Dotfile paths must not use shape_garbage (white-on-red) 1341 let text = highlight.highlight(True, "ls .jj ./src /tmp ~/code") 1342 let assert True = string_contains(text, ".jj") 1343 let assert False = string_contains(text, "\u{001b}[1;37;41m") 1344 // args use shape_externalarg (bold green) 1345 let assert True = string_contains(text, "\u{001b}[1;32m") 1346 Nil 1347} 1348 1349fn string_contains(haystack: String, needle: String) -> Bool { 1350 case string.split(haystack, needle) { 1351 [_] -> False 1352 _ -> True 1353 } 1354} 1355 1356// --- file syntax (cat highlighters) --- 1357 1358pub fn syntax_language_from_path_test() { 1359 let assert syntax.Json = syntax.language_from_path("data/foo.json") 1360 let assert syntax.Gleam = syntax.language_from_path("src/main.gleam") 1361 let assert syntax.Toml = syntax.language_from_path("gleam.toml") 1362 let assert syntax.Markdown = syntax.language_from_path("README.md") 1363 let assert syntax.Plain = syntax.language_from_path("notes.txt") 1364 let assert syntax.Plain = syntax.language_from_path("Makefile") 1365 Nil 1366} 1367 1368pub fn syntax_detect_sniff_json_test() { 1369 let assert syntax.Json = syntax.detect("data", "{\"a\": 1}") 1370 let assert syntax.Markdown = syntax.detect("notes", "# Title\n\nbody") 1371 let assert syntax.Plain = syntax.detect("x", "just words") 1372 // Extension wins over sniff 1373 let assert syntax.Toml = syntax.detect("x.toml", "{\"a\": 1}") 1374 Nil 1375} 1376 1377pub fn syntax_is_binary_test() { 1378 let assert False = syntax.is_binary("hello\nworld\t!") 1379 let assert True = syntax.is_binary("a\u{0000}b") 1380 Nil 1381} 1382 1383pub fn syntax_paint_json_test() { 1384 let src = "{\"n\": 1, \"msg\": \"hi\", \"ok\": true}" 1385 let assert True = syntax.paint(False, syntax.Json, src) == src 1386 let painted = syntax.paint(True, syntax.Json, src) 1387 // Truecolor roles: keys (sky), string values (green), numbers, bools 1388 let assert True = string_contains(painted, "38;2") 1389 let assert True = string_contains(painted, "true") 1390 let assert True = string_contains(painted, "137;220;235") 1391 let assert True = string_contains(painted, "166;227;161") 1392 let assert True = string_contains(painted, "250;179;135") 1393 let assert True = color.strip_ansi(painted) == src 1394 Nil 1395} 1396 1397pub fn syntax_paint_gleam_test() { 1398 let src = "pub fn main() {\n // hi\n 42\n}" 1399 let painted = syntax.paint(True, syntax.Gleam, src) 1400 // keyword mauve, fn name blue, comment italic, number peach 1401 let assert True = string_contains(painted, "203;166;247") 1402 let assert True = string_contains(painted, "137;180;250") 1403 let assert True = string_contains(painted, "108;112;134") 1404 let assert True = string_contains(painted, "250;179;135") 1405 let assert True = string_contains(painted, "pub") 1406 let assert True = string_contains(painted, "main") 1407 let assert True = color.strip_ansi(painted) == src 1408 Nil 1409} 1410 1411pub fn syntax_paint_toml_test() { 1412 let src = "name = \"gleshell\"\n# comment\nenabled = true" 1413 let painted = syntax.paint(True, syntax.Toml, src) 1414 let assert True = string_contains(painted, "38;2") 1415 let assert True = string_contains(painted, "166;227;161") 1416 let assert True = string_contains(painted, "108;112;134") 1417 let assert True = color.strip_ansi(painted) == src 1418 Nil 1419} 1420 1421pub fn syntax_paint_markdown_test() { 1422 let src = "# Title\n\nUse `code` and **bold**.\n" 1423 let painted = syntax.paint(True, syntax.Markdown, src) 1424 let assert True = string_contains(painted, "Title") 1425 let assert True = string_contains(painted, "code") 1426 // H1 lavender + bold peach for **bold** 1427 let assert True = string_contains(painted, "180;190;254") 1428 let assert True = string_contains(painted, "250;179;135") 1429 let assert True = color.visible_length(painted) 1430 >= color.visible_length(src) - 1 1431 Nil 1432} 1433 1434pub fn syntax_frame_gutter_test() { 1435 let body = "alpha\nbeta" 1436 let framed = syntax.frame("src/demo.gleam", syntax.Gleam, body) 1437 let stripped = color.strip_ansi(framed) 1438 // Header carries basename + language badge 1439 let assert True = string_contains(stripped, "demo.gleam") 1440 let assert True = string_contains(stripped, "gleam") 1441 // Line numbers + pipe gutter 1442 let assert True = string_contains(stripped, "1") 1443 let assert True = string_contains(stripped, "2") 1444 let assert True = string_contains(stripped, "") 1445 let assert True = string_contains(stripped, "alpha") 1446 let assert True = string_contains(stripped, "beta") 1447 Nil 1448} 1449 1450pub fn cat_raw_and_language_test() { 1451 let env = env.new() 1452 // Write a temp json file under the project (simplifile needs a real path). 1453 let path = "build/cat_syntax_test.json" 1454 let body = "{\"x\": 1}" 1455 let assert Ok(Nil) = simplifile.write(to: path, contents: body) 1456 1457 // --raw must return plain content even when colors would be on. 1458 let assert eval.Continue(_, String(raw_out)) = 1459 eval.eval_source(env, "cat " <> path <> " --raw") 1460 let assert True = raw_out == body 1461 1462 // --language plain: no syntax colors (may still frame with line numbers on TTY). 1463 let assert eval.Continue(_, String(plain_out)) = 1464 eval.eval_source(env, "cat " <> path <> " --language plain") 1465 let plain_stripped = color.strip_ansi(plain_out) 1466 let assert True = 1467 plain_out == body 1468 || plain_stripped == body 1469 || string_contains(plain_stripped, body) 1470 1471 // Forced gleam: visible body still present (gutter/header OK). 1472 let assert eval.Continue(_, String(gleam_out)) = 1473 eval.eval_source(env, "cat " <> path <> " --language gleam") 1474 let gleam_stripped = color.strip_ansi(gleam_out) 1475 let assert True = 1476 gleam_stripped == body || string_contains(gleam_stripped, body) 1477 1478 // Unknown language errors. 1479 let assert eval.Continue(_, value.Fail(msg)) = 1480 eval.eval_source(env, "cat " <> path <> " --language cobol") 1481 let assert True = string.contains(msg, "unknown language") 1482 1483 let _ = simplifile.delete(path) 1484 Nil 1485} 1486 1487// --- tab completion --- 1488 1489pub fn complete_command_builtin_test() { 1490 // Start of line: complete builtins 1491 let #(matches, kind) = sys.complete_word("", "ech") 1492 let assert True = kind == "command" 1493 let assert True = list_contains(matches, "echo") 1494 // After pipeline separator 1495 let #(matches2, kind2) = sys.complete_word("ls | ", "wher") 1496 let assert True = kind2 == "command" 1497 let assert True = list_contains(matches2, "where") 1498 // `to` / `from` are single-word commands (subcommand is a normal arg) 1499 let #(matches3, kind3) = sys.complete_word("", "to") 1500 let assert True = kind3 == "command" 1501 let assert True = list_contains(matches3, "to") 1502 let #(matches_from, kind_from) = sys.complete_word("", "fro") 1503 let assert True = kind_from == "command" 1504 let assert True = list_contains(matches_from, "from") 1505 // Keyword 1506 let #(matches4, kind4) = sys.complete_word("", "le") 1507 let assert True = kind4 == "command" 1508 let assert True = list_contains(matches4, "let") 1509 Nil 1510} 1511 1512pub fn complete_command_after_assign_test() { 1513 let #(matches, kind) = sys.complete_word("let x = ", "ran") 1514 let assert True = kind == "command" 1515 let assert True = list_contains(matches, "range") 1516 Nil 1517} 1518 1519pub fn complete_path_for_args_test() { 1520 // After a command name, Tab completes files not commands 1521 let #(_matches, kind) = sys.complete_word("echo ", "ech") 1522 let assert True = kind == "path" 1523 Nil 1524} 1525 1526pub fn complete_path_like_command_test() { 1527 // Path-shaped command words stay on filename completion 1528 let #(_matches, kind) = sys.complete_word("", "./ec") 1529 let assert True = kind == "path" 1530 let #(_matches2, kind2) = sys.complete_word("", "/us") 1531 let assert True = kind2 == "path" 1532 Nil 1533} 1534 1535pub fn complete_path_executable_test() { 1536 // PATH + builtins for a non-empty prefix 1537 let #(matches, kind) = sys.complete_word("", "s") 1538 let assert True = kind == "command" 1539 // At least builtins starting with s (select, skip, sort-by, …) 1540 let assert True = list_contains(matches, "select") 1541 Nil 1542} 1543 1544// --- history ghost-text hints (Nu/fish style) --- 1545 1546pub fn history_hint_newest_prefix_test() { 1547 // Newest-first: first entry that has the buffer as a proper prefix wins. 1548 let hist = ["ls | where type == file", "ls | first 3", "echo hi"] 1549 let assert " | where type == file" = sys.history_hint(hist, "ls") 1550 let assert " | first 3" = sys.history_hint(["ls | first 3", "ls | where x"], "ls") 1551 let assert " type == file" = sys.history_hint(hist, "ls | where") 1552 let assert " hi" = sys.history_hint(hist, "echo") 1553 Nil 1554} 1555 1556pub fn history_hint_no_match_test() { 1557 let hist = ["ls", "echo hi"] 1558 let assert "" = sys.history_hint(hist, "cd") 1559 // Exact match only — no suffix 1560 let assert "" = sys.history_hint(hist, "ls") 1561 // Empty buffer never suggests 1562 let assert "" = sys.history_hint(hist, "") 1563 Nil 1564} 1565 1566// --- Ctrl+R reverse search filter (stinkpot-style fuzzy) --- 1567 1568pub fn history_search_empty_query_returns_all_test() { 1569 let hist = ["ls | first 3", "echo hi", "cd src"] 1570 let assert ["ls | first 3", "echo hi", "cd src"] = sys.history_search(hist, "") 1571 let assert ["ls | first 3", "echo hi", "cd src"] = 1572 sys.history_search(hist, " ") 1573 Nil 1574} 1575 1576pub fn history_search_dedupes_newest_first_test() { 1577 let hist = ["echo a", "ls", "echo a", "cd"] 1578 let assert ["echo a", "ls", "cd"] = sys.history_search(hist, "") 1579 Nil 1580} 1581 1582pub fn history_search_substring_and_fuzzy_test() { 1583 let hist = [ 1584 "gleam test", 1585 "ls | where type == file", 1586 "git status", 1587 "echo hello", 1588 ] 1589 let hits = sys.history_search(hist, "gleam") 1590 let assert True = list_contains(hits, "gleam test") 1591 let assert False = list_contains(hits, "git status") 1592 1593 // Subsequence: "gts" matches "git status" 1594 let fuzzy = sys.history_search(hist, "gts") 1595 let assert True = list_contains(fuzzy, "git status") 1596 1597 // Case-insensitive 1598 let assert True = 1599 list_contains(sys.history_search(hist, "GLEAM"), "gleam test") 1600 Nil 1601} 1602 1603pub fn history_search_no_match_test() { 1604 let hist = ["ls", "echo hi"] 1605 let assert [] = sys.history_search(hist, "zzzz-nope") 1606 Nil 1607} 1608 1609fn list_contains(items: List(String), needle: String) -> Bool { 1610 case items { 1611 [] -> False 1612 [x, ..] if x == needle -> True 1613 [_, ..rest] -> list_contains(rest, needle) 1614 } 1615}