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)