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
146 changes: 146 additions & 0 deletions dev/breeze/src/airflow_breeze/utils/environment_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
Comment on lines +1 to +5
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

This module also appears to be using CRLF line endings; .editorconfig specifies end_of_line = lf. Please convert the file to LF to align with repository conventions and avoid noisy diffs/tooling issues.

Copilot uses AI. Check for mistakes.
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""
Environment context detection helper for Breeze AI workflows.

This module provides utilities to detect the execution environment (WSL, Docker, etc.)
and recommend appropriate commands for AI-assisted Breeze workflows.
"""

from __future__ import annotations

import os
import subprocess
from dataclasses import dataclass


@dataclass
class EnvironmentContext:
"""Information about the current execution environment."""

is_wsl: bool
"""True if running under Windows Subsystem for Linux (WSL)."""

inside_container: bool
"""True if running inside a Docker container."""

docker_available: bool
"""True if Docker is available and accessible."""

recommended_command: str
"""Recommended command for the current environment (e.g., 'breeze shell' or 'pytest')."""


def _detect_wsl() -> bool:
"""
Detect whether we are running under Windows Subsystem for Linux (WSL).

Uses multiple detection methods:
1. Check WSL_DISTRO_NAME environment variable (WSL 2 specific)
2. Check /proc/version for Microsoft/WSL markers (WSL 1 & 2)
3. Check uname release for microsoft marker (WSL 2)

Returns:
True if running under WSL, False otherwise.
"""
# Method 1: Environment variable (WSL 2 specific)
if "WSL_DISTRO_NAME" in os.environ:
return True

Comment on lines +48 to +63
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

WSL detection in _detect_wsl() duplicates existing logic in airflow_breeze/commands/developer_commands.py:is_wsl() and uses a different implementation. To avoid divergence/bugs over time, consider reusing a shared helper (or moving WSL detection into a common utils module and importing it from both places).

Copilot uses AI. Check for mistakes.
# Method 2: Check /proc/version (WSL 1 & 2)
try:
with open("/proc/version", encoding="utf-8") as version_file:
version = version_file.read()
if "Microsoft" in version or "WSL" in version:
Comment on lines +67 to +68
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

/proc/version detection is currently case-sensitive ("Microsoft" / "WSL"). Elsewhere in Breeze (e.g. developer_commands.is_wsl) the content is normalized with .lower() before checking, which is more robust. Consider lowercasing the file contents and checking for "microsoft"/"wsl" to avoid false negatives.

Suggested change
version = version_file.read()
if "Microsoft" in version or "WSL" in version:
version = version_file.read().lower()
if "microsoft" in version or "wsl" in version:

Copilot uses AI. Check for mistakes.
return True
except OSError:
# /proc may not be available on non-Linux systems
pass

# Method 3: Check uname release (WSL 2)
try:
release = os.uname().release
if "microsoft" in release.lower():
return True
except (AttributeError, OSError):
pass

return False


def _is_docker_available() -> bool:
"""
Check if Docker is available and accessible.

Runs 'docker info' to verify Docker daemon is running.

Returns:
True if Docker is available, False otherwise.
"""
try:
result = subprocess.run(
["docker", "info"],
capture_output=True,
timeout=5,
check=False,
)
return result.returncode == 0
except (FileNotFoundError, subprocess.TimeoutExpired):
return False


def detect_environment_context() -> EnvironmentContext:
"""
Detect the current execution environment and return context information.

Detects:
- Whether running in WSL
- Whether running inside a Docker container
- Whether Docker is available
- Recommended command based on environment

Returns:
EnvironmentContext: Information about the current environment.

