BDD framework: unit, integration, Gherkin
PatLang's own self-hosted Gherkin-style BDD framework (lib/test.patlang: t_init/check/step/run_feature_tagged), running against the point of sale: unit assertions, event-driven integration checks, and a Gherkin feature whose steps dispatch through apply. The same framework drives PatLang's own self-hosted regression suites (self_hosting/reflect_transpile_selftest.patlang, self_hosting/examples/pos_tests.patlang), each wired into cargo test via a thin Rust cucumber step that just asserts on this framework's own ALL TESTS PASSED output.
PatLang source
# =============================================================================
# Test framework (Stage 1 dialect): unit assertions plus a Gherkin-style
# feature runner. Step definitions are registered in the object store keyed
# by their text; features are plain text dispatched line by line, so the
# same framework covers unit, integration, and behaviour tests.
# =============================================================================
make a function called t_init returns done
set_var("t_pass", 0)
set_var("t_fail", 0)
set_var("t_tagfilter", "")
set_var("t_pending_tags", "")
set_var("t_skipping", 0)
return true
end
make a function called contains_text takes hay, needle returns r
if needle.length > hay.length then
return false
end
let i = 0
while i <= hay.length - needle.length do
if substr(hay, i, needle.length) == needle then
return true
end
let i = i + 1
end
return false
end
make a function called check takes label, actual, expected returns ok
if actual == expected then
set_var("t_pass", get("__vars", "t_pass") + 1)
print(" ok: " + label)
return true
else
set_var("t_fail", get("__vars", "t_fail") + 1)
print(" FAIL: " + label + " (got " + actual + ", want " + expected + ")")
return false
end
end
make a function called t_report returns ok
let p = get("__vars", "t_pass")
let f = get("__vars", "t_fail")
print("tests: " + p + " passed, " + f + " failed")
if f == 0 then
print("ALL TESTS PASSED")
return true
else
print("TESTS FAILED")
return false
end
end
# ---- Gherkin runner ----
# Register a step: step("a fresh till", "st_fresh_till")
make a function called step takes text, fname returns done
new("Step", text)
send(text, "set", "fn", fname)
return true
end
make a function called starts_with takes s, prefix returns r
if s.length < prefix.length then
return false
end
return substr(s, 0, prefix.length) == prefix
end
make a function called trim_left takes s returns out
let i = 0
let scanning = true
while (i < s.length) and scanning do
let c = char_code(s, i)
if (c == 32) or (c == 9) then
let i = i + 1
else
let scanning = false
end
end
return substr(s, i, s.length - i)
end
# Strip a Gherkin keyword; returns the step text or "" if not a step line
make a function called step_text takes line returns out
if starts_with(line, "Given ") then
return substr(line, 6, line.length - 6)
end
if starts_with(line, "When ") then
return substr(line, 5, line.length - 5)
end
if starts_with(line, "Then ") then
return substr(line, 5, line.length - 5)
end
if starts_with(line, "And ") then
return substr(line, 4, line.length - 4)
end
return ""
end
# Run only scenarios whose preceding @tag line contains `tag` ("" = all)
make a function called run_feature_tagged takes feature, tag returns ok
set_var("t_tagfilter", tag)
return run_feature(feature)
end
make a function called run_feature_file takes path returns ok
return run_feature(read_file(path))
end
make a function called run_feature takes feature returns ok
let h = str_intern(feature)
let n = sc_len(h)
let i = 0
let line = sb_new()
while i <= n do
let c = sc_code(h, i)
if (c == 10) or (c == -1) then
let raw = trim_left(sb_str(line))
let line = sb_new()
if starts_with(raw, "@") then
set_var("t_pending_tags", raw)
end
if starts_with(raw, "Feature:") then
print(raw)
end
if starts_with(raw, "Scenario:") then
let filter = get("__vars", "t_tagfilter")
let tags = get("__vars", "t_pending_tags")
set_var("t_pending_tags", "")
if filter then
if tags then
if contains_text(tags, filter) then
set_var("t_skipping", 0)
print(raw + " [" + tags + "]")
else
set_var("t_skipping", 1)
print(raw + " [skipped: needs " + filter + "]")
end
else
set_var("t_skipping", 1)
print(raw + " [skipped: needs " + filter + "]")
end
else
set_var("t_skipping", 0)
print(raw)
end
else
let text = step_text(raw)
if (text != "") and (get("__vars", "t_skipping") != 1) then
let fname = get(text, "fn")
if fname then
apply(fname)
else
set_var("t_fail", get("__vars", "t_fail") + 1)
print(" FAIL: undefined step: " + text)
end
end
end
let i = i + 1
else
if c == 13 then
let i = i + 1
else
sb_push(line, sc_char(h, i))
let i = i + 1
end
end
end
return true
end
# =============================================================================
# Point-of-sale library (Stage 1 dialect): event-driven checkout combining
# events (scan/pay), OO (product catalog in the object store), and logic
# (dairy facts drive a discount rule). Prices are integer pence.
# =============================================================================
make a function called pos_setup returns done
new("Product", "apple")
send("apple", "set", "price", 30)
new("Product", "banana")
send("banana", "set", "price", 25)
new("Product", "milk")
send("milk", "set", "price", 120)
fact("dairy", "milk", "yes")
fact("dairy", "cheese", "yes")
set_var("total", 0)
set_var("items", 0)
set_var("receipt", "")
goal("serve_customer", "till-1")
return true
end
make a function called pos_scan takes item returns price
let price = get(item, "price")
if not price then
print(" unknown item: " + item)
return 0
end
if query("dairy", item, 0) > 0 then
# dairy discount: 10% off
let price = price * 9 / 10
end
set_var("total", get("__vars", "total") + price)
set_var("items", get("__vars", "items") + 1)
set_var("receipt", get("__vars", "receipt") + " " + item + " " + price + "p\n")
return price
end
make a function called pos_total returns t
return get("__vars", "total")
end
make a function called pos_pay takes tendered returns change
let total = get("__vars", "total")
let change = tendered - total
print("--- RECEIPT ---")
print(get("__vars", "receipt") + " items: " + get("__vars", "items"))
print(" total: " + total + "p, tendered: " + tendered + "p, change: " + change + "p")
return change
end
when scan do
pos_scan(event_data)
end
when pay do
pos_pay(event_data)
end
# Test suite for the point-of-sale library (concatenate after lib/test.patlang
# and lib/pos.patlang). Shows unit tests, event-driven integration tests, and
# a Gherkin feature exercising the same code.
t_init()
# ---- unit tests ----
print("unit: catalog and pricing")
pos_setup()
check("apple price", get("apple", "price"), 30)
check("milk is dairy", query("dairy", "milk", 0), 1)
check("banana is not dairy", query("dairy", "banana", 0), 0)
check("scan returns discounted dairy price", pos_scan("milk"), 108)
check("scan returns full price", pos_scan("banana"), 25)
check("running total", pos_total(), 133)
# ---- integration test: the same flow through events ----
print("integration: checkout via events")
pos_setup()
emit("scan", "apple")
emit("scan", "apple")
emit("scan", "milk")
check("event-driven total", pos_total(), 168)
check("change from 2 pounds", pos_pay(200), 32)
# ---- Gherkin feature ----
make a function called st_fresh_till returns done
pos_setup()
return true
end
make a function called st_scan_two_apples returns done
emit("scan", "apple")
emit("scan", "apple")
return true
end
make a function called st_scan_milk returns done
emit("scan", "milk")
return true
end
make a function called st_total_is_168 returns done
return check("the total is 168p", pos_total(), 168)
end
make a function called st_dairy_discount_applied returns done
return check("dairy discount applied", get("milk", "price") - 12, 108)
end
step("a fresh till", "st_fresh_till")
step("the customer buys two apples", "st_scan_two_apples")
step("the customer buys a bottle of milk", "st_scan_milk")
step("the total is 168 pence", "st_total_is_168")
step("the dairy discount was applied", "st_dairy_discount_applied")
let feature = "Feature: checkout
@smoke @till
Scenario: apples and discounted milk
Given a fresh till
When the customer buys two apples
And the customer buys a bottle of milk
Then the total is 168 pence
And the dairy discount was applied
@audit
Scenario: nightly stock audit
Given a fresh till
Then the total is 168 pence
"
# run only @smoke scenarios: the @audit scenario (which would fail) is skipped
run_feature_tagged(feature, "@smoke")
t_report()
(not run yet)
Native run on the build machine:
unit: catalog and pricing ok: apple price ok: milk is dairy ok: banana is not dairy ok: scan returns discounted dairy price ok: scan returns full price ok: running total integration: checkout via events ok: event-driven total --- RECEIPT --- apple 30p apple 30p milk 108p items: 3 total: 168p, tendered: 200p, change: 32p ok: change from 2 pounds Feature: checkout Scenario: apples and discounted milk [@smoke @till] ok: the total is 168p ok: dairy discount applied Scenario: nightly stock audit [skipped: needs @smoke] tests: 10 passed, 0 failed ALL TESTS PASSED true