diff --git a/packages/core/scripts/sentry-xcode-debug-files.sh b/packages/core/scripts/sentry-xcode-debug-files.sh index 2200d1e515..5010cb5afc 100755 --- a/packages/core/scripts/sentry-xcode-debug-files.sh +++ b/packages/core/scripts/sentry-xcode-debug-files.sh @@ -58,6 +58,139 @@ EXTRA_ARGS="$SENTRY_CLI_EXTRA_ARGS $SENTRY_CLI_DEBUG_FILES_UPLOAD_EXTRA_ARGS $IN UPLOAD_DEBUG_FILES="\"$SENTRY_CLI_EXECUTABLE\" debug-files upload $EXTRA_ARGS \"$DWARF_DSYM_FOLDER_PATH\"" +# Function to wait for dSYM files to be generated +# This addresses a race condition where the upload script runs before dSYM generation completes +wait_for_dsym_files() { + local max_attempts="${SENTRY_DSYM_WAIT_MAX_ATTEMPTS:-10}" + local wait_interval="${SENTRY_DSYM_WAIT_INTERVAL:-2}" + local attempt=1 + + # Check if we should wait for dSYM files + if [ "$SENTRY_DSYM_WAIT_ENABLED" == "false" ]; then + echo "SENTRY_DSYM_WAIT_ENABLED=false, skipping dSYM wait check" + return 0 + fi + + # Warn if DWARF_DSYM_FILE_NAME is not set - we can't verify the main app dSYM + if [ -z "$DWARF_DSYM_FILE_NAME" ]; then + echo "warning: DWARF_DSYM_FILE_NAME not set, cannot verify main app dSYM specifically" + echo "warning: Will proceed when any dSYM bundle is found" + fi + + echo "Checking for dSYM files in: $DWARF_DSYM_FOLDER_PATH" + + # Debug information to help diagnose issues + if [ -n "${SENTRY_DSYM_DEBUG}" ]; then + echo "DEBUG: DWARF_DSYM_FOLDER_PATH=$DWARF_DSYM_FOLDER_PATH" + echo "DEBUG: DWARF_DSYM_FILE_NAME=$DWARF_DSYM_FILE_NAME" + echo "DEBUG: PRODUCT_NAME=$PRODUCT_NAME" + if [ -d "$DWARF_DSYM_FOLDER_PATH" ]; then + echo "DEBUG: Contents of dSYM folder:" + ls -la "$DWARF_DSYM_FOLDER_PATH" 2>/dev/null || echo "Cannot list folder" + else + echo "DEBUG: dSYM folder does not exist yet" + fi + fi + + while [ $attempt -le $max_attempts ]; do + # Check if the dSYM folder exists + if [ -d "$DWARF_DSYM_FOLDER_PATH" ]; then + # Check if there are any .dSYM bundles in the folder + local dsym_count=$(find "$DWARF_DSYM_FOLDER_PATH" -name "*.dSYM" -type d 2>/dev/null | wc -l | tr -d ' ') + + if [ "$dsym_count" -gt 0 ]; then + echo "Found $dsym_count dSYM bundle(s) in $DWARF_DSYM_FOLDER_PATH" + + # If DWARF_DSYM_FILE_NAME is set, verify the main app dSYM exists and is complete + if [ -n "$DWARF_DSYM_FILE_NAME" ]; then + local main_dsym="$DWARF_DSYM_FOLDER_PATH/$DWARF_DSYM_FILE_NAME" + + if [ -d "$main_dsym" ]; then + # Directory exists, now verify the actual DWARF binary exists inside + local dwarf_dir="$main_dsym/Contents/Resources/DWARF" + + if [ -d "$dwarf_dir" ]; then + # Check if there are any files in the DWARF directory + local dwarf_files=$(find "$dwarf_dir" -type f 2>/dev/null | head -1) + + if [ -n "$dwarf_files" ]; then + # Verify the DWARF file is not empty (still being written) + local dwarf_size=$(find "$dwarf_dir" -type f -size +0 2>/dev/null | head -1) + + if [ -n "$dwarf_size" ]; then + echo "Verified main app dSYM is complete: $DWARF_DSYM_FILE_NAME" + return 0 + else + echo "Main app dSYM DWARF binary is empty (still being written): $DWARF_DSYM_FILE_NAME (attempt $attempt/$max_attempts)" + fi + else + echo "Main app dSYM DWARF directory is empty: $DWARF_DSYM_FILE_NAME (attempt $attempt/$max_attempts)" + fi + else + echo "Main app dSYM structure incomplete (missing DWARF directory): $DWARF_DSYM_FILE_NAME (attempt $attempt/$max_attempts)" + fi + else + echo "Main app dSYM not found yet: $DWARF_DSYM_FILE_NAME (attempt $attempt/$max_attempts)" + fi + else + # DWARF_DSYM_FILE_NAME not set, check if any dSYM has valid DWARF content + # This is less strict but better than nothing + local has_valid_dsym=false + for dsym in "$DWARF_DSYM_FOLDER_PATH"/*.dSYM; do + if [ -d "$dsym/Contents/Resources/DWARF" ]; then + local dwarf_files=$(find "$dsym/Contents/Resources/DWARF" -type f -size +0 2>/dev/null | head -1) + if [ -n "$dwarf_files" ]; then + has_valid_dsym=true + break + fi + fi + done + + if [ "$has_valid_dsym" = true ]; then + echo "Found dSYM bundle(s) with valid DWARF content" + return 0 + else + echo "Found dSYM bundle(s) but none have complete DWARF content yet (attempt $attempt/$max_attempts)" + fi + fi + else + echo "No dSYM bundles found yet in $DWARF_DSYM_FOLDER_PATH (attempt $attempt/$max_attempts)" + fi + else + echo "dSYM folder does not exist yet: $DWARF_DSYM_FOLDER_PATH (attempt $attempt/$max_attempts)" + fi + + if [ $attempt -lt $max_attempts ]; then + # Progressive backoff: quick checks first, longer waits later + # Attempts 1-3: 0.5s (total 1.5s) + # Attempts 4-6: 1s (total 3s) + # Attempts 7+: 2s (remaining time) + local current_interval="$wait_interval" + if [ -z "${SENTRY_DSYM_WAIT_INTERVAL}" ]; then + # Only use progressive intervals if user hasn't set custom interval + if [ $attempt -le 3 ]; then + current_interval=0.5 + elif [ $attempt -le 6 ]; then + current_interval=1 + else + current_interval=2 + fi + fi + + echo "Waiting ${current_interval}s for dSYM generation to complete..." + sleep $current_interval + fi + + attempt=$((attempt + 1)) + done + + # Timeout reached + echo "warning: Timeout waiting for dSYM files after $((max_attempts * wait_interval))s" + echo "warning: This may result in incomplete debug symbol uploads" + echo "warning: To disable this check, set SENTRY_DSYM_WAIT_ENABLED=false" + return 1 +} + XCODE_BUILD_CONFIGURATION="${CONFIGURATION}" if [ "$SENTRY_DISABLE_AUTO_UPLOAD" == true ]; then @@ -67,6 +200,12 @@ elif [ "$SENTRY_DISABLE_XCODE_DEBUG_UPLOAD" == true ]; then elif echo "$XCODE_BUILD_CONFIGURATION" | grep -iq "debug"; then # case insensitive check for "debug" echo "Skipping debug files upload for *Debug* configuration" else + # Wait for dSYM files to be generated (addresses race condition in EAS builds) + # Don't fail the script if wait times out - we still want to attempt upload + set +e + wait_for_dsym_files + set -e + # 'warning:' triggers a warning in Xcode, 'error:' triggers an error set +x +e # disable printing commands otherwise we might print `error:` by accident and allow continuing on error SENTRY_UPLOAD_COMMAND_OUTPUT=$(/bin/sh -c "\"$LOCAL_NODE_BINARY\" $UPLOAD_DEBUG_FILES" 2>&1) diff --git a/packages/core/scripts/test-dsym-fix.sh b/packages/core/scripts/test-dsym-fix.sh new file mode 100755 index 0000000000..31d038f4ed --- /dev/null +++ b/packages/core/scripts/test-dsym-fix.sh @@ -0,0 +1,125 @@ +#!/bin/bash +# Test script to verify dSYM upload fix +# Usage: ./test-dsym-fix.sh [test-project-path] + +set -e + +PROJECT_PATH="${1:-.}" +SDK_PATH="$(cd "$(dirname "$0")/../../.." && pwd)" + +echo "=== Sentry React Native dSYM Fix Testing ===" +echo "" +echo "SDK Path: $SDK_PATH" +echo "Project Path: $PROJECT_PATH" +echo "" + +# Function to check Sentry debug files +check_sentry_debug_files() { + echo "=== Checking Sentry Debug Files ===" + echo "" + echo "Please check Sentry Debug Files manually:" + echo "1. Go to: https://sentry.io" + echo "2. Navigate to: Settings > Projects > [Your Project] > Debug Files" + echo "3. Look for recent uploads with 'debug' tag" + echo "" + echo "Expected to see:" + echo " ✓ Main app dSYM with 'debug' tag (~145MB)" + echo " ✓ Framework dSYMs" + echo "" + read -p "Press Enter to continue..." +} + +# Test with current version +test_current_version() { + echo "=== Phase 1: Testing with v7.12.1 (current stable) ===" + echo "" + + cd "$PROJECT_PATH" + + echo "Installing @sentry/react-native@7.12.1..." + yarn add @sentry/react-native@7.12.1 || npm install @sentry/react-native@7.12.1 + + echo "" + echo "Cleaning and regenerating native code..." + npx expo prebuild --clean + + echo "" + echo "Building with EAS..." + echo "Watch for 'Upload Debug Symbols to Sentry' in logs" + echo "" + + eas build --platform ios --profile production --local 2>&1 | tee build-v7.12.1.log + + echo "" + check_sentry_debug_files +} + +# Test with our fix +test_with_fix() { + echo "=== Phase 2: Testing with dSYM wait fix ===" + echo "" + + cd "$SDK_PATH" + echo "Building SDK..." + yarn build + + cd "$PROJECT_PATH" + + echo "" + echo "Linking to local SDK..." + yarn link "$SDK_PATH/packages/core" || npm link "$SDK_PATH/packages/core" + + echo "" + echo "Cleaning and regenerating native code..." + npx expo prebuild --clean + + echo "" + echo "Building with debug logging enabled..." + echo "Look for:" + echo " - 'DEBUG: DWARF_DSYM_FOLDER_PATH=...'" + echo " - 'DEBUG: DWARF_DSYM_FILE_NAME=...'" + echo " - 'Verified main app dSYM is complete'" + echo "" + + SENTRY_DSYM_DEBUG=true eas build --platform ios --profile production --local 2>&1 | tee build-with-fix.log + + echo "" + check_sentry_debug_files +} + +# Main menu +echo "Choose test to run:" +echo "1) Test current v7.12.1 (reproduce issue)" +echo "2) Test with fix (verify solution)" +echo "3) Run both tests" +echo "" +read -p "Enter choice [1-3]: " choice + +case $choice in + 1) + test_current_version + ;; + 2) + test_with_fix + ;; + 3) + test_current_version + echo "" + echo "========================================" + echo "" + test_with_fix + ;; + *) + echo "Invalid choice" + exit 1 + ;; +esac + +echo "" +echo "=== Testing Complete ===" +echo "" +echo "Build logs saved:" +echo " - build-v7.12.1.log (if tested)" +echo " - build-with-fix.log (if tested)" +echo "" +echo "Please compare the results in Sentry Debug Files" diff --git a/packages/core/scripts/test-dsym-wait.sh b/packages/core/scripts/test-dsym-wait.sh new file mode 100755 index 0000000000..e451ddbd5b --- /dev/null +++ b/packages/core/scripts/test-dsym-wait.sh @@ -0,0 +1,61 @@ +#!/bin/bash +# Manual test script for dSYM wait functionality +# This simulates the wait behavior without needing a full Xcode build + +set -x + +# Create a test directory +TEST_DIR="/tmp/sentry-dsym-wait-test-$$" +mkdir -p "$TEST_DIR" + +echo "=== Test 1: dSYM appears immediately ===" +DSYM_DIR="$TEST_DIR/test1" +mkdir -p "$DSYM_DIR/TestApp.app.dSYM" +export DWARF_DSYM_FOLDER_PATH="$DSYM_DIR" +export DWARF_DSYM_FILE_NAME="TestApp.app.dSYM" +export SENTRY_DSYM_WAIT_MAX_ATTEMPTS=3 +export SENTRY_DSYM_WAIT_INTERVAL=1 + +# Source the wait function +source "$(dirname "$0")/sentry-xcode-debug-files.sh" 2>/dev/null || { + # If sourcing fails, extract just the wait function + eval "$(sed -n '/^wait_for_dsym_files()/,/^}/p' "$(dirname "$0")/sentry-xcode-debug-files.sh")" +} + +wait_for_dsym_files +echo "Test 1 result: $?" +echo "" + +echo "=== Test 2: dSYM appears after delay ===" +DSYM_DIR2="$TEST_DIR/test2" +mkdir -p "$DSYM_DIR2" +export DWARF_DSYM_FOLDER_PATH="$DSYM_DIR2" +export DWARF_DSYM_FILE_NAME="DelayedApp.app.dSYM" + +# Create dSYM in background after 2 seconds +(sleep 2 && mkdir -p "$DSYM_DIR2/DelayedApp.app.dSYM" && echo "Background: Created dSYM") & + +wait_for_dsym_files +echo "Test 2 result: $?" +echo "" + +echo "=== Test 3: dSYM never appears (timeout) ===" +DSYM_DIR3="$TEST_DIR/test3" +mkdir -p "$DSYM_DIR3" +export DWARF_DSYM_FOLDER_PATH="$DSYM_DIR3" +export DWARF_DSYM_FILE_NAME="NeverExists.app.dSYM" +export SENTRY_DSYM_WAIT_MAX_ATTEMPTS=2 + +wait_for_dsym_files +echo "Test 3 result: $?" +echo "" + +echo "=== Test 4: Wait disabled ===" +export SENTRY_DSYM_WAIT_ENABLED=false +wait_for_dsym_files +echo "Test 4 result: $?" +echo "" + +# Cleanup +rm -rf "$TEST_DIR" +echo "=== All tests complete ===" diff --git a/packages/core/test/expo-plugin/modifyXcodeProject.test.ts b/packages/core/test/expo-plugin/modifyXcodeProject.test.ts index 92dc615835..23cbe42c7e 100644 --- a/packages/core/test/expo-plugin/modifyXcodeProject.test.ts +++ b/packages/core/test/expo-plugin/modifyXcodeProject.test.ts @@ -77,3 +77,110 @@ describe('Configures iOS native project correctly', () => { expect(warnOnce).toHaveBeenCalled(); }); }); + +describe('Upload Debug Symbols to Sentry build phase', () => { + let mockXcodeProject: any; + let addBuildPhaseSpy: jest.Mock; + const expectedShellScript = + "/bin/sh `${NODE_BINARY:-node} --print \"require('path').dirname(require.resolve('@sentry/react-native/package.json')) + '/scripts/sentry-xcode-debug-files.sh'\"`"; + + const getOptions = () => { + const callArgs = addBuildPhaseSpy.mock.calls[0]; + return callArgs[4]; + }; + + beforeEach(() => { + addBuildPhaseSpy = jest.fn(); + mockXcodeProject = { + pbxItemByComment: jest.fn().mockReturnValue(null), + addBuildPhase: addBuildPhaseSpy, + }; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('creates Upload Debug Symbols build phase with correct shell script', () => { + mockXcodeProject.addBuildPhase([], 'PBXShellScriptBuildPhase', 'Upload Debug Symbols to Sentry', null, { + shellPath: '/bin/sh', + shellScript: expectedShellScript, + }); + + expect(addBuildPhaseSpy).toHaveBeenCalledWith( + [], + 'PBXShellScriptBuildPhase', + 'Upload Debug Symbols to Sentry', + null, + { + shellPath: '/bin/sh', + shellScript: expectedShellScript, + }, + ); + }); + + it('does not include inputPaths to avoid circular dependency', () => { + // We don't use inputPaths because they cause circular dependency errors in Xcode 15+ + // (see issue #5641). Instead, the bash script waits for dSYM files to be generated. + mockXcodeProject.addBuildPhase([], 'PBXShellScriptBuildPhase', 'Upload Debug Symbols to Sentry', null, { + shellPath: '/bin/sh', + shellScript: expectedShellScript, + }); + + const options = getOptions(); + + expect(options.inputPaths).toBeUndefined(); + }); + + it('skips creating build phase if it already exists', () => { + mockXcodeProject.pbxItemByComment = jest.fn().mockReturnValue({ + shellScript: 'existing', + }); + + expect(addBuildPhaseSpy).not.toHaveBeenCalled(); + }); + + describe('Race condition handling', () => { + it('documents why we do not use inputPaths', () => { + // This test documents the decision NOT to use inputPaths. + // + // ISSUE #5288: Race condition where upload script runs before dSYM generation completes + // ISSUE #5641: inputPaths cause circular dependency errors in Xcode 15+ + // + // We attempted to fix #5288 by adding inputPaths to declare dependency on dSYM files: + // inputPaths: [ + // '"$(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)/Contents/Resources/DWARF/$(PRODUCT_NAME)"', + // '"$(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)"', + // ] + // + // However, this caused Xcode 15+ to fail with: + // "Cycle inside X; building could produce unreliable results" + // + // The cycle occurs because: + // 1. The target produces the dSYM as an output during linking + // 2. The "Upload Debug Symbols" build phase (part of the same target) declares the dSYM as an input + // 3. Xcode detects: target depends on its own output = CYCLE + // + // SOLUTION: Instead of using inputPaths, the bash script (sentry-xcode-debug-files.sh) + // now waits for dSYM files to exist before uploading. This avoids the circular dependency + // while still handling the race condition. + // + // See: + // - https://github.com/getsentry/sentry-react-native/issues/5288 + // - https://github.com/getsentry/sentry-react-native/issues/5641 + // - https://developer.apple.com/forums/thread/730974 + + mockXcodeProject.addBuildPhase([], 'PBXShellScriptBuildPhase', 'Upload Debug Symbols to Sentry', null, { + shellPath: '/bin/sh', + shellScript: expectedShellScript, + }); + + const options = getOptions(); + + // Verify that inputPaths are NOT used + expect(options.inputPaths).toBeUndefined(); + expect(options.shellPath).toBe('/bin/sh'); + expect(options.shellScript).toBe(expectedShellScript); + }); + }); +}); diff --git a/packages/core/test/scripts/sentry-xcode-scripts.test.ts b/packages/core/test/scripts/sentry-xcode-scripts.test.ts index b4529b9104..6b4c77b0a8 100644 --- a/packages/core/test/scripts/sentry-xcode-scripts.test.ts +++ b/packages/core/test/scripts/sentry-xcode-scripts.test.ts @@ -136,6 +136,193 @@ describe('sentry-xcode-debug-files.sh', () => { expect(result.exitCode).toBe(0); expect(result.stdout).toContain('Skipping debug files upload for *Debug* configuration'); }); + + describe('dSYM wait functionality', () => { + it('proceeds immediately when dSYM folder already exists with complete dSYM files', () => { + // Create a complete dSYM bundle structure with DWARF binary + const dsymPath = path.join(tempDir, 'TestApp.app.dSYM'); + const dwarfDir = path.join(dsymPath, 'Contents', 'Resources', 'DWARF'); + fs.mkdirSync(dwarfDir, { recursive: true }); + // Create a non-empty DWARF binary file + fs.writeFileSync(path.join(dwarfDir, 'TestApp'), 'mock dwarf binary content'); + + const result = runScript({ + DWARF_DSYM_FOLDER_PATH: tempDir, + DWARF_DSYM_FILE_NAME: 'TestApp.app.dSYM', + MOCK_CLI_EXIT_CODE: '0', + }); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Checking for dSYM files'); + expect(result.stdout).toContain('Found'); + expect(result.stdout).toContain('dSYM bundle(s)'); + expect(result.stdout).toContain('Verified main app dSYM is complete'); + // Should not have waited since dSYM exists + expect(result.stdout).not.toContain('Waiting'); + }); + + // Note: Testing "file appears during wait" scenario is difficult with execSync + // as it blocks the Node.js process. The wait logic is adequately covered by + // the "proceeds immediately" and "times out" tests. + + it('times out when dSYM never appears', () => { + const dsymFolderPath = path.join(tempDir, 'empty-dsym-folder'); + fs.mkdirSync(dsymFolderPath, { recursive: true }); + + const result = runScript({ + DWARF_DSYM_FOLDER_PATH: dsymFolderPath, + DWARF_DSYM_FILE_NAME: 'NonExistent.app.dSYM', + SENTRY_DSYM_WAIT_MAX_ATTEMPTS: '2', + SENTRY_DSYM_WAIT_INTERVAL: '1', + MOCK_CLI_EXIT_CODE: '0', + }); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Checking for dSYM files'); + expect(result.stdout).toContain('Waiting'); + expect(result.stdout).toContain('warning: Timeout waiting for dSYM files'); + expect(result.stdout).toContain('This may result in incomplete debug symbol uploads'); + }); + + it('skips wait check when SENTRY_DSYM_WAIT_ENABLED=false', () => { + const result = runScript({ + SENTRY_DSYM_WAIT_ENABLED: 'false', + MOCK_CLI_EXIT_CODE: '0', + }); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('SENTRY_DSYM_WAIT_ENABLED=false'); + expect(result.stdout).toContain('skipping dSYM wait check'); + expect(result.stdout).not.toContain('Checking for dSYM files'); + }); + + it('proceeds when folder contains any dSYM even without DWARF_DSYM_FILE_NAME', () => { + // Create some complete dSYM bundles + const dsymPath1 = path.join(tempDir, 'Framework1.framework.dSYM'); + const dwarfDir1 = path.join(dsymPath1, 'Contents', 'Resources', 'DWARF'); + fs.mkdirSync(dwarfDir1, { recursive: true }); + fs.writeFileSync(path.join(dwarfDir1, 'Framework1'), 'mock dwarf content'); + + const dsymPath2 = path.join(tempDir, 'Framework2.framework.dSYM'); + const dwarfDir2 = path.join(dsymPath2, 'Contents', 'Resources', 'DWARF'); + fs.mkdirSync(dwarfDir2, { recursive: true }); + fs.writeFileSync(path.join(dwarfDir2, 'Framework2'), 'mock dwarf content'); + + const result = runScript({ + DWARF_DSYM_FOLDER_PATH: tempDir, + // DWARF_DSYM_FILE_NAME not set + MOCK_CLI_EXIT_CODE: '0', + }); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('warning: DWARF_DSYM_FILE_NAME not set'); + expect(result.stdout).toContain('Found'); + expect(result.stdout).toContain('dSYM bundle(s)'); + expect(result.stdout).toContain('Found dSYM bundle(s) with valid DWARF content'); + }); + + it('continues waiting if main app dSYM not found but other dSYMs exist', () => { + const dsymFolderPath = path.join(tempDir, 'dsym-folder'); + fs.mkdirSync(dsymFolderPath, { recursive: true }); + + // Create only framework dSYM with complete structure, not the main app dSYM + const frameworkDsym = path.join(dsymFolderPath, 'SomeFramework.framework.dSYM'); + const frameworkDwarfDir = path.join(frameworkDsym, 'Contents', 'Resources', 'DWARF'); + fs.mkdirSync(frameworkDwarfDir, { recursive: true }); + fs.writeFileSync(path.join(frameworkDwarfDir, 'SomeFramework'), 'mock dwarf content'); + + const result = runScript({ + DWARF_DSYM_FOLDER_PATH: dsymFolderPath, + DWARF_DSYM_FILE_NAME: 'MainApp.app.dSYM', // Looking for this specific one + SENTRY_DSYM_WAIT_MAX_ATTEMPTS: '2', + SENTRY_DSYM_WAIT_INTERVAL: '1', + MOCK_CLI_EXIT_CODE: '0', + }); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Main app dSYM not found yet'); + expect(result.stdout).toContain('warning: Timeout waiting for dSYM files'); + }); + + it('waits when dSYM directory exists but DWARF binary is missing (incomplete)', () => { + const dsymFolderPath = path.join(tempDir, 'incomplete-dsym-folder'); + fs.mkdirSync(dsymFolderPath, { recursive: true }); + + // Create dSYM directory structure but without DWARF binary (incomplete) + const incompleteDsym = path.join(dsymFolderPath, 'IncompleteApp.app.dSYM'); + const dwarfDir = path.join(incompleteDsym, 'Contents', 'Resources', 'DWARF'); + fs.mkdirSync(dwarfDir, { recursive: true }); + // Note: NOT creating the actual DWARF file + + const result = runScript({ + DWARF_DSYM_FOLDER_PATH: dsymFolderPath, + DWARF_DSYM_FILE_NAME: 'IncompleteApp.app.dSYM', + SENTRY_DSYM_WAIT_MAX_ATTEMPTS: '2', + SENTRY_DSYM_WAIT_INTERVAL: '1', + MOCK_CLI_EXIT_CODE: '0', + }); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Main app dSYM DWARF directory is empty'); + expect(result.stdout).toContain('warning: Timeout waiting for dSYM files'); + }); + + it('waits when dSYM exists but DWARF binary is empty (still being written)', () => { + const dsymFolderPath = path.join(tempDir, 'empty-dwarf-folder'); + fs.mkdirSync(dsymFolderPath, { recursive: true }); + + // Create dSYM with empty DWARF file (simulates file being created but not written yet) + const dsymPath = path.join(dsymFolderPath, 'WritingApp.app.dSYM'); + const dwarfDir = path.join(dsymPath, 'Contents', 'Resources', 'DWARF'); + fs.mkdirSync(dwarfDir, { recursive: true }); + fs.writeFileSync(path.join(dwarfDir, 'WritingApp'), ''); // Empty file + + const result = runScript({ + DWARF_DSYM_FOLDER_PATH: dsymFolderPath, + DWARF_DSYM_FILE_NAME: 'WritingApp.app.dSYM', + SENTRY_DSYM_WAIT_MAX_ATTEMPTS: '2', + SENTRY_DSYM_WAIT_INTERVAL: '1', + MOCK_CLI_EXIT_CODE: '0', + }); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Main app dSYM DWARF binary is empty (still being written)'); + expect(result.stdout).toContain('warning: Timeout waiting for dSYM files'); + }); + + it('handles non-existent dSYM folder path', () => { + const nonExistentPath = path.join(tempDir, 'does-not-exist'); + + const result = runScript({ + DWARF_DSYM_FOLDER_PATH: nonExistentPath, + SENTRY_DSYM_WAIT_MAX_ATTEMPTS: '2', + SENTRY_DSYM_WAIT_INTERVAL: '1', + MOCK_CLI_EXIT_CODE: '0', + }); + + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('dSYM folder does not exist yet'); + expect(result.stdout).toContain('warning: Timeout waiting for dSYM files'); + }); + + it('respects custom wait interval and max attempts', () => { + const startTime = Date.now(); + + const result = runScript({ + DWARF_DSYM_FOLDER_PATH: path.join(tempDir, 'nonexistent'), + SENTRY_DSYM_WAIT_MAX_ATTEMPTS: '3', + SENTRY_DSYM_WAIT_INTERVAL: '1', + MOCK_CLI_EXIT_CODE: '0', + }); + + const duration = Date.now() - startTime; + + expect(result.exitCode).toBe(0); + // Should have waited approximately 2 seconds (3 attempts with 1s interval, but no wait after last attempt) + expect(duration).toBeGreaterThanOrEqual(2000); + expect(duration).toBeLessThan(4000); // Allow some margin + }); + }); }); describe('sentry-xcode.sh', () => {