pytest-html icon indicating copy to clipboard operation
pytest-html copied to clipboard

Unable to save additional screenshots

Open BeyondEvil opened this issue 2 years ago • 2 comments

          Sure, is not related to this at all. 

I have the following code to display the screenshots I'm taking during my tests in the report:

@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item):
    """
    This method takes the images in the folder of the report and writes them inside the report.
    """
    pytest_html = item.config.pluginmanager.getplugin("html")
    outcome = yield

    # Get the images to be displayed inside the report
    report = outcome.get_result()
    extra = getattr(report, "extra", [])
    if report.when == "call":

        node_id = item.funcargs['request'].node.nodeid

        # Get output directory
        directory = _get_report_folder()

        # Extract images for current tc
        for file in os.listdir(directory):
            if re.search(re.escape(f'{node_id}_'), file):
                image = os.path.join(directory, file)
                with open(image, "rb") as image_file:
                    screenshot = base64.b64encode(image_file.read())
                    extra.append(pytest_html.extras.image(screenshot.decode(), ''))

                # Remove as it is already saved
                try:
                    os.remove(image)
                except OSError:
                    pass

        report.extra = extra

In the new version it throws:


INTERNALERROR> Traceback (most recent call last):
INTERNALERROR>   File "<my_path>\.venv\lib\site-packages\_pytest\main.py", line 270, in wrap_session
INTERNALERROR>     session.exitstatus = doit(config, session) or 0
INTERNALERROR>   File "<my_path>\.venv\lib\site-packages\_pytest\main.py", line 324, in _main
INTERNALERROR>     config.hook.pytest_runtestloop(session=session)
INTERNALERROR>   File "<my_path>\.venv\lib\site-packages\pluggy\_hooks.py", line 265, in __call__
INTERNALERROR>     return self._hookexec(self.name, self.get_hookimpls(), kwargs, firstresult)
INTERNALERROR>   File "<my_path>\.venv\lib\site-packages\pluggy\_manager.py", line 80, in _hookexec
INTERNALERROR>     return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
INTERNALERROR>   File "<my_path>\.venv\lib\site-packages\pluggy\_callers.py", line 60, in _multicall
INTERNALERROR>     return outcome.get_result()
INTERNALERROR>   File "<my_path>\.venv\lib\site-packages\pluggy\_result.py", line 60, in get_result
INTERNALERROR>     raise ex[1].with_traceback(ex[2])
INTERNALERROR>   File "<my_path>\.venv\lib\site-packages\pluggy\_callers.py", line 39, in _multicall
INTERNALERROR>     res = hook_impl.function(*args)
INTERNALERROR>   File "<my_path>\.venv\lib\site-packages\_pytest\main.py", line 349, in pytest_runtestloop
INTERNALERROR>     item.config.hook.pytest_runtest_protocol(item=item, nextitem=nextitem)
INTERNALERROR>   File "<my_path>\.venv\lib\site-packages\pluggy\_hooks.py", line 265, in __call__
INTERNALERROR>     return self._hookexec(self.name, self.get_hookimpls(), kwargs, firstresult)
INTERNALERROR>   File "<my_path>\.venv\lib\site-packages\pluggy\_manager.py", line 80, in _hookexec
INTERNALERROR>     return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
INTERNALERROR>   File "<my_path>\.venv\lib\site-packages\pluggy\_callers.py", line 60, in _multicall
INTERNALERROR>     return outcome.get_result()
INTERNALERROR>   File "<my_path>\.venv\lib\site-packages\pluggy\_result.py", line 60, in get_result
INTERNALERROR>     raise ex[1].with_traceback(ex[2])
INTERNALERROR>   File "<my_path>\.venv\lib\site-packages\pluggy\_callers.py", line 39, in _multicall
INTERNALERROR>     res = hook_impl.function(*args)
INTERNALERROR>   File "<my_path>\.venv\lib\site-packages\_pytest\runner.py", line 112, in pytest_runtest_protocol
INTERNALERROR>     runtestprotocol(item, nextitem=nextitem)
INTERNALERROR>   File "<my_path>\.venv\lib\site-packages\_pytest\runner.py", line 131, in runtestprotocol
INTERNALERROR>     reports.append(call_and_report(item, "call", log))
INTERNALERROR>   File "<my_path>\.venv\lib\site-packages\_pytest\runner.py", line 222, in call_and_report
INTERNALERROR>     report: TestReport = hook.pytest_runtest_makereport(item=item, call=call)
INTERNALERROR>   File "<my_path>\.venv\lib\site-packages\pluggy\_hooks.py", line 265, in __call__
INTERNALERROR>     return self._hookexec(self.name, self.get_hookimpls(), kwargs, firstresult)
INTERNALERROR>   File "<my_path>\.venv\lib\site-packages\pluggy\_manager.py", line 80, in _hookexec
INTERNALERROR>     return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
INTERNALERROR>   File "<my_path>\.venv\lib\site-packages\pluggy\_callers.py", line 55, in _multicall
INTERNALERROR>     gen.send(outcome)
INTERNALERROR>   File "<my_path>\conftest.py", line 76, in pytest_runtest_makereport
INTERNALERROR>     extra.append(pytest_html.extras.image(screenshot.decode(), ''))
INTERNALERROR> AttributeError: 'function' object has no attribute 'image'

