From 00ad174d1a21fd335abbcd1e2aa2e47e7e4f1515 Mon Sep 17 00:00:00 2001 From: Eneko Martin Martinez <61285767+enekomartinmartinez@users.noreply.github.com> Date: Mon, 9 Aug 2021 15:50:40 +0200 Subject: [PATCH 1/3] Move CI tests to GitHub Actions (#288) * Add CI * Add verbosity * Update relative to coverage * Recover package name for source * Run the tests from any folder * Move CI --- .github/workflows/ci.yml | 33 ++++ .travis.yml | 18 -- dev-requirements.txt | 5 + setup.py | 4 + tests/Makefile | 12 +- tests/integration_test_vensim_pathway.py | 209 ++++++++++++----------- tests/integration_test_xmile_pathway.py | 151 ++++++++-------- tests/pytest.ini | 2 + tests/unit_test_benchmarking.py | 34 ++-- tests/unit_test_cli.py | 36 ++-- tests/unit_test_external.py | 11 +- tests/unit_test_pysd.py | 115 ++++++++----- tests/unit_test_table2py.py | 6 +- tests/unit_test_xmile2py.py | 4 +- 14 files changed, 364 insertions(+), 276 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .travis.yml create mode 100644 dev-requirements.txt create mode 100644 tests/pytest.ini diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..665254c2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,33 @@ +# Run CI tests with pytest and update coverage to coveralls + +name: CI + +on: [push, pull_request] + +jobs: + test: + #runs-on: ${{ matrix.os }} + runs-on: ubuntu-latest + strategy: + matrix: + #os: [ubuntu-latest, macos-latest, windows-latest] + python-version: [3.7, 3.9] + + steps: + - uses: actions/checkout@v2 + with: + submodules: recursive + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: pip install -e .[test] + - name: Test and coverage + run: pytest tests/ --cov=pysd + - name: Coveralls + if: matrix.python-version == 3.7 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: coveralls --service=github + diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 8982ab48..00000000 --- a/.travis.yml +++ /dev/null @@ -1,18 +0,0 @@ -language: python -python: - - "3.7" - - "3.9" -# command to install dependencies -cache: pip -install: - - pip install cython - - pip install --upgrade pip setuptools wheel - - pip install -e . - - pip install psutil - - pip install pytest pytest-cov - - pip install coveralls -# command to run tests -script: - - cd tests - - pytest --cov=pysd *.py - - coveralls diff --git a/dev-requirements.txt b/dev-requirements.txt new file mode 100644 index 00000000..34790784 --- /dev/null +++ b/dev-requirements.txt @@ -0,0 +1,5 @@ +pytest +pytest-cov +coverage +coveralls +psutil diff --git a/setup.py b/setup.py index c19b7f15..f0ceb264 100755 --- a/setup.py +++ b/setup.py @@ -3,6 +3,8 @@ exec(open('pysd/_version.py').read()) print(__version__) +test_pckgs = open('dev-requirements.txt').read().strip().split('\n') + setup( name='pysd', version=__version__, @@ -28,6 +30,8 @@ 'Programming Language :: Python :: 3.9', ], install_requires=open('requirements.txt').read().strip().split('\n'), + tests_require=test_pckgs, + extras_require={"test": test_pckgs}, package_data={ 'py_backend': [ 'xmile/smile.grammar' diff --git a/tests/Makefile b/tests/Makefile index 34fff190..648c2e27 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -15,22 +15,22 @@ clean: tests: clean ifeq ($(NUM_PROC), 1) - pytest *.py + pytest else - pytest *.py -n $(NUM_PROC) + pytest -n $(NUM_PROC) endif cover: clean ifeq ($(NUM_PROC), 1) - pytest --cov=pysd --cov-report term *.py + pytest --cov=pysd --cov-report term else - pytest --cov=pysd --cov-report term *.py -n $(NUM_PROC) + pytest --cov=pysd --cov-report term -n $(NUM_PROC) endif coverhtml: clean ifeq ($(NUM_PROC), 1) - pytest --cov=pysd --cov-report html --cov-report term *.py + pytest --cov=pysd --cov-report html --cov-report term else - pytest --cov=pysd --cov-report html --cov-report term *.py -n $(NUM_PROC) + pytest --cov=pysd --cov-report html --cov-report term -n $(NUM_PROC) endif diff --git a/tests/integration_test_vensim_pathway.py b/tests/integration_test_vensim_pathway.py index c34f867a..d560df0f 100644 --- a/tests/integration_test_vensim_pathway.py +++ b/tests/integration_test_vensim_pathway.py @@ -3,97 +3,100 @@ Note that this file is autogenerated by `integration_test_factory.py` and changes are likely to be overwritten. """ - +import os import warnings import unittest from pysd.tools.benchmarking import runner, assert_frames_close rtol = .05 +_root = os.path.dirname(__file__) +test_models = os.path.join(_root, "test-models/tests") + class TestIntegrationExamples(unittest.TestCase): def test_abs(self): - output, canon = runner('test-models/tests/abs/test_abs.mdl') + output, canon = runner(test_models + '/abs/test_abs.mdl') assert_frames_close(output, canon, rtol=rtol) def test_active_initial(self): - output, canon = runner('test-models/tests/active_initial/test_active_initial.mdl') + output, canon = runner(test_models + '/active_initial/test_active_initial.mdl') assert_frames_close(output, canon, rtol=rtol) def test_arguments(self): with warnings.catch_warnings(): warnings.simplefilter("ignore") - output, canon = runner('test-models/tests/arguments/test_arguments.mdl') + output, canon = runner(test_models + '/arguments/test_arguments.mdl') assert_frames_close(output, canon, rtol=rtol) def test_builtin_max(self): - output, canon = runner('test-models/tests/builtin_max/builtin_max.mdl') + output, canon = runner(test_models + '/builtin_max/builtin_max.mdl') assert_frames_close(output, canon, rtol=rtol) def test_builtin_min(self): - output, canon = runner('test-models/tests/builtin_min/builtin_min.mdl') + output, canon = runner(test_models + '/builtin_min/builtin_min.mdl') assert_frames_close(output, canon, rtol=rtol) def test_chained_initialization(self): - output, canon = runner('test-models/tests/chained_initialization/test_chained_initialization.mdl') + output, canon = runner(test_models + '/chained_initialization/test_chained_initialization.mdl') assert_frames_close(output, canon, rtol=rtol) def test_constant_expressions(self): - output, canon = runner('test-models/tests/constant_expressions/test_constant_expressions.mdl') + output, canon = runner(test_models + '/constant_expressions/test_constant_expressions.mdl') assert_frames_close(output, canon, rtol=rtol) def test_delay_fixed(self): # issue https://github.com/JamesPHoughton/pysd/issues/147 with warnings.catch_warnings(): warnings.simplefilter("ignore") - output, canon = runner('test-models/tests/delay_fixed/test_delay_fixed.mdl') + output, canon = runner(test_models + '/delay_fixed/test_delay_fixed.mdl') assert_frames_close(output, canon, rtol=rtol) def test_delay_numeric_error(self): # issue https://github.com/JamesPHoughton/pysd/issues/225 - output, canon = runner('test-models/tests/delay_numeric_error/test_delay_numeric_error.mdl') + output, canon = runner(test_models + '/delay_numeric_error/test_delay_numeric_error.mdl') assert_frames_close(output, canon, rtol=rtol) def test_delay_parentheses(self): - output, canon = runner('test-models/tests/delay_parentheses/test_delay_parentheses.mdl') + output, canon = runner(test_models + '/delay_parentheses/test_delay_parentheses.mdl') assert_frames_close(output, canon, rtol=rtol) def test_delay_pipeline(self): # issue https://github.com/JamesPHoughton/pysd/issues/147 with warnings.catch_warnings(): warnings.simplefilter("ignore") - output, canon = runner('test-models/tests/delay_pipeline/test_pipeline_delays.mdl') + output, canon = runner(test_models + '/delay_pipeline/test_pipeline_delays.mdl') assert_frames_close(output, canon, rtol=rtol) def test_delays(self): # issue https://github.com/JamesPHoughton/pysd/issues/147 - output, canon = runner('test-models/tests/delays/test_delays.mdl') + output, canon = runner(test_models + '/delays/test_delays.mdl') assert_frames_close(output, canon, rtol=rtol) def test_dynamic_final_time(self): # issue https://github.com/JamesPHoughton/pysd/issues/278 - output, canon = runner('test-models/tests/dynamic_final_time/test_dynamic_final_time.mdl') + output, canon = runner(test_models + '/dynamic_final_time/test_dynamic_final_time.mdl') assert_frames_close(output, canon, rtol=rtol) def test_euler_step_vs_saveper(self): - output, canon = runner('test-models/tests/euler_step_vs_saveper/test_euler_step_vs_saveper.mdl') + output, canon = runner(test_models + '/euler_step_vs_saveper/test_euler_step_vs_saveper.mdl') assert_frames_close(output, canon, rtol=rtol) def test_exp(self): - output, canon = runner('test-models/tests/exp/test_exp.mdl') + output, canon = runner(test_models + '/exp/test_exp.mdl') assert_frames_close(output, canon, rtol=rtol) def test_exponentiation(self): - output, canon = runner('test-models/tests/exponentiation/exponentiation.mdl') + output, canon = runner(test_models + '/exponentiation/exponentiation.mdl') assert_frames_close(output, canon, rtol=rtol) def test_function_capitalization(self): - output, canon = runner('test-models/tests/function_capitalization/test_function_capitalization.mdl') + output, canon = runner(test_models + '/function_capitalization/test_function_capitalization.mdl') assert_frames_close(output, canon, rtol=rtol) def test_game(self): - output, canon = runner('test-models/tests/game/test_game.mdl') + output, canon = runner(test_models + '/game/test_game.mdl') assert_frames_close(output, canon, rtol=rtol) def test_get_data_args_3d_xls(self): @@ -104,7 +107,7 @@ def test_get_data_args_3d_xls(self): good working of the builder """ output, canon = runner( - 'test-models/tests/get_data_args_3d_xls/' + test_models + '/get_data_args_3d_xls/' + 'test_get_data_args_3d_xls.mdl' ) assert_frames_close(output, canon, rtol=rtol) @@ -117,7 +120,7 @@ def test_get_lookups_data_3d_xls(self): good working of the builder """ output, canon = runner( - 'test-models/tests/get_lookups_data_3d_xls/' + test_models + '/get_lookups_data_3d_xls/' + 'test_get_lookups_data_3d_xls.mdl' ) assert_frames_close(output, canon, rtol=rtol) @@ -126,14 +129,14 @@ def test_get_lookups_subscripted_args(self): with warnings.catch_warnings(): warnings.simplefilter("ignore") output, canon = runner( - 'test-models/tests/get_lookups_subscripted_args/' + test_models + '/get_lookups_subscripted_args/' + 'test_get_lookups_subscripted_args.mdl' ) assert_frames_close(output, canon, rtol=rtol) def test_get_lookups_subset(self): output, canon = runner( - 'test-models/tests/get_lookups_subset/' + test_models + '/get_lookups_subset/' + 'test_get_lookups_subset.mdl' ) assert_frames_close(output, canon, rtol=rtol) @@ -142,7 +145,7 @@ def test_get_with_missing_values_xlsx(self): with warnings.catch_warnings(): warnings.simplefilter("ignore") output, canon = runner( - 'test-models/tests/get_with_missing_values_xlsx/' + test_models + '/get_with_missing_values_xlsx/' + 'test_get_with_missing_values_xlsx.mdl' ) @@ -150,7 +153,7 @@ def test_get_with_missing_values_xlsx(self): def test_get_mixed_definitions(self): output, canon = runner( - 'test-models/tests/get_mixed_definitions/' + test_models + '/get_mixed_definitions/' + 'test_get_mixed_definitions.mdl' ) assert_frames_close(output, canon, rtol=rtol) @@ -163,333 +166,333 @@ def test_get_subscript_3d_arrays_xls(self): good working of the builder """ output, canon = runner( - 'test-models/tests/get_subscript_3d_arrays_xls/' + test_models + '/get_subscript_3d_arrays_xls/' + 'test_get_subscript_3d_arrays_xls.mdl' ) assert_frames_close(output, canon, rtol=rtol) def test_get_xls_cellrange(self): output, canon = runner( - 'test-models/tests/get_xls_cellrange/' + test_models + '/get_xls_cellrange/' + 'test_get_xls_cellrange.mdl' ) assert_frames_close(output, canon, rtol=rtol) def test_if_stmt(self): - output, canon = runner('test-models/tests/if_stmt/if_stmt.mdl') + output, canon = runner(test_models + '/if_stmt/if_stmt.mdl') assert_frames_close(output, canon, rtol=rtol) def test_initial_function(self): - output, canon = runner('test-models/tests/initial_function/test_initial.mdl') + output, canon = runner(test_models + '/initial_function/test_initial.mdl') assert_frames_close(output, canon, rtol=rtol) def test_input_functions(self): - output, canon = runner('test-models/tests/input_functions/test_inputs.mdl') + output, canon = runner(test_models + '/input_functions/test_inputs.mdl') assert_frames_close(output, canon, rtol=rtol) def test_limits(self): - output, canon = runner('test-models/tests/limits/test_limits.mdl') + output, canon = runner(test_models + '/limits/test_limits.mdl') assert_frames_close(output, canon, rtol=rtol) def test_line_breaks(self): - output, canon = runner('test-models/tests/line_breaks/test_line_breaks.mdl') + output, canon = runner(test_models + '/line_breaks/test_line_breaks.mdl') assert_frames_close(output, canon, rtol=rtol) def test_line_continuation(self): - output, canon = runner('test-models/tests/line_continuation/test_line_continuation.mdl') + output, canon = runner(test_models + '/line_continuation/test_line_continuation.mdl') assert_frames_close(output, canon, rtol=rtol) def test_ln(self): - output, canon = runner('test-models/tests/ln/test_ln.mdl') + output, canon = runner(test_models + '/ln/test_ln.mdl') assert_frames_close(output, canon, rtol=rtol) def test_log(self): - output, canon = runner('test-models/tests/log/test_log.mdl') + output, canon = runner(test_models + '/log/test_log.mdl') assert_frames_close(output, canon, rtol=rtol) def test_logicals(self): - output, canon = runner('test-models/tests/logicals/test_logicals.mdl') + output, canon = runner(test_models + '/logicals/test_logicals.mdl') assert_frames_close(output, canon, rtol=rtol) def test_lookups(self): - output, canon = runner('test-models/tests/lookups/test_lookups.mdl') + output, canon = runner(test_models + '/lookups/test_lookups.mdl') assert_frames_close(output, canon, rtol=rtol) def test_lookups_without_range(self): - output, canon = runner('test-models/tests/lookups_without_range/test_lookups_without_range.mdl') + output, canon = runner(test_models + '/lookups_without_range/test_lookups_without_range.mdl') assert_frames_close(output, canon, rtol=rtol) def test_lookups_funcnames(self): - output, canon = runner('test-models/tests/lookups_funcnames/test_lookups_funcnames.mdl') + output, canon = runner(test_models + '/lookups_funcnames/test_lookups_funcnames.mdl') assert_frames_close(output, canon, rtol=rtol) def test_lookups_inline(self): - output, canon = runner('test-models/tests/lookups_inline/test_lookups_inline.mdl') + output, canon = runner(test_models + '/lookups_inline/test_lookups_inline.mdl') assert_frames_close(output, canon, rtol=rtol) def test_lookups_inline_bounded(self): - output, canon = runner('test-models/tests/lookups_inline_bounded/test_lookups_inline_bounded.mdl') + output, canon = runner(test_models + '/lookups_inline_bounded/test_lookups_inline_bounded.mdl') assert_frames_close(output, canon, rtol=rtol) def test_lookups_with_expr(self): - output, canon = runner('test-models/tests/lookups_with_expr/test_lookups_with_expr.mdl') + output, canon = runner(test_models + '/lookups_with_expr/test_lookups_with_expr.mdl') assert_frames_close(output, canon, rtol=rtol) def test_macro_cross_reference(self): - output, canon = runner('test-models/tests/macro_cross_reference/test_macro_cross_reference.mdl') + output, canon = runner(test_models + '/macro_cross_reference/test_macro_cross_reference.mdl') assert_frames_close(output, canon, rtol=rtol) def test_macro_expression(self): - output, canon = runner('test-models/tests/macro_expression/test_macro_expression.mdl') + output, canon = runner(test_models + '/macro_expression/test_macro_expression.mdl') assert_frames_close(output, canon, rtol=rtol) def test_macro_multi_expression(self): - output, canon = runner('test-models/tests/macro_multi_expression/test_macro_multi_expression.mdl') + output, canon = runner(test_models + '/macro_multi_expression/test_macro_multi_expression.mdl') assert_frames_close(output, canon, rtol=rtol) def test_macro_multi_macros(self): - output, canon = runner('test-models/tests/macro_multi_macros/test_macro_multi_macros.mdl') + output, canon = runner(test_models + '/macro_multi_macros/test_macro_multi_macros.mdl') assert_frames_close(output, canon, rtol=rtol) @unittest.skip('working') def test_macro_output(self): - output, canon = runner('test-models/tests/macro_output/test_macro_output.mdl') + output, canon = runner(test_models + '/macro_output/test_macro_output.mdl') assert_frames_close(output, canon, rtol=rtol) def test_macro_stock(self): - output, canon = runner('test-models/tests/macro_stock/test_macro_stock.mdl') + output, canon = runner(test_models + '/macro_stock/test_macro_stock.mdl') assert_frames_close(output, canon, rtol=rtol) @unittest.skip('do we need this?') def test_macro_trailing_definition(self): - output, canon = runner('test-models/tests/macro_trailing_definition/test_macro_trailing_definition.mdl') + output, canon = runner(test_models + '/macro_trailing_definition/test_macro_trailing_definition.mdl') assert_frames_close(output, canon, rtol=rtol) def test_model_doc(self): - output, canon = runner('test-models/tests/model_doc/model_doc.mdl') + output, canon = runner(test_models + '/model_doc/model_doc.mdl') assert_frames_close(output, canon, rtol=rtol) def test_nested_functions(self): - output, canon = runner('test-models/tests/nested_functions/test_nested_functions.mdl') + output, canon = runner(test_models + '/nested_functions/test_nested_functions.mdl') assert_frames_close(output, canon, rtol=rtol) def test_number_handling(self): - output, canon = runner('test-models/tests/number_handling/test_number_handling.mdl') + output, canon = runner(test_models + '/number_handling/test_number_handling.mdl') assert_frames_close(output, canon, rtol=rtol) def test_parentheses(self): - output, canon = runner('test-models/tests/parentheses/test_parens.mdl') + output, canon = runner(test_models + '/parentheses/test_parens.mdl') assert_frames_close(output, canon, rtol=rtol) @unittest.skip('low priority') def test_reference_capitalization(self): """A properly formatted Vensim model should never create this failure""" - output, canon = runner('test-models/tests/reference_capitalization/test_reference_capitalization.mdl') + output, canon = runner(test_models + '/reference_capitalization/test_reference_capitalization.mdl') assert_frames_close(output, canon, rtol=rtol) def test_repeated_subscript(self): with warnings.catch_warnings(): warnings.simplefilter("ignore") - output, canon = runner('test-models/tests/repeated_subscript/test_repeated_subscript.mdl') + output, canon = runner(test_models + '/repeated_subscript/test_repeated_subscript.mdl') assert_frames_close(output, canon, rtol=rtol) @unittest.skip('in branch') def test_rounding(self): - output, canon = runner('test-models/tests/rounding/test_rounding.mdl') + output, canon = runner(test_models + '/rounding/test_rounding.mdl') assert_frames_close(output, canon, rtol=rtol) def test_sample_if_true(self): - output, canon = runner('test-models/tests/sample_if_true/test_sample_if_true.mdl') + output, canon = runner(test_models + '/sample_if_true/test_sample_if_true.mdl') assert_frames_close(output, canon, rtol=rtol) def test_smooth(self): - output, canon = runner('test-models/tests/smooth/test_smooth.mdl') + output, canon = runner(test_models + '/smooth/test_smooth.mdl') assert_frames_close(output, canon, rtol=rtol) def test_smooth_and_stock(self): - output, canon = runner('test-models/tests/smooth_and_stock/test_smooth_and_stock.mdl') + output, canon = runner(test_models + '/smooth_and_stock/test_smooth_and_stock.mdl') assert_frames_close(output, canon, rtol=rtol) def test_special_characters(self): - output, canon = runner('test-models/tests/special_characters/test_special_variable_names.mdl') + output, canon = runner(test_models + '/special_characters/test_special_variable_names.mdl') assert_frames_close(output, canon, rtol=rtol) def test_sqrt(self): - output, canon = runner('test-models/tests/sqrt/test_sqrt.mdl') + output, canon = runner(test_models + '/sqrt/test_sqrt.mdl') assert_frames_close(output, canon, rtol=rtol) def test_subrange_merge(self): - output, canon = runner('test-models/tests/subrange_merge/test_subrange_merge.mdl') + output, canon = runner(test_models + '/subrange_merge/test_subrange_merge.mdl') assert_frames_close(output, canon, rtol=rtol) def test_subscript_multiples(self): - output, canon = runner('test-models/tests/subscript_multiples/test_multiple_subscripts.mdl') + output, canon = runner(test_models + '/subscript_multiples/test_multiple_subscripts.mdl') assert_frames_close(output, canon, rtol=rtol) def test_subscript_1d_arrays(self): - output, canon = runner('test-models/tests/subscript_1d_arrays/test_subscript_1d_arrays.mdl') + output, canon = runner(test_models + '/subscript_1d_arrays/test_subscript_1d_arrays.mdl') assert_frames_close(output, canon, rtol=rtol) def test_subscript_2d_arrays(self): - output, canon = runner('test-models/tests/subscript_2d_arrays/test_subscript_2d_arrays.mdl') + output, canon = runner(test_models + '/subscript_2d_arrays/test_subscript_2d_arrays.mdl') assert_frames_close(output, canon, rtol=rtol) def test_subscript_3d_arrays(self): - output, canon = runner('test-models/tests/subscript_3d_arrays/test_subscript_3d_arrays.mdl') + output, canon = runner(test_models + '/subscript_3d_arrays/test_subscript_3d_arrays.mdl') assert_frames_close(output, canon, rtol=rtol) def test_subscript_3d_arrays_lengthwise(self): - output, canon = runner('test-models/tests/subscript_3d_arrays_lengthwise/test_subscript_3d_arrays_lengthwise.mdl') + output, canon = runner(test_models + '/subscript_3d_arrays_lengthwise/test_subscript_3d_arrays_lengthwise.mdl') assert_frames_close(output, canon, rtol=rtol) def test_subscript_3d_arrays_widthwise(self): - output, canon = runner('test-models/tests/subscript_3d_arrays_widthwise/test_subscript_3d_arrays_widthwise.mdl') + output, canon = runner(test_models + '/subscript_3d_arrays_widthwise/test_subscript_3d_arrays_widthwise.mdl') assert_frames_close(output, canon, rtol=rtol) def test_subscript_aggregation(self): - output, canon = runner('test-models/tests/subscript_aggregation/test_subscript_aggregation.mdl') + output, canon = runner(test_models + '/subscript_aggregation/test_subscript_aggregation.mdl') assert_frames_close(output, canon, rtol=rtol) def test_subscript_constant_call(self): - output, canon = runner('test-models/tests/subscript_constant_call/test_subscript_constant_call.mdl') + output, canon = runner(test_models + '/subscript_constant_call/test_subscript_constant_call.mdl') assert_frames_close(output, canon, rtol=rtol) def test_subscript_copy(self): - output, canon = runner('test-models/tests/subscript_copy/test_subscript_copy.mdl') + output, canon = runner(test_models + '/subscript_copy/test_subscript_copy.mdl') assert_frames_close(output, canon, rtol=rtol) def test_subscript_docs(self): - output, canon = runner('test-models/tests/subscript_docs/subscript_docs.mdl') + output, canon = runner(test_models + '/subscript_docs/subscript_docs.mdl') assert_frames_close(output, canon, rtol=rtol) def test_subscript_element_name(self): # issue https://github.com/JamesPHoughton/pysd/issues/216 - output, canon = runner('test-models/tests/subscript_element_name/test_subscript_element_name.mdl') + output, canon = runner(test_models + '/subscript_element_name/test_subscript_element_name.mdl') assert_frames_close(output, canon, rtol=rtol) def test_subscript_individually_defined_1_of_2d_arrays(self): - output, canon = runner('test-models/tests/subscript_individually_defined_1_of_2d_arrays/subscript_individually_defined_1_of_2d_arrays.mdl') + output, canon = runner(test_models + '/subscript_individually_defined_1_of_2d_arrays/subscript_individually_defined_1_of_2d_arrays.mdl') assert_frames_close(output, canon, rtol=rtol) def test_subscript_individually_defined_1_of_2d_arrays_from_floats(self): - output, canon = runner('test-models/tests/subscript_individually_defined_1_of_2d_arrays_from_floats/subscript_individually_defined_1_of_2d_arrays_from_floats.mdl') + output, canon = runner(test_models + '/subscript_individually_defined_1_of_2d_arrays_from_floats/subscript_individually_defined_1_of_2d_arrays_from_floats.mdl') assert_frames_close(output, canon, rtol=rtol) def test_subscript_individually_defined_1d_arrays(self): - output, canon = runner('test-models/tests/subscript_individually_defined_1d_arrays/subscript_individually_defined_1d_arrays.mdl') + output, canon = runner(test_models + '/subscript_individually_defined_1d_arrays/subscript_individually_defined_1d_arrays.mdl') assert_frames_close(output, canon, rtol=rtol) def test_subscript_individually_defined_stocks(self): - output, canon = runner('test-models/tests/subscript_individually_defined_stocks/test_subscript_individually_defined_stocks.mdl') + output, canon = runner(test_models + '/subscript_individually_defined_stocks/test_subscript_individually_defined_stocks.mdl') assert_frames_close(output, canon, rtol=rtol) def test_subscript_mapping_simple(self): with warnings.catch_warnings(): warnings.simplefilter("ignore") - output, canon = runner('test-models/tests/subscript_mapping_simple/test_subscript_mapping_simple.mdl') + output, canon = runner(test_models + '/subscript_mapping_simple/test_subscript_mapping_simple.mdl') assert_frames_close(output, canon, rtol=rtol) def test_subscript_mapping_vensim(self): with warnings.catch_warnings(): warnings.simplefilter("ignore") - output, canon = runner('test-models/tests/subscript_mapping_vensim/test_subscript_mapping_vensim.mdl') + output, canon = runner(test_models + '/subscript_mapping_vensim/test_subscript_mapping_vensim.mdl') assert_frames_close(output, canon, rtol=rtol) def test_subscript_mixed_assembly(self): - output, canon = runner('test-models/tests/subscript_mixed_assembly/test_subscript_mixed_assembly.mdl') + output, canon = runner(test_models + '/subscript_mixed_assembly/test_subscript_mixed_assembly.mdl') assert_frames_close(output, canon, rtol=rtol) def test_subscript_selection(self): - output, canon = runner('test-models/tests/subscript_selection/subscript_selection.mdl') + output, canon = runner(test_models + '/subscript_selection/subscript_selection.mdl') assert_frames_close(output, canon, rtol=rtol) def test_subscript_numeric_range(self): - output, canon = runner('test-models/tests/subscript_numeric_range/test_subscript_numeric_range.mdl') + output, canon = runner(test_models + '/subscript_numeric_range/test_subscript_numeric_range.mdl') assert_frames_close(output, canon, rtol=rtol) def test_subscript_subranges(self): - output, canon = runner('test-models/tests/subscript_subranges/test_subscript_subrange.mdl') + output, canon = runner(test_models + '/subscript_subranges/test_subscript_subrange.mdl') assert_frames_close(output, canon, rtol=rtol) def test_subscript_subranges_equal(self): - output, canon = runner('test-models/tests/subscript_subranges_equal/test_subscript_subrange_equal.mdl') + output, canon = runner(test_models + '/subscript_subranges_equal/test_subscript_subrange_equal.mdl') assert_frames_close(output, canon, rtol=rtol) def test_subscript_switching(self): - output, canon = runner('test-models/tests/subscript_switching/subscript_switching.mdl') + output, canon = runner(test_models + '/subscript_switching/subscript_switching.mdl') assert_frames_close(output, canon, rtol=rtol) def test_subscript_transposition(self): - output, canon = runner('test-models/tests/subscript_transposition/test_subscript_transposition.mdl') + output, canon = runner(test_models + '/subscript_transposition/test_subscript_transposition.mdl') assert_frames_close(output, canon, rtol=rtol) def test_subscript_updimensioning(self): - output, canon = runner('test-models/tests/subscript_updimensioning/test_subscript_updimensioning.mdl') + output, canon = runner(test_models + '/subscript_updimensioning/test_subscript_updimensioning.mdl') assert_frames_close(output, canon, rtol=rtol) def test_subscripted_delays(self): - output, canon = runner('test-models/tests/subscripted_delays/test_subscripted_delays.mdl') + output, canon = runner(test_models + '/subscripted_delays/test_subscripted_delays.mdl') assert_frames_close(output, canon, rtol=rtol) def test_subscripted_flows(self): - output, canon = runner('test-models/tests/subscripted_flows/test_subscripted_flows.mdl') + output, canon = runner(test_models + '/subscripted_flows/test_subscripted_flows.mdl') assert_frames_close(output, canon, rtol=rtol) def test_subscripted_if_then_else(self): - output, canon = runner('test-models/tests/subscripted_if_then_else/test_subscripted_if_then_else.mdl') + output, canon = runner(test_models + '/subscripted_if_then_else/test_subscripted_if_then_else.mdl') assert_frames_close(output, canon, rtol=rtol) def test_subscripted_logicals(self): - output, canon = runner('test-models/tests/subscripted_logicals/test_subscripted_logicals.mdl') + output, canon = runner(test_models + '/subscripted_logicals/test_subscripted_logicals.mdl') assert_frames_close(output, canon, rtol=rtol) def test_subscripted_smooth(self): # issue https://github.com/JamesPHoughton/pysd/issues/226 - output, canon = runner('test-models/tests/subscripted_smooth/test_subscripted_smooth.mdl') + output, canon = runner(test_models + '/subscripted_smooth/test_subscripted_smooth.mdl') assert_frames_close(output, canon, rtol=rtol) def test_subscripted_trend(self): # issue https://github.com/JamesPHoughton/pysd/issues/226 - output, canon = runner('test-models/tests/subscripted_trend/test_subscripted_trend.mdl') + output, canon = runner(test_models + '/subscripted_trend/test_subscripted_trend.mdl') assert_frames_close(output, canon, rtol=rtol) def test_subscripted_xidz(self): - output, canon = runner('test-models/tests/subscripted_xidz/test_subscripted_xidz.mdl') + output, canon = runner(test_models + '/subscripted_xidz/test_subscripted_xidz.mdl') assert_frames_close(output, canon, rtol=rtol) def test_subset_duplicated_coord(self): - output, canon = runner('test-models/tests/subset_duplicated_coord/' + output, canon = runner(test_models + '/subset_duplicated_coord/' + 'test_subset_duplicated_coord.mdl') assert_frames_close(output, canon, rtol=rtol) def test_time(self): - output, canon = runner('test-models/tests/time/test_time.mdl') + output, canon = runner(test_models + '/time/test_time.mdl') assert_frames_close(output, canon, rtol=rtol) def test_trend(self): - output, canon = runner('test-models/tests/trend/test_trend.mdl') + output, canon = runner(test_models + '/trend/test_trend.mdl') assert_frames_close(output, canon, rtol=rtol) def test_trig(self): - output, canon = runner('test-models/tests/trig/test_trig.mdl') + output, canon = runner(test_models + '/trig/test_trig.mdl') assert_frames_close(output, canon, rtol=rtol) def test_variable_ranges(self): - output, canon = runner('test-models/tests/variable_ranges/test_variable_ranges.mdl') + output, canon = runner(test_models + '/variable_ranges/test_variable_ranges.mdl') assert_frames_close(output, canon, rtol=rtol) def test_unicode_characters(self): - output, canon = runner('test-models/tests/unicode_characters/unicode_test_model.mdl') + output, canon = runner(test_models + '/unicode_characters/unicode_test_model.mdl') assert_frames_close(output, canon, rtol=rtol) def test_xidz_zidz(self): - output, canon = runner('test-models/tests/xidz_zidz/xidz_zidz.mdl') + output, canon = runner(test_models + '/xidz_zidz/xidz_zidz.mdl') assert_frames_close(output, canon, rtol=rtol) def test_run_uppercase(self): - output, canon = runner('test-models/tests/case_sensitive_extension/teacup-upper.MDL') + output, canon = runner(test_models + '/case_sensitive_extension/teacup-upper.MDL') assert_frames_close(output, canon, rtol=rtol) def test_odd_number_quotes(self): - output, canon = runner('test-models/tests/odd_number_quotes/teacup_3quotes.mdl') + output, canon = runner(test_models + '/odd_number_quotes/teacup_3quotes.mdl') assert_frames_close(output, canon, rtol=rtol) diff --git a/tests/integration_test_xmile_pathway.py b/tests/integration_test_xmile_pathway.py index b4af0e32..3fcfc1ad 100644 --- a/tests/integration_test_xmile_pathway.py +++ b/tests/integration_test_xmile_pathway.py @@ -1,367 +1,370 @@ - +import os import unittest from pysd.tools.benchmarking import runner, assert_frames_close rtol = .05 +_root = os.path.dirname(__file__) +test_models = os.path.join(_root, "test-models/tests") + class TestIntegrationExamples(unittest.TestCase): def test_abs(self): - output, canon = runner('test-models/tests/abs/test_abs.xmile') + output, canon = runner(test_models + '/abs/test_abs.xmile') assert_frames_close(output, canon, rtol=rtol) @unittest.skip('error in model file') def test_active_initial(self): - output, canon = runner('test-models/tests/active_initial/test_active_initial.xmile') + output, canon = runner(test_models + '/active_initial/test_active_initial.xmile') assert_frames_close(output, canon, rtol=rtol) @unittest.skip('missing model file') def test_arguments(self): - output, canon = runner('test-models/tests/arguments/test_arguments.mdl') + output, canon = runner(test_models + '/arguments/test_arguments.mdl') assert_frames_close(output, canon, rtol=rtol) def test_builtin_max(self): - output, canon = runner('test-models/tests/builtin_max/builtin_max.xmile') + output, canon = runner(test_models + '/builtin_max/builtin_max.xmile') assert_frames_close(output, canon, rtol=rtol) def test_builtin_min(self): - output, canon = runner('test-models/tests/builtin_min/builtin_min.xmile') + output, canon = runner(test_models + '/builtin_min/builtin_min.xmile') assert_frames_close(output, canon, rtol=rtol) def test_chained_initialization(self): output, canon = runner( - 'test-models/tests/chained_initialization/test_chained_initialization.xmile') + test_models + '/chained_initialization/test_chained_initialization.xmile') assert_frames_close(output, canon, rtol=rtol) def test_comparisons(self): output, canon = runner( - 'test-models/tests/comparisons/comparisons.xmile') + test_models + '/comparisons/comparisons.xmile') assert_frames_close(output, canon, rtol=rtol) def test_constant_expressions(self): output, canon = runner( - 'test-models/tests/constant_expressions/test_constant_expressions.xmile') + test_models + '/constant_expressions/test_constant_expressions.xmile') assert_frames_close(output, canon, rtol=rtol) @unittest.skip('missing test model') def test_delay_parentheses(self): output, canon = runner( - 'test-models/tests/delay_parentheses/test_delay_parentheses.xmile') + test_models + '/delay_parentheses/test_delay_parentheses.xmile') assert_frames_close(output, canon, rtol=rtol) @unittest.skip('missing test model') def test_delays(self): - output, canon = runner('test-models/tests/delays/test_delays.mdl') + output, canon = runner(test_models + '/delays/test_delays.mdl') assert_frames_close(output, canon, rtol=rtol) @unittest.skip('missing test model') def test_euler_step_vs_saveper(self): output, canon = runner( - 'test-models/tests/euler_step_vs_saveper/test_euler_step_vs_saveper.xmile') + test_models + '/euler_step_vs_saveper/test_euler_step_vs_saveper.xmile') assert_frames_close(output, canon, rtol=rtol) def test_eval_order(self): output, canon = runner( - 'test-models/tests/eval_order/eval_order.xmile') + test_models + '/eval_order/eval_order.xmile') assert_frames_close(output, canon, rtol=rtol) def test_exp(self): - output, canon = runner('test-models/tests/exp/test_exp.xmile') + output, canon = runner(test_models + '/exp/test_exp.xmile') assert_frames_close(output, canon, rtol=rtol) def test_exponentiation(self): - output, canon = runner('test-models/tests/exponentiation/exponentiation.xmile') + output, canon = runner(test_models + '/exponentiation/exponentiation.xmile') assert_frames_close(output, canon, rtol=rtol) def test_function_capitalization(self): output, canon = runner( - 'test-models/tests/function_capitalization/test_function_capitalization.xmile') + test_models + '/function_capitalization/test_function_capitalization.xmile') assert_frames_close(output, canon, rtol=rtol) @unittest.skip('not sure if this is implemented in xmile?') def test_game(self): - output, canon = runner('test-models/tests/game/test_game.xmile') + output, canon = runner(test_models + '/game/test_game.xmile') assert_frames_close(output, canon, rtol=rtol) def test_if_stmt(self): - output, canon = runner('test-models/tests/if_stmt/if_stmt.xmile') + output, canon = runner(test_models + '/if_stmt/if_stmt.xmile') assert_frames_close(output, canon, rtol=rtol) def test_initial_function(self): - output, canon = runner('test-models/tests/initial_function/test_initial.xmile') + output, canon = runner(test_models + '/initial_function/test_initial.xmile') assert_frames_close(output, canon, rtol=rtol) @unittest.skip('no xmile model') def test_input_functions(self): - output, canon = runner('test-models/tests/input_functions/test_inputs.mdl') + output, canon = runner(test_models + '/input_functions/test_inputs.mdl') assert_frames_close(output, canon, rtol=rtol) def test_limits(self): - output, canon = runner('test-models/tests/limits/test_limits.xmile') + output, canon = runner(test_models + '/limits/test_limits.xmile') assert_frames_close(output, canon, rtol=rtol) def test_line_breaks(self): - output, canon = runner('test-models/tests/line_breaks/test_line_breaks.xmile') + output, canon = runner(test_models + '/line_breaks/test_line_breaks.xmile') assert_frames_close(output, canon, rtol=rtol) def test_line_continuation(self): - output, canon = runner('test-models/tests/line_continuation/test_line_continuation.xmile') + output, canon = runner(test_models + '/line_continuation/test_line_continuation.xmile') assert_frames_close(output, canon, rtol=rtol) def test_ln(self): - output, canon = runner('test-models/tests/ln/test_ln.xmile') + output, canon = runner(test_models + '/ln/test_ln.xmile') assert_frames_close(output, canon, rtol=rtol) def test_log(self): - output, canon = runner('test-models/tests/log/test_log.xmile') + output, canon = runner(test_models + '/log/test_log.xmile') assert_frames_close(output, canon, rtol=rtol) def test_logicals(self): - output, canon = runner('test-models/tests/logicals/test_logicals.xmile') + output, canon = runner(test_models + '/logicals/test_logicals.xmile') assert_frames_close(output, canon, rtol=rtol) def test_lookups(self): - output, canon = runner('test-models/tests/lookups/test_lookups.xmile') + output, canon = runner(test_models + '/lookups/test_lookups.xmile') assert_frames_close(output, canon, rtol=rtol) def test_lookups_xscale(self): - output, canon = runner('test-models/tests/lookups/test_lookups_xscale.xmile') + output, canon = runner(test_models + '/lookups/test_lookups_xscale.xmile') assert_frames_close(output, canon, rtol=rtol) def test_lookups_xpts_sep(self): - output, canon = runner('test-models/tests/lookups/test_lookups_xpts_sep.xmile') + output, canon = runner(test_models + '/lookups/test_lookups_xpts_sep.xmile') assert_frames_close(output, canon, rtol=rtol) def test_lookups_ypts_sep(self): - output, canon = runner('test-models/tests/lookups/test_lookups_ypts_sep.xmile') + output, canon = runner(test_models + '/lookups/test_lookups_ypts_sep.xmile') assert_frames_close(output, canon, rtol=rtol) @unittest.skip('no xmile') def test_lookups_funcnames(self): - output, canon = runner('test-models/tests/lookups_funcnames/test_lookups_funcnames.mdl') + output, canon = runner(test_models + '/lookups_funcnames/test_lookups_funcnames.mdl') assert_frames_close(output, canon, rtol=rtol) def test_lookups_inline(self): - output, canon = runner('test-models/tests/lookups_inline/test_lookups_inline.xmile') + output, canon = runner(test_models + '/lookups_inline/test_lookups_inline.xmile') assert_frames_close(output, canon, rtol=rtol) @unittest.skip('no xmile') def test_lookups_inline_bounded(self): output, canon = runner( - 'test-models/tests/lookups_inline_bounded/test_lookups_inline_bounded.mdl') + test_models + '/lookups_inline_bounded/test_lookups_inline_bounded.mdl') assert_frames_close(output, canon, rtol=rtol) @unittest.skip('no xmile') def test_macro_cross_reference(self): - output, canon = runner('test-models/tests/macro_cross_reference/test_macro_cross_reference.mdl') + output, canon = runner(test_models + '/macro_cross_reference/test_macro_cross_reference.mdl') assert_frames_close(output, canon, rtol=rtol) @unittest.skip('missing test model') def test_macro_expression(self): - output, canon = runner('test-models/tests/macro_expression/test_macro_expression.xmile') + output, canon = runner(test_models + '/macro_expression/test_macro_expression.xmile') assert_frames_close(output, canon, rtol=rtol) @unittest.skip('missing test model') def test_macro_multi_expression(self): output, canon = runner( - 'test-models/tests/macro_multi_expression/test_macro_multi_expression.xmile') + test_models + '/macro_multi_expression/test_macro_multi_expression.xmile') assert_frames_close(output, canon, rtol=rtol) @unittest.skip('missing test model') def test_macro_multi_macros(self): output, canon = runner( - 'test-models/tests/macro_multi_macros/test_macro_multi_macros.xmile') + test_models + '/macro_multi_macros/test_macro_multi_macros.xmile') assert_frames_close(output, canon, rtol=rtol) @unittest.skip('no xmile') def test_macro_output(self): - output, canon = runner('test-models/tests/macro_output/test_macro_output.mdl') + output, canon = runner(test_models + '/macro_output/test_macro_output.mdl') assert_frames_close(output, canon, rtol=rtol) @unittest.skip('missing test model') def test_macro_stock(self): - output, canon = runner('test-models/tests/macro_stock/test_macro_stock.xmile') + output, canon = runner(test_models + '/macro_stock/test_macro_stock.xmile') assert_frames_close(output, canon, rtol=rtol) @unittest.skip('do we need this?') def test_macro_trailing_definition(self): - output, canon = runner('test-models/tests/macro_trailing_definition/test_macro_trailing_definition.mdl') + output, canon = runner(test_models + '/macro_trailing_definition/test_macro_trailing_definition.mdl') assert_frames_close(output, canon, rtol=rtol) def test_model_doc(self): - output, canon = runner('test-models/tests/model_doc/model_doc.xmile') + output, canon = runner(test_models + '/model_doc/model_doc.xmile') assert_frames_close(output, canon, rtol=rtol) def test_number_handling(self): - output, canon = runner('test-models/tests/number_handling/test_number_handling.xmile') + output, canon = runner(test_models + '/number_handling/test_number_handling.xmile') assert_frames_close(output, canon, rtol=rtol) def test_parentheses(self): - output, canon = runner('test-models/tests/parentheses/test_parens.xmile') + output, canon = runner(test_models + '/parentheses/test_parens.xmile') assert_frames_close(output, canon, rtol=rtol) def test_reference_capitalization(self): """A properly formatted Vensim model should never create this failure""" output, canon = runner( - 'test-models/tests/reference_capitalization/test_reference_capitalization.xmile') + test_models + '/reference_capitalization/test_reference_capitalization.xmile') assert_frames_close(output, canon, rtol=rtol) @unittest.skip('in branch') def test_rounding(self): - output, canon = runner('test-models/tests/rounding/test_rounding.mdl') + output, canon = runner(test_models + '/rounding/test_rounding.mdl') assert_frames_close(output, canon, rtol=rtol) @unittest.skip('no xmile') def test_smooth(self): - output, canon = runner('test-models/tests/smooth/test_smooth.mdl') + output, canon = runner(test_models + '/smooth/test_smooth.mdl') assert_frames_close(output, canon, rtol=rtol) @unittest.skip('missing test model') def test_smooth_and_stock(self): - output, canon = runner('test-models/tests/smooth_and_stock/test_smooth_and_stock.xmile') + output, canon = runner(test_models + '/smooth_and_stock/test_smooth_and_stock.xmile') assert_frames_close(output, canon, rtol=rtol) @unittest.skip('missing test model') def test_special_characters(self): output, canon = runner( - 'test-models/tests/special_characters/test_special_variable_names.xmile') + test_models + '/special_characters/test_special_variable_names.xmile') assert_frames_close(output, canon, rtol=rtol) def test_sqrt(self): - output, canon = runner('test-models/tests/sqrt/test_sqrt.xmile') + output, canon = runner(test_models + '/sqrt/test_sqrt.xmile') assert_frames_close(output, canon, rtol=rtol) @unittest.skip('missing test model') def test_subscript_multiples(self): output, canon = runner( - 'test-models/tests/subscript multiples/test_multiple_subscripts.xmile') + test_models + '/subscript multiples/test_multiple_subscripts.xmile') assert_frames_close(output, canon, rtol=rtol) @unittest.skip('missing test model') def test_subscript_1d_arrays(self): output, canon = runner( - 'test-models/tests/subscript_1d_arrays/test_subscript_1d_arrays.xmile') + test_models + '/subscript_1d_arrays/test_subscript_1d_arrays.xmile') assert_frames_close(output, canon, rtol=rtol) @unittest.skip('no xmile') def test_subscript_2d_arrays(self): output, canon = runner( - 'test-models/tests/subscript_2d_arrays/test_subscript_2d_arrays.xmile') + test_models + '/subscript_2d_arrays/test_subscript_2d_arrays.xmile') assert_frames_close(output, canon, rtol=rtol) @unittest.skip('no xmile') def test_subscript_3d_arrays(self): - output, canon = runner('test-models/tests/subscript_3d_arrays/test_subscript_3d_arrays.mdl') + output, canon = runner(test_models + '/subscript_3d_arrays/test_subscript_3d_arrays.mdl') assert_frames_close(output, canon, rtol=rtol) @unittest.skip('no xmile') def test_subscript_3d_arrays_lengthwise(self): - output, canon = runner('test-models/tests/subscript_3d_arrays_lengthwise/test_subscript_3d_arrays_lengthwise.mdl') + output, canon = runner(test_models + '/subscript_3d_arrays_lengthwise/test_subscript_3d_arrays_lengthwise.mdl') assert_frames_close(output, canon, rtol=rtol) @unittest.skip('no xmile') def test_subscript_3d_arrays_widthwise(self): - output, canon = runner('test-models/tests/subscript_3d_arrays_widthwise/test_subscript_3d_arrays_widthwise.mdl') + output, canon = runner(test_models + '/subscript_3d_arrays_widthwise/test_subscript_3d_arrays_widthwise.mdl') assert_frames_close(output, canon, rtol=rtol) @unittest.skip('in branch') def test_subscript_aggregation(self): - output, canon = runner('test-models/tests/subscript_aggregation/test_subscript_aggregation.mdl') + output, canon = runner(test_models + '/subscript_aggregation/test_subscript_aggregation.mdl') assert_frames_close(output, canon, rtol=rtol) @unittest.skip('missing test model') def test_subscript_constant_call(self): output, canon = runner( - 'test-models/tests/subscript_constant_call/test_subscript_constant_call.xmile') + test_models + '/subscript_constant_call/test_subscript_constant_call.xmile') assert_frames_close(output, canon, rtol=rtol) @unittest.skip('no xmile') def test_subscript_docs(self): - output, canon = runner('test-models/tests/subscript_docs/subscript_docs.mdl') + output, canon = runner(test_models + '/subscript_docs/subscript_docs.mdl') assert_frames_close(output, canon, rtol=rtol) @unittest.skip('no xmile') def test_subscript_individually_defined_1_of_2d_arrays(self): - output, canon = runner('test-models/tests/subscript_individually_defined_1_of_2d_arrays/subscript_individually_defined_1_of_2d_arrays.mdl') + output, canon = runner(test_models + '/subscript_individually_defined_1_of_2d_arrays/subscript_individually_defined_1_of_2d_arrays.mdl') assert_frames_close(output, canon, rtol=rtol) @unittest.skip('no xmile') def test_subscript_individually_defined_1_of_2d_arrays_from_floats(self): - output, canon = runner('test-models/tests/subscript_individually_defined_1_of_2d_arrays_from_floats/subscript_individually_defined_1_of_2d_arrays_from_floats.mdl') + output, canon = runner(test_models + '/subscript_individually_defined_1_of_2d_arrays_from_floats/subscript_individually_defined_1_of_2d_arrays_from_floats.mdl') assert_frames_close(output, canon, rtol=rtol) @unittest.skip('no xmile') def test_subscript_individually_defined_1d_arrays(self): - output, canon = runner('test-models/tests/subscript_individually_defined_1d_arrays/subscript_individually_defined_1d_arrays.mdl') + output, canon = runner(test_models + '/subscript_individually_defined_1d_arrays/subscript_individually_defined_1d_arrays.mdl') assert_frames_close(output, canon, rtol=rtol) @unittest.skip('no xmile') def test_subscript_individually_defined_stocks(self): - output, canon = runner('test-models/tests/subscript_individually_defined_stocks/test_subscript_individually_defined_stocks.mdl') + output, canon = runner(test_models + '/subscript_individually_defined_stocks/test_subscript_individually_defined_stocks.mdl') assert_frames_close(output, canon, rtol=rtol) @unittest.skip('no xmile') def test_subscript_mixed_assembly(self): - output, canon = runner('test-models/tests/subscript_mixed_assembly/test_subscript_mixed_assembly.mdl') + output, canon = runner(test_models + '/subscript_mixed_assembly/test_subscript_mixed_assembly.mdl') assert_frames_close(output, canon, rtol=rtol) @unittest.skip('no xmile') def test_subscript_selection(self): - output, canon = runner('test-models/tests/subscript_selection/subscript_selection.mdl') + output, canon = runner(test_models + '/subscript_selection/subscript_selection.mdl') assert_frames_close(output, canon, rtol=rtol) @unittest.skip('missing test model') def test_subscript_subranges(self): output, canon = runner( - 'test-models/tests/subscript_subranges/test_subscript_subrange.xmile') + test_models + '/subscript_subranges/test_subscript_subrange.xmile') assert_frames_close(output, canon, rtol=rtol) @unittest.skip('missing test model') def test_subscript_subranges_equal(self): output, canon = runner( - 'test-models/tests/subscript_subranges_equal/test_subscript_subrange_equal.xmile') + test_models + '/subscript_subranges_equal/test_subscript_subrange_equal.xmile') assert_frames_close(output, canon, rtol=rtol) @unittest.skip('no xmile') def test_subscript_switching(self): - output, canon = runner('test-models/tests/subscript_switching/subscript_switching.mdl') + output, canon = runner(test_models + '/subscript_switching/subscript_switching.mdl') assert_frames_close(output, canon, rtol=rtol) @unittest.skip('missing test model') def test_subscript_updimensioning(self): output, canon = runner( - 'test-models/tests/subscript_updimensioning/test_subscript_updimensioning.xmile') + test_models + '/subscript_updimensioning/test_subscript_updimensioning.xmile') assert_frames_close(output, canon, rtol=rtol) @unittest.skip('no xmile') def test_subscripted_delays(self): - output, canon = runner('test-models/tests/subscripted_delays/test_subscripted_delays.mdl') + output, canon = runner(test_models + '/subscripted_delays/test_subscripted_delays.mdl') assert_frames_close(output, canon, rtol=rtol) @unittest.skip('no xmile') def test_subscripted_flows(self): - output, canon = runner('test-models/tests/subscripted_flows/test_subscripted_flows.mdl') + output, canon = runner(test_models + '/subscripted_flows/test_subscripted_flows.mdl') assert_frames_close(output, canon, rtol=rtol) @unittest.skip('no xmile') def test_time(self): - output, canon = runner('test-models/tests/time/test_time.mdl') + output, canon = runner(test_models + '/time/test_time.mdl') assert_frames_close(output, canon, rtol=rtol) def test_trig(self): - output, canon = runner('test-models/tests/trig/test_trig.xmile') + output, canon = runner(test_models + '/trig/test_trig.xmile') assert_frames_close(output, canon, rtol=rtol) @unittest.skip('no xmile') def test_trend(self): - output, canon = runner('test-models/tests/trend/test_trend.xmile') + output, canon = runner(test_models + '/trend/test_trend.xmile') assert_frames_close(output, canon, rtol=rtol) @unittest.skip('no xmile') def test_xidz_zidz(self): - output, canon = runner('test-models/tests/xidz_zidz/xidz_zidz.xmile') + output, canon = runner(test_models + '/xidz_zidz/xidz_zidz.xmile') assert_frames_close(output, canon, rtol=rtol) diff --git a/tests/pytest.ini b/tests/pytest.ini new file mode 100644 index 00000000..033e9f5f --- /dev/null +++ b/tests/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +python_files = unit_test_*.py integration_test_*.py \ No newline at end of file diff --git a/tests/unit_test_benchmarking.py b/tests/unit_test_benchmarking.py index 106b5fe4..9383e467 100644 --- a/tests/unit_test_benchmarking.py +++ b/tests/unit_test_benchmarking.py @@ -1,8 +1,11 @@ +import os from unittest import TestCase # most of the features of this script are already tested indirectly when # running vensim and xmile integration tests +_root = os.path.dirname(__file__) + class TestErrors(TestCase): @@ -10,7 +13,7 @@ def test_canonical_file_not_found(self): from pysd.tools.benchmarking import runner with self.assertRaises(FileNotFoundError) as err: - runner('more-tests/not_existent.mdl') + runner(os.path.join(_root, "more-tests/not_existent.mdl")) self.assertIn( 'Canonical output file not found.', @@ -20,7 +23,9 @@ def test_non_valid_model(self): from pysd.tools.benchmarking import runner with self.assertRaises(ValueError) as err: - runner('more-tests/not_vensim/test_not_vensim.txt') + runner(os.path.join( + _root, + "more-tests/not_vensim/test_not_vensim.txt")) self.assertIn( 'Modelfile should be *.mdl or *.xmile', @@ -30,10 +35,16 @@ def test_non_valid_outputs(self): from pysd.tools.benchmarking import load_outputs with self.assertRaises(ValueError) as err: - load_outputs('more-tests/not_vensim/test_not_vensim.txt') + load_outputs( + os.path.join( + _root, + "more-tests/not_vensim/test_not_vensim.txt")) self.assertIn( - "Not able to read 'more-tests/not_vensim/test_not_vensim.txt'.", + "Not able to read '", + str(err.exception)) + self.assertIn( + "more-tests/not_vensim/test_not_vensim.txt'.", str(err.exception)) def test_different_frames_error(self): @@ -41,8 +52,9 @@ def test_different_frames_error(self): with self.assertRaises(AssertionError) as err: assert_frames_close( - load_outputs('data/out_teacup.csv'), - load_outputs('data/out_teacup_modified.csv')) + load_outputs(os.path.join(_root, "data/out_teacup.csv")), + load_outputs( + os.path.join(_root, "data/out_teacup_modified.csv"))) self.assertIn( "Column 'Teacup Temperature' is not close.", @@ -62,8 +74,9 @@ def test_different_frames_warning(self): with catch_warnings(record=True) as ws: assert_frames_close( - load_outputs('data/out_teacup.csv'), - load_outputs('data/out_teacup_modified.csv'), + load_outputs(os.path.join(_root, "data/out_teacup.csv")), + load_outputs( + os.path.join(_root, "data/out_teacup_modified.csv")), assertion="warn") # use only user warnings @@ -86,5 +99,6 @@ def test_transposed_frame(self): from pysd.tools.benchmarking import load_outputs, assert_frames_close assert_frames_close( - load_outputs('data/out_teacup.csv'), - load_outputs('data/out_teacup_transposed.csv', transpose=True)) \ No newline at end of file + load_outputs(os.path.join(_root, "data/out_teacup.csv")), + load_outputs(os.path.join(_root, "data/out_teacup_transposed.csv"), + transpose=True)) diff --git a/tests/unit_test_cli.py b/tests/unit_test_cli.py index eb1d51dd..997a0457 100644 --- a/tests/unit_test_cli.py +++ b/tests/unit_test_cli.py @@ -10,15 +10,21 @@ from pysd.tools.benchmarking import load_outputs, assert_frames_close from pysd import __version__ -test_model = 'test-models/samples/teacup/teacup.mdl' -test_model_xmile = 'test-models/samples/teacup/teacup.xmile' -test_model_subs = 'test-models/tests/subscript_2d_arrays/'\ - + 'test_subscript_2d_arrays.mdl' -test_model_look = 'test-models/tests/get_lookups_subscripted_args/'\ - + 'test_get_lookups_subscripted_args.mdl' - -out_tab_file = 'cli_output.tab' -out_csv_file = 'cli_output.csv' +_root = os.path.dirname(__file__) + +test_model = os.path.join(_root, 'test-models/samples/teacup/teacup.mdl') +test_model_xmile = os.path.join( + _root, 'test-models/samples/teacup/teacup.xmile') +test_model_subs = os.path.join( + _root, + 'test-models/tests/subscript_2d_arrays/test_subscript_2d_arrays.mdl') +test_model_look = os.path.join( + _root, + 'test-models/tests/get_lookups_subscripted_args/' + + 'test_get_lookups_subscripted_args.mdl') + +out_tab_file = os.path.join(_root, 'cli_output.tab') +out_csv_file = os.path.join(_root, 'cli_output.csv') encoding_stdout = sys.stdout.encoding or 'utf-8' encoding_stderr = sys.stderr.encoding or 'utf-8' @@ -52,7 +58,8 @@ class TestPySD(unittest.TestCase): """ These tests are similar to unit_test_pysd but adapted for cli """ def test_read_not_model(self): - model = 'more-tests/not_vensim/test_not_vensim.txt' + model = os.path.join( + _root, 'more-tests/not_vensim/test_not_vensim.txt') command = f'{call} {model}' out = subprocess.run(split_bash(command), capture_output=True) stderr = out.stderr.decode(encoding_stderr) @@ -65,7 +72,8 @@ def test_read_not_model(self): def test_read_model_not_exists(self): - model = 'more-tests/not_vensim/test_not_vensim.mdl' + model = os.path.join( + _root, 'more-tests/not_vensim/test_not_vensim.mdl') command = f'{call} {model}' out = subprocess.run(split_bash(command), capture_output=True) stderr = out.stderr.decode(encoding_stderr) @@ -77,7 +85,7 @@ def test_read_model_not_exists(self): def test_read_not_valid_output(self): - out_xls_file = 'cli_output.xls' + out_xls_file = os.path.join(_root, 'cli_output.xls') command = f'{call} -o {out_xls_file} {test_model}' out = subprocess.run(split_bash(command), capture_output=True) stderr = out.stderr.decode(encoding_stderr) @@ -198,7 +206,7 @@ def test_translate_file(self): def test_read_vensim_split_model(self): - root_dir = "more-tests/split_model/" + root_dir = os.path.join(_root, "more-tests/split_model") + "/" model_name = "test_split_model" namespace_filename = "_namespace_" + model_name + ".json" @@ -271,7 +279,7 @@ def test_run_return_columns(self): os.remove(out_csv_file) # from txt - txt_file = 'return_columns.txt' + txt_file = os.path.join(_root, 'return_columns.txt') return_columns = ['Room Temperature', 'Teacup Temperature'] with open(txt_file, 'w') as file: file.write("\n".join(return_columns)) diff --git a/tests/unit_test_external.py b/tests/unit_test_external.py index b6164737..3e852165 100644 --- a/tests/unit_test_external.py +++ b/tests/unit_test_external.py @@ -6,7 +6,10 @@ import xarray as xr _root = os.path.dirname(__file__) -_exp = SourceFileLoader('expected_data', 'data/expected_data.py').load_module() +_exp = SourceFileLoader( + 'expected_data', + os.path.join(_root, 'data/expected_data.py') + ).load_module() class TestExcels(unittest.TestCase): @@ -19,7 +22,7 @@ def test_read_clean(self): """ import pysd - file_name = "data/input.xlsx" + file_name = os.path.join(_root, "data/input.xlsx") sheet_name = "Vertical" sheet_name2 = "Horizontal" @@ -47,7 +50,7 @@ def test_read_clean_opyxl(self): import pysd from openpyxl import Workbook - file_name = "data/input.xlsx" + file_name = os.path.join(_root, "data/input.xlsx") # reading a file excel = pysd.external.Excels.read_opyxl(file_name) @@ -78,7 +81,7 @@ def test_close_file(self): # number of files already open n_files = len(p.open_files()) - file_name = "data/input.xlsx" + file_name = os.path.join(_root, "data/input.xlsx") sheet_name = "Vertical" sheet_name2 = "Horizontal" diff --git a/tests/unit_test_pysd.py b/tests/unit_test_pysd.py index a47d07af..2ebd4751 100644 --- a/tests/unit_test_pysd.py +++ b/tests/unit_test_pysd.py @@ -6,14 +6,18 @@ import numpy as np import xarray as xr -test_model = "test-models/samples/teacup/teacup.mdl" -test_model_subs = ( - "test-models/tests/subscript_2d_arrays/" + "test_subscript_2d_arrays.mdl" -) -test_model_look = ( +_root = os.path.dirname(__file__) + +test_model = os.path.join(_root, "test-models/samples/teacup/teacup.mdl") +test_model_subs = os.path.join( + _root, + "test-models/tests/subscript_2d_arrays/test_subscript_2d_arrays.mdl") +test_model_look = os.path.join( + _root, "test-models/tests/get_lookups_subscripted_args/" - + "test_get_lookups_subscripted_args.mdl" -) + + "test_get_lookups_subscripted_args.mdl") + +more_tests = os.path.join(_root, "more-tests") class TestPySD(unittest.TestCase): @@ -22,22 +26,22 @@ def test_load_different_version_error(self): # old PySD major version with self.assertRaises(ImportError): - pysd.load("more-tests/version/test_old_version.py") + pysd.load(more_tests + "/version/test_old_version.py") # current PySD major version - pysd.load("more-tests/version/test_current_version.py") + pysd.load(more_tests + "/version/test_current_version.py") def test_load_type_error(self): import pysd with self.assertRaises(ImportError): - pysd.load("more-tests/type_error/test_type_error.py") + pysd.load(more_tests + "/type_error/test_type_error.py") def test_read_not_model_vensim(self): import pysd with self.assertRaises(ValueError): - pysd.read_vensim('more-tests/not_vensim/test_not_vensim.txt') + pysd.read_vensim(more_tests + "/not_vensim/test_not_vensim.txt") def test_run(self): import pysd @@ -56,10 +60,14 @@ def test_run(self): def test_run_ignore_missing(self): import pysd - model_mdl = 'test-models/tests/get_with_missing_values_xlsx/'\ - + 'test_get_with_missing_values_xlsx.mdl' - model_py = 'test-models/tests/get_with_missing_values_xlsx/'\ - + 'test_get_with_missing_values_xlsx.py' + model_mdl = os.path.join( + _root, + 'test-models/tests/get_with_missing_values_xlsx/' + + 'test_get_with_missing_values_xlsx.mdl') + model_py = os.path.join( + _root, + 'test-models/tests/get_with_missing_values_xlsx/' + + 'test_get_with_missing_values_xlsx.py') with catch_warnings(record=True) as ws: # warnings for missing values @@ -89,7 +97,7 @@ def test_read_vensim_split_model(self): import pysd from pysd.tools.benchmarking import assert_frames_close - root_dir = "more-tests/split_model/" + root_dir = more_tests + "/split_model/" model_name = "test_split_model" model_split = pysd.read_vensim( @@ -152,7 +160,7 @@ def test_read_vensim_split_model_with_macro(self): import pysd from pysd.tools.benchmarking import assert_frames_close - root_dir = "more-tests/split_model_with_macro/" + root_dir = more_tests + "/split_model_with_macro/" model_name = "test_split_model_with_macro" model_split = pysd.read_vensim( @@ -350,7 +358,9 @@ def test_run_export_import(self): assert_frames_close(stocks2, stocks.loc[[20, 30]]) # delays - test_delays = 'test-models/tests/delays/test_delays.mdl' + test_delays = os.path.join( + _root, + 'test-models/tests/delays/test_delays.mdl') model = pysd.read_vensim(test_delays) stocks = model.run(return_timestamps=20) model.initialize() @@ -369,7 +379,9 @@ def test_run_export_import(self): assert_frames_close(stocks2, stocks) # delay fixed - test_delayf = 'test-models/tests/delay_fixed/test_delay_fixed.mdl' + test_delayf = os.path.join( + _root, + 'test-models/tests/delay_fixed/test_delay_fixed.mdl') model = pysd.read_vensim(test_delayf) stocks = model.run(return_timestamps=20) model.initialize() @@ -388,8 +400,10 @@ def test_run_export_import(self): assert_frames_close(stocks2, stocks) # smooth - test_smooth = 'test-models/tests/subscripted_smooth/'\ - 'test_subscripted_smooth.mdl' + test_smooth = os.path.join( + _root, + 'test-models/tests/subscripted_smooth/' + + 'test_subscripted_smooth.mdl') model = pysd.read_vensim(test_smooth) stocks = model.run(return_timestamps=20, flatten_output=True) model.initialize() @@ -409,8 +423,10 @@ def test_run_export_import(self): assert_frames_close(stocks2, stocks) # trend - test_trend = 'test-models/tests/subscripted_trend/'\ - 'test_subscripted_trend.mdl' + test_trend = os.path.join( + _root, + 'test-models/tests/subscripted_trend/' + + 'test_subscripted_trend.mdl') model = pysd.read_vensim(test_trend) stocks = model.run(return_timestamps=20, flatten_output=True) model.initialize() @@ -430,8 +446,8 @@ def test_run_export_import(self): assert_frames_close(stocks2, stocks) # initial - test_initial = 'test-models/tests/initial_function/'\ - 'test_initial.mdl' + test_initial = os.path.join( + _root, 'test-models/tests/initial_function/test_initial.mdl') model = pysd.read_vensim(test_initial) stocks = model.run(return_timestamps=20) model.initialize() @@ -450,8 +466,9 @@ def test_run_export_import(self): assert_frames_close(stocks2, stocks) # sample if true - test_sample_if_true = 'test-models/tests/sample_if_true/'\ - 'test_sample_if_true.mdl' + test_sample_if_true = os.path.join( + _root, + 'test-models/tests/sample_if_true/test_sample_if_true.mdl') model = pysd.read_vensim(test_sample_if_true) stocks = model.run(return_timestamps=20, flatten_output=True) model.initialize() @@ -989,10 +1006,10 @@ def test_docs_multiline_eqn(self): """ Test that the model prints some documentation """ import pysd - path2model = ( + path2model = os.path.join( + _root, "test-models/tests/multiple_lines_def/" + - "test_multiple_lines_def.mdl" - ) + "test_multiple_lines_def.mdl") model = pysd.read_vensim(path2model) doc = model.doc() @@ -1133,8 +1150,8 @@ def test_initialize(self): def test_initialize_order(self): import pysd - model = pysd.load('more-tests/initialization_order/' - 'test_initialization_order.py') + model = pysd.load(more_tests + "/initialization_order/" + "test_initialization_order.py") if model._stateful_elements[0].py_name.endswith('stock_a'): # we want to have stock b first always @@ -1624,7 +1641,8 @@ def test_default_returns_with_construction_functions(self): """ import pysd - model = pysd.read_vensim("test-models/tests/delays/test_delays.mdl") + model = pysd.read_vensim(os.path.join( + _root, "test-models/tests/delays/test_delays.mdl")) ret = model.run() self.assertTrue( { @@ -1646,7 +1664,8 @@ def test_default_returns_with_lookups(self): """ import pysd - model = pysd.read_vensim("test-models/tests/lookups/test_lookups.mdl") + model = pysd.read_vensim(os.path.join( + _root, "test-models/tests/lookups/test_lookups.mdl")) ret = model.run() self.assertTrue( {"accumulation", "rate", "lookup function call"} <= @@ -1674,9 +1693,11 @@ def test_incomplete_model(self): with catch_warnings(record=True) as w: simplefilter("always") - model = pysd.read_vensim( - "test-models/tests/incomplete_equations/test_incomplete_model.mdl" - ) + model = pysd.read_vensim(os.path.join( + _root, + "test-models/tests/incomplete_equations/" + + "test_incomplete_model.mdl" + )) self.assertTrue(any([warn.category == SyntaxWarning for warn in w])) with catch_warnings(record=True) as w: @@ -1701,8 +1722,10 @@ def test_multiple_load(self): """ import pysd - model_1 = pysd.read_vensim("test-models/samples/teacup/teacup.mdl") - model_2 = pysd.read_vensim("test-models/samples/SIR/SIR.mdl") + model_1 = pysd.read_vensim(os.path.join( + _root, "test-models/samples/teacup/teacup.mdl")) + model_2 = pysd.read_vensim(os.path.join( + _root, "test-models/samples/SIR/SIR.mdl")) self.assertNotIn("teacup_temperature", dir(model_2.components)) self.assertIn("susceptible", dir(model_2.components)) @@ -1722,8 +1745,10 @@ def test_no_crosstalk(self): # Todo: this test could be made more comprehensive import pysd - model_1 = pysd.read_vensim("test-models/samples/teacup/teacup.mdl") - model_2 = pysd.read_vensim("test-models/samples/SIR/SIR.mdl") + model_1 = pysd.read_vensim(os.path.join( + _root, "test-models/samples/teacup/teacup.mdl")) + model_2 = pysd.read_vensim(os.path.join( + _root, "test-models/samples/SIR/SIR.mdl")) model_1.components.initial_time = lambda: 10 self.assertNotEqual(model_2.components.initial_time, 10) @@ -1754,7 +1779,8 @@ def test_circular_reference(self): with self.assertRaises(ValueError) as err: pysd.load( - "more-tests/circular_reference/test_circular_reference.py") + more_tests + + "/circular_reference/test_circular_reference.py") self.assertIn("_integ_integ", str(err.exception)) self.assertIn("_delay_delay", str(err.exception)) @@ -1789,8 +1815,9 @@ class TestMultiRun(unittest.TestCase): def test_delay_reinitializes(self): import pysd - model = pysd.read_vensim( - "../tests/test-models/tests/delays/test_delays.mdl") + model = pysd.read_vensim(os.path.join( + _root, + "test-models/tests/delays/test_delays.mdl")) res1 = model.run() res2 = model.run() self.assertTrue(all(res1 == res2)) diff --git a/tests/unit_test_table2py.py b/tests/unit_test_table2py.py index 36424022..bbe33e7c 100644 --- a/tests/unit_test_table2py.py +++ b/tests/unit_test_table2py.py @@ -1,11 +1,15 @@ +import os import unittest import pandas as pd +_root = os.path.dirname(__file__) + class TestReadTabular(unittest.TestCase): def test_read_tab_file(self): import pysd - model = pysd.read_tabular('test-models/samples/teacup/teacup_mdl.tab') + model = pysd.read_tabular(os.path.join( + _root, 'test-models/samples/teacup/teacup_mdl.tab')) result = model.run() self.assertTrue(isinstance(result, pd.DataFrame)) # return a dataframe self.assertTrue('Teacup Temperature' in result.columns.values) # contains correct column diff --git a/tests/unit_test_xmile2py.py b/tests/unit_test_xmile2py.py index bf5c5bdd..78c8e1ea 100644 --- a/tests/unit_test_xmile2py.py +++ b/tests/unit_test_xmile2py.py @@ -5,8 +5,8 @@ from pysd.py_backend.xmile.xmile2py import translate_xmile - -TARGET_STMX_FILE = 'test-models/tests/game/test_game.stmx' +_root = os.path.dirname(__file__) +TARGET_STMX_FILE = os.path.join(_root, "test-models/tests/game/test_game.stmx") class TestXmileConversion(unittest.TestCase): From 934e22b2ffbbb29504d7c8dc5eea79dc10b29ac4 Mon Sep 17 00:00:00 2001 From: RogerSC Date: Mon, 9 Aug 2021 16:04:01 +0200 Subject: [PATCH 2/3] Split Vensim views into submodules (#283) * untested implementation of views separator * tests implementation and adds doc * fixed documentation * fixed documentation * Small corrections * Remove duplicated vars * Remove subscripts dicts when possible from external objects * remove increasing constraint in monotonic external data * Making the order of Externals series more flexible * addapting sketch grammar for Vensin 8.2.1 Co-authored-by: Eneko Martin Martinez --- docs/advanced_usage.rst | 17 +- pysd/_version.py | 2 +- pysd/cli/main.py | 19 +- pysd/cli/parser.py | 18 +- pysd/py_backend/builder.py | 121 +++++++---- pysd/py_backend/external.py | 21 +- pysd/py_backend/utils.py | 84 +++++++- pysd/py_backend/vensim/vensim2py.py | 191 +++++++++--------- pysd/pysd.py | 19 +- tests/data/input.xlsx | Bin 14657 -> 15013 bytes .../split_model/test_split_model_subviews.mdl | 105 ++++++++++ .../test_split_model_vensim_8_2_1.mdl | 143 +++++++++++++ tests/unit_test_cli.py | 62 +++++- tests/unit_test_external.py | 63 +++++- tests/unit_test_pysd.py | 183 ++++++++++++++++- tests/unit_test_vensim2py.py | 13 +- 16 files changed, 871 insertions(+), 190 deletions(-) create mode 100644 tests/more-tests/split_model/test_split_model_subviews.mdl create mode 100644 tests/more-tests/split_model_vensim_8_2_1/test_split_model_vensim_8_2_1.mdl diff --git a/docs/advanced_usage.rst b/docs/advanced_usage.rst index b8d5f7a8..04f56341 100644 --- a/docs/advanced_usage.rst +++ b/docs/advanced_usage.rst @@ -42,16 +42,16 @@ We can substitute this function directly for the heat_loss_to_room model compone If you want to replace a subscripted variable, you need to ensure that the output from the new function is the same as the previous one. You can check the current coordinates and dimensions of a component by using :py:data:`.get_coords(variable_name)` as it is explained in :doc:`basic usage <../basic_usage>`. -Splitting Vensim views in different files ------------------------------------------ -In order to replicate the Vensim views in translated models, the user can set the `split_modules` argument to True in the :py:func:`read_vensim` function:: +Splitting Vensim views in separate Python files (modules) +--------------------------------------------------------- +In order to replicate the Vensim views in translated models, the user can set the `split_views` argument to True in the :py:func:`read_vensim` function:: - read_vensim("many_views_model.mdl", split_modules=True) + read_vensim("many_views_model.mdl", split_views=True) The option to split the model in views is particularly interesting for large models with tens of views. Translating those models into a single file may make the resulting Python model difficult to read and maintain. -In a Vensim model with three separate views (e.g. `view_1`, `view_2` and `view_3`), setting `split_modules` to True would create the following tree inside the directory where the `.mdl` model is located: +In a Vensim model with three separate views (e.g. `view_1`, `view_2` and `view_3`), setting `split_views` to True would create the following tree inside the directory where the `.mdl` model is located: | main-folder | ├── modules_many_views_model @@ -64,6 +64,13 @@ In a Vensim model with three separate views (e.g. `view_1`, `view_2` and `view_3 | ├── many_views_model.py | | + +.. note :: + Often, modelers wish to organise views further. To that end, a common practice is to include a particular character in the View name to indicate that what comes after it is the name of the subview. For instance, we could name one view as `ENERGY.Supply` and another one as `ENERGY.Demand`. + In that particular case, setting the `subview_sep` kwarg equal to `"."`, as in the code below, would name the translated views as `demand.py` and `supply.py` and place them inside the `ENERGY` folder:: + + read_vensim("many_views_model.mdl", split_views=True, subview_sep=".") + If macros are present, they will be self-contained in files named as the macro itself. The macro inner variables will be placed inside the module that corresponds with the view in which they were defined. diff --git a/pysd/_version.py b/pysd/_version.py index 2d986fc5..0a0a43a5 100644 --- a/pysd/_version.py +++ b/pysd/_version.py @@ -1 +1 @@ -__version__ = "1.8.1" +__version__ = "1.9.0" diff --git a/pysd/cli/main.py b/pysd/cli/main.py index e5a4c3cd..216b8e66 100644 --- a/pysd/cli/main.py +++ b/pysd/cli/main.py @@ -26,7 +26,7 @@ def main(args): options = parser.parse_args(args) model = load(options.model_file, options.missing_values, - options.split_modules) + options.split_views, subview_sep=options.subview_sep) if not options.run: print("\nFinished!") @@ -44,7 +44,7 @@ def main(args): sys.exit() -def load(model_file, missing_values, split_modules): +def load(model_file, missing_values, split_views, **kwargs): """ Translate and load model file. @@ -53,6 +53,19 @@ def load(model_file, missing_values, split_modules): model_file: str Vensim, Xmile or PySD model file. + split_views: bool (optional) + If True, the sketch is parsed to detect model elements in each + model view, and then translate each view in a separate python + file. Setting this argument to True is recommended for large + models split in many different views. Default is False. + + **kwargs: (optional) + Additional keyword arguments. + subview_sep:(str) + Character used to separate views and subviews. If provided, + and split_views=True, each submodule will be placed inside the + folder of the parent view. + Returns ------- pysd.model @@ -62,7 +75,7 @@ def load(model_file, missing_values, split_modules): print("\nTranslating model file...\n") return pysd.read_vensim(model_file, initialize=False, missing_values=missing_values, - split_modules=split_modules) + split_views=split_views, **kwargs) elif model_file.lower().endswith('.xmile'): print("\nTranslating model file...\n") return pysd.read_xmile(model_file, initialize=False, diff --git a/pysd/cli/parser.py b/pysd/cli/parser.py index a38929eb..df734e1d 100644 --- a/pysd/cli/parser.py +++ b/pysd/cli/parser.py @@ -218,9 +218,9 @@ def __call__(self, parser, namespace, values, option_string=None): '--saveper will be ignored') -################### -# Model arguments # -################### +######################### +# Translation arguments # +######################### trans_arguments = parser.add_argument_group( 'translation arguments', @@ -233,10 +233,18 @@ def __call__(self, parser, namespace, values, option_string=None): 'it does not run it after translation') trans_arguments.add_argument( - '--split-modules', dest='split_modules', + '--split-views', dest='split_views', action='store_true', default=False, help='parse the sketch to detect model elements in each model view,' - ' and then translate each view in a separate python file') + ' and then translate each view in a separate Python file') + +trans_arguments.add_argument( + '--subview-sep', dest='subview_sep', + action='store', type=str, default="", metavar='STRING', + help='further division of views split in subviews, by identifying the' + 'separator string in the view name, only availabe if --split-views' + ' is used') + ####################### # Warnings and errors # diff --git a/pysd/py_backend/builder.py b/pysd/py_backend/builder.py index 4fe22e28..36204229 100644 --- a/pysd/py_backend/builder.py +++ b/pysd/py_backend/builder.py @@ -117,20 +117,19 @@ def reset(cls): build_names = set() -def build_modular_model( - elements, subscript_dict, namespace, main_filename, elements_per_module -): +def build_modular_model(elements, subscript_dict, namespace, main_filename, + elements_per_view): """ This is equivalent to the build function, but is used when the - split_modules parameter is set to True in the read_vensim function. + split_views parameter is set to True in the read_vensim function. The main python model file will be named as the original model file, and stored in the same folder. The modules will be stored in a separate folder named modules + original_model_name. Three extra json files will be generated, containing the namespace, subscripts_dict and the module names plus the variables included in each module, respectively. - Setting split_modules=True is recommended for large models with many + Setting split_views=True is recommended for large models with many different views. Parameters @@ -164,32 +163,57 @@ def build_modular_model( # create modules directory if it does not exist os.makedirs(modules_dir, exist_ok=True) - modules_list = elements_per_module.keys() + # check if there are subviews or only main views + subviews = all(isinstance(n, dict) for n in elements_per_view.values()) + + all_views = elements_per_view.keys() # creating the rest of files per module (this needs to be run before the # main module, as it updates the import_modules) processed_elements = [] - for module in modules_list: - module_elems = [] - for element in elements: - if element.get("py_name", None) in elements_per_module[module] or\ - element.get("parent_name", None) in elements_per_module[module]: - module_elems.append(element) + for view_name in all_views: + view_elems = [] + if not subviews: # only main views + for element in elements: + if element.get("py_name", None) in \ + elements_per_view[view_name] or \ + element.get("parent_name", None) in \ + elements_per_view[view_name]: + view_elems.append(element) + + _build_separate_module(view_elems, subscript_dict, view_name, + modules_dir) + + else: + # create subdirectory + view_dir = os.path.join(modules_dir, view_name) + os.makedirs(view_dir, exist_ok=True) - _build_separate_module(module_elems, subscript_dict, - module, modules_dir) + for subview_name in elements_per_view[view_name].keys(): + subview_elems = [] + for element in elements: + if element.get("py_name", None) in \ + elements_per_view[view_name][subview_name] or \ + element.get("parent_name", None) in \ + elements_per_view[view_name][subview_name]: + subview_elems.append(element) - processed_elements += module_elems + _build_separate_module(subview_elems, subscript_dict, + subview_name, view_dir) + view_elems += subview_elems + + processed_elements += view_elems # the unprocessed will go in the main file unprocessed_elements = [ element for element in elements if element not in processed_elements ] # building main file using the build function - _build_main_module(unprocessed_elements, subscript_dict, main_filename) + _build_main_module(unprocessed_elements, subscript_dict, + main_filename, subviews) # create json file for the modules and corresponding model elements with open(os.path.join(modules_dir, "_modules.json"), "w") as outfile: - json.dump(elements_per_module, outfile, indent=4, sort_keys=True) + json.dump(elements_per_view, outfile, indent=4, sort_keys=True) # create single namespace in a separate json file with open( @@ -203,13 +227,11 @@ def build_modular_model( ) as outfile: json.dump(subscript_dict, outfile, indent=4, sort_keys=True) - return None - -def _build_main_module(elements, subscript_dict, file_name): +def _build_main_module(elements, subscript_dict, file_name, subviews): """ Constructs and writes the python representation of the main model - module, when the split_modules=True in the read_vensim function. + module, when the split_views=True in the read_vensim function. Parameters ---------- @@ -230,6 +252,10 @@ def _build_main_module(elements, subscript_dict, file_name): file_name: str Path of the file where the main module will be stored. + subviews: bool + True or false depending on whether the views are split in subviews or + not. + Returns ------- None or text: None or str @@ -276,15 +302,25 @@ def _build_main_module(elements, subscript_dict, file_name): text += _get_control_vars(control_vars) - text += textwrap.dedent(""" - # load modules from the modules_%(outfile)s directory - for module in _modules: - exec(open_module(_root, "%(outfile)s", module)) + if not subviews: + text += textwrap.dedent(""" + # load modules from the modules_%(outfile)s directory + for module in _modules: + exec(open_module(_root, "%(outfile)s", module)) - """ % { - "outfile": os.path.basename(file_name).split(".")[0], - - }) + """ % { + "outfile": os.path.basename(file_name).split(".")[0], + }) + else: + text += textwrap.dedent(""" + # load submodules from subdirs in modules_%(outfile)s directory + for mod_name, mod_submods in _modules.items(): + for submod_name in mod_submods.keys(): + exec(open_module(_root, "%(outfile)s", mod_name, submod_name)) + + """ % { + "outfile": os.path.basename(file_name).split(".")[0], + }) text += funcs text = black.format_file_contents(text, fast=True, mode=black.FileMode()) @@ -292,10 +328,6 @@ def _build_main_module(elements, subscript_dict, file_name): # Needed for various sessions build_names.clear() - # this is used for testing - if file_name == "return": - return text - with open(file_name, "w", encoding="UTF-8") as out: out.write(text) @@ -303,7 +335,7 @@ def _build_main_module(elements, subscript_dict, file_name): def _build_separate_module(elements, subscript_dict, module_name, module_dir): """ Constructs and writes the python representation of a specific model - module, when the split_modules=True in the read_vensim function + module, when the split_views=True in the read_vensim function Parameters ---------- @@ -1544,14 +1576,16 @@ def add_ext_data(identifier, file_name, tab, time_row_or_col, cell, subs, List of element construction dictionaries for the builder to assemble. """ - coords = utils.make_coord_dict(subs, subscript_dict, terse=False) + Imports.add("external", "ExtData") + + coords = utils.simplify_subscript_input( + utils.make_coord_dict(subs, subscript_dict, terse=False), + subscript_dict, return_full=False) keyword = ( "'%s'" % keyword.strip(":").lower() if isinstance(keyword, str) else keyword) name = utils.make_python_identifier("_ext_data_%s" % identifier)[0] - Imports.add("external", "ExtData") - # Check if the object already exists if name in build_names: # Create a new py_name with ADD_# ending @@ -1626,7 +1660,9 @@ def add_ext_constant(identifier, file_name, tab, cell, subs, subscript_dict): """ Imports.add("external", "ExtConstant") - coords = utils.make_coord_dict(subs, subscript_dict, terse=False) + coords = utils.simplify_subscript_input( + utils.make_coord_dict(subs, subscript_dict, terse=False), + subscript_dict, return_full=False) name = utils.make_python_identifier("_ext_constant_%s" % identifier)[0] # Check if the object already exists @@ -1663,9 +1699,8 @@ def add_ext_constant(identifier, file_name, tab, cell, subs, subscript_dict): return "%s()" % external["py_name"], [external] -def add_ext_lookup( - identifier, file_name, tab, x_row_or_col, cell, subs, subscript_dict -): +def add_ext_lookup(identifier, file_name, tab, x_row_or_col, cell, + subs, subscript_dict): """ Constructs a external object for handling Vensim's GET XLS LOOKUPS and GET DIRECT LOOKUPS functionality. @@ -1707,7 +1742,9 @@ def add_ext_lookup( """ Imports.add("external", "ExtLookup") - coords = utils.make_coord_dict(subs, subscript_dict, terse=False) + coords = utils.simplify_subscript_input( + utils.make_coord_dict(subs, subscript_dict, terse=False), + subscript_dict, return_full=False) name = utils.make_python_identifier("_ext_lookup_%s" % identifier)[0] # Check if the object already exists diff --git a/pysd/py_backend/external.py b/pysd/py_backend/external.py index 436799bd..10035aed 100644 --- a/pysd/py_backend/external.py +++ b/pysd/py_backend/external.py @@ -405,13 +405,20 @@ def _initialize_data(self, element_type): + "\t{}:\t{}\n".format(series_across, self.x_row_or_col) ) - # Check if the lookup/time dimension is strictly monotonous - if np.any(np.diff(series) <= 0) and self.missing != "keep": - raise ValueError(self.py_name + "\n" - + "Dimension given in:\n" - + self._file_sheet - + "\t{}:\t{}\n".format(series_across, self.x_row_or_col) - + " is not strictly monotonous") + # reorder data with increasing series + if not np.all(np.diff(series) > 0) and self.missing != "keep": + order = np.argsort(series) + series = series[order] + data = data[order] + # Check if the lookup/time dimension is well defined + if np.any(np.diff(series) == 0): + raise ValueError(self.py_name + "\n" + + "Dimension given in:\n" + + self._file_sheet + + "\t{}:\t{}\n".format( + series_across, self.x_row_or_col) + + " has repeated values") + # Check for missing values in data if np.any(np.isnan(data)) and self.missing != "keep": diff --git a/pysd/py_backend/utils.py b/pysd/py_backend/utils.py index 0df26058..f9dab9ab 100644 --- a/pysd/py_backend/utils.py +++ b/pysd/py_backend/utils.py @@ -714,6 +714,49 @@ def round_(x): return round(x) +def simplify_subscript_input(coords, subscript_dict, + return_full=True): + """ + Parameters + ---------- + coords: dict + Coordinates to write in the model file. + + subscript_dict: dict + The subscript dictionary of the model file. + + return_full: bool (optional) + If True the when coords == subscript_dict, '_subscript_dict' + will be returned. Default is True + + partial: bool (optional) + If True "_subscript_dict" will not be returned as possible dict. + Used when subscript_dict is not the full dictionary. Default is False. + + Returns + ------- + coords: str + The equations to generate the coord dicttionary in the model file. + + """ + + if coords == subscript_dict and return_full: + # variable defined with all the subscripts + return "_subscript_dict" + + coordsp = [] + for dim, coord in coords.items(): + # find dimensions can be retrieved from _subscript_dict + if coord == subscript_dict[dim]: + # use _subscript_dict + coordsp.append(f"'{dim}': _subscript_dict['{dim}']") + else: + # write whole dict + coordsp.append(f"'{dim}': {coord}") + + return "{" + ", ".join(coordsp) + "}" + + def add_entries_underscore(*dictionaries): """ Expands dictionaries adding new keys underscoring the white spaces @@ -785,10 +828,10 @@ def load_model_data(root_dir, model_name): return namespace, subscripts, modules -def open_module(root_dir, model_name, module): +def open_module(root_dir, model_name, module, submodule=None): """ Used to load model modules from the main model file, when - split_modules=True in the read_vensim function. + split_views=True in the read_vensim function. Parameters ---------- @@ -799,17 +842,46 @@ def open_module(root_dir, model_name, module): Name of the model without file type extension (e.g. "my_model"). module: str - Name of the module to open. + Name of the module folder or file to open. + + sub_module: str (optional) + Name of the submodule to open. Returns ------- str: Model file content. - """ + if not submodule: + rel_file_path = module + ".py" + else: + rel_file_path = os.path.join(module, submodule + ".py") + return open( - os.path.join(root_dir, "modules_" + model_name, module + ".py") - ).read() + os.path.join(root_dir, "modules_" + model_name, rel_file_path)).read() + + +def clean_file_names(*args): + """ + Removes special characters and makes clean file names + + Parameters + ---------- + *args: tuple + Any number of strings to to clean + + Returns + ------- + clean: list + List containing the clean strings + """ + clean = [] + for name in args: + clean.append(re.sub( + r"[\W]+", "", name.replace(" ", "_") + ).lstrip("0123456789") + ) + return clean class ProgressBar: diff --git a/pysd/py_backend/vensim/vensim2py.py b/pysd/py_backend/vensim/vensim2py.py index 1eb1ed79..ceaa4cb4 100644 --- a/pysd/py_backend/vensim/vensim2py.py +++ b/pysd/py_backend/vensim/vensim2py.py @@ -4,6 +4,7 @@ knowledge of vensim syntax should be here. """ +from inspect import cleandoc import os import re import warnings @@ -491,12 +492,12 @@ def parse_sketch_line(sketch_line, namespace): sketch_grammar = _include_common_grammar( r""" - line = var_definition / module_intro / module_title / module_definition / arrow / flow / other_objects / anything - module_intro = ~r"\s*Sketch.*?names$" / ~r"^V300.*?ignored$" - module_title = "*" module_name - module_name = ~r"(?<=\*)[^\n]+$" - module_definition = "$" color "," digit "," font_properties "|" ( ( color / ones_and_dashes ) "|")* module_code - var_definition = var_code "," var_number "," var_name "," position "," var_box_type "," arrows_in_allowed "," hide_level "," var_face "," var_word_position "," var_thickness "," var_rest_conf ","? ( ( ones_and_dashes / color) ",")* font_properties? + line = var_definition / view_intro / view_title / view_definition / arrow / flow / other_objects / anything + view_intro = ~r"\s*Sketch.*?names$" / ~r"^V300.*?ignored$" + view_title = "*" view_name + view_name = ~r"(?<=\*)[^\n]+$" + view_definition = "$" color "," digit "," font_properties "|" ( ( color / ones_and_dashes ) "|")* view_code + var_definition = var_code "," var_number "," var_name "," position "," var_box_type "," arrows_in_allowed "," hide_level "," var_face "," var_word_position "," var_thickness "," var_rest_conf ","? ( ( ones_and_dashes / color) ",")* font_properties? ","? extra_bytes? # elements used in a line defining the properties of a variable or stock var_name = element var_name = ~r"(?<=,)[^,]+(?=,)" @@ -509,6 +510,7 @@ def parse_sketch_line(sketch_line, namespace): var_word_position = ~r"(?<=,)\-*\d+(?=,)" var_thickness = digit var_rest_conf = digit "," ~r"\d+" + extra_bytes = ~r"\d+,\d+,\d+,\d+,\d+,\d+" # required since Vensim 8.2.1 arrow = arrow_code "," digit "," origin_var "," destination_var "," (digit ",")+ (ones_and_dashes ",")? ((color ",") / ("," ~r"\d+") / (font_properties "," ~r"\d+"))* "|(" position ")|" # arrow origin and destination (this may be useful if further # parsing is required) @@ -537,7 +539,7 @@ def parse_sketch_line(sketch_line, namespace): var_code = ~r"^10(?=,)" multipurpose_code = ~r"^12(?=,)" # source, sink, plot, comment other_objects_code = ~r"^(30|31)(?=,)" - module_code = ~r"\d+" "," digit "," digit "," ~r"\d+" # code at + view_code = ~r"\d+" "," digit "," digit "," ~r"\d+" # code at digit = ~r"(?<=,)\d+(?=,)" # comma separated value/s ones_and_dashes = ~r"\-1\-\-1\-\-1" @@ -550,22 +552,22 @@ def parse_sketch_line(sketch_line, namespace): class SketchParser(parsimonious.NodeVisitor): def __init__(self, ast, namespace): self.namespace = namespace - self.module_or_var = {"variable_name": "", "module_name": ""} + self.view_or_var = {"variable_name": "", "view_name": ""} self.visit(ast) - def visit_module_name(self, n, vc): - self.module_or_var["module_name"] = n.text + def visit_view_name(self, n, vc): + self.view_or_var["view_name"] = n.text def visit_var_definition(self, n, vc): if int(vc[10]) % 2 != 0: # not a shadow variable - self.module_or_var["variable_name"] = self.namespace.get(vc[4], - "") + self.view_or_var["variable_name"] = self.namespace.get(vc[4], + "") def generic_visit(self, n, vc): return "".join(filter(None, vc)) or n.text or "" - + tree = parser.parse(sketch_line) - return SketchParser(tree, namespace=namespace).module_or_var + return SketchParser(tree, namespace=namespace).view_or_var def parse_units(units_str): @@ -1263,43 +1265,17 @@ def visit_array(self, n, vc): else: datastr = n.text - # Following implementation makes cleaner the model file - if list(coords) == element["subs"]: - if len(coords) == len(subscript_dict): - # variable is defined with all subscrips - return builder.build_function_call( - functions_utils["DataArray"], - [datastr, "_subscript_dict", repr(list(coords))], - ) - - from_dict, no_from_dict = [], {} - for coord, sub in zip(coords, element["subs"]): - # find dimensions can be retrieved from _subscript_dict - if coord == sub: - from_dict.append(coord) - else: - no_from_dict[coord] = coords[coord] - - if from_dict and no_from_dict: - # some dimensons can be retrieved from _subscript_dict - coordsp = ( - "{**{dim: _subscript_dict[dim] for dim in %s}, " - % from_dict - + repr(no_from_dict)[1:] - ) - elif from_dict: - # all dimensons can be retrieved from _subscript_dict - coordsp = "{dim: _subscript_dict[dim] for dim in %s}" \ - % from_dict - else: - # no dimensons can be retrieved from _subscript_dict - coordsp = repr(no_from_dict) + # Create a cleaner dictionary of coords using _subscript_dict + # when possible + utils.simplify_subscript_input(coords, subscript_dict, + return_full=True) return builder.build_function_call( - functions_utils["DataArray"], [datastr, coordsp, repr(list( - coords))] - ) - + functions_utils["DataArray"], + [datastr, + utils.simplify_subscript_input(coords, subscript_dict), + repr(list(coords))] + ) else: return n.text.replace(" ", "") @@ -1477,7 +1453,7 @@ def generic_visit(self, n, vc): ) -def translate_section(section, macro_list, sketch, root_path): +def translate_section(section, macro_list, sketch, root_path, subview_sep=""): model_elements = get_model_elements(section["string"]) @@ -1577,16 +1553,15 @@ def translate_section(section, macro_list, sketch, root_path): ] # macros are built in their own separate files, and their inputs and - # outputs are put in modules + # outputs are put in views/subviews if sketch and (section["name"] == "_main_"): - - module_elements = _classify_elements_by_module(sketch, namespace) - - if len(module_elements.keys()) == 1: + module_elements = _classify_elements_by_module(sketch, namespace, + subview_sep) + if (len(module_elements.keys()) == 1) \ + and (isinstance(module_elements[list(module_elements)[0]], list)): warnings.warn( - "Only one module was detected. The model will be built " - "in a single file." - ) + "Only a single view with no subviews was detected. The model" + " will be built in a single file.") else: builder.build_modular_model( build_elements, @@ -1603,11 +1578,11 @@ def translate_section(section, macro_list, sketch, root_path): return section["file_name"] -def _classify_elements_by_module(sketch, namespace): +def _classify_elements_by_module(sketch, namespace, subview_sep): """ Takes the Vensim sketch as a string, parses it (line by line) and - returns a list of the model elements that belong to each vensim view - (here we call the modules). + returns a dictionary containing the views/subviews as keys and the model + elements that belong to each view/subview inside a list as values. Parameters ---------- @@ -1618,48 +1593,68 @@ def _classify_elements_by_module(sketch, namespace): Translation from original model element names (keys) to python safe function identifiers (values). + subview_sep: str + Character used to split view names into view + subview + (e.g. if a view is named ENERGY.Demand and suview_sep is set to ".", + then the Demand subview would be placed inside the ENERGY directory) + Returns ------- - module_elements_: dict - Dictionary containing view (module) names as keys and a list of - the corresponding variables as values. + views_dict: dict + Dictionary containing view names as keys and a list of the + corresponding variables as values. If the subview_sep is defined, + then the dictionary will have a nested dict containing the subviews. """ - # TODO how about macros??? are they also put in the sketch? - - # splitting the sketch in different modules + # split the sketch in different views sketch = list(map(lambda x: x.strip(), sketch.split("\\\\\\---/// "))) - modules_list = [] # list of all modules - module_elements = {} - + view_elements = {} for module in sketch: for sketch_line in module.split("\n"): + # line is a dict with keys "variable_name" and "view_name" line = parse_sketch_line(sketch_line.strip(), namespace) - # When a module name is found, the "new_module" becomes True. - # When a variable name is found, the "new_module" is set back to - # False - if line["module_name"]: - # remove characters that are not [a-zA-Z0-9_] from the module - # name - module_name = re.sub( - r"[\W]+", "", line["module_name"].replace(" ", "_") - ).lstrip( - "0123456789" - ) # there's probably a more elegant way to do it with regex - module_elements[module_name] = [] - if module_name not in modules_list: - modules_list.append(module_name) + + if line["view_name"]: + view_name = line["view_name"] + view_elements[view_name] = [] if line["variable_name"]: - if line["variable_name"] not in module_elements[module_name]: - module_elements[module_name].append(line["variable_name"]) - # removes modules that do not include any variable in them - module_elements_ = { - key.lower(): value for key, value in module_elements.items() if value + if line["variable_name"] not in view_elements[view_name]: + view_elements[view_name].append(line["variable_name"]) + + # removes views that do not include any variable in them + non_empty_views = { + key.lower(): value for key, value in view_elements.items() if value } - return module_elements_ + # split into subviews, if subview_sep is provided + views_dict = {} + + if subview_sep and any(filter(lambda x: subview_sep in x, + non_empty_views.keys())): + for name, elements in non_empty_views.items(): + # split and clean view/subview names as they are not yet safe + view_subview = name.split(subview_sep) + + if len(view_subview) == 2: + view, subview = utils.clean_file_names(*view_subview) + else: + view = utils.clean_file_names(*view_subview)[0] + subview = "" + + if view.upper() not in views_dict.keys(): + views_dict[view.upper()] = {} + if not subview: + views_dict[view.upper()][view.lower()] = elements + else: + views_dict[view.upper()][subview.lower()] = elements + else: + # clean file names + for view_name, elements in non_empty_views.items(): + views_dict[utils.clean_file_names(view_name)[0]] = elements + + return views_dict def _split_sketch(text): @@ -1694,7 +1689,7 @@ def _split_sketch(text): return text, sketch -def translate_vensim(mdl_file, split_modules): +def translate_vensim(mdl_file, split_views, **kwargs): """ Translate a vensim file. @@ -1703,11 +1698,14 @@ def translate_vensim(mdl_file, split_modules): mdl_file: str File path of a vensim model file to translate to python. - split_modules: bool + split_views: bool If True, the sketch is parsed to detect model elements in each model view, and then translate each view in a separate python file. Setting this argument to True is recommended for large - models split in many different views. + models that are split in many different views. + + **kwargs: (optional) + Additional parameters passed to the translate_vensim function Returns ------- @@ -1719,6 +1717,9 @@ def translate_vensim(mdl_file, split_modules): >>> translate_vensim('../tests/test-models/tests/subscript_3d_arrays/test_subscript_3d_arrays.mdl') """ + # character used to place subviews in the parent view folder + subview_sep = kwargs.get("subview_sep", "") + root_path = os.path.split(mdl_file)[0] with open(mdl_file, "r", encoding="UTF-8") as in_file: text = in_file.read() @@ -1734,7 +1735,7 @@ def translate_vensim(mdl_file, split_modules): outfile_name = mdl_insensitive.sub(".py", mdl_file) out_dir = os.path.dirname(outfile_name) - if split_modules: + if split_views: text, sketch = _split_sketch(text) else: sketch = "" @@ -1752,6 +1753,6 @@ def translate_vensim(mdl_file, split_modules): macro_list = [s for s in file_sections if s["name"] != "_main_"] for section in file_sections: - translate_section(section, macro_list, sketch, root_path) + translate_section(section, macro_list, sketch, root_path, subview_sep) return outfile_name diff --git a/pysd/pysd.py b/pysd/pysd.py index aeaacd6b..f6f21ee6 100644 --- a/pysd/pysd.py +++ b/pysd/pysd.py @@ -7,7 +7,7 @@ import sys -if sys.version_info[:2] < (3, 7): +if sys.version_info[:2] < (3, 7): # pragma: no cover raise RuntimeError( "\n\n" + "Your Python version is not longer supported by PySD.\n" @@ -61,9 +61,8 @@ def read_xmile(xmile_file, initialize=True, missing_values="warning"): return model -def read_vensim( - mdl_file, initialize=True, missing_values="warning", split_modules=False -): +def read_vensim(mdl_file, initialize=True, missing_values="warning", + split_views=False, **kwargs): """ Construct a model from Vensim `.mdl` file. @@ -84,12 +83,20 @@ def read_vensim( the missing values, this option may cause the integration to fail, but it may be used to check the quality of the data. - split_modules: bool (optional) + split_views: bool (optional) If True, the sketch is parsed to detect model elements in each model view, and then translate each view in a separate python file. Setting this argument to True is recommended for large models split in many different views. Default is False. + **kwargs: (optional) + Additional keyword arguments. + subview_sep:(str) + Character used to separate views and subviews. If provided, + and split_views=True, each submodule will be placed inside the + folder of the parent view. + + Returns ------- model: a PySD class object @@ -103,7 +110,7 @@ def read_vensim( """ from .py_backend.vensim.vensim2py import translate_vensim - py_model_file = translate_vensim(mdl_file, split_modules) + py_model_file = translate_vensim(mdl_file, split_views, **kwargs) model = load(py_model_file, initialize, missing_values) model.mdl_file = mdl_file return model diff --git a/tests/data/input.xlsx b/tests/data/input.xlsx index 4f04faeefbe3219ed529add74a23c755eb59eb6a..46a5695f579bff86d2cdbc5478304975f4fedbaa 100644 GIT binary patch delta 12848 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 delta 12517 zcma)i1yCH@+AR_sg1bv_cXxMpcXx+jfM5ZF4em~G_u%d%cnEI6-R+Uw|9|(K`_8R; zT{Sf`wb!@0xAeEx?*58AdR&o|iCNZR~j>}2f*(mtZr!$Osq`6+ z^Ita+r8lcU>($54n&G!0DK({slHp$4h7giG-!`@K;}~wKC7Ia1qAE>^t7+uoDYn}) zdENUd!cx!c)vialWVNW*N2hjq@VAD$W(lG1g;c$`@wC87#=d+_P}m={p6mWbWi14z zEC&N?Sr**94h9Cs2Lbl>|1bSl1p@Ps8QP7lroLKTHaCMlQ>%F(N{}eAS z{bT91Q;HK>EO-HF7s5E}RP^awI;1euXIQX4PDFc{eDp-%YD%kXMJh}^ zRf2mX+WS|U^i{yqvh~1#Soi%$@awdzY5mfHX?6>4Dw<>~e-WI-e*Hpi5!Ig=uB5QQ znC(oIB+6nl9Raq1#Jr!q%CogSTd85&3E^Xj%t`pD6g}|2b}|_$@M2BVRrjWrhqBIMoOtkautNxo-3=5JkF>!LhqfY5`%^WWMA|aLdG@nn)Z4&Cv)hvGid2 z!#R?uS^ZS0OIUWz8mBjO69GCGJGm2m=O!=vi|*Rj|&J z!gHgt}xfiAxJbAZAUa^Mw5)2pk&i^Q?VsN;}n5 z)>f*!J}|4&et>N9cPz|wf$A!v$KBV&*+0fl+tf4d_GWVw z?(+IT*8xwrr;~#Dj1iy8ie|HOS{&^?!aCrHAylFw-ZVl$8q)Jl%#d`81uQ9?LKh?l zNk)vV|8lOX?bCVV4rUbeXyKZ6AhD@cSH9s-JnBmq)?>6>0aY?|W_$)_oW zoc;!uHFH-@({E_DdaNr-uNm10Z7k@lDDEsB?)Ofsgo2x-k0H1kwMr@2Svy4U7W>Du z1n4BoC^fr0`ICK;wPU2KtxUn-!Q9wx^vg*1d+K87C1DVc=cPHE3EN?%cGZbgv(J~l zgQ{kzd+(oEg~34ErX|R(&s7EF^FQ;~PvGpKrN~{3&CENiNf|m@_7_oBSFF()<$K5dz1yxJJ(RUqQAno%4WY9uQK%SO5 zO@j6bRM#cyORo*7T(IhJ3m(bkaWz}+VwtP7P2AhsLPM`X(AZr zDhTSYw30)P%?ne)S0X|NGpRFmo(PXRCe*W zQKRpqgO|+3ZumHijN@PbX+eJL z&|tLAR!F)hp$2V+soBMlNa~U5A@4k&%pX10H*;zLqwzUu#`}UWI%V)MIK^LWrNYNI z6;E)-pS#O<(*2nnU9NBQknxoSdUBvoU`R$EV7JSy&5+(BgbDmksYt7N$B*W*6e2Uy z-^lN8qEzu9;8dk$bVmg~G}bp~>g|1Li#k9S>akQ5?2!C1_5zSAe;7x(8JpUXgl#0Z zliY=C8r)7nAG^h2jpedEfQzn+*$x@7>qM(W9}idCz{vD8AU+zS+|Q)PA`$CQHWR0D z_tcNpRHg0ikpUbsNQ!9Z2gv34xZ;tTW;my)Ula5{d%Ij|vFz-Sl%uCdu=p6RiCM^~ z3jGq+U8rqp>DhK90qpSubi+SMgZS|bzP?XyR;xrm2wl6Y8X4QLeRlEQeX>)yZYgM! zpD>$+o9ufia7!H!h7awmd$yl#A6s~AmfcxXkm+w!8vq^zF?9*f4_ePhQ{RxHlOX^H zRT?ob6PzeP`~IvMF-ksY7PZ=0?k+m5sKm#o`2HQz|M`i zckeI1_0sAyz`sy_(w$9PO~O>V6)DlnJni~i5Mfb*@c8RBRhbQ6rGsL8>=HtUVgCB~ zU^MyKAY2;8J2zK#n)(+8B0MEjRIox%-n!};^&Lhq0#4*pc0$EfrgPxs`+<(N|TLgFU``qox zrm=pEH!W%5-}&|L4QYr6v15%owPZbhwi57?yGN6FvGBeT+zE?ToS?M3RViA9@D(Ej z1A~=FahmBPWTp^IUA0k4lwRK*C<%T!D6D_M5$Pgym&Cylk+bp)*UoZl zW|AMKe5+m&14w?x6T{X|{{BD(p#mF{eZ3-rNDEOctwQE-O}r$zv!)Iwe+B2h4VEPX zg6ZmIaO#W`?B!d)<>Ysp6;j@?xUcwGsQ-eSy-O>d!QGg&d8T^3{ON6E!=%TNti-em`-D>S6sh6ZMKultamc+xI=|>wh zBB~7^G$QJaPiRCm8);}nv>T;pM06XiXhifIBWOenjadvM0Aj`w=eBqG!PX?a5#Xl5 z+9aG2kek8YB!bc;&4e5yfE&3uh1F61iASN~`z@tz9$y;_ z3Z^*ZHxWb>^nQq15dswKe#mnXh$4rQva1d?4dW8{f9ij3!?AkO>M}_UG6X(p4LXD# zX$>|+25AjG#4>3OF$6vYprHPvm~6piaH5rbcOB7Xj_wfj@Wg6-9>J7q+7 z{FAov*e;p^y9#Ds8!Sv#z8PiPUb#B{02W{LmziN-Cu5&`Woww^*HxGfNXXsL(Up0B z#twVLau=dEG-qWK6kWwi-}->Nq*TYVG=Uy4o+HGiUI#hY?V<#)y1Vc`-D&-#4aK0=yC~ z3kT4r`$;`9Jl0t(6$}SzA4<@pO+P*KxW--P)AqKY#m0pkRuk9%nsU(3YCA3Z0?UNA zvXHMxt8}<#Mt!JxEx%Z04xL5F5SlzR*5mL@GK80DF>#qnxUeLsCkY~_iR8jpDXFj> z6jItBj#Vk_$`)EGhlz?mQ^a+qJ;$m zTVeeV(f_0MH7J-s`XGl9PYVy#Rims=GoHm>+7)`9D<-G?_tk$%7 z9vv=c9^UkDOu#IO0(yA#ajCQHqW%8s&yzQ?$9@xg?mFPszr>!O>9^Rssiu!74VdyM z^>Jr3lK;dMVufBi-ej1$yNRo-u!W(~M$5aTh?_G-D5ulW0_&Hf-S zIp3zkVfx$*#dKfQws>*H&GB_?`TjWLQNCG-wU(W}ljs83bY1`FPqobcxn|3l`iVB& zCp94H@n)fE=oGn1ia~0kJi%C>)y;63(8aFzbtn*RnlRgG{&H(1!viA?L~1Zs}2n_V#HG1)ZP5Wa;xlu1{w)E;+K8U{ST(O8sSKwouT!Ob007 z{<>GiD|uOkx+AzNy1h}&U3Oz?c`@MH+Gem?QKgiI7W$_3o-W$0D8whH`6O8cGZV|| zen+7n$U4(h*7`Aj@MU(Tb-M`aYKLZc^ca0m`OLczUbkt3@7~sb|M18isqM)-E%}-Q z$5D@ZE!X>%ze|Gi&;iie)?@*gy9c5Ml>D>S6OBs*D9LD2D%OdDzvNMf0hCh7IoG?u_72^&E!;S|N@sb}gi3htV{^x0yVD(9y&&({FkZ`qwV)p4^ z|La1kV9MGvTiWDwS9Y{DHKKFp2nofAKQl82V+|4@*Glf$&b;Hs(1$020_KR;5PUhY z-8`tW_Rxw$zF1ZqfPL`uVHX{O)x|O6@9#%Z<+Ee&ErxTZZo%M(7YsG#P+rOSRPHBM z?gx2K@~T#fBT^oP+Q9ta{afW(cdbJ>@J#-fK1bA%cEQ zgp%N8#@||Ft0qF+<@W)#hTlGlBnl zFaeQI?!UA{$MIjkk3-ZG{^xe!bW=3{b!RjpKHEP7rY%45Upsz6T>kK1+Ofj;uZhAC zGjRS(`(QBt^<*%j9>PDi123jsjnaHutGiu^#i70lyQ4t%zpE9tM}g^wIv4hj6UNo` zq*G&R$JF-x_=N;7;=O5LDhXBu(q>=_2~h+x8nxJ=k;Mfi%#mw1C(Mysw>B%#mleC(MyocNol(H;!&Nty;AhrgnJn-)L0+&7*8_;PD0GQ28INiG?*`nS954 z4P(N8wkC*`Vo9Y7dGHltZou=>kO%c2Pr=&oUZdDBoVMouKYI#eLf%sj zl9CE?|UJ&8;WWS`55pav_G=Qwd6 z+HaSw$l0UhszNnTMFjX>0t?XRnpvktn>_eb-&+12l->M#4CSy%w^fFZ-8(9dYqh3YW*h5iy_x& z)a_M@OEYo7_GS%a>p3^`QL_rhj#i#P6!j{oFTWtvW%`{{REoE@T=HJ9eOiY=i zzm{X^1VB&@XcBHSl{oqC!T05;iyUSBr zzyY1nLGjWUGZ!k9$W*i47B52Z*D`2fO>b8V$qWt}@#EI$7+>7>s|!2KRr>w ze-ksNxg zZ_u23qQ>a#hHRp+5Cya$C9vP#40R3XD18e;=7=r9*kre(d0OI%?4mxSmKjMm@S)%h zD~@qBMxxz{{wfC%g#uXmK-{_&f_%aQ3wc9p#y%tl+<)TZm=_slewOvG8p8{v*>!;X zu`V?pKv|b15P$kJj!SPV&*Q5P{}p7mC~3RMxv%-^Qge9|1B%QO`ns6zgMFc%tFw1t zJ4#WjkWJ=HcNenhcxCiS%pmbLGwDXKRj`omP8g6yNyjm_$8t>U>yVvhi}|m6)bK^K z#cKCUV2v5+>CQQaFQv&w&yZaMLRv)7t(3Ln+GU)SB|RFRYYqh}K;9WAGAKQaN;}1C zx`X92muS8eawxxoXSqh(3#C)IX8g3@m8>T#(~u?$VK$r(D&6=kL9GU z9dfvzSik(*oPFgy0}x;%;Q}a0aFG_6#ASSa#RtWd+^HHAB&z4Xok8JIPBVvw;j(j2 z1DV6apx9fcYeK>_MvTwRF>&pLn(5ysR}%h(P*E(1u(IhOaJFUtvL#s@%9b#MP$tr&d{g zu||z;^0Uo@_##j4ayM~_Q^!7EuUKDf^!oB)gx%@F@+7Sf>+*AGF|yqaQySaQ!{uI^ zwo>)8XurWLhJ6P&xorXx2F&D?0$HqT3@mDJsh(1?;~MWO`Vo@AMq6}EOFw7=_aY+O zp2Oyj>FhYXU%IXu?L3U=nq`HHEPeY`P~&!6Wdj!uL=XGs(Bv3uc~jyee|`+xA?hCC zpJtqctYlUbU{ynnW0R5_*MLYAN2o;BlB3svh_>*E`SQiCIsGMIG5CRqVNdfA{vxKA z1F@p%DxyGYmqVI3qT+UQo}^|K_?^7`@>418iu-4Wy}5N68|$>i2eLNkAhu@cptT6w zIJ+<9k=}b?9Ff*z$kH5MHyp|_aV$GaA+Z~qM2(XrmtRra0 zj@zNMq|!4Q>jS#SAb6gC%>tAbi=!;{O4Hw-P=`6`>gh=-+M!r<+M<>~1664E5K==0 zjhlrPg)l|O%Ir%==iNE%Z!wo(^$hcRKk8UqOmO;uoXs0RazEu8coH`r9Ya;GAf6m zQ@+7pWZTn1j-^8I9M4nZ>-%y=;k`?y_BHqEN!8prV_mBFE@>9BFX@<7%)io}`pF?B z;ku=DrM2Vm(_>x-kggMLo_%pe@4T+u4)^D(1^&)_>FGIhp2#>68qf!&XVH{GQMquS zpq9rWL2QiN^1kXid3ybGKVm340h_#>-nyT>&Uz`5X4B3q<~N0IzV|ztzK3O0xBfOa z-n%K$6yz@b1v4j@uXhE{@;JccL)|9V*OzT$1z0)*y#bex2Bs9$Jy$H=eX0`;4LP0f ztw;N-?wbv+=$*Lg3xIIrqo$`*N9(Mgu(3MKuTurmk)%Sv6vx=kFioG# zr#fzU_7~*ea_3rkT>cdr3`~*YALI_m&gku6-=U-6mLZMOaax_rz1_g_P$PIGEz2Vv zq0Zwn>dC3$huf&CBKu3?3br4j# z((!8%O96tY&Rn1oDCkQ()RZKX5JIsDm-T~*yq1Ur&lqnJk0Wag9cz0*Cx;ER(b*H3}xV zy<=-qBlh^8VT07DPd-V=cUpf2-q`1Vd_|d$lt_ql;t%PpkWN(5mK#YIRK91MJG_X0 z1*>`? zRu(QAzFiksYvsS_fm_sl+i25hYeEE9a*>=TSD9m~CX~S7eG~p(6Tiy{OhGmG@&1(K ztu6t(AGsZ`jz>a_{h{IO09N`Prw3ow zk83XgpJY;A^LFUokCf*!UxI9Ipk`d;vWmlZ3+j(*`9xVA@XKU@jy8F_W4QqaKW#0# zJmick4^rXvU-fSWXZ)bkfgN5D`ga4Ot>6YLQ6^T-JOr`ONlMdLIkYn-tzO`|fx{jEfp5@&oUee~nXFv6ekvBKC(vEXmu zM0AndnaFHB7fI@6a=({OTGE!+!*m*V4|EeiA|QI>Xq$FdgTGfd1SYNVx!nus3qtE7 zjw$xb7>;sqkg8wO(MoQ8^=kRz>)Kh zGJ?c|HppJem9s~I{)xtou^<7R7HtTsWyXD`?6aGwv!B?^8U4=e<7^3nS=<&;uyM7Y zX|0U*p))gKjVm#v1De^e3X(+v6TY7Tye-$l7jI4UI;uvxJDUf!8(8^4K?Vj*YENpq z_?@wnM~|?Nf~<-8Rtp@scY4K1Zzow~tmS7^5<_Lc-hdtg#MNAt5b8`YAfsPACi623RQ zj#tM3joJWWKX5V}R1xd-XreqLy+VRx8ICbtVF$QN`5Hed$Peho=CV4|C>8n|Rpt#u zPahjGT}7a57{2~kXmNvO%P0)~aKSiNpkpWYDV>H`GW@V_5rrICJ=yIm682FNRf0I9 z0tIGc-g+|p2!vY?O<3+)gf^Qr!TyHno@wD8&^UjQWT$Aml*kv^x8MMVa{)c2CL^ZO z{!083#dx5#!hmlMj8myHu~41AMyqT&ZjLgVj*_)>H)8GbPG-#!yYGjqdvjNXrAJdy z?)N5>#e*TA{&28nm7u zRfc|67&jj<8oN5wu_qsV3o94rJ^3A-I_MHIOPAwOa@4&>CiFCIpVLQ1qyKA*?N_mhaPOn$6SKawiJj>dI zu#sdu{TQw@qqMge4z&8G3NYe3$KHL2=s>P=pEF|UAz9%#1w?o!=fbiC_)21yfuP)NZPsI|&~*Me0m@X>=gcZ}>k~A*%$(Lq zy1fyzK}*`4NVR7NZMT;SQiJAlHP+a?XNCBeT?|@%81S}% z*7Q=~k9QY-g!ePz`N-wN?)$rYtXXM3HYCTr2xl8Ga2klCK;NvFT)!Wek4Y+VV-R)= z8?&d~T923))bYIz*?fSH&8ZM;u(qbtBtT)OY9OgqBnBQb)EW;;SQ9TOl8UF)R^>KU z2FFyBaQmnWmerO-Fj7^gi*(B*cX{8s=;Q#i8yi1$v3nM}%E8zY(hXmGvHLr;CQ>(~ z6Ya+*a{TROAoNmqKXfstvjZd>e(G{}FLW|z&Bd$n^nN@NZzED&kPIBRa&`}3`Fvn{ zIru2oy=w72^X(zzO)xK?tFo1+GAA>S zoy@7ONF%NQ-51V2R_l37ZIh{GM7mLl3=@e~{zZ#9pr!s-LiN{Sk?YUQYp@`vfwQ8u zIL5U|lkY+}J0gP~873ZSgy!SR(dbci-10g^vuzLfW=!LkRqTTu16d)Qzy zn+1D23CI@u>zyVBJ>Ez9Bb~^1$B;}p%Uxu(q|>SCK`qO;`iKt$iTXypI5sy8okCv% zy4N_gjDP7-)u#0xLZI%Ixg2Zoi6!?IrkkEi8`kQ#^qmn<3J6%+ZULxcM<3WIcPR(-IE(YpT2f>aH*hL2K(P6zrd@9q#g5hi! z^hFU?M9==UqjUhC1D-6yUq6?0YLqm<{()KH7Zr`zm-xVvcPXzv+ny8AGK$jqu)(II z>|Z#N&J1u2oemd#`L|6{odn*jri_rnco%Vir9Qwn7$C>nTq7rN_O2F4dWEa%=5(1~dKkC!-I8J=cj*yX?cp(m4MG1t)*?QGDi&&FK|dV4=k*Eip7GyZc;n*UQEmv|O zcGp;l(r85WQ2i2vg)|}<==~&stPA}GYoT8mXJ5$t_-!Xj#9UN-@xFe$M>QNYgTg~& ziy$rkLYQ+w+rrCjh1C_REjjBJ-zBw9fZV(hoPj;`El1-0lZX!-c33Hsa4jPo5H&rI ziAt}@a@X$UX%~)YTDkqB4u*P$73vOk9z%PHHtV(1WOWKU{szR$hJ7{PRJNi?mv}%Z zNn4oPU>8P-mp*F)o`(9M)Z&zgza;6Ir3c|Ft9k_|b*Do72BDlhq|LD9#Jeb@f)zk{ zFRXlBzz8P^9@r?FDy$ieF|TqX5D*?+(%Ik&9}@DjIF*g`ER}pM%He1&0_bw$R}Hqr zO9>$%>ce;F*WBUM=(-w@O`S=9H>h0=z~uy+nm2a|Z<0R2&|J-2#4a)2@OxLur&-t9 zaZoi@nf-DCAKY1Nh}H6_pJKW1h?v}_3ZZc~SSl00V2zR;T5x7cz{zU@)DUDV=V=>) z^shCl#cM43PAr>ux)=i&?G@6-Ij)?|#7&}nu7e_G86Vsc-YD#{C5k^hnoB8>A+(do zy2L&D%Lv8QzQ#9r`^ReR!}lT16{Ou6MhGssh7-#I6O5xcTm8_V*|4~TVdF|6(a2(0 zRHBpb#RRipd4v#kvSHyefN@|P_92MC`Z$yDG|%zg?3fLf+wABeQ~+iy`2!h|)D;JR z%-yFD&>G#w@wRkt6j5#NW$$*RT>a<@%BAX2I=A-^#&!oj`?cV#&J!OR>KFL$C@e!G z!|G!rFU&AQB2|vJd{{f$jC4$ps^a;$z!zTlt+Hpp_B^3Mc`Dj{Ycc(pT zq#H2P86J-g^yX$wf$^86KuaZ6?7YVQI5)l7c+7%CyM6HsY9%!KM0Q4zu~N=z>gNVK zrbBuN*)bD7KdHK>zMs#XkA{KwXCY>Wez)Ac#H8dGQ&LR>xK*+xQTU%%x-zJ(P3TQ! zc3(+#Rrqh8){W^ZsDEXnr%YIF9|`?V2{^XwOGj@-S&{I_|B(~wn7G0J6lL)-vqPvz zfLxeu;p-Iz(Z0l4p&^umc9?~6>rNE1d)}%l8^Ql079_~R2>iHpW^M67jq<4b64YQLhoZw4k-PU4{=z}E z@>mrW9}0hgL4U4q51|{iLvm`<%&i1N=O(qG)}qnXDW@I`tIjno-oyDG6s4;*-lon} zYwACd9Job|w*iEwr%SbHK}Af`5|Wpb2xN3X?hQW2P zB1C|?MMfYfyMwD2-K*WeX~C(~F(6o|xVnDcmfF0Y_C2*+%tz&_ARTH5D&clds_l-~ zt4t+jEGZWZG_(Y2DzP8HT$X$Jvw=s+nkc)h4r%R$oArtJa}llWg61pl>=DfGsT@x=^Y?t4iK~Cr zxWfOt$QN|M$^@ivw-cd8`oVr2(m=@DpqKQ4(OeevMMlNv5o$W{(!cImFXuR?vSig9 z1}E6FNzzST1zMB0=(tKUXF{)2TqmNJ)+93&kB~H%187YA@se2_iB#?#zHLM7w`y28 zSL6;@?h#I!EPP*7HJ+o0LtciBxsh5Jd>IiN3FAm(%rC&7MnS(s^$9nDd7~pU0Fz|` zz^2Q*@agA>Zrs+Q&r!YXZ7?s|j0w&ROZB&sa;P9G|AH1x&Diz#U;EhyeAcUnh9|UM5PlEHmZ@muxB)R<7}Wya{b)S~Z5Zw!=;URqxtQjzJeFFnLBuOPNT=mk3QC8A`i zZRYcF9Q$=u6RpZo+Nns=F(=cd<%6FTz?v81q+DVMOge5Mu;bn)EJ=R+>|fnLLz_91 z60%qUCc>bJS6ID03VZN0H~y)3DR?pn@$ngTB+;G3;0OW+h)1;$H)FA%pV0_hJ@Dm@ ztZD*?&`PKuCRokhv%+bpMzJaXfL@oagY-IYXFH15h-GMLqB5eh<`F_!C!;_(y^g>} zx{fPbXSsc8N3uduD0T5tzk|NONI?1Nn$26}A!C@N!%x@PJ+Wq3P<)YT)vexyK@?oc zDV)XtmPzdb^e`1^eUe;jQ-Eu7xQ@Jn8KDdOFvnuO)F1OIP23lA$9)_TTmSZT8WHSN*?5+MrQRdbqcG zP}M&rqo7w#Od#%CkoQ~px09=#sgsl4@6FbKhEK7y(HFn1{Z(`@Fr>F>|Mx-uwrhI} zc60Z!w{ZKX(7#rx;QTx8?bo}v_fT)4pj~1VPyrVz$?vQB>-(v{quo(J8^oBP8Wbqd z2^ZoY@L!+q{T+OO^9Rht{V&$PZZH21JR*Jr;(>g*|Hb%M7x#DImk)m!+y960-?<$O z?C-!p+TTEC&VL$<=t#(=%eS8Iz8OWlzl}v5h=qsdPpAHx_%Z&5Ic5U+@{s)j{WTw< z{ta}-4EoALO#FK+|9X$UxzfKJMW6>B;y*-xSu|j8bNiQLl?~*}OZfgh*n6<7HxY~B I_}%t@0G}u7_5c6? diff --git a/tests/more-tests/split_model/test_split_model_subviews.mdl b/tests/more-tests/split_model/test_split_model_subviews.mdl new file mode 100644 index 00000000..e302c23c --- /dev/null +++ b/tests/more-tests/split_model/test_split_model_subviews.mdl @@ -0,0 +1,105 @@ +{UTF-8} +another var= + 3*Stock + ~ + ~ | + +"rate-1"= + "var-n" + ~ + ~ | + +"var-n"= + 5 + ~ + ~ | + +"variable-x"= + 6*another var + ~ + ~ | + +Stock= INTEG ( + "rate-1", + 1) + ~ + ~ | + +******************************************************** + .Control +********************************************************~ + Simulation Control Parameters + | + +FINAL TIME = 100 + ~ Month + ~ The final time for the simulation. + | + +INITIAL TIME = 0 + ~ Month + ~ The initial time for the simulation. + | + +SAVEPER = + TIME STEP + ~ Month [0,?] + ~ The frequency with which output is stored. + | + +TIME STEP = 1 + ~ Month [0,?] + ~ The time step for the simulation. + | + +\\\---/// Sketch information - do not modify anything except names +V300 Do not put anything below this section - it will be ignored +*View 1.Submodule 1 +$255-128-0,0,Times New Roman|12||0-0-0|0-0-0|0-192-192|-1--1--1|-1--1--1|96,96,100,0 +10,1,Stock,497,237,40,20,3,3,0,0,0,0,0,0 +12,2,48,297,243,10,8,0,3,0,0,-1,0,0,0 +1,3,5,1,4,0,0,22,0,0,0,-1--1--1,,1|(422,243)| +1,4,5,2,100,0,0,22,0,0,0,-1--1--1,,1|(341,243)| +11,5,48,382,243,6,8,34,3,0,0,1,0,0,0 +10,6,"rate-1",382,262,21,11,40,3,0,0,-1,0,0,0 +12,7,0,1141,258,150,150,3,12,0,0,1,0,0,0 +Stock +10,8,"var-n",207,367,18,11,8,3,0,0,0,0,0,0 +1,9,8,6,0,0,0,0,0,128,0,-1--1--1,,1|(288,318)| +\\\---/// Sketch information - do not modify anything except names +V300 Do not put anything below this section - it will be ignored +*View 1.Submodule 2 +$192-192-192,0,Times New Roman|12||0-0-0|0-0-0|0-0-255|-1--1--1|-1--1--1|96,96,100,0 +10,1,another var,89,168,36,11,8,3,0,0,0,0,0,0 +10,2,Stock,334,243,29,11,8,2,0,3,-1,0,0,0,128-128-128,0-0-0,|12||128-128-128 +1,3,2,1,0,0,0,0,0,128,0,-1--1--1,,1|(221,209)| +\\\---/// Sketch information - do not modify anything except names +V300 Do not put anything below this section - it will be ignored +*View 2 +$192-192-192,0,Times New Roman|12||0-0-0|0-0-0|0-0-255|-1--1--1|-1--1--1|96,96,100,0 +10,1,"variable-x",191,176,32,11,8,3,0,0,0,0,0,0 +10,2,another var,223,395,45,11,8,2,0,3,-1,0,0,0,128-128-128,0-0-0,|12||128-128-128 +12,3,0,461,148,43,11,8,7,0,0,-1,0,0,0 +This is view 2 +1,4,2,1,0,0,0,0,0,128,0,-1--1--1,,1|(208,292)| +///---\\\ +:L<%^E!@ +1:Current.vdf +9:Current +15:0,0,0,0,0,0 +19:100,0 +27:0, +34:0, +4:Time +5:another var +35:Date +36:YYYY-MM-DD +37:2000 +38:1 +39:1 +40:2 +41:0 +42:1 +24:0 +25:100 +26:100 diff --git a/tests/more-tests/split_model_vensim_8_2_1/test_split_model_vensim_8_2_1.mdl b/tests/more-tests/split_model_vensim_8_2_1/test_split_model_vensim_8_2_1.mdl new file mode 100644 index 00000000..d645c2d6 --- /dev/null +++ b/tests/more-tests/split_model_vensim_8_2_1/test_split_model_vensim_8_2_1.mdl @@ -0,0 +1,143 @@ +{UTF-8} +Heating= + (Cream Temperature - Room Temperature) / Characteristic Time + ~ + ~ | + +Cream Temperature= INTEG ( + -Heating, + 10) + ~ Degrees + ~ | + +Room Temperature= + 70 + ~ Degrees + ~ | + +Transfer Coef= + 0.37 + ~ + ~ | + +Heat Loss to Room= + (Teacup Temperature - Room Temperature) * Transfer Coef / Characteristic Time + ~ Degrees/Minute + ~ This is the rate at which heat flows from the cup into the room. We can \ + ignore it at this point. + | + +Characteristic Time= + 10 + ~ Minutes + ~ | + +Teacup Temperature= INTEG ( + -Heat Loss to Room, + 100) + ~ Degrees + ~ | + +******************************************************** + .Control +********************************************************~ + Simulation Control Parameters + | + +FINAL TIME = 30 + ~ Minute + ~ The final time for the simulation. + | + +INITIAL TIME = 0 + ~ Minute + ~ The initial time for the simulation. + | + +SAVEPER = + TIME STEP + ~ Minute [0,?] + ~ The frequency with which output is stored. + | + +TIME STEP = 0.125 + ~ Minute [0,?] + ~ The time step for the simulation. + | + +\\\---/// Sketch information - do not modify anything except names +V300 Do not put anything below this section - it will be ignored +*TeaCup +$192-192-192,0,Times New Roman|12||0-0-0|0-0-0|0-0-255|-1--1--1|-1--1--1|72,72,100,0 +10,1,Teacup Temperature,307,224,40,20,3,3,0,0,0,0,0,0,0,0,0,0,0,0 +12,2,48,605,221,8,8,0,3,0,0,-1,0,0,0,0,0,0,0,0,0 +1,3,5,2,4,0,0,22,0,0,0,-1--1--1,,1|(508,220)| +1,4,5,1,100,0,0,22,0,0,0,-1--1--1,,1|(377,220)| +11,5,48,413,220,5,8,34,3,0,0,1,0,0,0,0,0,0,0,0,0 +10,6,Heat Loss to Room,413,232,49,8,40,3,0,0,-1,0,0,0,0,0,0,0,0,0 +10,7,Room Temperature,504,373,49,8,8,3,0,0,0,0,0,0,0,0,0,0,0,0 +10,8,Characteristic Time,408,164,49,8,8,3,0,0,0,0,0,0,0,0,0,0,0,0 +1,9,8,5,0,0,0,0,0,64,0,-1--1--1,,1|(412,187)| +1,10,1,6,1,0,0,0,0,64,0,-1--1--1,,1|(393,308)| +1,11,7,6,1,0,0,0,0,64,0,-1--1--1,,1|(477,336)| +10,12,Transfer Coef,541,164,33,8,8,3,0,0,0,0,0,0,0,0,0,0,0,0 +1,13,12,6,0,0,0,0,0,64,0,-1--1--1,,1|(484,195)| +\\\---/// Sketch information - do not modify anything except names +V300 Do not put anything below this section - it will be ignored +*Cream +$192-192-192,0,Times New Roman|12||0-0-0|0-0-0|0-0-255|-1--1--1|255-255-255|72,72,100,0 +10,1,Cream Temperature,363,287,56,15,3,131,0,0,0,0,0,0,0,0,0,0,0,0 +12,2,48,680,284,8,8,0,3,0,0,-1,0,0,0,0,0,0,0,0,0 +1,3,5,2,4,0,0,22,0,0,0,-1--1--1,,1|(611,284)| +1,4,5,1,100,0,0,22,0,0,0,-1--1--1,,1|(480,284)| +11,5,0,545,284,5,8,34,3,0,0,1,0,0,0,0,0,0,0,0,0 +10,6,Heating,545,296,19,8,40,3,0,0,-1,0,0,0,0,0,0,0,0,0 +10,7,Room Temperature,532,407,35,16,8,2,0,3,-1,0,0,0,128-128-128,0-0-0,|0||128-128-128,0,0,0,0,0,0 +1,8,7,6,1,0,0,0,0,64,0,-1--1--1,,1|(608,364)| +10,9,Characteristic Time,544,188,35,17,8,130,0,3,-1,0,0,0,128-128-128,0-0-0,|0||128-128-128,0,0,0,0,0,0 +1,10,9,6,1,0,0,0,0,64,0,-1--1--1,,1|(593,241)| +1,11,1,6,1,0,0,0,0,64,0,-1--1--1,,1|(460,341)| +///---\\\ +:L<%^E!@ +4:Time +5:Teacup Temperature +9:Current +19:100,0 +24:0 +25:30 +26:30 +22:$,Dollar,Dollars,$s +22:Hour,Hours +22:Month,Months +22:Person,People,Persons +22:Unit,Units +22:Week,Weeks +22:Year,Years +22:Day,Days +23:0 +15:0,0,0,0,0,0 +27:0, +34:0, +42:1 +72:0 +73:0 +35:Date +36:YYYY-MM-DD +37:2000 +38:1 +39:1 +40:6 +41:0 +95:0 +96:0 +77:0 +78:0 +93:0 +94:0 +92:0 +91:0 +90:0 +87:0 +75: +43: + diff --git a/tests/unit_test_cli.py b/tests/unit_test_cli.py index 997a0457..f743dc96 100644 --- a/tests/unit_test_cli.py +++ b/tests/unit_test_cli.py @@ -215,7 +215,7 @@ def test_read_vensim_split_model(self): modules_dirname = "modules_" + model_name model_name_mdl = root_dir + model_name + ".mdl" - command = f'{call} --translate --split-modules {model_name_mdl}' + command = f'{call} --translate --split-views {model_name_mdl}' out = subprocess.run(split_bash(command), capture_output=True) self.assertEqual(out.returncode, 0) @@ -248,6 +248,66 @@ def test_read_vensim_split_model(self): # remove newly created modules folder shutil.rmtree(root_dir + modules_dirname) + def test_read_vensim_split_model_subviews(self): + import pysd + from pysd.tools.benchmarking import assert_frames_close + + root_dir = "more-tests/split_model/" + + model_name = "test_split_model_subviews" + model_name_mdl = root_dir + model_name + ".mdl" + + model_split = pysd.read_vensim( + root_dir + model_name + ".mdl", split_views=True, + subview_sep="." + ) + + namespace_filename = "_namespace_" + model_name + ".json" + subscript_dict_filename = "_subscripts_" + model_name + ".json" + modules_dirname = "modules_" + model_name + + separator = "." + command = f'{call} --translate --split-views '\ + f'--subview-sep={separator} {model_name_mdl}' + out = subprocess.run(split_bash(command), capture_output=True) + self.assertEqual(out.returncode, 0) + + # check that the modules folders were created + self.assertTrue(os.path.isdir(root_dir + modules_dirname + "/VIEW_1")) + self.assertTrue(os.path.isdir(root_dir + modules_dirname + "/VIEW_2")) + + # check creation of module files + self.assertTrue( + os.path.isfile(root_dir + modules_dirname + "/VIEW_1/" + + "submodule_1.py")) + self.assertTrue( + os.path.isfile(root_dir + modules_dirname + "/VIEW_1/" + + "submodule_2.py")) + self.assertTrue( + os.path.isfile(root_dir + modules_dirname + "/VIEW_2/" + + "view_2.py")) + + # check that the results of the split model are the same than those + # without splitting + model_non_split = pysd.read_vensim( + root_dir + model_name + ".mdl", split_views=False + ) + + result_split = model_split.run() + result_non_split = model_non_split.run() + + # results of a split model are the same that those of the regular + # model (un-split) + assert_frames_close(result_split, result_non_split, atol=0, rtol=0) + + # remove newly created files + os.remove(root_dir + model_name + ".py") + os.remove(root_dir + namespace_filename) + os.remove(root_dir + subscript_dict_filename) + + # remove newly created modules folder + shutil.rmtree(root_dir + modules_dirname) + def test_run_return_timestamps(self): timestamps = np.round(np.random.rand(5).cumsum(), 4).astype(str) diff --git a/tests/unit_test_external.py b/tests/unit_test_external.py index 3e852165..a3cf641b 100644 --- a/tests/unit_test_external.py +++ b/tests/unit_test_external.py @@ -2687,10 +2687,10 @@ def test_data_interp_vnss(self): data.initialize() # Following test are independent of the reading option - def test_data_interp_hnnm(self): + def test_data_interp_hnnwd(self): """ Test for error in series when the series is not - strictly monotonous + well defined """ import pysd @@ -2700,7 +2700,7 @@ def test_data_interp_hnnm(self): cell = "data_1d" coords = {} interp = None - py_name = "test_data_interp_hnnm" + py_name = "test_data_interp_hnnwd" data = pysd.external.ExtData(file_name=file_name, sheet=sheet, @@ -2711,9 +2711,64 @@ def test_data_interp_hnnm(self): interp=interp, py_name=py_name) - with self.assertRaises(ValueError): + with self.assertRaises(ValueError) as err: data.initialize() + self.assertIn("has repeated values", str(err.exception)) + + def test_data_raw_hnnm(self): + """ + Test for error in series when the series is not monotonous + """ + import pysd + + file_name = "data/input.xlsx" + sheet = "No monotonous" + time_row_or_col = "10" + cell = "C12" + coords = {} + interp = None + py_name = "test_data_interp_hnnnm" + + data = pysd.external.ExtData(file_name=file_name, + sheet=sheet, + time_row_or_col=time_row_or_col, + root=_root, + cell=cell, + coords=coords, + interp=interp, + py_name=py_name) + + data.initialize() + + expected = {-1: 2, 0: 2, 1: 2, 2: 3, + 3: -1, 4: -1, 5: 1, 6: 1, + 7: 0, 8: 0, 9: 0} + + for i in range(-1, 9): + self.assertEqual(data(i), expected[i]) + + time_row_or_col = "11" + py_name = "test_data_interp_hnnnm2" + + data = pysd.external.ExtData(file_name=file_name, + sheet=sheet, + time_row_or_col=time_row_or_col, + root=_root, + cell=cell, + coords=coords, + interp=interp, + py_name=py_name) + + data.initialize() + + expected = {-1: 0, 0: 0, 1: 0, 2: 1, + 3: 2, 4: 3, 5: -1, 6: -1, + 7: 1, 8: 2, 9: 2} + + for i in range(-1, 9): + self.assertEqual(data(i), expected[i]) + def test_data_h3d_interpnv(self): """ ExtData test for error when the interpolation method is not valid diff --git a/tests/unit_test_pysd.py b/tests/unit_test_pysd.py index 2ebd4751..ff00b56b 100644 --- a/tests/unit_test_pysd.py +++ b/tests/unit_test_pysd.py @@ -101,7 +101,7 @@ def test_read_vensim_split_model(self): model_name = "test_split_model" model_split = pysd.read_vensim( - root_dir + model_name + ".mdl", split_modules=True + root_dir + model_name + ".mdl", split_views=True ) namespace_filename = "_namespace_" + model_name + ".json" @@ -135,10 +135,20 @@ def test_read_vensim_split_model(self): self.assertIn("view2", model_split.components._modules.keys()) self.assertIsInstance(model_split.components._subscript_dict, dict) + with open(root_dir + model_name + ".py", 'r') as file: + file_content = file.read() + + # assert that the functions are not defined in the main file + self.assertNotIn("def another_var()", file_content) + self.assertNotIn("def rate1()", file_content) + self.assertNotIn("def varn()", file_content) + self.assertNotIn("def variablex()", file_content) + self.assertNotIn("def stock()", file_content) + # check that the results of the split model are the same than those # without splitting model_non_split = pysd.read_vensim( - root_dir + model_name + ".mdl", split_modules=False + root_dir + model_name + ".mdl", split_views=False ) result_split = model_split.run() @@ -148,6 +158,164 @@ def test_read_vensim_split_model(self): # model (un-split) assert_frames_close(result_split, result_non_split, atol=0, rtol=0) + with open(root_dir + model_name + ".py", 'r') as file: + file_content = file.read() + + # assert that the functions are in the main file for regular trans + self.assertIn("def another_var()", file_content) + self.assertIn("def rate1()", file_content) + self.assertIn("def varn()", file_content) + self.assertIn("def variablex()", file_content) + self.assertIn("def stock()", file_content) + + # remove newly created files + os.remove(root_dir + model_name + ".py") + os.remove(root_dir + namespace_filename) + os.remove(root_dir + subscript_dict_filename) + + # remove newly created modules folder + shutil.rmtree(root_dir + modules_dirname) + + def test_read_vensim_split_model_vensim_8_2_1(self): + import pysd + from pysd.tools.benchmarking import assert_frames_close + + root_dir = "more-tests/split_model_vensim_8_2_1/" + + model_name = "test_split_model_vensim_8_2_1" + model_split = pysd.read_vensim( + root_dir + model_name + ".mdl", split_views=True, subview_sep="." + ) + + namespace_filename = "_namespace_" + model_name + ".json" + subscript_dict_filename = "_subscripts_" + model_name + ".json" + modules_filename = "_modules.json" + modules_dirname = "modules_" + model_name + + # check that _namespace and _subscript_dict json files where created + self.assertTrue(os.path.isfile(root_dir + namespace_filename)) + self.assertTrue(os.path.isfile(root_dir + subscript_dict_filename)) + + # check that the main model file was created + self.assertTrue(os.path.isfile(root_dir + model_name + ".py")) + + # check that the modules folder was created + self.assertTrue(os.path.isdir(root_dir + modules_dirname)) + self.assertTrue( + os.path.isfile(root_dir + modules_dirname + "/" + modules_filename) + ) + + # check creation of module files + self.assertTrue( + os.path.isfile(root_dir + modules_dirname + "/" + "teacup.py")) + self.assertTrue( + os.path.isfile(root_dir + modules_dirname + "/" + "cream.py")) + + # check dictionaries + self.assertIn("Cream Temperature", + model_split.components._namespace.keys()) + self.assertIn("cream", model_split.components._modules.keys()) + self.assertIsInstance(model_split.components._subscript_dict, dict) + + with open(root_dir + model_name + ".py", 'r') as file: + file_content = file.read() + + # assert that the functions are not defined in the main file + self.assertNotIn("def teacup_temperature()", file_content) + self.assertNotIn("def cream_temperature()", file_content) + + # check that the results of the split model are the same than those + # without splitting + model_non_split = pysd.read_vensim( + root_dir + model_name + ".mdl", split_views=False + ) + + result_split = model_split.run() + result_non_split = model_non_split.run() + + # results of a split model are the same that those of the regular + # model (un-split) + assert_frames_close(result_split, result_non_split, atol=0, rtol=0) + + with open(root_dir + model_name + ".py", 'r') as file: + file_content = file.read() + + # assert that the functions are in the main file for regular trans + self.assertIn("def teacup_temperature()", file_content) + self.assertIn("def cream_temperature()", file_content) + + # remove newly created files + os.remove(root_dir + model_name + ".py") + os.remove(root_dir + namespace_filename) + os.remove(root_dir + subscript_dict_filename) + + # remove newly created modules folder + shutil.rmtree(root_dir + modules_dirname) + + def test_read_vensim_split_model_subviews(self): + import pysd + from pysd.tools.benchmarking import assert_frames_close + + root_dir = "more-tests/split_model/" + + model_name = "test_split_model_subviews" + model_split = pysd.read_vensim( + root_dir + model_name + ".mdl", split_views=True, + subview_sep="." + ) + + namespace_filename = "_namespace_" + model_name + ".json" + subscript_dict_filename = "_subscripts_" + model_name + ".json" + modules_dirname = "modules_" + model_name + + # check that the modules folders were created + self.assertTrue(os.path.isdir(root_dir + modules_dirname + "/VIEW_1")) + self.assertTrue(os.path.isdir(root_dir + modules_dirname + "/VIEW_2")) + + # check creation of module files + self.assertTrue( + os.path.isfile(root_dir + modules_dirname + "/VIEW_1/" + + "submodule_1.py")) + self.assertTrue( + os.path.isfile(root_dir + modules_dirname + "/VIEW_1/" + + "submodule_2.py")) + self.assertTrue( + os.path.isfile(root_dir + modules_dirname + "/VIEW_2/" + + "view_2.py")) + + with open(root_dir + model_name + ".py", 'r') as file: + file_content = file.read() + + # assert that the functions are not defined in the main file + self.assertNotIn("def another_var()", file_content) + self.assertNotIn("def rate1()", file_content) + self.assertNotIn("def varn()", file_content) + self.assertNotIn("def variablex()", file_content) + self.assertNotIn("def stock()", file_content) + + # check that the results of the split model are the same than those + # without splitting + model_non_split = pysd.read_vensim( + root_dir + model_name + ".mdl", split_views=False + ) + + result_split = model_split.run() + result_non_split = model_non_split.run() + + # results of a split model are the same that those of the regular + # model (un-split) + assert_frames_close(result_split, result_non_split, atol=0, rtol=0) + + with open(root_dir + model_name + ".py", 'r') as file: + file_content = file.read() + + # assert that the functions are in the main file for regular trans + self.assertIn("def another_var()", file_content) + self.assertIn("def rate1()", file_content) + self.assertIn("def varn()", file_content) + self.assertIn("def variablex()", file_content) + self.assertIn("def stock()", file_content) + # remove newly created files os.remove(root_dir + model_name + ".py") os.remove(root_dir + namespace_filename) @@ -164,7 +332,7 @@ def test_read_vensim_split_model_with_macro(self): model_name = "test_split_model_with_macro" model_split = pysd.read_vensim( - root_dir + model_name + ".mdl", split_modules=True + root_dir + model_name + ".mdl", split_views=True ) namespace_filename = "_namespace_" + model_name + ".json" @@ -174,7 +342,7 @@ def test_read_vensim_split_model_with_macro(self): # check that the results of the split model are the same # than those without splitting model_non_split = pysd.read_vensim( - root_dir + model_name + ".mdl", split_modules=False + root_dir + model_name + ".mdl", split_views=False ) result_split = model_split.run() @@ -194,18 +362,19 @@ def test_read_vensim_split_model_with_macro(self): def test_read_vensim_split_model_warning(self): import pysd - # setting the split_modules=True when the model has a single + # setting the split_views=True when the model has a single # view should generate a warning with catch_warnings(record=True) as ws: pysd.read_vensim( - test_model, split_modules=True + test_model, split_views=True ) # set stock value using params wu = [w for w in ws if issubclass(w.category, UserWarning)] self.assertEqual(len(wu), 1) self.assertTrue( - "Only one module was detected" in str(wu[0].message) + "Only a single view with no subviews was detected" in str( + wu[0].message) ) # check that warning references the stock def test_run_includes_last_value(self): diff --git a/tests/unit_test_vensim2py.py b/tests/unit_test_vensim2py.py index ecb8a368..a969ffe6 100644 --- a/tests/unit_test_vensim2py.py +++ b/tests/unit_test_vensim2py.py @@ -557,9 +557,7 @@ def test_subscript_float_initialization(self): # hoewever eval is not detecting _subscript_dict variable self.assertEqual( string, - "xr.DataArray(3.32,{dim: " - + "_subscript_dict[dim] for dim in " - + "['Dim1']},['Dim1'])", + "xr.DataArray(3.32,{'Dim1': _subscript_dict['Dim1']},['Dim1'])", ) a = xr.DataArray( 3.32, {dim: _subscript_dict[dim] for dim in ["Dim1"]}, ["Dim1"] @@ -580,11 +578,10 @@ def test_subscript_1d_constant(self): string = element[0]["py_expr"] # TODO we should use a = eval(string) # hoewever eval is not detecting _subscript_dict variable - self.assertTrue( + self.assertEqual( string, - "xr.DataArray([1.,2.,3.]," - + "{dim: _subscript_dict[dim]" - + " for dim in ['Dim1']}, ['Dim1'])", + "xr.DataArray([1.,2.,3.],{'Dim1': _subscript_dict['Dim1']}," + "['Dim1'])", ) a = xr.DataArray([1.0, 2.0, 3.0], {dim: _subscript_dict[dim] for dim in ["Dim1"]}, @@ -787,7 +784,7 @@ def test_parse_sketch_line(self): for num, line in enumerate(lines): res = parse_sketch_line(line.strip(), namespace) self.assertEqual(res["variable_name"], expected_var[num]) - self.assertEqual(res["module_name"], expected_mod[num]) + self.assertEqual(res["view_name"], expected_mod[num]) class TestParse_private_functions(unittest.TestCase): From fa3ab656ae5831f4a98cdd8cd27fbdf102b6af9a Mon Sep 17 00:00:00 2001 From: RogerSC Date: Mon, 9 Aug 2021 21:41:11 +0200 Subject: [PATCH 3/3] fix paths in tests (#290) --- tests/unit_test_cli.py | 2 +- tests/unit_test_pysd.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit_test_cli.py b/tests/unit_test_cli.py index f743dc96..be0400a7 100644 --- a/tests/unit_test_cli.py +++ b/tests/unit_test_cli.py @@ -252,7 +252,7 @@ def test_read_vensim_split_model_subviews(self): import pysd from pysd.tools.benchmarking import assert_frames_close - root_dir = "more-tests/split_model/" + root_dir = os.path.join(_root, "more-tests/split_model/") model_name = "test_split_model_subviews" model_name_mdl = root_dir + model_name + ".mdl" diff --git a/tests/unit_test_pysd.py b/tests/unit_test_pysd.py index ff00b56b..c602bb88 100644 --- a/tests/unit_test_pysd.py +++ b/tests/unit_test_pysd.py @@ -180,7 +180,7 @@ def test_read_vensim_split_model_vensim_8_2_1(self): import pysd from pysd.tools.benchmarking import assert_frames_close - root_dir = "more-tests/split_model_vensim_8_2_1/" + root_dir = os.path.join(_root, "more-tests/split_model_vensim_8_2_1/") model_name = "test_split_model_vensim_8_2_1" model_split = pysd.read_vensim( @@ -256,7 +256,7 @@ def test_read_vensim_split_model_subviews(self): import pysd from pysd.tools.benchmarking import assert_frames_close - root_dir = "more-tests/split_model/" + root_dir = os.path.join(_root, "more-tests/split_model/") model_name = "test_split_model_subviews" model_split = pysd.read_vensim(