OpenFisca-Core-test 46.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openfisca_core/__init__.py +0 -0
- openfisca_core/commons/__init__.py +18 -0
- openfisca_core/commons/dummy.py +25 -0
- openfisca_core/commons/formulas.py +121 -0
- openfisca_core/commons/misc.py +109 -0
- openfisca_core/commons/py.typed +0 -0
- openfisca_core/commons/rates.py +114 -0
- openfisca_core/commons/tests/__init__.py +0 -0
- openfisca_core/commons/tests/test_dummy.py +9 -0
- openfisca_core/commons/tests/test_formulas.py +75 -0
- openfisca_core/commons/tests/test_rates.py +26 -0
- openfisca_core/data_storage/__init__.py +7 -0
- openfisca_core/data_storage/in_memory_storage.py +198 -0
- openfisca_core/data_storage/on_disk_storage.py +307 -0
- openfisca_core/data_storage/types.py +14 -0
- openfisca_core/entities/__init__.py +23 -0
- openfisca_core/entities/_core_entity.py +217 -0
- openfisca_core/entities/_description.py +55 -0
- openfisca_core/entities/entity.py +60 -0
- openfisca_core/entities/group_entity.py +123 -0
- openfisca_core/entities/helpers.py +165 -0
- openfisca_core/entities/py.typed +0 -0
- openfisca_core/entities/role.py +92 -0
- openfisca_core/entities/tests/__init__.py +0 -0
- openfisca_core/entities/tests/test_entity.py +10 -0
- openfisca_core/entities/tests/test_group_entity.py +70 -0
- openfisca_core/entities/tests/test_role.py +11 -0
- openfisca_core/entities/types.py +44 -0
- openfisca_core/errors/__init__.py +64 -0
- openfisca_core/errors/cycle_error.py +2 -0
- openfisca_core/errors/empty_argument_error.py +39 -0
- openfisca_core/errors/nan_creation_error.py +2 -0
- openfisca_core/errors/parameter_not_found_error.py +16 -0
- openfisca_core/errors/parameter_parsing_error.py +18 -0
- openfisca_core/errors/period_mismatch_error.py +9 -0
- openfisca_core/errors/situation_parsing_error.py +26 -0
- openfisca_core/errors/spiral_error.py +2 -0
- openfisca_core/errors/variable_name_config_error.py +2 -0
- openfisca_core/errors/variable_not_found_error.py +29 -0
- openfisca_core/experimental/__init__.py +9 -0
- openfisca_core/experimental/_errors.py +5 -0
- openfisca_core/experimental/_memory_config.py +41 -0
- openfisca_core/formula_helpers.py +9 -0
- openfisca_core/holders/__init__.py +33 -0
- openfisca_core/holders/helpers.py +93 -0
- openfisca_core/holders/holder.py +328 -0
- openfisca_core/holders/tests/__init__.py +0 -0
- openfisca_core/holders/tests/test_helpers.py +134 -0
- openfisca_core/holders/types.py +3 -0
- openfisca_core/indexed_enums/__init__.py +18 -0
- openfisca_core/indexed_enums/_enum_type.py +79 -0
- openfisca_core/indexed_enums/_errors.py +35 -0
- openfisca_core/indexed_enums/_guards.py +209 -0
- openfisca_core/indexed_enums/_utils.py +180 -0
- openfisca_core/indexed_enums/config.py +6 -0
- openfisca_core/indexed_enums/enum.py +218 -0
- openfisca_core/indexed_enums/enum_array.py +316 -0
- openfisca_core/indexed_enums/py.typed +0 -0
- openfisca_core/indexed_enums/tests/__init__.py +0 -0
- openfisca_core/indexed_enums/tests/test_enum.py +135 -0
- openfisca_core/indexed_enums/tests/test_enum_array.py +30 -0
- openfisca_core/indexed_enums/types.py +49 -0
- openfisca_core/memory_config.py +9 -0
- openfisca_core/model_api.py +71 -0
- openfisca_core/parameters/__init__.py +71 -0
- openfisca_core/parameters/at_instant_like.py +17 -0
- openfisca_core/parameters/config.py +55 -0
- openfisca_core/parameters/helpers.py +106 -0
- openfisca_core/parameters/parameter.py +217 -0
- openfisca_core/parameters/parameter_at_instant.py +75 -0
- openfisca_core/parameters/parameter_node.py +176 -0
- openfisca_core/parameters/parameter_node_at_instant.py +57 -0
- openfisca_core/parameters/parameter_scale.py +121 -0
- openfisca_core/parameters/parameter_scale_bracket.py +7 -0
- openfisca_core/parameters/values_history.py +5 -0
- openfisca_core/parameters/vectorial_parameter_node_at_instant.py +179 -0
- openfisca_core/periods/__init__.py +77 -0
- openfisca_core/periods/_errors.py +28 -0
- openfisca_core/periods/_parsers.py +122 -0
- openfisca_core/periods/config.py +20 -0
- openfisca_core/periods/date_unit.py +110 -0
- openfisca_core/periods/helpers.py +314 -0
- openfisca_core/periods/instant_.py +226 -0
- openfisca_core/periods/period_.py +919 -0
- openfisca_core/periods/py.typed +0 -0
- openfisca_core/periods/tests/__init__.py +0 -0
- openfisca_core/periods/tests/helpers/__init__.py +0 -0
- openfisca_core/periods/tests/helpers/test_helpers.py +65 -0
- openfisca_core/periods/tests/helpers/test_instant.py +73 -0
- openfisca_core/periods/tests/helpers/test_period.py +134 -0
- openfisca_core/periods/tests/test_instant.py +32 -0
- openfisca_core/periods/tests/test_parsers.py +129 -0
- openfisca_core/periods/tests/test_period.py +283 -0
- openfisca_core/populations/__init__.py +64 -0
- openfisca_core/populations/_core_population.py +454 -0
- openfisca_core/populations/_errors.py +65 -0
- openfisca_core/populations/group_population.py +326 -0
- openfisca_core/populations/population.py +142 -0
- openfisca_core/populations/tests/__init__.py +0 -0
- openfisca_core/populations/tests/test_members_position.py +116 -0
- openfisca_core/populations/types.py +110 -0
- openfisca_core/projectors/__init__.py +39 -0
- openfisca_core/projectors/entity_to_person_projector.py +12 -0
- openfisca_core/projectors/first_person_to_entity_projector.py +13 -0
- openfisca_core/projectors/helpers.py +139 -0
- openfisca_core/projectors/projector.py +38 -0
- openfisca_core/projectors/typing.py +27 -0
- openfisca_core/projectors/unique_role_to_entity_projector.py +14 -0
- openfisca_core/rates.py +9 -0
- openfisca_core/reforms/__init__.py +24 -0
- openfisca_core/reforms/in_yaml_test_reform.py +28 -0
- openfisca_core/reforms/reform.py +86 -0
- openfisca_core/scripts/__init__.py +106 -0
- openfisca_core/scripts/__pycache__/__init__.cpython-310-pytest-8.3.3.pyc +0 -0
- openfisca_core/scripts/__pycache__/__init__.cpython-310-pytest-8.4.2.pyc +0 -0
- openfisca_core/scripts/__pycache__/__init__.cpython-310.pyc +0 -0
- openfisca_core/scripts/__pycache__/__init__.cpython-311-pytest-8.3.3.pyc +0 -0
- openfisca_core/scripts/__pycache__/__init__.cpython-311-pytest-8.4.2.pyc +0 -0
- openfisca_core/scripts/__pycache__/__init__.cpython-311.pyc +0 -0
- openfisca_core/scripts/__pycache__/__init__.cpython-312-pytest-8.4.2.pyc +0 -0
- openfisca_core/scripts/__pycache__/__init__.cpython-312.pyc +0 -0
- openfisca_core/scripts/__pycache__/__init__.cpython-313-pytest-8.3.3.pyc +0 -0
- openfisca_core/scripts/__pycache__/__init__.cpython-313-pytest-8.4.2.pyc +0 -0
- openfisca_core/scripts/__pycache__/__init__.cpython-313.pyc +0 -0
- openfisca_core/scripts/__pycache__/find_placeholders.cpython-311.pyc +0 -0
- openfisca_core/scripts/__pycache__/find_placeholders.cpython-313.pyc +0 -0
- openfisca_core/scripts/__pycache__/measure_numpy_condition_notations.cpython-311.pyc +0 -0
- openfisca_core/scripts/__pycache__/measure_numpy_condition_notations.cpython-313.pyc +0 -0
- openfisca_core/scripts/__pycache__/measure_performances.cpython-311.pyc +0 -0
- openfisca_core/scripts/__pycache__/measure_performances.cpython-313.pyc +0 -0
- openfisca_core/scripts/__pycache__/measure_performances_fancy_indexing.cpython-311.pyc +0 -0
- openfisca_core/scripts/__pycache__/measure_performances_fancy_indexing.cpython-313.pyc +0 -0
- openfisca_core/scripts/__pycache__/openfisca_command.cpython-310.pyc +0 -0
- openfisca_core/scripts/__pycache__/openfisca_command.cpython-311.pyc +0 -0
- openfisca_core/scripts/__pycache__/openfisca_command.cpython-312.pyc +0 -0
- openfisca_core/scripts/__pycache__/openfisca_command.cpython-313.pyc +0 -0
- openfisca_core/scripts/__pycache__/remove_fuzzy.cpython-311.pyc +0 -0
- openfisca_core/scripts/__pycache__/remove_fuzzy.cpython-313.pyc +0 -0
- openfisca_core/scripts/__pycache__/run_test.cpython-310.pyc +0 -0
- openfisca_core/scripts/__pycache__/run_test.cpython-311.pyc +0 -0
- openfisca_core/scripts/__pycache__/run_test.cpython-312.pyc +0 -0
- openfisca_core/scripts/__pycache__/run_test.cpython-313.pyc +0 -0
- openfisca_core/scripts/__pycache__/simulation_generator.cpython-311.pyc +0 -0
- openfisca_core/scripts/__pycache__/simulation_generator.cpython-313.pyc +0 -0
- openfisca_core/scripts/assets/__init__.py +0 -0
- openfisca_core/scripts/assets/__pycache__/__init__.cpython-311.pyc +0 -0
- openfisca_core/scripts/assets/__pycache__/__init__.cpython-313.pyc +0 -0
- openfisca_core/scripts/assets/index.html +140 -0
- openfisca_core/scripts/find_placeholders.py +51 -0
- openfisca_core/scripts/measure_numpy_condition_notations.py +137 -0
- openfisca_core/scripts/measure_performances.py +291 -0
- openfisca_core/scripts/measure_performances_fancy_indexing.py +89 -0
- openfisca_core/scripts/migrations/__init__.py +0 -0
- openfisca_core/scripts/migrations/__pycache__/__init__.cpython-311.pyc +0 -0
- openfisca_core/scripts/migrations/__pycache__/__init__.cpython-313.pyc +0 -0
- openfisca_core/scripts/migrations/__pycache__/v24_to_25.cpython-311.pyc +0 -0
- openfisca_core/scripts/migrations/__pycache__/v24_to_25.cpython-313.pyc +0 -0
- openfisca_core/scripts/migrations/v16_2_to_v17/__init__.py +0 -0
- openfisca_core/scripts/migrations/v16_2_to_v17/__pycache__/__init__.cpython-311.pyc +0 -0
- openfisca_core/scripts/migrations/v16_2_to_v17/__pycache__/__init__.cpython-313.pyc +0 -0
- openfisca_core/scripts/migrations/v16_2_to_v17/__pycache__/xml_to_yaml_country_template.cpython-311.pyc +0 -0
- openfisca_core/scripts/migrations/v16_2_to_v17/__pycache__/xml_to_yaml_country_template.cpython-313.pyc +0 -0
- openfisca_core/scripts/migrations/v16_2_to_v17/__pycache__/xml_to_yaml_extension_template.cpython-311.pyc +0 -0
- openfisca_core/scripts/migrations/v16_2_to_v17/__pycache__/xml_to_yaml_extension_template.cpython-313.pyc +0 -0
- openfisca_core/scripts/migrations/v16_2_to_v17/legislation.xsd +102 -0
- openfisca_core/scripts/migrations/v16_2_to_v17/xml_to_yaml_country_template.py +30 -0
- openfisca_core/scripts/migrations/v16_2_to_v17/xml_to_yaml_extension_template.py +26 -0
- openfisca_core/scripts/migrations/v24_to_25.py +156 -0
- openfisca_core/scripts/openfisca_command.py +192 -0
- openfisca_core/scripts/remove_fuzzy.py +213 -0
- openfisca_core/scripts/run_test.py +51 -0
- openfisca_core/scripts/simulation_generator.py +90 -0
- openfisca_core/simulation_builder.py +16 -0
- openfisca_core/simulations/__init__.py +45 -0
- openfisca_core/simulations/_build_default_simulation.py +158 -0
- openfisca_core/simulations/_build_from_variables.py +230 -0
- openfisca_core/simulations/_type_guards.py +298 -0
- openfisca_core/simulations/helpers.py +111 -0
- openfisca_core/simulations/simulation.py +618 -0
- openfisca_core/simulations/simulation_builder.py +855 -0
- openfisca_core/simulations/typing.py +203 -0
- openfisca_core/taxbenefitsystems/__init__.py +26 -0
- openfisca_core/taxbenefitsystems/tax_benefit_system.py +584 -0
- openfisca_core/taxscales/__init__.py +35 -0
- openfisca_core/taxscales/abstract_rate_tax_scale.py +41 -0
- openfisca_core/taxscales/abstract_tax_scale.py +53 -0
- openfisca_core/taxscales/amount_tax_scale_like.py +55 -0
- openfisca_core/taxscales/helpers.py +44 -0
- openfisca_core/taxscales/linear_average_rate_tax_scale.py +76 -0
- openfisca_core/taxscales/marginal_amount_tax_scale.py +34 -0
- openfisca_core/taxscales/marginal_rate_tax_scale.py +286 -0
- openfisca_core/taxscales/rate_tax_scale_like.py +214 -0
- openfisca_core/taxscales/single_amount_tax_scale.py +32 -0
- openfisca_core/taxscales/tax_scale_like.py +65 -0
- openfisca_core/tools/__init__.py +94 -0
- openfisca_core/tools/parallel_plugin.py +55 -0
- openfisca_core/tools/simulation_dumper.py +137 -0
- openfisca_core/tools/test_runner.py +802 -0
- openfisca_core/tracers/__init__.py +40 -0
- openfisca_core/tracers/computation_log.py +196 -0
- openfisca_core/tracers/flat_trace.py +87 -0
- openfisca_core/tracers/full_tracer.py +172 -0
- openfisca_core/tracers/performance_log.py +133 -0
- openfisca_core/tracers/simple_tracer.py +64 -0
- openfisca_core/tracers/trace_node.py +121 -0
- openfisca_core/tracers/tracing_parameter_node_at_instant.py +81 -0
- openfisca_core/types.py +548 -0
- openfisca_core/variables/__init__.py +26 -0
- openfisca_core/variables/config.py +53 -0
- openfisca_core/variables/helpers.py +67 -0
- openfisca_core/variables/tests/__init__.py +0 -0
- openfisca_core/variables/tests/test_definition_period.py +43 -0
- openfisca_core/variables/variable.py +474 -0
- openfisca_core/warnings/__init__.py +25 -0
- openfisca_core/warnings/libyaml_warning.py +2 -0
- openfisca_core/warnings/tempfile_warning.py +2 -0
- openfisca_core_test-46.0.0.data/data/share/openfisca/openfisca-core/CHANGELOG.md +4209 -0
- openfisca_core_test-46.0.0.data/data/share/openfisca/openfisca-core/README.md +152 -0
- openfisca_core_test-46.0.0.dist-info/METADATA +194 -0
- openfisca_core_test-46.0.0.dist-info/RECORD +237 -0
- openfisca_core_test-46.0.0.dist-info/WHEEL +5 -0
- openfisca_core_test-46.0.0.dist-info/entry_points.txt +3 -0
- openfisca_core_test-46.0.0.dist-info/licenses/LICENSE +661 -0
- openfisca_core_test-46.0.0.dist-info/top_level.txt +2 -0
- openfisca_web_api/__init__.py +0 -0
- openfisca_web_api/app.py +226 -0
- openfisca_web_api/errors.py +11 -0
- openfisca_web_api/handlers.py +90 -0
- openfisca_web_api/loader/__init__.py +22 -0
- openfisca_web_api/loader/entities.py +28 -0
- openfisca_web_api/loader/parameters.py +122 -0
- openfisca_web_api/loader/spec.py +168 -0
- openfisca_web_api/loader/tax_benefit_system.py +27 -0
- openfisca_web_api/loader/variables.py +121 -0
- openfisca_web_api/openAPI.yml +434 -0
- openfisca_web_api/scripts/__init__.py +0 -0
- openfisca_web_api/scripts/serve.py +99 -0
|
@@ -0,0 +1,802 @@
|
|
|
1
|
+
# Avoid T201 warning for print statements in this test runner
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import dataclasses
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import pathlib
|
|
8
|
+
import shutil
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
import textwrap
|
|
12
|
+
import time
|
|
13
|
+
import traceback
|
|
14
|
+
import warnings
|
|
15
|
+
from collections.abc import Sequence
|
|
16
|
+
from typing import Any, Literal
|
|
17
|
+
|
|
18
|
+
from typing_extensions import TypedDict
|
|
19
|
+
|
|
20
|
+
from openfisca_core.types import TaxBenefitSystem
|
|
21
|
+
|
|
22
|
+
# Unix-specific modules for parallel testing (not available on Windows)
|
|
23
|
+
try:
|
|
24
|
+
import pty
|
|
25
|
+
import select
|
|
26
|
+
|
|
27
|
+
PARALLEL_AVAILABLE = True
|
|
28
|
+
except ImportError:
|
|
29
|
+
PARALLEL_AVAILABLE = False
|
|
30
|
+
|
|
31
|
+
import pytest
|
|
32
|
+
|
|
33
|
+
from openfisca_core.errors import SituationParsingError, VariableNotFound
|
|
34
|
+
from openfisca_core.reforms.in_yaml_test_reform import InYamlTestReform
|
|
35
|
+
from openfisca_core.simulations import SimulationBuilder
|
|
36
|
+
from openfisca_core.tools import assert_near
|
|
37
|
+
from openfisca_core.warnings import LibYAMLWarning
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Options(TypedDict, total=False):
|
|
41
|
+
aggregate: bool
|
|
42
|
+
ignore_default: bool
|
|
43
|
+
ignore_variables: Sequence[str] | None
|
|
44
|
+
max_depth: int
|
|
45
|
+
name_filter: str | None
|
|
46
|
+
only_variables: Sequence[str] | None
|
|
47
|
+
pdb: bool
|
|
48
|
+
performance_graph: bool
|
|
49
|
+
performance_tables: bool
|
|
50
|
+
verbose: bool
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclasses.dataclass(frozen=True)
|
|
54
|
+
class ErrorMargin:
|
|
55
|
+
__root__: dict[str | Literal["default"], float | None]
|
|
56
|
+
|
|
57
|
+
def __getitem__(self, key: str) -> float | None:
|
|
58
|
+
if key in self.__root__:
|
|
59
|
+
return self.__root__[key]
|
|
60
|
+
|
|
61
|
+
return self.__root__["default"]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclasses.dataclass
|
|
65
|
+
class Test:
|
|
66
|
+
absolute_error_margin: ErrorMargin
|
|
67
|
+
relative_error_margin: ErrorMargin
|
|
68
|
+
name: str = ""
|
|
69
|
+
input: dict[str, float | dict[str, float]] = dataclasses.field(default_factory=dict)
|
|
70
|
+
output: dict[str, float | dict[str, float]] | None = None
|
|
71
|
+
period: str | None = None
|
|
72
|
+
reforms: Sequence[str] = dataclasses.field(default_factory=list)
|
|
73
|
+
keywords: Sequence[str] | None = None
|
|
74
|
+
extensions: Sequence[str] = dataclasses.field(default_factory=list)
|
|
75
|
+
description: str | None = None
|
|
76
|
+
parameters: dict[str, float | dict[str, float]] | None = None
|
|
77
|
+
max_spiral_loops: int | None = None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def build_test(params: dict[str, Any]) -> Test:
|
|
81
|
+
for key in ["absolute_error_margin", "relative_error_margin"]:
|
|
82
|
+
value = params.get(key)
|
|
83
|
+
|
|
84
|
+
if value is None:
|
|
85
|
+
value = {"default": None}
|
|
86
|
+
|
|
87
|
+
elif isinstance(value, (float, int, str)):
|
|
88
|
+
value = {"default": float(value)}
|
|
89
|
+
|
|
90
|
+
params[key] = ErrorMargin(value)
|
|
91
|
+
|
|
92
|
+
return Test(**params)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def import_yaml():
|
|
96
|
+
import yaml
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
from yaml import CLoader as Loader
|
|
100
|
+
except ImportError:
|
|
101
|
+
message = [
|
|
102
|
+
"libyaml is not installed in your environment.",
|
|
103
|
+
"This can make your test suite slower to run. Once you have installed libyaml, ",
|
|
104
|
+
"run 'pip uninstall pyyaml && pip install pyyaml --no-cache-dir'",
|
|
105
|
+
"so that it is used in your Python environment.",
|
|
106
|
+
]
|
|
107
|
+
warnings.warn(" ".join(message), LibYAMLWarning, stacklevel=2)
|
|
108
|
+
from yaml import SafeLoader as Loader
|
|
109
|
+
return yaml, Loader
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
TEST_KEYWORDS = {
|
|
113
|
+
"absolute_error_margin",
|
|
114
|
+
"description",
|
|
115
|
+
"extensions",
|
|
116
|
+
"ignore_variables",
|
|
117
|
+
"input",
|
|
118
|
+
"keywords",
|
|
119
|
+
"max_spiral_loops",
|
|
120
|
+
"name",
|
|
121
|
+
"only_variables",
|
|
122
|
+
"output",
|
|
123
|
+
"period",
|
|
124
|
+
"parameters",
|
|
125
|
+
"reforms",
|
|
126
|
+
"relative_error_margin",
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
yaml, Loader = import_yaml()
|
|
130
|
+
|
|
131
|
+
_tax_benefit_system_cache: dict = {}
|
|
132
|
+
|
|
133
|
+
options: Options = Options()
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _create_worker_environment(options: Options) -> dict:
|
|
137
|
+
"""Create environment variables for worker processes.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
options: Test options to pass to workers
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Environment dictionary with OpenFisca configuration
|
|
144
|
+
"""
|
|
145
|
+
env = os.environ.copy()
|
|
146
|
+
env["PYTHONUNBUFFERED"] = "1" # Ensure output is not buffered
|
|
147
|
+
if options.get("country_package"):
|
|
148
|
+
env["OPENFISCA_COUNTRY_PACKAGE"] = options.get("country_package")
|
|
149
|
+
env["OPENFISCA_EXTENSIONS"] = json.dumps(options.get("extensions") or [])
|
|
150
|
+
env["OPENFISCA_REFORMS"] = json.dumps(options.get("reforms") or [])
|
|
151
|
+
env["OPENFISCA_OPTIONS"] = json.dumps(options)
|
|
152
|
+
return env
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _spawn_worker(
|
|
156
|
+
batch: list[str], python_bin: str, env: dict, verbose: bool
|
|
157
|
+
) -> tuple[subprocess.Popen, int]:
|
|
158
|
+
"""Spawn a single worker process with PTY.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
batch: List of test files for this worker
|
|
162
|
+
python_bin: Path to Python executable
|
|
163
|
+
env: Environment variables
|
|
164
|
+
verbose: Whether to enable verbose output
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Tuple of (subprocess.Popen, master_fd)
|
|
168
|
+
"""
|
|
169
|
+
# Create PTY (pseudo-terminal) for real-time output capture
|
|
170
|
+
# PTY ensures output is line-buffered and flushed immediately
|
|
171
|
+
master_fd, slave_fd = pty.openpty()
|
|
172
|
+
|
|
173
|
+
# Build pytest command for this worker
|
|
174
|
+
cmd = [
|
|
175
|
+
python_bin,
|
|
176
|
+
"-m",
|
|
177
|
+
"pytest",
|
|
178
|
+
"-p",
|
|
179
|
+
"openfisca_core.tools.parallel_plugin", # Load our custom plugin
|
|
180
|
+
"--maxfail=1", # Stop on first failure within this worker
|
|
181
|
+
"--disable-warnings", # Reduce noise in output
|
|
182
|
+
]
|
|
183
|
+
if verbose:
|
|
184
|
+
cmd.append("-vv")
|
|
185
|
+
cmd.extend(batch) # Add test files for this worker
|
|
186
|
+
|
|
187
|
+
# Spawn worker process with PTY output
|
|
188
|
+
p = subprocess.Popen(
|
|
189
|
+
cmd, stdout=slave_fd, stderr=subprocess.STDOUT, env=env, close_fds=True
|
|
190
|
+
)
|
|
191
|
+
os.close(slave_fd) # Close slave end in parent process
|
|
192
|
+
return p, master_fd
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _format_file_list(file_names: list[str], max_display: int = 3) -> str:
|
|
196
|
+
"""Format list of files for display.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
file_names: List of file names
|
|
200
|
+
max_display: Maximum number of files to show before truncating
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
Formatted string of file names
|
|
204
|
+
"""
|
|
205
|
+
if len(file_names) <= max_display:
|
|
206
|
+
return ", ".join(file_names)
|
|
207
|
+
return (
|
|
208
|
+
f"{', '.join(file_names[:max_display])} (+{len(file_names) - max_display} more)"
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _read_worker_output(
|
|
213
|
+
readable: list, fd_to_idx: dict, outputs: list[str], verbose: bool
|
|
214
|
+
) -> None:
|
|
215
|
+
"""Read output from workers that have data available.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
readable: List of file descriptors with data available
|
|
219
|
+
fd_to_idx: Mapping from file descriptor to worker index
|
|
220
|
+
outputs: List of accumulated outputs per worker
|
|
221
|
+
verbose: Whether to print output in real-time
|
|
222
|
+
"""
|
|
223
|
+
for fd in readable:
|
|
224
|
+
idx = fd_to_idx.get(fd)
|
|
225
|
+
if idx is not None:
|
|
226
|
+
try:
|
|
227
|
+
chunk = os.read(fd, 4096).decode("utf-8", "replace")
|
|
228
|
+
outputs[idx] += chunk
|
|
229
|
+
if verbose:
|
|
230
|
+
print(f"[Worker {idx}] {chunk}", end="", flush=True)
|
|
231
|
+
except OSError:
|
|
232
|
+
pass # FD closed or error, ignore
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _terminate_workers(procs: list, running: set) -> None:
|
|
236
|
+
"""Terminate all running workers.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
procs: List of (worker_id, subprocess.Popen) tuples
|
|
240
|
+
running: Set of worker IDs still running
|
|
241
|
+
"""
|
|
242
|
+
for idx, p in procs:
|
|
243
|
+
if idx in running:
|
|
244
|
+
try:
|
|
245
|
+
p.terminate()
|
|
246
|
+
except (ProcessLookupError, PermissionError):
|
|
247
|
+
pass # Process already dead or permission denied
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def discover_test_files(
|
|
251
|
+
paths: Sequence[str], name_filter: str | None = None
|
|
252
|
+
) -> list[str]:
|
|
253
|
+
"""Discover all test files (YAML and Python) in the given paths.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
paths: List of file or directory paths to search for test files
|
|
257
|
+
name_filter: Optional string filter to match against file names
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
Sorted list of unique absolute paths to test files
|
|
261
|
+
|
|
262
|
+
Notes:
|
|
263
|
+
- Accepts .yaml and .yml files
|
|
264
|
+
- Only accepts Python files starting with 'test_'
|
|
265
|
+
- Recursively explores directories
|
|
266
|
+
"""
|
|
267
|
+
files = []
|
|
268
|
+
yaml_exts = {".yaml", ".yml"}
|
|
269
|
+
for p in paths:
|
|
270
|
+
p = pathlib.Path(p)
|
|
271
|
+
if p.is_file():
|
|
272
|
+
if (p.suffix in yaml_exts) or (
|
|
273
|
+
p.name.startswith("test_") and p.suffix == ".py"
|
|
274
|
+
):
|
|
275
|
+
files.append(str(p.resolve()))
|
|
276
|
+
elif p.is_dir():
|
|
277
|
+
# collect yaml files
|
|
278
|
+
for ext in yaml_exts:
|
|
279
|
+
for f in p.rglob(f"*{ext}"):
|
|
280
|
+
files.append(str(f.resolve()))
|
|
281
|
+
# collect only python test files named test_*.py
|
|
282
|
+
for f in p.rglob("test_*.py"):
|
|
283
|
+
files.append(str(f.resolve()))
|
|
284
|
+
if name_filter:
|
|
285
|
+
files = [f for f in files if name_filter in pathlib.Path(f).name]
|
|
286
|
+
return sorted(set(files))
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def run_tests_in_parallel(tax_benefit_system, paths, options, num_workers, verbose):
|
|
290
|
+
"""Run OpenFisca tests in parallel across multiple worker processes.
|
|
291
|
+
|
|
292
|
+
This function implements parallel test execution by:
|
|
293
|
+
1. Discovering all test files in the given paths
|
|
294
|
+
2. Splitting test files into batches for each worker
|
|
295
|
+
3. Spawning pytest worker processes with the parallel_plugin
|
|
296
|
+
4. Monitoring worker progress and collecting output
|
|
297
|
+
5. Stopping all workers on first failure (fail-fast behavior)
|
|
298
|
+
6. Reporting results with colored output and timing
|
|
299
|
+
|
|
300
|
+
Architecture:
|
|
301
|
+
- Main process: orchestrates workers via subprocess.Popen
|
|
302
|
+
- Each worker: independent pytest process with parallel_plugin loaded
|
|
303
|
+
- Communication: PTY for real-time output capture
|
|
304
|
+
- Configuration: passed via environment variables
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
tax_benefit_system: The tax-benefit system to test
|
|
308
|
+
paths: List of paths containing test files
|
|
309
|
+
options: Test options dict (name_filter, verbose, etc.)
|
|
310
|
+
num_workers: Number of parallel workers (0 = auto-detect from CPU count)
|
|
311
|
+
verbose: If True, print detailed output from each worker
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
Exit code: 0 for success, 1 for failure
|
|
315
|
+
|
|
316
|
+
Notes:
|
|
317
|
+
- Uses fail-fast: stops all workers on first failure
|
|
318
|
+
- Provides progress updates every 2 seconds
|
|
319
|
+
- Captures and displays output from failed workers
|
|
320
|
+
- Uses PTY to ensure proper output flushing
|
|
321
|
+
- On Windows, falls back to single-threaded testing
|
|
322
|
+
"""
|
|
323
|
+
if not PARALLEL_AVAILABLE:
|
|
324
|
+
print(
|
|
325
|
+
"Parallel testing not available on this platform (requires Unix PTY support)."
|
|
326
|
+
)
|
|
327
|
+
print("Falling back to single-threaded testing...")
|
|
328
|
+
return run_tests(tax_benefit_system, paths, options)
|
|
329
|
+
|
|
330
|
+
test_files = discover_test_files(paths, options.get("name_filter"))
|
|
331
|
+
|
|
332
|
+
if not test_files:
|
|
333
|
+
print("No test files found")
|
|
334
|
+
return 0
|
|
335
|
+
|
|
336
|
+
# Auto-detect number of workers based on CPU count
|
|
337
|
+
if num_workers <= 0:
|
|
338
|
+
try:
|
|
339
|
+
import multiprocessing
|
|
340
|
+
|
|
341
|
+
# Use N-1 CPUs to leave one for the system
|
|
342
|
+
num_workers = max(1, multiprocessing.cpu_count() - 1)
|
|
343
|
+
except (ImportError, NotImplementedError):
|
|
344
|
+
num_workers = 1
|
|
345
|
+
|
|
346
|
+
# Limit workers to number of test files (no point having idle workers)
|
|
347
|
+
num_workers = min(num_workers, len(test_files))
|
|
348
|
+
|
|
349
|
+
# Split test files evenly across workers using round-robin distribution
|
|
350
|
+
# This ensures balanced workload even with uneven file counts
|
|
351
|
+
batches = [[] for _ in range(num_workers)]
|
|
352
|
+
for i, f in enumerate(test_files):
|
|
353
|
+
batches[i % num_workers].append(f)
|
|
354
|
+
|
|
355
|
+
# Remove empty batches and adjust worker count
|
|
356
|
+
batches = [b for b in batches if b]
|
|
357
|
+
num_workers = len(batches)
|
|
358
|
+
|
|
359
|
+
print(f"Running {len(test_files)} test files across {num_workers} workers...")
|
|
360
|
+
|
|
361
|
+
# Prepare environment variables for pytest workers
|
|
362
|
+
env = _create_worker_environment(options)
|
|
363
|
+
|
|
364
|
+
# Get python executable with fallback
|
|
365
|
+
python_bin = sys.executable or shutil.which("python3") or shutil.which("python")
|
|
366
|
+
if not python_bin:
|
|
367
|
+
print("Error: Could not find Python executable")
|
|
368
|
+
return 1
|
|
369
|
+
|
|
370
|
+
# Initialize data structures for worker management
|
|
371
|
+
procs = [] # List of (worker_id, subprocess.Popen) tuples
|
|
372
|
+
fds = [] # List of (worker_id, master_fd) tuples for PTY communication
|
|
373
|
+
outputs = ["" for _ in range(num_workers)] # Accumulated output from each worker
|
|
374
|
+
worker_info = {} # Metadata about each worker (files, timing, status)
|
|
375
|
+
start_time = time.time()
|
|
376
|
+
|
|
377
|
+
# Launch worker processes
|
|
378
|
+
for idx, batch in enumerate(batches):
|
|
379
|
+
if not batch:
|
|
380
|
+
continue
|
|
381
|
+
|
|
382
|
+
# Spawn worker process
|
|
383
|
+
p, master_fd = _spawn_worker(batch, python_bin, env, verbose)
|
|
384
|
+
procs.append((idx, p))
|
|
385
|
+
fds.append((idx, master_fd))
|
|
386
|
+
|
|
387
|
+
# Store worker metadata for progress reporting
|
|
388
|
+
file_names = [os.path.basename(f) for f in batch]
|
|
389
|
+
file_str = _format_file_list(file_names)
|
|
390
|
+
|
|
391
|
+
worker_info[idx] = {
|
|
392
|
+
"files": file_str,
|
|
393
|
+
"batch": batch,
|
|
394
|
+
"start_time": time.time(),
|
|
395
|
+
"status": "running",
|
|
396
|
+
}
|
|
397
|
+
print(f" Worker {idx}: {file_str}")
|
|
398
|
+
|
|
399
|
+
print()
|
|
400
|
+
|
|
401
|
+
running = {i for (i, _) in procs} # Set of worker IDs still running
|
|
402
|
+
exit_codes = {} # Map of worker_id -> exit_code
|
|
403
|
+
last_update = time.time() # For throttling progress updates
|
|
404
|
+
|
|
405
|
+
# Create fd -> worker_idx mapping for O(1) lookup
|
|
406
|
+
fd_to_idx = {fd: idx for idx, fd in fds}
|
|
407
|
+
|
|
408
|
+
# Monitor workers until all complete or one fails
|
|
409
|
+
while running:
|
|
410
|
+
# Use select() to wait for output from any worker
|
|
411
|
+
# Timeout of 0.1s allows periodic checking of process status
|
|
412
|
+
rlist = [fd for (_, fd) in fds]
|
|
413
|
+
readable, _, _ = select.select(rlist, [], [], 0.1)
|
|
414
|
+
|
|
415
|
+
# Read output from any workers that have data available
|
|
416
|
+
_read_worker_output(readable, fd_to_idx, outputs, verbose)
|
|
417
|
+
|
|
418
|
+
# Print progress update every 2 seconds (avoid spamming)
|
|
419
|
+
current_time = time.time()
|
|
420
|
+
if current_time - last_update >= 2.0:
|
|
421
|
+
completed = len(exit_codes)
|
|
422
|
+
total = num_workers
|
|
423
|
+
elapsed = current_time - start_time
|
|
424
|
+
print(
|
|
425
|
+
f"\rProgress: {completed}/{total} workers completed ({elapsed:.1f}s elapsed)",
|
|
426
|
+
end="",
|
|
427
|
+
flush=True,
|
|
428
|
+
)
|
|
429
|
+
last_update = current_time
|
|
430
|
+
|
|
431
|
+
# Check each worker for completion
|
|
432
|
+
for idx2, p in procs:
|
|
433
|
+
if idx2 in running:
|
|
434
|
+
ret = p.poll() # Non-blocking check if process finished
|
|
435
|
+
if ret is not None:
|
|
436
|
+
# Worker completed - record exit code and timing
|
|
437
|
+
exit_codes[idx2] = ret
|
|
438
|
+
duration = time.time() - worker_info[idx2]["start_time"]
|
|
439
|
+
worker_info[idx2]["status"] = "passed" if ret == 0 else "failed"
|
|
440
|
+
worker_info[idx2]["duration"] = duration
|
|
441
|
+
|
|
442
|
+
# Print completion with colored status indicator
|
|
443
|
+
status_symbol = "✓" if ret == 0 else "✗"
|
|
444
|
+
status_color = "\033[32m" if ret == 0 else "\033[31m"
|
|
445
|
+
reset_color = "\033[0m"
|
|
446
|
+
print(
|
|
447
|
+
f"\r{status_color}{status_symbol}{reset_color} Worker {idx2}: {worker_info[idx2]['files']} ({duration:.1f}s)"
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
running.remove(idx2)
|
|
451
|
+
|
|
452
|
+
# Fail-fast: if any worker fails, terminate all others
|
|
453
|
+
if ret != 0:
|
|
454
|
+
_terminate_workers(procs, running)
|
|
455
|
+
running.clear()
|
|
456
|
+
break
|
|
457
|
+
|
|
458
|
+
# Close file descriptors
|
|
459
|
+
for _, fd in fds:
|
|
460
|
+
try:
|
|
461
|
+
os.close(fd)
|
|
462
|
+
except OSError:
|
|
463
|
+
pass
|
|
464
|
+
|
|
465
|
+
total_duration = time.time() - start_time
|
|
466
|
+
print()
|
|
467
|
+
|
|
468
|
+
# Report failures
|
|
469
|
+
for idx, code in exit_codes.items():
|
|
470
|
+
if code != 0:
|
|
471
|
+
print(f"\n{'=' * 80}")
|
|
472
|
+
print(f"Worker {idx} FAILED")
|
|
473
|
+
print(f"{'=' * 80}")
|
|
474
|
+
print(f"Files: {', '.join(worker_info[idx]['batch'])}")
|
|
475
|
+
print(f"{'=' * 80}")
|
|
476
|
+
print(outputs[idx])
|
|
477
|
+
return 1
|
|
478
|
+
|
|
479
|
+
# Success summary
|
|
480
|
+
print(f"{'=' * 80}")
|
|
481
|
+
print("✓ All tests passed!")
|
|
482
|
+
print(
|
|
483
|
+
f" {len(test_files)} test files across {num_workers} workers in {total_duration:.2f}s"
|
|
484
|
+
)
|
|
485
|
+
print(f"{'=' * 80}")
|
|
486
|
+
return 0
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def run_tests(
|
|
490
|
+
tax_benefit_system: TaxBenefitSystem,
|
|
491
|
+
paths: str | Sequence[str],
|
|
492
|
+
options: Options = options,
|
|
493
|
+
) -> int:
|
|
494
|
+
"""Runs all the YAML tests contained in a file or a directory.
|
|
495
|
+
|
|
496
|
+
If ``path`` is a directory, subdirectories will be recursively explored.
|
|
497
|
+
|
|
498
|
+
Args:
|
|
499
|
+
tax_benefit_system: the tax-benefit system to use to run the tests.
|
|
500
|
+
paths: A path, or a list of paths, towards the files or directories containing the tests to run. If a path is a directory, subdirectories will be recursively explored.
|
|
501
|
+
options: See more details below.
|
|
502
|
+
|
|
503
|
+
Returns:
|
|
504
|
+
The number of successful tests executed.
|
|
505
|
+
|
|
506
|
+
Raises:
|
|
507
|
+
:exc:`AssertionError`: if a test does not pass.
|
|
508
|
+
|
|
509
|
+
**Testing options**:
|
|
510
|
+
|
|
511
|
+
+-------------------------------+-----------+-------------------------------------------+
|
|
512
|
+
| Key | Type | Role |
|
|
513
|
+
+===============================+===========+===========================================+
|
|
514
|
+
| verbose | ``bool`` | |
|
|
515
|
+
+-------------------------------+-----------+ See :any:`openfisca_test` options doc |
|
|
516
|
+
| name_filter | ``str`` | |
|
|
517
|
+
+-------------------------------+-----------+-------------------------------------------+
|
|
518
|
+
|
|
519
|
+
"""
|
|
520
|
+
argv = []
|
|
521
|
+
plugins = [OpenFiscaPlugin(tax_benefit_system, options)]
|
|
522
|
+
|
|
523
|
+
if options.get("pdb"):
|
|
524
|
+
argv.append("--pdb")
|
|
525
|
+
|
|
526
|
+
if options.get("verbose"):
|
|
527
|
+
argv.append("--verbose")
|
|
528
|
+
|
|
529
|
+
if isinstance(paths, str):
|
|
530
|
+
paths = [paths]
|
|
531
|
+
|
|
532
|
+
return pytest.main([*argv, *paths], plugins=plugins)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
class YamlFile(pytest.File):
|
|
536
|
+
def __init__(self, *, tax_benefit_system, options, **kwargs) -> None:
|
|
537
|
+
super().__init__(**kwargs)
|
|
538
|
+
self.tax_benefit_system = tax_benefit_system
|
|
539
|
+
self.options = options
|
|
540
|
+
|
|
541
|
+
def collect(self):
|
|
542
|
+
try:
|
|
543
|
+
with open(self.path) as file:
|
|
544
|
+
tests = yaml.load(file, Loader=Loader)
|
|
545
|
+
except (yaml.scanner.ScannerError, yaml.parser.ParserError, TypeError):
|
|
546
|
+
message = os.linesep.join(
|
|
547
|
+
[
|
|
548
|
+
traceback.format_exc(),
|
|
549
|
+
f"'{self.path}' is not a valid YAML file. Check the stack trace above for more details.",
|
|
550
|
+
],
|
|
551
|
+
)
|
|
552
|
+
raise ValueError(message)
|
|
553
|
+
|
|
554
|
+
if not isinstance(tests, list):
|
|
555
|
+
tests: Sequence[dict] = [tests]
|
|
556
|
+
|
|
557
|
+
for test in tests:
|
|
558
|
+
if not self.should_ignore(test):
|
|
559
|
+
yield YamlItem.from_parent(
|
|
560
|
+
self,
|
|
561
|
+
name="",
|
|
562
|
+
baseline_tax_benefit_system=self.tax_benefit_system,
|
|
563
|
+
test=test,
|
|
564
|
+
options=self.options,
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
def should_ignore(self, test):
|
|
568
|
+
name_filter = self.options.get("name_filter")
|
|
569
|
+
return (
|
|
570
|
+
name_filter is not None
|
|
571
|
+
and name_filter not in os.path.splitext(os.path.basename(self.path))[0]
|
|
572
|
+
and name_filter not in test.get("name", "")
|
|
573
|
+
and name_filter not in test.get("keywords", [])
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
class YamlItem(pytest.Item):
|
|
578
|
+
"""Terminal nodes of the test collection tree."""
|
|
579
|
+
|
|
580
|
+
def __init__(self, *, baseline_tax_benefit_system, test, options, **kwargs) -> None:
|
|
581
|
+
super().__init__(**kwargs)
|
|
582
|
+
self.baseline_tax_benefit_system = baseline_tax_benefit_system
|
|
583
|
+
self.options = options
|
|
584
|
+
self.test = build_test(test)
|
|
585
|
+
self.simulation = None
|
|
586
|
+
self.tax_benefit_system = None
|
|
587
|
+
|
|
588
|
+
def runtest(self) -> None:
|
|
589
|
+
self.name = self.test.name
|
|
590
|
+
|
|
591
|
+
if self.test.output is None:
|
|
592
|
+
msg = f"Missing key 'output' in test '{self.name}' in file '{self.path}'"
|
|
593
|
+
raise ValueError(msg)
|
|
594
|
+
|
|
595
|
+
self.tax_benefit_system = _get_tax_benefit_system(
|
|
596
|
+
self.baseline_tax_benefit_system,
|
|
597
|
+
self.test.reforms,
|
|
598
|
+
self.test.extensions,
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
if self.test.parameters:
|
|
602
|
+
self.tax_benefit_system = InYamlTestReform(
|
|
603
|
+
self.tax_benefit_system, self.test.parameters
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
builder = SimulationBuilder()
|
|
607
|
+
input = self.test.input
|
|
608
|
+
period = self.test.period
|
|
609
|
+
max_spiral_loops = self.test.max_spiral_loops
|
|
610
|
+
verbose = self.options.get("verbose")
|
|
611
|
+
aggregate = self.options.get("aggregate")
|
|
612
|
+
max_depth = self.options.get("max_depth")
|
|
613
|
+
performance_graph = self.options.get("performance_graph")
|
|
614
|
+
performance_tables = self.options.get("performance_tables")
|
|
615
|
+
|
|
616
|
+
try:
|
|
617
|
+
builder.set_default_period(period)
|
|
618
|
+
self.simulation = builder.build_from_dict(self.tax_benefit_system, input)
|
|
619
|
+
assert self.simulation is not None, (
|
|
620
|
+
"Simulation should be properly initialized, Check your test input"
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
except (VariableNotFound, SituationParsingError, AssertionError):
|
|
624
|
+
raise
|
|
625
|
+
except Exception as e:
|
|
626
|
+
error_message = os.linesep.join(
|
|
627
|
+
[str(e), "", f"Unexpected error raised while parsing '{self.path}'"],
|
|
628
|
+
)
|
|
629
|
+
raise ValueError(error_message).with_traceback(
|
|
630
|
+
sys.exc_info()[2],
|
|
631
|
+
) from e # Keep the stack trace from the root error
|
|
632
|
+
|
|
633
|
+
if max_spiral_loops:
|
|
634
|
+
self.simulation.max_spiral_loops = max_spiral_loops
|
|
635
|
+
|
|
636
|
+
try:
|
|
637
|
+
self.simulation.trace = verbose or performance_graph or performance_tables
|
|
638
|
+
self.check_output()
|
|
639
|
+
finally:
|
|
640
|
+
tracer = self.simulation.tracer
|
|
641
|
+
if verbose:
|
|
642
|
+
ignore_default = self.options.get("ignore_default", False)
|
|
643
|
+
self.print_computation_log(
|
|
644
|
+
tracer,
|
|
645
|
+
aggregate,
|
|
646
|
+
max_depth,
|
|
647
|
+
ignore_default,
|
|
648
|
+
self.tax_benefit_system,
|
|
649
|
+
)
|
|
650
|
+
if performance_graph:
|
|
651
|
+
self.generate_performance_graph(tracer)
|
|
652
|
+
if performance_tables:
|
|
653
|
+
self.generate_performance_tables(tracer)
|
|
654
|
+
|
|
655
|
+
def print_computation_log(
|
|
656
|
+
self, tracer, aggregate, max_depth, ignore_default, tax_benefit_system
|
|
657
|
+
) -> None:
|
|
658
|
+
tracer.print_computation_log(
|
|
659
|
+
aggregate, max_depth, ignore_default, tax_benefit_system
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
def generate_performance_graph(self, tracer) -> None:
|
|
663
|
+
tracer.generate_performance_graph(".")
|
|
664
|
+
|
|
665
|
+
def generate_performance_tables(self, tracer) -> None:
|
|
666
|
+
tracer.generate_performance_tables(".")
|
|
667
|
+
|
|
668
|
+
def check_output(self) -> None:
|
|
669
|
+
output = self.test.output
|
|
670
|
+
|
|
671
|
+
if output is None:
|
|
672
|
+
return
|
|
673
|
+
for key, expected_value in output.items():
|
|
674
|
+
if self.tax_benefit_system.get_variable(key): # If key is a variable
|
|
675
|
+
self.check_variable(key, expected_value, self.test.period)
|
|
676
|
+
elif self.simulation.populations.get(key): # If key is an entity singular
|
|
677
|
+
for variable_name, value in expected_value.items():
|
|
678
|
+
self.check_variable(variable_name, value, self.test.period)
|
|
679
|
+
else:
|
|
680
|
+
population = self.simulation.get_population(plural=key)
|
|
681
|
+
if population is not None: # If key is an entity plural
|
|
682
|
+
for instance_id, instance_values in expected_value.items():
|
|
683
|
+
for variable_name, value in instance_values.items():
|
|
684
|
+
entity_index = population.get_index(instance_id)
|
|
685
|
+
self.check_variable(
|
|
686
|
+
variable_name,
|
|
687
|
+
value,
|
|
688
|
+
self.test.period,
|
|
689
|
+
entity_index,
|
|
690
|
+
)
|
|
691
|
+
else:
|
|
692
|
+
raise VariableNotFound(key, self.tax_benefit_system)
|
|
693
|
+
|
|
694
|
+
def check_variable(
|
|
695
|
+
self,
|
|
696
|
+
variable_name: str,
|
|
697
|
+
expected_value,
|
|
698
|
+
period,
|
|
699
|
+
entity_index=None,
|
|
700
|
+
):
|
|
701
|
+
if self.should_ignore_variable(variable_name):
|
|
702
|
+
return None
|
|
703
|
+
|
|
704
|
+
if isinstance(expected_value, dict):
|
|
705
|
+
for requested_period, expected_value_at_period in expected_value.items():
|
|
706
|
+
self.check_variable(
|
|
707
|
+
variable_name,
|
|
708
|
+
expected_value_at_period,
|
|
709
|
+
requested_period,
|
|
710
|
+
entity_index,
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
return None
|
|
714
|
+
|
|
715
|
+
actual_value = self.simulation.calculate(variable_name, period)
|
|
716
|
+
|
|
717
|
+
if entity_index is not None:
|
|
718
|
+
actual_value = actual_value[entity_index]
|
|
719
|
+
|
|
720
|
+
return assert_near(
|
|
721
|
+
actual_value,
|
|
722
|
+
expected_value,
|
|
723
|
+
self.test.absolute_error_margin[variable_name],
|
|
724
|
+
f"{variable_name}@{period}: ",
|
|
725
|
+
self.test.relative_error_margin[variable_name],
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
def should_ignore_variable(self, variable_name: str):
|
|
729
|
+
only_variables = self.options.get("only_variables")
|
|
730
|
+
ignore_variables = self.options.get("ignore_variables")
|
|
731
|
+
variable_ignored = (
|
|
732
|
+
ignore_variables is not None and variable_name in ignore_variables
|
|
733
|
+
)
|
|
734
|
+
variable_not_tested = (
|
|
735
|
+
only_variables is not None and variable_name not in only_variables
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
return variable_ignored or variable_not_tested
|
|
739
|
+
|
|
740
|
+
def repr_failure(self, excinfo):
|
|
741
|
+
if not isinstance(
|
|
742
|
+
excinfo.value,
|
|
743
|
+
(AssertionError, VariableNotFound, SituationParsingError),
|
|
744
|
+
):
|
|
745
|
+
return super().repr_failure(excinfo)
|
|
746
|
+
|
|
747
|
+
message = excinfo.value.args[0]
|
|
748
|
+
if isinstance(excinfo.value, SituationParsingError):
|
|
749
|
+
message = f"Could not parse situation described: {message}"
|
|
750
|
+
|
|
751
|
+
return os.linesep.join(
|
|
752
|
+
[
|
|
753
|
+
f"{self.path!s}:",
|
|
754
|
+
f" Test '{self.name!s}':",
|
|
755
|
+
textwrap.indent(message, " "),
|
|
756
|
+
],
|
|
757
|
+
)
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
class OpenFiscaPlugin:
|
|
761
|
+
def __init__(self, tax_benefit_system, options) -> None:
|
|
762
|
+
self.tax_benefit_system = tax_benefit_system
|
|
763
|
+
self.options = options
|
|
764
|
+
|
|
765
|
+
def pytest_collect_file(self, parent, path):
|
|
766
|
+
"""Called by pytest for all plugins.
|
|
767
|
+
:return: The collector for test methods.
|
|
768
|
+
"""
|
|
769
|
+
if path.ext in [".yaml", ".yml"]:
|
|
770
|
+
return YamlFile.from_parent(
|
|
771
|
+
parent,
|
|
772
|
+
path=pathlib.Path(path),
|
|
773
|
+
tax_benefit_system=self.tax_benefit_system,
|
|
774
|
+
options=self.options,
|
|
775
|
+
)
|
|
776
|
+
return None
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
def _get_tax_benefit_system(baseline, reforms, extensions):
|
|
780
|
+
if not isinstance(reforms, list):
|
|
781
|
+
reforms = [reforms]
|
|
782
|
+
if not isinstance(extensions, list):
|
|
783
|
+
extensions = [extensions]
|
|
784
|
+
|
|
785
|
+
# keep reforms order in cache, ignore extensions order
|
|
786
|
+
key = hash((id(baseline), ":".join(reforms), frozenset(extensions)))
|
|
787
|
+
if _tax_benefit_system_cache.get(key):
|
|
788
|
+
return _tax_benefit_system_cache.get(key)
|
|
789
|
+
|
|
790
|
+
current_tax_benefit_system = baseline.clone()
|
|
791
|
+
|
|
792
|
+
for reform_path in reforms:
|
|
793
|
+
current_tax_benefit_system = current_tax_benefit_system.apply_reform(
|
|
794
|
+
reform_path,
|
|
795
|
+
)
|
|
796
|
+
|
|
797
|
+
for extension in extensions:
|
|
798
|
+
current_tax_benefit_system.load_extension(extension)
|
|
799
|
+
|
|
800
|
+
_tax_benefit_system_cache[key] = current_tax_benefit_system
|
|
801
|
+
|
|
802
|
+
return current_tax_benefit_system
|