Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
27 changes: 25 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
build:
name: Windows x64 (${{ matrix.configuration }})
runs-on: windows-2022
timeout-minutes: 15
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
Expand All @@ -37,11 +37,34 @@ jobs:
}

- name: Configure
run: cmake -S . -B build -G "Visual Studio 17 2022" -A x64
run: |
$coverage = if ('${{ matrix.configuration }}' -eq 'Debug') { 'ON' } else { 'OFF' }
cmake -S . -B build -G "Visual Studio 17 2022" -A x64 -DSTEAMFIX_ENABLE_CODE_ANALYSIS=ON "-DSTEAMFIX_ENABLE_COVERAGE=$coverage"

- name: Build
run: cmake --build build --config ${{ matrix.configuration }} --parallel

- name: Test
if: matrix.configuration != 'Debug'
run: ctest --test-dir build -C ${{ matrix.configuration }} --output-on-failure --no-tests=error

- name: Install coverage tool
if: matrix.configuration == 'Debug'
run: choco install opencppcoverage --version 0.9.9.0 --yes --no-progress

- name: Test with production line coverage
id: coverage
if: matrix.configuration == 'Debug'
run: ./cmake/RunCoverage.ps1 -BuildDirectory build

- name: Upload coverage report
if: always() && matrix.configuration == 'Debug' && steps.coverage.outcome != 'skipped'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: steamfix-windows-x64-coverage
path: build/coverage/
if-no-files-found: error

- name: Upload executable
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/build/
/build-analysis/
/build-coverage/
/.vs/
/out/
*.log
*.log.old
*.dat
*.dat
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ D2SteamFix is a Windows x64 C++20 launch wrapper for the Steam version of Destin

## Validation

Follow [docs/BUILDING.md](docs/BUILDING.md) and build both Debug and Release on Windows x64 with MSVC. CI runs those builds for pushes and pull requests. This repository has no automated runtime test suite; do not claim runtime validation from a successful compile.
Follow [docs/BUILDING.md](docs/BUILDING.md) and build both Debug and Release on Windows x64 with MSVC. CI runs those builds and the automated tests for pushes and pull requests. Tests use controlled fixtures; do not claim live Steam, desktop, UAC, or repair validation from a successful compile or unit-test run.

For changes to launch, permissions, elevation, or repair, manually check the affected paths on a Windows test installation:

Expand Down
99 changes: 94 additions & 5 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.24)

project(D2SteamFix VERSION 1.1.0 LANGUAGES CXX)
project(D2SteamFix VERSION 1.1.1 LANGUAGES CXX)

set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")

Expand All @@ -11,11 +11,28 @@ if(NOT CMAKE_SIZEOF_VOID_P EQUAL 8)
message(FATAL_ERROR "D2SteamFix supports only 64-bit Windows.")
endif()

option(STEAMFIX_ENABLE_CODE_ANALYSIS "Run Microsoft C++ Core Check during the build" OFF)
set(STEAMFIX_CODE_ANALYSIS_RULESET "${CMAKE_CURRENT_SOURCE_DIR}/cmake/CoreGuidelines.ruleset"
CACHE STRING "Microsoft native code analysis ruleset")
if(STEAMFIX_ENABLE_CODE_ANALYSIS AND NOT CMAKE_GENERATOR MATCHES "^Visual Studio")
message(FATAL_ERROR "STEAMFIX_ENABLE_CODE_ANALYSIS requires a Visual Studio generator.")
endif()

function(steamfix_enable_code_analysis target)
if(STEAMFIX_ENABLE_CODE_ANALYSIS)
set_target_properties(${target} PROPERTIES
VS_GLOBAL_EnableCppCoreCheck true
VS_GLOBAL_RunCodeAnalysis true
VS_GLOBAL_CodeAnalysisRuleSet "${STEAMFIX_CODE_ANALYSIS_RULESET}"
VS_GLOBAL_CodeAnalysisTreatWarningsAsErrors true
)
endif()
endfunction()

