snapshot of https://github.com/jitsi/jitsi-meet.git @ fd1749dd28f4138aa80234fc7f311b19aaffa0e7 (2026-09-15)
Some checks failed
Prosody Plugin Tests / test (push) Failing after 10m11s
Close stale issues and PRs / stale (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-15 15:56:32 +05:00
commit 1d2bfcac0c
3159 changed files with 464524 additions and 0 deletions

View File

@@ -0,0 +1,329 @@
-- Unit tests for SpeakerStats in mod_speakerstats_component.lua
-- Run with busted from resources/prosody-plugins/:
-- busted spec/lua/
--
-- Stubs every Prosody dependency so no Prosody installation is needed.
-- Only the pure SpeakerStats class (new_SpeakerStats / setDominantSpeaker /
-- isDominantSpeaker / isSilent) is exercised here. Functions that depend on
-- live rooms, stanzas, or XMPP routing are covered by the integration tests.
-- ---------------------------------------------------------------------------
-- Controllable clock
-- socket.gettime() returns seconds (float); the module multiplies by 1000.
-- Call set_clock(ms) to advance time before each action.
-- ---------------------------------------------------------------------------
local _clock_ms = 0
local function set_clock(ms)
_clock_ms = ms
end
-- ---------------------------------------------------------------------------
-- Package preloads — registered before dofile() so the module sees them
-- ---------------------------------------------------------------------------
package.preload['socket'] = function()
return {
gettime = function() return _clock_ms / 1000 end,
}
end
package.preload['util.jid'] = function()
local function split(jid)
if not jid then return nil, nil, nil end
local node = jid:match('^([^@]+)@')
local host = jid:match('@([^/]+)')
local res = jid:match('/(.+)$')
return node, host, res
end
return {
resource = function(jid)
if not jid then return nil end
return jid:match('/(.+)$')
end,
split = split,
}
end
package.preload['util.stanza'] = function()
-- Only used by occupant_joined (route_stanza) — not exercised here.
return {
message = function()
local s = {}
s.tag = function(self, _, _) return self end
s.text = function(self, _) return self end
s.up = function(self) return self end
return s
end,
}
end
package.preload['cjson.safe'] = function()
return {
encode = function(_) return '{}', nil end,
decode = function(_) return {}, nil end,
}
end
package.preload['util.queue'] = function()
return {
new = function(max_size)
local items = {}
local q = {}
function q:push(v)
table.insert(items, v)
while #items > max_size do
table.remove(items, 1)
end
end
function q:items()
local i = 0
return function()
i = i + 1
return items[i]
end
end
function q:count() return #items end
return q
end,
}
end
package.preload['util.array'] = function()
-- Prosody's util.array module is itself callable as a constructor.
local arr_mt = {}
arr_mt.__index = arr_mt
return setmetatable({}, {
__call = function(_, iter)
local t = setmetatable({}, arr_mt)
if iter then
for v in iter do t[#t + 1] = v end
end
return t
end,
})
end
package.preload['util.async'] = function() return {} end
-- ---------------------------------------------------------------------------
-- Global stubs required by mod_speakerstats_component.lua at load time
-- ---------------------------------------------------------------------------
_G.prosody = { hosts = {}, version = 'test' }
-- Stub for the util library loaded via module:require "util"
local util_stub = {
is_admin = function() return false end,
get_room_from_jid = function() return nil end,
room_jid_match_rewrite = function(jid) return jid end,
is_jibri = function() return false end,
is_healthcheck_room = function() return false end,
is_transcriber = function() return false end,
-- noop: prevents hook registration against live muc components
process_host_module = function() end,
}
local mod = { host = 'speakerstats.test.example.com' }
mod.log = function() end
mod.get_option_string = function(_, key, default)
if key == 'muc_component' then return 'conference.test.example.com' end
if key == 'muc_mapper_domain_base' then return 'test.example.com' end
return default
end
mod.get_option_number = function(_, _, default) return default end
mod.hook = function() end
mod.fire_event = function() end
mod.context = function() return { fire_event = function() end } end
mod.require = function(_, name)
if name == 'util' then return util_stub end
error('unexpected module:require("' .. tostring(name) .. '")')
end
_G.module = mod
-- ---------------------------------------------------------------------------
-- Load the module under test
-- ---------------------------------------------------------------------------
local ok, err = pcall(dofile, 'mod_speakerstats_component.lua')
if not ok then
describe('mod_speakerstats_component', function()
it('skipped — failed to load', function()
pending(tostring(err):match('([^\n]+)') or tostring(err))
end)
end)
return
end
-- new_SpeakerStats is a module-level global (no `local` prefix in the source).
assert(type(new_SpeakerStats) == 'function',
'new_SpeakerStats must be a global function after loading the module')
-- ---------------------------------------------------------------------------
-- Helpers
-- ---------------------------------------------------------------------------
local function new_stats(nick)
return new_SpeakerStats(nick or 'testnick', nil)
end
-- ---------------------------------------------------------------------------
-- Tests
-- ---------------------------------------------------------------------------
describe('SpeakerStats', function()
-- Reset clock before every test so time never leaks between cases.
before_each(function() set_clock(0) end)
-- -----------------------------------------------------------------------
describe('initial state', function()
it('is not dominant speaker', function()
assert.is_false(new_stats():isDominantSpeaker())
end)
it('is not silent', function()
assert.is_false(new_stats():isSilent())
end)
it('has zero total speaking time', function()
assert.equal(0, new_stats().totalDominantSpeakerTime)
end)
end)
-- -----------------------------------------------------------------------
describe('setDominantSpeaker', function()
it('accumulates time when dominant speaker stops', function()
local s = new_stats()
set_clock(0)
s:setDominantSpeaker(true, false)
set_clock(1000)
s:setDominantSpeaker(false, false)
assert.equal(1000, s.totalDominantSpeakerTime)
end)
it('does not count time when starting dominant+silent', function()
local s = new_stats()
set_clock(0)
s:setDominantSpeaker(true, true) -- dominant but muted from the start
set_clock(5000)
s:setDominantSpeaker(false, false)
assert.equal(0, s.totalDominantSpeakerTime)
end)
it('accumulates time up to the moment of going silent', function()
local s = new_stats()
set_clock(0)
s:setDominantSpeaker(true, false) -- start speaking
set_clock(500)
s:setDominantSpeaker(true, true) -- go silent while still dominant
assert.equal(500, s.totalDominantSpeakerTime)
end)
it('resumes accumulating after returning from silence', function()
local s = new_stats()
set_clock(0)
s:setDominantSpeaker(true, false) -- speak: timer starts at 0
set_clock(500)
s:setDominantSpeaker(true, true) -- mute: +500 ms accumulated
set_clock(700)
s:setDominantSpeaker(true, false) -- unmute: timer restarts at 700
set_clock(900)
s:setDominantSpeaker(false, false) -- stop: +200 ms
assert.equal(700, s.totalDominantSpeakerTime) -- 500 + 200
end)
it('does not double-count when stopped twice in a row', function()
local s = new_stats()
set_clock(0)
s:setDominantSpeaker(true, false)
set_clock(300)
s:setDominantSpeaker(false, false) -- first stop: +300 ms
set_clock(9999)
s:setDominantSpeaker(false, false) -- second stop — must be a no-op
assert.equal(300, s.totalDominantSpeakerTime)
end)
it('accumulates across multiple dominant speaker episodes', function()
local s = new_stats()
set_clock(0)
s:setDominantSpeaker(true, false)
set_clock(400)
s:setDominantSpeaker(false, false) -- first episode: +400 ms
set_clock(1000)
s:setDominantSpeaker(true, false)
set_clock(1600)
s:setDominantSpeaker(false, false) -- second episode: +600 ms
assert.equal(1000, s.totalDominantSpeakerTime)
end)
it('updates isDominantSpeaker state', function()
local s = new_stats()
s:setDominantSpeaker(true, false)
assert.is_true(s:isDominantSpeaker())
s:setDominantSpeaker(false, false)
assert.is_false(s:isDominantSpeaker())
end)
it('updates isSilent state', function()
local s = new_stats()
s:setDominantSpeaker(true, false)
assert.is_false(s:isSilent())
s:setDominantSpeaker(true, true)
assert.is_true(s:isSilent())
s:setDominantSpeaker(true, false)
assert.is_false(s:isSilent())
end)
it('does not start timer when becoming dominant while silent', function()
-- Silence=true on the transition to dominant → no timer started.
-- Stopping later must not add spurious time.
local s = new_stats()
set_clock(0)
s:setDominantSpeaker(true, true) -- dominant+silent, no timer
set_clock(2000)
s:setDominantSpeaker(true, false) -- unmute: timer starts NOW at t=2000
set_clock(2500)
s:setDominantSpeaker(false, false) -- stop: +500 ms
assert.equal(500, s.totalDominantSpeakerTime)
end)
end) -- setDominantSpeaker
end) -- SpeakerStats