diff --git a/.github/workflows/release-workflow-checks.yml b/.github/workflows/release-workflow-checks.yml index 1ca556c..3a18d7e 100644 --- a/.github/workflows/release-workflow-checks.yml +++ b/.github/workflows/release-workflow-checks.yml @@ -2,10 +2,8 @@ name: Validate release workflow on: push: branches: ['feature/**', 'master-1.12', 'master-1.12.2'] - paths: ['.github/workflows/**', '.github/scripts/**'] pull_request: branches: ['master-1.12'] - paths: ['.github/workflows/**', '.github/scripts/**'] permissions: contents: read jobs: diff --git a/tools/fixture_login.py b/tools/fixture_login.py index e83620f..a747166 100644 --- a/tools/fixture_login.py +++ b/tools/fixture_login.py @@ -27,13 +27,24 @@ def completed_client(home, role): if any(line==marker or line.endswith(': '+marker) for line in lines):return True return False +def planned_disconnect(home, role, phase): + expected={'8':'A','10':'B'} + if expected.get(phase)!=role:return False + log=home/('client'+role+'.log') + if not log.is_file():return False + marker='VISIBILITY_CLIENT_PHASE '+role+' '+phase + return any(line==marker or line.endswith(': '+marker) + for line in log.read_text(encoding='utf-8',errors='replace').splitlines()) + class LoginMonitor: def __init__(self, home): - self.home=Path(home);self.seen=0;self.counts={'A':0,'B':0} + self.home=Path(home);self.seen=0;self.counts={'A':0,'B':0};self.visibility_phase=None def poll(self): text=(self.home/'server.log').read_text(encoding='utf-8',errors='replace') lines=text[:text.rfind('\n')+1].splitlines() for line in lines[self.seen:]: + phase=re.search(r'(?:^|: )VISIBILITY_PHASE ([0-9]+)$',line) + if phase:self.visibility_phase=phase[1] joined=JOIN.fullmatch(line) if joined and joined[1]=='A' and joined[2]=='joined': (self.home/'sync/client-A-joined').touch() @@ -44,7 +55,7 @@ def poll(self): (self.home/'sync'/('login-retry-'+role)).write_text(str(self.counts[role])) elif 'lost connection: Internal Exception:' in line: reset=RESET.fullmatch(line) - if not reset or not completed_client(self.home,reset[1]): + if not reset or not (completed_client(self.home,reset[1]) or planned_disconnect(self.home,reset[1],self.visibility_phase)): raise RuntimeError('Unrecognised connection failure: '+line) self.seen=len(lines) diff --git a/tools/test_fixture_login.py b/tools/test_fixture_login.py index 4cc0725..9981515 100644 --- a/tools/test_fixture_login.py +++ b/tools/test_fixture_login.py @@ -14,6 +14,27 @@ def setUp(self): self.root=Path(self.temp.name);(self.root/'sync').mkdir() self.log=self.root/'server.log';self.log.write_text('') self.monitor=LoginMonitor(self.root) + def test_planned_owner_and_observer_disconnects_do_not_abort_reconnect(self): + for phase,role in [('8','A'),('10','B')]: + with self.subTest(phase=phase,role=role): + reset=FAILURE.replace("io.netty.handler.codec.EncoderException: java.io.IOException: Can't serialize unregistered packet",'io.netty.channel.unix.Errors$NativeIoException: syscall:read(..) failed: Connection reset by peer').replace('ContainerA','Container'+role) + (self.root/('client'+role+'.log')).write_text('VISIBILITY_CLIENT_PHASE '+role+' '+phase+'\n') + self.log.write_text('VISIBILITY_PHASE '+phase+'\n'+reset+'VISIBILITY_PHASE '+str(int(phase)+1)+'\n') + LoginMonitor(self.root).poll() + self.assertFalse((self.root/('sync/login-retry-'+role)).exists()) + def test_planned_disconnect_rejects_wrong_player_or_missing_client_phase(self): + reset=FAILURE.replace("io.netty.handler.codec.EncoderException: java.io.IOException: Can't serialize unregistered packet",'io.netty.channel.unix.Errors$NativeIoException: syscall:read(..) failed: Connection reset by peer') + self.log.write_text('VISIBILITY_PHASE 8\n'+reset) + with self.assertRaises(RuntimeError):LoginMonitor(self.root).poll() + (self.root/'clientA.log').write_text('VISIBILITY_CLIENT_PHASE A 8\n') + self.log.write_text('VISIBILITY_PHASE 10\n'+reset) + with self.assertRaises(RuntimeError):LoginMonitor(self.root).poll() + def test_old_disconnect_phase_cannot_explain_a_later_active_reset(self): + reset=FAILURE.replace("io.netty.handler.codec.EncoderException: java.io.IOException: Can't serialize unregistered packet",'io.netty.channel.unix.Errors$NativeIoException: syscall:read(..) failed: Connection reset by peer') + (self.root/'clientA.log').write_text('VISIBILITY_CLIENT_PHASE A 8\n') + self.log.write_text('VISIBILITY_PHASE 8\nVISIBILITY_PHASE 12\n'+reset) + with self.assertRaises(RuntimeError):LoginMonitor(self.root).poll() + def test_completed_respawn_client_may_close_its_transport(self): reset=FAILURE.replace("io.netty.handler.codec.EncoderException: java.io.IOException: Can't serialize unregistered packet",'io.netty.channel.unix.Errors$NativeIoException: syscall:read(..) failed: Connection reset by peer') self.log.write_text(reset)