Transpiler seems complete

This commit is contained in:
Neale Pickett 2020-09-04 18:28:23 -06:00
parent 31a50cbf2c
commit 6696d27ee0
8 changed files with 297 additions and 66 deletions

View File

@ -143,6 +143,8 @@ func (c FsCommandCategory) Puzzle(points int) (Puzzle, error) {
return p, err return p, err
} }
p.computeAnswerHashes()
return p, nil return p, nil
} }

View File

@ -32,6 +32,7 @@ func TestFsCategory(t *testing.T) {
} }
if r, err := c.Open(1, "moo.txt"); err != nil { if r, err := c.Open(1, "moo.txt"); err != nil {
t.Log(c.Puzzle(1))
t.Error(err) t.Error(err)
} else { } else {
defer r.Close() defer r.Close()

View File

@ -28,39 +28,6 @@ type Category interface {
Answer(points int, answer string) bool 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). // 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
@ -95,6 +62,8 @@ func (t *T) Handle(action string) error {
return t.PrintInventory() return t.PrintInventory()
case "open": case "open":
return t.Open() return t.Open()
case "mothball":
return t.Mothball()
default: default:
return fmt.Errorf("Unimplemented action: %s", action) return fmt.Errorf("Unimplemented action: %s", action)
} }
@ -134,6 +103,7 @@ func (t *T) Open() error {
switch t.Filename { switch t.Filename {
case "puzzle.json", "": case "puzzle.json", "":
// BUG(neale): we need a way to tell the transpiler to strip answers
p, err := c.Puzzle(t.Points) p, err := c.Puzzle(t.Points)
if err != nil { if err != nil {
return err return err
@ -157,6 +127,19 @@ func (t *T) Open() error {
return nil 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. // NewCategory returns a new Fs-backed category.
func (t *T) NewCategory(name string) Category { func (t *T) NewCategory(name string) Category {
return NewFsCategory(t.Fs, name) return NewFsCategory(t.Fs, name)

View File

@ -17,6 +17,8 @@ pre:
- Arthur - Arthur
- Buster - Buster
- DW - DW
attachments:
- filename: moo.txt
--- ---
YAML body YAML body
`) `)
@ -46,9 +48,12 @@ body
`), 0644) `), 0644)
afero.WriteFile(fs, "cat0/20/puzzle.md", []byte("Answer: no\nBadField: yes\n\nbody\n"), 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/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/93/puzzle.md", []byte("Answer: no\n\nbody"), 0644)
afero.WriteFile(fs, "cat1/barney/puzzle.md", testMothYaml, 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 return fs
} }
@ -62,7 +67,7 @@ func TestEverything(t *testing.T) {
if err := tp.Handle("inventory"); err != nil { if err := tp.Handle("inventory"); err != nil {
t.Error(err) 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()) t.Errorf("Bad inventory: %#v", stdout.String())
} }
@ -89,4 +94,16 @@ func TestEverything(t *testing.T) {
if stdout.String() != "Moo." { if stdout.String() != "Moo." {
t.Error("Wrong file pulled") 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!")
}
} }

View File

@ -1,2 +1,77 @@
package main 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
}

View File

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

View File

@ -4,6 +4,7 @@ import (
"bufio" "bufio"
"bytes" "bytes"
"context" "context"
"crypto/sha256"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
@ -20,6 +21,77 @@ import (
"gopkg.in/yaml.v2" "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. // 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.
@ -60,11 +132,64 @@ type FsPuzzle struct {
// Puzzle returns a Puzzle struct for the current puzzle. // Puzzle returns a Puzzle struct for the current puzzle.
func (fp FsPuzzle) Puzzle() (Puzzle, error) { 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") r, err := fp.fs.Open("puzzle.md")
if err != nil { if err != nil {
var err2 error var err2 error
if r, err2 = fp.fs.Open("puzzle.moth"); err2 != nil { if r, err2 = fp.fs.Open("puzzle.moth"); err2 != nil {
return Puzzle{}, err return StaticPuzzle{}, nil, err
} }
} }
defer r.Close() defer r.Close()
@ -101,33 +226,21 @@ func (fp FsPuzzle) Puzzle() (Puzzle, error) {
bodyBuf.WriteRune('\n') bodyBuf.WriteRune('\n')
} }
puzzle, err := headerParser(headerBuf) static, err := headerParser(headerBuf)
if err != nil { if err != nil {
return puzzle, err return static, nil, err
} }
// Markdownify the body body := blackfriday.Run(bodyBuf.Bytes())
if puzzle.Pre.Body != "" {
if bodyBuf.Len() > 0 { return static, body, err
return puzzle, fmt.Errorf("Puzzle body present in header and in moth body")
}
} else {
puzzle.Pre.Body = string(blackfriday.Run(bodyBuf.Bytes()))
} }
return puzzle, nil func legacyAttachmentParser(val []string) []StaticAttachment {
} ret := make([]StaticAttachment, len(val))
// 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))
for idx, txt := range val { for idx, txt := range val {
parts := strings.SplitN(txt, " ", 3) parts := strings.SplitN(txt, " ", 3)
cur := Attachment{} cur := StaticAttachment{}
cur.FilesystemPath = parts[0] cur.FilesystemPath = parts[0]
if len(parts) > 1 { if len(parts) > 1 {
cur.Filename = parts[1] cur.Filename = parts[1]
@ -144,16 +257,16 @@ func legacyAttachmentParser(val []string) []Attachment {
return ret return ret
} }
func yamlHeaderParser(r io.Reader) (Puzzle, error) { func yamlHeaderParser(r io.Reader) (StaticPuzzle, error) {
p := Puzzle{} p := StaticPuzzle{}
decoder := yaml.NewDecoder(r) decoder := yaml.NewDecoder(r)
decoder.SetStrict(true) decoder.SetStrict(true)
err := decoder.Decode(&p) err := decoder.Decode(&p)
return p, err return p, err
} }
func rfc822HeaderParser(r io.Reader) (Puzzle, error) { func rfc822HeaderParser(r io.Reader) (StaticPuzzle, error) {
p := Puzzle{} p := StaticPuzzle{}
m, err := mail.ReadMessage(r) m, err := mail.ReadMessage(r)
if err != nil { if err != nil {
return p, fmt.Errorf("Parsing RFC822 headers: %v", err) 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. // Answer checks whether the given answer is correct.
func (fp FsPuzzle) Answer(answer string) bool { 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 return false
} }
@ -216,6 +338,8 @@ func (fp FsCommandPuzzle) Puzzle() (Puzzle, error) {
return Puzzle{}, err return Puzzle{}, err
} }
puzzle.computeAnswerHashes()
return puzzle, nil return puzzle, nil
} }

View File

@ -3,7 +3,6 @@ package main
import ( import (
"bytes" "bytes"
"io" "io"
"strings"
"testing" "testing"
"github.com/spf13/afero" "github.com/spf13/afero"
@ -64,12 +63,6 @@ func TestPuzzle(t *testing.T) {
if _, err := NewFsPuzzle(catFs, 21).Puzzle(); err == nil { if _, err := NewFsPuzzle(catFs, 21).Puzzle(); err == nil {
t.Error("Boken RFC822 header") 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() fs := afero.NewMemMapFs()