github.com/ubuntu/ubuntu-report@v1.7.4-0.20240410144652-96f37d845fac/internal/helper/helper.go (about)

     1  package helper
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"io"
     7  	"io/ioutil"
     8  	"os"
     9  	"os/exec"
    10  	"reflect"
    11  	"strings"
    12  	"testing"
    13  	"time"
    14  
    15  	log "github.com/sirupsen/logrus"
    16  )
    17  
    18  /*
    19   * Contains test helpers across package
    20   */
    21  
    22  // Asserter for testing purposes
    23  type Asserter struct {
    24  	*testing.T
    25  }
    26  
    27  // Equal checks that the 2 values are equals
    28  // slices and arrays can be of different orders
    29  func (m Asserter) Equal(got, want interface{}) {
    30  	m.Helper()
    31  
    32  	var same bool
    33  	switch t := reflect.TypeOf(got); t.Kind() {
    34  	case reflect.Slice:
    35  		// We treat slice of bytes differently, order is important
    36  		a, gotIsBytes := got.([]byte)
    37  		b, wantIsBytes := want.([]byte)
    38  		if gotIsBytes && wantIsBytes {
    39  			// convert them to string for easier comparaison once
    40  			// they don'tmatch
    41  			if same = reflect.DeepEqual(a, b); !same {
    42  				m.Errorf("got: %s (converted from []byte), wants %s (converted from []byte)", string(a), string(b))
    43  			}
    44  			return
    45  		}
    46  		same = unsortedEqualsSliceArray(got, want)
    47  	case reflect.Array:
    48  		same = unsortedEqualsSliceArray(got, want)
    49  	case reflect.Map, reflect.Ptr:
    50  		same = reflect.DeepEqual(got, want)
    51  	default:
    52  		same = got == want
    53  	}
    54  
    55  	if !same {
    56  		m.Errorf("got: %#v (%T), wants %#v (%T)", got, got, want, want)
    57  	}
    58  }
    59  
    60  // CheckWantedErr checks that we received an error when desired or none other
    61  func (m Asserter) CheckWantedErr(err error, wantErr bool) {
    62  	m.Helper()
    63  	if err != nil && !wantErr {
    64  		m.Fatal("got an unexpected err:", err)
    65  	}
    66  	if err == nil && wantErr {
    67  		m.Error("expected an error and got none")
    68  	}
    69  }
    70  
    71  // LoadOrUpdateGolden returns golden file content.
    72  // It will update it beforehand if requested.
    73  func LoadOrUpdateGolden(t *testing.T, p string, data []byte, update bool) []byte {
    74  	t.Helper()
    75  
    76  	if update {
    77  		t.Log("update golden file at", p)
    78  		if data == nil {
    79  			t.Logf("No file to create as data is nil")
    80  			os.Remove(p)
    81  			return nil
    82  		}
    83  		if err := ioutil.WriteFile(p, data, 0666); err != nil {
    84  			t.Fatalf("can't update golden file %s: %v", p, err)
    85  		}
    86  	}
    87  
    88  	var content []byte
    89  	var err error
    90  	if content, err = ioutil.ReadFile(p); os.IsExist(err) && err != nil {
    91  		t.Fatalf("got an error loading golden file %s: %v", p, err)
    92  	}
    93  	return content
    94  }
    95  
    96  func unsortedEqualsSliceArray(a, b interface{}) bool {
    97  	if a == nil || b == nil {
    98  		return a == b
    99  	}
   100  
   101  	a1 := reflect.ValueOf(a)
   102  	a2 := reflect.ValueOf(b)
   103  
   104  	if a1.Len() != a2.Len() {
   105  		return false
   106  	}
   107  
   108  	// mark indexes in b that we already matched against
   109  	seen := make([]bool, a2.Len())
   110  	for i := 0; i < a1.Len(); i++ {
   111  		cur := a1.Index(i).Interface()
   112  
   113  		found := false
   114  		for j := 0; j < a2.Len(); j++ {
   115  			if seen[j] {
   116  				continue
   117  			}
   118  
   119  			if reflect.DeepEqual(a2.Index(j).Interface(), cur) {
   120  				seen[j] = true
   121  				found = true
   122  				break
   123  			}
   124  		}
   125  		if !found {
   126  			return false
   127  		}
   128  	}
   129  
   130  	return true
   131  }
   132  
   133  // ShortProcess helper is mocking a command supposed to return quickly
   134  // (within 100 milliseconds)
   135  // (inspired by stdlib)
   136  func ShortProcess(t *testing.T, helper string, s ...string) (*exec.Cmd, context.CancelFunc) {
   137  	t.Helper()
   138  
   139  	cs := []string{"-test.run=" + helper, "--"}
   140  	cs = append(cs, s...)
   141  
   142  	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
   143  
   144  	cmd := exec.CommandContext(ctx, os.Args[0], cs...)
   145  	cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"}
   146  
   147  	return cmd, cancel
   148  }
   149  
   150  // CopyFile for testing from src to dst
   151  func CopyFile(t *testing.T, src, dst string) {
   152  	t.Helper()
   153  
   154  	s, err := os.Open(src)
   155  	if err != nil {
   156  		t.Fatalf("couldn't open %s: %v", src, err)
   157  	}
   158  	defer s.Close()
   159  
   160  	d, err := os.Create(dst)
   161  	if err != nil {
   162  		t.Fatalf("couldn't create %s: %v", dst, err)
   163  	}
   164  	defer func() {
   165  		if err := d.Close(); err != nil {
   166  			t.Fatalf("couldn't close properly %s: %v", dst, err)
   167  		}
   168  	}()
   169  
   170  	if _, err := io.Copy(d, s); err != nil {
   171  		t.Fatalf("couldn't copy %s content to %s: %v", src, dst, err)
   172  	}
   173  }
   174  
   175  // SkipIfShort will skip current test if -short isn't passed
   176  func SkipIfShort(t *testing.T) {
   177  	t.Helper()
   178  	if testing.Short() {
   179  		t.Skip("short tests only, skipping")
   180  	}
   181  }
   182  
   183  // GetenvFromMap generates a getenv function mock from a map[string]string
   184  // no value returns empty string
   185  func GetenvFromMap(env map[string]string) func(key string) string {
   186  	return func(key string) string {
   187  		value, ok := env[key]
   188  		if !ok {
   189  			value = ""
   190  		}
   191  		return value
   192  	}
   193  }
   194  
   195  // TempDir creates and give defer to remove temporary dir safely for testing
   196  func TempDir(t *testing.T) (string, func()) {
   197  	t.Helper()
   198  	d, err := ioutil.TempDir("", "ubuntu-report-tests")
   199  	if err != nil {
   200  		t.Fatal("couldn't create temporary directory", err)
   201  	}
   202  	return d, func() {
   203  		if err = os.RemoveAll(d); err != nil {
   204  			t.Fatalf("couldn't clean temporary directory %s, %v", d, err)
   205  		}
   206  	}
   207  }
   208  
   209  // CaptureStdout returns an io.Reader to read what was printed, and teardown
   210  func CaptureStdout(t *testing.T) (io.Reader, func()) {
   211  	t.Helper()
   212  	stdout, stdoutW, err := os.Pipe()
   213  	if err != nil {
   214  		t.Fatal("couldn't create stdout pipe", err)
   215  	}
   216  	oldStdout := os.Stdout
   217  	os.Stdout = stdoutW
   218  	closed := false
   219  	return stdout, func() {
   220  		// only teardown once
   221  		if closed {
   222  			return
   223  		}
   224  		os.Stdout = oldStdout
   225  		stdoutW.Close()
   226  		closed = true
   227  	}
   228  }
   229  
   230  // CaptureStdin returns an i.Writer to write, as stdin, and teardown
   231  func CaptureStdin(t *testing.T) (io.WriteCloser, func()) {
   232  	t.Helper()
   233  	stdin, stdinW, err := os.Pipe()
   234  	if err != nil {
   235  		t.Fatal("couldn't create stdin pipe", err)
   236  	}
   237  	oldStdin := os.Stdin
   238  	os.Stdin = stdin
   239  	return stdinW, func() { os.Stdin = oldStdin }
   240  }
   241  
   242  // CaptureLogs returns an io.Reader to read what was logged, and teardown
   243  func CaptureLogs(t *testing.T) (io.Reader, func()) {
   244  	t.Helper()
   245  
   246  	pr, pw := io.Pipe()
   247  	log.SetOutput(pw)
   248  	old := log.StandardLogger().Out
   249  	closed := false
   250  	return pr, func() {
   251  		// only teardown once
   252  		if closed {
   253  			return
   254  		}
   255  		log.SetOutput(old)
   256  		pw.Close()
   257  		closed = true
   258  	}
   259  }
   260  
   261  // RunFunctionWithTimeout run in a go routing the fn functionL
   262  // There is a timeout as a maximum limit for the function, to run
   263  // The returned channel of errors is closed once the command ends
   264  // or if timeout is reached
   265  func RunFunctionWithTimeout(t *testing.T, fn func() error) chan error {
   266  	errs := make(chan error)
   267  	go func() {
   268  		defer close(errs)
   269  
   270  		// add a timeout
   271  		cmd := func() chan error {
   272  			cmderr := make(chan error)
   273  			go func() {
   274  				defer close(cmderr)
   275  				err := fn()
   276  				cmderr <- err
   277  			}()
   278  			return cmderr
   279  		}
   280  
   281  		select {
   282  		case err := <-cmd():
   283  			errs <- err
   284  		case <-time.After(10 * time.Second):
   285  			errs <- errors.New("function under test timed out")
   286  		}
   287  	}()
   288  	return errs
   289  }
   290  
   291  // ChangeEnv change a key in process environ
   292  // Return a teardown
   293  func ChangeEnv(key, value string) func() {
   294  	old := os.Getenv(key)
   295  	os.Setenv(key, value)
   296  
   297  	return func() {
   298  		os.Setenv(key, old)
   299  	}
   300  }
   301  
   302  // FindInDirectory return first match of prefix in d
   303  func FindInDirectory(t *testing.T, prefix, d string) string {
   304  	t.Helper()
   305  
   306  	files, err := ioutil.ReadDir(d)
   307  	if err != nil {
   308  		t.Fatalf("couldn't scan %s: %v", d, err)
   309  	}
   310  
   311  	for _, f := range files {
   312  		if strings.HasPrefix(f.Name(), prefix) {
   313  			return f.Name()
   314  		}
   315  	}
   316  	t.Fatalf("didn't find %s in %s. Only got: %v", prefix, d, files)
   317  	return ""
   318  }