1from __future__ import unicode_literals 2import pytest # noqa 3import os 4import os.path 5from pytest import raises 6from textx import (get_location, metamodel_from_str, 7 metamodel_for_language, 8 register_language, clear_language_registrations) 9import textx.scoping.providers as scoping_providers 10import textx.scoping as scoping 11import textx.exceptions 12 13 14grammarA = """ 15Model: a+=A; 16A:'A' name=ID; 17""" 18grammarB = """ 19reference A 20Model: b+=B; 21B:'B' name=ID '->' a=[A.A]; 22""" 23grammarBWithImport = """ 24reference A as a 25Model: imports+=Import b+=B; 26B:'B' name=ID '->' a=[a.A]; 27Import: 'import' importURI=STRING; 28""" 29 30 31def register_languages(): 32 33 clear_language_registrations() 34 35 global_repo = scoping.GlobalModelRepository() 36 global_repo_provider = scoping_providers.PlainNameGlobalRepo() 37 38 class A(object): 39 def __init__(self, **kwargs): 40 super(A, self).__init__() 41 for k, v in kwargs.items(): 42 self.__dict__[k] = v 43 44 def __setattr__(self, name, value): 45 raise Exception("test: this is not allowed.") 46 47 def get_A_mm(): 48 mm_A = metamodel_from_str(grammarA, global_repository=global_repo, 49 classes=[A]) 50 mm_A.register_scope_providers({"*.*": global_repo_provider}) 51 return mm_A 52 53 def get_B_mm(): 54 mm_B = metamodel_from_str(grammarB, global_repository=global_repo) 55 mm_B.register_scope_providers({"*.*": global_repo_provider}) 56 return mm_B 57 58 def get_BwithImport_mm(): 59 mm_B = metamodel_from_str(grammarBWithImport, 60 global_repository=global_repo) 61 62 # define a default scope provider supporting the importURI feature 63 mm_B.register_scope_providers( 64 {"*.*": scoping_providers.FQNImportURI()}) 65 return mm_B 66 67 register_language('A', 68 pattern="*.a", 69 metamodel=get_A_mm) 70 register_language('B', 71 pattern="*.b", 72 metamodel=get_B_mm) 73 74 register_language('BwithImport', 75 pattern="*.b", 76 metamodel=get_BwithImport_mm) 77 78 return global_repo_provider 79 80 81def test_multi_metamodel_references1(): 82 83 global_repo_provider = register_languages() 84 85 mm_A = metamodel_for_language('A') 86 mA = mm_A.model_from_str(''' 87 A a1 A a2 A a3 88 ''') 89 global_repo_provider.add_model(mA) 90 91 mm_B = metamodel_for_language('B') 92 mm_B.model_from_str(''' 93 B b1 -> a1 B b2 -> a2 B b3 -> a3 94 ''') 95 96 with raises(textx.exceptions.TextXSemanticError, 97 match=r'.*UNKNOWN.*'): 98 mm_B.model_from_str(''' 99 B b1 -> a1 B b2 -> a2 B b3 -> UNKNOWN 100 ''') 101 102 103def test_multi_metamodel_references2(): 104 mm_A = metamodel_from_str(grammarA) 105 mm_B = metamodel_from_str(grammarB) 106 107 global_repo_provider = scoping_providers.PlainNameGlobalRepo() 108 mm_B.register_scope_providers({"*.*": global_repo_provider}) 109 110 mA = mm_A.model_from_str(''' 111 A a1 A a2 A a3 112 ''') 113 global_repo_provider.add_model(mA) 114 115 mm_B.model_from_str(''' 116 B b1 -> a1 B b2 -> a2 B b3 -> a3 117 ''') 118 119 with raises(textx.exceptions.TextXSemanticError, 120 match=r'.*UNKNOWN.*'): 121 mm_B.model_from_str(''' 122 B b1 -> a1 B b2 -> a2 B b3 -> UNKNOWN 123 ''') 124 125 126def test_multi_metamodel_references_with_importURI(): 127 # Use a global repo. 128 # This is useful, especially with circular includes or diamond shaped 129 # includes. Without such a repo, you might get double instantiations of 130 # model elements. 131 # However, if B includes A, but A not B, both meta models might have 132 # global repos on their own (global between model files of the same 133 # meta model --> global_repository=True). Circular dependencies 134 # will require shared grammars, like in test_metamodel_provider3.py, 135 # because it is not possible to share meta models for referencing, before 136 # the meta model is constructed (like in our example, mm_A cannot 137 # reference mm_B, if mm_B already references mm_A because one has to 138 # constructed first). 139 # Add a custom setattr for a rule used in the language with is imported 140 # via the importURI feature. This should test that the attr 141 # replacement also works for models not representing the "main outer 142 # model" of a load_from_xxx-call. 143 144 register_languages() 145 146 # Create two meta models with the global repo. 147 # The second meta model allows referencing the first one. 148 mm_A = metamodel_for_language('A') 149 mm_B = metamodel_for_language('BwithImport') 150 151 modelA = mm_A.model_from_str(''' 152 A a1 A a2 A a3 153 ''') 154 155 with raises(Exception, 156 match=r'.*test: this is not allowed.*'): 157 modelA.a[0].x = 1 158 159 # load a model from B which includes a model from A. 160 current_dir = os.path.dirname(__file__) 161 modelB = mm_B.model_from_file( 162 os.path.join(current_dir, 'multi_metamodel', 'refs', 'b.b')) 163 164 # check that the classes from the correct meta model are used 165 # (and that the model was loaded). 166 assert modelB.b[0].__class__ == mm_B[modelB.b[0].__class__.__name__] 167 assert modelB.b[0].a.__class__ == mm_A[modelB.b[0].a.__class__.__name__] 168 169 with raises(Exception, 170 match=r'.*test: this is not allowed.*'): 171 modelB.b[0].a.x = 1 172 173 174# ------------------------------------- 175 176 177class LibTypes: 178 """ Library for Typedefs: 179 type int 180 type string 181 """ 182 183 @staticmethod 184 def get_metamodel(): 185 return metamodel_for_language('types') 186 187 @staticmethod 188 def library_init(repo_selector): 189 if repo_selector == "no global scope": 190 global_repo = False 191 elif repo_selector == "global repo": 192 global_repo = True 193 else: 194 raise Exception("unexpected parameter 'repo_selector={}'" 195 .format(repo_selector)) 196 197 def get_metamodel(): 198 mm = metamodel_from_str( 199 r''' 200 Model: types+=Type; 201 Type: 'type' name=ID; 202 Comment: /\/\/.*$/; 203 ''', 204 global_repository=global_repo) 205 206 def check_type(t): 207 if t.name[0].isupper(): 208 raise textx.exceptions.TextXSyntaxError( 209 "types must be lowercase", 210 **get_location(t) 211 ) 212 mm.register_obj_processors({ 213 'Type': check_type 214 }) 215 216 return mm 217 218 register_language('types', pattern='*.type', 219 metamodel=get_metamodel) 220 221 222class LibData: 223 """ Library for Datadefs: 224 data Point { x: int y: int} 225 data City { name: string } 226 data Population { count: int} 227 """ 228 229 @staticmethod 230 def get_metamodel(): 231 return metamodel_for_language('data') 232 233 @staticmethod 234 def library_init(repo_selector): 235 if repo_selector == "no global scope": 236 global_repo = False 237 elif repo_selector == "global repo": 238 # get the global repo from the inherited meta model: 239 global_repo = LibTypes.get_metamodel()._tx_model_repository 240 else: 241 raise Exception("unexpected parameter 'repo_selector={}'" 242 .format(repo_selector)) 243 244 def get_metamodel(): 245 mm = metamodel_from_str( 246 r''' 247 reference types as t 248 Model: includes*=Include data+=Data; 249 Data: 'data' name=ID '{' 250 attributes+=Attribute 251 '}'; 252 Attribute: name=ID ':' type=[t.Type]; 253 Include: '#include' importURI=STRING; 254 Comment: /\/\/.*$/; 255 ''', 256 global_repository=global_repo) 257 258 mm.register_scope_providers( 259 {"*.*": scoping_providers.FQNImportURI()}) 260 261 return mm 262 263 register_language('data', 264 pattern='*.data', 265 metamodel=get_metamodel) 266 267 268class LibFlow: 269 """ Library for DataFlows 270 algo A1 : Point -> City 271 algo A2 : City -> Population 272 connect A1 -> A2 273 """ 274 275 @staticmethod 276 def get_metamodel(): 277 return metamodel_for_language('flow') 278 279 @staticmethod 280 def library_init(repo_selector): 281 if repo_selector == "no global scope": 282 global_repo = False 283 elif repo_selector == "global repo": 284 # get the global repo from the inherited meta model: 285 global_repo = LibData.get_metamodel()._tx_model_repository 286 else: 287 raise Exception("unexpected parameter 'repo_selector={}'" 288 .format(repo_selector)) 289 290 def get_metamodel(): 291 292 mm = metamodel_from_str( 293 r''' 294 reference data as d 295 Model: includes*=Include algos+=Algo flows+=Flow; 296 Algo: 'algo' name=ID ':' inp=[d.Data] '->' outp=[d.Data]; 297 Flow: 'connect' algo1=[Algo] '->' algo2=[Algo] ; 298 Include: '#include' importURI=STRING; 299 Comment: /\/\/.*$/; 300 ''', 301 global_repository=global_repo) 302 303 mm.register_scope_providers( 304 {"*.*": scoping_providers.FQNImportURI()}) 305 306 def check_flow(f): 307 if f.algo1.outp != f.algo2.inp: 308 raise textx.exceptions.TextXSemanticError( 309 "algo data types must match", 310 **get_location(f) 311 ) 312 mm.register_obj_processors({ 313 'Flow': check_flow 314 }) 315 316 return mm 317 318 register_language('flow', 319 pattern='*.flow', 320 metamodel=get_metamodel) 321 322 323def test_multi_metamodel_types_data_flow1(): 324 325 # this stuff normally happens in the python module directly of the 326 # third party lib 327 selector = "no global scope" 328 clear_language_registrations() 329 LibTypes.library_init(selector) 330 LibData.library_init(selector) 331 LibFlow.library_init(selector) 332 333 current_dir = os.path.dirname(__file__) 334 model1 = LibFlow.get_metamodel().model_from_file( 335 os.path.join(current_dir, 'multi_metamodel', 'types_data_flow', 336 'data_flow.flow') 337 ) 338 339 # althought, types.type is included 2x, it is only present 1x 340 # (scope providers share a common repo within on model and all 341 # loaded models in that model) 342 assert 3 == len(model1._tx_model_repository.all_models) 343 344 # load the type model also used by model1 345 model2 = LibData.get_metamodel().model_from_file( 346 os.path.join(current_dir, 'multi_metamodel', 'types_data_flow', 347 'data_structures.data') 348 ) 349 350 # load the type model also used by model1 and model2 351 model3 = LibTypes.get_metamodel().model_from_file( 352 os.path.join(current_dir, 'multi_metamodel', 'types_data_flow', 353 'types.type') 354 ) 355 356 # the types (reloaded by the second model) 357 # are not shared with the first model 358 # --> no global repo 359 assert model1.algos[0].inp.attributes[0].type \ 360 not in model2.includes[0]._tx_loaded_models[0].types 361 assert model1.algos[0].inp.attributes[0].type not in model3.types 362 363 364def test_multi_metamodel_types_data_flow2(): 365 366 # this stuff normally happens in the python module directly of the 367 # third party lib 368 selector = "global repo" 369 clear_language_registrations() 370 LibTypes.library_init(selector) 371 LibData.library_init(selector) 372 LibFlow.library_init(selector) 373 374 current_dir = os.path.dirname(__file__) 375 model1 = LibFlow.get_metamodel().model_from_file( 376 os.path.join(current_dir, 'multi_metamodel', 'types_data_flow', 377 'data_flow.flow') 378 ) 379 # althought, types.type is included 2x, it is only present 1x 380 assert 3 == len(model1._tx_model_repository.all_models) 381 382 # load the type model also used by model1 383 model2 = LibData.get_metamodel().model_from_file( 384 os.path.join(current_dir, 'multi_metamodel', 'types_data_flow', 385 'data_structures.data') 386 ) 387 388 # load the type model also used by model1 and model2 389 model3 = LibTypes.get_metamodel().model_from_file( 390 os.path.join(current_dir, 'multi_metamodel', 'types_data_flow', 391 'types.type') 392 ) 393 394 # the types (reloaded by the second model) 395 # are shared with the first model 396 # --> global repo 397 assert model1.algos[0].inp.attributes[0].type \ 398 in model2.includes[0]._tx_loaded_models[0].types 399 assert model1.algos[0].inp.attributes[0].type in model3.types 400 401 402def test_multi_metamodel_types_data_flow_validation_error_in_types(): 403 404 selector = "no global scope" 405 clear_language_registrations() 406 LibTypes.library_init(selector) 407 LibData.library_init(selector) 408 LibFlow.library_init(selector) 409 410 current_dir = os.path.dirname(__file__) 411 412 with raises(textx.exceptions.TextXSyntaxError, 413 match=r'.*lowercase.*'): 414 LibFlow.get_metamodel().model_from_file( 415 os.path.join(current_dir, 'multi_metamodel', 'types_data_flow', 416 'data_flow_including_error.flow') 417 ) 418 419 420def test_multi_metamodel_types_data_flow_validation_error_in_data_flow(): 421 422 selector = "no global scope" 423 clear_language_registrations() 424 LibTypes.library_init(selector) 425 LibData.library_init(selector) 426 LibFlow.library_init(selector) 427 428 current_dir = os.path.dirname(__file__) 429 430 with raises(textx.exceptions.TextXSemanticError, 431 match=r'.*data types must match.*'): 432 LibFlow.get_metamodel().model_from_file( 433 os.path.join(current_dir, 'multi_metamodel', 'types_data_flow', 434 'data_flow_with_error.flow') 435 ) 436