From e43dd79fc78e57cf4b384f24569f66163f1c72d9 Mon Sep 17 00:00:00 2001 From: aniekanessien Date: Fri, 20 Jun 2025 14:39:58 -0400 Subject: [PATCH] feat: add obinna quiz implementation --- students/obinna/go.mod | 5 + students/obinna/main.go | 173 +++++++++++++ students/obinna/main_test.go | 243 ++++++++++++++++++ students/obinna/problems.csv | 12 + students/obinna/testdata/emptytestdata.csv | 0 students/obinna/testdata/testdata.csv | 3 + .../obinna/testdata/testmalformeddata.csv | 3 + 7 files changed, 439 insertions(+) create mode 100644 students/obinna/go.mod create mode 100644 students/obinna/main.go create mode 100644 students/obinna/main_test.go create mode 100644 students/obinna/problems.csv create mode 100644 students/obinna/testdata/emptytestdata.csv create mode 100644 students/obinna/testdata/testdata.csv create mode 100644 students/obinna/testdata/testmalformeddata.csv diff --git a/students/obinna/go.mod b/students/obinna/go.mod new file mode 100644 index 0000000..6427f40 --- /dev/null +++ b/students/obinna/go.mod @@ -0,0 +1,5 @@ +module gophersizeEx1 + +go 1.24.3 + +require github.com/google/go-cmp v0.7.0 diff --git a/students/obinna/main.go b/students/obinna/main.go new file mode 100644 index 0000000..85fa3be --- /dev/null +++ b/students/obinna/main.go @@ -0,0 +1,173 @@ +package main + +import ( + "bufio" + "context" + "encoding/csv" + "flag" + "fmt" + "io" + "log" + "os" + "path/filepath" + "strings" + "time" +) + +func main() { + var file string // flag -f for file + flag.StringVar(&file, "f", "problems.csv", "file location of quiz") + + var timeLimit int + flag.IntVar(&timeLimit, "t", 30, "time limit for the quiz") + + //parse the flag + flag.Parse() + + // validate correct file is used + if err := validatePath(file); err != nil { + log.Fatal(err) + } + + problems := NewProblemSheet() + err := problems.populateSheet(file) + if err != nil { + log.Fatal(err) + } + + problems.StartQuiz(time.Duration(timeLimit)*time.Second, os.Stdin, os.Stdout) + problems.DisplayResults(os.Stdout) +} + +func validatePath(path string) error { + // validate extension is correct + if filepath.Ext(path) != ".csv" { + return fmt.Errorf("malformed file path: %s contains no csv extension", path) + } + + //check that file exists + _, err := os.Stat(path) + if os.IsNotExist(err) { + return fmt.Errorf("path does not exist: %w", err) + } else if err != nil { + return fmt.Errorf("error getting file info: %w", err) + } + + return nil +} + +type Problem struct { + Question string + Answer string +} + +type ProblemSheet struct { + Questions []Problem + CorrectlyAnswered int +} + +func (p *ProblemSheet) populateSheet(file string) error { + //open file + f, err := os.Open(file) + if err != nil { + return fmt.Errorf("failed to open file: %w", err) + } + defer f.Close() + + reader := csv.NewReader(f) + lines, err := reader.ReadAll() + if err != nil { + return fmt.Errorf("failed to read csv: %w", err) + } + + if len(lines) == 0 { + return fmt.Errorf("empty csv file loaded") + } + + for _, line := range lines { + + newProblem := Problem{ + Question: strings.TrimSpace(line[0]), + Answer: strings.TrimSpace(line[1]), + } + + p.Questions = append(p.Questions, newProblem) + } + return nil +} + +func NewProblemSheet() *ProblemSheet { + return &ProblemSheet{} +} + +func (p *ProblemSheet) StartQuiz(limit time.Duration, r io.Reader, w io.Writer) { + + reader := bufio.NewReader(r) + + fmt.Fprintln(w, "Press Enter to begin quiz") + reader.ReadString('\n') + + ctx, cancel := context.WithTimeout(context.Background(), limit) + defer cancel() + + for _, problem := range p.Questions { + answerCh := make(chan string, 1) + + go func(q string) { + fmt.Fprintf(w, "\n\nQuestion: %s\n", q) + fmt.Fprint(w, "answer: ") + userAns, _ := reader.ReadString('\n') + answerCh <- strings.TrimSpace(userAns) + }(problem.Question) + + select { + case userAnswer := <-answerCh: + if userAnswer == problem.Answer { + p.CorrectlyAnswered++ + } + case <-ctx.Done(): + fmt.Fprintln(w, "\nquiz not completed before timeout") + return + } + } +} + +func (p *ProblemSheet) DisplayResults(w io.Writer) { + fmt.Fprintln(w, "Quiz has ended") + fmt.Fprintf(w, "You answered %d/%d questions\n", p.CorrectlyAnswered, len(p.Questions)) +} + +//type workerFunc func() string +// +//func timeLimit(worker workerFunc, limit time.Duration) (string, error) { +// out := make(chan string, 1) +// ctx, cancel := context.WithTimeout(context.Background(), limit) +// defer cancel() +// +// go func() { +// fmt.Println("goroutine started") +// out <- worker() +// }() +// +// select { +// case result := <-out: +// return result, nil +// case <-ctx.Done(): +// return "quiz not completed before timeout", errors.New("work timed out") +// } +//} + +//func (p *problemSheet) StartQuiz() string { +// for _, problem := range p.questions { +// fmt.Printf("\n\nQuestion: %s\n", problem.question) +// fmt.Print("answer: ") +// var answer string +// fmt.Scanln(&answer) +// +// if answer == problem.answer { +// p.correctlyAnswered++ +// } +// } +// +// return "Quiz completed before timeout" +//} diff --git a/students/obinna/main_test.go b/students/obinna/main_test.go new file mode 100644 index 0000000..dc4eadb --- /dev/null +++ b/students/obinna/main_test.go @@ -0,0 +1,243 @@ +package main + +import ( + "fmt" + "github.com/google/go-cmp/cmp" + "os" + "strings" + "testing" + "time" +) + +func TestValidatePath(t *testing.T) { + // Setup a valid temp .csv file + validFile, err := os.CreateTemp("", "*.csv") + if err != nil { + t.Fatalf("could not create temp file: %v", err) + } + defer os.Remove(validFile.Name()) + + tests := []struct { + name string + path string + expectError bool + expectedErr string // optional: substring or exact message + }{ + { + name: "valid csv file", + path: validFile.Name(), + expectError: false, + }, + { + name: "invalid file extension", + path: "bad.txt", + expectError: true, + expectedErr: "malformed file path: bad.txt contains no csv extension", + }, + { + name: "non-existent file", + path: "does-not-exist.csv", + expectError: true, + expectedErr: "path does not exist", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + err := validatePath(tc.path) + + if tc.expectError { + fmt.Println("actual error:", err.Error()) + fmt.Println("expected substring:", tc.expectedErr) + if err == nil { + t.Fatalf("expected error, got nil") + } + // if error does not contain below text this is triggered + if tc.expectedErr != "" && !strings.Contains(err.Error(), tc.expectedErr) { + t.Errorf("expected error containing %q, got: %v", tc.expectedErr, err) + } + } else { + if err != nil { + t.Errorf("unexpected error: %v", err) + } + } + }) + } +} + +func TestPopulateSheet(t *testing.T) { + + tests := []struct { + name string + path string + expectError bool + expectedError string + expectedResult *ProblemSheet + }{ + { + name: "populateSheet with well formed csv", + path: "testdata/testdata.csv", + expectError: false, + expectedResult: validProblemSheet(), + }, + { + name: "populateSheet with malformed csv", + path: "testdata/testmalformeddata.csv", + expectError: true, + expectedError: "wrong number of fields", + }, + { + name: "empty CSV file", + path: "testdata/emptytestdata.csv", + expectError: true, + expectedError: "empty csv file loaded", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + path := tc.path + sheet := NewProblemSheet() + err := sheet.populateSheet(path) + + if tc.expectError { + + if err == nil { + t.Fatalf("expected error but got nil") + } + + //run if this if does not contain right error + if !strings.Contains(err.Error(), tc.expectedError) { + t.Errorf("expected error containing %q, got: %v", tc.expectedError, err) + } + + } else { + if diff := cmp.Diff(tc.expectedResult, sheet); diff != "" { + t.Errorf("populateSheet() mismatch (-want +got):\n%s", diff) + } + } + + }) + } + +} + +func validProblemSheet() *ProblemSheet { + return &ProblemSheet{ + Questions: []Problem{ + {Question: "2+2", Answer: "4"}, + {Question: "5+5", Answer: "10"}, + {Question: "3+1", Answer: "4"}, + }, + } +} + +func TestStartQuiz(t *testing.T) { + + tests := []struct { + name string + answers string + timelimit time.Duration + expectTimeout bool + expectedResult int + expectedOutput string + }{ + { + name: "test all correct answers", + answers: "\n4\n10\n4", + timelimit: 10 * time.Second, + expectTimeout: false, + expectedResult: 3, + }, + { + name: "test one wrong answer", + answers: "\n4\n5\n4", + timelimit: 10 * time.Second, + expectTimeout: false, + expectedResult: 2, + }, + { + name: "test timeout feature", + answers: "4\n10\n4", + expectedResult: 0, + timelimit: 10 * time.Nanosecond, // set to just 10 to force timeout + expectTimeout: true, + expectedOutput: "quiz not completed before timeout", + }, + } + + for _, tc := range tests { + + t.Run(tc.name, func(t *testing.T) { + sheet := validProblemSheet() + + input := strings.NewReader(tc.answers) + var output strings.Builder + + sheet.StartQuiz(tc.timelimit, input, &output) + + if tc.expectTimeout { + if !strings.Contains(output.String(), tc.expectedOutput) { + t.Errorf("expected timeout message, got: %s", output.String()) + } + } + + if tc.expectedResult != sheet.CorrectlyAnswered { + t.Errorf("expected %d correct answers, got %d", tc.expectedResult, sheet.CorrectlyAnswered) + } + + }) + } +} + +func TestDisplayResults(t *testing.T) { + tests := []struct { + name string + sheet *ProblemSheet + expectedOutput string + }{ + { + name: "all correct", + sheet: &ProblemSheet{ + Questions: []Problem{ + {Question: "2+2", Answer: "4"}, + {Question: "5+5", Answer: "10"}, + }, + CorrectlyAnswered: 2, + }, + expectedOutput: "Quiz has ended\nYou answered 2/2 questions\n", + }, + { + name: "some correct", + sheet: &ProblemSheet{ + Questions: []Problem{ + {Question: "2+2", Answer: "4"}, + {Question: "5+5", Answer: "10"}, + }, + CorrectlyAnswered: 1, + }, + expectedOutput: "Quiz has ended\nYou answered 1/2 questions\n", + }, + { + name: "no questions", + sheet: &ProblemSheet{ + Questions: []Problem{}, + CorrectlyAnswered: 0, + }, + expectedOutput: "Quiz has ended\nYou answered 0/0 questions\n", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var output strings.Builder + + tc.sheet.DisplayResults(&output) + + actual := output.String() + if actual != tc.expectedOutput { + t.Errorf("unexpected output:\nGot:\n%q\nWant:\n%q", actual, tc.expectedOutput) + } + }) + } +} diff --git a/students/obinna/problems.csv b/students/obinna/problems.csv new file mode 100644 index 0000000..11506cb --- /dev/null +++ b/students/obinna/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/obinna/testdata/emptytestdata.csv b/students/obinna/testdata/emptytestdata.csv new file mode 100644 index 0000000..e69de29 diff --git a/students/obinna/testdata/testdata.csv b/students/obinna/testdata/testdata.csv new file mode 100644 index 0000000..3a48357 --- /dev/null +++ b/students/obinna/testdata/testdata.csv @@ -0,0 +1,3 @@ +2+2,4 +5+5,10 +3+1,4 \ No newline at end of file diff --git a/students/obinna/testdata/testmalformeddata.csv b/students/obinna/testdata/testmalformeddata.csv new file mode 100644 index 0000000..e158382 --- /dev/null +++ b/students/obinna/testdata/testmalformeddata.csv @@ -0,0 +1,3 @@ +2+2,4 +5+5,10 +3+1,4,extra \ No newline at end of file