diff --git a/plugins/tensorboard-plugins/tb_graph_ascend/pytest.ini b/plugins/tensorboard-plugins/tb_graph_ascend/pytest.ini new file mode 100644 index 0000000000000000000000000000000000000000..5784e1ce16f029a93ac9dbbe1960a49133f39325 --- /dev/null +++ b/plugins/tensorboard-plugins/tb_graph_ascend/pytest.ini @@ -0,0 +1,8 @@ +[pytest] +testpaths = + test/unit + test/integration + +markers = + unit: unit tests + integration: integration tests \ No newline at end of file diff --git a/plugins/tensorboard-plugins/tb_graph_ascend/server/app/controllers/hierarchy.py b/plugins/tensorboard-plugins/tb_graph_ascend/server/app/controllers/hierarchy.py index 1a83932002196f47ece21cc99069fac231469eb3..33bd1b7bae092c2c538d5837a64fe8bb7ed78dad 100644 --- a/plugins/tensorboard-plugins/tb_graph_ascend/server/app/controllers/hierarchy.py +++ b/plugins/tensorboard-plugins/tb_graph_ascend/server/app/controllers/hierarchy.py @@ -292,7 +292,7 @@ class Hierarchy: for node_name, node_info in self.current_hierarchy.items(): graph_node_info = self.graph.get('node', {}).get(node_name, {}) node_info['matchedNodeLink'] = graph_node_info.get('matched_node_link', []) - node_info['precisionIndex'] = graph_node_info.get('data', {}).get('precision_index', "NaN"), # 精度 + node_info['precisionIndex'] = graph_node_info.get('data', {}).get('precision_index', "NaN") return self.current_hierarchy def get_hierarchy(self): diff --git a/plugins/tensorboard-plugins/tb_graph_ascend/server/app/controllers/layout_hierarchy_controller.py b/plugins/tensorboard-plugins/tb_graph_ascend/server/app/controllers/layout_hierarchy_controller.py index 6bc27b96cda5476ff6583c332f372c9ee0223d99..a88f09474b7526fe72b037f7a57b2dbb663ffb93 100644 --- a/plugins/tensorboard-plugins/tb_graph_ascend/server/app/controllers/layout_hierarchy_controller.py +++ b/plugins/tensorboard-plugins/tb_graph_ascend/server/app/controllers/layout_hierarchy_controller.py @@ -13,7 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. # ============================================================================== -import time from .hierarchy import Hierarchy @@ -32,7 +31,7 @@ class LayoutHierarchyController: def change_expand_state(node_name, graph_type, graph, micro_step): if node_name == 'root': LayoutHierarchyController.hierarchy[graph_type] = Hierarchy(graph_type, graph, micro_step) - elif LayoutHierarchyController.hierarchy[graph_type]: + elif LayoutHierarchyController.hierarchy.get(graph_type, None): LayoutHierarchyController.hierarchy[graph_type].update_graph_data(node_name, graph) LayoutHierarchyController.hierarchy[graph_type].update_graph_shape() LayoutHierarchyController.hierarchy[graph_type].update_graph_position() diff --git a/plugins/tensorboard-plugins/tb_graph_ascend/server/app/controllers/match_nodes_controller.py b/plugins/tensorboard-plugins/tb_graph_ascend/server/app/controllers/match_nodes_controller.py index e2e500c133494f0b9efb5900008aaf8fb6a09a8e..a15005adbe008467cb8b18f5fbadae9fb958df3d 100644 --- a/plugins/tensorboard-plugins/tb_graph_ascend/server/app/controllers/match_nodes_controller.py +++ b/plugins/tensorboard-plugins/tb_graph_ascend/server/app/controllers/match_nodes_controller.py @@ -25,12 +25,19 @@ class MatchNodesController: def is_same_node_type(graph_data, npu_node_name, bench_node_name): npu_node_type = graph_data.get('NPU', {}).get('node', {}).get(npu_node_name, {}).get('node_type') bench_node_type = graph_data.get('Bench', {}).get('node', {}).get(bench_node_name, {}).get('node_type') + if npu_node_type is None or bench_node_type is None or npu_node_type != bench_node_type: return False return True @staticmethod def process_task_add(graph_data, npu_node_name, bench_node_name, task): + if not MatchNodesController.is_same_node_type(graph_data, npu_node_name, bench_node_name): + return { + 'success': False, + 'error': '节点类型不一致,无法添加匹配关系' + } + result = {} if task == 'md5': result = MatchNodesController.process_md5_task_add(graph_data, npu_node_name, bench_node_name) @@ -125,7 +132,6 @@ class MatchNodesController: for key in common_keys: npu_subnode_list = npu_match_names.get(key, []) bench_subnode_list = bench_match_names.get(key, []) - # 多个节点可能有一个module name for npu_subnode_name, bench_subnode_name in zip(npu_subnode_list, bench_subnode_list): result = MatchNodesController.process_task_add(graph_data, npu_subnode_name, bench_subnode_name, @@ -193,6 +199,7 @@ class MatchNodesController: npu_subnodes = npu_nodes.get(npu_node_name, {}).get('subnodes', []) bench_subnodes = bench_nodes.get(bench_node_name, {}).get('subnodes', []) + if result.get('success') and npu_subnodes and bench_subnodes: process_child_layer(npu_subnodes) if result.get('success'): @@ -233,8 +240,8 @@ class MatchNodesController: @staticmethod def process_summary_task_add(graph_data, npu_node_name, bench_node_name): # 节点信息提取 - npu_node_data = graph_data.get('NPU', {}).get('node', {}).get(npu_node_name) - bench_node_data = graph_data.get('Bench', {}).get('node', {}).get(bench_node_name) + npu_node_data = graph_data.get('NPU', {}).get('node', {}).get(npu_node_name, {}) + bench_node_data = graph_data.get('Bench', {}).get('node', {}).get(bench_node_name, {}) # 计算统计误差 intput_statistical_diff = MatchNodesController.calculate_statistical_diff( npu_node_data.get('input_data'), bench_node_data.get('input_data'), npu_node_name, bench_node_name diff --git a/plugins/tensorboard-plugins/tb_graph_ascend/server/app/service/graph_service.py b/plugins/tensorboard-plugins/tb_graph_ascend/server/app/service/graph_service.py index 9aa68910b334566719bddd3e2c28ac56acdc0c3f..43c05357fc69489431b964c853f9a49c1faae973 100644 --- a/plugins/tensorboard-plugins/tb_graph_ascend/server/app/service/graph_service.py +++ b/plugins/tensorboard-plugins/tb_graph_ascend/server/app/service/graph_service.py @@ -31,9 +31,11 @@ class GraphService: @staticmethod def load_meta_dir(is_safe_check): """Scan logdir for directories containing .vis files, modified to return a tuple of (run, tag).""" + logdir = GraphState.get_global_value('logdir') runs = GraphState.get_global_value('runs', {}) first_run_tags = GraphState.get_global_value('first_run_tags', {}) + meta_dir = {} error_list = [] for root, _, files in GraphUtils.walk_with_max_depth(logdir, 2): @@ -365,6 +367,8 @@ class GraphService: @staticmethod def save_data(meta_data): + if not meta_data: + return {'success': False, 'error': '参数为空'} graph_data, error_message = GraphUtils.get_graph_data(meta_data) if error_message: return {'success': False, 'error': error_message} @@ -381,6 +385,8 @@ class GraphService: @staticmethod def save_matched_relations(meta_data): + if not meta_data: + return {'success': False, 'error': '参数为空'} config_data = GraphState.get_global_value("config_data") # 匹配列表和未匹配列表 npu_match_nodes_list = config_data.get('manualMatchNodes', {}) diff --git a/plugins/tensorboard-plugins/tb_graph_ascend/server/app/utils/graph_utils.py b/plugins/tensorboard-plugins/tb_graph_ascend/server/app/utils/graph_utils.py index 497f11e37fbe2f5b49564868518c5885ec492bb4..a862b4dbc42eff7e733d41ad61c45d98c125050c 100644 --- a/plugins/tensorboard-plugins/tb_graph_ascend/server/app/utils/graph_utils.py +++ b/plugins/tensorboard-plugins/tb_graph_ascend/server/app/utils/graph_utils.py @@ -473,16 +473,3 @@ class GraphUtils: return sorted_data - @staticmethod - def process_vis_file(dir_path, file, run_tag_pairs): - file_path = os.path.join(dir_path, file) - if os.path.isfile(file_path) and file.endswith('.vis'): - run = dir_path - run_name = os.path.basename(run) - GraphState.set_global_value('runs', run, run_name) - tag = file[:-4] # Use the filename without extension as tag - _, error = GraphUtils.safe_load_data(run_name, tag, True) - if error: - logger.error(f'Error: File run:"{run}, tag:{tag}" is not accessible. Error: {error}') - return - run_tag_pairs.setdefault(run_name, []).append(tag) diff --git a/plugins/tensorboard-plugins/tb_graph_ascend/server/plugin.py b/plugins/tensorboard-plugins/tb_graph_ascend/server/plugin.py index 19284303899878eba24c0f2cef26562a023ef8dc..2c65227a97413679521a4dbb1fba69539e90647a 100644 --- a/plugins/tensorboard-plugins/tb_graph_ascend/server/plugin.py +++ b/plugins/tensorboard-plugins/tb_graph_ascend/server/plugin.py @@ -47,6 +47,7 @@ class GraphsPlugin(base_plugin.TBPlugin): context: A base_plugin.TBContext instance. """ super().__init__(context) + GraphState.reset_global_state() self._data_provider = context.data_provider self.logdir = os.path.abspath(os.path.expanduser(context.logdir.rstrip('/'))) # 将logdir赋值给global_state中的logdir属性,方便其他模块使用 diff --git a/plugins/tensorboard-plugins/tb_graph_ascend/test/conftest.py b/plugins/tensorboard-plugins/tb_graph_ascend/test/conftest.py index 4c582b8162f97cc50ea59fea807c0251c8c47f45..93be4c86c2b93705a66e2072f30ca8c462ee65ee 100644 --- a/plugins/tensorboard-plugins/tb_graph_ascend/test/conftest.py +++ b/plugins/tensorboard-plugins/tb_graph_ascend/test/conftest.py @@ -20,14 +20,13 @@ from data.test_case_factory import TestCaseFactory @pytest.fixture(scope="function", autouse=True) -def reset_global_state(): +def reset_global_state(request): """每个测试后重置全局状态""" - # 执行测试 yield - # 恢复原始状态 - GraphState.init_defaults() + if request.module.__name__ != "test_graph_views": + GraphState.init_defaults() def pytest_addoption(parser): diff --git a/plugins/tensorboard-plugins/tb_graph_ascend/test/data/test_case_factory.py b/plugins/tensorboard-plugins/tb_graph_ascend/test/data/test_case_factory.py deleted file mode 100644 index afdcb1b07df5b2d016586dd2f4537d54b1afa37d..0000000000000000000000000000000000000000 --- a/plugins/tensorboard-plugins/tb_graph_ascend/test/data/test_case_factory.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright (c) 2025, Huawei Technologies. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ============================================================================== -import json -import os - - -class TestCaseFactory: - """管理所有测试用例的统一工厂""" - - CASE_DIR = os.path.join(os.path.dirname(__file__), 'ut_test_cases') - - @classmethod - def get_process_task_add_cases(cls): - return cls._load_cases('test_match_node_controller\\process_task_add_case.json') - - @classmethod - def get_process_task_delete_cases(cls): - return cls._load_cases('test_match_node_controller\\process_task_delete_case.json') - - @classmethod - def _load_cases(cls, filename): - """从JSON文件加载测试用例""" - path = os.path.join(cls.CASE_DIR, filename) - with open(path, 'r', encoding='utf-8') as f: - return json.load(f) diff --git a/plugins/tensorboard-plugins/tb_graph_ascend/test/data/ut_test_cases/test_layout_hierarchy_controller/change_expand_state_case.json b/plugins/tensorboard-plugins/tb_graph_ascend/test/data/ut_test_cases/test_layout_hierarchy_controller/change_expand_state_case.json deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/plugins/tensorboard-plugins/tb_graph_ascend/test/data/ut_test_cases/test_layout_hierarchy_controller/update_hierarchy_data_case.json b/plugins/tensorboard-plugins/tb_graph_ascend/test/data/ut_test_cases/test_layout_hierarchy_controller/update_hierarchy_data_case.json deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/plugins/tensorboard-plugins/tb_graph_ascend/test/data/ut_test_cases/test_match_node_controller/process_task_add_case.json b/plugins/tensorboard-plugins/tb_graph_ascend/test/data/ut_test_cases/test_match_node_controller/process_task_add_case.json deleted file mode 100644 index d4f2989192e85d68e6f53d4b3fb777b774114ad8..0000000000000000000000000000000000000000 --- a/plugins/tensorboard-plugins/tb_graph_ascend/test/data/ut_test_cases/test_match_node_controller/process_task_add_case.json +++ /dev/null @@ -1,385 +0,0 @@ -[ - { - "case_id": 1, - "description": "测试无效任务类型", - "input": { - "graph_data": { - "NPU": { - "node": { - "npu_node": { - "node_type": "Module", - "input_data": { - "input1": { - "md5": "abcd1234" - } - }, - "output_data": { - "output1": { - "md5": "efgh5678" - } - } - } - } - }, - "Bench": { - "node": { - "bench_node": { - "node_type": "Module", - "input_data": { - "input1": { - "md5": "abcd1234" - } - }, - "output_data": { - "output1": { - "md5": "efgh5678" - } - } - } - } - } - }, - "npu_node_name": "npu_node", - "bench_node_name": "bench_node", - "task": "invalid_task" - }, - "expected": { - "success": false, - "error": "task类型错误" - } - }, - { - "case_id": 2, - "description": "测试MD5任务匹配成功", - "input": { - "graph_data": { - "NPU": { - "node": { - "npu_node": { - "node_type": "Module", - "matched_node_link": [], - "data": {}, - "input_data": { - "input_arg0": { - "md5": "1234567890abcdef" - }, - "input_arg1": { - "md5": "abcdef1234567890" - }, - "output_data": { - "output_arg0": { - "md5": "abcdef1234567890" - }, - "output_arg1": { - "md5": "1234567890abcdef" - } - } - } - } - }, - "Bench": { - "node": { - "bench_node": { - "node_type": "Module", - "matched_node_link": [], - "input_data": { - "input_arg0": { - "md5": "1234567890abcdef" - }, - "input_arg1": { - "md5": "abcdef1234567890" - }, - "output_data": { - "output_arg0": { - "md5": "1234567890abcdef" - }, - "output_arg1": { - "md5": "1234567890abcdef" - } - } - } - } - } - } - } - }, - "npu_node_name": "npu_node", - "bench_node_name": "bench_node", - "task": "md5" - }, - "expected": { - "success": true - } - }, - { - "case_id": 3, - "description": "测试MD5任务添加失败(节点数据缺失)", - "input": { - "graph_data": { - "NPU": { - "node": {} - }, - "Bench": { - "node": { - "bench_node": { - "node_type": "Module", - "input_data": { - "input1": { - "md5": "abcd1234" - } - }, - "output_data": { - "output1": { - "md5": "efgh5678" - } - } - } - } - } - }, - "npu_node_name": "npu_node", - "bench_node_name": "bench_node", - "task": "md5" - }, - "expected": { - "success": true - } - }, - { - "case_id": 4, - "description": "summary任务匹配", - "input": { - "graph_data": { - "NPU": { - "node": { - "npu_node_summary": { - "node_type": "Module", - "input_data": { - "input1": { - "Max": 1.0, - "Min": 0.1, - "Mean": 0.5, - "Norm": 0.7 - }, - "input2": { - "Max": 1.0, - "Min": 0.1, - "Mean": 0.5, - "Norm": 0.7 - } - }, - "output_data": { - "output1": { - "Max": 2.0, - "Min": 0.2, - "Mean": 1.0, - "Norm": 1.4 - }, - "output2": { - "Max": 2.0, - "Min": 0.2, - "Mean": 1.0, - "Norm": 1.4 - } - }, - "matched_node_link": [] - } - } - }, - "Bench": { - "node": { - "bench_node_summary": { - "node_type": "Module", - "input_data": { - "input1": { - "Max": 1.0, - "Min": 0.1, - "Mean": 0.5, - "Norm": 0.7 - }, - "input2": { - "Max": 1.0, - "Min": 0.1, - "Mean": 0.5, - "Norm": 0.7 - } - }, - "output_data": { - "output1": { - "Max": 2.0, - "Min": 0.2, - "Mean": 1.0, - "Norm": 1.4 - }, - "output2": { - "Max": 2.0, - "Min": 0.2, - "Mean": 1.0, - "Norm": 1.4 - } - }, - "matched_node_link": [] - } - } - } - }, - "npu_node_name": "npu_node_summary", - "bench_node_name": "bench_node_summary", - "task": "summary" - }, - "expected": { - "success": true - } - }, - { - "case_id": 5, - "description": "测试SUMMARY任务添加失败(输入输出数据异常)", - "input": { - "graph_data": { - "NPU": { - "node": { - "npu_node": { - "node_type": "Module", - "input_data": {}, - "output_data": { - "output1": { - "Max": "1.0" - } - } - } - } - }, - "Bench": { - "node": { - "bench_node": { - "node_type": "Module", - "input_data": { - "input1": { - "Max": "1.0" - } - }, - "output_data": {} - } - } - } - }, - "npu_node_name": "npu_node", - "bench_node_name": "bench_node", - "task": "summary" - }, - "expected": { - "success": false, - "error": "输入或输出统计误差值为空(Input and output statistical error calculation failed)" - } - }, - { - "case_id": 6, - "description": "测试SUMMARY任务添加失败(精度计算异常)", - "type": "process_task_add", - "input": { - "graph_data": { - "NPU": { - "node": { - "npu_node": { - "node_type": "Module", - "input_data": { - "input1": { - "Max": "1.0" - } - }, - "output_data": { - "output1": { - "Max": "invalid_value" - } - } - } - } - }, - "Bench": { - "node": { - "bench_node": { - "node_type": "Module", - "input_data": { - "input1": { - "Max": "1.0" - } - }, - "output_data": { - "output1": { - "Max": "1.0" - } - } - } - } - } - }, - "npu_node_name": "npu_node", - "bench_node_name": "bench_node", - "task": "summary" - }, - "expected": { - "success": false, - "error": "输出统计误差值为空,计算精度误差失败(Calculation of precision error failed)" - } - }, - { - "case_id": 7, - "description": "测试SUMMARY任务添加成功(避免除零错误)", - "input": { - "graph_data": { - "NPU": { - "node": { - "npu_node": { - "node_type": "Module", - "input_data": { - "input1": { - "Max": "1.0", - "Min": "0.1", - "Norm": "0.5", - "Mean": "0.5" - } - }, - "output_data": { - "output1": { - "Max": "0.0", - "Min": "0.0", - "Norm": "0.0", - "Mean": "0.0" - } - } - } - } - }, - "Bench": { - "node": { - "bench_node": { - "node_type": "Module", - "input_data": { - "input1": { - "Max": "1.0", - "Min": "0.1", - "Norm": "0.5", - "Mean": "0.5" - } - }, - "output_data": { - "output1": { - "Max": "0.0", - "Min": "0.0", - "Norm": "0.0", - "Mean": "0.0" - } - } - } - } - } - }, - "npu_node_name": "npu_node", - "bench_node_name": "bench_node", - "task": "summary" - }, - "expected": { - "success": true - } - } -] \ No newline at end of file diff --git a/plugins/tensorboard-plugins/tb_graph_ascend/test/data/ut_test_cases/test_match_node_controller/process_task_delete_case.json b/plugins/tensorboard-plugins/tb_graph_ascend/test/data/ut_test_cases/test_match_node_controller/process_task_delete_case.json deleted file mode 100644 index 802d474fec73bcc0e32163e5faf60bbda7d64d26..0000000000000000000000000000000000000000 --- a/plugins/tensorboard-plugins/tb_graph_ascend/test/data/ut_test_cases/test_match_node_controller/process_task_delete_case.json +++ /dev/null @@ -1,74 +0,0 @@ -[ - { - "case_id": 1, - "description": "测试无效任务类型", - "input": { - "graph_data": { - "NPU": { - "node": { - "npu_node": { - "node_type": "Module", - "matched_node_link": [ - "bench_node" - ], - "data": { - "precision_index": 0.95 - } - } - } - }, - "Bench": { - "node": { - "bench_node": { - "node_type": "Module", - "matched_node_link": [ - "npu_node" - ] - } - } - } - }, - "npu_node_name": "npu_node", - "bench_node_name": "bench_node", - "task": "invalid_task" - }, - "expected": { - "success": false, - "error": "task类型错误" - } - }, - { - "case_id": 3, - "description": "测试MD5任务删除失败(节点未匹配)", - "input": { - "graph_data": { - "NPU": { - "node": { - "npu_node": { - "node_type": "Module", - "matched_node_link": [], - "data": { - "precision_index": 0.95 - } - } - } - }, - "Bench": { - "node": { - "bench_node": { - "node_type": "Module", - "matched_node_link": [] - } - } - } - }, - "npu_node_name": "npu_node", - "bench_node_name": "bench_node", - "task": "md5" - }, - "expected": { - "success": false, - "error": "操作失败:节点未匹配,请先匹配节点" - } - } -] \ No newline at end of file diff --git a/plugins/tensorboard-plugins/tb_graph_ascend/test/integration/views/test_graph_views.py b/plugins/tensorboard-plugins/tb_graph_ascend/test/integration/views/test_graph_views.py new file mode 100644 index 0000000000000000000000000000000000000000..66e481c2477e6fea6b3e5735b64c49fa8f6077d6 --- /dev/null +++ b/plugins/tensorboard-plugins/tb_graph_ascend/test/integration/views/test_graph_views.py @@ -0,0 +1,225 @@ +# Copyright (c) 2025, Huawei Technologies. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +import json +from pathlib import Path +from types import SimpleNamespace + +import pytest + +from werkzeug.wrappers import Request +from werkzeug.test import EnvironBuilder +from data.test_case_factory import TestCaseFactory +from server.app.utils.global_state import GraphState +from server.app.views.graph_views import GraphView + + +@pytest.mark.integration +class TestGraphViews: + + captured = SimpleNamespace(status=None, headers=None) + + mock_vis_tag = 'mock_compare_resnet_data' + + @staticmethod + def start_response(status, response_headers): + TestGraphViews.captured.status = status + TestGraphViews.captured.headers = dict(response_headers) + return lambda x: None # 必须返回一个 writer callable + + @staticmethod + def create_mock_request(path="/meta"): + builder = EnvironBuilder(path=path) + return builder.get_environ() + + @pytest.mark.parametrize("test_case", + [ + {"case_id": "1", + "description": "测试index.html", + "input": "/data/plugin/graph_ascend/index.html", + "excepted": "200 OK" + }, + {"case_id": "2", + "description": "测试index.js", + "input": "/data/plugin/graph_ascend/index.js", + "excepted": "200 OK" + }, + {"case_id": "3", + "description": "测试404文件", + "input": "/data/plugin/graph_ascend/index.css", + "excepted": "404 NOT FOUND" + }, + + ], ids=lambda c: f"{c['case_id']}:{c['description']}") + def test_static_file_route(self, test_case): + request = TestGraphViews.create_mock_request(test_case['input']) + excepted = test_case['excepted'] + GraphView.static_file_route(request, TestGraphViews.start_response) + assert TestGraphViews.captured.status == excepted + + @pytest.mark.parametrize("test_case", + [ + {"case_id": "1", + "description": "test_load_meta_dir", + "excepted": {'data': {'st_test_cases': ['mock_compare_resnet_data']}, 'error': []} + } + ], + ids=lambda c: f"{c['case_id']}: {c['description']}") + def test_load_meta_dir(self, test_case): + logdir = Path(__file__).resolve().parent.parent.parent / 'data' / 'st_test_cases' + GraphState.set_global_value('logdir', str(logdir)) + # 构造请求 + request = TestGraphViews.create_mock_request("/data/plugin/graph_ascend/load_meta_dir") + response_iter = GraphView.load_meta_dir(request, TestGraphViews.start_response) + excepted = test_case['excepted'] + # 获取响应内容 + response_body = json.loads(b''.join(response_iter).decode('utf-8')) + assert response_body == excepted + assert TestGraphViews.captured.status == "200 OK" + assert TestGraphViews.captured.headers["Content-Type"] == "application/json" + + @pytest.mark.parametrize("test_case", [{"case_id": "2", "description": "test_load_graph_data"}], + ids=lambda c: f"{c['case_id']}:{c['description']}") + def test_load_graph_data(self, test_case): + request = TestGraphViews.create_mock_request( + f"/data/plugin/graph_ascend/load_graph_data?run=st_test_cases&tag={TestGraphViews.mock_vis_tag}") + response_iter = GraphView.load_graph_data(request, TestGraphViews.start_response) + response_body = b''.join(response_iter) + runs = GraphState.get_global_value('runs') + current_run = GraphState.get_global_value('current_run') + current_tag = GraphState.get_global_value('current_tag') + assert current_run == runs.get('st_test_cases') + assert current_tag == TestGraphViews.mock_vis_tag + assert TestGraphViews.captured.status == "200 OK" + assert TestGraphViews.captured.headers["Content-Type"] == "text/event-stream; charset=utf-8" + + @pytest.mark.parametrize("test_case", + TestCaseFactory.get_load_graph_config_info_cases(), ids=lambda c: f"{c['case_id']}:{c['description']}") + def test_load_graph_config_info(self, test_case): + request = TestGraphViews.create_mock_request( + f"/data/plugin/graph_ascend/load_graph_config_info?run=st_test_cases&tag={TestGraphViews.mock_vis_tag}") + response_iter = GraphView.load_graph_config_info(request, TestGraphViews.start_response) + response_body = b''.join(response_iter).decode('utf-8') + excepted = test_case['expected'] + assert response_body == json.dumps(excepted) + + @pytest.mark.parametrize("test_case", + TestCaseFactory.get_load_graph_all_node_list_cases(), ids=lambda c: f"{c['case_id']}:{c['description']}") + def test_load_graph_all_node_list(self, test_case): + request = TestGraphViews.create_mock_request( + f"/data/plugin/graph_ascend/load_graph_all_node_list?run=st_test_cases&tag={TestGraphViews.mock_vis_tag}") + response_iter = GraphView.load_graph_all_node_list(request, TestGraphViews.start_response) + response_body = b''.join(response_iter).decode('utf-8') + excepted = test_case['expected'] + assert response_body == json.dumps(excepted) + + @pytest.mark.parametrize("test_case", + TestCaseFactory.get_change_node_expand_state_cases(), ids=lambda c: f"{c['case_id']}:{c['description']}") + def test_change_node_expand_state(self, test_case): + excepted = test_case['expected'] + request = TestGraphViews.create_mock_request(test_case['input']) + response_iter = GraphView.change_node_expand_state(request, TestGraphViews.start_response) + response_body = b''.join(response_iter).decode('utf-8') + assert response_body == json.dumps(excepted) + + @pytest.mark.parametrize("test_case", + TestCaseFactory.get_test_add_match_nodes_cases(), ids=lambda c: f"{c['case_id']}:{c['description']}") + def test_add_match_nodes(self, test_case): + excepted = test_case['expected'] + request = TestGraphViews.create_mock_request(test_case['input']) + response_iter = GraphView.add_match_nodes(request, TestGraphViews.start_response) + response_body = b''.join(response_iter).decode('utf-8') + assert response_body == json.dumps(excepted) + + @pytest.mark.parametrize("test_case", + TestCaseFactory.get_test_update_hierarchy_data_cases(), ids=lambda c: f"{c['case_id']}:{c['description']}") + def test_update_hierarchy_data(self, test_case): + excepted = test_case['expected'] + request = TestGraphViews.create_mock_request(test_case['input']) + response_iter = GraphView.update_hierarchy_data(request, TestGraphViews.start_response) + response_body = b''.join(response_iter).decode('utf-8') + assert response_body == json.dumps(excepted) + + @pytest.mark.parametrize("test_case", [ + { + "case_id": "1", + "description": "测试save_matched_relations接口", + "expected": {"success": True, "data": "mock_compare_resnet_data.vis.config"} + } + ], ids=lambda c: f"{c['case_id']}:{c['description']}") + def test_save_matched_relations(self, test_case): + url = 'data/plugin/graph_ascend/saveMatchedRelations' + params = 'metaData={"run":"st_test_cases","tag":"mock_compare_resnet_data"}' + request_url = f"{url}?{params}" + request = TestGraphViews.create_mock_request(request_url) + response_iter = GraphView.save_matched_relations(request, TestGraphViews.start_response) + response_body = b''.join(response_iter).decode('utf-8') + excepted = test_case['expected'] + assert response_body == json.dumps(excepted) + + @pytest.mark.parametrize("test_case", + TestCaseFactory.get_test_add_match_nodes_by_config_cases(), ids=lambda c: f"{c['case_id']}:{c['description']}") + def test_add_match_nodes_by_config(self, test_case): + excepted = test_case['expected'] + request = TestGraphViews.create_mock_request(test_case['input']) + response_iter = GraphView.add_match_nodes_by_config(request, TestGraphViews.start_response) + response_body = b''.join(response_iter).decode('utf-8') + assert response_body == json.dumps(excepted) + + @pytest.mark.parametrize("test_case", + TestCaseFactory.get_test_delete_match_nodes_cases(), ids=lambda c: f"{c['case_id']}:{c['description']}") + def test_delete_match_nodes(self, test_case): + excepted = test_case['expected'] + request = TestGraphViews.create_mock_request(test_case['input']) + response_iter = GraphView.delete_match_nodes(request, TestGraphViews.start_response) + response_body = b''.join(response_iter).decode('utf-8') + assert response_body == json.dumps(excepted) + + @pytest.mark.parametrize("test_case", + TestCaseFactory.get_test_update_colors_cases(), ids=lambda c: f"{c['case_id']}:{c['description']}") + def test_update_colors(self, test_case): + excepted = test_case['expected'] + request = TestGraphViews.create_mock_request(test_case['input']) + response_iter = GraphView.update_colors(request, TestGraphViews.start_response) + response_body = b''.join(response_iter).decode('utf-8') + assert response_body == json.dumps(excepted) + + @pytest.mark.parametrize("test_case", + TestCaseFactory.get_test_get_node_info_cases(), ids=lambda c: f"{c['case_id']}:{c['description']}") + def test_get_node_info(self, test_case): + excepted = test_case['expected'] + request = TestGraphViews.create_mock_request(test_case['input']) + response_iter = GraphView.get_node_info(request, TestGraphViews.start_response) + response_body = b''.join(response_iter).decode('utf-8') + assert response_body == json.dumps(excepted) + + @pytest.mark.parametrize("test_case", [ + { + "case_id": "1", + "description": "测试save_data接口", + "expected": {"success": True} + } + ], ids=lambda c: f"{c['case_id']}:{c['description']}") + def test_save_data(self, test_case): + excepted = test_case['expected'] + url = 'data/plugin/graph_ascend/saveData' + params = 'metaData={"run":"st_test_cases","tag":"mock_compare_resnet_data"}' + request_url = f"{url}?{params}" + request = TestGraphViews.create_mock_request(request_url) + response_iter = GraphView.save_data(request, TestGraphViews.start_response) + response_body = b''.join(response_iter).decode('utf-8') + assert response_body == json.dumps(excepted) + diff --git a/plugins/tensorboard-plugins/tb_graph_ascend/test/pytest.ini b/plugins/tensorboard-plugins/tb_graph_ascend/test/pytest.ini deleted file mode 100644 index 532b3cd39b22a715e5c0ecfbf4ca0ac1daaea262..0000000000000000000000000000000000000000 --- a/plugins/tensorboard-plugins/tb_graph_ascend/test/pytest.ini +++ /dev/null @@ -1,12 +0,0 @@ -[pytest] -testpaths = - tests/unit - tests/functional - -markers = - unit: unit tests - functional: functional tests - graph: graph module tests - slow: mark test as slow to run - smoke: smoke tests - large_dataset: tests requiring large datasets \ No newline at end of file diff --git a/plugins/tensorboard-plugins/tb_graph_ascend/test/unit/controllers/test_layout_hierarchy_controller.py b/plugins/tensorboard-plugins/tb_graph_ascend/test/unit/controllers/test_layout_hierarchy_controller.py index ee2432f470b406bca849a0d9362b8e396a7e21b2..3c2ad18e7c5f1ed1f9635d8168706c22ee20fe34 100644 --- a/plugins/tensorboard-plugins/tb_graph_ascend/test/unit/controllers/test_layout_hierarchy_controller.py +++ b/plugins/tensorboard-plugins/tb_graph_ascend/test/unit/controllers/test_layout_hierarchy_controller.py @@ -13,3 +13,34 @@ # See the License for the specific language governing permissions and # limitations under the License. # ============================================================================== + +import pytest +from data.test_case_factory import TestCaseFactory +from server.app.utils.global_state import SINGLE +from server.app.controllers.layout_hierarchy_controller import LayoutHierarchyController + + +@pytest.mark.unit +class TestLayoutHierarchyController: + + @pytest.mark.parametrize("test_case", + TestCaseFactory.get_change_expand_state_cases(), ids=lambda c: f"{c['case_id']}:{c['description']}") + def test_change_expand_state(self, test_case): + graph_type = test_case['input']['graph_type'] + if graph_type == SINGLE: + test_case['input']['graph'] = TestCaseFactory.load_single_graph_test_data() + else: + test_case['input']['graph'] = TestCaseFactory.load_compare_graph_test_data().get(graph_type, {}) + node_name, graph_type, graph, micro_step = test_case['input'].values() + excepted = test_case['expected'] + actual = LayoutHierarchyController.change_expand_state(node_name, graph_type, graph, micro_step) + assert actual == excepted + + @pytest.mark.parametrize("test_case", + TestCaseFactory.get_update_hierarchy_data_cases(), ids=lambda c: f"{c['case_id']}:{c['description']}") + def test_update_hierarchy_data(self, test_case): + graph_type = test_case['input']['graph_type'] + excepted = test_case['expected'] + actual = LayoutHierarchyController.update_hierarchy_data(graph_type) + assert actual == excepted + diff --git a/plugins/tensorboard-plugins/tb_graph_ascend/test/unit/controllers/test_match_nodes_controller.py b/plugins/tensorboard-plugins/tb_graph_ascend/test/unit/controllers/test_match_nodes_controller.py index 0f2d04142abdded0ed66fbf2521d7c06dde2b7f2..a677b968b26992c5fd17812bacf40d1537397a82 100644 --- a/plugins/tensorboard-plugins/tb_graph_ascend/test/unit/controllers/test_match_nodes_controller.py +++ b/plugins/tensorboard-plugins/tb_graph_ascend/test/unit/controllers/test_match_nodes_controller.py @@ -17,6 +17,7 @@ import pytest from server.app.controllers.match_nodes_controller import MatchNodesController +from server.app.utils.global_state import GraphState from data.test_case_factory import TestCaseFactory @@ -27,7 +28,7 @@ class TestMatchNodesController: @pytest.mark.parametrize("test_case", TestCaseFactory.get_process_task_add_cases(), ids=lambda c: f"{c['case_id']}:{c['description']}") def test_process_task_add(self, test_case): - """测试添加子节点层功能""" + """测试添加节点功能""" graph_data, npu_node_name, bench_node_name, task = test_case['input'].values() expected = test_case['expected'] actual = MatchNodesController.process_task_add(graph_data, npu_node_name, bench_node_name, task) @@ -36,9 +37,40 @@ class TestMatchNodesController: @pytest.mark.parametrize("test_case", TestCaseFactory.get_process_task_delete_cases(), ids=lambda c: f"{c['case_id']}:{c['description']}") def test_process_task_delete(self, test_case): - """测试删除子节点层功能""" + """测试删除节点功能""" + if(test_case.get('config', None)): + GraphState.set_global_value("config_data", test_case['config']) graph_data, npu_node_name, bench_node_name, task = test_case['input'].values() expected = test_case['expected'] actual = MatchNodesController.process_task_delete(graph_data, npu_node_name, bench_node_name, task) assert actual == expected + + @pytest.mark.parametrize("test_case", TestCaseFactory.get_process_task_add_child_layer_cases(), + ids=lambda c: f"{c['case_id']}:{c['description']}") + def test_process_task_add_child_layer(self, test_case): + """测试添加子节点层功能""" + graph_data, npu_node_name, bench_node_name, task = test_case['input'].values() + excepted = test_case['expected'] + actual = MatchNodesController.process_task_add_child_layer(graph_data, npu_node_name, bench_node_name, task) + assert actual == excepted + + @pytest.mark.parametrize("test_case", TestCaseFactory.get_process_task_delete_child_layer_cases(), + ids=lambda c: f"{c['case_id']}:{c['description']}") + def test_process_task_delete_child_layer(self, test_case): + """测试删除子节点层功能""" + if(test_case.get('config', None)): + GraphState.set_global_value("config_data", test_case['config']) + graph_data, npu_node_name, bench_node_name, task = test_case['input'].values() + excepted = test_case['expected'] + actual = MatchNodesController.process_task_delete_child_layer(graph_data, npu_node_name, bench_node_name, task) + assert actual == excepted + + @pytest.mark.parametrize("test_case", TestCaseFactory.get_process_task_add_child_layer_by_config_cases(), + ids=lambda c: f"{c['case_id']}:{c['description']}") + def test_process_task_add_child_layer_by_config(self, test_case): + """测试根据配置文件添加子节点层功能""" + graph_data, match_node_links, task = test_case['input'].values() + excepted = test_case['expected'] + actual = MatchNodesController.process_task_add_child_layer_by_config(graph_data, match_node_links, task) + assert actual == excepted diff --git a/plugins/tensorboard-plugins/tb_graph_ascend/test/unit/utils/test_graph_utils.py b/plugins/tensorboard-plugins/tb_graph_ascend/test/unit/utils/test_graph_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..38352365cad0d229fb4d36820c18a5eae5245073 --- /dev/null +++ b/plugins/tensorboard-plugins/tb_graph_ascend/test/unit/utils/test_graph_utils.py @@ -0,0 +1,222 @@ +# Copyright (c) 2025, Huawei Technologies. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +import pytest +from server.app.utils.graph_utils import GraphUtils + + +class TestGraphUtils: + + @pytest.mark.parametrize("test_case", + [ + { + "case_id": "1", + "description": "正常的多层节点 A -> B -> C", + "input": { + "graph_data": { + "node": { + "C": {"upnode": "B"}, + "B": {"upnode": "A"}, + "A": {"upnode": None} + } + }, + "node_name": "C" + }, + "expected": ["A", "B", "C"] + }, + { + "case_id": "2", + "description": "单一节点无上级", + "input": { + "graph_data": { + "node": { + "A": {"upnode": None} + } + }, + "node_name": "A" + }, + "expected": ["A"] + }, + { + "case_id": "3", + "description": "节点不存在于图中", + "input": { + "graph_data": { + "node": { + "A": {"upnode": None} + } + }, + "node_name": "B" + }, + "expected": ["B"] + }, + { + "case_id": "4", + "description": "图为空", + "input": { + "graph_data": {}, + "node_name": "A" + }, + "expected": [] + }, + { + "case_id": "5", + "description": "节点名为空", + "input": { + "graph_data": { + "node": { + "A": {"upnode": None} + } + }, + "node_name": "" + }, + "expected": [] + } + ], + ids=lambda c: f"{c['case_id']}:{c['description']}") + def test_get_parent_node_list(self, test_case): + graph_data, node_name = test_case['input'].values() + expected = test_case['expected'] + actual = GraphUtils.get_parent_node_list(graph_data, node_name) + assert actual == expected + + @pytest.mark.parametrize("test_case", + [ + { + "case_id": "1", + "description": "数字大小比较,10 大于 2", + "input": {"a": "file_10", "b": "file_2"}, + "expected": 1 + }, + { + "case_id": "2", + "description": "相同前缀,数字部分较小", + "input": {"a": "item_3_part", "b": "item_12_part"}, + "expected": "-1" + }, + { + "case_id": "3", + "description": "路径比较,a/b/c 小于 a/b/d", + "input": {"a": "a/b/c", "b": "a/b/d"}, + "expected": "-1" + }, + { + "case_id": "4", + "description": "混合路径和下划线分隔,等价内容", + "input": {"a": "a_b_1", "b": "a/b/1"}, + "expected": 0 + }, + { + "case_id": "5", + "description": "子路径多一级,a/b 小于 a/b/c", + "input": {"a": "a/b", "b": "a/b/c"}, + "expected": "-1" + }, + { + "case_id": "6", + "description": "数字 vs 字母,数字优先", + "input": {"a": "file_1", "b": "file_a"}, + "expected": "-1" + }, + { + "case_id": "7", + "description": "完全相同", + "input": {"a": "dir/subdir_10/file_5", "b": "dir/subdir_10/file_5"}, + "expected": 0 + }, + { + "case_id": "8", + "description": "字母 vs 数字,字母在后", + "input": {"a": "a2b", "b": "a10"}, + "expected": "-1" + } + ], + ids=lambda c: f"{c['case_id']}:{c['description']}") + def test_compare_tag_names(self, test_case): + + def normalize(val: int) -> int: + return 0 if val == 0 else (1 if val > 0 else -1) + + a, b = test_case['input'].values() + expected = int(test_case['expected']) + actual = GraphUtils.compare_tag_names(a, b) + assert normalize(actual) == expected + + @pytest.mark.parametrize("test_case", + [ + { + "case_id": "1", + "description": "输入为 0 字节", + "input": {"size_bytes": 0}, + "expected": "0 B" + }, + { + "case_id": "2", + "description": "输入为字节(小于 1KB)", + "input": {"size_bytes": 512}, + "expected": "512.00 B" + }, + { + "case_id": "3", + "description": "输入为 1KB", + "input": {"size_bytes": 1024}, + "expected": "1.00 KB" + }, + { + "case_id": "4", + "description": "输入为 1MB", + "input": {"size_bytes": 1024 * 1024}, + "expected": "1.00 MB" + }, + { + "case_id": "5", + "description": "输入为 1.5MB", + "input": {"size_bytes": 1.5 * 1024 * 1024}, + "expected": "1.50 MB" + }, + { + "case_id": "6", + "description": "输入为 1GB,保留 3 位小数", + "input": {"size_bytes": 1024 ** 3, "decimal_places": 3}, + "expected": "1.000 GB" + }, + { + "case_id": "7", + "description": "输入为 2TB", + "input": {"size_bytes": 2 * 1024 ** 4}, + "expected": "2.00 TB" + }, + { + "case_id": "8", + "description": "输入为浮点字节数", + "input": {"size_bytes": 12345678.9}, + "expected": "11.77 MB" + }, + { + "case_id": "9", + "description": "输入为 PB 范围", + "input": {"size_bytes": 1.2 * 1024 ** 5}, + "expected": "1.20 PB" + } + ], + ids=lambda c: f"{c['case_id']}:{c['description']}") + def test_bytes_to_human_readable(self, test_case): + size_bytes = test_case["input"]["size_bytes"] + decimal_places = test_case["input"].get("decimal_places", 2) + expected = test_case['expected'] + actual = GraphUtils.bytes_to_human_readable(size_bytes, decimal_places) + assert actual == expected +