• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/python3
2#
3# Copyright (c) 2018-2019 Collabora, Ltd.
4#
5# SPDX-License-Identifier: Apache-2.0
6#
7# Author(s):    Ryan Pavlik <ryan.pavlik@collabora.com>
8#
9# Purpose:      This file contains tests for check_spec_links.py
10
11import pytest
12
13from check_spec_links import MessageId, makeMacroChecker
14from spec_tools.console_printer import ConsolePrinter
15from spec_tools.macro_checker_file import shouldEntityBeText
16
17# API-specific constants
18PROTO = 'vkCreateInstance'
19STRUCT = 'VkInstanceCreateInfo'
20EXT = 'VK_KHR_display'
21
22
23class CheckerWrapper(object):
24    """Little wrapper object for a MacroChecker.
25
26    Intended for use in making test assertions shorter and easier to read."""
27
28    def __init__(self, capsys):
29        self.ckr = makeMacroChecker(set())
30        self.capsys = capsys
31
32    def enabled(self, enabled_messages):
33        """Updates the checker's enable message type set, from an iterable."""
34        self.ckr.enabled_messages = set(enabled_messages)
35        return self
36
37    def check(self, string):
38        """Checks a string (as if it were a file), outputs the results to the console, then returns the MacroCheckerFile."""
39
40        # Flush the captured output.
41        _ = self.capsys.readouterr()
42
43        # Process
44        f = self.ckr.processString(string + '\n')
45
46        # Dump messages
47        ConsolePrinter().output(f)
48        return f
49
50
51@pytest.fixture
52def ckr(capsys):
53    """Fixture - add an arg named ckr to your test function to automatically get one passed to you."""
54    return CheckerWrapper(capsys)
55
56
57def msgReplacement(file_checker, which=0):
58    """Return the replacement text associated with the specified message."""
59    assert(len(file_checker.messages) > which)
60    msg = file_checker.messages[which]
61    from pprint import pprint
62    pprint(msg.script_location)
63    pprint(msg.replacement)
64    pprint(msg.fix)
65    return msg.replacement
66
67
68def loneMsgReplacement(file_checker):
69    """Assert there's only one message in a file checker, and return the replacement text associated with it."""
70    assert(len(file_checker.messages) == 1)
71    return msgReplacement(file_checker)
72
73
74def message(file_checker, which=0):
75    """Return a string of the message lines associated with the message of a file checker."""
76    assert(len(file_checker.messages) > which)
77    return "\n".join(file_checker.messages[which].message)
78
79
80def allMessages(file_checker):
81    """Return a list of strings, each being the combination of the message lines of a message from a file checker."""
82    return ['\n'.join(msg.message) for msg in file_checker.messages]
83
84
85def test_missing_macro(ckr):
86    """Verify correct functioning of MessageId.MISSING_MACRO."""
87    ckr.enabled([MessageId.MISSING_MACRO])
88
89    # This should have a missing macro warning
90    assert(ckr.check('with %s by' % PROTO).numDiagnostics() == 1)
91
92    # These 3 should not have a missing macro warning because of their context
93    # (in a link)
94    assert(not ckr.check('<<%s' % PROTO).messages)
95    # These 2 are simulating links that broke over lines
96    assert(not ckr.check('%s>>' % PROTO).messages)
97    assert(not ckr.check(
98        '%s asdf>> table' % PROTO).messages)
99
100
101def test_entity_detection(ckr):
102    ckr.enabled([MessageId.BAD_ENTITY])
103    # Should complain about BAD_ENTITY
104    assert(ckr.check('flink:abcd').numDiagnostics() == 1)
105
106    # Should just give BAD_ENTITY (an error), not MISSING_TEXT (a warning).
107    # Verifying that wrapping in asterisks (for formatting) doesn't get picked up as
108    # an asterisk in the entity name (a placeholder).
109    ckr.enabled(
110        [MessageId.MISSING_TEXT, MessageId.BAD_ENTITY])
111    assert(ckr.check('*flink:abcd*').numErrors() == 1)
112
113
114def test_wrong_macro(ckr):
115    ckr.enabled([MessageId.WRONG_MACRO])
116    # Should error - this ought to be code:uint32_t
117    assert(ckr.check('basetype:uint32_t').numErrors() == 1)
118
119    # This shouldn't error
120    assert(ckr.check('code:uint32_t').numErrors() == 0)
121
122
123def test_should_entity_be_text():
124    # These 5 are all examples of patterns that would merit usage of a ptext/etext/etc
125    # macro, for various reasons:
126
127    # has variable in subscript
128    assert(shouldEntityBeText('pBuffers[i]', '[i]'))
129    assert(shouldEntityBeText('API_ENUM_[X]', '[X]'))
130
131    # has asterisk
132    assert(shouldEntityBeText('maxPerStage*', None))
133
134    # double-underscores make italicized placeholders
135    # (triple are double-underscores delimited by underscores...)
136    assert(shouldEntityBeText('API_ENUM[__x__]', '[__x__]'))
137    assert(shouldEntityBeText('API_ENUM___i___EXT', None))
138
139    # This shouldn't be a *text: macro because it only has single underscores
140    assert(False == shouldEntityBeText('API_ENUM_i_EXT', None))
141
142
143def test_misused_text(ckr):
144    # Tests the same patterns as test_should_entity_be_text(),
145    # but in a whole checker
146    ckr.enabled([MessageId.MISUSED_TEXT])
147
148    assert(ckr.check('etext:API_ENUM_').numDiagnostics() == 0)
149    assert(ckr.check('etext:API_ENUM_[X]').numDiagnostics() == 0)
150    assert(ckr.check('etext:API_ENUM[i]').numDiagnostics() == 0)
151    assert(ckr.check('etext:API_ENUM[__x__]').numDiagnostics() == 0)
152
153    # Should be OK, since __i__ is a placeholder here
154    assert(ckr.check('etext:API_ENUM___i___EXT').numDiagnostics() == 0)
155
156    # This shouldn't be a *text: macro because it only has single underscores
157    assert(ckr.check('API_ENUM_i_EXT').numDiagnostics() == 0)
158
159
160def test_extension(ckr):
161    ckr.enabled(set(MessageId))
162    # Check formatting of extension names:
163    # the following is the canonical way to refer to an extension
164    # (link wrapped in backticks)
165    expected_replacement = '`<<%s>>`' % EXT
166
167    # Extension name mentioned without any markup, should be added
168    assert(loneMsgReplacement(ckr.check('asdf %s asdf' % EXT))
169           == expected_replacement)
170
171    # Extension name mentioned without any markup and wrong case,
172    # should be added and have case fixed
173    assert(loneMsgReplacement(ckr.check('asdf %s asdf' % EXT.upper()))
174           == expected_replacement)
175
176    # Extension name using wrong/old macro: ename isn't for extensions.
177    assert(loneMsgReplacement(ckr.check('asdf ename:%s asdf' % EXT))
178           == expected_replacement)
179
180    # Extension name using wrong macro: elink isn't for extensions.
181    assert(loneMsgReplacement(ckr.check('asdf elink:%s asdf' % EXT))
182           == expected_replacement)
183
184    # Extension name using wrong macro and wrong case: should have markup and
185    # case fixed
186    assert(loneMsgReplacement(ckr.check('asdf elink:%s asdf' % EXT.upper()))
187           == expected_replacement)
188
189    # This shouldn't cause errors because this is how we want it to look.
190    assert(not ckr.check('asdf `<<%s>>` asdf' % EXT).messages)
191
192    # This doesn't (shouldn't?) cause errors because just backticks on their own
193    # "escape" names from the "missing markup" tests.
194    assert(not ckr.check('asdf `%s` asdf' % EXT).messages)
195
196    # TODO can we auto-correct this to add the backticks?
197    # Doesn't error now, but would be nice if it did...
198    assert(not ckr.check('asdf <<%s>> asdf' % EXT).messages)
199
200
201def test_refpage_tag(ckr):
202    ckr.enabled([MessageId.REFPAGE_TAG])
203
204    # Should error: missing refpage='' field
205    assert(ckr.check("[open,desc='',type='',xrefs='']").numErrors() == 1)
206    # Should error: missing desc='' field
207    assert(ckr.check("[open,refpage='',type='',xrefs='']").numErrors() == 1)
208    # Should error: missing type='' field
209    assert(ckr.check("[open,refpage='',desc='',xrefs='']").numErrors() == 1)
210
211    # Should not error: missing xrefs field is optional
212    assert(not ckr.check("[open,refpage='',desc='',type='']").messages)
213
214    # Should error, due to missing refpage, but not crash due to message printing (note the unicode smart quote)
215    assert(ckr.check("[open,desc='',type='',xrefs=’']").numDiagnostics() == 1)
216
217
218def test_refpage_name(ckr):
219    ckr.enabled([MessageId.REFPAGE_NAME])
220    # Should not error: actually exists.
221    assert(ckr.check(
222        "[open,refpage='%s',desc='',type='']" % PROTO).numDiagnostics() == 0)
223
224    # Should error: does not exist.
225    assert(
226        ckr.check("[open,refpage='bogus',desc='',type='']").numDiagnostics() == 1)
227
228
229def test_refpage_missing_desc(ckr):
230    ckr.enabled([MessageId.REFPAGE_MISSING_DESC])
231    # Should not warn: non-empty description actually exists.
232    assert(ckr.check(
233        "[open,refpage='',desc='non-empty description',type='']").numDiagnostics() == 0)
234
235    # Should warn: desc field is empty.
236    assert(
237        ckr.check("[open,refpage='',desc='',type='']").numDiagnostics() == 1)
238
239
240def test_refpage_type(ckr):
241    ckr.enabled([MessageId.REFPAGE_TYPE])
242    # Should not error: this is of type 'protos'.
243    assert(not ckr.check(
244        "[open,refpage='%s',desc='',type='protos']" % PROTO).messages)
245
246    # Should error: this is of type 'protos', not 'structs'.
247    assert(
248        ckr.check("[open,refpage='%s',desc='',type='structs']" % PROTO).messages)
249
250
251def test_refpage_xrefs(ckr):
252    ckr.enabled([MessageId.REFPAGE_XREFS])
253    # Should not error: this is a valid entity to have an xref to.
254    assert(not ckr.check(
255        "[open,refpage='',desc='',type='protos',xrefs='%s']" % STRUCT).messages)
256
257    # case difference:
258    # should error but offer a replacement.
259    assert(loneMsgReplacement(ckr.check("[open,refpage='',xrefs='%s']" % STRUCT.lower()))
260           == STRUCT)
261
262    # Should error: not a valid entity.
263    assert(ckr.check(
264        "[open,refpage='',desc='',type='protos',xrefs='bogus']").numDiagnostics() == 1)
265
266
267def test_refpage_xrefs_comma(ckr):
268    ckr.enabled([MessageId.REFPAGE_XREFS_COMMA])
269    # Should not error: no commas in the xrefs field
270    assert(not ckr.check(
271        "[open,refpage='',xrefs='abc']").messages)
272
273    # Should error: commas shouldn't be there since it's space-delimited.
274    assert(loneMsgReplacement(
275        ckr.check("[open,refpage='',xrefs='abc,']")) == 'abc')
276
277    # All should correct to the same thing.
278    equivalent_tags_with_commas = [
279        "[open,refpage='',xrefs='abc, 123']",
280        "[open,refpage='',xrefs='abc,123']",
281        "[open,refpage='',xrefs='abc , 123']"]
282    for has_comma in equivalent_tags_with_commas:
283        assert(loneMsgReplacement(ckr.check(has_comma)) == 'abc 123')
284
285
286def test_refpage_block(ckr):
287    """Tests of the REFPAGE_BLOCK message."""
288    ckr.enabled([MessageId.REFPAGE_BLOCK])
289    # Should not error: have the tag, an open, and a close
290    assert(not ckr.check(
291        """[open,]
292        --
293        bla
294        --""").messages)
295    assert(not ckr.check(
296        """[open,refpage='abc']
297        --
298        bla
299        --
300
301        [open,refpage='123']
302        --
303        bla2
304        --""").messages)
305
306    # Should have 1 error: file ends immediately after tag
307    assert(ckr.check(
308        "[open,]").numDiagnostics() == 1)
309
310    # Should have 1 error: line after tag isn't --
311    assert(ckr.check(
312        """[open,]
313        bla
314        --""").numDiagnostics() == 1)
315    # Checking precedence of checks: this should have 1 error because line after tag isn't --
316    # (but it is something that causes a line to be handled differently)
317    assert(ckr.check(
318        """[open,]
319        == Heading
320        --""").numDiagnostics() == 1)
321    assert(ckr.check(
322        """[open,]
323        ----
324        this is in a code block
325        ----
326        --""").numDiagnostics() == 1)
327
328    # Should have 1 error: tag inside refpage.
329    tag_inside = """[open,]
330        --
331        bla
332        [open,]
333        --"""
334    assert(ckr.check(tag_inside).numDiagnostics() == 1)
335    assert("already in a refpage block" in
336           message(ckr.check(tag_inside)))
337
338
339def test_refpage_missing(ckr):
340    """Test the REFPAGE_MISSING message."""
341    ckr.enabled([MessageId.REFPAGE_MISSING])
342    # Should not error: have the tag, an open, and the include
343    assert(not ckr.check(
344        """[open,refpage='%s']
345        --
346        include::{generated}/api/protos/%s.txt[]""" % (PROTO, PROTO)).messages)
347    assert(not ckr.check(
348        """[open,refpage='%s']
349        --
350        include::{generated}/validity/protos/%s.txt[]""" % (PROTO, PROTO)).messages)
351
352    # Should not error: manual anchors shouldn't trigger this.
353    assert(not ckr.check("[[%s]]" % PROTO).messages)
354
355    # Should have 1 error: file ends immediately after include
356    assert(ckr.check(
357        "include::{generated}/api/protos/%s.txt[]" % PROTO).numDiagnostics() == 1)
358    assert(ckr.check(
359        "include::{generated}/validity/protos/%s.txt[]" % PROTO).numDiagnostics() == 1)
360
361    # Should have 1 error: include is before the refpage open
362    assert(ckr.check(
363        """include::{generated}/api/protos/%s.txt[]
364        [open,refpage='%s']
365        --""" % (PROTO, PROTO)).numDiagnostics() == 1)
366    assert(ckr.check(
367        """include::{generated}/validity/protos/%s.txt[]
368        [open,refpage='%s']
369        --""" % (PROTO, PROTO)).numDiagnostics() == 1)
370
371
372def test_refpage_mismatch(ckr):
373    """Test the REFPAGE_MISMATCH message."""
374    ckr.enabled([MessageId.REFPAGE_MISMATCH])
375    # Should not error: have the tag, an open, and a matching include
376    assert(not ckr.check(
377        """[open,refpage='%s']
378        --
379        include::{generated}/api/protos/%s.txt[]""" % (PROTO, PROTO)).messages)
380    assert(not ckr.check(
381        """[open,refpage='%s']
382        --
383        include::{generated}/validity/protos/%s.txt[]""" % (PROTO, PROTO)).messages)
384
385    # Should error: have the tag, an open, and a mis-matching include
386    assert(ckr.check(
387        """[open,refpage='%s']
388        --
389        include::{generated}/api/structs/%s.txt[]""" % (PROTO, STRUCT)).numDiagnostics() == 1)
390    assert(ckr.check(
391        """[open,refpage='%s']
392        --
393        include::{generated}/validity/structs/%s.txt[]""" % (PROTO, STRUCT)).numDiagnostics() == 1)
394
395
396def test_refpage_unknown_attrib(ckr):
397    """Check the REFPAGE_UNKNOWN_ATTRIB message."""
398    ckr.enabled([MessageId.REFPAGE_UNKNOWN_ATTRIB])
399    # Should not error: these are known attribute names
400    assert(not ckr.check(
401        "[open,refpage='',desc='',type='',xrefs='']").messages)
402
403    # Should error: xref isn't an attribute name.
404    assert(ckr.check(
405        "[open,xref='']").numDiagnostics() == 1)
406
407
408def test_refpage_self_xref(ckr):
409    """Check the REFPAGE_SELF_XREF message."""
410    ckr.enabled([MessageId.REFPAGE_SELF_XREF])
411    # Should not error: not self-referencing
412    assert(not ckr.check(
413        "[open,refpage='abc',xrefs='']").messages)
414    assert(not ckr.check(
415        "[open,refpage='abc',xrefs='123']").messages)
416
417    # Should error: self-referencing isn't an attribute name.
418    assert(loneMsgReplacement(
419        ckr.check("[open,refpage='abc',xrefs='abc']")) == '')
420    assert(loneMsgReplacement(
421        ckr.check("[open,refpage='abc',xrefs='abc 123']")) == '123')
422    assert(loneMsgReplacement(
423        ckr.check("[open,refpage='abc',xrefs='123 abc']")) == '123')
424
425
426def test_refpage_xref_dupe(ckr):
427    """Check the REFPAGE_XREF_DUPE message."""
428    ckr.enabled([MessageId.REFPAGE_XREF_DUPE])
429    # Should not error: no dupes
430    assert(not ckr.check("[open,xrefs='']").messages)
431    assert(not ckr.check("[open,xrefs='123']").messages)
432    assert(not ckr.check("[open,xrefs='abc 123']").messages)
433
434    # Should error: one dupe.
435    assert(loneMsgReplacement(
436        ckr.check("[open,xrefs='abc abc']")) == 'abc')
437    assert(loneMsgReplacement(
438        ckr.check("[open,xrefs='abc   abc']")) == 'abc')
439    assert(loneMsgReplacement(
440        ckr.check("[open,xrefs='abc abc abc']")) == 'abc')
441    assert(loneMsgReplacement(
442        ckr.check("[open,xrefs='abc 123 abc']")) == 'abc 123')
443    assert(loneMsgReplacement(
444        ckr.check("[open,xrefs='123 abc abc']")) == '123 abc')
445
446
447def test_REFPAGE_WHITESPACE(ckr):
448    """Check the REFPAGE_WHITESPACE message."""
449    ckr.enabled([MessageId.REFPAGE_WHITESPACE])
450    # Should not error: no extra whitespace
451    assert(not ckr.check("[open,xrefs='']").messages)
452    assert(not ckr.check("[open,xrefs='123']").messages)
453    assert(not ckr.check("[open,xrefs='abc 123']").messages)
454
455    # Should error: some extraneous whitespace.
456    assert(loneMsgReplacement(
457        ckr.check("[open,xrefs='   \t   ']")) == '')
458    assert(loneMsgReplacement(
459        ckr.check("[open,xrefs='  abc   123  ']")) == 'abc 123')
460    assert(loneMsgReplacement(
461        ckr.check("[open,xrefs='  abc\t123    xyz  ']")) == 'abc 123 xyz')
462
463    # Should *NOT* remove self-reference, just extra whitespace
464    assert(loneMsgReplacement(
465        ckr.check("[open,refpage='abc',xrefs='  abc   123  ']")) == 'abc 123')
466
467    # Even if we turn on the self-reference warning
468    ckr.enabled([MessageId.REFPAGE_WHITESPACE, MessageId.REFPAGE_SELF_XREF])
469    assert(msgReplacement(
470        ckr.check("[open,refpage='abc',xrefs='  abc   123  ']"), 1) == 'abc 123')
471
472
473def test_REFPAGE_DUPLICATE(ckr):
474    """Check the REFPAGE_DUPLICATE message."""
475    ckr.enabled([MessageId.REFPAGE_DUPLICATE])
476    # Should not error: no duplicate refpages.
477    assert(not ckr.check("[open,refpage='abc']").messages)
478    assert(not ckr.check("[open,refpage='123']").messages)
479
480    # Should error: repeated refpage
481    assert(ckr.check(
482        """[open,refpage='abc']
483        [open,refpage='abc']""").messages)
484
485    # Should error: repeated refpage with something intervening
486    assert(ckr.check(
487        """[open,refpage='abc']
488        [open,refpage='123']
489        [open,refpage='abc']""").messages)
490
491
492def test_UNCLOSED_BLOCK(ckr):
493    """Check the UNCLOSED_BLOCK message."""
494    ckr.enabled([MessageId.UNCLOSED_BLOCK])
495    # These should all have 0 errors
496    assert(not ckr.check("== Heading").messages)
497    assert(not ckr.check(
498        """****
499        == Heading
500        ****""").messages)
501    assert(not ckr.check(
502        """****
503        contents
504        ****""").messages)
505    assert(not ckr.check(
506        """****
507        [source,c]
508        ----
509        this is code
510        ----
511        ****""").messages)
512    assert(not ckr.check(
513        """[open,]
514        --
515        123
516
517        [source,c]
518        ----
519        this is code
520        ----
521        ****
522        * this is in a box
523        ****
524
525        Now we can close the ref page.
526        --""").messages)
527
528    # These should all have 1 error because I removed a block close.
529    # Because some of them, the missing block close is an interior one, the stack might look weird,
530    # but it's still only 1 error - no matter how many are left unclosed.
531    assert(ckr.check(
532        """****
533        == Heading""").numDiagnostics() == 1)
534    assert(ckr.check(
535        """****
536        contents""").numDiagnostics() == 1)
537    assert(ckr.check(
538        """****
539        [source,c]
540        ----
541        this is code
542        ****""").numDiagnostics() == 1)
543    assert(ckr.check(
544        """****
545        [source,c]
546        ----
547        this is code
548        ----""").numDiagnostics() == 1)
549    assert(ckr.check(
550        """[open,]
551        --
552        123
553
554        [source,c]
555        ----
556        this is code
557        ----
558        ****
559        * this is in a box
560        ****""").numDiagnostics() == 1)
561    assert(ckr.check(
562        """[open,]
563        --
564        123
565
566        [source,c]
567        ----
568        this is code
569        ----
570        ****
571        * this is in a box
572        --""").numDiagnostics() == 1)
573    assert(ckr.check(
574        """[open,]
575        --
576        123
577
578        [source,c]
579        ----
580        this is code
581        ****
582        * this is in a box
583        ****
584
585        Now we can close the ref page.
586        --""").numDiagnostics() == 1)
587    assert(ckr.check(
588        """[open,]
589        --
590        123
591
592        [source,c]
593        ----
594        this is code
595        ****
596        * this is in a box
597
598        Now we can close the ref page.
599        --""").numDiagnostics() == 1)
600    assert(ckr.check(
601        """[open,]
602        --
603        123
604
605        [source,c]
606        ----
607        this is code
608        ****
609        * this is in a box""").numDiagnostics() == 1)
610
611    # This should have 0 errors of UNCLOSED_BLOCK: the missing opening -- should get automatically fake-inserted,
612    assert(not ckr.check(
613        """[open,]
614        == Heading
615        --""").messages)
616
617    # Should have 1 error: block left open at end of file
618    assert(ckr.check(
619        """[open,]
620        --
621        bla""").numDiagnostics() == 1)
622
623
624def test_code_block_tracking(ckr):
625    """Check to make sure that no other messages get triggered in a code block."""
626    ckr.enabled([MessageId.BAD_ENTITY])
627
628    # Should have 1 error: not a valid entity
629    assert(ckr.check("slink:BogusStruct").numDiagnostics() == 1)
630    assert(ckr.check(
631        """****
632        * slink:BogusStruct
633        ****""").numDiagnostics() == 1)
634
635    # should have zero errors: the invalid entity is inside a code block,
636    # so it shouldn't be parsed.
637    # (In reality, it's mostly the MISSING_MACRO message that might interact with code block tracking,
638    # but this is easier to test in an API-agnostic way.)
639    assert(not ckr.check(
640        """[source,c]
641        ----
642        This code happens to include the characters slink:BogusStruct
643        ----""").messages)
644