Testing
Three levels of tests, from seconds to about half an hour.
Unit tests and lint
python -m unittest discover -s tests
ruff check --select F,E9 .
The unit tests cover the engineering computations, checked against the books' worked examples: the tolerance stack-up (Fischer’s pin and ground plate, Drake’s MRSS factor, Cogorno’s fastener and boundary formulas), ISO 286 limits and fits (against the ISO 286-1 formulas and published values) and ISO 2768, the PMI semantics (ISO 8015 and ASME Y14.5 checks, inspection plan, gauge sizes), the release package manifest and its verification, the clearance classification, the AP242 mapping of the PMI (and the correction of OpenCASCADE’s IT grade), sheet metal bend allowance and flat length (Shih’s actuator bracket), pin location (Drake ch. 24, Fischer’s lever), composite position frames and the pattern evaluation (its fit checked against a brute-force search), measurement capability (Drake’s gauge study example, 42.6 %; TUR and ISO 14253-1 acceptance limits), and the title block values and revision block.
They cover the selection rules too: the scope verdict on occurrence paths (a look-alike sibling outside the work part is refused), collector counts and refusal reasons, the tangent and connected chains, and the select-through choice.
They cover the command controller’s decisions as well (nxstyle/command_core.py). Typed lengths are read with their units and the field’s decimal separator; 3.175 in a field whose separator is a comma is refused, not guessed. Every Extrude Boolean is either built (None for a new body, Unite, Subtract) or refused with a reason (Intersect), and the Boolean NX would infer is checked.
So are Revolve’s: its Boolean, its axis (a vector through the origin is the Body’s axis, elsewhere a datum line) and its Start and End angles, refused where FreeCAD cannot revolve them.
A section’s picked curves must close: the impeller casing’s three loops do, a loop with a curve missing is open, and the whole casing branches.
Picked geometry is matched to the sketch shape’s edge names, which follow another order.
They cover ISO 10303 too: Part 21 strings, reals, binaries, comments, line breaks inside tokens and several data sections (with the standard’s own examples), the EXPRESS reader and the checker (attribute order of internal mapping, * for re-derived attributes, ONEOF, abstract entities, SELECT and aggregate values), the bundled dictionary against ISO 10303-108 Cor.1 annex F, and a parametric model written, checked without an error, read back and compared, including rules that must catch broken data; a Part 112 sketch of every kind of command evaluated against hand-computed geometry, and sketches written as Part 112 commands that reproduce them; lengths and angles in inches and degrees; the application protocol named in a header; and the product structure of an assembly with its quantities. The EXPRESS interpreter has its own tests: the operators and built-in functions with indeterminate values, a schema FUNCTION with loops and a SET local, derived and inverse attributes, SELECT types in TYPEOF, the complex entity constructor, and a small schema whose every kind of rule (an entity’s WHERE rule, a defined type’s, an aggregate bound given by an attribute, UNIQUE and a global RULE) is broken once and met once, with a rule left undecided when an unknown entity may use the instance.
In a real FreeCAD, freecadcmd tests/fc_parametric_step.py exports a constrained, parameter-driven plate and a CSG part as STEP with parameters, imports them into new documents and compares the solids, before and after the same parameter change (NXSTYLE_OUT keeps the files).
They cover the Carbon client (Manufacturing with Carbon): every flow against a stand-in Carbon server on this computer that checks each request against Carbon’s published OpenAPI schemas, with a negative control (a request the schema rejects is refused); publishing a part with its model, documents and inspection plan, an assembly’s BOM from its STEP, a second publish that writes nothing, a review that writes nothing, a BOM change on an Active method going to a new Draft, a new revision created inactive, an unreleased or tampered package refused, the API key’s sources and that it is never printed.
In a real FreeCAD, freecadcmd tests/fc_carbon.py builds an assembly of links to other documents (a link array among them), reads its BOM, makes its release package and publishes it to the stand-in server.
They also cover the launcher: the profile folders and their reset, refusing a profile inside or above the normal FreeCAD folder, the environment and command it builds, which Strata, Katydid, SheetMetal, LCInterlocking and DFM it loads (DFM only with its OCP, whose folder goes first, installed by a pinned --no-deps pip command) and where it reads nxstyle.json, that every application’s ribbon has the Application tab, that the Laser Cut ribbon names only commands LCInterlocking registers, and the memory ceiling (a runaway is stopped, a second concurrent run refused).
FreeCAD is not needed.
GitHub Actions runs both, and checks that the ribbon files are valid JSON, on every push (Windows and Ubuntu).
Self-test
python launch_nx.py --smoke-test out
This starts FreeCAD with NX-Style in a throwaway profile and drives the real UI with synthetic mouse and key events. It ends with the count of checks passed, failed and skipped (171 checks on FreeCAD 1.1.4 with Strata, Katydid, SheetMetal and LCInterlocking, 2026-10-27). Among them:
-
every key in Keyboard shortcuts reaches its command through Qt’s real shortcut system, with no other action holding the same key. This checks the dispatch: the test stops the command from then running. Ctrl+Shift+B is pressed for real as the positive control, and a letter key typed into the Command Finder must stay text;
-
MB2 rotate, pan and zoom, MB2 click = OK, the MB3 hold radial menu;
-
the ribbon, the Type Filter, the Resource Bar and no empty tab strip over the Top Border Bar;
-
every button of the Modeling, Drafting and Sketch ribbons is enabled whenever its FreeCAD command is active, FreeCAD group commands (Line, Radius / Diameter) included; Radius / Diameter clicked on a selected arc makes a Radius constraint;
-
the window-size matrix (below) for Modeling, Drafting and Pre/Post: nothing clipped and every command reachable at 1366x768 and 1920x1080, each at 100, 125, 150 and 200 % scaling;
-
a window too short for the ribbon minimises it, a tab then drops its page down, and the ribbon comes back when the window is taller;
-
a group set to Always Collapsed stays so after the ribbon is rebuilt and is saved in
nxstyle.ribbon.json, and its menu entry runs the command; -
the guided Pre/Post ribbon: the Home tab is the analysis workflow in order, and an analysis with a material and a fixed support ticks steps 1 to 3 and makes the mesh the next step;
-
metric units even when the profile was set to US customary;
-
Strata starts and sits with the navigators;
-
the DFM Workbench’s commands under Analysis › Molded Part Validation, enabled, with NX names and found by the Command Finder; Moldability Validation clicked on a stepped block flags its one sharp inside edge as a Sharp Internal Corner, and on the same block with that edge filleted lists the rule as Passed (with
--no-dfm, or before--install-dfm-deps, the group is checked to be absent instead); -
the PLM tab’s Manufacturing group; Review, Publish to Manufacturing, Manufacturing Status and Test Connection through their dialogs, against the stand-in Carbon server of the unit tests;
-
the Algorithmic Modeling application opens Katydid’s canvas, floating, with Canvas, Bake and the Application tab on the ribbon; Canvas is on the Tools tab before Katydid’s workbench has started; the selected category tab has a readable label; the canvas stays open after switching back to Modeling;
-
the eight PMI model views look as FreeCAD’s own standard views do, a new PMI goes into the work view, switching views hides the others' PMI, and Display in Views copies one;
-
a plate with PMI written as STEP AP242: datums, the H7 and g6 classes with their right IT grades, position with Ⓜ and flatness in the file, and OpenCASCADE reading back the dimensions and tolerances; the file imported again gives the same PMI;
-
a linked part inside a moved assembly is where the assembly puts it for volume claims and clearance, and its hidden source is not counted;
-
selection (Selecting), on a nested assembly where two links to the same part sit one above the other and the work part is the lower one:
-
QuickPick lists both look-alike occurrences, greys the nearer sibling with its reason, and picks exactly the work part’s occurrence;
-
a pick on the sibling is refused and the cue line says why;
-
an MB1 click selects through the sibling to the work part’s occurrence, and with Entire Assembly the same click takes the sibling (the control);
-
resting the pointer there shows the three-dot cursor and the next click opens QuickPick (no dots over empty space), holding MB1 still opens it too; Within Work Part Only refuses a component’s geometry;
-
the Part Navigator names the work part and the selected occurrence;
-
-
Edge Blend’s collector, command first:
Select Edge (0)and OK refused, a Type Filter change during the command does not replace its gate, Tangent Curves collects the line–arc–line chain of a rounded corner, refused picks carry their reasons, and Cancel brings back the previous gate with the filter chosen meanwhile; -
Edge Blend, selection first: an unusable face is refused and deselected and the edge reaches FreeCAD’s Fillet; with only usable edges the Fillet opens at once;
-
a Type Filter change while a FreeCAD-style dialog holds its own gate waits, and the Type Filter’s gate comes back when the dialog closes;
-
Extrude started by its key (X) with a sketch selected and the Type Filter on Edge: the panel opens, the filter is lifted and greyed out while it runs and back after Cancel; a start that fails ends the command as failed with the reason, and Extrude then runs again;
-
sections on the impeller casing sketch of tutorial AG (19 curves, three loops that share curves, two overlapping lines), with the Curve Rule on the Top Border Bar:
-
Revolve from the ribbon with nothing selected, the 10 wall curves picked in the graphics window, XC through the origin, End 180: the Profile is those curves and the solid the Part API’s; with 9 picked the section is open and OK unavailable, and Ctrl+MB1 takes a curve out;
-
the 10 lip curves preselected, then X, Boolean None, 0.5 in Reversed: a new Body whose binder holds only those curves, extruded along −Z;
-
the 4-curve flange, three sides with one Connected Curves pick and one Single Curve pick, revolved and united with the wall;
-
Esc in a Revolve about a vector through (0, 10, 0) and in an Extrude of picked curves leaves the objects and the undo list as they were;
-
-
Move Object by Ctrl+T at once after the selection: the book’s L-bar block (4.7.3) lifted 200 in along ZC with its placement, bounding box, one undo step and Undo checked, and the same move along YC failing the ZC position check; the 4.6.9 cylinder clicked in the graphics window and moved 5 in along XC; Copy Original making two body copies at +1 and +2 steps, removed again by one Undo; Angle with Apply, then Point to Point; Cancel and Esc leaving the body and the undo list as they were; Distance 0 warning that nothing moves;
-
the curve rules on a rectangle sketch (Connected takes 4 sides, Tangent stops at the corners); the Part Navigator marks the active body, a suppressed feature and a failed one;
-
a composite position frame, its two-row label and the evaluation of a measured pattern; a gauge study on a PMI size; Populate Title Block from part attributes onto an ISO 5457 sheet, with its revision block;
-
broken references are reported, not papered over:
-
a diameter whose face became a plane is invalid, shows its error, and is kept out of the inspection plan, the stack-up and the STEP export, then valid again when repaired;
-
a stack-up whose PMI was deleted gives no result;
-
clearance results are out of date after a part moves;
-
a pair rule naming a label two parts share is not applied;
-
-
the geometry comparison the tutorials use tells a solid from a different one of the same volume;
-
the status bar names NX-Style’s mouse handling rather than FreeCAD’s Blender base style alone.
A check that needs something this FreeCAD build lacks (pythonocc for AP242, the FEM workbench, QtTest) is counted as skipped, never as passed; --require-all makes a skipped check fail the run.
It writes out/smoke.json and screenshots.
While developing, NXSTYLE_SMOKE_ONLY=s_move,s_thread (step names from `smoke.py’s plan) runs only those steps after Strata’s start; a full run is still the check.
Window sizes and display scaling
The self-test resizes the main window to each cell of a matrix and measures widget rectangles, not screenshots:
| Screen | 100 % | 125 % | 150 % | 200 % |
|---|---|---|---|---|
1366x768 |
1366x689 |
1092x535 |
910x433 |
683x305 |
1920x1080 |
1920x1001 |
1536x785 |
1280x641 |
960x461 |
The window sizes are in Qt’s logical pixels: the screen divided by the scale, less 48 px for the Windows taskbar and 31 px for the title bar. Qt lays a window out in logical pixels and scales fonts and icons with the display, so a logical size stands for that resolution and scale. The window is resized twice, because a short window minimises the ribbon, which lowers the window’s minimum height; a maximised window is then given the screen’s work area again. A screen smaller than the cell limits the height. On a 1920x1080 screen at 125 %, for instance, the 1920x1001 cell is measured 844 px high, and the result notes it. This makes the vertical checks harder and leaves the ribbon’s width the same; a width that differs fails the cell. When the ribbon is minimised (the 683x305 cell), each tab is measured in the drop-down a click on it shows, and the drop-down must lie inside the window. For each cell and each ribbon tab it checks that:
-
every shown ribbon control and group label lies wholly inside the ribbon page, and no page needs sideways scrolling;
-
the File button, Quick Access Toolbar, tabs, Command Finder and collapse button lie inside the tab row, and tabs or Quick Access buttons that do not fit have their scroll or extension buttons;
-
every command of the tab is reachable: on a button that is fully shown, or in the menu of a collapsed group or of the More button, which must itself be fully shown;
-
the application’s essential commands (
"essential"in its ribbon file) are reachable, and in Modeling at 1920x1080 and 100 % they are one click away; -
the ribbon and bars lie inside the window, and the graphics area keeps at least 80 px of height.
Two negative controls show that the checks can fail: with the reduction switched off, the narrowest Modeling cell is reported as clipped or scrolling; and a collapsed group whose menu has lost a command is reported as unreachable.
The narrowest cell must also have collapsed some groups, so that the matrix exercises the reduction and does not merely find wide windows.
The results of every cell are in smoke.json under info.
The matrix does not change Qt’s real scale factor during a run. Rounding of fractional scales and the platform’s font hinting can differ by a pixel or so from a real display at that scale.
What the launcher accepts
A result file counts only when it is this run’s and the run finished.
The launcher passes each run a new ID, and the result (written atomically by nxstyle/harness_result.py) must carry that ID, the schema nxstyle-harness/1, status complete, and every planned step listed as run.
A tutorial that fails part way lists the steps it then skipped.
So a passing smoke.json left in the folder by an earlier run cannot pass a run that crashed, and neither can {}, empty lists, a truncated file or a run that stopped half way.
A FreeCAD that exits with an error after writing a good result fails too.
tests/test_harness_result.py covers each of these, through launch_nx.main with the FreeCAD run replaced by a stub.
Tutorial replay
python launch_nx.py --tutorial out
The replay works through the tutorials the way the books tell an NX user to: the same keys, the NX-named ribbon buttons, MB1 clicks on sketch points in the graphics window, MB2 to end tools and accept dialogs. On the Katydid canvas (tutorials O to R) it double-clicks the empty canvas and types each component’s name, drags wires from grip to grip (Shift adds), sets Graft, Flatten and input values from the grips' right-click menu, and types slider values into the slider’s dialog; after each step it saves a screenshot with the floating canvas laid over the lower half of the graphics window. Dimension values are applied through FreeCAD’s API, the equivalent of typing them into Rapid Dimension; everything else is real UI input.
Each feature is checked by its volume against the same operation done directly with FreeCAD’s Part API.
Volume alone does not prove the shape: a slot cut in the wrong place has the same volume.
Tutorial A therefore also compares the part with a reference solid built with the Part API: their symmetric difference and their bounding boxes must agree (nxstyle/geomcheck.py). The boxes are exact ones (optimalBoundingBox without the display mesh): Shape.BoundBox is the box of the mesh once the 3D view has drawn a curved solid, 0.02 to 0.09 mm off for a disc of R60.
The final part must also be drawn in the graphics window. That is an occupancy check (something is drawn), not a check of the image.
Save steps press the real Ctrl+S and check that the file was rewritten; only a new part’s first file name is given by script, because the replay cannot drive the system file dialog.
Before the key the part’s Comment gets a marker unique to that save; the step passes when the file on disk holds the marker and the part is unmodified. The file’s time is no witness: FreeCAD saves to a temporary file and renames it over the old one, and in replays on Windows the new file kept the replaced file’s last-write time in 4 of 10 saves, with the change in it.
Tutorials S to X check engineering results: the stack-up against Fischer’s figures, the fit against ISO 286, the STEP file’s PMI and what OpenCASCADE reads back, the inherited dimensions on the sheet, the clearance kinds and penetration depth, the release states in Strata and the release packages against their manifests, the flat pattern against Shih’s hand calculation, and the pin location against Drake’s formulas.
Dialogs that Strata opens are answered by the replay the way a user fills them in.
Tutorials AA to AC check the composite frame’s plan rows and gauges, the evaluation of measured holes, the TUR and acceptance limits, Drake’s gauge study result, and the title block against the Strata release.
Tutorial AE is the acceptance case for the command controller (Extrude). It does New, Sketch, then Extrude four times through the real UI: None (a new body), Unite, Subtract chosen in the Boolean list, and a separate new body.
It also checks what must not happen:
-
choosing Intersect changes nothing and the dialog gives the reason;
-
a Unite that misses the part leaves OK unavailable, and MB2 does not close the dialog;
-
changing the Boolean adds no undo step.
Then it edits the first extrude. From the Part Navigator’s MB3 menu it previews a new length and presses Cancel. The value must come back, with no undo step added and nothing left to recompute. It edits again from the ribbon and presses OK, which must add one undo step. Ctrl+Z twice takes back the edit and then the whole New Body extrude, and Ctrl+Y twice restores both. Extrude (Subtract) is then started from the Command Finder and cancelled at the plane pick, which must leave no object and no undo step. Finally it saves, reopens and edits again, and Esc cancels.
The radial menu’s Edit Parameters runs the same command as the ribbon’s, but the replay does not drive the radial menu itself.
Every solid is compared with nxstyle/geomcheck.py, and every command must add exactly one undo step.
--tutorials G,H picks some; N runs M first, V runs T first, AC and BC run T and V first.
A run may take 5 minutes plus 5 per tutorial chosen before it counts as timed out; the canvas tutorials O to R take about 20 minutes together.
The replay writes out/tutorial.json, a progress.log and screenshots.
Exit code 1 means a step FAILED.
The same, differs and missing outcomes are the replay’s own judgement of each step against the book; they are not an independently measured equivalence with NX.
Hidden desktop and memory ceiling
On Windows both runs use a hidden desktop, so nothing appears on your screen; --visible shows FreeCAD instead.
Every run is capped at 4 GB of memory (--max-memory-gb) inside a Windows job object.
FreeCAD’s allocations beyond the ceiling are refused and the run ends with exit code 4, so a runaway cannot take the machine down.
Run one at a time.
| Exit code | Means |
|---|---|
0 |
Passed |
1 |
A check or step failed |
2 |
No result: FreeCAD crashed or NX-Style failed to load |
3 |
Timed out |
4 |
Stopped at the memory ceiling |
5 |
Refused to start, for example because another test run is going |
6 |
The result is not this run’s, or is incomplete, empty or unreadable |
7 |
FreeCAD exited with an error code |
Testing a second start
Each run uses a fresh throwaway profile.
To test what a second start sees, give both runs the same --profile DIR; it is kept between runs.
python launch_nx.py --smoke-test out1 --profile p
python launch_nx.py --smoke-test out2 --profile p
From results to the docs
The tutorial pages' step tables come from docs/results/tutorials.json: a replay’s steps and the FreeCAD version, nothing else.
After a full replay that should be published:
python tools/gen_docs.py --results out/tutorial.json
It refuses results that name a folder in your home directory, since they are published.
python tools/tutorial_table.py out/tutorial.json prints the result counts per tutorial and updates the table in docs/TUTORIAL-REPLAY.md.
See Writing these docs for building the site.