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" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"sync"
"time"
"git.andreafazzi.eu/andrea/probo/client" "git.andreafazzi.eu/andrea/probo/client"
"git.andreafazzi.eu/andrea/probo/hasher/sha256" "git.andreafazzi.eu/andrea/probo/hasher/sha256"
@ -20,16 +22,30 @@ type FileProboCollectorStore struct {
memoryStore *memory.MemoryProboCollectorStore memoryStore *memory.MemoryProboCollectorStore
paths map[string]string 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) { func NewFileProboCollectorStore(dirname string) (*FileProboCollectorStore, error) {
s := new(FileProboCollectorStore) s := new(FileProboCollectorStore)
files, err := ioutil.ReadDir(dirname) s.Dir = dirname
err := s.Reindex()
if err != nil { if err != nil {
return nil, err 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) s.paths = make(map[string]string)
markdownFiles := make([]fs.FileInfo, 0) markdownFiles := make([]fs.FileInfo, 0)
@ -42,7 +58,7 @@ func NewFileProboCollectorStore(dirname string) (*FileProboCollectorStore, error
} }
if len(markdownFiles) == 0 { if len(markdownFiles) == 0 {
return nil, fmt.Errorf("The directory is empty.") return fmt.Errorf("The directory is empty.")
} }
s.memoryStore = memory.NewMemoryProboCollectorStore( s.memoryStore = memory.NewMemoryProboCollectorStore(
@ -51,24 +67,27 @@ func NewFileProboCollectorStore(dirname string) (*FileProboCollectorStore, error
for _, file := range markdownFiles { for _, file := range markdownFiles {
filename := file.Name() filename := file.Name()
fullPath := filepath.Join(dirname, filename) fullPath := filepath.Join(s.Dir, filename)
content, err := os.ReadFile(fullPath) content, err := os.ReadFile(fullPath)
if err != nil { if err != nil {
return nil, err return err
} }
quiz, err := QuizFromMarkdown(string(content)) quiz, err := QuizFromMarkdown(string(content))
if err != nil { if err != nil {
return nil, err return err
} }
s.memoryStore.CreateQuiz(&client.CreateUpdateQuizRequest{ q, err := s.memoryStore.CreateQuiz(&client.CreateUpdateQuizRequest{
Quiz: quiz, Quiz: quiz,
}) })
if err != nil {
return err
} }
s.Dir = dirname s.SetPath(q, fullPath)
}
return s, nil return nil
} }
func (s *FileProboCollectorStore) ReadAllQuizzes() ([]*models.Quiz, error) { func (s *FileProboCollectorStore) ReadAllQuizzes() ([]*models.Quiz, error) {
@ -86,7 +105,12 @@ func (s *FileProboCollectorStore) CreateQuiz(r *client.CreateUpdateQuizRequest)
return nil, err 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) { 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 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) { func MarkdownFromQuiz(quiz *models.Quiz) (string, error) {
@ -103,20 +155,22 @@ func MarkdownFromQuiz(quiz *models.Quiz) (string, error) {
return "", errors.New("Quiz should contain a question but it wasn't provided.") 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.") return "", errors.New("Quiz should contain at least 2 answers but none was provided.")
} }
if quiz.Correct == nil { 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 correctAnswer := "* " + quiz.Correct.Text
var otherAnswers string var otherAnswers string
for _, answer := range quiz.Answers { for _, answer := range quiz.Answers {
if quiz.Correct != answer {
otherAnswers += "* " + answer.Text + "\n" otherAnswers += "* " + answer.Text + "\n"
} }
}
markdown := quiz.Question.Text + "\n\n" + correctAnswer + "\n" + otherAnswers 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 { func (s *FileProboCollectorStore) createOrUpdateMarkdownFile(quiz *models.Quiz) error {
var filename string
markdown, err := MarkdownFromQuiz(quiz) markdown, err := MarkdownFromQuiz(quiz)
if err != nil { if err != nil {
return err return err
} }
fn := s.paths[quiz.ID]
fn, _ := s.GetPath(quiz)
if fn == "" { 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 { if err != nil {
return err return err
} }
defer file.Close() defer file.Close()
_, err = file.Write([]byte(markdown)) _, err = file.Write([]byte(markdown))
@ -181,20 +236,7 @@ func (s *FileProboCollectorStore) createOrUpdateMarkdownFile(quiz *models.Quiz)
return err return err
} }
s.SetPath(quiz, fn)
return nil 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 ( import (
"fmt" "fmt"
"os" "os"
"path/filepath"
"reflect" "reflect"
"testing" "testing"
"git.andreafazzi.eu/andrea/probo/client" "git.andreafazzi.eu/andrea/probo/client"
"git.andreafazzi.eu/andrea/probo/hasher/sha256"
"git.andreafazzi.eu/andrea/probo/models" "git.andreafazzi.eu/andrea/probo/models"
"git.andreafazzi.eu/andrea/probo/store/memory"
"github.com/remogatto/prettytest" "github.com/remogatto/prettytest"
) )
@ -54,7 +55,7 @@ Question text (3).
} }
func (t *testSuite) TestReadAllQuizzes() { 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)) t.True(err == nil, fmt.Sprintf("A file store should be initialized without problems but an error occurred: %v", err))
if !t.Failed() { if !t.Failed() {
@ -68,19 +69,14 @@ func (t *testSuite) TestReadAllQuizzes() {
len(result), 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)), 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() { func (t *testSuite) TestMarkdownFromQuiz() {
dirname := "./test/quizzes" store := memory.NewMemoryProboCollectorStore(sha256.NewDefault256Hasher(sha256.DefaultSHA256HashingFn))
store, err := NewFileProboCollectorStore(dirname) quiz, err := store.CreateQuiz(
t.True(err == nil, fmt.Sprintf("A file store should be initialized without problems but an error occurred: %v", err))
_, err = store.CreateQuiz(
&client.CreateUpdateQuizRequest{ &client.CreateUpdateQuizRequest{
Quiz: &client.Quiz{ Quiz: &client.Quiz{
Question: &client.Question{Text: "Newly created question text."}, 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.
* Answer 1
* Answer 2
* Answer 3
* Answer 4
`, md)
}
}
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() {
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)) t.Nil(err, fmt.Sprintf("An error was raised when saving the quiz on disk: %v", err))
newFilename := filepath.Join( if !t.Failed() {
dirname, path, err := store.GetPath(quiz)
"94ed4e9cdf8e0a75a2c5ce925cb791ebc5977ce1801e12059f58ce4d66c0c7f6.md", t.Nil(err, "GetPath should not raise an error.")
)
exists, err := os.Stat(newFilename) if !t.Failed() {
exists, err := os.Stat(path)
t.Nil(err, "Stat should not return an error") t.Nil(err, "Stat should not return an error")
if !t.Failed() { if !t.Failed() {
t.True(exists != nil, "The new quiz file was not created.") t.True(exists != nil, "The new quiz file was not created.")
err := os.Remove(newFilename) if !t.Failed() {
t.Nil(err, "Stat should not return an error") 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() { func (t *testSuite) TestUpdateQuiz() {
dirname := "./test/quizzes" dirname := "./testdata/quizzes"
store, err := NewFileProboCollectorStore(dirname) store, err := NewFileProboCollectorStore(dirname)
t.True(err == nil, fmt.Sprintf("A file store should be initialized without problems but an error occurred: %v", err)) 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") t.Nil(err, "The quiz to be updated should be created without issue")
if !t.Failed() { if !t.Failed() {
_, err = store.UpdateQuiz( clientQuiz := &client.Quiz{
&client.CreateUpdateQuizRequest{ Question: &client.Question{Text: "Newly created question text."},
Quiz: &client.Quiz{
Question: &client.Question{Text: "Updated question text."},
Answers: []*client.Answer{ Answers: []*client.Answer{
{Text: "Answer 1", Correct: true}, {Text: "Answer 1", Correct: true},
{Text: "Answer 2", Correct: false}, {Text: "Answer 2", Correct: false},
{Text: "Answer 3", Correct: false}, {Text: "Answer 3", Correct: false},
{Text: "Answer 4", Correct: false}, {Text: "Answer 4", Correct: false},
}, },
}, }
updatedQuiz, err := store.UpdateQuiz(
&client.CreateUpdateQuizRequest{
Quiz: clientQuiz,
}, quiz.ID) }, 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 { func testsAreEqual(got, want []*models.Quiz) bool {
return reflect.DeepEqual(got, want) 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 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) { func (s *MemoryProboCollectorStore) createOrUpdateQuiz(r *client.CreateUpdateQuizRequest, id string) (*models.Quiz, error) {
hashes := s.hasher.QuizHashes(r.Quiz) hashes := s.hasher.QuizHashes(r.Quiz)
quizHash := hashes[len(hashes)-1] 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 { type ProboCollectorStore interface {
ReadAllQuizzes() ([]*models.Quiz, error) ReadAllQuizzes() ([]*models.Quiz, error)
ReadQuizByHash(hash string) (*models.Quiz, error)
CreateQuiz(r *client.CreateUpdateQuizRequest) (*models.Quiz, error) CreateQuiz(r *client.CreateUpdateQuizRequest) (*models.Quiz, error)
UpdateQuiz(r *client.CreateUpdateQuizRequest, id string) (*models.Quiz, error) UpdateQuiz(r *client.CreateUpdateQuizRequest, id string) (*models.Quiz, error)
} }