diff --git a/comtypes/automation.py b/comtypes/automation.py index 8552fca16..e0d626468 100644 --- a/comtypes/automation.py +++ b/comtypes/automation.py @@ -29,6 +29,38 @@ _CArgObject = type(byref(c_int())) +# ============================================================ +# Fixed-point number (VT_CY / VT_DECIMAL) marshaling helpers +# ============================================================ + +# CY range constants +_CY_BITS = 64 +_LLONG_MIN = -(1 << (_CY_BITS - 1)) +_LLONG_MAX = (1 << (_CY_BITS - 1)) - 1 +_CY_SCALE = decimal.Decimal("10000") +_CY_MIN = decimal.Decimal(_LLONG_MIN) / _CY_SCALE +_CY_MAX = decimal.Decimal(_LLONG_MAX) / _CY_SCALE + +# DECIMAL limits +_DECIMAL_MAX_SCALE = 28 +_DECIMAL_MAX_DIGITS = 96 # 96 bits = max 28 decimal digits (2**96-1 has 29 digits, but we use 96-bit mantissa) +_DECIMAL_MAX_MANTISSA = (1 << 96) - 1 + + +def _coerce_decimal(value: Any, context: str) -> decimal.Decimal: + """Coerce a value to Decimal for VARIANT marshaling. + + Accepts only Decimal. Rejects int, float, bool, NaN, and Infinity. + """ + if not isinstance(value, decimal.Decimal): + raise TypeError(f"{context}: only Decimal is accepted") + if value.is_nan(): + raise ValueError(f"{context}: NaN cannot be converted to VARIANT") + if value.is_infinite(): + raise OverflowError(f"{context}: Infinity cannot be converted to VARIANT") + return value + + LCID = DWORD DISPID = LONG SCODE = LONG @@ -111,6 +143,37 @@ class tagCY(Structure): _fields_ = [("int64", c_longlong)] + def as_decimal(self) -> decimal.Decimal: + """Convert a tagCY struct to Decimal with scale 4 (10000).""" + return decimal.Decimal(self.int64) / _CY_SCALE + + def __repr__(self) -> str: + return f"tagCY(int64={self.int64})" + + @classmethod + def from_decimal(cls, d: decimal.Decimal) -> "hints.Self": + """Create a tagCY from a Decimal. + + The value is multiplied by 10000 and must be exactly representable + as an integer (no fractional part after scaling). Raises OverflowError + if the value has more than 4 decimal places, or if the value is + outside the int64 range. + """ + d = _coerce_decimal(d, "tagCY.from_decimal") + # Use localcontext with high precision to avoid context-dependent rounding + with decimal.localcontext(decimal.Context(prec=decimal.MAX_PREC)): + scaled = d * _CY_SCALE + # Check if the scaled value has a fractional part + if scaled != scaled.to_integral_value(): + raise OverflowError( + f"Value {d} has more than 4 decimal places, cannot convert to VT_CY exactly. " + f"Round explicitly before calling." + ) + n = int(scaled) + if not _LLONG_MIN <= n <= _LLONG_MAX: + raise OverflowError(f"Value {d} out of range for VT_CY") + return cls(n) + CY = tagCY CURRENCY = CY @@ -136,6 +199,92 @@ def as_decimal(self) -> decimal.Decimal: sign = "-" if self.sign else "" return decimal.Decimal(f"{sign}{digits}e-{self.scale}") + def __repr__(self) -> str: + return f"tagDEC(scale={self.scale}, sign={self.sign}, Hi32={self.Hi32}, Lo64={self.Lo64})" + + @classmethod + def from_decimal(cls, d: decimal.Decimal) -> "hints.Self": + """Create a tagDEC from a Decimal. + + The scale is chosen as min(max(-exponent, 0), 28) for zero values, + and min(-exponent, 28) for non-zero values. Trailing zeros in the + fractional part are stripped only when they would cause the scale + to exceed 28 (i.e., when exponent < -28). This allows values like + 1.230000... (many trailing zeros) to be accepted by reducing scale. + + Returns a tagDEC instance. + + Raises: + TypeError: If d is not a Decimal. + ValueError: If d is NaN. + OverflowError: If: + - d is Infinity. + - The value has more than 28 non-zero decimal places (exceeds + VT_DECIMAL maximum scale). + - The value exceeds the 96-bit mantissa (≈7.9e28) at the + required scale. + """ + d = _coerce_decimal(d, "tagDEC.from_decimal") + sign, digits, exp = d.as_tuple() + coef = int("".join(map(str, digits))) if digits else 0 + + # Handle zero specially - preserve scale and sign for round-trip fidelity + if coef == 0: + # Zero: scale = min(max(-exp, 0), 28) to preserve trailing zeros + scale = min(max(-exp, 0), _DECIMAL_MAX_SCALE) + scaled_coef = 0 + else: + # Strip trailing zeros from coefficient ONLY when they would cause + # scale to exceed 28 (i.e., when exp < -28). This allows values like + # 1.230000... (many trailing zeros) to be accepted by reducing scale. + while exp < -_DECIMAL_MAX_SCALE and coef % 10 == 0: + coef //= 10 + exp += 1 + + # Check for huge positive exponent that would cause huge integer allocation + # 96-bit mantissa max is 2**96-1 ≈ 7.9e28, so 29+ digits exceeds it. + # Use 29 explicitly (not _DECIMAL_MAX_SCALE + 1) to avoid confusion. + if d.adjusted() >= 29: + raise OverflowError( + f"Value {d} exceeds 96-bit mantissa. " + f"Reduce precision explicitly before calling." + ) + + # Start with scale = min(-exp, 28) + scale = min(-exp, _DECIMAL_MAX_SCALE) if exp < 0 else 0 + + # If the value has more decimal places than max scale, it cannot be + # represented exactly in VT_DECIMAL. Require explicit rounding by caller. + if exp < -_DECIMAL_MAX_SCALE: + raise OverflowError( + f"Value {d} has more than {_DECIMAL_MAX_SCALE} decimal places, " + f"cannot convert to VT_DECIMAL exactly. Round explicitly before calling." + ) + + # After the check above, we know exp >= -28 (or exp >= 0). + # For exp >= 0: scale=0, multiply by 10**exp. + # For exp < 0: scale=-exp, use coef directly (no scaling needed). + if exp >= 0: + scaled_coef = coef * 10**exp + else: + scaled_coef = coef + + # Check if it fits in 96 bits at the required scale + if scaled_coef > _DECIMAL_MAX_MANTISSA: + raise OverflowError( + f"Value {d} exceeds 96-bit mantissa at scale {scale}. " + f"Reduce precision explicitly before calling." + ) + + # Build the tagDEC + dec = cls() + dec.wReserved = 0 + dec.scale = scale + dec.sign = 0x80 if sign else 0x00 + dec.Hi32 = (scaled_coef >> 64) & 0xFFFFFFFF + dec.Lo64 = scaled_coef & 0xFFFFFFFFFFFFFFFF + return dec + DECIMAL = tagDEC @@ -173,7 +322,7 @@ class _tagBRECORD(Structure): ("scode", SCODE), ("VT_R4", c_float), ("VT_R8", c_double), - ("VT_CY", c_longlong), + ("VT_CY", tagCY), ("c_wchar_p", c_wchar_p), ("c_void_p", c_void_p), ("pparray", POINTER(POINTER(_safearray.tagSAFEARRAY))), @@ -304,8 +453,18 @@ def _set_value(self, value): com_days /= comtypes.npsupport.numpy.timedelta64(1, "D") self.vt = VT_DATE self._.VT_R8 = com_days - elif decimal is not None and isinstance(value, decimal.Decimal): - self._.VT_CY = int(round(value * 10000)) + elif isinstance(value, decimal.Decimal): + # TODO: Use `self._.VT_CY = tagCY.from_decimal(value)` directly. + # Currently writing via `VT_I8` because `VT_CY` is now a `tagCY` + # struct in the union. This is correct since `VT_I8` and `VT_CY` + # share the same memory in the union. + # + # Direct assignment to `VT_CY` would require constructing a + # `tagCY` instance. This is avoided because `tagCY.from_decimal()` + # raises `OverflowError` for values outside the CY range or with + # more than 4 decimal places, which would break user code that + # implicitly relied on rounding behavior. + self._.VT_I8 = int(round(value * _CY_SCALE)) self.vt = VT_CY elif isinstance(value, POINTER(IDispatch)): CopyComPointer(value, byref(self._)) @@ -471,7 +630,7 @@ def _get_value(self, dynamic=False): days = self._.VT_R8 return datetime.timedelta(days=days) + _com_null_date elif vt == VT_CY: - return self._.VT_CY / decimal.Decimal("10000") + return self._.VT_CY.as_decimal() elif vt == VT_UNKNOWN: val = self._.c_void_p if not val: @@ -1029,6 +1188,8 @@ def Invoke(self, dispid: int, *args: Any, **kw: Any) -> Any: BSTR: VT_BSTR, HRESULT: VT_HRESULT, VARIANT: VT_VARIANT, + tagCY: VT_CY, + tagDEC: VT_DECIMAL, # SAFEARRAY(VARIANT *) # # It is unlear to me if this is allowed or not. Apparently there diff --git a/comtypes/safearray.py b/comtypes/safearray.py index 291af6724..3db798bda 100644 --- a/comtypes/safearray.py +++ b/comtypes/safearray.py @@ -298,7 +298,7 @@ def unpack(self): def _get_elements_raw(self, num_elements): """Returns a flat list or ndarray containing ALL elements in the safearray.""" - from comtypes.automation import VARIANT + from comtypes.automation import VARIANT, tagCY, tagDEC # XXX Not sure this is true: # For VT_UNKNOWN and VT_DISPATCH, we should retrieve the @@ -327,6 +327,12 @@ def _get_elements_raw(self, num_elements): # return a NULL-interface pointer. result.append(POINTER(itf)()) return result + elif self._itemtype_ == tagCY: + # Convert tagCY to Decimal for easier use + return [x.as_decimal() for x in ptr[:num_elements]] + elif self._itemtype_ == tagDEC: + # Convert tagDEC to Decimal for easier use + return [x.as_decimal() for x in ptr[:num_elements]] else: # If the safearray element are NOT native python # objects, the containing safearray must be kept diff --git a/comtypes/test/test_fixed_point_marshaling.py b/comtypes/test/test_fixed_point_marshaling.py new file mode 100644 index 000000000..712c392f9 --- /dev/null +++ b/comtypes/test/test_fixed_point_marshaling.py @@ -0,0 +1,409 @@ +"""Unit tests for fixed-point number (VT_CY / VT_DECIMAL) marshaling helpers. + +These tests do not require a COM server and test the tagCY/tagDEC conversion +methods directly. +""" + +import decimal +import unittest +from decimal import Decimal + +from comtypes.automation import _CY_MAX, _CY_MIN, _DECIMAL_MAX_MANTISSA, tagCY, tagDEC + + +class TestTagCY(unittest.TestCase): + """Tests for tagCY conversion methods.""" + + def test_repr(self): + """Test tagCY.__repr__().""" + test_cases = [ + (tagCY(int64=1234500), "tagCY(int64=1234500)"), + (tagCY(int64=-1234500), "tagCY(int64=-1234500)"), + (tagCY(int64=0), "tagCY(int64=0)"), + ] + for cy, expected in test_cases: + with self.subTest(cy=cy): + self.assertEqual(repr(cy), expected) + + def test_as_decimal(self): + """Test tagCY.as_decimal().""" + cy = tagCY(int64=1234500) + self.assertEqual(cy.as_decimal(), Decimal("123.45")) + + def test_as_decimal_negative(self): + """Test tagCY.as_decimal() with negative value.""" + cy = tagCY(int64=-1234500) + self.assertEqual(cy.as_decimal(), Decimal("-123.45")) + + def test_from_decimal_exact(self): + """Test tagCY.from_decimal() with exact 4-decimal values.""" + test_cases = [ + (Decimal("123.45"), 1234500), + (Decimal("123.4500"), 1234500), + (Decimal("123.450000"), 1234500), + (Decimal("3.14"), 31400), + (Decimal("1.2300"), 12300), + (Decimal("0"), 0), + (Decimal("-3.14"), -31400), + (Decimal("-1.2300"), -12300), + (Decimal("-0"), 0), + ] + for val, int64 in test_cases: + with self.subTest(val=val): + cy = tagCY.from_decimal(val) + self.assertEqual(cy.int64, int64) + + def test_from_decimal_inexact_raises(self): + """Test tagCY.from_decimal() raises OverflowError for values with >4 decimal places. + + These values would be silently rounded if we allowed them (e.g., 123.45678 + would become 123.4567). Instead, we require the caller to explicitly round + to 4 decimal places before passing the value, giving them control over + the rounding behavior. + """ + test_cases = [ + Decimal("123.45678"), # would round to 123.4567 + Decimal("0.00005"), # would round to 0 + Decimal("0.00015"), # would round to 0.0001 + Decimal("1.23456"), # would round to 1.2345 + ] + for val in test_cases: + with self.subTest(val=val): + with self.assertRaises(OverflowError) as cm: + tagCY.from_decimal(val) + self.assertIn("more than 4 decimal places", str(cm.exception)) + self.assertIn("Round explicitly before calling", str(cm.exception)) + + def test_from_decimal_overflow(self): + """Test tagCY.from_decimal() raises OverflowError for out-of-range values.""" + test_cases = [ + _CY_MAX + Decimal("0.0001"), # above CY_MAX + _CY_MIN - Decimal("0.0001"), # below CY_MIN + Decimal("1E+30"), # very large value + ] + for val in test_cases: + with self.subTest(val=val): + with self.assertRaises(OverflowError) as cm: + tagCY.from_decimal(val) + self.assertIn("out of range for VT_CY", str(cm.exception)) + + def test_round_trip(self): + """Test round-trip Decimal -> tagCY -> Decimal.""" + test_cases = [ + Decimal("0"), + Decimal("123.45"), + Decimal("-123.45"), + Decimal("0.0001"), + Decimal("-0.0001"), + Decimal("922337203685477.5807"), # CY_MAX + Decimal("-922337203685477.5808"), # CY_MIN + ] + for val in test_cases: + with self.subTest(val=val): + cy = tagCY.from_decimal(val) + self.assertEqual(cy.as_decimal(), val) + + def test_tagCY_from_decimal_context_independent(self): + """Test tagCY.from_decimal is context-independent.""" + with decimal.localcontext(decimal.Context(prec=5)): + cy = tagCY.from_decimal(Decimal("123.4567")) + self.assertEqual(cy.int64, 1234567) + + def test_tagCY_from_decimal_traps_independent(self): + """Test tagCY.from_decimal is not affected by context traps.""" + with decimal.localcontext( + decimal.Context(traps=[decimal.Inexact, decimal.Rounded]) + ): + # Should not raise + cy = tagCY.from_decimal(Decimal("123.4567")) + self.assertEqual(cy.int64, 1234567) + + def test_tagCY_from_decimal_rejects_nan(self): + """Test tagCY.from_decimal rejects NaN.""" + with self.assertRaises(ValueError) as cm: + tagCY.from_decimal(Decimal("NaN")) + self.assertIn("NaN cannot be converted", str(cm.exception)) + + def test_tagCY_from_decimal_rejects_infinity(self): + """Test tagCY.from_decimal rejects Infinity.""" + with self.assertRaises(OverflowError) as cm: + tagCY.from_decimal(Decimal("Infinity")) + self.assertIn("Infinity cannot be converted", str(cm.exception)) + + def test_tagCY_from_decimal_rejects_non_decimal(self): + """Test tagCY.from_decimal rejects non-decimal.""" + with self.assertRaises(TypeError) as cm: + tagCY.from_decimal(object()) + self.assertIn("only Decimal is accepted", str(cm.exception)) + + +class TestTagDEC(unittest.TestCase): + """Tests for tagDEC conversion methods.""" + + def test_repr(self): + """Test tagDEC.__repr__().""" + test_cases = [ + ( + tagDEC(scale=2, sign=0, Hi32=0, Lo64=314), + "tagDEC(scale=2, sign=0, Hi32=0, Lo64=314)", + ), + ( + tagDEC(scale=4, sign=0x80, Hi32=0, Lo64=987654321), + "tagDEC(scale=4, sign=128, Hi32=0, Lo64=987654321)", + ), + ( + tagDEC(scale=0, sign=0, Hi32=0, Lo64=0), + "tagDEC(scale=0, sign=0, Hi32=0, Lo64=0)", + ), + ] + for dec, expected in test_cases: + with self.subTest(dec=dec): + self.assertEqual(repr(dec), expected) + + def test_as_decimal(self): + """Test tagDEC.as_decimal().""" + dec = tagDEC(scale=2, sign=0, Hi32=0, Lo64=314) + self.assertEqual(dec.as_decimal(), Decimal("3.14")) + + def test_as_decimal_negative(self): + """Test tagDEC.as_decimal() with negative value.""" + dec = tagDEC(scale=2, sign=0x80, Hi32=0, Lo64=314) + self.assertEqual(dec.as_decimal(), Decimal("-3.14")) + + def test_from_decimal_exact(self): + """Test tagDEC.from_decimal() with exact values.""" + test_cases = [ + (Decimal("3.14"), Decimal("3.14"), 2, 0), + (Decimal("1.2300"), Decimal("1.2300"), 4, 0), + (Decimal("0"), Decimal("0"), 0, 0), + (Decimal("-3.14"), Decimal("-3.14"), 2, 0x80), + (Decimal("-1.2300"), Decimal("-1.2300"), 4, 0x80), + (Decimal("-0"), Decimal("-0"), 0, 0x80), + ] + for val, expected, scale, sign in test_cases: + with self.subTest(val=val): + dec = tagDEC.from_decimal(val) + self.assertEqual(dec.as_decimal(), expected) + self.assertEqual(dec.scale, scale) + self.assertEqual(dec.sign, sign) + + def test_from_decimal_max_mantissa(self): + """Test tagDEC.from_decimal() with max 96-bit mantissa.""" + # 2**96 - 1 is the max mantissa + val = Decimal(_DECIMAL_MAX_MANTISSA) + dec = tagDEC.from_decimal(val) + mantissa = (dec.Hi32 << 64) + dec.Lo64 + self.assertEqual(mantissa, _DECIMAL_MAX_MANTISSA) + + def test_from_decimal_acceptable_many_decimal_places(self): + val = Decimal("1.2345678901234567890123456789") # 28 decimal places - OK + dec = tagDEC.from_decimal(val) + self.assertEqual(dec.scale, 28) + + def test_from_decimal_overflow(self): + """Test tagDEC.from_decimal() raises OverflowError for too-large values.""" + # Value exceeding 96-bit mantissa at the required scale + val = Decimal("12345678901234567890123456789.0123456789") + with self.assertRaises(OverflowError) as cm: + tagDEC.from_decimal(val) + self.assertIn("exceeds 96-bit mantissa", str(cm.exception)) + self.assertIn("Reduce precision explicitly", str(cm.exception)) + + def test_from_decimal_too_many_decimal_places(self): + """Test tagDEC.from_decimal() raises OverflowError for >28 decimal places.""" + # Use a value with 29 non-zero decimal places (no trailing zeros to strip) + val = Decimal("1.23456789012345678901234567891") + with self.assertRaises(OverflowError) as cm: + tagDEC.from_decimal(val) + self.assertIn("more than 28 decimal places", str(cm.exception)) + self.assertIn("Round explicitly before calling", str(cm.exception)) + + def test_from_decimal_trailing_zeros(self): + """Test tagDEC.from_decimal() preserves trailing zeros in scale.""" + # 1.2300 should have scale 4 + dec = tagDEC.from_decimal(Decimal("1.2300")) + self.assertEqual(dec.scale, 4) + self.assertEqual(dec.as_decimal(), Decimal("1.2300")) + + def test_from_decimal_trailing_zeros_stripped(self): + """Test tagDEC.from_decimal() accepts values with many trailing zeros beyond 28 places.""" + # 1.23 with 30 trailing zeros (32 decimal places total) - should be accepted + # because trailing zeros are stripped until exp >= -28, reducing scale to 28 + val = Decimal("1.23" + "0" * 30) + dec = tagDEC.from_decimal(val) + self.assertEqual(dec.scale, 28) + self.assertEqual(dec.as_decimal(), Decimal("1.23" + "0" * 28)) + + def test_from_decimal_zero_with_many_decimal_places(self): + """Test tagDEC.from_decimal() accepts zero with many decimal places.""" + # 0E-30 should be accepted (zero is special-cased, scale preserved) + val = Decimal("0E-30") + dec = tagDEC.from_decimal(val) + self.assertEqual(dec.scale, 28) # scale preserved from exponent + self.assertEqual(dec.as_decimal(), Decimal("0E-28")) + + def test_from_decimal_huge_positive_exponent(self): + """Test tagDEC.from_decimal() rejects huge positive exponent early.""" + # 1E1000000 would cause huge integer allocation if not checked early + val = Decimal("1E1000000") + with self.assertRaises(OverflowError) as cm: + tagDEC.from_decimal(val) + self.assertIn("exceeds 96-bit mantissa", str(cm.exception)) + self.assertIn("Reduce precision explicitly", str(cm.exception)) + + def test_from_decimal_negative_zero(self): + """Test tagDEC.from_decimal() with negative zero.""" + dec = tagDEC.from_decimal(Decimal("-0")) + self.assertEqual(dec.sign, 0x80) + self.assertEqual(dec.as_decimal(), Decimal("-0")) + + def test_round_trip(self): + """Test round-trip Decimal -> tagDEC -> Decimal.""" + test_cases = [ + Decimal("0"), + Decimal("123.45"), + Decimal("-123.45"), + Decimal("1E-28"), + Decimal("-1E-28"), + Decimal("1.2300"), + Decimal("-1.2300"), + Decimal(str(_DECIMAL_MAX_MANTISSA)), + ] + for val in test_cases: + with self.subTest(val=val): + dec = tagDEC.from_decimal(val) + self.assertEqual(dec.as_decimal(), val) + + def test_tagDEC_from_decimal_context_independent(self): + """Test tagDEC.from_decimal is context-independent.""" + with decimal.localcontext(decimal.Context(prec=5)): + val = Decimal("123.45678901234567890") + dec = tagDEC.from_decimal(val) + # Should not be affected by context precision - round-trip must match + self.assertEqual(dec.as_decimal(), val) + + def test_tagDEC_from_decimal_traps_independent(self): + """Test tagDEC.from_decimal is not affected by context traps.""" + with decimal.localcontext( + decimal.Context(traps=[decimal.Inexact, decimal.Rounded]) + ): + val = Decimal("123.45678901234567890") + dec = tagDEC.from_decimal(val) + # Should not raise and round-trip must match + self.assertEqual(dec.as_decimal(), val) + + def test_tagDEC_from_decimal_positive_exponent_zero(self): + """Test tagDEC.from_decimal with positive exponent zero (0E+5).""" + val = Decimal("0E+5") + dec = tagDEC.from_decimal(val) + self.assertEqual(dec.scale, 0) + self.assertEqual(dec.as_decimal(), Decimal("0")) + + def test_tagDEC_from_decimal_positive_exponent(self): + """Test tagDEC.from_decimal with positive exponent (1E+1).""" + val = Decimal("1E+1") # = 10 + dec = tagDEC.from_decimal(val) + self.assertEqual(dec.scale, 0) + self.assertEqual(dec.as_decimal(), Decimal("10")) + + # Larger positive exponent + val = Decimal("123E+2") # = 12300 + dec = tagDEC.from_decimal(val) + self.assertEqual(dec.scale, 0) + self.assertEqual(dec.as_decimal(), Decimal("12300")) + + def test_tagDEC_from_decimal_rejects_nan(self): + """Test tagDEC.from_decimal rejects NaN.""" + with self.assertRaises(ValueError) as cm: + tagDEC.from_decimal(Decimal("NaN")) + self.assertIn("NaN cannot be converted", str(cm.exception)) + + def test_tagDEC_from_decimal_rejects_snan(self): + """Test tagDEC.from_decimal rejects signaling NaN.""" + with self.assertRaises(ValueError) as cm: + tagDEC.from_decimal(Decimal("sNaN")) + self.assertIn("NaN cannot be converted", str(cm.exception)) + + def test_tagDEC_from_decimal_rejects_infinity(self): + """Test tagDEC.from_decimal rejects Infinity.""" + with self.assertRaises(OverflowError) as cm: + tagDEC.from_decimal(Decimal("Infinity")) + self.assertIn("Infinity cannot be converted", str(cm.exception)) + + def test_tagDEC_from_decimal_rejects_negative_infinity(self): + """Test tagDEC.from_decimal rejects -Infinity.""" + with self.assertRaises(OverflowError) as cm: + tagDEC.from_decimal(Decimal("-Infinity")) + self.assertIn("Infinity cannot be converted", str(cm.exception)) + + def test_tagDEC_from_decimal_rejects_non_decimal(self): + """Test tagDEC.from_decimal rejects non-decimal.""" + with self.assertRaises(TypeError) as cm: + tagDEC.from_decimal(object()) + self.assertIn("only Decimal is accepted", str(cm.exception)) + + def test_tagDEC_from_decimal_boundary_values(self): + """Test tagDEC.from_decimal with boundary values.""" + # 1E+28 (29 digits) - should be accepted (fits in 96-bit mantissa) + val = Decimal("1E+28") + dec = tagDEC.from_decimal(val) + self.assertEqual(dec.as_decimal(), val) + + # 1E+29 (30 digits) - should be rejected (exceeds 96-bit mantissa) + val = Decimal("1E+29") + with self.assertRaises(OverflowError) as cm: + tagDEC.from_decimal(val) + self.assertIn("exceeds 96-bit mantissa", str(cm.exception)) + + # Max mantissa value (2**96 - 1) - should be accepted + val = Decimal(str(_DECIMAL_MAX_MANTISSA)) + dec = tagDEC.from_decimal(val) + mantissa = (dec.Hi32 << 64) + dec.Lo64 + self.assertEqual(mantissa, _DECIMAL_MAX_MANTISSA) + + # Max mantissa + 1 - should be rejected + val = Decimal(str(_DECIMAL_MAX_MANTISSA + 1)) + with self.assertRaises(OverflowError) as cm: + tagDEC.from_decimal(val) + self.assertIn("exceeds 96-bit mantissa", str(cm.exception)) + + # Negative boundary values + # -1E+28 - should be accepted + val = Decimal("-1E+28") + dec = tagDEC.from_decimal(val) + self.assertEqual(dec.as_decimal(), val) + self.assertEqual(dec.sign, 0x80) + + # -1E+29 - should be rejected + val = Decimal("-1E+29") + with self.assertRaises(OverflowError) as cm: + tagDEC.from_decimal(val) + self.assertIn("exceeds 96-bit mantissa", str(cm.exception)) + + # -Max mantissa - should be accepted + val = Decimal(str(-_DECIMAL_MAX_MANTISSA)) + dec = tagDEC.from_decimal(val) + mantissa = (dec.Hi32 << 64) + dec.Lo64 + self.assertEqual(mantissa, _DECIMAL_MAX_MANTISSA) + self.assertEqual(dec.sign, 0x80) + + # -Max mantissa - 1 - should be rejected + val = Decimal(str(-_DECIMAL_MAX_MANTISSA - 1)) + with self.assertRaises(OverflowError) as cm: + tagDEC.from_decimal(val) + self.assertIn("exceeds 96-bit mantissa", str(cm.exception)) + + # 1.5E-27 (28 decimal places, non-zero) - should be accepted + val = Decimal("1.5E-27") + dec = tagDEC.from_decimal(val) + self.assertEqual(dec.as_decimal(), val) + + # 1E-29 (29 decimal places) - should be rejected + val = Decimal("1E-29") + with self.assertRaises(OverflowError) as cm: + tagDEC.from_decimal(val) + self.assertIn("more than 28 decimal places", str(cm.exception)) + + +if __name__ == "__main__": + unittest.main() diff --git a/comtypes/test/test_fixed_point_numbers.py b/comtypes/test/test_fixed_point_numbers.py new file mode 100644 index 000000000..c771f2f42 --- /dev/null +++ b/comtypes/test/test_fixed_point_numbers.py @@ -0,0 +1,240 @@ +"""Fixed point number (VT_DECIMAL, VT_CY) round-trip tests via the native C++ COM test server.""" + +import decimal +import unittest +from ctypes import POINTER, byref, pointer, sizeof + +from comtypes import CLSCTX_LOCAL_SERVER +from comtypes.automation import VARIANT, VT_CY, VT_DECIMAL, VT_NULL, tagCY, tagDEC +from comtypes.client import CreateObject, GetModule + +try: + GetModule(("{07D2AEE5-1DF8-4D2C-953A-554ADFD25F99}", 1, 0, 0)) + from comtypes.gen.ComtypesCppTestSrvLib import ( + CoFixedPointNumberTest, + IFixedPointNumberTest, + ) + + IMPORT_SERVER_FAILED = False +except (ImportError, OSError): + IMPORT_SERVER_FAILED = True + + +_CY_BITS = sizeof(tagCY) * 8 +_LLONG_MIN = -(1 << (_CY_BITS - 1)) +_LLONG_MAX = (1 << (_CY_BITS - 1)) - 1 +_CY_SCALE = decimal.Decimal("10000") +_CY_MIN = decimal.Decimal(_LLONG_MIN) / _CY_SCALE +_CY_MAX = decimal.Decimal(_LLONG_MAX) / _CY_SCALE + + +def _create_server() -> "IFixedPointNumberTest": + return CreateObject( + CoFixedPointNumberTest, + clsctx=CLSCTX_LOCAL_SERVER, + interface=IFixedPointNumberTest, + ) + + +@unittest.skipIf(IMPORT_SERVER_FAILED, "Requires the C++ out-of-process COM server.") +class TestInitCyVariantWith3dot14(unittest.TestCase): + def setUp(self): + self.server = _create_server() + + def test_empty_variant_becomes_decimal(self): + """VARIANT() passed by value: return value is Decimal. + + In comtypes, VARIANT passed by value is passed as [in, out] VARIANT*. + The server modifies the VARIANT in place. The return value is the + modified VARIANT converted to Decimal. The original `var` is cleared + (becomes VT_NULL) by the return value processing. + """ + var = VARIANT() + result = self.server.InitCyVariantWith3dot14(var) + self.assertIsInstance(result, decimal.Decimal) + self.assertEqual(result, decimal.Decimal("3.14")) + # The original var is cleared by return value processing (VT_NULL) + self.assertEqual(var.vt, VT_NULL) + + def test_incoming_bstr_is_replaced(self): + """The server releases the incoming content before reflecting the value. + + The original VARIANT is cleared by return value processing (VT_NULL). + """ + var = VARIANT("not a number") + result = self.server.InitCyVariantWith3dot14(var) + self.assertEqual(result, decimal.Decimal("3.14")) + # The original var is cleared by return value processing (VT_NULL) + self.assertEqual(var.vt, VT_NULL) + + def test_incoming_decimal_is_replaced(self): + """The server releases the incoming content before reflecting the value. + + The original VARIANT is cleared by return value processing (VT_NULL). + """ + var = VARIANT(decimal.Decimal("1.5")) + result = self.server.InitCyVariantWith3dot14(var) + self.assertEqual(result, decimal.Decimal("3.14")) + # The original var is cleared by return value processing (VT_NULL) + self.assertEqual(var.vt, VT_NULL) + + def test_pointer_to_variant_is_written_through(self): + """A POINTER(VARIANT) is passed as it is, so the caller's VARIANT is + filled. + Return value is the pointer to VARIANT. + """ + var = VARIANT() + result = self.server.InitCyVariantWith3dot14(pointer(var)) + # Return value is the pointer (ctypes pointer object) + self.assertIsInstance(result, POINTER(VARIANT)) + # The pointed-to VARIANT is mutated + self.assertEqual(result.contents.vt, VT_CY) + self.assertEqual(result.contents.value, decimal.Decimal("3.14")) + # Argument is mutated + self.assertEqual(var.vt, VT_CY) + self.assertEqual(var.value, decimal.Decimal("3.14")) + + def test_byref_variant_writes_through(self): + """`byref(VARIANT)` is wrapped into a new VARIANT (VT_BYREF | VT_VARIANT); + the server writes through to the referenced VARIANT. + Return value is the outer VARIANT (VT_NULL), argument var is mutated. + """ + var = VARIANT() + result = self.server.InitCyVariantWith3dot14(byref(var)) + # Return value is the outer VARIANT (VT_NULL because server doesn't modify it) + self.assertEqual(result.vt, VT_NULL) + # Argument var is mutated through the reference + self.assertEqual(var.vt, VT_CY) + self.assertEqual(var.value, decimal.Decimal("3.14")) + + def test_byref_cy_is_written_through(self): + """`byref(tagCY)` becomes a VT_BYREF | VT_CY VARIANT; the server writes + through it. Return value is the outer VARIANT (VT_NULL). + """ + cy = tagCY(0) + result = self.server.InitCyVariantWith3dot14(byref(cy)) + # Return value is the outer VARIANT (VT_NULL) + self.assertEqual(result.vt, VT_NULL) + # Argument is mutated + self.assertEqual(cy.int64, 31400) + + def test_pointer_to_cy_is_written_through(self): + """`pointer(tagCY)` becomes a VT_BYREF | VT_CY VARIANT; the server + writes through it. Return value is the outer VARIANT (VT_NULL). + """ + cy = tagCY(0) + result = self.server.InitCyVariantWith3dot14(pointer(cy)) + # Return value is the outer VARIANT (VT_NULL) created to wrap the pointer + self.assertEqual(result.vt, VT_NULL) + # Argument is mutated + self.assertEqual(cy.int64, 31400) + + +@unittest.skipIf(IMPORT_SERVER_FAILED, "Requires the C++ out-of-process COM server.") +class TestInitDecVariantWith3dot14(unittest.TestCase): + def setUp(self): + self.server = _create_server() + + def test_empty_variant_becomes_decimal(self): + """VARIANT() passed by value: return value is Decimal. + + In comtypes, VARIANT passed by value is passed as [in, out] VARIANT*. + The server modifies the VARIANT in place. The return value is the + modified VARIANT converted to Decimal. The original `var` is cleared + (becomes VT_NULL) by the return value processing. + """ + var = VARIANT() + result = self.server.InitDecVariantWith3dot14(var) + self.assertIsInstance(result, decimal.Decimal) + self.assertEqual(result, decimal.Decimal("3.14")) + # The original var is cleared by return value processing (VT_NULL) + self.assertEqual(var.vt, VT_NULL) + + def test_scale_is_preserved(self): + """VT_DECIMAL keeps its scale `(2)`, so the tuple is exactly + `(0, (3, 1, 4), -2)`. + """ + var = VARIANT() + result = self.server.InitDecVariantWith3dot14(var) + self.assertEqual(result.as_tuple(), decimal.DecimalTuple(0, (3, 1, 4), -2)) + # The original var is cleared by return value processing (VT_NULL) + self.assertEqual(var.vt, VT_NULL) + + def test_incoming_bstr_is_replaced(self): + """The server releases the incoming content before reflecting the value. + + The original VARIANT is cleared by return value processing (VT_NULL). + """ + var = VARIANT("not a number") + result = self.server.InitDecVariantWith3dot14(var) + self.assertEqual(result, decimal.Decimal("3.14")) + # The original var is cleared by return value processing (VT_NULL) + self.assertEqual(var.vt, VT_NULL) + + def test_incoming_decimal_is_replaced(self): + """The server releases the incoming content before reflecting the value. + + The original VARIANT is cleared by return value processing (VT_NULL). + """ + var = VARIANT(decimal.Decimal("1.5")) + result = self.server.InitDecVariantWith3dot14(var) + self.assertEqual(result, decimal.Decimal("3.14")) + # The original var is cleared by return value processing (VT_NULL) + self.assertEqual(var.vt, VT_NULL) + + def test_pointer_to_variant_is_written_through(self): + """A POINTER(VARIANT) is passed as it is, so the caller's VARIANT is + filled. + Return value is the pointer to VARIANT. + """ + var = VARIANT() + result = self.server.InitDecVariantWith3dot14(pointer(var)) + # Return value is the pointer (ctypes pointer object) + self.assertIsInstance(result, POINTER(VARIANT)) + # The pointed-to VARIANT is mutated + self.assertEqual(result.contents.vt, VT_DECIMAL) + self.assertEqual(result.contents.value, decimal.Decimal("3.14")) + # Argument is mutated + self.assertEqual(var.vt, VT_DECIMAL) + self.assertEqual(var.value, decimal.Decimal("3.14")) + + def test_byref_variant_writes_through(self): + """`byref(VARIANT)` is wrapped into a new VARIANT (VT_BYREF | VT_VARIANT); + the server writes through to the referenced VARIANT. + Return value is the outer VARIANT (VT_NULL), argument var is mutated. + """ + var = VARIANT() + result = self.server.InitDecVariantWith3dot14(byref(var)) + # Return value is the outer VARIANT (VT_NULL because server doesn't modify it) + self.assertEqual(result.vt, VT_NULL) + # Argument var is mutated through the reference + self.assertEqual(var.vt, VT_DECIMAL) + self.assertEqual(var.value, decimal.Decimal("3.14")) + + def test_byref_decimal_is_written_through(self): + """`byref(tagDEC)` becomes a VT_BYREF | VT_DECIMAL VARIANT; the server + writes through it. Return value is the outer VARIANT (VT_NULL). + """ + dec = tagDEC() + result = self.server.InitDecVariantWith3dot14(byref(dec)) + # Return value is the outer VARIANT (VT_NULL) + self.assertEqual(result.vt, VT_NULL) + # Argument is mutated + self.assertEqual(dec.as_decimal(), decimal.Decimal("3.14")) + self.assertEqual((dec.scale, dec.sign, dec.Hi32, dec.Lo64), (2, 0, 0, 314)) + + def test_pointer_to_decimal_is_written_through(self): + """`pointer(tagDEC)` becomes a VT_BYREF | VT_DECIMAL VARIANT; the server + writes through it. Return value is the outer VARIANT (VT_NULL). + """ + dec = tagDEC() + result = self.server.InitDecVariantWith3dot14(pointer(dec)) + # Return value is the outer VARIANT (VT_NULL) created to wrap the pointer + self.assertEqual(result.vt, VT_NULL) + # Argument is mutated + self.assertEqual(dec.as_decimal(), decimal.Decimal("3.14")) + self.assertEqual((dec.scale, dec.sign, dec.Hi32, dec.Lo64), (2, 0, 0, 314)) + + +if __name__ == "__main__": + unittest.main() diff --git a/comtypes/test/test_midl_safearray_create.py b/comtypes/test/test_midl_safearray_create.py index fb81acca1..626c1cdd4 100644 --- a/comtypes/test/test_midl_safearray_create.py +++ b/comtypes/test/test_midl_safearray_create.py @@ -1,3 +1,4 @@ +import decimal import unittest from ctypes import HRESULT, POINTER, c_int, pointer @@ -5,6 +6,7 @@ import comtypes.safearray import comtypes.typeinfo from comtypes import CLSCTX_INPROC_SERVER, CLSCTX_LOCAL_SERVER +from comtypes.automation import tagCY, tagDEC from comtypes.client import CreateObject, GetModule GetModule("UIAutomationCore.dll") @@ -103,6 +105,30 @@ def test_ctype(self): self.assertIsInstance(unpacked, int) self.assertEqual(unpacked, 1) + def test_tagCY(self): + """Test SAFEARRAY(tagCY) creation and unpacking. + + Unpacking returns Decimal for easier use (tagCY.as_decimal()). + """ + cy_val = tagCY.from_decimal(decimal.Decimal("123.45")) + sa_type = comtypes.safearray._midlSAFEARRAY(tagCY) + sa = sa_type.create([cy_val]) + (unpacked,) = sa.unpack() + self.assertIsInstance(unpacked, decimal.Decimal) + self.assertEqual(unpacked, decimal.Decimal("123.45")) + + def test_tagDEC(self): + """Test SAFEARRAY(tagDEC) creation and unpacking. + + Unpacking returns Decimal for easier use (tagDEC.as_decimal()). + """ + dec_val = tagDEC.from_decimal(decimal.Decimal("3.14")) + sa_type = comtypes.safearray._midlSAFEARRAY(tagDEC) + sa = sa_type.create([dec_val]) + (unpacked,) = sa.unpack() + self.assertIsInstance(unpacked, decimal.Decimal) + self.assertEqual(unpacked, decimal.Decimal("3.14")) + if __name__ == "__main__": unittest.main() diff --git a/source/CppTestSrv/CoFixedPointNumberTest.cpp b/source/CppTestSrv/CoFixedPointNumberTest.cpp new file mode 100644 index 000000000..f034d50e9 --- /dev/null +++ b/source/CppTestSrv/CoFixedPointNumberTest.cpp @@ -0,0 +1,161 @@ +// CoFixedPointNumberTest.cpp +// Test component for converting strings to VT_DECIMAL and VT_CY VARIANTs. +// InitCyVariantWith3dot14: [in, out] VARIANT*. Discards the incoming content and reflects a VT_CY VARIANT holding 3.14. +// If the incoming VARIANT is VT_BYREF | VT_CY, writes 3.14 through the reference instead. +// InitDecVariantWith3dot14: [in, out] VARIANT*. Discards the incoming content and reflects a VT_DECIMAL VARIANT holding 3.14. +// If the incoming VARIANT is VT_BYREF | VT_DECIMAL, writes 3.14 through the reference instead. +#include +#include +#include +#include "CoFixedPointNumberTest.h" + +HRESULT CFixedPointNumberTest::CreateInstance(IUnknown* pUnknownOuter, + CUnknown** ppNewComponent) +{ + if (pUnknownOuter != NULL) + { + return CLASS_E_NOAGGREGATION ; + } + *ppNewComponent = new CFixedPointNumberTest(pUnknownOuter) ; + return S_OK ; +} + +HRESULT __stdcall CFixedPointNumberTest::NondelegatingQueryInterface(const IID& iid, + void** ppv) +{ + if (iid == IID_IFixedPointNumberTest) + { + return FinishQI(static_cast(this), ppv) ; + } + return CUnknown::NondelegatingQueryInterface(iid, ppv) ; +} + +// InitCyVariantWith3dot14 +// [in, out] VARIANT*: discards whatever the caller passed in and reflects a +// VT_CY VARIANT holding 3.14 into it. +// If the caller passed a reference to a CURRENCY (VT_BYREF | VT_CY), 3.14 is +// written through the reference and the VARIANT itself is left as it is. +// If the caller passed a reference to a VARIANT (VT_BYREF | VT_VARIANT), 3.14 is +// written through the reference and the outer VARIANT is left as it is. +HRESULT __stdcall CFixedPointNumberTest::InitCyVariantWith3dot14(VARIANT* value) +{ + if (value == NULL) + { + return E_POINTER ; + } + + if (value->vt == (VT_BYREF | VT_CY)) + { + if (value->pcyVal == NULL) + { + return E_POINTER ; + } + value->pcyVal->int64 = 31400 ; + return S_OK ; + } + + if (value->vt == (VT_BYREF | VT_VARIANT)) + { + if (value->pvarVal == NULL) + { + return E_POINTER ; + } + // Write through to the referenced VARIANT + HRESULT hr = VariantClear(value->pvarVal) ; + if (FAILED(hr)) + { + return hr ; + } + value->pvarVal->cyVal.int64 = 31400 ; + value->pvarVal->vt = VT_CY ; + return S_OK ; + } + + // The incoming VARIANT may own resources (e.g. a BSTR), so release them first. + HRESULT hr = VariantClear(value) ; + if (FAILED(hr)) + { + return hr ; + } + + // CY is a 64-bit integer scaled by 10000 (4 decimal places): 3.14 -> 31400 + value->cyVal.int64 = 31400 ; + value->vt = VT_CY ; + + return S_OK ; +} + +// InitDecVariantWith3dot14 +// [in, out] VARIANT*: discards whatever the caller passed in and reflects a +// VT_DECIMAL VARIANT holding 3.14 into it. +// If the caller passed a reference to a DECIMAL (VT_BYREF | VT_DECIMAL), 3.14 is +// written through the reference and the VARIANT itself is left as it is. +// If the caller passed a reference to a VARIANT (VT_BYREF | VT_VARIANT), 3.14 is +// written through the reference and the outer VARIANT is left as it is. +HRESULT __stdcall CFixedPointNumberTest::InitDecVariantWith3dot14(VARIANT* value) +{ + if (value == NULL) + { + return E_POINTER ; + } + + if (value->vt == (VT_BYREF | VT_DECIMAL)) + { + if (value->pdecVal == NULL) + { + return E_POINTER ; + } + // Not a VARIANT-overlaid DECIMAL here, so the field order does not matter. + memset(value->pdecVal, 0, sizeof(DECIMAL)) ; + value->pdecVal->scale = 2 ; + value->pdecVal->Lo64 = 314 ; + return S_OK ; + } + + if (value->vt == (VT_BYREF | VT_VARIANT)) + { + if (value->pvarVal == NULL) + { + return E_POINTER ; + } + // Write through to the referenced VARIANT + HRESULT hr = VariantClear(value->pvarVal) ; + if (FAILED(hr)) + { + return hr ; + } + // DECIMAL overlays the whole VARIANT: DECIMAL::wReserved shares its storage + // with VARIANT::vt. Build the DECIMAL first, then set vt afterwards. + // 3.14 = 314 * 10^-2 -> scale 2, sign 0, 96-bit integer 314 + DECIMAL dec ; + memset(&dec, 0, sizeof(dec)) ; + dec.scale = 2 ; + dec.sign = 0 ; + dec.Hi32 = 0 ; + dec.Lo64 = 314 ; + value->pvarVal->decVal = dec ; + value->pvarVal->vt = VT_DECIMAL ; + return S_OK ; + } + + // The incoming VARIANT may own resources (e.g. a BSTR), so release them first. + HRESULT hr = VariantClear(value) ; + if (FAILED(hr)) + { + return hr ; + } + + // DECIMAL overlays the whole VARIANT: DECIMAL::wReserved shares its storage + // with VARIANT::vt. Build the DECIMAL first, then set vt afterwards. + // 3.14 = 314 * 10^-2 -> scale 2, sign 0, 96-bit integer 314 + DECIMAL dec ; + memset(&dec, 0, sizeof(dec)) ; + dec.scale = 2 ; + dec.sign = 0 ; + dec.Hi32 = 0 ; + dec.Lo64 = 314 ; + value->decVal = dec ; + value->vt = VT_DECIMAL ; + + return S_OK ; +} \ No newline at end of file diff --git a/source/CppTestSrv/CoFixedPointNumberTest.h b/source/CppTestSrv/CoFixedPointNumberTest.h new file mode 100644 index 000000000..674f13486 --- /dev/null +++ b/source/CppTestSrv/CoFixedPointNumberTest.h @@ -0,0 +1,24 @@ +// CoFixedPointNumberTest.h +// Test component for converting strings to VT_DECIMAL and VT_CY VARIANTs. +// InitCyVariantWith3dot14: [in, out] VARIANT*. Discards the incoming content and reflects a VT_CY VARIANT holding 3.14. +// If the incoming VARIANT is VT_BYREF | VT_CY, writes 3.14 through the reference instead. +// InitDecVariantWith3dot14: [in, out] VARIANT*. Discards the incoming content and reflects a VT_DECIMAL VARIANT holding 3.14. +// If the incoming VARIANT is VT_BYREF | VT_DECIMAL, writes 3.14 through the reference instead. +#include "CUnknown.h" +#include "Iface.h" + +class CFixedPointNumberTest : public CUnknown, public IFixedPointNumberTest +{ +public: + static HRESULT CreateInstance(IUnknown* pUnknownOuter, CUnknown** ppNewComponent) ; + +private: + DECLARE_IUNKNOWN + virtual HRESULT __stdcall NondelegatingQueryInterface(const IID& iid, void** ppv) ; + + // Interface IFixedPointNumberTest + virtual HRESULT __stdcall InitCyVariantWith3dot14(VARIANT* value) ; + virtual HRESULT __stdcall InitDecVariantWith3dot14(VARIANT* value) ; + + CFixedPointNumberTest(IUnknown* pUnknownOuter) : CUnknown(pUnknownOuter) {} ; +} ; \ No newline at end of file diff --git a/source/CppTestSrv/MAKEFILE b/source/CppTestSrv/MAKEFILE index d42893b46..85c90d9af 100644 --- a/source/CppTestSrv/MAKEFILE +++ b/source/CppTestSrv/MAKEFILE @@ -41,7 +41,7 @@ guids.obj : guids.c # Component/server source files # -server.obj : server.cpp cunknown.h cfactory.h iface.h CoNumericSafearrayTest.h CoByteCompatibilityTest.h CoVariantErrorTest.h +server.obj : server.cpp cunknown.h cfactory.h iface.h CoNumericSafearrayTest.h CoByteCompatibilityTest.h CoVariantErrorTest.h CoFixedPointNumberTest.h cl $(CPP_FLAGS) server.cpp CoComtypesDispRecordParamTest.obj : CoComtypesDispRecordParamTest.cpp \ @@ -67,6 +67,10 @@ CoVariantErrorTest.obj : CoVariantErrorTest.cpp \ CoVariantErrorTest.h iface.h CUnknown.h cl $(CPP_FLAGS) CoVariantErrorTest.cpp +CoFixedPointNumberTest.obj : CoFixedPointNumberTest.cpp \ + CoFixedPointNumberTest.h iface.h CUnknown.h + cl $(CPP_FLAGS) CoFixedPointNumberTest.cpp + # # Helper classes # @@ -100,11 +104,12 @@ SERVER_OBJS = Server.obj \ CoByteCompatibilityTest.obj \ CoStringSafearrayInsideVariantTest.obj \ CoVariantErrorTest.obj \ + CoFixedPointNumberTest.obj \ Registry.obj \ Cfactory.obj \ Cunknown.obj \ Util.obj \ - Guids.obj + Guids.obj Server.exe: $(SERVER_OBJS) outproc.obj link $(EXE_LINK_FLAGS) $(SERVER_OBJS) \ diff --git a/source/CppTestSrv/SERVER.CPP b/source/CppTestSrv/SERVER.CPP index 433b27231..54d1089d8 100644 --- a/source/CppTestSrv/SERVER.CPP +++ b/source/CppTestSrv/SERVER.CPP @@ -14,6 +14,7 @@ #include "CoByteCompatibilityTest.h" #include "CoStringSafearrayInsideVariantTest.h" #include "CoVariantErrorTest.h" +#include "CoFixedPointNumberTest.h" /////////////////////////////////////////////////////////// @@ -75,6 +76,12 @@ CFactoryData g_FactoryDataArray[] = L"Comtypes.VariantErrorTest.1", L"Comtypes.VariantErrorTest", &LIBID_ComtypesCppTestSrvLib, + NULL, 0}, + {&CLSID_CoFixedPointNumberTest, CFixedPointNumberTest::CreateInstance, + L"Comtypes component for fixed point number conversion tests", + L"Comtypes.FixedPointNumberTest.1", + L"Comtypes.FixedPointNumberTest", + &LIBID_ComtypesCppTestSrvLib, NULL, 0} } ; int g_cFactoryDataEntries diff --git a/source/CppTestSrv/SERVER.IDL b/source/CppTestSrv/SERVER.IDL index c261ed3c7..fddc0028d 100644 --- a/source/CppTestSrv/SERVER.IDL +++ b/source/CppTestSrv/SERVER.IDL @@ -132,6 +132,18 @@ interface IVariantErrorTest : IUnknown HRESULT GetErrorCode([in] VARIANT value, [out, retval] LONG* errorCode); } ; +// Interface IFixedPointNumberTest +// Test interface for converting strings to VT_DECIMAL and VT_CY VARIANTs. +[ + object, + uuid(8F4E2D1A-7C3B-4A9E-8F1D-2B5C6D7E8F9A), + oleautomation +] +interface IFixedPointNumberTest : IUnknown +{ + HRESULT InitCyVariantWith3dot14([in, out] VARIANT* value); + HRESULT InitDecVariantWith3dot14([in, out] VARIANT* value); +} ; // // Component and type library descriptions @@ -238,4 +250,15 @@ library ComtypesCppTestSrvLib { [default] interface IVariantErrorTest ; } ; + + // CoFixedPointNumberTest + // Component for testing string to VT_DECIMAL and VT_CY VARIANT conversion. + [ + uuid(9A2B3C4D-5E6F-7A8B-9C0D-1E2F3A4B5C6D), + helpstring("Comtypes component for fixed point number conversion tests.") + ] + coclass CoFixedPointNumberTest + { + [default] interface IFixedPointNumberTest ; + } ; } ;