vole
Clone
Read only, no credentials:
https://rodent.software/vole.git
Write over SSH with a registered key:
git clone ssh://git@rodent.software:2222/vole.git
A free and open-source forge that lives in your Git repo.
| 1 | package index |
| 2 | |
| 3 | // The index is derived, which is what lets a schema change be a rebuild instead |
| 4 | // of a migration chain. These tests hold that promise to its two halves: a file |
| 5 | // written by an older build is emptied rather than misread, and the version it |
| 6 | // reports is the one this build writes. |
| 7 | |
| 8 | import ( |
| 9 | "path/filepath" |
| 10 | "slices" |
| 11 | "testing" |
| 12 | ) |
| 13 | |
| 14 | func TestSearchIssuesMatchesTitleNumberAndIDPrefix(t *testing.T) { |
| 15 | t.Parallel() |
| 16 | s, err := Open(filepath.Join(t.TempDir(), "index.db")) |
| 17 | if err != nil { |
| 18 | t.Fatal(err) |
| 19 | } |
| 20 | defer s.Close() |
| 21 | |
| 22 | const ( |
| 23 | first = "01M5JR2JK29KKMAA1T9MDNTPBP" |
| 24 | second = "01M5JR2JK29KKMAA1T9MDNTRBZ" |
| 25 | ) |
| 26 | for _, issue := range []struct { |
| 27 | id string |
| 28 | title string |
| 29 | number int |
| 30 | }{ |
| 31 | {first, "Quick retry on timeout", 42}, |
| 32 | {second, "Slow parser", 43}, |
| 33 | } { |
| 34 | if _, err := s.db.Exec( |
| 35 | `INSERT INTO issues(repo_id, id, title, state, author, created) VALUES('r1',?,?,'open','keenan',0)`, |
| 36 | issue.id, issue.title); err != nil { |
| 37 | t.Fatal(err) |
| 38 | } |
| 39 | if _, err := s.db.Exec( |
| 40 | `INSERT INTO entity_numbers(repo_id, number, kind, id) VALUES('r1',?,'issue',?)`, |
| 41 | issue.number, issue.id); err != nil { |
| 42 | t.Fatal(err) |
| 43 | } |
| 44 | } |
| 45 | |
| 46 | for _, test := range []struct { |
| 47 | term string |
| 48 | want []string |
| 49 | }{ |
| 50 | {"quick", []string{first}}, |
| 51 | {"Quick", []string{first}}, |
| 52 | {"#42", []string{first}}, |
| 53 | {"42", []string{first}}, |
| 54 | {"01M5JR2JK29KKMAA1T9MDNTR", []string{second}}, |
| 55 | {first, []string{first}}, |
| 56 | {"nothing", nil}, |
| 57 | {"", nil}, |
| 58 | } { |
| 59 | got, err := s.SearchIssues("r1", test.term, 8) |
| 60 | if err != nil { |
| 61 | t.Fatalf("SearchIssues(%q): %v", test.term, err) |
| 62 | } |
| 63 | ids := make([]string, len(got)) |
| 64 | for i, issue := range got { |
| 65 | ids[i] = issue.ID |
| 66 | } |
| 67 | if !slices.Equal(ids, test.want) { |
| 68 | t.Fatalf("SearchIssues(%q) = %v, want %v", test.term, ids, test.want) |
| 69 | } |
| 70 | } |
| 71 | } |
| 72 | |
| 73 | func TestSchemaVersionDropsRowsFromAnOlderShape(t *testing.T) { |
| 74 | t.Parallel() |
| 75 | path := filepath.Join(t.TempDir(), "index.db") |
| 76 | s, err := Open(path) |
| 77 | if err != nil { |
| 78 | t.Fatal(err) |
| 79 | } |
| 80 | // Rows as an older build would have left them, with the file claiming the |
| 81 | // previous version — the state every existing instance is in on upgrade. |
| 82 | if _, err := s.db.Exec(` |
| 83 | INSERT INTO repos(id, name, created, issues_sha) VALUES('r1','demo',0,'deadbeef'); |
| 84 | INSERT INTO issues(repo_id, id, title, state, author, created) VALUES('r1','i1','stale','open','keenan',0); |
| 85 | INSERT INTO issue_labels(repo_id, issue_id, label) VALUES('r1','i1','old'); |
| 86 | INSERT INTO issue_assignees(repo_id, issue_id, assignee) VALUES('r1','i1','wilbur'); |
| 87 | PRAGMA user_version = 1;`); err != nil { |
| 88 | t.Fatal(err) |
| 89 | } |
| 90 | if err := s.Close(); err != nil { |
| 91 | t.Fatal(err) |
| 92 | } |
| 93 | |
| 94 | // Opening with this build clears the derived rows and leaves the repo with no |
| 95 | // indexed issues, so the next read rebuilds it from git instead of serving |
| 96 | // rows derived from a shape that no longer exists. |
| 97 | s2, err := Open(path) |
| 98 | if err != nil { |
| 99 | t.Fatal(err) |
| 100 | } |
| 101 | defer s2.Close() |
| 102 | |
| 103 | for _, q := range []string{ |
| 104 | `SELECT COUNT(*) FROM issues`, |
| 105 | `SELECT COUNT(*) FROM issue_labels`, |
| 106 | `SELECT COUNT(*) FROM issue_assignees`, |
| 107 | `SELECT COUNT(*) FROM entity_numbers`, |
| 108 | } { |
| 109 | var n int |
| 110 | if err := s2.db.QueryRow(q).Scan(&n); err != nil { |
| 111 | t.Fatalf("%s: %v", q, err) |
| 112 | } |
| 113 | if n != 0 { |
| 114 | t.Fatalf("%s = %d, want 0 after a version bump", q, n) |
| 115 | } |
| 116 | } |
| 117 | var sha string |
| 118 | if err := s2.db.QueryRow(`SELECT issues_sha FROM repos WHERE id='r1'`).Scan(&sha); err != nil { |
| 119 | t.Fatal(err) |
| 120 | } |
| 121 | if sha != "" { |
| 122 | t.Fatalf("issues_sha = %q, want empty so the repo is re-read from git", sha) |
| 123 | } |
| 124 | var v int |
| 125 | if err := s2.db.QueryRow(`PRAGMA user_version`).Scan(&v); err != nil { |
| 126 | t.Fatal(err) |
| 127 | } |
| 128 | if v != schemaVersion { |
| 129 | t.Fatalf("user_version = %d, want %d", v, schemaVersion) |
| 130 | } |
| 131 | |
| 132 | // A second open is a no-op: the fix is not re-applied every time. |
| 133 | if _, err := s2.db.Exec(`INSERT INTO issues(repo_id, id, title, state, author, created) VALUES('r1','i2','fresh','open','keenan',0)`); err != nil { |
| 134 | t.Fatal(err) |
| 135 | } |
| 136 | if err := s2.Close(); err != nil { |
| 137 | t.Fatal(err) |
| 138 | } |
| 139 | s3, err := Open(path) |
| 140 | if err != nil { |
| 141 | t.Fatal(err) |
| 142 | } |
| 143 | defer s3.Close() |
| 144 | var n int |
| 145 | if err := s3.db.QueryRow(`SELECT COUNT(*) FROM issues`).Scan(&n); err != nil { |
| 146 | t.Fatal(err) |
| 147 | } |
| 148 | if n != 1 { |
| 149 | t.Fatalf("reopening at the current version dropped rows: %d", n) |
| 150 | } |
| 151 | } |
| 152 |