Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""add execution_config to test_run_execution

Revision ID: e6153267b741
Revises: 0a251edfd975
Create Date: 2026-01-15 17:36:00.000000

"""
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

from alembic import op

# revision identifiers, used by Alembic.
revision = "e6153267b741"
down_revision = "0a251edfd975"
branch_labels = None
depends_on = None


def upgrade():
# Add execution_config column to testrunexecution table (optional JSON)
op.add_column(
"testrunexecution",
sa.Column(
"execution_config", postgresql.JSON(astext_type=sa.Text()), nullable=True
),
)


def downgrade():
# Remove execution_config column
op.drop_column("testrunexecution", "execution_config")
19 changes: 15 additions & 4 deletions app/api/api_v1/endpoints/test_run_executions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#
# Copyright (c) 2025 Project CHIP Authors
# Copyright (c) 2023-2026 Project CHIP Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -129,7 +129,7 @@ def __convert_pics_dict_to_object(pics: dict) -> schemas.PICS | None:
return None


def __cli_project(
def _cli_project(
db: Session,
project_id: int | None,
config: dict | None,
Expand Down Expand Up @@ -179,14 +179,19 @@ def create_cli_test_run_execution(
test_run_execution_in: schemas.TestRunExecutionCreate,
selected_tests: schemas.TestSelection,
config: dict | None = None,
execution_config: dict | None = None,
pics: dict = {},
) -> TestRunExecution:
"""Creates a new test run execution on CLI request.
Attention: if both config and execution_config are provided,
only config will be persisted, while execution_config will be for
this execution only.

