Skip to content

Commit 36d2eb8

Browse files
committed
pytest-simcore
1 parent 849e05e commit 36d2eb8

File tree

3 files changed

+47
-153
lines changed

3 files changed

+47
-153
lines changed

packages/pytest-simcore/src/pytest_simcore/helpers/storage_utils.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import logging
22
import os
3+
from dataclasses import dataclass
34
from pathlib import Path
45
from typing import Any, TypedDict
56

67
import sqlalchemy as sa
8+
from faker import Faker
79
from models_library.basic_types import SHA256Str
10+
from pydantic import ByteSize
811
from simcore_postgres_database.storage_models import projects
912
from sqlalchemy.ext.asyncio import AsyncEngine
1013

@@ -33,3 +36,21 @@ async def get_updated_project(
3336
class FileIDDict(TypedDict):
3437
path: Path
3538
sha256_checksum: SHA256Str
39+
40+
41+
@dataclass(frozen=True, kw_only=True, slots=True)
42+
class ProjectWithFilesParams:
43+
num_nodes: int
44+
allowed_file_sizes: tuple[ByteSize, ...]
45+
workspace_files_count: int
46+
allowed_file_checksums: tuple[SHA256Str, ...] = None # type: ignore # NOTE: OK for testing
47+
48+
def __post_init__(self):
49+
if self.allowed_file_checksums is None:
50+
# generate some random checksums for the corresponding file sizes
51+
faker = Faker()
52+
checksums = tuple(faker.sha256() for _ in self.allowed_file_sizes)
53+
object.__setattr__(self, "allowed_file_checksums", checksums)
54+
55+
def __repr__(self) -> str:
56+
return f"ProjectWithFilesParams: #nodes={self.num_nodes}, file sizes={[_.human_readable() for _ in self.allowed_file_sizes]}"

packages/pytest-simcore/src/pytest_simcore/simcore_storage_data_models.py

Lines changed: 3 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,23 @@
22
# pylint: disable=unused-argument
33
# pylint: disable=unused-variable
44

5-
from collections import deque
65
from collections.abc import AsyncIterator, Awaitable, Callable
76
from contextlib import asynccontextmanager
8-
from pathlib import Path
9-
from random import choice, randint
10-
from typing import Any, cast
7+
from typing import Any
118

129
import pytest
1310
import sqlalchemy as sa
1411
from faker import Faker
15-
from models_library.basic_types import SHA256Str
1612
from models_library.projects import ProjectID
17-
from models_library.projects_nodes_io import NodeID, SimcoreS3FileID, StorageFileID
13+
from models_library.projects_nodes_io import NodeID
1814
from models_library.users import UserID
19-
from pydantic import ByteSize, TypeAdapter
20-
from servicelib.utils import limited_gather
15+
from pydantic import TypeAdapter
2116
from simcore_postgres_database.models.project_to_groups import project_to_groups
2217
from simcore_postgres_database.storage_models import projects, users
2318
from sqlalchemy.dialects.postgresql import insert as pg_insert
2419
from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine
2520

2621
from .helpers.faker_factories import random_project, random_user
27-
from .helpers.storage_utils import FileIDDict, get_updated_project
2822

2923

3024
@asynccontextmanager
@@ -257,134 +251,3 @@ async def _creator(
257251
return new_node_id
258252

259253
return _creator
260-
261-
262-
async def _upload_file_and_update_project(
263-
project_id: ProjectID,
264-
node_id: NodeID,
265-
*,
266-
file_name: str | None,
267-
file_id: StorageFileID | None,
268-
file_sizes: tuple[ByteSize, ...],
269-
file_checksums: tuple[SHA256Str, ...],
270-
node_to_files_mapping: dict[NodeID, dict[SimcoreS3FileID, FileIDDict]],
271-
upload_file: Callable[..., Awaitable[tuple[Path, SimcoreS3FileID]]],
272-
create_simcore_file_id: Callable[
273-
[ProjectID, NodeID, str, Path | None], SimcoreS3FileID
274-
],
275-
faker: Faker,
276-
) -> None:
277-
if file_name is None:
278-
file_name = faker.file_name()
279-
file_id = create_simcore_file_id(project_id, node_id, file_name, None)
280-
checksum: SHA256Str = choice(file_checksums) # noqa: S311
281-
src_file, _ = await upload_file(
282-
file_size=choice(file_sizes), # noqa: S311
283-
file_name=file_name,
284-
file_id=file_id,
285-
sha256_checksum=checksum,
286-
)
287-
assert file_name is not None
288-
assert file_id is not None
289-
node_to_files_mapping[node_id][file_id] = {
290-
"path": src_file,
291-
"sha256_checksum": checksum,
292-
}
293-
294-
295-
@pytest.fixture
296-
async def random_project_with_files(
297-
sqlalchemy_async_engine: AsyncEngine,
298-
create_project: Callable[..., Awaitable[dict[str, Any]]],
299-
create_project_node: Callable[..., Awaitable[NodeID]],
300-
create_simcore_file_id: Callable[
301-
[ProjectID, NodeID, str, Path | None], SimcoreS3FileID
302-
],
303-
upload_file: Callable[..., Awaitable[tuple[Path, SimcoreS3FileID]]],
304-
faker: Faker,
305-
) -> Callable[
306-
[int, tuple[ByteSize, ...], tuple[SHA256Str, ...]],
307-
Awaitable[tuple[dict[str, Any], dict[NodeID, dict[SimcoreS3FileID, FileIDDict]]]],
308-
]:
309-
async def _creator(
310-
num_nodes: int = 12,
311-
file_sizes: tuple[ByteSize, ...] = (
312-
TypeAdapter(ByteSize).validate_python("7Mib"),
313-
TypeAdapter(ByteSize).validate_python("110Mib"),
314-
TypeAdapter(ByteSize).validate_python("1Mib"),
315-
),
316-
file_checksums: tuple[SHA256Str, ...] = (
317-
TypeAdapter(SHA256Str).validate_python(
318-
"311e2e130d83cfea9c3b7560699c221b0b7f9e5d58b02870bd52b695d8b4aabd"
319-
),
320-
TypeAdapter(SHA256Str).validate_python(
321-
"08e297db979d3c84f6b072c2a1e269e8aa04e82714ca7b295933a0c9c0f62b2e"
322-
),
323-
TypeAdapter(SHA256Str).validate_python(
324-
"488f3b57932803bbf644593bd46d95599b1d4da1d63bc020d7ebe6f1c255f7f3"
325-
),
326-
),
327-
) -> tuple[dict[str, Any], dict[NodeID, dict[SimcoreS3FileID, FileIDDict]]]:
328-
assert len(file_sizes) == len(file_checksums)
329-
project = await create_project(name="random-project")
330-
node_to_files_mapping: dict[NodeID, dict[SimcoreS3FileID, FileIDDict]] = {}
331-
upload_tasks: deque[Awaitable] = deque()
332-
for _node_index in range(num_nodes):
333-
# Create a node with outputs (files and others)
334-
project_id = ProjectID(project["uuid"])
335-
node_id = cast(NodeID, faker.uuid4(cast_to=None))
336-
output3_file_name = faker.file_name()
337-
output3_file_id = create_simcore_file_id(
338-
project_id, node_id, output3_file_name, Path("outputs/output_3")
339-
)
340-
created_node_id = await create_project_node(
341-
ProjectID(project["uuid"]),
342-
node_id,
343-
outputs={
344-
"output_1": faker.pyint(),
345-
"output_2": faker.pystr(),
346-
"output_3": f"{output3_file_id}",
347-
},
348-
)
349-
assert created_node_id == node_id
350-
351-
node_to_files_mapping[created_node_id] = {}
352-
upload_tasks.append(
353-
_upload_file_and_update_project(
354-
project_id,
355-
node_id,
356-
file_name=output3_file_name,
357-
file_id=output3_file_id,
358-
file_sizes=file_sizes,
359-
file_checksums=file_checksums,
360-
upload_file=upload_file,
361-
create_simcore_file_id=create_simcore_file_id,
362-
faker=faker,
363-
node_to_files_mapping=node_to_files_mapping,
364-
)
365-
)
366-
367-
# add a few random files in the node workspace
368-
upload_tasks.extend(
369-
[
370-
_upload_file_and_update_project(
371-
project_id,
372-
node_id,
373-
file_name=None,
374-
file_id=None,
375-
file_sizes=file_sizes,
376-
file_checksums=file_checksums,
377-
upload_file=upload_file,
378-
create_simcore_file_id=create_simcore_file_id,
379-
faker=faker,
380-
node_to_files_mapping=node_to_files_mapping,
381-
)
382-
for _ in range(randint(0, 3)) # noqa: S311
383-
]
384-
)
385-
await limited_gather(*upload_tasks, limit=10)
386-
387-
project = await get_updated_project(sqlalchemy_async_engine, project["uuid"])
388-
return project, node_to_files_mapping
389-
390-
return _creator

packages/pytest-simcore/src/pytest_simcore/simcore_storage_service.py

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import os
55
from collections.abc import Callable, Iterable
66
from copy import deepcopy
7+
from pathlib import Path
78

89
import aiohttp
910
import pytest
@@ -12,7 +13,6 @@
1213
from models_library.projects_nodes_io import NodeID, SimcoreS3FileID
1314
from pydantic import TypeAdapter
1415
from pytest_mock import MockerFixture
15-
from servicelib.minio_utils import ServiceRetryPolicyUponInitialization
1616
from yarl import URL
1717

1818
from .helpers.docker import get_service_published_port
@@ -60,24 +60,34 @@ async def storage_service(
6060
return storage_endpoint
6161

6262

63-
# TODO: this can be used by ANY of the simcore services!
64-
@tenacity.retry(**ServiceRetryPolicyUponInitialization().kwargs)
63+
@tenacity.retry(
64+
wait=tenacity.wait_fixed(1),
65+
stop=tenacity.stop_after_delay(30),
66+
reraise=True,
67+
)
6568
async def wait_till_storage_responsive(storage_endpoint: URL):
66-
async with aiohttp.ClientSession() as session:
67-
async with session.get(storage_endpoint.with_path("/v0/")) as resp:
68-
assert resp.status == 200
69-
data = await resp.json()
70-
assert "data" in data
71-
assert data["data"] is not None
69+
async with (
70+
aiohttp.ClientSession() as session,
71+
session.get(storage_endpoint.with_path("/v0/")) as resp,
72+
):
73+
assert resp.status == 200
74+
data = await resp.json()
75+
assert "data" in data
76+
assert data["data"] is not None
7277

7378

7479
@pytest.fixture
7580
def create_simcore_file_id() -> Callable[[ProjectID, NodeID, str], SimcoreS3FileID]:
7681
def _creator(
77-
project_id: ProjectID, node_id: NodeID, file_name: str
82+
project_id: ProjectID,
83+
node_id: NodeID,
84+
file_name: str,
85+
file_base_path: Path | None = None,
7886
) -> SimcoreS3FileID:
79-
return TypeAdapter(SimcoreS3FileID).validate_python(
80-
f"{project_id}/{node_id}/{file_name}"
81-
)
87+
s3_file_name = file_name
88+
if file_base_path:
89+
s3_file_name = f"{file_base_path / file_name}"
90+
clean_path = Path(f"{project_id}/{node_id}/{s3_file_name}")
91+
return TypeAdapter(SimcoreS3FileID).validate_python(f"{clean_path}")
8292

8393
return _creator

0 commit comments

Comments
 (0)