Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tester #4

Merged
merged 9 commits into from
Feb 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/qc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [ "3.8", "3.11" ]
python-version: [ "3.9", "3.11" ]

steps:
- uses: actions/[email protected]
Expand All @@ -33,4 +33,5 @@ jobs:
run: poetry run tox -e lint

- name: Test with pytest and generate coverage file
run: poetry run tox -e py
run: poetry run pytest --ignore=tests/test_tester.py
# Changed from tox to pytest to make --ignore work.
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,50 @@ code [OPTIONS] COMMAND [ARGS]...
"""
```

5. `write-tests`: Writes tests for the specified code file. The user can specify a function and/or a class within the file to target with the tests.

```shell
code write-tests <path> [--function <function_name>] [--class <classname>]
```

#### Example
- Let's consider a python file `example.py`:
```python
# example.py

def add(a, b):
return a + b

class Calculator:
def subtract(self, a, b):
return a - b
```
```shell
$ code write-tests example.py --function add --class Calculator
```
results in test files being generated that contain test cases for the `add` function and the `Calculator` class. The actual content of the test files will depend on the implementation of the `coder.test_writer` method but would typically look something like this:

```python
import unittest
from example import add, Calculator

class TestAddFunction(unittest.TestCase):

def test_addition(self):
self.assertEqual(add(3, 4), 7)

class TestCalculator(unittest.TestCase):

def setUp(self):
self.calc = Calculator()

def test_subtract(self):
self.assertEqual(self.calc.subtract(10, 5), 5)
```

In this example, running the command generates unit tests for both the `add` function and the `Calculator` class in the `example.py` file. The tests check if the `add` function correctly adds two numbers and if the `Calculator`'s `subtract` method correctly subtracts one number from another.


## Development

The CLI is built using Python and the `click` library. Below is an example of how to define a new command:
Expand Down
46 changes: 46 additions & 0 deletions docs/description.rst
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,52 @@ Commands
By using these optimizations, we improve the efficiency and readability of the code.
"""

5s. **write-tests**: Generates test cases for specified functions and/or classes within a Python code file.

.. code-block:: shell

code write-tests <path> [--function <function_name>] [--class <classname>]

**Example**

- Let's consider a Python file `example.py`:

.. code-block:: python

# example.py

def add(a, b):
return a + b

class Calculator:
def subtract(self, a, b):
return a - b

.. code-block:: shell

$ code write-tests example.py --function add --class Calculator

results in the creation of test files that contain test cases for both the `add` function and the `Calculator` class. The content of the generated test files might look like this:

.. code-block:: python

import unittest
from example import add, Calculator

class TestAddFunction(unittest.TestCase):

def test_addition(self):
self.assertEqual(add(3, 4), 7)

class TestCalculator(unittest.TestCase):

def setUp(self):
self.calc = Calculator()

def test_subtract(self):
self.assertEqual(self.calc.subtract(10, 5), 5)

In this example, executing the command generates unit tests for the `add` function and the `Calculator` class defined in `example.py`. The tests verify whether the `add` function correctly computes the sum of two numbers and if the `Calculator`'s `subtract` method accurately performs subtraction.

Development
-----------
Expand Down
30 changes: 30 additions & 0 deletions docs/test.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
.. py:module:: codergpt

Test writing module
===================

.. py:class:: CodeTester(chain)

The CodeTester class is responsible for generating testing code from a given source file. It utilizes a llm chain to produce tests for specific functions or classes within the source file.

.. py:method:: __init__(chain)

Initializes the CodeTester instance with a provided llm chain.

:param chain: A RunnableSerializable object capable of executing tasks.
:type chain: RunnableSerializable[Dict, Any]

.. py:method:: write_tests(filename, function=None, classname=None, outfile=None)

Generates test cases for the specified code by invoking the llm chain. If a function or class name is provided, it will generate tests specifically for that function or class. Otherwise, it will attempt to create tests for the entire code.

:param filename: The path to the code file for which tests are to be written.
:type filename: Union[str, Path]
:param function: The name of the function for which tests should be generated. Defaults to None, indicating that no specific function is targeted.
:type function: Optional[str]
:param classname: The name of the class for which tests should be generated. Defaults to None, indicating that no specific class is targeted.
:type classname: Optional[str]
:param outfile: The path where the generated test file should be saved. If not provided, a default path within the TEST_DIR will be used.
:type outfile: Optional[str]

The method reads the source code from the provided filename and uses the llm chain to generate appropriate test cases. The resulting test code is then written to either the specified outfile or a new file within the TEST_DIR directory.
1 change: 1 addition & 0 deletions src/codergpt/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from codergpt.explainer import CodeExplainer
from codergpt.optimizer import CodeOptimizer

# from codergpt.tester import CodeTester
from .main import CoderGPT

