backend-infra-engineer: Post v0.3.9-hotfix7 snapshot (build cleanup)
This commit is contained in:
617
test/unit/tools/code_gen_tool_test.cc
Normal file
617
test/unit/tools/code_gen_tool_test.cc
Normal file
@@ -0,0 +1,617 @@
|
||||
/**
|
||||
* @file code_gen_tool_test.cc
|
||||
* @brief Unit tests for the CodeGenTool AI agent tools
|
||||
*
|
||||
* Tests the code generation functionality including ASM templates,
|
||||
* placeholder substitution, freespace detection, and hook validation.
|
||||
*/
|
||||
|
||||
#include "cli/service/agent/tools/code_gen_tool.h"
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace cli {
|
||||
namespace agent {
|
||||
namespace tools {
|
||||
namespace {
|
||||
|
||||
using ::testing::Contains;
|
||||
using ::testing::Eq;
|
||||
using ::testing::HasSubstr;
|
||||
using ::testing::IsEmpty;
|
||||
using ::testing::Not;
|
||||
using ::testing::SizeIs;
|
||||
|
||||
// =============================================================================
|
||||
// AsmTemplate Structure Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST(AsmTemplateTest, StructureHasExpectedFields) {
|
||||
AsmTemplate tmpl;
|
||||
tmpl.name = "test";
|
||||
tmpl.code_template = "NOP";
|
||||
tmpl.required_params = {"PARAM1", "PARAM2"};
|
||||
tmpl.description = "Test template";
|
||||
|
||||
EXPECT_EQ(tmpl.name, "test");
|
||||
EXPECT_EQ(tmpl.code_template, "NOP");
|
||||
EXPECT_THAT(tmpl.required_params, SizeIs(2));
|
||||
EXPECT_EQ(tmpl.description, "Test template");
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// FreeSpaceRegion Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST(FreeSpaceRegionTest, StructureHasExpectedFields) {
|
||||
FreeSpaceRegion region;
|
||||
region.start = 0x1F8000;
|
||||
region.end = 0x1FFFFF;
|
||||
region.description = "Bank $3F freespace";
|
||||
region.free_percent = 95;
|
||||
|
||||
EXPECT_EQ(region.start, 0x1F8000u);
|
||||
EXPECT_EQ(region.end, 0x1FFFFFu);
|
||||
EXPECT_EQ(region.description, "Bank $3F freespace");
|
||||
EXPECT_EQ(region.free_percent, 95);
|
||||
}
|
||||
|
||||
TEST(FreeSpaceRegionTest, SizeCalculation) {
|
||||
FreeSpaceRegion region;
|
||||
region.start = 0x1F8000;
|
||||
region.end = 0x1FFFFF;
|
||||
|
||||
// 0x1FFFFF - 0x1F8000 + 1 = 0x8000 = 32768 bytes
|
||||
EXPECT_EQ(region.Size(), 0x8000u);
|
||||
}
|
||||
|
||||
TEST(FreeSpaceRegionTest, SizeCalculationSingleByte) {
|
||||
FreeSpaceRegion region;
|
||||
region.start = 0x100;
|
||||
region.end = 0x100;
|
||||
|
||||
EXPECT_EQ(region.Size(), 1u);
|
||||
}
|
||||
|
||||
TEST(FreeSpaceRegionTest, SizeCalculationLargeRegion) {
|
||||
FreeSpaceRegion region;
|
||||
region.start = 0x000000;
|
||||
region.end = 0x3FFFFF;
|
||||
|
||||
// 4MB region
|
||||
EXPECT_EQ(region.Size(), 0x400000u);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CodeGenerationDiagnostic Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST(CodeGenerationDiagnosticTest, SeverityStringInfo) {
|
||||
CodeGenerationDiagnostic diag;
|
||||
diag.severity = CodeGenerationDiagnostic::Severity::kInfo;
|
||||
EXPECT_EQ(diag.SeverityString(), "info");
|
||||
}
|
||||
|
||||
TEST(CodeGenerationDiagnosticTest, SeverityStringWarning) {
|
||||
CodeGenerationDiagnostic diag;
|
||||
diag.severity = CodeGenerationDiagnostic::Severity::kWarning;
|
||||
EXPECT_EQ(diag.SeverityString(), "warning");
|
||||
}
|
||||
|
||||
TEST(CodeGenerationDiagnosticTest, SeverityStringError) {
|
||||
CodeGenerationDiagnostic diag;
|
||||
diag.severity = CodeGenerationDiagnostic::Severity::kError;
|
||||
EXPECT_EQ(diag.SeverityString(), "error");
|
||||
}
|
||||
|
||||
TEST(CodeGenerationDiagnosticTest, StructureHasExpectedFields) {
|
||||
CodeGenerationDiagnostic diag;
|
||||
diag.severity = CodeGenerationDiagnostic::Severity::kWarning;
|
||||
diag.message = "Test warning";
|
||||
diag.address = 0x008000;
|
||||
|
||||
EXPECT_EQ(diag.message, "Test warning");
|
||||
EXPECT_EQ(diag.address, 0x008000u);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CodeGenerationResult Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST(CodeGenerationResultTest, DefaultConstruction) {
|
||||
CodeGenerationResult result;
|
||||
// success is uninitialized by default - not testing its value
|
||||
EXPECT_TRUE(result.generated_code.empty());
|
||||
EXPECT_TRUE(result.diagnostics.empty());
|
||||
EXPECT_TRUE(result.symbols.empty());
|
||||
}
|
||||
|
||||
TEST(CodeGenerationResultTest, AddInfoAddsDiagnostic) {
|
||||
CodeGenerationResult result;
|
||||
result.success = true;
|
||||
|
||||
result.AddInfo("Test info", 0x008000);
|
||||
|
||||
EXPECT_THAT(result.diagnostics, SizeIs(1));
|
||||
EXPECT_EQ(result.diagnostics[0].severity,
|
||||
CodeGenerationDiagnostic::Severity::kInfo);
|
||||
EXPECT_EQ(result.diagnostics[0].message, "Test info");
|
||||
EXPECT_EQ(result.diagnostics[0].address, 0x008000u);
|
||||
EXPECT_TRUE(result.success); // Info doesn't change success
|
||||
}
|
||||
|
||||
TEST(CodeGenerationResultTest, AddWarningAddsDiagnostic) {
|
||||
CodeGenerationResult result;
|
||||
result.success = true;
|
||||
|
||||
result.AddWarning("Test warning", 0x00A000);
|
||||
|
||||
EXPECT_THAT(result.diagnostics, SizeIs(1));
|
||||
EXPECT_EQ(result.diagnostics[0].severity,
|
||||
CodeGenerationDiagnostic::Severity::kWarning);
|
||||
EXPECT_EQ(result.diagnostics[0].message, "Test warning");
|
||||
EXPECT_TRUE(result.success); // Warning doesn't change success
|
||||
}
|
||||
|
||||
TEST(CodeGenerationResultTest, AddErrorSetsFailure) {
|
||||
CodeGenerationResult result;
|
||||
result.success = true;
|
||||
|
||||
result.AddError("Test error", 0x00C000);
|
||||
|
||||
EXPECT_THAT(result.diagnostics, SizeIs(1));
|
||||
EXPECT_EQ(result.diagnostics[0].severity,
|
||||
CodeGenerationDiagnostic::Severity::kError);
|
||||
EXPECT_EQ(result.diagnostics[0].message, "Test error");
|
||||
EXPECT_FALSE(result.success); // Error sets success to false
|
||||
}
|
||||
|
||||
TEST(CodeGenerationResultTest, MultipleDiagnostics) {
|
||||
CodeGenerationResult result;
|
||||
result.success = true;
|
||||
|
||||
result.AddInfo("Info 1");
|
||||
result.AddWarning("Warning 1");
|
||||
result.AddError("Error 1");
|
||||
result.AddInfo("Info 2");
|
||||
|
||||
EXPECT_THAT(result.diagnostics, SizeIs(4));
|
||||
EXPECT_FALSE(result.success); // Because we added an error
|
||||
}
|
||||
|
||||
TEST(CodeGenerationResultTest, SymbolsMap) {
|
||||
CodeGenerationResult result;
|
||||
result.symbols["MyLabel"] = 0x1F8000;
|
||||
result.symbols["OtherLabel"] = 0x00A000;
|
||||
|
||||
EXPECT_EQ(result.symbols["MyLabel"], 0x1F8000u);
|
||||
EXPECT_EQ(result.symbols["OtherLabel"], 0x00A000u);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Tool Name Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST(CodeGenToolsTest, AsmHookToolName) {
|
||||
CodeGenAsmHookTool tool;
|
||||
EXPECT_EQ(tool.GetName(), "codegen-asm-hook");
|
||||
}
|
||||
|
||||
TEST(CodeGenToolsTest, FreespacePatchToolName) {
|
||||
CodeGenFreespacePatchTool tool;
|
||||
EXPECT_EQ(tool.GetName(), "codegen-freespace-patch");
|
||||
}
|
||||
|
||||
TEST(CodeGenToolsTest, SpriteTemplateToolName) {
|
||||
CodeGenSpriteTemplateTool tool;
|
||||
EXPECT_EQ(tool.GetName(), "codegen-sprite-template");
|
||||
}
|
||||
|
||||
TEST(CodeGenToolsTest, EventHandlerToolName) {
|
||||
CodeGenEventHandlerTool tool;
|
||||
EXPECT_EQ(tool.GetName(), "codegen-event-handler");
|
||||
}
|
||||
|
||||
TEST(CodeGenToolsTest, AllToolNamesStartWithCodegen) {
|
||||
CodeGenAsmHookTool hook;
|
||||
CodeGenFreespacePatchTool freespace;
|
||||
CodeGenSpriteTemplateTool sprite;
|
||||
CodeGenEventHandlerTool event;
|
||||
|
||||
EXPECT_THAT(hook.GetName(), HasSubstr("codegen-"));
|
||||
EXPECT_THAT(freespace.GetName(), HasSubstr("codegen-"));
|
||||
EXPECT_THAT(sprite.GetName(), HasSubstr("codegen-"));
|
||||
EXPECT_THAT(event.GetName(), HasSubstr("codegen-"));
|
||||
}
|
||||
|
||||
TEST(CodeGenToolsTest, AllToolNamesAreUnique) {
|
||||
CodeGenAsmHookTool hook;
|
||||
CodeGenFreespacePatchTool freespace;
|
||||
CodeGenSpriteTemplateTool sprite;
|
||||
CodeGenEventHandlerTool event;
|
||||
|
||||
std::vector<std::string> names = {hook.GetName(), freespace.GetName(),
|
||||
sprite.GetName(), event.GetName()};
|
||||
|
||||
std::set<std::string> unique_names(names.begin(), names.end());
|
||||
EXPECT_EQ(unique_names.size(), names.size())
|
||||
<< "All code gen tool names should be unique";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Tool Usage String Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST(CodeGenToolsTest, AsmHookToolUsageFormat) {
|
||||
CodeGenAsmHookTool tool;
|
||||
EXPECT_THAT(tool.GetUsage(), HasSubstr("--address"));
|
||||
EXPECT_THAT(tool.GetUsage(), HasSubstr("--label"));
|
||||
EXPECT_THAT(tool.GetUsage(), HasSubstr("--nop-fill"));
|
||||
}
|
||||
|
||||
TEST(CodeGenToolsTest, FreespacePatchToolUsageFormat) {
|
||||
CodeGenFreespacePatchTool tool;
|
||||
EXPECT_THAT(tool.GetUsage(), HasSubstr("--label"));
|
||||
EXPECT_THAT(tool.GetUsage(), HasSubstr("--size"));
|
||||
EXPECT_THAT(tool.GetUsage(), HasSubstr("--prefer-bank"));
|
||||
}
|
||||
|
||||
TEST(CodeGenToolsTest, SpriteTemplateToolUsageFormat) {
|
||||
CodeGenSpriteTemplateTool tool;
|
||||
EXPECT_THAT(tool.GetUsage(), HasSubstr("--name"));
|
||||
EXPECT_THAT(tool.GetUsage(), HasSubstr("--init-code"));
|
||||
EXPECT_THAT(tool.GetUsage(), HasSubstr("--main-code"));
|
||||
}
|
||||
|
||||
TEST(CodeGenToolsTest, EventHandlerToolUsageFormat) {
|
||||
CodeGenEventHandlerTool tool;
|
||||
EXPECT_THAT(tool.GetUsage(), HasSubstr("--type"));
|
||||
EXPECT_THAT(tool.GetUsage(), HasSubstr("--label"));
|
||||
EXPECT_THAT(tool.GetUsage(), HasSubstr("--custom-code"));
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CodeGenToolBase Method Tests (via concrete class)
|
||||
// =============================================================================
|
||||
|
||||
// Test class to expose protected methods for testing
|
||||
class TestableCodeGenTool : public CodeGenToolBase {
|
||||
public:
|
||||
using CodeGenToolBase::DetectFreeSpace;
|
||||
using CodeGenToolBase::FormatResultAsJson;
|
||||
using CodeGenToolBase::FormatResultAsText;
|
||||
using CodeGenToolBase::GetAllTemplates;
|
||||
using CodeGenToolBase::GetHookLocationDescription;
|
||||
using CodeGenToolBase::GetTemplate;
|
||||
using CodeGenToolBase::IsKnownHookLocation;
|
||||
using CodeGenToolBase::SubstitutePlaceholders;
|
||||
using CodeGenToolBase::ValidateHookAddress;
|
||||
|
||||
std::string GetName() const override { return "testable-codegen"; }
|
||||
std::string GetUsage() const override { return "test"; }
|
||||
|
||||
protected:
|
||||
absl::Status ValidateArgs(
|
||||
const resources::ArgumentParser& /*parser*/) override {
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status Execute(Rom* /*rom*/,
|
||||
const resources::ArgumentParser& /*parser*/,
|
||||
resources::OutputFormatter& /*formatter*/) override {
|
||||
return absl::OkStatus();
|
||||
}
|
||||
};
|
||||
|
||||
TEST(CodeGenToolBaseTest, SubstitutePlaceholdersSimple) {
|
||||
TestableCodeGenTool tool;
|
||||
|
||||
std::string tmpl = "Hello {{NAME}}!";
|
||||
std::map<std::string, std::string> params = {{"NAME", "World"}};
|
||||
|
||||
std::string result = tool.SubstitutePlaceholders(tmpl, params);
|
||||
EXPECT_EQ(result, "Hello World!");
|
||||
}
|
||||
|
||||
TEST(CodeGenToolBaseTest, SubstitutePlaceholdersMultiple) {
|
||||
TestableCodeGenTool tool;
|
||||
|
||||
std::string tmpl = "org ${{ADDRESS}}\n{{LABEL}}:\n JSR {{SUBROUTINE}}\n RTL";
|
||||
std::map<std::string, std::string> params = {
|
||||
{"ADDRESS", "1F8000"}, {"LABEL", "MyCode"}, {"SUBROUTINE", "DoStuff"}};
|
||||
|
||||
std::string result = tool.SubstitutePlaceholders(tmpl, params);
|
||||
|
||||
EXPECT_THAT(result, HasSubstr("org $1F8000"));
|
||||
EXPECT_THAT(result, HasSubstr("MyCode:"));
|
||||
EXPECT_THAT(result, HasSubstr("JSR DoStuff"));
|
||||
}
|
||||
|
||||
TEST(CodeGenToolBaseTest, SubstitutePlaceholdersNoMatch) {
|
||||
TestableCodeGenTool tool;
|
||||
|
||||
std::string tmpl = "No placeholders here";
|
||||
std::map<std::string, std::string> params = {{"UNUSED", "value"}};
|
||||
|
||||
std::string result = tool.SubstitutePlaceholders(tmpl, params);
|
||||
EXPECT_EQ(result, "No placeholders here");
|
||||
}
|
||||
|
||||
TEST(CodeGenToolBaseTest, SubstitutePlaceholdersMissingParam) {
|
||||
TestableCodeGenTool tool;
|
||||
|
||||
std::string tmpl = "Hello {{NAME}} and {{OTHER}}!";
|
||||
std::map<std::string, std::string> params = {{"NAME", "World"}};
|
||||
|
||||
std::string result = tool.SubstitutePlaceholders(tmpl, params);
|
||||
// Missing param should remain as placeholder
|
||||
EXPECT_THAT(result, HasSubstr("World"));
|
||||
EXPECT_THAT(result, HasSubstr("{{OTHER}}"));
|
||||
}
|
||||
|
||||
TEST(CodeGenToolBaseTest, GetAllTemplatesNotEmpty) {
|
||||
TestableCodeGenTool tool;
|
||||
const auto& templates = tool.GetAllTemplates();
|
||||
|
||||
EXPECT_FALSE(templates.empty());
|
||||
}
|
||||
|
||||
TEST(CodeGenToolBaseTest, GetAllTemplatesContainsExpectedTemplates) {
|
||||
TestableCodeGenTool tool;
|
||||
const auto& templates = tool.GetAllTemplates();
|
||||
|
||||
std::vector<std::string> names;
|
||||
for (const auto& tmpl : templates) {
|
||||
names.push_back(tmpl.name);
|
||||
}
|
||||
|
||||
EXPECT_THAT(names, Contains("nmi_hook"));
|
||||
EXPECT_THAT(names, Contains("sprite"));
|
||||
EXPECT_THAT(names, Contains("freespace_alloc"));
|
||||
EXPECT_THAT(names, Contains("jsl_hook"));
|
||||
EXPECT_THAT(names, Contains("event_handler"));
|
||||
}
|
||||
|
||||
TEST(CodeGenToolBaseTest, GetTemplateFound) {
|
||||
TestableCodeGenTool tool;
|
||||
|
||||
auto result = tool.GetTemplate("sprite");
|
||||
ASSERT_TRUE(result.ok()) << result.status().message();
|
||||
|
||||
EXPECT_EQ(result->name, "sprite");
|
||||
EXPECT_FALSE(result->code_template.empty());
|
||||
EXPECT_FALSE(result->required_params.empty());
|
||||
}
|
||||
|
||||
TEST(CodeGenToolBaseTest, GetTemplateNotFound) {
|
||||
TestableCodeGenTool tool;
|
||||
|
||||
auto result = tool.GetTemplate("nonexistent");
|
||||
EXPECT_FALSE(result.ok());
|
||||
EXPECT_EQ(result.status().code(), absl::StatusCode::kNotFound);
|
||||
}
|
||||
|
||||
TEST(CodeGenToolBaseTest, IsKnownHookLocationTrue) {
|
||||
TestableCodeGenTool tool;
|
||||
|
||||
// Known hook: EnableForceBlank at 0x00893D
|
||||
EXPECT_TRUE(tool.IsKnownHookLocation(0x00893D));
|
||||
}
|
||||
|
||||
TEST(CodeGenToolBaseTest, IsKnownHookLocationFalse) {
|
||||
TestableCodeGenTool tool;
|
||||
|
||||
EXPECT_FALSE(tool.IsKnownHookLocation(0x000000));
|
||||
EXPECT_FALSE(tool.IsKnownHookLocation(0xFFFFFF));
|
||||
}
|
||||
|
||||
TEST(CodeGenToolBaseTest, GetHookLocationDescriptionKnown) {
|
||||
TestableCodeGenTool tool;
|
||||
|
||||
std::string desc = tool.GetHookLocationDescription(0x00893D);
|
||||
EXPECT_EQ(desc, "EnableForceBlank");
|
||||
}
|
||||
|
||||
TEST(CodeGenToolBaseTest, GetHookLocationDescriptionUnknown) {
|
||||
TestableCodeGenTool tool;
|
||||
|
||||
std::string desc = tool.GetHookLocationDescription(0x000000);
|
||||
EXPECT_EQ(desc, "Unknown");
|
||||
}
|
||||
|
||||
TEST(CodeGenToolBaseTest, FormatResultAsJsonContainsSuccess) {
|
||||
TestableCodeGenTool tool;
|
||||
CodeGenerationResult result;
|
||||
result.success = true;
|
||||
result.generated_code = "NOP";
|
||||
|
||||
std::string json = tool.FormatResultAsJson(result);
|
||||
EXPECT_THAT(json, HasSubstr("\"success\": true"));
|
||||
}
|
||||
|
||||
TEST(CodeGenToolBaseTest, FormatResultAsJsonContainsCode) {
|
||||
TestableCodeGenTool tool;
|
||||
CodeGenerationResult result;
|
||||
result.success = true;
|
||||
result.generated_code = "LDA #$00";
|
||||
|
||||
std::string json = tool.FormatResultAsJson(result);
|
||||
EXPECT_THAT(json, HasSubstr("\"code\":"));
|
||||
EXPECT_THAT(json, HasSubstr("LDA"));
|
||||
}
|
||||
|
||||
TEST(CodeGenToolBaseTest, FormatResultAsJsonContainsDiagnostics) {
|
||||
TestableCodeGenTool tool;
|
||||
CodeGenerationResult result;
|
||||
result.success = true;
|
||||
result.AddInfo("Test info");
|
||||
|
||||
std::string json = tool.FormatResultAsJson(result);
|
||||
EXPECT_THAT(json, HasSubstr("\"diagnostics\":"));
|
||||
EXPECT_THAT(json, HasSubstr("Test info"));
|
||||
}
|
||||
|
||||
TEST(CodeGenToolBaseTest, FormatResultAsTextContainsStatus) {
|
||||
TestableCodeGenTool tool;
|
||||
CodeGenerationResult result;
|
||||
result.success = true;
|
||||
|
||||
std::string text = tool.FormatResultAsText(result);
|
||||
EXPECT_THAT(text, HasSubstr("SUCCESS"));
|
||||
}
|
||||
|
||||
TEST(CodeGenToolBaseTest, FormatResultAsTextContainsFailedStatus) {
|
||||
TestableCodeGenTool tool;
|
||||
CodeGenerationResult result;
|
||||
result.success = false;
|
||||
result.AddError("Something failed");
|
||||
|
||||
std::string text = tool.FormatResultAsText(result);
|
||||
EXPECT_THAT(text, HasSubstr("FAILED"));
|
||||
}
|
||||
|
||||
TEST(CodeGenToolBaseTest, FormatResultAsTextContainsGeneratedCode) {
|
||||
TestableCodeGenTool tool;
|
||||
CodeGenerationResult result;
|
||||
result.success = true;
|
||||
result.generated_code = "JSL MyRoutine\nRTL";
|
||||
|
||||
std::string text = tool.FormatResultAsText(result);
|
||||
EXPECT_THAT(text, HasSubstr("Generated Code:"));
|
||||
EXPECT_THAT(text, HasSubstr("JSL MyRoutine"));
|
||||
}
|
||||
|
||||
TEST(CodeGenToolBaseTest, FormatResultAsTextContainsSymbols) {
|
||||
TestableCodeGenTool tool;
|
||||
CodeGenerationResult result;
|
||||
result.success = true;
|
||||
result.symbols["MyLabel"] = 0x1F8000;
|
||||
|
||||
std::string text = tool.FormatResultAsText(result);
|
||||
EXPECT_THAT(text, HasSubstr("Symbols:"));
|
||||
EXPECT_THAT(text, HasSubstr("MyLabel"));
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Template Content Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST(AsmTemplateTest, NmiHookTemplateHasRequiredParams) {
|
||||
TestableCodeGenTool tool;
|
||||
auto result = tool.GetTemplate("nmi_hook");
|
||||
ASSERT_TRUE(result.ok());
|
||||
|
||||
EXPECT_THAT(result->required_params, Contains("LABEL"));
|
||||
EXPECT_THAT(result->required_params, Contains("NMI_HOOK_ADDRESS"));
|
||||
EXPECT_THAT(result->required_params, Contains("CUSTOM_CODE"));
|
||||
}
|
||||
|
||||
TEST(AsmTemplateTest, SpriteTemplateHasRequiredParams) {
|
||||
TestableCodeGenTool tool;
|
||||
auto result = tool.GetTemplate("sprite");
|
||||
ASSERT_TRUE(result.ok());
|
||||
|
||||
EXPECT_THAT(result->required_params, Contains("SPRITE_NAME"));
|
||||
EXPECT_THAT(result->required_params, Contains("INIT_CODE"));
|
||||
EXPECT_THAT(result->required_params, Contains("MAIN_CODE"));
|
||||
}
|
||||
|
||||
TEST(AsmTemplateTest, JslHookTemplateHasRequiredParams) {
|
||||
TestableCodeGenTool tool;
|
||||
auto result = tool.GetTemplate("jsl_hook");
|
||||
ASSERT_TRUE(result.ok());
|
||||
|
||||
EXPECT_THAT(result->required_params, Contains("HOOK_ADDRESS"));
|
||||
EXPECT_THAT(result->required_params, Contains("LABEL"));
|
||||
}
|
||||
|
||||
TEST(AsmTemplateTest, EventHandlerTemplateHasRequiredParams) {
|
||||
TestableCodeGenTool tool;
|
||||
auto result = tool.GetTemplate("event_handler");
|
||||
ASSERT_TRUE(result.ok());
|
||||
|
||||
EXPECT_THAT(result->required_params, Contains("EVENT_TYPE"));
|
||||
EXPECT_THAT(result->required_params, Contains("HOOK_ADDRESS"));
|
||||
EXPECT_THAT(result->required_params, Contains("LABEL"));
|
||||
EXPECT_THAT(result->required_params, Contains("CUSTOM_CODE"));
|
||||
}
|
||||
|
||||
TEST(AsmTemplateTest, AllTemplatesHaveDescriptions) {
|
||||
TestableCodeGenTool tool;
|
||||
const auto& templates = tool.GetAllTemplates();
|
||||
|
||||
for (const auto& tmpl : templates) {
|
||||
EXPECT_FALSE(tmpl.description.empty())
|
||||
<< "Template '" << tmpl.name << "' has no description";
|
||||
}
|
||||
}
|
||||
|
||||
TEST(AsmTemplateTest, AllTemplatesHaveCode) {
|
||||
TestableCodeGenTool tool;
|
||||
const auto& templates = tool.GetAllTemplates();
|
||||
|
||||
for (const auto& tmpl : templates) {
|
||||
EXPECT_FALSE(tmpl.code_template.empty())
|
||||
<< "Template '" << tmpl.name << "' has no code template";
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Template Substitution Integration Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST(AsmTemplateTest, SpriteTemplateSubstitution) {
|
||||
TestableCodeGenTool tool;
|
||||
auto tmpl_result = tool.GetTemplate("sprite");
|
||||
ASSERT_TRUE(tmpl_result.ok());
|
||||
|
||||
std::map<std::string, std::string> params = {
|
||||
{"SPRITE_NAME", "MyCustomSprite"},
|
||||
{"INIT_CODE", "LDA #$00 : STA $0F50, X"},
|
||||
{"MAIN_CODE", "JSR Sprite_Move"},
|
||||
};
|
||||
|
||||
std::string code =
|
||||
tool.SubstitutePlaceholders(tmpl_result->code_template, params);
|
||||
|
||||
EXPECT_THAT(code, HasSubstr("MyCustomSprite:"));
|
||||
EXPECT_THAT(code, HasSubstr("LDA #$00 : STA $0F50, X"));
|
||||
EXPECT_THAT(code, HasSubstr("JSR Sprite_Move"));
|
||||
// Should not have unsubstituted placeholders
|
||||
EXPECT_THAT(code, Not(HasSubstr("{{SPRITE_NAME}}")));
|
||||
EXPECT_THAT(code, Not(HasSubstr("{{INIT_CODE}}")));
|
||||
EXPECT_THAT(code, Not(HasSubstr("{{MAIN_CODE}}")));
|
||||
}
|
||||
|
||||
TEST(AsmTemplateTest, JslHookTemplateSubstitution) {
|
||||
TestableCodeGenTool tool;
|
||||
auto tmpl_result = tool.GetTemplate("jsl_hook");
|
||||
ASSERT_TRUE(tmpl_result.ok());
|
||||
|
||||
std::map<std::string, std::string> params = {
|
||||
{"HOOK_ADDRESS", "008040"},
|
||||
{"LABEL", "MyHook"},
|
||||
{"NOP_FILL", "NOP\n NOP"},
|
||||
};
|
||||
|
||||
std::string code =
|
||||
tool.SubstitutePlaceholders(tmpl_result->code_template, params);
|
||||
|
||||
EXPECT_THAT(code, HasSubstr("org $008040"));
|
||||
EXPECT_THAT(code, HasSubstr("JSL MyHook"));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace tools
|
||||
} // namespace agent
|
||||
} // namespace cli
|
||||
} // namespace yaze
|
||||
362
test/unit/tools/memory_inspector_tool_test.cc
Normal file
362
test/unit/tools/memory_inspector_tool_test.cc
Normal file
@@ -0,0 +1,362 @@
|
||||
/**
|
||||
* @file memory_inspector_tool_test.cc
|
||||
* @brief Unit tests for the MemoryInspectorTool AI agent tools
|
||||
*
|
||||
* Tests the memory inspection functionality including analyzing,
|
||||
* searching, comparing, checking, and region listing tools.
|
||||
*/
|
||||
|
||||
#include "cli/service/agent/tools/memory_inspector_tool.h"
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace cli {
|
||||
namespace agent {
|
||||
namespace tools {
|
||||
namespace {
|
||||
|
||||
using ::testing::Contains;
|
||||
using ::testing::Ge;
|
||||
using ::testing::HasSubstr;
|
||||
using ::testing::Le;
|
||||
using ::testing::Not;
|
||||
using ::testing::SizeIs;
|
||||
|
||||
// =============================================================================
|
||||
// ALTTPMemoryMap Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST(ALTTPMemoryMapTest, WRAMBoundsAreCorrect) {
|
||||
EXPECT_EQ(ALTTPMemoryMap::kWRAMStart, 0x7E0000u);
|
||||
EXPECT_EQ(ALTTPMemoryMap::kWRAMEnd, 0x7FFFFFu);
|
||||
}
|
||||
|
||||
TEST(ALTTPMemoryMapTest, IsWRAMDetectsValidAddresses) {
|
||||
// Valid WRAM addresses
|
||||
EXPECT_TRUE(ALTTPMemoryMap::IsWRAM(0x7E0000));
|
||||
EXPECT_TRUE(ALTTPMemoryMap::IsWRAM(0x7E8000));
|
||||
EXPECT_TRUE(ALTTPMemoryMap::IsWRAM(0x7FFFFF));
|
||||
|
||||
// Invalid addresses (outside WRAM)
|
||||
EXPECT_FALSE(ALTTPMemoryMap::IsWRAM(0x000000));
|
||||
EXPECT_FALSE(ALTTPMemoryMap::IsWRAM(0x7DFFFF));
|
||||
EXPECT_FALSE(ALTTPMemoryMap::IsWRAM(0x800000));
|
||||
}
|
||||
|
||||
TEST(ALTTPMemoryMapTest, IsSpriteTableDetectsValidAddresses) {
|
||||
// Valid sprite table addresses
|
||||
EXPECT_TRUE(ALTTPMemoryMap::IsSpriteTable(0x7E0D00));
|
||||
EXPECT_TRUE(ALTTPMemoryMap::IsSpriteTable(0x7E0D50));
|
||||
EXPECT_TRUE(ALTTPMemoryMap::IsSpriteTable(0x7E0FFF));
|
||||
|
||||
// Invalid addresses (outside sprite table)
|
||||
EXPECT_FALSE(ALTTPMemoryMap::IsSpriteTable(0x7E0CFF));
|
||||
EXPECT_FALSE(ALTTPMemoryMap::IsSpriteTable(0x7E1000));
|
||||
}
|
||||
|
||||
TEST(ALTTPMemoryMapTest, IsSaveDataDetectsValidAddresses) {
|
||||
// Valid save data addresses
|
||||
EXPECT_TRUE(ALTTPMemoryMap::IsSaveData(0x7EF000));
|
||||
EXPECT_TRUE(ALTTPMemoryMap::IsSaveData(0x7EF360)); // Rupees
|
||||
EXPECT_TRUE(ALTTPMemoryMap::IsSaveData(0x7EF4FF));
|
||||
|
||||
// Invalid addresses (outside save data)
|
||||
EXPECT_FALSE(ALTTPMemoryMap::IsSaveData(0x7EEFFF));
|
||||
EXPECT_FALSE(ALTTPMemoryMap::IsSaveData(0x7EF500));
|
||||
}
|
||||
|
||||
TEST(ALTTPMemoryMapTest, KnownAddressesAreInWRAM) {
|
||||
// All known addresses should be in WRAM range
|
||||
EXPECT_TRUE(ALTTPMemoryMap::IsWRAM(ALTTPMemoryMap::kGameMode));
|
||||
EXPECT_TRUE(ALTTPMemoryMap::IsWRAM(ALTTPMemoryMap::kSubmodule));
|
||||
EXPECT_TRUE(ALTTPMemoryMap::IsWRAM(ALTTPMemoryMap::kLinkXLow));
|
||||
EXPECT_TRUE(ALTTPMemoryMap::IsWRAM(ALTTPMemoryMap::kLinkYLow));
|
||||
EXPECT_TRUE(ALTTPMemoryMap::IsWRAM(ALTTPMemoryMap::kPlayerHealth));
|
||||
EXPECT_TRUE(ALTTPMemoryMap::IsWRAM(ALTTPMemoryMap::kSpriteType));
|
||||
}
|
||||
|
||||
TEST(ALTTPMemoryMapTest, MaxSpritesIsCorrect) {
|
||||
EXPECT_EQ(ALTTPMemoryMap::kMaxSprites, 16);
|
||||
}
|
||||
|
||||
TEST(ALTTPMemoryMapTest, PlayerAddressesAreConsistent) {
|
||||
// X and Y coordinates should be adjacent
|
||||
EXPECT_EQ(ALTTPMemoryMap::kLinkXHigh - ALTTPMemoryMap::kLinkXLow, 1u);
|
||||
EXPECT_EQ(ALTTPMemoryMap::kLinkYHigh - ALTTPMemoryMap::kLinkYLow, 1u);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MemoryRegionInfo Structure Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST(MemoryRegionInfoTest, StructureHasExpectedFields) {
|
||||
MemoryRegionInfo info;
|
||||
info.name = "Test Region";
|
||||
info.description = "Test description";
|
||||
info.start_address = 0x7E0000;
|
||||
info.end_address = 0x7EFFFF;
|
||||
info.data_type = "byte";
|
||||
|
||||
EXPECT_EQ(info.name, "Test Region");
|
||||
EXPECT_EQ(info.description, "Test description");
|
||||
EXPECT_EQ(info.start_address, 0x7E0000u);
|
||||
EXPECT_EQ(info.end_address, 0x7EFFFFu);
|
||||
EXPECT_EQ(info.data_type, "byte");
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MemoryAnomaly Structure Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST(MemoryAnomalyTest, StructureHasExpectedFields) {
|
||||
MemoryAnomaly anomaly;
|
||||
anomaly.address = 0x7E0D00;
|
||||
anomaly.type = "out_of_bounds";
|
||||
anomaly.description = "Sprite X position out of bounds";
|
||||
anomaly.severity = 3;
|
||||
|
||||
EXPECT_EQ(anomaly.address, 0x7E0D00u);
|
||||
EXPECT_EQ(anomaly.type, "out_of_bounds");
|
||||
EXPECT_THAT(anomaly.description, HasSubstr("Sprite"));
|
||||
EXPECT_THAT(anomaly.severity, Ge(1));
|
||||
EXPECT_THAT(anomaly.severity, Le(5));
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PatternMatch Structure Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST(PatternMatchTest, StructureHasExpectedFields) {
|
||||
PatternMatch match;
|
||||
match.address = 0x7E0D00;
|
||||
match.matched_bytes = {0x12, 0x34, 0x56};
|
||||
match.context = "Sprite Table";
|
||||
|
||||
EXPECT_EQ(match.address, 0x7E0D00u);
|
||||
EXPECT_THAT(match.matched_bytes, SizeIs(3));
|
||||
EXPECT_EQ(match.context, "Sprite Table");
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MemoryAnalyzeTool Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST(MemoryAnalyzeToolTest, GetNameReturnsCorrectName) {
|
||||
MemoryAnalyzeTool tool;
|
||||
EXPECT_EQ(tool.GetName(), "memory-analyze");
|
||||
}
|
||||
|
||||
TEST(MemoryAnalyzeToolTest, GetUsageContainsAddress) {
|
||||
MemoryAnalyzeTool tool;
|
||||
EXPECT_THAT(tool.GetUsage(), HasSubstr("--address"));
|
||||
}
|
||||
|
||||
TEST(MemoryAnalyzeToolTest, GetUsageContainsLength) {
|
||||
MemoryAnalyzeTool tool;
|
||||
EXPECT_THAT(tool.GetUsage(), HasSubstr("--length"));
|
||||
}
|
||||
|
||||
TEST(MemoryAnalyzeToolTest, GetDescriptionIsNotEmpty) {
|
||||
MemoryAnalyzeTool tool;
|
||||
EXPECT_THAT(tool.GetDescription(), Not(HasSubstr("")));
|
||||
}
|
||||
|
||||
TEST(MemoryAnalyzeToolTest, DoesNotRequireLabels) {
|
||||
MemoryAnalyzeTool tool;
|
||||
EXPECT_FALSE(tool.RequiresLabels());
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MemorySearchTool Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST(MemorySearchToolTest, GetNameReturnsCorrectName) {
|
||||
MemorySearchTool tool;
|
||||
EXPECT_EQ(tool.GetName(), "memory-search");
|
||||
}
|
||||
|
||||
TEST(MemorySearchToolTest, GetUsageContainsPattern) {
|
||||
MemorySearchTool tool;
|
||||
EXPECT_THAT(tool.GetUsage(), HasSubstr("--pattern"));
|
||||
}
|
||||
|
||||
TEST(MemorySearchToolTest, GetUsageContainsStartEnd) {
|
||||
MemorySearchTool tool;
|
||||
std::string usage = tool.GetUsage();
|
||||
EXPECT_THAT(usage, HasSubstr("--start"));
|
||||
EXPECT_THAT(usage, HasSubstr("--end"));
|
||||
}
|
||||
|
||||
TEST(MemorySearchToolTest, GetDescriptionIsNotEmpty) {
|
||||
MemorySearchTool tool;
|
||||
EXPECT_THAT(tool.GetDescription(), Not(HasSubstr("")));
|
||||
}
|
||||
|
||||
TEST(MemorySearchToolTest, DoesNotRequireLabels) {
|
||||
MemorySearchTool tool;
|
||||
EXPECT_FALSE(tool.RequiresLabels());
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MemoryCompareTool Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST(MemoryCompareToolTest, GetNameReturnsCorrectName) {
|
||||
MemoryCompareTool tool;
|
||||
EXPECT_EQ(tool.GetName(), "memory-compare");
|
||||
}
|
||||
|
||||
TEST(MemoryCompareToolTest, GetUsageContainsAddress) {
|
||||
MemoryCompareTool tool;
|
||||
EXPECT_THAT(tool.GetUsage(), HasSubstr("--address"));
|
||||
}
|
||||
|
||||
TEST(MemoryCompareToolTest, GetUsageContainsExpected) {
|
||||
MemoryCompareTool tool;
|
||||
EXPECT_THAT(tool.GetUsage(), HasSubstr("--expected"));
|
||||
}
|
||||
|
||||
TEST(MemoryCompareToolTest, GetDescriptionIsNotEmpty) {
|
||||
MemoryCompareTool tool;
|
||||
EXPECT_THAT(tool.GetDescription(), Not(HasSubstr("")));
|
||||
}
|
||||
|
||||
TEST(MemoryCompareToolTest, DoesNotRequireLabels) {
|
||||
MemoryCompareTool tool;
|
||||
EXPECT_FALSE(tool.RequiresLabels());
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MemoryCheckTool Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST(MemoryCheckToolTest, GetNameReturnsCorrectName) {
|
||||
MemoryCheckTool tool;
|
||||
EXPECT_EQ(tool.GetName(), "memory-check");
|
||||
}
|
||||
|
||||
TEST(MemoryCheckToolTest, GetUsageContainsRegion) {
|
||||
MemoryCheckTool tool;
|
||||
EXPECT_THAT(tool.GetUsage(), HasSubstr("--region"));
|
||||
}
|
||||
|
||||
TEST(MemoryCheckToolTest, GetDescriptionIsNotEmpty) {
|
||||
MemoryCheckTool tool;
|
||||
EXPECT_THAT(tool.GetDescription(), Not(HasSubstr("")));
|
||||
}
|
||||
|
||||
TEST(MemoryCheckToolTest, DoesNotRequireLabels) {
|
||||
MemoryCheckTool tool;
|
||||
EXPECT_FALSE(tool.RequiresLabels());
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MemoryRegionsTool Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST(MemoryRegionsToolTest, GetNameReturnsCorrectName) {
|
||||
MemoryRegionsTool tool;
|
||||
EXPECT_EQ(tool.GetName(), "memory-regions");
|
||||
}
|
||||
|
||||
TEST(MemoryRegionsToolTest, GetUsageContainsFilter) {
|
||||
MemoryRegionsTool tool;
|
||||
EXPECT_THAT(tool.GetUsage(), HasSubstr("--filter"));
|
||||
}
|
||||
|
||||
TEST(MemoryRegionsToolTest, GetUsageContainsFormat) {
|
||||
MemoryRegionsTool tool;
|
||||
EXPECT_THAT(tool.GetUsage(), HasSubstr("--format"));
|
||||
}
|
||||
|
||||
TEST(MemoryRegionsToolTest, GetDescriptionIsNotEmpty) {
|
||||
MemoryRegionsTool tool;
|
||||
EXPECT_THAT(tool.GetDescription(), Not(HasSubstr("")));
|
||||
}
|
||||
|
||||
TEST(MemoryRegionsToolTest, DoesNotRequireLabels) {
|
||||
MemoryRegionsTool tool;
|
||||
EXPECT_FALSE(tool.RequiresLabels());
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Tool Name Uniqueness Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST(MemoryToolsTest, AllToolNamesAreUnique) {
|
||||
MemoryAnalyzeTool analyze;
|
||||
MemorySearchTool search;
|
||||
MemoryCompareTool compare;
|
||||
MemoryCheckTool check;
|
||||
MemoryRegionsTool regions;
|
||||
|
||||
std::vector<std::string> names = {
|
||||
analyze.GetName(), search.GetName(), compare.GetName(),
|
||||
check.GetName(), regions.GetName()};
|
||||
|
||||
// Check all names are unique
|
||||
std::set<std::string> unique_names(names.begin(), names.end());
|
||||
EXPECT_EQ(unique_names.size(), names.size())
|
||||
<< "All memory tool names should be unique";
|
||||
}
|
||||
|
||||
TEST(MemoryToolsTest, AllToolNamesStartWithMemory) {
|
||||
MemoryAnalyzeTool analyze;
|
||||
MemorySearchTool search;
|
||||
MemoryCompareTool compare;
|
||||
MemoryCheckTool check;
|
||||
MemoryRegionsTool regions;
|
||||
|
||||
// All memory tools should have names starting with "memory-"
|
||||
EXPECT_THAT(analyze.GetName(), HasSubstr("memory-"));
|
||||
EXPECT_THAT(search.GetName(), HasSubstr("memory-"));
|
||||
EXPECT_THAT(compare.GetName(), HasSubstr("memory-"));
|
||||
EXPECT_THAT(check.GetName(), HasSubstr("memory-"));
|
||||
EXPECT_THAT(regions.GetName(), HasSubstr("memory-"));
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Memory Address Constants Validation
|
||||
// =============================================================================
|
||||
|
||||
TEST(ALTTPMemoryMapTest, SpriteTableAddressesAreSequential) {
|
||||
// Sprite tables should be at sequential offsets
|
||||
uint32_t sprite_y_low = ALTTPMemoryMap::kSpriteYLow;
|
||||
uint32_t sprite_x_low = ALTTPMemoryMap::kSpriteXLow;
|
||||
uint32_t sprite_y_high = ALTTPMemoryMap::kSpriteYHigh;
|
||||
uint32_t sprite_x_high = ALTTPMemoryMap::kSpriteXHigh;
|
||||
|
||||
// Each table is 16 bytes (one per sprite)
|
||||
EXPECT_EQ(sprite_x_low - sprite_y_low, 0x10u);
|
||||
EXPECT_EQ(sprite_y_high - sprite_x_low, 0x10u);
|
||||
EXPECT_EQ(sprite_x_high - sprite_y_high, 0x10u);
|
||||
}
|
||||
|
||||
TEST(ALTTPMemoryMapTest, OAMBufferSizeIsCorrect) {
|
||||
uint32_t oam_size =
|
||||
ALTTPMemoryMap::kOAMBufferEnd - ALTTPMemoryMap::kOAMBuffer + 1;
|
||||
// OAM buffer should be 544 bytes (512 for main OAM + 32 for high table)
|
||||
EXPECT_EQ(oam_size, 0x220u); // 544 bytes
|
||||
}
|
||||
|
||||
TEST(ALTTPMemoryMapTest, SRAMRegionSizeIsCorrect) {
|
||||
uint32_t sram_size =
|
||||
ALTTPMemoryMap::kSRAMEnd - ALTTPMemoryMap::kSRAMStart + 1;
|
||||
// SRAM region should be 0x500 bytes (1280 bytes)
|
||||
EXPECT_EQ(sram_size, 0x500u);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace tools
|
||||
} // namespace agent
|
||||
} // namespace cli
|
||||
} // namespace yaze
|
||||
|
||||
555
test/unit/tools/project_tool_test.cc
Normal file
555
test/unit/tools/project_tool_test.cc
Normal file
@@ -0,0 +1,555 @@
|
||||
/**
|
||||
* @file project_tool_test.cc
|
||||
* @brief Unit tests for the ProjectTool AI agent tools
|
||||
*
|
||||
* Tests the project management functionality including snapshots,
|
||||
* edit serialization, checksum computation, and project diffing.
|
||||
*/
|
||||
|
||||
#include "cli/service/agent/tools/project_tool.h"
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "cli/service/agent/agent_context.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace cli {
|
||||
namespace agent {
|
||||
namespace tools {
|
||||
namespace {
|
||||
|
||||
using ::testing::Contains;
|
||||
using ::testing::Eq;
|
||||
using ::testing::HasSubstr;
|
||||
using ::testing::IsEmpty;
|
||||
using ::testing::Not;
|
||||
using ::testing::SizeIs;
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
// =============================================================================
|
||||
// EditFileHeader Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST(EditFileHeaderTest, MagicConstantIsYAZE) {
|
||||
// "YAZE" in ASCII = 0x59 0x41 0x5A 0x45 = 0x59415A45 (big endian)
|
||||
// But stored as 0x59415A45 which is little-endian "EZAY" or big-endian "YAZE"
|
||||
EXPECT_EQ(EditFileHeader::kMagic, 0x59415A45u);
|
||||
}
|
||||
|
||||
TEST(EditFileHeaderTest, CurrentVersionIsOne) {
|
||||
EXPECT_EQ(EditFileHeader::kCurrentVersion, 1u);
|
||||
}
|
||||
|
||||
TEST(EditFileHeaderTest, DefaultValuesAreCorrect) {
|
||||
EditFileHeader header;
|
||||
EXPECT_EQ(header.magic, EditFileHeader::kMagic);
|
||||
EXPECT_EQ(header.version, EditFileHeader::kCurrentVersion);
|
||||
}
|
||||
|
||||
TEST(EditFileHeaderTest, HasRomChecksumField) {
|
||||
EditFileHeader header;
|
||||
EXPECT_EQ(header.base_rom_sha256.size(), 32u);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SerializedEdit Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST(SerializedEditTest, StructureHasExpectedFields) {
|
||||
SerializedEdit edit;
|
||||
edit.address = 0x008000;
|
||||
edit.length = 4;
|
||||
|
||||
EXPECT_EQ(edit.address, 0x008000u);
|
||||
EXPECT_EQ(edit.length, 4u);
|
||||
}
|
||||
|
||||
TEST(SerializedEditTest, StructureSize) {
|
||||
// SerializedEdit should be 8 bytes (2 uint32_t)
|
||||
EXPECT_EQ(sizeof(SerializedEdit), 8u);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ProjectToolUtils Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST(ProjectToolUtilsTest, ComputeSHA256ProducesCorrectLength) {
|
||||
std::vector<uint8_t> data = {0x00, 0x01, 0x02, 0x03};
|
||||
auto hash = ProjectToolUtils::ComputeSHA256(data.data(), data.size());
|
||||
EXPECT_EQ(hash.size(), 32u);
|
||||
}
|
||||
|
||||
TEST(ProjectToolUtilsTest, ComputeSHA256IsDeterministic) {
|
||||
std::vector<uint8_t> data = {0x48, 0x65, 0x6C, 0x6C, 0x6F}; // "Hello"
|
||||
|
||||
auto hash1 = ProjectToolUtils::ComputeSHA256(data.data(), data.size());
|
||||
auto hash2 = ProjectToolUtils::ComputeSHA256(data.data(), data.size());
|
||||
|
||||
EXPECT_EQ(hash1, hash2);
|
||||
}
|
||||
|
||||
TEST(ProjectToolUtilsTest, ComputeSHA256DifferentDataDifferentHash) {
|
||||
std::vector<uint8_t> data1 = {0x00, 0x01, 0x02};
|
||||
std::vector<uint8_t> data2 = {0x00, 0x01, 0x03};
|
||||
|
||||
auto hash1 = ProjectToolUtils::ComputeSHA256(data1.data(), data1.size());
|
||||
auto hash2 = ProjectToolUtils::ComputeSHA256(data2.data(), data2.size());
|
||||
|
||||
EXPECT_NE(hash1, hash2);
|
||||
}
|
||||
|
||||
TEST(ProjectToolUtilsTest, ComputeSHA256EmptyInput) {
|
||||
auto hash = ProjectToolUtils::ComputeSHA256(nullptr, 0);
|
||||
EXPECT_EQ(hash.size(), 32u);
|
||||
// SHA-256 of empty string is well-known
|
||||
// e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
|
||||
EXPECT_EQ(hash[0], 0xe3);
|
||||
EXPECT_EQ(hash[1], 0xb0);
|
||||
EXPECT_EQ(hash[2], 0xc4);
|
||||
}
|
||||
|
||||
TEST(ProjectToolUtilsTest, FormatChecksumProduces64Chars) {
|
||||
std::array<uint8_t, 32> checksum;
|
||||
checksum.fill(0xAB);
|
||||
|
||||
std::string formatted = ProjectToolUtils::FormatChecksum(checksum);
|
||||
EXPECT_EQ(formatted.size(), 64u);
|
||||
}
|
||||
|
||||
TEST(ProjectToolUtilsTest, FormatChecksumIsHex) {
|
||||
std::array<uint8_t, 32> checksum;
|
||||
for (size_t i = 0; i < 32; ++i) {
|
||||
checksum[i] = static_cast<uint8_t>(i);
|
||||
}
|
||||
|
||||
std::string formatted = ProjectToolUtils::FormatChecksum(checksum);
|
||||
|
||||
// Should only contain hex characters
|
||||
for (char c : formatted) {
|
||||
EXPECT_TRUE((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))
|
||||
<< "Non-hex character: " << c;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(ProjectToolUtilsTest, FormatTimestampProducesISO8601) {
|
||||
auto now = std::chrono::system_clock::now();
|
||||
std::string formatted = ProjectToolUtils::FormatTimestamp(now);
|
||||
|
||||
// Should match ISO 8601 format: YYYY-MM-DDTHH:MM:SSZ
|
||||
EXPECT_THAT(formatted, HasSubstr("T"));
|
||||
EXPECT_THAT(formatted, HasSubstr("Z"));
|
||||
EXPECT_GE(formatted.size(), 20u);
|
||||
}
|
||||
|
||||
TEST(ProjectToolUtilsTest, ParseTimestampRoundTrip) {
|
||||
auto original = std::chrono::system_clock::now();
|
||||
std::string formatted = ProjectToolUtils::FormatTimestamp(original);
|
||||
|
||||
auto parsed_result = ProjectToolUtils::ParseTimestamp(formatted);
|
||||
ASSERT_TRUE(parsed_result.ok()) << parsed_result.status().message();
|
||||
|
||||
// Due to second-precision and timezone handling (gmtime vs mktime),
|
||||
// allow for timezone differences (up to 24 hours)
|
||||
auto parsed = *parsed_result;
|
||||
auto diff = std::chrono::duration_cast<std::chrono::hours>(
|
||||
original - parsed).count();
|
||||
EXPECT_LE(std::abs(diff), 24) << "Timestamp difference exceeds 24 hours";
|
||||
}
|
||||
|
||||
TEST(ProjectToolUtilsTest, ParseTimestampInvalidFormat) {
|
||||
auto result = ProjectToolUtils::ParseTimestamp("invalid");
|
||||
EXPECT_FALSE(result.ok());
|
||||
EXPECT_EQ(result.status().code(), absl::StatusCode::kInvalidArgument);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ProjectSnapshot Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST(ProjectSnapshotTest, DefaultConstruction) {
|
||||
ProjectSnapshot snapshot;
|
||||
EXPECT_TRUE(snapshot.name.empty());
|
||||
EXPECT_TRUE(snapshot.description.empty());
|
||||
EXPECT_TRUE(snapshot.edits.empty());
|
||||
EXPECT_TRUE(snapshot.metadata.empty());
|
||||
}
|
||||
|
||||
TEST(ProjectSnapshotTest, HasAllRequiredFields) {
|
||||
ProjectSnapshot snapshot;
|
||||
snapshot.name = "test-snapshot";
|
||||
snapshot.description = "Test description";
|
||||
snapshot.created = std::chrono::system_clock::now();
|
||||
RomEdit edit;
|
||||
edit.address = 0x008000;
|
||||
edit.old_value = {0x00};
|
||||
edit.new_value = {0x01};
|
||||
edit.description = "Test edit";
|
||||
edit.timestamp = std::chrono::system_clock::now();
|
||||
snapshot.edits.push_back(edit);
|
||||
snapshot.metadata["author"] = "test";
|
||||
snapshot.rom_checksum.fill(0xAB);
|
||||
|
||||
EXPECT_EQ(snapshot.name, "test-snapshot");
|
||||
EXPECT_EQ(snapshot.description, "Test description");
|
||||
EXPECT_THAT(snapshot.edits, SizeIs(1));
|
||||
EXPECT_EQ(snapshot.metadata["author"], "test");
|
||||
EXPECT_EQ(snapshot.rom_checksum[0], 0xAB);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ProjectManager Tests
|
||||
// =============================================================================
|
||||
|
||||
class ProjectManagerTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
// Create a temporary directory for tests
|
||||
test_dir_ = fs::temp_directory_path() / "yaze_project_test";
|
||||
fs::create_directories(test_dir_);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
// Clean up
|
||||
if (fs::exists(test_dir_)) {
|
||||
fs::remove_all(test_dir_);
|
||||
}
|
||||
}
|
||||
|
||||
fs::path test_dir_;
|
||||
};
|
||||
|
||||
TEST_F(ProjectManagerTest, IsNotInitializedByDefault) {
|
||||
ProjectManager manager;
|
||||
EXPECT_FALSE(manager.IsInitialized());
|
||||
}
|
||||
|
||||
TEST_F(ProjectManagerTest, InitializeCreatesProjectDirectory) {
|
||||
ProjectManager manager;
|
||||
auto status = manager.Initialize(test_dir_.string());
|
||||
ASSERT_TRUE(status.ok()) << status.message();
|
||||
|
||||
EXPECT_TRUE(manager.IsInitialized());
|
||||
EXPECT_TRUE(fs::exists(test_dir_ / ".yaze-project"));
|
||||
EXPECT_TRUE(fs::exists(test_dir_ / ".yaze-project" / "snapshots"));
|
||||
EXPECT_TRUE(fs::exists(test_dir_ / ".yaze-project" / "project.json"));
|
||||
}
|
||||
|
||||
TEST_F(ProjectManagerTest, ListSnapshotsEmptyInitially) {
|
||||
ProjectManager manager;
|
||||
auto status = manager.Initialize(test_dir_.string());
|
||||
ASSERT_TRUE(status.ok());
|
||||
|
||||
auto snapshots = manager.ListSnapshots();
|
||||
EXPECT_TRUE(snapshots.empty());
|
||||
}
|
||||
|
||||
TEST_F(ProjectManagerTest, CreateSnapshotEmptyNameFails) {
|
||||
ProjectManager manager;
|
||||
manager.Initialize(test_dir_.string());
|
||||
|
||||
std::array<uint8_t, 32> checksum;
|
||||
checksum.fill(0);
|
||||
|
||||
auto status = manager.CreateSnapshot("", "description", {}, checksum);
|
||||
EXPECT_FALSE(status.ok());
|
||||
EXPECT_EQ(status.code(), absl::StatusCode::kInvalidArgument);
|
||||
}
|
||||
|
||||
TEST_F(ProjectManagerTest, CreateSnapshotDuplicateNameFails) {
|
||||
ProjectManager manager;
|
||||
manager.Initialize(test_dir_.string());
|
||||
|
||||
std::array<uint8_t, 32> checksum;
|
||||
checksum.fill(0);
|
||||
|
||||
auto status1 = manager.CreateSnapshot("test", "first", {}, checksum);
|
||||
ASSERT_TRUE(status1.ok());
|
||||
|
||||
auto status2 = manager.CreateSnapshot("test", "second", {}, checksum);
|
||||
EXPECT_FALSE(status2.ok());
|
||||
EXPECT_EQ(status2.code(), absl::StatusCode::kAlreadyExists);
|
||||
}
|
||||
|
||||
TEST_F(ProjectManagerTest, CreateAndListSnapshot) {
|
||||
ProjectManager manager;
|
||||
manager.Initialize(test_dir_.string());
|
||||
|
||||
std::array<uint8_t, 32> checksum;
|
||||
checksum.fill(0xAB);
|
||||
|
||||
auto status = manager.CreateSnapshot("v1.0", "Initial version", {}, checksum);
|
||||
ASSERT_TRUE(status.ok());
|
||||
|
||||
auto snapshots = manager.ListSnapshots();
|
||||
EXPECT_THAT(snapshots, Contains("v1.0"));
|
||||
}
|
||||
|
||||
TEST_F(ProjectManagerTest, GetSnapshotNotFound) {
|
||||
ProjectManager manager;
|
||||
manager.Initialize(test_dir_.string());
|
||||
|
||||
auto result = manager.GetSnapshot("nonexistent");
|
||||
EXPECT_FALSE(result.ok());
|
||||
EXPECT_EQ(result.status().code(), absl::StatusCode::kNotFound);
|
||||
}
|
||||
|
||||
TEST_F(ProjectManagerTest, DeleteSnapshotNotFound) {
|
||||
ProjectManager manager;
|
||||
manager.Initialize(test_dir_.string());
|
||||
|
||||
auto status = manager.DeleteSnapshot("nonexistent");
|
||||
EXPECT_FALSE(status.ok());
|
||||
EXPECT_EQ(status.code(), absl::StatusCode::kNotFound);
|
||||
}
|
||||
|
||||
TEST_F(ProjectManagerTest, CreateGetDeleteSnapshot) {
|
||||
ProjectManager manager;
|
||||
manager.Initialize(test_dir_.string());
|
||||
|
||||
std::array<uint8_t, 32> checksum;
|
||||
checksum.fill(0xAB);
|
||||
|
||||
// Create
|
||||
auto create_status = manager.CreateSnapshot("test", "desc", {}, checksum);
|
||||
ASSERT_TRUE(create_status.ok());
|
||||
|
||||
// Get
|
||||
auto get_result = manager.GetSnapshot("test");
|
||||
ASSERT_TRUE(get_result.ok());
|
||||
EXPECT_EQ(get_result->name, "test");
|
||||
EXPECT_EQ(get_result->description, "desc");
|
||||
|
||||
// Delete
|
||||
auto delete_status = manager.DeleteSnapshot("test");
|
||||
ASSERT_TRUE(delete_status.ok());
|
||||
|
||||
// Verify deleted
|
||||
auto verify_result = manager.GetSnapshot("test");
|
||||
EXPECT_FALSE(verify_result.ok());
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Tool Name Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST(ProjectToolsTest, ProjectStatusToolName) {
|
||||
ProjectStatusTool tool;
|
||||
EXPECT_EQ(tool.GetName(), "project-status");
|
||||
}
|
||||
|
||||
TEST(ProjectToolsTest, ProjectSnapshotToolName) {
|
||||
ProjectSnapshotTool tool;
|
||||
EXPECT_EQ(tool.GetName(), "project-snapshot");
|
||||
}
|
||||
|
||||
TEST(ProjectToolsTest, ProjectRestoreToolName) {
|
||||
ProjectRestoreTool tool;
|
||||
EXPECT_EQ(tool.GetName(), "project-restore");
|
||||
}
|
||||
|
||||
TEST(ProjectToolsTest, ProjectExportToolName) {
|
||||
ProjectExportTool tool;
|
||||
EXPECT_EQ(tool.GetName(), "project-export");
|
||||
}
|
||||
|
||||
TEST(ProjectToolsTest, ProjectImportToolName) {
|
||||
ProjectImportTool tool;
|
||||
EXPECT_EQ(tool.GetName(), "project-import");
|
||||
}
|
||||
|
||||
TEST(ProjectToolsTest, ProjectDiffToolName) {
|
||||
ProjectDiffTool tool;
|
||||
EXPECT_EQ(tool.GetName(), "project-diff");
|
||||
}
|
||||
|
||||
TEST(ProjectToolsTest, AllToolNamesStartWithProject) {
|
||||
ProjectStatusTool status;
|
||||
ProjectSnapshotTool snapshot;
|
||||
ProjectRestoreTool restore;
|
||||
ProjectExportTool export_tool;
|
||||
ProjectImportTool import_tool;
|
||||
ProjectDiffTool diff;
|
||||
|
||||
EXPECT_THAT(status.GetName(), HasSubstr("project-"));
|
||||
EXPECT_THAT(snapshot.GetName(), HasSubstr("project-"));
|
||||
EXPECT_THAT(restore.GetName(), HasSubstr("project-"));
|
||||
EXPECT_THAT(export_tool.GetName(), HasSubstr("project-"));
|
||||
EXPECT_THAT(import_tool.GetName(), HasSubstr("project-"));
|
||||
EXPECT_THAT(diff.GetName(), HasSubstr("project-"));
|
||||
}
|
||||
|
||||
TEST(ProjectToolsTest, AllToolNamesAreUnique) {
|
||||
ProjectStatusTool status;
|
||||
ProjectSnapshotTool snapshot;
|
||||
ProjectRestoreTool restore;
|
||||
ProjectExportTool export_tool;
|
||||
ProjectImportTool import_tool;
|
||||
ProjectDiffTool diff;
|
||||
|
||||
std::vector<std::string> names = {
|
||||
status.GetName(), snapshot.GetName(), restore.GetName(),
|
||||
export_tool.GetName(), import_tool.GetName(), diff.GetName()};
|
||||
|
||||
std::set<std::string> unique_names(names.begin(), names.end());
|
||||
EXPECT_EQ(unique_names.size(), names.size())
|
||||
<< "All project tool names should be unique";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Tool Usage String Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST(ProjectToolsTest, StatusToolUsageFormat) {
|
||||
ProjectStatusTool tool;
|
||||
EXPECT_THAT(tool.GetUsage(), HasSubstr("project-status"));
|
||||
EXPECT_THAT(tool.GetUsage(), HasSubstr("--format"));
|
||||
}
|
||||
|
||||
TEST(ProjectToolsTest, SnapshotToolUsageFormat) {
|
||||
ProjectSnapshotTool tool;
|
||||
EXPECT_THAT(tool.GetUsage(), HasSubstr("--name"));
|
||||
EXPECT_THAT(tool.GetUsage(), HasSubstr("--description"));
|
||||
}
|
||||
|
||||
TEST(ProjectToolsTest, RestoreToolUsageFormat) {
|
||||
ProjectRestoreTool tool;
|
||||
EXPECT_THAT(tool.GetUsage(), HasSubstr("--name"));
|
||||
}
|
||||
|
||||
TEST(ProjectToolsTest, ExportToolUsageFormat) {
|
||||
ProjectExportTool tool;
|
||||
EXPECT_THAT(tool.GetUsage(), HasSubstr("--path"));
|
||||
EXPECT_THAT(tool.GetUsage(), HasSubstr("--include-rom"));
|
||||
}
|
||||
|
||||
TEST(ProjectToolsTest, ImportToolUsageFormat) {
|
||||
ProjectImportTool tool;
|
||||
EXPECT_THAT(tool.GetUsage(), HasSubstr("--path"));
|
||||
}
|
||||
|
||||
TEST(ProjectToolsTest, DiffToolUsageFormat) {
|
||||
ProjectDiffTool tool;
|
||||
EXPECT_THAT(tool.GetUsage(), HasSubstr("--snapshot1"));
|
||||
EXPECT_THAT(tool.GetUsage(), HasSubstr("--snapshot2"));
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// RequiresLabels Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST(ProjectToolsTest, NoToolsRequireLabels) {
|
||||
ProjectStatusTool status;
|
||||
ProjectSnapshotTool snapshot;
|
||||
ProjectRestoreTool restore;
|
||||
ProjectExportTool export_tool;
|
||||
ProjectImportTool import_tool;
|
||||
ProjectDiffTool diff;
|
||||
|
||||
EXPECT_FALSE(status.RequiresLabels());
|
||||
EXPECT_FALSE(snapshot.RequiresLabels());
|
||||
EXPECT_FALSE(restore.RequiresLabels());
|
||||
EXPECT_FALSE(export_tool.RequiresLabels());
|
||||
EXPECT_FALSE(import_tool.RequiresLabels());
|
||||
EXPECT_FALSE(diff.RequiresLabels());
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Snapshot Serialization Round-Trip Test
|
||||
// =============================================================================
|
||||
|
||||
class SnapshotSerializationTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
test_file_ = fs::temp_directory_path() / "test_snapshot.edits";
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (fs::exists(test_file_)) {
|
||||
fs::remove(test_file_);
|
||||
}
|
||||
}
|
||||
|
||||
fs::path test_file_;
|
||||
};
|
||||
|
||||
TEST_F(SnapshotSerializationTest, SaveAndLoadRoundTrip) {
|
||||
// Create snapshot with edits
|
||||
ProjectSnapshot original;
|
||||
original.name = "test-snapshot";
|
||||
original.description = "Test description";
|
||||
original.created = std::chrono::system_clock::now();
|
||||
original.rom_checksum.fill(0xAB);
|
||||
|
||||
RomEdit edit1;
|
||||
edit1.address = 0x008000;
|
||||
edit1.old_value = {0x00, 0x01, 0x02};
|
||||
edit1.new_value = {0x10, 0x11, 0x12};
|
||||
original.edits.push_back(edit1);
|
||||
|
||||
RomEdit edit2;
|
||||
edit2.address = 0x00A000;
|
||||
edit2.old_value = {0xFF};
|
||||
edit2.new_value = {0x00};
|
||||
original.edits.push_back(edit2);
|
||||
|
||||
original.metadata["author"] = "test";
|
||||
original.metadata["version"] = "1.0";
|
||||
|
||||
// Save
|
||||
auto save_status = original.SaveToFile(test_file_.string());
|
||||
ASSERT_TRUE(save_status.ok()) << save_status.message();
|
||||
ASSERT_TRUE(fs::exists(test_file_));
|
||||
|
||||
// Load
|
||||
auto load_result = ProjectSnapshot::LoadFromFile(test_file_.string());
|
||||
ASSERT_TRUE(load_result.ok()) << load_result.status().message();
|
||||
|
||||
const ProjectSnapshot& loaded = *load_result;
|
||||
|
||||
// Verify
|
||||
EXPECT_EQ(loaded.name, original.name);
|
||||
EXPECT_EQ(loaded.description, original.description);
|
||||
EXPECT_EQ(loaded.rom_checksum, original.rom_checksum);
|
||||
ASSERT_EQ(loaded.edits.size(), original.edits.size());
|
||||
|
||||
for (size_t i = 0; i < loaded.edits.size(); ++i) {
|
||||
EXPECT_EQ(loaded.edits[i].address, original.edits[i].address);
|
||||
EXPECT_EQ(loaded.edits[i].old_value, original.edits[i].old_value);
|
||||
EXPECT_EQ(loaded.edits[i].new_value, original.edits[i].new_value);
|
||||
}
|
||||
|
||||
EXPECT_EQ(loaded.metadata, original.metadata);
|
||||
}
|
||||
|
||||
TEST_F(SnapshotSerializationTest, LoadNonexistentFileFails) {
|
||||
auto result = ProjectSnapshot::LoadFromFile("/nonexistent/path/file.edits");
|
||||
EXPECT_FALSE(result.ok());
|
||||
EXPECT_EQ(result.status().code(), absl::StatusCode::kNotFound);
|
||||
}
|
||||
|
||||
TEST_F(SnapshotSerializationTest, LoadInvalidFileFails) {
|
||||
// Create an invalid file
|
||||
std::ofstream file(test_file_, std::ios::binary);
|
||||
file << "invalid data";
|
||||
file.close();
|
||||
|
||||
auto result = ProjectSnapshot::LoadFromFile(test_file_.string());
|
||||
EXPECT_FALSE(result.ok());
|
||||
EXPECT_EQ(result.status().code(), absl::StatusCode::kInvalidArgument);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace tools
|
||||
} // namespace agent
|
||||
} // namespace cli
|
||||
} // namespace yaze
|
||||
311
test/unit/tools/visual_analysis_tool_test.cc
Normal file
311
test/unit/tools/visual_analysis_tool_test.cc
Normal file
@@ -0,0 +1,311 @@
|
||||
/**
|
||||
* @file visual_analysis_tool_test.cc
|
||||
* @brief Unit tests for visual analysis tools
|
||||
*/
|
||||
|
||||
#include "cli/service/agent/tools/visual_analysis_tool.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
|
||||
namespace yaze {
|
||||
namespace cli {
|
||||
namespace agent {
|
||||
namespace tools {
|
||||
namespace {
|
||||
|
||||
// Test fixture for VisualAnalysisBase helper functions
|
||||
class VisualAnalysisBaseTest : public ::testing::Test {
|
||||
protected:
|
||||
// Create a test subclass to access protected methods
|
||||
class TestableVisualAnalysis : public VisualAnalysisBase {
|
||||
public:
|
||||
std::string GetName() const override { return "test-visual-analysis"; }
|
||||
std::string GetUsage() const override { return "test usage"; }
|
||||
|
||||
// Expose protected methods for testing
|
||||
using VisualAnalysisBase::ComputePixelDifference;
|
||||
using VisualAnalysisBase::ComputeStructuralSimilarity;
|
||||
using VisualAnalysisBase::IsRegionEmpty;
|
||||
using VisualAnalysisBase::FormatMatchesAsJson;
|
||||
using VisualAnalysisBase::FormatRegionsAsJson;
|
||||
|
||||
protected:
|
||||
absl::Status ValidateArgs(
|
||||
const resources::ArgumentParser& /*parser*/) override {
|
||||
return absl::OkStatus();
|
||||
}
|
||||
absl::Status Execute(Rom* /*rom*/,
|
||||
const resources::ArgumentParser& /*parser*/,
|
||||
resources::OutputFormatter& /*formatter*/) override {
|
||||
return absl::OkStatus();
|
||||
}
|
||||
};
|
||||
|
||||
TestableVisualAnalysis tool_;
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// ComputePixelDifference Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(VisualAnalysisBaseTest, PixelDifference_IdenticalTiles_Returns100) {
|
||||
std::vector<uint8_t> tile(64, 0x42); // All pixels same value
|
||||
double similarity = tool_.ComputePixelDifference(tile, tile);
|
||||
EXPECT_DOUBLE_EQ(similarity, 100.0);
|
||||
}
|
||||
|
||||
TEST_F(VisualAnalysisBaseTest, PixelDifference_CompletelyDifferent_Returns0) {
|
||||
std::vector<uint8_t> tile_a(64, 0x00); // All black
|
||||
std::vector<uint8_t> tile_b(64, 0xFF); // All white
|
||||
double similarity = tool_.ComputePixelDifference(tile_a, tile_b);
|
||||
EXPECT_DOUBLE_EQ(similarity, 0.0);
|
||||
}
|
||||
|
||||
TEST_F(VisualAnalysisBaseTest, PixelDifference_HalfDifferent_Returns50) {
|
||||
std::vector<uint8_t> tile_a(64, 0x00);
|
||||
std::vector<uint8_t> tile_b(64, 0x00);
|
||||
// Make half the pixels maximally different
|
||||
for (int i = 0; i < 32; ++i) {
|
||||
tile_b[i] = 0xFF;
|
||||
}
|
||||
double similarity = tool_.ComputePixelDifference(tile_a, tile_b);
|
||||
EXPECT_NEAR(similarity, 50.0, 0.01);
|
||||
}
|
||||
|
||||
TEST_F(VisualAnalysisBaseTest, PixelDifference_EmptyTiles_Returns0) {
|
||||
std::vector<uint8_t> empty;
|
||||
double similarity = tool_.ComputePixelDifference(empty, empty);
|
||||
EXPECT_DOUBLE_EQ(similarity, 0.0);
|
||||
}
|
||||
|
||||
TEST_F(VisualAnalysisBaseTest, PixelDifference_DifferentSizes_Returns0) {
|
||||
std::vector<uint8_t> tile_a(64, 0x42);
|
||||
std::vector<uint8_t> tile_b(32, 0x42);
|
||||
double similarity = tool_.ComputePixelDifference(tile_a, tile_b);
|
||||
EXPECT_DOUBLE_EQ(similarity, 0.0);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ComputeStructuralSimilarity Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(VisualAnalysisBaseTest, StructuralSimilarity_IdenticalTiles_Returns100) {
|
||||
std::vector<uint8_t> tile(64, 0x42);
|
||||
double similarity = tool_.ComputeStructuralSimilarity(tile, tile);
|
||||
EXPECT_GE(similarity, 99.0); // Should be very close to 100
|
||||
}
|
||||
|
||||
TEST_F(VisualAnalysisBaseTest, StructuralSimilarity_DifferentTiles_ReturnsLow) {
|
||||
std::vector<uint8_t> tile_a(64, 0x00);
|
||||
std::vector<uint8_t> tile_b(64, 0xFF);
|
||||
double similarity = tool_.ComputeStructuralSimilarity(tile_a, tile_b);
|
||||
EXPECT_LT(similarity, 50.0);
|
||||
}
|
||||
|
||||
TEST_F(VisualAnalysisBaseTest, StructuralSimilarity_SimilarPattern_ReturnsHigh) {
|
||||
// Create two tiles with similar structure but slightly different values
|
||||
std::vector<uint8_t> tile_a(64);
|
||||
std::vector<uint8_t> tile_b(64);
|
||||
|
||||
for (int i = 0; i < 64; ++i) {
|
||||
tile_a[i] = i % 16; // Pattern 0-15 repeated
|
||||
tile_b[i] = (i % 16) + 1; // Same pattern, shifted by 1
|
||||
}
|
||||
|
||||
double similarity = tool_.ComputeStructuralSimilarity(tile_a, tile_b);
|
||||
EXPECT_GT(similarity, 80.0); // Should be high due to similar structure
|
||||
}
|
||||
|
||||
TEST_F(VisualAnalysisBaseTest, StructuralSimilarity_EmptyTiles_Returns0) {
|
||||
std::vector<uint8_t> empty;
|
||||
double similarity = tool_.ComputeStructuralSimilarity(empty, empty);
|
||||
EXPECT_DOUBLE_EQ(similarity, 0.0);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// IsRegionEmpty Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(VisualAnalysisBaseTest, IsRegionEmpty_AllZeros_ReturnsTrue) {
|
||||
std::vector<uint8_t> data(64, 0x00);
|
||||
EXPECT_TRUE(tool_.IsRegionEmpty(data));
|
||||
}
|
||||
|
||||
TEST_F(VisualAnalysisBaseTest, IsRegionEmpty_AllFF_ReturnsTrue) {
|
||||
std::vector<uint8_t> data(64, 0xFF);
|
||||
EXPECT_TRUE(tool_.IsRegionEmpty(data));
|
||||
}
|
||||
|
||||
TEST_F(VisualAnalysisBaseTest, IsRegionEmpty_MostlyZeros_ReturnsTrue) {
|
||||
std::vector<uint8_t> data(100, 0x00);
|
||||
data[0] = 0x01; // Only 1% non-zero
|
||||
EXPECT_TRUE(tool_.IsRegionEmpty(data)); // >95% zeros
|
||||
}
|
||||
|
||||
TEST_F(VisualAnalysisBaseTest, IsRegionEmpty_HalfFilled_ReturnsFalse) {
|
||||
std::vector<uint8_t> data(64, 0x00);
|
||||
for (int i = 0; i < 32; ++i) {
|
||||
data[i] = 0x42;
|
||||
}
|
||||
EXPECT_FALSE(tool_.IsRegionEmpty(data));
|
||||
}
|
||||
|
||||
TEST_F(VisualAnalysisBaseTest, IsRegionEmpty_CompletelyFilled_ReturnsFalse) {
|
||||
std::vector<uint8_t> data(64, 0x42);
|
||||
EXPECT_FALSE(tool_.IsRegionEmpty(data));
|
||||
}
|
||||
|
||||
TEST_F(VisualAnalysisBaseTest, IsRegionEmpty_EmptyVector_ReturnsTrue) {
|
||||
std::vector<uint8_t> data;
|
||||
EXPECT_TRUE(tool_.IsRegionEmpty(data));
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// JSON Formatting Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(VisualAnalysisBaseTest, FormatMatchesAsJson_EmptyList_ReturnsValidJson) {
|
||||
std::vector<TileSimilarityMatch> matches;
|
||||
std::string json = tool_.FormatMatchesAsJson(matches);
|
||||
|
||||
EXPECT_TRUE(json.find("\"matches\": []") != std::string::npos);
|
||||
EXPECT_TRUE(json.find("\"total_matches\": 0") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_F(VisualAnalysisBaseTest, FormatMatchesAsJson_SingleMatch_ReturnsValidJson) {
|
||||
std::vector<TileSimilarityMatch> matches = {
|
||||
{.tile_id = 42, .similarity_score = 95.5, .sheet_index = 1,
|
||||
.x_position = 16, .y_position = 8}
|
||||
};
|
||||
std::string json = tool_.FormatMatchesAsJson(matches);
|
||||
|
||||
EXPECT_TRUE(json.find("\"tile_id\": 42") != std::string::npos);
|
||||
EXPECT_TRUE(json.find("\"similarity_score\": 95.50") != std::string::npos);
|
||||
EXPECT_TRUE(json.find("\"sheet_index\": 1") != std::string::npos);
|
||||
EXPECT_TRUE(json.find("\"total_matches\": 1") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_F(VisualAnalysisBaseTest, FormatRegionsAsJson_EmptyList_ReturnsValidJson) {
|
||||
std::vector<UnusedRegion> regions;
|
||||
std::string json = tool_.FormatRegionsAsJson(regions);
|
||||
|
||||
EXPECT_TRUE(json.find("\"unused_regions\": []") != std::string::npos);
|
||||
EXPECT_TRUE(json.find("\"total_regions\": 0") != std::string::npos);
|
||||
EXPECT_TRUE(json.find("\"total_free_tiles\": 0") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_F(VisualAnalysisBaseTest, FormatRegionsAsJson_SingleRegion_ReturnsValidJson) {
|
||||
std::vector<UnusedRegion> regions = {
|
||||
{.sheet_index = 5, .x = 0, .y = 0, .width = 16, .height = 8, .tile_count = 2}
|
||||
};
|
||||
std::string json = tool_.FormatRegionsAsJson(regions);
|
||||
|
||||
EXPECT_TRUE(json.find("\"sheet_index\": 5") != std::string::npos);
|
||||
EXPECT_TRUE(json.find("\"width\": 16") != std::string::npos);
|
||||
EXPECT_TRUE(json.find("\"tile_count\": 2") != std::string::npos);
|
||||
EXPECT_TRUE(json.find("\"total_free_tiles\": 2") != std::string::npos);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TileSimilarityMatch Struct Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST(TileSimilarityMatchTest, DefaultInitialization) {
|
||||
TileSimilarityMatch match = {};
|
||||
EXPECT_EQ(match.tile_id, 0);
|
||||
EXPECT_DOUBLE_EQ(match.similarity_score, 0.0);
|
||||
EXPECT_EQ(match.sheet_index, 0);
|
||||
EXPECT_EQ(match.x_position, 0);
|
||||
EXPECT_EQ(match.y_position, 0);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// UnusedRegion Struct Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST(UnusedRegionTest, DefaultInitialization) {
|
||||
UnusedRegion region = {};
|
||||
EXPECT_EQ(region.sheet_index, 0);
|
||||
EXPECT_EQ(region.x, 0);
|
||||
EXPECT_EQ(region.y, 0);
|
||||
EXPECT_EQ(region.width, 0);
|
||||
EXPECT_EQ(region.height, 0);
|
||||
EXPECT_EQ(region.tile_count, 0);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PaletteUsageStats Struct Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST(PaletteUsageStatsTest, DefaultInitialization) {
|
||||
PaletteUsageStats stats = {};
|
||||
EXPECT_EQ(stats.palette_index, 0);
|
||||
EXPECT_EQ(stats.usage_count, 0);
|
||||
EXPECT_DOUBLE_EQ(stats.usage_percentage, 0.0);
|
||||
EXPECT_TRUE(stats.used_by_maps.empty());
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TileUsageEntry Struct Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST(TileUsageEntryTest, DefaultInitialization) {
|
||||
TileUsageEntry entry = {};
|
||||
EXPECT_EQ(entry.tile_id, 0);
|
||||
EXPECT_EQ(entry.usage_count, 0);
|
||||
EXPECT_DOUBLE_EQ(entry.usage_percentage, 0.0);
|
||||
EXPECT_TRUE(entry.locations.empty());
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Constants Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST(VisualAnalysisConstantsTest, TileConstants) {
|
||||
EXPECT_EQ(VisualAnalysisBase::kTileWidth, 8);
|
||||
EXPECT_EQ(VisualAnalysisBase::kTileHeight, 8);
|
||||
EXPECT_EQ(VisualAnalysisBase::kTilePixels, 64);
|
||||
EXPECT_EQ(VisualAnalysisBase::kSheetWidth, 128);
|
||||
EXPECT_EQ(VisualAnalysisBase::kSheetHeight, 32);
|
||||
EXPECT_EQ(VisualAnalysisBase::kTilesPerRow, 16);
|
||||
EXPECT_EQ(VisualAnalysisBase::kMaxSheets, 223);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Edge Case Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(VisualAnalysisBaseTest, PixelDifference_LargeTile_HandlesCorrectly) {
|
||||
// Test with a larger tile (256 pixels = 16x16)
|
||||
std::vector<uint8_t> tile_a(256, 0x80);
|
||||
std::vector<uint8_t> tile_b(256, 0x80);
|
||||
tile_b[0] = 0x00; // Single pixel difference
|
||||
|
||||
double similarity = tool_.ComputePixelDifference(tile_a, tile_b);
|
||||
// (255/256 pixels same) = 99.6% similar after accounting for intensity diff
|
||||
EXPECT_GT(similarity, 99.0);
|
||||
}
|
||||
|
||||
TEST_F(VisualAnalysisBaseTest, StructuralSimilarity_GradientPattern_MatchesWell) {
|
||||
// Create gradient patterns
|
||||
std::vector<uint8_t> tile_a(64);
|
||||
std::vector<uint8_t> tile_b(64);
|
||||
|
||||
for (int i = 0; i < 64; ++i) {
|
||||
tile_a[i] = i * 4; // Gradient 0-252
|
||||
tile_b[i] = i * 4 + 2; // Same gradient, offset by 2
|
||||
}
|
||||
|
||||
double similarity = tool_.ComputeStructuralSimilarity(tile_a, tile_b);
|
||||
EXPECT_GT(similarity, 90.0); // Same structure
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace tools
|
||||
} // namespace agent
|
||||
} // namespace cli
|
||||
} // namespace yaze
|
||||
|
||||
Reference in New Issue
Block a user