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"
)
// 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) {

View File

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

View File

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

View File

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

View File

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

View File

@ -1 +0,0 @@
package main

View File

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