File size: 15,888 Bytes
d1ceb73 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 |
import importlib
import codecs
import time
import unicodedata
import pytest
import numpy as np
from numpy.f2py.crackfortran import markinnerspaces, nameargspattern
from . import util
from numpy.f2py import crackfortran
import textwrap
import contextlib
import io
class TestNoSpace(util.F2PyTest):
# issue gh-15035: add handling for endsubroutine, endfunction with no space
# between "end" and the block name
sources = [util.getpath("tests", "src", "crackfortran", "gh15035.f")]
def test_module(self):
k = np.array([1, 2, 3], dtype=np.float64)
w = np.array([1, 2, 3], dtype=np.float64)
self.module.subb(k)
assert np.allclose(k, w + 1)
self.module.subc([w, k])
assert np.allclose(k, w + 1)
assert self.module.t0("23") == b"2"
class TestPublicPrivate:
def test_defaultPrivate(self):
fpath = util.getpath("tests", "src", "crackfortran", "privatemod.f90")
mod = crackfortran.crackfortran([str(fpath)])
assert len(mod) == 1
mod = mod[0]
assert "private" in mod["vars"]["a"]["attrspec"]
assert "public" not in mod["vars"]["a"]["attrspec"]
assert "private" in mod["vars"]["b"]["attrspec"]
assert "public" not in mod["vars"]["b"]["attrspec"]
assert "private" not in mod["vars"]["seta"]["attrspec"]
assert "public" in mod["vars"]["seta"]["attrspec"]
def test_defaultPublic(self, tmp_path):
fpath = util.getpath("tests", "src", "crackfortran", "publicmod.f90")
mod = crackfortran.crackfortran([str(fpath)])
assert len(mod) == 1
mod = mod[0]
assert "private" in mod["vars"]["a"]["attrspec"]
assert "public" not in mod["vars"]["a"]["attrspec"]
assert "private" not in mod["vars"]["seta"]["attrspec"]
assert "public" in mod["vars"]["seta"]["attrspec"]
def test_access_type(self, tmp_path):
fpath = util.getpath("tests", "src", "crackfortran", "accesstype.f90")
mod = crackfortran.crackfortran([str(fpath)])
assert len(mod) == 1
tt = mod[0]['vars']
assert set(tt['a']['attrspec']) == {'private', 'bind(c)'}
assert set(tt['b_']['attrspec']) == {'public', 'bind(c)'}
assert set(tt['c']['attrspec']) == {'public'}
def test_nowrap_private_proceedures(self, tmp_path):
fpath = util.getpath("tests", "src", "crackfortran", "gh23879.f90")
mod = crackfortran.crackfortran([str(fpath)])
assert len(mod) == 1
pyf = crackfortran.crack2fortran(mod)
assert 'bar' not in pyf
class TestModuleProcedure:
def test_moduleOperators(self, tmp_path):
fpath = util.getpath("tests", "src", "crackfortran", "operators.f90")
mod = crackfortran.crackfortran([str(fpath)])
assert len(mod) == 1
mod = mod[0]
assert "body" in mod and len(mod["body"]) == 9
assert mod["body"][1]["name"] == "operator(.item.)"
assert "implementedby" in mod["body"][1]
assert mod["body"][1]["implementedby"] == \
["item_int", "item_real"]
assert mod["body"][2]["name"] == "operator(==)"
assert "implementedby" in mod["body"][2]
assert mod["body"][2]["implementedby"] == ["items_are_equal"]
assert mod["body"][3]["name"] == "assignment(=)"
assert "implementedby" in mod["body"][3]
assert mod["body"][3]["implementedby"] == \
["get_int", "get_real"]
def test_notPublicPrivate(self, tmp_path):
fpath = util.getpath("tests", "src", "crackfortran", "pubprivmod.f90")
mod = crackfortran.crackfortran([str(fpath)])
assert len(mod) == 1
mod = mod[0]
assert mod['vars']['a']['attrspec'] == ['private', ]
assert mod['vars']['b']['attrspec'] == ['public', ]
assert mod['vars']['seta']['attrspec'] == ['public', ]
class TestExternal(util.F2PyTest):
# issue gh-17859: add external attribute support
sources = [util.getpath("tests", "src", "crackfortran", "gh17859.f")]
def test_external_as_statement(self):
def incr(x):
return x + 123
r = self.module.external_as_statement(incr)
assert r == 123
def test_external_as_attribute(self):
def incr(x):
return x + 123
r = self.module.external_as_attribute(incr)
assert r == 123
class TestCrackFortran(util.F2PyTest):
# gh-2848: commented lines between parameters in subroutine parameter lists
sources = [util.getpath("tests", "src", "crackfortran", "gh2848.f90")]
def test_gh2848(self):
r = self.module.gh2848(1, 2)
assert r == (1, 2)
class TestMarkinnerspaces:
# gh-14118: markinnerspaces does not handle multiple quotations
def test_do_not_touch_normal_spaces(self):
test_list = ["a ", " a", "a b c", "'abcdefghij'"]
for i in test_list:
assert markinnerspaces(i) == i
def test_one_relevant_space(self):
assert markinnerspaces("a 'b c' \\' \\'") == "a 'b@_@c' \\' \\'"
assert markinnerspaces(r'a "b c" \" \"') == r'a "b@_@c" \" \"'
def test_ignore_inner_quotes(self):
assert markinnerspaces("a 'b c\" \" d' e") == "a 'b@_@c\"@_@\"@_@d' e"
assert markinnerspaces("a \"b c' ' d\" e") == "a \"b@_@c'@_@'@_@d\" e"
def test_multiple_relevant_spaces(self):
assert markinnerspaces("a 'b c' 'd e'") == "a 'b@_@c' 'd@_@e'"
assert markinnerspaces(r'a "b c" "d e"') == r'a "b@_@c" "d@_@e"'
class TestDimSpec(util.F2PyTest):
"""This test suite tests various expressions that are used as dimension
specifications.
There exists two usage cases where analyzing dimensions
specifications are important.
In the first case, the size of output arrays must be defined based
on the inputs to a Fortran function. Because Fortran supports
arbitrary bases for indexing, for instance, `arr(lower:upper)`,
f2py has to evaluate an expression `upper - lower + 1` where
`lower` and `upper` are arbitrary expressions of input parameters.
The evaluation is performed in C, so f2py has to translate Fortran
expressions to valid C expressions (an alternative approach is
that a developer specifies the corresponding C expressions in a
.pyf file).
In the second case, when user provides an input array with a given
size but some hidden parameters used in dimensions specifications
need to be determined based on the input array size. This is a
harder problem because f2py has to solve the inverse problem: find
a parameter `p` such that `upper(p) - lower(p) + 1` equals to the
size of input array. In the case when this equation cannot be
solved (e.g. because the input array size is wrong), raise an
error before calling the Fortran function (that otherwise would
likely crash Python process when the size of input arrays is
wrong). f2py currently supports this case only when the equation
is linear with respect to unknown parameter.
"""
suffix = ".f90"
code_template = textwrap.dedent("""
function get_arr_size_{count}(a, n) result (length)
integer, intent(in) :: n
integer, dimension({dimspec}), intent(out) :: a
integer length
length = size(a)
end function
subroutine get_inv_arr_size_{count}(a, n)
integer :: n
! the value of n is computed in f2py wrapper
!f2py intent(out) n
integer, dimension({dimspec}), intent(in) :: a
if (a({first}).gt.0) then
! print*, "a=", a
endif
end subroutine
""")
linear_dimspecs = [
"n", "2*n", "2:n", "n/2", "5 - n/2", "3*n:20", "n*(n+1):n*(n+5)",
"2*n, n"
]
nonlinear_dimspecs = ["2*n:3*n*n+2*n"]
all_dimspecs = linear_dimspecs + nonlinear_dimspecs
code = ""
for count, dimspec in enumerate(all_dimspecs):
lst = [(d.split(":")[0] if ":" in d else "1") for d in dimspec.split(',')]
code += code_template.format(
count=count,
dimspec=dimspec,
first=", ".join(lst),
)
@pytest.mark.parametrize("dimspec", all_dimspecs)
@pytest.mark.slow
def test_array_size(self, dimspec):
count = self.all_dimspecs.index(dimspec)
get_arr_size = getattr(self.module, f"get_arr_size_{count}")
for n in [1, 2, 3, 4, 5]:
sz, a = get_arr_size(n)
assert a.size == sz
@pytest.mark.parametrize("dimspec", all_dimspecs)
def test_inv_array_size(self, dimspec):
count = self.all_dimspecs.index(dimspec)
get_arr_size = getattr(self.module, f"get_arr_size_{count}")
get_inv_arr_size = getattr(self.module, f"get_inv_arr_size_{count}")
for n in [1, 2, 3, 4, 5]:
sz, a = get_arr_size(n)
if dimspec in self.nonlinear_dimspecs:
# one must specify n as input, the call we'll ensure
# that a and n are compatible:
n1 = get_inv_arr_size(a, n)
else:
# in case of linear dependence, n can be determined
# from the shape of a:
n1 = get_inv_arr_size(a)
# n1 may be different from n (for instance, when `a` size
# is a function of some `n` fraction) but it must produce
# the same sized array
sz1, _ = get_arr_size(n1)
assert sz == sz1, (n, n1, sz, sz1)
class TestModuleDeclaration:
def test_dependencies(self, tmp_path):
fpath = util.getpath("tests", "src", "crackfortran", "foo_deps.f90")
mod = crackfortran.crackfortran([str(fpath)])
assert len(mod) == 1
assert mod[0]["vars"]["abar"]["="] == "bar('abar')"
class TestEval(util.F2PyTest):
def test_eval_scalar(self):
eval_scalar = crackfortran._eval_scalar
assert eval_scalar('123', {}) == '123'
assert eval_scalar('12 + 3', {}) == '15'
assert eval_scalar('a + b', dict(a=1, b=2)) == '3'
assert eval_scalar('"123"', {}) == "'123'"
class TestFortranReader(util.F2PyTest):
@pytest.mark.parametrize("encoding",
['ascii', 'utf-8', 'utf-16', 'utf-32'])
def test_input_encoding(self, tmp_path, encoding):
# gh-635
f_path = tmp_path / f"input_with_{encoding}_encoding.f90"
with f_path.open('w', encoding=encoding) as ff:
ff.write("""
subroutine foo()
end subroutine foo
""")
mod = crackfortran.crackfortran([str(f_path)])
assert mod[0]['name'] == 'foo'
@pytest.mark.slow
class TestUnicodeComment(util.F2PyTest):
sources = [util.getpath("tests", "src", "crackfortran", "unicode_comment.f90")]
@pytest.mark.skipif(
(importlib.util.find_spec("charset_normalizer") is None),
reason="test requires charset_normalizer which is not installed",
)
def test_encoding_comment(self):
self.module.foo(3)
class TestNameArgsPatternBacktracking:
@pytest.mark.parametrize(
['adversary'],
[
('@)@bind@(@',),
('@)@bind @(@',),
('@)@bind foo bar baz@(@',)
]
)
def test_nameargspattern_backtracking(self, adversary):
'''address ReDOS vulnerability:
https://github.com/numpy/numpy/issues/23338'''
trials_per_batch = 12
batches_per_regex = 4
start_reps, end_reps = 15, 25
for ii in range(start_reps, end_reps):
repeated_adversary = adversary * ii
# test times in small batches.
# this gives us more chances to catch a bad regex
# while still catching it before too long if it is bad
for _ in range(batches_per_regex):
times = []
for _ in range(trials_per_batch):
t0 = time.perf_counter()
mtch = nameargspattern.search(repeated_adversary)
times.append(time.perf_counter() - t0)
# our pattern should be much faster than 0.2s per search
# it's unlikely that a bad regex will pass even on fast CPUs
assert np.median(times) < 0.2
assert not mtch
# if the adversary is capped with @)@, it becomes acceptable
# according to the old version of the regex.
# that should still be true.
good_version_of_adversary = repeated_adversary + '@)@'
assert nameargspattern.search(good_version_of_adversary)
class TestFunctionReturn(util.F2PyTest):
sources = [util.getpath("tests", "src", "crackfortran", "gh23598.f90")]
@pytest.mark.slow
def test_function_rettype(self):
# gh-23598
assert self.module.intproduct(3, 4) == 12
class TestFortranGroupCounters(util.F2PyTest):
def test_end_if_comment(self):
# gh-23533
fpath = util.getpath("tests", "src", "crackfortran", "gh23533.f")
try:
crackfortran.crackfortran([str(fpath)])
except Exception as exc:
assert False, f"'crackfortran.crackfortran' raised an exception {exc}"
class TestF77CommonBlockReader:
def test_gh22648(self, tmp_path):
fpath = util.getpath("tests", "src", "crackfortran", "gh22648.pyf")
with contextlib.redirect_stdout(io.StringIO()) as stdout_f2py:
mod = crackfortran.crackfortran([str(fpath)])
assert "Mismatch" not in stdout_f2py.getvalue()
class TestParamEval:
# issue gh-11612, array parameter parsing
def test_param_eval_nested(self):
v = '(/3.14, 4./)'
g_params = dict(kind=crackfortran._kind_func,
selected_int_kind=crackfortran._selected_int_kind_func,
selected_real_kind=crackfortran._selected_real_kind_func)
params = {'dp': 8, 'intparamarray': {1: 3, 2: 5},
'nested': {1: 1, 2: 2, 3: 3}}
dimspec = '(2)'
ret = crackfortran.param_eval(v, g_params, params, dimspec=dimspec)
assert ret == {1: 3.14, 2: 4.0}
def test_param_eval_nonstandard_range(self):
v = '(/ 6, 3, 1 /)'
g_params = dict(kind=crackfortran._kind_func,
selected_int_kind=crackfortran._selected_int_kind_func,
selected_real_kind=crackfortran._selected_real_kind_func)
params = {}
dimspec = '(-1:1)'
ret = crackfortran.param_eval(v, g_params, params, dimspec=dimspec)
assert ret == {-1: 6, 0: 3, 1: 1}
def test_param_eval_empty_range(self):
v = '6'
g_params = dict(kind=crackfortran._kind_func,
selected_int_kind=crackfortran._selected_int_kind_func,
selected_real_kind=crackfortran._selected_real_kind_func)
params = {}
dimspec = ''
pytest.raises(ValueError, crackfortran.param_eval, v, g_params, params,
dimspec=dimspec)
def test_param_eval_non_array_param(self):
v = '3.14_dp'
g_params = dict(kind=crackfortran._kind_func,
selected_int_kind=crackfortran._selected_int_kind_func,
selected_real_kind=crackfortran._selected_real_kind_func)
params = {}
ret = crackfortran.param_eval(v, g_params, params, dimspec=None)
assert ret == '3.14_dp'
def test_param_eval_too_many_dims(self):
v = 'reshape((/ (i, i=1, 250) /), (/5, 10, 5/))'
g_params = dict(kind=crackfortran._kind_func,
selected_int_kind=crackfortran._selected_int_kind_func,
selected_real_kind=crackfortran._selected_real_kind_func)
params = {}
dimspec = '(0:4, 3:12, 5)'
pytest.raises(ValueError, crackfortran.param_eval, v, g_params, params,
dimspec=dimspec)
|