Documentation
¶
Overview ¶
Package dbimptest holds the test helpers that every driver in github.com/xo/dbimp shares. It replays and records the responses of a real server, checks that a test leaves no goroutine behind, runs the contract of docs/DRIVER.md against a driver, and writes the type table and the interface table of a driver into its document.
Only a test imports this package. It imports testing and net/http/httptest, which a driver must not pull into the build of a consumer, so it is not in the root package (D37 in docs/decisions/).
Index ¶
- Constants
- func CheckGoroutines(t *testing.T)
- func DefaultMatch(r *http.Request, body []byte, ex *Exchange) bool
- func InterfaceTable(t *testing.T, doc string, reasons map[string]string, ...)
- func Replay(t *testing.T, dir string, match Match) *httptest.Server
- func ReplayRelease(t *testing.T, dir, release string, match Match) *httptest.Server
- func RoundTrip(tb testing.TB, db *sql.DB, c RoundTripCase)
- func RunContract(t *testing.T, c Contract)
- func TypeTable(t *testing.T, doc string, rows []TypeRow)
- func WithLabel(ctx context.Context, item int, principal string) context.Context
- func WriteManifest(path string, m *Manifest) error
- type ColumnsCase
- type Contract
- type Entry
- type ErrorCase
- type Exchange
- type Feature
- type Features
- type Manifest
- type Match
- type NullCase
- type Recorder
- type Request
- type Response
- type RoundTripCase
- type Source
- type StreamCase
- type TypeRow
- type Value
Constants ¶
const ( // KindCRUD is a statement of CRUD, such as update. KindCRUD = "crud" // KindSchema is an operation on a schema, such as a foreign key. KindSchema = "schema" // KindFeature is a feature of the database that is its own. KindFeature = "feature" // KindType is a native type. KindType = "type" )
The kinds of an entry of the survey.
const ( // SourceModel is an AI model. SourceModel = "model" // SourceDriver is another driver, in Go or in another language. SourceDriver = "driver" )
The kinds of a source of the survey.
const ( // NotMeasured is the verdict of an entry that no server settled yet. NotMeasured = "not measured" // Yes is the verdict of an entry that the server supports. Yes = "yes" // No is the verdict of an entry that the server refused. No = "no" )
The verdicts of an entry of the survey.
const ( // Administrator is the principal that administers the server. Administrator = "administrator" // Ordinary is the ordinary user that the Init step of dbrun creates. Ordinary = "ordinary" )
The principals of step 6 of docs/DRIVER.md.
const ( TypesBegin = "<!-- dbimp:types -->" TypesEnd = "<!-- /dbimp:types -->" InterfacesBegin = "<!-- dbimp:interfaces -->" InterfacesEnd = "<!-- /dbimp:interfaces -->" )
The markers around a generated table in docs/<PRODUCT>.md.
const EnvRecord = "DBIMP_RECORD"
EnvRecord is the environment variable that a driver test reads to decide whether it records. A driver test that records runs only when it is set.
const EnvUpdate = "DBIMP_UPDATE"
EnvUpdate is the environment variable that makes TypeTable and InterfaceTable write their table into the document, rather than compare it.
const FeaturesName = "features.json"
FeaturesName is the name of the survey of step 5a of docs/DRIVER.md in testdata/<driver>/.
const ManifestName = "manifest.json"
ManifestName is the name of the manifest in testdata/<driver>/.
const RequestsName = "requests.json"
RequestsName is the name of the script in testdata/<driver>/ that the command dbimptest/cmd/record reads. It lists the requests of step 6.
Variables ¶
This section is empty.
Functions ¶
func CheckGoroutines ¶
CheckGoroutines fails the test if a goroutine that started during the test is still running when the test ends, as step 12 of docs/DRIVER.md requires. Call it first in the test, so that it runs after every other cleanup, such as the one that closes the database. It counts every goroutine of the process, so the test that calls it must not run in parallel with another test.
func DefaultMatch ¶
DefaultMatch matches the method, the path and the query exactly. It matches two JSON bodies if their canonical forms are equal, and any other two bodies if their bytes are equal.
func InterfaceTable ¶
func InterfaceTable(t *testing.T, doc string, reasons map[string]string, connector driver.Connector, values ...any)
InterfaceTable writes the interface table of step 10 of docs/DRIVER.md between InterfacesBegin and InterfacesEnd in the document at doc. values are the types of the driver, such as its driver, its connector, a connection and its rows. An interface is implemented if one of values implements it. reasons holds, for each interface, why the driver implements it or does not, and the test fails if one is missing. The connector is the only value that must implement io.Closer.
func Replay ¶
Replay starts a fake server that answers each request with the response of the first exchange in dir that match accepts. If match is nil, it uses DefaultMatch. A request that no exchange matches fails the test. Replay reads every file in dir except the manifest, the script of requests and the survey, and closes the server when the test ends.
func ReplayRelease ¶
ReplayRelease is Replay for the exchanges of one release only, whose files start with the name of the release, as dbrun names it. A test uses it when releases answer the same request in different ways.
func RoundTrip ¶
func RoundTrip(tb testing.TB, db *sql.DB, c RoundTripCase)
RoundTrip runs the round trip of c against db. For each value, and once with the value as a bound argument and once as a literal, it inserts a row, selects it by its key and compares the value, updates it to the next value, selects and compares again, deletes it, and selects to see that it is gone. It reads each value into a *any, so the comparison covers the Go type that database/sql returns, and it never selects a literal in place of a stored value.
func RunContract ¶
RunContract tests the driver against c. Each subtest checks that it leaves no goroutine behind, so the test that calls RunContract must not run in parallel with another test.
func TypeTable ¶
TypeTable writes rows as the type table between TypesBegin and TypesEnd in the document at doc, and fails the test if the document holds another table. If EnvUpdate is set, it writes the table into the document.
func WithLabel ¶
WithLabel returns a context that labels the request made with it. A Recorder uses that label rather than the one that Label set, so that a request that runs while others are sent keeps its own item.
func WriteManifest ¶
WriteManifest writes m to the file at path.
Types ¶
type ColumnsCase ¶
ColumnsCase is a result and the names of its columns, in order.
type Contract ¶
type Contract struct {
// Open returns a database for the fake server at url, which is an
// http:// URL. The driver builds its own DSN from url.
Open func(t *testing.T, url string) *sql.DB
// Query is any statement. A fake server ignores it, and answers each case
// with the body of that case.
Query string
// Columns is a result with at least two columns.
Columns ColumnsCase
// Null is a result whose first row holds a NULL.
Null NullCase
// ErrorAfterRows is a result that holds some rows and then an error.
ErrorAfterRows ErrorCase
// Stream is the form of a result with many rows.
Stream StreamCase
// Transactions is true if the driver supports transactions. If it is
// false, BeginTx must return dbimp.ErrNotSupported (D20).
Transactions bool
// ContentType is the content type of the bodies of the cases, such as
// "application/cbor". It is "application/json" when it is empty.
ContentType string
}
Contract is the part of the contract of D8 and D18 to D21 that every driver keeps in the same way. RunContract tests it against fake servers, which answer with bodies in the form of the product. The driver supplies the bodies, because each product has its own form.
type Entry ¶
type Entry struct {
// Item is the number of the item in step 6 of docs/DRIVER.md.
Item int `json:"item"`
// Principal is Administrator or Ordinary.
Principal string `json:"principal"`
// Release is the release of the server, as dbrun names it.
Release string `json:"release"`
// Date is the date of the recording, as YYYY-MM-DD.
Date string `json:"date"`
// File is the name of the exchange in testdata/<driver>/, or "" if Absent
// is set.
File string `json:"file,omitzero"`
// Absent is the reason why the item does not apply to the product, such as
// "no transactions", or "" if File is set.
Absent string `json:"absent,omitzero"`
}
Entry is one recorded exchange, or one item of step 6 that does not apply to the product.
type Exchange ¶
Exchange is one request to a real server and its response, as a file under testdata/<driver>/ holds it.
func ReadExchange ¶
ReadExchange reads the exchange in the file at path.
type Feature ¶
type Feature struct {
// Kind is KindCRUD, KindSchema, KindFeature or KindType.
Kind string `json:"kind"`
// Name names it, such as "update", "foreign key" or "number". A type is
// named as the type table names it.
Name string `json:"name"`
// Sources names each source that named it.
Sources []string `json:"sources"`
// Verdict is NotMeasured, Yes or No.
Verdict string `json:"verdict"`
// Evidence is the recorded file under testdata/<driver>/ that shows what
// the server answered.
Evidence string `json:"evidence,omitzero"`
// Test is the test that exercises it, as a function and a subtest, such
// as "TestIntegrationCRUD/update". For the verdict No, the test sends
// the operation and expects the refusal of the server.
Test string `json:"test,omitzero"`
}
Feature is one operation, feature or type of the survey.
type Features ¶
type Features struct {
// Driver is the name of the driver.
Driver string `json:"driver"`
// Consulted lists each model and each driver that the survey asked.
Consulted []Source `json:"consulted"`
// Entries has one entry for each operation, feature and type.
Entries []Feature `json:"entries"`
}
Features is the survey of step 5a of docs/DRIVER.md: each operation, feature and type of one database, where each came from, what the server said about it, and the test that exercises it.
func ReadFeatures ¶
ReadFeatures reads the survey in the file at path.
type Manifest ¶
type Manifest struct {
// Driver is the name of the driver.
Driver string `json:"driver"`
// NoOrdinaryUser is the reason why the product has no ordinary user, or
// "" if it has one.
NoOrdinaryUser string `json:"noOrdinaryUser,omitzero"`
// Entries has one entry for each recorded exchange.
Entries []Entry `json:"entries"`
}
Manifest lists the recorded exchanges of one driver, as step 6 of docs/DRIVER.md requires.
func ReadManifest ¶
ReadManifest reads the manifest in the file at path.
type Match ¶
Match reports whether a request that a fake server received, with its body, is the request of ex.
type Recorder ¶
type Recorder struct {
// contains filtered or unexported fields
}
Recorder is an http.RoundTripper that writes each exchange with a real server into a directory, as step 6 of docs/DRIVER.md requires. It removes the value of each header that holds a credential. It reads each response whole, so it is for a recording and never for a driver.
func NewRecorder ¶
func NewRecorder(dir, release string, next http.RoundTripper) *Recorder
NewRecorder returns a Recorder that sends each request with next and writes each exchange into dir. release is the release of the server, as dbrun names it, and the name of each file starts with it.
func (*Recorder) Label ¶
Label sets the item of step 6 and the principal of the exchanges that follow, when a request carries no label of its own.
func (*Recorder) WriteManifest ¶
WriteManifest adds the entries of the recording to the manifest of driver in the directory, and writes the manifest.
type Request ¶
type Request struct {
Method string `json:"method"`
Path string `json:"path"`
Query string `json:"query,omitzero"`
Header http.Header `json:"header,omitzero"`
Body string `json:"body,omitzero"`
Binary []byte `json:"binary,omitzero"`
}
Request is the recorded part of a request. A body that is text is in Body, and a binary body, such as one in CBOR, is in Binary, which the file holds as base64.
type Response ¶
type Response struct {
Status int `json:"status"`
Header http.Header `json:"header,omitzero"`
Body string `json:"body"`
Binary []byte `json:"binary,omitzero"`
}
Response is the recorded part of a response. A body that is text is in Body, and a binary body is in Binary, as for a Request.
type RoundTripCase ¶
type RoundTripCase struct {
// Type names the type, as the type table names it.
Type string
// Setup holds the statements that create the table, which RoundTrip
// runs once, before any value.
Setup []string
// Teardown holds the statements that drop what Setup made. RoundTrip
// runs them in a cleanup, so they run even when the test fails.
Teardown []string
// Insert inserts one row. Its arguments are the key and the value.
Insert string
// Literal returns a statement that inserts one row, with the key and the
// value written as literals. It returns an error that wraps
// dbimp.ErrNotSupported for a value that the database cannot write as a
// literal, and RoundTrip logs that and goes on.
Literal func(key string, v any) (string, error)
// Select selects the value of the row with a key, which is its one
// argument. It returns one column.
Select string
// Update sets the value of the row with a key. Its arguments are the
// value and the key.
Update string
// Delete deletes the row with a key, which is its one argument.
Delete string
// Values are the values to store. RoundTrip updates each value to the
// next one, and the last one to the first, so it needs at least two.
Values []Value
// Wait is how long RoundTrip reads again until a read sees the write
// before it, for a database that updates an index after a write. Zero
// reads once.
Wait time.Duration
// Equal compares a value read back with the value wanted. If it is nil,
// RoundTrip uses reflect.DeepEqual, which also compares the Go types.
Equal func(got, want any) bool
// Named passes each argument by name, for a database that binds named
// parameters only: the key as sql.Named("key", k), and the value as
// sql.Named("value", v). The statements then name the key and the value,
// such as $key and $value.
Named bool
// KeyArg turns the key into the argument that the statements take, such
// as a record id. If it is nil, the argument is the key as a string.
// Literal gets the key as a string, and writes it in the same form.
KeyArg func(key string) any
}
RoundTripCase is the round trip of one type, as step 14a of docs/DRIVER.md requires. The driver supplies the statements, because each database writes them in its own way. RoundTrip runs every step itself.
type Source ¶
type Source struct {
// Source names the model, or the import path of the driver.
Source string `json:"source"`
// Kind is SourceModel or SourceDriver.
Kind string `json:"kind"`
// Date is the date of the question, as YYYY-MM-DD.
Date string `json:"date"`
}
Source is one model or one driver that the survey asked.
type StreamCase ¶
StreamCase is the form of a result with many rows of one column: Head, then Row repeated with Sep between each two, then Tail.
type TypeRow ¶
type TypeRow struct {
// Wire is the name of the type on the wire, such as "number".
Wire string
// Go is the Go type of a value, such as "int64".
Go string
// ScanType is the type that ColumnTypeScanType returns.
ScanType string
// DatabaseType is the name that ColumnTypeDatabaseTypeName returns, in
// upper case.
DatabaseType string
// Nullable is true if a column of the type can be NULL.
Nullable bool
}
TypeRow is one row of the type table of step 10 of docs/DRIVER.md.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
record
command
Command record sends the requests of step 6 of docs/DRIVER.md to a real server, as the administrator and as the ordinary user, and writes each exchange and the manifest into testdata/<driver>/.
|
Command record sends the requests of step 6 of docs/DRIVER.md to a real server, as the administrator and as the ordinary user, and writes each exchange and the manifest into testdata/<driver>/. |