From b7fb250115017b68972f7de922742e8f03a841d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20Kokosi=C5=84ski?= Date: Tue, 26 Feb 2019 10:30:32 +0100 Subject: [PATCH 1/2] Make DataSize final --- src/main/java/io/airlift/units/DataSize.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/io/airlift/units/DataSize.java b/src/main/java/io/airlift/units/DataSize.java index e34476a..45a7048 100644 --- a/src/main/java/io/airlift/units/DataSize.java +++ b/src/main/java/io/airlift/units/DataSize.java @@ -27,7 +27,7 @@ import static java.lang.Math.floor; import static java.util.Objects.requireNonNull; -public class DataSize +public final class DataSize implements Comparable { private static final Pattern PATTERN = Pattern.compile("^\\s*(\\d+(?:\\.\\d+)?)\\s*([a-zA-Z]+)\\s*$"); From b805a0a8c72d2c9659cb1896d2b0a072ae787a13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20Kokosi=C5=84ski?= Date: Tue, 26 Feb 2019 10:30:35 +0100 Subject: [PATCH 2/2] Introduce Count --- src/main/java/io/airlift/units/Count.java | 203 ++++++++++++ src/main/java/io/airlift/units/MaxCount.java | 40 +++ .../io/airlift/units/MaxCountValidator.java | 41 +++ src/main/java/io/airlift/units/MinCount.java | 40 +++ .../io/airlift/units/MinCountValidator.java | 41 +++ .../java/io/airlift/units/MockMaxCount.java | 62 ++++ .../java/io/airlift/units/MockMinCount.java | 62 ++++ src/test/java/io/airlift/units/TestCount.java | 291 ++++++++++++++++++ .../io/airlift/units/TestCountValidator.java | 205 ++++++++++++ 9 files changed, 985 insertions(+) create mode 100644 src/main/java/io/airlift/units/Count.java create mode 100644 src/main/java/io/airlift/units/MaxCount.java create mode 100644 src/main/java/io/airlift/units/MaxCountValidator.java create mode 100644 src/main/java/io/airlift/units/MinCount.java create mode 100644 src/main/java/io/airlift/units/MinCountValidator.java create mode 100644 src/test/java/io/airlift/units/MockMaxCount.java create mode 100644 src/test/java/io/airlift/units/MockMinCount.java create mode 100644 src/test/java/io/airlift/units/TestCount.java create mode 100644 src/test/java/io/airlift/units/TestCountValidator.java diff --git a/src/main/java/io/airlift/units/Count.java b/src/main/java/io/airlift/units/Count.java new file mode 100644 index 0000000..04c201c --- /dev/null +++ b/src/main/java/io/airlift/units/Count.java @@ -0,0 +1,203 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.airlift.units; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static io.airlift.units.Preconditions.checkArgument; +import static java.lang.Long.parseLong; +import static java.lang.Math.multiplyExact; +import static java.lang.String.format; +import static java.util.Objects.requireNonNull; + +public final class Count + implements Comparable +{ + private static final Pattern PATTERN = Pattern.compile("^\\s*(\\d+)\\s*([a-zA-Z]?)\\s*$"); + + // We iterate over the MAGNITUDES constant in convertToMostSuccinctRounded() + // instead of Magnitude.values() as the latter results in non-trivial amount of memory + // allocation when that method is called in a tight loop. The reason is that the values() + // call allocates a new array at each call. + private static final Magnitude[] MAGNITUDES = Magnitude.values(); + + /** + * @return count with no bigger value than 1000 in succinct magnitude, fractional part is rounded + */ + public static Count succinctRounded(long count) + { + return succinctRounded(count, Magnitude.SINGLE); + } + + /** + * @return count with no bigger value than 1000 in succinct magnitude, fractional part is rounded + */ + public static Count succinctRounded(long count, Magnitude magnitude) + { + return new Count(count, magnitude).convertToMostSuccinctRounded(); + } + + private final long value; + private final Magnitude magnitude; + + public Count(long count, Magnitude magnitude) + { + checkArgument(count >= 0, "count is negative"); + requireNonNull(magnitude, "magnitude is null"); + + this.value = count; + this.magnitude = magnitude; + } + + public long getValue() + { + return value; + } + + public Magnitude getMagnitude() + { + return magnitude; + } + + public long getValue(Magnitude magnitude) + { + requireNonNull(magnitude, "magnitude is null"); + + if (value == 0L) { + return 0L; + } + + long scale = this.magnitude.getFactor() / magnitude.getFactor(); + if (scale * magnitude.getFactor() != this.magnitude.getFactor()) { + throw new IllegalArgumentException(format("Unable to represent %s in %s, conversion would cause a precision loss", this, magnitude)); + } + try { + return multiplyExact(value, scale); + } + catch (ArithmeticException e) { + throw new IllegalArgumentException(format("Unable to represent %s in %s due the Long value overflow", this, magnitude)); + } + } + + public Count convertTo(Magnitude magnitude) + { + return new Count(getValue(magnitude), magnitude); + } + + /** + * @return converted count with no bigger value than 1000 in succinct magnitude, fractional part is rounded + */ + public Count convertToMostSuccinctRounded() + { + for (Magnitude magnitude : MAGNITUDES) { + double converted = (double) value * this.magnitude.getFactor() / magnitude.getFactor(); + if (converted < 1000) { + return new Count(Math.round(converted), magnitude); + } + } + throw new IllegalStateException(); + } + + @JsonValue + @Override + public String toString() + { + return value + magnitude.getMagnitudeString(); + } + + @JsonCreator + public static Count valueOf(String count) + throws IllegalArgumentException + { + requireNonNull(count, "count is null"); + checkArgument(!count.isEmpty(), "count is empty"); + + Matcher matcher = PATTERN.matcher(count); + if (!matcher.matches()) { + throw new IllegalArgumentException("Not a valid count string: " + count); + } + + long value = parseLong(matcher.group(1)); + String magnitutdeString = matcher.group(2); + + for (Magnitude magnitude : Magnitude.values()) { + if (magnitude.getMagnitudeString().equals(magnitutdeString)) { + return new Count(value, magnitude); + } + } + + throw new IllegalArgumentException("Unknown magnitude: " + magnitutdeString); + } + + @Override + public int compareTo(Count o) + { + return Long.compare(getValue(Magnitude.SINGLE), o.getValue(Magnitude.SINGLE)); + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + Count count = (Count) o; + + return compareTo(count) == 0; + } + + @Override + public int hashCode() + { + return Long.hashCode(getValue(Magnitude.SINGLE)); + } + + public enum Magnitude + { + // must be in increasing magnitude order + SINGLE(1L, ""), + THOUSAND(1000L, "K"), + MILLION(1000_000L, "M"), + BILLION(1000_000_000L, "B"), + TRILION(1000_000_000_000L, "T"), + QUADRILLION(1000_000_000_000_000L, "P"); + + private final long factor; + private final String magnitudeString; + + Magnitude(long factor, String magnitudeString) + { + this.factor = factor; + this.magnitudeString = requireNonNull(magnitudeString, "magnitudeString is null"); + } + + long getFactor() + { + return factor; + } + + public String getMagnitudeString() + { + return magnitudeString; + } + } +} diff --git a/src/main/java/io/airlift/units/MaxCount.java b/src/main/java/io/airlift/units/MaxCount.java new file mode 100644 index 0000000..807a272 --- /dev/null +++ b/src/main/java/io/airlift/units/MaxCount.java @@ -0,0 +1,40 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.airlift.units; + +import javax.validation.Constraint; +import javax.validation.Payload; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Target({METHOD, ANNOTATION_TYPE}) +@Retention(RUNTIME) +@Documented +@Constraint(validatedBy = MaxCountValidator.class) +public @interface MaxCount +{ + String value(); + + String message() default "{io.airlift.units.MaxCount.message}"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/src/main/java/io/airlift/units/MaxCountValidator.java b/src/main/java/io/airlift/units/MaxCountValidator.java new file mode 100644 index 0000000..0e09ece --- /dev/null +++ b/src/main/java/io/airlift/units/MaxCountValidator.java @@ -0,0 +1,41 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.airlift.units; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +public class MaxCountValidator + implements ConstraintValidator +{ + private Count max; + + @Override + public void initialize(MaxCount count) + { + this.max = Count.valueOf(count.value()); + } + + @Override + public boolean isValid(Count count, ConstraintValidatorContext context) + { + return (count == null) || (count.compareTo(max) <= 0); + } + + @Override + public String toString() + { + return "max:" + max; + } +} diff --git a/src/main/java/io/airlift/units/MinCount.java b/src/main/java/io/airlift/units/MinCount.java new file mode 100644 index 0000000..db5006f --- /dev/null +++ b/src/main/java/io/airlift/units/MinCount.java @@ -0,0 +1,40 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.airlift.units; + +import javax.validation.Constraint; +import javax.validation.Payload; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Target({METHOD, ANNOTATION_TYPE}) +@Retention(RUNTIME) +@Documented +@Constraint(validatedBy = MinCountValidator.class) +public @interface MinCount +{ + String value(); + + String message() default "{io.airlift.units.MinCount.message}"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/src/main/java/io/airlift/units/MinCountValidator.java b/src/main/java/io/airlift/units/MinCountValidator.java new file mode 100644 index 0000000..2a0f86b --- /dev/null +++ b/src/main/java/io/airlift/units/MinCountValidator.java @@ -0,0 +1,41 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.airlift.units; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +public class MinCountValidator + implements ConstraintValidator +{ + private Count min; + + @Override + public void initialize(MinCount dataSize) + { + this.min = Count.valueOf(dataSize.value()); + } + + @Override + public boolean isValid(Count count, ConstraintValidatorContext context) + { + return (count == null) || (count.compareTo(min) >= 0); + } + + @Override + public String toString() + { + return "min:" + min; + } +} diff --git a/src/test/java/io/airlift/units/MockMaxCount.java b/src/test/java/io/airlift/units/MockMaxCount.java new file mode 100644 index 0000000..e2ccc5e --- /dev/null +++ b/src/test/java/io/airlift/units/MockMaxCount.java @@ -0,0 +1,62 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.airlift.units; + +import javax.validation.Payload; + +import java.lang.annotation.Annotation; + +import static java.util.Objects.requireNonNull; + +@SuppressWarnings("ClassExplicitlyAnnotation") +class MockMaxCount + implements MaxCount +{ + private final Count count; + + public MockMaxCount(Count count) + { + this.count = requireNonNull(count, "count is null"); + } + + @Override + public String value() + { + return count.toString(); + } + + @Override + public String message() + { + throw new UnsupportedOperationException(); + } + + @Override + public Class[] groups() + { + throw new UnsupportedOperationException(); + } + + @Override + public Class[] payload() + { + throw new UnsupportedOperationException(); + } + + @Override + public Class annotationType() + { + return MaxDuration.class; + } +} diff --git a/src/test/java/io/airlift/units/MockMinCount.java b/src/test/java/io/airlift/units/MockMinCount.java new file mode 100644 index 0000000..378f367 --- /dev/null +++ b/src/test/java/io/airlift/units/MockMinCount.java @@ -0,0 +1,62 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.airlift.units; + +import javax.validation.Payload; + +import java.lang.annotation.Annotation; + +import static java.util.Objects.requireNonNull; + +@SuppressWarnings("ClassExplicitlyAnnotation") +class MockMinCount + implements MinCount +{ + private final Count count; + + public MockMinCount(Count count) + { + this.count = requireNonNull(count, "count is null"); + } + + @Override + public String value() + { + return count.toString(); + } + + @Override + public String message() + { + throw new UnsupportedOperationException(); + } + + @Override + public Class[] groups() + { + throw new UnsupportedOperationException(); + } + + @Override + public Class[] payload() + { + throw new UnsupportedOperationException(); + } + + @Override + public Class annotationType() + { + return MinCount.class; + } +} diff --git a/src/test/java/io/airlift/units/TestCount.java b/src/test/java/io/airlift/units/TestCount.java new file mode 100644 index 0000000..f03ab7f --- /dev/null +++ b/src/test/java/io/airlift/units/TestCount.java @@ -0,0 +1,291 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.airlift.units; + +import com.google.common.collect.ImmutableList; +import io.airlift.json.JsonCodec; +import io.airlift.units.Count.Magnitude; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.util.Locale; + +import static io.airlift.testing.EquivalenceTester.comparisonTester; +import static io.airlift.units.Count.Magnitude.BILLION; +import static io.airlift.units.Count.Magnitude.MILLION; +import static io.airlift.units.Count.Magnitude.QUADRILLION; +import static io.airlift.units.Count.Magnitude.SINGLE; +import static io.airlift.units.Count.Magnitude.THOUSAND; +import static io.airlift.units.Count.Magnitude.TRILION; +import static io.airlift.units.Count.succinctRounded; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.data.Percentage.withPercentage; +import static org.testng.Assert.assertEquals; + +public class TestCount +{ + @Test + public void testSuccinctFactories() + { + assertEquals(succinctRounded(123), new Count(123, SINGLE)); + assertEquals(succinctRounded(5 * 1000 * 1000), new Count(5, MILLION)); + + assertEquals(succinctRounded(123, SINGLE), new Count(123, SINGLE)); + assertEquals(succinctRounded(5 * 1000, THOUSAND), new Count(5, MILLION)); + } + + @Test(dataProvider = "conversions") + public void testConversions(Magnitude magnitude, Magnitude toMagnitude, long factor) + { + Count count = new Count(1, magnitude).convertTo(toMagnitude); + assertEquals(count.getMagnitude(), toMagnitude); + assertEquals(count.getValue(), factor); + + assertEquals(count.getValue(toMagnitude), factor); + } + + @Test(dataProvider = "conversions") + public void testConvertToMostSuccinctCountRounded(Magnitude magnitude, Magnitude toMagnitude, long factor) + { + Count count = new Count(factor, toMagnitude); + Count actual = count.convertToMostSuccinctRounded(); + assertThat(actual.getValue()).isEqualTo(1); + assertThat(actual.getMagnitude()).isEqualTo(magnitude); + assertThat(actual.getValue(magnitude)).isEqualTo(1); + assertThat(actual.getMagnitude()).isEqualTo(magnitude); + } + + + @Test + public void testConvertToMostSuccinctRounded() + { + assertThat(new Count(1_499, SINGLE).convertToMostSuccinctRounded()).isEqualTo(new Count(1, THOUSAND)); + assertThat(new Count(1_500, SINGLE).convertToMostSuccinctRounded()).isEqualTo(new Count(2, THOUSAND)); + } + + @SuppressWarnings("ResultOfObjectAllocationIgnored") + @Test(dataProvider = "precisionLossConversions", expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = ".*precision loss") + public void testPrecisionLossConversions(Magnitude magnitude, Magnitude toMagnitude) + { + new Count(1, magnitude).convertTo(toMagnitude); + } + + @Test + public void testEquivalence() + { + comparisonTester() + .addLesserGroup(group(1_000_000)) + .addGreaterGroup(group(2_000_000)) + .addGreaterGroup(group(1_000_000_000)) + .check(); + } + + private static Iterable group(long count) + { + return ImmutableList.of( + new Count(count, SINGLE), + new Count(count / 1000, THOUSAND), + new Count(count / 1000 / 1000, MILLION) + ); + } + + @Test(dataProvider = "printedValues") + public void testToString(String expectedString, long value, Magnitude magnitude) + { + assertEquals(new Count(value, magnitude).toString(), expectedString); + } + + @Test(dataProvider = "printedValues") + public void testNonEnglishLocale(String expectedString, long value, Magnitude magnitude) + { + synchronized (Locale.class) { + Locale previous = Locale.getDefault(); + Locale.setDefault(Locale.GERMAN); + try { + assertEquals(new Count(value, magnitude).toString(), expectedString); + } + finally { + Locale.setDefault(previous); + } + } + } + + @Test(dataProvider = "parseableValues") + public void testValueOf(String string, long expectedValue, Magnitude expectedMagnitude) + { + Count count = Count.valueOf(string); + + assertEquals(count.getMagnitude(), expectedMagnitude); + assertEquals(count.getValue(), expectedValue); + } + + @Test(expectedExceptions = NullPointerException.class, expectedExceptionsMessageRegExp = "count is null") + public void testValueOfRejectsNull() + { + Count.valueOf(null); + } + + @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "count is empty") + public void testValueOfRejectsEmptyString() + { + Count.valueOf(""); + } + + @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Unknown magnitude: x") + public void testValueOfRejectsInvalidUnit() + { + Count.valueOf("1234 x"); + } + + @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Not a valid count string.*") + public void testValueOfRejectsInvalidNumber() + { + Count.valueOf("1.24 B"); + } + + @SuppressWarnings("ResultOfObjectAllocationIgnored") + @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "count is negative") + public void testConstructorRejectsNegativeSize() + { + new Count(-1, SINGLE); + } + + @Test + public void testMaxValue() + { + new Count(Long.MAX_VALUE, SINGLE); + } + + @SuppressWarnings("ResultOfObjectAllocationIgnored") + @Test(expectedExceptions = NullPointerException.class, expectedExceptionsMessageRegExp = "magnitude is null") + public void testConstructorRejectsNullUnit() + { + new Count(1, null); + } + + @Test + public void testJsonRoundTrip() + { + assertJsonRoundTrip(new Count(1234, SINGLE)); + assertJsonRoundTrip(new Count(1234, THOUSAND)); + assertJsonRoundTrip(new Count(1234, MILLION)); + assertJsonRoundTrip(new Count(1234, BILLION)); + assertJsonRoundTrip(new Count(1234, TRILION)); + assertJsonRoundTrip(new Count(1234, QUADRILLION)); + } + + private static void assertJsonRoundTrip(Count count) + { + JsonCodec dataSizeCodec = JsonCodec.jsonCodec(Count.class); + String json = dataSizeCodec.toJson(count); + Count dataSizeCopy = dataSizeCodec.fromJson(json); + + assertThat(dataSizeCopy.getValue(SINGLE)) + .isCloseTo(count.getValue(SINGLE), withPercentage(1)); + } + + @DataProvider(name = "parseableValues", parallel = true) + private Object[][] parseableValues() + { + return new Object[][] { + // spaces + new Object[] {"1000", 1000, SINGLE}, + new Object[] {"1000 K", 1000, THOUSAND}, + new Object[] {"1000 M", 1000, MILLION}, + new Object[] {"1000 B", 1000, BILLION}, + new Object[] {"1000 T", 1000, TRILION}, + new Object[] {"1000 P", 1000, QUADRILLION}, + // no spaces + new Object[] {"1000", 1000, SINGLE}, + new Object[] {"1000K", 1000, THOUSAND}, + new Object[] {"1000M", 1000, MILLION}, + new Object[] {"1000B", 1000, BILLION}, + new Object[] {"1000T", 1000, TRILION}, + new Object[] {"1000P", 1000, QUADRILLION}, + }; + } + + @DataProvider(name = "printedValues", parallel = true) + private Object[][] printedValues() + { + return new Object[][] { + new Object[] {"1000", 1000, SINGLE}, + new Object[] {"1000K", 1000, THOUSAND}, + new Object[] {"1000M", 1000, MILLION}, + new Object[] {"1000B", 1000, BILLION}, + new Object[] {"1000T", 1000, TRILION}, + new Object[] {"1000P", 1000, QUADRILLION}, + }; + } + + @DataProvider(name = "conversions", parallel = true) + private Object[][] conversions() + { + return new Object[][] { + + new Object[] {SINGLE, SINGLE, 1}, + + new Object[] {THOUSAND, SINGLE, 1000}, + new Object[] {THOUSAND, THOUSAND, 1}, + + new Object[] {MILLION, SINGLE, 1000_000}, + new Object[] {MILLION, THOUSAND, 1000}, + new Object[] {MILLION, MILLION, 1}, + + new Object[] {BILLION, SINGLE, 1000_000_000}, + new Object[] {BILLION, THOUSAND, 1000_000}, + new Object[] {BILLION, MILLION, 1000}, + new Object[] {BILLION, BILLION, 1}, + + new Object[] {TRILION, SINGLE, 1000_000_000_000L}, + new Object[] {TRILION, THOUSAND, 1000_000_000}, + new Object[] {TRILION, MILLION, 1000_000}, + new Object[] {TRILION, BILLION, 1000}, + new Object[] {TRILION, TRILION, 1}, + + new Object[] {QUADRILLION, SINGLE, 1000_000_000_000_000L}, + new Object[] {QUADRILLION, THOUSAND, 1000_000_000_000L}, + new Object[] {QUADRILLION, MILLION, 1000_000_000}, + new Object[] {QUADRILLION, BILLION, 1000_000}, + new Object[] {QUADRILLION, TRILION, 1000}, + new Object[] {QUADRILLION, QUADRILLION, 1}, + }; + } + + @DataProvider(name = "precisionLossConversions", parallel = true) + private Object[][] precisionLossConversions() + { + return new Object[][] { + new Object[] {SINGLE, THOUSAND}, + new Object[] {SINGLE, MILLION}, + new Object[] {SINGLE, BILLION}, + new Object[] {SINGLE, TRILION}, + new Object[] {SINGLE, QUADRILLION}, + + new Object[] {THOUSAND, MILLION}, + new Object[] {THOUSAND, BILLION}, + new Object[] {THOUSAND, TRILION}, + new Object[] {THOUSAND, QUADRILLION}, + + new Object[] {MILLION, BILLION}, + new Object[] {MILLION, TRILION}, + new Object[] {MILLION, QUADRILLION}, + + new Object[] {BILLION, TRILION}, + new Object[] {BILLION, QUADRILLION}, + + new Object[] {TRILION, QUADRILLION}, + }; + } +} diff --git a/src/test/java/io/airlift/units/TestCountValidator.java b/src/test/java/io/airlift/units/TestCountValidator.java new file mode 100644 index 0000000..be5dca7 --- /dev/null +++ b/src/test/java/io/airlift/units/TestCountValidator.java @@ -0,0 +1,205 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.airlift.units; + +import org.apache.bval.jsr.ApacheValidationProvider; +import org.testng.annotations.Test; + +import javax.validation.ConstraintViolation; +import javax.validation.Validation; +import javax.validation.ValidationException; +import javax.validation.Validator; + +import java.util.Set; + +import static io.airlift.units.ConstraintValidatorAssert.assertThat; +import static io.airlift.units.Count.Magnitude.BILLION; +import static io.airlift.units.Count.Magnitude.MILLION; +import static io.airlift.units.Count.Magnitude.THOUSAND; +import static org.assertj.core.api.Assertions.assertThat; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.fail; + +public class TestCountValidator +{ + private static final Validator VALIDATOR = Validation.byProvider(ApacheValidationProvider.class).configure().buildValidatorFactory().getValidator(); + + @Test + public void testMaxCountValidator() + { + MaxCountValidator maxValidator = new MaxCountValidator(); + maxValidator.initialize(new MockMaxCount(new Count(8, MILLION))); + + assertThat(maxValidator).isValidFor(new Count(0, THOUSAND)); + assertThat(maxValidator).isValidFor(new Count(5, THOUSAND)); + assertThat(maxValidator).isValidFor(new Count(5005, THOUSAND)); + assertThat(maxValidator).isValidFor(new Count(5, MILLION)); + assertThat(maxValidator).isValidFor(new Count(8, MILLION)); + assertThat(maxValidator).isValidFor(new Count(8000, THOUSAND)); + assertThat(maxValidator).isInvalidFor(new Count(8001, THOUSAND)); + assertThat(maxValidator).isInvalidFor(new Count(9, MILLION)); + assertThat(maxValidator).isInvalidFor(new Count(1, BILLION)); + } + + @Test + public void testMinCountValidator() + { + MinCountValidator minValidator = new MinCountValidator(); + minValidator.initialize(new MockMinCount(new Count(4, MILLION))); + + assertThat(minValidator).isValidFor(new Count(4, MILLION)); + assertThat(minValidator).isValidFor(new Count(4096, THOUSAND)); + assertThat(minValidator).isValidFor(new Count(5, MILLION)); + assertThat(minValidator).isInvalidFor(new Count(0, BILLION)); + assertThat(minValidator).isInvalidFor(new Count(1, MILLION)); + } + + @Test + public void testAllowsNullMinAnnotation() + { + VALIDATOR.validate(new NullMinAnnotation()); + } + + @Test + public void testAllowsNullMaxAnnotation() + { + VALIDATOR.validate(new NullMaxAnnotation()); + } + + @Test + public void testDetectsBrokenMinAnnotation() + { + try { + VALIDATOR.validate(new BrokenMinAnnotation()); + fail("expected a ValidationException caused by an IllegalArgumentException"); + } + catch (ValidationException e) { + assertThat(e).hasRootCauseInstanceOf(IllegalArgumentException.class); + } + } + + @Test + public void testDetectsBrokenMaxAnnotation() + { + try { + VALIDATOR.validate(new BrokenMaxAnnotation()); + fail("expected a ValidationException caused by an IllegalArgumentException"); + } + catch (ValidationException e) { + assertThat(e).hasRootCauseInstanceOf(IllegalArgumentException.class); + } + } + + @Test + public void testPassesValidation() + { + ConstrainedCount object = new ConstrainedCount(new Count(7, MILLION)); + Set> violations = VALIDATOR.validate(object); + assertTrue(violations.isEmpty()); + } + + @Test + public void testFailsMaxCountConstraint() + { + ConstrainedCount object = new ConstrainedCount(new Count(11, MILLION)); + Set> violations = VALIDATOR.validate(object); + assertThat(violations).hasSize(2); + + for (ConstraintViolation violation : violations) { + assertThat(violation.getConstraintDescriptor().getAnnotation()).isInstanceOf(MaxCount.class); + } + } + + @Test + public void testFailsMinCountConstraint() + { + ConstrainedCount object = new ConstrainedCount(new Count(1, MILLION)); + Set> violations = VALIDATOR.validate(object); + assertThat(violations).hasSize(2); + + for (ConstraintViolation violation : violations) { + assertThat(violation.getConstraintDescriptor().getAnnotation()).isInstanceOf(MinCount.class); + } + } + + @SuppressWarnings("UnusedDeclaration") + public static class ConstrainedCount + { + private final Count count; + + public ConstrainedCount(Count count) + { + this.count = count; + } + + @MinCount("5M") + public Count getConstrainedByMin() + { + return count; + } + + @MaxCount("10M") + public Count getConstrainedByMax() + { + return count; + } + + @MinCount("5000K") + @MaxCount("10000K") + public Count getConstrainedByMinAndMax() + { + return count; + } + } + + @SuppressWarnings("UnusedDeclaration") + public static class NullMinAnnotation + { + @MinCount("1M") + public Count getConstrainedByMin() + { + return null; + } + } + + @SuppressWarnings("UnusedDeclaration") + public static class NullMaxAnnotation + { + @MaxCount("1M") + public Count getConstrainedByMin() + { + return null; + } + } + + @SuppressWarnings("UnusedDeclaration") + public static class BrokenMinAnnotation + { + @MinCount("broken") + public Count getConstrainedByMin() + { + return new Count(32, THOUSAND); + } + } + + @SuppressWarnings("UnusedDeclaration") + public static class BrokenMaxAnnotation + { + @MinCount("broken") + public Count getConstrainedByMin() + { + return new Count(32, THOUSAND); + } + } +}