Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion candy/plugin-command/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ go 1.26.4
require github.com/opencharly/sdk v0.2026234.347

require (
cuelang.org/go v0.16.1 // indirect
cuelang.org/go v0.16.1
github.com/alecthomas/kong v1.15.0 // indirect
github.com/cockroachdb/apd/v3 v3.2.1 // indirect
github.com/emicklei/proto v1.14.3 // indirect
Expand Down
23 changes: 23 additions & 0 deletions candy/plugin-command/params/cue_types_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 45 additions & 0 deletions candy/plugin-command/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,35 @@ func (verb) RunVerb(ctx context.Context, cc kit.CheckContext, op *spec.Op) kit.R
inContainer = false
}

// skip_exit — the author declares that this command reports "the phase ran
// nothing" with the org's skip code (spec/exitcode: "the phase RAN NOTHING —
// neither a pass nor a failure"), so the step is SKIPPED instead of failed.
// The declaration is VALIDATED, not trusted:
//
// - only the org's code is accepted: a step that invented a private code
// would make "3" mean two different things across the org;
// - a declaration that contradicts the step's own exit_status assertion is
// refused rather than silently overriding it — a step cannot both PASS
// and SKIP on one exit code;
// - a backgrounded command has no exit status to interpret, so a skip code
// declared there would be silently inert.
//
// The declaration is checked BEFORE the command runs, so an authoring error
// never runs a probe.
if in.SkipExit != nil {
if *in.SkipExit != sdk.CheckSkippedExitCode {
return kit.Failf("skip_exit=%d is not the org's skip code (%d) — a step declares the org's code, never a private one",
*in.SkipExit, sdk.CheckSkippedExitCode)
}
if op.ExitStatus != nil && *op.ExitStatus == *in.SkipExit {
return kit.Failf("skip_exit and exit_status both declare %d — the step cannot both PASS and SKIP on one exit code",
*in.SkipExit)
}
if in.Background {
return kit.Failf("skip_exit with background: true — a backgrounded command has no exit status to interpret")
}
}

// The step's env: modifier (a base #Op field) is applied to the executed
// command as shell exports. RCA: the field was schema-declared but no executor
// path ever read it — the command ran with the container's default env, so an
Expand Down Expand Up @@ -134,6 +163,22 @@ func (verb) RunVerb(ctx context.Context, cc kit.CheckContext, op *spec.Op) kit.R
elapsed.Round(time.Millisecond), trimPreview(in.Command))
}

// The skip code — the ONE channel a step has for saying "the phase ran
// nothing". A command that exits it neither proved nor disproved its
// assertion, so the step is SKIPPED: it must mint neither a red nor a green.
// It is read BEFORE the exit comparison (an exit the author declared as a
// skip is not a mismatch), and the stdout/stderr matchers do not run for it —
// a step that ran nothing has no output to assert on.
//
// PRECEDENCE, and it is the same one the schema states: the skip wins over
// expect_non_zero, so a step declaring both means "exit this code → skipped;
// any other non-zero → passed" (a wildcard narrowed by an exact exception).
// The exact-code contradiction (skip_exit and exit_status naming the SAME
// code) is refused above, before the command runs.
if in.SkipExit != nil && exitCode == *in.SkipExit {
return kit.Skipf("exit=%d — the step declared this code as 'the phase ran nothing' (skip_exit); it is neither a pass nor a failure", exitCode)
}

// expect_non_zero asserts the command FAILED (any non-zero code) and IGNORES
// exit_status — the two are mutually-exclusive intents (any-non-zero vs
// exact-code). Otherwise assert the exact code (exit_status, default 0).
Expand Down
176 changes: 176 additions & 0 deletions candy/plugin-command/plugin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"testing"
"time"

"github.com/opencharly/sdk"
"github.com/opencharly/sdk/kit"
"github.com/opencharly/spec/spec"
)
Expand Down Expand Up @@ -326,3 +327,178 @@ func TestCommandVerb_StdoutBelowCapUntruncated(t *testing.T) {
t.Errorf("message = %q, want no truncation note for under-cap output", res.Message)
}
}

