Normalize Decimal64 scale in range constraints 65/101765/27
authorOleksandrZharov <Oleksandr.Zharov@pantheon.tech>
Thu, 7 Jul 2022 11:08:47 +0000 (13:08 +0200)
committerRobert Varga <nite@hq.sk>
Wed, 10 Aug 2022 09:23:55 +0000 (09:23 +0000)
Modified logic of DecimalTypeBuilder to ensure proper scale
of fraction-digits. Updated test in order to ensure that range
constraints are correct.

JIRA: YANGTOOLS-1441
Change-Id: I01ac7c7d940119f76867b15a3aaff50b3cd983e0
Signed-off-by: OleksandrZharov <Oleksandr.Zharov@pantheon.tech>
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
model/yang-model-ri/src/main/java/org/opendaylight/yangtools/yang/model/ri/type/DecimalTypeBuilder.java
model/yang-model-ri/src/main/java/org/opendaylight/yangtools/yang/model/ri/type/RangeRestrictedTypeBuilder.java
model/yang-model-ri/src/main/java/org/opendaylight/yangtools/yang/model/ri/type/RangeRestrictedTypeBuilderWithBase.java
parser/yang-parser-rfc7950/src/main/java/org/opendaylight/yangtools/yang/parser/rfc7950/stmt/type/Decimal64SpecificationSupport.java
parser/yang-parser-rfc7950/src/test/java/org/opendaylight/yangtools/yang/stmt/TypedefConstraintsTest.java
parser/yang-parser-rfc7950/src/test/java/org/opendaylight/yangtools/yang/stmt/YT1441Test.java [new file with mode: 0644]
parser/yang-parser-rfc7950/src/test/resources/bugs/YT1441/foo.yang [new file with mode: 0644]

index cc199996a1fa0c2b8a7cbfb72032a16316a31c15..6be391842a9e57fb9ced24cb5fc300f403b7c0a0 100644 (file)
@@ -9,8 +9,16 @@ package org.opendaylight.yangtools.yang.model.ri.type;
 
 import static com.google.common.base.Preconditions.checkState;
 
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableRangeSet;
+import com.google.common.collect.Range;
+import com.google.common.collect.RangeSet;
+import java.util.Set;
+import org.eclipse.jdt.annotation.NonNull;
 import org.opendaylight.yangtools.yang.common.Decimal64;
 import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.model.api.ConstraintMetaDefinition;
+import org.opendaylight.yangtools.yang.model.api.stmt.ValueRange;
 import org.opendaylight.yangtools.yang.model.api.type.DecimalTypeDefinition;
 
 public final class DecimalTypeBuilder extends RangeRestrictedTypeBuilder<DecimalTypeDefinition, Decimal64> {
@@ -27,9 +35,58 @@ public final class DecimalTypeBuilder extends RangeRestrictedTypeBuilder<Decimal
     }
 
     @Override
-    DecimalTypeDefinition buildType() {
-        checkState(fractionDigits != null, "Fraction digits not defined");
-        return new BaseDecimalType(getQName(), getUnknownSchemaNodes(), fractionDigits,
-            calculateRangeConstraint(BaseDecimalType.constraintsForDigits(fractionDigits)));
+    DecimalTypeDefinition buildConstrainedType(final ConstraintMetaDefinition constraint,
+            final ImmutableList<ValueRange> ranges) {
+        final int scale = scale();
+        return new BaseDecimalType(getQName(), getUnknownSchemaNodes(), scale,
+            new ResolvedRangeConstraint<>(constraint, ensureResolvedScale(
+                calculateRanges(BaseDecimalType.constraintsForDigits(scale), ranges), scale)));
+    }
+
+    @Override
+    DecimalTypeDefinition buildUnconstrainedType() {
+        final int scale = scale();
+        return new BaseDecimalType(getQName(), getUnknownSchemaNodes(), scale,
+            BaseDecimalType.constraintsForDigits(scale));
+    }
+
+    private int scale() {
+        final var local = fractionDigits;
+        checkState(local != null, "Fraction digits not defined");
+        return local;
+    }
+
+    private static @NonNull RangeSet<Decimal64> ensureResolvedScale(final RangeSet<Decimal64> ranges, final int scale) {
+        // Check if we need to resolve anything at all
+        final var rangeSet = ranges.asRanges();
+        for (final var range : rangeSet) {
+            if (range.lowerEndpoint().scale() != scale || range.upperEndpoint().scale() != scale) {
+                return resolveScale(rangeSet, scale);
+            }
+        }
+
+        // Everything is good - return same ranges
+        return ranges;
+    }
+
+    private static @NonNull RangeSet<Decimal64> resolveScale(final Set<Range<Decimal64>> ranges, final int scale) {
+        final var builder = ImmutableRangeSet.<Decimal64>builder();
+        for (final var range : ranges) {
+            boolean reuse = true;
+
+            var lower = range.lowerEndpoint();
+            if (lower.scale() != scale) {
+                lower = lower.scaleTo(scale);
+                reuse = false;
+            }
+            var upper = range.upperEndpoint();
+            if (upper.scale() != scale) {
+                upper = upper.scaleTo(scale);
+                reuse = false;
+            }
+            builder.add(reuse ? range : Range.closed(lower, upper));
+        }
+
+        return builder.build();
     }
 }
index 0727f648ce6b039bafdd7717bdd91e3ec6486ce1..fbdae973e5c1fd42ac05d0b4a9eb75bd933cd86d 100644 (file)
@@ -7,11 +7,12 @@
  */
 package org.opendaylight.yangtools.yang.model.ri.type;
 
+import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.base.Verify.verify;
+import static com.google.common.base.Verify.verifyNotNull;
 import static java.util.Objects.requireNonNull;
 
-import com.google.common.base.Preconditions;
-import com.google.common.base.Verify;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableRangeSet;
 import com.google.common.collect.ImmutableRangeSet.Builder;
@@ -47,17 +48,26 @@ public abstract class RangeRestrictedTypeBuilder<T extends RangeRestrictedTypeDe
         touch();
     }
 
