From 872353dc8d6be557a0a32b2a6e3a7832cd37b23c Mon Sep 17 00:00:00 2001 From: 4gl <@> Date: Thu, 14 May 2026 15:02:14 +0100 Subject: [PATCH 1/2] feat: new mx_append2pgm macro --- tests/x-platform/mx_append2pgm.test.sas | 192 ++++++++++++++++++++++++ xplatform/mx_append2pgm.sas | 149 ++++++++++++++++++ 2 files changed, 341 insertions(+) create mode 100644 tests/x-platform/mx_append2pgm.test.sas create mode 100644 xplatform/mx_append2pgm.sas diff --git a/tests/x-platform/mx_append2pgm.test.sas b/tests/x-platform/mx_append2pgm.test.sas new file mode 100644 index 0000000..3ef1360 --- /dev/null +++ b/tests/x-platform/mx_append2pgm.test.sas @@ -0,0 +1,192 @@ +/** + @file + @brief Testing mx_append2pgm.sas macro + + Be sure to run %let mcTestAppLoc=/Public/temp/macrocore; when + running in Studio + +

SAS Macros

+ @li mf_getplatform.sas + @li mf_uid.sas + @li mp_assert.sas + @li mp_assertscope.sas + @li ms_createfile.sas + @li mv_createfile.sas + @li mm_createstp.sas + @li mx_append2pgm.sas + @li mx_getcode.sas + +**/ + +/** + * Test 1 - Append content to an existing program and verify combined output + * Also checking for scope leakage + */ + +/* create a unique name for the program */ +%let item=test_%mf_uid(); + +/* create the initial program with some code */ +filename initpgm temp; +data _null_; + file initpgm; + put '%put ORIGINAL LINE;'; +run; + +%macro setup_pgm(); + %let platform=%mf_getplatform(); + %if &platform=SASJS %then %do; + %ms_createfile(&mcTestAppLoc/temp/&item..sas, inref=initpgm) + %end; + %else %if &platform=SASVIYA %then %do; + %mv_createfile(path=&mcTestAppLoc/temp, name=&item..sas, inref=initpgm) + %end; + %else %do; + %let work=%sysfunc(pathname(work)); + data _null_; + file "&work/&item..sas"; + infile initpgm; + input; + put _infile_; + run; + %mm_createstp(stpname=&item + ,filename=&item..sas + ,directory=&work + ,tree=&mcTestAppLoc/temp + ,stptype=2 + ,minify=NO + ) + %end; +%mend setup_pgm; +%setup_pgm() + +/* create the content to append */ +filename toappnd temp; +data _null_; + file toappnd; + put '%put APPENDED LINE;'; +run; + +/* run the macro under test with scope checks */ +%mp_assertscope(SNAPSHOT) +%mx_append2pgm(&mcTestAppLoc/temp/&item, inref=toappnd) +%mp_assertscope(COMPARE, + desc=Test 1: mx_append2pgm does not leak scope, + outds=work.test_results, + ignorelist=MC2_JADP1LEN MC2_JADP2LEN MC2_JADPNUM MC2_JADVLEN MC2_JADP3LEN +) + +%mp_assert( + iftrue=(&syscc=0), + desc=Test 1: No errors after appending content to program, + outds=work.test_results +) + +/** + * Test 2 - Verify the appended content is present + * Fetch the modified program and check both original and appended lines exist + */ + +%let test2_orig=0; +%let test2_appd=0; + +%macro verify_test2(); + %let platform=%mf_getplatform(); + %if &platform=SASVIYA %then %do; + filename verifrf filesrvc folderpath="&mcTestAppLoc/temp"; + data _null_; + infile verifrf("&item..sas") lrecl=32000; + input; + if index(_infile_,'ORIGINAL LINE') then call symputx('test2_orig','1'); + if index(_infile_,'APPENDED LINE') then call symputx('test2_appd','1'); + run; + filename verifrf clear; + %end; + %else %do; + %mx_getcode(&mcTestAppLoc/temp/&item, outref=verifrf) + data _null_; + infile verifrf lrecl=32000; + input; + if index(_infile_,'ORIGINAL LINE') then call symputx('test2_orig','1'); + if index(_infile_,'APPENDED LINE') then call symputx('test2_appd','1'); + run; + %end; +%mend verify_test2; +%verify_test2() + +%mp_assert( + iftrue=(&test2_orig=1), + desc=Test 2a: Original content is preserved after append, + outds=work.test_results +) + +%mp_assert( + iftrue=(&test2_appd=1), + desc=Test 2b: Appended content is present in modified program, + outds=work.test_results +) + +/** + * Test 3 - Append multiple times to ensure repeated appends work + */ +filename toappd2 temp; +data _null_; + file toappd2; + put '%put SECOND APPEND;'; +run; + +%mp_assertscope(SNAPSHOT) +%mx_append2pgm(&mcTestAppLoc/temp/&item, inref=toappd2) +%mp_assertscope(COMPARE, + desc=Test 3: mx_append2pgm does not leak scope on second call, + outds=work.test_results +) + +/* verify all three pieces of content exist */ +%let test3_orig=0; +%let test3_appd=0; +%let test3_app2=0; + +%macro verify_test3(); + %let platform=%mf_getplatform(); + %if &platform=SASVIYA %then %do; + filename verifr2 filesrvc folderpath="&mcTestAppLoc/temp"; + data _null_; + infile verifr2("&item..sas") lrecl=32000; + input; + if index(_infile_,'ORIGINAL LINE') then call symputx('test3_orig','1'); + if index(_infile_,'APPENDED LINE') then call symputx('test3_appd','1'); + if index(_infile_,'SECOND APPEND') then call symputx('test3_app2','1'); + run; + filename verifr2 clear; + %end; + %else %do; + %mx_getcode(&mcTestAppLoc/temp/&item, outref=verifr2) + data _null_; + infile verifr2 lrecl=32000; + input; + if index(_infile_,'ORIGINAL LINE') then call symputx('test3_orig','1'); + if index(_infile_,'APPENDED LINE') then call symputx('test3_appd','1'); + if index(_infile_,'SECOND APPEND') then call symputx('test3_app2','1'); + run; + %end; +%mend verify_test3; +%verify_test3() + +%mp_assert( + iftrue=(&test3_orig=1), + desc=Test 3a: Original content still present after second append, + outds=work.test_results +) + +%mp_assert( + iftrue=(&test3_appd=1), + desc=Test 3b: First appended content still present after second append, + outds=work.test_results +) + +%mp_assert( + iftrue=(&test3_app2=1), + desc=Test 3c: Second appended content is present, + outds=work.test_results +) diff --git a/xplatform/mx_append2pgm.sas b/xplatform/mx_append2pgm.sas new file mode 100644 index 0000000..65e21d2 --- /dev/null +++ b/xplatform/mx_append2pgm.sas @@ -0,0 +1,149 @@ +/** + @file + @brief Appends a text file to a SASjs Stored Program, Viya SAS program, or + SAS 9 Stored Process + @details Extracts the source code from a SASjs Stored Program, Viya SAS + program (file in SAS Drive), or SAS 9 Stored Process, appends the contents + of a provided text file, then deletes and recreates the target item with the + combined content. + + This is useful for dynamically modifying deployed programs, for example to + add test-specific configuration or runtime settings. + + Usage: + + %* compile macros ; + filename mc url + "https://raw.githubusercontent.com/sasjs/core/main/all.sas"; + %inc mc; + + %* write some content to append; + filename append temp; + data _null_; + file append; + put "libname mylib '/some/path';"; + run; + + %* append to existing program; + %mx_append2pgm(/Public/app/common/settings, inref=append) + + @param [in] loc The full path to the Viya SAS program, SAS 9 Stored Process, + or SASjs Stored Program in Drive or Metadata, WITHOUT the .sas extension + (SASjs only) + @param [in] inref= (0) Fileref pointing to the content to be appended to the + target program. + @param [in] mdebug= (0) Set to 1 to show debug messages in the log + +

SAS Macros

+ @li mf_getplatform.sas + @li mf_getuniquefileref.sas + @li mm_createstp.sas + @li mm_deletestp.sas + @li mm_getstpcode.sas + @li ms_createfile.sas + @li ms_deletefile.sas + @li mv_createfile.sas + @li mv_deletefoldermember.sas + @li mx_getcode.sas + +

Related Macros

+ @li mx_append2pgm.test.sas + @li mx_getcode.sas + @li mx_createjob.sas + + @author Allan Bowe + +**/ + +%macro mx_append2pgm(loc + ,inref=0 + ,mdebug=0 +)/*/STORE SOURCE*/; + +%local platform name shortloc coderef combref work tmpfile viyaref; +%let platform=%mf_getplatform(); + +%if &mdebug=1 %then %do; + %put &sysmacroname entry vars:; + %put _local_; +%end; +%if &syscc ne 0 %then %do; + %put syscc=&syscc - &sysmacroname will not execute in this state; + %return; +%end; + +/* extract name & path from loc */ +data _null_; + length name shortloc $500; + loc=symget('loc'); + name=scan(loc,-1,'/'); + shortloc=substr(loc,1,length(loc)-length(name)-1); + call symputx('name',name,'l'); + call symputx('shortloc',shortloc,'l'); +run; + +/* create a combined fileref with original + appended content */ +%let combref=%mf_getuniquefileref(); +%let work=%sysfunc(pathname(work)); +%let tmpfile=&combref..sas; +filename &combref "&work/&tmpfile" lrecl=32000; + +%if &platform=SASVIYA %then %do; + /* On Viya, read the SAS program file from SAS Drive using filesrvc */ + %let viyaref=%mf_getuniquefileref(); + filename &viyaref filesrvc folderpath="&shortloc"; + data _null_; + file &combref lrecl=32000 termstr=crlf; + infile &viyaref("&name..sas") lrecl=32000 end=last; + input; + put _infile_; + run; + filename &viyaref clear; + %symdel _FILESRVC_&viyaref._URI; +%end; +%else %do; + /* For SAS9 and SASJS, use mx_getcode */ + %let coderef=%mf_getuniquefileref(); + %mx_getcode(&loc, outref=&coderef) + data _null_; + file &combref lrecl=32000 termstr=crlf; + infile &coderef lrecl=32000 end=last; + input; + put _infile_; + run; + filename &coderef clear; +%end; + +/* append the new content */ +data _null_; + file &combref lrecl=32000 termstr=crlf mod; + infile &inref lrecl=32000; + input; + put _infile_; +run; + +/* delete and recreate the target item */ +%if &platform=SASJS %then %do; + %ms_deletefile(&loc..sas) + %ms_createfile(&loc..sas, inref=&combref, mdebug=&mdebug) +%end; +%else %if &platform=SASVIYA %then %do; + %mv_deletefoldermember(path=&shortloc, name=&name..sas, contenttype=file) + %mv_createfile(path=&shortloc, name=&name..sas, inref=&combref) +%end; +%else %do; + /* SAS 9 */ + %mm_deletestp(target=&loc) + %mm_createstp(stpname=&name + ,filename=&tmpfile + ,directory=&work + ,tree=&shortloc + ,stptype=2 + ,mDebug=&mdebug + ,minify=NO + ) +%end; + +filename &combref clear; + +%mend mx_append2pgm; From c8e1126a4072ae17fa13f1b011323bc4c328668e Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 14 May 2026 14:02:39 +0000 Subject: [PATCH 2/2] chore: updating all.sas --- all.sas | 149 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) diff --git a/all.sas b/all.sas index 7435be1..b3d8a0e 100644 --- a/all.sas +++ b/all.sas @@ -31469,6 +31469,155 @@ endsub; %end; %mend mcf_string2file;/** + @file + @brief Appends a text file to a SASjs Stored Program, Viya SAS program, or + SAS 9 Stored Process + @details Extracts the source code from a SASjs Stored Program, Viya SAS + program (file in SAS Drive), or SAS 9 Stored Process, appends the contents + of a provided text file, then deletes and recreates the target item with the + combined content. + + This is useful for dynamically modifying deployed programs, for example to + add test-specific configuration or runtime settings. + + Usage: + + %* compile macros ; + filename mc url + "https://raw.githubusercontent.com/sasjs/core/main/all.sas"; + %inc mc; + + %* write some content to append; + filename append temp; + data _null_; + file append; + put "libname mylib '/some/path';"; + run; + + %* append to existing program; + %mx_append2pgm(/Public/app/common/settings, inref=append) + + @param [in] loc The full path to the Viya SAS program, SAS 9 Stored Process, + or SASjs Stored Program in Drive or Metadata, WITHOUT the .sas extension + (SASjs only) + @param [in] inref= (0) Fileref pointing to the content to be appended to the + target program. + @param [in] mdebug= (0) Set to 1 to show debug messages in the log + +

SAS Macros

+ @li mf_getplatform.sas + @li mf_getuniquefileref.sas + @li mm_createstp.sas + @li mm_deletestp.sas + @li mm_getstpcode.sas + @li ms_createfile.sas + @li ms_deletefile.sas + @li mv_createfile.sas + @li mv_deletefoldermember.sas + @li mx_getcode.sas + +

Related Macros

+ @li mx_append2pgm.test.sas + @li mx_getcode.sas + @li mx_createjob.sas + + @author Allan Bowe + +**/ + +%macro mx_append2pgm(loc + ,inref=0 + ,mdebug=0 +)/*/STORE SOURCE*/; + +%local platform name shortloc coderef combref work tmpfile viyaref; +%let platform=%mf_getplatform(); + +%if &mdebug=1 %then %do; + %put &sysmacroname entry vars:; + %put _local_; +%end; +%if &syscc ne 0 %then %do; + %put syscc=&syscc - &sysmacroname will not execute in this state; + %return; +%end; + +/* extract name & path from loc */ +data _null_; + length name shortloc $500; + loc=symget('loc'); + name=scan(loc,-1,'/'); + shortloc=substr(loc,1,length(loc)-length(name)-1); + call symputx('name',name,'l'); + call symputx('shortloc',shortloc,'l'); +run; + +/* create a combined fileref with original + appended content */ +%let combref=%mf_getuniquefileref(); +%let work=%sysfunc(pathname(work)); +%let tmpfile=&combref..sas; +filename &combref "&work/&tmpfile" lrecl=32000; + +%if &platform=SASVIYA %then %do; + /* On Viya, read the SAS program file from SAS Drive using filesrvc */ + %let viyaref=%mf_getuniquefileref(); + filename &viyaref filesrvc folderpath="&shortloc"; + data _null_; + file &combref lrecl=32000 termstr=crlf; + infile &viyaref("&name..sas") lrecl=32000 end=last; + input; + put _infile_; + run; + filename &viyaref clear; + %symdel _FILESRVC_&viyaref._URI; +%end; +%else %do; + /* For SAS9 and SASJS, use mx_getcode */ + %let coderef=%mf_getuniquefileref(); + %mx_getcode(&loc, outref=&coderef) + data _null_; + file &combref lrecl=32000 termstr=crlf; + infile &coderef lrecl=32000 end=last; + input; + put _infile_; + run; + filename &coderef clear; +%end; + +/* append the new content */ +data _null_; + file &combref lrecl=32000 termstr=crlf mod; + infile &inref lrecl=32000; + input; + put _infile_; +run; + +/* delete and recreate the target item */ +%if &platform=SASJS %then %do; + %ms_deletefile(&loc..sas) + %ms_createfile(&loc..sas, inref=&combref, mdebug=&mdebug) +%end; +%else %if &platform=SASVIYA %then %do; + %mv_deletefoldermember(path=&shortloc, name=&name..sas, contenttype=file) + %mv_createfile(path=&shortloc, name=&name..sas, inref=&combref) +%end; +%else %do; + /* SAS 9 */ + %mm_deletestp(target=&loc) + %mm_createstp(stpname=&name + ,filename=&tmpfile + ,directory=&work + ,tree=&shortloc + ,stptype=2 + ,mDebug=&mdebug + ,minify=NO + ) +%end; + +filename &combref clear; + +%mend mx_append2pgm; +/** @file mx_createjob.sas @brief Create a job in SAS 9, Viya or SASjs @details Creates a Stored Process in SAS 9, a Job Execution Service in SAS