// --- the skip channel: skip_exit ---------------------------------------------
//
// The channel exists so a command can say "the phase ran nothing" with the org's
// OWN exit code instead of a private one. The org's code is
// sdk.CheckSkippedExitCode (spec/exitcode: "the phase RAN NOTHING — neither a
// pass nor a failure") — it is read from the contract here, never written as a
// literal, because a second statement of the contract is a second contract (R2).

// TestCommandVerb_SkipExitIsSkipped proves the one thing the channel exists for:
// a command that exits the org's skip code, on a step that DECLARES it, is
// SKIPPED — neither a pass nor a failure.
func TestCommandVerb_SkipExitIsSkipped(t *testing.T) {
code := sdk.CheckSkippedExitCode
res := runCommandVerb(code, map[string]any{"command": "exit 3", "skip_exit": code}, nil)
if res.Status != kit.StatusSkip {
t.Fatalf("status = %v, want skip — a step that declared skip_exit=%d ran nothing, so it is neither a pass nor a failure: %s",
res.Status, code, res.Message)
}
if !strings.Contains(res.Message, fmt.Sprintf("exit=%d", code)) {
t.Errorf("message = %q, want it to name the exit code the step ran out with", res.Message)
}
}

// TestCommandVerb_SkipExitIsOptIn is the NEGATIVE CONTROL, and the reason the
// channel is opt-in: the SAME exit code on a step that does NOT declare it is
// still a FAILURE. A command's exit code is the author's to interpret; reading
// an undeclared 3 as a skip would turn a real failure into a non-failure.
func TestCommandVerb_SkipExitIsOptIn(t *testing.T) {
res := runCommandVerb(sdk.CheckSkippedExitCode, map[string]any{"command": "exit 3"}, nil)
if res.Status != kit.StatusFail {
t.Fatalf("status = %v, want fail — an undeclared exit code is the author's to read, not the verb's", res.Status)
}
if !strings.Contains(res.Message, fmt.Sprintf("exit=%d", sdk.CheckSkippedExitCode)) {
t.Errorf("message = %q, want the actual exit code", res.Message)
}
}

// TestCommandVerb_SkipExitDoesNotSwallowOtherFailures proves the channel skips
// EXACTLY the declared code: a step that declares skip_exit=3 and whose command
// exits 1 is still a FAILURE.
func TestCommandVerb_SkipExitDoesNotSwallowOtherFailures(t *testing.T) {
code := sdk.CheckSkippedExitCode
res := runCommandVerb(1, map[string]any{"command": "exit 1", "skip_exit": code}, nil)
if res.Status != kit.StatusFail {
t.Fatalf("status = %v, want fail — only the DECLARED code is a skip", res.Status)
}
}

// TestCommandVerb_SkipExitRejectsAPrivateCode proves the declaration is
// validated, not trusted: a step that invents its own skip code is refused with
// a message that names the org's code, so "3" cannot mean two things.
func TestCommandVerb_SkipExitRejectsAPrivateCode(t *testing.T) {
res := runCommandVerb(0, map[string]any{"command": "true", "skip_exit": 7}, nil)
if res.Status != kit.StatusFail {
t.Fatalf("status = %v, want fail — a private skip code is an authoring error, not a channel", res.Status)
}
for _, want := range []string{"skip_exit=7", fmt.Sprintf("%d", sdk.CheckSkippedExitCode)} {
if !strings.Contains(res.Message, want) {
t.Errorf("message = %q, want it to name %q so the cause is readable without guessing", res.Message, want)
}
}
}

