diff --git a/.agents/docs/tests.md b/.agents/docs/tests.md new file mode 100644 index 0000000..a448a13 --- /dev/null +++ b/.agents/docs/tests.md @@ -0,0 +1,59 @@ +# Testing in @sasjs/core + +## Overview + +Tests are executed on a real SAS server using the SASjs CLI (`sasjs test`), not locally. Each test is a self-contained `.sas` file that is submitted to the server, and results are collected in the `sasjsresults` folder. + +## Running Tests + +```bash +npm test # runs: npx @sasjs/cli test -t server +``` + +The `-t server` flag selects the target (server type) from `sasjs/sasjsconfig.json`. + +## Test Structure + +Test files live under `tests/` in subfolders by platform applicability: + +- `tests/base` — run on all platforms (SAS 9 and Viya) +- `tests/sas9only` — SAS 9 only (metadata server macros) +- `tests/viyaonly` — Viya only +- `tests/serveronly` — SASjs Server only +- `tests/x-platform` — cross-platform (both SAS 9 and Viya) +- `tests/ddlonly` — DDL-related tests + +Naming convention: `.test.sas`, with numbered variants (`.test.1.sas`, `.test.2.sas`, ...) for multiple tests of the same macro. File names are lowercase, matching the lint rules. + +## Test Flow + +1. **Init**: `tests/testinit.sas` runs before every test (configured in `sasjsconfig.json` under `testConfig.initProgram`). It sets up a unique app location (`mcTestAppLoc`), the compute context, calls `%mp_init()`, and enables debug options when `_debug` is set. +2. **Test body**: the test file itself runs. It should use `%mp_assert()` to record results into `work.test_results`: + +```sas +%mp_assert( + iftrue=(&syscc=0), + desc=Checking for error condition, + outds=work.test_results +) +``` + +3. **Term**: `tests/testterm.sas` runs after every test (`testConfig.termProgram`). It adds a final assertion that `&syscc=0`, then writes the results as JSON via `%webout(OPEN) / %webout(OBJ,TEST_RESULTS) / %webout(CLOSE)`. + +## Results + +After a test run, check the `sasjsresults` folder: + +- `testResults.json` / `testResults.xml` / `testResults.csv` — per-test PASS/FAIL with descriptions and comments +- `logs/.log` — the full SAS log for each test; check here first when a test fails +- `coverage.lcov` — coverage data + +## Writing Tests — Things to Know + +- Tests follow the same Doxygen header and lint standards as regular macros (`@file`, `@brief`, `

SAS Macros

