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