|
3 | 3 | from unittest.mock import patch |
4 | 4 |
|
5 | 5 | import gget.gget_pineapple as gget_pineapple |
| 6 | +import requests |
6 | 7 | from gget.gget_pineapple import ( |
7 | 8 | _catalog_row, |
8 | 9 | _parse_gdrive_form, |
@@ -87,5 +88,104 @@ def test_download_invokes_gdrive(self, mock_dl): |
87 | 88 | self.assertEqual(df.iloc[0]["name"], "vicar_2021") |
88 | 89 |
|
89 | 90 |
|
| 91 | +class TestPineappleLiveAccess(unittest.TestCase): |
| 92 | + """Live data test: verify EVERY curated Pineapple resource is still |
| 93 | + downloadable from Google Drive (issue #161). |
| 94 | +
|
| 95 | + This hits Google Drive over the network but never downloads the (multi-GB) |
| 96 | + bodies. Each file ID is resolved through the *same* production code path as |
| 97 | + a real download (``_resolve_gdrive_response``, including the large-file |
| 98 | + virus-scan-warning confirmation), and only the response headers are checked, |
| 99 | + so the whole catalog can be swept cheaply. |
| 100 | +
|
| 101 | + Purpose: if someone edits the catalog, or upstream repoints/removes a file, |
| 102 | + this fails loudly -- the Google-Drive-reported filename must still match the |
| 103 | + catalog and the ID must still resolve to a binary download rather than an |
| 104 | + error/quota HTML page. |
| 105 | + """ |
| 106 | + |
| 107 | + _CATALOGS = { |
| 108 | + "segmentation": gget_pineapple._SEGMENTATION, |
| 109 | + "benchmark": gget_pineapple._BENCHMARK, |
| 110 | + "weights": gget_pineapple._WEIGHTS, |
| 111 | + } |
| 112 | + |
| 113 | + # 1 MB floor: catches an ID repointed to a tiny placeholder/error file. |
| 114 | + # NOT tied to the catalog's size_gb, which is only approximate (e.g. |
| 115 | + # livecell_2021 lists 3.26 GB but the real file is ~1.81 GB). |
| 116 | + _MIN_BYTES = 1_000_000 |
| 117 | + |
| 118 | + def _assert_resource_headers(self, response, expected, name): |
| 119 | + """Header-only assertions for a resolved resource (no body download).""" |
| 120 | + self.assertEqual(response.status_code, 200, f"{name}: unexpected status code") |
| 121 | + self.assertIn( |
| 122 | + "octet-stream", |
| 123 | + response.headers.get("Content-Type", ""), |
| 124 | + f"{name}: expected a binary download, got Content-Type " |
| 125 | + f"{response.headers.get('Content-Type', '')!r}", |
| 126 | + ) |
| 127 | + disposition = response.headers.get("Content-Disposition", "") |
| 128 | + self.assertIn( |
| 129 | + f'filename="{expected["filename"]}"', |
| 130 | + disposition, |
| 131 | + f"{name}: Google Drive filename does not match the catalog " |
| 132 | + f"(expected {expected['filename']!r}, Content-Disposition={disposition!r}). " |
| 133 | + f"The file ID may have been repointed upstream.", |
| 134 | + ) |
| 135 | + length = response.headers.get("Content-Length") |
| 136 | + self.assertIsNotNone(length, f"{name}: response is missing Content-Length") |
| 137 | + self.assertGreater( |
| 138 | + int(length), |
| 139 | + self._MIN_BYTES, |
| 140 | + f"{name}: file is implausibly small ({length} bytes) -- possible placeholder", |
| 141 | + ) |
| 142 | + |
| 143 | + def test_live_all_resources_accessible(self): |
| 144 | + session = requests.Session() |
| 145 | + session.headers.update({"User-Agent": "Mozilla/5.0 (compatible; gget)"}) |
| 146 | + |
| 147 | + total = sum(len(cat) for cat in self._CATALOGS.values()) |
| 148 | + verified = 0 |
| 149 | + unavailable = [] # (name, reason) -- transient: throttling / network, not a failure |
| 150 | + |
| 151 | + for category, catalog in self._CATALOGS.items(): |
| 152 | + for name, info in catalog.items(): |
| 153 | + expected = _catalog_row(category, name, info) |
| 154 | + with self.subTest(category=category, name=name): |
| 155 | + try: |
| 156 | + response = gget_pineapple._resolve_gdrive_response( |
| 157 | + session, expected["google_drive_id"] |
| 158 | + ) |
| 159 | + except requests.RequestException as exc: |
| 160 | + unavailable.append((name, f"network error: {exc}")) |
| 161 | + continue |
| 162 | + try: |
| 163 | + # Google Drive serves a transient HTML "download quota |
| 164 | + # exceeded" page under load. Treat as unavailable (not a |
| 165 | + # failure) and move on; a genuinely missing file would have |
| 166 | + # raised a 404/410 in _resolve_gdrive_response above. |
| 167 | + if "text/html" in response.headers.get("Content-Type", ""): |
| 168 | + unavailable.append((name, "Google Drive HTML page (quota throttling?)")) |
| 169 | + continue |
| 170 | + self._assert_resource_headers(response, expected, name) |
| 171 | + verified += 1 |
| 172 | + finally: |
| 173 | + response.close() |
| 174 | + |
| 175 | + # Only skip when the ENTIRE sweep was transiently unavailable (throttling / |
| 176 | + # network) -- never when a real assertion failed (those are already recorded |
| 177 | + # as subTest failures and must keep the build red). |
| 178 | + if verified == 0 and len(unavailable) == total: |
| 179 | + self.skipTest( |
| 180 | + f"Could not verify any of the {total} resources (all transiently " |
| 181 | + f"unavailable): {unavailable}" |
| 182 | + ) |
| 183 | + if unavailable: |
| 184 | + print( |
| 185 | + f"\n[pineapple live] verified {verified}/{total}; " |
| 186 | + f"{len(unavailable)} transiently unavailable: {unavailable}" |
| 187 | + ) |
| 188 | + |
| 189 | + |
90 | 190 | if __name__ == "__main__": |
91 | 191 | unittest.main() |
0 commit comments