configure_file("${CMAKE_CURRENT_SOURCE_DIR}/src/steamfix.rc.in"
"${CMAKE_CURRENT_BINARY_DIR}/steamfix.rc" @ONLY)

add_executable(steamfix WIN32
src/main.cpp
set(STEAMFIX_SOURCES
src/acl.cpp
src/app.cpp
src/elevation.cpp
Expand All @@ -26,9 +43,14 @@ add_executable(steamfix WIN32
src/process.cpp
src/repair.cpp
src/steam.cpp
src/ui.cpp
)
set(STEAMFIX_LIBRARIES advapi32 comctl32 comdlg32 crypt32 ole32 shell32 user32)
add_executable(steamfix WIN32
src/main.cpp
${STEAMFIX_SOURCES}
"${CMAKE_CURRENT_BINARY_DIR}/steamfix.rc"
src/steamfix.manifest
src/ui.cpp
)
target_include_directories(steamfix PRIVATE include)
target_compile_features(steamfix PRIVATE cxx_std_20)
Expand All @@ -49,8 +71,75 @@ target_link_options(steamfix PRIVATE
$<$<CONFIG:Release>:/Brepro>
$<$<CONFIG:Release>:/PDBALTPATH:steamfix.pdb>
)
target_link_libraries(steamfix PRIVATE advapi32 comctl32 comdlg32 crypt32 ole32 shell32 user32)
target_link_libraries(steamfix PRIVATE ${STEAMFIX_LIBRARIES})
set_target_properties(steamfix PROPERTIES INTERPROCEDURAL_OPTIMIZATION_RELEASE TRUE)
steamfix_enable_code_analysis(steamfix)

include(CTest)
option(STEAMFIX_ENABLE_COVERAGE "Build the complete Debug coverage source inventory" OFF)
if(STEAMFIX_ENABLE_COVERAGE AND NOT BUILD_TESTING)
message(FATAL_ERROR "STEAMFIX_ENABLE_COVERAGE requires BUILD_TESTING.")
endif()

function(steamfix_add_test target)
cmake_parse_arguments(TEST "" "" "ARGS" ${ARGN})
# Repair code imports TaskDialogIndirect, which requires Common Controls 6
# even when a test never opens a dialog. Match the application's manifest.
add_executable(${target} ${TEST_UNPARSED_ARGUMENTS} src/steamfix.manifest)
target_include_directories(${target} PRIVATE include)
target_compile_features(${target} PRIVATE cxx_std_20)
target_compile_definitions(${target} PRIVATE
UNICODE _UNICODE WIN32_LEAN_AND_MEAN NOMINMAX STEAMFIX_VERSION="${PROJECT_VERSION}")
target_compile_options(${target} PRIVATE /W4 /WX /permissive- /EHsc /utf-8 /sdl)
target_link_libraries(${target} PRIVATE ${STEAMFIX_LIBRARIES})
steamfix_enable_code_analysis(${target})
add_test(NAME ${target} COMMAND ${target} ${TEST_ARGS})
set_tests_properties(${target} PROPERTIES TIMEOUT 120)
endfunction()

if(BUILD_TESTING)
steamfix_add_test(command_line_tests tests/command_line_tests.cpp src/command_line.cpp)
steamfix_add_test(win32_tests tests/win32_tests.cpp)

set(STEAMFIX_APP_TEST_SOURCES ${STEAMFIX_SOURCES})
list(REMOVE_ITEM STEAMFIX_APP_TEST_SOURCES src/acl.cpp src/app.cpp src/ui.cpp)
steamfix_add_test(app_tests tests/app_tests.cpp ${STEAMFIX_APP_TEST_SOURCES} ARGS --entry-check)
steamfix_add_test(acl_tests tests/acl_tests.cpp
src/elevation.cpp src/logging.cpp src/paths.cpp src/command_line.cpp)
steamfix_add_test(elevation_tests tests/elevation_tests.cpp src/command_line.cpp src/paths.cpp)
steamfix_add_test(runtime_tests tests/runtime_tests.cpp
src/command_line.cpp src/elevation.cpp src/logging.cpp src/ui.cpp)
steamfix_add_test(process_fault_tests tests/process_fault_tests.cpp
src/elevation.cpp src/logging.cpp src/paths.cpp src/command_line.cpp)
steamfix_add_test(repair_workflow_tests tests/repair_workflow_tests.cpp
src/command_line.cpp src/elevation.cpp src/logging.cpp src/paths.cpp src/process.cpp src/steam.cpp)
steamfix_add_test(ui_entry_tests tests/ui_entry_tests.cpp src/ui.cpp src/command_line.cpp
ARGS $<TARGET_FILE:steamfix>)
add_dependencies(ui_entry_tests steamfix)
set_tests_properties(ui_entry_tests PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60)
add_library(overlay_fixture SHARED tests/overlay_fixture.cpp)
set_target_properties(overlay_fixture PROPERTIES OUTPUT_NAME overlay_fixture PREFIX "")
target_compile_features(overlay_fixture PRIVATE cxx_std_20)
target_compile_options(overlay_fixture PRIVATE /W4 /WX /permissive- /EHsc /utf-8 /sdl)
add_dependencies(runtime_tests overlay_fixture)
add_dependencies(app_tests overlay_fixture)

if(STEAMFIX_ENABLE_COVERAGE)
# Load every production translation unit in one Debug module. This keeps
# wholly uncalled code in the coverage denominator; it is not a test.
add_executable(steamfix_coverage_inventory
cmake/coverage_inventory.cpp src/main.cpp ${STEAMFIX_SOURCES} src/steamfix.manifest)
target_include_directories(steamfix_coverage_inventory PRIVATE include)
target_compile_features(steamfix_coverage_inventory PRIVATE cxx_std_20)
target_compile_definitions(steamfix_coverage_inventory PRIVATE
UNICODE _UNICODE WIN32_LEAN_AND_MEAN NOMINMAX STEAMFIX_VERSION="${PROJECT_VERSION}")
target_compile_options(steamfix_coverage_inventory PRIVATE
/W4 /WX /permissive- /EHsc /utf-8 /sdl /Zi /Od /Ob0)
target_link_options(steamfix_coverage_inventory PRIVATE
/DEBUG:FULL /OPT:NOREF /OPT:NOICF /INCREMENTAL:NO)
target_link_libraries(steamfix_coverage_inventory PRIVATE ${STEAMFIX_LIBRARIES})
endif()
endif()

install(TARGETS steamfix RUNTIME DESTINATION .)
install(FILES LICENSE.md README.md DESTINATION .)
Expand Down
21 changes: 21 additions & 0 deletions cmake/CoreGuidelines.ruleset
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<RuleSet Name="D2SteamFix Core Guidelines" Description="Focused resource, initialization, and lifetime checks; manual review covers the remaining guidelines." ToolsVersion="10.0">
<Rules AnalyzerId="Microsoft.Analyzers.NativeCodeAnalysis" RuleNamespace="Microsoft.Rules.Native">
<!-- R.11: avoid explicit new/delete; C.21: complete special members. -->
<Rule Id="C26409" Action="Error" />
<Rule Id="C26432" Action="Error" />
<!-- CP.44: name RAII objects. -->
<Rule Id="C26444" Action="Error" />
<!-- Lifetime.1: do not dereference invalid pointers. -->
<Rule Id="C26486" Action="Error" />
<Rule Id="C26487" Action="Error" />
<Rule Id="C26489" Action="Error" />
<!-- ES.20 / Type.5 / Type.6: initialize objects and members. -->
<Rule Id="C26494" Action="Error" />
<Rule Id="C26495" Action="Error" />
<!-- ES.56 and Lifetime.1: avoid moved-from and dangling values. -->
<Rule Id="C26800" Action="Error" />
<Rule Id="C26815" Action="Error" />
<Rule Id="C26816" Action="Error" />
</Rules>
</RuleSet>
74 changes: 74 additions & 0 deletions cmake/RunCoverage.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
param(
[string]$BuildDirectory = "build-coverage",
[string]$OpenCppCoverage = ""
)

$ErrorActionPreference = "Stop"
$sourceRoot = (Get-Item (Join-Path $PSScriptRoot "..")).FullName
$buildRoot = (Resolve-Path $BuildDirectory).Path
$inventory = Join-Path $buildRoot "Debug/steamfix_coverage_inventory.exe"
if (-not (Test-Path $inventory)) {
throw "Build Debug with STEAMFIX_ENABLE_COVERAGE=ON before collecting coverage."
}

if (-not $OpenCppCoverage) {
$coverageCommand = Get-Command OpenCppCoverage.exe -ErrorAction SilentlyContinue
if ($coverageCommand) {
$OpenCppCoverage = $coverageCommand.Source
} else {
$OpenCppCoverage = Join-Path $env:ProgramFiles "OpenCppCoverage/OpenCppCoverage.exe"
}
}
if (-not (Test-Path $OpenCppCoverage)) {
throw "OpenCppCoverage was not found. Install OpenCppCoverage or pass -OpenCppCoverage."
}

$outputRoot = Join-Path $buildRoot "coverage"
New-Item -ItemType Directory -Force $outputRoot | Out-Null
$archivedTestLog = Join-Path $outputRoot "LastTest.log"
if (Test-Path $archivedTestLog) {
Remove-Item -LiteralPath $archivedTestLog -Force
}
$coverageArguments = @(
"--cover_children",
"--continue_after_cpp_exception",
"--modules", $buildRoot,
"--sources", (Join-Path $sourceRoot "src"),
"--sources", (Join-Path $sourceRoot "include"),
"--export_type", "cobertura:$(Join-Path $outputRoot 'coverage.xml')",
"--export_type", "html:$(Join-Path $outputRoot 'html')",
"--", (Get-Command cmake).Source,
"-DSTEAMFIX_INVENTORY=$inventory",
"-DSTEAMFIX_CTEST=$((Get-Command ctest).Source)",
"-DSTEAMFIX_BINARY_DIR=$buildRoot",
"-P", (Join-Path $PSScriptRoot "RunCoverageTests.cmake")
)
& $OpenCppCoverage @coverageArguments
$coverageStatus = $LASTEXITCODE
if (Test-Path "LastCoverageResults.log") {
Copy-Item "LastCoverageResults.log" (Join-Path $outputRoot "LastCoverageResults.log") -Force
}
$testLog = Join-Path $buildRoot "Testing/Temporary/LastTest.log"
if (Test-Path $testLog) {
Copy-Item $testLog $archivedTestLog -Force
}

if (Test-Path (Join-Path $outputRoot "coverage.xml")) {
$summaryArguments = @(
(Join-Path $PSScriptRoot "SummarizeCoverage.py"),
(Join-Path $outputRoot "coverage.xml"), "--source-root", $sourceRoot,
"--output", (Join-Path $outputRoot "summary.md")
)
if ($coverageStatus -ne 0) {
$summaryArguments += "--test-run-failed"
}
python @summaryArguments
if ($LASTEXITCODE -ne 0) {
throw "Coverage report is incomplete; inspect coverage/summary.md."
}
} else {
throw "OpenCppCoverage did not create the Cobertura report."
}
if ($coverageStatus -ne 0) {
throw "Coverage collection or a regression test failed with exit code $coverageStatus."
}
12 changes: 12 additions & 0 deletions cmake/RunCoverageTests.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
file(REMOVE "${STEAMFIX_BINARY_DIR}/Testing/Temporary/LastTest.log")

execute_process(COMMAND "${STEAMFIX_INVENTORY}" RESULT_VARIABLE inventory_status)
if(NOT "${inventory_status}" STREQUAL "0")
message(FATAL_ERROR "Coverage inventory failed: ${inventory_status}")
endif()

execute_process(COMMAND "${STEAMFIX_CTEST}" --test-dir "${STEAMFIX_BINARY_DIR}"
-C Debug --output-on-failure --no-tests=error RESULT_VARIABLE test_status)
if(NOT "${test_status}" STREQUAL "0")
message(FATAL_ERROR "Coverage tests failed: ${test_status}")
endif()
Loading
Loading