11"""This module contains the main entrypoint functionality to the Causal Testing Framework."""
22
3- import json
3+ import argparse
44import logging
5- import os
6- import tempfile
5+ from enum import Enum
76from importlib .metadata import entry_points
8- from pathlib import Path
7+ from typing import Optional , Sequence
98
109import networkx as nx
1110import pandas as pd
1211
12+ from causal_testing .causal_testing_framework import CausalTestingFramework , read_dataframe
1313from causal_testing .testing .metamorphic_relation import generate_causal_tests
1414
15- from .main import CausalTestingFramework , CausalTestingPaths , Command , parse_args , setup_logging
15+ logger = logging .getLogger (__name__ )
16+
17+
18+ class Command (Enum ):
19+ """
20+ Enum for supported CTF commands.
21+ """
22+
23+ TEST = "test"
24+ GENERATE = "generate"
25+ DISCOVER = "discover"
26+
27+
28+ def setup_logging (level : str ) -> None :
29+ """Set up logging configuration."""
30+ logging .basicConfig (
31+ level = level , format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" , datefmt = "%Y-%m-%d %H:%M:%S"
32+ )
33+
34+
35+ def parse_args (args : Optional [Sequence [str ]] = None ) -> argparse .Namespace :
36+ """Parse command line arguments."""
37+ main_parser = argparse .ArgumentParser (
38+ add_help = True ,
39+ description = "Causal Testing Framework - "
40+ "A causal inference-driven framework for functional black-box testing of complex software." ,
41+ )
42+
43+ main_parser .add_argument (
44+ "-l" ,
45+ "--log_level" ,
46+ default = "WARNING" ,
47+ type = str .upper ,
48+ choices = ["NONE" , "DEBUG" , "INFO" , "WARNING" , "ERROR" , "CRITICAL" ],
49+ help = "Set the logging level (default: WARNING)." ,
50+ )
51+
52+ subparsers = main_parser .add_subparsers (
53+ help = "The action you want to run - call `causal_testing {action} -h` for further details" , dest = "command"
54+ )
55+
56+ # Generation
57+ parser_generate = subparsers .add_parser (Command .GENERATE .value , help = "Generate causal tests from a DAG" )
58+ parser_generate .add_argument ("-D" , "--dag-path" , help = "Path to the DAG file (.dot)" , required = True )
59+ parser_generate .add_argument ("-o" , "--output" , help = "Path for output file (.json)" , required = True )
60+ parser_generate .add_argument (
61+ "-e" ,
62+ "--estimator" ,
63+ help = "The name of the estimator class to use when evaluating tests (defaults to LinearRegressionEstimator)" ,
64+ default = "LinearRegressionEstimator" ,
65+ )
66+ parser_generate .add_argument (
67+ "-T" ,
68+ "--effect-type" ,
69+ help = "The effect type to estimate {direct, total}" ,
70+ default = "direct" ,
71+ )
72+ parser_generate .add_argument (
73+ "-E" ,
74+ "--estimate-type" ,
75+ help = "The estimate type to use when evaluating tests (defaults to coefficient)" ,
76+ default = "coefficient" ,
77+ )
78+ parser_generate .add_argument (
79+ "-i" , "--ignore-cycles" , help = "Ignore cycles in DAG" , action = "store_true" , default = False
80+ )
81+ parser_generate .add_argument (
82+ "--threads" , "-t" , type = int , help = "The number of parallel threads to use." , required = False , default = 0
83+ )
84+
85+ # Testing
86+ parser_test = subparsers .add_parser (Command .TEST .value , help = "Run causal tests" )
87+ parser_test .add_argument ("-D" , "--dag-path" , help = "Path to the DAG file (.dot)" , required = True )
88+ parser_test .add_argument ("-o" , "--output" , help = "Path for output file (.json)" , required = True )
89+ parser_test .add_argument ("-i" , "--ignore-cycles" , help = "Ignore cycles in DAG" , action = "store_true" , default = False )
90+ parser_test .add_argument ("-d" , "--data-paths" , help = "Paths to data files (.csv)" , nargs = "+" , required = True )
91+ parser_test .add_argument ("-t" , "--test-config" , help = "Path to test configuration file (.json)" , required = True )
92+ parser_test .add_argument ("-q" , "--query" , help = "Query string to filter data (e.g. 'age > 18')" , type = str )
93+ parser_test .add_argument (
94+ "-a" , "--adequacy" , help = "Calculate causal test adequacy for each test case" , action = "store_true" , default = False
95+ )
96+ parser_test .add_argument (
97+ "-b" ,
98+ "--adequacy-bootstrap-size" ,
99+ dest = "bootstrap_size" ,
100+ help = "Number of bootstrap samples for causal test adequacy. Defaults to 100" ,
101+ type = int ,
102+ )
103+ parser_test .add_argument (
104+ "-s" ,
105+ "--silent" ,
106+ action = "store_true" ,
107+ help = "Do not crash on error. If set to true, errors are recorded as test results." ,
108+ default = False ,
109+ )
110+
111+ # Discovery
112+ parser_discover = subparsers .add_parser (Command .DISCOVER .value , help = "Discover causal structures from data" )
113+ parser_discover .add_argument ("-d" , "--data-paths" , help = "Paths to data files (.csv)" , nargs = "+" , required = True )
114+ parser_discover .add_argument (
115+ "-a" ,
116+ "--alpha" ,
117+ help = (
118+ "The significance level of the confidence intervals used to determine causality. "
119+ "This should be a value between 0 and 1. Defaults to 0.05 for 95%% confidence intervals."
120+ ),
121+ default = 0.05 ,
122+ )
123+ parser_discover .add_argument (
124+ "-t" ,
125+ "--technique" ,
126+ help = "The name of the technique to use. Currently supported are 'HillClimberDiscovery' and 'NSGADiscovery'" ,
127+ required = True ,
128+ )
129+ parser_discover .add_argument (
130+ "-V" ,
131+ "--variables" ,
132+ help = "The subset of variables from the data to consider. Defaults to all." ,
133+ nargs = "*" ,
134+ default = [],
135+ )
136+ parser_discover .add_argument ("-o" , "--output" , help = "Path for output DAG file (.dot)" , required = True )
137+ parser_discover .add_argument (
138+ "-i" , "--include-edges" , help = "Path to file containing edges to include" , required = False
139+ )
140+ parser_discover .add_argument (
141+ "-e" , "--exclude-edges" , help = "Path to file containing edges to exclude" , required = False
142+ )
143+ parser_discover .add_argument (
144+ "--technique-kwargs" ,
145+ help = "Keywords for the discovery technique. These should be specified as `arg1=value1 arg2=value2...`." ,
146+ nargs = "*" ,
147+ default = [],
148+ )
149+
150+ args = main_parser .parse_args (args )
151+
152+ # Assume the user wants test adequacy if they're setting bootstrap_size
153+ if getattr (args , "bootstrap_size" , None ) is not None :
154+ args .adequacy = True
155+ if getattr (args , "adequacy" , False ) and getattr (args , "bootstrap_size" , None ) is None :
156+ # Need this here rather than a default value because otherwise the above always sets adequacy to True
157+ args .bootstrap_size = 100
158+
159+ args .command = Command (args .command )
160+ return args
16161
17162
18163def main () -> None :
@@ -60,9 +205,14 @@ def main() -> None:
60205 logging .info ("Discovering causal structure" )
61206 # Need to reset index to allow for multiple files having the same index (i.e. starting at zero).
62207 # Otherwise you end up with duplicate indices, which causes problems further down the line
63- df = pd .concat ([pd . read_csv (path ) for path in args .data_paths ]).reset_index ()
208+ df = pd .concat ([read_dataframe (path ) for path in args .data_paths ]).reset_index ()
64209 if args .variables :
65210 df = df [args .variables ]
211+ # Drop unnamed columns
212+ unnamed_columns = [c for c in df .columns if c .startswith ("Unnamed: " )]
213+ if unnamed_columns :
214+ logger .warning (f"Dropping unnamed columns: { unnamed_columns } " )
215+ df = df .drop (unnamed_columns )
66216
67217 discover_class = discover_map [args .technique ].load ()
68218 discover = discover_class (
@@ -80,55 +230,20 @@ def main() -> None:
80230 discover .write_dot (evolved_dag , args .output )
81231 logging .info ("Causal structure discovery completed successfully." )
82232 case Command .TEST :
83- # Create paths object
84- paths = CausalTestingPaths (
233+ # Create and setup framework
234+ framework = CausalTestingFramework ()
235+
236+ framework .setup (
85237 dag_path = args .dag_path ,
86238 data_paths = args .data_paths ,
87- test_config_path = args .test_config ,
88- output_path = args .output ,
239+ test_cases_path = args .test_config ,
240+ query = args .query ,
241+ ignore_cycles = args .ignore_cycles ,
89242 )
90243
91- # Create and setup framework
92- framework = CausalTestingFramework (paths , ignore_cycles = args .ignore_cycles , query = args .query )
93- framework .setup ()
94-
95- # Load and run tests
96- framework .load_tests ()
97-
98- if args .batch_size > 0 :
99- logging .info (f"Running tests in batches of size { args .batch_size } " )
100- with tempfile .TemporaryDirectory () as tmpdir :
101- output_files = []
102- for i , results in enumerate (
103- framework .run_tests_in_batches (
104- batch_size = args .batch_size ,
105- silent = args .silent ,
106- adequacy = args .adequacy ,
107- bootstrap_size = args .bootstrap_size ,
108- )
109- ):
110- temp_file_path = os .path .join (tmpdir , f"output_{ i } .json" )
111- framework .save_results (results , temp_file_path )
112- output_files .append (temp_file_path )
113- del results
114-
115- # Now stitch the results together from the temporary files
116- all_results = []
117- for file_path in output_files :
118- with open (file_path , "r" , encoding = "utf-8" ) as f :
119- all_results .extend (json .load (f ))
120-
121- output_path = Path (args .output )
122- output_path .parent .mkdir (parents = True , exist_ok = True )
123-
124- with open (args .output , "w" , encoding = "utf-8" ) as f :
125- json .dump (all_results , f , indent = 4 )
126- else :
127- logging .info ("Running tests in regular mode" )
128- results = framework .run_tests (
129- silent = args .silent , adequacy = args .adequacy , bootstrap_size = args .bootstrap_size
130- )
131- framework .save_results (results )
244+ logging .info ("Running tests" )
245+ framework .run_tests (silent = args .silent , adequacy = args .adequacy , bootstrap_size = args .bootstrap_size )
246+ framework .save_results (args .output )
132247
133248 logging .info ("Causal testing completed successfully." )
134249
0 commit comments