* test_projectbuilder.do -- test battery for projectbuilder v2.0.1
* Run from the package folder, or from any scratch directory by naming the
* package folder:
* stata-mp -b do test_projectbuilder.do
* stata-mp -b do test_projectbuilder.do "/path/to/projectbuilder-stata-public"
* All paths live in globals set here; synthetic data only (no committed
* .dta/.log). Judge success by the absence of r(NNN); and failed asserts.
* ---- where the package lives ------------------------------------------
* Read the caller's overrides BEFORE -clear all-, which drops globals.
* Resolution order, first hit wins:
* 1. an argument passed to this do-file
* 2. a global $pkgroot the caller set beforehand
* 3. the current directory, if projectbuilder.ado sits in it
* 4. -findfile-, which searches the ado-path
local argroot `"`1'"'
local preroot `"$pkgroot"'
clear all
set more off
version 16.0
set seed 20260707
global pkgroot `"`argroot'"'
if `"$pkgroot"' == "" global pkgroot `"`preroot'"'
if `"$pkgroot"' == "" {
capture confirm file `"`c(pwd)'/projectbuilder.ado"'
if !_rc global pkgroot `"`c(pwd)'"'
}
if `"$pkgroot"' == "" {
capture findfile projectbuilder.ado
if !_rc {
local fp `"`r(fn)'"'
local s = max(strrpos(`"`fp'"', "/"), strrpos(`"`fp'"', "\"))
if `s' > 1 global pkgroot = substr(`"`fp'"', 1, `s' - 1)
}
}
if `"$pkgroot"' == "" {
di as err "test_projectbuilder: cannot locate projectbuilder.ado."
di as err `" Pass the package folder: do test_projectbuilder.do "/path/to/pkg""'
exit 601
}
confirm file "$pkgroot/projectbuilder.ado"
di as text `"test_projectbuilder: pkgroot = $pkgroot"'
* Prepend so this source copy wins over any older installed copy in PLUS.
adopath ++ "$pkgroot"
* ---- fresh scratch tree for this run ----------------------------------
tempfile tbase
global work "`tbase'_pbtest"
mkdir "$work"
* helper: load a text file one line per obs into v1 (line-oriented read)
capture program drop loadlines
program define loadlines
import delimited using `0', clear delimiters("`=char(1)'", asstring) ///
varnames(nonames) bindquote(nobind) encoding("utf-8")
end
* helper: write one line to a file, turning ~B into a literal backtick, so
* this do-file can emit an ado-file that itself uses local macros.
capture program drop wline
program define wline
gettoken fh 0 : 0
file write `fh' (subinstr(`0', "~B", char(96), .)) _n
end
di as text "{hline 70}"
di as text "TEST a1: Method B -- scaffold only, every folder and file exists"
di as text "{hline 70}"
projectbuilder Demo, path("$work")
assert `"`r(project)'"' == "Demo"
assert `"`r(path)'"' == `"$work/Demo"'
assert r(nraw) == 0
assert r(nconverted) == 0
assert r(rebuilt) == 0
foreach d in 01_raw 01_raw/_archive 01_raw/_converted ///
02_cleaned 02_cleaned/_archive ///
03_output 03_output/_archive ///
_code _code/_archive ///
_documentation _documentation/_archive _documentation/website ///
_archive {
confirm file "$work/Demo/`d'/."
}
foreach f in 000_control.do 100_data_download.do 200_data_management.do ///
300_labels.do 400_data_profiler.do 500_aggregation.do ///
600_analysis.do {
confirm file "$work/Demo/_code/`f'"
}
foreach f in index.do _runall.do Readme.md _project_meta.txt {
confirm file "$work/Demo/_documentation/`f'"
}
confirm file "$work/Demo/_documentation/website/index.html"
* 01_raw must be empty (no files; subfolders don't count)
local rl : dir "$work/Demo/01_raw" files "*"
assert `: word count `rl'' == 0
di as text "{hline 70}"
di as text "TEST a2: edit a code file, drop data, rebuild -> convert/combine/docs"
di as text "{hline 70}"
* sentinel line appended to a code file the 'user' has edited
tempname sfh
file open `sfh' using "$work/Demo/_code/000_control.do", write text append
file write `sfh' _n "* SENTINEL-KEEP-ME-4711" _n
file close `sfh'
* two synthetic CSVs dropped into 01_raw by hand
sysuse auto, clear
quietly export delimited using "$work/Demo/01_raw/auto_part1.csv", replace
keep make price mpg
quietly export delimited using "$work/Demo/01_raw/auto_part2.csv", replace
projectbuilder Demo, path("$work") rebuild
assert r(nraw) == 2
assert r(rebuilt) == 1
confirm file "$work/Demo/_documentation/website/index.html"
* the rebuilt documentation mentions both raw files
loadlines "$work/Demo/_documentation/website/index.html"
quietly count if strpos(v1, "auto_part1.csv")
assert r(N) >= 1
quietly count if strpos(v1, "auto_part2.csv")
assert r(N) >= 1
* rebuild WITHOUT replace must NOT overwrite the edited code file
loadlines "$work/Demo/_code/000_control.do"
quietly count if strpos(v1, "SENTINEL-KEEP-ME-4711")
assert r(N) == 1
di as text "{hline 70}"
di as text "TEST a3: rebuild WITH replace overwrites the code file"
di as text "{hline 70}"
projectbuilder Demo, path("$work") rebuild replace
loadlines "$work/Demo/_code/000_control.do"
quietly count if strpos(v1, "SENTINEL-KEEP-ME-4711")
assert r(N) == 0
di as text "{hline 70}"
di as text "TEST b: Method A -- data() copies a folder of files into 01_raw/"
di as text "{hline 70}"
mkdir "$work/_src2"
sysuse auto, clear
quietly export delimited using "$work/_src2/src_a.csv", replace
keep make foreign
quietly export delimited using "$work/_src2/src_b.csv", replace
projectbuilder Demo2, path("$work") data("$work/_src2") ///
description("two synthetic CSVs")
assert r(nraw) == 2
confirm file "$work/Demo2/01_raw/src_a.csv"
confirm file "$work/Demo2/01_raw/src_b.csv"
di as text "{hline 70}"
di as text "TEST c: clobber guard -- rerun without rebuild -> _rc==602"
di as text "{hline 70}"
capture noisily projectbuilder Demo, path("$work")
assert _rc == 602
confirm file "$work/Demo/_code/000_control.do"
di as text "{hline 70}"
di as text "TEST d: leaf/option validation -> _rc==198"
di as text "{hline 70}"
capture noisily projectbuilder "../evil", path("$work")
assert _rc == 198
capture noisily projectbuilder EvilPF, path("$work") publicfacing(maybe)
assert _rc == 198
di as text "{hline 70}"
di as text "TEST e: Source/Subsource nesting"
di as text "{hline 70}"
projectbuilder Agency/Extract, path("$work")
assert `"`r(project)'"' == "Agency_Extract"
assert `"`r(path)'"' == `"$work/Agency/Extract"'
confirm file "$work/Agency/Extract/_code/000_control.do"
confirm file "$work/Agency/Extract/_documentation/website/index.html"
di as text "{hline 70}"
di as text "TEST f: a stray extra token is rejected -> _rc==198"
di as text "{hline 70}"
capture noisily projectbuilder E1 E2, path("$work")
assert _rc == 198
capture confirm file "$work/E1 E2/."
assert _rc != 0
* a quoted name containing a space is still a single, legal name
projectbuilder "Two Words", path("$work")
assert `"`r(project)'"' == "Two Words"
confirm file "$work/Two Words/_code/000_control.do"
di as text "{hline 70}"
di as text "TEST g: a bare rebuild PRESERVES the recorded metadata"
di as text "{hline 70}"
projectbuilder Meta, path("$work") description("desc one") topic("t") ///
publicfacing(yes) timeline("annual") othernotes("from the county") ///
outcomes(price mpg) over(foreign)
confirm file "$work/Meta/_documentation/_project_meta.txt"
copy "$work/Meta/_documentation/website/index.html" "$work/meta_index_before.html"
copy "$work/Meta/_documentation/Readme.md" "$work/meta_readme_before.md"
projectbuilder Meta, path("$work") rebuild
assert r(rebuilt) == 1
* the placeholders must NOT have replaced the recorded values
loadlines "$work/Meta/_documentation/website/index.html"
quietly count if strpos(v1, "
desc one
")
assert r(N) == 1
quietly count if strpos(v1, "t | ")
assert r(N) == 1
quietly count if strpos(v1, "annual | ")
assert r(N) == 1
quietly count if strpos(v1, "from the county | ")
assert r(N) == 1
quietly count if strpos(v1, "add a one-line description")
assert r(N) == 0
quietly count if strpos(v1, "(not recorded)")
assert r(N) == 0
loadlines "$work/Meta/_documentation/Readme.md"
quietly count if strpos(v1, "| Topic | t |")
assert r(N) == 1
quietly count if strpos(v1, "(not recorded)")
assert r(N) == 0
* line-by-line diff: only the build-time footer may differ
loadlines "$work/meta_index_before.html"
gen long _ln = _n
tempfile before_html
quietly save `before_html'
loadlines "$work/Meta/_documentation/website/index.html"
gen long _ln = _n
rename v1 v1_after
quietly merge 1:1 _ln using `before_html'
assert _merge == 3
quietly count if v1_after != v1 & !strpos(v1, "Built ")
assert r(N) == 0
loadlines "$work/meta_readme_before.md"
gen long _ln = _n
tempfile before_md
quietly save `before_md'
loadlines "$work/Meta/_documentation/Readme.md"
gen long _ln = _n
rename v1 v1_after
quietly merge 1:1 _ln using `before_md'
assert _merge == 3
quietly count if v1_after != v1 & !strpos(v1, "Built ")
assert r(N) == 0
* -rebuild replace- keeps the recorded outcomes()/over() in the profiler
projectbuilder Meta, path("$work") rebuild replace
loadlines "$work/Meta/_code/400_data_profiler.do"
quietly count if strpos(v1, `"local outcomes "price mpg""')
assert r(N) == 1
quietly count if strpos(v1, `"local over "foreign""')
assert r(N) == 1
* an option given on the current call still wins over the recorded value
projectbuilder Meta, path("$work") rebuild topic("procurement")
loadlines "$work/Meta/_documentation/website/index.html"
quietly count if strpos(v1, "procurement | ")
assert r(N) == 1
quietly count if strpos(v1, "desc one
")
assert r(N) == 1
di as text "{hline 70}"
di as text "TEST h: the generated control file pins the package's version floor"
di as text "{hline 70}"
loadlines "$work/Demo2/_code/000_control.do"
quietly count if strpos(v1, "version 16.0")
assert r(N) == 1
di as text "{hline 70}"
di as text "TEST i: each output gets the escaping ITS format needs"
di as text "{hline 70}"
projectbuilder Esc, path("$work") description("R&D bold") ///
topic("a&b ") othernotes("left|right")
* index.html is HTML, so &, < and > all become entities
loadlines "$work/Esc/_documentation/website/index.html"
quietly count if strpos(v1, "a&b <tag> | ")
assert r(N) == 1
quietly count if strpos(v1, "R&D <b>bold</b>
")
assert r(N) == 1
quietly count if strpos(v1, "a&b | ")
assert r(N) == 0
* Readme.md is Markdown, so it gets ONLY what Markdown misreads: | would open
* an extra table column and < would open a raw HTML tag. A bare & is
* ordinary text and must survive as itself -- this file is read raw as often
* as it is rendered, and "R&D" in an editor is simply wrong.
loadlines "$work/Esc/_documentation/Readme.md"
quietly count if strpos(v1, "| Topic | a&b <tag> |")
assert r(N) == 1
quietly count if strpos(v1, "| Other notes | left\|right |")
assert r(N) == 1
quietly count if strpos(v1, "&")
assert r(N) == 0 // no HTML entity for & anywhere in the .md
quietly count if strpos(v1, "R&D <b>bold</b>")
assert r(N) == 1
di as text "{hline 70}"
di as text "TEST j: a seeded 01_raw/_converted/ drives the combine branch"
di as text "{hline 70}"
* convertanything and combineall are optional and usually absent, so the
* shipped battery never reached the code path that fires when there IS
* something converted to combine. Seed 01_raw/_converted/ by hand to reach
* it: r(nconverted) is non-zero for the first time here.
projectbuilder Seeded, path("$work") description("seeded converted fixture")
sysuse auto, clear
quietly export delimited using "$work/Seeded/01_raw/auto_raw.csv", replace
keep make price mpg foreign
quietly save "$work/Seeded/01_raw/_converted/part1.dta", replace
quietly save "$work/Seeded/01_raw/_converted/part2.dta", replace
projectbuilder Seeded, path("$work") rebuild
assert r(nraw) == 1
assert r(nconverted) >= 2
loadlines "$work/Seeded/_documentation/website/index.html"
quietly count if strpos(v1, "part1.dta")
assert r(N) >= 1
quietly count if strpos(v1, "part2.dta")
assert r(N) >= 1
* Now put a minimal stand-in for -combineall- on the ado-path, so the call
* projectbuilder makes is actually executed and the analytic file it names
* is actually produced. The stand-in accepts the same option signature.
mkdir "$work/_stubado"
tempname cfh
file open `cfh' using "$work/_stubado/combineall.ado", write text replace
wline `cfh' `"*! test stand-in for combineall (test_projectbuilder.do)"'
wline `cfh' `"program define combineall"'
wline `cfh' `" version 16"'
wline `cfh' `" syntax using/ [, CMethod(string) DIRectory(string) FILEtype(string) REPLACE ]"'
wline `cfh' `" local fl : dir "~Bdirectory'" files "*.~Bfiletype'""'
wline `cfh' `" local k = 0"'
wline `cfh' `" foreach f of local fl {"'
wline `cfh' `" local ++k"'
wline `cfh' `" if ~Bk' == 1 use "~Bdirectory'/~Bf'", clear"'
wline `cfh' `" else append using "~Bdirectory'/~Bf'", force"'
wline `cfh' `" }"'
wline `cfh' `" if ~Bk' == 0 {"'
wline `cfh' `" di as err "combineall stand-in: nothing to combine""'
wline `cfh' `" exit 601"'
wline `cfh' `" }"'
wline `cfh' `" save "~Busing'", replace"'
wline `cfh' `"end"'
file close `cfh'
adopath ++ "$work/_stubado"
capture program drop combineall
projectbuilder Seeded, path("$work") rebuild
assert r(nconverted) >= 2
confirm file "$work/Seeded/02_cleaned/Seeded_analytic.dta"
use "$work/Seeded/02_cleaned/Seeded_analytic.dta", clear
assert _N >= 148 // 74 observations from each seeded .dta
capture program drop combineall
adopath - "$work/_stubado"
di as text "{hline 70}"
di as text "TEST k: the generated 000_control.do runs; its globals are real dirs"
di as text "{hline 70}"
* stash test state: the control file runs -clear all-, which drops globals,
* programs, and adopath additions.
local W "$work"
local PK "$pkgroot"
do "$work/Demo2/_code/000_control.do"
assert `"$root"' == `"`W'/Demo2"' // NB: $root is stamped absolute
assert `"$raw"' == `"`W'/Demo2/01_raw"'
assert `"$converted"' == `"`W'/Demo2/01_raw/_converted"'
assert `"$cleaned"' == `"`W'/Demo2/02_cleaned"'
assert `"$output"' == `"`W'/Demo2/03_output"'
assert `"$code"' == `"`W'/Demo2/_code"'
assert `"$docs"' == `"`W'/Demo2/_documentation"'
foreach g in root raw converted cleaned output code docs {
confirm file "${`g'}/."
}
*=======================================================================*
* v2.0.1 REGRESSION TESTS *
* One test per defect fixed in 2.0.1. Each says what used to happen. *
*=======================================================================*
* TEST k ran the generated control file, which begins with -clear all-.
* That drops the globals, the helper programs, and the adopath addition made
* at the top of this file. Locals survive it, so put everything back from
* the copies TEST k stashed.
global work `"`W'"'
global pkgroot `"`PK'"'
adopath ++ "$pkgroot"
capture program drop loadlines
program define loadlines
import delimited using `0', clear delimiters("`=char(1)'", asstring) ///
varnames(nonames) bindquote(nobind) encoding("utf-8")
end
confirm file "$work/."
di as text `"test_projectbuilder: resumed with work = $work"'
di as text "{hline 70}"
di as text "TEST l: the auto-convert pass leaves the data in memory alone"
di as text "{hline 70}"
* Used to: -convertanything, clear- and -combineall- ran in the caller's
* frame, so scaffolding a project threw away the user's dataset silently.
projectbuilder MemKeep, path("$work") description("memory guard")
clear
set obs 3
gen str9 fixture_only = "fixture"
quietly export delimited using "$work/MemKeep/01_raw/fixture.csv", replace
sysuse auto, clear
local n_before = _N
local k_before = c(k)
quietly projectbuilder MemKeep, path("$work") rebuild
confirm variable make // the user's data is still here
assert _N == `n_before'
assert c(k) == `k_before'
capture confirm variable fixture_only // and the fixture did NOT land in memory
assert _rc != 0
* noautoconvert leaves memory alone too (it never runs the pass at all)
sysuse auto, clear
quietly projectbuilder MemKeep, path("$work") rebuild noautoconvert
confirm variable make
assert _N == `n_before'
di as text "{hline 70}"
di as text "TEST m: a backtick is refused before anything is created"
di as text "{hline 70}"
* Used to: the run got as far as building the folder tree, then died with a
* bare "invalid syntax". The half-built folder then blocked the corrected
* re-run with 602, so the user could not simply fix the typo and retry.
capture noisily projectbuilder Tick, path("$work") description("don`t do this")
assert _rc == 198
capture confirm file "$work/Tick/."
assert _rc != 0 // no folder was left behind
capture confirm file "$work/Tick/_code/000_control.do"
assert _rc != 0
* so the corrected call succeeds instead of hitting the clobber guard
projectbuilder Tick, path("$work") description("dont do this")
assert r(rebuilt) == 0
confirm file "$work/Tick/_code/000_control.do"
* every free-text option is checked, not just description()
foreach o in topic timeline othernotes url {
capture projectbuilder TickB, path("$work") `o'("bad`=char(96)'value")
assert _rc == 198
}
capture confirm file "$work/TickB/."
assert _rc != 0
di as text "{hline 70}"
di as text "TEST n: a tilde survives into the generated files"
di as text "{hline 70}"
* Used to: pb_wl substituted its own ~D/~B markers on the finished line, so
* url("https://example.edu/~Dave/data.csv") was written out as
* ".../$ave/data.csv" -- silently, and only in the do-files, so the docs and
* the code disagreed.
projectbuilder Tilde, path("$work") noautoconvert ///
url("https://example.edu/~Dave/data.csv") description("~Daily refresh")
loadlines "$work/Tilde/_code/100_data_download.do"
quietly count if strpos(v1, "https://example.edu/~Dave/data.csv")
assert r(N) >= 1
* NB: write the dollar sign with char(36). A literal "$ave" in this do-file
* would be expanded as a global by Stata before -strpos- ever saw it, and
* strpos(v1, "") is 1 for every row, so the assert would pass vacuously.
quietly count if strpos(v1, char(36) + "ave")
assert r(N) == 0
loadlines "$work/Tilde/_documentation/index.do"
quietly count if strpos(v1, "wput ~Daily refresh")
assert r(N) == 1
* the docs agree with the code
loadlines "$work/Tilde/_documentation/Readme.md"
quietly count if strpos(v1, "~Daily refresh")
assert r(N) >= 1
* The markers themselves still work: the control file really does get $ and `.
* NB: do NOT assert that "~D" is absent from this file. The description is
* stamped into its header, so a description of "~Daily refresh" puts a real
* "~D" there -- which is the point. Check the markers pb_wl owns instead.
loadlines "$work/Tilde/_code/000_control.do"
quietly count if strpos(v1, "global raw")
assert r(N) == 1
foreach mark in "~Draw" "~Dcode" "~Dcleaned" "~Broot" "~Brun_all" {
quietly count if strpos(v1, "`mark'")
assert r(N) == 0
}
* and the user's own tilde survived into the header
quietly count if strpos(v1, "~Daily refresh")
assert r(N) == 1
di as text "{hline 70}"
di as text "TEST o: a relative path() still yields an absolute r(path)/\$root"
di as text "{hline 70}"
* Used to: path() was carried through verbatim, so a relative path produced a
* relative r(path) and a relative -global root- in 000_control.do -- both
* documented as absolute, and the control file has to survive being moved.
local pwd_before `"`c(pwd)'"'
quietly cd "$work"
capture mkdir "relbase"
projectbuilder RelPath, path("relbase")
local rp `"`r(path)'"'
assert substr(`"`rp'"', 1, 1) == "/" | regexm(`"`rp'"', "^[a-zA-Z]:")
confirm file `"`rp'/_code/000_control.do"'
loadlines `"`rp'/_code/000_control.do"'
quietly count if strpos(v1, `"global root ""') & ///
(strpos(v1, `"global root "/"') | regexm(v1, `"global root "[a-zA-Z]:"'))
assert r(N) == 1
quietly cd `"`pwd_before'"'
* a path() that is already absolute is left alone
projectbuilder AbsPath, path("$work")
assert `"`r(path)'"' == `"$work/AbsPath"'
* a trailing separator on path() must not survive into a doubled one. NB:
* do not test this by searching r(path) for "//" -- on macOS c(tmpdir) itself
* ends in a slash, so $work already contains one and the test would fail on
* the environment rather than on the code. Compare against the exact join.
projectbuilder TrailSlash, path("$work/")
assert `"`r(path)'"' == `"$work/TrailSlash"'
confirm file `"$work/TrailSlash/_code/000_control.do"'
di as text "{hline 70}"
di as text "TEST p: _runall.do carries a literal docs path, not \$docs"
di as text "{hline 70}"
* Used to: the generated _runall.do did -cd "$docs"-, but projectbuilder runs
* that file itself under -builddocs-, before 000_control.do has ever run. So
* $docs was empty and the cd went somewhere else entirely -- in a session
* where another project's control file HAD run, it went to that project.
macro drop docs
projectbuilder Runall, path("$work")
loadlines "$work/Runall/_documentation/_runall.do"
quietly count if strpos(v1, `"local here "$work/Runall/_documentation""')
assert r(N) == 1
* builddocs must not leave the working directory moved
local pwd_before `"`c(pwd)'"'
capture noisily projectbuilder Runall, path("$work") rebuild builddocs
assert `"`c(pwd)'"' == `"`pwd_before'"'
di as text "{hline 70}"
di as text "TEST q: the generated profiler runs under the version 16.0 pin"
di as text "{hline 70}"
* Used to: 400_data_profiler.do emitted -table `g', statistic(...)-, which is
* Stata 17 syntax, while 000_control.do pins -version 16.0-. The shipped
* pipeline therefore stopped with r(198) on its own generated code.
projectbuilder Prof, path("$work") outcomes(price mpg) over(foreign)
sysuse auto, clear
quietly save "$work/Prof/02_cleaned/Prof_analytic.dta", replace
* run the profiler body under the pin the control file sets, without the
* -clear all- that running 000_control.do itself would do
global root "$work/Prof"
global cleaned "$work/Prof/02_cleaned"
capture noisily version 16.0: do "$work/Prof/_code/400_data_profiler.do"
assert _rc == 0
di as text "{hline 70}"
di as text "TEST r: recorded metadata survives \$, ~ and a stray backtick"
di as text "{hline 70}"
* Used to: the read-back did -local `m' `"`mval'"'-, a bare re-expansion. A
* recorded $ was eaten and the damaged value written back over the original;
* a recorded backtick stopped the run, permanently, on every later rebuild.
* The help file invites hand-editing this file, so it has to survive it.
projectbuilder MetaChar, path("$work") description("placeholder") noautoconvert
tempname mch
file open `mch' using "$work/MetaChar/_documentation/_project_meta.txt", write text replace
file write `mch' ("description=cost is " + char(36) + "M per year, ~Dept share") _n
file write `mch' "created=2026-01-01" _n
file close `mch'
projectbuilder MetaChar, path("$work") rebuild noautoconvert
loadlines "$work/MetaChar/_documentation/Readme.md"
quietly count if strpos(v1, "cost is " + char(36) + "M per year, ~Dept share")
assert r(N) == 1
* and it is written back intact, not silently damaged
loadlines "$work/MetaChar/_documentation/_project_meta.txt"
quietly count if strpos(v1, "description=cost is " + char(36) + "M per year, ~Dept share")
assert r(N) == 1
* the recorded created date is honoured rather than restamped with today
loadlines "$work/MetaChar/_documentation/Readme.md"
quietly count if strpos(v1, "| Created | 2026-01-01 |")
assert r(N) == 1
* a backtick in the file is skipped with a note; the rebuild still finishes
file open `mch' using "$work/MetaChar/_documentation/_project_meta.txt", write text replace
file write `mch' ("description=Booth" + char(96) + "s cohort") _n
file write `mch' "topic=survey" _n
file close `mch'
capture noisily projectbuilder MetaChar, path("$work") rebuild noautoconvert
assert _rc == 0
loadlines "$work/MetaChar/_documentation/Readme.md"
quietly count if strpos(v1, "| Topic | survey |") // the good keys still load
assert r(N) == 1
di as text "{hline 70}"
di as text "TEST s: a base that cannot be written names the folder, not a do-file"
di as text "{hline 70}"
* Used to: every mkdir was captured and only the BASE was checked, so on a
* read-only base the tree silently failed to appear and the run carried on to
* the first -file open-, stopping with a bare r(603) about a do-file.
capture noisily projectbuilder NoSuchBase, path("$work/missing/deeper")
assert _rc == 601
* An existing but unwritable base is caught at the target, with 693. Making a
* directory unwritable needs -chmod-, so this half runs on Unix-likes only;
* everything above and below is platform-neutral. $unixlike is set once here
* and reused by TEST t.
global unixlike = inlist("`c(os)'", "MacOSX", "Unix")
if $unixlike {
capture mkdir "$work/ro"
if !_rc {
shell chmod 555 "$work/ro"
capture noisily projectbuilder ROproj, path("$work/ro")
assert _rc == 693
shell chmod 755 "$work/ro"
}
}
else di as text " (skipped the unwritable-base half: needs chmod)"
di as text "{hline 70}"
di as text "TEST t: data() reports files it could not copy"
di as text "{hline 70}"
* Used to: -capture copy- swallowed the failure and only successes were
* counted, so an unreadable source file dropped out of the analytic file and
* the run still printed OK.
mkdir "$work/_src3"
sysuse auto, clear
quietly export delimited using "$work/_src3/ok1.csv", replace
quietly export delimited using "$work/_src3/ok2.csv", replace
* Making a file unreadable needs -chmod-, so the skip-and-report half is
* Unix-only. On Windows, check the platform-neutral half instead: every
* readable file is copied and counted.
if $unixlike {
quietly export delimited using "$work/_src3/locked.csv", replace
shell chmod 000 "$work/_src3/locked.csv"
capture noisily projectbuilder Skipper, path("$work") data("$work/_src3") noautoconvert
assert r(nraw) == 2
capture confirm file "$work/Skipper/01_raw/locked.csv"
assert _rc != 0
shell chmod 644 "$work/_src3/locked.csv"
}
else {
capture noisily projectbuilder Skipper, path("$work") data("$work/_src3") noautoconvert
assert r(nraw) == 2
di as text " (skipped the unreadable-file half: needs chmod)"
}
confirm file "$work/Skipper/01_raw/ok1.csv"
confirm file "$work/Skipper/01_raw/ok2.csv"
di as text "{hline 70}"
di as text "TEST u: pb_count survives a folder it cannot list"
di as text "{hline 70}"
* Used to: -local list : dir- inside the counter was uncaptured, so a missing
* or clobbered 01_raw/_converted aborted the whole run with a bare r(601).
projectbuilder Counted, path("$work") noautoconvert
capture rmdir "$work/Counted/01_raw/_converted"
* put a plain FILE where the folder should be: -: dir- errors on that too
tempname ffh
file open `ffh' using "$work/Counted/01_raw/_converted", write text replace
file write `ffh' "not a directory" _n
file close `ffh'
capture noisily projectbuilder Counted, path("$work") rebuild noautoconvert
assert _rc == 0
assert r(nconverted) == 0
di as text "{hline 70}"
di as text "TEST v: the documented examples run as written"
di as text "{hline 70}"
* The Examples section of projectbuilder.sthlp is meant to be runnable top to
* bottom in one directory. It used to re-scaffold a project an earlier
* section had already created, so reading the help and typing along hit 602.
*
* Run them from a directory whose name contains a space. That is the ordinary
* case on Windows ("C:\Users\First Last\...") and on Mac ("~/My Drive/..."),
* and it is where an unquoted path in generated code shows up.
mkdir "$work/_help ex"
local pwd_before `"`c(pwd)'"'
quietly cd "$work/_help ex"
projectbuilder Ex01Feed, description("Monthly vendor extract")
confirm file "Ex01Feed/_documentation/Readme.md"
projectbuilder Ex02Feed, path("projects")
projectbuilder Agency/Extract, description("One agency extract")
assert `"`r(project)'"' == "Agency_Extract"
projectbuilder "Vendor Feed 2027", description("Quoted name")
assert `"`r(project)'"' == "Vendor Feed 2027"
projectbuilder Ex01Feed, rebuild
projectbuilder Ex01Feed, rebuild topic("procurement")
projectbuilder Ex01Feed, rebuild replace
projectbuilder Ex03Rates, outcomes(enroll_rate cost_pp) over(year region)
mkdir "drop"
sysuse auto, clear
quietly export delimited using "drop/auto.csv", replace
quietly projectbuilder Ex04Auto, data("drop")
assert `"`r(project)'"' == "Ex04Auto"
assert r(nraw) == 1
assert `"`r(path)'"' != ""
* the two -display- lines exactly as the help prints them: r() macros have to
* resolve inside an expression, not just via `r(name)' macro quoting
capture noisily display "built " r(project) " with " r(nraw) " raw file(s)"
assert _rc == 0
capture noisily display "it lives at " r(path)
assert _rc == 0
* the every-option call, reusing the drop/ folder the previous example made
projectbuilder Ex07Budgets, ///
data("drop") ///
description("County budget CSVs, one row per dept per FY") ///
topic("local government, budgets") publicfacing(unsure) ///
timeline("annual") outcomes(total_budget) over(year dept) ///
descsave
assert r(nraw) == 1
loadlines "Ex07Budgets/_code/300_labels.do"
quietly count if strpos(v1, "_codebook.dta")
assert r(N) >= 1 // descsave really did seed 300_labels.do
loadlines "Ex07Budgets/_code/400_data_profiler.do"
quietly count if strpos(v1, `"local outcomes "total_budget""')
assert r(N) == 1
* a url() that cannot be reached must still leave a complete scaffold
capture noisily projectbuilder Ex08Open, url("https://example.com/data.csv")
assert _rc == 0
confirm file "Ex08Open/_code/000_control.do"
confirm file "Ex08Open/_documentation/website/index.html"
loadlines "Ex08Open/_code/100_data_download.do"
quietly count if strpos(v1, "https://example.com/data.csv")
assert r(N) >= 1 // the fetch is recorded either way
projectbuilder Ex05Feed, noautoconvert
projectbuilder Ex01Feed, rebuild builddocs
projectbuilder Ex06Cost, description("Reported in \$ millions")
loadlines "Ex06Cost/_documentation/Readme.md"
quietly count if strpos(v1, "Reported in " + char(36) + " millions")
assert r(N) == 1
* the refresh-everything loop from the Examples section
foreach p in Ex01Feed Ex03Rates Ex04Auto {
capture noisily projectbuilder `p', rebuild
assert _rc == 0
}
* Because the base held a space, every path the scaffold wrote had to be
* quoted. Prove the generated control file actually runs from here.
do "Ex01Feed/_code/000_control.do"
foreach g in root raw converted cleaned output code docs {
confirm file "${`g'}/."
}
assert strpos("$root", " ") // the space really did survive
quietly cd `"`pwd_before'"'
* -do 000_control.do- ran -clear all- again; restore the harness.
global work `"`W'"'
global pkgroot `"`PK'"'
adopath ++ "$pkgroot"
capture program drop loadlines
program define loadlines
import delimited using `0', clear delimiters("`=char(1)'", asstring) ///
varnames(nonames) bindquote(nobind) encoding("utf-8")
end
di as text "{hline 70}"
di as text "TEST w: the Try-it-now walkthrough runs as written"
di as text "{hline 70}"
* Every clickable line in the help file's Try it now section, in order, using
* the same path("`c(tmpdir)'") the help file uses. The walkthrough deliberately
* never calls -cd-, so this must not either: if any step secretly depended on
* the working directory, it would show up here.
local pwd_before `"`c(pwd)'"'
* c(tmpdir) survives between Stata sessions, so a previous run of this battery
* (or of the walkthrough itself) may have left pbdemo behind. That is exactly
* the 602 the help file warns about in its opening note, so assert the warning
* is accurate, then continue as the note tells the reader to.
global c_tmpdir `"`c(tmpdir)'"'
mata: st_local("pbleft", strofreal(direxists(st_global("c_tmpdir") + "pbdemo")))
local step1 ""
if `pbleft' {
capture noisily projectbuilder pbdemo, path("`c(tmpdir)'") ///
description("A walkthrough") topic(demo) publicfacing(no)
assert _rc == 602 // the help's opening note is correct
local step1 "rebuild"
di as text " (pbdemo already existed; continuing with rebuild, as the help says)"
}
* Step 1
projectbuilder pbdemo, path("`c(tmpdir)'") `step1' ///
description("A walkthrough") topic(demo) publicfacing(no)
* Step 2 -- r(path) is what every later step leans on
global pbdemo "`r(path)'"
assert `"$pbdemo"' != ""
assert substr(`"$pbdemo"', 1, 1) == "/" | regexm(`"$pbdemo"', "^[a-zA-Z]:")
* Step 3
confirm file "$pbdemo/_code/000_control.do"
confirm file "$pbdemo/_documentation/Readme.md"
* Step 4
sysuse auto, clear
quietly export delimited using "$pbdemo/01_raw/auto.csv", replace
* Step 5 -- and its claim that -describe- still shows the auto data
projectbuilder pbdemo, path("`c(tmpdir)'") rebuild
confirm variable make
assert _N == 74
* Step 6 -- both guards
capture noisily projectbuilder pbdemo, path("`c(tmpdir)'")
assert _rc == 602
capture noisily projectbuilder pbdemo2, path("`c(tmpdir)'") publicfacing(maybe)
assert _rc == 198
capture confirm file "`c(tmpdir)'pbdemo2/."
assert _rc != 0 // "before anything is created"
* Step 7 -- the topic changes, the rest survives
projectbuilder pbdemo, path("`c(tmpdir)'") rebuild topic("procurement")
loadlines "$pbdemo/_documentation/Readme.md"
quietly count if strpos(v1, "| Topic | procurement |")
assert r(N) == 1
quietly count if strpos(v1, "A walkthrough")
assert r(N) >= 1
* Step 8 -- the control file runs and its globals are real directories
do "$pbdemo/_code/000_control.do"
foreach g in root raw converted cleaned output code docs {
confirm file "${`g'}/."
}
* the walkthrough never moved the working directory
assert `"`c(pwd)'"' == `"`pwd_before'"'
* restore the harness after that -clear all-
global work `"`W'"'
global pkgroot `"`PK'"'
adopath ++ "$pkgroot"
capture program drop loadlines
program define loadlines
import delimited using `0', clear delimiters("`=char(1)'", asstring) ///
varnames(nonames) bindquote(nobind) encoding("utf-8")
end
di as text "{hline 70}"
di as text "TEST x0: Workflow A and the Quick start run exactly as printed"
di as text "{hline 70}"
* Workflow A's worked example used to read from a folder called budget_drop
* that the help never told you to create, so the first thing a reader ran
* produced an empty project and a green OK. Step 1 now makes the folder.
mkdir "$work/_wfa"
local pwd_before `"`c(pwd)'"'
quietly cd "$work/_wfa"
* Workflow A, Step 1 as printed
mkdir "budget_drop"
sysuse auto, clear
quietly export delimited using "budget_drop/budgets_fy24.csv", replace
quietly export delimited using "budget_drop/budgets_fy25.csv", replace
* Workflow A, Step 2 as printed
projectbuilder CountyBudgets, ///
data("budget_drop") ///
description("County budget CSVs, one row per dept per FY") ///
topic("local government, budgets") publicfacing(unsure) ///
timeline("annual") outcomes(total_budget) over(year dept) ///
descsave
assert r(nraw) == 2 // the files really were ingested
confirm file "CountyBudgets/01_raw/budgets_fy24.csv"
confirm file "CountyBudgets/01_raw/budgets_fy25.csv"
* Workflow A, Step 3 as printed
do "CountyBudgets/_code/000_control.do"
foreach g in root raw converted cleaned output code docs {
confirm file "${`g'}/."
}
quietly cd `"`pwd_before'"'
global work `"`W'"'
global pkgroot `"`PK'"'
adopath ++ "$pkgroot"
capture program drop loadlines
program define loadlines
import delimited using `0', clear delimiters("`=char(1)'", asstring) ///
varnames(nonames) bindquote(nobind) encoding("utf-8")
end
* the Quick start block: two alternatives, then the refresh
mkdir "$work/_qs"
local pwd_before `"`c(pwd)'"'
quietly cd "$work/_qs"
projectbuilder MyProject, description("What this project is for")
capture noisily projectbuilder MyProject, data("nothing_here") ///
description("What this project is for")
assert _rc == 602 // the help says exactly this
projectbuilder MyProject, rebuild
assert r(rebuilt) == 1
quietly cd `"`pwd_before'"'
di as text "{hline 70}"
di as text "TEST x: what a first-time user is told when the companions are absent"
di as text "{hline 70}"
* A new user on a bare install has neither convertanything nor combineall, so
* nothing is converted and no analytic file appears. The summary used to end
* "projectbuilder OK" and then tell them to "Review 02_cleaned/_analytic
* .dta" -- a file that was never created. Drop PLUS from the adopath to
* reproduce a bare install even on a machine where the companions ARE present.
* Drop PLUS to simulate a machine without the optional companions. Remember
* where it was: -adopath - PLUS- is not undone by anything later in this file,
* and forgetting to put it back silently disables convertanything, combineall,
* descsave and webdoc2 for EVERY test after this one -- which quietly turned
* their checks into no-ops.
local pk `"$pkgroot"'
local plusdir `"`c(sysdir_plus)'"'
adopath - PLUS
capture which convertanything
local haveconv = (_rc == 0)
assert !`haveconv' // PLUS really is off the path now
mkdir "$work/_bare"
sysuse auto, clear
quietly export delimited using "$work/_bare/a.csv", replace
projectbuilder Bare, path("$work") data("$work/_bare")
assert r(nraw) == 1
assert r(nconverted) == 0
capture confirm file "$work/Bare/02_cleaned/Bare_analytic.dta"
assert _rc != 0 // there is genuinely no analytic file
* the scaffold is still complete -- that is the promise of a no-dependency tool
confirm file "$work/Bare/_code/000_control.do"
confirm file "$work/Bare/_documentation/website/index.html"
* Put PLUS back before anything else runs, then the package copy on top of it.
adopath ++ `"`plusdir'"'
adopath ++ `"`pk'"'
capture which convertanything
assert _rc == 0 // the companions are reachable again
di as text "{hline 70}"
di as text "TEST y: data() that does not exist is reported, not glossed over"
di as text "{hline 70}"
* Workflow A's first worked example used to be run verbatim against a folder
* that did not exist; the run said so in passing and then printed OK, so the
* reader's first project was empty and looked successful.
capture noisily projectbuilder Missing, path("$work") data("$work/no_such_drop")
assert _rc == 0 // still a usable scaffold
assert r(nraw) == 0
confirm file "$work/Missing/_code/000_control.do"
di as text "{hline 70}"
di as text "TEST z: rebuild on a project that is not there says so"
di as text "{hline 70}"
* -rebuild- on a missing project scaffolds one rather than erroring, which is
* what makes it safe in a scheduled script. A mistyped name therefore looks
* like a successful refresh unless the run says otherwise. r(rebuilt)
* distinguishes the two cases.
capture noisily projectbuilder NeverMade, path("$work") rebuild
assert _rc == 0
assert r(rebuilt) == 0 // scaffolded, NOT refreshed
confirm file "$work/NeverMade/_code/000_control.do"
* and a real refresh does report 1
projectbuilder NeverMade, path("$work") rebuild
assert r(rebuilt) == 1
* the -replace- gotcha the Options section now warns about: an option that
* changes a guarded do-file needs -replace- to take effect
projectbuilder Guarded, path("$work")
projectbuilder Guarded, path("$work") rebuild descsave
loadlines "$work/Guarded/_code/300_labels.do"
quietly count if strpos(v1, "_codebook.dta")
assert r(N) == 0 // bare rebuild leaves _code/ alone
projectbuilder Guarded, path("$work") rebuild replace descsave
loadlines "$work/Guarded/_code/300_labels.do"
quietly count if strpos(v1, "_codebook.dta")
assert r(N) >= 1 // with replace, it lands
di as text "{hline 70}"
di as text "TEST aa: what the run prints back can be pasted straight back in"
di as text "{hline 70}"
* The "Rerun:" line printed the name bare, so a project called
* "Vendor Feed 2027" produced a command projectbuilder itself rejects with
* "too many project names". Two separate readers hit this.
* NB: use a NAMED log -- -log close _all- would close the batch log this whole
* battery is written to, and -quietly- would keep the very lines under test out
* of it. Widen linesize first: the hint carries an absolute path and the log
* would otherwise wrap it onto a continuation line, defeating any match.
local ls_before = c(linesize)
set linesize 250
projectbuilder "Spaced Name 2027", path("$work") description("quoted name")
log using "$work/rerun.log", replace text name(pbcap)
projectbuilder "Spaced Name 2027", path("$work") rebuild
log close pbcap
loadlines "$work/rerun.log"
* the one line a user is most likely to copy must be runnable as printed:
* quoted name, and the path() they built with
quietly count if strpos(v1, "Rerun:") & ///
strpos(v1, `"projectbuilder "Spaced Name 2027""') & ///
strpos(v1, `"path("$work")"') & strpos(v1, "rebuild")
assert r(N) == 1
* and never the bare, unpasteable form
quietly count if strpos(v1, "Rerun:") & strpos(v1, "projectbuilder Spaced Name 2027,")
assert r(N) == 0
* A name without a space is printed bare -- but path() is printed EVEN WHEN THE
* USER DID NOT TYPE IT. That is deliberate: the base is absolute by this point,
* so the hint runs from any directory. Printing it only when path() was typed
* meant that a project built in the current directory produced a hint which,
* pasted anywhere else, scaffolded a second empty project.
local pwd_before `"`c(pwd)'"'
quietly cd "$work"
projectbuilder Plain
log using "$work/rerun2.log", replace text name(pbcap)
projectbuilder Plain, rebuild
log close pbcap
quietly cd `"`pwd_before'"'
loadlines "$work/rerun2.log"
quietly count if strpos(v1, "Rerun:") & strpos(v1, "projectbuilder Plain,") & ///
strpos(v1, "path(") & strpos(v1, "rebuild")
assert r(N) == 1
* the name itself is still unquoted when it has no space
quietly count if strpos(v1, "Rerun:") & strpos(v1, char(34) + "Plain" + char(34))
assert r(N) == 0
set linesize `ls_before'
di as text "{hline 70}"
di as text "TEST bb: descsave is remembered like every other recorded option"
di as text "{hline 70}"
* descsave was the one option missing from _project_meta.txt, so a bare
* rebuild reported "Descsave: no" and -rebuild replace- deleted the codebook
* call it had originally written.
projectbuilder Cb, path("$work") description("codebook") descsave noautoconvert
loadlines "$work/Cb/_code/300_labels.do"
quietly count if strpos(v1, "_codebook.dta")
assert r(N) >= 1
loadlines "$work/Cb/_documentation/_project_meta.txt"
quietly count if strpos(v1, "descsave=descsave")
assert r(N) == 1
* a bare rebuild keeps it on record ...
projectbuilder Cb, path("$work") rebuild noautoconvert
loadlines "$work/Cb/_documentation/_project_meta.txt"
quietly count if strpos(v1, "descsave=descsave")
assert r(N) == 1
* ... and -rebuild replace- rewrites 300_labels.do WITH the call still in it
projectbuilder Cb, path("$work") rebuild replace noautoconvert
loadlines "$work/Cb/_code/300_labels.do"
quietly count if strpos(v1, "_codebook.dta")
assert r(N) >= 1
di as text "{hline 70}"
di as text "TEST cc: publicfacing() trims, othernotes is echoed, description reaches _code/"
di as text "{hline 70}"
* publicfacing("yes ") used to be rejected with 198 over one trailing space
projectbuilder Pf, path("$work") publicfacing("Yes ") ///
othernotes("sent by the county in August") description("A tidy description")
loadlines "$work/Pf/_documentation/Readme.md"
quietly count if strpos(v1, "| Public-facing | yes |")
assert r(N) == 1
* the help says description() reaches the pipeline headers -- check that it does
loadlines "$work/Pf/_code/000_control.do"
quietly count if strpos(v1, "A tidy description")
assert r(N) == 1
di as text "{hline 70}"
di as text "TEST dd: every generated do-file actually RUNS"
di as text "{hline 70}"
* The battery used to check what the generated files CONTAIN, and ran only
* 000_control.do. That let a real defect ship in 300_labels.do: the descsave
* block passed a -replace- option that -descsave- does not have, so on any
* machine where descsave IS installed the file stopped with r(198). The
* "not installed" branch is what everyone else hits, which is why nobody saw
* it. Run all seven, in order, and require rc 0 from each.
mkdir "$work/_gen"
local pwd_before `"`c(pwd)'"'
quietly cd "$work/_gen"
projectbuilder Gen, description("generated-pipeline check") ///
topic("testing") publicfacing(no) timeline("annual") ///
outcomes(price mpg) over(foreign) descsave
sysuse auto, clear
quietly export delimited using "Gen/01_raw/auto_a.csv", replace
keep make price mpg foreign
quietly export delimited using "Gen/01_raw/auto_b.csv", replace
quietly projectbuilder Gen, rebuild
* 300 onward read the analytic file, which only exists if the companions are
* installed. Seed it by hand when they are not, so the pipeline is exercised
* on every machine rather than only on the author's.
capture confirm file "Gen/02_cleaned/Gen_analytic.dta"
if _rc {
sysuse auto, clear
quietly save "Gen/02_cleaned/Gen_analytic.dta", replace
di as text " (seeded the analytic file: companions absent on this machine)"
}
local genpwd `"`c(pwd)'"'
foreach f in 000_control.do 100_data_download.do 200_data_management.do ///
300_labels.do 400_data_profiler.do 500_aggregation.do ///
600_analysis.do {
capture noisily do "`genpwd'/Gen/_code/`f'"
local generc = _rc
adopath ++ "$pkgroot" // each control-file run drops the addition
if `generc' di as err " FAILED: `f' returned r(`generc')"
assert `generc' == 0
}
di as text " all seven generated do-files ran"
* 000_control.do ran -clear all-, which dropped the helper program; restore it
capture program drop loadlines
program define loadlines
import delimited using `0', clear delimiters("`=char(1)'", asstring) ///
varnames(nonames) bindquote(nobind) encoding("utf-8")
end
* descsave really produced something, and left the analytic data loaded
capture which descsave
if !_rc {
confirm file "`genpwd'/Gen/_documentation/Gen_codebook.dta"
di as text " descsave wrote the codebook dataset"
}
* no file-writer marker survived into any generated file
foreach f in _code/000_control.do _code/100_data_download.do ///
_code/200_data_management.do _code/300_labels.do ///
_code/400_data_profiler.do _code/500_aggregation.do ///
_code/600_analysis.do _documentation/index.do ///
_documentation/_runall.do {
loadlines "`genpwd'/Gen/`f'"
quietly count if strpos(v1, "~D") | strpos(v1, "~B")
assert r(N) == 0
}
di as text " no ~D/~B markers left anywhere in the generated files"
quietly cd `"`pwd_before'"'
global work `"`W'"'
global pkgroot `"`PK'"'
adopath ++ "$pkgroot"
di as text "{hline 70}"
di as text "TEST ee: builddocs renders into website/, or says why it could not"
di as text "{hline 70}"
* Two bugs lived here. webdoc2 writes index.html BESIDE index.do, so the
* rendered page never reached website/index.html -- which is the page the run
* points you at and the one the built-in fallback writes. And the generated
* _runall.do swallowed webdoc2's error, so every render reported success.
*
* The render also needs webdoc2's header.html, which webdoc2 ships as an
* ANCILLARY file: -net install webdoc2- never places it. _runall.do therefore
* looks for it before it cd's into _documentation/ and borrows a copy.
mkdir "$work/_docs"
local pwd_before `"`c(pwd)'"'
quietly cd "$work/_docs"
projectbuilder Rendered, description("builddocs check") topic("docs")
capture which webdoc2
local havewd2 = (_rc == 0)
capture findfile "header.html"
local havehdr = (_rc == 0)
capture noisily projectbuilder Rendered, rebuild builddocs
assert _rc == 0 // never fatal, either way
confirm file "Rendered/_documentation/website/index.html"
if `havewd2' & `havehdr' {
* The render should have succeeded and landed in website/.
loadlines "Rendered/_documentation/website/index.html"
quietly count if strpos(lower(v1), "bootstrap")
assert r(N) >= 1 // it is the webdoc2 page ...
quietly count if strpos(v1, "Install webdoc2 for a richer")
assert r(N) == 0 // ... not the built-in fallback
* and the borrowed header was not left lying in the project
capture confirm file "Rendered/_documentation/header.html"
assert _rc != 0
di as text " webdoc2 rendered into website/index.html"
}
else {
* Without webdoc2, or without its header.html, the built-in page must
* survive intact -- that is the whole promise of the fallback.
loadlines "Rendered/_documentation/website/index.html"
quietly count if strpos(v1, "Install webdoc2 for a richer")
assert r(N) == 1
di as text " (webdoc2/header.html unavailable; checked the fallback instead)"
}
quietly cd `"`pwd_before'"'
di as text ""
di as text "{hline 70}"
di as text "projectbuilder v2.0.1: ALL TESTS PASSED"
di as text "{hline 70}"