diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 229abef..0000000 --- a/.travis.yml +++ /dev/null @@ -1,16 +0,0 @@ -sudo: false -language: go - -go: - - 1.6 - - 1.7 - - tip - -matrix: - allow_failures: - - go: tip - -install: - - mkdir -p $HOME/gopath/src/gopkg.in - - mv $HOME/gopath/src/github.com/go-sourcemap/sourcemap $HOME/gopath/src/gopkg.in/sourcemap.v1 - - cd $HOME/gopath/src/gopkg.in/sourcemap.v1 diff --git a/README.md b/README.md index fb319d2..bc6b624 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,19 @@ -# Source Maps consumer for Golang [![Build Status](https://travis-ci.org/go-sourcemap/sourcemap.svg?branch=v1)](https://travis-ci.org/go-sourcemap/sourcemap) +# Source maps consumer for Golang + +[![PkgGoDev](https://pkg.go.dev/badge/github.com/go-sourcemap/sourcemap)](https://pkg.go.dev/github.com/go-sourcemap/sourcemap) + +> This package is brought to you by :star: [**uptrace/uptrace**](https://github.com/uptrace/uptrace). +> Uptrace is an open-source APM tool that supports distributed tracing, metrics, and logs. You can +> use it to monitor applications and set up automatic alerts to receive notifications via email, +> Slack, Telegram, and others. ## Installation Install: - go get gopkg.in/sourcemap.v1 +```shell +go get -u github.com/go-sourcemap/sourcemap +``` ## Quickstart diff --git a/bench_test.go b/bench_test.go index 69837dc..2557b93 100644 --- a/bench_test.go +++ b/bench_test.go @@ -3,7 +3,7 @@ package sourcemap_test import ( "testing" - "gopkg.in/sourcemap.v1" + "github.com/go-sourcemap/sourcemap" ) func BenchmarkParse(b *testing.B) { diff --git a/consumer.go b/consumer.go index 3bed06a..2c269f4 100644 --- a/consumer.go +++ b/consumer.go @@ -6,42 +6,43 @@ import ( "net/url" "path" "sort" - "strconv" ) -type Consumer struct { - sourceRootURL *url.URL - smap *sourceMap - mappings []mapping +type sourceMap struct { + Version int `json:"version"` + File string `json:"file"` + SourceRoot string `json:"sourceRoot"` + Sources []string `json:"sources"` + SourcesContent []string `json:"sourcesContent"` + Names []json.RawMessage `json:"names,string"` + Mappings string `json:"mappings"` + + mappings []mapping } -func Parse(mapURL string, b []byte) (*Consumer, error) { - smap := new(sourceMap) - err := json.Unmarshal(b, smap) - if err != nil { - return nil, err - } +type v3 struct { + sourceMap + Sections []section `json:"sections"` +} - if smap.Version != 3 { - return nil, fmt.Errorf( - "sourcemap: got version=%d, but only 3rd version is supported", - smap.Version, - ) +func (m *sourceMap) parse(sourcemapURL string) error { + if err := checkVersion(m.Version); err != nil { + return err } var sourceRootURL *url.URL - if smap.SourceRoot != "" { - u, err := url.Parse(smap.SourceRoot) + if m.SourceRoot != "" { + u, err := url.Parse(m.SourceRoot) if err != nil { - return nil, err + return err } if u.IsAbs() { sourceRootURL = u } - } else if mapURL != "" { - u, err := url.Parse(mapURL) + } else if sourcemapURL != "" { + u, err := url.Parse(sourcemapURL) if err != nil { - return nil, err + return err } if u.IsAbs() { u.Path = path.Dir(u.Path) @@ -49,86 +50,213 @@ func Parse(mapURL string, b []byte) (*Consumer, error) { } } - mappings, err := parseMappings(smap.Mappings) + for i, src := range m.Sources { + m.Sources[i] = m.absSource(sourceRootURL, src) + } + + mappings, err := parseMappings(m.Mappings) if err != nil { - return nil, err + return err } + + m.mappings = mappings // Free memory. - smap.Mappings = "" + m.Mappings = "" + + return nil +} + +func (m *sourceMap) absSource(root *url.URL, source string) string { + if path.IsAbs(source) { + return source + } + + if u, err := url.Parse(source); err == nil && u.IsAbs() { + return source + } + + if root != nil { + u := *root + u.Path = path.Join(u.Path, source) + return u.String() + } + + if m.SourceRoot != "" { + return path.Join(m.SourceRoot, source) + } + + return source +} + +func (m *sourceMap) name(idx int) string { + if idx >= len(m.Names) { + return "" + } + + raw := m.Names[idx] + if len(raw) == 0 { + return "" + } + + if raw[0] == '"' && raw[len(raw)-1] == '"' { + var str string + if err := unmarshalJSON(raw, &str); err == nil { + return str + } + } + + return string(raw) +} + +type section struct { + Offset struct { + Line int `json:"line"` + Column int `json:"column"` + } `json:"offset"` + Map *sourceMap `json:"map"` +} +type Consumer struct { + sourcemapURL string + file string + sections []section +} + +func Parse(sourcemapURL string, b []byte) (*Consumer, error) { + v3 := new(v3) + err := unmarshalJSON(b, v3) + if err != nil { + return nil, err + } + + if err := checkVersion(v3.Version); err != nil { + return nil, err + } + + if len(v3.Sections) == 0 { + v3.Sections = append(v3.Sections, section{ + Map: &v3.sourceMap, + }) + } + + for _, s := range v3.Sections { + err := s.Map.parse(sourcemapURL) + if err != nil { + return nil, err + } + } + + reverse(v3.Sections) return &Consumer{ - sourceRootURL: sourceRootURL, - smap: smap, - mappings: mappings, + sourcemapURL: sourcemapURL, + file: v3.File, + sections: v3.Sections, }, nil } +func (c *Consumer) SourcemapURL() string { + return c.sourcemapURL +} + +// File returns an optional name of the generated code +// that this source map is associated with. func (c *Consumer) File() string { - return c.smap.File + return c.file } -func (c *Consumer) Source(genLine, genCol int) (source, name string, line, col int, ok bool) { - i := sort.Search(len(c.mappings), func(i int) bool { - m := &c.mappings[i] - if m.genLine == genLine { - return m.genCol >= genCol +// Source returns the original source, name, line, and column information +// for the generated source's line and column positions. +func (c *Consumer) Source( + genLine, genColumn int, +) (source, name string, line, column int, ok bool) { + for i := range c.sections { + s := &c.sections[i] + if s.Offset.Line < genLine || + (s.Offset.Line+1 == genLine && s.Offset.Column <= genColumn) { + genLine -= s.Offset.Line + genColumn -= s.Offset.Column + return c.source(s.Map, genLine, genColumn) } - return m.genLine >= genLine - }) + } + return +} - // Mapping not found. - if i == len(c.mappings) { +func (c *Consumer) source( + m *sourceMap, genLine, genColumn int, +) (source, name string, line, column int, ok bool) { + if len(m.mappings) == 0 { return } - match := &c.mappings[i] + i := sort.Search(len(m.mappings), func(i int) bool { + m := &m.mappings[i] + if int(m.genLine) == genLine { + return int(m.genColumn) >= genColumn + } + return int(m.genLine) >= genLine + }) - // Fuzzy match. - if match.genLine > genLine || match.genCol > genCol { - if i == 0 { + var match *mapping + // Mapping not found + if i == len(m.mappings) { + // lets see if the line is correct but the column is bigger + match = &m.mappings[i-1] + if int(match.genLine) != genLine { return } - match = &c.mappings[i-1] + } else { + match = &m.mappings[i] + + // Fuzzy match. + if int(match.genLine) > genLine || int(match.genColumn) > genColumn { + if i == 0 { + return + } + match = &m.mappings[i-1] + } } if match.sourcesInd >= 0 { - source = c.absSource(c.smap.Sources[match.sourcesInd]) + source = m.Sources[match.sourcesInd] } if match.namesInd >= 0 { - v := c.smap.Names[match.namesInd] - switch v := v.(type) { - case string: - name = v - case float64: - name = strconv.FormatFloat(v, 'f', -1, 64) - default: - name = fmt.Sprint(v) - } + name = m.name(int(match.namesInd)) } - line = match.sourceLine - col = match.sourceCol + line = int(match.sourceLine) + column = int(match.sourceColumn) ok = true return } -func (c *Consumer) absSource(source string) string { - if path.IsAbs(source) { - return source - } - - if u, err := url.Parse(source); err == nil && u.IsAbs() { - return source +// SourceContent returns the original source content for the source. +func (c *Consumer) SourceContent(source string) string { + for i := range c.sections { + s := &c.sections[i] + for i, src := range s.Map.Sources { + if src == source { + if i < len(s.Map.SourcesContent) { + return s.Map.SourcesContent[i] + } + break + } + } } + return "" +} - if c.sourceRootURL != nil { - u := *c.sourceRootURL - u.Path = path.Join(c.sourceRootURL.Path, source) - return u.String() +func checkVersion(version int) error { + if version == 3 || version == 0 { + return nil } + return fmt.Errorf( + "sourcemap: got version=%d, but only 3rd version is supported", + version, + ) +} - if c.smap.SourceRoot != "" { - return path.Join(c.smap.SourceRoot, source) +func reverse(ss []section) { + last := len(ss) - 1 + for i := 0; i < len(ss)/2; i++ { + ss[i], ss[last-i] = ss[last-i], ss[i] } - - return source } diff --git a/consumer_test.go b/consumer_test.go index 79287e1..7fc84f3 100644 --- a/consumer_test.go +++ b/consumer_test.go @@ -1,13 +1,14 @@ package sourcemap_test import ( + "encoding/json" "fmt" "io/ioutil" "net/http" "strings" "testing" - "gopkg.in/sourcemap.v1" + "github.com/go-sourcemap/sourcemap" ) const jqSourceMapURL = "http://code.jquery.com/jquery-2.0.3.min.map" @@ -29,24 +30,24 @@ func init() { type sourceMapTest struct { genLine int - genCol int + genColumn int wantedSource string wantedName string wantedLine int - wantedCol int + wantedColumn int } func (test *sourceMapTest) String() string { - return fmt.Sprintf("line=%d col=%d in file=%s", test.genLine, test.genCol, test.wantedSource) + return fmt.Sprintf("line=%d col=%d in file=%s", test.genLine, test.genColumn, test.wantedSource) } func (test *sourceMapTest) assert(t *testing.T, smap *sourcemap.Consumer) { - source, name, line, col, ok := smap.Source(test.genLine, test.genCol) + source, name, line, col, ok := smap.Source(test.genLine, test.genColumn) if !ok { if test.wantedSource == "" && test.wantedName == "" && test.wantedLine == 0 && - test.wantedCol == 0 { + test.wantedColumn == 0 { return } t.Fatalf("Source not found for %s", test) @@ -60,18 +61,43 @@ func (test *sourceMapTest) assert(t *testing.T, smap *sourcemap.Consumer) { if line != test.wantedLine { t.Fatalf("line: got %d, wanted %d (%s)", line, test.wantedLine, test) } - if col != test.wantedCol { - t.Fatalf("column: got %d, wanted %d (%s)", col, test.wantedCol, test) + if col != test.wantedColumn { + t.Fatalf("column: got %d, wanted %d (%s)", col, test.wantedColumn, test) + } +} + +func TestSourceWithEmptySourceMap(t *testing.T) { + var jsmap = `{ + "version": 3, + "mappings": ";;" +}` + + smap, err := sourcemap.Parse("noname", []byte(jsmap)) + if err != nil { + t.Fatal(err) + } + + _, _, _, _, matched := smap.Source(1, 1) + if matched { + t.Error("it is unexpected to match an empty SourceMap") } } func TestSourceMap(t *testing.T) { - smap, err := sourcemap.Parse("", []byte(sourceMapJSON)) + testSourceMap(t, sourceMapJSON) +} + +func TestIndexedSourceMap(t *testing.T) { + testSourceMap(t, indexedSourceMapJSON) +} + +func testSourceMap(t *testing.T, json string) { + smap, err := sourcemap.Parse("", []byte(json)) if err != nil { t.Fatal(err) } - tests := []*sourceMapTest{ + tests := []sourceMapTest{ {1, 1, "/the/root/one.js", "", 1, 1}, {1, 5, "/the/root/one.js", "", 1, 5}, {1, 9, "/the/root/one.js", "", 1, 11}, @@ -87,13 +113,26 @@ func TestSourceMap(t *testing.T) { {2, 21, "/the/root/two.js", "", 2, 3}, {2, 28, "/the/root/two.js", "n", 2, 10}, + // line correct, column bigger than last mapping + {2, 29, "/the/root/two.js", "n", 2, 10}, + // Fuzzy match. {1, 20, "/the/root/one.js", "bar", 1, 21}, {1, 30, "/the/root/one.js", "baz", 2, 10}, {2, 12, "/the/root/two.js", "", 1, 11}, } - for _, test := range tests { - test.assert(t, smap) + for i := range tests { + tests[i].assert(t, smap) + } + + content := smap.SourceContent("/the/root/one.js") + if content != oneSourceContent { + t.Fatalf("%q != %q", content, oneSourceContent) + } + + content = smap.SourceContent("/the/root/two.js") + if content != twoSourceContent { + t.Fatalf("%q != %q", content, twoSourceContent) } _, _, _, _, ok := smap.Source(3, 0) @@ -186,6 +225,8 @@ func TestJQuerySourceMap(t *testing.T) { } } +// https://github.com/mozilla/source-map/blob/master/test/util.js +// // This is a test mapping which maps functions from two different files // (one.js and two.js) to a minified generated source. // @@ -206,14 +247,56 @@ func TestJQuerySourceMap(t *testing.T) { // ONE.foo=function(a){return baz(a);}; // TWO.inc=function(a){return a+1;}; -var genCode = `exports.testGeneratedCode = "ONE.foo=function(a){return baz(a);}; +const genCode = `exports.testGeneratedCode = "ONE.foo=function(a){return baz(a);}; TWO.inc=function(a){return a+1;};` +var oneSourceContent = `ONE.foo = function (bar) { + return baz(bar); +};` + +var twoSourceContent = `TWO.inc = function (n) { + return n + 1; +};` + var sourceMapJSON = `{ "version": 3, "file": "min.js", - "names": ["bar", "baz", "n"], "sources": ["one.js", "two.js"], + "sourcesContent": ` + j([]string{oneSourceContent, twoSourceContent}) + `, "sourceRoot": "/the/root", + "names": ["bar", "baz", "n"], "mappings": "CAAC,IAAI,IAAM,SAAUA,GAClB,OAAOC,IAAID;CCDb,IAAI,IAAM,SAAUE,GAClB,OAAOA" }` + +func j(v interface{}) string { + b, _ := json.Marshal(v) + return string(b) +} + +var indexedSourceMapJSON = `{ + "version": 3, + "file": "min.js", + "sections": [{ + "offset": {"line": 0, "column": 0}, + "map": { + "version": 3, + "file": "min.js", + "sources": ["one.js"], + "sourcesContent": ` + j([]string{oneSourceContent}) + `, + "sourceRoot": "/the/root", + "names": ["bar", "baz"], + "mappings": "CAAC,IAAI,IAAM,SAAUA,GAClB,OAAOC,IAAID" + } + }, { + "offset": {"line": 1, "column": 0}, + "map": { + "version": 3, + "file": "min.js", + "sources": ["two.js"], + "sourcesContent": ` + j([]string{twoSourceContent}) + `, + "sourceRoot": "/the/root", + "names": ["n"], + "mappings": "CAAC,IAAI,IAAM,SAAUA,GAClB,OAAOA" + } + }] +}` diff --git a/example_test.go b/example_test.go index ffac414..341de43 100644 --- a/example_test.go +++ b/example_test.go @@ -5,7 +5,7 @@ import ( "io/ioutil" "net/http" - "gopkg.in/sourcemap.v1" + "github.com/go-sourcemap/sourcemap" ) func ExampleParse() { diff --git a/base64vlq/base64_vlq.go b/internal/base64vlq/base64vlq.go similarity index 76% rename from base64vlq/base64_vlq.go rename to internal/base64vlq/base64vlq.go index 16cbfb5..4804f5a 100644 --- a/base64vlq/base64_vlq.go +++ b/internal/base64vlq/base64vlq.go @@ -1,8 +1,6 @@ package base64vlq -import ( - "io" -) +import "io" const encodeStd = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" @@ -22,14 +20,14 @@ func init() { } } -func toVLQSigned(n int) int { +func toVLQSigned(n int32) int32 { if n < 0 { return -n<<1 + 1 } return n << 1 } -func fromVLQSigned(n int) int { +func fromVLQSigned(n int32) int32 { isNeg := n&vlqSignBit != 0 n >>= 1 if isNeg { @@ -48,9 +46,9 @@ func NewEncoder(w io.ByteWriter) *Encoder { } } -func (enc Encoder) Encode(n int) error { +func (enc Encoder) Encode(n int32) error { n = toVLQSigned(n) - for digit := vlqContinuationBit; digit&vlqContinuationBit != 0; { + for digit := int32(vlqContinuationBit); digit&vlqContinuationBit != 0; { digit = n & vlqBaseMask n >>= vlqBaseShift if n > 0 { @@ -69,13 +67,13 @@ type Decoder struct { r io.ByteReader } -func NewDecoder(r io.ByteReader) *Decoder { - return &Decoder{ +func NewDecoder(r io.ByteReader) Decoder { + return Decoder{ r: r, } } -func (dec Decoder) Decode() (n int, err error) { +func (dec Decoder) Decode() (n int32, err error) { shift := uint(0) for continuation := true; continuation; { c, err := dec.r.ReadByte() @@ -85,7 +83,7 @@ func (dec Decoder) Decode() (n int, err error) { c = decodeMap[c] continuation = c&vlqContinuationBit != 0 - n += int(c&vlqBaseMask) << shift + n += int32(c&vlqBaseMask) << shift shift += vlqBaseShift } return fromVLQSigned(n), nil diff --git a/base64vlq/base64_vlq_test.go b/internal/base64vlq/base64vlq_test.go similarity index 86% rename from base64vlq/base64_vlq_test.go rename to internal/base64vlq/base64vlq_test.go index 2aafd59..24cd889 100644 --- a/base64vlq/base64_vlq_test.go +++ b/internal/base64vlq/base64vlq_test.go @@ -4,7 +4,7 @@ import ( "bytes" "testing" - "gopkg.in/sourcemap.v1/base64vlq" + "github.com/go-sourcemap/sourcemap/internal/base64vlq" ) func TestEncodeDecode(t *testing.T) { @@ -12,13 +12,13 @@ func TestEncodeDecode(t *testing.T) { enc := base64vlq.NewEncoder(buf) dec := base64vlq.NewDecoder(buf) - for n := -1000; n < 1000; n++ { + for n := int32(-1000); n < 1000; n++ { if err := enc.Encode(n); err != nil { panic(err) } } - for n := -1000; n < 1000; n++ { + for n := int32(-1000); n < 1000; n++ { nn, err := dec.Decode() if err != nil { panic(err) diff --git a/json_decoder.go b/json_decoder.go new file mode 100644 index 0000000..7d4c4b5 --- /dev/null +++ b/json_decoder.go @@ -0,0 +1,12 @@ +//go:build !jsonv2 +// +build !jsonv2 + +package sourcemap + +import "encoding/json" + +// unmarshalJSON is the JSON unmarshaling function +// This version uses the standard encoding/json package +func unmarshalJSON(data []byte, v interface{}) error { + return json.Unmarshal(data, v) +} \ No newline at end of file diff --git a/json_decoder_v2.go b/json_decoder_v2.go new file mode 100644 index 0000000..37a2760 --- /dev/null +++ b/json_decoder_v2.go @@ -0,0 +1,13 @@ +//go:build jsonv2 +// +build jsonv2 + +package sourcemap + +import "encoding/json/v2" + +// unmarshalJSON is the JSON unmarshaling function +// This version uses the experimental json/v2 package for better performance +// Build with: GOEXPERIMENT=jsonv2 go build -tags=jsonv2 ./... +func unmarshalJSON(data []byte, v interface{}) error { + return json.Unmarshal(data, v) +} \ No newline at end of file diff --git a/sourcemap.go b/mappings.go similarity index 67% rename from sourcemap.go rename to mappings.go index 0e9af1a..eb4b04b 100644 --- a/sourcemap.go +++ b/mappings.go @@ -1,47 +1,46 @@ -package sourcemap // import "gopkg.in/sourcemap.v1" +package sourcemap import ( + "errors" "io" "strings" - "gopkg.in/sourcemap.v1/base64vlq" + "github.com/go-sourcemap/sourcemap/internal/base64vlq" ) type fn func(m *mappings) (fn, error) -type sourceMap struct { - Version int `json:"version"` - File string `json:"file"` - SourceRoot string `json:"sourceRoot"` - Sources []string `json:"sources"` - Names []interface{} `json:"names"` - Mappings string `json:"mappings"` -} - type mapping struct { - genLine int - genCol int - sourcesInd int - sourceLine int - sourceCol int - namesInd int + genLine int32 + genColumn int32 + sourcesInd int32 + sourceLine int32 + sourceColumn int32 + namesInd int32 } type mappings struct { rd *strings.Reader - dec *base64vlq.Decoder + dec base64vlq.Decoder - hasName bool - value mapping + hasValue bool + hasName bool + value mapping values []mapping } func parseMappings(s string) ([]mapping, error) { + if s == "" { + return nil, errors.New("sourcemap: mappings are empty") + } + rd := strings.NewReader(s) m := &mappings{ rd: rd, dec: base64vlq.NewDecoder(rd), + + values: make([]mapping, 0, mappingsNumber(s)), } m.value.genLine = 1 m.value.sourceLine = 1 @@ -50,7 +49,14 @@ func parseMappings(s string) ([]mapping, error) { if err != nil { return nil, err } - return m.values, nil + + values := m.values + m.values = nil + return values, nil +} + +func mappingsNumber(s string) int { + return strings.Count(s, ",") + strings.Count(s, ";") } func (m *mappings) parse() error { @@ -73,7 +79,7 @@ func (m *mappings) parse() error { m.pushValue() m.value.genLine++ - m.value.genCol = 0 + m.value.genColumn = 0 next = parseGenCol default: @@ -86,6 +92,7 @@ func (m *mappings) parse() error { if err != nil { return err } + m.hasValue = true } } } @@ -95,7 +102,7 @@ func parseGenCol(m *mappings) (fn, error) { if err != nil { return nil, err } - m.value.genCol += n + m.value.genColumn += n return parseSourcesInd, nil } @@ -122,7 +129,7 @@ func parseSourceCol(m *mappings) (fn, error) { if err != nil { return nil, err } - m.value.sourceCol += n + m.value.sourceColumn += n return parseNamesInd, nil } @@ -137,21 +144,21 @@ func parseNamesInd(m *mappings) (fn, error) { } func (m *mappings) pushValue() { - if m.value.sourceLine == 1 && m.value.sourceCol == 0 { + if !m.hasValue { return } - + m.hasValue = false if m.hasName { m.values = append(m.values, m.value) m.hasName = false } else { m.values = append(m.values, mapping{ - genLine: m.value.genLine, - genCol: m.value.genCol, - sourcesInd: m.value.sourcesInd, - sourceLine: m.value.sourceLine, - sourceCol: m.value.sourceCol, - namesInd: -1, + genLine: m.value.genLine, + genColumn: m.value.genColumn, + sourcesInd: m.value.sourcesInd, + sourceLine: m.value.sourceLine, + sourceColumn: m.value.sourceColumn, + namesInd: -1, }) } } diff --git a/mappings_test.go b/mappings_test.go new file mode 100644 index 0000000..f35cb61 --- /dev/null +++ b/mappings_test.go @@ -0,0 +1,32 @@ +package sourcemap + +import ( + "reflect" + "testing" +) + +func TestParseMappings(t *testing.T) { + t.Parallel() + cases := map[string][]mapping{ + ";;;;;;kBAEe,YAAY,CAC1B,C;;AAHD": { + {genLine: 7, genColumn: 18, sourceLine: 3, sourceColumn: 15, namesInd: -1}, + {genLine: 7, genColumn: 30, sourceLine: 3, sourceColumn: 27, namesInd: -1}, + {genLine: 7, genColumn: 31, sourceLine: 4, sourceColumn: 1, namesInd: -1}, + {genLine: 7, genColumn: 32, sourceLine: 4, sourceColumn: 1, namesInd: -1}, + {genLine: 9, genColumn: 0, sourceLine: 1, sourceColumn: 0, namesInd: -1}, + }, + } + for k, c := range cases { + k, c := k, c + t.Run(k, func(t *testing.T) { + t.Parallel() + v, err := parseMappings(k) + if err != nil { + t.Fatalf("got error %s", err) + } + if !reflect.DeepEqual(v, c) { + t.Fatalf("expected %v got %v", c, v) + } + }) + } +}