1+ # SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+ # SPDX-License-Identifier: Apache-2.0
3+ #
4+ #
5+ # Licensed under the Apache License, Version 2.0 (the "License");
6+ # you may not use this file except in compliance with the License.
7+ # You may obtain a copy of the License at
8+ #
9+ # http://www.apache.org/licenses/LICENSE-2.0
10+ #
11+ # Unless required by applicable law or agreed to in writing, software
12+ # distributed under the License is distributed on an "AS IS" BASIS,
13+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+ # See the License for the specific language governing permissions and
15+ # limitations under the License.
16+
17+ import unittest
18+ from unittest import mock
19+ from skyhook_agent .chroot_exec import _get_process_env , _get_chroot_env
20+
21+
22+ class TestChrootExec (unittest .TestCase ):
23+
24+ def test_get_process_env_basic_functionality (self ):
25+ """Test _get_process_env with non-overlapping keys"""
26+ container_env = {"CONTAINER_VAR" : "container_value" }
27+ chroot_env = {"CHROOT_VAR" : "chroot_value" }
28+ skyhook_env = {"SKYHOOK_VAR" : "skyhook_value" }
29+
30+ result = _get_process_env (container_env , skyhook_env , chroot_env )
31+
32+ expected = {
33+ "CONTAINER_VAR" : "container_value" ,
34+ "CHROOT_VAR" : "chroot_value" ,
35+ "SKYHOOK_VAR" : "skyhook_value"
36+ }
37+ self .assertEqual (result , expected )
38+
39+ def test_get_process_env_chroot_overrides_container (self ):
40+ """Test that chroot_env overrides container_env for same keys"""
41+ container_env = {"SAME_VAR" : "container_value" , "CONTAINER_VAR" : "container_value" }
42+ chroot_env = {"SAME_VAR" : "chroot_value" , "CHROOT_VAR" : "chroot_value" }
43+ skyhook_env = {"SKYHOOK_VAR" : "skyhook_value" }
44+
45+ result = _get_process_env (container_env , skyhook_env , chroot_env )
46+
47+ expected = {
48+ "SAME_VAR" : "chroot_value" , # chroot overrides container
49+ "CONTAINER_VAR" : "container_value" ,
50+ "CHROOT_VAR" : "chroot_value" ,
51+ "SKYHOOK_VAR" : "skyhook_value"
52+ }
53+ self .assertEqual (result , expected )
54+
55+ def test_get_process_env_skyhook_overrides_all (self ):
56+ """Test that skyhook_env has highest priority and overrides both chroot and container"""
57+ container_env = {"SAME_VAR" : "container_value" , "CONTAINER_VAR" : "container_value" }
58+ chroot_env = {"SAME_VAR" : "chroot_value" , "CHROOT_VAR" : "chroot_value" }
59+ skyhook_env = {"SAME_VAR" : "skyhook_value" , "SKYHOOK_VAR" : "skyhook_value" }
60+
61+ result = _get_process_env (container_env , skyhook_env , chroot_env )
62+
63+ expected = {
64+ "SAME_VAR" : "skyhook_value" , # skyhook overrides both chroot and container
65+ "CONTAINER_VAR" : "container_value" ,
66+ "CHROOT_VAR" : "chroot_value" ,
67+ "SKYHOOK_VAR" : "skyhook_value"
68+ }
69+ self .assertEqual (result , expected )
70+
71+ def test_get_process_env_with_empty_dicts (self ):
72+ """Test _get_process_env with empty dictionaries"""
73+ result = _get_process_env ({}, {}, {})
74+ self .assertEqual (result , {})
75+
76+ # Test with only one dict having values
77+ container_env = {"VAR" : "value" }
78+ result = _get_process_env (container_env , {}, {})
79+ self .assertEqual (result , {"VAR" : "value" })
80+
81+ chroot_env = {"VAR" : "value" }
82+ result = _get_process_env ({}, {}, chroot_env )
83+ self .assertEqual (result , {"VAR" : "value" })
84+
85+ skyhook_env = {"VAR" : "value" }
86+ result = _get_process_env ({}, skyhook_env , {})
87+ self .assertEqual (result , {"VAR" : "value" })
88+
89+ def test_get_process_env_precedence_order (self ):
90+ """Test complete precedence order: skyhook > chroot > container"""
91+ container_env = {
92+ "PATH" : "/container/path" ,
93+ "HOME" : "/container/home" ,
94+ "USER" : "container_user" ,
95+ "ONLY_CONTAINER" : "container_only"
96+ }
97+ chroot_env = {
98+ "PATH" : "/chroot/path" ,
99+ "HOME" : "/chroot/home" ,
100+ "ONLY_CHROOT" : "chroot_only"
101+ }
102+ skyhook_env = {
103+ "PATH" : "/skyhook/path" ,
104+ "ONLY_SKYHOOK" : "skyhook_only"
105+ }
106+
107+ result = _get_process_env (container_env , skyhook_env , chroot_env )
108+
109+ expected = {
110+ "PATH" : "/skyhook/path" , # skyhook wins
111+ "HOME" : "/chroot/home" , # chroot wins over container
112+ "USER" : "container_user" , # only in container
113+ "ONLY_CONTAINER" : "container_only" ,
114+ "ONLY_CHROOT" : "chroot_only" ,
115+ "ONLY_SKYHOOK" : "skyhook_only"
116+ }
117+ self .assertEqual (result , expected )
118+
119+ def test_get_process_env_does_not_modify_input_dicts (self ):
120+ """Test that input dictionaries are not modified"""
121+ container_env = {"VAR" : "container" }
122+ chroot_env = {"VAR" : "chroot" }
123+ skyhook_env = {"VAR" : "skyhook" }
124+
125+ # Keep original references
126+ original_container = container_env .copy ()
127+ original_chroot = chroot_env .copy ()
128+ original_skyhook = skyhook_env .copy ()
129+
130+ result = _get_process_env (container_env , skyhook_env , chroot_env )
131+
132+ # Verify input dicts weren't modified
133+ self .assertEqual (container_env , original_container )
134+ self .assertEqual (chroot_env , original_chroot )
135+ self .assertEqual (skyhook_env , original_skyhook )
136+
137+ # Verify result is correct
138+ self .assertEqual (result , {"VAR" : "skyhook" })
139+
140+ @mock .patch ('skyhook_agent.chroot_exec.subprocess.run' )
141+ def test_get_chroot_env_basic_functionality (self , mock_subprocess ):
142+ """Test _get_chroot_env with typical environment output"""
143+ mock_result = mock .MagicMock ()
144+ mock_result .stdout = "PATH=/usr/bin:/bin\n HOME=/root\n USER=root\n "
145+ mock_subprocess .return_value = mock_result
146+
147+ result = _get_chroot_env ()
148+
149+ expected = {
150+ "PATH" : "/usr/bin:/bin" ,
151+ "HOME" : "/root" ,
152+ "USER" : "root"
153+ }
154+ self .assertEqual (result , expected )
155+ mock_subprocess .assert_called_once_with (["env" ], capture_output = True , text = True )
156+
157+ @mock .patch ('skyhook_agent.chroot_exec.subprocess.run' )
158+ def test_get_chroot_env_with_multiple_equals (self , mock_subprocess ):
159+ """Test _get_chroot_env correctly handles lines with multiple '=' characters"""
160+ mock_result = mock .MagicMock ()
161+ mock_result .stdout = "VAR1=value=with=equals\n VAR2=simple_value\n "
162+ mock_subprocess .return_value = mock_result
163+
164+ result = _get_chroot_env ()
165+
166+ expected = {
167+ "VAR1" : "value=with=equals" , # Should split only on first =
168+ "VAR2" : "simple_value"
169+ }
170+ self .assertEqual (result , expected )
171+
172+ @mock .patch ('skyhook_agent.chroot_exec.subprocess.run' )
173+ def test_get_chroot_env_ignores_lines_without_equals (self , mock_subprocess ):
174+ """Test _get_chroot_env ignores lines that don't contain '='"""
175+ mock_result = mock .MagicMock ()
176+ mock_result .stdout = "PATH=/usr/bin\n invalid_line_no_equals\n HOME=/root\n \n "
177+ mock_subprocess .return_value = mock_result
178+
179+ result = _get_chroot_env ()
180+
181+ expected = {
182+ "PATH" : "/usr/bin" ,
183+ "HOME" : "/root"
184+ }
185+ self .assertEqual (result , expected )
186+
187+ @mock .patch ('skyhook_agent.chroot_exec.subprocess.run' )
188+ def test_get_chroot_env_with_empty_output (self , mock_subprocess ):
189+ """Test _get_chroot_env with empty subprocess output"""
190+ mock_result = mock .MagicMock ()
191+ mock_result .stdout = ""
192+ mock_subprocess .return_value = mock_result
193+
194+ result = _get_chroot_env ()
195+
196+ self .assertEqual (result , {})
197+ mock_subprocess .assert_called_once_with (["env" ], capture_output = True , text = True )
198+
199+ @mock .patch ('skyhook_agent.chroot_exec.subprocess.run' )
200+ def test_get_chroot_env_with_empty_values (self , mock_subprocess ):
201+ """Test _get_chroot_env handles environment variables with empty values"""
202+ mock_result = mock .MagicMock ()
203+ mock_result .stdout = "EMPTY_VAR=\n NORM_VAR=value\n ANOTHER_EMPTY=\n "
204+ mock_subprocess .return_value = mock_result
205+
206+ result = _get_chroot_env ()
207+
208+ expected = {
209+ "EMPTY_VAR" : "" ,
210+ "NORM_VAR" : "value" ,
211+ "ANOTHER_EMPTY" : ""
212+ }
213+ self .assertEqual (result , expected )
214+
215+ @mock .patch ('skyhook_agent.chroot_exec.subprocess.run' )
216+ def test_get_chroot_env_with_whitespace_and_special_chars (self , mock_subprocess ):
217+ """Test _get_chroot_env handles values with whitespace and special characters"""
218+ mock_result = mock .MagicMock ()
219+ mock_result .stdout = "VAR_WITH_SPACES=value with spaces\n SPECIAL_CHARS=!@#$%^&*()\n PATH=/usr/bin:/bin\n "
220+ mock_subprocess .return_value = mock_result
221+
222+ result = _get_chroot_env ()
223+
224+ expected = {
225+ "VAR_WITH_SPACES" : "value with spaces" ,
226+ "SPECIAL_CHARS" : "!@#$%^&*()" ,
227+ "PATH" : "/usr/bin:/bin"
228+ }
229+ self .assertEqual (result , expected )
230+
231+
232+ if __name__ == '__main__' :
233+ unittest .main ()
0 commit comments