| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497 |
- package export
- import (
- "archive/zip"
- "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"
- func must(t *testing.T, err error) {
- t.Helper()
- if err != nil {
- t.Fatal(err)
- }
- }
- func gunzip(t *testing.T, b []byte) []byte {
- t.Helper()
- zr, err := gzip.NewReader(bytes.NewReader(b))
- must(t, err)
- out, err := io.ReadAll(zr)
- must(t, err)
- return out
- }
- func readFile(t *testing.T, path string) []byte {
- t.Helper()
- b, err := os.ReadFile(path)
- must(t, err)
- return b
- }
- // buildT1 makes MpckitT1 through the model (as in the M2 golden test).
- func buildT1(t *testing.T) *project.Project {
- t.Helper()
- p, err := project.New("MpckitT1")
- must(t, err)
- dir := filepath.Join(testdata, "accepted", "MpckitT1"+project.DataSuffix)
- must(t, p.AssignSample(0, filepath.Join(dir, "Linn 9000 kick.wav")))
- must(t, p.AssignSample(16, filepath.Join(dir, "Linn 9000 kick001.wav")))
- must(t, p.SetTune(16, 5, 0))
- must(t, p.SetSequence(0, "Verse", 96, 8))
- must(t, p.AddSequence(1))
- must(t, p.SetSequence(1, "Chorus", 96, 4))
- return p
- }
- func listDir(t *testing.T, dir string) []string {
- t.Helper()
- ents, err := os.ReadDir(dir)
- must(t, err)
- var names []string
- for _, e := range ents {
- names = append(names, e.Name())
- }
- sort.Strings(names)
- return names
- }
- func TestFolder(t *testing.T) {
- dest := t.TempDir()
- p := buildT1(t)
- path, err := Folder(p, dest, false)
- must(t, err)
- if path != filepath.Join(dest, "MpckitT1.xpj") {
- t.Errorf("path = %s", path)
- }
- if got := listDir(t, dest); strings.Join(got, "|") != "MpckitT1.xpj|MpckitT1_[ProjectData]" {
- t.Errorf("dest holds %v", got)
- }
- want := gunzip(t, readFile(t, filepath.Join(testdata, "accepted", "MpckitT1.xpj")))
- if !bytes.Equal(gunzip(t, readFile(t, path)), want) {
- t.Error("exported .xpj differs from MpckitT1.xpj")
- }
- src := filepath.Join(testdata, "accepted", "MpckitT1"+project.DataSuffix)
- data := filepath.Join(dest, "MpckitT1"+project.DataSuffix)
- if got := listDir(t, data); strings.Join(got, "|") != "Linn 9000 kick.wav|Linn 9000 kick001.wav" {
- t.Errorf("data folder holds %v", got)
- }
- for _, f := range listDir(t, data) {
- if !bytes.Equal(readFile(t, filepath.Join(data, f)), readFile(t, filepath.Join(src, f))) {
- t.Errorf("%s differs from its source", f)
- }
- }
- // Again without overwrite: refused, nothing touched.
- marker := filepath.Join(data, "keep.txt")
- must(t, os.WriteFile(marker, []byte("x"), 0o644))
- _, err = Folder(p, dest, false)
- var exists *ExistsError
- if !errors.As(err, &exists) || len(exists.Paths) != 2 {
- t.Fatalf("second export: %v", err)
- }
- if _, err := os.Stat(marker); err != nil {
- t.Error("refused export changed the folder")
- }
- // With overwrite: the old data folder is replaced, not merged.
- _, err = Folder(p, dest, true)
- must(t, err)
- if _, err := os.Stat(marker); err == nil {
- t.Error("overwrite kept a stale file in the data folder")
- }
- if got := listDir(t, dest); len(got) != 2 {
- t.Errorf("temporary files left behind: %v", got)
- }
- }
- func TestFolderEmptyProject(t *testing.T) {
- dest := t.TempDir()
- p, err := project.New("Empty")
- must(t, err)
- _, err = Folder(p, dest, false)
- must(t, err)
- // The device creates the data folder even with no samples; so do we.
- if got := listDir(t, filepath.Join(dest, "Empty"+project.DataSuffix)); len(got) != 0 {
- t.Errorf("data folder holds %v", got)
- }
- }
- func TestRenamedLoadedProject(t *testing.T) {
- p, _, err := project.Load(filepath.Join(testdata, "device", "DemoFile.xpj"))
- must(t, err)
- must(t, p.SetName("Renamed"))
- dest := t.TempDir()
- _, err = Folder(p, dest, false)
- must(t, err)
- got := listDir(t, filepath.Join(dest, "Renamed"+project.DataSuffix))
- if len(got) != 6 {
- t.Errorf("data folder holds %v", got)
- }
- want := gunzip(t, readFile(t, filepath.Join(testdata, "device", "DemoFile.xpj")))
- if !bytes.Equal(gunzip(t, readFile(t, filepath.Join(dest, "Renamed.xpj"))), want) {
- t.Error("renaming changed the file contents")
- }
- }
- func TestMissingSources(t *testing.T) {
- tmp := t.TempDir()
- p, err := project.New("Missing")
- must(t, err)
- for i, name := range []string{"a.wav", "b.wav", "c.wav"} {
- src := filepath.Join(tmp, name)
- must(t, os.WriteFile(src, readFile(t, filepath.Join(testdata, "accepted", "MpckitT2"+project.DataSuffix, "Linn 9000 kick.wav")), 0o644))
- must(t, p.AssignSample(i, src))
- }
- must(t, os.Remove(filepath.Join(tmp, "a.wav")))
- must(t, os.Remove(filepath.Join(tmp, "c.wav")))
- dest := t.TempDir()
- _, err = Folder(p, dest, false)
- var missing *MissingError
- if !errors.As(err, &missing) || strings.Join(missing.Files, ",") != "a.wav,c.wav" {
- t.Fatalf("Folder: %v", err)
- }
- if !strings.Contains(err.Error(), "2 sample file(s)") {
- t.Errorf("message: %v", err)
- }
- if err := Zip(p, filepath.Join(dest, "m.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 TestZip(t *testing.T) {
- dest := t.TempDir()
- zpath := filepath.Join(dest, "kit.zip")
- p := buildT1(t)
- must(t, Zip(p, zpath, false))
- zr, err := zip.OpenReader(zpath)
- must(t, err)
- defer zr.Close()
- var names []string
- files := map[string][]byte{}
- for _, f := range zr.File {
- names = append(names, f.Name)
- rc, err := f.Open()
- must(t, err)
- b, err := io.ReadAll(rc)
- rc.Close()
- must(t, err)
- files[f.Name] = b
- }
- want := "MpckitT1.xpj|MpckitT1_[ProjectData]/|MpckitT1_[ProjectData]/Linn 9000 kick.wav|MpckitT1_[ProjectData]/Linn 9000 kick001.wav"
- if strings.Join(names, "|") != want {
- t.Errorf("entries %v", names)
- }
- if !bytes.Equal(gunzip(t, files["MpckitT1.xpj"]), gunzip(t, readFile(t, filepath.Join(testdata, "accepted", "MpckitT1.xpj")))) {
- t.Error("zipped .xpj differs from MpckitT1.xpj")
- }
- src := filepath.Join(testdata, "accepted", "MpckitT1"+project.DataSuffix, "Linn 9000 kick001.wav")
- if !bytes.Equal(files["MpckitT1_[ProjectData]/Linn 9000 kick001.wav"], readFile(t, src)) {
- t.Error("zipped WAV differs from its source")
- }
- var exists *ExistsError
- if err := Zip(p, zpath, false); !errors.As(err, &exists) {
- t.Errorf("existing zip: %v", err)
- }
- must(t, Zip(p, zpath, true))
- if got := listDir(t, dest); strings.Join(got, "|") != "kit.zip" {
- 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)
- }
- }
|