When making stuff for others to use it is important to verify that it works as intended. It is important to make sure fixes to one issue don’t break another. For this purpose we’ve created very nice developer-experiences around success/failure counters!
Expect
Expect is one such example built around the TCL interpretor. Presumably Expect’s the main reason Linux From Scratch considers TCL to be part of the core OS? Several other OS components use it to ensure they’re built correctly.
Once it initializes the TCL interpreter Expect performs version checks, saves some existing APIs aside, registers some new ones, upon first-run initializes various systems including OS-specific terminal handling & cleanup routines, registers the bulk of new APIs & variables in a semi-specific order, & runs some TCL code to register the new library. I’ll go over those APIs tomorrow.
Then it parses commandline flags deferring to TCL where appropriate, followed by trailing args.
Finally Expect uses 1 of 3 codepaths to run the given TCL code depending on its input source. Before running a TCL exit
command with appropriate error code, ensuring any attached exit-handlers get called.
Expect TCL Commands
Expect consists mostly of a relatively-small API exposed to the TCL interpreter it runs.
There’s an interact
TCL command, which with extensive state evaluates some expression & parses commandline flags + keywords before outputting a prompt, reading from a TCL channel into a linkedlist, & evaluates them. Followed by richer timestamped-input mainloops & cleanup. Including a filedescriptor hashtable & subprocesses.
Glossing over a lot…
Expect provides a couple teletype TCL commands. stty
redirects the standard filedescriptors parsing redirected input then its options then the output. Finally this gets rewritten into an exec
command to evaluate.
And the system
command parses its commandline flags running a tcsetattr
syscall or equivalent IOCTL reporting any errors, concatenates remaining args into a buffer, calls system
ignoring SIGCHLD signals, calls tcgetattr
with error reporting, outputs results, & cleans up.
There’s also a few utilities in that file for configuring the terminal called by initialization & other modules.
Another file defines the bulk of APIs exposed to TCL testscripts. exp_open
parses its arguments, opens the TCL channel in 1 of 2 ways, attaching a PID & wrapping in a “file channel”.inter_return
defers to return
with adjusted exit codes.exp_continue
determines an appropriate exit code to return. interpreter
parses flags then recurses back to entrypoint.
overlay
with parsed arguments gathers a subcommand multistring with default SIGINT & SIGQUIT handlers execvp
s it before cleaning up & outputting the error. disconnect
with validation & ignoring SIGHUP configures terminal I/O specially & reconnects the standard filedescriptors to /dev/null before forking and/or configuring cgroups & exitting. fork
wraps the corresponding syscall with bookkeeping. strace
creates & possibly destroys Trace objects.
wait
parses args looking up the channel, checks its atomic state looking up a pid to waitpid
on or spinlocks as appropriate, converts results to TCL data, & cleans up. close
wraps the corresponding syscall with argument parsing & channel lookup. exp_configure
wraps TCL’s typesystem. exit
runs exit handlers, some cleanup TCL code, & closes the interpreter. exp_internal
wraps the TCL datamodel & the “diag” channels. debug
, if supported, reconfigures some globals.
log_user
also sets a global, as per log_file
. send
, send_error
, send_tty
, & send_user
all parses their arguments, retrieves the property to send, & with logging writes it in the appropriate format to the channel before cleaning up. send_log
wraps the Diag subsystem. sleep
wraps some system/event-loop specific code. getpid
wraps the corresponding syscall. exp_pid
retrieves a channel property. spawn
operates on channels & its implementation heavily depends on build flags.
Apart from some support functions (a few of which are only called from outside), that pretty well covers the bulk of the Expect-specific commands!
trap
parses flags, possibly examines current signal to get the data to return, once validated & with the list retrieved iterates over said list converting to an alternate TCL datastructure. Possibly configuring a signal handler to get it this data next time it recieves the signal in the TCL datamodel, via a background tasking checking its counters. Includes lookuptable conversions between signals & strings.
As for the main attractions of Expect commands…
expect
, expect_user
, & expect_tty
all (sharing the same code) interprets their args based on quantity evaluating an innerexpression, retrieves results, performs more complex argument parsing, iterates over commands & their states for deduplication, validates states, considers preparing a timeout, repeatedly reads results with handling & traversing the statemachines, compares against expected patterns of various types, & cleans up.
expect_after
, expect_before
, & expect_background
all, sharing the same code, does a lot of similar stuff. Just without checking the output stream. match_max
sets & validates a global (or channel-property) having retrieved channels state; as for remove_nulls
, parity
, & close_on_eof
. timestamp
puts more effort into argument parsing, retrieves the time, formats it according to the given format-string, & returns result to TCL.
That file also defines an optional debugging command stringifying the current state for output disabled in production builds. There’s a background handler to retrieve the output data for the expect*
commands, though a seperate file adds more infrastructure around it. Those commands also have several other support functions, including for carefully reading the resulting stream. There’s a couple obsolete functions for running subcommands.
Miscallanea
What else do I see in Expect’s codebase?
- Deprecated
select
-syscall I/O utilities, disabled in all builds - API around terminal-windowsize accessing IOCTLs
- It’s own date-formatting utility
- Opening sub-pseudoteletypes
- Implementation of glob-patterns
- Utilities upon those sub-pseudoteletypes, across 3 files
- Multiple sources for input events, including a noop one
- Configuring redirects on /dev/console
- Legacy cleanup routine
- Deprecated regular-expression engine, disabled in all builds
- Another globbing implementation
- Expose events to TCL
- Logging, including to TCL channels
- Abstractions around TCL channels
- TCL breakpoint debugging
- Stdlib adaptors to previously-internal APIs
I’m not entirely sure why TCL includes all this code, beyond there appearing to be a traditional distrust in the standardization of UNIX”s standard library.
DejaGNU
DejaGNU is an extended standard library for the barebones TCL-based Expect test-runner. As such it'll inevitably be a toolbox, best summarized in list form:
DejaGNU provides TCL functions for:
- Linking against LibGloss
- Connecting to other computers’ filesystems & commandline, most of DejaGNU are backends for this.
- Dispatch to “load”ers & “compile”rs.
- Directory walks, apart from certain dev or version-control dirs.
- Retrieve “DMUCS” hostnames.
- Globals accessors.
- Relative filepaths.
- Push & pop various globals.
- Wraps
size
to report the filesize of different executable-file sections. - Run the C compiler.
- Test standard error messages.
- Link newlib.
- Reimplement
which
. - Execute commands remotely.
- Yet more global accessors.
- Pop certain globals to a file.
- Link LibIO.
- Reimplementation of
grep
. - Configure/normalize the test command to run.
- Link G++.
- Prune a list.
- Configure expected error messages.
- Open an XML file to log results to.
- Remove extraneous warnings from output.
- Checks whether a test was configured to be skipped.
- Configure an expected warning.
- Close the XML logfile.
- Link LibStdC++.
- Diff results.
- Access envvars.
Remote filesystem backends:
- rlogin basic client
- SSH basic client
- Telnet basic client
- tip basic client
- FTP basic client
- rsh basic client
- Local operations (bundled within the dispatcher)
Reading through the more complex parts of DejaGNU I see:
- Nicer debugger commands abstract (mostly) TCL’s
uplevel
builtin-command. - It’s own error handling for unknown TCL commands, or rather ones which throws exceptions upon being dynamically loaded.
- Clear global state.
- Log status & exit.
- Log test results to XML, building upon custom XML serialization utilities.
- Configure whether/how to expect failures.
- Record test results with all relevant information to XML as per global configuration. Now this here looks a proper testrunner! Has numerous wrappers for different success statuses.
- Counters to be run during that recording.
- Access containing testsuite & configure where it logs to.
- Assemble “target” strings.
- Run all tests in a testsuite.
- Run a test validating its output as per global config.
- Many configurably-remote testrunning utils, with oens to wait on a command.
- Utils for running build commands.
- Utils for locating build commands.
- Internal utils (used for linking common libraries) for loading “multilibs”.
DejaGNU looks more like a proper testrunner, & more, than Expect!