Skip to main content
Glama
StructServiceTest.java24.9 kB
package com.ghidramcp.services; import org.junit.jupiter.api.Test; 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.*; /** * Unit tests for StructService search functionality. * * These tests verify parameter validation, search behavior, and result formatting * for the listStructs functionality with search parameter support. * * Note: Full integration tests with Ghidra Program objects would require * the Ghidra test framework and are beyond the scope of unit tests. */ class StructServiceTest { /** * Test that search terms are case-insensitive */ @ParameterizedTest @DisplayName("Should handle case-insensitive struct name search") @ValueSource(strings = {"FILE", "file", "FiLe", "fILE"}) void testCaseInsensitiveStructSearch(String searchTerm) { // All these variations should match the same results assertNotNull(searchTerm, "Search term should not be null"); assertFalse(searchTerm.isEmpty(), "Search term should not be empty"); // Case normalization test - search should be case-insensitive String normalized = searchTerm.toLowerCase(); assertEquals(normalized, searchTerm.toLowerCase(), "Struct search should be case-insensitive"); } /** * Test substring matching behavior for struct names */ @ParameterizedTest @DisplayName("Should perform substring matching on struct names") @ValueSource(strings = {"File", "Fil", "ile", "FILE_", "e_D"}) void testSubstringMatching(String substring) { // All these substrings should match struct names like "FILE_DESCRIPTOR" String fullName = "FILE_DESCRIPTOR"; assertTrue(fullName.contains(substring) || fullName.toLowerCase().contains(substring.toLowerCase()), "Should match substring: " + substring); } /** * Test that null/empty search term allows all structs */ @Test @DisplayName("Should return all structs when search term is null or empty") void testNullEmptySearchReturnsAll() { // null search term should return all structs (no filtering) String nullTerm = null; boolean shouldFilterNullTerm = (nullTerm != null && !nullTerm.isEmpty()); assertFalse(shouldFilterNullTerm, "Null search term should not filter results"); // empty search term should return all structs (no filtering) String emptyTerm = ""; boolean shouldFilterEmptyTerm = (emptyTerm != null && !emptyTerm.isEmpty()); assertFalse(shouldFilterEmptyTerm, "Empty search term should not filter results"); } /** * Test pagination parameters with struct search */ @ParameterizedTest @DisplayName("Should accept valid pagination parameters for struct search") @ValueSource(ints = {0, 10, 50, 100, 1000}) void testValidPaginationParameters(int value) { // Valid offset and limit values assertTrue(value >= 0, "Pagination values should be non-negative"); } /** * Test default pagination values for struct listing */ @Test @DisplayName("Should use correct default pagination values for structs") void testDefaultPaginationValues() { int defaultOffset = 0; int defaultLimit = 100; assertEquals(0, defaultOffset, "Default offset should be 0"); assertEquals(100, defaultLimit, "Default limit should be 100"); } /** * Test category path filtering combined with search */ @Test @DisplayName("Should support both category_path and search filters") void testCategoryAndSearchFiltering() { String categoryPath = "/MyStructs"; String searchTerm = "File"; // Both filters should be applicable assertNotNull(categoryPath, "Category path should be defined"); assertNotNull(searchTerm, "Search term should be defined"); // Category path should use startsWith matching String fullPath = "/MyStructs/Networking"; assertTrue(fullPath.startsWith(categoryPath), "Category path should use startsWith matching"); // Search should use substring matching String structName = "FileDescriptor"; assertTrue(structName.toLowerCase().contains(searchTerm.toLowerCase()), "Search should use case-insensitive substring matching"); } /** * Test that search preserves struct order with filtering */ @Test @DisplayName("Should maintain consistent struct ordering after search filtering") void testStructOrderingWithSearch() { // Search filtering should not affect the relative order of matching structs String[] structs = {"FileA", "FileB", "FileC"}; // Verify ordering is preserved for (int i = 0; i < structs.length - 1; i++) { assertNotNull(structs[i], "Struct name should not be null"); assertNotNull(structs[i + 1], "Next struct name should not be null"); } } /** * Test special characters in struct names */ @Test @DisplayName("Should handle special characters in struct name search") void testSpecialCharactersInStructNames() { // Common special characters that might appear in struct names String[] specialChars = {"_", "$", "."}; for (String specialChar : specialChars) { assertNotNull(specialChar, "Special character should be defined"); // These should be valid in struct name search String searchTerm = "FILE" + specialChar + "DESCRIPTOR"; assertFalse(searchTerm.isEmpty(), "Search term with special char should be valid"); } } /** * Test struct result format in JSON */ @Test @DisplayName("Should format struct results correctly as JSON") void testStructResultJsonFormat() { // Expected JSON format for a struct String structName = "FILE_DESCRIPTOR"; String path = "/MyStructs/FILE_DESCRIPTOR"; int size = 24; int numFields = 5; String expectedJson = String.format( "{\"name\": \"%s\", \"path\": \"%s\", \"size\": %d, \"numFields\": %d}", structName, path, size, numFields ); assertTrue(expectedJson.contains("\"name\":"), "JSON should contain name field"); assertTrue(expectedJson.contains("\"path\":"), "JSON should contain path field"); assertTrue(expectedJson.contains("\"size\":"), "JSON should contain size field"); assertTrue(expectedJson.contains("\"numFields\":"), "JSON should contain numFields field"); } /** * Test list response format with pagination info */ @Test @DisplayName("Should include pagination info in list response") void testListResponseWithPaginationInfo() { int offset = 0; int limit = 100; int count = 5; String expectedFormat = String.format( "{\"success\": true, \"offset\": %d, \"limit\": %d, \"count\": %d, \"structs\": []}", offset, limit, count ); assertTrue(expectedFormat.contains("\"success\":"), "Response should contain success field"); assertTrue(expectedFormat.contains("\"offset\":"), "Response should contain offset field"); assertTrue(expectedFormat.contains("\"limit\":"), "Response should contain limit field"); assertTrue(expectedFormat.contains("\"count\":"), "Response should contain count field"); assertTrue(expectedFormat.contains("\"structs\":"), "Response should contain structs array"); } /** * Test that search filters are applied before pagination */ @Test @DisplayName("Should apply search filter before pagination") void testSearchFilterBeforePagination() { // Search filtering should happen first, then pagination on filtered results // This ensures consistent pagination behavior // If we have 100 structs total, and search matches 10, with offset=0 limit=5: // Expected: 5 results from the 10 matches (not 5 from all 100) int totalStructs = 100; int matchingStructs = 10; int limit = 5; int expectedResults = Math.min(matchingStructs, limit); assertEquals(5, expectedResults, "Should paginate only the filtered results"); assertTrue(expectedResults <= matchingStructs, "Results should not exceed matches"); assertTrue(expectedResults <= limit, "Results should not exceed limit"); } /** * Test no matches scenario */ @Test @DisplayName("Should handle case when no structs match search") void testNoMatchingStructs() { // When search term matches no structs, count should be 0 String searchTerm = "NonexistentStructXYZ123"; int expectedCount = 0; assertEquals(0, expectedCount, "Count should be 0 when no structs match"); } /** * Test search with namespace-like struct names */ @Test @DisplayName("Should search structs with namespace-style names") void testNamespaceStyleStructNames() { // Struct names might contain namespace-like patterns String[] structNames = { "Network::Socket", "File::Descriptor", "Memory::Buffer" }; for (String structName : structNames) { assertNotNull(structName, "Struct name should not be null"); assertTrue(structName.contains("::"), "Namespace-style struct name should contain '::'"); } // Search for "Socket" should match "Network::Socket" String searchTerm = "Socket"; String fullName = "Network::Socket"; assertTrue(fullName.toLowerCase().contains(searchTerm.toLowerCase()), "Should match structs with namespace prefixes"); } /** * Test that category path filter is independent of search */ @Test @DisplayName("Should apply category_path and search filters independently") void testIndependentFiltering() { // Both filters should be applied as AND condition String categoryPath = "/Networking"; String structPath = "/Networking/File"; String structName = "FileDescriptor"; String searchTerm = "Descriptor"; // Category filter: struct path should start with category path assertTrue(structPath.startsWith(categoryPath), "Category filter should match path prefix"); // Search filter: struct name should contain search term assertTrue(structName.toLowerCase().contains(searchTerm.toLowerCase()), "Search filter should match struct name"); // Both conditions must be true for struct to be included boolean shouldInclude = structPath.startsWith(categoryPath) && structName.toLowerCase().contains(searchTerm.toLowerCase()); assertTrue(shouldInclude, "Both filters should be satisfied for inclusion"); } /** * Test empty category path behavior */ @Test @DisplayName("Should handle null/empty category_path correctly") void testNullEmptyCategoryPath() { // null category path should not filter String nullCategory = null; boolean shouldFilterNullCategory = (nullCategory != null && !nullCategory.isEmpty()); assertFalse(shouldFilterNullCategory, "Null category path should not filter results"); // empty category path should not filter String emptyCategory = ""; boolean shouldFilterEmptyCategory = (emptyCategory != null && !emptyCategory.isEmpty()); assertFalse(shouldFilterEmptyCategory, "Empty category path should not filter results"); } /** * Test pointer type parsing for basic types */ @ParameterizedTest @DisplayName("Should parse basic pointer types correctly") @ValueSource(strings = {"int *", "char *", "void *", "long *", "short *"}) void testBasicPointerTypeParsing(String pointerType) { // Pointer types should be recognized by the * character assertTrue(pointerType.contains("*"), "Pointer type should contain asterisk"); // Extract base type String[] parts = pointerType.split("\\*"); assertTrue(parts.length >= 1, "Should have base type before asterisk"); String baseType = parts[0].trim(); assertFalse(baseType.isEmpty(), "Base type should not be empty"); } /** * Test struct pointer type parsing */ @ParameterizedTest @DisplayName("Should parse struct pointer types correctly") @ValueSource(strings = {"MemoryPoolBlock *", "FileDescriptor *", "MyStruct *"}) void testStructPointerTypeParsing(String pointerType) { // Struct pointer types should also be recognized assertTrue(pointerType.contains("*"), "Struct pointer type should contain asterisk"); String[] parts = pointerType.split("\\*"); assertTrue(parts.length >= 1, "Should have base type before asterisk"); String baseType = parts[0].trim(); assertFalse(baseType.isEmpty(), "Base struct type should not be empty"); // Struct names typically start with uppercase assertTrue(Character.isUpperCase(baseType.charAt(0)), "Struct names typically start with uppercase"); } /** * Test pointer types with explicit sizes */ @ParameterizedTest @DisplayName("Should parse pointer types with explicit sizes") @ValueSource(strings = {"int *32", "char *16", "void *64"}) void testPointerTypesWithExplicitSize(String pointerType) { // Pointer types can specify size in bits assertTrue(pointerType.contains("*"), "Pointer type should contain asterisk"); String[] parts = pointerType.split("\\*"); assertEquals(2, parts.length, "Should have base type and size"); String baseType = parts[0].trim(); String sizeStr = parts[1].trim(); assertFalse(baseType.isEmpty(), "Base type should not be empty"); assertFalse(sizeStr.isEmpty(), "Size should not be empty"); assertTrue(sizeStr.matches("\\d+"), "Size should be numeric"); } /** * Test that pointer types are not converted to int */ @Test @DisplayName("Should preserve pointer types and not convert to int") void testPointerTypesNotConvertedToInt() { // This test documents the bug fix: pointers should NOT become "int" String pointerType = "MemoryPoolBlock *"; String incorrectType = "int"; // Pointer types should maintain their pointer nature assertTrue(pointerType.contains("*"), "Pointer type should be preserved with asterisk"); assertNotEquals(incorrectType, pointerType, "Pointer type should not be converted to int"); } /** * Test array type parsing */ @ParameterizedTest @DisplayName("Should parse array types correctly") @ValueSource(strings = {"int[10]", "char[256]", "byte[20]"}) void testArrayTypeParsing(String arrayType) { // Array types should contain brackets assertTrue(arrayType.contains("[") && arrayType.contains("]"), "Array type should contain brackets"); int openBracket = arrayType.indexOf('['); int closeBracket = arrayType.indexOf(']'); assertTrue(openBracket > 0, "Should have type before bracket"); assertTrue(closeBracket > openBracket, "Close bracket after open bracket"); String baseType = arrayType.substring(0, openBracket).trim(); String sizeStr = arrayType.substring(openBracket + 1, closeBracket).trim(); assertFalse(baseType.isEmpty(), "Base type should not be empty"); assertFalse(sizeStr.isEmpty(), "Array size should not be empty"); assertTrue(sizeStr.matches("\\d+"), "Array size should be numeric"); } /** * Test multi-dimensional array parsing */ @Test @DisplayName("Should parse multi-dimensional arrays correctly") void testMultiDimensionalArrays() { String arrayType = "int[10][20]"; assertTrue(arrayType.contains("["), "Should contain brackets"); // Count dimensions int dimensionCount = 0; for (char c : arrayType.toCharArray()) { if (c == '[') dimensionCount++; } assertEquals(2, dimensionCount, "Should have 2 dimensions"); } // ==================== POINTER TYPE RESOLUTION TESTS ==================== /** * Test pointer types WITHOUT spaces (the bug fix case). * Previously, "void*" would fail to parse because split("\\*") returns ["void"] * instead of ["void", ""]. */ @ParameterizedTest @DisplayName("Should parse pointer types without spaces correctly") @ValueSource(strings = {"void*", "int*", "char*", "long*", "short*", "float*", "double*"}) void testPointerTypesWithoutSpaces(String pointerType) { assertTrue(pointerType.contains("*"), "Should contain asterisk"); // Count pointer levels int pointerLevels = 0; for (char c : pointerType.toCharArray()) { if (c == '*') pointerLevels++; } assertEquals(1, pointerLevels, "Should have exactly 1 pointer level"); // Extract base type String baseTypeName = pointerType.substring(0, pointerType.indexOf('*')).trim(); assertFalse(baseTypeName.isEmpty(), "Base type should not be empty"); // After the asterisk should be empty String afterStar = pointerType.substring(pointerType.lastIndexOf('*') + 1).trim(); assertTrue(afterStar.isEmpty(), "No size specification for regular pointer"); } /** * Test double pointer types (e.g., void**, int**) */ @ParameterizedTest @DisplayName("Should parse double pointer types correctly") @ValueSource(strings = {"void**", "int**", "char**", "void **", "int **"}) void testDoublePointerTypes(String pointerType) { assertTrue(pointerType.contains("*"), "Should contain asterisk"); // Count pointer levels int pointerLevels = 0; for (char c : pointerType.toCharArray()) { if (c == '*') pointerLevels++; } assertEquals(2, pointerLevels, "Should have exactly 2 pointer levels"); // Extract base type String baseTypeName = pointerType.substring(0, pointerType.indexOf('*')).trim(); assertFalse(baseTypeName.isEmpty(), "Base type should not be empty"); } /** * Test triple pointer types (rare but valid) */ @ParameterizedTest @DisplayName("Should parse triple pointer types correctly") @ValueSource(strings = {"void***", "char***"}) void testTriplePointerTypes(String pointerType) { int pointerLevels = 0; for (char c : pointerType.toCharArray()) { if (c == '*') pointerLevels++; } assertEquals(3, pointerLevels, "Should have exactly 3 pointer levels"); } /** * Test pointer to custom/struct types */ @ParameterizedTest @DisplayName("Should parse pointers to custom types correctly") @ValueSource(strings = {"MyStruct*", "CustomType*", "FILE_DESCRIPTOR*", "namespace::Class*"}) void testPointerToCustomTypes(String pointerType) { assertTrue(pointerType.contains("*"), "Should contain asterisk"); String baseTypeName = pointerType.substring(0, pointerType.indexOf('*')).trim(); assertFalse(baseTypeName.isEmpty(), "Custom base type should not be empty"); } /** * Test the new pointer parsing logic counts asterisks correctly */ @Test @DisplayName("Should count pointer levels correctly for various types") void testPointerLevelCounting() { // Test different pointer levels String[] testCases = {"void", "void*", "void**", "void***"}; int[] expectedLevels = {0, 1, 2, 3}; for (int i = 0; i < testCases.length; i++) { String typeName = testCases[i]; int expectedLevel = expectedLevels[i]; int pointerLevels = 0; for (char c : typeName.toCharArray()) { if (c == '*') pointerLevels++; } assertEquals(expectedLevel, pointerLevels, "Type '" + typeName + "' should have " + expectedLevel + " pointer levels"); } } /** * Test mixed spacing in pointer types */ @ParameterizedTest @DisplayName("Should handle various spacing in pointer types") @ValueSource(strings = {"int*", "int *", "int * ", " int*", " int * "}) void testMixedSpacingInPointerTypes(String pointerType) { assertTrue(pointerType.contains("*"), "Should contain asterisk"); // Extract base type with proper trimming String baseTypeName = pointerType.substring(0, pointerType.indexOf('*')).trim(); assertEquals("int", baseTypeName, "Base type should be 'int' after trimming"); // After asterisk should be empty for regular pointers String afterStar = pointerType.substring(pointerType.lastIndexOf('*') + 1).trim(); assertTrue(afterStar.isEmpty() || afterStar.matches("\\d+"), "After asterisk should be empty or numeric"); } /** * Test that the bug case (void* returning null) is fixed. * This documents the specific issue that was reported. */ @Test @DisplayName("Bug fix: void* should not return null during type resolution") void testVoidPointerBugFix() { String typeName = "void*"; // Using the new parsing logic: // 1. Count asterisks int pointerLevels = 0; for (char c : typeName.toCharArray()) { if (c == '*') pointerLevels++; } assertEquals(1, pointerLevels, "void* has 1 pointer level"); // 2. Extract base type String baseTypeName = typeName.substring(0, typeName.indexOf('*')).trim(); assertEquals("void", baseTypeName, "Base type should be 'void'"); // 3. Check what's after the asterisk String afterLastStar = typeName.substring(typeName.lastIndexOf('*') + 1).trim(); assertTrue(afterLastStar.isEmpty(), "Regular pointer has empty afterStar"); // The old buggy code used split("\\*") which returns ["void"] for "void*" // because Java's split() discards trailing empty strings by default String[] oldBuggySplit = typeName.split("\\*"); assertEquals(1, oldBuggySplit.length, "Old split returned only 1 element (the bug)"); // The fix doesn't rely on split(), so it works correctly } /** * Test that int* is correctly identified as pointer type */ @Test @DisplayName("Bug fix: int* should be recognized as pointer type") void testIntPointerBugFix() { String typeName = "int*"; // Verify contains asterisk assertTrue(typeName.contains("*"), "Should contain asterisk"); // Extract base type String baseTypeName = typeName.substring(0, typeName.indexOf('*')).trim(); assertEquals("int", baseTypeName, "Base type should be 'int'"); // Verify this is a regular pointer (not far pointer with size) String afterStar = typeName.substring(typeName.lastIndexOf('*') + 1).trim(); assertTrue(afterStar.isEmpty(), "Regular pointer should have empty afterStar"); } /** * Test that char* string pointers are handled */ @Test @DisplayName("Bug fix: char* should be recognized as pointer type") void testCharPointerBugFix() { String typeName = "char*"; assertTrue(typeName.contains("*"), "Should contain asterisk"); String baseTypeName = typeName.substring(0, typeName.indexOf('*')).trim(); assertEquals("char", baseTypeName, "Base type should be 'char'"); } /** * Test CustomStruct* pointer types */ @Test @DisplayName("Bug fix: CustomStruct* should be recognized as pointer type") void testCustomStructPointerBugFix() { String typeName = "CustomStruct*"; assertTrue(typeName.contains("*"), "Should contain asterisk"); String baseTypeName = typeName.substring(0, typeName.indexOf('*')).trim(); assertEquals("CustomStruct", baseTypeName, "Base type should be 'CustomStruct'"); String afterStar = typeName.substring(typeName.lastIndexOf('*') + 1).trim(); assertTrue(afterStar.isEmpty(), "Regular pointer should have empty afterStar"); } }

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