Step 567: add leak corruption signal bridge
This commit is contained in:
@@ -3982,4 +3982,13 @@ target_link_libraries(step566_test PRIVATE
|
|||||||
tree_sitter_javascript tree_sitter_typescript
|
tree_sitter_javascript tree_sitter_typescript
|
||||||
tree_sitter_java tree_sitter_rust tree_sitter_go)
|
tree_sitter_java tree_sitter_rust tree_sitter_go)
|
||||||
|
|
||||||
|
add_executable(step567_test tests/step567_test.cpp)
|
||||||
|
target_include_directories(step567_test PRIVATE src)
|
||||||
|
target_link_libraries(step567_test PRIVATE
|
||||||
|
nlohmann_json::nlohmann_json
|
||||||
|
unofficial::tree-sitter::tree-sitter
|
||||||
|
tree_sitter_python tree_sitter_cpp tree_sitter_elisp
|
||||||
|
tree_sitter_javascript tree_sitter_typescript
|
||||||
|
tree_sitter_java tree_sitter_rust tree_sitter_go)
|
||||||
|
|
||||||
# Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies)
|
# Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies)
|
||||||
|
|||||||
154
editor/src/LeakCorruptionSignalBridge.h
Normal file
154
editor/src/LeakCorruptionSignalBridge.h
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
#pragma once
|
||||||
|
// Step 567: Leak/Corruption Signal Bridge
|
||||||
|
|
||||||
|
#include "DebugValidationUtil.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
enum class MemorySignalType {
|
||||||
|
LeakSuspected,
|
||||||
|
LeakConfirmed,
|
||||||
|
CorruptionSuspected,
|
||||||
|
CorruptionConfirmed
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class MemorySignalSeverity {
|
||||||
|
Info = 0,
|
||||||
|
Warning = 1,
|
||||||
|
Error = 2,
|
||||||
|
Critical = 3
|
||||||
|
};
|
||||||
|
|
||||||
|
struct MemorySignalRecord {
|
||||||
|
std::string signalId;
|
||||||
|
std::string sessionId;
|
||||||
|
std::string allocationId;
|
||||||
|
MemorySignalType type = MemorySignalType::LeakSuspected;
|
||||||
|
std::string message;
|
||||||
|
std::uint64_t timestamp = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct MemoryDiagnosticRecord {
|
||||||
|
std::string diagnosticId;
|
||||||
|
std::string sessionId;
|
||||||
|
std::string allocationId;
|
||||||
|
std::string paneKey;
|
||||||
|
std::string message;
|
||||||
|
MemorySignalSeverity severity = MemorySignalSeverity::Info;
|
||||||
|
std::uint64_t timestamp = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class LeakCorruptionSignalBridge {
|
||||||
|
public:
|
||||||
|
bool ingestSignal(const MemorySignalRecord& signal, std::string* error) {
|
||||||
|
if (!error) return false;
|
||||||
|
error->clear();
|
||||||
|
if (!hasRequiredDebugIds(signal.signalId, signal.sessionId)) return fail(error, "signal_or_session_missing");
|
||||||
|
if (signal.allocationId.empty()) return fail(error, "allocation_id_missing");
|
||||||
|
if (signal.message.empty()) return fail(error, "signal_message_missing");
|
||||||
|
if (signal.timestamp == 0) return fail(error, "signal_timestamp_invalid");
|
||||||
|
if (containsSignal(signal.signalId)) return fail(error, "signal_duplicate");
|
||||||
|
|
||||||
|
signals_.push_back(signal);
|
||||||
|
diagnostics_.push_back(buildDiagnostic(signal));
|
||||||
|
stableSortByTimestamp(&signals_);
|
||||||
|
stableSortByTimestamp(&diagnostics_);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<MemoryDiagnosticRecord> diagnosticsForSession(const std::string& sessionId) const {
|
||||||
|
std::vector<MemoryDiagnosticRecord> filtered;
|
||||||
|
for (const auto& diagnostic : diagnostics_) {
|
||||||
|
if (sessionId.empty() || diagnostic.sessionId == sessionId) filtered.push_back(diagnostic);
|
||||||
|
}
|
||||||
|
return filtered;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<MemorySignalRecord> signalsForAllocation(const std::string& allocationId) const {
|
||||||
|
std::vector<MemorySignalRecord> filtered;
|
||||||
|
for (const auto& signal : signals_) {
|
||||||
|
if (allocationId.empty() || signal.allocationId == allocationId) filtered.push_back(signal);
|
||||||
|
}
|
||||||
|
return filtered;
|
||||||
|
}
|
||||||
|
|
||||||
|
MemorySignalSeverity highestSeverityForSession(const std::string& sessionId) const {
|
||||||
|
MemorySignalSeverity highest = MemorySignalSeverity::Info;
|
||||||
|
for (const auto& diagnostic : diagnostics_) {
|
||||||
|
if (!sessionId.empty() && diagnostic.sessionId != sessionId) continue;
|
||||||
|
if (static_cast<int>(diagnostic.severity) > static_cast<int>(highest)) highest = diagnostic.severity;
|
||||||
|
}
|
||||||
|
return highest;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<MemorySignalRecord> signals_;
|
||||||
|
std::vector<MemoryDiagnosticRecord> diagnostics_;
|
||||||
|
|
||||||
|
static bool fail(std::string* error, const char* code) {
|
||||||
|
*error = code;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool containsSignal(const std::string& signalId) const {
|
||||||
|
for (const auto& signal : signals_) {
|
||||||
|
if (signal.signalId == signalId) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static MemoryDiagnosticRecord buildDiagnostic(const MemorySignalRecord& signal) {
|
||||||
|
MemoryDiagnosticRecord diagnostic;
|
||||||
|
diagnostic.diagnosticId = "diag-" + signal.signalId;
|
||||||
|
diagnostic.sessionId = signal.sessionId;
|
||||||
|
diagnostic.allocationId = signal.allocationId;
|
||||||
|
diagnostic.message = signal.message;
|
||||||
|
diagnostic.timestamp = signal.timestamp;
|
||||||
|
diagnostic.paneKey = paneForSignal(signal.type);
|
||||||
|
diagnostic.severity = severityForSignal(signal.type);
|
||||||
|
return diagnostic;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string paneForSignal(MemorySignalType type) {
|
||||||
|
switch (type) {
|
||||||
|
case MemorySignalType::LeakSuspected:
|
||||||
|
case MemorySignalType::LeakConfirmed:
|
||||||
|
return "memory-leaks";
|
||||||
|
case MemorySignalType::CorruptionSuspected:
|
||||||
|
case MemorySignalType::CorruptionConfirmed:
|
||||||
|
return "memory-corruption";
|
||||||
|
}
|
||||||
|
return "memory-diagnostics";
|
||||||
|
}
|
||||||
|
|
||||||
|
static MemorySignalSeverity severityForSignal(MemorySignalType type) {
|
||||||
|
switch (type) {
|
||||||
|
case MemorySignalType::LeakSuspected: return MemorySignalSeverity::Warning;
|
||||||
|
case MemorySignalType::LeakConfirmed: return MemorySignalSeverity::Error;
|
||||||
|
case MemorySignalType::CorruptionSuspected: return MemorySignalSeverity::Error;
|
||||||
|
case MemorySignalType::CorruptionConfirmed: return MemorySignalSeverity::Critical;
|
||||||
|
}
|
||||||
|
return MemorySignalSeverity::Info;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void stableSortByTimestamp(std::vector<MemorySignalRecord>* signals) {
|
||||||
|
std::stable_sort(signals->begin(),
|
||||||
|
signals->end(),
|
||||||
|
[](const MemorySignalRecord& a, const MemorySignalRecord& b) {
|
||||||
|
if (a.timestamp != b.timestamp) return a.timestamp < b.timestamp;
|
||||||
|
return a.signalId < b.signalId;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static void stableSortByTimestamp(std::vector<MemoryDiagnosticRecord>* diagnostics) {
|
||||||
|
std::stable_sort(diagnostics->begin(),
|
||||||
|
diagnostics->end(),
|
||||||
|
[](const MemoryDiagnosticRecord& a, const MemoryDiagnosticRecord& b) {
|
||||||
|
if (a.timestamp != b.timestamp) return a.timestamp < b.timestamp;
|
||||||
|
return a.diagnosticId < b.diagnosticId;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
176
editor/tests/step567_test.cpp
Normal file
176
editor/tests/step567_test.cpp
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
// Step 567: Leak/Corruption Signal Bridge (12 tests)
|
||||||
|
|
||||||
|
#include "LeakCorruptionSignalBridge.h"
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
static int passed = 0, failed = 0;
|
||||||
|
#define TEST(name) { std::cout << " " << #name << "... "; }
|
||||||
|
#define PASS() { std::cout << "PASS\n"; ++passed; }
|
||||||
|
#define FAIL(msg) { std::cout << "FAIL: " << msg << "\n"; ++failed; }
|
||||||
|
#define CHECK(cond, msg) if (!(cond)) { FAIL(msg); return; } else {}
|
||||||
|
|
||||||
|
static MemorySignalRecord signal(const std::string& id,
|
||||||
|
const std::string& session,
|
||||||
|
const std::string& allocation,
|
||||||
|
MemorySignalType type,
|
||||||
|
std::uint64_t timestamp) {
|
||||||
|
MemorySignalRecord s;
|
||||||
|
s.signalId = id;
|
||||||
|
s.sessionId = session;
|
||||||
|
s.allocationId = allocation;
|
||||||
|
s.type = type;
|
||||||
|
s.message = "message-" + id;
|
||||||
|
s.timestamp = timestamp;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_ingest_leak_suspected_signal_success() {
|
||||||
|
TEST(ingest_leak_suspected_signal_success);
|
||||||
|
LeakCorruptionSignalBridge bridge;
|
||||||
|
std::string error;
|
||||||
|
CHECK(bridge.ingestSignal(signal("s1", "sess-1", "alloc-1", MemorySignalType::LeakSuspected, 5), &error), "ingest should succeed");
|
||||||
|
const auto diagnostics = bridge.diagnosticsForSession("sess-1");
|
||||||
|
CHECK(diagnostics.size() == 1, "expected one diagnostic");
|
||||||
|
CHECK(diagnostics[0].paneKey == "memory-leaks", "leak pane expected");
|
||||||
|
CHECK(diagnostics[0].severity == MemorySignalSeverity::Warning, "warning severity expected");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_ingest_corruption_confirmed_maps_to_critical() {
|
||||||
|
TEST(ingest_corruption_confirmed_maps_to_critical);
|
||||||
|
LeakCorruptionSignalBridge bridge;
|
||||||
|
std::string error;
|
||||||
|
CHECK(bridge.ingestSignal(signal("s1", "sess-1", "alloc-1", MemorySignalType::CorruptionConfirmed, 5), &error), "ingest should succeed");
|
||||||
|
const auto diagnostics = bridge.diagnosticsForSession("sess-1");
|
||||||
|
CHECK(diagnostics[0].paneKey == "memory-corruption", "corruption pane expected");
|
||||||
|
CHECK(diagnostics[0].severity == MemorySignalSeverity::Critical, "critical severity expected");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_ingest_rejects_missing_signal_or_session_id() {
|
||||||
|
TEST(ingest_rejects_missing_signal_or_session_id);
|
||||||
|
LeakCorruptionSignalBridge bridge;
|
||||||
|
std::string error;
|
||||||
|
auto s = signal("", "sess-1", "alloc-1", MemorySignalType::LeakSuspected, 5);
|
||||||
|
CHECK(!bridge.ingestSignal(s, &error), "ingest should fail");
|
||||||
|
CHECK(error == "signal_or_session_missing", "wrong error");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_ingest_rejects_missing_allocation_id() {
|
||||||
|
TEST(ingest_rejects_missing_allocation_id);
|
||||||
|
LeakCorruptionSignalBridge bridge;
|
||||||
|
std::string error;
|
||||||
|
auto s = signal("s1", "sess-1", "", MemorySignalType::LeakSuspected, 5);
|
||||||
|
CHECK(!bridge.ingestSignal(s, &error), "ingest should fail");
|
||||||
|
CHECK(error == "allocation_id_missing", "wrong error");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_ingest_rejects_missing_message() {
|
||||||
|
TEST(ingest_rejects_missing_message);
|
||||||
|
LeakCorruptionSignalBridge bridge;
|
||||||
|
std::string error;
|
||||||
|
auto s = signal("s1", "sess-1", "alloc-1", MemorySignalType::LeakSuspected, 5);
|
||||||
|
s.message = "";
|
||||||
|
CHECK(!bridge.ingestSignal(s, &error), "ingest should fail");
|
||||||
|
CHECK(error == "signal_message_missing", "wrong error");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_ingest_rejects_zero_timestamp() {
|
||||||
|
TEST(ingest_rejects_zero_timestamp);
|
||||||
|
LeakCorruptionSignalBridge bridge;
|
||||||
|
std::string error;
|
||||||
|
auto s = signal("s1", "sess-1", "alloc-1", MemorySignalType::LeakSuspected, 0);
|
||||||
|
CHECK(!bridge.ingestSignal(s, &error), "ingest should fail");
|
||||||
|
CHECK(error == "signal_timestamp_invalid", "wrong error");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_ingest_rejects_duplicate_signal_id() {
|
||||||
|
TEST(ingest_rejects_duplicate_signal_id);
|
||||||
|
LeakCorruptionSignalBridge bridge;
|
||||||
|
std::string error;
|
||||||
|
auto s = signal("s1", "sess-1", "alloc-1", MemorySignalType::LeakSuspected, 5);
|
||||||
|
CHECK(bridge.ingestSignal(s, &error), "first ingest should succeed");
|
||||||
|
CHECK(!bridge.ingestSignal(s, &error), "duplicate ingest should fail");
|
||||||
|
CHECK(error == "signal_duplicate", "wrong error");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_diagnostics_for_session_filters_results() {
|
||||||
|
TEST(diagnostics_for_session_filters_results);
|
||||||
|
LeakCorruptionSignalBridge bridge;
|
||||||
|
std::string error;
|
||||||
|
CHECK(bridge.ingestSignal(signal("s1", "sess-1", "alloc-1", MemorySignalType::LeakConfirmed, 5), &error), "ingest s1 failed");
|
||||||
|
CHECK(bridge.ingestSignal(signal("s2", "sess-2", "alloc-2", MemorySignalType::LeakConfirmed, 6), &error), "ingest s2 failed");
|
||||||
|
const auto diagnostics = bridge.diagnosticsForSession("sess-2");
|
||||||
|
CHECK(diagnostics.size() == 1, "expected one filtered diagnostic");
|
||||||
|
CHECK(diagnostics[0].sessionId == "sess-2", "wrong session");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_signals_for_allocation_filters_results() {
|
||||||
|
TEST(signals_for_allocation_filters_results);
|
||||||
|
LeakCorruptionSignalBridge bridge;
|
||||||
|
std::string error;
|
||||||
|
CHECK(bridge.ingestSignal(signal("s1", "sess-1", "alloc-1", MemorySignalType::LeakConfirmed, 5), &error), "ingest s1 failed");
|
||||||
|
CHECK(bridge.ingestSignal(signal("s2", "sess-1", "alloc-2", MemorySignalType::CorruptionSuspected, 6), &error), "ingest s2 failed");
|
||||||
|
const auto signals = bridge.signalsForAllocation("alloc-2");
|
||||||
|
CHECK(signals.size() == 1, "expected one filtered signal");
|
||||||
|
CHECK(signals[0].signalId == "s2", "wrong signal");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_highest_severity_for_session_aggregates() {
|
||||||
|
TEST(highest_severity_for_session_aggregates);
|
||||||
|
LeakCorruptionSignalBridge bridge;
|
||||||
|
std::string error;
|
||||||
|
CHECK(bridge.ingestSignal(signal("s1", "sess-1", "alloc-1", MemorySignalType::LeakSuspected, 5), &error), "ingest s1 failed");
|
||||||
|
CHECK(bridge.ingestSignal(signal("s2", "sess-1", "alloc-1", MemorySignalType::LeakConfirmed, 6), &error), "ingest s2 failed");
|
||||||
|
CHECK(bridge.ingestSignal(signal("s3", "sess-1", "alloc-1", MemorySignalType::CorruptionConfirmed, 7), &error), "ingest s3 failed");
|
||||||
|
CHECK(bridge.highestSeverityForSession("sess-1") == MemorySignalSeverity::Critical, "critical severity expected");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_highest_severity_defaults_to_info_when_empty() {
|
||||||
|
TEST(highest_severity_defaults_to_info_when_empty);
|
||||||
|
LeakCorruptionSignalBridge bridge;
|
||||||
|
CHECK(bridge.highestSeverityForSession("sess-1") == MemorySignalSeverity::Info, "default info expected");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_diagnostics_sorted_by_timestamp() {
|
||||||
|
TEST(diagnostics_sorted_by_timestamp);
|
||||||
|
LeakCorruptionSignalBridge bridge;
|
||||||
|
std::string error;
|
||||||
|
CHECK(bridge.ingestSignal(signal("s2", "sess-1", "alloc-1", MemorySignalType::LeakConfirmed, 20), &error), "ingest s2 failed");
|
||||||
|
CHECK(bridge.ingestSignal(signal("s1", "sess-1", "alloc-1", MemorySignalType::LeakSuspected, 10), &error), "ingest s1 failed");
|
||||||
|
const auto diagnostics = bridge.diagnosticsForSession("sess-1");
|
||||||
|
CHECK(diagnostics.size() == 2, "diagnostic count mismatch");
|
||||||
|
CHECK(diagnostics[0].diagnosticId == "diag-s1", "first diagnostic should be earliest");
|
||||||
|
CHECK(diagnostics[1].diagnosticId == "diag-s2", "second diagnostic should be later");
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
std::cout << "Step 567: Leak/Corruption Signal Bridge\n";
|
||||||
|
|
||||||
|
test_ingest_leak_suspected_signal_success(); // 1
|
||||||
|
test_ingest_corruption_confirmed_maps_to_critical(); // 2
|
||||||
|
test_ingest_rejects_missing_signal_or_session_id(); // 3
|
||||||
|
test_ingest_rejects_missing_allocation_id(); // 4
|
||||||
|
test_ingest_rejects_missing_message(); // 5
|
||||||
|
test_ingest_rejects_zero_timestamp(); // 6
|
||||||
|
test_ingest_rejects_duplicate_signal_id(); // 7
|
||||||
|
test_diagnostics_for_session_filters_results(); // 8
|
||||||
|
test_signals_for_allocation_filters_results(); // 9
|
||||||
|
test_highest_severity_for_session_aggregates(); // 10
|
||||||
|
test_highest_severity_defaults_to_info_when_empty(); // 11
|
||||||
|
test_diagnostics_sorted_by_timestamp(); // 12
|
||||||
|
|
||||||
|
std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n";
|
||||||
|
return failed == 0 ? 0 : 1;
|
||||||
|
}
|
||||||
35
progress.md
35
progress.md
@@ -10259,3 +10259,38 @@ deterministic timeline ordering, active-allocation tracking, and session filters
|
|||||||
- `editor/src/AllocationOwnershipTraceHooks.h` within header-size limit (`150` <= `600`)
|
- `editor/src/AllocationOwnershipTraceHooks.h` within header-size limit (`150` <= `600`)
|
||||||
- `editor/tests/step566_test.cpp` within test-file size guidance (`160` lines)
|
- `editor/tests/step566_test.cpp` within test-file size guidance (`160` lines)
|
||||||
- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`
|
- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`
|
||||||
|
|
||||||
|
### Step 567: Leak/Corruption Signal Bridge
|
||||||
|
**Status:** PASS (12/12 tests)
|
||||||
|
|
||||||
|
Implements a bridge that converts runtime leak/corruption signals into
|
||||||
|
session-scoped diagnostics and pane routing for memory debug surfaces.
|
||||||
|
|
||||||
|
**Files added:**
|
||||||
|
- `editor/src/LeakCorruptionSignalBridge.h` - signal bridge module:
|
||||||
|
- leak/corruption signal schema and severity mapping
|
||||||
|
- signal ingestion validation and duplicate guard
|
||||||
|
- diagnostic emission with pane routing (`memory-leaks` / `memory-corruption`)
|
||||||
|
- session/allocation filtering helpers
|
||||||
|
- session-level highest-severity aggregation
|
||||||
|
- `editor/tests/step567_test.cpp` - 12 tests covering:
|
||||||
|
- signal ingestion success and validation failures
|
||||||
|
- duplicate signal rejection behavior
|
||||||
|
- pane/severity mapping behavior
|
||||||
|
- session/allocation filtering behavior
|
||||||
|
- highest-severity aggregation behavior
|
||||||
|
- deterministic timestamp ordering behavior
|
||||||
|
|
||||||
|
**Files modified:**
|
||||||
|
- `editor/CMakeLists.txt` - `step567_test` target
|
||||||
|
|
||||||
|
**Verification run:**
|
||||||
|
- `cmake -S editor -B editor/build-native` - PASS
|
||||||
|
- `cmake --build editor/build-native --target step567_test step566_test` - PASS
|
||||||
|
- `./editor/build-native/step567_test` - PASS (12/12)
|
||||||
|
- `./editor/build-native/step566_test` - PASS (12/12) regression coverage
|
||||||
|
|
||||||
|
**Architecture gate check:**
|
||||||
|
- `editor/src/LeakCorruptionSignalBridge.h` within header-size limit (`154` <= `600`)
|
||||||
|
- `editor/tests/step567_test.cpp` within test-file size guidance (`176` lines)
|
||||||
|
- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`
|
||||||
|
|||||||
Reference in New Issue
Block a user