moth/pkg/transpile/category.go

202 lines
5.2 KiB
Go
Raw Normal View History

2020-09-08 17:49:02 -06:00
package transpile
2019-08-17 16:00:15 -06:00
import (
2020-09-04 15:29:06 -06:00
"bytes"
"context"
"encoding/json"
2020-09-16 17:54:29 -06:00
"fmt"
2019-08-18 21:59:06 -06:00
"log"
2020-09-04 15:29:06 -06:00
"os/exec"
2020-09-14 18:23:56 -06:00
"path"
2019-08-18 21:59:06 -06:00
"strconv"
2020-11-02 14:40:43 -07:00
"strings"
2020-09-04 15:29:06 -06:00
"time"
2019-08-18 21:59:06 -06:00
2020-08-28 17:41:17 -06:00
"github.com/spf13/afero"
)
2019-08-18 21:59:06 -06:00
2020-10-16 14:18:44 -06:00
// InventoryResponse is what's handed back when we ask for an inventory.
type InventoryResponse struct {
Puzzles []int
}
2020-09-08 17:49:02 -06:00
// Category defines the functionality required to be a puzzle category.
type Category interface {
// Inventory lists every puzzle in the category.
Inventory() ([]int, error)
// Puzzle provides a Puzzle structure for the given point value.
Puzzle(points int) (Puzzle, error)
// Open returns an io.ReadCloser for the given filename.
Open(points int, filename string) (ReadSeekCloser, error)
// Answer returns whether the given answer is correct.
Answer(points int, answer string) bool
}
2020-09-04 13:00:23 -06:00
// NopReadCloser provides an io.ReadCloser which does nothing.
2020-09-03 20:04:43 -06:00
type NopReadCloser struct {
}
2020-09-04 13:00:23 -06:00
// Read satisfies io.Reader.
2020-09-03 20:04:43 -06:00
func (n NopReadCloser) Read(b []byte) (int, error) {
return 0, nil
}
2020-09-04 13:00:23 -06:00
// Close satisfies io.Closer.
2020-09-03 20:04:43 -06:00
func (n NopReadCloser) Close() error {
return nil
}
// NewFsCategory returns a Category based on which files are present.
// If 'mkcategory' is present and executable, an FsCommandCategory is returned.
// Otherwise, FsCategory is returned.
2020-09-04 15:29:06 -06:00
func NewFsCategory(fs afero.Fs, cat string) Category {
bfs := NewRecursiveBasePathFs(fs, cat)
if info, err := bfs.Stat("mkcategory"); (err == nil) && (info.Mode()&0100 != 0) {
if command, err := bfs.RealPath(info.Name()); err != nil {
log.Println("Unable to resolve full path to", info.Name(), bfs)
} else {
return FsCommandCategory{
fs: bfs,
command: command,
timeout: 2 * time.Second,
}
}
2019-08-18 21:59:06 -06:00
}
2020-09-04 15:29:06 -06:00
return FsCategory{fs: bfs}
2019-08-18 21:59:06 -06:00
}
2020-09-04 13:00:23 -06:00
// FsCategory provides a category backed by a .md file.
2020-09-03 20:04:43 -06:00
type FsCategory struct {
fs afero.Fs
2020-08-28 17:41:17 -06:00
}
2019-08-18 21:59:06 -06:00
2020-09-04 13:00:23 -06:00
// Inventory returns a list of point values for this category.
2020-09-03 20:04:43 -06:00
func (c FsCategory) Inventory() ([]int, error) {
puzzleEntries, err := afero.ReadDir(c.fs, ".")
2019-08-18 21:59:06 -06:00
if err != nil {
return nil, err
}
2020-08-28 17:41:17 -06:00
puzzles := make([]int, 0, len(puzzleEntries))
for _, ent := range puzzleEntries {
if !ent.IsDir() {
continue
}
2020-08-28 17:41:17 -06:00
if points, err := strconv.Atoi(ent.Name()); err != nil {
log.Println("Skipping non-numeric directory", ent.Name())
continue
} else {
puzzles = append(puzzles, points)
}
}
2020-08-28 17:41:17 -06:00
return puzzles, nil
}
2020-09-04 13:00:23 -06:00
// Puzzle returns a Puzzle structure for the given point value.
2020-09-03 20:04:43 -06:00
func (c FsCategory) Puzzle(points int) (Puzzle, error) {
2020-09-11 13:03:19 -06:00
return NewFsPuzzlePoints(c.fs, points).Puzzle()
2020-09-03 20:04:43 -06:00
}
2020-09-04 13:00:23 -06:00
// Open returns an io.ReadCloser for the given filename.
2020-09-08 17:49:02 -06:00
func (c FsCategory) Open(points int, filename string) (ReadSeekCloser, error) {
2020-09-11 13:03:19 -06:00
return NewFsPuzzlePoints(c.fs, points).Open(filename)
2020-09-03 20:04:43 -06:00
}
2020-09-04 15:29:06 -06:00
// Answer checks whether an answer is correct.
2020-09-03 20:04:43 -06:00
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)
if err != nil {
return false
}
for _, a := range p.Answers {
if a == answer {
return true
}
}
return false
}
2020-09-04 13:00:23 -06:00
// FsCommandCategory provides a category backed by running an external command.
2020-09-03 20:04:43 -06:00
type FsCommandCategory struct {
2020-09-04 15:29:06 -06:00
fs afero.Fs
command string
timeout time.Duration
2020-09-03 20:04:43 -06:00
}
2020-09-14 18:23:56 -06:00
func (c FsCommandCategory) run(command string, args ...string) ([]byte, error) {
2020-09-04 15:29:06 -06:00
ctx, cancel := context.WithTimeout(context.Background(), c.timeout)
defer cancel()
2020-09-14 18:23:56 -06:00
cmdargs := append([]string{command}, args...)
cmd := exec.CommandContext(ctx, "./"+path.Base(c.command), cmdargs...)
cmd.Dir = path.Dir(c.command)
2020-11-02 14:40:43 -07:00
out, err := cmd.Output()
if err, ok := err.(*exec.ExitError); ok {
stderr := strings.TrimSpace(string(err.Stderr))
return nil, fmt.Errorf("%s (%s)", stderr, err.String())
}
return out, err
2020-09-14 18:23:56 -06:00
}
// Inventory returns a list of point values for this category.
func (c FsCommandCategory) Inventory() ([]int, error) {
stdout, err := c.run("inventory")
2020-09-16 17:54:29 -06:00
if exerr, ok := err.(*exec.ExitError); ok {
return nil, fmt.Errorf("inventory: %s: %s", err, string(exerr.Stderr))
} else if err != nil {
2020-09-04 15:29:06 -06:00
return nil, err
}
2020-10-16 14:18:44 -06:00
inv := InventoryResponse{}
if err := json.Unmarshal(stdout, &inv); err != nil {
2020-09-04 15:29:06 -06:00
return nil, err
}
2020-10-16 14:18:44 -06:00
return inv.Puzzles, nil
2020-09-03 20:04:43 -06:00
}
2020-09-04 13:00:23 -06:00
// Puzzle returns a Puzzle structure for the given point value.
2020-09-03 20:04:43 -06:00
func (c FsCommandCategory) Puzzle(points int) (Puzzle, error) {
2020-09-04 15:29:06 -06:00
var p Puzzle
2020-09-14 18:23:56 -06:00
stdout, err := c.run("puzzle", strconv.Itoa(points))
2020-09-04 15:29:06 -06:00
if err != nil {
return p, err
}
if err := json.Unmarshal(stdout, &p); err != nil {
return p, err
}
2020-09-04 18:28:23 -06:00
p.computeAnswerHashes()
2020-09-04 15:29:06 -06:00
return p, nil
2020-09-03 20:04:43 -06:00
}
2020-09-04 13:00:23 -06:00
// Open returns an io.ReadCloser for the given filename.
2020-09-08 17:49:02 -06:00
func (c FsCommandCategory) Open(points int, filename string) (ReadSeekCloser, error) {
2020-09-14 18:23:56 -06:00
stdout, err := c.run("file", strconv.Itoa(points), filename)
2020-09-08 17:49:02 -06:00
return nopCloser{bytes.NewReader(stdout)}, err
2020-09-03 20:04:43 -06:00
}
2020-09-04 15:29:06 -06:00
// Answer checks whether an answer is correct.
2020-09-03 20:04:43 -06:00
func (c FsCommandCategory) Answer(points int, answer string) bool {
2020-09-14 18:23:56 -06:00
stdout, err := c.run("answer", strconv.Itoa(points), answer)
2020-09-04 15:29:06 -06:00
if err != nil {
log.Printf("ERROR: Answering %d points: %s", points, err)
return false
}
2020-10-16 14:18:44 -06:00
ans := AnswerResponse{}
if err := json.Unmarshal(stdout, &ans); err != nil {
log.Printf("ERROR: Answering %d points: %s", points, err)
return false
2020-09-04 15:29:06 -06:00
}
2020-10-16 14:18:44 -06:00
return ans.Correct
2019-08-18 21:59:06 -06:00
}