diff --git a/examples/pytest_attributes.py b/examples/pytest_attributes.py new file mode 100644 index 0000000..55f6679 --- /dev/null +++ b/examples/pytest_attributes.py @@ -0,0 +1,46 @@ +""" +Test the ways to skip a test using `pytest` + +- Skip Mark +- SkipIf Mark +- Skip Exception +- Xfail Mark (Expected Failure) +""" + +import pytest +import unittest + + +@pytest.mark.skip(reason="some reason") +def test_skip_mark(): + assert False + + +@unittest.expectedFailure +@pytest.mark.skipIf(False, reason="some reason") +def test_skipif_false(): + assert False + + +@pytest.mark.skipIf(True, reason="i don't know") +def test_skipif_true(): + assert False + + +def test_skip_throw(): + pytest.skip("it will throw Skipped") + + +@pytest.mark.xfail +def test_should_fail(): + assert False + + +@pytest.mark.xfail(True) +def test_should_fail_on_condition(): + assert False + + +@pytest.mark.xfail(False) +def test_should_fail_on_condition_ignored(): + assert True diff --git a/src/python.rs b/src/python.rs index 6f24c38..fb076f3 100644 --- a/src/python.rs +++ b/src/python.rs @@ -237,6 +237,11 @@ impl PyObject { unsafe { ffi::PyCode_Check(self.as_ptr()) == 1 } } + /// Assume is a tuple, and get the size of the tuple + pub fn tuple_size(&self) -> isize { + unsafe { ffi::PyTuple_Size(self.as_ptr()) } + } + /// Assume is a tuple, and get the item at the given index pub fn get_tuple_item(&self, index: isize) -> PyObject { let result = unsafe { ffi::PyTuple_GetItem(self.as_ptr(), index) }; @@ -244,6 +249,16 @@ impl PyObject { PyObject(pointer) } + + /// Assume is a dict, and get the item with the given key + pub fn get_dict_item(&self, key: &CStr) -> Option { + let result = unsafe { ffi::PyDict_GetItemString(self.as_ptr(), key.as_ptr()) }; + + if result.is_null() { + None + } else { + Some(PyObject::new(result).unwrap()) + } } /// Assume is a Long, and get the value @@ -256,6 +271,11 @@ impl PyObject { .try_into() .unwrap() } + + /// Is the object truthy? + pub fn is_truthy(&self) -> bool { + unsafe { ffi::PyObject_IsTrue(self.as_ptr()) == 1 } + } } impl fmt::Display for PyObject { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { diff --git a/src/run.rs b/src/run.rs index 9bd846d..c204734 100644 --- a/src/run.rs +++ b/src/run.rs @@ -163,6 +163,7 @@ fn test_function(test: &Test) -> OutcomeKind { fn has_skip_annotation(object: &PyObject) -> Option { const SKIP_ATTRIBUTE: &CStr = c"__unittest_skip__"; const SKIP_REASON_ATTRIBUTE: &CStr = c"__unittest_skip_why__"; + const PYTEST_MARK: &CStr = c"pytestmark"; if object.has_truthy_attr(SKIP_ATTRIBUTE) { let reason = object @@ -170,16 +171,69 @@ fn has_skip_annotation(object: &PyObject) -> Option { .map(|x| x.to_string()) .unwrap_or_default(); - Some(reason) - } else { - None + return Some(reason); } + + if object.has_attr(PYTEST_MARK) { + let pytest_marks = object.get_attr_cstr(PYTEST_MARK).unwrap(); + + for mark in pytest_marks.into_iter() { + let mark_name = mark.get_attr_cstr(c"name").unwrap().to_string(); + + let should_skip = match mark_name.as_str() { + "skip" => true, + "skipIf" => mark + .get_attr_cstr(c"args") + .is_ok_and(|args| args.get_tuple_item(0).is_truthy()), + _ => false, + }; + + if should_skip { + if let Ok(kwargs) = mark.get_attr_cstr(c"kwargs") { + return Some( + kwargs + .get_dict_item(c"reason") + .map(|reason| reason.to_string()) + .unwrap_or_default(), + ); + } + } + } + } + + None } /// Checks a [`PyObject`] for the annotation for expecting a failure fn is_expecting_failure(object: &PyObject) -> bool { const EXPECT_ERROR_ATTRIBUTE: &CStr = c"__unittest_expecting_failure__"; - object.has_truthy_attr(EXPECT_ERROR_ATTRIBUTE) + const PYTEST_MARK: &CStr = c"pytestmark"; + + if object.has_truthy_attr(EXPECT_ERROR_ATTRIBUTE) { + return true; + } + + if object.has_attr(PYTEST_MARK) { + let pytest_marks = object.get_attr_cstr(PYTEST_MARK).unwrap(); + + for mark in pytest_marks.into_iter() { + let mark_name = mark.get_attr_cstr(c"name").unwrap().to_string(); + + if mark_name == "xfail" { + let args = mark.get_attr_cstr(c"args").unwrap(); + + if args.tuple_size() > 0 { + let condition = args.get_tuple_item(0); + + return condition.is_truthy(); + } else { + return true; + }; + } + } + } + + false } fn call_optional_method(object: &PyObject, method: &CStr) -> Result<(), Error> {