Macros for SAS Application Developers
https://github.com/sasjs/core
mp_assertscope.sas
Go to the documentation of this file.
1 /**
2  @file
3  @brief Used to capture scope leakage of macro variables
4  @details
5 
6  A common 'difficult to detect' bug in macros is where a nested macro
7  over-writes variables in a higher level macro.
8 
9  This assertion takes a snapshot of the macro variables before and after
10  a macro invocation. Differences are captured in the `&outds` table. This
11  makes it easy to detect whether any macro variables were modified or
12  changed.
13 
14  The following variables are NOT tested (as they are known, global variables
15  used in SASjs):
16 
17  @li &sasjs_prefix._FUNCTIONS
18 
19  Global variables are initialised in mp_init.sas - which will also trigger
20  "strict mode" in your SAS session. Whilst this is a default in SASjs
21  produced apps, if you prefer not to use this mode, simply instantiate the
22  following variable to prevent the macro from running: `SASJS_PREFIX`
23 
24  Example usage:
25 
26  %mp_assertscope(SNAPSHOT)
27 
28  %let oops=I did it again;
29 
30  %mp_assertscope(COMPARE,
31  desc=Checking macro variables against previous snapshot
32  )
33 
34  This macro is designed to work alongside `sasjs test` - for more information
35  about this facility, visit [cli.sasjs.io/test](https://cli.sasjs.io/test).
36 
37  @param [in] action (SNAPSHOT) The action to take. Valid values:
38  @li SNAPSHOT - take a copy of the current macro variables
39  @li COMPARE - compare the current macro variables against previous values
40  @param [in] scope= (GLOBAL) The scope of the variables to be checked. This
41  corresponds to the values in the SCOPE column in `sashelp.vmacro`.
42  @param [in] desc= (Testing scope leakage) The user provided test description
43  @param [in] ignorelist= Provide a list of macro variable names to ignore from
44  the comparison
45  @param [in,out] scopeds= (work.mp_assertscope) The dataset to contain the
46  scope snapshot
47  @param [out] outds= (work.test_results) The output dataset to contain the
48  results. If it does not exist, it will be created, with the following format:
49  |TEST_DESCRIPTION:$256|TEST_RESULT:$4|TEST_COMMENTS:$256|
50  |---|---|---|
51  |User Provided description|PASS|No out of scope variables created or modified|
52 
53  <h4> SAS Macros </h4>
54  @li mf_getquotedstr.sas
55  @li mp_init.sas
56 
57  <h4> Related Macros </h4>
58  @li mp_assert.sas
59  @li mp_assertcols.sas
60  @li mp_assertcolvals.sas
61  @li mp_assertdsobs.sas
62  @li mp_assertscope.test.sas
63 
64  @version 9.2
65  @author Allan Bowe
66 
67 **/
68 
69 %macro mp_assertscope(action,
70  desc=Testing Scope Leakage,
71  scope=GLOBAL,
72  scopeds=work.mp_assertscope,
73  ignorelist=,
74  outds=work.test_results
75 )/*/STORE SOURCE*/;
76 %local ds test_result test_comments del add mod ilist;
77 %let ilist=%upcase(&sasjs_prefix._FUNCTIONS SYS_PROCHTTP_STATUS_CODE
78  SYS_PROCHTTP_STATUS_CODE SYS_PROCHTTP_STATUS_PHRASE &ignorelist);
79 
80 /**
81  * this sets up the global vars, it will also enter STRICT mode. If this
82  * behaviour is not desired, simply initiate the following global macro
83  * variable to prevent the macro from running: SASJS_PREFIX
84  */
85 %mp_init()
86 
87 /* get current variables */
88 %if &action=SNAPSHOT %then %do;
89  proc sql;
90  create table &scopeds as
91  select name,offset,value
92  from dictionary.macros
93  where scope="&scope" and upcase(name) not in (%mf_getquotedstr(&ilist))
94  order by name,offset;
95 %end;
96 %else %if &action=COMPARE %then %do;
97 
98  proc sql;
99  create table _data_ as
100  select name,offset,value
101  from dictionary.macros
102  where scope="&scope" and upcase(name) not in (%mf_getquotedstr(&ilist))
103  order by name,offset;
104 
105  %let ds=&syslast;
106 
107  proc compare
108  base=&scopeds(where=(upcase(name) not in (%mf_getquotedstr(&ilist))))
109  compare=&ds noprint;
110  run;
111 
112  %if &sysinfo=0 %then %do;
113  %let test_result=PASS;
114  %let test_comments=&scope Variables Unmodified;
115  %end;
116  %else %do;
117  proc sql noprint undo_policy=none;
118  select distinct name into: del separated by ' ' from &scopeds
119  where name not in (select name from &ds);
120  select distinct name into: add separated by ' ' from &ds
121  where name not in (select name from &scopeds);
122  select distinct a.name into: mod separated by ' '
123  from &scopeds a
124  inner join &ds b
125  on a.name=b.name
126  and a.offset=b.offset
127  where a.value ne b.value;
128  %let test_result=FAIL;
129  %let test_comments=%str(Mod:(&mod) Add:(&add) Del:(&del));
130  %end;
131 
132 
133  data ;
134  length test_description $256 test_result $4 test_comments $256;
135  test_description=symget('desc');
136  test_comments=symget('test_comments');
137  test_result=symget('test_result');
138  run;
139 
140  %let ds=&syslast;
141  proc append base=&outds data=&ds;
142  run;
143  proc sql;
144  drop table &ds;
145 %end;
146 
147 %mend mp_assertscope;