From abe0668bad9190d80b04d89e6b328d9b2d453c81 Mon Sep 17 00:00:00 2001 From: Lee Nipper Date: Mon, 22 May 2017 20:02:39 -0500 Subject: [PATCH] variable-length-quantity: create test case generator Generate two test case arrays, one for encode and one for decode, from the canonical-data.json. Update example solution to pass the tests. The API changed to use slices in order to match the canonical-data.json test cases. Retain the old functions but rename to decodeInt and encodeInt and utilize these as helpers to support the new slice based API. Update test program to use generated test case arrays. Put FAIL and PASS in test result output. Increment testVersion and targetTestVersion in example solution and test program. --- .../variable-length-quantity/.meta/gen.go | 113 ++++++++++++ .../variable-length-quantity/cases_test.go | 161 ++++++++++++++++++ exercises/variable-length-quantity/example.go | 65 +++++-- .../variable_length_quantity_test.go | 50 +++--- 4 files changed, 347 insertions(+), 42 deletions(-) create mode 100644 exercises/variable-length-quantity/.meta/gen.go create mode 100644 exercises/variable-length-quantity/cases_test.go diff --git a/exercises/variable-length-quantity/.meta/gen.go b/exercises/variable-length-quantity/.meta/gen.go new file mode 100644 index 000000000..5b71a941d --- /dev/null +++ b/exercises/variable-length-quantity/.meta/gen.go @@ -0,0 +1,113 @@ +package main + +import ( + "log" + "text/template" + + "../../../gen" +) + +func main() { + t := template.New("").Funcs(template.FuncMap{ + "GroupComment": GroupComment, + "byteSlice": byteSlice, + "lengthSlice": lengthSlice, + }) + t, err := t.Parse(tmpl) + if err != nil { + log.Fatal(err) + } + var j js + if err := gen.Gen("variable-length-quantity", &j, t); err != nil { + log.Fatal(err) + } +} + +// byteSlice converts a slice of uint32 to a byte slice. +func byteSlice(ns []uint32) []byte { + b := make([]byte, len(ns)) + for i, n := range ns { + b[i] = byte(n) + } + return b +} + +// lengthSlice returns the length of given slice. +func lengthSlice(ns []uint32) int { + return len(ns) +} + +// The JSON structure we expect to be able to unmarshal into +type js struct { + Groups []TestGroup `json:"Cases"` +} + +type TestGroup struct { + Description string + Cases []OneCase +} + +type OneCase struct { + Description string + Property string // "encode" or "decode" + Input []uint32 // supports both []byte and []uint32 in JSON. + Expected []uint32 // supports []byte, []uint32, or null in JSON. +} + +// PropertyMatch returns true when given test case c has .Property field matching property; +// this serves as a filter to put test cases with "like" property into the same group. +func (c OneCase) PropertyMatch(property string) bool { return c.Property == property } + +// GroupComment looks in each of the test case groups to find the +// group for which every test case has the .Property matching given property; +// it returns the .Description field for the matching property group, +// or a 'Note: ...' if no test group consistently matches given property. +func GroupComment(groups []TestGroup, property string) string { + for _, group := range groups { + propertyGroupMatch := true + for _, testcase := range group.Cases { + if !testcase.PropertyMatch(property) { + propertyGroupMatch = false + break + } + } + if propertyGroupMatch { + return group.Description + } + } + return "Note: Apparent inconsistent use of \"property\": \"" + property + "\" within test case group!" +} + +// template applied to above data structure generates the Go test cases +var tmpl = `package variablelengthquantity + +{{.Header}} + +// {{GroupComment .J.Groups "encode"}} +var encodeTestCases = []struct { + description string + input []uint32 + output []byte +}{ {{range .J.Groups}} {{range .Cases}} + {{if .PropertyMatch "encode"}} { + {{printf "%q" .Description}}, + {{printf "%#v" .Input }}, + {{byteSlice .Expected | printf "%#v" }}, + },{{- end}}{{end}}{{end}} +} + +// {{GroupComment .J.Groups "decode"}} +var decodeTestCases = []struct { + description string + input []byte + output []uint32 // nil slice indicates error expected. + size int +}{ {{range .J.Groups}} {{range .Cases}} + {{if .PropertyMatch "decode"}} { + {{printf "%q" .Description}}, + {{byteSlice .Input | printf "%#v" }}, + {{printf "%#v" .Expected }}, + {{lengthSlice .Input}}, + },{{- end}}{{end}}{{end}} +} +` diff --git a/exercises/variable-length-quantity/cases_test.go b/exercises/variable-length-quantity/cases_test.go new file mode 100644 index 000000000..d4026fab4 --- /dev/null +++ b/exercises/variable-length-quantity/cases_test.go @@ -0,0 +1,161 @@ +package variablelengthquantity + +// Source: exercism/x-common +// Commit: d6a62f7 variable-length-quantity: Fix canonical-data.json formatting +// x-common version: 1.0.0 + +// Encode a series of integers, producing a series of bytes. +var encodeTestCases = []struct { + description string + input []uint32 + output []byte +}{ + { + "zero", + []uint32{0x0}, + []byte{0x0}, + }, + { + "arbitrary single byte", + []uint32{0x40}, + []byte{0x40}, + }, + { + "largest single byte", + []uint32{0x7f}, + []byte{0x7f}, + }, + { + "smallest double byte", + []uint32{0x80}, + []byte{0x81, 0x0}, + }, + { + "arbitrary double byte", + []uint32{0x2000}, + []byte{0xc0, 0x0}, + }, + { + "largest double byte", + []uint32{0x3fff}, + []byte{0xff, 0x7f}, + }, + { + "smallest triple byte", + []uint32{0x4000}, + []byte{0x81, 0x80, 0x0}, + }, + { + "arbitrary triple byte", + []uint32{0x100000}, + []byte{0xc0, 0x80, 0x0}, + }, + { + "largest triple byte", + []uint32{0x1fffff}, + []byte{0xff, 0xff, 0x7f}, + }, + { + "smallest quadruple byte", + []uint32{0x200000}, + []byte{0x81, 0x80, 0x80, 0x0}, + }, + { + "arbitrary quadruple byte", + []uint32{0x8000000}, + []byte{0xc0, 0x80, 0x80, 0x0}, + }, + { + "largest quadruple byte", + []uint32{0xfffffff}, + []byte{0xff, 0xff, 0xff, 0x7f}, + }, + { + "smallest quintuple byte", + []uint32{0x10000000}, + []byte{0x81, 0x80, 0x80, 0x80, 0x0}, + }, + { + "arbitrary quintuple byte", + []uint32{0xff000000}, + []byte{0x8f, 0xf8, 0x80, 0x80, 0x0}, + }, + { + "maximum 32-bit integer input", + []uint32{0xffffffff}, + []byte{0x8f, 0xff, 0xff, 0xff, 0x7f}, + }, + { + "two single-byte values", + []uint32{0x40, 0x7f}, + []byte{0x40, 0x7f}, + }, + { + "two multi-byte values", + []uint32{0x4000, 0x123456}, + []byte{0x81, 0x80, 0x0, 0xc8, 0xe8, 0x56}, + }, + { + "many multi-byte values", + []uint32{0x2000, 0x123456, 0xfffffff, 0x0, 0x3fff, 0x4000}, + []byte{0xc0, 0x0, 0xc8, 0xe8, 0x56, 0xff, 0xff, 0xff, 0x7f, 0x0, 0xff, 0x7f, 0x81, 0x80, 0x0}, + }, +} + +// Decode a series of bytes, producing a series of integers. +var decodeTestCases = []struct { + description string + input []byte + output []uint32 // nil slice indicates error expected. + size int +}{ + + { + "one byte", + []byte{0x7f}, + []uint32{0x7f}, + 1, + }, + { + "two bytes", + []byte{0xc0, 0x0}, + []uint32{0x2000}, + 2, + }, + { + "three bytes", + []byte{0xff, 0xff, 0x7f}, + []uint32{0x1fffff}, + 3, + }, + { + "four bytes", + []byte{0x81, 0x80, 0x80, 0x0}, + []uint32{0x200000}, + 4, + }, + { + "maximum 32-bit integer", + []byte{0x8f, 0xff, 0xff, 0xff, 0x7f}, + []uint32{0xffffffff}, + 5, + }, + { + "incomplete sequence causes error", + []byte{0xff}, + []uint32(nil), + 1, + }, + { + "incomplete sequence causes error, even if value is zero", + []byte{0x80}, + []uint32(nil), + 1, + }, + { + "multiple values", + []byte{0xc0, 0x0, 0xc8, 0xe8, 0x56, 0xff, 0xff, 0xff, 0x7f, 0x0, 0xff, 0x7f, 0x81, 0x80, 0x0}, + []uint32{0x2000, 0x123456, 0xfffffff, 0x0, 0x3fff, 0x4000}, + 15, + }, +} diff --git a/exercises/variable-length-quantity/example.go b/exercises/variable-length-quantity/example.go index 685217eab..530cd075d 100644 --- a/exercises/variable-length-quantity/example.go +++ b/exercises/variable-length-quantity/example.go @@ -1,9 +1,13 @@ package variablelengthquantity -const testVersion = 2 +import "errors" -// EncodeVarint returns the varint encoding of x. -func EncodeVarint(x uint32) []byte { +const testVersion = 3 + +var ErrUnterminatedSequence = errors.New("unterminated sequence") + +// encodeInt returns the varint encoding of x. +func encodeInt(x uint32) []byte { if x>>7 == 0 { return []byte{ byte(x), @@ -25,7 +29,17 @@ func EncodeVarint(x uint32) []byte { } } + if x>>28 == 0 { + return []byte{ + byte(0x80 | x>>21), + byte(0x80 | x>>14), + byte(0x80 | x>>6), + byte(127 & x), + } + } + return []byte{ + byte(0x80 | x>>28), byte(0x80 | x>>21), byte(0x80 | x>>14), byte(0x80 | x>>7), @@ -33,26 +47,55 @@ func EncodeVarint(x uint32) []byte { } } -// DecodeVarint reads a varint-encoded integer from the slice. +// decodeInt reads a varint-encoded integer from the slice. // It returns the integer and the number of bytes consumed, or // zero if there is not enough. -func DecodeVarint(buf []byte) (x uint32, n int) { +func decodeInt(buf []byte) (x uint32, n int, err error) { if len(buf) < 1 { - return 0, 0 + return 0, 0, nil } - if buf[0] <= 0x80 { - return uint32(buf[0]), 1 + if buf[0] < 0x80 { + return uint32(buf[0]), 1, nil } var b byte for n, b = range buf { x = x << 7 - x |= uint32(b) & 0x7F + x |= uint32(b) & 0x7f if (b & 0x80) == 0 { - return x, n + 1 + return x, n + 1, nil } } - return x, 0 + return x, 0, ErrUnterminatedSequence +} + +// EncodeVarint encodes a slice of uint32 into a var-int encoded bytes. +func EncodeVarint(xa []uint32) []byte { + result := make([]byte, 0) + for _, x := range xa { + result = append(result, encodeInt(x)...) + } + return result +} + +// DecodeVarint decodes a buffer of var-int encoded values into +// a slice of uint32 values; an error is returned if buf doesn't +// decode var-int successfully. +func DecodeVarint(buf []byte) (ra []uint32, n int, err error) { + if len(buf) == 0 { + return []uint32{0}, 1, nil + } + usedBytes := 0 + ra = make([]uint32, 0) + for usedBytes < len(buf) { + r, nUsed, err := decodeInt(buf[usedBytes:]) + if err != nil { + return nil, usedBytes, err + } + ra = append(ra, r) + usedBytes += nUsed + } + return ra, usedBytes, nil } diff --git a/exercises/variable-length-quantity/variable_length_quantity_test.go b/exercises/variable-length-quantity/variable_length_quantity_test.go index e31326cee..bee3e7b5c 100644 --- a/exercises/variable-length-quantity/variable_length_quantity_test.go +++ b/exercises/variable-length-quantity/variable_length_quantity_test.go @@ -2,28 +2,11 @@ package variablelengthquantity import ( "bytes" + "reflect" "testing" ) -const targetTestVersion = 2 - -var testCases = []struct { - input []byte - output uint32 - size int -}{ - 0: {[]byte{0x7F}, 127, 1}, - 1: {[]byte{0x81, 0x00}, 128, 2}, - 2: {[]byte{0xC0, 0x00}, 8192, 2}, - 3: {[]byte{0xFF, 0x7F}, 16383, 2}, - 4: {[]byte{0x81, 0x80, 0x00}, 16384, 3}, - 5: {[]byte{0xFF, 0xFF, 0x7F}, 2097151, 3}, - 6: {[]byte{0x81, 0x80, 0x80, 0x00}, 2097152, 4}, - 7: {[]byte{0xC0, 0x80, 0x80, 0x00}, 134217728, 4}, - 8: {[]byte{0xFF, 0xFF, 0xFF, 0x7F}, 268435455, 4}, - 9: {[]byte{0x82, 0x00}, 256, 2}, - 10: {[]byte{0x81, 0x10}, 144, 2}, -} +const targetTestVersion = 3 func TestTestVersion(t *testing.T) { if testVersion != targetTestVersion { @@ -32,24 +15,29 @@ func TestTestVersion(t *testing.T) { } func TestDecodeVarint(t *testing.T) { - for i, tc := range testCases { - o, size := DecodeVarint(tc.input) - if o != tc.output { - t.Fatalf("FAIL: case %d - expected %d got %d\n", i, tc.output, o) + for i, tc := range decodeTestCases { + o, size, err := DecodeVarint(tc.input) + if err != nil { + var _ error = err + if tc.output != nil { + t.Fatalf("FAIL: case %d | %s\nexpected %#v got error: %q\n", i, tc.description, tc.output, err) + } + } else if tc.output == nil { + t.Fatalf("FAIL: case %d | %s\nexpected error, got %#v\n", i, tc.description, o) + } else if !reflect.DeepEqual(o, tc.output) { + t.Fatalf("FAIL: case %d | %s\nexpected\t%#v\ngot\t\t%#v\n", i, tc.description, tc.output, o) } else if size != tc.size { - t.Fatalf("FAIL: case %d - expected encoding size of %d bytes\ngot %d bytes\n", i, tc.size, size) - } else { - t.Logf("PASS: case %d - %#v\n", i, tc.input) + t.Fatalf("FAIL: case %d | %s\n expected encoding size of %d bytes\ngot %d bytes\n", i, tc.description, tc.size, size) } + t.Logf("PASS: case %d | %s\n", i, tc.description) } } func TestEncodeVarint(t *testing.T) { - for i, tc := range testCases { - if encoded := EncodeVarint(tc.output); bytes.Compare(encoded, tc.input) != 0 { - t.Fatalf("FAIL: case %d - %d \nexpected\t%#v\ngot\t\t%#v\n", i, tc.output, tc.input, encoded) - } else { - t.Logf("PASS: case %d - %#v\n", i, tc.input) + for i, tc := range encodeTestCases { + if encoded := EncodeVarint(tc.input); bytes.Compare(encoded, tc.output) != 0 { + t.Fatalf("FAIL: case %d | %s\nexpected\t%#v\ngot\t\t%#v\n", i, tc.description, tc.output, encoded) } + t.Logf("PASS: case %d | %s\n", i, tc.description) } }