Creating a Product¶
A product is the unit of test configuration in Frasy. It bundles the environment declaration (hardware setup, UUT count, execution policy) with the test sequences that validate a specific PCBA. This page walks through creating one from scratch.
Directory Layout¶
Frasy discovers products by scanning the lua/user/ directory at runtime. Any subdirectory
containing an environment.lua file is treated as a product. The directory name becomes the
product name shown in the UI's product dropdown.
src/lua/user/
<product-name>/
environment.lua ← required
tests/
*.lua ← one or more test files (loaded automatically)
Tip
You can organize test files however you like — by phase (power_on.lua,
functional.lua, teardown.lua), by board subsystem, or any other grouping that makes sense
for your workflow. Frasy loads all .lua files found under tests/ recursively.
Step 1: Create the Directory¶
From your application root:
Step 2: Write environment.lua¶
This file declares the static configuration of your test fixture. It must call
Environment.Make() with a configuration function.
Minimal Environment (No Hardware)¶
If you just want to define logic-only tests (no instrumentation boards):
With Instrumentation Boards¶
If your fixture uses physical hardware boards for measurement and stimulus:
Environment.Make(function()
Environment.ScriptVersion("1.0.0")
Environment.Uut.Count(1)
Environment.Ib.Add(DAQ:New { name = "daq", nodeId = 2 })
Environment.Ib.Add(PIO:New { name = "pio", nodeId = 3 })
end)
Once registered, they are accessible at runtime through Context.map.ibs.<name>.
Multi-UUT Environment¶
For fixtures that test multiple boards in parallel:
Environment.Make(function()
Environment.ScriptVersion("1.0.0")
Environment.Uut.Count(4)
Environment.Ib.Add(DAQ:New { name = "daq", nodeId = 2 }) -- shared by all UUTs
Environment.SetExecutionPolicy(ExecutionPolicy.parallel)
end)
Mapping Test Points to Hardware Resources¶
Most test fixtures need to route logical test points on the UUT (e.g., "VCC", "SDA") to physical
hardware resources (e.g., a DAQ multiplexer channel). You can store this mapping directly in
Context.values so that test scripts reference named test points without hard-coding hardware
details.
A common pattern is to build a route table in Context.values:
Environment.Make(function()
Environment.ScriptVersion("1.0.0")
Environment.Uut.Count(1)
Environment.Ib.Add(DAQ:New { name = "daq", nodeId = 2 })
Context.values.route = {
vcc = DAQ.RoutingPointsEnum.MUX1_A0,
gnd = DAQ.RoutingPointsEnum.MUX1_A1,
sda = DAQ.RoutingPointsEnum.MUX2_A0,
scl = DAQ.RoutingPointsEnum.MUX2_A1,
led_out = DAQ.RoutingPointsEnum.MUX3_A0,
}
end)
In your tests, you then reference the named route instead of raw hardware constants:
Test("Supply Voltage", function()
local daq = Context.map.ibs.daq --[[@as DAQ]]
local v = daq:MeasureVoltage(Context.values.route.vcc)
Expect(v.average, "VCC"):ToBeInPercentage(3.3, 5.0)
end)
This separation means that if a test point moves to a different mux channel (due to a fixture
revision), you only update the route table in environment.lua — no test files need to change.
You can also store non-routing values such as calibration constants, expected firmware versions, or fixture-specific thresholds:
Context.values.expected_fw_version = "2.4.1"
Context.values.calibration = {
voltage_offset = 0.012,
current_gain = 1.003,
}
Environment API Reference¶
| Function | Purpose |
|---|---|
Environment.ScriptVersion(version) |
Script version string (appears in reports) |
Environment.Uut.Count(n) |
Number of UUTs tested in parallel |
Environment.Ib.Add(board) |
Register an instrumentation board |
Environment.SetExecutionPolicy(policy) |
ExecutionPolicy.parallel (default) or ExecutionPolicy.sequential |
Environment.Team.Add(leader, ...) |
Group UUTs into a team (for team-based synchronization). See Teams. |
Environment.UutValue.Add(key) |
Declare a per-UUT value (e.g., different test point routing per slot) |
Environment.SetOnReport(fn) |
Hook called on report generation to transform the report |
Environment.SetOnReportInfo(fn) |
Hook to inject extra metadata into reports |
Step 3: Write Test Files¶
Create one or more .lua files in the tests/ directory. Each file defines sequences and tests:
-- src/lua/user/my_board/tests/power_on.lua
Sequence("Power On", function()
Requires(Sequence():ToBeFirst())
Test("Supply Voltage", function()
local daq = Context.map.ibs.daq --[[@as DAQ]]
local v = daq:MeasureVoltage(Context.values.route.vcc)
Expect(v.average, "VCC"):ToBeInPercentage(3.3, 5.0)
end)
Test("Current Draw", function()
local daq = Context.map.ibs.daq --[[@as DAQ]]
local i = daq:MeasureCurrent(Context.values.route.supply)
Expect(i.average, "Idle Current"):ToBeLesser(0.100)
end)
end)
-- src/lua/user/my_board/tests/functional.lua
Sequence("Functional", function()
Requires(Sequence("Power On"):ToPass())
Test("LED Toggle", function()
local pio = Context.map.ibs.pio --[[@as PIO]]
pio:SetOutput(1, true)
SleepFor(100)
local state = pio:ReadInput(2)
Expect(state, "LED Feedback"):ToBeTrue()
end)
end)
-- src/lua/user/my_board/tests/teardown.lua
Sequence("Teardown", function()
Requires(Sequence():ToBeLast())
Test("Power Off", function()
local pio = Context.map.ibs.pio --[[@as PIO]]
pio:SetOutput(1, false)
end)
end)
Step 4: Build and Run¶
Rebuild your application (or just let CMake's refresh_lua_user target sync the files):
Launch the application. Your new product should appear in the product dropdown. Select it and press Run.
Note
If you modify Lua files while the application is running, press F9 (or the "Reload" button) to re-scan products and reload the environment.
Per-UUT Values¶
When testing multiple UUTs on the same fixture, each UUT might be wired to a different test point
route. Use Environment.UutValue.Add() to declare per-UUT constants:
local MyDaq = DAQ:New { name = "daq", nodeId = 2 }
Environment.Make(function()
Environment.ScriptVersion("1.0.0")
Environment.Uut.Count(2)
Environment.Ib.Add(MyDaq)
Environment.UutValue.Add("vcc_route")
:Link(1, DAQ.RoutingPointsEnum.MUX1_A0)
:Link(2, DAQ.RoutingPointsEnum.MUX1_A1)
end)
In tests, access the value for the current UUT through Context.values:
Test("VCC", function()
local route = Context.values.vcc_route
local v = Context.map.ibs.daq:MeasureVoltage(route)
Expect(v.average, "VCC"):ToBeInPercentage(3.3, 5.0)
end)
Report Hooks¶
Frasy generates test reports in multiple formats (JSON, Key-Value, Markdown, PDF) after each run. Two hooks let you inject extra information or transform the report before it is saved to disk.
Environment.SetOnReportInfo(fn)¶
Called during report generation to inject extra metadata into the report header. The function receives no arguments and must return a table of key-value pairs. These appear alongside the built-in info fields (date, operator, serial, version, etc.) in all report formats.
Use cases:
- Identifying the physical fixture or station that produced the report
- Recording firmware versions read from the DUT during the test
- Tagging reports with a production lot or work order number
- Adding traceability fields required by your quality management system
Environment.SetOnReportInfo(function()
return {
fixture_id = "FIX-001",
station = "Station A",
work_order = "WO-2026-1234",
fw_version = Context.values.detected_fw_version or "unknown",
}
end)
In the generated JSON report, these appear as top-level fields in the info object. In
Key-Value reports, they are printed as <key>: <value> lines in the header section.
Environment.SetOnReport(fn)¶
Called after the full report table has been assembled but before it is serialized to JSON on disk. The function receives the complete report as a mutable table. You can add, remove, or modify any field. The report is passed by reference — mutations apply directly.
Use cases:
- Stripping internal fields that should not appear in customer-facing reports
- Adding computed summaries (e.g., total pass rate, yield statistics)
- Renaming or restructuring fields to match an external system's expected schema
- Injecting a digital signature or checksum for tamper detection
- Filtering out expectations marked as debug-only
Environment.SetOnReport(function(report)
-- Add a computed yield field
local total = 0
local passed = 0
for _, seq in pairs(report.sequences) do
for _, test in pairs(seq.tests or {}) do
total = total + 1
if test.pass then passed = passed + 1 end
end
end
report.info.yield = string.format("%.1f%%", (passed / total) * 100)
-- Remove a debug-only sequence from the report
report.sequences["Debug Checks"] = nil
end)
Warning
The report table is passed by reference. If your onReport function errors, the report is
still saved in its current state (the error is logged but does not block report generation).
Test your hook thoroughly to avoid corrupted reports.
Checklist¶
Before running your product for the first time, verify:
- [ ]
src/lua/user/<name>/environment.luaexists and callsEnvironment.Make() - [ ] At least one test file exists in
src/lua/user/<name>/tests/ - [ ] Each test file defines at least one
Sequencecontaining at least oneTest - [ ] If using IBs, they are instantiated and registered with
Environment.Ib.Add() - [ ] EDS files are accessible at the paths declared by your boards
- [ ] The project builds without errors
Next Steps¶
- Teams — grouping UUTs for coordinated execution
- Lua Setup — environment and test scripting overview
- Expectations — full list of matchers
- Requirements — ordering and conditional execution
- Custom Instrumentation Boards — defining your own board types