Skip to content

Commit b68aee3

Browse files
Merge pull request #175 from CITCOM-project/json_output_to_file
Json output to file
2 parents 7e01033 + 3adc6a2 commit b68aee3

File tree

4 files changed

+89
-34
lines changed

4 files changed

+89
-34
lines changed

causal_testing/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,3 @@
1212

1313
logger = logging.getLogger(__name__)
1414
logger.setLevel(logging.INFO)
15-
logger.addHandler(logging.StreamHandler())

causal_testing/json_front/json_class.py

Lines changed: 55 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from dataclasses import dataclass
99
from pathlib import Path
10+
from statistics import StatisticsError
1011

1112
import pandas as pd
1213
import scipy
@@ -42,14 +43,15 @@ class JsonUtility:
4243
:attr {CausalSpecification} causal_specification:
4344
"""
4445

45-
def __init__(self, log_path):
46-
self.paths = None
46+
def __init__(self, output_path: str, output_overwrite: bool = False):
47+
self.input_paths = None
4748
self.variables = None
4849
self.data = []
4950
self.test_plan = None
5051
self.scenario = None
5152
self.causal_specification = None
52-
self.setup_logger(log_path)
53+
self.output_path = Path(output_path)
54+
self.check_file_exists(self.output_path, output_overwrite)
5355

5456
def set_paths(self, json_path: str, dag_path: str, data_paths: str):
5557
"""
@@ -58,14 +60,14 @@ def set_paths(self, json_path: str, dag_path: str, data_paths: str):
5860
:param dag_path: string path representation to the .dot file containing the Causal DAG
5961
:param data_paths: string path representation to the data files
6062
"""
61-
self.paths = JsonClassPaths(json_path=json_path, dag_path=dag_path, data_paths=data_paths)
63+
self.input_paths = JsonClassPaths(json_path=json_path, dag_path=dag_path, data_paths=data_paths)
6264

6365
def setup(self, scenario: Scenario):
6466
"""Function to populate all the necessary parts of the json_class needed to execute tests"""
6567
self.scenario = scenario
6668
self.scenario.setup_treatment_variables()
6769
self.causal_specification = CausalSpecification(
68-
scenario=self.scenario, causal_dag=CausalDAG(self.paths.dag_path)
70+
scenario=self.scenario, causal_dag=CausalDAG(self.input_paths.dag_path)
6971
)
7072
self._json_parse()
7173
self._populate_metas()
@@ -103,12 +105,16 @@ def generate_tests(self, effects: dict, mutates: dict, estimators: dict, f_flag:
103105
abstract_test = self._create_abstract_test_case(test, mutates, effects)
104106

105107
concrete_tests, dummy = abstract_test.generate_concrete_tests(5, 0.05)
106-
logger.info("Executing test: %s", test["name"])
107-
logger.info(abstract_test)
108-
logger.info([abstract_test.treatment_variable.name, abstract_test.treatment_variable.distribution])
109-
logger.info("Number of concrete tests for test case: %s", str(len(concrete_tests)))
110108
failures = self._execute_tests(concrete_tests, estimators, test, f_flag)
111-
logger.info("%s/%s failed for %s\n", failures, len(concrete_tests), test["name"])
109+
msg = (
110+
f"Executing test: {test['name']} \n"
111+
+ "abstract_test \n"
112+
+ f"{abstract_test} \n"
113+
+ f"{abstract_test.treatment_variable.name},{abstract_test.treatment_variable.distribution} \n"
114+
+ f"Number of concrete tests for test case: {str(len(concrete_tests))} \n"
115+
+ f"{failures}/{len(concrete_tests)} failed for {test['name']}"
116+
)
117+
self._append_to_file(msg, logging.INFO)
112118

113119
def _execute_tests(self, concrete_tests, estimators, test, f_flag):
114120
failures = 0
@@ -120,9 +126,9 @@ def _execute_tests(self, concrete_tests, estimators, test, f_flag):
120126

121127
def _json_parse(self):
122128
"""Parse a JSON input file into inputs, outputs, metas and a test plan"""
123-
with open(self.paths.json_path, encoding="utf-8") as f:
129+
with open(self.input_paths.json_path, encoding="utf-8") as f:
124130
self.test_plan = json.load(f)
125-
for data_file in self.paths.data_paths:
131+
for data_file in self.input_paths.data_paths:
126132
df = pd.read_csv(data_file, header=0)
127133
self.data.append(df)
128134
self.data = pd.concat(self.data)
@@ -139,7 +145,7 @@ def _populate_metas(self):
139145
fitter.fit()
140146
(dist, params) = list(fitter.get_best(method="sumsquare_error").items())[0]
141147
var.distribution = getattr(scipy.stats, dist)(**params)
142-
logger.info(var.name + f" {dist}({params})")
148+
self._append_to_file(var.name + f" {dist}({params})", logging.INFO)
143149

144150
def _execute_test_case(self, causal_test_case: CausalTestCase, estimator: Estimator, f_flag: bool) -> bool:
145151
"""Executes a singular test case, prints the results and returns the test case result
@@ -166,12 +172,13 @@ def _execute_test_case(self, causal_test_case: CausalTestCase, estimator: Estima
166172
)
167173
else:
168174
result_string = f"{causal_test_result.test_value.value} no confidence intervals"
169-
if f_flag:
170-
assert test_passes, (
171-
f"{causal_test_case}\n FAILED - expected {causal_test_case.expected_causal_effect}, "
172-
f"got {result_string}"
173-
)
175+
174176
if not test_passes:
177+
if f_flag:
178+
raise StatisticsError(
179+
f"{causal_test_case}\n FAILED - expected {causal_test_case.expected_causal_effect}, "
180+
f"got {result_string}"
181+
)
175182
failed = True
176183
logger.warning(" FAILED- expected %s, got %s", causal_test_case.expected_causal_effect, result_string)
177184
return failed
@@ -211,15 +218,32 @@ def add_modelling_assumptions(self, estimation_model: Estimator): # pylint: dis
211218
"""
212219
return
213220

221+
def _append_to_file(self, line: str, log_level: int = None):
222+
"""Appends given line(s) to the current output file. If log_level is specified it also logs that message to the
223+
logging level.
224+
:param line: The line or lines of text to be appended to the file
225+
:param log_level: An integer representing the logging level as specified by pythons inbuilt logging module. It
226+
is possible to use the inbuilt logging level variables such as logging.INFO and logging.WARNING
227+
"""
228+
with open(self.output_path, "a", encoding="utf-8") as f:
229+
f.write(
230+
line + "\n",
231+
)
232+
if log_level:
233+
logger.log(level=log_level, msg=line)
234+
214235
@staticmethod
215-
def setup_logger(log_path: str):
216-
"""Setups up logging instance for the module and adds a FileHandler stream so all stdout prints are also
217-
sent to the logfile
218-
:param log_path: Path specifying location and name of the logging file to be used
236+
def check_file_exists(output_path: Path, overwrite: bool):
237+
"""Method that checks if the given path to an output file already exists. If overwrite is true the check is
238+
passed.
239+
:param output_path: File path for the output file of the JSON Frontend
240+
:param overwrite: bool that if true, the current file can be overwritten
219241
"""
220-
setup_log = logging.getLogger(__name__)
221-
file_handler = logging.FileHandler(Path(log_path))
222-
setup_log.addHandler(file_handler)
242+
if output_path.is_file():
243+
if overwrite:
244+
output_path.unlink()
245+
else:
246+
raise FileExistsError(f"Chosen file output ({output_path}) already exists")
223247