-    final RangeConstraint<N> calculateRangeConstraint(final RangeConstraint<N> baseRangeConstraint) {
-        if (ranges == null) {
-            return baseRangeConstraint;
-        }
+    @Override
+    final T buildType() {
+        final var localRanges = ranges;
+        return localRanges == null ? buildUnconstrainedType()
+            : buildConstrainedType(verifyNotNull(constraint), localRanges);
+    }
+
+    abstract @NonNull T buildConstrainedType(@NonNull ConstraintMetaDefinition constraint,
+        @NonNull ImmutableList<ValueRange> ranges);
 
+    abstract @NonNull T buildUnconstrainedType();
+
+    final RangeSet<N> calculateRanges(final @NonNull RangeConstraint<N> baseConstraint,
+            final @NonNull ImmutableList<ValueRange> contraintRanges) {
         // Run through alternatives and resolve them against the base type
-        final RangeSet<N> baseRangeSet = baseRangeConstraint.getAllowedRanges();
-        Verify.verify(!baseRangeSet.isEmpty(), "Base type %s does not define constraints", getBaseType());
+        final RangeSet<N> baseRangeSet = baseConstraint.getAllowedRanges();
+        verify(!baseRangeSet.isEmpty(), "Base type %s does not define constraints", getBaseType());
 
         final Range<N> baseRange = baseRangeSet.span();
-        final List<ValueRange> resolvedRanges = ensureResolvedRanges(ranges, baseRange);
+        final List<ValueRange> resolvedRanges = ensureResolvedRanges(contraintRanges, baseRange);
 
         // Next up, ensure the of boundaries match base constraints
         final RangeSet<N> typedRanges = ensureTypedRanges(resolvedRanges, baseRange.lowerEndpoint().getClass());
@@ -67,8 +77,7 @@ public abstract class RangeRestrictedTypeBuilder<T extends RangeRestrictedTypeDe
             throw new InvalidRangeConstraintException(typedRanges,
                 "Range constraint %s is not a subset of parent constraint %s", typedRanges, baseRangeSet);
         }
-
-        return new ResolvedRangeConstraint<>(constraint, typedRanges);
+        return typedRanges;
     }
 
     private static <C extends Number & Comparable<C>> List<ValueRange> ensureResolvedRanges(
@@ -108,7 +117,7 @@ public abstract class RangeRestrictedTypeBuilder<T extends RangeRestrictedTypeDe
     @SuppressWarnings("unchecked")
     private static <T extends Number & Comparable<T>> RangeSet<T> ensureTypedRanges(final List<ValueRange> ranges,
             final Class<? extends Number> clazz) {
-        final Builder<T> builder = ImmutableRangeSet.builder();
+        final Builder<T> builder = ImmutableRangeSet.<T>builder();
         for (ValueRange range : ranges) {
             if (!clazz.isInstance(range.lowerBound()) || !clazz.isInstance(range.upperBound())) {
                 return typedRanges(ranges, clazz);
@@ -124,7 +133,7 @@ public abstract class RangeRestrictedTypeBuilder<T extends RangeRestrictedTypeDe
     private static <T extends Number & Comparable<T>> RangeSet<T> typedRanges(final List<ValueRange> ranges,
             final Class<? extends Number> clazz) {
         final Function<Number, ? extends Number> function = NumberUtil.converterTo(clazz);
-        Preconditions.checkArgument(function != null, "Unsupported range class %s", clazz);
+        checkArgument(function != null, "Unsupported range class %s", clazz);
 
         final Builder<T> builder = ImmutableRangeSet.builder();
 
index e9b7a1f509649bdd91533c84cc4c0b43979aedb2..a11b836907822e2821dff7908ce83d9bdcb03186 100644 (file)
@@ -9,8 +9,11 @@ package org.opendaylight.yangtools.yang.model.ri.type;
 
 import static java.util.Objects.requireNonNull;
 
+import com.google.common.collect.ImmutableList;
 import org.eclipse.jdt.annotation.NonNull;
 import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.model.api.ConstraintMetaDefinition;
+import org.opendaylight.yangtools.yang.model.api.stmt.ValueRange;
 import org.opendaylight.yangtools.yang.model.api.type.RangeConstraint;
 import org.opendaylight.yangtools.yang.model.api.type.RangeRestrictedTypeDefinition;
 
@@ -21,9 +24,19 @@ abstract class RangeRestrictedTypeBuilderWithBase<T extends RangeRestrictedTypeD
     }
 
     @Override
-    final T buildType() {
-        return buildType(calculateRangeConstraint(getBaseType().getRangeConstraint().get()));
+    final T buildConstrainedType(final ConstraintMetaDefinition constraint,
+            final ImmutableList<ValueRange> ranges) {
+        return buildType(new ResolvedRangeConstraint<>(constraint, calculateRanges(getBaseRangeConstraint(), ranges)));
+    }
+
+    @Override
+    final T buildUnconstrainedType() {
+        return buildType(getBaseRangeConstraint());
     }
 
     abstract @NonNull T buildType(RangeConstraint<N> rangeConstraints);
+
+    private RangeConstraint<N> getBaseRangeConstraint() {
+        return getBaseType().getRangeConstraint().orElseThrow();
+    }
 }
index e2e7066f725a5f6d7e25a324d590e45b1dce5ee4..90ecd881754114d2d9703dbf5a34cca99d0d919d 100644 (file)
@@ -61,16 +61,19 @@ final class Decimal64SpecificationSupport extends AbstractTypeSupport<Decimal64S
 
         final DecimalTypeBuilder builder = BaseTypes.decimalTypeBuilder(stmt.argumentAsTypeQName());
         for (final EffectiveStatement<?, ?> subStmt : substatements) {
-            if (subStmt instanceof FractionDigitsEffectiveStatement) {
-                builder.setFractionDigits(((FractionDigitsEffectiveStatement) subStmt).argument());
+            if (subStmt instanceof FractionDigitsEffectiveStatement fracDigits) {
+                builder.setFractionDigits(fracDigits.argument());
             }
-            if (subStmt instanceof RangeEffectiveStatement) {
-                final RangeEffectiveStatement range = (RangeEffectiveStatement) subStmt;
+            if (subStmt instanceof RangeEffectiveStatement range) {
                 builder.setRangeConstraint(range, range.argument());
             }
         }
 
-        return new TypeEffectiveStatementImpl<>(stmt.declared(), substatements, builder);
+        try {
+            return new TypeEffectiveStatementImpl<>(stmt.declared(), substatements, builder);
+        } catch (ArithmeticException e) {
+            throw new SourceException("Range constraint does not match fraction-digits: " + e.getMessage(), stmt, e);
+        }
     }
 
     private static SourceException noFracDigits(final CommonStmtCtx stmt) {
index f51d9d340b1e5951d496443dad608f3d97ab1451..a2a755dde91602907277fddbd44b13ec87d3ac25 100644 (file)
@@ -56,15 +56,18 @@ public class TypedefConstraintsTest extends AbstractYangTest {
 
         assertTrue(type instanceof DecimalTypeDefinition);
         final DecimalTypeDefinition decType = (DecimalTypeDefinition) type;
+        assertEquals(4, decType.getFractionDigits());
 
-        final Set<? extends Range<?>> decRangeConstraints = decType.getRangeConstraint().get().getAllowedRanges()
-                .asRanges();
+        final Set<? extends Range<Decimal64>> decRangeConstraints = decType.getRangeConstraint().get()
+                .getAllowedRanges().asRanges();
 
         assertEquals(1, decRangeConstraints.size());
 
-        final Range<?> range = decRangeConstraints.iterator().next();
-        assertEquals(Decimal64.valueOf("1.5"), range.lowerEndpoint());
-        assertEquals(Decimal64.valueOf("5.5"), range.upperEndpoint());
+        final Range<Decimal64> range = decRangeConstraints.iterator().next();
+        assertEquals(Decimal64.of(4, 15000), range.lowerEndpoint());
+        assertEquals(4, range.lowerEndpoint().scale());
+        assertEquals(Decimal64.of(4, 55000), range.upperEndpoint());
+        assertEquals(4, range.upperEndpoint().scale());
 
         assertEquals(TypeDefinitions.DECIMAL64.bindTo(leafDecimal.getQName().getModule()), decType.getQName());
         assertNull(decType.getBaseType());
diff --git a/parser/yang-parser-rfc7950/src/test/java/org/opendaylight/yangtools/yang/stmt/YT1441Test.java b/parser/yang-parser-rfc7950/src/test/java/org/opendaylight/yangtools/yang/stmt/YT1441Test.java
new file mode 100644 (file)
index 0000000..858674c
--- /dev/null
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2022 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.yang.stmt;
+
+import static org.hamcrest.CoreMatchers.startsWith;
+
+import org.junit.Test;
+
+public class YT1441Test extends AbstractYangTest {
+    @Test
+    public void testInvalidRange() {
+        assertSourceException(startsWith(
+            "Range constraint does not match fraction-digits: Decreasing scale of 2.345 to 2 requires rounding [at "),
+            "/bugs/YT1441/foo.yang");
+    }
+}
diff --git a/parser/yang-parser-rfc7950/src/test/resources/bugs/YT1441/foo.yang b/parser/yang-parser-rfc7950/src/test/resources/bugs/YT1441/foo.yang
new file mode 100644 (file)
index 0000000..f0c22f7
--- /dev/null
@@ -0,0 +1,11 @@
+module foo {
+  namespace foo;
+  prefix foo;
+
+  typedef foo {
+    type decimal64 {
+      fraction-digits 2;
+      range 1..2.345;
+    }
+  }
+}