Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
0bb6d33
Initial plan
Copilot Sep 15, 2025
e141dd9
Initial analysis of CI debugging issue
Copilot Sep 15, 2025
05947af
Implement enhanced error reporting for cmake failures
Copilot Sep 15, 2025
361b89c
Fix code formatting with black
Copilot Sep 15, 2025
6cae72c
Fix CI output: only print cmake output on failure and fix test compat…
Copilot Sep 15, 2025
c187a88
Implement comprehensive CI output reduction: quiet builds, no command…
Copilot Sep 15, 2025
5ccaee6
Final CI output optimizations: quiet pip install, silent kcov/adb ope…
Copilot Sep 15, 2025
e6038cd
Fix error reporting newline and reduce code duplication
Copilot Sep 15, 2025
e1cba0b
Remove --quiet flag from pip install as requested
Copilot Sep 15, 2025
7f8d71b
Implement comprehensive code review improvements: simplify Android lo…
Copilot Sep 15, 2025
7348b73
Refactor test to use actual format_error_output function instead of m…
Copilot Sep 15, 2025
1e7edca
Fix resource leak in run_with_capture_on_failure function
Copilot Sep 16, 2025
2826c4e
Add timeout to final process.wait() to prevent hanging
Copilot Sep 16, 2025
e21e9df
Fix CI failure by making psutil dependency optional in resource clean…
Copilot Sep 16, 2025
7bfbdcb
Add psutil dependency and revert conditional test logic
Copilot Sep 16, 2025
9208fda
Fix Android test failure due to segfault output parsing
Copilot Sep 16, 2025
c5c6c0a
Improve Android return code parsing to handle segfault outputs
Copilot Sep 16, 2025
7c0db5b
Simplify Android return code parsing logic
Copilot Sep 16, 2025
dad6a3b
Use single regex for Android return code parsing
Copilot Sep 16, 2025
c8335c4
Simplify Android parsing to only handle ret: pattern
Copilot Sep 16, 2025
983dccf
Fix Black formatter issues in Android parsing code
Copilot Sep 16, 2025
bbeb874
Remove limit_lines parameter to show full output on failure
Copilot Sep 16, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -317,10 +317,14 @@ jobs:
cat /etc/hosts
shell: bash

- name: Install Python Dependencies
shell: bash
run: |
pip install --upgrade --requirement tests/requirements.txt --quiet

- name: Test
shell: bash
run: |
pip install --upgrade --requirement tests/requirements.txt
[ "${{ matrix.CC }}" ] && export CC="${{ matrix.CC }}"
[ "${{ matrix.CXX }}" ] && export CXX="${{ matrix.CXX }}"
pytest --capture=no --verbose tests
Expand Down
101 changes: 90 additions & 11 deletions tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,13 @@ def run(cwd, exe, args, env=dict(os.environ), **kwargs):
# older android emulators do not correctly pass down the returncode
# so we basically echo the return code, and parse it manually
is_pipe = kwargs.get("stdout") == subprocess.PIPE
kwargs["stdout"] = subprocess.PIPE
should_capture_android = not is_pipe and "stdout" not in kwargs

if should_capture_android:
# Capture output for potential display on failure
kwargs["stdout"] = subprocess.PIPE
kwargs["stderr"] = subprocess.STDOUT

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Android Path Modifies kwargs and Fails stdout Capture

The Android execution path in run requires stdout to be captured for return code parsing. The new conditional capture logic prevents this when stdout is explicitly provided in kwargs, leading to a runtime error when child.stdout is None. Also, the Android path modifies the caller's kwargs in-place, unlike the non-Android path.

Fix in Cursor Fix in Web

