diff --git a/students/hackeryarn/hackeryarn b/students/hackeryarn/hackeryarn new file mode 100755 index 0000000..7d4e32e Binary files /dev/null and b/students/hackeryarn/hackeryarn differ diff --git a/students/hackeryarn/main.go b/students/hackeryarn/main.go new file mode 100644 index 0000000..f6546d7 --- /dev/null +++ b/students/hackeryarn/main.go @@ -0,0 +1,120 @@ +package main + +import ( + "encoding/csv" + "flag" + "fmt" + "io" + "log" + "os" + "time" + + quiz "github.com/gophercises/quiz/students/hackeryarn/myquiz" + "github.com/gophercises/quiz/students/hackeryarn/problem" +) + +const ( + // FileFlag is used to set a file for the questions + FileFlag = "file" + // FileFlagValue is the value used when no FileFlag is provided + FileFlagValue = "problems.csv" + // FileFlagUsage is the help string for the FileFlag + FileFlagUsage = "Questions file" + + // TimerFlag is used for setting a timer for the quiz + TimerFlag = "timer" + // TimerFlagValue is the value used when no TimerFlag is provided + TimerFlagValue = 30 + // TimerFlagUsage is the help string for the TimerFlag + TimerFlagUsage = "Amount of seconds the quiz will allow" +) + +// Flagger configures the flags used +type Flagger interface { + StringVar(p *string, name, value, usage string) + IntVar(p *int, name string, value int, usage string) +} + +type quizFlagger struct{} + +func (q *quizFlagger) StringVar(p *string, name, value, usage string) { + flag.StringVar(p, name, value, usage) +} + +func (q *quizFlagger) IntVar(p *int, name string, value int, usage string) { + flag.IntVar(p, name, value, usage) +} + +// Timer is used to start a timer +type Timer interface { + NewTimer(d time.Duration) *time.Timer +} + +type quizTimer struct{} + +func (q quizTimer) NewTimer(d time.Duration) *time.Timer { + return time.NewTimer(d) +} + +// ReadCSV parses the CSV file into a Problem struct +func ReadCSV(reader io.Reader) quiz.Quiz { + csvReader := csv.NewReader(reader) + + problems := []problem.Problem{} + for { + record, err := csvReader.Read() + if err == io.EOF { + break + } else if err != nil { + log.Fatalln("Error reading CSV:", err) + } + + problems = append(problems, problem.New(record)) + } + + return quiz.New(problems) +} + +// TimerSeconds is the amount of time allowed for the quiz +var TimerSeconds int +var file string + +// ConfigFlags sets all the flags used by the application +func ConfigFlags(f Flagger) { + f.StringVar(&file, FileFlag, FileFlagValue, FileFlagUsage) + f.IntVar(&TimerSeconds, TimerFlag, TimerFlagValue, TimerFlagUsage) +} + +// StartTimer begins a timer once the user provides input +func StartTimer(w io.Writer, r io.Reader, timer Timer) *time.Timer { + fmt.Fprint(w, "Ready to start?") + fmt.Fscanln(r) + + return timer.NewTimer(time.Second * time.Duration(TimerSeconds)) +} + +func init() { + flagger := &quizFlagger{} + ConfigFlags(flagger) + + flag.Parse() +} + +func main() { + file, err := os.Open(file) + if err != nil { + log.Fatalln("Could not open file", err) + } + + quiz := ReadCSV(file) + + timer := StartTimer(os.Stdout, os.Stdin, quizTimer{}) + go func() { + <-timer.C + fmt.Println("") + quiz.PrintResults(os.Stdout) + os.Exit(0) + }() + + quiz.Run(os.Stdout, os.Stdin) +} diff --git a/students/hackeryarn/myquiz/myquiz.go b/students/hackeryarn/myquiz/myquiz.go new file mode 100644 index 0000000..d37d229 --- /dev/null +++ b/students/hackeryarn/myquiz/myquiz.go @@ -0,0 +1,40 @@ +package quiz + +import ( + "fmt" + "io" + + "github.com/gophercises/quiz/students/hackeryarn/problem" +) + +// Quiz represents the quiz to be given to the user +type Quiz struct { + problems []problem.Problem + rightAnswers int +} + +// Run runs the quiz for all the problems keeping track of correct answers +func (q *Quiz) Run(w io.Writer, r io.Reader) { + for _, problem := range q.problems { + problem.AskQuestion(w) + correct := problem.CheckAnswer(r) + if correct { + q.rightAnswers++ + } + } + + q.PrintResults(w) +} + +// PrintResults outputs the results of the quiz +func (q Quiz) PrintResults(w io.Writer) { + fmt.Fprintf(w, "You got %d questions right!\n", q.rightAnswers) +} + +// New creates a new quiz from the supplied slice of problems +func New(problems []problem.Problem) Quiz { + return Quiz{ + problems: problems, + rightAnswers: 0, + } +} diff --git a/students/hackeryarn/myquiz/myquiz_test.go b/students/hackeryarn/myquiz/myquiz_test.go new file mode 100644 index 0000000..94b30de --- /dev/null +++ b/students/hackeryarn/myquiz/myquiz_test.go @@ -0,0 +1,66 @@ +package quiz + +import ( + "bytes" + "io" + "reflect" + "strings" + "testing" + + "github.com/gophercises/quiz/students/hackeryarn/problem" +) + +func TestNew(t *testing.T) { + problems := sampleProblems() + + want := Quiz{problems: problems, rightAnswers: 0} + got := New(problems) + + if !reflect.DeepEqual(want, got) { + t.Errorf("expeted to create quiz %v got %v", want, got) + } +} + +func TestRun(t *testing.T) { + t.Run("it runs the quiz", func(t *testing.T) { + buffer := &bytes.Buffer{} + quiz := createQuiz() + runQuiz(buffer, &quiz) + + expectedResults := 2 + results := quiz.rightAnswers + + if expectedResults != results { + t.Errorf("expected right answers of %v, got %v", + expectedResults, results) + } + + expectedOutput := "7+3: 1+1: You got 2 questions right!\n" + + if buffer.String() != expectedOutput { + t.Errorf("expected full output %v, got %v", + expectedOutput, buffer) + } + + }) +} + +func sampleProblems() []problem.Problem { + record1 := []string{"7+3", "10"} + record2 := []string{"1+1", "2"} + + return []problem.Problem{ + problem.New(record1), + problem.New(record2), + } +} + +func createQuiz() Quiz { + problems := sampleProblems() + return New(problems) +} + +func runQuiz(buffer io.Writer, quiz *Quiz) { + answers := strings.NewReader("10\n2\n") + quiz.Run(buffer, answers) +} diff --git a/students/hackeryarn/problem/problem.go b/students/hackeryarn/problem/problem.go new file mode 100644 index 0000000..31c9265 --- /dev/null +++ b/students/hackeryarn/problem/problem.go @@ -0,0 +1,49 @@ +package problem + +import ( + "fmt" + "io" + "log" + "strings" +) + +// Problem represents a single question answer pair +type Problem struct { + question string + answer string +} + +// CheckAnswer checks the answer against the provided input +func (p Problem) CheckAnswer(r io.Reader) bool { + answer := readAnswer(r) + + if answer != p.answer { + return false + } + return true +} + +func readAnswer(r io.Reader) (answer string) { + _, err := fmt.Fscanln(r, &answer) + if err != nil { + log.Fatalln("Error reading in answer", err) + } + + return strings.TrimSpace(answer) +} + +// AskQuestion prints out the question +func (p Problem) AskQuestion(w io.Writer) { + _, err := fmt.Fprintf(w, "%s: ", p.question) + if err != nil { + log.Fatalln("Could not ask the question", err) + } +} + +// New creates a Problem from a provided CSV record +func New(record []string) Problem { + return Problem{ + question: record[0], + answer: record[1], + } +} diff --git a/students/hackeryarn/problem/problem_test.go b/students/hackeryarn/problem/problem_test.go new file mode 100644 index 0000000..651a42c --- /dev/null +++ b/students/hackeryarn/problem/problem_test.go @@ -0,0 +1,68 @@ +package problem + +import ( + "bytes" + "testing" +) + +func TestNew(t *testing.T) { + record := []string{"question", "answer"} + + want := Problem{"question", "answer"} + got := New(record) + + if got != want { + t.Errorf("expected to create problem %v got %v", want, got) + } +} + +func TestCheckAnswer(t *testing.T) { + problem := createProblem() + + t.Run("it checks the correct answer", func(t *testing.T) { + answer := getAnswer(problem, "10\n") + + checkAnswer(t, answer, true) + }) + + t.Run("it checks incorrect answer", func(t *testing.T) { + answer := getAnswer(problem, "2\n") + + checkAnswer(t, answer, false) + }) +} + +func TestAskQuestion(t *testing.T) { + problem := createProblem() + + t.Run("it asks the question", func(t *testing.T) { + buffer := bytes.NewBuffer(nil) + + problem.AskQuestion(buffer) + + want := "7+3: " + got := buffer.String() + + if want != got { + t.Errorf("Expected question %s, got %s", want, got) + } + }) + +} + +func createProblem() Problem { + record := []string{"7+3", "10"} + return New(record) +} + +func getAnswer(problem Problem, input string) bool { + r := bytes.NewBufferString(input) + + return problem.CheckAnswer(r) +} + +func checkAnswer(t *testing.T, got, want bool) { + if want != got { + t.Errorf("Expected to return %v got %v", want, got) + } +} diff --git a/students/hackeryarn/problems.csv b/students/hackeryarn/problems.csv new file mode 100644 index 0000000..11506cb --- /dev/null +++ b/students/hackeryarn/problems.csv @@ -0,0 +1,12 @@ +5+5,10 +1+1,2 +8+3,11 +1+2,3 +8+6,14 +3+1,4 +1+4,5 +5+1,6 +2+3,5 +3+3,6 +2+4,6 +5+2,7 diff --git a/students/hackeryarn/quiz_test.go b/students/hackeryarn/quiz_test.go new file mode 100644 index 0000000..97d6818 --- /dev/null +++ b/students/hackeryarn/quiz_test.go @@ -0,0 +1,136 @@ +package main + +import ( + "bytes" + "reflect" + "testing" + "time" + + quiz "github.com/gophercises/quiz/students/hackeryarn/myquiz" + "github.com/gophercises/quiz/students/hackeryarn/problem" +) + +type flaggerMock struct { + stringVarCalls int + intVarCalls int + varNames []string + varUsages []string + varStringValues []string + varIntValues []int +} + +func (f *flaggerMock) StringVar(p *string, name, value, usage string) { + f.stringVarCalls++ + f.varNames = append(f.varNames, name) + f.varStringValues = append(f.varStringValues, value) + f.varUsages = append(f.varUsages, usage) +} + +func (f *flaggerMock) IntVar(p *int, name string, value int, usage string) { + f.intVarCalls++ + f.varNames = append(f.varNames, name) + f.varIntValues = append(f.varIntValues, value) + f.varUsages = append(f.varUsages, usage) +} + +type timerMock struct { + duration int +} + +func (t *timerMock) NewTimer(d time.Duration) *time.Timer { + t.duration = int(d.Seconds()) + return time.NewTimer(1 * time.Millisecond) +} + +func TestReadCSV(t *testing.T) { + input := "7+3,10\n1+1,2" + reader := bytes.NewBufferString(input) + + record1 := []string{"7+3", "10"} + record2 := []string{"1+1", "2"} + problems := []problem.Problem{ + problem.New(record1), + problem.New(record2), + } + + want := quiz.New(problems) + got := ReadCSV(reader) + + if !reflect.DeepEqual(want, got) { + t.Errorf("it should read in %v got %v", want, got) + } +} + +func TestConfigFlags(t *testing.T) { + flagger := &flaggerMock{} + + ConfigFlags(flagger) + + assertStringCalls(t, flagger) + assertIntCalls(t, flagger) + assertFlags(t, flagger) +} + +func TestStartTimer(t *testing.T) { + timer := &timerMock{} + w := &bytes.Buffer{} + r := bytes.NewBufferString("\n") + TimerSeconds := 30 + + StartTimer(w, r, timer) + + if timer.duration != TimerSeconds { + t.Errorf("it should set timer for %d seconds, set for %d", + TimerSeconds, timer.duration) + } + + if w.String() != "Ready to start?" { + t.Errorf("it should ask user if the user is ready, got %s", w.String()) + } +} + +func assertStringCalls(t *testing.T, flagger *flaggerMock) { + t.Helper() + if flagger.stringVarCalls != 1 { + t.Errorf("it should call StringVar %d times, called %d", + 1, flagger.stringVarCalls) + } +} + +func assertIntCalls(t *testing.T, flagger *flaggerMock) { + t.Helper() + if flagger.intVarCalls != 1 { + t.Errorf("it should call IntVar %d times, called %d", + 1, flagger.intVarCalls) + } +} + +func assertFlags(t *testing.T, flagger *flaggerMock) { + t.Helper() + + expectedNames := []string{FileFlag, TimerFlag} + expectedUsages := []string{FileFlagUsage, TimerFlagUsage} + expectedStringValues := []string{FileFlagValue} + expectedIntValues := []int{TimerFlagValue} + + if !reflect.DeepEqual(expectedNames, flagger.varNames) { + t.Errorf("it should setup flag names to be %v, got %v", + expectedNames, flagger.varNames) + } + + if !reflect.DeepEqual(expectedUsages, flagger.varUsages) { + t.Errorf("it should setup flag usages to be %v, got %v", + expectedUsages, flagger.varUsages) + } + + if !reflect.DeepEqual(expectedStringValues, flagger.varStringValues) { + t.Errorf("it should setup string values to be %v, got %v", + expectedStringValues, flagger.varStringValues) + } + + if !reflect.DeepEqual(expectedIntValues, flagger.varIntValues) { + t.Errorf("it should setup int values to be %v, got %v", + expectedIntValues, flagger.varIntValues) + } + +}