diff --git a/argus/backend/service/results_service.py b/argus/backend/service/results_service.py index 55a16a2..b01c9b8 100644 --- a/argus/backend/service/results_service.py +++ b/argus/backend/service/results_service.py @@ -127,13 +127,30 @@ class RunsDetails: shapes = ["circle", "triangle", "rect", "star", "dash", "crossRot", "line"] -def get_sorted_data_for_column_and_row(data: List[ArgusGenericResultData], column: str, row: str) -> List[Dict[str, Any]]: - return sorted([{"x": entry.sut_timestamp.strftime('%Y-%m-%dT%H:%M:%SZ'), +def get_sorted_data_for_column_and_row(data: List[ArgusGenericResultData], column: str, row: str, + runs_details: RunsDetails) -> List[Dict[str, Any]]: + points = sorted([{"x": entry.sut_timestamp.strftime('%Y-%m-%dT%H:%M:%SZ'), "y": entry.value, - "id": entry.run_id} + "id": entry.run_id, + } for entry in data if entry.column == column and entry.row == row], key=lambda point: point["x"]) - + if not points: + return points + packages = runs_details.packages + points[0]['changes'] = [] + prev_versions = {pkg.name: pkg.version for pkg in packages.get(points[0]["id"], [])} + for point in points[1:]: + changes = [] + current_versions = {pkg.name: pkg.version for pkg in packages.get(point["id"], [])} + for pkg_name in current_versions.keys() | prev_versions.keys(): + curr_ver = current_versions.get(pkg_name) + prev_ver = prev_versions.get(pkg_name) + if curr_ver != prev_ver: + changes.append({'name': pkg_name, 'prev_version': prev_ver, 'curr_version': curr_ver}) + point['changes'] = [f"{change['name']}: {change['prev_version']} -> {change['curr_version']}" for change in changes] + prev_versions = current_versions + return points def get_min_max_y(datasets: List[Dict[str, Any]]) -> (float, float): """0.5 - 1.5 of min/max of 50% results""" @@ -191,7 +208,8 @@ def calculate_limits(points: List[dict], best_results: List, validation_rules_li def create_datasets_for_column(table: ArgusGenericResultMetadata, data: list[ArgusGenericResultData], - best_results: dict[str, List[BestResult]], releases_map: ReleasesMap, column: ColumnMetadata) -> List[Dict]: + best_results: dict[str, List[BestResult]], releases_map: ReleasesMap, column: ColumnMetadata, + runs_details: RunsDetails) -> List[Dict]: """ Create datasets (series) for a specific column, splitting by version and showing limit lines. """ @@ -200,7 +218,7 @@ def create_datasets_for_column(table: ArgusGenericResultMetadata, data: list[Arg for idx, row in enumerate(table.rows_meta): line_color = colors[idx % len(colors)] - points = get_sorted_data_for_column_and_row(data, column.name, row) + points = get_sorted_data_for_column_and_row(data, column.name, row, runs_details) datasets.extend(create_release_datasets(points, row, releases_map, line_color)) @@ -226,7 +244,7 @@ def create_release_datasets(points: list[Dict], row: str, releases_map: Releases "label": f"{release} - {row}", "borderColor": line_color, "borderWidth": 2, - "pointRadius": 2, + "pointRadius": 3, "showLine": True, "data": release_points, "pointStyle": shapes[v_idx % len(shapes)] @@ -324,7 +342,7 @@ def _split_results_by_release(packages: dict[str, list[PackageVersion]], main_pa def create_chartjs(table: ArgusGenericResultMetadata, data: list[ArgusGenericResultData], best_results: dict[str, List[BestResult]], - releases_map: ReleasesMap) -> List[Dict]: + releases_map: ReleasesMap, runs_details: RunsDetails) -> List[Dict]: """ Create Chart.js-compatible graph for each column in the table. """ @@ -332,7 +350,7 @@ def create_chartjs(table: ArgusGenericResultMetadata, data: list[ArgusGenericRes columns = [column for column in table.columns_meta if column.type != "TEXT"] for column in columns: - datasets = create_datasets_for_column(table, data, best_results, releases_map, column) + datasets = create_datasets_for_column(table, data, best_results, releases_map, column, runs_details) if datasets: min_y, max_y = get_min_max_y(datasets) @@ -430,8 +448,7 @@ def get_test_graphs(self, test_id: UUID, start_date: datetime | None = None, end best_results = self.get_best_results(test_id=test_id, name=table.name) main_package = _identify_most_changed_package([pkg for sublist in runs_details.packages.values() for pkg in sublist]) releases_map = _split_results_by_release(runs_details.packages, main_package=main_package) - graphs.extend(create_chartjs(table, data, best_results, - releases_map=releases_map)) + graphs.extend(create_chartjs(table, data, best_results, releases_map=releases_map, runs_details=runs_details)) releases_filters.update(releases_map.keys()) ticks = calculate_graph_ticks(graphs) return graphs, ticks, list(releases_filters) diff --git a/argus/backend/tests/results_service/test_chartjs_additional_functions.py b/argus/backend/tests/results_service/test_chartjs_additional_functions.py index 9cab7bb..59cbeec 100644 --- a/argus/backend/tests/results_service/test_chartjs_additional_functions.py +++ b/argus/backend/tests/results_service/test_chartjs_additional_functions.py @@ -14,7 +14,7 @@ create_limit_dataset, calculate_limits, calculate_graph_ticks, _identify_most_changed_package, _split_results_by_release, - BestResult + BestResult, RunsDetails ) from argus.backend.models.result import ArgusGenericResultMetadata, ArgusGenericResultData, ColumnMetadata, ValidationRules @@ -49,24 +49,32 @@ def test_split_results_by_versions_should_group_correctly(package_data): def test_get_sorted_data_for_column_and_row(): + run_id1 = uuid4() + run_id2 = uuid4() + run_id3 = uuid4() data = [ - ArgusGenericResultData(run_id=uuid4(), column="col1", row="row1", value=1.5, status="PASS", sut_timestamp=datetime(2023, 10, 23)), - ArgusGenericResultData(run_id=uuid4(), column="col1", row="row1", value=2.5, status="PASS", sut_timestamp=datetime(2023, 10, 24)), - ArgusGenericResultData(run_id=uuid4(), column="col1", row="row1", value=0.5, status="PASS", sut_timestamp=datetime(2023, 10, 22)), - ArgusGenericResultData(run_id=uuid4(), column="col1", row="row2", value=3.5, status="PASS", sut_timestamp=datetime(2023, 10, 25)), - ArgusGenericResultData(run_id=uuid4(), column="col2", row="row1", value=4.5, status="PASS", sut_timestamp=datetime(2023, 10, 26)), + ArgusGenericResultData(run_id=run_id1, column="col1", row="row1", value=1.5, status="PASS", + sut_timestamp=datetime(2023, 10, 23)), + ArgusGenericResultData(run_id=run_id2, column="col1", row="row1", value=2.5, status="PASS", + sut_timestamp=datetime(2023, 10, 24)), + ArgusGenericResultData(run_id=run_id3, column="col1", row="row1", value=0.5, status="PASS", + sut_timestamp=datetime(2023, 10, 22)), ] - result = get_sorted_data_for_column_and_row(data, "col1", "row1") + packages = { + run_id3: [PackageVersion(name='pkg1', version='1.0', date='', revision_id='', build_id='')], + run_id1: [PackageVersion(name='pkg1', version='1.0', date='', revision_id='', build_id=''), + PackageVersion(name='pkg2', version='1.0', date='', revision_id='', build_id='')], + run_id2: [PackageVersion(name='pkg1', version='1.1', date='', revision_id='', build_id='')], + } + runs_details = RunsDetails(ignored=[], packages=packages) + result = get_sorted_data_for_column_and_row(data, "col1", "row1", runs_details) expected = [ - {"x": "2023-10-22T00:00:00Z", "y": 0.5}, - {"x": "2023-10-23T00:00:00Z", "y": 1.5}, - {"x": "2023-10-24T00:00:00Z", "y": 2.5}, + {"x": "2023-10-22T00:00:00Z", "y": 0.5, "changes": []}, + {"x": "2023-10-23T00:00:00Z", "y": 1.5, "changes": ["pkg2: None -> 1.0"]}, + {"x": "2023-10-24T00:00:00Z", "y": 2.5, "changes": ["pkg2: 1.0 -> None", "pkg1: 1.0 -> 1.1"]}, ] - - result_without_id = [{"x": item["x"], "y": item["y"]} for item in result] - - assert result_without_id == expected - + result_data = [{"x": item["x"], "y": item["y"], "changes": item["changes"]} for item in result] + assert result_data == expected def test_get_min_max_y(): datasets = [ @@ -138,7 +146,8 @@ def test_create_datasets_for_column(): best_results = {} releases_map = {"2024.2": [point.run_id for point in data][:1], "2024.3": [point.run_id for point in data][2:]} column = table.columns_meta[0] - datasets = create_datasets_for_column(table, data, best_results, releases_map, column) + runs_details = RunsDetails(ignored=[], packages={}) + datasets = create_datasets_for_column(table, data, best_results, releases_map, column, runs_details) assert len(datasets) == 2 labels = [dataset["label"] for dataset in datasets] assert "2024.2 - row1" in labels diff --git a/argus/backend/tests/results_service/test_create_chartjs.py b/argus/backend/tests/results_service/test_create_chartjs.py index 29ef1c4..df1c044 100644 --- a/argus/backend/tests/results_service/test_create_chartjs.py +++ b/argus/backend/tests/results_service/test_create_chartjs.py @@ -2,7 +2,7 @@ from uuid import uuid4 from argus.backend.models.result import ArgusGenericResultMetadata, ColumnMetadata, ArgusGenericResultData, ValidationRules -from argus.backend.service.results_service import create_chartjs, BestResult +from argus.backend.service.results_service import create_chartjs, BestResult, RunsDetails def test_create_chartjs_without_validation_rules_should_create_chart_without_limits_series(): @@ -31,7 +31,8 @@ def test_create_chartjs_without_validation_rules_should_create_chart_without_lim 'col1:row1': [BestResult(key='col1:row1', value=100.0, result_date=datetime(2021, 1, 1), run_id=str(uuid4()))] } releases_map = {"1.0": [point.run_id for point in data]} - graphs = create_chartjs(table, data, best_results, releases_map) + runs_details = RunsDetails(ignored=[], packages={}) + graphs = create_chartjs(table, data, best_results, releases_map, runs_details) assert len(graphs) == 1 assert len(graphs[0]['data']['datasets']) == 1 # no limits series @@ -59,7 +60,8 @@ def test_create_chartjs_without_best_results_should_not_fail(): ] best_results = {} releases_map = {"1.0": [point.run_id for point in data]} - graphs = create_chartjs(table, data, best_results, releases_map) + runs_details = RunsDetails(ignored=[], packages={}) + graphs = create_chartjs(table, data, best_results, releases_map, runs_details) assert len(graphs) == 1 assert len(graphs[0]['data']['datasets']) == 1 # no limits series @@ -91,7 +93,8 @@ def test_create_chartjs_with_validation_rules_should_add_limit_series(): 'col1:row1': [BestResult(key='col1:row1', value=100.0, result_date=datetime(2021, 1, 1), run_id=str(uuid4()))] } releases_map = {"1.0": [point.run_id for point in data]} - graphs = create_chartjs(table, data, best_results, releases_map) + runs_details = RunsDetails(ignored=[], packages={}) + graphs = create_chartjs(table, data, best_results, releases_map, runs_details) assert 'limit' in graphs[0]['data']['datasets'][0]['data'][0] def test_chartjs_with_multiple_best_results_and_validation_rules_should_adjust_limits_for_each_point(): @@ -138,7 +141,8 @@ def test_chartjs_with_multiple_best_results_and_validation_rules_should_adjust_l ] } releases_map = {"1.0": [point.run_id for point in data]} - graphs = create_chartjs(table, data, best_results, releases_map) + runs_details = RunsDetails(ignored=[], packages={}) + graphs = create_chartjs(table, data, best_results, releases_map, runs_details) datasets = graphs[0]['data']['datasets'] limits = [point.get('limit') for dataset in datasets for point in dataset['data'] if 'limit' in point] assert len(limits) == 2 @@ -156,7 +160,8 @@ def test_create_chartjs_no_data_should_not_fail(): data = [] best_results = {} releases_map = {"1.0": []} - graphs = create_chartjs(table, data, best_results, releases_map) + runs_details = RunsDetails(ignored=[], packages={}) + graphs = create_chartjs(table, data, best_results, releases_map, runs_details) assert len(graphs) == 0 def test_create_chartjs_multiple_columns_and_rows(): @@ -203,7 +208,8 @@ def test_create_chartjs_multiple_columns_and_rows(): ] } releases_map = {"1.0": [point.run_id for point in data]} - graphs = create_chartjs(table, data, best_results, releases_map) + runs_details = RunsDetails(ignored=[], packages={}) + graphs = create_chartjs(table, data, best_results, releases_map, runs_details) assert len(graphs) == 2 assert len(graphs[0]['data']['datasets']) == 2 # should have also limits dataset assert len(graphs[1]['data']['datasets']) == 1 # no limits series diff --git a/frontend/TestRun/ResultsGraph.svelte b/frontend/TestRun/ResultsGraph.svelte index cf0e6f5..fb404b4 100644 --- a/frontend/TestRun/ResultsGraph.svelte +++ b/frontend/TestRun/ResultsGraph.svelte @@ -74,14 +74,16 @@ graph.options.responsive = false; graph.options.lazy = true; graph.options.plugins.tooltip = { + usePointStyle: true, callbacks: { label: function (tooltipItem) { const y = tooltipItem.parsed.y.toFixed(2); const x = new Date(tooltipItem.parsed.x).toLocaleDateString("sv-SE"); const ori = tooltipItem.raw.ori; const limit = tooltipItem.raw.limit; - return `${x}: ${ori ? ori : y} (limit: ${limit?.toFixed(2) || "N/A"})`; - } + const changes = tooltipItem.raw.changes; + return [`${x}: ${ori ? ori : y} (limit: ${limit?.toFixed(2) || "N/A"})`, ...changes]; + }, } }; graph.options.scales.x.min = ticks["min"]; @@ -94,6 +96,14 @@ } } }; + + graph.data.datasets.forEach((dataset) => { + const pointBackgroundColors = dataset.data.map((point) => + point.changes?.length > 0 ? 'white' : dataset.backgroundColor || dataset.borderColor + ); + dataset.pointBackgroundColor = pointBackgroundColors; + }); + chart = new Chart( document.getElementById(`graph-${test_id}-${index}`), {type: "scatter", data: graph.data, options: {...graph.options, ...actions}}