puzzle.go looking good

This commit is contained in:
Neale Pickett 2020-09-04 13:00:23 -06:00
parent f9cabc5255
commit 36dd72f3e9
7 changed files with 153 additions and 44 deletions

View File

@ -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) {

View File

@ -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
} }

View File

@ -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() {

View File

@ -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
} }

View File

@ -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")
}
} }

View File

@ -1 +0,0 @@
package main

View File

@ -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