dbimptest

package
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 27, 2026 License: MIT Imports: 25 Imported by: 0

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

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
const (
	TypesBegin      = "<!-- dbimp:types -->"
	TypesEnd        = "<!-- /dbimp:types -->"
	InterfacesBegin = "<!-- dbimp:interfaces -->"
	InterfacesEnd   = "<!-- /dbimp:interfaces -->"
)

The markers around a generated table in docs/<PRODUCT>.md.

View Source
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.

View Source
const EnvUpdate = "DBIMP_UPDATE"

EnvUpdate is the environment variable that makes TypeTable and InterfaceTable write their table into the document, rather than compare it.

View Source
const FeaturesName = "features.json"

FeaturesName is the name of the survey of step 5a of docs/DRIVER.md in testdata/<driver>/.

View Source
const ManifestName = "manifest.json"

ManifestName is the name of the manifest in testdata/<driver>/.

View Source
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

func CheckGoroutines(t *testing.T)

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

func DefaultMatch(r *http.Request, body []byte, ex *Exchange) bool

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

func Replay(t *testing.T, dir string, match Match) *httptest.Server

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

func ReplayRelease(t *testing.T, dir, release string, match Match) *httptest.Server

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

func RunContract(t *testing.T, c Contract)

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

func TypeTable(t *testing.T, doc string, rows []TypeRow)

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

func WithLabel(ctx context.Context, item int, principal string) context.Context

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

func WriteManifest(path string, m *Manifest) error

WriteManifest writes m to the file at path.

Types

type ColumnsCase

type ColumnsCase struct {
	Body string
	Want []string
}

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 ErrorCase

type ErrorCase struct {
	Body string
	Rows int
}

ErrorCase is a result that holds Rows rows and then an error.

type Exchange

type Exchange struct {
	Request  Request  `json:"request"`
	Response Response `json:"response"`
}

Exchange is one request to a real server and its response, as a file under testdata/<driver>/ holds it.

func ReadExchange

func ReadExchange(path string) (*Exchange, error)

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

func ReadFeatures(path string) (*Features, error)

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

func ReadManifest(path string) (*Manifest, error)

ReadManifest reads the manifest in the file at path.

type Match

type Match func(r *http.Request, body []byte, ex *Exchange) bool

Match reports whether a request that a fake server received, with its body, is the request of ex.

type NullCase

type NullCase struct {
	Body   string
	Column int
}

NullCase is a result whose first row holds a NULL in column Column.

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

func (r *Recorder) Label(item int, principal string)

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) RoundTrip

func (r *Recorder) RoundTrip(req *http.Request) (*http.Response, error)

RoundTrip sends req, and writes the exchange.

func (*Recorder) WriteManifest

func (r *Recorder) WriteManifest(driver string) error

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.

func (Request) Content added in v0.2.0

func (r Request) Content() []byte

Content returns the bytes of the body.

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.

func (Response) Content added in v0.2.0

func (r Response) Content() []byte

Content returns the bytes of the body.

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

type StreamCase struct {
	Head string
	Row  string
	Sep  string
	Tail string
}

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.

type Value

type Value struct {
	// Name names the value, such as "max" or "unicode".
	Name string
	// In is the value to write.
	In any
	// Want is the value that a read returns into a *any. If it is nil and In
	// is not, RoundTrip wants In.
	Want any
}

Value is one value of a round trip.

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>/.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL