diff --git a/.github/labview/test-dashboard-api.py b/.github/labview/test-dashboard-api.py new file mode 100644 index 00000000..9ff9f7cc --- /dev/null +++ b/.github/labview/test-dashboard-api.py @@ -0,0 +1,48 @@ +import ast +import http.client +import io +import json +import pathlib +import sys +import time +import unittest +import urllib.error +import urllib.request +from unittest.mock import patch + + +ROOT = pathlib.Path(__file__).resolve().parents[2] +SOURCE = ROOT / 'actions' / 'dashboard' / 'dashboard.py' +TREE = ast.parse(SOURCE.read_text(encoding='utf-8')) +FUNCTIONS = [ + node for node in TREE.body + if isinstance(node, ast.FunctionDef) and node.name in {'_retry_delay', 'gh_get'} +] +DASHBOARD = { + 'json': json, + 'repo': 'owner/repo', + 'sys': sys, + 'time': time, + 'token': 'test-token', + 'urllib': urllib, + '_API': {'degraded': False}, +} +exec(compile(ast.Module(body=FUNCTIONS, type_ignores=[]), str(SOURCE), 'exec'), DASHBOARD) + + +class DashboardApiTests(unittest.TestCase): + def test_remote_disconnect_retries_and_returns_response(self): + response = io.StringIO('{"ok": true}') + with patch('urllib.request.urlopen', + side_effect=[http.client.RemoteDisconnected('connection dropped'), response]) as request: + with patch('time.sleep') as sleep: + result = DASHBOARD['gh_get']('commits/sha', _tries=2) + + self.assertEqual(result, {'ok': True}) + self.assertEqual(request.call_count, 2) + sleep.assert_called_once_with(1) + self.assertFalse(DASHBOARD['_API']['degraded']) + + +if __name__ == '__main__': + unittest.main() diff --git a/.github/pages/documentation.html b/.github/pages/documentation.html index 3dc1c028..3d439557 100644 --- a/.github/pages/documentation.html +++ b/.github/pages/documentation.html @@ -740,7 +740,8 @@

9. The dashboard & Pages site only project revisions — until it has collected the project's revisions or reaches a large safety cap on how many commits it will classify. Reading that history is resilient to a transient GitHub API failure: rate-limited - (HTTP 403/429), server, and network errors are retried with backoff + (HTTP 403/429), server, and network errors (including dropped connections) + are retried with backoff (honoring Retry-After/X-RateLimit-Reset), and if a call still fails so the revision list would be incomplete, the build refuses to publish and exits — the previous good dashboard stays live and rebuilds automatically once the API recovers, so a burst of diff --git a/actions/dashboard/dashboard.py b/actions/dashboard/dashboard.py index 96330f34..36f72265 100644 --- a/actions/dashboard/dashboard.py +++ b/actions/dashboard/dashboard.py @@ -62,7 +62,7 @@ def gh_get(path, _tries=4): if transient: _API['degraded'] = True return None - except (urllib.error.URLError, TimeoutError) as e: + except (urllib.error.URLError, OSError) as e: if attempt < n_tries - 1: time.sleep(2 ** attempt) continue diff --git a/actions/dashboard/documentation.html b/actions/dashboard/documentation.html index 3dc1c028..3d439557 100644 --- a/actions/dashboard/documentation.html +++ b/actions/dashboard/documentation.html @@ -740,7 +740,8 @@

9. The dashboard & Pages site only project revisions — until it has collected the project's revisions or reaches a large safety cap on how many commits it will classify. Reading that history is resilient to a transient GitHub API failure: rate-limited - (HTTP 403/429), server, and network errors are retried with backoff + (HTTP 403/429), server, and network errors (including dropped connections) + are retried with backoff (honoring Retry-After/X-RateLimit-Reset), and if a call still fails so the revision list would be incomplete, the build refuses to publish and exits — the previous good dashboard stays live and rebuilds automatically once the API recovers, so a burst of