blob: 6b12b99e467b7fb478b4d01ef84316134b644993 [file] [log] [blame]
# Copyright 2020 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
"""Tests for env_setup.environment.
This tests the error-checking, context manager, and written environment scripts
of the Environment class.
Tests that end in "_ctx" modify the environment and validate it in-process.
Tests that end in "_written" write the environment to a file intended to be
evaluated by the shell, then launches the shell and then saves the environment.
This environment is then validated in the test process.
"""
import logging
import os
import subprocess
import tempfile
import unittest
import six
from pw_env_setup import environment
# pylint: disable=super-with-arguments
class WrittenEnvFailure(Exception):
pass
def _evaluate_env_in_shell(env):
"""Write env to a file then evaluate and save the resulting environment.
Write env to a file, then launch a shell command that sources that file
and dumps the environment to stdout. Parse that output into a dict and
return it.
Args:
env(environment.Environment): environment to write out
Returns dictionary of resulting environment.
"""
# Write env sourcing script to file.
with tempfile.NamedTemporaryFile(
prefix='pw-test-written-env-',
suffix='.bat' if os.name == 'nt' else '.sh',
delete=False,
mode='w+') as temp:
env.write(temp)
temp_name = temp.name
# Evaluate env sourcing script and capture output of 'env'.
if os.name == 'nt':
# On Windows you just run batch files and they modify your
# environment, no need to call 'source' or '.'.
cmd = '{} && set'.format(temp_name)
else:
# Using '.' instead of 'source' because 'source' is not POSIX.
cmd = '. {} && env'.format(temp_name)
res = subprocess.run(cmd, capture_output=True, shell=True)
if res.returncode:
raise WrittenEnvFailure(res.stderr)
# Parse environment from stdout of subprocess.
env_ret = {}
for line in res.stdout.splitlines():
line = line.decode()
# Some people inexplicably have newlines in some of their
# environment variables. This module does not allow that so we can
# ignore any such extra lines.
if '=' not in line:
continue
var, value = line.split('=', 1)
env_ret[var] = value
return env_ret
# pylint: disable=too-many-public-methods
class EnvironmentTest(unittest.TestCase):
"""Tests for env_setup.environment."""
def setUp(self):
self.env = environment.Environment()
# Name of a variable that is already set when the test starts.
self.var_already_set = self.env.normalize_key('var_already_set')
os.environ[self.var_already_set] = 'orig value'
self.assertIn(self.var_already_set, os.environ)
# Name of a variable that is not set when the test starts.
self.var_not_set = self.env.normalize_key('var_not_set')
if self.var_not_set in os.environ:
del os.environ[self.var_not_set]
self.assertNotIn(self.var_not_set, os.environ)
self.orig_env = os.environ.copy()
def tearDown(self):
self.assertEqual(os.environ, self.orig_env)
def test_set_notpresent_ctx(self):
self.env.set(self.var_not_set, '1')
with self.env(export=False) as env:
self.assertIn(self.var_not_set, env)
self.assertEqual(env[self.var_not_set], '1')
def test_set_notpresent_written(self):
self.env.set(self.var_not_set, '1')
env = _evaluate_env_in_shell(self.env)
self.assertIn(self.var_not_set, env)
self.assertEqual(env[self.var_not_set], '1')
def test_set_present_ctx(self):
self.env.set(self.var_already_set, '1')
with self.env(export=False) as env:
self.assertIn(self.var_already_set, env)
self.assertEqual(env[self.var_already_set], '1')
def test_set_present_written(self):
self.env.set(self.var_already_set, '1')
env = _evaluate_env_in_shell(self.env)
self.assertIn(self.var_already_set, env)
self.assertEqual(env[self.var_already_set], '1')
def test_clear_notpresent_ctx(self):
self.env.clear(self.var_not_set)
with self.env(export=False) as env:
self.assertNotIn(self.var_not_set, env)
def test_clear_notpresent_written(self):
self.env.clear(self.var_not_set)
env = _evaluate_env_in_shell(self.env)
self.assertNotIn(self.var_not_set, env)
def test_clear_present_ctx(self):
self.env.clear(self.var_already_set)
with self.env(export=False) as env:
self.assertNotIn(self.var_already_set, env)
def test_clear_present_written(self):
self.env.clear(self.var_already_set)
env = _evaluate_env_in_shell(self.env)
self.assertNotIn(self.var_already_set, env)
def test_value_replacement(self):
self.env.set(self.var_not_set, '/foo/bar/baz')
self.env.add_replacement('FOOBAR', '/foo/bar')
buf = six.StringIO()
self.env.write(buf)
assert '/foo/bar' not in buf.getvalue()
def test_variable_replacement(self):
self.env.set('FOOBAR', '/foo/bar')
self.env.set(self.var_not_set, '/foo/bar/baz')
self.env.add_replacement('FOOBAR')
buf = six.StringIO()
self.env.write(buf)
print(buf.getvalue())
assert '/foo/bar/baz' not in buf.getvalue()
def test_nonglobal(self):
self.env.set(self.var_not_set, '1')
with self.env(export=False) as env:
self.assertIn(self.var_not_set, env)
self.assertNotIn(self.var_not_set, os.environ)
def test_global(self):
self.env.set(self.var_not_set, '1')
with self.env(export=True) as env:
self.assertIn(self.var_not_set, env)
self.assertIn(self.var_not_set, os.environ)
def test_set_badnametype(self):
with self.assertRaises(environment.BadNameType):
self.env.set(123, '123')
def test_set_badvaluetype(self):
with self.assertRaises(environment.BadValueType):
self.env.set('var', 123)
def test_prepend_badnametype(self):
with self.assertRaises(environment.BadNameType):
self.env.prepend(123, '123')
def test_prepend_badvaluetype(self):
with self.assertRaises(environment.BadValueType):
self.env.prepend('var', 123)
def test_append_badnametype(self):
with self.assertRaises(environment.BadNameType):
self.env.append(123, '123')
def test_append_badvaluetype(self):
with self.assertRaises(environment.BadValueType):
self.env.append('var', 123)
def test_set_badname_empty(self):
with self.assertRaises(environment.BadVariableName):
self.env.set('', '123')
def test_set_badname_digitstart(self):
with self.assertRaises(environment.BadVariableName):
self.env.set('123', '123')
def test_set_badname_equals(self):
with self.assertRaises(environment.BadVariableName):
self.env.set('foo=bar', '123')
def test_set_badname_period(self):
with self.assertRaises(environment.BadVariableName):
self.env.set('abc.def', '123')
def test_set_badname_hyphen(self):
with self.assertRaises(environment.BadVariableName):
self.env.set('abc-def', '123')
def test_set_empty_value(self):
with self.assertRaises(environment.EmptyValue):
self.env.set('var', '')
def test_set_newline_in_value(self):
with self.assertRaises(environment.NewlineInValue):
self.env.set('var', '123\n456')
def test_equal_sign_in_value(self):
with self.assertRaises(environment.BadVariableValue):
self.env.append(self.var_already_set, 'pa=th')
class _PrependAppendEnvironmentTest(unittest.TestCase):
"""Tests for env_setup.environment."""
def __init__(self, *args, **kwargs):
windows = kwargs.pop('windows', False)
pathsep = kwargs.pop('pathsep', os.pathsep)
allcaps = kwargs.pop('allcaps', False)
super(_PrependAppendEnvironmentTest, self).__init__(*args, **kwargs)
self.windows = windows
self.pathsep = pathsep
self.allcaps = allcaps
# If we're testing Windows behavior and actually running on Windows,
# actually launch a subprocess to evaluate the shell init script.
# Likewise if we're testing POSIX behavior and actually on a POSIX
# system. Tests can check self.run_shell_tests and exit without
# doing anything.
real_windows = (os.name == 'nt')
self.run_shell_tests = (self.windows == real_windows)
def setUp(self):
self.env = environment.Environment(windows=self.windows,
pathsep=self.pathsep,
allcaps=self.allcaps)
self.var_already_set = self.env.normalize_key('VAR_ALREADY_SET')
os.environ[self.var_already_set] = self.pathsep.join(
'one two three'.split())
self.assertIn(self.var_already_set, os.environ)
self.var_not_set = self.env.normalize_key('VAR_NOT_SET')
if self.var_not_set in os.environ:
del os.environ[self.var_not_set]
self.assertNotIn(self.var_not_set, os.environ)
self.orig_env = os.environ.copy()
def split(self, val):
return val.split(self.pathsep)
def tearDown(self):
self.assertEqual(os.environ, self.orig_env)
# TODO(mohrr) remove disable=useless-object-inheritance once in Python 3.
# pylint: disable=useless-object-inheritance
class _AppendPrependTestMixin(object):
def test_prepend_present_ctx(self):
orig = os.environ[self.var_already_set]
self.env.prepend(self.var_already_set, 'path')
with self.env(export=False) as env:
self.assertEqual(env[self.var_already_set],
self.pathsep.join(('path', orig)))
def test_prepend_present_written(self):
if not self.run_shell_tests:
return
orig = os.environ[self.var_already_set]
self.env.prepend(self.var_already_set, 'path')
env = _evaluate_env_in_shell(self.env)
self.assertEqual(env[self.var_already_set],
self.pathsep.join(('path', orig)))
def test_prepend_notpresent_ctx(self):
self.env.prepend(self.var_not_set, 'path')
with self.env(export=False) as env:
self.assertEqual(env[self.var_not_set], 'path')
def test_prepend_notpresent_written(self):
if not self.run_shell_tests:
return
self.env.prepend(self.var_not_set, 'path')
env = _evaluate_env_in_shell(self.env)
self.assertEqual(env[self.var_not_set], 'path')
def test_append_present_ctx(self):
orig = os.environ[self.var_already_set]
self.env.append(self.var_already_set, 'path')
with self.env(export=False) as env:
self.assertEqual(env[self.var_already_set],
self.pathsep.join((orig, 'path')))
def test_append_present_written(self):
if not self.run_shell_tests:
return
orig = os.environ[self.var_already_set]
self.env.append(self.var_already_set, 'path')
env = _evaluate_env_in_shell(self.env)
self.assertEqual(env[self.var_already_set],
self.pathsep.join((orig, 'path')))
def test_append_notpresent_ctx(self):
self.env.append(self.var_not_set, 'path')
with self.env(export=False) as env:
self.assertEqual(env[self.var_not_set], 'path')
def test_append_notpresent_written(self):
if not self.run_shell_tests:
return
self.env.append(self.var_not_set, 'path')
env = _evaluate_env_in_shell(self.env)
self.assertEqual(env[self.var_not_set], 'path')
def test_remove_ctx(self):
self.env.set(self.var_not_set,
self.pathsep.join(('path', 'one', 'path', 'two', 'path')))
self.env.append(self.var_not_set, 'path')
with self.env(export=False) as env:
self.assertEqual(env[self.var_not_set],
self.pathsep.join(('one', 'two', 'path')))
def test_remove_written(self):
if not self.run_shell_tests:
return
if self.windows: # TODO(pwbug/231) Re-enable for Windows.
return
self.env.set(self.var_not_set,
self.pathsep.join(('path', 'one', 'path', 'two', 'path')))
self.env.append(self.var_not_set, 'path')
env = _evaluate_env_in_shell(self.env)
self.assertEqual(env[self.var_not_set],
self.pathsep.join(('one', 'two', 'path')))
def test_remove_ctx_space(self):
self.env.set(self.var_not_set,
self.pathsep.join(('pa th', 'one', 'pa th', 'two')))
self.env.append(self.var_not_set, 'pa th')
with self.env(export=False) as env:
self.assertEqual(env[self.var_not_set],
self.pathsep.join(('one', 'two', 'pa th')))
def test_remove_written_space(self):
if not self.run_shell_tests:
return
if self.windows: # TODO(pwbug/231) Re-enable for Windows.
return
self.env.set(self.var_not_set,
self.pathsep.join(('pa th', 'one', 'pa th', 'two')))
self.env.append(self.var_not_set, 'pa th')
env = _evaluate_env_in_shell(self.env)
self.assertEqual(env[self.var_not_set],
self.pathsep.join(('one', 'two', 'pa th')))
def test_remove_ctx_empty(self):
self.env.remove(self.var_not_set, 'path')
with self.env(export=False) as env:
self.assertNotIn(self.var_not_set, env)
def test_remove_written_empty(self):
if not self.run_shell_tests:
return
self.env.remove(self.var_not_set, 'path')
env = _evaluate_env_in_shell(self.env)
self.assertNotIn(self.var_not_set, env)
class WindowsEnvironmentTest(_PrependAppendEnvironmentTest,
_AppendPrependTestMixin):
def __init__(self, *args, **kwargs):
kwargs['pathsep'] = ';'
kwargs['windows'] = True
kwargs['allcaps'] = True
super(WindowsEnvironmentTest, self).__init__(*args, **kwargs)
class PosixEnvironmentTest(_PrependAppendEnvironmentTest,
_AppendPrependTestMixin):
def __init__(self, *args, **kwargs):
kwargs['pathsep'] = ':'
kwargs['windows'] = False
kwargs['allcaps'] = False
super(PosixEnvironmentTest, self).__init__(*args, **kwargs)
self.real_windows = (os.name == 'nt')
class WindowsCaseInsensitiveTest(unittest.TestCase):
def test_lower_handling(self):
# This is only for testing case-handling on Windows. It doesn't make
# sense to run it on other systems.
if os.name != 'nt':
return
lower_var = 'lower_var'
upper_var = lower_var.upper()
if upper_var in os.environ:
del os.environ[upper_var]
self.assertNotIn(lower_var, os.environ)
env = environment.Environment()
env.append(lower_var, 'foo')
env.append(upper_var, 'bar')
with env(export=False) as env_:
self.assertNotIn(lower_var, env_)
self.assertIn(upper_var, env_)
self.assertEqual(env_[upper_var], 'foo;bar')
if __name__ == '__main__':
import sys
logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
unittest.main()