|
9 | 9 | _TYPE_MAP, |
10 | 10 | wrap_com_object, |
11 | 11 | ) |
| 12 | +from sapsucker._wrap import com_collection_item |
12 | 13 | from sapsucker.components.base import GuiComponent |
13 | 14 | from sapsucker.components.button import GuiButton |
14 | 15 | from sapsucker.components.field import GuiTextField |
@@ -94,3 +95,98 @@ def test_shell_subtype_entry(self, sub_type, cls): |
94 | 95 | com = make_mock_com(type_as_number=122, type_name="GuiShell", SubType=sub_type) |
95 | 96 | result = wrap_com_object(com) |
96 | 97 | assert isinstance(result, cls) |
| 98 | + |
| 99 | + |
| 100 | +class _FakeComError(Exception): |
| 101 | + """Stand-in for pywintypes.com_error carrying SAP GUI error 618. |
| 102 | +
|
| 103 | + Mirrors the real payload observed on the failing host (sapgui.mcp#804): |
| 104 | + ``(-2147352567, 'Exception occurred.', (618, 'saplogon', |
| 105 | + 'Bad index type for collection access.', None, 0, 0), None)``. |
| 106 | + """ |
| 107 | + |
| 108 | + |
| 109 | +_BAD_INDEX_ARGS = ( |
| 110 | + -2147352567, |
| 111 | + "Exception occurred.", |
| 112 | + (618, "saplogon", "Bad index type for collection access.", None, 0, 0), |
| 113 | + None, |
| 114 | +) |
| 115 | + |
| 116 | + |
| 117 | +class _FakeCollection: |
| 118 | + """Fake SAP GUI COM collection with independently-toggleable accessors. |
| 119 | +
|
| 120 | + Models the exact quirk from sapgui.mcp#804: ``Item(<int>)`` can raise COM |
| 121 | + error 618 while ``ElementAt(<int>)`` and the default member ``collection(<int>)`` |
| 122 | + return the element fine. |
| 123 | + """ |
| 124 | + |
| 125 | + def __init__(self, items, *, item_ok=True, element_at_ok=True, default_ok=True, has_element_at=True): |
| 126 | + self._items = items |
| 127 | + self._item_ok = item_ok |
| 128 | + self._element_at_ok = element_at_ok |
| 129 | + self._default_ok = default_ok |
| 130 | + self._has_element_at = has_element_at |
| 131 | + self.Count = len(items) |
| 132 | + |
| 133 | + def Item(self, index): |
| 134 | + if not self._item_ok: |
| 135 | + raise _FakeComError(*_BAD_INDEX_ARGS) |
| 136 | + return self._items[index] |
| 137 | + |
| 138 | + def __call__(self, index): # default member: collection(index) |
| 139 | + if not self._default_ok: |
| 140 | + raise _FakeComError(*_BAD_INDEX_ARGS) |
| 141 | + return self._items[index] |
| 142 | + |
| 143 | + def __getattr__(self, name): |
| 144 | + # Only synthesise ElementAt; everything else is a genuine miss so that |
| 145 | + # ``getattr(col, "ElementAt", None)`` returns None when absent. |
| 146 | + if name == "ElementAt" and self.__dict__.get("_has_element_at", False): |
| 147 | + |
| 148 | + def _element_at(index): |
| 149 | + if not self._element_at_ok: |
| 150 | + raise _FakeComError(*_BAD_INDEX_ARGS) |
| 151 | + return self._items[index] |
| 152 | + |
| 153 | + return _element_at |
| 154 | + raise AttributeError(name) |
| 155 | + |
| 156 | + |
| 157 | +class TestComCollectionItem: |
| 158 | + """Tests for com_collection_item() — the SAP GUI error 618 workaround (mcp#804).""" |
| 159 | + |
| 160 | + def test_uses_item_when_it_works(self): |
| 161 | + """When Item(index) succeeds it is used and ElementAt is never consulted.""" |
| 162 | + items = ["a", "b", "c"] |
| 163 | + # ElementAt would raise if reached — proves it is not reached. |
| 164 | + col = _FakeCollection(items, item_ok=True, element_at_ok=False) |
| 165 | + assert com_collection_item(col, 1) == "b" |
| 166 | + |
| 167 | + def test_falls_back_to_element_at_on_bad_index(self): |
| 168 | + """Item raising 618 falls back to ElementAt(index).""" |
| 169 | + items = ["a", "b", "c"] |
| 170 | + col = _FakeCollection(items, item_ok=False, element_at_ok=True) |
| 171 | + assert com_collection_item(col, 2) == "c" |
| 172 | + |
| 173 | + def test_falls_back_to_default_member_when_item_and_element_at_fail(self): |
| 174 | + """Item and ElementAt both raising falls back to the default member call.""" |
| 175 | + items = ["a", "b", "c"] |
| 176 | + col = _FakeCollection(items, item_ok=False, element_at_ok=False, default_ok=True) |
| 177 | + assert com_collection_item(col, 0) == "a" |
| 178 | + |
| 179 | + def test_falls_back_to_default_member_when_element_at_absent(self): |
| 180 | + """A collection without ElementAt falls straight to the default member.""" |
| 181 | + items = ["a", "b"] |
| 182 | + col = _FakeCollection(items, item_ok=False, has_element_at=False, default_ok=True) |
| 183 | + assert com_collection_item(col, 1) == "b" |
| 184 | + |
| 185 | + def test_reraises_original_item_error_when_all_strategies_fail(self): |
| 186 | + """If every access strategy fails, the ORIGINAL Item error is re-raised.""" |
| 187 | + col = _FakeCollection(["a"], item_ok=False, element_at_ok=False, default_ok=False) |
| 188 | + with pytest.raises(_FakeComError) as exc_info: |
| 189 | + com_collection_item(col, 0) |
| 190 | + # The re-raised error is the one from Item (carries the 618 payload), not a |
| 191 | + # masked/replaced exception from a later fallback. |
| 192 | + assert exc_info.value.args[2][0] == 618 |
0 commit comments