fix(mpe_assert_abort): anchor the cut on the whole escaped separator
Cutting on the marker plus a newline still truncated a message that ends a line with "Log Extract:", and an abort whose message merely prefixed the expected one could pass. The appended extract always emits '\n\nLog Extract:\n', so the anchor is that whole string (doubled form first, single as fallback): a message cannot reproduce it by accident, and nothing is cut when neither form is present, so an unexpected layout fails loudly instead of shortening the text. The tail strip goes with it - the leading newlines are part of the anchor - and the test for the doubled separator now writes the fully escaped form the estate actually emits. mpe_abortmessage cuts an over-long message to the room left after the prefix instead of losing its tail to the 32767 limit, and mpe_execute's fallback skips the trailing blank when the child's message is blank, matching the helper.
This commit is contained in:
1 parent
24b8cbe9dc
commit
e74a7ba115
4 files changed
+29
-40
No files matched your search
@@ -27,28 +27,26 @@
|
||||
|
||||
%macro mpe_abortmessage(programvar=program,statevar=state,msgvar=msg);
|
||||
data _null_;
|
||||
/* explicit lengths: an inline symget() is typed by the data step default,
|
||||
which truncates a long child message */
|
||||
/* explicit lengths: symget() nested inside another function is typed by
|
||||
the data step default of $200, so a long endpoint name or message is
|
||||
truncated before the assignment whatever the target declares. Each value
|
||||
is read by name for the same reason: interpolating program or state into
|
||||
this step would let a %, & or quote in an endpoint name be macro-resolved
|
||||
or break the literal. */
|
||||
length _prog $32767 _state $32767 _prefix $32767 _msg $32767 _out $32767;
|
||||
/* every value is read by name: interpolating program or state into this
|
||||
data step would let a %, & or quote in an endpoint name be macro-resolved
|
||||
or break the literal */
|
||||
/* cat, not cats: cats strips the blanks around each argument, which would
|
||||
close up the space before 'did' */
|
||||
/* read each value into its own variable first: symget() nested inside
|
||||
another function is typed by the data step default of $200, so a long
|
||||
endpoint name is truncated before the assignment, whatever length the
|
||||
target declares */
|
||||
_prog=symget("&programvar");
|
||||
_state=symget("&statevar");
|
||||
_prefix=cat(trim(_prog),' did not complete: state=',trim(_state));
|
||||
_msg=symget("&msgvar");
|
||||
_prefix=cat(trim(_prog),' did not complete: state=',trim(_state));
|
||||
/* plain concatenation, not CATX: CATX strips the blanks around each
|
||||
argument, which would close up the space before the message. Internal
|
||||
blanks are kept; leading and trailing ones are not, because symputx
|
||||
strips them when the text becomes a macro variable */
|
||||
/* trim the prefix: it is declared as a character variable, so it arrives
|
||||
padded to length and the child's message would land past the buffer end */
|
||||
strips them when the text becomes a macro variable. */
|
||||
/* SAS character variables stop at 32767, so a message already at that
|
||||
limit cannot be joined to the prefix: cut it to the room that is left, so
|
||||
the text is deliberately short rather than silently losing its tail */
|
||||
_room=32767-length(trim(_prefix))-2;
|
||||
if length(_msg)>_room then _msg=substr(_msg,1,_room);
|
||||
if missing(_msg) then _out=trim(_prefix);
|
||||
else _out=trim(_prefix)||' '||_msg;
|
||||
call symputx('_mpe_abortmsg',_out,'g');
|
||||
|
||||
@@ -82,29 +82,17 @@ data _null_;
|
||||
end;
|
||||
call symputx('abortseen',abortseen,'l');
|
||||
/* mp_abort appends the extract as cats(msg,'\n\nLog Extract:\n',logmsg)
|
||||
and then escapes the message, so the separator arrives as the marker
|
||||
followed by an escaped newline. Cut at the FIRST separator - the marker
|
||||
plus that newline. A message that merely mentions "Log Extract:" is not
|
||||
followed by a newline, so it survives; and a log line that contains the
|
||||
marker cannot move the cut past the separator and leave extract residue. */
|
||||
/* the newline after the marker is escaped like every other backslash,
|
||||
so it arrives doubled; the single form is the fallback for a response
|
||||
assembled without that escape */
|
||||
_marker=index(got,'Log Extract:'||'\\n');
|
||||
if _marker=0 then _marker=index(got,'Log Extract:'||'\n');
|
||||
if _marker>0 then do;
|
||||
if _marker=1 then got='';
|
||||
else got=substr(got,1,_marker-1);
|
||||
/* mp_abort appends the extract as cats(msg,'\n\nLog Extract:\n',logmsg)
|
||||
and then escapes the message, so the separator arrives as two escaped
|
||||
newlines - six characters once the backslashes are doubled.
|
||||
Remove exactly that tail: a message that ends in its own escaped newline
|
||||
keeps it, which a loop over trailing escapes got wrong. */
|
||||
if length(got)>=6 and substr(got,length(got)-5,6)='\\n\\n' then
|
||||
got=substr(got,1,length(got)-6);
|
||||
else if length(got)>=4 and substr(got,length(got)-3,4)='\n\n' then
|
||||
got=substr(got,1,length(got)-4);
|
||||
end;
|
||||
and then escapes every backslash, so the separator arrives fully doubled
|
||||
- or single-escaped for a response assembled without that step. Anchor on
|
||||
the whole separator rather than on the marker plus a newline: a message
|
||||
that ends a line with "Log Extract:" cannot then be truncated, and a log
|
||||
line that contains the marker cannot move the cut. Nothing is cut when
|
||||
neither form is present, so an unexpected layout fails the comparison
|
||||
loudly instead of silently shortening the text. */
|
||||
_marker=index(got,'\\n\\nLog Extract:\\n');
|
||||
if _marker=0 then _marker=index(got,'\n\nLog Extract:\n');
|
||||
if _marker>1 then got=substr(got,1,_marker-1);
|
||||
else if _marker=1 then got='';
|
||||
/* unescape what mp_abort escaped, so the comparison is against the text the
|
||||
hook raised rather than against its JSON form. This is independent of the
|
||||
tail strip: a response that carries no appended extract is still escaped. */
|
||||
|
||||
@@ -166,12 +166,12 @@ run;
|
||||
)
|
||||
|
||||
/**
|
||||
* Test 8 - the six-character separator, as the estate emits it
|
||||
* Test 8 - a fully escaped separator, as the estate emits it
|
||||
*/
|
||||
%let f8=%mf_getuniquefileref();
|
||||
data _null_;
|
||||
file &f8 termstr=crlf;
|
||||
put '{"sasjsAbort": [{"MSG": "doubled\\n\\nLog Extract:\nNOTE: a log line"}]}';
|
||||
put '{"sasjsAbort": [{"MSG": "doubled\\n\\nLog Extract:\\nNOTE: a log line"}]}';
|
||||
run;
|
||||
|
||||
%mpe_assert_abort(
|
||||
|
||||
@@ -163,7 +163,10 @@
|
||||
%else %do;
|
||||
/* no helper on the autocall path: the abort still names the program and
|
||||
state, and carries the child's message, rather than arriving blank */
|
||||
%if "%superq(msg)" ne "" %then
|
||||
%let _mpe_abortmsg=%superq(program) did not complete: state=%superq(state) %superq(msg);
|
||||
%else
|
||||
%let _mpe_abortmsg=%superq(program) did not complete: state=%superq(state);
|
||||
%end;
|
||||
%mp_abort(
|
||||
iftrue=(1=1)
|
||||
|
||||
Reference in new issue
Block a user