Skip to main content
Glama
crash-analysis.integration.test.ts11.5 kB
/** * iOS Crash Analysis Integration Tests * Tests crash log parsing and pattern detection * * Prerequisites: * - macOS with Xcode installed * - iOS Simulator available * - SpecterCounter app can be crashed via Debug tab */ import { describe, it, expect, beforeAll } from 'vitest'; import { executeShell } from '../../../src/utils/shell.js'; import { parseIPSFormat, parseClassicFormat, parseCrashLog } from '../../../src/platforms/ios/crash-parser.js'; import { findDSYMInCommonLocations } from '../../../src/platforms/ios/symbolicate.js'; import { detectCrashPatterns, CrashPattern, CrashReport } from '../../../src/models/crash-report.js'; import { analyzePatterns } from '../../../src/tools/crash/pattern-detector.js'; import * as path from 'path'; import * as os from 'os'; import * as fs from 'fs'; const BUNDLE_ID = 'com.specter.counter'; async function getBootedDeviceId(): Promise<string | null> { try { const result = await executeShell('xcrun', ['simctl', 'list', 'devices']); const bootedMatch = result.stdout.match(/([A-F0-9-]{36})\) \(Booted\)/); return bootedMatch ? bootedMatch[1] : null; } catch { return null; } } async function getCrashLogDirectory(_deviceId: string): Promise<string> { // Simulator crash logs location return path.join( os.homedir(), 'Library/Logs/DiagnosticReports' ); } async function findLatestCrashLog(directory: string, _bundleId: string): Promise<string | null> { try { const files = fs.readdirSync(directory); const crashFiles = files .filter(f => (f.endsWith('.ips') || f.endsWith('.crash')) && f.includes('SpecterCounter')) .sort() .reverse(); return crashFiles.length > 0 ? path.join(directory, crashFiles[0]) : null; } catch { return null; } } describe('iOS Crash Analysis Integration', () => { let deviceId: string | null = null; let crashLogDir: string | null = null; beforeAll(async () => { if (os.platform() !== 'darwin') { console.log('Skipping iOS crash tests on non-macOS platform'); return; } deviceId = await getBootedDeviceId(); if (deviceId) { crashLogDir = await getCrashLogDirectory(deviceId); } console.log(`Device ID: ${deviceId}`); console.log(`Crash log directory: ${crashLogDir}`); }); describe('parseIPSFormat', () => { it('should parse IPS format crash log', () => { const ipsContent = ` { "app_name": "SpecterCounter", "app_version": "1.0", "bundleID": "com.specter.counter", "exception": { "type": "EXC_BAD_ACCESS", "subtype": "KERN_INVALID_ADDRESS at 0x0000000000000000", "codes": "0x0000000000000001, 0x0000000000000000" }, "termination": { "reason": "Namespace SIGNAL, Code 11 Segmentation fault: 11" }, "threads": [ { "id": 0, "frames": [ { "imageIndex": 0, "imageOffset": 123456, "symbol": "ContentView.buttonTapped(_:)" } ], "triggered": true } ], "usedImages": [ { "source": "P", "arch": "arm64", "base": 4294967296, "name": "SpecterCounter", "path": "/path/to/SpecterCounter.app/SpecterCounter", "uuid": "12345678-1234-1234-1234-123456789012" } ] } `; const report = parseIPSFormat(ipsContent); expect(report).toBeDefined(); expect(report.bundleId).toBe('com.specter.counter'); expect(report.exception.type).toBe('EXC_BAD_ACCESS'); }); it('should handle malformed IPS content gracefully', () => { const invalidContent = 'not valid json'; // Should throw or return a report with default values try { const report = parseIPSFormat(invalidContent); // If it doesn't throw, check for sensible defaults expect(report.exception).toBeDefined(); } catch (error) { // Expected behavior for invalid JSON expect(error).toBeDefined(); } }); }); describe('parseClassicFormat', () => { it('should parse legacy crash format', () => { const crashContent = ` Incident Identifier: 12345678-1234-1234-1234-123456789012 CrashReporter Key: abcdef123456 Hardware Model: iPhone15,2 Process: SpecterCounter [12345] Path: /private/var/containers/Bundle/Application/.../SpecterCounter.app/SpecterCounter Identifier: com.specter.counter Version: 1.0 (1) Code Type: ARM-64 Exception Type: EXC_BAD_ACCESS (SIGSEGV) Exception Subtype: KERN_INVALID_ADDRESS at 0x0000000000000000 Termination Reason: SIGNAL 11 Segmentation fault: 11 Thread 0 Crashed: 0 SpecterCounter 0x0000000100001234 ContentView.buttonTapped(_:) + 100 1 SpecterCounter 0x0000000100001334 specialized closure #1 in ContentView.body.getter + 200 2 SwiftUI 0x00000001a1234567 partial apply + 300 `; const report = parseClassicFormat(crashContent); expect(report).toBeDefined(); expect(report.bundleId).toBe('com.specter.counter'); expect(report.exception.type).toContain('EXC_BAD_ACCESS'); expect(report.crashedThread).toBeDefined(); expect(report.crashedThread.frames.length).toBeGreaterThan(0); }); }); describe('detectCrashPatterns', () => { it('should detect null pointer crash pattern', () => { // Create a minimal CrashReport for testing const report: CrashReport = { timestamp: new Date(), platform: 'ios', processName: 'SpecterCounter', bundleId: 'com.specter.counter', exception: { type: 'EXC_BAD_ACCESS', signal: 'SIGSEGV', codes: 'KERN_INVALID_ADDRESS at 0x0000000000000000', faultAddress: '0x0000000000000000', }, threads: [], crashedThread: { id: 0, crashed: true, frames: [ { index: 0, address: '0x1234', symbol: 'buttonTapped', imageName: 'SpecterCounter', offset: '100' } ], }, binaryImages: [], isSymbolicated: false, }; const patterns = detectCrashPatterns(report); expect(patterns).toBeDefined(); expect(patterns.length).toBeGreaterThan(0); // Should detect the null pointer pattern const nullPattern = patterns.find(p => p.id === 'exc_bad_access_null' || p.id === 'exc_bad_access_kern_invalid'); expect(nullPattern).toBeDefined(); expect(nullPattern?.suggestion).toBeDefined(); }); it('should detect assertion failure pattern', () => { const report: CrashReport = { timestamp: new Date(), platform: 'ios', processName: 'SpecterCounter', exception: { type: 'SIGABRT', signal: 'SIGABRT', }, threads: [], crashedThread: { id: 0, crashed: true, frames: [ { index: 0, address: '0x1234', symbol: 'assertionFailure', imageName: 'Swift', offset: '100' }, { index: 1, address: '0x5678', symbol: 'ContentView.validate', imageName: 'SpecterCounter', offset: '50' } ], }, binaryImages: [], isSymbolicated: false, }; const patterns = detectCrashPatterns(report); expect(patterns).toBeDefined(); // May or may not find a pattern depending on exact matcher logic if (patterns.length > 0) { const assertPattern = patterns.find(p => p.id === 'sigabrt_assertion'); if (assertPattern) { expect(assertPattern.severity).toBe('high'); } } }); it('should handle empty crash report gracefully', () => { const report: CrashReport = { timestamp: new Date(), platform: 'ios', processName: 'Unknown', exception: { type: 'UNKNOWN', }, threads: [], crashedThread: { id: 0, crashed: true, frames: [], }, binaryImages: [], isSymbolicated: false, }; const patterns = detectCrashPatterns(report); // Should return empty array, not throw expect(patterns).toBeDefined(); expect(Array.isArray(patterns)).toBe(true); }); }); describe('analyzePatterns', () => { it('should provide pattern analysis with category', () => { const report: CrashReport = { timestamp: new Date(), platform: 'ios', processName: 'SpecterCounter', exception: { type: 'EXC_BAD_ACCESS', signal: 'SIGSEGV', faultAddress: '0x0000000000000000', }, threads: [], crashedThread: { id: 0, crashed: true, frames: [ { index: 0, address: '0x1234', symbol: 'testFunction', imageName: 'SpecterCounter', offset: '100' } ], }, binaryImages: [], isSymbolicated: false, }; const analysis = analyzePatterns(report); expect(analysis).toBeDefined(); expect(analysis.category).toBeDefined(); expect(analysis.suggestions).toBeDefined(); expect(analysis.severity).toBeDefined(); }); }); describe('findDSYMInCommonLocations', () => { it('should search common dSYM locations', () => { expect(os.platform(), 'This test requires macOS').toBe('darwin'); // This may or may not find a dSYM depending on build state const dsymPath = findDSYMInCommonLocations(BUNDLE_ID, '12345678-1234-1234-1234-123456789012'); console.log(`dSYM path found: ${dsymPath}`); // Just verify it doesn't throw expect(true).toBe(true); }); it('should return undefined for non-existent bundle', () => { expect(os.platform(), 'This test requires macOS').toBe('darwin'); const dsymPath = findDSYMInCommonLocations('com.nonexistent.app', 'fake-uuid'); expect(dsymPath).toBeUndefined(); }); }); describe('Real crash log analysis', () => { it('should analyze real crash log if available', async () => { expect(os.platform(), 'This test requires macOS').toBe('darwin'); expect(crashLogDir, 'Crash log directory not available').toBeTruthy(); const crashFile = await findLatestCrashLog(crashLogDir, BUNDLE_ID); if (!crashFile) { console.log('No crash logs found for SpecterCounter'); return; } console.log(`Analyzing crash log: ${crashFile}`); const content = fs.readFileSync(crashFile, 'utf-8'); let report: CrashReport; if (crashFile.endsWith('.ips')) { report = parseIPSFormat(content); } else { report = parseClassicFormat(content); } if (report) { console.log(`Exception type: ${report.exception.type}`); console.log(`Process: ${report.processName}`); console.log(`Stack frames: ${report.crashedThread?.frames?.length ?? 0}`); const patterns = detectCrashPatterns(report); if (patterns.length > 0) { console.log(`Detected ${patterns.length} patterns:`); for (const pattern of patterns) { console.log(` - ${pattern.name}: ${pattern.description}`); console.log(` Suggestion: ${pattern.suggestion}`); } } const analysis = analyzePatterns(report); console.log(`Category: ${analysis.category}`); console.log(`Severity: ${analysis.severity}`); console.log(`Suggestions: ${analysis.suggestions.join(', ')}`); } expect(true).toBe(true); }); }); });

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/abd3lraouf/specter-mcp'

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