Originally posted by @nck974 in https://github.com/pytest-dev/pytest-html/issues/581#issuecomment-1464632768

BeyondEvil avatar Mar 10 '23 23:03 BeyondEvil

Some context on why did I do this: This was created for the following requirements:

  1. I want to be able to multithread some selenium tests, therefore I need to save the images in a directory and collect them at the end of the tests with a unique identifier for the test, I have been using the following code to get the id:
import re

def get_node_id(request):
    """
    Return the node id as a string without special characters so that it can be saved as a file.
    """
    node_id = request.node.nodeid

    return re.sub('[^a-zA-Z0-9]', '_', node_id) # remove special characters
  1. The screenshots are taken from selenium. Some tests need more than one screenshot as proof:
def save_proof_screenshot(driver: WebDriver, folder_name: str, node: str) -> None:
    """
    Take a screenshot of the current page
    """
    directory = os.path.join(
        '.',
        'output',
        'report',
        folder_name,
        datetime.now().strftime("%Y-%m-%d"),
    )

    time = int(datetime.now().timestamp())

    Path(directory).mkdir(parents=True, exist_ok=True)
    file_path = os.path.join(directory, f'{node}_{time}.png')
    driver.save_screenshot(file_path)  # has scrollbar
  1. I want the screenshots displayed together with the logs.
  2. I want the report to contain the images (base64).

And currently until the last RC it was working with that code. Example: image

nck974 avatar Mar 10 '23 23:03 nck974

@nck974

I found the root cause for this. It's this line:

pytest_html = item.config.pluginmanager.getplugin("html")

It's broken in 4.0.0rc4 (and earlier versions). But I'm adding a fix for it since it seems to be a common way.

If you want to fix it, you can replace it with:

import pytest_html

@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item):
    """
    This method takes the images in the folder of the report and writes them inside the report.
    """
    outcome = yield

    # Get the images to be displayed inside the report
    report = outcome.get_result()
    extras = getattr(report, "extras", [])
    if report.when == "call":

        node_id = item.funcargs['request'].node.nodeid

        # Get output directory
        directory = _get_report_folder()

        # Extract images for current tc
        for file in os.listdir(directory):
            if re.search(re.escape(f'{node_id}_'), file):
                image = os.path.join(directory, file)
                with open(image, "rb") as image_file:
                    screenshot = base64.b64encode(image_file.read())
                    extras.append(pytest_html.extras.image(screenshot.decode(), ''))

                # Remove as it is already saved
                try:
                    os.remove(image)
                except OSError:
                    pass

        report.extras = extras

and it should work again.

BeyondEvil avatar Apr 14 '23 14:04 BeyondEvil