diff --git a/exercises/practice/bowling/.meta/config.json b/exercises/practice/bowling/.meta/config.json index 8dd20868f..2e3b6ba31 100644 --- a/exercises/practice/bowling/.meta/config.json +++ b/exercises/practice/bowling/.meta/config.json @@ -10,7 +10,8 @@ "ferhatelmas", "hilary", "robphoenix", - "sebito91" + "sebito91", + "eklatzer" ], "files": { "solution": [ diff --git a/exercises/practice/bowling/.meta/gen.go b/exercises/practice/bowling/.meta/gen.go index 0adf778e2..5571cca69 100644 --- a/exercises/practice/bowling/.meta/gen.go +++ b/exercises/practice/bowling/.meta/gen.go @@ -12,70 +12,50 @@ func main() { if err != nil { log.Fatal(err) } - var j js - if err := gen.Gen("bowling", &j, t); err != nil { + var j = map[string]interface{}{ + "roll": &[]Case{}, + "score": &[]Case{}, + } + if err := gen.Gen("bowling", j, t); err != nil { log.Fatal(err) } } -// The JSON structure we expect to be able to unmarshal into -type js struct { - Exercise string - Version string - Comments []string - Cases []OneCase -} - -// template applied to above data structure generates the Go test cases - -type OneCase struct { - Description string - Property string +type Case struct { + Description string `json:"description"` Input struct { - PreviousRolls []int - Roll int - } - Expected interface{} -} - -// ScoreTest and RollTest help determine which type of test case -// to generate in the template. -func (c OneCase) ScoreTest() bool { return c.Property == "score" } -func (c OneCase) RollTest() bool { return c.Property == "roll" } - -func (c OneCase) Valid() bool { - valid, _, _ := determineExpected(c.Expected) - return valid + PreviousRolls []int `json:"previousRolls"` + Roll int `json:"roll"` + } `json:"input"` + Expected interface{} `json:"expected"` } -func (c OneCase) Score() int { - _, score, _ := determineExpected(c.Expected) - return score +func (t Case) Score() int { + score, ok := t.Expected.(float64) + if !ok { + return 0 + } + return int(score) } -func (c OneCase) ExplainText() string { - _, _, explainText := determineExpected(c.Expected) - return explainText +func (t Case) Valid() bool { + _, ok := t.Expected.(float64) + return ok } -// determineExpected examines an .Expected interface{} object and determines -// whether a test case is valid(bool), has a score field, and/or has an expected error, -// returning valid, score, and error explanation text. -func determineExpected(expected interface{}) (bool, int, string) { - score, ok := expected.(float64) - if ok { - return ok, int(score), "" - } - m, ok := expected.(map[string]interface{}) - if !ok { - return false, 0, "" +func (t Case) ExplainText() string { + if !t.Valid() { + m, ok := t.Expected.(map[string]interface{}) + if !ok { + return "" + } + b, ok := m["error"].(string) + if !ok { + return "" + } + return b } - iError, ok := m["error"].(interface{}) - if !ok { - return false, 0, "" - } - explainText, ok := iError.(string) - return false, 0, explainText + return "" } // Template to generate two sets of test cases, one for Score tests and one for Roll tests. @@ -89,29 +69,30 @@ var scoreTestCases = []struct { valid bool // true => no error, false => error expected score int // when .valid == true, the expected score value explainText string // when .valid == false, error explanation text -}{ {{range .J.Cases}} -{{if .ScoreTest}}{ - {{printf "%q" .Description}}, - {{printf "%#v" .Input.PreviousRolls}}, - {{printf "%v" .Valid}}, - {{printf "%d" .Score}}, - {{printf "%q" .ExplainText}}, -},{{- end}}{{end}} +}{ {{range .J.score}} +{ + description: {{printf "%q" .Description}}, + previousRolls: {{printf "%#v" .Input.PreviousRolls}}, + valid: {{printf "%v" .Valid}}, + score: {{printf "%d" .Score}}, + explainText: {{printf "%q" .ExplainText}}, +},{{end}} } + var rollTestCases = []struct { description string previousRolls []int // bowling rolls to do before the Roll(roll) test valid bool // true => no error, false => error expected roll int // pin count for the test roll explainText string // when .valid == false, error explanation text -}{ {{range .J.Cases}} -{{if .RollTest}}{ - {{printf "%q" .Description}}, - {{printf "%#v" .Input.PreviousRolls}}, - {{printf "%v" .Valid}}, - {{printf "%d" .Input.Roll}}, - {{printf "%q" .ExplainText}}, -},{{- end}}{{end}} +}{ {{range .J.roll}} +{ + description: {{printf "%q" .Description}}, + previousRolls: {{printf "%#v" .Input.PreviousRolls}}, + valid: {{printf "%v" .Valid}}, + roll: {{printf "%d" .Input.Roll}}, + explainText: {{printf "%q" .ExplainText}}, +},{{end}} } ` diff --git a/exercises/practice/bowling/cases_test.go b/exercises/practice/bowling/cases_test.go index 321620eb9..a318a35a1 100644 --- a/exercises/practice/bowling/cases_test.go +++ b/exercises/practice/bowling/cases_test.go @@ -1,8 +1,7 @@ package bowling // Source: exercism/problem-specifications -// Commit: 1806718 bowling: add tests for rolling after bonus rolls -// Problem Specifications Version: 1.2.0 +// Commit: daf84d6 bowling, transpose: conform array format to rest of file var scoreTestCases = []struct { description string @@ -12,147 +11,151 @@ var scoreTestCases = []struct { explainText string // when .valid == false, error explanation text }{ { - "should be able to score a game with all zeros", - []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, - true, - 0, - "", + description: "should be able to score a game with all zeros", + previousRolls: []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + valid: true, + score: 0, + explainText: "", }, { - "should be able to score a game with no strikes or spares", - []int{3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6}, - true, - 90, - "", + description: "should be able to score a game with no strikes or spares", + previousRolls: []int{3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6}, + valid: true, + score: 90, + explainText: "", }, { - "a spare followed by zeros is worth ten points", - []int{6, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, - true, - 10, - "", + description: "a spare followed by zeros is worth ten points", + previousRolls: []int{6, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + valid: true, + score: 10, + explainText: "", }, { - "points scored in the roll after a spare are counted twice", - []int{6, 4, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, - true, - 16, - "", + description: "points scored in the roll after a spare are counted twice", + previousRolls: []int{6, 4, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + valid: true, + score: 16, + explainText: "", }, { - "consecutive spares each get a one roll bonus", - []int{5, 5, 3, 7, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, - true, - 31, - "", + description: "consecutive spares each get a one roll bonus", + previousRolls: []int{5, 5, 3, 7, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + valid: true, + score: 31, + explainText: "", }, { - "a spare in the last frame gets a one roll bonus that is counted once", - []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 3, 7}, - true, - 17, - "", + description: "a spare in the last frame gets a one roll bonus that is counted once", + previousRolls: []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 3, 7}, + valid: true, + score: 17, + explainText: "", }, { - "a strike earns ten points in a frame with a single roll", - []int{10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, - true, - 10, - "", + description: "a strike earns ten points in a frame with a single roll", + previousRolls: []int{10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + valid: true, + score: 10, + explainText: "", }, { - "points scored in the two rolls after a strike are counted twice as a bonus", - []int{10, 5, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, - true, - 26, - "", + description: "points scored in the two rolls after a strike are counted twice as a bonus", + previousRolls: []int{10, 5, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + valid: true, + score: 26, + explainText: "", }, { - "consecutive strikes each get the two roll bonus", - []int{10, 10, 10, 5, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, - true, - 81, - "", + description: "consecutive strikes each get the two roll bonus", + previousRolls: []int{10, 10, 10, 5, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + valid: true, + score: 81, + explainText: "", }, { - "a strike in the last frame gets a two roll bonus that is counted once", - []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 7, 1}, - true, - 18, - "", + description: "a strike in the last frame gets a two roll bonus that is counted once", + previousRolls: []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 7, 1}, + valid: true, + score: 18, + explainText: "", }, { - "rolling a spare with the two roll bonus does not get a bonus roll", - []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 7, 3}, - true, - 20, - "", + description: "rolling a spare with the two roll bonus does not get a bonus roll", + previousRolls: []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 7, 3}, + valid: true, + score: 20, + explainText: "", }, { - "strikes with the two roll bonus do not get bonus rolls", - []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 10, 10}, - true, - 30, - "", + description: "strikes with the two roll bonus do not get bonus rolls", + previousRolls: []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 10, 10}, + valid: true, + score: 30, + explainText: "", }, { - "a strike with the one roll bonus after a spare in the last frame does not get a bonus", - []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 3, 10}, - true, - 20, - "", + description: "last two strikes followed by only last bonus with non strike points", + previousRolls: []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 10, 0, 1}, + valid: true, + score: 31, + explainText: "", }, { - "all strikes is a perfect game", - []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, - true, - 300, - "", + description: "a strike with the one roll bonus after a spare in the last frame does not get a bonus", + previousRolls: []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 3, 10}, + valid: true, + score: 20, + explainText: "", }, - { - "two bonus rolls after a strike in the last frame can score more than 10 points if one is a strike", - []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 10, 6}, - true, - 26, - "", + description: "all strikes is a perfect game", + previousRolls: []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, + valid: true, + score: 300, + explainText: "", }, - { - "an unstarted game cannot be scored", - []int{}, - false, - 0, - "Score cannot be taken until the end of the game", + description: "two bonus rolls after a strike in the last frame can score more than 10 points if one is a strike", + previousRolls: []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 10, 6}, + valid: true, + score: 26, + explainText: "", }, { - "an incomplete game cannot be scored", - []int{0, 0}, - false, - 0, - "Score cannot be taken until the end of the game", + description: "an unstarted game cannot be scored", + previousRolls: []int{}, + valid: false, + score: 0, + explainText: "Score cannot be taken until the end of the game", + }, + { + description: "an incomplete game cannot be scored", + previousRolls: []int{0, 0}, + valid: false, + score: 0, + explainText: "Score cannot be taken until the end of the game", }, - { - "bonus rolls for a strike in the last frame must be rolled before score can be calculated", - []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10}, - false, - 0, - "Score cannot be taken until the end of the game", + description: "bonus rolls for a strike in the last frame must be rolled before score can be calculated", + previousRolls: []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10}, + valid: false, + score: 0, + explainText: "Score cannot be taken until the end of the game", }, { - "both bonus rolls for a strike in the last frame must be rolled before score can be calculated", - []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 10}, - false, - 0, - "Score cannot be taken until the end of the game", + description: "both bonus rolls for a strike in the last frame must be rolled before score can be calculated", + previousRolls: []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 10}, + valid: false, + score: 0, + explainText: "Score cannot be taken until the end of the game", }, { - "bonus roll for a spare in the last frame must be rolled before score can be calculated", - []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 3}, - false, - 0, - "Score cannot be taken until the end of the game", + description: "bonus roll for a spare in the last frame must be rolled before score can be calculated", + previousRolls: []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 3}, + valid: false, + score: 0, + explainText: "Score cannot be taken until the end of the game", }, } @@ -163,78 +166,74 @@ var rollTestCases = []struct { roll int // pin count for the test roll explainText string // when .valid == false, error explanation text }{ - { - "rolls cannot score negative points", - []int{}, - false, - -1, - "Negative roll is invalid", + description: "rolls cannot score negative points", + previousRolls: []int{}, + valid: false, + roll: -1, + explainText: "Negative roll is invalid", }, { - "a roll cannot score more than 10 points", - []int{}, - false, - 11, - "Pin count exceeds pins on the lane", + description: "a roll cannot score more than 10 points", + previousRolls: []int{}, + valid: false, + roll: 11, + explainText: "Pin count exceeds pins on the lane", }, { - "two rolls in a frame cannot score more than 10 points", - []int{5}, - false, - 6, - "Pin count exceeds pins on the lane", + description: "two rolls in a frame cannot score more than 10 points", + previousRolls: []int{5}, + valid: false, + roll: 6, + explainText: "Pin count exceeds pins on the lane", }, { - "bonus roll after a strike in the last frame cannot score more than 10 points", - []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10}, - false, - 11, - "Pin count exceeds pins on the lane", + description: "bonus roll after a strike in the last frame cannot score more than 10 points", + previousRolls: []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10}, + valid: false, + roll: 11, + explainText: "Pin count exceeds pins on the lane", }, { - "two bonus rolls after a strike in the last frame cannot score more than 10 points", - []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 5}, - false, - 6, - "Pin count exceeds pins on the lane", + description: "two bonus rolls after a strike in the last frame cannot score more than 10 points", + previousRolls: []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 5}, + valid: false, + roll: 6, + explainText: "Pin count exceeds pins on the lane", }, - { - "the second bonus rolls after a strike in the last frame cannot be a strike if the first one is not a strike", - []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 6}, - false, - 10, - "Pin count exceeds pins on the lane", + description: "the second bonus rolls after a strike in the last frame cannot be a strike if the first one is not a strike", + previousRolls: []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 6}, + valid: false, + roll: 10, + explainText: "Pin count exceeds pins on the lane", }, { - "second bonus roll after a strike in the last frame cannot score more than 10 points", - []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 10}, - false, - 11, - "Pin count exceeds pins on the lane", + description: "second bonus roll after a strike in the last frame cannot score more than 10 points", + previousRolls: []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 10}, + valid: false, + roll: 11, + explainText: "Pin count exceeds pins on the lane", }, - { - "cannot roll if game already has ten frames", - []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, - false, - 0, - "Cannot roll after game is over", + description: "cannot roll if game already has ten frames", + previousRolls: []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + valid: false, + roll: 0, + explainText: "Cannot roll after game is over", }, - { - "cannot roll after bonus roll for spare", - []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 3, 2}, - false, - 2, - "Cannot roll after game is over", + description: "cannot roll after bonus roll for spare", + previousRolls: []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 3, 2}, + valid: false, + roll: 2, + explainText: "Cannot roll after game is over", }, { - "cannot roll after bonus rolls for strike", - []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 3, 2}, - false, - 2, - "Cannot roll after game is over", + description: "cannot roll after bonus rolls for strike", + previousRolls: []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 3, 2}, + valid: false, + roll: 2, + explainText: "Cannot roll after game is over", }, } diff --git a/exercises/practice/diamond/.meta/config.json b/exercises/practice/diamond/.meta/config.json index 6e17998eb..f7fb1d99c 100644 --- a/exercises/practice/diamond/.meta/config.json +++ b/exercises/practice/diamond/.meta/config.json @@ -12,7 +12,8 @@ "petertseng", "robphoenix", "sebito91", - "tleen" + "tleen", + "eklatzer" ], "files": { "solution": [ diff --git a/exercises/practice/diamond/.meta/gen.go b/exercises/practice/diamond/.meta/gen.go index 8ca9e0eaf..bf0be3817 100644 --- a/exercises/practice/diamond/.meta/gen.go +++ b/exercises/practice/diamond/.meta/gen.go @@ -12,21 +12,20 @@ func main() { if err != nil { log.Fatal(err) } - var j js - if err := gen.Gen("diamond", &j, t); err != nil { + var j = map[string]interface{}{ + "rows": &[]testCase{}, + } + if err := gen.Gen("diamond", j, t); err != nil { log.Fatal(err) } } -// The JSON structure we expect to be able to unmarshal into -type js struct { - Cases []struct { - Description string `json:"description"` - Input struct { - Letter string `json:"letter"` - } `json:"input"` - Expected []string `json:"expected"` - } `json:"cases"` +type testCase struct { + Description string `json:"description"` + Input struct { + Letter string `json:"letter"` + } `json:"input"` + Expected []string `json:"expected"` } // template applied to above data structure generates the Go test cases @@ -40,12 +39,14 @@ var testCases = []struct { expected []string expectedError error }{ -{{range .J.Cases}}{ - description: {{printf "%q" .Description}}, +{{range .J.rows}}{ + description: {{printf "%q" .Description}}, input: {{printf "%q" .Input.Letter}}, + expected: []string{ + {{range .Expected}} "{{printf "%v" .}}", + {{end}} + }, expectedError: nil, - expected: []string { {{range $line := .Expected}}{{printf "\n%q," $line}}{{end}} -}, }, {{end}} }` diff --git a/exercises/practice/diamond/cases_test.go b/exercises/practice/diamond/cases_test.go index cb46281be..c3b298d9c 100644 --- a/exercises/practice/diamond/cases_test.go +++ b/exercises/practice/diamond/cases_test.go @@ -10,27 +10,26 @@ var testCases = []struct { expectedError error }{ { - description: "Degenerate case with a single 'A' row", - input: "A", - expectedError: nil, + description: "Degenerate case with a single 'A' row", + input: "A", expected: []string{ "A", }, + expectedError: nil, }, { - description: "Degenerate case with no row containing 3 distinct groups of spaces", - input: "B", - expectedError: nil, + description: "Degenerate case with no row containing 3 distinct groups of spaces", + input: "B", expected: []string{ " A ", "B B", " A ", }, + expectedError: nil, }, { - description: "Smallest non-degenerate case with odd diamond side length", - input: "C", - expectedError: nil, + description: "Smallest non-degenerate case with odd diamond side length", + input: "C", expected: []string{ " A ", " B B ", @@ -38,11 +37,11 @@ var testCases = []struct { " B B ", " A ", }, + expectedError: nil, }, { - description: "Smallest non-degenerate case with even diamond side length", - input: "D", - expectedError: nil, + description: "Smallest non-degenerate case with even diamond side length", + input: "D", expected: []string{ " A ", " B B ", @@ -52,11 +51,11 @@ var testCases = []struct { " B B ", " A ", }, + expectedError: nil, }, { - description: "Largest possible diamond", - input: "Z", - expectedError: nil, + description: "Largest possible diamond", + input: "Z", expected: []string{ " A ", " B B ", @@ -110,5 +109,6 @@ var testCases = []struct { " B B ", " A ", }, + expectedError: nil, }, } diff --git a/exercises/practice/forth/.meta/config.json b/exercises/practice/forth/.meta/config.json index f47158ef0..3b8de0fef 100644 --- a/exercises/practice/forth/.meta/config.json +++ b/exercises/practice/forth/.meta/config.json @@ -12,7 +12,8 @@ "hilary", "ilmanzo", "robphoenix", - "sebito91" + "sebito91", + "eklatzer" ], "files": { "solution": [ diff --git a/exercises/practice/forth/.meta/gen.go b/exercises/practice/forth/.meta/gen.go index 331ce670b..3d62a8096 100644 --- a/exercises/practice/forth/.meta/gen.go +++ b/exercises/practice/forth/.meta/gen.go @@ -12,54 +12,52 @@ func main() { if err != nil { log.Fatal(err) } - var j js - if err := gen.Gen("forth", &j, t); err != nil { + var j = map[string]interface{}{ + "evaluate": &[]testCase{}, + //TODO: add test with property `evaluateBoth` to forth_test.go and generate cases in cases_test.go + } + if err := gen.Gen("forth", j, t); err != nil { log.Fatal(err) } } -// The JSON structure we expect to be able to unmarshal into -type js struct { - Groups []Group `json:"cases"` -} - -// A group of test cases. -type Group struct { - Name string `json:"description"` - Cases []OneCase +type testCase struct { + Description string `json:"description"` + Input struct { + Instructions []string `json:"instructions"` + } `json:"input"` + Expected interface{} `json:"expected"` } -// One test case. -type OneCase struct { - Description string - Input struct { - Instructions []string +func (t testCase) ExpectedNumbers() []int { + numbers, ok := t.Expected.([]interface{}) + if !ok { + return nil + } + var result = make([]int, 0) + for _, number := range numbers { + x, ok := number.(float64) + if !ok { + return nil + } + result = append(result, int(x)) } - Expected interface{} + return result } -// IntSlice converts an .Expected interface{} object -// to either nil when an error is indicated by a map[string]interface{} in the JSON, -// or a slice of integers. -func (c OneCase) IntSlice() (list []int) { - _, ok := c.Expected.(map[string]interface{}) - if ok { - return nil +func (t testCase) ExplainText() string { + if t.ExpectedNumbers() != nil { + return "" } - ilist, ok := c.Expected.([]interface{}) + m, ok := t.Expected.(map[string]interface{}) if !ok { - return nil + return "" } - list = make([]int, 0) - for _, iv := range ilist { - // The literals from the JSON are unmarshalled to float64 values, - // which are converted to int for the template output. - v, isFloat64 := iv.(float64) - if isFloat64 { - list = append(list, int(v)) - } + errText, ok := m["error"].(string) + if !ok { + return "" } - return list + return errText } // template applied to above data structure generates the Go test cases @@ -67,29 +65,17 @@ var tmpl = `package forth {{.Header}} -type testGroup struct { - group string - tests []testCase -} - -type testCase struct { - description string - input []string - expected []int // nil slice indicates error expected. -} - -var testGroups = []testGroup{ -{{range .J.Groups}}{ -group: {{printf "%q" .Name}}, -tests: []testCase{ -{{range .Cases}}{ -{{printf "%q" .Description}}, -{{printf "%#v" .Input.Instructions}}, -{{printf "%#v" .IntSlice}}, -}, -{{end}} -}, -}, -{{end}} +var testCases = []struct { + description string + input []string + expected []int // nil slice indicates error expected. + explainText string // error explanation text +}{ {{range .J.evaluate}} +{ + description: {{printf "%q" .Description}}, + input: {{printf "%#v" .Input.Instructions}}, + expected: {{printf "%#v" .ExpectedNumbers}}, + explainText: {{printf "%q" .ExplainText}}, +},{{end}} } ` diff --git a/exercises/practice/forth/.meta/tests.toml b/exercises/practice/forth/.meta/tests.toml index 4cdd8ab5b..6ce30e63a 100644 --- a/exercises/practice/forth/.meta/tests.toml +++ b/exercises/practice/forth/.meta/tests.toml @@ -1,141 +1,158 @@ -# This is an auto-generated file. Regular comments will be removed when this -# file is regenerated. Regenerating will not touch any manually added keys, -# so comments can be added in a "comment" key. - -[9962203f-f00a-4a85-b404-8a8ecbcec09d] -description = "numbers just get pushed onto the stack" - -[9e69588e-a3d8-41a3-a371-ea02206c1e6e] -description = "can add two numbers" - -[52336dd3-30da-4e5c-8523-bdf9a3427657] -description = "errors if there is nothing on the stack" - -[06efb9a4-817a-435e-b509-06166993c1b8] -description = "errors if there is only one value on the stack" - -[09687c99-7bbc-44af-8526-e402f997ccbf] -description = "can subtract two numbers" - -[5d63eee2-1f7d-4538-b475-e27682ab8032] -description = "errors if there is nothing on the stack" - -[b3cee1b2-9159-418a-b00d-a1bb3765c23b] -description = "errors if there is only one value on the stack" - -[5df0ceb5-922e-401f-974d-8287427dbf21] -description = "can multiply two numbers" - -[9e004339-15ac-4063-8ec1-5720f4e75046] -description = "errors if there is nothing on the stack" - -[8ba4b432-9f94-41e0-8fae-3b3712bd51b3] -description = "errors if there is only one value on the stack" - -[e74c2204-b057-4cff-9aa9-31c7c97a93f5] -description = "can divide two numbers" - -[54f6711c-4b14-4bb0-98ad-d974a22c4620] -description = "performs integer division" - -[a5df3219-29b4-4d2f-b427-81f82f42a3f1] -description = "errors if dividing by zero" - -[1d5bb6b3-6749-4e02-8a79-b5d4d334cb8a] -description = "errors if there is nothing on the stack" - -[d5547f43-c2ff-4d5c-9cb0-2a4f6684c20d] -description = "errors if there is only one value on the stack" - -[ee28d729-6692-4a30-b9be-0d830c52a68c] -description = "addition and subtraction" - -[40b197da-fa4b-4aca-a50b-f000d19422c1] -description = "multiplication and division" - -[c5758235-6eef-4bf6-ab62-c878e50b9957] -description = "copies a value on the stack" - -[f6889006-5a40-41e7-beb3-43b09e5a22f4] -description = "copies the top value on the stack" - -[40b7569c-8401-4bd4-a30d-9adf70d11bc4] -description = "errors if there is nothing on the stack" - -[1971da68-1df2-4569-927a-72bf5bb7263c] -description = "removes the top value on the stack if it is the only one" - -[8929d9f2-4a78-4e0f-90ad-be1a0f313fd9] -description = "removes the top value on the stack if it is not the only one" - -[6dd31873-6dd7-4cb8-9e90-7daa33ba045c] -description = "errors if there is nothing on the stack" - -[3ee68e62-f98a-4cce-9e6c-8aae6c65a4e3] -description = "swaps the top two values on the stack if they are the only ones" - -[8ce869d5-a503-44e4-ab55-1da36816ff1c] -description = "swaps the top two values on the stack if they are not the only ones" - -[74ba5b2a-b028-4759-9176-c5c0e7b2b154] -description = "errors if there is nothing on the stack" - -[dd52e154-5d0d-4a5c-9e5d-73eb36052bc8] -description = "errors if there is only one value on the stack" - -[a2654074-ba68-4f93-b014-6b12693a8b50] -description = "copies the second element if there are only two" - -[c5b51097-741a-4da7-8736-5c93fa856339] -description = "copies the second element if there are more than two" - -[6e1703a6-5963-4a03-abba-02e77e3181fd] -description = "errors if there is nothing on the stack" - -[ee574dc4-ef71-46f6-8c6a-b4af3a10c45f] -description = "errors if there is only one value on the stack" - -[ed45cbbf-4dbf-4901-825b-54b20dbee53b] -description = "can consist of built-in words" - -[2726ea44-73e4-436b-bc2b-5ff0c6aa014b] -description = "execute in the right order" - -[9e53c2d0-b8ef-4ad8-b2c9-a559b421eb33] -description = "can override other user-defined words" - -[669db3f3-5bd6-4be0-83d1-618cd6e4984b] -description = "can override built-in words" - -[588de2f0-c56e-4c68-be0b-0bb1e603c500] -description = "can override built-in operators" - -[ac12aaaf-26c6-4a10-8b3c-1c958fa2914c] -description = "can use different words with the same name" - -[53f82ef0-2750-4ccb-ac04-5d8c1aefabb1] -description = "can define word that uses word with the same name" - -[35958cee-a976-4a0f-9378-f678518fa322] -description = "cannot redefine numbers" - -[5180f261-89dd-491e-b230-62737e09806f] -description = "errors if executing a non-existent word" - -[7b83bb2e-b0e8-461f-ad3b-96ee2e111ed6] -description = "DUP is case-insensitive" - -[339ed30b-f5b4-47ff-ab1c-67591a9cd336] -description = "DROP is case-insensitive" - -[ee1af31e-1355-4b1b-bb95-f9d0b2961b87] -description = "SWAP is case-insensitive" - -[acdc3a49-14c8-4cc2-945d-11edee6408fa] -description = "OVER is case-insensitive" - -[5934454f-a24f-4efc-9fdd-5794e5f0c23c] -description = "user-defined words are case-insensitive" - -[037d4299-195f-4be7-a46d-f07ca6280a06] -description = "definitions are case-insensitive" +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[9962203f-f00a-4a85-b404-8a8ecbcec09d] +description = "parsing and numbers -> numbers just get pushed onto the stack" + +[fd7a8da2-6818-4203-a866-fed0714e7aa0] +description = "parsing and numbers -> pushes negative numbers onto the stack" + +[9e69588e-a3d8-41a3-a371-ea02206c1e6e] +description = "addition -> can add two numbers" + +[52336dd3-30da-4e5c-8523-bdf9a3427657] +description = "addition -> errors if there is nothing on the stack" + +[06efb9a4-817a-435e-b509-06166993c1b8] +description = "addition -> errors if there is only one value on the stack" + +[09687c99-7bbc-44af-8526-e402f997ccbf] +description = "subtraction -> can subtract two numbers" + +[5d63eee2-1f7d-4538-b475-e27682ab8032] +description = "subtraction -> errors if there is nothing on the stack" + +[b3cee1b2-9159-418a-b00d-a1bb3765c23b] +description = "subtraction -> errors if there is only one value on the stack" + +[5df0ceb5-922e-401f-974d-8287427dbf21] +description = "multiplication -> can multiply two numbers" + +[9e004339-15ac-4063-8ec1-5720f4e75046] +description = "multiplication -> errors if there is nothing on the stack" + +[8ba4b432-9f94-41e0-8fae-3b3712bd51b3] +description = "multiplication -> errors if there is only one value on the stack" + +[e74c2204-b057-4cff-9aa9-31c7c97a93f5] +description = "division -> can divide two numbers" + +[54f6711c-4b14-4bb0-98ad-d974a22c4620] +description = "division -> performs integer division" + +[a5df3219-29b4-4d2f-b427-81f82f42a3f1] +description = "division -> errors if dividing by zero" + +[1d5bb6b3-6749-4e02-8a79-b5d4d334cb8a] +description = "division -> errors if there is nothing on the stack" + +[d5547f43-c2ff-4d5c-9cb0-2a4f6684c20d] +description = "division -> errors if there is only one value on the stack" + +[ee28d729-6692-4a30-b9be-0d830c52a68c] +description = "combined arithmetic -> addition and subtraction" + +[40b197da-fa4b-4aca-a50b-f000d19422c1] +description = "combined arithmetic -> multiplication and division" + +[c5758235-6eef-4bf6-ab62-c878e50b9957] +description = "dup -> copies a value on the stack" + +[f6889006-5a40-41e7-beb3-43b09e5a22f4] +description = "dup -> copies the top value on the stack" + +[40b7569c-8401-4bd4-a30d-9adf70d11bc4] +description = "dup -> errors if there is nothing on the stack" + +[1971da68-1df2-4569-927a-72bf5bb7263c] +description = "drop -> removes the top value on the stack if it is the only one" + +[8929d9f2-4a78-4e0f-90ad-be1a0f313fd9] +description = "drop -> removes the top value on the stack if it is not the only one" + +[6dd31873-6dd7-4cb8-9e90-7daa33ba045c] +description = "drop -> errors if there is nothing on the stack" + +[3ee68e62-f98a-4cce-9e6c-8aae6c65a4e3] +description = "swap -> swaps the top two values on the stack if they are the only ones" + +[8ce869d5-a503-44e4-ab55-1da36816ff1c] +description = "swap -> swaps the top two values on the stack if they are not the only ones" + +[74ba5b2a-b028-4759-9176-c5c0e7b2b154] +description = "swap -> errors if there is nothing on the stack" + +[dd52e154-5d0d-4a5c-9e5d-73eb36052bc8] +description = "swap -> errors if there is only one value on the stack" + +[a2654074-ba68-4f93-b014-6b12693a8b50] +description = "over -> copies the second element if there are only two" + +[c5b51097-741a-4da7-8736-5c93fa856339] +description = "over -> copies the second element if there are more than two" + +[6e1703a6-5963-4a03-abba-02e77e3181fd] +description = "over -> errors if there is nothing on the stack" + +[ee574dc4-ef71-46f6-8c6a-b4af3a10c45f] +description = "over -> errors if there is only one value on the stack" + +[ed45cbbf-4dbf-4901-825b-54b20dbee53b] +description = "user-defined words -> can consist of built-in words" + +[2726ea44-73e4-436b-bc2b-5ff0c6aa014b] +description = "user-defined words -> execute in the right order" + +[9e53c2d0-b8ef-4ad8-b2c9-a559b421eb33] +description = "user-defined words -> can override other user-defined words" + +[669db3f3-5bd6-4be0-83d1-618cd6e4984b] +description = "user-defined words -> can override built-in words" + +[588de2f0-c56e-4c68-be0b-0bb1e603c500] +description = "user-defined words -> can override built-in operators" + +[ac12aaaf-26c6-4a10-8b3c-1c958fa2914c] +description = "user-defined words -> can use different words with the same name" + +[53f82ef0-2750-4ccb-ac04-5d8c1aefabb1] +description = "user-defined words -> can define word that uses word with the same name" + +[35958cee-a976-4a0f-9378-f678518fa322] +description = "user-defined words -> cannot redefine non-negative numbers" + +[df5b2815-3843-4f55-b16c-c3ed507292a7] +description = "user-defined words -> cannot redefine negative numbers" + +[5180f261-89dd-491e-b230-62737e09806f] +description = "user-defined words -> errors if executing a non-existent word" + +[3c8bfef3-edbb-49c1-9993-21d4030043cb] +description = "user-defined words -> only defines locally" +include = false + +[7b83bb2e-b0e8-461f-ad3b-96ee2e111ed6] +description = "case-insensitivity -> DUP is case-insensitive" + +[339ed30b-f5b4-47ff-ab1c-67591a9cd336] +description = "case-insensitivity -> DROP is case-insensitive" + +[ee1af31e-1355-4b1b-bb95-f9d0b2961b87] +description = "case-insensitivity -> SWAP is case-insensitive" + +[acdc3a49-14c8-4cc2-945d-11edee6408fa] +description = "case-insensitivity -> OVER is case-insensitive" + +[5934454f-a24f-4efc-9fdd-5794e5f0c23c] +description = "case-insensitivity -> user-defined words are case-insensitive" + +[037d4299-195f-4be7-a46d-f07ca6280a06] +description = "case-insensitivity -> definitions are case-insensitive" diff --git a/exercises/practice/forth/cases_test.go b/exercises/practice/forth/cases_test.go index 735089a43..a52a71b75 100644 --- a/exercises/practice/forth/cases_test.go +++ b/exercises/practice/forth/cases_test.go @@ -1,309 +1,300 @@ package forth // Source: exercism/problem-specifications -// Commit: 75f4c0a Corrected minor typos in the error msg expectation (doesn't match other similar error patterns and so breaks auto generated tests) -// Problem Specifications Version: 1.7.1 +// Commit: b230e1e forth: Add local-scope test -type testGroup struct { - group string - tests []testCase -} - -type testCase struct { +var testCases = []struct { description string input []string - expected []int // nil slice indicates error expected. -} - -var testGroups = []testGroup{ - { - group: "parsing and numbers", - tests: []testCase{ - { - "numbers just get pushed onto the stack", - []string{"1 2 3 4 5"}, - []int{1, 2, 3, 4, 5}, - }, - }, - }, - { - group: "addition", - tests: []testCase{ - { - "can add two numbers", - []string{"1 2 +"}, - []int{3}, - }, - { - "errors if there is nothing on the stack", - []string{"+"}, - []int(nil), - }, - { - "errors if there is only one value on the stack", - []string{"1 +"}, - []int(nil), - }, - }, - }, - { - group: "subtraction", - tests: []testCase{ - { - "can subtract two numbers", - []string{"3 4 -"}, - []int{-1}, - }, - { - "errors if there is nothing on the stack", - []string{"-"}, - []int(nil), - }, - { - "errors if there is only one value on the stack", - []string{"1 -"}, - []int(nil), - }, - }, - }, - { - group: "multiplication", - tests: []testCase{ - { - "can multiply two numbers", - []string{"2 4 *"}, - []int{8}, - }, - { - "errors if there is nothing on the stack", - []string{"*"}, - []int(nil), - }, - { - "errors if there is only one value on the stack", - []string{"1 *"}, - []int(nil), - }, - }, - }, - { - group: "division", - tests: []testCase{ - { - "can divide two numbers", - []string{"12 3 /"}, - []int{4}, - }, - { - "performs integer division", - []string{"8 3 /"}, - []int{2}, - }, - { - "errors if dividing by zero", - []string{"4 0 /"}, - []int(nil), - }, - { - "errors if there is nothing on the stack", - []string{"/"}, - []int(nil), - }, - { - "errors if there is only one value on the stack", - []string{"1 /"}, - []int(nil), - }, - }, - }, - { - group: "combined arithmetic", - tests: []testCase{ - { - "addition and subtraction", - []string{"1 2 + 4 -"}, - []int{-1}, - }, - { - "multiplication and division", - []string{"2 4 * 3 /"}, - []int{2}, - }, - }, - }, - { - group: "dup", - tests: []testCase{ - { - "copies a value on the stack", - []string{"1 dup"}, - []int{1, 1}, - }, - { - "copies the top value on the stack", - []string{"1 2 dup"}, - []int{1, 2, 2}, - }, - { - "errors if there is nothing on the stack", - []string{"dup"}, - []int(nil), - }, - }, - }, - { - group: "drop", - tests: []testCase{ - { - "removes the top value on the stack if it is the only one", - []string{"1 drop"}, - []int{}, - }, - { - "removes the top value on the stack if it is not the only one", - []string{"1 2 drop"}, - []int{1}, - }, - { - "errors if there is nothing on the stack", - []string{"drop"}, - []int(nil), - }, - }, - }, - { - group: "swap", - tests: []testCase{ - { - "swaps the top two values on the stack if they are the only ones", - []string{"1 2 swap"}, - []int{2, 1}, - }, - { - "swaps the top two values on the stack if they are not the only ones", - []string{"1 2 3 swap"}, - []int{1, 3, 2}, - }, - { - "errors if there is nothing on the stack", - []string{"swap"}, - []int(nil), - }, - { - "errors if there is only one value on the stack", - []string{"1 swap"}, - []int(nil), - }, - }, - }, - { - group: "over", - tests: []testCase{ - { - "copies the second element if there are only two", - []string{"1 2 over"}, - []int{1, 2, 1}, - }, - { - "copies the second element if there are more than two", - []string{"1 2 3 over"}, - []int{1, 2, 3, 2}, - }, - { - "errors if there is nothing on the stack", - []string{"over"}, - []int(nil), - }, - { - "errors if there is only one value on the stack", - []string{"1 over"}, - []int(nil), - }, - }, - }, - { - group: "user-defined words", - tests: []testCase{ - { - "can consist of built-in words", - []string{": dup-twice dup dup ;", "1 dup-twice"}, - []int{1, 1, 1}, - }, - { - "execute in the right order", - []string{": countup 1 2 3 ;", "countup"}, - []int{1, 2, 3}, - }, - { - "can override other user-defined words", - []string{": foo dup ;", ": foo dup dup ;", "1 foo"}, - []int{1, 1, 1}, - }, - { - "can override built-in words", - []string{": swap dup ;", "1 swap"}, - []int{1, 1}, - }, - { - "can override built-in operators", - []string{": + * ;", "3 4 +"}, - []int{12}, - }, - { - "can use different words with the same name", - []string{": foo 5 ;", ": bar foo ;", ": foo 6 ;", "bar foo"}, - []int{5, 6}, - }, - { - "can define word that uses word with the same name", - []string{": foo 10 ;", ": foo foo 1 + ;", "foo"}, - []int{11}, - }, - { - "cannot redefine numbers", - []string{": 1 2 ;"}, - []int(nil), - }, - { - "errors if executing a non-existent word", - []string{"foo"}, - []int(nil), - }, - }, - }, - { - group: "case-insensitivity", - tests: []testCase{ - { - "DUP is case-insensitive", - []string{"1 DUP Dup dup"}, - []int{1, 1, 1, 1}, - }, - { - "DROP is case-insensitive", - []string{"1 2 3 4 DROP Drop drop"}, - []int{1}, - }, - { - "SWAP is case-insensitive", - []string{"1 2 SWAP 3 Swap 4 swap"}, - []int{2, 3, 4, 1}, - }, - { - "OVER is case-insensitive", - []string{"1 2 OVER Over over"}, - []int{1, 2, 1, 2, 1}, - }, - { - "user-defined words are case-insensitive", - []string{": foo dup ;", "1 FOO Foo foo"}, - []int{1, 1, 1, 1}, - }, - { - "definitions are case-insensitive", - []string{": SWAP DUP Dup dup ;", "1 swap"}, - []int{1, 1, 1, 1}, - }, - }, + expected []int // nil slice indicates error expected. + explainText string // error explanation text +}{ + { + description: "numbers just get pushed onto the stack", + input: []string{"1 2 3 4 5"}, + expected: []int{1, 2, 3, 4, 5}, + explainText: "", + }, + { + description: "pushes negative numbers onto the stack", + input: []string{"-1 -2 -3 -4 -5"}, + expected: []int{-1, -2, -3, -4, -5}, + explainText: "", + }, + { + description: "can add two numbers", + input: []string{"1 2 +"}, + expected: []int{3}, + explainText: "", + }, + { + description: "errors if there is nothing on the stack", + input: []string{"+"}, + expected: []int(nil), + explainText: "empty stack", + }, + { + description: "errors if there is only one value on the stack", + input: []string{"1 +"}, + expected: []int(nil), + explainText: "only one value on the stack", + }, + { + description: "can subtract two numbers", + input: []string{"3 4 -"}, + expected: []int{-1}, + explainText: "", + }, + { + description: "errors if there is nothing on the stack", + input: []string{"-"}, + expected: []int(nil), + explainText: "empty stack", + }, + { + description: "errors if there is only one value on the stack", + input: []string{"1 -"}, + expected: []int(nil), + explainText: "only one value on the stack", + }, + { + description: "can multiply two numbers", + input: []string{"2 4 *"}, + expected: []int{8}, + explainText: "", + }, + { + description: "errors if there is nothing on the stack", + input: []string{"*"}, + expected: []int(nil), + explainText: "empty stack", + }, + { + description: "errors if there is only one value on the stack", + input: []string{"1 *"}, + expected: []int(nil), + explainText: "only one value on the stack", + }, + { + description: "can divide two numbers", + input: []string{"12 3 /"}, + expected: []int{4}, + explainText: "", + }, + { + description: "performs integer division", + input: []string{"8 3 /"}, + expected: []int{2}, + explainText: "", + }, + { + description: "errors if dividing by zero", + input: []string{"4 0 /"}, + expected: []int(nil), + explainText: "divide by zero", + }, + { + description: "errors if there is nothing on the stack", + input: []string{"/"}, + expected: []int(nil), + explainText: "empty stack", + }, + { + description: "errors if there is only one value on the stack", + input: []string{"1 /"}, + expected: []int(nil), + explainText: "only one value on the stack", + }, + { + description: "addition and subtraction", + input: []string{"1 2 + 4 -"}, + expected: []int{-1}, + explainText: "", + }, + { + description: "multiplication and division", + input: []string{"2 4 * 3 /"}, + expected: []int{2}, + explainText: "", + }, + { + description: "copies a value on the stack", + input: []string{"1 dup"}, + expected: []int{1, 1}, + explainText: "", + }, + { + description: "copies the top value on the stack", + input: []string{"1 2 dup"}, + expected: []int{1, 2, 2}, + explainText: "", + }, + { + description: "errors if there is nothing on the stack", + input: []string{"dup"}, + expected: []int(nil), + explainText: "empty stack", + }, + { + description: "removes the top value on the stack if it is the only one", + input: []string{"1 drop"}, + expected: []int{}, + explainText: "", + }, + { + description: "removes the top value on the stack if it is not the only one", + input: []string{"1 2 drop"}, + expected: []int{1}, + explainText: "", + }, + { + description: "errors if there is nothing on the stack", + input: []string{"drop"}, + expected: []int(nil), + explainText: "empty stack", + }, + { + description: "swaps the top two values on the stack if they are the only ones", + input: []string{"1 2 swap"}, + expected: []int{2, 1}, + explainText: "", + }, + { + description: "swaps the top two values on the stack if they are not the only ones", + input: []string{"1 2 3 swap"}, + expected: []int{1, 3, 2}, + explainText: "", + }, + { + description: "errors if there is nothing on the stack", + input: []string{"swap"}, + expected: []int(nil), + explainText: "empty stack", + }, + { + description: "errors if there is only one value on the stack", + input: []string{"1 swap"}, + expected: []int(nil), + explainText: "only one value on the stack", + }, + { + description: "copies the second element if there are only two", + input: []string{"1 2 over"}, + expected: []int{1, 2, 1}, + explainText: "", + }, + { + description: "copies the second element if there are more than two", + input: []string{"1 2 3 over"}, + expected: []int{1, 2, 3, 2}, + explainText: "", + }, + { + description: "errors if there is nothing on the stack", + input: []string{"over"}, + expected: []int(nil), + explainText: "empty stack", + }, + { + description: "errors if there is only one value on the stack", + input: []string{"1 over"}, + expected: []int(nil), + explainText: "only one value on the stack", + }, + { + description: "can consist of built-in words", + input: []string{": dup-twice dup dup ;", "1 dup-twice"}, + expected: []int{1, 1, 1}, + explainText: "", + }, + { + description: "execute in the right order", + input: []string{": countup 1 2 3 ;", "countup"}, + expected: []int{1, 2, 3}, + explainText: "", + }, + { + description: "can override other user-defined words", + input: []string{": foo dup ;", ": foo dup dup ;", "1 foo"}, + expected: []int{1, 1, 1}, + explainText: "", + }, + { + description: "can override built-in words", + input: []string{": swap dup ;", "1 swap"}, + expected: []int{1, 1}, + explainText: "", + }, + { + description: "can override built-in operators", + input: []string{": + * ;", "3 4 +"}, + expected: []int{12}, + explainText: "", + }, + { + description: "can use different words with the same name", + input: []string{": foo 5 ;", ": bar foo ;", ": foo 6 ;", "bar foo"}, + expected: []int{5, 6}, + explainText: "", + }, + { + description: "can define word that uses word with the same name", + input: []string{": foo 10 ;", ": foo foo 1 + ;", "foo"}, + expected: []int{11}, + explainText: "", + }, + { + description: "cannot redefine non-negative numbers", + input: []string{": 1 2 ;"}, + expected: []int(nil), + explainText: "illegal operation", + }, + { + description: "cannot redefine negative numbers", + input: []string{": -1 2 ;"}, + expected: []int(nil), + explainText: "illegal operation", + }, + { + description: "errors if executing a non-existent word", + input: []string{"foo"}, + expected: []int(nil), + explainText: "undefined operation", + }, + { + description: "DUP is case-insensitive", + input: []string{"1 DUP Dup dup"}, + expected: []int{1, 1, 1, 1}, + explainText: "", + }, + { + description: "DROP is case-insensitive", + input: []string{"1 2 3 4 DROP Drop drop"}, + expected: []int{1}, + explainText: "", + }, + { + description: "SWAP is case-insensitive", + input: []string{"1 2 SWAP 3 Swap 4 swap"}, + expected: []int{2, 3, 4, 1}, + explainText: "", + }, + { + description: "OVER is case-insensitive", + input: []string{"1 2 OVER Over over"}, + expected: []int{1, 2, 1, 2, 1}, + explainText: "", + }, + { + description: "user-defined words are case-insensitive", + input: []string{": foo dup ;", "1 FOO Foo foo"}, + expected: []int{1, 1, 1, 1}, + explainText: "", + }, + { + description: "definitions are case-insensitive", + input: []string{": SWAP DUP Dup dup ;", "1 swap"}, + expected: []int{1, 1, 1, 1}, + explainText: "", }, } diff --git a/exercises/practice/forth/forth_test.go b/exercises/practice/forth/forth_test.go index 3a5fb7300..56999934e 100644 --- a/exercises/practice/forth/forth_test.go +++ b/exercises/practice/forth/forth_test.go @@ -6,23 +6,19 @@ import ( ) func TestForth(t *testing.T) { - for _, tg := range testGroups { - for _, tc := range tg.tests { - if v, err := Forth(tc.input); err == nil { - var _ error = err + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + v, err := Forth(tc.input) + if err == nil { if tc.expected == nil { - t.Fatalf("FAIL: %s | %s\n\tForth(%#v) expected an error, got %v", - tg.group, tc.description, tc.input, v) + t.Fatalf("Forth(%#v) expected an error, got %v", tc.input, v) } else if !reflect.DeepEqual(v, tc.expected) { - t.Fatalf("FAIL: %s | %s\n\tForth(%#v) expected %v, got %v", - tg.group, tc.description, tc.input, tc.expected, v) + t.Fatalf("Forth(%#v) expected %v, got %v", tc.input, tc.expected, v) } } else if tc.expected != nil { - t.Fatalf("FAIL: %s | %s\n\tForth(%#v) expected %v, got an error: %q", - tg.group, tc.description, tc.input, tc.expected, err) + t.Fatalf("Forth(%#v) expected %v, got an error: %q", tc.input, tc.expected, err) } - t.Logf("PASS: %s | %s", tg.group, tc.description) - } + }) } } @@ -31,10 +27,8 @@ func BenchmarkForth(b *testing.B) { b.Skip("skipping benchmark in short mode.") } for i := 0; i < b.N; i++ { - for _, tg := range testGroups { - for _, tc := range tg.tests { - Forth(tc.input) - } + for _, tc := range testCases { + Forth(tc.input) } } } diff --git a/exercises/practice/minesweeper/minesweeper.go b/exercises/practice/minesweeper/minesweeper.go index 23f7f091f..b49411aed 100644 --- a/exercises/practice/minesweeper/minesweeper.go +++ b/exercises/practice/minesweeper/minesweeper.go @@ -3,4 +3,4 @@ package minesweeper // Annotate returns an annotated board func Annotate(board []string) []string { panic("Please implement the Annotate function") -} +} diff --git a/exercises/practice/saddle-points/.meta/config.json b/exercises/practice/saddle-points/.meta/config.json index 70c8cb9fc..1f2f10361 100644 --- a/exercises/practice/saddle-points/.meta/config.json +++ b/exercises/practice/saddle-points/.meta/config.json @@ -15,7 +15,8 @@ "petertseng", "robphoenix", "sebito91", - "tleen" + "tleen", + "eklatzer" ], "files": { "solution": [ diff --git a/exercises/practice/saddle-points/.meta/example.go b/exercises/practice/saddle-points/.meta/example.go index d20ac3206..56c581cf7 100644 --- a/exercises/practice/saddle-points/.meta/example.go +++ b/exercises/practice/saddle-points/.meta/example.go @@ -81,7 +81,7 @@ func (m *Matrix) Saddle() (p []Pair) { for _, c := range maxs(row) { for _, rmin := range colMin[c] { if rmin == r { - p = append(p, Pair{r, c}) + p = append(p, Pair{r + 1, c + 1}) break } } diff --git a/exercises/practice/saddle-points/.meta/gen.go b/exercises/practice/saddle-points/.meta/gen.go new file mode 100644 index 000000000..6c3ca335b --- /dev/null +++ b/exercises/practice/saddle-points/.meta/gen.go @@ -0,0 +1,60 @@ +package main + +import ( + "log" + "text/template" + + "../../../../gen" +) + +func main() { + t, err := template.New("").Parse(tmpl) + if err != nil { + log.Fatal(err) + } + var j = map[string]interface{}{ + "saddlePoints": &[]Case{}, + } + if err := gen.Gen("saddle-points", j, t); err != nil { + log.Fatal(err) + } +} + +type Case struct { + Description string `json:"description"` + Input struct { + Matrix [][]int `json:"matrix"` + } `json:"input"` + Expected []struct { + Row int `json:"row"` + Column int `json:"column"` + } `json:"expected"` +} + +// Template to generate two sets of test cases, one for Score tests and one for Roll tests. +var tmpl = `package matrix + +{{.Header}} + +var testCases = []struct { + description string + input [][]int + expectedOutput []Pair +}{ + {{range .J.saddlePoints}} + { + description: {{printf "%q" .Description}}, + input: [][]int{ + {{range .Input.Matrix}} { {{range .}} {{printf "%v" .}}, {{end}} }, {{end}} + }, + expectedOutput: []Pair{ + {{range .Expected}}{ + {{printf "%d" .Row}}, + {{printf "%d" .Column}}, + }, + {{end}} + }, + }, + {{end}} +} +` diff --git a/exercises/practice/saddle-points/.meta/tests.toml b/exercises/practice/saddle-points/.meta/tests.toml index 17d9c8388..ca0085202 100644 --- a/exercises/practice/saddle-points/.meta/tests.toml +++ b/exercises/practice/saddle-points/.meta/tests.toml @@ -1,6 +1,13 @@ -# This is an auto-generated file. Regular comments will be removed when this -# file is regenerated. Regenerating will not touch any manually added keys, -# so comments can be added in a "comment" key. +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. [3e374e63-a2e0-4530-a39a-d53c560382bd] description = "Can identify single saddle point" diff --git a/exercises/practice/saddle-points/cases_test.go b/exercises/practice/saddle-points/cases_test.go new file mode 100644 index 000000000..32ac27206 --- /dev/null +++ b/exercises/practice/saddle-points/cases_test.go @@ -0,0 +1,146 @@ +package matrix + +// Source: exercism/problem-specifications +// Commit: 2e820e1 Auto-format portions of some JSON files (#1967) + +var testCases = []struct { + description string + input [][]int + expectedOutput []Pair +}{ + + { + description: "Can identify single saddle point", + input: [][]int{ + {9, 8, 7}, {5, 3, 2}, {6, 6, 7}, + }, + expectedOutput: []Pair{ + { + 2, + 1, + }, + }, + }, + + { + description: "Can identify that empty matrix has no saddle points", + input: [][]int{ + {}, + }, + expectedOutput: []Pair{}, + }, + + { + description: "Can identify lack of saddle points when there are none", + input: [][]int{ + {1, 2, 3}, {3, 1, 2}, {2, 3, 1}, + }, + expectedOutput: []Pair{}, + }, + + { + description: "Can identify multiple saddle points in a column", + input: [][]int{ + {4, 5, 4}, {3, 5, 5}, {1, 5, 4}, + }, + expectedOutput: []Pair{ + { + 1, + 2, + }, + { + 2, + 2, + }, + { + 3, + 2, + }, + }, + }, + + { + description: "Can identify multiple saddle points in a row", + input: [][]int{ + {6, 7, 8}, {5, 5, 5}, {7, 5, 6}, + }, + expectedOutput: []Pair{ + { + 2, + 1, + }, + { + 2, + 2, + }, + { + 2, + 3, + }, + }, + }, + + { + description: "Can identify saddle point in bottom right corner", + input: [][]int{ + {8, 7, 9}, {6, 7, 6}, {3, 2, 5}, + }, + expectedOutput: []Pair{ + { + 3, + 3, + }, + }, + }, + + { + description: "Can identify saddle points in a non square matrix", + input: [][]int{ + {3, 1, 3}, {3, 2, 4}, + }, + expectedOutput: []Pair{ + { + 1, + 3, + }, + { + 1, + 1, + }, + }, + }, + + { + description: "Can identify that saddle points in a single column matrix are those with the minimum value", + input: [][]int{ + {2}, {1}, {4}, {1}, + }, + expectedOutput: []Pair{ + { + 2, + 1, + }, + { + 4, + 1, + }, + }, + }, + + { + description: "Can identify that saddle points in a single row matrix are those with the maximum value", + input: [][]int{ + {2, 5, 3, 5}, + }, + expectedOutput: []Pair{ + { + 1, + 2, + }, + { + 1, + 4, + }, + }, + }, +} diff --git a/exercises/practice/saddle-points/saddle_points_test.go b/exercises/practice/saddle-points/saddle_points_test.go index 51481d89e..54b48ccd7 100644 --- a/exercises/practice/saddle-points/saddle_points_test.go +++ b/exercises/practice/saddle-points/saddle_points_test.go @@ -6,31 +6,37 @@ package matrix -import "testing" +import ( + "strconv" + "strings" + "testing" +) -var tests = []struct { - m string - sp []Pair -}{ - {"2 1\n1 2", nil}, - {"1 2\n3 4", []Pair{{0, 1}}}, - {"18 3 39 19 91\n38 10 8 77 320\n3 4 8 6 7", []Pair{{2, 2}}}, - {"4 5 4\n3 5 5\n1 5 4", []Pair{{0, 1}, {1, 1}, {2, 1}}}, +func TestSaddle(t *testing.T) { + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + var in = generateString(tc.input) + m, err := New(in) + if err != nil { + t.Fatalf("TestSaddle needs working New. New(%s) returned %q. Error not expected.", in, err) + } + if got := m.Saddle(); !eq(got, tc.expectedOutput) { + t.Fatalf("%v.Saddle() = %v, want %v", m, got, tc.expectedOutput) + } + }) + } } -func TestSaddle(t *testing.T) { - for _, test := range tests { - m, err := New(test.m) - if err != nil { - var _ error = err - t.Fatalf("TestSaddle needs working New. "+ - "New(%s) returned %s. Error not expected.", - test.m, err) - } - if sp := m.Saddle(); !eq(sp, test.sp) { - t.Fatalf("%v.Saddle() = %v, want %v", m, sp, test.sp) +func generateString(in [][]int) string { + var parts []string + for _, numbersPerLine := range in { + var lineParts []string + for _, number := range numbersPerLine { + lineParts = append(lineParts, strconv.Itoa(number)) } + parts = append(parts, strings.Join(lineParts, " ")) } + return strings.Join(parts, "\n") } func eq(got, exp []Pair) bool { @@ -53,13 +59,12 @@ func BenchmarkSaddle(b *testing.B) { if testing.Short() { b.Skip("skipping benchmark in short mode.") } - ms := make([]*Matrix, len(tests)) + ms := make([]*Matrix, len(testCases)) var err error - for i, test := range tests { - if ms[i], err = New(test.m); err != nil { - b.Fatalf("BenchmarkSaddle needs working New. "+ - "New(%s) returned %s. Error not expected.", - test.m, err) + for i, tc := range testCases { + var in = generateString(tc.input) + if ms[i], err = New(in); err != nil { + b.Fatalf("BenchmarkSaddle needs working New. New(%s) returned %q. Error not expected.", in, err) } } b.ResetTimer() diff --git a/exercises/practice/scrabble-score/scrabble_score.go b/exercises/practice/scrabble-score/scrabble_score.go index 59243a149..02dfd9538 100644 --- a/exercises/practice/scrabble-score/scrabble_score.go +++ b/exercises/practice/scrabble-score/scrabble_score.go @@ -1,5 +1,5 @@ package scrabble func Score(word string) int { - panic("Please implement the Score function") + panic("Please implement the Score function") } diff --git a/gen/filter.go b/gen/filter.go index 7477ca241..825f11893 100644 --- a/gen/filter.go +++ b/gen/filter.go @@ -2,76 +2,71 @@ package gen import ( "encoding/json" + "fmt" ) -func filterTestsJson(jsrc []byte, excludeList map[string]struct{}) ([]byte, error) { - // put the json object in an array to match the recursive structure - jsrcArr := make([]byte, 1, len(jsrc)+2) - jsrcArr[0] = '[' - jsrcArr = append(jsrcArr, jsrc...) - jsrcArr = append(jsrcArr, ']') +// getAllTestCasesFiltered recursively collects all test cases in a flattened list except the ones excluded by excludedTests. +func getAllTestCasesFiltered(jSrc []byte, excludedTests map[string]struct{}) (*[]testCase, error) { + var result = &[]testCase{} - // recursively remove excluded cases from json source, starting from top level - filteredCases, err := recursiveFilterCases(json.RawMessage(jsrcArr), excludeList) - if err != nil { - return nil, err - } + // put the json object in an array to match the recursive structure + jSrc = append([]byte{'['}, append(jSrc, ']')...) - // remove the json object back out of the array - filteredData, err := json.MarshalIndent(filteredCases[0], "", "\t") + // recursively get all test cases except the excluded ones + err := recursiveFilterCases(jSrc, result, excludedTests) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to get/filter all test cases: %w", err) } - return filteredData, nil + return result, nil } -func recursiveFilterCases(cases json.RawMessage, excludeList map[string]struct{}) ([]map[string](json.RawMessage), error) { - var js []map[string](json.RawMessage) - var validCases []map[string](json.RawMessage) +func recursiveFilterCases(cases json.RawMessage, result *[]testCase, excludedTests map[string]struct{}) error { + var js []map[string]json.RawMessage // 'cases' is always an array, where every item is either a single test or an object containing nested cases err := json.Unmarshal(cases, &js) if err != nil { - return validCases, err + return err } // iterating over every item in 'cases' for _, j := range js { - var uuidStr string - uuid, ok := j["uuid"] - // If uuid key is not present, this item is an object containing nested cases. - // So we recursively filter this nested array and set the filtered nested json. - if !ok { - filteredCases, err := recursiveFilterCases(j["cases"], excludeList) + // If uuid key is present, this item is a single test-case + if uuid, ok := j["uuid"]; ok { + var uuidStr string + + err := json.Unmarshal(uuid, &uuidStr) if err != nil { - return validCases, err + return fmt.Errorf("failed to unmarshal uuid %v: %w", uuid, err) } - // convert filteredCases map to json string - filteredCasesJson, err := json.Marshal(filteredCases) + //ignore test-cases with include=false in tests.toml + if _, isExcluded := excludedTests[uuidStr]; isExcluded { + continue + } + + jTestCase, err := json.Marshal(j) + if err != nil { + return err + } + + var tc = testCase{} + err = json.Unmarshal(jTestCase, &tc) if err != nil { - return validCases, err + return err } + *result = append(*result, tc) - // typecast it as json.RawMessage to match types - j["cases"] = json.RawMessage(filteredCasesJson) - validCases = append(validCases, j) continue } - // If uuid key is present, this item is a single test case. - // So we check if this uuid is present in the exclude list. - // If it is not present, we add it to the list of valid cases. - err := json.Unmarshal(uuid, &uuidStr) + // If uuid key is not present, this item is an object containing nested cases. + err := recursiveFilterCases(j["cases"], result, excludedTests) if err != nil { - return validCases, err - } - _, ok = excludeList[uuidStr] - if !ok { - validCases = append(validCases, j) + return err } } - return validCases, nil + return nil } diff --git a/gen/filter_test.go b/gen/filter_test.go index 2fc7079ae..ac15a6703 100644 --- a/gen/filter_test.go +++ b/gen/filter_test.go @@ -1,13 +1,12 @@ package gen import ( - "bytes" - "strings" + "reflect" "testing" ) var ( - validInputJson = ` + validInputJSON = ` { "exercise": "some-exercise-name", "comments": [ @@ -16,16 +15,18 @@ var ( ], "cases": [ { - "description": "test case 1", - "expected": "abcde", - "input": { - "some": "inp" - }, - "uuid": "alskjdb-f781-4c52-b73b-d4b867f41540" + "description": "test case 1", + "expected": "abcde", + "property" : "test", + "input": { + "some": "inp" + }, + "uuid": "alskjdb-f781-4c52-b73b-d4b867f41540" }, { "description": "test case 2", "expected": "rvedv", + "property" : "test2", "input": { "some": "ukbt" }, @@ -100,104 +101,89 @@ var ( ` excludeList = map[string]struct{}{ - "8snv0f-f781-4c52-b73b-8e76427defd0": struct{}{}, - "klnhng-f781-4c52-b73b-8e76427defd0": struct{}{}, - "98axn89-29f9-46f2-8c95-6c5b7a595aee": struct{}{}, + "8snv0f-f781-4c52-b73b-8e76427defd0": {}, // test case 2 + "klnhng-f781-4c52-b73b-8e76427defd0": {}, // test case 3 + "98axn89-29f9-46f2-8c95-6c5b7a595aee": {}, // test case 6 } +) - expectedJson = ` -{ - "cases": [ +func TestGetAllTestCasesFiltered(t *testing.T) { + testCases := []struct { + description string + inputJSON []byte + excludeList map[string]struct{} + expectedOutput []testCase + }{ { - "description": "test case 1", - "expected": "abcde", - "input": { - "some": "inp" + description: "Filter valid json successfully", + inputJSON: []byte(validInputJSON), + excludeList: excludeList, + expectedOutput: []testCase{ + { + UUID: "alskjdb-f781-4c52-b73b-d4b867f41540", + Description: "test case 1", + Property: "test", + Input: map[string]interface{}{ + "some": "inp", + }, + Expected: "abcde", + }, + { + UUID: "dsvhsd-a151-4604-a10e-d4b867f41540", + Description: "test case 4", + Property: "some property", + Input: map[string]interface{}{ + "integers": []interface{}{64.0}, + }, + Expected: []interface{}{64.0}, + }, { + UUID: "dvthrd4-4514-4915-bac0-f7f585e0e59a", + Description: "test case 5", + Property: "some other property", + Input: map[string]interface{}{ + "bools": []interface{}{true, false}, + }, + Expected: false, + }, }, - "uuid": "alskjdb-f781-4c52-b73b-d4b867f41540" }, { - "cases": [ - { - "description": "test case 4", - "expected": [ - 64 - ], - "input": { - "integers": [ - 64 - ] - }, - "property": "some property", - "uuid": "dsvhsd-a151-4604-a10e-d4b867f41540" - } - ] + description: "Filtering invalid json should fail", + inputJSON: []byte("{\"asd"), + excludeList: excludeList, + expectedOutput: nil, }, { - "cases": [ - { - "cases": [ - { - "description": "test case 5", - "expected": false, - "input": { - "bools": [ - true, - false - ] - }, - "property": "some other property", - "uuid": "dvthrd4-4514-4915-bac0-f7f585e0e59a" - } - ], - "description": "nested cases" - } - ], - "description": "some nested cases" - } - ], - "comments": [ - "comment 123", - "comment 456" - ], - "exercise": "some-exercise-name" -} -` -) - -func TestFilterTestsJson(t *testing.T) { - tests := []struct { - description string - inputJson []byte - excludeList map[string]struct{} - expectedOutput []byte - wantErr bool - }{ + description: "invalid uuid (bool instead of string)", + inputJSON: []byte("{\"cases\":[{\"uuid\":false}]}"), + expectedOutput: nil, + }, { - description: "Filter valid json successfully", - inputJson: []byte(validInputJson), - excludeList: excludeList, - expectedOutput: []byte(strings.TrimSpace(expectedJson)), + description: "invalid description (number instead of string)", + inputJSON: []byte("{\"cases\":[{\"uuid\":\"dvthrd4-4514-4915-bac0-f7f585e0e59a\", \"description\":510}]}"), + expectedOutput: nil, }, { - description: "Filtering invalid json should fail", - inputJson: []byte("{\"asd"), - excludeList: excludeList, - wantErr: true, + description: "invalid uuid in subcase", + inputJSON: []byte("{\"cases\":[{\"cases\":{\"uuid\":false}}]}"), + expectedOutput: nil, }, } - for _, test := range tests { - t.Run(test.description, func(t *testing.T) { - output, err := filterTestsJson(test.inputJson, test.excludeList) - if test.wantErr && err == nil { + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + output, err := getAllTestCasesFiltered(tc.inputJSON, tc.excludeList) + if tc.expectedOutput == nil && err == nil { t.Errorf("expected error but got none") } - if !test.wantErr && err != nil { - t.Errorf("unexpected error %v", err) - } - if !bytes.Equal(test.expectedOutput, output) { - t.Fatalf("wrong output. expected: %s, got %s", test.expectedOutput, output) + + if tc.expectedOutput != nil { + if err != nil { + t.Errorf("unexpected error %v", err) + } + if output == nil || !reflect.DeepEqual(tc.expectedOutput, *output) { + t.Errorf("wrong output. expected: %v, got %v", tc.expectedOutput, output) + } } }) } diff --git a/gen/gen.go b/gen/gen.go index a41a7bbe9..e4a5383d6 100644 --- a/gen/gen.go +++ b/gen/gen.go @@ -3,22 +3,26 @@ package gen import ( "bytes" "encoding/json" - "errors" "fmt" "go/format" - "io/ioutil" - "log" "net/http" "os" - "os/exec" "path/filepath" "runtime" - "strings" "text/template" "time" ) -// dirMetadata is the location of the problem-specifications repository on the filesystem. +const ( + // canonicalDataURL is the URL for the raw canonical-data.json data and requires the exercise name. + canonicalDataURL = "https://raw.githubusercontent.com/exercism/problem-specifications/master/exercises/%s/canonical-data.json" + // commitsURL is the GitHub api endpoint for the commit history of canonical-data.json and requires the exercise name. + commitsURL = "https://api.github.com/repos/exercism/problem-specifications/commits?path=exercises/%s/canonical-data.json" + // defaultOrigin is the origin used in the header of cases_test.go + defaultOrigin = "exercism/problem-specifications" +) + +// problemSpecificationsDir is the location of the problem-specifications repository on the filesystem. // We're making the assumption that the problem-specifications repository has been cloned to // the same parent directory as the Exercism Go track repository. // E.g. @@ -28,68 +32,38 @@ import ( // ├── problem-specifications // └── go // -var dirMetadata string +var problemSpecificationsDir string -// dirExercise is the location that the test cases should be generated to. +// exerciseDir is the location of the exercise and also the cases_test.go file. // This assumes that the generator script lives in the .meta directory within // the exercise directory. Falls back to the present working directory. -var dirExercise string - -// genClient creates an http client with a 10 second timeout so we don't get -// stuck waiting for a response. -var genClient = &http.Client{Timeout: 10 * time.Second} +var exerciseDir string -const ( - // canonicalDataURL is the URL for the raw canonical-data.json data, - // requires exercise name. - canonicalDataURL = "https://raw.githubusercontent.com/exercism/problem-specifications/master/exercises/%s/canonical-data.json" - // commitsURL is the GitHub api endpoint for the canonical-data.json - // file commit history, requires exercise name. - commitsURL = "https://api.github.com/repos/exercism/problem-specifications/commits?path=exercises/%s/canonical-data.json" -) +// httpClient creates a http client with a timeout of 10 seconds in order not to get stuck waiting for a response. +var httpClient = &http.Client{Timeout: 10 * time.Second} -// Header tells how the test data was generated, for display in the header of -// cases_test.go +// Header tells how the test data was generated, for display in the header of cases_test.go type Header struct { - Origin string - Commit string - Version string + Commit string + Origin string } +// String generates the header for cases_test.go file. func (h Header) String() string { - s := fmt.Sprintf("// Source: %s\n", h.Origin) - if h.Commit != "" { - s += fmt.Sprintf("// Commit: %s\n", h.Commit) - } - if h.Version != "" { - s += fmt.Sprintf("// Problem Specifications Version: %s\n", h.Version) - } - return s + return fmt.Sprintf("// Source: %s\n// Commit: %s", h.Origin, h.Commit) } -func init() { - if _, path, _, ok := runtime.Caller(0); ok { - dirMetadata = filepath.Join(path, "..", "..", "..", "problem-specifications") - } -} - -// outputSource puts the src text into given fileName -// and outputs a log message with given [status]. -func outputSource(status, fileName string, src []byte) error { - ioerr := ioutil.WriteFile(fileName, src, 0666) - if ioerr != nil { - log.Printf("[FAILED] %s\n", ioerr) - return ioerr - } - log.Printf("[%s] output: %s\n", status, fileName) - return nil +type testCase struct { + UUID string `json:"uuid"` + Description string `json:"description"` + Property string `json:"property"` + Scenario string `json:"scenario"` + Input interface{} `json:"input"` + Expected interface{} `json:"expected"` } // Gen generates the exercise cases_test.go file from the relevant canonical-data.json -func Gen(exercise string, j interface{}, t *template.Template) error { - if dirMetadata == "" { - return errors.New("unable to determine current path") - } +func Gen(exercise string, tests map[string]interface{}, t *template.Template) error { // Determine the exercise directory. // Use runtime.Caller to determine path location of the generator main package. // Call frames: 0 is this frame, Gen(). @@ -98,167 +72,118 @@ func Gen(exercise string, j interface{}, t *template.Template) error { // | // V if _, path, _, ok := runtime.Caller(1); ok { - // Construct a path 2 directories higher than the path for .meta/gen.go - // and it should be exercise directory. - dirExercise = filepath.Join(path, "..", "..") + // Construct a path 2 directories higher than the path for .meta/gen.go (should be exercise directory). + exerciseDir = filepath.Join(path, "..", "..") + } else { + return fmt.Errorf("unable to determine directory of exercise %s", exercise) } - if dirExercise == "" { - dirExercise = "." + + if exerciseDir == "" { + exerciseDir = "." } + + problemSpecificationsDir = filepath.Join(exerciseDir, "..", "..", "..", "..", "problem-specifications") + jFile := filepath.Join("exercises", exercise, "canonical-data.json") - // try to find and read the local json source file - log.Printf("[LOCAL] fetching %s test data\n", exercise) - jPath, jOrigin, jCommit := getLocal(jFile) - jFilePath := filepath.Join(jPath, jFile) - if jPath != "" { - log.Printf("[LOCAL] source: %s\n", jFilePath) + fmt.Printf("[LOCAL] fetching %s test data from canonical-data.json\n", exercise) + + var header Header + jTestData, err := getLocalTestData(jFile) + if err == nil { + header, err = getLatestLocalCommitMessage(jFile) } - jSrc, err := ioutil.ReadFile(jFilePath) + if err != nil { // fetch json data remotely if there's no local file - log.Println("[LOCAL] No test data found") - log.Printf("[REMOTE] fetching %s test data\n", exercise) - jSrc, jOrigin, jCommit, err = getRemote(exercise) + fmt.Printf("[LOCAL] No test data found: %v\n", err) + fmt.Printf("[REMOTE] fetching %s test data\n", exercise) + jTestData, err = getRemoteTestData(exercise) + if err != nil { + return err + } + header, err = getRemoteCommit(exercise) if err != nil { return err } } + if !json.Valid(jTestData) { + return fmt.Errorf("[ERROR] canonical-data.json seems not to be valid json") + } + // read tests.toml file to find which test cases should be excluded - tomlFile := filepath.Join(dirExercise, ".meta", "tests.toml") - log.Printf("[LOCAL] reading tests.toml file from exercise directory %s\n", tomlFile) + tomlFile := filepath.Join(exerciseDir, ".meta", "tests.toml") + fmt.Printf("[LOCAL] reading tests.toml file from exercise directory %s\n", tomlFile) excludedTests, err := getExcludedTestCases(tomlFile) if err != nil { - return fmt.Errorf("[LOCAL] unable to read tests.toml file : %v", err) + return fmt.Errorf("[LOCAL] unable to read tests.toml file (%v)", err) } - // remove the excluded test cases from the source json - filteredJSrc, err := filterTestsJson(jSrc, excludedTests) + fmt.Println("collecting and filtering all test cases from the fetched test data") + + allTestCases, err := getAllTestCasesFiltered(jTestData, excludedTests) if err != nil { - return fmt.Errorf("unable to filter tests cases : %v", err) + return fmt.Errorf("failed to get filtered test-cases: %w", err) } - // unmarshal the json source to a Go structure - if err = json.Unmarshal(filteredJSrc, j); err != nil { - // This error message is usually enough if the problem is a wrong - // data structure defined here. Sadly it doesn't locate the error well - // in the case of invalid JSON. Use a real validator tool if you can't - // spot the problem right away. - return fmt.Errorf(`unexpected data structure: %v`, err) - } + var casesPerProperty = map[string][]testCase{} - // These fields are guaranteed to be in every problem - var commonMetadata struct { - Version string - } - if err := json.Unmarshal(filteredJSrc, &commonMetadata); err != nil { - return fmt.Errorf(`didn't contain version: %v`, err) + for _, testCase := range *allTestCases { + casesPerProperty[testCase.Property] = append(casesPerProperty[testCase.Property], testCase) } - if err := classifyByProperty(j); err != nil { - return fmt.Errorf("couldn't auto-classify based on property: %v", err) + for property, testCases := range casesPerProperty { + fmt.Printf(" > parsing cases for property %s\n", property) + marshal, err := json.Marshal(testCases) + if err != nil { + return fmt.Errorf("[ERROR] failed to marshal test cases with property %s: %w", property, err) + } + + // valueForProperty is an instance of the struct (defined in gen.go) for the current property + // and is used to unmarshal the test cases + valueForProperty, ok := tests[property] + if !ok { + return fmt.Errorf("[ERROR] failed to get struct for tests with property %s", property) + } + err = json.Unmarshal(marshal, valueForProperty) + if err != nil { + return err + } } // package up a little meta data d := struct { Header - J interface{} - }{Header{ - Origin: jOrigin, - Commit: jCommit, - Version: commonMetadata.Version, - }, j} + J map[string]interface{} + }{Header: header, J: tests} - casesFileName := filepath.Join(dirExercise, "cases_test.go") + casesFile := filepath.Join(exerciseDir, "cases_test.go") // render the Go test cases - var b bytes.Buffer - if err := t.Execute(&b, &d); err != nil { - log.Print("[ERROR] template.Execute failed. The template has a semantic error.") - return err + var out bytes.Buffer + if err := t.Execute(&out, &d); err != nil { + return fmt.Errorf("[ERROR] template.Execute failed. The template has a semantic error: %w", err) } - // clean it up - srcBuf := b.Bytes() - src, err := format.Source(srcBuf) + + formattedOut, err := format.Source(out.Bytes()) if err != nil { - log.Print("[ERROR] format.Source failed. The generated source has a syntax error.") - b.Reset() - _, _ = b.Write(srcBuf) - _, _ = b.Write([]byte( - "// !NOTE: Error during source formatting: Line:Column " + fmt.Sprint(err) + "\n")) - src = b.Bytes() + fmt.Print("[ERROR] failed to format the output with gofmt (the generated source has a syntax error)") + _, _ = out.Write([]byte("\n// !NOTE: Error during source formatting: Line:Column " + fmt.Sprint(err) + "\n")) + _, _ = out.Write(out.Bytes()) // Save the raw unformatted, error-containing source for purposes of debugging the generator. - _ = outputSource("ERROR", casesFileName, src) + _ = outputSource("ERROR", casesFile, out.Bytes()) return err } // write output file for the Go test cases. - return outputSource("SUCCESS", casesFileName, src) -} - -func getLocal(jFile string) (jPath, jOrigin, jCommit string) { - // Ideally draw from a .json which is pulled from the official problem-specifications - // repository. For development however, accept a file in current directory - // if there is no .json in source control. Also allow an override in any - // case by environment variable. - const localFile = "local file" - if jPath := os.Getenv("EXTEST"); jPath > "" { - return jPath, localFile, "" // override - } - c := exec.Command("git", "log", "-1", "--oneline", jFile) - c.Dir = dirMetadata - origin, err := c.Output() - if err != nil { - return "", localFile, "" // no source control - } - if _, err = os.Stat(filepath.Join(c.Dir, jFile)); err != nil { - return "", localFile, "" // not in source control - } - // good. return source control dir and commit. - return c.Dir, "exercism/problem-specifications", string(bytes.TrimSpace(origin)) + return outputSource("SUCCESS", casesFile, formattedOut) } -func getRemote(exercise string) (body []byte, jOrigin, jCommit string, err error) { - url := fmt.Sprintf(canonicalDataURL, exercise) - resp, err := genClient.Get(url) - if err != nil { - return []byte{}, "", "", err - } - if resp.StatusCode != http.StatusOK { - return []byte{}, "", "", fmt.Errorf("error fetching remote data: %s", resp.Status) - } - defer resp.Body.Close() - body, err = ioutil.ReadAll(resp.Body) - if err != nil { - return []byte{}, "", "", err - } - c, err := getRemoteCommit(exercise) - if err != nil { - // we always expect to have the commit in the cases_test.go - // file, so return the error if we can't fetch it - return []byte{}, "", "", err - } - log.Printf("[REMOTE] source: %s\n", url) - return body, "exercism/problem-specifications", c, nil -} - -func getRemoteCommit(exercise string) (string, error) { - type Commits struct { - Sha string - Commit struct { - Message string - } - } - resp, err := genClient.Get(fmt.Sprintf(commitsURL, exercise)) - if err != nil { - return "", err - } - defer resp.Body.Close() - var c []Commits - err = json.NewDecoder(resp.Body).Decode(&c) +// outputSource writes the src text to the given fileName and outputs a log message with given [status]. +func outputSource(status, fileName string, src []byte) error { + err := os.WriteFile(fileName, src, 0666) if err != nil { - return "", err + return fmt.Errorf("[FAILED] %q\n", err) } - // Use only 1st line of the commit message - lines := strings.SplitN(c[0].Commit.Message, "\n", 2) - return fmt.Sprintf("%s %s", c[0].Sha[0:7], lines[0]), nil + fmt.Printf("[%s] output: %s\n", status, fileName) + return nil } diff --git a/gen/local.go b/gen/local.go new file mode 100644 index 000000000..b93c41ff2 --- /dev/null +++ b/gen/local.go @@ -0,0 +1,36 @@ +package gen + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" +) + +func getLocalTestData(jFile string) ([]byte, error) { + jFilePath := filepath.Join(problemSpecificationsDir, jFile) + if _, err := os.Stat(jFilePath); err != nil { + return nil, fmt.Errorf("canonical-data.json can't be found: %w", err) + } + + fmt.Printf("[LOCAL] source: %s\n", jFilePath) + + jTestData, err := ioutil.ReadFile(jFilePath) + if err != nil { + return nil, err + } + return jTestData, nil +} + +func getLatestLocalCommitMessage(jFile string) (Header, error) { + c := exec.Command("git", "log", "-1", "--oneline", jFile) + c.Dir = problemSpecificationsDir + + msg, err := c.Output() + if err != nil { + return Header{}, fmt.Errorf("failed to determine latest commit message of %s: %w", jFile, err) + } + return Header{Commit: string(bytes.TrimSpace(msg)), Origin: defaultOrigin}, nil +} diff --git a/gen/property.go b/gen/property.go deleted file mode 100644 index 2fbc14d43..000000000 --- a/gen/property.go +++ /dev/null @@ -1,71 +0,0 @@ -package gen - -import ( - "encoding/json" - "fmt" - "reflect" -) - -const tagName = "property" - -func classifyByProperty(group interface{}) error { - v := reflect.ValueOf(group) - if v.Kind() != reflect.Ptr || v.IsNil() { - return fmt.Errorf("Can only classify pointers, not %s (%v)", v.Kind(), v) - } - - e := v.Elem() - t := e.Type() - - var raws []json.RawMessage - properties := make(map[string]reflect.Value) - - for i := 0; i < t.NumField(); i++ { - tag := t.Field(i).Tag.Get(tagName) - field := e.Field(i) - switch { - case tag == "RAW": - raws = field.Interface().([]json.RawMessage) - case tag != "": - properties[tag] = field - default: - switch field.Kind() { - case reflect.Slice: - elem := field.Type().Elem() - if elem.Kind() == reflect.Struct { - for j := 0; j < field.Len(); j++ { - err := classifyByProperty(field.Index(j).Addr().Interface()) - if err != nil { - return fmt.Errorf("Couldn't classify %s element %d: %s", t.Field(i).Name, j, err) - } - } - } - case reflect.Struct: - err := classifyByProperty(field.Addr().Interface()) - if err != nil { - return fmt.Errorf("Couldn't classify %s: %s", t.Field(i).Name, err) - } - } - } - } - - for _, raw := range raws { - var prop struct { - Property string - } - if err := json.Unmarshal(raw, &prop); err != nil { - return err - } - caseSlice, ok := properties[prop.Property] - if !ok { - return fmt.Errorf("Found property %s but no element tagged with that property", prop.Property) - } - oneCase := reflect.New(caseSlice.Type().Elem()).Interface() - if err := json.Unmarshal(raw, &oneCase); err != nil { - return err - } - caseSlice.Set(reflect.Append(caseSlice, reflect.ValueOf(oneCase).Elem())) - } - - return nil -} diff --git a/gen/remote.go b/gen/remote.go new file mode 100644 index 000000000..051a5c21c --- /dev/null +++ b/gen/remote.go @@ -0,0 +1,62 @@ +package gen + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "strings" +) + +func getRemoteTestData(exercise string) ([]byte, error) { + url := fmt.Sprintf(canonicalDataURL, exercise) + fmt.Printf("[REMOTE] source: %s\n", url) + resp, err := httpClient.Get(url) + if err != nil { + return nil, fmt.Errorf("failed to fetch remote test data: %w", err) + } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("error fetching remote data: (status-code: %s)", resp.Status) + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + return body, nil +} + +type Commit struct { + Sha string + Details struct { + Message string + } `json:"commit"` +} + +func (c Commit) Summary() string { + lines := strings.SplitN(c.Details.Message, "\n", 2) + return fmt.Sprintf("%s %s", c.Sha[0:7], lines[0]) +} + +func getRemoteCommit(exercise string) (Header, error) { + url := fmt.Sprintf(commitsURL, exercise) + fmt.Printf("[REMOTE] fetching latest commit (source: %s)\n", url) + + resp, err := httpClient.Get(url) + if err != nil { + return Header{}, fmt.Errorf("failed to fetch latest commit: %w", err) + } + defer resp.Body.Close() + var c []Commit + err = json.NewDecoder(resp.Body).Decode(&c) + if err != nil { + return Header{}, err + } + + if len(c) == 0 { + return Header{}, errors.New("no commits found") + } + + return Header{Commit: c[0].Summary(), Origin: defaultOrigin}, nil +} diff --git a/gen/toml.go b/gen/toml.go index 42082b3ad..d9bd7a347 100644 --- a/gen/toml.go +++ b/gen/toml.go @@ -2,14 +2,21 @@ package gen import ( "bufio" + "fmt" "os" "strings" ) +// getExcludedTestCases reads the file at tomlFilePath and manually parses the toml +// as no third party libraries are currently available (https://github.com/exercism/go/issues/2055). func getExcludedTestCases(tomlFilePath string) (map[string]struct{}, error) { + if _, err := os.Stat(tomlFilePath); err != nil { + return nil, fmt.Errorf("tests.toml can't be found (%q)", err) + } + file, err := os.Open(tomlFilePath) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to open %s: %w", tomlFilePath, err) } defer file.Close()