From b08fba23a2b5f0e4fea191688630c5b8d5542fac Mon Sep 17 00:00:00 2001 From: rpguiteras Date: Tue, 19 May 2026 15:55:06 -0400 Subject: [PATCH 01/21] Add .dtas frameset signing and tests Support signing Stata .dtas framesets end-to-end: add a frameset_file option to complete_datasignature.ado (bumped version to 3.0.4) which signs each frame, concatenates results, and ignores time-tainted frlink_* chars; add skip_char to control excluded characteristics and tolerate empty frames. Add dev helper dev_adopath_prefix to let tests point Stata at a local src/ during development. Extend Python SCons integration: accept a file_arg in get_datasign, emit the dev adopath prefix into generated recipes, add get_dtas_sign wrapper, and register .dtas in special_sig_fns so SCons pipelines can build/consume framesets. Add SCons test pipeline and several Stata smoke/unit test scripts (producer/consumer, smoke tests) and update tests/SConstruct and statacons_test.do to exercise the .dtas path. --- pypkg/src/pystatacons/dev_helpers.py | 28 ++++++++ pypkg/src/pystatacons/stata_utils.py | 18 ++++- src/complete_datasignature.ado | 90 ++++++++++++++++++++--- tests/SConstruct | 12 ++++ tests/code/dtas_consumer.do | 4 ++ tests/code/dtas_producer.do | 4 ++ tests/smoke_dtas.do | 104 +++++++++++++++++++++++++++ tests/smoke_scons_dtas.do | 41 +++++++++++ tests/statacons_test.do | 74 ++++++++++++++++++- 9 files changed, 361 insertions(+), 14 deletions(-) create mode 100644 pypkg/src/pystatacons/dev_helpers.py create mode 100644 tests/code/dtas_consumer.do create mode 100644 tests/code/dtas_producer.do create mode 100644 tests/smoke_dtas.do create mode 100644 tests/smoke_scons_dtas.do diff --git a/pypkg/src/pystatacons/dev_helpers.py b/pypkg/src/pystatacons/dev_helpers.py new file mode 100644 index 0000000..89368b9 --- /dev/null +++ b/pypkg/src/pystatacons/dev_helpers.py @@ -0,0 +1,28 @@ +"""Development-only helpers for statacons. + +These functions are no-ops in production. They activate only when +STATACONS_DEV_SRC is set in the environment, which is the convention +for working against an editable install of pystatacons paired with a +not-yet-released .ado source tree. + +Production users do not set STATACONS_DEV_SRC; everything here returns +the empty string or a no-op for them. +""" +import os + + +def dev_adopath_prefix(): + """Return an `adopath ++` line for the dev .ado tree, or empty string. + + When STATACONS_DEV_SRC is set, signature recipes need to find the + dev complete_datasignature.ado instead of whatever is installed in + the user's plus dir. Callers prepend this string to recipe.do + contents so the dev .ado wins precedence on Stata's adopath. + + In production (env var unset) this returns "" and recipes are + unchanged. + """ + dev_src = os.environ.get('STATACONS_DEV_SRC') + if not dev_src: + return '' + return 'adopath ++ "' + dev_src.replace('\\', '/') + '"\n' diff --git a/pypkg/src/pystatacons/stata_utils.py b/pypkg/src/pystatacons/stata_utils.py index 213572b..eb0a34b 100644 --- a/pypkg/src/pystatacons/stata_utils.py +++ b/pypkg/src/pystatacons/stata_utils.py @@ -17,6 +17,7 @@ from .configuration import configuration, query_config, print_config from .special_sigs import monkey_patch_scons, special_sig_fns +from .dev_helpers import dev_adopath_prefix has_pywin32 = False if platform.system() == "Windows": @@ -430,7 +431,7 @@ def stata_run_params_factory(self: Environment, target: Union[list, str], source int_env = None -def get_datasign(fname): +def get_datasign(fname, file_arg='dta_file'): if 'STATABATCHEXE' not in int_env: raise LookupError("Can't find Stata") m_str = int_env['CONFIG']['SCons']['use_custom_datasignature'] @@ -444,7 +445,7 @@ def get_datasign(fname): fname_abs = os.path.abspath(fname) sig_fname = "sig-" + get_basic_hash(fname_abs) + ".txt" - st_cmd_split = (['complete_datasignature,', 'dta_file(' + fname_abs + ')', 'fname(' + sig_fname + ')'] + meta_arg_split + st_cmd_split = (['complete_datasignature,', file_arg + '(' + fname_abs + ')', 'fname(' + sig_fname + ')'] + meta_arg_split + fast_arg_split + vv_only_arg_split) st_cmd = ' '.join(st_cmd_split) @@ -455,6 +456,7 @@ def get_datasign(fname): recipe_fname = recipe_basename + ".do" log_name = recipe_basename + ".log" with open(recipe_fname, "w") as recipe: + recipe.write(dev_adopath_prefix()) recipe.write(st_cmd + '\n') args_split = [int_env['STATABATCHEXE'], int_env['STATABATCHFLAG'], "do", recipe_fname] cmd_line = int_env['STATABATCHCOM'] + " do " + recipe_fname @@ -484,6 +486,15 @@ def get_datasign(fname): sfi.SFIToolkit.pollnow() return sig + +def get_dtas_sign(fname): + """Timestamp-independent signature for a .dtas frameset. + Delegates to get_datasign with file_arg='frameset_file' so all config + plumbing (custom metadata mode, fast/slow, cache_dir) is shared. + """ + return get_datasign(fname, file_arg='frameset_file') + + # Used to use packaging.version.parse from pkg_resources's packaging, but that's now deprecated. # Could have used the packaging package directly, but didn't want to add another dependency for a rare case. def version_lessthan(version_a_str, version_b_tuple): @@ -524,7 +535,7 @@ def init_env(env: Environment = None, tools: list = [], patch_scons_sig_fns: boo List of tools to initialize the returned environment with. patch_scons_sig_fns : Whether to patch the SCons file signature functions to support special signature functions by file extensions. - Default support is provided for .dta files. + Default support is provided for .dta and .dtas (frameset) files. skip_scons_vs_check: If false, will not output a warning when using a version of SCons that has not been tested. """ @@ -585,6 +596,7 @@ def init_env(env: Environment = None, tools: list = [], patch_scons_sig_fns: boo if m_str != "False": monkey_patch_scons() special_sig_fns[".dta"] = get_datasign + special_sig_fns[".dtas"] = get_dtas_sign if not GetOption('silent'): if m_str != "DataOnly" and m_str != "Datasignature" and m_str != "LabelsFormatsOnly": diff --git a/src/complete_datasignature.ado b/src/complete_datasignature.ado index a2dccc1..f2c404f 100644 --- a/src/complete_datasignature.ado +++ b/src/complete_datasignature.ado @@ -6,11 +6,59 @@ version 16.1 // .sthlp compiled using markdown program complete_datasignature, rclass -* sometimes due to weird shell issues we have loc 0 as (even when we don't quote the filename: +* sometimes due to weird shell issues we have loc 0 as (even when we don't quote the filename: * , "dta_file(file name with space)" loc 0 : subinstr local 0 `", "dta"' ", dta" loc 0 : subinstr local 0 `")""' ")" -syntax, [dta_file(string) fname(string) nometa fast labels_formats_only] +syntax, [dta_file(string) frameset_file(string) fname(string) nometa fast labels_formats_only skip_char(string)] + +if "`frameset_file'" != "" { + // frameset (.dtas) path: sign each frame in the archive and concatenate. + // In interactive mode, round-trip the user's frames via a temp .dtas so memory + // is restored on exit. In batch mode, skip the round-trip for speed. + loc is_batch = ("`c(mode)'" == "batch") + loc need_restore = 0 + tempfile tempdtas_base + loc tempdtas "`tempdtas_base'.dtas" + + if !`is_batch' { + qui frames save "`tempdtas'", frames(_all) replace emptyok + loc need_restore = 1 + } + + qui frames use "`frameset_file'", clear + loc fnames "`r(frames)'" + + // Inner per-frame calls always drop frlink_* chars (they carry a timestamp via + // frlink_date). User-supplied skip_char patterns are appended. + loc inner_skip "frlink_*" + if `"`skip_char'"' != "" loc inner_skip "`inner_skip' `skip_char'" + + loc agg "" + loc sep "" + foreach f in `: list sort fnames' { + qui frame change `f' + qui complete_datasignature, `meta' `fast' `labels_formats_only' skip_char(`"`inner_skip'"') + loc agg "`agg'`sep'`f'=`r(signature)'" + loc sep "|" + } + + if `need_restore' { + qui frames use "`tempdtas'", clear + cap erase "`tempdtas'" + } + + if "`fname'" != "" { + tempname sig_handle + file open `sig_handle' using "`fname'", write text replace + file write `sig_handle' "`agg'" + file close `sig_handle' + } + di "`agg'" + return local signature "`agg'" + exit +} + if "`dta_file'"!="" qui use "`dta_file'" //datasignature set, saving("`fname'", replace) //sig file has weird format with other stuff //datasignature @@ -23,7 +71,9 @@ if "`meta'"!="nometa" { tempname meta_handle qui file open `meta_handle' using "`meta_fname'", write text replace - unab vlist : * + // tolerate empty data (e.g., a .dtas containing an empty frame) + cap unab vlist : * + if _rc loc vlist "" //value labels. Do seprately from vars as some might not be attached to variables and some might be attached to multiple qui label dir loc val_labels `r(names)' @@ -48,18 +98,24 @@ if "`meta'"!="nometa" { //don't use describe as that prints timestamp file write `meta_handle' `"`: data label'"' _newline - //chars: includes notes + //chars: includes notes. skip_char is a globlist of patterns to drop + //(used by the frameset path to skip frlink_* chars whose frlink_date + //is time-tainted). //can't use -char dir- as it only prints to screen loc evlist _dta `vlist' foreach ev in `: list sort evlist' { file write `meta_handle' "`ev'" _newline loc c_list : char `ev'[] foreach c in `: list sort c_list' { - file write `meta_handle' `"`c': `: char `ev'[`c']'"' _newline + loc skip_this 0 + foreach pat in `skip_char' { + if strmatch("`c'", "`pat'") loc skip_this 1 + } + if !`skip_this' file write `meta_handle' `"`c': `: char `ev'[`c']'"' _newline } } } - + //could add the sortlist: -describe, varlist; r(sortlist)- file close `meta_handle' @@ -86,20 +142,20 @@ end /*** -_version 3.0.3_ +_version 3.0.4_ complete_datasignature ====== -__complete_datasignature__ creates a signature for a Stata .dta-file that does __not__ depend on the embedded timestamp but __does__ depend on the data and, optionally, no other metadata, variable and value labels only, or all metadata. +__complete_datasignature__ creates a signature for a Stata .dta-file or .dtas frameset that does __not__ depend on the embedded timestamp but __does__ depend on the data and, optionally, no other metadata, variable and value labels only, or all metadata. -__complete_datasignature__ extends Stata's __datasignature__ by allowing the inclusion of different sets of metadata. +__complete_datasignature__ extends Stata's __datasignature__ by allowing the inclusion of different sets of metadata. When called with __frameset_file__, it signs every frame in a __.dtas__ frameset and returns a concatenation keyed by frame name. Syntax ------ -> complete_datasignature [, dta_file("file.dta") fname("sigfile.ext") nometa fast labels_formats_only] +> complete_datasignature [, dta_file("file.dta") frameset_file("file.dtas") fname("sigfile.ext") nometa fast labels_formats_only skip_char("globlist")] By default, __complete_datasignature__ will use the dta-file in memory to create create a signature that depends on the data and all metadata, but not the embedded timestamp. @@ -109,12 +165,23 @@ By default, __complete_datasignature__ will use the dta-file in memory to create | Option | Description | |----------------------------|----------------------------------------------------| | dta_file("file.dta") | Use "file.dta" instead of dta-file in memory | +| frameset_file("file.dtas") | Sign a __.dtas__ frameset; iterate frames alphabetically and return "frameA=sigA|frameB=sigB|..." | | fname("sigfile.ext") | write signature to "sigfile.ext" | | nometa | Do not include any metadata -- equivalent of Stata's __datasignature__ | | labels_formats_only | Include variable formats, variable and value labels | | fast | use ___datasignature__ in _fast_ mode -- faster but not machine-independent | +| skip_char("globlist") | Skip variable/dataset characteristics whose names match any pattern in the space-separated globlist. The __frameset_file__ path always adds __frlink_*__ to this list. | +### Behavior of __frameset_file__ + +When __frameset_file__ is set, the program: + +1. In interactive mode (__c(mode)__ is empty), saves all in-memory frames to a temporary __.dtas__ so user state can be restored on exit. In batch mode, this round-trip is skipped. +2. Loads the target __.dtas__ via __frames use, clear__. +3. For each frame in alphabetical order, computes a per-frame signature using the same metadata options (__nometa__, __fast__, __labels_formats_only__) plus __skip_char("frlink_*")__. +4. Assembles "frameA=sigA|frameB=sigB|...". +5. In interactive mode, restores the user's frames from the temporary __.dtas__. Example(s) ---------- @@ -138,6 +205,9 @@ Example(s) . complete_datasignature, nometa fast 74:12(71728):3831085005:186045760 + . complete_datasignature, frameset_file("myframeset.dtas") + census=74:12(71728):...|housing=50:12(...):... + diff --git a/tests/SConstruct b/tests/SConstruct index b1a644e..a2fc944 100644 --- a/tests/SConstruct +++ b/tests/SConstruct @@ -45,6 +45,18 @@ env.StataBuild( params='saving("outputs/auto-modified-escape.dta")' ) +# .dtas frameset pipeline (producer/consumer) +env.StataBuild( + target=['outputs/myset.dtas'], + source='code/dtas_producer.do', + depends=['inputs/auto-original.dta'] +) +env.StataBuild( + target=['outputs/foreign_from_dtas.dta'], + source='code/dtas_consumer.do', + depends=['outputs/myset.dtas'] +) + # Python builder env.Append(BUILDERS={'PyBuilder': Builder(action='python $SOURCE > out.log')}) env.PyBuilder(target="outputs/py.txt", source="code/test.py") diff --git a/tests/code/dtas_consumer.do b/tests/code/dtas_consumer.do new file mode 100644 index 0000000..4670f35 --- /dev/null +++ b/tests/code/dtas_consumer.do @@ -0,0 +1,4 @@ +frames use "outputs/myset.dtas", clear +frame change foreign_cars +keep make price +save "outputs/foreign_from_dtas.dta", replace diff --git a/tests/code/dtas_producer.do b/tests/code/dtas_producer.do new file mode 100644 index 0000000..8a99cc8 --- /dev/null +++ b/tests/code/dtas_producer.do @@ -0,0 +1,4 @@ +use "inputs/auto-original.dta", clear +frame put * if foreign==1, into(foreign_cars) +frame put * if foreign==0, into(domestic_cars) +frames save "outputs/myset.dtas", frames(default foreign_cars domestic_cars) replace diff --git a/tests/smoke_dtas.do b/tests/smoke_dtas.do new file mode 100644 index 0000000..3ea9342 --- /dev/null +++ b/tests/smoke_dtas.do @@ -0,0 +1,104 @@ +// ============================================================ +// DEV SCAFFOLD -- not part of the formal statacons_test.do suite. +// Kept in tests/ for quick standalone validation of the .dtas +// signature path during development. The corresponding asserts +// are mirrored into statacons_test.do; this file just runs them +// in isolation without the rest of the harness. +// ============================================================ +// Focused regression test for .dtas signature support +// Run from C:/Users/rpguiter/Work/statacons/tests with: StataMP-64.exe -e do smoke_dtas.do + +clear all +adopath ++ "../src" +cap mkdir outputs + +// ============================================================ +// 1. Determinism: identical content, different save times -> identical signatures +// ============================================================ +clear all +sysuse auto, clear +frame put make price, into(prices) +frames save "outputs/_dtas_a.dtas", frames(default prices) replace +sleep 1500 +frames save "outputs/_dtas_b.dtas", frames(default prices) replace + +complete_datasignature, frameset_file("outputs/_dtas_a.dtas") +local dtas_sig_a "`r(signature)'" +complete_datasignature, frameset_file("outputs/_dtas_b.dtas") +local dtas_sig_b "`r(signature)'" + +di as txt "sig_a = " as res "`dtas_sig_a'" +di as txt "sig_b = " as res "`dtas_sig_b'" +_assert "`dtas_sig_a'"=="`dtas_sig_b'", msg("determinism across re-save failed") +di as result "PASS: determinism across re-save" + +// ============================================================ +// 2. Mutation isolation: change one frame -> only that slot differs +// ============================================================ +clear all +sysuse auto, clear +frame put make price, into(prices) +replace price = price + 1 in 1 +frames save "outputs/_dtas_a.dtas", frames(default prices) replace +complete_datasignature, frameset_file("outputs/_dtas_a.dtas") +local dtas_sig_mut "`r(signature)'" + +di as txt "sig_mut = " as res "`dtas_sig_mut'" +_assert "`dtas_sig_a'"!="`dtas_sig_mut'", msg("mutation should change signature") +di as result "PASS: mutation changes aggregate signature" + +// inspect: only the default-frame slot should differ +tokenize `"`dtas_sig_a'"', parse("|") +local a_default "`1'" +local a_prices "`3'" +tokenize `"`dtas_sig_mut'"', parse("|") +local m_default "`1'" +local m_prices "`3'" +di as txt "default slot orig: " as res "`a_default'" +di as txt "default slot mut: " as res "`m_default'" +di as txt "prices slot orig: " as res "`a_prices'" +di as txt "prices slot mut: " as res "`m_prices'" +_assert "`a_default'"!="`m_default'", msg("default slot should differ (mutated)") +_assert "`a_prices'"=="`m_prices'", msg("prices slot should be stable (unmutated)") +di as result "PASS: mutation isolated to the changed frame" + +// ============================================================ +// 3. frlink_* insensitivity: re-saving a linked frameset refreshes +// frlink_date but the .dtas signature MUST stay stable. +// ============================================================ +clear all +sysuse auto, clear +frame put rep78 if !mi(rep78), into(quality) +frame change quality +contract rep78 +gen quality_label = "rating " + string(rep78) +frame change default +frlink m:1 rep78, frame(quality) +frames save "outputs/_dtas_link.dtas", frames(default quality) replace +complete_datasignature, frameset_file("outputs/_dtas_link.dtas") +local dtas_sig_link1 "`r(signature)'" +di as txt "link sig (1) = " as res "`dtas_sig_link1'" + +clear all +sysuse auto, clear +frame put rep78 if !mi(rep78), into(quality) +frame change quality +contract rep78 +gen quality_label = "rating " + string(rep78) +frame change default +sleep 1500 +frlink m:1 rep78, frame(quality) +frames save "outputs/_dtas_link.dtas", frames(default quality) replace +complete_datasignature, frameset_file("outputs/_dtas_link.dtas") +local dtas_sig_link2 "`r(signature)'" +di as txt "link sig (2) = " as res "`dtas_sig_link2'" + +_assert "`dtas_sig_link1'"=="`dtas_sig_link2'", msg("frlink_* characteristics leaked into signature") +di as result "PASS: frlink_* characteristics excluded from signature" + +// cleanup +cap erase "outputs/_dtas_a.dtas" +cap erase "outputs/_dtas_b.dtas" +cap erase "outputs/_dtas_link.dtas" + +di _newline as result "ALL .dtas SMOKE TESTS PASSED" diff --git a/tests/smoke_scons_dtas.do b/tests/smoke_scons_dtas.do new file mode 100644 index 0000000..b24a065 --- /dev/null +++ b/tests/smoke_scons_dtas.do @@ -0,0 +1,41 @@ +// ============================================================ +// DEV SCAFFOLD -- not part of the formal statacons_test.do suite. +// Kept in tests/ for quick standalone validation of the SCons +// .dtas pipeline (producer -> .dtas -> consumer -> .dta) during +// development. Mirrors the SCons-end-to-end section in +// statacons_test.do; this file just runs it in isolation. +// ============================================================ +// Focused SCons end-to-end test for .dtas signature path +// Verifies that the Python env-var dev hatch works from within Stata. + +clear all +adopath ++ "../src" + +local dev_src "`c(pwd)'/../src" +di as txt "dev_src = `dev_src'" +python: import os; os.environ['STATACONS_DEV_SRC'] = r"`dev_src'" +python: import os; print('STATACONS_DEV_SRC =', os.environ.get('STATACONS_DEV_SRC')) + +// Clean and build +statacons -c +statacons outputs/myset.dtas outputs/foreign_from_dtas.dta + +cap program drop store_modts +program store_modts + syntax anything, local(string) + filesys `c(pwd)'/`anything', attr + c_local `local' "`r(modifiednum)'" +end + +cap store_modts outputs/myset.dtas, local(mod1_dtas) +cap store_modts outputs/foreign_from_dtas.dta, local(mod1_dta) +di as txt "After first build: mod1_dtas = `mod1_dtas' mod1_dta = `mod1_dta'" + +// Re-run -- should be a no-op +statacons outputs/myset.dtas outputs/foreign_from_dtas.dta +cap store_modts outputs/myset.dtas, local(mod2_dtas) +cap store_modts outputs/foreign_from_dtas.dta, local(mod2_dta) +di as txt "After second run: mod2_dtas = `mod2_dtas' mod2_dta = `mod2_dta'" + +_assert "`mod1_dtas'`mod1_dta'"=="`mod2_dtas'`mod2_dta'", msg(".dtas pipeline re-ran despite no input change") +di as result "PASS: .dtas pipeline does not rebuild on re-run with identical inputs" diff --git a/tests/statacons_test.do b/tests/statacons_test.do index 8493ccf..1f0d2b9 100644 --- a/tests/statacons_test.do +++ b/tests/statacons_test.do @@ -60,6 +60,63 @@ _assert "`r2'"!="`r4'", msg("Should be different") _assert "`r3'"!="`r4'", msg("Should be different") +*************************** Test .dtas signature: determinism, mutation, frlink *************************** +* 1. Determinism: identical content saved at different times -> identical signatures +clear all +sysuse auto, clear +frame put make price, into(prices) +frames save "outputs/_dtas_a.dtas", frames(default prices) replace +sleep 1500 +frames save "outputs/_dtas_b.dtas", frames(default prices) replace +complete_datasignature, frameset_file("outputs/_dtas_a.dtas") +loc dtas_sig_a "`r(signature)'" +complete_datasignature, frameset_file("outputs/_dtas_b.dtas") +loc dtas_sig_b "`r(signature)'" +_assert "`dtas_sig_a'"=="`dtas_sig_b'", msg(".dtas signature changed on re-save with identical content") + +* 2. Mutation isolation: change one frame -> aggregate signature differs +clear all +sysuse auto, clear +frame put make price, into(prices) +replace price = price + 1 in 1 +frames save "outputs/_dtas_a.dtas", frames(default prices) replace +complete_datasignature, frameset_file("outputs/_dtas_a.dtas") +loc dtas_sig_mut "`r(signature)'" +_assert "`dtas_sig_a'"!="`dtas_sig_mut'", msg("Mutating a frame didn't change .dtas signature") + +* 3. frlink_* insensitivity: re-saving a linked frameset refreshes frlink_date +* in the link variable's characteristics; the .dtas signature MUST stay stable. +clear all +sysuse auto, clear +frame put rep78 if !mi(rep78), into(quality) +frame change quality +contract rep78 +gen quality_label = "rating " + string(rep78) +frame change default +frlink m:1 rep78, frame(quality) +frames save "outputs/_dtas_link.dtas", frames(default quality) replace +complete_datasignature, frameset_file("outputs/_dtas_link.dtas") +loc dtas_sig_link1 "`r(signature)'" + +clear all +sysuse auto, clear +frame put rep78 if !mi(rep78), into(quality) +frame change quality +contract rep78 +gen quality_label = "rating " + string(rep78) +frame change default +sleep 1500 +frlink m:1 rep78, frame(quality) +frames save "outputs/_dtas_link.dtas", frames(default quality) replace +complete_datasignature, frameset_file("outputs/_dtas_link.dtas") +loc dtas_sig_link2 "`r(signature)'" +_assert "`dtas_sig_link1'"=="`dtas_sig_link2'", msg("frlink_* characteristics leaked into .dtas signature") + +cap erase "outputs/_dtas_a.dtas" +cap erase "outputs/_dtas_b.dtas" +cap erase "outputs/_dtas_link.dtas" + + *************************** Test output *************************** *MANUAL look at all of these * Test info @@ -103,7 +160,22 @@ rm "output space/auto-modified.dta" //to rebuild the first step statacons store_modts outputs/auto-modified2.dta, local(mod2) _assert "`mod1'"=="`mod2'", msg("Didn't do early stopping") -*MANUAL: Check that dta sig was called in second run +*MANUAL: Check that dta sig was called in second run + +*Test .dtas signature in SCons: build a frameset pipeline, then re-run -> no rebuild +* When running against the dev tree (editable pystatacons install), point statacons +* at the dev .ado source so signature recipes find our complete_datasignature.ado +* (the released version installed in plus/ predates the frameset_file() option). +local dev_src "`c(pwd)'/../src" +python: import os; os.environ['STATACONS_DEV_SRC'] = r"`dev_src'" +statacons -c +statacons outputs/myset.dtas outputs/foreign_from_dtas.dta +store_modts outputs/myset.dtas, local(mod1_dtas) +store_modts outputs/foreign_from_dtas.dta, local(mod1_dta) +statacons outputs/myset.dtas outputs/foreign_from_dtas.dta --debug=explain +store_modts outputs/myset.dtas, local(mod2_dtas) +store_modts outputs/foreign_from_dtas.dta, local(mod2_dta) +_assert "`mod1_dtas'`mod1_dta'"=="`mod2_dtas'`mod2_dta'", msg("Re-ran .dtas pipeline despite no input change") *************************** Test options *************************** *Test assume-built (easier to test these with timestamp Decider). From fba9a28fa59d2249f91c33e0ab549f2962db1768 Mon Sep 17 00:00:00 2001 From: rpguiteras Date: Wed, 20 May 2026 16:17:51 -0400 Subject: [PATCH 02/21] Extract test helper programs into .ado files Remove embedded program definitions from tests/statacons_test.do and add separate helper files: tests/store_modts.ado, tests/touch_dta.ado, and tests/write_txt.ado. This separates reusable test helper programs (store_modts, touch_dta, write_txt) into their own .ado files for clarity and reuse; no functional changes intended. --- tests/statacons_test.do | 25 ------------------------- tests/store_modts.ado | 6 ++++++ tests/touch_dta.ado | 7 +++++++ tests/write_txt.ado | 7 +++++++ 4 files changed, 20 insertions(+), 25 deletions(-) create mode 100644 tests/store_modts.ado create mode 100644 tests/touch_dta.ado create mode 100644 tests/write_txt.ado diff --git a/tests/statacons_test.do b/tests/statacons_test.do index 1f0d2b9..93ef2ea 100644 --- a/tests/statacons_test.do +++ b/tests/statacons_test.do @@ -16,31 +16,6 @@ if substr(`"$S_ADO"',3,6)!="../src" { adopath ++ "`c(pwd)'/../src" } -cap program drop store_modts -program store_modts - syntax anything, local(string) - - filesys `c(pwd)'/`anything', attr - c_local `local' "`r(modifiednum)'" -end - -cap program drop write_txt -program write_txt - syntax anything, fname(string) - - file open hand using "`fname'", write text replace - file write hand "`anything'" - file close hand -end - -cap program drop touch_dta -program touch_dta - syntax anything - - preserve - use `anything', clear - save `anything', replace -end *************************** Test output *************************** sysuse auto, clear diff --git a/tests/store_modts.ado b/tests/store_modts.ado new file mode 100644 index 0000000..6357bd6 --- /dev/null +++ b/tests/store_modts.ado @@ -0,0 +1,6 @@ +program define store_modts + syntax anything, local(string) + + filesys `c(pwd)'/`anything', attr + c_local `local' "`r(modifiednum)'" +end diff --git a/tests/touch_dta.ado b/tests/touch_dta.ado new file mode 100644 index 0000000..161d37e --- /dev/null +++ b/tests/touch_dta.ado @@ -0,0 +1,7 @@ +program define touch_dta + syntax anything + + preserve + use `anything', clear + save `anything', replace +end diff --git a/tests/write_txt.ado b/tests/write_txt.ado new file mode 100644 index 0000000..5598958 --- /dev/null +++ b/tests/write_txt.ado @@ -0,0 +1,7 @@ +program define write_txt + syntax anything, fname(string) + + file open hand using "`fname'", write text replace + file write hand "`anything'" + file close hand +end From bae436112f7e247e6e526ab30e401a415815643a Mon Sep 17 00:00:00 2001 From: rpguiteras Date: Wed, 20 May 2026 18:29:21 -0400 Subject: [PATCH 03/21] Add CLAUDE.md and notes/ to .gitignore Both are local-only private files (project instructions and session log) that should not appear in the public repo. Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 03dbba0..e6a3a29 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ local/ .vscode/ docs/.buildinfo *.stswp +CLAUDE.md +notes/ From 1fccdd70ff5d7d20a9829d71f4742435b6ce6736 Mon Sep 17 00:00:00 2001 From: rpguiteras Date: Wed, 20 May 2026 18:29:35 -0400 Subject: [PATCH 04/21] Add frames/datasets/ -- example datasets for frameset do-files Includes Stata-bundled datasets (auto, census) and Stata Press webuse datasets (persons, txcounty, family, discharge1/2, hsng) along with _refresh_datasets.do to re-download the webuse datasets from Stata's servers. Co-Authored-By: Claude Sonnet 4.6 --- frames/datasets/_refresh_datasets.do | 11 +++++++++++ frames/datasets/auto.dta | Bin 0 -> 12766 bytes frames/datasets/auto16.dta | Bin 0 -> 12208 bytes frames/datasets/auto2.dta | Bin 0 -> 12769 bytes frames/datasets/census.dta | Bin 0 -> 6189 bytes frames/datasets/discharge1.dta | Bin 0 -> 159859 bytes frames/datasets/discharge2.dta | Bin 0 -> 159859 bytes frames/datasets/family.dta | Bin 0 -> 26152 bytes frames/datasets/hsng.dta | Bin 0 -> 11364 bytes frames/datasets/persons.dta | Bin 0 -> 3154 bytes frames/datasets/txcounty.dta | Bin 0 -> 3168 bytes 11 files changed, 11 insertions(+) create mode 100644 frames/datasets/_refresh_datasets.do create mode 100644 frames/datasets/auto.dta create mode 100644 frames/datasets/auto16.dta create mode 100644 frames/datasets/auto2.dta create mode 100644 frames/datasets/census.dta create mode 100644 frames/datasets/discharge1.dta create mode 100644 frames/datasets/discharge2.dta create mode 100644 frames/datasets/family.dta create mode 100644 frames/datasets/hsng.dta create mode 100644 frames/datasets/persons.dta create mode 100644 frames/datasets/txcounty.dta diff --git a/frames/datasets/_refresh_datasets.do b/frames/datasets/_refresh_datasets.do new file mode 100644 index 0000000..8a1a16a --- /dev/null +++ b/frames/datasets/_refresh_datasets.do @@ -0,0 +1,11 @@ +// _refresh_datasets.do +// Downloads the correct webuse example datasets and saves them locally. +// Run once from this directory to populate ../datasets/. + +cd "C:/Users/rpguiter/Work/StataFrames/documentation/applications/datasets" + +foreach ds in persons txcounty discharge1 discharge2 family hsng { + webuse `ds', clear + save "`ds'.dta", replace + di "Saved `ds'.dta" +} diff --git a/frames/datasets/auto.dta b/frames/datasets/auto.dta new file mode 100644 index 0000000000000000000000000000000000000000..072478e988b9be4d6e68431394442f9cad9dc274 GIT binary patch literal 12766 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 LumL*%qr?9{sIeR% literal 0 HcmV?d00001 diff --git a/frames/datasets/auto16.dta b/frames/datasets/auto16.dta new file mode 100644 index 0000000000000000000000000000000000000000..00e8e838319d5ba5226213ea655d86cc60e101eb GIT binary patch literal 12208 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 literal 0 HcmV?d00001 diff --git a/frames/datasets/census.dta b/frames/datasets/census.dta new file mode 100644 index 0000000000000000000000000000000000000000..96ad0a2730e623f0763fe4e4f2867904e4e02c1f GIT binary patch literal 6189 zcmd^EdstLu8vmU$hs$skyy1Nm(LiA2jyjGU0fmsk0GY^EJu`EFkzt0NGoWa$mX|hL zGtKN`S($b*D@!v?^Fpk4v2x4Ql3K5M!F%nRkGrz(IcGp^1NT||W1nXSnD1QP-}hd> z_kF()gX9rB!bF=#Fd8aE!6v$m2Dj)C1xYj}B%~SiEjI>Bokw)JWr8t9qYV1%w}w1p zFbw)UqrqfM1nAOaG&lr{=rDFm7?M6%mnk}>8cAm(MmkHKPUa{Y^eTnHW3LkFNmaEm zOqZ(TtKGW9!HLPbgp{GlNe2D(w+4NScvV8Rk=Q7ocfDgAqh3^a#~6)zKi)ew`FIJv zLvx^d(Jx3HXLnV{#okdDq29-OM}uDGYN!?5cEREhCwl6tMadWf7FU{ox#8!s_u8QU z6{!q$3Z$x0QD2??x4#vFM9gmZ?bm=?EVsLy*dN{&LA9$Io|LxU(sLqk+=?J&VnQ0) zl!0bQNUw&oO(sN;nsN(*8n;Dgdxk2}CX|a1+hk!A1y2Qtq^(bt;3j{yLtwMly4=>b zC!knv^_xL*xjhcM8`>5DM9JxD^QHXa)&xqrYg-1W`LG>=%+RdR z?9fr|2zlEq9txcuIw!OpJKR!+c8x?YN^Qc7Xq^EcDC(CsOn;)-*K9*^i&3%5?NSlnW*UGT^?=)aiI+YNY& zm`A+r3CFssYaFULB+;FjFji;KrReHJ!F|UjOwoBUm3q^K!CE1>1uLy?YHaLQ$!O63+Y`BOH}V7!ssv!e z`Uf$KAEUvn$-~e%mqTo7Fs8m0jW0L&;+;1lQC4=EKlKPOZ5vQm0T5EEB%l|W$6<3{ zUqn3(EGc8aMsny82l!pm;Pc&k_$*)GVkh83Np^9CH5L56?bxia%Wlgw_HL`7*Lna7u#Yg+NFhn)(h(=LGY%;Y(2?QdtQ!@9#+IU0vv^rd0>GD%72 zdBXt~YtuAXrDc)na=6@r%>@-%z~M|pEZ)x7V5?S(nLGNR*G>(B7eo@zWBl1%Dtvzg z9?c=1E~itplKE>qAfCkxewnO??Q0g1o3;3TV?Sgp@_|!`k~8+mCn>#er@ytyK$hqb zrU-61;g>E)R<;H!!nD}-BEX$y@Yi|17<2+?D)%L?oZ~fQ*KuJ0iGxujtlcI+VHq)s z{JG@)C8%EKk7M~gkg=f}ows+xO9eAX+JKSQmNH0xmBF&E3`Wx~u$;`JB1iKY@b?#C zf3Yvde-e&R<4{aL;)hLKGI`}VzjF<6t|No@9|m$rR=XWuWKJ%JqKzSl7|fuY(_ruH z0Mte^&}|7qQqocWB5iNR#seiJajs3MkedjVXKogU5#=EmU&Y|D3=K*)X>ozBeRuhh zw2i#bo$4Q1r=c4m9S*zGWtSkEHX?Tni+%UK1?IaT^jy&&n|^oz!}*TbKWq{4JkOUb zWYD~ZLCP*dI^~2z(WfF!EC%l^!fvZC>hr^~qSG+EcFrH~eww00U&BV=pNGk=&k@o! zMId*zAbA!YYGbiQ&|qA;7R?9y0H=NMaR(9;7x*(}Kw%|dB3<*;sP!WAidggs>yHKX zEJdLo_C|Myc@2kyff1zV1%Bx`ig;pUAzkxCr>DkRS*J>ySE|9OgK4N7%b__r2s29t z;#h_+22Sot$dml>*}#742FCNi7*~y5QZ*$DjUj)IiOaz7IX*b?aR}}@Gze*fe6b)T zmMnCT-#VY{`Z};>6p%02os^AbO$nKw%^*2C4DTi~*tA}QPxv6z4q%Z!z@Lym@f~6S ze?2gqw$%B8yUszyi-0mROM`{?W?=C^4iT>hL*Mnw5w>C<7Dx;q1!E`fMrLN0mb3jgpvUzm0u1p4VACUg5L9BtLruYGzHB9)Kk^fz7`TfVTz*?d()yHK9svqZiiG+E zBN8b0=o5lr85@f+_xPdk$!H}K<~&3z>rpL=dnS=> zFZ1tg2ZV2c&nS-aT~3eU6BSv^fn|cQ>nhOUF&2ZS_@m%%28-{bM2nNWKLz3AI*Njh z6oI0}t>kJ2Y075N_vfw{d5FQROB^Ql4TC3AgRjFwN!qjggXzEvX+R9EGLu*@&ZY(->mabs#p3Tx{%8b)k17KQd694I0zAK!K{)AY5~t{L==4Xbuu~c{ais6%3jzdM}i&O1Zmx9da;5^(BgSIosd6C1_S;44pV6fR7 zqC|XpB*4%*`QR=US?Y3ELPeHRRy-ZZq)<|=PfP`GX~4{1bSQrR2qByKE6Z8TOJ>pO z5+UPtnS$Fz$w;6R4HMO{SGJYH5#x&^K&QN3`IY+Rq) zLy3fc6a$T=l)-3yjuo9wsm@U=$h8+^X=R)J-a~zPAeLwJ$K?rgk#c`mtgD_udSc{^ z!wgPUF_=s>w6MZu6LqXg>@6wg7y3ojlnpNP=QwDILZled0i0jBa3ZjAJ~FJuyQqnm3!now1a<@3Y2jq zb#T2Y=ZfwsN+=Wsva}`Cn;iPw56k~Vx%>?VLzns>^(e4y1xtE1@l8~#C)3&-L3)SDEmv8 zsRuIzkHBYdaQJ3J5T08dhfkmMMTt3HK?cpB^UM!`Bh(<6<8{~B!d#-k?rpvBb~+33 z9$#!s>5SNyG#L0%07?53f9Y=Ed?wJBR<_wLSzS)au7obkR2@!yJ{pERA8dDoqN#Hd zjxY1amag%Np5|wOr(Oi+Q$;e@xvEH7ItrEbG?#KniSa`m)eYMtWYdcr(iRb)KXByF z1N=fdaNbKf_vKJ|L$9>f2Fc@g$ep)$ykVD5PaPJAQ9g0>9<Dpkur H2m<~E($@vG literal 0 HcmV?d00001 diff --git a/frames/datasets/discharge1.dta b/frames/datasets/discharge1.dta new file mode 100644 index 0000000000000000000000000000000000000000..0ec54f9233de05536eb801cc2577f606bcca211c GIT binary patch literal 159859 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 literal 0 HcmV?d00001 diff --git a/frames/datasets/discharge2.dta b/frames/datasets/discharge2.dta new file mode 100644 index 0000000000000000000000000000000000000000..d9279828269613564264f2a85cbf794cecb7636b GIT binary patch literal 159859 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 C;Dr?c literal 0 HcmV?d00001 diff --git a/frames/datasets/family.dta b/frames/datasets/family.dta new file mode 100644 index 0000000000000000000000000000000000000000..b7bb839c6dec7a2290bc5aefa7a28ddc7db95b24 GIT binary patch literal 26152 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 literal 0 HcmV?d00001 diff --git a/frames/datasets/hsng.dta b/frames/datasets/hsng.dta new file mode 100644 index 0000000000000000000000000000000000000000..bd5bf0a16bf8498a49af540c4f426aaef30c2efc GIT binary patch literal 11364 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 B86yAy literal 0 HcmV?d00001 diff --git a/frames/datasets/persons.dta b/frames/datasets/persons.dta new file mode 100644 index 0000000000000000000000000000000000000000..75a59b11de7f17ee35174109147e2add0656c86c GIT binary patch literal 3154 zcmeHKJ!}&(6n1jG^q11K9ZDB&K>{J-T!j!VmQ`qRK!pl{gjlL@P2BdxB^TYft+D_k zW0$T>l|T$#_?ary0i^P?&?z&h6t)UPL0N+DY$s7*sw%?J8}9S7pP!$fpTB#Dd}c8# zon;nAbJWUG4Q9xco3Y#$g!rW zg8?5CEyU~`{mA{SqQvNEt zU@YXgg8=S1fS25PpU$~X){ty1Lt8zugAA-DThRC;1M4aPd8tum&ho4bqaOd<*cl)5 z4jktbgQ^NJR`5q4TwGjCgr(N0f=7Kn!M~I+%PTMFnZr<&4t^rg-_IxSGHP;z`g=hK zvLFHj>t92iB@M?!dH88qEv?$ozCM_EHTqb%HJo!#y z5ny~Htr{4JrP?%be3!KAVD%m8(81Cd(isJ>TT@-UF5e)BBH-C4(yfE!8*(@ToDZZY x3Pxk8xCX{INpBROx1=uudbdcw4r1H9whxy_KJy%&%ur(8=Pl@M5Np2?zX3XZuj&8* literal 0 HcmV?d00001 diff --git a/frames/datasets/txcounty.dta b/frames/datasets/txcounty.dta new file mode 100644 index 0000000000000000000000000000000000000000..d2e82d59d1fdfab805125b0d1849ca688b4a04d4 GIT binary patch literal 3168 zcmds4&2Jnv6nDPb2%?IX_JWWwLPbGA>z!?d(yeRSG+|nmv=LcQd#JX~tdkuvGhW%= zO_u{y;?fgv;l{BNoH!uCDIks<_yc<35{Z8Ss$RnL*q$UC<%XK7k-X2(^Zfk$dtVOK zh8y1Q7|v){@QzT7Dv=4U1&gA2N&-XIhDJzb1wLH;fRbI7-eBX1k{gWPWYfs~QF4>f zjIW7|y@8fRC*=iBi$vxE2yTh{T;oLc3zG^>i4&wI%>|f~_t?qk0$%1r+^9F+!%@_X z8kFpQr6gFI^B(gjqF?=Fe#}kodTPcU|Fx&5Cmeri($Vc>j{fzMqo-bW^i5A&GmgLK zDJAw)I^ZhhYnf=9VNYoGbm!k4N=6o~P{H9+ti0b*XZ0G&_5tR^M z(JrI1?fqwlmEh4#j@kh0zfn|nlc2;(v#U0xhxqy;`eebTpHDUP&k4r|=zMAh_5i*2 z=@_u|PP%I`t`(U#+bo$2AC_TINkm!f)f4%MUi zcTfR3^SocAkQutK24Yx4sE|h3Q3rob{pq54$~*DNPupd*f8(R;w^v;DPy&|ky~)$h z%N-Lxy6B$vdI*;k2Z zV*K$czFK-4FZ7k*{Tg1)d6wZTz~JldS@SktkprQ?$4d}q(8GkT;n_wR0Ts%v=LO}) z5u)5${Gak{`PN5EF3%jm5X&Q(ZZn#Vp^5YHIQs5xJn>-mt9TO4ig*gm%*Dsh?RVqj zXzX%4jm|ubDJjLNHY$TEK|I~l_Fy$z%h;CyUIP92@<^8oU178f9QPR)imqoXGAbV) z(TB;0{I+rzy%u}M2^eQ!ybj|n7;`XeI$3#J=xsN)k6v46?4#H2u{stM|4eGM2qDci xYAHTQHM+(X6il=t4OC9Fl;IVwC0dZ1sX#H$2Jljs1=J4<$iBCMgD(72@gH{8UIPFC literal 0 HcmV?d00001 From a3a357515931a81af2f0031e6a5562b82bfe7945 Mon Sep 17 00:00:00 2001 From: rpguiteras Date: Wed, 20 May 2026 18:29:42 -0400 Subject: [PATCH 05/21] Add frames/examples/ -- annotated frameset example do-files Four examples covering: frame create/change/copy/put/drop/reset (01), frlink/frget/fralias/frval (02), frame post Monte Carlo pattern (03), and frames save/use/modify/describe (04). Datasets loaded via `local datasets "../datasets"` relative to frames/datasets/. Co-Authored-By: Claude Sonnet 4.6 --- frames/examples/example-01-basic-frames.do | 43 ++++++++ frames/examples/example-02-frlink-frget.do | 99 +++++++++++++++++++ .../example-03-simulation-frame-post.do | 42 ++++++++ .../examples/example-04-frameset-save-use.do | 59 +++++++++++ 4 files changed, 243 insertions(+) create mode 100644 frames/examples/example-01-basic-frames.do create mode 100644 frames/examples/example-02-frlink-frget.do create mode 100644 frames/examples/example-03-simulation-frame-post.do create mode 100644 frames/examples/example-04-frameset-save-use.do diff --git a/frames/examples/example-01-basic-frames.do b/frames/examples/example-01-basic-frames.do new file mode 100644 index 0000000..838b6e0 --- /dev/null +++ b/frames/examples/example-01-basic-frames.do @@ -0,0 +1,43 @@ +// example-01-basic-frames.do +// Demonstrates: frame create, frame change, frame copy, frame put, +// frames dir, frame prefix, frame drop, frames reset +// +// Datasets required: none (uses sysuse auto, sysuse census) +// ---------------------------------------------------------------- + +clear all + +// ---- 1. Create and switch between frames ---- +sysuse auto // loads into default frame +frame // print current frame name +frames dir // list all frames + +frame create second +frames dir // now two frames + +frame change second +count // 0 obs -- empty frame +cwf default // switch back + +// ---- 2. Frame prefix: run a command in another frame ---- +frame second: sysuse census, clear // load census into 'second' +frames dir // default=auto(74x12), second=census(50x7) + +frame second: summarize pop // summarize without switching + +// ---- 3. Copy a frame ---- +frame copy second census_copy // full copy of second -> census_copy +frames dir + +// ---- 4. frame put: copy a subset ---- +// Start from auto in default frame +frame put make price mpg if foreign==1, into(foreign_cars) +frame foreign_cars: list in 1/5 + +// ---- 5. Drop frames ---- +frame drop second census_copy foreign_cars +frames dir + +// ---- 6. Reset ---- +clear all +frames dir // back to single 'default' frame diff --git a/frames/examples/example-02-frlink-frget.do b/frames/examples/example-02-frlink-frget.do new file mode 100644 index 0000000..09b115c --- /dev/null +++ b/frames/examples/example-02-frlink-frget.do @@ -0,0 +1,99 @@ +// example-02-frlink-frget.do +// Demonstrates: frlink (m:1 and 1:1), frget, frval(), fralias add +// +// Datasets used (from Stata Press webuse): +// persons: personid, countyid, income (20 obs) +// txcounty: countyid, median_income (8 obs) +// discharge1: patientid, age, sex, billed, ... (1980 obs) +// discharge2: patientid, age, sex, billed, ... (1980 obs, same structure) +// family: pid, pid_m, pid_f, x1-x5 (639 obs) +// ---------------------------------------------------------------- + +clear all + +cd "C:/Users/rpguiter/Work/statacons/frames/examples" +local datasets "../datasets" + +// ---- 1. m:1 linkage: persons linked to Texas counties ---- +// persons has countyid; txcounty has the county-level median income +use "`datasets'/persons", clear +frame create txcounty +frame txcounty: use "`datasets'/txcounty", clear + +frames dir +// persons: 20 person-level obs with countyid and income +// txcounty: 8 county-level obs with countyid and median_income + +// Create the linkage (m:1: many persons per county) +frlink m:1 countyid, frame(txcounty) + +// Inspect the linkage +frlink dir +frlink describe txcounty + +// Copy median_income from txcounty into the persons frame +frget median_income, from(txcounty) + +// Check result +describe +list countyid income median_income in 1/10 + +// ---- 2. Using frval() for inline access ---- +// Flag persons whose income exceeds their county median -- no variable copy needed +generate above_median = (income > frval(txcounty, median_income)) +tabulate above_median + +// ---- 3. 1:1 linkage: discharge data ---- +// discharge1 and discharge2 have the same structure (same patients, two records). +// Use frget syntax 2 (newvar = oldvar) to pull variables with explicit renaming. +clear all +use "`datasets'/discharge1", clear +frame create discharge2 +frame discharge2: use "`datasets'/discharge2", clear + +frlink 1:1 patientid, frame(discharge2) +frlink describe discharge2 + +// Pull billed amount from discharge2, renamed to distinguish from discharge1's billed +frget billed2 = billed, from(discharge2) +list patientid billed billed2 in 1/5 + +// ---- 4. fralias add (Stata 18+): memory-efficient alternative to frget ---- +clear all +use "`datasets'/persons", clear +frame create txcounty2 +frame txcounty2: use "`datasets'/txcounty", clear + +frlink m:1 countyid, frame(txcounty2) + +// Create aliases instead of copies -- uses very little extra memory +fralias add median_income, from(txcounty2) +fralias describe + +// Aliases work in most commands just like regular variables +summarize median_income income +regress income median_income + +// ---- 5. Generational data: linking a frame to itself ---- +clear all +use "`datasets'/family", clear +frame create family +frame family: use "`datasets'/family", clear // same data in a second frame named 'family' + +// Link each person to their mother and father +frlink m:1 pid_m, frame(family pid) generate(m) +frlink m:1 pid_f, frame(family pid) generate(f) + +// Get grandparent IDs via intermediate links +frget pid_mm = pid_m, from(m) // mother's mother +frget pid_mf = pid_f, from(m) // mother's father +frget pid_fm = pid_m, from(f) // father's mother +frget pid_ff = pid_f, from(f) // father's father + +// Create links to all four grandparents +frlink m:1 pid_mm, frame(family pid) generate(mm) +frlink m:1 pid_mf, frame(family pid) generate(mf) +frlink m:1 pid_fm, frame(family pid) generate(fm) +frlink m:1 pid_ff, frame(family pid) generate(ff) + +list pid pid_m pid_f pid_mm pid_mf pid_fm pid_ff in 1/10 diff --git a/frames/examples/example-03-simulation-frame-post.do b/frames/examples/example-03-simulation-frame-post.do new file mode 100644 index 0000000..ce46373 --- /dev/null +++ b/frames/examples/example-03-simulation-frame-post.do @@ -0,0 +1,42 @@ +// example-03-simulation-frame-post.do +// Demonstrates: frame create (with newvarlist), frame post, simulation loop +// +// Replicates the simulation use case from [D] frames intro and [P] frame post. +// Runs a small Monte Carlo experiment: tests whether a 95% CI for the slope +// in a simple OLS regression actually covers the true value 95% of the time. +// +// Datasets required: none +// ---------------------------------------------------------------- + +clear all +set seed 12345 + +local reps = 1000 // number of simulation replications +local n = 50 // sample size per replication +local beta = 2 // true slope + +// ---- 1. Create the results frame ---- +frame create results double(b se covered) +// 'results' is now a 0-obs dataset with three double-precision variables + +// ---- 2. Simulation loop ---- +forvalues i = 1/`reps' { + quietly { + clear + set obs `n' + gen x = rnormal() + gen y = 1 + `beta'*x + rnormal() // true slope = beta + regress y x + } + // Post one observation to the results frame + frame post results /// + (_b[x]) /// estimated slope + (_se[x]) /// standard error + (el(r(table),5,1) < `beta' & `beta' < el(r(table),6,1)) // 95% CI coverage +} + +// ---- 3. Analyze results ---- +frame change results +summarize b se covered + +di "95% CI coverage rate: " r(mean) " (should be ~0.95)" diff --git a/frames/examples/example-04-frameset-save-use.do b/frames/examples/example-04-frameset-save-use.do new file mode 100644 index 0000000..a686356 --- /dev/null +++ b/frames/examples/example-04-frameset-save-use.do @@ -0,0 +1,59 @@ +// example-04-frameset-save-use.do +// Demonstrates: frames save, frames use, frames modify, frames describe +// +// Replicates the frames save/use/modify examples from the help files. +// Datasets required: ../datasets/hsng.dta (plus sysuse auto, sysuse census) +// ---------------------------------------------------------------- + +clear all + +cd "C:/Users/rpguiter/Work/statacons/frames/examples" +local datasets "../datasets" + +// ---- 1. Build two frames ---- +frame create census +frame change census +sysuse census // 50 states, 1980 census + +frame create housing +frame change housing +use "`datasets'/hsng", clear // housing cost data + +// ---- 2. Save to a .dtas file ---- +frames save myframeset, frames(census housing) +// Creates myframeset.dtas in the current working directory + +// ---- 3. Describe the frameset on disk ---- +frames describe using myframeset // detailed +frames describe using myframeset, short // just obs/vars summary + +// ---- 4. Load the frameset back ---- +frames reset // clear everything +frames use myframeset, clear +frames dir +pwf // 'census' -- first frame saved + +// ---- 5. Partial load ---- +frames reset +frames use myframeset, frames(housing) // load only housing +frames dir // only housing in memory + +// ---- 6. frames modify: add a frame to the file ---- +frame create auto +frame change auto +sysuse auto + +frames modify using myframeset, add(auto) +frames describe using myframeset, short // now 3 frames in the file + +// ---- 7. frames modify: drop a frame from the file ---- +frames modify using myframeset, drop(housing) +frames describe using myframeset, short // back to 2 frames + +// ---- 8. save with linked option ---- +// (Requires frlink to exist; illustrative only) +// frames save mylinked, frames(persons) linked // saves linked frames too + +// ---- 9. Cleanup ---- +frames reset +erase myframeset.dtas From 39d07fbc0bd98bc65a977c3c42c8d08448d1a313 Mon Sep 17 00:00:00 2001 From: rpguiteras Date: Wed, 20 May 2026 18:29:48 -0400 Subject: [PATCH 06/21] Add frames/docs/ -- user-authored format and application source indexes Three files: sources-format.md (bibliography of .dtas format sources), sources-applications.md (bibliography of frames application sources), and stata-features-frameset.md (format summary notes). Verbatim Stata IP (.sthlp, PDFs, blog-post conversions) stays in a private repo. Co-Authored-By: Claude Sonnet 4.6 --- frames/docs/sources-applications.md | 222 +++++++++++++++++++++++++ frames/docs/sources-format.md | 149 +++++++++++++++++ frames/docs/stata-features-frameset.md | 52 ++++++ 3 files changed, 423 insertions(+) create mode 100644 frames/docs/sources-applications.md create mode 100644 frames/docs/sources-format.md create mode 100644 frames/docs/stata-features-frameset.md diff --git a/frames/docs/sources-applications.md b/frames/docs/sources-applications.md new file mode 100644 index 0000000..cd854ab --- /dev/null +++ b/frames/docs/sources-applications.md @@ -0,0 +1,222 @@ +--- +header-includes: + - \usepackage{amsmath} +--- + +# Sources: Applied Use of Stata Frames + +This document catalogues sources gathered on the practical use of Stata frames and `.dtas` framesets. All files are saved in this directory (`documentation/applications/`), organized into subfolders. + +--- + +## Subfolder Structure + +``` +applications/ + help-files/ Stata internal help files (.sthlp) + markdown conversions + datasets/ Example .dta datasets (Stata installation + Stata Press webuse) + official-docs/ PDF manuals for applied frame commands + blog-posts/ Stata blog posts with worked examples + examples/ Self-contained replicable .do files +``` + +--- + +## Stata Internal Help Files (`help-files/`) + +All `.sthlp` files were copied verbatim from `C:\Program Files\StataNow19\ado\base\f\`. Each was converted to a clean markdown (`.md`) file in the same folder. + +### `frames_intro.sthlp` / `frames_intro.md` +**Version:** 1.2.1, 05 Aug 2025 + +The primary practical guide to using frames. Covers all major use cases with worked +examples: multitasking, working with simultaneous datasets, simulation via `frame post`, +the preserve/restore performance benefit, and a full tutorial on every frames command. +Also covers ado and Mata programming patterns. Most valuable single source for +applied use. + +### `frames.sthlp` / `frames.md` +**Version:** 1.2.1, 05 Aug 2025 + +Quick-reference index listing syntax for every frame-related command and function with +one-line descriptions and cross-references. Good starting point for looking up syntax. + +### `frlink.sthlp` / `frlink.md` +**Version:** 1.1.1, 10 Jul 2024 + +Full syntax and examples for `frlink` (link frames via key variables). Documents +`1:1` and `m:1` linkages, the `frame()` option for different variable names across +frames, `frlink dir`, `frlink describe`, and `frlink rebuild`. Includes three detailed +examples: persons-counties (m:1), generational data with six simultaneous linkages, +and discharge data (1:1). + +### `frget.sthlp` / `frget.md` +**Version:** 1.1.0, 06 Mar 2023 + +Syntax and options for `frget` -- copies variables from a linked frame to the current +frame. Documents `prefix()`, `suffix()`, `exclude()` options and stored results. + +### `fralias.sthlp` / `fralias.md` +**Version:** 1.0.1, 15 Mar 2023 + +Syntax and examples for `fralias add` (Stata 18+) -- creates memory-efficient alias +variables that reference variables in linked frames without copying. Contrasts with +`frget`. Covers `fralias describe`. + +### `frames_save.sthlp` / `frames_save.md` +**Version:** 1.1.0, 20 Mar 2025 + +Full option set for `frames save`: `frames()`, `replace`, `linked`, `relaxed`, +`complevel()`, `nolabel`, `orphans`, `emptyok`. Notes that `linked` recursively saves +all transitively linked frames. + +### `frames_use.sthlp` / `frames_use.md` +**Version:** 1.1.0, 20 Mar 2025 + +Full option set for `frames use`: `frames()`, `clear`, `replace`. Notes on how +`clear` sets the working frame and how `replace` interacts with existing frames. + +### `frames_describe.sthlp` / `frames_describe.md` +**Version:** 1.0.0, 21 Feb 2023 + +Two syntaxes (in-memory vs. `using filename`). Documents `simple`, `short`, +`fullnames`, `numbers` options and stored results including `r(changed)`. + +### `frames_modify.sthlp` / `frames_modify.md` +**Version:** 1.0.1, 05 May 2025 + +Syntax for adding or dropping frames from a `.dtas` file on disk without loading the +full frameset into memory. Documents `add(framelist [, replace])` and `drop(framelist)`. + +### `frame_post.sthlp` / `frame_post.md` +**Version:** 1.0.0, 14 Jun 2019 + +The `frame create newframename newvarlist` / `frame post framename (exp)...` pattern +for accumulating results from simulations. Notes that `tempname` should be used for +the frame name in programs. Allows `strL` (unlike `postfile`). + +### `frame_put.sthlp` / `frame_put.md` +**Version:** 1.0.1, 13 Jan 2020 + +`frame put varlist [if] [in], into(newframename)` -- copies a subset of variables or +observations from the current frame to a new frame, leaving the current frame unchanged. + +### Additional `.sthlp` files copied (not converted to `.md`) + +The following were copied from the Stata installation for reference but are smaller +command pages fully covered by `frames_intro.md` and `frames.md`: + +- `frame_change.sthlp`, `frame_copy.sthlp`, `frame_drop.sthlp` +- `frame_prefix.sthlp`, `frame_putlabel.sthlp`, `frame_rename.sthlp` +- `frames_dir.sthlp`, `frames_reset.sthlp` + +--- + +## PDF Manuals (`official-docs/`) + +Downloaded from `https://www.stata.com/manuals/`. + +### `stata-frlink.pdf` +**URL:** https://www.stata.com/manuals/dfrlink.pdf + +Full [D] frlink manual including Quick start and Remarks and examples sections not +present in the help file. Contains detailed worked examples with the `persons` and +`txcounty` datasets and the generational family linkage example. + +### `stata-frget.pdf` +**URL:** https://www.stata.com/manuals/dfrget.pdf + +Full [D] frget manual including the explanation of how `frget` handles underscore +variables and match variables. + +### `stata-fralias.pdf` +**URL:** https://www.stata.com/manuals/dfralias.pdf + +Full [D] fralias manual including Quick start and detailed remarks on how alias +variables differ from copies and their memory implications. + +### `stata-frames-modify.pdf` +**URL:** https://www.stata.com/manuals/dframesmodify.pdf + +Full [D] frames modify manual including Quick start and Remarks. + +*Note: PDFs for `frames intro`, `frames save`, `frames use`, and `frames describe` were +downloaded during the format documentation phase and are in `documentation/format/`.* + +--- + +## Datasets (`datasets/`) + +### From Stata installation (`C:\Program Files\StataNow19\ado\base\`) + +| File | Description | Used in | +|------|-------------|---------| +| `auto.dta` | 1978 automobile data (74 obs, 12 vars) | General examples; `dtas.sthlp` | +| `auto2.dta` | Automobile data with extra variables | `dtas.sthlp` format example | +| `auto16.dta` | Automobile data (Stata 16 format) | Format testing | +| `census.dta` | 1980 US census by state (50 obs) | `frames_save` and `frames_describe` examples | + +### From Stata Press web server (`http://www.stata-press.com/data/r19/`) + +| File | Description | Used in | +|------|-------------|---------| +| `persons.dta` | Person-level data with `countyid` | `frlink` m:1 example | +| `txcounty.dta` | Texas county-level data | `frlink` m:1 example | +| `family.dta` | Generational family data with parent IDs | `frlink` self-link example | +| `discharge1.dta` | Hospital discharge data, part 1 | `frlink` 1:1 example | +| `discharge2.dta` | Hospital discharge data, part 2 | `frlink` 1:1 example | +| `hsng.dta` | Housing cost data (50 obs) | `frames_save` and `frames_modify` examples | + +--- + +## Blog Posts (`blog-posts/`) + +### `stata-blog-fun-with-frames-2019.md` +**URL:** https://blog.stata.com/2019/09/06/fun-with-frames/ +**Author:** Chuck Huber | **Date:** September 6, 2019 + +The Stata 16 launch blog post on frames. Demonstrates five applied scenarios: (1) +fitting models on multiple datasets and comparing estimates, (2) storing `margins` +output in a separate frame for a contour plot, (3) using `frval()` for inline +cross-frame calculations, (4) using `frget` to pull demographics into a longitudinal +dataset for mixed-effects modeling, and (5) opening 22 chromosome datasets +simultaneously in Stata/MP. Key practical insight: frames eliminate the +clear/load/run/save cycle when coordinating multiple datasets. + +### `stata-blog-framesets-alias-2023.md` +**URL:** https://blog.stata.com/2023/09/12/from-datasets-to-framesets-and-alias-variables-data-management-advances-in-stata/ +**Author:** Kreshna Gopal | **Date:** September 12, 2023 + +The Stata 18 blog post introducing framesets (`.dtas`) and alias variables. Covers the +full workflow: creating multiple frames, saving them with `frames save`, describing with +`frames describe using`, reloading with `frames use`, saving with the `linked` option, +and creating alias variables with `fralias add`. Includes a historical timeline of +Stata data management milestones from 1985 to 2023. + +*Note: An earlier version was saved in `documentation/format/stata-blog-framesets-alias-2023.md`.* + +--- + +## Example Do-files (`examples/`) + +Self-contained replicable scripts demonstrating key workflows. Each script lists +required datasets at the top. Where `webuse` is used, the dataset is also available +in the `datasets/` folder. + +### `example-01-basic-frames.do` +Basic frame management: `frame create`, `frame change`, `frame prefix`, `frame copy`, +`frame put`, `frame drop`, `frames reset`. Uses `sysuse auto` and `sysuse census`. + +### `example-02-frlink-frget.do` +Linking frames with `frlink`: m:1 (persons-counties), 1:1 (discharge data), self-link +(generational family). Also demonstrates `frget`, `fralias add`, and `frval()`. +Uses `webuse persons`, `webuse txcounty`, `webuse family`, `webuse discharge1/2`. + +### `example-03-simulation-frame-post.do` +Monte Carlo simulation using `frame create` / `frame post`. Runs 1,000 OLS replications +and collects results (slope estimate, SE, CI coverage) in a separate frame. Tests that +the 95% CI covers the true slope approximately 95% of the time. Uses no external datasets. + +### `example-04-frameset-save-use.do` +Full frameset lifecycle: `frames save`, `frames describe using`, `frames use`, +`frames modify add`, `frames modify drop`. Uses `sysuse census` and `webuse hsng`. diff --git a/frames/docs/sources-format.md b/frames/docs/sources-format.md new file mode 100644 index 0000000..2cbc2dd --- /dev/null +++ b/frames/docs/sources-format.md @@ -0,0 +1,149 @@ +--- +header-includes: + - \usepackage{amsmath} +--- + +# Sources: Stata `.dtas` File Format + +This document catalogues sources gathered on the `.dtas` file format (Stata framesets) and the +underlying `.dta` format. All files are saved in this directory (`documentation/format/`). + +--- + +## Official Stata Manuals (PDFs) + +### `stata-fileformats-dtas.pdf` +**URL:** https://www.stata.com/manuals/pfileformatsdtas.pdf + +The primary technical reference for the `.dtas` format, intended for programmers who want +other software to create or read frameset files. Documents the internal structure: a `.dtas` +file is a zip archive (using Stata's `zipfile`/`unzipfile` commands internally) containing +one `.dta` file per frame, plus a manifest of frame names and internal filenames. Covers +compression levels (0--9, default 1). This is the most authoritative source for format +internals. + +### `stata-fileformats-dta.pdf` +**URL:** https://www.stata.com/manuals/pfileformatsdta.pdf + +Current (Stata 18/19) technical specification of the underlying `.dta` file format, for +programmers. Covers the binary layout of a single-frame dataset: header, variable types +(byte, int, long, float, double, str#, strL), value labels, notes, and metadata chunks. +Format numbers 119--121 are documented here. Essential background since each frame inside a +`.dtas` is a `.dta` file. + +### `stata14-fileformats-dta.pdf` +**URL:** https://www.stata.com/manuals14/pfileformatsdta.pdf + +Stata 14 version of the `.dta` format spec, covering format 118. Useful for understanding +the format history and what changed between versions. Format 118 is the first version with +the XML-like chunked structure introduced in Stata 13. + +### `stata-frames-intro.pdf` +**URL:** https://www.stata.com/manuals/dframesintro.pdf + +Introduction to Stata frames (Stata 16+). Explains the conceptual model: multiple named +datasets held simultaneously in memory, linked via `frlink`. Background needed to understand +why framesets exist. + +### `stata-frames-save.pdf` +**URL:** https://www.stata.com/manuals/dframessave.pdf + +Manual page for the `frames save` command. Documents syntax, options (including `linked` to +auto-include linked frames and `complevel(#)` for zip compression), and behavior when +overwriting existing `.dtas` files. + +### `stata-frames-use.pdf` +**URL:** https://www.stata.com/manuals/dframesuse.pdf + +Manual page for the `frames use` command. Documents how a `.dtas` file is read back into +memory, including the `frames()` option to load a subset of frames and behavior on name +conflicts. + +### `stata-frames-describe.pdf` +**URL:** https://www.stata.com/manuals/dframesdescribe.pdf + +Manual page for the `frames describe` command. Shows how to inspect a `.dtas` file on disk +without loading it -- reporting frame names, variable counts, observation counts, and +`.dta` format version of each frame. + +### `stata-zipfile.pdf` +**URL:** https://www.stata.com/manuals/dzipfile.pdf + +Manual page for Stata's `zipfile` and `unzipfile` commands. Relevant because `.dtas` files +are zip archives; Stata uses these commands internally when saving and loading framesets. +Useful for understanding the container format at the zip level. + +--- + +## Stata Internal Help Files (SMCL, from local installation) + +These files were copied from the Stata 19 installation at +`C:\Program Files\StataNow19\ado\base\`. They are SMCL (Stata Markup and Control Language) +source files that Stata renders in its Help viewer. They may contain information not in the +public PDF manuals. + +### `stata-help-dtas.sthlp` +**Path:** `C:\Program Files\StataNow19\ado\base\d\dtas.sthlp` +**Version stamp:** 1.0.0, 06 Mar 2023 + +The most technically detailed single source for the `.dtas` format. Documents the internal +structure of the zip archive and, crucially, the exact format of the `.frameinfo` manifest +file that must be present in every `.dtas`: + +- Line 1: `*! VERSION 1` (`.frameinfo` schema version) +- Line 2: `*! COMPLEVEL n` (recorded but has no effect on reading) +- Remaining lines: three whitespace-separated columns -- frame name, `.dta` filename + (no extension, no spaces), and format number (e.g., 118) + +Also documents the optional per-frame `.hdr` header file (created with +`save filename, headeronly`) that allows `frames describe` to work efficiently without +loading the full dataset. + +### `stata-help-set_dtascomplevel.sthlp` +**Path:** `C:\Program Files\StataNow19\ado\base\s\set_dtascomplevel.sthlp` +**Version stamp:** 1.0.0, 21 Feb 2023 + +Documents `set dtascomplevel #` (integer 0--9, default 1). Explains the compression +tradeoff: higher levels produce smaller files but take longer; on slow I/O systems, +level 1 can be faster than level 0; levels 2--9 are rarely worth it unless file size +is the primary concern. + +--- + +## Unofficial / Third-Party Sources (Markdown) + +### `readstata13-cran-manual.md` +**URL:** https://cran.r-project.org/web/packages/readstata13/vignettes/readstata13_basic_manual.html + +Vignette for the `readstata13` R package, which can read `.dtas` files via `read.dtas()` +and inspect them via `get.frames()`. Documents supported format numbers by Stata version +(formats 102--121), `.dtas` handling, strL long-string support, missing value encoding, and +endianness. Useful as an independent implementer's perspective on the format. + +### `stata-blog-framesets-alias-2023.md` +**URL:** https://blog.stata.com/2023/09/12/from-datasets-to-framesets-and-alias-variables-data-management-advances-in-stata/ + +Official Stata blog post (September 2023) announcing frameset and alias variable features in +Stata 18. Provides historical context (frames added in Stata 16, framesets in Stata 18), +workflow examples, and a plain-language description of how `.dtas` files work. Good +orientation piece, not a format spec. + +### `stata-features-frameset.md` +**URL:** https://www.stata.com/features/overview/frameset/ + +Stata product features page for framesets. Brief marketing-oriented overview of `frames +save`, `frames use`, and `frames describe`, confirming that `.dtas` is described as "the +plural of `.dta`" and that compression is automatic. No format internals, but useful for +understanding intended use cases. + +--- + +## Sources Not Retrieved (Access Blocked) + +### Library of Congress: Stata Data File Format (.dta), Version 118 +**URL:** https://www.loc.gov/preservation/digital/formats/fdd/fdd000471.shtml + +The Library of Congress Sustainability of Digital Formats database entry for the Stata `.dta` +format (version 118 / format number 118). Likely contains format registry information, +file signatures, and preservation notes. Returned HTTP 403 at time of access (2026-05-19); +worth retrying or accessing via browser. diff --git a/frames/docs/stata-features-frameset.md b/frames/docs/stata-features-frameset.md new file mode 100644 index 0000000..edf65e1 --- /dev/null +++ b/frames/docs/stata-features-frameset.md @@ -0,0 +1,52 @@ +# Stata Features: Saving, Using, and Describing a Set of Frames (Framesets) + +Source: https://www.stata.com/features/overview/frameset/ +Fetched: 2026-05-19 + +--- + +## What Are Frame Sets? + +Frame sets represent Stata's solution for managing multiple related datasets simultaneously. +The feature allows users to bundle interconnected datasets into a single file format (**.dtas**), +enabling efficient project-based workflow management. + +## Core Capabilities + +The frameset functionality includes three primary operations: + +1. **Saving**: Users can save a set of frames through the `frames save` command, with automatic + compression applied. + +2. **Using**: The `frames use` command restores all datasets from a frameset file into memory + at once. + +3. **Describing**: The `frames describe` command provides inventory information about frames + and their contained variables, both in active memory and on disk. + +## Key Features + +**Automatic Linked Frame Handling**: When saving a frameset, users can specify the `linked` +option to automatically include all frames connected to the primary frame through linking +relationships. + +**Compression**: Framesets stored on disk receive automatic compressed formatting, reducing +file size without requiring manual intervention. + +**Syntax Consistency**: The command structure mirrors traditional dataset operations. Dataset +and frameset commands similarly handle things like labels, empty datasets, the level of detail +in describing datasets, etc. + +## File Format + +The **.dtas** extension represents "the plural of **.dta**," creating a standardized format +that Stata reads and writes seamlessly while remaining accessible to external programmers +through documented specifications. + +## Practical Example + +The documentation demonstrates a census and housing analysis workflow where users: +- Create separate frames for related datasets +- Link frames using `frlink` based on matching variables (state) +- Create variable aliases referencing linked frame data +- Bundle everything into a single **.dtas** file for later retrieval From 6bab84bf51f1373b59ea463d4e25de68ad1857c1 Mon Sep 17 00:00:00 2001 From: rpguiteras Date: Wed, 20 May 2026 18:29:55 -0400 Subject: [PATCH 07/21] Add interactive_roundtrip_test.do -- 5-case round-trip test for .dtas signing Tests: basic round-trip, empty default frame, name collision, frame count, and signature stability across re-saves. All cases pass. Must be run by opening Stata interactively and doing the file -- not with -e do. Co-Authored-By: Claude Sonnet 4.6 --- tests/interactive_roundtrip_test.do | 224 ++++++++++++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 tests/interactive_roundtrip_test.do diff --git a/tests/interactive_roundtrip_test.do b/tests/interactive_roundtrip_test.do new file mode 100644 index 0000000..1732fac --- /dev/null +++ b/tests/interactive_roundtrip_test.do @@ -0,0 +1,224 @@ +// ============================================================ +// MANUAL INTERACTIVE TEST -- run by opening Stata interactively, +// NOT with -e do or -b do (those set c(mode)=="batch" and skip +// the round-trip path this script is designed to exercise). +// +// How to run: +// 1. Open Stata interactively. +// 2. cd to C:/Users/rpguiter/Work/statacons/tests +// 3. do interactive_roundtrip_test.do +// +// What passes: all _assert lines exit without error; the final +// "ALL INTERACTIVE ROUND-TRIP TESTS PASSED" message is displayed. +// +// Note: user-defined programs are dropped by frames use ..., clear +// inside complete_datasignature, so all checks use the built-in +// _assert command rather than a helper program. +// ============================================================ + +clear all +adopath ++ "../src" +cap mkdir outputs + +// Guard: abort if accidentally run in batch mode. +if "`c(mode)'" == "batch" { + di as error "This script must be run interactively (c(mode) is 'batch')." + di as error "Open Stata and do this file -- do not run it with -e do." + exit 198 +} + +// ============================================================ +// Setup: build a target .dtas for the tests to sign. +// frames: default (census, 50 obs) + regions (2 vars) +// ============================================================ +clear all +sysuse census, clear +frame put state region, into(regions) +frames save "outputs/_rt_target.dtas", frames(default regions) replace +clear all + +// ============================================================ +// Test 1. Basic round-trip. +// - User frames before the call: default (auto data) + prices +// - Target to sign: _rt_target.dtas (census + regions frames) +// - After the call: user's own frames must be intact. +// ============================================================ +di as txt _newline "--- Test 1: basic round-trip ---" + +sysuse auto, clear +frame put make price mpg, into(prices) + +qui frames dir +local before_frames "`r(frames)'" +local before_N = _N +local before_price1 = price[1] + +complete_datasignature, frameset_file("outputs/_rt_target.dtas") +local sig_t1 "`r(signature)'" + +_assert "`sig_t1'" != "", msg("Test 1: signature is empty") +di as result "PASS: Test 1: signature non-empty" + +qui frames dir +local after_frames "`r(frames)'" +_assert "`after_frames'" == "`before_frames'", /// + msg("Test 1: frame list -- expected `before_frames', got `after_frames'") +di as result "PASS: Test 1: frame list restored (`after_frames')" + +_assert _N == `before_N', msg("Test 1: default frame N -- expected `before_N', got `=_N'") +di as result "PASS: Test 1: default frame N = `=_N'" + +_assert price[1] == `before_price1', /// + msg("Test 1: default frame price[1] -- expected `before_price1', got `=price[1]'") +di as result "PASS: Test 1: default frame price[1] intact" + +frame prices { + _assert _N == 74, msg("Test 1: prices frame N -- expected 74, got `=_N'") + di as result "PASS: Test 1: prices frame N = `=_N'" + _assert c(k) == 3, msg("Test 1: prices frame k -- expected 3, got `=c(k)'") + di as result "PASS: Test 1: prices frame k = `=c(k)'" +} + +di as result "Test 1 passed." + +// ============================================================ +// Test 2. Empty default frame. +// - User default frame is empty; one additional frame has data. +// - Verifies the emptyok save guard works and the empty default +// is restored (not replaced by the target's frames). +// ============================================================ +di as txt _newline "--- Test 2: empty default frame ---" + +clear all +frame create withdata +frame withdata: sysuse auto, clear + +qui frames dir +local before_frames "`r(frames)'" // "default withdata" + +complete_datasignature, frameset_file("outputs/_rt_target.dtas") +local sig_t2 "`r(signature)'" + +_assert "`sig_t2'" != "", msg("Test 2: signature is empty") +di as result "PASS: Test 2: signature non-empty" + +qui frames dir +local after_frames "`r(frames)'" +_assert "`after_frames'" == "`before_frames'", /// + msg("Test 2: frame list -- expected `before_frames', got `after_frames'") +di as result "PASS: Test 2: frame list restored (`after_frames')" + +_assert _N == 0, msg("Test 2: default frame should be empty, got `=_N' rows") +di as result "PASS: Test 2: default frame still empty" + +frame withdata { + _assert _N == 74, msg("Test 2: withdata N -- expected 74, got `=_N'") + di as result "PASS: Test 2: withdata frame N = `=_N'" +} + +di as result "Test 2 passed." + +// ============================================================ +// Test 3. Frame name collision. +// - One user frame has the same name as a frame inside the +// target .dtas ("regions"). The user's frame must be +// restored with the user's content, not the target's. +// ============================================================ +di as txt _newline "--- Test 3: frame name collision ---" + +clear all +sysuse auto, clear +frame create regions // same name as a frame in _rt_target +frame regions: sysuse auto, clear // different data (74 obs, 12 vars) + +qui frames dir +local before_frames "`r(frames)'" + +complete_datasignature, frameset_file("outputs/_rt_target.dtas") +local sig_t3 "`r(signature)'" + +_assert "`sig_t3'" != "", msg("Test 3: signature is empty") +di as result "PASS: Test 3: signature non-empty" + +qui frames dir +local after_frames "`r(frames)'" +_assert "`after_frames'" == "`before_frames'", /// + msg("Test 3: frame list -- expected `before_frames', got `after_frames'") +di as result "PASS: Test 3: frame list restored (`after_frames')" + +// User's "regions" must still hold auto (74 obs, 12 vars), +// not the 2-variable regions frame from the target. +frame regions { + _assert _N == 74, msg("Test 3: collision frame N -- expected 74, got `=_N'") + di as result "PASS: Test 3: collision frame N = `=_N'" + _assert c(k) == 12, msg("Test 3: collision frame k -- expected 12, got `=c(k)'") + di as result "PASS: Test 3: collision frame k = `=c(k)'" +} + +di as result "Test 3 passed." + +// ============================================================ +// Test 4. Frame count preserved (user has more frames than target). +// - User has 4 frames; target .dtas has 2. +// - After the call: still exactly 4 user frames. +// ============================================================ +di as txt _newline "--- Test 4: frame count preserved ---" + +clear all +sysuse auto, clear +frame create f2 +frame f2: sysuse census, clear +frame create f3 +frame f3: sysuse auto, clear +frame create f4 +frame f4: sysuse auto, clear + +qui frames dir +local before_frames "`r(frames)'" // "default f2 f3 f4" +local before_nframes = wordcount("`before_frames'") + +complete_datasignature, frameset_file("outputs/_rt_target.dtas") + +qui frames dir +local after_frames "`r(frames)'" +local after_nframes = wordcount("`after_frames'") + +_assert `after_nframes' == `before_nframes', /// + msg("Test 4: frame count -- expected `before_nframes', got `after_nframes'") +di as result "PASS: Test 4: frame count = `after_nframes'" + +_assert "`after_frames'" == "`before_frames'", /// + msg("Test 4: frame list -- expected `before_frames', got `after_frames'") +di as result "PASS: Test 4: frame list restored (`after_frames')" + +di as result "Test 4 passed." + +// ============================================================ +// Test 5. Signature stability across calls. +// - Calling twice from the same interactive session must return +// the same value (the round-trip must not mutate state that +// the signature depends on inside the target file). +// ============================================================ +di as txt _newline "--- Test 5: signature stability across calls ---" + +clear all +sysuse auto, clear +frame put make price, into(prices) + +complete_datasignature, frameset_file("outputs/_rt_target.dtas") +local sig_call1 "`r(signature)'" +complete_datasignature, frameset_file("outputs/_rt_target.dtas") +local sig_call2 "`r(signature)'" + +_assert "`sig_call1'" == "`sig_call2'", /// + msg("Test 5: sig changed between calls -- call1=`sig_call1' call2=`sig_call2'") +di as result "PASS: Test 5: sig stable across calls" + +di as result "Test 5 passed." + +// ============================================================ +// Cleanup +// ============================================================ +cap erase "outputs/_rt_target.dtas" + +di _newline as result "ALL INTERACTIVE ROUND-TRIP TESTS PASSED" From e736d8cfce454f03b4656f68ae435d1ba73d3209 Mon Sep 17 00:00:00 2001 From: rpguiteras Date: Thu, 21 May 2026 15:36:41 -0400 Subject: [PATCH 08/21] additional tests of frames Introduce a comprehensive test harness for .dtas support under frames/tests: SConstruct files for scons-driven builds, producer/consumer Stata do-files for dtas and linked workflows, interactive and smoke test scripts (including SCons rerun checks), and a Python helper (make_malformed_dtas.py) to generate malformed .dtas fixtures. Also add testlib helpers, run_all wrapper, and binary fixtures for expected outputs. These tests validate signature stability across compression/topology/alias/linked workflows, error handling for malformed archives, and SCons no-rebuild behavior on identical reruns. --- frames/tests/.gitignore | 10 + frames/tests/SConstruct | 27 +++ frames/tests/SConstruct-life | 15 ++ frames/tests/SConstruct-linked | 16 ++ frames/tests/code/dtas_blog_life_consumer.do | 5 + frames/tests/code/dtas_blog_life_producer.do | 9 + frames/tests/code/dtas_linked_consumer.do | 9 + frames/tests/code/dtas_linked_producer.do | 7 + frames/tests/interactive_roundtrip_test.do | 104 ++++++++++ frames/tests/logs/.gitkeepdir | 0 frames/tests/make_malformed_dtas.py | 63 ++++++ frames/tests/run_all.do | 9 + frames/tests/smoke_dtas_blog.do | 202 +++++++++++++++++++ frames/tests/smoke_dtas_errors.do | 50 +++++ frames/tests/smoke_scons_dtas_blog.do | 85 ++++++++ frames/tests/testlib.do | 48 +++++ 16 files changed, 659 insertions(+) create mode 100644 frames/tests/.gitignore create mode 100644 frames/tests/SConstruct create mode 100644 frames/tests/SConstruct-life create mode 100644 frames/tests/SConstruct-linked create mode 100644 frames/tests/code/dtas_blog_life_consumer.do create mode 100644 frames/tests/code/dtas_blog_life_producer.do create mode 100644 frames/tests/code/dtas_linked_consumer.do create mode 100644 frames/tests/code/dtas_linked_producer.do create mode 100644 frames/tests/interactive_roundtrip_test.do create mode 100644 frames/tests/logs/.gitkeepdir create mode 100644 frames/tests/make_malformed_dtas.py create mode 100644 frames/tests/run_all.do create mode 100644 frames/tests/smoke_dtas_blog.do create mode 100644 frames/tests/smoke_dtas_errors.do create mode 100644 frames/tests/smoke_scons_dtas_blog.do create mode 100644 frames/tests/testlib.do diff --git a/frames/tests/.gitignore b/frames/tests/.gitignore new file mode 100644 index 0000000..0b30327 --- /dev/null +++ b/frames/tests/.gitignore @@ -0,0 +1,10 @@ +logs/*.smcl +outputs/ +*.stswp +.sconsign.dblite +sig-*.txt +stata-*.do +stata-*.log +repro_datasig.do +repro_datasig.log + diff --git a/frames/tests/SConstruct b/frames/tests/SConstruct new file mode 100644 index 0000000..56b3ead --- /dev/null +++ b/frames/tests/SConstruct @@ -0,0 +1,27 @@ +import pystatacons + +env = pystatacons.init_env() + +env.StataBuild( + target=['outputs/life_blog.dtas'], + source='code/dtas_blog_life_producer.do' +) + +env.StataBuild( + target=['outputs/life_blog_subset.dta'], + source='code/dtas_blog_life_consumer.do', + depends=['outputs/life_blog.dtas'] +) + +env.StataBuild( + target=['outputs/linked_project.dtas'], + source='code/dtas_linked_producer.do', + depends=['../datasets/persons.dta', '../datasets/txcounty.dta'] +) + +env.StataBuild( + target=['outputs/person_ratio_from_dtas.dta'], + source='code/dtas_linked_consumer.do', + depends=['outputs/linked_project.dtas'] +) + diff --git a/frames/tests/SConstruct-life b/frames/tests/SConstruct-life new file mode 100644 index 0000000..f372e21 --- /dev/null +++ b/frames/tests/SConstruct-life @@ -0,0 +1,15 @@ +import pystatacons + +env = pystatacons.init_env() + +env.StataBuild( + target=['outputs/life_blog.dtas'], + source='code/dtas_blog_life_producer.do' +) + +env.StataBuild( + target=['outputs/life_blog_subset.dta'], + source='code/dtas_blog_life_consumer.do', + depends=['outputs/life_blog.dtas'] +) + diff --git a/frames/tests/SConstruct-linked b/frames/tests/SConstruct-linked new file mode 100644 index 0000000..3ac4925 --- /dev/null +++ b/frames/tests/SConstruct-linked @@ -0,0 +1,16 @@ +import pystatacons + +env = pystatacons.init_env() + +env.StataBuild( + target=['outputs/linked_project.dtas'], + source='code/dtas_linked_producer.do', + depends=['../datasets/persons.dta', '../datasets/txcounty.dta'] +) + +env.StataBuild( + target=['outputs/person_ratio_from_dtas.dta'], + source='code/dtas_linked_consumer.do', + depends=['outputs/linked_project.dtas'] +) + diff --git a/frames/tests/code/dtas_blog_life_consumer.do b/frames/tests/code/dtas_blog_life_consumer.do new file mode 100644 index 0000000..042b9db --- /dev/null +++ b/frames/tests/code/dtas_blog_life_consumer.do @@ -0,0 +1,5 @@ +frames use "outputs/life_blog.dtas", clear +frame change life1 +keep in 1/10 +save "outputs/life_blog_subset.dta", replace + diff --git a/frames/tests/code/dtas_blog_life_producer.do b/frames/tests/code/dtas_blog_life_producer.do new file mode 100644 index 0000000..e07f764 --- /dev/null +++ b/frames/tests/code/dtas_blog_life_producer.do @@ -0,0 +1,9 @@ +clear all +frame create life0 +frame create life1 +frame create life2 +frame life0: sysuse lifeexp, clear +frame life1: sysuse uslifeexp, clear +frame life2: sysuse uslifeexp2, clear +frames save "outputs/life_blog.dtas", frames(life0 life1 life2) replace + diff --git a/frames/tests/code/dtas_linked_consumer.do b/frames/tests/code/dtas_linked_consumer.do new file mode 100644 index 0000000..f045879 --- /dev/null +++ b/frames/tests/code/dtas_linked_consumer.do @@ -0,0 +1,9 @@ +frames use "outputs/linked_project.dtas", clear +frame change default +fralias add median = median_income, from(counties) +gen income_ratio = income / median +gen median_copy = median +drop median +rename median_copy median +keep personid countyid income median income_ratio +save "outputs/person_ratio_from_dtas.dta", replace diff --git a/frames/tests/code/dtas_linked_producer.do b/frames/tests/code/dtas_linked_producer.do new file mode 100644 index 0000000..e47a010 --- /dev/null +++ b/frames/tests/code/dtas_linked_producer.do @@ -0,0 +1,7 @@ +clear all +use "../datasets/persons.dta", clear +frame create counties +frame counties: use "../datasets/txcounty.dta", clear +frlink m:1 countyid, frame(counties) +frames save "outputs/linked_project.dtas", frames(default) linked replace + diff --git a/frames/tests/interactive_roundtrip_test.do b/frames/tests/interactive_roundtrip_test.do new file mode 100644 index 0000000..b3bfa22 --- /dev/null +++ b/frames/tests/interactive_roundtrip_test.do @@ -0,0 +1,104 @@ +// ============================================================ +// MANUAL INTERACTIVE TEST -- run by opening Stata interactively, +// then do this file from frames/tests. +// +// What passes: all _assert lines succeed and the final message is +// displayed. This targets c(mode)=="interactive" state restoration. +// ============================================================ + +clear all +do testlib.do +frames_tests_setup + +if "`c(mode)'" == "batch" { + di as error "This script must be run interactively." + exit 198 +} + +cap mkdir outputs + +// Build target framesets used by the interactive checks. +frame create life0 +frame create life1 +frame create life2 +frame life0: sysuse lifeexp, clear +frame life1: sysuse uslifeexp, clear +frame life2: sysuse uslifeexp2, clear +frames save "outputs/_rt_life.dtas", frames(life0 life1 life2) replace + +clear all +use "../datasets/persons.dta", clear +frame create counties +frame counties: use "../datasets/txcounty.dta", clear +frlink m:1 countyid, frame(counties) +frames save "outputs/_rt_linked.dtas", frames(default) linked replace + +// ============================================================ +// Test 1. Linked + alias user state is restored after signing. +// ============================================================ +clear all +use "../datasets/persons.dta", clear +frame create counties +frame counties: use "../datasets/txcounty.dta", clear +frlink m:1 countyid, frame(counties) +fralias add median = median_income, from(counties) +gen income_ratio = income / median + +qui frames dir +local before_frames "`r(frames)'" +local before_obs = _N +local before_ratio = income_ratio[1] + +complete_datasignature, frameset_file("outputs/_rt_life.dtas") +local sig_linked_alias "`r(signature)'" +_assert "`sig_linked_alias'" != "", msg("Test 1: signature is empty") + +qui frames dir +local after_frames "`r(frames)'" +_assert "`after_frames'" == "`before_frames'", /// + msg("Test 1: frame list was not restored") +_assert _N == `before_obs', msg("Test 1: default frame row count changed") +_assert income_ratio[1] == `before_ratio', msg("Test 1: alias-derived value changed") +confirm variable median +frame counties: _assert _N == 8, msg("Test 1: linked counties frame was not restored") + +di as result "PASS: interactive linked + alias state restores correctly" + +// ============================================================ +// Test 2. Name collisions with life* frames do not clobber the user. +// ============================================================ +clear all +frame create life1 +frame create life2 +sysuse auto, clear +frame life1: sysuse census, clear +frame life2: sysuse auto, clear + +qui frames dir +local before_collision_frames "`r(frames)'" + +complete_datasignature, frameset_file("outputs/_rt_life.dtas") +local sig_collision "`r(signature)'" +_assert "`sig_collision'" != "", msg("Test 2: signature is empty") + +qui frames dir +local after_collision_frames "`r(frames)'" +_assert "`after_collision_frames'" == "`before_collision_frames'", /// + msg("Test 2: colliding frame names were not restored") + +frame life1 { + _assert _N == 50, msg("Test 2: life1 frame row count changed") + _assert c(k) == 7, msg("Test 2: life1 frame column count changed") +} +frame life2 { + _assert _N == 74, msg("Test 2: life2 frame row count changed") + _assert c(k) == 12, msg("Test 2: life2 frame column count changed") +} + +di as result "PASS: interactive frame-name collisions restore correctly" + +cap erase "outputs/_rt_life.dtas" +cap erase "outputs/_rt_linked.dtas" + +di _newline as result "ALL frames/tests interactive checks passed" + diff --git a/frames/tests/logs/.gitkeepdir b/frames/tests/logs/.gitkeepdir new file mode 100644 index 0000000..e69de29 diff --git a/frames/tests/make_malformed_dtas.py b/frames/tests/make_malformed_dtas.py new file mode 100644 index 0000000..d92e80c --- /dev/null +++ b/frames/tests/make_malformed_dtas.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +import sys +import tempfile +import zipfile +from pathlib import Path + + +def write_zip_from_tree(src_dir: Path, dst_zip: Path, skip_name: str | None = None) -> None: + with zipfile.ZipFile(dst_zip, "w", compression=zipfile.ZIP_DEFLATED) as zf: + for path in sorted(src_dir.rglob("*")): + if not path.is_file(): + continue + rel = path.relative_to(src_dir).as_posix() + if rel == skip_name: + continue + zf.write(path, rel) + + +def main() -> int: + if len(sys.argv) != 3: + print("usage: make_malformed_dtas.py SOURCE_DTAS OUTPUT_DIR") + return 2 + + src = Path(sys.argv[1]).resolve() + out_dir = Path(sys.argv[2]).resolve() + out_dir.mkdir(parents=True, exist_ok=True) + + (out_dir / "not_a_zip.dtas").write_text("not a zip\n", encoding="ascii") + + with tempfile.TemporaryDirectory() as tmp_dir: + tmp_path = Path(tmp_dir) + with zipfile.ZipFile(src, "r") as zf: + zf.extractall(tmp_path) + + frameinfo = tmp_path / ".frameinfo" + dta_files = sorted(tmp_path.glob("*.dta")) + if not frameinfo.exists() or not dta_files: + raise RuntimeError("valid source .dtas did not contain .frameinfo and embedded .dta files") + + write_zip_from_tree(tmp_path, out_dir / "missing_frameinfo.dtas", skip_name=".frameinfo") + write_zip_from_tree(tmp_path, out_dir / "missing_member.dtas", skip_name=dta_files[0].name) + + broken_dir = tmp_path / "_broken_frameinfo" + broken_dir.mkdir() + for path in tmp_path.iterdir(): + if path.name == broken_dir.name: + continue + if path.is_file(): + (broken_dir / path.name).write_bytes(path.read_bytes()) + + lines = frameinfo.read_text(encoding="utf-8").splitlines() + lines.append("this_manifest_line_is_invalid") + (broken_dir / ".frameinfo").write_text("\n".join(lines) + "\n", encoding="utf-8") + write_zip_from_tree(broken_dir, out_dir / "bad_frameinfo.dtas") + + print("created malformed fixtures in", out_dir) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) + diff --git a/frames/tests/run_all.do b/frames/tests/run_all.do new file mode 100644 index 0000000..1d24408 --- /dev/null +++ b/frames/tests/run_all.do @@ -0,0 +1,9 @@ +clear all +do testlib.do + +do smoke_dtas_blog.do +do smoke_dtas_errors.do +do smoke_scons_dtas_blog.do + +di _newline as result "ALL frames/tests batch checks passed" + diff --git a/frames/tests/smoke_dtas_blog.do b/frames/tests/smoke_dtas_blog.do new file mode 100644 index 0000000..3e73c00 --- /dev/null +++ b/frames/tests/smoke_dtas_blog.do @@ -0,0 +1,202 @@ +// ============================================================ +// Focused regression tests for .dtas support using official +// Stata frames workflows and shipped or vendored datasets. +// Run from frames/tests with: StataMP-64.exe -e do smoke_dtas_blog.do +// ============================================================ + +clear all +do testlib.do +frames_tests_setup + +// ============================================================ +// 1. Compression invariance using the 2023 blog frameset flow. +// Same frame content, different zip compression -> same sig. +// ============================================================ +clear all +frame create life0 +frame create life1 +frame create life2 +frame life0: sysuse lifeexp, clear +frame life1: sysuse uslifeexp, clear +frame life2: sysuse uslifeexp2, clear + +frames save "outputs/_life_default.dtas", frames(life0 life1 life2) replace +frames save "outputs/_life_store.dtas", frames(life0 life1 life2) complevel(0) replace +frames save "outputs/_life_max.dtas", frames(life0 life1 life2) complevel(9) replace + +complete_datasignature, frameset_file("outputs/_life_default.dtas") +local sig_default "`r(signature)'" +complete_datasignature, frameset_file("outputs/_life_store.dtas") +local sig_store "`r(signature)'" +complete_datasignature, frameset_file("outputs/_life_max.dtas") +local sig_max "`r(signature)'" + +_assert "`sig_default'" == "`sig_store'", msg("compression level 0 changed .dtas signature") +_assert "`sig_default'" == "`sig_max'", msg("compression level 9 changed .dtas signature") +di as result "PASS: compression-only changes do not affect .dtas signatures" + +// ============================================================ +// 2. Frameset topology changes are visible. +// Adding or dropping a frame must change the signature. +// ============================================================ +clear all +frame create life0 +frame create life1 +frame life0: sysuse lifeexp, clear +frame life1: sysuse uslifeexp, clear + +frames save "outputs/_life_topology.dtas", frames(life0 life1) replace +complete_datasignature, frameset_file("outputs/_life_topology.dtas") +local sig_two "`r(signature)'" + +frame create life2 +frame life2: sysuse uslifeexp2, clear +frames modify using "outputs/_life_topology.dtas", add(life2) +complete_datasignature, frameset_file("outputs/_life_topology.dtas") +local sig_three "`r(signature)'" + +frames modify using "outputs/_life_topology.dtas", drop(life1) +complete_datasignature, frameset_file("outputs/_life_topology.dtas") +local sig_drop "`r(signature)'" + +_assert "`sig_two'" != "`sig_three'", msg("adding a frame did not change .dtas signature") +_assert "`sig_three'" != "`sig_drop'", msg("dropping a frame did not change .dtas signature") +_assert "`sig_two'" != "`sig_drop'", msg("different frame sets produced the same signature") +di as result "PASS: frame add/drop operations change the aggregate signature" + +// ============================================================ +// 3. Per-frame isolation with 3 frames. +// Mutating only life1 should change only the middle slot. +// ============================================================ +clear all +frame create life0 +frame create life1 +frame create life2 +frame life0: sysuse lifeexp, clear +frame life1: sysuse uslifeexp, clear +frame life2: sysuse uslifeexp2, clear + +frames save "outputs/_life_slots_a.dtas", frames(life0 life1 life2) replace + +frame life1: gen slot_probe = _n +frames save "outputs/_life_slots_b.dtas", frames(life0 life1 life2) replace + +complete_datasignature, frameset_file("outputs/_life_slots_a.dtas") +local sig_slots_a "`r(signature)'" +complete_datasignature, frameset_file("outputs/_life_slots_b.dtas") +local sig_slots_b "`r(signature)'" + +local sig_slots_a_tokens : subinstr local sig_slots_a "|" " ", all +local sig_slots_b_tokens : subinstr local sig_slots_b "|" " ", all +tokenize `"`sig_slots_a_tokens'"' +local a1 "`1'" +local a2 "`2'" +local a3 "`3'" +tokenize `"`sig_slots_b_tokens'"' +local b1 "`1'" +local b2 "`2'" +local b3 "`3'" + +_assert "`a1'" == "`b1'", msg("life0 slot changed despite no mutation") +_assert "`a2'" != "`b2'", msg("life1 slot did not change after mutation") +_assert "`a3'" == "`b3'", msg("life2 slot changed despite no mutation") +di as result "PASS: only the mutated frame slot changes" + +// ============================================================ +// 4. Linked-save stability using the 2023 linked frameset flow. +// ============================================================ +clear all +use "../datasets/persons.dta", clear +frame create counties +frame counties: use "../datasets/txcounty.dta", clear +frlink m:1 countyid, frame(counties) +frames save "outputs/_linked_a.dtas", frames(default) linked replace +complete_datasignature, frameset_file("outputs/_linked_a.dtas") +local linked_sig_a "`r(signature)'" + +clear all +sleep 1500 +use "../datasets/persons.dta", clear +frame create counties +frame counties: use "../datasets/txcounty.dta", clear +frlink m:1 countyid, frame(counties) +frames save "outputs/_linked_b.dtas", frames(default) linked replace +complete_datasignature, frameset_file("outputs/_linked_b.dtas") +local linked_sig_b "`r(signature)'" + +_assert "`linked_sig_a'" == "`linked_sig_b'", /// + msg("linked frameset signature changed across identical relink and re-save") +di as result "PASS: linked framesets stay stable across identical relink and re-save" + +// ============================================================ +// 5. Alias-variable workflow stability from the 2023 blog post. +// ============================================================ +clear all +use "../datasets/persons.dta", clear +frame create counties +frame counties: use "../datasets/txcounty.dta", clear +frlink m:1 countyid, frame(counties) +fralias add median = median_income, from(counties) +gen income_ratio = income / median +frames save "outputs/_alias_a.dtas", frames(default counties) replace +complete_datasignature, frameset_file("outputs/_alias_a.dtas") +local alias_sig_a "`r(signature)'" + +clear all +sleep 1500 +use "../datasets/persons.dta", clear +frame create counties +frame counties: use "../datasets/txcounty.dta", clear +frlink m:1 countyid, frame(counties) +fralias add median = median_income, from(counties) +gen income_ratio = income / median +frames save "outputs/_alias_b.dtas", frames(default counties) replace +complete_datasignature, frameset_file("outputs/_alias_b.dtas") +local alias_sig_b "`r(signature)'" + +_assert "`alias_sig_a'" == "`alias_sig_b'", /// + msg("alias-variable workflow changed .dtas signature across identical re-save") +di as result "PASS: alias-variable workflow is stable across identical re-save" + +// ============================================================ +// 6. Diagnostic: same frames, different save order. +// This does not fail the suite; it documents current behavior. +// ============================================================ +clear all +frame create life0 +frame create life1 +frame create life2 +frame life0: sysuse lifeexp, clear +frame life1: sysuse uslifeexp, clear +frame life2: sysuse uslifeexp2, clear + +frames save "outputs/_life_order_a.dtas", frames(life0 life1 life2) replace +frames save "outputs/_life_order_b.dtas", frames(life2 life1 life0) replace +complete_datasignature, frameset_file("outputs/_life_order_a.dtas") +local order_sig_a "`r(signature)'" +complete_datasignature, frameset_file("outputs/_life_order_b.dtas") +local order_sig_b "`r(signature)'" + +if "`order_sig_a'" == "`order_sig_b'" { + di as txt "NOTE: frame save order did not change the .dtas signature" +} +else { + di as txt "NOTE: .dtas signature depends on frame save order" +} + +// cleanup +cap erase "outputs/_life_default.dtas" +cap erase "outputs/_life_store.dtas" +cap erase "outputs/_life_max.dtas" +cap erase "outputs/_life_topology.dtas" +cap erase "outputs/_life_slots_a.dtas" +cap erase "outputs/_life_slots_b.dtas" +cap erase "outputs/_linked_a.dtas" +cap erase "outputs/_linked_b.dtas" +cap erase "outputs/_alias_a.dtas" +cap erase "outputs/_alias_b.dtas" +cap erase "outputs/_life_order_a.dtas" +cap erase "outputs/_life_order_b.dtas" + +di _newline as result "ALL .dtas blog-style smoke tests passed" + diff --git a/frames/tests/smoke_dtas_errors.do b/frames/tests/smoke_dtas_errors.do new file mode 100644 index 0000000..125250e --- /dev/null +++ b/frames/tests/smoke_dtas_errors.do @@ -0,0 +1,50 @@ +// ============================================================ +// Hard-error tests for malformed .dtas inputs. +// Run from frames/tests with: StataMP-64.exe -e do smoke_dtas_errors.do +// ============================================================ + +clear all +do testlib.do +frames_tests_setup +frames_tests_require_python + +// Build one valid source frameset from official Stata datasets. +frame create life0 +frame create life1 +frame life0: sysuse lifeexp, clear +frame life1: sysuse uslifeexp, clear +frames save "outputs/_valid_source.dtas", frames(life0 life1) replace + +local py_script "`c(pwd)'/make_malformed_dtas.py" +local valid_src "`c(pwd)'/outputs/_valid_source.dtas" +local out_dir "`c(pwd)'/outputs" + +! "`c(python_exec)'" "`py_script'" "`valid_src'" "`out_dir'" +_assert _rc == 0, msg("failed to create malformed .dtas fixtures") + +cap noi complete_datasignature, frameset_file("outputs/not_a_zip.dtas") +local rc_not_zip = _rc +_assert `rc_not_zip' != 0, msg("plain-text .dtas unexpectedly signed successfully") + +cap noi complete_datasignature, frameset_file("outputs/missing_frameinfo.dtas") +local rc_missing_frameinfo = _rc +_assert `rc_missing_frameinfo' != 0, msg("missing .frameinfo unexpectedly signed successfully") + +cap noi complete_datasignature, frameset_file("outputs/missing_member.dtas") +local rc_missing_member = _rc +_assert `rc_missing_member' != 0, msg("missing embedded .dta unexpectedly signed successfully") + +cap noi complete_datasignature, frameset_file("outputs/bad_frameinfo.dtas") +local rc_bad_frameinfo = _rc +_assert `rc_bad_frameinfo' != 0, msg("malformed .frameinfo unexpectedly signed successfully") + +di as result "PASS: malformed .dtas files fail loudly" + +cap erase "outputs/_valid_source.dtas" +cap erase "outputs/not_a_zip.dtas" +cap erase "outputs/missing_frameinfo.dtas" +cap erase "outputs/missing_member.dtas" +cap erase "outputs/bad_frameinfo.dtas" + +di _newline as result "ALL malformed .dtas smoke tests passed" + diff --git a/frames/tests/smoke_scons_dtas_blog.do b/frames/tests/smoke_scons_dtas_blog.do new file mode 100644 index 0000000..3385d0d --- /dev/null +++ b/frames/tests/smoke_scons_dtas_blog.do @@ -0,0 +1,85 @@ +// ============================================================ +// Focused SCons regression test for the frames/tests .dtas +// harness. Verifies no rebuild on a second identical run. +// Run from frames/tests with: StataMP-64.exe -e do smoke_scons_dtas_blog.do +// ============================================================ + +clear all +cap log close _all +do testlib.do +frames_tests_setup + +local smoke_log "logs/smoke-scons-dtas-blog.smcl" +cap log using "`smoke_log'", replace name(smoke_scons_dtas_blog) +if _rc != 0 { + di as error "Could not open `smoke_log'" + exit _rc +} + +local config_nohidden "../../tests/config_nohidden.ini" +local sconstruct_life "SConstruct-life" +local sconstruct_linked "SConstruct-linked" + +capture noisily { + which statacons + which complete_datasignature + + frames_tests_set_dev_src + + cap noi statacons -f "`sconstruct_life'" --config-files="`config_nohidden'" -c + _assert _rc == 0, msg("statacons -c failed") + + cap noi statacons -f "`sconstruct_life'" --config-files="`config_nohidden'" outputs/life_blog.dtas + _assert _rc == 0, msg("initial life_blog producer build failed") + sleep 3000 + + cap noi statacons -f "`sconstruct_life'" --config-files="`config_nohidden'" outputs/life_blog_subset.dta + _assert _rc == 0, msg("initial life_blog consumer build failed") + + cap noi statacons -f "`sconstruct_linked'" --config-files="`config_nohidden'" -c + _assert _rc == 0, msg("linked_project clean failed") + + cap noi statacons -f "`sconstruct_linked'" --config-files="`config_nohidden'" outputs/linked_project.dtas + _assert _rc == 0, msg("initial linked_project producer build failed") + sleep 3000 + + cap noi statacons -f "`sconstruct_linked'" --config-files="`config_nohidden'" outputs/person_ratio_from_dtas.dta + _assert _rc == 0, msg("initial linked_project consumer build failed") + + confirm file "outputs/life_blog.dtas" + confirm file "outputs/life_blog_subset.dta" + confirm file "outputs/linked_project.dtas" + confirm file "outputs/person_ratio_from_dtas.dta" + + store_modts outputs/life_blog.dtas, local(mod1_life_dtas) + store_modts outputs/life_blog_subset.dta, local(mod1_life_dta) + store_modts outputs/linked_project.dtas, local(mod1_linked_dtas) + store_modts outputs/person_ratio_from_dtas.dta, local(mod1_ratio_dta) + + cap noi statacons -f "`sconstruct_life'" --config-files="`config_nohidden'" outputs/life_blog.dtas --debug=explain + _assert _rc == 0, msg("life_blog producer rerun with --debug=explain failed") + + cap noi statacons -f "`sconstruct_life'" --config-files="`config_nohidden'" outputs/life_blog_subset.dta --debug=explain + _assert _rc == 0, msg("life_blog consumer rerun with --debug=explain failed") + + cap noi statacons -f "`sconstruct_linked'" --config-files="`config_nohidden'" outputs/linked_project.dtas --debug=explain + _assert _rc == 0, msg("linked_project producer rerun with --debug=explain failed") + + cap noi statacons -f "`sconstruct_linked'" --config-files="`config_nohidden'" outputs/person_ratio_from_dtas.dta --debug=explain + _assert _rc == 0, msg("linked_project consumer rerun with --debug=explain failed") + + store_modts outputs/life_blog.dtas, local(mod2_life_dtas) + store_modts outputs/life_blog_subset.dta, local(mod2_life_dta) + store_modts outputs/linked_project.dtas, local(mod2_linked_dtas) + store_modts outputs/person_ratio_from_dtas.dta, local(mod2_ratio_dta) + + _assert "`mod1_life_dtas'`mod1_life_dta'`mod1_linked_dtas'`mod1_ratio_dta'" == /// + "`mod2_life_dtas'`mod2_life_dta'`mod2_linked_dtas'`mod2_ratio_dta'", /// + msg("frames/tests SCons .dtas pipeline rebuilt on an identical rerun") + + di as result "PASS: frames/tests SCons .dtas pipeline does not rebuild on rerun" +} +local rc = _rc + +cap log close smoke_scons_dtas_blog +exit `rc' diff --git a/frames/tests/testlib.do b/frames/tests/testlib.do new file mode 100644 index 0000000..6b6f397 --- /dev/null +++ b/frames/tests/testlib.do @@ -0,0 +1,48 @@ +cap program drop frames_tests_setup +program frames_tests_setup + adopath ++ "../../src" + cap mkdir outputs + cap mkdir logs +end + +cap program drop frames_tests_require_python +program frames_tests_require_python + if "`c(python_exec)'" == "" { + if `"$STATACONS_TEST_PYTHON"' != "" { + set python_exec `"$STATACONS_TEST_PYTHON"' + } + else if `"$PYTHON_EXEC"' != "" { + set python_exec `"$PYTHON_EXEC"' + } + else { + cap which doenv + if _rc == 0 { + cap confirm file "../../.env" + if _rc == 0 { + qui doenv using "../../.env" + if "`r(python_env)'" != "" { + set python_exec "`r(python_env)'" + } + } + } + } + } + + _assert "`c(python_exec)'" != "", /// + msg("Need python_exec or STATACONS_TEST_PYTHON/PYTHON_EXEC or ../../.env") +end + +cap program drop frames_tests_set_dev_src +program frames_tests_set_dev_src + frames_tests_require_python + local dev_src "`c(pwd)'/../../src" + python: import os; os.environ["STATACONS_DEV_SRC"] = r"""`dev_src'"""; print("STATACONS_DEV_SRC =", os.environ["STATACONS_DEV_SRC"]) +end + +cap program drop store_modts +program store_modts + syntax anything, local(string) + + frames_tests_require_python + python: from pathlib import Path; from sfi import Macro; Macro.setLocal("`local'", str(Path(r"""`anything'""").stat().st_mtime_ns)) +end From a1da39bf7d42e7affd36d2ba8b0321a30cc5bb48 Mon Sep 17 00:00:00 2001 From: rpguiteras Date: Thu, 21 May 2026 15:49:22 -0400 Subject: [PATCH 09/21] improve explanatory comments in test do-files --- frames/tests/code/dtas_blog_life_consumer.do | 4 ++- frames/tests/code/dtas_blog_life_producer.do | 4 ++- frames/tests/code/dtas_linked_consumer.do | 3 ++ frames/tests/code/dtas_linked_producer.do | 3 +- frames/tests/interactive_roundtrip_test.do | 15 +++++++-- frames/tests/run_all.do | 4 ++- frames/tests/smoke_dtas_blog.do | 29 ++++++++++++++++- frames/tests/smoke_dtas_errors.do | 17 +++++++++- frames/tests/smoke_scons_dtas_blog.do | 34 +++++++++++++++++++- 9 files changed, 104 insertions(+), 9 deletions(-) diff --git a/frames/tests/code/dtas_blog_life_consumer.do b/frames/tests/code/dtas_blog_life_consumer.do index 042b9db..0e79e6c 100644 --- a/frames/tests/code/dtas_blog_life_consumer.do +++ b/frames/tests/code/dtas_blog_life_consumer.do @@ -1,5 +1,7 @@ frames use "outputs/life_blog.dtas", clear +// Consumer fixture for the life-blog pipeline. If this file fails in +// the SCons smoke test, statacons built the .dtas but downstream code +// could not reopen it and save a derived .dta. frame change life1 keep in 1/10 save "outputs/life_blog_subset.dta", replace - diff --git a/frames/tests/code/dtas_blog_life_producer.do b/frames/tests/code/dtas_blog_life_producer.do index e07f764..414f96e 100644 --- a/frames/tests/code/dtas_blog_life_producer.do +++ b/frames/tests/code/dtas_blog_life_producer.do @@ -1,4 +1,7 @@ clear all +// Producer fixture for the "Fun with frames"-style life expectancy +// example. SCons uses this to build a .dtas target from three official +// Stata example datasets. frame create life0 frame create life1 frame create life2 @@ -6,4 +9,3 @@ frame life0: sysuse lifeexp, clear frame life1: sysuse uslifeexp, clear frame life2: sysuse uslifeexp2, clear frames save "outputs/life_blog.dtas", frames(life0 life1 life2) replace - diff --git a/frames/tests/code/dtas_linked_consumer.do b/frames/tests/code/dtas_linked_consumer.do index f045879..98d1b5e 100644 --- a/frames/tests/code/dtas_linked_consumer.do +++ b/frames/tests/code/dtas_linked_consumer.do @@ -1,4 +1,7 @@ frames use "outputs/linked_project.dtas", clear +// Consumer fixture for the linked workflow. The median alias is turned +// into a regular variable before save so the resulting .dta does not +// depend on reopening the counties backing frame later. frame change default fralias add median = median_income, from(counties) gen income_ratio = income / median diff --git a/frames/tests/code/dtas_linked_producer.do b/frames/tests/code/dtas_linked_producer.do index e47a010..8b32609 100644 --- a/frames/tests/code/dtas_linked_producer.do +++ b/frames/tests/code/dtas_linked_producer.do @@ -1,7 +1,8 @@ clear all +// Producer fixture for the linked-frames blog workflow. This creates +// a linked frameset that exercises frlink-aware .dtas handling. use "../datasets/persons.dta", clear frame create counties frame counties: use "../datasets/txcounty.dta", clear frlink m:1 countyid, frame(counties) frames save "outputs/linked_project.dtas", frames(default) linked replace - diff --git a/frames/tests/interactive_roundtrip_test.do b/frames/tests/interactive_roundtrip_test.do index b3bfa22..9151d87 100644 --- a/frames/tests/interactive_roundtrip_test.do +++ b/frames/tests/interactive_roundtrip_test.do @@ -4,6 +4,12 @@ // // What passes: all _assert lines succeed and the final message is // displayed. This targets c(mode)=="interactive" state restoration. +// +// What failures mean: +// - Test 1 failures mean signature computation did not fully restore +// the user's linked/alias session state after loading the frameset. +// - Test 2 failures mean temporary frame-name collisions during +// signing clobbered pre-existing interactive frames. // ============================================================ clear all @@ -17,7 +23,9 @@ if "`c(mode)'" == "batch" { cap mkdir outputs -// Build target framesets used by the interactive checks. +// Build target framesets used by the interactive checks. These are +// just fixtures; the real tests are about what happens to the user's +// live interactive session after complete_datasignature runs. frame create life0 frame create life1 frame create life2 @@ -35,6 +43,8 @@ frames save "outputs/_rt_linked.dtas", frames(default) linked replace // ============================================================ // Test 1. Linked + alias user state is restored after signing. +// If this block fails, the interactive-mode preservation logic is not +// putting the user's original working state back correctly. // ============================================================ clear all use "../datasets/persons.dta", clear @@ -66,6 +76,8 @@ di as result "PASS: interactive linked + alias state restores correctly" // ============================================================ // Test 2. Name collisions with life* frames do not clobber the user. +// If this fails, temporary frames created while reading the .dtas file +// are overwriting or failing to restore user frames of the same names. // ============================================================ clear all frame create life1 @@ -101,4 +113,3 @@ cap erase "outputs/_rt_life.dtas" cap erase "outputs/_rt_linked.dtas" di _newline as result "ALL frames/tests interactive checks passed" - diff --git a/frames/tests/run_all.do b/frames/tests/run_all.do index 1d24408..0ea2b3d 100644 --- a/frames/tests/run_all.do +++ b/frames/tests/run_all.do @@ -1,9 +1,11 @@ clear all do testlib.do +// Batch entry point for the frames/tests smoke suite. +// If this script stops before the final PASS line, the first failing +// child do-file identifies which part of the .dtas test harness broke. do smoke_dtas_blog.do do smoke_dtas_errors.do do smoke_scons_dtas_blog.do di _newline as result "ALL frames/tests batch checks passed" - diff --git a/frames/tests/smoke_dtas_blog.do b/frames/tests/smoke_dtas_blog.do index 3e73c00..f5320f2 100644 --- a/frames/tests/smoke_dtas_blog.do +++ b/frames/tests/smoke_dtas_blog.do @@ -1,6 +1,20 @@ // ============================================================ // Focused regression tests for .dtas support using official // Stata frames workflows and shipped or vendored datasets. +// +// What this file tests: +// - compression-only changes should not affect a .dtas signature +// - adding or dropping frames should affect the aggregate signature +// - mutating one frame should only change that frame's signature slot +// - linked and alias-heavy workflows should be stable across +// identical rebuilds +// - frame save order is recorded as a diagnostic, because whether it +// should affect the signature is an implementation choice +// +// In general, a failure means either statacons is missing a real +// content change, or it is treating irrelevant container changes as +// meaningful data changes. +// // Run from frames/tests with: StataMP-64.exe -e do smoke_dtas_blog.do // ============================================================ @@ -11,6 +25,8 @@ frames_tests_setup // ============================================================ // 1. Compression invariance using the 2023 blog frameset flow. // Same frame content, different zip compression -> same sig. +// A failure here means we are hashing zip/container details rather +// than just the frame contents that statacons should track. // ============================================================ clear all frame create life0 @@ -38,6 +54,8 @@ di as result "PASS: compression-only changes do not affect .dtas signatures" // ============================================================ // 2. Frameset topology changes are visible. // Adding or dropping a frame must change the signature. +// A failure here means the aggregate signature is not sensitive to +// frame membership, so statacons could miss true dependency changes. // ============================================================ clear all frame create life0 @@ -67,6 +85,9 @@ di as result "PASS: frame add/drop operations change the aggregate signature" // ============================================================ // 3. Per-frame isolation with 3 frames. // Mutating only life1 should change only the middle slot. +// A failure in slot 1 or slot 3 means unrelated frames are being +// dirtied. A failure in slot 2 means the changed frame was not +// reflected in the signature at all. // ============================================================ clear all frame create life0 @@ -104,6 +125,9 @@ di as result "PASS: only the mutated frame slot changes" // ============================================================ // 4. Linked-save stability using the 2023 linked frameset flow. +// This checks that reconstructing the same linked workflow twice +// yields the same .dtas signature. A failure means volatile linked +// metadata is leaking into the signature. // ============================================================ clear all use "../datasets/persons.dta", clear @@ -130,6 +154,8 @@ di as result "PASS: linked framesets stay stable across identical relink and re- // ============================================================ // 5. Alias-variable workflow stability from the 2023 blog post. +// This is the alias-heavy analogue of test 4. A failure means +// alias mechanics are introducing instability into the signature. // ============================================================ clear all use "../datasets/persons.dta", clear @@ -161,6 +187,8 @@ di as result "PASS: alias-variable workflow is stable across identical re-save" // ============================================================ // 6. Diagnostic: same frames, different save order. // This does not fail the suite; it documents current behavior. +// Unlike the SCons smoke test, this is the place where we ask +// whether frame order changes the signature. // ============================================================ clear all frame create life0 @@ -199,4 +227,3 @@ cap erase "outputs/_life_order_a.dtas" cap erase "outputs/_life_order_b.dtas" di _newline as result "ALL .dtas blog-style smoke tests passed" - diff --git a/frames/tests/smoke_dtas_errors.do b/frames/tests/smoke_dtas_errors.do index 125250e..85ff060 100644 --- a/frames/tests/smoke_dtas_errors.do +++ b/frames/tests/smoke_dtas_errors.do @@ -1,5 +1,16 @@ // ============================================================ // Hard-error tests for malformed .dtas inputs. +// +// What this file tests: +// - non-zip junk with a .dtas suffix +// - missing .frameinfo manifest +// - missing embedded .dta members +// - malformed .frameinfo contents +// +// Every case below should fail loudly. A failure of this test file +// means complete_datasignature accepted a broken frameset that should +// have been rejected instead of silently hashed. +// // Run from frames/tests with: StataMP-64.exe -e do smoke_dtas_errors.do // ============================================================ @@ -9,6 +20,8 @@ frames_tests_setup frames_tests_require_python // Build one valid source frameset from official Stata datasets. +// The Python helper mutates this known-good file into several broken +// variants, so if the helper step fails we never created the fixtures. frame create life0 frame create life1 frame life0: sysuse lifeexp, clear @@ -22,6 +35,9 @@ local out_dir "`c(pwd)'/outputs" ! "`c(python_exec)'" "`py_script'" "`valid_src'" "`out_dir'" _assert _rc == 0, msg("failed to create malformed .dtas fixtures") +// Each assertion below checks "must error", not "must return a +// particular rc". The important contract is hard failure, not the +// exact code path used to reject the bad archive. cap noi complete_datasignature, frameset_file("outputs/not_a_zip.dtas") local rc_not_zip = _rc _assert `rc_not_zip' != 0, msg("plain-text .dtas unexpectedly signed successfully") @@ -47,4 +63,3 @@ cap erase "outputs/missing_member.dtas" cap erase "outputs/bad_frameinfo.dtas" di _newline as result "ALL malformed .dtas smoke tests passed" - diff --git a/frames/tests/smoke_scons_dtas_blog.do b/frames/tests/smoke_scons_dtas_blog.do index 3385d0d..fc491ec 100644 --- a/frames/tests/smoke_scons_dtas_blog.do +++ b/frames/tests/smoke_scons_dtas_blog.do @@ -1,6 +1,21 @@ // ============================================================ // Focused SCons regression test for the frames/tests .dtas -// harness. Verifies no rebuild on a second identical run. +// harness. +// +// What this file tests: +// 1. statacons can build the blog-style .dtas producer and +// consumer pipelines end to end. +// 2. A second identical statacons invocation is a true no-op: +// the output files are left untouched rather than rebuilt. +// +// What failures mean: +// - a producer-build failure means statacons could not sign or +// build the upstream .dtas target. +// - a consumer-build failure means statacons could build the +// .dtas but could not consume it correctly downstream. +// - the final timestamp-comparison failure means an identical +// rerun rebuilt at least one output when it should not have. +// // Run from frames/tests with: StataMP-64.exe -e do smoke_scons_dtas_blog.do // ============================================================ @@ -26,16 +41,23 @@ capture noisily { frames_tests_set_dev_src + // Start from a clean tree so later no-rebuild checks are about + // the second run only, not leftover outputs from earlier runs. cap noi statacons -f "`sconstruct_life'" --config-files="`config_nohidden'" -c _assert _rc == 0, msg("statacons -c failed") + // Producer target: build the frameset itself. cap noi statacons -f "`sconstruct_life'" --config-files="`config_nohidden'" outputs/life_blog.dtas _assert _rc == 0, msg("initial life_blog producer build failed") sleep 3000 + // Consumer target: reopen the built frameset and save a derived .dta. + // If this fails, .dtas consumption inside the pipeline is broken. cap noi statacons -f "`sconstruct_life'" --config-files="`config_nohidden'" outputs/life_blog_subset.dta _assert _rc == 0, msg("initial life_blog consumer build failed") + // Repeat the same clean -> build -> consume pattern for the linked + // frames workflow that uses frlink/fralias-style state. cap noi statacons -f "`sconstruct_linked'" --config-files="`config_nohidden'" -c _assert _rc == 0, msg("linked_project clean failed") @@ -46,16 +68,22 @@ capture noisily { cap noi statacons -f "`sconstruct_linked'" --config-files="`config_nohidden'" outputs/person_ratio_from_dtas.dta _assert _rc == 0, msg("initial linked_project consumer build failed") + // These confirms distinguish "statacons exited 0" from "the target + // file we expected was actually produced on disk". confirm file "outputs/life_blog.dtas" confirm file "outputs/life_blog_subset.dta" confirm file "outputs/linked_project.dtas" confirm file "outputs/person_ratio_from_dtas.dta" + // Capture output mtimes after the first successful build. + // The rerun below should leave all four mtimes unchanged. store_modts outputs/life_blog.dtas, local(mod1_life_dtas) store_modts outputs/life_blog_subset.dta, local(mod1_life_dta) store_modts outputs/linked_project.dtas, local(mod1_linked_dtas) store_modts outputs/person_ratio_from_dtas.dta, local(mod1_ratio_dta) + // Rerun the exact same targets with no input changes. --debug=explain + // helps show why statacons thinks a target is or is not dirty. cap noi statacons -f "`sconstruct_life'" --config-files="`config_nohidden'" outputs/life_blog.dtas --debug=explain _assert _rc == 0, msg("life_blog producer rerun with --debug=explain failed") @@ -68,11 +96,15 @@ capture noisily { cap noi statacons -f "`sconstruct_linked'" --config-files="`config_nohidden'" outputs/person_ratio_from_dtas.dta --debug=explain _assert _rc == 0, msg("linked_project consumer rerun with --debug=explain failed") + // Capture mtimes again after the identical rerun. store_modts outputs/life_blog.dtas, local(mod2_life_dtas) store_modts outputs/life_blog_subset.dta, local(mod2_life_dta) store_modts outputs/linked_project.dtas, local(mod2_linked_dtas) store_modts outputs/person_ratio_from_dtas.dta, local(mod2_ratio_dta) + // This is a no-rebuild check, not a signature-order check. + // If it fails, at least one target was rewritten on rerun, meaning + // statacons treated an unchanged pipeline as dirty. _assert "`mod1_life_dtas'`mod1_life_dta'`mod1_linked_dtas'`mod1_ratio_dta'" == /// "`mod2_life_dtas'`mod2_life_dta'`mod2_linked_dtas'`mod2_ratio_dta'", /// msg("frames/tests SCons .dtas pipeline rebuilt on an identical rerun") From c7f1def25a2f46f50216de3724cec72f086541c8 Mon Sep 17 00:00:00 2001 From: rpguiteras Date: Thu, 21 May 2026 15:56:57 -0400 Subject: [PATCH 10/21] Create README-tests.md --- frames/tests/README-tests.md | 129 +++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 frames/tests/README-tests.md diff --git a/frames/tests/README-tests.md b/frames/tests/README-tests.md new file mode 100644 index 0000000..d232b2c --- /dev/null +++ b/frames/tests/README-tests.md @@ -0,0 +1,129 @@ +# frames/tests test guide + +This folder contains a small test harness for the new `.dtas` / frameset support in `statacons`. + +The goal is simple: make sure `statacons` notices the changes that matter in a `.dtas` file, ignores the changes that should not matter, fails clearly on broken `.dtas` files, and works both in batch mode and in interactive Stata sessions. + +## In plain terms, what are these tests checking? + +There are four main ideas: + +1. **Stable signatures for unchanged data.** If the actual frame contents are the same, `statacons` should treat the `.dtas` file as unchanged. +2. **Changed signatures for real data changes.** If a frame is added, dropped, or edited, `statacons` should notice. +3. **Clear failures for broken `.dtas` files.** A malformed frameset should fail loudly, not be accepted quietly. +4. **Correct behavior inside a build pipeline.** When `.dtas` files are used in an SCons build, they should build correctly the first time and not rebuild on an identical second run. + +## The main test files + +| File | What it does | How it fits | +|---|---|---| +| `run_all.do` | Runs the batch smoke tests in sequence. | This is the simplest entry point for the batch test suite. | +| `smoke_dtas_blog.do` | Checks core `.dtas` signature behavior using blog-style frames workflows and shipped/vendored datasets. | This is the main "does signing behave sensibly?" test. | +| `smoke_dtas_errors.do` | Checks that malformed `.dtas` files are rejected with errors. | This is the main "fail loudly on bad input" test. | +| `smoke_scons_dtas_blog.do` | Checks that `statacons` can build `.dtas` targets in SCons and does not rebuild them unnecessarily on an identical rerun. | This is the main end-to-end pipeline test. | +| `interactive_roundtrip_test.do` | Manually checks that interactive Stata state is restored after signing a `.dtas` file. | This covers the special interactive-mode behavior that batch tests cannot fully check. | +| `testlib.do` | Defines small helper programs used by the other test files. | This is shared setup code, not a test by itself. | + +## What each main test is looking for + +### 1. `smoke_dtas_blog.do` + +This file checks the basic logic of `.dtas` signatures. + +It asks questions like: + +- If I save the same frameset with different zip compression, do I get the same signature? +- If I add or remove a frame, does the signature change? +- If I change only one frame, does only that frame's part of the signature change? +- If I recreate the same linked or alias-based workflow twice, do I get the same result both times? +- If I save the same frames in a different order, does the signature change or not? + +Why this matters: a good signature should react to **real data changes**, not to unimportant packaging differences. + +### 2. `smoke_dtas_errors.do` + +This file creates intentionally broken `.dtas` files and checks that `complete_datasignature` refuses to sign them. + +It covers cases like: + +- a file that is not really a zip archive +- a frameset missing its `.frameinfo` manifest +- a frameset missing one of the embedded `.dta` members +- a frameset with bad manifest contents + +Why this matters: if broken inputs are accepted quietly, build results become hard to trust. + +### 3. `smoke_scons_dtas_blog.do` + +This file checks the build-system side. + +It runs two small SCons pipelines: + +- a "life expectancy" pipeline based on the frames blog examples +- a linked-frames pipeline based on the framesets/alias-variable blog post + +For each pipeline, it checks that: + +1. the upstream `.dtas` file can be built +2. a downstream `.dta` file can be built from that `.dtas` +3. running the exact same build again does **not** rewrite the outputs + +That last check is important. It compares file modification times before and after the rerun. If those times change, the test concludes that `statacons` rebuilt something it should have left alone. + +### 4. `interactive_roundtrip_test.do` + +This is a **manual** test for interactive Stata. + +It checks two tricky cases: + +- a user already has linked frames and alias variables in memory, and signing a `.dtas` file should not disturb that setup +- a user already has frames with names like `life1` and `life2`, and signing a `.dtas` file should not clobber them + +Why this matters: interactive mode is harder than batch mode because the user's current session state has to be preserved and restored correctly. + +## The small do-files in `code/` + +These are tiny fixture scripts used by the SCons smoke test. + +| File | What it does | How it fits | +|---|---|---| +| `code/dtas_blog_life_producer.do` | Builds `outputs/life_blog.dtas` from three official Stata example datasets. | Upstream producer for the life-expectancy SCons pipeline. | +| `code/dtas_blog_life_consumer.do` | Opens `outputs/life_blog.dtas` and saves a smaller `.dta` derived from one frame. | Downstream consumer for the life-expectancy SCons pipeline. | +| `code/dtas_linked_producer.do` | Builds `outputs/linked_project.dtas` from linked `persons` and `txcounty` data. | Upstream producer for the linked-frames SCons pipeline. | +| `code/dtas_linked_consumer.do` | Opens `outputs/linked_project.dtas`, computes an income ratio, and saves a regular `.dta`. | Downstream consumer for the linked-frames SCons pipeline. | + +## Supporting files + +These are not do-files, but they help the tests run: + +| File | What it does | +|---|---| +| `SConstruct-life` | SCons recipe for the life-expectancy pipeline. | +| `SConstruct-linked` | SCons recipe for the linked-frames pipeline. | +| `SConstruct` | Older combined SCons fixture kept in the folder. | +| `make_malformed_dtas.py` | Creates broken `.dtas` files for the error tests. | +| `logs/` | Stores SMCL logs written by the SCons smoke test. | +| `outputs/` | Stores temporary test outputs created during runs. | + +## How the pieces fit together + +If you want the shortest mental model, think of the test harness like this: + +- `smoke_dtas_blog.do` asks: **Are the signatures themselves sensible?** +- `smoke_dtas_errors.do` asks: **Do broken framesets fail clearly?** +- `smoke_scons_dtas_blog.do` asks: **Does the build pipeline behave correctly?** +- `interactive_roundtrip_test.do` asks: **Does this still behave correctly in a live interactive session?** + +And `run_all.do` is simply the batch runner that ties the first three together. + +## Practical reading order + +If you are new to this folder, a good order is: + +1. `README-tests.md` -- this overview +2. `smoke_dtas_blog.do` -- the core signature ideas +3. `smoke_dtas_errors.do` -- the bad-input checks +4. `smoke_scons_dtas_blog.do` -- the end-to-end build check +5. `interactive_roundtrip_test.do` -- the interactive-only edge cases + +That order goes from the simplest ideas to the trickiest ones. From fe89471596c081804993de261b934944a7282a81 Mon Sep 17 00:00:00 2001 From: rpguiteras Date: Thu, 21 May 2026 16:28:14 -0400 Subject: [PATCH 11/21] Migrate legacy .dtas tests into frames/tests Move legacy small-data .dtas test scaffolding from the top-level tests/ folder into frames/tests. Added SConstruct-legacy, legacy producer/consumer do-files, legacy interactive round-trip and legacy smoke scripts, and integrated them into run_all.do and README-tests.md. Removed the old duplicates from tests/ to centralize the harness and keep branch-only legacy scripts out of the top-level tests tree. --- frames/tests/README-tests.md | 59 ++++- frames/tests/SConstruct-legacy | 13 ++ frames/tests/code/dtas_legacy_consumer.do | 7 + frames/tests/code/dtas_legacy_producer.do | 8 + frames/tests/interactive_roundtrip_legacy.do | 129 +++++++++++ frames/tests/run_all.do | 2 + frames/tests/smoke_dtas_legacy.do | 96 ++++++++ frames/tests/smoke_scons_dtas_legacy.do | 55 +++++ tests/code/dtas_consumer.do | 4 - tests/code/dtas_producer.do | 4 - tests/interactive_roundtrip_test.do | 224 ------------------- tests/smoke_dtas.do | 104 --------- tests/smoke_scons_dtas.do | 41 ---- 13 files changed, 364 insertions(+), 382 deletions(-) create mode 100644 frames/tests/SConstruct-legacy create mode 100644 frames/tests/code/dtas_legacy_consumer.do create mode 100644 frames/tests/code/dtas_legacy_producer.do create mode 100644 frames/tests/interactive_roundtrip_legacy.do create mode 100644 frames/tests/smoke_dtas_legacy.do create mode 100644 frames/tests/smoke_scons_dtas_legacy.do delete mode 100644 tests/code/dtas_consumer.do delete mode 100644 tests/code/dtas_producer.do delete mode 100644 tests/interactive_roundtrip_test.do delete mode 100644 tests/smoke_dtas.do delete mode 100644 tests/smoke_scons_dtas.do diff --git a/frames/tests/README-tests.md b/frames/tests/README-tests.md index d232b2c..94a3483 100644 --- a/frames/tests/README-tests.md +++ b/frames/tests/README-tests.md @@ -18,10 +18,13 @@ There are four main ideas: | File | What it does | How it fits | |---|---|---| | `run_all.do` | Runs the batch smoke tests in sequence. | This is the simplest entry point for the batch test suite. | +| `smoke_dtas_legacy.do` | Keeps the older small-data `.dtas` signature checks that originally lived under `tests\`. | This is the migrated legacy/simple signature smoke test. | | `smoke_dtas_blog.do` | Checks core `.dtas` signature behavior using blog-style frames workflows and shipped/vendored datasets. | This is the main "does signing behave sensibly?" test. | | `smoke_dtas_errors.do` | Checks that malformed `.dtas` files are rejected with errors. | This is the main "fail loudly on bad input" test. | +| `smoke_scons_dtas_legacy.do` | Keeps the older small producer -> `.dtas` -> consumer SCons pipeline that originally lived under `tests\`. | This is the migrated legacy/simple end-to-end pipeline test. | | `smoke_scons_dtas_blog.do` | Checks that `statacons` can build `.dtas` targets in SCons and does not rebuild them unnecessarily on an identical rerun. | This is the main end-to-end pipeline test. | | `interactive_roundtrip_test.do` | Manually checks that interactive Stata state is restored after signing a `.dtas` file. | This covers the special interactive-mode behavior that batch tests cannot fully check. | +| `interactive_roundtrip_legacy.do` | Keeps the older interactive round-trip checks that originally lived under `tests\`. | This is the migrated legacy/simple interactive test file. | | `testlib.do` | Defines small helper programs used by the other test files. | This is shared setup code, not a test by itself. | ## What each main test is looking for @@ -40,6 +43,18 @@ It asks questions like: Why this matters: a good signature should react to **real data changes**, not to unimportant packaging differences. +### 1a. `smoke_dtas_legacy.do` + +This is the older, smaller version of the signature smoke test. + +It keeps three simple checks together: + +- repeated save with the same content gives the same signature +- changing one frame changes the aggregate signature +- `frlink_*` metadata does not create false changes + +Why keep it: it is a compact legacy regression file, and moving it here keeps the top-level `tests\` tree from carrying a separate branch-only `.dtas` smoke script. + ### 2. `smoke_dtas_errors.do` This file creates intentionally broken `.dtas` files and checks that `complete_datasignature` refuses to sign them. @@ -70,6 +85,19 @@ For each pipeline, it checks that: That last check is important. It compares file modification times before and after the rerun. If those times change, the test concludes that `statacons` rebuilt something it should have left alone. +### 3a. `smoke_scons_dtas_legacy.do` + +This is the older, simpler SCons pipeline check. + +It uses a very small frameset: + +- producer builds `legacy_myset.dtas` +- consumer reopens that frameset and writes `legacy_foreign_from_dtas.dta` + +It then reruns the same build and checks that neither output was rewritten. + +Why keep it: this is the smallest end-to-end `.dtas` pipeline in the harness, so it is still useful as a simple regression test. + ### 4. `interactive_roundtrip_test.do` This is a **manual** test for interactive Stata. @@ -81,6 +109,19 @@ It checks two tricky cases: Why this matters: interactive mode is harder than batch mode because the user's current session state has to be preserved and restored correctly. +### 4a. `interactive_roundtrip_legacy.do` + +This is the older interactive round-trip file that was originally created under `tests\`. + +It keeps four smaller checks: + +- basic round-trip +- empty default frame restoration +- frame-count preservation +- repeated-call signature stability + +Why keep it: the newer interactive file is more blog-oriented, while this one is still a useful compact set of session-restoration regressions. + ## The small do-files in `code/` These are tiny fixture scripts used by the SCons smoke test. @@ -89,6 +130,8 @@ These are tiny fixture scripts used by the SCons smoke test. |---|---|---| | `code/dtas_blog_life_producer.do` | Builds `outputs/life_blog.dtas` from three official Stata example datasets. | Upstream producer for the life-expectancy SCons pipeline. | | `code/dtas_blog_life_consumer.do` | Opens `outputs/life_blog.dtas` and saves a smaller `.dta` derived from one frame. | Downstream consumer for the life-expectancy SCons pipeline. | +| `code/dtas_legacy_producer.do` | Builds `outputs/legacy_myset.dtas` from a small split of `sysuse auto`. | Upstream producer for the migrated legacy/simple SCons pipeline. | +| `code/dtas_legacy_consumer.do` | Opens `outputs/legacy_myset.dtas` and saves a derived `.dta` from the foreign-cars frame. | Downstream consumer for the migrated legacy/simple SCons pipeline. | | `code/dtas_linked_producer.do` | Builds `outputs/linked_project.dtas` from linked `persons` and `txcounty` data. | Upstream producer for the linked-frames SCons pipeline. | | `code/dtas_linked_consumer.do` | Opens `outputs/linked_project.dtas`, computes an income ratio, and saves a regular `.dta`. | Downstream consumer for the linked-frames SCons pipeline. | @@ -100,6 +143,7 @@ These are not do-files, but they help the tests run: |---|---| | `SConstruct-life` | SCons recipe for the life-expectancy pipeline. | | `SConstruct-linked` | SCons recipe for the linked-frames pipeline. | +| `SConstruct-legacy` | SCons recipe for the migrated legacy/simple `.dtas` pipeline. | | `SConstruct` | Older combined SCons fixture kept in the folder. | | `make_malformed_dtas.py` | Creates broken `.dtas` files for the error tests. | | `logs/` | Stores SMCL logs written by the SCons smoke test. | @@ -110,20 +154,25 @@ These are not do-files, but they help the tests run: If you want the shortest mental model, think of the test harness like this: - `smoke_dtas_blog.do` asks: **Are the signatures themselves sensible?** +- `smoke_dtas_legacy.do` asks: **Do the old small-data signature regressions still pass?** - `smoke_dtas_errors.do` asks: **Do broken framesets fail clearly?** +- `smoke_scons_dtas_legacy.do` asks: **Does the old simple `.dtas` pipeline still work and stay a no-op on rerun?** - `smoke_scons_dtas_blog.do` asks: **Does the build pipeline behave correctly?** - `interactive_roundtrip_test.do` asks: **Does this still behave correctly in a live interactive session?** +- `interactive_roundtrip_legacy.do` asks: **Do the older interactive restoration checks still pass?** -And `run_all.do` is simply the batch runner that ties the first three together. +And `run_all.do` is simply the batch runner that ties the non-interactive smoke tests together. ## Practical reading order If you are new to this folder, a good order is: 1. `README-tests.md` -- this overview -2. `smoke_dtas_blog.do` -- the core signature ideas -3. `smoke_dtas_errors.do` -- the bad-input checks -4. `smoke_scons_dtas_blog.do` -- the end-to-end build check -5. `interactive_roundtrip_test.do` -- the interactive-only edge cases +2. `smoke_dtas_legacy.do` -- the smallest legacy signature checks +3. `smoke_dtas_blog.do` -- the richer signature ideas +4. `smoke_dtas_errors.do` -- the bad-input checks +5. `smoke_scons_dtas_legacy.do` -- the smallest SCons pipeline +6. `smoke_scons_dtas_blog.do` -- the richer end-to-end build check +7. `interactive_roundtrip_legacy.do` and `interactive_roundtrip_test.do` -- the interactive-only edge cases That order goes from the simplest ideas to the trickiest ones. diff --git a/frames/tests/SConstruct-legacy b/frames/tests/SConstruct-legacy new file mode 100644 index 0000000..8c33188 --- /dev/null +++ b/frames/tests/SConstruct-legacy @@ -0,0 +1,13 @@ +import pystatacons +env = pystatacons.init_env() + +# Legacy/simple .dtas frameset pipeline migrated from tests\SConstruct. +env.StataBuild( + target=['outputs/legacy_myset.dtas'], + source='code/dtas_legacy_producer.do' +) +env.StataBuild( + target=['outputs/legacy_foreign_from_dtas.dta'], + source='code/dtas_legacy_consumer.do', + depends=['outputs/legacy_myset.dtas'] +) diff --git a/frames/tests/code/dtas_legacy_consumer.do b/frames/tests/code/dtas_legacy_consumer.do new file mode 100644 index 0000000..d896704 --- /dev/null +++ b/frames/tests/code/dtas_legacy_consumer.do @@ -0,0 +1,7 @@ +frames use "outputs/legacy_myset.dtas", clear +// Legacy/simple consumer fixture migrated from tests\code\dtas_consumer.do. +// It reopens the legacy frameset and saves a derived .dta from the +// foreign_cars frame. +frame change foreign_cars +keep make price +save "outputs/legacy_foreign_from_dtas.dta", replace diff --git a/frames/tests/code/dtas_legacy_producer.do b/frames/tests/code/dtas_legacy_producer.do new file mode 100644 index 0000000..5af5f24 --- /dev/null +++ b/frames/tests/code/dtas_legacy_producer.do @@ -0,0 +1,8 @@ +clear all +// Legacy/simple producer fixture migrated from tests\code\dtas_producer.do. +// It splits sysuse auto into foreign and domestic frames, then saves a +// small frameset for the legacy SCons smoke pipeline. +sysuse auto, clear +frame put * if foreign == 1, into(foreign_cars) +frame put * if foreign == 0, into(domestic_cars) +frames save "outputs/legacy_myset.dtas", frames(default foreign_cars domestic_cars) replace diff --git a/frames/tests/interactive_roundtrip_legacy.do b/frames/tests/interactive_roundtrip_legacy.do new file mode 100644 index 0000000..ec22621 --- /dev/null +++ b/frames/tests/interactive_roundtrip_legacy.do @@ -0,0 +1,129 @@ +// ============================================================ +// Legacy interactive round-trip checks migrated from +// tests\interactive_roundtrip_test.do. +// +// Run by opening Stata interactively, then doing this file from +// frames/tests. These checks cover small but still useful interactive +// restoration cases that are separate from the newer blog-style script. +// ============================================================ + +clear all +do testlib.do +frames_tests_setup + +if "`c(mode)'" == "batch" { + di as error "This script must be run interactively." + exit 198 +} + +cap mkdir outputs + +// ============================================================ +// Setup: build a target .dtas file for the legacy round-trip checks. +// ============================================================ +clear all +sysuse census, clear +frame put state region, into(regions) +frames save "outputs/_rt_target_legacy.dtas", frames(default regions) replace +clear all + +// ============================================================ +// Test 1. Basic round-trip. +// ============================================================ +sysuse auto, clear +frame put make price mpg, into(prices) + +qui frames dir +local before_frames "`r(frames)'" +local before_N = _N +local before_price1 = price[1] + +complete_datasignature, frameset_file("outputs/_rt_target_legacy.dtas") +local sig_t1 "`r(signature)'" + +_assert "`sig_t1'" != "", msg("Legacy test 1: signature is empty") +qui frames dir +local after_frames "`r(frames)'" +_assert "`after_frames'" == "`before_frames'", /// + msg("Legacy test 1: frame list was not restored") +_assert _N == `before_N', /// + msg("Legacy test 1: default frame row count changed") +_assert price[1] == `before_price1', /// + msg("Legacy test 1: default frame contents changed") +frame prices { + _assert _N == 74, msg("Legacy test 1: prices frame row count changed") + _assert c(k) == 3, msg("Legacy test 1: prices frame column count changed") +} +di as result "PASS: legacy interactive basic round-trip" + +// ============================================================ +// Test 2. Empty default frame stays empty. +// ============================================================ +clear all +frame create withdata +frame withdata: sysuse auto, clear + +qui frames dir +local before_frames "`r(frames)'" + +complete_datasignature, frameset_file("outputs/_rt_target_legacy.dtas") +local sig_t2 "`r(signature)'" + +_assert "`sig_t2'" != "", msg("Legacy test 2: signature is empty") +qui frames dir +local after_frames "`r(frames)'" +_assert "`after_frames'" == "`before_frames'", /// + msg("Legacy test 2: frame list was not restored") +_assert _N == 0, msg("Legacy test 2: default frame should still be empty") +frame withdata { + _assert _N == 74, msg("Legacy test 2: withdata frame row count changed") +} +di as result "PASS: legacy interactive empty-default restoration" + +// ============================================================ +// Test 3. Frame count is preserved. +// ============================================================ +clear all +sysuse auto, clear +frame create f2 +frame f2: sysuse census, clear +frame create f3 +frame f3: sysuse auto, clear +frame create f4 +frame f4: sysuse auto, clear + +qui frames dir +local before_frames "`r(frames)'" +local before_nframes = wordcount("`before_frames'") + +complete_datasignature, frameset_file("outputs/_rt_target_legacy.dtas") + +qui frames dir +local after_frames "`r(frames)'" +local after_nframes = wordcount("`after_frames'") + +_assert `after_nframes' == `before_nframes', /// + msg("Legacy test 3: frame count changed") +_assert "`after_frames'" == "`before_frames'", /// + msg("Legacy test 3: frame list was not restored") +di as result "PASS: legacy interactive frame-count preservation" + +// ============================================================ +// Test 4. Signature stability across repeated calls. +// ============================================================ +clear all +sysuse auto, clear +frame put make price, into(prices) + +complete_datasignature, frameset_file("outputs/_rt_target_legacy.dtas") +local sig_call1 "`r(signature)'" +complete_datasignature, frameset_file("outputs/_rt_target_legacy.dtas") +local sig_call2 "`r(signature)'" + +_assert "`sig_call1'" == "`sig_call2'", /// + msg("Legacy test 4: signature changed across repeated calls") +di as result "PASS: legacy interactive repeated-call stability" + +cap erase "outputs/_rt_target_legacy.dtas" + +di _newline as result "ALL legacy interactive round-trip tests passed" diff --git a/frames/tests/run_all.do b/frames/tests/run_all.do index 0ea2b3d..4727b05 100644 --- a/frames/tests/run_all.do +++ b/frames/tests/run_all.do @@ -4,8 +4,10 @@ do testlib.do // Batch entry point for the frames/tests smoke suite. // If this script stops before the final PASS line, the first failing // child do-file identifies which part of the .dtas test harness broke. +do smoke_dtas_legacy.do do smoke_dtas_blog.do do smoke_dtas_errors.do +do smoke_scons_dtas_legacy.do do smoke_scons_dtas_blog.do di _newline as result "ALL frames/tests batch checks passed" diff --git a/frames/tests/smoke_dtas_legacy.do b/frames/tests/smoke_dtas_legacy.do new file mode 100644 index 0000000..e6e3573 --- /dev/null +++ b/frames/tests/smoke_dtas_legacy.do @@ -0,0 +1,96 @@ +// ============================================================ +// Legacy/simple .dtas smoke test migrated from tests\smoke_dtas.do. +// +// This keeps the older small-data checks in frames/tests so the +// top-level tests tree no longer carries a separate branch-only +// standalone .dtas script. +// ============================================================ + +clear all +do testlib.do +frames_tests_setup + +// ============================================================ +// 1. Determinism: identical content, different save times. +// ============================================================ +clear all +sysuse auto, clear +frame put make price, into(prices) +frames save "outputs/_legacy_dtas_a.dtas", frames(default prices) replace +sleep 1500 +frames save "outputs/_legacy_dtas_b.dtas", frames(default prices) replace + +complete_datasignature, frameset_file("outputs/_legacy_dtas_a.dtas") +local dtas_sig_a "`r(signature)'" +complete_datasignature, frameset_file("outputs/_legacy_dtas_b.dtas") +local dtas_sig_b "`r(signature)'" + +_assert "`dtas_sig_a'" == "`dtas_sig_b'", /// + msg("legacy determinism check failed across identical re-save") +di as result "PASS: legacy determinism across re-save" + +// ============================================================ +// 2. Mutation isolation: change one frame -> aggregate signature differs. +// ============================================================ +clear all +sysuse auto, clear +frame put make price, into(prices) +replace price = price + 1 in 1 +frames save "outputs/_legacy_dtas_a.dtas", frames(default prices) replace +complete_datasignature, frameset_file("outputs/_legacy_dtas_a.dtas") +local dtas_sig_mut "`r(signature)'" + +_assert "`dtas_sig_a'" != "`dtas_sig_mut'", /// + msg("legacy mutation check did not change aggregate signature") +di as result "PASS: legacy mutation changes aggregate signature" + +// Only the default-frame slot should differ here. +tokenize `"`dtas_sig_a'"', parse("|") +local a_default "`1'" +local a_prices "`3'" +tokenize `"`dtas_sig_mut'"', parse("|") +local m_default "`1'" +local m_prices "`3'" +_assert "`a_default'" != "`m_default'", /// + msg("legacy mutation did not affect the changed default-frame slot") +_assert "`a_prices'" == "`m_prices'", /// + msg("legacy mutation changed the unmodified prices slot") +di as result "PASS: legacy mutation stays isolated to the changed frame" + +// ============================================================ +// 3. frlink_* insensitivity. +// ============================================================ +clear all +sysuse auto, clear +frame put rep78 if !mi(rep78), into(quality) +frame change quality +contract rep78 +gen quality_label = "rating " + string(rep78) +frame change default +frlink m:1 rep78, frame(quality) +frames save "outputs/_legacy_dtas_link.dtas", frames(default quality) replace +complete_datasignature, frameset_file("outputs/_legacy_dtas_link.dtas") +local dtas_sig_link1 "`r(signature)'" + +clear all +sysuse auto, clear +frame put rep78 if !mi(rep78), into(quality) +frame change quality +contract rep78 +gen quality_label = "rating " + string(rep78) +frame change default +sleep 1500 +frlink m:1 rep78, frame(quality) +frames save "outputs/_legacy_dtas_link.dtas", frames(default quality) replace +complete_datasignature, frameset_file("outputs/_legacy_dtas_link.dtas") +local dtas_sig_link2 "`r(signature)'" + +_assert "`dtas_sig_link1'" == "`dtas_sig_link2'", /// + msg("legacy frlink_* characteristics leaked into the .dtas signature") +di as result "PASS: legacy frlink_* metadata is excluded from the signature" + +cap erase "outputs/_legacy_dtas_a.dtas" +cap erase "outputs/_legacy_dtas_b.dtas" +cap erase "outputs/_legacy_dtas_link.dtas" + +di _newline as result "ALL legacy/simple .dtas smoke tests passed" diff --git a/frames/tests/smoke_scons_dtas_legacy.do b/frames/tests/smoke_scons_dtas_legacy.do new file mode 100644 index 0000000..4993606 --- /dev/null +++ b/frames/tests/smoke_scons_dtas_legacy.do @@ -0,0 +1,55 @@ +// ============================================================ +// Legacy/simple SCons .dtas smoke test migrated from +// tests\smoke_scons_dtas.do. +// +// This preserves the original small producer -> .dtas -> consumer +// pipeline in frames/tests, alongside the richer blog-style SCons +// checks. +// ============================================================ + +clear all +cap log close _all +do testlib.do +frames_tests_setup + +local smoke_log "logs/smoke-scons-dtas-legacy.smcl" +cap log using "`smoke_log'", replace name(smoke_scons_dtas_legacy) +if _rc != 0 { + di as error "Could not open `smoke_log'" + exit _rc +} + +local config_nohidden "../../tests/config_nohidden.ini" +local sconstruct_legacy "SConstruct-legacy" + +capture noisily { + frames_tests_set_dev_src + + cap noi statacons -f "`sconstruct_legacy'" --config-files="`config_nohidden'" -c + _assert _rc == 0, msg("legacy SCons clean failed") + + cap noi statacons -f "`sconstruct_legacy'" --config-files="`config_nohidden'" /// + outputs/legacy_myset.dtas outputs/legacy_foreign_from_dtas.dta + _assert _rc == 0, msg("legacy .dtas pipeline build failed") + + confirm file "outputs/legacy_myset.dtas" + confirm file "outputs/legacy_foreign_from_dtas.dta" + + store_modts outputs/legacy_myset.dtas, local(mod1_dtas) + store_modts outputs/legacy_foreign_from_dtas.dta, local(mod1_dta) + + cap noi statacons -f "`sconstruct_legacy'" --config-files="`config_nohidden'" /// + outputs/legacy_myset.dtas outputs/legacy_foreign_from_dtas.dta --debug=explain + _assert _rc == 0, msg("legacy .dtas pipeline rerun failed") + + store_modts outputs/legacy_myset.dtas, local(mod2_dtas) + store_modts outputs/legacy_foreign_from_dtas.dta, local(mod2_dta) + + _assert "`mod1_dtas'`mod1_dta'" == "`mod2_dtas'`mod2_dta'", /// + msg("legacy .dtas pipeline rebuilt on an identical rerun") + di as result "PASS: legacy/simple .dtas pipeline does not rebuild on rerun" +} +local rc = _rc + +cap log close smoke_scons_dtas_legacy +exit `rc' diff --git a/tests/code/dtas_consumer.do b/tests/code/dtas_consumer.do deleted file mode 100644 index 4670f35..0000000 --- a/tests/code/dtas_consumer.do +++ /dev/null @@ -1,4 +0,0 @@ -frames use "outputs/myset.dtas", clear -frame change foreign_cars -keep make price -save "outputs/foreign_from_dtas.dta", replace diff --git a/tests/code/dtas_producer.do b/tests/code/dtas_producer.do deleted file mode 100644 index 8a99cc8..0000000 --- a/tests/code/dtas_producer.do +++ /dev/null @@ -1,4 +0,0 @@ -use "inputs/auto-original.dta", clear -frame put * if foreign==1, into(foreign_cars) -frame put * if foreign==0, into(domestic_cars) -frames save "outputs/myset.dtas", frames(default foreign_cars domestic_cars) replace diff --git a/tests/interactive_roundtrip_test.do b/tests/interactive_roundtrip_test.do deleted file mode 100644 index 1732fac..0000000 --- a/tests/interactive_roundtrip_test.do +++ /dev/null @@ -1,224 +0,0 @@ -// ============================================================ -// MANUAL INTERACTIVE TEST -- run by opening Stata interactively, -// NOT with -e do or -b do (those set c(mode)=="batch" and skip -// the round-trip path this script is designed to exercise). -// -// How to run: -// 1. Open Stata interactively. -// 2. cd to C:/Users/rpguiter/Work/statacons/tests -// 3. do interactive_roundtrip_test.do -// -// What passes: all _assert lines exit without error; the final -// "ALL INTERACTIVE ROUND-TRIP TESTS PASSED" message is displayed. -// -// Note: user-defined programs are dropped by frames use ..., clear -// inside complete_datasignature, so all checks use the built-in -// _assert command rather than a helper program. -// ============================================================ - -clear all -adopath ++ "../src" -cap mkdir outputs - -// Guard: abort if accidentally run in batch mode. -if "`c(mode)'" == "batch" { - di as error "This script must be run interactively (c(mode) is 'batch')." - di as error "Open Stata and do this file -- do not run it with -e do." - exit 198 -} - -// ============================================================ -// Setup: build a target .dtas for the tests to sign. -// frames: default (census, 50 obs) + regions (2 vars) -// ============================================================ -clear all -sysuse census, clear -frame put state region, into(regions) -frames save "outputs/_rt_target.dtas", frames(default regions) replace -clear all - -// ============================================================ -// Test 1. Basic round-trip. -// - User frames before the call: default (auto data) + prices -// - Target to sign: _rt_target.dtas (census + regions frames) -// - After the call: user's own frames must be intact. -// ============================================================ -di as txt _newline "--- Test 1: basic round-trip ---" - -sysuse auto, clear -frame put make price mpg, into(prices) - -qui frames dir -local before_frames "`r(frames)'" -local before_N = _N -local before_price1 = price[1] - -complete_datasignature, frameset_file("outputs/_rt_target.dtas") -local sig_t1 "`r(signature)'" - -_assert "`sig_t1'" != "", msg("Test 1: signature is empty") -di as result "PASS: Test 1: signature non-empty" - -qui frames dir -local after_frames "`r(frames)'" -_assert "`after_frames'" == "`before_frames'", /// - msg("Test 1: frame list -- expected `before_frames', got `after_frames'") -di as result "PASS: Test 1: frame list restored (`after_frames')" - -_assert _N == `before_N', msg("Test 1: default frame N -- expected `before_N', got `=_N'") -di as result "PASS: Test 1: default frame N = `=_N'" - -_assert price[1] == `before_price1', /// - msg("Test 1: default frame price[1] -- expected `before_price1', got `=price[1]'") -di as result "PASS: Test 1: default frame price[1] intact" - -frame prices { - _assert _N == 74, msg("Test 1: prices frame N -- expected 74, got `=_N'") - di as result "PASS: Test 1: prices frame N = `=_N'" - _assert c(k) == 3, msg("Test 1: prices frame k -- expected 3, got `=c(k)'") - di as result "PASS: Test 1: prices frame k = `=c(k)'" -} - -di as result "Test 1 passed." - -// ============================================================ -// Test 2. Empty default frame. -// - User default frame is empty; one additional frame has data. -// - Verifies the emptyok save guard works and the empty default -// is restored (not replaced by the target's frames). -// ============================================================ -di as txt _newline "--- Test 2: empty default frame ---" - -clear all -frame create withdata -frame withdata: sysuse auto, clear - -qui frames dir -local before_frames "`r(frames)'" // "default withdata" - -complete_datasignature, frameset_file("outputs/_rt_target.dtas") -local sig_t2 "`r(signature)'" - -_assert "`sig_t2'" != "", msg("Test 2: signature is empty") -di as result "PASS: Test 2: signature non-empty" - -qui frames dir -local after_frames "`r(frames)'" -_assert "`after_frames'" == "`before_frames'", /// - msg("Test 2: frame list -- expected `before_frames', got `after_frames'") -di as result "PASS: Test 2: frame list restored (`after_frames')" - -_assert _N == 0, msg("Test 2: default frame should be empty, got `=_N' rows") -di as result "PASS: Test 2: default frame still empty" - -frame withdata { - _assert _N == 74, msg("Test 2: withdata N -- expected 74, got `=_N'") - di as result "PASS: Test 2: withdata frame N = `=_N'" -} - -di as result "Test 2 passed." - -// ============================================================ -// Test 3. Frame name collision. -// - One user frame has the same name as a frame inside the -// target .dtas ("regions"). The user's frame must be -// restored with the user's content, not the target's. -// ============================================================ -di as txt _newline "--- Test 3: frame name collision ---" - -clear all -sysuse auto, clear -frame create regions // same name as a frame in _rt_target -frame regions: sysuse auto, clear // different data (74 obs, 12 vars) - -qui frames dir -local before_frames "`r(frames)'" - -complete_datasignature, frameset_file("outputs/_rt_target.dtas") -local sig_t3 "`r(signature)'" - -_assert "`sig_t3'" != "", msg("Test 3: signature is empty") -di as result "PASS: Test 3: signature non-empty" - -qui frames dir -local after_frames "`r(frames)'" -_assert "`after_frames'" == "`before_frames'", /// - msg("Test 3: frame list -- expected `before_frames', got `after_frames'") -di as result "PASS: Test 3: frame list restored (`after_frames')" - -// User's "regions" must still hold auto (74 obs, 12 vars), -// not the 2-variable regions frame from the target. -frame regions { - _assert _N == 74, msg("Test 3: collision frame N -- expected 74, got `=_N'") - di as result "PASS: Test 3: collision frame N = `=_N'" - _assert c(k) == 12, msg("Test 3: collision frame k -- expected 12, got `=c(k)'") - di as result "PASS: Test 3: collision frame k = `=c(k)'" -} - -di as result "Test 3 passed." - -// ============================================================ -// Test 4. Frame count preserved (user has more frames than target). -// - User has 4 frames; target .dtas has 2. -// - After the call: still exactly 4 user frames. -// ============================================================ -di as txt _newline "--- Test 4: frame count preserved ---" - -clear all -sysuse auto, clear -frame create f2 -frame f2: sysuse census, clear -frame create f3 -frame f3: sysuse auto, clear -frame create f4 -frame f4: sysuse auto, clear - -qui frames dir -local before_frames "`r(frames)'" // "default f2 f3 f4" -local before_nframes = wordcount("`before_frames'") - -complete_datasignature, frameset_file("outputs/_rt_target.dtas") - -qui frames dir -local after_frames "`r(frames)'" -local after_nframes = wordcount("`after_frames'") - -_assert `after_nframes' == `before_nframes', /// - msg("Test 4: frame count -- expected `before_nframes', got `after_nframes'") -di as result "PASS: Test 4: frame count = `after_nframes'" - -_assert "`after_frames'" == "`before_frames'", /// - msg("Test 4: frame list -- expected `before_frames', got `after_frames'") -di as result "PASS: Test 4: frame list restored (`after_frames')" - -di as result "Test 4 passed." - -// ============================================================ -// Test 5. Signature stability across calls. -// - Calling twice from the same interactive session must return -// the same value (the round-trip must not mutate state that -// the signature depends on inside the target file). -// ============================================================ -di as txt _newline "--- Test 5: signature stability across calls ---" - -clear all -sysuse auto, clear -frame put make price, into(prices) - -complete_datasignature, frameset_file("outputs/_rt_target.dtas") -local sig_call1 "`r(signature)'" -complete_datasignature, frameset_file("outputs/_rt_target.dtas") -local sig_call2 "`r(signature)'" - -_assert "`sig_call1'" == "`sig_call2'", /// - msg("Test 5: sig changed between calls -- call1=`sig_call1' call2=`sig_call2'") -di as result "PASS: Test 5: sig stable across calls" - -di as result "Test 5 passed." - -// ============================================================ -// Cleanup -// ============================================================ -cap erase "outputs/_rt_target.dtas" - -di _newline as result "ALL INTERACTIVE ROUND-TRIP TESTS PASSED" diff --git a/tests/smoke_dtas.do b/tests/smoke_dtas.do deleted file mode 100644 index 3ea9342..0000000 --- a/tests/smoke_dtas.do +++ /dev/null @@ -1,104 +0,0 @@ -// ============================================================ -// DEV SCAFFOLD -- not part of the formal statacons_test.do suite. -// Kept in tests/ for quick standalone validation of the .dtas -// signature path during development. The corresponding asserts -// are mirrored into statacons_test.do; this file just runs them -// in isolation without the rest of the harness. -// ============================================================ -// Focused regression test for .dtas signature support -// Run from C:/Users/rpguiter/Work/statacons/tests with: StataMP-64.exe -e do smoke_dtas.do - -clear all -adopath ++ "../src" -cap mkdir outputs - -// ============================================================ -// 1. Determinism: identical content, different save times -> identical signatures -// ============================================================ -clear all -sysuse auto, clear -frame put make price, into(prices) -frames save "outputs/_dtas_a.dtas", frames(default prices) replace -sleep 1500 -frames save "outputs/_dtas_b.dtas", frames(default prices) replace - -complete_datasignature, frameset_file("outputs/_dtas_a.dtas") -local dtas_sig_a "`r(signature)'" -complete_datasignature, frameset_file("outputs/_dtas_b.dtas") -local dtas_sig_b "`r(signature)'" - -di as txt "sig_a = " as res "`dtas_sig_a'" -di as txt "sig_b = " as res "`dtas_sig_b'" -_assert "`dtas_sig_a'"=="`dtas_sig_b'", msg("determinism across re-save failed") -di as result "PASS: determinism across re-save" - -// ============================================================ -// 2. Mutation isolation: change one frame -> only that slot differs -// ============================================================ -clear all -sysuse auto, clear -frame put make price, into(prices) -replace price = price + 1 in 1 -frames save "outputs/_dtas_a.dtas", frames(default prices) replace -complete_datasignature, frameset_file("outputs/_dtas_a.dtas") -local dtas_sig_mut "`r(signature)'" - -di as txt "sig_mut = " as res "`dtas_sig_mut'" -_assert "`dtas_sig_a'"!="`dtas_sig_mut'", msg("mutation should change signature") -di as result "PASS: mutation changes aggregate signature" - -// inspect: only the default-frame slot should differ -tokenize `"`dtas_sig_a'"', parse("|") -local a_default "`1'" -local a_prices "`3'" -tokenize `"`dtas_sig_mut'"', parse("|") -local m_default "`1'" -local m_prices "`3'" -di as txt "default slot orig: " as res "`a_default'" -di as txt "default slot mut: " as res "`m_default'" -di as txt "prices slot orig: " as res "`a_prices'" -di as txt "prices slot mut: " as res "`m_prices'" -_assert "`a_default'"!="`m_default'", msg("default slot should differ (mutated)") -_assert "`a_prices'"=="`m_prices'", msg("prices slot should be stable (unmutated)") -di as result "PASS: mutation isolated to the changed frame" - -// ============================================================ -// 3. frlink_* insensitivity: re-saving a linked frameset refreshes -// frlink_date but the .dtas signature MUST stay stable. -// ============================================================ -clear all -sysuse auto, clear -frame put rep78 if !mi(rep78), into(quality) -frame change quality -contract rep78 -gen quality_label = "rating " + string(rep78) -frame change default -frlink m:1 rep78, frame(quality) -frames save "outputs/_dtas_link.dtas", frames(default quality) replace -complete_datasignature, frameset_file("outputs/_dtas_link.dtas") -local dtas_sig_link1 "`r(signature)'" -di as txt "link sig (1) = " as res "`dtas_sig_link1'" - -clear all -sysuse auto, clear -frame put rep78 if !mi(rep78), into(quality) -frame change quality -contract rep78 -gen quality_label = "rating " + string(rep78) -frame change default -sleep 1500 -frlink m:1 rep78, frame(quality) -frames save "outputs/_dtas_link.dtas", frames(default quality) replace -complete_datasignature, frameset_file("outputs/_dtas_link.dtas") -local dtas_sig_link2 "`r(signature)'" -di as txt "link sig (2) = " as res "`dtas_sig_link2'" - -_assert "`dtas_sig_link1'"=="`dtas_sig_link2'", msg("frlink_* characteristics leaked into signature") -di as result "PASS: frlink_* characteristics excluded from signature" - -// cleanup -cap erase "outputs/_dtas_a.dtas" -cap erase "outputs/_dtas_b.dtas" -cap erase "outputs/_dtas_link.dtas" - -di _newline as result "ALL .dtas SMOKE TESTS PASSED" diff --git a/tests/smoke_scons_dtas.do b/tests/smoke_scons_dtas.do deleted file mode 100644 index b24a065..0000000 --- a/tests/smoke_scons_dtas.do +++ /dev/null @@ -1,41 +0,0 @@ -// ============================================================ -// DEV SCAFFOLD -- not part of the formal statacons_test.do suite. -// Kept in tests/ for quick standalone validation of the SCons -// .dtas pipeline (producer -> .dtas -> consumer -> .dta) during -// development. Mirrors the SCons-end-to-end section in -// statacons_test.do; this file just runs it in isolation. -// ============================================================ -// Focused SCons end-to-end test for .dtas signature path -// Verifies that the Python env-var dev hatch works from within Stata. - -clear all -adopath ++ "../src" - -local dev_src "`c(pwd)'/../src" -di as txt "dev_src = `dev_src'" -python: import os; os.environ['STATACONS_DEV_SRC'] = r"`dev_src'" -python: import os; print('STATACONS_DEV_SRC =', os.environ.get('STATACONS_DEV_SRC')) - -// Clean and build -statacons -c -statacons outputs/myset.dtas outputs/foreign_from_dtas.dta - -cap program drop store_modts -program store_modts - syntax anything, local(string) - filesys `c(pwd)'/`anything', attr - c_local `local' "`r(modifiednum)'" -end - -cap store_modts outputs/myset.dtas, local(mod1_dtas) -cap store_modts outputs/foreign_from_dtas.dta, local(mod1_dta) -di as txt "After first build: mod1_dtas = `mod1_dtas' mod1_dta = `mod1_dta'" - -// Re-run -- should be a no-op -statacons outputs/myset.dtas outputs/foreign_from_dtas.dta -cap store_modts outputs/myset.dtas, local(mod2_dtas) -cap store_modts outputs/foreign_from_dtas.dta, local(mod2_dta) -di as txt "After second run: mod2_dtas = `mod2_dtas' mod2_dta = `mod2_dta'" - -_assert "`mod1_dtas'`mod1_dta'"=="`mod2_dtas'`mod2_dta'", msg(".dtas pipeline re-ran despite no input change") -di as result "PASS: .dtas pipeline does not rebuild on re-run with identical inputs" From b137d541ffc8fddbd713a9dfa4bcb9f6c0f51f2a Mon Sep 17 00:00:00 2001 From: rpguiteras Date: Thu, 21 May 2026 16:33:05 -0400 Subject: [PATCH 12/21] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index e6a3a29..2790065 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ docs/.buildinfo *.stswp CLAUDE.md notes/ +frames/session-notes.md From e98442b44c1190bf0f06ed9d853dd8f8e0f4f708 Mon Sep 17 00:00:00 2001 From: rpguiteras Date: Fri, 22 May 2026 09:24:08 -0400 Subject: [PATCH 13/21] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 2790065..fd0ea1f 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ docs/.buildinfo CLAUDE.md notes/ frames/session-notes.md +statacons.code-workspace From 3ca8a3f51841601646c2108e6664ab83ea90f322 Mon Sep 17 00:00:00 2001 From: rpguiteras Date: Fri, 22 May 2026 10:10:10 -0400 Subject: [PATCH 14/21] update version in ado and regenerate help --- src/complete_datasignature.ado | 4 ++-- src/complete_datasignature.md | 22 ++++++++++++++++++---- src/complete_datasignature.sthlp | 23 +++++++++++++++++++---- src/statacons.ado | 4 ++-- src/statacons.md | 2 +- src/statacons.sthlp | 2 +- src/stataconsign.ado | 4 ++-- src/stataconsign.md | 2 +- src/stataconsign.sthlp | 2 +- 9 files changed, 47 insertions(+), 18 deletions(-) diff --git a/src/complete_datasignature.ado b/src/complete_datasignature.ado index f2c404f..bd97bfa 100644 --- a/src/complete_datasignature.ado +++ b/src/complete_datasignature.ado @@ -1,4 +1,4 @@ -*! version 3.0.4 October 2025 statacons team +*! version 3.1.0-alpha1 22 May 2026 statacons team * Copyright 2023. This work is licensed under a CC BY 4.0 license. version 16.1 @@ -142,7 +142,7 @@ end /*** -_version 3.0.4_ +_version 3.1.0-alpha1_ complete_datasignature ====== diff --git a/src/complete_datasignature.md b/src/complete_datasignature.md index 97b66c3..60ecdb5 100644 --- a/src/complete_datasignature.md +++ b/src/complete_datasignature.md @@ -1,17 +1,17 @@ -_version 3.0.3_ +_version 3.1.0-alpha1_ complete_datasignature ====== -__complete_datasignature__ creates a signature for a Stata .dta-file that does __not__ depend on the embedded timestamp but __does__ depend on the data and, optionally, no other metadata, variable and value labels only, or all metadata. +__complete_datasignature__ creates a signature for a Stata .dta-file or .dtas frameset that does __not__ depend on the embedded timestamp but __does__ depend on the data and, optionally, no other metadata, variable and value labels only, or all metadata. -__complete_datasignature__ extends Stata's __datasignature__ by allowing the inclusion of different sets of metadata. +__complete_datasignature__ extends Stata's __datasignature__ by allowing the inclusion of different sets of metadata. When called with __frameset_file__, it signs every frame in a __.dtas__ frameset and returns a concatenation keyed by frame name. Syntax ------ -> complete_datasignature [, dta_file("file.dta") fname("sigfile.ext") nometa fast labels_formats_only] +> complete_datasignature [, dta_file("file.dta") frameset_file("file.dtas") fname("sigfile.ext") nometa fast labels_formats_only skip_char("globlist")] By default, __complete_datasignature__ will use the dta-file in memory to create create a signature that depends on the data and all metadata, but not the embedded timestamp. @@ -21,12 +21,23 @@ By default, __complete_datasignature__ will use the dta-file in memory to create | Option | Description | |----------------------------|----------------------------------------------------| | dta_file("file.dta") | Use "file.dta" instead of dta-file in memory | +| frameset_file("file.dtas") | Sign a __.dtas__ frameset; iterate frames alphabetically and return "frameA=sigA|frameB=sigB|..." | | fname("sigfile.ext") | write signature to "sigfile.ext" | | nometa | Do not include any metadata -- equivalent of Stata's __datasignature__ | | labels_formats_only | Include variable formats, variable and value labels | | fast | use ___datasignature__ in _fast_ mode -- faster but not machine-independent | +| skip_char("globlist") | Skip variable/dataset characteristics whose names match any pattern in the space-separated globlist. The __frameset_file__ path always adds __frlink_*__ to this list. | +### Behavior of __frameset_file__ + +When __frameset_file__ is set, the program: + +1. In interactive mode (__c(mode)__ is empty), saves all in-memory frames to a temporary __.dtas__ so user state can be restored on exit. In batch mode, this round-trip is skipped. +2. Loads the target __.dtas__ via __frames use, clear__. +3. For each frame in alphabetical order, computes a per-frame signature using the same metadata options (__nometa__, __fast__, __labels_formats_only__) plus __skip_char("frlink_*")__. +4. Assembles "frameA=sigA|frameB=sigB|...". +5. In interactive mode, restores the user's frames from the temporary __.dtas__. Example(s) ---------- @@ -50,6 +61,9 @@ Example(s) . complete_datasignature, nometa fast 74:12(71728):3831085005:186045760 + . complete_datasignature, frameset_file("myframeset.dtas") + census=74:12(71728):...|housing=50:12(...):... + diff --git a/src/complete_datasignature.sthlp b/src/complete_datasignature.sthlp index dc9a83f..da973f3 100644 --- a/src/complete_datasignature.sthlp +++ b/src/complete_datasignature.sthlp @@ -1,22 +1,22 @@ {smcl} {p 4 4 2} -{it:version 3.0.3} +{it:version 3.1.0-alpha1} {title:complete_datasignature} {p 4 4 2} -{bf:complete_datasignature} creates a signature for a Stata .dta-file that does {bf:not} depend on the embedded timestamp but {bf:does} depend on the data and, optionally, no other metadata, variable and value labels only, or all metadata. +{bf:complete_datasignature} creates a signature for a Stata .dta-file or .dtas frameset that does {bf:not} depend on the embedded timestamp but {bf:does} depend on the data and, optionally, no other metadata, variable and value labels only, or all metadata. {p 4 4 2} -{bf:complete_datasignature} extends Stata{c 39}s {bf:datasignature} by allowing the inclusion of different sets of metadata. +{bf:complete_datasignature} extends Stata{c 39}s {bf:datasignature} by allowing the inclusion of different sets of metadata. When called with {bf:frameset_file}, it signs every frame in a {bf:.dtas} frameset and returns a concatenation keyed by frame name. {title:Syntax} -{p 8 8 2} complete_datasignature [, dta_file("file.dta") fname("sigfile.ext") nometa fast labels_formats_only] +{p 8 8 2} complete_datasignature [, dta_file("file.dta") frameset_file("file.dtas") fname("sigfile.ext") nometa fast labels_formats_only skip_char("globlist")] {p 4 4 2} @@ -27,12 +27,24 @@ By default, {bf:complete_datasignature} will use the dta-file in memory to creat {col 5}Option{col 33}Description {space 4}{hline} {col 5}dta_file("file.dta"){col 33}Use "file.dta" instead of dta-file in memory +{col 5}frameset_file("file.dtas"){col 33}Sign a {bf:.dtas} frameset; iterate frames alphabetically and return "frameA=sigA|frameB=sigB|..." {col 5}fname("sigfile.ext"){col 33}write signature to "sigfile.ext" {col 5}nometa{col 33}Do not include any metadata -- equivalent of Stata{c 39}s {bf:datasignature} {col 5}labels_formats_only{col 33}Include variable formats, variable and value labels {col 5}fast{col 33}use {bf:_datasignature} in {it:fast} mode -- faster but not machine-independent +{col 5}skip_char("globlist"){col 33}Skip variable/dataset characteristics whose names match any pattern in the space-separated globlist. The {bf:frameset_file} path always adds {bf:frlink_*} to this list. {space 4}{hline} +{p 4 4 2}{bf:Behavior of {bf:frameset_file__} + +{p 4 4 2} +When {bf:frameset_file} is set, the program: + +{break} 1. In interactive mode ({it:_c(mode)} is empty), saves all in-memory frames to a temporary {bf:.dtas} so user state can be restored on exit. In batch mode, this round-trip is skipped. +{break} 2. Loads the target {bf:.dtas} via {bf:frames use, clear}. +{break} 3. For each frame in alphabetical order, computes a per-frame signature using the same metadata options ({it:_nometa}, {bf:fast}, {bf:labels_formats_only_}) plus {bf:skip_char("frlink_*")}. +{break} 4. Assembles "frameA=sigA|frameB=sigB|...". +{break} 5. In interactive mode, restores the user{c 39}s frames from the temporary {bf:.dtas}. {title:Example(s)} @@ -56,6 +68,9 @@ By default, {bf:complete_datasignature} will use the dta-file in memory to creat . complete_datasignature, nometa fast 74:12(71728):3831085005:186045760 + . complete_datasignature, frameset_file("myframeset.dtas") + census=74:12(71728):...|housing=50:12(...):... + diff --git a/src/statacons.ado b/src/statacons.ado index 997964f..a76c85d 100644 --- a/src/statacons.ado +++ b/src/statacons.ado @@ -1,4 +1,4 @@ -*! version 3.0.4 November 11 2025 statacons team +*! version 3.1.0-alpha1 22 May 2026 statacons team * Copyright 2025. This work is licensed under a CC BY 4.0 license. program statacons, rclass @@ -54,7 +54,7 @@ end /*** -_version 3.0.2_ +_version 3.1.0-alpha1_ statacons ====== diff --git a/src/statacons.md b/src/statacons.md index 84c656b..f1809da 100644 --- a/src/statacons.md +++ b/src/statacons.md @@ -1,4 +1,4 @@ -_version 3.0.2_ +_version 3.1.0-alpha1_ statacons ====== diff --git a/src/statacons.sthlp b/src/statacons.sthlp index 95ce23d..ff57f34 100644 --- a/src/statacons.sthlp +++ b/src/statacons.sthlp @@ -2,7 +2,7 @@ {p 4 4 2} -{it:version 3.0.2} +{it:version 3.1.0-alpha1} {title:statacons} diff --git a/src/stataconsign.ado b/src/stataconsign.ado index fe9e4a5..76888d9 100644 --- a/src/stataconsign.ado +++ b/src/stataconsign.ado @@ -1,4 +1,4 @@ -*! version 3.0.4 November 1 2025 statacons team +*! version 3.1.0-alpha1 22 May 2026 statacons team * Copyright 2025. This work is licensed under a CC BY 4.0 license. * Anaconda env's script dir is added to path when you switch environments, but not added to PYTHONPATH/sys.path, @@ -18,7 +18,7 @@ end /*** -_version 3.0.2_ +_version 3.1.0-alpha1_ stataconsign ====== diff --git a/src/stataconsign.md b/src/stataconsign.md index 2a2aafe..e17f7f3 100644 --- a/src/stataconsign.md +++ b/src/stataconsign.md @@ -1,4 +1,4 @@ -_version 3.0.2_ +_version 3.1.0-alpha1_ stataconsign ====== diff --git a/src/stataconsign.sthlp b/src/stataconsign.sthlp index 09122d2..5c784c8 100644 --- a/src/stataconsign.sthlp +++ b/src/stataconsign.sthlp @@ -1,7 +1,7 @@ {smcl} {p 4 4 2} -{it:version 3.0.2} +{it:version 3.1.0-alpha1} {title:stataconsign} From ebf230b56abb8c54d38c30d6ffcd7487b96ce9b8 Mon Sep 17 00:00:00 2001 From: rpguiteras Date: Fri, 22 May 2026 10:17:36 -0400 Subject: [PATCH 15/21] more updating version numbers --- pypkg/setup.cfg | 2 +- pypkg/src/pystatacons/__init__.py | 2 +- src/statacons.pkg | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pypkg/setup.cfg b/pypkg/setup.cfg index 71bd76d..5b8458e 100644 --- a/pypkg/setup.cfg +++ b/pypkg/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pystatacons -version = 3.0.4 +version = 3.1.0-alpha1 author = Statacons team author_email = Brian.Quistorff@bea.gov description = statacons Python package diff --git a/pypkg/src/pystatacons/__init__.py b/pypkg/src/pystatacons/__init__.py index 8b323fe..e68cf71 100644 --- a/pypkg/src/pystatacons/__init__.py +++ b/pypkg/src/pystatacons/__init__.py @@ -32,7 +32,7 @@ """ -__version__ = "3.0.4" +__version__ = "3.1.0-alpha1" __all__ = ['init_env', 'decider_str_lookup', 'special_sig_fns', 'stata_run_params_factory'] from .deciders import decider_str_lookup, dependency_newer_then_content_changed, \ changed_timestamp_then_dependency_newer_then_content_content diff --git a/src/statacons.pkg b/src/statacons.pkg index 0234c9a..881ec2d 100644 --- a/src/statacons.pkg +++ b/src/statacons.pkg @@ -2,9 +2,9 @@ d 'statacons': statacons d d Enables using SCons with Stata d -d Version 3.0.4 +d Version 3.1.0-alpha d -d Distribution-Date: 20251111 +d Distribution-Date: 20260522 d Requires: Python and SCons package d d KW: build system, dependency From a6f9a2e6095806a2862e1cb7a3fe2a280a89c9cc Mon Sep 17 00:00:00 2001 From: rpguiteras Date: Fri, 22 May 2026 10:25:14 -0400 Subject: [PATCH 16/21] Update CHANGELOG.md --- CHANGELOG.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6ca173..2a5508a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,23 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased +## 3.1.0-alpha1 +### Added +- complete_datasignature: New `frameset_file("file.dtas")` option to sign Stata frameset + (`.dtas`) files as first-class build artifacts. Generates a concatenated per-frame + signature in the form `frameA=sigA|frameB=sigB|...`. +- complete_datasignature: New `skip_char(globlist)` option to exclude characteristics + matching any pattern in a space-separated globlist (used to drop time-tainted metadata + such as `frlink_*` characteristics). +- pystatacons: `.dtas` framesets registered with SCons as signed nodes via `get_dtas_sign`, + giving them timestamp-independent signatures alongside `.dta` files. +- pystatacons: New `dev_helpers.py` module with `dev_adopath_prefix()` to support + editable-install development workflows via the `STATACONS_DEV_SRC` environment variable. +### Changed +- complete_datasignature: In interactive mode, in-memory frames are preserved across + frameset signing via a temporary `.dtas` round-trip; batch mode skips the round-trip. +- complete_datasignature: Metadata collection now handles empty datasets (frames with no + variables) without error. ### Fixed - pystatacons: Fixed error with opening log files, #25. From 7e2dac42e7ddf6b08b1eb9a82aed7a02fed5bf11 Mon Sep 17 00:00:00 2001 From: rpguiteras Date: Fri, 22 May 2026 13:35:08 -0400 Subject: [PATCH 17/21] Add fallback for Stata < 18 Introduce a new 'frameset_signing' SCons config (auto|enabled|disabled) to control .dtas (frameset) signing on Stata <18. Implement logic in pystatacons/stata_utils: get_dtas_sign now detects Stata version once, delegates to get_datasign on Stata 18+, and in Stata <18 either falls back to MD5 with a one-time warning (auto), raises a hard error (enabled), or always use MD5 (disabled). init_env defaults frameset_signing to 'auto' and only registers the .dtas special signature function when not disabled. Also improve error reporting from get_datasign to include the Stata log when a batch run fails. Add tests and SCons test scaffolding for Stata 17, update config templates and docs, and bump package and ado/documentation versions to 3.1.0-alpha2. Note: switching between frameset-aware and MD5 signatures requires a one-time full rebuild due to incompatible sconsign entries. --- CHANGELOG.md | 17 +++++ frames/tests/Stata17/.gitignore | 1 + frames/tests/Stata17/SConstruct-stata17test | 18 ++++++ frames/tests/Stata17/test_disabled_config.ini | 3 + frames/tests/Stata17/test_stata17_config.ini | 6 ++ frames/tests/Stata17/test_stata17_guard.do | 14 ++++ .../tests/Stata17/test_stata17_nocapture.do | 7 ++ pypkg/setup.cfg | 2 +- pypkg/src/pystatacons/__init__.py | 2 +- pypkg/src/pystatacons/stata_utils.py | 64 +++++++++++++++++-- src/complete_datasignature.ado | 8 ++- src/complete_datasignature.md | 2 +- src/complete_datasignature.sthlp | 2 +- src/config_project.ini | 11 ++++ src/statacons.ado | 4 +- src/statacons.md | 2 +- src/statacons.sthlp | 2 +- src/stataconsign.ado | 4 +- src/stataconsign.md | 2 +- src/stataconsign.sthlp | 2 +- src/utils/config_local_template.ini | 6 ++ 21 files changed, 159 insertions(+), 20 deletions(-) create mode 100644 frames/tests/Stata17/.gitignore create mode 100644 frames/tests/Stata17/SConstruct-stata17test create mode 100644 frames/tests/Stata17/test_disabled_config.ini create mode 100644 frames/tests/Stata17/test_stata17_config.ini create mode 100644 frames/tests/Stata17/test_stata17_guard.do create mode 100644 frames/tests/Stata17/test_stata17_nocapture.do diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a5508a..ec91f87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,23 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 3.1.0-alpha2 +### Added +- pystatacons: New `frameset_signing` config option (`auto` / `enabled` / `disabled`) in + `config_project.ini` to control `.dtas` signing behaviour on Stata < 18. + Default `auto` falls back to standard MD5 checksums on Stata 16/17 (with a one-time + warning); `disabled` always uses MD5 for `.dtas` without running a Stata batch; + `enabled` raises a hard error if Stata < 18 is detected (useful to enforce Stata 18+ + across a team). `.dta` signing is unaffected in all modes. + Note: switching between frameset-aware and MD5 signatures for `.dtas` nodes causes a + one-time full rebuild (`.sconsign.dblite` entries are incompatible between the two + formats). +- complete_datasignature: Version guard in `frameset_file()` branch raises a clear error + (`STATACONS_REQUIRES_STATA18`) on Stata < 18, enabling the Python-side MD5 fallback. +### Fixed +- pystatacons: Stata log content now included in exception message when Stata returns a + non-zero exit code, improving debuggability of batch errors. + ## 3.1.0-alpha1 ### Added - complete_datasignature: New `frameset_file("file.dtas")` option to sign Stata frameset diff --git a/frames/tests/Stata17/.gitignore b/frames/tests/Stata17/.gitignore new file mode 100644 index 0000000..a7b6192 --- /dev/null +++ b/frames/tests/Stata17/.gitignore @@ -0,0 +1 @@ +test_sig_out.txt diff --git a/frames/tests/Stata17/SConstruct-stata17test b/frames/tests/Stata17/SConstruct-stata17test new file mode 100644 index 0000000..a902846 --- /dev/null +++ b/frames/tests/Stata17/SConstruct-stata17test @@ -0,0 +1,18 @@ +# Minimal SCons test for Stata <18 .dtas signing fallback. +# Run from frames/tests/Stata17/ with: +# python -m SCons --sconstruct=SConstruct-stata17test --config-files=test_stata17_config.ini -n +# python -m SCons --sconstruct=SConstruct-stata17test --config-files=test_disabled_config.ini -n +import os +import pystatacons + +os.environ.setdefault('STATACONS_DEV_SRC', + os.path.normpath(os.path.join(os.getcwd(), '../../../src'))) + +env = pystatacons.init_env() + +consumer = env.StataBuild( + target=['../outputs/legacy_foreign_from_dtas_s17.dta'], + source='../code/dtas_legacy_consumer.do', + depends=['../outputs/legacy_myset.dtas'] +) +Default(consumer) diff --git a/frames/tests/Stata17/test_disabled_config.ini b/frames/tests/Stata17/test_disabled_config.ini new file mode 100644 index 0000000..e8b637a --- /dev/null +++ b/frames/tests/Stata17/test_disabled_config.ini @@ -0,0 +1,3 @@ +[SCons] +use_custom_datasignature: Strict +frameset_signing: disabled diff --git a/frames/tests/Stata17/test_stata17_config.ini b/frames/tests/Stata17/test_stata17_config.ini new file mode 100644 index 0000000..2c42787 --- /dev/null +++ b/frames/tests/Stata17/test_stata17_config.ini @@ -0,0 +1,6 @@ +[Programs] +stata_exe: "C:/Program Files/Stata17/StataMP-64.exe" + +[SCons] +use_custom_datasignature: Strict +frameset_signing: auto diff --git a/frames/tests/Stata17/test_stata17_guard.do b/frames/tests/Stata17/test_stata17_guard.do new file mode 100644 index 0000000..caa3e82 --- /dev/null +++ b/frames/tests/Stata17/test_stata17_guard.do @@ -0,0 +1,14 @@ +// Test: complete_datasignature frameset_file() should return rc=198 on Stata <18. +// Run from frames/tests/Stata17/ with: +// Start-Process "C:\Program Files\Stata17\StataMP-64.exe" -ArgumentList "/e do test_stata17_guard.do" -Wait +adopath ++ "../../../src" +capture complete_datasignature, frameset_file("../outputs/legacy_myset.dtas") fname("test_sig_out.txt") +loc rc = _rc +di "return code: `rc'" +if `rc' == 198 { + di as result "PASS: exit 198 as expected on Stata `=int(`c(stata_version)')'" +} +else { + di as error "FAIL: unexpected return code `rc'" + exit 1 +} diff --git a/frames/tests/Stata17/test_stata17_nocapture.do b/frames/tests/Stata17/test_stata17_nocapture.do new file mode 100644 index 0000000..1419a5f --- /dev/null +++ b/frames/tests/Stata17/test_stata17_nocapture.do @@ -0,0 +1,7 @@ +// Test: show raw error output from frameset_file() on Stata <18 (no capture). +// The log should contain "STATACONS_REQUIRES_STATA18" -- this is the sentinel +// string Python searches for in get_dtas_sign() to trigger the MD5 fallback. +// Run from frames/tests/Stata17/ with: +// Start-Process "C:\Program Files\Stata17\StataMP-64.exe" -ArgumentList "/e do test_stata17_nocapture.do" -Wait +adopath ++ "../../../src" +complete_datasignature, frameset_file("../outputs/legacy_myset.dtas") fname("test_sig_out.txt") diff --git a/pypkg/setup.cfg b/pypkg/setup.cfg index 5b8458e..cc92e6e 100644 --- a/pypkg/setup.cfg +++ b/pypkg/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pystatacons -version = 3.1.0-alpha1 +version = 3.1.0-alpha2 author = Statacons team author_email = Brian.Quistorff@bea.gov description = statacons Python package diff --git a/pypkg/src/pystatacons/__init__.py b/pypkg/src/pystatacons/__init__.py index e68cf71..c0f41d4 100644 --- a/pypkg/src/pystatacons/__init__.py +++ b/pypkg/src/pystatacons/__init__.py @@ -32,7 +32,7 @@ """ -__version__ = "3.1.0-alpha1" +__version__ = "3.1.0-alpha2" __all__ = ['init_env', 'decider_str_lookup', 'special_sig_fns', 'stata_run_params_factory'] from .deciders import decider_str_lookup, dependency_newer_then_content_changed, \ changed_timestamp_then_dependency_newer_then_content_content diff --git a/pypkg/src/pystatacons/stata_utils.py b/pypkg/src/pystatacons/stata_utils.py index eb0a34b..258a888 100644 --- a/pypkg/src/pystatacons/stata_utils.py +++ b/pypkg/src/pystatacons/stata_utils.py @@ -429,6 +429,7 @@ def stata_run_params_factory(self: Environment, target: Union[list, str], source ############################################## # how to get a Stata-style datasignature int_env = None +_dtas_stata_version_ok = None # None=unknown, True=Stata 18+, False=Stata <18 def get_datasign(fname, file_arg='dta_file'): @@ -462,10 +463,19 @@ def get_datasign(fname, file_arg='dta_file'): cmd_line = int_env['STATABATCHCOM'] + " do " + recipe_fname ret_code = run_stata_cmd(cmd_line, args_split, None, has_pywin32 and not no_hidden) silentremove(recipe_fname) - if ret_code != 0: # In case the Stata executable has a real issue + if ret_code != 0: silentremove(sig_fname) + err_msg = "Couldn't get the file data-signature. Stata error=" + str(ret_code) + "\nStata log:\n" + try: + with open(log_name, 'r') as f: + err_msg += f.read() + except UnicodeDecodeError: + with open(log_name, 'r', encoding='utf-8') as f: + err_msg += f.read() + except Exception: + err_msg += "(log not available)" silentremove(log_name) - raise Exception("Couldn't get the file data-signature. Stata error=" + str(ret_code)) + raise Exception(err_msg) if not os.path.exists(sig_fname): err_msg = "Couldn't get the file data-signature. Stata log:\n" try: @@ -489,10 +499,45 @@ def get_datasign(fname, file_arg='dta_file'): def get_dtas_sign(fname): """Timestamp-independent signature for a .dtas frameset. - Delegates to get_datasign with file_arg='frameset_file' so all config + On Stata 18+, delegates to get_datasign (frameset_file option) so all config plumbing (custom metadata mode, fast/slow, cache_dir) is shared. + On Stata < 18, behaviour is controlled by the frameset_signing config option: + auto (default) -- fall back to standard MD5, warn once per scons run + enabled -- raise an error (enforces Stata 18+ across the team) + disabled -- .dtas not registered; standard MD5 used without a batch call """ - return get_datasign(fname, file_arg='frameset_file') + global _dtas_stata_version_ok + fs_str = int_env['CONFIG']['SCons'].get('frameset_signing', 'auto') + + if _dtas_stata_version_ok is False: + if fs_str == 'auto': + import SCons.Util + return SCons.Util.hash_file_signature(fname) + # enabled: we already know Stata <18; raise immediately without another batch + raise Exception( + "frameset_file() requires Stata 18 or newer. " + "Set frameset_signing = auto in config_project.ini to fall back to MD5." + ) + + try: + sig = get_datasign(fname, file_arg='frameset_file') + _dtas_stata_version_ok = True + return sig + except Exception as e: + if 'STATACONS_REQUIRES_STATA18' in str(e): + _dtas_stata_version_ok = False + if fs_str == 'enabled': + raise Exception( + "frameset_file() requires Stata 18 or newer. " + "Set frameset_signing = auto in config_project.ini to fall back to MD5." + ) from e + # auto mode: warn once and fall back to MD5 + print("WARNING: Stata < 18 detected. .dtas files will use standard MD5 checksums.") + print(" Frameset-aware signing requires Stata 18 or newer.") + print(" Set frameset_signing = disabled in config_project.ini to suppress this warning.") + import SCons.Util + return SCons.Util.hash_file_signature(fname) + raise # Used to use packaging.version.parse from pkg_resources's packaging, but that's now deprecated. @@ -551,7 +596,8 @@ def init_env(env: Environment = None, tools: list = [], patch_scons_sig_fns: boo int_env = env config = configparser.ConfigParser() - config['SCons'] = {'success_batch_log_dir': '', 'use_custom_datasignature': 'Strict', 'stata_chdir': ''} + config['SCons'] = {'success_batch_log_dir': '', 'use_custom_datasignature': 'Strict', + 'stata_chdir': '', 'frameset_signing': 'auto'} strip_quote_keys = [('Programs', 'stata_exe'), ('SCons', 'success_batch_log_dir'), ('Project', 'cache_dir')] if GetOption("config_files") is not None: config = configuration(config_files=GetOption("config_files").split(':'), config=config, @@ -593,10 +639,12 @@ def init_env(env: Environment = None, tools: list = [], patch_scons_sig_fns: boo if not GetOption("clean") and patch_scons_sig_fns: m_str = config['SCons']['use_custom_datasignature'] + fs_str = config['SCons'].get('frameset_signing', 'auto') if m_str != "False": monkey_patch_scons() special_sig_fns[".dta"] = get_datasign - special_sig_fns[".dtas"] = get_dtas_sign + if fs_str != "disabled": + special_sig_fns[".dtas"] = get_dtas_sign if not GetOption('silent'): if m_str != "DataOnly" and m_str != "Datasignature" and m_str != "LabelsFormatsOnly": @@ -617,6 +665,10 @@ def init_env(env: Environment = None, tools: list = [], patch_scons_sig_fns: boo print(" not including metadata.") print("Edit use_custom_datasignature in config_project.ini to change.") print(" (other options are Strict, LabelsFormatsOnly, False)") + if fs_str == "disabled": + print("frameset_signing = disabled: .dtas files will use standard MD5 checksums.") + elif fs_str == "enabled": + print("frameset_signing = enabled: .dtas signing requires Stata 18 or newer.") elif not GetOption('silent'): print("Using default timestamp-dependent checksums of dataset,") print("Edit use_custom_datasignature in config_project.ini to change (Strict, DataOnly, LabelsFormatsOnly)") diff --git a/src/complete_datasignature.ado b/src/complete_datasignature.ado index bd97bfa..29e0e75 100644 --- a/src/complete_datasignature.ado +++ b/src/complete_datasignature.ado @@ -1,4 +1,4 @@ -*! version 3.1.0-alpha1 22 May 2026 statacons team +*! version 3.1.0-alpha2 22 May 2026 statacons team * Copyright 2023. This work is licensed under a CC BY 4.0 license. version 16.1 @@ -13,6 +13,10 @@ loc 0 : subinstr local 0 `")""' ")" syntax, [dta_file(string) frameset_file(string) fname(string) nometa fast labels_formats_only skip_char(string)] if "`frameset_file'" != "" { + if `c(stata_version)' < 18 { + di as error "STATACONS_REQUIRES_STATA18: frameset_file() requires Stata 18 or newer (detected Stata `=int(`c(stata_version)')')" + exit 198 + } // frameset (.dtas) path: sign each frame in the archive and concatenate. // In interactive mode, round-trip the user's frames via a temp .dtas so memory // is restored on exit. In batch mode, skip the round-trip for speed. @@ -142,7 +146,7 @@ end /*** -_version 3.1.0-alpha1_ +_version 3.1.0-alpha2_ complete_datasignature ====== diff --git a/src/complete_datasignature.md b/src/complete_datasignature.md index 60ecdb5..9c35b96 100644 --- a/src/complete_datasignature.md +++ b/src/complete_datasignature.md @@ -1,4 +1,4 @@ -_version 3.1.0-alpha1_ +_version 3.1.0-alpha2_ complete_datasignature ====== diff --git a/src/complete_datasignature.sthlp b/src/complete_datasignature.sthlp index da973f3..10912e5 100644 --- a/src/complete_datasignature.sthlp +++ b/src/complete_datasignature.sthlp @@ -1,7 +1,7 @@ {smcl} {p 4 4 2} -{it:version 3.1.0-alpha1} +{it:version 3.1.0-alpha2} {title:complete_datasignature} diff --git a/src/config_project.ini b/src/config_project.ini index 1568c36..b38e91f 100644 --- a/src/config_project.ini +++ b/src/config_project.ini @@ -15,6 +15,17 @@ # (variable formats, variable, value and dataset labels, notes, characteristics) use_custom_datasignature: Strict +#frameset_signing +# controls signing of .dtas (frameset) files, which require Stata 18 or newer +#auto (default): use frameset-aware signing on Stata 18+; fall back to standard +# MD5 on Stata < 18 (with a one-time warning). Suitable for most projects. +#disabled: always use standard MD5 for .dtas files; .dta signing unaffected. +# Use when all team members have Stata < 18, or to match what a Stata < 18 +# user would produce. Suppresses the Stata < 18 warning. +#enabled: always attempt frameset signing; hard error if Stata < 18. +# Use to enforce that all team members have Stata 18+. +#frameset_signing: auto + #dta_sig_mode # controls the "fast" mode of Stata's datasignature # Fast: faster, but not machine-independent diff --git a/src/statacons.ado b/src/statacons.ado index a76c85d..c895411 100644 --- a/src/statacons.ado +++ b/src/statacons.ado @@ -1,4 +1,4 @@ -*! version 3.1.0-alpha1 22 May 2026 statacons team +*! version 3.1.0-alpha2 22 May 2026 statacons team * Copyright 2025. This work is licensed under a CC BY 4.0 license. program statacons, rclass @@ -54,7 +54,7 @@ end /*** -_version 3.1.0-alpha1_ +_version 3.1.0-alpha2_ statacons ====== diff --git a/src/statacons.md b/src/statacons.md index f1809da..bfb3273 100644 --- a/src/statacons.md +++ b/src/statacons.md @@ -1,4 +1,4 @@ -_version 3.1.0-alpha1_ +_version 3.1.0-alpha2_ statacons ====== diff --git a/src/statacons.sthlp b/src/statacons.sthlp index ff57f34..44fd810 100644 --- a/src/statacons.sthlp +++ b/src/statacons.sthlp @@ -2,7 +2,7 @@ {p 4 4 2} -{it:version 3.1.0-alpha1} +{it:version 3.1.0-alpha2} {title:statacons} diff --git a/src/stataconsign.ado b/src/stataconsign.ado index 76888d9..1177d8a 100644 --- a/src/stataconsign.ado +++ b/src/stataconsign.ado @@ -1,4 +1,4 @@ -*! version 3.1.0-alpha1 22 May 2026 statacons team +*! version 3.1.0-alpha2 22 May 2026 statacons team * Copyright 2025. This work is licensed under a CC BY 4.0 license. * Anaconda env's script dir is added to path when you switch environments, but not added to PYTHONPATH/sys.path, @@ -18,7 +18,7 @@ end /*** -_version 3.1.0-alpha1_ +_version 3.1.0-alpha2_ stataconsign ====== diff --git a/src/stataconsign.md b/src/stataconsign.md index e17f7f3..7ff9386 100644 --- a/src/stataconsign.md +++ b/src/stataconsign.md @@ -1,4 +1,4 @@ -_version 3.1.0-alpha1_ +_version 3.1.0-alpha2_ stataconsign ====== diff --git a/src/stataconsign.sthlp b/src/stataconsign.sthlp index 5c784c8..ef37a74 100644 --- a/src/stataconsign.sthlp +++ b/src/stataconsign.sthlp @@ -1,7 +1,7 @@ {smcl} {p 4 4 2} -{it:version 3.1.0-alpha1} +{it:version 3.1.0-alpha2} {title:stataconsign} diff --git a/src/utils/config_local_template.ini b/src/utils/config_local_template.ini index 0738159..f899a54 100644 --- a/src/utils/config_local_template.ini +++ b/src/utils/config_local_template.ini @@ -50,6 +50,12 @@ [SCons] +# frameset_signing: controls .dtas signing (requires Stata 18+) +# auto (default): frameset-aware signing on Stata 18+; MD5 fallback on Stata < 18 (with warning) +# disabled: always MD5 for .dtas (use when team has Stata < 18, or to suppress the warning) +# enabled: hard error if Stata < 18 (enforces Stata 18+ across the team) +#frameset_signing: auto + # What to do with Stata batch-mode logs when successful (error logs always left) # empty for delete success_batch_log_dir: From 58078c45415b477d424665878272174496220467 Mon Sep 17 00:00:00 2001 From: rpguiteras Date: Fri, 22 May 2026 14:29:52 -0400 Subject: [PATCH 18/21] clarify assumed working directory and relative paths in test and example do-files Add consistent header comments to example and test .do scripts clarifying the assumed working directory (frames/examples, frames/tests, tests/, etc.) and showing how to run each script interactively or in batch (StataMP-64.exe -e). Mark legacy/interactive roundtrip tests as interactive-only and adjust run_all.do header and ordering (place clear all/do testlib.do after header). Also update the datasets refresh script comment to use paths relative to frames/datasets/. --- frames/datasets/_refresh_datasets.do | 7 ++++--- frames/examples/example-01-basic-frames.do | 4 ++++ frames/examples/example-02-frlink-frget.do | 5 ++++- frames/examples/example-03-simulation-frame-post.do | 4 ++++ frames/examples/example-04-frameset-save-use.do | 5 ++++- frames/tests/Stata17/test_stata17_guard.do | 1 + frames/tests/Stata17/test_stata17_nocapture.do | 1 + frames/tests/interactive_roundtrip_legacy.do | 6 ++++-- frames/tests/interactive_roundtrip_test.do | 6 ++++-- frames/tests/run_all.do | 13 ++++++++++--- frames/tests/smoke_dtas_blog.do | 5 ++++- frames/tests/smoke_dtas_errors.do | 5 ++++- frames/tests/smoke_dtas_legacy.do | 5 +++++ frames/tests/smoke_scons_dtas_blog.do | 5 ++++- frames/tests/smoke_scons_dtas_legacy.do | 5 +++++ tests/statacons_test.do | 4 ++++ 16 files changed, 66 insertions(+), 15 deletions(-) diff --git a/frames/datasets/_refresh_datasets.do b/frames/datasets/_refresh_datasets.do index 8a1a16a..e711294 100644 --- a/frames/datasets/_refresh_datasets.do +++ b/frames/datasets/_refresh_datasets.do @@ -1,8 +1,9 @@ // _refresh_datasets.do // Downloads the correct webuse example datasets and saves them locally. -// Run once from this directory to populate ../datasets/. - -cd "C:/Users/rpguiter/Work/StataFrames/documentation/applications/datasets" +// All paths are relative to the assumed working directory: frames/datasets/ +// Run from frames/datasets/ with: +// do _refresh_datasets.do (interactive) +// StataMP-64.exe -e do _refresh_datasets.do (batch) foreach ds in persons txcounty discharge1 discharge2 family hsng { webuse `ds', clear diff --git a/frames/examples/example-01-basic-frames.do b/frames/examples/example-01-basic-frames.do index 838b6e0..298008c 100644 --- a/frames/examples/example-01-basic-frames.do +++ b/frames/examples/example-01-basic-frames.do @@ -3,6 +3,10 @@ // frames dir, frame prefix, frame drop, frames reset // // Datasets required: none (uses sysuse auto, sysuse census) +// All paths are relative to the assumed working directory: frames/examples/ +// Run from frames/examples/ with: +// do example-01-basic-frames.do (interactive) +// StataMP-64.exe -e do example-01-basic-frames.do (batch) // ---------------------------------------------------------------- clear all diff --git a/frames/examples/example-02-frlink-frget.do b/frames/examples/example-02-frlink-frget.do index 09b115c..2baed5b 100644 --- a/frames/examples/example-02-frlink-frget.do +++ b/frames/examples/example-02-frlink-frget.do @@ -7,11 +7,14 @@ // discharge1: patientid, age, sex, billed, ... (1980 obs) // discharge2: patientid, age, sex, billed, ... (1980 obs, same structure) // family: pid, pid_m, pid_f, x1-x5 (639 obs) +// All paths are relative to the assumed working directory: frames/examples/ +// Run from frames/examples/ with: +// do example-02-frlink-frget.do (interactive) +// StataMP-64.exe -e do example-02-frlink-frget.do (batch) // ---------------------------------------------------------------- clear all -cd "C:/Users/rpguiter/Work/statacons/frames/examples" local datasets "../datasets" // ---- 1. m:1 linkage: persons linked to Texas counties ---- diff --git a/frames/examples/example-03-simulation-frame-post.do b/frames/examples/example-03-simulation-frame-post.do index ce46373..07b130b 100644 --- a/frames/examples/example-03-simulation-frame-post.do +++ b/frames/examples/example-03-simulation-frame-post.do @@ -6,6 +6,10 @@ // in a simple OLS regression actually covers the true value 95% of the time. // // Datasets required: none +// All paths are relative to the assumed working directory: frames/examples/ +// Run from frames/examples/ with: +// do example-03-simulation-frame-post.do (interactive) +// StataMP-64.exe -e do example-03-simulation-frame-post.do (batch) // ---------------------------------------------------------------- clear all diff --git a/frames/examples/example-04-frameset-save-use.do b/frames/examples/example-04-frameset-save-use.do index a686356..84b98a1 100644 --- a/frames/examples/example-04-frameset-save-use.do +++ b/frames/examples/example-04-frameset-save-use.do @@ -3,11 +3,14 @@ // // Replicates the frames save/use/modify examples from the help files. // Datasets required: ../datasets/hsng.dta (plus sysuse auto, sysuse census) +// All paths are relative to the assumed working directory: frames/examples/ +// Run from frames/examples/ with: +// do example-04-frameset-save-use.do (interactive) +// StataMP-64.exe -e do example-04-frameset-save-use.do (batch) // ---------------------------------------------------------------- clear all -cd "C:/Users/rpguiter/Work/statacons/frames/examples" local datasets "../datasets" // ---- 1. Build two frames ---- diff --git a/frames/tests/Stata17/test_stata17_guard.do b/frames/tests/Stata17/test_stata17_guard.do index caa3e82..83c7e6e 100644 --- a/frames/tests/Stata17/test_stata17_guard.do +++ b/frames/tests/Stata17/test_stata17_guard.do @@ -1,4 +1,5 @@ // Test: complete_datasignature frameset_file() should return rc=198 on Stata <18. +// All paths are relative to the assumed working directory: frames/tests/Stata17/ // Run from frames/tests/Stata17/ with: // Start-Process "C:\Program Files\Stata17\StataMP-64.exe" -ArgumentList "/e do test_stata17_guard.do" -Wait adopath ++ "../../../src" diff --git a/frames/tests/Stata17/test_stata17_nocapture.do b/frames/tests/Stata17/test_stata17_nocapture.do index 1419a5f..f2c8f4b 100644 --- a/frames/tests/Stata17/test_stata17_nocapture.do +++ b/frames/tests/Stata17/test_stata17_nocapture.do @@ -1,6 +1,7 @@ // Test: show raw error output from frameset_file() on Stata <18 (no capture). // The log should contain "STATACONS_REQUIRES_STATA18" -- this is the sentinel // string Python searches for in get_dtas_sign() to trigger the MD5 fallback. +// All paths are relative to the assumed working directory: frames/tests/Stata17/ // Run from frames/tests/Stata17/ with: // Start-Process "C:\Program Files\Stata17\StataMP-64.exe" -ArgumentList "/e do test_stata17_nocapture.do" -Wait adopath ++ "../../../src" diff --git a/frames/tests/interactive_roundtrip_legacy.do b/frames/tests/interactive_roundtrip_legacy.do index ec22621..d4e3853 100644 --- a/frames/tests/interactive_roundtrip_legacy.do +++ b/frames/tests/interactive_roundtrip_legacy.do @@ -2,8 +2,10 @@ // Legacy interactive round-trip checks migrated from // tests\interactive_roundtrip_test.do. // -// Run by opening Stata interactively, then doing this file from -// frames/tests. These checks cover small but still useful interactive +// MANUAL INTERACTIVE TEST -- must not be run in batch mode. +// All paths are relative to the assumed working directory: frames/tests/ +// Run from frames/tests/ with: +// do interactive_roundtrip_legacy.do (interactive only) These checks cover small but still useful interactive // restoration cases that are separate from the newer blog-style script. // ============================================================ diff --git a/frames/tests/interactive_roundtrip_test.do b/frames/tests/interactive_roundtrip_test.do index 9151d87..3ce80d2 100644 --- a/frames/tests/interactive_roundtrip_test.do +++ b/frames/tests/interactive_roundtrip_test.do @@ -1,6 +1,8 @@ // ============================================================ -// MANUAL INTERACTIVE TEST -- run by opening Stata interactively, -// then do this file from frames/tests. +// MANUAL INTERACTIVE TEST -- must not be run in batch mode. +// All paths are relative to the assumed working directory: frames/tests/ +// Run from frames/tests/ with: +// do interactive_roundtrip_test.do (interactive only) // // What passes: all _assert lines succeed and the final message is // displayed. This targets c(mode)=="interactive" state restoration. diff --git a/frames/tests/run_all.do b/frames/tests/run_all.do index 4727b05..ca80846 100644 --- a/frames/tests/run_all.do +++ b/frames/tests/run_all.do @@ -1,9 +1,16 @@ -clear all -do testlib.do - +// run_all.do // Batch entry point for the frames/tests smoke suite. +// All paths are relative to the assumed working directory: frames/tests/ +// Run from frames/tests/ with: +// do run_all.do (interactive) +// StataMP-64.exe -e do run_all.do (batch) +// // If this script stops before the final PASS line, the first failing // child do-file identifies which part of the .dtas test harness broke. + +clear all +do testlib.do + do smoke_dtas_legacy.do do smoke_dtas_blog.do do smoke_dtas_errors.do diff --git a/frames/tests/smoke_dtas_blog.do b/frames/tests/smoke_dtas_blog.do index f5320f2..2ec71b6 100644 --- a/frames/tests/smoke_dtas_blog.do +++ b/frames/tests/smoke_dtas_blog.do @@ -15,7 +15,10 @@ // content change, or it is treating irrelevant container changes as // meaningful data changes. // -// Run from frames/tests with: StataMP-64.exe -e do smoke_dtas_blog.do +// All paths are relative to the assumed working directory: frames/tests/ +// Run from frames/tests/ with: +// do smoke_dtas_blog.do (interactive) +// StataMP-64.exe -e do smoke_dtas_blog.do (batch) // ============================================================ clear all diff --git a/frames/tests/smoke_dtas_errors.do b/frames/tests/smoke_dtas_errors.do index 85ff060..313e228 100644 --- a/frames/tests/smoke_dtas_errors.do +++ b/frames/tests/smoke_dtas_errors.do @@ -11,7 +11,10 @@ // means complete_datasignature accepted a broken frameset that should // have been rejected instead of silently hashed. // -// Run from frames/tests with: StataMP-64.exe -e do smoke_dtas_errors.do +// All paths are relative to the assumed working directory: frames/tests/ +// Run from frames/tests/ with: +// do smoke_dtas_errors.do (interactive) +// StataMP-64.exe -e do smoke_dtas_errors.do (batch) // ============================================================ clear all diff --git a/frames/tests/smoke_dtas_legacy.do b/frames/tests/smoke_dtas_legacy.do index e6e3573..a1af8c5 100644 --- a/frames/tests/smoke_dtas_legacy.do +++ b/frames/tests/smoke_dtas_legacy.do @@ -4,6 +4,11 @@ // This keeps the older small-data checks in frames/tests so the // top-level tests tree no longer carries a separate branch-only // standalone .dtas script. +// +// All paths are relative to the assumed working directory: frames/tests/ +// Run from frames/tests/ with: +// do smoke_dtas_legacy.do (interactive) +// StataMP-64.exe -e do smoke_dtas_legacy.do (batch) // ============================================================ clear all diff --git a/frames/tests/smoke_scons_dtas_blog.do b/frames/tests/smoke_scons_dtas_blog.do index fc491ec..4570729 100644 --- a/frames/tests/smoke_scons_dtas_blog.do +++ b/frames/tests/smoke_scons_dtas_blog.do @@ -16,7 +16,10 @@ // - the final timestamp-comparison failure means an identical // rerun rebuilt at least one output when it should not have. // -// Run from frames/tests with: StataMP-64.exe -e do smoke_scons_dtas_blog.do +// All paths are relative to the assumed working directory: frames/tests/ +// Run from frames/tests/ with: +// do smoke_scons_dtas_blog.do (interactive) +// StataMP-64.exe -e do smoke_scons_dtas_blog.do (batch) // ============================================================ clear all diff --git a/frames/tests/smoke_scons_dtas_legacy.do b/frames/tests/smoke_scons_dtas_legacy.do index 4993606..7246eb0 100644 --- a/frames/tests/smoke_scons_dtas_legacy.do +++ b/frames/tests/smoke_scons_dtas_legacy.do @@ -5,6 +5,11 @@ // This preserves the original small producer -> .dtas -> consumer // pipeline in frames/tests, alongside the richer blog-style SCons // checks. +// +// All paths are relative to the assumed working directory: frames/tests/ +// Run from frames/tests/ with: +// do smoke_scons_dtas_legacy.do (interactive) +// StataMP-64.exe -e do smoke_scons_dtas_legacy.do (batch) // ============================================================ clear all diff --git a/tests/statacons_test.do b/tests/statacons_test.do index 93ef2ea..9827f16 100644 --- a/tests/statacons_test.do +++ b/tests/statacons_test.do @@ -1,4 +1,8 @@ * This tests statacons from inside Stata. Tests that require looking at the output are marked with MANUAL +* All paths are relative to the assumed working directory: tests/ +* Run from tests/ with: +* do statacons_test.do (interactive) +* StataMP-64.exe -e do statacons_test.do (batch) * To do: * - How to get stata_exe config? From 15da081c5b1ba166dcf60f4845f5ca998f5f870a Mon Sep 17 00:00:00 2001 From: rpguiteras Date: Tue, 26 May 2026 16:01:49 -0400 Subject: [PATCH 19/21] Restore active frame after datasignature call Save and restore the previously-active frame when complete_datasignature loads a frameset in interactive mode. Implemented by capturing `c(frame)` before saving frames and calling `frame change` after restoring from the temporary .dtas. Add a legacy interactive test to verify restoration when calling from a non-default frame, and update documentation to mention that the previously-active frame is restored. --- frames/tests/interactive_roundtrip_legacy.do | 17 +++++++++++++++++ src/complete_datasignature.ado | 4 +++- src/complete_datasignature.md | 2 +- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/frames/tests/interactive_roundtrip_legacy.do b/frames/tests/interactive_roundtrip_legacy.do index d4e3853..2ac9054 100644 --- a/frames/tests/interactive_roundtrip_legacy.do +++ b/frames/tests/interactive_roundtrip_legacy.do @@ -126,6 +126,23 @@ _assert "`sig_call1'" == "`sig_call2'", /// msg("Legacy test 4: signature changed across repeated calls") di as result "PASS: legacy interactive repeated-call stability" +// ============================================================ +// Test 5. Active frame is restored after call from non-default frame. +// ============================================================ +clear all +sysuse auto, clear +frame create alt +frame alt: sysuse census, clear +frame change alt + +local before_frame "`c(frame)'" + +complete_datasignature, frameset_file("outputs/_rt_target_legacy.dtas") + +_assert "`c(frame)'" == "`before_frame'", /// + msg("Legacy test 5: active frame was not restored") +di as result "PASS: legacy interactive non-default active frame restoration" + cap erase "outputs/_rt_target_legacy.dtas" di _newline as result "ALL legacy interactive round-trip tests passed" diff --git a/src/complete_datasignature.ado b/src/complete_datasignature.ado index 29e0e75..98a2458 100644 --- a/src/complete_datasignature.ado +++ b/src/complete_datasignature.ado @@ -26,6 +26,7 @@ if "`frameset_file'" != "" { loc tempdtas "`tempdtas_base'.dtas" if !`is_batch' { + loc saved_frame "`c(frame)'" qui frames save "`tempdtas'", frames(_all) replace emptyok loc need_restore = 1 } @@ -50,6 +51,7 @@ if "`frameset_file'" != "" { if `need_restore' { qui frames use "`tempdtas'", clear cap erase "`tempdtas'" + frame change `saved_frame' } if "`fname'" != "" { @@ -185,7 +187,7 @@ When __frameset_file__ is set, the program: 2. Loads the target __.dtas__ via __frames use, clear__. 3. For each frame in alphabetical order, computes a per-frame signature using the same metadata options (__nometa__, __fast__, __labels_formats_only__) plus __skip_char("frlink_*")__. 4. Assembles "frameA=sigA|frameB=sigB|...". -5. In interactive mode, restores the user's frames from the temporary __.dtas__. +5. In interactive mode, restores the user's frames from the temporary __.dtas__, including the previously-active frame. Example(s) ---------- diff --git a/src/complete_datasignature.md b/src/complete_datasignature.md index 9c35b96..c0675df 100644 --- a/src/complete_datasignature.md +++ b/src/complete_datasignature.md @@ -37,7 +37,7 @@ When __frameset_file__ is set, the program: 2. Loads the target __.dtas__ via __frames use, clear__. 3. For each frame in alphabetical order, computes a per-frame signature using the same metadata options (__nometa__, __fast__, __labels_formats_only__) plus __skip_char("frlink_*")__. 4. Assembles "frameA=sigA|frameB=sigB|...". -5. In interactive mode, restores the user's frames from the temporary __.dtas__. +5. In interactive mode, restores the user's frames from the temporary __.dtas__, including the previously-active frame. Example(s) ---------- From 683c8f59de80036dde4ed67ac383a10ce9274bd8 Mon Sep 17 00:00:00 2001 From: rpguiteras Date: Tue, 26 May 2026 16:04:50 -0400 Subject: [PATCH 20/21] update version number to 3.1.0-alpha3 --- pypkg/setup.cfg | 2 +- pypkg/src/pystatacons/__init__.py | 2 +- src/complete_datasignature.ado | 4 ++-- src/complete_datasignature.md | 2 +- src/statacons.ado | 4 ++-- src/statacons.md | 2 +- src/stataconsign.ado | 4 ++-- src/stataconsign.md | 2 +- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/pypkg/setup.cfg b/pypkg/setup.cfg index cc92e6e..0613b19 100644 --- a/pypkg/setup.cfg +++ b/pypkg/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pystatacons -version = 3.1.0-alpha2 +version = 3.1.0-alpha3 author = Statacons team author_email = Brian.Quistorff@bea.gov description = statacons Python package diff --git a/pypkg/src/pystatacons/__init__.py b/pypkg/src/pystatacons/__init__.py index c0f41d4..6f34023 100644 --- a/pypkg/src/pystatacons/__init__.py +++ b/pypkg/src/pystatacons/__init__.py @@ -32,7 +32,7 @@ """ -__version__ = "3.1.0-alpha2" +__version__ = "3.1.0-alpha3" __all__ = ['init_env', 'decider_str_lookup', 'special_sig_fns', 'stata_run_params_factory'] from .deciders import decider_str_lookup, dependency_newer_then_content_changed, \ changed_timestamp_then_dependency_newer_then_content_content diff --git a/src/complete_datasignature.ado b/src/complete_datasignature.ado index 98a2458..dbfef88 100644 --- a/src/complete_datasignature.ado +++ b/src/complete_datasignature.ado @@ -1,4 +1,4 @@ -*! version 3.1.0-alpha2 22 May 2026 statacons team +*! version 3.1.0-alpha3 26 May 2026 statacons team * Copyright 2023. This work is licensed under a CC BY 4.0 license. version 16.1 @@ -148,7 +148,7 @@ end /*** -_version 3.1.0-alpha2_ +_version 3.1.0-alpha3_ complete_datasignature ====== diff --git a/src/complete_datasignature.md b/src/complete_datasignature.md index c0675df..1b99866 100644 --- a/src/complete_datasignature.md +++ b/src/complete_datasignature.md @@ -1,4 +1,4 @@ -_version 3.1.0-alpha2_ +_version 3.1.0-alpha3_ complete_datasignature ====== diff --git a/src/statacons.ado b/src/statacons.ado index c895411..a204920 100644 --- a/src/statacons.ado +++ b/src/statacons.ado @@ -1,4 +1,4 @@ -*! version 3.1.0-alpha2 22 May 2026 statacons team +*! version 3.1.0-alpha3 26 May 2026 statacons team * Copyright 2025. This work is licensed under a CC BY 4.0 license. program statacons, rclass @@ -54,7 +54,7 @@ end /*** -_version 3.1.0-alpha2_ +_version 3.1.0-alpha3_ statacons ====== diff --git a/src/statacons.md b/src/statacons.md index bfb3273..22cd6f6 100644 --- a/src/statacons.md +++ b/src/statacons.md @@ -1,4 +1,4 @@ -_version 3.1.0-alpha2_ +_version 3.1.0-alpha3_ statacons ====== diff --git a/src/stataconsign.ado b/src/stataconsign.ado index 1177d8a..5ab63da 100644 --- a/src/stataconsign.ado +++ b/src/stataconsign.ado @@ -1,4 +1,4 @@ -*! version 3.1.0-alpha2 22 May 2026 statacons team +*! version 3.1.0-alpha3 26 May 2026 statacons team * Copyright 2025. This work is licensed under a CC BY 4.0 license. * Anaconda env's script dir is added to path when you switch environments, but not added to PYTHONPATH/sys.path, @@ -18,7 +18,7 @@ end /*** -_version 3.1.0-alpha2_ +_version 3.1.0-alpha3_ stataconsign ====== diff --git a/src/stataconsign.md b/src/stataconsign.md index 7ff9386..74dd2c7 100644 --- a/src/stataconsign.md +++ b/src/stataconsign.md @@ -1,4 +1,4 @@ -_version 3.1.0-alpha2_ +_version 3.1.0-alpha3_ stataconsign ====== From 633047cd69e073efdb9dc4f55f96fef30d29f18a Mon Sep 17 00:00:00 2001 From: rpguiteras Date: Wed, 27 May 2026 11:29:15 -0400 Subject: [PATCH 21/21] Add new frameset tests Introduce several new frames/tests: smoke_dtas_frame_order, smoke_dtas_volatile_chars, smoke_dtas_fralias, smoke_dtas_degenerate, smoke_stata17_fallback, and interactive_collision. Update run_all.do to include the new non-interactive smoke tests and expand README-tests.md with descriptions, expected behavior, and diagnostics for each new test (including notes about interactive vs batch usage). The new tests cover frame ordering, volatile dataset characteristics and skip_char(), fralias aliasing behavior, single-/empty-frameset edge cases, Stata 17 version-guard behavior, and an interactive name-collision restoration scenario. Logs and cleanup steps are included in the test files. --- frames/tests/README-tests.md | 118 ++++++++++++++++++++- frames/tests/interactive_collision.do | 102 ++++++++++++++++++ frames/tests/run_all.do | 6 ++ frames/tests/smoke_dtas_degenerate.do | 75 ++++++++++++++ frames/tests/smoke_dtas_fralias.do | 121 ++++++++++++++++++++++ frames/tests/smoke_dtas_frame_order.do | 109 +++++++++++++++++++ frames/tests/smoke_dtas_volatile_chars.do | 105 +++++++++++++++++++ frames/tests/smoke_stata17_fallback.do | 111 ++++++++++++++++++++ 8 files changed, 746 insertions(+), 1 deletion(-) create mode 100644 frames/tests/interactive_collision.do create mode 100644 frames/tests/smoke_dtas_degenerate.do create mode 100644 frames/tests/smoke_dtas_fralias.do create mode 100644 frames/tests/smoke_dtas_frame_order.do create mode 100644 frames/tests/smoke_dtas_volatile_chars.do create mode 100644 frames/tests/smoke_stata17_fallback.do diff --git a/frames/tests/README-tests.md b/frames/tests/README-tests.md index 94a3483..7359ad8 100644 --- a/frames/tests/README-tests.md +++ b/frames/tests/README-tests.md @@ -23,8 +23,14 @@ There are four main ideas: | `smoke_dtas_errors.do` | Checks that malformed `.dtas` files are rejected with errors. | This is the main "fail loudly on bad input" test. | | `smoke_scons_dtas_legacy.do` | Keeps the older small producer -> `.dtas` -> consumer SCons pipeline that originally lived under `tests\`. | This is the migrated legacy/simple end-to-end pipeline test. | | `smoke_scons_dtas_blog.do` | Checks that `statacons` can build `.dtas` targets in SCons and does not rebuild them unnecessarily on an identical rerun. | This is the main end-to-end pipeline test. | +| `smoke_dtas_frame_order.do` | Checks that the signature is identical regardless of the order in which frames were created or listed in `frames save`. | This pins the alphabetical-sort behavior that makes signatures stable across different workflows. | +| `smoke_dtas_volatile_chars.do` | Checks that volatile dataset characteristics (e.g. a timestamp written into `_dta[lastrun]`) change the signature by default, and that `skip_char()` suppresses them. | This covers a real-world reason signatures might change without any data change. | +| `smoke_dtas_fralias.do` | Checks that framesets with `fralias` alias variables sign stably when nothing changes, and that a change to the source frame is detected. | This documents the live-alias behavior of `fralias` and confirms signatures respond to it correctly. | +| `smoke_dtas_degenerate.do` | Checks edge cases: a frameset with only one frame, and an empty frameset. | This guards against simple corner cases that the main tests skip over. | +| `smoke_stata17_fallback.do` | Checks that `complete_datasignature` exits with code 198 and emits a specific sentinel string when run under Stata 17. | This is the Stata-side half of the version-guard. It requires a Stata 17 installation and skips cleanly if none is found. | | `interactive_roundtrip_test.do` | Manually checks that interactive Stata state is restored after signing a `.dtas` file. | This covers the special interactive-mode behavior that batch tests cannot fully check. | | `interactive_roundtrip_legacy.do` | Keeps the older interactive round-trip checks that originally lived under `tests\`. | This is the migrated legacy/simple interactive test file. | +| `interactive_collision.do` | Manually checks that interactive state is fully restored when the user's in-memory frame names collide with frames inside the target `.dtas`. | This is the hardest interactive edge case: the restoration logic must not corrupt the user's session even when names overlap. | | `testlib.do` | Defines small helper programs used by the other test files. | This is shared setup code, not a test by itself. | ## What each main test is looking for @@ -122,6 +128,104 @@ It keeps four smaller checks: Why keep it: the newer interactive file is more blog-oriented, while this one is still a useful compact set of session-restoration regressions. +### 5. `smoke_dtas_frame_order.do` + +This file checks that the signing logic is independent of the order in which frames appear. + +It asks three related questions: + +- If I create frame A first and then frame B, versus B first and then A, but save both with `frames(A B)`, do I get the same signature? +- If I save the same two frames in opposite order -- `frames(A B)` versus `frames(B A)` -- do I still get the same signature? +- Are the slots in the signature always labelled in alphabetical frame-name order regardless of creation or save order? + +**What success looks like:** all three signatures are identical, and the first slot in the aggregate signature starts with `A=` while the second starts with `B=`. + +**What failure looks like:** if the signatures differ, it means the signing code is sensitive to zip entry order inside the `.dtas` archive rather than sorting frame names before hashing. That would make signatures unstable across slightly different workflows that produce the same data. + +### 6. `smoke_dtas_volatile_chars.do` + +This file checks how `complete_datasignature` handles dataset characteristics that are expected to change on every run -- for example, a `_dta[lastrun]` characteristic that stores a timestamp. + +It builds two otherwise identical framesets, written two seconds apart, where the only difference is a `lastrun` characteristic set to the current date and time. + +It then asks: + +- Do those two framesets produce different signatures by default? (They should, because the timestamp is baked in.) +- If I pass `skip_char("lastrun")`, do the two signatures become identical? +- Does the glob form `skip_char("last*")` also stabilize the signatures? + +**What success looks like:** default signing is timestamp-sensitive; both `skip_char` forms produce matching signatures for the otherwise-identical data. + +**What failure looks like:** if the default signatures are the same despite different timestamps, the signing code is ignoring characteristics it should be including. If `skip_char` does not stabilize the signatures, the glob-matching logic is broken. + +### 7. `smoke_dtas_fralias.do` + +This file checks the interaction between `complete_datasignature` and `fralias` -- Stata 18's feature for creating live column aliases that point into another frame. + +It builds a frameset with a `persons` frame that has a `fralias` alias column pointing into a linked `txcounty` frame. + +It then asks: + +- Does a repeated save of the same fralias frameset produce an identical signature? (Stability check.) +- If I mutate one value in the `txcounty` source frame and save again, does the signature change? (Detection check.) +- Does mutating `txcounty` change only the `txcounty` slot, or does it also change the `persons` slot? + +**What success looks like:** signatures are stable across identical re-saves, and a mutation in the source frame is detected. As a diagnostic finding: `fralias` aliases are **live** -- they are materialized into the persons frame at `frames save` time, so mutating `txcounty` changes the `persons` slot too, not just the `txcounty` slot. The test records this finding explicitly. + +**What failure looks like:** if a mutation in `txcounty` does not change the overall signature, the signing code is missing changes that flow through alias links. + +### 8. `smoke_dtas_degenerate.do` + +This file checks two edge cases that the main signature tests skip because they focus on multi-frame workflows. + +**Subtest A: single-frame frameset.** A frameset with exactly one frame should produce a signature that starts with `framename=` and contains no `|` pipe separator. This confirms the format holds when there is nothing to concatenate. + +**Subtest B: empty frameset.** Stata refuses to create an empty `.dtas` file (it requires at least one frame), so this subtest is skipped gracefully on current Stata. It is left in the file as a placeholder in case behavior changes in a future Stata version. + +**What success looks like:** the single-frame signature is well-formed (no pipe, starts with the frame name), and the test exits with PASS whether or not Stata accepts an empty frameset. + +**What failure looks like:** a single-frame signature containing a spurious `|` or not starting with the frame name would indicate a bug in the signature assembly logic. + +### 9. `smoke_stata17_fallback.do` + +This file checks the Stata-side half of the version-guard mechanism for users running Stata 17 (which lacks `frames save`/`frames use`). + +It requires a real Stata 17 installation at `C:\Program Files\Stata17\`. If no such installation is found, the test prints a PASS note and exits cleanly. + +When Stata 17 is present, it runs two sub-tests by invoking Stata 17 via batch files: + +- **Sub-test A1: exit code.** `complete_datasignature` should exit Stata with return code 198 when called under Stata 17. This confirms the version guard is working. +- **Sub-test A2: sentinel string.** The same call (this time without `capture`) should write the string `STATACONS_REQUIRES_STATA18` to the log. This is the signal the Python layer watches for when deciding whether to fall back to MD5. + +**What success looks like:** Stata 17 returns exit code 0 from the guard-runner (which itself checks that `complete_datasignature` returned 198 and exits 0 on pass, 1 on fail), and `findstr` locates the sentinel string in the nocapture log. + +**What failure looks like:** if the exit code check fails, the version guard is not tripping correctly -- Stata 17 may be running without error, which would mean it is trying to sign framesets it cannot read. If the sentinel is missing, the Python fallback will not know to switch to MD5 and may report a corrupt signature instead. + +**Note on sub-tests B and C** (Python-side fallback): these are deferred. They would test that `get_dtas_sign` in `stata_utils.py` detects the sentinel and falls back to MD5 under `frameset_signing: auto`, and raises a hard error under `frameset_signing: enabled`. Those tests require running a full SCons pipeline with a Stata 17 binary. + +### 4b. `interactive_collision.do` + +This is a **manual** test for interactive Stata 18+. Run it by opening Stata interactively from `frames/tests/` and typing `do interactive_collision.do`. + +It covers the hardest interactive edge case: what happens when the user already has frames in memory with the **same names** as frames inside the `.dtas` file being signed? + +The test sets up a colliding in-memory state on purpose: + +- Loads a `collision_target.dtas` containing `X` (4 obs) and `Y` (4 obs). +- Then clears all frames and builds a user `X` frame with 7 observations and data values that differ from the target. +- Makes `X` the active frame. +- Calls `complete_datasignature, frameset_file("out/collision_target.dtas")`. + +After the call, it checks: + +- Is the active frame still named `X`? +- Does the `X` frame still have 7 observations (not 4)? +- Is `v[1]` still the pre-call value (not overwritten by the target data)? + +**What success looks like:** all three checks pass -- the user's colliding session is fully restored and the target `.dtas` was signed without corrupting in-memory state. + +**What failure looks like:** if any check fails, the round-trip logic has a bug in the collision case. The most common failure mode would be that the user's `X` frame is left with 4 observations (the target data) rather than the original 7. + ## The small do-files in `code/` These are tiny fixture scripts used by the SCons smoke test. @@ -158,8 +262,14 @@ If you want the shortest mental model, think of the test harness like this: - `smoke_dtas_errors.do` asks: **Do broken framesets fail clearly?** - `smoke_scons_dtas_legacy.do` asks: **Does the old simple `.dtas` pipeline still work and stay a no-op on rerun?** - `smoke_scons_dtas_blog.do` asks: **Does the build pipeline behave correctly?** +- `smoke_dtas_frame_order.do` asks: **Is the signature independent of frame creation and save order?** +- `smoke_dtas_volatile_chars.do` asks: **Does `skip_char()` correctly suppress volatile metadata?** +- `smoke_dtas_fralias.do` asks: **Are signatures stable for fralias framesets, and are live-alias changes detected?** +- `smoke_dtas_degenerate.do` asks: **Does signing handle a single frame (or no frames) gracefully?** +- `smoke_stata17_fallback.do` asks: **Does the version guard fire correctly under Stata 17?** - `interactive_roundtrip_test.do` asks: **Does this still behave correctly in a live interactive session?** - `interactive_roundtrip_legacy.do` asks: **Do the older interactive restoration checks still pass?** +- `interactive_collision.do` asks: **Is the user's session fully restored when their frame names collide with the target `.dtas`?** And `run_all.do` is simply the batch runner that ties the non-interactive smoke tests together. @@ -173,6 +283,12 @@ If you are new to this folder, a good order is: 4. `smoke_dtas_errors.do` -- the bad-input checks 5. `smoke_scons_dtas_legacy.do` -- the smallest SCons pipeline 6. `smoke_scons_dtas_blog.do` -- the richer end-to-end build check -7. `interactive_roundtrip_legacy.do` and `interactive_roundtrip_test.do` -- the interactive-only edge cases +7. `smoke_dtas_frame_order.do` -- frame ordering edge case +8. `smoke_dtas_volatile_chars.do` -- volatile characteristics edge case +9. `smoke_dtas_fralias.do` -- live alias edge case +10. `smoke_dtas_degenerate.do` -- single-frame and empty-frameset edge cases +11. `smoke_stata17_fallback.do` -- version guard edge case (Stata 17) +12. `interactive_roundtrip_legacy.do` and `interactive_roundtrip_test.do` -- interactive session restoration +13. `interactive_collision.do` -- the hardest interactive edge case (name collisions) That order goes from the simplest ideas to the trickiest ones. diff --git a/frames/tests/interactive_collision.do b/frames/tests/interactive_collision.do new file mode 100644 index 0000000..570ff5c --- /dev/null +++ b/frames/tests/interactive_collision.do @@ -0,0 +1,102 @@ +// ============================================================ +// MANUAL INTERACTIVE TEST -- must not be run in batch mode. +// All paths are relative to the assumed working directory: frames/tests/ +// Run from frames/tests/ with: +// do interactive_collision.do (interactive only) +// +// What this tests: +// When the user's current state has a frame whose name also appears +// in the target .dtas, the dual-mode round-trip +// (save-current -> sign-target -> restore-current) restores the +// user's state -- not the target's -- without name collision and +// without silent clobber. +// +// Design decision exercised: +// complete_datasignature.ado lines 28-32 (interactive round-trip via +// temp .dtas) and lines 51-55 (restore + explicit `frame change`). +// +// What passes: all _assert lines succeed and the final message is +// displayed. This targets c(mode)=="interactive" state preservation. +// ============================================================ + +clear all +do testlib.do +frames_tests_setup + +if "`c(mode)'" == "batch" { + di as error "This script must be run interactively." + exit 198 +} + +cap mkdir out + +// ============================================================ +// Step 1: Build the target .dtas with small frames X and Y. +// X has 4 obs with v = _n; Y has 4 obs with w = _n * 10. +// ============================================================ +frames reset +frame create X +frame X { + set obs 4 + gen v = _n +} +frame create Y +frame Y { + set obs 4 + gen w = _n * 10 +} +frames save "out/collision_target.dtas", frames(X Y) replace +di as txt "Built target .dtas: X (4 obs, v=1..4) and Y (4 obs, w=10..40)" + +// ============================================================ +// Step 2: Build colliding current state. +// User's frame X has 7 obs with v = _n * 100 -- very different from +// the target .dtas frame X. The active frame is X. +// ============================================================ +frames reset +frame create X +frame X { + set obs 7 + gen v = _n * 100 +} +frame change X + +// Capture pre-call invariants +qui count +local n_pre = r(N) +local fname_pre "`c(frame)'" +local v1_pre = v[1] +di as txt "Pre-call: frame=`fname_pre', N=`n_pre', v[1]=`v1_pre'" + +// ============================================================ +// Step 3: Sign the target while user's colliding X is active. +// The interactive round-trip should: +// (a) save all current frames to a temp .dtas, +// (b) load the target for signing, +// (c) restore the user's frames and re-activate X. +// ============================================================ +complete_datasignature, frameset_file("out/collision_target.dtas") +local sig "`r(signature)'" +_assert "`sig'" != "", msg("signature should not be empty after collision test") +di as txt "Signature after collision round-trip: `sig'" + +// ============================================================ +// Step 4: Verify the user's state is fully restored. +// ============================================================ +_assert "`c(frame)'" == "X", /// + msg("active frame name must be restored (got '`c(frame)'', expected 'X')") + +qui count +_assert r(N) == `n_pre', /// + msg("active frame obs count must be restored (got r(N)=`r(N)', expected `n_pre')") + +_assert v[1] == `v1_pre', /// + msg("active frame data values must match pre-call state (v[1]=`=v[1]', expected `v1_pre')") + +di as result "PASS: interactive frame-name collision restores correctly" + +// cleanup +cap erase "out/collision_target.dtas" +frames reset + +di _newline as result "PASS: interactive_collision" diff --git a/frames/tests/run_all.do b/frames/tests/run_all.do index ca80846..b1a6492 100644 --- a/frames/tests/run_all.do +++ b/frames/tests/run_all.do @@ -17,4 +17,10 @@ do smoke_dtas_errors.do do smoke_scons_dtas_legacy.do do smoke_scons_dtas_blog.do +do smoke_dtas_frame_order.do +do smoke_dtas_volatile_chars.do +do smoke_dtas_fralias.do +do smoke_dtas_degenerate.do +do smoke_stata17_fallback.do + di _newline as result "ALL frames/tests batch checks passed" diff --git a/frames/tests/smoke_dtas_degenerate.do b/frames/tests/smoke_dtas_degenerate.do new file mode 100644 index 0000000..547a59c --- /dev/null +++ b/frames/tests/smoke_dtas_degenerate.do @@ -0,0 +1,75 @@ +// ============================================================ +// Test: Degenerate .dtas cases. +// +// Subtest A (must pass): single-frame .dtas signature is well-formed: +// starts with '=' and contains no pipe. +// Subtest B (best effort): empty .dtas; Stata may refuse to create one. +// If it succeeds, the signature should be empty or very short. +// If Stata refuses (rc != 0), the subtest is skipped with a note. +// +// All paths are relative to the assumed working directory: frames/tests/ +// Run from frames/tests/ with: +// do smoke_dtas_degenerate.do (interactive) +// StataMP-64.exe -e do smoke_dtas_degenerate.do (batch) +// ============================================================ + +cap log close +log using logs/smoke_dtas_degenerate.log, replace text +clear all +do testlib.do +frames_tests_setup + +cap mkdir out + +// ============================================================ +// Subtest A: Single-frame .dtas +// Signature should start with 'lonely=' and have no pipe separator. +// ============================================================ +clear all +frame create lonely +frame lonely { + set obs 2 + gen id = _n +} +frames save "out/single.dtas", frames(lonely) replace +complete_datasignature, frameset_file("out/single.dtas") +local sig "`r(signature)'" +di as txt "single-frame sig = `sig'" + +_assert strpos("`sig'", "lonely=") == 1, /// + msg("single-frame sig should start with 'lonely='") +_assert strpos("`sig'", "|") == 0, /// + msg("single-frame sig should have no pipe separator") +di as result "PASS: single-frame .dtas signature is well-formed" + +// ============================================================ +// Subtest B: Empty .dtas (best effort) +// ============================================================ +frames reset +cap noi frames save "out/empty.dtas", emptyok replace +local rc_empty = _rc +if `rc_empty' == 0 { + di as txt "NOTE: Stata created empty .dtas (rc=0); signing it." + cap noi complete_datasignature, frameset_file("out/empty.dtas") + if _rc == 0 { + local sig_empty "`r(signature)'" + di as txt "NOTE: empty .dtas signature = '`sig_empty''" + _assert length("`sig_empty'") < 5, /// + msg("empty .dtas sig should be empty or very short (<5 chars)") + di as result "PASS: empty .dtas signed as expected" + } + else { + di as txt "NOTE: signing empty .dtas returned rc=`_rc'; skipping assertion." + } +} +else { + di as txt "NOTE: Stata refused empty .dtas (rc=`rc_empty'); skipping subtest B." +} + +// cleanup +cap erase "out/single.dtas" +cap erase "out/empty.dtas" +frames reset + +di "PASS: smoke_dtas_degenerate" +log close diff --git a/frames/tests/smoke_dtas_fralias.do b/frames/tests/smoke_dtas_fralias.do new file mode 100644 index 0000000..de82057 --- /dev/null +++ b/frames/tests/smoke_dtas_fralias.do @@ -0,0 +1,121 @@ +// ============================================================ +// Test: fralias frameset signature stability. +// +// Locks: +// (a) A frameset containing fralias columns signs identically +// across re-saves of identical content. +// (b) Mutating the fralias source frame changes the aggregate +// signature; diagnostic output records which frame slots change. +// +// Mirrors the fralias workflow from +// frames/examples/example-02-frlink-frget.do (Section 4). +// +// NOTE: if the first stability assertion (_assert sig1 == sig2) trips, +// that is a real finding -- stop and report rather than patching the test. +// It would mean fralias columns are introducing non-determinism into the +// signing path. +// +// All paths are relative to the assumed working directory: frames/tests/ +// Run from frames/tests/ with: +// do smoke_dtas_fralias.do (interactive) +// StataMP-64.exe -e do smoke_dtas_fralias.do (batch) +// ============================================================ + +cap log close +log using logs/smoke_dtas_fralias.log, replace text +clear all +do testlib.do +frames_tests_setup + +cap mkdir out + +// ============================================================ +// Build fralias1.dtas: persons frame with fralias to txcounty. +// ============================================================ +clear all +frame create persons +frame persons: use "../datasets/persons.dta", clear +frame create txcounty +frame txcounty: use "../datasets/txcounty.dta", clear +frame persons { + frlink m:1 countyid, frame(txcounty) + fralias add txcounty_median_income = median_income, from(txcounty) +} +frames save "out/fralias1.dtas", frames(persons txcounty) replace + +complete_datasignature, frameset_file("out/fralias1.dtas") +local sig1 "`r(signature)'" +di as txt "sig1 (fralias1) = `sig1'" + +// ============================================================ +// Stability check: re-save identical content to fralias2.dtas. +// After complete_datasignature in batch mode, persons and txcounty +// frames from fralias1 are in memory (no mutation). Re-save and +// re-sign; signatures must be identical. +// ============================================================ +sleep 1500 +frames save "out/fralias2.dtas", frames(persons txcounty) replace +complete_datasignature, frameset_file("out/fralias2.dtas") +local sig2 "`r(signature)'" +di as txt "sig2 (fralias2) = `sig2'" + +_assert "`sig1'" == "`sig2'", /// + msg("fralias frameset should sign stably across re-saves") +di as result "PASS: fralias frameset signs identically across re-saves" + +// ============================================================ +// Diagnostic: print per-frame slots so future inspection can +// see exactly what fralias content does to the persons slot. +// Frames are signed alphabetically (persons < txcounty). +// ============================================================ +local sig1_tokens : subinstr local sig1 "|" " ", all +tokenize `"`sig1_tokens'"' +local sig1_persons "`1'" +local sig1_txcounty "`2'" +di as txt " persons slot: `sig1_persons'" +di as txt " txcounty slot: `sig1_txcounty'" + +// ============================================================ +// Mutation sub-test: change one value of median_income in txcounty. +// Must change the overall signature (and the txcounty slot at minimum). +// Diagnostic output records which slots change, locking observed behavior. +// ============================================================ +frame txcounty: replace median_income = median_income + 1 in 1 +frames save "out/fralias3.dtas", frames(persons txcounty) replace +complete_datasignature, frameset_file("out/fralias3.dtas") +local sig3 "`r(signature)'" +di as txt "sig3 (fralias3, mutated txcounty) = `sig3'" + +_assert "`sig1'" != "`sig3'", /// + msg("mutating fralias source must change overall sig") +di as result "PASS: mutating fralias source changes aggregate signature" + +// Diagnostic: which slots changed? +local sig3_tokens : subinstr local sig3 "|" " ", all +tokenize `"`sig3_tokens'"' +local sig3_persons "`1'" +local sig3_txcounty "`2'" + +if "`sig1_persons'" != "`sig3_persons'" { + di as txt " NOTE: persons slot changed after mutating txcounty source" + di as txt " (fralias alias variable materialized live into persons frame)" +} +else { + di as txt " NOTE: persons slot UNCHANGED after mutating txcounty source" + di as txt " (fralias alias variable materialized at save time)" +} +if "`sig1_txcounty'" != "`sig3_txcounty'" { + di as txt " NOTE: txcounty slot changed after mutating txcounty source" +} +else { + di as txt " NOTE: txcounty slot UNCHANGED after mutating txcounty (unexpected)" +} + +// cleanup +cap erase "out/fralias1.dtas" +cap erase "out/fralias2.dtas" +cap erase "out/fralias3.dtas" +frames reset + +di "PASS: smoke_dtas_fralias" +log close diff --git a/frames/tests/smoke_dtas_frame_order.do b/frames/tests/smoke_dtas_frame_order.do new file mode 100644 index 0000000..cdf75c3 --- /dev/null +++ b/frames/tests/smoke_dtas_frame_order.do @@ -0,0 +1,109 @@ +// ============================================================ +// Test: Frame creation and save order do not affect .dtas signatures. +// +// Locks the invariant: complete_datasignature iterates frames via +// `list sort` (alphabetical), so the aggregate signature must be +// identical regardless of: +// (a) the order in which frames were created in memory, and +// (b) the order passed to `frames save`. +// See: complete_datasignature.ado line 44 (`foreach f in `: list sort fnames'`) +// +// All paths are relative to the assumed working directory: frames/tests/ +// Run from frames/tests/ with: +// do smoke_dtas_frame_order.do (interactive) +// StataMP-64.exe -e do smoke_dtas_frame_order.do (batch) +// ============================================================ + +cap log close +log using logs/smoke_dtas_frame_order.log, replace text +clear all +do testlib.do +frames_tests_setup + +cap mkdir out + +// ============================================================ +// Build order_AB: A created first, then B; saved frames(A B) +// ============================================================ +clear all +frame create A +frame A { + set obs 5 + gen x = _n +} +frame create B +frame B { + set obs 5 + gen y = _n * 2 +} +frames save "out/order_AB.dtas", frames(A B) replace +complete_datasignature, frameset_file("out/order_AB.dtas") +local sig_AB "`r(signature)'" +di as txt "sig_AB = `sig_AB'" + +// ============================================================ +// Build order_BA: B created first, then A; saved frames(A B) +// Alphabetical sort must produce the same signature. +// ============================================================ +frames reset +frame create B +frame B { + set obs 5 + gen y = _n * 2 +} +frame create A +frame A { + set obs 5 + gen x = _n +} +frames save "out/order_BA.dtas", frames(A B) replace +complete_datasignature, frameset_file("out/order_BA.dtas") +local sig_BA "`r(signature)'" +di as txt "sig_BA = `sig_BA'" + +_assert "`sig_AB'" == "`sig_BA'", msg("creation order must not affect sig") +di as result "PASS: creation order does not affect .dtas signature" + +// ============================================================ +// Build order_save_BA: A created first, B second; saved frames(B A). +// The ado sorts alphabetically regardless of the save-list order, +// so this signature must equal sig_AB. +// ============================================================ +frames reset +frame create A +frame A { + set obs 5 + gen x = _n +} +frame create B +frame B { + set obs 5 + gen y = _n * 2 +} +frames save "out/order_save_BA.dtas", frames(B A) replace +complete_datasignature, frameset_file("out/order_save_BA.dtas") +local sig_save_BA "`r(signature)'" +di as txt "sig_save_BA = `sig_save_BA'" + +_assert "`sig_save_BA'" == "`sig_AB'", msg("save order must not affect sig") +di as result "PASS: save order does not affect .dtas signature" + +// ============================================================ +// Confirm slot labels are alphabetically ordered (A= first, B= second) +// ============================================================ +local sig_tokens : subinstr local sig_AB "|" " ", all +tokenize `"`sig_tokens'"' +_assert substr("`1'", 1, 2) == "A=", /// + msg("slot 1 should start with A= (alphabetical sort)") +_assert substr("`2'", 1, 2) == "B=", /// + msg("slot 2 should start with B= (alphabetical sort)") +di as result "PASS: signature slots are in alphabetical frame-name order" + +// cleanup +cap erase "out/order_AB.dtas" +cap erase "out/order_BA.dtas" +cap erase "out/order_save_BA.dtas" +frames reset + +di "PASS: smoke_dtas_frame_order" +log close diff --git a/frames/tests/smoke_dtas_volatile_chars.do b/frames/tests/smoke_dtas_volatile_chars.do new file mode 100644 index 0000000..a227f5b --- /dev/null +++ b/frames/tests/smoke_dtas_volatile_chars.do @@ -0,0 +1,105 @@ +// ============================================================ +// Test: Volatile characteristics and skip_char() glob. +// +// Locks: +// (a) Default skip_char strips only frlink_*. A non-frlink_* characteristic +// carrying volatile data (e.g. a timestamp) WILL change the signature +// run-to-run -- this is documented default behavior. +// (b) The user can recover stability via skip_char() glob using strmatch. +// +// See: complete_datasignature.ado lines 39-40 (inner_skip) and 117-121 +// (strmatch matching). +// +// All paths are relative to the assumed working directory: frames/tests/ +// Run from frames/tests/ with: +// do smoke_dtas_volatile_chars.do (interactive) +// StataMP-64.exe -e do smoke_dtas_volatile_chars.do (batch) +// ============================================================ + +cap log close +log using logs/smoke_dtas_volatile_chars.log, replace text +clear all +do testlib.do +frames_tests_setup + +cap mkdir out + +// ============================================================ +// Build vol1.dtas with a timestamp characteristic on frame A. +// Use c(current_date) and c(current_time) so the value is +// actually different on each run (unlike $S_DATE/$S_TIME which +// are fixed at Stata startup). +// ============================================================ +clear all +frame create A +frame A { + set obs 3 + gen v = _n +} +local ts1 "`c(current_date)' `c(current_time)'" +frame A: char _dta[lastrun] "`ts1'" +frames save "out/vol1.dtas", frames(A) replace +complete_datasignature, frameset_file("out/vol1.dtas") +local sig1 "`r(signature)'" +di as txt "sig1 (vol1) = `sig1'" +di as txt " timestamp used: `ts1'" + +// ============================================================ +// Wait, reset, build vol2.dtas with a later timestamp. +// After sleep 1500 the second-resolution clock has advanced. +// ============================================================ +sleep 1500 +frames reset +frame create A +frame A { + set obs 3 + gen v = _n +} +local ts2 "`c(current_date)' `c(current_time)'" +frame A: char _dta[lastrun] "`ts2'" +frames save "out/vol2.dtas", frames(A) replace +complete_datasignature, frameset_file("out/vol2.dtas") +local sig2 "`r(signature)'" +di as txt "sig2 (vol2) = `sig2'" +di as txt " timestamp used: `ts2'" + +_assert "`sig1'" != "`sig2'", /// + msg("timestamp char should change sig by default (ts1='`ts1'' ts2='`ts2'')") +di as result "PASS: volatile timestamp char changes signature by default" + +// ============================================================ +// Re-sign both files with skip_char("lastrun") -- exact match. +// The signatures should now be identical (same data, skip the char). +// ============================================================ +complete_datasignature, frameset_file("out/vol1.dtas") skip_char("lastrun") +local sig1s "`r(signature)'" +complete_datasignature, frameset_file("out/vol2.dtas") skip_char("lastrun") +local sig2s "`r(signature)'" +di as txt "sig1s (skip lastrun) = `sig1s'" +di as txt "sig2s (skip lastrun) = `sig2s'" + +_assert "`sig1s'" == "`sig2s'", /// + msg("skip_char(lastrun) should stabilize over volatile char") +di as result "PASS: skip_char(lastrun) stabilizes signature across timestamp char" + +// ============================================================ +// Glob variant: skip_char("last*") -- confirms strmatch globbing. +// ============================================================ +complete_datasignature, frameset_file("out/vol1.dtas") skip_char("last*") +local sig1g "`r(signature)'" +complete_datasignature, frameset_file("out/vol2.dtas") skip_char("last*") +local sig2g "`r(signature)'" +di as txt "sig1g (skip last*) = `sig1g'" +di as txt "sig2g (skip last*) = `sig2g'" + +_assert "`sig1g'" == "`sig2g'", /// + msg("skip_char(last*) glob should stabilize signature") +di as result "PASS: skip_char glob pattern stabilizes signature" + +// cleanup +cap erase "out/vol1.dtas" +cap erase "out/vol2.dtas" +frames reset + +di "PASS: smoke_dtas_volatile_chars" +log close diff --git a/frames/tests/smoke_stata17_fallback.do b/frames/tests/smoke_stata17_fallback.do new file mode 100644 index 0000000..4cd156f --- /dev/null +++ b/frames/tests/smoke_stata17_fallback.do @@ -0,0 +1,111 @@ +// ============================================================ +// Test: Stata <18 fallback path. +// +// Sub-test A (Stata-side, implemented here): +// Calling complete_datasignature, frameset_file() under Stata 17 +// exits with code 198 AND emits sentinel STATACONS_REQUIRES_STATA18. +// Uses the existing Stata17/ harness (test_stata17_guard.do and +// test_stata17_nocapture.do). +// +// Sub-tests B and C (Python-side, deferred -- TODO): +// TODO B: get_dtas_sign under frameset_signing:auto detects the +// sentinel and falls back to hash_file_signature (MD5), +// emitting a one-time warning. +// TODO C: get_dtas_sign under frameset_signing:enabled raises a +// hard error. +// +// Sub-test A requires Stata 17 at C:\Program Files\Stata17\StataMP-64.exe. +// If Stata 17 is absent the test skips gracefully. +// +// The two Stata17/ helper scripts (test_stata17_guard.do, +// test_stata17_nocapture.do) are invoked from their own directory via +// a small batch file placed in out/ so that relative paths inside them +// resolve correctly. +// +// All paths are relative to the assumed working directory: frames/tests/ +// Run from frames/tests/ with: +// do smoke_stata17_fallback.do (interactive) +// StataMP-64.exe -e do smoke_stata17_fallback.do (batch) +// ============================================================ + +cap log close +log using logs/smoke_stata17_fallback.log, replace text +clear all +do testlib.do +frames_tests_setup + +cap mkdir out + +// ============================================================ +// Guard: skip if Stata 17 is not installed. +// ============================================================ +cap confirm file "C:\Program Files\Stata17\StataMP-64.exe" +if _rc != 0 { + di as txt "NOTE: Stata 17 not found at C:\Program Files\Stata17\; skipping." + di "PASS: smoke_stata17_fallback (skipped -- Stata 17 not installed)" + log close + exit +} + +// ============================================================ +// Write helper batch files into out/. +// Each batch uses %~dp0 (the batch file's own directory) so that +// pushd resolves the relative Stata17\ sibling directory correctly +// from any working directory. +// ============================================================ + +// Guard-runner: runs test_stata17_guard.do; exits 0/1 based on +// whether complete_datasignature returned 198 as expected. +file open _bfh using "out/_stata17_guard_runner.bat", write text replace +file write _bfh "@echo off" _n +file write _bfh `"pushd "%~dp0..\Stata17""' _n +file write _bfh `""C:\Program Files\Stata17\StataMP-64.exe" -e do test_stata17_guard.do"' _n +file write _bfh "set EXITCODE=%ERRORLEVEL%" _n +file write _bfh "popd" _n +file write _bfh "exit /b %EXITCODE%" _n +file close _bfh + +// Nocapture-runner: runs test_stata17_nocapture.do (expected to fail +// with Stata exit 198); always returns 0 so _rc is not checked here. +// The log is inspected separately for the sentinel string. +file open _bfh using "out/_stata17_nocap_runner.bat", write text replace +file write _bfh "@echo off" _n +file write _bfh `"pushd "%~dp0..\Stata17""' _n +file write _bfh `""C:\Program Files\Stata17\StataMP-64.exe" -e do test_stata17_nocapture.do"' _n +file write _bfh "popd" _n +file write _bfh "exit /b 0" _n +file close _bfh + +// ============================================================ +// Sub-test A, part 1: version guard exits 198. +// test_stata17_guard.do uses `capture`, checks _rc==198, and +// exits Stata with code 0 (pass) or 1 (fail). +// ============================================================ +! "out\_stata17_guard_runner.bat" +_assert _rc == 0, /// + msg("Stata17 guard test exited non-zero: rc=198 version guard not working (shell rc=`_rc')") +di as result "PASS: Stata 17 exits 198 on frameset_file() call" + +// ============================================================ +// Sub-test A, part 2: sentinel STATACONS_REQUIRES_STATA18 is emitted. +// test_stata17_nocapture.do runs without capture so the error message +// appears in its log. findstr returns 0 if the string is found. +// ============================================================ +! "out\_stata17_nocap_runner.bat" +// (expected non-zero from Stata -- ignored by the nocap runner itself) + +! cmd /c findstr /C:"STATACONS_REQUIRES_STATA18" Stata17\test_stata17_nocapture.log +_assert _rc == 0, /// + msg("sentinel STATACONS_REQUIRES_STATA18 not found in Stata17 nocapture log") +di as result "PASS: sentinel STATACONS_REQUIRES_STATA18 emitted by Stata 17" + +di as txt _newline "NOTE: Sub-tests B and C (Python-side fallback) are deferred." +di as txt " TODO B: get_dtas_sign under frameset_signing:auto detects sentinel, falls back to MD5" +di as txt " TODO C: get_dtas_sign under frameset_signing:enabled raises hard error" + +// cleanup batch runners (logs kept for inspection) +cap erase "out\_stata17_guard_runner.bat" +cap erase "out\_stata17_nocap_runner.bat" + +di "PASS: smoke_stata17_fallback" +log close