diff --git a/CMakeLists.txt b/CMakeLists.txt index 5afd2a5b4..cb9ff19ab 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1006,6 +1006,9 @@ if(SENTRY_BUILD_TESTS) add_subdirectory(tests/unit) add_subdirectory(tests/fixtures/crash_reporter) add_subdirectory(tests/fixtures/screenshot) + if(WIN32 AND NOT XBOX) + add_subdirectory(tests/fixtures/appx) + endif() if(SENTRY_BUILD_BENCHMARKS) set(BENCHMARK_ENABLE_TESTING OFF) set(BENCHMARK_ENABLE_GTEST_TESTS OFF) diff --git a/examples/example.c b/examples/example.c index 0d606b8d3..02db336c6 100644 --- a/examples/example.c +++ b/examples/example.c @@ -35,11 +35,44 @@ #if defined(SENTRY_PLATFORM_WINDOWS) # include +# ifdef HAVE_APPMODEL +# include +# endif unsigned long get_current_thread_id() { return GetCurrentThreadId(); } + +# ifdef HAVE_APPMODEL +static void +print_package_identity(void) +{ + UINT32 length = 0; + LONG status = GetCurrentPackageFullName(&length, NULL); + if (status == APPMODEL_ERROR_NO_PACKAGE) { + printf("PACKAGE_IDENTITY:none\n"); + fflush(stdout); + return; + } + + wchar_t *package_name = (wchar_t *)calloc(length, sizeof(wchar_t)); + if (!package_name) { + printf("PACKAGE_IDENTITY:allocation-failed\n"); + fflush(stdout); + return; + } + + status = GetCurrentPackageFullName(&length, package_name); + if (status == ERROR_SUCCESS) { + printf("PACKAGE_IDENTITY:present\n"); + } else { + printf("PACKAGE_IDENTITY:error:%ld\n", status); + } + fflush(stdout); + free(package_name); +} +# endif #elif defined(SENTRY_PLATFORM_MACOS) # include # include @@ -932,6 +965,12 @@ main(int argc, char **argv) sentry_value_new_string("my_global_value"), NULL)); } +#if defined(SENTRY_PLATFORM_WINDOWS) && defined(HAVE_APPMODEL) + if (has_arg(argc, argv, "appx")) { + print_package_identity(); + } +#endif + // E2E test mode: set tags and output test ID for event correlation if (e2e_test_id[0] != '\0') { sentry_set_tag("test.id", e2e_test_id); diff --git a/tests/fixtures/appx/AppxManifest.xml b/tests/fixtures/appx/AppxManifest.xml new file mode 100644 index 000000000..36538d431 --- /dev/null +++ b/tests/fixtures/appx/AppxManifest.xml @@ -0,0 +1,61 @@ + + + + + Sentry Native AppX Test + Sentry Native Test + Assets\StoreLogo.png + true + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/fixtures/appx/CMakeLists.txt b/tests/fixtures/appx/CMakeLists.txt new file mode 100644 index 000000000..414f8616e --- /dev/null +++ b/tests/fixtures/appx/CMakeLists.txt @@ -0,0 +1,49 @@ +cmake_minimum_required(VERSION 3.10) +project(sentry_example_appx LANGUAGES C) + +if(WIN32 AND NOT XBOX) + include(CheckSymbolExists) + check_symbol_exists(GetCurrentPackageFullName "windows.h;appmodel.h" HAVE_APPMODEL) + + add_executable(sentry_example_appx + ${SENTRY_SOURCE_DIR}/examples/example.c + ) + target_link_libraries(sentry_example_appx PRIVATE sentry) + if(HAVE_APPMODEL) + target_compile_definitions(sentry_example_appx PRIVATE HAVE_APPMODEL) + endif() + + if(MSVC) + target_sources(sentry_example_appx PRIVATE sentry_example_appx.manifest) + target_compile_options(sentry_example_appx PRIVATE /wd5105 /wd4717) + if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + target_compile_options(sentry_example_appx PRIVATE -Wno-infinite-recursion /Od) + endif() + + target_compile_options(sentry_example_appx PRIVATE /guard:cf) + target_link_options(sentry_example_appx PRIVATE /GUARD:CF) + else() + target_sources(sentry_example_appx PRIVATE sentry_example_appx.rc) + endif() + + if(SENTRY_BUILD_RUNTIMESTATIC AND MSVC) + set_property(TARGET sentry_example_appx PROPERTY + MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") + endif() + + set_target_properties(sentry_example_appx PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" + RUNTIME_OUTPUT_DIRECTORY_DEBUG "${CMAKE_BINARY_DIR}" + RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}" + RUNTIME_OUTPUT_DIRECTORY_RELWITHDEBINFO "${CMAKE_BINARY_DIR}" + RUNTIME_OUTPUT_DIRECTORY_MINSIZEREL "${CMAKE_BINARY_DIR}" + ) + + add_custom_command(TARGET sentry_example_appx POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${CMAKE_CURRENT_SOURCE_DIR}/AppxManifest.xml" + "$/AppxManifest.xml" + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${CMAKE_CURRENT_SOURCE_DIR}/classicAppCompat.sccd" + "$/classicAppCompat.sccd") +endif() diff --git a/tests/fixtures/appx/classicAppCompat.sccd b/tests/fixtures/appx/classicAppCompat.sccd new file mode 100644 index 000000000..e7d427b6c --- /dev/null +++ b/tests/fixtures/appx/classicAppCompat.sccd @@ -0,0 +1,14 @@ + + + + + + + + + 0000 + + diff --git a/tests/fixtures/appx/sentry_example_appx.manifest b/tests/fixtures/appx/sentry_example_appx.manifest new file mode 100644 index 000000000..4fda8c8af --- /dev/null +++ b/tests/fixtures/appx/sentry_example_appx.manifest @@ -0,0 +1,8 @@ + + + + + diff --git a/tests/fixtures/appx/sentry_example_appx.rc b/tests/fixtures/appx/sentry_example_appx.rc new file mode 100644 index 000000000..cc25d2c7d --- /dev/null +++ b/tests/fixtures/appx/sentry_example_appx.rc @@ -0,0 +1 @@ +1 RT_MANIFEST "sentry_example_appx.manifest" diff --git a/tests/test_integration_wer.py b/tests/test_integration_wer.py index dbf088ea5..91db9f3ab 100644 --- a/tests/test_integration_wer.py +++ b/tests/test_integration_wer.py @@ -1,6 +1,8 @@ import ctypes from ctypes import wintypes +import base64 import os +import re import subprocess import sys @@ -29,12 +31,19 @@ ] S_OK = 0 +APPX_PACKAGE_NAME = "SentryNativeExample" +APPX_ACTIVITY_ID_RE = re.compile(r"ActivityId[:\s]+([0-9a-fA-F-]{36})", re.IGNORECASE) E_STORE_USER_ARCHIVE = 0 E_STORE_USER_QUEUE = 1 E_STORE_MACHINE_ARCHIVE = 2 E_STORE_MACHINE_QUEUE = 3 +PNG_1X1_TRANSPARENT = base64.b64decode( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADgg" + "GBlN2eJwAAAABJRU5ErkJggg==" +) + class WerStore: def __init__(self): @@ -147,7 +156,75 @@ def assert_sentry_event(httpserver, backend, crash_arg): assert_native_crash(envelope) -def run_wer_crash(cmake, backend, crash_arg, httpserver=None): +def powershell(command, check=True): + completed = subprocess.run( + [ + "powershell.exe", + "-NoLogo", + "-NoProfile", + "-NonInteractive", + "-ExecutionPolicy", + "Bypass", + "-Command", + command, + ], + capture_output=True, + text=True, + ) + if check and completed.returncode != 0: + pytest.fail( + f"PowerShell command failed with exit code {completed.returncode}\n" + f"Command:\n{command}\n" + f"stdout:\n{completed.stdout}\n" + f"stderr:\n{completed.stderr}" + ) + return completed + + +def remove_appx_package(): + powershell( + f"Get-AppxPackage -Name {APPX_PACKAGE_NAME} | Remove-AppxPackage", + check=False, + ) + + +def ensure_appx_assets(tmp_path): + assets = tmp_path / "Assets" + assets.mkdir(exist_ok=True) + for name in ("StoreLogo.png", "Square150x150Logo.png", "Square44x44Logo.png"): + (assets / name).write_bytes(PNG_1X1_TRANSPARENT) + + +def register_appx_package(tmp_path): + ensure_appx_assets(tmp_path) + remove_appx_package() + + manifest = tmp_path / "AppxManifest.xml" + completed = powershell( + "Add-AppxPackage " f"-Register '{manifest}' " f"-ExternalLocation '{tmp_path}'", + check=False, + ) + if completed.returncode != 0: + output = "\n".join(filter(None, [completed.stdout, completed.stderr])) + match = APPX_ACTIVITY_ID_RE.search(output) + appx_log = "" + if match: + log_result = powershell( + f"Get-AppxLog -ActivityID '{match.group(1)}'", + check=False, + ) + appx_log = "\nAppX deployment log:\n" + "\n".join( + filter(None, [log_result.stdout, log_result.stderr]) + ) + pytest.fail( + "Add-AppxPackage -Register -ExternalLocation failed" + f"\nstdout:\n{completed.stdout}" + f"\nstderr:\n{completed.stderr}" + f"{appx_log}" + ) + + +def run_wer_crash(cmake, backend, crash_arg, httpserver=None, appx=False): build_options = { "SENTRY_BACKEND": backend, "SENTRY_INTEGRATION_WER": "ON", @@ -155,66 +232,80 @@ def run_wer_crash(cmake, backend, crash_arg, httpserver=None): if httpserver is None: build_options["SENTRY_TRANSPORT"] = "none" + target = "sentry_example_appx" if appx else "sentry_example" tmp_path = cmake( - ["sentry_example"], + [target], build_options, ) - env = None - if httpserver is not None: - env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) + if appx: + register_appx_package(tmp_path) + + try: + env = None + if httpserver is not None: + env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) + if backend == "crashpad": + httpserver.expect_oneshot_request( + "/api/123456/minidump/" + ).respond_with_data("OK") + else: + httpserver.expect_oneshot_request( + "/api/123456/envelope/" + ).respond_with_data("OK") + + run_args = ["e2e-test"] + if appx: + run_args.append("appx") + run_args.append(crash_arg) if backend == "crashpad": - httpserver.expect_oneshot_request( - "/api/123456/minidump/" - ).respond_with_data("OK") - else: - httpserver.expect_oneshot_request( - "/api/123456/envelope/" - ).respond_with_data("OK") - - run_args = ["e2e-test", crash_arg] - if backend == "crashpad": - run_args.append("crashpad-wait-for-upload") + run_args.append("crashpad-wait-for-upload") - if httpserver is None: - completed = run( - tmp_path, - "sentry_example", - run_args, - expect_failure=True, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True, - ) - else: - with httpserver.wait(timeout=10) as waiting: + if httpserver is None: completed = run( tmp_path, - "sentry_example", + target, run_args, expect_failure=True, - env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, ) - if backend in ("breakpad", "inproc"): - run(tmp_path, "sentry_example", ["no-setup"], env=env) - - assert waiting.result - assert_sentry_event(httpserver, backend, crash_arg) - - test_id = None - for line in completed.stdout.splitlines(): - if line.startswith("TEST_ID:"): - test_id = line.removeprefix("TEST_ID:") - break - assert test_id, completed.stdout - - report_path, report = wait_for_wer_report(WerStore(), test_id) - assert report_path.name == "Report.wer" - - return report + else: + with httpserver.wait(timeout=10) as waiting: + completed = run( + tmp_path, + target, + run_args, + expect_failure=True, + env=env, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + ) + if backend in ("breakpad", "inproc"): + run(tmp_path, target, ["no-setup"], env=env) + + assert waiting.result + assert_sentry_event(httpserver, backend, crash_arg) + + if appx: + assert "PACKAGE_IDENTITY:present" in completed.stdout + + test_id = None + for line in completed.stdout.splitlines(): + if line.startswith("TEST_ID:"): + test_id = line.removeprefix("TEST_ID:") + break + assert test_id, completed.stdout + + report_path, report = wait_for_wer_report(WerStore(), test_id) + assert report_path.name == "Report.wer" + + return report + finally: + if appx: + remove_appx_package() @pytest.mark.parametrize( @@ -346,3 +437,10 @@ def test_wer_compatibility(cmake, httpserver, backend, crash_arg): report = run_wer_crash(cmake, backend, crash_arg, httpserver) assert "test.id" in report.lower() + + +@pytest.mark.skipif(not has_native or is_qemu, reason="native backend not available") +def test_wer_appx(cmake, httpserver): + report = run_wer_crash(cmake, "native", "fastfail", httpserver, appx=True) + + assert "test.id" in report.lower()