|
| 1 | +#!/usr/bin/env python3 |
| 2 | +""" |
| 3 | +TinyEngine DSL Validator 回归测试 |
| 4 | +
|
| 5 | +锁定 validate_dsl.py 对"外层包装 + 内层 DSL"结构(page_content / content) |
| 6 | +的识别与递归校验行为,防止文档推荐的一键校验命令对真实页面/区块文件误报失败。 |
| 7 | +
|
| 8 | +运行: |
| 9 | + python3 test_validate_dsl.py |
| 10 | +""" |
| 11 | + |
| 12 | +import os |
| 13 | +import sys |
| 14 | +import unittest |
| 15 | + |
| 16 | +# 让本脚本能直接 import 同目录下的 validate_dsl 模块 |
| 17 | +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) |
| 18 | +from validate_dsl import TinyEngineValidator # noqa: E402 |
| 19 | + |
| 20 | +REPO_ROOT = os.path.abspath( |
| 21 | + os.path.join(os.path.dirname(__file__), '..', '..', '..', '..') |
| 22 | +) |
| 23 | +PAGES_DIR = os.path.join(REPO_ROOT, 'mockServer', 'data', 'pages') |
| 24 | +BLOCKS_DIR = os.path.join(REPO_ROOT, 'mockServer', 'data', 'blocks') |
| 25 | + |
| 26 | + |
| 27 | +def _page_wrapper(): |
| 28 | + """外层包装页面(page_content 内才是真正的页面 DSL)""" |
| 29 | + return { |
| 30 | + "name": "Login", |
| 31 | + "id": "5a3f8e1b9c4d2e7f", |
| 32 | + "app": 1, |
| 33 | + "route": "login", |
| 34 | + "page_content": { |
| 35 | + "componentName": "Page", |
| 36 | + "fileName": "Login", |
| 37 | + "props": {"className": "login-page"}, |
| 38 | + "css": ".login-page { padding: 40px; }", |
| 39 | + "state": {}, |
| 40 | + "methods": {}, |
| 41 | + "lifeCycles": {}, |
| 42 | + "children": [], |
| 43 | + "dataSource": {"list": []}, |
| 44 | + }, |
| 45 | + "isPage": True, |
| 46 | + "group": "staticPages", |
| 47 | + "occupier": None, |
| 48 | + "isHome": False, |
| 49 | + } |
| 50 | + |
| 51 | + |
| 52 | +def _block_wrapper(): |
| 53 | + """外层包装区块(content 内才是真正的区块 DSL)""" |
| 54 | + return { |
| 55 | + "id": "V85zd9sWEya25Kxh", |
| 56 | + "label": "PortalBlock", |
| 57 | + "framework": "Vue", |
| 58 | + "path": "portal", |
| 59 | + "content": { |
| 60 | + "componentName": "Block", |
| 61 | + "fileName": "PortalBlock", |
| 62 | + "props": {}, |
| 63 | + "state": {}, |
| 64 | + "methods": {}, |
| 65 | + "children": [], |
| 66 | + "schema": {"properties": [], "events": {}, "slots": {}}, |
| 67 | + "dataSource": {}, |
| 68 | + }, |
| 69 | + "occupier": None, |
| 70 | + "is_published": True, |
| 71 | + } |
| 72 | + |
| 73 | + |
| 74 | +def _raw_page_with_meta(): |
| 75 | + """原始(裸)页面 DSL,带 meta —— 协议要求的完整形态""" |
| 76 | + return { |
| 77 | + "componentName": "Page", |
| 78 | + "fileName": "HomePage", |
| 79 | + "meta": { |
| 80 | + "id": 1, |
| 81 | + "title": "首页", |
| 82 | + "router": "home", |
| 83 | + "creator": "admin", |
| 84 | + "isHome": True, |
| 85 | + "parentId": "0", |
| 86 | + "rootElement": "div", |
| 87 | + }, |
| 88 | + "children": [], |
| 89 | + } |
| 90 | + |
| 91 | + |
| 92 | +class WrapperDetectionTests(unittest.TestCase): |
| 93 | + """外层包装结构的识别与解包""" |
| 94 | + |
| 95 | + def test_page_wrapper_detected_as_page_and_valid(self): |
| 96 | + v = TinyEngineValidator(_page_wrapper()) |
| 97 | + self.assertEqual(v.schema_type, 'page') |
| 98 | + self.assertTrue(v.validate(), msg=str(v.errors)) |
| 99 | + self.assertEqual(v.errors, []) |
| 100 | + |
| 101 | + def test_block_wrapper_detected_as_block_and_valid(self): |
| 102 | + v = TinyEngineValidator(_block_wrapper()) |
| 103 | + self.assertEqual(v.schema_type, 'block') |
| 104 | + self.assertTrue(v.validate(), msg=str(v.errors)) |
| 105 | + self.assertEqual(v.errors, []) |
| 106 | + |
| 107 | + def test_wrapper_does_not_require_meta(self): |
| 108 | + """包装格式把 meta 上提到外层,内层 page_content 不应再强制要求 meta""" |
| 109 | + v = TinyEngineValidator(_page_wrapper()) |
| 110 | + v.validate() |
| 111 | + self.assertFalse(any('meta' in e.lower() for e in v.errors), |
| 112 | + f"不应因缺少 meta 报错: {v.errors}") |
| 113 | + |
| 114 | + def test_unwrap_tracks_wrapper_flag(self): |
| 115 | + wrapped, flag = TinyEngineValidator._unwrap(_page_wrapper()) |
| 116 | + self.assertTrue(flag) |
| 117 | + self.assertEqual(wrapped['componentName'], 'Page') |
| 118 | + |
| 119 | + def test_non_wrapper_returns_unchanged(self): |
| 120 | + raw = _raw_page_with_meta() |
| 121 | + inner, flag = TinyEngineValidator._unwrap(raw) |
| 122 | + self.assertFalse(flag) |
| 123 | + self.assertIs(inner, raw) |
| 124 | + |
| 125 | + |
| 126 | +class RawSchemaBackwardCompatTests(unittest.TestCase): |
| 127 | + """原始(裸)DSL 的既有严格行为不应被破坏""" |
| 128 | + |
| 129 | + def test_raw_page_with_meta_still_passes(self): |
| 130 | + v = TinyEngineValidator(_raw_page_with_meta()) |
| 131 | + self.assertEqual(v.schema_type, 'page') |
| 132 | + self.assertTrue(v.validate(), msg=str(v.errors)) |
| 133 | + |
| 134 | + def test_raw_page_without_meta_still_errors(self): |
| 135 | + """裸页面仍必须带 meta(只有包装格式才豁免)""" |
| 136 | + raw = {"componentName": "Page", "fileName": "HomePage", "children": []} |
| 137 | + v = TinyEngineValidator(raw) |
| 138 | + v.validate() |
| 139 | + self.assertTrue(any('meta' in e.lower() for e in v.errors), |
| 140 | + "裸页面缺少 meta 应报错") |
| 141 | + |
| 142 | + def test_unknown_schema_still_fails(self): |
| 143 | + v = TinyEngineValidator({"foo": "bar"}) |
| 144 | + self.assertEqual(v.schema_type, 'unknown') |
| 145 | + self.assertFalse(v.validate()) |
| 146 | + |
| 147 | + |
| 148 | +class StringChildrenTests(unittest.TestCase): |
| 149 | + """children 为字符串(文本子节点)是协议合法形态,不应误报""" |
| 150 | + |
| 151 | + def test_string_children_not_flagged(self): |
| 152 | + dsl = { |
| 153 | + "componentName": "Page", |
| 154 | + "fileName": "TextPage", |
| 155 | + "meta": { |
| 156 | + "id": 1, "title": "t", "router": "r", "creator": "a", |
| 157 | + "isHome": False, "parentId": "0", "rootElement": "div", |
| 158 | + }, |
| 159 | + "children": [ |
| 160 | + # a / h1 的 children 是字符串文本 —— 协议允许 |
| 161 | + {"componentName": "a", "id": "l1", "children": "链接文本"}, |
| 162 | + {"componentName": "h1", "id": "h1", "children": "标题文本"}, |
| 163 | + ], |
| 164 | + } |
| 165 | + v = TinyEngineValidator(dsl) |
| 166 | + self.assertTrue(v.validate(), msg=str(v.errors)) |
| 167 | + self.assertFalse( |
| 168 | + any('is not an object' in e for e in v.errors), |
| 169 | + f"字符串子节点不应被判为非对象: {v.errors}", |
| 170 | + ) |
| 171 | + |
| 172 | + |
| 173 | +@unittest.skipUnless( |
| 174 | + os.path.isdir(PAGES_DIR), |
| 175 | + f"找不到真实页面目录: {PAGES_DIR}", |
| 176 | +) |
| 177 | +class RealFileTests(unittest.TestCase): |
| 178 | + """对仓库内真实页面/区块文件跑端到端校验""" |
| 179 | + |
| 180 | + def _validate_file(self, path): |
| 181 | + import json |
| 182 | + with open(path, 'r', encoding='utf-8') as f: |
| 183 | + data = json.load(f) |
| 184 | + v = TinyEngineValidator(data) |
| 185 | + return v |
| 186 | + |
| 187 | + def test_real_login_page_passes(self): |
| 188 | + path = os.path.join(PAGES_DIR, 'Login.json') |
| 189 | + if not os.path.exists(path): |
| 190 | + self.skipTest("Login.json 不存在") |
| 191 | + v = self._validate_file(path) |
| 192 | + self.assertEqual(v.schema_type, 'page') |
| 193 | + self.assertTrue(v.validate(), msg=str(v.errors)) |
| 194 | + |
| 195 | + def test_real_block_passes(self): |
| 196 | + path = os.path.join(BLOCKS_DIR, 'PortalBlock.json') |
| 197 | + if not os.path.exists(path): |
| 198 | + self.skipTest("PortalBlock.json 不存在") |
| 199 | + v = self._validate_file(path) |
| 200 | + self.assertEqual(v.schema_type, 'block') |
| 201 | + self.assertTrue(v.validate(), msg=str(v.errors)) |
| 202 | + |
| 203 | + |
| 204 | +if __name__ == '__main__': |
| 205 | + unittest.main(verbosity=2) |
0 commit comments