Belle II Software development
root_returnvalues.py
1#!/usr/bin/env python3
2
3
10
11# this is a test executable, not a module so we don't need doxygen warnings
12# @cond SUPPRESS_DOXYGEN
13
14"""
15Check that ROOT returns the correct values (ints, floats) when calling a C++
16function from python
17"""
18
19import sys
20import ROOT
21import numpy as np
22
23testclass = [
24 "#include <cstdint>",
25 "#include <limits>",
26 "struct ReturnValueTests {"
27]
28
29results = []
30# what integer sizes to check
31integral_types = [8, 16, 32, 64]
32# which floating types to check
33floating_types = ["float", "double"]
34# nan and inf need to be expressed differently so make a mapping
35floating_expressions = {
36 -np.inf: "-std::numeric_limits<{0}>::infinity()",
37 np.inf: "std::numeric_limits<{0}>::infinity()",
38 np.nan: "std::numeric_limits<{0}>::quiet_NaN()",
39}
40
41for width in integral_types:
42 # check signed types, we want min, max -1, 0 and 1
43 dtype = np.dtype(f"int{width}")
44 for i, value in enumerate([-2**(width - 1), -1, 0, 1, 2**(width - 1) - 1]):
45 # create a unique member name
46 func_name = f"int{width}test{i}"
47 # do we need a prefix to the literal?
48 prefix = "ull" if width > 32 else ""
49 # append it to the test class
50 testclass.append(f"int{width}_t {func_name}() const {{ return {value}{prefix}; }}")
51 testclass.append(f"int{width}_t& ref_{func_name}() {{ static int{width}_t val = {value}{prefix}; return val; }}")
52 # and remember name and result for checking
53 results.append([func_name, value, dtype])
54 results.append(['ref_' + func_name, value, dtype])
55
56 # check unsigned types, just 0, 1, and max
57 dtype = np.dtype(f"uint{width}")
58 for i, value in enumerate([0, 1, 2**(width) - 1]):
59 # create a unique member name
60 func_name = f"uint{width}test{i}"
61 # do we need a prefix to the literal?
62 prefix = "ull" if width > 32 else ""
63 # append it to the test class
64 testclass.append(f"uint{width}_t {func_name}() const {{ return {value}{prefix}; }}")
65 testclass.append(f"uint{width}_t& ref_{func_name}() {{ static uint{width}_t val = {value}{prefix}; return val; }}")
66 # and remember name and result for checking
67 results.append([func_name, value, dtype])
68 results.append(['ref_' + func_name, value, dtype])
69
70# now add floating types
71for t in floating_types:
72 # and exploit that numpy offers a numeric_limits equivalent
73 info = np.finfo(t[0])
74
75 # check some values
76 for i, value in enumerate([-np.inf, info.min, -1, 0, info.tiny, info.eps, 1, info.max, np.inf, np.nan]):
77 func_name = f"{t}test{i}"
78 # see if we can just have the literal or if we need a mapping
79 expression = repr(value) if value not in floating_expressions else floating_expressions[value].format(t)
80 testclass.append(f"{t} {func_name}() const {{ return {expression}; }}")
81 testclass.append(f"{t}& ref_{func_name}() {{ static {t} val = {expression}; return val; }}")
82 results.append([func_name, value, info.dtype])
83 results.append(['ref_' + func_name, value, info.dtype])
84
85
86# compile the test class
87testclass.append("};")
88ROOT.gROOT.ProcessLine("\n".join(testclass))
89# get an instance
90tests = ROOT.ReturnValueTests()
91# and do all the checks
92failures = 0
93for func, value, dtype in results:
94 ret = getattr(tests, func)()
95 # char is odd and returns strings instead of int. The simple way of using
96 # ord(string) will loose the sign so we have to convert it to the correct
97 # type using numpy but for that we have to convert the string to 8bit.
98 if isinstance(ret, str):
99 ret = np.fromstring(ret.encode("latin1"), dtype)[0]
100
101 # print the test
102 print(f"check {func}(): {value!r} == {ret!r}: ", end="")
103 # nan needs special care because it cannot be compared to itself
104 if np.isnan(value):
105 passed = np.isnan(ret)
106 else:
107 passed = (ret == value)
108
109 print("\033[32mOK\033[0m" if passed else "\033[31mFAIL\033[0m")
110 if not passed:
111 failures += 1
112
113sys.exit(failures)
114
115# @endcond