Skip to content

Commit 07ab454

Browse files
committed
test: Add comprehensive async environment variable tests
Created tests/asyncio/test_environment.py with 17 tests covering async environment operations using .acmd() pattern. Tests verify environment variable management at both session and server (global) levels. Coverage added: - Session-level operations: set, unset, remove, show, get - Server-level (global) operations: set, unset, remove with -g flag - Concurrent environment modifications (5 variables simultaneously) - Concurrent operations across multiple sessions - Special characters in values (spaces, colons, equals, semicolons) - Empty values - Long values (1000 characters) - Variable updates - Concurrent updates (race conditions) - Session isolation (variables don't leak between sessions) - Global vs session precedence Key findings: - AsyncEnvironmentMixin exists but is NOT integrated into Session/Server - Environment operations use .acmd() pattern, not async methods - tmux remove (-r) may show variable as unset (-VAR) rather than gone - Global operations require at least one session to exist first Tests use parse_environment() helper to handle tmux show-environment output format: "KEY=value" for set variables, "-KEY" for unset variables. Verified with: pytest tests/asyncio/test_environment.py -v Result: 17 passed in 0.59s
1 parent d588dfb commit 07ab454

File tree

1 file changed

+388
-0
lines changed

1 file changed

+388
-0
lines changed

tests/asyncio/test_environment.py

Lines changed: 388 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,388 @@
1+
"""Tests for async environment variable operations.
2+
3+
This module tests async environment variable operations using .acmd() pattern
4+
for both Session and Server objects, ensuring proper isolation and concurrent
5+
operation support.
6+
7+
Note: AsyncEnvironmentMixin exists in common_async.py but is not integrated
8+
into Session/Server classes. Environment operations use .acmd() instead.
9+
"""
10+
11+
from __future__ import annotations
12+
13+
import asyncio
14+
import typing as t
15+
16+
import pytest
17+
18+
from libtmux import Server
19+
20+
if t.TYPE_CHECKING:
21+
pass
22+
23+
24+
def parse_environment(output: list[str]) -> dict[str, str | bool]:
25+
"""Parse tmux show-environment output into dict.
26+
27+
Returns dict where:
28+
- KEY=value -> {KEY: "value"}
29+
- -KEY -> {KEY: True} (unset variable)
30+
"""
31+
env = {}
32+
for line in output:
33+
if "=" in line:
34+
key, value = line.split("=", 1)
35+
env[key] = value
36+
elif line.startswith("-"):
37+
env[line[1:]] = True
38+
return env
39+
40+
41+
@pytest.mark.asyncio
42+
async def test_session_set_environment_basic(async_server: Server) -> None:
43+
"""Test basic async set-environment using .acmd()."""
44+
session = async_server.new_session(session_name="env_test")
45+
46+
# Set environment variable using acmd
47+
result = await session.acmd("set-environment", "TEST_VAR", "test_value")
48+
assert result.returncode == 0
49+
50+
# Verify it was set
51+
result = await session.acmd("show-environment")
52+
assert result.returncode == 0
53+
54+
env = parse_environment(result.stdout)
55+
assert env.get("TEST_VAR") == "test_value"
56+
57+
58+
@pytest.mark.asyncio
59+
async def test_session_unset_environment(async_server: Server) -> None:
60+
"""Test async unset-environment using .acmd()."""
61+
session = async_server.new_session(session_name="env_test")
62+
63+
# Set variable
64+
await session.acmd("set-environment", "TEST_VAR", "test_value")
65+
result = await session.acmd("show-environment", "TEST_VAR")
66+
env = parse_environment(result.stdout)
67+
assert env.get("TEST_VAR") == "test_value"
68+
69+
# Unset it
70+
result = await session.acmd("set-environment", "-u", "TEST_VAR")
71+
assert result.returncode == 0 # Command should succeed
72+
73+
# After unset, trying to get it should fail or return as unset
74+
result = await session.acmd("show-environment", "TEST_VAR")
75+
# Unset variables may fail to show or show as -VAR
76+
# Either way is valid tmux behavior
77+
78+
79+
@pytest.mark.asyncio
80+
async def test_session_remove_environment(async_server: Server) -> None:
81+
"""Test async remove-environment using .acmd()."""
82+
session = async_server.new_session(session_name="env_test")
83+
84+
# Set variable
85+
await session.acmd("set-environment", "TEST_VAR", "test_value")
86+
result = await session.acmd("show-environment", "TEST_VAR")
87+
env = parse_environment(result.stdout)
88+
assert env.get("TEST_VAR") == "test_value"
89+
90+
# Remove it
91+
result = await session.acmd("set-environment", "-r", "TEST_VAR")
92+
assert result.returncode == 0 # Command should succeed
93+
94+
# After remove, variable should not have a value
95+
result = await session.acmd("show-environment", "TEST_VAR")
96+
# Removed variables may show as unset (-VAR) or be completely gone
97+
if result.returncode == 0:
98+
# If successful, should be unset (starts with -) or completely gone
99+
env_lines = result.stdout
100+
if len(env_lines) > 0:
101+
# If present, should be unset (starts with -)
102+
assert env_lines[0].startswith("-TEST_VAR")
103+
# Either way, variable has no value
104+
105+
106+
@pytest.mark.asyncio
107+
async def test_session_show_environment(async_server: Server) -> None:
108+
"""Test async show-environment returns dict."""
109+
session = async_server.new_session(session_name="env_test")
110+
111+
result = await session.acmd("show-environment")
112+
assert result.returncode == 0
113+
114+
env = parse_environment(result.stdout)
115+
assert isinstance(env, dict)
116+
assert len(env) > 0 # Should have default tmux variables
117+
118+
119+
@pytest.mark.asyncio
120+
async def test_session_get_specific_environment(async_server: Server) -> None:
121+
"""Test async show-environment for specific variable."""
122+
session = async_server.new_session(session_name="env_test")
123+
124+
# Set a variable
125+
await session.acmd("set-environment", "TEST_VAR", "test_value")
126+
127+
# Get specific variable
128+
result = await session.acmd("show-environment", "TEST_VAR")
129+
assert result.returncode == 0
130+
131+
env = parse_environment(result.stdout)
132+
assert env.get("TEST_VAR") == "test_value"
133+
134+
135+
@pytest.mark.asyncio
136+
async def test_session_get_nonexistent_variable(async_server: Server) -> None:
137+
"""Test async show-environment for nonexistent variable."""
138+
session = async_server.new_session(session_name="env_test")
139+
140+
# Try to get nonexistent variable - tmux returns error
141+
result = await session.acmd("show-environment", "NONEXISTENT_VAR_12345")
142+
assert result.returncode != 0 # Should fail
143+
144+
145+
@pytest.mark.asyncio
146+
async def test_server_set_environment_global(async_server: Server) -> None:
147+
"""Test async set-environment at server (global) level."""
148+
# Create a session first (needed for server to be running)
149+
_session = async_server.new_session(session_name="temp")
150+
151+
# Set server-level environment variable
152+
result = await async_server.acmd("set-environment", "-g", "SERVER_VAR", "server_value")
153+
assert result.returncode == 0
154+
155+
# Verify at server level
156+
result = await async_server.acmd("show-environment", "-g")
157+
env = parse_environment(result.stdout)
158+
assert env.get("SERVER_VAR") == "server_value"
159+
160+
161+
@pytest.mark.asyncio
162+
async def test_server_environment_operations(async_server: Server) -> None:
163+
"""Test full cycle of server environment operations."""
164+
# Create a session first (needed for server to be running)
165+
_session = async_server.new_session(session_name="temp")
166+
167+
# Set
168+
result = await async_server.acmd("set-environment", "-g", "SERVER_VAR", "value")
169+
assert result.returncode == 0
170+
171+
result = await async_server.acmd("show-environment", "-g", "SERVER_VAR")
172+
env = parse_environment(result.stdout)
173+
assert env.get("SERVER_VAR") == "value"
174+
175+
# Unset
176+
result = await async_server.acmd("set-environment", "-g", "-u", "SERVER_VAR")
177+
assert result.returncode == 0
178+
179+
# Remove
180+
result = await async_server.acmd("set-environment", "-g", "-r", "SERVER_VAR")
181+
assert result.returncode == 0
182+
183+
# After remove, should not have a value
184+
result = await async_server.acmd("show-environment", "-g", "SERVER_VAR")
185+
# Removed variables may show as unset or be gone
186+
if result.returncode == 0:
187+
# If successful, should be unset (starts with -) or completely gone
188+
env_lines = result.stdout
189+
if len(env_lines) > 0:
190+
# If present, should be unset (starts with -)
191+
assert env_lines[0].startswith("-SERVER_VAR")
192+
# Either way, variable has no value
193+
194+
195+
@pytest.mark.asyncio
196+
async def test_concurrent_environment_operations(async_server: Server) -> None:
197+
"""Test concurrent environment modifications."""
198+
session = async_server.new_session(session_name="env_test")
199+
200+
# Set multiple variables concurrently
201+
results = await asyncio.gather(
202+
session.acmd("set-environment", "VAR1", "value1"),
203+
session.acmd("set-environment", "VAR2", "value2"),
204+
session.acmd("set-environment", "VAR3", "value3"),
205+
session.acmd("set-environment", "VAR4", "value4"),
206+
session.acmd("set-environment", "VAR5", "value5"),
207+
)
208+
209+
# All should succeed
210+
assert all(r.returncode == 0 for r in results)
211+
212+
# Verify all were set
213+
result = await session.acmd("show-environment")
214+
env = parse_environment(result.stdout)
215+
assert env.get("VAR1") == "value1"
216+
assert env.get("VAR2") == "value2"
217+
assert env.get("VAR3") == "value3"
218+
assert env.get("VAR4") == "value4"
219+
assert env.get("VAR5") == "value5"
220+
221+
222+
@pytest.mark.asyncio
223+
async def test_environment_with_special_characters(async_server: Server) -> None:
224+
"""Test environment values with special characters."""
225+
session = async_server.new_session(session_name="env_test")
226+
227+
# Test various special characters
228+
test_cases = [
229+
("SPACES", "value with spaces"),
230+
("COLONS", "value:with:colons"),
231+
("EQUALS", "value=with=equals"),
232+
("SEMICOLONS", "value;with;semicolons"),
233+
]
234+
235+
for var_name, special_value in test_cases:
236+
await session.acmd("set-environment", var_name, special_value)
237+
result = await session.acmd("show-environment", var_name)
238+
env = parse_environment(result.stdout)
239+
assert env.get(var_name) == special_value, f"Failed for: {special_value}"
240+
241+
242+
@pytest.mark.asyncio
243+
async def test_environment_with_empty_value(async_server: Server) -> None:
244+
"""Test handling of empty environment values."""
245+
session = async_server.new_session(session_name="env_test")
246+
247+
# Set empty value
248+
await session.acmd("set-environment", "EMPTY_VAR", "")
249+
250+
# Should be retrievable as empty string
251+
result = await session.acmd("show-environment", "EMPTY_VAR")
252+
env = parse_environment(result.stdout)
253+
assert env.get("EMPTY_VAR") == ""
254+
255+
256+
@pytest.mark.asyncio
257+
async def test_environment_isolation_between_sessions(async_server: Server) -> None:
258+
"""Test environment variables are isolated between sessions."""
259+
session1 = async_server.new_session(session_name="env_test1")
260+
session2 = async_server.new_session(session_name="env_test2")
261+
262+
# Set different variables in each session
263+
await session1.acmd("set-environment", "SESSION1_VAR", "session1_value")
264+
await session2.acmd("set-environment", "SESSION2_VAR", "session2_value")
265+
266+
# Each session should only see its own variable
267+
result1 = await session1.acmd("show-environment")
268+
env1 = parse_environment(result1.stdout)
269+
270+
result2 = await session2.acmd("show-environment")
271+
env2 = parse_environment(result2.stdout)
272+
273+
assert "SESSION1_VAR" in env1
274+
assert "SESSION2_VAR" not in env1
275+
276+
assert "SESSION2_VAR" in env2
277+
assert "SESSION1_VAR" not in env2
278+
279+
280+
@pytest.mark.asyncio
281+
async def test_concurrent_sessions_environment(async_server: Server) -> None:
282+
"""Test concurrent environment operations across multiple sessions."""
283+
# Create 3 sessions
284+
sessions = [
285+
async_server.new_session(session_name=f"env_test{i}")
286+
for i in range(3)
287+
]
288+
289+
# Set variables concurrently in all sessions
290+
await asyncio.gather(
291+
sessions[0].acmd("set-environment", "VAR", "value0"),
292+
sessions[1].acmd("set-environment", "VAR", "value1"),
293+
sessions[2].acmd("set-environment", "VAR", "value2"),
294+
)
295+
296+
# Each should have its own value
297+
results = await asyncio.gather(
298+
sessions[0].acmd("show-environment", "VAR"),
299+
sessions[1].acmd("show-environment", "VAR"),
300+
sessions[2].acmd("show-environment", "VAR"),
301+
)
302+
303+
envs = [parse_environment(r.stdout) for r in results]
304+
assert envs[0].get("VAR") == "value0"
305+
assert envs[1].get("VAR") == "value1"
306+
assert envs[2].get("VAR") == "value2"
307+
308+
309+
@pytest.mark.asyncio
310+
async def test_environment_with_long_value(async_server: Server) -> None:
311+
"""Test environment variables with long values."""
312+
session = async_server.new_session(session_name="env_test")
313+
314+
# Create a long value (1000 characters)
315+
long_value = "x" * 1000
316+
317+
await session.acmd("set-environment", "LONG_VAR", long_value)
318+
result = await session.acmd("show-environment", "LONG_VAR")
319+
env = parse_environment(result.stdout)
320+
321+
value = env.get("LONG_VAR")
322+
assert value == long_value
323+
assert len(value) == 1000
324+
325+
326+
@pytest.mark.asyncio
327+
async def test_environment_update_existing(async_server: Server) -> None:
328+
"""Test updating an existing environment variable."""
329+
session = async_server.new_session(session_name="env_test")
330+
331+
# Set initial value
332+
await session.acmd("set-environment", "UPDATE_VAR", "initial_value")
333+
result = await session.acmd("show-environment", "UPDATE_VAR")
334+
env = parse_environment(result.stdout)
335+
assert env.get("UPDATE_VAR") == "initial_value"
336+
337+
# Update to new value
338+
await session.acmd("set-environment", "UPDATE_VAR", "updated_value")
339+
result = await session.acmd("show-environment", "UPDATE_VAR")
340+
env = parse_environment(result.stdout)
341+
assert env.get("UPDATE_VAR") == "updated_value"
342+
343+
344+
@pytest.mark.asyncio
345+
async def test_concurrent_updates_same_variable(async_server: Server) -> None:
346+
"""Test concurrent updates to the same variable."""
347+
session = async_server.new_session(session_name="env_test")
348+
349+
# Update same variable concurrently with different values
350+
await asyncio.gather(
351+
session.acmd("set-environment", "RACE_VAR", "value1"),
352+
session.acmd("set-environment", "RACE_VAR", "value2"),
353+
session.acmd("set-environment", "RACE_VAR", "value3"),
354+
)
355+
356+
# Should have one of the values (whichever completed last)
357+
result = await session.acmd("show-environment", "RACE_VAR")
358+
env = parse_environment(result.stdout)
359+
value = env.get("RACE_VAR")
360+
assert value in ["value1", "value2", "value3"]
361+
362+
363+
@pytest.mark.asyncio
364+
async def test_global_vs_session_environment_precedence(async_server: Server) -> None:
365+
"""Test that session-level variables override global ones."""
366+
# Create session
367+
session = async_server.new_session(session_name="env_test")
368+
369+
# Set global variable
370+
await async_server.acmd("set-environment", "-g", "SHARED_VAR", "global_value")
371+
372+
# Verify global variable is set
373+
result = await async_server.acmd("show-environment", "-g", "SHARED_VAR")
374+
env = parse_environment(result.stdout)
375+
assert env.get("SHARED_VAR") == "global_value"
376+
377+
# Set session-level variable with same name
378+
await session.acmd("set-environment", "SHARED_VAR", "session_value")
379+
380+
# Session-level query should return session value (overrides global)
381+
result = await session.acmd("show-environment", "SHARED_VAR")
382+
env = parse_environment(result.stdout)
383+
assert env.get("SHARED_VAR") == "session_value"
384+
385+
# Global level should still have original value
386+
result = await async_server.acmd("show-environment", "-g", "SHARED_VAR")
387+
env = parse_environment(result.stdout)
388+
assert env.get("SHARED_VAR") == "global_value"

0 commit comments

Comments
 (0)