` listing macros used). +- Macro *calls* are not terminated with semicolons: use `%mp_assert(...)` not `%mp_assert(...);`. +- Use `%mp_assert(iftrue=(...), desc=..., outds=work.test_results)` for every check — always append to `work.test_results`. +- When comparing datasets after a round trip (eg through JSON), do not assert `proc compare` SYSINFO=0 directly — SYSINFO is a bitmask that includes attribute differences (length, format, label) which round trips legitimately change. Mask it to data-related bits only (64=missing obs in compare, 128=extra obs in compare, 4096=unequal values, 32768=obs count differs). Note `SYSINFO` is a read-only automatic macro variable, so store the masked value in a new variable. +- Be careful with character data round trips: `cats()` and the plain `$` informat strip leading blanks; use `trim()` and `$char` where leading blanks must be preserved. +- After any change, run `npx sasjs lint`. +- Do not edit generated copies under `sasjsbuild/` — they are refreshed by the CI build. diff --git a/.npmignore b/.npmignore index c9a7666..545ecea 100644 --- a/.npmignore +++ b/.npmignore @@ -8,3 +8,4 @@ sasjs/ make_singlefile.sh *.md .all-contributorsrc +.agents \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 487194c..2477ab0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,6 +25,12 @@ This repo is the SASjs Macro Core library — a collection of MIT-licensed, prod - Macro variables must always be local, to prevent scope leakage. +## Testing +- Read `.agents/docs/tests.md` for details on how the testing process works (how to run tests, structure, assertions, and where to find logs/results). + +## Markdown Files +- Markdown files must not use word-wrap: never insert carriage returns mid-sentence. Each sentence/paragraph stays on one line. + ## Build / Generated Files - Do not run the build script locally; it is executed in the CI/CD pipeline. - Generated files, including the consolidated `all.sas`, the per-folder `mc_*.sas` files, and the LUA macro wrappers in the `lua` folder, can generally be ignored unless the pipeline requires an update. Do not edit generated files by hand. diff --git a/meta/mm_createwebservice.sas b/meta/mm_createwebservice.sas index 5c30c69..e7c1592 100644 --- a/meta/mm_createwebservice.sas +++ b/meta/mm_createwebservice.sas @@ -355,7 +355,8 @@ data _null_; put ' prxchange(''s/\\/\\\\/'',-1,&&name&i) '; put ' )))))))))))))!!''"''; '; put ' end; '; - put ' else &&name&i=quote(cats(&&name&i)); '; + put ' /* trim (not cats) so leading blanks are retained */ '; + put ' else &&name&i=''"''!!trim(&&name&i)!!''"''; '; put ' %end; '; put ' %end; '; put ' run; '; @@ -474,7 +475,10 @@ data _null_; put ' data _null_; '; put ' infile &&_webin_fileref&i termstr=crlf; '; put ' input; '; - put ' call symputx(''input_statement'',_infile_); '; + put ' /* a plain $ informat strips leading blanks - use $char instead */ '; + put ' call symputx(''input_statement'' '; + put ' ,prxchange(''s/:\$(?=[0-9 ])/:\$char/i'',-1,_infile_) '; + put ' ); '; put ' putlog "&&_webin_name&i input statement: " _infile_; '; put ' stop; '; put ' data &&_webin_name&i; '; diff --git a/server/ms_createwebservice.sas b/server/ms_createwebservice.sas index 351f0d6..2950f06 100644 --- a/server/ms_createwebservice.sas +++ b/server/ms_createwebservice.sas @@ -355,7 +355,8 @@ data _null_; put ' prxchange(''s/\\/\\\\/'',-1,&&name&i) '; put ' )))))))))))))!!''"''; '; put ' end; '; - put ' else &&name&i=quote(cats(&&name&i)); '; + put ' /* trim (not cats) so leading blanks are retained */ '; + put ' else &&name&i=''"''!!trim(&&name&i)!!''"''; '; put ' %end; '; put ' %end; '; put ' run; '; @@ -472,7 +473,10 @@ data _null_; put ' data _null_; '; put ' infile &&_webin_fileref&i termstr=crlf lrecl=32767; '; put ' input; '; - put ' call symputx(''input_statement'',_infile_); '; + put ' /* a plain $ informat strips leading blanks - use $char instead */ '; + put ' call symputx(''input_statement'' '; + put ' ,prxchange(''s/:\$(?=[0-9 ])/:\$char/i'',-1,_infile_) '; + put ' ); '; put ' putlog "&&_webin_name&i input statement: " _infile_; '; put ' stop; '; put ' data &&_webin_name&i; '; diff --git a/tests/base/mp_jsonout.test.3.sas b/tests/base/mp_jsonout.test.3.sas index 7ee5af2..56bbf03 100644 --- a/tests/base/mp_jsonout.test.3.sas +++ b/tests/base/mp_jsonout.test.3.sas @@ -57,8 +57,19 @@ describe table web.demo; proc compare base=work.demo compare=web.demo(keep=x); quit; +/* sysinfo is a bitmask - keep only data-related bits, ie: + 64 Base data set has observation not in comparison + 128 Comparison data set has observation not in base + 4096 A value comparison was unequal + 32768 Number of observations differ + Attribute diffs (eg 16 - variable length) are ignored, as a JSON + round trip will not preserve lengths/formats/labels. +*/ +/* SYSINFO is read only, so store the masked value in a new variable */ +%let sysinfo_masked=%sysfunc(band(&sysinfo, 64+128+4096+32768)); + %mp_assert( - iftrue=(&sysinfo=0), + iftrue=(&sysinfo_masked=0), desc=Returned json is identical to input table for all special chars, outds=work.test_results )