github.com/derat/nup@v0.0.0-20230418113745-15592ba7c620/test/lib.go (about) 1 // Copyright 2020 Daniel Erat. 2 // All rights reserved. 3 4 // Package test contains common functionality and data used by tests. 5 package test 6 7 import ( 8 "encoding/json" 9 "fmt" 10 "io" 11 "io/ioutil" 12 "os" 13 "path/filepath" 14 "sort" 15 "strings" 16 "testing" 17 "time" 18 19 "github.com/derat/nup/server/db" 20 ) 21 22 // Must aborts t if err is non-nil. 23 func Must(t *testing.T, err error) { 24 if err != nil { 25 t.Fatalf("Failed at %v: %v", Caller(), err) 26 } 27 } 28 29 // SongsDir returns the test/data/songs directory containing sample song files. 30 func SongsDir() (string, error) { 31 libDir, err := CallerDir() 32 if err != nil { 33 return "", err 34 } 35 return filepath.Join(libDir, "data/songs"), nil 36 } 37 38 // CopySongs copies the provided songs (e.g. Song0s.Filename) from SongsDir into dir. 39 // The supplied directory is created if it doesn't already exist. 40 func CopySongs(dir string, filenames ...string) error { 41 srcDir, err := SongsDir() 42 if err != nil { 43 return err 44 } 45 46 if err := os.MkdirAll(dir, 0755); err != nil { 47 return err 48 } 49 50 for _, fn := range filenames { 51 sp := filepath.Join(srcDir, fn) 52 s, err := os.Open(sp) 53 if err != nil { 54 return err 55 } 56 defer s.Close() 57 58 dp := filepath.Join(dir, fn) 59 d, err := os.Create(dp) 60 if err != nil { 61 return err 62 } 63 if _, err := io.Copy(d, s); err != nil { 64 d.Close() 65 return err 66 } 67 if err := d.Close(); err != nil { 68 return err 69 } 70 71 now := time.Now() 72 if err := os.Chtimes(dp, now, now); err != nil { 73 return err 74 } 75 } 76 return nil 77 } 78 79 // DeleteSongs removes the provided songs (e.g. Song0s.Filename) from dir. 80 func DeleteSongs(dir string, filenames ...string) error { 81 for _, fn := range filenames { 82 if err := os.Remove(filepath.Join(dir, fn)); err != nil { 83 return err 84 } 85 } 86 return nil 87 } 88 89 // WriteSongsToJSONFile creates a file in dir containing JSON-marshaled songs. 90 // The file's path is returned. 91 func WriteSongsToJSONFile(dir string, songs ...db.Song) (string, error) { 92 f, err := ioutil.TempFile(dir, "songs-json.") 93 if err != nil { 94 return "", err 95 } 96 e := json.NewEncoder(f) 97 for _, s := range songs { 98 if err = e.Encode(s); err != nil { 99 f.Close() 100 return "", err 101 } 102 } 103 return f.Name(), f.Close() 104 } 105 106 // WriteSongPathsFile creates a file in dir listing filenames, 107 // suitable for passing to the `nup update -song-paths-file` flag. 108 func WriteSongPathsFile(dir string, filenames ...string) (string, error) { 109 f, err := ioutil.TempFile(dir, "song-list.") 110 if err != nil { 111 return "", err 112 } 113 for _, fn := range filenames { 114 if _, err := f.WriteString(fn + "\n"); err != nil { 115 f.Close() 116 return "", err 117 } 118 } 119 return f.Name(), f.Close() 120 } 121 122 // OrderPolicy specifies whether CompareSongs requires that songs appear in the specified order. 123 type OrderPolicy int 124 125 const ( 126 CompareOrder OrderPolicy = iota 127 IgnoreOrder 128 ) 129 130 // CompareSongs compares expected against actual. 131 // A descriptive error is returned if the songs don't match. 132 // TODO: Returning a multi-line error seems dumb. 133 func CompareSongs(expected, actual []db.Song, order OrderPolicy) error { 134 if order == IgnoreOrder { 135 sort.Slice(expected, func(i, j int) bool { return expected[i].Filename < expected[j].Filename }) 136 sort.Slice(actual, func(i, j int) bool { return actual[i].Filename < actual[j].Filename }) 137 } 138 139 m := make([]string, 0) 140 141 stringify := func(s db.Song) string { 142 if s.Plays != nil { 143 for j := range s.Plays { 144 s.Plays[j].StartTime = s.Plays[j].StartTime.UTC() 145 // Ugly hack to handle IPv6 addresses. 146 if s.Plays[j].IPAddress == "::1" { 147 s.Plays[j].IPAddress = "127.0.0.1" 148 } 149 } 150 sort.Sort(db.PlayArray(s.Plays)) 151 } 152 b, err := json.Marshal(s) 153 if err != nil { 154 return "failed: " + err.Error() 155 } 156 return string(b) 157 } 158 159 for i := 0; i < len(expected); i++ { 160 if i >= len(actual) { 161 m = append(m, fmt.Sprintf("missing song at position %v; expected %v", i, stringify(expected[i]))) 162 } else { 163 a := stringify(actual[i]) 164 e := stringify(expected[i]) 165 if a != e { 166 m = append(m, fmt.Sprintf("song %v didn't match expected values:\nexpected: %v\n actual: %v", i, e, a)) 167 } 168 } 169 } 170 for i := len(expected); i < len(actual); i++ { 171 m = append(m, fmt.Sprintf("unexpected song at position %v: %v", i, stringify(actual[i]))) 172 } 173 174 if len(m) > 0 { 175 return fmt.Errorf("actual songs didn't match expected:\n%v", strings.Join(m, "\n")) 176 } 177 return nil 178 } 179 180 // Date is a convenience wrapper around time.Date that constructs a time.Time in UTC. 181 // Hour, minute, second, and nanosecond values are taken from tm if present. 182 func Date(year int, month time.Month, day int, tm ...int) time.Time { 183 get := func(idx int) int { 184 if idx < len(tm) { 185 return tm[idx] 186 } 187 return 0 188 } 189 return time.Date(year, month, day, get(0), get(1), get(2), get(3), time.UTC) 190 }