55# pylint:disable=no-name-in-module
66# pylint:disable=too-many-nested-blocks
77
8- import asyncio
9- import logging
108import sys
119from collections .abc import Awaitable , Callable
1210from pathlib import Path
13- from typing import Any , Literal
11+ from typing import Literal
1412
1513import httpx
1614import pytest
1715from faker import Faker
1816from fastapi import FastAPI
1917from models_library .api_schemas_storage .storage_schemas import (
2018 FileMetaDataGet ,
21- FoldersBody ,
2219)
2320from models_library .basic_types import SHA256Str
2421from models_library .projects import ProjectID
25- from models_library .projects_nodes_io import NodeID , NodeIDStr , SimcoreS3FileID
22+ from models_library .projects_nodes_io import SimcoreS3FileID
2623from models_library .users import UserID
2724from models_library .utils .fastapi_encoders import jsonable_encoder
2825from pydantic import ByteSize , TypeAdapter
2926from pytest_simcore .helpers .fastapi import url_from_operation_id
3027from pytest_simcore .helpers .httpx_assert_checks import assert_status
31- from pytest_simcore .helpers .logging_tools import log_context
32- from pytest_simcore .helpers .storage_utils import (
33- FileIDDict ,
34- ProjectWithFilesParams ,
35- get_updated_project ,
36- )
37- from pytest_simcore .helpers .storage_utils_file_meta_data import (
38- assert_file_meta_data_in_db ,
39- )
40- from pytest_simcore .helpers .storage_utils_project import clone_project_data
4128from servicelib .aiohttp import status
42- from servicelib .fastapi .long_running_tasks .client import long_running_task_request
4329from settings_library .s3 import S3Settings
4430from simcore_service_storage .models import SearchFilesQueryParams
4531from simcore_service_storage .simcore_s3_dsm import SimcoreS3DataManager
46- from sqlalchemy .ext .asyncio import AsyncEngine
47- from yarl import URL
4832
4933pytest_simcore_core_services_selection = ["postgres" , "rabbit" ]
5034pytest_simcore_ops_services_selection = ["adminer" ]
5337CURRENT_DIR = Path (sys .argv [0 ] if __name__ == "__main__" else __file__ ).resolve ().parent
5438
5539
56- @pytest .fixture
57- def mock_datcore_download (mocker , client ):
58- # Use to mock downloading from DATCore
59- async def _fake_download_to_file_or_raise (session , url , dest_path ):
60- with log_context (logging .INFO , f"Faking download: { url } -> { dest_path } " ):
61- Path (dest_path ).write_text (
62- "FAKE: test_create_and_delete_folders_from_project"
63- )
64-
65- mocker .patch (
66- "simcore_service_storage.simcore_s3_dsm.download_to_file_or_raise" ,
67- side_effect = _fake_download_to_file_or_raise ,
68- autospec = True ,
69- )
70-
71- mocker .patch (
72- "simcore_service_storage.simcore_s3_dsm.datcore_adapter.get_file_download_presigned_link" ,
73- autospec = True ,
74- return_value = URL ("https://httpbin.org/image" ),
75- )
76-
77-
7840async def test_simcore_s3_access_returns_default (
7941 initialized_app : FastAPI , client : httpx .AsyncClient
8042):
@@ -88,202 +50,6 @@ async def test_simcore_s3_access_returns_default(
8850 assert received_settings
8951
9052
91- async def _request_copy_folders (
92- initialized_app : FastAPI ,
93- client : httpx .AsyncClient ,
94- user_id : UserID ,
95- source_project : dict [str , Any ],
96- dst_project : dict [str , Any ],
97- nodes_map : dict [NodeID , NodeID ],
98- ) -> dict [str , Any ]:
99- url = url_from_operation_id (
100- client , initialized_app , "copy_folders_from_project"
101- ).with_query (user_id = user_id )
102-
103- with log_context (
104- logging .INFO ,
105- f"Copying folders from { source_project ['uuid' ]} to { dst_project ['uuid' ]} " ,
106- ) as ctx :
107- async for lr_task in long_running_task_request (
108- client ,
109- url ,
110- json = jsonable_encoder (
111- FoldersBody (
112- source = source_project , destination = dst_project , nodes_map = nodes_map
113- )
114- ),
115- ):
116- ctx .logger .info ("%s" , f"<-- current state is { lr_task .progress = } " )
117- if lr_task .done ():
118- return await lr_task .result ()
119-
120- pytest .fail (reason = "Copy folders failed!" )
121-
122-
123- @pytest .mark .parametrize (
124- "location_id" ,
125- [SimcoreS3DataManager .get_location_id ()],
126- ids = [SimcoreS3DataManager .get_location_name ()],
127- indirect = True ,
128- )
129- @pytest .mark .parametrize (
130- "project_params" ,
131- [
132- ProjectWithFilesParams (
133- num_nodes = 12 ,
134- allowed_file_sizes = (
135- TypeAdapter (ByteSize ).validate_python ("7Mib" ),
136- TypeAdapter (ByteSize ).validate_python ("110Mib" ),
137- TypeAdapter (ByteSize ).validate_python ("1Mib" ),
138- ),
139- allowed_file_checksums = (
140- TypeAdapter (SHA256Str ).validate_python (
141- "311e2e130d83cfea9c3b7560699c221b0b7f9e5d58b02870bd52b695d8b4aabd"
142- ),
143- TypeAdapter (SHA256Str ).validate_python (
144- "08e297db979d3c84f6b072c2a1e269e8aa04e82714ca7b295933a0c9c0f62b2e"
145- ),
146- TypeAdapter (SHA256Str ).validate_python (
147- "488f3b57932803bbf644593bd46d95599b1d4da1d63bc020d7ebe6f1c255f7f3"
148- ),
149- ),
150- workspace_files_count = 0 ,
151- ),
152- ],
153- ids = str ,
154- )
155- async def test_copy_folders_from_valid_project (
156- short_dsm_cleaner_interval : int ,
157- initialized_app : FastAPI ,
158- client : httpx .AsyncClient ,
159- user_id : UserID ,
160- create_project : Callable [[], Awaitable [dict [str , Any ]]],
161- create_simcore_file_id : Callable [[ProjectID , NodeID , str ], SimcoreS3FileID ],
162- sqlalchemy_async_engine : AsyncEngine ,
163- random_project_with_files : Callable [
164- [ProjectWithFilesParams ],
165- Awaitable [
166- tuple [dict [str , Any ], dict [NodeID , dict [SimcoreS3FileID , FileIDDict ]]]
167- ],
168- ],
169- project_params : ProjectWithFilesParams ,
170- ):
171- # 1. create a src project with some files
172- src_project , src_projects_list = await random_project_with_files (project_params )
173- # 2. create a dst project without files
174- dst_project , nodes_map = clone_project_data (src_project )
175- dst_project = await create_project (** dst_project )
176- # copy the project files
177- data = await _request_copy_folders (
178- initialized_app ,
179- client ,
180- user_id ,
181- src_project ,
182- dst_project ,
183- nodes_map = {NodeID (i ): NodeID (j ) for i , j in nodes_map .items ()},
184- )
185- assert data == jsonable_encoder (
186- await get_updated_project (sqlalchemy_async_engine , dst_project ["uuid" ])
187- )
188-
189- # check that file meta data was effectively copied
190- for src_node_id in src_projects_list :
191- dst_node_id = nodes_map .get (
192- TypeAdapter (NodeIDStr ).validate_python (f"{ src_node_id } " )
193- )
194- assert dst_node_id
195- for src_file_id , src_file in src_projects_list [src_node_id ].items ():
196- path : Any = src_file ["path" ]
197- assert isinstance (path , Path )
198- checksum : Any = src_file ["sha256_checksum" ]
199- assert isinstance (checksum , str )
200- await assert_file_meta_data_in_db (
201- sqlalchemy_async_engine ,
202- file_id = TypeAdapter (SimcoreS3FileID ).validate_python (
203- f"{ src_file_id } " .replace (
204- f"{ src_project ['uuid' ]} " , dst_project ["uuid" ]
205- ).replace (f"{ src_node_id } " , f"{ dst_node_id } " )
206- ),
207- expected_entry_exists = True ,
208- expected_file_size = path .stat ().st_size ,
209- expected_upload_id = None ,
210- expected_upload_expiration_date = None ,
211- expected_sha256_checksum = TypeAdapter (SHA256Str ).validate_python (
212- checksum
213- ),
214- )
215-
216-
217- async def _create_and_delete_folders_from_project (
218- user_id : UserID ,
219- project : dict [str , Any ],
220- initialized_app : FastAPI ,
221- client : httpx .AsyncClient ,
222- project_db_creator : Callable ,
223- check_list_files : bool ,
224- ) -> None :
225- destination_project , nodes_map = clone_project_data (project )
226- await project_db_creator (** destination_project )
227-
228- # creating a copy
229- data = await _request_copy_folders (
230- initialized_app ,
231- client ,
232- user_id ,
233- project ,
234- destination_project ,
235- nodes_map = {NodeID (i ): NodeID (j ) for i , j in nodes_map .items ()},
236- )
237-
238- # data should be equal to the destination project, and all store entries should point to simcore.s3
239- # NOTE: data is jsonized where destination project is not!
240- assert jsonable_encoder (destination_project ) == data
241-
242- project_id = data ["uuid" ]
243-
244- # list data to check all is here
245-
246- if check_list_files :
247- url = url_from_operation_id (
248- client ,
249- initialized_app ,
250- "list_files_metadata" ,
251- location_id = f"{ SimcoreS3DataManager .get_location_id ()} " ,
252- ).with_query (user_id = f"{ user_id } " , uuid_filter = f"{ project_id } " )
253-
254- resp = await client .get (f"{ url } " )
255- data , error = assert_status (resp , status .HTTP_200_OK , list [FileMetaDataGet ])
256- assert not error
257- # DELETING
258- url = url_from_operation_id (
259- client ,
260- initialized_app ,
261- "delete_folders_of_project" ,
262- folder_id = project_id ,
263- ).with_query (user_id = f"{ user_id } " )
264- resp = await client .delete (f"{ url } " )
265- assert_status (resp , status .HTTP_204_NO_CONTENT , None )
266-
267- # list data is gone
268- if check_list_files :
269- url = url_from_operation_id (
270- client ,
271- initialized_app ,
272- "list_files_metadata" ,
273- location_id = f"{ SimcoreS3DataManager .get_location_id ()} " ,
274- ).with_query (user_id = f"{ user_id } " , uuid_filter = f"{ project_id } " )
275- resp = await client .get (f"{ url } " )
276- data , error = assert_status (resp , status .HTTP_200_OK , list [FileMetaDataGet ])
277- assert not error
278- assert not data
279-
280-
281- @pytest .fixture
282- def set_log_levels_for_noisy_libraries () -> None :
283- # Reduce the log level for 'werkzeug'
284- logging .getLogger ("werkzeug" ).setLevel (logging .WARNING )
285-
286-
28753async def test_connect_to_external (
28854 set_log_levels_for_noisy_libraries : None ,
28955 initialized_app : FastAPI ,
@@ -302,101 +68,6 @@ async def test_connect_to_external(
30268 print (data )
30369
30470
305- @pytest .mark .parametrize (
306- "location_id" ,
307- [SimcoreS3DataManager .get_location_id ()],
308- ids = [SimcoreS3DataManager .get_location_name ()],
309- indirect = True ,
310- )
311- @pytest .mark .parametrize (
312- "project_params" ,
313- [
314- ProjectWithFilesParams (
315- num_nodes = 3 ,
316- allowed_file_sizes = (
317- TypeAdapter (ByteSize ).validate_python ("7Mib" ),
318- TypeAdapter (ByteSize ).validate_python ("110Mib" ),
319- TypeAdapter (ByteSize ).validate_python ("1Mib" ),
320- ),
321- workspace_files_count = 0 ,
322- )
323- ],
324- )
325- async def test_create_and_delete_folders_from_project (
326- set_log_levels_for_noisy_libraries : None ,
327- initialized_app : FastAPI ,
328- client : httpx .AsyncClient ,
329- user_id : UserID ,
330- create_project : Callable [..., Awaitable [dict [str , Any ]]],
331- with_random_project_with_files : tuple [
332- dict [str , Any ],
333- dict [NodeID , dict [SimcoreS3FileID , dict [str , Path | str ]]],
334- ],
335- mock_datcore_download ,
336- ):
337- project_in_db , _ = with_random_project_with_files
338- await _create_and_delete_folders_from_project (
339- user_id ,
340- project_in_db ,
341- initialized_app ,
342- client ,
343- create_project ,
344- check_list_files = True ,
345- )
346-
347-
348- @pytest .mark .flaky (max_runs = 3 )
349- @pytest .mark .parametrize (
350- "location_id" ,
351- [SimcoreS3DataManager .get_location_id ()],
352- ids = [SimcoreS3DataManager .get_location_name ()],
353- indirect = True ,
354- )
355- @pytest .mark .parametrize (
356- "project_params" ,
357- [
358- ProjectWithFilesParams (
359- num_nodes = 3 ,
360- allowed_file_sizes = (
361- TypeAdapter (ByteSize ).validate_python ("7Mib" ),
362- TypeAdapter (ByteSize ).validate_python ("110Mib" ),
363- TypeAdapter (ByteSize ).validate_python ("1Mib" ),
364- ),
365- workspace_files_count = 0 ,
366- )
367- ],
368- )
369- @pytest .mark .parametrize ("num_concurrent_calls" , [50 ])
370- async def test_create_and_delete_folders_from_project_burst (
371- set_log_levels_for_noisy_libraries : None ,
372- initialized_app : FastAPI ,
373- client : httpx .AsyncClient ,
374- user_id : UserID ,
375- with_random_project_with_files : tuple [
376- dict [str , Any ],
377- dict [NodeID , dict [SimcoreS3FileID , dict [str , Path | str ]]],
378- ],
379- create_project : Callable [..., Awaitable [dict [str , Any ]]],
380- mock_datcore_download ,
381- num_concurrent_calls : int ,
382- ):
383- project_in_db , _ = with_random_project_with_files
384- # NOTE: here the point is to NOT have a limit on the number of calls!!
385- await asyncio .gather (
386- * [
387- _create_and_delete_folders_from_project (
388- user_id ,
389- project_in_db ,
390- initialized_app ,
391- client ,
392- create_project ,
393- check_list_files = False ,
394- )
395- for _ in range (num_concurrent_calls )
396- ]
397- )
398-
399-
40071@pytest .fixture
40172async def uploaded_file_ids (
40273 faker : Faker ,
0 commit comments