|
|
@@ -5,14 +5,18 @@ import (
|
|
|
"bytes"
|
|
|
"compress/gzip"
|
|
|
"errors"
|
|
|
+ "fmt"
|
|
|
"io"
|
|
|
+ "io/fs"
|
|
|
"os"
|
|
|
"path/filepath"
|
|
|
+ "runtime"
|
|
|
"sort"
|
|
|
"strings"
|
|
|
"testing"
|
|
|
|
|
|
"git.monoexp.com/bajdev/mpckit/internal/project"
|
|
|
+ "git.monoexp.com/bajdev/mpckit/internal/xpj"
|
|
|
)
|
|
|
|
|
|
const testdata = "../../testdata"
|
|
|
@@ -214,3 +218,280 @@ func TestZip(t *testing.T) {
|
|
|
t.Errorf("temporary files left behind: %v", got)
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+// snapshot returns every file and folder below dir with its contents
|
|
|
+// ("<dir>" for a folder).
|
|
|
+func snapshot(t *testing.T, dir string) map[string]string {
|
|
|
+ t.Helper()
|
|
|
+ out := map[string]string{}
|
|
|
+ err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
|
|
|
+ if err != nil || path == dir {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ rel, _ := filepath.Rel(dir, path)
|
|
|
+ if d.IsDir() {
|
|
|
+ out[rel] = "<dir>"
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+ b, err := os.ReadFile(path)
|
|
|
+ out[rel] = string(b)
|
|
|
+ return err
|
|
|
+ })
|
|
|
+ must(t, err)
|
|
|
+ return out
|
|
|
+}
|
|
|
+
|
|
|
+func sameSnapshot(t *testing.T, got, want map[string]string) {
|
|
|
+ t.Helper()
|
|
|
+ if len(got) != len(want) {
|
|
|
+ t.Errorf("%d entries, want %d: %v", len(got), len(want), keys(got))
|
|
|
+ }
|
|
|
+ for k, v := range want {
|
|
|
+ if got[k] != v {
|
|
|
+ t.Errorf("%s changed or missing", k)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func keys(m map[string]string) []string {
|
|
|
+ var out []string
|
|
|
+ for k := range m {
|
|
|
+ out = append(out, k)
|
|
|
+ }
|
|
|
+ sort.Strings(out)
|
|
|
+ return out
|
|
|
+}
|
|
|
+
|
|
|
+// symlink makes a link, or skips the test where the system does not allow
|
|
|
+// it (Windows without developer mode).
|
|
|
+func symlink(t *testing.T, target, link string) {
|
|
|
+ t.Helper()
|
|
|
+ if err := os.Symlink(target, link); err != nil {
|
|
|
+ t.Skipf("cannot create a symlink: %v", err)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestSymlinkedSource(t *testing.T) {
|
|
|
+ // A copy of MpckitT1 whose data folder holds a link instead of a WAV.
|
|
|
+ src := t.TempDir()
|
|
|
+ must(t, os.WriteFile(filepath.Join(src, "Kit.xpj"), readFile(t, filepath.Join(testdata, "accepted", "MpckitT1.xpj")), 0o644))
|
|
|
+ data := filepath.Join(src, "Kit"+project.DataSuffix)
|
|
|
+ must(t, os.Mkdir(data, 0o755))
|
|
|
+ acc := filepath.Join(testdata, "accepted", "MpckitT1"+project.DataSuffix)
|
|
|
+ must(t, os.WriteFile(filepath.Join(data, "Linn 9000 kick.wav"), readFile(t, filepath.Join(acc, "Linn 9000 kick.wav")), 0o644))
|
|
|
+ outside := filepath.Join(t.TempDir(), "secret.txt")
|
|
|
+ must(t, os.WriteFile(outside, []byte("secret"), 0o644))
|
|
|
+ link := filepath.Join(data, "Linn 9000 kick001.wav")
|
|
|
+ symlink(t, outside, link)
|
|
|
+
|
|
|
+ if Usable(link) || !Usable(filepath.Join(data, "Linn 9000 kick.wav")) || Usable(data) || Usable(outside+"x") {
|
|
|
+ t.Error("Usable: wrong answer for a link, a file, a folder or a missing file")
|
|
|
+ }
|
|
|
+ p, _, err := project.Load(filepath.Join(src, "Kit.xpj"))
|
|
|
+ must(t, err)
|
|
|
+ dest := t.TempDir()
|
|
|
+ _, err = Folder(p, dest, false)
|
|
|
+ var missing *MissingError
|
|
|
+ if !errors.As(err, &missing) || strings.Join(missing.Files, ",") != "Linn 9000 kick001.wav" {
|
|
|
+ t.Fatalf("Folder: %v", err)
|
|
|
+ }
|
|
|
+ if err := Zip(p, filepath.Join(dest, "kit.zip"), false); !errors.As(err, &missing) {
|
|
|
+ t.Fatalf("Zip: %v", err)
|
|
|
+ }
|
|
|
+ if got := listDir(t, dest); len(got) != 0 {
|
|
|
+ t.Errorf("failed export wrote %v", got)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestLinkedDataFolder(t *testing.T) {
|
|
|
+ // The whole data folder is a link to a folder holding a file with a
|
|
|
+ // pool file's name.
|
|
|
+ src := t.TempDir()
|
|
|
+ must(t, os.WriteFile(filepath.Join(src, "Kit.xpj"), readFile(t, filepath.Join(testdata, "accepted", "MpckitT1.xpj")), 0o644))
|
|
|
+ elsewhere := t.TempDir()
|
|
|
+ for _, f := range []string{"Linn 9000 kick.wav", "Linn 9000 kick001.wav"} {
|
|
|
+ must(t, os.WriteFile(filepath.Join(elsewhere, f), []byte("private"), 0o644))
|
|
|
+ }
|
|
|
+ symlink(t, elsewhere, filepath.Join(src, "Kit"+project.DataSuffix))
|
|
|
+
|
|
|
+ p, _, err := project.Load(filepath.Join(src, "Kit.xpj"))
|
|
|
+ must(t, err)
|
|
|
+ dest := t.TempDir()
|
|
|
+ _, err = Folder(p, dest, false)
|
|
|
+ var missing *MissingError
|
|
|
+ if !errors.As(err, &missing) || len(missing.Files) != 2 {
|
|
|
+ t.Fatalf("Folder: %v", err)
|
|
|
+ }
|
|
|
+ if got := listDir(t, dest); len(got) != 0 {
|
|
|
+ t.Errorf("failed export wrote %v", got)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestBadPoolName(t *testing.T) {
|
|
|
+ p := buildT1(t)
|
|
|
+ // Load and AssignSample never produce such a name; change the tree
|
|
|
+ // directly to reach the export check.
|
|
|
+ d, err := p.File().Data()
|
|
|
+ must(t, err)
|
|
|
+ src := p.Sources["Linn 9000 kick.wav"]
|
|
|
+ must(t, d.Set("samples[0].path", xpj.NewString("../evil.wav")))
|
|
|
+ p.Sources["../evil.wav"] = src
|
|
|
+
|
|
|
+ dest := filepath.Join(t.TempDir(), "out")
|
|
|
+ must(t, os.Mkdir(dest, 0o755))
|
|
|
+ want := `sample "../evil.wav": file name "../evil.wav" contains a folder separator`
|
|
|
+ if _, err := Folder(p, dest, false); err == nil || err.Error() != want {
|
|
|
+ t.Errorf("Folder: got %v, want %q", err, want)
|
|
|
+ }
|
|
|
+ if err := Zip(p, filepath.Join(dest, "kit.zip"), false); err == nil || err.Error() != want {
|
|
|
+ t.Errorf("Zip: got %v, want %q", err, want)
|
|
|
+ }
|
|
|
+ if got := listDir(t, dest); len(got) != 0 {
|
|
|
+ t.Errorf("failed export wrote %v", got)
|
|
|
+ }
|
|
|
+ if _, err := os.Stat(filepath.Join(filepath.Dir(dest), "evil.wav")); err == nil {
|
|
|
+ t.Error("export wrote outside the data folder")
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestMissingDestination(t *testing.T) {
|
|
|
+ dest := filepath.Join(t.TempDir(), "gone")
|
|
|
+ _, err := Folder(buildT1(t), dest, false)
|
|
|
+ if want := "export folder does not exist: " + dest; err == nil || err.Error() != want {
|
|
|
+ t.Errorf("got %v, want %q", err, want)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// failRename makes the listed renames from now fail, counting from 1, until
|
|
|
+// the test ends.
|
|
|
+func failRename(t *testing.T, fail ...int) {
|
|
|
+ t.Helper()
|
|
|
+ calls := 0
|
|
|
+ rename = func(from, to string) error {
|
|
|
+ calls++
|
|
|
+ for _, n := range fail {
|
|
|
+ if calls == n {
|
|
|
+ return &os.LinkError{Op: "rename", Old: from, New: to, Err: errors.New("test failure")}
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return os.Rename(from, to)
|
|
|
+ }
|
|
|
+ t.Cleanup(func() { rename = os.Rename })
|
|
|
+}
|
|
|
+
|
|
|
+func TestOverwriteFailureKeepsOldExport(t *testing.T) {
|
|
|
+ // The renames of an overwrite: 1 old data folder aside, 2 old .xpj
|
|
|
+ // aside, 3 new data folder in, 4 new .xpj in. Failing any of them
|
|
|
+ // leaves the previous export exactly as it was.
|
|
|
+ for n := 1; n <= 4; n++ {
|
|
|
+ t.Run(fmt.Sprint(n), func(t *testing.T) {
|
|
|
+ dest := t.TempDir()
|
|
|
+ _, err := Folder(buildT1(t), dest, false)
|
|
|
+ must(t, err)
|
|
|
+ before := snapshot(t, dest)
|
|
|
+
|
|
|
+ changed := buildT1(t)
|
|
|
+ must(t, changed.ClearPad(16))
|
|
|
+ failRename(t, n)
|
|
|
+ if _, err := Folder(changed, dest, true); err == nil || !strings.Contains(err.Error(), "test failure") {
|
|
|
+ t.Fatalf("Folder: %v", err)
|
|
|
+ }
|
|
|
+ sameSnapshot(t, snapshot(t, dest), before)
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestOverwriteUndoFails(t *testing.T) {
|
|
|
+ dest := t.TempDir()
|
|
|
+ _, err := Folder(buildT1(t), dest, false)
|
|
|
+ must(t, err)
|
|
|
+ before := snapshot(t, dest)
|
|
|
+
|
|
|
+ // The new .xpj cannot be moved in (4), and moving the new data folder
|
|
|
+ // back out fails too (5): the old pair stays in the temporary folder.
|
|
|
+ failRename(t, 4, 5)
|
|
|
+ _, err = Folder(buildT1(t), dest, true)
|
|
|
+ if err == nil || !strings.Contains(err.Error(), "could not be put back and is kept in") {
|
|
|
+ t.Fatalf("Folder: %v", err)
|
|
|
+ }
|
|
|
+ _, kept, _ := strings.Cut(err.Error(), "is kept in ")
|
|
|
+ if got := snapshot(t, kept); len(got) != len(before) {
|
|
|
+ t.Errorf("kept %v, want the %d entries of the old export", keys(got), len(before))
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestOverwriteHalfPair(t *testing.T) {
|
|
|
+ // Only the .xpj of an earlier export is there: it is replaced and the
|
|
|
+ // data folder created.
|
|
|
+ dest := t.TempDir()
|
|
|
+ must(t, os.WriteFile(filepath.Join(dest, "MpckitT1.xpj"), []byte("old"), 0o644))
|
|
|
+ _, err := Folder(buildT1(t), dest, true)
|
|
|
+ must(t, err)
|
|
|
+ if got := listDir(t, dest); strings.Join(got, "|") != "MpckitT1.xpj|MpckitT1_[ProjectData]" {
|
|
|
+ t.Errorf("dest holds %v", got)
|
|
|
+ }
|
|
|
+ if string(readFile(t, filepath.Join(dest, "MpckitT1.xpj"))) == "old" {
|
|
|
+ t.Error("old .xpj not replaced")
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestBackslashNameExportsAndLoads(t *testing.T) {
|
|
|
+ if runtime.GOOS == "windows" {
|
|
|
+ t.Skip("Windows file names cannot hold a backslash")
|
|
|
+ }
|
|
|
+ src := filepath.Join(t.TempDir(), `kick\one.wav`)
|
|
|
+ must(t, os.WriteFile(src, readFile(t, filepath.Join(testdata, "accepted", "MpckitT1"+project.DataSuffix, "Linn 9000 kick.wav")), 0o644))
|
|
|
+ p, err := project.New("Backslash")
|
|
|
+ must(t, err)
|
|
|
+ must(t, p.AssignSample(0, src))
|
|
|
+ if got := p.PoolFiles(); len(got) != 1 || got[0] != "kick_one.wav" {
|
|
|
+ t.Fatalf("pool = %q, want kick_one.wav", got)
|
|
|
+ }
|
|
|
+ dest := t.TempDir()
|
|
|
+ path, err := Folder(p, dest, false)
|
|
|
+ must(t, err)
|
|
|
+ q, _, err := project.Load(path)
|
|
|
+ must(t, err)
|
|
|
+ if v, err := q.Pad(0); err != nil || v.File != "kick_one.wav" || v.SampleName != "kick_one" {
|
|
|
+ t.Errorf("reloaded pad A1 = %+v, %v", v, err)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestExportErrors(t *testing.T) {
|
|
|
+ dir := t.TempDir()
|
|
|
+ file := filepath.Join(dir, "file")
|
|
|
+ must(t, os.WriteFile(file, nil, 0o644))
|
|
|
+
|
|
|
+ bad := buildT1(t)
|
|
|
+ bad.Name = "a/b"
|
|
|
+ if _, err := Folder(bad, dir, false); err == nil || !strings.HasPrefix(err.Error(), "project name:") {
|
|
|
+ t.Errorf("Folder with a bad name: %v", err)
|
|
|
+ }
|
|
|
+ if err := Zip(bad, filepath.Join(dir, "kit.zip"), false); err == nil || !strings.HasPrefix(err.Error(), "project name:") {
|
|
|
+ t.Errorf("Zip with a bad name: %v", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ unknown := buildT1(t)
|
|
|
+ delete(unknown.Sources, "Linn 9000 kick.wav")
|
|
|
+ _, err := Folder(unknown, dir, false)
|
|
|
+ if err == nil || !strings.Contains(err.Error(), "Linn 9000 kick.wav (no source recorded)") {
|
|
|
+ t.Errorf("no source: %v", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ if _, err := Folder(buildT1(t), file, false); err == nil {
|
|
|
+ t.Error("Folder into a file: no error")
|
|
|
+ }
|
|
|
+ if err := Zip(buildT1(t), filepath.Join(dir, "gone", "kit.zip"), false); err == nil {
|
|
|
+ t.Error("Zip into a missing folder: no error")
|
|
|
+ }
|
|
|
+
|
|
|
+ _, err = Folder(buildT1(t), dir, false)
|
|
|
+ must(t, err)
|
|
|
+ _, err = Folder(buildT1(t), dir, false)
|
|
|
+ if want := "already exists: " + filepath.Join(dir, "MpckitT1.xpj") + ", " + filepath.Join(dir, "MpckitT1_[ProjectData]"); err == nil || err.Error() != want {
|
|
|
+ t.Errorf("got %v, want %q", err, want)
|
|
|
+ }
|
|
|
+ if got := listDir(t, dir); strings.Join(got, "|") != "MpckitT1.xpj|MpckitT1_[ProjectData]|file" {
|
|
|
+ t.Errorf("failed exports left %v", got)
|
|
|
+ }
|
|
|
+}
|