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  }