Files
whetstone_DSL/editor/tests/step295_test.cpp

306 lines
10 KiB
C++

// Step 295: Semanno Sidecar Integration (12 tests)
#include "SemannoSidecar.h"
#include "SidecarPersistence.h"
#include "ast/Module.h"
#include "ast/Function.h"
#include "ast/Annotation.h"
#include "EnvironmentSpec.h"
#include <iostream>
#include <filesystem>
#include <fstream>
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 std::string tmpDir() {
auto p = std::filesystem::temp_directory_path() / "whetstone_test_295";
std::filesystem::create_directories(p);
return p.string();
}
static void cleanup() {
std::filesystem::remove_all(std::filesystem::temp_directory_path() / "whetstone_test_295");
}
// Helper: create a module with annotations
static std::unique_ptr<Module> makeAnnotatedModule() {
auto mod = std::make_unique<Module>();
mod->name = "test_module";
auto fn = std::make_unique<Function>();
fn->name = "compute";
fn->spanStartLine = 5;
auto intent = std::make_unique<IntentAnnotation>();
intent->summary = "computes result";
intent->category = "pure";
fn->addChild("annotations", intent.release());
auto risk = std::make_unique<RiskAnnotation>();
risk->level = "low";
risk->reason = "simple math";
fn->addChild("annotations", risk.release());
mod->addChild("functions", fn.release());
return mod;
}
// 1. Sidecar path generation
void test_sidecar_path() {
TEST(sidecar_path);
std::string path = semannoSidecarPath("/workspace", "src/main.py");
CHECK(path.find(".whetstone") != std::string::npos, "missing .whetstone dir");
CHECK(path.find("src/main.py.semanno") != std::string::npos, "wrong extension");
PASS();
}
// 2. Save creates file on disk
void test_save_creates_file() {
TEST(save_creates_file);
cleanup();
auto mod = makeAnnotatedModule();
auto result = saveSemannoSidecar(tmpDir(), "test.py", mod.get());
CHECK(result.success, "save failed: " + result.error);
CHECK(result.annotationCount == 2, "wrong count: " + std::to_string(result.annotationCount));
CHECK(std::filesystem::exists(result.path), "file not created");
PASS();
}
// 3. Save content format (L<line>: @semanno:...)
void test_save_content_format() {
TEST(save_content_format);
cleanup();
auto mod = makeAnnotatedModule();
auto result = saveSemannoSidecar(tmpDir(), "test.py", mod.get());
CHECK(result.success, "save failed");
std::ifstream in(result.path);
std::string content((std::istreambuf_iterator<char>(in)),
std::istreambuf_iterator<char>());
CHECK(content.find("L5: @semanno:intent") != std::string::npos, "missing L5 intent");
CHECK(content.find("L5: @semanno:risk") != std::string::npos, "missing L5 risk");
PASS();
}
// 4. Load roundtrip
void test_load_roundtrip() {
TEST(load_roundtrip);
cleanup();
auto mod = makeAnnotatedModule();
saveSemannoSidecar(tmpDir(), "test.py", mod.get());
auto loaded = loadSemannoSidecar(tmpDir(), "test.py");
CHECK(loaded.success, "load failed: " + loaded.error);
CHECK(loaded.count == 2, "wrong count: " + std::to_string(loaded.count));
// Verify entries
bool hasIntent = false, hasRisk = false;
for (const auto& entry : loaded.entries) {
if (entry.type == "intent") hasIntent = true;
if (entry.type == "risk") hasRisk = true;
}
CHECK(hasIntent, "missing intent entry");
CHECK(hasRisk, "missing risk entry");
PASS();
}
// 5. Load nonexistent file returns error
void test_load_nonexistent() {
TEST(load_nonexistent);
cleanup();
auto loaded = loadSemannoSidecar(tmpDir(), "nonexistent.py");
CHECK(!loaded.success, "should fail");
CHECK(loaded.count == 0, "should have 0 entries");
PASS();
}
// 6. Save null AST returns error
void test_save_null_ast() {
TEST(save_null_ast);
auto result = saveSemannoSidecar(tmpDir(), "test.py", nullptr);
CHECK(!result.success, "should fail");
CHECK(result.error.find("No AST") != std::string::npos, "wrong error");
PASS();
}
// 7. Multiple annotation types in sidecar
void test_multiple_types() {
TEST(multiple_types);
cleanup();
auto mod = std::make_unique<Module>();
mod->name = "multi";
auto fn = std::make_unique<Function>();
fn->name = "process";
fn->spanStartLine = 10;
fn->addChild("annotations", new IntentAnnotation());
fn->addChild("annotations", new ComplexityAnnotation());
auto contract = new ContractAnnotation();
contract->preconditions = "x > 0";
fn->addChild("annotations", contract);
auto tags = new SemanticTagAnnotation();
tags->tags = {"io", "network"};
fn->addChild("annotations", tags);
mod->addChild("functions", fn.release());
auto result = saveSemannoSidecar(tmpDir(), "multi.py", mod.get());
CHECK(result.success, "save failed");
CHECK(result.annotationCount == 4, "wrong count: " + std::to_string(result.annotationCount));
auto loaded = loadSemannoSidecar(tmpDir(), "multi.py");
CHECK(loaded.count == 4, "loaded wrong count: " + std::to_string(loaded.count));
PASS();
}
// 8. Sidecar alongside .ast.json
void test_alongside_ast_sidecar() {
TEST(alongside_ast_sidecar);
cleanup();
auto mod = makeAnnotatedModule();
// Save both types
auto semannoResult = saveSemannoSidecar(tmpDir(), "test.py", mod.get());
CHECK(semannoResult.success, "semanno save failed");
// Also save AST sidecar using SidecarPersistence
auto astPath = sidecarPath(tmpDir(), "test.py");
namespace fs = std::filesystem;
fs::create_directories(fs::path(astPath).parent_path());
saveSidecarAST(tmpDir(), "test.py", mod.get());
// Both files should exist in same .whetstone dir
CHECK(fs::exists(semannoResult.path), "semanno file missing");
CHECK(fs::exists(astPath), "ast file missing");
// Both under .whetstone/
CHECK(semannoResult.path.find(".whetstone") != std::string::npos, "wrong dir");
CHECK(astPath.find(".whetstone") != std::string::npos, "wrong dir");
PASS();
}
// 9. Environment annotations in sidecar
void test_environment_annotations() {
TEST(environment_annotations);
cleanup();
auto mod = std::make_unique<Module>();
mod->name = "env_test";
auto fn = std::make_unique<Function>();
fn->name = "net_call";
fn->spanStartLine = 1;
auto cap = new CapabilityRequirement();
cap->capability = "io.net";
cap->required = true;
fn->addChild("annotations", cap);
mod->addChild("functions", fn.release());
auto result = saveSemannoSidecar(tmpDir(), "net.py", mod.get());
CHECK(result.success, "save failed");
CHECK(result.annotationCount == 1, "wrong count");
auto loaded = loadSemannoSidecar(tmpDir(), "net.py");
CHECK(loaded.count == 1, "loaded wrong count");
CHECK(loaded.entries[0].type == "capability", "wrong type");
CHECK(loaded.entries[0].properties["capability"] == "io.net", "wrong cap");
PASS();
}
// 10. Overwrite existing sidecar
void test_overwrite() {
TEST(overwrite);
cleanup();
auto mod1 = makeAnnotatedModule();
saveSemannoSidecar(tmpDir(), "test.py", mod1.get());
// Save again with different content
auto mod2 = std::make_unique<Module>();
mod2->name = "changed";
auto fn = std::make_unique<Function>();
fn->name = "new_func";
fn->spanStartLine = 1;
fn->addChild("annotations", new PureAnnotation());
mod2->addChild("functions", fn.release());
auto result = saveSemannoSidecar(tmpDir(), "test.py", mod2.get());
CHECK(result.success, "save failed");
// PureAnnotation is Subject 1 memory — check if isSemanticAnnotation includes it
// PureAnnotation is from Sprint 3 original set, check what isSemanticAnnotation says
// Either way the save should succeed
auto loaded = loadSemannoSidecar(tmpDir(), "test.py");
CHECK(loaded.success, "load failed");
PASS();
}
// 11. Properties preserved in roundtrip
void test_properties_preserved() {
TEST(properties_preserved);
cleanup();
auto mod = std::make_unique<Module>();
mod->name = "props";
auto fn = std::make_unique<Function>();
fn->name = "f";
fn->spanStartLine = 3;
auto contract = new ContractAnnotation();
contract->preconditions = "len(arr) > 0";
contract->postconditions = "result >= 0";
contract->returnShape = "int";
contract->sideEffects = "none";
fn->addChild("annotations", contract);
mod->addChild("functions", fn.release());
saveSemannoSidecar(tmpDir(), "props.py", mod.get());
auto loaded = loadSemannoSidecar(tmpDir(), "props.py");
CHECK(loaded.success, "load failed");
CHECK(loaded.count == 1, "wrong count");
auto& entry = loaded.entries[0];
CHECK(entry.type == "contract", "wrong type");
CHECK(entry.properties["preconditions"] == "len(arr) > 0", "preconditions lost");
CHECK(entry.properties["postconditions"] == "result >= 0", "postconditions lost");
PASS();
}
// 12. Empty module produces empty sidecar
void test_empty_module() {
TEST(empty_module);
cleanup();
auto mod = std::make_unique<Module>();
mod->name = "empty";
auto result = saveSemannoSidecar(tmpDir(), "empty.py", mod.get());
CHECK(result.success, "save failed");
CHECK(result.annotationCount == 0, "should be empty");
auto loaded = loadSemannoSidecar(tmpDir(), "empty.py");
CHECK(loaded.success, "load failed");
CHECK(loaded.count == 0, "should be empty");
PASS();
}
int main() {
std::cout << "=== Step 295: Semanno Sidecar Integration Tests ===\n";
test_sidecar_path();
test_save_creates_file();
test_save_content_format();
test_load_roundtrip();
test_load_nonexistent();
test_save_null_ast();
test_multiple_types();
test_alongside_ast_sidecar();
test_environment_annotations();
test_overwrite();
test_properties_preserved();
test_empty_module();
cleanup();
std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n";
return failed > 0 ? 1 : 0;
}