Skip to content

Commit 1bab8eb

Browse files
committed
test(protocol): Provide protocol correctness tests for backends to use
1 parent a5f9a72 commit 1bab8eb

5 files changed

Lines changed: 433 additions & 6 deletions

File tree

PROTOCOL.md

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ protocol_version = '>=3.0.0 <4.0.0'
2020
Core accepts any version or range that overlaps with its supported range. Read the current values from Lua:
2121

2222
```lua
23-
require('smart-splits').PROTOCOL_VERSION --> vim.Version (3.0.0)
24-
require('smart-splits.backend').SUPPORTED_VERSIONS --> vim.VersionRange (^3.0.0)
23+
local proto_version = require('smart-splits').PROTOCOL_VERSION --> vim.Version (3.0.0)
24+
local supported_versions = require('smart-splits').SUPPORTED_VERSIONS --> vim.VersionRange (^3.0.0)
2525
```
2626

2727
Protocol versions follow semantic versioning. Additions that do not break existing backends do not bump the major version. When a breaking change does land, core keeps accepting the previous major version for at least one release, so the supported range may span multiple major versions.
@@ -256,7 +256,7 @@ list them in priority order, so every installed backend gets configured on every
256256
the ones whose multiplexer is not even running:
257257

258258
```lua
259-
{
259+
return {
260260
'smart-splits-nvim/smart-splits.nvim',
261261
dependencies = {
262262
{
@@ -354,6 +354,53 @@ require('smart-splits').setup({
354354

355355
Then drive your movement keys and read `:SmartSplitsLog`.
356356

357+
## Testing
358+
359+
This repository is the source of truth for protocol correctness. It ships a Lua module,
360+
`smart-splits.protocol_tests`, that your backend can require in its own test suite to verify
361+
conformance. The module is framework-agnostic: it returns a list of test cases you can run in
362+
busted, plenary, or any other test runner.
363+
364+
Each test is a `{name, fn}` pair. `fn()` returns `true` on pass, or an error string on failure:
365+
366+
```lua
367+
local protocol_tests = require('smart-splits.protocol_tests')
368+
369+
describe('backend conformance', function()
370+
for _, test in ipairs(protocol_tests.tests(my_backend)) do
371+
it(test.name, function()
372+
local result = test.fn()
373+
if result ~= true then
374+
error(result)
375+
end
376+
end)
377+
end
378+
end)
379+
```
380+
381+
Or run them all at once without a framework:
382+
383+
```lua
384+
local results = require('smart-splits.protocol_tests').run(my_backend)
385+
for _, r in ipairs(results) do
386+
print(r.ok and 'PASS' or 'FAIL', r.name, r.ok == true and '' or r.ok)
387+
end
388+
```
389+
390+
The tests cover:
391+
392+
- **Structural validation** — required fields exist with correct types, protocol version overlaps
393+
the supported range.
394+
- **`detect()`** — returns a boolean, does not throw.
395+
- **`move(direction, {})`** — returns a boolean for each of `left`, `right`, `up`, `down`; does not
396+
throw.
397+
- **`resize(direction, {amount=1})`** — same, but only when `resize` is present on the backend.
398+
- **`activate()`** — does not throw, if present.
399+
- **`health()`** — does not throw, if present.
400+
401+
Optional fields (`resize`, `activate`, `health`) are only tested when the backend provides them. A
402+
backend that omits `resize` entirely will not see resize tests.
403+
357404
## What core does not give you
358405

359406
- **Pane identifiers.** Core no longer asks for them, and no longer compares them to work out whether

lua/smart-splits/backend.lua

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ end
121121

122122
---@param backend table
123123
---@return string|nil error
124-
local function validate(backend)
124+
function M.validate(backend)
125125
for field, expected in pairs(REQUIRED) do
126126
local actual = type(backend[field])
127127
if backend[field] == nil then
@@ -184,7 +184,7 @@ end
184184
---@return string|nil error
185185
local function load(spec)
186186
if type(spec) ~= 'string' then
187-
local err = validate(spec)
187+
local err = M.validate(spec)
188188
if err then
189189
return nil, err
190190
end
@@ -199,7 +199,7 @@ local function load(spec)
199199
return nil, ('module returned a %s, expected a table'):format(type(module))
200200
end
201201

202-
local err = validate(module)
202+
local err = M.validate(module)
203203
if err then
204204
return nil, err
205205
end

lua/smart-splits/init.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ local M = {}
33
---The multiplexer protocol version this release implements. Backends can assert
44
---against it, see PROTOCOL.md.
55
M.PROTOCOL_VERSION = require('smart-splits.backend').PROTOCOL_VERSION
6+
M.SUPPORTED_VERSIONS = require('smart-splits.backend').SUPPORTED_VERSIONS
67

78
---@class (partial) SmartSplitsSetupOpts: SmartSplitsConfig
89

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
---Protocol conformance tests for backends. Backends can require this module
2+
---in their own test suites to verify they implement the protocol correctly.
3+
---
4+
---Each test is a `{name, fn}` pair. `fn()` returns `true` on pass, or an error
5+
---string on failure. Backends can iterate them in any test framework:
6+
---
7+
---```lua
8+
---local protocol_tests = require('smart-splits.protocol_tests')
9+
---for _, test in ipairs(protocol_tests.tests(my_backend)) do
10+
--- it(test.name, function()
11+
--- local result = test.fn()
12+
--- if result ~= true then
13+
--- error(result)
14+
--- end
15+
--- end)
16+
---end
17+
---```
18+
---
19+
---Or run them all at once without a framework:
20+
---
21+
---```lua
22+
---local results = require('smart-splits.protocol_tests').run(my_backend)
23+
---for _, r in ipairs(results) do
24+
--- print(r.ok and 'PASS' or 'FAIL', r.name, r.ok == true and '' or r.ok)
25+
---end
26+
---```
27+
28+
local Backend = require('smart-splits.backend')
29+
30+
local DIRECTIONS = { 'left', 'right', 'up', 'down' }
31+
32+
local M = {}
33+
34+
---@class SmartSplitsProtocolTest
35+
---@field name string
36+
---@field fn fun():true|string returns true on pass, error message on failure
37+
38+
---@param backend SmartSplitsBackend
39+
---@return SmartSplitsProtocolTest[]
40+
function M.tests(backend)
41+
local tests = {}
42+
43+
table.insert(tests, {
44+
name = 'passes structural validation',
45+
fn = function()
46+
local err = Backend.validate(backend)
47+
if err then
48+
return err
49+
end
50+
return true
51+
end,
52+
})
53+
54+
table.insert(tests, {
55+
name = 'detect() returns a boolean',
56+
fn = function()
57+
local ok, result = pcall(backend.detect)
58+
if not ok then
59+
return ('detect() errored: %s'):format(result)
60+
end
61+
if type(result) ~= 'boolean' then
62+
return ('detect() returned %s, expected boolean'):format(type(result))
63+
end
64+
return true
65+
end,
66+
})
67+
68+
for _, direction in ipairs(DIRECTIONS) do
69+
table.insert(tests, {
70+
name = ('move(%q, {}) returns a boolean'):format(direction),
71+
fn = function()
72+
local ok, result = pcall(backend.move, direction, {})
73+
if not ok then
74+
return ('move(%q) errored: %s'):format(direction, result)
75+
end
76+
if type(result) ~= 'boolean' then
77+
return ('move(%q) returned %s, expected boolean'):format(direction, type(result))
78+
end
79+
return true
80+
end,
81+
})
82+
end
83+
84+
if backend.resize then
85+
for _, direction in ipairs(DIRECTIONS) do
86+
table.insert(tests, {
87+
name = ('resize(%q, {amount=1}) returns a boolean'):format(direction),
88+
fn = function()
89+
local ok, result = pcall(backend.resize, direction, { amount = 1 })
90+
if not ok then
91+
return ('resize(%q) errored: %s'):format(direction, result)
92+
end
93+
if type(result) ~= 'boolean' then
94+
return ('resize(%q) returned %s, expected boolean'):format(direction, type(result))
95+
end
96+
return true
97+
end,
98+
})
99+
end
100+
end
101+
102+
if backend.activate then
103+
table.insert(tests, {
104+
name = 'activate() does not error',
105+
fn = function()
106+
local ok, err = pcall(backend.activate)
107+
if not ok then
108+
return ('activate() errored: %s'):format(err)
109+
end
110+
return true
111+
end,
112+
})
113+
end
114+
115+
if backend.health then
116+
table.insert(tests, {
117+
name = 'health() does not error',
118+
fn = function()
119+
local ok, err = pcall(backend.health)
120+
if not ok then
121+
return ('health() errored: %s'):format(err)
122+
end
123+
return true
124+
end,
125+
})
126+
end
127+
128+
return tests
129+
end
130+
131+
---@class SmartSplitsProtocolTestResult
132+
---@field name string
133+
---@field ok true|string
134+
135+
---@param backend SmartSplitsBackend
136+
---@return SmartSplitsProtocolTestResult[]
137+
function M.run(backend)
138+
local results = {}
139+
for _, test in ipairs(M.tests(backend)) do
140+
local ok = test.fn()
141+
table.insert(results, { name = test.name, ok = ok })
142+
end
143+
return results
144+
end
145+
146+
return M

0 commit comments

Comments
 (0)