Skip to content

Commit

Permalink
Show spinner on first edit of source parameters.
Browse files Browse the repository at this point in the history
The spinner indicating that the latest measurement of a metric is not up-to-date with the latest source configuration would not appear on the first edit of a metric after upgrading to v5.12.0.

Fixes #8736.
  • Loading branch information
fniessink committed May 23, 2024
1 parent 76f5d03 commit 740ba85
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 62 deletions.
95 changes: 59 additions & 36 deletions components/api_server/src/initialization/migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,39 @@

import logging

import pymongo
from pymongo.collection import Collection
from pymongo.database import Database

from shared.model.metric import Metric


def perform_migrations(database: Database) -> None: # pragma: no feature-test-cover
"""Perform database migrations."""
change_accessibility_violation_metrics_to_violations(database)
fix_branch_parameters_without_value(database)
for report in database.reports.find(filter={"last": True, "deleted": {"$exists": False}}):
change_accessibility_violation_metrics_to_violations(database, report)
fix_branch_parameters_without_value(database, report)
add_source_parameter_hash(database, report)


def change_accessibility_violation_metrics_to_violations(database: Database) -> None: # pragma: no feature-test-cover
def change_accessibility_violation_metrics_to_violations( # pragma: no feature-test-cover
database: Database, report
) -> None:
"""Replace accessibility metrics with the violations metric."""
# Added after Quality-time v5.5.0, see https://github.com/ICTU/quality-time/issues/562
for report in database.reports.find(filter={"last": True, "deleted": {"$exists": False}}):
report_uuid = report["report_uuid"]
logging.info("Checking report for accessibility metrics: %s", report_uuid)
changed = False
for subject in report["subjects"].values():
for metric in subject["metrics"].values():
if metric["type"] == "accessibility":
change_accessibility_violations_metric_to_violations(metric)
changed = True
if changed:
logging.info("Updating report to change its accessibility metrics to violations metrics: %s", report_uuid)
replace_report(database, report)
else:
logging.info("No accessibility metrics found in report: %s", report_uuid)
report_uuid = report["report_uuid"]
logging.info("Checking report for accessibility metrics: %s", report_uuid)
changed = False
for subject in report["subjects"].values():
for metric in subject["metrics"].values():
if metric["type"] == "accessibility":
change_accessibility_violations_metric_to_violations(metric)
changed = True
if changed:
logging.info("Updating report to change its accessibility metrics to violations metrics: %s", report_uuid)
replace_document(database.reports, report)
else:
logging.info("No accessibility metrics found in report: %s", report_uuid)


def change_accessibility_violations_metric_to_violations(metric: dict) -> None: # pragma: no feature-test-cover
Expand All @@ -39,22 +46,21 @@ def change_accessibility_violations_metric_to_violations(metric: dict) -> None:
metric["unit"] = "accessibility violations"


def fix_branch_parameters_without_value(database: Database) -> None: # pragma: no feature-test-cover
def fix_branch_parameters_without_value(database: Database, report) -> None: # pragma: no feature-test-cover
"""Set the branch parameter of sources to 'master' (the previous default) if they have no value."""
# Added after Quality-time v5.11.0, see https://github.com/ICTU/quality-time/issues/8045
for report in database.reports.find(filter={"last": True, "deleted": {"$exists": False}}):
report_uuid = report["report_uuid"]
logging.info("Checking report for sources with empty branch parameters: %s", report_uuid)
changed = False
for source in sources_with_branch_parameter(report):
if not source["parameters"].get("branch"):
source["parameters"]["branch"] = "master"
changed = True
if changed:
logging.info("Updating report to change sources with empty branch parameter: %s", report_uuid)
replace_report(database, report)
else:
logging.info("No sources with empty branch parameters found in report: %s", report_uuid)
report_uuid = report["report_uuid"]
logging.info("Checking report for sources with empty branch parameters: %s", report_uuid)
changed = False
for source in sources_with_branch_parameter(report):
if not source["parameters"].get("branch"):
source["parameters"]["branch"] = "master"
changed = True
if changed:
logging.info("Updating report to change sources with empty branch parameter: %s", report_uuid)
replace_document(database.reports, report)
else:
logging.info("No sources with empty branch parameters found in report: %s", report_uuid)