try:
Expand Down
23 changes: 23 additions & 0 deletions src/codergpt/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,5 +146,28 @@ def optimize_code(path: Union[str, Path], function: str, classname: str, overwri
raise ValueError("The path provided is not a file.")


@main.command("write-tests")
@path_argument
@function_option
@class_option
def write_test_code(path: Union[str, Path], function: str, classname: str):
"""
Write tests for the code file.

:param path: The path to the code file.
:param function: The name of the function to test. Default is None.
:param classname: The name of the class to test. Default is None.
"""
# Ensure path is a string or Path object for consistency
if isinstance(path, str):
path = Path(path)

# Check if path is a file
if path.is_file():
coder.test_writer(path=path, function=function, classname=classname)
else:
raise ValueError("The path provided is not a file.")


if __name__ == "__main__":
main()
1 change: 1 addition & 0 deletions src/codergpt/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from pathlib import Path

TEST_DIR = Path(__file__).resolve().parents[2] / "tests"
SRC = Path(__file__).resolve().parents[1]
PACKAGE_DIR = SRC / "codergpt"
EXTENSION_MAP_FILE = PACKAGE_DIR / "extensions.yaml"
Expand Down
10 changes: 6 additions & 4 deletions src/codergpt/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,16 @@
from tabulate import tabulate

from codergpt.commenter.commenter import CodeCommenter
from codergpt.constants import EXTENSION_MAP_FILE, GPT_3_5_TURBO, INSPECTION_HEADERS
from codergpt.constants import EXTENSION_MAP_FILE, GPT_4_TURBO, INSPECTION_HEADERS
from codergpt.explainer.explainer import CodeExplainer
from codergpt.optimizer.optimizer import CodeOptimizer
from codergpt.test_writer.test_writer import CodeTester


class CoderGPT:
"""CoderGPT class."""

def __init__(self, model: str = GPT_3_5_TURBO):
def __init__(self, model: str = GPT_4_TURBO):
"""Initialize the CoderGPT class."""
self.llm = ChatOpenAI(openai_api_key=os.environ.get("OPENAI_API_KEY"), temperature=0.7, model=model)
self.prompt = ChatPromptTemplate.from_messages(
Expand Down Expand Up @@ -126,13 +127,14 @@ def optimizer(self, path: Union[str, Path], function: str = None, classname=None
# code, language = self.get_code(filename=path, function_name=function, class_name=classname)
code_optimizer.optimize(filename=path, function=function, classname=classname, overwrite=overwrite)

def tester(self, path: Union[str, Path]):
def test_writer(self, path: Union[str, Path], function: str = None, classname: str = None):
"""
Test the code file.

:param path: The path to the code file.
"""
pass
code_tester = CodeTester(self.chain)
code_tester.write_tests(filename=path, function=function, classname=classname)


if __name__ == "__main__":
Expand Down
33 changes: 17 additions & 16 deletions src/codergpt/optimizer/optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,22 +33,23 @@ def optimize(
"""
with open(filename, "r") as source_file:
source_code = source_file.read()
if function:
response = self.chain.invoke(
{
"input": f"Optimize, comment and add sphinx docstrings"
f" to the function '{function}' in \n\n```\n{source_code}\n```"
"Also explain the optimization in a systematic way as a comment."
}
)
elif classname:
response = self.chain.invoke(
{
"input": f"Optimize, comment and add sphinx docstrings"
f" to the class '{classname}' in \n\n```\n{source_code}\n```"
"Also explain the optimization in a systematic way."
}
)
if function or classname:
if function:
response = self.chain.invoke(
{
"input": f"Optimize, comment and add sphinx docstrings"
f" to the function '{function}' in \n\n```\n{source_code}\n```"
"Also explain the optimization in a systematic way as a comment."
}
)
if classname:
response = self.chain.invoke(
{
"input": f"Optimize, comment and add sphinx docstrings"
f" to the class '{classname}' in \n\n```\n{source_code}\n```"
"Also explain the optimization in a systematic way."
}
)
else:
# Optimize full code
response = self.chain.invoke(
Expand Down
70 changes: 70 additions & 0 deletions src/codergpt/test_writer/test_writer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""Test writing module."""

import os
from pathlib import Path
from typing import Any, Dict, Optional, Union

from langchain_core.runnables.base import RunnableSerializable

from codergpt.constants import TEST_DIR


class CodeTester:
"""Code tester class writes testing code from a given file."""

def __init__(self, chain: RunnableSerializable[Dict, Any]):
"""
Initialize the CodeTester class with a runnable chain.

:param chain: A RunnableSerializable object capable of executing tasks.
"""
self.chain = chain

def write_tests(
self,
filename: Union[str, Path],
function: Optional[str] = None,
classname: Optional[str] = None,
outfile: Optional[str] = None,
):
"""
Write tests for the code by invoking the runnable chain.

:param path: The path to the code file to be explained.
:param function: The name of the function to explain. Default is None.
:param classname: The name of the class to explain. Default is None.
"""
with open(filename, "r") as source_file:
source_code = source_file.read()
if function or classname:
if function:
response = self.chain.invoke(
{
"input": f"Write tests for the function '{function}' in \n\n```\n{source_code}\n```"
"Return just the code block. Also explain the tests in a systematic way as a comment."
}
)
if classname:
response = self.chain.invoke(
{
"input": f"Write tests for the class '{classname}' in \n\n```\n{source_code}\n```"
"Also explain the tests in a systematic way."
}
)
else:
# Write tests for full code
response = self.chain.invoke(
{
"input": f"Write tests for the following code: \n\n```\n{source_code}\n```"
"Also explain the tests in a systematic way."
}
)
test_code = response.content
base_filename = os.path.basename(filename)
if outfile:
new_filepath = outfile
else:
new_filepath = f"{TEST_DIR}/test_{base_filename}"
# Write the test to the new file
with open(new_filepath, "w") as updated_file:
updated_file.write(test_code)
1 change: 0 additions & 1 deletion src/codergpt/test_writer/tester.py

This file was deleted.

35 changes: 35 additions & 0 deletions tests/input/math.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""Test python code."""

def calculate_sum(numbers):
"""
Calculate the sum of a list of numbers.

:param numbers: A list of numbers.
:type numbers: list[int]
:return: The sum of the numbers.
:rtype: int
"""
result = 0
for number in numbers:
result += number
return result


class MathOperations:
"""Class to perform mathematical operations."""

def multiply(self, a, b):
"""
Multiply two numbers.

:param a: The first number.
:type a: int
:param b: The second number.
:type b: int
:return: The product of the two numbers.
:rtype: int
"""
answer = 0
for i in range(b):
answer += a
return answer
Loading
Loading