Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 149 additions & 0 deletions bublik/core/run/dto.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright (C) 2026 OKTET Labs Ltd. All rights reserved.

from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING


if TYPE_CHECKING:
from datetime import datetime, timedelta


@dataclass
class RunCompromisedDetails:
status: bool
comment: str | None = None
bug_id: str | None = None
bug_url: str | None = None


@dataclass
class RunRevision:
name: str
value: str
url: str


@dataclass
class RunSpecialCategory:
name: str
values: list[str]


@dataclass
class RunDetailsResult:
project_id: int
project_name: str
id: int
start: datetime | None
finish: datetime | None
duration: timedelta | None
main_package: str | None
status: str | None
status_by_nok: str
compromised: RunCompromisedDetails | None
conclusion: str
conclusion_reason: str | None
important_tags: list[str]
relevant_tags: list[str]
branches: list[str]
revisions: list[RunRevision]
labels: list[str]
special_categories: list[RunSpecialCategory]
configuration: str | None


@dataclass
class RunStatsValues:
passed: int
failed: int
passed_unexpected: int
failed_unexpected: int
skipped: int
skipped_unexpected: int
abnormal: int


@dataclass
class RunStatsComment:
comment_id: str
updated: str
serial: str
comment: str


@dataclass
class RunStatsResult:
result_id: int
exec_seqno: int
parent_id: int | None
type: str
test_id: int
test_name: str
period: str
path: list[str]
objective: str
children: list[RunStatsResult]
stats: RunStatsValues
comments: list[RunStatsComment]


@dataclass
class RunCompromisedResult:
compromised: bool


@dataclass
class MarkRunCompromisedResult:
comment: str
bug: str | None


@dataclass
class RunSummaryStats:
tests_total: int
tests_total_plan_percent: int | None
tests_total_ok: int
tests_total_ok_percent: int
tests_total_nok: int
tests_total_nok_percent: int


@dataclass
class RunSummaryResult:
id: int
project_id: int
project_name: str
start: datetime | None
finish: datetime | None
duration: timedelta | None
status: str | None
status_by_nok: str
compromised: bool | None
conclusion: str
conclusion_reason: str | None
metadata: list[str]
important_tags: list[str]
relevant_tags: list[str]
stats: RunSummaryStats | None


@dataclass
class RunListPagination:
count: int
next: str | None
previous: str | None


@dataclass
class RunListResult:
pagination: RunListPagination
results: list[RunSummaryResult]