child = subprocess.run(
[
"{}/platform-tools/adb".format(os.environ["ANDROID_HOME"]),
Expand All @@ -76,8 +82,35 @@ def run(cwd, exe, args, env=dict(os.environ), **kwargs):
stdout = child.stdout
child.returncode = int(stdout[stdout.rfind(b"ret:") :][4:])
child.stdout = stdout[: stdout.rfind(b"ret:")]
if not is_pipe:
sys.stdout.buffer.write(child.stdout)

# Only write output to stdout if not capturing or on success
if not should_capture_android or child.returncode == 0:
if not is_pipe:
sys.stdout.buffer.write(child.stdout)
elif should_capture_android and child.returncode != 0:
# Enhanced error reporting for Android test execution failures
error_details = []
error_details.append("=" * 60)
error_details.append("ANDROID TEST EXECUTION FAILED")
error_details.append("=" * 60)
error_details.append(f"Executable: {exe}")
error_details.append(f"Arguments: {' '.join(args)}")
error_details.append(f"Return code: {child.returncode}")

# Display captured output (last 50 lines to avoid too much noise)
if child.stdout:
output_text = child.stdout.decode('utf-8', errors='replace')
output_lines = output_text.strip().split('\n')
error_details.append("--- OUTPUT (last 50 lines) ---")
last_lines = output_lines[-50:] if len(output_lines) > 50 else output_lines
error_details.append('\n'.join(last_lines))

error_details.append("=" * 60)

# Print the detailed error information
error_message = "\n".join(error_details)
print(error_message, flush=True)

if kwargs.get("check") and child.returncode:
raise subprocess.CalledProcessError(
child.returncode, child.args, output=child.stdout, stderr=child.stderr
Expand Down Expand Up @@ -114,14 +147,60 @@ def run(cwd, exe, args, env=dict(os.environ), **kwargs):
"--leak-check=yes",
*cmd,
]
try:
return subprocess.run([*cmd, *args], cwd=cwd, env=env, **kwargs)
except subprocess.CalledProcessError:
raise pytest.fail.Exception(
"running command failed: {cmd} {args}".format(
cmd=" ".join(cmd), args=" ".join(args)
)
) from None

# Capture output unless explicitly requested to pipe to caller or stream to stdout
should_capture = kwargs.get("stdout") != subprocess.PIPE and "stdout" not in kwargs

if should_capture:
# Capture both stdout and stderr for potential display on failure
kwargs_with_capture = kwargs.copy()
kwargs_with_capture["stdout"] = subprocess.PIPE
kwargs_with_capture["stderr"] = subprocess.STDOUT
kwargs_with_capture["universal_newlines"] = True

try:
result = subprocess.run([*cmd, *args], cwd=cwd, env=env, **kwargs_with_capture)
if result.returncode != 0 and kwargs.get("check"):
# Enhanced error reporting for test execution failures
error_details = []
error_details.append("=" * 60)
error_details.append("TEST EXECUTION FAILED")
error_details.append("=" * 60)
error_details.append(f"Command: {' '.join(cmd + args)}")
error_details.append(f"Working directory: {cwd}")
error_details.append(f"Return code: {result.returncode}")

# Display captured output (last 50 lines to avoid too much noise)
if result.stdout:
output_lines = result.stdout.strip().split('\n')
error_details.append("--- OUTPUT (last 50 lines) ---")
last_lines = output_lines[-50:] if len(output_lines) > 50 else output_lines
error_details.append('\n'.join(last_lines))

error_details.append("=" * 60)

# Print the detailed error information
error_message = "\n".join(error_details)
print(error_message, flush=True)

raise subprocess.CalledProcessError(result.returncode, result.args)
return result
except subprocess.CalledProcessError:
raise pytest.fail.Exception(
"running command failed: {cmd} {args}".format(
cmd=" ".join(cmd), args=" ".join(args)
)
) from None
else:
# Use original behavior when stdout is explicitly handled by caller
try:
return subprocess.run([*cmd, *args], cwd=cwd, env=env, **kwargs)
except subprocess.CalledProcessError:
raise pytest.fail.Exception(
"running command failed: {cmd} {args}".format(
cmd=" ".join(cmd), args=" ".join(args)
)
) from None


def check_output(*args, **kwargs):
Expand Down
96 changes: 89 additions & 7 deletions tests/cmake.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,9 @@ def destroy(self):
"--merge",
coveragedir,
*coverage_dirs,
]
],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)


Expand Down Expand Up @@ -216,10 +218,48 @@ def cmake(cwd, targets, options=None, cflags=None):

config_cmd.append(source_dir)

print("\n{} > {}".format(cwd, " ".join(config_cmd)), flush=True)
# Run with output capture, only print on failure
process = subprocess.Popen(
config_cmd,
cwd=cwd,
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
universal_newlines=True,
bufsize=1,
)

# Capture output without streaming
captured_output = []
try:
subprocess.run(config_cmd, cwd=cwd, env=env, check=True)
except subprocess.CalledProcessError:
for line in process.stdout:
captured_output.append(line)

return_code = process.wait()
if return_code != 0:
raise subprocess.CalledProcessError(return_code, config_cmd)

except subprocess.CalledProcessError as e:
# Enhanced error reporting with captured output
error_details = []
error_details.append("=" * 60)
error_details.append("CMAKE CONFIGURE FAILED")
error_details.append("=" * 60)
error_details.append(f"Command: {' '.join(config_cmd)}")
error_details.append(f"Working directory: {cwd}")
error_details.append(f"Return code: {e.returncode}")

# Display captured output
if captured_output:
error_details.append("--- OUTPUT ---")
error_details.append("".join(captured_output).strip())

error_details.append("=" * 60)

# Print the detailed error information
error_message = "\n".join(error_details)
print(error_message, flush=True)

raise pytest.fail.Exception("cmake configure failed") from None

# CodeChecker invocations and options are documented here:
Expand All @@ -241,10 +281,50 @@ def cmake(cwd, targets, options=None, cflags=None):
" ".join(buildcmd),
]

