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