Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update test runner to Go 1.22 #113

Merged
merged 3 commits into from
Jul 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ jobs:
- name: Install Go
uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9
with:
go-version: 1.21.x
go-version: 1.22.x
- name: Checkout code
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b
- name: Run linters
uses: golangci/golangci-lint-action@3a919529898de77ec3da873e3063ca4b10e7f5cc
uses: golangci/golangci-lint-action@a4f60bb28d35aeee14e6880718e0c85ff1882e64
with:
version: v1.51
version: v1.59.1

test:
strategy:
Expand All @@ -27,7 +27,7 @@ jobs:
if: success()
uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9
with:
go-version: 1.21.x
go-version: 1.22.x
- name: Checkout code
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b
- name: Run tests
Expand Down Expand Up @@ -64,7 +64,7 @@ jobs:
if: success()
uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9
with:
go-version: 1.21.x
go-version: 1.22.x
- name: Checkout code
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b
- name: Calc coverage
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM golang:1.21.1-alpine3.17
FROM golang:1.22.5-alpine3.20

# Add addtional packages needed for the race detector to work
RUN apk add --update build-base make
Expand Down
2 changes: 2 additions & 0 deletions external-packages/go.mod
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
module external_packages

// This version should be the same as the default version of go
// in the go.mod files given to the students
go 1.18

