mirror of https://github.com/dirtbags/moth.git
category.go working well
This commit is contained in:
parent
36dd72f3e9
commit
31a50cbf2c
|
@ -6,11 +6,15 @@ 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],
|
||||||
|
"nealegory": [1, 3, 2]
|
||||||
|
}
|
||||||
|
EOT
|
||||||
;;
|
;;
|
||||||
open:*:*)
|
open:*:*)
|
||||||
case "$CAT:$POINTS:$FILENAME" in
|
case "$CAT:$POINTS:$FILENAME" in
|
||||||
*:*:moo.txt)
|
*:*:moo.txt)
|
||||||
echo "Moo."
|
echo "Moo."
|
||||||
|
@ -20,17 +24,17 @@ open:*:*)
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
;;
|
;;
|
||||||
answer:pategory:1)
|
answer:pategory:1)
|
||||||
if [ "$ANSWER" = "answer" ]; then
|
if [ "$ANSWER" = "answer" ]; then
|
||||||
echo "correct"
|
echo "correct"
|
||||||
else
|
else
|
||||||
echo "Sorry, wrong answer."
|
echo "Sorry, wrong answer."
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
answer:pategory:2)
|
answer:pategory:2)
|
||||||
fail "Internal error"
|
fail "Internal error"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
fail "ERROR: Unknown action: $action"
|
fail "ERROR: Unknown action: $action"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
|
@ -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)
|
||||||
|
@ -88,24 +103,80 @@ 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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
|
@ -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() {
|
||||||
|
|
|
@ -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())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
Loading…
Reference in New Issue