| OLD | NEW |
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # Copyright 2013 The LUCI Authors. All rights reserved. | 2 # Copyright 2013 The LUCI Authors. All rights reserved. |
| 3 # Use of this source code is governed under the Apache License, Version 2.0 | 3 # Use of this source code is governed under the Apache License, Version 2.0 |
| 4 # that can be found in the LICENSE file. | 4 # that can be found in the LICENSE file. |
| 5 | 5 |
| 6 import datetime | 6 import datetime |
| 7 import json | 7 import json |
| 8 import logging | 8 import logging |
| 9 import os | 9 import os |
| 10 import re | 10 import re |
| 11 import StringIO | 11 import StringIO |
| 12 import subprocess | 12 import subprocess |
| 13 import sys | 13 import sys |
| 14 import tempfile | 14 import tempfile |
| 15 import threading | 15 import threading |
| 16 import time | 16 import time |
| 17 import traceback |
| 17 import unittest | 18 import unittest |
| 18 | 19 |
| 19 # net_utils adjusts sys.path. | 20 # net_utils adjusts sys.path. |
| 20 import net_utils | 21 import net_utils |
| 21 | 22 |
| 22 from depot_tools import auto_stub | 23 from depot_tools import auto_stub |
| 23 | 24 |
| 24 import auth | 25 import auth |
| 25 import isolateserver | 26 import isolateserver |
| 26 import swarming | 27 import swarming |
| (...skipping 68 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 95 'command': None, | 96 'command': None, |
| 96 'dimensions': [ | 97 'dimensions': [ |
| 97 {'key': 'foo', 'value': 'bar'}, | 98 {'key': 'foo', 'value': 'bar'}, |
| 98 {'key': 'os', 'value': 'Mac'}, | 99 {'key': 'os', 'value': 'Mac'}, |
| 99 ], | 100 ], |
| 100 'env': [], | 101 'env': [], |
| 101 'execution_timeout_secs': 60, | 102 'execution_timeout_secs': 60, |
| 102 'extra_args': ['--some-arg', '123'], | 103 'extra_args': ['--some-arg', '123'], |
| 103 'grace_period_secs': 30, | 104 'grace_period_secs': 30, |
| 104 'idempotent': False, | 105 'idempotent': False, |
| 105 'inputs_ref': None, | 106 'inputs_ref': { |
| 107 'isolated': None, |
| 108 'isolatedserver': '', |
| 109 'namespace': 'default-gzip', |
| 110 }, |
| 106 'io_timeout_secs': 60, | 111 'io_timeout_secs': 60, |
| 107 'outputs': [], | 112 'outputs': [], |
| 108 'secret_bytes': None, | 113 'secret_bytes': None, |
| 109 }, | 114 }, |
| 110 'tags': ['tag:a', 'tag:b'], | 115 'tags': ['tag:a', 'tag:b'], |
| 111 'user': 'joe@localhost', | 116 'user': 'joe@localhost', |
| 112 } | 117 } |
| 113 out.update(kwargs) | 118 out.update(kwargs) |
| 114 out['properties'].update(properties or {}) | 119 out['properties'].update(properties or {}) |
| 115 return out | 120 return out |
| (...skipping 113 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 229 | 234 |
| 230 def main_safe(self, args): | 235 def main_safe(self, args): |
| 231 """Bypasses swarming.main()'s exception handling. | 236 """Bypasses swarming.main()'s exception handling. |
| 232 | 237 |
| 233 It gets in the way when debugging test failures. | 238 It gets in the way when debugging test failures. |
| 234 """ | 239 """ |
| 235 # pylint: disable=bare-except | 240 # pylint: disable=bare-except |
| 236 try: | 241 try: |
| 237 return main(args) | 242 return main(args) |
| 238 except: | 243 except: |
| 239 logging.exception('Unexpected exception thrown in main') | 244 data = '%s\nSTDOUT:\n%s\nSTDERR:\n%s' % ( |
| 240 logging.error( | 245 traceback.format_exc(), sys.stdout.getvalue(), sys.stderr.getvalue()) |
| 241 'STDOUT:\n%s\nSTDERR:\n%s', | 246 self.fail(data) |
| 242 sys.stdout.getvalue(), sys.stderr.getvalue()) | |
| 243 self.fail() | |
| 244 | 247 |
| 245 | 248 |
| 246 class NetTestCase(net_utils.TestCase, Common): | 249 class NetTestCase(net_utils.TestCase, Common): |
| 247 """Base class that defines the url_open mock.""" | 250 """Base class that defines the url_open mock.""" |
| 248 def setUp(self): | 251 def setUp(self): |
| 249 net_utils.TestCase.setUp(self) | 252 net_utils.TestCase.setUp(self) |
| 250 Common.setUp(self) | 253 Common.setUp(self) |
| 251 self.mock(time, 'sleep', lambda _: None) | 254 self.mock(time, 'sleep', lambda _: None) |
| 252 self.mock(subprocess, 'call', lambda *_: self.fail()) | 255 self.mock(subprocess, 'call', lambda *_: self.fail()) |
| 253 self.mock(threading, 'Event', NonBlockingEvent) | 256 self.mock(threading, 'Event', NonBlockingEvent) |
| (...skipping 79 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 333 properties=swarming.TaskProperties( | 336 properties=swarming.TaskProperties( |
| 334 caches=[], | 337 caches=[], |
| 335 cipd_input=None, | 338 cipd_input=None, |
| 336 command=['a', 'b'], | 339 command=['a', 'b'], |
| 337 dimensions={'foo': 'bar', 'os': 'Mac'}, | 340 dimensions={'foo': 'bar', 'os': 'Mac'}, |
| 338 env={}, | 341 env={}, |
| 339 execution_timeout_secs=60, | 342 execution_timeout_secs=60, |
| 340 extra_args=[], | 343 extra_args=[], |
| 341 grace_period_secs=30, | 344 grace_period_secs=30, |
| 342 idempotent=False, | 345 idempotent=False, |
| 343 inputs_ref=None, | 346 inputs_ref={ |
| 347 'isolated': None, |
| 348 'isolatedserver': '', |
| 349 'namespace': 'default-gzip', |
| 350 }, |
| 344 io_timeout_secs=60, | 351 io_timeout_secs=60, |
| 345 outputs=[], | 352 outputs=[], |
| 346 secret_bytes=None), | 353 secret_bytes=None), |
| 347 service_account_token=None, | 354 service_account_token=None, |
| 348 tags=['tag:a', 'tag:b'], | 355 tags=['tag:a', 'tag:b'], |
| 349 user='joe@localhost') | 356 user='joe@localhost') |
| 350 | 357 |
| 351 request_1 = swarming.task_request_to_raw_request(task_request, False) | 358 request_1 = swarming.task_request_to_raw_request(task_request, False) |
| 352 request_1['name'] = u'unit_tests:0:2' | 359 request_1['name'] = u'unit_tests:0:2' |
| 353 request_1['properties']['env'] = [ | 360 request_1['properties']['env'] = [ |
| (...skipping 50 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 404 properties=swarming.TaskProperties( | 411 properties=swarming.TaskProperties( |
| 405 caches=[], | 412 caches=[], |
| 406 cipd_input=None, | 413 cipd_input=None, |
| 407 command=['a', 'b'], | 414 command=['a', 'b'], |
| 408 dimensions={'foo': 'bar', 'os': 'Mac'}, | 415 dimensions={'foo': 'bar', 'os': 'Mac'}, |
| 409 env={}, | 416 env={}, |
| 410 execution_timeout_secs=60, | 417 execution_timeout_secs=60, |
| 411 extra_args=[], | 418 extra_args=[], |
| 412 grace_period_secs=30, | 419 grace_period_secs=30, |
| 413 idempotent=False, | 420 idempotent=False, |
| 414 inputs_ref=None, | 421 inputs_ref={ |
| 422 'isolated': None, |
| 423 'isolatedserver': '', |
| 424 'namespace': 'default-gzip', |
| 425 }, |
| 415 io_timeout_secs=60, | 426 io_timeout_secs=60, |
| 416 outputs=[], | 427 outputs=[], |
| 417 secret_bytes=None), | 428 secret_bytes=None), |
| 418 service_account_token=None, | 429 service_account_token=None, |
| 419 tags=['tag:a', 'tag:b'], | 430 tags=['tag:a', 'tag:b'], |
| 420 user='joe@localhost') | 431 user='joe@localhost') |
| 421 | 432 |
| 422 request = swarming.task_request_to_raw_request(task_request, False) | 433 request = swarming.task_request_to_raw_request(task_request, False) |
| 423 self.assertEqual('123', request['parent_task_id']) | 434 self.assertEqual('123', request['parent_task_id']) |
| 424 | 435 |
| (...skipping 42 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 467 path='path/to/package', | 478 path='path/to/package', |
| 468 version='abc123')], | 479 version='abc123')], |
| 469 server=None), | 480 server=None), |
| 470 command=['a', 'b'], | 481 command=['a', 'b'], |
| 471 dimensions={'foo': 'bar', 'os': 'Mac'}, | 482 dimensions={'foo': 'bar', 'os': 'Mac'}, |
| 472 env={}, | 483 env={}, |
| 473 execution_timeout_secs=60, | 484 execution_timeout_secs=60, |
| 474 extra_args=[], | 485 extra_args=[], |
| 475 grace_period_secs=30, | 486 grace_period_secs=30, |
| 476 idempotent=False, | 487 idempotent=False, |
| 477 inputs_ref=None, | 488 inputs_ref={ |
| 489 'isolated': None, |
| 490 'isolatedserver': '', |
| 491 'namespace': 'default-gzip', |
| 492 }, |
| 478 io_timeout_secs=60, | 493 io_timeout_secs=60, |
| 479 outputs=[], | 494 outputs=[], |
| 480 secret_bytes=None), | 495 secret_bytes=None), |
| 481 service_account_token=None, | 496 service_account_token=None, |
| 482 tags=['tag:a', 'tag:b'], | 497 tags=['tag:a', 'tag:b'], |
| 483 user='joe@localhost') | 498 user='joe@localhost') |
| 484 | 499 |
| 485 request = swarming.task_request_to_raw_request(task_request, False) | 500 request = swarming.task_request_to_raw_request(task_request, False) |
| 486 expected = { | 501 expected = { |
| 487 'client_package': None, | 502 'client_package': None, |
| (...skipping 418 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 906 actual = sys.stdout.getvalue() | 921 actual = sys.stdout.getvalue() |
| 907 self.assertEqual(0, ret, (actual, sys.stderr.getvalue())) | 922 self.assertEqual(0, ret, (actual, sys.stderr.getvalue())) |
| 908 self._check_output( | 923 self._check_output( |
| 909 'Triggered task: None/foo=bar\n' | 924 'Triggered task: None/foo=bar\n' |
| 910 'To collect results, use:\n' | 925 'To collect results, use:\n' |
| 911 ' swarming.py collect -S https://localhost:1 12300\n' | 926 ' swarming.py collect -S https://localhost:1 12300\n' |
| 912 'Or visit:\n' | 927 'Or visit:\n' |
| 913 ' https://localhost:1/user/task/12300\n', | 928 ' https://localhost:1/user/task/12300\n', |
| 914 '') | 929 '') |
| 915 | 930 |
| 931 def test_run_raw_cmd_isolated(self): |
| 932 # Minimalist use. |
| 933 request = { |
| 934 'expiration_secs': 21600, |
| 935 'name': u'None/foo=bar/' + FILE_HASH, |
| 936 'parent_task_id': '', |
| 937 'priority': 100, |
| 938 'properties': { |
| 939 'caches': [], |
| 940 'cipd_input': None, |
| 941 'command': ['python', '-c', 'print(\'hi\')'], |
| 942 'dimensions': [ |
| 943 {'key': 'foo', 'value': 'bar'}, |
| 944 ], |
| 945 'env': [], |
| 946 'execution_timeout_secs': 3600, |
| 947 'extra_args': None, |
| 948 'grace_period_secs': 30, |
| 949 'idempotent': False, |
| 950 'inputs_ref': { |
| 951 'isolated': FILE_HASH, |
| 952 'isolatedserver': 'https://localhost:2', |
| 953 'namespace': 'default-gzip', |
| 954 }, |
| 955 'io_timeout_secs': 1200, |
| 956 'outputs': [], |
| 957 'secret_bytes': None, |
| 958 }, |
| 959 'tags': [], |
| 960 'user': None, |
| 961 } |
| 962 result = gen_request_response(request) |
| 963 self.expected_requests( |
| 964 [ |
| 965 ( |
| 966 'https://localhost:1/api/swarming/v1/tasks/new', |
| 967 {'data': request}, |
| 968 result, |
| 969 ), |
| 970 ]) |
| 971 ret = self.main_safe([ |
| 972 'trigger', |
| 973 '--swarming', 'https://localhost:1', |
| 974 '--dimension', 'foo', 'bar', |
| 975 '--raw-cmd', |
| 976 '--isolate-server', 'https://localhost:2', |
| 977 '--isolated', FILE_HASH, |
| 978 '--', |
| 979 'python', |
| 980 '-c', |
| 981 'print(\'hi\')', |
| 982 ]) |
| 983 actual = sys.stdout.getvalue() |
| 984 self.assertEqual(0, ret, (actual, sys.stderr.getvalue())) |
| 985 self._check_output( |
| 986 u'Triggered task: None/foo=bar/' + FILE_HASH + u'\n' |
| 987 u'To collect results, use:\n' |
| 988 u' swarming.py collect -S https://localhost:1 12300\n' |
| 989 u'Or visit:\n' |
| 990 u' https://localhost:1/user/task/12300\n', |
| 991 u'') |
| 992 |
| 916 def test_run_raw_cmd_with_service_account(self): | 993 def test_run_raw_cmd_with_service_account(self): |
| 917 # Minimalist use. | 994 # Minimalist use. |
| 918 request = { | 995 request = { |
| 919 'expiration_secs': 21600, | 996 'expiration_secs': 21600, |
| 920 'name': u'None/foo=bar', | 997 'name': u'None/foo=bar', |
| 921 'parent_task_id': '', | 998 'parent_task_id': '', |
| 922 'priority': 100, | 999 'priority': 100, |
| 923 'properties': { | 1000 'properties': { |
| 924 'caches': [], | 1001 'caches': [], |
| 925 'cipd_input': None, | 1002 'cipd_input': None, |
| (...skipping 46 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 972 '') | 1049 '') |
| 973 | 1050 |
| 974 def test_run_isolated_hash(self): | 1051 def test_run_isolated_hash(self): |
| 975 # pylint: disable=unused-argument | 1052 # pylint: disable=unused-argument |
| 976 self.mock(swarming, 'now', lambda: 123456) | 1053 self.mock(swarming, 'now', lambda: 123456) |
| 977 | 1054 |
| 978 request = gen_request_data( | 1055 request = gen_request_data( |
| 979 properties={ | 1056 properties={ |
| 980 'command': None, | 1057 'command': None, |
| 981 'inputs_ref': { | 1058 'inputs_ref': { |
| 982 'isolated': u'1111111111111111111111111111111111111111', | 1059 'isolated': FILE_HASH, |
| 983 'isolatedserver': 'https://localhost:2', | 1060 'isolatedserver': 'https://localhost:2', |
| 984 'namespace': 'default-gzip', | 1061 'namespace': 'default-gzip', |
| 985 }, | 1062 }, |
| 986 'secret_bytes': None, | 1063 'secret_bytes': None, |
| 987 }) | 1064 }) |
| 988 result = gen_request_response(request) | 1065 result = gen_request_response(request) |
| 989 self.expected_requests( | 1066 self.expected_requests( |
| 990 [ | 1067 [ |
| 991 ( | 1068 ( |
| 992 'https://localhost:1/api/swarming/v1/tasks/new', | 1069 'https://localhost:1/api/swarming/v1/tasks/new', |
| (...skipping 154 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1147 'client_package': None, | 1224 'client_package': None, |
| 1148 'packages': [{ | 1225 'packages': [{ |
| 1149 'package_name': 'super/awesome/pkg', | 1226 'package_name': 'super/awesome/pkg', |
| 1150 'path': 'path/to/pkg', | 1227 'path': 'path/to/pkg', |
| 1151 'version': 'version:42', | 1228 'version': 'version:42', |
| 1152 }], | 1229 }], |
| 1153 'server': None, | 1230 'server': None, |
| 1154 }, | 1231 }, |
| 1155 'command': None, | 1232 'command': None, |
| 1156 'inputs_ref': { | 1233 'inputs_ref': { |
| 1157 'isolated': u'1111111111111111111111111111111111111111', | 1234 'isolated': FILE_HASH, |
| 1158 'isolatedserver': 'https://localhost:2', | 1235 'isolatedserver': 'https://localhost:2', |
| 1159 'namespace': 'default-gzip', | 1236 'namespace': 'default-gzip', |
| 1160 }, | 1237 }, |
| 1161 'secret_bytes': None, | 1238 'secret_bytes': None, |
| 1162 }) | 1239 }) |
| 1163 result = gen_request_response(request) | 1240 result = gen_request_response(request) |
| 1164 self.expected_requests( | 1241 self.expected_requests( |
| 1165 [ | 1242 [ |
| 1166 ( | 1243 ( |
| 1167 'https://localhost:1/api/swarming/v1/tasks/new', | 1244 'https://localhost:1/api/swarming/v1/tasks/new', |
| (...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1205 main([ | 1282 main([ |
| 1206 'trigger', '--swarming', 'https://host', | 1283 'trigger', '--swarming', 'https://host', |
| 1207 '--isolate-server', 'https://host', '-T', 'foo', | 1284 '--isolate-server', 'https://host', '-T', 'foo', |
| 1208 '-d', 'os', 'amgia', | 1285 '-d', 'os', 'amgia', |
| 1209 ]) | 1286 ]) |
| 1210 self._check_output( | 1287 self._check_output( |
| 1211 '', | 1288 '', |
| 1212 'Usage: swarming.py trigger [options] (hash|isolated) ' | 1289 'Usage: swarming.py trigger [options] (hash|isolated) ' |
| 1213 '[-- extra_args|raw command]\n' | 1290 '[-- extra_args|raw command]\n' |
| 1214 '\n' | 1291 '\n' |
| 1215 'swarming.py: error: Use --isolated, --raw-cmd or \'--\' to pass ' | 1292 'swarming.py: error: Specify at least one of --raw-cmd or --isolated ' |
| 1216 'arguments to the called process.\n') | 1293 'or both\n') |
| 1217 | 1294 |
| 1218 def test_trigger_no_env_vars(self): | 1295 def test_trigger_no_env_vars(self): |
| 1219 with self.assertRaises(SystemExit): | 1296 with self.assertRaises(SystemExit): |
| 1220 main(['trigger']) | 1297 main(['trigger']) |
| 1221 self._check_output( | 1298 self._check_output( |
| 1222 '', | 1299 '', |
| 1223 'Usage: swarming.py trigger [options] (hash|isolated) ' | 1300 'Usage: swarming.py trigger [options] (hash|isolated) ' |
| 1224 '[-- extra_args|raw command]' | 1301 '[-- extra_args|raw command]' |
| 1225 '\n\n' | 1302 '\n\n' |
| 1226 'swarming.py: error: --swarming is required.' | 1303 'swarming.py: error: --swarming is required.' |
| (...skipping 13 matching lines...) Expand all Loading... |
| 1240 | 1317 |
| 1241 def test_trigger_no_isolate_server(self): | 1318 def test_trigger_no_isolate_server(self): |
| 1242 with self.assertRaises(SystemExit): | 1319 with self.assertRaises(SystemExit): |
| 1243 with test_utils.EnvVars({'SWARMING_SERVER': 'https://host'}): | 1320 with test_utils.EnvVars({'SWARMING_SERVER': 'https://host'}): |
| 1244 main(['trigger', 'foo.isolated', '-d', 'os', 'amiga']) | 1321 main(['trigger', 'foo.isolated', '-d', 'os', 'amiga']) |
| 1245 self._check_output( | 1322 self._check_output( |
| 1246 '', | 1323 '', |
| 1247 'Usage: swarming.py trigger [options] (hash|isolated) ' | 1324 'Usage: swarming.py trigger [options] (hash|isolated) ' |
| 1248 '[-- extra_args|raw command]' | 1325 '[-- extra_args|raw command]' |
| 1249 '\n\n' | 1326 '\n\n' |
| 1250 'swarming.py: error: --isolate-server is required.' | 1327 'swarming.py: error: Specify at least one of --raw-cmd or --isolated ' |
| 1251 '\n') | 1328 'or both\n') |
| 1252 | 1329 |
| 1253 def test_trigger_no_dimension(self): | 1330 def test_trigger_no_dimension(self): |
| 1254 with self.assertRaises(SystemExit): | 1331 with self.assertRaises(SystemExit): |
| 1255 main([ | 1332 main([ |
| 1256 'trigger', '--swarming', 'https://host', '--raw-cmd', '--', 'foo', | 1333 'trigger', '--swarming', 'https://host', '--raw-cmd', '--', 'foo', |
| 1257 ]) | 1334 ]) |
| 1258 self._check_output( | 1335 self._check_output( |
| 1259 '', | 1336 '', |
| 1260 'Usage: swarming.py trigger [options] (hash|isolated) ' | 1337 'Usage: swarming.py trigger [options] (hash|isolated) ' |
| 1261 '[-- extra_args|raw command]' | 1338 '[-- extra_args|raw command]' |
| (...skipping 21 matching lines...) Expand all Loading... |
| 1283 'dimensions': [ | 1360 'dimensions': [ |
| 1284 {'key': 'foo', 'value': 'bar'}, | 1361 {'key': 'foo', 'value': 'bar'}, |
| 1285 {'key': 'os', 'value': 'Mac'}, | 1362 {'key': 'os', 'value': 'Mac'}, |
| 1286 ], | 1363 ], |
| 1287 'env': [], | 1364 'env': [], |
| 1288 'execution_timeout_secs': 60, | 1365 'execution_timeout_secs': 60, |
| 1289 'extra_args': ['--some-arg', '123'], | 1366 'extra_args': ['--some-arg', '123'], |
| 1290 'grace_period_secs': 30, | 1367 'grace_period_secs': 30, |
| 1291 'idempotent': True, | 1368 'idempotent': True, |
| 1292 'inputs_ref': { | 1369 'inputs_ref': { |
| 1293 'isolated': '1'*40, | 1370 'isolated': FILE_HASH, |
| 1294 'isolatedserver': 'https://localhost:2', | 1371 'isolatedserver': 'https://localhost:2', |
| 1295 'namespace': 'default-gzip', | 1372 'namespace': 'default-gzip', |
| 1296 }, | 1373 }, |
| 1297 'io_timeout_secs': 60, | 1374 'io_timeout_secs': 60, |
| 1298 'secret_bytes': None, | 1375 'secret_bytes': None, |
| 1299 }, | 1376 }, |
| 1300 'tags': ['tag:a', 'tag:b'], | 1377 'tags': ['tag:a', 'tag:b'], |
| 1301 'user': 'joe@localhost', | 1378 'user': 'joe@localhost', |
| 1302 }, | 1379 }, |
| 1303 } | 1380 } |
| (...skipping 322 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1626 | 1703 |
| 1627 if __name__ == '__main__': | 1704 if __name__ == '__main__': |
| 1628 fix_encoding.fix_encoding() | 1705 fix_encoding.fix_encoding() |
| 1629 logging.basicConfig( | 1706 logging.basicConfig( |
| 1630 level=logging.DEBUG if '-v' in sys.argv else logging.CRITICAL) | 1707 level=logging.DEBUG if '-v' in sys.argv else logging.CRITICAL) |
| 1631 if '-v' in sys.argv: | 1708 if '-v' in sys.argv: |
| 1632 unittest.TestCase.maxDiff = None | 1709 unittest.TestCase.maxDiff = None |
| 1633 for e in ('ISOLATE_SERVER', 'SWARMING_TASK_ID', 'SWARMING_SERVER'): | 1710 for e in ('ISOLATE_SERVER', 'SWARMING_TASK_ID', 'SWARMING_SERVER'): |
| 1634 os.environ.pop(e, None) | 1711 os.environ.pop(e, None) |
| 1635 unittest.main() | 1712 unittest.main() |
| OLD | NEW |