224248
@staticmethod
225249
def get_args(test_args=None) -> argparse.Namespace:
@@ -235,6 +259,12 @@ def get_args(test_args=None) -> argparse.Namespace:
235259
help="if included, the script will stop if a test fails",
236260
action="store_true",
237261
)
262+
parser.add_argument(
263+
"-w",
264+
help="Specify to overwrite any existing output files. This can lead to the loss of existing outputs if not "
265+
"careful",
266+
action="store_true",
267+
)
238268
parser.add_argument(
239269
"--log_path",
240270
help="Specify a directory to change the location of the log file",

causal_testing/testing/estimators.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ def _run_logistic_regression(self, data) -> RegressionResultsWrapper:
158158
model = smf.logit(formula=self.formula, data=data).fit(disp=0)
159159
return model
160160

161-
def estimate(self, data: pd.DataFrame, adjustment_config:dict = None) -> RegressionResultsWrapper:
161+
def estimate(self, data: pd.DataFrame, adjustment_config: dict = None) -> RegressionResultsWrapper:
162162
"""add terms to the dataframe and estimate the outcome from the data
163163
:param data: A pandas dataframe containing execution data from the system-under-test.
164164
:param adjustment_config: Dictionary containing the adjustment configuration of the adjustment set

tests/json_front_tests/test_json_class.py

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import unittest
22
from pathlib import Path
3+
from statistics import StatisticsError
34
import scipy
45
import csv
56
import json
@@ -29,7 +30,7 @@ def setUp(self) -> None:
2930
self.json_path = str(test_data_dir_path / json_file_name)
3031
self.dag_path = str(test_data_dir_path / dag_file_name)
3132
self.data_path = [str(test_data_dir_path / data_file_name)]
32-
self.json_class = JsonUtility("logs.log")
33+
self.json_class = JsonUtility("temp_out.txt", True)
3334
self.example_distribution = scipy.stats.uniform(1, 10)
3435
self.input_dict_list = [{"name": "test_input", "datatype": float, "distribution": self.example_distribution}]
3536
self.output_dict_list = [{"name": "test_output", "datatype": float}]
@@ -41,9 +42,9 @@ def setUp(self) -> None:
4142
self.json_class.setup(self.scenario)
4243

4344
def test_setting_paths(self):
44-
self.assertEqual(self.json_class.paths.json_path, Path(self.json_path))
45-
self.assertEqual(self.json_class.paths.dag_path, Path(self.dag_path))
46-
self.assertEqual(self.json_class.paths.data_paths, [Path(self.data_path[0])]) # Needs to be list of Paths
45+
self.assertEqual(self.json_class.input_paths.json_path, Path(self.json_path))
46+
self.assertEqual(self.json_class.input_paths.dag_path, Path(self.dag_path))
47+
self.assertEqual(self.json_class.input_paths.data_paths, [Path(self.data_path[0])]) # Needs to be list of Paths
4748

4849
def test_set_inputs(self):
4950
ctf_input = [Input("test_input", float, self.example_distribution)]
@@ -73,6 +74,30 @@ def test_setup_scenario(self):
7374
def test_setup_causal_specification(self):
7475
self.assertIsInstance(self.json_class.causal_specification, CausalSpecification)
7576

77+
def test_f_flag(self):
78+
example_test = {
79+
"tests": [
80+
{
81+
"name": "test1",
82+
"mutations": {"test_input": "Increase"},
83+
"estimator": "LinearRegressionEstimator",
84+
"estimate_type": "ate",
85+
"effect_modifiers": [],
86+
"expectedEffect": {"test_output": "NoEffect"},
87+
"skip": False,
88+
}
89+
]
90+
}
91+
self.json_class.test_plan = example_test
92+
effects = {"NoEffect": NoEffect()}
93+
mutates = {
94+
"Increase": lambda x: self.json_class.scenario.treatment_variables[x].z3
95+
> self.json_class.scenario.variables[x].z3
96+
}
97+
estimators = {"LinearRegressionEstimator": LinearRegressionEstimator}
98+
with self.assertRaises(StatisticsError):
99+
self.json_class.generate_tests(effects, mutates, estimators, True)
100+
76101
def test_generate_tests_from_json(self):
77102
example_test = {
78103
"tests": [
@@ -95,11 +120,12 @@ def test_generate_tests_from_json(self):
95120
}
96121
estimators = {"LinearRegressionEstimator": LinearRegressionEstimator}
97122

98-
with self.assertLogs() as captured:
99-
self.json_class.generate_tests(effects, mutates, estimators, False)
123+
self.json_class.generate_tests(effects, mutates, estimators, False)
100124

101125
# Test that the final log message prints that failed tests are printed, which is expected behaviour for this scenario
102-
self.assertIn("failed", captured.records[-1].getMessage())
126+
with open("temp_out.txt", 'r') as reader:
127+
temp_out = reader.readlines()
128+
self.assertIn("failed", temp_out[-1])
103129

104130
def tearDown(self) -> None:
105131
pass

0 commit comments

Comments
 (0)