mirror of https://github.com/dirtbags/moth.git
Transpiler seems complete
This commit is contained in:
parent
31a50cbf2c
commit
6696d27ee0
|
@ -143,6 +143,8 @@ func (c FsCommandCategory) Puzzle(points int) (Puzzle, error) {
|
|||
return p, err
|
||||
}
|
||||
|
||||
p.computeAnswerHashes()
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ func TestFsCategory(t *testing.T) {
|
|||
}
|
||||
|
||||
if r, err := c.Open(1, "moo.txt"); err != nil {
|
||||
t.Log(c.Puzzle(1))
|
||||
t.Error(err)
|
||||
} else {
|
||||
defer r.Close()
|
||||
|
|
|
@ -28,39 +28,6 @@ type Category interface {
|
|||
Answer(points int, answer string) bool
|
||||
}
|
||||
|
||||
// Puzzle contains everything about a puzzle.
|
||||
type Puzzle struct {
|
||||
Pre struct {
|
||||
Authors []string
|
||||
Attachments []Attachment
|
||||
Scripts []Attachment
|
||||
AnswerPattern string
|
||||
Body string
|
||||
}
|
||||
Post struct {
|
||||
Objective string
|
||||
Success struct {
|
||||
Acceptable string
|
||||
Mastery string
|
||||
}
|
||||
KSAs []string
|
||||
}
|
||||
Debug struct {
|
||||
Log []string
|
||||
Errors []string
|
||||
Hints []string
|
||||
Summary string
|
||||
}
|
||||
Answers []string
|
||||
}
|
||||
|
||||
// Attachment carries information about an attached file.
|
||||
type Attachment struct {
|
||||
Filename string // Filename presented as part of puzzle
|
||||
FilesystemPath string // Filename in backing FS (URL, mothball, or local FS)
|
||||
Listed bool // Whether this file is listed as an attachment
|
||||
}
|
||||
|
||||
// T contains everything required for a transpilation invocation (across the nation).
|
||||
type T struct {
|
||||
// What action to take
|
||||
|
@ -95,6 +62,8 @@ func (t *T) Handle(action string) error {
|
|||
return t.PrintInventory()
|
||||
case "open":
|
||||
return t.Open()
|
||||
case "mothball":
|
||||
return t.Mothball()
|
||||
default:
|
||||
return fmt.Errorf("Unimplemented action: %s", action)
|
||||
}
|
||||
|
@ -134,6 +103,7 @@ func (t *T) Open() error {
|
|||
|
||||
switch t.Filename {
|
||||
case "puzzle.json", "":
|
||||
// BUG(neale): we need a way to tell the transpiler to strip answers
|
||||
p, err := c.Puzzle(t.Points)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -157,6 +127,19 @@ func (t *T) Open() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Mothball writes a mothball to the writer.
|
||||
func (t *T) Mothball() error {
|
||||
c := t.NewCategory(t.Cat)
|
||||
mb, err := Mothball(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(t.w, mb); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewCategory returns a new Fs-backed category.
|
||||
func (t *T) NewCategory(name string) Category {
|
||||
return NewFsCategory(t.Fs, name)
|
||||
|
|
|
@ -17,6 +17,8 @@ pre:
|
|||
- Arthur
|
||||
- Buster
|
||||
- DW
|
||||
attachments:
|
||||
- filename: moo.txt
|
||||
---
|
||||
YAML body
|
||||
`)
|
||||
|
@ -46,9 +48,12 @@ 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 body: Spooon\n---\nSpoon?\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
|
||||
}
|
||||
|
||||
|
@ -62,7 +67,7 @@ func TestEverything(t *testing.T) {
|
|||
if err := tp.Handle("inventory"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if strings.TrimSpace(stdout.String()) != `{"cat0":[1,2,3,4,5,10,20,21,22],"cat1":[93]}` {
|
||||
if strings.TrimSpace(stdout.String()) != `{"cat0":[1,2,3,4,5,10,20,21,22],"cat1":[93],"unbroken":[1,2]}` {
|
||||
t.Errorf("Bad inventory: %#v", stdout.String())
|
||||
}
|
||||
|
||||
|
@ -89,4 +94,16 @@ func TestEverything(t *testing.T) {
|
|||
if stdout.String() != "Moo." {
|
||||
t.Error("Wrong file pulled")
|
||||
}
|
||||
|
||||
stdout.Reset()
|
||||
tp.Cat = "unbroken"
|
||||
if err := tp.Handle("mothball"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if stdout.Len() < 200 {
|
||||
t.Error("That's way too short to be a mothball")
|
||||
}
|
||||
if stdout.String()[:2] != "PK" {
|
||||
t.Error("This mothball isn't a zip file!")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,2 +1,77 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// Mothball packages a Category up for a production server run.
|
||||
func Mothball(c Category) (*bytes.Reader, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
zf := zip.NewWriter(buf)
|
||||
|
||||
inv, err := c.Inventory()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
puzzlesTxt, err := zf.Create("puzzles.txt")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
answersTxt, err := zf.Create("answers.txt")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, points := range inv {
|
||||
fmt.Fprintln(puzzlesTxt, points)
|
||||
|
||||
puzzlePath := fmt.Sprintf("%d/puzzle.json", points)
|
||||
pw, err := zf.Create(puzzlePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
puzzle, err := c.Puzzle(points)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Record answers in answers.txt
|
||||
for _, answer := range puzzle.Answers {
|
||||
fmt.Fprintln(answersTxt, points, answer)
|
||||
}
|
||||
|
||||
// Remove all answers from puzzle object
|
||||
puzzle.Answers = []string{}
|
||||
|
||||
// Write out Puzzle object
|
||||
penc := json.NewEncoder(pw)
|
||||
if err := penc.Encode(puzzle); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Write out all attachments and scripts
|
||||
attachments := append(puzzle.Pre.Attachments, puzzle.Pre.Scripts...)
|
||||
for _, att := range attachments {
|
||||
attPath := fmt.Sprintf("%d/%s", points, att)
|
||||
aw, err := zf.Create(attPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ar, err := c.Open(points, att)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := io.Copy(aw, ar); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
zf.Close()
|
||||
|
||||
return bytes.NewReader(buf.Bytes()), nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
"github.com/spf13/afero/zipfs"
|
||||
)
|
||||
|
||||
func TestMothballs(t *testing.T) {
|
||||
fs := NewRecursiveBasePathFs(afero.NewOsFs(), "testdata")
|
||||
static := NewFsCategory(fs, "static")
|
||||
mb, err := Mothball(static)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
mbr, err := zip.NewReader(mb, int64(mb.Len()))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
zfs := zipfs.New(mbr)
|
||||
|
||||
if f, err := zfs.Open("puzzles.txt"); err != nil {
|
||||
t.Error(err)
|
||||
} else {
|
||||
defer f.Close()
|
||||
if buf, err := ioutil.ReadAll(f); err != nil {
|
||||
t.Error(err)
|
||||
} else if string(buf) != "" {
|
||||
t.Error("Bad puzzles.txt", string(buf))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ import (
|
|||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
|
@ -20,6 +21,77 @@ import (
|
|||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// Puzzle contains everything about a puzzle that a client would see.
|
||||
type Puzzle struct {
|
||||
Pre struct {
|
||||
Authors []string
|
||||
Attachments []string
|
||||
Scripts []string
|
||||
AnswerHashes []string
|
||||
AnswerPattern string
|
||||
Body string
|
||||
}
|
||||
Post struct {
|
||||
Objective string
|
||||
Success struct {
|
||||
Acceptable string
|
||||
Mastery string
|
||||
}
|
||||
KSAs []string
|
||||
}
|
||||
Debug struct {
|
||||
Log []string
|
||||
Errors []string
|
||||
Hints []string
|
||||
Summary string
|
||||
}
|
||||
Answers []string
|
||||
}
|
||||
|
||||
func (puzzle *Puzzle) computeAnswerHashes() {
|
||||
if len(puzzle.Answers) == 0 {
|
||||
return
|
||||
}
|
||||
puzzle.Pre.AnswerHashes = make([]string, len(puzzle.Answers))
|
||||
for i, answer := range puzzle.Answers {
|
||||
sum := sha256.Sum256([]byte(answer))
|
||||
hexsum := fmt.Sprintf("%x", sum)
|
||||
puzzle.Pre.AnswerHashes[i] = hexsum
|
||||
}
|
||||
}
|
||||
|
||||
// StaticPuzzle contains everything a static puzzle might tell us.
|
||||
type StaticPuzzle struct {
|
||||
Pre struct {
|
||||
Authors []string
|
||||
Attachments []StaticAttachment
|
||||
Scripts []StaticAttachment
|
||||
AnswerPattern string
|
||||
}
|
||||
Post struct {
|
||||
Objective string
|
||||
Success struct {
|
||||
Acceptable string
|
||||
Mastery string
|
||||
}
|
||||
KSAs []string
|
||||
}
|
||||
Debug struct {
|
||||
Log []string
|
||||
Errors []string
|
||||
Hints []string
|
||||
Summary string
|
||||
}
|
||||
Answers []string
|
||||
}
|
||||
|
||||
// StaticAttachment carries information about an attached file.
|
||||
type StaticAttachment struct {
|
||||
Filename string // Filename presented as part of puzzle
|
||||
FilesystemPath string // Filename in backing FS (URL, mothball, or local FS)
|
||||
Listed bool // Whether this file is listed as an attachment
|
||||
}
|
||||
|
||||
// PuzzleProvider establishes the functionality required to provide one puzzle.
|
||||
type PuzzleProvider interface {
|
||||
// Puzzle returns a Puzzle struct for the current puzzle.
|
||||
|
@ -60,11 +132,64 @@ type FsPuzzle struct {
|
|||
|
||||
// Puzzle returns a Puzzle struct for the current puzzle.
|
||||
func (fp FsPuzzle) Puzzle() (Puzzle, error) {
|
||||
var puzzle Puzzle
|
||||
|
||||
static, body, err := fp.staticPuzzle()
|
||||
if err != nil {
|
||||
return puzzle, err
|
||||
}
|
||||
|
||||
// Convert to an exportable Puzzle
|
||||
puzzle.Post = static.Post
|
||||
puzzle.Debug = static.Debug
|
||||
puzzle.Answers = static.Answers
|
||||
puzzle.Pre.Authors = static.Pre.Authors
|
||||
puzzle.Pre.Body = string(body)
|
||||
puzzle.Pre.AnswerPattern = static.Pre.AnswerPattern
|
||||
puzzle.Pre.Attachments = make([]string, len(static.Pre.Attachments))
|
||||
for i, attachment := range static.Pre.Attachments {
|
||||
puzzle.Pre.Attachments[i] = attachment.Filename
|
||||
}
|
||||
puzzle.Pre.Scripts = make([]string, len(static.Pre.Scripts))
|
||||
for i, script := range static.Pre.Scripts {
|
||||
puzzle.Pre.Scripts[i] = script.Filename
|
||||
}
|
||||
puzzle.computeAnswerHashes()
|
||||
|
||||
return puzzle, nil
|
||||
}
|
||||
|
||||
// Open returns a newly-opened file.
|
||||
func (fp FsPuzzle) Open(name string) (io.ReadCloser, error) {
|
||||
empty := ioutil.NopCloser(new(bytes.Buffer))
|
||||
static, _, err := fp.staticPuzzle()
|
||||
if err != nil {
|
||||
return empty, err
|
||||
}
|
||||
|
||||
var fsPath string
|
||||
for _, attachment := range append(static.Pre.Attachments, static.Pre.Scripts...) {
|
||||
if attachment.Filename == name {
|
||||
if attachment.FilesystemPath == "" {
|
||||
fsPath = attachment.Filename
|
||||
} else {
|
||||
fsPath = attachment.FilesystemPath
|
||||
}
|
||||
}
|
||||
}
|
||||
if fsPath == "" {
|
||||
return empty, fmt.Errorf("Not listed in attachments or scripts: %s", name)
|
||||
}
|
||||
|
||||
return fp.fs.Open(fsPath)
|
||||
}
|
||||
|
||||
func (fp FsPuzzle) staticPuzzle() (StaticPuzzle, []byte, error) {
|
||||
r, err := fp.fs.Open("puzzle.md")
|
||||
if err != nil {
|
||||
var err2 error
|
||||
if r, err2 = fp.fs.Open("puzzle.moth"); err2 != nil {
|
||||
return Puzzle{}, err
|
||||
return StaticPuzzle{}, nil, err
|
||||
}
|
||||
}
|
||||
defer r.Close()
|
||||
|
@ -101,33 +226,21 @@ func (fp FsPuzzle) Puzzle() (Puzzle, error) {
|
|||
bodyBuf.WriteRune('\n')
|
||||
}
|
||||
|
||||
puzzle, err := headerParser(headerBuf)
|
||||
static, err := headerParser(headerBuf)
|
||||
if err != nil {
|
||||
return puzzle, err
|
||||
return static, nil, err
|
||||
}
|
||||
|
||||
// Markdownify the body
|
||||
if puzzle.Pre.Body != "" {
|
||||
if bodyBuf.Len() > 0 {
|
||||
return puzzle, fmt.Errorf("Puzzle body present in header and in moth body")
|
||||
}
|
||||
} else {
|
||||
puzzle.Pre.Body = string(blackfriday.Run(bodyBuf.Bytes()))
|
||||
body := blackfriday.Run(bodyBuf.Bytes())
|
||||
|
||||
return static, body, err
|
||||
}
|
||||
|
||||
return puzzle, nil
|
||||
}
|
||||
|
||||
// Open returns a newly-opened file.
|
||||
func (fp FsPuzzle) Open(name string) (io.ReadCloser, error) {
|
||||
return fp.fs.Open(name)
|
||||
}
|
||||
|
||||
func legacyAttachmentParser(val []string) []Attachment {
|
||||
ret := make([]Attachment, len(val))
|
||||
func legacyAttachmentParser(val []string) []StaticAttachment {
|
||||
ret := make([]StaticAttachment, len(val))
|
||||
for idx, txt := range val {
|
||||
parts := strings.SplitN(txt, " ", 3)
|
||||
cur := Attachment{}
|
||||
cur := StaticAttachment{}
|
||||
cur.FilesystemPath = parts[0]
|
||||
if len(parts) > 1 {
|
||||
cur.Filename = parts[1]
|
||||
|
@ -144,16 +257,16 @@ func legacyAttachmentParser(val []string) []Attachment {
|
|||
return ret
|
||||
}
|
||||
|
||||
func yamlHeaderParser(r io.Reader) (Puzzle, error) {
|
||||
p := Puzzle{}
|
||||
func yamlHeaderParser(r io.Reader) (StaticPuzzle, error) {
|
||||
p := StaticPuzzle{}
|
||||
decoder := yaml.NewDecoder(r)
|
||||
decoder.SetStrict(true)
|
||||
err := decoder.Decode(&p)
|
||||
return p, err
|
||||
}
|
||||
|
||||
func rfc822HeaderParser(r io.Reader) (Puzzle, error) {
|
||||
p := Puzzle{}
|
||||
func rfc822HeaderParser(r io.Reader) (StaticPuzzle, error) {
|
||||
p := StaticPuzzle{}
|
||||
m, err := mail.ReadMessage(r)
|
||||
if err != nil {
|
||||
return p, fmt.Errorf("Parsing RFC822 headers: %v", err)
|
||||
|
@ -188,6 +301,15 @@ func rfc822HeaderParser(r io.Reader) (Puzzle, error) {
|
|||
|
||||
// Answer checks whether the given answer is correct.
|
||||
func (fp FsPuzzle) Answer(answer string) bool {
|
||||
p, _, err := fp.staticPuzzle()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
for _, ans := range p.Answers {
|
||||
if ans == answer {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -216,6 +338,8 @@ func (fp FsCommandPuzzle) Puzzle() (Puzzle, error) {
|
|||
return Puzzle{}, err
|
||||
}
|
||||
|
||||
puzzle.computeAnswerHashes()
|
||||
|
||||
return puzzle, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,6 @@ package main
|
|||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
|
@ -64,12 +63,6 @@ func TestPuzzle(t *testing.T) {
|
|||
if _, err := NewFsPuzzle(catFs, 21).Puzzle(); err == nil {
|
||||
t.Error("Boken RFC822 header")
|
||||
}
|
||||
if p, err := NewFsPuzzle(catFs, 22).Puzzle(); err == nil {
|
||||
t.Error("Duplicate bodies")
|
||||
} else if !strings.HasPrefix(err.Error(), "Puzzle body present") {
|
||||
t.Log(p)
|
||||
t.Error("Wrong error for duplicate body:", err)
|
||||
}
|
||||
|
||||
{
|
||||
fs := afero.NewMemMapFs()
|
||||
|
|
Loading…
Reference in New Issue