kythe.io@v0.0.68-0.20240422202219-7225dbc01741/kythe/go/platform/kcd/testutil/testutil.go (about) 1 /* 2 * Copyright 2016 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 provides support functions for unit testing implementations 18 // of the kcd.ReadWriter interface. 19 package testutil // import "kythe.io/kythe/go/platform/kcd/testutil" 20 21 import ( 22 "context" 23 "errors" 24 "fmt" 25 "os" 26 "regexp" 27 "strings" 28 "testing" 29 "time" 30 31 "kythe.io/kythe/go/platform/kcd" 32 "kythe.io/kythe/go/platform/kcd/kythe" 33 34 "google.golang.org/protobuf/proto" 35 36 apb "kythe.io/kythe/proto/analysis_go_proto" 37 spb "kythe.io/kythe/proto/storage_go_proto" 38 ) 39 40 // TestError is the concrete type of errors returned by the Run function. 41 type TestError struct { 42 Desc string // Description of the test that failed (human-readable) 43 Method string // The name of the method that returned an error 44 Err error // The underlying error returned by the method 45 } 46 47 func (t *TestError) Error() string { 48 return fmt.Sprintf("R [%s]: %s: %v", t.Desc, t.Method, t.Err) 49 } 50 51 // These constants are the expected values used by the Run tests. 52 const ( 53 Revision = "1234" 54 Corpus = "ratzafratza" 55 FormatKey = "λ" 56 Language = "go" 57 ) 58 59 // UnitType is the type of compilation message stored in the database by the 60 // tests in Run. 61 var UnitType *apb.CompilationUnit 62 63 func regexps(exprs ...string) (res []*regexp.Regexp) { 64 for _, expr := range exprs { 65 res = append(res, regexp.MustCompile(expr)) 66 } 67 return 68 } 69 70 // Run applies a sequence of correctness tests to db, which must be initially 71 // empty, and returns any errors that occur. If db passes all the tests, the 72 // return value is nil; otherwise each error is of concrete type *TestError. 73 func Run(t *testing.T, ctx context.Context, db kcd.ReadWriter) []error { 74 var errs []error 75 76 // Each check is passed a function to report errors. The errors are packed 77 // into *TestError wrappers and accumulated to return. 78 type failer func(method string, err error) 79 check := func(desc string, test func(failer)) { 80 t.Helper() 81 test(func(method string, err error) { 82 t.Helper() 83 err = &TestError{desc, method, err} 84 t.Error(err) 85 errs = append(errs, err) 86 }) 87 } 88 89 // The order of the tests below is significant; each modifies the state of 90 // the database being tested in a way that can be used by subsequent tests 91 // on success. 92 93 check("initial revisions list is empty", func(fail failer) { 94 if err := db.Revisions(ctx, nil, func(rev kcd.Revision) error { 95 return fmt.Errorf("unexpected revision %v", rev) // any hit is an error 96 }); err != nil { 97 fail("Revisions", err) 98 } 99 }) 100 101 check("initial units list is empty", func(fail failer) { 102 anyTarget := &kcd.FindFilter{Targets: regexps(".*")} 103 if err := db.Find(ctx, anyTarget, func(digest string) error { 104 return fmt.Errorf("unexpected compilation %q", digest) 105 }); err != nil { 106 fail("Find", err) 107 } 108 }) 109 110 check("written revisions round-trip", func(fail failer) { 111 wantTime := time.Now().In(time.UTC).Round(time.Microsecond) 112 wantRev := kcd.Revision{Revision, Corpus, wantTime} 113 if err := db.WriteRevision(ctx, wantRev, true); err != nil { 114 fail("WriteRevision", err) 115 return 116 } 117 var gotRev kcd.Revision 118 if err := db.Revisions(ctx, nil, func(rev kcd.Revision) error { 119 gotRev = rev 120 return nil 121 }); err != nil { 122 fail("Revisions", err) 123 } 124 if got, want := gotRev.Corpus, wantRev.Corpus; got != want { 125 fail("corpus", fmt.Errorf("got %q, want %q", got, want)) 126 } 127 if got, want := gotRev.Revision, wantRev.Revision; got != want { 128 fail("marker", fmt.Errorf("got %q, want %q", got, want)) 129 } 130 if got, want := gotRev.Timestamp, wantRev.Timestamp; !got.Equal(want) { 131 fail("timestamp", fmt.Errorf("got %v, want %v", got, want)) 132 } 133 }) 134 135 check("write revision error checks", func(fail failer) { 136 if db.WriteRevision(ctx, kcd.Revision{"foo", "", time.Time{}}, false) == nil { 137 fail("WriteRevision", errors.New("no error on empty corpus")) 138 } 139 if db.WriteRevision(ctx, kcd.Revision{"", "bar", time.Time{}}, false) == nil { 140 fail("WriteRevision", errors.New("no error on empty revision")) 141 } 142 }) 143 144 const missingDigest = "0000000000000000000000000000000000000000000000000000000000000000" 145 const badDigest = "bad digest" 146 147 check("missing units are not found", func(fail failer) { 148 if err := db.Units(ctx, []string{missingDigest, badDigest}, func(digest, key string, data []byte) error { 149 fail("Units", fmt.Errorf("unexpected digest %q and key %q", digest, key)) 150 return nil 151 }); err != nil { 152 fail("Units", err) 153 } 154 }) 155 156 check("missing files are not found", func(fail failer) { 157 if err := db.Files(ctx, []string{missingDigest, badDigest}, func(digest string, data []byte) error { 158 fail("Files", fmt.Errorf("unexpected digest %q and data %q", digest, string(data))) 159 return nil 160 }); err != nil { 161 fail("Files", err) 162 } 163 164 if err := db.FilesExist(ctx, []string{missingDigest, badDigest}, func(digest string) error { 165 fail("FilesExist", fmt.Errorf("unexpected digest %q", digest)) 166 return nil 167 }); err != nil { 168 fail("FilesExist", err) 169 } 170 }) 171 172 // SHA256 test vector from http://www.nsrl.nist.gov/testdata/ 173 const wantData = "abc" 174 const wantDigest = "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" 175 176 check("written files round-trip", func(fail failer) { 177 gotDigest, err := db.WriteFile(ctx, strings.NewReader(wantData)) 178 if err != nil { 179 fail("WriteFile", err) 180 } 181 if gotDigest != wantDigest { 182 fail("WriteFile", fmt.Errorf("got digest %q, want %q", gotDigest, wantDigest)) 183 } 184 var gotData string 185 if err := db.Files(ctx, []string{gotDigest}, func(_ string, data []byte) error { 186 gotData = string(data) 187 return nil 188 }); err != nil { 189 fail("Files", err) 190 } 191 if gotData != wantData { 192 fail("Files", fmt.Errorf("got %q, want %q", gotData, wantData)) 193 } 194 }) 195 196 var unitDigest string // set by the test below 197 198 check("written units round-trip", func(fail failer) { 199 const inputDigest = "b05ffa4eea8fb5609d576a68c1066be3f99e4dc53d365a0ac2a78259b2dd91f9" 200 dummy := kythe.Unit{&apb.CompilationUnit{ 201 VName: &spb.VName{Signature: "//foo/bar/baz:quux", Language: "go"}, 202 SourceFile: []string{"quux.cc"}, 203 RequiredInput: []*apb.CompilationUnit_FileInput{{ 204 VName: &spb.VName{Path: "foo/bar/baz/quux.cc"}, 205 Info: &apb.FileInfo{ 206 Path: "quux.cc", 207 Digest: inputDigest, 208 }, 209 }}, 210 OutputKey: "quux.a", 211 }} 212 digest, err := db.WriteUnit(ctx, kcd.Revision{ 213 Revision: Revision, 214 Corpus: Corpus, 215 }, FormatKey, dummy) 216 if err != nil { 217 fail("WriteUnit", err) 218 return 219 } 220 221 unitDigest = digest 222 var foundUnit bool 223 if err := db.Units(ctx, []string{digest}, func(gotDigest, gotKey string, data []byte) error { 224 if gotDigest != digest { 225 fail("Units", fmt.Errorf("got digest %q, want %q", gotDigest, digest)) 226 } 227 if gotKey != FormatKey { 228 fail("Units", fmt.Errorf("got key %q, want %q", gotKey, FormatKey)) 229 } 230 var gotUnit apb.CompilationUnit 231 if err := proto.Unmarshal(data, &gotUnit); err != nil { 232 fail("Units", fmt.Errorf("unmarshaling proto: %v", err)) 233 } else if !proto.Equal(&gotUnit, dummy.Proto) { 234 fail("Units", fmt.Errorf("got %+v, want %+v", &gotUnit, dummy.Proto)) 235 } 236 foundUnit = true 237 return nil 238 }); err != nil { 239 fail("Units", err) 240 } 241 if !foundUnit { 242 fail("Units", fmt.Errorf("failed to find unit: %q", unitDigest)) 243 } 244 245 // Check that required input digests do not exist until their contents are written. 246 if err := db.FilesExist(ctx, []string{inputDigest}, func(digest string) error { 247 fail("FilesExist", fmt.Errorf("unexpected digest %q", digest)) 248 return nil 249 }); err != nil { 250 fail("FilesExist", err) 251 } 252 }) 253 254 check("basic filters match index terms", func(fail failer) { 255 tests := []*kcd.FindFilter{ 256 {Revisions: []string{Revision}}, 257 {BuildCorpus: []string{Corpus}}, 258 {Revisions: []string{Revision}, BuildCorpus: []string{Corpus}}, 259 {Languages: []string{Language}}, 260 {Targets: regexps(`//foo/bar/baz:\w+`)}, 261 {Sources: regexps("quux.*")}, 262 {Outputs: regexps(`quux\.a`)}, 263 } 264 for _, test := range tests { 265 var numSeen int 266 if err := db.Find(ctx, test, func(got string) error { 267 numSeen++ 268 if got != unitDigest { 269 fail("result", fmt.Errorf("on filter %+v; got %q, want %q", test, got, unitDigest)) 270 } 271 return nil 272 }); err != nil { 273 fail("Find", err) 274 } 275 if numSeen != 1 { 276 fail("result count", fmt.Errorf("on filter %+v; got %d, want %d", test, numSeen, 1)) 277 } 278 } 279 }) 280 281 check("empty find filter returns nothing", func(fail failer) { 282 if err := db.Find(ctx, nil, func(digest string) error { 283 fail("Find", fmt.Errorf("unexpected digest %q", digest)) 284 return nil 285 }); err != nil { 286 fail("Find", err) 287 } 288 }) 289 290 check("add more revisions", func(fail failer) { 291 revs := []kcd.Revision{ 292 {"5678", Corpus, time.Unix(1, 1)}, 293 {"9012", Corpus, time.Unix(2, 3)}, 294 {"3459", "alt", time.Unix(5, 8)}, 295 } 296 for _, rev := range revs { 297 if err := db.WriteRevision(ctx, rev, false); err != nil { 298 fail("WriteRevision", err) 299 } 300 } 301 }) 302 303 check("empty revision filter returns everything", func(fail failer) { 304 wantRevs := map[string]*kcd.Revision{ 305 Revision: nil, "5678": nil, "9012": nil, "3459": nil, 306 } 307 var numSeen int 308 if err := db.Revisions(ctx, nil, func(rev kcd.Revision) error { 309 numSeen++ 310 wantRevs[rev.Revision] = &rev 311 return nil 312 }); err != nil { 313 fail("Revisions", err) 314 } 315 if numSeen != len(wantRevs) { 316 fail("result count", fmt.Errorf("got %d, want %d", numSeen, len(wantRevs))) 317 } 318 for marker, rev := range wantRevs { 319 if rev == nil { 320 fail("Revisions", fmt.Errorf("missing revision for %q", marker)) 321 } 322 } 323 }) 324 325 check("revision filter by corpus", func(fail failer) { 326 gotRevs := make(map[string]*kcd.Revision) 327 filter := &kcd.RevisionsFilter{Corpus: "alt"} 328 if err := db.Revisions(ctx, filter, func(rev kcd.Revision) error { 329 gotRevs[rev.Revision] = &rev 330 return nil 331 }); err != nil { 332 fail("Revisions", err) 333 } 334 if len(gotRevs) != 1 { 335 fail("result count", fmt.Errorf("got %d, want %d", len(gotRevs), 1)) 336 } 337 if rev := gotRevs["3459"]; rev == nil { 338 fail("Revisions", fmt.Errorf("missing data for 3459\nFilter: %+v", filter)) 339 } else if rev.Corpus != "alt" { 340 fail("Revisions", fmt.Errorf("got corpus %q, want %q", rev.Corpus, "alt")) 341 } 342 }) 343 344 check("revision filter by timestamp", func(fail failer) { 345 wantRevs := map[string]*kcd.Revision{"5678": nil, "9012": nil} 346 filter := &kcd.RevisionsFilter{Until: time.Unix(2, 3)} 347 if err := db.Revisions(ctx, filter, func(rev kcd.Revision) error { 348 wantRevs[rev.Revision] = &rev 349 return nil 350 }); err != nil { 351 fail("Revisions", err) 352 } 353 if len(wantRevs) > 2 { 354 fail("result count", fmt.Errorf("got %d, want %d", len(wantRevs), 2)) 355 } 356 for marker, rev := range wantRevs { 357 if rev == nil { 358 fail("Revisions", fmt.Errorf("missing revision for %q\nFilter: %+v", marker, filter)) 359 } 360 } 361 }) 362 363 check("revision corpus does not match regexp", func(fail failer) { 364 filter := &kcd.RevisionsFilter{Corpus: "a.."} // matches "alt", but shouldn't hit 365 if err := db.Revisions(ctx, filter, func(rev kcd.Revision) error { 366 return fmt.Errorf("unexpected corpus regexp match\nFilter: %+v\nResult: %+v", filter, rev) 367 }); err != nil { 368 fail("Revisions", err) 369 } 370 371 wantRevs := map[string]*kcd.Revision{"9012": nil, "3459": nil} 372 filter = &kcd.RevisionsFilter{Revision: ".*9.*"} 373 if err := db.Revisions(ctx, filter, func(rev kcd.Revision) error { 374 wantRevs[rev.Revision] = &rev 375 return nil 376 }); err != nil { 377 fail("Revisions", err) 378 } 379 for marker, rev := range wantRevs { 380 if rev == nil { 381 fail("Revisions", fmt.Errorf("missing revision for %q\nFilter: %+v", marker, filter)) 382 } 383 } 384 }) 385 386 // If db implements the Deleter interface, verify that it works. 387 del, ok := db.(kcd.Deleter) 388 if !ok { 389 return errs 390 } 391 392 check("deleting an existing unit succeeds", func(fail failer) { 393 if err := del.DeleteUnit(ctx, unitDigest); err != nil { 394 fail("DeleteUnit", err) 395 } 396 }) 397 398 check("deleting a nonexistent unit reports an error", func(fail failer) { 399 if err := del.DeleteUnit(ctx, "no-such-unit"); err == nil { 400 fail("DeleteUnit", errors.New("no error returned for absent digest")) 401 } else if !os.IsNotExist(err) { 402 fail("DeleteUnit", fmt.Errorf("error %v does not satisfy os.IsNotExist", err)) 403 } 404 }) 405 406 check("deleting an existing file succeeds", func(fail failer) { 407 if err := del.DeleteFile(ctx, wantDigest); err != nil { 408 fail("DeleteFile", err) 409 } 410 }) 411 412 check("deleting a nonexistent file reports an error", func(fail failer) { 413 if err := del.DeleteFile(ctx, "no-such-file"); err == nil { 414 fail("DeleteFile", errors.New("no error returned for absent file")) 415 } else if !os.IsNotExist(err) { 416 fail("DeleteFile", fmt.Errorf("error %v does not satisfy os.IsNotExist", err)) 417 } 418 }) 419 420 check("deleting an existing revision succeeds", func(fail failer) { 421 if err := del.DeleteRevision(ctx, Revision, Corpus); err != nil { 422 fail("DeleteRevision", err) 423 } 424 }) 425 426 check("deleting a nonexistent revision reports an error", func(fail failer) { 427 if err := del.DeleteRevision(ctx, "nsrev", "nscorp"); err == nil { 428 fail("DeleteRevision", errors.New("no error returned for absent revision")) 429 } else if !os.IsNotExist(err) { 430 fail("DeleteRevision", fmt.Errorf("error %v does not satisfy os.IsNotExist", err)) 431 } 432 }) 433 434 return errs 435 }