diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/accumulator/stdDevSamp/test_expression_stdDevSamp_boundaries.py b/documentdb_tests/compatibility/tests/core/operator/expressions/accumulator/stdDevSamp/test_expression_stdDevSamp_boundaries.py new file mode 100644 index 000000000..3cb0aa0fd --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/accumulator/stdDevSamp/test_expression_stdDevSamp_boundaries.py @@ -0,0 +1,129 @@ +import math +from dataclasses import dataclass +from typing import Any + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_case import BaseTestCase +from documentdb_tests.framework.test_constants import ( + DOUBLE_MAX, + DOUBLE_MAX_SAFE_INTEGER, + DOUBLE_MIN, + DOUBLE_MIN_SUBNORMAL, + DOUBLE_NEAR_MIN, + DOUBLE_PRECISION_LOSS, + INT32_MAX, + INT32_MAX_MINUS_1, + INT32_MIN, + INT32_MIN_PLUS_1, + INT32_OVERFLOW, + INT64_MAX, + INT64_MAX_MINUS_1, + INT64_MIN, + INT64_MIN_PLUS_1, +) + + +@dataclass(frozen=True) +class StdDevSampTest(BaseTestCase): + values: Any = None + + +STDDEVSAMP_BOUNDARIES_TESTS: list[StdDevSampTest] = [ + # boundaries + StdDevSampTest( + "bound_int32_max", + values=[INT32_MAX, INT32_MAX_MINUS_1], + expected=pytest.approx(0.7071067811865476), + msg="Should compute sample std dev for values near the int32 maximum", + ), + StdDevSampTest( + "bound_int32_min", + values=[INT32_MIN, INT32_MIN_PLUS_1], + expected=pytest.approx(0.7071067811865476), + msg="Should compute sample std dev for values near the int32 minimum", + ), + StdDevSampTest( + "bound_int64_max", + values=[INT64_MAX, INT64_MAX_MINUS_1], + expected=0.0, + msg="Should return 0.0 when int64 max values are indistinguishable at double precision", + ), + StdDevSampTest( + "bound_int64_min", + values=[INT64_MIN, INT64_MIN_PLUS_1], + expected=0.0, + msg="Should return 0.0 when int64 min values are indistinguishable at double precision", + ), + StdDevSampTest( + "bound_int32_overflow", + values=[INT32_MAX, INT32_OVERFLOW], + expected=pytest.approx(0.7071067811865476), + msg="Should handle value at int32 overflow boundary", + ), + StdDevSampTest( + "bound_double_max", + values=[DOUBLE_MAX, DOUBLE_MAX - 1], + expected=0.0, + msg="Should return 0.0 when values are indistinguishable at double maximum precision", + ), + StdDevSampTest( + "bound_double_near_0", + values=[DOUBLE_NEAR_MIN, 2e-308, 3e-308], + expected=0.0, + msg="Should return 0.0 when near-min doubles are indistinguishable at double precision", + ), + StdDevSampTest( + "bound_double_subnormal", + values=[DOUBLE_MIN_SUBNORMAL, DOUBLE_NEAR_MIN], + expected=0.0, + msg="Should return 0.0 for indistinguishable subnormal doubles at double precision", + ), + StdDevSampTest( + "bound_double_full_range", + values=[DOUBLE_MIN, DOUBLE_MAX], + expected=pytest.approx(math.nan, nan_ok=True), + msg="Should return NaN when double range spread overflows during variance computation", + ), + StdDevSampTest( + "bound_double_precision_loss", + values=[DOUBLE_MAX_SAFE_INTEGER, DOUBLE_PRECISION_LOSS], + expected=0.0, + msg="Should return 0.0 when values are indistinguishable due to double precision loss", + ), +] + + +@pytest.mark.parametrize("test_case", pytest_params(STDDEVSAMP_BOUNDARIES_TESTS)) +def test_stdDevSamp_boundaries_from_list(collection, test_case: StdDevSampTest): + """Test $stdDevSamp expression boundaries from a literal argument list.""" + + result = execute_expression(collection, {"$stdDevSamp": test_case.values}) + + assert_expression_result( + result, + expected=test_case.expected, + error_code=test_case.error_code, + msg=test_case.msg, + ) + + +@pytest.mark.parametrize("test_case", pytest_params(STDDEVSAMP_BOUNDARIES_TESTS)) +def test_stdDevSamp_boundaries_from_field(collection, test_case: StdDevSampTest): + """Test $stdDevSamp expression boundaries from an inserted array field.""" + result = execute_expression_with_insert( + collection, {"$stdDevSamp": "$values"}, {"values": test_case.values} + ) + + assert_expression_result( + result, + expected=test_case.expected, + error_code=test_case.error_code, + msg=test_case.msg, + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/accumulator/stdDevSamp/test_expression_stdDevSamp_core.py b/documentdb_tests/compatibility/tests/core/operator/expressions/accumulator/stdDevSamp/test_expression_stdDevSamp_core.py new file mode 100644 index 000000000..c82a8ef7a --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/accumulator/stdDevSamp/test_expression_stdDevSamp_core.py @@ -0,0 +1,158 @@ +from dataclasses import dataclass +from typing import Any + +import pytest +from bson import Decimal128, Int64 + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_case import BaseTestCase +from documentdb_tests.framework.test_constants import ( + DOUBLE_NEGATIVE_ZERO, +) + + +@dataclass(frozen=True) +class StdDevSampTest(BaseTestCase): + values: Any = None + + +STDDEVSAMP_CORE_TESTS: list[StdDevSampTest] = [ + # same type operations + StdDevSampTest( + "core_numeric_int32", + values=[1, 2, 3, 4], + expected=pytest.approx(1.2909944487358056), + msg="Should compute sample std dev of int32 values", + ), + StdDevSampTest( + "core_numeric_int64", + values=[Int64(1), Int64(3), Int64(2), Int64(4)], + expected=pytest.approx(1.2909944487358056), + msg="Should compute sample std dev of int64 values", + ), + StdDevSampTest( + "core_numeric_double", + values=[1.0, 2.0, 4.0, 3.0], + expected=pytest.approx(1.2909944487358056), + msg="Should compute sample std dev of double values", + ), + StdDevSampTest( + "core_numeric_decimal128", + values=[Decimal128("1"), Decimal128("2"), Decimal128("3"), Decimal128("4")], + expected=pytest.approx(1.2909944487358056), + msg="Should compute sample std dev of decimal128 values", + ), + # mix type operation + StdDevSampTest( + "core_numeric_mix", + values=[Decimal128("1"), Int64(2), 3, 4.0], + expected=pytest.approx(1.2909944487358056), + msg="Should compute sample std dev of mix numerical values", + ), + # fractional + StdDevSampTest( + "core_fractional_double", + values=[1.5, 2.5], + expected=pytest.approx(0.7071067811865476), + msg="Should compute sample std dev of fractional double values", + ), + StdDevSampTest( + "core_fractional_decimal", + values=[Decimal128("1.5"), Decimal128("2.5")], + expected=pytest.approx(0.7071067811865476), + msg="Should compute sample std dev of fractional decimal values", + ), + # negative operation + StdDevSampTest( + "core_numeric_negative", + values=[-1, -2, -3, -4], + expected=pytest.approx(1.2909944487358056), + msg="Should compute sample std dev of negative values", + ), + StdDevSampTest( + "core_numeric_mixed_signs", + values=[-3, 3, 9], + expected=pytest.approx(6.0), + msg="Should compute sample std dev of both positive and negative values", + ), + StdDevSampTest( + "core_negative_zero", + values=[DOUBLE_NEGATIVE_ZERO, DOUBLE_NEGATIVE_ZERO], + expected=0.0, + msg="Should compute sample std dev treating negative zero as zero", + ), + # large N + StdDevSampTest( + "core_large_n", + values=list(range(1000)), + expected=pytest.approx(288.8194360957494), + msg="Should compute sample std dev of all 1000 values", + ), + # dec_high_precision_not_preserved + StdDevSampTest( + "core_high_precision", + values=[Decimal128("1.000000000000000000000000000000001"), Decimal128("1.0")], + expected=pytest.approx(0.0), + msg="Should match preservation behavior for high-precision Decimal128 inputs", + ), + # N<2 -> null rule + StdDevSampTest( + "core_single_value", + values=[5], + expected=None, + msg="Should return null when single value", + ), + StdDevSampTest( + "core_no_value", + values=[], + expected=None, + msg="Should return null when no values", + ), + StdDevSampTest( + "core_scalar_value", + values=42, + expected=None, + msg="Should return null when scalar value", + ), + # zero variance + StdDevSampTest( + "core_zero_variance", + values=[5, 5, 5], + expected=0.0, + msg="Should return 0.0 when there's no variance", + ), +] + + +@pytest.mark.parametrize("test_case", pytest_params(STDDEVSAMP_CORE_TESTS)) +def test_stdDevSamp_core_from_list(collection, test_case: StdDevSampTest): + """Test $stdDevSamp expression core properties from a literal argument list.""" + + result = execute_expression(collection, {"$stdDevSamp": test_case.values}) + + assert_expression_result( + result, + expected=test_case.expected, + error_code=test_case.error_code, + msg=test_case.msg, + ) + + +@pytest.mark.parametrize("test_case", pytest_params(STDDEVSAMP_CORE_TESTS)) +def test_stdDevSamp_core_from_field(collection, test_case: StdDevSampTest): + """Test $stdDevSamp expression core properties from an inserted array field.""" + result = execute_expression_with_insert( + collection, {"$stdDevSamp": "$values"}, {"values": test_case.values} + ) + + assert_expression_result( + result, + expected=test_case.expected, + error_code=test_case.error_code, + msg=test_case.msg, + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/accumulator/stdDevSamp/test_expression_stdDevSamp_input_forms.py b/documentdb_tests/compatibility/tests/core/operator/expressions/accumulator/stdDevSamp/test_expression_stdDevSamp_input_forms.py new file mode 100644 index 000000000..02ecccc7c --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/accumulator/stdDevSamp/test_expression_stdDevSamp_input_forms.py @@ -0,0 +1,137 @@ +from dataclasses import dataclass +from typing import Any + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.error_codes import FAILED_TO_PARSE_ERROR, INVALID_DOLLAR_FIELD_PATH +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_case import BaseTestCase + + +@dataclass(frozen=True) +class StdDevSampTest(BaseTestCase): + values: Any = None + document: dict[str, Any] | None = None + + +STDDEVSAMP_INPUT_FORMS_TESTS: list[StdDevSampTest] = [ + # basic traversal + StdDevSampTest( + "traversal_basic", + values={"$literal": [1, 2, 3]}, + document=None, + expected=pytest.approx(1.0), + msg="Should compute stdDevSamp traversing the literal array", + ), + StdDevSampTest( + "traversal_empty_array", + values={"$literal": []}, + document=None, + expected=None, + msg="Should return null for an empty traversed array", + ), + StdDevSampTest( + "traversal_single_element", + values={"$literal": [5]}, + document=None, + expected=None, + msg="Should return null for a single traversed element", + ), + StdDevSampTest( + "nested_array", + values={"$literal": [[1, 2, 3]]}, + document=None, + expected=None, + msg="Should return null for a nested array", + ), + # expression with operand args + StdDevSampTest( + "expression_operand_add", + values=[{"$add": [1, 1]}, 4, 6], + document=None, + expected=pytest.approx(2.0), + msg="Should compute stdDevSamp dev from expression operands", + ), + StdDevSampTest( + "expression_operand_null", + values=[{"$literal": None}, 4, 6], + document=None, + expected=pytest.approx(1.4142135623730951), + msg="Should calculate stdDevSamp ignoring expressions returning null", + ), + # document traversal + StdDevSampTest( + "field_refs", + values=["$a", "$b", "$c"], + document={"a": 1, "b": 2, "c": 3}, + expected=pytest.approx(1.0), + msg="Should compute stdDevSamp from multiple field references", + ), + StdDevSampTest( + "nested_object_path", + values=["$a.x", "$a.y", "$a.z"], + document={"a": {"x": 1, "y": 2, "z": 3}}, + expected=pytest.approx(1.0), + msg="Should compute stdDevSamp from nested object paths", + ), + StdDevSampTest( + "composite_array_path", + values="$a.val", + document={"a": [{"val": 1}, {"val": 5}]}, + expected=pytest.approx(2.8284271247461903), + msg="Should compute stdDevSamp from a composite array path", + ), + # field path errors + StdDevSampTest( + "fieldpath_bare_dollar", + values="$", + document=None, + error_code=INVALID_DOLLAR_FIELD_PATH, + msg="Should reject bare '$' as an invalid field path", + ), + StdDevSampTest( + "fieldpath_double_dollar", + values="$$", + document=None, + error_code=FAILED_TO_PARSE_ERROR, + msg="Should reject '$$' as an empty variable name", + ), +] + + +@pytest.mark.parametrize( + "test_case", pytest_params([t for t in STDDEVSAMP_INPUT_FORMS_TESTS if t.document is None]) +) +def test_stdDevSamp_expression(collection, test_case): + """Test $stdDevSamp expression input form expressions from a literal argument list.""" + result = execute_expression(collection, {"$stdDevSamp": test_case.values}) + + assert_expression_result( + result, + expected=test_case.expected, + error_code=test_case.error_code, + msg=test_case.msg, + ) + + +@pytest.mark.parametrize( + "test_case", + pytest_params([t for t in STDDEVSAMP_INPUT_FORMS_TESTS if t.document is not None]), +) +def test_stdDevSamp_expression_from_document(collection, test_case): + """Test $stdDevSamp expression input forms from inserted document fields.""" + result = execute_expression_with_insert( + collection, {"$stdDevSamp": test_case.values}, test_case.document + ) + + assert_expression_result( + result, + expected=test_case.expected, + error_code=test_case.error_code, + msg=test_case.msg, + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/accumulator/stdDevSamp/test_expression_stdDevSamp_non_numeric.py b/documentdb_tests/compatibility/tests/core/operator/expressions/accumulator/stdDevSamp/test_expression_stdDevSamp_non_numeric.py new file mode 100644 index 000000000..8b0e34d18 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/accumulator/stdDevSamp/test_expression_stdDevSamp_non_numeric.py @@ -0,0 +1,109 @@ +from dataclasses import dataclass +from datetime import datetime +from typing import Any + +import pytest +from bson import ObjectId, Regex + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_case import BaseTestCase + + +@dataclass(frozen=True) +class StdDevSampTest(BaseTestCase): + values: Any = None + + +STDDEVSAMP_NON_NUMERIC_TESTS: list[StdDevSampTest] = [ + # sole non-numerics are treated as scalar => None output + StdDevSampTest( + "sole_string", + values="string", + expected=None, + msg="Should return None for any scalar string", + ), + StdDevSampTest( + "sole_numeric_string", + values="30", + expected=None, + msg="Should return None for any scalar numeric string", + ), + StdDevSampTest( + "scalar_boolean", + values=True, + expected=None, + msg="Should return None for a boolean scalar", + ), + # non-numeric only arrays return None + StdDevSampTest( + "array_numeric_string", + values=["30"], + expected=None, + msg="Should return None for one element array with string", + ), + StdDevSampTest( + "array_all_non_numerics", + values=["3", True, Regex("a"), datetime(2026, 6, 1), ObjectId("000000000000000000000000")], + expected=None, + msg="Should return None for arrays with only non-numerics", + ), + # non-numerics are ignored among numerics + StdDevSampTest( + "numeric_string_with_one_int", + values=[60, "30"], + expected=None, + msg="Should return None when fewer than two numeric values remain after ignoring strings", + ), + StdDevSampTest( + "numeric_string_with_two_int", + values=[60, "30", 50], + expected=pytest.approx(7.0710678118654755), + msg="Should return stdDevSamp of all numerical values, strings are ignored", + ), + StdDevSampTest( + "non_numerics_with_two_int", + values=[60, "3", 50, True, datetime(2026, 6, 1), ObjectId("000000000000000000000000")], + expected=pytest.approx(7.0710678118654755), + msg="Should return stdDevSamp of numerical values, non-numericals are ignored", + ), + StdDevSampTest( + "bool_ignored_with_numerics", + values=[True, 5, 10, False], + expected=pytest.approx(3.5355339059327378), + msg="Should ignore boolean values and compute over remaining numerics", + ), +] + + +@pytest.mark.parametrize("test_case", pytest_params(STDDEVSAMP_NON_NUMERIC_TESTS)) +def test_stdDevSamp_non_numeric_from_list(collection, test_case: StdDevSampTest): + """Test $stdDevSamp expression non numeric properties from a literal argument list.""" + + result = execute_expression(collection, {"$stdDevSamp": test_case.values}) + + assert_expression_result( + result, + expected=test_case.expected, + error_code=test_case.error_code, + msg=test_case.msg, + ) + + +@pytest.mark.parametrize("test_case", pytest_params(STDDEVSAMP_NON_NUMERIC_TESTS)) +def test_stdDevSamp_non_numeric_from_field(collection, test_case: StdDevSampTest): + """Test $stdDevSamp expression non numeric properties from an inserted array field.""" + result = execute_expression_with_insert( + collection, {"$stdDevSamp": "$values"}, {"values": test_case.values} + ) + + assert_expression_result( + result, + expected=test_case.expected, + error_code=test_case.error_code, + msg=test_case.msg, + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/accumulator/stdDevSamp/test_expression_stdDevSamp_null_missing.py b/documentdb_tests/compatibility/tests/core/operator/expressions/accumulator/stdDevSamp/test_expression_stdDevSamp_null_missing.py new file mode 100644 index 000000000..7c3cf6953 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/accumulator/stdDevSamp/test_expression_stdDevSamp_null_missing.py @@ -0,0 +1,104 @@ +from dataclasses import dataclass +from typing import Any + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_case import BaseTestCase +from documentdb_tests.framework.test_constants import MISSING + + +@dataclass(frozen=True) +class StdDevSampTest(BaseTestCase): + values: Any = None + + +STDDEVSAMP_NULL_MISSING_TESTS: list[StdDevSampTest] = [ + StdDevSampTest( + "null_scalar", + values=None, + expected=None, + msg="Should return None for a null scalar", + ), + StdDevSampTest( + "missing_scalar", + values=MISSING, + expected=None, + msg="Should return None for a missing scalar", + ), + StdDevSampTest( + "null_single_array", + values=[None], + expected=None, + msg="Should return None for an array with single null", + ), + StdDevSampTest( + "null_first_array", + values=[None, 2, 3], + expected=0.7071067811865476, + msg="Should calculate stdDevSamp and ignore leading null", + ), + StdDevSampTest( + "null_middle_array", + values=[1, None, 3], + expected=1.4142135623730951, + msg="Should calculate stdDevSamp and ignore middle null", + ), + StdDevSampTest( + "null_end_array", + values=[1, 2, None], + expected=0.7071067811865476, + msg="Should calculate stdDevSamp and ignore ending null", + ), + StdDevSampTest( + "all_null", + values=[None, None], + expected=None, + msg="Should return None when all values are null", + ), + StdDevSampTest( + "null_leaves_one_numeric", + values=[None, 5], + expected=None, + msg="Should return None when only one numeric value with rest None", + ), +] + + +@pytest.mark.parametrize("test_case", pytest_params(STDDEVSAMP_NULL_MISSING_TESTS)) +def test_stdDevSamp_null_missing_from_list(collection, test_case: StdDevSampTest): + """Test $stdDevSamp expression null & missing properties from a literal argument list.""" + + result = execute_expression(collection, {"$stdDevSamp": test_case.values}) + + assert_expression_result( + result, + expected=test_case.expected, + error_code=test_case.error_code, + msg=test_case.msg, + ) + + +@pytest.mark.parametrize("test_case", pytest_params(STDDEVSAMP_NULL_MISSING_TESTS)) +def test_stdDevSamp_null_missing_from_field(collection, test_case: StdDevSampTest): + """Test $stdDevSamp expression null & missing properties from an inserted array field.""" + + document = {} if test_case.values is MISSING else {"values": test_case.values} + + result = execute_expression_with_insert( + collection, + {"$stdDevSamp": "$values"}, + document, + ) + + assert_expression_result( + result, + expected=test_case.expected, + error_code=test_case.error_code, + msg=test_case.msg, + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/accumulator/stdDevSamp/test_expression_stdDevSamp_special_values.py b/documentdb_tests/compatibility/tests/core/operator/expressions/accumulator/stdDevSamp/test_expression_stdDevSamp_special_values.py new file mode 100644 index 000000000..c8c005aa1 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/accumulator/stdDevSamp/test_expression_stdDevSamp_special_values.py @@ -0,0 +1,192 @@ +import math +from dataclasses import dataclass +from typing import Any + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_case import BaseTestCase +from documentdb_tests.framework.test_constants import ( + DECIMAL128_INFINITY, + DECIMAL128_NAN, + DECIMAL128_NEGATIVE_INFINITY, + DECIMAL128_NEGATIVE_NAN, + FLOAT_INFINITY, + FLOAT_NAN, + FLOAT_NEGATIVE_INFINITY, +) + + +@dataclass(frozen=True) +class StdDevSampTest(BaseTestCase): + values: Any = None + + +STDDEVSAMP_INFINITY_TESTS: list[StdDevSampTest] = [ + StdDevSampTest( + "inf_scalar", + values=FLOAT_INFINITY, + expected=None, + msg="Should return None for a double infinity scalar", + ), + StdDevSampTest( + "negative_inf_scalar", + values=FLOAT_NEGATIVE_INFINITY, + expected=None, + msg="Should return None for a negative double infinity scalar", + ), + StdDevSampTest( + "decimal_inf_scalar", + values=DECIMAL128_INFINITY, + expected=None, + msg="Should return None for a decimal infinity scalar", + ), + StdDevSampTest( + "negative_decimal_inf_scalar", + values=DECIMAL128_NEGATIVE_INFINITY, + expected=None, + msg="Should return None for a negative decimal infinity scalar", + ), + StdDevSampTest( + "inf_single_array", + values=[FLOAT_INFINITY], + expected=None, + msg="Should return None for a double infinity single element array", + ), + StdDevSampTest( + "negative_inf_single_array", + values=[FLOAT_NEGATIVE_INFINITY], + expected=None, + msg="Should return None for a negative double infinity single element array", + ), + StdDevSampTest( + "decimal_inf_single_array", + values=[DECIMAL128_INFINITY], + expected=None, + msg="Should return None for a decimal infinity single element array", + ), + StdDevSampTest( + "negative_decimal_inf_single_array", + values=[DECIMAL128_NEGATIVE_INFINITY], + expected=None, + msg="Should return None for a negative decimal infinity single element array", + ), + StdDevSampTest( + "inf_with_finite", + values=[1, 2, 3, FLOAT_INFINITY], + expected=pytest.approx(math.nan, nan_ok=True), + msg="Should return NaN if a double infinity is present", + ), + StdDevSampTest( + "negative_inf_with_finite", + values=[1, 2, 3, FLOAT_NEGATIVE_INFINITY], + expected=pytest.approx(math.nan, nan_ok=True), + msg="Should return NaN if a negative double infinity is present", + ), + StdDevSampTest( + "decimal_inf_with_finite", + values=[1, 2, 3, DECIMAL128_INFINITY], + expected=pytest.approx(math.nan, nan_ok=True), + msg="Should return NaN if a decimal infinity is present", + ), + StdDevSampTest( + "negative_decimal_inf_with_finite", + values=[1, 2, 3, DECIMAL128_NEGATIVE_INFINITY], + expected=pytest.approx(math.nan, nan_ok=True), + msg="Should return NaN if a negative decimal infinity is present", + ), + StdDevSampTest( + "inf_same_sign_pair", + values=[FLOAT_INFINITY, FLOAT_INFINITY], + expected=pytest.approx(math.nan, nan_ok=True), + msg="Should return NaN for a double infinity pair of same signs", + ), + StdDevSampTest( + "inf_opp_sign_pair", + values=[FLOAT_INFINITY, FLOAT_NEGATIVE_INFINITY], + expected=pytest.approx(math.nan, nan_ok=True), + msg="Should return NaN for a double infinity pair of opposite signs", + ), + StdDevSampTest( + "decimal_inf_same_sign_pair", + values=[DECIMAL128_INFINITY, DECIMAL128_INFINITY], + expected=pytest.approx(math.nan, nan_ok=True), + msg="Should return NaN for a decimal infinity pair of same signs", + ), + StdDevSampTest( + "decimal_inf_opp_sign_pair", + values=[DECIMAL128_INFINITY, DECIMAL128_NEGATIVE_INFINITY], + expected=pytest.approx(math.nan, nan_ok=True), + msg="Should return NaN for a decimal infinity pair of opposite signs", + ), +] + + +STDDEVSAMP_NAN_TESTS: list[StdDevSampTest] = [ + StdDevSampTest( + "core_nan_present", + values=[1, 2, FLOAT_NAN], + expected=pytest.approx(math.nan, nan_ok=True), + msg="Should return NaN when a NaN is present", + ), + StdDevSampTest( + "core_nan_decimal_present", + values=[1, 2, DECIMAL128_NAN], + expected=pytest.approx(math.nan, nan_ok=True), + msg="Should return NaN when a Decimal NaN value is present", + ), + StdDevSampTest( + "core_negative_nan_present", + values=[1, 2, DECIMAL128_NEGATIVE_NAN], + expected=pytest.approx(math.nan, nan_ok=True), + msg="Should still return NaN when negative NaN is present", + ), + StdDevSampTest( + "core_nan_pair", + values=[FLOAT_NAN, FLOAT_NAN], + expected=pytest.approx(math.nan, nan_ok=True), + msg="Should return NaN even for NaN pairs", + ), + StdDevSampTest( + "core_single_value_nan", + values=[FLOAT_NAN], + expected=None, + msg="Should return None when single value", + ), +] + +STDDEVSAMP_SPECIAL_TESTS = STDDEVSAMP_INFINITY_TESTS + STDDEVSAMP_NAN_TESTS + + +@pytest.mark.parametrize("test_case", pytest_params(STDDEVSAMP_SPECIAL_TESTS)) +def test_stdDevSamp_special_from_list(collection, test_case: StdDevSampTest): + """Test $stdDevSamp expression infinity & NaN properties from a literal argument list.""" + + result = execute_expression(collection, {"$stdDevSamp": test_case.values}) + + assert_expression_result( + result, + expected=test_case.expected, + error_code=test_case.error_code, + msg=test_case.msg, + ) + + +@pytest.mark.parametrize("test_case", pytest_params(STDDEVSAMP_SPECIAL_TESTS)) +def test_stdDevSamp_special_from_field(collection, test_case: StdDevSampTest): + """Test $stdDevSamp expression infinity & NaN properties from an inserted array field.""" + result = execute_expression_with_insert( + collection, {"$stdDevSamp": "$values"}, {"values": test_case.values} + ) + + assert_expression_result( + result, + expected=test_case.expected, + error_code=test_case.error_code, + msg=test_case.msg, + )