unit-tests-generator 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.
Potentially problematic release.
This version of unit-tests-generator might be problematic. Click here for more details.
- unit_tests_generator-0.1.0.dist-info/METADATA +236 -0
- unit_tests_generator-0.1.0.dist-info/RECORD +23 -0
- unit_tests_generator-0.1.0.dist-info/WHEEL +5 -0
- unit_tests_generator-0.1.0.dist-info/entry_points.txt +2 -0
- unit_tests_generator-0.1.0.dist-info/licenses/LICENSE +21 -0
- unit_tests_generator-0.1.0.dist-info/top_level.txt +1 -0
- utgen/__init__.py +0 -0
- utgen/config/__init__.py +0 -0
- utgen/config/params.py +1 -0
- utgen/constants.py +1 -0
- utgen/logger.py +87 -0
- utgen/main.py +43 -0
- utgen/pipeline.py +84 -0
- utgen/raggraph/__init__.py +0 -0
- utgen/raggraph/parser.py +235 -0
- utgen/raggraph/utils.py +201 -0
- utgen/raggraph/walker.py +70 -0
- utgen/test_generation_crew/__init__.py +0 -0
- utgen/test_generation_crew/crew.py +98 -0
- utgen/test_generation_crew/guardrails.py +99 -0
- utgen/test_generation_crew/llm_config.py +24 -0
- utgen/test_generation_crew/schemas.py +26 -0
- utgen/validation.py +82 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: unit-tests-generator
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Open source project to build a unit tests generator in Python.
|
|
5
|
+
Author: Arnau Fabregat
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Requires-Python: >=3.13
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: crewai[google-genai]>=1.10.1
|
|
17
|
+
Requires-Dist: litellm>=1.82.1
|
|
18
|
+
Requires-Dist: loguru>=0.7.3
|
|
19
|
+
Requires-Dist: networkx>=3.6.1
|
|
20
|
+
Requires-Dist: pytest>=9.0.2
|
|
21
|
+
Requires-Dist: pytest-cov>=7.0.0
|
|
22
|
+
Requires-Dist: typer>=0.23.1
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# 🛡️ Unit-Tests-Generator
|
|
26
|
+
*Autonomous, RAG-Driven Test Engineering for Python.*
|
|
27
|
+
|
|
28
|
+
**Unit-Tests-Generator** is an intelligent agentic tool that automates the entire `pytest` lifecycle. Unlike static template generators, it leverages an in-memory knowledge graph of your codebase and RAG to produce context-aware, executable, and validated test suites.
|
|
29
|
+
|
|
30
|
+
The system operates through a multi-stage pipeline:
|
|
31
|
+
1. **Codebase Graphing**: Maps your repository into an in-memory graph to understand cross-file dependencies and type definitions.
|
|
32
|
+
|
|
33
|
+
2. **Contextual RAG**: Retrieves relevant context for the LLM, ensuring the generated tests understand your project's unique patterns.
|
|
34
|
+
|
|
35
|
+
3. **Inference & Guardrails**: Generates test cases via LLM API, filtered through strict structural and security guardrails.
|
|
36
|
+
|
|
37
|
+
4. **The "Sandbox" Validation**: Automatically executes the generated tests.
|
|
38
|
+
|
|
39
|
+
5. **Persistence**: Only tests that pass execution and meet coverage criteria are committed to your `/tests` directory.
|
|
40
|
+
|
|
41
|
+
## Quick Start
|
|
42
|
+
```bash
|
|
43
|
+
# Install the package
|
|
44
|
+
pip install unit-tests-generator
|
|
45
|
+
|
|
46
|
+
# Generate tests using source and output paths
|
|
47
|
+
utgen -s src/ -t tests/
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Setting up your LLM
|
|
51
|
+
Required environment variables:
|
|
52
|
+
```bash
|
|
53
|
+
# .env file
|
|
54
|
+
LLM_OPENROUTER_MODEL = ""
|
|
55
|
+
LLM_OPENROUTER_API_KEY = ""
|
|
56
|
+
```
|
|
57
|
+
- Powered by CrewAI and OpenRouter: https://docs.crewai.com/en/concepts/llms#open-router.
|
|
58
|
+
- OpenRouter models: https://openrouter.ai/models.
|
|
59
|
+
|
|
60
|
+
## Table of Contents
|
|
61
|
+
1. [Usage](#usage)
|
|
62
|
+
2. [Examples](#examples)
|
|
63
|
+
3. [Graph structure](#graph-structure)
|
|
64
|
+
4. [Code Quality & Documentation](#code-quality--documentation)
|
|
65
|
+
- [Pre-commit Hooks](#pre-commit-hooks)
|
|
66
|
+
- [Unit Testing](#unit-testing)
|
|
67
|
+
5. [Virtual Environment](#virtual-environment)
|
|
68
|
+
- [Create a new virtualenv with the project's dependencies](#create-a-new-virtualenv-with-the-projects-dependencies)
|
|
69
|
+
- [Checking if the project's virtual environment is active](#checking-if-the-projects-virtual-environment-is-active)
|
|
70
|
+
- [Updating the project's dependencies](#updating-the-projects-dependencies)
|
|
71
|
+
6. [LICENSE](#license)
|
|
72
|
+
7. [TODO](#todo)
|
|
73
|
+
|
|
74
|
+
## Usage
|
|
75
|
+
`utgen` is designed to be simple and terminal-first. Once installed, you can generate tests by pointing the tool to your source code and specifying an output directory.
|
|
76
|
+
|
|
77
|
+
### CLI Arguments
|
|
78
|
+
| Flag | Shortcut | Description | Required | Default |
|
|
79
|
+
| :--- | :--- | :--- | :--- | :--- |
|
|
80
|
+
| `--src_path` | `-s` | Path to the directory containing your source code. | **Yes** | — |
|
|
81
|
+
| `--test_path` | `-t` | Path to the directory where generated tests will be saved. | **Yes** | — |
|
|
82
|
+
| `--graph_path` | `-g` | Path to the RAG-Graph data (enables advanced context). | No | `None` |
|
|
83
|
+
| `--help` | — | Show the help message and exit. | No | — |
|
|
84
|
+
|
|
85
|
+
### Basic Command
|
|
86
|
+
Run a standard generation without a RAG-Graph:
|
|
87
|
+
```bash
|
|
88
|
+
utgen -s src/ -t tests/
|
|
89
|
+
```
|
|
90
|
+
### Full Command
|
|
91
|
+
Enable RAG-Graph context for more accurate, context-aware test generation:
|
|
92
|
+
```bash
|
|
93
|
+
utgen --src_path src/ --test_path tests/ --graph_path data/
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Examples
|
|
97
|
+
Upload `repo.graphml` to https://lite.gephi.org/.
|
|
98
|
+
|
|
99
|
+
- This repository:
|
|
100
|
+
- Code coverage:
|
|
101
|
+
- Example for repository https://github.com/ArnauFabregat/probability_estimation
|
|
102
|
+

