diff --git a/tools/release.py b/tools/release.py index 0263d9a9e6f6ea9ca23cac69112e12a977b6b1df..689c1b26d8474c9356bcdfcf23d15b0c5eeb7728 100755 --- a/tools/release.py +++ b/tools/release.py @@ -207,7 +207,7 @@ def notary(): def staple(bundle): - for attempt in range(2): + for attempt in range(5): with tempfile.TemporaryFile(mode='w+') as log: try: try: @@ -219,9 +219,10 @@ def staple(bundle): print(diagnostic, end='', flush=True) except subprocess.CalledProcessError as error: # EX_NOHOST alone also means the bundle has never been notarized. - if attempt or error.returncode != 68 or not re.search( + if attempt == 4 or error.returncode != 68 or not re.search( r'^error is Error Domain=NSURLErrorDomain Code=-1005(?:\s|$)', diagnostic, re.M): raise + time.sleep(10 * 2 ** attempt) else: return diff --git a/tools/test_release.py b/tools/test_release.py index b3f6e5756bb0dd74b778eb2d432d02550a3edfd8..b49e1af7e5144c4e93668b52b93af025d6ace2b2 100644 --- a/tools/test_release.py +++ b/tools/test_release.py @@ -94,9 +94,9 @@ class ReleaseTest(unittest.TestCase): self.assertEqual(error.stderr, b'diagnostic') self.assertIn('codesign --verify Snowbound.app', printed.getvalue()) - def test_staple_retries_only_a_lost_connection_and_propagates_second_failure(self): + def test_staple_retries_only_a_lost_connection_and_propagates_last_failure(self): network = 'error is Error Domain=NSURLErrorDomain Code=-1005 "The network connection was lost."\n' - for failures in ([(68, network)], [(68, network), (68, network)], + for failures in ([(68, network)], [(68, network)] * 4, [(68, network)] * 5, [(68, 'The bundle has not been notarized.\n')], [(77, network)], [(68, network.replace('-1005', '-10050'))]): with self.subTest(failures=failures), tempfile.TemporaryDirectory() as folder: @@ -114,15 +114,18 @@ class ReleaseTest(unittest.TestCase): kwargs['stdout'].write('The staple and validate action worked!\n') printed = io.StringIO() - with patch.dict(release['staple'].__globals__, {'run': failing_tool}), redirect_stdout(printed): - if len(failures) == 1 and failures[0] == (68, network): + sleep = Mock() + with patch.dict(release['staple'].__globals__, {'run': failing_tool}), \ + patch.object(release['time'], 'sleep', sleep), redirect_stdout(printed): + if failures[0] == (68, network) and len(failures) < 5: self.assertIsNone(release['staple'](Path(folder) / 'Snowbound.app')) else: with self.assertRaises(subprocess.CalledProcessError) as raised: release['staple'](Path(folder) / 'Snowbound.app') self.assertIs(raised.exception, errors[-1]) - expected = 2 if failures[0] == (68, network) else 1 - self.assertEqual(len(calls), expected) + retried = failures[0] == (68, network) + self.assertEqual(len(calls), min(len(failures) + 1, 5) if retried else 1) + self.assertEqual([wait for (wait,), _ in sleep.call_args_list], [10, 20, 40, 80][:len(calls) - 1]) self.assertEqual(printed.getvalue(), ''.join(text for _, text in failures) + ('The staple and validate action worked!\n' if len(errors) < len(calls) else '')) for command, kwargs in calls: