metadata.json claimed MIT while no LICENSE file existed, so the repository had no effective licence at all. It is now GPL-3.0-or-later: the full FSF text in LICENSE, the identifier in metadata.json, and REUSE-style SPDX-FileCopyrightText and SPDX-License-Identifier headers on all 33 source files, which is the convention KDE itself uses. Worth recording that GPL-3.0 is a preference here rather than an obligation. Nothing in the dependency graph forces it: linkctl is MIT and is invoked as a subprocess rather than linked, and the KDE QML modules the applet imports are LGPL. Removed two things that should not ship in a public repository. tests's QML linter had an absolute sandbox path, /tmp/pyside/PySide6/qmllint, baked into its search list; it now looks for Qt's qmllint and PySide6's pyside6-qmllint on PATH and honours a QMLLINT override, and says how to install one when it finds nothing. The systemd unit carried a Documentation= URL pointing at a host that does not exist, and its ExecStart silently assumed a particular checkout directory; the URL is gone and the path is now an obvious PATH/TO/CHECKOUT placeholder next to the comment telling you to edit it. Added a Credits section naming the work this is built on, since the project is mostly glue: linkctl does the camera control, schlarpc's firmware teardown documents the extension-unit protocol, and vrwallace's controller established that the Link 2 needs both pan and tilt axes in a single VIDIOC_S_EXT_CTRLS call, which is the detail the whole thing depends on. Also made the two Python tests executable, matching the other scripts. Audited for operator-specific detail: no credentials, tokens, private hostnames, IP addresses or absolute home paths in any tracked file or anywhere in the history's diffs. Three merge commits do carry Reviewed-on and Reviewed-by trailers naming a private forge, added by the forge at merge time; those are commit metadata rather than file content and are handled separately. Tested: 169 checks across seven suites plus the QML linter, all passing after the header insertion, which confirms it did not disturb the order-sensitive `.pragma library` line in logic.js or the qmldir module definitions. shellcheck clean.
88 lines
3.2 KiB
Lua
88 lines
3.2 KiB
Lua
-- SPDX-FileCopyrightText: 2026 branderson
|
|
-- SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
-- Offline tests for the pure helpers in obs/obs-linkctl.lua.
|
|
-- Run: lua tests/test_obs_linkctl.lua
|
|
--
|
|
-- These cover the parts that do not need OBS or a camera: shell quoting,
|
|
-- action tokenizing, the scene-mapping parser and command construction.
|
|
|
|
local m = dofile("obs/obs-linkctl.lua")
|
|
|
|
local failures, checks = 0, 0
|
|
|
|
local function eq(got, want, label)
|
|
checks = checks + 1
|
|
if got ~= want then
|
|
failures = failures + 1
|
|
io.write(string.format("FAIL %s\n got: %s\n want: %s\n",
|
|
label, tostring(got), tostring(want)))
|
|
end
|
|
end
|
|
|
|
local function eqlist(got, want, label)
|
|
eq(table.concat(got, "|"), table.concat(want, "|"), label)
|
|
end
|
|
|
|
-- shquote: the boundary between user input and /bin/sh
|
|
eq(m.shquote("home"), "'home'", "shquote plain")
|
|
eq(m.shquote("my desk"), "'my desk'", "shquote space")
|
|
eq(m.shquote("it's"), [['it'\''s']], "shquote embedded single quote")
|
|
eq(m.shquote("a;rm -rf /"), "'a;rm -rf /'", "shquote neutralises metacharacters")
|
|
eq(m.shquote("$(whoami)"), "'$(whoami)'", "shquote neutralises substitution")
|
|
|
|
-- tokenize: splitting an action string into linkctl arguments
|
|
eqlist(m.tokenize("tracking on"), { "tracking", "on" }, "tokenize simple")
|
|
eqlist(m.tokenize(" center "), { "center" }, "tokenize trims")
|
|
eqlist(m.tokenize("preset load home"), { "preset", "load", "home" }, "tokenize three")
|
|
eqlist(m.tokenize('preset load "my desk"'),
|
|
{ "preset", "load", "my desk" }, "tokenize quoted")
|
|
eqlist(m.tokenize('preset load "unterminated'),
|
|
{ "preset", "load", "unterminated" }, "tokenize unterminated quote")
|
|
eqlist(m.tokenize(""), {}, "tokenize empty")
|
|
|
|
-- parse_scene_map
|
|
local map = m.parse_scene_map([[
|
|
Talking Head = tracking on
|
|
Overhead = preset load overhead
|
|
|
|
# a comment
|
|
Slides = tracking off
|
|
Blank Action =
|
|
]])
|
|
eq(map["Talking Head"], "tracking on", "map simple")
|
|
eq(map["Overhead"], "preset load overhead", "map multiword action")
|
|
eq(map["Slides"], "tracking off", "map after comment and blank")
|
|
eq(map["# a comment"], nil, "map ignores comments")
|
|
eq(map["Blank Action"], nil, "map ignores empty action")
|
|
|
|
local spaced = m.parse_scene_map(" Scene With Spaces = tracking toggle ")
|
|
eq(spaced["Scene With Spaces"], "tracking toggle", "map trims around separator")
|
|
|
|
-- An action containing '=' must keep it; only the first separator splits.
|
|
local eqaction = m.parse_scene_map("S = brightness 1=2")
|
|
eq(eqaction["S"], "brightness 1=2", "map splits on first separator only")
|
|
|
|
-- build_cmd honours configuration
|
|
m.cfg.linkctl = "linkctl"
|
|
m.cfg.device = ""
|
|
m.cfg.force = true
|
|
eq(m.build_cmd({ "tracking", "on" }),
|
|
"'linkctl' --force 'tracking' 'on'", "build_cmd with force")
|
|
|
|
m.cfg.force = false
|
|
eq(m.build_cmd({ "center" }), "'linkctl' 'center'", "build_cmd without force")
|
|
|
|
m.cfg.device = "/dev/video0"
|
|
eq(m.build_cmd({ "center" }),
|
|
"'linkctl' --device '/dev/video0' 'center'", "build_cmd with device")
|
|
|
|
m.cfg.linkctl = "/usr/local/bin/linkctl"
|
|
m.cfg.device = ""
|
|
eq(m.build_cmd({ "preset", "load", "my desk" }),
|
|
"'/usr/local/bin/linkctl' 'preset' 'load' 'my desk'",
|
|
"build_cmd quotes preset with space")
|
|
|
|
io.write(string.format("\n%d checks, %d failures\n", checks, failures))
|
|
os.exit(failures == 0 and 0 or 1)
|