Skip to content
Open
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
10 changes: 10 additions & 0 deletions di/serverselect/init.q
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
\l ::serverselect.q

/ logging is an injected dependency: the start-up script that wires the modules together -
/ or the user at run time - must call init with a required `log dependency before using the
/ module. kx.log is intentionally NOT loaded here.
/ note: the injected log dict is stored in .z.m.log and called as .z.m.log[`info][`ctx;"msg"]

export:([init;
addserverfull;addserverattr;addserver;setserveractive;getserverstable;addserversfromtable;
getservers;selector;getserverbytype;gethandlebytype;gethpbytype;getserverids])
271 changes: 271 additions & 0 deletions di/serverselect/integration.q
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
/ ===========================================================================
/ di.serverselect integration test (real processes)
/ Spawns child q listeners, opens REAL IPC handles to them, and drives every
/ exported function through a realistic gateway lifecycle - register ->
/ query -> select -> route a live query to the chosen handle -> bulk-register
/ -> disconnect -> error handling -> init. Unlike a simulated test, the
/ selection functions hand back handles that are actually queried, so the
/ assertions prove queries reach the expected backend process.
/ .
/ Self-contained: no helper files. The child backends are bare `q` listeners
/ whose identity (`whoami`) and `ping` api are injected over IPC at setup; the
/ test launches and tears them down itself.
/ .
/ Requirements:
/ - run THIS script with KDB-X q (needs `use`)
/ - a q on PATH (override with $QBIN) to launch the child listeners
/ - some free TCP ports: the test scans for free ports from a pid-derived
/ base (so concurrent runs don't collide); set $SSPORT to pin the base
/ Run (QPATH must include this repo and the kx module dir):
/ QPATH=/path/to/kx/mod:/path/to/kdbx-modules <kdbx>/q integration.q
/ Exits with a non-zero code equal to the number of failed assertions.
/ ===========================================================================

srvsel:use`di.serverselect;

/ ---- tiny assertion harness ----
/ note: avoid `desc`/`exp`/`log` as names - they are reserved words in KDB-X
PASS:0; FAIL:0;
lbl:{[ok] $[ok;" ok | ";" FAIL | "]};
chk:{[nm;got;expd]
ok:got~expd;
$[ok;PASS+:1;FAIL+:1];
m:lbl[ok],nm;
if[not ok; m:m," || got=",(.Q.s1 got)," exp=",.Q.s1 expd];
-1 m;
};
errtok:`$"__ERRORED__";
chkerr:{[nm;f]
r:@[f;();{[e](errtok;e)}];
ok:(0<count r) and errtok~first r;
$[ok;PASS+:1;FAIL+:1];
m:lbl[ok],nm;
m:m,$[ok;" || signalled: ",(r 1);" || NO ERROR raised, got=",.Q.s1 r];
-1 m;
};
hdr:{[t] -1""; -1"==== ",t," ===="; };

d1:2024.01.01; d2:2024.01.02; d3:2024.01.03;

/ ---- child-process management ----
qbin:$[0<count s:getenv`QBIN; s; "q"];
/ fleet: 0,1 rdb | 2,3 hdb (attr) | 4 gateway | 5,6 bulk-registered rdb/hdb
names:`rdb1`rdb2`hdb1`hdb2`gw1`rdb3`hdb3;
pids:`int$();

