debatefloor / tests /test_cli /test_init.py
AniketAsla's picture
sync: mirror git d05fcb5 to Space
b4ac377 verified
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.
"""Tests for the openenv init command."""
import os
from pathlib import Path
from openenv.cli.__main__ import app
from typer.testing import CliRunner
runner = CliRunner()
def _snake_to_pascal(snake_str: str) -> str:
"""Helper function matching the one in init.py"""
return "".join(word.capitalize() for word in snake_str.split("_"))
def test_init_creates_directory_structure(tmp_path: Path) -> None:
"""Test that init creates the correct directory structure."""
env_name = "test_env"
env_dir = tmp_path / env_name
old_cwd = os.getcwd()
try:
os.chdir(str(tmp_path))
result = runner.invoke(app, ["init", env_name], input="\n")
finally:
os.chdir(old_cwd)
assert result.exit_code == 0
assert env_dir.exists()
assert env_dir.is_dir()
# Check for required files
assert (env_dir / "__init__.py").exists()
assert (env_dir / "models.py").exists()
assert (env_dir / "client.py").exists()
assert (env_dir / "README.md").exists()
assert (env_dir / "openenv.yaml").exists()
assert (env_dir / "server").exists()
assert (env_dir / "server" / "__init__.py").exists()
assert (env_dir / "server" / "app.py").exists()
assert (env_dir / "server" / f"{env_name}_environment.py").exists()
assert (env_dir / "server" / "Dockerfile").exists()
assert (env_dir / "server" / "requirements.txt").exists()
def test_init_replaces_template_placeholders(tmp_path: Path) -> None:
"""Test that template placeholders are replaced correctly."""
env_name = "my_game_env"
env_dir = tmp_path / env_name
old_cwd = os.getcwd()
try:
os.chdir(str(tmp_path))
result = runner.invoke(app, ["init", env_name], input="\n")
finally:
os.chdir(old_cwd)
assert result.exit_code == 0
# Check models.py has correct class names
# For 'my_game_env', prefix is 'MyGame' (removes trailing '_env')
models_content = (env_dir / "models.py").read_text()
assert "MyGameAction" in models_content
assert "MyGameObservation" in models_content
assert "__ENV_NAME__" not in models_content
assert "__ENV_CLASS_NAME__" not in models_content
# Check client.py has correct class names
client_content = (env_dir / "client.py").read_text()
assert "MyGameEnv" in client_content
assert "MyGameAction" in client_content
assert "MyGameObservation" in client_content
assert "__ENV_NAME__" not in client_content
# Check __init__.py has correct exports
init_content = (env_dir / "__init__.py").read_text()
assert "MyGameAction" in init_content
assert "MyGameObservation" in init_content
assert "MyGameEnv" in init_content
# Check environment file has correct class name
env_file = env_dir / "server" / f"{env_name}_environment.py"
assert env_file.exists()
env_content = env_file.read_text()
assert "MyGameEnvironment" in env_content
assert "__ENV_CLASS_NAME__" not in env_content
def test_init_generates_openenv_yaml(tmp_path: Path) -> None:
"""Test that openenv.yaml is generated correctly."""
env_name = "test_env"
env_dir = tmp_path / env_name
old_cwd = os.getcwd()
try:
os.chdir(str(tmp_path))
result = runner.invoke(app, ["init", env_name], input="\n")
finally:
os.chdir(old_cwd)
assert result.exit_code == 0
yaml_file = env_dir / "openenv.yaml"
assert yaml_file.exists()
yaml_content = yaml_file.read_text()
assert f"name: {env_name}" in yaml_content
assert "type: space" in yaml_content
assert "runtime: fastapi" in yaml_content
assert "app: server.app:app" in yaml_content
assert "port: 8000" in yaml_content
assert "__ENV_NAME__" not in yaml_content
def test_init_readme_has_hf_frontmatter(tmp_path: Path) -> None:
"""Test that README has Hugging Face Space compatible frontmatter."""
env_name = "test_env"
env_dir = tmp_path / env_name
old_cwd = os.getcwd()
try:
os.chdir(str(tmp_path))
result = runner.invoke(app, ["init", env_name], input="\n")
finally:
os.chdir(old_cwd)
assert result.exit_code == 0
readme_file = env_dir / "README.md"
assert readme_file.exists()
readme_content = readme_file.read_text()
# Check for required HF Space frontmatter
assert "---" in readme_content
assert "title:" in readme_content
assert "sdk: docker" in readme_content
assert "app_port: 8000" in readme_content
assert "tags:" in readme_content
assert "- openenv" in readme_content
# Check that placeholders are replaced
assert "__ENV_NAME__" not in readme_content
assert "__ENV_TITLE_NAME__" not in readme_content
def test_init_validates_env_name(tmp_path: Path) -> None:
"""Test that invalid environment names are rejected."""
old_cwd = os.getcwd()
try:
os.chdir(str(tmp_path))
# Invalid: starts with number
result = runner.invoke(app, ["init", "123_env"], input="\n")
assert result.exit_code != 0
assert (
"not a valid python identifier" in result.output.lower()
or "not a valid identifier" in result.output.lower()
)
# Invalid: contains spaces
result = runner.invoke(app, ["init", "my env"], input="\n")
assert result.exit_code != 0
# Invalid: contains hyphens
result = runner.invoke(app, ["init", "my-env"], input="\n")
assert result.exit_code != 0
finally:
os.chdir(old_cwd)
def test_init_handles_existing_directory(tmp_path: Path) -> None:
"""Test that init fails gracefully when directory exists."""
env_name = "existing_env"
env_dir = tmp_path / env_name
env_dir.mkdir()
(env_dir / "some_file.txt").write_text("existing content")
old_cwd = os.getcwd()
try:
os.chdir(str(tmp_path))
result = runner.invoke(app, ["init", env_name], input="\n")
finally:
os.chdir(old_cwd)
assert result.exit_code != 0
assert (
"already exists" in result.output.lower()
or "not empty" in result.output.lower()
)
def test_init_handles_empty_directory(tmp_path: Path) -> None:
"""Test that init works when directory exists but is empty."""
env_name = "empty_env"
env_dir = tmp_path / env_name
env_dir.mkdir()
old_cwd = os.getcwd()
try:
os.chdir(str(tmp_path))
result = runner.invoke(app, ["init", env_name], input="\n")
finally:
os.chdir(old_cwd)
# Should work - empty directory is okay
assert result.exit_code == 0
assert (env_dir / "models.py").exists()
def test_init_with_output_dir(tmp_path: Path) -> None:
"""Test that init works with custom output directory."""
env_name = "output_env"
output_dir = tmp_path / "custom_output"
output_dir.mkdir()
env_dir = output_dir / env_name
result = runner.invoke(
app,
["init", env_name, "--output-dir", str(output_dir)],
input="\n",
)
assert result.exit_code == 0
assert env_dir.exists()
assert (env_dir / "models.py").exists()
def test_init_filename_templating(tmp_path: Path) -> None:
"""Test that filenames with placeholders are renamed correctly."""
env_name = "test_env"
env_dir = tmp_path / env_name
old_cwd = os.getcwd()
try:
os.chdir(str(tmp_path))
result = runner.invoke(app, ["init", env_name], input="\n")
finally:
os.chdir(old_cwd)
assert result.exit_code == 0
# Check that environment file is renamed correctly
env_file = env_dir / "server" / f"{env_name}_environment.py"
assert env_file.exists()
# Check that __ENV_NAME___environment.py doesn't exist (should be renamed)
template_name = env_dir / "server" / "__ENV_NAME___environment.py"
assert not template_name.exists()
def test_init_all_naming_conventions(tmp_path: Path) -> None:
"""Test that all naming conventions are replaced correctly."""
env_name = "complex_test_env"
env_dir = tmp_path / env_name
old_cwd = os.getcwd()
try:
os.chdir(str(tmp_path))
result = runner.invoke(app, ["init", env_name], input="\n")
finally:
os.chdir(old_cwd)
assert result.exit_code == 0
# Check PascalCase
# For 'complex_test_env', prefix is 'ComplexTest' (removes trailing '_env')
models_content = (env_dir / "models.py").read_text()
assert "ComplexTestAction" in models_content
assert "ComplexTestObservation" in models_content
# Check snake_case in imports
assert env_name in models_content # Should see snake_case module name
# Check Title Case in README
readme_content = (env_dir / "README.md").read_text()
assert (
"Complex Test Env" in readme_content
or env_name.lower() in readme_content.lower()
)
def test_init_server_app_imports(tmp_path: Path) -> None:
"""Test that server/app.py has correct imports after templating."""
env_name = "test_env"
env_dir = tmp_path / env_name
old_cwd = os.getcwd()
try:
os.chdir(str(tmp_path))
result = runner.invoke(app, ["init", env_name], input="\n")
finally:
os.chdir(old_cwd)
assert result.exit_code == 0
app_content = (env_dir / "server" / "app.py").read_text()
# Check imports use correct class names
# For 'test_env', prefix is 'Test' (removes trailing '_env')
# Template uses direct imports (PYTHONPATH includes env dir in Docker)
assert f"from .{env_name}_environment import" in app_content
assert "from models import" in app_content # Direct import for Docker compatibility
assert "TestEnvironment" in app_content # Prefix is 'Test', not 'TestEnv'
assert "TestAction" in app_content # Prefix is 'Test', not 'TestEnv'
assert "TestObservation" in app_content # Prefix is 'Test', not 'TestEnv'
# Check that no template placeholders remain
assert "__ENV_NAME__" not in app_content
assert "__ENV_CLASS_NAME__" not in app_content
def test_init_dockerfile_uses_correct_base(tmp_path: Path) -> None:
"""Test that Dockerfile uses correct base image and paths."""
env_name = "test_env"
env_dir = tmp_path / env_name
old_cwd = os.getcwd()
try:
os.chdir(str(tmp_path))
result = runner.invoke(app, ["init", env_name], input="\n")
finally:
os.chdir(old_cwd)
assert result.exit_code == 0
dockerfile = env_dir / "server" / "Dockerfile"
assert dockerfile.exists()
dockerfile_content = dockerfile.read_text()
# Check base image
assert "ghcr.io/meta-pytorch/openenv-base:latest" in dockerfile_content
# Check CMD uses correct module path (could be in list format or string format)
assert "server.app:app" in dockerfile_content
# Check that no template placeholders remain
assert "__ENV_NAME__" not in dockerfile_content
def test_init_requirements_file(tmp_path: Path) -> None:
"""Test that requirements.txt is generated correctly."""
env_name = "test_env"
env_dir = tmp_path / env_name
old_cwd = os.getcwd()
try:
os.chdir(str(tmp_path))
result = runner.invoke(app, ["init", env_name], input="\n")
finally:
os.chdir(old_cwd)
assert result.exit_code == 0
requirements = env_dir / "server" / "requirements.txt"
assert requirements.exists()
req_content = requirements.read_text()
assert "fastapi" in req_content
assert "uvicorn" in req_content
assert "openenv[core]>=0.2.0" in req_content
def test_init_validates_empty_env_name(tmp_path: Path) -> None:
"""Test that init validates empty environment name."""
old_cwd = os.getcwd()
try:
os.chdir(str(tmp_path))
result = runner.invoke(app, ["init", ""], input="\n")
finally:
os.chdir(old_cwd)
assert result.exit_code != 0
assert "cannot be empty" in result.output.lower()
def test_init_env_name_without_env_suffix(tmp_path: Path) -> None:
"""Test that init works with env names that don't end with _env."""
env_name = "mygame" # No _env suffix
env_dir = tmp_path / env_name
old_cwd = os.getcwd()
try:
os.chdir(str(tmp_path))
result = runner.invoke(app, ["init", env_name], input="\n")
finally:
os.chdir(old_cwd)
assert result.exit_code == 0
assert env_dir.exists()
# Check that prefix is correctly derived (should be "Mygame" for "mygame")
models_content = (env_dir / "models.py").read_text()
assert "MygameAction" in models_content or "Mygame" in models_content
def test_init_single_part_env_name(tmp_path: Path) -> None:
"""Test that init works with single-part env names."""
env_name = "game" # Single part, no underscores
env_dir = tmp_path / env_name
old_cwd = os.getcwd()
try:
os.chdir(str(tmp_path))
result = runner.invoke(app, ["init", env_name], input="\n")
finally:
os.chdir(old_cwd)
assert result.exit_code == 0
assert env_dir.exists()
def test_init_handles_file_path_collision(tmp_path: Path) -> None:
"""Test that init fails when path exists as a file."""
env_name = "existing_file"
file_path = tmp_path / env_name
file_path.write_text("existing file content")
old_cwd = os.getcwd()
try:
os.chdir(str(tmp_path))
result = runner.invoke(app, ["init", env_name], input="\n")
finally:
os.chdir(old_cwd)
# The command should fail with exit code 2 (typer bad parameter)
assert result.exit_code != 0, (
f"Expected command to fail, but it succeeded. Output: {result.output}"
)
# Check that it's a BadParameter error (exit code 2) and not just a usage error
# Typer formats BadParameter errors in the Error section
error_output = result.output.lower()
# The error message should mention the path or file, or at least indicate an error
# Exit code 2 indicates BadParameter, and "error" in output indicates it's an error
assert (
result.exit_code == 2 # BadParameter exit code
or "error" in error_output
or "exists" in error_output
or "file" in error_output
or str(file_path).lower() in error_output
or env_name.lower() in error_output
), (
f"Expected BadParameter error about file collision. Exit code: {result.exit_code}, Output: {result.output}"
)