METRICS_WITH_SOURCES_WITH_BRANCH_PARAMETER = {
Expand Down Expand Up @@ -87,8 +93,25 @@ def sources_with_branch_parameter(report: dict): # pragma: no feature-test-cove
yield source


def replace_report(database: Database, report) -> None: # pragma: no feature-test-cover
"""Replace the report in the database."""
report_id = report["_id"]
del report["_id"]
database.reports.replace_one({"_id": report_id}, report)
def add_source_parameter_hash(database: Database, report) -> None: # pragma: no feature-test-cover
"""Add source parameter hashes to the latest measurements."""
# Added after Quality-time v5.12.0, see https://github.com/ICTU/quality-time/issues/8736
for subject in report["subjects"].values():
for metric_uuid, metric in subject["metrics"].items():
latest_measurement = database.measurements.find_one(
filter={"metric_uuid": metric_uuid},
sort=[("start", pymongo.DESCENDING)],
)
if not latest_measurement:
continue
if latest_measurement.get("source_parameter_hash"):
continue
latest_measurement["source_parameter_hash"] = Metric({}, metric, metric_uuid).source_parameter_hash()
replace_document(database.measurements, latest_measurement)


def replace_document(collection: Collection, document) -> None: # pragma: no feature-test-cover
"""Replace the document in the collection."""
document_id = document["_id"]
del document["_id"]
collection.replace_one({"_id": document_id}, document)
76 changes: 50 additions & 26 deletions components/api_server/tests/initialization/test_migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,22 @@ def inserted_report(self, **kwargs):
return report


class NoOpMigrationTest(MigrationTestCase):
"""Unit tests for empty database and empty reports."""

def test_no_reports(self):
"""Test that the migration succeeds without reports."""
self.database.reports.find.return_value = []
perform_migrations(self.database)
self.database.reports.replace_one.assert_not_called()

def test_empty_reports(self):
"""Test that the migration succeeds when the report does not have anything to migrate."""
self.database.reports.find.return_value = [self.existing_report("issues")]
perform_migrations(self.database)
self.database.reports.replace_one.assert_not_called()


class ChangeAccessibilityViolationsTest(MigrationTestCase):
"""Unit tests for the accessibility violations database migration."""

Expand Down Expand Up @@ -55,18 +71,6 @@ def inserted_report(
metric_type="violations", metric_name=metric_name, metric_unit=metric_unit, **kwargs
)

def test_no_reports(self):
"""Test that the migration succeeds without reports."""
self.database.reports.find.return_value = []
perform_migrations(self.database)
self.database.reports.replace_one.assert_not_called()

def test_report_without_accessibility_metrics(self):
"""Test that the migration succeeds with reports, but without accessibility metrics."""
self.database.reports.find.return_value = [self.existing_report(metric_type="loc")]
perform_migrations(self.database)
self.database.reports.replace_one.assert_not_called()

def test_report_with_accessibility_metric(self):
"""Test that the migration succeeds with an accessibility metric."""
self.database.reports.find.return_value = [self.existing_report()]
Expand Down Expand Up @@ -101,22 +105,9 @@ def existing_report(
"""Extend to add sources and an extra metric without sources."""
report = super().existing_report(metric_type=metric_type)
report["subjects"][SUBJECT_ID]["metrics"][METRIC_ID2] = {"type": "issues"}
if sources:
report["subjects"][SUBJECT_ID]["metrics"][METRIC_ID]["sources"] = sources
report["subjects"][SUBJECT_ID]["metrics"][METRIC_ID]["sources"] = sources
return report

def test_no_reports(self):
"""Test that the migration succeeds without reports."""
self.database.reports.find.return_value = []
perform_migrations(self.database)
self.database.reports.replace_one.assert_not_called()

def test_report_without_branch_parameter(self):
"""Test that the migration succeeds with reports, but without metrics with a branch parameter."""
self.database.reports.find.return_value = [self.existing_report()]
perform_migrations(self.database)
self.database.reports.replace_one.assert_not_called()

def test_report_with_non_empty_branch_parameter(self):
"""Test that the migration succeeds when the branch parameter is not empty."""
self.database.reports.find.return_value = [
Expand All @@ -140,3 +131,36 @@ def test_report_with_branch_parameter_without_value(self):
}
inserted_report = self.inserted_report(metric_type="source_up_to_dateness", sources=inserted_sources)
self.database.reports.replace_one.assert_called_once_with({"_id": "id"}, inserted_report)


class SourceParameterHashMigrationTest(MigrationTestCase):
"""Unit tests for the source parameter hash database migration."""

def existing_report(self, sources: dict[SourceId, dict[str, str | dict[str, str]]] | None = None):
"""Extend to add sources and an extra metric without sources."""
report = super().existing_report(metric_type="loc")
report["subjects"][SUBJECT_ID]["metrics"][METRIC_ID2] = {"type": "issues"}
if sources:
report["subjects"][SUBJECT_ID]["metrics"][METRIC_ID]["sources"] = sources
return report

def test_report_with_sources_without_source_parameter_hash(self):
"""Test a report with sources and measurements."""
self.database.measurements.find_one.return_value = {"_id": "id", "metric_uuid": METRIC_ID}
self.database.reports.find.return_value = [self.existing_report(sources={SOURCE_ID: {"type": "cloc"}})]
perform_migrations(self.database)
inserted_measurement = {"metric_uuid": METRIC_ID, "source_parameter_hash": "8c3b464958e9ad0f20fb2e3b74c80519"}
self.database.measurements.replace_one.assert_called_once_with({"_id": "id"}, inserted_measurement)

def test_report_without_sources(self):
"""Test a report without sources."""
self.database.reports.find.return_value = [self.existing_report()]
perform_migrations(self.database)
self.database.measurements.replace_one.assert_not_called()

def test_metric_without_measurement(self):
"""Test a metric without measurements."""
self.database.measurements.find_one.return_value = None
self.database.reports.find.return_value = [self.existing_report(sources={SOURCE_ID: {"type": "cloc"}})]
perform_migrations(self.database)
self.database.measurements.replace_one.assert_not_called()
1 change: 1 addition & 0 deletions docs/src/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ If your currently installed *Quality-time* version is v4.10.0 or older, please r
- When creating an issue fails, show the reason in the toaster message instead of "undefined". Fixes [#8567](https://github.com/ICTU/quality-time/issues/8567).
- Hiding metrics without issues would not hide metrics with deleted issues. Fixes [#8699](https://github.com/ICTU/quality-time/issues/8699).
- The spinner indicating that the latest measurement of a metric is not up-to-date with the latest source configuration would not disappear if the measurement value made with the latest source configuration was equal to the measurement value made with the previous source configuration. Fixes [#8702](https://github.com/ICTU/quality-time/issues/8702).
- The spinner indicating that the latest measurement of a metric is not up-to-date with the latest source configuration would not appear on the first edit of a metric after upgrading to v5.12.0. Fixes [#8736](https://github.com/ICTU/quality-time/issues/8736).

### Added

Expand Down

0 comments on commit 740ba85

Please sign in to comment.