Skip to content
Merged
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
30 changes: 30 additions & 0 deletions forklet/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,36 @@ def info(ctx, repository: str, ref: str):
sys.exit(1)


@cli.command()
def status():
"""Show current download status and progress"""

try:
app = ForkletCLI()
app.initialize_services()

# Check if there's an active download
progress = app.orchestrator.get_current_progress() if hasattr(app, 'orchestrator') else None

if progress is None:
click.echo("πŸ“Š No active downloads")
else:
click.echo("πŸ“Š Current Download Status:")
click.echo(f" πŸ“ Files: {progress.downloaded_files}/{progress.total_files}")
click.echo(f" πŸ“Š Progress: {progress.progress_percentage:.1f}%")
click.echo(f" πŸ’Ύ Downloaded: {progress.downloaded_bytes}/{progress.total_bytes} bytes")
if progress.current_file:
click.echo(f" πŸ“„ Current file: {progress.current_file}")
if progress.download_speed > 0:
click.echo(f" ⚑ Speed: {progress.download_speed:.2f} bytes/sec")
if progress.eta_seconds:
click.echo(f" ⏱️ ETA: {progress.eta_seconds:.0f} seconds")

except Exception as e:
click.echo(f"❌ Error: {e}", err=True)
sys.exit(1)


@cli.command()
def version():
"""Print Forklet version"""
Expand Down
70 changes: 55 additions & 15 deletions forklet/interfaces/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,30 +271,70 @@ async def get_rate_limit_info(self) -> Dict[str, Any]:

return await self.github_service.get_rate_limit_info()

async def cancel_current_download(self) -> None:
"""Cancel the currently running download operation."""

await self.orchestrator.cancel()
def cancel_current_download(self) -> Optional[DownloadResult]:
"""
Cancel the currently running download operation.

Returns:
DownloadResult marked as cancelled, or None if no active download

Example:
>>> downloader = GitHubDownloader()
>>> # Start download in background...
>>> result = downloader.cancel_current_download()
>>> if result:
... print(f"Download cancelled: {result.status}")
"""
return self.orchestrator.cancel()

async def pause_current_download(self) -> None:
"""Pause the currently running download operation."""

await self.orchestrator.pause()
async def pause_current_download(self) -> Optional[DownloadResult]:
"""
Pause the currently running download operation.

Returns:
DownloadResult marked as paused, or None if no active download

Example:
>>> downloader = GitHubDownloader()
>>> # Start download in background...
>>> result = await downloader.pause_current_download()
>>> if result:
... print(f"Download paused: {result.status}")
"""
return await self.orchestrator.pause()

async def resume_current_download(self) -> None:
"""Resume a paused download operation."""

await self.orchestrator.resume()
async def resume_current_download(self) -> Optional[DownloadResult]:
"""
Resume a paused download operation.

Returns:
DownloadResult marked as resuming, or None if no paused download

Example:
>>> downloader = GitHubDownloader()
>>> # After pausing a download...
>>> result = await downloader.resume_current_download()
>>> if result:
... print(f"Download resumed: {result.status}")
"""
return await self.orchestrator.resume()

async def get_download_progress(self) -> Optional[ProgressInfo]:
def get_download_progress(self) -> Optional[ProgressInfo]:
"""
Get progress information for the current download.

Returns:
ProgressInfo object, or None if no download in progress

Example:
>>> downloader = GitHubDownloader()
>>> # During an active download...
>>> progress = downloader.get_download_progress()
>>> if progress:
... print(f"Progress: {progress.progress_percentage:.1f}%")
... print(f"Files: {progress.downloaded_files}/{progress.total_files}")
"""

return await self.orchestrator.get_current_progress()
return self.orchestrator.get_current_progress()

