Step 51 TDD test: Text-AST synchronization
Tests bidirectional sync: text->parse->AST, AST mutation->text regen, round-trip preservation, debounce coalescing of rapid edits, and C++ text sync. Defines TextASTSync interface. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -157,6 +157,9 @@ target_include_directories(step46_test PRIVATE src)
|
||||
add_executable(step47_test tests/step47_test.cpp)
|
||||
target_include_directories(step47_test PRIVATE src)
|
||||
|
||||
add_executable(step51_test tests/step51_test.cpp)
|
||||
target_include_directories(step51_test PRIVATE src)
|
||||
|
||||
add_executable(whetstone_editor src/main.cpp)
|
||||
target_include_directories(whetstone_editor PRIVATE src)
|
||||
# find_package(SDL2 REQUIRED) # Commented out for now to avoid build issues
|
||||
|
||||
156
editor/tests/step51_test.cpp
Normal file
156
editor/tests/step51_test.cpp
Normal file
@@ -0,0 +1,156 @@
|
||||
// Step 51 TDD Test: Text → AST synchronization
|
||||
//
|
||||
// Tests bidirectional sync between text editing and AST:
|
||||
// 1. Text change → re-parse via tree-sitter → AST updates
|
||||
// 2. AST change (structured edit) → regenerate text
|
||||
// 3. Round-trip: text → AST → text produces semantically equivalent output
|
||||
// 4. Debouncing: rapid text changes don't cause excessive re-parsing
|
||||
//
|
||||
// Will fail until the text-AST sync layer is implemented.
|
||||
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <cassert>
|
||||
#include <memory>
|
||||
#include "ast/Parser.h"
|
||||
#include "ast/Generator.h"
|
||||
|
||||
static bool contains(const std::string& haystack, const std::string& needle) {
|
||||
return haystack.find(needle) != std::string::npos;
|
||||
}
|
||||
|
||||
// Forward declaration for the sync class that doesn't exist yet
|
||||
// This will need to be implemented as part of Step 51
|
||||
class TextASTSync {
|
||||
public:
|
||||
// Set the source text; triggers re-parse (possibly debounced)
|
||||
void setText(const std::string& text, const std::string& language);
|
||||
|
||||
// Get the current AST root
|
||||
Module* getAST() const;
|
||||
|
||||
// Mutate the AST; triggers text regeneration
|
||||
void setAST(std::unique_ptr<Module> module);
|
||||
|
||||
// Get the current text representation
|
||||
std::string getText() const;
|
||||
|
||||
// Force immediate sync (skip debounce)
|
||||
void syncNow();
|
||||
|
||||
// Check if a re-parse is pending (debounced but not yet executed)
|
||||
bool isParsePending() const;
|
||||
};
|
||||
|
||||
int main() {
|
||||
int passed = 0;
|
||||
int failed = 0;
|
||||
|
||||
// --- Test 1: Set text → AST updates ---
|
||||
{
|
||||
TextASTSync sync;
|
||||
sync.setText("def greet(name):\n return name\n", "python");
|
||||
sync.syncNow();
|
||||
|
||||
Module* ast = sync.getAST();
|
||||
assert(ast != nullptr && "AST should be non-null after setText");
|
||||
|
||||
auto functions = ast->getChildren("functions");
|
||||
assert(!functions.empty() && "AST should have parsed functions");
|
||||
|
||||
auto* fn = static_cast<Function*>(functions[0]);
|
||||
assert(fn->name == "greet" && "Function name should be 'greet'");
|
||||
|
||||
std::cout << "Test 1 PASS: setText → AST has correct function" << std::endl;
|
||||
++passed;
|
||||
}
|
||||
|
||||
// --- Test 2: AST change → text updates ---
|
||||
{
|
||||
TextASTSync sync;
|
||||
sync.setText("def f(x):\n return x\n", "python");
|
||||
sync.syncNow();
|
||||
|
||||
// Modify AST: change function name
|
||||
Module* ast = sync.getAST();
|
||||
auto functions = ast->getChildren("functions");
|
||||
auto* fn = static_cast<Function*>(functions[0]);
|
||||
fn->name = "compute";
|
||||
|
||||
// Trigger text regeneration from modified AST
|
||||
// (setAST or a notifyASTChanged method)
|
||||
std::string newText = sync.getText();
|
||||
assert(contains(newText, "compute") && "Text should reflect renamed function");
|
||||
assert(!contains(newText, "def f(") && "Old function name should be gone");
|
||||
|
||||
std::cout << "Test 2 PASS: AST change → text updates" << std::endl;
|
||||
++passed;
|
||||
}
|
||||
|
||||
// --- Test 3: Round-trip text → AST → text ---
|
||||
{
|
||||
std::string original = "def add(a, b):\n return a + b\n";
|
||||
TextASTSync sync;
|
||||
sync.setText(original, "python");
|
||||
sync.syncNow();
|
||||
|
||||
// Get the regenerated text from the AST
|
||||
std::string roundTripped = sync.getText();
|
||||
|
||||
// The output should be semantically equivalent
|
||||
assert(contains(roundTripped, "def add") && "Should contain function name");
|
||||
assert(contains(roundTripped, "a") && "Should contain parameter a");
|
||||
assert(contains(roundTripped, "b") && "Should contain parameter b");
|
||||
assert(contains(roundTripped, "return") && "Should contain return");
|
||||
assert(contains(roundTripped, "+") && "Should contain operator");
|
||||
|
||||
std::cout << "Test 3 PASS: Round-trip preserves semantics" << std::endl;
|
||||
++passed;
|
||||
}
|
||||
|
||||
// --- Test 4: Debounce — rapid changes don't cause immediate re-parse ---
|
||||
{
|
||||
TextASTSync sync;
|
||||
sync.setText("def a():\n pass\n", "python");
|
||||
// Don't call syncNow — simulate rapid edits
|
||||
sync.setText("def ab():\n pass\n", "python");
|
||||
sync.setText("def abc():\n pass\n", "python");
|
||||
|
||||
// Parse should be pending but not yet executed
|
||||
assert(sync.isParsePending() && "Parse should be pending after rapid edits");
|
||||
|
||||
// Now force sync
|
||||
sync.syncNow();
|
||||
assert(!sync.isParsePending() && "Parse should no longer be pending after syncNow");
|
||||
|
||||
Module* ast = sync.getAST();
|
||||
auto functions = ast->getChildren("functions");
|
||||
auto* fn = static_cast<Function*>(functions[0]);
|
||||
// Should have the LAST text, not intermediate ones
|
||||
assert(fn->name == "abc" && "Should have parsed the final text, not intermediate");
|
||||
|
||||
std::cout << "Test 4 PASS: Debounce coalesces rapid edits" << std::endl;
|
||||
++passed;
|
||||
}
|
||||
|
||||
// --- Test 5: C++ text → AST sync ---
|
||||
{
|
||||
TextASTSync sync;
|
||||
sync.setText("int f(int x) { return x + 1; }", "cpp");
|
||||
sync.syncNow();
|
||||
|
||||
Module* ast = sync.getAST();
|
||||
assert(ast != nullptr && "AST should be non-null for C++");
|
||||
assert(ast->targetLanguage == "cpp" && "Target language should be cpp");
|
||||
|
||||
auto functions = ast->getChildren("functions");
|
||||
assert(!functions.empty() && "Should parse C++ function");
|
||||
|
||||
std::cout << "Test 5 PASS: C++ text → AST sync works" << std::endl;
|
||||
++passed;
|
||||
}
|
||||
|
||||
// --- Summary ---
|
||||
std::cout << "\n=== Step 51 Results: " << passed << " passed, " << failed << " failed ===" << std::endl;
|
||||
return failed > 0 ? 1 : 0;
|
||||
}
|
||||
Reference in New Issue
Block a user