Loading...
Searching...
No Matches
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/**
77 * this sets up the global vars, it will also enter STRICT mode. If this
78 * behaviour is not desired, simply initiate the following global macro
79 * variable to prevent the macro from running: SASJS_PREFIX
80 */
81%mp_init()
82
83%local ds test_result test_comments del add mod ilist;
84%let ilist=%upcase(&sasjs_prefix._FUNCTIONS SYS_PROCHTTP_STATUS_CODE
85 SYS_PROCHTTP_STATUS_CODE SYS_PROCHTTP_STATUS_PHRASE &ignorelist);
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 quit;
96%end;
97%else %if &action=COMPARE %then %do;
98
99 proc sql;
100 create table _data_ as
101 select name,offset,value
102 from dictionary.macros
103 where scope="&scope" and upcase(name) not in (%mf_getquotedstr(&ilist))
104 order by name,offset;
105
106 %let ds=&syslast;
107
108 proc compare
109 base=&scopeds(where=(upcase(name) not in (%mf_getquotedstr(&ilist))))
110 compare=&ds noprint;
111 run;
112
113 %if &sysinfo=0 %then %do;
114 %let test_result=PASS;
115 %let test_comments=&scope Variables Unmodified;
116 %end;
117 %else %do;
118 proc sql noprint undo_policy=none;
119 select distinct name into: del separated by ' ' from &scopeds
120 where name not in (select name from &ds);
121 select distinct name into: add separated by ' ' from &ds
122 where name not in (select name from &scopeds);
123 select distinct a.name into: mod separated by ' '
124 from &scopeds a
125 inner join &ds b
126 on a.name=b.name
127 and a.offset=b.offset
128 where a.value ne b.value;
129 %let test_result=FAIL;
130 %let test_comments=%str(Mod:(&mod) Add:(&add) Del:(&del));
131 %end;
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 quit;
146%end;
147
148%mend mp_assertscope;