Skip to content
Open
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
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,22 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e .
pip install -r requirements-dev.txt

- name: Compile all examples
run: |
for file in $(find examples -name "*.py"); do
echo "Compiling $file..."
python -m py_compile "$file" || exit 1
done
echo "All examples compiled successfully."

- name: Check snippet coverage
run: |
pip install python-dotenv
python scripts/check_snippet_coverage.py

- name: Lint and format check with Ruff
run: |
ruff check sinch/domains/numbers --statistics
Expand Down Expand Up @@ -98,3 +112,5 @@ jobs:
python -m behave tests/e2e/sms/features
python -m behave tests/e2e/conversation/features
python -m behave tests/e2e/number-lookup/features


Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

response = sinch_client.numbers.rent_any(
region_code="US",
type_="LOCAL",
number_type="LOCAL",
capabilities=["SMS", "VOICE"],
sms_configuration=sms_configuration
)
Expand Down
101 changes: 101 additions & 0 deletions scripts/check_snippet_coverage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"""
Validate that snippets have valid syntax, working imports, and correct SDK method names.
"""
Comment on lines +1 to +3
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"""
Validate that snippets have valid syntax, working imports, and correct SDK method names.
"""
"""
Validate that snippets have valid syntax, working imports, and reference existing SDK methods by executing them until the first outbound API call.
"""

import argparse
import os
import sys
from pathlib import Path
from unittest.mock import patch

for var in [
"SINCH_PROJECT_ID", "SINCH_KEY_ID", "SINCH_KEY_SECRET",
"SINCH_SMS_REGION", "SINCH_CONVERSATION_REGION",
"SINCH_PHONE_NUMBER", "SINCH_SERVICE_PLAN_ID",
]:
os.environ.setdefault(var, "test")


class SnippetValidationComplete(Exception):
"""Raised when snippet successfully reaches first API call."""


def validate_snippet(snippet_path: Path, quiet: bool = True) -> tuple[bool, str]:
"""Run snippet; success when it reaches the first API call."""
def mock_request(self, endpoint):
raise SnippetValidationComplete()

try:
with patch(
"sinch.core.adapters.requests_http_transport.HTTPTransportRequests.request",
mock_request,
):
with open(snippet_path) as f:
source = f.read()
if quiet:
with open(os.devnull, "w") as devnull:
old_stdout, old_stderr = sys.stdout, sys.stderr
sys.stdout, sys.stderr = devnull, devnull
try:
exec(source, {"__name__": "__main__"})
finally:
sys.stdout, sys.stderr = old_stdout, old_stderr
else:
exec(source, {"__name__": "__main__"})
return False, "Snippet ran without making API call"
except SnippetValidationComplete:
return True, ""
except ModuleNotFoundError as e:
return False, f"Broken import: {e}"
except ImportError as e:
return False, f"Broken import: {e}"
except AttributeError as e:
return False, f"Method/attribute does not exist: {e}"
except SyntaxError as e:
return False, f"Syntax error: {e}"
except Exception as e:
return False, f"{type(e).__name__}: {e}"


def main():
parser = argparse.ArgumentParser(
description="Validate snippets (imports, syntax, SDK method names)"
)
parser.add_argument("-q", "--quiet", action="store_true", help="Only print failures")
args = parser.parse_args()

root = Path(__file__).parent.parent
os.chdir(root)

snippets_dir = root / "examples" / "snippets"
if not snippets_dir.exists():
print("ERROR: examples/snippets directory not found")
return 1

snippet_files = list(snippets_dir.rglob("snippet.py"))
if not snippet_files:
print("ERROR: No snippet.py files found")
return 1

failed = []
for snippet_path in sorted(snippet_files):
rel_path = snippet_path.relative_to(root)
success, error = validate_snippet(snippet_path, quiet=args.quiet)
if success:
if not args.quiet:
print(f" OK {rel_path}")
else:
print(f" FAIL {rel_path}\n {error}")
failed.append((rel_path, error))

if failed:
print(f"\n{len(failed)} snippet(s) failed validation:")
for path, err in failed:
print(f" - {path}: {err}")
return 1

print(f"\nAll {len(snippet_files)} snippets validated successfully.")
return 0


