TestSchema generates an isolated schema for use during a single test run. Migrations are run in the schema (this adds ~50 ms of overhead) to prepare it for River testing. After a test run, the schema in use is checked back into a pool for potential reuse. When a schema is reused, tables in TruncateT
(ctx context.Context, tb testutil.TestingTB, driver riverdriver.Driver[TTx], opts *TestSchemaOpts)
| 126 | // problems where test cases accidentally inject data into the default schema |
| 127 | // rather than the one returned by this function. |
| 128 | func TestSchema[TTx any](ctx context.Context, tb testutil.TestingTB, driver riverdriver.Driver[TTx], opts *TestSchemaOpts) string { |
| 129 | tb.Helper() |
| 130 | |
| 131 | require.NotNil(tb, driver, "driver should not be nil") |
| 132 | |
| 133 | if opts == nil { |
| 134 | opts = &TestSchemaOpts{} |
| 135 | } |
| 136 | |
| 137 | // An initial pass to calculate a friendly package name that'll be used to |
| 138 | // prefix this package's schemas so that it won't clash with packages |
| 139 | // running their own tests in parallel. Generated name is like `river` or |
| 140 | // `jobcompleter` or `riverpro`. |
| 141 | genSchemaBase.Do(func() { |
| 142 | var ( |
| 143 | programCounterAddr, _, _, _ = runtime.Caller(4 + opts.skipExtraFrames) // skip `TestSchema.func1` (closure) + `sync.(*Once).doSlow` + `sync.(*Once).Do` + `TestSchema` and end up at `TestSchema`'s caller |
| 144 | funcName = runtime.FuncForPC(programCounterAddr).Name() // like: github.com/riverqueue/river.Test_Client.func1 |
| 145 | ) |
| 146 | |
| 147 | packageName = packageFromFunc(funcName) // like: `river` (or `jobcompleter`, or `riverpro`) |
| 148 | |
| 149 | // Check to make sure we're skipping the right number of frames above. |
| 150 | // If the location of `runtime.Caller` is changed at all (a single new |
| 151 | // function is added to the stack), the reported package will be |
| 152 | // completely wrong, so we try to take precautions about it. |
| 153 | if packageName == "riverdbtest" && !opts.skipPackageNameCheck { |
| 154 | panic("package name should not resolve to riverdbtest") |
| 155 | } |
| 156 | |
| 157 | // Notification topics are prefixed with schemas. The max Postgres |
| 158 | // length of topics is 63, and "river_leadership" is the longest topic |
| 159 | // name. If the package suffix is longer than the max that could fit |
| 160 | // into 63 when combined with a schema name and leadership, trim it down |
| 161 | // a bit. The only package where this is needed as I write this is |
| 162 | // `riverencrypt_test`. If this happens in too many places we may want |
| 163 | // to trim "_schema_" to an abbreviation or shorten "river_leadership". |
| 164 | const maxLength = 63 - len("_2025_04_20t16_00_20_schema_01.river_leadership") - 1 |
| 165 | if len(packageName) > maxLength { |
| 166 | packageName = packageName[0:maxLength] |
| 167 | } |
| 168 | |
| 169 | schemaBaseName = packageName + "_" + time.Now().Format(schemaDateFormat) + "_schema_" |
| 170 | }) |
| 171 | |
| 172 | // Schemas aren't dropped after a package test run. Instead, we drop them |
| 173 | // before starting a test run. This happens in a `sync.Once` to minimize the |
| 174 | // amount of work that needs to be done (it has to run once, but all other |
| 175 | // TestSchema invocations skip it). It's done once per database (i.e. once |
| 176 | // for Postgres and once for SQLite). |
| 177 | // |
| 178 | // The code below may look a little odd using both a mutex and `sync.Once`. |
| 179 | // It's done this way as a minor optimization so that cleanups for different |
| 180 | // database types can run in parallel with each other. |
| 181 | initialCleanupOnce := func() *sync.Once { |
| 182 | initialCleanupMu.Lock() |
| 183 | defer initialCleanupMu.Unlock() |
| 184 | |
| 185 | if _, ok := initialCleanup[driver.DatabaseName()]; !ok { |
no test coverage detected
searching dependent graphs…