xref: /aosp_15_r20/external/pigweed/pw_env_setup/py/environment_test.py (revision 61c4878ac05f98d0ceed94b57d316916de578985)
1# Copyright 2020 The Pigweed Authors
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may not
4# use this file except in compliance with the License. You may obtain a copy of
5# the License at
6#
7#     https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations under
13# the License.
14"""Tests for env_setup.environment.
15
16This tests the error-checking, context manager, and written environment scripts
17of the Environment class.
18
19Tests that end in "_ctx" modify the environment and validate it in-process.
20
21Tests that end in "_written" write the environment to a file intended to be
22evaluated by the shell, then launches the shell and then saves the environment.
23This environment is then validated in the test process.
24"""
25
26import logging
27import os
28import subprocess
29import tempfile
30import unittest
31
32import six
33
34from pw_env_setup import environment
35
36
37class WrittenEnvFailure(Exception):
38    pass
39
40
41def _evaluate_env_in_shell(env):
42    """Write env to a file then evaluate and save the resulting environment.
43
44    Write env to a file, then launch a shell command that sources that file
45    and dumps the environment to stdout. Parse that output into a dict and
46    return it.
47
48    Args:
49      env(environment.Environment): environment to write out
50
51    Returns dictionary of resulting environment.
52    """
53
54    # Write env sourcing script to file.
55    with tempfile.NamedTemporaryFile(
56        prefix='pw-test-written-env-',
57        suffix='.bat' if os.name == 'nt' else '.sh',
58        delete=False,
59        mode='w+',
60    ) as temp:
61        temp_name = temp.name
62        env.write(temp, shell_file=temp_name)
63
64    # Evaluate env sourcing script and capture output of 'env'.
65    if os.name == 'nt':
66        # On Windows you just run batch files and they modify your
67        # environment, no need to call 'source' or '.'.
68        cmd = '{} && set'.format(temp_name)
69    else:
70        # Using '.' instead of 'source' because 'source' is not POSIX.
71        cmd = '. {} && env'.format(temp_name)
72
73    res = subprocess.run(cmd, capture_output=True, shell=True)
74    if res.returncode:
75        raise WrittenEnvFailure(res.stderr)
76
77    # Parse environment from stdout of subprocess.
78    env_ret = {}
79    for line in res.stdout.splitlines():
80        line = line.decode()
81
82        # Some people inexplicably have newlines in some of their
83        # environment variables. This module does not allow that so we can
84        # ignore any such extra lines.
85        if '=' not in line:
86            continue
87
88        var, value = line.split('=', 1)
89        env_ret[var] = value
90
91    return env_ret
92
93
94# pylint: disable=too-many-public-methods
95class EnvironmentTest(unittest.TestCase):
96    """Tests for env_setup.environment."""
97
98    def setUp(self):
99        self.env = environment.Environment()
100
101        # Name of a variable that is already set when the test starts.
102        self.var_already_set = self.env.normalize_key('var_already_set')
103        os.environ[self.var_already_set] = 'orig value'
104        self.assertIn(self.var_already_set, os.environ)
105
106        # Name of a variable that is not set when the test starts.
107        self.var_not_set = self.env.normalize_key('var_not_set')
108        if self.var_not_set in os.environ:
109            del os.environ[self.var_not_set]
110        self.assertNotIn(self.var_not_set, os.environ)
111
112        self.orig_env = os.environ.copy()
113
114    def tearDown(self):
115        self.assertEqual(os.environ, self.orig_env)
116
117    def test_set_notpresent_ctx(self):
118        self.env.set(self.var_not_set, '1')
119        with self.env(export=False) as env:
120            self.assertIn(self.var_not_set, env)
121            self.assertEqual(env[self.var_not_set], '1')
122
123    def test_set_notpresent_written(self):
124        self.env.set(self.var_not_set, '1')
125        env = _evaluate_env_in_shell(self.env)
126        self.assertIn(self.var_not_set, env)
127        self.assertEqual(env[self.var_not_set], '1')
128
129    def test_set_present_ctx(self):
130        self.env.set(self.var_already_set, '1')
131        with self.env(export=False) as env:
132            self.assertIn(self.var_already_set, env)
133            self.assertEqual(env[self.var_already_set], '1')
134
135    def test_set_present_written(self):
136        self.env.set(self.var_already_set, '1')
137        env = _evaluate_env_in_shell(self.env)
138        self.assertIn(self.var_already_set, env)
139        self.assertEqual(env[self.var_already_set], '1')
140
141    def test_clear_notpresent_ctx(self):
142        self.env.clear(self.var_not_set)
143        with self.env(export=False) as env:
144            self.assertNotIn(self.var_not_set, env)
145
146    def test_clear_notpresent_written(self):
147        self.env.clear(self.var_not_set)
148        env = _evaluate_env_in_shell(self.env)
149        self.assertNotIn(self.var_not_set, env)
150
151    def test_clear_present_ctx(self):
152        self.env.clear(self.var_already_set)
153        with self.env(export=False) as env:
154            self.assertNotIn(self.var_already_set, env)
155
156    def test_clear_present_written(self):
157        self.env.clear(self.var_already_set)
158        env = _evaluate_env_in_shell(self.env)
159        self.assertNotIn(self.var_already_set, env)
160
161    def test_value_replacement(self):
162        self.env.set(self.var_not_set, '/foo/bar/baz')
163        self.env.add_replacement('FOOBAR', '/foo/bar')
164        buf = six.StringIO()
165        self.env.write(buf, shell_file='test.sh')
166        assert '/foo/bar' not in buf.getvalue()
167
168    def test_variable_replacement(self):
169        self.env.set('FOOBAR', '/foo/bar')
170        self.env.set(self.var_not_set, '/foo/bar/baz')
171        self.env.add_replacement('FOOBAR')
172        buf = six.StringIO()
173        self.env.write(buf, shell_file='test.sh')
174        print(buf.getvalue())
175        assert '/foo/bar/baz' not in buf.getvalue()
176
177    def test_nonglobal(self):
178        self.env.set(self.var_not_set, '1')
179        with self.env(export=False) as env:
180            self.assertIn(self.var_not_set, env)
181            self.assertNotIn(self.var_not_set, os.environ)
182
183    def test_global(self):
184        self.env.set(self.var_not_set, '1')
185        with self.env(export=True) as env:
186            self.assertIn(self.var_not_set, env)
187            self.assertIn(self.var_not_set, os.environ)
188
189    def test_set_badnametype(self):
190        with self.assertRaises(environment.BadNameType):
191            self.env.set(123, '123')
192
193    def test_set_badvaluetype(self):
194        with self.assertRaises(environment.BadValueType):
195            self.env.set('var', 123)
196
197    def test_prepend_badnametype(self):
198        with self.assertRaises(environment.BadNameType):
199            self.env.prepend(123, '123')
200
201    def test_prepend_badvaluetype(self):
202        with self.assertRaises(environment.BadValueType):
203            self.env.prepend('var', 123)
204
205    def test_append_badnametype(self):
206        with self.assertRaises(environment.BadNameType):
207            self.env.append(123, '123')
208
209    def test_append_badvaluetype(self):
210        with self.assertRaises(environment.BadValueType):
211            self.env.append('var', 123)
212
213    def test_set_badname_empty(self):
214        with self.assertRaises(environment.BadVariableName):
215            self.env.set('', '123')
216
217    def test_set_badname_digitstart(self):
218        with self.assertRaises(environment.BadVariableName):
219            self.env.set('123', '123')
220
221    def test_set_badname_equals(self):
222        with self.assertRaises(environment.BadVariableName):
223            self.env.set('foo=bar', '123')
224
225    def test_set_badname_period(self):
226        with self.assertRaises(environment.BadVariableName):
227            self.env.set('abc.def', '123')
228
229    def test_set_badname_hyphen(self):
230        with self.assertRaises(environment.BadVariableName):
231            self.env.set('abc-def', '123')
232
233    def test_set_empty_value(self):
234        with self.assertRaises(environment.EmptyValue):
235            self.env.set('var', '')
236
237    def test_set_newline_in_value(self):
238        with self.assertRaises(environment.NewlineInValue):
239            self.env.set('var', '123\n456')
240
241    def test_equal_sign_in_value(self):
242        with self.assertRaises(environment.BadVariableValue):
243            self.env.append(self.var_already_set, 'pa=th')
244
245
246class _PrependAppendEnvironmentTest(unittest.TestCase):
247    """Tests for env_setup.environment."""
248
249    def __init__(self, *args, **kwargs):
250        windows = kwargs.pop('windows', False)
251        pathsep = kwargs.pop('pathsep', os.pathsep)
252        allcaps = kwargs.pop('allcaps', False)
253        super().__init__(*args, **kwargs)
254        self.windows = windows
255        self.pathsep = pathsep
256        self.allcaps = allcaps
257
258        # If we're testing Windows behavior and actually running on Windows,
259        # actually launch a subprocess to evaluate the shell init script.
260        # Likewise if we're testing POSIX behavior and actually on a POSIX
261        # system. Tests can check self.run_shell_tests and exit without
262        # doing anything.
263        real_windows = os.name == 'nt'
264        self.run_shell_tests = self.windows == real_windows
265
266    def setUp(self):
267        self.env = environment.Environment(
268            windows=self.windows, pathsep=self.pathsep, allcaps=self.allcaps
269        )
270
271        self.var_already_set = self.env.normalize_key('VAR_ALREADY_SET')
272        os.environ[self.var_already_set] = self.pathsep.join(
273            'one two three'.split()
274        )
275        self.assertIn(self.var_already_set, os.environ)
276
277        self.var_not_set = self.env.normalize_key('VAR_NOT_SET')
278        if self.var_not_set in os.environ:
279            del os.environ[self.var_not_set]
280        self.assertNotIn(self.var_not_set, os.environ)
281
282        self.orig_env = os.environ.copy()
283
284    def split(self, val):
285        return val.split(self.pathsep)
286
287    def tearDown(self):
288        self.assertEqual(os.environ, self.orig_env)
289
290
291class _AppendPrependTestMixin:
292    def test_prepend_present_ctx(self):
293        orig = os.environ[self.var_already_set]
294        self.env.prepend(self.var_already_set, 'path')
295        with self.env(export=False) as env:
296            self.assertEqual(
297                env[self.var_already_set], self.pathsep.join(('path', orig))
298            )
299
300    def test_prepend_present_written(self):
301        if not self.run_shell_tests:
302            return
303
304        orig = os.environ[self.var_already_set]
305        self.env.prepend(self.var_already_set, 'path')
306        env = _evaluate_env_in_shell(self.env)
307        self.assertEqual(
308            env[self.var_already_set], self.pathsep.join(('path', orig))
309        )
310
311    def test_prepend_notpresent_ctx(self):
312        self.env.prepend(self.var_not_set, 'path')
313        with self.env(export=False) as env:
314            self.assertEqual(env[self.var_not_set], 'path')
315
316    def test_prepend_notpresent_written(self):
317        if not self.run_shell_tests:
318            return
319
320        self.env.prepend(self.var_not_set, 'path')
321        env = _evaluate_env_in_shell(self.env)
322        self.assertEqual(env[self.var_not_set], 'path')
323
324    def test_append_present_ctx(self):
325        orig = os.environ[self.var_already_set]
326        self.env.append(self.var_already_set, 'path')
327        with self.env(export=False) as env:
328            self.assertEqual(
329                env[self.var_already_set], self.pathsep.join((orig, 'path'))
330            )
331
332    def test_append_present_written(self):
333        if not self.run_shell_tests:
334            return
335
336        orig = os.environ[self.var_already_set]
337        self.env.append(self.var_already_set, 'path')
338        env = _evaluate_env_in_shell(self.env)
339        self.assertEqual(
340            env[self.var_already_set], self.pathsep.join((orig, 'path'))
341        )
342
343    def test_append_notpresent_ctx(self):
344        self.env.append(self.var_not_set, 'path')
345        with self.env(export=False) as env:
346            self.assertEqual(env[self.var_not_set], 'path')
347
348    def test_append_notpresent_written(self):
349        if not self.run_shell_tests:
350            return
351
352        self.env.append(self.var_not_set, 'path')
353        env = _evaluate_env_in_shell(self.env)
354        self.assertEqual(env[self.var_not_set], 'path')
355
356    def test_remove_ctx(self):
357        self.env.set(
358            self.var_not_set,
359            self.pathsep.join(('path', 'one', 'path', 'two', 'path')),
360        )
361
362        self.env.append(self.var_not_set, 'path')
363        with self.env(export=False) as env:
364            self.assertEqual(
365                env[self.var_not_set], self.pathsep.join(('one', 'two', 'path'))
366            )
367
368    def test_remove_written(self):
369        if not self.run_shell_tests:
370            return
371
372        if self.windows:
373            return
374
375        self.env.set(
376            self.var_not_set,
377            self.pathsep.join(('path', 'one', 'path', 'two', 'path')),
378        )
379
380        self.env.append(self.var_not_set, 'path')
381        env = _evaluate_env_in_shell(self.env)
382        self.assertEqual(
383            env[self.var_not_set], self.pathsep.join(('one', 'two', 'path'))
384        )
385
386    def test_remove_ctx_space(self):
387        self.env.set(
388            self.var_not_set,
389            self.pathsep.join(('pa th', 'one', 'pa th', 'two')),
390        )
391
392        self.env.append(self.var_not_set, 'pa th')
393        with self.env(export=False) as env:
394            self.assertEqual(
395                env[self.var_not_set],
396                self.pathsep.join(('one', 'two', 'pa th')),
397            )
398
399    def test_remove_written_space(self):
400        if not self.run_shell_tests:
401            return
402
403        if self.windows:
404            return
405
406        self.env.set(
407            self.var_not_set,
408            self.pathsep.join(('pa th', 'one', 'pa th', 'two')),
409        )
410
411        self.env.append(self.var_not_set, 'pa th')
412        env = _evaluate_env_in_shell(self.env)
413        self.assertEqual(
414            env[self.var_not_set], self.pathsep.join(('one', 'two', 'pa th'))
415        )
416
417    def test_remove_ctx_empty(self):
418        self.env.remove(self.var_not_set, 'path')
419        with self.env(export=False) as env:
420            self.assertNotIn(self.var_not_set, env)
421
422    def test_remove_written_empty(self):
423        if not self.run_shell_tests:
424            return
425
426        self.env.remove(self.var_not_set, 'path')
427        env = _evaluate_env_in_shell(self.env)
428        self.assertNotIn(self.var_not_set, env)
429
430
431class WindowsEnvironmentTest(
432    _PrependAppendEnvironmentTest, _AppendPrependTestMixin
433):
434    def __init__(self, *args, **kwargs):
435        kwargs['pathsep'] = ';'
436        kwargs['windows'] = True
437        kwargs['allcaps'] = True
438        super().__init__(*args, **kwargs)
439
440
441class PosixEnvironmentTest(
442    _PrependAppendEnvironmentTest, _AppendPrependTestMixin
443):
444    def __init__(self, *args, **kwargs):
445        kwargs['pathsep'] = ':'
446        kwargs['windows'] = False
447        kwargs['allcaps'] = False
448        super().__init__(*args, **kwargs)
449        self.real_windows = os.name == 'nt'
450
451
452class WindowsCaseInsensitiveTest(unittest.TestCase):
453    def test_lower_handling(self):
454        # This is only for testing case-handling on Windows. It doesn't make
455        # sense to run it on other systems.
456        if os.name != 'nt':
457            return
458
459        lower_var = 'lower_var'
460        upper_var = lower_var.upper()
461
462        if upper_var in os.environ:
463            del os.environ[upper_var]
464
465        self.assertNotIn(lower_var, os.environ)
466
467        env = environment.Environment()
468        env.append(lower_var, 'foo')
469        env.append(upper_var, 'bar')
470        with env(export=False) as env_:
471            self.assertNotIn(lower_var, env_)
472            self.assertIn(upper_var, env_)
473            self.assertEqual(env_[upper_var], 'foo;bar')
474
475
476if __name__ == '__main__':
477    import sys
478
479    logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
480    unittest.main()
481