Skip to content

Commit

Permalink
Make REPL testable from go test
Browse files Browse the repository at this point in the history
  • Loading branch information
sam-at-luther committed Dec 16, 2023
1 parent b07b667 commit 13795f1
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 9 deletions.
9 changes: 9 additions & 0 deletions lisp/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,12 @@ func WithStderr(w io.Writer) Config {
return Nil()
}
}

// WithLibrary returns a Config that makes environments use l
// as a source library.
func WithLibrary(l SourceLibrary) Config {
return func(env *LEnv) *LVal {
env.Runtime.Library = l
return Nil()
}
}
65 changes: 56 additions & 9 deletions repl/repl.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,50 @@ import (
"github.com/luthersystems/elps/parser/token"
)

type config struct {
stdin io.ReadCloser
stderr io.WriteCloser
}

func newConfig(opts ...Option) *config {
config := &config{}
for _, opt := range opts {
opt(config)
}
return config
}

type Option func(*config)

// WithStderr allows overriding the input to the REPL.
func WithStdin(stdin io.ReadCloser) Option {
return func(c *config) {
c.stdin = stdin
}
}

// WithStderr allows overriding the output to the REPL.
func WithStderr(stderr io.WriteCloser) Option {
return func(c *config) {
c.stderr = stderr
}
}

// RunRepl runs a simple repl in a vanilla elps environment.
func RunRepl(prompt string) {
func RunRepl(prompt string, opts ...Option) {
env := lisp.NewEnv(nil)
env.Runtime.Reader = parser.NewReader()
env.Runtime.Library = &lisp.RelativeFileSystemLibrary{}
rc := lisp.InitializeUserEnv(env)

envOpts := []lisp.Config{
lisp.WithReader(parser.NewReader()),
lisp.WithLibrary(&lisp.RelativeFileSystemLibrary{}),
}

cfg := newConfig(opts...)
if cfg.stderr != nil {
envOpts = append(envOpts, lisp.WithStderr(cfg.stderr))
}

rc := lisp.InitializeUserEnv(env, envOpts...)
if !rc.IsNil() {
errlnf("Language initialization failure: %v", rc)
os.Exit(1)
Expand All @@ -44,21 +82,30 @@ func RunRepl(prompt string) {
os.Exit(1)
}

RunEnv(env, prompt, strings.Repeat(" ", len(prompt)))
RunEnv(env, prompt, strings.Repeat(" ", len(prompt)), opts...)
}

// RunEnv runs a simple repl with env as a root environment.
func RunEnv(env *lisp.LEnv, prompt, cont string) {
func RunEnv(env *lisp.LEnv, prompt, cont string, opts ...Option) {
if env.Parent != nil {
errlnf("REPL environment is not a root environment.")
os.Exit(1)
}

p := rdparser.NewInteractive(nil)
p.SetPrompts(prompt, cont)
rl, err := readline.NewEx(&readline.Config{

rlCfg := &readline.Config{
Stdout: env.Runtime.Stderr,
Stderr: env.Runtime.Stderr,
Prompt: p.Prompt(),
})
}

cfg := newConfig(opts...)
if cfg.stdin != nil {
rlCfg.Stdin = cfg.stdin
}
rl, err := readline.NewEx(rlCfg)
if err != nil {
panic(err)
}
Expand Down Expand Up @@ -114,7 +161,7 @@ func RunEnv(env *lisp.LEnv, prompt, cont string) {
}
val := env.Eval(expr)
if val.Type == lisp.LError {
_, _ = (*lisp.ErrorVal)(val).WriteTrace(os.Stderr)
_, _ = (*lisp.ErrorVal)(val).WriteTrace(env.Runtime.Stderr)
} else {
fmt.Fprintln(env.Runtime.Stderr, val)
}
Expand Down
62 changes: 62 additions & 0 deletions repl/repl_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package repl

import (
"bytes"
"io"
"testing"

"github.com/stretchr/testify/require"
)

func runReplWithString(t *testing.T, input string) (string, error) {
t.Helper()
inR, inW := io.Pipe()
outR, outW := io.Pipe()

// Start a goroutine to write the input string to the Stdin pipe
go func() {
defer inW.Close()
_, _ = io.WriteString(inW, input)
}()

// Start a goroutine to run the REPL
go func() {
RunRepl("elps> ", WithStdin(inR), WithStderr(outW))
inR.Close()
outW.Close()
}()

// Read the output from the Stderr pipe
var output bytes.Buffer
_, _ = io.Copy(&output, outR)
outR.Close()

return output.String(), nil
}

func TestRunRepl(t *testing.T) {
testCases := []struct {
name string
input string
expected string
}{
{
name: "Simple Addition",
input: `(+ 1 1)`,
expected: "2\n",
},
{
name: "Error",
input: `fnord`,
expected: "unbound symbol",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got, err := runReplWithString(t, tc.input)
require.NoError(t, err)
require.Contains(t, got, tc.expected)
})
}
}

0 comments on commit 13795f1

Please sign in to comment.