diff --git a/.github/run-tests.sh b/.github/run-tests.sh index 38b5047..3f05572 100755 --- a/.github/run-tests.sh +++ b/.github/run-tests.sh @@ -2,3 +2,6 @@ # Run our golang tests go test ./... -race + +# Run the perl script to look for function orders +.github/test-ordering.pl || exit 1 diff --git a/.github/test-ordering.pl b/.github/test-ordering.pl new file mode 100755 index 0000000..d7d354f --- /dev/null +++ b/.github/test-ordering.pl @@ -0,0 +1,71 @@ +#!/usr/bin/perl +# +# Trivial script to find *.go files, and ensure their +# functions are all defined in alphabetical order. +# +# Ignore "init" and any "BenchmarkXXX" functions. +# + +use strict; +use warnings; + +use File::Find; + +# Failure count +my $failed = 0; + +# Find all files beneath the current directory +find( { wanted => \&process, no_chdir => 1, follow => 0 }, '.' ); + +# Return the result as an exit code +exit $failed; + + +# Process a file +sub process +{ + # Get the filename, and make sure it is a file. + my $file = $File::Find::name; + return unless ( $file =~ /.go$/ ); + + print "$file\n"; + + open( my $handle, "<", $file ) or + die "Failed to read $file: $!"; + + my @subs; + + foreach my $line (<$handle>) + { + if ( $line =~ /^func\s+([^(]+)\(/ ) + { + my $func = $1; + + # Skip init + next if $func eq "init"; + + # Skip BenchmarkXXX + next if $func =~ /^Benchmark/; + + # Record the function now. + push( @subs, $func ); + } + } + close $handle; + + # Is the list of functions sorted? + my @sorted = sort @subs; + my $len = $#sorted; + + my $i = 0; + while ( $i < $len ) + { + if ( $sorted[$i] ne $subs[$i] ) + { + print "$sorted[$i] ne $subs[$i]\n"; + $failed++; + } + $i++; + + } +} diff --git a/builtins/builtins.go b/builtins/builtins.go index 265bda3..abdd062 100644 --- a/builtins/builtins.go +++ b/builtins/builtins.go @@ -8,7 +8,6 @@ package builtins import ( "bufio" "bytes" - "flag" "fmt" "math" "math/rand" @@ -542,7 +541,7 @@ func fileReadFn(env *env.Environment, args []primitive.Primitive) primitive.Prim return primitive.String(string(data)) } -// fileStatFn implements (file:lines) +// fileStatFn implements (file:stat) // // Return value is (NAME SIZE UID GID MODE) func fileStatFn(env *env.Environment, args []primitive.Primitive) primitive.Primitive { @@ -564,16 +563,15 @@ func fileStatFn(env *env.Environment, args []primitive.Primitive) primitive.Prim return primitive.Nil{} } - var UID int - var GID int + // If we're not on Linux the Stat_t type won't be available, + // so we'd default to the current user. + UID := os.Getuid() + GID := os.Getgid() + + // But if we can get the "real" values, then use them. if stat, ok := info.Sys().(*syscall.Stat_t); ok { UID = int(stat.Uid) GID = int(stat.Gid) - } else { - // we are not in linux, this won't work anyway in windows, - // but maybe you want to log warnings - UID = os.Getuid() - GID = os.Getgid() } var res primitive.List @@ -1068,7 +1066,7 @@ func shellFn(env *env.Environment, args []primitive.Primitive) primitive.Primiti // If we're running a test-case we'll stop here, because // fuzzing might run commands. - if flag.Lookup("test.v") != nil { + if os.Getenv("FUZZ") != "" { return primitive.List{} } diff --git a/builtins/builtins_shell_test.go b/builtins/builtins_shell_test.go new file mode 100644 index 0000000..e12cf07 --- /dev/null +++ b/builtins/builtins_shell_test.go @@ -0,0 +1,92 @@ +//go:build !windows && !darwin +// +build !windows,!darwin + +package builtins + +import ( + "os" + "strings" + "testing" + + "github.com/skx/yal/primitive" +) + +// TestShell tests shell - but only on Linux/Unix +func TestShell(t *testing.T) { + + // calling with no argument + out := shellFn(ENV, []primitive.Primitive{}) + + // Will lead to an error + _, ok := out.(primitive.Error) + if !ok { + t.Fatalf("expected error, got %v", out) + } + + // One argument, but the wrong type + out = shellFn(ENV, []primitive.Primitive{ + primitive.Number(3), + }) + + var e primitive.Primitive + e, ok = out.(primitive.Error) + if !ok { + t.Fatalf("expected error, got %v", out) + } + if !strings.Contains(e.ToString(), "not a list") { + t.Fatalf("got error, but wrong one %v", out) + } + + // Echo command to execute. + cmd := primitive.List{} + cmd = append(cmd, primitive.String("echo")) + cmd = append(cmd, primitive.String("foo")) + cmd = append(cmd, primitive.String("bar")) + + // Run the command + res := shellFn(ENV, []primitive.Primitive{cmd}) + + // Response should be a list + lst, ok2 := res.(primitive.List) + if !ok2 { + t.Fatalf("expected (shell) to return a list, got %v", res) + } + + // with two entries + if len(lst) != 2 { + t.Fatalf("expected (shell) result to have two entries, got %v", lst) + } + + // + // Now: run a command that will fail + // + fail := primitive.List{} + fail = append(fail, primitive.String("/fdsf/fdsf/-path-not/exists")) + + // Run the command + out = shellFn(ENV, []primitive.Primitive{fail}) + + // Will lead to an error + _, ok = out.(primitive.Error) + if !ok { + t.Fatalf("expected error, got %v", out) + } + + // + // Now: Pretend we're running under a fuzzer + // + os.Setenv("FUZZ", "FUZZ") + res = shellFn(ENV, []primitive.Primitive{cmd}) + + // Response should still be a list + lst, ok2 = res.(primitive.List) + if !ok2 { + t.Fatalf("expected (shell) to return a list, got %v", res) + } + + // with zero entries + if len(lst) != 0 { + t.Fatalf("expected (shell) result to have zero entries, got %v", lst) + } + +} diff --git a/builtins/builtins_test.go b/builtins/builtins_test.go index 6896af2..007c7b5 100644 --- a/builtins/builtins_test.go +++ b/builtins/builtins_test.go @@ -10,7 +10,9 @@ import ( "time" "github.com/skx/yal/env" + "github.com/skx/yal/eval" "github.com/skx/yal/primitive" + "github.com/skx/yal/stdlib" ) // ENV contains a local environment for the test functions @@ -969,6 +971,59 @@ func TestFile(t *testing.T) { } +// TestFileLines tests file:lines +func TestFileLines(t *testing.T) { + + // calling with no argument + out := fileLinesFn(ENV, []primitive.Primitive{}) + + // Will lead to an error + _, ok := out.(primitive.Error) + if !ok { + t.Fatalf("expected error, got %v", out) + } + + // calling with the wrong argument type. + out = fileLinesFn(ENV, []primitive.Primitive{ + primitive.Number(3)}) + + // Will lead to an error + _, ok = out.(primitive.Error) + if !ok { + t.Fatalf("expected error, got %v", out) + } + + // Call with a file that doesn't exist + out = fileLinesFn(ENV, []primitive.Primitive{ + primitive.String("path/not/found")}) + + _, ok = out.(primitive.Error) + if !ok { + t.Fatalf("expected error, got %v", out) + } + + // Create a temporary file, and read the contents + tmp, _ := os.CreateTemp("", "yal") + err := os.WriteFile(tmp.Name(), []byte("I like cake\nAnd pie."), 0777) + if err != nil { + t.Fatalf("failed to write to file") + } + defer os.Remove(tmp.Name()) + + str := fileLinesFn(ENV, []primitive.Primitive{ + primitive.String(tmp.Name())}) + + // Will lead to a list + lst, ok2 := str.(primitive.List) + if !ok2 { + t.Fatalf("expected list, got %v", out) + } + + if len(lst) != 2 { + t.Fatalf("re-reading the temporary file gave bogus contents") + } +} + // TestFileRead tests file:read func TestFileRead(t *testing.T) { @@ -981,6 +1036,15 @@ func TestFileRead(t *testing.T) { t.Fatalf("expected error, got %v", out) } + // Call with the wrong type + out = fileReadFn(ENV, []primitive.Primitive{ + primitive.Number(3)}) + + _, ok = out.(primitive.Error) + if !ok { + t.Fatalf("expected error, got %v", out) + } + // Call with a file that doesn't exist out = fileReadFn(ENV, []primitive.Primitive{ primitive.String("path/not/found")}) @@ -1001,7 +1065,7 @@ func TestFileRead(t *testing.T) { str := fileReadFn(ENV, []primitive.Primitive{ primitive.String(tmp.Name())}) - // Will lead to an error + // Will lead to a string txt, ok2 := str.(primitive.String) if !ok2 { t.Fatalf("expected string, got %v", out) @@ -1012,6 +1076,60 @@ func TestFileRead(t *testing.T) { } } +// TestFileStat tests file:stat +func TestFileStat(t *testing.T) { + + // calling with no argument + out := fileStatFn(ENV, []primitive.Primitive{}) + + // Will lead to an error + _, ok := out.(primitive.Error) + if !ok { + t.Fatalf("expected error, got %v", out) + } + + // Call with the wrong type + out = fileStatFn(ENV, []primitive.Primitive{ + primitive.Number(3)}) + + _, ok = out.(primitive.Error) + if !ok { + t.Fatalf("expected error, got %v", out) + } + + // Call with a file that doesn't exist + out = fileStatFn(ENV, []primitive.Primitive{ + primitive.String("path/not/found")}) + + _, ok = out.(primitive.Nil) + if !ok { + t.Fatalf("expected nil, got %v", out) + } + + // Create a temporary file + tmp, _ := os.CreateTemp("", "yal") + err := os.WriteFile(tmp.Name(), []byte("42 is the answer"), 0777) + if err != nil { + t.Fatalf("failed to write to file") + } + defer os.Remove(tmp.Name()) + + // stat that + str := fileStatFn(ENV, []primitive.Primitive{ + primitive.String(tmp.Name())}) + + // Will lead to a list + lst, ok2 := str.(primitive.List) + if !ok2 { + t.Fatalf("expected list, got %v", out) + } + + // List will be: NAME SIZE .. + if lst[1].ToString() != "16" { + t.Fatalf("The size was wrong: %s", lst) + } +} + // TestGenSym tests gensym func TestGenSym(t *testing.T) { @@ -1229,7 +1347,16 @@ func TestHelp(t *testing.T) { env := env.New() PopulateEnvironment(env) - for _, name := range []string{"print", "sprintf"} { + for _, name := range []string{"print", "sprintf", "length"} { + + // Load our standard library + st := stdlib.Contents() + std := string(st) + + // Create a new interpreter + l := eval.New(std + "\n") + + l.Evaluate(env) fn, ok := env.Get(name) if !ok { @@ -1242,7 +1369,7 @@ func TestHelp(t *testing.T) { if !ok2 { t.Fatalf("expected a string, got %v", result) } - if !strings.Contains(txt.ToString(), "print") { + if !strings.Contains(txt.ToString(), name) { t.Fatalf("got help text, but didn't find expected content: %v", result) } } @@ -2103,33 +2230,6 @@ func TestSetup(t *testing.T) { } } -// TestShell tests shell - but not fully -func TestShell(t *testing.T) { - - // calling with no argument - out := shellFn(ENV, []primitive.Primitive{}) - - // Will lead to an error - _, ok := out.(primitive.Error) - if !ok { - t.Fatalf("expected error, got %v", out) - } - - // One argument, but the wrong type - out = shellFn(ENV, []primitive.Primitive{ - primitive.Number(3), - }) - - var e primitive.Primitive - e, ok = out.(primitive.Error) - if !ok { - t.Fatalf("expected error, got %v", out) - } - if !strings.Contains(e.ToString(), "not a list") { - t.Fatalf("got error, but wrong one %v", out) - } -} - func TestSort(t *testing.T) { // No arguments diff --git a/eval/eval_test.go b/eval/eval_test.go index ad1307e..f84be63 100644 --- a/eval/eval_test.go +++ b/eval/eval_test.go @@ -2,6 +2,7 @@ package eval import ( "context" + "fmt" "strings" "testing" "time" @@ -456,6 +457,7 @@ func TestTimeout(t *testing.T) { (set! a 1) (while true (do + (sleep) (set! a (+ a 1) true))) ` // Load our standard library @@ -473,20 +475,29 @@ func TestTimeout(t *testing.T) { l.SetContext(ctx) // With a new environment - env := env.New() + ev := env.New() + + // Add a new function + ev.Set("sleep", + &primitive.Procedure{ + Help: "sleep delays for two seconds", + F: func(e *env.Environment, args []primitive.Primitive) primitive.Primitive { + fmt.Printf("Sleeping for two seconds") + time.Sleep(2 * time.Second) + return primitive.Nil{} + }}) // Populate the default primitives - builtins.PopulateEnvironment(env) + builtins.PopulateEnvironment(ev) // Run it - out := l.Evaluate(env) + out := l.Evaluate(ev) // Test for both possible errors here. // // We should get the context error, but sometimes we don't // the important thing is we DON'T hang forever - if !strings.Contains(out.ToString(), "deadline exceeded") && - !strings.Contains(out.ToString(), "not a function") { + if !strings.Contains(out.ToString(), "deadline exceeded") { t.Fatalf("Didn't get the expected output. Got: %s", out.ToString()) } } diff --git a/fuzz_test.go b/fuzz_test.go index 2007295..018cd62 100644 --- a/fuzz_test.go +++ b/fuzz_test.go @@ -5,6 +5,7 @@ package main import ( "context" + "os" "strings" "testing" "time" @@ -18,6 +19,11 @@ import ( func FuzzYAL(f *testing.F) { + // We're running fuzzing, and that means we need + // to disable "shell". That is done via the use + // of an environmental variable + os.Setenv("FUZZ", "FUZZ") + // empty string f.Add([]byte(""))