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
|
||||
inventory::)
|
||||
echo "pategory 1 2 3 4 5 10 20 300"
|
||||
echo "nealegory 1 3 2"
|
||||
inventory::)
|
||||
cat <<EOT
|
||||
{
|
||||
"pategory": [1, 2, 3, 4, 5, 10, 20, 300],
|
||||
"nealegory": [1, 3, 2]
|
||||
}
|
||||
EOT
|
||||
;;
|
||||
open:*:*)
|
||||
open:*:*)
|
||||
case "$CAT:$POINTS:$FILENAME" in
|
||||
*:*:moo.txt)
|
||||
echo "Moo."
|
||||
|
@ -20,17 +24,17 @@ open:*:*)
|
|||
;;
|
||||
esac
|
||||
;;
|
||||
answer:pategory:1)
|
||||
answer:pategory:1)
|
||||
if [ "$ANSWER" = "answer" ]; then
|
||||
echo "correct"
|
||||
else
|
||||
echo "Sorry, wrong answer."
|
||||
fi
|
||||
;;
|
||||
answer:pategory:2)
|
||||
answer:pategory:2)
|
||||
fail "Internal error"
|
||||
;;
|
||||
*)
|
||||
*)
|
||||
fail "ERROR: Unknown action: $action"
|
||||
;;
|
||||
esac
|
||||
|
|
|
@ -1,10 +1,16 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
@ -26,11 +32,20 @@ func (n NopReadCloser) Close() error {
|
|||
// NewFsCategory returns a Category based on which files are present.
|
||||
// If 'mkcategory' is present and executable, an FsCommandCategory is returned.
|
||||
// Otherwise, FsCategory is returned.
|
||||
func NewFsCategory(fs afero.Fs) Category {
|
||||
if info, err := fs.Stat("mkcategory"); (err == nil) && (info.Mode()&0100 != 0) {
|
||||
return FsCommandCategory{fs: fs}
|
||||
func NewFsCategory(fs afero.Fs, cat string) Category {
|
||||
bfs := NewRecursiveBasePathFs(fs, cat)
|
||||
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.
|
||||
|
@ -70,7 +85,7 @@ func (c FsCategory) Open(points int, filename string) (io.ReadCloser, error) {
|
|||
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 {
|
||||
// BUG(neale): FsCategory.Answer should probably always return false, to prevent you from running uncompiled puzzles with participants.
|
||||
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.
|
||||
type FsCommandCategory struct {
|
||||
fs afero.Fs
|
||||
command string
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
// Inventory returns a list of point values for this category.
|
||||
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.
|
||||
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.
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
func (t *T) PrintInventory() error {
|
||||
inv := make(map[string][]int)
|
||||
|
||||
dirEnts, err := afero.ReadDir(t.Fs, ".")
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -113,16 +115,16 @@ func (t *T) PrintInventory() error {
|
|||
log.Print(err)
|
||||
continue
|
||||
} else {
|
||||
fmt.Fprint(t.w, ent.Name())
|
||||
sort.Ints(puzzles)
|
||||
for _, points := range puzzles {
|
||||
fmt.Fprint(t.w, " ")
|
||||
fmt.Fprint(t.w, points)
|
||||
}
|
||||
fmt.Fprintln(t.w)
|
||||
inv[ent.Name()] = puzzles
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m := json.NewEncoder(t.w)
|
||||
if err := m.Encode(inv); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -157,7 +159,7 @@ func (t *T) Open() error {
|
|||
|
||||
// NewCategory returns a new Fs-backed category.
|
||||
func (t *T) NewCategory(name string) Category {
|
||||
return NewFsCategory(NewRecursiveBasePathFs(t.Fs, name))
|
||||
return NewFsCategory(t.Fs, name)
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
|
|
@ -3,6 +3,7 @@ package main
|
|||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
|
@ -61,7 +62,7 @@ func TestEverything(t *testing.T) {
|
|||
if err := tp.Handle("inventory"); err != nil {
|
||||
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())
|
||||
}
|
||||
|
||||
|
|
|
@ -227,12 +227,12 @@ func (fp FsCommandPuzzle) Open(filename string) (io.ReadCloser, error) {
|
|||
cmd := exec.CommandContext(ctx, fp.command, "-file", filename)
|
||||
// BUG(neale): FsCommandPuzzle.Open() reads everything into memory, and will suck for large files.
|
||||
out, err := cmd.Output()
|
||||
buf := ioutil.NopCloser(bytes.NewBuffer(out))
|
||||
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.
|
||||
|
@ -243,7 +243,7 @@ func (fp FsCommandPuzzle) Answer(answer string) bool {
|
|||
cmd := exec.CommandContext(ctx, fp.command, "-answer", answer)
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
log.Print("ERROR", err)
|
||||
log.Printf("ERROR: checking answer: %s", err)
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
|
@ -87,7 +87,7 @@ func TestPuzzle(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 {
|
||||
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