kythe.io@v0.0.68-0.20240422202219-7225dbc01741/kythe/go/test/testutil/testutil.go (about) 1 /* 2 * Copyright 2015 The Kythe Authors. All rights reserved. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 // Package testutil contains common utilities to test Kythe libraries. 18 package testutil // import "kythe.io/kythe/go/test/testutil" 19 20 import ( 21 "encoding/json" 22 "flag" 23 "fmt" 24 "math/rand" 25 "os" 26 "path/filepath" 27 "regexp" 28 "runtime" 29 "strings" 30 "testing" 31 32 "kythe.io/kythe/go/util/compare" 33 34 "github.com/google/go-cmp/cmp" 35 "sigs.k8s.io/yaml" 36 ) 37 38 // DeepEqual determines if expected is deeply equal to got, returning a 39 // detailed error if not. It is okay for expected and got to be protobuf 40 // message values. 41 func DeepEqual[T any](expected, got T, opts ...cmp.Option) error { 42 if diff := compare.ProtoDiff(expected, got, opts...); diff != "" { 43 return fmt.Errorf("(-expected; +found)\n%s", diff) 44 } 45 return nil 46 } 47 48 var multipleNewLines = regexp.MustCompile("\n{2,}") 49 50 // TrimmedEqual compares two strings after collapsing irrelevant whitespace at 51 // the beginning or end of lines. It returns both a boolean indicating equality, 52 // as well as any relevant diff. 53 func TrimmedEqual(got, want []byte) (bool, string) { 54 // remove superfluous whitespace 55 gotStr := strings.Trim(string(got[:]), " \n") 56 wantStr := strings.Trim(string(want[:]), " \n") 57 gotStr = multipleNewLines.ReplaceAllString(gotStr, "\n") 58 wantStr = multipleNewLines.ReplaceAllString(wantStr, "\n") 59 60 // diff want vs got 61 diff := cmp.Diff(gotStr, wantStr) 62 return diff == "", diff 63 } 64 65 // YAMLEqual compares two bytes assuming they are yaml, by converting to json 66 // and doing an ordering-agnostic comparison. Note this carries some 67 // restrictions because yaml->json conversion fails for nil or binary map keys. 68 func YAMLEqual(expected, got []byte) error { 69 e, err := yaml.YAMLToJSON(expected) 70 if err != nil { 71 return fmt.Errorf("yaml->json failure for expected: %v", err) 72 } 73 g, err := yaml.YAMLToJSON(got) 74 if err != nil { 75 return fmt.Errorf("yaml->json failure for got: %v", err) 76 } 77 return JSONEqual(e, g) 78 } 79 80 // JSONEqual compares two bytes assuming they are json, using encoding/json 81 // and DeepEqual. 82 func JSONEqual(expected, got []byte) error { 83 var e, g any 84 if err := json.Unmarshal(expected, &e); err != nil { 85 return fmt.Errorf("decoding expected json: %v", err) 86 } 87 if err := json.Unmarshal(got, &g); err != nil { 88 return fmt.Errorf("decoding got json: %v", err) 89 } 90 return DeepEqual(e, g) 91 } 92 93 func caller(up int) (file string, line int) { 94 _, file, line, ok := runtime.Caller(up + 2) 95 if !ok { 96 panic("could not get runtime.Caller") 97 } 98 return filepath.Base(file), line 99 } 100 101 // Errorf is equivalent to t.Errorf(msg, err, args...) if err != nil. 102 func Errorf(t testing.TB, msg string, err error, args ...any) { 103 if err != nil { 104 t.Helper() 105 t.Errorf(msg, append([]any{err}, args...)...) 106 } 107 } 108 109 // Fatalf is equivalent to t.Fatalf(msg, err, args...) if err != nil. 110 func Fatalf(t testing.TB, msg string, err error, args ...any) { 111 if err != nil { 112 t.Helper() 113 t.Fatalf(msg, append([]any{err}, args...)...) 114 } 115 } 116 117 // RandStr returns a random string of the given length 118 func RandStr(size int) string { 119 const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" 120 buf := make([]byte, size) 121 RandBytes(buf) 122 for i, b := range buf { 123 buf[i] = chars[b%byte(len(chars))] 124 } 125 return string(buf) 126 } 127 128 // RandBytes fills the given slice with random bytes 129 func RandBytes(bytes []byte) { 130 i := len(bytes) - 1 131 for { 132 n := rand.Int63() 133 for j := 0; j < 8; j++ { 134 bytes[i] = byte(n) 135 i-- 136 if i == -1 { 137 return 138 } 139 n >>= 8 140 } 141 } 142 } 143 144 // TestFilePath takes a path and resolves it based on the testdir. If it 145 // cannot successfully do so, it calls t.Fatal and abandons. 146 func TestFilePath(t *testing.T, path string) string { 147 t.Helper() 148 pwd, err := os.Getwd() 149 if err != nil { 150 t.Fatalf("Failed to resolve path %s: %v", path, err) 151 } 152 return filepath.Join(pwd, filepath.FromSlash(path)) 153 } 154 155 // SetFlag sets a flag to a value for the duration of the test, resetting it 156 // to the flag's default value during cleanup. 157 func SetFlag(t testing.TB, flagName string, value string) { 158 f := flag.Lookup(flagName) 159 if err := f.Value.Set(value); err != nil { 160 t.Fatalf("Error setting --%s: %v", flagName, err) 161 } 162 t.Cleanup(func() { f.Value.Set(f.DefValue) }) 163 }