category.go working well

This commit is contained in:
Neale Pickett 2020-09-04 15:29:06 -06:00
parent 36dd72f3e9
commit 31a50cbf2c
13 changed files with 281 additions and 48 deletions

View File

@ -6,31 +6,35 @@ fail () {
} }
case "$ACTION:$CAT:$POINTS" in case "$ACTION:$CAT:$POINTS" in
inventory::) inventory::)
echo "pategory 1 2 3 4 5 10 20 300" cat <<EOT
echo "nealegory 1 3 2" {
;; "pategory": [1, 2, 3, 4, 5, 10, 20, 300],
open:*:*) "nealegory": [1, 3, 2]
case "$CAT:$POINTS:$FILENAME" in }
*:*:moo.txt) EOT
echo "Moo." ;;
open:*:*)
case "$CAT:$POINTS:$FILENAME" in
*:*:moo.txt)
echo "Moo."
;;
*)
fail "Cannot open: $FILENAME"
;;
esac
;;
answer:pategory:1)
if [ "$ANSWER" = "answer" ]; then
echo "correct"
else
echo "Sorry, wrong answer."
fi
;;
answer:pategory:2)
fail "Internal error"
;; ;;
*) *)
fail "Cannot open: $FILENAME" fail "ERROR: Unknown action: $action"
;; ;;
esac
;;
answer:pategory:1)
if [ "$ANSWER" = "answer" ]; then
echo "correct"
else
echo "Sorry, wrong answer."
fi
;;
answer:pategory:2)
fail "Internal error"
;;
*)
fail "ERROR: Unknown action: $action"
;;
esac esac

View File