// TestCommandVerb_SkipExitContradictingExitStatusIsRefused proves a step cannot
// both PASS and SKIP on one exit code: the contradiction is refused loudly
// rather than resolved silently in favour of either reading.
func TestCommandVerb_SkipExitContradictingExitStatusIsRefused(t *testing.T) {
code := sdk.CheckSkippedExitCode
res := runCommandVerb(code, map[string]any{"command": "exit 3", "skip_exit": code}, &code)
if res.Status != kit.StatusFail {
t.Fatalf("status = %v, want fail — skip_exit and exit_status both declare %d", res.Status, code)
}
if !strings.Contains(res.Message, "exit_status") {
t.Errorf("message = %q, want it to name the contradiction", res.Message)
}
}

// TestCommandVerb_SkipExitWithBackgroundIsRefused proves a declaration that
// could never take effect is refused instead of being silently inert: a
// backgrounded command has no exit status to interpret.
func TestCommandVerb_SkipExitWithBackgroundIsRefused(t *testing.T) {
code := sdk.CheckSkippedExitCode
res := runCommandVerb(0, map[string]any{"command": "true", "skip_exit": code, "background": true}, nil)
if res.Status != kit.StatusFail {
t.Fatalf("status = %v, want fail — background: true has no exit status to skip on", res.Status)
}
if !strings.Contains(res.Message, "background") {
t.Errorf("message = %q, want it to name the contradiction", res.Message)
}
}

// TestCommandVerb_SkipExitShortCircuitsTheMatchers proves a skipped step's
// stdout/stderr matchers do NOT run: a step that ran nothing has no output to
// assert on, and a matcher that turned the skip back into a failure would
// defeat the channel. The command's stdout is deliberately EMPTY here, which
// no `contains` matcher could accept.
func TestCommandVerb_SkipExitShortCircuitsTheMatchers(t *testing.T) {
code := sdk.CheckSkippedExitCode
res := verb{}.RunVerb(context.Background(), &fakeCC{mode: kit.ModeLive, exec: &fakeExec{stdout: "", exit: code}},
&spec.Op{PluginInput: map[string]any{"command": "exit 3", "skip_exit": code},
Stdout: spec.MatcherList{{Op: "contains", Value: "never printed"}}})
if res.Status != kit.StatusSkip {
t.Fatalf("status = %v, want skip — a step that ran nothing has no output to assert on: %s", res.Status, res.Message)
}
}

// TestCommandVerb_SkipExitCarriesNoStdout proves a SKIP stashes no value: like
// a failure, a skipped step's output was never vouched for, and a consumer that
// read it would be acting on a value the step itself declined to assert.
func TestCommandVerb_SkipExitCarriesNoStdout(t *testing.T) {
code := sdk.CheckSkippedExitCode
res := runCommandVerbOut("output of a step that ran nothing\n", code, map[string]any{"command": "exit 3", "skip_exit": code})
if res.Status != kit.StatusSkip {
t.Fatalf("status = %v, want skip", res.Status)
}
if res.CapturedValue != "" {
t.Fatalf("CapturedValue = %q, want empty — a skip must not stash output", res.CapturedValue)
}
}

// --- the skip channel against a REAL process ---------------------------------
//
// The tests above drive the verb through a canned exit code, which is the file's
// established fixture (and the right shape for the verb's branches). But the
// boundary this verb actually crosses is a PROCESS: the step's command is a shell
// command, and its exit code is what the channel reads. R8 says a boundary that
// crosses a real service runs against that service and is never mocked, so the
// channel is proven here against a real `sh -c` that really exits with the org's
// skip code — the host-side leg (from_host: true) executes through os/exec, so
// no executor is involved at all.

// TestCommandVerb_SkipExitIsSkipped_Live proves the channel against a real
// process: a command that really exits 3, on a step that declares the code, is
// SKIPPED.
func TestCommandVerb_SkipExitIsSkipped_Live(t *testing.T) {
code := sdk.CheckSkippedExitCode
res := verb{}.RunVerb(context.Background(), &fakeCC{mode: kit.ModeLive},
&spec.Op{PluginInput: map[string]any{"command": "exit 3", "from_host": true, "skip_exit": code}})
if res.Status != kit.StatusSkip {
t.Fatalf("status = %v, want skip — a real command that exited %d, on a step that declared it, ran nothing: %s",
res.Status, code, res.Message)
}
}

