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