| // 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" |
| "errors" |
| "flag" |
| "fmt" |
| "os" |
| "strconv" |
| "strings" |
| |
| _ "github.com/jackc/pgx/v4/stdlib" // for pgx driver |
| "golang.org/x/pkgsite/internal/config" |
| "golang.org/x/pkgsite/internal/database" |
| "golang.org/x/pkgsite/internal/derrors" |
| "golang.org/x/pkgsite/internal/frontend" |
| "golang.org/x/pkgsite/internal/log" |
| "golang.org/x/pkgsite/internal/postgres" |
| ) |
| |
| var frontendHost = flag.String("frontend", "http://localhost:8080", |
| "Use the frontend host referred to by this URL for comparing data") |
| |
| func main() { |
| flag.Parse() |
| |
| ctx := context.Background() |
| cfg, err := config.Init(ctx) |
| if err != nil { |
| log.Fatal(ctx, err) |
| } |
| log.SetLevel(cfg.LogLevel) |
| |
| if err := runImportedByUpdates(ctx, cfg.DBConnInfo(), cfg.DBHost); err != nil { |
| log.Fatal(ctx, err) |
| } |
| if err := run(*frontendHost); err != nil { |
| log.Fatal(ctx, err) |
| } |
| } |
| |
| const ( |
| importedbyFile = "tests/search/importedby.txt" |
| ) |
| |
| var testFiles = []string{ |
| "tests/search/scripts/default.txt", |
| "tests/search/scripts/symbolsearch.txt", |
| } |
| |
| func runImportedByUpdates(ctx context.Context, dbConnInfo, dbHost string) error { |
| ddb, err := database.Open("pgx", dbConnInfo, "seeddb") |
| if err != nil { |
| log.Fatalf(ctx, "database.Open for host %s failed with %v", dbHost, err) |
| } |
| db := postgres.New(ddb) |
| defer db.Close() |
| counts, err := readImportedByCounts(importedbyFile) |
| if err != nil { |
| return err |
| } |
| _, err = db.UpdateSearchDocumentsImportedByCountWithCounts(ctx, counts) |
| return err |
| } |
| |
| func run(frontendHost string) error { |
| var tests []*searchTest |
| for _, testFile := range testFiles { |
| ts, err := readSearchTests(testFile) |
| if err != nil { |
| return err |
| } |
| tests = append(tests, ts...) |
| } |
| client := frontend.NewClient(frontendHost) |
| var failed bool |
| for _, st := range tests { |
| output, err := runTest(client, 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(client *frontend.Client, st *searchTest) (output []string, err error) { |
| defer derrors.Wrap(&err, "runTest(ctx, db, st.title: %q)", st.title) |
| searchPage, err := client.Search(st.query, st.mode) |
| if err != nil { |
| return nil, err |
| } |
| gotResults := searchPage.Results |
| if strings.ContainsAny(searchPage.PackageTabQuery, "#") { |
| output = append(output, "invalid package tab query, should not contain #: %q", searchPage.PackageTabQuery) |
| } |
| for i, want := range st.results { |
| got := &frontend.SearchResult{} |
| if len(gotResults) > i { |
| got = gotResults[i] |
| } |
| // The mode we expect is determined by whether the expected result |
| // indicates a symbol is present. |
| wantMode := "package" |
| if want.symbol != "" { |
| wantMode = "symbol" |
| } |
| if want.symbol != got.SymbolName || want.pkg != got.PackagePath || wantMode != searchPage.SearchMode { |
| output = append(output, |
| fmt.Sprintf("query: %q, mismatch result %d:\n\twant: %q %q [m=%q]\n\t got: %q %q [m=%q]\n", |
| st.query, i+1, |
| want.pkg, want.symbol, wantMode, |
| got.PackagePath, got.SymbolName, searchPage.SearchMode)) |
| } |
| } |
| return output, nil |
| } |
| |
| type searchTest struct { |
| title string |
| query string |
| mode 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): %q", num, filename, 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 |
| parts := strings.Fields(line) |
| mode := strings.TrimSuffix(strings.TrimPrefix(parts[0], "["), "]") |
| if len(parts) <= 1 { |
| return nil, fmt.Errorf("invalid syntax on line %d: %q (not enough elements)", num, line) |
| } |
| if mode != "" && mode != "package" && mode != "symbol" { |
| return nil, fmt.Errorf("invalid syntax on line %d: %q (invalid mode: %q)", num, line, mode) |
| } |
| test.mode = mode |
| test.query = strings.Join(parts[1:], " ") |
| 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, " ") |
| r := &searchResult{} |
| if test.mode == "symbol" { |
| if len(parts) != 2 { |
| return nil, fmt.Errorf("invalid syntax on line %d (%q): %q (want symbol result)", num, filename, line) |
| } |
| r.symbol = parts[0] |
| r.pkg = parts[1] |
| } else { |
| if len(parts) != 1 { |
| return nil, fmt.Errorf("invalid syntax on line %d (%q): %q (want package result)", num, filename, line) |
| } |
| r.pkg = parts[0] |
| } |
| 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 |
| } |
| path, count, found := strings.Cut(line, ", ") |
| if !found { |
| return nil, errors.New("missing comma") |
| } |
| c, err := strconv.Atoi(count) |
| if err != nil { |
| return nil, err |
| } |
| counts[path] = c |
| } |
| if err := scan.Err(); err != nil { |
| return nil, err |
| } |
| return counts, nil |
| } |
| |
| const ( |
| posNewline = 1 << iota |
| posTitle |
| posQuery |
| posResult |
| ) |