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  }