This PR fixes #17724.
It checks for valgrind's presence and version number.
This information will be used to decide whether the flag --exit-on-first-error should be active or not.
Versions >=3.14 will have it activated by default.
106 | suppressions_file = os.getenv("VALGRIND_SUPPRESSIONS_FILE",
107 | default_suppressions_file)
108 | self.args = ["valgrind", "--suppressions={}".format(suppressions_file),
109 | - "--gen-suppressions=all", "--exit-on-first-error=yes",
110 | + "--gen-suppressions=all",
111 | + "--exit-on-first-error=yes" if exit_on_first_error else "--exit-on-first-error=no",
perhaps construct this string outside of the arg array assignment, or do:
"--exit-on-first-error={}".format("yes" if exit_on_first_error else "no"),
I haven't tested this, but I expect --exit-on-first-error=no to throw an error on versions that don't have this option.
Many thanks! I am going through the comments and will adapt the code accordingly.
Will test it on my macOS Catalina, because valgrind can't be installed there. :)
You mean the previous comment? This one is easiest to test on e.g. a fresh Ubuntu 18.04 machine, which has valgrind 3.13. You can then compile it from source and install 3.15 to see the difference.
Yeah, I have one cheap VPS running, with Ubuntu 18.04. Will follow your recommendation. Thanks!
--exit-on-first-error=yes should always be used if the option --exit-on-first-error is supported :)
@jonatack Would prefer your solution, but because not all old versions support this flag I'll have to stick with the other variant. Will basically return an empty string when the flag should not be active, because this is the default for some older versions anyway.
--exit-on-first-error=yesshould always be used if the option--exit-on-first-erroris supported :)
But the original discussion was about disabling it for v3.15. Or am I missing something?
I'd rename exit_on_first_error to supports_exit_on_first_error for clarity.
Done!
I'd rename
exit_on_first_errortosupports_exit_on_first_errorfor clarity.
Then I will also put the expected version (major, minor) to 3.14, ok? If I understand @practicalswift correctly, every version that supports this flag should also have it activated by default.
If I understand @practicalswift correctly, every version that supports this flag should also have it activated by default.
Correct! :)
It is supported by Valgrind 3.14 and above.
(Unfortunately I said the wrong version number in the linked issue. I've now corrected that. Sorry for the confusion :))
96 | @@ -97,14 +97,16 @@ def __init__(self, i, datadir, *, chain, rpchost, timewait, bitcoind, bitcoin_cl 97 | "-debugexclude=leveldb", 98 | "-uacomment=testnode%d" % i, 99 | ] 100 | - if use_valgrind: 101 | + if self.is_valgrind_present() and use_valgrind:
This won't throw an error if --valgrind is used on a system without valgrind.
This will create an unnecessary valgrind execution in the normal case when Valgrind is not used (no --valgrind).
Better to do it as if use_valgrind and … instead :)
Yes, indeed. Thanks!
107 | default_suppressions_file)
108 | self.args = ["valgrind", "--suppressions={}".format(suppressions_file),
109 | - "--gen-suppressions=all", "--exit-on-first-error=yes",
110 | + "--gen-suppressions=all",
111 | + "--exit-on-first-error=yes" if exit_on_first_error else "--exit-on-first-error=no",
112 | "--error-exitcode=1", "--quiet"] + self.args
nit: suggestion for readability, format the args array like in line 90 above:
- self.args = ["valgrind", "--suppressions={}".format(suppressions_file),
- "--gen-suppressions=all",
- "--exit-on-first-error=yes" if exit_on_first_error else "--exit-on-first-error=no",
- "--error-exitcode=1", "--quiet"] + self.args
+ self.args = [
+ "valgrind",
+ "--suppressions={}".format(suppressions_file),
+ "--gen-suppressions=all",
+ "--exit-on-first-error=yes" if exit_on_first_error else "--exit-on-first-error=no",
+ "--error-exitcode=1",
+ "--quiet",
+ ] + self.args
Yes! Thanks :)
152 | + return False 153 | + except OSError: 154 | + return False 155 | + 156 | + def should_exit_valgrind_on_first_error(self): 157 | + """Determine if valgrind should run with --exit-on-first-error flag (>=3.15 runs with it while older versions don't)"""
I believe this was added in 3.14: https://stackoverflow.com/a/50289059
Will adapt!
Will then simply produce an empty string if the flag should not be active, because it's "no" anyway for versions that support it.
Like: "--exit-on-first-error=yes" if exit_on_first_error else ""
Yes, I have valgrind-3.14.0 installed on debian, and valgrind --help does have the option for --exit-on-first-error=no|yes. Will test the updated version of this PR!
Just force pushed changes. :)
Concept ACK
154 | + return False 155 | + 156 | + def should_exit_valgrind_on_first_error(self): 157 | + """Determine if valgrind should run with --exit-on-first-error flag (>=3.15 runs with it while older versions don't)""" 158 | + version_regex = re.compile('[.bv]') 159 | + version_info = subprocess.run(["valgrind", "--version"], stdout=subprocess.PIPE).stdout.decode("utf-8").rstrip().split("valgrind-")[1:][0]
suggest wrapping long lines like here and the docstring just above
I think this can be simplified. Only one execution of valgrind --version should be needed to a.) check if Valgrind is present and b.) to parse what version it is, so try to merge the logic of is_valgrind_present and should_exit_valgrind_on_first_error.
I suggest get_valgrind_version which could return the tuple (valgrind_major, valgrind_minor) if valgrind is installed or None otherwise.
Something like this:
valgrind_version = get_valgrind_version()
if valgrind_version and valgrind_version >= (3, 14):
# add --exit-on-first-error=true
:)
158 | + version_regex = re.compile('[.bv]') 159 | + version_info = subprocess.run(["valgrind", "--version"], stdout=subprocess.PIPE).stdout.decode("utf-8").rstrip().split("valgrind-")[1:][0] 160 | + version_array = re.split(version_regex, version_info) 161 | + major = int(version_array[0]) 162 | + minor = int(version_array[1]) 163 | + if major >=3 and minor >=15:
suggest hoisting these two magic values to constants
Concept/approach ACK. Could be helpful to describe how to test this in the PR description. A few nits below.
141 | @@ -140,6 +142,29 @@ def __init__(self, i, datadir, *, chain, rpchost, timewait, bitcoind, bitcoin_cl 142 | AddressKeyPair('mzRe8QZMfGi58KyWCse2exxEFry2sfF2Y7', 'cPiRWE8KMjTRxH1MWkPerhfoHFn5iHPWVK5aPqjW8NxmdwenFinJ'), 143 | ] 144 | 145 | + def is_valgrind_present(self): 146 | + """Check for valgrind presence""" 147 | + try: 148 | + version = subprocess.run(["valgrind", "--version"], stdout=subprocess.PIPE).stdout.decode("utf-8") 149 | + if version.find("valgrind", 0, 10) > -1:
if version.startswith("valgrind-") :)
Believe it or not, I was just typing the same changes. Thanks!
Concept ACK on the general idea of enabling --exit-on-first-error for Valgrind version 3.14 and above only :)
33 | @@ -34,7 +34,8 @@ 34 | ) 35 | 36 | BITCOIND_PROC_WAIT_TIMEOUT = 60 37 | - 38 | +VALGRIND_VERSION_MAJOR = 3
nit: VALGRIND_MINIMUM_VERSION_MAJOR
148 | @@ -140,6 +149,23 @@ def __init__(self, i, datadir, *, chain, rpchost, timewait, bitcoind, bitcoin_cl 149 | AddressKeyPair('mzRe8QZMfGi58KyWCse2exxEFry2sfF2Y7', 'cPiRWE8KMjTRxH1MWkPerhfoHFn5iHPWVK5aPqjW8NxmdwenFinJ'), 150 | ] 151 | 152 | + def get_valgrind_version(self): 153 | + """Returns valgrind version (major, minor) in a tuple"""
nit: "or None if not found"
97 | @@ -97,15 +98,23 @@ def __init__(self, i, datadir, *, chain, rpchost, timewait, bitcoind, bitcoin_cl 98 | "-debugexclude=leveldb", 99 | "-uacomment=testnode%d" % i, 100 | ] 101 | + valgrind_version = self.get_valgrind_version()
I'd move all this below if use_valgrind. Don't forget to throw if valgrind_version == None
Ok, will do it. Many thanks!
33 | @@ -34,7 +34,8 @@ 34 | ) 35 | 36 | BITCOIND_PROC_WAIT_TIMEOUT = 60 37 | - 38 | +VALGRIND_VERSION_MAJOR = 3 39 | +VALGRIND_VERSION_MINOR = 14
The scope can be narrowed for these and the meaning could be clarified by doing it like this on L103 instead :)
MIN_VALGRIND_VERSION_EARLY_EXIT = (3, 14)
if valgrind_version and valgrind_version >= MIN_VALGRIND_VERSION_EARLY_EXIT:
148 | @@ -140,6 +149,23 @@ def __init__(self, i, datadir, *, chain, rpchost, timewait, bitcoind, bitcoin_cl 149 | AddressKeyPair('mzRe8QZMfGi58KyWCse2exxEFry2sfF2Y7', 'cPiRWE8KMjTRxH1MWkPerhfoHFn5iHPWVK5aPqjW8NxmdwenFinJ'), 150 | ] 151 | 152 | + def get_valgrind_version(self): 153 | + """Returns valgrind version (major, minor) in a tuple""" 154 | + try: 155 | + rc = subprocess.call(['which', 'valgrind'],
Use shutil.which("valgrind") to avoid shelling out unnecessarily :)
Yup, much better!
156 | + try: 157 | + rc = subprocess.call(['which', 'valgrind'], 158 | + stdout=subprocess.PIPE, 159 | + stderr=subprocess.PIPE) 160 | + if rc == 0: 161 | + version_regex = re.compile('[.bv]')
Have you seen instances where the Valgrind version identifier contains b or v? :) If not, perhaps the simpler version_info.split(".") could do?
Check the indentation here so that it matches the four spaces used in the rest of this file :) Applies throughout this PR.
I must admit that I wasn't sure abut valgrind's versioning. But I will adapt the code of course. Will also update the indentations.
106 | - self.args = ["valgrind", "--suppressions={}".format(suppressions_file), 107 | - "--gen-suppressions=all", "--exit-on-first-error=yes", 108 | - "--error-exitcode=1", "--quiet"] + self.args 109 | + valgrind_version = self.get_valgrind_version() 110 | + exit_on_first_error_flag = "" 111 | + if valgrind_version and valgrind_version >= MIN_VALGRIND_VERSION_EARLY_EXIT:
I think the logic is wrong here. This will only run valgrind if valgrind_version >= MIN_VALGRIND_VERSION_EARLY_EXIT :)
The only impact valgrind_version >= MIN_VALGRIND_VERSION_EARLY_EXIT should have is to enable --exit-on-first-error=yes. self.args should be set both for old and new versions of Valgrind :)
Yeah, just saw it. Will reorganize the code.
119 | + "--suppressions={}".format(suppressions_file), 120 | + "--gen-suppressions=all", 121 | + exit_on_first_error_flag, 122 | + "--error-exitcode=1", 123 | + "--quiet"] + self.args 124 | + elif valgrind_version == None:
I think you want to do this check right after valgrind_version = self.get_valgrind_version(). With the current logic the exception will be thrown also in the case where valgrind is present (but with an older version).
I suggest testing the PR with the following alternatives to make sure all code paths are working as expected:
1.) Without valgrind in PATH
2.) With new valgrind version (>= 3.14) in PATH
3.) With old valgrind version (< 3.14) in PATH
:)
148 | @@ -140,6 +149,20 @@ def __init__(self, i, datadir, *, chain, rpchost, timewait, bitcoind, bitcoin_cl 149 | AddressKeyPair('mzRe8QZMfGi58KyWCse2exxEFry2sfF2Y7', 'cPiRWE8KMjTRxH1MWkPerhfoHFn5iHPWVK5aPqjW8NxmdwenFinJ'), 150 | ] 151 | 152 | + def get_valgrind_version(self):
Suggested alternative:
def get_valgrind_version(self):
"""Returns valgrind version tuple (major, minor), or None if valgrind is not found"""
if not shutil.which("valgrind"):
return None
version = (
subprocess.run(["valgrind", "--version"], stdout=subprocess.PIPE)
.stdout.decode("utf-8")
.rstrip()
.split("valgrind-")[1:][0]
)
return tuple(int(i) for i in version.split(".")[:2])
Much better.
98 | @@ -98,14 +99,23 @@ def __init__(self, i, datadir, *, chain, rpchost, timewait, bitcoind, bitcoin_cl 99 | "-uacomment=testnode%d" % i, 100 | ] 101 | if use_valgrind: 102 | + valgrind_version = self.get_valgrind_version() 103 | + if not valgrind_version: 104 | + raise Exception('Valgrind not present') 105 | + exit_on_first_error_flag = "" 106 | + if valgrind_version and valgrind_version >= MIN_VALGRIND_VERSION_EARLY_EXIT:
This can be reduced to if valgrind_version >= MIN_VALGRIND_VERSION_EARLY_EXIT: since the not valgrind_version case is handled on L103.
19 | @@ -20,6 +20,7 @@ 20 | import collections 21 | import shlex 22 | import sys 23 | +import shutil
nit: alphabetical order
98 | @@ -98,14 +99,23 @@ def __init__(self, i, datadir, *, chain, rpchost, timewait, bitcoind, bitcoin_cl 99 | "-uacomment=testnode%d" % i, 100 | ] 101 | if use_valgrind: 102 | + valgrind_version = self.get_valgrind_version() 103 | + if not valgrind_version: 104 | + raise Exception('Valgrind not present') 105 | + exit_on_first_error_flag = "" 106 | + if valgrind_version >= MIN_VALGRIND_VERSION_EARLY_EXIT: 107 | + exit_on_first_error_flag = "--exit-on-first-error=yes"
consider extracting this to a function like below, or just put "--exit-on-first-error=yes" if version >= MIN_VALGRIND_VERSION_EARLY_EXIT else "" in the array after all :)
- exit_on_first_error_flag = ""
- if valgrind_version >= MIN_VALGRIND_VERSION_EARLY_EXIT:
- exit_on_first_error_flag = "--exit-on-first-error=yes"
self.args = ["valgrind",
"--suppressions={}".format(suppressions_file),
"--gen-suppressions=all",
- exit_on_first_error_flag,
+ self.valgrind_early_exit(valgrind_version),
+ def valgrind_early_exit(self, version):
+ return "--exit-on-first-error=yes" if version >= MIN_VALGRIND_VERSION_EARLY_EXIT else ""
Ok, let's change it! :)
119 | + self.args = ["valgrind", 120 | + "--suppressions={}".format(suppressions_file), 121 | + "--gen-suppressions=all", 122 | + exit_on_first_error_flag, 123 | + "--error-exitcode=1", 124 | + "--quiet"] + self.args
Suggest:
- self.args = ["valgrind",
- "--suppressions={}".format(suppressions_file),
- "--gen-suppressions=all",
- exit_on_first_error_flag,
- "--error-exitcode=1",
- "--quiet"] + self.args
+ self.args = [
+ "valgrind",
+ "--suppressions={}".format(suppressions_file),
+ "--gen-suppressions=all",
+ self.valgrind_early_exit(valgrind_version),
+ "--error-exitcode=1",
+ "--quiet",
+ ] + self.args
Also done. Will do another force push. :)
149 | @@ -140,6 +150,17 @@ def __init__(self, i, datadir, *, chain, rpchost, timewait, bitcoind, bitcoin_cl 150 | AddressKeyPair('mzRe8QZMfGi58KyWCse2exxEFry2sfF2Y7', 'cPiRWE8KMjTRxH1MWkPerhfoHFn5iHPWVK5aPqjW8NxmdwenFinJ'), 151 | ] 152 | 153 | + def get_valgrind_version(self): 154 | + """Returns valgrind version (major, minor) in a tuple or None if not found"""
nit: s/Returns/Return/ like the other docstrings
Ok :)
Tested f10ca18 with valgrind 3.14, the code changes in my comments below (feel free to pick and choose), and this print, which show that the valgrind version is being parsed correctly and the exit flag set.
print("Running tests with valgrind {} and flag {}"
.format(valgrind_version, self.valgrind_early_exit(valgrind_version)))
$ tool_wallet.py --valgrind
2019-12-12T20:09:32.902000Z TestFramework (INFO): Initializing test directory /tmp/bitcoin_func_test_7jnxbiit
Running tests with valgrind (3, 14) and flag --exit-on-first-error=yes
Partial tested ACK 9a61f78
After removing valgrind:
$ test/functional/tool_wallet.py --valgrind
2019-12-12T21:23:04.053000Z TestFramework (INFO): Initializing test directory /tmp/bitcoin_func_test_0212zhle
2019-12-12T21:23:04.054000Z TestFramework (ERROR): Unexpected exception caught during testing
Traceback (most recent call last):
File "/home/jon/projects/bitcoin/bitcoin/test/functional/test_framework/test_framework.py", line 111, in main
self.setup()
File "/home/jon/projects/bitcoin/bitcoin/test/functional/test_framework/test_framework.py", line 226, in setup
self.setup_network()
File "/home/jon/projects/bitcoin/bitcoin/test/functional/test_framework/test_framework.py", line 315, in setup_network
self.setup_nodes()
File "/home/jon/projects/bitcoin/bitcoin/test/functional/test_framework/test_framework.py", line 339, in setup_nodes
self.add_nodes(self.num_nodes, extra_args)
File "/home/jon/projects/bitcoin/bitcoin/test/functional/test_framework/test_framework.py", line 403, in add_nodes
use_valgrind=self.options.valgrind,
File "/home/jon/projects/bitcoin/bitcoin/test/functional/test_framework/test_node.py", line 104, in __init__
raise Exception('Valgrind not present')
Exception: Valgrind not present
After reinstalling valgrind 3.14 and adding this print statement to the bottom of the if use_valgrind block:
print("Running tests with valgrind {} and flag {}"
.format(valgrind_version, self.valgrind_early_exit(valgrind_version)))
$ test/functional/tool_wallet.py --valgrind
2019-12-12T21:24:04.216000Z TestFramework (INFO): Initializing test directory /tmp/bitcoin_func_test_w4dpyzgc
Running tests with valgrind (3, 14) and flag --exit-on-first-error=yes
Not tested: running tests with valgrind version < 3.14.
On macOS I'm seeing the right error when valgrind is missing, but then it goes into an infinite recursion:
2019-12-16T12:47:33.219000Z TestFramework (ERROR): Unexpected exception caught during testing
Traceback (most recent call last):
File "/Users/sjors/dev/bitcoin/test/functional/test_framework/test_framework.py", line 111, in main
self.setup()
File "/Users/sjors/dev/bitcoin/test/functional/test_framework/test_framework.py", line 226, in setup
self.setup_network()
File "test/functional/wallet_dump.py", line 98, in setup_network
self.add_nodes(self.num_nodes, extra_args=self.extra_args)
File "/Users/sjors/dev/bitcoin/test/functional/test_framework/test_framework.py", line 403, in add_nodes
use_valgrind=self.options.valgrind,
File "/Users/sjors/dev/bitcoin/test/functional/test_framework/test_node.py", line 104, in __init__
raise Exception('Valgrind not present')
Exception: Valgrind not present
Exception ignored in: <bound method TestNode.__del__ of <test_framework.test_node.TestNode object at 0x103280be0>>
Traceback (most recent call last):
File "/Users/sjors/dev/bitcoin/test/functional/test_framework/test_node.py", line 182, in __del__
if self.process and self.cleanup_on_exit:
File "/Users/sjors/dev/bitcoin/test/functional/test_framework/test_node.py", line 191, in __getattr__
if self.use_cli:
File "/Users/sjors/dev/bitcoin/test/functional/test_framework/test_node.py", line 191, in __getattr__
if self.use_cli:
...
File "/Users/sjors/dev/bitcoin/test/functional/test_framework/test_node.py", line 191, in __getattr__
if self.use_cli:
RecursionError: maximum recursion depth exceeded
2019-12-16T12:47:33.302000Z TestFramework (INFO): Stopping nodes
On macOS I'm seeing the right error when valgrind is missing, but then it goes into an infinite recursion:
File "/Users/sjors/dev/bitcoin/test/functional/test_framework/test_node.py", line 191, in getattr if self.use_cli: RecursionError: maximum recursion depth exceeded 2019-12-16T12:47:33.302000Z TestFramework (INFO): Stopping nodes
The __ del __ method doesn't have a try-catch block. When I put one it prevents the recursion. Not sure if I should change unrelated code here. Maybe I should wait for input from people who coded it in the first place?
Removing up for grabs. See #17724 (comment).