| // Copyright 2026 The Chromium Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include "base/test/bind.h" | |
| #include "content/browser/devtools/worker_or_worklet_devtools_agent_host.h" | |
| #include "content/public/test/browser_task_environment.h" | |
| #include "content/public/test/mock_render_process_host.h" | |
| #include "content/public/test/test_browser_context.h" | |
| #include "testing/gtest/include/gtest/gtest.h" | |
| namespace content { | |
| namespace { | |
| class TestAgentHost : public WorkerOrWorkletDevToolsAgentHost { | |
| public: | |
| TestAgentHost( | |
| int process_id, | |
| const GURL& url, | |
| const std::string& name, | |
| const base::UnguessableToken& devtools_worker_token, | |
| const std::string& parent_id, | |
| base::OnceCallback<void(DevToolsAgentHostImpl*)> destroyed_callback) | |
| : WorkerOrWorkletDevToolsAgentHost(process_id, | |
| url, | |
| name, | |
| devtools_worker_token, | |
| parent_id, | |
| std::move(destroyed_callback)) { | |
| NotifyCreated(); | |
| } | |
| using WorkerOrWorkletDevToolsAgentHost::Disconnected; | |
| // DevToolsAgentHost implementation: | |
| std::string GetType() override { return "test"; } | |
| bool AttachSession(DevToolsSession* session) override { return true; } | |
| void DetachSession(DevToolsSession* session) override {} | |
| private: | |
| ~TestAgentHost() override = default; | |
| }; | |
| } // namespace | |
| class DevToolsRefcountTest : public testing::Test { | |
| public: | |
| DevToolsRefcountTest() | |
| : task_environment_(content::BrowserTaskEnvironment::IO_MAINLOOP) {} | |
| protected: | |
| content::BrowserTaskEnvironment task_environment_; | |
| TestBrowserContext browser_context_; | |
| }; | |
| // Tests that WorkerOrWorkletDevToolsAgentHost doesn't underflow refcount | |
| // when Disconnected() is called multiple times. | |
| TEST_F(DevToolsRefcountTest, PreventDoubleRelease) { | |
| MockRenderProcessHost renderer_host(&browser_context_); | |
| const int process_id = renderer_host.GetDeprecatedID(); | |
| base::UnguessableToken token = base::UnguessableToken::Create(); | |
| bool destroyed_called = false; | |
| auto destroyed_callback = base::BindLambdaForTesting( | |
| [&](DevToolsAgentHostImpl* host) { destroyed_called = true; }); | |
| // 1. Create a TestAgentHost. | |
| // It starts with refcount 1 (from our scoped_refptr) + 1 (from | |
| // self-keep-alive in constructor). | |
| scoped_refptr<TestAgentHost> agent_host = base::MakeRefCounted<TestAgentHost>( | |
| process_id, GURL("http://example.com"), "worker", token, "parent_id", | |
| std::move(destroyed_callback)); | |
| // 2. Call Disconnected() first time. | |
| // This should trigger the first Release() and destroyed_callback. | |
| agent_host->Disconnected(); | |
| EXPECT_TRUE(destroyed_called); | |
| // 3. Call Disconnected() multiple times. | |
| // If the fix is working, these subsequent calls won't cause double-release. | |
| // We reset destroyed_called to ensure it's not called again. | |
| destroyed_called = false; | |
| agent_host->Disconnected(); | |
| agent_host->Disconnected(); | |
| EXPECT_FALSE(destroyed_called); | |
| // 4. Verification: If we can still access agent_host, it hasn't been | |
| // over-released. The refcount should be 1 (only our scoped_refptr). | |
| EXPECT_TRUE(agent_host->HasOneRef()); | |
| } | |
| } // namespace content |