diff --git a/README.md b/README.md index 47ac276..b8c7026 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Usage menu: ``` $ jsoned -h -usage: jsoned [-v value] [-s] [-D] [-i infile] [-o outfile] keypath +usage: jsoned [-v value] [-r] [-D] [-O] [-i infile] [-o outfile] keypath examples: jsoned keypath read value from stdin or: jsoned -i infile keypath read value from infile @@ -40,11 +40,13 @@ examples: jsoned keypath read value from stdin options: -v value Edit JSON key path value + -r Use raw values, otherwise types are auto-detected + -O Performance boost for value updates. -D Delete the value at the specified key path -i infile Use input file instead of stdin -o outfile Use output file instead of stdout - -r Use raw values, otherwise types are auto-detected keypath JSON key path (like "name.last") + ``` @@ -161,6 +163,14 @@ $ echo '{"friends":["Andy","Carol"]}' | ./jsoned -D friends.-1 {"friends":["Andy"]} ``` +### Optimisticlly update a value + +The `-O` option can be used when the caller expects that a value at the +specified keypath already exists. + +Using this option can speed up an operation by as much as 6x, but +slow down as much as 20% when the value does not exist. + ## Contact Josh Baker [@tidwall](http://twitter.com/tidwall) diff --git a/build.sh b/build.sh index 40c7730..a450e05 100755 --- a/build.sh +++ b/build.sh @@ -1,7 +1,7 @@ #!/bin/bash set -e -VERSION="0.1.1" +VERSION="0.2.1" PROTECTED_MODE="no" export GO15VENDOREXPERIMENT=1 diff --git a/cmd/jsoned/main.go b/cmd/jsoned/main.go index ab829e3..bd57c89 100644 --- a/cmd/jsoned/main.go +++ b/cmd/jsoned/main.go @@ -15,7 +15,7 @@ var ( version = "0.0.1" tag = "jsoned - JSON Stream Editor " + version usage = ` -usage: jsoned [-v value] [-s] [-D] [-i infile] [-o outfile] keypath +usage: jsoned [-v value] [-r] [-D] [-O] [-i infile] [-o outfile] keypath examples: jsoned keypath read value from stdin or: jsoned -i infile keypath read value from infile @@ -24,10 +24,11 @@ examples: jsoned keypath read value from stdin options: -v value Edit JSON key path value + -r Use raw values, otherwise types are auto-detected + -O Performance boost for value updates. -D Delete the value at the specified key path -i infile Use input file instead of stdin -o outfile Use output file instead of stdout - -r Use raw values, otherwise types are auto-detected keypath JSON key path (like "name.last") for more info: https://github.com/tidwall/jsoned @@ -40,6 +41,7 @@ type args struct { value *string raw bool del bool + opt bool keypath string } @@ -86,6 +88,8 @@ func parseArgs() args { a.raw = true case "-D": a.del = true + case "-O": + a.opt = true case "-h", "--help", "-?": help() } @@ -133,12 +137,18 @@ func main() { raw = true } } + opts := &sjson.Options{} + if a.opt { + opts.Optimistic = true + opts.ReplaceInPlace = true + } if raw { // set as raw block - outb, err = sjson.SetRawBytes(input, a.keypath, []byte(val)) + outb, err = sjson.SetRawBytesOptions( + input, a.keypath, []byte(val), opts) } else { // set as a string - outb, err = sjson.SetBytes(input, a.keypath, val) + outb, err = sjson.SetBytesOptions(input, a.keypath, val, opts) } if err != nil { goto fail diff --git a/vendor/github.com/tidwall/sjson/sjson.go b/vendor/github.com/tidwall/sjson/sjson.go index 2aad9fd..df6581c 100644 --- a/vendor/github.com/tidwall/sjson/sjson.go +++ b/vendor/github.com/tidwall/sjson/sjson.go @@ -18,6 +18,20 @@ func (err *errorType) Error() string { return err.msg } +// Options represents addtional options for the Set and Delete functions. +type Options struct { + // Optimistic is a hint that the value likely exists which + // allows for the sjson to perform a fast-track search and replace. + Optimistic bool + // ReplaceInPlace is a hint to replace the input json rather than + // allocate a new json byte slice. When this field is specified + // the input json will not longer be valid and it should not be used + // There is no guarentees that the memory will be replaced in-place. + // The Optimistic flag must be set to true and the input must be a + // byte slice in order to use this field. + ReplaceInPlace bool +} + type pathResult struct { part string // current key part path string // remaining path @@ -64,9 +78,11 @@ func parsePath(path string) (pathResult, error) { r.more = true return r, nil } else if path[i] == '*' || path[i] == '?' { - return r, &errorType{"wildcard characters not allowed in path"} + return r, &errorType{ + "wildcard characters not allowed in path"} } else if path[i] == '#' { - return r, &errorType{"array access character not allowed in path"} + return r, &errorType{ + "array access character not allowed in path"} } epart = append(epart, path[i]) } @@ -80,14 +96,21 @@ func parsePath(path string) (pathResult, error) { return r, nil } -// appendStringify makes a json string and appends to buf. -func appendStringify(buf []byte, s string) []byte { +func mustMarshalString(s string) bool { for i := 0; i < len(s); i++ { if s[i] < ' ' || s[i] > 0x7f || s[i] == '"' { - b, _ := jsongo.Marshal(s) - return append(buf, b...) + return true } } + return false +} + +// appendStringify makes a json string and appends to buf. +func appendStringify(buf []byte, s string) []byte { + if mustMarshalString(s) { + b, _ := jsongo.Marshal(s) + return append(buf, b...) + } buf = append(buf, '"') buf = append(buf, s...) buf = append(buf, '"') @@ -95,7 +118,8 @@ func appendStringify(buf []byte, s string) []byte { } // appendBuild builds a json block from a json path. -func appendBuild(buf []byte, array bool, paths []pathResult, raw string, stringify bool) []byte { +func appendBuild(buf []byte, array bool, paths []pathResult, raw string, + stringify bool) []byte { if !array { buf = appendStringify(buf, paths[0].part) buf = append(buf, ':') @@ -208,7 +232,8 @@ loop: var errNoChange = &errorType{"no change"} -func appendRawPaths(buf []byte, jstr string, paths []pathResult, raw string, stringify, del bool) ([]byte, error) { +func appendRawPaths(buf []byte, jstr string, paths []pathResult, raw string, + stringify, del bool) ([]byte, error) { var err error var res gjson.Result var found bool @@ -227,7 +252,8 @@ func appendRawPaths(buf []byte, jstr string, paths []pathResult, raw string, str if res.Index > 0 { if len(paths) > 1 { buf = append(buf, jstr[:res.Index]...) - buf, err = appendRawPaths(buf, res.Raw, paths[1:], raw, stringify, del) + buf, err = appendRawPaths(buf, res.Raw, paths[1:], raw, + stringify, del) if err != nil { return nil, err } @@ -240,7 +266,8 @@ func appendRawPaths(buf []byte, jstr string, paths []pathResult, raw string, str var delNextComma bool buf, delNextComma = deleteTailItem(buf) if delNextComma { - for i, j := res.Index+len(res.Raw), 0; i < len(jstr); i, j = i+1, j+1 { + i, j := res.Index+len(res.Raw), 0 + for ; i < len(jstr); i, j = i+1, j+1 { if jstr[i] <= ' ' { continue } @@ -280,7 +307,12 @@ func appendRawPaths(buf []byte, jstr string, paths []pathResult, raw string, str } jsres := gjson.Parse(jstr) if jsres.Type != gjson.JSON { - return nil, &errorType{"json must be an object or array"} + if numeric { + jstr = "[]" + } else { + jstr = "{}" + } + jsres = gjson.Parse(jstr) } var comma bool for i := 1; i < len(jsres.Raw); i++ { @@ -310,7 +342,9 @@ func appendRawPaths(buf []byte, jstr string, paths []pathResult, raw string, str if paths[0].part == "-1" && !paths[0].force { appendit = true } else { - return nil, &errorType{"array key must be numeric"} + return nil, &errorType{ + "cannot set array element for non-numeric key '" + + paths[0].part + "'"} } } if appendit { @@ -349,12 +383,67 @@ func appendRawPaths(buf []byte, jstr string, paths []pathResult, raw string, str } } -func set(jstr, path, raw string, stringify, del bool) ([]byte, error) { - // parse the path, make sure that it does not contain invalid characters - // such as '#', '?', '*' +func isOptimisticPath(path string) bool { + for i := 0; i < len(path); i++ { + if path[i] < '.' || path[i] > 'z' { + return false + } + if path[i] > '9' && path[i] < 'A' { + return false + } + if path[i] > 'z' { + return false + } + } + return true +} + +func set(jstr, path, raw string, + stringify, del, optimistic, inplace bool) ([]byte, error) { if path == "" { return nil, &errorType{"path cannot be empty"} } + if !del && optimistic && isOptimisticPath(path) { + res := gjson.Get(jstr, path) + if res.Exists() && res.Index > 0 { + sz := len(jstr) - len(res.Raw) + len(raw) + if stringify { + sz += 2 + } + if inplace && sz <= len(jstr) { + if !stringify || !mustMarshalString(raw) { + jsonh := *(*reflect.StringHeader)(unsafe.Pointer(&jstr)) + jsonbh := reflect.SliceHeader{ + Data: jsonh.Data, Len: jsonh.Len, Cap: jsonh.Len} + jbytes := *(*[]byte)(unsafe.Pointer(&jsonbh)) + if stringify { + jbytes[res.Index] = '"' + copy(jbytes[res.Index+1:], []byte(raw)) + jbytes[res.Index+1+len(raw)] = '"' + copy(jbytes[res.Index+1+len(raw)+1:], + jbytes[res.Index+len(res.Raw):]) + } else { + copy(jbytes[res.Index:], []byte(raw)) + copy(jbytes[res.Index+len(raw):], + jbytes[res.Index+len(res.Raw):]) + } + return jbytes[:sz], nil + } + return nil, nil + } + buf := make([]byte, 0, sz) + buf = append(buf, jstr[:res.Index]...) + if stringify { + buf = appendStringify(buf, raw) + } else { + buf = append(buf, raw...) + } + buf = append(buf, jstr[res.Index+len(res.Raw):]...) + return buf, nil + } + } + // parse the path, make sure that it does not contain invalid characters + // such as '#', '?', '*' paths := make([]pathResult, 0, 4) r, err := parsePath(path) if err != nil { @@ -397,51 +486,49 @@ func set(jstr, path, raw string, stringify, del bool) ([]byte, error) { // "children.1" >> "Alex" // func Set(json, path string, value interface{}) (string, error) { + return SetOptions(json, path, value, nil) +} + +// SetOptions sets a json value for the specified path with options. +// A path is in dot syntax, such as "name.last" or "age". +// This function expects that the json is well-formed, and does not validate. +// Invalid json will not panic, but it may return back unexpected results. +// An error is returned if the path is not valid. +func SetOptions(json, path string, value interface{}, + opts *Options) (string, error) { + if opts != nil { + if opts.ReplaceInPlace { + // it's not safe to replace bytes in-place for strings + // copy the Options and set options.ReplaceInPlace to false. + nopts := *opts + opts = &nopts + opts.ReplaceInPlace = false + } + } jsonh := *(*reflect.StringHeader)(unsafe.Pointer(&json)) jsonbh := reflect.SliceHeader{Data: jsonh.Data, Len: jsonh.Len} jsonb := *(*[]byte)(unsafe.Pointer(&jsonbh)) - res, err := SetBytes(jsonb, path, value) + res, err := SetBytesOptions(jsonb, path, value, opts) return string(res), err } -type dtype struct{} - -// Delete deletes a value from json for the specified path. -func Delete(json, path string) (string, error) { - return Set(json, path, dtype{}) -} - -// DeleteBytes deletes a value from json for the specified path. -func DeleteBytes(json []byte, path string) ([]byte, error) { - return SetBytes(json, path, dtype{}) -} - -// SetRaw sets a raw json value for the specified path. The works the same as -// Set except that the value is set as a raw block of json. This allows for setting -// premarshalled json objects. -func SetRaw(json, path, value string) (string, error) { - res, err := set(json, path, value, false, false) - if err == errNoChange { - return json, nil - } - return string(res), err +// SetBytes sets a json value for the specified path. +// If working with bytes, this method preferred over +// Set(string(data), path, value) +func SetBytes(json []byte, path string, value interface{}) ([]byte, error) { + return SetBytesOptions(json, path, value, nil) } -// SetRawBytes sets a raw json value for the specified path. -// If working with bytes, this method preferred over SetRaw(string(data), path, value) -func SetRawBytes(json []byte, path string, value []byte) ([]byte, error) { - jstr := *(*string)(unsafe.Pointer(&json)) - vstr := *(*string)(unsafe.Pointer(&value)) - res, err := set(jstr, path, vstr, false, false) - if err == errNoChange { - return json, nil +// SetBytesOptions sets a json value for the specified path with options. +// If working with bytes, this method preferred over +// SetOptions(string(data), path, value) +func SetBytesOptions(json []byte, path string, value interface{}, + opts *Options) ([]byte, error) { + var optimistic, inplace bool + if opts != nil { + optimistic = opts.Optimistic + inplace = opts.ReplaceInPlace } - return res, nil -} - -// SetBytes sets a json value for the specified path. -// If working with bytes, this method preferred over Set(string(data), path, value) -func SetBytes(json []byte, path string, value interface{}) ([]byte, error) { jstr := *(*string)(unsafe.Pointer(&json)) var res []byte var err error @@ -452,43 +539,113 @@ func SetBytes(json []byte, path string, value interface{}) ([]byte, error) { return nil, err } raw := *(*string)(unsafe.Pointer(&b)) - res, err = set(jstr, path, raw, false, false) + res, err = set(jstr, path, raw, false, false, optimistic, inplace) case dtype: - res, err = set(jstr, path, "", false, true) + res, err = set(jstr, path, "", false, true, optimistic, inplace) case string: - res, err = set(jstr, path, v, true, false) + res, err = set(jstr, path, v, true, false, optimistic, inplace) case []byte: raw := *(*string)(unsafe.Pointer(&v)) - res, err = set(jstr, path, raw, true, false) + res, err = set(jstr, path, raw, true, false, optimistic, inplace) case bool: if v { - res, err = set(jstr, path, "true", false, false) + res, err = set(jstr, path, "true", false, false, optimistic, inplace) } else { - res, err = set(jstr, path, "false", false, false) + res, err = set(jstr, path, "false", false, false, optimistic, inplace) } case int8: - res, err = set(jstr, path, strconv.FormatInt(int64(v), 10), false, false) + res, err = set(jstr, path, strconv.FormatInt(int64(v), 10), + false, false, optimistic, inplace) case int16: - res, err = set(jstr, path, strconv.FormatInt(int64(v), 10), false, false) + res, err = set(jstr, path, strconv.FormatInt(int64(v), 10), + false, false, optimistic, inplace) case int32: - res, err = set(jstr, path, strconv.FormatInt(int64(v), 10), false, false) + res, err = set(jstr, path, strconv.FormatInt(int64(v), 10), + false, false, optimistic, inplace) case int64: - res, err = set(jstr, path, strconv.FormatInt(int64(v), 10), false, false) + res, err = set(jstr, path, strconv.FormatInt(int64(v), 10), + false, false, optimistic, inplace) case uint8: - res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10), false, false) + res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10), + false, false, optimistic, inplace) case uint16: - res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10), false, false) + res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10), + false, false, optimistic, inplace) case uint32: - res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10), false, false) + res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10), + false, false, optimistic, inplace) case uint64: - res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10), false, false) + res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10), + false, false, optimistic, inplace) case float32: - res, err = set(jstr, path, strconv.FormatFloat(float64(v), 'f', -1, 64), false, false) + res, err = set(jstr, path, strconv.FormatFloat(float64(v), 'f', -1, 64), + false, false, optimistic, inplace) case float64: - res, err = set(jstr, path, strconv.FormatFloat(float64(v), 'f', -1, 64), false, false) + res, err = set(jstr, path, strconv.FormatFloat(float64(v), 'f', -1, 64), + false, false, optimistic, inplace) } if err == errNoChange { return json, nil } return res, err } + +// SetRaw sets a raw json value for the specified path. +// This function works the same as Set except that the value is set as a +// raw block of json. This allows for setting premarshalled json objects. +func SetRaw(json, path, value string) (string, error) { + return SetRawOptions(json, path, value, nil) +} + +// SetRawOptions sets a raw json value for the specified path with options. +// This furnction works the same as SetOptions except that the value is set +// as a raw block of json. This allows for setting premarshalled json objects. +func SetRawOptions(json, path, value string, opts *Options) (string, error) { + var optimistic bool + if opts != nil { + optimistic = opts.Optimistic + } + res, err := set(json, path, value, false, false, optimistic, false) + if err == errNoChange { + return json, nil + } + return string(res), err +} + +// SetRawBytes sets a raw json value for the specified path. +// If working with bytes, this method preferred over +// SetRaw(string(data), path, value) +func SetRawBytes(json []byte, path string, value []byte) ([]byte, error) { + return SetRawBytesOptions(json, path, value, nil) +} + +// SetRawBytesOptions sets a raw json value for the specified path with options. +// If working with bytes, this method preferred over +// SetRawOptions(string(data), path, value, opts) +func SetRawBytesOptions(json []byte, path string, value []byte, + opts *Options) ([]byte, error) { + jstr := *(*string)(unsafe.Pointer(&json)) + vstr := *(*string)(unsafe.Pointer(&value)) + var optimistic, inplace bool + if opts != nil { + optimistic = opts.Optimistic + inplace = opts.ReplaceInPlace + } + res, err := set(jstr, path, vstr, false, false, optimistic, inplace) + if err == errNoChange { + return json, nil + } + return res, nil +} + +type dtype struct{} + +// Delete deletes a value from json for the specified path. +func Delete(json, path string) (string, error) { + return Set(json, path, dtype{}) +} + +// DeleteBytes deletes a value from json for the specified path. +func DeleteBytes(json []byte, path string) ([]byte, error) { + return SetBytes(json, path, dtype{}) +} diff --git a/vendor/github.com/tidwall/sjson/sjson_test.go b/vendor/github.com/tidwall/sjson/sjson_test.go index 4c7119f..08f7e29 100644 --- a/vendor/github.com/tidwall/sjson/sjson_test.go +++ b/vendor/github.com/tidwall/sjson/sjson_test.go @@ -1,8 +1,10 @@ package sjson import ( + "bytes" "encoding/hex" "math/rand" + "strings" "testing" "time" ) @@ -166,3 +168,196 @@ func TestRandomData(t *testing.T) { SetRaw(lstr, "zzzz.zzzz.zzzz", "123") } } + +var json = ` +{ + "sha": "d25341478381063d1c76e81b3a52e0592a7c997f", + "commit": { + "author": { + "name": "Tom Tom Anderson", + "email": "tomtom@anderson.edu", + "date": "2013-06-22T16:30:59Z" + }, + "committer": { + "name": "Tom Tom Anderson", + "email": "jeffditto@anderson.edu", + "date": "2013-06-22T16:30:59Z" + }, + "message": "Merge pull request #162 from stedolan/utf8-fixes\n\nUtf8 fixes. Closes #161", + "tree": { + "sha": "6ab697a8dfb5a96e124666bf6d6213822599fb40", + "url": "https://api.github.com/repos/stedolan/jq/git/trees/6ab697a8dfb5a96e124666bf6d6213822599fb40" + }, + "url": "https://api.github.com/repos/stedolan/jq/git/commits/d25341478381063d1c76e81b3a52e0592a7c997f", + "comment_count": 0 + } +} +` +var path = "commit.committer.email" +var value = "tomtom@anderson.com" +var rawValue = `"tomtom@anderson.com"` +var rawValueBytes = []byte(rawValue) +var expect = strings.Replace(json, "jeffditto@anderson.edu", "tomtom@anderson.com", 1) +var jsonBytes = []byte(json) +var jsonBytes2 = []byte(json) +var expectBytes = []byte(expect) +var opts = &Options{Optimistic: true} +var optsInPlace = &Options{Optimistic: true, ReplaceInPlace: true} + +func BenchmarkSet(t *testing.B) { + t.ReportAllocs() + for i := 0; i < t.N; i++ { + res, err := Set(json, path, value) + if err != nil { + t.Fatal(err) + } + if res != expect { + t.Fatal("expected '%v', got '%v'", expect, res) + } + } +} + +func BenchmarkSetRaw(t *testing.B) { + t.ReportAllocs() + for i := 0; i < t.N; i++ { + res, err := SetRaw(json, path, rawValue) + if err != nil { + t.Fatal(err) + } + if res != expect { + t.Fatal("expected '%v', got '%v'", expect, res) + } + } +} + +func BenchmarkSetBytes(t *testing.B) { + t.ReportAllocs() + for i := 0; i < t.N; i++ { + res, err := SetBytes(jsonBytes, path, value) + if err != nil { + t.Fatal(err) + } + if bytes.Compare(res, expectBytes) != 0 { + t.Fatal("expected '%v', got '%v'", expect, res) + } + } +} + +func BenchmarkSetRawBytes(t *testing.B) { + t.ReportAllocs() + for i := 0; i < t.N; i++ { + res, err := SetRawBytes(jsonBytes, path, rawValueBytes) + if err != nil { + t.Fatal(err) + } + if bytes.Compare(res, expectBytes) != 0 { + t.Fatal("expected '%v', got '%v'", expect, res) + } + } +} + +func BenchmarkSetOptimistic(t *testing.B) { + t.ReportAllocs() + for i := 0; i < t.N; i++ { + res, err := SetOptions(json, path, value, opts) + if err != nil { + t.Fatal(err) + } + if res != expect { + t.Fatal("expected '%v', got '%v'", expect, res) + } + } +} + +func BenchmarkSetInPlace(t *testing.B) { + t.ReportAllocs() + for i := 0; i < t.N; i++ { + res, err := SetOptions(json, path, value, optsInPlace) + if err != nil { + t.Fatal(err) + } + if res != expect { + t.Fatal("expected '%v', got '%v'", expect, res) + } + } +} + +func BenchmarkSetRawOptimistic(t *testing.B) { + t.ReportAllocs() + for i := 0; i < t.N; i++ { + res, err := SetRawOptions(json, path, rawValue, opts) + if err != nil { + t.Fatal(err) + } + if res != expect { + t.Fatal("expected '%v', got '%v'", expect, res) + } + } +} + +func BenchmarkSetRawInPlace(t *testing.B) { + t.ReportAllocs() + for i := 0; i < t.N; i++ { + res, err := SetRawOptions(json, path, rawValue, optsInPlace) + if err != nil { + t.Fatal(err) + } + if res != expect { + t.Fatal("expected '%v', got '%v'", expect, res) + } + } +} + +func BenchmarkSetBytesOptimistic(t *testing.B) { + t.ReportAllocs() + for i := 0; i < t.N; i++ { + res, err := SetBytesOptions(jsonBytes, path, value, opts) + if err != nil { + t.Fatal(err) + } + if bytes.Compare(res, expectBytes) != 0 { + t.Fatal("expected '%v', got '%v'", string(expectBytes), string(res)) + } + } +} + +func BenchmarkSetBytesInPlace(t *testing.B) { + t.ReportAllocs() + for i := 0; i < t.N; i++ { + copy(jsonBytes2, jsonBytes) + res, err := SetBytesOptions(jsonBytes2, path, value, optsInPlace) + if err != nil { + t.Fatal(err) + } + if bytes.Compare(res, expectBytes) != 0 { + t.Fatal("expected '%v', got '%v'", string(expectBytes), string(res)) + } + } +} + +func BenchmarkSetRawBytesOptimistic(t *testing.B) { + t.ReportAllocs() + for i := 0; i < t.N; i++ { + res, err := SetRawBytesOptions(jsonBytes, path, rawValueBytes, opts) + if err != nil { + t.Fatal(err) + } + if bytes.Compare(res, expectBytes) != 0 { + t.Fatal("expected '%v', got '%v'", string(expectBytes), string(res)) + } + } +} + +func BenchmarkSetRawBytesInPlace(t *testing.B) { + t.ReportAllocs() + for i := 0; i < t.N; i++ { + copy(jsonBytes2, jsonBytes) + res, err := SetRawBytesOptions(jsonBytes2, path, rawValueBytes, optsInPlace) + if err != nil { + t.Fatal(err) + } + if bytes.Compare(res, expectBytes) != 0 { + t.Fatal("expected '%v', got '%v'", string(expectBytes), string(res)) + } + } +}