1*90c8c64dSAndroid Build Coastguard Worker# 2*90c8c64dSAndroid Build Coastguard Worker# Copyright (C) 2023 The Android Open Source Project 3*90c8c64dSAndroid Build Coastguard Worker# 4*90c8c64dSAndroid Build Coastguard Worker# Licensed under the Apache License, Version 2.0 (the "License"); 5*90c8c64dSAndroid Build Coastguard Worker# you may not use this file except in compliance with the License. 6*90c8c64dSAndroid Build Coastguard Worker# You may obtain a copy of the License at 7*90c8c64dSAndroid Build Coastguard Worker# 8*90c8c64dSAndroid Build Coastguard Worker# http://www.apache.org/licenses/LICENSE-2.0 9*90c8c64dSAndroid Build Coastguard Worker# 10*90c8c64dSAndroid Build Coastguard Worker# Unless required by applicable law or agreed to in writing, software 11*90c8c64dSAndroid Build Coastguard Worker# distributed under the License is distributed on an "AS IS" BASIS, 12*90c8c64dSAndroid Build Coastguard Worker# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13*90c8c64dSAndroid Build Coastguard Worker# See the License for the specific language governing permissions and 14*90c8c64dSAndroid Build Coastguard Worker# limitations under the License. 15*90c8c64dSAndroid Build Coastguard Worker# 16*90c8c64dSAndroid Build Coastguard Worker"""Tests for fetchartifact.""" 17*90c8c64dSAndroid Build Coastguard Workerfrom typing import cast 18*90c8c64dSAndroid Build Coastguard Worker 19*90c8c64dSAndroid Build Coastguard Workerimport pytest 20*90c8c64dSAndroid Build Coastguard Workerfrom aiohttp import ClientResponseError, ClientSession 21*90c8c64dSAndroid Build Coastguard Workerfrom aiohttp.test_utils import TestClient 22*90c8c64dSAndroid Build Coastguard Workerfrom aiohttp.web import Application, Request, Response 23*90c8c64dSAndroid Build Coastguard Worker 24*90c8c64dSAndroid Build Coastguard Workerfrom fetchartifact import ArtifactDownloader, fetch_artifact, fetch_artifact_chunked 25*90c8c64dSAndroid Build Coastguard Worker 26*90c8c64dSAndroid Build Coastguard WorkerTEST_BUILD_ID = "1234" 27*90c8c64dSAndroid Build Coastguard WorkerTEST_TARGET = "linux" 28*90c8c64dSAndroid Build Coastguard WorkerTEST_ARTIFACT_NAME = "output.zip" 29*90c8c64dSAndroid Build Coastguard WorkerTEST_DOWNLOAD_URL = ( 30*90c8c64dSAndroid Build Coastguard Worker f"/android/internal/build/v3/builds/{TEST_BUILD_ID}/{TEST_TARGET}/" 31*90c8c64dSAndroid Build Coastguard Worker f"attempts/latest/artifacts/{TEST_ARTIFACT_NAME}/url" 32*90c8c64dSAndroid Build Coastguard Worker) 33*90c8c64dSAndroid Build Coastguard WorkerTEST_RESPONSE = b"Hello, world!" 34*90c8c64dSAndroid Build Coastguard Worker 35*90c8c64dSAndroid Build Coastguard Worker 36*90c8c64dSAndroid Build Coastguard Worker@pytest.fixture(name="android_ci_client") 37*90c8c64dSAndroid Build Coastguard Workerasync def fixture_android_ci_client(aiohttp_client: type[TestClient]) -> TestClient: 38*90c8c64dSAndroid Build Coastguard Worker """Fixture for mocking the Android CI APIs.""" 39*90c8c64dSAndroid Build Coastguard Worker 40*90c8c64dSAndroid Build Coastguard Worker async def download(_request: Request) -> Response: 41*90c8c64dSAndroid Build Coastguard Worker return Response(text=TEST_RESPONSE.decode("utf-8")) 42*90c8c64dSAndroid Build Coastguard Worker 43*90c8c64dSAndroid Build Coastguard Worker app = Application() 44*90c8c64dSAndroid Build Coastguard Worker app.router.add_get(TEST_DOWNLOAD_URL, download) 45*90c8c64dSAndroid Build Coastguard Worker return await aiohttp_client(app) # type: ignore 46*90c8c64dSAndroid Build Coastguard Worker 47*90c8c64dSAndroid Build Coastguard Worker 48*90c8c64dSAndroid Build Coastguard Workerasync def test_fetch_artifact(android_ci_client: TestClient) -> None: 49*90c8c64dSAndroid Build Coastguard Worker """Tests that the download URL is queried.""" 50*90c8c64dSAndroid Build Coastguard Worker assert TEST_RESPONSE == await fetch_artifact( 51*90c8c64dSAndroid Build Coastguard Worker TEST_TARGET, 52*90c8c64dSAndroid Build Coastguard Worker TEST_BUILD_ID, 53*90c8c64dSAndroid Build Coastguard Worker TEST_ARTIFACT_NAME, 54*90c8c64dSAndroid Build Coastguard Worker cast(ClientSession, android_ci_client), 55*90c8c64dSAndroid Build Coastguard Worker query_url_base="", 56*90c8c64dSAndroid Build Coastguard Worker ) 57*90c8c64dSAndroid Build Coastguard Worker 58*90c8c64dSAndroid Build Coastguard Worker 59*90c8c64dSAndroid Build Coastguard Workerasync def test_fetch_artifact_chunked(android_ci_client: TestClient) -> None: 60*90c8c64dSAndroid Build Coastguard Worker """Tests that the full file contents are downloaded.""" 61*90c8c64dSAndroid Build Coastguard Worker assert [c.encode("utf-8") for c in TEST_RESPONSE.decode("utf-8")] == [ 62*90c8c64dSAndroid Build Coastguard Worker chunk 63*90c8c64dSAndroid Build Coastguard Worker async for chunk in fetch_artifact_chunked( 64*90c8c64dSAndroid Build Coastguard Worker TEST_TARGET, 65*90c8c64dSAndroid Build Coastguard Worker TEST_BUILD_ID, 66*90c8c64dSAndroid Build Coastguard Worker TEST_ARTIFACT_NAME, 67*90c8c64dSAndroid Build Coastguard Worker cast(ClientSession, android_ci_client), 68*90c8c64dSAndroid Build Coastguard Worker chunk_size=1, 69*90c8c64dSAndroid Build Coastguard Worker query_url_base="", 70*90c8c64dSAndroid Build Coastguard Worker ) 71*90c8c64dSAndroid Build Coastguard Worker ] 72*90c8c64dSAndroid Build Coastguard Worker 73*90c8c64dSAndroid Build Coastguard Worker 74*90c8c64dSAndroid Build Coastguard Workerasync def test_failure_raises(android_ci_client: TestClient) -> None: 75*90c8c64dSAndroid Build Coastguard Worker """Tests that fetch failure raises an exception.""" 76*90c8c64dSAndroid Build Coastguard Worker with pytest.raises(ClientResponseError): 77*90c8c64dSAndroid Build Coastguard Worker await fetch_artifact( 78*90c8c64dSAndroid Build Coastguard Worker TEST_TARGET, 79*90c8c64dSAndroid Build Coastguard Worker TEST_BUILD_ID, 80*90c8c64dSAndroid Build Coastguard Worker TEST_ARTIFACT_NAME, 81*90c8c64dSAndroid Build Coastguard Worker cast(ClientSession, android_ci_client), 82*90c8c64dSAndroid Build Coastguard Worker query_url_base="/bad", 83*90c8c64dSAndroid Build Coastguard Worker ) 84*90c8c64dSAndroid Build Coastguard Worker 85*90c8c64dSAndroid Build Coastguard Worker with pytest.raises(ClientResponseError): 86*90c8c64dSAndroid Build Coastguard Worker async for _chunk in fetch_artifact_chunked( 87*90c8c64dSAndroid Build Coastguard Worker TEST_TARGET, 88*90c8c64dSAndroid Build Coastguard Worker TEST_BUILD_ID, 89*90c8c64dSAndroid Build Coastguard Worker TEST_ARTIFACT_NAME, 90*90c8c64dSAndroid Build Coastguard Worker cast(ClientSession, android_ci_client), 91*90c8c64dSAndroid Build Coastguard Worker query_url_base="/bad", 92*90c8c64dSAndroid Build Coastguard Worker ): 93*90c8c64dSAndroid Build Coastguard Worker pass 94*90c8c64dSAndroid Build Coastguard Worker 95*90c8c64dSAndroid Build Coastguard Worker 96*90c8c64dSAndroid Build Coastguard Workerclass TestDownloader(ArtifactDownloader): 97*90c8c64dSAndroid Build Coastguard Worker """Downloader which tracks calls to on_artifact_size and after_chunk.""" 98*90c8c64dSAndroid Build Coastguard Worker 99*90c8c64dSAndroid Build Coastguard Worker def __init__(self, target: str, build_id: str, artifact_name: str) -> None: 100*90c8c64dSAndroid Build Coastguard Worker super().__init__(target, build_id, artifact_name, query_url_base="") 101*90c8c64dSAndroid Build Coastguard Worker self.reported_content_length: int | None = None 102*90c8c64dSAndroid Build Coastguard Worker self.reported_chunk_sizes: list[int] = [] 103*90c8c64dSAndroid Build Coastguard Worker 104*90c8c64dSAndroid Build Coastguard Worker def on_artifact_size(self, size: int) -> None: 105*90c8c64dSAndroid Build Coastguard Worker super().on_artifact_size(size) 106*90c8c64dSAndroid Build Coastguard Worker assert self.reported_content_length is None 107*90c8c64dSAndroid Build Coastguard Worker self.reported_content_length = size 108*90c8c64dSAndroid Build Coastguard Worker 109*90c8c64dSAndroid Build Coastguard Worker def after_chunk(self, size: int) -> None: 110*90c8c64dSAndroid Build Coastguard Worker super().after_chunk(size) 111*90c8c64dSAndroid Build Coastguard Worker self.reported_chunk_sizes.append(size) 112*90c8c64dSAndroid Build Coastguard Worker 113*90c8c64dSAndroid Build Coastguard Worker 114*90c8c64dSAndroid Build Coastguard Workerasync def test_downloader_progress_reports(android_ci_client: TestClient) -> None: 115*90c8c64dSAndroid Build Coastguard Worker """Tests that progress is reported when using ArtifactDownloader.""" 116*90c8c64dSAndroid Build Coastguard Worker downloader = TestDownloader(TEST_TARGET, TEST_BUILD_ID, TEST_ARTIFACT_NAME) 117*90c8c64dSAndroid Build Coastguard Worker 118*90c8c64dSAndroid Build Coastguard Worker assert [b"Hell", b"o, w", b"orld", b"!"] == [ 119*90c8c64dSAndroid Build Coastguard Worker chunk 120*90c8c64dSAndroid Build Coastguard Worker async for chunk in downloader.download( 121*90c8c64dSAndroid Build Coastguard Worker cast(ClientSession, android_ci_client), chunk_size=4 122*90c8c64dSAndroid Build Coastguard Worker ) 123*90c8c64dSAndroid Build Coastguard Worker ] 124*90c8c64dSAndroid Build Coastguard Worker assert downloader.reported_content_length == len(TEST_RESPONSE.decode("utf-8")) 125*90c8c64dSAndroid Build Coastguard Worker assert downloader.reported_chunk_sizes == [4, 4, 4, 1] 126*90c8c64dSAndroid Build Coastguard Worker 127*90c8c64dSAndroid Build Coastguard Worker 128*90c8c64dSAndroid Build Coastguard Worker@pytest.mark.requires_network 129*90c8c64dSAndroid Build Coastguard Workerasync def test_real_artifact() -> None: 130*90c8c64dSAndroid Build Coastguard Worker """Tests with a real artifact. Requires an internet connection.""" 131*90c8c64dSAndroid Build Coastguard Worker async with ClientSession() as session: 132*90c8c64dSAndroid Build Coastguard Worker contents = await fetch_artifact("linux", "9945621", "logs/SUCCEEDED", session) 133*90c8c64dSAndroid Build Coastguard Worker assert contents == b"1681499053\n" 134