0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-04 10:39:43 +08:00
discourse/spec/tasks/qunit_cli_spec.rb
Sérgio Saquetim dbea1f184f
DEV: Fail fast when the qunit browser stalls (#41919)
`bin/qunit` could wait indefinitely for a headless browser that
connected but then stopped making test progress, hanging the whole run
with no useful diagnostics. This is most painful when the run was
launched in an automated way, for example by an agent or in CI: a wedged
headless Chrome blocks the caller with no signal, instead of returning a
fast, diagnosable failure it can act on.

This adds a browser watchdog that fails fast and identifies the reason
instead of hanging. testem's native start timeout handles a browser that
never connects, and a small testem patch handles one that connects but
then goes idle outside an active test, reporting `Browser made no test
progress for N seconds` on stderr and through the test result. There is
deliberately no retry logic: on a stall the run exits non-zero so the
caller that launched it can decide to retry the whole run.

The watchdog is on by default and its two timeouts are configurable
through flags and matching environment variables: `--browser-watchdog` /
`QUNIT_BROWSER_WATCHDOG`, `--browser-start-timeout` /
`QUNIT_BROWSER_START_TIMEOUT` (default 45s), and
`--browser-inactivity-timeout` / `QUNIT_BROWSER_INACTIVITY_TIMEOUT`
(default 30s).

The CLI flag and environment wiring, including precedence over the
environment, is covered by RSpec. The testem patch itself is not
unit-tested, to avoid standing up a separate node test harness just to
exercise another test harness.
2026-07-22 12:43:54 -03:00

249 lines
6.4 KiB
Ruby
Vendored

# frozen_string_literal: true
describe "bin/qunit" do
def run(*args, env: {})
out, err, status = Open3.capture3(env, "bin/qunit", "--dry-run", *args, chdir: Rails.root.to_s)
parsed_args, parsed_env =
if parsed_result = out.match(/Executing: (?<args>\[.+?\])\nwith env: (?<env>\{.+?\})/m)
[JSON.parse(parsed_result[:args]), JSON.parse(parsed_result[:env])]
end
OpenStruct.new(
out: out,
err: err,
status: status.exitstatus,
args: parsed_args,
env: parsed_env,
launched_server: out.include?("[dry-run] skipping server startup"),
)
end
let(:core_test_file) do
Dir.glob("#{Rails.root.join("frontend/discourse/tests/integration/**/*-test.js")}").first
end
let(:chat_test_file) { Dir.glob("#{Rails.root.join("plugins/chat/test/**/*-test.js")}").first }
let(:default_watchdog_env) do
{
"QUNIT_BROWSER_WATCHDOG" => "1",
"QUNIT_BROWSER_START_TIMEOUT" => "45",
"QUNIT_BROWSER_INACTIVITY_TIMEOUT" => "30",
}
end
it "runs all core tests by default" do
result = run
expect(result.status).to eq(0)
expect(result.launched_server).to eq(false)
expect(result.args).to match(
[
"pnpm",
"ember",
"exam",
"--query",
"target=core&testem=1",
"--random",
a_string_matching(/\A[a-zA-Z0-9]{8}\z/),
"--path",
"dist",
],
)
expect(result.env).to match(
default_watchdog_env.merge(
"UNICORN_PORT" => a_truthy_value,
"TESTEM_DEFAULT_BROWSER" => a_truthy_value,
"LOAD_PLUGINS" => "0",
),
)
end
it "allows running specific file" do
result = run(core_test_file)
expect(result.status).to eq(0)
expect(result.launched_server).to eq(false)
expect(result.args).to match(
[
"pnpm",
"ember",
"exam",
"--query",
"target=core&testem=1",
"--file-path",
core_test_file.sub("#{Rails.root.join("frontend/discourse/tests/")}", ""),
"--random",
a_string_matching(/\A[a-zA-Z0-9]{8}\z/),
"--path",
"dist",
],
)
expect(result.env).to match(
default_watchdog_env.merge(
"UNICORN_PORT" => a_truthy_value,
"TESTEM_DEFAULT_BROWSER" => a_truthy_value,
"LOAD_PLUGINS" => "0",
),
)
end
it "allows running all plugin tests" do
result = run("--target", "plugins")
expect(result.status).to eq(0)
expect(result.launched_server).to eq(false)
expect(result.args).to match(
[
"pnpm",
"ember",
"exam",
"--query",
"testem=1",
"--random",
a_string_matching(/\A[a-zA-Z0-9]{8}\z/),
"--path",
"dist",
],
)
expect(result.env).to match(
default_watchdog_env.merge(
"UNICORN_PORT" => a_truthy_value,
"TESTEM_DEFAULT_BROWSER" => a_truthy_value,
"LOAD_PLUGINS" => "1",
"PLUGIN_TARGETS" => a_string_matching(/,/),
),
)
end
it "allows running tests for multiple plugins" do
result = run("--target", "chat,discourse-local-dates")
expect(result.status).to eq(0)
expect(result.launched_server).to eq(false)
expect(result.args).to match(
[
"pnpm",
"ember",
"exam",
"--query",
"testem=1",
"--random",
a_string_matching(/\A[a-zA-Z0-9]{8}\z/),
"--path",
"dist",
],
)
expect(result.env).to match(
default_watchdog_env.merge(
"UNICORN_PORT" => a_truthy_value,
"TESTEM_DEFAULT_BROWSER" => a_truthy_value,
"LOAD_PLUGINS" => "1",
"PLUGIN_TARGETS" => "chat,discourse-local-dates",
),
)
end
it "allows running specific plugin test file" do
result = run(chat_test_file)
expect(result.status).to eq(0)
expect(result.launched_server).to eq(false)
expect(result.args).to match(
[
"pnpm",
"ember",
"exam",
"--query",
"target=chat&testem=1",
"--file-path",
chat_test_file.sub(
"#{Rails.root.join("plugins/chat/test/javascripts/")}",
"discourse/plugins/chat/",
),
"--random",
a_string_matching(/\A[a-zA-Z0-9]{8}\z/),
"--path",
"dist",
],
)
expect(result.env).to match(
default_watchdog_env.merge(
"UNICORN_PORT" => a_truthy_value,
"TESTEM_DEFAULT_BROWSER" => a_truthy_value,
"LOAD_PLUGINS" => "1",
),
)
end
it "prevents running files from multiple targets" do
result = run(core_test_file, chat_test_file)
expect(result.status).to eq(1)
expect(result.out).to include(
"Error: Cannot mix multiple plugin/core targets when running specific files",
)
end
it "launches server when using --standalone" do
result = run("--standalone")
expect(result.status).to eq(0)
expect(result.launched_server).to eq(true)
end
it "enables the browser watchdog with default settings" do
result = run
expect(result.env).to include(
"QUNIT_BROWSER_WATCHDOG" => "1",
"QUNIT_BROWSER_START_TIMEOUT" => "45",
"QUNIT_BROWSER_INACTIVITY_TIMEOUT" => "30",
)
end
it "allows browser watchdog settings to be configured through the environment" do
result =
run(
env: {
"QUNIT_BROWSER_WATCHDOG" => "0",
"QUNIT_BROWSER_START_TIMEOUT" => "60",
"QUNIT_BROWSER_INACTIVITY_TIMEOUT" => "40",
},
)
expect(result.env).to include(
"QUNIT_BROWSER_WATCHDOG" => "0",
"QUNIT_BROWSER_START_TIMEOUT" => "60",
"QUNIT_BROWSER_INACTIVITY_TIMEOUT" => "40",
)
end
it "gives browser watchdog CLI settings precedence over the environment" do
result =
run(
"--browser-watchdog",
"--browser-start-timeout",
"70",
"--browser-inactivity-timeout",
"50",
env: {
"QUNIT_BROWSER_WATCHDOG" => "0",
"QUNIT_BROWSER_START_TIMEOUT" => "60",
"QUNIT_BROWSER_INACTIVITY_TIMEOUT" => "40",
},
)
expect(result.env).to include(
"QUNIT_BROWSER_WATCHDOG" => "1",
"QUNIT_BROWSER_START_TIMEOUT" => "70",
"QUNIT_BROWSER_INACTIVITY_TIMEOUT" => "50",
)
end
it "rejects non-positive browser watchdog settings" do
result = run("--browser-inactivity-timeout", "0")
expect(result.status).to eq(1)
expect(result.err).to include("--browser-inactivity-timeout must be greater than 0")
end
end