diff --git a/.github/workflows/ci-homebrew.yml b/.github/workflows/ci-homebrew.yml index 1cc07d38ba8..54e28ef28df 100644 --- a/.github/workflows/ci-homebrew.yml +++ b/.github/workflows/ci-homebrew.yml @@ -177,7 +177,7 @@ jobs: - name: Validate Homebrew Formula id: test if: matrix.release != true - uses: LizardByte/actions/actions/release_homebrew@200eaeb897a2b065a65cb6f16b41077432007490 # v2026.605.34721 + uses: LizardByte/actions/actions/release_homebrew@a46850981292c4bbc0c41715f286ad95adaaaf4a # v2026.625.20301 with: actionlint_config: "---\n# empty config" formula_file: ${{ github.workspace }}/homebrew/sunshine.rb @@ -198,6 +198,7 @@ jobs: name: coverage-Homebrew-${{ matrix.os_name }}-${{ matrix.os_version }} path: | ${{ steps.test.outputs.testpath }}/coverage.xml + ${{ steps.test.outputs.testpath }}/coverage.lcov ${{ steps.test.outputs.testpath }}/tests/test_results.xml if-no-files-found: error diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a6de425f85d..f7ee8d8afb6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -218,13 +218,16 @@ jobs: coverage: true pr: true - name: Homebrew-macos-14 - coverage: false + coverage: true + coverage_file: coverage.lcov pr: true - name: Homebrew-macos-15 - coverage: false + coverage: true + coverage_file: coverage.lcov pr: true - name: Homebrew-macos-26 - coverage: false + coverage: true + coverage_file: coverage.lcov pr: true - name: Homebrew-ubuntu-24.04 coverage: true @@ -264,7 +267,7 @@ jobs: with: disable_search: true fail_ci_if_error: true - files: ./_coverage/coverage.xml + files: ./_coverage/${{ matrix.coverage_file || 'coverage.xml' }} report_type: coverage flags: ${{ matrix.name }} token: ${{ secrets.CODECOV_TOKEN }} @@ -343,7 +346,7 @@ jobs: path: homebrew - name: Upload Homebrew Beta Formula - uses: LizardByte/actions/actions/release_homebrew@200eaeb897a2b065a65cb6f16b41077432007490 # v2026.605.34721 + uses: LizardByte/actions/actions/release_homebrew@a46850981292c4bbc0c41715f286ad95adaaaf4a # v2026.625.20301 with: actionlint_config: "---\n# empty config" formula_file: ${{ github.workspace }}/homebrew/sunshine-beta.rb diff --git a/packaging/sunshine.rb b/packaging/sunshine.rb index 48d7f682073..08f68709f80 100644 --- a/packaging/sunshine.rb +++ b/packaging/sunshine.rb @@ -5,8 +5,13 @@ class Sunshine < Formula CUDA_VERSION = "13.1".freeze CUDA_FORMULA = "cuda@#{CUDA_VERSION}".freeze + COVERAGE_LCOV = "coverage.lcov".freeze + COVERAGE_PROFDATA = "coverage.profdata".freeze + COVERAGE_XML = "coverage.xml".freeze GCC_VERSION = "14".freeze GCC_FORMULA = "gcc@#{GCC_VERSION}".freeze + LLVM_PROFILE_FILE_ENV = "LLVM_PROFILE_FILE".freeze + TEST_BINARY = "test_sunshine".freeze IS_UPSTREAM_REPO = ENV.fetch("GITHUB_REPOSITORY", "") == "LizardByte/Sunshine" desc "@PROJECT_DESCRIPTION@" @@ -48,7 +53,6 @@ class Sunshine < Formula depends_on "graphviz" => :build if build.with? "docs" depends_on "node" => :build depends_on "pkgconf" => :build - depends_on "gcovr" => :test depends_on "boost" depends_on "curl" depends_on "icu4c@78" @@ -56,12 +60,9 @@ class Sunshine < Formula depends_on "openssl@3" depends_on "opus" - on_macos do - depends_on "llvm" => [:build, :test] - end - on_linux do depends_on GCC_FORMULA => [:build, :test] + depends_on "gcovr" => [:build, :test] depends_on "lizardbyte/homebrew/#{CUDA_FORMULA}" => :build if build.with? "cuda" depends_on "python3" => :build depends_on "at-spi2-core" @@ -183,6 +184,7 @@ def base_cmake_args def add_test_args(args) if IS_UPSTREAM_REPO args << "-DBUILD_TESTS=ON" + args << "-DSUNSHINE_LLVM_COVERAGE=ON" if OS.mac? ohai "Building tests: enabled" else args << "-DBUILD_TESTS=OFF" @@ -248,6 +250,164 @@ def configure_cuda(args) ohai "CUDA enabled with nvcc at: #{nvcc_path}" end + def release_homebrew_testpath + testpath_value = ENV.fetch("HOMEBREW_TEST_ARTIFACTS_DIR", "") + return Pathname.new(testpath_value) unless testpath_value.empty? + + return if ENV.fetch("HOMEBREW_TEST_BOT", "") != "1" + + temp_path = ENV.fetch("HOMEBREW_TEMP", "") + return if temp_path.empty? + + Pathname.new(temp_path)/name/"test" + end + + def ensure_artifact_exists(path) + odie "#{path} was not created" unless path.exist? + end + + def run_test_suite(artifact_dir) + mkdir_p artifact_dir/"tests" + test_results = artifact_dir/"tests/test_results.xml" + + if OS.mac? + with_llvm_profile_file(artifact_dir) do + system bin/TEST_BINARY, "--gtest_color=yes", "--gtest_output=xml:#{test_results}" + end + else + system bin/TEST_BINARY, "--gtest_color=yes", "--gtest_output=xml:#{test_results}" + end + + ensure_artifact_exists test_results + end + + def with_llvm_profile_file(artifact_dir) + original_profile_file = ENV.fetch(LLVM_PROFILE_FILE_ENV, nil) + ENV[LLVM_PROFILE_FILE_ENV] = "#{artifact_dir}/sunshine-%p.profraw" + yield + ensure + if original_profile_file + ENV[LLVM_PROFILE_FILE_ENV] = original_profile_file + else + ENV.delete(LLVM_PROFILE_FILE_ENV) + end + end + + def coverage_gcov_options + gcc_path = Formula[GCC_FORMULA] + ["--gcov-executable", "#{gcc_path.opt_bin}/gcov-#{GCC_VERSION}"] + end + + def llvm_profdata_executable + Utils.safe_popen_read("xcrun", "--find", "llvm-profdata").strip + end + + def llvm_cov_executable + Utils.safe_popen_read("xcrun", "--find", "llvm-cov").strip + end + + def coverage_report_path(artifact_dir) + artifact_dir/(OS.mac? ? COVERAGE_LCOV : COVERAGE_XML) + end + + def coverage_common_options(coverage_report) + [ + "--exclude-noncode-lines", + "--exclude-throw-branches", + "--exclude-unreachable-branches", + "--xml-pretty", + "-o=#{coverage_report}", + ] + end + + def generate_coverage_report(artifact_dir, coverage_buildpath) + return if coverage_buildpath.to_s.empty? + + coverage_report = coverage_report_path(artifact_dir) + + if OS.mac? + generate_llvm_coverage_report artifact_dir, coverage_buildpath, coverage_report + else + generate_gcov_coverage_report coverage_report, coverage_buildpath + end + + ensure_artifact_exists coverage_report + end + + def generate_llvm_coverage_report(artifact_dir, coverage_buildpath, coverage_report) + profile_files = Dir["#{artifact_dir}/*.profraw"] + odie "No LLVM profile data was created" if profile_files.empty? + + profile_data = artifact_dir/COVERAGE_PROFDATA + system llvm_profdata_executable, "merge", "--sparse", "-o", profile_data.to_s, *profile_files + lcov = Utils.safe_popen_read( + llvm_cov_executable, + "export", + "-format=lcov", + "-instr-profile=#{profile_data}", + (bin/TEST_BINARY).to_s, + ) + coverage_report.write lcov_for_source_files(lcov, coverage_buildpath) + ensure_lcov_report_has_lines coverage_report + end + + def generate_gcov_coverage_report(coverage_report, coverage_buildpath) + cd "#{coverage_buildpath}/build" do + system "gcovr", ".", + "-r", "../src", + *coverage_gcov_options, + *coverage_common_options(coverage_report) + end + + ensure_cobertura_report_has_lines coverage_report + end + + def coverage_source_prefixes(coverage_buildpath) + paths = [ + coverage_buildpath.to_s, + Pathname.new(coverage_buildpath.to_s).realpath.to_s, + ] + paths.uniq.map { |path| "#{path}/src/" } + end + + def relative_lcov_record(record, source_prefixes) + lines = record.lines + source_index = lines.index { |line| line.start_with?("SF:") } + return unless source_index + + source_path = lines[source_index].delete_prefix("SF:").strip + source_prefix = source_prefixes.find { |prefix| source_path.start_with?(prefix) } + return unless source_prefix + + lines[source_index] = "SF:src/#{source_path.delete_prefix(source_prefix)}\n" + "#{lines.join}end_of_record\n" + end + + def lcov_for_source_files(lcov, coverage_buildpath) + source_prefixes = coverage_source_prefixes(coverage_buildpath) + records = lcov.split("end_of_record\n") + records.filter_map { |record| relative_lcov_record(record, source_prefixes) }.join + end + + def ensure_cobertura_report_has_lines(path) + lines_valid = path.read[/lines-valid="(\d+)"/, 1].to_i + odie "#{path} does not contain any source lines" if lines_valid.zero? + end + + def ensure_lcov_report_has_lines(path) + has_lines = path.read.lines.any? { |line| line.start_with?("DA:") } + odie "#{path} does not contain any source lines" unless has_lines + end + + def collect_test_artifacts + artifact_dir = release_homebrew_testpath + return unless IS_UPSTREAM_REPO + return unless artifact_dir + + run_test_suite artifact_dir + generate_coverage_report artifact_dir, buildpath + end + def build_cmake_args args = base_cmake_args add_test_args(args) @@ -267,7 +427,7 @@ def build_and_install_project end def install_platform_specific_files - bin.install "build/tests/test_sunshine" if IS_UPSTREAM_REPO + bin.install "build/tests/#{TEST_BINARY}" if IS_UPSTREAM_REPO # codesign the binary on intel macs system "codesign", "-s", "-", "--force", "--deep", bin/"sunshine" if OS.mac? && Hardware::CPU.intel? @@ -279,6 +439,7 @@ def install setup_build_environment build_and_install_project install_platform_specific_files + collect_test_artifacts end service do @@ -314,33 +475,14 @@ def caveats # test that the binary runs at all system bin/"sunshine", "--version" - if IS_UPSTREAM_REPO && ENV.fetch("HOMEBREW_BOTTLE_BUILD", "false") != "true" - # run the test suite - system bin/"test_sunshine", "--gtest_color=yes", "--gtest_output=xml:tests/test_results.xml" - assert_path_exists File.join(testpath, "tests", "test_results.xml") - - # create gcovr report - buildpath = ENV.fetch("HOMEBREW_BUILDPATH", "") - unless buildpath.empty? - # Change to the source directory for gcovr to work properly - cd "#{buildpath}/build" do - # Use GCC version to match what was used during compilation - if OS.linux? - gcc_path = Formula[GCC_FORMULA] - gcov_executable = "#{gcc_path.opt_bin}/gcov-#{GCC_VERSION}" - - system "gcovr", ".", - "-r", "../src", - "--gcov-executable", gcov_executable, - "--exclude-noncode-lines", - "--exclude-throw-branches", - "--exclude-unreachable-branches", - "--xml-pretty", - "-o=#{testpath}/coverage.xml" - - assert_path_exists File.join(testpath, "coverage.xml") - end - end + if IS_UPSTREAM_REPO + artifact_dir = release_homebrew_testpath + if artifact_dir + assert_path_exists artifact_dir/"tests/test_results.xml" + assert_path_exists coverage_report_path(artifact_dir) + elsif ENV.fetch("HOMEBREW_BOTTLE_BUILD", "false") != "true" + run_test_suite testpath + generate_coverage_report testpath, ENV.fetch("HOMEBREW_BUILDPATH", "") end end end diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 984481f6010..c38547f4d57 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -23,8 +23,14 @@ include_directories("${GTEST_SOURCE_DIR}/googletest/include" "${GTEST_SOURCE_DIR # coverage # https://gcovr.com/en/stable/guide/compiling.html#compiler-options -set(CMAKE_CXX_FLAGS "-fprofile-arcs -ftest-coverage -ggdb -O0") -set(CMAKE_C_FLAGS "-fprofile-arcs -ftest-coverage -ggdb -O0") +option(SUNSHINE_LLVM_COVERAGE "Use LLVM source-based coverage" OFF) +if(SUNSHINE_LLVM_COVERAGE) + add_compile_options(-fprofile-instr-generate -fcoverage-mapping -ggdb -O0) + add_link_options(-fprofile-instr-generate) +else() + set(CMAKE_CXX_FLAGS "-fprofile-arcs -ftest-coverage -ggdb -O0") + set(CMAKE_C_FLAGS "-fprofile-arcs -ftest-coverage -ggdb -O0") +endif() # Find the correct libgcov library path matching the gcc compiler version # This ensures the test executable links against the same libgcov version used during compilation