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