Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AttributeError: 'ExceptionChainRepr' object has no attribute 'replace' #55

Closed
sandruczyk opened this issue May 27, 2022 · 4 comments · Fixed by #65
Closed

AttributeError: 'ExceptionChainRepr' object has no attribute 'replace' #55

sandruczyk opened this issue May 27, 2022 · 4 comments · Fixed by #65

Comments

@sandruczyk
Copy link

sandruczyk commented May 27, 2022

MacOS 12.4
Python 3.9.12 (system Python)
pytest 7.1.2
pytest-nunit 1.0.0

When executing code with a failing fixture, and a test with 'xfail' mark:

import pytest

@pytest.fixture(scope="class")
def failing_fixture():
    raise Exception("SomeException")

@pytest.mark.usefixtures("failing_fixture")
class TestOne:
    def test_one(self):
        pass

    @pytest.mark.xfail(reason="No reason")
    def test_two(self):
        pytest.fail()

I'm getting an exception:
AttributeError: 'ExceptionChainRepr' object has no attribute 'replace'
nunit_exception.log

But only when executing with '--nunit-xml' option

@polkapolka
Copy link

I have re-created the error in a test case.

Traceback (most recent call last):
  File "/Users/ppolk/Projects/open_source/pytest-nunit/.tox/py36-pytest46/lib/python3.6/site-packages/_pytest/pytester.py", line 881, in runpytest_inprocess
    reprec = self.inline_run(*args, **kwargs)
  File "/Users/ppolk/Projects/open_source/pytest-nunit/.tox/py36-pytest46/lib/python3.6/site-packages/_pytest/pytester.py", line 847, in inline_run
    ret = pytest.main(list(args), plugins=plugins)
  File "/Users/ppolk/Projects/open_source/pytest-nunit/.tox/py36-pytest46/lib/python3.6/site-packages/_pytest/config/__init__.py", line 84, in main
    return config.hook.pytest_cmdline_main(config=config)
  File "/Users/ppolk/Projects/open_source/pytest-nunit/.tox/py36-pytest46/lib/python3.6/site-packages/pluggy/hooks.py", line 286, in __call__
    return self._hookexec(self, self.get_hookimpls(), kwargs)
  File "/Users/ppolk/Projects/open_source/pytest-nunit/.tox/py36-pytest46/lib/python3.6/site-packages/pluggy/manager.py", line 93, in _hookexec
    return self._inner_hookexec(hook, methods, kwargs)
  File "/Users/ppolk/Projects/open_source/pytest-nunit/.tox/py36-pytest46/lib/python3.6/site-packages/pluggy/manager.py", line 87, in <lambda>
    firstresult=hook.spec.opts.get("firstresult") if hook.spec else False,
  File "/Users/ppolk/Projects/open_source/pytest-nunit/.tox/py36-pytest46/lib/python3.6/site-packages/pluggy/callers.py", line 208, in _multicall
    return outcome.get_result()
  File "/Users/ppolk/Projects/open_source/pytest-nunit/.tox/py36-pytest46/lib/python3.6/site-packages/pluggy/callers.py", line 80, in get_result
    raise ex[1].with_traceback(ex[2])
  File "/Users/ppolk/Projects/open_source/pytest-nunit/.tox/py36-pytest46/lib/python3.6/site-packages/pluggy/callers.py", line 187, in _multicall
    res = hook_impl.function(*args)
  File "/Users/ppolk/Projects/open_source/pytest-nunit/.tox/py36-pytest46/lib/python3.6/site-packages/_pytest/main.py", line 243, in pytest_cmdline_main
    return wrap_session(config, _main)
  File "/Users/ppolk/Projects/open_source/pytest-nunit/.tox/py36-pytest46/lib/python3.6/site-packages/_pytest/main.py", line 236, in wrap_session
    session=session, exitstatus=session.exitstatus
  File "/Users/ppolk/Projects/open_source/pytest-nunit/.tox/py36-pytest46/lib/python3.6/site-packages/pluggy/hooks.py", line 286, in __call__
    return self._hookexec(self, self.get_hookimpls(), kwargs)
  File "/Users/ppolk/Projects/open_source/pytest-nunit/.tox/py36-pytest46/lib/python3.6/site-packages/pluggy/manager.py", line 93, in _hookexec
    return self._inner_hookexec(hook, methods, kwargs)
  File "/Users/ppolk/Projects/open_source/pytest-nunit/.tox/py36-pytest46/lib/python3.6/site-packages/pluggy/manager.py", line 337, in traced_hookexec
    return outcome.get_result()
  File "/Users/ppolk/Projects/open_source/pytest-nunit/.tox/py36-pytest46/lib/python3.6/site-packages/pluggy/callers.py", line 80, in get_result
    raise ex[1].with_traceback(ex[2])
  File "/Users/ppolk/Projects/open_source/pytest-nunit/.tox/py36-pytest46/lib/python3.6/site-packages/pluggy/callers.py", line 52, in from_call
    result = func()
  File "/Users/ppolk/Projects/open_source/pytest-nunit/.tox/py36-pytest46/lib/python3.6/site-packages/pluggy/manager.py", line 335, in <lambda>
    outcome = _Result.from_call(lambda: oldcall(hook, hook_impls, kwargs))
  File "/Users/ppolk/Projects/open_source/pytest-nunit/.tox/py36-pytest46/lib/python3.6/site-packages/pluggy/manager.py", line 87, in <lambda>
    firstresult=hook.spec.opts.get("firstresult") if hook.spec else False,
  File "/Users/ppolk/Projects/open_source/pytest-nunit/.tox/py36-pytest46/lib/python3.6/site-packages/pluggy/callers.py", line 203, in _multicall
    gen.send(outcome)
  File "/Users/ppolk/Projects/open_source/pytest-nunit/.tox/py36-pytest46/lib/python3.6/site-packages/_pytest/terminal.py", line 669, in pytest_sessionfinish
    outcome.get_result()
  File "/Users/ppolk/Projects/open_source/pytest-nunit/.tox/py36-pytest46/lib/python3.6/site-packages/pluggy/callers.py", line 80, in get_result
    raise ex[1].with_traceback(ex[2])
  File "/Users/ppolk/Projects/open_source/pytest-nunit/.tox/py36-pytest46/lib/python3.6/site-packages/pluggy/callers.py", line 187, in _multicall
    res = hook_impl.function(*args)
  File "/Users/ppolk/Projects/open_source/pytest-nunit/pytest_nunit/plugin.py", line 412, in pytest_sessionfinish
    result = NunitTestRun(self).generate_xml()
  File "/Users/ppolk/Projects/open_source/pytest-nunit/pytest_nunit/nunit.py", line 263, in generate_xml
    tr = self.as_test_run()
  File "/Users/ppolk/Projects/open_source/pytest-nunit/pytest_nunit/nunit.py", line 257, in as_test_run
    test_suite=self.test_suites,
  File "/Users/ppolk/Projects/open_source/pytest-nunit/pytest_nunit/nunit.py", line 237, in test_suites
    for nodeid, module in self.nunitxml.modules.items()
  File "/Users/ppolk/Projects/open_source/pytest-nunit/pytest_nunit/nunit.py", line 237, in <listcomp>
    for nodeid, module in self.nunitxml.modules.items()
  File "/Users/ppolk/Projects/open_source/pytest-nunit/pytest_nunit/nunit.py", line 196, in test_cases
    for nodeid, case in self.nunitxml.modules[module].cases.items()
  File "/Users/ppolk/Projects/open_source/pytest-nunit/pytest_nunit/nunit.py", line 196, in <listcomp>
    for nodeid, case in self.nunitxml.modules[module].cases.items()
  File "/Users/ppolk/Projects/open_source/pytest-nunit/pytest_nunit/attrs2xml.py", line 9, in __init__
    self.text = escape(text, {\'\\x1b\': "&#x1b;"})
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/xml/sax/saxutils.py", line 27, in escape
    data = data.replace("&", "&amp;")',
 "AttributeError: 'ExceptionChainRepr' object has no attribute 'replace'"

@polkapolka
Copy link

polkapolka commented Oct 16, 2022

Understanding this issue.

On these lines, a FailureType is initialized.

failure=FailureType(
message=CdataComment(text=case["error"]),
stack_trace=CdataComment(text=case["stack-trace"]),
),

The result from case["error"] is expected to be a string value that gets assigned to the text attribute on the CDataComment object.

class CdataComment(ET.Element):
def __init__(self, text):
super(CdataComment, self).__init__("CDATA!")
self.text = escape(text, {"\x1b": "&#x1b;"})

This testcase instead of returning a string, returns an instance of the object ExceptionChainRepr.

https://github.com/pytest-dev/pytest/blob/3dac833a520cc299c20b7270db87d62412b4119a/src/_pytest/_code/code.py#L1018-L1034

Compare this output from the test_fail testcase with the output from the test_fixture_error testcase.

test_fail testcase

	{'attachments': {'fail.pth': 'desc'},
	 'call-report': <TestReport 'test_attachment_attach_on_any.py::test_fail' when='call' outcome='failed'>,
	 'duration': 0.008849,
	 'error': 'add_nunit_attachment = <bound method '
	          '_NunitNodeReporter.add_attachment of '
	          '<pytest_nunit.plugin._NunitNodeReporter object at 0x7f9a88608320>>\n'
	          '\n'
	          '    def test_fail(add_nunit_attachment):\n'
	          '        add_nunit_attachment("fail.pth", "desc")\n'
	          '>       assert 1 == 0\n'
	          'E       assert 1 == 0\n'
	          'E         -1\n'
	          'E         +0\n'
	          '\n'
	          'test_attachment_attach_on_any.py:6: AssertionError',
	 'idref': 101,
	 'name': 'test_attachment_attach_on_any.py::test_fail',
	 'outcome': 'failed',
	 'path': 'test_attachment_attach_on_any.py',
	 'properties': {'fspath': 'test_attachment_attach_on_any.py',
	                'python-version': '3.6.5 (v3.6.5:f59c0932b4, Mar 28 2018, '
	                                  '05:52:31) \n'
	                                  '[GCC 4.2.1 Compatible Apple LLVM 6.0 '
	                                  '(clang-600.0.57)]'},
	 'reason': '',
	 'setup-report': <TestReport 'test_attachment_attach_on_any.py::test_fail' when='setup' outcome='passed'>,
	 'stack-trace': '/private/var/folders/d4/ry1gfxvx6l78_jp31gz9fjk40000gq/T/pytest-of-ppolk/pytest-202/test_attachment_attach_on_any0/test_attachment_attach_on_any.py:6: '
	                'assert 1 == 0',
	 'start': datetime.datetime(2022, 10, 16, 7, 52, 23, 989521),
	 'stderr': '',
	 'stdout': '',
	 'stop': datetime.datetime(2022, 10, 16, 7, 52, 23, 998370),
	 'teardown-report': <TestReport 'test_attachment_attach_on_any.py::test_fail' when='teardown' outcome='passed'>}

test_fixture_error testcase

	{'attachments': None,
	 'call-report': None,
	 'duration': 0,
	 'error': <<class '_pytest._code.code.ExceptionChainRepr'> instance at 7fddf06e5208>,
	 'idref': 101,
	 'name': 'test_failing_fixture.py::TestOne::test_two',
	 'outcome': 'skipped',
	 'path': 'test_failing_fixture.py',
	 'properties': {'fspath': 'test_failing_fixture.py',
	                'python-version': '3.6.5 (v3.6.5:f59c0932b4, Mar 28 2018, '
	                                  '05:52:31) \n'
	                                  '[GCC 4.2.1 Compatible Apple LLVM 6.0 '
	                                  '(clang-600.0.57)]'},
	 'reason': '',
	 'setup-report': <TestReport 'test_failing_fixture.py::TestOne::test_two' when='setup' outcome='skipped'>,
	 'stack-trace': '',
	 'start': datetime.datetime(2022, 10, 16, 8, 2, 32, 562000),
	 'stderr': '',
	 'stdout': '',
	 'stop': datetime.datetime(2022, 10, 16, 8, 2, 32, 563996),
	 'teardown-report': <TestReport 'test_failing_fixture.py::TestOne::test_two' when='teardown' outcome='passed'>}

One potential solution, is to coerce the ReprObject into a string by str(case["error"]).

That would set the 'error' value to this.

     '@pytest.fixture(scope="class")\n    def failing_fixture():\n>       raise Exception("SomeException")\nE       Exception: SomeException\n\ntest_failing_fixture.py:5: Exception'

The only concern I have is whether all the other case values for this testcase are captured correctly.
@tonybaloney How would you suggest I solve this issue?

@tonybaloney
Copy link
Collaborator

I think it should do the following:

  1. if isinstance matches the ExceptionChainRepr type then do special formatting to str if required (does it have a sensible __str__?)
  2. else use str() and force the type into string

@polkapolka
Copy link

polkapolka commented Oct 17, 2022

Ok, looked into it some more.

  • The ExceptionChainRepr has a sensible __str__.
  • The ExceptionChainRepr object also contains the stack trace value. This value is not passed into case["stack-trace"], so I added that.
  • Finally, I changed the assertions. The described tests fail due to an exception during the test node setup. This is a return code of 1, see description, and not return code 0 or 3. So the results in stdout from test_one should be Error, and the result in stdout from test_two should be XFail, and the stderr should be empty. These assertions catch the original error.

Screen Shot 2022-10-17 at 2 57 47 PM

And are resolved by the latest commit.

polkapolka pushed a commit to polkapolka/pytest-nunit that referenced this issue Oct 17, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants