end"), v)
- }
-
- @Test
- def resolveDoubleInString(): Unit = {
- val s = substInString("bar.double")
- val v = resolveWithoutFallbacks(s, simpleObject)
- assertEquals(stringValue("start<3.14>end"), v)
- }
-
- @Test
- def missingInArray(): Unit = {
- val obj = parseObject("""
- a : [ ${?missing}, ${?also.missing} ]
-""")
-
- val resolved = resolve(obj)
-
- assertEquals(Seq(), resolved.getList("a").asScala)
- }
-
- @Test
- def missingInObject(): Unit = {
- val obj = parseObject(
- """
- a : ${?missing}, b : ${?also.missing}, c : ${?b}, d : ${?c}
-"""
- )
-
- val resolved = resolve(obj)
-
- assertTrue(resolved.isEmpty)
- }
-
- private val substChainObject = {
- parseObject("""
-{
- "foo" : ${bar},
- "bar" : ${a.b.c},
- "a" : { "b" : { "c" : 57 } }
-}
-""")
- }
-
- @Test
- def chainSubstitutions(): Unit = {
- val s = subst("foo")
- val v = resolveWithoutFallbacks(s, substChainObject)
- assertEquals(intValue(57), v)
- }
-
- @Test
- def substitutionsLookForward(): Unit = {
- val obj = parseObject("""a=1,b=${a},a=2""")
- val resolved = resolve(obj)
- assertEquals(2, resolved.getInt("b"))
- }
-
- @Test
- def throwOnIncrediblyTrivialCycle(): Unit = {
- val s = subst("a")
- val e = intercept[ConfigException.UnresolvedSubstitution] {
- val v = resolveWithoutFallbacks(s, parseObject("a: ${a}"))
- }
- assertTrue(
- "Wrong exception: " + e.getMessage,
- e.getMessage().contains("cycle")
- )
- assertTrue(
- "Wrong exception: " + e.getMessage,
- e.getMessage().contains("${a}")
- )
- }
-
- private val substCycleObject = {
- parseObject("""
-{
- "foo" : ${bar},
- "bar" : ${a.b.c},
- "a" : { "b" : { "c" : ${foo} } }
-}
-""")
- }
-
- @Test
- def throwOnCycles(): Unit = {
- val s = subst("foo")
- val e = intercept[ConfigException.UnresolvedSubstitution] {
- val v = resolveWithoutFallbacks(s, substCycleObject)
- }
- assertTrue(
- "Wrong exception: " + e.getMessage,
- e.getMessage().contains("cycle")
- )
- assertTrue(
- "Wrong exception: " + e.getMessage,
- e.getMessage().contains("${foo}, ${bar}, ${a.b.c}, ${foo}")
- )
- }
-
- @Test
- def throwOnOptionalReferenceToNonOptionalCycle(): Unit = {
- // we look up ${?foo}, but the cycle has hard
- // non-optional links in it so still has to throw.
- val s = subst("foo", optional = true)
- val e = intercept[ConfigException.UnresolvedSubstitution] {
- val v = resolveWithoutFallbacks(s, substCycleObject)
- }
- assertTrue(
- "Wrong exception: " + e.getMessage,
- e.getMessage().contains("cycle")
- )
- }
-
- // ALL the links have to be optional here for the cycle to be ignored
- private val substCycleObjectOptionalLink = {
- parseObject("""
-{
- "foo" : ${?bar},
- "bar" : ${?a.b.c},
- "a" : { "b" : { "c" : ${?foo} } }
-}
-""")
- }
-
- @Test
- def optionalLinkCyclesActLikeUndefined(): Unit = {
- val s = subst("foo", optional = true)
- val v = resolveWithoutFallbacks(s, substCycleObjectOptionalLink)
- assertNull(
- "Cycle with optional links in it resolves to null if it's a cycle",
- v
- )
- }
-
- @Test
- def throwOnTwoKeyCycle(): Unit = {
- val obj = parseObject("""a:${b},b:${a}""")
- val e = intercept[ConfigException.UnresolvedSubstitution] {
- resolve(obj)
- }
- assertTrue(
- "Wrong exception: " + e.getMessage,
- e.getMessage().contains("cycle")
- )
- }
-
- @Test
- def throwOnFourKeyCycle(): Unit = {
- val obj = parseObject("""a:${b},b:${c},c:${d},d:${a}""")
- val e = intercept[ConfigException.UnresolvedSubstitution] {
- resolve(obj)
- }
- assertTrue(
- "Wrong exception: " + e.getMessage,
- e.getMessage().contains("cycle")
- )
- }
-
- @Test
- def resolveObject(): Unit = {
- val resolved = resolveWithoutFallbacks(substChainObject)
- assertEquals(57, resolved.getInt("foo"))
- assertEquals(57, resolved.getInt("bar"))
- assertEquals(57, resolved.getInt("a.b.c"))
- }
-
- private val substSideEffectCycle = {
- parseObject("""
-{
- "foo" : ${a.b.c},
- "a" : { "b" : { "c" : 42, "cycle" : ${foo} }, "cycle" : ${foo} }
-}
-""")
- }
-
- @Test
- def avoidSideEffectCycles(): Unit = {
- // The point of this test is that in traversing objects
- // to resolve a path, we need to avoid resolving
- // substitutions that are in the traversed objects but
- // are not directly required to resolve the path.
- // i.e. there should not be a cycle in this test.
-
- val resolved = resolveWithoutFallbacks(substSideEffectCycle)
-
- assertEquals(42, resolved.getInt("foo"))
- assertEquals(42, resolved.getInt("a.b.cycle"))
- assertEquals(42, resolved.getInt("a.cycle"))
- }
-
- @Test
- def ignoreHiddenUndefinedSubst(): Unit = {
- // if a substitution is overridden then it shouldn't matter that it's undefined
- val obj = parseObject("""a=${nonexistent},a=42""")
- val resolved = resolve(obj)
- assertEquals(42, resolved.getInt("a"))
- }
-
- @Test
- def objectDoesNotHideUndefinedSubst(): Unit = {
- // if a substitution is overridden by an object we still need to
- // evaluate the substitution
- val obj = parseObject("""a=${nonexistent},a={ b : 42 }""")
- val e = intercept[ConfigException.UnresolvedSubstitution] {
- resolve(obj)
- }
- assertTrue(
- "wrong exception: " + e.getMessage,
- e.getMessage.contains("Could not resolve")
- )
- }
-
- @Test
- def ignoreHiddenCircularSubst(): Unit = {
- // if a substitution is overridden then it shouldn't matter that it's circular
- val obj = parseObject("""a=${a},a=42""")
- val resolved = resolve(obj)
- assertEquals(42, resolved.getInt("a"))
- }
-
- private val delayedMergeObjectResolveProblem1 = {
- parseObject("""
- defaults {
- a = 1
- b = 2
- }
- // make item1 into a ConfigDelayedMergeObject
- item1 = ${defaults}
- // note that we'll resolve to a non-object value
- // so item1.b will ignoreFallbacks and not depend on
- // ${defaults}
- item1.b = 3
- // be sure we can resolve a substitution to a value in
- // a delayed-merge object.
- item2.b = ${item1.b}
-""")
- }
-
- @Test
- def avoidDelayedMergeObjectResolveProblem1(): Unit = {
- assertTrue(
- delayedMergeObjectResolveProblem1
- .attemptPeekWithPartialResolve("item1")
- .isInstanceOf[ConfigDelayedMergeObject]
- )
-
- val resolved = resolveWithoutFallbacks(delayedMergeObjectResolveProblem1)
-
- assertEquals(3, resolved.getInt("item1.b"))
- assertEquals(3, resolved.getInt("item2.b"))
- }
-
- private val delayedMergeObjectResolveProblem2 = {
- parseObject("""
- defaults {
- a = 1
- b = 2
- }
- // make item1 into a ConfigDelayedMergeObject
- item1 = ${defaults}
- // note that we'll resolve to an object value
- // so item1.b will depend on also looking up ${defaults}
- item1.b = { c : 43 }
- // be sure we can resolve a substitution to a value in
- // a delayed-merge object.
- item2.b = ${item1.b}
-""")
- }
-
- @Test
- def avoidDelayedMergeObjectResolveProblem2(): Unit = {
- assertTrue(
- delayedMergeObjectResolveProblem2
- .attemptPeekWithPartialResolve("item1")
- .isInstanceOf[ConfigDelayedMergeObject]
- )
-
- val resolved = resolveWithoutFallbacks(delayedMergeObjectResolveProblem2)
-
- assertEquals(parseObject("{ c : 43 }"), resolved.getObject("item1.b"))
- assertEquals(43, resolved.getInt("item1.b.c"))
- assertEquals(43, resolved.getInt("item2.b.c"))
- }
-
- // in this case, item1 is self-referential because
- // it refers to ${defaults} which refers back to
- // ${item1}. When self-referencing, only the
- // value of ${item1} "looking back" should be
- // visible. This is really a test of the
- // self-referencing semantics.
- private val delayedMergeObjectResolveProblem3 = {
- parseObject("""
- item1.b.c = 100
- defaults {
- // we depend on item1.b.c
- a = ${item1.b.c}
- b = 2
- }
- // make item1 into a ConfigDelayedMergeObject
- item1 = ${defaults}
- // the ${item1.b.c} above in ${defaults} should ignore
- // this because it only looks back
- item1.b = { c : 43 }
- // be sure we can resolve a substitution to a value in
- // a delayed-merge object.
- item2.b = ${item1.b}
-""")
- }
-
- @Test
- def avoidDelayedMergeObjectResolveProblem3(): Unit = {
- assertTrue(
- delayedMergeObjectResolveProblem3
- .attemptPeekWithPartialResolve("item1")
- .isInstanceOf[ConfigDelayedMergeObject]
- )
-
- val resolved = resolveWithoutFallbacks(delayedMergeObjectResolveProblem3)
-
- assertEquals(parseObject("{ c : 43 }"), resolved.getObject("item1.b"))
- assertEquals(43, resolved.getInt("item1.b.c"))
- assertEquals(43, resolved.getInt("item2.b.c"))
- assertEquals(100, resolved.getInt("defaults.a"))
- }
-
- private val delayedMergeObjectResolveProblem4 = {
- parseObject("""
- defaults {
- a = 1
- b = 2
- }
-
- item1.b = 7
- // make item1 into a ConfigDelayedMerge
- item1 = ${defaults}
- // be sure we can resolve a substitution to a value in
- // a delayed-merge object.
- item2.b = ${item1.b}
-""")
- }
-
- @Test
- def avoidDelayedMergeObjectResolveProblem4(): Unit = {
- // in this case we have a ConfigDelayedMerge not a ConfigDelayedMergeObject
- assertTrue(
- delayedMergeObjectResolveProblem4
- .attemptPeekWithPartialResolve("item1")
- .isInstanceOf[ConfigDelayedMerge]
- )
-
- val resolved = resolveWithoutFallbacks(delayedMergeObjectResolveProblem4)
-
- assertEquals(2, resolved.getInt("item1.b"))
- assertEquals(2, resolved.getInt("item2.b"))
- }
-
- private val delayedMergeObjectResolveProblem5 = {
- parseObject("""
- defaults {
- a = ${item1.b} // tricky cycle - we won't see ${defaults}
- // as we resolve this
- b = 2
- }
-
- item1.b = 7
- // make item1 into a ConfigDelayedMerge
- item1 = ${defaults}
- // be sure we can resolve a substitution to a value in
- // a delayed-merge object.
- item2.b = ${item1.b}
-""")
- }
-
- @Test
- def avoidDelayedMergeObjectResolveProblem5(): Unit = {
- // in this case we have a ConfigDelayedMerge not a ConfigDelayedMergeObject
- assertTrue(
- delayedMergeObjectResolveProblem5
- .attemptPeekWithPartialResolve("item1")
- .isInstanceOf[ConfigDelayedMerge]
- )
-
- val resolved = resolveWithoutFallbacks(delayedMergeObjectResolveProblem5)
-
- assertEquals("item1.b", 2, resolved.getInt("item1.b"))
- assertEquals("item2.b", 2, resolved.getInt("item2.b"))
- assertEquals("defaults.a", 7, resolved.getInt("defaults.a"))
- }
-
- private val delayedMergeObjectResolveProblem6 = {
- parseObject(
- """
- z = 15
- defaults-defaults-defaults {
- m = ${z}
- n.o.p = ${z}
- }
- defaults-defaults {
- x = 10
- y = 11
- asdf = ${z}
- }
- defaults {
- a = 1
- b = 2
- }
- defaults-alias = ${defaults}
- // make item1 into a ConfigDelayedMergeObject several layers deep
- // that will NOT become resolved just because we resolve one path
- // through it.
- item1 = 345
- item1 = ${?NONEXISTENT}
- item1 = ${defaults-defaults-defaults}
- item1 = {}
- item1 = ${defaults-defaults}
- item1 = ${defaults-alias}
- item1 = ${defaults}
- item1.b = { c : 43 }
- item1.xyz = 101
- // be sure we can resolve a substitution to a value in
- // a delayed-merge object.
- item2.b = ${item1.b}
-"""
- )
- }
-
- @Test
- def avoidDelayedMergeObjectResolveProblem6(): Unit = {
- assertTrue(
- delayedMergeObjectResolveProblem6
- .attemptPeekWithPartialResolve("item1")
- .isInstanceOf[ConfigDelayedMergeObject]
- )
-
- // should be able to attemptPeekWithPartialResolve() a known non-object without resolving
- assertEquals(
- 101,
- delayedMergeObjectResolveProblem6.toConfig
- .getObject("item1")
- .attemptPeekWithPartialResolve("xyz")
- .unwrapped
- )
-
- val resolved = resolveWithoutFallbacks(delayedMergeObjectResolveProblem6)
-
- assertEquals(parseObject("{ c : 43 }"), resolved.getObject("item1.b"))
- assertEquals(43, resolved.getInt("item1.b.c"))
- assertEquals(43, resolved.getInt("item2.b.c"))
- assertEquals(15, resolved.getInt("item1.n.o.p"))
- }
-
- private val delayedMergeObjectWithKnownValue = {
- parseObject("""
- defaults {
- a = 1
- b = 2
- }
- // make item1 into a ConfigDelayedMergeObject
- item1 = ${defaults}
- // note that we'll resolve to a non-object value
- // so item1.b will ignoreFallbacks and not depend on
- // ${defaults}
- item1.b = 3
-""")
- }
-
- @Test
- def fetchKnownValueFromDelayedMergeObject(): Unit = {
- assertTrue(
- delayedMergeObjectWithKnownValue
- .attemptPeekWithPartialResolve("item1")
- .isInstanceOf[ConfigDelayedMergeObject]
- )
-
- assertEquals(
- 3,
- delayedMergeObjectWithKnownValue.toConfig.getConfig("item1").getInt("b")
- )
- }
-
- private val delayedMergeObjectNeedsFullResolve = {
- parseObject(
- """
- defaults {
- a = 1
- b = { c : 31 }
- }
- item1 = ${defaults}
- // because b is an object, fetching it requires resolving ${defaults} above
- // to see if there are more keys to merge with b.
- item1.b = { c : 41 }
-"""
- )
- }
-
- @Test
- def failToFetchFromDelayedMergeObjectNeedsFullResolve(): Unit = {
- assertTrue(
- delayedMergeObjectWithKnownValue
- .attemptPeekWithPartialResolve("item1")
- .isInstanceOf[ConfigDelayedMergeObject]
- )
-
- val e = intercept[ConfigException.NotResolved] {
- delayedMergeObjectNeedsFullResolve.toConfig.getObject("item1.b")
- }
-
- assertTrue(
- "wrong exception: " + e.getMessage,
- e.getMessage.contains("item1.b")
- )
- }
-
- // objects that mutually refer to each other
- private val delayedMergeObjectEmbrace = {
- parseObject(
- """
- defaults {
- a = 1
- b = 2
- }
-
- item1 = ${defaults}
- // item1.c refers to a field in item2 that refers to item1
- item1.c = ${item2.d}
- // item1.x refers to a field in item2 that doesn't go back to item1
- item1.x = ${item2.y}
-
- item2 = ${defaults}
- // item2.d refers to a field in item1
- item2.d = ${item1.a}
- item2.y = 15
-"""
- )
- }
-
- @Test
- def resolveDelayedMergeObjectEmbrace(): Unit = {
- assertTrue(
- delayedMergeObjectEmbrace
- .attemptPeekWithPartialResolve("item1")
- .isInstanceOf[ConfigDelayedMergeObject]
- )
- assertTrue(
- delayedMergeObjectEmbrace
- .attemptPeekWithPartialResolve("item2")
- .isInstanceOf[ConfigDelayedMergeObject]
- )
-
- val resolved = delayedMergeObjectEmbrace.toConfig.resolve()
- assertEquals(1, resolved.getInt("item1.c"))
- assertEquals(1, resolved.getInt("item2.d"))
- assertEquals(15, resolved.getInt("item1.x"))
- }
-
- // objects that mutually refer to each other
- private val plainObjectEmbrace = {
- parseObject("""
- item1.a = 10
- item1.b = ${item2.d}
- item2.c = 12
- item2.d = 14
- item2.e = ${item1.a}
- item2.f = ${item1.b} // item1.b goes back to item2
- item2.g = ${item2.f} // goes back to ourselves
-""")
- }
-
- @Test
- def resolvePlainObjectEmbrace(): Unit = {
- assertTrue(
- plainObjectEmbrace
- .attemptPeekWithPartialResolve("item1")
- .isInstanceOf[SimpleConfigObject]
- )
- assertTrue(
- plainObjectEmbrace
- .attemptPeekWithPartialResolve("item2")
- .isInstanceOf[SimpleConfigObject]
- )
-
- val resolved = plainObjectEmbrace.toConfig.resolve()
- assertEquals(14, resolved.getInt("item1.b"))
- assertEquals(10, resolved.getInt("item2.e"))
- assertEquals(14, resolved.getInt("item2.f"))
- assertEquals(14, resolved.getInt("item2.g"))
- }
-
- @Test
- def useRelativeToSameFileWhenRelativized(): Unit = {
- val child = parseObject("""foo=in child,bar=${foo}""")
-
- val values = new java.util.HashMap[String, AbstractConfigValue]()
-
- values.put("a", child.relativized(new Path("a")))
- // this "foo" should NOT be used.
- values.put("foo", stringValue("in parent"))
-
- val resolved = resolve(new SimpleConfigObject(fakeOrigin(), values))
-
- assertEquals("in child", resolved.getString("a.bar"))
- }
-
- @Test
- def useRelativeToRootWhenRelativized(): Unit = {
- // here, "foo" is not defined in the child
- val child = parseObject("""bar=${foo}""")
-
- val values = new java.util.HashMap[String, AbstractConfigValue]()
-
- values.put("a", child.relativized(new Path("a")))
- // so this "foo" SHOULD be used
- values.put("foo", stringValue("in parent"))
-
- val resolved = resolve(new SimpleConfigObject(fakeOrigin(), values))
-
- assertEquals("in parent", resolved.getString("a.bar"))
- }
-
private val substComplexObject = {
parseObject(
"""
@@ -797,29 +73,6 @@ class ConfigSubstitutionTest extends TestUtils {
)
}
- @Test
- def complexResolve(): Unit = {
- val resolved = resolveWithoutFallbacks(substComplexObject)
-
- assertEquals(57, resolved.getInt("foo"))
- assertEquals(57, resolved.getInt("bar"))
- assertEquals(57, resolved.getInt("a.b.c"))
- assertEquals(57, resolved.getInt("a.b.d"))
- assertEquals(57, resolved.getInt("objB.d"))
- assertEquals(
- Seq(57, 57, 37, 57, 57, 57),
- resolved.getIntList("arr").asScala
- )
- assertEquals(
- Seq(57, 57, 37, 57, 57, 57),
- resolved.getIntList("ptrToArr").asScala
- )
- assertEquals(
- Seq(57, 57, 37, 57, 57, 57),
- resolved.getIntList("x.y.ptrToPtrToArr").asScala
- )
- }
-
private val substSystemPropsObject =
parseObject("""
{
@@ -926,27 +179,6 @@ class ConfigSubstitutionTest extends TestUtils {
}
}
- @Test
- def noFallbackToEnvIfValuesAreNull(): Unit = {
- // create a fallback object with all the env var names
- // set to null. we want to be sure this blocks
- // lookup in the environment. i.e. if there is a
- // { HOME : null } then ${HOME} should be null.
- val nullsMap = new java.util.HashMap[String, Object]
- for (k <- substEnvVarObject.keySet().asScala) {
- val envVarName = k.replace("key_", "")
- nullsMap.put(envVarName, null)
- }
- val nulls = ConfigFactory.parseMap(nullsMap, "nulls map")
-
- val resolved = resolve(substEnvVarObject.withFallback(nulls))
-
- for (k <- resolved.root.keySet().asScala) {
- assertNotNull(resolved.root.get(k))
- assertEquals(nullValue(), resolved.root.get(k))
- }
- }
-
@Test
def fallbackToEnvWhenRelativized(): Unit = {
val values = new java.util.HashMap[String, AbstractConfigValue]()
@@ -972,443 +204,4 @@ class ConfigSubstitutionTest extends TestUtils {
)
}
}
-
- @Test
- def throwWhenEnvNotFound(): Unit = {
- val obj = parseObject("""{ a : ${NOT_HERE} }""")
- intercept[ConfigException.UnresolvedSubstitution] {
- resolve(obj)
- }
- }
-
- @Test
- def optionalOverrideNotProvided(): Unit = {
- val obj = parseObject("""{ a: 42, a : ${?NOT_HERE} }""")
- val resolved = resolve(obj)
- assertEquals(42, resolved.getInt("a"))
- }
-
- @Test
- def optionalOverrideProvided(): Unit = {
- val obj = parseObject("""{ HERE : 43, a: 42, a : ${?HERE} }""")
- val resolved = resolve(obj)
- assertEquals(43, resolved.getInt("a"))
- }
-
- @Test
- def optionalOverrideOfObjectNotProvided(): Unit = {
- val obj = parseObject("""{ a: { b : 42 }, a : ${?NOT_HERE} }""")
- val resolved = resolve(obj)
- assertEquals(42, resolved.getInt("a.b"))
- }
-
- @Test
- def optionalOverrideOfObjectProvided(): Unit = {
- val obj = parseObject("""{ HERE : 43, a: { b : 42 }, a : ${?HERE} }""")
- val resolved = resolve(obj)
- assertEquals(43, resolved.getInt("a"))
- assertFalse(resolved.hasPath("a.b"))
- }
-
- @Test
- def optionalVanishesFromArray(): Unit = {
- val obj = parseObject("""{ a : [ 1, 2, 3, ${?NOT_HERE} ] }""")
- val resolved = resolve(obj)
- assertEquals(Seq(1, 2, 3), resolved.getIntList("a").asScala)
- }
-
- @Test
- def optionalUsedInArray(): Unit = {
- val obj = parseObject("""{ HERE: 4, a : [ 1, 2, 3, ${?HERE} ] }""")
- val resolved = resolve(obj)
- assertEquals(Seq(1, 2, 3, 4), resolved.getIntList("a").asScala)
- }
-
- @Test
- def substSelfReference(): Unit = {
- val obj = parseObject("""a=1, a=${a}""")
- val resolved = resolve(obj)
- assertEquals(1, resolved.getInt("a"))
- }
-
- @Test
- def substSelfReferenceUndefined(): Unit = {
- val obj = parseObject("""a=${a}""")
- val e = intercept[ConfigException.UnresolvedSubstitution] {
- resolve(obj)
- }
- assertTrue(
- "wrong exception: " + e.getMessage,
- e.getMessage.contains("cycle")
- )
- }
-
- @Test
- def substSelfReferenceOptional(): Unit = {
- val obj = parseObject("""a=${?a}""")
- val resolved = resolve(obj)
- assertEquals("optional self reference disappears", 0, resolved.root.size)
- }
-
- @Test
- def substSelfReferenceAlongPath(): Unit = {
- val obj = parseObject("""a.b=1, a.b=${a.b}""")
- val resolved = resolve(obj)
- assertEquals(1, resolved.getInt("a.b"))
- }
-
- @Test
- def substSelfReferenceAlongLongerPath(): Unit = {
- val obj = parseObject("""a.b.c=1, a.b.c=${a.b.c}""")
- val resolved = resolve(obj)
- assertEquals(1, resolved.getInt("a.b.c"))
- }
-
- @Test
- def substSelfReferenceAlongPathMoreComplex(): Unit = {
- // this is an example from the spec
- val obj = parseObject("""
- foo : { a : { c : 1 } }
- foo : ${foo.a}
- foo : { a : 2 }
- """)
- val resolved = resolve(obj)
- assertEquals(1, resolved.getInt("foo.c"))
- assertEquals(2, resolved.getInt("foo.a"))
- }
-
- @Test
- def substSelfReferenceIndirect(): Unit = {
- // this has two possible outcomes depending on whether
- // we resolve and memoize a first or b first. currently
- // java 8's hash table makes it resolve OK, but
- // it's also allowed to throw an exception.
- val obj = parseObject("""a=1, b=${a}, a=${b}""")
- val resolved = resolve(obj)
- assertEquals(1, resolved.getInt("a"))
- }
-
- @Test
- def substSelfReferenceDoubleIndirect(): Unit = {
- // this has two possible outcomes depending on whether we
- // resolve and memoize a, b, or c first. currently java
- // 8's hash table makes it resolve OK, but it's also
- // allowed to throw an exception.
- val obj = parseObject("""a=1, b=${c}, c=${a}, a=${b}""")
- val resolved = resolve(obj)
- assertEquals(1, resolved.getInt("a"))
- }
-
- @Test
- def substSelfReferenceIndirectStackCycle(): Unit = {
- // this situation is undefined, depends on
- // whether we resolve a or b first.
- val obj = parseObject("""a=1, b={c=5}, b=${a}, a=${b}""")
- val resolved = resolve(obj)
- val option1 = parseObject(""" b={c=5}, a={c=5} """).toConfig
- val option2 = parseObject(""" b=1, a=1 """).toConfig
- assertTrue(
- "not an expected possibility: " + resolved +
- " expected 1: " + option1 + " or 2: " + option2,
- resolved == option1 || resolved == option2
- )
- }
-
- @Test
- def substSelfReferenceObject(): Unit = {
- val obj = parseObject("""a={b=5}, a=${a}""")
- val resolved = resolve(obj)
- assertEquals(5, resolved.getInt("a.b"))
- }
-
- @Test
- def substSelfReferenceObjectAlongPath(): Unit = {
- val obj = parseObject("""a.b={c=5}, a.b=${a.b}""")
- val resolved = resolve(obj)
- assertEquals(5, resolved.getInt("a.b.c"))
- }
-
- @Test
- def substSelfReferenceInConcat(): Unit = {
- val obj = parseObject("""a=1, a=${a}foo""")
- val resolved = resolve(obj)
- assertEquals("1foo", resolved.getString("a"))
- }
-
- @Test
- def substSelfReferenceIndirectInConcat(): Unit = {
- // this situation is undefined, depends on
- // whether we resolve a or b first. If b first
- // then there's an error because ${a} is undefined.
- // if a first then b=1foo and a=1foo.
- val obj = parseObject("""a=1, b=${a}foo, a=${b}""")
- val either =
- try {
- Left(resolve(obj))
- } catch {
- case e: ConfigException.UnresolvedSubstitution =>
- Right(e)
- }
- val option1 = Left(parseObject("""a:1foo,b:1foo""").toConfig)
- assertTrue(
- "not an expected possibility: " + either +
- " expected value " + option1 + " or an exception",
- either == option1 || either.isRight
- )
- }
-
- @Test
- def substOptionalSelfReferenceInConcat(): Unit = {
- val obj = parseObject("""a=${?a}foo""")
- val resolved = resolve(obj)
- assertEquals("foo", resolved.getString("a"))
- }
-
- @Test
- def substOptionalIndirectSelfReferenceInConcat(): Unit = {
- val obj = parseObject("""a=${?b}foo,b=${?a}""")
- val resolved = resolve(obj)
- assertEquals("foo", resolved.getString("a"))
- }
-
- @Test
- def substTwoOptionalSelfReferencesInConcat(): Unit = {
- val obj = parseObject("""a=${?a}foo${?a}""")
- val resolved = resolve(obj)
- assertEquals("foo", resolved.getString("a"))
- }
-
- @Test
- def substTwoOptionalSelfReferencesInConcatWithPriorValue(): Unit = {
- val obj = parseObject("""a=1,a=${?a}foo${?a}""")
- val resolved = resolve(obj)
- assertEquals("1foo1", resolved.getString("a"))
- }
-
- @Test
- def substSelfReferenceMiddleOfStack(): Unit = {
- val obj = parseObject("""a=1, a=${a}, a=2""")
- val resolved = resolve(obj)
- // the substitution would be 1, but then 2 overrides
- assertEquals(2, resolved.getInt("a"))
- }
-
- @Test
- def substSelfReferenceObjectMiddleOfStack(): Unit = {
- val obj = parseObject("""a={b=5}, a=${a}, a={c=6}""")
- val resolved = resolve(obj)
- assertEquals(5, resolved.getInt("a.b"))
- assertEquals(6, resolved.getInt("a.c"))
- }
-
- @Test
- def substOptionalSelfReferenceMiddleOfStack(): Unit = {
- val obj = parseObject("""a=1, a=${?a}, a=2""")
- val resolved = resolve(obj)
- // the substitution would be 1, but then 2 overrides
- assertEquals(2, resolved.getInt("a"))
- }
-
- @Test
- def substSelfReferenceBottomOfStack(): Unit = {
- // self-reference should just be ignored since it's
- // overridden
- val obj = parseObject("""a=${a}, a=1, a=2""")
- val resolved = resolve(obj)
- assertEquals(2, resolved.getInt("a"))
- }
-
- @Test
- def substOptionalSelfReferenceBottomOfStack(): Unit = {
- val obj = parseObject("""a=${?a}, a=1, a=2""")
- val resolved = resolve(obj)
- assertEquals(2, resolved.getInt("a"))
- }
-
- @Test
- def substSelfReferenceTopOfStack(): Unit = {
- val obj = parseObject("""a=1, a=2, a=${a}""")
- val resolved = resolve(obj)
- assertEquals(2, resolved.getInt("a"))
- }
-
- @Test
- def substOptionalSelfReferenceTopOfStack(): Unit = {
- val obj = parseObject("""a=1, a=2, a=${?a}""")
- val resolved = resolve(obj)
- assertEquals(2, resolved.getInt("a"))
- }
-
- @Test
- def substSelfReferenceAlongAPath(): Unit = {
- // ${a} in the middle of the stack means "${a} in the stack
- // below us" and so ${a.b} means b inside the "${a} below us"
- // not b inside the final "${a}"
- val obj = parseObject("""a={b={c=5}}, a=${a.b}, a={b=2}""")
- val resolved = resolve(obj)
- assertEquals(5, resolved.getInt("a.c"))
- }
-
- @Test
- def substSelfReferenceAlongAPathInsideObject(): Unit = {
- // if the ${a.b} is _inside_ a field value instead of
- // _being_ the field value, it does not look backward.
- val obj = parseObject("""a={b={c=5}}, a={ x : ${a.b} }, a={b=2}""")
- val resolved = resolve(obj)
- assertEquals(2, resolved.getInt("a.x"))
- }
-
- @Test
- def substInChildFieldNotASelfReference1(): Unit = {
- // here, ${bar.foo} is not a self reference because
- // it's the value of a child field of bar, not bar
- // itself; so we use bar's current value, rather than
- // looking back in the merge stack
- val obj = parseObject("""
- bar : { foo : 42,
- baz : ${bar.foo}
- }
- """)
- val resolved = resolve(obj)
- assertEquals(42, resolved.getInt("bar.baz"))
- assertEquals(42, resolved.getInt("bar.foo"))
- }
-
- @Test
- def substInChildFieldNotASelfReference2(): Unit = {
- // checking that having bar.foo later in the stack
- // doesn't break the behavior
- val obj = parseObject("""
- bar : { foo : 42,
- baz : ${bar.foo}
- }
- bar : { foo : 43 }
- """)
- val resolved = resolve(obj)
- assertEquals(43, resolved.getInt("bar.baz"))
- assertEquals(43, resolved.getInt("bar.foo"))
- }
-
- @Test
- def substInChildFieldNotASelfReference3(): Unit = {
- // checking that having bar.foo earlier in the merge
- // stack doesn't break the behavior.
- val obj = parseObject("""
- bar : { foo : 43 }
- bar : { foo : 42,
- baz : ${bar.foo}
- }
- """)
- val resolved = resolve(obj)
- assertEquals(42, resolved.getInt("bar.baz"))
- assertEquals(42, resolved.getInt("bar.foo"))
- }
-
- @Test
- def substInChildFieldNotASelfReference4(): Unit = {
- // checking that having bar set to non-object earlier
- // doesn't break the behavior.
- val obj = parseObject("""
- bar : 101
- bar : { foo : 42,
- baz : ${bar.foo}
- }
- """)
- val resolved = resolve(obj)
- assertEquals(42, resolved.getInt("bar.baz"))
- assertEquals(42, resolved.getInt("bar.foo"))
- }
-
- @Test
- def substInChildFieldNotASelfReference5(): Unit = {
- // checking that having bar set to unresolved array earlier
- // doesn't break the behavior.
- val obj = parseObject("""
- x : 0
- bar : [ ${x}, 1, 2, 3 ]
- bar : { foo : 42,
- baz : ${bar.foo}
- }
- """)
- val resolved = resolve(obj)
- assertEquals(42, resolved.getInt("bar.baz"))
- assertEquals(42, resolved.getInt("bar.foo"))
- }
-
- @Test
- def mutuallyReferringNotASelfReference(): Unit = {
- val obj = parseObject("""
- // bar.a should end up as 4
- bar : { a : ${foo.d}, b : 1 }
- bar.b = 3
- // foo.c should end up as 3
- foo : { c : ${bar.b}, d : 2 }
- foo.d = 4
- """)
- val resolved = resolve(obj)
- assertEquals(4, resolved.getInt("bar.a"))
- assertEquals(3, resolved.getInt("foo.c"))
- }
-
- @Test
- def substSelfReferenceMultipleTimes(): Unit = {
- val obj = parseObject("""a=1,a=${a},a=${a},a=${a}""")
- val resolved = resolve(obj)
- assertEquals(1, resolved.getInt("a"))
- }
-
- @Test
- def substSelfReferenceInConcatMultipleTimes(): Unit = {
- val obj = parseObject("""a=1,a=${a}x,a=${a}y,a=${a}z""")
- val resolved = resolve(obj)
- assertEquals("1xyz", resolved.getString("a"))
- }
-
- @Test
- def substSelfReferenceInArray(): Unit = {
- // never "look back" from "inside" an array
- val obj = parseObject("""a=1,a=[${a}, 2]""")
- val e = intercept[ConfigException.UnresolvedSubstitution] {
- resolve(obj)
- }
- assertTrue(
- "wrong exception: " + e.getMessage,
- e.getMessage.contains("cycle") && e.getMessage.contains("${a}")
- )
- }
-
- @Test
- def substSelfReferenceInObject(): Unit = {
- // never "look back" from "inside" an object
- val obj = parseObject("""a=1,a={ x : ${a} }""")
- val e = intercept[ConfigException.UnresolvedSubstitution] {
- resolve(obj)
- }
- assertTrue(
- "wrong exception: " + e.getMessage,
- e.getMessage.contains("cycle") && e.getMessage.contains("${a}")
- )
- }
-
- @Test
- def selfReferentialObjectNotAffectedByOverriding(): Unit = {
- // this is testing that we can still refer to another
- // field in the same object, even though we are overriding
- // an earlier object.
- val obj = parseObject("""a={ x : 42, y : ${a.x} }""")
- val resolved = resolve(obj)
- assertEquals(
- parseObject("{ x : 42, y : 42 }"),
- resolved.getConfig("a").root
- )
-
- // this is expected because if adding "a=1" here affects the outcome,
- // it would be flat-out bizarre.
- val obj2 = parseObject("""a=1, a={ x : 42, y : ${a.x} }""")
- val resolved2 = resolve(obj2)
- assertEquals(
- parseObject("{ x : 42, y : 42 }"),
- resolved2.getConfig("a").root
- )
- }
}
diff --git a/sconfig/jvm/src/test/scala/org/ekrich/config/impl/ConfigValueTest.scala b/sconfig/jvm/src/test/scala/org/ekrich/config/impl/ConfigValueTest.scala
index 8b9eba65..6ba6c7cc 100644
--- a/sconfig/jvm/src/test/scala/org/ekrich/config/impl/ConfigValueTest.scala
+++ b/sconfig/jvm/src/test/scala/org/ekrich/config/impl/ConfigValueTest.scala
@@ -5,31 +5,29 @@ package org.ekrich.config.impl
import org.junit.Assert._
import org.junit._
-import org.ekrich.config.ConfigValue
-import java.util.Collections
import java.net.URL
import scala.jdk.CollectionConverters._
-import org.ekrich.config.ConfigObject
import org.ekrich.config.ConfigList
-import org.ekrich.config.ConfigException
-import org.ekrich.config.ConfigValueType
-import org.ekrich.config.ConfigRenderOptions
-import org.ekrich.config.ConfigValueFactory
-import org.ekrich.config.ConfigFactory
import FileUtils._
+/**
+ * Lack of URL on native and js preclude this test from working.
+ *
+ * The following is the test/code in question:
+ * {{{
+ * def configOriginFileAndLine()
+ *
+ * SimpleConfigOrigin line 34
+ * url = new PlatformUri(uri).toURL().toExternalForm()
+ * }}}
+ * The test could conceiveable work on native with `File` and even deeper in the
+ * API with a URL implementation.
+ *
+ * The serialization tests won't work on JS due to the lack of
+ * `ObjectOutputStream` and others but maybe could work on Native.
+ */
class ConfigValueTest extends TestUtils {
- @Test
- def configOriginEquality(): Unit = {
- val a = SimpleConfigOrigin.newSimple("foo")
- val sameAsA = SimpleConfigOrigin.newSimple("foo")
- val b = SimpleConfigOrigin.newSimple("bar")
-
- checkEqualObjects(a, a)
- checkEqualObjects(a, sameAsA)
- checkNotEqualObjects(a, b)
- }
@Test
def configOriginNotSerializable(): Unit = {
@@ -37,17 +35,6 @@ class ConfigValueTest extends TestUtils {
checkNotSerializable(a)
}
- @Test
- def configIntEquality(): Unit = {
- val a = intValue(42)
- val sameAsA = intValue(42)
- val b = intValue(43)
-
- checkEqualObjects(a, a)
- checkEqualObjects(a, sameAsA)
- checkNotEqualObjects(a, b)
- }
-
@Test
def configIntSerializable(): Unit = {
val expectedSerialization = "" +
@@ -60,17 +47,6 @@ class ConfigValueTest extends TestUtils {
assertEquals(42, b.unwrapped)
}
- @Test
- def configLongEquality(): Unit = {
- val a = longValue(Integer.MAX_VALUE + 42L)
- val sameAsA = longValue(Integer.MAX_VALUE + 42L)
- val b = longValue(Integer.MAX_VALUE + 43L)
-
- checkEqualObjects(a, a)
- checkEqualObjects(a, sameAsA)
- checkNotEqualObjects(a, b)
- }
-
@Test
def configLongSerializable(): Unit = {
val expectedSerialization = "" +
@@ -84,30 +60,6 @@ class ConfigValueTest extends TestUtils {
assertEquals(Integer.MAX_VALUE + 42L, b.unwrapped)
}
- @Test
- def configIntAndLongEquality(): Unit = {
- val longVal = longValue(42L)
- val intValue = longValue(42)
- val longValueB = longValue(43L)
- val intValueB = longValue(43)
-
- checkEqualObjects(intValue, longVal)
- checkEqualObjects(intValueB, longValueB)
- checkNotEqualObjects(intValue, longValueB)
- checkNotEqualObjects(intValueB, longVal)
- }
-
- @Test
- def configDoubleEquality(): Unit = {
- val a = doubleValue(3.14)
- val sameAsA = doubleValue(3.14)
- val b = doubleValue(4.14)
-
- checkEqualObjects(a, a)
- checkEqualObjects(a, sameAsA)
- checkNotEqualObjects(a, b)
- }
-
@Test
def configDoubleSerializable(): Unit = {
val expectedSerialization = "" +
@@ -121,19 +73,6 @@ class ConfigValueTest extends TestUtils {
assertEquals(3.14, b.unwrapped)
}
- @Test
- def configIntAndDoubleEquality(): Unit = {
- val doubleVal = doubleValue(3.0)
- val intValue = longValue(3)
- val doubleValueB = doubleValue(4.0)
- val intValueB = doubleValue(4)
-
- checkEqualObjects(intValue, doubleVal)
- checkEqualObjects(intValueB, doubleValueB)
- checkNotEqualObjects(intValue, doubleValueB)
- checkNotEqualObjects(intValueB, doubleVal)
- }
-
@Test
def configNullSerializable(): Unit = {
val expectedSerialization = "" +
@@ -171,6 +110,7 @@ class ConfigValueTest extends TestUtils {
assertEquals("The quick brown fox", b.unwrapped)
}
+ // in both ConfigValueTest and ConfigValueSharedTest
private def configMap(
pairs: (String, Int)*
): java.util.Map[String, AbstractConfigValue] = {
@@ -181,39 +121,6 @@ class ConfigValueTest extends TestUtils {
m
}
- @Test
- def configObjectEquality(): Unit = {
- val aMap = configMap("a" -> 1, "b" -> 2, "c" -> 3)
- val sameAsAMap = configMap("a" -> 1, "b" -> 2, "c" -> 3)
- val bMap = configMap("a" -> 3, "b" -> 4, "c" -> 5)
- // different keys is a different case in the equals implementation
- val cMap = configMap("x" -> 3, "y" -> 4, "z" -> 5)
- val a = new SimpleConfigObject(fakeOrigin(), aMap)
- val sameAsA = new SimpleConfigObject(fakeOrigin(), sameAsAMap)
- val b = new SimpleConfigObject(fakeOrigin(), bMap)
- val c = new SimpleConfigObject(fakeOrigin(), cMap)
-
- checkEqualObjects(a, a)
- checkEqualObjects(a, sameAsA)
- checkEqualObjects(b, b)
- checkEqualObjects(c, c)
- checkNotEqualObjects(a, b)
- checkNotEqualObjects(a, c)
- checkNotEqualObjects(b, c)
-
- // the config for an equal object is also equal
- val config = a.toConfig
- checkEqualObjects(config, config)
- checkEqualObjects(config, sameAsA.toConfig)
- checkEqualObjects(a.toConfig, config)
- checkNotEqualObjects(config, b.toConfig)
- checkNotEqualObjects(config, c.toConfig)
-
- // configs are not equal to objects
- checkNotEqualObjects(a, a.toConfig)
- checkNotEqualObjects(b, b.toConfig)
- }
-
@Test
def java6ConfigObjectSerializable(): Unit = {
val expectedSerialization = "" +
@@ -303,19 +210,6 @@ class ConfigValueTest extends TestUtils {
assertEquals(expected, actual)
}
- @Test
- def configListEquality(): Unit = {
- val aScalaSeq = Seq(1, 2, 3) map { intValue(_): AbstractConfigValue }
- val aList = new SimpleConfigList(fakeOrigin(), aScalaSeq.asJava)
- val sameAsAList = new SimpleConfigList(fakeOrigin(), aScalaSeq.asJava)
- val bScalaSeq = Seq(4, 5, 6) map { intValue(_): AbstractConfigValue }
- val bList = new SimpleConfigList(fakeOrigin(), bScalaSeq.asJava)
-
- checkEqualObjects(aList, aList)
- checkEqualObjects(aList, sameAsAList)
- checkNotEqualObjects(aList, bList)
- }
-
@Test
def configListSerializable(): Unit = {
val expectedSerialization = "" +
@@ -330,23 +224,6 @@ class ConfigValueTest extends TestUtils {
assertEquals(1, bList.get(0).unwrapped)
}
- @Test
- def configReferenceEquality(): Unit = {
- val a = subst("foo")
- val sameAsA = subst("foo")
- val b = subst("bar")
- val c = subst("foo", optional = true)
-
- assertTrue("wrong type " + a, a.isInstanceOf[ConfigReference])
- assertTrue("wrong type " + b, b.isInstanceOf[ConfigReference])
- assertTrue("wrong type " + c, c.isInstanceOf[ConfigReference])
-
- checkEqualObjects(a, a)
- checkEqualObjects(a, sameAsA)
- checkNotEqualObjects(a, b)
- checkNotEqualObjects(a, c)
- }
-
@Test
def configReferenceNotSerializable(): Unit = {
val a = subst("foo")
@@ -354,23 +231,6 @@ class ConfigValueTest extends TestUtils {
checkNotSerializable(a)
}
- @Test
- def configConcatenationEquality(): Unit = {
- val a = substInString("foo")
- val sameAsA = substInString("foo")
- val b = substInString("bar")
- val c = substInString("foo", optional = true)
-
- assertTrue("wrong type " + a, a.isInstanceOf[ConfigConcatenation])
- assertTrue("wrong type " + b, b.isInstanceOf[ConfigConcatenation])
- assertTrue("wrong type " + c, c.isInstanceOf[ConfigConcatenation])
-
- checkEqualObjects(a, a)
- checkEqualObjects(a, sameAsA)
- checkNotEqualObjects(a, b)
- checkNotEqualObjects(a, c)
- }
-
@Test
def configConcatenationNotSerializable(): Unit = {
val a = substInString("foo")
@@ -378,28 +238,6 @@ class ConfigValueTest extends TestUtils {
checkNotSerializable(a)
}
- @Test
- def configDelayedMergeEquality(): Unit = {
- val s1 = subst("foo")
- val s2 = subst("bar")
- val a = new ConfigDelayedMerge(
- fakeOrigin(),
- List[AbstractConfigValue](s1, s2).asJava
- )
- val sameAsA = new ConfigDelayedMerge(
- fakeOrigin(),
- List[AbstractConfigValue](s1, s2).asJava
- )
- val b = new ConfigDelayedMerge(
- fakeOrigin(),
- List[AbstractConfigValue](s2, s1).asJava
- )
-
- checkEqualObjects(a, a)
- checkEqualObjects(a, sameAsA)
- checkNotEqualObjects(a, b)
- }
-
@Test
def configDelayedMergeNotSerializable(): Unit = {
val s1 = subst("foo")
@@ -411,29 +249,6 @@ class ConfigValueTest extends TestUtils {
checkNotSerializable(a)
}
- @Test
- def configDelayedMergeObjectEquality(): Unit = {
- val empty = SimpleConfigObject.empty()
- val s1 = subst("foo")
- val s2 = subst("bar")
- val a = new ConfigDelayedMergeObject(
- fakeOrigin(),
- List[AbstractConfigValue](empty, s1, s2).asJava
- )
- val sameAsA = new ConfigDelayedMergeObject(
- fakeOrigin(),
- List[AbstractConfigValue](empty, s1, s2).asJava
- )
- val b = new ConfigDelayedMergeObject(
- fakeOrigin(),
- List[AbstractConfigValue](empty, s2, s1).asJava
- )
-
- checkEqualObjects(a, a)
- checkEqualObjects(a, sameAsA)
- checkNotEqualObjects(a, b)
- }
-
@Test
def configDelayedMergeObjectNotSerializable(): Unit = {
val empty = SimpleConfigObject.empty()
@@ -446,342 +261,6 @@ class ConfigValueTest extends TestUtils {
checkNotSerializable(a)
}
- @Test
- def valuesToString(): Unit = {
- // just check that these don't throw, the exact output
- // isn't super important since it's just for debugging
- intValue(10).toString()
- longValue(11).toString()
- doubleValue(3.14).toString()
- stringValue("hi").toString()
- nullValue().toString()
- boolValue(true).toString()
- val emptyObj = SimpleConfigObject.empty()
- emptyObj.toString()
- (new SimpleConfigList(
- fakeOrigin(),
- Collections.emptyList[AbstractConfigValue]()
- )).toString()
- subst("a").toString()
- substInString("b").toString()
- val dm = new ConfigDelayedMerge(
- fakeOrigin(),
- List[AbstractConfigValue](subst("a"), subst("b")).asJava
- )
- dm.toString()
- val dmo = new ConfigDelayedMergeObject(
- fakeOrigin(),
- List[AbstractConfigValue](emptyObj, subst("a"), subst("b")).asJava
- )
- dmo.toString()
-
- fakeOrigin().toString()
- }
-
- private def unsupported(body: => Unit): Unit = {
- intercept[UnsupportedOperationException] {
- body
- }
- }
-
- @Test
- def configObjectUnwraps(): Unit = {
- val m = new SimpleConfigObject(
- fakeOrigin(),
- configMap("a" -> 1, "b" -> 2, "c" -> 3)
- )
- assertEquals(Map("a" -> 1, "b" -> 2, "c" -> 3), m.unwrapped.asScala)
- }
-
- @Test
- def configObjectImplementsMap(): Unit = {
- val m: ConfigObject = new SimpleConfigObject(
- fakeOrigin(),
- configMap("a" -> 1, "b" -> 2, "c" -> 3)
- )
-
- assertEquals(intValue(1), m.get("a"))
- assertEquals(intValue(2), m.get("b"))
- assertEquals(intValue(3), m.get("c"))
- assertNull(m.get("d"))
- // get can take a non-string
- assertNull(m.get(new Object()))
-
- assertTrue(m.containsKey("a"))
- assertFalse(m.containsKey("z"))
- // containsKey can take a non-string
- assertFalse(m.containsKey(new Object()))
-
- assertTrue(m.containsValue(intValue(1)))
- assertFalse(m.containsValue(intValue(10)))
-
- // can take a non-ConfigValue
- assertFalse(m.containsValue(new Object()))
-
- assertFalse(m.isEmpty())
-
- assertEquals(3, m.size())
-
- val values = Set(intValue(1), intValue(2), intValue(3))
- assertEquals(values, m.values().asScala.toSet)
- assertEquals(values, (m.entrySet().asScala map { _.getValue() }).toSet)
-
- val keys = Set("a", "b", "c")
- assertEquals(keys, m.keySet().asScala.toSet)
- assertEquals(keys, (m.entrySet().asScala map { _.getKey() }).toSet)
-
- unsupported { m.clear() }
- unsupported { m.put("hello", intValue(42)) }
- unsupported {
- m.putAll(Collections.emptyMap[String, AbstractConfigValue]())
- }
- unsupported { m.remove("a") }
- }
-
- @Test
- def configListImplementsList(): Unit = {
- val scalaSeq = Seq[AbstractConfigValue](
- stringValue("a"),
- stringValue("b"),
- stringValue("c")
- )
- val l: ConfigList = new SimpleConfigList(fakeOrigin(), scalaSeq.asJava)
-
- assertEquals(scalaSeq(0), l.get(0))
- assertEquals(scalaSeq(1), l.get(1))
- assertEquals(scalaSeq(2), l.get(2))
-
- assertTrue(l.contains(stringValue("a")))
-
- assertTrue(
- l.containsAll(List[AbstractConfigValue](stringValue("b")).asJava)
- )
- assertFalse(
- l.containsAll(List[AbstractConfigValue](stringValue("d")).asJava)
- )
-
- assertEquals(1, l.indexOf(scalaSeq(1)))
-
- assertFalse(l.isEmpty())
-
- assertEquals(scalaSeq, l.iterator().asScala.toSeq)
-
- unsupported { l.iterator().remove() }
-
- assertEquals(1, l.lastIndexOf(scalaSeq(1)))
-
- val li = l.listIterator()
- var i = 0
- while (li.hasNext()) {
- assertEquals(i > 0, li.hasPrevious())
- assertEquals(i, li.nextIndex())
- assertEquals(i - 1, li.previousIndex())
-
- unsupported { li.remove() }
- unsupported { li.add(intValue(3)) }
- unsupported { li.set(stringValue("foo")) }
-
- val v = li.next()
- assertEquals(l.get(i), v)
-
- if (li.hasPrevious()) {
- // go backward
- assertEquals(scalaSeq(i), li.previous())
- // go back forward
- li.next()
- }
-
- i += 1
- }
-
- l.listIterator(1) // doesn't throw!
-
- assertEquals(3, l.size())
-
- assertEquals(scalaSeq.tail, l.subList(1, l.size()).asScala)
-
- assertEquals(scalaSeq, l.toArray.toList)
-
- assertEquals(scalaSeq, l.toArray(new Array[ConfigValue](l.size())).toList)
-
- unsupported { l.add(intValue(3)) }
- unsupported { l.add(1, intValue(4)) }
- unsupported { l.addAll(List[ConfigValue]().asJava) }
- unsupported { l.addAll(1, List[ConfigValue]().asJava) }
- unsupported { l.clear() }
- unsupported { l.remove(intValue(2)) }
- unsupported { l.remove(1) }
- unsupported { l.removeAll(List[ConfigValue](intValue(1)).asJava) }
- unsupported { l.retainAll(List[ConfigValue](intValue(1)).asJava) }
- unsupported { l.set(0, intValue(42)) }
- }
-
- private def unresolved(body: => Unit): Unit = {
- intercept[ConfigException.NotResolved] {
- body
- }
- }
-
- @Test
- def notResolvedThrown(): Unit = {
- // ConfigSubstitution
- unresolved { subst("foo").valueType }
- unresolved { subst("foo").unwrapped }
-
- // ConfigDelayedMerge
- val dm = new ConfigDelayedMerge(
- fakeOrigin(),
- List[AbstractConfigValue](subst("a"), subst("b")).asJava
- )
- unresolved { dm.valueType }
- unresolved { dm.unwrapped }
-
- // ConfigDelayedMergeObject
- val emptyObj = SimpleConfigObject.empty()
- val dmo = new ConfigDelayedMergeObject(
- fakeOrigin(),
- List[AbstractConfigValue](emptyObj, subst("a"), subst("b")).asJava
- )
- assertEquals(ConfigValueType.OBJECT, dmo.valueType)
- unresolved { dmo.unwrapped }
- unresolved { dmo.get("foo") }
- unresolved { dmo.containsKey(null) }
- unresolved { dmo.containsValue(null) }
- unresolved { dmo.entrySet() }
- unresolved { dmo.isEmpty() }
- unresolved { dmo.keySet() }
- unresolved { dmo.size() }
- unresolved { dmo.values() }
- unresolved { dmo.toConfig.getInt("foo") }
- }
-
- @Test
- def roundTripNumbersThroughString(): Unit = {
- // formats rounded off with E notation
- val a = "132454454354353245.3254652656454808909932874873298473298472"
- // formats as 100000.0
- val b = "1e6"
- // formats as 5.0E-5
- val c = "0.00005"
- // formats as 1E100 (capital E)
- val d = "1e100"
-
- val obj = parseConfig(
- "{ a : " + a + ", b : " + b + ", c : " + c + ", d : " + d + "}"
- )
- assertEquals(
- Seq(a, b, c, d),
- Seq("a", "b", "c", "d") map {
- obj.getString(_)
- }
- )
-
- // make sure it still works if we're doing concatenation
- val obj2 = parseConfig(
- "{ a : xx " + a + " yy, b : xx " + b + " yy, c : xx " + c + " yy, d : xx " + d + " yy}"
- )
- assertEquals(
- Seq(a, b, c, d) map { "xx " + _ + " yy" },
- Seq("a", "b", "c", "d") map { obj2.getString(_) }
- )
- }
-
- @Test
- def mergeOriginsWorks(): Unit = {
- def o(desc: String, empty: Boolean) = {
- val values = new java.util.HashMap[String, AbstractConfigValue]()
- if (!empty)
- values.put("hello", intValue(37))
- new SimpleConfigObject(SimpleConfigOrigin.newSimple(desc), values)
- }
- def m(values: AbstractConfigObject*) = {
- AbstractConfigObject.mergeOrigins(values: _*).description
- }
-
- // simplest case
- assertEquals("merge of a,b", m(o("a", false), o("b", false)))
- // combine duplicate "merge of"
- assertEquals("merge of a,x,y", m(o("a", false), o("merge of x,y", false)))
- assertEquals(
- "merge of a,b,x,y",
- m(o("merge of a,b", false), o("merge of x,y", false))
- )
- // ignore empty objects
- assertEquals("a", m(o("foo", true), o("a", false)))
- // unless they are all empty, pick the first one
- assertEquals("foo", m(o("foo", true), o("a", true)))
- // merge just one
- assertEquals("foo", m(o("foo", false)))
- // merge three
- assertEquals(
- "merge of a,b,c",
- m(o("a", false), o("b", false), o("c", false))
- )
- }
-
- @Test
- def hasPathWorks(): Unit = {
- val empty = parseConfig("{}")
-
- assertFalse(empty.hasPath("foo"))
-
- val obj = parseConfig("a=null, b.c.d=11, foo=bar")
-
- // returns true for the non-null values
- assertTrue(obj.hasPath("foo"))
- assertTrue(obj.hasPath("b.c.d"))
- assertTrue(obj.hasPath("b.c"))
- assertTrue(obj.hasPath("b"))
-
- // hasPath() is false for null values but containsKey is true
- assertEquals(nullValue(), obj.root.get("a"))
- assertTrue(obj.root.containsKey("a"))
- assertFalse(obj.hasPath("a"))
-
- // false for totally absent values
- assertFalse(obj.root.containsKey("notinhere"))
- assertFalse(obj.hasPath("notinhere"))
-
- // throws proper exceptions
- intercept[ConfigException.BadPath] {
- empty.hasPath("a.")
- }
-
- intercept[ConfigException.BadPath] {
- empty.hasPath("..")
- }
- }
-
- @Test
- def newNumberWorks(): Unit = {
- def nL(v: Long) = ConfigNumber.newNumber(fakeOrigin(), v, null)
- def nD(v: Double) = ConfigNumber.newNumber(fakeOrigin(), v, null)
-
- // the general idea is that the destination type should depend
- // only on the actual numeric value, not on the type of the source
- // value.
- assertEquals(3.14, nD(3.14).unwrapped)
- assertEquals(1, nL(1).unwrapped)
- assertEquals(1, nD(1.0).unwrapped)
- assertEquals(Int.MaxValue + 1L, nL(Int.MaxValue + 1L).unwrapped)
- assertEquals(Int.MinValue - 1L, nL(Int.MinValue - 1L).unwrapped)
- assertEquals(Int.MaxValue + 1L, nD(Int.MaxValue + 1.0).unwrapped)
- assertEquals(Int.MinValue - 1L, nD(Int.MinValue - 1.0).unwrapped)
- }
-
- @Test
- def automaticBooleanConversions(): Unit = {
- val trues = parseObject("{ a=true, b=yes, c=on }").toConfig
- assertEquals(true, trues.getBoolean("a"))
- assertEquals(true, trues.getBoolean("b"))
- assertEquals(true, trues.getBoolean("c"))
-
- val falses = parseObject("{ a=false, b=no, c=off }").toConfig
- assertEquals(false, falses.getBoolean("a"))
- assertEquals(false, falses.getBoolean("b"))
- assertEquals(false, falses.getBoolean("c"))
- }
-
@Test
def configOriginFileAndLine(): Unit = {
val hasFilename = SimpleConfigOrigin.newFile("foo")
@@ -818,242 +297,6 @@ class ConfigValueTest extends TestUtils {
assertEquals("file:/foo", urlOrigin.url.toExternalForm)
}
- @Test
- def withOnly(): Unit = {
- val obj = parseObject("{ a=1, b=2, c.d.y=3, e.f.g=4, c.d.z=5 }")
- assertEquals("keep only a", parseObject("{ a=1 }"), obj.withOnlyKey("a"))
- assertEquals(
- "keep only e",
- parseObject("{ e.f.g=4 }"),
- obj.withOnlyKey("e")
- )
- assertEquals(
- "keep only c.d",
- parseObject("{ c.d.y=3, c.d.z=5 }"),
- obj.toConfig.withOnlyPath("c.d").root
- )
- assertEquals(
- "keep only c.d.z",
- parseObject("{ c.d.z=5 }"),
- obj.toConfig.withOnlyPath("c.d.z").root
- )
- assertEquals(
- "keep nonexistent key",
- parseObject("{ }"),
- obj.withOnlyKey("nope")
- )
- assertEquals(
- "keep nonexistent path",
- parseObject("{ }"),
- obj.toConfig.withOnlyPath("q.w.e.r.t.y").root
- )
- assertEquals(
- "keep only nonexistent underneath non-object",
- parseObject("{ }"),
- obj.toConfig.withOnlyPath("a.nonexistent").root
- )
- assertEquals(
- "keep only nonexistent underneath nested non-object",
- parseObject("{ }"),
- obj.toConfig.withOnlyPath("c.d.z.nonexistent").root
- )
- }
-
- @Test
- def withOnlyInvolvingUnresolved(): Unit = {
- val obj = parseObject(
- "{ a = {}, a=${x}, b=${y}, b=${z}, x={asf:1}, y=2, z=3 }"
- )
- assertEquals(
- "keep only a.asf",
- parseObject("{ a={asf:1} }"),
- obj.toConfig.resolve().withOnlyPath("a.asf").root
- )
-
- intercept[ConfigException.UnresolvedSubstitution] {
- obj.withOnlyKey("a").toConfig.resolve()
- }
-
- intercept[ConfigException.UnresolvedSubstitution] {
- obj.withOnlyKey("b").toConfig.resolve()
- }
-
- assertEquals(ResolveStatus.UNRESOLVED, obj.resolveStatus)
- assertEquals(ResolveStatus.RESOLVED, obj.withOnlyKey("z").resolveStatus)
- }
-
- @Test
- def without(): Unit = {
- val obj = parseObject("{ a=1, b=2, c.d.y=3, e.f.g=4, c.d.z=5 }")
- assertEquals(
- "without a",
- parseObject("{ b=2, c.d.y=3, e.f.g=4, c.d.z=5 }"),
- obj.withoutKey("a")
- )
- assertEquals(
- "without c",
- parseObject("{ a=1, b=2, e.f.g=4 }"),
- obj.withoutKey("c")
- )
- assertEquals(
- "without c.d",
- parseObject("{ a=1, b=2, e.f.g=4, c={} }"),
- obj.toConfig.withoutPath("c.d").root
- )
- assertEquals(
- "without c.d.z",
- parseObject("{ a=1, b=2, c.d.y=3, e.f.g=4 }"),
- obj.toConfig.withoutPath("c.d.z").root
- )
- assertEquals(
- "without nonexistent key",
- parseObject("{ a=1, b=2, c.d.y=3, e.f.g=4, c.d.z=5 }"),
- obj.withoutKey("nonexistent")
- )
- assertEquals(
- "without nonexistent path",
- parseObject("{ a=1, b=2, c.d.y=3, e.f.g=4, c.d.z=5 }"),
- obj.toConfig.withoutPath("q.w.e.r.t.y").root
- )
- assertEquals(
- "without nonexistent path with existing prefix",
- parseObject("{ a=1, b=2, c.d.y=3, e.f.g=4, c.d.z=5 }"),
- obj.toConfig.withoutPath("a.foo").root
- )
- }
-
- @Test
- def withoutInvolvingUnresolved(): Unit = {
- val obj = parseObject(
- "{ a = {}, a=${x}, b=${y}, b=${z}, x={asf:1}, y=2, z=3 }"
- )
- assertEquals(
- "without a.asf",
- parseObject("{ a={}, b=3, x={asf:1}, y=2, z=3 }"),
- obj.toConfig.resolve().withoutPath("a.asf").root
- )
-
- intercept[ConfigException.UnresolvedSubstitution] {
- obj.withoutKey("x").toConfig.resolve()
- }
-
- intercept[ConfigException.UnresolvedSubstitution] {
- obj.withoutKey("z").toConfig.resolve()
- }
-
- assertEquals(ResolveStatus.UNRESOLVED, obj.resolveStatus)
- assertEquals(ResolveStatus.UNRESOLVED, obj.withoutKey("a").resolveStatus)
- assertEquals(
- ResolveStatus.RESOLVED,
- obj.withoutKey("a").withoutKey("b").resolveStatus
- )
- }
-
- @Test
- def atPathWorksOneElement(): Unit = {
- val v = ConfigValueFactory.fromAnyRef(42: Integer)
- val config = v.atPath("a")
- assertEquals(parseConfig("a=42"), config)
- assertTrue(config.getValue("a") eq v)
- assertTrue(config.origin.description.contains("atPath"))
- }
-
- @Test
- def atPathWorksTwoElements(): Unit = {
- val v = ConfigValueFactory.fromAnyRef(42: Integer)
- val config = v.atPath("a.b")
- assertEquals(parseConfig("a.b=42"), config)
- assertTrue(config.getValue("a.b") eq v)
- assertTrue(config.origin.description.contains("atPath"))
- }
-
- @Test
- def atPathWorksFourElements(): Unit = {
- val v = ConfigValueFactory.fromAnyRef(42: Integer)
- val config = v.atPath("a.b.c.d")
- assertEquals(parseConfig("a.b.c.d=42"), config)
- assertTrue(config.getValue("a.b.c.d") eq v)
- assertTrue(config.origin.description.contains("atPath"))
- }
-
- @Test
- def atKeyWorks(): Unit = {
- val v = ConfigValueFactory.fromAnyRef(42: Integer)
- val config = v.atKey("a")
- assertEquals(parseConfig("a=42"), config)
- assertTrue(config.getValue("a") eq v)
- assertTrue(config.origin.description.contains("atKey"))
- }
-
- @Test
- def withValueDepth1FromEmpty(): Unit = {
- val v = ConfigValueFactory.fromAnyRef(42: Integer)
- val config = ConfigFactory.empty().withValue("a", v)
- assertEquals(parseConfig("a=42"), config)
- assertTrue(config.getValue("a") eq v)
- }
-
- @Test
- def withValueDepth2FromEmpty(): Unit = {
- val v = ConfigValueFactory.fromAnyRef(42: Integer)
- val config = ConfigFactory.empty().withValue("a.b", v)
- assertEquals(parseConfig("a.b=42"), config)
- assertTrue(config.getValue("a.b") eq v)
- }
-
- @Test
- def withValueDepth3FromEmpty(): Unit = {
- val v = ConfigValueFactory.fromAnyRef(42: Integer)
- val config = ConfigFactory.empty().withValue("a.b.c", v)
- assertEquals(parseConfig("a.b.c=42"), config)
- assertTrue(config.getValue("a.b.c") eq v)
- }
-
- @Test
- def withValueDepth1OverwritesExisting(): Unit = {
- val v = ConfigValueFactory.fromAnyRef(47: Integer)
- val old = v.atPath("a")
- val config = old.withValue("a", ConfigValueFactory.fromAnyRef(42: Integer))
- assertEquals(parseConfig("a=42"), config)
- assertEquals(42, config.getInt("a"))
- }
-
- @Test
- def withValueDepth2OverwritesExisting(): Unit = {
- val v = ConfigValueFactory.fromAnyRef(47: Integer)
- val old = v.atPath("a.b")
- val config =
- old.withValue("a.b", ConfigValueFactory.fromAnyRef(42: Integer))
- assertEquals(parseConfig("a.b=42"), config)
- assertEquals(42, config.getInt("a.b"))
- }
-
- @Test
- def withValueInsideExistingObject(): Unit = {
- val v = ConfigValueFactory.fromAnyRef(47: Integer)
- val old = v.atPath("a.c")
- val config =
- old.withValue("a.b", ConfigValueFactory.fromAnyRef(42: Integer))
- assertEquals(parseConfig("a.b=42,a.c=47"), config)
- assertEquals(42, config.getInt("a.b"))
- assertEquals(47, config.getInt("a.c"))
- }
-
- @Test
- def withValueBuildComplexConfig(): Unit = {
- val v1 = ConfigValueFactory.fromAnyRef(1: Integer)
- val v2 = ConfigValueFactory.fromAnyRef(2: Integer)
- val v3 = ConfigValueFactory.fromAnyRef(3: Integer)
- val v4 = ConfigValueFactory.fromAnyRef(4: Integer)
- val config = ConfigFactory
- .empty()
- .withValue("a", v1)
- .withValue("b.c", v2)
- .withValue("b.d", v3)
- .withValue("x.y.z", v4)
- assertEquals(parseConfig("a=1,b.c=2,b.d=3,x.y.z=4"), config)
- }
-
@Test
def configOriginsInSerialization(): Unit = {
val bases = Seq(
@@ -1126,40 +369,4 @@ class ConfigValueTest extends TestUtils {
assertEquals(bottom(v), bottom(deserialized))
}
}
-
- @Test
- def renderWithNewlinesInDescription(): Unit = {
- val v = ConfigValueFactory.fromAnyRef(
- 89: Integer,
- "this is a description\nwith some\nnewlines"
- )
- val list = new SimpleConfigList(
- SimpleConfigOrigin.newSimple("\n5\n6\n7\n"),
- java.util.Collections.singletonList(v.asInstanceOf[AbstractConfigValue])
- )
- val conf = ConfigFactory.empty().withValue("bar", list)
- val rendered = conf.root.render
- def assertHas(s: String): Unit =
- assertTrue(s"has ${s.replace("\n", "\\n")} in it", rendered.contains(s))
- assertHas("is a description\n")
- assertHas("with some\n")
- assertHas("newlines\n")
- assertHas("#\n")
- assertHas("5\n")
- assertHas("6\n")
- assertHas("7\n")
- val parsed = ConfigFactory.parseString(rendered)
-
- assertEquals(conf, parsed)
- }
-
- @Test
- def renderSorting(): Unit = {
- val config = parseConfig("""0=a,1=b,2=c,3=d,10=e,20=f,30=g""")
- val rendered = config.root.render(ConfigRenderOptions.concise)
- assertEquals(
- """{"0":"a","1":"b","2":"c","3":"d","10":"e","20":"f","30":"g"}""",
- rendered
- )
- }
}
diff --git a/sconfig/jvm/src/test/scala/org/ekrich/config/impl/ParseableReaderTest.scala b/sconfig/jvm/src/test/scala/org/ekrich/config/impl/ParseableReaderTest.scala
deleted file mode 100644
index bfcd5a23..00000000
--- a/sconfig/jvm/src/test/scala/org/ekrich/config/impl/ParseableReaderTest.scala
+++ /dev/null
@@ -1,40 +0,0 @@
-package org.ekrich.config.impl
-
-import java.io.InputStreamReader
-
-import org.ekrich.config.{ConfigException, ConfigFactory, ConfigParseOptions}
-import org.hamcrest.CoreMatchers.containsString
-import org.hamcrest.MatcherAssert.assertThat
-import org.junit.Assert._
-import org.junit.Test
-
-class ParseableReaderTest extends TestUtils {
- @Test
- def parse(): Unit = {
- val filename = "/test01.properties"
- val configInput = new InputStreamReader(
- getClass.getResourceAsStream(filename)
- )
- val config = ConfigFactory.parseReader(
- configInput,
- ConfigParseOptions.defaults
- .setSyntaxFromFilename(filename)
- )
- assertEquals("hello^^", config.getString("fromProps.specialChars"))
- }
-
- @Test
- def parseIncorrectFormat(): Unit = {
- val filename = "/test01.properties"
- val configInput = new InputStreamReader(
- getClass.getResourceAsStream(filename)
- )
- val e = intercept[ConfigException.Parse] {
- ConfigFactory.parseReader(configInput)
- }
- assertThat(
- e.getMessage,
- containsString("Expecting end of input or a comma, got '^'")
- )
- }
-}
diff --git a/sconfig/native/src/main/scala/java/net/URL.scala b/sconfig/native/src/main/scala/java/net/URL.scala
new file mode 100644
index 00000000..3ea2f7af
--- /dev/null
+++ b/sconfig/native/src/main/scala/java/net/URL.scala
@@ -0,0 +1,21 @@
+package java.net
+
+// from Scala Native with @stub removed
+class URL(from: String) {
+ def getPath(): java.lang.String = ???
+
+ def getProtocol(): java.lang.String = ???
+
+ def openConnection(): java.net.URLConnection = ???
+
+ def openStream(): java.io.InputStream = ???
+
+ override def hashCode: Int = ???
+
+ def toURI(): java.net.URI = ???
+
+ def toExternalForm(): java.lang.String = ???
+
+ // added
+ def getFile(): String = ???
+}
diff --git a/sconfig/native/src/main/scala/org/ekrich/config/impl/PlatformConfigFactory.scala b/sconfig/native/src/main/scala/org/ekrich/config/impl/PlatformConfigFactory.scala
new file mode 100644
index 00000000..0ca38cff
--- /dev/null
+++ b/sconfig/native/src/main/scala/org/ekrich/config/impl/PlatformConfigFactory.scala
@@ -0,0 +1,6 @@
+package org.ekrich.config
+
+/**
+ * [[ConfigFactory]] methods for Scala Native platform
+ */
+abstract class PlatformConfigFactory extends ConfigFactoryJvmNative {}
diff --git a/sconfig/js/src/main/scala/PlatformThread.scala b/sconfig/native/src/main/scala/org/ekrich/config/impl/PlatformThread.scala
similarity index 69%
rename from sconfig/js/src/main/scala/PlatformThread.scala
rename to sconfig/native/src/main/scala/org/ekrich/config/impl/PlatformThread.scala
index 76656e5b..930074da 100644
--- a/sconfig/js/src/main/scala/PlatformThread.scala
+++ b/sconfig/native/src/main/scala/org/ekrich/config/impl/PlatformThread.scala
@@ -3,6 +3,6 @@ package org.ekrich.config.impl
/**
* To workaround missing implementations
*/
-class PlatformThread(thread: Thread) extends ThreadLike {
+class PlatformThread(thread: Thread) extends TraitThread {
def getContextClassLoader(): ClassLoader = ???
}
diff --git a/sconfig/shared/src/main/scala/org/ekrich/config/Config.scala b/sconfig/shared/src/main/scala/org/ekrich/config/Config.scala
index f32c26ee..750a58a8 100644
--- a/sconfig/shared/src/main/scala/org/ekrich/config/Config.scala
+++ b/sconfig/shared/src/main/scala/org/ekrich/config/Config.scala
@@ -117,18 +117,17 @@ import scala.annotation.varargs
* Serialization
*
*
Convert a `Config` to a JSON or HOCON string by calling [[#root root]] to
- * get the [[ConfigObject]] and then call
- * [[ConfigValue!.render():String render]] on the root object,
- * `myConfig.root.render`. There's also a variant
+ * get the [[ConfigObject]] and then call [[ConfigValue!.render:String render]]
+ * on the root object, `myConfig.root.render`. There's also a variant
* [[ConfigValue!.render(options:org\.ekrich\.config\.ConfigRenderOptions)* render(ConfigRenderOptions)]]
* inherited from [[ConfigValue]] which allows you to control the format of the
* rendered string. (See [[ConfigRenderOptions]].) Note that `Config` does not
* remember the formatting of the original file, so if you load, modify, and
* re-save a config file, it will be substantially reformatted.
*
- *
As an alternative to [[ConfigValue!.render render]], the `toString`
- * method produces a debug-output-oriented representation (which is not valid
- * JSON).
+ *
As an alternative to [[ConfigValue!.render:String render]], the
+ * `toString` method produces a debug-output-oriented representation (which is
+ * not valid JSON).
*
* Note: no arg render links do not link correctly. See
* https://github.com/lampepfl/dotty/issues/14212
diff --git a/sconfig/shared/src/main/scala/org/ekrich/config/ConfigFactory.scala b/sconfig/shared/src/main/scala/org/ekrich/config/ConfigFactory.scala
index 93a2fbd9..61061963 100644
--- a/sconfig/shared/src/main/scala/org/ekrich/config/ConfigFactory.scala
+++ b/sconfig/shared/src/main/scala/org/ekrich/config/ConfigFactory.scala
@@ -3,7 +3,7 @@
*/
package org.ekrich.config
-import java.io.{File, Reader}
+import java.io.File
import java.net.URL
import java.{util => ju}
import java.util.Properties
@@ -29,7 +29,7 @@ import org.ekrich.config.impl.Parseable
* sure to read the package
* overview which describes the big picture as shown in those examples.
*/
-object ConfigFactory {
+object ConfigFactory extends PlatformConfigFactory {
private val STRATEGY_PROPERTY_NAME = "config.strategy"
/**
@@ -615,40 +615,6 @@ object ConfigFactory {
def parseProperties(properties: Properties): Config =
parseProperties(properties, ConfigParseOptions.defaults)
- /**
- * Parses a Reader into a Config instance. Does not call
- * [[Config!.resolve()* Config.resolve()]] or merge the parsed stream with any
- * other configuration; this method parses a single stream and does nothing
- * else. It does process "include" statements in the parsed stream, and may
- * end up doing other IO due to those statements.
- *
- * @param reader
- * the reader to parse
- * @param options
- * parse options to control how the reader is interpreted
- * @return
- * the parsed configuration
- * @throws ConfigException
- * on IO or parse errors
- */
- def parseReader(reader: Reader, options: ConfigParseOptions): Config =
- Parseable.newReader(reader, options).parse().toConfig
-
- /**
- * Parses a reader into a Config instance as with
- * [[#parseReader(reader:java\.io\.Reader,options:org\.ekrich\.config\.ConfigParseOptions)* parseReader(Reader, ConfigParseOptions)]]
- * but always uses the default parse options.
- *
- * @param reader
- * the reader to parse
- * @return
- * the parsed configuration
- * @throws ConfigException
- * on IO or parse errors
- */
- def parseReader(reader: Reader): Config =
- parseReader(reader, ConfigParseOptions.defaults)
-
/**
* Parses a URL into a Config instance. Does not call
* [[Config!.resolve()* Config.resolve()]] or merge the parsed stream with any
@@ -1039,31 +1005,6 @@ object ConfigFactory {
def parseResourcesAnySyntax(resourceBasename: String): Config =
parseResourcesAnySyntax(resourceBasename, ConfigParseOptions.defaults)
- /**
- * Parses a string (which should be valid HOCON or JSON by default, or the
- * syntax specified in the options otherwise).
- *
- * @param s
- * string to parse
- * @param options
- * parse options
- * @return
- * the parsed configuration
- */
- def parseString(s: String, options: ConfigParseOptions): Config =
- Parseable.newString(s, options).parse().toConfig
-
- /**
- * Parses a string (which should be valid HOCON or JSON).
- *
- * @param s
- * string to parse
- * @return
- * the parsed configuration
- */
- def parseString(s: String): Config =
- parseString(s, ConfigParseOptions.defaults)
-
/**
* Creates a [[Config]] based on a `java.util.Map` from paths to plain Java
* values. Similar to
diff --git a/sconfig/shared/src/main/scala/org/ekrich/config/ConfigFactoryCommon.scala b/sconfig/shared/src/main/scala/org/ekrich/config/ConfigFactoryCommon.scala
new file mode 100644
index 00000000..45a0c5c8
--- /dev/null
+++ b/sconfig/shared/src/main/scala/org/ekrich/config/ConfigFactoryCommon.scala
@@ -0,0 +1,68 @@
+package org.ekrich.config
+
+import java.io.Reader
+
+import org.ekrich.config.impl.Parseable
+
+abstract class ConfigFactoryCommon {
+
+ /**
+ * Parses a string (which should be valid HOCON or JSON by default, or the
+ * syntax specified in the options otherwise).
+ *
+ * @param s
+ * string to parse
+ * @param options
+ * parse options
+ * @return
+ * the parsed configuration
+ */
+ def parseString(s: String, options: ConfigParseOptions): Config =
+ Parseable.newString(s, options).parse().toConfig
+
+ /**
+ * Parses a string (which should be valid HOCON or JSON).
+ *
+ * @param s
+ * string to parse
+ * @return
+ * the parsed configuration
+ */
+ def parseString(s: String): Config =
+ parseString(s, ConfigParseOptions.defaults)
+
+ /**
+ * Parses a Reader into a Config instance. Does not call
+ * [[Config!.resolve()* Config.resolve()]] or merge the parsed stream with any
+ * other configuration; this method parses a single stream and does nothing
+ * else. It does process "include" statements in the parsed stream, and may
+ * end up doing other IO due to those statements.
+ *
+ * @param reader
+ * the reader to parse
+ * @param options
+ * parse options to control how the reader is interpreted
+ * @return
+ * the parsed configuration
+ * @throws ConfigException
+ * on IO or parse errors
+ */
+ def parseReader(reader: Reader, options: ConfigParseOptions): Config =
+ Parseable.newReader(reader, options).parse().toConfig
+
+ /**
+ * Parses a reader into a Config instance as with
+ * [[#parseReader(reader:java\.io\.Reader,options:org\.ekrich\.config\.ConfigParseOptions)* parseReader(Reader, ConfigParseOptions)]]
+ * but always uses the default parse options.
+ *
+ * @param reader
+ * the reader to parse
+ * @return
+ * the parsed configuration
+ * @throws ConfigException
+ * on IO or parse errors
+ */
+ def parseReader(reader: Reader): Config =
+ parseReader(reader, ConfigParseOptions.defaults)
+
+}
diff --git a/sconfig/shared/src/main/scala/ClassLoaderLike.scala b/sconfig/shared/src/main/scala/org/ekrich/config/impl/TraitClassLoader.scala
similarity index 89%
rename from sconfig/shared/src/main/scala/ClassLoaderLike.scala
rename to sconfig/shared/src/main/scala/org/ekrich/config/impl/TraitClassLoader.scala
index dea8caab..d7078ff9 100644
--- a/sconfig/shared/src/main/scala/ClassLoaderLike.scala
+++ b/sconfig/shared/src/main/scala/org/ekrich/config/impl/TraitClassLoader.scala
@@ -6,6 +6,6 @@ import java.{util => ju}
/**
* To workaround missing implementations in Scala.js and Scala Native
*/
-trait ClassLoaderLike {
+trait TraitClassLoader {
def getResources(name: String): ju.Enumeration[URL]
}
diff --git a/sconfig/shared/src/main/scala/ThreadLike.scala b/sconfig/shared/src/main/scala/org/ekrich/config/impl/TraitThread.scala
similarity index 88%
rename from sconfig/shared/src/main/scala/ThreadLike.scala
rename to sconfig/shared/src/main/scala/org/ekrich/config/impl/TraitThread.scala
index 31454cec..0265aa67 100644
--- a/sconfig/shared/src/main/scala/ThreadLike.scala
+++ b/sconfig/shared/src/main/scala/org/ekrich/config/impl/TraitThread.scala
@@ -3,6 +3,6 @@ package org.ekrich.config.impl
/**
* To workaround missing implementations in Scala.js and Scala Native
*/
-trait ThreadLike {
+trait TraitThread {
def getContextClassLoader(): ClassLoader
}
diff --git a/sconfig/shared/src/main/scala/UriLike.scala b/sconfig/shared/src/main/scala/org/ekrich/config/impl/TraitUri.scala
similarity index 89%
rename from sconfig/shared/src/main/scala/UriLike.scala
rename to sconfig/shared/src/main/scala/org/ekrich/config/impl/TraitUri.scala
index cac42949..6e7cd6f2 100644
--- a/sconfig/shared/src/main/scala/UriLike.scala
+++ b/sconfig/shared/src/main/scala/org/ekrich/config/impl/TraitUri.scala
@@ -5,6 +5,6 @@ import java.net.URL
/**
* To workaround missing implementations in Scala.js and Scala Native
*/
-trait UriLike {
+trait TraitUri {
def toURL(): URL
}
diff --git a/sconfig/shared/src/test/scala/junit/AssertThrows.scala b/sconfig/shared/src/test/scala/junit/AssertThrows.scala
deleted file mode 100644
index 4440916c..00000000
--- a/sconfig/shared/src/test/scala/junit/AssertThrows.scala
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Ported from Scala.js (https://www.scala-js.org/)
- *
- * Copyright EPFL.
- *
- * Licensed under Apache License 2.0
- * (https://www.apache.org/licenses/LICENSE-2.0).
- *
- * See the NOTICE file distributed with this work for
- * additional information regarding copyright ownership.
- */
-
-package scala.scalanative.junit.utils
-
-import org.junit.Assert
-import org.junit.function.ThrowingRunnable
-
-object AssertThrows {
- def assertThrows[T <: Throwable, U](
- expectedThrowable: Class[T],
- code: => U
- ): T = {
- Assert.assertThrows(
- expectedThrowable,
- new ThrowingRunnable {
- def run(): Unit = code
- }
- )
- }
-}
diff --git a/sconfig/shared/src/test/scala/ConfigFactoryTests.scala b/sconfig/shared/src/test/scala/org/ekrich/config/impl/ConfigFactoryTest.scala
similarity index 52%
rename from sconfig/shared/src/test/scala/ConfigFactoryTests.scala
rename to sconfig/shared/src/test/scala/org/ekrich/config/impl/ConfigFactoryTest.scala
index 4d882fb3..0b09221f 100644
--- a/sconfig/shared/src/test/scala/ConfigFactoryTests.scala
+++ b/sconfig/shared/src/test/scala/org/ekrich/config/impl/ConfigFactoryTest.scala
@@ -1,11 +1,28 @@
-package test
+package org.ekrich.config.impl
+
+import java.io.StringReader
import org.junit.Assert._
import org.junit.Test
+import org.ekrich.config.ConfigException
import org.ekrich.config.ConfigFactory
+import org.ekrich.config.ConfigParseOptions
+
+class ConfigFactoryTest {
+ // use string
+ val fileStr =
+ """
+ |# test01.properties file
+ |fromProps.abc=abc
+ |fromProps.one=1
+ |fromProps.bool=true
+ |fromProps.specialChars=hello^^
+ """.stripMargin
+
+ // create Reader
+ var test01Reader = new StringReader(fileStr)
-class ConfigFactoryTests {
@Test def parseString: Unit = {
val configStr =
"""
@@ -43,4 +60,26 @@ class ConfigFactoryTests {
val pattern = rconf.getList("core.extends").get(0).unwrapped
assertEquals(pattern, "default")
}
+
+ @Test
+ def parse(): Unit = {
+ val filename = "/test01.properties"
+ val config = ConfigFactory.parseReader(
+ test01Reader,
+ ConfigParseOptions.defaults
+ .setSyntaxFromFilename(filename)
+ )
+ assertEquals("hello^^", config.getString("fromProps.specialChars"))
+ }
+
+ @Test
+ def parseIncorrectFormat(): Unit = {
+ val e = assertThrows(
+ classOf[ConfigException.Parse],
+ () => ConfigFactory.parseReader(test01Reader)
+ )
+ assertTrue(
+ e.getMessage.contains("Expecting end of input or a comma, got '^'")
+ )
+ }
}
diff --git a/sconfig/shared/src/test/scala/org/ekrich/config/impl/ConfigSubstitutionSharedTest.scala b/sconfig/shared/src/test/scala/org/ekrich/config/impl/ConfigSubstitutionSharedTest.scala
new file mode 100644
index 00000000..8783bd82
--- /dev/null
+++ b/sconfig/shared/src/test/scala/org/ekrich/config/impl/ConfigSubstitutionSharedTest.scala
@@ -0,0 +1,1416 @@
+/**
+ * Copyright (C) 2011 Typesafe Inc.
+ */
+package org.ekrich.config.impl
+
+import org.junit.Assert._
+import org.junit._
+
+import org.ekrich.config.ConfigException
+import org.ekrich.config.ConfigResolveOptions
+import org.ekrich.config.ConfigFactory
+import scala.jdk.CollectionConverters._
+
+/**
+ * Should be able to handle env vars for Scala.js with this:
+ * https://github.com/typelevel/cats-effect/blob/44545879b453c9a9b91d5d3c472b58b4ab04adb1/std/js/src/main/scala/cats/effect/std/EnvCompanionPlatform.scala#L38
+ *
+ * Commented out tests compile so should and fail on JS but should be
+ * supportable
+ */
+class ConfigSubstitutionSharedTest extends TestUtilsShared {
+ private def resolveWithoutFallbacks(v: AbstractConfigObject) = {
+ val options = ConfigResolveOptions.noSystem
+ ResolveContext
+ .resolve(v, v, options)
+ .asInstanceOf[AbstractConfigObject]
+ .toConfig
+ }
+ private def resolveWithoutFallbacks(
+ s: AbstractConfigValue,
+ root: AbstractConfigObject
+ ) = {
+ val options = ConfigResolveOptions.noSystem
+ ResolveContext.resolve(s, root, options)
+ }
+
+ private def resolve(v: AbstractConfigObject) = {
+ val options = ConfigResolveOptions.defaults
+ ResolveContext
+ .resolve(v, v, options)
+ .asInstanceOf[AbstractConfigObject]
+ .toConfig
+ }
+ private def resolve(s: AbstractConfigValue, root: AbstractConfigObject) = {
+ val options = ConfigResolveOptions.defaults
+ ResolveContext.resolve(s, root, options)
+ }
+
+ private val simpleObject = {
+ parseObject("""
+{
+ "foo" : 42,
+ "bar" : {
+ "int" : 43,
+ "bool" : true,
+ "null" : null,
+ "string" : "hello",
+ "double" : 3.14
+ }
+}
+""")
+ }
+
+ @Test
+ def resolveTrivialKey(): Unit = {
+ val s = subst("foo")
+ val v = resolveWithoutFallbacks(s, simpleObject)
+ assertEquals(intValue(42), v)
+ }
+
+ @Test
+ def resolveTrivialPath(): Unit = {
+ val s = subst("bar.int")
+ val v = resolveWithoutFallbacks(s, simpleObject)
+ assertEquals(intValue(43), v)
+ }
+
+ @Test
+ def resolveInt(): Unit = {
+ val s = subst("bar.int")
+ val v = resolveWithoutFallbacks(s, simpleObject)
+ assertEquals(intValue(43), v)
+ }
+
+ @Test
+ def resolveBool(): Unit = {
+ val s = subst("bar.bool")
+ val v = resolveWithoutFallbacks(s, simpleObject)
+ assertEquals(boolValue(true), v)
+ }
+
+ @Test
+ def resolveNull(): Unit = {
+ val s = subst("bar.null")
+ val v = resolveWithoutFallbacks(s, simpleObject)
+ assertEquals(nullValue(), v)
+ }
+
+ @Test
+ def resolveString(): Unit = {
+ val s = subst("bar.string")
+ val v = resolveWithoutFallbacks(s, simpleObject)
+ assertEquals(stringValue("hello"), v)
+ }
+
+ @Test
+ def resolveDouble(): Unit = {
+ val s = subst("bar.double")
+ val v = resolveWithoutFallbacks(s, simpleObject)
+ assertEquals(doubleValue(3.14), v)
+ }
+
+ @Test
+ def resolveMissingThrows(): Unit = {
+ val e = intercept[ConfigException.UnresolvedSubstitution] {
+ val s = subst("bar.missing")
+ val v = resolveWithoutFallbacks(s, simpleObject)
+ }
+ assertTrue(
+ "wrong exception: " + e.getMessage,
+ !e.getMessage.contains("cycle")
+ )
+ }
+
+ @Test
+ def resolveIntInString(): Unit = {
+ val s = substInString("bar.int")
+ val v = resolveWithoutFallbacks(s, simpleObject)
+ assertEquals(stringValue("start<43>end"), v)
+ }
+
+ @Test
+ def resolveNullInString(): Unit = {
+ val s = substInString("bar.null")
+ val v = resolveWithoutFallbacks(s, simpleObject)
+ assertEquals(stringValue("startend"), v)
+
+ // when null is NOT a subst, it should also not become empty
+ val o = parseConfig("""{ "a" : null foo bar }""")
+ assertEquals("null foo bar", o.getString("a"))
+ }
+
+ @Test
+ def resolveMissingInString(): Unit = {
+ val s = substInString("bar.missing", true /* optional */ )
+ val v = resolveWithoutFallbacks(s, simpleObject)
+ // absent object becomes empty string
+ assertEquals(stringValue("start<>end"), v)
+
+ intercept[ConfigException.UnresolvedSubstitution] {
+ val s2 = substInString("bar.missing", false /* optional */ )
+ resolveWithoutFallbacks(s2, simpleObject)
+ }
+ }
+
+ @Test
+ def resolveBoolInString(): Unit = {
+ val s = substInString("bar.bool")
+ val v = resolveWithoutFallbacks(s, simpleObject)
+ assertEquals(stringValue("startend"), v)
+ }
+
+ @Test
+ def resolveStringInString(): Unit = {
+ val s = substInString("bar.string")
+ val v = resolveWithoutFallbacks(s, simpleObject)
+ assertEquals(stringValue("startend"), v)
+ }
+
+ @Test
+ def resolveDoubleInString(): Unit = {
+ val s = substInString("bar.double")
+ val v = resolveWithoutFallbacks(s, simpleObject)
+ assertEquals(stringValue("start<3.14>end"), v)
+ }
+
+ @Test
+ def missingInArray(): Unit = {
+ val obj = parseObject("""
+ a : [ ${?missing}, ${?also.missing} ]
+""")
+
+ val resolved = resolve(obj)
+
+ assertEquals(Seq(), resolved.getList("a").asScala)
+ }
+
+ @Test
+ def missingInObject(): Unit = {
+ val obj = parseObject(
+ """
+ a : ${?missing}, b : ${?also.missing}, c : ${?b}, d : ${?c}
+"""
+ )
+
+ val resolved = resolve(obj)
+
+ assertTrue(resolved.isEmpty)
+ }
+
+ private val substChainObject = {
+ parseObject("""
+{
+ "foo" : ${bar},
+ "bar" : ${a.b.c},
+ "a" : { "b" : { "c" : 57 } }
+}
+""")
+ }
+
+ @Test
+ def chainSubstitutions(): Unit = {
+ val s = subst("foo")
+ val v = resolveWithoutFallbacks(s, substChainObject)
+ assertEquals(intValue(57), v)
+ }
+
+ @Test
+ def substitutionsLookForward(): Unit = {
+ val obj = parseObject("""a=1,b=${a},a=2""")
+ val resolved = resolve(obj)
+ assertEquals(2, resolved.getInt("b"))
+ }
+
+ @Test
+ def throwOnIncrediblyTrivialCycle(): Unit = {
+ val s = subst("a")
+ val e = intercept[ConfigException.UnresolvedSubstitution] {
+ val v = resolveWithoutFallbacks(s, parseObject("a: ${a}"))
+ }
+ assertTrue(
+ "Wrong exception: " + e.getMessage,
+ e.getMessage().contains("cycle")
+ )
+ assertTrue(
+ "Wrong exception: " + e.getMessage,
+ e.getMessage().contains("${a}")
+ )
+ }
+
+ private val substCycleObject = {
+ parseObject("""
+{
+ "foo" : ${bar},
+ "bar" : ${a.b.c},
+ "a" : { "b" : { "c" : ${foo} } }
+}
+""")
+ }
+
+ @Test
+ def throwOnCycles(): Unit = {
+ val s = subst("foo")
+ val e = intercept[ConfigException.UnresolvedSubstitution] {
+ val v = resolveWithoutFallbacks(s, substCycleObject)
+ }
+ assertTrue(
+ "Wrong exception: " + e.getMessage,
+ e.getMessage().contains("cycle")
+ )
+ assertTrue(
+ "Wrong exception: " + e.getMessage,
+ e.getMessage().contains("${foo}, ${bar}, ${a.b.c}, ${foo}")
+ )
+ }
+
+ @Test
+ def throwOnOptionalReferenceToNonOptionalCycle(): Unit = {
+ // we look up ${?foo}, but the cycle has hard
+ // non-optional links in it so still has to throw.
+ val s = subst("foo", optional = true)
+ val e = intercept[ConfigException.UnresolvedSubstitution] {
+ val v = resolveWithoutFallbacks(s, substCycleObject)
+ }
+ assertTrue(
+ "Wrong exception: " + e.getMessage,
+ e.getMessage().contains("cycle")
+ )
+ }
+
+ // ALL the links have to be optional here for the cycle to be ignored
+ private val substCycleObjectOptionalLink = {
+ parseObject("""
+{
+ "foo" : ${?bar},
+ "bar" : ${?a.b.c},
+ "a" : { "b" : { "c" : ${?foo} } }
+}
+""")
+ }
+
+ @Test
+ def optionalLinkCyclesActLikeUndefined(): Unit = {
+ val s = subst("foo", optional = true)
+ val v = resolveWithoutFallbacks(s, substCycleObjectOptionalLink)
+ assertNull(
+ "Cycle with optional links in it resolves to null if it's a cycle",
+ v
+ )
+ }
+
+ @Test
+ def throwOnTwoKeyCycle(): Unit = {
+ val obj = parseObject("""a:${b},b:${a}""")
+ val e = intercept[ConfigException.UnresolvedSubstitution] {
+ resolve(obj)
+ }
+ assertTrue(
+ "Wrong exception: " + e.getMessage,
+ e.getMessage().contains("cycle")
+ )
+ }
+
+ @Test
+ def throwOnFourKeyCycle(): Unit = {
+ val obj = parseObject("""a:${b},b:${c},c:${d},d:${a}""")
+ val e = intercept[ConfigException.UnresolvedSubstitution] {
+ resolve(obj)
+ }
+ assertTrue(
+ "Wrong exception: " + e.getMessage,
+ e.getMessage().contains("cycle")
+ )
+ }
+
+ @Test
+ def resolveObject(): Unit = {
+ val resolved = resolveWithoutFallbacks(substChainObject)
+ assertEquals(57, resolved.getInt("foo"))
+ assertEquals(57, resolved.getInt("bar"))
+ assertEquals(57, resolved.getInt("a.b.c"))
+ }
+
+ private val substSideEffectCycle = {
+ parseObject("""
+{
+ "foo" : ${a.b.c},
+ "a" : { "b" : { "c" : 42, "cycle" : ${foo} }, "cycle" : ${foo} }
+}
+""")
+ }
+
+ @Test
+ def avoidSideEffectCycles(): Unit = {
+ // The point of this test is that in traversing objects
+ // to resolve a path, we need to avoid resolving
+ // substitutions that are in the traversed objects but
+ // are not directly required to resolve the path.
+ // i.e. there should not be a cycle in this test.
+
+ val resolved = resolveWithoutFallbacks(substSideEffectCycle)
+
+ assertEquals(42, resolved.getInt("foo"))
+ assertEquals(42, resolved.getInt("a.b.cycle"))
+ assertEquals(42, resolved.getInt("a.cycle"))
+ }
+
+ @Test
+ def ignoreHiddenUndefinedSubst(): Unit = {
+ // if a substitution is overridden then it shouldn't matter that it's undefined
+ val obj = parseObject("""a=${nonexistent},a=42""")
+ val resolved = resolve(obj)
+ assertEquals(42, resolved.getInt("a"))
+ }
+
+ @Test
+ def objectDoesNotHideUndefinedSubst(): Unit = {
+ // if a substitution is overridden by an object we still need to
+ // evaluate the substitution
+ val obj = parseObject("""a=${nonexistent},a={ b : 42 }""")
+ val e = intercept[ConfigException.UnresolvedSubstitution] {
+ resolve(obj)
+ }
+ assertTrue(
+ "wrong exception: " + e.getMessage,
+ e.getMessage.contains("Could not resolve")
+ )
+ }
+
+ @Test
+ def ignoreHiddenCircularSubst(): Unit = {
+ // if a substitution is overridden then it shouldn't matter that it's circular
+ val obj = parseObject("""a=${a},a=42""")
+ val resolved = resolve(obj)
+ assertEquals(42, resolved.getInt("a"))
+ }
+
+ private val delayedMergeObjectResolveProblem1 = {
+ parseObject("""
+ defaults {
+ a = 1
+ b = 2
+ }
+ // make item1 into a ConfigDelayedMergeObject
+ item1 = ${defaults}
+ // note that we'll resolve to a non-object value
+ // so item1.b will ignoreFallbacks and not depend on
+ // ${defaults}
+ item1.b = 3
+ // be sure we can resolve a substitution to a value in
+ // a delayed-merge object.
+ item2.b = ${item1.b}
+""")
+ }
+
+ @Test
+ def avoidDelayedMergeObjectResolveProblem1(): Unit = {
+ assertTrue(
+ delayedMergeObjectResolveProblem1
+ .attemptPeekWithPartialResolve("item1")
+ .isInstanceOf[ConfigDelayedMergeObject]
+ )
+
+ val resolved = resolveWithoutFallbacks(delayedMergeObjectResolveProblem1)
+
+ assertEquals(3, resolved.getInt("item1.b"))
+ assertEquals(3, resolved.getInt("item2.b"))
+ }
+
+ private val delayedMergeObjectResolveProblem2 = {
+ parseObject("""
+ defaults {
+ a = 1
+ b = 2
+ }
+ // make item1 into a ConfigDelayedMergeObject
+ item1 = ${defaults}
+ // note that we'll resolve to an object value
+ // so item1.b will depend on also looking up ${defaults}
+ item1.b = { c : 43 }
+ // be sure we can resolve a substitution to a value in
+ // a delayed-merge object.
+ item2.b = ${item1.b}
+""")
+ }
+
+ @Test
+ def avoidDelayedMergeObjectResolveProblem2(): Unit = {
+ assertTrue(
+ delayedMergeObjectResolveProblem2
+ .attemptPeekWithPartialResolve("item1")
+ .isInstanceOf[ConfigDelayedMergeObject]
+ )
+
+ val resolved = resolveWithoutFallbacks(delayedMergeObjectResolveProblem2)
+
+ assertEquals(parseObject("{ c : 43 }"), resolved.getObject("item1.b"))
+ assertEquals(43, resolved.getInt("item1.b.c"))
+ assertEquals(43, resolved.getInt("item2.b.c"))
+ }
+
+ // in this case, item1 is self-referential because
+ // it refers to ${defaults} which refers back to
+ // ${item1}. When self-referencing, only the
+ // value of ${item1} "looking back" should be
+ // visible. This is really a test of the
+ // self-referencing semantics.
+ private val delayedMergeObjectResolveProblem3 = {
+ parseObject("""
+ item1.b.c = 100
+ defaults {
+ // we depend on item1.b.c
+ a = ${item1.b.c}
+ b = 2
+ }
+ // make item1 into a ConfigDelayedMergeObject
+ item1 = ${defaults}
+ // the ${item1.b.c} above in ${defaults} should ignore
+ // this because it only looks back
+ item1.b = { c : 43 }
+ // be sure we can resolve a substitution to a value in
+ // a delayed-merge object.
+ item2.b = ${item1.b}
+""")
+ }
+
+ @Test
+ def avoidDelayedMergeObjectResolveProblem3(): Unit = {
+ assertTrue(
+ delayedMergeObjectResolveProblem3
+ .attemptPeekWithPartialResolve("item1")
+ .isInstanceOf[ConfigDelayedMergeObject]
+ )
+
+ val resolved = resolveWithoutFallbacks(delayedMergeObjectResolveProblem3)
+
+ assertEquals(parseObject("{ c : 43 }"), resolved.getObject("item1.b"))
+ assertEquals(43, resolved.getInt("item1.b.c"))
+ assertEquals(43, resolved.getInt("item2.b.c"))
+ assertEquals(100, resolved.getInt("defaults.a"))
+ }
+
+ private val delayedMergeObjectResolveProblem4 = {
+ parseObject("""
+ defaults {
+ a = 1
+ b = 2
+ }
+
+ item1.b = 7
+ // make item1 into a ConfigDelayedMerge
+ item1 = ${defaults}
+ // be sure we can resolve a substitution to a value in
+ // a delayed-merge object.
+ item2.b = ${item1.b}
+""")
+ }
+
+ @Test
+ def avoidDelayedMergeObjectResolveProblem4(): Unit = {
+ // in this case we have a ConfigDelayedMerge not a ConfigDelayedMergeObject
+ assertTrue(
+ delayedMergeObjectResolveProblem4
+ .attemptPeekWithPartialResolve("item1")
+ .isInstanceOf[ConfigDelayedMerge]
+ )
+
+ val resolved = resolveWithoutFallbacks(delayedMergeObjectResolveProblem4)
+
+ assertEquals(2, resolved.getInt("item1.b"))
+ assertEquals(2, resolved.getInt("item2.b"))
+ }
+
+ private val delayedMergeObjectResolveProblem5 = {
+ parseObject("""
+ defaults {
+ a = ${item1.b} // tricky cycle - we won't see ${defaults}
+ // as we resolve this
+ b = 2
+ }
+
+ item1.b = 7
+ // make item1 into a ConfigDelayedMerge
+ item1 = ${defaults}
+ // be sure we can resolve a substitution to a value in
+ // a delayed-merge object.
+ item2.b = ${item1.b}
+""")
+ }
+
+ @Test
+ def avoidDelayedMergeObjectResolveProblem5(): Unit = {
+ // in this case we have a ConfigDelayedMerge not a ConfigDelayedMergeObject
+ assertTrue(
+ delayedMergeObjectResolveProblem5
+ .attemptPeekWithPartialResolve("item1")
+ .isInstanceOf[ConfigDelayedMerge]
+ )
+
+ val resolved = resolveWithoutFallbacks(delayedMergeObjectResolveProblem5)
+
+ assertEquals("item1.b", 2, resolved.getInt("item1.b"))
+ assertEquals("item2.b", 2, resolved.getInt("item2.b"))
+ assertEquals("defaults.a", 7, resolved.getInt("defaults.a"))
+ }
+
+ private val delayedMergeObjectResolveProblem6 = {
+ parseObject(
+ """
+ z = 15
+ defaults-defaults-defaults {
+ m = ${z}
+ n.o.p = ${z}
+ }
+ defaults-defaults {
+ x = 10
+ y = 11
+ asdf = ${z}
+ }
+ defaults {
+ a = 1
+ b = 2
+ }
+ defaults-alias = ${defaults}
+ // make item1 into a ConfigDelayedMergeObject several layers deep
+ // that will NOT become resolved just because we resolve one path
+ // through it.
+ item1 = 345
+ item1 = ${?NONEXISTENT}
+ item1 = ${defaults-defaults-defaults}
+ item1 = {}
+ item1 = ${defaults-defaults}
+ item1 = ${defaults-alias}
+ item1 = ${defaults}
+ item1.b = { c : 43 }
+ item1.xyz = 101
+ // be sure we can resolve a substitution to a value in
+ // a delayed-merge object.
+ item2.b = ${item1.b}
+"""
+ )
+ }
+
+ @Test
+ def avoidDelayedMergeObjectResolveProblem6(): Unit = {
+ assertTrue(
+ delayedMergeObjectResolveProblem6
+ .attemptPeekWithPartialResolve("item1")
+ .isInstanceOf[ConfigDelayedMergeObject]
+ )
+
+ // should be able to attemptPeekWithPartialResolve() a known non-object without resolving
+ assertEquals(
+ 101,
+ delayedMergeObjectResolveProblem6.toConfig
+ .getObject("item1")
+ .attemptPeekWithPartialResolve("xyz")
+ .unwrapped
+ )
+
+ val resolved = resolveWithoutFallbacks(delayedMergeObjectResolveProblem6)
+
+ assertEquals(parseObject("{ c : 43 }"), resolved.getObject("item1.b"))
+ assertEquals(43, resolved.getInt("item1.b.c"))
+ assertEquals(43, resolved.getInt("item2.b.c"))
+ assertEquals(15, resolved.getInt("item1.n.o.p"))
+ }
+
+ private val delayedMergeObjectWithKnownValue = {
+ parseObject("""
+ defaults {
+ a = 1
+ b = 2
+ }
+ // make item1 into a ConfigDelayedMergeObject
+ item1 = ${defaults}
+ // note that we'll resolve to a non-object value
+ // so item1.b will ignoreFallbacks and not depend on
+ // ${defaults}
+ item1.b = 3
+""")
+ }
+
+ @Test
+ def fetchKnownValueFromDelayedMergeObject(): Unit = {
+ assertTrue(
+ delayedMergeObjectWithKnownValue
+ .attemptPeekWithPartialResolve("item1")
+ .isInstanceOf[ConfigDelayedMergeObject]
+ )
+
+ assertEquals(
+ 3,
+ delayedMergeObjectWithKnownValue.toConfig.getConfig("item1").getInt("b")
+ )
+ }
+
+ private val delayedMergeObjectNeedsFullResolve = {
+ parseObject(
+ """
+ defaults {
+ a = 1
+ b = { c : 31 }
+ }
+ item1 = ${defaults}
+ // because b is an object, fetching it requires resolving ${defaults} above
+ // to see if there are more keys to merge with b.
+ item1.b = { c : 41 }
+"""
+ )
+ }
+
+ @Test
+ def failToFetchFromDelayedMergeObjectNeedsFullResolve(): Unit = {
+ assertTrue(
+ delayedMergeObjectWithKnownValue
+ .attemptPeekWithPartialResolve("item1")
+ .isInstanceOf[ConfigDelayedMergeObject]
+ )
+
+ val e = intercept[ConfigException.NotResolved] {
+ delayedMergeObjectNeedsFullResolve.toConfig.getObject("item1.b")
+ }
+
+ assertTrue(
+ "wrong exception: " + e.getMessage,
+ e.getMessage.contains("item1.b")
+ )
+ }
+
+ // objects that mutually refer to each other
+ private val delayedMergeObjectEmbrace = {
+ parseObject(
+ """
+ defaults {
+ a = 1
+ b = 2
+ }
+
+ item1 = ${defaults}
+ // item1.c refers to a field in item2 that refers to item1
+ item1.c = ${item2.d}
+ // item1.x refers to a field in item2 that doesn't go back to item1
+ item1.x = ${item2.y}
+
+ item2 = ${defaults}
+ // item2.d refers to a field in item1
+ item2.d = ${item1.a}
+ item2.y = 15
+"""
+ )
+ }
+
+ @Test
+ def resolveDelayedMergeObjectEmbrace(): Unit = {
+ assertTrue(
+ delayedMergeObjectEmbrace
+ .attemptPeekWithPartialResolve("item1")
+ .isInstanceOf[ConfigDelayedMergeObject]
+ )
+ assertTrue(
+ delayedMergeObjectEmbrace
+ .attemptPeekWithPartialResolve("item2")
+ .isInstanceOf[ConfigDelayedMergeObject]
+ )
+
+ val resolved = delayedMergeObjectEmbrace.toConfig.resolve()
+ assertEquals(1, resolved.getInt("item1.c"))
+ assertEquals(1, resolved.getInt("item2.d"))
+ assertEquals(15, resolved.getInt("item1.x"))
+ }
+
+ // objects that mutually refer to each other
+ private val plainObjectEmbrace = {
+ parseObject("""
+ item1.a = 10
+ item1.b = ${item2.d}
+ item2.c = 12
+ item2.d = 14
+ item2.e = ${item1.a}
+ item2.f = ${item1.b} // item1.b goes back to item2
+ item2.g = ${item2.f} // goes back to ourselves
+""")
+ }
+
+ @Test
+ def resolvePlainObjectEmbrace(): Unit = {
+ assertTrue(
+ plainObjectEmbrace
+ .attemptPeekWithPartialResolve("item1")
+ .isInstanceOf[SimpleConfigObject]
+ )
+ assertTrue(
+ plainObjectEmbrace
+ .attemptPeekWithPartialResolve("item2")
+ .isInstanceOf[SimpleConfigObject]
+ )
+
+ val resolved = plainObjectEmbrace.toConfig.resolve()
+ assertEquals(14, resolved.getInt("item1.b"))
+ assertEquals(10, resolved.getInt("item2.e"))
+ assertEquals(14, resolved.getInt("item2.f"))
+ assertEquals(14, resolved.getInt("item2.g"))
+ }
+
+ @Test
+ def useRelativeToSameFileWhenRelativized(): Unit = {
+ val child = parseObject("""foo=in child,bar=${foo}""")
+
+ val values = new java.util.HashMap[String, AbstractConfigValue]()
+
+ values.put("a", child.relativized(new Path("a")))
+ // this "foo" should NOT be used.
+ values.put("foo", stringValue("in parent"))
+
+ val resolved = resolve(new SimpleConfigObject(fakeOrigin(), values))
+
+ assertEquals("in child", resolved.getString("a.bar"))
+ }
+
+ @Test
+ def useRelativeToRootWhenRelativized(): Unit = {
+ // here, "foo" is not defined in the child
+ val child = parseObject("""bar=${foo}""")
+
+ val values = new java.util.HashMap[String, AbstractConfigValue]()
+
+ values.put("a", child.relativized(new Path("a")))
+ // so this "foo" SHOULD be used
+ values.put("foo", stringValue("in parent"))
+
+ val resolved = resolve(new SimpleConfigObject(fakeOrigin(), values))
+
+ assertEquals("in parent", resolved.getString("a.bar"))
+ }
+
+ private val substComplexObject = {
+ parseObject(
+ """
+{
+ "foo" : ${bar},
+ "bar" : ${a.b.c},
+ "a" : { "b" : { "c" : 57, "d" : ${foo}, "e" : { "f" : ${foo} } } },
+ "objA" : ${a},
+ "objB" : ${a.b},
+ "objE" : ${a.b.e},
+ "foo.bar" : 37,
+ "arr" : [ ${foo}, ${a.b.c}, ${"foo.bar"}, ${objB.d}, ${objA.b.e.f}, ${objE.f} ],
+ "ptrToArr" : ${arr},
+ "x" : { "y" : { "ptrToPtrToArr" : ${ptrToArr} } }
+}
+"""
+ )
+ }
+
+ @Test
+ def complexResolve(): Unit = {
+ val resolved = resolveWithoutFallbacks(substComplexObject)
+
+ assertEquals(57, resolved.getInt("foo"))
+ assertEquals(57, resolved.getInt("bar"))
+ assertEquals(57, resolved.getInt("a.b.c"))
+ assertEquals(57, resolved.getInt("a.b.d"))
+ assertEquals(57, resolved.getInt("objB.d"))
+ assertEquals(
+ Seq(57, 57, 37, 57, 57, 57),
+ resolved.getIntList("arr").asScala
+ )
+ assertEquals(
+ Seq(57, 57, 37, 57, 57, 57),
+ resolved.getIntList("ptrToArr").asScala
+ )
+ assertEquals(
+ Seq(57, 57, 37, 57, 57, 57),
+ resolved.getIntList("x.y.ptrToPtrToArr").asScala
+ )
+ }
+
+ private val substSystemPropsObject =
+ parseObject("""
+ {
+ "a" : ${configtest.a},
+ "b" : ${configtest.b}
+ }
+ """)
+
+ // @Test
+ // def resolveListFromSystemProps(): Unit = {
+ // val props = parseObject("""
+ // |"a": ${testList}
+ // """.stripMargin)
+
+ // System.setProperty("testList.0", "0")
+ // System.setProperty("testList.1", "1")
+ // ConfigImpl.reloadSystemPropertiesConfig()
+
+ // val resolved = resolve(
+ // ConfigFactory
+ // .systemProperties()
+ // .withFallback(props)
+ // .root
+ // .asInstanceOf[AbstractConfigObject]
+ // )
+
+ // assertEquals(List("0", "1"), resolved.getList("a").unwrapped.asScala)
+ // }
+
+ // @Test
+ // def resolveListFromEnvVars(): Unit = {
+ // val props = parseObject("""
+ // |"a": ${testList}
+ // """.stripMargin)
+
+ // // "testList.0" and "testList.1" are defined as envVars in build.sbt
+ // val resolved = resolve(props)
+
+ // assertEquals(List("0", "1"), resolved.getList("a").unwrapped.asScala)
+ // }
+
+ // this is a weird test, it used to test fallback to system props which made more sense.
+ // Now it just tests that if you override with system props, you can use system props
+ // in substitutions.
+ // @Test
+ // def overrideWithSystemProps(): Unit = {
+ // System.setProperty("configtest.a", "1234")
+ // System.setProperty("configtest.b", "5678")
+ // ConfigImpl.reloadSystemPropertiesConfig()
+
+ // val resolved = resolve(
+ // ConfigFactory
+ // .systemProperties()
+ // .withFallback(substSystemPropsObject)
+ // .root
+ // .asInstanceOf[AbstractConfigObject]
+ // )
+
+ // assertEquals("1234", resolved.getString("a"))
+ // assertEquals("5678", resolved.getString("b"))
+ // }
+
+ private val substEnvVarObject = {
+ // prefix the names of keys with "key_" to allow us to embed a case sensitive env var name
+ // in the key that wont therefore risk a naming collision with env vars themselves
+ parseObject(
+ """
+{
+ "key_HOME" : ${?HOME},
+ "key_PWD" : ${?PWD},
+ "key_SHELL" : ${?SHELL},
+ "key_LANG" : ${?LANG},
+ "key_PATH" : ${?PATH},
+ "key_Path" : ${?Path}, // many windows machines use Path rather than PATH
+ "key_NOT_HERE" : ${?NOT_HERE}
+}
+"""
+ )
+ }
+
+ // @Test
+ // def fallbackToEnv(): Unit = {
+ // val resolved = resolve(substEnvVarObject)
+
+ // var existed = 0
+ // for (k <- resolved.root.keySet().asScala) {
+ // val envVarName = k.replace("key_", "")
+ // val e = System.getenv(envVarName)
+ // if (e != null) {
+ // existed += 1
+ // assertEquals(e, resolved.getString(k))
+ // } else {
+ // assertNull(resolved.root.get(k))
+ // }
+ // }
+ // if (existed == 0) {
+ // throw new Exception(
+ // "None of the env vars we tried to use for testing were set"
+ // )
+ // }
+ // }
+
+ @Test
+ def noFallbackToEnvIfValuesAreNull(): Unit = {
+ // create a fallback object with all the env var names
+ // set to null. we want to be sure this blocks
+ // lookup in the environment. i.e. if there is a
+ // { HOME : null } then ${HOME} should be null.
+ val nullsMap = new java.util.HashMap[String, Object]
+ for (k <- substEnvVarObject.keySet().asScala) {
+ val envVarName = k.replace("key_", "")
+ nullsMap.put(envVarName, null)
+ }
+ val nulls = ConfigFactory.parseMap(nullsMap, "nulls map")
+
+ val resolved = resolve(substEnvVarObject.withFallback(nulls))
+
+ for (k <- resolved.root.keySet().asScala) {
+ assertNotNull(resolved.root.get(k))
+ assertEquals(nullValue(), resolved.root.get(k))
+ }
+ }
+
+ // @Test
+ // def fallbackToEnvWhenRelativized(): Unit = {
+ // val values = new java.util.HashMap[String, AbstractConfigValue]()
+
+ // values.put("a", substEnvVarObject.relativized(new Path("a")))
+
+ // val resolved = resolve(new SimpleConfigObject(fakeOrigin(), values))
+
+ // var existed = 0
+ // for (k <- resolved.getObject("a").keySet().asScala) {
+ // val envVarName = k.replace("key_", "")
+ // val e = System.getenv(envVarName)
+ // if (e != null) {
+ // existed += 1
+ // assertEquals(e, resolved.getConfig("a").getString(k))
+ // } else {
+ // assertNull(resolved.getObject("a").get(k))
+ // }
+ // }
+ // if (existed == 0) {
+ // throw new Exception(
+ // "None of the env vars we tried to use for testing were set"
+ // )
+ // }
+ // }
+
+ @Test
+ def throwWhenEnvNotFound(): Unit = {
+ val obj = parseObject("""{ a : ${NOT_HERE} }""")
+ intercept[ConfigException.UnresolvedSubstitution] {
+ resolve(obj)
+ }
+ }
+
+ @Test
+ def optionalOverrideNotProvided(): Unit = {
+ val obj = parseObject("""{ a: 42, a : ${?NOT_HERE} }""")
+ val resolved = resolve(obj)
+ assertEquals(42, resolved.getInt("a"))
+ }
+
+ @Test
+ def optionalOverrideProvided(): Unit = {
+ val obj = parseObject("""{ HERE : 43, a: 42, a : ${?HERE} }""")
+ val resolved = resolve(obj)
+ assertEquals(43, resolved.getInt("a"))
+ }
+
+ @Test
+ def optionalOverrideOfObjectNotProvided(): Unit = {
+ val obj = parseObject("""{ a: { b : 42 }, a : ${?NOT_HERE} }""")
+ val resolved = resolve(obj)
+ assertEquals(42, resolved.getInt("a.b"))
+ }
+
+ @Test
+ def optionalOverrideOfObjectProvided(): Unit = {
+ val obj = parseObject("""{ HERE : 43, a: { b : 42 }, a : ${?HERE} }""")
+ val resolved = resolve(obj)
+ assertEquals(43, resolved.getInt("a"))
+ assertFalse(resolved.hasPath("a.b"))
+ }
+
+ @Test
+ def optionalVanishesFromArray(): Unit = {
+ val obj = parseObject("""{ a : [ 1, 2, 3, ${?NOT_HERE} ] }""")
+ val resolved = resolve(obj)
+ assertEquals(Seq(1, 2, 3), resolved.getIntList("a").asScala)
+ }
+
+ @Test
+ def optionalUsedInArray(): Unit = {
+ val obj = parseObject("""{ HERE: 4, a : [ 1, 2, 3, ${?HERE} ] }""")
+ val resolved = resolve(obj)
+ assertEquals(Seq(1, 2, 3, 4), resolved.getIntList("a").asScala)
+ }
+
+ @Test
+ def substSelfReference(): Unit = {
+ val obj = parseObject("""a=1, a=${a}""")
+ val resolved = resolve(obj)
+ assertEquals(1, resolved.getInt("a"))
+ }
+
+ @Test
+ def substSelfReferenceUndefined(): Unit = {
+ val obj = parseObject("""a=${a}""")
+ val e = intercept[ConfigException.UnresolvedSubstitution] {
+ resolve(obj)
+ }
+ assertTrue(
+ "wrong exception: " + e.getMessage,
+ e.getMessage.contains("cycle")
+ )
+ }
+
+ @Test
+ def substSelfReferenceOptional(): Unit = {
+ val obj = parseObject("""a=${?a}""")
+ val resolved = resolve(obj)
+ assertEquals("optional self reference disappears", 0, resolved.root.size)
+ }
+
+ @Test
+ def substSelfReferenceAlongPath(): Unit = {
+ val obj = parseObject("""a.b=1, a.b=${a.b}""")
+ val resolved = resolve(obj)
+ assertEquals(1, resolved.getInt("a.b"))
+ }
+
+ @Test
+ def substSelfReferenceAlongLongerPath(): Unit = {
+ val obj = parseObject("""a.b.c=1, a.b.c=${a.b.c}""")
+ val resolved = resolve(obj)
+ assertEquals(1, resolved.getInt("a.b.c"))
+ }
+
+ @Test
+ def substSelfReferenceAlongPathMoreComplex(): Unit = {
+ // this is an example from the spec
+ val obj = parseObject("""
+ foo : { a : { c : 1 } }
+ foo : ${foo.a}
+ foo : { a : 2 }
+ """)
+ val resolved = resolve(obj)
+ assertEquals(1, resolved.getInt("foo.c"))
+ assertEquals(2, resolved.getInt("foo.a"))
+ }
+
+ @Test
+ def substSelfReferenceIndirect(): Unit = {
+ // this has two possible outcomes depending on whether
+ // we resolve and memoize a first or b first. currently
+ // java 8's hash table makes it resolve OK, but
+ // it's also allowed to throw an exception.
+ val obj = parseObject("""a=1, b=${a}, a=${b}""")
+ val resolved = resolve(obj)
+ assertEquals(1, resolved.getInt("a"))
+ }
+
+ @Test
+ def substSelfReferenceDoubleIndirect(): Unit = {
+ // this has two possible outcomes depending on whether we
+ // resolve and memoize a, b, or c first. currently java
+ // 8's hash table makes it resolve OK, but it's also
+ // allowed to throw an exception.
+ val obj = parseObject("""a=1, b=${c}, c=${a}, a=${b}""")
+ val resolved = resolve(obj)
+ assertEquals(1, resolved.getInt("a"))
+ }
+
+ @Test
+ def substSelfReferenceIndirectStackCycle(): Unit = {
+ // this situation is undefined, depends on
+ // whether we resolve a or b first.
+ val obj = parseObject("""a=1, b={c=5}, b=${a}, a=${b}""")
+ val resolved = resolve(obj)
+ val option1 = parseObject(""" b={c=5}, a={c=5} """).toConfig
+ val option2 = parseObject(""" b=1, a=1 """).toConfig
+ assertTrue(
+ "not an expected possibility: " + resolved +
+ " expected 1: " + option1 + " or 2: " + option2,
+ resolved == option1 || resolved == option2
+ )
+ }
+
+ @Test
+ def substSelfReferenceObject(): Unit = {
+ val obj = parseObject("""a={b=5}, a=${a}""")
+ val resolved = resolve(obj)
+ assertEquals(5, resolved.getInt("a.b"))
+ }
+
+ @Test
+ def substSelfReferenceObjectAlongPath(): Unit = {
+ val obj = parseObject("""a.b={c=5}, a.b=${a.b}""")
+ val resolved = resolve(obj)
+ assertEquals(5, resolved.getInt("a.b.c"))
+ }
+
+ @Test
+ def substSelfReferenceInConcat(): Unit = {
+ val obj = parseObject("""a=1, a=${a}foo""")
+ val resolved = resolve(obj)
+ assertEquals("1foo", resolved.getString("a"))
+ }
+
+ @Test
+ def substSelfReferenceIndirectInConcat(): Unit = {
+ // this situation is undefined, depends on
+ // whether we resolve a or b first. If b first
+ // then there's an error because ${a} is undefined.
+ // if a first then b=1foo and a=1foo.
+ val obj = parseObject("""a=1, b=${a}foo, a=${b}""")
+ val either =
+ try {
+ Left(resolve(obj))
+ } catch {
+ case e: ConfigException.UnresolvedSubstitution =>
+ Right(e)
+ }
+ val option1 = Left(parseObject("""a:1foo,b:1foo""").toConfig)
+ assertTrue(
+ "not an expected possibility: " + either +
+ " expected value " + option1 + " or an exception",
+ either == option1 || either.isRight
+ )
+ }
+
+ @Test
+ def substOptionalSelfReferenceInConcat(): Unit = {
+ val obj = parseObject("""a=${?a}foo""")
+ val resolved = resolve(obj)
+ assertEquals("foo", resolved.getString("a"))
+ }
+
+ @Test
+ def substOptionalIndirectSelfReferenceInConcat(): Unit = {
+ val obj = parseObject("""a=${?b}foo,b=${?a}""")
+ val resolved = resolve(obj)
+ assertEquals("foo", resolved.getString("a"))
+ }
+
+ @Test
+ def substTwoOptionalSelfReferencesInConcat(): Unit = {
+ val obj = parseObject("""a=${?a}foo${?a}""")
+ val resolved = resolve(obj)
+ assertEquals("foo", resolved.getString("a"))
+ }
+
+ @Test
+ def substTwoOptionalSelfReferencesInConcatWithPriorValue(): Unit = {
+ val obj = parseObject("""a=1,a=${?a}foo${?a}""")
+ val resolved = resolve(obj)
+ assertEquals("1foo1", resolved.getString("a"))
+ }
+
+ @Test
+ def substSelfReferenceMiddleOfStack(): Unit = {
+ val obj = parseObject("""a=1, a=${a}, a=2""")
+ val resolved = resolve(obj)
+ // the substitution would be 1, but then 2 overrides
+ assertEquals(2, resolved.getInt("a"))
+ }
+
+ @Test
+ def substSelfReferenceObjectMiddleOfStack(): Unit = {
+ val obj = parseObject("""a={b=5}, a=${a}, a={c=6}""")
+ val resolved = resolve(obj)
+ assertEquals(5, resolved.getInt("a.b"))
+ assertEquals(6, resolved.getInt("a.c"))
+ }
+
+ @Test
+ def substOptionalSelfReferenceMiddleOfStack(): Unit = {
+ val obj = parseObject("""a=1, a=${?a}, a=2""")
+ val resolved = resolve(obj)
+ // the substitution would be 1, but then 2 overrides
+ assertEquals(2, resolved.getInt("a"))
+ }
+
+ @Test
+ def substSelfReferenceBottomOfStack(): Unit = {
+ // self-reference should just be ignored since it's
+ // overridden
+ val obj = parseObject("""a=${a}, a=1, a=2""")
+ val resolved = resolve(obj)
+ assertEquals(2, resolved.getInt("a"))
+ }
+
+ @Test
+ def substOptionalSelfReferenceBottomOfStack(): Unit = {
+ val obj = parseObject("""a=${?a}, a=1, a=2""")
+ val resolved = resolve(obj)
+ assertEquals(2, resolved.getInt("a"))
+ }
+
+ @Test
+ def substSelfReferenceTopOfStack(): Unit = {
+ val obj = parseObject("""a=1, a=2, a=${a}""")
+ val resolved = resolve(obj)
+ assertEquals(2, resolved.getInt("a"))
+ }
+
+ @Test
+ def substOptionalSelfReferenceTopOfStack(): Unit = {
+ val obj = parseObject("""a=1, a=2, a=${?a}""")
+ val resolved = resolve(obj)
+ assertEquals(2, resolved.getInt("a"))
+ }
+
+ @Test
+ def substSelfReferenceAlongAPath(): Unit = {
+ // ${a} in the middle of the stack means "${a} in the stack
+ // below us" and so ${a.b} means b inside the "${a} below us"
+ // not b inside the final "${a}"
+ val obj = parseObject("""a={b={c=5}}, a=${a.b}, a={b=2}""")
+ val resolved = resolve(obj)
+ assertEquals(5, resolved.getInt("a.c"))
+ }
+
+ @Test
+ def substSelfReferenceAlongAPathInsideObject(): Unit = {
+ // if the ${a.b} is _inside_ a field value instead of
+ // _being_ the field value, it does not look backward.
+ val obj = parseObject("""a={b={c=5}}, a={ x : ${a.b} }, a={b=2}""")
+ val resolved = resolve(obj)
+ assertEquals(2, resolved.getInt("a.x"))
+ }
+
+ @Test
+ def substInChildFieldNotASelfReference1(): Unit = {
+ // here, ${bar.foo} is not a self reference because
+ // it's the value of a child field of bar, not bar
+ // itself; so we use bar's current value, rather than
+ // looking back in the merge stack
+ val obj = parseObject("""
+ bar : { foo : 42,
+ baz : ${bar.foo}
+ }
+ """)
+ val resolved = resolve(obj)
+ assertEquals(42, resolved.getInt("bar.baz"))
+ assertEquals(42, resolved.getInt("bar.foo"))
+ }
+
+ @Test
+ def substInChildFieldNotASelfReference2(): Unit = {
+ // checking that having bar.foo later in the stack
+ // doesn't break the behavior
+ val obj = parseObject("""
+ bar : { foo : 42,
+ baz : ${bar.foo}
+ }
+ bar : { foo : 43 }
+ """)
+ val resolved = resolve(obj)
+ assertEquals(43, resolved.getInt("bar.baz"))
+ assertEquals(43, resolved.getInt("bar.foo"))
+ }
+
+ @Test
+ def substInChildFieldNotASelfReference3(): Unit = {
+ // checking that having bar.foo earlier in the merge
+ // stack doesn't break the behavior.
+ val obj = parseObject("""
+ bar : { foo : 43 }
+ bar : { foo : 42,
+ baz : ${bar.foo}
+ }
+ """)
+ val resolved = resolve(obj)
+ assertEquals(42, resolved.getInt("bar.baz"))
+ assertEquals(42, resolved.getInt("bar.foo"))
+ }
+
+ @Test
+ def substInChildFieldNotASelfReference4(): Unit = {
+ // checking that having bar set to non-object earlier
+ // doesn't break the behavior.
+ val obj = parseObject("""
+ bar : 101
+ bar : { foo : 42,
+ baz : ${bar.foo}
+ }
+ """)
+ val resolved = resolve(obj)
+ assertEquals(42, resolved.getInt("bar.baz"))
+ assertEquals(42, resolved.getInt("bar.foo"))
+ }
+
+ @Test
+ def substInChildFieldNotASelfReference5(): Unit = {
+ // checking that having bar set to unresolved array earlier
+ // doesn't break the behavior.
+ val obj = parseObject("""
+ x : 0
+ bar : [ ${x}, 1, 2, 3 ]
+ bar : { foo : 42,
+ baz : ${bar.foo}
+ }
+ """)
+ val resolved = resolve(obj)
+ assertEquals(42, resolved.getInt("bar.baz"))
+ assertEquals(42, resolved.getInt("bar.foo"))
+ }
+
+ @Test
+ def mutuallyReferringNotASelfReference(): Unit = {
+ val obj = parseObject("""
+ // bar.a should end up as 4
+ bar : { a : ${foo.d}, b : 1 }
+ bar.b = 3
+ // foo.c should end up as 3
+ foo : { c : ${bar.b}, d : 2 }
+ foo.d = 4
+ """)
+ val resolved = resolve(obj)
+ assertEquals(4, resolved.getInt("bar.a"))
+ assertEquals(3, resolved.getInt("foo.c"))
+ }
+
+ @Test
+ def substSelfReferenceMultipleTimes(): Unit = {
+ val obj = parseObject("""a=1,a=${a},a=${a},a=${a}""")
+ val resolved = resolve(obj)
+ assertEquals(1, resolved.getInt("a"))
+ }
+
+ @Test
+ def substSelfReferenceInConcatMultipleTimes(): Unit = {
+ val obj = parseObject("""a=1,a=${a}x,a=${a}y,a=${a}z""")
+ val resolved = resolve(obj)
+ assertEquals("1xyz", resolved.getString("a"))
+ }
+
+ @Test
+ def substSelfReferenceInArray(): Unit = {
+ // never "look back" from "inside" an array
+ val obj = parseObject("""a=1,a=[${a}, 2]""")
+ val e = intercept[ConfigException.UnresolvedSubstitution] {
+ resolve(obj)
+ }
+ assertTrue(
+ "wrong exception: " + e.getMessage,
+ e.getMessage.contains("cycle") && e.getMessage.contains("${a}")
+ )
+ }
+
+ @Test
+ def substSelfReferenceInObject(): Unit = {
+ // never "look back" from "inside" an object
+ val obj = parseObject("""a=1,a={ x : ${a} }""")
+ val e = intercept[ConfigException.UnresolvedSubstitution] {
+ resolve(obj)
+ }
+ assertTrue(
+ "wrong exception: " + e.getMessage,
+ e.getMessage.contains("cycle") && e.getMessage.contains("${a}")
+ )
+ }
+
+ @Test
+ def selfReferentialObjectNotAffectedByOverriding(): Unit = {
+ // this is testing that we can still refer to another
+ // field in the same object, even though we are overriding
+ // an earlier object.
+ val obj = parseObject("""a={ x : 42, y : ${a.x} }""")
+ val resolved = resolve(obj)
+ assertEquals(
+ parseObject("{ x : 42, y : 42 }"),
+ resolved.getConfig("a").root
+ )
+
+ // this is expected because if adding "a=1" here affects the outcome,
+ // it would be flat-out bizarre.
+ val obj2 = parseObject("""a=1, a={ x : 42, y : ${a.x} }""")
+ val resolved2 = resolve(obj2)
+ assertEquals(
+ parseObject("{ x : 42, y : 42 }"),
+ resolved2.getConfig("a").root
+ )
+ }
+}
diff --git a/sconfig/shared/src/test/scala/org/ekrich/config/impl/ConfigTest.scala b/sconfig/shared/src/test/scala/org/ekrich/config/impl/ConfigTest.scala
new file mode 100644
index 00000000..08467278
--- /dev/null
+++ b/sconfig/shared/src/test/scala/org/ekrich/config/impl/ConfigTest.scala
@@ -0,0 +1,779 @@
+/**
+ * Copyright (C) 2011 Typesafe Inc.
+ */
+package org.ekrich.config.impl
+
+import scala.jdk.CollectionConverters._
+
+import org.junit.Assert._
+import org.junit._
+
+import org.ekrich.config._
+import org.ekrich.config.ConfigResolveOptions
+
+class ConfigTest extends TestUtils {
+ private def resolveNoSystem(
+ v: AbstractConfigValue,
+ root: AbstractConfigObject
+ ) = {
+ ResolveContext.resolve(v, root, ConfigResolveOptions.noSystem)
+ }
+
+ private def resolveNoSystem(v: SimpleConfig, root: SimpleConfig) = {
+ ResolveContext
+ .resolve(v.root, root.root, ConfigResolveOptions.noSystem)
+ .asInstanceOf[AbstractConfigObject]
+ .toConfig
+ }
+
+ def mergeUnresolved(toMerge: AbstractConfigObject*) = {
+ if (toMerge.isEmpty) {
+ SimpleConfigObject.empty()
+ } else {
+ toMerge.reduce((first, second) => first.withFallback(second))
+ }
+ }
+
+ def merge(toMerge: AbstractConfigObject*) = {
+ val obj = mergeUnresolved(toMerge: _*)
+ resolveNoSystem(obj, obj) match {
+ case x: AbstractConfigObject => x
+ }
+ }
+
+ // Merging should always be associative (same results however the values are grouped,
+ // as long as they remain in the same order)
+ private def associativeMerge(
+ allObjects: Seq[AbstractConfigObject]
+ )(assertions: SimpleConfig => Unit): Unit = {
+ def makeTrees(
+ objects: Seq[AbstractConfigObject]
+ ): Iterator[AbstractConfigObject] = {
+ objects.length match {
+ case 0 => Iterator.empty
+ case 1 => {
+ Iterator(objects(0))
+ }
+ case 2 => {
+ Iterator(objects(0).withFallback(objects(1)))
+ }
+ case n => {
+ val leftSplits = for {
+ i <- (1 until n)
+ pair = objects.splitAt(i)
+ first = pair._1.reduceLeft(_.withFallback(_))
+ second = pair._2.reduceLeft(_.withFallback(_))
+ } yield first.withFallback(second)
+ val rightSplits = for {
+ i <- (1 until n)
+ pair = objects.splitAt(i)
+ first = pair._1.reduceRight(_.withFallback(_))
+ second = pair._2.reduceRight(_.withFallback(_))
+ } yield first.withFallback(second)
+ leftSplits.iterator ++ rightSplits.iterator
+ }
+ }
+ }
+
+ val trees = makeTrees(allObjects).toSeq
+ for (tree <- trees) {
+ // if this fails, we were not associative.
+ 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. two results were: \none: " + trees(0) + "\ntwo: " +
+ tree + "\noriginal list: " + allObjects
+ )
+ }
+
+ for (tree <- trees) {
+ assertions(tree.toConfig)
+ }
+ }
+
+ @Test
+ def mergeTrivial(): Unit = {
+ val obj1 = parseObject("""{ "a" : 1 }""")
+ val obj2 = parseObject("""{ "b" : 2 }""")
+ val merged = merge(obj1, obj2).toConfig
+
+ assertEquals(1, merged.getInt("a"))
+ assertEquals(2, merged.getInt("b"))
+ assertEquals(2, merged.root.size)
+ }
+
+ @Test
+ def mergeEmpty(): Unit = {
+ val merged = merge().toConfig
+
+ assertEquals(0, merged.root.size)
+ }
+
+ @Test
+ def mergeOne(): Unit = {
+ val obj1 = parseObject("""{ "a" : 1 }""")
+ val merged = merge(obj1).toConfig
+
+ assertEquals(1, merged.getInt("a"))
+ assertEquals(1, merged.root.size)
+ }
+
+ @Test
+ def mergeOverride(): Unit = {
+ val obj1 = parseObject("""{ "a" : 1 }""")
+ val obj2 = parseObject("""{ "a" : 2 }""")
+ val merged = merge(obj1, obj2).toConfig
+
+ assertEquals(1, merged.getInt("a"))
+ assertEquals(1, merged.root.size)
+
+ val merged2 = merge(obj2, obj1).toConfig
+
+ assertEquals(2, merged2.getInt("a"))
+ assertEquals(1, merged2.root.size)
+ }
+
+ @Test
+ def mergeN(): Unit = {
+ val obj1 = parseObject("""{ "a" : 1 }""")
+ val obj2 = parseObject("""{ "b" : 2 }""")
+ val obj3 = parseObject("""{ "c" : 3 }""")
+ val obj4 = parseObject("""{ "d" : 4 }""")
+
+ associativeMerge(Seq(obj1, obj2, obj3, obj4)) { merged =>
+ assertEquals(1, merged.getInt("a"))
+ assertEquals(2, merged.getInt("b"))
+ assertEquals(3, merged.getInt("c"))
+ assertEquals(4, merged.getInt("d"))
+ assertEquals(4, merged.root.size)
+ }
+ }
+
+ @Test
+ def mergeOverrideN(): Unit = {
+ val obj1 = parseObject("""{ "a" : 1 }""")
+ val obj2 = parseObject("""{ "a" : 2 }""")
+ val obj3 = parseObject("""{ "a" : 3 }""")
+ val obj4 = parseObject("""{ "a" : 4 }""")
+ associativeMerge(Seq(obj1, obj2, obj3, obj4)) { merged =>
+ assertEquals(1, merged.getInt("a"))
+ assertEquals(1, merged.root.size)
+ }
+
+ associativeMerge(Seq(obj4, obj3, obj2, obj1)) { merged2 =>
+ assertEquals(4, merged2.getInt("a"))
+ assertEquals(1, merged2.root.size)
+ }
+ }
+
+ @Test
+ def mergeNested(): Unit = {
+ val obj1 = parseObject("""{ "root" : { "a" : 1, "z" : 101 } }""")
+ val obj2 = parseObject("""{ "root" : { "b" : 2, "z" : 102 } }""")
+ val merged = merge(obj1, obj2).toConfig
+
+ assertEquals(1, merged.getInt("root.a"))
+ assertEquals(2, merged.getInt("root.b"))
+ assertEquals(101, merged.getInt("root.z"))
+ assertEquals(1, merged.root.size)
+ assertEquals(3, merged.getConfig("root").root.size)
+ }
+
+ @Test
+ def mergeWithEmpty(): Unit = {
+ val obj1 = parseObject("""{ "a" : 1 }""")
+ val obj2 = parseObject("""{ }""")
+ val merged = merge(obj1, obj2).toConfig
+
+ assertEquals(1, merged.getInt("a"))
+ assertEquals(1, merged.root.size)
+
+ val merged2 = merge(obj2, obj1).toConfig
+
+ assertEquals(1, merged2.getInt("a"))
+ assertEquals(1, merged2.root.size)
+ }
+
+ @Test
+ def mergeOverrideObjectAndPrimitive(): Unit = {
+ val obj1 = parseObject("""{ "a" : 1 }""")
+ val obj2 = parseObject("""{ "a" : { "b" : 42 } }""")
+ val merged = merge(obj1, obj2).toConfig
+
+ assertEquals(1, merged.getInt("a"))
+ assertEquals(1, merged.root.size)
+
+ val merged2 = merge(obj2, obj1).toConfig
+
+ assertEquals(42, merged2.getConfig("a").getInt("b"))
+ assertEquals(42, merged2.getInt("a.b"))
+ assertEquals(1, merged2.root.size)
+ assertEquals(1, merged2.getObject("a").size)
+ }
+
+ @Test
+ def mergeOverrideObjectAndSubstitution(): Unit = {
+ val obj1 = parseObject("""{ "a" : 1 }""")
+ val obj2 = parseObject("""{ "a" : { "b" : ${c} }, "c" : 42 }""")
+ val merged = merge(obj1, obj2).toConfig
+
+ assertEquals(1, merged.getInt("a"))
+ assertEquals(2, merged.root.size)
+
+ val merged2 = merge(obj2, obj1).toConfig
+
+ assertEquals(42, merged2.getConfig("a").getInt("b"))
+ assertEquals(42, merged2.getInt("a.b"))
+ assertEquals(2, merged2.root.size)
+ assertEquals(1, merged2.getObject("a").size)
+ }
+
+ @Test
+ def mergeObjectThenPrimitiveThenObject(): Unit = {
+ // the semantic here is that the primitive blocks the
+ // object that occurs at lower priority. This is consistent
+ // with duplicate keys in the same file.
+ val obj1 = parseObject("""{ "a" : { "b" : 42 } }""")
+ val obj2 = parseObject("""{ "a" : 2 }""")
+ val obj3 = parseObject("""{ "a" : { "b" : 43, "c" : 44 } }""")
+
+ associativeMerge(Seq(obj1, obj2, obj3)) { merged =>
+ assertEquals(42, merged.getInt("a.b"))
+ assertEquals(1, merged.root.size)
+ assertEquals(1, merged.getObject("a").size())
+ }
+
+ associativeMerge(Seq(obj3, obj2, obj1)) { merged2 =>
+ assertEquals(43, merged2.getInt("a.b"))
+ assertEquals(44, merged2.getInt("a.c"))
+ assertEquals(1, merged2.root.size)
+ assertEquals(2, merged2.getObject("a").size())
+ }
+ }
+
+ @Test
+ def mergeObjectThenSubstitutionThenObject(): Unit = {
+ // the semantic here is that the primitive blocks the
+ // object that occurs at lower priority. This is consistent
+ // with duplicate keys in the same file.
+ val obj1 = parseObject("""{ "a" : { "b" : ${f} } }""")
+ val obj2 = parseObject("""{ "a" : 2 }""")
+ val obj3 = parseObject(
+ """{ "a" : { "b" : ${d}, "c" : ${e} }, "d" : 43, "e" : 44, "f" : 42 }"""
+ )
+
+ associativeMerge(Seq(obj1, obj2, obj3)) { unresolved =>
+ val merged = resolveNoSystem(unresolved, unresolved)
+ assertEquals(42, merged.getInt("a.b"))
+ assertEquals(4, merged.root.size)
+ assertEquals(1, merged.getObject("a").size())
+ }
+
+ associativeMerge(Seq(obj3, obj2, obj1)) { unresolved =>
+ val merged2 = resolveNoSystem(unresolved, unresolved)
+ assertEquals(43, merged2.getInt("a.b"))
+ assertEquals(44, merged2.getInt("a.c"))
+ assertEquals(4, merged2.root.size)
+ assertEquals(2, merged2.getObject("a").size())
+ }
+ }
+
+ @Test
+ def mergePrimitiveThenObjectThenPrimitive(): Unit = {
+ // the primitive should override the object
+ val obj1 = parseObject("""{ "a" : 1 }""")
+ val obj2 = parseObject("""{ "a" : { "b" : 42 } }""")
+ val obj3 = parseObject("""{ "a" : 3 }""")
+
+ associativeMerge(Seq(obj1, obj2, obj3)) { merged =>
+ assertEquals(1, merged.getInt("a"))
+ assertEquals(1, merged.root.size)
+ }
+ }
+
+ @Test
+ def mergeSubstitutionThenObjectThenSubstitution(): Unit = {
+ // the substitution should override the object
+ val obj1 = parseObject("""{ "a" : ${b}, "b" : 1 }""")
+ val obj2 = parseObject("""{ "a" : { "b" : 42 } }""")
+ val obj3 = parseObject("""{ "a" : ${c}, "c" : 2 }""")
+
+ associativeMerge(Seq(obj1, obj2, obj3)) { merged =>
+ val resolved = resolveNoSystem(merged, merged)
+
+ assertEquals(1, resolved.getInt("a"))
+ assertEquals(3, resolved.root.size)
+ }
+ }
+
+ @Test
+ def mergeSubstitutedValues(): Unit = {
+ val obj1 = parseObject("""{ "a" : { "x" : 1, "z" : 4 }, "c" : ${a} }""")
+ val obj2 = parseObject("""{ "b" : { "y" : 2, "z" : 5 }, "c" : ${b} }""")
+
+ val resolved = merge(obj1, obj2).toConfig
+
+ assertEquals(3, resolved.getObject("c").size())
+ assertEquals(1, resolved.getInt("c.x"))
+ assertEquals(2, resolved.getInt("c.y"))
+ assertEquals(4, resolved.getInt("c.z"))
+ }
+
+ @Test
+ def mergeObjectWithSubstituted(): Unit = {
+ val obj1 = parseObject(
+ """{ "a" : { "x" : 1, "z" : 4 }, "c" : { "z" : 42 } }"""
+ )
+ val obj2 = parseObject("""{ "b" : { "y" : 2, "z" : 5 }, "c" : ${b} }""")
+
+ val resolved = merge(obj1, obj2).toConfig
+
+ assertEquals(2, resolved.getObject("c").size())
+ assertEquals(2, resolved.getInt("c.y"))
+ assertEquals(42, resolved.getInt("c.z"))
+
+ val resolved2 = merge(obj2, obj1).toConfig
+
+ assertEquals(2, resolved2.getObject("c").size())
+ assertEquals(2, resolved2.getInt("c.y"))
+ assertEquals(5, resolved2.getInt("c.z"))
+ }
+
+ private val cycleObject = {
+ parseObject("""
+{
+ "foo" : ${bar},
+ "bar" : ${a.b.c},
+ "a" : { "b" : { "c" : ${foo} } }
+}
+""")
+ }
+
+ @Test
+ def mergeHidesCycles(): Unit = {
+ // the point here is that we should not try to evaluate a substitution
+ // that's been overridden, and thus not end up with a cycle as long
+ // as we override the problematic link in the cycle.
+ val e = intercept[ConfigException.UnresolvedSubstitution] {
+ val v = resolveNoSystem(subst("foo"), cycleObject)
+ }
+ assertTrue(
+ "wrong exception: " + e.getMessage,
+ e.getMessage().contains("cycle")
+ )
+
+ val fixUpCycle = parseObject(""" { "a" : { "b" : { "c" : 57 } } } """)
+ val merged = mergeUnresolved(fixUpCycle, cycleObject)
+ val v = resolveNoSystem(subst("foo"), merged)
+ assertEquals(intValue(57), v)
+ }
+
+ @Test
+ def mergeWithObjectInFrontKeepsCycles(): Unit = {
+ // the point here is that if our eventual value will be an object, then
+ // we have to evaluate the substitution to see if it's an object to merge,
+ // so we don't avoid the cycle.
+ val e = intercept[ConfigException.UnresolvedSubstitution] {
+ val v = resolveNoSystem(subst("foo"), cycleObject)
+ }
+ assertTrue(
+ "wrong exception: " + e.getMessage,
+ e.getMessage().contains("cycle")
+ )
+
+ val fixUpCycle = parseObject(
+ """ { "a" : { "b" : { "c" : { "q" : "u" } } } } """
+ )
+ val merged = mergeUnresolved(fixUpCycle, cycleObject)
+ val e2 = intercept[ConfigException.UnresolvedSubstitution] {
+ val v = resolveNoSystem(subst("foo"), merged)
+ }
+ // TODO: it would be nicer if the above threw BadValue with an
+ // explanation about the cycle.
+ // assertTrue(e2.getMessage().contains("cycle"))
+ }
+
+ @Test
+ def mergeSeriesOfSubstitutions(): Unit = {
+ val obj1 = parseObject("""{ "a" : { "x" : 1, "q" : 4 }, "j" : ${a} }""")
+ val obj2 = parseObject("""{ "b" : { "y" : 2, "q" : 5 }, "j" : ${b} }""")
+ val obj3 = parseObject("""{ "c" : { "z" : 3, "q" : 6 }, "j" : ${c} }""")
+
+ associativeMerge(Seq(obj1, obj2, obj3)) { merged =>
+ val resolved = resolveNoSystem(merged, merged)
+
+ assertEquals(4, resolved.getObject("j").size())
+ assertEquals(1, resolved.getInt("j.x"))
+ assertEquals(2, resolved.getInt("j.y"))
+ assertEquals(3, resolved.getInt("j.z"))
+ assertEquals(4, resolved.getInt("j.q"))
+ }
+ }
+
+ @Test
+ def mergePrimitiveAndTwoSubstitutions(): Unit = {
+ val obj1 = parseObject("""{ "j" : 42 }""")
+ val obj2 = parseObject("""{ "b" : { "y" : 2, "q" : 5 }, "j" : ${b} }""")
+ val obj3 = parseObject("""{ "c" : { "z" : 3, "q" : 6 }, "j" : ${c} }""")
+
+ associativeMerge(Seq(obj1, obj2, obj3)) { merged =>
+ val resolved = resolveNoSystem(merged, merged)
+
+ assertEquals(3, resolved.root.size())
+ assertEquals(42, resolved.getInt("j"))
+ assertEquals(2, resolved.getInt("b.y"))
+ assertEquals(3, resolved.getInt("c.z"))
+ }
+ }
+
+ @Test
+ def mergeObjectAndTwoSubstitutions(): Unit = {
+ val obj1 = parseObject("""{ "j" : { "x" : 1, "q" : 4 } }""")
+ val obj2 = parseObject("""{ "b" : { "y" : 2, "q" : 5 }, "j" : ${b} }""")
+ val obj3 = parseObject("""{ "c" : { "z" : 3, "q" : 6 }, "j" : ${c} }""")
+
+ associativeMerge(Seq(obj1, obj2, obj3)) { merged =>
+ val resolved = resolveNoSystem(merged, merged)
+
+ assertEquals(4, resolved.getObject("j").size())
+ assertEquals(1, resolved.getInt("j.x"))
+ assertEquals(2, resolved.getInt("j.y"))
+ assertEquals(3, resolved.getInt("j.z"))
+ assertEquals(4, resolved.getInt("j.q"))
+ }
+ }
+
+ @Test
+ def mergeObjectSubstitutionObjectSubstitution(): Unit = {
+ val obj1 = parseObject("""{ "j" : { "w" : 1, "q" : 5 } }""")
+ val obj2 = parseObject("""{ "b" : { "x" : 2, "q" : 6 }, "j" : ${b} }""")
+ val obj3 = parseObject("""{ "j" : { "y" : 3, "q" : 7 } }""")
+ val obj4 = parseObject("""{ "c" : { "z" : 4, "q" : 8 }, "j" : ${c} }""")
+
+ associativeMerge(Seq(obj1, obj2, obj3, obj4)) { merged =>
+ val resolved = resolveNoSystem(merged, merged)
+
+ assertEquals(5, resolved.getObject("j").size())
+ assertEquals(1, resolved.getInt("j.w"))
+ assertEquals(2, resolved.getInt("j.x"))
+ assertEquals(3, resolved.getInt("j.y"))
+ assertEquals(4, resolved.getInt("j.z"))
+ assertEquals(5, resolved.getInt("j.q"))
+ }
+ }
+
+ private def ignoresFallbacks(m: ConfigMergeable) = {
+ m match {
+ case v: AbstractConfigValue =>
+ v.ignoresFallbacks
+ case c: SimpleConfig =>
+ c.root.ignoresFallbacks
+ }
+ }
+
+ private def testIgnoredMergesDoNothing(nonEmpty: ConfigMergeable): Unit = {
+ // 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 one object to another should not make us ignore fallbacks
+ assertEquals(false, ignoresFallbacks(nonEmpty.withFallback(empty)))
+ assertEquals(false, ignoresFallbacks(empty.withFallback(nonEmpty)))
+ assertEquals(false, ignoresFallbacks(empty.withFallback(empty)))
+ assertEquals(false, ignoresFallbacks(nonEmpty.withFallback(nonEmpty)))
+
+ // 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(): Unit = {
+ val conf = parseConfig("{ a : 1 }")
+ testIgnoredMergesDoNothing(conf)
+ }
+
+ @Test
+ def testNoMergeAcrossArray(): Unit = {
+ val conf = parseConfig("a: {b:1}, a: [2,3], a:{c:4}")
+ assertFalse("a.b found in: " + conf, conf.hasPath("a.b"))
+ assertTrue("a.c not found in: " + conf, conf.hasPath("a.c"))
+ }
+
+ @Test
+ def testNoMergeAcrossUnresolvedArray(): Unit = {
+ val conf = parseConfig("a: {b:1}, a: [2,${x}], a:{c:4}, x: 42")
+ assertFalse("a.b found in: " + conf, conf.hasPath("a.b"))
+ assertTrue("a.c not found in: " + conf, conf.hasPath("a.c"))
+ }
+
+ @Test
+ def testNoMergeLists(): Unit = {
+ val conf = parseConfig("a: [1,2], a: [3,4]")
+ assertEquals("lists did not merge", Seq(3, 4), conf.getIntList("a").asScala)
+ }
+
+ @Test
+ def testListsWithFallback(): Unit = {
+ val list1 = ConfigValueFactory.fromIterable(Seq(1, 2, 3).asJava)
+ val list2 = ConfigValueFactory.fromIterable(Seq(4, 5, 6).asJava)
+ val merged1 = list1.withFallback(list2)
+ val merged2 = list2.withFallback(list1)
+ assertEquals("lists did not merge 1", list1, merged1)
+ assertEquals("lists did not merge 2", list2, merged2)
+ assertFalse("equals is working on these", list1 == list2)
+ assertFalse("equals is working on these", list1 == merged2)
+ assertFalse("equals is working on these", list2 == merged1)
+ }
+
+ @Test
+ def integerRangeChecks(): Unit = {
+ val conf = parseConfig(
+ "{ tooNegative: " + (Integer.MIN_VALUE - 1L) + ", tooPositive: " + (Integer.MAX_VALUE + 1L) + "}"
+ )
+ val en = intercept[ConfigException.WrongType] {
+ conf.getInt("tooNegative")
+ }
+ assertTrue(en.getMessage.contains("range"))
+
+ val ep = intercept[ConfigException.WrongType] {
+ conf.getInt("tooPositive")
+ }
+ assertTrue(ep.getMessage.contains("range"))
+ }
+
+ @Test
+ def isResolvedWorks(): Unit = {
+ val resolved = ConfigFactory.parseString("foo = 1")
+ assertTrue(
+ "config with no substitutions starts as resolved",
+ resolved.isResolved
+ )
+ val unresolved = ConfigFactory.parseString("foo = ${a}, a=42")
+ assertFalse(
+ "config with substitutions starts as not resolved",
+ unresolved.isResolved
+ )
+ val resolved2 = unresolved.resolve()
+ assertTrue("after resolution, config is now resolved", resolved2.isResolved)
+ }
+
+ @Test
+ def allowUnresolvedDoesAllowUnresolvedArrayElements(): Unit = {
+ val values = ConfigFactory.parseString("unknown = [someVal], known = 42")
+ val unresolved = ConfigFactory.parseString(
+ "concat = [${unknown}[]], sibling = [${unknown}, ${known}]"
+ )
+ unresolved.resolve(ConfigResolveOptions.defaults.setAllowUnresolved(true))
+ unresolved.withFallback(values).resolve()
+ unresolved.resolveWith(values)
+ }
+
+ @Test
+ def allowUnresolvedDoesAllowUnresolved(): Unit = {
+ val values = ConfigFactory.parseString("{ foo = 1, bar = 2, m = 3, n = 4}")
+ assertTrue(
+ "config with no substitutions starts as resolved",
+ values.isResolved
+ )
+ val unresolved = ConfigFactory.parseString(
+ "a = ${foo}, b = ${bar}, c { x = ${m}, y = ${n}, z = foo${m}bar }, alwaysResolveable=${alwaysValue}, alwaysValue=42"
+ )
+ assertFalse(
+ "config with substitutions starts as not resolved",
+ unresolved.isResolved
+ )
+
+ // resolve() by default throws with unresolveable substs
+ intercept[ConfigException.UnresolvedSubstitution] {
+ unresolved.resolve(ConfigResolveOptions.defaults)
+ }
+ // we shouldn't be able to get a value without resolving it
+ intercept[ConfigException.NotResolved] {
+ unresolved.getInt("alwaysResolveable")
+ }
+ val allowedUnresolved =
+ unresolved.resolve(ConfigResolveOptions.defaults.setAllowUnresolved(true))
+ // when we partially-resolve we should still resolve what we can
+ assertEquals(
+ "we resolved the resolveable",
+ 42,
+ allowedUnresolved.getInt("alwaysResolveable")
+ )
+ // but unresolved should still all throw
+ for (k <- Seq("a", "b", "c.x", "c.y")) {
+ intercept[ConfigException.NotResolved] { allowedUnresolved.getInt(k) }
+ }
+ intercept[ConfigException.NotResolved] {
+ allowedUnresolved.getString("c.z")
+ }
+
+ // and the partially-resolved thing is not resolved
+ assertFalse(
+ "partially-resolved object is not resolved",
+ allowedUnresolved.isResolved
+ )
+
+ // scope "val resolved"
+ {
+ // and given the values for the resolve, we should be able to
+ val resolved = allowedUnresolved.withFallback(values).resolve()
+ for (kv <- Seq("a" -> 1, "b" -> 2, "c.x" -> 3, "c.y" -> 4)) {
+ assertEquals(kv._2, resolved.getInt(kv._1))
+ }
+ assertEquals("foo3bar", resolved.getString("c.z"))
+ assertTrue("fully resolved object is resolved", resolved.isResolved)
+ }
+
+ // we should also be able to use resolveWith
+ {
+ val resolved = allowedUnresolved.resolveWith(values)
+ for (kv <- Seq("a" -> 1, "b" -> 2, "c.x" -> 3, "c.y" -> 4)) {
+ assertEquals(kv._2, resolved.getInt(kv._1))
+ }
+ assertEquals("foo3bar", resolved.getString("c.z"))
+ assertTrue("fully resolved object is resolved", resolved.isResolved)
+ }
+ }
+
+ @Test
+ def resolveWithWorks(): Unit = {
+ // the a=42 is present here to be sure it gets ignored when we resolveWith
+ val unresolved = ConfigFactory.parseString("foo = ${a}, a = 42")
+ assertEquals(42, unresolved.resolve().getInt("foo"))
+ val source = ConfigFactory.parseString("a = 43")
+ val resolved = unresolved.resolveWith(source)
+ assertEquals(43, resolved.getInt("foo"))
+ }
+
+ /**
+ * A resolver that replaces paths that start with a particular prefix with
+ * strings where that prefix has been replaced with another prefix.
+ */
+ class DummyResolver(
+ prefix: String,
+ newPrefix: String,
+ fallback: ConfigResolver
+ ) extends ConfigResolver {
+ override def lookup(path: String): ConfigValue = {
+ if (path.startsWith(prefix))
+ ConfigValueFactory.fromAnyRef(newPrefix + path.substring(prefix.length))
+ else if (fallback != null)
+ fallback.lookup(path)
+ else
+ null
+ }
+
+ override def withFallback(f: ConfigResolver): ConfigResolver = {
+ if (fallback == null)
+ new DummyResolver(prefix, newPrefix, f)
+ else
+ new DummyResolver(prefix, newPrefix, fallback.withFallback(f))
+ }
+ }
+
+ private def runFallbackTest(
+ expected: String,
+ source: String,
+ allowUnresolved: Boolean,
+ resolvers: ConfigResolver*
+ ) = {
+ val unresolved = ConfigFactory.parseString(source)
+ var options =
+ ConfigResolveOptions.defaults.setAllowUnresolved(allowUnresolved)
+ for (resolver <- resolvers)
+ options = options.appendResolver(resolver)
+ val obj = unresolved.resolve(options).root
+ assertEquals(
+ expected,
+ obj.render(ConfigRenderOptions.concise.setJson(false))
+ )
+ }
+
+ @Test
+ def resolveFallback(): Unit = {
+ runFallbackTest(
+ "x=a,y=b",
+ "x=${a},y=${b}",
+ false,
+ new DummyResolver("", "", null)
+ )
+ runFallbackTest(
+ "x=\"a.b.c\",y=\"a.b.d\"",
+ "x=${a.b.c},y=${a.b.d}",
+ false,
+ new DummyResolver("", "", null)
+ )
+ runFallbackTest(
+ "x=${a.b.c},y=${a.b.d}",
+ "x=${a.b.c},y=${a.b.d}",
+ true,
+ new DummyResolver("x.", "", null)
+ )
+ runFallbackTest(
+ "x=${a.b.c},y=\"e.f\"",
+ "x=${a.b.c},y=${d.e.f}",
+ true,
+ new DummyResolver("d.", "", null)
+ )
+ runFallbackTest(
+ "w=\"Y.c.d\",x=${a},y=\"X.b\",z=\"Y.c\"",
+ "x=${a},y=${a.b},z=${a.b.c},w=${a.b.c.d}",
+ true,
+ new DummyResolver("a.b.", "Y.", null),
+ new DummyResolver("a.", "X.", null)
+ )
+
+ runFallbackTest(
+ "x=${a.b.c}",
+ "x=${a.b.c}",
+ true,
+ new DummyResolver("x.", "", null)
+ )
+ val e = intercept[ConfigException.UnresolvedSubstitution] {
+ runFallbackTest(
+ "x=${a.b.c}",
+ "x=${a.b.c}",
+ false,
+ new DummyResolver("x.", "", null)
+ )
+ }
+ assertTrue(e.getMessage.contains("${a.b.c}"))
+ }
+}
diff --git a/sconfig/shared/src/test/scala/org/ekrich/config/impl/ConfigValueSharedTest.scala b/sconfig/shared/src/test/scala/org/ekrich/config/impl/ConfigValueSharedTest.scala
new file mode 100644
index 00000000..59825130
--- /dev/null
+++ b/sconfig/shared/src/test/scala/org/ekrich/config/impl/ConfigValueSharedTest.scala
@@ -0,0 +1,832 @@
+/**
+ * Copyright (C) 2011 Typesafe Inc.
+ */
+package org.ekrich.config.impl
+
+import org.junit.Assert._
+import org.junit._
+import org.ekrich.config.ConfigValue
+import java.util.Collections
+import scala.jdk.CollectionConverters._
+import org.ekrich.config.ConfigObject
+import org.ekrich.config.ConfigList
+import org.ekrich.config.ConfigException
+import org.ekrich.config.ConfigValueType
+import org.ekrich.config.ConfigRenderOptions
+import org.ekrich.config.ConfigValueFactory
+import org.ekrich.config.ConfigFactory
+
+class ConfigValueSharedTest extends TestUtilsShared {
+ @Test
+ def configOriginEquality(): Unit = {
+ val a = SimpleConfigOrigin.newSimple("foo")
+ val sameAsA = SimpleConfigOrigin.newSimple("foo")
+ val b = SimpleConfigOrigin.newSimple("bar")
+
+ checkEqualObjects(a, a)
+ checkEqualObjects(a, sameAsA)
+ checkNotEqualObjects(a, b)
+ }
+
+ @Test
+ def configIntEquality(): Unit = {
+ val a = intValue(42)
+ val sameAsA = intValue(42)
+ val b = intValue(43)
+
+ checkEqualObjects(a, a)
+ checkEqualObjects(a, sameAsA)
+ checkNotEqualObjects(a, b)
+ }
+
+ @Test
+ def configLongEquality(): Unit = {
+ val a = longValue(Integer.MAX_VALUE + 42L)
+ val sameAsA = longValue(Integer.MAX_VALUE + 42L)
+ val b = longValue(Integer.MAX_VALUE + 43L)
+
+ checkEqualObjects(a, a)
+ checkEqualObjects(a, sameAsA)
+ checkNotEqualObjects(a, b)
+ }
+
+ @Test
+ def configIntAndLongEquality(): Unit = {
+ val longVal = longValue(42L)
+ val intValue = longValue(42)
+ val longValueB = longValue(43L)
+ val intValueB = longValue(43)
+
+ checkEqualObjects(intValue, longVal)
+ checkEqualObjects(intValueB, longValueB)
+ checkNotEqualObjects(intValue, longValueB)
+ checkNotEqualObjects(intValueB, longVal)
+ }
+
+ @Test
+ def configDoubleEquality(): Unit = {
+ val a = doubleValue(3.14)
+ val sameAsA = doubleValue(3.14)
+ val b = doubleValue(4.14)
+
+ checkEqualObjects(a, a)
+ checkEqualObjects(a, sameAsA)
+ checkNotEqualObjects(a, b)
+ }
+
+ @Test
+ def configIntAndDoubleEquality(): Unit = {
+ val doubleVal = doubleValue(3.0)
+ val intValue = longValue(3)
+ val doubleValueB = doubleValue(4.0)
+ val intValueB = doubleValue(4)
+
+ checkEqualObjects(intValue, doubleVal)
+ checkEqualObjects(intValueB, doubleValueB)
+ checkNotEqualObjects(intValue, doubleValueB)
+ checkNotEqualObjects(intValueB, doubleVal)
+ }
+
+ private def configMap(
+ pairs: (String, Int)*
+ ): java.util.Map[String, AbstractConfigValue] = {
+ val m = new java.util.HashMap[String, AbstractConfigValue]()
+ for (p <- pairs) {
+ m.put(p._1, intValue(p._2))
+ }
+ m
+ }
+
+ @Test
+ def configObjectEquality(): Unit = {
+ val aMap = configMap("a" -> 1, "b" -> 2, "c" -> 3)
+ val sameAsAMap = configMap("a" -> 1, "b" -> 2, "c" -> 3)
+ val bMap = configMap("a" -> 3, "b" -> 4, "c" -> 5)
+ // different keys is a different case in the equals implementation
+ val cMap = configMap("x" -> 3, "y" -> 4, "z" -> 5)
+ val a = new SimpleConfigObject(fakeOrigin(), aMap)
+ val sameAsA = new SimpleConfigObject(fakeOrigin(), sameAsAMap)
+ val b = new SimpleConfigObject(fakeOrigin(), bMap)
+ val c = new SimpleConfigObject(fakeOrigin(), cMap)
+
+ checkEqualObjects(a, a)
+ checkEqualObjects(a, sameAsA)
+ checkEqualObjects(b, b)
+ checkEqualObjects(c, c)
+ checkNotEqualObjects(a, b)
+ checkNotEqualObjects(a, c)
+ checkNotEqualObjects(b, c)
+
+ // the config for an equal object is also equal
+ val config = a.toConfig
+ checkEqualObjects(config, config)
+ checkEqualObjects(config, sameAsA.toConfig)
+ checkEqualObjects(a.toConfig, config)
+ checkNotEqualObjects(config, b.toConfig)
+ checkNotEqualObjects(config, c.toConfig)
+
+ // configs are not equal to objects
+ checkNotEqualObjects(a, a.toConfig)
+ checkNotEqualObjects(b, b.toConfig)
+ }
+
+ @Test
+ def configListEquality(): Unit = {
+ val aScalaSeq = Seq(1, 2, 3) map { intValue(_): AbstractConfigValue }
+ val aList = new SimpleConfigList(fakeOrigin(), aScalaSeq.asJava)
+ val sameAsAList = new SimpleConfigList(fakeOrigin(), aScalaSeq.asJava)
+ val bScalaSeq = Seq(4, 5, 6) map { intValue(_): AbstractConfigValue }
+ val bList = new SimpleConfigList(fakeOrigin(), bScalaSeq.asJava)
+
+ checkEqualObjects(aList, aList)
+ checkEqualObjects(aList, sameAsAList)
+ checkNotEqualObjects(aList, bList)
+ }
+
+ @Test
+ def configReferenceEquality(): Unit = {
+ val a = subst("foo")
+ val sameAsA = subst("foo")
+ val b = subst("bar")
+ val c = subst("foo", optional = true)
+
+ assertTrue("wrong type " + a, a.isInstanceOf[ConfigReference])
+ assertTrue("wrong type " + b, b.isInstanceOf[ConfigReference])
+ assertTrue("wrong type " + c, c.isInstanceOf[ConfigReference])
+
+ checkEqualObjects(a, a)
+ checkEqualObjects(a, sameAsA)
+ checkNotEqualObjects(a, b)
+ checkNotEqualObjects(a, c)
+ }
+
+ @Test
+ def configConcatenationEquality(): Unit = {
+ val a = substInString("foo")
+ val sameAsA = substInString("foo")
+ val b = substInString("bar")
+ val c = substInString("foo", optional = true)
+
+ assertTrue("wrong type " + a, a.isInstanceOf[ConfigConcatenation])
+ assertTrue("wrong type " + b, b.isInstanceOf[ConfigConcatenation])
+ assertTrue("wrong type " + c, c.isInstanceOf[ConfigConcatenation])
+
+ checkEqualObjects(a, a)
+ checkEqualObjects(a, sameAsA)
+ checkNotEqualObjects(a, b)
+ checkNotEqualObjects(a, c)
+ }
+
+ @Test
+ def configDelayedMergeEquality(): Unit = {
+ val s1 = subst("foo")
+ val s2 = subst("bar")
+ val a = new ConfigDelayedMerge(
+ fakeOrigin(),
+ List[AbstractConfigValue](s1, s2).asJava
+ )
+ val sameAsA = new ConfigDelayedMerge(
+ fakeOrigin(),
+ List[AbstractConfigValue](s1, s2).asJava
+ )
+ val b = new ConfigDelayedMerge(
+ fakeOrigin(),
+ List[AbstractConfigValue](s2, s1).asJava
+ )
+
+ checkEqualObjects(a, a)
+ checkEqualObjects(a, sameAsA)
+ checkNotEqualObjects(a, b)
+ }
+
+ @Test
+ def configDelayedMergeObjectEquality(): Unit = {
+ val empty = SimpleConfigObject.empty()
+ val s1 = subst("foo")
+ val s2 = subst("bar")
+ val a = new ConfigDelayedMergeObject(
+ fakeOrigin(),
+ List[AbstractConfigValue](empty, s1, s2).asJava
+ )
+ val sameAsA = new ConfigDelayedMergeObject(
+ fakeOrigin(),
+ List[AbstractConfigValue](empty, s1, s2).asJava
+ )
+ val b = new ConfigDelayedMergeObject(
+ fakeOrigin(),
+ List[AbstractConfigValue](empty, s2, s1).asJava
+ )
+
+ checkEqualObjects(a, a)
+ checkEqualObjects(a, sameAsA)
+ checkNotEqualObjects(a, b)
+ }
+
+ @Test
+ def valuesToString(): Unit = {
+ // just check that these don't throw, the exact output
+ // isn't super important since it's just for debugging
+ intValue(10).toString()
+ longValue(11).toString()
+ doubleValue(3.14).toString()
+ stringValue("hi").toString()
+ nullValue().toString()
+ boolValue(true).toString()
+ val emptyObj = SimpleConfigObject.empty()
+ emptyObj.toString()
+ (new SimpleConfigList(
+ fakeOrigin(),
+ Collections.emptyList[AbstractConfigValue]()
+ )).toString()
+ subst("a").toString()
+ substInString("b").toString()
+ val dm = new ConfigDelayedMerge(
+ fakeOrigin(),
+ List[AbstractConfigValue](subst("a"), subst("b")).asJava
+ )
+ dm.toString()
+ val dmo = new ConfigDelayedMergeObject(
+ fakeOrigin(),
+ List[AbstractConfigValue](emptyObj, subst("a"), subst("b")).asJava
+ )
+ dmo.toString()
+
+ fakeOrigin().toString()
+ }
+
+ private def unsupported(body: => Unit): Unit = {
+ intercept[UnsupportedOperationException] {
+ body
+ }
+ }
+
+ @Test
+ def configObjectUnwraps(): Unit = {
+ val m = new SimpleConfigObject(
+ fakeOrigin(),
+ configMap("a" -> 1, "b" -> 2, "c" -> 3)
+ )
+ assertEquals(Map("a" -> 1, "b" -> 2, "c" -> 3), m.unwrapped.asScala)
+ }
+
+ @Test
+ def configObjectImplementsMap(): Unit = {
+ val m: ConfigObject = new SimpleConfigObject(
+ fakeOrigin(),
+ configMap("a" -> 1, "b" -> 2, "c" -> 3)
+ )
+
+ assertEquals(intValue(1), m.get("a"))
+ assertEquals(intValue(2), m.get("b"))
+ assertEquals(intValue(3), m.get("c"))
+ assertNull(m.get("d"))
+ // get can take a non-string
+ assertNull(m.get(new Object()))
+
+ assertTrue(m.containsKey("a"))
+ assertFalse(m.containsKey("z"))
+ // containsKey can take a non-string
+ assertFalse(m.containsKey(new Object()))
+
+ assertTrue(m.containsValue(intValue(1)))
+ assertFalse(m.containsValue(intValue(10)))
+
+ // can take a non-ConfigValue
+ assertFalse(m.containsValue(new Object()))
+
+ assertFalse(m.isEmpty())
+
+ assertEquals(3, m.size())
+
+ val values = Set(intValue(1), intValue(2), intValue(3))
+ assertEquals(values, m.values().asScala.toSet)
+ assertEquals(values, (m.entrySet().asScala map { _.getValue() }).toSet)
+
+ val keys = Set("a", "b", "c")
+ assertEquals(keys, m.keySet().asScala.toSet)
+ assertEquals(keys, (m.entrySet().asScala map { _.getKey() }).toSet)
+
+ unsupported { m.clear() }
+ unsupported { m.put("hello", intValue(42)) }
+ unsupported {
+ m.putAll(Collections.emptyMap[String, AbstractConfigValue]())
+ }
+ unsupported { m.remove("a") }
+ }
+
+ @Test
+ def configListImplementsList(): Unit = {
+ val scalaSeq = Seq[AbstractConfigValue](
+ stringValue("a"),
+ stringValue("b"),
+ stringValue("c")
+ )
+ val l: ConfigList = new SimpleConfigList(fakeOrigin(), scalaSeq.asJava)
+
+ assertEquals(scalaSeq(0), l.get(0))
+ assertEquals(scalaSeq(1), l.get(1))
+ assertEquals(scalaSeq(2), l.get(2))
+
+ assertTrue(l.contains(stringValue("a")))
+
+ assertTrue(
+ l.containsAll(List[AbstractConfigValue](stringValue("b")).asJava)
+ )
+ assertFalse(
+ l.containsAll(List[AbstractConfigValue](stringValue("d")).asJava)
+ )
+
+ assertEquals(1, l.indexOf(scalaSeq(1)))
+
+ assertFalse(l.isEmpty())
+
+ assertEquals(scalaSeq, l.iterator().asScala.toSeq)
+
+ unsupported { l.iterator().remove() }
+
+ assertEquals(1, l.lastIndexOf(scalaSeq(1)))
+
+ val li = l.listIterator()
+ var i = 0
+ while (li.hasNext()) {
+ assertEquals(i > 0, li.hasPrevious())
+ assertEquals(i, li.nextIndex())
+ assertEquals(i - 1, li.previousIndex())
+
+ unsupported { li.remove() }
+ unsupported { li.add(intValue(3)) }
+ unsupported { li.set(stringValue("foo")) }
+
+ val v = li.next()
+ assertEquals(l.get(i), v)
+
+ if (li.hasPrevious()) {
+ // go backward
+ assertEquals(scalaSeq(i), li.previous())
+ // go back forward
+ li.next()
+ }
+
+ i += 1
+ }
+
+ l.listIterator(1) // doesn't throw!
+
+ assertEquals(3, l.size())
+
+ assertEquals(scalaSeq.tail, l.subList(1, l.size()).asScala)
+
+ assertEquals(scalaSeq, l.toArray.toList)
+
+ assertEquals(scalaSeq, l.toArray(new Array[ConfigValue](l.size())).toList)
+
+ unsupported { l.add(intValue(3)) }
+ unsupported { l.add(1, intValue(4)) }
+ unsupported { l.addAll(List[ConfigValue]().asJava) }
+ unsupported { l.addAll(1, List[ConfigValue]().asJava) }
+ unsupported { l.clear() }
+ unsupported { l.remove(intValue(2)) }
+ unsupported { l.remove(1) }
+ unsupported { l.removeAll(List[ConfigValue](intValue(1)).asJava) }
+ unsupported { l.retainAll(List[ConfigValue](intValue(1)).asJava) }
+ unsupported { l.set(0, intValue(42)) }
+ }
+
+ private def unresolved(body: => Unit): Unit = {
+ intercept[ConfigException.NotResolved] {
+ body
+ }
+ }
+
+ @Test
+ def notResolvedThrown(): Unit = {
+ // ConfigSubstitution
+ unresolved { subst("foo").valueType }
+ unresolved { subst("foo").unwrapped }
+
+ // ConfigDelayedMerge
+ val dm = new ConfigDelayedMerge(
+ fakeOrigin(),
+ List[AbstractConfigValue](subst("a"), subst("b")).asJava
+ )
+ unresolved { dm.valueType }
+ unresolved { dm.unwrapped }
+
+ // ConfigDelayedMergeObject
+ val emptyObj = SimpleConfigObject.empty()
+ val dmo = new ConfigDelayedMergeObject(
+ fakeOrigin(),
+ List[AbstractConfigValue](emptyObj, subst("a"), subst("b")).asJava
+ )
+ assertEquals(ConfigValueType.OBJECT, dmo.valueType)
+ unresolved { dmo.unwrapped }
+ unresolved { dmo.get("foo") }
+ unresolved { dmo.containsKey(null) }
+ unresolved { dmo.containsValue(null) }
+ unresolved { dmo.entrySet() }
+ unresolved { dmo.isEmpty() }
+ unresolved { dmo.keySet() }
+ unresolved { dmo.size() }
+ unresolved { dmo.values() }
+ unresolved { dmo.toConfig.getInt("foo") }
+ }
+
+ @Test
+ def roundTripNumbersThroughString(): Unit = {
+ // formats rounded off with E notation
+ val a = "132454454354353245.3254652656454808909932874873298473298472"
+ // formats as 100000.0
+ val b = "1e6"
+ // formats as 5.0E-5
+ val c = "0.00005"
+ // formats as 1E100 (capital E)
+ val d = "1e100"
+
+ val obj = parseConfig(
+ "{ a : " + a + ", b : " + b + ", c : " + c + ", d : " + d + "}"
+ )
+ assertEquals(
+ Seq(a, b, c, d),
+ Seq("a", "b", "c", "d") map {
+ obj.getString(_)
+ }
+ )
+
+ // make sure it still works if we're doing concatenation
+ val obj2 = parseConfig(
+ "{ a : xx " + a + " yy, b : xx " + b + " yy, c : xx " + c + " yy, d : xx " + d + " yy}"
+ )
+ assertEquals(
+ Seq(a, b, c, d) map { "xx " + _ + " yy" },
+ Seq("a", "b", "c", "d") map { obj2.getString(_) }
+ )
+ }
+
+ @Test
+ def mergeOriginsWorks(): Unit = {
+ def o(desc: String, empty: Boolean) = {
+ val values = new java.util.HashMap[String, AbstractConfigValue]()
+ if (!empty)
+ values.put("hello", intValue(37))
+ new SimpleConfigObject(SimpleConfigOrigin.newSimple(desc), values)
+ }
+ def m(values: AbstractConfigObject*) = {
+ AbstractConfigObject.mergeOrigins(values: _*).description
+ }
+
+ // simplest case
+ assertEquals("merge of a,b", m(o("a", false), o("b", false)))
+ // combine duplicate "merge of"
+ assertEquals("merge of a,x,y", m(o("a", false), o("merge of x,y", false)))
+ assertEquals(
+ "merge of a,b,x,y",
+ m(o("merge of a,b", false), o("merge of x,y", false))
+ )
+ // ignore empty objects
+ assertEquals("a", m(o("foo", true), o("a", false)))
+ // unless they are all empty, pick the first one
+ assertEquals("foo", m(o("foo", true), o("a", true)))
+ // merge just one
+ assertEquals("foo", m(o("foo", false)))
+ // merge three
+ assertEquals(
+ "merge of a,b,c",
+ m(o("a", false), o("b", false), o("c", false))
+ )
+ }
+
+ @Test
+ def hasPathWorks(): Unit = {
+ val empty = parseConfig("{}")
+
+ assertFalse(empty.hasPath("foo"))
+
+ val obj = parseConfig("a=null, b.c.d=11, foo=bar")
+
+ // returns true for the non-null values
+ assertTrue(obj.hasPath("foo"))
+ assertTrue(obj.hasPath("b.c.d"))
+ assertTrue(obj.hasPath("b.c"))
+ assertTrue(obj.hasPath("b"))
+
+ // hasPath() is false for null values but containsKey is true
+ assertEquals(nullValue(), obj.root.get("a"))
+ assertTrue(obj.root.containsKey("a"))
+ assertFalse(obj.hasPath("a"))
+
+ // false for totally absent values
+ assertFalse(obj.root.containsKey("notinhere"))
+ assertFalse(obj.hasPath("notinhere"))
+
+ // throws proper exceptions
+ intercept[ConfigException.BadPath] {
+ empty.hasPath("a.")
+ }
+
+ intercept[ConfigException.BadPath] {
+ empty.hasPath("..")
+ }
+ }
+
+ @Test
+ def newNumberWorks(): Unit = {
+ def nL(v: Long) = ConfigNumber.newNumber(fakeOrigin(), v, null)
+ def nD(v: Double) = ConfigNumber.newNumber(fakeOrigin(), v, null)
+
+ // the general idea is that the destination type should depend
+ // only on the actual numeric value, not on the type of the source
+ // value.
+ assertEquals(3.14, nD(3.14).unwrapped)
+ assertEquals(1, nL(1).unwrapped)
+ assertEquals(1, nD(1.0).unwrapped)
+ assertEquals(Int.MaxValue + 1L, nL(Int.MaxValue + 1L).unwrapped)
+ assertEquals(Int.MinValue - 1L, nL(Int.MinValue - 1L).unwrapped)
+ assertEquals(Int.MaxValue + 1L, nD(Int.MaxValue + 1.0).unwrapped)
+ assertEquals(Int.MinValue - 1L, nD(Int.MinValue - 1.0).unwrapped)
+ }
+
+ @Test
+ def automaticBooleanConversions(): Unit = {
+ val trues = parseObject("{ a=true, b=yes, c=on }").toConfig
+ assertEquals(true, trues.getBoolean("a"))
+ assertEquals(true, trues.getBoolean("b"))
+ assertEquals(true, trues.getBoolean("c"))
+
+ val falses = parseObject("{ a=false, b=no, c=off }").toConfig
+ assertEquals(false, falses.getBoolean("a"))
+ assertEquals(false, falses.getBoolean("b"))
+ assertEquals(false, falses.getBoolean("c"))
+ }
+
+ @Test
+ def withOnly(): Unit = {
+ val obj = parseObject("{ a=1, b=2, c.d.y=3, e.f.g=4, c.d.z=5 }")
+ assertEquals("keep only a", parseObject("{ a=1 }"), obj.withOnlyKey("a"))
+ assertEquals(
+ "keep only e",
+ parseObject("{ e.f.g=4 }"),
+ obj.withOnlyKey("e")
+ )
+ assertEquals(
+ "keep only c.d",
+ parseObject("{ c.d.y=3, c.d.z=5 }"),
+ obj.toConfig.withOnlyPath("c.d").root
+ )
+ assertEquals(
+ "keep only c.d.z",
+ parseObject("{ c.d.z=5 }"),
+ obj.toConfig.withOnlyPath("c.d.z").root
+ )
+ assertEquals(
+ "keep nonexistent key",
+ parseObject("{ }"),
+ obj.withOnlyKey("nope")
+ )
+ assertEquals(
+ "keep nonexistent path",
+ parseObject("{ }"),
+ obj.toConfig.withOnlyPath("q.w.e.r.t.y").root
+ )
+ assertEquals(
+ "keep only nonexistent underneath non-object",
+ parseObject("{ }"),
+ obj.toConfig.withOnlyPath("a.nonexistent").root
+ )
+ assertEquals(
+ "keep only nonexistent underneath nested non-object",
+ parseObject("{ }"),
+ obj.toConfig.withOnlyPath("c.d.z.nonexistent").root
+ )
+ }
+
+ @Test
+ def withOnlyInvolvingUnresolved(): Unit = {
+ val obj = parseObject(
+ "{ a = {}, a=${x}, b=${y}, b=${z}, x={asf:1}, y=2, z=3 }"
+ )
+ assertEquals(
+ "keep only a.asf",
+ parseObject("{ a={asf:1} }"),
+ obj.toConfig.resolve().withOnlyPath("a.asf").root
+ )
+
+ intercept[ConfigException.UnresolvedSubstitution] {
+ obj.withOnlyKey("a").toConfig.resolve()
+ }
+
+ intercept[ConfigException.UnresolvedSubstitution] {
+ obj.withOnlyKey("b").toConfig.resolve()
+ }
+
+ assertEquals(ResolveStatus.UNRESOLVED, obj.resolveStatus)
+ assertEquals(ResolveStatus.RESOLVED, obj.withOnlyKey("z").resolveStatus)
+ }
+
+ @Test
+ def without(): Unit = {
+ val obj = parseObject("{ a=1, b=2, c.d.y=3, e.f.g=4, c.d.z=5 }")
+ assertEquals(
+ "without a",
+ parseObject("{ b=2, c.d.y=3, e.f.g=4, c.d.z=5 }"),
+ obj.withoutKey("a")
+ )
+ assertEquals(
+ "without c",
+ parseObject("{ a=1, b=2, e.f.g=4 }"),
+ obj.withoutKey("c")
+ )
+ assertEquals(
+ "without c.d",
+ parseObject("{ a=1, b=2, e.f.g=4, c={} }"),
+ obj.toConfig.withoutPath("c.d").root
+ )
+ assertEquals(
+ "without c.d.z",
+ parseObject("{ a=1, b=2, c.d.y=3, e.f.g=4 }"),
+ obj.toConfig.withoutPath("c.d.z").root
+ )
+ assertEquals(
+ "without nonexistent key",
+ parseObject("{ a=1, b=2, c.d.y=3, e.f.g=4, c.d.z=5 }"),
+ obj.withoutKey("nonexistent")
+ )
+ assertEquals(
+ "without nonexistent path",
+ parseObject("{ a=1, b=2, c.d.y=3, e.f.g=4, c.d.z=5 }"),
+ obj.toConfig.withoutPath("q.w.e.r.t.y").root
+ )
+ assertEquals(
+ "without nonexistent path with existing prefix",
+ parseObject("{ a=1, b=2, c.d.y=3, e.f.g=4, c.d.z=5 }"),
+ obj.toConfig.withoutPath("a.foo").root
+ )
+ }
+
+ @Test
+ def withoutInvolvingUnresolved(): Unit = {
+ val obj = parseObject(
+ "{ a = {}, a=${x}, b=${y}, b=${z}, x={asf:1}, y=2, z=3 }"
+ )
+ assertEquals(
+ "without a.asf",
+ parseObject("{ a={}, b=3, x={asf:1}, y=2, z=3 }"),
+ obj.toConfig.resolve().withoutPath("a.asf").root
+ )
+
+ intercept[ConfigException.UnresolvedSubstitution] {
+ obj.withoutKey("x").toConfig.resolve()
+ }
+
+ intercept[ConfigException.UnresolvedSubstitution] {
+ obj.withoutKey("z").toConfig.resolve()
+ }
+
+ assertEquals(ResolveStatus.UNRESOLVED, obj.resolveStatus)
+ assertEquals(ResolveStatus.UNRESOLVED, obj.withoutKey("a").resolveStatus)
+ assertEquals(
+ ResolveStatus.RESOLVED,
+ obj.withoutKey("a").withoutKey("b").resolveStatus
+ )
+ }
+
+ @Test
+ def atPathWorksOneElement(): Unit = {
+ val v = ConfigValueFactory.fromAnyRef(42: Integer)
+ val config = v.atPath("a")
+ assertEquals(parseConfig("a=42"), config)
+ assertTrue(config.getValue("a") eq v)
+ assertTrue(config.origin.description.contains("atPath"))
+ }
+
+ @Test
+ def atPathWorksTwoElements(): Unit = {
+ val v = ConfigValueFactory.fromAnyRef(42: Integer)
+ val config = v.atPath("a.b")
+ assertEquals(parseConfig("a.b=42"), config)
+ assertTrue(config.getValue("a.b") eq v)
+ assertTrue(config.origin.description.contains("atPath"))
+ }
+
+ @Test
+ def atPathWorksFourElements(): Unit = {
+ val v = ConfigValueFactory.fromAnyRef(42: Integer)
+ val config = v.atPath("a.b.c.d")
+ assertEquals(parseConfig("a.b.c.d=42"), config)
+ assertTrue(config.getValue("a.b.c.d") eq v)
+ assertTrue(config.origin.description.contains("atPath"))
+ }
+
+ @Test
+ def atKeyWorks(): Unit = {
+ val v = ConfigValueFactory.fromAnyRef(42: Integer)
+ val config = v.atKey("a")
+ assertEquals(parseConfig("a=42"), config)
+ assertTrue(config.getValue("a") eq v)
+ assertTrue(config.origin.description.contains("atKey"))
+ }
+
+ @Test
+ def withValueDepth1FromEmpty(): Unit = {
+ val v = ConfigValueFactory.fromAnyRef(42: Integer)
+ val config = ConfigFactory.empty().withValue("a", v)
+ assertEquals(parseConfig("a=42"), config)
+ assertTrue(config.getValue("a") eq v)
+ }
+
+ @Test
+ def withValueDepth2FromEmpty(): Unit = {
+ val v = ConfigValueFactory.fromAnyRef(42: Integer)
+ val config = ConfigFactory.empty().withValue("a.b", v)
+ assertEquals(parseConfig("a.b=42"), config)
+ assertTrue(config.getValue("a.b") eq v)
+ }
+
+ @Test
+ def withValueDepth3FromEmpty(): Unit = {
+ val v = ConfigValueFactory.fromAnyRef(42: Integer)
+ val config = ConfigFactory.empty().withValue("a.b.c", v)
+ assertEquals(parseConfig("a.b.c=42"), config)
+ assertTrue(config.getValue("a.b.c") eq v)
+ }
+
+ @Test
+ def withValueDepth1OverwritesExisting(): Unit = {
+ val v = ConfigValueFactory.fromAnyRef(47: Integer)
+ val old = v.atPath("a")
+ val config = old.withValue("a", ConfigValueFactory.fromAnyRef(42: Integer))
+ assertEquals(parseConfig("a=42"), config)
+ assertEquals(42, config.getInt("a"))
+ }
+
+ @Test
+ def withValueDepth2OverwritesExisting(): Unit = {
+ val v = ConfigValueFactory.fromAnyRef(47: Integer)
+ val old = v.atPath("a.b")
+ val config =
+ old.withValue("a.b", ConfigValueFactory.fromAnyRef(42: Integer))
+ assertEquals(parseConfig("a.b=42"), config)
+ assertEquals(42, config.getInt("a.b"))
+ }
+
+ @Test
+ def withValueInsideExistingObject(): Unit = {
+ val v = ConfigValueFactory.fromAnyRef(47: Integer)
+ val old = v.atPath("a.c")
+ val config =
+ old.withValue("a.b", ConfigValueFactory.fromAnyRef(42: Integer))
+ assertEquals(parseConfig("a.b=42,a.c=47"), config)
+ assertEquals(42, config.getInt("a.b"))
+ assertEquals(47, config.getInt("a.c"))
+ }
+
+ @Test
+ def withValueBuildComplexConfig(): Unit = {
+ val v1 = ConfigValueFactory.fromAnyRef(1: Integer)
+ val v2 = ConfigValueFactory.fromAnyRef(2: Integer)
+ val v3 = ConfigValueFactory.fromAnyRef(3: Integer)
+ val v4 = ConfigValueFactory.fromAnyRef(4: Integer)
+ val config = ConfigFactory
+ .empty()
+ .withValue("a", v1)
+ .withValue("b.c", v2)
+ .withValue("b.d", v3)
+ .withValue("x.y.z", v4)
+ assertEquals(parseConfig("a=1,b.c=2,b.d=3,x.y.z=4"), config)
+ }
+
+ @Test
+ def renderWithNewlinesInDescription(): Unit = {
+ val v = ConfigValueFactory.fromAnyRef(
+ 89: Integer,
+ "this is a description\nwith some\nnewlines"
+ )
+ val list = new SimpleConfigList(
+ SimpleConfigOrigin.newSimple("\n5\n6\n7\n"),
+ java.util.Collections.singletonList(v.asInstanceOf[AbstractConfigValue])
+ )
+ val conf = ConfigFactory.empty().withValue("bar", list)
+ val rendered = conf.root.render
+ def assertHas(s: String): Unit =
+ assertTrue(s"has ${s.replace("\n", "\\n")} in it", rendered.contains(s))
+ assertHas("is a description\n")
+ assertHas("with some\n")
+ assertHas("newlines\n")
+ assertHas("#\n")
+ assertHas("5\n")
+ assertHas("6\n")
+ assertHas("7\n")
+ val parsed = ConfigFactory.parseString(rendered)
+
+ assertEquals(conf, parsed)
+ }
+
+ @Test
+ def renderSorting(): Unit = {
+ val config = parseConfig("""0=a,1=b,2=c,3=d,10=e,20=f,30=g""")
+ val rendered = config.root.render(ConfigRenderOptions.concise)
+ assertEquals(
+ """{"0":"a","1":"b","2":"c","3":"d","10":"e","20":"f","30":"g"}""",
+ rendered
+ )
+ }
+}
diff --git a/sconfig/jvm/src/test/scala/org/ekrich/config/impl/JsonTest.scala b/sconfig/shared/src/test/scala/org/ekrich/config/impl/Json4sTest.scala
similarity index 75%
rename from sconfig/jvm/src/test/scala/org/ekrich/config/impl/JsonTest.scala
rename to sconfig/shared/src/test/scala/org/ekrich/config/impl/Json4sTest.scala
index 6dc05834..78363ff7 100644
--- a/sconfig/jvm/src/test/scala/org/ekrich/config/impl/JsonTest.scala
+++ b/sconfig/shared/src/test/scala/org/ekrich/config/impl/Json4sTest.scala
@@ -3,17 +3,17 @@
*/
package org.ekrich.config.impl
-import java.util.HashMap
-
import org.junit.Assert._
import org.junit._
-import spray.json._
+import org.json4s._
+import org.json4s.native.JsonParser
+import java.{util => ju}
import scala.jdk.CollectionConverters._
import org.ekrich.config._
-class JsonTest extends TestUtils {
+class Json4sTest extends TestUtilsJson4s {
def parse(s: String): ConfigValue = {
val options = ConfigParseOptions.defaults
.setOriginDescription("test json string")
@@ -28,52 +28,55 @@ class JsonTest extends TestUtils {
Parseable.newString(s, options).parseValue()
}
- private[this] def toJson(value: ConfigValue): JsValue = {
+ private[this] def toJson(value: ConfigValue): JValue = {
value match {
case v: ConfigObject =>
- JsObject(
+ JObject(
v.keySet()
.asScala
- .map(k => (k, toJson(v.get(k))))
- .toMap
+ .map(k => JsonAST.JField(k, toJson(v.get(k))))
+ .toList
)
case v: ConfigList =>
- JsArray(v.asScala.toVector.map(elem => toJson(elem)))
+ JArray(v.asScala.toList.map(elem => toJson(elem)))
case v: ConfigBoolean =>
- JsBoolean(v.unwrapped)
+ JBool(v.unwrapped)
case v: ConfigInt =>
- JsNumber(BigInt(v.unwrapped))
+ JInt(BigInt(v.unwrapped))
case v: ConfigLong =>
- JsNumber(BigInt(v.unwrapped))
+ JInt(BigInt(v.unwrapped))
case v: ConfigDouble =>
- JsNumber(v.unwrapped)
+ JDouble(v.unwrapped)
case v: ConfigString =>
- JsString(v.unwrapped)
+ JString(v.unwrapped)
case v: ConfigNull =>
- JsNull
+ JNull
}
}
- private[this] def fromJson(jsonValue: JsValue): AbstractConfigValue = {
+ private[this] def fromJson(jsonValue: JValue): AbstractConfigValue = {
jsonValue match {
- case JsObject(fields) =>
- val m = new HashMap[String, AbstractConfigValue]()
+ case JObject(fields) =>
+ val m = new ju.HashMap[String, AbstractConfigValue]()
fields.foreach(field => m.put(field._1, fromJson(field._2)))
new SimpleConfigObject(fakeOrigin(), m)
- case JsArray(values) =>
+ case JArray(values) =>
new SimpleConfigList(fakeOrigin(), values.map(fromJson(_)).asJava)
- case JsNumber(n) =>
- if (n.isValidInt) intValue(n.intValue)
- else if (n.isValidLong) longValue(n.longValue)
- else doubleValue(n.doubleValue)
- case JsBoolean(b) =>
+ case JInt(n) => intValue(n.intValue)
+ case JLong(n) => longValue(n)
+ case JDouble(n) => doubleValue(n)
+ case JBool(b) =>
new ConfigBoolean(fakeOrigin(), b)
- case JsString(s) =>
+ case JString(s) =>
new ConfigString.Quoted(fakeOrigin(), s)
- case JsNull =>
+ case JNull =>
new ConfigNull(fakeOrigin())
+ case JNothing =>
+ throw new ConfigException.BugOrBroken(
+ "Returned JNothing, probably an empty document (?)"
+ )
case _ =>
- throw new IllegalStateException("Unexpected JsValue: " + jsonValue)
+ throw new IllegalStateException("Unexpected JValue: " + jsonValue)
}
}
@@ -81,7 +84,7 @@ class JsonTest extends TestUtils {
try {
block
} catch {
- case e: JsonParser.ParsingException =>
+ case e: ParserUtil.ParseException =>
throw new ConfigException.Parse(
SimpleConfigOrigin.newSimple("json parser"),
e.getMessage(),
@@ -94,7 +97,7 @@ class JsonTest extends TestUtils {
// the Json parser for a variety of JSON strings.
private def fromJsonWithJsonParser(json: String): ConfigValue = {
- withJsonExceptionsConverted(fromJson(JsonParser(ParserInput(json))))
+ withJsonExceptionsConverted(fromJson(JsonParser.parse(json)))
}
// For string quoting, check behavior of escaping a random character instead of one on the list
@@ -110,9 +113,10 @@ class JsonTest extends TestUtils {
}
} else {
addOffendingJsonToException("json", invalid.test) {
- intercept[ConfigException] {
- fromJsonWithJsonParser(invalid.test)
- }
+ assertThrows(
+ classOf[ConfigException],
+ () => fromJsonWithJsonParser(invalid.test)
+ )
tested += 1
}
}
@@ -124,9 +128,10 @@ class JsonTest extends TestUtils {
// be sure we also throw
for (invalid <- whitespaceVariations(invalidJson, false)) {
addOffendingJsonToException("config", invalid.test) {
- intercept[ConfigException] {
- parse(invalid.test)
- }
+ assertThrows(
+ classOf[ConfigException],
+ () => parse(invalid.test)
+ )
tested += 1
}
}
diff --git a/sconfig/shared/src/test/scala/org/ekrich/config/impl/TestUtilsJson4s.scala b/sconfig/shared/src/test/scala/org/ekrich/config/impl/TestUtilsJson4s.scala
new file mode 100644
index 00000000..3f754b9a
--- /dev/null
+++ b/sconfig/shared/src/test/scala/org/ekrich/config/impl/TestUtilsJson4s.scala
@@ -0,0 +1,366 @@
+/**
+ * Copyright (C) 2011 Typesafe Inc.
+ */
+package org.ekrich.config.impl
+
+import java.io.Reader
+import java.io.StringReader
+
+import language.implicitConversions
+import scala.jdk.CollectionConverters._
+
+import org.ekrich.config.ConfigOrigin
+import org.ekrich.config.ConfigSyntax
+
+abstract trait TestUtilsJson4s {
+
+ case class ParseTest(
+ jsonBehaviorUnexpected: Boolean,
+ whitespaceMatters: Boolean,
+ test: String
+ )
+ object ParseTest {
+ def apply(jsonBehaviorUnexpected: Boolean, test: String): ParseTest = {
+ ParseTest(jsonBehaviorUnexpected, false, test)
+ }
+ }
+ implicit def string2jsontest(test: String): ParseTest =
+ ParseTest(false, test)
+
+ // note: it's important to put {} or [] at the root if you
+ // want to test "invalidity reasons" other than "wrong root"
+ // if json throws, change to false
+ private val invalidJsonInvalidConf = List[ParseTest](
+ "{",
+ "}",
+ "[",
+ "]",
+ ",",
+ ParseTest(false, "10"), // value not in array or object
+ ParseTest(false, "\"foo\""), // value not in array or object
+ "\"", // single quote by itself
+ ParseTest(true, "[,]"), // array with just a comma in it
+ ParseTest(true, "[,,]"), // array with just two commas in it
+ ParseTest(true, "[1,2,,]"), // array with two trailing commas
+ ParseTest(true, "[,1,2]"), // array with initial comma
+ ParseTest(true, "{ , }"), // object with just a comma in it
+ ParseTest(true, "{ , , }"), // object with just two commas in it
+ "{ 1,2 }", // object with single values not key-value pair
+ ParseTest(true, "{ , \"foo\" : 10 }"), // object starts with comma
+ ParseTest(true, "{ \"foo\" : 10 ,, }"), // object has two trailing commas
+ " \"a\" : 10 ,, ", // two trailing commas for braceless root object
+ "{ \"foo\" : }", // no value in object
+ "{ : 10 }", // no key in object
+ ParseTest(false, " \"foo\" : "), // no value in object with no braces
+ ParseTest(false, " : 10 "), // no key in object with no braces
+ " \"foo\" : 10 } ", // close brace but no open
+ " \"foo\" : 10 [ ", // no-braces object with trailing gunk
+ "{ \"foo\" }", // no value or colon
+ "{ \"a\" : [ }", // [ is not a valid value
+ "{ \"foo\" : 10, true }", // non-key after comma
+ "{ foo \n bar : 10 }", // newline in the middle of the unquoted key
+ "[ 1, \\", // ends with backslash
+ // these two problems are ignored by the json tokenizer
+ "[:\"foo\", \"bar\"]", // colon in an array
+ "[\"foo\" : \"bar\"]", // colon in an array another way
+ "[ \"hello ]", // unterminated string
+ ParseTest(true, "{ \"foo\" , true }"), // comma instead of colon
+ ParseTest(
+ true,
+ "{ \"foo\" : true \"bar\" : false }"
+ ), // missing comma between fields
+ "[ 10, }]", // array with } as an element
+ "[ 10, {]", // array with { as an element
+ "{}x", // trailing invalid token after the root object
+ "[]x", // trailing invalid token after the root array
+ ParseTest(true, "{}{}"), // trailing token after the root object
+ ParseTest(false, "{}true"), // trailing token after the root object
+ ParseTest(true, "[]{}"), // trailing valid token after the root array
+ ParseTest(false, "[]true"), // trailing valid token after the root array
+ "[${]", // unclosed substitution
+ "[$]", // '$' by itself
+ "[$ ]", // '$' by itself with spaces after
+ "[${}]", // empty substitution (no path)
+ "[${?}]", // no path with ? substitution
+ ParseTest(false, true, "[${ ?foo}]"), // space before ? not allowed
+ """{ "a" : [1,2], "b" : y${a}z }""", // trying to interpolate an array in a string
+ """{ "a" : { "c" : 2 }, "b" : y${a}z }""", // trying to interpolate an object in a string
+ """{ "a" : ${a} }""", // simple cycle
+ """[ { "a" : 2, "b" : ${${a}} } ]""", // nested substitution
+ "[ = ]", // = is not a valid token in unquoted text
+ "[ + ]",
+ "[ # ]",
+ "[ ` ]",
+ "[ ^ ]",
+ "[ ? ]",
+ "[ ! ]",
+ "[ @ ]",
+ "[ * ]",
+ "[ & ]",
+ "[ \\ ]",
+ "+=",
+ "[ += ]",
+ "+= 10",
+ "10 +=",
+ "[ 10e+3e ]", // "+" not allowed in unquoted strings, and not a valid number
+ ParseTest(true, "[ \"foo\nbar\" ]"), // unescaped newline in quoted string
+ "[ # comment ]",
+ "${ #comment }",
+ "[ // comment ]",
+ "${ // comment }",
+ "{ include \"bar\" : 10 }", // include with a value after it
+ "{ include foo }", // include with unquoted string
+ "{ include : { \"a\" : 1 } }", // include used as unquoted key
+ "a=", // no value
+ "a:", // no value with colon
+ "a= ", // no value with whitespace after
+ "a.b=", // no value with path
+ "{ a= }", // no value inside braces
+ "{ a: }"
+ ) // no value with colon inside braces
+
+ // We'll automatically try each of these with whitespace modifications
+ // so no need to add every possible whitespace variation
+ protected val validJson = List[ParseTest](
+ "{}",
+ "[]",
+ """{ "foo" : "bar" }""",
+ """["foo", "bar"]""",
+ """{ "foo" : 42 }""",
+ "{ \"foo\"\n : 42 }", // newline after key
+ "{ \"foo\" : \n 42 }", // newline after colon
+ """[10, 11]""",
+ """[10,"foo"]""",
+ """{ "foo" : "bar", "baz" : "boo" }""",
+ """{ "foo" : { "bar" : "baz" }, "baz" : "boo" }""",
+ """{ "foo" : { "bar" : "baz", "woo" : "w00t" }, "baz" : "boo" }""",
+ """{ "foo" : [10,11,12], "baz" : "boo" }""",
+ """[{},{},{},{}]""",
+ """[[[[[[]]]]]]""",
+ """[[1], [1,2], [1,2,3], []]""", // nested multiple-valued array
+ """{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":42}}}}}}}}""",
+ "[ \"#comment\" ]", // quoted # comment
+ "[ \"//comment\" ]", // quoted // comment
+ // this long one is mostly to test rendering
+ """{ "foo" : { "bar" : "baz", "woo" : "w00t" }, "baz" : { "bar" : "baz", "woo" : [1,2,3,4], "w00t" : true, "a" : false, "b" : 3.14, "c" : null } }""",
+ "{}",
+ ParseTest(true, "[ 10e+3 ]")
+ )
+
+ // if json throws, change to false
+ private val validConfInvalidJson = List[ParseTest](
+ "", // empty document
+ " ", // empty document single space
+ "\n", // empty document single newline
+ " \n \n \n\n\n", // complicated empty document
+ "# foo", // just a comment
+ "# bar\n", // just a comment with a newline
+ "# foo\n//bar", // comment then another with no newline
+ """{ "foo" = 42 }""", // equals rather than colon
+ """{ "foo" = (42) }""", // value with round braces
+ """{ foo { "bar" : 42 } }""", // omit the colon for object value
+ """{ foo baz { "bar" : 42 } }""", // omit the colon with unquoted key with spaces
+ """ "foo" : 42 """, // omit braces on root object
+ """{ "foo" : bar }""", // no quotes on value
+ """{ "foo" : null bar 42 baz true 3.14 "hi" }""", // bunch of values to concat into a string
+ "{ foo : \"bar\" }", // no quotes on key
+ "{ foo : bar }", // no quotes on key or value
+ "{ foo.bar : bar }", // path expression in key
+ "{ foo.\"hello world\".baz : bar }", // partly-quoted path expression in key
+ "{ foo.bar \n : bar }", // newline after path expression in key
+ "{ foo bar : bar }", // whitespace in the key
+ "{ true : bar }", // key is a non-string token
+ ParseTest(true, """{ "foo" : "bar", "foo" : "bar2" }"""), // dup keys
+ ParseTest(true, "[ 1, 2, 3, ]"), // single trailing comma
+ ParseTest(true, "[1,2,3 , ]"), // single trailing comma with whitespace
+ ParseTest(true, "[1,2,3\n\n , \n]"), // single trailing comma with newlines
+ ParseTest(true, "[1,]"), // single trailing comma with one-element array
+ ParseTest(true, "{ \"foo\" : 10, }"), // extra trailing comma
+ ParseTest(true, "{ \"a\" : \"b\", }"), // single trailing comma in object
+ "{ a : b, }", // single trailing comma in object (unquoted strings)
+ "{ a : b \n , \n }", // single trailing comma in object with newlines
+ "a : b, c : d,", // single trailing comma in object with no root braces
+ "{ a : b\nc : d }", // skip comma if there's a newline
+ "a : b\nc : d", // skip comma if there's a newline and no root braces
+ "a : b\nc : d,", // skip one comma but still have one at the end
+ "[ foo ]", // not a known token in JSON
+ "[ t ]", // start of "true" but ends wrong in JSON
+ "[ tx ]",
+ "[ tr ]",
+ "[ trx ]",
+ "[ tru ]",
+ "[ trux ]",
+ "[ truex ]",
+ "[ 10x ]", // number token with trailing junk
+ "[ / ]", // unquoted string "slash"
+ "{ include \"foo\" }", // valid include
+ "{ include\n\"foo\" }", // include with just a newline separating from string
+ "{ include\"foo\" }", // include with no whitespace after it
+ "[ include ]", // include can be a string value in an array
+ "{ foo : include }", // include can be a field value also
+ "{ include \"foo\", \"a\" : \"b\" }", // valid include followed by comma and field
+ "{ foo include : 42 }", // valid to have a key not starting with include
+ "[ ${foo} ]",
+ "[ ${?foo} ]",
+ "[ ${\"foo\"} ]",
+ "[ ${foo.bar} ]",
+ "[ abc xyz ${foo.bar} qrs tuv ]", // value concatenation
+ "[ 1, 2, 3, blah ]",
+ "[ ${\"foo.bar\"} ]",
+ "{} # comment",
+ "{} // comment",
+ """{ "foo" #comment
+: 10 }""",
+ """{ "foo" // comment
+: 10 }""",
+ """{ "foo" : #comment
+ 10 }""",
+ """{ "foo" : // comment
+ 10 }""",
+ """{ "foo" : 10 #comment
+ }""",
+ """{ "foo" : 10 // comment
+ }""",
+ """[ 10, # comment
+ 11]""",
+ """[ 10, // comment
+ 11]""",
+ """[ 10 # comment
+, 11]""",
+ """[ 10 // comment
+, 11]""",
+ """{ /a/b/c : 10 }""", // key has a slash in it
+ ParseTest(false, true, "[${ foo.bar}]"), // substitution with leading spaces
+ ParseTest(
+ false,
+ true,
+ "[${foo.bar }]"
+ ), // substitution with trailing spaces
+ ParseTest(
+ false,
+ true,
+ "[${ \"foo.bar\"}]"
+ ), // substitution with leading spaces and quoted
+ ParseTest(
+ false,
+ true,
+ "[${\"foo.bar\" }]"
+ ), // substitution with trailing spaces and quoted
+ """[ ${"foo""bar"} ]""", // multiple strings in substitution
+ """[ ${foo "bar" baz} ]""", // multiple strings and whitespace in substitution
+ "[${true}]", // substitution with unquoted true token
+ "a = [], a += b", // += operator with previous init
+ "{ a = [], a += 10 }", // += in braces object with previous init
+ "a += b", // += operator without previous init
+ "{ a += 10 }", // += in braces object without previous init
+ "[ 10e3e3 ]", // two exponents. this should parse to a number plus string "e3"
+ "[ 1-e3 ]", // malformed number should end up as a string instead
+ "[ 1.0.0 ]", // two decimals, should end up as a string
+ "[ 1.0. ]"
+ ) // trailing decimal should end up as a string
+
+ protected val invalidJson = validConfInvalidJson ++ invalidJsonInvalidConf
+
+ protected val invalidConf = invalidJsonInvalidConf
+
+ // .conf is a superset of JSON so validJson just goes in here
+ protected val validConf = validConfInvalidJson ++ validJson
+
+ protected def addOffendingJsonToException[R](parserName: String, s: String)(
+ body: => R
+ ) = {
+ try {
+ body
+ } catch {
+ case t: Throwable =>
+ val tokens =
+ try {
+ "tokens: " + tokenizeAsList(s)
+ } catch {
+ case e: Throwable =>
+ "tokenizer failed: " + e.getMessage()
+ }
+ // don't use AssertionError because it seems to keep Eclipse
+ // from showing the causing exception in JUnit view for some reason
+ throw new Exception(
+ parserName + " parser did wrong thing on '" + s + "', " + tokens,
+ t
+ )
+ }
+ }
+
+ protected def whitespaceVariations(
+ tests: Seq[ParseTest],
+ validInJsonParser: Boolean
+ ): Seq[ParseTest] = {
+ val variations = List(
+ (s: String) => s, // identity
+ (s: String) => " " + s,
+ (s: String) => s + " ",
+ (s: String) => " " + s + " ",
+ (s: String) =>
+ s.replace(
+ " ",
+ ""
+ ), // this would break with whitespace in a key or value
+ (s: String) =>
+ s.replace(":", " : "), // could break with : in a key or value
+ (s: String) =>
+ s.replace(",", " , ") // could break with , in a key or value
+ )
+ tests flatMap { t =>
+ if (t.whitespaceMatters) {
+ Seq(t)
+ } else {
+ val withNonAscii =
+ if (t.test.contains(" "))
+ Seq(
+ ParseTest(validInJsonParser, t.test.replace(" ", "\u2003"))
+ ) // 2003 = em space, to test non-ascii whitespace
+ else
+ Seq()
+ withNonAscii ++ (for (v <- variations)
+ yield ParseTest(t.jsonBehaviorUnexpected, v(t.test)))
+ }
+ }
+ }
+
+ def fakeOrigin() = {
+ SimpleConfigOrigin.newSimple("fake origin")
+ }
+
+ // it's important that these do NOT use the public API to create the
+ // instances, because we may be testing that the public API returns the
+ // right instance by comparing to these, so using public API here would
+ // make the test compare public API to itself.
+ protected def intValue(i: Int) = new ConfigInt(fakeOrigin(), i, null)
+ protected def longValue(l: Long) = new ConfigLong(fakeOrigin(), l, null)
+ protected def boolValue(b: Boolean) = new ConfigBoolean(fakeOrigin(), b)
+ protected def nullValue() = new ConfigNull(fakeOrigin())
+ protected def stringValue(s: String) =
+ new ConfigString.Quoted(fakeOrigin(), s)
+ protected def doubleValue(d: Double) =
+ new ConfigDouble(fakeOrigin(), d, null)
+
+ def tokenize(
+ origin: ConfigOrigin,
+ input: Reader
+ ): java.util.Iterator[Token] = {
+ Tokenizer.tokenize(origin, input, ConfigSyntax.CONF)
+ }
+
+ def tokenize(input: Reader): java.util.Iterator[Token] = {
+ tokenize(SimpleConfigOrigin.newSimple("anonymous Reader"), input)
+ }
+
+ def tokenize(s: String): java.util.Iterator[Token] = {
+ val reader = new StringReader(s)
+ val result = tokenize(reader)
+ // reader.close() // can't close until the iterator is traversed, so this tokenize() flavor is inherently broken
+ result
+ }
+
+ def tokenizeAsList(s: String) = {
+ tokenize(s).asScala.toList
+ }
+}
diff --git a/sconfig/shared/src/test/scala/org/ekrich/config/impl/TestUtilsShared.scala b/sconfig/shared/src/test/scala/org/ekrich/config/impl/TestUtilsShared.scala
new file mode 100644
index 00000000..d088ef90
--- /dev/null
+++ b/sconfig/shared/src/test/scala/org/ekrich/config/impl/TestUtilsShared.scala
@@ -0,0 +1,830 @@
+/**
+ * Copyright (C) 2011 Typesafe Inc.
+ */
+package org.ekrich.config.impl
+
+import org.junit.Assert._
+import org.ekrich.config.ConfigOrigin
+import java.io.Reader
+import java.io.StringReader
+import org.ekrich.config.ConfigParseOptions
+import org.ekrich.config.ConfigSyntax
+import org.ekrich.config.ConfigFactory
+import scala.annotation.tailrec
+import java.net.URL
+import java.util.concurrent.Executors
+import java.util.concurrent.Callable
+import org.ekrich.config._
+import scala.reflect.ClassTag
+import scala.reflect.classTag
+import scala.jdk.CollectionConverters._
+import language.implicitConversions
+
+abstract trait TestUtilsShared {
+ protected def intercept[E <: Throwable: ClassTag](block: => Any): E = {
+ val expectedClass = classTag[E].runtimeClass
+ var thrown: Option[Throwable] = None
+ val result =
+ try {
+ Some(block)
+ } catch {
+ case t: Throwable =>
+ thrown = Some(t)
+ None
+ }
+ thrown match {
+ case Some(t) if expectedClass.isAssignableFrom(t.getClass) =>
+ t.asInstanceOf[E]
+ case Some(t) =>
+ throw new Exception(
+ s"Expected exception ${expectedClass.getName} was not thrown, got $t",
+ t
+ )
+ case None =>
+ throw new Exception(
+ s"Expected exception ${expectedClass.getName} was not thrown, no exception was thrown and got result $result"
+ )
+ }
+ }
+
+ protected def describeFailure[A](desc: String)(code: => A): A = {
+ try {
+ code
+ } catch {
+ case t: Throwable =>
+ println("Failure on: '%s'".format(desc))
+ throw t
+ }
+ }
+
+ private class NotEqualToAnythingElse {
+ override def equals(other: Any) = {
+ other match {
+ case x: NotEqualToAnythingElse => true
+ case _ => false
+ }
+ }
+
+ override def hashCode() = 971
+ }
+
+ private object notEqualToAnything extends NotEqualToAnythingElse
+
+ private def checkNotEqualToRandomOtherThing(a: Any): Unit = {
+ assertFalse(a.equals(notEqualToAnything))
+ assertFalse(notEqualToAnything.equals(a))
+ }
+
+ protected def checkNotEqualObjects(a: Any, b: Any): Unit = {
+ assertNotEquals(a, b)
+ assertNotEquals(b, a)
+ // hashcode inequality isn't guaranteed, but
+ // as long as it happens to work it might
+ // detect a bug (if hashcodes are equal,
+ // check if it's due to a bug or correct
+ // before you remove this)
+ assertFalse(a.hashCode() == b.hashCode())
+ checkNotEqualToRandomOtherThing(a)
+ checkNotEqualToRandomOtherThing(b)
+ }
+
+ protected def checkEqualObjects(a: Any, b: Any): Unit = {
+ assertEquals(a, b)
+ assertEquals(b, a)
+ assertTrue(a.hashCode() == b.hashCode())
+ checkNotEqualToRandomOtherThing(a)
+ checkNotEqualToRandomOtherThing(b)
+ }
+
+ private val hexDigits = {
+ val a = new Array[Char](16)
+ var i = 0
+ for (c <- '0' to '9') {
+ a(i) = c
+ i += 1
+ }
+ for (c <- 'A' to 'F') {
+ a(i) = c
+ i += 1
+ }
+ a
+ }
+
+ private def encodeLegibleBinary(bytes: Array[Byte]): String = {
+ val sb = new java.lang.StringBuilder()
+ for (b <- bytes) {
+ if ((b >= 'a' && b <= 'z') ||
+ (b >= 'A' && b <= 'Z') ||
+ (b >= '0' && b <= '9') ||
+ b == '-' || b == ':' || b == '.' || b == '/' || b == ' ') {
+ sb.append('_')
+ sb.appendCodePoint(b.asInstanceOf[Char])
+ } else {
+ sb.appendCodePoint(hexDigits((b & 0xf0) >> 4))
+ sb.appendCodePoint(hexDigits(b & 0x0f))
+ }
+ }
+ sb.toString
+ }
+
+ private def decodeLegibleBinary(s: String): Array[Byte] = {
+ val a = new Array[Byte](s.length() / 2)
+ var i = 0
+ var j = 0
+ while (i < s.length()) {
+ val sub = s.substring(i, i + 2)
+ i += 2
+ if (sub.charAt(0) == '_') {
+ a(j) = charWrapper(sub.charAt(1)).byteValue
+ } else {
+ a(j) = Integer.parseInt(sub, 16).byteValue
+ }
+ j += 1
+ }
+ a
+ }
+
+ def outputStringLiteral(bytes: Array[Byte]): Unit = {
+ val hex = encodeLegibleBinary(bytes)
+ outputStringLiteral(hex)
+ }
+
+ @tailrec
+ final def outputStringLiteral(hex: String): Unit = {
+ if (hex.nonEmpty) {
+ val (head, tail) = hex.splitAt(80)
+ val plus = if (tail.isEmpty) "" else " +"
+ System.err.println("\"" + head + "\"" + plus)
+ outputStringLiteral(tail)
+ }
+ }
+
+ // origin() is not part of value equality but is serialized, so
+ // we check it separately
+ protected def checkEqualOrigins[T](a: T, b: T): Unit = (a, b) match {
+ case (obj1: ConfigObject, obj2: ConfigObject) =>
+ assertEquals(obj1.origin, obj2.origin)
+ for (e <- obj1.entrySet().asScala) {
+ checkEqualOrigins(e.getValue(), obj2.get(e.getKey()))
+ }
+ case (list1: ConfigList, list2: ConfigList) =>
+ assertEquals(list1.origin, list2.origin)
+ for ((v1, v2) <- list1.asScala zip list2.asScala) {
+ checkEqualOrigins(v1, v2)
+ }
+ case (value1: ConfigValue, value2: ConfigValue) =>
+ assertEquals(value1.origin, value2.origin)
+ case _ =>
+ }
+
+ def fakeOrigin() = {
+ SimpleConfigOrigin.newSimple("fake origin")
+ }
+
+ def includer = {
+ ConfigImpl.defaultIncluder
+ }
+
+ case class ParseTest(
+ jsonBehaviorUnexpected: Boolean,
+ whitespaceMatters: Boolean,
+ test: String
+ )
+ object ParseTest {
+ def apply(jsonBehaviorUnexpected: Boolean, test: String): ParseTest = {
+ ParseTest(jsonBehaviorUnexpected, false, test)
+ }
+ }
+ implicit def string2jsontest(test: String): ParseTest =
+ ParseTest(false, test)
+
+ // note: it's important to put {} or [] at the root if you
+ // want to test "invalidity reasons" other than "wrong root"
+ // spray-json throws so change to false
+ private val invalidJsonInvalidConf = List[ParseTest](
+ "{",
+ "}",
+ "[",
+ "]",
+ ",",
+ ParseTest(true, "10"), // value not in array or object
+ ParseTest(true, "\"foo\""), // value not in array or object
+ "\"", // single quote by itself
+ ParseTest(false, "[,]"), // array with just a comma in it
+ ParseTest(false, "[,,]"), // array with just two commas in it
+ ParseTest(false, "[1,2,,]"), // array with two trailing commas
+ ParseTest(false, "[,1,2]"), // array with initial comma
+ ParseTest(false, "{ , }"), // object with just a comma in it
+ ParseTest(false, "{ , , }"), // object with just two commas in it
+ "{ 1,2 }", // object with single values not key-value pair
+ ParseTest(false, "{ , \"foo\" : 10 }"), // object starts with comma
+ ParseTest(false, "{ \"foo\" : 10 ,, }"), // object has two trailing commas
+ " \"a\" : 10 ,, ", // two trailing commas for braceless root object
+ "{ \"foo\" : }", // no value in object
+ "{ : 10 }", // no key in object
+ ParseTest(false, " \"foo\" : "), // no value in object with no braces
+ ParseTest(false, " : 10 "), // no key in object with no braces
+ " \"foo\" : 10 } ", // close brace but no open
+ " \"foo\" : 10 [ ", // no-braces object with trailing gunk
+ "{ \"foo\" }", // no value or colon
+ "{ \"a\" : [ }", // [ is not a valid value
+ "{ \"foo\" : 10, true }", // non-key after comma
+ "{ foo \n bar : 10 }", // newline in the middle of the unquoted key
+ "[ 1, \\", // ends with backslash
+ // these two problems are ignored by the json tokenizer
+ "[:\"foo\", \"bar\"]", // colon in an array
+ "[\"foo\" : \"bar\"]", // colon in an array another way
+ "[ \"hello ]", // unterminated string
+ ParseTest(false, "{ \"foo\" , true }"), // comma instead of colon
+ ParseTest(
+ false,
+ "{ \"foo\" : true \"bar\" : false }"
+ ), // missing comma between fields
+ "[ 10, }]", // array with } as an element
+ "[ 10, {]", // array with { as an element
+ "{}x", // trailing invalid token after the root object
+ "[]x", // trailing invalid token after the root array
+ ParseTest(false, "{}{}"), // trailing token after the root object
+ ParseTest(false, "{}true"), // trailing token after the root object
+ ParseTest(false, "[]{}"), // trailing valid token after the root array
+ ParseTest(false, "[]true"), // trailing valid token after the root array
+ "[${]", // unclosed substitution
+ "[$]", // '$' by itself
+ "[$ ]", // '$' by itself with spaces after
+ "[${}]", // empty substitution (no path)
+ "[${?}]", // no path with ? substitution
+ ParseTest(false, true, "[${ ?foo}]"), // space before ? not allowed
+ """{ "a" : [1,2], "b" : y${a}z }""", // trying to interpolate an array in a string
+ """{ "a" : { "c" : 2 }, "b" : y${a}z }""", // trying to interpolate an object in a string
+ """{ "a" : ${a} }""", // simple cycle
+ """[ { "a" : 2, "b" : ${${a}} } ]""", // nested substitution
+ "[ = ]", // = is not a valid token in unquoted text
+ "[ + ]",
+ "[ # ]",
+ "[ ` ]",
+ "[ ^ ]",
+ "[ ? ]",
+ "[ ! ]",
+ "[ @ ]",
+ "[ * ]",
+ "[ & ]",
+ "[ \\ ]",
+ "+=",
+ "[ += ]",
+ "+= 10",
+ "10 +=",
+ "[ 10e+3e ]", // "+" not allowed in unquoted strings, and not a valid number
+ ParseTest(false, "[ \"foo\nbar\" ]"), // unescaped newline in quoted string
+ "[ # comment ]",
+ "${ #comment }",
+ "[ // comment ]",
+ "${ // comment }",
+ "{ include \"bar\" : 10 }", // include with a value after it
+ "{ include foo }", // include with unquoted string
+ "{ include : { \"a\" : 1 } }", // include used as unquoted key
+ "a=", // no value
+ "a:", // no value with colon
+ "a= ", // no value with whitespace after
+ "a.b=", // no value with path
+ "{ a= }", // no value inside braces
+ "{ a: }"
+ ) // no value with colon inside braces
+
+ // We'll automatically try each of these with whitespace modifications
+ // so no need to add every possible whitespace variation
+ protected val validJson = List[ParseTest](
+ "{}",
+ "[]",
+ """{ "foo" : "bar" }""",
+ """["foo", "bar"]""",
+ """{ "foo" : 42 }""",
+ "{ \"foo\"\n : 42 }", // newline after key
+ "{ \"foo\" : \n 42 }", // newline after colon
+ """[10, 11]""",
+ """[10,"foo"]""",
+ """{ "foo" : "bar", "baz" : "boo" }""",
+ """{ "foo" : { "bar" : "baz" }, "baz" : "boo" }""",
+ """{ "foo" : { "bar" : "baz", "woo" : "w00t" }, "baz" : "boo" }""",
+ """{ "foo" : [10,11,12], "baz" : "boo" }""",
+ """[{},{},{},{}]""",
+ """[[[[[[]]]]]]""",
+ """[[1], [1,2], [1,2,3], []]""", // nested multiple-valued array
+ """{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":42}}}}}}}}""",
+ "[ \"#comment\" ]", // quoted # comment
+ "[ \"//comment\" ]", // quoted // comment
+ // this long one is mostly to test rendering
+ """{ "foo" : { "bar" : "baz", "woo" : "w00t" }, "baz" : { "bar" : "baz", "woo" : [1,2,3,4], "w00t" : true, "a" : false, "b" : 3.14, "c" : null } }""",
+ "{}",
+ ParseTest(true, "[ 10e+3 ]")
+ )
+
+ // spray-json throws so change to false
+ private val validConfInvalidJson = List[ParseTest](
+ "", // empty document
+ " ", // empty document single space
+ "\n", // empty document single newline
+ " \n \n \n\n\n", // complicated empty document
+ "# foo", // just a comment
+ "# bar\n", // just a comment with a newline
+ "# foo\n//bar", // comment then another with no newline
+ """{ "foo" = 42 }""", // equals rather than colon
+ """{ "foo" = (42) }""", // value with round braces
+ """{ foo { "bar" : 42 } }""", // omit the colon for object value
+ """{ foo baz { "bar" : 42 } }""", // omit the colon with unquoted key with spaces
+ """ "foo" : 42 """, // omit braces on root object
+ """{ "foo" : bar }""", // no quotes on value
+ """{ "foo" : null bar 42 baz true 3.14 "hi" }""", // bunch of values to concat into a string
+ "{ foo : \"bar\" }", // no quotes on key
+ "{ foo : bar }", // no quotes on key or value
+ "{ foo.bar : bar }", // path expression in key
+ "{ foo.\"hello world\".baz : bar }", // partly-quoted path expression in key
+ "{ foo.bar \n : bar }", // newline after path expression in key
+ "{ foo bar : bar }", // whitespace in the key
+ "{ true : bar }", // key is a non-string token
+ ParseTest(true, """{ "foo" : "bar", "foo" : "bar2" }"""), // dup keys
+ ParseTest(false, "[ 1, 2, 3, ]"), // single trailing comma
+ ParseTest(false, "[1,2,3 , ]"), // single trailing comma with whitespace
+ ParseTest(false, "[1,2,3\n\n , \n]"), // single trailing comma with newlines
+ ParseTest(false, "[1,]"), // single trailing comma with one-element array
+ ParseTest(false, "{ \"foo\" : 10, }"), // extra trailing comma
+ ParseTest(false, "{ \"a\" : \"b\", }"), // single trailing comma in object
+ "{ a : b, }", // single trailing comma in object (unquoted strings)
+ "{ a : b \n , \n }", // single trailing comma in object with newlines
+ "a : b, c : d,", // single trailing comma in object with no root braces
+ "{ a : b\nc : d }", // skip comma if there's a newline
+ "a : b\nc : d", // skip comma if there's a newline and no root braces
+ "a : b\nc : d,", // skip one comma but still have one at the end
+ "[ foo ]", // not a known token in JSON
+ "[ t ]", // start of "true" but ends wrong in JSON
+ "[ tx ]",
+ "[ tr ]",
+ "[ trx ]",
+ "[ tru ]",
+ "[ trux ]",
+ "[ truex ]",
+ "[ 10x ]", // number token with trailing junk
+ "[ / ]", // unquoted string "slash"
+ "{ include \"foo\" }", // valid include
+ "{ include\n\"foo\" }", // include with just a newline separating from string
+ "{ include\"foo\" }", // include with no whitespace after it
+ "[ include ]", // include can be a string value in an array
+ "{ foo : include }", // include can be a field value also
+ "{ include \"foo\", \"a\" : \"b\" }", // valid include followed by comma and field
+ "{ foo include : 42 }", // valid to have a key not starting with include
+ "[ ${foo} ]",
+ "[ ${?foo} ]",
+ "[ ${\"foo\"} ]",
+ "[ ${foo.bar} ]",
+ "[ abc xyz ${foo.bar} qrs tuv ]", // value concatenation
+ "[ 1, 2, 3, blah ]",
+ "[ ${\"foo.bar\"} ]",
+ "{} # comment",
+ "{} // comment",
+ """{ "foo" #comment
+: 10 }""",
+ """{ "foo" // comment
+: 10 }""",
+ """{ "foo" : #comment
+ 10 }""",
+ """{ "foo" : // comment
+ 10 }""",
+ """{ "foo" : 10 #comment
+ }""",
+ """{ "foo" : 10 // comment
+ }""",
+ """[ 10, # comment
+ 11]""",
+ """[ 10, // comment
+ 11]""",
+ """[ 10 # comment
+, 11]""",
+ """[ 10 // comment
+, 11]""",
+ """{ /a/b/c : 10 }""", // key has a slash in it
+ ParseTest(false, true, "[${ foo.bar}]"), // substitution with leading spaces
+ ParseTest(
+ false,
+ true,
+ "[${foo.bar }]"
+ ), // substitution with trailing spaces
+ ParseTest(
+ false,
+ true,
+ "[${ \"foo.bar\"}]"
+ ), // substitution with leading spaces and quoted
+ ParseTest(
+ false,
+ true,
+ "[${\"foo.bar\" }]"
+ ), // substitution with trailing spaces and quoted
+ """[ ${"foo""bar"} ]""", // multiple strings in substitution
+ """[ ${foo "bar" baz} ]""", // multiple strings and whitespace in substitution
+ "[${true}]", // substitution with unquoted true token
+ "a = [], a += b", // += operator with previous init
+ "{ a = [], a += 10 }", // += in braces object with previous init
+ "a += b", // += operator without previous init
+ "{ a += 10 }", // += in braces object without previous init
+ "[ 10e3e3 ]", // two exponents. this should parse to a number plus string "e3"
+ "[ 1-e3 ]", // malformed number should end up as a string instead
+ "[ 1.0.0 ]", // two decimals, should end up as a string
+ "[ 1.0. ]"
+ ) // trailing decimal should end up as a string
+
+ protected val invalidJson = validConfInvalidJson ++ invalidJsonInvalidConf
+
+ protected val invalidConf = invalidJsonInvalidConf
+
+ // .conf is a superset of JSON so validJson just goes in here
+ protected val validConf = validConfInvalidJson ++ validJson
+
+ protected def addOffendingJsonToException[R](parserName: String, s: String)(
+ body: => R
+ ) = {
+ try {
+ body
+ } catch {
+ case t: Throwable =>
+ val tokens =
+ try {
+ "tokens: " + tokenizeAsList(s)
+ } catch {
+ case e: Throwable =>
+ "tokenizer failed: " + e.getMessage()
+ }
+ // don't use AssertionError because it seems to keep Eclipse
+ // from showing the causing exception in JUnit view for some reason
+ throw new Exception(
+ parserName + " parser did wrong thing on '" + s + "', " + tokens,
+ t
+ )
+ }
+ }
+
+ protected def whitespaceVariations(
+ tests: Seq[ParseTest],
+ validInJsonParser: Boolean
+ ): Seq[ParseTest] = {
+ val variations = List(
+ (s: String) => s, // identity
+ (s: String) => " " + s,
+ (s: String) => s + " ",
+ (s: String) => " " + s + " ",
+ (s: String) =>
+ s.replace(
+ " ",
+ ""
+ ), // this would break with whitespace in a key or value
+ (s: String) =>
+ s.replace(":", " : "), // could break with : in a key or value
+ (s: String) =>
+ s.replace(",", " , ") // could break with , in a key or value
+ )
+ tests flatMap { t =>
+ if (t.whitespaceMatters) {
+ Seq(t)
+ } else {
+ val withNonAscii =
+ if (t.test.contains(" "))
+ Seq(
+ ParseTest(validInJsonParser, t.test.replace(" ", "\u2003"))
+ ) // 2003 = em space, to test non-ascii whitespace
+ else
+ Seq()
+ withNonAscii ++ (for (v <- variations)
+ yield ParseTest(t.jsonBehaviorUnexpected, v(t.test)))
+ }
+ }
+ }
+
+ // it's important that these do NOT use the public API to create the
+ // instances, because we may be testing that the public API returns the
+ // right instance by comparing to these, so using public API here would
+ // make the test compare public API to itself.
+ protected def intValue(i: Int) = new ConfigInt(fakeOrigin(), i, null)
+ protected def longValue(l: Long) = new ConfigLong(fakeOrigin(), l, null)
+ protected def boolValue(b: Boolean) = new ConfigBoolean(fakeOrigin(), b)
+ protected def nullValue() = new ConfigNull(fakeOrigin())
+ protected def stringValue(s: String) =
+ new ConfigString.Quoted(fakeOrigin(), s)
+ protected def doubleValue(d: Double) =
+ new ConfigDouble(fakeOrigin(), d, null)
+
+ protected def parseObject(s: String) = {
+ parseConfig(s).root
+ }
+
+ protected def parseConfig(s: String) = {
+ val options = ConfigParseOptions.defaults
+ .setOriginDescription("test string")
+ .setSyntax(ConfigSyntax.CONF)
+ ConfigFactory.parseString(s, options).asInstanceOf[SimpleConfig]
+ }
+
+ protected def subst(ref: String, optional: Boolean): ConfigReference = {
+ val path = Path.newPath(ref)
+ new ConfigReference(
+ fakeOrigin(),
+ new SubstitutionExpression(path, optional)
+ )
+ }
+
+ protected def subst(ref: String): ConfigReference = {
+ subst(ref, false)
+ }
+
+ protected def substInString(
+ ref: String,
+ optional: Boolean
+ ): ConfigConcatenation = {
+ val path = Path.newPath(ref)
+ val pieces = List[AbstractConfigValue](
+ stringValue("start<"),
+ subst(ref, optional),
+ stringValue(">end")
+ )
+ new ConfigConcatenation(fakeOrigin(), pieces.asJava)
+ }
+
+ protected def substInString(ref: String): ConfigConcatenation = {
+ substInString(ref, false)
+ }
+
+ def tokenTrue = Tokens.newBoolean(fakeOrigin(), true)
+ def tokenFalse = Tokens.newBoolean(fakeOrigin(), false)
+ def tokenNull = Tokens.newNull(fakeOrigin())
+ def tokenUnquoted(s: String) = Tokens.newUnquotedText(fakeOrigin(), s)
+ def tokenString(s: String) =
+ Tokens.newString(fakeOrigin(), s, "\"" + s + "\"")
+ def tokenDouble(d: Double) = Tokens.newDouble(fakeOrigin(), d, "" + d)
+ def tokenInt(i: Int) = Tokens.newInt(fakeOrigin(), i, "" + i)
+ def tokenLong(l: Long) = Tokens.newLong(fakeOrigin(), l, l.toString())
+ def tokenLine(line: Int) = Tokens.newLine(fakeOrigin().withLineNumber(line))
+ def tokenCommentDoubleSlash(text: String) =
+ Tokens.newCommentDoubleSlash(fakeOrigin(), text)
+ def tokenCommentHash(text: String) =
+ Tokens.newCommentHash(fakeOrigin(), text)
+ def tokenWhitespace(text: String) =
+ Tokens.newIgnoredWhitespace(fakeOrigin(), text)
+
+ private def tokenMaybeOptionalSubstitution(
+ optional: Boolean,
+ expression: Token*
+ ) = {
+ val l = new java.util.ArrayList[Token]
+ for (t <- expression) {
+ l.add(t)
+ }
+ Tokens.newSubstitution(fakeOrigin(), optional, l)
+ }
+
+ def tokenSubstitution(expression: Token*) = {
+ tokenMaybeOptionalSubstitution(false, expression: _*)
+ }
+
+ def tokenOptionalSubstitution(expression: Token*) = {
+ tokenMaybeOptionalSubstitution(true, expression: _*)
+ }
+
+ // quoted string substitution (no interpretation of periods)
+ def tokenKeySubstitution(s: String) = tokenSubstitution(tokenString(s))
+
+ def tokenize(
+ origin: ConfigOrigin,
+ input: Reader
+ ): java.util.Iterator[Token] = {
+ Tokenizer.tokenize(origin, input, ConfigSyntax.CONF)
+ }
+
+ def tokenize(input: Reader): java.util.Iterator[Token] = {
+ tokenize(SimpleConfigOrigin.newSimple("anonymous Reader"), input)
+ }
+
+ def tokenize(s: String): java.util.Iterator[Token] = {
+ val reader = new StringReader(s)
+ val result = tokenize(reader)
+ // reader.close() // can't close until the iterator is traversed, so this tokenize() flavor is inherently broken
+ result
+ }
+
+ def tokenizeAsList(s: String) = {
+ tokenize(s).asScala.toList
+ }
+
+ def tokenizeAsString(s: String) = {
+ Tokenizer.render(tokenize(s))
+ }
+
+ def configNodeSimpleValue(value: Token) = {
+ new ConfigNodeSimpleValue(value)
+ }
+
+ def configNodeKey(path: String) = PathParser.parsePathNode(path)
+
+ def configNodeSingleToken(value: Token) = {
+ new ConfigNodeSingleToken(value: Token)
+ }
+
+ def configNodeObject(nodes: List[AbstractConfigNode]) = {
+ new ConfigNodeObject(nodes.asJavaCollection)
+ }
+
+ def configNodeArray(nodes: List[AbstractConfigNode]) = {
+ new ConfigNodeArray(nodes.asJavaCollection)
+ }
+
+ def configNodeConcatenation(nodes: List[AbstractConfigNode]) = {
+ new ConfigNodeConcatenation(nodes.asJavaCollection)
+ }
+
+ def nodeColon = new ConfigNodeSingleToken(Tokens.COLON)
+ def nodeSpace = new ConfigNodeSingleToken(tokenUnquoted(" "))
+ def nodeOpenBrace = new ConfigNodeSingleToken(Tokens.OPEN_CURLY)
+ def nodeCloseBrace = new ConfigNodeSingleToken(Tokens.CLOSE_CURLY)
+ def nodeOpenBracket = new ConfigNodeSingleToken(Tokens.OPEN_SQUARE)
+ def nodeCloseBracket = new ConfigNodeSingleToken(Tokens.CLOSE_SQUARE)
+ def nodeComma = new ConfigNodeSingleToken(Tokens.COMMA)
+ def nodeLine(line: Integer) = new ConfigNodeSingleToken(tokenLine(line))
+ def nodeWhitespace(whitespace: String) =
+ new ConfigNodeSingleToken(tokenWhitespace(whitespace))
+ def nodeKeyValuePair(key: ConfigNodePath, value: AbstractConfigNodeValue) = {
+ val nodes = List(key, nodeSpace, nodeColon, nodeSpace, value)
+ new ConfigNodeField(nodes.asJavaCollection)
+ }
+ def nodeInt(value: Integer) = new ConfigNodeSimpleValue(tokenInt(value))
+ def nodeString(value: String) = new ConfigNodeSimpleValue(tokenString(value))
+ def nodeLong(value: Long) = new ConfigNodeSimpleValue(tokenLong(value))
+ def nodeDouble(value: Double) = new ConfigNodeSimpleValue(tokenDouble(value))
+ def nodeTrue = new ConfigNodeSimpleValue(tokenTrue)
+ def nodeFalse = new ConfigNodeSimpleValue(tokenFalse)
+ def nodeCommentHash(text: String) =
+ new ConfigNodeComment(tokenCommentHash(text))
+ def nodeCommentDoubleSlash(text: String) =
+ new ConfigNodeComment(tokenCommentDoubleSlash(text))
+ def nodeUnquotedText(text: String) =
+ new ConfigNodeSimpleValue(tokenUnquoted(text))
+ def nodeNull = new ConfigNodeSimpleValue(tokenNull)
+ def nodeKeySubstitution(s: String) =
+ new ConfigNodeSimpleValue(tokenKeySubstitution(s))
+ def nodeOptionalSubstitution(expression: Token*) =
+ new ConfigNodeSimpleValue(tokenOptionalSubstitution(expression: _*))
+ def nodeSubstitution(expression: Token*) =
+ new ConfigNodeSimpleValue(tokenSubstitution(expression: _*))
+
+ // this is importantly NOT using Path.newPath, which relies on
+ // the parser; in the test suite we are often testing the parser,
+ // so we don't want to use the parser to build the expected result.
+ def path(elements: String*) = new Path(elements: _*)
+
+ protected class TestClassLoader(
+ parent: ClassLoader,
+ val additions: Map[String, URL]
+ ) extends ClassLoader(parent) {
+ override def findResources(name: String) = {
+ val other = super.findResources(name).asScala
+ additions
+ .get(name)
+ .map({ url => Iterator(url) ++ other })
+ .getOrElse(other)
+ .asJavaEnumeration
+ }
+ override def findResource(name: String) = {
+ additions.get(name).getOrElse(null)
+ }
+ }
+
+ protected def withContextClassLoader[T](
+ loader: ClassLoader
+ )(body: => T): T = {
+ val executor = Executors.newSingleThreadExecutor()
+ val f = executor.submit(new Callable[T] {
+ override def call(): T = {
+ val t = Thread.currentThread()
+ val old = t.getContextClassLoader()
+ t.setContextClassLoader(loader)
+ val result =
+ try {
+ body
+ } finally {
+ t.setContextClassLoader(old)
+ }
+ result
+ }
+ })
+ f.get
+ }
+
+ private def printIndented(indent: Int, s: String): Unit = {
+ for (i <- 0 to indent)
+ System.err.print(' ')
+ System.err.println(s)
+ }
+
+ protected def showDiff(
+ a: ConfigValue,
+ b: ConfigValue,
+ indent: Int = 0
+ ): Unit = {
+ if (a != b) {
+ if (a.valueType != b.valueType) {
+ printIndented(indent, "- " + a.valueType)
+ printIndented(indent, "+ " + b.valueType)
+ } else if (a.valueType == ConfigValueType.OBJECT) {
+ printIndented(indent, "OBJECT")
+ val aS = a.asInstanceOf[ConfigObject].asScala
+ val bS = b.asInstanceOf[ConfigObject].asScala
+ for (aKV <- aS) {
+ val bVOption = bS.get(aKV._1)
+ if (Some(aKV._2) != bVOption) {
+ printIndented(indent + 1, aKV._1)
+ if (bVOption.isDefined) {
+ showDiff(aKV._2, bVOption.get, indent + 2)
+ } else {
+ printIndented(indent + 2, "- " + aKV._2)
+ printIndented(indent + 2, "+ (missing)")
+ }
+ }
+ }
+ } else {
+ printIndented(indent, "- " + a)
+ printIndented(indent, "+ " + b)
+ }
+ }
+ }
+
+ protected def quoteJsonString(s: String): String =
+ ConfigImplUtil.renderJsonString(s)
+
+ sealed abstract class Problem(path: String, line: Int) {
+ def check(p: ConfigException.ValidationProblem): Unit = {
+ assertEquals("matching path", path, p.path)
+ assertEquals("matching line for " + path, line, p.origin.lineNumber)
+ }
+
+ protected def assertMessage(
+ p: ConfigException.ValidationProblem,
+ re: String
+ ): Unit = {
+ assertTrue(
+ "didn't get expected message for " + path + ": got '" + p.problem + "'",
+ p.problem.matches(re)
+ )
+ }
+ }
+
+ case class Missing(path: String, line: Int, expected: String)
+ extends Problem(path, line) {
+ override def check(p: ConfigException.ValidationProblem): Unit = {
+ super.check(p)
+ val re = "No setting.*" + path + ".*expecting.*" + expected + ".*"
+ assertMessage(p, re)
+ }
+ }
+
+ case class WrongType(path: String, line: Int, expected: String, got: String)
+ extends Problem(path, line) {
+ override def check(p: ConfigException.ValidationProblem): Unit = {
+ super.check(p)
+ val re =
+ "Wrong value type.*" + path + ".*expecting.*" + expected + ".*got.*" + got + ".*"
+ assertMessage(p, re)
+ }
+ }
+
+ case class WrongElementType(
+ path: String,
+ line: Int,
+ expected: String,
+ got: String
+ ) extends Problem(path, line) {
+ override def check(p: ConfigException.ValidationProblem): Unit = {
+ super.check(p)
+ val re =
+ "List at.*" + path + ".*wrong value type.*expecting.*" + expected + ".*got.*element of.*" + got + ".*"
+ assertMessage(p, re)
+ }
+ }
+
+ protected def checkValidationException(
+ e: ConfigException.ValidationFailed,
+ expecteds: Seq[Problem]
+ ): Unit = {
+ val problems =
+ e.problems.asScala.toIndexedSeq
+ .sortBy(_.path)
+ .sortBy(_.origin.lineNumber)
+
+ // for (problem <- problems)
+ // System.err.println(
+ // problem.origin.description + ": " + problem.path + ": " + problem.problem
+ // )
+
+ for ((problem, expected) <- problems zip expecteds) {
+ expected.check(problem)
+ }
+ assertEquals(
+ "found expected validation problems, got '" + problems + "' and expected '" + expecteds + "'",
+ expecteds.size,
+ problems.size
+ )
+ }
+}