testgenie-py 0.1.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.
- testgen/__init__.py +0 -0
- testgen/analyzer/__init__.py +0 -0
- testgen/analyzer/ast_analyzer.py +149 -0
- testgen/analyzer/contracts/__init__.py +0 -0
- testgen/analyzer/contracts/contract.py +13 -0
- testgen/analyzer/contracts/no_exception_contract.py +16 -0
- testgen/analyzer/contracts/nonnull_contract.py +15 -0
- testgen/analyzer/fuzz_analyzer.py +106 -0
- testgen/analyzer/random_feedback_analyzer.py +291 -0
- testgen/analyzer/reinforcement_analyzer.py +75 -0
- testgen/analyzer/test_case_analyzer.py +46 -0
- testgen/analyzer/test_case_analyzer_context.py +58 -0
- testgen/controller/__init__.py +0 -0
- testgen/controller/cli_controller.py +194 -0
- testgen/controller/docker_controller.py +169 -0
- testgen/docker/Dockerfile +22 -0
- testgen/docker/poetry.lock +361 -0
- testgen/docker/pyproject.toml +22 -0
- testgen/generator/__init__.py +0 -0
- testgen/generator/code_generator.py +66 -0
- testgen/generator/doctest_generator.py +208 -0
- testgen/generator/generator.py +55 -0
- testgen/generator/pytest_generator.py +77 -0
- testgen/generator/test_generator.py +26 -0
- testgen/generator/unit_test_generator.py +84 -0
- testgen/inspector/__init__.py +0 -0
- testgen/inspector/inspector.py +61 -0
- testgen/main.py +13 -0
- testgen/models/__init__.py +0 -0
- testgen/models/analysis_context.py +56 -0
- testgen/models/function_metadata.py +61 -0
- testgen/models/generator_context.py +63 -0
- testgen/models/test_case.py +8 -0
- testgen/presentation/__init__.py +0 -0
- testgen/presentation/cli_view.py +12 -0
- testgen/q_table/global_q_table.json +1 -0
- testgen/reinforcement/__init__.py +0 -0
- testgen/reinforcement/abstract_state.py +7 -0
- testgen/reinforcement/agent.py +153 -0
- testgen/reinforcement/environment.py +215 -0
- testgen/reinforcement/statement_coverage_state.py +33 -0
- testgen/service/__init__.py +0 -0
- testgen/service/analysis_service.py +260 -0
- testgen/service/cfg_service.py +55 -0
- testgen/service/generator_service.py +169 -0
- testgen/service/service.py +389 -0
- testgen/sqlite/__init__.py +0 -0
- testgen/sqlite/db.py +84 -0
- testgen/sqlite/db_service.py +219 -0
- testgen/tree/__init__.py +0 -0
- testgen/tree/node.py +7 -0
- testgen/tree/tree_utils.py +79 -0
- testgen/util/__init__.py +0 -0
- testgen/util/coverage_utils.py +168 -0
- testgen/util/coverage_visualizer.py +154 -0
- testgen/util/file_utils.py +110 -0
- testgen/util/randomizer.py +122 -0
- testgen/util/utils.py +143 -0
- testgen/util/z3_utils/__init__.py +0 -0
- testgen/util/z3_utils/ast_to_z3.py +99 -0
- testgen/util/z3_utils/branch_condition.py +72 -0
- testgen/util/z3_utils/constraint_extractor.py +36 -0
- testgen/util/z3_utils/variable_finder.py +10 -0
- testgen/util/z3_utils/z3_test_case.py +94 -0
- testgenie_py-0.1.0.dist-info/METADATA +24 -0
- testgenie_py-0.1.0.dist-info/RECORD +68 -0
- testgenie_py-0.1.0.dist-info/WHEEL +4 -0
- testgenie_py-0.1.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,46 @@
|
|
1
|
+
import ast
|
2
|
+
from abc import ABC, abstractmethod
|
3
|
+
from typing import List, Dict
|
4
|
+
|
5
|
+
from testgen.models.test_case import TestCase
|
6
|
+
from testgen.models.analysis_context import AnalysisContext
|
7
|
+
from testgen.models.function_metadata import FunctionMetadata
|
8
|
+
|
9
|
+
class TestCaseAnalyzerStrategy(ABC):
|
10
|
+
def __init__(self, analysis_context: AnalysisContext = None):
|
11
|
+
self._analysis_context = analysis_context
|
12
|
+
|
13
|
+
@abstractmethod
|
14
|
+
def collect_test_cases(self, function_metadata: FunctionMetadata) -> List[TestCase]:
|
15
|
+
pass
|
16
|
+
|
17
|
+
def get_function_metadata(self, func_name: str) -> FunctionMetadata | None:
|
18
|
+
if self._analysis_context and self._analysis_context.function_data:
|
19
|
+
for func_metadata in self._analysis_context.function_data:
|
20
|
+
if func_metadata.function_name == func_name:
|
21
|
+
return func_metadata
|
22
|
+
return None
|
23
|
+
|
24
|
+
def get_function_metadata_by_node(self, func_node: ast.FunctionDef) -> FunctionMetadata:
|
25
|
+
return self.get_function_metadata(func_node.name)
|
26
|
+
|
27
|
+
def get_param_types(self, func_node: ast.FunctionDef) -> Dict[str, str] | None:
|
28
|
+
func_metadata = self.get_function_metadata_by_node(func_node)
|
29
|
+
if func_metadata:
|
30
|
+
return func_metadata.params
|
31
|
+
|
32
|
+
return None
|
33
|
+
|
34
|
+
@property
|
35
|
+
def analysis_context(self) -> AnalysisContext:
|
36
|
+
return self._analysis_context
|
37
|
+
|
38
|
+
@analysis_context.setter
|
39
|
+
def analysis_context(self, context: AnalysisContext):
|
40
|
+
self._analysis_context = context
|
41
|
+
|
42
|
+
|
43
|
+
|
44
|
+
|
45
|
+
|
46
|
+
|
@@ -0,0 +1,58 @@
|
|
1
|
+
import time
|
2
|
+
from typing import List
|
3
|
+
|
4
|
+
from testgen.analyzer.test_case_analyzer import TestCaseAnalyzerStrategy
|
5
|
+
from testgen.models.test_case import TestCase
|
6
|
+
from testgen.models.analysis_context import AnalysisContext
|
7
|
+
|
8
|
+
|
9
|
+
class TestCaseAnalyzerContext:
|
10
|
+
def __init__(self, analysis_context: AnalysisContext, test_case_analyzer: TestCaseAnalyzerStrategy):
|
11
|
+
self._test_case_analyzer = test_case_analyzer
|
12
|
+
self._analysis_context = analysis_context
|
13
|
+
self._test_cases = []
|
14
|
+
|
15
|
+
# TODO: GET RID OF THIS STUPID METHOD IT IS POINTLESS
|
16
|
+
# JUST CALL INSIDE ANALYZER_SERVICE
|
17
|
+
def do_logic(self) -> List[TestCase]:
|
18
|
+
"""Run the analysis process"""
|
19
|
+
self.do_strategy(20)
|
20
|
+
|
21
|
+
def do_strategy(self, time_limit=None) -> List[TestCase]:
|
22
|
+
"""Execute the analysis strategy for all functions with an optional time limit"""
|
23
|
+
start_time = time.time()
|
24
|
+
for func_metadata in self._analysis_context.function_data:
|
25
|
+
if time_limit and (time.time() - start_time) >= time_limit:
|
26
|
+
break
|
27
|
+
test_cases = self._test_case_analyzer.collect_test_cases(func_metadata)
|
28
|
+
for test_case in test_cases:
|
29
|
+
print(f"Test Case: {test_case.func_name}, {test_case.inputs}, {test_case.expected}")
|
30
|
+
self._test_cases.extend(test_cases)
|
31
|
+
|
32
|
+
@property
|
33
|
+
def strategy(self) -> TestCaseAnalyzerStrategy:
|
34
|
+
return self._test_case_analyzer
|
35
|
+
|
36
|
+
@property
|
37
|
+
def test_cases(self):
|
38
|
+
return self._test_cases
|
39
|
+
|
40
|
+
@strategy.setter
|
41
|
+
def strategy(self, test_case_analyzer: TestCaseAnalyzerStrategy) -> None:
|
42
|
+
self._test_case_analyzer = test_case_analyzer
|
43
|
+
if self._analysis_context:
|
44
|
+
self._test_case_analyzer.analysis_context = self._analysis_context
|
45
|
+
|
46
|
+
@property
|
47
|
+
def analysis_context(self) -> AnalysisContext:
|
48
|
+
return self._analysis_context
|
49
|
+
|
50
|
+
@analysis_context.setter
|
51
|
+
def analysis_context(self, context: AnalysisContext) -> None:
|
52
|
+
self._analysis_context = context
|
53
|
+
if self._test_case_analyzer:
|
54
|
+
self._test_case_analyzer.analysis_context = context
|
55
|
+
|
56
|
+
|
57
|
+
|
58
|
+
|
File without changes
|
@@ -0,0 +1,194 @@
|
|
1
|
+
import argparse
|
2
|
+
import inspect
|
3
|
+
import os
|
4
|
+
import sys
|
5
|
+
|
6
|
+
import docker
|
7
|
+
from docker import DockerClient
|
8
|
+
from docker import errors
|
9
|
+
|
10
|
+
from testgen.controller.docker_controller import DockerController
|
11
|
+
from testgen.service.service import Service
|
12
|
+
from testgen.presentation.cli_view import CLIView
|
13
|
+
from testgen.sqlite.db_service import DBService
|
14
|
+
|
15
|
+
AST_STRAT = 1
|
16
|
+
FUZZ_STRAT = 2
|
17
|
+
RANDOM_STRAT = 3
|
18
|
+
REINFORCE_STRAT = 4
|
19
|
+
|
20
|
+
UNITTEST_FORMAT = 1
|
21
|
+
PYTEST_FORMAT = 2
|
22
|
+
DOCTEST_FORMAT = 3
|
23
|
+
|
24
|
+
class CLIController:
|
25
|
+
#TODO: Possibly create a view 'interface' and use dependency injection to extend other views
|
26
|
+
def __init__(self, service: Service, view: CLIView):
|
27
|
+
self.service = service
|
28
|
+
self.view = view
|
29
|
+
|
30
|
+
def run(self):
|
31
|
+
parser = argparse.ArgumentParser(description="A CLI tool for generating unit tests.")
|
32
|
+
parser.add_argument("file_path", type=str, help="Path to the Python file.")
|
33
|
+
parser.add_argument("--output", "-o", type=str, help="Path to output directory.")
|
34
|
+
parser.add_argument(
|
35
|
+
"--generate-only", "-g",
|
36
|
+
action="store_true",
|
37
|
+
help="Generate branched code but skip running unit tests and coverage."
|
38
|
+
)
|
39
|
+
parser.add_argument(
|
40
|
+
"--test-mode",
|
41
|
+
choices=["ast", "random", "fuzz", "reinforce"],
|
42
|
+
default="ast",
|
43
|
+
help="Set the test generation analysis technique"
|
44
|
+
)
|
45
|
+
parser.add_argument(
|
46
|
+
"--reinforce-mode",
|
47
|
+
choices=["train", "collect"],
|
48
|
+
default="train",
|
49
|
+
help="Set mode for reinforcement learning"
|
50
|
+
)
|
51
|
+
parser.add_argument(
|
52
|
+
"--test-format",
|
53
|
+
choices=["unittest", "pytest", "doctest"],
|
54
|
+
default="unittest",
|
55
|
+
help="Set the test generation format"
|
56
|
+
)
|
57
|
+
parser.add_argument(
|
58
|
+
"--safe",
|
59
|
+
action="store_true",
|
60
|
+
help="Run test generation from within a docker container."
|
61
|
+
)
|
62
|
+
parser.add_argument(
|
63
|
+
"--db",
|
64
|
+
type=str,
|
65
|
+
default="testgen.db",
|
66
|
+
help="Path to SQLite database file (default: testgen.db)"
|
67
|
+
)
|
68
|
+
parser.add_argument(
|
69
|
+
"--select-all",
|
70
|
+
action="store_true",
|
71
|
+
help="Select all from sqlite db"
|
72
|
+
)
|
73
|
+
parser.add_argument(
|
74
|
+
"--visualize",
|
75
|
+
action="store_true",
|
76
|
+
help = "Visualize the tests with graphviz"
|
77
|
+
)
|
78
|
+
|
79
|
+
args = parser.parse_args()
|
80
|
+
|
81
|
+
if args.select_all:
|
82
|
+
self.view.display_message("Selecting all from SQLite database...")
|
83
|
+
# Assuming you have a method in your service to handle this
|
84
|
+
self.service.select_all_from_db()
|
85
|
+
return
|
86
|
+
|
87
|
+
# Initialize database service with specified path
|
88
|
+
if hasattr(args, 'db') and args.db:
|
89
|
+
self.service.db_service = DBService(args.db)
|
90
|
+
self.view.display_message(f"Using database: {args.db}")
|
91
|
+
|
92
|
+
running_in_docker = os.environ.get("RUNNING_IN_DOCKER") is not None
|
93
|
+
if running_in_docker:
|
94
|
+
args.file_path = self.adjust_file_path_for_docker(args.file_path)
|
95
|
+
self.execute_generation(args)
|
96
|
+
elif args.safe and not running_in_docker:
|
97
|
+
client = self.docker_available()
|
98
|
+
# Skip Docker-dependent operations if client is None
|
99
|
+
if client is None and args.safe:
|
100
|
+
self.view.display_message("Running with --safe flag requires Docker. Continuing without safe mode.")
|
101
|
+
args.safe = False
|
102
|
+
docker_controller = DockerController()
|
103
|
+
project_root = self.get_project_root_in_docker(args.file_path)
|
104
|
+
successful: bool = docker_controller.run_in_docker(project_root, client, args)
|
105
|
+
if not successful:
|
106
|
+
self.execute_generation(args)
|
107
|
+
else:
|
108
|
+
self.view.display_message("Running in local mode...")
|
109
|
+
self.execute_generation(args)
|
110
|
+
|
111
|
+
def execute_generation(self, args: argparse.Namespace):
|
112
|
+
try:
|
113
|
+
self.service.set_file_path(args.file_path)
|
114
|
+
if args.test_format == "pytest":
|
115
|
+
self.service.set_test_generator_format(PYTEST_FORMAT)
|
116
|
+
elif args.test_format == "doctest":
|
117
|
+
self.service.set_test_generator_format(DOCTEST_FORMAT)
|
118
|
+
else:
|
119
|
+
self.service.set_test_generator_format(UNITTEST_FORMAT)
|
120
|
+
if args.test_mode == "random":
|
121
|
+
self.view.display_message("Using Random Feedback-Directed Test Generation Strategy.")
|
122
|
+
self.service.set_test_analysis_strategy(RANDOM_STRAT)
|
123
|
+
elif args.test_mode == "fuzz":
|
124
|
+
self.view.display_message("Using Fuzz Test Generation Strategy...")
|
125
|
+
self.service.set_test_analysis_strategy(FUZZ_STRAT)
|
126
|
+
elif args.test_mode == "reinforce":
|
127
|
+
self.view.display_message("Using Reinforcement Learning Test Generation Strategy...")
|
128
|
+
if args.reinforce_mode == "train":
|
129
|
+
self.view.display_message("Training mode enabled - will update Q-table")
|
130
|
+
else:
|
131
|
+
self.view.display_message("Training mode disabled - will use existing Q-table")
|
132
|
+
self.service.set_test_analysis_strategy(REINFORCE_STRAT)
|
133
|
+
self.service.set_reinforcement_mode(args.reinforce_mode)
|
134
|
+
else:
|
135
|
+
self.view.display_message("Generating function code using AST analysis...")
|
136
|
+
generated_file_path = self.service.generate_function_code()
|
137
|
+
self.view.display_message(f"Generated code saved to: {generated_file_path}")
|
138
|
+
if not args.generate_only:
|
139
|
+
self.view.display_message("Using Simple AST Traversal Test Generation Strategy...")
|
140
|
+
self.service.set_test_analysis_strategy(AST_STRAT)
|
141
|
+
|
142
|
+
test_file = self.service.generate_tests(args.output)
|
143
|
+
self.view.display_message(f"Unit tests saved to: {test_file}")
|
144
|
+
self.view.display_message("Running coverage...")
|
145
|
+
self.service.run_coverage(test_file)
|
146
|
+
self.view.display_message("Tests and coverage data saved to database.")
|
147
|
+
|
148
|
+
if args.visualize:
|
149
|
+
self.service.visualize_test_coverage()
|
150
|
+
|
151
|
+
except Exception as e:
|
152
|
+
self.view.display_error(f"An error occurred: {e}")
|
153
|
+
# Make sure to close the DB connection on error
|
154
|
+
if hasattr(self.service, 'db_service'):
|
155
|
+
self.service.db_service.close()
|
156
|
+
|
157
|
+
def adjust_file_path_for_docker(self, file_path) -> str:
|
158
|
+
file_dir = os.path.abspath(os.path.dirname(file_path))
|
159
|
+
sys.path.append(file_dir)
|
160
|
+
sys.path.append('/controller')
|
161
|
+
file_abs_path = os.path.abspath(file_path)
|
162
|
+
if not os.path.exists(file_abs_path):
|
163
|
+
testgen_path = os.path.join('/controller/testgen', os.path.basename(file_path))
|
164
|
+
if os.path.exists(testgen_path):
|
165
|
+
file_path = testgen_path
|
166
|
+
else:
|
167
|
+
app_path = os.path.join('/controller', os.path.basename(file_path))
|
168
|
+
if os.path.exists(app_path):
|
169
|
+
file_path = app_path
|
170
|
+
return file_path
|
171
|
+
|
172
|
+
def get_project_root_in_docker(self, script_path) -> str:
|
173
|
+
script_path = os.path.abspath(sys.argv[0])
|
174
|
+
print(f"Script path: {script_path}")
|
175
|
+
script_dir = os.path.dirname(script_path)
|
176
|
+
print(f"Script directory: {script_dir}")
|
177
|
+
project_root = os.path.dirname(script_dir)
|
178
|
+
print(f"Project root directory: {project_root}")
|
179
|
+
return project_root
|
180
|
+
|
181
|
+
def docker_available(self) -> DockerClient | None:
|
182
|
+
try:
|
183
|
+
client = docker.from_env()
|
184
|
+
client.ping()
|
185
|
+
self.view.display_message("Docker daemon is running and connected.")
|
186
|
+
return client
|
187
|
+
except docker.errors.DockerException as err:
|
188
|
+
print(f"Docker is not available: {err}")
|
189
|
+
print(f"Make sure the Docker daemon is running, and try again.")
|
190
|
+
choice = input("Continue without Docker (y/n)?")
|
191
|
+
if choice.lower() == 'y':
|
192
|
+
return None
|
193
|
+
else:
|
194
|
+
sys.exit(1)
|
@@ -0,0 +1,169 @@
|
|
1
|
+
import os
|
2
|
+
import sys
|
3
|
+
from argparse import Namespace
|
4
|
+
import docker
|
5
|
+
from docker import DockerClient, client
|
6
|
+
from docker import errors
|
7
|
+
from docker.models.containers import Container
|
8
|
+
|
9
|
+
from testgen.service.service import Service
|
10
|
+
|
11
|
+
AST_STRAT = 1
|
12
|
+
FUZZ_STRAT = 2
|
13
|
+
RANDOM_STRAT = 3
|
14
|
+
|
15
|
+
UNITTEST_FORMAT = 1
|
16
|
+
PYTEST_FORMAT = 2
|
17
|
+
DOCTEST_FORMAT = 3
|
18
|
+
|
19
|
+
class DockerController:
|
20
|
+
def __init__(self):
|
21
|
+
self.service = Service()
|
22
|
+
self.args = None
|
23
|
+
|
24
|
+
def run_in_docker(self, project_root: str, docker_client: DockerClient, args: Namespace) -> bool:
|
25
|
+
self.args = args
|
26
|
+
os.environ["RUNNING_IN_DOCKER"] = "1"
|
27
|
+
|
28
|
+
# Check if Docker image exists, build it if not
|
29
|
+
image_name = "testgen-runner"
|
30
|
+
# If args.safe is set to false it means the image was not found and the system will try to run_locally
|
31
|
+
self.get_image(docker_client, image_name, project_root)
|
32
|
+
if not self.args.safe:
|
33
|
+
print("Docker image not found. Running locally...")
|
34
|
+
return False
|
35
|
+
|
36
|
+
docker_args = [os.path.basename(args.file_path)] + [arg for arg in sys.argv[2:] if arg != "--safe"]
|
37
|
+
|
38
|
+
# Run the container with the same arguments
|
39
|
+
try:
|
40
|
+
container = self.run_container(docker_client, image_name, docker_args, project_root)
|
41
|
+
|
42
|
+
# Stream the logs to the console
|
43
|
+
logs_output = self.get_logs(container)
|
44
|
+
print(logs_output)
|
45
|
+
|
46
|
+
try:
|
47
|
+
# Create the target directory if it doesn't exist
|
48
|
+
if args.output is None:
|
49
|
+
target_path = os.path.join(os.getcwd(), 'tests')
|
50
|
+
else:
|
51
|
+
target_path = args.output
|
52
|
+
os.makedirs(target_path, exist_ok=True)
|
53
|
+
|
54
|
+
print(f"SERVICE target path after logs: {target_path}")
|
55
|
+
|
56
|
+
test_cases = self.service.parse_test_cases_from_logs(logs_output)
|
57
|
+
|
58
|
+
print(f"Extracted {len(test_cases)} test cases from container.")
|
59
|
+
|
60
|
+
file_path = os.path.abspath(args.file_path)
|
61
|
+
print(f"Filepath in CLI CONTROLLER: {file_path}")
|
62
|
+
self.service.set_file_path(file_path)
|
63
|
+
|
64
|
+
if args.test_format == "pytest":
|
65
|
+
self.service.set_test_generator_format(PYTEST_FORMAT)
|
66
|
+
elif args.test_format == "doctest":
|
67
|
+
self.service.set_test_generator_format(DOCTEST_FORMAT)
|
68
|
+
else:
|
69
|
+
self.service.set_test_generator_format(UNITTEST_FORMAT)
|
70
|
+
|
71
|
+
test_file = self.service.generate_test_file(test_cases, target_path)
|
72
|
+
print(f"Unit tests saved to: {test_file}")
|
73
|
+
|
74
|
+
if not args.generate_only:
|
75
|
+
print("Running coverage...")
|
76
|
+
import traceback
|
77
|
+
print(traceback.format_exc())
|
78
|
+
self.service.run_coverage(test_file)
|
79
|
+
|
80
|
+
except Exception as e:
|
81
|
+
print(f"Error running container: {e}")
|
82
|
+
sys.exit(1)
|
83
|
+
|
84
|
+
except Exception as e:
|
85
|
+
print(f"Error running container: {e}")
|
86
|
+
sys.exit(1)
|
87
|
+
|
88
|
+
def get_image(self, docker_client: DockerClient, image_name: str, project_root: str):
|
89
|
+
try:
|
90
|
+
docker_client.images.get(image_name)
|
91
|
+
print(f"Using existing {image_name} Docker image")
|
92
|
+
except docker.errors.ImageNotFound:
|
93
|
+
print(f"Building {image_name} Docker image...")
|
94
|
+
|
95
|
+
# Look for Dockerfile in the project root
|
96
|
+
docker_dir = os.path.join(project_root, "testgen", "docker")
|
97
|
+
dockerfile_path = os.path.join(docker_dir, "Dockerfile")
|
98
|
+
if not os.path.exists(dockerfile_path):
|
99
|
+
print(f"Dockerfile not found at {dockerfile_path}")
|
100
|
+
sys.exit(1)
|
101
|
+
|
102
|
+
print(f"Using Dockerfile at: {dockerfile_path}")
|
103
|
+
|
104
|
+
if not self.build_docker_image(docker_client, image_name, dockerfile_path, project_root):
|
105
|
+
print("Failed to build Docker image. Continuing without safe mode.")
|
106
|
+
self.args.safe = False
|
107
|
+
|
108
|
+
@staticmethod
|
109
|
+
def get_logs(container) -> str:
|
110
|
+
# Stream the logs to the console
|
111
|
+
print("Running in Docker container...")
|
112
|
+
logs = container.logs(stream=True)
|
113
|
+
logs_output = ""
|
114
|
+
for log in logs:
|
115
|
+
log_line = log.decode()
|
116
|
+
logs_output += log_line
|
117
|
+
print(log_line, end="")
|
118
|
+
return logs_output
|
119
|
+
|
120
|
+
@staticmethod
|
121
|
+
def run_container(docker_client: DockerClient, image_name: str, docker_args: list, project_root: str) -> Container:
|
122
|
+
return docker_client.containers.run(
|
123
|
+
image=image_name,
|
124
|
+
command=["poetry", "run", "python", "-m", "testgen.main"] + docker_args,
|
125
|
+
volumes={project_root: {"bind": "/controller", "mode": "rw"}}, # Mount current dir
|
126
|
+
environment={"RUNNING_IN_DOCKER": "1"},
|
127
|
+
detach=True,
|
128
|
+
remove=True,
|
129
|
+
stdout=True,
|
130
|
+
stderr=True
|
131
|
+
)
|
132
|
+
|
133
|
+
@staticmethod
|
134
|
+
def build_docker_image(docker_client, image_name, dockerfile_path, project_root):
|
135
|
+
try:
|
136
|
+
print(f"Starting Docker build for image: {image_name}")
|
137
|
+
dockerfile_rel_path = os.path.relpath(dockerfile_path, project_root)
|
138
|
+
print(f"Project root {project_root}")
|
139
|
+
print(f"Docker directory: {os.path.dirname(dockerfile_path)}")
|
140
|
+
print(f"Docker rel path: {dockerfile_rel_path}")
|
141
|
+
build_progress = docker_client.api.build(
|
142
|
+
path=os.path.join(project_root, "testgen", "docker"),
|
143
|
+
dockerfile=os.path.join(project_root, "testgen", "docker", "Dockerfile"),
|
144
|
+
tag=image_name,
|
145
|
+
rm=True,
|
146
|
+
decode=True
|
147
|
+
)
|
148
|
+
|
149
|
+
for chunk in build_progress:
|
150
|
+
print(f"CHUNK: {chunk}")
|
151
|
+
if 'stream' in chunk:
|
152
|
+
for line in chunk['stream'].splitlines():
|
153
|
+
if line.strip():
|
154
|
+
print(f"Docker: {line.strip()}")
|
155
|
+
elif 'error' in chunk:
|
156
|
+
print(f"Docker build error: {chunk['error']}")
|
157
|
+
return False
|
158
|
+
print(f"Docker image built successfully: {image_name}")
|
159
|
+
return True
|
160
|
+
|
161
|
+
except docker.errors.BuildError as e:
|
162
|
+
print(f"Docker build error: {e}")
|
163
|
+
except docker.errors.APIError as e:
|
164
|
+
print(f"Docker API error: {e}")
|
165
|
+
except Exception as e:
|
166
|
+
print(f"Unexpected error during Docker build: {str(e)}")
|
167
|
+
return False
|
168
|
+
|
169
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
FROM python:3.10
|
2
|
+
|
3
|
+
RUN apt-get update && apt-get install -y curl build-essential
|
4
|
+
|
5
|
+
RUN curl -sSL https://install.python-poetry.org | python3 - \
|
6
|
+
&& ln -s /root/.local/bin/poetry /usr/local/bin/poetry
|
7
|
+
|
8
|
+
ENV POETRY_VIRTUALENVS_CREATE=false \
|
9
|
+
PYTHONUNBUFFERED=1 \
|
10
|
+
RUNNING_IN_DOCKER=true
|
11
|
+
|
12
|
+
WORKDIR /app
|
13
|
+
|
14
|
+
# Copy poetry files
|
15
|
+
COPY . .
|
16
|
+
|
17
|
+
ENV PYTHONPATH=/app:/app/testgen
|
18
|
+
|
19
|
+
RUN poetry install --no-root
|
20
|
+
|
21
|
+
# Entrypoint (Can be overridden by Docker SDK in Python)
|
22
|
+
CMD ["poetry", "run", "python", "-m", "testgen.main"]
|