From 06fa34bfcd8e791c452b0f8a2c1422254cf13c71 Mon Sep 17 00:00:00 2001 From: jenner-analytics <299455446+jenner-analytics@users.noreply.github.com> Date: Sat, 11 Jul 2026 03:58:17 +0000 Subject: [PATCH] jenner-check: add 2 Jenner compatibility bundles + runner Adds a self-contained jenner-check/ directory with two small bundles derived from this repo's own code, plus a bash runner (run_jenner.sh) and README: - t002_legumes_reshape : the documentation example's wide-to-long reshape (SET + IN= + RENAME= stacking legumes_rouges/salades into quantite), run against a small inline sample. - t003_macro_opendata : callers for the %batch_file and %opendata macros, producing the OPENMICRODATA/OPENMETADATA Tau-Argus batch lines. Nothing outside jenner-check/ is touched; nothing runs on merge. --- jenner-check/.gitignore | 2 + jenner-check/README.md | 76 ++++ jenner-check/run_jenner.sh | 357 ++++++++++++++++++ .../t002_legumes_reshape/autoexec.sas | 2 + .../t002_legumes_reshape/expected.json | 14 + .../t002_legumes_reshape/expected/log.txt | 28 ++ .../t002_legumes_reshape/expected/output.txt | 20 + jenner-check/t002_legumes_reshape/meta.json | 7 + jenner-check/t002_legumes_reshape/script.sas | 42 +++ jenner-check/t003_macro_opendata/autoexec.sas | 2 + .../t003_macro_opendata/expected.json | 14 + .../t003_macro_opendata/expected/log.txt | 42 +++ .../t003_macro_opendata/expected/output.txt | 6 + jenner-check/t003_macro_opendata/meta.json | 7 + jenner-check/t003_macro_opendata/script.sas | 78 ++++ 15 files changed, 697 insertions(+) create mode 100644 jenner-check/.gitignore create mode 100644 jenner-check/README.md create mode 100755 jenner-check/run_jenner.sh create mode 100644 jenner-check/t002_legumes_reshape/autoexec.sas create mode 100644 jenner-check/t002_legumes_reshape/expected.json create mode 100644 jenner-check/t002_legumes_reshape/expected/log.txt create mode 100644 jenner-check/t002_legumes_reshape/expected/output.txt create mode 100644 jenner-check/t002_legumes_reshape/meta.json create mode 100644 jenner-check/t002_legumes_reshape/script.sas create mode 100644 jenner-check/t003_macro_opendata/autoexec.sas create mode 100644 jenner-check/t003_macro_opendata/expected.json create mode 100644 jenner-check/t003_macro_opendata/expected/log.txt create mode 100644 jenner-check/t003_macro_opendata/expected/output.txt create mode 100644 jenner-check/t003_macro_opendata/meta.json create mode 100644 jenner-check/t003_macro_opendata/script.sas diff --git a/jenner-check/.gitignore b/jenner-check/.gitignore new file mode 100644 index 0000000..75fb4a3 --- /dev/null +++ b/jenner-check/.gitignore @@ -0,0 +1,2 @@ +*_response.json +response.json diff --git a/jenner-check/README.md b/jenner-check/README.md new file mode 100644 index 0000000..58459d6 --- /dev/null +++ b/jenner-check/README.md @@ -0,0 +1,76 @@ +# jenner-check: compatibility test bundles + +Each `tNNN_/` subdirectory is a self-contained test bundle built from SAS +code that already lives in this repository. A bundle pins the output of a +captured passing run so the same script can be re-run later and compared +against that snapshot. + +This directory is fully self-contained: nothing outside `jenner-check/` is +referenced or modified, nothing runs on merge or checkout, and deleting the +directory removes every trace of it. + +## What the runner sends — read before running + +`run_jenner.sh` uploads only a bundle's SAS **source text** — `autoexec.sas` +plus `script.sas`, and nothing else — over HTTPS to `api.jenneranalytics.com`, +where it runs and returns the log and listing. It does not read or upload any +data files, so anything sitting next to a script stays on your machine; the +only thing transmitted is the code you run, as when pasting a snippet into any +hosted tool. Nothing is sent unless you run a command yourself. To point the +runner at a different endpoint, set `JENNER_HOST`. + +## What's in here + +``` +jenner-check/ +├── README.md # this file +├── run_jenner.sh # runner (bash + curl; python3 used if present) +└── tNNN_/ + ├── script.sas # the SAS under test (derived from this repo) + ├── autoexec.sas # run options prepended to script.sas at run time + ├── expected.json # fields pinned from the captured passing run + ├── meta.json # provenance: source file, blob sha, commit + └── expected/ # human-readable snapshot of that run + ├── log.txt + └── output.txt +``` + +`meta.json` records the source path, blob sha, and commit of the script each +bundle was built from, so you can verify the copy matches your code. + +## How to run + +From inside this directory: + +```bash +./run_jenner.sh --list # show the bundles in this directory +./run_jenner.sh --all # run every bundle, verify pinned fields +./run_jenner.sh tNNN_ # run just one (use a name from --list) +``` + +For each bundle the runner submits the SAS, writes the JSON response to +`_response.json`, and compares the response against the bundle's +`expected.json` — status, exit code, and the pinned log lines. A bundle +passes only when every pinned field matches, so an `N pass, 0 fail` summary +from `--all` means the captured results still hold. + +You can also compare a saved response offline, with no network call: + +```bash +./run_jenner.sh --compare tNNN__response.json tNNN_/expected.json +``` + +Requirements: `bash` 4+ and `curl` (both ship with mainstream Linux and +macOS); `python3` for the pinned-field comparison. On Windows, run it under +WSL. + +## Removing this directory + +`git rm -r jenner-check/` (or just delete the folder). Nothing else in the +repository references it. + +--- + +`run_jenner.sh` and this README are provided under the MIT license. Each +bundle's `script.sas` is a copy of code that already lives in this repository +and remains under this repository's own license — nothing here relicenses it. diff --git a/jenner-check/run_jenner.sh b/jenner-check/run_jenner.sh new file mode 100755 index 0000000..ad7825a --- /dev/null +++ b/jenner-check/run_jenner.sh @@ -0,0 +1,357 @@ +#!/usr/bin/env bash +# run_jenner.sh - mac/linux runner for Jenner compatibility checks. +# +# Quick start: +# cd jenner-check/ +# ./run_jenner.sh # lists bundles in the current dir +# ./run_jenner.sh t001_something # run that one +# ./run_jenner.sh --all # run every bundle in the current dir +# +# Usage: ./run_jenner.sh [bundle-dir | script.sas | --all | --list] [response.json] +# ./run_jenner.sh --compare RESPONSE.json EXPECTED.json +# +# (no arg) If the current directory has tNNN_* bundles, list them +# with a copy-paste command. Otherwise show this help. +# +# --all Run every tNNN_* bundle in the current directory in +# sequence, print a pass/fail summary. +# +# --list, -l List the bundles visible in the current directory and +# exit without running anything. +# +# --compare Purely local, no network: check RESPONSE.json against the +# pinned EXPECTED.json and print one line per check. Only +# keys present in EXPECTED.json are checked (keys starting +# with "_" are provenance and ignored): status / exit_code +# must match exactly, every log_contains entry must appear +# in the response log, no log_does_not_contain entry may +# appear, output_contains entries must appear in the output, +# and pinned diagnostics lists must match exactly. Unknown +# keys print a warning and do not fail. +# Exit 0 all-match, 1 any mismatch, 2 usage/unreadable file/ +# invalid JSON, 5 python3 unavailable. +# +# bundle-dir A directory containing script.sas and (optionally) +# autoexec.sas. The two are concatenated (autoexec first, +# then a blank line, then script) and submitted together. +# This is the normal case. +# +# script.sas A single .sas file. Submitted as-is — no autoexec. +# +# The API response is written to (or response.json in +# the current directory if omitted) and the most useful fields are also +# printed to stdout for a quick sanity check. +# +# What "pass" means: a bundle run passes only if the POST returns HTTP 200 +# AND — when the bundle carries an expected.json and python3 is available — +# the fresh response matches every pinned field (the --compare semantics +# above). Without python3 the pinned comparison is skipped with a notice +# and the run falls back to the old HTTP/status-only behavior. +# +# Requires: bash 4+, curl. Both ship with every mainstream Linux distro +# and macOS 12+. python3 (present on virtually every modern system) is +# optional but needed for the pinned-result comparison and the pretty +# summary. Windows: use run_jenner.bat (single-file mode) or WSL. +# +# IMPORTANT: execute this script, don't source it. Running with `. ./...` +# or `source ./...` will short-circuit error handling and can close your +# terminal if an error path fires. + +# --- refuse to be sourced ------------------------------------------------ +# `return` only works inside a sourced script. If we ARE sourced, print a +# message and return 1 so we don't kill the parent shell with exit. If +# we're running directly, (return 0) fails and we fall through. +(return 0 2>/dev/null) && { + printf 'run_jenner.sh: execute this script, do not source it.\n ./run_jenner.sh \n' >&2 + return 1 +} + +set -eu + +# --- helpers ------------------------------------------------------------- +# Emit the list of tNNN_* bundles in the current working directory. A +# "bundle" is a directory matching t[0-9]*_* whose name contains a +# script.sas file. Writes one path per line (no prefix); empty output +# if nothing found. +list_bundles_here() { + local d + for d in ./t[0-9]*_*/ ; do + [[ -d "$d" && -f "$d/script.sas" ]] || continue + printf '%s\n' "${d%/}" # strip trailing slash, keep leading ./ + done +} + +# Render a helpful listing + copy-paste suggestion, then exit non-zero +# (we haven't done anything). Used when the user runs with no args. +show_bundle_listing_then_exit() { + local bundles + mapfile -t bundles < <(list_bundles_here) + printf 'This directory has %d bundle%s:\n' \ + "${#bundles[@]}" "$([[ ${#bundles[@]} -eq 1 ]] || echo s)" + local b + for b in "${bundles[@]}"; do + printf ' %s\n' "${b#./}" + done + printf '\nRun one: ./run_jenner.sh %s\n' "${bundles[0]#./}" + printf 'Run them all: ./run_jenner.sh --all\n' + printf 'Just list: ./run_jenner.sh --list\n' + exit 2 +} + +# Show the usage block when we have nothing better to offer. +show_usage_then_exit() { + local status=${1:-2} + { + printf 'Usage: %s [bundle-dir | script.sas | --all | --list] [response.json]\n' "$(basename "$0")" + printf ' %s --compare RESPONSE.json EXPECTED.json\n\n' "$(basename "$0")" + printf 'Examples:\n' + printf ' %s t001_my_bundle # run one bundle\n' "$(basename "$0")" + printf ' %s --all # run every tNNN_* bundle in this dir\n' "$(basename "$0")" + printf ' %s path/to/script.sas # run a single file, no autoexec\n' "$(basename "$0")" + printf ' %s --compare r.json e.json # check a response against pinned fields\n' "$(basename "$0")" + } >&2 + exit "$status" +} + +# Compare a response.json against a pinned expected.json. Purely local — +# no network. Only keys present in expected.json are checked; keys whose +# name starts with "_" are capture provenance and are ignored. Prints one +# line per check (ok/FAIL/warn). Return codes: 0 all checks match, +# 1 any mismatch, 2 unreadable file or invalid JSON, 5 python3 missing. +compare_response_to_expected() { + local resp_file=$1 expected_file=$2 + if ! command -v python3 >/dev/null 2>&1; then + printf 'error: the pinned comparison needs python3, which was not found\n' >&2 + return 5 + fi + python3 - "$resp_file" "$expected_file" <<'PY' +import json, sys + +def load(path, label): + try: + with open(path, encoding="utf-8") as f: + return json.load(f) + except (OSError, ValueError) as e: + print(f"compare: cannot read {label} file {path}: {e}", file=sys.stderr) + sys.exit(2) + +resp = load(sys.argv[1], "response") +exp = load(sys.argv[2], "expected") +if not isinstance(resp, dict) or not isinstance(exp, dict): + print("compare: both files must contain a JSON object", file=sys.stderr) + sys.exit(2) + +failures = 0 + +def ok(msg): + print(f" ok {msg}") + +def bad(msg): + global failures + failures += 1 + print(f" FAIL {msg}") + +def as_list(v): + return v if isinstance(v, list) else [v] + +log = resp.get("log") or "" +output = resp.get("output") or "" + +for key, want in exp.items(): + if key.startswith("_"): + continue # provenance (_captured_at, ...), not a check + if key in ("status", "exit_code"): + got = resp.get(key) + if got == want: + ok(f"{key} == {want!r}") + else: + bad(f"{key}: expected {want!r}, got {got!r}") + elif key == "log_contains": + for s in as_list(want): + if s in log: + ok(f"log contains {s!r}") + else: + bad(f"log_contains: log is missing {s!r}") + elif key == "log_does_not_contain": + for s in as_list(want): + if s not in log: + ok(f"log does not contain {s!r}") + else: + bad(f"log_does_not_contain: log unexpectedly contains {s!r}") + elif key == "output_contains": + for s in as_list(want): + if s in output: + ok(f"output contains {s!r}") + else: + bad(f"output_contains: output is missing {s!r}") + elif key == "diagnostics": + diag = resp.get("diagnostics") or {} + for dk, dv in (want or {}).items(): + if dk.startswith("_"): + continue + got = diag.get(dk) + if got == dv: + ok(f"diagnostics.{dk} == {dv!r}") + else: + bad(f"diagnostics.{dk}: expected {dv!r}, got {got!r}") + else: + print(f" warn unknown expected.json key {key!r} — not checked") + +if failures: + print(f"pinned comparison: {failures} check(s) FAILED") + sys.exit(1) +print("pinned comparison: all pinned checks passed") +PY +} + +# --- arg parsing --------------------------------------------------------- +if [[ $# -lt 1 ]]; then + # No args: if the cwd contains bundles, list them; otherwise show help. + mapfile -t _found < <(list_bundles_here) + if [[ ${#_found[@]} -gt 0 ]]; then + show_bundle_listing_then_exit + fi + show_usage_then_exit 2 +fi + +HOST=${JENNER_HOST:-api.jenneranalytics.com} + +case "$1" in + -h|--help) + show_usage_then_exit 0 + ;; + -l|--list) + mapfile -t _found < <(list_bundles_here) + if [[ ${#_found[@]} -eq 0 ]]; then + printf 'No tNNN_* bundles found in %s\n' "$(pwd)" + exit 0 + fi + printf 'Bundles in %s:\n' "$(pwd)" + for b in "${_found[@]}"; do + printf ' %s\n' "${b#./}" + done + exit 0 + ;; + --compare) + # Purely local: diff a response.json against a pinned expected.json. + if [[ $# -ne 3 ]]; then + printf 'usage: %s --compare RESPONSE.json EXPECTED.json\n' "$(basename "$0")" >&2 + exit 2 + fi + _rc=0 + compare_response_to_expected "$2" "$3" || _rc=$? + exit "$_rc" + ;; + --all) + mapfile -t _found < <(list_bundles_here) + if [[ ${#_found[@]} -eq 0 ]]; then + printf 'No tNNN_* bundles found in %s\n' "$(pwd)" >&2 + exit 3 + fi + _pass=0; _fail=0 + for b in "${_found[@]}"; do + printf '\n── %s ──\n' "${b#./}" + if "$0" "$b" "${b#./}_response.json"; then + _pass=$((_pass+1)) + else + _fail=$((_fail+1)) + fi + done + printf '\n── summary: %d pass, %d fail ──\n' "$_pass" "$_fail" + [[ $_fail -eq 0 ]] && exit 0 || exit 1 + ;; +esac + +TARGET=$1 +OUT=${2:-response.json} + +# --- assemble the submission body --------------------------------------- +# If TARGET is a directory, treat it as a bundle. If it's a file, submit +# it directly. +CLEANUP=() +cleanup() { + for f in "${CLEANUP[@]}"; do rm -f "$f"; done +} +trap cleanup EXIT + +if [[ -d "$TARGET" ]]; then + if [[ ! -f "$TARGET/script.sas" ]]; then + printf 'error: %s is a directory but has no script.sas\n' "$TARGET" >&2 + exit 3 + fi + SUBMIT=$(mktemp -t jc_submit.XXXXXX.sas) + CLEANUP+=("$SUBMIT") + if [[ -f "$TARGET/autoexec.sas" ]]; then + cat "$TARGET/autoexec.sas" > "$SUBMIT" + printf '\n' >> "$SUBMIT" + fi + cat "$TARGET/script.sas" >> "$SUBMIT" + printf 'Submitting bundle: %s\n' "$TARGET" + if [[ -f "$TARGET/autoexec.sas" ]]; then + printf ' autoexec.sas (%d bytes) + script.sas (%d bytes)\n' \ + "$(wc -c < "$TARGET/autoexec.sas")" "$(wc -c < "$TARGET/script.sas")" + else + printf ' script.sas (%d bytes), no autoexec\n' "$(wc -c < "$TARGET/script.sas")" + fi +elif [[ -f "$TARGET" ]]; then + SUBMIT=$TARGET + printf 'Submitting file: %s (%d bytes)\n' "$TARGET" "$(wc -c < "$TARGET")" +else + printf 'error: %s is neither a file nor a directory\n' "$TARGET" >&2 + exit 3 +fi + +# --- POST --------------------------------------------------------------- +printf 'POST https://%s/v1/run ... ' "$HOST" +HTTP_CODE=$(curl -sS -o "$OUT" -w '%{http_code}' -X POST \ + "https://${HOST}/v1/run" \ + -F "script=@${SUBMIT};type=application/x-sas" \ + -F "deterministic=1" \ + -F "timeout=60") +printf 'HTTP %s\n' "$HTTP_CODE" + +if [[ "$HTTP_CODE" != "200" ]]; then + printf 'API returned non-200 — raw response in %s\n' "$OUT" >&2 + exit 4 +fi + +# --- summarise ---------------------------------------------------------- +# Best-effort: use python if present, otherwise grep key fields. +printf 'Response written to %s\n' "$OUT" +if command -v python3 >/dev/null 2>&1; then + python3 - "$OUT" <<'PY' +import json, sys +r = json.load(open(sys.argv[1])) +print(f" status : {r.get('status')}") +print(f" exit_code : {r.get('exit_code')}") +print(f" duration_ms: {r.get('duration_ms')}") +print(f" run_id : {r.get('run_id')}") +print(f" jenner_ver : {r.get('jenner_version')}") +log = r.get('log', '') +if log: + print(' log (first 10 lines):') + for line in log.splitlines()[:10]: + print(f' {line}') +PY +else + printf ' (install python3 for a pretty summary; raw JSON in %s)\n' "$OUT" +fi + +# --- pinned-result comparison --------------------------------------------- +# A bundle that ships an expected.json only passes if the fresh response +# matches every pinned field — HTTP 200 alone is not a pass. Without +# python3 we can't parse JSON portably, so fall back to the old behavior +# and say so. +if [[ -d "$TARGET" && -f "$TARGET/expected.json" ]]; then + if command -v python3 >/dev/null 2>&1; then + printf 'Comparing response against pinned %s/expected.json\n' "${TARGET%/}" + _rc=0 + compare_response_to_expected "$OUT" "$TARGET/expected.json" || _rc=$? + if [[ $_rc -ne 0 ]]; then + printf 'Bundle FAILED the pinned comparison (see lines above)\n' >&2 + exit "$_rc" + fi + else + printf 'pinned comparison skipped (python3 not found) — HTTP/status only\n' + fi +fi diff --git a/jenner-check/t002_legumes_reshape/autoexec.sas b/jenner-check/t002_legumes_reshape/autoexec.sas new file mode 100644 index 0000000..719f93f --- /dev/null +++ b/jenner-check/t002_legumes_reshape/autoexec.sas @@ -0,0 +1,2 @@ +/* cap input rows for the captured run */ +options obs=100; diff --git a/jenner-check/t002_legumes_reshape/expected.json b/jenner-check/t002_legumes_reshape/expected.json new file mode 100644 index 0000000..5873a86 --- /dev/null +++ b/jenner-check/t002_legumes_reshape/expected.json @@ -0,0 +1,14 @@ +{ + "_captured_at": "2026-07-11T03:50:00Z", + "_captured_run_id": "r_95b7679e057144f39475f10a6d28e10e", + "status": "ok", + "exit_code": 0, + "log_contains": [ + "NOTE: Wrote legumes (8 rows, 5 columns).", + "NOTE: Read 16 rows from legumes.", + "NOTE: Wrote legumes2 (16 rows, 6 columns).", + "NOTE: PROC PRINT data=legumes2" + ], + "log_does_not_contain": ["ERROR:", "[JENNER-ERROR"], + "diagnostics": {"parse_warnings": [], "runtime_warnings": []} +} diff --git a/jenner-check/t002_legumes_reshape/expected/log.txt b/jenner-check/t002_legumes_reshape/expected/log.txt new file mode 100644 index 0000000..3feb627 --- /dev/null +++ b/jenner-check/t002_legumes_reshape/expected/log.txt @@ -0,0 +1,28 @@ + +NOTE: Option OBS changed to 100. +NOTE: DATA legumes + +NOTE: Processing inline DATALINES (8 lines) + +NOTE: Read 8 rows from DATALINES. +NOTE: Wrote legumes (8 rows, 5 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: DATA legumes2 + + +NOTE: Read 8 rows from legumes. +NOTE: Read 16 rows from legumes. +NOTE: Wrote legumes2 (16 rows, 6 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: PROC SORT data=legumes2 + +NOTE: Read 16 rows from legumes2. +NOTE: Wrote legumes2 (16 rows, 6 columns). +NOTE: PROC SORT statement used. +NOTE: PROC PRINT data=legumes2 + +NOTE: PROC PRINT completed: 16 observations printed, 5 variables diff --git a/jenner-check/t002_legumes_reshape/expected/output.txt b/jenner-check/t002_legumes_reshape/expected/output.txt new file mode 100644 index 0000000..8a4fc45 --- /dev/null +++ b/jenner-check/t002_legumes_reshape/expected/output.txt @@ -0,0 +1,20 @@ + legumes reshaped wide-to-long by the SASTauArgus example (solution 2) + +ident type_leg a88 cj quantite +S001 legumes_rouges A NORMAL 12 +S001 salades A NORMAL 4 +S002 legumes_rouges A NORMAL 7 +S002 salades A NORMAL 9 +S003 legumes_rouges B SARL 3 +S003 salades B SARL 15 +S004 legumes_rouges B SARL 21 +S004 salades B SARL 2 +S005 legumes_rouges C NORMAL 5 +S005 salades C NORMAL 5 +S006 legumes_rouges C SARL 0 +S006 salades C SARL 11 +S007 legumes_rouges A SARL 18 +S007 salades A SARL 6 +S008 legumes_rouges B NORMAL 9 +S008 salades B NORMAL 0 + diff --git a/jenner-check/t002_legumes_reshape/meta.json b/jenner-check/t002_legumes_reshape/meta.json new file mode 100644 index 0000000..247e044 --- /dev/null +++ b/jenner-check/t002_legumes_reshape/meta.json @@ -0,0 +1,7 @@ +{ + "bundle": "t002_legumes_reshape", + "source_file": "Documentation/Code_exemple_SASTauArgus.sas", + "source_blob_sha": "fa0184b17e2646c717c2ee48846fd35c427e84bc", + "source_commit": "e9b15e8df359871623142f4c4aed35eb33f80a07", + "notes": "The 'solution 2' wide-to-long reshape (example lines 177-184): interleaves legumes with itself twice via SET + IN= + RENAME=(legumes_rouges=quantite / salades=quantite), tags each half with type_leg, sorts by ident. Author's DATA step and PROC SORT unchanged; source library pointed at WORK and an 8-row inline sample supplied for the columns the step reads (ident a88 cj legumes_rouges salades)." +} diff --git a/jenner-check/t002_legumes_reshape/script.sas b/jenner-check/t002_legumes_reshape/script.sas new file mode 100644 index 0000000..d6eb810 --- /dev/null +++ b/jenner-check/t002_legumes_reshape/script.sas @@ -0,0 +1,42 @@ +/************************************************************************************************/ +/* Compatibility bundle for the "wide -> long" reshape step of the SASTauArgus documentation */ +/* example (Documentation/Code_exemple_SASTauArgus.sas, "solution 2", lines 177-184). */ +/* */ +/* The step interleaves the legumes table with itself, once keeping legumes_rouges (renamed to */ +/* quantite) and once keeping salades (also renamed to quantite), tagging each half with */ +/* type_leg. This turns the two response columns into a single stacked "quantite" column so a */ +/* single tabulation can protect both vegetable types at once. The DATA step and PROC SORT are */ +/* the author's, unchanged; only the source library is pointed at a small sample table below */ +/* (WORK) so the reshape runs standalone. */ +/************************************************************************************************/ + +/* Small sample of the legumes individual-level table: the columns this step reads. */ +data legumes ; + input ident $ a88 $ cj $ legumes_rouges salades ; + datalines ; +S001 A NORMAL 12 4 +S002 A NORMAL 7 9 +S003 B SARL 3 15 +S004 B SARL 21 2 +S005 C NORMAL 5 5 +S006 C SARL 0 11 +S007 A SARL 18 6 +S008 B NORMAL 9 0 +; +run ; + +/* --- author's reshape, verbatim (library reference pointed at WORK) --- */ +data legumes2 ; + set legumes (in = leg_r rename = (legumes_rouges = quantite)) + legumes (in = salad rename = (salades = quantite)); + if leg_r = 1 then type_leg = "legumes_rouges"; + if salad = 1 then type_leg = "salades"; +run; + +proc sort data = legumes2 ; by ident ; run; + +/* Show the stacked result: each individual now appears twice, once per vegetable type. */ +proc print data = legumes2 noobs ; + var ident type_leg a88 cj quantite ; + title "legumes reshaped wide-to-long by the SASTauArgus example (solution 2)" ; +run ; diff --git a/jenner-check/t003_macro_opendata/autoexec.sas b/jenner-check/t003_macro_opendata/autoexec.sas new file mode 100644 index 0000000..719f93f --- /dev/null +++ b/jenner-check/t003_macro_opendata/autoexec.sas @@ -0,0 +1,2 @@ +/* cap input rows for the captured run */ +options obs=100; diff --git a/jenner-check/t003_macro_opendata/expected.json b/jenner-check/t003_macro_opendata/expected.json new file mode 100644 index 0000000..237ee6c --- /dev/null +++ b/jenner-check/t003_macro_opendata/expected.json @@ -0,0 +1,14 @@ +{ + "_captured_at": "2026-07-11T03:52:00Z", + "_captured_run_id": "r_bb8c00726bd1421dbe99380c0592b127", + "status": "ok", + "exit_code": 0, + "log_contains": [ + "NOTE: 12 row(s) inserted into empty_batch.", + "NOTE: Read 2 rows from empty_batch.", + "NOTE: Wrote tab1 (2 rows, 2 columns).", + "NOTE: PROC PRINT data=tab1" + ], + "log_does_not_contain": ["ERROR:", "[JENNER-ERROR"], + "diagnostics": {"parse_warnings": [], "runtime_warnings": []} +} diff --git a/jenner-check/t003_macro_opendata/expected/log.txt b/jenner-check/t003_macro_opendata/expected/log.txt new file mode 100644 index 0000000..e4d6bca --- /dev/null +++ b/jenner-check/t003_macro_opendata/expected/log.txt @@ -0,0 +1,42 @@ + +NOTE: Option OBS changed to 100. +NOTE: PROC SQL + +NOTE: Table empty_batch created with 1 columns. +NOTE: PROC SQL statement used. +NOTE: PROC SQL + +NOTE: 12 row(s) inserted into empty_batch. +NOTE: PROC SQL statement used. +NOTE: DATA tab1 tab2 + +NOTE: Multi-output DATA step splits stream into: tab1 tab2 + +NOTE: Read 0 rows from empty_batch. +NOTE: Wrote tab1 (0 rows, 4 columns). +NOTE: Wrote tab2 (0 rows, 4 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: DATA tab3 tab4 tab5 + +NOTE: Multi-output DATA step splits stream into: tab3 tab4 tab5 + +NOTE: Read 0 rows from empty_batch. +NOTE: Wrote tab3 (0 rows, 2 columns). +NOTE: Wrote tab4 (0 rows, 2 columns). +NOTE: Wrote tab5 (0 rows, 2 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: DATA tab1 + + +NOTE: Read 2 rows from empty_batch. +NOTE: Wrote tab1 (2 rows, 2 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: PROC PRINT data=tab1 + +NOTE: PROC PRINT completed: 2 observations printed, 2 variables diff --git a/jenner-check/t003_macro_opendata/expected/output.txt b/jenner-check/t003_macro_opendata/expected/output.txt new file mode 100644 index 0000000..9fd6068 --- /dev/null +++ b/jenner-check/t003_macro_opendata/expected/output.txt @@ -0,0 +1,6 @@ + OPENMICRODATA / OPENMETADATA batch lines built by %opendata + + operation instruction + "C:\Chemin vers le depot\SASTauArgus\Documentation\legumes.asc" + "C:\Chemin vers le depot\SASTauArgus\Documentation\legumes.rda" + diff --git a/jenner-check/t003_macro_opendata/meta.json b/jenner-check/t003_macro_opendata/meta.json new file mode 100644 index 0000000..09b89aa --- /dev/null +++ b/jenner-check/t003_macro_opendata/meta.json @@ -0,0 +1,7 @@ +{ + "bundle": "t003_macro_opendata", + "source_file": "Macro_Tau_Argus.sas", + "source_blob_sha": "90c290ed36fa48b15b7075a8a2d0b4bad1883846", + "source_commit": "e9b15e8df359871623142f4c4aed35eb33f80a07", + "notes": "Caller for two of the library's batch-building macros: %batch_file (lines 1133-1174) creates the empty_batch operation template and the tab1..tab5 shells; %opendata (lines 1181-1193) fills the / lines with the quoted .asc/.rda paths. Both macros verbatim; called with the library path from the documentation example so the produced batch lines are printed. No external files or shell-outs on this path." +} diff --git a/jenner-check/t003_macro_opendata/script.sas b/jenner-check/t003_macro_opendata/script.sas new file mode 100644 index 0000000..8e6b3cd --- /dev/null +++ b/jenner-check/t003_macro_opendata/script.sas @@ -0,0 +1,78 @@ +/************************************************************************************************/ +/* Compatibility caller for the %batch_file and %opendata macros of SASTauArgus */ +/* (Macro_Tau_Argus.sas: %batch_file lines 1133-1174, %opendata lines 1181-1193). */ +/* */ +/* %batch_file creates the empty_batch operation template that the whole pipeline fills in; */ +/* %opendata then fills the and lines with the quoted .asc / .rda */ +/* paths Tau-Argus reads. Both macros are the author's, unchanged. We call them with the library */ +/* path from the documentation example so the produced batch lines are shown directly. */ +/************************************************************************************************/ + +%macro batch_file ( + library = ) ; + + proc sql ; + create table empty_batch (operation char(16)) ; + quit ; + + proc sql ; + insert into empty_batch (operation) + values('') + values('') + values('') + values('') + values('') + values('') + values('') + values('') + values('') + values('') + values('') + values('test') ; + quit ; + + %let library = &library ; + + data tab1 tab2 ; + length instruction $150 ; + length tabulation $150 ; + length order 8. ; + set empty_batch (where = (operation = '')) ; + instruction = '' ; + run ; + + data tab3 tab4 tab5 ; + length instruction $150 ; + set empty_batch (where = (operation = '')) ; + instruction = '' ; + run ; + +%mend ; + +%macro opendata ( + library_opendata = , + asc = microdata, + rda = metadata) ; + + data tab1 ; + length instruction $150 ; + set empty_batch (where = (operation in("" ""))) ; + if operation = "" then instruction = "'&library_opendata.\&asc..asc'" ; + if operation = "" then instruction = "'&library_opendata.\&rda..rda'" ; + instruction = tranwrd(instruction,"'",'"') ; + run ; +%mend ; + +/* Build the batch template, then fill the microdata / metadata open lines, using the library */ +/* path and dataset name from the documentation example. */ +%batch_file (library = C:\Chemin vers le depot\SASTauArgus\Documentation) ; + +%opendata ( + library_opendata = C:\Chemin vers le depot\SASTauArgus\Documentation, + asc = legumes, + rda = legumes) ; + +proc print data = tab1 noobs ; + var operation instruction ; + title "OPENMICRODATA / OPENMETADATA batch lines built by %nrstr(%opendata)" ; +run ;