mirror of https://github.com/dirtbags/moth.git
puzzle.go looking good
This commit is contained in:
parent
f9cabc5255
commit
36dd72f3e9
|
@ -9,24 +9,24 @@ import (
|
|||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
// BasePathFs is an overloaded afero.BasePathFs that has a recursive RealPath().
|
||||
type BasePathFs struct {
|
||||
// RecursiveBasePathFs is an overloaded afero.BasePathFs that has a recursive RealPath().
|
||||
type RecursiveBasePathFs struct {
|
||||
afero.Fs
|
||||
source afero.Fs
|
||||
path string
|
||||
}
|
||||
|
||||
// NewBasePathFs returns a new BasePathFs.
|
||||
func NewBasePathFs(source afero.Fs, path string) afero.Fs {
|
||||
return &BasePathFs{
|
||||
// NewRecursiveBasePathFs returns a new RecursiveBasePathFs.
|
||||
func NewRecursiveBasePathFs(source afero.Fs, path string) *RecursiveBasePathFs {
|
||||
return &RecursiveBasePathFs{
|
||||
Fs: afero.NewBasePathFs(source, path),
|
||||
source: source,
|
||||
path: path,
|
||||
}
|
||||
}
|
||||
|
||||
// RealPath returns the real path to a file, "breaking out" of the BasePathFs.
|
||||
func (b *BasePathFs) RealPath(name string) (path string, err error) {
|
||||
// RealPath returns the real path to a file, "breaking out" of the RecursiveBasePathFs.
|
||||
func (b *RecursiveBasePathFs) RealPath(name string) (path string, err error) {
|
||||
if err := validateBasePathName(name); err != nil {
|
||||
return name, err
|
||||
}
|
||||
|
@ -34,10 +34,10 @@ func (b *BasePathFs) RealPath(name string) (path string, err error) {
|
|||
bpath := filepath.Clean(b.path)
|
||||
path = filepath.Clean(filepath.Join(bpath, name))
|
||||
|
||||
if parentBasePathFs, ok := b.source.(*BasePathFs); ok {
|
||||
return parentBasePathFs.RealPath(path)
|
||||
} else if parentBasePathFs, ok := b.source.(*afero.BasePathFs); ok {
|
||||
return parentBasePathFs.RealPath(path)
|
||||
if parentRecursiveBasePathFs, ok := b.source.(*RecursiveBasePathFs); ok {
|
||||
return parentRecursiveBasePathFs.RealPath(path)
|
||||
} else if parentRecursiveBasePathFs, ok := b.source.(*afero.BasePathFs); ok {
|
||||
return parentRecursiveBasePathFs.RealPath(path)
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(path, bpath) {
|
||||
|
|
|
@ -9,12 +9,16 @@ import (
|
|||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
// NopReadCloser provides an io.ReadCloser which does nothing.
|
||||
type NopReadCloser struct {
|
||||
}
|
||||
|
||||
// Read satisfies io.Reader.
|
||||
func (n NopReadCloser) Read(b []byte) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// Close satisfies io.Closer.
|
||||
func (n NopReadCloser) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
@ -25,16 +29,16 @@ func (n NopReadCloser) Close() error {
|
|||
func NewFsCategory(fs afero.Fs) Category {
|
||||
if info, err := fs.Stat("mkcategory"); (err == nil) && (info.Mode()&0100 != 0) {
|
||||
return FsCommandCategory{fs: fs}
|
||||
} else {
|
||||
return FsCategory{fs: fs}
|
||||
}
|
||||
return FsCategory{fs: fs}
|
||||
}
|
||||
|
||||
// FsCategory provides a category backed by a .md file.
|
||||
type FsCategory struct {
|
||||
fs afero.Fs
|
||||
}
|
||||
|
||||
// Category returns a list of puzzle values.
|
||||
// Inventory returns a list of point values for this category.
|
||||
func (c FsCategory) Inventory() ([]int, error) {
|
||||
puzzleEntries, err := afero.ReadDir(c.fs, ".")
|
||||
if err != nil {
|
||||
|
@ -56,14 +60,17 @@ func (c FsCategory) Inventory() ([]int, error) {
|
|||
return puzzles, nil
|
||||
}
|
||||
|
||||
// Puzzle returns a Puzzle structure for the given point value.
|
||||
func (c FsCategory) Puzzle(points int) (Puzzle, error) {
|
||||
return NewFsPuzzle(c.fs, points).Puzzle()
|
||||
}
|
||||
|
||||
// Open returns an io.ReadCloser for the given filename.
|
||||
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.
|
||||
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)
|
||||
|
@ -78,22 +85,27 @@ func (c FsCategory) Answer(points int, answer string) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// FsCommandCategory provides a category backed by running an external command.
|
||||
type FsCommandCategory struct {
|
||||
fs afero.Fs
|
||||
}
|
||||
|
||||
// Inventory returns a list of point values for this category.
|
||||
func (c FsCommandCategory) Inventory() ([]int, error) {
|
||||
return nil, fmt.Errorf("Not implemented")
|
||||
}
|
||||
|
||||
// Puzzle returns a Puzzle structure for the given point value.
|
||||
func (c FsCommandCategory) Puzzle(points int) (Puzzle, error) {
|
||||
return Puzzle{}, fmt.Errorf("Not implemented")
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
// Answer check whether an answer is correct.
|
||||
func (c FsCommandCategory) Answer(points int, answer string) bool {
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ type Category interface {
|
|||
Answer(points int, answer string) bool
|
||||
}
|
||||
|
||||
// PuzzleDef contains everything about a puzzle.
|
||||
// Puzzle contains everything about a puzzle.
|
||||
type Puzzle struct {
|
||||
Pre struct {
|
||||
Authors []string
|
||||
|
@ -155,8 +155,9 @@ func (t *T) Open() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// NewCategory returns a new Fs-backed category.
|
||||
func (t *T) NewCategory(name string) Category {
|
||||
return NewFsCategory(NewBasePathFs(t.Fs, name))
|
||||
return NewFsCategory(NewRecursiveBasePathFs(t.Fs, name))
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/mail"
|
||||
"os/exec"
|
||||
|
@ -19,13 +20,36 @@ import (
|
|||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// PuzzleProvider establishes the functionality required to provide one puzzle.
|
||||
type PuzzleProvider interface {
|
||||
// Puzzle returns a Puzzle struct for the current puzzle.
|
||||
Puzzle() (Puzzle, error)
|
||||
|
||||
// Open returns a newly-opened file.
|
||||
Open(filename string) (io.ReadCloser, error)
|
||||
|
||||
// Answer returns whether the provided answer is correct.
|
||||
Answer(answer string) bool
|
||||
}
|
||||
|
||||
// NewFsPuzzle returns a new FsPuzzle for points.
|
||||
func NewFsPuzzle(fs afero.Fs, points int) *FsPuzzle {
|
||||
fp := &FsPuzzle{
|
||||
fs: NewBasePathFs(fs, strconv.Itoa(points)),
|
||||
func NewFsPuzzle(fs afero.Fs, points int) PuzzleProvider {
|
||||
pfs := NewRecursiveBasePathFs(fs, strconv.Itoa(points))
|
||||
if info, err := pfs.Stat("mkpuzzle"); (err == nil) && (info.Mode()&0100 != 0) {
|
||||
if command, err := pfs.RealPath(info.Name()); err != nil {
|
||||
log.Println("Unable to resolve full path to", info.Name(), pfs)
|
||||
} else {
|
||||
return FsCommandPuzzle{
|
||||
fs: pfs,
|
||||
command: command,
|
||||
timeout: 2 * time.Second,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fp
|
||||
return FsPuzzle{
|
||||
fs: pfs,
|
||||
}
|
||||
}
|
||||
|
||||
// FsPuzzle is a single puzzle's directory.
|
||||
|
@ -162,29 +186,24 @@ func rfc822HeaderParser(r io.Reader) (Puzzle, error) {
|
|||
return p, nil
|
||||
}
|
||||
|
||||
// Answer checks whether the given answer is correct.
|
||||
func (fp FsPuzzle) Answer(answer string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// FsCommandPuzzle provides an FsPuzzle backed by running a command.
|
||||
type FsCommandPuzzle struct {
|
||||
fs afero.Fs
|
||||
command string
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
// Puzzle returns a Puzzle struct for the current puzzle.
|
||||
func (fp FsCommandPuzzle) Puzzle() (Puzzle, error) {
|
||||
bfs, ok := fp.fs.(*BasePathFs)
|
||||
if !ok {
|
||||
return Puzzle{}, fmt.Errorf("Fs won't resolve real paths for %v", fp)
|
||||
}
|
||||
mkpuzzlePath, err := bfs.RealPath("mkpuzzle")
|
||||
if err != nil {
|
||||
return Puzzle{}, err
|
||||
}
|
||||
log.Print(mkpuzzlePath)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), fp.timeout)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, mkpuzzlePath)
|
||||
cmd := exec.CommandContext(ctx, fp.command)
|
||||
stdout, err := cmd.Output()
|
||||
if err != nil {
|
||||
return Puzzle{}, err
|
||||
|
@ -200,10 +219,37 @@ func (fp FsCommandPuzzle) Puzzle() (Puzzle, error) {
|
|||
return puzzle, nil
|
||||
}
|
||||
|
||||
// Open returns a newly-opened file.
|
||||
func (fp FsCommandPuzzle) Open(filename string) (io.ReadCloser, error) {
|
||||
return NopReadCloser{}, fmt.Errorf("Not implemented")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), fp.timeout)
|
||||
defer cancel()
|
||||
|
||||
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()
|
||||
if err != nil {
|
||||
return NopReadCloser{}, err
|
||||
}
|
||||
buf := bytes.NewBuffer(out)
|
||||
|
||||
return ioutil.NopCloser(buf), nil
|
||||
}
|
||||
|
||||
// Answer checks whether the given answer is correct.
|
||||
func (fp FsCommandPuzzle) Answer(answer string) bool {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), fp.timeout)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, fp.command, "-answer", answer)
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
log.Print("ERROR", err)
|
||||
return false
|
||||
}
|
||||
|
||||
switch strings.TrimSpace(string(out)) {
|
||||
case "correct":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ import (
|
|||
|
||||
func TestPuzzle(t *testing.T) {
|
||||
puzzleFs := newTestFs()
|
||||
catFs := afero.NewBasePathFs(puzzleFs, "cat0")
|
||||
catFs := NewRecursiveBasePathFs(puzzleFs, "cat0")
|
||||
|
||||
{
|
||||
pd := NewFsPuzzle(catFs, 1)
|
||||
|
@ -70,10 +70,24 @@ func TestPuzzle(t *testing.T) {
|
|||
t.Log(p)
|
||||
t.Error("Wrong error for duplicate body:", err)
|
||||
}
|
||||
|
||||
{
|
||||
fs := afero.NewMemMapFs()
|
||||
if err := afero.WriteFile(fs, "1/mkpuzzle", []byte("bleat"), 0755); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
p := NewFsPuzzle(fs, 1)
|
||||
if _, ok := p.(FsCommandPuzzle); !ok {
|
||||
t.Error("We didn't get an FsCommandPuzzle")
|
||||
}
|
||||
if _, err := p.Puzzle(); err == nil {
|
||||
t.Error("We didn't get an error trying to run a command from a MemMapFs")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFsPuzzle(t *testing.T) {
|
||||
catFs := afero.NewBasePathFs(afero.NewOsFs(), "testdata")
|
||||
catFs := NewRecursiveBasePathFs(afero.NewOsFs(), "testdata")
|
||||
|
||||
if _, err := NewFsPuzzle(catFs, 1).Puzzle(); err != nil {
|
||||
t.Error(err)
|
||||
|
@ -88,16 +102,31 @@ func TestFsPuzzle(t *testing.T) {
|
|||
t.Error(err)
|
||||
}
|
||||
|
||||
if body, err := mkpuzzleDir.Open("moo.txt"); err != nil {
|
||||
if r, err := mkpuzzleDir.Open("moo.txt"); err != nil {
|
||||
t.Error(err)
|
||||
} else {
|
||||
defer body.Close()
|
||||
defer r.Close()
|
||||
buf := new(bytes.Buffer)
|
||||
if _, err := io.Copy(buf, body); err != nil {
|
||||
if _, err := io.Copy(buf, r); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if buf.String() != "Moo.\n" {
|
||||
t.Error("Wrong body")
|
||||
t.Errorf("Wrong body: %#v", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
if r, err := mkpuzzleDir.Open("error"); err == nil {
|
||||
r.Close()
|
||||
t.Error("Error open didn't return error")
|
||||
}
|
||||
|
||||
if !mkpuzzleDir.Answer("moo") {
|
||||
t.Error("Right answer marked wrong")
|
||||
}
|
||||
if mkpuzzleDir.Answer("wrong") {
|
||||
t.Error("Wrong answer marked correct")
|
||||
}
|
||||
if mkpuzzleDir.Answer("error") {
|
||||
t.Error("Error answer marked correct")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
package main
|
|
@ -12,11 +12,33 @@ case $1 in
|
|||
}
|
||||
EOT
|
||||
;;
|
||||
-file|--file)
|
||||
case $2 in
|
||||
moo.txt)
|
||||
echo "Moo."
|
||||
;;
|
||||
*)
|
||||
echo "Error: no such file: $1" 1>&2
|
||||
echo "ERROR: no such file: $1" 1>&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
-answer|--answer)
|
||||
case $2 in
|
||||
moo)
|
||||
echo "correct"
|
||||
;;
|
||||
error)
|
||||
echo "error" 1>&2
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
echo "incorrect"
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: don't know what to do with $1" 1>&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
|
Loading…
Reference in New Issue