From f0cd02bb6153320208a37c0f4d48c6a0d7af3657 Mon Sep 17 00:00:00 2001 From: Marcel van Lohuizen Date: Thu, 2 May 2024 19:50:53 +0200 Subject: [PATCH] cuecontext: add options to set version and debug flags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This changes Runtime.SetSettings back to SetVersion and adds AddDebugOptions. SetSettings has now served its purpose of identifying all call sites where these options need to be set. :) Note that we enable the API before it is fully working. But it probably works well enough for many applications. For open options search for the following across the repo: - '-- diff(/.*)?todo/p? --' in txtar files - todo_* fields in table-driven tests - TODO_(V3|Sharing|NoSharing) function calls in table-driven tests - TODO(evalv3) around matrix tests (like in trim_test.go) Removed use of "stderr 'str'" in txtar, as it does not support CUE_UPDATE=1. Closes #3060 Signed-off-by: Marcel van Lohuizen Change-Id: Ib3970d867363711f461c8c303abfdb0402706fff Reviewed-on: https://review.gerrithub.io/c/cue-lang/cue/+/1194231 TryBot-Result: CUEcueckoo Reviewed-by: Daniel Martí Unity-Result: CUE porcuepine --- cmd/cue/cmd/root.go | 3 +- .../testdata/script/experiment_unknown.txtar | 4 +- cue/cuecontext/cuecontext.go | 59 +++++++++++++++++++ cue/matrix_test.go | 3 +- internal/core/runtime/runtime.go | 11 +++- internal/cuetdtest/matrix.go | 3 +- internal/envflag/flag.go | 53 ++++++++++++----- internal/envflag/flag_test.go | 31 +++++++++- 8 files changed, 143 insertions(+), 24 deletions(-) diff --git a/cmd/cue/cmd/root.go b/cmd/cue/cmd/root.go index d5809c21acc..9b1928458a3 100644 --- a/cmd/cue/cmd/root.go +++ b/cmd/cue/cmd/root.go @@ -116,7 +116,8 @@ func mkRunE(c *Command, f runFunction) func(*cobra.Command, []string) error { // in a non-tooling context. if cueexperiment.Flags.EvalV3 { const dev = internal.DevVersion - (*cueruntime.Runtime)(c.ctx).SetSettings(internal.EvaluatorVersion(dev), cuedebug.Flags) + (*cueruntime.Runtime)(c.ctx).SetVersion(internal.EvaluatorVersion(dev)) + (*cueruntime.Runtime)(c.ctx).SetDebugOptions(&cuedebug.Flags) } err := f(c, args) diff --git a/cmd/cue/cmd/testdata/script/experiment_unknown.txtar b/cmd/cue/cmd/testdata/script/experiment_unknown.txtar index 2015f0917ac..bdde13c808e 100644 --- a/cmd/cue/cmd/testdata/script/experiment_unknown.txtar +++ b/cmd/cue/cmd/testdata/script/experiment_unknown.txtar @@ -1,3 +1,5 @@ env CUE_EXPERIMENT=xxx ! exec cue eval something -stderr 'unknown CUE_EXPERIMENT xxx' +cmp stderr errout +-- errout -- +cannot parse CUE_EXPERIMENT: unknown xxx diff --git a/cue/cuecontext/cuecontext.go b/cue/cuecontext/cuecontext.go index 1d25463a7db..60fb2a9e2ea 100644 --- a/cue/cuecontext/cuecontext.go +++ b/cue/cuecontext/cuecontext.go @@ -15,8 +15,13 @@ package cuecontext import ( + "fmt" + "cuelang.org/go/cue" + "cuelang.org/go/internal" "cuelang.org/go/internal/core/runtime" + "cuelang.org/go/internal/cuedebug" + "cuelang.org/go/internal/envflag" _ "cuelang.org/go/pkg" ) @@ -26,9 +31,20 @@ type Option struct { apply func(r *runtime.Runtime) } +// defaultFlags defines the debug flags that are set by default. +var defaultFlags cuedebug.Config + +func init() { + if err := envflag.Parse(&defaultFlags, ""); err != nil { + panic(err) + } +} + // New creates a new Context. func New(options ...Option) *cue.Context { r := runtime.New() + // Ensure default behavior if the flags are not set explicitly. + r.SetDebugOptions(&defaultFlags) for _, o := range options { o.apply(r) } @@ -46,3 +62,46 @@ func Interpreter(i ExternInterpreter) Option { r.SetInterpreter(i) }} } + +type EvalVersion = internal.EvaluatorVersion + +const ( + // EvalDefault is the latest stable version of the evaluator. + EvalDefault EvalVersion = EvalV2 + + // EvalExperiment refers to the latest unstable version of the evaluator. + // Note that this version may change without notice. + EvalExperiment EvalVersion = EvalV3 + + // EvalV2 is the currently latest stable version of the evaluator. + // It was introduced in CUE version 0.3 and is being maintained until 2024. + EvalV2 EvalVersion = internal.DefaultVersion + + // EvalV3 is the currently experimental version of the evaluator. + // It was introduced in 2024 and brought a new disjunction algorithm, + // a new closedness algorithm, a new core scheduler, and adds performance + // enhancements like structure sharing. + EvalV3 EvalVersion = internal.DevVersion +) + +// EvaluatorVersion indicates which version of the evaluator to use. Currently +// only experimental versions can be selected as an alternative. +func EvaluatorVersion(v EvalVersion) Option { + return Option{func(r *runtime.Runtime) { + r.SetVersion(v) + }} +} + +// CUE_DEBUG takes a string with the same contents as CUE_DEBUG and configures +// the context with the relevant debug options. It panics for unknown or +// malformed options. +func CUE_DEBUG(s string) Option { + var c cuedebug.Config + if err := envflag.Parse(&c, s); err != nil { + panic(fmt.Errorf("cuecontext.CUE_DEBUG: %v", err)) + } + + return Option{func(r *runtime.Runtime) { + r.SetDebugOptions(&c) + }} +} diff --git a/cue/matrix_test.go b/cue/matrix_test.go index b97afe730eb..58945301de5 100644 --- a/cue/matrix_test.go +++ b/cue/matrix_test.go @@ -35,7 +35,8 @@ func (c *evalConfig) runtime() *Runtime { } func (c *evalConfig) updateRuntime(r *runtime.Runtime) { - r.SetSettings(c.version, c.flags) + r.SetVersion(c.version) + r.SetDebugOptions(&c.flags) } func runMatrix(t *testing.T, name string, f func(t *testing.T, c *evalConfig)) { diff --git a/internal/core/runtime/runtime.go b/internal/core/runtime/runtime.go index e56c3c70190..5b54015f022 100644 --- a/internal/core/runtime/runtime.go +++ b/internal/core/runtime/runtime.go @@ -64,11 +64,16 @@ func NewWithSettings(v internal.EvaluatorVersion, flags cuedebug.Config) *Runtim return r } -// SetSettings sets the version to use for the Runtime. This should only be set +// SetVersion sets the version to use for the Runtime. This should only be set // before first use. -func (r *Runtime) SetSettings(v internal.EvaluatorVersion, flags cuedebug.Config) { +func (r *Runtime) SetVersion(v internal.EvaluatorVersion) { r.version = v - r.flags = flags +} + +// SetDebugOptions sets the debug flags to use for the Runtime. This should only +// be set before first use. +func (r *Runtime) SetDebugOptions(flags *cuedebug.Config) { + r.flags = *flags } // IsInitialized reports whether the runtime has been initialized. diff --git a/internal/cuetdtest/matrix.go b/internal/cuetdtest/matrix.go index ac30394384a..4c8583371d7 100644 --- a/internal/cuetdtest/matrix.go +++ b/internal/cuetdtest/matrix.go @@ -43,7 +43,8 @@ func (t *M) Runtime() *runtime.Runtime { } func (t *M) UpdateRuntime(r *runtime.Runtime) { - r.SetSettings(t.version, t.flags) + r.SetVersion(t.version) + r.SetDebugOptions(&t.flags) } const DefaultVersion = "v2" diff --git a/internal/envflag/flag.go b/internal/envflag/flag.go index adcea16100a..4c640ffafd2 100644 --- a/internal/envflag/flag.go +++ b/internal/envflag/flag.go @@ -1,6 +1,7 @@ package envflag import ( + "errors" "fmt" "os" "reflect" @@ -8,20 +9,29 @@ import ( "strings" ) -// Init initializes the fields in flags from the attached struct field tags -// as well as the contents of the given environment variable. +// Init uses Parse with the contents of the given environment variable as input. +func Init[T any](flags *T, envVar string) error { + err := Parse(flags, os.Getenv(envVar)) + if err != nil { + return fmt.Errorf("cannot parse %s: %w", envVar, err) + } + return nil +} + +// Parse initializes the fields in flags from the attached struct field tags as +// well as the contents of the given string. // // The struct field tag may contain a default value other than the zero value, // such as `envflag:"default:true"` to set a boolean field to true by default. // -// The environment variable may contain a comma-separated list of name=value -// pairs values representing the boolean fields in the struct type T. -// If the value is omitted entirely, the value is assumed to be name=true. +// The string may contain a comma-separated list of name=value pairs values +// representing the boolean fields in the struct type T. If the value is omitted +// entirely, the value is assumed to be name=true. // -// Names are treated case insensitively. -// Value strings are parsed as Go booleans via [strconv.ParseBool], -// meaning that they accept "true" and "false" but also the shorter "1" and "0". -func Init[T any](flags *T, envVar string) error { +// Names are treated case insensitively. Value strings are parsed as Go booleans +// via [strconv.ParseBool], meaning that they accept "true" and "false" but also +// the shorter "1" and "0". +func Parse[T any](flags *T, env string) error { // Collect the field indices and set the default values. indexByName := make(map[string]int) fv := reflect.ValueOf(flags).Elem() @@ -31,6 +41,7 @@ func Init[T any](flags *T, envVar string) error { defaultValue := false if tagStr, ok := field.Tag.Lookup("envflag"); ok { defaultStr, ok := strings.CutPrefix(tagStr, "default:") + // TODO: consider panicking for these error types. if !ok { return fmt.Errorf("expected tag like `envflag:\"default:true\"`: %s", tagStr) } @@ -44,11 +55,10 @@ func Init[T any](flags *T, envVar string) error { indexByName[strings.ToLower(field.Name)] = i } - // Parse the env value to set the fields. - env := os.Getenv(envVar) if env == "" { return nil } + var errs []error for _, elem := range strings.Split(env, ",") { name, valueStr, ok := strings.Cut(elem, "=") // "somename" is short for "somename=true" or "somename=1". @@ -56,15 +66,30 @@ func Init[T any](flags *T, envVar string) error { if ok { v, err := strconv.ParseBool(valueStr) if err != nil { - return fmt.Errorf("invalid bool value for %s: %v", name, err) + // Invalid format, return an error immediately. + return invalidError{ + fmt.Errorf("invalid bool value for %s: %v", name, err), + } } value = v } index, ok := indexByName[name] if !ok { - return fmt.Errorf("unknown %s %s", envVar, elem) + // Unknown option, proceed processing options as long as the format + // is valid. + errs = append(errs, fmt.Errorf("unknown %s", elem)) + continue } fv.Field(index).SetBool(value) } - return nil + return errors.Join(errs...) +} + +// An InvalidError indicates a malformed input string. +var InvalidError = errors.New("invalid value") + +type invalidError struct{ error } + +func (invalidError) Is(err error) bool { + return err == InvalidError } diff --git a/internal/envflag/flag_test.go b/internal/envflag/flag_test.go index 6d78be88ee9..378a980a086 100644 --- a/internal/envflag/flag_test.go +++ b/internal/envflag/flag_test.go @@ -23,11 +23,21 @@ func success[T comparable](want T) func(t *testing.T) { } } -func failure[T comparable](wantError string) func(t *testing.T) { +func failure[T comparable](want T, wantError string) func(t *testing.T) { return func(t *testing.T) { var x T err := Init(&x, "TEST_VAR") qt.Assert(t, qt.ErrorMatches(err, wantError)) + qt.Assert(t, qt.Equals(x, want)) + } +} + +func invalid[T comparable](want T) func(t *testing.T) { + return func(t *testing.T) { + var x T + err := Init(&x, "TEST_VAR") + qt.Assert(t, qt.ErrorIs(err, InvalidError)) + qt.Assert(t, qt.Equals(x, want)) } } @@ -44,7 +54,8 @@ var tests = []struct { }, { testName: "Unknown", envVal: "ratchet", - test: failure[testFlags]("unknown TEST_VAR ratchet"), + test: failure[testFlags](testFlags{DefaultTrue: true}, + "cannot parse TEST_VAR: unknown ratchet"), }, { testName: "Set", envVal: "foo", @@ -62,7 +73,10 @@ var tests = []struct { }, { testName: "SetWithUnknown", envVal: "foo,other", - test: failure[testFlags]("unknown TEST_VAR other"), + test: failure[testFlags](testFlags{ + Foo: true, + DefaultTrue: true, + }, "cannot parse TEST_VAR: unknown other"), }, { testName: "TwoFlags", envVal: "barbaz,foo", @@ -83,6 +97,17 @@ var tests = []struct { test: success(testFlags{ DefaultFalse: true, }), +}, { + testName: "MultipleUnknown", + envVal: "other1,other2,foo", + test: failure(testFlags{ + Foo: true, + DefaultTrue: true, + }, "cannot parse TEST_VAR: unknown other1\nunknown other2"), +}, { + testName: "Invalid", + envVal: "foo=2,BarBaz=true", + test: invalid(testFlags{DefaultTrue: true}), }} func TestInit(t *testing.T) {