Documentation
¶
Overview ¶
Package kcleval reads values out of a forge project's KCL from Go, so a test can assert against what the project DECLARES instead of restating it.
Why a Go package and not `exec.Command("kcl", ...)` ¶
A forge project's KCL resolves `import forge` from the forge binary, not from a kcl.mod dependency or a copy on disk. A plain `kcl run` therefore cannot evaluate a project file at all, and the workaround — make forge materialize its module into a cache directory, dig the path out, pass `-E forge=<dir>` — reconstructs forge's private render setup inside a test. It breaks whenever the cache layout, the module name or the plugin namespace changes, and it breaks in a consumer's CI with an error about KCL that names nothing leading back to the cause. It also needs `kcl` on PATH, which forge itself has not needed since it embedded the runtime.
This package calls `forge kcl eval` instead. One consequence is worth being explicit about: the values come from the forge binary the project is pinned to, which is the same binary `forge env deploy` renders with. A test and a deploy therefore cannot disagree about what the KCL says — the property that makes such a test worth writing.
Why it drives the binary rather than linking the evaluator ¶
Linking forge's evaluator into a consumer's test binary would put the KCL runtime there too, and a hard dependency from the test binary onto a specific forge VERSION through go.mod. That is wrong for the job. A project pins its forge in CI with `go install .../cmd/forge@vX.Y.Z`; the value a test reads should come from THAT forge, not from whatever version the test binary happened to compile against. Driving the binary keeps the pin in one place and keeps the test binary light.
Typical use ¶
func TestDaemonPlacementCoversEveryEnv(t *testing.T) {
var placement map[string]struct {
ClientID string `json:"client_id"`
Context string `json:"context"`
}
kcleval.MustSelect(t, "deploy/kcl/lib/daemon_placement.k", "daemon_placement", &placement)
// assert against placement
}
Index ¶
- Constants
- Variables
- func Eval(ctx context.Context, file string, out any, opts Options) error
- func MustSelect(t testing.TB, file, path string, out any)
- func MustSelectString(t testing.TB, file, path string) string
- func Select(ctx context.Context, file, path string, out any, opts Options) error
- type Options
Constants ¶
const DefaultTimeout = 2 * time.Minute
DefaultTimeout bounds one evaluation. A KCL evaluation of a library file is well under a second; the bound exists so a test that somehow blocks reports a timeout naming this call rather than hanging until the whole package's deadline expires, which says nothing about where it stopped.
Variables ¶
var ErrForgeNotFound = errors.New("no forge binary")
ErrForgeNotFound is returned when no forge binary can be run. It is a distinct error so a test can choose to skip on it — a developer laptop without forge installed — while CI treats it as the failure it is.
Functions ¶
func MustSelect ¶
MustSelect is Select for a test: it fails the test on any error and needs no context or options.
It is the form nearly every call site wants, and it exists so that reading a declared value is a one-line assertion rather than five lines of plumbing — the friction that made restating the value in Go the easier choice, which is how a test and a deploy drift apart in the first place.
func MustSelectString ¶
MustSelectString is MustSelect for a string field, returned rather than written through a pointer.
func Select ¶
Select evaluates file and decodes the value at path into out.
file is relative to the project root (or absolute inside it). path is a dotted field path, kcl-style, where a numeric segment indexes a list. out is any encoding/json target: a *string and a *int work for scalars, and a struct or map for a composite.
A path the document does not have is an error, never a zero value. That distinction is the point: a selector typo that returned "" would be read as a legitimately empty declaration, and a test asserting on it would pass while checking nothing.
Types ¶
type Options ¶
type Options struct {
// ProjectDir is the project root (or any directory inside it — forge
// walks up to the forge.yaml). Empty means the working directory.
ProjectDir string
// Binary is the forge executable to drive. Empty means "forge" on PATH.
//
// Set it when a test must pin an exact build. Nothing here guesses a
// path: a test that silently drove a different forge than CI does would
// assert against a different module version, which is the whole failure
// this package exists to prevent.
Binary string
// Options are `key=value` KCL option bindings, passed as -D.
Options []string
// Timeout bounds the evaluation. Zero means DefaultTimeout.
Timeout time.Duration
}
Options configures an evaluation. The zero value is valid: the project is found by walking up from the working directory, the forge binary is found on PATH, and DefaultTimeout applies.