exercise

package
v1.1.1 Latest Latest
Warning

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

Go to latest
Published: Jun 9, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

Documentation

Overview

Package exercise ejerce la superficie pública de Quark contra cada motor, marcando los símbolos que invoca (para el gate de cobertura del manifiesto) y asertando el resultado funcional. Reusa engine.Run (S4) para el lifecycle por motor + el chequeo de fugas; añade el recorder por motor y la cobertura.

Un Exerciser cubre un área (crud, builder, relations, tx, cache, tenant, migrate, security, ha, observability). Cada uno es un `fn` que engine.Run corre por motor; los asserts funcionales viven dentro de cada Exerciser.

Index

Constants

This section is empty.

Variables

View Source
var BUILDER = Exerciser{Name: "builder", Fn: func(ctx context.Context, client *quark.Client, rec *recorder.Recorder, _ Conn) error {
	rec.Note(QF("For"))
	n := atomic.AddInt64(&builderSeq, 1)

	owner := &domain.Account{Email: fmt.Sprintf("builder%d@superapp.test", n), Name: "b", Role: "member", Active: true}
	if err := quark.For[domain.Account](ctx, client).Create(owner); err != nil {
		return fmt.Errorf("seed owner: %w", err)
	}
	proj := &domain.Project{OwnerID: owner.ID, Name: "builder-proj", Status: "active"}
	if err := quark.For[domain.Project](ctx, client).Create(proj); err != nil {
		return fmt.Errorf("seed project: %w", err)
	}
	for p := 1; p <= 5; p++ {
		t := &domain.Task{ProjectID: proj.ID, Title: fmt.Sprintf("t%d", p), Priority: p, Done: p%2 == 0}
		if err := quark.For[domain.Task](ctx, client).Create(t); err != nil {
			return fmt.Errorf("seed task: %w", err)
		}
	}
	scope := func() *quark.Query[domain.Task] {
		return quark.For[domain.Task](ctx, client).Where("project_id", "=", proj.ID)
	}

	if sum, err := quark.For[domain.Task](rec.Mark(ctx, QM("Sum")), client).Where("project_id", "=", proj.ID).Sum("priority"); err != nil || sum != 15 {
		return fmt.Errorf("Sum(priority)=%v err=%v, esperaba 15", sum, err)
	}
	if avg, err := quark.For[domain.Task](rec.Mark(ctx, QM("Avg")), client).Where("project_id", "=", proj.ID).Avg("priority"); err != nil || avg != 3 {
		return fmt.Errorf("Avg=%v err=%v, esperaba 3", avg, err)
	}
	if mn, err := quark.For[domain.Task](rec.Mark(ctx, QM("Min")), client).Where("project_id", "=", proj.ID).Min("priority"); err != nil || mn != 1 {
		return fmt.Errorf("Min=%v err=%v, esperaba 1", mn, err)
	}
	if mx, err := quark.For[domain.Task](rec.Mark(ctx, QM("Max")), client).Where("project_id", "=", proj.ID).Max("priority"); err != nil || mx != 5 {
		return fmt.Errorf("Max=%v err=%v, esperaba 5", mx, err)
	}

	rec.Note(QM("Select"), QM("GroupBy"), QM("Having"))
	groups, err := scope().Select("done").GroupBy("done").Having("done", "=", true).List()
	if err != nil {
		return fmt.Errorf("Select/GroupBy/Having: %w", err)
	}
	if len(groups) == 0 {
		return fmt.Errorf("GroupBy no devolvió grupos")
	}

	if c, err := quark.For[domain.Task](rec.Mark(ctx, QM("WhereIn")), client).Where("project_id", "=", proj.ID).WhereIn("priority", []any{1, 2, 3}).Count(); err != nil || c != 3 {
		return fmt.Errorf("WhereIn count=%d err=%v, esperaba 3", c, err)
	}

	if c, err := scope().Where("priority", "=", 5).Or(func(q *quark.Query[domain.Task]) *quark.Query[domain.Task] {
		return q.Where("project_id", "=", proj.ID).Where("priority", "=", 1)
	}).Count(); err != nil || c < 1 {
		return fmt.Errorf("Or count=%d err=%v, esperaba >=1", c, err)
	}
	rec.Note(QM("Or"))

	rec.Note(QM("OrderBy"), QM("Offset"))
	top, err := scope().OrderBy("priority", "DESC").Offset(1).Limit(1).List()
	if err != nil || len(top) != 1 || top[0].Priority != 4 {
		return fmt.Errorf("OrderBy/Offset: %+v err=%v, esperaba priority=4", top, err)
	}

	rec.Note(QM("Distinct"))
	if _, err := scope().Distinct().List(); err != nil {
		return fmt.Errorf("Distinct: %w", err)
	}

	want := top[0].ID
	found, err := quark.For[domain.Task](rec.Mark(ctx, QM("Find")), client).Find(want)
	if err != nil || found.ID != want {
		return fmt.Errorf("Find(%d)=%+v err=%v", want, found, err)
	}

	count := 0
	if err := scope().Iter(func(domain.Task) error { count++; return nil }); err != nil {
		return fmt.Errorf("Iter: %w", err)
	}
	if count != 5 {
		return fmt.Errorf("Iter contó %d, esperaba 5", count)
	}
	rec.Note(QM("Iter"))

	cur, err := scope().Cursor()
	if err != nil {
		return fmt.Errorf("Cursor: %w", err)
	}
	cn := 0
	for cur.Next() {
		var t domain.Task
		if err := cur.Scan(&t); err != nil {
			_ = cur.Close()
			return fmt.Errorf("Cursor.Scan: %w", err)
		}
		cn++
	}
	if err := cur.Err(); err != nil {
		_ = cur.Close()
		return fmt.Errorf("Cursor.Err: %w", err)
	}
	if err := cur.Close(); err != nil {
		return fmt.Errorf("Cursor.Close: %w", err)
	}
	if cn != 5 {
		return fmt.Errorf("Cursor contó %d, esperaba 5", cn)
	}
	rec.Note(QM("Cursor"))

	page, err := scope().Paginate(2, 0)
	if err != nil {
		return fmt.Errorf("Paginate: %w", err)
	}
	if page.Total != 5 || len(page.Items) != 2 {
		return fmt.Errorf("Paginate total=%d items=%d, esperaba 5/2", page.Total, len(page.Items))
	}
	rec.Note(QM("Paginate"))

	return nil
}}

BUILDER ejerce la superficie de construcción de queries sobre datos propios y deterministas: agregados, group/having, filtrado (WhereIn/Or), orden/paginado, streaming (Iter/Cursor), Find. Setops/locking/CTE quedan para exercisers posteriores (necesitan matriz de capacidad por motor).

View Source
var CACHE = Exerciser{Name: "cache", Fn: runCache}

CacheExerciser ejerce las garantías de la caché L2 integrada (ADR-0004) por CONTEO de statements (diff de rec.Count()), no por inspección del store — reusa el patrón de recorder/infra_test.go. La caché la instala el suite en newClient (WithCacheStore(memory.New())); el store dormita para los exercisers que no llaman .Cache() y se cierra en el fn del suite antes del leak-check (cleanupLoop es una goroutine).

Tres garantías observables sin tocar el store:

  1. hit = 0 SQL — una 2ª query idéntica con .Cache() no ejecuta SQL (el middleware del recorder no se dispara → Count() no cambia).
  2. invalidación por mutación — un Create sobre la tabla cacheada llama InvalidateTags(tabla) (la misma tag que .Cache() auto-añade), así que la siguiente .Cache() vuelve a ejecutar.
  3. N+1 acotado — un Preload de M padres suma 1 statement (hijos vía IN ...), no M: el delta de Count() queda en 2 (padres + IN), no en 1+M.
View Source
var CRUD = Exerciser{Name: "crud", Fn: func(ctx context.Context, client *quark.Client, rec *recorder.Recorder, _ Conn) error {
	rec.Note(QF("For"))
	email := fmt.Sprintf("crud%d@superapp.test", atomic.AddInt64(&crudSeq, 1))

	a := &domain.Account{Email: email, Name: "crud", Role: "member", Active: true}
	if err := quark.For[domain.Account](rec.Mark(ctx, QM("Create")), client).Create(a); err != nil {
		return fmt.Errorf("create: %w", err)
	}
	if a.ID == 0 {
		return fmt.Errorf("create no asignó ID")
	}

	rec.Note(QM("Where"))
	got, err := quark.For[domain.Account](rec.Mark(ctx, QM("First")), client).Where("email", "=", email).First()
	if err != nil {
		return fmt.Errorf("first: %w", err)
	}
	if got.ID != a.ID || got.Email != email {
		return fmt.Errorf("first round-trip roto: got id=%d email=%q", got.ID, got.Email)
	}

	n, err := quark.For[domain.Account](rec.Mark(ctx, QM("Count")), client).Where("email", "=", email).Count()
	if err != nil {
		return fmt.Errorf("count: %w", err)
	}
	if n != 1 {
		return fmt.Errorf("count=%d, esperaba 1", n)
	}

	got.Name = "crud-updated"
	rows, err := quark.For[domain.Account](rec.Mark(ctx, QM("Update")), client).Update(&got)
	if err != nil {
		return fmt.Errorf("update: %w", err)
	}
	if rows != 1 {
		return fmt.Errorf("update afectó %d filas, esperaba 1", rows)
	}

	fresh, err := quark.For[domain.Account](ctx, client).Where("id", "=", a.ID).First()
	if err != nil {
		return fmt.Errorf("reread: %w", err)
	}
	if fresh.Name != "crud-updated" {
		return fmt.Errorf("update no persistió: name=%q", fresh.Name)
	}

	if _, err := quark.For[domain.Account](rec.Mark(ctx, QM("Delete")), client).Delete(&fresh); err != nil {
		return fmt.Errorf("delete: %w", err)
	}
	after, err := quark.For[domain.Account](ctx, client).Where("id", "=", a.ID).Count()
	if err != nil {
		return fmt.Errorf("count post-delete: %w", err)
	}
	if after != 0 {
		return fmt.Errorf("soft-delete no excluyó la fila: count=%d", after)
	}

	rec.Note(QM("Limit"))
	if _, err := quark.For[domain.Account](rec.Mark(ctx, QM("List")), client).Limit(10).List(); err != nil {
		return fmt.Errorf("list: %w", err)
	}
	return nil
}}

CRUD es el patrón canónico: Create → First → Count → Update → Delete(soft) → List, con un assert funcional por paso y marcando cada símbolo invocado.

View Source
var DBPERTENANT = Exerciser{Name: "tenant-db-per", Fn: runDBPerTenant}

DBPERTENANT ejerce la estrategia DatabasePerTenant (ADR-0007): el router abre un *Client por tenant vía factory y los cachea en un LRU. Se asertan las dos garantías del contrato:

  1. Aislamiento físico — cada tenant ve sólo su base (counts exactos, sin columna tenant_id de por medio), y los datos sobreviven al ciclo del pool (evicción → re-open → siguen ahí).
  2. El LRU evicta — con MaxCachedPools=1 y 2 tenants alternados, el factory se invoca en cada cambio de tenant (4 veces, no 2) y ActiveTenants() refleja sólo el pool vivo. La evicción cierra el client evictado (async); el exerciser cierra además todos los que abrió antes del leak-check.

Aprovisionamiento por motor (FeatDBPerTenantProvision): SQLite = un fichero por tenant; PG/MySQL/MariaDB/MSSQL = CREATE DATABASE vía un admin client (client.Exec va directo a db.ExecContext, sin tx — PG exige CREATE DATABASE fuera de tx) + rewrite del DSN (tenant_dsn.go). Oracle se salta documentado: una database por tenant ahí es un PDB, fuera del alcance del harness.

View Source
var RELATIONS = Exerciser{Name: "relations", Fn: func(ctx context.Context, client *quark.Client, rec *recorder.Recorder, _ Conn) error {
	rec.Note(QF("For"), QM("Preload"), QM("Where"))
	n := atomic.AddInt64(&relSeq, 1)

	owner := &domain.Account{Email: fmt.Sprintf("rel%d@superapp.test", n), Name: "rel", Role: "member", Active: true}
	if err := quark.For[domain.Account](ctx, client).Create(owner); err != nil {
		return fmt.Errorf("seed owner: %w", err)
	}
	proj := &domain.Project{OwnerID: owner.ID, Name: "rel-proj", Status: "active"}
	if err := quark.For[domain.Project](ctx, client).Create(proj); err != nil {
		return fmt.Errorf("seed project: %w", err)
	}
	aid := owner.ID
	assigned := &domain.Task{ProjectID: proj.ID, Title: "assigned", AssigneeID: &aid}
	if err := quark.For[domain.Task](ctx, client).Create(assigned); err != nil {
		return fmt.Errorf("seed assigned task: %w", err)
	}
	unassigned := &domain.Task{ProjectID: proj.ID, Title: "unassigned"}
	if err := quark.For[domain.Task](ctx, client).Create(unassigned); err != nil {
		return fmt.Errorf("seed unassigned task: %w", err)
	}

	t1, err := quark.For[domain.Task](rec.Mark(ctx, QM("First")), client).
		Preload("Project").Preload("Assignee").Where("id", "=", assigned.ID).First()
	if err != nil {
		return fmt.Errorf("preload belongs_to: %w", err)
	}
	if t1.Project == nil || t1.Project.ID != proj.ID {
		return fmt.Errorf("belongs_to Project no cargado: %+v", t1.Project)
	}
	if t1.Assignee == nil || t1.Assignee.ID != owner.ID {
		return fmt.Errorf("belongs_to Assignee no cargado: %+v", t1.Assignee)
	}

	t2, err := quark.For[domain.Task](ctx, client).Preload("Assignee").Where("id", "=", unassigned.ID).First()
	if err != nil {
		return fmt.Errorf("preload nullable FK: %w", err)
	}
	if t2.Assignee != nil {
		return fmt.Errorf("BB-5: Assignee debía quedar nil, cargó %+v", t2.Assignee)
	}

	acc, err := quark.For[domain.Account](ctx, client).Preload("Projects").Where("id", "=", owner.ID).First()
	if err != nil {
		return fmt.Errorf("preload has_many Projects: %w", err)
	}
	if len(acc.Projects) < 1 {
		return fmt.Errorf("has_many Projects vacío")
	}
	p, err := quark.For[domain.Project](ctx, client).Preload("Tasks").Where("id", "=", proj.ID).First()
	if err != nil {
		return fmt.Errorf("preload has_many Tasks: %w", err)
	}
	if len(p.Tasks) != 2 {
		return fmt.Errorf("has_many Tasks=%d, esperaba 2", len(p.Tasks))
	}

	mProj := &domain.Project{
		OwnerID: owner.ID, Name: "m2m-proj", Status: "active",
		Tags: []domain.Tag{
			{Slug: fmt.Sprintf("rel-tag-a-%d", n)},
			{Slug: fmt.Sprintf("rel-tag-b-%d", n)},
		},
	}
	if err := quark.For[domain.Project](rec.Mark(ctx, QM("Create")), client).Create(mProj); err != nil {
		return fmt.Errorf("m2m create con tags: %w", err)
	}
	mp, err := quark.For[domain.Project](ctx, client).Preload("Tags").Where("id", "=", mProj.ID).First()
	if err != nil {
		return fmt.Errorf("preload m2m Tags: %w", err)
	}
	if len(mp.Tags) != 2 {
		return fmt.Errorf("m2m Tags=%d, esperaba 2", len(mp.Tags))
	}

	return nil
}}

RELATIONS ejerce Preload en las tres formas: belongs_to (con el caso BB-5 de FK nullable que NO debe cargar basura), has_many, y many_to_many con persistencia de asociación (Create de un Project con Tags inserta en la tabla join).

View Source
var RLSNATIVE = Exerciser{Name: "tenant-rls-native", Fn: runRLSNative}

RLSNATIVE ejerce la estrategia RowLevelSecurityNative (ADR-0012, F5-2): el aislamiento lo FUERZA el motor vía CREATE POLICY + set_config('app.tenant_id'), no la WHERE-injection del builder (esa es RowLevelSecurityClient, exerciser TENANT). La distinción es observable: bajo Native el builder NO inyecta `WHERE tenant_id = ?` — un `SELECT * FROM rls_native_orders` plano devuelve sólo las filas del tenant porque la policy del motor las filtra. Es PG-only:

  • En Postgres instala un rol no-superuser + policy y aserta, vía router.Tx (el camino recomendado bajo Native), que cada tenant ve sólo sus filas y que un INSERT respeta el WITH CHECK de la policy.
  • En los otros 5 motores aserta que la estrategia se rechaza con quark.ErrUnsupportedFeature (capacidad desigual ≠ fallo, premisa #4 del HANDOFF) — mirror de rls_native_test.go.

A diferencia del exerciser RLSClient (builder-only sobre el client del harness), éste necesita el DSN del motor (conn): el client del harness corre como superuser y los superusers se saltan RLS incondicionalmente, así que el sujeto del aislamiento debe ser un rol no-superuser distinto, y el DDL de policy exige un admin client con AllowRawQueries. Por eso S5 cambió la firma de Exerciser.Fn para recibir el Conn.

Usa SÓLO router.Tx (no el path implicit-tx de For[T] bajo Native): router.Tx commitea de forma síncrona y libera la conexión y la goroutine awaitDone de la tx, así que es determinista y no deja fugas para el leak-check del harness. El path implicit-tx de For[T] (nativeRLSExecutor con context.AfterFunc) queda cubierto por rls_native_postgres_test.go; aquí seguimos el camino que el propio rls_native.go marca como recomendado para cualquier operación no trivial.

View Source
var SCHEMAPERTENANT = Exerciser{Name: "tenant-schema-per", Fn: runSchemaPerTenant}

SCHEMAPERTENANT ejerce la estrategia SchemaPerTenant (ADR-0007): una base, un schema por tenant; For[T] bajo el router fija q.schema = tenantID y todo el SQL sale schema-qualified. Se asertan las dos garantías:

  1. Aislamiento por schema — cada tenant ve sólo las filas de SU schema (tablas físicamente distintas dentro de la misma base).
  2. La qualificación llega al SQL EMITIDO, incluida la regresión BB-8 (los write-paths construían BaseQuery internos que perdían q.schema y los INSERT caían al schema default): se inspecciona rec.Statements() y se exige que el INSERT mencione el schema del tenant.

Sólo corre el path funcional donde el motor tiene schemas reales (FeatSchemaPerTenant: PG y MSSQL — fuente docs/playbooks/tenant.md); en el resto salta limpio (Quark NO gatea esta estrategia con ErrUnsupportedFeature, así que no hay error que asertar — ver el comment en capability.go). El onboarding (CREATE SCHEMA + migrar la tabla al schema) es responsabilidad del caller per el playbook ("SchemaPerTenant no auto-crea schema"): aquí el admin crea los schemas y un client efímero con search_path=<schema> migra dentro de cada uno — en MSSQL no existe el equivalente de search_path en DSN, así que su mecanismo de migrate-into-schema queda TODO (error ruidoso, no skip silencioso).

View Source
var SECURITY = Exerciser{Name: "security", Fn: func(ctx context.Context, client *quark.Client, rec *recorder.Recorder, _ Conn) error {
	rec.Note(QF("For"), QM("Where"), QM("WhereJSON"), QM("Join"))

	hostile := []string{
		`id; DROP TABLE accounts;--`,
		`id) OR 1=1 --`,
		`name'`,
		`1=1`,
		`id'); DELETE FROM accounts;--`,
	}
	for _, h := range hostile {
		if _, err := quark.For[domain.Account](ctx, client).Where(h, "=", 1).List(); err == nil {
			return fmt.Errorf("identificador hostil %q NO fue rechazado", h)
		} else if !strings.Contains(strings.ToLower(err.Error()), "identifier") {
			return fmt.Errorf("identificador hostil %q: error inesperado: %v", h, err)
		}
	}

	if _, err := quark.For[domain.Account](ctx, client).OrderBy("name; DROP TABLE--", "ASC").Limit(1).List(); err == nil {
		return fmt.Errorf("OrderBy con columna hostil NO fue rechazado")
	}

	if _, err := quark.For[domain.Account](ctx, client).WhereJSON("settings", `theme'; DROP--`, "=", "x").List(); !errors.Is(err, quark.ErrInvalidJSONPath) {
		return fmt.Errorf("JSON-path hostil: esperaba ErrInvalidJSONPath, got %v", err)
	}

	if _, err := quark.For[domain.Task](ctx, client).Join("projects").On(`id; DROP TABLE projects`, "=", "x").List(); !errors.Is(err, quark.ErrInvalidJoin) {
		return fmt.Errorf("JOIN ON hostil: esperaba ErrInvalidJoin, got %v", err)
	}

	return nil
}}

SECURITY ejerce el SQLGuard: identificadores, JSON-path y JOIN-ON hostiles deben rechazarse ANTES de tocar la BD. Verifica que la inyección se ataja (err != nil, query no ejecutada) y, donde Quark envuelve el sentinel con %w, que errors.Is lo alcanza.

View Source
var TENANT = Exerciser{Name: "tenant", Fn: runTenant}

TENANT ejerce la modalidad RowLevelSecurityClient de multi-tenancy (ADR-0007): inyección de `WHERE tenant_id = ?` en el builder, disponible en los 6 motores. Asierta la garantía de seguridad crítica — aislamiento cross-tenant — y las trampas que el playbook marca: la propagación del predicado a los Or-groups (regresión del P0-1) y que el aislamiento sólo aplica A TRAVÉS del router (una query con el client base, igual que `client.Raw()`/`Exec()`, lo evita).

Es builder-only a propósito (sin SQL raw): así corre portable en los 6 motores sin tropezar con el case de identificadores de Oracle.

Las otras 3 estrategias necesitan fixtures más pesados y llegan en PRs propios (ver examples/superapp/HANDOFF.md): RowLevelSecurityNative (PG-only, requiere un rol no-superuser + CREATE POLICY), SchemaPerTenant (PG/MSSQL, CREATE SCHEMA) y DatabasePerTenant (factory de *Client por tenant con DSN propio).

View Source
var TX = Exerciser{Name: "tx", Fn: func(ctx context.Context, client *quark.Client, rec *recorder.Recorder, _ Conn) error {
	rec.Note(CM("Tx"), QF("ForTx"))
	n := atomic.AddInt64(&txSeq, 1)

	email := fmt.Sprintf("tx%d@superapp.test", n)
	var accID int64
	err := client.Tx(rec.Mark(ctx, CM("Tx")), func(tx *quark.Tx) error {
		a := &domain.Account{Email: email, Name: "tx", Role: "member", Active: true}
		if err := quark.ForTx[domain.Account](ctx, tx).Create(a); err != nil {
			return err
		}
		accID = a.ID
		p := &domain.Project{OwnerID: a.ID, Name: "tx-proj", Status: "active"}
		return quark.ForTx[domain.Project](ctx, tx).Create(p)
	})
	if err != nil {
		return fmt.Errorf("commit: %w", err)
	}
	if c, err := quark.For[domain.Account](ctx, client).Where("email", "=", email).Count(); err != nil || c != 1 {
		return fmt.Errorf("commit no persistió account (count=%d err=%v)", c, err)
	}
	if c, err := quark.For[domain.Project](ctx, client).Where("owner_id", "=", accID).Count(); err != nil || c != 1 {
		return fmt.Errorf("commit no persistió project (count=%d err=%v)", c, err)
	}

	rbEmail := fmt.Sprintf("txrb%d@superapp.test", n)
	sentinel := errors.New("rollback intencional")
	err = client.Tx(ctx, func(tx *quark.Tx) error {
		a := &domain.Account{Email: rbEmail, Name: "rb", Role: "member", Active: true}
		if cerr := quark.ForTx[domain.Account](ctx, tx).Create(a); cerr != nil {
			return cerr
		}
		return sentinel
	})
	if !errors.Is(err, sentinel) {
		return fmt.Errorf("tx con error no propagó el sentinel: %v", err)
	}
	if c, err := quark.For[domain.Account](ctx, client).Where("email", "=", rbEmail).Count(); err != nil || c != 0 {
		return fmt.Errorf("rollback no revirtió (count=%d err=%v)", c, err)
	}
	return nil
}}

TX ejerce transacciones: un commit multi-entidad atómico (account + project) y un rollback (la closure devuelve error → nada persiste). Marca Client.Tx y ForTx.

Functions

func CM

func CM(method string) string

CM es la key de un método de *Client.

func Coverage

func Coverage(results map[control.Engine]EngineResult) control.Invoked

Coverage pliega la cobertura de todos los motores en un control.Invoked, listo para control.Manifest.Reconcile.

func QF

func QF(name string) string

QF es la key de una func/tipo/const a nivel paquete (For, ForTx, New, NewTenantRouter, RowLevelSecurityClient…).

func QM

func QM(method string) string

QM es la key de manifiesto de un método de *Query[T]: QM("Create") → "github.com/jcsvwinston/quark.(*Query[T]).Create".

func Run

func Run(conns map[control.Engine]engine.Conn, tol int, exercisers []Exerciser) map[control.Engine]EngineResult

Run corre los exercisers contra cada motor vía engine.Run (lifecycle + anti-fugas), instalando un recorder por motor y migrando el dominio primero.

func TRM

func TRM(method string) string

TRM es la key de un método de *TenantRouter: TRM("Tx") → "github.com/jcsvwinston/quark.(*TenantRouter).Tx".

Types

type Conn

type Conn = engine.Conn

Conn re-exporta engine.Conn para que los exercisers que necesitan el driver/DSN del motor lo reciban sin importar el paquete engine. Lo usan los que abren clientes propios además del client del harness: RLSNative deriva un rol no-superuser y aplica CREATE POLICY; DBPerTenant abre un *Client por tenant. El resto lo ignora (_ Conn).

type EngineResult

type EngineResult struct {
	Err  error              // primer error funcional (o de open/migrate)
	Leak engine.LeakReport  // del chequeo de fugas de engine.Run
	Rec  *recorder.Recorder // recorder del motor (cobertura + SQL capturado)
}

EngineResult reúne el resultado por motor.

type Exerciser

type Exerciser struct {
	Name string
	Fn   func(ctx context.Context, client *quark.Client, rec *recorder.Recorder, conn Conn) error
}

Exerciser ejerce un área de la API: marca los símbolos que toca (rec.Mark / rec.Note) y aserta el resultado funcional, devolviendo error al primer fallo. El Conn da acceso al driver/DSN del motor a los exercisers que abren sus propios clientes; los que sólo usan el client del harness lo ignoran.

Jump to

Keyboard shortcuts

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