1import os 2import pytest # type: ignore 3 4import sphinx.addnodes 5import sphinx.environment 6from breathe.parser.compound import ( 7 compounddefTypeSub, linkedTextTypeSub, memberdefTypeSub, paramTypeSub, 8 refTypeSub, MixedContainer 9) 10from breathe.renderer.sphinxrenderer import SphinxRenderer 11from breathe.renderer.filter import OpenFilter 12from docutils import frontend, nodes, parsers, utils 13 14from sphinx.testing.fixtures import ( 15 test_params, app_params, make_app, shared_result, 16 sphinx_test_tempdir, rootdir 17) 18from sphinx.testing.path import path 19 20sphinx.locale.init([], '') 21 22 23@pytest.fixture(scope='function') 24def app(test_params, app_params, make_app, shared_result): 25 """ 26 Based on sphinx.testing.fixtures.app 27 """ 28 args, kwargs = app_params 29 assert 'srcdir' in kwargs 30 kwargs['srcdir'].makedirs(exist_ok=True) 31 (kwargs['srcdir'] / 'conf.py').write_text('') 32 app_ = make_app(*args, **kwargs) 33 yield app_ 34 35 print('# testroot:', kwargs.get('testroot', 'root')) 36 print('# builder:', app_.builder.name) 37 print('# srcdir:', app_.srcdir) 38 print('# outdir:', app_.outdir) 39 print('# status:', '\n' + app_._status.getvalue()) 40 print('# warning:', '\n' + app_._warning.getvalue()) 41 42 if test_params['shared_result']: 43 shared_result.store(test_params['shared_result'], app_) 44 45 46class WrappedDoxygenNode: 47 """ 48 A base class for test wrappers of Doxygen nodes. It allows setting all attributes via keyword arguments 49 in the constructor. 50 """ 51 def __init__(self, cls, *args, **kwargs): 52 if cls: 53 cls.__init__(self, args) 54 for name, value in kwargs.items(): 55 if not hasattr(self, name): 56 raise AttributeError('invalid attribute ' + name) 57 setattr(self, name, value) 58 59 60class WrappedMixedContainer(MixedContainer, WrappedDoxygenNode): 61 """A test wrapper of Doxygen mixed container.""" 62 def __init__(self, **kwargs): 63 MixedContainer.__init__(self, None, None, None, None) 64 WrappedDoxygenNode.__init__(self, None, **kwargs) 65 66 67class WrappedLinkedText(linkedTextTypeSub, WrappedDoxygenNode): 68 """A test wrapper of Doxygen linked text.""" 69 def __init__(self, **kwargs): 70 WrappedDoxygenNode.__init__(self, linkedTextTypeSub, **kwargs) 71 72 73class WrappedMemberDef(memberdefTypeSub, WrappedDoxygenNode): 74 """A test wrapper of Doxygen class/file/namespace member symbol such as a function declaration.""" 75 def __init__(self, **kwargs): 76 WrappedDoxygenNode.__init__(self, memberdefTypeSub, **kwargs) 77 78 79class WrappedParam(paramTypeSub, WrappedDoxygenNode): 80 """A test wrapper of Doxygen parameter.""" 81 def __init__(self, **kwargs): 82 WrappedDoxygenNode.__init__(self, paramTypeSub, **kwargs) 83 84 85class WrappedRef(refTypeSub, WrappedDoxygenNode): 86 """A test wrapper of Doxygen ref.""" 87 def __init__(self, node_name, **kwargs): 88 WrappedDoxygenNode.__init__(self, refTypeSub, node_name, **kwargs) 89 90 91class WrappedCompoundDef(compounddefTypeSub, WrappedDoxygenNode): 92 """A test wrapper of Doxygen compound definition.""" 93 def __init__(self, **kwargs): 94 WrappedDoxygenNode.__init__(self, compounddefTypeSub, **kwargs) 95 96 97class MockState: 98 def __init__(self, app): 99 env = sphinx.environment.BuildEnvironment(app) 100 env.setup(app) 101 env.temp_data['docname'] = 'mock-doc' 102 settings = frontend.OptionParser( 103 components=(parsers.rst.Parser,)).get_default_values() 104 settings.env = env 105 self.document = utils.new_document('', settings) 106 107 def nested_parse(self, content, content_offset, contentnode): 108 pass 109 110 111class MockReporter: 112 def __init__(self): 113 pass 114 115 def warning(self, description, line): 116 pass 117 118 def debug(self, message): 119 pass 120 121 122class MockStateMachine: 123 def __init__(self): 124 self.reporter = MockReporter() 125 126 def get_source_and_line(self, lineno: int): 127 if lineno is None: 128 lineno = 42 129 return 'mock-doc', lineno 130 131 132class MockMaskFactory: 133 def __init__(self): 134 pass 135 136 def mask(self, node): 137 return node 138 139 140class MockContext: 141 def __init__(self, app, node_stack, domain=None, options=[]): 142 self.domain = domain 143 self.node_stack = node_stack 144 self.directive_args = [ 145 None, # name 146 None, # arguments 147 options, # options 148 None, # content 149 None, # lineno 150 None, # content_offset 151 None, # block_text 152 MockState(app), MockStateMachine()] 153 self.child = None 154 self.mask_factory = MockMaskFactory() 155 156 def create_child_context(self, attribute): 157 return self 158 159 160class MockTargetHandler: 161 def __init__(self): 162 pass 163 164 def create_target(self, refid): 165 pass 166 167 168class MockDocument: 169 def __init__(self): 170 self.reporter = MockReporter() 171 172 173class MockCompoundParser: 174 """ 175 A compound parser reads a doxygen XML file from disk; this mock implements 176 a mapping of what would be the file name on disk to data using a dict. 177 """ 178 def __init__(self, compound_dict): 179 self.compound_dict = compound_dict 180 181 class MockFileData: 182 def __init__(self, compounddef): 183 self.compounddef = compounddef 184 185 def parse(self, compoundname): 186 compounddef = self.compound_dict[compoundname] 187 return self.MockFileData(compounddef) 188 189 190class NodeFinder(nodes.NodeVisitor): 191 """Find node with specified class name.""" 192 def __init__(self, name, document): 193 nodes.NodeVisitor.__init__(self, document) 194 self.name = name 195 self.found_nodes = [] 196 197 def unknown_visit(self, node): 198 if node.__class__.__name__ == self.name: 199 self.found_nodes.append(node) 200 201 202def find_nodes(nodes, name): 203 """Find all docutils nodes with specified class name in *nodes*.""" 204 finder = NodeFinder(name, MockDocument()) 205 for node in nodes: 206 node.walk(finder) 207 return finder.found_nodes 208 209 210def find_node(nodes, name): 211 """ 212 Find a single docutils node with specified class name in *nodes*. 213 Throw an exception if there isn't exactly one such node. 214 """ 215 found_nodes = find_nodes(nodes, name) 216 if len(found_nodes) != 1: 217 raise Exception('the number of nodes {0} is {1}'.format(name, len(found_nodes))) 218 return found_nodes[0] 219 220 221def test_find_nodes(): 222 section = nodes.section() 223 foo = nodes.Text('foo') 224 desc = nodes.description() 225 bar = nodes.Text('bar') 226 section.children = [foo, desc, bar] 227 assert(find_nodes(section, 'description') == [desc]) 228 assert(find_nodes([section, desc], 'description') == [desc, desc]) 229 assert(find_nodes([], 'description') == []) 230 assert(find_nodes(section, 'unknown') == []) 231 assert(find_nodes(section, 'Text') == [foo, bar]) 232 233 234def check_exception(func, message): 235 """Check if func() throws an exception with the specified message.""" 236 exception = None 237 try: 238 func() 239 except Exception as e: 240 exception = e 241 print(str(exception)) 242 assert exception and str(exception) == message 243 244 245def test_find_node(): 246 section = nodes.section() 247 foo = nodes.Text('foo') 248 desc = nodes.description() 249 bar = nodes.Text('bar') 250 section.children = [foo, desc, bar] 251 assert(find_node(section, 'description') == desc) 252 check_exception(lambda: find_node([section, desc], 'description'), 253 'the number of nodes description is 2') 254 check_exception(lambda: find_node([], 'description'), 255 'the number of nodes description is 0') 256 check_exception(lambda: find_node([section], 'unknown'), 257 'the number of nodes unknown is 0') 258 check_exception(lambda: find_node([section], 'Text'), 259 'the number of nodes Text is 2') 260 261 262def render(app, member_def, domain=None, show_define_initializer=False, 263 compound_parser=None, options=[]): 264 """Render Doxygen *member_def* with *renderer_class*.""" 265 266 app.config.breathe_separate_member_pages = False 267 app.config.breathe_use_project_refids = False 268 app.config.breathe_show_define_initializer = show_define_initializer 269 app.config.breathe_order_parameters_first = False 270 app.config.breathe_debug_trace_directives = False 271 app.config.breathe_debug_trace_doxygen_ids = False 272 app.config.breathe_debug_trace_qualification = False 273 renderer = SphinxRenderer(app, 274 None, # project_info 275 [], # node_stack 276 None, # state 277 None, # document 278 MockTargetHandler(), 279 compound_parser, 280 OpenFilter()) 281 renderer.context = MockContext(app, [member_def], domain, options) 282 return renderer.render(member_def) 283 284 285def test_render_func(app): 286 member_def = WrappedMemberDef(kind='function', definition='void foo', type_='void', name='foo', argsstring='(int)', 287 virt='non-virtual', 288 param=[WrappedParam(type_=WrappedLinkedText(content_=[WrappedMixedContainer(value=u'int')]))]) 289 signature = find_node(render(app, member_def), 'desc_signature') 290 assert signature.astext().startswith('void') 291 if sphinx.version_info[0] < 4: 292 assert find_node(signature, 'desc_name')[0] == 'foo' 293 else: 294 n = find_node(signature, 'desc_name')[0] 295 assert isinstance(n, sphinx.addnodes.desc_sig_name) 296 assert len(n) == 1 297 assert n[0] == 'foo' 298 params = find_node(signature, 'desc_parameterlist') 299 assert len(params) == 1 300 param = params[0] 301 if sphinx.version_info[0] < 4: 302 assert param[0] == 'int' 303 else: 304 assert isinstance(param[0], sphinx.addnodes.desc_sig_keyword_type) 305 assert param[0][0] == 'int' 306 307 308def test_render_typedef(app): 309 member_def = WrappedMemberDef(kind='typedef', definition='typedef int foo', type_='int', name='foo') 310 signature = find_node(render(app, member_def), 'desc_signature') 311 assert signature.astext() == 'typedef int foo' 312 313 314def test_render_c_typedef(app): 315 member_def = WrappedMemberDef(kind='typedef', definition='typedef unsigned int bar', type_='unsigned int', name='bar') 316 signature = find_node(render(app, member_def, domain='c'), 'desc_signature') 317 assert signature.astext() == 'typedef unsigned int bar' 318 319 320def test_render_c_function_typedef(app): 321 member_def = WrappedMemberDef(kind='typedef', definition='typedef void* (*voidFuncPtr)(float, int)', 322 type_='void* (*', name='voidFuncPtr', argsstring=')(float, int)') 323 signature = find_node(render(app, member_def, domain='c'), 'desc_signature') 324 assert signature.astext().startswith('typedef void *') 325 if sphinx.version_info[0] < 4: 326 params = find_node(signature, 'desc_parameterlist') 327 assert len(params) == 2 328 assert params[0].astext() == "float" 329 assert params[1].astext() == "int" 330 else: 331 # the use of desc_parameterlist in this case was not correct, 332 # it should only be used for a top-level function 333 pass 334 335 336def test_render_using_alias(app): 337 member_def = WrappedMemberDef(kind='typedef', definition='using foo = int', type_='int', name='foo') 338 signature = find_node(render(app, member_def), 'desc_signature') 339 assert signature.astext() == 'using foo = int' 340 341 342def test_render_const_func(app): 343 member_def = WrappedMemberDef(kind='function', definition='void f', type_='void', name='f', argsstring='() const', 344 virt='non-virtual', const='yes') 345 signature = find_node(render(app, member_def), 'desc_signature') 346 assert '_CPPv2NK1fEv' in signature['ids'] 347 348 349def test_render_lvalue_func(app): 350 member_def = WrappedMemberDef(kind='function', definition='void f', type_='void', name='f', argsstring='() &', 351 virt='non-virtual', refqual='lvalue') 352 signature = find_node(render(app, member_def), 'desc_signature') 353 assert signature.astext().endswith('&') 354 355 356def test_render_rvalue_func(app): 357 member_def = WrappedMemberDef(kind='function', definition='void f', type_='void', name='f', argsstring='() &&', 358 virt='non-virtual', refqual='rvalue') 359 signature = find_node(render(app, member_def), 'desc_signature') 360 assert signature.astext().endswith('&&') 361 362 363def test_render_const_lvalue_func(app): 364 member_def = WrappedMemberDef(kind='function', definition='void f', type_='void', name='f',argsstring='() const &', 365 virt='non-virtual', const='yes', refqual='lvalue') 366 signature = find_node(render(app, member_def), 'desc_signature') 367 assert signature.astext().endswith('const &') 368 369 370def test_render_const_rvalue_func(app): 371 member_def = WrappedMemberDef(kind='function', definition='void f', type_='void', name='f', argsstring='() const &&', 372 virt='non-virtual', const='yes', refqual='rvalue') 373 signature = find_node(render(app, member_def), 'desc_signature') 374 assert signature.astext().endswith('const &&') 375 376 377def test_render_variable_initializer(app): 378 member_def = WrappedMemberDef(kind='variable', definition='const int EOF', type_='const int', name='EOF', 379 initializer=WrappedMixedContainer(value=u'= -1')) 380 signature = find_node(render(app, member_def), 'desc_signature') 381 assert signature.astext() == 'const int EOF = -1' 382 383 384def test_render_define_initializer(app): 385 member_def = WrappedMemberDef(kind='define', name='MAX_LENGTH', 386 initializer=WrappedLinkedText(content_=[WrappedMixedContainer(value=u'100')])) 387 signature_w_initializer = find_node(render(app, member_def, show_define_initializer=True), 'desc_signature') 388 assert signature_w_initializer.astext() == 'MAX_LENGTH 100' 389 390 member_def_no_show = WrappedMemberDef(kind='define', name='MAX_LENGTH_NO_INITIALIZER', 391 initializer=WrappedLinkedText(content_=[WrappedMixedContainer(value=u'100')])) 392 393 signature_wo_initializer = find_node(render(app, member_def_no_show, show_define_initializer=False), 'desc_signature') 394 assert signature_wo_initializer.astext() == 'MAX_LENGTH_NO_INITIALIZER' 395 396 397def test_render_define_no_initializer(app): 398 sphinx.addnodes.setup(app) 399 member_def = WrappedMemberDef(kind='define', name='USE_MILK') 400 signature = find_node(render(app, member_def), 'desc_signature') 401 assert signature.astext() == 'USE_MILK' 402 403 404def test_render_innergroup(app): 405 refid = 'group__innergroup' 406 mock_compound_parser = MockCompoundParser({ 407 refid: WrappedCompoundDef(kind='group', 408 compoundname='InnerGroup', 409 briefdescription='InnerGroup') 410 }) 411 ref = WrappedRef('InnerGroup', refid=refid) 412 compound_def = WrappedCompoundDef(kind='group', 413 compoundname='OuterGroup', 414 briefdescription='OuterGroup', 415 innergroup=[ref]) 416 assert all(el.astext() != 'InnerGroup' 417 for el in render(app, compound_def, 418 compound_parser=mock_compound_parser)) 419 assert any(el.astext() == 'InnerGroup' 420 for el in render(app, compound_def, 421 compound_parser=mock_compound_parser, 422 options=['inner'])) 423 424def get_directive(app): 425 from breathe.directives.function import DoxygenFunctionDirective 426 from breathe.project import ProjectInfoFactory 427 from breathe.parser import DoxygenParserFactory 428 from breathe.finder.factory import FinderFactory 429 from docutils.statemachine import StringList 430 app.config.breathe_separate_member_pages = False 431 app.config.breathe_default_project = 'test_project' 432 app.config.breathe_domain_by_extension = {} 433 app.config.breathe_domain_by_file_pattern = {} 434 app.config.breathe_use_project_refids = False 435 project_info_factory = ProjectInfoFactory(app) 436 parser_factory = DoxygenParserFactory(app) 437 finder_factory = FinderFactory(app, parser_factory) 438 cls_args = ('doxygenclass', ['at::Tensor'], 439 {'members': '', 'protected-members': None, 'undoc-members': None}, 440 StringList([], items=[]), 441 20, 24, 442 ('.. doxygenclass:: at::Tensor\n :members:\n' 443 ' :protected-members:\n :undoc-members:'), 444 MockState(app), 445 MockStateMachine(), 446 ) 447 return DoxygenFunctionDirective(finder_factory, project_info_factory, parser_factory, *cls_args) 448 449 450def get_matches(datafile): 451 from breathe.parser.compoundsuper import sectiondefType 452 from xml.dom import minidom 453 454 argsstrings = [] 455 with open(os.path.join(os.path.dirname(__file__), 'data', datafile)) as fid: 456 xml = fid.read() 457 doc = minidom.parseString(xml) 458 459 sectiondef = sectiondefType.factory() 460 for child in doc.documentElement.childNodes: 461 sectiondef.buildChildren(child, 'memberdef') 462 if getattr(child, 'tagName', None) == 'memberdef': 463 # Get the argsstring function declaration 464 argsstrings.append(child.getElementsByTagName('argsstring')[0].childNodes[0].data) 465 matches = [[m, sectiondef] for m in sectiondef.memberdef] 466 return argsstrings, matches 467 468 469def test_resolve_overrides(app): 470 # Test that multiple function overrides works 471 argsstrings, matches = get_matches('arange.xml') 472 cls = get_directive(app) 473 474 # Verify that the exact arguments returns one override 475 for args in argsstrings: 476 ast_param = cls._parse_args(args) 477 ret = cls._resolve_function(matches, ast_param, None) 478 479def test_ellipsis(app): 480 argsstrings, matches = get_matches('ellipsis.xml') 481 cls = get_directive(app) 482 483 # Verify that parsing an ellipsis works 484 ast_param = cls._parse_args(argsstrings[0]) 485 ret = cls._resolve_function(matches, ast_param, None) 486 487