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 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.
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.
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
execvps 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
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_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_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
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.
What else do I see in Expect’s codebase?
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 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.
sizeto report the filesize of different executable-file sections.
- Run the C compiler.
- Test standard error messages.
- Link newlib.
- Execute commands remotely.
- Yet more global accessors.
- Pop certain globals to a file.
- Link LibIO.
- Reimplementation of
- 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
- 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!