def set_verbose(self, verbose: bool) -> None:
"""
Expand Down
167 changes: 167 additions & 0 deletions tests/interfaces/test_download_control.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
"""
Unit tests for download control functionality in GitHubDownloader API.
"""

import pytest
from unittest.mock import Mock, AsyncMock, patch
from forklet.interfaces.api import GitHubDownloader
from forklet.models import DownloadResult, DownloadStatus, ProgressInfo


class TestDownloadControl:
"""Test cases for download control functionality."""

def test_cancel_current_download_success(self):
"""Test successful cancellation of current download."""
# Create downloader
downloader = GitHubDownloader()

# Mock the orchestrator's cancel method
mock_result = Mock(spec=DownloadResult)
mock_result.status = DownloadStatus.CANCELLED
downloader.orchestrator.cancel = Mock(return_value=mock_result)

# Call cancel
result = downloader.cancel_current_download()

# Verify
assert result == mock_result
assert result.status == DownloadStatus.CANCELLED
downloader.orchestrator.cancel.assert_called_once()

def test_cancel_current_download_no_active(self):
"""Test cancellation when no download is active."""
downloader = GitHubDownloader()
downloader.orchestrator.cancel = Mock(return_value=None)

result = downloader.cancel_current_download()

assert result is None
downloader.orchestrator.cancel.assert_called_once()

@pytest.mark.asyncio
async def test_pause_current_download_success(self):
"""Test successful pausing of current download."""
downloader = GitHubDownloader()

# Mock the orchestrator's pause method
mock_result = Mock(spec=DownloadResult)
mock_result.status = DownloadStatus.PAUSED
downloader.orchestrator.pause = AsyncMock(return_value=mock_result)

# Call pause
result = await downloader.pause_current_download()

# Verify
assert result == mock_result
assert result.status == DownloadStatus.PAUSED
downloader.orchestrator.pause.assert_called_once()

@pytest.mark.asyncio
async def test_pause_current_download_no_active(self):
"""Test pausing when no download is active."""
downloader = GitHubDownloader()
downloader.orchestrator.pause = AsyncMock(return_value=None)

result = await downloader.pause_current_download()

assert result is None
downloader.orchestrator.pause.assert_called_once()

@pytest.mark.asyncio
async def test_resume_current_download_success(self):
"""Test successful resuming of paused download."""
downloader = GitHubDownloader()

# Mock the orchestrator's resume method
mock_result = Mock(spec=DownloadResult)
mock_result.status = DownloadStatus.IN_PROGRESS
downloader.orchestrator.resume = AsyncMock(return_value=mock_result)

# Call resume
result = await downloader.resume_current_download()

# Verify
assert result == mock_result
assert result.status == DownloadStatus.IN_PROGRESS
downloader.orchestrator.resume.assert_called_once()

@pytest.mark.asyncio
async def test_resume_current_download_no_paused(self):
"""Test resuming when no download is paused."""
downloader = GitHubDownloader()
downloader.orchestrator.resume = AsyncMock(return_value=None)

result = await downloader.resume_current_download()

assert result is None
downloader.orchestrator.resume.assert_called_once()

def test_get_download_progress_with_active_download(self):
"""Test getting progress when download is active."""
downloader = GitHubDownloader()

# Mock progress info
mock_progress = Mock(spec=ProgressInfo)
mock_progress.total_files = 100
mock_progress.downloaded_files = 50
mock_progress.progress_percentage = 50.0

downloader.orchestrator.get_current_progress = Mock(return_value=mock_progress)

# Call get_progress
result = downloader.get_download_progress()

# Verify
assert result == mock_progress
assert result.progress_percentage == 50.0
downloader.orchestrator.get_current_progress.assert_called_once()

def test_get_download_progress_no_active_download(self):
"""Test getting progress when no download is active."""
downloader = GitHubDownloader()
downloader.orchestrator.get_current_progress = Mock(return_value=None)

result = downloader.get_download_progress()

assert result is None
downloader.orchestrator.get_current_progress.assert_called_once()

@pytest.mark.asyncio
async def test_download_control_workflow(self):
"""Test complete workflow: start -> pause -> resume -> cancel."""
downloader = GitHubDownloader()

# Mock orchestrator methods
paused_result = Mock(spec=DownloadResult)
paused_result.status = DownloadStatus.PAUSED

resumed_result = Mock(spec=DownloadResult)
resumed_result.status = DownloadStatus.IN_PROGRESS

cancelled_result = Mock(spec=DownloadResult)
cancelled_result.status = DownloadStatus.CANCELLED

downloader.orchestrator.pause = AsyncMock(return_value=paused_result)
downloader.orchestrator.resume = AsyncMock(return_value=resumed_result)
downloader.orchestrator.cancel = Mock(return_value=cancelled_result)

# Test workflow
# 1. Pause
result = await downloader.pause_current_download()
assert result.status == DownloadStatus.PAUSED

# 2. Resume
result = await downloader.resume_current_download()
assert result.status == DownloadStatus.IN_PROGRESS

# 3. Cancel
result = downloader.cancel_current_download()
assert result.status == DownloadStatus.CANCELLED

# Verify all methods were called
downloader.orchestrator.pause.assert_called_once()
downloader.orchestrator.resume.assert_called_once()
downloader.orchestrator.cancel.assert_called_once()