diff --git a/exercises/variable-length-quantity/.meta/gen.go b/exercises/variable-length-quantity/.meta/gen.go new file mode 100644 index 000000000..9981fefe8 --- /dev/null +++ b/exercises/variable-length-quantity/.meta/gen.go @@ -0,0 +1,115 @@ +// +build ignore + +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 + 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..a17de84db --- /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 + 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..d7cb6869f 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,28 @@ 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 { + if len(tc.output) != 0 { + t.Fatalf("FAIL: case %d | %s\nexpected %#v got error: %q\n", i, tc.description, tc.output, err) + } + } else if len(tc.output) == 0 { + 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) } }