Macros for SAS Application Developers
https://github.com/sasjs/core
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%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;