Pytest plugin ignoring hooks for test run - python

I am trying to add a plugin to eavesdrop on my pytest runs. from what I can find in code samples and documentation, this "should" work (emphasis on should)
"""
to test scripted pytest control
"""
import sys
import pytest
class MyPytestPlugin:
"""
Hooks to get info from pytest
"""
def __init__(self) -> None:
pass
# pylint: disable=unused-argument
def pytest_collection_modifyitems(self, config, items):
print("collection done.")
def pytest_sessionstart(self, session):
print("*" * 10)
print("session started", session)
print("*" * 10)
def pytest_sessionfinish(self, session, exitstatus):
print("-" * 10)
print("session finished", session)
def pytest_runtest_logstart(self, nodeid, location):
print("<<<<< log start >>>>>>", nodeid)
def pytest_runtest_logfinish(self, nodeid, location):
print("<<<<< log finish >>>>>>", nodeid)
def pytest_runtest_protocol(self, item, nextitem):
print("<<<<< protocol >>>>>>")
def pytest_collectreport(self, report):
print("<<<<< collect report >>>>>>")
def pytest_report_teststatus(self, report):
print("<<<<< test status >>>>>>")
def pytest_runtest_setup(self, item):
print("<<<<< setup >>>>>>")
def pytest_runtest_call(self, item):
print("<<<<< runtest call >>>>>>")
def pytest_runtest_teardown(self, item):
print("<<<<< runtest teardown >>>>>>")
my_plugin = MyPytestPlugin()
sys.exit(
pytest.main(
["-s", "sample_test.py"],
plugins=[my_plugin],
)
)
however, when I run this, only the session (start/finish) and the log (start/finish) print their message. nothing else (for example, the runtest setup, call, teardown etc.) seems to be called. has anyone seen this before?
here is the output
python sample_test_runner.py
Test session starts (platform: linux, Python 3.9.6, pytest 7.2.0, pytest-sugar 0.9.5)
cachedir: .pytest_cache
metadata: {'Python': '3.9.6', 'Platform': 'Linux-5.10.124-linuxkit-x86_64-with-glibc2.28', 'Packages': {'pytest': '7.2.0', 'py': '1.11.0', 'pluggy': '1.0.0'}, 'Plugins': {'cov': '4.0.0', 'xdist': '3.0.2', 'metadata': '2.0.2', 'aiohttp': '0.3.0', 'asyncio': '0.16.0', 'html': '3.1.1', 'sugar': '0.9.5', 'email': '0.3', 'forked': '1.4.0', 'timeout': '2.1.0'}}
rootdir: /src/*******, configfile: tox.ini
plugins: cov-4.0.0, xdist-3.0.2, metadata-2.0.2, aiohttp-0.3.0, asyncio-0.16.0, html-3.1.1, sugar-0.9.5, email-0.3, forked-1.4.0, timeout-2.1.0
timeout: 7200.0s
timeout method: signal
timeout func_only: False
**********
session started <Session xxxxxxxx exitstatus=<ExitCode.OK: 0> testsfailed=0 testscollected=0>
**********
[gw0] Python 3.9.6 (default, Aug 17 2021, 02:38:04) -- [GCC 8.3.0]
[gw1] Python 3.9.6 (default, Aug 17 2021, 02:38:04) -- [GCC 8.3.0]
[gw2] Python 3.9.6 (default, Aug 17 2021, 02:38:04) -- [GCC 8.3.0]
gw0 [5] / gw1 [5] / gw2 [5]
scheduling tests via LoadScheduling
<<<<< log start >>>>>> sample_test.py::test_addon[t1]
<<<<< log start >>>>>> sample_test.py::test_addon[t2]
<<<<< log start >>>>>> sample_test.py::test_addon[t3]
[gw0] PASSED sample_test.py
sample_test.py::test_addon[t2] ✓ 20% ██
[gw1] PASSED sample_test.py
<<<<< log finish >>>>>> sample_test.py::test_addon[t2]
sample_test.py::test_addon[t1] ✓ 60% ██████
sample_test.py::test_addon[t3] ✓ 40% ████
[gw2] PASSED sample_test.py
<<<<< log finish >>>>>> sample_test.py::test_addon[t3]
<<<<< log finish >>>>>> sample_test.py::test_addon[t1]
<<<<< log start >>>>>> sample_test.py::test_addon[t4]
sample_test.py::test_addon[t5] ✓ 80% ████████
[gw1] PASSED sample_test.py
<<<<< log finish >>>>>> sample_test.py::test_addon[t5]
sample_test.py::test_addon[t4] ✓ 100% ██████████
[gw0] PASSED sample_test.py
<<<<< log finish >>>>>> sample_test.py::test_addon[t4]
----------
session finished <Session xxxxxxxxx exitstatus=0 testsfailed=0 testscollected=5> 0 0 5
Results (0.70s):
5 passed
for the curious, here is my test file:
import pytest
#pytest.mark.parametrize(
"a,b,expected",
[
pytest.param(1, 1, 2, id="t1"),
pytest.param(2, 2, 4, id="t2"),
pytest.param(2, 1, 3, id="t3"),
pytest.param(2, 3, 5, id="t4"),
pytest.param(3, 4, 7, id="t5"),
],
)
def test_addon(a, b, expected):
print(f"{a}, {b}, {expected}")
assert a + b == expected
Update:
Thanks to #Teejay, it is true that in cases like these we should wrap the hook. however, even if we change all the above hooks to generators like below, we still won't get any output except from session and log start and finish hooks:
#pytest.hookimpl(hookwrapper=True)
def pytest_runtest_protocol(self, item, nextitem):
print("<<<<< protocol >>>>>>")
outcome = yield
# outcome.excinfo may be None or a (cls, val, tb) tuple
print(outcome.get_result())
neither will anything change if these methods were static:
#pytest.hookimpl(hookwrapper=True)
#staticmethod
def pytest_runtest_protocol(item, nextitem):
print("<<<<< protocol >>>>>>")
outcome = yield
# outcome.excinfo may be None or a (cls, val, tb) tuple
print(outcome.get_result())

Related

Capture assertion message in Pytest

I am trying to capture the return value of a PyTest. I am running these tests programmatically, and I want to return relevant information when the test fails.
I thought I could perhaps return the value of kernel as follows such that I can print that information later when listing failed tests:
def test_eval(test_input, expected):
kernel = os.system("uname -r")
assert eval(test_input) == expected, kernel
This doens't work. When I am later looping through the TestReports which are generated, there is no way to access any return information. The only information available in the TestReport is the name of the test and a True/False.
For example one of the test reports looks as follows:
<TestReport 'test_simulation.py::test_host_has_correct_kernel_version[simulation-host]' when='call' outcome='failed'>
Is there a way to return a value after the assert fails, back to the TestReport? I have tried doing this with PyTest plugins but have been unsuccessful.
Here is the code I am using to run the tests programmatically. You can see where I am trying to access the return value.
import pytest
from util import bcolors
class Plugin:
def __init__(self):
self.passed_tests = set()
self.skipped_tests = set()
self.failed_tests = set()
self.unknown_tests = set()
def pytest_runtest_logreport(self, report):
print(report)
if report.passed:
self.passed_tests.add(report)
elif report.skipped:
self.skipped_tests.add(report)
elif report.failed:
self.failed_tests.add(report)
else:
self.unknown_tests.add(report)
if __name__ == "__main__":
plugin = Plugin()
pytest.main(["-s", "-p", "no:terminal"], plugins=[plugin])
for passed in plugin.passed_tests:
result = passed.nodeid
print(bcolors.OKGREEN + "[OK]\t" + bcolors.ENDC + result)
for skipped in plugin.skipped_tests:
result = skipped.nodeid
print(bcolors.OKBLUE + "[SKIPPED]\t" + bcolors.ENDC + result)
for failed in plugin.failed_tests:
result = failed.nodeid
print(bcolors.FAIL + "[FAIL]\t" + bcolors.ENDC + result)
for unknown in plugin.unknown_tests:
result = unknown.nodeid
print(bcolors.FAIL + "[FAIL]\t" + bcolors.ENDC + result)
The goal is to be able to print out "extra context information" when printing the FAILED tests, so that there is information immediately available to help debug why the test is failing.
You can extract failure details from the raised AssertionError in the custom pytest_exception_interact hookimpl. Example:
# conftest.py
def pytest_exception_interact(node, call, report):
# assertion message should be parsed here
# because pytest rewrites assert statements in bytecode
message = call.excinfo.value.args[0]
lines = message.split()
kernel = lines[0]
report.sections.append((
'Kernels reported in assert failures:',
f'{report.nodeid} reported {kernel}'
))
Running a test module
import subprocess
def test_bacon():
assert True
def test_eggs():
kernel = subprocess.run(
["uname", "-r"],
stdout=subprocess.PIPE,
text=True
).stdout
assert 0 == 1, kernel
yields:
test_spam.py::test_bacon PASSED [ 50%]
test_spam.py::test_eggs FAILED [100%]
=================================== FAILURES ===================================
__________________________________ test_eggs ___________________________________
def test_eggs():
kernel = subprocess.run(
["uname", "-r"],
stdout=subprocess.PIPE,
text=True
).stdout
> assert 0 == 1, kernel
E AssertionError: 5.5.15-200.fc31.x86_64
E
E assert 0 == 1
E +0
E -1
test_spam.py:12: AssertionError
--------------------- Kernels reported in assert failures: ---------------------
test_spam.py::test_eggs reported 5.5.15-200.fc31.x86_64
=========================== short test summary info ============================
FAILED test_spam.py::test_eggs - AssertionError: 5.5.15-200.fc31.x86_64
========================= 1 failed, 1 passed in 0.05s ==========================

Python3 get process base-address from PID

I am trying to get the base-address of a process in Windows (64-bit), with Python3, assuming to know the PID.
I looked over all questions here on stack, but the solutions are old/not working.
I assume to have the PID of the process in a variable called pid.
One of the many pieces of code I tried is
PROCESS_ALL_ACCESS = 0x1F0FFF
processHandle = win32api.OpenProcess(PROCESS_ALL_ACCESS, False, pid)
modules = win32process.EnumProcessModules(processHandle)
fileName = win32process.GetModuleFileNameEx(processHandle, modules[0])
base_address = win32api.GetModuleHandle(fileName)
processHandle.close()
But I get error on GetModuleHandle: 'Impossible to find the specified module'.
Thank you for the help.
According to [MS.Docs]: GetModuleHandleW function (emphasis is mine):
Retrieves a module handle for the specified module. The module must have been loaded by the calling process.
That means that it will work fine for the current process, but for any other one you'd get Undefined Behavior, because you try retrieving:
The .dll (or .exe) name from the other process (GetModuleFileNameEx call)
The handle for the name at previous step (GetModuleHandle call) but in the current process (if loaded), which makes no sense
Although there's no clear documentation on this topic (or at least I couldn't find any), the handle is the base address. This is a principle that you also rely on (calling GetModuleHandle), but you can use the values returned by EnumProcessModules directly (look at the example below, the values are the same).
If you want to be rigorous, you could use [MS.Docs]: GetModuleInformation function. Unfortunately, that's not exported by PyWin32, and an alternative is using [Python 3.Docs]: ctypes - A foreign function library for Python.
code00.py:
#!/usr/bin/env python3
import sys
import win32api as wapi
import win32process as wproc
import win32con as wcon
import ctypes as ct
from ctypes import wintypes as wt
import traceback as tb
class MODULEINFO(ct.Structure):
_fields_ = [
("lpBaseOfDll", ct.c_void_p),
("SizeOfImage", wt.DWORD),
("EntryPoint", ct.c_void_p),
]
get_module_information_func_name = "GetModuleInformation"
GetModuleInformation = getattr(ct.WinDLL("kernel32"), get_module_information_func_name, getattr(ct.WinDLL("psapi"), get_module_information_func_name))
GetModuleInformation.argtypes = [wt.HANDLE, wt.HMODULE, ct.POINTER(MODULEINFO)]
GetModuleInformation.restype = wt.BOOL
def get_base_address_original(process_handle, module_handle):
module_file_name = wproc.GetModuleFileNameEx(process_handle, module_handle)
print(" File for module {0:d}: {1:s}".format(module_handle, module_file_name))
module_base_address = wapi.GetModuleHandle(module_file_name)
return module_base_address
def get_base_address_new(process_handle, module_handle):
module_info = MODULEINFO()
res = GetModuleInformation(process_handle.handle, module_handle, ct.byref(module_info))
print(" Result: {0:}, Base: {1:d}, Size: {2:d}".format(res, module_info.lpBaseOfDll, module_info.SizeOfImage))
if not res:
print(" {0:s} failed: {1:d}".format(get_module_information_func_name, getattr(ct.WinDLL("kernel32"), "GetLastError")()))
return module_info.lpBaseOfDll
def main(*argv):
pid = int(argv[0]) if argv and argv[0].isdecimal() else wapi.GetCurrentProcessId()
print("Working on pid {0:d}".format(pid))
process_handle = wapi.OpenProcess(wcon.PROCESS_ALL_ACCESS, False, pid)
print("Process handle: {0:d}".format(process_handle.handle))
module_handles = wproc.EnumProcessModules(process_handle)
print("Loaded modules: {0:}".format(module_handles))
module_index = 0 # 0 - the executable itself
module_handle = module_handles[module_index]
get_base_address_funcs = [
#get_base_address_original, # Original behavior moved in a function
get_base_address_new,
]
for get_base_address in get_base_address_funcs:
print("\nAttempting {0:s}".format(get_base_address.__name__))
try:
module_base_address = get_base_address(process_handle, module_handle)
print(" Base address: 0x{0:016X} ({1:d})".format(module_base_address, module_base_address))
except:
tb.print_exc()
process_handle.close()
#input("\nPress ENTER to exit> ")
if __name__ == "__main__":
print("Python {0:s} {1:d}bit on {2:s}\n".format(" ".join(item.strip() for item in sys.version.split("\n")), 64 if sys.maxsize > 0x100000000 else 32, sys.platform))
main(*sys.argv[1:])
print("\nDone.")
Output:
e:\Work\Dev\StackOverflow\q059610466>"e:\Work\Dev\VEnvs\py_pc064_03.07.06_test0\Scripts\python.exe" code00.py
Python 3.7.6 (tags/v3.7.6:43364a7ae0, Dec 19 2019, 00:42:30) [MSC v.1916 64 bit (AMD64)] 64bit on win32
Working on pid 59608
Process handle: 452
Loaded modules: (140696816713728, 140714582343680, 140714572513280, 140714535354368, 140714547544064, 140713592946688, 140714443341824, 140714557898752, 140714556325888, 140714550362112, 140714414964736, 140714562486272, 140714532798464, 140714555473920, 140714548592640, 140714533322752, 140714531946496, 140714553769984, 140714555670528, 140714558750720, 140714581426176, 140714556129280, 140714546036736, 140714518052864, 140714532601856, 140714524737536, 140714210361344, 1797128192, 140714574151680, 140714535026688, 140714557046784, 140714538172416, 140714531291136, 140714530963456, 140714530766848, 140714530832384, 1796931584, 140714561044480, 140714573299712, 140714215014400, 140714529849344, 1798438912, 140714559995904, 140714167042048)
Attempting get_base_address_new
Result: 1, Base: 140696816713728, Size: 110592
Base address: 0x00007FF687C80000 (140696816713728)
Done.
e:\Work\Dev\StackOverflow\q059610466>:: Attempting to run with Task Manager pid
e:\Work\Dev\StackOverflow\q059610466>"e:\Work\Dev\VEnvs\py_pc064_03.07.06_test0\Scripts\python.exe" code00.py 22784
Python 3.7.6 (tags/v3.7.6:43364a7ae0, Dec 19 2019, 00:42:30) [MSC v.1916 64 bit (AMD64)] 64bit on win32
Working on pid 22784
Process handle: 480
Loaded modules: (140699900903424, 140714582343680, 140714572513280, 140714535354368, 140714547544064, 140714573299712, 140714531946496, 140714550362112, 140714562486272, 140714532798464, 140714530963456, 140714530766848, 140714556981248, 140714557898752, 140714556325888, 140714555473920, 140714365222912, 140714548592640, 140714496753664, 140714533322752, 140714553769984, 140714574151680, 140714535026688, 140714557046784, 140714538172416, 140714581426176, 140714558750720, 140714531291136, 140714530832384, 140714546036736, 140714444521472, 140714567467008, 140714532601856, 140714468966400, 140714452385792, 140714267115520, 140714510843904, 140714478731264, 140713698263040, 140714510254080, 140714556129280, 140714565435392, 140714110091264, 140714491379712, 140714455007232, 140714514382848, 140714459529216, 140714281140224, 140714370859008, 140714471260160, 140714566746112, 140713839362048, 140714555670528, 140714171695104, 140714508615680, 140714514841600, 140714029154304, 140714036625408, 140714329636864, 140714447011840, 140714434691072, 140714470866944, 140714561044480, 140714520870912, 140714469883904, 140714494787584, 140714293592064, 140713999335424, 140714400743424, 140714497605632, 140714502193152, 140714197254144, 140714415030272, 140714035576832, 140714065854464, 140714513006592, 140714529652736, 140714512809984, 140714495049728, 140714038657024, 140714371448832, 140714421911552, 140714325966848, 140714196074496, 140714057924608, 140714058317824, 140714064281600, 140714058121216, 140714519756800, 140714327539712, 140714311614464, 140714501079040, 140714546167808, 140714531422208, 140714531553280, 140714557767680, 140714518052864, 140714524737536, 140714167631872, 140714528669696, 140714331865088, 140714310369280, 140714310238208, 140714520018944, 140714458939392, 2018133999616, 140714401988608, 2018141863936, 140714514644992, 140714454810624, 140714294640640)
Attempting get_base_address_new
Result: 1, Base: 140699900903424, Size: 1105920
Base address: 0x00007FF73F9D0000 (140699900903424)
Done.
Update #0
According to [MS.Docs]: MODULEINFO structure (Remarks section, emphasis still mine):
The load address of a module is the same as the HMODULE value.
So, things seem to be pretty straightforward.
code01.py:
#!/usr/bin/env python3
import sys
import win32api as wapi
import win32process as wproc
import win32con as wcon
def main(*argv):
pid = int(argv[0]) if argv and argv[0].isdecimal() else wapi.GetCurrentProcessId()
print("Working on pid {0:d}".format(pid))
process_handle = wapi.OpenProcess(wcon.PROCESS_ALL_ACCESS, False, pid)
print(" Process handle: {0:d}".format(process_handle.handle))
module_handles = wproc.EnumProcessModules(process_handle)
module_handles_count = len(module_handles)
print(" Loaded modules count: {0:d}".format(module_handles_count))
module_index = 0 # 0 - the executable itself
if module_index > module_handles_count:
module_index = 0
module_handle = module_handles[module_index]
module_file_name = wproc.GetModuleFileNameEx(process_handle, module_handle)
print(" File [{0:s}] (index {1:d}) is loaded at address 0x{2:016X} ({3:d})".format(module_file_name, module_index, module_handle, module_handle))
process_handle.close()
if __name__ == "__main__":
print("Python {0:s} {1:d}bit on {2:s}\n".format(" ".join(item.strip() for item in sys.version.split("\n")), 64 if sys.maxsize > 0x100000000 else 32, sys.platform))
main(*sys.argv[1:])
print("\nDone.")
Output:
e:\Work\Dev\StackOverflow\q059610466>"e:\Work\Dev\VEnvs\py_pc064_03.07.06_test0\Scripts\python.exe" code01.py
Python 3.7.6 (tags/v3.7.6:43364a7ae0, Dec 19 2019, 00:42:30) [MSC v.1916 64 bit (AMD64)] 64bit on win32
Working on pid 7184
Process handle: 456
Loaded modules count: 43
File [e:\Work\Dev\VEnvs\py_pc064_03.07.06_test0\Scripts\python.exe] (index 0) is loaded at address 0x00007FF687C80000 (140696816713728)
Done.
e:\Work\Dev\StackOverflow\q059610466>:: Attempting to run with Task Manager pid
e:\Work\Dev\StackOverflow\q059610466>"e:\Work\Dev\VEnvs\py_pc064_03.07.06_test0\Scripts\python.exe" code01.py 22784
Python 3.7.6 (tags/v3.7.6:43364a7ae0, Dec 19 2019, 00:42:30) [MSC v.1916 64 bit (AMD64)] 64bit on win32
Working on pid 22784
Process handle: 624
Loaded modules count: 111
File [C:\WINDOWS\system32\taskmgr.exe] (index 0) is loaded at address 0x00007FF73F9D0000 (140699900903424)
Done.

pytest-4.x.x: How to report SKIPPED tests like XFAILED?

When a test is xfailed the reason that is printed reports about test file, test class and test case, while the skipped test case reports only test file and a line where skip is called.
Here is a test example:
#!/usr/bin/env pytest
import pytest
#pytest.mark.xfail(reason="Reason of failure")
def test_1():
pytest.fail("This will fail here")
#pytest.mark.skip(reason="Reason of skipping")
def test_2():
pytest.fail("This will fail here")
This is the actual result:
pytest test_file.py -rsx
============================= test session starts =============================
platform linux -- Python 3.5.2, pytest-4.4.1, py-1.7.0, pluggy-0.9.0
rootdir: /home/ashot/questions
collected 2 items
test_file.py xs [100%]
=========================== short test summary info ===========================
SKIPPED [1] test_file.py:9: Reason of skipping
XFAIL test_file.py::test_1
Reason of failure
==================== 1 skipped, 1 xfailed in 0.05 seconds =====================
But I would expect to get something like:
pytest test_file.py -rsx
============================= test session starts =============================
platform linux -- Python 3.5.2, pytest-4.4.1, py-1.7.0, pluggy-0.9.0
rootdir: /home/ashot/questions
collected 2 items
test_file.py xs [100%]
=========================== short test summary info ===========================
XFAIL test_file.py::test_1: Reason of failure
SKIPPED test_file.py::test_2: Reason of skipping
==================== 1 skipped, 1 xfailed in 0.05 seconds =====================
You have two possible ways to achieve this. The quick and dirty way: just redefine _pytest.skipping.show_xfailed in your test_file.py:
import _pytest
def custom_show_xfailed(terminalreporter, lines):
xfailed = terminalreporter.stats.get("xfailed")
if xfailed:
for rep in xfailed:
pos = terminalreporter.config.cwd_relative_nodeid(rep.nodeid)
reason = rep.wasxfail
s = "XFAIL %s" % (pos,)
if reason:
s += ": " + str(reason)
lines.append(s)
# show_xfailed_bkp = _pytest.skipping.show_xfailed
_pytest.skipping.show_xfailed = custom_show_xfailed
... your tests
The (not so) clean way: create a conftest.py file in the same directory as your test_file.py, and add a hook:
import pytest
import _pytest
def custom_show_xfailed(terminalreporter, lines):
xfailed = terminalreporter.stats.get("xfailed")
if xfailed:
for rep in xfailed:
pos = terminalreporter.config.cwd_relative_nodeid(rep.nodeid)
reason = rep.wasxfail
s = "XFAIL %s" % (pos,)
if reason:
s += ": " + str(reason)
lines.append(s)
#pytest.hookimpl(tryfirst=True)
def pytest_terminal_summary(terminalreporter):
tr = terminalreporter
if not tr.reportchars:
return
lines = []
for char in tr.reportchars:
if char == "x":
custom_show_xfailed(terminalreporter, lines)
elif char == "X":
_pytest.skipping.show_xpassed(terminalreporter, lines)
elif char in "fF":
_pytest.skipping.show_simple(terminalreporter, lines, 'failed', "FAIL %s")
elif char in "sS":
_pytest.skipping.show_skipped(terminalreporter, lines)
elif char == "E":
_pytest.skipping.show_simple(terminalreporter, lines, 'error', "ERROR %s")
elif char == 'p':
_pytest.skipping.show_simple(terminalreporter, lines, 'passed', "PASSED %s")
if lines:
tr._tw.sep("=", "short test summary info")
for line in lines:
tr._tw.line(line)
tr.reportchars = [] # to avoid further output
The second method is overkill, because you have to redefine the whole pytest_terminal_summary.
Thanks to this answer I've found the following solution that works perfectly for me.
I've created conftest.py file in the root of my test suite with the following content:
import _pytest.skipping as s
def show_xfailed(tr, lines):
for rep in tr.stats.get("xfailed", []):
pos = tr.config.cwd_relative_nodeid(rep.nodeid)
reason = rep.wasxfail
s = "XFAIL\t%s" % pos
if reason:
s += ": " + str(reason)
lines.append(s)
s.REPORTCHAR_ACTIONS["x"] = show_xfailed
def show_skipped(tr, lines):
for rep in tr.stats.get("skipped", []):
pos = tr.config.cwd_relative_nodeid(rep.nodeid)
reason = rep.longrepr[-1]
if reason.startswith("Skipped: "):
reason = reason[9:]
verbose_word = s._get_report_str(tr.config, report=rep)
lines.append("%s\t%s: %s" % (verbose_word, pos, reason))
s.REPORTCHAR_ACTIONS["s"] = show_skipped
s.REPORTCHAR_ACTIONS["S"] = show_skipped
And now I'm getting to following output:
./test_file.py -rsx
============================= test session starts =============================
platform linux -- Python 3.5.2, pytest-4.4.1, py-1.7.0, pluggy-0.9.0
rootdir: /home/ashot/questions
collected 2 items
test_file.py xs [100%]
=========================== short test summary info ===========================
SKIPPED test_file.py::test_2: Reason of skipping
XFAIL test_file.py::test_1: Reason of failure
==================== 1 skipped, 1 xfailed in 0.05 seconds =====================

How to get a list of TestReports at the end of a py.test run?

I want to get a list of all tests (e.g. in the form of a py.test TestReport) at the end of all tests.
I know that pytest_runtest_makereportdoes something similar, but only for a single test. But I want to implement a hook or something in conftest.py to process the whole list of tests before the py.test application terminates.
Is there a way to do this?
Here an example which can help you. Structure of files:
/example:
__init__.py # empty file
/test_pack_1
__init__.py # empty file
conftest.py # pytest hooks
test_my.py # a few tests for demonstration
There are 2 tests in test_my.py:
def test_one():
assert 1 == 1
print('1==1')
def test_two():
assert 1 == 2
print('1!=2')
Example of conftest.py:
import pytest
from _pytest.runner import TestReport
from _pytest.terminal import TerminalReporter
#pytest.hookimpl(hookwrapper=True)
def pytest_terminal_summary(terminalreporter): # type: (TerminalReporter) -> generator
yield
# you can do here anything - I just print report info
print('*' * 8 + 'HERE CUSTOM LOGIC' + '*' * 8)
for failed in terminalreporter.stats.get('failed', []): # type: TestReport
print('failed! node_id:%s, duration: %s, details: %s' % (failed.nodeid,
failed.duration,
str(failed.longrepr)))
for passed in terminalreporter.stats.get('passed', []): # type: TestReport
print('passed! node_id:%s, duration: %s, details: %s' % (passed.nodeid,
passed.duration,
str(passed.longrepr)))
Documentation says that pytest_terminal_summary has exitstatus arg
Run tests without any additional options: py.test ./example. Example of output:
example/test_pack_1/test_my.py .F
********HERE CUSTOM LOGIC********
failed! node_id:test_pack_1/test_my.py::test_two, duration: 0.000385999679565, details: def test_two():
> assert 1 == 2
E assert 1 == 2
example/test_pack_1/test_my.py:7: AssertionError
passed! node_id:test_pack_1/test_my.py::test_one, duration: 0.00019907951355, details: None
=================================== FAILURES ===================================
___________________________________ test_two ___________________________________
def test_two():
> assert 1 == 2
E assert 1 == 2
example/test_pack_1/test_my.py:7: AssertionError
====================== 1 failed, 1 passed in 0.01 seconds ======================
Hope this helps.
Note! Make sure that .pyc files was removed before running tests

stdout and stderror from fabric task wrapped in celery

im running a simple task, triggered from a django view:
task = mock_deploy.delay()
mock_deploy is defined as:
from celery.decorators import task as ctask
from project.fabscripts.task.mock import *
#ctask(name="mock_deploy")
def mock_deploy():
print "hi form celery task b4 mockdeploy 1234"
output = execute(mock_deploy2)
return "out: %s" % (output)
And the fabric task itself is defined as:
#task
def mock_deploy2():
lrun("ls -l /")
lrun("ifconfig eth0")
# i need to get the full output from those commands and save them to db
And now... I was trying to substitute stdout, overwriting fabric execute function:
def execute(task):
output = StringIO()
error = StringIO()
sys.stdout = output
sys.stderr = error
task()
sys.stdout = sys.__stdout__
sys.stderr = sys.__stderr__
return (output.getvalue(), error.getvalue())
And I was trying to substitute stdout within fabric task. No matter what i did, the only output i was getting was a first line of "what fabric wants to do"
out: [localhost] local: ls -l /
Then, the whole output of the ls command was printed perfectly fine in celery log. Except for the missing one line of out: [localhost] local: ls -l / `9the one i managed to get as output)
[2012-06-14 21:33:56,587: DEBUG/MainProcess] TaskPool: Apply <function execute_and_trace at 0x36710c8> (args:('mock_deploy', '2a90d920-130a-4942-829b-87f4d5ebe80f', [], {}) kwargs:{'hostname': 's16079364', 'request': {'retries': 0, 'task': 'mock_deploy', 'utc': False, 'loglevel': 10, 'delivery_info': {'routing_key': u'celery', 'exchange': u'celery'}, 'args': [], 'expires': None, 'is_eager': False, 'eta': None, 'hostname': 's16079364', 'kwargs': {}, 'logfile': None, 'id': '2a90d920-130a-4942-829b-87f4d5ebe80f'}})
[2012-06-14 21:33:56,591: DEBUG/MainProcess] Task accepted: mock_deploy[2a90d920-130a-4942-829b-87f4d5ebe80f] pid:22214
hi form celery task b4 mockdeploy 1234
total 3231728
-rw-r--r-- 1 root root 3305551148 2012-06-13 14:43 dumpling.sql
drwxr-xr-x 2 root root 4096 2012-05-09 17:42 bin
drwxr-xr-x 4 root root 4096 2012-02-14 15:21 boot
drwxr-xr-x 2 root root 4096 2012-03-09 14:10 build
drwxr-xr-x 2 root root 4096 2010-05-11 19:58 cdrom
-rw------- 1 root root 2174976 2012-05-23 11:23 core
drwxr-xr-x 15 root root 4080 2012-06-11 12:55 dev
drwxr-xr-x 135 root root 12288 2012-06-14 21:15 etc
drwxr-xr-x 6 root root 77 2012-05-21 14:41 home
...
A horrible horrible workaround is wrapping up a fabric run command to add a "> /tmp/logfile.log" on each command, then when the task is finished ill retrieve the file with scp...
My question in short is how do i get the full output of a fabric task when its triggered with celery?
The following did the trick:
#ctask(name="mock_deploy")
def mock_deploy():
env.roledefs.update({'remote': ['root#1.1.1.1',]})
output = StringIO()
sys.stdout = output
execute(mock_deploy2)
sys.stdout = sys.__stdout__
return output.getvalue()

Categories

Resources