| import importlib |
| import time |
| 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): |
| |
| |
| 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): |
| |
| 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): |
| |
| sources = [util.getpath("tests", "src", "crackfortran", "gh2848.f90"), |
| util.getpath("tests", "src", "crackfortran", "common_with_division.f") |
| ] |
|
|
| def test_gh2848(self): |
| r = self.module.gh2848(1, 2) |
| assert r == (1, 2) |
|
|
| def test_common_with_division(self): |
| assert len(self.module.mortmp.ctmp) == 11 |
|
|
| class TestMarkinnerspaces: |
| |
|
|
| 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: |
| |
| |
| n1 = get_inv_arr_size(a, n) |
| else: |
| |
| |
| n1 = get_inv_arr_size(a) |
| |
| |
| |
| 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): |
| |
| 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 |
| |
| |
| |
| 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) |
| |
| |
| assert np.median(times) < 0.2 |
| assert not mtch |
| |
| |
| |
| 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): |
| |
| assert self.module.intproduct(3, 4) == 12 |
|
|
|
|
| class TestFortranGroupCounters(util.F2PyTest): |
| def test_end_if_comment(self): |
| |
| 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: |
| |
| 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) |
|
|
| @pytest.mark.slow |
| class TestLowerF2PYDirective(util.F2PyTest): |
| sources = [util.getpath("tests", "src", "crackfortran", "gh27697.f90")] |
| options = ['--lower'] |
|
|
| def test_no_lower_fail(self): |
| with pytest.raises(ValueError, match='aborting directly') as exc: |
| self.module.utils.my_abort('aborting directly') |
|
|