From 8b6f4c6156e83d65583f569722e886436fbbcd11 Mon Sep 17 00:00:00 2001 From: Havoc Pennington Date: Fri, 18 Nov 2011 16:12:46 -0500 Subject: [PATCH] Refactor withFallback() implementations to share more logic among subtypes Also add some tests and fix some correctness issues. If we fall back to a value that has ignoresFallbacks()=true, then we need to "catch" its ignoresFallbacks. Don't create new RootConfig objects if the underlying has not changed. Get rid of ConfigImpl.merge() utility, no longer useful. --- .../config/impl/AbstractConfigObject.java | 93 ++++++++++--------- .../config/impl/AbstractConfigValue.java | 50 +++++++++- .../config/impl/ConfigDelayedMerge.java | 72 +++++++------- .../config/impl/ConfigDelayedMergeObject.java | 50 +++++----- .../com/typesafe/config/impl/ConfigImpl.java | 17 ---- .../config/impl/ConfigSubstitution.java | 56 ++++++----- .../com/typesafe/config/impl/RootConfig.java | 6 +- .../typesafe/config/impl/SimpleConfig.java | 7 ++ .../config/impl/SimpleConfigObject.java | 6 +- .../com/typesafe/config/impl/ConfigTest.scala | 61 +++++++++++- 10 files changed, 268 insertions(+), 150 deletions(-) diff --git a/src/main/java/com/typesafe/config/impl/AbstractConfigObject.java b/src/main/java/com/typesafe/config/impl/AbstractConfigObject.java index 8bf81f5b..d9d41e3f 100644 --- a/src/main/java/com/typesafe/config/impl/AbstractConfigObject.java +++ b/src/main/java/com/typesafe/config/impl/AbstractConfigObject.java @@ -106,50 +106,57 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements boolean ignoresFallbacks); @Override - public AbstractConfigObject withFallback(ConfigMergeable mergeable) { - ConfigValue other = mergeable.toValue(); + protected AbstractConfigObject newCopy(boolean ignoresFallbacks) { + return newCopy(resolveStatus(), ignoresFallbacks); + } - if (ignoresFallbacks()) { - return this; - } else if (other instanceof Unmergeable) { - List stack = new ArrayList(); - stack.add(this); - stack.addAll(((Unmergeable) other).unmergedValues()); - return new ConfigDelayedMergeObject(mergeOrigins(stack), stack); - } else if (other instanceof AbstractConfigObject) { - AbstractConfigObject fallback = (AbstractConfigObject) other; - if (fallback.isEmpty()) { - return this; // nothing to do - } else { - boolean allResolved = true; - Map merged = new HashMap(); - Set allKeys = new HashSet(); - allKeys.addAll(this.keySet()); - allKeys.addAll(fallback.keySet()); - for (String key : allKeys) { - AbstractConfigValue first = this.peek(key); - AbstractConfigValue second = fallback.peek(key); - AbstractConfigValue kept; - if (first == null) - kept = second; - else if (second == null) - kept = first; - else - kept = first.withFallback(second); - merged.put(key, kept); - if (kept.resolveStatus() == ResolveStatus.UNRESOLVED) - allResolved = false; - } - return new SimpleConfigObject(mergeOrigins(this, fallback), - merged, ResolveStatus.fromBoolean(allResolved), - ignoresFallbacks()); - } + @Override + protected final AbstractConfigObject mergedWithTheUnmergeable(Unmergeable fallback) { + if (ignoresFallbacks()) + throw new ConfigException.BugOrBroken("should not be reached"); + + List stack = new ArrayList(); + if (this instanceof Unmergeable) { + stack.addAll(((Unmergeable) this).unmergedValues()); } else { - // falling back to a non-object doesn't merge anything, and also - // prohibits merging any objects that we fall back to later. - // so we have to switch to ignoresFallbacks mode. - return newCopy(resolveStatus(), true /* ignoresFallbacks */); + stack.add(this); } + stack.addAll(fallback.unmergedValues()); + return new ConfigDelayedMergeObject(mergeOrigins(stack), stack, + ((AbstractConfigValue) fallback).ignoresFallbacks()); + } + + @Override + protected AbstractConfigObject mergedWithObject(AbstractConfigObject fallback) { + if (ignoresFallbacks()) + throw new ConfigException.BugOrBroken("should not be reached"); + + boolean allResolved = true; + Map merged = new HashMap(); + Set allKeys = new HashSet(); + allKeys.addAll(this.keySet()); + allKeys.addAll(fallback.keySet()); + for (String key : allKeys) { + AbstractConfigValue first = this.peek(key); + AbstractConfigValue second = fallback.peek(key); + AbstractConfigValue kept; + if (first == null) + kept = second; + else if (second == null) + kept = first; + else + kept = first.withFallback(second); + merged.put(key, kept); + if (kept.resolveStatus() == ResolveStatus.UNRESOLVED) + allResolved = false; + } + return new SimpleConfigObject(mergeOrigins(this, fallback), merged, + ResolveStatus.fromBoolean(allResolved), fallback.ignoresFallbacks()); + } + + @Override + public AbstractConfigObject withFallback(ConfigMergeable mergeable) { + return (AbstractConfigObject) super.withFallback(mergeable); } static ConfigOrigin mergeOrigins( @@ -169,7 +176,9 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements if (desc.startsWith(prefix)) desc = desc.substring(prefix.length()); - if (v instanceof ConfigObject && ((ConfigObject) v).isEmpty()) { + if (v instanceof AbstractConfigObject + && ((AbstractConfigObject) v).resolveStatus() == ResolveStatus.RESOLVED + && ((ConfigObject) v).isEmpty()) { // don't include empty files or the .empty() // config in the description, since they are // likely to be "implementation details" diff --git a/src/main/java/com/typesafe/config/impl/AbstractConfigValue.java b/src/main/java/com/typesafe/config/impl/AbstractConfigValue.java index f4cf9e75..2125cf72 100644 --- a/src/main/java/com/typesafe/config/impl/AbstractConfigValue.java +++ b/src/main/java/com/typesafe/config/impl/AbstractConfigValue.java @@ -3,6 +3,7 @@ */ package com.typesafe.config.impl; +import com.typesafe.config.ConfigException; import com.typesafe.config.ConfigMergeable; import com.typesafe.config.ConfigOrigin; import com.typesafe.config.ConfigResolveOptions; @@ -75,6 +76,10 @@ abstract class AbstractConfigValue implements ConfigValue { return this; } + protected AbstractConfigValue newCopy(boolean ignoresFallbacks) { + return this; + } + // this is virtualized rather than a field because only some subclasses // really need to store the boolean, and they may be able to pack it // with another boolean to save space. @@ -82,9 +87,50 @@ abstract class AbstractConfigValue implements ConfigValue { return true; } + private ConfigException badMergeException() { + if (ignoresFallbacks()) + throw new ConfigException.BugOrBroken( + "method should not have been called with ignoresFallbacks=true" + + getClass().getSimpleName()); + else + throw new ConfigException.BugOrBroken("should override this in " + + getClass().getSimpleName()); + } + + protected AbstractConfigValue mergedWithTheUnmergeable(Unmergeable fallback) { + throw badMergeException(); + } + + protected AbstractConfigValue mergedWithObject(AbstractConfigObject fallback) { + throw badMergeException(); + } + @Override - public AbstractConfigValue withFallback(ConfigMergeable other) { - return this; + public AbstractConfigValue withFallback(ConfigMergeable mergeable) { + if (ignoresFallbacks()) { + return this; + } else { + ConfigValue other = mergeable.toValue(); + + if (other instanceof Unmergeable) { + return mergedWithTheUnmergeable((Unmergeable) other); + } else if (other instanceof AbstractConfigObject) { + AbstractConfigObject fallback = (AbstractConfigObject) other; + if (fallback.resolveStatus() == ResolveStatus.RESOLVED && fallback.isEmpty()) { + if (fallback.ignoresFallbacks()) + return newCopy(true /* ignoresFallbacks */); + else + return this; + } else { + return mergedWithObject((AbstractConfigObject) other); + } + } else { + // falling back to a non-object doesn't merge anything, and also + // prohibits merging any objects that we fall back to later. + // so we have to switch to ignoresFallbacks mode. + return newCopy(true /* ignoresFallbacks */); + } + } } protected boolean canEqual(Object other) { diff --git a/src/main/java/com/typesafe/config/impl/ConfigDelayedMerge.java b/src/main/java/com/typesafe/config/impl/ConfigDelayedMerge.java index 3b93b56f..0c20aa57 100644 --- a/src/main/java/com/typesafe/config/impl/ConfigDelayedMerge.java +++ b/src/main/java/com/typesafe/config/impl/ConfigDelayedMerge.java @@ -8,10 +8,8 @@ import java.util.Collection; import java.util.List; import com.typesafe.config.ConfigException; -import com.typesafe.config.ConfigMergeable; import com.typesafe.config.ConfigOrigin; import com.typesafe.config.ConfigResolveOptions; -import com.typesafe.config.ConfigValue; import com.typesafe.config.ConfigValueType; /** @@ -29,8 +27,8 @@ final class ConfigDelayedMerge extends AbstractConfigValue implements final private List stack; final private boolean ignoresFallbacks; - private ConfigDelayedMerge(ConfigOrigin origin, - List stack, boolean ignoresFallbacks) { + ConfigDelayedMerge(ConfigOrigin origin, List stack, + boolean ignoresFallbacks) { super(origin); this.stack = stack; this.ignoresFallbacks = ignoresFallbacks; @@ -38,6 +36,11 @@ final class ConfigDelayedMerge extends AbstractConfigValue implements throw new ConfigException.BugOrBroken( "creating empty delayed merge value"); + for (AbstractConfigValue v : stack) { + if (v instanceof ConfigDelayedMerge || v instanceof ConfigDelayedMergeObject) + throw new ConfigException.BugOrBroken( + "placed nested DelayedMerge in a ConfigDelayedMerge, should have consolidated stack"); + } } ConfigDelayedMerge(ConfigOrigin origin, List stack) { @@ -67,18 +70,19 @@ final class ConfigDelayedMerge extends AbstractConfigValue implements List stack, SubstitutionResolver resolver, int depth, ConfigResolveOptions options) { // to resolve substitutions, we need to recursively resolve - // the stack of stuff to merge, and then merge the stack. - List toMerge = new ArrayList(); + // the stack of stuff to merge, and merge the stack so + // we won't be a delayed merge anymore. + AbstractConfigValue merged = null; for (AbstractConfigValue v : stack) { AbstractConfigValue resolved = resolver.resolve(v, depth, options); - toMerge.add(resolved); + if (merged == null) + merged = resolved; + else + merged = merged.withFallback(resolved); } - // we shouldn't have a delayed merge object with an empty stack, so - // it should be safe to ignore the toMerge.isEmpty case. - return ConfigImpl.merge(AbstractConfigValue.class, toMerge.get(0), - toMerge.subList(1, toMerge.size())); + return merged; } @Override @@ -101,29 +105,31 @@ final class ConfigDelayedMerge extends AbstractConfigValue implements } @Override - public AbstractConfigValue withFallback(ConfigMergeable mergeable) { - ConfigValue other = mergeable.toValue(); + protected final ConfigDelayedMerge mergedWithTheUnmergeable(Unmergeable fallback) { + if (ignoresFallbacks) + throw new ConfigException.BugOrBroken("should not be reached"); - if (ignoresFallbacks) { - return this; - } else if (other instanceof AbstractConfigObject - || other instanceof Unmergeable) { - // if we turn out to be an object, and the fallback also does, - // then a merge may be required; delay until we resolve. - List newStack = new ArrayList(); - newStack.addAll(stack); - if (other instanceof Unmergeable) - newStack.addAll(((Unmergeable) other).unmergedValues()); - else - newStack.add((AbstractConfigValue) other); - return new ConfigDelayedMerge( - AbstractConfigObject.mergeOrigins(newStack), newStack, - ignoresFallbacks); - } else { - // if the other is not an object, there won't be anything - // to merge with, so we are it even if we are an object. - return new ConfigDelayedMerge(origin(), stack, true /* ignoresFallbacks */); - } + // if we turn out to be an object, and the fallback also does, + // then a merge may be required; delay until we resolve. + List newStack = new ArrayList(); + newStack.addAll(stack); + newStack.addAll(fallback.unmergedValues()); + return new ConfigDelayedMerge(AbstractConfigObject.mergeOrigins(newStack), newStack, + ((AbstractConfigValue) fallback).ignoresFallbacks()); + } + + @Override + protected final ConfigDelayedMerge mergedWithObject(AbstractConfigObject fallback) { + if (ignoresFallbacks) + throw new ConfigException.BugOrBroken("should not be reached"); + + // if we turn out to be an object, and the fallback also does, + // then a merge may be required; delay until we resolve. + List newStack = new ArrayList(); + newStack.addAll(stack); + newStack.add(fallback); + return new ConfigDelayedMerge(AbstractConfigObject.mergeOrigins(newStack), newStack, + fallback.ignoresFallbacks()); } @Override diff --git a/src/main/java/com/typesafe/config/impl/ConfigDelayedMergeObject.java b/src/main/java/com/typesafe/config/impl/ConfigDelayedMergeObject.java index 73051aa5..6f381afc 100644 --- a/src/main/java/com/typesafe/config/impl/ConfigDelayedMergeObject.java +++ b/src/main/java/com/typesafe/config/impl/ConfigDelayedMergeObject.java @@ -28,8 +28,8 @@ class ConfigDelayedMergeObject extends AbstractConfigObject implements this(origin, stack, false /* ignoresFallbacks */); } - private ConfigDelayedMergeObject(ConfigOrigin origin, - List stack, boolean ignoresFallbacks) { + ConfigDelayedMergeObject(ConfigOrigin origin, List stack, + boolean ignoresFallbacks) { super(origin); this.stack = stack; this.ignoresFallbacks = ignoresFallbacks; @@ -40,10 +40,16 @@ class ConfigDelayedMergeObject extends AbstractConfigObject implements if (!(stack.get(0) instanceof AbstractConfigObject)) throw new ConfigException.BugOrBroken( "created a delayed merge object not guaranteed to be an object"); + + for (AbstractConfigValue v : stack) { + if (v instanceof ConfigDelayedMerge || v instanceof ConfigDelayedMergeObject) + throw new ConfigException.BugOrBroken( + "placed nested DelayedMerge in a ConfigDelayedMergeObject, should have consolidated stack"); + } } @Override - public ConfigDelayedMergeObject newCopy(ResolveStatus status, + protected ConfigDelayedMergeObject newCopy(ResolveStatus status, boolean ignoresFallbacks) { if (status != resolveStatus()) throw new ConfigException.BugOrBroken( @@ -86,30 +92,22 @@ class ConfigDelayedMergeObject extends AbstractConfigObject implements } @Override - public ConfigDelayedMergeObject withFallback(ConfigMergeable mergeable) { - ConfigValue other = mergeable.toValue(); + protected ConfigDelayedMergeObject mergedWithObject(AbstractConfigObject fallback) { + if (ignoresFallbacks) + throw new ConfigException.BugOrBroken("should not be reached"); - if (ignoresFallbacks) { - return this; - } else if (other instanceof AbstractConfigObject - || other instanceof Unmergeable) { - // since we are an object, and the fallback could be, - // then a merge may be required; delay until we resolve. - List newStack = new ArrayList(); - newStack.addAll(stack); - if (other instanceof Unmergeable) - newStack.addAll(((Unmergeable) other).unmergedValues()); - else - newStack.add((AbstractConfigValue) other); - return new ConfigDelayedMergeObject( - AbstractConfigObject.mergeOrigins(newStack), newStack, - ignoresFallbacks); - } else { - // if the other is not an object, there won't be anything - // to merge with but we do need to ignore any fallbacks - // that occur after it. - return newCopy(resolveStatus(), true /* ignoresFallbacks */); - } + // since we are an object, and the fallback is, we'll need to + // merge the fallback once we resolve. + List newStack = new ArrayList(); + newStack.addAll(stack); + newStack.add(fallback); + return new ConfigDelayedMergeObject(AbstractConfigObject.mergeOrigins(newStack), newStack, + fallback.ignoresFallbacks()); + } + + @Override + public ConfigDelayedMergeObject withFallback(ConfigMergeable mergeable) { + return (ConfigDelayedMergeObject) super.withFallback(mergeable); } @Override diff --git a/src/main/java/com/typesafe/config/impl/ConfigImpl.java b/src/main/java/com/typesafe/config/impl/ConfigImpl.java index a8ee038f..8c2bd01e 100644 --- a/src/main/java/com/typesafe/config/impl/ConfigImpl.java +++ b/src/main/java/com/typesafe/config/impl/ConfigImpl.java @@ -5,7 +5,6 @@ package com.typesafe.config.impl; import java.io.File; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; @@ -16,7 +15,6 @@ import com.typesafe.config.Config; import com.typesafe.config.ConfigException; import com.typesafe.config.ConfigIncludeContext; import com.typesafe.config.ConfigIncluder; -import com.typesafe.config.ConfigMergeable; import com.typesafe.config.ConfigObject; import com.typesafe.config.ConfigOrigin; import com.typesafe.config.ConfigParseOptions; @@ -28,21 +26,6 @@ import com.typesafe.config.ConfigValue; /** This is public but is only supposed to be used by the "config" package */ public class ConfigImpl { - static T merge(Class klass, T first, - ConfigMergeable... others) { - List stack = Arrays.asList(others); - return merge(klass, first, stack); - } - - static T merge(Class klass, T first, - List stack) { - ConfigMergeable merged = first; - for (ConfigMergeable fallback : stack) { - merged = merged.withFallback(fallback); - } - return klass.cast(merged); - } - private interface NameSource { ConfigParseable nameToParseable(String name); } diff --git a/src/main/java/com/typesafe/config/impl/ConfigSubstitution.java b/src/main/java/com/typesafe/config/impl/ConfigSubstitution.java index 87492e94..2c5531e7 100644 --- a/src/main/java/com/typesafe/config/impl/ConfigSubstitution.java +++ b/src/main/java/com/typesafe/config/impl/ConfigSubstitution.java @@ -9,7 +9,6 @@ import java.util.Collections; import java.util.List; import com.typesafe.config.ConfigException; -import com.typesafe.config.ConfigMergeable; import com.typesafe.config.ConfigOrigin; import com.typesafe.config.ConfigResolveOptions; import com.typesafe.config.ConfigValue; @@ -57,28 +56,41 @@ final class ConfigSubstitution extends AbstractConfigValue implements } @Override - public AbstractConfigValue withFallback(ConfigMergeable mergeable) { - ConfigValue other = mergeable.toValue(); + protected ConfigSubstitution newCopy(boolean ignoresFallbacks) { + return new ConfigSubstitution(origin(), pieces, prefixLength, ignoresFallbacks); + } - if (ignoresFallbacks) { - return this; - } else if (other instanceof AbstractConfigObject - || other instanceof Unmergeable) { - // if we turn out to be an object, and the fallback also does, - // then a merge may be required; delay until we resolve. - List newStack = new ArrayList(); - newStack.add(this); - if (other instanceof Unmergeable) - newStack.addAll(((Unmergeable) other).unmergedValues()); - else - newStack.add((AbstractConfigValue) other); - return new ConfigDelayedMerge( - AbstractConfigObject.mergeOrigins(newStack), newStack); - } else { - // if the other is not an object, there won't be anything - // to merge with, so we are it even if we are an object. - return new ConfigSubstitution(origin(), pieces, prefixLength, true /* ignoresFallbacks */); - } + @Override + protected boolean ignoresFallbacks() { + return ignoresFallbacks; + } + + @Override + protected AbstractConfigValue mergedWithTheUnmergeable(Unmergeable fallback) { + if (ignoresFallbacks) + throw new ConfigException.BugOrBroken("should not be reached"); + + // if we turn out to be an object, and the fallback also does, + // then a merge may be required; delay until we resolve. + List newStack = new ArrayList(); + newStack.add(this); + newStack.addAll(fallback.unmergedValues()); + return new ConfigDelayedMerge(AbstractConfigObject.mergeOrigins(newStack), newStack, + ((AbstractConfigValue) fallback).ignoresFallbacks()); + } + + @Override + protected AbstractConfigValue mergedWithObject(AbstractConfigObject fallback) { + if (ignoresFallbacks) + throw new ConfigException.BugOrBroken("should not be reached"); + + // if we turn out to be an object, and the fallback also does, + // then a merge may be required; delay until we resolve. + List newStack = new ArrayList(); + newStack.add(this); + newStack.add(fallback); + return new ConfigDelayedMerge(AbstractConfigObject.mergeOrigins(newStack), newStack, + fallback.ignoresFallbacks()); } @Override diff --git a/src/main/java/com/typesafe/config/impl/RootConfig.java b/src/main/java/com/typesafe/config/impl/RootConfig.java index de0c9343..723b9fad 100644 --- a/src/main/java/com/typesafe/config/impl/RootConfig.java +++ b/src/main/java/com/typesafe/config/impl/RootConfig.java @@ -35,14 +35,14 @@ final class RootConfig extends SimpleConfig implements ConfigRoot { // if the object is already resolved then we should end up returning // "this" here, since asRoot() should return this if the path // is unchanged. - SimpleConfig resolved = resolvedObject(options).toConfig(); - return resolved.asRoot(rootPath); + AbstractConfigObject resolved = resolvedObject(options); + return newRootIfObjectChanged(this, resolved); } @Override public RootConfig withFallback(ConfigMergeable value) { // this can return "this" if the withFallback does nothing - return super.withFallback(value).asRoot(rootPath); + return newRootIfObjectChanged(this, super.withFallback(value).toObject()); } Path rootPathObject() { diff --git a/src/main/java/com/typesafe/config/impl/SimpleConfig.java b/src/main/java/com/typesafe/config/impl/SimpleConfig.java index 7c5c61e8..0f166fab 100644 --- a/src/main/java/com/typesafe/config/impl/SimpleConfig.java +++ b/src/main/java/com/typesafe/config/impl/SimpleConfig.java @@ -59,6 +59,13 @@ class SimpleConfig implements Config { return new RootConfig(underlying, newRootPath); } + static protected RootConfig newRootIfObjectChanged(RootConfig self, AbstractConfigObject underlying) { + if (underlying == self.object) + return self; + else + return new RootConfig(underlying, self.rootPathObject()); + } + protected AbstractConfigObject resolvedObject(ConfigResolveOptions options) { AbstractConfigValue resolved = SubstitutionResolver.resolve(object, object, options); diff --git a/src/main/java/com/typesafe/config/impl/SimpleConfigObject.java b/src/main/java/com/typesafe/config/impl/SimpleConfigObject.java index 3534fca0..88b2b909 100644 --- a/src/main/java/com/typesafe/config/impl/SimpleConfigObject.java +++ b/src/main/java/com/typesafe/config/impl/SimpleConfigObject.java @@ -45,10 +45,8 @@ final class SimpleConfigObject extends AbstractConfigObject { } @Override - public SimpleConfigObject newCopy(ResolveStatus newStatus, - boolean ignoresFallbacks) { - return new SimpleConfigObject(origin(), value, newStatus, - ignoresFallbacks); + protected SimpleConfigObject newCopy(ResolveStatus newStatus, boolean newIgnoresFallbacks) { + return new SimpleConfigObject(origin(), value, newStatus, newIgnoresFallbacks); } @Override diff --git a/src/test/scala/com/typesafe/config/impl/ConfigTest.scala b/src/test/scala/com/typesafe/config/impl/ConfigTest.scala index fcf68ade..71aaa810 100644 --- a/src/test/scala/com/typesafe/config/impl/ConfigTest.scala +++ b/src/test/scala/com/typesafe/config/impl/ConfigTest.scala @@ -15,6 +15,7 @@ import com.typesafe.config.ConfigResolveOptions import java.io.File import com.typesafe.config.ConfigParseOptions import com.typesafe.config.ConfigFactory +import com.typesafe.config.ConfigMergeable class ConfigTest extends TestUtils { @@ -83,7 +84,10 @@ class ConfigTest extends TestUtils { val trees = Seq(m(allObjects: _*)) ++ makeTrees(allObjects) for (tree <- trees) { // if this fails, we were not associative. - assertEquals(trees(0), tree) + if (!trees(0).equals(tree)) + throw new AssertionError("Merge was not associative, " + + "verify that it should not be, then don't use associativeMerge " + + "for this one.\none: " + trees(0) + "\ntwo: " + tree) } for (tree <- trees) { @@ -445,6 +449,61 @@ class ConfigTest extends TestUtils { } } + private def ignoresFallbacks(m: ConfigMergeable) = { + m match { + case v: AbstractConfigValue => + v.ignoresFallbacks() + case c: SimpleConfig => + c.toObject.ignoresFallbacks() + } + } + + private def testIgnoredMergesDoNothing(nonEmpty: ConfigMergeable) { + // falling back to a primitive once should switch us to "ignoreFallbacks" mode + // and then twice should "return this". Falling back to an empty object should + // return this unless the empty object was ignoreFallbacks and then we should + // "catch" its ignoreFallbacks. + + // some of what this tests is just optimization, not API contract (withFallback + // can return a new object anytime it likes) but want to be sure we do the + // optimizations. + + val empty = SimpleConfigObject.empty(null) + val primitive = intValue(42) + val emptyIgnoringFallbacks = empty.withFallback(primitive) + val nonEmptyIgnoringFallbacks = nonEmpty.withFallback(primitive) + + assertEquals(false, empty.ignoresFallbacks()) + assertEquals(true, primitive.ignoresFallbacks()) + assertEquals(true, emptyIgnoringFallbacks.ignoresFallbacks()) + assertEquals(false, ignoresFallbacks(nonEmpty)) + assertEquals(true, ignoresFallbacks(nonEmptyIgnoringFallbacks)) + + assertTrue(nonEmpty ne nonEmptyIgnoringFallbacks) + assertTrue(empty ne emptyIgnoringFallbacks) + + // falling back from primitive just returns this + assertTrue(primitive eq primitive.withFallback(empty)) + assertTrue(primitive eq primitive.withFallback(nonEmpty)) + assertTrue(primitive eq primitive.withFallback(nonEmptyIgnoringFallbacks)) + + // falling back again from an ignoreFallbacks should be a no-op, return this + assertTrue(nonEmptyIgnoringFallbacks eq nonEmptyIgnoringFallbacks.withFallback(empty)) + assertTrue(nonEmptyIgnoringFallbacks eq nonEmptyIgnoringFallbacks.withFallback(primitive)) + assertTrue(emptyIgnoringFallbacks eq emptyIgnoringFallbacks.withFallback(empty)) + assertTrue(emptyIgnoringFallbacks eq emptyIgnoringFallbacks.withFallback(primitive)) + } + + @Test + def ignoredMergesDoNothing() { + val conf = parseConfig("{ a : 1 }") + testIgnoredMergesDoNothing(conf) + + // ConfigRoot mode uses a little different codepath + val root = conf.asRoot(path("whatever")) + testIgnoredMergesDoNothing(root) + } + @Test def test01Getting() { val conf = ConfigFactory.load("test01")