print("{} > {}".format(cwd, " ".join(buildcmd)), flush=True)
# Run with output capture, only print on failure
process = subprocess.Popen(
buildcmd,
cwd=cwd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
universal_newlines=True,
bufsize=1,
)

# Capture output without streaming
captured_output = []
try:
subprocess.run(buildcmd, cwd=cwd, check=True)
except subprocess.CalledProcessError:
for line in process.stdout:
captured_output.append(line)

return_code = process.wait()
if return_code != 0:
raise subprocess.CalledProcessError(return_code, buildcmd)

except subprocess.CalledProcessError as e:
# Enhanced error reporting with captured output
error_details = []
error_details.append("=" * 60)
error_details.append("CMAKE BUILD FAILED")
error_details.append("=" * 60)
error_details.append(f"Command: {' '.join(buildcmd)}")
error_details.append(f"Working directory: {cwd}")
error_details.append(f"Return code: {e.returncode}")

# Display captured output (last 50 lines to avoid too much noise)
if captured_output:
error_details.append("--- OUTPUT (last 50 lines) ---")
last_lines = (
captured_output[-50:] if len(captured_output) > 50 else captured_output
)
error_details.append("".join(last_lines).strip())

error_details.append("=" * 60)

# Print the detailed error information
error_message = "\n".join(error_details)
print(error_message, flush=True)

raise pytest.fail.Exception("cmake build failed") from None

# check if the DLL and EXE artifacts contain version-information
Expand Down Expand Up @@ -302,4 +382,6 @@ def cmake(cwd, targets, options=None, cflags=None):
],
cwd=cwd,
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
96 changes: 96 additions & 0 deletions tests/test_enhanced_error_reporting.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"""
Test enhanced error reporting for cmake failures.

This test validates that when cmake configure or build fails, detailed error
information is captured and displayed to help with debugging.
"""

import subprocess
import tempfile
import os
import pytest
from .cmake import cmake


def test_cmake_error_reporting(tmp_path):
"""Test that cmake failures show detailed error information."""
# Create a temporary working directory
cwd = tmp_path / "build"
cwd.mkdir()

# Try to build a non-existent target, which should either:
# - Fail at configure (if dependencies missing) with "cmake configure failed"
# - Fail at build (if dependencies available) with "cmake build failed"
# Both scenarios should show enhanced error reporting

with pytest.raises(pytest.fail.Exception, match="cmake .* failed"):
cmake(cwd, ["nonexistent_target_that_will_fail"], {}, [])


def test_cmake_successful_configure_shows_no_extra_output(tmp_path):
"""Test that successful cmake operations don't show error reporting sections."""
# This test verifies that our enhanced error reporting doesn't interfere
# with normal operation by running a simple successful case
sourcedir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))

cwd = tmp_path / "build"
cwd.mkdir()

# This should succeed without showing error reporting sections
try:
cmake(
cwd,
["sentry_test_unit"],
{"SENTRY_BACKEND": "none", "SENTRY_TRANSPORT": "none"},
[],
)
# If we get here, the test passed successfully
assert True
except Exception as e:
# If it fails, make sure it's not due to our error reporting
assert "CMAKE CONFIGURE FAILED" not in str(e)
assert "CMAKE BUILD FAILED" not in str(e)
# Re-raise the original exception if it's a different kind of failure
raise


def test_enhanced_error_format():
"""Test that the error formatting function creates properly formatted output."""
# This is a unit test for the error formatting logic we added

# Create mock subprocess.CalledProcessError
mock_cmd = ["cmake", "-DTEST=1", "/some/source"]
mock_cwd = "/some/build/dir"
mock_returncode = 1
mock_stderr = "CMake Error: Invalid option TEST"
mock_stdout = "-- Configuring incomplete, errors occurred!"

# Format error details using the same logic as in cmake.py
error_details = []
error_details.append("=" * 60)
error_details.append("CMAKE CONFIGURE FAILED")
error_details.append("=" * 60)
error_details.append(f"Command: {' '.join(mock_cmd)}")
error_details.append(f"Working directory: {mock_cwd}")
error_details.append(f"Return code: {mock_returncode}")

if mock_stderr and mock_stderr.strip():
error_details.append("--- STDERR ---")
error_details.append(mock_stderr.strip())
if mock_stdout and mock_stdout.strip():
error_details.append("--- STDOUT ---")
error_details.append(mock_stdout.strip())

error_details.append("=" * 60)
error_message = "\n".join(error_details)

# Verify the formatted output contains expected sections
assert "CMAKE CONFIGURE FAILED" in error_message
assert "Command: cmake -DTEST=1 /some/source" in error_message
assert "Working directory: /some/build/dir" in error_message
assert "Return code: 1" in error_message
assert "--- STDERR ---" in error_message
assert "CMake Error: Invalid option TEST" in error_message
assert "--- STDOUT ---" in error_message
assert "-- Configuring incomplete, errors occurred!" in error_message
assert error_message.count("=" * 60) == 3 # Header, middle, and footer
Loading