blob: 6ec742b171e109b83716d5272e4199de1a70bcc8 [file] [log] [blame] [edit]
// Copyright 2025 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "farfetchd/trace_manager.h"
#include <memory>
#include <queue>
#include <string>
#include <utility>
#include <vector>
#include <base/files/file_util.h>
#include <base/files/scoped_temp_dir.h>
#include <base/strings/string_util.h>
#include <base/test/task_environment.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
namespace farfetchd {
// Mock implementation of TraceReader for testing
class MockTraceReader : public TraceReader {
public:
MockTraceReader() = default;
~MockTraceReader() override = default;
bool Open() override {
is_open_ = true;
return true; // Always succeed in tests
}
bool ReadLine(std::string* line) override {
if (!is_open_) {
return false; // Reader is closed
}
if (!lines_to_read_.empty()) {
*line = lines_to_read_.front();
lines_to_read_.pop();
return true;
}
// If max_reads_ is 0, simulate immediate end of data (for most tests)
// We'll sleep briefly to allow the trace to be stopped gracefully
if (max_reads_ == 0) {
// Give the test a chance to stop the trace before we return false
base::PlatformThread::Sleep(base::Milliseconds(10));
// Check if we're still supposed to be reading
return false; // No more data
}
if (max_reads_ > 0 && read_count_ >= max_reads_) {
return false; // Simulate end of data
}
// Return some mock trace data for testing
*line =
"test-process-123 [000] .... 1234.567890: event: file=\"/test/path\"";
read_count_++;
return true;
}
void Close() override { is_open_ = false; }
// Test helpers
void set_max_reads(int max) { max_reads_ = max; }
void AddLine(const std::string& line) { lines_to_read_.push(line); }
private:
bool is_open_ = false;
int read_count_ = 0;
int max_reads_ = 0; // Limit reads to prevent infinite loops in tests
std::queue<std::string> lines_to_read_;
};
class TraceManagerTest : public ::testing::Test {
protected:
void SetUp() override {
ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
// Create a task environment for testing async operations
task_environment_ = std::make_unique<base::test::TaskEnvironment>(
base::test::TaskEnvironment::TimeSource::MOCK_TIME);
// Create a mock trace reader
auto mock_reader = std::make_unique<MockTraceReader>();
mock_trace_reader_ = mock_reader.get(); // Keep a pointer for test control
// Note: Individual tests can call mock_trace_reader_->set_max_reads() to
// control behavior Default is 0 which means immediate end-of-stream
manager_ = std::make_unique<TraceManager>(std::move(mock_reader));
manager_->SetTraceBaseDirForTest(temp_dir_.GetPath());
}
void TearDown() override {
// Run any pending tasks
task_environment_->RunUntilIdle();
}
base::ScopedTempDir temp_dir_;
std::unique_ptr<base::test::TaskEnvironment> task_environment_;
std::unique_ptr<TraceManager> manager_;
MockTraceReader* mock_trace_reader_;
};
// Test the basic trace creation functionality
TEST_F(TraceManagerTest, StartTrace) {
std::string app_name = "test_app";
std::vector<std::string> process_names = {"chrome", "chrome_renderer"};
std::string trace_id = manager_->StartTrace(app_name, process_names, {}, {});
EXPECT_FALSE(trace_id.empty());
EXPECT_TRUE(
base::StartsWith(trace_id, "trace_")); // Trace IDs start with prefix.
// Verify the trace directory was created
base::FilePath trace_dir = temp_dir_.GetPath().Append(trace_id);
EXPECT_TRUE(base::DirectoryExists(trace_dir));
// Verify the trace status is "Tracing"
EXPECT_EQ(manager_->GetTraceStatus(trace_id), "Tracing");
}
TEST_F(TraceManagerTest, StartTraceWithoutProcesses) {
std::string app_name = "test_app";
std::vector<std::string> empty_process_names;
std::string trace_id =
manager_->StartTrace(app_name, empty_process_names, {}, {});
EXPECT_TRUE(trace_id.empty());
}
TEST_F(TraceManagerTest, StartMultipleTraces) {
std::string app1 = "app1";
std::string app2 = "app2";
std::vector<std::string> process_names1 = {"chrome"};
std::vector<std::string> process_names2 = {"firefox"};
std::string trace_id1 = manager_->StartTrace(app1, process_names1, {}, {});
std::string trace_id2 = manager_->StartTrace(app2, process_names2, {}, {});
EXPECT_FALSE(trace_id1.empty());
EXPECT_FALSE(trace_id2.empty());
EXPECT_NE(trace_id1, trace_id2);
// Give traces a moment to start, then check status
task_environment_->RunUntilIdle();
// Stop traces to avoid error state from mock reader
manager_->StopTrace(trace_id1);
manager_->StopTrace(trace_id2);
// Run tasks to complete processing
task_environment_->RunUntilIdle();
}
TEST_F(TraceManagerTest, StopTrace) {
// Ensure reader doesn't hit EOF while tracing.
mock_trace_reader_->set_max_reads(-1);
std::vector<std::string> process_names = {"chrome"};
std::string trace_id =
manager_->StartTrace("test_app", process_names, {}, {});
ASSERT_FALSE(trace_id.empty());
// Ensure there is at least an empty raw file so processing doesn't fail.
base::FilePath raw_path =
temp_dir_.GetPath().Append(trace_id).Append("trace.raw");
ASSERT_TRUE(base::CreateDirectory(raw_path.DirName()));
base::WriteFile(raw_path, "");
bool result = manager_->StopTrace(trace_id);
EXPECT_TRUE(result);
// Allow some time for async processing
task_environment_->RunUntilIdle();
// Status should eventually become "Processing" or "Completed"
std::string status = manager_->GetTraceStatus(trace_id);
EXPECT_THAT(status, ::testing::AnyOf(::testing::Eq("Processing"),
::testing::Eq("Completed")));
}
TEST_F(TraceManagerTest, StopMissingTrace) {
bool result = manager_->StopTrace("nonexistent_trace_id");
EXPECT_FALSE(result);
}
TEST_F(TraceManagerTest, StopTwice) {
std::vector<std::string> process_names = {"chrome"};
std::string trace_id =
manager_->StartTrace("test_app", process_names, {}, {});
ASSERT_FALSE(trace_id.empty());
// Stop the trace once
EXPECT_TRUE(manager_->StopTrace(trace_id));
// Try to stop it again
EXPECT_FALSE(manager_->StopTrace(trace_id));
}
TEST_F(TraceManagerTest, CancelTrace) {
std::vector<std::string> process_names = {"chrome"};
std::string trace_id =
manager_->StartTrace("test_app", process_names, {}, {});
ASSERT_FALSE(trace_id.empty());
bool result = manager_->CancelTrace(trace_id);
EXPECT_TRUE(result);
EXPECT_EQ(manager_->GetTraceStatus(trace_id), "Cancelled");
}
TEST_F(TraceManagerTest, CancelMissingTrace) {
bool result = manager_->CancelTrace("nonexistent_trace_id");
EXPECT_FALSE(result);
}
TEST_F(TraceManagerTest, GetStatusMissingTrace) {
std::string status = manager_->GetTraceStatus("nonexistent_trace_id");
EXPECT_TRUE(status.empty());
}
TEST_F(TraceManagerTest, GetPathWhileTracing) {
std::vector<std::string> process_names = {"chrome"};
std::string trace_id =
manager_->StartTrace("test_app", process_names, {}, {});
ASSERT_FALSE(trace_id.empty());
// Should return empty path while tracing
std::string path = manager_->GetTracePath(trace_id);
EXPECT_TRUE(path.empty());
}
TEST_F(TraceManagerTest, GetPathAfterStop) {
std::vector<std::string> process_names = {"chrome"};
std::string trace_id =
manager_->StartTrace("test_app", process_names, {}, {});
ASSERT_FALSE(trace_id.empty());
// Stop and wait for completion
EXPECT_TRUE(manager_->StopTrace(trace_id));
task_environment_->RunUntilIdle();
// Should return the trace file path once completed
std::string path = manager_->GetTracePath(trace_id);
if (manager_->GetTraceStatus(trace_id) == "Completed") {
EXPECT_FALSE(path.empty());
EXPECT_TRUE(path.find(trace_id) != std::string::npos);
}
}
TEST_F(TraceManagerTest, GetPathMissingTrace) {
std::string path = manager_->GetTracePath("nonexistent_trace_id");
EXPECT_TRUE(path.empty());
}
TEST_F(TraceManagerTest, Timeout) {
mock_trace_reader_->set_max_reads(100);
std::vector<std::string> process_names = {"chrome"};
std::string trace_id =
manager_->StartTrace("test_app", process_names, {}, {});
ASSERT_FALSE(trace_id.empty());
// Verify initial state.
EXPECT_EQ(manager_->GetTraceStatus(trace_id), "Tracing");
// Fast forward time by the timeout amount.
task_environment_->FastForwardBy(base::Minutes(5));
// Run tasks to process the timeout
task_environment_->RunUntilIdle();
// The trace should have been stopped by the timeout handler.
// Since we don't manually create a raw file, ProcessTrace might fail,
// but the important thing is that it's no longer "Tracing".
std::string status = manager_->GetTraceStatus(trace_id);
EXPECT_NE(status, "Tracing");
}
TEST_F(TraceManagerTest, MultipleOperations) {
std::vector<std::string> process_names = {"chrome"};
std::string trace_id =
manager_->StartTrace("test_app", process_names, {}, {});
ASSERT_FALSE(trace_id.empty());
// Try multiple operations
EXPECT_EQ(manager_->GetTraceStatus(trace_id), "Tracing");
EXPECT_TRUE(manager_->GetTracePath(trace_id).empty()); // Not completed yet
// Stop the trace
EXPECT_TRUE(manager_->StopTrace(trace_id));
// Try to stop again (should fail)
EXPECT_FALSE(manager_->StopTrace(trace_id));
// Try to cancel after stopping (should still work)
EXPECT_TRUE(manager_->CancelTrace(trace_id));
EXPECT_EQ(manager_->GetTraceStatus(trace_id), "Cancelled");
}
TEST_F(TraceManagerTest, UniqueIds) {
std::vector<std::string> process_names = {"chrome"};
// Create multiple traces and verify they have unique IDs
std::string trace_id1 = manager_->StartTrace("app1", process_names, {}, {});
std::string trace_id2 = manager_->StartTrace("app2", process_names, {}, {});
std::string trace_id3 = manager_->StartTrace("app3", process_names, {}, {});
EXPECT_FALSE(trace_id1.empty());
EXPECT_FALSE(trace_id2.empty());
EXPECT_FALSE(trace_id3.empty());
EXPECT_NE(trace_id1, trace_id2);
EXPECT_NE(trace_id2, trace_id3);
EXPECT_NE(trace_id1, trace_id3);
}
TEST_F(TraceManagerTest, PidFiltering) {
// This test verifies that ProcessTrace correctly filters by PID during
// the offline processing phase (not during the live trace reading phase).
// We'll create a trace that includes lines from multiple PIDs.
std::string trace_id =
manager_->StartTrace("test_app", {"some_process"}, {}, {});
ASSERT_FALSE(trace_id.empty());
// Manually create a raw trace file with data from multiple PIDs.
// The key insight is that ProcessTrace doesn't filter by PID - that's done
// during live tracing. ProcessTrace only filters by path patterns.
base::FilePath raw_path =
temp_dir_.GetPath().Append(trace_id).Append("trace.raw");
std::string raw_content =
"some-process-123 [000] ... event: file=\"/path/to/allowed_file\"\n"
"other-process-456 [000] ... event: file=\"/path/to/other_file\"\n";
ASSERT_TRUE(base::WriteFile(raw_path, raw_content));
// Stop the trace and wait for processing.
ASSERT_TRUE(manager_->StopTrace(trace_id));
task_environment_->RunUntilIdle();
ASSERT_EQ(manager_->GetTraceStatus(trace_id), "Completed");
// Check the final trace file.
base::FilePath final_path =
temp_dir_.GetPath().Append(trace_id).Append("trace.log");
std::string final_content;
ASSERT_TRUE(base::ReadFileToString(final_path, &final_content));
// Since we don't have path filtering rules, both lines should be present.
// This test demonstrates that ProcessTrace preserves all lines from the raw
// file.
EXPECT_NE(final_content.find("/path/to/allowed_file"), std::string::npos);
EXPECT_NE(final_content.find("/path/to/other_file"), std::string::npos);
}
// Test error conditions and edge cases
TEST_F(TraceManagerTest, EmptyAppName) {
std::vector<std::string> process_names = {"chrome"};
// Test with empty app name
std::string trace_id = manager_->StartTrace("", process_names, {}, {});
EXPECT_FALSE(trace_id.empty()); // Should still work with empty app name
}
TEST_F(TraceManagerTest, ManyProcesses) {
std::vector<std::string> large_process_list;
for (int i = 0; i < 100; ++i) {
large_process_list.push_back("process_" + std::to_string(i));
}
std::string trace_id =
manager_->StartTrace("large_app", large_process_list, {}, {});
EXPECT_FALSE(trace_id.empty());
EXPECT_EQ(manager_->GetTraceStatus(trace_id), "Tracing");
}
TEST_F(TraceManagerTest, CreateAndStart) {
std::string trace_id = manager_->StartTrace("test_app", {"process1"}, {}, {});
ASSERT_FALSE(trace_id.empty());
ASSERT_EQ(manager_->GetTraceStatus(trace_id), "Tracing");
}
TEST_F(TraceManagerTest, StartAndStopTrace) {
std::string trace_id = manager_->StartTrace("test_app", {"process1"}, {}, {});
ASSERT_FALSE(trace_id.empty());
base::FilePath trace_dir = temp_dir_.GetPath().Append(trace_id);
ASSERT_TRUE(base::DirectoryExists(trace_dir));
ASSERT_TRUE(manager_->StopTrace(trace_id));
task_environment_->RunUntilIdle(); // Wait for processing to complete.
ASSERT_EQ(manager_->GetTraceStatus(trace_id), "Completed");
}
TEST_F(TraceManagerTest, PathFiltering) {
// Start a trace with path filtering rules.
std::string trace_id = manager_->StartTrace(
"test_app", {"process1"},
{"/usr/lib/.*", "/opt/google/chrome/.*"}, // path_allowlist
{".*\\.log", "/tmp/.*"}); // path_denylist
ASSERT_FALSE(trace_id.empty());
// Manually create a fake raw trace file.
base::FilePath raw_path =
temp_dir_.GetPath().Append(trace_id).Append("trace.raw");
std::string raw_content =
"# TRACE HEADER\n"
"some-proc-123 ... event: file=\"/usr/lib/libfoo.so\"\n"
"some-proc-123 ... event: file=\"/opt/google/chrome/chrome\"\n"
"some-proc-123 ... event: file=\"/home/user/somefile.txt\"\n"
"some-proc-123 ... event: file=\"/var/log/messages.log\"\n"
"some-proc-123 ... event: file=\"/usr/lib/some.log\"\n"
"some-proc-123 ... event: file=\"/tmp/tempfile\"\n";
ASSERT_TRUE(base::WriteFile(raw_path, raw_content));
// Stop the trace to trigger processing.
ASSERT_TRUE(manager_->StopTrace(trace_id));
task_environment_->RunUntilIdle();
ASSERT_EQ(manager_->GetTraceStatus(trace_id), "Completed");
// Check the final processed trace file.
base::FilePath final_path =
temp_dir_.GetPath().Append(trace_id).Append("trace.log");
std::string final_content;
ASSERT_TRUE(base::ReadFileToString(final_path, &final_content));
// Verify that only the allowed paths are present.
EXPECT_NE(final_content.find("/usr/lib/libfoo.so"), std::string::npos);
EXPECT_NE(final_content.find("/opt/google/chrome/chrome"), std::string::npos);
EXPECT_EQ(final_content.find("/home/user/somefile.txt"), std::string::npos);
EXPECT_EQ(final_content.find("/var/log/messages.log"), std::string::npos);
EXPECT_EQ(final_content.find("/usr/lib/some.log"), std::string::npos);
EXPECT_EQ(final_content.find("/tmp/tempfile"), std::string::npos);
}
TEST_F(TraceManagerTest, InvalidRegex) {
// Pass an invalid regex to the allow list. RE2 will mark it !ok() and we
// expect tracing to continue without crashing.
std::string trace_id =
manager_->StartTrace("test_app", {"proc1"}, {"[unclosed_regex"}, {});
ASSERT_FALSE(trace_id.empty());
// Manually create an empty raw file
base::FilePath raw_path =
temp_dir_.GetPath().Append(trace_id).Append("trace.raw");
ASSERT_TRUE(base::WriteFile(raw_path, ""));
// Stop the trace to force processing.
ASSERT_TRUE(manager_->StopTrace(trace_id));
task_environment_->RunUntilIdle();
// Even with invalid regex, trace should complete (or at least proceed to
// Processing state) instead of erroring out.
std::string status = manager_->GetTraceStatus(trace_id);
EXPECT_THAT(status, ::testing::AnyOf(::testing::Eq("Processing"),
::testing::Eq("Completed")));
}
TEST_F(TraceManagerTest, DenyOnlyPathFiltering) {
// Start a trace that only has a deny list (no allow list).
std::string trace_id = manager_->StartTrace("test_app", {"proc1"},
/*allow*/ {}, {".*/secret.txt"});
ASSERT_FALSE(trace_id.empty());
// Craft raw trace containing one denied and one allowed path.
base::FilePath raw_path =
temp_dir_.GetPath().Append(trace_id).Append("trace.raw");
std::string raw_content =
"# HEADER\n"
"x-123 event: file=\"/var/data/normal.txt\"\n"
"x-123 event: file=\"/home/user/secret.txt\"\n"; // should be denied
ASSERT_TRUE(base::WriteFile(raw_path, raw_content));
// Stop trace and process.
ASSERT_TRUE(manager_->StopTrace(trace_id));
task_environment_->RunUntilIdle();
ASSERT_EQ(manager_->GetTraceStatus(trace_id), "Completed");
// Validate results.
base::FilePath final_path =
temp_dir_.GetPath().Append(trace_id).Append("trace.log");
std::string final_content;
ASSERT_TRUE(base::ReadFileToString(final_path, &final_content));
EXPECT_NE(final_content.find("/var/data/normal.txt"), std::string::npos);
EXPECT_EQ(final_content.find("/home/user/secret.txt"), std::string::npos);
}
} // namespace farfetchd