Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 50 additions & 3 deletions PROTOCOL.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ protocol_version = '>=3.0.0 <4.0.0'
Core accepts any version or range that overlaps with its supported range. Read the current values from Lua:

```lua
require('smart-splits').PROTOCOL_VERSION --> vim.Version (3.0.0)
require('smart-splits.backend').SUPPORTED_VERSIONS --> vim.VersionRange (^3.0.0)
local proto_version = require('smart-splits').PROTOCOL_VERSION --> vim.Version (3.0.0)
local supported_versions = require('smart-splits').SUPPORTED_VERSIONS --> vim.VersionRange (^3.0.0)
```

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.
Expand Down Expand Up @@ -256,7 +256,7 @@ list them in priority order, so every installed backend gets configured on every
the ones whose multiplexer is not even running:

```lua
{
return {
'smart-splits-nvim/smart-splits.nvim',
dependencies = {
{
Expand Down Expand Up @@ -354,6 +354,53 @@ require('smart-splits').setup({

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

## Testing

This repository is the source of truth for protocol correctness. It ships a Lua module,
`smart-splits.protocol_tests`, that your backend can require in its own test suite to verify
conformance. The module is framework-agnostic: it returns a list of test cases you can run in
busted, plenary, or any other test runner.

Each test is a `{name, fn}` pair. `fn()` returns `true` on pass, or an error string on failure:

```lua
local protocol_tests = require('smart-splits.protocol_tests')

describe('backend conformance', function()
for _, test in ipairs(protocol_tests.tests(my_backend)) do
it(test.name, function()
local result = test.fn()
if result ~= true then
error(result)
end
end)
end
end)
```

Or run them all at once without a framework:

```lua
local results = require('smart-splits.protocol_tests').run(my_backend)
for _, r in ipairs(results) do
print(r.ok and 'PASS' or 'FAIL', r.name, r.ok == true and '' or r.ok)
end
```

The tests cover:

- **Structural validation** — required fields exist with correct types, protocol version overlaps
the supported range.
- **`detect()`** — returns a boolean, does not throw.
- **`move(direction, {})`** — returns a boolean for each of `left`, `right`, `up`, `down`; does not
throw.
- **`resize(direction, {amount=1})`** — same, but only when `resize` is present on the backend.
- **`activate()`** — does not throw, if present.
- **`health()`** — does not throw, if present.

Optional fields (`resize`, `activate`, `health`) are only tested when the backend provides them. A
backend that omits `resize` entirely will not see resize tests.

## What core does not give you

- **Pane identifiers.** Core no longer asks for them, and no longer compares them to work out whether
Expand Down
6 changes: 3 additions & 3 deletions lua/smart-splits/backend.lua
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ end

---@param backend table
---@return string|nil error
local function validate(backend)
function M.validate(backend)
for field, expected in pairs(REQUIRED) do
local actual = type(backend[field])
if backend[field] == nil then
Expand Down Expand Up @@ -184,7 +184,7 @@ end
---@return string|nil error
local function load(spec)
if type(spec) ~= 'string' then
local err = validate(spec)
local err = M.validate(spec)
if err then
return nil, err
end
Expand All @@ -199,7 +199,7 @@ local function load(spec)
return nil, ('module returned a %s, expected a table'):format(type(module))
end

local err = validate(module)
local err = M.validate(module)
if err then
return nil, err
end
Expand Down
1 change: 1 addition & 0 deletions lua/smart-splits/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ local M = {}
---The multiplexer protocol version this release implements. Backends can assert
---against it, see PROTOCOL.md.
M.PROTOCOL_VERSION = require('smart-splits.backend').PROTOCOL_VERSION
M.SUPPORTED_VERSIONS = require('smart-splits.backend').SUPPORTED_VERSIONS

---@class (partial) SmartSplitsSetupOpts: SmartSplitsConfig

Expand Down
146 changes: 146 additions & 0 deletions lua/smart-splits/protocol_tests.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
---Protocol conformance tests for backends. Backends can require this module
---in their own test suites to verify they implement the protocol correctly.
---
---Each test is a `{name, fn}` pair. `fn()` returns `true` on pass, or an error
---string on failure. Backends can iterate them in any test framework:
---
---```lua
---local protocol_tests = require('smart-splits.protocol_tests')
---for _, test in ipairs(protocol_tests.tests(my_backend)) do
--- it(test.name, function()
--- local result = test.fn()
--- if result ~= true then
--- error(result)
--- end
--- end)
---end
---```
---
---Or run them all at once without a framework:
---
---```lua
---local results = require('smart-splits.protocol_tests').run(my_backend)
---for _, r in ipairs(results) do
--- print(r.ok and 'PASS' or 'FAIL', r.name, r.ok == true and '' or r.ok)
---end
---```

local Backend = require('smart-splits.backend')

local DIRECTIONS = { 'left', 'right', 'up', 'down' }

local M = {}

---@class SmartSplitsProtocolTest
---@field name string
---@field fn fun():true|string returns true on pass, error message on failure

---@param backend SmartSplitsBackend
---@return SmartSplitsProtocolTest[]
function M.tests(backend)
local tests = {}

table.insert(tests, {
name = 'passes structural validation',
fn = function()
local err = Backend.validate(backend)
if err then
return err
end
return true
end,
})

table.insert(tests, {
name = 'detect() returns a boolean',
fn = function()
local ok, result = pcall(backend.detect)
if not ok then
return ('detect() errored: %s'):format(result)
end
if type(result) ~= 'boolean' then
return ('detect() returned %s, expected boolean'):format(type(result))
end
return true
end,
})

for _, direction in ipairs(DIRECTIONS) do
table.insert(tests, {
name = ('move(%q, {}) returns a boolean'):format(direction),
fn = function()
local ok, result = pcall(backend.move, direction, {})
if not ok then
return ('move(%q) errored: %s'):format(direction, result)
end
if type(result) ~= 'boolean' then
return ('move(%q) returned %s, expected boolean'):format(direction, type(result))
end
return true
end,
})
end

if backend.resize then
for _, direction in ipairs(DIRECTIONS) do
table.insert(tests, {
name = ('resize(%q, {amount=1}) returns a boolean'):format(direction),
fn = function()
local ok, result = pcall(backend.resize, direction, { amount = 1 })
if not ok then
return ('resize(%q) errored: %s'):format(direction, result)
end
if type(result) ~= 'boolean' then
return ('resize(%q) returned %s, expected boolean'):format(direction, type(result))
end
return true
end,
})
end
end

if backend.activate then
table.insert(tests, {
name = 'activate() does not error',
fn = function()
local ok, err = pcall(backend.activate)
if not ok then
return ('activate() errored: %s'):format(err)
end
return true
end,
})
end

if backend.health then
table.insert(tests, {
name = 'health() does not error',
fn = function()
local ok, err = pcall(backend.health)
if not ok then
return ('health() errored: %s'):format(err)
end
return true
end,
})
end

return tests
end

---@class SmartSplitsProtocolTestResult
---@field name string
---@field ok true|string

---@param backend SmartSplitsBackend
---@return SmartSplitsProtocolTestResult[]
function M.run(backend)
local results = {}
for _, test in ipairs(M.tests(backend)) do
local ok = test.fn()
table.insert(results, { name = test.name, ok = ok })
end
return results
end

return M
Loading
Loading