mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-01-09 08:08:32 +00:00
- Ensure generated class names are always valid Python identifiers - Handle edge cases: names starting with numbers, special characters, keywords, built-ins - Add sanitization logic to remove invalid characters and prefix with 'Crew' when needed - Add comprehensive test coverage for class name validation edge cases - Addresses GitHub PR comment from lucasgomide about class name validity Fixes include: - Names starting with numbers: '123project' -> 'Crew123Project' - Python built-ins: 'True' -> 'TrueCrew', 'False' -> 'FalseCrew' - Special characters: 'hello@world' -> 'HelloWorld' - Empty/whitespace: ' ' -> 'DefaultCrew' - All generated class names pass isidentifier() and keyword checks Co-Authored-By: João <joao@crewai.com>
236 lines
8.3 KiB
Python
236 lines
8.3 KiB
Python
import os
|
|
import shutil
|
|
import tempfile
|
|
from pathlib import Path
|
|
from unittest import mock
|
|
|
|
import pytest
|
|
from click.testing import CliRunner
|
|
|
|
from crewai.cli.create_crew import create_crew, create_folder_structure
|
|
|
|
|
|
@pytest.fixture
|
|
def runner():
|
|
return CliRunner()
|
|
|
|
|
|
@pytest.fixture
|
|
def temp_dir():
|
|
temp_path = tempfile.mkdtemp()
|
|
yield temp_path
|
|
shutil.rmtree(temp_path)
|
|
|
|
|
|
def test_create_folder_structure_strips_single_trailing_slash():
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
os.chdir(temp_dir)
|
|
folder_path, folder_name, class_name = create_folder_structure("hello/")
|
|
|
|
assert folder_name == "hello"
|
|
assert class_name == "Hello"
|
|
assert folder_path.name == "hello"
|
|
assert folder_path.exists()
|
|
|
|
|
|
def test_create_folder_structure_strips_multiple_trailing_slashes():
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
os.chdir(temp_dir)
|
|
folder_path, folder_name, class_name = create_folder_structure("hello///")
|
|
|
|
assert folder_name == "hello"
|
|
assert class_name == "Hello"
|
|
assert folder_path.name == "hello"
|
|
assert folder_path.exists()
|
|
|
|
|
|
def test_create_folder_structure_handles_complex_name_with_trailing_slash():
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
os.chdir(temp_dir)
|
|
folder_path, folder_name, class_name = create_folder_structure("my-awesome_project/")
|
|
|
|
assert folder_name == "my_awesome_project"
|
|
assert class_name == "MyAwesomeProject"
|
|
assert folder_path.name == "my_awesome_project"
|
|
assert folder_path.exists()
|
|
|
|
|
|
def test_create_folder_structure_normal_name_unchanged():
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
os.chdir(temp_dir)
|
|
folder_path, folder_name, class_name = create_folder_structure("hello")
|
|
|
|
assert folder_name == "hello"
|
|
assert class_name == "Hello"
|
|
assert folder_path.name == "hello"
|
|
assert folder_path.exists()
|
|
|
|
|
|
|
|
|
|
|
|
def test_create_folder_structure_with_parent_folder():
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
os.chdir(temp_dir)
|
|
parent_path = Path(temp_dir) / "parent"
|
|
parent_path.mkdir()
|
|
|
|
folder_path, folder_name, class_name = create_folder_structure("child/", parent_folder=parent_path)
|
|
|
|
assert folder_name == "child"
|
|
assert class_name == "Child"
|
|
assert folder_path.name == "child"
|
|
assert folder_path.parent == parent_path
|
|
assert folder_path.exists()
|
|
|
|
|
|
@mock.patch("crewai.cli.create_crew.copy_template")
|
|
@mock.patch("crewai.cli.create_crew.write_env_file")
|
|
@mock.patch("crewai.cli.create_crew.load_env_vars")
|
|
def test_create_crew_with_trailing_slash_creates_valid_project(mock_load_env, mock_write_env, mock_copy_template, temp_dir):
|
|
mock_load_env.return_value = {}
|
|
|
|
with tempfile.TemporaryDirectory() as work_dir:
|
|
os.chdir(work_dir)
|
|
create_crew("test-project/", skip_provider=True)
|
|
|
|
project_path = Path(work_dir) / "test_project"
|
|
assert project_path.exists()
|
|
assert (project_path / "src" / "test_project").exists()
|
|
|
|
mock_copy_template.assert_called()
|
|
copy_calls = mock_copy_template.call_args_list
|
|
|
|
for call in copy_calls:
|
|
args = call[0]
|
|
if len(args) >= 5:
|
|
folder_name_arg = args[4]
|
|
assert not folder_name_arg.endswith("/"), f"folder_name should not end with slash: {folder_name_arg}"
|
|
|
|
|
|
@mock.patch("crewai.cli.create_crew.copy_template")
|
|
@mock.patch("crewai.cli.create_crew.write_env_file")
|
|
@mock.patch("crewai.cli.create_crew.load_env_vars")
|
|
def test_create_crew_with_multiple_trailing_slashes(mock_load_env, mock_write_env, mock_copy_template, temp_dir):
|
|
mock_load_env.return_value = {}
|
|
|
|
with tempfile.TemporaryDirectory() as work_dir:
|
|
os.chdir(work_dir)
|
|
create_crew("test-project///", skip_provider=True)
|
|
|
|
project_path = Path(work_dir) / "test_project"
|
|
assert project_path.exists()
|
|
assert (project_path / "src" / "test_project").exists()
|
|
|
|
|
|
@mock.patch("crewai.cli.create_crew.copy_template")
|
|
@mock.patch("crewai.cli.create_crew.write_env_file")
|
|
@mock.patch("crewai.cli.create_crew.load_env_vars")
|
|
def test_create_crew_normal_name_still_works(mock_load_env, mock_write_env, mock_copy_template, temp_dir):
|
|
mock_load_env.return_value = {}
|
|
|
|
with tempfile.TemporaryDirectory() as work_dir:
|
|
os.chdir(work_dir)
|
|
create_crew("normal-project", skip_provider=True)
|
|
|
|
project_path = Path(work_dir) / "normal_project"
|
|
assert project_path.exists()
|
|
assert (project_path / "src" / "normal_project").exists()
|
|
|
|
|
|
def test_create_folder_structure_handles_spaces_and_dashes_with_slash():
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
os.chdir(temp_dir)
|
|
folder_path, folder_name, class_name = create_folder_structure("My Cool-Project/")
|
|
|
|
assert folder_name == "my_cool_project"
|
|
assert class_name == "MyCoolProject"
|
|
assert folder_path.name == "my_cool_project"
|
|
assert folder_path.exists()
|
|
|
|
|
|
def test_create_folder_structure_handles_invalid_class_name_edge_cases():
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
os.chdir(temp_dir)
|
|
|
|
folder_path, folder_name, class_name = create_folder_structure("123project/")
|
|
assert folder_name == "123project"
|
|
assert class_name == "Crew123Project"
|
|
assert class_name.isidentifier()
|
|
assert folder_path.exists()
|
|
|
|
folder_path, folder_name, class_name = create_folder_structure("True/")
|
|
assert folder_name == "true"
|
|
assert class_name == "TrueCrew"
|
|
assert class_name.isidentifier()
|
|
assert folder_path.exists()
|
|
|
|
folder_path, folder_name, class_name = create_folder_structure(" /")
|
|
assert folder_name == "___" # Spaces become underscores in folder_name
|
|
assert class_name == "DefaultCrew" # But class_name should be DefaultCrew for whitespace-only input
|
|
assert class_name.isidentifier()
|
|
assert folder_path.exists()
|
|
|
|
folder_path, folder_name, class_name = create_folder_structure("hello@world/")
|
|
assert folder_name == "hello@world"
|
|
assert class_name == "HelloWorld"
|
|
assert class_name.isidentifier()
|
|
assert folder_path.exists()
|
|
|
|
|
|
def test_create_folder_structure_class_names_are_valid_python_identifiers():
|
|
import keyword
|
|
|
|
test_cases = [
|
|
"hello/",
|
|
"my-project/",
|
|
"123project/",
|
|
"class/",
|
|
"def/",
|
|
"import/",
|
|
"True/",
|
|
"False/",
|
|
"None/",
|
|
"hello.world/",
|
|
"hello@world/",
|
|
"hello#world/",
|
|
"hello$world/",
|
|
"///",
|
|
"",
|
|
" /",
|
|
]
|
|
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
os.chdir(temp_dir)
|
|
|
|
for i, test_case in enumerate(test_cases):
|
|
unique_name = f"{test_case.rstrip('/')}_test_{i}/"
|
|
if not test_case.strip('/'):
|
|
unique_name = f"empty_test_{i}/"
|
|
|
|
folder_path, folder_name, class_name = create_folder_structure(unique_name)
|
|
|
|
assert class_name.isidentifier(), f"Class name '{class_name}' from input '{unique_name}' is not a valid identifier"
|
|
assert not keyword.iskeyword(class_name), f"Class name '{class_name}' from input '{unique_name}' is a Python keyword"
|
|
assert class_name not in ('True', 'False', 'None'), f"Class name '{class_name}' from input '{unique_name}' is a Python built-in"
|
|
|
|
if folder_path.exists():
|
|
shutil.rmtree(folder_path)
|
|
|
|
|
|
@mock.patch("crewai.cli.create_crew.copy_template")
|
|
@mock.patch("crewai.cli.create_crew.write_env_file")
|
|
@mock.patch("crewai.cli.create_crew.load_env_vars")
|
|
def test_create_crew_with_parent_folder_and_trailing_slash(mock_load_env, mock_write_env, mock_copy_template, temp_dir):
|
|
mock_load_env.return_value = {}
|
|
|
|
with tempfile.TemporaryDirectory() as work_dir:
|
|
parent_path = Path(work_dir) / "parent"
|
|
parent_path.mkdir()
|
|
|
|
create_crew("child-crew/", skip_provider=True, parent_folder=parent_path)
|
|
|
|
crew_path = parent_path / "child_crew"
|
|
assert crew_path.exists()
|
|
assert not (crew_path / "src").exists()
|