@@ -530,5 +530,149 @@ def test_all_modified_workflows_pin_actions(self):
530530 self .assertRegex (use , r'@[0-9a-f]{40}$' )
531531
532532
533+ @unittest .skipIf (os .name == 'nt' , 'Darwin proof uses a POSIX shell' )
534+ class DarwinPeerAliasLifecycle (unittest .TestCase ):
535+ def execute (self , * , preexisting = False , failure = '' , signal_name = '' ):
536+ with tempfile .TemporaryDirectory () as directory :
537+ root = Path (directory )
538+ tools = root / 'tools'
539+ tools .mkdir ()
540+ (root / 'scripts' ).mkdir ()
541+ state = root / 'alias'
542+ if preexisting :
543+ state .touch ()
544+ capture = root / 'calls.jsonl'
545+ boundary = '''import json, os, pathlib, sys
546+ sys.excepthook = sys.__excepthook__
547+ def record(value):
548+ with open(os.environ['CAPTURE'], 'a') as stream:
549+ stream.write(json.dumps(value) + '\\ n')
550+ state = pathlib.Path(os.environ['ALIAS_STATE'])
551+ failure = os.environ['FAILURE']
552+ '''
553+ scripts = {
554+ 'ifconfig' : boundary + '''
555+ args = sys.argv[1:]
556+ record(['ifconfig'] + args)
557+ if args == ['lo0', 'inet']:
558+ if failure == 'query': sys.exit(43)
559+ print('lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST>')
560+ print(' inet 127.0.0.1 netmask 0xff000000')
561+ print(' inet 127.0.0.20 netmask 0xffffffff')
562+ if state.exists(): print(' inet 127.0.0.2 netmask 0xffffffff')
563+ elif args == ['lo0', 'inet', '127.0.0.2', 'netmask', '255.255.255.255', 'alias']:
564+ assert not state.exists(), 'preexisting alias must be preserved'
565+ if failure == 'create': sys.exit(47)
566+ state.touch()
567+ elif args == ['lo0', 'inet', '127.0.0.2', '-alias']:
568+ assert state.exists(), 'cleanup needs an owned alias'
569+ if 'cleanup' in failure: sys.exit(53)
570+ state.unlink()
571+ else:
572+ raise AssertionError(args)
573+ ''' ,
574+ 'sudo' : 'import os, sys\n os.execvp(sys.argv[1], sys.argv[1:])\n ' ,
575+ 'brew' : "print('/fixture/homebrew')\n " ,
576+ }
577+ for name , code in scripts .items ():
578+ tool = tools / name
579+ tool .write_text ('#!' + sys .executable + '\n ' + code )
580+ tool .chmod (0o755 )
581+ (tools / 'python3' ).symlink_to (sys .executable )
582+ # Execute the workflow's actual preflight Python, replacing only
583+ # the OS socket boundary so Linux cannot mask Darwin's bind refusal.
584+ (root / 'socket.py' ).write_text (boundary + '''
585+ class socket:
586+ def __enter__(self): return self
587+ def __exit__(self, *args): self.close()
588+ def close(self): pass
589+ def bind(self, address):
590+ record(['bind', *address])
591+ if address[0] == '127.0.0.2' and (not state.exists() or failure == 'preflight'):
592+ raise OSError(49, 'Cannot assign requested address')
593+ ''' )
594+ (root / 'scripts/run-v3-release-ci.py' ).write_text (boundary + '''
595+ import signal
596+ record(['proof'] + sys.argv[1:])
597+ assert state.exists(), 'proof requires the second peer source address'
598+ if os.environ['SIGNAL_NAME']:
599+ os.kill(os.getppid(), getattr(signal, os.environ['SIGNAL_NAME']))
600+ if 'proof' in failure: sys.exit(61)
601+ ''' )
602+ lane = workflow ('v3-release-acceptance.yml' )
603+ job = lane ['jobs' ]['darwin' ]
604+ step = next (step for step in job ['steps' ]
605+ if step .get ('name' ) == 'Complete native and installed TLS off/on proof' )
606+ env = dict (os .environ , PATH = str (tools ) + os .pathsep + os .environ ['PATH' ],
607+ PYTHONPATH = str (root ), CAPTURE = str (capture ), ALIAS_STATE = str (state ),
608+ FAILURE = failure , SIGNAL_NAME = signal_name , RUNNER_TEMP = str (root ),
609+ V3_CXX = '/selected/clang++' , V3_CC = '/selected/clang' , V3_PACKAGE_TOOLCHAIN = '/selected' )
610+ result = subprocess .run (workflow_shell (lane , job , step ) + [step ['run' ]], cwd = root ,
611+ env = env , capture_output = True , text = True , timeout = 10 )
612+ calls = [json .loads (line ) for line in capture .read_text ().splitlines ()] if capture .exists () else []
613+ return result , calls , state .exists ()
614+
615+ def assert_preflight (self , calls ):
616+ proof = next (i for i , call in enumerate (calls ) if call [0 ] == 'proof' )
617+ for address in ('127.0.0.1' , '127.0.0.2' ):
618+ self .assertIn (['bind' , address , 0 ], calls [:proof ])
619+
620+ def test_owned_alias_surrounds_proof_and_both_actual_bind_preflights (self ):
621+ result , calls , remains = self .execute ()
622+ self .assertEqual (result .returncode , 0 , result .stdout + result .stderr )
623+ self .assert_preflight (calls )
624+ self .assertEqual (calls [0 ], ['ifconfig' , 'lo0' , 'inet' ])
625+ self .assertEqual (calls [- 1 ], ['ifconfig' , 'lo0' , 'inet' , '127.0.0.2' , '-alias' ])
626+ self .assertFalse (remains )
627+ proof = next (call for call in calls if call [0 ] == 'proof' )
628+ self .assertIn ('/selected/clang++' , proof )
629+
630+ def test_preexisting_alias_survives_success_and_proof_failure (self ):
631+ for failure , status in (('' , 0 ), ('proof' , 61 )):
632+ with self .subTest (failure = failure ):
633+ result , calls , remains = self .execute (preexisting = True , failure = failure )
634+ self .assertEqual (result .returncode , status , result .stdout + result .stderr )
635+ self .assert_preflight (calls )
636+ self .assertEqual ([call for call in calls if call [0 ] == 'ifconfig' ], [['ifconfig' , 'lo0' , 'inet' ]])
637+ self .assertTrue (remains )
638+
639+ def test_setup_and_preflight_fail_visibly_before_proof (self ):
640+ for failure in ('query' , 'create' , 'preflight' ):
641+ with self .subTest (failure = failure ):
642+ result , calls , remains = self .execute (failure = failure )
643+ self .assertNotEqual (result .returncode , 0 )
644+ self .assertIn ('peer-policy' , result .stderr )
645+ self .assertFalse (any (call [0 ] == 'proof' for call in calls ))
646+ self .assertFalse (remains )
647+ cleanup = ['ifconfig' , 'lo0' , 'inet' , '127.0.0.2' , '-alias' ]
648+ self .assertEqual (cleanup in calls , failure == 'preflight' )
649+
650+ def test_preexisting_alias_survives_failed_bind_preflight (self ):
651+ result , calls , remains = self .execute (preexisting = True , failure = 'preflight' )
652+ self .assertNotEqual (result .returncode , 0 )
653+ self .assertIn ('peer-policy' , result .stderr )
654+ self .assertFalse (any (call [0 ] == 'proof' for call in calls ))
655+ self .assertEqual ([call for call in calls if call [0 ] == 'ifconfig' ], [['ifconfig' , 'lo0' , 'inet' ]])
656+ self .assertTrue (remains )
657+
658+ def test_owned_alias_is_cleaned_after_proof_failure_and_signals (self ):
659+ for failure , signal_name , status in (('proof' , '' , 61 ), ('' , 'SIGHUP' , 129 ),
660+ ('' , 'SIGINT' , 130 ), ('' , 'SIGTERM' , 143 )):
661+ with self .subTest (failure = failure , signal = signal_name ):
662+ result , calls , remains = self .execute (failure = failure , signal_name = signal_name )
663+ self .assertEqual (result .returncode , status , result .stdout + result .stderr )
664+ self .assertEqual (calls [- 1 ], ['ifconfig' , 'lo0' , 'inet' , '127.0.0.2' , '-alias' ])
665+ self .assertFalse (remains )
666+
667+ def test_cleanup_failure_is_visible_and_preserves_prior_failure (self ):
668+ for failure , status in (('cleanup' , 1 ), ('proof-cleanup' , 61 )):
669+ with self .subTest (failure = failure ):
670+ result , calls , remains = self .execute (failure = failure )
671+ self .assertEqual (result .returncode , status , result .stdout + result .stderr )
672+ self .assertIn ('peer-policy' , result .stderr )
673+ self .assertEqual (calls [- 1 ], ['ifconfig' , 'lo0' , 'inet' , '127.0.0.2' , '-alias' ])
674+ self .assertTrue (remains )
675+
676+
533677if __name__ == '__main__' :
534678 unittest .main ()
0 commit comments