diff --git a/cmd/transpile/basepath.go b/cmd/transpile/basepath.go index 4dddc40..722be1c 100644 --- a/cmd/transpile/basepath.go +++ b/cmd/transpile/basepath.go @@ -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) { diff --git a/cmd/transpile/category.go b/cmd/transpile/category.go index f2ab73a..f11198f 100644 --- a/cmd/transpile/category.go +++ b/cmd/transpile/category.go @@ -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 } diff --git a/cmd/transpile/main.go b/cmd/transpile/main.go index 9562746..b40bb7e 100644 --- a/cmd/transpile/main.go +++ b/cmd/transpile/main.go @@ -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() { diff --git a/cmd/transpile/puzzle.go b/cmd/transpile/puzzle.go index cf6d5a9..50c9d28 100644 --- a/cmd/transpile/puzzle.go +++ b/cmd/transpile/puzzle.go @@ -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 + 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 } diff --git a/cmd/transpile/puzzle_test.go b/cmd/transpile/puzzle_test.go index 9b6a6d9..c0bd933 100644 --- a/cmd/transpile/puzzle_test.go +++ b/cmd/transpile/puzzle_test.go @@ -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") + } } diff --git a/cmd/transpile/puzzlehandler.go b/cmd/transpile/puzzlehandler.go deleted file mode 100644 index 06ab7d0..0000000 --- a/cmd/transpile/puzzlehandler.go +++ /dev/null @@ -1 +0,0 @@ -package main diff --git a/cmd/transpile/testdata/3/mkpuzzle b/cmd/transpile/testdata/3/mkpuzzle index d7d868b..df19db0 100755 --- a/cmd/transpile/testdata/3/mkpuzzle +++ b/cmd/transpile/testdata/3/mkpuzzle @@ -12,11 +12,33 @@ case $1 in } EOT ;; - moo.txt) - echo "Moo." + -file|--file) + 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 ;; esac