7. The tempest command#
The tempest command is a thin layer, argument parsing and output, over
the temper-driver crate, which holds the registry of backends, the one
way from a settled library to each backend's files, and running tests
with each language's tools; the playground's wasm uses it too.
Temper's two main commands are test, which runs a project's tests,
and build, which writes every library in every language to
temper.out. Temper's std library is built into the
binary, so it needs nothing beside it.
tempest test [-b interp|BACKEND]... [--library NAME] [-v] [-w DIR | DIR]
tempest build [-w DIR] [-b BACKEND]... [-o DIR] [-v]
tempest run --library NAME -b interp|BACKEND [-w DIR]
tempest init [-o DIR]
tempest serve | lsp
tempest version | license | licenses | help [COMMAND]
Its commands and flags are Kotlin Temper's temper's, by the same
names and short forms; every one, and how each differs, is in
the command line. build, test, and run also
take --ignorefile, --no-connected, and --logformat json, which
writes a JSON record per line, as Kotlin's does
(log). run builds the
work root, then runs a library's top level with the backend's tools,
or on the interpreter; init writes Kotlin's project skeleton
(init). watch, repl,
and doc say they aren't in tempest yet, and exit with 3.
serve, or lsp, is a language server for editors, over stdin and
stdout, on the same library finding and Sources as test and
build; it's the temper-lsp crate, with
a page of its own.
Finding libraries#
Both commands start the same way. From the directory given, they find
the work root: the nearest directory holding temper.out, or else
the highest one holding a config.temper.md. Under it, each directory
with a config.temper.md is a library, and each directory in a library
is a module. Directories that .gitignore and .temperignore files
list are skipped, as is what --ignorefile lists, which has the last
word.
All reading goes through one Sources
value, so the compiler never touches the file system itself. It stacks
layers: the disk, std built in, or files in memory. The playground uses
memory, and the language server puts unsaved editor buffers in front of
the disk. Why, and how the search differs from Kotlin's (ignore files,
configs read rather than run), is in
the detailed page.
One compile path#
Diagram source
flowchart LR
settled["Settled library"] --> checked["checked: to_tmpl, then validate"]
checked --> withstd["with_std: std's names, if used"]
withstd --> translate["translate: one BACKEND"]
translate --> files["Files"]
Callers take these three steps, from
temper-driver:
checked turns a settled library
into tmpl and validates it, with_std
gives it std's names as the backend spells them, and
translate runs one backend from
the Registry, which holds each
built-in backend crate's BACKEND, one per cargo feature, all on by
default. (build sets std's names itself rather than calling
with_std.) Which backends build writes without -b, and which names
this build lacks, come from the registry and the list of
KNOWN names, not lists of
their own. The trade-offs are in
the registry,
a backend as plain data,
and one compile path, in detail.
test and build#
Diagram source
flowchart TD
find["Find libraries"] --> settle["Settle each library"]
settle -->|"-b interp, the default"| interp["Run its tests on the interpreter"]
settle -->|"-b BACKEND"| compile["The compile path"]
compile --> written["Write temper.out/BACKEND"]
written --> tools["Run the tests with the language's own tools"]
interp --> tally["One summary of tests passed"]
tools --> tally
- On the interpreter (the default), each library settles and runs
its tests in-process. That's fast: no files are written. Each test
gets a step budget (
--fuel, 10 billion by default), so one that loops forever fails alone, and what tests log is kept up to 64 KiB. - With
-b BACKEND, each library is built asbuilddoes (test_backend), then its tests run with the language's own tools: Python'stemper_core.testing, Mocha for JavaScript, LuaUnit, JUnit throughjavac,dotnet test,cargo test, or CMake and the test program for C++. Each backend crate says how, in itstoolsmodule, as itsBACKEND'sTools, and the driver just runs them. Their results are read back into the same summary, so-b interpand-b pyprint alike. - With several
-b, each backend runs in turn, with its own summary, then the total, which is Kotlin's one line. buildwritestemper.out/BACKEND/LIBRARY/…(or under-o DIR), laid out as Temper always has, with package files (pyproject.toml,Cargo.toml,package.json, the pom, the.csprojs, and a rockspec), and each backend's runtime beside the libraries. A library that imports another gets the other's names as that backend named them.- Errors never stop either command. Every error is reported, the code is still written, and the tests still run; a test fails only if it reaches an error. The exit status is 1 whenever there were errors. Each is reported once, under the library it's in, even when other libraries import that one.
Each command step by step, the tools each backend runs, and why imported libraries are settled again in each importer: the commands, in detail, each language's own tools, and libraries settled on their own.
Worked example#
A library with one file and one test:
triple/config.temper.md export let name = "triple";
triple/triple.temper
export let triple(n: Int): Int { n * 3 }
test("triple of 14") {
assert(triple(14) == 42);
}
$ tempest test -v .
Testing triple
ok triple of 14
Tests passed: 1 of 1
$ tempest test -b py .
Tests passed: 1 of 1
tempest build -b py writes the library, std, and the runtime under
temper.out/py/. The test becomes a unittest case, with the
assert macro's message built in:
class TestTriple(TestCase):
def test_triple_of_14(self, /) -> None:
'triple of 14'
test: Test = Test()
try:
actual = triple(14)
expected = 42
test.assert_(actual == expected, lambda: f'expected triple(14) == ({expected}) not ({actual})')
finally:
test.soft_fail_to_hard()
Add a second test that misspells triple as tripel, and only that
test fails, on the interpreter and in Python alike:
$ tempest test -b py .
triple/triple.temper:8:10: error: tripel has not been declared
triple: 1 error
Testing triple on py
failed misspelled - Traceback (most recent call last):
Tests passed: 1 of 2
Where in the code#
temper-driver: theRegistry,KNOWN, and the compile path above.main.rs: argument parsing (parse),build_command,test_command,run_library,init_command, andserve;help.rs: whathelpprints.serve: the language server.build: every library, each backend, in dependency order.test_backendandrunintest_run: any backend's test tools, as each backend crate'stoolsmodule gives them (Tools), with JUnit XML and tool lookup shared intools.runinrun:tempest run, through each backend'sTools::run, or on the interpreter;init:tempest init.- Tests:
crates/temper-driver/tests/(dependencies between libraries, and the built-in runtimes). test_libraries,test_library, andTally: tests on the interpreter, and the summary;log: JSON records.work_root,find_libraries_ignoring,ignore_file, andload_library: finding and loading libraries.programinstdlib: std, built in, as its own library.- Tests:
crates/tempest/tests/(the CLI, the layout oftemper.out, andlicenses' currency); all the tests.
Dynamic backends (future work)#
The registry is a list built at startup, not a table fixed at compile time, so backends that aren't built in can join it later. The plan: first as external processes, each given a library as tmpl in a versioned, serialized form and answering with its files (and running its tests), so a backend can live in its own repository and language; then as DLLs, only if processes prove too slow. The wasm build keeps a fixed list of built-in backends. What has to change first: dynamic backends, in detail.
watch, repl, and doc come later
(parity with Kotlin's temper);
the next page says how watch will reuse all this,
and page 9 how lsp does.
In detail: The tempest command in detail: each decision with its alternatives and Kotlin Temper's way, the main types, the tests, and known weaknesses.