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