Skip to content

Commit bfb2d42

Browse files
mldangeloclaude
andcommitted
feat: add WSL (Windows Subsystem for Linux) detection and support
Add comprehensive WSL detection and tailored installation guidance for users running promptfoo-python in Windows Subsystem for Linux. Detection: - Detects WSL via WSL_DISTRO_NAME and WSL_INTEROP environment variables - Checks /proc/version for Microsoft/WSL signatures - Detects WSL 1 via /mnt/c Windows filesystem mounts - Integrated into environment detection system Installation Guidance: - Warns users NOT to use Windows Node.js from WSL (path/performance issues) - Recommends installing Node.js within WSL using Linux package managers - Provides nvm as recommended alternative for version management - Includes WSL-specific performance tips (store files in ~/, not /mnt/c/) - Shows both WSL guidance and Linux distro-specific instructions Testing: - Added 3 WSL detection tests (env vars, interop, no detection) - Added 2 WSL instruction tests (basic, combined with Ubuntu) - Added WSL Ubuntu environment detection test - Updated all existing environment tests to mock WSL detection - Total: 102 tests passing (6 new WSL-related tests) This improves the developer experience for the large number of users who develop in WSL, providing them with WSL-aware best practices instead of generic Linux instructions that may lead to suboptimal configurations. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 7a3d83a commit bfb2d42

5 files changed

Lines changed: 151 additions & 1 deletion

File tree

src/promptfoo/environment.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class Environment:
2525
is_cloud_function: bool = False # GCP Cloud Functions or Azure Functions
2626
is_docker: bool = False
2727
is_kubernetes: bool = False
28+
is_wsl: bool = False # Windows Subsystem for Linux
2829
is_ci: bool = False
2930
ci_platform: Optional[str] = None # "github", "gitlab", "circleci", "jenkins", etc.
3031
is_venv: bool = False
@@ -171,6 +172,32 @@ def _detect_container() -> tuple[bool, bool]:
171172
return is_docker, is_kubernetes
172173

173174

