Skip to content
Draft
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
6 changes: 6 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ deployml = "deployml.cli.cli:main"
[tool.poetry]
packages = [{include = "deployml", from = "src"}]

[tool.poetry.group.dev.dependencies]
pytest = "^8.0"

[tool.pytest.ini_options]
testpaths = ["tests"]

[tool.setuptools]
include-package-data = true

Expand Down
Empty file added tests/__init__.py
Empty file.
1 change: 1 addition & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Pytest auto-discovers this file. Shared fixtures go here when we need them.
84 changes: 84 additions & 0 deletions tests/test_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""Unit tests for src/deployml/utils/helpers.py.

First batch — covers three patterns:
1. Pure functions (no mocks needed) → generate_bucket_name
2. Functions that call out to the OS → check_command (uses mocks)
3. Functions that parse text → estimate_terraform_time
"""

from unittest.mock import patch

from deployml.utils.helpers import (
check_command,
estimate_terraform_time,
generate_bucket_name,
)


# ---------------------------------------------------------------------------
# generate_bucket_name — pure function, no I/O, easiest possible test
# ---------------------------------------------------------------------------

def test_generate_bucket_name_basic():
# Arrange + Act
name = generate_bucket_name("my-project-123")
# Assert
assert name == "mlflow-artifacts-my-project-123"


def test_generate_bucket_name_replaces_underscores():
# GCS bucket names disallow underscores. The helper must convert them
# to hyphens — otherwise a project ID like "my_project" would produce
# an invalid bucket name and deploy would fail at apply time.
name = generate_bucket_name("my_project_123")
assert name == "mlflow-artifacts-my-project-123"
assert "_" not in name


# ---------------------------------------------------------------------------
# check_command — calls shutil.which under the hood. We mock it so the test
# doesn't depend on what's installed on the machine running the suite.
# ---------------------------------------------------------------------------

def test_check_command_returns_true_when_found():
with patch("deployml.utils.helpers.shutil.which", return_value="/usr/bin/terraform"):
assert check_command("terraform") is True


def test_check_command_returns_false_when_missing():
with patch("deployml.utils.helpers.shutil.which", return_value=None):
assert check_command("nonexistent-cmd") is False


# ---------------------------------------------------------------------------
# estimate_terraform_time — parses terraform plan output with regex.
# We feed it realistic snippets and assert on the returned string.
# ---------------------------------------------------------------------------

def test_estimate_terraform_time_detects_cloud_sql():
# Cloud SQL gets a special 20-min-per-instance estimate because
# provisioning a Postgres instance is the slowest part of a deploy.
plan_output = """
Terraform will perform the following actions:

# google_sql_database_instance.main will be created
+ resource "google_sql_database_instance" "main" {
name = "mlflow-db"
}
"""
result = estimate_terraform_time(plan_output)
assert "20 minutes" in result
assert "Cloud SQL" in result


def test_estimate_terraform_time_small_stack_without_cloud_sql():
# Two non-SQL resources → falls into the generic resource-count branch.
# 2 resources × 0.5 min avg = 1 minute (clamped by max(1, ...)).
plan_output = """
# google_cloud_run_service.mlflow will be created
# google_cloud_run_service.fastapi will be created
"""
result = estimate_terraform_time(plan_output)
assert result.startswith("~")
assert "minute" in result
assert "Cloud SQL" not in result
Loading