Args:
test_run_execution_in: Test run execution data
selected_tests: Selected tests to run
config: Configuration parameters (optional)
config: Configuration parameters that update project (optional, persists)
execution_config: Execution-specific config override (optional, temporary)
pics: PICS configuration (optional)
"""

Expand All @@ -200,9 +205,15 @@ def create_cli_test_run_execution(
)

# Retrieve or create the CLI project
cli_project = __cli_project(db, test_run_execution_in.project_id, config, pics_obj)
# Only update project if config is provided (not execution_config)
cli_project = _cli_project(db, test_run_execution_in.project_id, config, pics_obj)
Comment thread
rquidute marked this conversation as resolved.
test_run_execution_in.project_id = cli_project.id

# Store execution_config if provided (temporary, per-execution)
if execution_config is not None:
logger.info(f"CLI Execution Config (Temporary): {execution_config}")
test_run_execution_in.execution_config = execution_config

test_run_execution_in.certification_mode = False

test_run_execution = crud.test_run_execution.create(
Expand Down
27 changes: 16 additions & 11 deletions app/models/test_run_execution.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#
# Copyright (c) 2023 Project CHIP Authors
# Copyright (c) 2023-2026 Project CHIP Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand All @@ -14,9 +14,9 @@
# limitations under the License.
#
from datetime import datetime
from typing import TYPE_CHECKING, Optional
from typing import TYPE_CHECKING, Any

from sqlalchemy import Enum, ForeignKey, func, select
from sqlalchemy import JSON, Enum, ForeignKey, func, select
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.ext.mutable import MutableList
from sqlalchemy.ext.orderinglist import ordering_list
Expand Down Expand Up @@ -46,15 +46,20 @@ class TestRunExecution(Base):
)
title: Mapped[str] = mapped_column(nullable=False)
created_at: Mapped[datetime] = mapped_column(default=datetime.now, nullable=False)
started_at: Mapped[Optional[datetime]]
completed_at: Mapped[Optional[datetime]]
archived_at: Mapped[Optional[datetime]]
imported_at: Mapped[Optional[datetime]]
started_at: Mapped[datetime | None]
completed_at: Mapped[datetime | None]
archived_at: Mapped[datetime | None]
imported_at: Mapped[datetime | None]
certification_mode: Mapped[bool] = mapped_column(default=False, nullable=False)

description: Mapped[Optional[str]] = mapped_column(default=None, nullable=True)
description: Mapped[str | None] = mapped_column(default=None, nullable=True)

test_run_config_id: Mapped[Optional[int]] = mapped_column(
# Execution-specific config (temporary, not persisted to project)
execution_config: Mapped[dict[str, Any] | None] = mapped_column(
JSON, nullable=True, default=None
)

test_run_config_id: Mapped[int | None] = mapped_column(
ForeignKey("testrunconfig.id"), nullable=True
)
test_run_config: Mapped["TestRunConfig"] = relationship(
Expand All @@ -76,7 +81,7 @@ class TestRunExecution(Base):
collection_class=ordering_list("execution_index"),
cascade="all, delete-orphan",
)
operator_id: Mapped[Optional[int]] = mapped_column(
operator_id: Mapped[int | None] = mapped_column(
ForeignKey("operator.id"), nullable=True
)
operator: Mapped["Operator"] = relationship(
Expand All @@ -92,7 +97,7 @@ def append_to_log(self, log_record: "TestRunLogEntry") -> None:

def test_suite_execution_by_public_id(
self, public_id: str
) -> Optional[TestSuiteExecution]:
) -> TestSuiteExecution | None:
return self.obj_session().scalar(
select(TestSuiteExecution)
.where(with_parent(self, TestSuiteExecution.test_run_execution))
Expand Down
48 changes: 24 additions & 24 deletions app/schemas/test_run_execution.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#
# Copyright (c) 2025 Project CHIP Authors
# Copyright (c) 2023-2026 Project CHIP Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand All @@ -14,7 +14,6 @@
# limitations under the License.
#
from datetime import datetime
from typing import Dict, List, Optional

from pydantic import BaseModel

Expand All @@ -30,46 +29,47 @@
class TestRunExecutionStats(BaseModel):
__test__ = False # Needed to indicate to PyTest that this is not a "test"
test_case_count: int = 0
states: Dict[TestStateEnum, int] = {}
states: dict[TestStateEnum, int] = {}


# Shared properties
class TestRunExecutionBase(BaseModel):
__test__ = False # Needed to indicate to PyTest that this is not a "test"

title: str
description: Optional[str]
description: str | None
execution_config: dict | None = None
certification_mode: bool = False


# Base + properties that represent relationhips
class TestRunExecutionBaseWithRelationships(TestRunExecutionBase):
test_run_config_id: Optional[int]
project_id: Optional[int]
test_run_config_id: int | None
project_id: int | None


# Properties additional fields on creation
class TestRunExecutionCreate(TestRunExecutionBaseWithRelationships):
# TODO(#124): Require project ID when UI supports project management.
operator_id: Optional[int]
operator_id: int | None


# Properties shared by models stored in DB
class TestRunExecutionInDBBase(TestRunExecutionBaseWithRelationships):
id: int
state: TestStateEnum
started_at: Optional[datetime]
completed_at: Optional[datetime]
imported_at: Optional[datetime]
archived_at: Optional[datetime]
started_at: datetime | None
completed_at: datetime | None
imported_at: datetime | None
archived_at: datetime | None

class Config:
orm_mode = True


# Properties to return to client
class TestRunExecution(TestRunExecutionInDBBase):
operator: Optional[Operator]
operator: Operator | None


# Properties to return to client
Expand All @@ -79,7 +79,7 @@ class TestRunExecutionWithStats(TestRunExecution):

# Properties to return to client
class TestRunExecutionWithChildren(TestRunExecution):
test_suite_executions: Optional[List[TestSuiteExecution]]
test_suite_executions: list[TestSuiteExecution] | None


class TestRunExecutionUpdate(TestRunExecutionBase):
Expand All @@ -91,18 +91,18 @@ class Config:

# Additional Properties properties stored in DB
class TestRunExecutionInDB(TestRunExecutionInDBBase):
operator_id: Optional[int]
operator_id: int | None
created_at: datetime
log: list[TestRunLogEntry]


# Shared properties between export and import schemas
class TestRunExecutionExportImportBase(TestRunExecutionBase):
state: TestStateEnum
started_at: Optional[datetime]
completed_at: Optional[datetime]
archived_at: Optional[datetime]
test_suite_executions: Optional[List[TestSuiteExecutionToExport]]
started_at: datetime | None
completed_at: datetime | None
archived_at: datetime | None
test_suite_executions: list[TestSuiteExecutionToExport] | None
created_at: datetime
log: list[TestRunLogEntry]

Expand All @@ -112,8 +112,8 @@ class Config:

# Schema used to export test run executions
class TestRunExecutionToExport(TestRunExecutionExportImportBase):
operator: Optional[OperatorToExport]
test_run_config: Optional[TestRunConfigToExport]
operator: OperatorToExport | None
test_run_config: TestRunConfigToExport | None


# Schema used to export test run executions
Expand All @@ -127,7 +127,7 @@ class Config:

# Schema used to import test run executions
class TestRunExecutionToImport(TestRunExecutionExportImportBase):
project_id: Optional[int]
operator_id: Optional[int]
imported_at: Optional[datetime]
test_run_config_id: Optional[int]
project_id: int | None
operator_id: int | None
imported_at: datetime | None
test_run_config_id: int | None
10 changes: 10 additions & 0 deletions app/test_engine/models/test_case.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,16 @@ def project(self) -> Project:

@property
def config(self) -> dict:
"""Get configuration for test case.

