From 68e66e25a0a02072fffdde2449a60d0e7af1cf0f Mon Sep 17 00:00:00 2001 From: dc Date: Tue, 3 Nov 2026 01:14:29 +0000 Subject: [PATCH] test(macros): cover mpe_dirlist and mpe_writecheck directly The service tests exercise the actions through a child session; these call the macros in place, so the guard and the answers are covered without a request: mpe_dirlist 7 assertions - iftrue=0 does nothing, one level, files excluded, three levels, the depth cap, the maxobs cap with TRUNCATED=Y, and a blank parent returning a root path mpe_writecheck 5 assertions - iftrue=0 does nothing, a writable folder, a file, a missing path with a reason, and the probe leaving an existing file that already had its old name alone They live in sasjs/macros beside the macros they test, which works: a test file in a declared macro folder compiles to sasjsbuild/tests/macros/ and sasjs test runs it from there. Two traps, both hit before this passed: - these macros write JSON to _webout, so the test has to point _webout at a temp file while they run and put it back before the term program writes the results - the first attempt redirected without restoring and the runner reported "No webout was returned by job"; - descriptions in mp_assert must not contain a comma: an unquoted comma splits the parameter list and the assertion fails to compile with "All positional parameters must precede keyword parameters", which names the call rather than the comma. --- sas/sasjs/macros/mpe_dirlist.test.sas | 172 +++++++++++++++++++++++ sas/sasjs/macros/mpe_writecheck.test.sas | 136 ++++++++++++++++++ 2 files changed, 308 insertions(+) create mode 100644 sas/sasjs/macros/mpe_dirlist.test.sas create mode 100644 sas/sasjs/macros/mpe_writecheck.test.sas diff --git a/sas/sasjs/macros/mpe_dirlist.test.sas b/sas/sasjs/macros/mpe_dirlist.test.sas new file mode 100644 index 0000000..3a49b3d --- /dev/null +++ b/sas/sasjs/macros/mpe_dirlist.test.sas @@ -0,0 +1,172 @@ +/** + @file + @brief Testing mpe_dirlist macro + @details Covers the guard, the listing shape and the cap. The abort paths + (a relative parent, one carrying macro triggers) are covered by + services/admin/makedata.test.sas, because mp_abort cannot be exercised + inside the test session itself. + +

SAS Macros

+ @li mf_getuniquefileref.sas + @li mf_mkdir.sas + @li mp_assert.sas + @li mpe_dirlist.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. + + @sasjslint {"maxLineLength": 80} + +**/ + +%macro countfolders(libds,outmac); + %global &outmac; + proc sql noprint; + select count(*) into:&outmac from &libds + where filepath is not null and filepath ne ''; + quit; +%mend countfolders; + +/* + These macros write their JSON answer to _webout. Point it at a temporary + file while they run and put it back afterwards, so the test's own result - + which the term program writes to _webout - still reaches the caller. The + assertions read the work datasets the macros build, not the JSON. +*/ +%let _webout_path=%sysfunc(pathname(_webout)); +filename _webout temp; + +%let root=%sysfunc(pathname(work))/dirlist%mf_getuniquefileref(); +%mf_mkdir(&root) +%mf_mkdir(&root/aa) +%mf_mkdir(&root/aa/bb) +%mf_mkdir(&root/aa/bb/cc) +data _null_; + file "&root/plain.txt"; + put 'x'; +run; + +/** + * Test 1 - the iftrue guard: nothing happens, and nothing is created + */ +%mpe_dirlist(iftrue=0) +%mp_assert( + iftrue=(%sysfunc(exist(work.dirlist))=0), + desc=The listing does nothing when iftrue is false, + outds=work.test_results +) + +/** + * Test 2 - one level: folders only, the file beside them is not offered + */ +data work.fromjs; + length parent $1000; + parent="&root"; + depth=1; + maxobs=2000; +run; +%mpe_dirlist(iftrue=1) +%countfolders(work.dirlist,c2) +%mp_assert( + iftrue=(&c2=1), + desc=One level returns the immediate folder only, + outds=work.test_results +) +data _null_; + set work.dirlist; + call symputx('name2',filename,'l'); + call symputx('type2',file_or_folder,'l'); +run; +%mp_assert( + iftrue=("%upcase(&name2)"="AA" and "%upcase(&type2)"="FOLDER"), + desc=The file beside it is not offered, + outds=work.test_results +) + +/** + * Test 3 - depth and its cap + */ +data work.fromjs; + length parent $1000; + parent="&root"; + depth=3; + maxobs=2000; +run; +%mpe_dirlist(iftrue=1) +%countfolders(work.dirlist,c3) +%mp_assert( + iftrue=(&c3=3), + desc=Three levels are returned, + outds=work.test_results +) + +data work.fromjs; + length parent $1000; + parent="&root"; + depth=9; + maxobs=2000; +run; +%mpe_dirlist(iftrue=1) +%countfolders(work.dirlist,c4) +%mp_assert( + iftrue=(&c4=3), + desc=Depth above three is capped at three, + outds=work.test_results +) + +/** + * Test 5 - the row cap, and the flag that says it was hit + */ +data work.fromjs; + length parent $1000; + parent="&root"; + depth=3; + maxobs=2; +run; +%mpe_dirlist(iftrue=1) +%countfolders(work.dirlist,c5) +data _null_; + set work.dirinfo; + call symputx('trunc5',truncated,'l'); +run; +%mp_assert( + iftrue=(&c5=2 and "%upcase(&trunc5)"="Y"), + desc=Rows are capped by maxobs and dirinfo says so, + outds=work.test_results +) + +/** + * Test 6 - a blank parent is the platform roots + */ +data work.fromjs; + length parent $1000; + parent=''; + depth=1; + maxobs=50; +run; +%mpe_dirlist(iftrue=1) +%countfolders(work.dirlist,c6) +data _null_; + set work.dirlist(obs=1); + call symputx('rootpath6',filepath,'l'); +run; +%mp_assert( + iftrue=(&c6>=1 and "%trim(&rootpath6)" ne ""), + desc=A blank parent returns a root entry with a path, + outds=work.test_results +) + +/* + hand _webout back before the results are dumped +*/ +filename _webout "&_webout_path" mod; + +/** + * dump results to the log for offline inspection + */ +data _null_; + set work.test_results; + putlog 'TEST_RESULT_LINE: ' test_result ' - ' test_description; +run; diff --git a/sas/sasjs/macros/mpe_writecheck.test.sas b/sas/sasjs/macros/mpe_writecheck.test.sas new file mode 100644 index 0000000..1287873 --- /dev/null +++ b/sas/sasjs/macros/mpe_writecheck.test.sas @@ -0,0 +1,136 @@ +/** + @file + @brief Testing mpe_writecheck macro + @details Covers the guard, the three answers, and the probe: it must not + destroy a file that happens to carry a name it used to use. The abort paths + are covered by services/admin/makedata.test.sas, because mp_abort cannot be + exercised inside the test session itself. + +

SAS Macros

+ @li mf_getuniquefileref.sas + @li mf_mkdir.sas + @li mp_assert.sas + @li mpe_writecheck.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. + + @sasjslint {"maxLineLength": 80} + +**/ + +/* + These macros write their JSON answer to _webout. Point it at a temporary + file while they run and put it back afterwards, so the test's own result - + which the term program writes to _webout - still reaches the caller. The + assertions read the work datasets the macros build, not the JSON. +*/ +%let _webout_path=%sysfunc(pathname(_webout)); +filename _webout temp; + +%let root=%sysfunc(pathname(work))/checkwrite%mf_getuniquefileref(); +%mf_mkdir(&root) +data _null_; + file "&root/plain.txt"; + put 'x'; +run; + +/* a file whose name the macro used to pick, which must survive the probe */ +data _null_; + file "&root/.dcprobe_&sysprocessid"; + put 'do not delete me'; +run; + +/** + * Test 1 - the iftrue guard + */ +%mpe_writecheck(iftrue=0) +%mp_assert( + iftrue=(%sysfunc(exist(work.checkwrite))=0), + desc=The check does nothing when iftrue is false, + outds=work.test_results +) + +/** + * Test 2 - a folder the deployment user can write to + */ +data work.fromjs; + length path $1000; + path="&root"; +run; +%mpe_writecheck(iftrue=1) +data _null_; + set work.checkwrite; + if upcase(path_exists)='Y' and upcase(is_folder)='Y' + and upcase(writeable)='Y' then call symputx('ok2',1,'l'); + else call symputx('ok2',0,'l'); +run; +%mp_assert( + iftrue=(&ok2=1), + desc=A writable folder is reported as writeable, + outds=work.test_results +) + +/** + * Test 3 - a file is not a folder you can write a control library into + */ +data work.fromjs; + length path $1000; + path="&root/plain.txt"; +run; +%mpe_writecheck(iftrue=1) +data _null_; + set work.checkwrite; + if upcase(path_exists)='Y' and upcase(is_folder)='N' + and upcase(writeable)='N' then call symputx('ok3',1,'l'); + else call symputx('ok3',0,'l'); +run; +%mp_assert( + iftrue=(&ok3=1), + desc=A file is not reported as a writeable folder, + outds=work.test_results +) + +/** + * Test 4 - a path that is not there is a negative answer, with a reason + */ +data work.fromjs; + length path $1000; + path="&root/nothing_here"; +run; +%mpe_writecheck(iftrue=1) +data _null_; + set work.checkwrite; + if upcase(path_exists)='N' and upcase(writeable)='N' + and length(trim(msg))>0 then call symputx('ok4',1,'l'); + else call symputx('ok4',0,'l'); +run; +%mp_assert( + iftrue=(&ok4=1), + desc=A missing path is reported as missing and carries a reason, + outds=work.test_results +) + +/** + * Test 5 - the probe leaves an existing file alone + */ +%mp_assert( + iftrue=(%sysfunc(fileexist("&root/.dcprobe_&sysprocessid"))=1), + desc=The write probe does not delete a file that already had its old name, + outds=work.test_results +) + +/* + hand _webout back before the results are dumped +*/ +filename _webout "&_webout_path" mod; + +/** + * dump results to the log for offline inspection + */ +data _null_; + set work.test_results; + putlog 'TEST_RESULT_LINE: ' test_result ' - ' test_description; +run;