|
|
103
|
+
- Code coverage:
|
|
104
|
+
- Example for repository
|
|
105
|
+
- Code coverage
|
|
106
|
+
|
|
107
|
+
## Graph structure
|
|
108
|
+
To extract relevant context, the graph needs:
|
|
109
|
+
|
|
110
|
+
### 🟢 Nodes for:
|
|
111
|
+
- Files
|
|
112
|
+
- Classes
|
|
113
|
+
- Functions
|
|
114
|
+
- Methods
|
|
115
|
+
- Nested functions
|
|
116
|
+
|
|
117
|
+
Fields:
|
|
118
|
+
- **id**: canonical ID (e.g., file::...::class::Calibrator / ...::method::Calibrator.fit)
|
|
119
|
+
- **type**: "file" | "class" | "method" | "function" | "nested_function"
|
|
120
|
+
- **name**: symbol name ("Calibrator", "fit", etc.)
|
|
121
|
+
- **file**: file path
|
|
122
|
+
- **signature**: normalized function/method signature "def fit(self, probs, y) -> None"
|
|
123
|
+
- **docstring**: (trimmed) docstring
|
|
124
|
+
- **source**: actual source code of the node
|
|
125
|
+
|
|
126
|
+
### ➡️ Edges for:
|
|
127
|
+
- **defines** (file → symbol, function → nested function)
|
|
128
|
+
- **has_method** (class → method)
|
|
129
|
+
- **calls** (function/method → called symbol)
|
|
130
|
+
- **references** (function/method → referenced symbol)
|
|
131
|
+
|
|
132
|
+
Fields:
|
|
133
|
+
- **src**: source node id
|
|
134
|
+
- **dst**: destination node id
|
|
135
|
+
- **rel**: "defines" | "has_method" | "references" | "calls"
|
|
136
|
+
|
|
137
|
+
### 🧠 Prompt template
|
|
138
|
+
TBD
|
|
139
|
+
|
|
140
|
+
## Code Quality & Documentation
|
|
141
|
+
### Pre-commit Hooks
|
|
142
|
+
---
|
|
143
|
+
This project uses [pre-commit](https://pre-commit.com/) hooks to enforce code quality standards automatically before each commit. The following hooks are configured:
|
|
144
|
+
|
|
145
|
+
- **Formatting & File Integrity**: `trailing-whitespace`, `end-of-file-fixer`, `check-yaml`, `check-toml`
|
|
146
|
+
- **Code Linting & Formatting**: `ruff-check`, `ruff-format`
|
|
147
|
+
- **Type Checking**: `mypy`
|
|
148
|
+
|
|
149
|
+
Pre-commit hooks are automatically installed during virtual environment setup (`uv sync`).
|
|
150
|
+
- To run them for modified (staged) files:
|
|
151
|
+
```bash
|
|
152
|
+
uv run pre-commit run
|
|
153
|
+
```
|
|
154
|
+
- To run them for the entire repository:
|
|
155
|
+
```bash
|
|
156
|
+
uv run pre-commit run --all-files
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Unit Testing
|
|
160
|
+
---
|
|
161
|
+
Unit tests ensure code reliability and prevent regressions. Tests are written using pytest and should cover critical functionality.
|
|
162
|
+
|
|
163
|
+
To run all tests:
|
|
164
|
+
```bash
|
|
165
|
+
uv run pytest
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
To run tests with coverage:
|
|
169
|
+
```bash
|
|
170
|
+
uv run pytest --cov
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Virtual Environment
|
|
174
|
+
### Create a new virtualenv with the project's dependencies
|
|
175
|
+
---
|
|
176
|
+
Install the project's virtual environment and set it as your project's Python interpreter.
|
|
177
|
+
This will also install the project's current dependencies.
|
|
178
|
+
|
|
179
|
+
Open a terminal in VSCode, then execute the following commands:
|
|
180
|
+
|
|
181
|
+
1. Install [UV: Python package and project manager](https://docs.astral.sh/uv/getting-started/installation/):
|
|
182
|
+
* On Mac OSX / Linux: `curl -LsSf https://astral.sh/uv/install.sh | sh`
|
|
183
|
+
* On Windows [In Powershell]: `powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"`
|
|
184
|
+
|
|
185
|
+
2. [optional] To create virtual environment from scratch with `uv`: [Working on projects](https://docs.astral.sh/uv/guides/projects/)
|
|
186
|
+
|
|
187
|
+
3. If the environment already exists, install the virtual environment. It will be installed in the project's root, under a new directory named `.venv`:
|
|
188
|
+
* `uv sync`
|
|
189
|
+
* `uv sync --group dev --group dashboard --group test` to install all the dependency groups
|
|
190
|
+
|
|
191
|
+
4. Activate the new virtual environment:
|
|
192
|
+
* On Mac OSX / Linux: `source .venv/bin/activate`
|
|
193
|
+
* On Windows [In Powershell]: `.venv\Scripts\activate`
|
|
194
|
+
|
|
195
|
+
5. Configure / install pre-commit hooks:
|
|
196
|
+
* [Pre-commit](https://pre-commit.com/) is a tool that helps us keep the repository complying with certain formatting and style standards, using the `hooks` configured in the `.pre-commit-config.yaml` file.
|
|
197
|
+
* Previously installed with `uv sync`.
|
|
198
|
+
|
|
199
|
+
### Checking if the project's virtual environment is active
|
|
200
|
+
---
|
|
201
|
+
All commands listed here assume the project's virtual env is active.
|
|
202
|
+
|
|
203
|
+
To ensure so, execute the following command, and ensure it points to: `{project_root}/.venv/bin/python`:
|
|
204
|
+
* On Mac OSX / Linux: `which python`
|
|
205
|
+
* On Windows / Mac OSX / Linux: `python -c "import sys; import os; print(os.path.abspath(sys.executable))"`
|
|
206
|
+
|
|
207
|
+
If not active, execute the following to activate:
|
|
208
|
+
* On Mac OSX / Linux: `source .venv/bin/activate`
|
|
209
|
+
* On Windows [In Powershell]: `.venv\Scripts\activate`
|
|
210
|
+
|
|
211
|
+
Alternatively, you can also run any command using the prefix `uv run` and `uv` will make sure that it uses the virtual env's Python executable.
|
|
212
|
+
|
|
213
|
+
### Updating the project's dependencies
|
|
214
|
+
---
|
|
215
|
+
#### Adding new dependencies
|
|
216
|
+
---
|
|
217
|
+
In order to avoid potential version conflicts, we should use uv's dependency manager to add new libraries additional to the project's current dependenies.
|
|
218
|
+
Open a terminal in VSCode and execute the following commands:
|
|
219
|
+
|
|
220
|
+
* `uv add {dependency}` e.g. `uv add pandas`
|
|
221
|
+
|
|
222
|
+
This command will update the project's files `pyproject.toml` and `uv.lock` automatically, which are the ones ensuring all developers and environments have the exact same dependencies.
|
|
223
|
+
|
|
224
|
+
#### Updating your virtual env with dependencies recently added or removed from the project
|
|
225
|
+
---
|
|
226
|
+
Open a terminal in VSCode and execute the following command:
|
|
227
|
+
* `uv sync`
|
|
228
|
+
|
|
229
|
+
## License
|
|
230
|
+
MIT. See [LICENSE](LICENSE) for more information.
|
|
231
|
+
|
|
232
|
+
## TODO
|
|
233
|
+
- Study the possibility to run the tests in docker isolated environment for safety
|
|
234
|
+
- Add the remaining guardrails: not allow to write files from test_*.py
|
|
235
|
+
- Replace crewai by langgraph
|
|
236
|
+
- Publish to PyPI
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
unit_tests_generator-0.1.0.dist-info/licenses/LICENSE,sha256=cglefN9tjSeVCrm5HTQ-5lczPVR1D3r_IKT_NJ2R2vY,1070
|
|
2
|
+
utgen/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
utgen/constants.py,sha256=qz-bCgeXV3ArpW9QvOCVf2k5zuCONBNeHyk7pjQ5Ogw,26
|
|
4
|
+
utgen/logger.py,sha256=Xh1ZxJnwZUbqwcWYqD2gsf6bEaQcaHqq5jbZuQnQmXc,2605
|
|
5
|
+
utgen/main.py,sha256=uYr5Ae9aCEJUPth7ue_EFHKsq3UCzpMA28P3gvoPPos,1518
|
|
6
|
+
utgen/pipeline.py,sha256=3c8nHfQMKWxjBcpoawBGija8Yy47aFVXWUODF4vhkR4,3858
|
|
7
|
+
utgen/validation.py,sha256=pMLcCv8I1U25pIXo_JS1m_X4Ph0zI0KlcCg9EZ4iru8,2828
|
|
8
|
+
utgen/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
utgen/config/params.py,sha256=EacHophIxiT2KzEyAiWynsbecrDjqf3_8-wdoks5C6w,19
|
|
10
|
+
utgen/raggraph/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
utgen/raggraph/parser.py,sha256=eCczESMEevtzTJs2Kqev3az6SKhsjszyf2Cc7NJBuY8,8329
|
|
12
|
+
utgen/raggraph/utils.py,sha256=VwXqXMvC7EtZEdOwwJn0wHeQRDPICPX7wWgG2OM8Sjo,5854
|
|
13
|
+
utgen/raggraph/walker.py,sha256=a90EsYdGV8dFiKdpHp7T3cvVlbo7EarQ1X18xExgG8Y,2591
|
|
14
|
+
utgen/test_generation_crew/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
+
utgen/test_generation_crew/crew.py,sha256=YPtZIplumHMylc4KptBC0ZBUgzW-LZWAfwSJhQwxjqk,2992
|
|
16
|
+
utgen/test_generation_crew/guardrails.py,sha256=D94ZL3ocfJvEYxejCd8LYJOZmQB8wndWUd6EQj8Ube0,3805
|
|
17
|
+
utgen/test_generation_crew/llm_config.py,sha256=rsetwm8PCVljM4sQSF1e1s_KmFssrMSclwn6nMi4odk,749
|
|
18
|
+
utgen/test_generation_crew/schemas.py,sha256=oBK9sIdrPLTMPTMdTRy3HFzKsjlFHf2csFUhcclNGkQ,855
|
|
19
|
+
unit_tests_generator-0.1.0.dist-info/METADATA,sha256=lFMX3jZHMmZ5L9F9XKjXtKhQx7X4krMcATDiKT-EB6c,9196
|
|
20
|
+
unit_tests_generator-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
21
|
+
unit_tests_generator-0.1.0.dist-info/entry_points.txt,sha256=j0RKTVAFIdePsOTKG8oQF4Mit_bGqa0UySKvaiaK2L0,41
|
|
22
|
+
unit_tests_generator-0.1.0.dist-info/top_level.txt,sha256=WUMK5MkWCv5d6k8BpwWCgiF02sKTDBpb-JeHvFD-Wiw,6
|
|
23
|
+
unit_tests_generator-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ArnauFabregat
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
utgen
|
utgen/__init__.py
ADDED
|
File without changes
|
utgen/config/__init__.py
ADDED
|
File without changes
|
utgen/config/params.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
DEBUG_LOGS = False
|
utgen/constants.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
GUARDRAIL_MAX_RETRIES = 3
|
utgen/logger.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Central logger configuration for the application.
|
|
3
|
+
|
|
4
|
+
This module sets up Loguru-based logging, featuring split streams for
|
|
5
|
+
standard output and errors, log rotation, and the ability to silence
|
|
6
|
+
noisy third-party library logs.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
from loguru import logger
|
|
13
|
+
|
|
14
|
+
from utgen.config.params import DEBUG_LOGS
|
|
15
|
+
|
|
16
|
+
# List of library names (e.g., 'chromadb', 'httpx') to silence
|
|
17
|
+
DEPENDENCIES_WITH_LOGGING: list[str] = []
|
|
18
|
+
|
|
19
|
+
LOG_FORMAT = (
|
|
20
|
+
"<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | "
|
|
21
|
+
"<level>{level: <8}</level> | "
|
|
22
|
+
"<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - "
|
|
23
|
+
"<level>{message}</level>"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def disable_dependency_loggers(dependencies: list[str]) -> None:
|
|
28
|
+
"""
|
|
29
|
+
Disables logging for specific third-party dependencies.
|
|
30
|
+
|
|
31
|
+
Prevents external library logs from cluttering the application logs
|
|
32
|
+
by setting their 'disabled' flag to True and stopping propagation.
|
|
33
|
+
|
|
34
|
+
Parameters
|
|
35
|
+
----------
|
|
36
|
+
dependencies : list[str]
|
|
37
|
+
A list of library names as strings to be disabled.
|
|
38
|
+
"""
|
|
39
|
+
for name in dependencies:
|
|
40
|
+
logging.getLogger(name).disabled = True
|
|
41
|
+
logging.getLogger(name).propagate = False
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def setup_logger(debug: bool | None = None) -> None:
|
|
45
|
+
"""
|
|
46
|
+
Configures and initializes the Loguru logger handlers.
|
|
47
|
+
|
|
48
|
+
Sets up a three-tier logging strategy:
|
|
49
|
+
1. Standard Output: Handles DEBUG/INFO levels (Severity < 30).
|
|
50
|
+
2. Standard Error: Handles WARNING and higher (Severity >= 30).
|
|
51
|
+
3. File: Persistent storage with rotation and compression.
|
|
52
|
+
|
|
53
|
+
Parameters
|
|
54
|
+
----------
|
|
55
|
+
debug : bool, optional
|
|
56
|
+
If True, the base log level is set to 'DEBUG'. If False, it
|
|
57
|
+
defaults to 'INFO'. If None, it uses the logic defined
|
|
58
|
+
within the function.
|
|
59
|
+
"""
|
|
60
|
+
# Remove default Loguru handler
|
|
61
|
+
logger.remove()
|
|
62
|
+
|
|
63
|
+
# Determine the "Floor" level (DEBUG or INFO)
|
|
64
|
+
base_level = "DEBUG" if debug else "INFO"
|
|
65
|
+
|
|
66
|
+
# 1. Console: Standard Output (DEBUG/INFO)
|
|
67
|
+
# Uses a filter to ensure higher severity levels don't duplicate to stdout
|
|
68
|
+
logger.add(
|
|
69
|
+
sys.stdout,
|
|
70
|
+
format=LOG_FORMAT,
|
|
71
|
+
level=base_level,
|
|
72
|
+
filter=lambda r: r["level"].no < 30, # Stop before WARNING
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# 2. Console: Standard Error (WARNING+)
|
|
76
|
+
# This ensures critical issues are highlighted in the error stream
|
|
77
|
+
logger.add(
|
|
78
|
+
sys.stderr,
|
|
79
|
+
format="\n" + LOG_FORMAT,
|
|
80
|
+
level="WARNING",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# --- INITIALIZATION ---
|
|
85
|
+
# Run once when module is imported to apply settings globally
|
|
86
|
+
disable_dependency_loggers(DEPENDENCIES_WITH_LOGGING)
|
|
87
|
+
setup_logger(debug=DEBUG_LOGS)
|
utgen/main.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from utgen.logger import logger
|
|
8
|
+
from utgen.pipeline import pipeline
|
|
9
|
+
|
|
10
|
+
app = typer.Typer(help="UTGen: Unit test generation with RAG-Graph and LLMs")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@app.callback(invoke_without_command=True)
|
|
14
|
+
def run(
|
|
15
|
+
src_path: Annotated[Path, typer.Option("--src_path", "-s", help="Path to source")],
|
|
16
|
+
test_path: Annotated[Path, typer.Option("--test_path", "-t", help="Path to tests")],
|
|
17
|
+
graph_path: Annotated[Path | None, typer.Option("--graph_path", "-g", help="Path to Graph")] = None,
|
|
18
|
+
) -> None:
|
|
19
|
+
"""
|
|
20
|
+
Generate unit tests based on your source code and RAG-Graph.
|
|
21
|
+
"""
|
|
22
|
+
logger.info("Initializing utgen...")
|
|
23
|
+
logger.info(f"Source: {src_path} | Tests: {test_path} | Graph: {graph_path}")
|
|
24
|
+
|
|
25
|
+
pipeline(
|
|
26
|
+
source_code_dir=str(src_path),
|
|
27
|
+
tests_output_dir=str(test_path),
|
|
28
|
+
save_graph_path=str(graph_path) if graph_path else "",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
logger.info("Running coverage report...")
|
|
32
|
+
# We run pytest-cov on the newly generated test directory
|
|
33
|
+
# --cov points to the source code we want to measure
|
|
34
|
+
try:
|
|
35
|
+
subprocess.run(["pytest", str(test_path), f"--cov={src_path}", "--cov-report=term-missing"], check=True)
|
|
36
|
+
except subprocess.CalledProcessError:
|
|
37
|
+
logger.warning("Some tests failed or coverage couldn't be calculated.")
|
|
38
|
+
except FileNotFoundError:
|
|
39
|
+
logger.error("`pytest` not found. Make sure it's installed in your environment.")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
if __name__ == "__main__":
|
|
43
|
+
app()
|
utgen/pipeline.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from utgen.logger import logger
|
|
6
|
+
from utgen.raggraph.utils import get_node_context
|
|
7
|
+
from utgen.raggraph.walker import build_graph_from_directory
|
|
8
|
+
from utgen.test_generation_crew.crew import TestGenerationCrew
|
|
9
|
+
from utgen.validation import save_and_clean_tests, validate_individual_test
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def pipeline(source_code_dir: str, tests_output_dir: str, save_graph_path: str = "") -> None:
|
|
13
|
+
"""
|
|
14
|
+
Main function to orchestrate the test generation process.
|
|
15
|
+
Args:
|
|
16
|
+
source_code_dir (str): Directory containing the source code to analyze.
|
|
17
|
+
tests_output_dir (str): Directory where generated tests will be saved.
|
|
18
|
+
save_graph_path (str): Path to save the graph representation.
|
|
19
|
+
"""
|
|
20
|
+
logger.info("Creating graph from source code...")
|
|
21
|
+
g = build_graph_from_directory(code_path=source_code_dir, save_graph_path=save_graph_path)
|
|
22
|
+
logger.info(f"Graph built with {g.number_of_nodes()} nodes and {g.number_of_edges()} edges.")
|
|
23
|
+
|
|
24
|
+
logger.info("Started test generation process...")
|
|
25
|
+
# Define defaultdict of dicts
|
|
26
|
+
tests_results: defaultdict[str, dict[str, dict]] = defaultdict(dict)
|
|
27
|
+
|
|
28
|
+
# TODO: afegir guardrails que falten
|
|
29
|
+
test_generator = TestGenerationCrew(guardrail_max_retries=5, verbose=False)
|
|
30
|
+
|
|
31
|
+
for node_id, data in list(g.nodes(data=True))[20:30]:
|
|
32
|
+
if data["type"] in ["function", "method"]:
|
|
33
|
+
logger.info(f"Generating tests for node: {node_id}")
|
|
34
|
+
try:
|
|
35
|
+
# Get context
|
|
36
|
+
context = get_node_context(g=g, node_id=node_id)
|
|
37
|
+
inputs = {"graph_context": context}
|
|
38
|
+
|
|
39
|
+
# Generate tests
|
|
40
|
+
response = test_generator.crew().kickoff(inputs=inputs)
|
|
41
|
+
|
|
42
|
+
# Convert string to dictionary
|
|
43
|
+
response_dict = json.loads(response.raw)
|
|
44
|
+
|
|
45
|
+
# Store results
|
|
46
|
+
p = Path(data["file"])
|
|
47
|
+
new_filename = f"test_{p.stem}{p.suffix}"
|
|
48
|
+
save_path = (p.parent / new_filename).as_posix()
|
|
49
|
+
tests_results[save_path][node_id] = response_dict["tests"]
|
|
50
|
+
|
|
51
|
+
except Exception:
|
|
52
|
+
# This catches guardrail retries exceeded, JSON parsing errors, etc.
|
|
53
|
+
logger.error(f"Failed to generate tests for {node_id} after max retries.")
|
|
54
|
+
# Use 'continue' to skip the rest of this iteration and move to the next node
|
|
55
|
+
continue
|
|
56
|
+
logger.info("Test generation process completed.")
|
|
57
|
+
|
|
58
|
+
logger.info("Validating and saving generated tests...")
|
|
59
|
+
for save_path, nodes in tests_results.items():
|
|
60
|
+
accepted_tests: list[tuple[str, str]] = []
|
|
61
|
+
for node_id, tests in nodes.items():
|
|
62
|
+
base_import = (
|
|
63
|
+
"from "
|
|
64
|
+
+ node_id.split("::")[0][:-3].replace("/", ".")
|
|
65
|
+
+ " import "
|
|
66
|
+
+ node_id.split("::")[-1].split(".")[0]
|
|
67
|
+
)
|
|
68
|
+
for name, values in tests.items():
|
|
69
|
+
imports, code = values["imports"], values["code"]
|
|
70
|
+
if base_import not in imports:
|
|
71
|
+
logger.debug(f"Added missing import `{base_import}` for test `{name}`.")
|
|
72
|
+
imports.append(base_import)
|
|
73
|
+
imports = "\n".join(imports)
|
|
74
|
+
|
|
75
|
+
if validate_individual_test(import_code=imports, test_code=code):
|
|
76
|
+
logger.debug(f"Test `{name}` accepted during validation.")
|
|
77
|
+
accepted_tests.append((imports, code))
|
|
78
|
+
else:
|
|
79
|
+
logger.debug(f"Test `{name}` rejected during validation.")
|
|
80
|
+
|
|
81
|
+
logger.debug(f"Saving cleaned tests for `{save_path}`...")
|
|
82
|
+
save_and_clean_tests(valid_tests=accepted_tests, destination=f"{tests_output_dir}/{save_path}")
|
|
83
|
+
logger.info("All tests validated and saved successfully.")
|
|
84
|
+
# TODO: run pytest coverage
|
|
File without changes
|