From 8aea668af7caff0137dffcca1507ddd85b438d36 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Thu, 27 Aug 2020 19:29:54 -0600 Subject: [PATCH] Begin trying to integrate the transpiler --- cmd/mothd/puzzlecmd.go | 124 ++++++++++++++++++++++++++++++++ cmd/mothd/puzzlecmd_test.go | 62 ++++++++++++++++ cmd/mothd/testdata/testpiler.sh | 30 ++++++++ 3 files changed, 216 insertions(+) create mode 100644 cmd/mothd/puzzlecmd.go create mode 100644 cmd/mothd/puzzlecmd_test.go create mode 100755 cmd/mothd/testdata/testpiler.sh diff --git a/cmd/mothd/puzzlecmd.go b/cmd/mothd/puzzlecmd.go new file mode 100644 index 0000000..1d8da13 --- /dev/null +++ b/cmd/mothd/puzzlecmd.go @@ -0,0 +1,124 @@ +// Provides a Puzzle interface that runs a command for each request +package main + +import ( + "bytes" + "context" + "fmt" + "io" + "log" + "os" + "os/exec" + "strconv" + "strings" + "time" +) + +// PuzzleCommand specifies a command to run for the puzzle API +type PuzzleCommand struct { + Path string + Args []string +} + +// Inventory runs with "action=inventory", and parses the output into a category list. +func (pc PuzzleCommand) Inventory() (inv []Category) { + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) + defer cancel() + + cmd := exec.CommandContext(ctx, pc.Path, pc.Args...) + cmd.Env = os.Environ() + cmd.Env = append(cmd.Env, "action=inventory") + + stdout, err := cmd.Output() + if err != nil { + log.Print(err) + return + } + + for _, line := range strings.Split(string(stdout), "\n") { + line = strings.TrimSpace(line) + if line == "" { + continue + } + parts := strings.Split(line, " ") + if len(parts) < 2 { + log.Println("Skipping misformatted line:", line) + continue + } + name := parts[0] + puzzles := make([]int, 0, 10) + for _, pointsString := range parts[1:] { + points, err := strconv.Atoi(pointsString) + if err != nil { + log.Println(err) + continue + } + puzzles = append(puzzles, points) + } + inv = append(inv, Category{name, puzzles}) + } + return +} + +type NullReadSeekCloser struct { + io.ReadSeeker +} + +func (f NullReadSeekCloser) Close() error { + return nil +} + +// Open passes its arguments to the command with "action=open". +func (pc PuzzleCommand) Open(cat string, points int, path string) (ReadSeekCloser, time.Time, error) { + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) + defer cancel() + + cmd := exec.CommandContext(ctx, pc.Path, pc.Args...) + cmd.Env = os.Environ() + cmd.Env = append(cmd.Env, "action=open") + cmd.Env = append(cmd.Env, "cat="+cat) + cmd.Env = append(cmd.Env, "points="+strconv.Itoa(points)) + cmd.Env = append(cmd.Env, "path="+path) + + stdoutBytes, err := cmd.Output() + stdout := NullReadSeekCloser{bytes.NewReader(stdoutBytes)} + now := time.Now() + return stdout, now, err +} + +// CheckAnswer passes its arguments to the command with "action=answer". +// If the command exits successfully and sends "correct" to stdout, +// nil is returned. +func (pc PuzzleCommand) CheckAnswer(cat string, points int, answer string) error { + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) + defer cancel() + + cmd := exec.CommandContext(ctx, pc.Path, pc.Args...) + cmd.Env = os.Environ() + cmd.Env = append(cmd.Env, "action=answer") + cmd.Env = append(cmd.Env, "cat="+cat) + cmd.Env = append(cmd.Env, "points="+strconv.Itoa(points)) + cmd.Env = append(cmd.Env, "answer="+answer) + + stdout, err := cmd.Output() + if ee, ok := err.(*exec.ExitError); ok { + log.Printf("%s: %s", pc.Path, string(ee.Stderr)) + return err + } else if err != nil { + return err + } + result := strings.TrimSpace(string(stdout)) + + if result != "correct" { + if result == "" { + result = "Nothing written to stdout" + } + return fmt.Errorf("Wrong answer: %s", result) + } + + return nil +} + +// Maintain does nothing: a command puzzle provider has no housekeeping +func (pc PuzzleCommand) Maintain(updateInterval time.Duration) { +} diff --git a/cmd/mothd/puzzlecmd_test.go b/cmd/mothd/puzzlecmd_test.go new file mode 100644 index 0000000..f3a9e06 --- /dev/null +++ b/cmd/mothd/puzzlecmd_test.go @@ -0,0 +1,62 @@ +package main + +import ( + "io/ioutil" + "os/exec" + "testing" +) + +func TestPuzzleCommand(t *testing.T) { + pc := PuzzleCommand{ + Path: "testdata/testpiler.sh", + } + + inv := pc.Inventory() + if len(inv) != 2 { + t.Errorf("Wrong length for inventory") + } + for _, cat := range inv { + switch cat.Name { + case "pategory": + if len(cat.Puzzles) != 8 { + t.Errorf("pategory wrong number of puzzles: %d", len(cat.Puzzles)) + } + if cat.Puzzles[5] != 10 { + t.Errorf("pategory puzzles[5] wrong value: %d", cat.Puzzles[5]) + } + case "nealegory": + if len(cat.Puzzles) != 3 { + t.Errorf("nealegoy wrong number of puzzles: %d", len(cat.Puzzles)) + } + } + } + + if err := pc.CheckAnswer("pategory", 1, "answer"); err != nil { + t.Errorf("Correct answer for pategory: %v", err) + } + if err := pc.CheckAnswer("pategory", 1, "wrong"); err == nil { + t.Errorf("Wrong answer for pategory judged correct") + } + + if err := pc.CheckAnswer("pategory", 2, "answer"); err == nil { + t.Errorf("Internal error not returned") + } else if ee, ok := err.(*exec.ExitError); ok { + if string(ee.Stderr) != "Internal error\n" { + t.Errorf("Unexpected error returned: %#v", string(ee.Stderr)) + } + } else if err.Error() != "moo" { + t.Error(err) + } + + if f, _, err := pc.Open("pategory", 1, "moo.txt"); err != nil { + t.Error(err) + } else if buf, err := ioutil.ReadAll(f); err != nil { + f.Close() + t.Error(err) + } else if string(buf) != "Moo.\n" { + f.Close() + t.Errorf("Wrong contents: %#v", string(buf)) + } else { + f.Close() + } +} diff --git a/cmd/mothd/testdata/testpiler.sh b/cmd/mothd/testdata/testpiler.sh new file mode 100755 index 0000000..aff3e9f --- /dev/null +++ b/cmd/mothd/testdata/testpiler.sh @@ -0,0 +1,30 @@ +#! /bin/sh -e + +case "$action:$cat:$points" in + inventory::) + echo "pategory 1 2 3 4 5 10 20 300" + echo "nealegory 1 2 3" + ;; + open:*:*) + if [ "$path" = "moo.txt" ]; then + echo "Moo." + else + cat $cat_$points_$path + fi + ;; + answer:pategory:1) + if [ "$answer" = "answer" ]; then + echo "correct" + else + echo "Sorry, wrong answer." + fi + ;; + answer:pategory:2) + echo "Internal error" 1>&2 + exit 1 + ;; + *) + echo "ERROR: Unknown action: $action" 1>&2 + exit 1 + ;; +esac