Example:
>>> context = detect_environment_context()
>>> print(f"Running in container: {context.inside_container}")
>>> print(f"Recommended: {context.recommended_command}")
"""
# Check if inside Docker container
inside_container = os.path.exists("/.dockerenv")

# Detect WSL
is_wsl_env = _detect_wsl()

# Check Docker availability
docker_available = _is_docker_available()
Comment on lines +130 to +131
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

detect_environment_context() always runs _is_docker_available() (which shells out to docker info with a 5s timeout) even when inside_container is True and the result doesn't affect recommended_command. Consider skipping the Docker availability check when running inside the container, or making the availability probe opt-in, to keep context detection fast and predictable.

Suggested change
# Check Docker availability
docker_available = _is_docker_available()
# Check Docker availability only when running outside the container.
docker_available = False if inside_container else _is_docker_available()

Copilot uses AI. Check for mistakes.

# Determine recommended command
if inside_container:
# Inside container, use pytest directly
recommended_command = "pytest"
else:
# Outside container, recommend entering Breeze
recommended_command = "breeze shell"

return EnvironmentContext(
is_wsl=is_wsl_env,
inside_container=inside_container,
docker_available=docker_available,
recommended_command=recommended_command,
)
226 changes: 226 additions & 0 deletions dev/breeze/tests/utils/test_environment_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
Comment on lines +1 to +5
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

This file appears to be using CRLF (Windows-style) line endings. The repository's .editorconfig requires end_of_line = lf, so please normalize the file to LF to match project conventions.

Copilot uses AI. Check for mistakes.
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""
Tests for environment context detection.

Tests the ability to detect execution environment (WSL, Docker, etc.)
and recommend appropriate commands for Breeze workflows.
"""

from __future__ import annotations

import subprocess
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

Comment on lines +27 to +31
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

Path and pytest are imported but unused in this test module, which will fail linting (unused imports). Remove the unused imports or use them as intended.

Suggested change
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from unittest.mock import MagicMock, patch

Copilot uses AI. Check for mistakes.
from airflow_breeze.utils.environment_context import (
EnvironmentContext,
_detect_wsl,
_is_docker_available,
detect_environment_context,
)


class TestDetectWSL:
"""Tests for WSL detection."""

def test_detect_wsl_via_env_var(self, monkeypatch):
"""Test WSL detection via WSL_DISTRO_NAME environment variable."""
monkeypatch.setenv("WSL_DISTRO_NAME", "Ubuntu")
assert _detect_wsl() is True

def test_detect_wsl_not_present_env_var(self, monkeypatch):
"""Test when WSL_DISTRO_NAME is not set but on non-WSL system."""
monkeypatch.delenv("WSL_DISTRO_NAME", raising=False)
# Mock all detection methods to return non-WSL values
with patch("builtins.open", side_effect=OSError): # /proc/version not available
Comment on lines +48 to +52
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

There are whitespace-only/trailing spaces on otherwise blank lines (e.g. the blank line after monkeypatch.delenv(...)). This violates the repo's trailing-whitespace checks and should be cleaned up.

Copilot uses AI. Check for mistakes.
with patch("os.uname") as mock_uname:
mock_uname.return_value.release = "5.10.16.3-generic" # Non-microsoft kernel
result = _detect_wsl()
assert result is False

def test_detect_wsl_via_proc_version(self, monkeypatch):
"""Test WSL detection via /proc/version."""
monkeypatch.delenv("WSL_DISTRO_NAME", raising=False)

with patch("builtins.open", create=True) as mock_open:
mock_open.return_value.__enter__.return_value.read.return_value = (
"Linux version 4.19.128-microsoft (Microsoft@microsoft.com) "
"(gcc version 8.3.0 (GCC)) #1 SMP..."
)
assert _detect_wsl() is True

def test_detect_wsl_via_uname_release(self, monkeypatch):
"""Test WSL detection via uname release."""
monkeypatch.delenv("WSL_DISTRO_NAME", raising=False)

with patch("builtins.open", side_effect=OSError):
with patch("os.uname") as mock_uname:
mock_uname.return_value.release = "4.19.128-microsoft-standard"
assert _detect_wsl() is True


class TestDockerAvailable:
"""Tests for Docker availability detection."""

def test_docker_available_success(self):
"""Test when Docker is available and running."""
with patch("subprocess.run") as mock_run:
mock_result = MagicMock()
mock_result.returncode = 0
mock_run.return_value = mock_result

Comment on lines +82 to +88
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

Tests create MagicMock() instances without a spec/autospec (e.g. for the subprocess.run result). Using spec/autospec (or a real subprocess.CompletedProcess) helps catch attribute typos and keeps mocks aligned with the real API.

Copilot uses AI. Check for mistakes.
assert _is_docker_available() is True
mock_run.assert_called_once_with(
["docker", "info"],
capture_output=True,
timeout=5,
check=False,
)

