Update quiz using file store
This commit is contained in:
parent
7a1135de0f
commit
8817d2d314
10 changed files with 290 additions and 72 deletions
18
models/models_test.go
Normal file
18
models/models_test.go
Normal file
|
@ -0,0 +1,18 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/remogatto/prettytest"
|
||||
)
|
||||
|
||||
type testSuite struct {
|
||||
prettytest.Suite
|
||||
}
|
||||
|
||||
func TestRunner(t *testing.T) {
|
||||
prettytest.Run(
|
||||
t,
|
||||
new(testSuite),
|
||||
)
|
||||
}
|
|
@ -8,6 +8,8 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.andreafazzi.eu/andrea/probo/client"
|
||||
"git.andreafazzi.eu/andrea/probo/hasher/sha256"
|
||||
|
@ -20,16 +22,30 @@ type FileProboCollectorStore struct {
|
|||
|
||||
memoryStore *memory.MemoryProboCollectorStore
|
||||
paths map[string]string
|
||||
|
||||
// A mutex is used to synchronize read/write access to the map
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
func NewFileProboCollectorStore(dirname string) (*FileProboCollectorStore, error) {
|
||||
s := new(FileProboCollectorStore)
|
||||
|
||||
files, err := ioutil.ReadDir(dirname)
|
||||
s.Dir = dirname
|
||||
|
||||
err := s.Reindex()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *FileProboCollectorStore) Reindex() error {
|
||||
files, err := ioutil.ReadDir(s.Dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.paths = make(map[string]string)
|
||||
|
||||
markdownFiles := make([]fs.FileInfo, 0)
|
||||
|
@ -42,7 +58,7 @@ func NewFileProboCollectorStore(dirname string) (*FileProboCollectorStore, error
|
|||
}
|
||||
|
||||
if len(markdownFiles) == 0 {
|
||||
return nil, fmt.Errorf("The directory is empty.")
|
||||
return fmt.Errorf("The directory is empty.")
|
||||
}
|
||||
|
||||
s.memoryStore = memory.NewMemoryProboCollectorStore(
|
||||
|
@ -51,24 +67,27 @@ func NewFileProboCollectorStore(dirname string) (*FileProboCollectorStore, error
|
|||
|
||||
for _, file := range markdownFiles {
|
||||
filename := file.Name()
|
||||
fullPath := filepath.Join(dirname, filename)
|
||||
fullPath := filepath.Join(s.Dir, filename)
|
||||
|
||||
content, err := os.ReadFile(fullPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
quiz, err := QuizFromMarkdown(string(content))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
s.memoryStore.CreateQuiz(&client.CreateUpdateQuizRequest{
|
||||
q, err := s.memoryStore.CreateQuiz(&client.CreateUpdateQuizRequest{
|
||||
Quiz: quiz,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.SetPath(q, fullPath)
|
||||
}
|
||||
|
||||
s.Dir = dirname
|
||||
|
||||
return s, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *FileProboCollectorStore) ReadAllQuizzes() ([]*models.Quiz, error) {
|
||||
|
@ -86,7 +105,12 @@ func (s *FileProboCollectorStore) CreateQuiz(r *client.CreateUpdateQuizRequest)
|
|||
return nil, err
|
||||
}
|
||||
|
||||
return quiz, nil
|
||||
err = s.Reindex()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.memoryStore.ReadQuizByHash(quiz.Hash)
|
||||
}
|
||||
|
||||
func (s *FileProboCollectorStore) UpdateQuiz(r *client.CreateUpdateQuizRequest, id string) (*models.Quiz, error) {
|
||||
|
@ -95,7 +119,35 @@ func (s *FileProboCollectorStore) UpdateQuiz(r *client.CreateUpdateQuizRequest,
|
|||
return nil, err
|
||||
}
|
||||
|
||||
return quiz, nil
|
||||
err = s.createOrUpdateMarkdownFile(quiz)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = s.Reindex()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.memoryStore.ReadQuizByHash(quiz.Hash)
|
||||
}
|
||||
|
||||
func (s *FileProboCollectorStore) GetPath(quiz *models.Quiz) (string, error) {
|
||||
s.lock.RLock()
|
||||
defer s.lock.RUnlock()
|
||||
|
||||
path, ok := s.paths[quiz.ID]
|
||||
if !ok {
|
||||
return "", errors.New(fmt.Sprintf("Path not found for quiz ID %v", quiz.ID))
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func (s *FileProboCollectorStore) SetPath(quiz *models.Quiz, path string) string {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
s.paths[quiz.ID] = path
|
||||
return path
|
||||
}
|
||||
|
||||
func MarkdownFromQuiz(quiz *models.Quiz) (string, error) {
|
||||
|
@ -103,19 +155,21 @@ func MarkdownFromQuiz(quiz *models.Quiz) (string, error) {
|
|||
return "", errors.New("Quiz should contain a question but it wasn't provided.")
|
||||
}
|
||||
|
||||
if len(quiz.Answers) == 0 {
|
||||
if len(quiz.Answers) < 2 {
|
||||
return "", errors.New("Quiz should contain at least 2 answers but none was provided.")
|
||||
}
|
||||
|
||||
if quiz.Correct == nil {
|
||||
return "", errors.New("Quiz should contain a correct answer but non was provided.")
|
||||
return "", errors.New("Quiz should contain a correct answer but not was provided.")
|
||||
}
|
||||
|
||||
correctAnswer := "* " + quiz.Correct.Text
|
||||
var otherAnswers string
|
||||
|
||||
for _, answer := range quiz.Answers {
|
||||
otherAnswers += "* " + answer.Text + "\n"
|
||||
if quiz.Correct != answer {
|
||||
otherAnswers += "* " + answer.Text + "\n"
|
||||
}
|
||||
}
|
||||
|
||||
markdown := quiz.Question.Text + "\n\n" + correctAnswer + "\n" + otherAnswers
|
||||
|
@ -160,20 +214,21 @@ func QuizFromMarkdown(markdown string) (*client.Quiz, error) {
|
|||
}
|
||||
|
||||
func (s *FileProboCollectorStore) createOrUpdateMarkdownFile(quiz *models.Quiz) error {
|
||||
var filename string
|
||||
|
||||
markdown, err := MarkdownFromQuiz(quiz)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fn := s.paths[quiz.ID]
|
||||
|
||||
fn, _ := s.GetPath(quiz)
|
||||
if fn == "" {
|
||||
filename = filepath.Join(s.Dir, quiz.Hash+".md")
|
||||
fn = filepath.Join(s.Dir, fmt.Sprintf("quiz_%v.%s", time.Now().Unix(), "md"))
|
||||
}
|
||||
file, err := os.Create(filename)
|
||||
|
||||
file, err := os.Create(fn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
_, err = file.Write([]byte(markdown))
|
||||
|
@ -181,20 +236,7 @@ func (s *FileProboCollectorStore) createOrUpdateMarkdownFile(quiz *models.Quiz)
|
|||
return err
|
||||
}
|
||||
|
||||
s.SetPath(quiz, fn)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// func (s *FileProboCollectorStore) writeMarkdownFile(quiz *models.Quiz) error {
|
||||
// markdown, err := MarkdownFromQuiz(quiz)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// filename := filepath.Join(s.Dir, quiz.Hash+".md")
|
||||
// err = ioutil.WriteFile(filename, []byte(markdown), 0644)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// return nil
|
||||
// }
|
||||
|
|
|
@ -3,12 +3,13 @@ package file
|
|||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"git.andreafazzi.eu/andrea/probo/client"
|
||||
"git.andreafazzi.eu/andrea/probo/hasher/sha256"
|
||||
"git.andreafazzi.eu/andrea/probo/models"
|
||||
"git.andreafazzi.eu/andrea/probo/store/memory"
|
||||
"github.com/remogatto/prettytest"
|
||||
)
|
||||
|
||||
|
@ -54,7 +55,7 @@ Question text (3).
|
|||
}
|
||||
|
||||
func (t *testSuite) TestReadAllQuizzes() {
|
||||
store, err := NewFileProboCollectorStore("./test/quizzes")
|
||||
store, err := NewFileProboCollectorStore("./testdata/quizzes")
|
||||
t.True(err == nil, fmt.Sprintf("A file store should be initialized without problems but an error occurred: %v", err))
|
||||
|
||||
if !t.Failed() {
|
||||
|
@ -68,19 +69,14 @@ func (t *testSuite) TestReadAllQuizzes() {
|
|||
len(result),
|
||||
fmt.Sprintf("The store contains 3 files but only 2 should be parsed (duplicated quiz). Total of parsed quizzes are instead %v", len(result)),
|
||||
)
|
||||
t.Equal("Question text 1.", result[0].Question.Text)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (t *testSuite) TestCreateQuiz() {
|
||||
dirname := "./test/quizzes"
|
||||
store, err := NewFileProboCollectorStore(dirname)
|
||||
|
||||
t.True(err == nil, fmt.Sprintf("A file store should be initialized without problems but an error occurred: %v", err))
|
||||
|
||||
_, err = store.CreateQuiz(
|
||||
func (t *testSuite) TestMarkdownFromQuiz() {
|
||||
store := memory.NewMemoryProboCollectorStore(sha256.NewDefault256Hasher(sha256.DefaultSHA256HashingFn))
|
||||
quiz, err := store.CreateQuiz(
|
||||
&client.CreateUpdateQuizRequest{
|
||||
Quiz: &client.Quiz{
|
||||
Question: &client.Question{Text: "Newly created question text."},
|
||||
|
@ -92,25 +88,70 @@ func (t *testSuite) TestCreateQuiz() {
|
|||
},
|
||||
},
|
||||
})
|
||||
md, err := MarkdownFromQuiz(quiz)
|
||||
t.Nil(err, "Conversion to markdown should not raise an error")
|
||||
if !t.Failed() {
|
||||
t.Equal(`Newly created question text.
|
||||
|
||||
t.Nil(err, fmt.Sprintf("An error was raised when saving the quiz on disk: %v", err))
|
||||
* Answer 1
|
||||
* Answer 2
|
||||
* Answer 3
|
||||
* Answer 4
|
||||
`, md)
|
||||
}
|
||||
}
|
||||
|
||||
newFilename := filepath.Join(
|
||||
dirname,
|
||||
"94ed4e9cdf8e0a75a2c5ce925cb791ebc5977ce1801e12059f58ce4d66c0c7f6.md",
|
||||
)
|
||||
exists, err := os.Stat(newFilename)
|
||||
t.Nil(err, "Stat should not return an error")
|
||||
func (t *testSuite) TestCreateQuiz() {
|
||||
dirname := "./testdata/quizzes"
|
||||
store, err := NewFileProboCollectorStore(dirname)
|
||||
|
||||
t.True(err == nil, fmt.Sprintf("A file store should be initialized without problems but an error occurred: %v", err))
|
||||
|
||||
if !t.Failed() {
|
||||
t.True(exists != nil, "The new quiz file was not created.")
|
||||
err := os.Remove(newFilename)
|
||||
t.Nil(err, "Stat should not return an error")
|
||||
clientQuiz := &client.Quiz{
|
||||
Question: &client.Question{Text: "Newly created question text."},
|
||||
Answers: []*client.Answer{
|
||||
{Text: "Answer 1", Correct: true},
|
||||
{Text: "Answer 2", Correct: false},
|
||||
{Text: "Answer 3", Correct: false},
|
||||
{Text: "Answer 4", Correct: false},
|
||||
},
|
||||
}
|
||||
quiz, err := store.CreateQuiz(
|
||||
&client.CreateUpdateQuizRequest{
|
||||
Quiz: clientQuiz,
|
||||
},
|
||||
)
|
||||
|
||||
t.Nil(err, fmt.Sprintf("An error was raised when saving the quiz on disk: %v", err))
|
||||
|
||||
if !t.Failed() {
|
||||
path, err := store.GetPath(quiz)
|
||||
t.Nil(err, "GetPath should not raise an error.")
|
||||
|
||||
if !t.Failed() {
|
||||
exists, err := os.Stat(path)
|
||||
t.Nil(err, "Stat should not return an error")
|
||||
|
||||
if !t.Failed() {
|
||||
t.True(exists != nil, "The new quiz file was not created.")
|
||||
if !t.Failed() {
|
||||
quizFromDisk, err := readQuizFromDisk(path)
|
||||
t.Nil(err, "Quiz should be read from disk without errors.")
|
||||
if !t.Failed() {
|
||||
t.True(reflect.DeepEqual(quizFromDisk, clientQuiz), "Quiz read from disk and stored in memory should be equal.")
|
||||
err := os.Remove(path)
|
||||
t.Nil(err, "Test file should be removed without errors.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *testSuite) TestUpdateQuiz() {
|
||||
dirname := "./test/quizzes"
|
||||
dirname := "./testdata/quizzes"
|
||||
store, err := NewFileProboCollectorStore(dirname)
|
||||
t.True(err == nil, fmt.Sprintf("A file store should be initialized without problems but an error occurred: %v", err))
|
||||
|
||||
|
@ -130,18 +171,40 @@ func (t *testSuite) TestUpdateQuiz() {
|
|||
t.Nil(err, "The quiz to be updated should be created without issue")
|
||||
|
||||
if !t.Failed() {
|
||||
_, err = store.UpdateQuiz(
|
||||
clientQuiz := &client.Quiz{
|
||||
Question: &client.Question{Text: "Newly created question text."},
|
||||
Answers: []*client.Answer{
|
||||
{Text: "Answer 1", Correct: true},
|
||||
{Text: "Answer 2", Correct: false},
|
||||
{Text: "Answer 3", Correct: false},
|
||||
{Text: "Answer 4", Correct: false},
|
||||
},
|
||||
}
|
||||
|
||||
updatedQuiz, err := store.UpdateQuiz(
|
||||
&client.CreateUpdateQuizRequest{
|
||||
Quiz: &client.Quiz{
|
||||
Question: &client.Question{Text: "Updated question text."},
|
||||
Answers: []*client.Answer{
|
||||
{Text: "Answer 1", Correct: true},
|
||||
{Text: "Answer 2", Correct: false},
|
||||
{Text: "Answer 3", Correct: false},
|
||||
{Text: "Answer 4", Correct: false},
|
||||
},
|
||||
},
|
||||
Quiz: clientQuiz,
|
||||
}, quiz.ID)
|
||||
|
||||
t.Nil(err, fmt.Sprintf("Quiz should be updated without errors: %v", err))
|
||||
|
||||
if !t.Failed() {
|
||||
path, err := store.GetPath(updatedQuiz)
|
||||
|
||||
if !t.Failed() {
|
||||
t.Nil(err, "GetPath should not raise an error.")
|
||||
|
||||
if !t.Failed() {
|
||||
quizFromDisk, err := readQuizFromDisk(path)
|
||||
t.Nil(err, "Quiz should be read from disk without errors.")
|
||||
if !t.Failed() {
|
||||
t.True(reflect.DeepEqual(clientQuiz, quizFromDisk), "Quiz should be updated.")
|
||||
err := os.Remove(path)
|
||||
t.Nil(err, "Stat should not return an error")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -152,6 +215,14 @@ func createQuizOnDisk(store *FileProboCollectorStore, req *client.CreateUpdateQu
|
|||
|
||||
}
|
||||
|
||||
func readQuizFromDisk(path string) (*client.Quiz, error) {
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return QuizFromMarkdown(string(content))
|
||||
}
|
||||
|
||||
func testsAreEqual(got, want []*models.Quiz) bool {
|
||||
return reflect.DeepEqual(got, want)
|
||||
}
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
Newly created question text.
|
||||
|
||||
* Answer 1
|
||||
* Answer 1
|
||||
* Answer 2
|
||||
* Answer 3
|
||||
* Answer 4
|
|
@ -124,7 +124,21 @@ func (s *MemoryProboCollectorStore) ReadAllQuizzes() ([]*models.Quiz, error) {
|
|||
return result, nil
|
||||
}
|
||||
|
||||
func (s *MemoryProboCollectorStore) ReadQuizByHash(hash string) (*models.Quiz, error) {
|
||||
quiz := s.getQuizFromHash(hash)
|
||||
if quiz == nil {
|
||||
return nil, fmt.Errorf("Quiz with hash %s was not found in the store.", hash)
|
||||
}
|
||||
return quiz, nil
|
||||
}
|
||||
|
||||
func (s *MemoryProboCollectorStore) CalculateQuizHash(quiz *client.Quiz) string {
|
||||
hashes := s.hasher.QuizHashes(quiz)
|
||||
return hashes[len(hashes)-1]
|
||||
}
|
||||
|
||||
func (s *MemoryProboCollectorStore) createOrUpdateQuiz(r *client.CreateUpdateQuizRequest, id string) (*models.Quiz, error) {
|
||||
|
||||
hashes := s.hasher.QuizHashes(r.Quiz)
|
||||
quizHash := hashes[len(hashes)-1]
|
||||
|
||||
|
|
78
store/memory/memory_test.go
Normal file
78
store/memory/memory_test.go
Normal file
|
@ -0,0 +1,78 @@
|
|||
package memory
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"git.andreafazzi.eu/andrea/probo/client"
|
||||
"git.andreafazzi.eu/andrea/probo/hasher/sha256"
|
||||
"github.com/remogatto/prettytest"
|
||||
)
|
||||
|
||||
type testSuite struct {
|
||||
prettytest.Suite
|
||||
}
|
||||
|
||||
func TestRunner(t *testing.T) {
|
||||
prettytest.Run(
|
||||
t,
|
||||
new(testSuite),
|
||||
)
|
||||
}
|
||||
|
||||
func (t *testSuite) TestReadQuizByHash() {
|
||||
store := NewMemoryProboCollectorStore(
|
||||
sha256.NewDefault256Hasher(sha256.DefaultSHA256HashingFn),
|
||||
)
|
||||
quiz, _ := store.CreateQuiz(
|
||||
&client.CreateUpdateQuizRequest{
|
||||
Quiz: &client.Quiz{
|
||||
Question: &client.Question{Text: "Newly created question text."},
|
||||
Answers: []*client.Answer{
|
||||
{Text: "Answer 1", Correct: true},
|
||||
{Text: "Answer 2", Correct: false},
|
||||
{Text: "Answer 3", Correct: false},
|
||||
{Text: "Answer 4", Correct: false},
|
||||
},
|
||||
},
|
||||
})
|
||||
quizFromMemory, err := store.ReadQuizByHash(quiz.Hash)
|
||||
t.Nil(err, "Quiz should be found in the store")
|
||||
if !t.Failed() {
|
||||
t.True(reflect.DeepEqual(quizFromMemory, quiz), "Quiz should be equal")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (t *testSuite) TestUpdateQuiz() {
|
||||
store := NewMemoryProboCollectorStore(
|
||||
sha256.NewDefault256Hasher(sha256.DefaultSHA256HashingFn),
|
||||
)
|
||||
quiz, _ := store.CreateQuiz(
|
||||
&client.CreateUpdateQuizRequest{
|
||||
Quiz: &client.Quiz{
|
||||
Question: &client.Question{Text: "Newly created question text."},
|
||||
Answers: []*client.Answer{
|
||||
{Text: "Answer 1", Correct: true},
|
||||
{Text: "Answer 2", Correct: false},
|
||||
{Text: "Answer 3", Correct: false},
|
||||
{Text: "Answer 4", Correct: false},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
updatedQuiz, _ := store.CreateQuiz(
|
||||
&client.CreateUpdateQuizRequest{
|
||||
Quiz: &client.Quiz{
|
||||
Question: &client.Question{Text: "Updated question text."},
|
||||
Answers: []*client.Answer{
|
||||
{Text: "Answer 1", Correct: true},
|
||||
{Text: "Answer 2", Correct: false},
|
||||
{Text: "Answer 3", Correct: false},
|
||||
{Text: "Answer 4", Correct: false},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
t.True(quiz.Hash != updatedQuiz.Hash, "The two hashes should not be equal.")
|
||||
}
|
|
@ -8,6 +8,8 @@ import (
|
|||
type ProboCollectorStore interface {
|
||||
ReadAllQuizzes() ([]*models.Quiz, error)
|
||||
|
||||
ReadQuizByHash(hash string) (*models.Quiz, error)
|
||||
|
||||
CreateQuiz(r *client.CreateUpdateQuizRequest) (*models.Quiz, error)
|
||||
UpdateQuiz(r *client.CreateUpdateQuizRequest, id string) (*models.Quiz, error)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue