pkg/models/mock_payload.go
Rama main · réplica de lectura del repositorio original.
package models
import (
"context"
"errors"
"fmt"
"maps"
"strings"
"unsafe"
)
// A replay reads a test set's mocks before its first test: it sorts them into
// the agent's pools, works out what each test owns and how the reports name
// each mock, and sends them to the agent, which parks the per-test ones on its
// own disk (the agent's disk store) and reads back only what a test's window
// needs. Holding the whole set in memory on the way there is what ran the CLI
// out of memory on a large recording: the decoded set is about the size of
// its mock file, and the parse that builds it about doubles that at its peak.
//
// So a read of a yaml or json mock file leaves every mock's payload in the
// file. The mock it keeps (LeavePayloadOnDisk) is a small entry: its name,
// kind, lifetime and test-mode info, its connection, its metadata less the
// operation it records (the one value that grows with the call), its request
// and response times, its async block, and where in the file its document is.
// What a replay asks of a mock before it sends it, and of the payload only
// (the reports' summary and target, the registered predicates, a MySQL
// command's place in a statement's lifecycle), is answered while the payload
// is still in memory and kept with the entry. The payload is read back, one
// mock at a time, only to be sent (WithPayload), and dropped once it is.
// PayloadSource is a mock file a read left its mocks' payloads in.
type PayloadSource interface {
// LoadPayload reads back the mock at at, as the read that left its
// payload decoded it.
LoadPayload(ctx context.Context, at PayloadAt) (*Mock, error)
// SetPayloadMutator has mutate change every payload read back from
// then on (Keploy Enterprise decrypts it), before WithPayload returns
// it.
SetPayloadMutator(mutate PayloadMutator)
// PayloadMutator is the mutator set, nil when none is.
PayloadMutator() PayloadMutator
}
// PayloadMutator changes one mock's payload before the mock is sent to the
// agent: a replay's hooks decrypt it. It is handed the mock whole, and may
// change its payload only (MutatePayload).
type PayloadMutator func(ctx context.Context, m *Mock) error
// PayloadAt is where a mock's document is in its file: the Length bytes from
// Offset, which decode to the mock named Name.
type PayloadAt struct {
Offset int64
Length int
Name string
}
// ErrPayloadFileReplaced is the error of reading back a payload whose mock
// file was replaced or removed since its mocks were read: the replay rewrites
// a test set's mock file once the set has run (it prunes the mocks no test
// consumed, folds learned noise in), so the mock is of a set that is over.
var ErrPayloadFileReplaced = errors.New("the mock file was rewritten after its mocks were read")
// ErrPayloadMutatorChangedMock is the error of a PayloadMutator that changed
// more than a mock's payload: what decides the mock's pool and its place in it
// was read before the payload was, and the agent is told the pools' sizes
// before it is sent any mock.
var ErrPayloadMutatorChangedMock = errors.New("a mock payload hook changed what decides the mock's pool")
// MockPayloadRefusal is the error of a PayloadMutator that failed on a mock:
// the replay's hooks refused to hand the mock to the agent (Keploy Enterprise
// refuses a sealed value its key service did not open). Err is the hook's
// reason. The replay fails the mock's test set, unrun, with it, and goes on
// to the next set.
type MockPayloadRefusal struct {
Mock string
Err error
}
func (e *MockPayloadRefusal) Error() string {
return fmt.Sprintf("the mock payload hook failed on mock %q: %v", e.Mock, e.Err)
}
func (e *MockPayloadRefusal) Unwrap() error { return e.Err }
// payloadOnDisk is where a mock's payload was left, and what the replay asks
// of the payload, answered before it was.
type payloadOnDisk struct {
src PayloadSource
at PayloadAt
// summary and target are MockSummaryFromSpec's and MockTargetFromSpec's,
// when no longer than maxKeptDisplay. A longer one (a Mongo mock's carries
// its whole recorded command) is not kept (longSummary, longTarget): it
// is worked out again from the payload when it is asked for, which only
// a report naming the mock does.
summary, target string
longSummary, longTarget bool
// fingerprint is RequestFingerprint's.
fingerprint string
// matched has the bit of each registry in onDiskPredicates that matched
// the mock.
matched uint8
// mysqlStatementLifecycle and mysqlSessionReusable are
// IsMySQLStatementLifecycle's and mysqlIsSessionReusableCommand's.
mysqlStatementLifecycle bool
mysqlSessionReusable bool
// fault is a copy of the mock's Spec.Fault (FaultOf), nil for none: what
// a replay's summary names when the mock was served (FaultServed).
fault *Fault
}
// onDiskPredicates is every registry whose predicates read a mock's payload;
// a mock whose payload is on disk keeps each one's answer by its index.
var onDiskPredicates = [...]*mockPredicates{&carryOverHooks, &stagedHooks, &releaseHooks, &answerPlacedHooks, &idleHooks, &sessionReusableHooks}
// predicateBit is r's bit in payloadOnDisk.matched.
func predicateBit(r *mockPredicates) uint8 {
for i, p := range onDiskPredicates {
if p == r {
return 1 << i
}
}
panic("models: a mock predicate registry that is not in onDiskPredicates")
}
// LeavePayloadOnDisk drops m's payload from memory: src reads it back, from
// the document at at, when the mock is sent (WithPayload). m keeps what a
// replay reads of it before then (see above). It is for a mock just decoded
// whose lifetime is derived (DeriveLifetime), and nothing else holds its
// payload.
func (m *Mock) LeavePayloadOnDisk(src PayloadSource, at PayloadAt) {
d := &payloadOnDisk{
src: src,
at: at,
mysqlStatementLifecycle: m.IsMySQLStatementLifecycle(),
mysqlSessionReusable: mysqlIsSessionReusableCommand(m),
}
d.fingerprint = requestFingerprint(m)
d.summary, d.longSummary = keptDisplay(MockSummaryFromSpec(m))
d.target, d.longTarget = keptDisplay(MockTargetFromSpec(m))
if f := m.Spec.Fault; f != nil {
kept := *f
kept.Observed = strings.Clone(f.Observed)
d.fault = &kept
}
for i, r := range onDiskPredicates {
if r.match(m) {
d.matched |= 1 << i
}
}
metadata := m.Spec.Metadata
delete(metadata, "operation")
m.Spec = MockSpec{
Metadata: metadata,
Created: m.Spec.Created,
ReqTimestampMock: m.Spec.ReqTimestampMock,
ResTimestampMock: m.Spec.ResTimestampMock,
Async: m.Spec.Async,
}
m.Noise = nil
m.onDisk = d
}
// maxKeptDisplay is the longest summary or target (MockSummaryFromSpec,
// MockTargetFromSpec) a mock whose payload is on disk keeps.
const maxKeptDisplay = 256
// keptDisplay is s as a mock whose payload is on disk keeps it: a copy of its
// own, which holds nothing of the payload, or none when s is longer than
// maxKeptDisplay (long).
func keptDisplay(s string) (kept string, long bool) {
if len(s) > maxKeptDisplay {
return "", true
}
return strings.Clone(s), false
}
// display works out of (MockSummaryFromSpec or MockTargetFromSpec) for m, a
// mock whose payload is on disk, from its payload read back as recorded:
// the source's mutator does not change it. A payload that cannot be read
// back (its file was rewritten once its set ran) has its kind's own, as a mock
// that carries nothing identifying does.
func (d *payloadOnDisk) display(m *Mock, of func(*Mock) string, unknown string) string {
read, err := d.src.LoadPayload(context.Background(), d.at)
if err != nil {
return unknown
}
c := *m
c.onDisk = nil
c.Spec = read.Spec
return of(&c)
}
// IndexEntryFrom is a copy of m, a mock whose payload is on disk
// (LeavePayloadOnDisk), whose payload src reads back from the same place in
// its file: the entry a read of a mock file left, reused for a later read of a
// file of the same content (mockdb's index cache). The copy is m as that read
// left it, unpooled, and shares nothing a replay changes in place with m: its
// metadata, async block and origin are its own. It is nil when m's payload is
// not on disk.
func (m *Mock) IndexEntryFrom(src PayloadSource) *Mock {
if !m.PayloadOnDisk() {
return nil
}
c := *m
c.pooled = false
c.responseHydrator = nil
d := *m.onDisk
d.src = src
c.onDisk = &d
if m.Spec.Metadata != nil {
c.Spec.Metadata = maps.Clone(m.Spec.Metadata)
}
if m.Spec.Async != nil {
a := *m.Spec.Async
c.Spec.Async = &a
}
c.Origin = m.Origin.clone()
return &c
}
// IndexEntryBytes estimates the heap m, a mock whose payload is on disk, holds
// of its own: the Mock and what LeavePayloadOnDisk kept of it (its strings,
// metadata, async block and origin, and the payload's place and the answers
// read from it). It is what a kept entry costs a cache of entries, 0 when m's
// payload is not on disk.
func (m *Mock) IndexEntryBytes() int64 {
if !m.PayloadOnDisk() {
return 0
}
// A string's bytes are allocated in a size class, a little more than
// its length; a map takes its header and groups of eight slots, each
// slot a key's and a value's string header, filled to seven.
const slot = 2 * int(unsafe.Sizeof(""))
str := func(s string) int {
if s == "" {
return 0
}
return len(s) + 8
}
n := int(unsafe.Sizeof(*m)) + int(unsafe.Sizeof(*m.onDisk))
n += str(string(m.Version)) + str(m.Name) + str(string(m.Kind)) + str(m.ConnectionID) + str(m.Start) + str(m.StartRef)
if md := m.Spec.Metadata; md != nil {
n += 48 + max(1, (len(md)+6)/7)*(8+8*slot)
for k, v := range md {
n += str(k) + str(v)
}
}
if a := m.Spec.Async; a != nil {
n += int(unsafe.Sizeof(*a)) + str(a.Lane) + str(a.AnchorAfter)
}
if o := m.Origin; o != nil {
n += int(unsafe.Sizeof(*o)) + str(o.Clock) + str(o.Comm) + str(o.Exe)
}
d := m.onDisk
// d.at.Name is m.Name's string, not a copy.
n += str(d.summary) + str(d.target) + str(d.fingerprint)
if f := d.fault; f != nil {
n += int(unsafe.Sizeof(*f)) + str(f.Observed)
}
return int64(n)
}
// PayloadOnDisk reports whether m's payload was left on disk
// (LeavePayloadOnDisk): m is not the mock as recorded until WithPayload reads
// it back.
func (m *Mock) PayloadOnDisk() bool { return m != nil && m.onDisk != nil }
// WithPayload is the mock to send: m itself when its payload is in memory,
// else a copy of m with its payload read back from its file and changed by the
// source's mutator. The copy is to be sent and dropped: m keeps its payload on
// disk, so holding m holds no payload.
func (m *Mock) WithPayload(ctx context.Context) (*Mock, error) {
if !m.PayloadOnDisk() {
return m, nil
}
d := m.onDisk
read, err := d.src.LoadPayload(ctx, d.at)
if err != nil {
return nil, err
}
c := m.ShallowCopy()
c.onDisk = nil
c.Spec = read.Spec
c.Noise = read.Noise
if err := MutatePayload(ctx, c, d.src.PayloadMutator()); err != nil {
return nil, err
}
return c, nil
}
// MutatePayload has mutate change m's payload, and fails when mutate fails
// (MockPayloadRefusal) or changed anything else that decides m's pool or its
// place in it (ErrPayloadMutatorChangedMock). A nil mutate changes nothing.
func MutatePayload(ctx context.Context, m *Mock, mutate PayloadMutator) error {
if mutate == nil {
return nil
}
before := poolKeyOf(m)
if err := mutate(ctx, m); err != nil {
return &MockPayloadRefusal{Mock: before.name, Err: err}
}
if after := poolKeyOf(m); after != before {
return fmt.Errorf("%w: mock %q, before %+v, after %+v", ErrPayloadMutatorChangedMock, before.name, before, after)
}
return nil
}
// poolKey is what of a mock decides its pool and its place in it.
type poolKey struct {
name, kind, typ, lifetime string
info TestModeInfo
req, res int64
}
func poolKeyOf(m *Mock) poolKey {
return poolKey{
name: m.Name,
kind: string(m.Kind),
typ: m.Spec.Metadata["type"],
lifetime: m.Spec.Metadata[MetadataLifetimeKey],
info: m.TestModeInfo,
req: m.Spec.ReqTimestampMock.UnixNano(),
res: m.Spec.ResTimestampMock.UnixNano(),
}
}