def test_docker_available_failure(self):
"""Test when Docker is not available."""
with patch("subprocess.run") as mock_run:
mock_result = MagicMock()
mock_result.returncode = 1
mock_run.return_value = mock_result

assert _is_docker_available() is False

def test_docker_not_installed(self):
"""Test when Docker is not installed."""
with patch("subprocess.run", side_effect=FileNotFoundError):
assert _is_docker_available() is False

def test_docker_timeout(self):
"""Test when Docker check times out."""
with patch("subprocess.run", side_effect=subprocess.TimeoutExpired("docker", 5)):
assert _is_docker_available() is False


class TestDetectEnvironmentContext:
"""Tests for the main environment context detection."""

def test_detect_inside_container(self, monkeypatch):
"""Test detection when running inside Docker container."""
monkeypatch.delenv("WSL_DISTRO_NAME", raising=False)

with patch("os.path.exists") as mock_exists:
# Mock /.dockerenv exists (inside container)
mock_exists.return_value = True

with patch("airflow_breeze.utils.environment_context._detect_wsl") as mock_wsl:
mock_wsl.return_value = False

with patch("airflow_breeze.utils.environment_context._is_docker_available") as mock_docker:
mock_docker.return_value = True

context = detect_environment_context()

assert context.inside_container is True
assert context.is_wsl is False
assert context.docker_available is True
assert context.recommended_command == "pytest"

def test_detect_host_linux(self, monkeypatch):
"""Test detection on host Linux system (not WSL)."""
monkeypatch.delenv("WSL_DISTRO_NAME", raising=False)

with patch("os.path.exists", return_value=False):
with patch("airflow_breeze.utils.environment_context._detect_wsl") as mock_wsl:
mock_wsl.return_value = False

with patch("airflow_breeze.utils.environment_context._is_docker_available") as mock_docker:
mock_docker.return_value = True

context = detect_environment_context()

assert context.inside_container is False
assert context.is_wsl is False
assert context.docker_available is True
assert context.recommended_command == "breeze shell"

def test_detect_host_wsl(self, monkeypatch):
"""Test detection on Windows Subsystem for Linux."""
monkeypatch.delenv("WSL_DISTRO_NAME", raising=False)

with patch("os.path.exists", return_value=False):
with patch("airflow_breeze.utils.environment_context._detect_wsl") as mock_wsl:
mock_wsl.return_value = True

with patch("airflow_breeze.utils.environment_context._is_docker_available") as mock_docker:
mock_docker.return_value = True

context = detect_environment_context()

assert context.inside_container is False
assert context.is_wsl is True
assert context.docker_available is True
assert context.recommended_command == "breeze shell"

def test_detect_host_docker_unavailable(self, monkeypatch):
"""Test detection when Docker is not available."""
monkeypatch.delenv("WSL_DISTRO_NAME", raising=False)

with patch("os.path.exists", return_value=False):
with patch("airflow_breeze.utils.environment_context._detect_wsl") as mock_wsl:
mock_wsl.return_value = False

with patch("airflow_breeze.utils.environment_context._is_docker_available") as mock_docker:
mock_docker.return_value = False

context = detect_environment_context()

assert context.inside_container is False
assert context.docker_available is False
# Should still recommend breeze shell (even if Docker not available)
assert context.recommended_command == "breeze shell"


class TestEnvironmentContextDataclass:
"""Tests for the EnvironmentContext dataclass."""

def test_environment_context_creation(self):
"""Test creating an EnvironmentContext."""
context = EnvironmentContext(
is_wsl=False,
inside_container=True,
docker_available=True,
recommended_command="pytest",
)

assert context.is_wsl is False
assert context.inside_container is True
assert context.docker_available is True
assert context.recommended_command == "pytest"

def test_environment_context_all_fields(self):
"""Test EnvironmentContext has all expected fields."""
context = EnvironmentContext(
is_wsl=True,
inside_container=False,
docker_available=True,
recommended_command="breeze shell",
)

# Verify all fields are accessible
assert hasattr(context, "is_wsl")
assert hasattr(context, "inside_container")
assert hasattr(context, "docker_available")
assert hasattr(context, "recommended_command")
Loading