if __name__ == "__main__":
sys.exit(main())
147 changes: 147 additions & 0 deletions tests/unit/test_check_snippet_coverage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import importlib.util
from pathlib import Path
from textwrap import dedent
import pytest

ROOT = Path(__file__).parent.parent.parent
_SPEC = importlib.util.spec_from_file_location(
"check_snippet_coverage",
ROOT / "scripts" / "check_snippet_coverage.py",
)
_CHECK_SNIPPET_MOD = importlib.util.module_from_spec(_SPEC)
_SPEC.loader.exec_module(_CHECK_SNIPPET_MOD)
validate_snippet = _CHECK_SNIPPET_MOD.validate_snippet


@pytest.fixture
def temp_snippet_dir(tmp_path):
"""Temporary directory for snippet files."""
return tmp_path


def test_nonexistent_module_import_expects_failure_with_broken_import_message(
temp_snippet_dir,
):
"""Test that importing a nonexistent module returns failure with broken import message."""
path = temp_snippet_dir / "snippet.py"
path.write_text("from nonexistent_module_xyz import foo")

success, error = validate_snippet(path)

assert success is False
assert "Broken import" in error
assert "nonexistent_module_xyz" in error


def test_missing_name_import_from_sinch_expects_failure(temp_snippet_dir):
"""Test that importing a missing name from sinch returns failure."""
path = temp_snippet_dir / "snippet.py"
path.write_text("from sinch import NonExistentClass")

success, error = validate_snippet(path)

assert success is False
assert "Broken import" in error or "ImportError" in error


def test_nonexistent_sdk_method_expects_attribute_error(temp_snippet_dir):
"""Test that calling a nonexistent SDK method returns failure with attribute error."""
snippet = """
from sinch import SinchClient

sinch_client = SinchClient(
project_id="my-project-id",
key_id="my-key-id",
key_secret="my-key-secret",
sms_region="us",
)
sinch_client.sms.batches.send_nonexistent_method(
to=["+1"], from_="+1", body="hi"
)
"""
path = temp_snippet_dir / "snippet.py"
path.write_text(dedent(snippet))

success, error = validate_snippet(path)

assert success is False
assert "Method/attribute does not exist" in error
assert "send_nonexistent_method" in error


def test_invalid_syntax_expects_syntax_error(temp_snippet_dir):
"""Test that invalid Python syntax returns failure with syntax error."""
path = temp_snippet_dir / "snippet.py"
path.write_text("def foo()\n return 42")

success, error = validate_snippet(path)

assert success is False
assert "Syntax error" in error


def test_snippet_without_api_call_expects_failure(temp_snippet_dir):
"""Test that a snippet that does not make an API call returns failure."""
snippet = """
from sinch import SinchClient

sinch_client = SinchClient(
project_id="my-project-id",
key_id="my-key-id",
key_secret="my-key-secret",
sms_region="us",
)
print("no api call")
"""
path = temp_snippet_dir / "snippet.py"
path.write_text(dedent(snippet))

success, error = validate_snippet(path)

assert success is False
assert "without making API call" in error


def test_invalid__args_expects_failure(temp_snippet_dir):
"""Test that invalid arguments return failure (TypeError or similar)."""
snippet = """
from sinch import SinchClient

sinch_client = SinchClient(
project_id="my-project-id",
key_id="my-key-id",
key_secret="my-key-secret",
sms_region="us",
)
sinch_client.sms.batches.send_sms(
to="not_a_list", from_="+1", body="hi"
)
"""
path = temp_snippet_dir / "snippet.py"
path.write_text(dedent(snippet))

success, error = validate_snippet(path)

assert success is False
assert "TypeError" in error or "AttributeError" in error or len(error) > 0


def test_valid_snippet_expects_success(temp_snippet_dir):
"""Test that a valid snippet (inline string) passes validation."""
snippet = """
from sinch import SinchClient

sinch_client = SinchClient(
project_id="my-project-id",
key_id="my-key-id",
key_secret="my-key-secret",
sms_region="us",
)
sinch_client.sms.batches.send_sms(to=["+1"], from_="+1", body="hi")
"""
path = temp_snippet_dir / "snippet.py"
path.write_text(dedent(snippet))

success, error = validate_snippet(path)

assert success is True, f"Snippet failed: {error}"