Skip to main content
Glama
CommentServiceTest.java17.2 kB
package com.ghidramcp.services; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; /** * Unit tests for CommentService. * * These tests verify the CommentService input validation and document * the expected behavior for each comment type. * * Note: Full integration tests with Ghidra Program objects would require * the Ghidra test framework. E2E tests cover the actual functionality * with real binaries. We cannot mock Ghidra's Program interface directly * due to its complex dependency hierarchy. */ class CommentServiceTest { private CommentService service; private FunctionNavigator mockNavigator; @BeforeEach void setUp() { mockNavigator = mock(FunctionNavigator.class); service = new CommentService(mockNavigator); } // ==================== // Constructor Tests // ==================== @Test @DisplayName("Should create service with navigator") void testServiceConstruction() { FunctionNavigator navigator = mock(FunctionNavigator.class); CommentService commentService = new CommentService(navigator); assertNotNull(commentService, "Service should be created successfully"); } // ==================== // Null Program Tests // ==================== @Test @DisplayName("setDecompilerComment should return false for null program") void testSetDecompilerCommentNullProgram() { when(mockNavigator.getCurrentProgram()).thenReturn(null); boolean result = service.setDecompilerComment("0x1000", "test comment"); assertFalse(result, "Should return false when program is null"); verify(mockNavigator).getCurrentProgram(); } @Test @DisplayName("setDisassemblyComment should return false for null program") void testSetDisassemblyCommentNullProgram() { when(mockNavigator.getCurrentProgram()).thenReturn(null); boolean result = service.setDisassemblyComment("0x1000", "test comment"); assertFalse(result, "Should return false when program is null"); verify(mockNavigator).getCurrentProgram(); } @Test @DisplayName("setPlateComment should return false for null program") void testSetPlateCommentNullProgram() { when(mockNavigator.getCurrentProgram()).thenReturn(null); boolean result = service.setPlateComment("0x1000", "test comment"); assertFalse(result, "Should return false when program is null"); verify(mockNavigator).getCurrentProgram(); } // ==================== // Input Validation Documentation Tests // ==================== /** * Document expected behavior for null/empty address validation * * When address is null or empty: * - Method returns false immediately after program check * - No transaction is started * - No changes are made to the program */ @Test @DisplayName("Should document null/empty address validation behavior") void testAddressValidationDocumentation() { // The service checks: addressStr == null || addressStr.isEmpty() // If true, returns false without attempting any operation // Null address check String nullAddress = null; assertTrue(nullAddress == null, "Null address should be rejected"); // Empty address check String emptyAddress = ""; assertTrue(emptyAddress.isEmpty(), "Empty address should be rejected"); // Note: Whitespace-only addresses pass isEmpty() but fail at parsing String whitespaceAddress = " "; assertFalse(whitespaceAddress.isEmpty(), "Whitespace passes isEmpty check but fails parsing"); } /** * Document expected behavior for null comment validation * * When comment is null: * - Method returns false immediately after address check * - No transaction is started * - No changes are made to the program * * Note: Empty string comments ARE valid and can be used to clear comments */ @Test @DisplayName("Should document null comment validation behavior") void testCommentValidationDocumentation() { // The service checks: comment == null // If true, returns false without attempting any operation String nullComment = null; assertTrue(nullComment == null, "Null comment should be rejected"); // Empty string is valid (used to clear comments) String emptyComment = ""; assertFalse(emptyComment == null, "Empty string comment should be accepted"); } // ==================== // Valid Input Format Tests // ==================== @ParameterizedTest @ValueSource(strings = {"0x1000", "0x00401000", "00401000", "1000"}) @DisplayName("Should accept various valid address formats") void testValidAddressFormats(String address) { // This test documents that the service accepts various address formats // Actual parsing is handled by Ghidra's AddressFactory assertNotNull(address, "Address format should not be null"); assertFalse(address.isEmpty(), "Address format should not be empty"); } @ParameterizedTest @ValueSource(strings = {"", "Simple comment", "Multi\nline\ncomment", "Comment with special chars: !@#$%^&*()"}) @DisplayName("Should accept various comment formats") void testValidCommentFormats(String comment) { // This test documents that the service accepts various comment formats // Empty string is valid (can be used to clear a comment) assertNotNull(comment, "Comment format should not be null"); } // ==================== // Comment Type Documentation Tests // ==================== /** * Document expected behavior for decompiler comments (PRE type) * * PRE comments appear before the instruction in the decompiler view. * They are useful for explaining the purpose of a code block or * providing context for complex operations. * * In the Ghidra UI, PRE comments appear: * - Above the instruction in the listing view * - As block comments in the decompiler view */ @Test @DisplayName("Should document decompiler comment (PRE) behavior") void testDecompilerCommentDocumentation() { // Decompiler comments use CommentType.PRE // They appear in both listing and decompiler views // Useful for documenting algorithm steps or code blocks assertTrue(true, "Documentation test for decompiler comments (PRE type)"); } /** * Document expected behavior for disassembly comments (EOL type) * * EOL (End of Line) comments appear at the end of the instruction line * in the disassembly listing. They are useful for quick annotations * about specific instructions. * * In the Ghidra UI, EOL comments appear: * - At the end of the instruction line in the listing view * - May not be visible in the decompiler view */ @Test @DisplayName("Should document disassembly comment (EOL) behavior") void testDisassemblyCommentDocumentation() { // Disassembly comments use CommentType.EOL // They appear at the end of instruction lines // Useful for brief inline annotations assertTrue(true, "Documentation test for disassembly comments (EOL type)"); } /** * Document expected behavior for plate comments (PLATE type) * * PLATE comments appear as a boxed header above a function or code block. * They are typically used for function-level documentation or * major section markers. * * In the Ghidra UI, PLATE comments appear: * - As a bordered box above the code in the listing view * - Often used at function entry points */ @Test @DisplayName("Should document plate comment (PLATE) behavior") void testPlateCommentDocumentation() { // Plate comments use CommentType.PLATE // They appear as boxed headers in the listing view // Useful for function or section documentation assertTrue(true, "Documentation test for plate comments (PLATE type)"); } // ==================== // Transaction Behavior Documentation Tests // ==================== /** * Document expected transaction behavior * * All comment operations are wrapped in Ghidra transactions: * - Transaction is started before modification * - Transaction is committed on success * - Transaction is rolled back on failure * * This ensures database consistency and enables undo/redo functionality. */ @Test @DisplayName("Should document transaction behavior") void testTransactionBehaviorDocumentation() { // Each comment operation: // 1. Starts a transaction with descriptive name // 2. Performs the modification // 3. Ends transaction with success/failure status // // This allows: // - Undo/redo in Ghidra UI // - Atomic operations // - Database consistency assertTrue(true, "Documentation test for transaction behavior"); } /** * Document expected threading behavior * * All comment operations execute on the Swing Event Dispatch Thread (EDT) * using SwingUtilities.invokeAndWait(). This ensures: * - Thread safety for Ghidra's database operations * - Proper synchronization with UI updates * - Consistent behavior with Ghidra's threading model */ @Test @DisplayName("Should document threading behavior") void testThreadingBehaviorDocumentation() { // Comment operations use SwingUtilities.invokeAndWait() // This ensures: // - Operations run on the EDT // - Caller blocks until completion // - Thread-safe access to program database assertTrue(true, "Documentation test for threading behavior"); } // ==================== // Error Handling Documentation Tests // ==================== /** * Document expected behavior for invalid addresses * * When an address cannot be parsed by Ghidra's AddressFactory: * - An exception is caught internally * - Error is logged via Msg.error() * - Method returns false * - Transaction is rolled back */ @Test @DisplayName("Should document invalid address handling") void testInvalidAddressHandlingDocumentation() { // When address parsing fails: // - Exception is caught in the transaction block // - Error is logged with context // - Transaction is ended with false (rollback) // - Method returns false to caller assertTrue(true, "Documentation test for invalid address handling"); } /** * Document expected behavior for interrupted operations * * If the Swing thread operation is interrupted: * - InterruptedException is caught * - Error is logged via Msg.error() * - Method returns false */ @Test @DisplayName("Should document interrupted operation handling") void testInterruptedOperationHandlingDocumentation() { // When SwingUtilities.invokeAndWait() is interrupted: // - InterruptedException is caught // - InvocationTargetException is also handled // - Error is logged with context // - Method returns false assertTrue(true, "Documentation test for interrupted operation handling"); } // ==================== // Edge Case Documentation Tests // ==================== @Test @DisplayName("Should document empty string comment behavior (clears comment)") void testEmptyStringCommentDocumentation() { // Empty string comments are valid and can be used to clear existing comments // This is different from null, which is rejected String emptyComment = ""; assertNotNull(emptyComment, "Empty string should be accepted"); assertFalse(emptyComment == null, "Empty string is not null"); } @Test @DisplayName("Should document whitespace-only address behavior") void testWhitespaceOnlyAddressDocumentation() { // Whitespace-only addresses are not empty strings, so they pass // the isEmpty() check but will fail at address parsing String whitespaceAddress = " "; assertFalse(whitespaceAddress.isEmpty(), "Whitespace-only is not considered empty by String.isEmpty()"); // These will fail at Ghidra's address parsing stage } @Test @DisplayName("Should handle very long comments") void testVeryLongComment() { // Document that very long comments are accepted // Actual storage limits are determined by Ghidra String longComment = "A".repeat(10000); assertEquals(10000, longComment.length(), "Should create long comment"); } @Test @DisplayName("Should handle comments with unicode characters") void testUnicodeComment() { // Document that unicode comments are supported String unicodeComment = "Comment with unicode: \u00e9\u00e8\u00ea \u4e2d\u6587 \u0410\u0411\u0412"; assertNotNull(unicodeComment, "Unicode comments should be supported"); assertFalse(unicodeComment.isEmpty(), "Unicode comment should not be empty"); } @Test @DisplayName("Should handle comments with newlines") void testMultilineComment() { // Document that multiline comments are supported String multilineComment = "Line 1\nLine 2\nLine 3"; assertTrue(multilineComment.contains("\n"), "Multiline comments should be supported"); } // ==================== // Multiple Operation Tests // ==================== @Test @DisplayName("Should allow multiple comment operations on same address") void testMultipleCommentsOnSameAddress() { // Document that different comment types can coexist at same address // Each type (PRE, EOL, PLATE) is stored separately when(mockNavigator.getCurrentProgram()).thenReturn(null); // All operations will return false due to null program, // but this documents that the service supports multiple comment types String address = "0x1000"; service.setDecompilerComment(address, "PRE comment"); service.setDisassemblyComment(address, "EOL comment"); service.setPlateComment(address, "PLATE comment"); // Verify that all three methods were called (program was checked 3 times) verify(mockNavigator, times(3)).getCurrentProgram(); } @Test @DisplayName("Should allow comment operations on different addresses") void testCommentsOnDifferentAddresses() { // Document that comments can be set on multiple addresses when(mockNavigator.getCurrentProgram()).thenReturn(null); service.setDecompilerComment("0x1000", "Comment 1"); service.setDecompilerComment("0x2000", "Comment 2"); service.setDecompilerComment("0x3000", "Comment 3"); // Verify that getCurrentProgram was called for each operation verify(mockNavigator, times(3)).getCurrentProgram(); } // ==================== // Method Signature Documentation // ==================== @Test @DisplayName("Should document setDecompilerComment method signature") void testSetDecompilerCommentSignature() { // Method: boolean setDecompilerComment(String addressStr, String comment) // Uses: CommentType.PRE // Transaction name: "Set decompiler comment" // Returns: true on success, false on failure when(mockNavigator.getCurrentProgram()).thenReturn(null); boolean result = service.setDecompilerComment("0x1000", "test"); assertFalse(result, "Should return boolean result"); } @Test @DisplayName("Should document setDisassemblyComment method signature") void testSetDisassemblyCommentSignature() { // Method: boolean setDisassemblyComment(String addressStr, String comment) // Uses: CommentType.EOL // Transaction name: "Set disassembly comment" // Returns: true on success, false on failure when(mockNavigator.getCurrentProgram()).thenReturn(null); boolean result = service.setDisassemblyComment("0x1000", "test"); assertFalse(result, "Should return boolean result"); } @Test @DisplayName("Should document setPlateComment method signature") void testSetPlateCommentSignature() { // Method: boolean setPlateComment(String addressStr, String comment) // Uses: CommentType.PLATE // Transaction name: "Set plate comment" // Returns: true on success, false on failure when(mockNavigator.getCurrentProgram()).thenReturn(null); boolean result = service.setPlateComment("0x1000", "test"); assertFalse(result, "Should return boolean result"); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/HK47196/GhidraMCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server