| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433 |
- package xpj
- import (
- "bytes"
- "compress/gzip"
- "io"
- "math"
- "os"
- "path/filepath"
- "strings"
- "testing"
- )
- const testdata = "../../testdata"
- func decompress(t *testing.T, path string) []byte {
- t.Helper()
- fh, err := os.Open(path)
- if err != nil {
- t.Fatal(err)
- }
- defer fh.Close()
- zr, err := gzip.NewReader(fh)
- if err != nil {
- t.Fatal(err)
- }
- raw, err := io.ReadAll(zr)
- if err != nil {
- t.Fatal(err)
- }
- return raw
- }
- // firstDiff describes where two byte slices first differ.
- func firstDiff(a, b []byte) string {
- n := min(len(a), len(b))
- for i := 0; i < n; i++ {
- if a[i] != b[i] {
- lo := max(0, i-40)
- return "at byte " + itoa(i) + ":\n got " + quote(a[lo:min(len(a), i+40)]) + "\n want " + quote(b[lo:min(len(b), i+40)])
- }
- }
- return "lengths " + itoa(len(a)) + " and " + itoa(len(b))
- }
- func itoa(i int) string { return NewInt(int64(i)).Text }
- func quote(b []byte) string {
- return strings.ReplaceAll(string(b), "\n", `\n`)
- }
- // The M1 milestone: every device-saved project re-writes byte-identical.
- func TestRoundTripDeviceProjects(t *testing.T) {
- for _, name := range []string{"DemoBlank", "Demo02", "DemoFile"} {
- t.Run(name, func(t *testing.T) {
- roundTrip(t, filepath.Join(testdata, "device", name+".xpj"))
- })
- }
- }
- // Files from the reference writer that the device accepted round-trip too.
- func TestRoundTripAcceptedProjects(t *testing.T) {
- paths, _ := filepath.Glob(filepath.Join(testdata, "accepted", "*.xpj"))
- more, _ := filepath.Glob(filepath.Join(testdata, "wav-formats", "*.xpj"))
- paths = append(paths, more...)
- if len(paths) < 5 {
- t.Fatalf("found only %d accepted projects", len(paths))
- }
- for _, p := range paths {
- t.Run(filepath.Base(p), func(t *testing.T) { roundTrip(t, p) })
- }
- }
- func roundTrip(t *testing.T, path string) {
- t.Helper()
- want := decompress(t, path)
- f, err := ReadFile(path)
- if err != nil {
- t.Fatal(err)
- }
- got, err := f.Encode()
- if err != nil {
- t.Fatal(err)
- }
- if !bytes.Equal(got, want) {
- t.Fatalf("re-encoded file differs %s", firstDiff(got, want))
- }
- // Through gzip and back, as an export would.
- var z bytes.Buffer
- if err := f.Write(&z); err != nil {
- t.Fatal(err)
- }
- f2, err := Read(&z)
- if err != nil {
- t.Fatal(err)
- }
- got2, _ := f2.Encode()
- if !bytes.Equal(got2, want) {
- t.Fatalf("gzip round trip differs %s", firstDiff(got2, want))
- }
- }
- func TestDeviceProjectShape(t *testing.T) {
- f, err := ReadFile(filepath.Join(testdata, "device", "DemoBlank.xpj"))
- if err != nil {
- t.Fatal(err)
- }
- if strings.Join(f.Header, "|") != strings.Join(TestedHeader, "|") {
- t.Errorf("header = %q", f.Header)
- }
- d, err := f.Data()
- if err != nil {
- t.Fatal(err)
- }
- if len(d.Members) != 66 {
- t.Errorf("data has %d keys, want 66", len(d.Members))
- }
- if v, _ := d.Lookup("version").Int(); v != 28 {
- t.Errorf("data.version = %d, want 28", v)
- }
- pads, err := d.Get("tracks[0].program.drum.instruments")
- if err != nil {
- t.Fatal(err)
- }
- if len(pads.Items) != 128 {
- t.Errorf("%d pads, want 128", len(pads.Items))
- }
- seq, err := d.Get("sequences[0].value")
- if err != nil {
- t.Fatal(err)
- }
- if seq.Lookup("name").Text != "Sequence 01" {
- t.Errorf("sequence name = %q", seq.Lookup("name").Text)
- }
- if bpm, _ := seq.Lookup("bpm").Float(); bpm != 128 {
- t.Errorf("bpm = %v", bpm)
- }
- }
- func TestGzipHeader(t *testing.T) {
- f := &File{Header: TestedHeader, Root: NewObject()}
- var b bytes.Buffer
- if err := f.Write(&b); err != nil {
- t.Fatal(err)
- }
- // ID, CM=deflate, FLG=0, MTIME=0, XFL=0, OS=3: the same ten bytes the device writes.
- want := []byte{0x1f, 0x8b, 8, 0, 0, 0, 0, 0, 0, 3}
- if got := b.Bytes()[:10]; !bytes.Equal(got, want) {
- t.Errorf("gzip header = % x, want % x", got, want)
- }
- }
- func TestLayout(t *testing.T) {
- in := `{"a": [1, 2.50, {}], "b": {"x": [], "y": "s<&>/"}, "c": null, "d": true, "e": -1e-05}`
- n, err := ParseJSON([]byte(in))
- if err != nil {
- t.Fatal(err)
- }
- got, err := n.MarshalJSON()
- if err != nil {
- t.Fatal(err)
- }
- want := "{\n\"a\": [\n1,\n2.50,\n{}\n],\n\"b\": {\n\"x\": [],\n\"y\": \"s<&>/\"\n},\n\"c\": null,\n\"d\": true,\n\"e\": -1e-05\n}"
- if string(got) != want {
- t.Errorf("got\n%s\nwant\n%s", got, want)
- }
- }
- func TestKeyOrderAndDuplicates(t *testing.T) {
- n, err := ParseJSON([]byte(`{"z": 1, "a": 2, "z": 3}`))
- if err != nil {
- t.Fatal(err)
- }
- got, _ := n.MarshalJSON()
- if want := "{\n\"z\": 1,\n\"a\": 2,\n\"z\": 3\n}"; string(got) != want {
- t.Errorf("got %q", got)
- }
- }
- func TestStringEscapes(t *testing.T) {
- s := "q\" b\\ n\n t\t c\x01 é ✓"
- n := NewString(s)
- got, _ := n.MarshalJSON()
- if want := `"q\" b\\ n\n t\t c\u0001 é ✓"`; string(got) != want {
- t.Errorf("got %s want %s", got, want)
- }
- back, err := ParseJSON(got)
- if err != nil || back.Text != s {
- t.Errorf("read back %q, %v", back.Text, err)
- }
- }
- func TestParseErrors(t *testing.T) {
- for _, in := range []string{``, `{`, `{"a": }`, `[1,]`, `{} {}`, `{"a": 1} x`, `nul`} {
- if _, err := ParseJSON([]byte(in)); err == nil {
- t.Errorf("ParseJSON(%q) succeeded", in)
- }
- }
- if _, err := Decode([]byte("ACVS\n1\n2\n3\n")); err == nil {
- t.Error("short header accepted")
- }
- if _, err := Decode([]byte("XXXX\n1\n2\n3\n4\n{}")); err == nil {
- t.Error("bad magic accepted")
- }
- if _, err := Decode([]byte("ACVS\n1\n2\n3\n4\n[]")); err == nil {
- t.Error("array body accepted")
- }
- if _, err := Read(strings.NewReader("not gzip")); err == nil {
- t.Error("non-gzip accepted")
- }
- }
- func TestInvalidNumberText(t *testing.T) {
- for _, s := range []string{"", "1.", ".5", "01", "+1", "1e", "true", `"1"`, "NaN", "1 "} {
- n := &Node{Kind: Number, Text: s}
- if _, err := n.MarshalJSON(); err == nil {
- t.Errorf("number text %q accepted", s)
- }
- }
- }
- func TestFormatFloat(t *testing.T) {
- // Expected values are Python's repr, which the reference writer used.
- cases := map[float64]string{
- 96: "96.0",
- 20: "20.0",
- 0: "0.0",
- math.Copysign(0, -1): "-0.0",
- 12.399999618530273: "12.399999618530273",
- 1e-05: "1e-05",
- 0.0001: "0.0001",
- 1e16: "1e+16",
- 1.5e16: "1.5e+16",
- 123456789012345.6: "123456789012345.6",
- 1e15: "1000000000000000.0",
- -2.5: "-2.5",
- 0.7079457640647888: "0.7079457640647888",
- 5e-324: "5e-324",
- 1.7976931348623157e308: "1.7976931348623157e+308",
- 0.1: "0.1",
- 100: "100.0",
- 9999999999999998: "9999999999999998.0",
- }
- for f, want := range cases {
- if got := FormatFloat(f); got != want {
- t.Errorf("FormatFloat(%v) = %q, want %q", f, got, want)
- }
- }
- // Tuning +12 semitones 40 cents, as the device writes it.
- if got := NewFloat32(12 + 40.0/100).Text; got != "12.399999618530273" {
- t.Errorf("NewFloat32(12.4) = %s", got)
- }
- if got := NewInt(-1).Text; got != "-1" {
- t.Errorf("NewInt(-1) = %s", got)
- }
- }
- func TestPaths(t *testing.T) {
- n, err := ParseJSON([]byte(`{"a": [{"b": [10, 20]}, {"c": "x"}], "d": {"e": 1.0}}`))
- if err != nil {
- t.Fatal(err)
- }
- get := func(p string) *Node {
- t.Helper()
- v, err := n.Get(p)
- if err != nil {
- t.Fatal(err)
- }
- return v
- }
- if get("a[0].b[1]").Text != "20" || get("a[1].c").Text != "x" || get("d.e").Text != "1.0" {
- t.Error("Get returned wrong nodes")
- }
- if get("") != n {
- t.Error("empty path is not the root")
- }
- for _, p := range []string{"a[2]", "a[0].x", "d[0]", "d.e.f", "a[", "a[-1]", "a[x]", ".a", "a..b", "a.", "a]", "a.[0]"} {
- if _, err := n.Get(p); err == nil {
- t.Errorf("Get(%q) succeeded", p)
- }
- }
- if err := n.Set("a[0].b[0]", NewInt(4)); err != nil {
- t.Fatal(err)
- }
- if err := n.Set("d.e", NewFloat(96)); err != nil {
- t.Fatal(err)
- }
- if err := n.Set("d.new", NewString("n")); err != nil {
- t.Fatal(err)
- }
- if err := n.Set("a[5]", NewNull()); err == nil {
- t.Error("Set past array end succeeded")
- }
- if err := n.Set("", NewNull()); err == nil {
- t.Error("Set root succeeded")
- }
- got, _ := n.MarshalJSON()
- want := "{\n\"a\": [\n{\n\"b\": [\n4,\n20\n]\n},\n{\n\"c\": \"x\"\n}\n],\n\"d\": {\n\"e\": 96.0,\n\"new\": \"n\"\n}\n}"
- if string(got) != want {
- t.Errorf("got\n%s", got)
- }
- }
- func TestArrayEditsAndClone(t *testing.T) {
- n, _ := ParseJSON([]byte(`{"s": [{"k": 0}]}`))
- arr, _ := n.Get("s")
- c := arr.Items[0].Clone()
- if err := c.SetKey("k", NewInt(1)); err != nil {
- t.Fatal(err)
- }
- if arr.Items[0].Lookup("k").Text != "0" {
- t.Fatal("clone shares data with its source")
- }
- if err := arr.Insert(0, c); err != nil {
- t.Fatal(err)
- }
- if err := arr.Append(NewInt(9)); err != nil {
- t.Fatal(err)
- }
- if err := arr.Insert(5, NewNull()); err == nil {
- t.Error("insert out of range succeeded")
- }
- if err := arr.Remove(1); err != nil {
- t.Fatal(err)
- }
- if err := arr.Remove(2); err == nil {
- t.Error("remove out of range succeeded")
- }
- got, _ := n.MarshalJSON()
- if want := "{\n\"s\": [\n{\n\"k\": 1\n},\n9\n]\n}"; string(got) != want {
- t.Errorf("got\n%s", got)
- }
- obj, _ := n.Get("s[0]")
- if !obj.DeleteKey("k") || obj.DeleteKey("k") {
- t.Error("DeleteKey")
- }
- if got, _ := obj.MarshalJSON(); string(got) != "{}" {
- t.Errorf("emptied object = %s", got)
- }
- f := &File{Header: TestedHeader, Root: n}
- fc := f.Clone()
- fc.Header[0] = "X"
- _ = fc.Root.SetKey("s", NewNull())
- if f.Header[0] != "ACVS" || n.Lookup("s").Kind != Array {
- t.Error("File.Clone shares data with its source")
- }
- }
- func TestNumberAccessors(t *testing.T) {
- for text, want := range map[string]int64{"4": 4, "-1": -1, "8212": 8212, "20.0": 20} {
- if got, err := (&Node{Kind: Number, Text: text}).Int(); err != nil || got != want {
- t.Errorf("Int(%s) = %d, %v", text, got, err)
- }
- }
- if _, err := (&Node{Kind: Number, Text: "1.5"}).Int(); err == nil {
- t.Error("Int(1.5) succeeded")
- }
- if _, err := NewString("1").Int(); err == nil {
- t.Error("Int on a string succeeded")
- }
- if f, err := NewFloat32(0.1).Float(); err != nil || f != float64(float32(0.1)) {
- t.Errorf("Float = %v, %v", f, err)
- }
- }
- func TestNilNode(t *testing.T) {
- var n *Node
- if _, err := n.Int(); err == nil {
- t.Error("Int on nil: no error")
- }
- if _, err := n.Float(); err == nil {
- t.Error("Float on nil: no error")
- }
- if n.Lookup("a") != nil {
- t.Error("Lookup on nil: not nil")
- }
- if _, err := n.Get("a[0]"); err == nil {
- t.Error("Get on nil: no error")
- }
- }
- // gzipPadded returns a gzip stream that decompresses to a minimal project
- // whose JSON body is padded with spaces to exactly size bytes.
- func gzipPadded(t *testing.T, size int) []byte {
- t.Helper()
- head := "ACVS\n1.3.0.12\nSerialisableProjectData\njson\nLinux\n{\"data\": {}"
- if size < len(head)+1 {
- t.Fatalf("size %d too small", size)
- }
- var b bytes.Buffer
- zw := gzip.NewWriter(&b)
- if _, err := io.WriteString(zw, head); err != nil {
- t.Fatal(err)
- }
- spaces := bytes.Repeat([]byte(" "), 1<<20)
- for left := size - len(head) - 1; left > 0; {
- n := min(left, len(spaces))
- if _, err := zw.Write(spaces[:n]); err != nil {
- t.Fatal(err)
- }
- left -= n
- }
- if _, err := io.WriteString(zw, "}"); err != nil {
- t.Fatal(err)
- }
- if err := zw.Close(); err != nil {
- t.Fatal(err)
- }
- return b.Bytes()
- }
- func TestDecompressionLimit(t *testing.T) {
- if testing.Short() {
- t.Skip("decompresses 1 GB")
- }
- // One byte over: refused.
- _, err := Read(bytes.NewReader(gzipPadded(t, MaxDecompressed+1)))
- want := "xpj: project is larger than 512 MB when decompressed"
- if err == nil || err.Error() != want {
- t.Fatalf("over the limit: got %v, want %q", err, want)
- }
- // Exactly at the limit: still decodes.
- f, err := Read(bytes.NewReader(gzipPadded(t, MaxDecompressed)))
- if err != nil {
- t.Fatalf("at the limit: %v", err)
- }
- if _, err := f.Data(); err != nil {
- t.Fatalf("at the limit: %v", err)
- }
- }
|