Checkpoint Lab — Dynamic and Hybrid Libraries, Remote Libraries, and Extension Architecture
Build equivalent static and dynamic providers, add a loopback Remote boundary, inject both a metadata mismatch and a connection failure, repair each layer, and produce an architecture decision record backed by evidence.
Checkpoint objectives
- Build equivalent static and dynamic keyword providers over one pure domain function.
- Prove metadata through Libdoc and actual execution through Robot results.
- Add a loopback-only Remote protocol boundary with explicit process ownership and cleanup.
- Inject and diagnose one metadata mismatch and one Remote connection failure without erasing original evidence.
- Produce an architecture decision record explaining when the added complexity is justified.
Current compatibility baseline — verified 2026-08-31.
Robot Framework 7.4.2 is the stable course baseline
and requires Python 3.8+. The mandatory examples use Robot
Framework's current static, dynamic, hybrid, Libdoc, and built-in
Remote interfaces. Dynamic libraries require
get_keyword_names and run_keyword;
metadata getters are optional but strongly recommended when the
proxy knows signatures, types, tags, documentation, or source.
Starting with Robot Framework 7.0, dynamic special methods may also
be asynchronous, but this chapter keeps the learning path
synchronous. Hybrid libraries perform dynamic name discovery but
Robot calls Python methods directly. Remote is a dynamic proxy over
XML-RPC and does not itself add authentication or transport
security. The mandatory Remote exercise therefore binds a disposable
protocol fixture only to 127.0.0.1, uses a bounded
client timeout, and does not expose a remote-stop operation. The
separately maintained robotremoteserver package is
optional: its latest published 1.1.1 release explicitly documents
Python support through 3.11, so it is not required for this course's
Python 3.8+ local path.
1. Checkpoint scenario
You are reviewing an extension design proposed for a release pipeline. The same synthetic normalization operation is available through a static provider, a dynamic registry, and a process-isolated Remote provider. Your job is not merely to make all three pass: you must prove each contract, deliberately break two different layers, repair them minimally, and decide which architecture should be retained.
The entire checkpoint runs locally. The Remote fixture binds only to loopback, all identifiers are synthetic, and every generated file lives inside the lab/evidence directories.
2. Preflight and exact assumptions
python --version
python -m robot --version
python -m robot.libdoc --version
python -c "from robot.libraries.Remote import Remote; print(Remote.ROBOT_LIBRARY_SCOPE)"
- Robot Framework baseline: 7.4.2 stable.
- Python: 3.8+ supported by Robot 7.4.2; use one interpreter for all commands.
-
No external
robotremoteserverdependency is required. - Network: only
127.0.0.1ephemeral port. - Evidence directories are append/separate-by-run; never overwrite the injected-failure output.
3. Build the project
Create the same source files used in Lesson 2.
from __future__ import annotations
def normalize_token(token: str, prefix: str = "ID") -> str:
"""Pure Python domain function shared by every provider."""
value = token.strip().upper().replace(" ", "-")
if not value:
raise ValueError("token must not be empty")
if not value.replace("-", "").isalnum():
raise ValueError(f"unsupported token characters: {token!r}")
clean_prefix = prefix.strip().upper()
if not clean_prefix.isalpha():
raise ValueError("prefix must contain letters only")
return f"{clean_prefix}:{value}"
from robot.api.deco import keyword, library
from .domain import normalize_token
@library(scope="TEST", version="1.0.0")
class StaticCatalog:
"""Known-at-import keyword surface: the default architecture."""
@keyword("Normalize Token")
def normalize_token_keyword(self, token: str, prefix: str = "ID") -> str:
return normalize_token(token, prefix)
@keyword("Describe Provider")
def describe_provider(self) -> str:
return "static"
from __future__ import annotations
from robot.api import logger
from .domain import normalize_token
class DynamicCatalog:
"""Runtime-discovered provider used only to demonstrate the dynamic API."""
def get_keyword_names(self):
return ["Normalize Token", "Describe Provider"]
def get_keyword_arguments(self, name):
specs = {
"Normalize Token": ["token", "prefix=ID"],
"Describe Provider": [],
}
return specs[name]
def get_keyword_types(self, name):
types = {
"Normalize Token": {"token": "str", "prefix": "str"},
"Describe Provider": {},
}
return types[name]
def get_keyword_tags(self, name):
return ["rf21", "dynamic"]
def get_keyword_documentation(self, name):
docs = {
"__intro__": "Synthetic dynamic provider for Chapter 21.",
"__init__": "No import arguments.",
"Normalize Token": "Normalize a synthetic token and return prefix:value.",
"Describe Provider": "Return the provider architecture name.",
}
return docs.get(name, "")
def run_keyword(self, name, args, kwargs):
logger.info(f"dynamic dispatch keyword={name}")
if name == "Normalize Token":
values = {"token": None, "prefix": "ID"}
if len(args) > 2:
raise AssertionError("Normalize Token accepts at most two positional arguments")
for key, value in zip(("token", "prefix"), args):
values[key] = value
values.update(kwargs)
return normalize_token(values["token"], values["prefix"])
if name == "Describe Provider":
return "dynamic"
raise AssertionError(f"Unknown dynamic keyword: {name}")
from __future__ import annotations
import argparse
import signal
import threading
from pathlib import Path
from xmlrpc.server import SimpleXMLRPCServer
def normalize_token(token: str, prefix: str = "ID") -> str:
value = token.strip().upper().replace(" ", "-")
if not value:
raise ValueError("token must not be empty")
if not value.replace("-", "").isalnum():
raise ValueError(f"unsupported token characters: {token!r}")
clean_prefix = prefix.strip().upper()
if not clean_prefix.isalpha():
raise ValueError("prefix must contain letters only")
return f"{clean_prefix}:{value}"
def get_keyword_names():
return ["Normalize Token", "Describe Provider"]
def get_keyword_arguments(name):
return {
"Normalize Token": ["token", "prefix=ID"],
"Describe Provider": [],
}[name]
def get_keyword_types(name):
return {
"Normalize Token": {"token": "str", "prefix": "str"},
"Describe Provider": {},
}[name]
def get_keyword_tags(name):
return ["rf21", "remote", "loopback"]
def get_keyword_documentation(name):
return {
"__intro__": "Loopback-only Chapter 21 Remote protocol fixture.",
"__init__": "The server is started outside Robot and bound to 127.0.0.1.",
"Normalize Token": "Normalize a synthetic token through XML-RPC.",
"Describe Provider": "Return the remote provider architecture name.",
}.get(name, "")
def run_keyword(name, args, kwargs=None):
kwargs = kwargs or {}
try:
if name == "Normalize Token":
values = {"token": None, "prefix": "ID"}
for key, value in zip(("token", "prefix"), args):
values[key] = value
values.update(kwargs)
result = normalize_token(values["token"], values["prefix"])
elif name == "Describe Provider":
result = "remote"
else:
raise ValueError(f"unknown remote keyword: {name}")
return {
"status": "PASS",
"return": result,
"output": f"*INFO* loopback remote executed {name}\n",
}
except Exception as exc:
return {
"status": "FAIL",
"error": f"{type(exc).__name__}: {exc}",
"traceback": "Synthetic fixture omits implementation traceback.",
}
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--port-file", required=True)
args = parser.parse_args()
port_file = Path(args.port_file)
port_file.parent.mkdir(parents=True, exist_ok=True)
# SECURITY BOUNDARY — DO NOT USE 0.0.0.0 in this lab. Bind loopback only.
server = SimpleXMLRPCServer(("127.0.0.1", 0), allow_none=False, logRequests=False)
for func in (
get_keyword_names,
get_keyword_arguments,
get_keyword_types,
get_keyword_tags,
get_keyword_documentation,
run_keyword,
):
server.register_function(func)
port_file.write_text(str(server.server_address[1]), encoding="utf-8")
print(f"RF21_REMOTE_READY http://127.0.0.1:{server.server_address[1]}", flush=True)
def request_shutdown(*_):
threading.Thread(target=server.shutdown, daemon=True).start()
if hasattr(signal, "SIGTERM"):
signal.signal(signal.SIGTERM, request_shutdown)
signal.signal(signal.SIGINT, request_shutdown)
try:
server.serve_forever()
finally:
server.server_close()
if __name__ == "__main__":
main()
Also create suites/local_providers.robot and
suites/remote_provider.robot from Lesson 2. The hybrid
provider is optional in this checkpoint because the required
comparison is static versus dynamic, but keeping it provides useful
third-party evidence.
4. Predict before executing
| Action | Prediction to record first |
|---|---|
| Generate static/dynamic Libdoc | Both expose Normalize Token and Describe Provider; dynamic signature comes from metadata |
| Run local provider suite | Static and dynamic return the same REF:AB-12 value |
| Start loopback server | A new owned PID listens on 127.0.0.1 and writes one ephemeral port |
| Run Remote suite | Remote result equals local contract and log includes server-provided INFO |
| Break dynamic metadata | Two-argument call fails before dynamic dispatch |
| Stop server and rerun Remote test | Connection/discovery fails while original PASS evidence remains unchanged |
5. Establish the known-good local baseline
mkdir -p evidence/baseline/libdoc evidence/baseline/robot
python -m robot.libdoc -P src rf21_ext.static_catalog.StaticCatalog evidence/baseline/libdoc/static.html
python -m robot.libdoc -P src rf21_ext.dynamic_catalog.DynamicCatalog evidence/baseline/libdoc/dynamic.html
python -m robot --pythonpath src --outputdir evidence/baseline/robot suites/local_providers.robot
Do not continue until Libdoc and Robot agree about the two provider contracts. This is your pre-failure reference.
6. Establish the known-good Remote baseline
Bash / zsh
mkdir -p evidence/baseline/remote
rm -f evidence/baseline/remote/port.txt
python remote_server.py --port-file evidence/baseline/remote/port.txt > evidence/baseline/remote/server.log 2>&1 &
SERVER_PID=$!
for i in 1 2 3 4 5 6 7 8 9 10; do test -s evidence/baseline/remote/port.txt && break; sleep 0.2; done
PORT=$(cat evidence/baseline/remote/port.txt)
REMOTE_URI="http://127.0.0.1:${PORT}"
printf 'pid=%s\nuri=%s\n' "$SERVER_PID" "$REMOTE_URI" > evidence/baseline/remote/process.txt
python -m robot.libdoc "Remote::${REMOTE_URI}::5s" evidence/baseline/remote/libdoc.html
python -m robot --variable "REMOTE_URI:${REMOTE_URI}" --outputdir evidence/baseline/remote/robot suites/remote_provider.robot
PowerShell
New-Item -ItemType Directory -Force evidence/baseline/remote | Out-Null
$server = Start-Process python -ArgumentList 'remote_server.py','--port-file','evidence/baseline/remote/port.txt' -PassThru -RedirectStandardOutput evidence/baseline/remote/server.log -RedirectStandardError evidence/baseline/remote/server.err
for ($i=0; $i -lt 20 -and -not (Test-Path evidence/baseline/remote/port.txt); $i++) { Start-Sleep -Milliseconds 200 }
$port = (Get-Content evidence/baseline/remote/port.txt -Raw).Trim()
$remoteUri = "http://127.0.0.1:$port"
"pid=$($server.Id)`nuri=$remoteUri" | Set-Content evidence/baseline/remote/process.txt
python -m robot.libdoc "Remote::$remoteUri::5s" evidence/baseline/remote/libdoc.html
python -m robot --variable "REMOTE_URI:$remoteUri" --outputdir evidence/baseline/remote/robot suites/remote_provider.robot
Verify the server address literally starts with
http://127.0.0.1:. Any other bind/URI fails the
checkpoint safety requirement.
7. Failure injection A — metadata mismatch
Save a copy of the known-good dynamic provider, then change only its
get_keyword_arguments result for
Normalize Token from two supported arguments to
["token"].
# BROKEN line inside get_keyword_arguments:
"Normalize Token": ["token"],
mkdir -p evidence/failure-metadata/libdoc evidence/failure-metadata/robot
python -m robot.libdoc -P src rf21_ext.dynamic_catalog.DynamicCatalog evidence/failure-metadata/libdoc/dynamic.html
python -m robot --pythonpath src --outputdir evidence/failure-metadata/robot suites/local_providers.robot
Interpret the evidence: Libdoc advertises one argument; the Robot suite supplies two; Robot rejects the call before the valid domain implementation is reached. Preserve this directory unchanged.
8. Repair A — restore the public metadata contract
Restore ["token", "prefix=ID"], regenerate Libdoc, and
rerun the local suite to a new directory:
mkdir -p evidence/repaired-metadata/libdoc evidence/repaired-metadata/robot
python -m robot.libdoc -P src rf21_ext.dynamic_catalog.DynamicCatalog evidence/repaired-metadata/libdoc/dynamic.html
python -m robot --pythonpath src --outputdir evidence/repaired-metadata/robot suites/local_providers.robot
The repair is accepted only when the metadata and execution evidence both return to the baseline contract.
9. Failure injection B — stop the owned Remote server
Stop the exact server PID recorded in the baseline. Keep the same URI value and run the Remote test into a new directory.
Bash
kill "$SERVER_PID"
wait "$SERVER_PID" 2>/dev/null || true
mkdir -p evidence/failure-connection
python -m robot --variable "REMOTE_URI:${REMOTE_URI}" --outputdir evidence/failure-connection suites/remote_provider.robot
PowerShell
Stop-Process -Id $server.Id -ErrorAction SilentlyContinue
$server.WaitForExit()
New-Item -ItemType Directory -Force evidence/failure-connection | Out-Null
python -m robot --variable "REMOTE_URI:$remoteUri" --outputdir evidence/failure-connection suites/remote_provider.robot
The expected failure must be attributable to the unavailable endpoint. Do not increase the timeout, edit the test to skip, or merge away this result.
10. Repair B — start a new owned loopback process
Start a fresh server. It may receive a different ephemeral port;
that is expected. Record the new PID/URI, run Remote Libdoc again,
then rerun only the Remote suite into
evidence/repaired-connection. Finally stop the new
process.
This proves that the fix was server availability/endpoint provenance, not a change to the domain keyword or assertion.
11. Serialization evidence
Use a direct XML-RPC call against the repaired server and retain the printed result dictionary:
python -c "from xmlrpc.client import ServerProxy; import sys; print(ServerProxy(sys.argv[1]).run_keyword('Normalize Token',['xy 9','LAB'],{}))" "$REMOTE_URI" \
> evidence/serialized-result.txt
Expected shape contains plain scalar/dictionary values such as
{'status': 'PASS', 'return': 'LAB:XY-9', ...}. Do not
claim Python object identity survived the transport.
12. Architecture decision record
Complete this record after the experiments:
Chosen production shape:
Why static is or is not sufficient:
Why hybrid is or is not sufficient:
Why dynamic dispatch is or is not required:
Why a separate process/language boundary is or is not required:
Authoritative metadata source:
Serialization schema:
Network exposure/authentication design if Remote is retained:
Server lifecycle owner and health check:
Timeout contract:
Parallel-worker isolation:
Failure evidence retained:
Version compatibility test:
Simpler rollback architecture:
For this synthetic operation, the correct production answer is normally static; the dynamic and Remote forms exist to teach contracts, not because the domain requires them. A good ADR says that explicitly.
13. Required evidence packet
| Artifact | What it proves |
|---|---|
| Baseline static/dynamic Libdoc | Known-good keyword metadata |
| Baseline local output.xml/log/report | Equivalent in-process behavior |
| Baseline Remote Libdoc + output.xml/log/report | Discovery and execution across XML-RPC |
| Loopback URI/PID/server-ready log | Network/process ownership and private bind |
| Metadata-failure Libdoc/result | Advertised signature mismatch caused pre-dispatch failure |
| Metadata-repair result | Public metadata contract restored |
| Connection-failure result | Endpoint unavailable while original PASS evidence preserved |
| Repaired-connection result | Fresh server/URI restores behavior |
| Serialized result dictionary | Wire-compatible data shape |
| ADR | Why complexity is or is not justified |
14. Verification checklist
- Exactly five course lesson files are unrelated to the lab filesystem; the lab itself is disposable.
- Static and dynamic providers call the same pure domain function.
- Dynamic metadata includes names, arguments, types, tags, and documentation.
- Hybrid, if retained, returns Python method names in the correct format.
-
Remote server binds only to
127.0.0.1and uses an ephemeral port. - Remote client timeout is bounded.
- No remote-stop capability, real credential, production endpoint, or public bind is required.
- First-failure evidence directories are not overwritten by repaired runs.
- Connection failure is repaired by restoring the owned service, not by giant retries/timeouts.
- All owned server processes are stopped at the end.
15. Cleanup / rollback
Stop only the PIDs created by this lab. Restore the good dynamic
metadata source. Keep evidence if it is part of the exercise
submission; otherwise delete only the disposable
rf21-extension-lab root. Do not kill unrelated Python
processes or remove global environments.
If the server cannot be stopped cleanly, record the PID and failure, then terminate that exact owned process using the operating system. Cleanup failure is evidence—not a reason to hide the preceding Robot result.
16. What Chapter 21 adds—and the bridge to Chapter 22
Chapter 21 adds an extension-architecture decision framework to the Robot operating model: static by default; hybrid for justified runtime discovery with direct callables; dynamic for justified proxy/registry dispatch; Remote only when process/machine/language isolation pays for its serialization, service lifecycle, security, and observability costs.
Chapter 22 moves from providing keywords to observing and transforming Robot execution itself with listeners, pre-run modifiers, visitors, and public programmatic APIs. The same principle continues: use explicit public contracts, preserve evidence, and add extension power only when the operational requirement warrants it.
Knowledge check
Why is the metadata mismatch a different failure layer from the Remote outage?
The metadata mismatch occurs inside Robot’s keyword-interface validation before dynamic dispatch; the outage occurs at the client/server transport and service-lifecycle boundary.
What evidence proves the Remote endpoint stayed local?
The recorded URI begins with 127.0.0.1, the server source binds 127.0.0.1, and the process/port evidence identifies the owned local listener.
Why is a passing repaired Remote run insufficient if the original failure directory was overwritten?
It destroys incident evidence and prevents proving what failed before the repair. Repaired evidence must be separate.
For the synthetic normalization operation, which architecture should the ADR normally select?
Static. The operation has a known Python keyword surface and no real runtime discovery or isolation requirement.
What new subject does Chapter 22 add beyond these library APIs?
Execution/model extensions: listeners, pre-run modifiers, visitors, and programmatic invocation rather than keyword-provider architecture.
References and version anchors
- Robot Framework 7.4.2 — Dynamic library API — checkpoint metadata baseline
- Robot Framework 7.4.2 — Hybrid library API — architecture comparison
- Robot Framework 7.4.2 — Remote protocol — wire/result contract
- Robot Framework 7.4.2 — Remote library — client configuration
- Python Remote Server — optional reference implementation
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.