Returns execution_config if available (temporary override from CLI),
otherwise returns project.config (persistent configuration).
"""
test_run_execution = (
self.test_case_execution.test_suite_execution.test_run_execution
)
if test_run_execution.execution_config is not None:
return test_run_execution.execution_config
return self.project.config

@property
Expand Down
25 changes: 15 additions & 10 deletions app/test_engine/models/test_run.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#
# Copyright (c) 2025 Project CHIP Authors
# Copyright (c) 2023-2026 Project CHIP Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand All @@ -14,7 +14,6 @@
# limitations under the License.
#
from asyncio import CancelledError, Task, create_task
from typing import List, Optional

from app.models import Project, TestRunExecution, TestStateEnum
from app.schemas.test_run_log_entry import TestRunLogEntry
Expand All @@ -37,10 +36,10 @@ class TestRun(TestObservable, UserPromptSupport):
def __init__(self, test_run_execution: TestRunExecution):
super().__init__()
self.test_run_execution = test_run_execution
self.current_test_suite: Optional[TestSuite] = None
self.test_suites: List[TestSuite] = []
self.current_test_suite: TestSuite | None = None
self.test_suites: list[TestSuite] = []
self.__state = TestStateEnum.PENDING
self.__current_testing_task: Optional[Task] = None
self.__current_testing_task: Task | None = None
self.log: list[TestRunLogEntry] = []

@property
Expand All @@ -54,7 +53,13 @@ def project(self) -> Project:

@property
def config(self) -> dict:
"""Convenience getter to access project config."""
"""Convenience getter to access config.

Returns execution_config if present (temporary, per-execution config),
otherwise returns project.config (persistent config).
"""
if self.test_run_execution.execution_config is not None:
return self.test_run_execution.execution_config
return self.project.config

@property
Expand Down Expand Up @@ -168,7 +173,7 @@ def append_log_entries(self, entries: list[TestRunLogEntry]) -> None:
self.log.extend(entries)
self.notify()

def subscribe(self, observers: List[Observer]) -> None:
def subscribe(self, observers: list[Observer]) -> None:
"""Subscribe a list of observers to test run changes, and changes on sub-models
test suites, test cases, and test steps.

Expand All @@ -178,7 +183,7 @@ def subscribe(self, observers: List[Observer]) -> None:
super().subscribe(observers)
self.__subscribe_test_suites(observers)

def __subscribe_test_suites(self, observers: List[Observer]) -> None:
def __subscribe_test_suites(self, observers: list[Observer]) -> None:
"""Subscribe sub-models to observers

Args:
Expand All @@ -187,7 +192,7 @@ def __subscribe_test_suites(self, observers: List[Observer]) -> None:
for test_suite in self.test_suites:
test_suite.subscribe(observers)

def unsubscribe(self, observers: List[Observer]) -> None:
def unsubscribe(self, observers: list[Observer]) -> None:
"""Unsubscribe observers from changes to test run changes, and sub-models
test suites, test cases, and test steps.

Expand All @@ -197,7 +202,7 @@ def unsubscribe(self, observers: List[Observer]) -> None:
super().unsubscribe(observers)
self.__unsubscribe_test_suites(observers)

def __unsubscribe_test_suites(self, observers: List[Observer]) -> None:
def __unsubscribe_test_suites(self, observers: list[Observer]) -> None:
"""Unsubscribe sub-models to observers

Args:
Expand Down
Loading
Loading