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