diff --git a/webapp/apps/taxbrain/helpers.py b/webapp/apps/taxbrain/helpers.py index 8992af47..aa31de4f 100644 --- a/webapp/apps/taxbrain/helpers.py +++ b/webapp/apps/taxbrain/helpers.py @@ -280,7 +280,11 @@ def get_default_policy_param_name(param, default_params): ix = int(end_piece) except ValueError: msg = "Parsing {}: Expected integer for index but got {}" - raise ValueError(msg.format(param, ix)) + raise ValueError(msg.format(param, end_piece)) + num_columns = len(default_params[no_suffix]['col_label']) + if ix < 0 or ix >= num_columns: + msg = "Parsing {}: Index {} not in range ({}, {})" + raise IndexError(msg.format(param, ix, 0, num_columns)) col_label = default_params[no_suffix]['col_label'][ix] return no_suffix + '_' + col_label msg = "Received unexpected parameter: {}" diff --git a/webapp/apps/test_assets/test_assumptions.py b/webapp/apps/test_assets/test_assumptions.py index 4265ced3..434ae2ce 100644 --- a/webapp/apps/test_assets/test_assumptions.py +++ b/webapp/apps/test_assets/test_assumptions.py @@ -8,3 +8,20 @@ "consumption": {}, "growdiff_baseline": {} }""" + +exp_assumptions_text = { + 'growdiff_response': {}, + 'consumption': {}, + 'behavior': { + 2017: {u'_BE_sub': [1.0], u'_BE_inc': [-0.6], u'_BE_cg': [-0.67]}}, + 'growdiff_baseline': {} +} + +no_assumptions_text = """ +{ + "growdiff_response": {}, + "consumption": {}, + "behavior": {}, + "growdiff_baseline": {} +} +""" diff --git a/webapp/apps/test_assets/test_param_formatters.py b/webapp/apps/test_assets/test_param_formatters.py new file mode 100644 index 00000000..29914719 --- /dev/null +++ b/webapp/apps/test_assets/test_param_formatters.py @@ -0,0 +1,127 @@ +import json +import pytest +import taxcalc +import numpy as np + +from ..taxbrain.views import read_json_reform, parse_errors_warnings +from ..taxbrain.helpers import (get_default_policy_param_name, to_json_reform) + +from test_reform import (test_coverage_fields, test_coverage_reform, + test_coverage_json_reform, + test_coverage_exp_read_json_reform, + errors_warnings_fields, errors_warnings_reform, + errors_warnings_json_reform, + errors_warnings_exp_read_json_reform, + errors_warnings, exp_errors_warnings, + map_back_to_tb) + +from test_assumptions import (assumptions_text, exp_assumptions_text, no_assumptions_text) + +START_YEAR = 2017 + +@pytest.fixture +def default_params_Policy(): + return taxcalc.Policy.default_data(start_year=START_YEAR, + metadata=True) + + +############################################################################### +# Test get_default_policy_param_name +# 3 Cases for search success: +# 1. "_" + param name +# 2. "_" + param name + index name (i.e. STD_0 --> _STD_single) +# 3. "_" + param name minus "_cpi" +@pytest.mark.parametrize("param,exp_param", + [("FICA_ss_trt", "_FICA_ss_trt"), + ("ID_BenefitSurtax_Switch_0", "_ID_BenefitSurtax_Switch_medical"), + ("CG_brk3_cpi", "_CG_brk3_cpi")]) +def test_get_default_policy_param_name_passing(param, exp_param, default_params_Policy): + act_param = get_default_policy_param_name(param, default_params_Policy) + np.testing.assert_equal(act_param, exp_param) + +@pytest.mark.parametrize("param", ["CG_brk3_extra_cpi", "not_a_param"]) +def test_get_default_policy_param_name_failing0(param, default_params_Policy): + """ + Check that non-recognized parameters throw a ValueError + """ + match="Received unexpected parameter: {0}".format(param) + with pytest.raises(ValueError, match=match): + get_default_policy_param_name(param, default_params_Policy) + + +def test_get_default_policy_param_name_failing1(default_params_Policy): + """ + Check that parameter with non-integer characters after the final '_' + throws ValueError + """ + param = "ID_BenefitSurtax_Switch_idx" + match = "Parsing {}: Expected integer for index but got {}".format(param, "idx") + with pytest.raises(ValueError, match=match): + get_default_policy_param_name(param, default_params_Policy) + + +def test_get_default_policy_param_name_failing2(default_params_Policy): + """ + Check that parameter with correct name but out of bounds index throws + IndexError + """ + param = "ID_BenefitSurtax_Switch_12" + # comment out "(" since this is treated as a regexp string + match = "Parsing {}: Index {} not in range \({}, {}\)" + match = match.format(param, 12, 0, 7) + with pytest.raises(IndexError, match=match): + get_default_policy_param_name(param, default_params_Policy) + +############################################################################### +# Test to_json_reform +# 2 Cases: +# 1. Fields do not cause errors +# 2. Fields cause errors +@pytest.mark.parametrize( + ("fields,exp_reform"), + [(test_coverage_fields, test_coverage_reform), + (errors_warnings_fields, errors_warnings_reform)] +) +def test_to_json_reform(fields, exp_reform): + act, _ = to_json_reform(fields, START_YEAR) + np.testing.assert_equal(act, exp_reform) + +############################################################################### +# Test parse_errors_warnings +def test_parse_errors_warnings(): + act = parse_errors_warnings(errors_warnings, map_back_to_tb) + np.testing.assert_equal(exp_errors_warnings, act) + + +############################################################################### +# Test read_json_reform +# 3 Cases: +# 1. Reform does not throw errors and no behavior assumptions are made +# 2. Reform does not throw errors and behavior assumptions are made +# 3. Reform throws errors and warnings and behavior assumptions are not made +@pytest.mark.parametrize( + ("test_reform,test_assump,map_back_to_tb,exp_reform,exp_assump," + "exp_errors_warnings"), + [(test_coverage_json_reform, no_assumptions_text, + map_back_to_tb, test_coverage_exp_read_json_reform, + json.loads(no_assumptions_text), + {'errors': {}, 'warnings': {}}), + (test_coverage_json_reform, assumptions_text, + map_back_to_tb, test_coverage_exp_read_json_reform, + exp_assumptions_text, + {'errors': {}, 'warnings': {}}), + (errors_warnings_json_reform, no_assumptions_text, + map_back_to_tb, errors_warnings_exp_read_json_reform, + json.loads(no_assumptions_text), exp_errors_warnings) + ] +) +def test_read_json_reform(test_reform, test_assump, map_back_to_tb, + exp_reform, exp_assump, exp_errors_warnings): + act_reform, act_assump, act_errors_warnings = read_json_reform( + test_reform, + test_assump, + map_back_to_tb + ) + np.testing.assert_equal(exp_reform, act_reform) + np.testing.assert_equal(exp_assump, act_assump) + np.testing.assert_equal(exp_errors_warnings, act_errors_warnings) diff --git a/webapp/apps/test_assets/test_reform.py b/webapp/apps/test_assets/test_reform.py index eff1e0ab..69804653 100644 --- a/webapp/apps/test_assets/test_reform.py +++ b/webapp/apps/test_assets/test_reform.py @@ -157,3 +157,215 @@ } } """ + +""" + ******************************************************************** + The following objects were created for test_param_formatters.py. + + fields_base -- typical metadata associated with TaxBrain run + + test_coverage_* objects -- these objects do not throw TC warnings or errors + but include a representative for each type of TC parameter in + current_law_policy.json + + errors_warnings_* -- these objects do throw warnings and errors and include + a mostly representative set of TC parameters + + map_back_to_tb -- required for mapping Tax-Calculator styled parameter + names to TaxBrain styled names + ******************************************************************** +""" + +fields_base = { + '_state': "", + 'creation_date': "datetime.datetime(2015, 1, 1, 0, 0)", + 'id': 64, + 'quick_calc': False, + 'first_year': 2017, +} + +test_coverage_fields = dict( + CG_nodiff = [False], + FICA_ss_trt = [u'*', 0.1, u'*', 0.2], + STD_0 = [8000.0, '*', 10000.0], + ID_BenefitSurtax_Switch_0 = [True], + ID_Charity_c_cpi = True, + EITC_rt_2 = [1.0], + **fields_base +) + +test_coverage_reform = { + '_CG_nodiff': {'2017': [False]}, + '_FICA_ss_trt': {'2020': [0.2], '2018': [0.1]}, + '_STD_single': {'2017': [8000.0], '2019': [10000.0]}, + '_ID_Charity_c_cpi': {'2017': True}, + '_ID_BenefitSurtax_Switch_medical': {'2017': [True]}, + '_EITC_rt_2kids': {'2017': [1.0]} +} + +errors_warnings_fields = dict( + STD_0 = [7000.0], + FICA_ss_trt = [-1.0,'*',0.1], + II_brk4_0 = [500.0], + STD_3= [10000.0, '*', '*', 150.0], + ID_BenefitSurtax_Switch_0= [True], + **fields_base +) + +errors_warnings_reform = { + u'_STD_single': {u'2017': [7000.0]}, + u'_FICA_ss_trt': {u'2017': [-1.0], u'2019': [0.1]}, + u'_II_brk4_single': {u'2017': [500.0]}, + u'_STD_headhousehold': {u'2017': [10000.0], u'2020': [150.0]}, + u'_ID_BenefitSurtax_Switch_medical': {u'2017': [True]} +} + + +map_back_to_tb = { + u'_ID_BenefitSurtax_Switch_charity': 'ID_BenefitSurtax_Switch_6', + '_ALD_InvInc_ec_base_RyanBrady': 'ALD_InvInc_ec_base_RyanBrady', + u'_ID_BenefitSurtax_Switch_interest': 'ID_BenefitSurtax_Switch_5', + '_EITC_indiv': 'EITC_indiv', + u'_ID_BenefitSurtax_Switch_misc': 'ID_BenefitSurtax_Switch_4', + u'_ID_BenefitCap_Switch_charity': 'ID_BenefitCap_Switch_6', + u'_STD_single': 'STD_0', + '_II_no_em_nu18': 'II_no_em_nu18', + u'_ID_BenefitSurtax_Switch_realestate': 'ID_BenefitSurtax_Switch_2', + u'_ID_BenefitCap_Switch_misc': 'ID_BenefitCap_Switch_4', + '_CG_nodiff': 'CG_nodiff', + u'_ID_BenefitSurtax_Switch_statelocal': 'ID_BenefitSurtax_Switch_1', + u'_ID_BenefitCap_Switch_medical': 'ID_BenefitCap_Switch_0', + '_FICA_ss_trt': 'FICA_ss_trt', + u'_ID_BenefitCap_Switch_casualty': 'ID_BenefitCap_Switch_3', + '_ID_Charity_c_cpi': 'ID_Charity_c_cpi', + u'_ID_BenefitCap_Switch_statelocal': 'ID_BenefitCap_Switch_1', + u'_EITC_rt_2kids': 'EITC_rt_2', + u'_ID_BenefitSurtax_Switch_casualty': 'ID_BenefitSurtax_Switch_3', + '_NIIT_PT_taxed': 'NIIT_PT_taxed', + u'_ID_BenefitSurtax_Switch_medical': 'ID_BenefitSurtax_Switch_0', + u'_ID_BenefitCap_Switch_interest': 'ID_BenefitCap_Switch_5', + '_CTC_new_refund_limited': 'CTC_new_refund_limited', + u'_ID_BenefitCap_Switch_realestate': 'ID_BenefitCap_Switch_2', + u'_STD_single': 'STD_0', + u'_STD_headhousehold': 'STD_3', + u'_II_brk4_single': 'II_brk4_0' +} + +test_coverage_json_reform = """ +{ + "policy": { + "_ID_BenefitSurtax_Switch_charity": {"2017": [0.0]}, + "_ALD_InvInc_ec_base_RyanBrady": {"2017": [false]}, + "_ID_BenefitSurtax_Switch_interest": {"2017": [0.0]}, + "_EITC_indiv": {"2017": [false]}, + "_ID_BenefitSurtax_Switch_misc": {"2017": [0.0]}, + "_ID_BenefitCap_Switch_charity": {"2017": [0.0]}, + "_STD_single": {"2017": [10000.0]}, + "_II_no_em_nu18": {"2017": [false]}, + "_ID_BenefitSurtax_Switch_realestate": {"2017": [0.0]}, + "_ID_BenefitCap_Switch_misc": {"2017": [0.0]}, + "_CG_nodiff": {"2017": [false]}, + "_ID_BenefitSurtax_Switch_statelocal": {"2017": [0.0]}, + "_ID_BenefitCap_Switch_medical": {"2017": [0.0]}, + "_FICA_ss_trt": {"2020": [0.2], "2018": [0.1]}, + "_ID_BenefitCap_Switch_casualty": {"2017": [0.0]}, + "_ID_Charity_c_cpi": {"2017": true}, + "_ID_BenefitCap_Switch_statelocal": {"2017": [0.0]}, + "_EITC_rt_2kids": {"2017": [1.0]}, + "_ID_BenefitSurtax_Switch_casualty": {"2017": [0.0]}, + "_NIIT_PT_taxed": {"2017": [false]}, + "_ID_BenefitSurtax_Switch_medical": {"2017": [1.0]}, + "_ID_BenefitCap_Switch_interest": {"2017": [0.0]}, + "_CTC_new_refund_limited": {"2017": [false]}, + "_ID_BenefitCap_Switch_realestate": {"2017": [0.0]} + } +} +""" + +errors_warnings_json_reform = """ +{ + "policy": { + "_ID_BenefitSurtax_Switch_medical": {"2017": [true]}, + "_STD_headhousehold": {"2017": [10000.0], "2020": [150.0]}, + "_FICA_ss_trt": {"2017": [-1.0], "2019": [0.1]}, + "_STD_single": {"2017": [7000.0]}, + "_II_brk4_single": {"2017": [500.0]} + } +} +""" + +test_coverage_exp_read_json_reform = { + 2017: {u'_EITC_rt': [[0.0765, 0.34, 1.0, 0.45]], + u'_NIIT_PT_taxed': [False], + u'_ID_BenefitCap_Switch': [[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]], + u'_ALD_InvInc_ec_base_RyanBrady': [False], + u'_EITC_indiv': [False], + u'_ID_BenefitSurtax_Switch': [[1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]], + u'_STD': [[10000.0, 12700.0, 6350.0, 9350.0, 12700.0]], + u'_II_no_em_nu18': [False], + u'_ID_Charity_c_cpi': True, + u'_CG_nodiff': [False], + u'_CTC_new_refund_limited': [False]}, + 2018: {u'_FICA_ss_trt': [0.1]}, + 2020: {u'_FICA_ss_trt': [0.2]} +} + + +errors_warnings_exp_read_json_reform = { + 2017: + {u'_II_brk4': [[500.0, 233350.0, 116675.0, 212500.0, 233350.0]], + u'_STD': [[7000.0, 12700.0, 6350.0, 10000.0, 12700.0]], + u'_ID_BenefitSurtax_Switch': [[True, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]], + u'_FICA_ss_trt': [-1.0]}, + 2019: {u'_FICA_ss_trt': [0.1]}, + 2020: {u'_STD': [[7489.8, 13588.64, 6794.31, 150.0, 13588.64]]} +} + +errors = ("ERROR: 2017 _FICA_ss_trt value -1.0 < min value 0\n" + "ERROR: 2018 _FICA_ss_trt value -1.0 < min value 0\n" + "ERROR: 2017 _II_brk4_0 value 500.0 < min value 91900.0 for _II_brk3_0\n" + "ERROR: 2018 _II_brk4_0 value 511.1 < min value 93940.18 for _II_brk3_0\n" + "ERROR: 2019 _II_brk4_0 value 522.8 < min value 96091.41 for _II_brk3_0\n" + "ERROR: 2020 _II_brk4_0 value 534.98 < min value 98330.34 for _II_brk3_0\n" + "ERROR: 2021 _II_brk4_0 value 547.45 < min value 100621.44 for _II_brk3_0\n" + "ERROR: 2022 _II_brk4_0 value 560.15 < min value 102955.86 for _II_brk3_0\n" + "ERROR: 2023 _II_brk4_0 value 573.2 < min value 105354.73 for _II_brk3_0\n" + "ERROR: 2024 _II_brk4_0 value 586.56 < min value 107809.5 for _II_brk3_0\n" + "ERROR: 2025 _II_brk4_0 value 600.29 < min value 110332.24 for _II_brk3_0\n" + "ERROR: 2026 _II_brk4_0 value 614.4 < min value 112925.05 for _II_brk3_0\n") + +warnings = ("WARNING: 2020 _STD_3 value 150.0 < min value 10004.23\n" + "WARNING: 2021 _STD_3 value 153.5 < min value 10237.33\n" + "WARNING: 2022 _STD_3 value 157.06 < min value 10474.84\n" + "WARNING: 2023 _STD_3 value 160.72 < min value 10718.9\n" + "WARNING: 2024 _STD_3 value 164.46 < min value 10968.65\n" + "WARNING: 2025 _STD_3 value 168.31 < min value 11225.32\n" + "WARNING: 2026 _STD_3 value 172.27 < min value 11489.12\n") + +errors_warnings = {'errors': errors, 'warnings': warnings} + +exp_errors_warnings = { + 'errors': { + '2024': {'II_brk4_0': 'ERROR: value 586.56 < min value 107809.5 for _II_brk3_0 for 2024'}, + '2025': {'II_brk4_0': 'ERROR: value 600.29 < min value 110332.24 for _II_brk3_0 for 2025'}, + '2026': {'II_brk4_0': 'ERROR: value 614.4 < min value 112925.05 for _II_brk3_0 for 2026'}, + '2020': {'II_brk4_0': 'ERROR: value 534.98 < min value 98330.34 for _II_brk3_0 for 2020'}, + '2018': {'FICA_ss_trt': 'ERROR: value -1.0 < min value 0 for 2018', + 'II_brk4_0': 'ERROR: value 511.1 < min value 93940.18 for _II_brk3_0 for 2018'}, + '2022': {'II_brk4_0': 'ERROR: value 560.15 < min value 102955.86 for _II_brk3_0 for 2022'}, + '2023': {'II_brk4_0': 'ERROR: value 573.2 < min value 105354.73 for _II_brk3_0 for 2023'}, + '2019': {'II_brk4_0': 'ERROR: value 522.8 < min value 96091.41 for _II_brk3_0 for 2019'}, + '2017': {'FICA_ss_trt': 'ERROR: value -1.0 < min value 0 for 2017', + 'II_brk4_0': 'ERROR: value 500.0 < min value 91900.0 for _II_brk3_0 for 2017'}, + '2021': {'II_brk4_0': 'ERROR: value 547.45 < min value 100621.44 for _II_brk3_0 for 2021'} + }, + 'warnings': { + '2024': {'STD_3': 'WARNING: value 164.46 < min value 10968.65 for 2024'}, + '2025': {'STD_3': 'WARNING: value 168.31 < min value 11225.32 for 2025'}, + '2026': {'STD_3': 'WARNING: value 172.27 < min value 11489.12 for 2026'}, + '2020': {'STD_3': 'WARNING: value 150.0 < min value 10004.23 for 2020'}, + '2021': {'STD_3': 'WARNING: value 153.5 < min value 10237.33 for 2021'}, + '2022': {'STD_3': 'WARNING: value 157.06 < min value 10474.84 for 2022'}, + '2023': {'STD_3': 'WARNING: value 160.72 < min value 10718.9 for 2023'} + } +}