load_test.go 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. package project
  2. import (
  3. "os"
  4. "path/filepath"
  5. "strings"
  6. "testing"
  7. "git.monoexp.com/bajdev/mpckit/internal/xpj"
  8. )
  9. // loadChanged reads the device's DemoFile, applies change to its data, writes
  10. // it to a temporary folder and loads it.
  11. func loadChanged(t *testing.T, change func(d *xpj.Node)) error {
  12. t.Helper()
  13. f, err := xpj.ReadFile(filepath.Join(testdata, "device", "DemoFile.xpj"))
  14. must(t, err)
  15. d, err := f.Data()
  16. must(t, err)
  17. change(d)
  18. path := filepath.Join(t.TempDir(), "Changed.xpj")
  19. must(t, f.WriteFile(path))
  20. _, _, err = Load(path)
  21. return err
  22. }
  23. func set(t *testing.T, d *xpj.Node, path string, v *xpj.Node) {
  24. t.Helper()
  25. must(t, d.Set(path, v))
  26. }
  27. func remove(t *testing.T, d *xpj.Node, path, key string) {
  28. t.Helper()
  29. n, err := d.Get(path)
  30. must(t, err)
  31. if !n.DeleteKey(key) {
  32. t.Fatalf("%s has no key %q", path, key)
  33. }
  34. }
  35. func TestLoadRejectsMalformed(t *testing.T) {
  36. for _, tc := range []struct {
  37. name string
  38. change func(t *testing.T, d *xpj.Node)
  39. want string // in the error
  40. }{
  41. {"parent folder", func(t *testing.T, d *xpj.Node) {
  42. set(t, d, "samples[0].path", xpj.NewString("../x.wav"))
  43. }, `samples[0].path: file name "../x.wav" contains a folder separator`},
  44. {"subfolder", func(t *testing.T, d *xpj.Node) {
  45. set(t, d, "samples[1].path", xpj.NewString("a/b.wav"))
  46. }, `samples[1].path: file name "a/b.wav" contains a folder separator`},
  47. {"backslash", func(t *testing.T, d *xpj.Node) {
  48. set(t, d, "samples[1].path", xpj.NewString(`..\b.wav`))
  49. }, `samples[1].path: file name "..\\b.wav" contains a folder separator`},
  50. {"dot dot", func(t *testing.T, d *xpj.Node) {
  51. set(t, d, "samples[2].path", xpj.NewString(".."))
  52. }, `samples[2].path: ".." is not a file name`},
  53. {"empty", func(t *testing.T, d *xpj.Node) {
  54. set(t, d, "samples[0].path", xpj.NewString(""))
  55. }, `samples[0].path: file name is empty`},
  56. {"control character", func(t *testing.T, d *xpj.Node) {
  57. set(t, d, "samples[0].path", xpj.NewString("a\x00.wav"))
  58. }, `samples[0].path: file name "a\x00.wav" contains a control character`},
  59. {"track pool", func(t *testing.T, d *xpj.Node) {
  60. set(t, d, "tracks[0].samples[0].path", xpj.NewString("../x.wav"))
  61. }, `tracks[0].samples[0].path: file name "../x.wav"`},
  62. {"missing path", func(t *testing.T, d *xpj.Node) {
  63. remove(t, d, "samples[3]", "path")
  64. }, "samples[3].path is missing or not text"},
  65. {"path not text", func(t *testing.T, d *xpj.Node) {
  66. set(t, d, "samples[3].path", xpj.NewInt(3))
  67. }, "samples[3].path is missing or not text"},
  68. {"layer outside", func(t *testing.T, d *xpj.Node) {
  69. set(t, d, "tracks[0].program.drum.instruments[17].layersv[2].sampleFile", xpj.NewString("../x.wav"))
  70. }, `pad B2 layer 3 sampleFile: file name "../x.wav" contains a folder separator`},
  71. {"layer not text", func(t *testing.T, d *xpj.Node) {
  72. set(t, d, "tracks[0].program.drum.instruments[0].layersv[0].sampleFile", xpj.NewNull())
  73. }, "pad A1 layer 1 sampleFile is not text"},
  74. {"missing bpm", func(t *testing.T, d *xpj.Node) {
  75. remove(t, d, "sequences[0].value", "bpm")
  76. }, "sequences[0]: bpm is missing or not a number"},
  77. {"missing key", func(t *testing.T, d *xpj.Node) {
  78. remove(t, d, "sequences[1]", "key")
  79. }, "sequences[1]: key is missing or not a whole number"},
  80. {"missing value", func(t *testing.T, d *xpj.Node) {
  81. set(t, d, "sequences[1].value", xpj.NewArray())
  82. }, "sequences[1]: value is missing or not an object"},
  83. {"missing length", func(t *testing.T, d *xpj.Node) {
  84. remove(t, d, "sequences[0].value", "lengthBars")
  85. }, "sequences[0]: lengthBars is missing or not a whole number"},
  86. {"name not text", func(t *testing.T, d *xpj.Node) {
  87. set(t, d, "sequences[0].value.name", xpj.NewInt(1))
  88. }, "sequences[0]: name is missing or not text"},
  89. } {
  90. t.Run(tc.name, func(t *testing.T) {
  91. err := loadChanged(t, func(d *xpj.Node) { tc.change(t, d) })
  92. if err == nil || !strings.Contains(err.Error(), tc.want) {
  93. t.Fatalf("got error %v, want one containing %q", err, tc.want)
  94. }
  95. })
  96. }
  97. }
  98. func TestLoadAcceptsMissingLayerFile(t *testing.T) {
  99. err := loadChanged(t, func(d *xpj.Node) {
  100. remove(t, d, "tracks[0].program.drum.instruments[0].layersv[0]", "sampleFile")
  101. })
  102. must(t, err)
  103. }
  104. func TestLoadMissingVersion(t *testing.T) {
  105. f, err := xpj.ReadFile(filepath.Join(testdata, "device", "DemoFile.xpj"))
  106. must(t, err)
  107. d, err := f.Data()
  108. must(t, err)
  109. d.DeleteKey("version")
  110. path := filepath.Join(t.TempDir(), "NoVersion.xpj")
  111. must(t, f.WriteFile(path))
  112. _, warnings, err := Load(path)
  113. must(t, err)
  114. if len(warnings) != 1 || !strings.Contains(warnings[0], "version is missing") {
  115. t.Fatalf("warnings = %q", warnings)
  116. }
  117. }
  118. func TestPlainFileName(t *testing.T) {
  119. for _, ok := range []string{"kick.wav", "80s-Kik-Dreamer2 SA.wav", "a..b.wav", ".hidden.wav", "Ünïcode.wav"} {
  120. if err := PlainFileName(ok); err != nil {
  121. t.Errorf("PlainFileName(%q) = %v", ok, err)
  122. }
  123. }
  124. for _, bad := range []string{"", ".", "..", "../x.wav", "a/b.wav", `a\b.wav`, "/x.wav", "x\n.wav", "x\x7f.wav"} {
  125. if err := PlainFileName(bad); err == nil {
  126. t.Errorf("PlainFileName(%q) = nil", bad)
  127. }
  128. }
  129. }
  130. // FuzzLoad loads arbitrary files; none may make the read-only calls panic.
  131. func FuzzLoad(f *testing.F) {
  132. seeds, err := filepath.Glob(filepath.Join(testdata, "device", "*.xpj"))
  133. if err != nil || len(seeds) == 0 {
  134. f.Fatalf("no seed files: %v", err)
  135. }
  136. for _, s := range seeds {
  137. b, err := os.ReadFile(s)
  138. if err != nil {
  139. f.Fatal(err)
  140. }
  141. f.Add(b)
  142. }
  143. f.Fuzz(func(t *testing.T, b []byte) {
  144. path := filepath.Join(t.TempDir(), "Fuzz.xpj")
  145. if err := os.WriteFile(path, b, 0o644); err != nil {
  146. t.Fatal(err)
  147. }
  148. p, _, err := Load(path)
  149. if err != nil {
  150. return
  151. }
  152. for slot := range Pads {
  153. _, _ = p.Pad(slot)
  154. }
  155. p.Sequences()
  156. p.PoolFiles()
  157. })
  158. }