Review: '%if &outref ne 0' handed %EVAL the text 'derref5 ne 0', which needs a numeric operand - so the catalog branch failed in exactly the case it was added for, on every caller that passes a fileref. Quoted, the default 0 still skips the file and a fileref creates it. The test now proves both halves: fopen tells us the file was created, and a read loop counted zero records tells us it is empty. A single INPUT is no good - on an empty file it stops the step before the symputx, which is why the first version recorded a FAIL. The outvars placeholder keeps 'keep=name'; the comment records why that is the whole column set: the engine builds the dataset in exactly two places and both project NAME, and the only projection in the SAS tree is getdata.sas.
344 lines
7.8 KiB
SAS
344 lines
7.8 KiB
SAS
/**
|
|
@file
|
|
@brief testing the mpe_derived macro
|
|
|
|
@details Covers the generated statements, the view, the metadata the callers
|
|
consume, and the dependency rules.
|
|
|
|
The configuration checks abort the run when they fail, so they are not
|
|
exercised here - this suite covers the behaviour that callers depend on.
|
|
|
|
<h4> SAS Macros </h4>
|
|
@li mf_existds.sas
|
|
@li mp_assert.sas
|
|
@li mp_assertcols.sas
|
|
@li mp_assertdsobs.sas
|
|
@li mp_assertscope.sas
|
|
@li mpe_derived.sas
|
|
|
|
@author 4GL Apps Ltd
|
|
@copyright 4GL Apps Ltd. This code may only be used within Data Controller
|
|
and may not be re-distributed or re-sold without the express permission of
|
|
4GL Apps Ltd.
|
|
|
|
**/
|
|
|
|
|
|
/* a source table, and a table with no derived variables */
|
|
data work.class;
|
|
set sashelp.class;
|
|
run;
|
|
data work.noconfig;
|
|
set sashelp.class;
|
|
run;
|
|
|
|
/* clean up any config left behind by an earlier run */
|
|
proc sql;
|
|
delete from &dclib..mpe_derived where upcase(libref)='WORK';
|
|
quit;
|
|
|
|
/**
|
|
* Test 1 - statements, view, chaining
|
|
*/
|
|
|
|
proc sql;
|
|
insert into &dclib..mpe_derived set
|
|
tx_from=0
|
|
,tx_to='31DEC5999:23:59:59'dt
|
|
,libref='WORK'
|
|
,dsn='CLASS'
|
|
,var_nm='BAND'
|
|
,var_deps='AGE'
|
|
,var_type='C'
|
|
,var_len=8
|
|
,var_fmt=''
|
|
,var_label='Age band'
|
|
,var_logic="ifc(AGE>13,'TEEN','JUNIOR')"
|
|
,active=1;
|
|
insert into &dclib..mpe_derived set
|
|
tx_from=0
|
|
,tx_to='31DEC5999:23:59:59'dt
|
|
,libref='WORK'
|
|
,dsn='CLASS'
|
|
,var_nm='BAND_AGE'
|
|
,var_deps='BAND AGE'
|
|
,var_type='C'
|
|
,var_len=16
|
|
,var_fmt=''
|
|
,var_logic="cats(BAND,'-',AGE)"
|
|
,active=1;
|
|
insert into &dclib..mpe_derived set
|
|
tx_from=0
|
|
,tx_to='31DEC5999:23:59:59'dt
|
|
,libref='WORK'
|
|
,dsn='CLASS'
|
|
,var_nm='DOUBLE_AGE'
|
|
,var_deps='AGE'
|
|
,var_type='N'
|
|
,var_len=8
|
|
,var_fmt=''
|
|
,var_logic='AGE * 2'
|
|
,active=1;
|
|
quit;
|
|
|
|
/* lower case libds on purpose - the config match must be case insensitive */
|
|
/* the setup SQL above creates the SQL automatic macro variables, so the
|
|
snapshot is taken here - the check is for the macro's own leakage */
|
|
%mp_assertscope(SNAPSHOT)
|
|
|
|
%mpe_derived(libds=work.class,
|
|
dclib=&dclib,
|
|
outref=derref1,
|
|
outview=work.vw_derived,
|
|
outvars=work.derived_vars
|
|
)
|
|
|
|
%mp_assertdsobs(work.derived_vars,
|
|
desc=Test1 - one row per derived variable,
|
|
test=EQUALS 3,
|
|
outds=work.test_results
|
|
)
|
|
|
|
%mp_assertcols(work.vw_derived,
|
|
desc=Test1 - the view carries the derived columns,
|
|
cols=BAND BAND_AGE DOUBLE_AGE,
|
|
test=ALL,
|
|
outds=work.test_results
|
|
)
|
|
|
|
data work.chk1;
|
|
set work.vw_derived;
|
|
where BAND='TEEN';
|
|
run;
|
|
%mp_assertdsobs(work.chk1,
|
|
desc=Test1 - the filter on a derived column selects rows,
|
|
test=HASOBS,
|
|
outds=work.test_results
|
|
)
|
|
|
|
/* the chained value only resolves if the statements run in order */
|
|
data work.chk1b;
|
|
set work.vw_derived;
|
|
where BAND='TEEN';
|
|
if BAND_AGE ne cats(BAND,'-',AGE) then output;
|
|
run;
|
|
%mp_assertdsobs(work.chk1b,
|
|
desc=Test1 - a derived variable can reference an earlier one,
|
|
test=EQUALS 0,
|
|
outds=work.test_results
|
|
)
|
|
|
|
data work.chk1c;
|
|
set work.vw_derived;
|
|
if DOUBLE_AGE ne AGE*2 then output;
|
|
run;
|
|
%mp_assertdsobs(work.chk1c,
|
|
desc=Test1 - the numeric derived value is correct,
|
|
test=EQUALS 0,
|
|
outds=work.test_results
|
|
)
|
|
|
|
/* proc contents on a view reports a format name without its width, so the
|
|
format is compared on the name only */
|
|
proc contents noprint data=work.vw_derived
|
|
out=work.vw_cols(keep=name label format);
|
|
run;
|
|
data work.chk1d;
|
|
set work.vw_cols;
|
|
where name='BAND' and label='Age band' and format=:'$';
|
|
run;
|
|
%mp_assertdsobs(work.chk1d,
|
|
desc=Test1 - the label and default format are applied,
|
|
test=EQUALS 1,
|
|
outds=work.test_results
|
|
)
|
|
|
|
/**
|
|
* Test 2 - a configured format is used as-is
|
|
*/
|
|
|
|
proc sql;
|
|
insert into &dclib..mpe_derived set
|
|
tx_from=0
|
|
,tx_to='31DEC5999:23:59:59'dt
|
|
,libref='WORK'
|
|
,dsn='CLASS'
|
|
,var_nm='FLAG_DT'
|
|
,var_deps=''
|
|
,var_type='N'
|
|
,var_len=8
|
|
,var_fmt='DATE9.'
|
|
,var_logic='mdy(1,1,2020)'
|
|
,active=1;
|
|
quit;
|
|
|
|
%mpe_derived(libds=WORK.CLASS,
|
|
dclib=&dclib,
|
|
outref=derref2,
|
|
outview=work.vw_derived2,
|
|
outvars=work.derived_vars2
|
|
)
|
|
|
|
/* asserted on the emitted statement - proc contents on a view does not
|
|
report the format in full */
|
|
data work.chk2;
|
|
infile derref2 truncover;
|
|
input line $4000.;
|
|
if index(line,'format=DATE9.')>0 then output;
|
|
run;
|
|
%mp_assertdsobs(work.chk2,
|
|
desc=Test2 - the configured format is applied,
|
|
test=EQUALS 1,
|
|
outds=work.test_results
|
|
)
|
|
|
|
data work.chk2b;
|
|
set work.vw_derived2;
|
|
if flag_dt ne mdy(1,1,2020) then output;
|
|
stop;
|
|
run;
|
|
%mp_assertdsobs(work.chk2b,
|
|
desc=Test2 - the numeric value is correct,
|
|
test=EQUALS 0,
|
|
outds=work.test_results
|
|
)
|
|
|
|
/**
|
|
* Test 3 - no configuration
|
|
*/
|
|
|
|
%mpe_derived(libds=WORK.NOCONFIG,
|
|
dclib=&dclib,
|
|
outref=derref3,
|
|
outview=work.vw_derived3,
|
|
outvars=work.derived_vars3
|
|
)
|
|
|
|
%mp_assertdsobs(work.derived_vars3,
|
|
desc=Test3 - no derived variables returned,
|
|
test=EQUALS 0,
|
|
outds=work.test_results
|
|
)
|
|
|
|
%mp_assert(iftrue=(%mf_existds(libds=work.vw_derived3)=0),
|
|
desc=Test3 - no view is built when there is no config,
|
|
outds=work.test_results
|
|
)
|
|
|
|
data _null_;
|
|
infile derref3 end=eof;
|
|
input;
|
|
if not eof or cats(_infile_) ne '' then do;
|
|
putlog 'ERR' 'OR: derived statement file is not empty';
|
|
call symputx('emptycheck','0','l');
|
|
end;
|
|
run;
|
|
%mp_assert(iftrue=(%symexist(emptycheck)=0),
|
|
desc=Test3 - no statements are generated,
|
|
outds=work.test_results
|
|
)
|
|
|
|
/**
|
|
* Test 4 - the wrapper scopes the config: only the current, active row of a
|
|
* table is used
|
|
*/
|
|
|
|
proc sql;
|
|
insert into &dclib..mpe_derived set
|
|
tx_from=0
|
|
,tx_to='01JAN2020:00:00:00'dt
|
|
,libref='WORK'
|
|
,dsn='CLASS'
|
|
,var_nm='CLOSED_OFF'
|
|
,var_deps='AGE'
|
|
,var_type='N'
|
|
,var_len=8
|
|
,var_fmt=''
|
|
,var_logic='AGE + 1'
|
|
,active=1;
|
|
insert into &dclib..mpe_derived set
|
|
tx_from=0
|
|
,tx_to='31DEC5999:23:59:59'dt
|
|
,libref='WORK'
|
|
,dsn='CLASS'
|
|
,var_nm='NOT_ACTIVE'
|
|
,var_deps='AGE'
|
|
,var_type='N'
|
|
,var_len=8
|
|
,var_fmt=''
|
|
,var_logic='AGE + 2'
|
|
,active=0;
|
|
quit;
|
|
|
|
%mpe_derived(libds=WORK.CLASS,
|
|
dclib=&dclib,
|
|
outref=derref4,
|
|
outview=work.vw_derived4,
|
|
outvars=work.derived_vars4
|
|
)
|
|
|
|
data work.chk4;
|
|
set work.derived_vars4;
|
|
where name in ('CLOSED_OFF','NOT_ACTIVE');
|
|
run;
|
|
|
|
%mp_assertdsobs(work.chk4,
|
|
desc=Test4 - a closed version and an inactive row are ignored,
|
|
test=EQUALS 0,
|
|
outds=work.test_results
|
|
)
|
|
|
|
/* tidy up so repeated runs start clean */
|
|
proc sql;
|
|
delete from &dclib..mpe_derived where upcase(libref)='WORK';
|
|
quit;
|
|
|
|
%mp_assertscope(COMPARE,ignorelist=_SASJS_WEBOUT_HEADERS,outds=work.test_results)
|
|
|
|
/* Test 5 - a format catalog is not a dataset: no derived variables, no abort.
|
|
Callers pass LIBREF.CATALOGNAME-FC (validatefilter, getrawdata, viewdata), and
|
|
the engine's existence check would otherwise end the service. */
|
|
proc format library=work.dcfmts;
|
|
value $band 'A'='Alpha';
|
|
run;
|
|
|
|
%mpe_derived(libds=work.DCFMTS-FC,
|
|
dclib=&dclib,
|
|
outref=derref5,
|
|
outvars=work.derived_vars5
|
|
)
|
|
|
|
%mp_assertdsobs(work.derived_vars5,
|
|
desc=Test5 - a format catalog yields no derived variables,
|
|
test=EQUALS 0,
|
|
outds=work.test_results
|
|
)
|
|
|
|
/* The branch has to create the statement file even though it writes nothing,
|
|
because callers %include it unconditionally. Two things to prove: the branch created the file, and it left it empty.
|
|
fopen answers the first (0 when the fileref cannot be opened), the loop the
|
|
second - a single INPUT would stop the step at once on an empty file and never
|
|
reach the symputx. */
|
|
%let deropen=0;
|
|
%let derlines=-1;
|
|
data _null_;
|
|
did=fopen('derref5');
|
|
call symputx('deropen',ifn(did>0,1,0),'l');
|
|
if did>0 then rc=fclose(did);
|
|
run;
|
|
data _null_;
|
|
infile derref5 end=eof;
|
|
n=0;
|
|
do while(not eof);
|
|
input;
|
|
n+1;
|
|
end;
|
|
call symputx('derlines',n,'l');
|
|
run;
|
|
|
|
%mp_assert(
|
|
iftrue=(&deropen=1 and &derlines=0),
|
|
desc=Test5 - a format catalog leaves an empty statement file,
|
|
outds=work.test_results
|
|
)
|