// TestCommandVerb_SkipExitIsOptIn_Live is the live NEGATIVE CONTROL: the SAME
// real command, on a step that does NOT declare the code, is still a FAILURE.
// The fake-exec twin of this case is TestCommandVerb_SkipExitIsOptIn; this one
// removes the mock from the comparison, so the opt-in property is proven against
// a process rather than a canned status.
func TestCommandVerb_SkipExitIsOptIn_Live(t *testing.T) {
res := verb{}.RunVerb(context.Background(), &fakeCC{mode: kit.ModeLive},
&spec.Op{PluginInput: map[string]any{"command": "exit 3", "from_host": true}})
if res.Status != kit.StatusFail {
t.Fatalf("status = %v, want fail — an undeclared exit code is the author's to read, not the verb's", res.Status)
}
}

// TestCommandVerb_SkipExitPrecedenceOverExpectNonZero proves the precedence the
// schema states: the skip is read FIRST, so a step declaring skip_exit together
// with expect_non_zero means "exit this code → skipped; any other non-zero →
// passed" — a wildcard narrowed by an exact exception, not a contradiction.
// Only an EXACT-code contradiction (skip_exit and exit_status naming the same
// code) is refused, and that is proven by the test above.
func TestCommandVerb_SkipExitPrecedenceOverExpectNonZero(t *testing.T) {
code := sdk.CheckSkippedExitCode
skipped := runCommandVerb(code, map[string]any{"command": "exit 3", "skip_exit": code, "expect_non_zero": true}, nil)
if skipped.Status != kit.StatusSkip {
t.Fatalf("status = %v, want skip — the skip is read before the exit comparison: %s", skipped.Status, skipped.Message)
}
passed := runCommandVerb(1, map[string]any{"command": "exit 1", "skip_exit": code, "expect_non_zero": true}, nil)
if passed.Status != kit.StatusPass {
t.Fatalf("status = %v, want pass — any OTHER non-zero still satisfies expect_non_zero: %s", passed.Status, passed.Message)
}
}
22 changes: 22 additions & 0 deletions candy/plugin-command/schema/command.cue
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,26 @@
// mutually-exclusive intents: exact-code vs any-non-zero). Default false =
// the historic behaviour (assert exit_status, defaulting to 0).
expect_non_zero?: bool @go(ExpectNonZero)
// skip_exit — declare that THIS step's command reports "the phase ran nothing"
// by exiting with the org's skip code: the check 0/1/2/3 convention's 3
// (github.com/opencharly/spec/exitcode — "the phase RAN NOTHING — neither a
// pass nor a failure"), re-exported as sdk.CheckSkippedExitCode. Only that
// code is accepted: a step declares the ORG's code, never a private one, so
// "3" means the same thing in a task step as it does in the check runner.
//
// A command that exits this code is SKIPPED, not failed: it proved nothing and
// disproved nothing, so it must mint neither a red nor a green. The
// stdout/stderr matchers do NOT run for a skipped step — a step that ran
// nothing has no output to assert on.
//
// OPT-IN, and it stays opt-in: a command that exits 3 WITHOUT this declaration
// is a command whose exit code the author never interpreted, and reading it as
// a skip would silently turn a real failure into a non-failure.
//
// PRECEDENCE: the skip is read FIRST. A step that declares skip_exit together
// with expect_non_zero therefore means "exit this code → skipped; any other
// non-zero → passed" — a wildcard narrowed by an exact exception. Only an
// EXACT-code contradiction is refused (skip_exit and exit_status naming the
// SAME code), because one code cannot be both the pass and the skip.
skip_exit?: int @go(SkipExit,type=*int)
}
Loading