test(html): guard JSON encoder against update field drift
Add a reflection-filled test comparing appendUpdateJSON with json.Marshal and pinning field counts. Invalid color kinds now return the same json.MarshalerError text as before. Fix dead frame.Set and U+FFFD comment; compare invalid UTF-8 bytes directly.
- commit
- 6bd019c91964a4e1cc8bb6d257320dcc39867a20
- author
- brice · · 2026-10-02 09:24 UTC
- parent
- b6d6950
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +108 -19
M html/encode.go +9 -3
| @@ -1,7 +1,9 @@ | |||
| 1 | 1 | package html | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "encoding/json" | |
| 4 | 5 | "fmt" | |
| 6 | + | "reflect" | |
| 5 | 7 | "strconv" | |
| 6 | 8 | "unicode/utf8" | |
| 7 | 9 | ) | |
| @@ -155,7 +157,11 @@ func appendColorJSON(dst []byte, color Color) ([]byte, error) { | |||
| 155 | 157 | dst = strconv.AppendUint(dst, uint64(color.RGB.B), 10) | |
| 156 | 158 | return append(dst, "}}"...), nil | |
| 157 | 159 | default: | |
| 158 | - | return nil, fmt.Errorf("html: invalid color kind %d", color.Kind) | |
| 160 | + | // Same error value encoding/json produced via Color.MarshalJSON. | |
| 161 | + | return nil, &json.MarshalerError{ | |
| 162 | + | Type: reflect.TypeFor[*Color](), | |
| 163 | + | Err: fmt.Errorf("html: invalid color kind %d", color.Kind), | |
| 164 | + | } | |
| 159 | 165 | } | |
| 160 | 166 | } | |
| 161 | 167 | ||
| @@ -163,8 +169,8 @@ const hexDigits = "0123456789abcdef" | |||
| 163 | 169 | ||
| 164 | 170 | // appendJSONString appends s as a JSON string using encoding/json's default | |
| 165 | 171 | // escaping: quotes, backslashes, control characters, <, >, & and U+2028/2029 | |
| 166 | - | // are escaped, and invalid UTF-8 becomes a literal U+FFFD (as encoding/json | |
| 167 | - | // does on this toolchain). Cell text is always valid UTF-8 here. | |
| 172 | + | // are escaped, and each invalid UTF-8 byte becomes U+FFFD written as the | |
| 173 | + | // literal character, byte-identical to encoding/json on this toolchain. | |
| 168 | 174 | func appendJSONString(dst []byte, s string) []byte { | |
| 169 | 175 | dst = append(dst, '"') | |
| 170 | 176 | start := 0 | |
| @@ -1,7 +1,9 @@ | |||||
| 1 | package html | 1 | package html | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "encoding/json" | |||
| 4 | "fmt" | 5 | "fmt" | ||
| 6 | + | "reflect" | |||
| 5 | "strconv" | 7 | "strconv" | ||
| 6 | "unicode/utf8" | 8 | "unicode/utf8" | ||
| 7 | ) | 9 | ) | ||
| @@ -155,7 +157,11 @@ func appendColorJSON(dst []byte, color Color) ([]byte, error) { | |||||
| 155 | dst = strconv.AppendUint(dst, uint64(color.RGB.B), 10) | 157 | dst = strconv.AppendUint(dst, uint64(color.RGB.B), 10) | ||
| 156 | return append(dst, "}}"...), nil | 158 | return append(dst, "}}"...), nil | ||
| 157 | default: | 159 | default: | ||
| 158 | - | return nil, fmt.Errorf("html: invalid color kind %d", color.Kind) | 160 | + | // Same error value encoding/json produced via Color.MarshalJSON. |
| 161 | + | return nil, &json.MarshalerError{ | |||
| 162 | + | Type: reflect.TypeFor[*Color](), | |||
| 163 | + | Err: fmt.Errorf("html: invalid color kind %d", color.Kind), | |||
| 164 | + | } | |||
| 159 | } | 165 | } | ||
| 160 | } | 166 | } | ||
| 161 | 167 | ||||
| @@ -163,8 +169,8 @@ const hexDigits = "0123456789abcdef" | |||||
| 163 | 169 | ||||
| 164 | // appendJSONString appends s as a JSON string using encoding/json's default | 170 | // appendJSONString appends s as a JSON string using encoding/json's default | ||
| 165 | // escaping: quotes, backslashes, control characters, <, >, & and U+2028/2029 | 171 | // escaping: quotes, backslashes, control characters, <, >, & and U+2028/2029 | ||
| 166 | - | // are escaped, and invalid UTF-8 becomes a literal U+FFFD (as encoding/json | 172 | + | // are escaped, and each invalid UTF-8 byte becomes U+FFFD written as the |
| 167 | - | // does on this toolchain). Cell text is always valid UTF-8 here. | 173 | + | // literal character, byte-identical to encoding/json on this toolchain. |
| 168 | func appendJSONString(dst []byte, s string) []byte { | 174 | func appendJSONString(dst []byte, s string) []byte { | ||
| 169 | dst = append(dst, '"') | 175 | dst = append(dst, '"') | ||
| 170 | start := 0 | 176 | start := 0 | ||
M html/encode_test.go +99 -16
| @@ -3,6 +3,7 @@ package html | |||
| 3 | 3 | import ( | |
| 4 | 4 | "encoding/json" | |
| 5 | 5 | "math/rand" | |
| 6 | + | "reflect" | |
| 6 | 7 | "testing" | |
| 7 | 8 | ||
| 8 | 9 | "github.com/bnema/vev-vt/core" | |
| @@ -24,17 +25,13 @@ func TestAppendJSONStringMatchesEncodingJSON(t *testing.T) { | |||
| 24 | 25 | } | |
| 25 | 26 | } | |
| 26 | 27 | ||
| 27 | - | // Invalid UTF-8 never reaches the encoder (cells are validated), and the | |
| 28 | - | // escaped form of U+FFFD differs between encoding/json implementations, so | |
| 29 | - | // compare decoded values only. | |
| 30 | - | func TestAppendJSONStringInvalidUTF8DecodesLikeEncodingJSON(t *testing.T) { | |
| 31 | - | for _, text := range []string{"\xff", "a\xc3", "\xed\xa0\x80"} { | |
| 28 | + | // Invalid UTF-8 never reaches the encoder (cells are validated), but the | |
| 29 | + | // escaping must still match encoding/json byte for byte. | |
| 30 | + | func TestAppendJSONStringInvalidUTF8MatchesEncodingJSON(t *testing.T) { | |
| 31 | + | for _, text := range []string{"\xff", "a\xc3", "\xed\xa0\x80", "\xe2\x80", "ok\xf0\x9f"} { | |
| 32 | 32 | want, err := json.Marshal(text) | |
| 33 | 33 | require.NoError(t, err) | |
| 34 | - | var wantDecoded, gotDecoded string | |
| 35 | - | require.NoError(t, json.Unmarshal(want, &wantDecoded)) | |
| 36 | - | require.NoError(t, json.Unmarshal(appendJSONString(nil, text), &gotDecoded)) | |
| 37 | - | require.Equal(t, wantDecoded, gotDecoded) | |
| 34 | + | require.Equal(t, string(want), string(appendJSONString(nil, text)), "%q", text) | |
| 38 | 35 | } | |
| 39 | 36 | } | |
| 40 | 37 | ||
| @@ -103,9 +100,9 @@ func TestAppendUpdateJSONMatchesEncodingJSON(t *testing.T) { | |||
| 103 | 100 | func TestAppendUpdateJSONRejectsInvalidColorKind(t *testing.T) { | |
| 104 | 101 | update := Update{Styles: []Style{{Foreground: Color{Kind: 9}}}} | |
| 105 | 102 | _, err := appendUpdateJSON(nil, update) | |
| 106 | - | require.ErrorContains(t, err, "invalid color kind") | |
| 107 | - | _, err = json.Marshal(update) | |
| 108 | - | require.Error(t, err) | |
| 103 | + | _, want := json.Marshal(update) | |
| 104 | + | require.Error(t, want) | |
| 105 | + | require.EqualError(t, err, want.Error()) | |
| 109 | 106 | } | |
| 110 | 107 | ||
| 111 | 108 | func TestPreparedJSONMatchesEncodingJSONOfUpdate(t *testing.T) { | |
| @@ -117,10 +114,10 @@ func TestPreparedJSONMatchesEncodingJSONOfUpdate(t *testing.T) { | |||
| 117 | 114 | frame.Set(0, 0, core.Cell{Rune: '<', Style: style}) | |
| 118 | 115 | frame.Set(1, 0, core.Cell{Rune: '界', Style: core.DefaultStyle()}) | |
| 119 | 116 | frame.Set(2, 0, core.Cell{Continuation: true, Style: core.DefaultStyle()}) | |
| 120 | - | frame.Set(4, 1, core.Cell{Rune: '"', Style: style}) | |
| 121 | - | frame.Set(5, 1, core.Cell{Rune: '😀', Style: core.DefaultStyle()}) | |
| 122 | - | // width-2 rune at the last column is invalid; keep it narrow. | |
| 123 | - | frame.Set(5, 1, core.Cell{Rune: '&', Style: core.DefaultStyle()}) | |
| 117 | + | frame.Set(3, 0, core.Cell{Rune: '😀', Style: core.DefaultStyle()}) | |
| 118 | + | frame.Set(4, 0, core.Cell{Continuation: true, Style: core.DefaultStyle()}) | |
| 119 | + | frame.Set(5, 0, core.Cell{Rune: '&', Style: core.DefaultStyle()}) | |
| 120 | + | frame.Set(0, 1, core.Cell{Rune: '"', Style: style}) | |
| 124 | 121 | ||
| 125 | 122 | renderer, err := New(Options{}) | |
| 126 | 123 | require.NoError(t, err) | |
| @@ -129,4 +126,90 @@ func TestPreparedJSONMatchesEncodingJSONOfUpdate(t *testing.T) { | |||
| 129 | 126 | want, err := json.Marshal(prepared.tx.update) | |
| 130 | 127 | require.NoError(t, err) | |
| 131 | 128 | require.Equal(t, string(want), string(prepared.JSON())) | |
| 129 | + | require.Contains(t, string(prepared.JSON()), "😀") | |
| 130 | + | } | |
| 131 | + | ||
| 132 | + | // fillAll sets every field reachable from v to a distinctive non-zero value | |
| 133 | + | // (negative for signed integers) so a field added to the update types without | |
| 134 | + | // a matching encoder change shows up as a JSON mismatch. | |
| 135 | + | func fillAll(t *testing.T, v reflect.Value, colorKind ColorKind, counter *int) { | |
| 136 | + | t.Helper() | |
| 137 | + | *counter++ | |
| 138 | + | n := *counter | |
| 139 | + | if v.Type() == reflect.TypeOf(Color{}) { | |
| 140 | + | v.FieldByName("Kind").SetUint(uint64(colorKind)) | |
| 141 | + | v.FieldByName("Index").SetUint(uint64(100 + n%100)) | |
| 142 | + | fillAll(t, v.FieldByName("RGB"), colorKind, counter) | |
| 143 | + | return | |
| 144 | + | } | |
| 145 | + | switch v.Kind() { | |
| 146 | + | case reflect.Bool: | |
| 147 | + | v.SetBool(true) | |
| 148 | + | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: | |
| 149 | + | v.SetInt(-int64(n) - 1) | |
| 150 | + | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: | |
| 151 | + | v.SetUint(uint64(1 + n%120)) | |
| 152 | + | case reflect.String: | |
| 153 | + | v.SetString("t<&>\"\\界\u2028" + string(rune('a'+n%26))) | |
| 154 | + | case reflect.Slice: | |
| 155 | + | v.Set(reflect.MakeSlice(v.Type(), 2, 2)) | |
| 156 | + | for i := range 2 { | |
| 157 | + | fillAll(t, v.Index(i), colorKind, counter) | |
| 158 | + | } | |
| 159 | + | case reflect.Struct: | |
| 160 | + | for i := range v.NumField() { | |
| 161 | + | require.True(t, v.Field(i).CanSet(), "%s.%s must be settable", v.Type(), v.Type().Field(i).Name) | |
| 162 | + | fillAll(t, v.Field(i), colorKind, counter) | |
| 163 | + | } | |
| 164 | + | default: | |
| 165 | + | t.Fatalf("fillAll: unsupported kind %s in %s", v.Kind(), v.Type()) | |
| 166 | + | } | |
| 167 | + | } | |
| 168 | + | ||
| 169 | + | func requireNoZeroFields(t *testing.T, v reflect.Value) { | |
| 170 | + | t.Helper() | |
| 171 | + | switch v.Kind() { | |
| 172 | + | case reflect.Struct: | |
| 173 | + | for i := range v.NumField() { | |
| 174 | + | requireNoZeroFields(t, v.Field(i)) | |
| 175 | + | } | |
| 176 | + | case reflect.Slice: | |
| 177 | + | require.NotZero(t, v.Len()) | |
| 178 | + | for i := range v.Len() { | |
| 179 | + | requireNoZeroFields(t, v.Index(i)) | |
| 180 | + | } | |
| 181 | + | default: | |
| 182 | + | require.False(t, v.IsZero(), "zero %s left in filled value", v.Type()) | |
| 183 | + | } | |
| 184 | + | } | |
| 185 | + | ||
| 186 | + | // TestEncoderCoversEveryUpdateField fails when a field is added to (or removed | |
| 187 | + | // from) the update types without updating appendUpdateJSON and this test. | |
| 188 | + | func TestEncoderCoversEveryUpdateField(t *testing.T) { | |
| 189 | + | for typ, fields := range map[reflect.Type]int{ | |
| 190 | + | reflect.TypeOf(Update{}): 7, | |
| 191 | + | reflect.TypeOf(RowUpdate{}): 2, | |
| 192 | + | reflect.TypeOf(CellUpdate{}): 4, | |
| 193 | + | reflect.TypeOf(Style{}): 11, | |
| 194 | + | reflect.TypeOf(Cursor{}): 5, | |
| 195 | + | reflect.TypeOf(RGB{}): 3, | |
| 196 | + | reflect.TypeOf(Color{}): 3, | |
| 197 | + | } { | |
| 198 | + | require.Equal(t, fields, typ.NumField(), "%s field count changed: update appendUpdateJSON", typ) | |
| 199 | + | } | |
| 200 | + | ||
| 201 | + | for _, kind := range []ColorKind{ColorDefault, ColorIndexed, ColorRGB} { | |
| 202 | + | var update Update | |
| 203 | + | counter := 0 | |
| 204 | + | fillAll(t, reflect.ValueOf(&update).Elem(), kind, &counter) | |
| 205 | + | update.Cursor.Style = 6 // keep within the documented DECSCUSR range | |
| 206 | + | if kind != ColorDefault { | |
| 207 | + | requireNoZeroFields(t, reflect.ValueOf(update)) | |
| 208 | + | } | |
| 209 | + | want, err := json.Marshal(update) | |
| 210 | + | require.NoError(t, err) | |
| 211 | + | got, err := appendUpdateJSON(nil, update) | |
| 212 | + | require.NoError(t, err) | |
| 213 | + | require.Equal(t, string(want), string(got), "color kind %d", kind) | |
| 214 | + | } | |
| 132 | 215 | } | |
| @@ -3,6 +3,7 @@ package html | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "encoding/json" | 4 | "encoding/json" | ||
| 5 | "math/rand" | 5 | "math/rand" | ||
| 6 | + | "reflect" | |||
| 6 | "testing" | 7 | "testing" | ||
| 7 | 8 | ||||
| 8 | "github.com/bnema/vev-vt/core" | 9 | "github.com/bnema/vev-vt/core" | ||
| @@ -24,17 +25,13 @@ func TestAppendJSONStringMatchesEncodingJSON(t *testing.T) { | |||||
| 24 | } | 25 | } | ||
| 25 | } | 26 | } | ||
| 26 | 27 | ||||
| 27 | - | // Invalid UTF-8 never reaches the encoder (cells are validated), and the | 28 | + | // Invalid UTF-8 never reaches the encoder (cells are validated), but the |
| 28 | - | // escaped form of U+FFFD differs between encoding/json implementations, so | 29 | + | // escaping must still match encoding/json byte for byte. |
| 29 | - | // compare decoded values only. | 30 | + | func TestAppendJSONStringInvalidUTF8MatchesEncodingJSON(t *testing.T) { |
| 30 | - | func TestAppendJSONStringInvalidUTF8DecodesLikeEncodingJSON(t *testing.T) { | 31 | + | for _, text := range []string{"\xff", "a\xc3", "\xed\xa0\x80", "\xe2\x80", "ok\xf0\x9f"} { |
| 31 | - | for _, text := range []string{"\xff", "a\xc3", "\xed\xa0\x80"} { | |||
| 32 | want, err := json.Marshal(text) | 32 | want, err := json.Marshal(text) | ||
| 33 | require.NoError(t, err) | 33 | require.NoError(t, err) | ||
| 34 | - | var wantDecoded, gotDecoded string | 34 | + | require.Equal(t, string(want), string(appendJSONString(nil, text)), "%q", text) |
| 35 | - | require.NoError(t, json.Unmarshal(want, &wantDecoded)) | |||
| 36 | - | require.NoError(t, json.Unmarshal(appendJSONString(nil, text), &gotDecoded)) | |||
| 37 | - | require.Equal(t, wantDecoded, gotDecoded) | |||
| 38 | } | 35 | } | ||
| 39 | } | 36 | } | ||
| 40 | 37 | ||||
| @@ -103,9 +100,9 @@ func TestAppendUpdateJSONMatchesEncodingJSON(t *testing.T) { | |||||
| 103 | func TestAppendUpdateJSONRejectsInvalidColorKind(t *testing.T) { | 100 | func TestAppendUpdateJSONRejectsInvalidColorKind(t *testing.T) { | ||
| 104 | update := Update{Styles: []Style{{Foreground: Color{Kind: 9}}}} | 101 | update := Update{Styles: []Style{{Foreground: Color{Kind: 9}}}} | ||
| 105 | _, err := appendUpdateJSON(nil, update) | 102 | _, err := appendUpdateJSON(nil, update) | ||
| 106 | - | require.ErrorContains(t, err, "invalid color kind") | 103 | + | _, want := json.Marshal(update) |
| 107 | - | _, err = json.Marshal(update) | 104 | + | require.Error(t, want) |
| 108 | - | require.Error(t, err) | 105 | + | require.EqualError(t, err, want.Error()) |
| 109 | } | 106 | } | ||
| 110 | 107 | ||||
| 111 | func TestPreparedJSONMatchesEncodingJSONOfUpdate(t *testing.T) { | 108 | func TestPreparedJSONMatchesEncodingJSONOfUpdate(t *testing.T) { | ||
| @@ -117,10 +114,10 @@ func TestPreparedJSONMatchesEncodingJSONOfUpdate(t *testing.T) { | |||||
| 117 | frame.Set(0, 0, core.Cell{Rune: '<', Style: style}) | 114 | frame.Set(0, 0, core.Cell{Rune: '<', Style: style}) | ||
| 118 | frame.Set(1, 0, core.Cell{Rune: '界', Style: core.DefaultStyle()}) | 115 | frame.Set(1, 0, core.Cell{Rune: '界', Style: core.DefaultStyle()}) | ||
| 119 | frame.Set(2, 0, core.Cell{Continuation: true, Style: core.DefaultStyle()}) | 116 | frame.Set(2, 0, core.Cell{Continuation: true, Style: core.DefaultStyle()}) | ||
| 120 | - | frame.Set(4, 1, core.Cell{Rune: '"', Style: style}) | 117 | + | frame.Set(3, 0, core.Cell{Rune: '😀', Style: core.DefaultStyle()}) |
| 121 | - | frame.Set(5, 1, core.Cell{Rune: '😀', Style: core.DefaultStyle()}) | 118 | + | frame.Set(4, 0, core.Cell{Continuation: true, Style: core.DefaultStyle()}) |
| 122 | - | // width-2 rune at the last column is invalid; keep it narrow. | 119 | + | frame.Set(5, 0, core.Cell{Rune: '&', Style: core.DefaultStyle()}) |
| 123 | - | frame.Set(5, 1, core.Cell{Rune: '&', Style: core.DefaultStyle()}) | 120 | + | frame.Set(0, 1, core.Cell{Rune: '"', Style: style}) |
| 124 | 121 | ||||
| 125 | renderer, err := New(Options{}) | 122 | renderer, err := New(Options{}) | ||
| 126 | require.NoError(t, err) | 123 | require.NoError(t, err) | ||
| @@ -129,4 +126,90 @@ func TestPreparedJSONMatchesEncodingJSONOfUpdate(t *testing.T) { | |||||
| 129 | want, err := json.Marshal(prepared.tx.update) | 126 | want, err := json.Marshal(prepared.tx.update) | ||
| 130 | require.NoError(t, err) | 127 | require.NoError(t, err) | ||
| 131 | require.Equal(t, string(want), string(prepared.JSON())) | 128 | require.Equal(t, string(want), string(prepared.JSON())) | ||
| 129 | + | require.Contains(t, string(prepared.JSON()), "😀") | |||
| 130 | + | } | |||
| 131 | + | ||||
| 132 | + | // fillAll sets every field reachable from v to a distinctive non-zero value | |||
| 133 | + | // (negative for signed integers) so a field added to the update types without | |||
| 134 | + | // a matching encoder change shows up as a JSON mismatch. | |||
| 135 | + | func fillAll(t *testing.T, v reflect.Value, colorKind ColorKind, counter *int) { | |||
| 136 | + | t.Helper() | |||
| 137 | + | *counter++ | |||
| 138 | + | n := *counter | |||
| 139 | + | if v.Type() == reflect.TypeOf(Color{}) { | |||
| 140 | + | v.FieldByName("Kind").SetUint(uint64(colorKind)) | |||
| 141 | + | v.FieldByName("Index").SetUint(uint64(100 + n%100)) | |||
| 142 | + | fillAll(t, v.FieldByName("RGB"), colorKind, counter) | |||
| 143 | + | return | |||
| 144 | + | } | |||
| 145 | + | switch v.Kind() { | |||
| 146 | + | case reflect.Bool: | |||
| 147 | + | v.SetBool(true) | |||
| 148 | + | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: | |||
| 149 | + | v.SetInt(-int64(n) - 1) | |||
| 150 | + | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: | |||
| 151 | + | v.SetUint(uint64(1 + n%120)) | |||
| 152 | + | case reflect.String: | |||
| 153 | + | v.SetString("t<&>\"\\界\u2028" + string(rune('a'+n%26))) | |||
| 154 | + | case reflect.Slice: | |||
| 155 | + | v.Set(reflect.MakeSlice(v.Type(), 2, 2)) | |||
| 156 | + | for i := range 2 { | |||
| 157 | + | fillAll(t, v.Index(i), colorKind, counter) | |||
| 158 | + | } | |||
| 159 | + | case reflect.Struct: | |||
| 160 | + | for i := range v.NumField() { | |||
| 161 | + | require.True(t, v.Field(i).CanSet(), "%s.%s must be settable", v.Type(), v.Type().Field(i).Name) | |||
| 162 | + | fillAll(t, v.Field(i), colorKind, counter) | |||
| 163 | + | } | |||
| 164 | + | default: | |||
| 165 | + | t.Fatalf("fillAll: unsupported kind %s in %s", v.Kind(), v.Type()) | |||
| 166 | + | } | |||
| 167 | + | } | |||
| 168 | + | ||||
| 169 | + | func requireNoZeroFields(t *testing.T, v reflect.Value) { | |||
| 170 | + | t.Helper() | |||
| 171 | + | switch v.Kind() { | |||
| 172 | + | case reflect.Struct: | |||
| 173 | + | for i := range v.NumField() { | |||
| 174 | + | requireNoZeroFields(t, v.Field(i)) | |||
| 175 | + | } | |||
| 176 | + | case reflect.Slice: | |||
| 177 | + | require.NotZero(t, v.Len()) | |||
| 178 | + | for i := range v.Len() { | |||
| 179 | + | requireNoZeroFields(t, v.Index(i)) | |||
| 180 | + | } | |||
| 181 | + | default: | |||
| 182 | + | require.False(t, v.IsZero(), "zero %s left in filled value", v.Type()) | |||
| 183 | + | } | |||
| 184 | + | } | |||
| 185 | + | ||||
| 186 | + | // TestEncoderCoversEveryUpdateField fails when a field is added to (or removed | |||
| 187 | + | // from) the update types without updating appendUpdateJSON and this test. | |||
| 188 | + | func TestEncoderCoversEveryUpdateField(t *testing.T) { | |||
| 189 | + | for typ, fields := range map[reflect.Type]int{ | |||
| 190 | + | reflect.TypeOf(Update{}): 7, | |||
| 191 | + | reflect.TypeOf(RowUpdate{}): 2, | |||
| 192 | + | reflect.TypeOf(CellUpdate{}): 4, | |||
| 193 | + | reflect.TypeOf(Style{}): 11, | |||
| 194 | + | reflect.TypeOf(Cursor{}): 5, | |||
| 195 | + | reflect.TypeOf(RGB{}): 3, | |||
| 196 | + | reflect.TypeOf(Color{}): 3, | |||
| 197 | + | } { | |||
| 198 | + | require.Equal(t, fields, typ.NumField(), "%s field count changed: update appendUpdateJSON", typ) | |||
| 199 | + | } | |||
| 200 | + | ||||
| 201 | + | for _, kind := range []ColorKind{ColorDefault, ColorIndexed, ColorRGB} { | |||
| 202 | + | var update Update | |||
| 203 | + | counter := 0 | |||
| 204 | + | fillAll(t, reflect.ValueOf(&update).Elem(), kind, &counter) | |||
| 205 | + | update.Cursor.Style = 6 // keep within the documented DECSCUSR range | |||
| 206 | + | if kind != ColorDefault { | |||
| 207 | + | requireNoZeroFields(t, reflect.ValueOf(update)) | |||
| 208 | + | } | |||
| 209 | + | want, err := json.Marshal(update) | |||
| 210 | + | require.NoError(t, err) | |||
| 211 | + | got, err := appendUpdateJSON(nil, update) | |||
| 212 | + | require.NoError(t, err) | |||
| 213 | + | require.Equal(t, string(want), string(got), "color kind %d", kind) | |||
| 214 | + | } | |||
| 132 | } | 215 | } | ||