require (
Expand Down
5 changes: 3 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
module github.com/exercism/go-test-runner

go 1.19
go 1.22

require github.com/stretchr/testify v1.9.0

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/testify v1.8.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
12 changes: 3 additions & 9 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
163 changes: 112 additions & 51 deletions testrunner/execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"bufio"
"bytes"
"encoding/json"
"errors"
"fmt"
"log"
"os"
Expand Down Expand Up @@ -54,15 +53,16 @@ func Execute(input_dir string) []byte {
ver := 3

exerciseConfig := parseExerciseConfig(input_dir)
cmdres, testsOk := runTests(input_dir, exerciseConfig.TestingFlags)
testOutput, err := parseTestOutput(cmdres)
if err != nil {
log.Fatalf("parsing test output: %s", err)
}

if cmdres, ok := runTests(input_dir, exerciseConfig.TestingFlags); ok {
report = getStructure(cmdres, input_dir, ver, exerciseConfig.TaskIDsEnabled)
if testsOk {
report = getStructureForTestsOk(testOutput, input_dir, ver, exerciseConfig.TaskIDsEnabled)
} else {
report = &testReport{
Status: statErr,
Version: ver,
Message: cmdres.String(),
}
report = getStructureForTestsNotOk(testOutput, ver)
}

bts, err := json.MarshalIndent(report, "", "\t")
Expand All @@ -72,7 +72,31 @@ func Execute(input_dir string) []byte {
return bts
}

func getStructure(lines bytes.Buffer, input_dir string, ver int, taskIDsEnabled bool) *testReport {
func getStructureForTestsNotOk(parsedOutput *parsedTestOutput, ver int) *testReport {
report := &testReport{
Status: statErr,
Version: ver,
Message: parsedOutput.joinPackageMessages("\n"),
}

report.Message += parsedOutput.joinFailMessages("\n")

jsonOutputMessages := make([]string, 0)

for _, line := range parsedOutput.testLines {
if line.Action == "output" {
jsonOutputMessages = append(jsonOutputMessages, line.Output)
}
}

if len(jsonOutputMessages) > 0 {
report.Message += "\n" + strings.Join(jsonOutputMessages, "\n")
}

return report
}

func getStructureForTestsOk(parsedOutput *parsedTestOutput, input_dir string, ver int, taskIDsEnabled bool) *testReport {
report := &testReport{
Status: statPass,
Version: ver,
Expand All @@ -84,10 +108,17 @@ func getStructure(lines bytes.Buffer, input_dir string, ver int, taskIDsEnabled
}
}()

tests, err := processTestResults(lines, input_dir, taskIDsEnabled)
if err != nil {
tests := processTestResults(parsedOutput, input_dir, taskIDsEnabled)

if parsedOutput.hasFailMessages() {
report.Status = statErr
report.Message = parsedOutput.joinFailMessages("\n")
return report
}

if len(tests) == 0 && parsedOutput.hasPackageMessages() {
report.Status = statErr
report.Message = err.Error()
report.Message = parsedOutput.joinPackageMessages("")
return report
}

Expand Down Expand Up @@ -117,49 +148,88 @@ func getStructure(lines bytes.Buffer, input_dir string, ver int, taskIDsEnabled
return report
}

func processTestResults(lines bytes.Buffer, input_dir string, taskIDsEnabled bool) ([]testResult, error) {
var (
results = []testResult{}
resultIdxByName = make(map[string]int)
failMsg [][]byte
pkgLevelMsg string
)
type parsedTestOutput struct {
testLines []testLine
pkgLevelMessages []string
failMessages []string
}

testFile := FindTestFile(input_dir)
rootLevelTests := FindAllRootLevelTests(testFile)
rootLevelTestsMap := ConvertToMapByTestName(rootLevelTests)
func (out *parsedTestOutput) hasFailMessages() bool {
return len(out.failMessages) > 0
}

func (out *parsedTestOutput) joinFailMessages(sep string) string {
return strings.Join(out.failMessages, sep)
}

func (out *parsedTestOutput) hasPackageMessages() bool {
return len(out.pkgLevelMessages) > 0
}

func (out *parsedTestOutput) joinPackageMessages(sep string) string {
return strings.Join(out.pkgLevelMessages, sep)
}

func parseTestOutput(lines bytes.Buffer) (*parsedTestOutput, error) {
parsedOutput := &parsedTestOutput{
testLines: make([]testLine, 0),
pkgLevelMessages: make([]string, 0),
failMessages: make([]string, 0),
}

scanner := bufio.NewScanner(&lines)
for scanner.Scan() {
lineBytes := scanner.Bytes()
var line testLine

switch {
case len(lineBytes) == 0:
if len(lineBytes) == 0 {
continue
case !bytes.HasPrefix(lineBytes, []byte{'{'}):
}

if !bytes.HasPrefix(lineBytes, []byte{'{'}) {
// if the line is not a json, we need to collect the lines to gather why `go test --json` failed
failMsg = append(failMsg, lineBytes)
parsedOutput.failMessages = append(parsedOutput.failMessages, string(lineBytes))
continue
}

if err := json.Unmarshal(lineBytes, &line); err != nil {
log.Println(err)
continue
return nil, fmt.Errorf("parsing line starting with '{' as json: %w", err)
}

if line.Test == "" {
// We collect messages that do not belong to an individual test and use them later
// as error message in case there was no test level message found at all.
pkgLevelMsg += line.Output
if line.Output != "" {
parsedOutput.pkgLevelMessages = append(parsedOutput.pkgLevelMessages, line.Output)
}
continue
}

switch line.Action {
parsedOutput.testLines = append(parsedOutput.testLines, line)
}

return parsedOutput, nil
}

func processTestResults(
parsedOutput *parsedTestOutput,
input_dir string,
taskIDsEnabled bool,
) []testResult {

results := make([]testResult, 0)
resultIdxByName := make(map[string]int)

testFile := FindTestFile(input_dir)
rootLevelTests := FindAllRootLevelTests(testFile)
rootLevelTestsMap := ConvertToMapByTestName(rootLevelTests)

for _, parsedLine := range parsedOutput.testLines {
switch parsedLine.Action {
case "run":
tc, taskID := ExtractTestCodeAndTaskID(rootLevelTestsMap, line.Test)
tc, taskID := ExtractTestCodeAndTaskID(rootLevelTestsMap, parsedLine.Test)
result := testResult{
Name: line.Test,
Name: parsedLine.Test,
// Use error as default state in case no other state is found later.
// No state is provided e.g. when there is a stack overflow.
Status: statErr,
Expand All @@ -172,43 +242,34 @@ func processTestResults(lines bytes.Buffer, input_dir string, taskIDsEnabled boo
results = append(results, result)
resultIdxByName[result.Name] = len(results) - 1
case "output":
if idx, found := resultIdxByName[line.Test]; found {
results[idx].Message += "\n" + line.Output
if idx, found := resultIdxByName[parsedLine.Test]; found {
results[idx].Message += "\n" + parsedLine.Output
} else {
log.Printf("cannot extend message for unknown test: %s\n", line.Test)
log.Printf("cannot extend message for unknown test: %s\n", parsedLine.Test)
continue
}
case statFail:
if idx, found := resultIdxByName[line.Test]; found {
if idx, found := resultIdxByName[parsedLine.Test]; found {
results[idx].Status = statFail
} else {
log.Printf("cannot set failed status for unknown test: %s\n", line.Test)
log.Printf("cannot set failed status for unknown test: %s\n", parsedLine.Test)
continue
}
case statPass:
if idx, found := resultIdxByName[line.Test]; found {
if idx, found := resultIdxByName[parsedLine.Test]; found {
results[idx].Status = statPass
} else {
log.Printf("cannot set passing status for unknown test: %s\n", line.Test)
log.Printf("cannot set passing status for unknown test: %s\n", parsedLine.Test)
continue
}
case statSkip:
if idx, found := resultIdxByName[line.Test]; found {
if idx, found := resultIdxByName[parsedLine.Test]; found {
results[idx].Status = statSkip
} else {
log.Printf("cannot set skipped status for unknown test: %s\n", line.Test)
log.Printf("cannot set skipped status for unknown test: %s\n", parsedLine.Test)
continue
}
}

}

if len(failMsg) != 0 {
return nil, errors.New(string(bytes.Join(failMsg, []byte{'\n'})))
}

if len(results) == 0 && pkgLevelMsg != "" {
return nil, errors.New(pkgLevelMsg)
}

if taskIDsEnabled {
Expand All @@ -217,7 +278,7 @@ func processTestResults(lines bytes.Buffer, input_dir string, taskIDsEnabled boo
results = addNonExecutedTests(rootLevelTests, results)
}

return results, nil
return results
}

// addNonExecutedTests adds tests to the result set that were not executed.
Expand Down
28 changes: 20 additions & 8 deletions testrunner/execute_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,25 @@ const version = 3
/*
Ideally, tests should be performed via running the tool and checking the resulting json
file matches the expected output. This is done in integration_test.go in the project root.
Tests should only be added in this file is comparing the full output is not possible for some reason.
Tests should only be added in this file if comparing the full output is not possible for some reason.
*/

func TestRunTests_RuntimeError(t *testing.T) {
input_dir := filepath.Join("testdata", "practice", "runtime_error")

cmdres, ok := runTests(input_dir, nil)
if !ok {
fmt.Printf("runtime error test expected to return ok: %s", cmdres.String())
}

output := getStructure(cmdres, input_dir, version, false)
jsonBytes, err := json.MarshalIndent(output, "", "\t")
testOutput, err := parseTestOutput(cmdres)
if err != nil {
t.Fatalf("parsing test output: %s", err)
}

report := getStructureForTestsOk(testOutput, input_dir, version, false)

jsonBytes, err := json.MarshalIndent(report, "", "\t")
if err != nil {
t.Fatalf("runtime error output not valid json: %s", err)
}
Expand Down Expand Up @@ -56,13 +63,18 @@ func TestRunTests_RaceDetector(t *testing.T) {
fmt.Printf("race detector test expected to return ok: %s", cmdres.String())
}

output := getStructure(cmdres, input_dir, version, false)
if output.Status != "fail" {
t.Errorf("wrong status for race detector test: got %q, want %q", output.Status, "fail")
testOutput, err := parseTestOutput(cmdres)
if err != nil {
t.Errorf("parsing test output: %s", err)
}

report := getStructureForTestsOk(testOutput, input_dir, version, false)
if report.Status != "fail" {
t.Errorf("wrong status for race detector test: got %q, want %q", report.Status, "fail")
}

if !strings.Contains(output.Tests[0].Message, "WARNING: DATA RACE") {
t.Errorf("no data race error included in message: %s", output.Tests[0].Message)
if !strings.Contains(report.Tests[0].Message, "WARNING: DATA RACE") {
t.Errorf("no data race error included in message: %s", report.Tests[0].Message)
}
}

Expand Down
Loading
Loading