mirror of https://github.com/dirtbags/moth.git
Server can now be a devel server
This commit is contained in:
parent
6696d27ee0
commit
f1f6140eea
|
@ -94,7 +94,7 @@ func TestHttpd(t *testing.T) {
|
||||||
|
|
||||||
if r := hs.TestRequest("/answer", map[string]string{"cat": "pategory", "points": "1", "answer": "moo"}); r.Result().StatusCode != 200 {
|
if r := hs.TestRequest("/answer", map[string]string{"cat": "pategory", "points": "1", "answer": "moo"}); r.Result().StatusCode != 200 {
|
||||||
t.Error(r.Result())
|
t.Error(r.Result())
|
||||||
} else if r.Body.String() != `{"status":"fail","data":{"short":"not accepted","description":"Invalid answer"}}` {
|
} else if r.Body.String() != `{"status":"fail","data":{"short":"not accepted","description":"Incorrect answer"}}` {
|
||||||
t.Error("Unexpected body", r.Body.String())
|
t.Error("Unexpected body", r.Body.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,7 +119,7 @@ func TestHttpd(t *testing.T) {
|
||||||
|
|
||||||
if r := hs.TestRequest("/answer", map[string]string{"cat": "pategory", "points": "1", "answer": "answer123"}); r.Result().StatusCode != 200 {
|
if r := hs.TestRequest("/answer", map[string]string{"cat": "pategory", "points": "1", "answer": "answer123"}); r.Result().StatusCode != 200 {
|
||||||
t.Error(r.Result())
|
t.Error(r.Result())
|
||||||
} else if r.Body.String() != `{"status":"fail","data":{"short":"not accepted","description":"Points already awarded to this team in this category"}}` {
|
} else if r.Body.String() != `{"status":"fail","data":{"short":"not accepted","description":"Error awarding points: Points already awarded to this team in this category"}}` {
|
||||||
t.Error("Unexpected body", r.Body.String())
|
t.Error("Unexpected body", r.Body.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,11 @@ func main() {
|
||||||
"mothballs",
|
"mothballs",
|
||||||
"Path to mothball files",
|
"Path to mothball files",
|
||||||
)
|
)
|
||||||
|
puzzlePath := flag.String(
|
||||||
|
"puzzles",
|
||||||
|
"",
|
||||||
|
"Path to puzzles tree; if specified, enables development mode",
|
||||||
|
)
|
||||||
refreshInterval := flag.Duration(
|
refreshInterval := flag.Duration(
|
||||||
"refresh",
|
"refresh",
|
||||||
2*time.Second,
|
2*time.Second,
|
||||||
|
@ -41,9 +46,18 @@ func main() {
|
||||||
)
|
)
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
theme := NewTheme(afero.NewBasePathFs(afero.NewOsFs(), *themePath))
|
osfs := afero.NewOsFs()
|
||||||
state := NewState(afero.NewBasePathFs(afero.NewOsFs(), *statePath))
|
theme := NewTheme(afero.NewBasePathFs(osfs, *themePath))
|
||||||
puzzles := NewMothballs(afero.NewBasePathFs(afero.NewOsFs(), *mothballPath))
|
state := NewState(afero.NewBasePathFs(osfs, *statePath))
|
||||||
|
|
||||||
|
config := Configuration{}
|
||||||
|
|
||||||
|
var provider PuzzleProvider
|
||||||
|
provider = NewMothballs(afero.NewBasePathFs(osfs, *mothballPath))
|
||||||
|
if *puzzlePath != "" {
|
||||||
|
provider = NewTranspilerProvider(afero.NewBasePathFs(osfs, *puzzlePath))
|
||||||
|
config.Devel = true
|
||||||
|
}
|
||||||
|
|
||||||
// Add some MIME extensions
|
// Add some MIME extensions
|
||||||
// Doing this avoids decompressing a mothball entry twice per request
|
// Doing this avoids decompressing a mothball entry twice per request
|
||||||
|
@ -52,9 +66,9 @@ func main() {
|
||||||
|
|
||||||
go theme.Maintain(*refreshInterval)
|
go theme.Maintain(*refreshInterval)
|
||||||
go state.Maintain(*refreshInterval)
|
go state.Maintain(*refreshInterval)
|
||||||
go puzzles.Maintain(*refreshInterval)
|
go provider.Maintain(*refreshInterval)
|
||||||
|
|
||||||
server := NewMothServer(puzzles, theme, state)
|
server := NewMothServer(config, theme, state, provider)
|
||||||
httpd := NewHTTPServer(*base, server)
|
httpd := NewHTTPServer(*base, server)
|
||||||
|
|
||||||
httpd.Run(*bindStr)
|
httpd.Run(*bindStr)
|
||||||
|
|
|
@ -88,15 +88,15 @@ func (m *Mothballs) Inventory() []Category {
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckAnswer returns an error if the provided answer is in any way incorrect for the given category and points
|
// CheckAnswer returns an error if the provided answer is in any way incorrect for the given category and points
|
||||||
func (m *Mothballs) CheckAnswer(cat string, points int, answer string) error {
|
func (m *Mothballs) CheckAnswer(cat string, points int, answer string) (bool, error) {
|
||||||
zfs, ok := m.getCat(cat)
|
zfs, ok := m.getCat(cat)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("No such category: %s", cat)
|
return false, fmt.Errorf("No such category: %s", cat)
|
||||||
}
|
}
|
||||||
|
|
||||||
af, err := zfs.Open("answers.txt")
|
af, err := zfs.Open("answers.txt")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("No answers.txt file")
|
return false, fmt.Errorf("No answers.txt file")
|
||||||
}
|
}
|
||||||
defer af.Close()
|
defer af.Close()
|
||||||
|
|
||||||
|
@ -104,11 +104,11 @@ func (m *Mothballs) CheckAnswer(cat string, points int, answer string) error {
|
||||||
scanner := bufio.NewScanner(af)
|
scanner := bufio.NewScanner(af)
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
if scanner.Text() == needle {
|
if scanner.Text() == needle {
|
||||||
return nil
|
return true, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("Invalid answer")
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// refresh refreshes internal state.
|
// refresh refreshes internal state.
|
||||||
|
|
|
@ -81,16 +81,16 @@ func TestMothballs(t *testing.T) {
|
||||||
t.Error("This file shouldn't exist")
|
t.Error("This file shouldn't exist")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.CheckAnswer("pategory", 1, "answer"); err == nil {
|
if ok, _ := m.CheckAnswer("pategory", 1, "answer"); ok {
|
||||||
t.Error("Wrong answer marked right")
|
t.Error("Wrong answer marked right")
|
||||||
}
|
}
|
||||||
if err := m.CheckAnswer("pategory", 1, "answer123"); err != nil {
|
if _, err := m.CheckAnswer("pategory", 1, "answer123"); err != nil {
|
||||||
t.Error("Right answer marked wrong", err)
|
t.Error("Right answer marked wrong", err)
|
||||||
}
|
}
|
||||||
if err := m.CheckAnswer("pategory", 1, "answer456"); err != nil {
|
if _, err := m.CheckAnswer("pategory", 1, "answer456"); err != nil {
|
||||||
t.Error("Right answer marked wrong", err)
|
t.Error("Right answer marked wrong", err)
|
||||||
}
|
}
|
||||||
if err := m.CheckAnswer("nealegory", 1, "moo"); err == nil {
|
if ok, err := m.CheckAnswer("nealegory", 1, "moo"); ok {
|
||||||
t.Error("Checking answer in non-existent category should fail")
|
t.Error("Checking answer in non-existent category should fail")
|
||||||
} else if err.Error() != "No such category: nealegory" {
|
} else if err.Error() != "No such category: nealegory" {
|
||||||
t.Error("Wrong error message")
|
t.Error("Wrong error message")
|
||||||
|
|
|
@ -4,7 +4,6 @@ package main
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
@ -15,14 +14,14 @@ import (
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PuzzleCommand specifies a command to run for the puzzle API
|
// ProviderCommand specifies a command to run for the puzzle API
|
||||||
type PuzzleCommand struct {
|
type ProviderCommand struct {
|
||||||
Path string
|
Path string
|
||||||
Args []string
|
Args []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inventory runs with "action=inventory", and parses the output into a category list.
|
// Inventory runs with "action=inventory", and parses the output into a category list.
|
||||||
func (pc PuzzleCommand) Inventory() (inv []Category) {
|
func (pc ProviderCommand) Inventory() (inv []Category) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
|
@ -62,16 +61,18 @@ func (pc PuzzleCommand) Inventory() (inv []Category) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NullReadSeekCloser wraps a no-op Close method around an io.ReadSeeker.
|
||||||
type NullReadSeekCloser struct {
|
type NullReadSeekCloser struct {
|
||||||
io.ReadSeeker
|
io.ReadSeeker
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close does nothing.
|
||||||
func (f NullReadSeekCloser) Close() error {
|
func (f NullReadSeekCloser) Close() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open passes its arguments to the command with "action=open".
|
// Open passes its arguments to the command with "action=open".
|
||||||
func (pc PuzzleCommand) Open(cat string, points int, path string) (ReadSeekCloser, time.Time, error) {
|
func (pc ProviderCommand) Open(cat string, points int, path string) (ReadSeekCloser, time.Time, error) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
|
@ -91,7 +92,7 @@ func (pc PuzzleCommand) Open(cat string, points int, path string) (ReadSeekClose
|
||||||
// CheckAnswer passes its arguments to the command with "action=answer".
|
// CheckAnswer passes its arguments to the command with "action=answer".
|
||||||
// If the command exits successfully and sends "correct" to stdout,
|
// If the command exits successfully and sends "correct" to stdout,
|
||||||
// nil is returned.
|
// nil is returned.
|
||||||
func (pc PuzzleCommand) CheckAnswer(cat string, points int, answer string) error {
|
func (pc ProviderCommand) CheckAnswer(cat string, points int, answer string) (bool, error) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
|
@ -105,9 +106,9 @@ func (pc PuzzleCommand) CheckAnswer(cat string, points int, answer string) error
|
||||||
stdout, err := cmd.Output()
|
stdout, err := cmd.Output()
|
||||||
if ee, ok := err.(*exec.ExitError); ok {
|
if ee, ok := err.(*exec.ExitError); ok {
|
||||||
log.Printf("%s: %s", pc.Path, string(ee.Stderr))
|
log.Printf("%s: %s", pc.Path, string(ee.Stderr))
|
||||||
return err
|
return false, err
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return err
|
return false, err
|
||||||
}
|
}
|
||||||
result := strings.TrimSpace(string(stdout))
|
result := strings.TrimSpace(string(stdout))
|
||||||
|
|
||||||
|
@ -115,12 +116,12 @@ func (pc PuzzleCommand) CheckAnswer(cat string, points int, answer string) error
|
||||||
if result == "" {
|
if result == "" {
|
||||||
result = "Nothing written to stdout"
|
result = "Nothing written to stdout"
|
||||||
}
|
}
|
||||||
return fmt.Errorf("Wrong answer: %s", result)
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Maintain does nothing: a command puzzle provider has no housekeeping
|
// Maintain does nothing: a command puzzle ProviderCommand has no housekeeping
|
||||||
func (pc PuzzleCommand) Maintain(updateInterval time.Duration) {
|
func (pc ProviderCommand) Maintain(updateInterval time.Duration) {
|
||||||
}
|
}
|
|
@ -6,8 +6,8 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPuzzleCommand(t *testing.T) {
|
func TestProviderCommand(t *testing.T) {
|
||||||
pc := PuzzleCommand{
|
pc := ProviderCommand{
|
||||||
Path: "testdata/testpiler.sh",
|
Path: "testdata/testpiler.sh",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,14 +34,14 @@ func TestPuzzleCommand(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := pc.CheckAnswer("pategory", 1, "answer"); err != nil {
|
if ok, err := pc.CheckAnswer("pategory", 1, "answer"); !ok {
|
||||||
t.Errorf("Correct answer for pategory: %v", err)
|
t.Errorf("Correct answer for pategory: %v", err)
|
||||||
}
|
}
|
||||||
if err := pc.CheckAnswer("pategory", 1, "wrong"); err == nil {
|
if ok, _ := pc.CheckAnswer("pategory", 1, "wrong"); ok {
|
||||||
t.Errorf("Wrong answer for pategory judged correct")
|
t.Errorf("Wrong answer for pategory judged correct")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := pc.CheckAnswer("pategory", 2, "answer"); err == nil {
|
if _, err := pc.CheckAnswer("pategory", 2, "answer"); err == nil {
|
||||||
t.Errorf("Internal error not returned")
|
t.Errorf("Internal error not returned")
|
||||||
} else if ee, ok := err.(*exec.ExitError); ok {
|
} else if ee, ok := err.(*exec.ExitError); ok {
|
||||||
if string(ee.Stderr) != "Internal error\n" {
|
if string(ee.Stderr) != "Internal error\n" {
|
|
@ -22,11 +22,14 @@ type ReadSeekCloser interface {
|
||||||
io.Closer
|
io.Closer
|
||||||
}
|
}
|
||||||
|
|
||||||
// StateExport is given to clients requesting the current state.
|
// Configuration stores information about server configuration.
|
||||||
type StateExport struct {
|
type Configuration struct {
|
||||||
Config struct {
|
|
||||||
Devel bool
|
Devel bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StateExport is given to clients requesting the current state.
|
||||||
|
type StateExport struct {
|
||||||
|
Config Configuration
|
||||||
Messages string
|
Messages string
|
||||||
TeamNames map[string]string
|
TeamNames map[string]string
|
||||||
PointsLog award.List
|
PointsLog award.List
|
||||||
|
@ -37,7 +40,7 @@ type StateExport struct {
|
||||||
type PuzzleProvider interface {
|
type PuzzleProvider interface {
|
||||||
Open(cat string, points int, path string) (ReadSeekCloser, time.Time, error)
|
Open(cat string, points int, path string) (ReadSeekCloser, time.Time, error)
|
||||||
Inventory() []Category
|
Inventory() []Category
|
||||||
CheckAnswer(cat string, points int, answer string) error
|
CheckAnswer(cat string, points int, answer string) (bool, error)
|
||||||
Maintainer
|
Maintainer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,15 +71,17 @@ type Maintainer interface {
|
||||||
|
|
||||||
// MothServer gathers together the providers that make up a MOTH server.
|
// MothServer gathers together the providers that make up a MOTH server.
|
||||||
type MothServer struct {
|
type MothServer struct {
|
||||||
Puzzles PuzzleProvider
|
PuzzleProviders []PuzzleProvider
|
||||||
Theme ThemeProvider
|
Theme ThemeProvider
|
||||||
State StateProvider
|
State StateProvider
|
||||||
|
Config Configuration
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMothServer returns a new MothServer.
|
// NewMothServer returns a new MothServer.
|
||||||
func NewMothServer(puzzles PuzzleProvider, theme ThemeProvider, state StateProvider) *MothServer {
|
func NewMothServer(config Configuration, theme ThemeProvider, state StateProvider, puzzleProviders ...PuzzleProvider) *MothServer {
|
||||||
return &MothServer{
|
return &MothServer{
|
||||||
Puzzles: puzzles,
|
Config: config,
|
||||||
|
PuzzleProviders: puzzleProviders,
|
||||||
Theme: theme,
|
Theme: theme,
|
||||||
State: state,
|
State: state,
|
||||||
}
|
}
|
||||||
|
@ -99,15 +104,52 @@ type MothRequestHandler struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// PuzzlesOpen opens a file associated with a puzzle.
|
// PuzzlesOpen opens a file associated with a puzzle.
|
||||||
func (mh *MothRequestHandler) PuzzlesOpen(cat string, points int, path string) (ReadSeekCloser, time.Time, error) {
|
// BUG(neale): Multiple providers with the same category name are not detected or handled well.
|
||||||
|
func (mh *MothRequestHandler) PuzzlesOpen(cat string, points int, path string) (r ReadSeekCloser, ts time.Time, err error) {
|
||||||
export := mh.ExportState()
|
export := mh.ExportState()
|
||||||
|
found := false
|
||||||
for _, p := range export.Puzzles[cat] {
|
for _, p := range export.Puzzles[cat] {
|
||||||
if p == points {
|
if p == points {
|
||||||
return mh.Puzzles.Open(cat, points, path)
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return nil, time.Time{}, fmt.Errorf("Category not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try every provider until someone doesn't return an error
|
||||||
|
for _, provider := range mh.PuzzleProviders {
|
||||||
|
r, ts, err = provider.Open(cat, points, path)
|
||||||
|
if err != nil {
|
||||||
|
return r, ts, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, time.Time{}, fmt.Errorf("Puzzle locked")
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckAnswer returns an error if answer is not a correct answer for puzzle points in category cat
|
||||||
|
func (mh *MothRequestHandler) CheckAnswer(cat string, points int, answer string) error {
|
||||||
|
correct := false
|
||||||
|
for _, provider := range mh.PuzzleProviders {
|
||||||
|
if ok, err := provider.CheckAnswer(cat, points, answer); err != nil {
|
||||||
|
return err
|
||||||
|
} else if ok {
|
||||||
|
correct = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !correct {
|
||||||
|
return fmt.Errorf("Incorrect answer")
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := fmt.Sprintf("GOOD %s %s %s %d", mh.participantID, mh.teamID, cat, points)
|
||||||
|
mh.State.LogEvent(msg)
|
||||||
|
|
||||||
|
if err := mh.State.AwardPoints(mh.teamID, cat, points); err != nil {
|
||||||
|
return fmt.Errorf("Error awarding points: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ThemeOpen opens a file from a theme.
|
// ThemeOpen opens a file from a theme.
|
||||||
|
@ -124,29 +166,13 @@ func (mh *MothRequestHandler) Register(teamName string) error {
|
||||||
return mh.State.SetTeamName(mh.teamID, teamName)
|
return mh.State.SetTeamName(mh.teamID, teamName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckAnswer returns an error if answer is not a correct answer for puzzle points in category cat
|
|
||||||
func (mh *MothRequestHandler) CheckAnswer(cat string, points int, answer string) error {
|
|
||||||
if err := mh.Puzzles.CheckAnswer(cat, points, answer); err != nil {
|
|
||||||
msg := fmt.Sprintf("BAD %s %s %s %d %s", mh.participantID, mh.teamID, cat, points, err.Error())
|
|
||||||
mh.State.LogEvent(msg)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := mh.State.AwardPoints(mh.teamID, cat, points); err != nil {
|
|
||||||
msg := fmt.Sprintf("GOOD %s %s %s %d", mh.participantID, mh.teamID, cat, points)
|
|
||||||
mh.State.LogEvent(msg)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExportState anonymizes team IDs and returns StateExport.
|
// ExportState anonymizes team IDs and returns StateExport.
|
||||||
// If a teamID has been specified for this MothRequestHandler,
|
// If a teamID has been specified for this MothRequestHandler,
|
||||||
// the anonymized team name for this teamID has the special value "self".
|
// the anonymized team name for this teamID has the special value "self".
|
||||||
// If not, the puzzles list is empty.
|
// If not, the puzzles list is empty.
|
||||||
func (mh *MothRequestHandler) ExportState() *StateExport {
|
func (mh *MothRequestHandler) ExportState() *StateExport {
|
||||||
export := StateExport{}
|
export := StateExport{}
|
||||||
|
export.Config = mh.Config
|
||||||
|
|
||||||
teamName, _ := mh.State.TeamName(mh.teamID)
|
teamName, _ := mh.State.TeamName(mh.teamID)
|
||||||
|
|
||||||
|
@ -182,7 +208,8 @@ func (mh *MothRequestHandler) ExportState() *StateExport {
|
||||||
// but then we got a bad reputation on some secretive blacklist,
|
// but then we got a bad reputation on some secretive blacklist,
|
||||||
// and now the Navy can't register for events.
|
// and now the Navy can't register for events.
|
||||||
|
|
||||||
for _, category := range mh.Puzzles.Inventory() {
|
for _, provider := range mh.PuzzleProviders {
|
||||||
|
for _, category := range provider.Inventory() {
|
||||||
// Append sentry (end of puzzles)
|
// Append sentry (end of puzzles)
|
||||||
allPuzzles := append(category.Puzzles, 0)
|
allPuzzles := append(category.Puzzles, 0)
|
||||||
|
|
||||||
|
@ -191,13 +218,14 @@ func (mh *MothRequestHandler) ExportState() *StateExport {
|
||||||
puzzles := make([]int, 0, len(allPuzzles))
|
puzzles := make([]int, 0, len(allPuzzles))
|
||||||
for i, val := range allPuzzles {
|
for i, val := range allPuzzles {
|
||||||
puzzles = allPuzzles[:i+1]
|
puzzles = allPuzzles[:i+1]
|
||||||
if val > max {
|
if !mh.Config.Devel && (val > max) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export.Puzzles[category.Name] = puzzles
|
export.Puzzles[category.Name] = puzzles
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return &export
|
return &export
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ func NewTestServer() *MothServer {
|
||||||
afero.WriteFile(theme.Fs, "/index.html", []byte("index.html"), 0644)
|
afero.WriteFile(theme.Fs, "/index.html", []byte("index.html"), 0644)
|
||||||
go theme.Maintain(TestMaintenanceInterval)
|
go theme.Maintain(TestMaintenanceInterval)
|
||||||
|
|
||||||
return NewMothServer(puzzles, theme, state)
|
return NewMothServer(Configuration{Devel: true}, theme, state, puzzles)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestServer(t *testing.T) {
|
func TestServer(t *testing.T) {
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
enabled: remove or rename to suspend the contest.
|
|
@ -0,0 +1,11 @@
|
||||||
|
# hours: when the contest is enabled
|
||||||
|
#
|
||||||
|
# Enable: + timestamp
|
||||||
|
# Disable: - timestamp
|
||||||
|
#
|
||||||
|
# You can have multiple start/stop times.
|
||||||
|
# Whatever time is the most recent, wins.
|
||||||
|
# Times in the future are ignored.
|
||||||
|
|
||||||
|
+ 2020-09-08T23:43:53Z
|
||||||
|
- 3019-10-31T00:00:00Z
|
|
@ -0,0 +1,3 @@
|
||||||
|
initialized: remove to re-initialize the contest.
|
||||||
|
|
||||||
|
This instance was initaliazed at 2020-09-08T23:43:53Z
|
|
@ -0,0 +1 @@
|
||||||
|
<!-- messages.html: put client broadcast messages here. -->
|
|
@ -0,0 +1,100 @@
|
||||||
|
ywf3=a32
|
||||||
|
c4hp2dxx
|
||||||
|
brb4r4t2
|
||||||
|
ybyen862
|
||||||
|
8mfqd834
|
||||||
|
yk788pqx
|
||||||
|
ymf6idk3
|
||||||
|
4qiwfar7
|
||||||
|
h2m7=nf7
|
||||||
|
nzbz=pzx
|
||||||
|
ddmd=m8p
|
||||||
|
87ahkr28
|
||||||
|
7rbq3=pd
|
||||||
|
y2xix7ep
|
||||||
|
37h86=64
|
||||||
|
7ey32edn
|
||||||
|
=f4rrhrr
|
||||||
|
4k2i2rzz
|
||||||
|
cie2p7ed
|
||||||
|
zcydpq44
|
||||||
|
riqxziqa
|
||||||
|
ptqqwh2k
|
||||||
|
=8=3q3kd
|
||||||
|
c8na=qix
|
||||||
|
reqhwkca
|
||||||
|
fkm3tkm7
|
||||||
|
6etm6kh7
|
||||||
|
pwd2p=fi
|
||||||
|
ryz7t4xe
|
||||||
|
qy3azp2k
|
||||||
|
h3rweqd8
|
||||||
|
d=2f3r=q
|
||||||
|
zha7t6rp
|
||||||
|
=nh6ncz4
|
||||||
|
kbabkwaq
|
||||||
|
7z4dmdbz
|
||||||
|
it8iddbb
|
||||||
|
dtwptqn8
|
||||||
|
anwhemzw
|
||||||
|
etptmc8w
|
||||||
|
c=pa3hz2
|
||||||
|
pe4r=ede
|
||||||
|
=cw23dhe
|
||||||
|
yw3xaw3n
|
||||||
|
=a7yz2f3
|
||||||
|
q6dqamia
|
||||||
|
4x8e6c8c
|
||||||
|
3tt88hkx
|
||||||
|
6crqe=kn
|
||||||
|
dhnprc4r
|
||||||
|
kdczyz7q
|
||||||
|
y=z8pkpk
|
||||||
|
6h3i6p=i
|
||||||
|
mipx4dmh
|
||||||
|
b6rdhb2z
|
||||||
|
kpqt8th2
|
||||||
|
mqwa=b3f
|
||||||
|
hzzr7dwa
|
||||||
|
x8833aa4
|
||||||
|
in327p7t
|
||||||
|
it=dnyh=
|
||||||
|
kr2pftrh
|
||||||
|
zahqwz32
|
||||||
|
66wkyc8q
|
||||||
|
8amz4ehy
|
||||||
|
ct37zri6
|
||||||
|
rd2zpp67
|
||||||
|
6hczfmpt
|
||||||
|
4dckadbz
|
||||||
|
7wx3r4hf
|
||||||
|
8p6aynxm
|
||||||
|
=xwanh=4
|
||||||
|
fw4y2qdf
|
||||||
|
6qz7k8ee
|
||||||
|
7z7neebn
|
||||||
|
a3mi3m3a
|
||||||
|
bhftc6dt
|
||||||
|
hhm3b4qd
|
||||||
|
hddy=t2c
|
||||||
|
cqi32enq
|
||||||
|
xnmknai4
|
||||||
|
=a24=2ci
|
||||||
|
fnfc322r
|
||||||
|
fzb62kmk
|
||||||
|
w8kenc7y
|
||||||
|
q8=y=pf4
|
||||||
|
=ry46dd8
|
||||||
|
tz=bp4kw
|
||||||
|
amwwfaqy
|
||||||
|
2fan2phi
|
||||||
|
73=387fb
|
||||||
|
ye==8i2w
|
||||||
|
r2zznx=e
|
||||||
|
nn83t4ni
|
||||||
|
dzxpi4ae
|
||||||
|
ef6reizk
|
||||||
|
4q8e8kbq
|
||||||
|
wwyq2=x7
|
||||||
|
y7db2nty
|
||||||
|
6222=fpb
|
|
@ -0,0 +1,3 @@
|
||||||
|
author: neale
|
||||||
|
|
||||||
|
Hello, world.
|
|
@ -0,0 +1,75 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/dirtbags/moth/pkg/transpile"
|
||||||
|
"github.com/spf13/afero"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewTranspilerProvider returns a new TranspilerProvider.
|
||||||
|
func NewTranspilerProvider(fs afero.Fs) TranspilerProvider {
|
||||||
|
return TranspilerProvider{fs}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TranspilerProvider provides puzzles generated from source files on disk
|
||||||
|
type TranspilerProvider struct {
|
||||||
|
fs afero.Fs
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inventory returns a Category list for this provider.
|
||||||
|
func (p TranspilerProvider) Inventory() []Category {
|
||||||
|
ret := make([]Category, 0)
|
||||||
|
inv, err := transpile.FsInventory(p.fs)
|
||||||
|
if err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
for name, points := range inv {
|
||||||
|
ret = append(ret, Category{name, points})
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
type nopCloser struct {
|
||||||
|
io.ReadSeeker
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c nopCloser) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open returns a file associated with the given category and point value.
|
||||||
|
func (p TranspilerProvider) Open(cat string, points int, filename string) (ReadSeekCloser, time.Time, error) {
|
||||||
|
c := transpile.NewFsCategory(p.fs, cat)
|
||||||
|
switch filename {
|
||||||
|
case "", "puzzle.json":
|
||||||
|
p, err := c.Puzzle(points)
|
||||||
|
if err != nil {
|
||||||
|
return nopCloser{new(bytes.Reader)}, time.Time{}, err
|
||||||
|
}
|
||||||
|
jp, err := json.Marshal(p)
|
||||||
|
if err != nil {
|
||||||
|
return nopCloser{new(bytes.Reader)}, time.Time{}, err
|
||||||
|
}
|
||||||
|
return nopCloser{bytes.NewReader(jp)}, time.Now(), nil
|
||||||
|
default:
|
||||||
|
r, err := c.Open(points, filename)
|
||||||
|
return r, time.Now(), err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckAnswer checks whether an answer si correct.
|
||||||
|
func (p TranspilerProvider) CheckAnswer(cat string, points int, answer string) (bool, error) {
|
||||||
|
c := transpile.NewFsCategory(p.fs, cat)
|
||||||
|
return c.Answer(points, answer), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Maintain performs housekeeping.
|
||||||
|
func (p TranspilerProvider) Maintain(updateInterval time.Duration) {
|
||||||
|
// Nothing to do here.
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/spf13/afero"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTranspiler(t *testing.T) {
|
||||||
|
fs := afero.NewBasePathFs(afero.NewOsFs(), "testdata")
|
||||||
|
p := NewTranspilerProvider(fs)
|
||||||
|
|
||||||
|
inv := p.Inventory()
|
||||||
|
if len(inv) != 1 {
|
||||||
|
t.Error("Wrong inventory:", inv)
|
||||||
|
} else if len(inv[0].Puzzles) != 1 {
|
||||||
|
t.Error("Wrong inventory:", inv)
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,25 +9,11 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"github.com/GoBike/envflag"
|
"github.com/dirtbags/moth/pkg/transpile"
|
||||||
|
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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) (io.ReadCloser, error)
|
|
||||||
|
|
||||||
// Answer returns whether the given answer is correct.
|
|
||||||
Answer(points int, answer string) bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// T contains everything required for a transpilation invocation (across the nation).
|
// T contains everything required for a transpilation invocation (across the nation).
|
||||||
type T struct {
|
type T struct {
|
||||||
// What action to take
|
// What action to take
|
||||||
|
@ -39,7 +25,8 @@ type T struct {
|
||||||
Fs afero.Fs
|
Fs afero.Fs
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseArgs parses command-line arguments into T, returning the action to take
|
// ParseArgs parses command-line arguments into T, returning the action to take.
|
||||||
|
// BUG(neale): CLI arguments are not related to how the CLI will be used.
|
||||||
func (t *T) ParseArgs() string {
|
func (t *T) ParseArgs() string {
|
||||||
action := flag.String("action", "inventory", "Action to take: must be 'inventory', 'open', 'answer', or 'mothball'")
|
action := flag.String("action", "inventory", "Action to take: must be 'inventory', 'open', 'answer', or 'mothball'")
|
||||||
flag.StringVar(&t.Cat, "cat", "", "Puzzle category")
|
flag.StringVar(&t.Cat, "cat", "", "Puzzle category")
|
||||||
|
@ -47,7 +34,7 @@ func (t *T) ParseArgs() string {
|
||||||
flag.StringVar(&t.Answer, "answer", "", "Answer to check for correctness, for 'answer' action")
|
flag.StringVar(&t.Answer, "answer", "", "Answer to check for correctness, for 'answer' action")
|
||||||
flag.StringVar(&t.Filename, "filename", "", "Filename, for 'open' action")
|
flag.StringVar(&t.Filename, "filename", "", "Filename, for 'open' action")
|
||||||
basedir := flag.String("basedir", ".", "Base directory containing all puzzles")
|
basedir := flag.String("basedir", ".", "Base directory containing all puzzles")
|
||||||
envflag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
osfs := afero.NewOsFs()
|
osfs := afero.NewOsFs()
|
||||||
t.Fs = afero.NewBasePathFs(osfs, *basedir)
|
t.Fs = afero.NewBasePathFs(osfs, *basedir)
|
||||||
|
@ -130,7 +117,7 @@ func (t *T) Open() error {
|
||||||
// Mothball writes a mothball to the writer.
|
// Mothball writes a mothball to the writer.
|
||||||
func (t *T) Mothball() error {
|
func (t *T) Mothball() error {
|
||||||
c := t.NewCategory(t.Cat)
|
c := t.NewCategory(t.Cat)
|
||||||
mb, err := Mothball(c)
|
mb, err := transpile.Mothball(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -141,8 +128,8 @@ func (t *T) Mothball() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCategory returns a new Fs-backed category.
|
// NewCategory returns a new Fs-backed category.
|
||||||
func (t *T) NewCategory(name string) Category {
|
func (t *T) NewCategory(name string) transpile.Category {
|
||||||
return NewFsCategory(t.Fs, name)
|
return transpile.NewFsCategory(t.Fs, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/dirtbags/moth/pkg/transpile"
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -78,7 +79,7 @@ func TestEverything(t *testing.T) {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
p := Puzzle{}
|
p := transpile.Puzzle{}
|
||||||
if err := json.Unmarshal(stdout.Bytes(), &p); err != nil {
|
if err := json.Unmarshal(stdout.Bytes(), &p); err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package main
|
package transpile
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
|
@ -1,11 +1,9 @@
|
||||||
package main
|
package transpile
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
"log"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -15,6 +13,21 @@ import (
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
// NopReadCloser provides an io.ReadCloser which does nothing.
|
// NopReadCloser provides an io.ReadCloser which does nothing.
|
||||||
type NopReadCloser struct {
|
type NopReadCloser struct {
|
||||||
}
|
}
|
||||||
|
@ -81,7 +94,7 @@ func (c FsCategory) Puzzle(points int) (Puzzle, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open returns an io.ReadCloser for the given filename.
|
// 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) (ReadSeekCloser, error) {
|
||||||
return NewFsPuzzle(c.fs, points).Open(filename)
|
return NewFsPuzzle(c.fs, points).Open(filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,18 +162,13 @@ func (c FsCommandCategory) Puzzle(points int) (Puzzle, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open returns an io.ReadCloser for the given filename.
|
// 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) (ReadSeekCloser, error) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), c.timeout)
|
ctx, cancel := context.WithTimeout(context.Background(), c.timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
cmd := exec.CommandContext(ctx, c.command, "file", strconv.Itoa(points), filename)
|
cmd := exec.CommandContext(ctx, c.command, "file", strconv.Itoa(points), filename)
|
||||||
stdout, err := cmd.Output()
|
stdout, err := cmd.Output()
|
||||||
buf := ioutil.NopCloser(bytes.NewBuffer(stdout))
|
return nopCloser{bytes.NewReader(stdout)}, err
|
||||||
if err != nil {
|
|
||||||
return buf, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Answer checks whether an answer is correct.
|
// Answer checks whether an answer is correct.
|
|
@ -1,4 +1,4 @@
|
||||||
package main
|
package transpile
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
|
@ -0,0 +1,53 @@
|
||||||
|
package transpile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/afero"
|
||||||
|
)
|
||||||
|
|
||||||
|
var testMothYaml = []byte(`---
|
||||||
|
answers:
|
||||||
|
- YAML answer
|
||||||
|
pre:
|
||||||
|
authors:
|
||||||
|
- Arthur
|
||||||
|
- Buster
|
||||||
|
- DW
|
||||||
|
attachments:
|
||||||
|
- filename: moo.txt
|
||||||
|
---
|
||||||
|
YAML body
|
||||||
|
`)
|
||||||
|
var testMothRfc822 = []byte(`author: test
|
||||||
|
Author: Arthur
|
||||||
|
author: Fred Flintstone
|
||||||
|
answer: RFC822 answer
|
||||||
|
|
||||||
|
RFC822 body
|
||||||
|
`)
|
||||||
|
|
||||||
|
func newTestFs() afero.Fs {
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
afero.WriteFile(fs, "cat0/1/puzzle.md", testMothYaml, 0644)
|
||||||
|
afero.WriteFile(fs, "cat0/1/moo.txt", []byte("Moo."), 0644)
|
||||||
|
afero.WriteFile(fs, "cat0/2/puzzle.md", testMothRfc822, 0644)
|
||||||
|
afero.WriteFile(fs, "cat0/3/puzzle.moth", testMothYaml, 0644)
|
||||||
|
afero.WriteFile(fs, "cat0/4/puzzle.md", testMothYaml, 0644)
|
||||||
|
afero.WriteFile(fs, "cat0/5/puzzle.md", testMothYaml, 0644)
|
||||||
|
afero.WriteFile(fs, "cat0/10/puzzle.md", []byte(`---
|
||||||
|
Answers:
|
||||||
|
- moo
|
||||||
|
Authors:
|
||||||
|
- bad field
|
||||||
|
---
|
||||||
|
body
|
||||||
|
`), 0644)
|
||||||
|
afero.WriteFile(fs, "cat0/20/puzzle.md", []byte("Answer: no\nBadField: yes\n\nbody\n"), 0644)
|
||||||
|
afero.WriteFile(fs, "cat0/21/puzzle.md", []byte("Answer: broken\nSpooon\n"), 0644)
|
||||||
|
afero.WriteFile(fs, "cat0/22/puzzle.md", []byte("---\nanswers:\n - pencil\npre:\n unused-field: Spooon\n---\nSpoon?\n"), 0644)
|
||||||
|
afero.WriteFile(fs, "cat1/93/puzzle.md", []byte("Answer: no\n\nbody"), 0644)
|
||||||
|
afero.WriteFile(fs, "cat1/barney/puzzle.md", testMothYaml, 0644)
|
||||||
|
afero.WriteFile(fs, "unbroken/1/puzzle.md", testMothYaml, 0644)
|
||||||
|
afero.WriteFile(fs, "unbroken/1/moo.txt", []byte("Moo."), 0644)
|
||||||
|
afero.WriteFile(fs, "unbroken/2/puzzle.md", testMothRfc822, 0644)
|
||||||
|
return fs
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
package transpile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/spf13/afero"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Inventory maps category names to lists of point values.
|
||||||
|
type Inventory map[string][]int
|
||||||
|
|
||||||
|
// FsInventory returns a mapping of category names to puzzle point values.
|
||||||
|
func FsInventory(fs afero.Fs) (Inventory, error) {
|
||||||
|
dirEnts, err := afero.ReadDir(fs, ".")
|
||||||
|
if err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
inv := make(Inventory)
|
||||||
|
for _, ent := range dirEnts {
|
||||||
|
if ent.IsDir() {
|
||||||
|
name := ent.Name()
|
||||||
|
c := NewFsCategory(fs, name)
|
||||||
|
puzzles, err := c.Inventory()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sort.Ints(puzzles)
|
||||||
|
inv[name] = puzzles
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return inv, nil
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
package transpile
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestInventory(t *testing.T) {
|
||||||
|
fs := newTestFs()
|
||||||
|
inv, err := FsInventory(fs)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if c, ok := inv["cat0"]; !ok {
|
||||||
|
t.Error("No cat0")
|
||||||
|
} else if len(c) != 9 {
|
||||||
|
t.Error("Wrong category length", c)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package main
|
package transpile
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/zip"
|
"archive/zip"
|
|
@ -1,4 +1,4 @@
|
||||||
package main
|
package transpile
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/zip"
|
"archive/zip"
|
|
@ -1,4 +1,4 @@
|
||||||
package main
|
package transpile
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
@ -8,7 +8,6 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
"log"
|
||||||
"net/mail"
|
"net/mail"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
@ -92,13 +91,20 @@ type StaticAttachment struct {
|
||||||
Listed bool // Whether this file is listed as an attachment
|
Listed bool // Whether this file is listed as an attachment
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReadSeekCloser provides io.Reader, io.Seeker, and io.Closer.
|
||||||
|
type ReadSeekCloser interface {
|
||||||
|
io.Reader
|
||||||
|
io.Seeker
|
||||||
|
io.Closer
|
||||||
|
}
|
||||||
|
|
||||||
// PuzzleProvider establishes the functionality required to provide one puzzle.
|
// PuzzleProvider establishes the functionality required to provide one puzzle.
|
||||||
type PuzzleProvider interface {
|
type PuzzleProvider interface {
|
||||||
// Puzzle returns a Puzzle struct for the current puzzle.
|
// Puzzle returns a Puzzle struct for the current puzzle.
|
||||||
Puzzle() (Puzzle, error)
|
Puzzle() (Puzzle, error)
|
||||||
|
|
||||||
// Open returns a newly-opened file.
|
// Open returns a newly-opened file.
|
||||||
Open(filename string) (io.ReadCloser, error)
|
Open(filename string) (ReadSeekCloser, error)
|
||||||
|
|
||||||
// Answer returns whether the provided answer is correct.
|
// Answer returns whether the provided answer is correct.
|
||||||
Answer(answer string) bool
|
Answer(answer string) bool
|
||||||
|
@ -160,8 +166,8 @@ func (fp FsPuzzle) Puzzle() (Puzzle, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open returns a newly-opened file.
|
// Open returns a newly-opened file.
|
||||||
func (fp FsPuzzle) Open(name string) (io.ReadCloser, error) {
|
func (fp FsPuzzle) Open(name string) (ReadSeekCloser, error) {
|
||||||
empty := ioutil.NopCloser(new(bytes.Buffer))
|
empty := nopCloser{new(bytes.Reader)}
|
||||||
static, _, err := fp.staticPuzzle()
|
static, _, err := fp.staticPuzzle()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return empty, err
|
return empty, err
|
||||||
|
@ -343,15 +349,23 @@ func (fp FsCommandPuzzle) Puzzle() (Puzzle, error) {
|
||||||
return puzzle, nil
|
return puzzle, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type nopCloser struct {
|
||||||
|
io.ReadSeeker
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c nopCloser) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Open returns a newly-opened file.
|
// Open returns a newly-opened file.
|
||||||
func (fp FsCommandPuzzle) Open(filename string) (io.ReadCloser, error) {
|
// BUG(neale): FsCommandPuzzle.Open() reads everything into memory, and will suck for large files.
|
||||||
|
func (fp FsCommandPuzzle) Open(filename string) (ReadSeekCloser, error) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), fp.timeout)
|
ctx, cancel := context.WithTimeout(context.Background(), fp.timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
cmd := exec.CommandContext(ctx, fp.command, "-file", filename)
|
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()
|
out, err := cmd.Output()
|
||||||
buf := ioutil.NopCloser(bytes.NewBuffer(out))
|
buf := nopCloser{bytes.NewReader(out)}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return buf, err
|
return buf, err
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package main
|
package transpile
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
Loading…
Reference in New Issue