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 }