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/t001_assignment4_symputx_counts/autoexec.sas b/jenner-check/t001_assignment4_symputx_counts/autoexec.sas new file mode 100644 index 0000000..8b0b97f --- /dev/null +++ b/jenner-check/t001_assignment4_symputx_counts/autoexec.sas @@ -0,0 +1 @@ +options obs=100; /* cap input rows for the captured run */ diff --git a/jenner-check/t001_assignment4_symputx_counts/expected.json b/jenner-check/t001_assignment4_symputx_counts/expected.json new file mode 100644 index 0000000..8866bb3 --- /dev/null +++ b/jenner-check/t001_assignment4_symputx_counts/expected.json @@ -0,0 +1,14 @@ +{ + "_captured_at": "2026-07-11T10:10:00Z", + "_captured_run_id": "r_7eaf5ad60cd441eba674ee03c78ef6ce", + "status": "ok", + "exit_code": 0, + "log_contains": [ + "In the class there are 10 boys and 9 girls.", + "NOTE: Read 19 rows from sashelp.class.", + "NOTE: Wrote _null_ (19 rows, 7 columns).", + "NOTE: Wrote _null_ (19 rows, 5 columns)." + ], + "log_does_not_contain": ["ERROR:", "[JENNER-ERROR"], + "diagnostics": {"parse_warnings": [], "runtime_warnings": []} +} diff --git a/jenner-check/t001_assignment4_symputx_counts/expected/log.txt b/jenner-check/t001_assignment4_symputx_counts/expected/log.txt new file mode 100644 index 0000000..bde2adf --- /dev/null +++ b/jenner-check/t001_assignment4_symputx_counts/expected/log.txt @@ -0,0 +1,20 @@ +NOTE: Option OBS changed to 100. +NOTE: DATA _null_ + +In the class there are 10 boys and 9 girls. + +NOTE: Read 19 rows from sashelp.class. +NOTE: Wrote _null_ (19 rows, 7 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: DATA _null_ + + +NOTE: Read 19 rows from sashelp.class. +NOTE: Wrote _null_ (19 rows, 5 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +Amir + diff --git a/jenner-check/t001_assignment4_symputx_counts/expected/output.txt b/jenner-check/t001_assignment4_symputx_counts/expected/output.txt new file mode 100644 index 0000000..e69de29 diff --git a/jenner-check/t001_assignment4_symputx_counts/meta.json b/jenner-check/t001_assignment4_symputx_counts/meta.json new file mode 100644 index 0000000..8c2c904 --- /dev/null +++ b/jenner-check/t001_assignment4_symputx_counts/meta.json @@ -0,0 +1,7 @@ +{ + "bundle": "t001_assignment4_symputx_counts", + "source_file": "Assignment 4.sas", + "source_blob_sha": "6401b1047af236f6b723ca6f396abe05c35cf13b", + "source_commit": "e70ed4967e4977ef513c6933f31fb0338a0a1183", + "notes": "Unmodified copy of Assignment 4.sas. Counts boys/girls in sashelp.class via call symputx, then generates name1..name19 macro variables from the same dataset. Self-contained (sashelp.class is built in), no edits needed." +} diff --git a/jenner-check/t001_assignment4_symputx_counts/script.sas b/jenner-check/t001_assignment4_symputx_counts/script.sas new file mode 100644 index 0000000..6401b10 --- /dev/null +++ b/jenner-check/t001_assignment4_symputx_counts/script.sas @@ -0,0 +1,24 @@ +/* Step 1: Read class dataset and count number of boys and girls */ +data _null_; + set sashelp.class end=last; + retain boys girls 0; + + if sex = 'M' then boys + 1; + else if sex = 'F' then girls + 1; + + if last then do; + call symputx('boys', boys); + call symputx('girls', girls); + put "In the class there are " boys " boys and " girls " girls."; + end; +run; + + +/* Step 2: Create macro variables name1 to name19 from the dataset */ +data _null_; + set sashelp.class; + call symputx(cats('name', _N_), name); +run; + +/*test*/ +%put &name1; diff --git a/jenner-check/t002_indirect_macro_refs/autoexec.sas b/jenner-check/t002_indirect_macro_refs/autoexec.sas new file mode 100644 index 0000000..8b0b97f --- /dev/null +++ b/jenner-check/t002_indirect_macro_refs/autoexec.sas @@ -0,0 +1 @@ +options obs=100; /* cap input rows for the captured run */ diff --git a/jenner-check/t002_indirect_macro_refs/expected.json b/jenner-check/t002_indirect_macro_refs/expected.json new file mode 100644 index 0000000..004bd34 --- /dev/null +++ b/jenner-check/t002_indirect_macro_refs/expected.json @@ -0,0 +1,12 @@ +{ + "_captured_at": "2026-07-11T10:12:00Z", + "_captured_run_id": "r_c52f6ab4049a49fd9e91473cda8de578", + "status": "ok", + "exit_code": 0, + "log_contains": [ + "four", + "three" + ], + "log_does_not_contain": ["ERROR:", "[JENNER-ERROR"], + "diagnostics": {"parse_warnings": [], "runtime_warnings": []} +} diff --git a/jenner-check/t002_indirect_macro_refs/expected/log.txt b/jenner-check/t002_indirect_macro_refs/expected/log.txt new file mode 100644 index 0000000..85f1bef --- /dev/null +++ b/jenner-check/t002_indirect_macro_refs/expected/log.txt @@ -0,0 +1,5 @@ +four +four +three +NOTE: Option OBS changed to 100. + diff --git a/jenner-check/t002_indirect_macro_refs/expected/output.txt b/jenner-check/t002_indirect_macro_refs/expected/output.txt new file mode 100644 index 0000000..e69de29 diff --git a/jenner-check/t002_indirect_macro_refs/meta.json b/jenner-check/t002_indirect_macro_refs/meta.json new file mode 100644 index 0000000..e80a002 --- /dev/null +++ b/jenner-check/t002_indirect_macro_refs/meta.json @@ -0,0 +1,7 @@ +{ + "bundle": "t002_indirect_macro_refs", + "source_file": "Assignment 7.sas", + "source_blob_sha": "e6278d1eecc4b79a0ab3512710ac24f4fc0869d4", + "source_commit": "e70ed4967e4977ef513c6933f31fb0338a0a1183", + "notes": "Unmodified copy of Assignment 7.sas. Demonstrates direct vs. multi-level indirect macro-variable references (&three, &&&two, &&&&&one) through a chain one->two->three->four. Pure macro-language, no data needed. Verified by hand-deriving SAS's documented ampersand-rescan algorithm before pinning the third %put's output (three)." +} diff --git a/jenner-check/t002_indirect_macro_refs/script.sas b/jenner-check/t002_indirect_macro_refs/script.sas new file mode 100644 index 0000000..e6278d1 --- /dev/null +++ b/jenner-check/t002_indirect_macro_refs/script.sas @@ -0,0 +1,12 @@ +%let one=two; +%let two=three; +%let three=four; + +/* 1. Direct four*/ +%put &three; + +/* 2. One level indirect */ +%put &&&two; + +/* 3. Two levels indirect */ +%put &&&&&one; diff --git a/jenner-check/t003_avg_macro_sashelp/autoexec.sas b/jenner-check/t003_avg_macro_sashelp/autoexec.sas new file mode 100644 index 0000000..8b0b97f --- /dev/null +++ b/jenner-check/t003_avg_macro_sashelp/autoexec.sas @@ -0,0 +1 @@ +options obs=100; /* cap input rows for the captured run */ diff --git a/jenner-check/t003_avg_macro_sashelp/expected.json b/jenner-check/t003_avg_macro_sashelp/expected.json new file mode 100644 index 0000000..8c44598 --- /dev/null +++ b/jenner-check/t003_avg_macro_sashelp/expected.json @@ -0,0 +1,12 @@ +{ + "_captured_at": "2026-07-11T10:14:00Z", + "_captured_run_id": "r_3f9a62533844406f92fccdb6946845e0", + "status": "ok", + "exit_code": 0, + "log_contains": [ + "NOTE: Output dataset avg_results has 1 observations and 5 variables.", + "NOTE: PROC PRINT completed: 1 observations printed, 5 variables" + ], + "log_does_not_contain": ["ERROR:", "[JENNER-ERROR"], + "diagnostics": {"parse_warnings": [], "runtime_warnings": []} +} diff --git a/jenner-check/t003_avg_macro_sashelp/expected/log.txt b/jenner-check/t003_avg_macro_sashelp/expected/log.txt new file mode 100644 index 0000000..c3c00e8 --- /dev/null +++ b/jenner-check/t003_avg_macro_sashelp/expected/log.txt @@ -0,0 +1,14 @@ +NOTE: Option OBS changed to 100. +NOTE: PROC MEANS +NOTE: Output dataset avg_results has 1 observations and 5 variables. +NOTE: PROC MEANS statement used. +NOTE: PROC PRINT data=avg_results + +NOTE: PROC PRINT completed: 1 observations printed, 5 variables +NOTE: PROC MEANS +NOTE: Output dataset avg_results has 1 observations and 5 variables. +NOTE: PROC MEANS statement used. +NOTE: PROC PRINT data=avg_results + +NOTE: PROC PRINT completed: 1 observations printed, 5 variables + diff --git a/jenner-check/t003_avg_macro_sashelp/expected/output.txt b/jenner-check/t003_avg_macro_sashelp/expected/output.txt new file mode 100644 index 0000000..d211858 --- /dev/null +++ b/jenner-check/t003_avg_macro_sashelp/expected/output.txt @@ -0,0 +1,11 @@ + Average values for variables: Invoice EngineSize Horsepower in dataset cars + +_TYPE_ _FREQ_ Invoice EngineSize Horsepower + 0 12 22840.75 2.5416666667 189.6666666667 + + Average values for variables: AgeAtStart Height Weight in dataset heart + +_TYPE_ _FREQ_ AgeAtStart Height Weight + 0 50 45.22 67.44 173.26 + + diff --git a/jenner-check/t003_avg_macro_sashelp/meta.json b/jenner-check/t003_avg_macro_sashelp/meta.json new file mode 100644 index 0000000..a61fd44 --- /dev/null +++ b/jenner-check/t003_avg_macro_sashelp/meta.json @@ -0,0 +1,7 @@ +{ + "bundle": "t003_avg_macro_sashelp", + "source_file": "Assignment 9.sas", + "source_blob_sha": "ed7c087095eadf881bc23e769f0dcfc7ee00907f", + "source_commit": "e70ed4967e4977ef513c6933f31fb0338a0a1183", + "notes": "Unmodified copy of Assignment 9.sas. A general macro %avg_macro(dataset_name=, varlist=) that computes and prints averages for any sashelp dataset/variable list, called twice (sashelp.cars, sashelp.heart) exactly as the author tested it. Self-contained, no edits needed." +} diff --git a/jenner-check/t003_avg_macro_sashelp/script.sas b/jenner-check/t003_avg_macro_sashelp/script.sas new file mode 100644 index 0000000..b85ee3a --- /dev/null +++ b/jenner-check/t003_avg_macro_sashelp/script.sas @@ -0,0 +1,19 @@ +%macro avg_macro(dataset_name=, varlist=); + + /* Calculate means */ + proc means data=sashelp.&dataset_name noprint; + var &varlist; + output out=avg_results mean=; + run; + + /* Print the calculated averages */ + proc print data=avg_results label noobs; + title "Average values for variables: &varlist in dataset &dataset_name"; + run; + +%mend; + +/* Test */ +%avg_macro(dataset_name=cars, varlist=Invoice EngineSize Horsepower); + +%avg_macro(dataset_name=heart, varlist=AgeAtStart Height Weight); diff --git a/jenner-check/t004_correlations_proc_corr/autoexec.sas b/jenner-check/t004_correlations_proc_corr/autoexec.sas new file mode 100644 index 0000000..8b0b97f --- /dev/null +++ b/jenner-check/t004_correlations_proc_corr/autoexec.sas @@ -0,0 +1 @@ +options obs=100; /* cap input rows for the captured run */ diff --git a/jenner-check/t004_correlations_proc_corr/expected.json b/jenner-check/t004_correlations_proc_corr/expected.json new file mode 100644 index 0000000..786555f --- /dev/null +++ b/jenner-check/t004_correlations_proc_corr/expected.json @@ -0,0 +1,14 @@ +{ + "_captured_at": "2026-07-11T10:16:00Z", + "_captured_run_id": "r_32e57671ecd8416aacb796015e279809", + "status": "ok", + "exit_code": 0, + "log_contains": [ + "NOTE: Read 19 rows from sashelp.class.", + "Hello World!", + "NOTE: Read 12 rows from sashelp.cars.", + "NOTE: PROC CORR data=class" + ], + "log_does_not_contain": ["ERROR:", "[JENNER-ERROR"], + "diagnostics": {"parse_warnings": [], "runtime_warnings": []} +} diff --git a/jenner-check/t004_correlations_proc_corr/expected/log.txt b/jenner-check/t004_correlations_proc_corr/expected/log.txt new file mode 100644 index 0000000..ca0a023 --- /dev/null +++ b/jenner-check/t004_correlations_proc_corr/expected/log.txt @@ -0,0 +1,30 @@ +NOTE: Option OBS changed to 100. +NOTE: DATA class + + +NOTE: Read 19 rows from sashelp.class. +NOTE: Wrote class (19 rows, 5 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: PROC MEANS +NOTE: Output dataset average_age has 1 observations and 2 variables. +NOTE: PROC MEANS statement used. +Hello World! +The variables are: weight age +NOTE: DATA cars + + +NOTE: Read 12 rows from sashelp.cars. +NOTE: Wrote cars (12 rows, 15 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: PROC MEANS +NOTE: Output dataset multi_stats has 1 observations and 2 variables. +NOTE: PROC MEANS statement used. +NOTE: PROC CORR data=class + +NOTE: ODS plot written: corr_heatmap.spec.json +NOTE: PROC CORR ODS Graphics generated. + diff --git a/jenner-check/t004_correlations_proc_corr/expected/output.txt b/jenner-check/t004_correlations_proc_corr/expected/output.txt new file mode 100644 index 0000000..16674bc --- /dev/null +++ b/jenner-check/t004_correlations_proc_corr/expected/output.txt @@ -0,0 +1,35 @@ + The MEANS Procedure + + Variable Label N Mean Std Dev Minimum Maximum + ------------------------------------------------------------------------------------------------ + Weight Weight in pounds 19 97.2105263 23.3947500 58.0000000 145.0000000 + Age Age in years 19 13.1578947 1.5004873 11.0000000 16.0000000 + ------------------------------------------------------------------------------------------------ + + The MEANS Procedure + + Variable Label N Mean Std Dev Minimum Maximum + ----------------------------------------------------------------------------------------------------------------- + Invoice Invoice price 12 22840.7500000 10956.6982346 11966.0000000 48690.0000000 + EngineSize Engine size (liters) 12 2.5416667 0.7879298 1.7000000 4.4000000 + ----------------------------------------------------------------------------------------------------------------- + + The CORR Procedure + 3 Variables: Age in years Weight in pounds Height in inches + Simple Statistics +Variable N Mean Std Dev Sum Minimum Maximum +-------- -------- -------- -------- -------- -------- -------- +Age in years 19 13.15789 1.50049 250.00000 11.00000 16.00000 +Weight in pounds 19 97.21053 23.39475 1847.00000 58.00000 145.00000 +Height in inches 19 61.32105 5.06859 1165.10000 52.50000 70.50000 + Pearson Correlation Coefficients, N = 19 + Prob > |r| under H0: Rho=0 + age weight height +-------- -------- -------- -------- +age 1.00000 0.95253 0.95208 + . <.0001 <.0001 +weight 0.95253 1.00000 0.98955 + <.0001 . <.0001 +height 0.95208 0.98955 1.00000 + <.0001 <.0001 . + diff --git a/jenner-check/t004_correlations_proc_corr/meta.json b/jenner-check/t004_correlations_proc_corr/meta.json new file mode 100644 index 0000000..e6dea8d --- /dev/null +++ b/jenner-check/t004_correlations_proc_corr/meta.json @@ -0,0 +1,7 @@ +{ + "bundle": "t004_correlations_proc_corr", + "source_file": "Calculate Correlations Between Variables.sas", + "source_blob_sha": "f7934e020634bcc4a68a94e4a899bb04737bf34c", + "source_commit": "e70ed4967e4977ef513c6933f31fb0338a0a1183", + "notes": "Unmodified copy of Calculate Correlations Between Variables.sas. Builds class/cars copies of sashelp data, runs proc means (autoname output) on macro-variable-driven var lists, then proc corr across age/weight/height. Self-contained, no edits needed." +} diff --git a/jenner-check/t004_correlations_proc_corr/script.sas b/jenner-check/t004_correlations_proc_corr/script.sas new file mode 100644 index 0000000..f7934e0 --- /dev/null +++ b/jenner-check/t004_correlations_proc_corr/script.sas @@ -0,0 +1,44 @@ +/* define some macro variables*/ +%let weight = WEIGHT; +%let age = AGE; + +/* read data from sashelp class lib */ +data class; +set sashelp.class; +run; + +/* calc mean age and weight*/ +%let var=weight age; +proc means data=class; +var &var; +output OUT=average_age(drop=_TYPE_ _FREQ_) mean= / autoname; +run; + +/* using put statement to print, for debugging */ +%put Hello World!; +/* can also print variable value */ +%put The variables are: &var; + +/* defining multiple macro variables */ + +/* read data from sashelp cars lib */ +data cars; +set sashelp.cars; +run; + +%let var_list = age weight; +%let var_list_2 = invoice engineSize; +%let database = class; +%let database_2 = cars; +proc means data=&database_2; +var &var_list_2; +output OUT=multi_stats(drop= _TYPE_ _FREQ_) mean= / autoname; +run; + +/*Assignment - calculate correlations between variables*/ +%let dataset = &database; +%let var_list_3 = age weight height; + +proc corr data=&dataset; +var &var_list_3; +run; diff --git a/jenner-check/t005_percentages_pipeline/autoexec.sas b/jenner-check/t005_percentages_pipeline/autoexec.sas new file mode 100644 index 0000000..8b0b97f --- /dev/null +++ b/jenner-check/t005_percentages_pipeline/autoexec.sas @@ -0,0 +1 @@ +options obs=100; /* cap input rows for the captured run */ diff --git a/jenner-check/t005_percentages_pipeline/expected.json b/jenner-check/t005_percentages_pipeline/expected.json new file mode 100644 index 0000000..861fdb0 --- /dev/null +++ b/jenner-check/t005_percentages_pipeline/expected.json @@ -0,0 +1,13 @@ +{ + "_captured_at": "2026-07-11T10:18:00Z", + "_captured_run_id": "r_2f70524485da4ae3a8d2754432056ba0", + "status": "ok", + "exit_code": 0, + "log_contains": [ + "NOTE: Output dataset class_stats has 1 observations and 2 variables.", + "NOTE: Output dataset tall_low has 2 observations and 2 variables.", + "NOTE: PROC PRINT completed: 2 observations printed, 3 variables" + ], + "log_does_not_contain": ["ERROR:", "[JENNER-ERROR"], + "diagnostics": {"parse_warnings": [], "runtime_warnings": []} +} diff --git a/jenner-check/t005_percentages_pipeline/expected/log.txt b/jenner-check/t005_percentages_pipeline/expected/log.txt new file mode 100644 index 0000000..10c1410 --- /dev/null +++ b/jenner-check/t005_percentages_pipeline/expected/log.txt @@ -0,0 +1,48 @@ +NOTE: Option OBS changed to 100. +NOTE: DATA class + + +NOTE: Read 19 rows from sashelp.class. +NOTE: Wrote class (19 rows, 5 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: PROC MEANS +NOTE: Output dataset class_stats has 1 observations and 2 variables. +NOTE: PROC MEANS statement used. +NOTE: DATA _null_ + + +NOTE: Read 1 rows from class_stats. +NOTE: Wrote _null_ (1 rows, 2 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: DATA class + + +NOTE: Read 19 rows from class. +NOTE: Wrote class (19 rows, 6 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: PROC SORT data=class + +NOTE: Read 19 rows from class. +NOTE: Wrote class (19 rows, 6 columns). +NOTE: PROC SORT statement used. +NOTE: PROC MEANS +NOTE: Output dataset tall_low has 2 observations and 2 variables. +NOTE: PROC MEANS statement used. +NOTE: DATA percent + + +NOTE: Read 2 rows from tall_low. +NOTE: Wrote percent (2 rows, 3 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: PROC PRINT data=percent + +NOTE: PROC PRINT completed: 2 observations printed, 3 variables + diff --git a/jenner-check/t005_percentages_pipeline/expected/output.txt b/jenner-check/t005_percentages_pipeline/expected/output.txt new file mode 100644 index 0000000..5a25b81 --- /dev/null +++ b/jenner-check/t005_percentages_pipeline/expected/output.txt @@ -0,0 +1,6 @@ +ind Obs percent +--- --- ------------ + 0 9 0.4736842105 + 1 10 0.5263157895 + + diff --git a/jenner-check/t005_percentages_pipeline/meta.json b/jenner-check/t005_percentages_pipeline/meta.json new file mode 100644 index 0000000..f58ff13 --- /dev/null +++ b/jenner-check/t005_percentages_pipeline/meta.json @@ -0,0 +1,7 @@ +{ + "bundle": "t005_percentages_pipeline", + "source_file": "Calculating Percentages Using Call Symput and Proc Means N.sas", + "source_blob_sha": "cc00ec0e7a31d9d306e052fa83183629675b17aa", + "source_commit": "e70ed4967e4977ef513c6933f31fb0338a0a1183", + "notes": "Adapted from the second half of Calculating Percentages Using Call Symput and Proc Means N.sas (the average-height-and-percentage-above-average pipeline: proc means -> call symputx -> conditional flag -> proc sort -> BY-group proc means -> percent calc). Trimmed the file's earlier, duplicate proc means/proc corr section (already covered by t004) and appended a proc print so the final percent dataset the script builds is actually visible in the captured run -- the original script computed it but never printed it." +} diff --git a/jenner-check/t005_percentages_pipeline/script.sas b/jenner-check/t005_percentages_pipeline/script.sas new file mode 100644 index 0000000..efb6820 --- /dev/null +++ b/jenner-check/t005_percentages_pipeline/script.sas @@ -0,0 +1,47 @@ +/* read data from sashelp lib class data set */ +data class; +set sashelp.class; +run; + +/* calculate average height and percentage above average */ + +/*now calculate average using proc means */ +proc means data=class noprint; +var height; +output out=class_stats(drop= _TYPE_ _FREQ_) mean=avg N=total; +run; + +/* define macro variables avg height and total students */ +data _null_; +set class_stats; +call symputx('average_height', avg); +call symputx('total', total); +run; + +/* create ind column for indicating above or below */ +data class; +set class; +if height > &average_height then ind=1; +else ind=0; +run; + +/*Add sort step before proc means*/ +proc sort data=class; + by ind; +run; + +/* check how many above average and below */ +proc means data=class noprint; +var ind; +by ind; +output out=tall_low(drop= _TYPE_ _FREQ_) N=Obs; +run; + +/* now calculate percentage*/ +data percent; +set tall_low; +percent = Obs/&total; +run; + +proc print data=percent noobs; +run; diff --git a/jenner-check/t006_makecounts_proportion/autoexec.sas b/jenner-check/t006_makecounts_proportion/autoexec.sas new file mode 100644 index 0000000..8b0b97f --- /dev/null +++ b/jenner-check/t006_makecounts_proportion/autoexec.sas @@ -0,0 +1 @@ +options obs=100; /* cap input rows for the captured run */ diff --git a/jenner-check/t006_makecounts_proportion/expected.json b/jenner-check/t006_makecounts_proportion/expected.json new file mode 100644 index 0000000..5ba768c --- /dev/null +++ b/jenner-check/t006_makecounts_proportion/expected.json @@ -0,0 +1,13 @@ +{ + "_captured_at": "2026-07-11T10:20:00Z", + "_captured_run_id": "r_3d9e42e633874264871cc68727d787ca", + "status": "ok", + "exit_code": 0, + "log_contains": [ + "NOTE: Output dataset make_counts has 6 observations and 2 variables.", + "NOTE: Output dataset total_vehicles has 1 observations and 1 variables.", + "NOTE: PROC PRINT completed: 6 observations printed, 2 variables" + ], + "log_does_not_contain": ["ERROR:", "[JENNER-ERROR"], + "diagnostics": {"parse_warnings": [], "runtime_warnings": []} +} diff --git a/jenner-check/t006_makecounts_proportion/expected/log.txt b/jenner-check/t006_makecounts_proportion/expected/log.txt new file mode 100644 index 0000000..68e03e5 --- /dev/null +++ b/jenner-check/t006_makecounts_proportion/expected/log.txt @@ -0,0 +1,27 @@ +NOTE: Option OBS changed to 100. +NOTE: DATA cars + + +NOTE: Read 12 rows from sashelp.cars. +NOTE: Wrote cars (12 rows, 15 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: PROC MEANS +NOTE: Output dataset make_counts has 6 observations and 2 variables. +NOTE: PROC MEANS statement used. +NOTE: PROC MEANS +NOTE: Output dataset total_vehicles has 1 observations and 1 variables. +NOTE: PROC MEANS statement used. +NOTE: DATA result + + +NOTE: Read 6 rows from make_counts. +NOTE: Wrote result (6 rows, 2 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: PROC PRINT data=result + +NOTE: PROC PRINT completed: 6 observations printed, 2 variables + diff --git a/jenner-check/t006_makecounts_proportion/expected/output.txt b/jenner-check/t006_makecounts_proportion/expected/output.txt new file mode 100644 index 0000000..b15f01b --- /dev/null +++ b/jenner-check/t006_makecounts_proportion/expected/output.txt @@ -0,0 +1,10 @@ + Make prct +--------- ------------ +Acura 0.3333333333 +BMW 0.1666666667 +Chevrolet 0.0833333333 +Ford 0.0833333333 +Honda 0.1666666667 +Toyota 0.1666666667 + + diff --git a/jenner-check/t006_makecounts_proportion/meta.json b/jenner-check/t006_makecounts_proportion/meta.json new file mode 100644 index 0000000..230e8d4 --- /dev/null +++ b/jenner-check/t006_makecounts_proportion/meta.json @@ -0,0 +1,7 @@ +{ + "bundle": "t006_makecounts_proportion", + "source_file": "Macro Variables Call Symput.sas", + "source_blob_sha": "d0c146957176de9a5b696c39518c0e1b1a3ae294", + "source_commit": "e70ed4967e4977ef513c6933f31fb0338a0a1183", + "notes": "Unmodified copy of Macro Variables Call Symput.sas (the repo's own 'Assignment 3'). proc means class nway to get per-Make counts from sashelp.cars, a second proc means for the grand total, then a single-pass merge (if _N_=1 then set totals; set counts;) to compute each make's proportion. Self-contained, no edits needed." +} diff --git a/jenner-check/t006_makecounts_proportion/script.sas b/jenner-check/t006_makecounts_proportion/script.sas new file mode 100644 index 0000000..71a810c --- /dev/null +++ b/jenner-check/t006_makecounts_proportion/script.sas @@ -0,0 +1,30 @@ +/* Assignment 3 - macro variables call symput */ + +/* Step 1: Load the dataset */ +data cars; +set sashelp.cars; +run; + +/* Step 2: calculate total cars and total per make */ +proc means data=cars noprint nway; + class Make; + output out=make_counts (drop=_TYPE_ _FREQ_) n=vehicle_count; +run; + +/* Step 3: Use PROC MEANS again to get total vehicle count */ +proc means data=cars noprint; + output out=total_vehicles (drop=_TYPE_ _FREQ_) n=total; +run; + +/* Step 4: Calculate proportion */ +data result; + if _N_ = 1 then set total_vehicles; + set make_counts; + prct = vehicle_count / total; + keep Make prct; +run; + + +/* Step 5: Display final result */ +proc print data=result noobs; +run;