Update quiz using file store

This commit is contained in:
andrea 2023-07-10 13:23:46 +02:00
parent 7a1135de0f
commit 8817d2d314
10 changed files with 290 additions and 72 deletions

18
models/models_test.go Normal file
View 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),
)
}

View file

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

View file

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

View file

@ -1,7 +0,0 @@
Newly created question text.
* Answer 1
* Answer 1
* Answer 2
* Answer 3
* Answer 4

View file

@ -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]

View 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.")
}

View file

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