Chapter 8 — Programming II
Lesson map
Lesson 08 published. Map, example, EX022.
flowchart TD
N0["Learning outcomes"]
N1["8.1 Pure functions are reviewable"]
N2["8.2 Fail closed"]
N3["8.3 Tests as documentation"]
N4["8.4 Logging decisions for review"]
N5["Synthesis"]
N0 --> N1 --> N2 --> N3 --> N4 --> N5
N5 --> EX["Worked example"]
EX --> Q["Exercise EX022"]
Logical drawing
flowchart LR
N0["Learning outcomes"]
N1["8.1 Pure functions are reviewable"]
N2["8.2 Fail closed"]
N3["8.3 Tests as documentation"]
N4["8.4 Logging decisions for review"]
N5["Synthesis"]
N0 --> N1 --> N2 --> N3 --> N4 --> N5 --> Q["EX022"]
[Learning outcomes]
|
v
[8.1 Pure functions are reviewable]
|
v
[8.2 Fail closed]
|
v
[8.3 Tests as documentation]
|
v
[8.4 Logging decisions for review]
|
v
[Synthesis]
|
v
[Worked example] --> [Exercise EX022]
Learning outcomes
You will (1) structure an analysis tool as pure functions with tests; (2) use unittest from the standard library; (3) make functions fail closed; and (4) version a script so a result can be reproduced months later.
8.1 Pure functions are reviewable
A function that takes inputs and returns a decision, without reading the network or the clock, can be tested exhaustively. For OT tooling this matters because the reviewer is often a control engineer who will accept a tool only if its behaviour on edge cases is demonstrated. Keep input and output (files, printing) at the edges and the decision logic in the middle.
Maya's classifier is written this way: classify(code) takes a function code and returns a word, so a control engineer can read the test list and see every decision without running a capture.
8.2 Fail closed
When an allow-list function receives an unknown source, an unknown function code or a malformed address, it should return "block" rather than raise an exception that a caller might catch and ignore. Failing closed in a monitoring tool means "flag for review"; in an enforcement device it means "deny". Neither means "assume fine".
Maya's first classifier failed open: anything outside the write set returned "read". A reviewer asked about code 8, diagnostics, whose sub-functions can restart communications, and the answer exposed the bug. Her fix returns "unknown", which in an offline monitor means flag for review.
8.3 Tests as documentation
A small unittest case list reads like a specification:
import unittest
def classify(fc):
return "write" if fc in (5, 6, 15, 16) else ("read" if fc in (1, 2, 3, 4) else "unknown")
class T(unittest.TestCase):
def test_codes(self):
self.assertEqual(classify(3), "read"); self.assertEqual(classify(16), "write")
self.assertEqual(classify(8), "unknown"); self.assertEqual(classify(43), "unknown")
unittest.main(argv=["x"], exit=False)
Running it prints Ran 1 test and OK. Code 43 (encapsulated interface transport, used for device identification) is deliberately "unknown" rather than "read", because the tool's author has not reasoned about it.
The test list is also the specification a reviewer signs: classify(3) is read, classify(16) is a write, classify(8) and classify(43) are unknown. Maya's original bug would have been caught by the single assertion that code 8 returns "unknown"; that assertion is now in the list. When a new function code appears in an LDHP capture, the first change is a new assertion with the expected answer, written before the classifier is edited.
Worked example (synthetic LDHP)
Maya's first classifier returned "read" for anything not in the write set. A reviewer asked what code 8 (diagnostics) would return. The answer — "read" — was wrong: some diagnostic sub-functions restart communications. She changed the default to "unknown" and added the test above.
Synthetic case — Lakeside District Heating Plant. The classifier is a teaching tool for offline captures, not an enforcement device.
8.4 Logging decisions for review
Each decision the tool makes should be written with the input that produced it, the rule that fired and the script version, for example fc=8 -> unknown (rule: default) v=3f2a91c. A control engineer reviewing a week of captures can then filter on "unknown" and decide, case by case, whether the classifier needs a new rule. This record is also what lets you show later that a surprising result came from a known version of the code and not from an edit made afterwards.
Errors and limits
Tests prove the cases you wrote, not the ones you forgot. Record the script's version (a git commit hash) next to every result you publish.
Exercises
- EX022 — Classifying diagnostics. What does
classify(8)return in the final version, and why is that preferable? Rubric: 3 = "unknown"; diagnostics may change device state, so do not assume read; 2 = value only; 1 = "read"; 0 = none. - EX023 — A missing test case. Write one additional assertion that would have caught Maya's original bug. Rubric: 3 = assertEqual(classify(8), "unknown") or similar non-read code; 2 = tests a write code; 1 = tests code 3; 0 = none.
- EX024 — Reproducing an old result. Name two things to record with a published analysis so it can be reproduced six months later. Rubric: 3 = script commit hash and exact input file hash/date; 2 = one; 1 = "save the output"; 0 = none.
Synthesis
Reviewable OT tools are pure, tested, fail closed and versioned — properties that let a sceptical engineer trust their output.
Continue studying — chapter 08 of 40
← Chapter 07 (draft) · Course chapter index · Chapter 09 (draft) →