Skip to content

Commit

Permalink
Add status update endpoint to receive updates from Testflinger
Browse files Browse the repository at this point in the history
  • Loading branch information
val500 committed Jun 11, 2024
1 parent 4b54b1a commit 78a0518
Show file tree
Hide file tree
Showing 7 changed files with 268 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""Create test status update tables
Revision ID: 2745d4e5bc72
Revises: 33c0383ea9ca
Create Date: 2024-06-11 20:02:00.064753+00:00
"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = "2745d4e5bc72"
down_revision = "33c0383ea9ca"
branch_labels = None
depends_on = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"test_event",
sa.Column("event_name", sa.String(), nullable=False),
sa.Column("timestamp", sa.DateTime(), nullable=False),
sa.Column("detail_msg", sa.String(), nullable=False),
sa.Column("test_execution_id", sa.Integer(), nullable=False),
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("created_at", sa.DateTime(), nullable=False),
sa.Column("updated_at", sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(
["test_execution_id"],
["test_execution.id"],
name=op.f("test_event_test_execution_id_fkey"),
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint("id", name=op.f("test_event_pkey")),
)
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("test_event")
# ### end Alembic commands ###
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@

from fastapi import APIRouter

from . import end_test, get_test_results, patch, reruns, start_test
from . import end_test, get_test_results, patch, reruns, start_test, status_update

router = APIRouter(tags=["test-executions"])
router.include_router(start_test.router)
router.include_router(get_test_results.router)
router.include_router(end_test.router)
router.include_router(patch.router)
router.include_router(reruns.router)
router.include_router(status_update.router)
11 changes: 11 additions & 0 deletions backend/test_observer/controllers/test_executions/logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
ArtefactBuild,
TestExecution,
TestResult,
TestEvent,
)
from test_observer.data_access.models_enums import TestExecutionStatus

Expand Down Expand Up @@ -55,6 +56,16 @@ def delete_previous_results(
db.commit()


def delete_previous_test_events(
db: Session,
test_execution: TestExecution,
):
db.execute(
delete(TestEvent).where(TestEvent.test_execution_id == test_execution.id)
)
db.commit()


def get_previous_artefact_builds_query(
session: Session,
artefact: Artefact,
Expand Down
11 changes: 11 additions & 0 deletions backend/test_observer/controllers/test_executions/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from enum import Enum
from typing import Annotated

from datetime import datetime
from pydantic import (
AliasPath,
BaseModel,
Expand Down Expand Up @@ -172,3 +173,13 @@ class PendingRerun(BaseModel):

class DeleteReruns(BaseModel):
test_execution_ids: set[int]


class TestEvent(BaseModel):
event_name: str
timestamp: datetime
detail_msg: str


class StatusUpdateRequest(BaseModel):
events: list[TestEvent]
53 changes: 53 additions & 0 deletions backend/test_observer/controllers/test_executions/status_update.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Copyright 2024 Canonical Ltd.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#

from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session, joinedload

from test_observer.data_access.models import (
TestEvent,
TestExecution,
)

from test_observer.data_access.setup import get_db

from .logic import delete_previous_test_events
from .models import StatusUpdateRequest

router = APIRouter()


@router.put("/{id}/status_update")
def status_update(id: int, request: StatusUpdateRequest, db: Session = Depends(get_db)):
test_execution = db.get(
TestExecution,
id,
options=[joinedload(TestExecution.test_events)],
)
if test_execution is None:
raise HTTPException(status_code=404, detail="TestExecution not found")

delete_previous_test_events(db, test_execution)

for event in request.events:
test_event = TestEvent(
event_name=event.event_name,
timestamp=event.timestamp,
detail_msg=event.detail_msg,
)
db.add(test_event)
test_execution.test_events.append(test_event)
db.commit()
21 changes: 21 additions & 0 deletions backend/test_observer/data_access/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,11 @@ class TestExecution(Base):
test_results: Mapped[list["TestResult"]] = relationship(
back_populates="test_execution", cascade="all, delete"
)
test_events: Mapped[list["TestEvent"]] = relationship(
back_populates="test_execution",
cascade="all, delete",
order_by="TestEvent.timestamp",
)
rerun_request: Mapped[TestExecutionRerunRequest | None] = relationship(
back_populates="test_execution", cascade="all, delete"
)
Expand Down Expand Up @@ -402,3 +407,19 @@ def __repr__(self) -> str:
"test_execution_id",
"test_case_id",
)


class TestEvent(Base):
"""
A table to represent test events that have ocurred during a job
"""

__tablename__ = "test_event"

event_name: Mapped[str]
timestamp: Mapped[datetime]
detail_msg: Mapped[str]
test_execution_id: Mapped[int] = mapped_column(
ForeignKey("test_execution.id", ondelete="CASCADE")
)
test_execution: Mapped["TestExecution"] = relationship(back_populates="test_events")
126 changes: 126 additions & 0 deletions backend/tests/controllers/test_executions/test_status_update.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# Copyright 2024 Canonical Ltd.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#

import datetime

from fastapi.testclient import TestClient

from tests.data_generator import DataGenerator


def test_status_updates_stored(test_client: TestClient, generator: DataGenerator):
artefact = generator.gen_artefact("beta")
artefact_build = generator.gen_artefact_build(artefact)
environment = generator.gen_environment()
test_execution = generator.gen_test_execution(
artefact_build, environment, ci_link="http://localhost"
)

response = test_client.put(
f"/v1/test-executions/{test_execution.id}/status_update",
json={
"agent_id": "test_agent",
"job_queue": "test_job_queue",
"events": [
{
"event_name": "started_setup",
"timestamp": "2015-03-21T11:08:14.859831",
"detail_msg": "my_detail_msg_one",
},
{
"event_name": "ended_setup",
"timestamp": "2015-03-21T11:08:15.859831",
"detail_msg": "my_detail_msg_two",
},
],
},
)
assert response.status_code == 200
assert test_execution.test_events[0].event_name == "started_setup"
assert test_execution.test_events[0].timestamp == datetime.datetime.fromisoformat(
"2015-03-21T11:08:14.859831"
)
assert test_execution.test_events[0].detail_msg == "my_detail_msg_one"
assert test_execution.test_events[1].event_name == "ended_setup"
assert test_execution.test_events[1].timestamp == datetime.datetime.fromisoformat(
"2015-03-21T11:08:15.859831"
)
assert test_execution.test_events[1].detail_msg == "my_detail_msg_two"


def test_status_updates_is_idempotent(
test_client: TestClient, generator: DataGenerator
):
artefact = generator.gen_artefact("beta")
artefact_build = generator.gen_artefact_build(artefact)
environment = generator.gen_environment()
test_execution = generator.gen_test_execution(
artefact_build, environment, ci_link="http://localhost"
)

for _ in range(3):
test_client.put(
f"/v1/test-executions/{test_execution.id}/status_update",
json={
"agent_id": "test_agent",
"job_queue": "test_job_queue",
"events": [
{
"event_name": "started_setup",
"timestamp": "2015-03-21T11:08:14.859831",
"detail_msg": "my_detail_msg_one",
},
{
"event_name": "ended_setup",
"timestamp": "2015-03-21T11:08:15.859831",
"detail_msg": "my_detail_msg_two",
},
],
},
)
assert len(test_execution.test_events) == 2


def test_status_updates_invalid_timestamp(
test_client: TestClient, generator: DataGenerator
):
artefact = generator.gen_artefact("beta")
artefact_build = generator.gen_artefact_build(artefact)
environment = generator.gen_environment()
test_execution = generator.gen_test_execution(
artefact_build, environment, ci_link="http://localhost"
)

response = test_client.put(
f"/v1/test-executions/{test_execution.id}/status_update",
json={
"agent_id": "test_agent",
"job_queue": "test_job_queue",
"events": [
{
"event_name": "started_setup",
"timestamp": "201-03-21T11:08:14.859831",
"detail_msg": "my_detail_msg_one",
},
{
"event_name": "ended_setup",
"timestamp": "20-03-21T11:08:15.859831",
"detail_msg": "my_detail_msg_two",
},
],
},
)
assert response.status_code == 422

0 comments on commit 78a0518

Please sign in to comment.