diff --git a/.kiro/specs/controller-peers-env-fallback/tasks.md b/.kiro/specs/controller-peers-env-fallback/tasks.md index 2b3fa72e..c20f06bf 100644 --- a/.kiro/specs/controller-peers-env-fallback/tasks.md +++ b/.kiro/specs/controller-peers-env-fallback/tasks.md @@ -63,7 +63,7 @@ - Done when, after rebuilding the worker, the worker suite passes and every worker case in the regression script passes - _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 1.7, 1.8_ -- [ ] 3.3 Default the harness `masters` setting from the environment variable +- [x] 3.3 Default the harness `masters` setting from the environment variable - Add the harness cases to the regression script first and see them fail, always with `--norc`: a bare `set` shows the env value as the `masters` default; after `set masters ...` the explicit value is shown; on a connecting command, the invalid-value message says explicit `set masters` when one ran, default from the variable when none ran and the variable is set, and an explicit invalid value still blames `set masters` with a valid variable set; `help set` names the variable and the opt-out - At startup, before rc-file loading and the command loop, set the `masters` default from the variable when it is set, otherwise keep today's built-in default; an explicit `set masters` still wins - Make the invalid-`masters` message name the source as above (today's wording when neither applies); update the `set` help text diff --git a/src/harness/test.c b/src/harness/test.c index 453597df..ae54b9f0 100644 --- a/src/harness/test.c +++ b/src/harness/test.c @@ -259,7 +259,9 @@ delaymgr_t delaymgr; static setting_t settings[] = { /* HA controller list: comma-separated "name=host:port" or "host:port" * entries (same syntax as the controller --peers flag), enabling the - * test client to fail over across controller candidates. */ + * test client to fail over across controller candidates. main() replaces + * this built-in default with XCE_CONTROLLER_PEERS when that is set and + * non-empty (masters_default_from_env), before the rc file is loaded. */ { (char*)"masters", (char*)NULL, (char*)"localhost:9990" }, { (char*)"secret", (char*)NULL, (char*)"keepSecret" }, { (char*)"myname", (char*)NULL, (char*)"localhost" }, @@ -412,6 +414,11 @@ static const char USAGE_SET[] = " regardless of TTY state, until +y\n" " +y require confirmation before those\n" " commands again (default)\n" + " masters default: $XCE_CONTROLLER_PEERS when it\n" + " is set and non-empty, else localhost:9990;\n" + " an explicit `set masters' wins. To opt\n" + " out, leave XCE_CONTROLLER_PEERS unset or\n" + " empty (e.g. XCE_CONTROLLER_PEERS= xce_test)\n" "example: set masters ctrl1=localhost:9991,ctrl2=localhost:9992\n"; static const char USAGE_JOIN_WORKER_HEAD[] = @@ -1011,6 +1018,54 @@ static xce_client_config_t config; * persists for the lifetime of the client context. */ static xce_cluster_config_t master_cfg; +/* The `masters` row of settings[] (the table every context shares). */ +static setting_t* masters_setting(void) +{ + int i; + + for (i = 0; settings[i].key; i++) { + if (strcmp(settings[i].key, "masters") == 0) { + return &settings[i]; + } + } + + return NULL; +} + +/* controller-peers-env-fallback (Req 1.2, 1.3, 1.7): at startup, before the + * rc file is loaded or any command runs, take the `masters` default from + * XCE_CONTROLLER_PEERS when it is set and non-empty; otherwise the built-in + * default stays. An explicit `set masters` (typed, scripted or in the rc + * file) still wins through get_setting()'s value-before-defval rule. The + * getenv() storage stays valid for the process lifetime and is only read. */ +static void masters_default_from_env(void) +{ + const char* env = xce_controller_peers_env(); + setting_t* m = masters_setting(); + + if ((env != NULL) && (m != NULL)) { + m->defval = (char*)env; + } +} + +/* Source note appended to enter_ctx()'s invalid-`masters` message (Req 1.4): + * an explicit `set masters` is blamed whenever one ran; otherwise the + * variable is named when it supplied the default; otherwise (the built-in + * default) the message is today's, with no note. */ +static const char* masters_source_note(void) +{ + setting_t* m = masters_setting(); + + if ((m != NULL) && (m->value != NULL)) { + return " (explicit `set masters')"; + } + if (xce_controller_peers_env() != NULL) { + return " (default from XCE_CONTROLLER_PEERS)"; + } + + return ""; +} + int enter_ctx(command_ctx_t* ctx) { char* masters = get_setting("masters"); @@ -1023,8 +1078,8 @@ int enter_ctx(command_ctx_t* ctx) memset(&master_cfg, 0, sizeof(master_cfg)); if ((masters == NULL) || (xce_parse_peers(masters, &master_cfg) != 0) || (master_cfg.candidates_count < 1)) { - fprintf(stderr, "enter_ctx: invalid 'masters' setting `%s'\n", - masters ? masters : "(null)"); + fprintf(stderr, "enter_ctx: invalid 'masters' setting `%s'%s\n", + masters ? masters : "(null)", masters_source_note()); return -EINVAL; } @@ -7061,6 +7116,10 @@ int main(int argc, char* argv[]) rl_attempted_completion_function = xcetest_complete; } + /* controller-peers-env-fallback task 3.3: must run before the rc file + * (and every later command) so an explicit `set masters` there wins. */ + masters_default_from_env(); + /* xce-test-rc-file task 2.2: auto-load $XDG_CONFIG_HOME/xce_test/rc (or * $HOME/.config/xce_test/rc) before the read loop starts, for both * interactive and scripted sessions alike -- unless --norc was given. diff --git a/src/tools/verify-controller-peers-env.sh b/src/tools/verify-controller-peers-env.sh index 502e1898..bdd4cf73 100755 --- a/src/tools/verify-controller-peers-env.sh +++ b/src/tools/verify-controller-peers-env.sh @@ -96,8 +96,6 @@ unset XCE_CONTROLLER_PEERS CONTROLLER_BIN="" SCHEDULER_BIN="" WORKER_BIN="" -# Set by preflight; the harness cases (task 3.3) are its only reader. -# shellcheck disable=SC2034 HARNESS_BIN="" # component:relative-path:variable @@ -181,10 +179,13 @@ stop_all() { # left in RUN_RC and /rc (timed out: 124 with coreutils timeout, 143 # with busybox timeout, 137 if it had to be SIGKILLed). `-k` rather than # `--kill-after` because busybox timeout only accepts the short form. +# stdin is /dev/null, or the file named by RUN_STDIN when the caller sets it +# for this one call (`RUN_STDIN= run_to_completion ...`) -- xce_test +# reads its script only from stdin. run_to_completion() { - local dir="$1" secs="$2"; shift 2 + local dir="$1" secs="$2" in="${RUN_STDIN:-/dev/null}"; shift 2 RUN_RC=0 - timeout -k 2 "$secs" env "$@" "$dir/stdout" 2>"$dir/stderr" || RUN_RC=$? echo "$RUN_RC" >"$dir/rc" } @@ -797,6 +798,92 @@ case_worker_help_names_variable() { add_case case_worker_help_names_variable \ "worker: -h documents XCE_CONTROLLER_PEERS, the opt-out and that sudo drops it (1.5, 1.7)" +# ── Task 3.3 (Req 1.1-1.5, 1.7): the harness (xce_test) takes its `masters` +# default from XCE_CONTROLLER_PEERS; an explicit `set masters` wins. Always +# run with --norc so the developer's own rc file cannot change the result, +# with the script fed on stdin. A bare `set` prints every setting with no +# connection attempt. The connecting command (`client`) is only ever given an +# INVALID masters value: enter_ctx() rejects it before any connection, so no +# case talks to a controller. + +# run_harness +# Write to /script and run `xce_test --norc` on it. +run_harness() { + local dir="$1" script="$2"; shift 2 + printf '%s' "$script" >"$dir/script" + RUN_STDIN="$dir/script" run_to_completion "$dir" 10 "$@" "$HARNESS_BIN" --norc +} + +HARNESS_INVALID_RE="enter_ctx: invalid 'masters' setting" + +case_harness_env_default() { + local d; d="$(new_scratch harness-env-default)" + run_harness "$d" $'set\n' "XCE_CONTROLLER_PEERS=$ENV3" + assert_rc 0 || return 1 + assert_grep "$d/stdout" "^masters +: default: $ENV3\$" "stdout" || return 1 +} +add_case case_harness_env_default \ + "harness: bare set shows XCE_CONTROLLER_PEERS as the masters default (1.2)" + +case_harness_builtin_default() { + local d; d="$(new_scratch harness-builtin-default)" + run_harness "$d" $'set\n' -u XCE_CONTROLLER_PEERS + assert_rc 0 || return 1 + assert_grep "$d/stdout" '^masters +: default: localhost:9990$' "stdout" || return 1 + + d="$(new_scratch harness-empty-default)" + run_harness "$d" $'set\n' "XCE_CONTROLLER_PEERS=" + assert_rc 0 || return 1 + assert_grep "$d/stdout" '^masters +: default: localhost:9990$' "stdout (empty variable)" || return 1 +} +add_case case_harness_builtin_default \ + "harness: unset or empty variable keeps the built-in masters default (1.3, 1.7)" + +case_harness_explicit_wins() { + local d; d="$(new_scratch harness-explicit-wins)" + run_harness "$d" $'set masters x=localhost:7\nset\n' "XCE_CONTROLLER_PEERS=$ENV3" + assert_rc 0 || return 1 + assert_grep "$d/stdout" "^masters +:x=localhost:7 default: $ENV3\$" "stdout" || return 1 +} +add_case case_harness_explicit_wins \ + "harness: explicit set masters is the value shown over the env default (1.1)" + +case_harness_invalid_source() { + local d + # (a) explicit invalid value, variable unset + d="$(new_scratch harness-invalid-explicit)" + run_harness "$d" $'set masters bad\nclient\n' -u XCE_CONTROLLER_PEERS + assert_rc nonzero || return 1 + assert_grep "$d/stderr" "$HARNESS_INVALID_RE \`bad' \(explicit \`set masters'\)" "stderr (a: explicit)" || return 1 + + # (b) no set masters, invalid variable + d="$(new_scratch harness-invalid-env)" + run_harness "$d" $'client\n' "XCE_CONTROLLER_PEERS=bad" + assert_rc nonzero || return 1 + assert_grep "$d/stderr" "$HARNESS_INVALID_RE \`bad' \(default from XCE_CONTROLLER_PEERS\)" "stderr (b: env)" || return 1 + + # (c) explicit invalid value with a valid variable set: explicit is blamed + d="$(new_scratch harness-invalid-explicit-env)" + run_harness "$d" $'set masters bad\nclient\n' "XCE_CONTROLLER_PEERS=$ENV3" + assert_rc nonzero || return 1 + assert_grep "$d/stderr" "$HARNESS_INVALID_RE \`bad' \(explicit \`set masters'\)" "stderr (c: explicit over env)" || return 1 + assert_no_grep "$d/stderr" 'XCE_CONTROLLER_PEERS' "stderr (c: explicit over env)" || return 1 +} +add_case case_harness_invalid_source \ + "harness: invalid masters message names explicit set masters or XCE_CONTROLLER_PEERS (1.1, 1.4)" + +case_harness_help_names_variable() { + local d; d="$(new_scratch harness-help)" + run_harness "$d" $'help set\n' -u XCE_CONTROLLER_PEERS + assert_rc 0 || return 1 + tr '\n' ' ' <"$d/stdout" | tr -s ' ' >"$d/help.txt" + assert_grep "$d/help.txt" 'XCE_CONTROLLER_PEERS' "help set output" || return 1 + assert_grep "$d/help.txt" 'unset or empty' "help set output" || return 1 + assert_grep "$d/help.txt" 'localhost:9990' "help set output" || return 1 +} +add_case case_harness_help_names_variable \ + "harness: help set documents XCE_CONTROLLER_PEERS and the opt-out (1.5, 1.7)" + # ════════════════════════════════════════════════════════════════════════════ # main