Skip to content

Conversation

theStack
Copy link
Contributor

@theStack theStack commented Jan 1, 2025

The determinstic line coverage checking script test_deterministic_coverage.sh was seemingly forgotten to be adapted in the course of switching from autotools to CMake (only the error messages containing build instructions were updated in a9964c0 / #30875). Since gcovr needs to access both the source and build files, the path to the latter has to be passed explicitly as they are not in the same anymore (see e.g. gcovr/gcovr#340).

Can be tested by:

$ cmake -B build -DCMAKE_BUILD_TYPE=Coverage
$ cmake --build build
$ ./contrib/devtools/test_deterministic_coverage.sh

Alternatively, if the script is not needed anymore (apparently no-one else tried to execute it for the last three months?), it could also be deleted.

@DrahtBot
Copy link
Contributor

DrahtBot commented Jan 1, 2025

The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

Code Coverage & Benchmarks

For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/31588.

Reviews

See the guideline for information on the review process.

Type Reviewers
ACK maflcko
Concept ACK hodlinator, fjahr

If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update.

Conflicts

Reviewers, this pull request conflicts with the following ones:

  • #31901 (contrib: Add deterministic-unittest-coverage by maflcko)

If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

@@ -34,7 +34,7 @@ NON_DETERMINISTIC_TESTS=(
"wallet_tests/wallet_disableprivkeys" # validation.cpp: if (signals.CallbacksPending() > 10)
)

TEST_BITCOIN_BINARY="src/test/test_bitcoin"
TEST_BITCOIN_BINARY="build/src/test/test_bitcoin"
Copy link
Member

Choose a reason for hiding this comment

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

not sure about hardcoding. It would be better to have a symbol for this. This would also allow to remove mktemp and just use the build dir.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, this is currently only a minimum-diff fix, happy to refine further. I assume with "symbol" you mean just another shell script variable, e.g. BUILD_DIR? (Could even go further and allow to specify the build-dir as an optional script parameter that defaults to "build".)

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree, hardcoding a different path isn't much of a fix even if it's the one we use in all our examples. Better to have a way to pass the path in as a parameter.

Copy link
Contributor

Choose a reason for hiding this comment

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

In gen-manpages.py and gen-bitcoin-conf.sh we use BUILDDIR fwiw.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the reviews. The build path can now specified via the BUILDDIR environment variable (set to "build" relative to the top-level-dir by default) and the script can be called from anywhere within the repository.

@maflcko

This would also allow to remove mktemp and just use the build dir.

Done.

@fanquake fanquake added this to the 29.0 milestone Jan 6, 2025
Copy link
Contributor

@fjahr fjahr left a comment

Choose a reason for hiding this comment

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

Concept ACK

@@ -34,7 +34,7 @@ NON_DETERMINISTIC_TESTS=(
"wallet_tests/wallet_disableprivkeys" # validation.cpp: if (signals.CallbacksPending() > 10)
)

TEST_BITCOIN_BINARY="src/test/test_bitcoin"
TEST_BITCOIN_BINARY="build/src/test/test_bitcoin"
Copy link
Contributor

Choose a reason for hiding this comment

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

I agree, hardcoding a different path isn't much of a fix even if it's the one we use in all our examples. Better to have a way to pass the path in as a parameter.

@@ -34,7 +34,7 @@ NON_DETERMINISTIC_TESTS=(
"wallet_tests/wallet_disableprivkeys" # validation.cpp: if (signals.CallbacksPending() > 10)
)

TEST_BITCOIN_BINARY="src/test/test_bitcoin"
TEST_BITCOIN_BINARY="build/src/test/test_bitcoin"
Copy link
Contributor

Choose a reason for hiding this comment

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

In gen-manpages.py and gen-bitcoin-conf.sh we use BUILDDIR fwiw.

…builds

This change also enables to run the script for anywhere within the repository.
@theStack theStack force-pushed the 202501-contrib-fix-test-deterministic_coverage_sh branch from e240426 to 517d30b Compare February 7, 2025 07:11
@maflcko
Copy link
Member

maflcko commented Feb 7, 2025

lgtm ACK 517d30b

I haven't tested this (I am using a modified script locally anyway), but the code changes look harmless and reasonable.

@DrahtBot DrahtBot requested a review from fjahr February 7, 2025 07:36
Copy link
Contributor

@fjahr fjahr left a comment

Choose a reason for hiding this comment

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

Hm, I tried to test this but wasn't successful so far. Aside from the fix I suggest in my inline comment I still get an error that gcov is not able to find the working dir. That seems to be because the coverage files include the build paths and not the source paths. After a little digging I tried to address this by compiling with -DAPPEND_CXXFLAGS="-coverage-prefix-map=${BUILDDIR}/src=${TOPDIR}/src" but that didn't work and it doesn't seem like a good enough fix anyway.

Does this just work for you @theStack ? Maybe I am missing some compile flags that are not documented but necessary...

@@ -102,7 +104,7 @@ TEST_RUN_ID=0
while [[ ${TEST_RUN_ID} -lt ${N_TEST_RUNS} ]]; do
TEST_RUN_ID=$((TEST_RUN_ID + 1))
echo "[$(date +"%Y-%m-%d %H:%M:%S")] Measuring coverage, run #${TEST_RUN_ID} of ${N_TEST_RUNS}"
find src/ -type f -name "*.gcda" -exec rm {} \;
find "$BUILDDIR/src/" -type f -name "*.gcda" -exec rm {} \;
if [[ $(get_file_suffix_count gcda) != 0 ]]; then
Copy link
Contributor

Choose a reason for hiding this comment

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

I needed to change this to an arithmetic comparison to make it work because the wc -l result includes some white space on my system.

Suggested change
if [[ $(get_file_suffix_count gcda) != 0 ]]; then
if (( $(get_file_suffix_count gcda) != 0 )); then

@theStack
Copy link
Contributor Author

theStack commented Feb 7, 2025

@fjahr: Executing the script with the commands mentioned in the PR description works for me on Ubuntu 22.04.03 LTS, didn't try it on any other machine yet. Which operating system are you running? (Some macOS I presume?) Some versions from my machine that would be relevant for comparison:

$ cat /etc/lsb-release | tail -n1
DISTRIB_DESCRIPTION="Ubuntu 22.04.3 LTS"
$ gcovr --version | head -n1
gcovr 5.0
$ gcc --version | head -n1
gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0
$ bash --version
GNU bash, version 5.1.16(1)-release (x86_64-pc-linux-gnu)
$ wc --version
wc (GNU coreutils) 8.32

Another potential cause for failure that comes to my mind is that you possibly compile with clang. In that case, the GCOV_EXECUTABLE variable has to be changed manually in the script:

# Use GCOV_EXECUTABLE="gcov" if compiling with gcc.
# Use GCOV_EXECUTABLE="llvm-cov gcov" if compiling with clang.
GCOV_EXECUTABLE="gcov"

That's ugly of course, but not sure how to fix this easily. We should detect and set this automatically in the future.

@fjahr
Copy link
Contributor

fjahr commented Feb 7, 2025

Yeah, I'm using macOS Sequoia 15.3 and I am also using clang but I have already been using GCOV_EXECUTABLE from the start.

Checking some of the other versions:

$ gcovr --version | head -n1
gcovr 8.3
$ clang --version | head -n1
Homebrew clang version 19.1.6
$ bash --version
GNU bash, version 5.2.37(1)-release (aarch64-apple-darwin24.0.0)

The built-in wc doesn't seem to provide version information but it is supposed to be taken from BSD tools.

I tried compiling with GCC 14 as well now. It's not giving me any loud errors but it's still not working either:

$ GCOV_EXECUTABLE="gcov-14" BUILDDIR="build" contrib/devtools/test_deterministic_coverage.sh
[2025-02-08 00:00:50] Measuring coverage, run #1 of 2
(INFO) Reading coverage data...
(INFO) Writing coverage report...
Error: Spurious gcovr output. Make sure the correct GCOV_EXECUTABLE variable is set in contrib/devtools/test_deterministic_coverage.sh ("gcov" for gcc, "llvm-cov gcov" for clang).

@theStack
Copy link
Contributor Author

theStack commented Feb 9, 2025

@fjahr: Note that GCOV_EXECUTABLE is hardcoded in the script currently, so setting it as environment variable won't have any effect. Can you try again by modifying it directly in the file? It's not very user friendly that this is necessary, can add a commit that allows passing it via environment, if reviewers feel that this is still in scope of this PR.

@fjahr
Copy link
Contributor

fjahr commented Feb 9, 2025

Note that GCOV_EXECUTABLE is hardcoded in the script currently, so setting it as environment variable won't have any effect.

Ah, I had indeed missed that. I tried this for llvm but it didn't change the outcome, seems to be the same issue. I didn't investigate further there and instead tried again with gcc-14, given this is at least a bit closer to your tooling. I get another error there with this. It appears the compile time macros are confusing gcov and it sees secp256k1_scalar_split_lambda defined twice. I'm not even sure if it should be considering secp at all since this is just to get coverage of our code, right?

Here is the error I'm getting:

$ BUILDDIR="build" contrib/devtools/test_deterministic_coverage.sh
[2025-02-09 14:45:24] Measuring coverage, run #1 of 2
(INFO) Reading coverage data...
(ERROR) Traceback (most recent call last):
  File "/opt/homebrew/Cellar/gcovr/8.3/libexec/lib/python3.13/site-packages/gcovr/formats/gcov/workers.py", line 95, in worker
    work(*args, **kwargs)
    ~~~~^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Cellar/gcovr/8.3/libexec/lib/python3.13/site-packages/gcovr/formats/gcov/read.py", line 471, in process_datafile
    done = run_gcov_and_process_files(
        abs_filename,
    ...<3 lines>...
        chdir=wd,
    )
  File "/opt/homebrew/Cellar/gcovr/8.3/libexec/lib/python3.13/site-packages/gcovr/formats/gcov/read.py", line 836, in run_gcov_and_process_files
    process_gcov_json_data(gcov_filename, covdata, options)
    ~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Cellar/gcovr/8.3/libexec/lib/python3.13/site-packages/gcovr/formats/gcov/read.py", line 210, in process_gcov_json_data
    insert_file_coverage(covdata, file_cov, get_merge_mode_from_options(options))
    ~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Cellar/gcovr/8.3/libexec/lib/python3.13/site-packages/gcovr/merging.py", line 250, in insert_file_coverage
    return _insert_coverage_item(
        target.data, file.filename, file, merge_file, options, None
    )
  File "/opt/homebrew/Cellar/gcovr/8.3/libexec/lib/python3.13/site-packages/gcovr/merging.py", line 222, in _insert_coverage_item
    merged_item = merge_item(target_dict[key], new_item, options, context)
  File "/opt/homebrew/Cellar/gcovr/8.3/libexec/lib/python3.13/site-packages/gcovr/merging.py", line 295, in merge_file
    raise AssertionError("\n".join(message)) from None
AssertionError: Got function secp256k1_scalar_split_lambda in /Users/XXX/projects/clones/bitcoin/src/secp256k1/src/scalar_impl.h on multiple lines: 67, 142.
	You can run gcovr with --merge-mode-functions=MERGE_MODE.
	The available values for MERGE_MODE are described in the documentation.
GCOV source files of merge source is/are:
	/Users/XXX/projects/clones/bitcoin/src/tests.c##63f67a257b368dda78c4068542388473.gcov.json.gz
and of merge target is/are:
	/Users/XXX/projects/clones/bitcoin/src/tests_exhaustive.c##a46642766247b4021fb41c87898a129a.gcov.json.gz

(ERROR) Error occurred while reading reports:
Traceback (most recent call last):
  File "/opt/homebrew/Cellar/gcovr/8.3/libexec/lib/python3.13/site-packages/gcovr/__main__.py", line 384, in main
    covdata = gcovr_formats.read_reports(options)
  File "/opt/homebrew/Cellar/gcovr/8.3/libexec/lib/python3.13/site-packages/gcovr/formats/__init__.py", line 99, in read_reports
    covdata = GcovHandler(options).read_report()
  File "/opt/homebrew/Cellar/gcovr/8.3/libexec/lib/python3.13/site-packages/gcovr/formats/gcov/__init__.py", line 233, in read_report
    return read_report(self.options)
  File "/opt/homebrew/Cellar/gcovr/8.3/libexec/lib/python3.13/site-packages/gcovr/formats/gcov/read.py", line 96, in read_report
    contexts = pool.wait()
  File "/opt/homebrew/Cellar/gcovr/8.3/libexec/lib/python3.13/site-packages/gcovr/formats/gcov/workers.py", line 181, in wait
    raise RuntimeError(
        "Worker thread raised exception, workers canceled."
    ) from None
RuntimeError: Worker thread raised exception, workers canceled.

Error: gcovr failed. Output written to build/deterministic_coverage_gcovr_output.tmp. Exiting.

@maflcko
Copy link
Member

maflcko commented Feb 10, 2025

lgtm ACK 459683b

Only change is a small mktemp cleanup.

I still haven't tested this (I am using a modified script locally anyway), but the code changes look harmless and reasonable. As for the scope, I wonder how much effort should be spent on this script, given that:

  • No one(?) is using it right now unmodified, or has been in the past years?
  • It is written in bash, causing compat issues with the ancient version of bash shipped on macOS, as well as the general bash issues (like error handling).
  • In light of [POC] cmake: Introduce LLVM's Source-based Code Coverage reports #31394, it is unclear whether or not it would be stale again anyway.

@DrahtBot DrahtBot requested a review from fjahr February 10, 2025 09:11
Copy link
Contributor

@hodlinator hodlinator left a comment

Choose a reason for hiding this comment

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

Concept ACK 459683b

Changes are certainly a step in the right direction unless we decide to delete the script.

Tested on NixOS

Without PR, it does indeed fail:

₿ ./contrib/devtools/test_deterministic_coverage.sh
Error: Executable src/test/test_bitcoin not found. Run "cmake -B build -DCMAKE_BUILD_TYPE=Coverage" and compile.

With the PR I'm getting a failure inside gcov - AssertionError: count must not be a negative value..

Full log
₿ ./contrib/devtools/test_deterministic_coverage.sh
[2025-02-10 15:02:26] Measuring coverage, run #1 of 2
(INFO) Reading coverage data...
Traceback (most recent call last):
  File "/nix/store/415rw62rhdn4di1nn41qhcsvpmjr14yx-python3.12-gcovr-8.2/lib/python3.12/site-packages/gcovr/formats/gcov/workers.py", line 81, in worker
    work(*args, **kwargs)
  File "/nix/store/415rw62rhdn4di1nn41qhcsvpmjr14yx-python3.12-gcovr-8.2/lib/python3.12/site-packages/gcovr/formats/gcov/read.py", line 566, in process_datafile
    done = run_gcov_and_process_files(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/nix/store/415rw62rhdn4di1nn41qhcsvpmjr14yx-python3.12-gcovr-8.2/lib/python3.12/site-packages/gcovr/formats/gcov/read.py", line 922, in run_gcov_and_process_files
    process_gcov_json_data(gcov_filename, covdata, options)
  File "/nix/store/415rw62rhdn4di1nn41qhcsvpmjr14yx-python3.12-gcovr-8.2/lib/python3.12/site-packages/gcovr/formats/gcov/read.py", line 234, in process_gcov_json_data
    LineCoverage(
  File "/nix/store/415rw62rhdn4di1nn41qhcsvpmjr14yx-python3.12-gcovr-8.2/lib/python3.12/site-packages/gcovr/coverage.py", line 410, in __init__
    raise AssertionError("count must not be a negative value.")
AssertionError: count must not be a negative value.
Traceback (most recent call last):
  File "/nix/store/415rw62rhdn4di1nn41qhcsvpmjr14yx-python3.12-gcovr-8.2/lib/python3.12/site-packages/gcovr/formats/gcov/workers.py", line 81, in worker
    work(*args, **kwargs)
  File "/nix/store/415rw62rhdn4di1nn41qhcsvpmjr14yx-python3.12-gcovr-8.2/lib/python3.12/site-packages/gcovr/formats/gcov/read.py", line 566, in process_datafile
    done = run_gcov_and_process_files(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/nix/store/415rw62rhdn4di1nn41qhcsvpmjr14yx-python3.12-gcovr-8.2/lib/python3.12/site-packages/gcovr/formats/gcov/read.py", line 922, in run_gcov_and_process_files
    process_gcov_json_data(gcov_filename, covdata, options)
  File "/nix/store/415rw62rhdn4di1nn41qhcsvpmjr14yx-python3.12-gcovr-8.2/lib/python3.12/site-packages/gcovr/formats/gcov/read.py", line 234, in process_gcov_json_data
    LineCoverage(
  File "/nix/store/415rw62rhdn4di1nn41qhcsvpmjr14yx-python3.12-gcovr-8.2/lib/python3.12/site-packages/gcovr/coverage.py", line 410, in __init__
    raise AssertionError("count must not be a negative value.")
AssertionError: count must not be a negative value.
(ERROR) Error occurred while reading reports:
Traceback (most recent call last):
  File "/nix/store/415rw62rhdn4di1nn41qhcsvpmjr14yx-python3.12-gcovr-8.2/lib/python3.12/site-packages/gcovr/__main__.py", line 358, in main
    covdata: CovData = gcovr_formats.read_reports(options)
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/nix/store/415rw62rhdn4di1nn41qhcsvpmjr14yx-python3.12-gcovr-8.2/lib/python3.12/site-packages/gcovr/formats/__init__.py", line 85, in read_reports
    covdata = GcovHandler(options).read_report()
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/nix/store/415rw62rhdn4di1nn41qhcsvpmjr14yx-python3.12-gcovr-8.2/lib/python3.12/site-packages/gcovr/formats/gcov/__init__.py", line 215, in read_report
    return read_report(self.options)
           ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/nix/store/415rw62rhdn4di1nn41qhcsvpmjr14yx-python3.12-gcovr-8.2/lib/python3.12/site-packages/gcovr/formats/gcov/read.py", line 96, in read_report
    with Workers(
         ^^^^^^^^
  File "/nix/store/415rw62rhdn4di1nn41qhcsvpmjr14yx-python3.12-gcovr-8.2/lib/python3.12/site-packages/gcovr/formats/gcov/workers.py", line 177, in __exit__
    self.wait()
  File "/nix/store/415rw62rhdn4di1nn41qhcsvpmjr14yx-python3.12-gcovr-8.2/lib/python3.12/site-packages/gcovr/formats/gcov/workers.py", line 168, in wait
    raise self.exceptions[0][1]
  File "/nix/store/415rw62rhdn4di1nn41qhcsvpmjr14yx-python3.12-gcovr-8.2/lib/python3.12/site-packages/gcovr/formats/gcov/read.py", line 103, in read_report
    contexts = pool.wait()
               ^^^^^^^^^^^
  File "/nix/store/415rw62rhdn4di1nn41qhcsvpmjr14yx-python3.12-gcovr-8.2/lib/python3.12/site-packages/gcovr/formats/gcov/workers.py", line 168, in wait
    raise self.exceptions[0][1]
  File "/nix/store/415rw62rhdn4di1nn41qhcsvpmjr14yx-python3.12-gcovr-8.2/lib/python3.12/site-packages/gcovr/formats/gcov/workers.py", line 81, in worker
    work(*args, **kwargs)
  File "/nix/store/415rw62rhdn4di1nn41qhcsvpmjr14yx-python3.12-gcovr-8.2/lib/python3.12/site-packages/gcovr/formats/gcov/read.py", line 566, in process_datafile
    done = run_gcov_and_process_files(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/nix/store/415rw62rhdn4di1nn41qhcsvpmjr14yx-python3.12-gcovr-8.2/lib/python3.12/site-packages/gcovr/formats/gcov/read.py", line 922, in run_gcov_and_process_files
    process_gcov_json_data(gcov_filename, covdata, options)
  File "/nix/store/415rw62rhdn4di1nn41qhcsvpmjr14yx-python3.12-gcovr-8.2/lib/python3.12/site-packages/gcovr/formats/gcov/read.py", line 234, in process_gcov_json_data
    LineCoverage(
  File "/nix/store/415rw62rhdn4di1nn41qhcsvpmjr14yx-python3.12-gcovr-8.2/lib/python3.12/site-packages/gcovr/coverage.py", line 410, in __init__
    raise AssertionError("count must not be a negative value.")
AssertionError: count must not be a negative value.

count must not be a negative value.
Error: gcovr failed. Output written to /home/hodlinator/b2/build/deterministic_coverage_gcovr_output.tmp. Exiting.

/home/hodlinator/b2/build/deterministic_coverage_gcovr_output.tmp is empty.

Tooling versions:

₿ g++ -v
...
gcc version 14.2.1 20241116 (GCC)

₿ gcov -v
gcov (GCC) 14.2.1 20241116
...

@maflcko
Copy link
Member

maflcko commented Feb 10, 2025

Given all the GCC/lcov/gcov problems, I wonder if it wouldn't be easier to just go with clang/llvm:

  • Require compilation with -DCMAKE_C_COMPILER='clang' -DCMAKE_CXX_COMPILER='clang++' -DCMAKE_CXX_FLAGS='-fPIC -fprofile-instr-generate -fcoverage-mapping'
  • apply the below diff
diff --git a/contrib/devtools/test_deterministic_coverage.sh b/contrib/devtools/test_deterministic_coverage.sh
index 885396bb25..48d331ae70 100755
--- a/contrib/devtools/test_deterministic_coverage.sh
+++ b/contrib/devtools/test_deterministic_coverage.sh
@@ -8,9 +8,7 @@
 
 export LC_ALL=C
 
-# Use GCOV_EXECUTABLE="gcov" if compiling with gcc.
-# Use GCOV_EXECUTABLE="llvm-cov gcov" if compiling with clang.
-GCOV_EXECUTABLE="gcov"
+BUILD_DIR="./bld-cmake"
 
 # Disable tests known to cause non-deterministic behaviour and document the source or point of non-determinism.
 NON_DETERMINISTIC_TESTS=(
@@ -34,7 +32,9 @@ NON_DETERMINISTIC_TESTS=(
     "wallet_tests/wallet_disableprivkeys"                     # validation.cpp: if (signals.CallbacksPending() > 10)
 )
 
-TEST_BITCOIN_BINARY="src/test/test_bitcoin"
+TEST_BITCOIN_BINARY="${BUILD_DIR}/src/test/test_bitcoin"
+LLVM_PROFRAW_FILE="${BUILD_DIR}/test_det_cov.profraw"
+LLVM_PROFDATA_FILE="${BUILD_DIR}/test_det_cov.profdata"
 
 print_usage() {
     echo "Usage: $0 [custom test filter (default: all but known non-deterministic tests)] [number of test runs (default: 2)]"
@@ -65,18 +65,17 @@ if [[ $# != 0 ]]; then
 fi
 if [[ ${BOOST_TEST_RUN_FILTERS} == "" ]]; then
     BOOST_TEST_RUN_FILTERS="$(IFS=":"; echo "!${NON_DETERMINISTIC_TESTS[*]}" | sed 's/:/:!/g')"
-else
-    echo "Using Boost test filter: ${BOOST_TEST_RUN_FILTERS}"
-    echo
 fi
+echo "Using Boost test filter: ${BOOST_TEST_RUN_FILTERS}"
+echo
 
-if ! command -v gcov > /dev/null; then
-    echo "Error: gcov not installed. Exiting."
+if ! command -v llvm-profdata > /dev/null; then
+    echo "Error: llvm-profdata not installed. Exiting."
     exit 1
 fi
 
-if ! command -v gcovr > /dev/null; then
-    echo "Error: gcovr not installed. Exiting."
+if ! command -v llvm-cov > /dev/null; then
+    echo "Error: llvm-cov not installed. Exiting."
     exit 1
 fi
 
@@ -85,52 +84,41 @@ if [[ ! -e ${TEST_BITCOIN_BINARY} ]]; then
     exit 1
 fi
 
-get_file_suffix_count() {
-    find src/ -type f -name "*.$1" | wc -l
-}
-
-if [[ $(get_file_suffix_count gcno) == 0 ]]; then
-    echo "Error: Could not find any *.gcno files. The *.gcno files are generated by the compiler. Run \"cmake -B build -DCMAKE_BUILD_TYPE=Coverage\" and re-compile."
-    exit 1
-fi
-
-get_covr_filename() {
-    echo "gcovr.run-$1.txt"
-}
+
+
+
+
 
 TEST_RUN_ID=0
 while [[ ${TEST_RUN_ID} -lt ${N_TEST_RUNS} ]]; do
     TEST_RUN_ID=$((TEST_RUN_ID + 1))
     echo "[$(date +"%Y-%m-%d %H:%M:%S")] Measuring coverage, run #${TEST_RUN_ID} of ${N_TEST_RUNS}"
-    find src/ -type f -name "*.gcda" -exec rm {} \;
-    if [[ $(get_file_suffix_count gcda) != 0 ]]; then
-        echo "Error: Stale *.gcda files found. Exiting."
-        exit 1
-    fi
-    TEST_OUTPUT_TEMPFILE=$(mktemp)
-    if ! BOOST_TEST_RUN_FILTERS="${BOOST_TEST_RUN_FILTERS}" ${TEST_BITCOIN_BINARY} > "${TEST_OUTPUT_TEMPFILE}" 2>&1; then
+
+    TEST_OUTPUT_TEMPFILE="$BUILD_DIR/test_det_cov_out.tmp"
+    LLVM_PROFILE_FILE="${LLVM_PROFRAW_FILE}" BOOST_TEST_RUN_FILTERS="${BOOST_TEST_RUN_FILTERS}" ${TEST_BITCOIN_BINARY} > "${TEST_OUTPUT_TEMPFILE}" 2>&1
+    if [[ $? -ne 0 ]]; then
         cat "${TEST_OUTPUT_TEMPFILE}"
         rm "${TEST_OUTPUT_TEMPFILE}"
         exit 1
     fi
     rm "${TEST_OUTPUT_TEMPFILE}"
-    if [[ $(get_file_suffix_count gcda) == 0 ]]; then
-        echo "Error: Running the test suite did not create any *.gcda files. The gcda files are generated when the instrumented test programs are executed. Run \"cmake -B build -DCMAKE_BUILD_TYPE=Coverage\" and re-compile."
-        exit 1
-    fi
-    GCOVR_TEMPFILE=$(mktemp)
-    if ! gcovr --gcov-executable "${GCOV_EXECUTABLE}" -r src/ > "${GCOVR_TEMPFILE}"; then
-        echo "Error: gcovr failed. Output written to ${GCOVR_TEMPFILE}. Exiting."
+
+    if [[ ! -f ${LLVM_PROFRAW_FILE} ]]; then
+        echo "Error: Profiling data not generated. Ensure the binary is compiled with Clang coverage flags."
         exit 1
     fi
-    GCOVR_FILENAME=$(get_covr_filename ${TEST_RUN_ID})
-    mv "${GCOVR_TEMPFILE}" "${GCOVR_FILENAME}"
-    if grep -E "^TOTAL *0 *0 " "${GCOVR_FILENAME}"; then
-        echo "Error: Spurious gcovr output. Make sure the correct GCOV_EXECUTABLE variable is set in $0 (\"gcov\" for gcc, \"llvm-cov gcov\" for clang)."
+
+    llvm-profdata merge --sparse "${LLVM_PROFRAW_FILE}" -o "${LLVM_PROFDATA_FILE}"
+    if [[ $? -ne 0 ]]; then
+        echo "Error: llvm-profdata failed to generate profiling data. Exiting."
         exit 1
     fi
+
+    COVERAGE_FILENAME="${BUILD_DIR}/test_det_cov.run-${TEST_RUN_ID}.txt"
+    llvm-cov show "${TEST_BITCOIN_BINARY}" -instr-profile="${LLVM_PROFDATA_FILE}" > "${COVERAGE_FILENAME}"
+
     if [[ ${TEST_RUN_ID} != 1 ]]; then
-        COVERAGE_DIFF=$(diff -u "$(get_covr_filename 1)" "${GCOVR_FILENAME}")
+        COVERAGE_DIFF=$(diff -u "${BUILD_DIR}/test_det_cov.run-1.txt" "${COVERAGE_FILENAME}")
         if [[ ${COVERAGE_DIFF} != "" ]]; then
             echo
             echo "The line coverage is non-deterministic between runs. Exiting."
@@ -142,7 +130,7 @@ while [[ ${TEST_RUN_ID} -lt ${N_TEST_RUNS} ]]; do
             echo "${COVERAGE_DIFF}"
             exit 1
         fi
-        rm "${GCOVR_FILENAME}"
+        rm "${COVERAGE_FILENAME}"
     fi
 done
 

But no strong opinion. I think anything is fine here and I've already reviewed this pull.

@hodlinator
Copy link
Contributor

Succeeded with maflcko's diff applied to master (modified build dir).

Got a big diff, indicating a lot of test non-determinism:

[2025-02-10 22:32:56] Measuring coverage, run #1 of 2
[2025-02-10 22:35:26] Measuring coverage, run #2 of 2

The line coverage is non-deterministic between runs. Exiting.

The test suite must be deterministic in the sense that the set of lines executed at least
once must be identical between runs. This is a necessary condition for meaningful
coverage measuring.

--- ./build/test_det_cov.run-1.txt	2025-02-10 22:35:26.017552349 +0100
+++ ./build/test_det_cov.run-2.txt	2025-02-10 22:37:53.626966257 +0100
@@ -1980,19 +1980,19 @@
   751|       |
   752|       |    // Loop through the addrman table until we find an appropriate entry
   753|     78|    double chance_factor = 1.0;
-  754|  85.7k|    while (1) {
+  754|  85.3k|    while (1) {
... ~39k+ lines omitted

Don't have a strong preference either really (GCC/LLVM). Seems like a cool concept to have this if only people thought to chip away at what it finds, and CI stats somewhere graphed the overall size of the diff.

@fjahr
Copy link
Contributor

fjahr commented Feb 10, 2025

I wonder if it wouldn't be easier to just go with clang/llvm

I've tested the suggested diff and it worked for me. llvm/clang is my default anyway so I would give this a Concept ACK if you want to follow this approach @theStack

@maflcko
Copy link
Member

maflcko commented Feb 11, 2025

Got a big diff, indicating a lot of test non-determinism:

I haven't looked in detail, but I presume much is due to randomness. On one hand, one could go with #31486 for the unit tests as well. On the other hand, some unit tests are fuzz tests (drawing inputs from a random seed, which is recorded), so they'd be a bit more limited if the randomness was made deterministic. But I think either way is fine.

@hodlinator
Copy link
Contributor

Got a big diff, indicating a lot of test non-determinism:

I haven't looked in detail, but I presume much is due to randomness. On one hand, one could go with #31486 for the unit tests as well. On the other hand, some unit tests are fuzz tests (drawing inputs from a random seed, which is recorded), so they'd be a bit more limited if the randomness was made deterministic. But I think either way is fine.

This gives me an 8.4K line diff instead of 39k, so there is some hope:

RANDOM_CTX_SEED=21 ./contrib/devtools/test_deterministic_coverage.sh

@theStack
Copy link
Contributor Author

I wonder if it wouldn't be easier to just go with clang/llvm

I've tested the suggested diff and it worked for me. llvm/clang is my default anyway so I would give this a Concept ACK if you want to follow this approach @theStack

Concept ACK on switching over to clang/llvm as well considering all the reported problems. Note though that this PR merely tries to restore the previous behaviour of the script for CMake builds (and minor improvements that are enabled by that), so I think a deeper change is out of scope.
@maflcko: Do you want to open a PR? Happy to close this one and test then. I'd rather not include a diff here that I don't understand myself, and there are seemingly additional changes needed, like documenting the needed build flags somewhere.

@maflcko
Copy link
Member

maflcko commented Feb 11, 2025

Sure, but it may take some time until I get around to it.

@maflcko
Copy link
Member

maflcko commented Feb 11, 2025

In any case, I am sure this script is broken or at least stale for years and shouldn't be a blocker for 29.0 (even if it was used, it is a dev-only tool). So I'll be removing the milestone for now.

@maflcko
Copy link
Member

maflcko commented Feb 18, 2025

Done in #31901

@maflcko
Copy link
Member

maflcko commented Feb 26, 2025

Maybe this can be closed, given that #31901 has 3 acks and is likely close to merge?

@fanquake
Copy link
Member

Closing in favour of #31901.

@fanquake fanquake closed this Feb 26, 2025
fanquake added a commit that referenced this pull request Mar 13, 2025
fa99c3b test: Exclude SeedStartup from coverage counts (MarcoFalke)
fa579d6 contrib: Add deterministic-unittest-coverage (MarcoFalke)
fa3940b contrib: deterministic-fuzz-coverage fixups (MarcoFalke)
faf905b doc: Remove unused -fPIC (MarcoFalke)
fa1e0a7 gitignore: target/ (MarcoFalke)

Pull request description:

  The `contrib/devtools/test_deterministic_coverage.sh` script is problematic:

  * It is written in bash. This can lead to issues when running with the ancient bash version shipped by macOS by default, or can lead to other compatibility issues, such as #31588 (comment). Also, pipefail isn't set, so IO errors may be silently ignored.
  * It is based on gcov. This can lead to issues, such as #31588 (review) (possibly due to prefix-map), or #31588 (comment) (gcovr processing error), or #31588 (review) (gcovr assertion error).
  * The script is severely outdated, with the last update to `NON_DETERMINISTIC_TESTS` being in the prior decade.

  Instead of patching around all issues one-by-one, just provide a fresh rewrite, based on the recently added `deterministic-fuzz-coverage` tool based on clang, llvm-cov, and llvm-profdata. (Initial feedback indicates that this is a more promising attempt: #31588 (comment) and #31588 (comment)).

  The new tool also sets `RANDOM_CTX_SEED=21` as suggested by hodlinator in #31588 (comment).

ACKs for top commit:
  Prabhat1308:
    Concept ACK [`fa99c3b`](fa99c3b)
  hodlinator:
    re-ACK fa99c3b
  brunoerg:
    light ACK fa99c3b
  dergoegge:
    tACK fa99c3b
  janb84:
    Concept ACK [fa99c3b](fa99c3b)

Tree-SHA512: 491d5e6413d929395a5c7caea54817bdc1a0e00562c9728a374d4e92f2e2017dba4a770ecdb2e7317e049df9fdeb390d83c90dff9aa5709f97aa3f6a0e70cdb4
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants