From bafcef223263e7d3418a6fab53d7d7b7e3f42dea Mon Sep 17 00:00:00 2001 From: Lukas Petr Date: Tue, 20 Jan 2026 22:33:43 +0100 Subject: [PATCH 1/2] Add field for expected fun result Added field to allow users add expected result for compared function. --- .../f04430af51e2_add_expected_res_for_funs.py | 34 ++++++++ backend/automation/db/__init__.py | 68 +++++++++++++++- backend/automation/models/results/function.py | 19 ++++- backend/automation/models/results/types.py | 7 ++ backend/automation/web/__init__.py | 2 + backend/automation/web/api.py | 77 +++++++++++++++++++ .../web/templates/commits/result.html | 18 ++++- backend/automation/web/templates/layout.html | 35 +++++++++ backend/automation/web/templates/macros.html | 27 +++++++ .../web/templates/versions/result.html | 15 +++- 10 files changed, 296 insertions(+), 6 deletions(-) create mode 100644 backend/alembic/versions/f04430af51e2_add_expected_res_for_funs.py create mode 100644 backend/automation/web/api.py create mode 100644 backend/automation/web/templates/macros.html diff --git a/backend/alembic/versions/f04430af51e2_add_expected_res_for_funs.py b/backend/alembic/versions/f04430af51e2_add_expected_res_for_funs.py new file mode 100644 index 0000000..9782eb0 --- /dev/null +++ b/backend/alembic/versions/f04430af51e2_add_expected_res_for_funs.py @@ -0,0 +1,34 @@ +"""Add expected res for funs + +Revision ID: f04430af51e2 +Revises: 8233dd7ca744 +Create Date: 2026-01-19 18:56:12.824385 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'f04430af51e2' +down_revision: Union[str, Sequence[str], None] = '8233dd7ca744' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('function_results', sa.Column('expected_result', sa.Enum('UNKNOWN', 'EQUAL', 'NON_EQUAL', name='expectedresulttype'), nullable=True)) + op.add_column('function_results', sa.Column('expected_result_changed_by', sa.String(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('function_results', 'expected_result_changed_by') + op.drop_column('function_results', 'expected_result') + # ### end Alembic commands ### diff --git a/backend/automation/db/__init__.py b/backend/automation/db/__init__.py index 7ac66dc..b54adf5 100644 --- a/backend/automation/db/__init__.py +++ b/backend/automation/db/__init__.py @@ -11,7 +11,8 @@ from automation.models.results.result import ResultBase from automation.models.results.results import ResultSubType from automation.models.results.types import (ComparisonStatus, - DiffKempResultType) + DiffKempResultType, + ExpectedResultType) from automation.models.results.versions import ResultVersion from automation.utils import RESULTS_DB_PATH @@ -48,6 +49,8 @@ Column("id", Integer, primary_key=True, autoincrement=True), Column("name", String, nullable=False), Column("diffkemp_result", Enum(DiffKempResultType), nullable=False), + Column("expected_result", Enum(ExpectedResultType)), + Column("expected_result_changed_by", String), Column("result_id", Integer, ForeignKey("results.id"), nullable=False), ) @@ -178,3 +181,66 @@ def _result_fun_list_to_dict(self, result: ResultSubType | None) -> None: # Safe deletion if hasattr(result, "_functions_list"): delattr(result, "_functions_list") + + +class FunctionResultsRepo: + """Repository for working with function results in DB.""" + + def getForCommit( + self, + config_name: str, + commit: str, + diffkemp_sha: str, + function_name: str, + ) -> Optional[FunctionResult]: + """Get a specific function result by filtering on parent result.""" + with Session() as session: + query = ( + session.query(FunctionResult) + .join(results_table, + function_results_table.c.result_id == results_table.c.id) + .where( + function_results_table.c.name == function_name, + results_table.c.config_file_name == config_name, + results_table.c.commit == commit, + results_table.c.diffkemp_sha == diffkemp_sha, + ) + ) + result = query.first() + if result: + session.expunge(result) + return result + + def getForVersion( + self, + config_name: str, + old_tag: str, + new_tag: str, + diffkemp_sha: str, + function_name: str, + ) -> Optional[FunctionResult]: + """Get a specific function result by filtering on parent result.""" + with Session() as session: + query = ( + session.query(FunctionResult) + .join(results_table, + function_results_table.c.result_id == results_table.c.id) + .where( + function_results_table.c.name == function_name, + results_table.c.config_file_name == config_name, + results_table.c.old_tag == old_tag, + results_table.c.new_tag == new_tag, + results_table.c.diffkemp_sha == diffkemp_sha, + ) + ) + result = query.first() + if result: + session.expunge(result) + return result + + def update(self, function_result: FunctionResult) -> None: + """Update a function result in the database.""" + with Session() as session: + # Merge the detached object back into the session + session.merge(function_result) + session.commit() diff --git a/backend/automation/models/results/function.py b/backend/automation/models/results/function.py index cad3f1b..ade4778 100644 --- a/backend/automation/models/results/function.py +++ b/backend/automation/models/results/function.py @@ -4,27 +4,42 @@ :author: Lukas Petr """ -from .types import DiffKempResultType +from typing import Optional + +from .types import DiffKempResultType, ExpectedResultType class FunctionResult: """Represents the comparison result for a single function.""" - def __init__(self, name: str, diffkemp_result: DiffKempResultType): + def __init__( + self, + name: str, + diffkemp_result: DiffKempResultType, + expected_result: ExpectedResultType = ExpectedResultType.UNKNOWN, + ) -> None: self.name: str = name self.diffkemp_result: DiffKempResultType = diffkemp_result + self.expected_result: Optional[ExpectedResultType] = expected_result + self.expected_result_changed_by: Optional[str] = None + + def set_expected_result(self, result: ExpectedResultType) -> None: + self.expected_result = result def to_yaml(self) -> dict: return { "name": self.name, "diffkemp_result": str(self.diffkemp_result), + "expected_result": str(self.expected_result) } @classmethod def from_yaml(cls, result: dict) -> "FunctionResult": name = result["name"] diffkemp_result = result["diffkemp_result"] + expected_result = result.get("expected_result", "unknown") return cls( name=name, diffkemp_result=DiffKempResultType(diffkemp_result), + expected_result=ExpectedResultType(expected_result), ) diff --git a/backend/automation/models/results/types.py b/backend/automation/models/results/types.py index cb4a415..4538e19 100644 --- a/backend/automation/models/results/types.py +++ b/backend/automation/models/results/types.py @@ -24,6 +24,13 @@ class DiffKempResultType(StrEnum): ERROR = "error" +class ExpectedResultType(StrEnum): + """Types of expected comparison results from DiffKemp analysis.""" + UNKNOWN = "unknown" + EQUAL = "equal" + NON_EQUAL = "non equal" + + class ComparisonStatus(StrEnum): """Status of the comparison.""" diff --git a/backend/automation/web/__init__.py b/backend/automation/web/__init__.py index d650d2c..da28631 100644 --- a/backend/automation/web/__init__.py +++ b/backend/automation/web/__init__.py @@ -17,6 +17,7 @@ def create_app() -> Flask: load_dotenv() app = Flask(__name__) + from .api import api_bp from .auth import auth_bp from .commits import commits_bp from .versions import versions_bp @@ -26,6 +27,7 @@ def create_app() -> Flask: app.register_blueprint(commits_bp) app.register_blueprint(view_results_bp) app.register_blueprint(auth_bp) + app.register_blueprint(api_bp) @app.route("/") def index() -> ResponseReturnValue: diff --git a/backend/automation/web/api.py b/backend/automation/web/api.py new file mode 100644 index 0000000..b907ccc --- /dev/null +++ b/backend/automation/web/api.py @@ -0,0 +1,77 @@ +"""File defining HTML REST API endpoints.""" +from flask import Blueprint, Response, abort, request, session + +from automation.db import FunctionResultsRepo +from automation.models.results.types import ExpectedResultType + +api_bp = Blueprint("api_bp", __name__, url_prefix="/api") + + +@api_bp.put( + "/commits///" + "/functions//expected-result" +) +def update_fun_expected_res_for_commit( + config_name: str, + commit: str, + diffkemp_sha: str, + function_name: str, +) -> Response: + """Update the expected result for a function.""" + # Check if user is logged in + if "user" not in session: + abort(403) + + repo = FunctionResultsRepo() + function_result = repo.getForCommit( + config_name=config_name, + commit=commit, + diffkemp_sha=diffkemp_sha, + function_name=function_name, + ) + + if not function_result: + abort(404) + + expected_result = ExpectedResultType(request.json["expected_result"]) + function_result.expected_result = expected_result + function_result.expected_result_changed_by = session["user"] + + repo.update(function_result) + return Response(status=200) + + +@api_bp.put( + "/versions////" + "/functions//expected-result" +) +def update_fun_expected_res_for_version( + config_name: str, + old_tag: str, + new_tag: str, + diffkemp_sha: str, + function_name: str, +) -> Response: + """Update the expected result for a function.""" + # Check if user is logged in + if "user" not in session: + abort(403) + + repo = FunctionResultsRepo() + function_result = repo.getForVersion( + config_name=config_name, + old_tag=old_tag, + new_tag=new_tag, + diffkemp_sha=diffkemp_sha, + function_name=function_name, + ) + + if not function_result: + abort(404) + + expected_result = ExpectedResultType(request.json["expected_result"]) + function_result.expected_result = expected_result + function_result.expected_result_changed_by = session["user"] + + repo.update(function_result) + return Response(status=200) diff --git a/backend/automation/web/templates/commits/result.html b/backend/automation/web/templates/commits/result.html index f55175c..d7a27d6 100644 --- a/backend/automation/web/templates/commits/result.html +++ b/backend/automation/web/templates/commits/result.html @@ -1,5 +1,7 @@ -{% extends "commits/layout.html" %} {% block head %} {{ super() }} {% endblock -%} {% block subcontent %} +{% extends "commits/layout.html" %} +{% from "macros.html" import expected_fun_result_select %} + +{% block subcontent %}

Results

    @@ -16,6 +18,7 @@

    Compared functions

    Compared function DiffKemp result + Expected result @@ -27,6 +30,17 @@

    Compared functions

    {{ info.diffkemp_result.value.capitalize() }} + + {% set api_url = url_for( + 'api_bp.update_fun_expected_res_for_commit', + config_name=result.config_name, + commit=result.commit.SHA, + diffkemp_sha=result.diffkemp_sha, + function_name=function, + ) + %} + {{ expected_fun_result_select(info, api_url) }} + {% endfor %} diff --git a/backend/automation/web/templates/layout.html b/backend/automation/web/templates/layout.html index ec71617..7460984 100644 --- a/backend/automation/web/templates/layout.html +++ b/backend/automation/web/templates/layout.html @@ -103,6 +103,41 @@ integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous" > + {% block scripts %}{% endblock %} diff --git a/backend/automation/web/templates/macros.html b/backend/automation/web/templates/macros.html new file mode 100644 index 0000000..a561339 --- /dev/null +++ b/backend/automation/web/templates/macros.html @@ -0,0 +1,27 @@ +{% macro expected_fun_result_select(function_result, api_url) %} + +
    + {% if "user" in session %} + Last changed by: {{ function_result.expected_result_changed_by or "" }} + {% endif %} +
    +{% endmacro %} diff --git a/backend/automation/web/templates/versions/result.html b/backend/automation/web/templates/versions/result.html index 1152701..4d7bcf3 100644 --- a/backend/automation/web/templates/versions/result.html +++ b/backend/automation/web/templates/versions/result.html @@ -1,4 +1,5 @@ {% extends "versions/layout.html" %} +{% from "macros.html" import expected_fun_result_select %} {% block subcontent %} @@ -17,6 +18,7 @@

    Compared functions

    Compared function DiffKemp result + Expected result @@ -28,9 +30,20 @@

    Compared functions

    {{ info.diffkemp_result.value.capitalize() }} + + {% set api_url = url_for( + 'api_bp.update_fun_expected_res_for_version', + config_name=result.config_name, + old_tag=result.old_tag, + new_tag=result.new_tag, + diffkemp_sha=result.diffkemp_sha, + function_name=function, + ) + %} + {{ expected_fun_result_select(info, api_url) }} + {% endfor %} - {% endblock %} From 2655925b204aa902ee35e92617f65a7ddd15b221 Mon Sep 17 00:00:00 2001 From: Lukas Petr Date: Sun, 25 Jan 2026 18:54:10 +0100 Subject: [PATCH 2/2] Removed old methods from yaml / to yaml Removed old methods which were used when saving/loading result to/from file. --- backend/automation/models/results/commits.py | 9 ------ backend/automation/models/results/function.py | 18 ----------- backend/automation/models/results/result.py | 30 +------------------ backend/automation/models/results/versions.py | 9 ------ 4 files changed, 1 insertion(+), 65 deletions(-) diff --git a/backend/automation/models/results/commits.py b/backend/automation/models/results/commits.py index a638a92..d2dc194 100644 --- a/backend/automation/models/results/commits.py +++ b/backend/automation/models/results/commits.py @@ -124,15 +124,6 @@ def get_commit_datetime(self) -> datetime: def get_diff(self) -> str: return self.get_project().repo.git.diff(f"{self.commit}^!") - @classmethod - def from_yaml(cls, result: dict) -> "ResultCommit": - kwargs = super()._parse_yaml_base(result) - return cls( - commit=result["commit"], - all_diffs_matched=result["all_diffs_matched"], - **kwargs - ) - class ResultsCommits(): """Class for parsing results for commits.""" diff --git a/backend/automation/models/results/function.py b/backend/automation/models/results/function.py index ade4778..1a3dd9a 100644 --- a/backend/automation/models/results/function.py +++ b/backend/automation/models/results/function.py @@ -25,21 +25,3 @@ def __init__( def set_expected_result(self, result: ExpectedResultType) -> None: self.expected_result = result - - def to_yaml(self) -> dict: - return { - "name": self.name, - "diffkemp_result": str(self.diffkemp_result), - "expected_result": str(self.expected_result) - } - - @classmethod - def from_yaml(cls, result: dict) -> "FunctionResult": - name = result["name"] - diffkemp_result = result["diffkemp_result"] - expected_result = result.get("expected_result", "unknown") - return cls( - name=name, - diffkemp_result=DiffKempResultType(diffkemp_result), - expected_result=ExpectedResultType(expected_result), - ) diff --git a/backend/automation/models/results/result.py b/backend/automation/models/results/result.py index 859b0d8..0421379 100644 --- a/backend/automation/models/results/result.py +++ b/backend/automation/models/results/result.py @@ -8,7 +8,7 @@ from abc import ABC, abstractmethod from datetime import datetime from pathlib import Path -from typing import Any, Dict, Generic, Optional, TypeVar, cast +from typing import Dict, Generic, Optional, TypeVar, cast from automation.models.projects.project import TProject from automation.models.projects.projects import ProjectsManager @@ -120,31 +120,3 @@ def get_diffkemp_out(self) -> Optional[DiffKempOutYaml]: if not path.exists(): return None return DiffKempOutYaml.from_file(path) - - @staticmethod - def _parse_yaml_base(result: Dict[str, Any]) -> dict: - """ - Parse common fields from YAML representation. - - This helper method extracts base class fields. Subclasses should - call this and add their specific fields. - - :param result: YAML result. - """ - functions = {} - for fun_res in result["functions"]: - f = FunctionResult.from_yaml(fun_res) - functions[f.name] = f - return { - "name": result["name"], - "config_file_name": result["config_file_name"], - "diffkemp_sha": result["diffkemp_sha"], - "equal": int(result["equal"]), - "non_equal": int(result["non_equal"]), - "unknown": int(result["unknown"]), - "error": int(result["error"]), - "no_differing": int(result["no_differing"]), - "date": datetime.fromisoformat(result["date"]), - "functions": functions, - "comparison_status": ComparisonStatus(result["comparison_status"]), - } diff --git a/backend/automation/models/results/versions.py b/backend/automation/models/results/versions.py index 7b74467..b744e55 100644 --- a/backend/automation/models/results/versions.py +++ b/backend/automation/models/results/versions.py @@ -128,15 +128,6 @@ def get_relative_viewer_path(self) -> Path: f"{self.old_tag}-{self.new_tag}" ) - @classmethod - def from_yaml(cls, result: dict) -> "ResultVersion": - kwargs = super()._parse_yaml_base(result) - return cls( - old_tag=result["old_tag"], - new_tag=result["new_tag"], - **kwargs, - ) - class ResultsVersions(): """Class for parsing results for versions."""