@dataclass
class RunCommentResult:
id: int
comment: str
71 changes: 42 additions & 29 deletions bublik/core/run/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@
unmark_run_compromised,
validate_compromised_request,
)
from bublik.core.run.dto import (
MarkRunCompromisedResult,
RunCommentResult,
RunCompromisedResult,
RunDetailsResult,
RunListPagination,
RunListResult,
RunStatsResult,
)
from bublik.core.run.external_links import get_sources
from bublik.core.run.filter_expression import filter_by_expression
from bublik.core.run.stats import (
Expand Down Expand Up @@ -71,15 +80,15 @@ def get_run(run_id: int) -> models.TestIterationResult:
raise NotFoundError(msg) from e

@staticmethod
def get_run_details(run_id: int) -> dict:
def get_run_details(run_id: int) -> RunDetailsResult:
'''
Get full details for a single run.

Args:
run_id: The ID of the test run

Returns:
Dictionary with full run details
RunDetailsResult with full run details
'''
run = RunService.get_run(run_id)
return generate_all_run_details(run)
Expand All @@ -99,7 +108,10 @@ def get_run_status(run_id: int) -> str:
return get_run_status(run)

@staticmethod
def get_run_stats(run_id: int, requirements: str | None = None) -> dict:
def get_run_stats(
run_id: int,
requirements: str | None = None,
) -> RunStatsResult | None:
'''
Get statistics for a run.

Expand All @@ -108,7 +120,7 @@ def get_run_stats(run_id: int, requirements: str | None = None) -> dict:
requirements: Optional requirements filter

Returns:
Dictionary with run statistics
RunStatsResult with run statistics or None if stats are unavailable
'''
return get_run_stats_detailed_with_comments(run_id, requirements)

Expand All @@ -127,29 +139,26 @@ def get_run_source(run_id: int) -> str:
return get_sources(run)

@staticmethod
def get_run_compromised(run_id: int) -> dict:
def get_run_compromised(run_id: int) -> RunCompromisedResult:
'''
Get compromised status for a run.

Args:
run_id: The ID of the test run

Returns:
Dictionary with compromised status data
RunCompromisedResult with compromised status data
'''
run = RunService.get_run(run_id)
compromised_data = is_run_compromised(run)
if not compromised_data:
compromised_data = {'compromised': False}
return compromised_data
return RunCompromisedResult(compromised=bool(is_run_compromised(run)))

@staticmethod
def mark_run_compromised(
run_id: int,
comment: str,
bug_id: str | None = None,
reference_key: str | None = None,
) -> dict:
) -> MarkRunCompromisedResult:
'''
Mark a run as compromised.

Expand All @@ -160,7 +169,7 @@ def mark_run_compromised(
reference_key: Optional reference key

Returns:
Dictionary with comment and bug info
MarkRunCompromisedResult with comment and bug info

Raises:
ValidationError: if validation fails
Expand All @@ -170,10 +179,10 @@ def mark_run_compromised(
raise ValidationError(err_msg)

mark_run_compromised(run_id, comment, bug_id, reference_key)
return {
'comment': comment,
'bug': f'Bug ID: {bug_id}' if bug_id else None,
}
return MarkRunCompromisedResult(
comment=comment,
bug=f'Bug ID: {bug_id}' if bug_id else None,
)

@staticmethod
def unmark_run_compromised(run_id: int) -> None:
Expand Down Expand Up @@ -260,7 +269,7 @@ def list_runs(
project_id: int | None = None,
page: int | None = None,
page_size: int | None = None,
) -> dict:
) -> RunListResult:
'''
List runs filtered by date range and optionally by project.

Expand All @@ -272,16 +281,20 @@ def list_runs(
page_size: Items per page (default: 25, max: 10000)

Returns:
Dictionary with pagination metadata and run detail dictionaries
RunListResult with pagination metadata and run details
'''

runs = RunService.list_runs_queryset(
start_date=start_date,
finish_date=finish_date,
project_id=project_id,
)
runs_details = generate_runs_details(runs)
return PaginatedResult.paginate_queryset(runs_details, page, page_size)
paginated_runs = PaginatedResult.paginate_queryset(runs, page, page_size)

return RunListResult(
pagination=RunListPagination(**paginated_runs['pagination']),
results=generate_runs_details(paginated_runs['results']),
)

@staticmethod
def aggregate_runs_by_period(
Expand Down Expand Up @@ -526,18 +539,18 @@ def get_run_requirements(run_id: int) -> list[str]:
)

@staticmethod
def get_nok_distribution(run_id: int) -> dict:
def get_nok_distribution(run_id: int) -> list[bool]:
'''
Get NOK (failure) distribution for a run.

Args:
run_id: The ID of the test run

Returns:
Dictionary with NOK distribution data
List of NOK distribution flags
'''
run = RunService.get_run(run_id)
return get_nok_results_distribution(run)
return list(get_nok_results_distribution(run))

@staticmethod
def get_run_comment(run_id: int) -> str | None:
Expand Down Expand Up @@ -566,7 +579,7 @@ def get_run_comment(run_id: int) -> str | None:
return comments.first().meta.value

@staticmethod
def create_run_comment(run_id: int, content: str) -> dict:
def create_run_comment(run_id: int, content: str) -> RunCommentResult:
'''
Create or update run comment.

Expand All @@ -575,7 +588,7 @@ def create_run_comment(run_id: int, content: str) -> dict:
content: Comment content

Returns:
Dictionary with comment details
RunCommentResult with comment details
'''
run = RunService.get_run(run_id)

Expand All @@ -594,10 +607,10 @@ def create_run_comment(run_id: int, content: str) -> dict:
)
mr, _ = mr_serializer.get_or_create()

return {
'id': mr.id,
'comment': mr.meta.value,
}
return RunCommentResult(
id=mr.id,
comment=mr.meta.value,
)

@staticmethod
def delete_run_comment(run_id: int) -> None:
Expand Down
Loading
Loading