@ -1,10 +1,16 @@
package main package main
import ( import (
"fmt" "bytes"
"context"
"encoding/json"
"io" "io"
"io/ioutil"
"log" "log"
"os/exec"
"strconv" "strconv"
"strings"
"time"
"github.com/spf13/afero" "github.com/spf13/afero"
) )
@ -26,11 +32,20 @@ func (n NopReadCloser) Close() error {
// NewFsCategory returns a Category based on which files are present. // NewFsCategory returns a Category based on which files are present.
// If 'mkcategory' is present and executable, an FsCommandCategory is returned. // If 'mkcategory' is present and executable, an FsCommandCategory is returned.
// Otherwise, FsCategory is returned. // Otherwise, FsCategory is returned.
func NewFsCategory(fs afero.Fs) Category { func NewFsCategory(fs afero.Fs, cat string) Category {
if info, err := fs.Stat("mkcategory"); (err == nil) && (info.Mode()&0100 != 0) { bfs := NewRecursiveBasePathFs(fs, cat)
return FsCommandCategory{fs: fs} if info, err := bfs.Stat("mkcategory"); (err == nil) && (info.Mode()&0100 != 0) {
if command, err := bfs.RealPath(info.Name()); err != nil {
log.Println("Unable to resolve full path to", info.Name(), bfs)
} else {
return FsCommandCategory{
fs: bfs,
command: command,
timeout: 2 * time.Second,
}
}
} }
return FsCategory{fs: fs} return FsCategory{fs: bfs}
} }
// FsCategory provides a category backed by a .md file. // FsCategory provides a category backed by a .md file.
@ -70,7 +85,7 @@ func (c FsCategory) Open(points int, filename string) (io.ReadCloser, error) {
return NewFsPuzzle(c.fs, points).Open(filename) return NewFsPuzzle(c.fs, points).Open(filename)
} }
// Answer check whether an answer is correct. // Answer checks whether an answer is correct.
func (c FsCategory) Answer(points int, answer string) bool { func (c FsCategory) Answer(points int, answer string) bool {
// BUG(neale): FsCategory.Answer should probably always return false, to prevent you from running uncompiled puzzles with participants. // BUG(neale): FsCategory.Answer should probably always return false, to prevent you from running uncompiled puzzles with participants.
p, err := c.Puzzle(points) p, err := c.Puzzle(points)
@ -87,25 +102,81 @@ func (c FsCategory) Answer(points int, answer string) bool {
// FsCommandCategory provides a category backed by running an external command. // FsCommandCategory provides a category backed by running an external command.
type FsCommandCategory struct { type FsCommandCategory struct {
fs afero.Fs fs afero.Fs
command string
timeout time.Duration
} }
// Inventory returns a list of point values for this category. // Inventory returns a list of point values for this category.
func (c FsCommandCategory) Inventory() ([]int, error) { func (c FsCommandCategory) Inventory() ([]int, error) {
return nil, fmt.Errorf("Not implemented") ctx, cancel := context.WithTimeout(context.Background(), c.timeout)
defer cancel()
cmd := exec.CommandContext(ctx, c.command, "inventory")
stdout, err := cmd.Output()
if err != nil {
return nil, err
}
ret := make([]int, 0)
if err := json.Unmarshal(stdout, &ret); err != nil {
return nil, err
}
return ret, nil
} }
// Puzzle returns a Puzzle structure for the given point value. // Puzzle returns a Puzzle structure for the given point value.
func (c FsCommandCategory) Puzzle(points int) (Puzzle, error) { func (c FsCommandCategory) Puzzle(points int) (Puzzle, error) {
return Puzzle{}, fmt.Errorf("Not implemented") var p Puzzle
ctx, cancel := context.WithTimeout(context.Background(), c.timeout)
defer cancel()
cmd := exec.CommandContext(ctx, c.command, "puzzle", strconv.Itoa(points))
stdout, err := cmd.Output()
if err != nil {
return p, err
}
if err := json.Unmarshal(stdout, &p); err != nil {
return p, err
}
return p, nil
} }
// Open returns an io.ReadCloser for the given filename. // Open returns an io.ReadCloser for the given filename.
func (c FsCommandCategory) Open(points int, filename string) (io.ReadCloser, error) { func (c FsCommandCategory) Open(points int, filename string) (io.ReadCloser, error) {
return NopReadCloser{}, fmt.Errorf("Not implemented") ctx, cancel := context.WithTimeout(context.Background(), c.timeout)
defer cancel()
cmd := exec.CommandContext(ctx, c.command, "file", strconv.Itoa(points), filename)
stdout, err := cmd.Output()
buf := ioutil.NopCloser(bytes.NewBuffer(stdout))
if err != nil {
return buf, err
}
return buf, nil
} }
// Answer check whether an answer is correct. // Answer checks whether an answer is correct.
func (c FsCommandCategory) Answer(points int, answer string) bool { func (c FsCommandCategory) Answer(points int, answer string) bool {
ctx, cancel := context.WithTimeout(context.Background(), c.timeout)
defer cancel()
cmd := exec.CommandContext(ctx, c.command, "answer", strconv.Itoa(points), answer)
stdout, err := cmd.Output()
if err != nil {
log.Printf("ERROR: Answering %d points: %s", points, err)
return false
}
switch strings.TrimSpace(string(stdout)) {
case "correct":
return true
}
return false return false
} }

View File

@ -0,0 +1,110 @@
package main
import (
"bytes"
"io"
"testing"
"github.com/spf13/afero"
)
func TestFsCategory(t *testing.T) {
c := NewFsCategory(newTestFs(), "cat0")
if inv, err := c.Inventory(); err != nil {
t.Error(err)
} else if len(inv) != 9 {
t.Error("Inventory wrong length", inv)
}
if p, err := c.Puzzle(1); err != nil {
t.Error(err)
} else if len(p.Answers) != 1 {
t.Error("Wrong length for answers", p.Answers)
} else if p.Answers[0] != "YAML answer" {
t.Error("Wrong answer list", p.Answers)
} else if !c.Answer(1, p.Answers[0]) {
t.Error("Correct answer not accepted")
}
if c.Answer(1, "incorrect answer") {
t.Error("Incorrect answer accepted as correct")
}
if r, err := c.Open(1, "moo.txt"); err != nil {
t.Error(err)
} else {
defer r.Close()
buf := new(bytes.Buffer)
if _, err := io.Copy(buf, r); err != nil {
t.Error(err)
}
if buf.String() != "Moo." {
t.Error("Opened file contents wrong")
}
}
if r, err := c.Open(1, "error"); err == nil {
r.Close()
t.Error("File wasn't supposed to exist")
}
}
func TestOsFsCategory(t *testing.T) {
fs := NewRecursiveBasePathFs(afero.NewOsFs(), "testdata")
static := NewFsCategory(fs, "static")
if p, err := static.Puzzle(1); err != nil {
t.Error(err)
} else if len(p.Pre.Authors) != 1 {
t.Error("Wrong authors list", p.Pre.Authors)
} else if p.Pre.Authors[0] != "neale" {
t.Error("Wrong authors", p.Pre.Authors)
}
generated := NewFsCategory(fs, "generated")
if inv, err := generated.Inventory(); err != nil {
t.Error(err)
} else if len(inv) != 5 {
t.Error("Wrong inventory", inv)
}
if p, err := generated.Puzzle(1); err != nil {
t.Error(err)
} else if len(p.Answers) != 1 {
t.Error("Wrong answers", p.Answers)
} else if p.Answers[0] != "answer1.0" {
t.Error("Wrong answers:", p.Answers)
}
if _, err := generated.Puzzle(20); err == nil {
t.Error("Puzzle shouldn't exist")
}
if r, err := generated.Open(1, "moo.txt"); err != nil {
t.Error(err)
} else {
defer r.Close()
buf := new(bytes.Buffer)
if _, err := io.Copy(buf, r); err != nil {
t.Error(err)
}
if buf.String() != "Moo.\n" {
t.Errorf("Wrong body: %#v", buf.String())
}
}
if r, err := generated.Open(1, "fail"); err == nil {
r.Close()
t.Error("File shouldn't exist")
}
if !generated.Answer(1, "answer1.0") {
t.Error("Correct answer failed")
}
if generated.Answer(1, "wrong") {
t.Error("Incorrect answer didn't fail")
}
if generated.Answer(2, "error") {
t.Error("Error answer didn't fail")
}
}

View File

@ -102,6 +102,8 @@ func (t *T) Handle(action string) error {
// PrintInventory prints a puzzle inventory to stdout // PrintInventory prints a puzzle inventory to stdout
func (t *T) PrintInventory() error { func (t *T) PrintInventory() error {
inv := make(map[string][]int)
dirEnts, err := afero.ReadDir(t.Fs, ".") dirEnts, err := afero.ReadDir(t.Fs, ".")
if err != nil { if err != nil {
return err return err
@ -113,16 +115,16 @@ func (t *T) PrintInventory() error {
log.Print(err) log.Print(err)
continue continue
} else { } else {
fmt.Fprint(t.w, ent.Name())
sort.Ints(puzzles) sort.Ints(puzzles)
for _, points := range puzzles { inv[ent.Name()] = puzzles
fmt.Fprint(t.w, " ")
fmt.Fprint(t.w, points)
}
fmt.Fprintln(t.w)
} }
} }
} }
m := json.NewEncoder(t.w)
if err := m.Encode(inv); err != nil {
return err
}
return nil return nil
} }
@ -157,7 +159,7 @@ func (t *T) Open() error {
// NewCategory returns a new Fs-backed category. // NewCategory returns a new Fs-backed category.
func (t *T) NewCategory(name string) Category { func (t *T) NewCategory(name string) Category {
return NewFsCategory(NewRecursiveBasePathFs(t.Fs, name)) return NewFsCategory(t.Fs, name)
} }
func main() { func main() {

View File

@ -3,6 +3,7 @@ package main
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"strings"
"testing" "testing"
"github.com/spf13/afero" "github.com/spf13/afero"
@ -61,7 +62,7 @@ func TestEverything(t *testing.T) {
if err := tp.Handle("inventory"); err != nil { if err := tp.Handle("inventory"); err != nil {
t.Error(err) t.Error(err)
} }
if stdout.String() != "cat0 1 2 3 4 5 10 20 21 22\ncat1 93\n" { if strings.TrimSpace(stdout.String()) != `{"cat0":[1,2,3,4,5,10,20,21,22],"cat1":[93]}` {
t.Errorf("Bad inventory: %#v", stdout.String()) t.Errorf("Bad inventory: %#v", stdout.String())
} }

View File

@ -227,12 +227,12 @@ func (fp FsCommandPuzzle) Open(filename string) (io.ReadCloser, error) {
cmd := exec.CommandContext(ctx, fp.command, "-file", filename) cmd := exec.CommandContext(ctx, fp.command, "-file", filename)
// BUG(neale): FsCommandPuzzle.Open() reads everything into memory, and will suck for large files. // BUG(neale): FsCommandPuzzle.Open() reads everything into memory, and will suck for large files.
out, err := cmd.Output() out, err := cmd.Output()
buf := ioutil.NopCloser(bytes.NewBuffer(out))
if err != nil { if err != nil {
return NopReadCloser{}, err return buf, err
} }
buf := bytes.NewBuffer(out)
return ioutil.NopCloser(buf), nil return buf, nil
} }
// Answer checks whether the given answer is correct. // Answer checks whether the given answer is correct.
@ -243,7 +243,7 @@ func (fp FsCommandPuzzle) Answer(answer string) bool {
cmd := exec.CommandContext(ctx, fp.command, "-answer", answer) cmd := exec.CommandContext(ctx, fp.command, "-answer", answer)
out, err := cmd.Output() out, err := cmd.Output()
if err != nil { if err != nil {
log.Print("ERROR", err) log.Printf("ERROR: checking answer: %s", err)
return false return false
} }

View File

@ -87,7 +87,7 @@ func TestPuzzle(t *testing.T) {
} }
func TestFsPuzzle(t *testing.T) { func TestFsPuzzle(t *testing.T) {
catFs := NewRecursiveBasePathFs(afero.NewOsFs(), "testdata") catFs := NewRecursiveBasePathFs(NewRecursiveBasePathFs(afero.NewOsFs(), "testdata"), "static")
if _, err := NewFsPuzzle(catFs, 1).Puzzle(); err != nil { if _, err := NewFsPuzzle(catFs, 1).Puzzle(); err != nil {
t.Error(err) t.Error(err)

45
cmd/transpile/testdata/generated/mkcategory vendored Executable file
View File

@ -0,0 +1,45 @@
#! /bin/sh -e
fail () {
echo "ERROR: $*" 1>&2
exit 1
}
case $1:$2:$3 in
inventory::)
echo "[1,2,3,"
echo "4,5]"
;;
puzzle:1:)
cat <<EOT
{
"Answers": ["answer1.0"],
"Pre": {
"Authors": ["author1.0"],
"Body": "<h1>moo.</h1>"
}
}
EOT
;;
puzzle:*:)
fail "No such puzzle"
;;
file:1:moo.txt)
echo "Moo."
;;
file:*:*)
fail "No such file: $2"
;;
answer:1:answer1.0)
echo "correct"
;;
answer:1:*)
echo "incorrect"
;;
answer:*:*)
fail "Fail answer"
;;
*)
fail "What is $1" 1>&2
;;
esac