Skip to main content
Glama
hierarchy.ts14.5 kB
import { ZebrunnerTestSuite, ZebrunnerTestCase } from "../types/core.js"; /** * Utility class for processing test suite hierarchies * Enhanced with comprehensive Java methodology from Zebrunner_MCP_API.md */ export class HierarchyProcessor { /** * Enrich suites with hierarchy information (alias for backward compatibility) */ static enrichSuitesWithHierarchy(suites: ZebrunnerTestSuite[]): ZebrunnerTestSuite[] { const rootSuiteMap = this.calculateRootSuiteIds(suites); const levelMap = this.calculateSuiteLevels(suites); return suites.map(suite => ({ ...suite, rootSuiteId: rootSuiteMap.get(suite.id) || suite.id, level: levelMap.get(suite.id) || 0, path: this.generateSuitePath(suite.id, suites) })); } /** * Build a hierarchical tree from flat suite list */ static buildSuiteTree(suites: ZebrunnerTestSuite[]): ZebrunnerTestSuite[] { const suiteMap = new Map<number, ZebrunnerTestSuite>(); const rootSuites: ZebrunnerTestSuite[] = []; // First pass: create map and initialize children arrays suites.forEach(suite => { suiteMap.set(suite.id, { ...suite, children: [] }); }); // Second pass: build parent-child relationships suites.forEach(suite => { const suiteWithChildren = suiteMap.get(suite.id)!; // Handle self-referencing suites (treat them as root suites) if (suite.parentSuiteId === suite.id) { console.error(`⚠️ Self-referencing suite detected: ${suite.id} (${suite.title || suite.name})`); // Set parentSuiteId to null for self-referencing suites suiteWithChildren.parentSuiteId = null; rootSuites.push(suiteWithChildren); } else if (suite.parentSuiteId && suiteMap.has(suite.parentSuiteId)) { const parent = suiteMap.get(suite.parentSuiteId)!; parent.children = parent.children || []; parent.children.push(suiteWithChildren); } else { rootSuites.push(suiteWithChildren); } }); return rootSuites; } /** * Calculate root suite IDs for all suites */ static calculateRootSuiteIds(suites: ZebrunnerTestSuite[]): Map<number, number> { const rootSuiteMap = new Map<number, number>(); const suiteMap = new Map<number, ZebrunnerTestSuite>(); // Build suite map suites.forEach(suite => { suiteMap.set(suite.id, suite); }); // Calculate root suite for each suite with circular reference protection const findRootSuite = (suiteId: number, visited: Set<number> = new Set()): number => { if (rootSuiteMap.has(suiteId)) { return rootSuiteMap.get(suiteId)!; } // Circular reference detection if (visited.has(suiteId)) { rootSuiteMap.set(suiteId, suiteId); return suiteId; } const suite = suiteMap.get(suiteId); if (!suite || !suite.parentSuiteId || !suiteMap.has(suite.parentSuiteId)) { rootSuiteMap.set(suiteId, suiteId); return suiteId; } visited.add(suiteId); const rootId = findRootSuite(suite.parentSuiteId, visited); rootSuiteMap.set(suiteId, rootId); return rootId; }; suites.forEach(suite => { findRootSuite(suite.id); }); return rootSuiteMap; } /** * Generate full path for a suite (e.g., "Root > Parent > Child") */ static generateSuitePath( suiteId: number, suites: ZebrunnerTestSuite[], separator: string = " > " ): string { const suiteMap = new Map<number, ZebrunnerTestSuite>(); suites.forEach(suite => { suiteMap.set(suite.id, suite); }); const buildPath = (id: number, visited: Set<number> = new Set()): string[] => { const suite = suiteMap.get(id); if (!suite) return [`Unknown Suite (${id})`]; // Circular reference detection if (visited.has(id)) { return [`Suite ${id} (circular)`]; } const name = suite.title || suite.name || `Suite ${suite.id}`; if (!suite.parentSuiteId) { return [name]; } visited.add(id); const parentPath = buildPath(suite.parentSuiteId, visited); return [...parentPath, name]; }; return buildPath(suiteId).join(separator); } /** * Calculate depth level for each suite */ static calculateSuiteLevels(suites: ZebrunnerTestSuite[]): Map<number, number> { const levelMap = new Map<number, number>(); const suiteMap = new Map<number, ZebrunnerTestSuite>(); suites.forEach(suite => { suiteMap.set(suite.id, suite); }); const calculateLevel = (suiteId: number, visited: Set<number> = new Set()): number => { if (levelMap.has(suiteId)) { return levelMap.get(suiteId)!; } // Circular reference detection if (visited.has(suiteId)) { levelMap.set(suiteId, 0); // Treat circular references as root level return 0; } const suite = suiteMap.get(suiteId); if (!suite || !suite.parentSuiteId || !suiteMap.has(suite.parentSuiteId)) { levelMap.set(suiteId, 0); return 0; } visited.add(suiteId); const level = calculateLevel(suite.parentSuiteId, visited) + 1; levelMap.set(suiteId, level); return level; }; suites.forEach(suite => { calculateLevel(suite.id); }); return levelMap; } /** * Flatten hierarchical tree back to list */ static flattenSuiteTree(rootSuites: ZebrunnerTestSuite[]): ZebrunnerTestSuite[] { const flattened: ZebrunnerTestSuite[] = []; const visited = new Set<number>(); // Prevent infinite loops from circular references const traverse = (suite: ZebrunnerTestSuite) => { if (visited.has(suite.id)) { console.error(`⚠️ Circular reference detected in suite hierarchy: ${suite.id}`); return; } visited.add(suite.id); flattened.push(suite); if (suite.children && Array.isArray(suite.children)) { suite.children.forEach((child: ZebrunnerTestSuite) => { if (child && typeof child.id === 'number') { traverse(child); } }); } visited.delete(suite.id); // Allow revisiting in different branches }; rootSuites.forEach(suite => { if (suite && typeof suite.id === 'number') { traverse(suite); } }); return flattened; } /** * Get all descendants of a suite */ static getSuiteDescendants( parentSuiteId: number, suites: ZebrunnerTestSuite[] ): ZebrunnerTestSuite[] { if (!Number.isInteger(parentSuiteId) || parentSuiteId <= 0) { throw new Error('Parent suite ID must be a positive integer'); } const descendants: ZebrunnerTestSuite[] = []; const visited = new Set<number>(); // Prevent infinite recursion const collectDescendants = (currentParentId: number) => { if (visited.has(currentParentId)) { return; // Avoid circular references } visited.add(currentParentId); const children = suites.filter((suite: ZebrunnerTestSuite) => suite && suite.parentSuiteId === currentParentId ); children.forEach((child: ZebrunnerTestSuite) => { if (child && typeof child.id === 'number') { descendants.push(child); collectDescendants(child.id); } }); visited.delete(currentParentId); }; collectDescendants(parentSuiteId); return descendants; } /** * Get path from root to specific suite */ static getSuiteAncestors( suiteId: number, suites: ZebrunnerTestSuite[] ): ZebrunnerTestSuite[] { const suiteMap = new Map<number, ZebrunnerTestSuite>(); suites.forEach(suite => { suiteMap.set(suite.id, suite); }); const ancestors: ZebrunnerTestSuite[] = []; const visited = new Set<number>(); // Prevent infinite loops let currentSuite = suiteMap.get(suiteId); // Skip the suite itself, start with its parent if (currentSuite && currentSuite.parentSuiteId) { currentSuite = suiteMap.get(currentSuite.parentSuiteId); } else { currentSuite = undefined; } while (currentSuite && !visited.has(currentSuite.id)) { visited.add(currentSuite.id); ancestors.unshift(currentSuite); currentSuite = currentSuite.parentSuiteId ? suiteMap.get(currentSuite.parentSuiteId) : undefined; } return ancestors; } // ===== COMPREHENSIVE JAVA METHODOLOGY METHODS ===== // Based on Zebrunner_MCP_API.md implementation guide /** * Builds a parent-child mapping for efficient hierarchy traversal * Equivalent to: TCMTestSuites.buildParentChildMap(List<TCMTestSuite> list) */ static buildParentChildMap(suites: ZebrunnerTestSuite[]): Map<number, number> { const parentChildMap = new Map<number, number>(); for (const suite of suites) { if (suite.parentSuiteId !== null && suite.parentSuiteId !== undefined) { parentChildMap.set(suite.id, suite.parentSuiteId); } } return parentChildMap; } /** * Finds root suite ID by traversing up the hierarchy * Equivalent to: TCMTestSuites.getRoot(Map<Integer, Integer> parentChildMap, int id) */ static getRoot(parentChildMap: Map<number, number>, id: number): number { let currentId = id; const visited = new Set<number>(); // Prevent infinite loops while (parentChildMap.has(currentId) && !visited.has(currentId)) { visited.add(currentId); currentId = parentChildMap.get(currentId)!; } return currentId; } /** * Convenience method to get root ID directly from suite list * Equivalent to: TCMTestSuites.getRootId(List<TCMTestSuite> list, int idToFindRootFor) */ static getRootId(suites: ZebrunnerTestSuite[], idToFindRootFor: number): number { const parentChildMap = this.buildParentChildMap(suites); return this.getRoot(parentChildMap, idToFindRootFor); } /** * Finds suite name by ID * Equivalent to: TCMTestSuites.getSuiteNameById(List<TCMTestSuite> itemList, Integer id) */ static getSuiteNameById(suites: ZebrunnerTestSuite[], id: number): string { const suite = suites.find(s => s.id === id); return suite?.name || suite?.title || ''; } /** * Finds suite object by ID * Equivalent to: TCMTestSuites.getTCMTestSuiteById(List<TCMTestSuite> itemList, Integer id) */ static getTCMTestSuiteById(suites: ZebrunnerTestSuite[], id: number): ZebrunnerTestSuite | null { return suites.find(s => s.id === id) || null; } /** * Filters test suites to return only root suites (parentSuiteId === null) * Equivalent to: getRootSuites(List<TCMTestSuite> list) */ static getRootSuites(suites: ZebrunnerTestSuite[]): ZebrunnerTestSuite[] { return suites.filter(suite => suite.parentSuiteId === null || suite.parentSuiteId === undefined); } /** * Sets root parent information for all suites in the list * Equivalent to: TCMTestSuites.setRootParentsToSuites(List<TCMTestSuite> itemList) */ static setRootParentsToSuites(suites: ZebrunnerTestSuite[]): ZebrunnerTestSuite[] { const processedSuites: ZebrunnerTestSuite[] = []; for (const suite of suites) { const rootId = this.getRootId(suites, suite.id); const enhancedSuite = { ...suite }; enhancedSuite.rootSuiteId = rootId; enhancedSuite.rootSuiteName = this.getSuiteNameById(suites, rootId); if (suite.parentSuiteId !== null && suite.parentSuiteId !== undefined) { enhancedSuite.parentSuiteName = this.getSuiteNameById(suites, suite.parentSuiteId); } enhancedSuite.treeNames = this.getSectionTree(suites, enhancedSuite); processedSuites.push(enhancedSuite); } return this.updateSuitesSectionsTree(processedSuites); } /** * Helper function to get root ID for a specific suite ID * Equivalent to: getRootIdBySuiteId(List<TCMTestSuite> allSuites, int id) */ static getRootIdBySuiteId(allSuites: ZebrunnerTestSuite[], id: number): number { const suite = allSuites.find(s => s.id === id); if (suite?.rootSuiteId) { return suite.rootSuiteId; } // If rootSuiteId is not set, calculate it return this.getRootId(allSuites, id); } /** * Generates basic section tree path */ private static getSectionTree(suites: ZebrunnerTestSuite[], suite: ZebrunnerTestSuite): string { const rootName = suite.rootSuiteName || this.getSuiteNameById(suites, suite.rootSuiteId || suite.id); const parentName = suite.parentSuiteName || (suite.parentSuiteId ? this.getSuiteNameById(suites, suite.parentSuiteId) : null); const suiteName = suite.name || suite.title || ''; if (!rootName) { return suiteName; } if (!parentName || rootName === parentName) { return `${rootName} > ${suiteName}`; } else { return `${rootName} > .. > ${parentName} > ${suiteName}`; } } /** * Updates complete section tree for all suites */ private static updateSuitesSectionsTree(suites: ZebrunnerTestSuite[]): ZebrunnerTestSuite[] { const processedSuites: ZebrunnerTestSuite[] = []; for (const suite of suites) { const enhancedSuite = { ...suite }; let treePath = suite.rootSuiteName || ''; const pathParts: string[] = []; if (suite.parentSuiteId !== null && suite.parentSuiteId !== undefined) { let currentSuite = suite; // Build path by traversing up the hierarchy const visited = new Set<number>(); // Prevent infinite loops while (currentSuite.parentSuiteId !== null && currentSuite.parentSuiteId !== undefined && currentSuite.parentSuiteId !== currentSuite.rootSuiteId && !visited.has(currentSuite.parentSuiteId)) { visited.add(currentSuite.parentSuiteId); if (currentSuite.parentSuiteName) { pathParts.push(currentSuite.parentSuiteName); } const parentSuite = this.getTCMTestSuiteById(suites, currentSuite.parentSuiteId); if (!parentSuite) break; currentSuite = parentSuite; } // Reverse to get correct order (root to leaf) pathParts.reverse(); for (const part of pathParts) { treePath += ` > ${part}`; } } treePath += ` > ${suite.name || suite.title || ''}`; enhancedSuite.treeNames = treePath; processedSuites.push(enhancedSuite); } console.error(`Updated tree in suites: ${processedSuites.length}`); return processedSuites; } }

Implementation Reference

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/maksimsarychau/mcp-zebrunner'

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