Trying to isolate a race condition in tests

This commit is contained in:
Neale Pickett 2021-10-20 11:29:55 -06:00
parent d51e4c2504
commit e349a18861
7 changed files with 68 additions and 5 deletions

View File

@ -10,7 +10,7 @@ test:
- main - main
- merge_requests - merge_requests
script: script:
- go test ./... - go test -race ./...
push: push:
stage: push stage: push

View File

@ -4,10 +4,12 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil"
"log"
"net/http/httptest" "net/http/httptest"
"net/url" "net/url"
"strings"
"testing" "testing"
"time"
"github.com/spf13/afero" "github.com/spf13/afero"
) )
@ -33,7 +35,8 @@ func (hs *HTTPServer) TestRequest(path string, args map[string]string) *httptest
} }
func TestHttpd(t *testing.T) { func TestHttpd(t *testing.T) {
hs := NewHTTPServer("/", NewTestServer()) server := NewTestServer()
hs := NewHTTPServer("/", server)
if r := hs.TestRequest("/", nil); r.Result().StatusCode != 200 { if r := hs.TestRequest("/", nil); r.Result().StatusCode != 200 {
t.Error(r.Result()) t.Error(r.Result())
@ -106,11 +109,26 @@ func TestHttpd(t *testing.T) {
if r := hs.TestRequest("/answer", map[string]string{"cat": "pategory", "points": "1", "answer": "answer123"}); r.Result().StatusCode != 200 { if r := hs.TestRequest("/answer", map[string]string{"cat": "pategory", "points": "1", "answer": "answer123"}); r.Result().StatusCode != 200 {
t.Error(r.Result()) t.Error(r.Result())
} else if strings.Contains(r.Body.String(), "incorrect answer") {
// Pernicious intermittent bug
t.Error("Incorrect answer that was actually correct")
for _, provider := range server.PuzzleProviders {
if mb, ok := provider.(*Mothballs); !ok {
t.Error("Provider is not a mothball")
} else {
cat, _ := mb.getCat("pategory")
f, _ := cat.Open("answers.txt")
defer f.Close()
answersBytes, _ := ioutil.ReadAll(f)
t.Errorf("Correct answers: %v", string(answersBytes))
}
}
t.Error("Wrong answer")
} else if r.Body.String() != `{"status":"success","data":{"short":"accepted","description":"1 points awarded in pategory"}}` { } else if r.Body.String() != `{"status":"success","data":{"short":"accepted","description":"1 points awarded in pategory"}}` {
t.Error("Unexpected body", r.Body.String()) t.Error("Unexpected body", r.Body.String())
} }
time.Sleep(TestMaintenanceInterval) server.State.refresh()
if r := hs.TestRequest("/content/pategory/2/puzzle.json", nil); r.Result().StatusCode != 200 { if r := hs.TestRequest("/content/pategory/2/puzzle.json", nil); r.Result().StatusCode != 200 {
t.Error(r.Result()) t.Error(r.Result())
@ -122,13 +140,37 @@ func TestHttpd(t *testing.T) {
} else if err := json.Unmarshal(r.Body.Bytes(), &state); err != nil { } else if err := json.Unmarshal(r.Body.Bytes(), &state); err != nil {
t.Error(err) t.Error(err)
} else if len(state.PointsLog) != 1 { } else if len(state.PointsLog) != 1 {
t.Error("Points log wrong length") switch v := server.State.(type) {
case *State:
log.Print(v)
}
t.Errorf("Points log wrong length. Wanted 1, got %v", state.PointsLog)
} else if len(state.Puzzles["pategory"]) != 2 { } else if len(state.Puzzles["pategory"]) != 2 {
t.Error("Didn't unlock next puzzle") t.Error("Didn't unlock next puzzle")
} }
if r := hs.TestRequest("/answer", map[string]string{"cat": "pategory", "points": "1", "answer": "answer123"}); r.Result().StatusCode != 200 { if r := hs.TestRequest("/answer", map[string]string{"cat": "pategory", "points": "1", "answer": "answer123"}); r.Result().StatusCode != 200 {
t.Error(r.Result()) t.Error(r.Result())
} else if strings.Contains(r.Body.String(), "incorrect answer") {
// Pernicious intermittent bug
t.Error("Incorrect answer that was actually correct")
for _, provider := range server.PuzzleProviders {
if mb, ok := provider.(*Mothballs); !ok {
t.Error("Provider is not a mothball")
} else {
if cat, ok := mb.getCat("pategory"); !ok {
t.Error("opening pategory failed")
} else if f, err := cat.Open("answers.txt"); err != nil {
t.Error("opening answers.txt", err)
} else {
defer f.Close()
answersBytes, _ := ioutil.ReadAll(f)
t.Errorf("Correct answers: %#v len %d", string(answersBytes), len(answersBytes))
}
}
}
t.Error("Wrong answer")
} else if r.Body.String() != `{"status":"fail","data":{"short":"not accepted","description":"error awarding points: points already awarded to this team in this category"}}` { } else if r.Body.String() != `{"status":"fail","data":{"short":"not accepted","description":"error awarding points: points already awarded to this team in this category"}}` {
t.Error("Unexpected body", r.Body.String()) t.Error("Unexpected body", r.Body.String())
} }

View File

@ -92,23 +92,30 @@ func (m *Mothballs) Inventory() []Category {
func (m *Mothballs) CheckAnswer(cat string, points int, answer string) (bool, error) { func (m *Mothballs) CheckAnswer(cat string, points int, answer string) (bool, error) {
zfs, ok := m.getCat(cat) zfs, ok := m.getCat(cat)
if !ok { if !ok {
log.Println("There's no such category")
return false, fmt.Errorf("no such category: %s", cat) return false, fmt.Errorf("no such category: %s", cat)
} }
log.Println("Opening answers.txt")
af, err := zfs.Open("answers.txt") af, err := zfs.Open("answers.txt")
if err != nil { if err != nil {
log.Println("I did not find an answer")
return false, fmt.Errorf("no answers.txt file") return false, fmt.Errorf("no answers.txt file")
} }
defer af.Close() defer af.Close()
log.Println("I'm going to start looking for an answer")
needle := fmt.Sprintf("%d %s", points, answer) needle := fmt.Sprintf("%d %s", points, answer)
scanner := bufio.NewScanner(af) scanner := bufio.NewScanner(af)
for scanner.Scan() { for scanner.Scan() {
log.Println("testing equality between", scanner.Text(), needle)
if scanner.Text() == needle { if scanner.Text() == needle {
return true, nil return true, nil
} }
} }
log.Println("I did not find the answer", answer)
return false, nil return false, nil
} }

View File

@ -68,6 +68,9 @@ type Maintainer interface {
// It will only be called once, when execution begins. // It will only be called once, when execution begins.
// It's okay to just exit if there's no maintenance to be done. // It's okay to just exit if there's no maintenance to be done.
Maintain(updateInterval time.Duration) Maintain(updateInterval time.Duration)
// refresh is a shortcut used internally for testing
refresh()
} }
// MothServer gathers together the providers that make up a MOTH server. // MothServer gathers together the providers that make up a MOTH server.

View File

@ -11,6 +11,9 @@ import (
const TestMaintenanceInterval = time.Millisecond * 1 const TestMaintenanceInterval = time.Millisecond * 1
const TestTeamID = "teamID" const TestTeamID = "teamID"
// NewTestServer creates a new MothServer with NewTestMothballs and some initial state.
//
// See function definition for details.
func NewTestServer() *MothServer { func NewTestServer() *MothServer {
puzzles := NewTestMothballs() puzzles := NewTestMothballs()
go puzzles.Maintain(TestMaintenanceInterval) go puzzles.Maintain(TestMaintenanceInterval)

View File

@ -38,3 +38,7 @@ func (t *Theme) Open(name string) (ReadSeekCloser, time.Time, error) {
func (t *Theme) Maintain(i time.Duration) { func (t *Theme) Maintain(i time.Duration) {
// No periodic tasks for a theme // No periodic tasks for a theme
} }
func (t *Theme) refresh() {
// Nothing to do for a theme
}

View File

@ -79,3 +79,7 @@ func (p TranspilerProvider) Mothball(cat string, w io.Writer) error {
func (p TranspilerProvider) Maintain(updateInterval time.Duration) { func (p TranspilerProvider) Maintain(updateInterval time.Duration) {
// Nothing to do here. // Nothing to do here.
} }
func (p TranspilerProvider) refresh() {
// Nothing to do for a theme
}