1package nushell23import (4 "context"5 "errors"6 "io"7 "os/exec"8 "strings"9 "testing"10 "time"11)1213// fake prints out, or fails with err, and keeps the script it was given.14type fake struct {15 out string16 err error17 script string18 job Job19}2021func (f *fake) Run(ctx context.Context, job Job, script string, stdout io.Writer) error {22 f.script, f.job = script, job23 if f.err != nil {24 return f.err25 }26 _, err := io.WriteString(stdout, f.out)27 return err28}2930func TestScript(t *testing.T) {31 got := Script(Job{Command: "$r1 | where size > 1kb", Tables: map[string][]byte{"r1": nil}}, []string{"big", "r1"})32 want := "let big = (open --raw $env.NU012_TABLE_0 | from nuon)\n" +33 "let r1 = (open --raw $env.NU012_TABLE_1 | from nuon)\n" +34 "do {\n$r1 | where size > 1kb\n} | to nuon\n"35 if got != want {36 t.Errorf("script:\n%s\nwant:\n%s", got, want)37 }38}3940func TestExec(t *testing.T) {41 out, err := Exec(context.Background(), &fake{out: " [[name, size]; [a, 1kb]]\n"}, Job{Command: "x"}, time.Second, 0)42 if err != nil || string(out) != "[[name, size]; [a, 1kb]]" {43 t.Errorf("%q %v", out, err)44 }45 _, err = Exec(context.Background(), &fake{err: &Error{Msg: "Cannot find column 'x'", Stderr: " × Cannot find column 'x'\n help: try get\n"}}, Job{Command: "x"}, time.Second, 0)46 var e *Error47 if !errors.As(err, &e) || e.Msg != "Cannot find column 'x'" || e.Help() != "help: try get" {48 t.Errorf("error %v", err)49 }50 _, err = Exec(context.Background(), &fake{out: strings.Repeat("x", 100)}, Job{Command: "x"}, time.Second, 10)51 if !errors.Is(err, ErrTooLarge) {52 t.Errorf("too large: %v", err)53 }54}5556// A run handing back variables prints a line for each after what the57// pipeline printed itself.58func TestExecVars(t *testing.T) {59 job := Job{Command: "x", Vars: []string{"files"}}60 f := &fake{out: "printed\n__out WzEsIDJd\nfiles WzNd\n"}61 out, vars, err := ExecVars(context.Background(), f, job, time.Second, 0)62 if err != nil || string(out) != "[1, 2]" || string(vars["files"]) != "[3]" || len(vars) != 1 {63 t.Errorf("%q %q %v", out, vars, err)64 }65 if !strings.Contains(f.script, `items {|k, v|`) || strings.Contains(f.script, "| to nuon\n") {66 t.Errorf("script %s", f.script)67 }68 if _, _, err := ExecVars(context.Background(), &fake{out: "[1, 2]"}, job, time.Second, 0); err == nil {69 t.Error("no variables came back, and no error")70 }71 out, vars, err = ExecVars(context.Background(), &fake{out: "[1, 2]"}, Job{Command: "x"}, time.Second, 0)72 if err != nil || string(out) != "[1, 2]" || vars != nil {73 t.Errorf("without variables %q %v %v", out, vars, err)74 }75}7677// The real nu hands back a cell's variables beside its output.78func TestNuVars(t *testing.T) {79 if _, err := exec.LookPath("nu"); err != nil {80 t.Skip("nu isn't installed")81 }82 job := Job{83 Command: "let files = [[name, size]; [a, 2kb], [b, 10b]]\n{__out: ($files | where size > 1kb | get name), files: ($files | length)}",84 Vars: []string{"files"},85 }86 out, vars, err := ExecVars(context.Background(), Nu{}, job, 30*time.Second, 0)87 if err != nil || string(out) != "[a]" || string(vars["files"]) != "2" {88 t.Errorf("%q %q %v", out, vars, err)89 }90}9192func TestMessage(t *testing.T) {93 stderr := "Error: nu::shell::column_not_found\n\n × Cannot find column 'xyz'\n ╭─[source:1:6]\n"94 if got := Message(stderr, errors.New("exit status 1")); got != "Cannot find column 'xyz'" {95 t.Errorf("message %q", got)96 }97 ascii := "Error: nu::shell::column_not_found\n\n x Cannot find column 'xyz'\n ,-[source:1:6]\n"98 if got := Message(ascii, errors.New("exit status 1")); got != "Cannot find column 'xyz'" {99 t.Errorf("ascii message %q", got)100 }101 if got := Message("x is not a command\n", errors.New("exit status 1")); got != "x is not a command" {102 t.Errorf("unmarked message %q", got)103 }104 if got := Message("", errors.New("exit status 1")); got != "exit status 1" {105 t.Errorf("message %q", got)106 }107}108109// The real nu, when it's installed: tables go in as variables and come110// back as NUON.111func TestNu(t *testing.T) {112 if _, err := exec.LookPath("nu"); err != nil {113 t.Skip("nu isn't installed")114 }115 job := Job{116 Command: "$r1 | where size > 1kb | append $__sheet1",117 Tables: map[string][]byte{"r1": []byte("[[name, size]; [a, 2kb], [b, 10b]]"), "__sheet1": []byte("[[name, size]; [c, 5mb]]")},118 }119 out, err := Exec(context.Background(), Nu{}, job, 30*time.Second, 0)120 if err != nil {121 t.Fatal(err)122 }123 if got := string(out); got != "[[name, size]; [a, 2000b], [c, 5000000b]]" && got != "[[name, size]; [a, 2kB], [c, 5MB]]" {124 t.Logf("output %s", got) // nu's own spelling of sizes varies by version125 if !strings.Contains(got, "a") || !strings.Contains(got, "c") || strings.Contains(got, "b,") {126 t.Errorf("output %s", got)127 }128 }129 _, err = Exec(context.Background(), Nu{}, Job{Command: "ls | get nosuchcolumn"}, 30*time.Second, 0)130 var e *Error131 if !errors.As(err, &e) || !strings.Contains(e.Msg, "nosuchcolumn") {132 t.Errorf("failure: %v", err)133 }134 _, err = Exec(context.Background(), Nu{}, Job{Command: "sleep 10sec"}, 200*time.Millisecond, 0)135 if err == nil || !strings.Contains(err.Error(), "stopped after") {136 t.Errorf("timeout: %v", err)137 }138 names, err := Commands(context.Background(), Nu{}, 30*time.Second)139 if err != nil || len(names) < 100 {140 t.Errorf("commands: %d %v", len(names), err)141 }142 if _, err := Exec(context.Background(), Nu{Path: "/nonexistent/nu"}, Job{Command: "1"}, time.Second, 0); err == nil {143 t.Error("a missing nu ran")144 }145}146147// A stream's script prints each value the pipeline yields on a line of148// its own, and Stream has no timeout of its own: it ends with the149// pipeline or when stopped.150func TestStream(t *testing.T) {151 got := StreamScript(Job{Command: "tail -f app.log | lines", Tables: map[string][]byte{"r1": nil}}, []string{"r1"})152 want := "let r1 = (open --raw $env.NU012_TABLE_0 | from nuon)\n" +153 "do {\ntail -f app.log | lines\n} | each {|row| $row | to nuon --raw | print } | ignore\n"154 if got != want {155 t.Errorf("script:\n%s\nwant:\n%s", got, want)156 }157 var b strings.Builder158 f := &fake{out: "{a: 1}\n{a: 2}\n"}159 if err := Stream(context.Background(), f, Job{Command: "x"}, &b); err != nil || b.String() != f.out || !strings.Contains(f.script, "print") {160 t.Errorf("%q %v", b.String(), err)161 }162 ctx, cancel := context.WithCancel(context.Background())163 cancel()164 if err := Stream(ctx, f, Job{Command: "x"}, &b); !errors.Is(err, context.Canceled) {165 t.Errorf("stopped: %v", err)166 }167}