|
|
@@ -233,7 +233,7 @@ func TestAssignAndPoolReuse(t *testing.T) {
|
|
|
if a.File != "kick.wav" || a.SampleName != "kick" || a.End != 99 {
|
|
|
t.Errorf("pad A2 = %+v", a)
|
|
|
}
|
|
|
- if !p.Dirty {
|
|
|
+ if !p.dirty {
|
|
|
t.Error("not dirty after assign")
|
|
|
}
|
|
|
// Reassigning the same source to its only pad keeps the entry.
|
|
|
@@ -246,13 +246,13 @@ func TestAssignAndPoolReuse(t *testing.T) {
|
|
|
// temporary folder is behind one).
|
|
|
want, err := filepath.EvalSymlinks(kick)
|
|
|
must(t, err)
|
|
|
- if got := p.Sources["kick.wav"]; got != want {
|
|
|
+ if got := p.sources["kick.wav"]; got != want {
|
|
|
t.Errorf("source = %q, want %q", got, want)
|
|
|
}
|
|
|
// Replacing drops the old entry when no pad uses it.
|
|
|
snare := writeWAV(t, filepath.Join(dir, "snare.wav"), 50)
|
|
|
must(t, p.AssignSample(0, snare))
|
|
|
- if p.SampleCount() != 1 || p.inPool("kick.wav") || p.Sources["kick.wav"] != "" {
|
|
|
+ if p.SampleCount() != 1 || p.inPool("kick.wav") || p.sources["kick.wav"] != "" {
|
|
|
t.Errorf("replace did not drop kick: %d entries", p.SampleCount())
|
|
|
}
|
|
|
}
|
|
|
@@ -287,7 +287,7 @@ func TestSampleLimit(t *testing.T) {
|
|
|
p := newProject(t)
|
|
|
// With one sample per pad the pool can only fill up when it also holds
|
|
|
// entries no pad uses, as a loaded project may. Simulate one.
|
|
|
- must(t, p.addPoolEntry("unused.wav"))
|
|
|
+ p.addPoolEntry("unused.wav")
|
|
|
for i := 0; i < MaxSamples-1; i++ {
|
|
|
must(t, p.AssignSample(i, writeWAV(t, filepath.Join(dir, fmt.Sprintf("s%03d.wav", i)), 4)))
|
|
|
}
|
|
|
@@ -324,7 +324,7 @@ func TestAssignRejectsBadFiles(t *testing.T) {
|
|
|
if err := p.AssignSample(128, writeWAV(t, filepath.Join(dir, "ok.wav"), 4)); err == nil {
|
|
|
t.Error("slot 128 accepted")
|
|
|
}
|
|
|
- if p.Dirty || p.SampleCount() != 0 {
|
|
|
+ if p.dirty || p.SampleCount() != 0 {
|
|
|
t.Error("failed assigns changed the project")
|
|
|
}
|
|
|
}
|
|
|
@@ -379,12 +379,12 @@ func TestClearCopyMoveSwap(t *testing.T) {
|
|
|
}
|
|
|
|
|
|
// No-ops and range checks.
|
|
|
- p.Dirty = false
|
|
|
+ p.dirty = false
|
|
|
must(t, p.ClearPad(100))
|
|
|
must(t, p.CopyPad(1, 1))
|
|
|
must(t, p.MovePad(1, 1))
|
|
|
must(t, p.SwapPads(1, 1))
|
|
|
- if p.Dirty {
|
|
|
+ if p.dirty {
|
|
|
t.Error("no-op marked the project dirty")
|
|
|
}
|
|
|
for _, err := range []error{p.CopyPad(-1, 0), p.MovePad(0, 128), p.SwapPads(200, 0), p.ClearPad(128), p.SetTune(128, 0, 0), p.SetMuteGroup(-1, 0)} {
|
|
|
@@ -503,10 +503,10 @@ func TestLoadDemoFile(t *testing.T) {
|
|
|
if len(warnings) != 0 {
|
|
|
t.Errorf("warnings: %v", warnings)
|
|
|
}
|
|
|
- if p.Name != "DemoFile" || p.Dirty || p.SampleCount() != 6 {
|
|
|
- t.Errorf("name %q dirty %v samples %d", p.Name, p.Dirty, p.SampleCount())
|
|
|
+ if p.Name != "DemoFile" || p.dirty || p.SampleCount() != 6 {
|
|
|
+ t.Errorf("name %q dirty %v samples %d", p.Name, p.dirty, p.SampleCount())
|
|
|
}
|
|
|
- for file, src := range p.Sources {
|
|
|
+ for file, src := range p.sources {
|
|
|
if _, err := os.Stat(src); err != nil {
|
|
|
t.Errorf("source for %s: %v", file, err)
|
|
|
}
|
|
|
@@ -552,7 +552,7 @@ func TestBlankPadsAndTemplate(t *testing.T) {
|
|
|
func TestNames(t *testing.T) {
|
|
|
p := newProject(t)
|
|
|
must(t, p.SetName("My Kit 2"))
|
|
|
- if p.Name != "My Kit 2" || !p.Dirty {
|
|
|
+ if p.Name != "My Kit 2" || !p.dirty {
|
|
|
t.Error("SetName")
|
|
|
}
|
|
|
for _, bad := range []string{"", " ", "a:b", "a?", "end.", "end ", "CON", "com1.txt", "tab\there"} {
|
|
|
@@ -610,8 +610,8 @@ func TestAssignFollowsLink(t *testing.T) {
|
|
|
want, err := filepath.EvalSymlinks(kick)
|
|
|
must(t, err)
|
|
|
// The pool file is named after the link; it is copied from the target.
|
|
|
- if got := p.PoolFiles(); len(got) != 1 || got[0] != "favourite.wav" || p.Sources["favourite.wav"] != want {
|
|
|
- t.Errorf("pool %q, source %q; want favourite.wav from %q", got, p.Sources["favourite.wav"], want)
|
|
|
+ if got := p.PoolFiles(); len(got) != 1 || got[0] != "favourite.wav" || p.sources["favourite.wav"] != want {
|
|
|
+ t.Errorf("pool %q, source %q; want favourite.wav from %q", got, p.sources["favourite.wav"], want)
|
|
|
}
|
|
|
// The target itself is the same source: no second pool entry.
|
|
|
must(t, p.AssignSample(1, kick))
|
|
|
@@ -619,3 +619,119 @@ func TestAssignFollowsLink(t *testing.T) {
|
|
|
t.Errorf("%d pool entries, want 1", p.SampleCount())
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+// loadDemo loads a copy of the device's DemoFile, with its data folder,
|
|
|
+// after applying change to its data.
|
|
|
+func loadDemo(t *testing.T, change func(d *xpj.Node)) *Project {
|
|
|
+ t.Helper()
|
|
|
+ dir := t.TempDir()
|
|
|
+ f, err := xpj.ReadFile(filepath.Join(testdata, "device", "DemoFile.xpj"))
|
|
|
+ must(t, err)
|
|
|
+ d, err := f.Data()
|
|
|
+ must(t, err)
|
|
|
+ change(d)
|
|
|
+ path := filepath.Join(dir, "DemoFile.xpj")
|
|
|
+ must(t, f.WriteFile(path))
|
|
|
+ p, _, err := Load(path)
|
|
|
+ must(t, err)
|
|
|
+ return p
|
|
|
+}
|
|
|
+
|
|
|
+// unchanged fails the test if p differs from the encoding and sources
|
|
|
+// taken before, or has become dirty.
|
|
|
+func unchanged(t *testing.T, p *Project, before []byte, sources map[string]string) {
|
|
|
+ t.Helper()
|
|
|
+ if !bytes.Equal(encode(t, p), before) {
|
|
|
+ t.Error("the project file changed")
|
|
|
+ }
|
|
|
+ if fmt.Sprint(p.sources) != fmt.Sprint(sources) {
|
|
|
+ t.Errorf("sources changed: %v, want %v", p.sources, sources)
|
|
|
+ }
|
|
|
+ if p.Dirty() {
|
|
|
+ t.Error("dirty after a failed edit")
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestFailedEditsChangeNothing(t *testing.T) {
|
|
|
+ dir := t.TempDir()
|
|
|
+ good := writeWAV(t, filepath.Join(dir, "good.wav"), 100)
|
|
|
+ notWAV := filepath.Join(dir, "notes.wav")
|
|
|
+ must(t, os.WriteFile(notWAV, []byte("not a WAV file"), 0o644))
|
|
|
+
|
|
|
+ p := loadDemo(t, func(d *xpj.Node) {
|
|
|
+ // A2's first layer is not an object, B5 is not an object at all,
|
|
|
+ // and a clip of the first sequence has no value.
|
|
|
+ must(t, d.Set("tracks[0].program.drum.instruments[1].layersv[0]", xpj.NewInt(7)))
|
|
|
+ must(t, d.Set("tracks[0].program.drum.instruments[20]", xpj.NewInt(7)))
|
|
|
+ clip, err := d.Get("sequences[0].value.trackClipMaps[0][3]")
|
|
|
+ must(t, err)
|
|
|
+ clip.DeleteKey("value")
|
|
|
+ })
|
|
|
+ before := encode(t, p)
|
|
|
+ sources := map[string]string{}
|
|
|
+ for k, v := range p.sources {
|
|
|
+ sources[k] = v
|
|
|
+ }
|
|
|
+ seq := p.Sequences()[1] // sequences[0] in the file is slot 1
|
|
|
+
|
|
|
+ for _, c := range []struct {
|
|
|
+ name string
|
|
|
+ edit func() error
|
|
|
+ }{
|
|
|
+ {"assign a missing file", func() error { return p.AssignSample(0, filepath.Join(dir, "gone.wav")) }},
|
|
|
+ {"assign a file that is not a WAV", func() error { return p.AssignSample(0, notWAV) }},
|
|
|
+ {"tune a pad whose layer is not an object", func() error { return p.SetTune(1, 3, 0) }},
|
|
|
+ {"mute group on a pad that is not an object", func() error { return p.SetMuteGroup(20, 2) }},
|
|
|
+ {"sequence whose clip has no value", func() error { return p.SetSequence(seq.Slot, "Renamed", 99, 8) }},
|
|
|
+ {"bad slot", func() error { return p.SetTune(Pads, 0, 0) }},
|
|
|
+ } {
|
|
|
+ t.Run(c.name, func(t *testing.T) {
|
|
|
+ if err := c.edit(); err == nil {
|
|
|
+ t.Fatal("no error")
|
|
|
+ }
|
|
|
+ unchanged(t, p, before, sources)
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ // The sample limit: a full pool refuses a new file and changes nothing.
|
|
|
+ full := newProject(t)
|
|
|
+ for i := range MaxSamples - 1 {
|
|
|
+ must(t, full.AssignSample(i, writeWAV(t, filepath.Join(dir, fmt.Sprintf("g%03d.wav", i)), 4)))
|
|
|
+ }
|
|
|
+ full.addPoolEntry("unused.wav") // a 128th entry no pad uses
|
|
|
+ full.MarkExported()
|
|
|
+ before = encode(t, full)
|
|
|
+ sources = map[string]string{}
|
|
|
+ for k, v := range full.sources {
|
|
|
+ sources[k] = v
|
|
|
+ }
|
|
|
+ if err := full.AssignSample(MaxSamples-1, good); err == nil {
|
|
|
+ t.Fatal("129th sample accepted")
|
|
|
+ }
|
|
|
+ unchanged(t, full, before, sources)
|
|
|
+}
|
|
|
+
|
|
|
+func TestAssignReplacesBrokenPad(t *testing.T) {
|
|
|
+ // AssignSample starts from the blank pad, so a pad with no layers is
|
|
|
+ // replaced whole rather than failing halfway.
|
|
|
+ p := loadDemo(t, func(d *xpj.Node) {
|
|
|
+ must(t, d.Set("tracks[0].program.drum.instruments[5].layersv", xpj.NewArray()))
|
|
|
+ })
|
|
|
+ kick := writeWAV(t, filepath.Join(t.TempDir(), "kick.wav"), 100)
|
|
|
+ must(t, p.AssignSample(5, kick))
|
|
|
+ if v := padOf(t, p, 5); v.File != "kick.wav" || v.End != 99 {
|
|
|
+ t.Errorf("pad A6 = %+v", v)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestAssignReusesFreedName(t *testing.T) {
|
|
|
+ // Replacing a pad's only sample frees its name for the new file, as
|
|
|
+ // before edits were made all-or-nothing.
|
|
|
+ dir := t.TempDir()
|
|
|
+ p := newProject(t)
|
|
|
+ must(t, p.AssignSample(0, writeWAV(t, filepath.Join(dir, "a", "kick.wav"), 10)))
|
|
|
+ must(t, p.AssignSample(0, writeWAV(t, filepath.Join(dir, "b", "kick.wav"), 20)))
|
|
|
+ if got := p.PoolFiles(); len(got) != 1 || got[0] != "kick.wav" {
|
|
|
+ t.Errorf("pool %q, want [kick.wav]", got)
|
|
|
+ }
|
|
|
+}
|