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