| // Copyright 2021 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // The search command is used to run tests for search using a dataset of |
| // modules specified at tests/search/seed.txt. |
| // See tests/README.md for details. |
| package main |
| |
| import ( |
| "bufio" |
| "context" |
| "flag" |
| "fmt" |
| "os" |
| "strconv" |
| "strings" |
| |
| _ "github.com/jackc/pgx/v4/stdlib" // for pgx driver |
| "golang.org/x/pkgsite/internal" |
| "golang.org/x/pkgsite/internal/config" |
| "golang.org/x/pkgsite/internal/database" |
| "golang.org/x/pkgsite/internal/derrors" |
| "golang.org/x/pkgsite/internal/experiment" |
| "golang.org/x/pkgsite/internal/log" |
| "golang.org/x/pkgsite/internal/postgres" |
| ) |
| |
| func main() { |
| flag.Parse() |
| |
| ctx := experiment.NewContext(context.Background(), symbolSearchExperiments...) |
| cfg, err := config.Init(ctx) |
| if err != nil { |
| log.Fatal(ctx, err) |
| } |
| log.SetLevel(cfg.LogLevel) |
| |
| // Wrap the postgres driver with our own wrapper, which adds OpenCensus instrumentation. |
| ddb, err := database.Open("pgx", cfg.DBConnInfo(), "seeddb") |
| if err != nil { |
| log.Fatalf(ctx, "database.Open for host %s failed with %v", cfg.DBHost, err) |
| } |
| db := postgres.New(ddb) |
| defer db.Close() |
| |
| if err := run(ctx, db); err != nil { |
| log.Fatal(ctx, err) |
| } |
| } |
| |
| const ( |
| importedbyFile = "tests/search/importedby.txt" |
| testFile = "tests/search/scripts/symbolsearch.txt" |
| ) |
| |
| var symbolSearchExperiments = []string{ |
| internal.ExperimentInsertSymbolSearchDocuments, |
| internal.ExperimentSearchGrouping, |
| internal.ExperimentSymbolSearch, |
| } |
| |
| func run(ctx context.Context, db *postgres.DB) error { |
| counts, err := readImportedByCounts(importedbyFile) |
| if err != nil { |
| return err |
| } |
| if _, err := db.UpdateSearchDocumentsImportedByCountWithCounts(ctx, counts); err != nil { |
| return err |
| } |
| tests, err := readSearchTests(testFile) |
| if err != nil { |
| return err |
| } |
| var failed bool |
| for _, st := range tests { |
| output, err := runTest(ctx, db, st) |
| if err != nil { |
| return err |
| } |
| if len(output) == 0 { |
| fmt.Println("--- PASSED: ", st.title) |
| continue |
| } |
| failed = true |
| fmt.Println("--- FAILED: ", st.title) |
| for _, e := range output { |
| fmt.Println(e) |
| } |
| } |
| if failed { |
| return fmt.Errorf("SEARCH TESTS FAILED: see output above") |
| } |
| return nil |
| } |
| |
| func runTest(ctx context.Context, db *postgres.DB, st *searchTest) (output []string, err error) { |
| defer derrors.Wrap(&err, "runTest(ctx, db, st.title: %q)", st.title) |
| results, err := db.Search(ctx, st.query, postgres.SearchOptions{MaxResults: 10, SearchSymbols: true}) |
| if err != nil { |
| return nil, err |
| } |
| for i, want := range st.results { |
| got := &postgres.SearchResult{} |
| if len(results) > i { |
| got = results[i] |
| } |
| if want.symbol != got.SymbolName || want.pkg != got.PackagePath { |
| output = append(output, |
| fmt.Sprintf("query %s, mismatch result %d:\n\twant: %q %q\n\t got: %q %q\n", |
| st.query, i+1, |
| want.pkg, want.symbol, |
| got.PackagePath, got.SymbolName)) |
| } |
| } |
| return output, nil |
| } |
| |
| type searchTest struct { |
| title string |
| query string |
| results []*searchResult |
| } |
| |
| type searchResult struct { |
| pkg string |
| symbol string |
| } |
| |
| // readSearchTests reads filename and returns the search tests from that file. |
| // See tests/README.md for a description of the syntax. |
| func readSearchTests(filename string) ([]*searchTest, error) { |
| f, err := os.Open(filename) |
| if err != nil { |
| return nil, err |
| } |
| defer f.Close() |
| scan := bufio.NewScanner(f) |
| |
| var ( |
| tests []*searchTest |
| test searchTest |
| num int |
| curr = posNewline |
| ) |
| for scan.Scan() { |
| num += 1 |
| line := strings.TrimSpace(scan.Text()) |
| |
| var prefix string |
| if len(line) > 0 { |
| prefix = string(line[0]) |
| } |
| switch prefix { |
| case "#": |
| // Skip comment lines. |
| continue |
| case "": |
| // Each set of tests is separated by a newline. Before a newline, we must |
| // have passed a test case result, another newline, or a comment, |
| // otherwise this file can't be valid. |
| if curr != posNewline && curr != posResult { |
| return nil, fmt.Errorf("invalid syntax on line %d: %q", num, line) |
| } |
| if curr == posResult { |
| // This is the first time that we have seen a newline for this |
| // test set. Now that we know the test set is complete, append |
| // it to the array of tests, and reset test to an empty |
| // searchTest struct. |
| t2 := test |
| tests = append(tests, &t2) |
| test = searchTest{} |
| } |
| curr = posNewline |
| default: |
| switch curr { |
| case posNewline: |
| // The last position was a newline, so this must be the start |
| // of a new test set. |
| curr = posTitle |
| test.title = line |
| case posTitle: |
| // The last position was a title, so this must be the start |
| // of a new test set. |
| curr = posQuery |
| test.query = line |
| case posQuery, posResult: |
| // The last position was a query or a result, so this must be |
| // an expected search result. |
| curr = posResult |
| parts := strings.Split(line, " ") |
| if len(parts) != 2 { |
| return nil, fmt.Errorf("invalid syntax on line %d: %q", num, line) |
| } |
| r := &searchResult{ |
| symbol: parts[0], |
| pkg: parts[1], |
| } |
| test.results = append(test.results, r) |
| default: |
| // We should never reach this error. |
| return nil, fmt.Errorf("invalid syntax on line %d: %q", num, line) |
| } |
| } |
| } |
| if err := scan.Err(); err != nil { |
| return nil, fmt.Errorf("scan.Err(): %v", err) |
| } |
| tests = append(tests, &test) |
| return tests, nil |
| } |
| |
| // readSearchTests reads filename and returns a map of package path to imported |
| // by count. See tests/README.md for a description of the syntax. |
| func readImportedByCounts(filename string) (map[string]int, error) { |
| counts := map[string]int{} |
| f, err := os.Open(filename) |
| if err != nil { |
| return nil, err |
| } |
| defer f.Close() |
| scan := bufio.NewScanner(f) |
| for scan.Scan() { |
| line := strings.TrimSpace(scan.Text()) |
| if line == "" || line[0] == '#' { |
| continue |
| } |
| parts := strings.SplitN(line, ", ", 2) |
| c, err := strconv.Atoi(parts[1]) |
| if err != nil { |
| return nil, err |
| } |
| counts[parts[0]] = c |
| } |
| if err := scan.Err(); err != nil { |
| return nil, err |
| } |
| return counts, nil |
| } |
| |
| const ( |
| posNewline = 1 << iota |
| posTitle |
| posQuery |
| posResult |
| ) |