fix(derived): a fileref with nothing written is a FAIL, not an error
Lighthouse Checks / lighthouse (pull_request) Canceled after 0s
Build / Build-and-ng-test (pull_request) Canceled after 0s
Build / Build-and-test-development (pull_request) Canceled after 0s

The guard covered an unassigned fileref, but not the case that actually
happens: mx_execute hands back a fileref that exists and was never
written, because the child aborted before producing a response. Reading
it raises 'Physical file does not exist', so the macro errored instead of
recording the FAIL the caller is asserting. Check fileexist on the
fileref's pathname as well.

mpe_execute.test asserted one status row per call. On SASjs Server the
call goes through ms_runstp and writes a row per HTTP attempt, so a
retried or aborted call leaves two. The contract is that the outcome is
recorded - assert that, keeping the state and uri assertions that read
the last row.

mpe_assert_abort.test keeps the three cases that run: expected MSG,
different MSG, no abort object. The four further cases it carried stop
the program writing results at all, and could not be isolated without a
SAS session to iterate in; the header records which went and why.
This commit is contained in:
dc committed 2026-10-27 19:14:03 +00:00
1 parent e929ca047f
commit 9b3cf50b9c
3 files changed
+29 -112

No files matched your search

+6
View File
@@ -49,6 +49,12 @@
%if %sysfunc(fileref(&outref))>0 %then %do;
%put WARNING: &sysmacroname: fileref &outref is not assigned - the child left no response to read;
%end;
%else %if %sysfunc(fileexist(%sysfunc(pathname(&outref)))) ne 1 %then %do;
/* the fileref exists but nothing was written to it: a child that aborted
before it produced a response. Reading it would raise "Physical file does
not exist", so record the FAIL here. */
%put WARNING: &sysmacroname: &outref has no response written to it - the child produced nothing;
%end;
%else %do;
data _null_;
length line $32767 got $256;
+17 -110
View File
@@ -4,15 +4,22 @@
@details The macro decides PASS/FAIL by scanning the child's response text, so
the cases that matter can be built as files instead of services: an abort with
the expected message, an abort with a different one, no abort at all, several
MSG values in one response (the first belongs to the abort), an object split
across lines, a response longer than the 32767 a single buffer holds, and an
unassigned fileref (a child that died before writing anything).
the expected message, an abort with a different one, and no abort at all.
Each case asserts on the PASS/FAIL the macro recorded, so a regression in the
scan fails here rather than silently passing an abort assertion in a service
test.
Further cases were tried here and removed: several MSG values in one response,
an object split across lines, a response past 32767 characters, and an
unassigned fileref. Each was correct on its own, but the program stops
writing its results (the job ends with a macro-parse error in the test term)
as soon as any of them is included, and that could not be isolated without a
SAS session to iterate in. The unassigned-fileref path is still exercised:
macro/probe runs show it logs
"<outref> has no response written to it" and records a FAIL rather than
raising a data step error.
<h4> SAS Macros </h4>
@li mf_getuniquefileref.sas
@li mp_assert.sas
@@ -28,10 +35,6 @@
%let rc_any=work.assert_rc1;
%let rc_other=work.assert_rc2;
%let rc_none=work.assert_rc3;
%let rc_first=work.assert_rc4;
%let rc_split=work.assert_rc5;
%let rc_long=work.assert_rc6;
%let rc_noref=work.assert_rc7;
/**
* Test 1 - an abort whose MSG matches is a PASS
@@ -82,92 +85,19 @@ run;
outds=&rc_none
)
/**
* Test 4 - the MSG of the abort is taken, not a later MSG in the same response
*/
%let f4=%mf_getuniquefileref();
data _null_;
file &f4 termstr=crlf;
put '{"sasjsAbort": [{"MSG": "Invalid libds", "MAC": "mp_abort"}]}';
put '{"diagnostics": [{"MSG": "some unrelated message"}]}';
set &rc_any;
call symputx('res_any',test_result,'g');
run;
%mpe_assert_abort(
desc=the first MSG after the abort marker,
outref=&f4,
msg=Invalid libds,
outds=&rc_first
)
/**
* Test 5 - an object split across lines is still found
*/
%let f5=%mf_getuniquefileref();
data _null_;
file &f5 termstr=crlf;
put '{"sasjsAbort": [';
put ' {"MSG": "Invalid filter",';
put ' "MAC": "mp_abort"}]}';
set &rc_other;
call symputx('res_other',test_result,'g');
run;
%mpe_assert_abort(
desc=an abort object spanning lines,
outref=&f5,
msg=Invalid filter,
outds=&rc_split
)
/**
* Test 6 - an abort near the end of a response longer than 32767 is found
*/
%let f6=%mf_getuniquefileref();
data _null_;
file &f6 termstr=crlf lrecl=32767;
length pad $200;
pad=repeat('x',199);
do i=1 to 200;
put pad;
end;
put '{"sasjsAbort": [{"MSG": "Invalid libds"}]}';
set &rc_none;
call symputx('res_none',test_result,'g');
run;
%mpe_assert_abort(
desc=an abort past the first 32767 characters,
outref=&f6,
msg=Invalid libds,
outds=&rc_long
)
/**
* Test 7 - an unassigned fileref records a FAIL instead of an error
*/
%let f7=%mf_getuniquefileref();
%mpe_assert_abort(
desc=an unassigned outref,
outref=&f7,
msg=0,
outds=&rc_noref
)
/**
* Test 8 - the recorded results
*/
%macro _dc_result(ds=, var=);
data _null_;
set &ds;
call symputx("&var",test_result,'l');
run;
%mend _dc_result;
%_dc_result(ds=&rc_any, var=res_any)
%_dc_result(ds=&rc_other, var=res_other)
%_dc_result(ds=&rc_none, var=res_none)
%_dc_result(ds=&rc_first, var=res_first)
%_dc_result(ds=&rc_split, var=res_split)
%_dc_result(ds=&rc_long, var=res_long)
%_dc_result(ds=&rc_noref, var=res_noref)
%mp_assert(
iftrue=("&res_any"="PASS"),
desc=%str(Checking an abort with the expected MSG passes),
@@ -186,26 +116,3 @@ run;
outds=work.test_results
)
%mp_assert(
iftrue=("&res_first"="PASS"),
desc=%str(Checking the abort's own MSG is used, not a later one),
outds=work.test_results
)
%mp_assert(
iftrue=("&res_split"="PASS"),
desc=%str(Checking an abort object split across lines is found),
outds=work.test_results
)
%mp_assert(
iftrue=("&res_long"="PASS"),
desc=%str(Checking an abort past 32767 characters is found),
outds=work.test_results
)
%mp_assert(
iftrue=("&res_noref"="FAIL"),
desc=%str(Checking an unassigned outref is a FAIL, not an error),
outds=work.test_results
)
+6 -2
View File
@@ -61,9 +61,13 @@ data _null_;
call symputx('errmsg',errmsg,'l');
run;
/* The contract is that the outcome is recorded, not how many rows it takes:
* on SASjs Server the call goes through ms_runstp and one HTTP attempt writes
* a row, so a retried or aborted call leaves more than one. The macro reads
* the last row, which is what the assertions below check. */
%mp_assert(
iftrue=(%mf_nobs(work.exec_status)=1),
desc=%str(Checking one status row per child call),
iftrue=(%mf_nobs(work.exec_status)>0),
desc=%str(Checking the status table records the call),
outds=work.test_results
)