load_test.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. package project
  2. import (
  3. "fmt"
  4. "os"
  5. "path/filepath"
  6. "strconv"
  7. "strings"
  8. "testing"
  9. "git.monoexp.com/bajdev/mpckit/internal/xpj"
  10. )
  11. // loadChanged reads the device's DemoFile, applies change to its data, writes
  12. // it to a temporary folder and loads it.
  13. func loadChanged(t *testing.T, change func(d *xpj.Node)) error {
  14. t.Helper()
  15. f, err := xpj.ReadFile(filepath.Join(testdata, "device", "DemoFile.xpj"))
  16. must(t, err)
  17. d, err := f.Data()
  18. must(t, err)
  19. change(d)
  20. path := filepath.Join(t.TempDir(), "Changed.xpj")
  21. must(t, f.WriteFile(path))
  22. _, _, err = Load(path)
  23. return err
  24. }
  25. func set(t *testing.T, d *xpj.Node, path string, v *xpj.Node) {
  26. t.Helper()
  27. must(t, d.Set(path, v))
  28. }
  29. func remove(t *testing.T, d *xpj.Node, path, key string) {
  30. t.Helper()
  31. n, err := d.Get(path)
  32. must(t, err)
  33. if !n.DeleteKey(key) {
  34. t.Fatalf("%s has no key %q", path, key)
  35. }
  36. }
  37. func TestLoadRejectsMalformed(t *testing.T) {
  38. for _, tc := range []struct {
  39. name string
  40. change func(t *testing.T, d *xpj.Node)
  41. want string // in the error
  42. }{
  43. {"parent folder", func(t *testing.T, d *xpj.Node) {
  44. set(t, d, "samples[0].path", xpj.NewString("../x.wav"))
  45. }, `samples[0].path: file name "../x.wav" contains a folder separator`},
  46. {"subfolder", func(t *testing.T, d *xpj.Node) {
  47. set(t, d, "samples[1].path", xpj.NewString("a/b.wav"))
  48. }, `samples[1].path: file name "a/b.wav" contains a folder separator`},
  49. {"backslash", func(t *testing.T, d *xpj.Node) {
  50. set(t, d, "samples[1].path", xpj.NewString(`..\b.wav`))
  51. }, `samples[1].path: file name "..\\b.wav" contains a folder separator`},
  52. {"dot dot", func(t *testing.T, d *xpj.Node) {
  53. set(t, d, "samples[2].path", xpj.NewString(".."))
  54. }, `samples[2].path: ".." is not a file name`},
  55. {"empty", func(t *testing.T, d *xpj.Node) {
  56. set(t, d, "samples[0].path", xpj.NewString(""))
  57. }, `samples[0].path: file name is empty`},
  58. {"control character", func(t *testing.T, d *xpj.Node) {
  59. set(t, d, "samples[0].path", xpj.NewString("a\x00.wav"))
  60. }, `samples[0].path: file name "a\x00.wav" contains a control character`},
  61. {"track pool", func(t *testing.T, d *xpj.Node) {
  62. set(t, d, "tracks[0].samples[0].path", xpj.NewString("../x.wav"))
  63. }, `tracks[0].samples[0].path: file name "../x.wav"`},
  64. {"missing path", func(t *testing.T, d *xpj.Node) {
  65. remove(t, d, "samples[3]", "path")
  66. }, "samples[3].path is missing or not text"},
  67. {"path not text", func(t *testing.T, d *xpj.Node) {
  68. set(t, d, "samples[3].path", xpj.NewInt(3))
  69. }, "samples[3].path is missing or not text"},
  70. {"layer outside", func(t *testing.T, d *xpj.Node) {
  71. set(t, d, "tracks[0].program.drum.instruments[17].layersv[2].sampleFile", xpj.NewString("../x.wav"))
  72. }, `pad B2 layer 3 sampleFile: file name "../x.wav" contains a folder separator`},
  73. {"layer not text", func(t *testing.T, d *xpj.Node) {
  74. set(t, d, "tracks[0].program.drum.instruments[0].layersv[0].sampleFile", xpj.NewNull())
  75. }, "pad A1 layer 1 sampleFile is not text"},
  76. {"missing bpm", func(t *testing.T, d *xpj.Node) {
  77. remove(t, d, "sequences[0].value", "bpm")
  78. }, "sequences[0]: bpm is missing or not a number"},
  79. {"missing key", func(t *testing.T, d *xpj.Node) {
  80. remove(t, d, "sequences[1]", "key")
  81. }, "sequences[1]: key is missing or not a whole number"},
  82. {"missing value", func(t *testing.T, d *xpj.Node) {
  83. set(t, d, "sequences[1].value", xpj.NewArray())
  84. }, "sequences[1]: value is missing or not an object"},
  85. {"missing length", func(t *testing.T, d *xpj.Node) {
  86. remove(t, d, "sequences[0].value", "lengthBars")
  87. }, "sequences[0]: lengthBars is missing or not a whole number"},
  88. {"name not text", func(t *testing.T, d *xpj.Node) {
  89. set(t, d, "sequences[0].value.name", xpj.NewInt(1))
  90. }, "sequences[0]: name is missing or not text"},
  91. } {
  92. t.Run(tc.name, func(t *testing.T) {
  93. err := loadChanged(t, func(d *xpj.Node) { tc.change(t, d) })
  94. if err == nil || !strings.Contains(err.Error(), tc.want) {
  95. t.Fatalf("got error %v, want one containing %q", err, tc.want)
  96. }
  97. })
  98. }
  99. }
  100. func TestLoadAcceptsMissingLayerFile(t *testing.T) {
  101. err := loadChanged(t, func(d *xpj.Node) {
  102. remove(t, d, "tracks[0].program.drum.instruments[0].layersv[0]", "sampleFile")
  103. })
  104. must(t, err)
  105. }
  106. func TestLoadMissingVersion(t *testing.T) {
  107. f, err := xpj.ReadFile(filepath.Join(testdata, "device", "DemoFile.xpj"))
  108. must(t, err)
  109. d, err := f.Data()
  110. must(t, err)
  111. d.DeleteKey("version")
  112. path := filepath.Join(t.TempDir(), "NoVersion.xpj")
  113. must(t, f.WriteFile(path))
  114. _, warnings, err := Load(path)
  115. must(t, err)
  116. if len(warnings) != 1 || !strings.Contains(warnings[0], "version is missing") {
  117. t.Fatalf("warnings = %q", warnings)
  118. }
  119. }
  120. func TestPlainFileName(t *testing.T) {
  121. for _, ok := range []string{"kick.wav", "80s-Kik-Dreamer2 SA.wav", "a..b.wav", ".hidden.wav", "Ünïcode.wav"} {
  122. if err := PlainFileName(ok); err != nil {
  123. t.Errorf("PlainFileName(%q) = %v", ok, err)
  124. }
  125. }
  126. for _, bad := range []string{"", ".", "..", "../x.wav", "a/b.wav", `a\b.wav`, "/x.wav", "x\n.wav", "x\x7f.wav"} {
  127. if err := PlainFileName(bad); err == nil {
  128. t.Errorf("PlainFileName(%q) = nil", bad)
  129. }
  130. }
  131. }
  132. // FuzzLoad loads arbitrary files; none may make the read-only calls panic.
  133. func FuzzLoad(f *testing.F) {
  134. seeds, err := filepath.Glob(filepath.Join(testdata, "device", "*.xpj"))
  135. if err != nil || len(seeds) == 0 {
  136. f.Fatalf("no seed files: %v", err)
  137. }
  138. for _, s := range seeds {
  139. b, err := os.ReadFile(s)
  140. if err != nil {
  141. f.Fatal(err)
  142. }
  143. f.Add(b)
  144. }
  145. f.Fuzz(func(t *testing.T, b []byte) {
  146. path := filepath.Join(t.TempDir(), "Fuzz.xpj")
  147. if err := os.WriteFile(path, b, 0o644); err != nil {
  148. t.Fatal(err)
  149. }
  150. p, _, err := Load(path)
  151. if err != nil {
  152. return
  153. }
  154. for slot := range Pads {
  155. _, _ = p.Pad(slot)
  156. }
  157. p.Sequences()
  158. p.PoolFiles()
  159. })
  160. }
  161. // TestBrokenFieldsDoNotPanic changes, one at a time, each field that project
  162. // reads in DemoFile: replaced by null, text, a negative or large number, an
  163. // empty list or object, or deleted. Whatever wrap makes of the result, reads
  164. // and edits must not panic. Only the first item of each list is changed, so
  165. // the test stays fast; a full sweep (all fields to depth 5, 12,726 cases)
  166. // found nothing either and takes about 10 minutes.
  167. func TestBrokenFieldsDoNotPanic(t *testing.T) {
  168. if testing.Short() {
  169. t.Skip("slow")
  170. }
  171. base, err := xpj.ReadFile(filepath.Join(testdata, "device", "DemoFile.xpj"))
  172. must(t, err)
  173. d, err := base.Data()
  174. must(t, err)
  175. events := "sequences[0].value.trackClipMaps[0][0].value.eventList.events"
  176. roots := []struct {
  177. path string
  178. depth int
  179. }{
  180. {"version", 0}, {"currentSequence", 0}, {"tracks[0].name", 0},
  181. {"samples", 3}, {"tracks[0].samples", 2},
  182. {"tracks[0].program.drum.instruments", 1},
  183. {"tracks[0].program.drum.instruments[0]", 1},
  184. {"tracks[0].program.drum.instruments[0].layersv", 2},
  185. {"tracks[0].program.drum.instruments[0].layersv[0].sliceInfo", 1},
  186. {"sequences", 3},
  187. {"sequences[0].value.trackClipMaps", 4},
  188. {events, 3},
  189. }
  190. // The first event is automation; add the first note event too.
  191. evs, err := d.Get(events)
  192. must(t, err)
  193. for i, ev := range evs.Items {
  194. if num(ev.Lookup("type")) == eventNote {
  195. roots = append(roots, struct {
  196. path string
  197. depth int
  198. }{fmt.Sprintf("%s[%d]", events, i), 1}, struct {
  199. path string
  200. depth int
  201. }{fmt.Sprintf("%s[%d].note.note", events, i), 0})
  202. break
  203. }
  204. }
  205. var paths []string
  206. for _, r := range roots {
  207. n, err := d.Get(r.path)
  208. must(t, err)
  209. fieldPaths(n, r.path, r.depth, &paths)
  210. }
  211. changes := []func(parent *xpj.Node, key string, i int){
  212. func(p *xpj.Node, k string, i int) { replace(p, k, i, xpj.NewNull()) },
  213. func(p *xpj.Node, k string, i int) { replace(p, k, i, xpj.NewString("../x.wav")) },
  214. func(p *xpj.Node, k string, i int) { replace(p, k, i, xpj.NewInt(-1)) },
  215. func(p *xpj.Node, k string, i int) { replace(p, k, i, xpj.NewInt(1000)) },
  216. func(p *xpj.Node, k string, i int) { replace(p, k, i, xpj.NewArray()) },
  217. func(p *xpj.Node, k string, i int) { replace(p, k, i, xpj.NewObject()) },
  218. func(p *xpj.Node, k string, i int) {
  219. if k != "" {
  220. p.DeleteKey(k)
  221. } else {
  222. _ = p.Remove(i)
  223. }
  224. },
  225. }
  226. names := []string{"null", "text", "-1", "1000", "list", "object", "deleted"}
  227. for _, path := range paths {
  228. parentPath, key, i := splitPath(path)
  229. for c, change := range changes {
  230. f := base.Clone()
  231. fd, _ := f.Data()
  232. parent, err := fd.Get(parentPath)
  233. if err != nil {
  234. continue // a key holding '.' or '[' cannot be addressed by path
  235. }
  236. change(parent, key, i)
  237. func() {
  238. defer func() {
  239. if r := recover(); r != nil {
  240. t.Errorf("%s %s: panic: %v", path, names[c], r)
  241. }
  242. }()
  243. p, err := wrap(f, "Broken", "")
  244. if err != nil {
  245. return
  246. }
  247. exercise(p)
  248. }()
  249. }
  250. }
  251. t.Logf("%d fields, %d cases", len(paths), len(paths)*len(changes))
  252. }
  253. // fieldPaths lists path and the paths below n to depth levels, taking only
  254. // the first item of each list.
  255. func fieldPaths(n *xpj.Node, path string, depth int, out *[]string) {
  256. *out = append(*out, path)
  257. if depth == 0 {
  258. return
  259. }
  260. if len(n.Items) > 0 {
  261. fieldPaths(n.Items[0], path+"[0]", depth-1, out)
  262. }
  263. for _, m := range n.Members {
  264. fieldPaths(m.Value, path+"."+m.Key, depth-1, out)
  265. }
  266. }
  267. // splitPath splits a path into its parent and last step: a key, or an index
  268. // when key is "".
  269. func splitPath(path string) (parent, key string, i int) {
  270. if strings.HasSuffix(path, "]") {
  271. open := strings.LastIndexByte(path, '[')
  272. i, _ = strconv.Atoi(path[open+1 : len(path)-1])
  273. return path[:open], "", i
  274. }
  275. dot := strings.LastIndexByte(path, '.')
  276. if dot < 0 {
  277. return "", path, 0
  278. }
  279. return path[:dot], path[dot+1:], 0
  280. }
  281. func replace(parent *xpj.Node, key string, i int, v *xpj.Node) {
  282. if key != "" {
  283. _ = parent.SetKey(key, v)
  284. } else if i < len(parent.Items) {
  285. parent.Items[i] = v
  286. }
  287. }
  288. // exercise runs every read and most edits on p.
  289. func exercise(p *Project) {
  290. for slot := range Pads {
  291. _, _ = p.Pad(slot)
  292. p.PadHasNotes(slot)
  293. }
  294. p.PoolFiles()
  295. for slot := range 2 {
  296. _ = p.SetTune(slot, 1, 1)
  297. _ = p.SetMuteGroup(slot, 1)
  298. _ = p.CopyPad(slot, slot+20)
  299. _ = p.MovePad(slot+20, slot+40)
  300. _ = p.SwapPads(slot, slot+1)
  301. _ = p.ClearPad(slot + 1)
  302. }
  303. for _, s := range p.Sequences() {
  304. _ = p.SetSequence(s.Slot, "Changed", 100, 4)
  305. }
  306. _ = p.AddSequence(50)
  307. _ = p.RemoveSequence(50)
  308. _ = p.RemoveSequence(0)
  309. }