/ no hardcoded ports: derive a starting point from this pid (so concurrent runs
/ on a shared host don't collide) - or take an explicit base from $SSPORT - then
/ scan upward for ports that are actually free. nothing assumes a fixed port.
/ probe whether a localhost port is in use: a free port refuses instantly, so this short
/ timeout only bounds the wait for a filtered (silently-dropped) port - keep it small so a
/ full scan of 10*count names candidates cannot stall for many seconds
inuse:{[p] @[{hclose hopen x; 1b}; (`$":localhost:",string p;50); 0b]};
baseport:$[0<count s:getenv`SSPORT; "I"$s; 20000+`int$.z.i mod 20000];
findfree:{[start;n]
/ take the first n free ports from a generous candidate window above start
cand:start+til 10*n;
free:cand where not inuse each cand;
if[n>count free;
'"could not find ",string[n]," free ports scanning up from ",string start];
:n#free;
};
ports:findfree[baseport;count names];

launch:{[port]
/ start a bare q listener detached and capture its shell background pid ($!) - so every

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return value of launch is now the shell pid string parsed as an int, but if the system call returns an empty list (e.g. on a platform where echo $! is unavailable or the shell strips the output), first of an empty list returns a null, and "I"$ of a null/empty result would silently produce 0Ni — which the rest of the code already guards against, but the failure mode is silent. The real risk is that first system … on some q versions returns a char list (the raw string), not a string atom, so "I"$ parses correctly; however if the command fails entirely, system raises a signal and the @[…;…;{}]-style trap is absent here. Wrap the call in a trap: @[{"I"$first system x}; cmd; {0Ni}] to prevent an uncaught signal from aborting the entire launch phase.

/ child can be reaped by exact pid later, whether or not we ever connect to it, and with
/ no dependence on the qbin path (identity/api are injected over IPC once connected)
:"I"$first system qbin," -p ",string[port]," -q </dev/null >/dev/null 2>&1 & echo $!";
};

connect:{[port]
/ open a handle, retrying for up to ~10s while the child binds and accepts
hp:`$":localhost:",string port;
step:{[hp;h] if[not null h; :h]; system"sleep 0.2"; @[hopen;(hp;500);{0Ni}]}[hp];
:50 step/ 0Ni;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

baseport:$[0<count s:getenvSSPORT; "I"$s; 20000+int$.z.i mod 20000] — when $SSPORT is not set, getenv returns an empty string "", so count s is 0 and the else branch is taken (correct). But when $SSPORT IS set, "I"$s casts a char-vector to an int-vector (type 6h), not a single int (type 6i). findfree then calls start+til 10*n where start is an int-vector, producing a matrix rather than a vector of candidate ports, so inuse each cand will fail at runtime. The fix is "i"$s (lowercase, int atom) or first "I"$s.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incorrect, casting using the lowercase "i" will results in ascii for each char instead of int

};

teardown:{[]
/ always-run cleanup. kill every launched child by its captured pid (exact). the pkill
/ fallback is a last resort for the rare case a pid was never captured - keyed on our own
/ unique port and -q flag (NOT the qbin path, which is configurable), so a custom qbin
/ cannot break it. the [-]p bracket keeps the pattern from matching this very kill command.
live:pids where not null pids;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pkill fallback pattern "[-]p ",string[x]," -q" matches any process with that port number and -q flag, not just processes spawned by this test run. On a shared host another q process started on the same port (possible if findfree races) would be killed. The previous pattern anchored on bin/q to narrow the match; removing that anchor widens it unnecessarily. Reintroduce a narrower anchor (e.g. include the qbin basename or the process group) so the fallback cannot kill unrelated processes.

if[count live; @[system;"kill ",(" " sv string live)," 2>/dev/null; true";{}]];
{@[system;"pkill -f \"[-]p ",string[x]," -q\" 2>/dev/null; true";{}]} each ports;
@[hclose;;{}] each handles where not null handles;
};

/ launch the fleet (capturing each child's shell pid for exact-pid teardown), then connect
-1 "selected free ports: ",.Q.s1 ports;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

connect uses 50 step/ 0Ni (a fixed-point / convergence scan). Because step returns 0Ni (a null int) on failure and an open handle integer on success, convergence will never naturally terminate if the child never comes up — 0Ni ~ 0Ni is 1b so q's fixed-point / would stop after the first failure, returning 0Ni immediately rather than retrying 50 times. The correct idiom for a counted retry is 50 step\ (scan, discard) followed by a check, or a do[50; ...] loop, or ({not null x} step\) with a separate retry limit.

@ascottDI ascottDI Jul 1, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incorrect, reviewer has the "/" converge semantics inverted. Correctly calls 50 times

pids:launch each ports;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pids:launch each ports replaces the previous two-step sequence where pids was populated after connecting and querying .z.i from each child. The new code assigns pids immediately from the shell's $!, which is the pid of the background shell job, not necessarily the pid of the q process itself (on some systems the shell forks an intermediate process). If the shell pid and the q process pid differ, kill <pid> in teardown and in STEP 9 will target the wrong process. Verify that the shell used by system on all target platforms makes $! equal to the q process pid, or revert to querying .z.i over IPC after connecting.

handles:connect each ports;
/ guarantee the fleet is reaped on ANY exit (normal, error-abort, or exit code)
.z.exit:{teardown[]};
if[any null handles;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.z.exit is assigned directly (".z.exit:{teardown[]}") rather than via .dotz.set, violating the TorQ convention and potentially silently overwriting an existing handler (e.g. one set by the TorQ framework itself). Use .dotz.set[.z.exit; {teardown[]}]` instead.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ran as a standalone test so not necessary, additionally the use of .dotz.set would result in a failure as it is not available here.

-2 "failed to connect to child processes on ports: ",(" " sv string ports where null handles);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If any null handles is true, exit 3 is called. .z.exit fires and teardown is invoked, but handles at that point may contain 0Ni nulls; the line @[hclose;;{}] each handles where not null handles in teardown should be safe. However, the processes that did start but whose handles are null (connect timed out) will only be reaped by the pkill fallback which matches "bin/q [-]p <port>". If qbin is a custom path that does not contain bin/q, the pkill pattern won't match and those children will be orphaned. The pkill pattern should be derived from qbin or use a more robust signal.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actual error,
Fixed. processes that started but never connected had no captured pid (came from .z.i), so were only reachable via pkill which a custom $QBIN breaks.

launch now captures the shell pid (& echo $!) -> exact pid for every child, no .z.i reliance.
pkill fallback rekeyed to [-]p -q (unique port + flag), qbin-independent.

exit 3];
/ inject each backend's identity + ping api over IPC
{[h;nm] h "whoami:`",string nm; h "ping:{whoami}";}'[handles;names];
-1 "spawned ",(string count handles)," backend processes; real handles: ",.Q.s1 handles;

/ route: select a server of type t via strategy sel, then SEND ping[] over the chosen handle
route:{[t;sel] h:srvsel.gethandlebytype[t;sel]; $[()~h; `NONE; h "ping[]"]};
/ map serverids -> their handles (to route attribute-path selections)
sidhandles:{[sids] exec handle from srvsel.getserverstable[] where serverid in sids};
/ the gateway's host/port for the hpup column - its real (dynamically chosen) address
gwhp:`$":localhost:",string ports 4;

/ test steps run at top level (a single enclosing function would exceed q's
/ per-function constant limit). chk/chkerr trap their own assertion failures;
/ .z.exit guarantees teardown regardless of how the script ends.

/ init must be called before use (log is a required injected dependency, no default)
srvsel.init[enlist[`log]!enlist `info`warn`error!({[c;m]};{[c;m]};{[c;m]})];

/ =========================================================================
hdr"STEP 1 initial empty state";
chk["fresh module: server table empty";0=count srvsel.getserverstable[];1b];
chk["schema columns correct";`serverid`handle`procname`servertype`hpup`active`lastp`hits`attributes;cols srvsel.getserverstable[]];

/ =========================================================================
hdr"STEP 2 registration of live handles (addserver / addserverattr / addserverfull)";
srvsel.addserver[handles 0;`rdb];
srvsel.addserver[handles 1;`rdb];
srvsel.addserverattr[handles 2;`hdb;`date`sym!((d1;d2);`A`B)];
srvsel.addserverattr[handles 3;`hdb;`date`sym!((d2;d3);`B`C)];
srvsel.addserverfull[handles 4;`gw1;`gateway;gwhp;(enlist`region)!enlist`EU];
chk["five servers registered";5=count srvsel.getserverstable[];1b];
chk["serverids autoincrement 1..5";1 2 3 4 5i;exec serverid from srvsel.getserverstable[]];
chk["handles stored are the real open handles";handles til 5;exec handle from srvsel.getserverstable[]];
chk["servertypes as registered";`rdb`rdb`hdb`hdb`gateway;exec servertype from srvsel.getserverstable[]];
chk["addserverfull populated procname";`gw1;first exec procname from srvsel.getserverstable[] where servertype=`gateway];
chk["addserverfull populated hpup";gwhp;first exec hpup from srvsel.getserverstable[] where servertype=`gateway];
chk["addserver -> empty attributes";(()!());first exec attributes from srvsel.getserverstable[] where handle=handles 0];
chk["all active by default";1b;all exec active from srvsel.getserverstable[]];

/ =========================================================================
hdr"STEP 3 getservers (lookup + attribute match scoring)";
chk["lookups=` returns all active";5;count srvsel.getservers[`;`;()!()]];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

STEP 9 kills pids 0 (rdb1) and then sleeps 0.3 s before deactivating it. However pids 0 may be 0Ni (if the @[x;".z.i";{0Ni}] capture failed), in which case system"kill 0Ni ..." passes a literal string "0Ni" to the shell — which will fail silently but leave rdb1 alive. The test then deactivates the handle and assumes rdb1 is dead, but subsequent route calls may still reach rdb1 via its open handle. Add a null check before killing, or assert not null pids 0 at the start of STEP 9.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added chk["rdb1 pid captured";1b;not null pids 0] at the start of STEP 9. Note the .z.i capture you cite was already replaced with a shell-$! capture in the prior fix from comment on line 98-101, so pids 0 is now essentially always set. Also, routing can't actually reach rdb1 — route selects only active servers and we deactivate it. The real risk the guard closes: a null pid → kill "0Ni" no-ops silently, letting STEP 9 pass while only exercising deactivation rather than a dead backend. Now it fails loudly instead.

chk["servertype=hdb returns 2";2;count srvsel.getservers[`servertype;`hdb;()!()]];
chk["servertype list rdb,hdb returns 4";4;count srvsel.getservers[`servertype;`rdb`hdb;()!()]];
chk["procname=gw1 returns the gateway handle";enlist handles 4;exec handle from srvsel.getservers[`procname;`gw1;()!()]];
chk["unknown servertype returns empty";0;count srvsel.getservers[`servertype;`nope;()!()]];
chk["attribmatch column present";1b;`attribmatch in cols srvsel.getservers[`;`;()!()]];
chk["complete date match flagged for some hdb on d1";1b;
any {x[`date]0} each (srvsel.getservers[`servertype;`hdb;(enlist`date)!enlist enlist d1])`attribmatch];

/ =========================================================================
hdr"STEP 4 selector (strategies on a known table)";
seltab:([]serverid:101 102 103i;handle:201 202 203i;lastp:(2024.01.03D0;2024.01.01D0;2024.01.02D0));
chk["roundrobin picks oldest lastp";202i;(srvsel.selector[seltab;`roundrobin])`handle];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

STEP 9 routes 6 queries after killing rdb1 and storing results as postresp. Then it asserts not rdb1 in postresp. However routereturns ``NONE when `gethandlebytype` returns `()`, and the open handle for rdb1 is still registered (only deactivated). If the dead rdb1 handle is somehow selected before deactivation takes effect (race), or if `hopen` to a dead process returns an error swallowed by the route trap, `route` returns NONE ``. The assertion not NONE in postresp catches that, but the assertion not rdb1 in postrespwould still pass in that case — leaving the route failure undetected in one scenario. This is acceptable defensive test logic, but the ordering ofkillsleepsetserveractivemeans there is a window where rdb1 is still active and its handle is dead, so the very firstroutecall inpostrespcould hit the dead handle and return ``NONE `` rather than rdb2/`rdb3`, causing `not `NONE in postresp` to fail spuriously.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not an error
the reviewer is misreading the ordering. setserveractive[handles 0;0b] runs before the postresp route loop, and as q is single-threaded, there's no race condition and no window where a route call sees rdb1 active-but-dead (that window is entirely between kill and setserveractive, with no routing in it).

chk["last picks newest lastp";201i;(srvsel.selector[seltab;`last])`handle];
chk["any picks a row from the table";1b;((srvsel.selector[seltab;`any])`handle) in 201 202 203i];
empt:([]serverid:`int$();handle:`int$();lastp:`timestamp$());
chk["empty table -> null handle";0Ni;(srvsel.selector[empt;`roundrobin])`handle];

/ =========================================================================
hdr"STEP 5 live routing (gethandlebytype / gethpbytype / getserverbytype)";
/ round-robin across the two live rdbs: both start with null lastp, so LRU
/ visits rdb1, rdb2, rdb1, rdb2 - and each query is answered by that process
resp:{[i] route[`rdb;`roundrobin]} each til 4;
chk["round-robin routes to live rdbs in LRU order";`rdb1`rdb2`rdb1`rdb2;resp];
chk["only the two live rdbs ever answer";`rdb1`rdb2;asc distinct resp];
chk["gethpbytype gateway returns its hpup";gwhp;srvsel.gethpbytype[`gateway;`roundrobin]];
chk["getserverbytype returns requested column";`gw1;srvsel.getserverbytype[`gateway;`procname;`roundrobin]];
chk["gethandlebytype unknown type -> ()";();srvsel.gethandlebytype[`nope;`roundrobin]];
chk["a single hdb query is answered by an hdb";1b;(route[`hdb;`roundrobin]) in `hdb1`hdb2];
hitsbefore:exec sum hits from srvsel.getserverstable[] where servertype=`gateway;
srvsel.gethandlebytype[`gateway;`roundrobin];
chk["selection increments hits";1b;(hitsbefore+1)=exec sum hits from srvsel.getserverstable[] where servertype=`gateway];

/ =========================================================================
hdr"STEP 6 getserverids - servertype (symbol) path";
chk["rdb -> the two rdb serverids";1 2i;asc raze srvsel.getserverids[`rdb]];
chk["rdb,hdb -> all four serverids";1 2 3 4i;asc raze srvsel.getserverids[`rdb`hdb]];
srvsel.setserveractive[handles 0;0b]; srvsel.setserveractive[handles 1;0b];
chkerr["all requested servertype inactive -> error";{srvsel.getserverids[`rdb]}];
srvsel.setserveractive[handles 0;1b]; srvsel.setserveractive[handles 1;1b];

/ =========================================================================
hdr"STEP 7 getserverids - attribute path + routing to the matched backend";
/ hdb1 covers (d1,d2)/(A,B); hdb2 covers (d2,d3)/(B,C)
chk["date=d1 -> hdb1 only";enlist`hdb1;
distinct {x"ping[]"} each sidhandles raze srvsel.getserverids[enlist[`date]!enlist enlist d1]];
chk["cross (d1,d2)x(A,B) -> hdb1 alone covers it";enlist`hdb1;
distinct {x"ping[]"} each sidhandles raze srvsel.getserverids[`date`sym!((d1;d2);`A`B)]];
chk["independent over d1,d2,d3 + A,B,C -> both hdbs contribute";`hdb1`hdb2;
asc distinct {x"ping[]"} each sidhandles raze srvsel.getserverids[`date`sym`attributetype!((d1;d2;d3);`A`B`C;`independent)]];
chk["servertype key scopes attribute filter to hdbs";3 4i;asc raze srvsel.getserverids[`servertype`date!(`hdb;enlist d2)]];
chk["empty requirement dict -> all active serverids";1 2 3 4 5i;asc raze srvsel.getserverids[()!()]];
chkerr["besteffort=0b impossible -> error";{srvsel.getserverids[`date`besteffort!(enlist 2099.01.01;0b)]}];
chkerr["no server has requested attribute value -> error";{srvsel.getserverids[enlist[`date]!enlist enlist 2099.01.01]}];

/ =========================================================================
hdr"STEP 8 addserversfromtable (bulk registration of live handles)";
conntab:([]w:handles 5 6;proctype:`rdb`hdb;attributes:(()!();`date`sym!((d1;d3);`A`C)));
srvsel.addserversfromtable[`rdb`hdb;conntab];
chk["bulk-registered handles 5,6 now present";2=count select from srvsel.getserverstable[] where handle in handles 5 6;1b];
chk["bulk-registered rdb3 is reachable via its handle";`rdb3;(handles 5)"ping[]"];
chk["bulk-registered rdb3 now appears in rdb serverids";1b;6i in raze srvsel.getserverids[`rdb]];
/ skip-already-active: re-offering a live handle must not duplicate it
n0:count srvsel.getserverstable[];
srvsel.addserversfromtable[`rdb;([]w:enlist handles 0;proctype:enlist`rdb;attributes:enlist()!())];
chk["already-active handle skipped (no new row)";n0;count srvsel.getserverstable[]];
/ (proctype filter, `ALL, and optional procname/hpup columns are covered in test.csv)

/ =========================================================================
hdr"STEP 9 disconnect: kill a backend, deactivate it, prove routing adapts";
/ a real gateway's .z.pc handler would call setserveractive[h;0b] on the dropped handle
/ guard: the kill is only a genuine test if rdb1's pid was captured. if pids 0 were null,
/ string 0Ni -> "0Ni" and the kill silently no-ops, degrading STEP 9 into a deactivation-only
/ test. fail loudly here rather than pass on a half-exercised path.
chk["rdb1 pid captured (so the kill below is real)";1b;not null pids 0];
@[system;"kill ",string[pids 0]," 2>/dev/null; true";{}]; system"sleep 0.3";
srvsel.setserveractive[handles 0;0b];
/ active rdbs are now rdb2 (live) and rdb3 (live); the dead rdb1 must never be picked
postresp:{[i] route[`rdb;`roundrobin]} each til 6;
chk["killed/deactivated rdb1 is never selected";1b;not `rdb1 in postresp];
chk["no routed query hits a dead handle (all answered)";1b;not `NONE in postresp];
chk["the two remaining live rdbs both serve";`rdb2`rdb3;asc distinct postresp];

/ =========================================================================
hdr"STEP 10 error handling (every guarded path signals + logs)";
chkerr["addserverfull rejects non-int handle";{srvsel.addserverfull[`bad;`;`rdb;`;()!()]}];
chkerr["addserverfull rejects non-symbol servertype";{srvsel.addserverfull[71i;`;"rdb";`;()!()]}];
chkerr["addserver rejects bad handle";{srvsel.addserver[`bad;`rdb]}];
chkerr["addserverattr rejects bad servertype";{srvsel.addserverattr[73i;"hdb";()!()]}];
chkerr["setserveractive rejects non-int handle";{srvsel.setserveractive["bad";0b]}];
chkerr["setserveractive rejects non-boolean flag";{srvsel.setserveractive[1i;`yes]}];
chkerr["getservers rejects bad nameortype";{srvsel.getservers[`badname;`x;()!()]}];
chkerr["selector rejects unknown strategy";{srvsel.selector[seltab;`bogus]}];
chkerr["getserverids rejects null servertype";{srvsel.getserverids[`]}];
chkerr["getserverids rejects unregistered servertype";{srvsel.getserverids[`nope]}];
chkerr["getserverids rejects non-symbol non-dict arg";{srvsel.getserverids[42]}];
chkerr["getserverids rejects nested/malformed attribute value";{srvsel.getserverids[(enlist`date)!enlist enlist d1,d2]}];
chkerr["addserversfromtable rejects missing columns";{srvsel.addserversfromtable[`rdb;([]x:enlist 1i)]}];

/ =========================================================================
hdr"STEP 11 init (required log dependency - bespoke + kx.log)";
caplog:([]lvl:`symbol$();msg:());
caplogger:`info`warn`error!({[c;m]`caplog upsert(`info;m)};{[c;m]`caplog upsert(`warn;m)};{[c;m]`caplog upsert(`error;m)});
srvsel.init[enlist[`log]!enlist caplogger];
caplog:0#caplog;
srvsel.addserver[40i;`rdb];
chk["injected logger captures an info message";1b;0<count select from caplog where lvl=`info];
chk["injected logger receives the message text";1b;any caplog[`msg] like "*registering server*"];
caplog:0#caplog;
@[{srvsel.selector[([]handle:1 2i;lastp:2#.z.p);`bogus]};();{}];
chk["injected logger captures an error message";1b;0<count select from caplog where lvl=`error];
chkerr["init rejects non-dictionary deps";{srvsel.init[(::)]}];
chkerr["init rejects deps missing log key";{srvsel.init[()!()]}];
chkerr["init rejects log value that is not a dict";{srvsel.init[enlist[`log]!enlist(::)]}];
chkerr["init rejects log dict missing error key";{srvsel.init[enlist[`log]!enlist(`info`warn!({[c;m]};{[c;m]}))]}];
srvsel.init[enlist[`log]!enlist (use`kx.log)[`createLog][]];
srvsel.addserver[41i;`rdb];
chk["kx.log logger wired and usable";1b;41i in exec handle from srvsel.getservers[`servertype;`rdb;()!()]];

-1"";-1"===========================================================";
-1" TOTAL: ",(string PASS+FAIL)," | PASS: ",(string PASS)," | FAIL: ",string FAIL;
-1"===========================================================";
exit FAIL
Loading