diff --git a/docs/changelog/119536.yaml b/docs/changelog/119536.yaml new file mode 100644 index 0000000000000..e4b0fca2bd8db --- /dev/null +++ b/docs/changelog/119536.yaml @@ -0,0 +1,5 @@ +pr: 119536 +summary: Fix ROUND() with unsigned longs throwing in some edge cases +area: ES|QL +type: bug +issues: [] diff --git a/docs/reference/esql/functions/kibana/definition/round.json b/docs/reference/esql/functions/kibana/definition/round.json index 4f4ddd36daa05..4ef20aa162b42 100644 --- a/docs/reference/esql/functions/kibana/definition/round.json +++ b/docs/reference/esql/functions/kibana/definition/round.json @@ -34,6 +34,24 @@ "variadic" : false, "returnType" : "double" }, + { + "params" : [ + { + "name" : "number", + "type" : "double", + "optional" : false, + "description" : "The numeric value to round. If `null`, the function returns `null`." + }, + { + "name" : "decimals", + "type" : "long", + "optional" : true, + "description" : "The number of decimal places to round to. Defaults to 0. If `null`, the function returns `null`." + } + ], + "variadic" : false, + "returnType" : "double" + }, { "params" : [ { @@ -64,6 +82,24 @@ "variadic" : false, "returnType" : "integer" }, + { + "params" : [ + { + "name" : "number", + "type" : "integer", + "optional" : false, + "description" : "The numeric value to round. If `null`, the function returns `null`." + }, + { + "name" : "decimals", + "type" : "long", + "optional" : true, + "description" : "The number of decimal places to round to. Defaults to 0. If `null`, the function returns `null`." + } + ], + "variadic" : false, + "returnType" : "integer" + }, { "params" : [ { @@ -94,6 +130,24 @@ "variadic" : false, "returnType" : "long" }, + { + "params" : [ + { + "name" : "number", + "type" : "long", + "optional" : false, + "description" : "The numeric value to round. If `null`, the function returns `null`." + }, + { + "name" : "decimals", + "type" : "long", + "optional" : true, + "description" : "The number of decimal places to round to. Defaults to 0. If `null`, the function returns `null`." + } + ], + "variadic" : false, + "returnType" : "long" + }, { "params" : [ { @@ -105,6 +159,42 @@ ], "variadic" : false, "returnType" : "unsigned_long" + }, + { + "params" : [ + { + "name" : "number", + "type" : "unsigned_long", + "optional" : false, + "description" : "The numeric value to round. If `null`, the function returns `null`." + }, + { + "name" : "decimals", + "type" : "integer", + "optional" : true, + "description" : "The number of decimal places to round to. Defaults to 0. If `null`, the function returns `null`." + } + ], + "variadic" : false, + "returnType" : "unsigned_long" + }, + { + "params" : [ + { + "name" : "number", + "type" : "unsigned_long", + "optional" : false, + "description" : "The numeric value to round. If `null`, the function returns `null`." + }, + { + "name" : "decimals", + "type" : "long", + "optional" : true, + "description" : "The number of decimal places to round to. Defaults to 0. If `null`, the function returns `null`." + } + ], + "variadic" : false, + "returnType" : "unsigned_long" } ], "examples" : [ diff --git a/docs/reference/esql/functions/types/round.asciidoc b/docs/reference/esql/functions/types/round.asciidoc index 2c0fe768741f6..9b102edf6ed94 100644 --- a/docs/reference/esql/functions/types/round.asciidoc +++ b/docs/reference/esql/functions/types/round.asciidoc @@ -6,10 +6,15 @@ |=== number | decimals | result double | integer | double +double | long | double double | | double integer | integer | integer +integer | long | integer integer | | integer long | integer | long +long | long | long long | | long +unsigned_long | integer | unsigned_long +unsigned_long | long | unsigned_long unsigned_long | | unsigned_long |=== diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/math.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/math.csv-spec index 2fe2feb3bc219..b2b4f15860484 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/math.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/math.csv-spec @@ -771,6 +771,28 @@ ul:ul 18446744073709551615 ; +roundMaxULWithBigNegativeDecimals +required_capability: fn_round_ul_fixes +row + ul1 = round(18446744073709551615, -6144415263046370459::long), + ul2 = round(18446744073709551615, -20::long), + ul3 = round(12446744073709551615, -19::long); + +ul1:ul | ul2:ul | ul3:ul +0 | 0 | 10000000000000000000 +; + +roundBigULWithRoundULOverflow +required_capability: fn_round_ul_fixes +row ul = round(18446744073709551615, -19::long); + +warning:Line 1:10: evaluation of [round(18446744073709551615, -19::long)] failed, treating result as null. Only first 20 failures recorded. +warning:Line 1:10: java.lang.ArithmeticException: unsigned_long overflow + +ul:ul +null +; + mvAvg from employees | where emp_no > 10008 | eval salary_change = mv_avg(salary_change) | sort emp_no | keep emp_no, salary_change.int, salary_change | limit 7; diff --git a/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/math/RoundUnsignedLongEvaluator.java b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/math/RoundUnsignedLongEvaluator.java index c9c6e3806cb04..9cc233b8aff0c 100644 --- a/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/math/RoundUnsignedLongEvaluator.java +++ b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/math/RoundUnsignedLongEvaluator.java @@ -4,6 +4,7 @@ // 2.0. package org.elasticsearch.xpack.esql.expression.function.scalar.math; +import java.lang.ArithmeticException; import java.lang.IllegalArgumentException; import java.lang.Override; import java.lang.String; @@ -52,7 +53,7 @@ public Block eval(Page page) { if (decimalsVector == null) { return eval(page.getPositionCount(), valBlock, decimalsBlock); } - return eval(page.getPositionCount(), valVector, decimalsVector).asBlock(); + return eval(page.getPositionCount(), valVector, decimalsVector); } } } @@ -82,16 +83,26 @@ public LongBlock eval(int positionCount, LongBlock valBlock, LongBlock decimalsB result.appendNull(); continue position; } - result.appendLong(Round.processUnsignedLong(valBlock.getLong(valBlock.getFirstValueIndex(p)), decimalsBlock.getLong(decimalsBlock.getFirstValueIndex(p)))); + try { + result.appendLong(Round.processUnsignedLong(valBlock.getLong(valBlock.getFirstValueIndex(p)), decimalsBlock.getLong(decimalsBlock.getFirstValueIndex(p)))); + } catch (ArithmeticException e) { + warnings().registerException(e); + result.appendNull(); + } } return result.build(); } } - public LongVector eval(int positionCount, LongVector valVector, LongVector decimalsVector) { - try(LongVector.FixedBuilder result = driverContext.blockFactory().newLongVectorFixedBuilder(positionCount)) { + public LongBlock eval(int positionCount, LongVector valVector, LongVector decimalsVector) { + try(LongBlock.Builder result = driverContext.blockFactory().newLongBlockBuilder(positionCount)) { position: for (int p = 0; p < positionCount; p++) { - result.appendLong(p, Round.processUnsignedLong(valVector.getLong(p), decimalsVector.getLong(p))); + try { + result.appendLong(Round.processUnsignedLong(valVector.getLong(p), decimalsVector.getLong(p))); + } catch (ArithmeticException e) { + warnings().registerException(e); + result.appendNull(); + } } return result.build(); } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java index fea97b4aee384..fb5433d7662af 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java @@ -192,6 +192,11 @@ public enum Cap { */ FN_SUBSTRING_EMPTY_NULL, + /** + * Fixes on function {@code ROUND} that avoid it throwing exceptions on runtime for unsigned long cases. + */ + FN_ROUND_UL_FIXES, + /** * All functions that take TEXT should never emit TEXT, only KEYWORD. #114334 */ diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/Round.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/Round.java index b1baa6c55ce47..7c977fd1ce5a6 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/Round.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/Round.java @@ -35,7 +35,7 @@ import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.FIRST; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.SECOND; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isNumeric; -import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isWholeNumber; +import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isType; import static org.elasticsearch.xpack.esql.core.util.NumericUtils.unsignedLongAsNumber; import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.bigIntegerToUnsignedLong; import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.longToUnsignedLong; @@ -63,7 +63,7 @@ public Round( @Param( optional = true, name = "decimals", - type = { "integer" }, // TODO long is supported here too + type = { "integer", "long" }, description = "The number of decimal places to round to. Defaults to 0. If `null`, the function returns `null`." ) Expression decimals ) { @@ -103,7 +103,15 @@ protected TypeResolution resolveType() { return resolution; } - return decimals == null ? TypeResolution.TYPE_RESOLVED : isWholeNumber(decimals, sourceText(), SECOND); + return decimals == null + ? TypeResolution.TYPE_RESOLVED + : isType( + decimals, + dt -> dt.isWholeNumber() && dt != DataType.UNSIGNED_LONG, + sourceText(), + SECOND, + "whole number except unsigned_long or counter types" + ); } @Override @@ -123,11 +131,16 @@ static int process(int val, long decimals) { @Evaluator(extraName = "Long") static long process(long val, long decimals) { - return Maths.round(val, decimals).longValue(); + return Maths.round(val, decimals); } - @Evaluator(extraName = "UnsignedLong") + @Evaluator(extraName = "UnsignedLong", warnExceptions = ArithmeticException.class) static long processUnsignedLong(long val, long decimals) { + if (decimals <= -20) { + // Unsigned long max value is 2^64 - 1, which has 20 digits + return longToUnsignedLong(0, false); + } + Number ul = unsignedLongAsNumber(val); if (ul instanceof BigInteger bi) { BigInteger rounded = Maths.round(bi, decimals); diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java index 619c0a24e80c6..e3214411698b0 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java @@ -284,27 +284,26 @@ public void testRoundFunctionInvalidInputs() { error("row a = 1, b = \"c\" | eval x = round(b)") ); assertEquals( - "1:31: second argument of [round(a, b)] must be [integer], found value [b] type [keyword]", + "1:31: second argument of [round(a, b)] must be [whole number except unsigned_long or counter types], " + + "found value [b] type [keyword]", error("row a = 1, b = \"c\" | eval x = round(a, b)") ); assertEquals( - "1:31: second argument of [round(a, 3.5)] must be [integer], found value [3.5] type [double]", + "1:31: second argument of [round(a, 3.5)] must be [whole number except unsigned_long or counter types], " + + "found value [3.5] type [double]", error("row a = 1, b = \"c\" | eval x = round(a, 3.5)") ); } public void testImplicitCastingErrorMessages() { - assertEquals( - "1:23: Cannot convert string [c] to [INTEGER], error [Cannot parse number [c]]", - error("row a = round(123.45, \"c\")") - ); + assertEquals("1:23: Cannot convert string [c] to [LONG], error [Cannot parse number [c]]", error("row a = round(123.45, \"c\")")); assertEquals( "1:27: Cannot convert string [c] to [DOUBLE], error [Cannot parse number [c]]", error("row a = 1 | eval x = acos(\"c\")") ); assertEquals( "1:33: Cannot convert string [c] to [DOUBLE], error [Cannot parse number [c]]\n" - + "line 1:38: Cannot convert string [a] to [INTEGER], error [Cannot parse number [a]]", + + "line 1:38: Cannot convert string [a] to [LONG], error [Cannot parse number [a]]", error("row a = 1 | eval x = round(acos(\"c\"),\"a\")") ); assertEquals( diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/AbstractScalarFunctionTestCase.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/AbstractScalarFunctionTestCase.java index cc18a76e9a2f7..429e6685a201c 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/AbstractScalarFunctionTestCase.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/AbstractScalarFunctionTestCase.java @@ -95,6 +95,24 @@ protected static Iterable parameterSuppliersFromTypedDataWithDefaultCh return parameterSuppliersFromTypedData(anyNullIsNull(entirelyNullPreservesType, randomizeBytesRefsOffset(suppliers))); } + /** + * Converts a list of test cases into a list of parameter suppliers. + * Also, adds a default set of extra test cases. + *

+ * Use if possible, as this method may get updated with new checks in the future. + *

+ * + * @param nullsExpectedType See {@link #anyNullIsNull(List, ExpectedType, ExpectedEvaluatorToString)} + * @param evaluatorToString See {@link #anyNullIsNull(List, ExpectedType, ExpectedEvaluatorToString)} + */ + protected static Iterable parameterSuppliersFromTypedDataWithDefaultChecksNoErrors( + ExpectedType nullsExpectedType, + ExpectedEvaluatorToString evaluatorToString, + List suppliers + ) { + return parameterSuppliersFromTypedData(anyNullIsNull(randomizeBytesRefsOffset(suppliers), nullsExpectedType, evaluatorToString)); + } + /** * Converts a list of test cases into a list of parameter suppliers. * Also, adds a default set of extra test cases. diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/RoundErrorTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/RoundErrorTests.java new file mode 100644 index 0000000000000..54020317bbcfd --- /dev/null +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/RoundErrorTests.java @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.esql.expression.function.scalar.math; + +import org.elasticsearch.xpack.esql.core.expression.Expression; +import org.elasticsearch.xpack.esql.core.tree.Source; +import org.elasticsearch.xpack.esql.core.type.DataType; +import org.elasticsearch.xpack.esql.expression.function.ErrorsForCasesWithoutExamplesTestCase; +import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier; +import org.hamcrest.Matcher; + +import java.util.List; +import java.util.Set; + +import static org.hamcrest.Matchers.equalTo; + +public class RoundErrorTests extends ErrorsForCasesWithoutExamplesTestCase { + @Override + protected List cases() { + return paramsToSuppliers(RoundTests.parameters()); + } + + @Override + protected Expression build(Source source, List args) { + return new Round(source, args.get(0), args.size() == 1 ? null : args.get(1)); + } + + @Override + protected Matcher expectedTypeErrorMatcher(List> validPerPosition, List signature) { + return equalTo( + typeErrorMessage( + true, + validPerPosition, + signature, + (v, p) -> p == 0 ? "numeric" : "whole number except unsigned_long or counter types" + ) + ); + } +} diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/RoundTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/RoundTests.java index c05388a9708da..e7a8d2d7ef9d4 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/RoundTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/RoundTests.java @@ -19,6 +19,7 @@ import org.elasticsearch.xpack.esql.expression.function.AbstractScalarFunctionTestCase; import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier; +import java.math.BigInteger; import java.util.ArrayList; import java.util.List; import java.util.function.BiFunction; @@ -26,8 +27,6 @@ import java.util.function.Supplier; import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; public class RoundTests extends AbstractScalarFunctionTestCase { public RoundTests(@Name("TestCase") Supplier testCaseSupplier) { @@ -37,11 +36,13 @@ public RoundTests(@Name("TestCase") Supplier testCase @ParametersFactory public static Iterable parameters() { List suppliers = new ArrayList<>(); + + // Double field suppliers.add( supplier( "", DataType.DOUBLE, - () -> 1 / randomDouble(), + () -> randomDoubleBetween(-Double.MAX_VALUE, Double.MAX_VALUE, true), "RoundDoubleNoDecimalsEvaluator[val=Attribute[channel=0]]", d -> Maths.round(d, 0) ) @@ -50,36 +51,252 @@ public static Iterable parameters() { supplier( ", ", DataType.DOUBLE, - () -> 1 / randomDouble(), + () -> randomDoubleBetween(-Double.MAX_VALUE, Double.MAX_VALUE, true), DataType.INTEGER, () -> between(-30, 30), "RoundDoubleEvaluator[val=Attribute[channel=0], decimals=CastIntToLongEvaluator[v=Attribute[channel=1]]]", Maths::round ) ); - // TODO randomized cases for more types - // TODO errorsForCasesWithoutExamples - suppliers = anyNullIsNull( - suppliers, - (nullPosition, nullValueDataType, original) -> nullPosition == 0 ? nullValueDataType : original.expectedType(), - (nullPosition, nullData, original) -> original + suppliers.add( + supplier( + ", ", + DataType.DOUBLE, + () -> randomDoubleBetween(-Double.MAX_VALUE, Double.MAX_VALUE, true), + DataType.LONG, + () -> randomLongBetween(-30, 30), + "RoundDoubleEvaluator[val=Attribute[channel=0], decimals=Attribute[channel=1]]", + Maths::round + ) ); - suppliers.add(new TestCaseSupplier("two doubles", List.of(DataType.DOUBLE, DataType.INTEGER), () -> { - double number1 = 1 / randomDouble(); - double number2 = 1 / randomDouble(); - int precision = between(-30, 30); - return new TestCaseSupplier.TestCase( - List.of( - new TestCaseSupplier.TypedData(List.of(number1, number2), DataType.DOUBLE, "number"), - new TestCaseSupplier.TypedData(precision, DataType.INTEGER, "decimals") - ), - "RoundDoubleEvaluator[val=Attribute[channel=0], decimals=CastIntToLongEvaluator[v=Attribute[channel=1]]]", - DataType.DOUBLE, - is(nullValue()) - ).withWarning("Line -1:-1: evaluation of [] failed, treating result as null. Only first 20 failures recorded.") - .withWarning("Line -1:-1: java.lang.IllegalArgumentException: single-value function encountered multi-value"); - })); + // Long decimals + suppliers.add( + supplier( + ", ", + DataType.INTEGER, + ESTestCase::randomInt, + DataType.LONG, + ESTestCase::randomLong, + "RoundIntEvaluator[val=Attribute[channel=0], decimals=Attribute[channel=1]]", + (n, d) -> Maths.round((Number) n, d) + ) + ); + suppliers.add( + supplier( + ", ", + DataType.LONG, + ESTestCase::randomLong, + DataType.LONG, + ESTestCase::randomLong, + "RoundLongEvaluator[val=Attribute[channel=0], decimals=Attribute[channel=1]]", + (n, d) -> Maths.round((Number) n, d) + ) + ); + suppliers.add( + supplier( + ", ", + DataType.UNSIGNED_LONG, + ESTestCase::randomLong, + DataType.LONG, + // Safe negative integer to not trigger an exception and not slow down the test + () -> randomLongBetween(-10_000, Long.MAX_VALUE), + "RoundUnsignedLongEvaluator[val=Attribute[channel=0], decimals=Attribute[channel=1]]", + (n, d) -> Maths.round(NumericUtils.unsignedLongAsBigInteger(n), d) + ) + ); + + // Integer decimals + suppliers.add( + supplier( + ", ", + DataType.INTEGER, + ESTestCase::randomInt, + DataType.INTEGER, + ESTestCase::randomInt, + "RoundIntEvaluator[val=Attribute[channel=0], decimals=CastIntToLongEvaluator[v=Attribute[channel=1]]]", + (n, d) -> Maths.round((Number) n, d) + ) + ); + suppliers.add( + supplier( + ", ", + DataType.LONG, + ESTestCase::randomLong, + DataType.INTEGER, + ESTestCase::randomInt, + "RoundLongEvaluator[val=Attribute[channel=0], decimals=CastIntToLongEvaluator[v=Attribute[channel=1]]]", + (n, d) -> Maths.round((Number) n, d) + ) + ); + suppliers.add( + supplier( + ", ", + DataType.UNSIGNED_LONG, + ESTestCase::randomLong, + DataType.INTEGER, + // Safe negative integer to not trigger an exception and not slow down the test + () -> randomIntBetween(-10_000, Integer.MAX_VALUE), + "RoundUnsignedLongEvaluator[val=Attribute[channel=0], decimals=CastIntToLongEvaluator[v=Attribute[channel=1]]]", + (n, d) -> Maths.round(NumericUtils.unsignedLongAsBigInteger(n), d) + ) + ); + + // Unsigned long errors + suppliers.add( + new TestCaseSupplier( + ", ", + List.of(DataType.UNSIGNED_LONG, DataType.LONG), + () -> new TestCaseSupplier.TestCase( + List.of( + new TestCaseSupplier.TypedData(new BigInteger("18446744073709551615"), DataType.UNSIGNED_LONG, "number"), + new TestCaseSupplier.TypedData(-9223372036854775808L, DataType.LONG, "decimals") + ), + "RoundUnsignedLongEvaluator[val=Attribute[channel=0], decimals=Attribute[channel=1]]", + DataType.UNSIGNED_LONG, + equalTo(BigInteger.ZERO) + ) + ) + ); + suppliers.add( + new TestCaseSupplier( + ", ", + List.of(DataType.UNSIGNED_LONG, DataType.LONG), + () -> new TestCaseSupplier.TestCase( + List.of( + new TestCaseSupplier.TypedData(new BigInteger("18446744073709551615"), DataType.UNSIGNED_LONG, "number"), + new TestCaseSupplier.TypedData(-2147483647L, DataType.LONG, "decimals") + ), + "RoundUnsignedLongEvaluator[val=Attribute[channel=0], decimals=Attribute[channel=1]]", + DataType.UNSIGNED_LONG, + equalTo(BigInteger.ZERO) + ) + ) + ); + suppliers.add( + new TestCaseSupplier( + ", <-20>", + List.of(DataType.UNSIGNED_LONG, DataType.LONG), + () -> new TestCaseSupplier.TestCase( + List.of( + new TestCaseSupplier.TypedData(new BigInteger("18446744073709551615"), DataType.UNSIGNED_LONG, "number"), + new TestCaseSupplier.TypedData(-20L, DataType.LONG, "decimals") + ), + "RoundUnsignedLongEvaluator[val=Attribute[channel=0], decimals=Attribute[channel=1]]", + DataType.UNSIGNED_LONG, + equalTo(BigInteger.ZERO) + ) + ) + ); + suppliers.add( + new TestCaseSupplier( + ", <-19>", + List.of(DataType.UNSIGNED_LONG, DataType.LONG), + () -> new TestCaseSupplier.TestCase( + List.of( + new TestCaseSupplier.TypedData(new BigInteger("18446744073709551615"), DataType.UNSIGNED_LONG, "number"), + new TestCaseSupplier.TypedData(-19L, DataType.LONG, "decimals") + ), + "RoundUnsignedLongEvaluator[val=Attribute[channel=0], decimals=Attribute[channel=1]]", + DataType.UNSIGNED_LONG, + equalTo(null) + ).withWarning("Line -1:-1: evaluation of [] failed, treating result as null. Only first 20 failures recorded.") + .withWarning("Line -1:-1: java.lang.ArithmeticException: unsigned_long overflow") + ) + ); + suppliers.add( + new TestCaseSupplier( + ", <-19>", + List.of(DataType.UNSIGNED_LONG, DataType.LONG), + () -> new TestCaseSupplier.TestCase( + List.of( + new TestCaseSupplier.TypedData(new BigInteger("14446744073709551615"), DataType.UNSIGNED_LONG, "number"), + new TestCaseSupplier.TypedData(-19L, DataType.LONG, "decimals") + ), + "RoundUnsignedLongEvaluator[val=Attribute[channel=0], decimals=Attribute[channel=1]]", + DataType.UNSIGNED_LONG, + equalTo(new BigInteger("10000000000000000000")) + ) + ) + ); + + // Max longs and overflows + suppliers.add( + new TestCaseSupplier( + ", <-20>", + List.of(DataType.LONG, DataType.LONG), + () -> new TestCaseSupplier.TestCase( + List.of( + new TestCaseSupplier.TypedData(Long.MAX_VALUE, DataType.LONG, "number"), + new TestCaseSupplier.TypedData(-20L, DataType.LONG, "decimals") + ), + "RoundLongEvaluator[val=Attribute[channel=0], decimals=Attribute[channel=1]]", + DataType.LONG, + equalTo(0L) + ) + ) + ); + suppliers.add( + new TestCaseSupplier( + ", <-19>", + List.of(DataType.LONG, DataType.LONG), + () -> new TestCaseSupplier.TestCase( + List.of( + new TestCaseSupplier.TypedData(Long.MAX_VALUE, DataType.LONG, "number"), + new TestCaseSupplier.TypedData(-19L, DataType.LONG, "decimals") + ), + "RoundLongEvaluator[val=Attribute[channel=0], decimals=Attribute[channel=1]]", + DataType.LONG, + equalTo(0L) + ) + ) + ); + suppliers.add( + new TestCaseSupplier( + ", <-18>", + List.of(DataType.LONG, DataType.LONG), + () -> new TestCaseSupplier.TestCase( + List.of( + new TestCaseSupplier.TypedData(Long.MAX_VALUE, DataType.LONG, "number"), + new TestCaseSupplier.TypedData(-18L, DataType.LONG, "decimals") + ), + "RoundLongEvaluator[val=Attribute[channel=0], decimals=Attribute[channel=1]]", + DataType.LONG, + equalTo(9000000000000000000L) + ) + ) + ); + // Max integers and overflows + suppliers.add( + new TestCaseSupplier( + ", <-10>", + List.of(DataType.INTEGER, DataType.LONG), + () -> new TestCaseSupplier.TestCase( + List.of( + new TestCaseSupplier.TypedData(Integer.MAX_VALUE, DataType.INTEGER, "number"), + new TestCaseSupplier.TypedData(-10L, DataType.LONG, "decimals") + ), + "RoundIntEvaluator[val=Attribute[channel=0], decimals=Attribute[channel=1]]", + DataType.INTEGER, + equalTo(0) + ) + ) + ); + suppliers.add( + new TestCaseSupplier( + ", <-9>", + List.of(DataType.INTEGER, DataType.LONG), + () -> new TestCaseSupplier.TestCase( + List.of( + new TestCaseSupplier.TypedData(Integer.MAX_VALUE, DataType.INTEGER, "number"), + new TestCaseSupplier.TypedData(-9L, DataType.LONG, "decimals") + ), + "RoundIntEvaluator[val=Attribute[channel=0], decimals=Attribute[channel=1]]", + DataType.INTEGER, + equalTo(2000000000) + ) + ) + ); // Integer or Long without a decimals parameter is a noop suppliers.add(supplier("", DataType.INTEGER, ESTestCase::randomInt, "Attribute[channel=0]", Function.identity())); @@ -128,7 +345,12 @@ public static Iterable parameters() { suppliers.add(supplier(0, 0, 0)); suppliers.add(supplier(123, 2, 123)); suppliers.add(supplier(123, -1, 120)); - return parameterSuppliersFromTypedData(suppliers); + + return parameterSuppliersFromTypedDataWithDefaultChecksNoErrors( + (nullPosition, nullValueDataType, original) -> nullPosition == 0 ? nullValueDataType : original.expectedType(), + (nullPosition, nullData, original) -> original, + suppliers + ); } private static TestCaseSupplier supplier(double v, double expected) {