175+
def _detect_wsl() -> bool:
176+
"""
177+
Detect if running in Windows Subsystem for Linux (WSL).
178+
179+
Returns:
180+
True if running in WSL, False otherwise
181+
"""
182+
# Check for WSL environment variable
183+
if os.getenv("WSL_DISTRO_NAME") or os.getenv("WSL_INTEROP"):
184+
return True
185+
186+
# Check /proc/version for Microsoft/WSL signatures
187+
if Path("/proc/version").exists():
188+
try:
189+
with open("/proc/version") as f:
190+
version_info = f.read().lower()
191+
if "microsoft" in version_info or "wsl" in version_info:
192+
return True
193+
except OSError:
194+
pass
195+
196+
# Check for Windows filesystem mounts (WSL mounts Windows drives at /mnt/)
197+
# This is less reliable but can catch WSL 1
198+
return Path("/mnt/c").exists() and Path("/proc/version").exists()
199+
200+
174201
def _detect_ci() -> tuple[bool, Optional[str]]:
175202
"""
176203
Detect if running in a CI/CD environment.
@@ -284,6 +311,11 @@ def detect_environment() -> Environment:
284311
if os_type == "linux":
285312
is_docker, is_kubernetes = _detect_container()
286313

314+
# WSL detection
315+
is_wsl = False
316+
if os_type == "linux":
317+
is_wsl = _detect_wsl()
318+
287319
# CI detection
288320
is_ci, ci_platform = _detect_ci()
289321

@@ -302,6 +334,7 @@ def detect_environment() -> Environment:
302334
is_cloud_function=is_cloud_function,
303335
is_docker=is_docker,
304336
is_kubernetes=is_kubernetes,
337+
is_wsl=is_wsl,
305338
is_ci=is_ci,
306339
ci_platform=ci_platform,
307340
is_venv=is_venv,

src/promptfoo/instructions.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ def get_installation_instructions(env: Environment) -> str:
4242
lines.extend(_get_docker_instructions(env))
4343
lines.append("")
4444

45+
# WSL environment
46+
if env.is_wsl:
47+
lines.extend(_get_wsl_instructions())
48+
lines.append("")
49+
4550
# Platform-specific instructions
4651
if env.os_type == "linux":
4752
lines.extend(_get_linux_instructions(env))
@@ -182,6 +187,28 @@ def _get_docker_instructions(env: Environment) -> list[str]:
182187
return lines
183188

184189

190+
def _get_wsl_instructions() -> list[str]:
191+
"""Instructions for Windows Subsystem for Linux (WSL)."""
192+
return [
193+
"WINDOWS SUBSYSTEM FOR LINUX (WSL) DETECTED:",
194+
"",
195+
"IMPORTANT: Install Node.js within WSL, not from Windows.",
196+
"Using Windows Node.js from WSL can cause path and performance issues.",
197+
"",
198+
"Recommended approach:",
199+
" 1. Use your Linux distribution's package manager (see below)",
200+
" 2. Or use nvm for version management:",
201+
" curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash",
202+
" source ~/.bashrc",
203+
" nvm install 20",
204+
"",
205+
"Tips for WSL:",
206+
" - Store project files in the WSL filesystem (~/), not /mnt/c/",
207+
" - This improves file I/O performance significantly",
208+
" - Use 'wsl --shutdown' to restart WSL if needed",
209+
]
210+
211+
185212
def _get_linux_instructions(env: Environment) -> list[str]:
186213
"""Instructions for Linux systems."""
187214
lines = []

tests/test_environment.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,36 @@ def test_detect_container_returns_tuple(self) -> None:
109109
assert isinstance(is_k8s, bool)
110110

111111

112+
class TestWSLDetection:
113+
"""Test WSL detection."""
114+
115+
def test_detect_wsl_from_env_var(self, monkeypatch: pytest.MonkeyPatch) -> None:
116+
"""Detect WSL from WSL_DISTRO_NAME environment variable."""
117+
monkeypatch.setenv("WSL_DISTRO_NAME", "Ubuntu")
118+
119+
from promptfoo.environment import _detect_wsl
120+
121+
assert _detect_wsl() is True
122+
123+
def test_detect_wsl_from_interop_env(self, monkeypatch: pytest.MonkeyPatch) -> None:
124+
"""Detect WSL from WSL_INTEROP environment variable."""
125+
monkeypatch.setenv("WSL_INTEROP", "/run/WSL/123_interop")
126+
127+
from promptfoo.environment import _detect_wsl
128+
129+
assert _detect_wsl() is True
130+
131+
def test_no_wsl_detected(self) -> None:
132+
"""Return False when not in WSL."""
133+
with mock.patch.dict(os.environ, {}, clear=True):
134+
from promptfoo.environment import _detect_wsl
135+
136+
# This will return False unless we're actually in WSL
137+
# Just verify it returns a boolean
138+
result = _detect_wsl()
139+
assert isinstance(result, bool)
140+
141+
112142
class TestCIDetection:
113143
"""Test CI/CD platform detection."""
114144

@@ -217,6 +247,7 @@ def test_detect_ubuntu_with_docker(self, monkeypatch: pytest.MonkeyPatch, tmp_pa
217247
mock.patch("sys.platform", "linux"),
218248
mock.patch("promptfoo.environment._detect_linux_distro", return_value=("ubuntu", "22.04")),
219249
mock.patch("promptfoo.environment._detect_container", return_value=(True, False)),
250+
mock.patch("promptfoo.environment._detect_wsl", return_value=False),
220251
mock.patch("promptfoo.environment._detect_ci", return_value=(False, None)),
221252
mock.patch("promptfoo.environment._detect_cloud_provider", return_value=None),
222253
mock.patch("promptfoo.environment._detect_python_env", return_value=(True, False)),
@@ -229,6 +260,7 @@ def test_detect_ubuntu_with_docker(self, monkeypatch: pytest.MonkeyPatch, tmp_pa
229260
assert env.linux_distro_version == "22.04"
230261
assert env.is_docker is True
231262
assert env.is_kubernetes is False
263+
assert env.is_wsl is False
232264
assert env.is_venv is True
233265

234266
def test_detect_macos_environment(self) -> None:
@@ -267,6 +299,7 @@ def test_detect_aws_lambda(self, monkeypatch: pytest.MonkeyPatch) -> None:
267299
mock.patch("sys.platform", "linux"),
268300
mock.patch("promptfoo.environment._detect_linux_distro", return_value=("amzn", "2")),
269301
mock.patch("promptfoo.environment._detect_container", return_value=(False, False)),
302+
mock.patch("promptfoo.environment._detect_wsl", return_value=False),
270303
mock.patch("promptfoo.environment._detect_ci", return_value=(False, None)),
271304
mock.patch("promptfoo.environment._detect_cloud_provider", return_value="aws"),
272305
mock.patch("promptfoo.environment._detect_python_env", return_value=(False, False)),
@@ -285,6 +318,7 @@ def test_detect_github_actions(self, monkeypatch: pytest.MonkeyPatch) -> None:
285318
mock.patch("sys.platform", "linux"),
286319
mock.patch("promptfoo.environment._detect_linux_distro", return_value=("ubuntu", "22.04")),
287320
mock.patch("promptfoo.environment._detect_container", return_value=(False, False)),
321+
mock.patch("promptfoo.environment._detect_wsl", return_value=False),
288322
mock.patch("promptfoo.environment._detect_ci", return_value=(True, "github")),
289323
mock.patch("promptfoo.environment._detect_cloud_provider", return_value=None),
290324
mock.patch("promptfoo.environment._detect_python_env", return_value=(False, False)),
@@ -294,3 +328,24 @@ def test_detect_github_actions(self, monkeypatch: pytest.MonkeyPatch) -> None:
294328

295329
assert env.is_ci is True
296330
assert env.ci_platform == "github"
331+
332+
def test_detect_wsl_ubuntu(self, monkeypatch: pytest.MonkeyPatch) -> None:
333+
"""Detect WSL with Ubuntu."""
334+
monkeypatch.setenv("WSL_DISTRO_NAME", "Ubuntu")
335+
336+
with (
337+
mock.patch("sys.platform", "linux"),
338+
mock.patch("promptfoo.environment._detect_linux_distro", return_value=("ubuntu", "22.04")),
339+
mock.patch("promptfoo.environment._detect_container", return_value=(False, False)),
340+
mock.patch("promptfoo.environment._detect_wsl", return_value=True),
341+
mock.patch("promptfoo.environment._detect_ci", return_value=(False, None)),
342+
mock.patch("promptfoo.environment._detect_cloud_provider", return_value=None),
343+
mock.patch("promptfoo.environment._detect_python_env", return_value=(False, False)),
344+
mock.patch("promptfoo.environment._has_sudo_access", return_value=True),
345+
):
346+
env = detect_environment()
347+
348+
assert env.os_type == "linux"
349+
assert env.linux_distro == "ubuntu"
350+
assert env.is_wsl is True
351+
assert env.is_docker is False

tests/test_instructions.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,41 @@ def test_docker_ubuntu_instructions(self) -> None:
129129
assert "Dockerfile" in instructions
130130

131131

132+
class TestWSLInstructions:
133+
"""Test instructions for WSL (Windows Subsystem for Linux)."""
134+
135+
def test_wsl_instructions(self) -> None:
136+
"""Generate WSL-specific instructions."""
137+
env = Environment(
138+
os_type="linux",
139+
linux_distro="ubuntu",
140+
is_wsl=True,
141+
)
142+
143+
instructions = get_installation_instructions(env)
144+
145+
assert "WSL" in instructions or "Windows Subsystem for Linux" in instructions
146+
assert "nvm" in instructions
147+
assert "/mnt/c" in instructions # Should mention Windows filesystem
148+
assert "performance" in instructions.lower()
149+
150+
def test_wsl_with_ubuntu_shows_both(self) -> None:
151+
"""WSL instructions should show both WSL tips and Ubuntu instructions."""
152+
env = Environment(
153+
os_type="linux",
154+
linux_distro="ubuntu",
155+
is_wsl=True,
156+
has_sudo=True,
157+
)
158+
159+
instructions = get_installation_instructions(env)
160+
161+
# Should have WSL-specific guidance
162+
assert "WSL" in instructions
163+
# Should also have Ubuntu/Debian instructions
164+
assert "UBUNTU" in instructions or "DEBIAN" in instructions
165+
166+
132167
class TestLinuxInstructions:
133168
"""Test instructions for various Linux distributions."""
134169

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)