Files
crewAI/tests/cli/test_create_crew.py
Devin AI dc25c32ca0 feat: add comprehensive class name validation for Python identifiers
- 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>
2025-06-24 19:17:52 +00:00

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()