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