diff --git a/README.md b/README.md index 954200e6..5b547deb 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,8 @@ At a high level, the process is as follows: ## Versions -Release [1.5.1](https://github.com/ekrich/sconfig/releases/tag/v1.5.1) - (2023-09-14)
+Release [1.6.0](https://github.com/ekrich/sconfig/releases/tag/v1.6.0) - (2023-12-28)
+Release [1.5.1](https://github.com/ekrich/sconfig/releases/tag/v1.5.1) - (2023-09-15)
Release [1.5.0](https://github.com/ekrich/sconfig/releases/tag/v1.5.0) - (2022-09-19)
Release [1.4.9](https://github.com/ekrich/sconfig/releases/tag/v1.4.9) - (2022-01-25)
Release [1.4.8](https://github.com/ekrich/sconfig/releases/tag/v1.4.8) - (2022-01-12)
diff --git a/build.sbt b/build.sbt index 99af12e3..91d20a1e 100644 --- a/build.sbt +++ b/build.sbt @@ -8,8 +8,8 @@ addCommandAlias( ).mkString(";", ";", "") ) -val prevVersion = "1.5.1" -val nextVersion = "1.5.2" +val prevVersion = "1.6.0" +val nextVersion = "1.7.0" // stable snapshot is not great for publish local def versionFmt(out: sbtdynver.GitDescribeOutput): String = { @@ -53,7 +53,7 @@ val scCompat = "2.11.0" val versionsBase = Seq(scala212, scala213) val versions = versionsBase :+ scala3 -ThisBuild / scalaVersion := scala3 +ThisBuild / scalaVersion := scala213 ThisBuild / crossScalaVersions := versions ThisBuild / versionScheme := Some("early-semver") ThisBuild / mimaFailOnNoPrevious := false @@ -116,14 +116,21 @@ lazy val sconfig = crossProject(JVMPlatform, NativePlatform, JSPlatform) scalacOptions ++= { if (isScala3.value) dotcOpts else scalacOpts }, - libraryDependencies += "org.scala-lang.modules" %%% "scala-collection-compat" % scCompat, - testOptions += Tests.Argument(TestFrameworks.JUnit, "-a", "-s", "-v") + libraryDependencies ++= Seq( + "org.scala-lang.modules" %%% "scala-collection-compat" % scCompat, + "org.json4s" %%% "json4s-native-core" % "4.0.7" % Test + ), + testOptions += Tests.Argument(TestFrameworks.JUnit, "-a", "-s", "-v"), + // env vars for tests + Test / envVars ++= Map( + "testList.0" -> "0", + "testList.1" -> "1", + "testClassesPath" -> (Test / classDirectory).value.getPath + ) ) .jvmSettings( crossScalaVersions := versions, libraryDependencies ++= Seq( - ("io.crashbox" %% "spray-json" % "1.3.5-7" % Test) - .cross(CrossVersion.for3Use2_13), "com.github.sbt" % "junit-interface" % "0.13.3" % Test // includes junit 4.13.2 ), @@ -141,12 +148,6 @@ lazy val sconfig = crossProject(JVMPlatform, NativePlatform, JSPlatform) Test / fork := true, run / fork := true, Test / run / fork := true, - // env vars for tests - Test / envVars ++= Map( - "testList.0" -> "0", - "testList.1" -> "1", - "testClassesPath" -> (Test / classDirectory).value.getPath - ), // uncomment for debugging // Test / javaOptions += "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005", // mima settings @@ -156,14 +157,21 @@ lazy val sconfig = crossProject(JVMPlatform, NativePlatform, JSPlatform) .nativeConfigure(_.enablePlugins(ScalaNativeJUnitPlugin)) .nativeSettings( crossScalaVersions := versions, - nativeConfig ~= (_.withLinkStubs(true)), + nativeConfig ~= ( + _.withLinkStubs(true) + .withEmbedResources(true) + ), logLevel := Level.Info, // Info or Debug libraryDependencies += "org.ekrich" %%% "sjavatime" % javaTime % "provided" ) .jsConfigure(_.enablePlugins(ScalaJSJUnitPlugin)) .jsSettings( crossScalaVersions := versions, - libraryDependencies += "org.ekrich" %%% "sjavatime" % javaTime % "provided" + libraryDependencies ++= Seq( + "org.ekrich" %%% "sjavatime" % javaTime % "provided", + ("org.scala-js" %%% "scalajs-weakreferences" % "1.0.0") + .cross(CrossVersion.for3Use2_13) + ) ) lazy val `scalafix-rules` = (project in file("scalafix/rules")) diff --git a/docs/SCALA-NATIVE.md b/docs/SCALA-NATIVE.md index b548a6a6..49ee1198 100644 --- a/docs/SCALA-NATIVE.md +++ b/docs/SCALA-NATIVE.md @@ -45,6 +45,34 @@ val maxCol = conf.getInt("maxColumn") val isGit = conf.getBoolean("project.git") ``` +## Using Reader - StringReader example + +Both JS and Native now support `java.io.Reader` which allows using `sconfig` +to parse all supported formats by passing a filename with extension. See the +following examples from the shared `ConfigFactoryTest` file. + +```scala +val filename = "/test01.properties" +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) + +val config = ConfigFactory.parseReader( + test01Reader, + ConfigParseOptions.defaults + .setSyntaxFromFilename(filename) +) +val specialChars = config.getString("fromProps.specialChars") +``` + ### How to read a HOCON configuation file into a String for Scala Native In order to read the configuration file into a `String` you need to know the relative diff --git a/project/plugins.sbt b/project/plugins.sbt index 7c4a39c1..af3527f9 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -3,7 +3,7 @@ resolvers ++= Resolver.sonatypeOssRepos("snapshots") // versions val crossVer = "1.3.2" -val scalaJSVersion = "1.14.0" +val scalaJSVersion = "1.15.0" val scalaNativeVersion = "0.4.16" val scalafix = "0.11.1" diff --git a/sconfig/js/src/main/scala/org/ekrich/config/PlatformConfigFactory.scala b/sconfig/js/src/main/scala/org/ekrich/config/PlatformConfigFactory.scala new file mode 100644 index 00000000..625e757f --- /dev/null +++ b/sconfig/js/src/main/scala/org/ekrich/config/PlatformConfigFactory.scala @@ -0,0 +1,6 @@ +package org.ekrich.config + +/** + * [[ConfigFactory]] methods for Scala.js platform + */ +abstract class PlatformConfigFactory extends ConfigFactoryCommon {} diff --git a/sconfig/js/src/main/scala/PlatformClassLoader.scala b/sconfig/js/src/main/scala/org/ekrich/config/impl/PlatformClassLoader.scala similarity index 72% rename from sconfig/js/src/main/scala/PlatformClassLoader.scala rename to sconfig/js/src/main/scala/org/ekrich/config/impl/PlatformClassLoader.scala index be94b370..2e107f20 100644 --- a/sconfig/js/src/main/scala/PlatformClassLoader.scala +++ b/sconfig/js/src/main/scala/org/ekrich/config/impl/PlatformClassLoader.scala @@ -6,6 +6,6 @@ import java.{util => ju} /** * To workaround missing implementations */ -class PlatformClassLoader(cl: ClassLoader) extends ClassLoaderLike { +class PlatformClassLoader(cl: ClassLoader) extends TraitClassLoader { def getResources(name: String): ju.Enumeration[URL] = ??? } diff --git a/sconfig/native/src/main/scala/PlatformThread.scala b/sconfig/js/src/main/scala/org/ekrich/config/impl/PlatformThread.scala similarity index 69% rename from sconfig/native/src/main/scala/PlatformThread.scala rename to sconfig/js/src/main/scala/org/ekrich/config/impl/PlatformThread.scala index 76656e5b..930074da 100644 --- a/sconfig/native/src/main/scala/PlatformThread.scala +++ b/sconfig/js/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/js/src/main/scala/PlatformUri.scala b/sconfig/js/src/main/scala/org/ekrich/config/impl/PlatformUri.scala similarity index 74% rename from sconfig/js/src/main/scala/PlatformUri.scala rename to sconfig/js/src/main/scala/org/ekrich/config/impl/PlatformUri.scala index 962ad810..41bab2a9 100644 --- a/sconfig/js/src/main/scala/PlatformUri.scala +++ b/sconfig/js/src/main/scala/org/ekrich/config/impl/PlatformUri.scala @@ -5,6 +5,6 @@ import java.net.{URI, URL} /** * To workaround missing implementations */ -class PlatformUri(uri: URI) extends UriLike { +class PlatformUri(uri: URI) extends TraitUri { def toURL(): URL = ??? } diff --git a/sconfig/jvm-native/src/main/scala/org/ekrich/config/ConfigFactoryJvmNative.scala b/sconfig/jvm-native/src/main/scala/org/ekrich/config/ConfigFactoryJvmNative.scala new file mode 100644 index 00000000..7e2b9551 --- /dev/null +++ b/sconfig/jvm-native/src/main/scala/org/ekrich/config/ConfigFactoryJvmNative.scala @@ -0,0 +1,6 @@ +package org.ekrich.config + +/** + * [[ConfigFactory]] methods common to JVM and Native + */ +abstract class ConfigFactoryJvmNative extends ConfigFactoryCommon {} diff --git a/sconfig/jvm-native/src/main/scala/PlatformClassLoader.scala b/sconfig/jvm-native/src/main/scala/org/ekrich/config/impl/PlatformClassLoader.scala similarity index 74% rename from sconfig/jvm-native/src/main/scala/PlatformClassLoader.scala rename to sconfig/jvm-native/src/main/scala/org/ekrich/config/impl/PlatformClassLoader.scala index ae8d4843..408af4d4 100644 --- a/sconfig/jvm-native/src/main/scala/PlatformClassLoader.scala +++ b/sconfig/jvm-native/src/main/scala/org/ekrich/config/impl/PlatformClassLoader.scala @@ -6,6 +6,6 @@ import java.{util => ju} /** * To workaround missing implementations */ -class PlatformClassLoader(cl: ClassLoader) extends ClassLoaderLike { +class PlatformClassLoader(cl: ClassLoader) extends TraitClassLoader { def getResources(name: String): ju.Enumeration[URL] = cl.getResources(name) } diff --git a/sconfig/jvm-native/src/main/scala/PlatformUri.scala b/sconfig/jvm-native/src/main/scala/org/ekrich/config/impl/PlatformUri.scala similarity index 75% rename from sconfig/jvm-native/src/main/scala/PlatformUri.scala rename to sconfig/jvm-native/src/main/scala/org/ekrich/config/impl/PlatformUri.scala index b5b52063..1f6bd03b 100644 --- a/sconfig/jvm-native/src/main/scala/PlatformUri.scala +++ b/sconfig/jvm-native/src/main/scala/org/ekrich/config/impl/PlatformUri.scala @@ -5,6 +5,6 @@ import java.net.{URI, URL} /** * To workaround missing implementations */ -class PlatformUri(uri: URI) extends UriLike { +class PlatformUri(uri: URI) extends TraitUri { def toURL(): URL = uri.toURL() } diff --git a/sconfig/jvm-native/src/test/scala/org/ekrich/config/impl/ConfigFactoryJvmNativeTest.scala b/sconfig/jvm-native/src/test/scala/org/ekrich/config/impl/ConfigFactoryJvmNativeTest.scala new file mode 100644 index 00000000..f67fea42 --- /dev/null +++ b/sconfig/jvm-native/src/test/scala/org/ekrich/config/impl/ConfigFactoryJvmNativeTest.scala @@ -0,0 +1,8 @@ +package org.ekrich.config.impl + +// import org.junit.Assert._ +// import org.junit.Test + +class ConfigFactoryJvmNativeTest { + // Empty for now - Scala.js has parody with Scala Native for now +} diff --git a/sconfig/jvm/src/main/scala/org/ekrich/config/PlatformConfigFactory.scala b/sconfig/jvm/src/main/scala/org/ekrich/config/PlatformConfigFactory.scala new file mode 100644 index 00000000..a52cce52 --- /dev/null +++ b/sconfig/jvm/src/main/scala/org/ekrich/config/PlatformConfigFactory.scala @@ -0,0 +1,6 @@ +package org.ekrich.config + +/** + * [[ConfigFactory]] methods for Scala JVM platform + */ +abstract class PlatformConfigFactory extends ConfigFactoryJvmNative {} diff --git a/sconfig/jvm/src/main/scala/PlatformThread.scala b/sconfig/jvm/src/main/scala/org/ekrich/config/impl/PlatformThread.scala similarity index 76% rename from sconfig/jvm/src/main/scala/PlatformThread.scala rename to sconfig/jvm/src/main/scala/org/ekrich/config/impl/PlatformThread.scala index 110dcc84..6b89f76d 100644 --- a/sconfig/jvm/src/main/scala/PlatformThread.scala +++ b/sconfig/jvm/src/main/scala/org/ekrich/config/impl/PlatformThread.scala @@ -3,6 +3,6 @@ package org.ekrich.config.impl /** * To workaround missing implementations in Scala.js and Scala Native */ -class PlatformThread(thread: Thread) extends ThreadLike { +class PlatformThread(thread: Thread) extends TraitThread { def getContextClassLoader(): ClassLoader = thread.getContextClassLoader() } diff --git a/sconfig/jvm/src/test/scala/org/ekrich/config/impl/ConfigDocumentTest.scala b/sconfig/jvm/src/test/scala/org/ekrich/config/impl/ConfigDocumentFactoryTest.scala similarity index 99% rename from sconfig/jvm/src/test/scala/org/ekrich/config/impl/ConfigDocumentTest.scala rename to sconfig/jvm/src/test/scala/org/ekrich/config/impl/ConfigDocumentFactoryTest.scala index b895c5b2..b849aa33 100644 --- a/sconfig/jvm/src/test/scala/org/ekrich/config/impl/ConfigDocumentTest.scala +++ b/sconfig/jvm/src/test/scala/org/ekrich/config/impl/ConfigDocumentFactoryTest.scala @@ -10,7 +10,7 @@ import org.junit.Test import scala.jdk.CollectionConverters._ import FileUtils._ -class ConfigDocumentTest extends TestUtils { +class ConfigDocumentFactoryTest extends TestUtils { private def configDocumentReplaceJsonTest( origText: String, finalText: String, diff --git a/sconfig/jvm/src/test/scala/org/ekrich/config/impl/ConfigTest.scala b/sconfig/jvm/src/test/scala/org/ekrich/config/impl/ConfigFactoryJvmTest.scala similarity index 52% rename from sconfig/jvm/src/test/scala/org/ekrich/config/impl/ConfigTest.scala rename to sconfig/jvm/src/test/scala/org/ekrich/config/impl/ConfigFactoryJvmTest.scala index f71d737e..ce11b2eb 100644 --- a/sconfig/jvm/src/test/scala/org/ekrich/config/impl/ConfigTest.scala +++ b/sconfig/jvm/src/test/scala/org/ekrich/config/impl/ConfigFactoryJvmTest.scala @@ -1,18 +1,6 @@ -/** - * Copyright (C) 2011 Typesafe Inc. - */ package org.ekrich.config.impl import java.time.temporal.ChronoUnit - -import org.junit.Assert._ -import org.junit._ - -import org.ekrich.config._ -import org.ekrich.config.ConfigResolveOptions - -import scala.jdk.CollectionConverters._ - import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit.{ DAYS, @@ -23,575 +11,17 @@ import java.util.concurrent.TimeUnit.{ NANOSECONDS, SECONDS } -import FileUtils._ - -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) - } +import scala.jdk.CollectionConverters._ - @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) - } +import org.junit.Assert._ +import org.junit.Test - @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")) +import FileUtils._ - val ep = intercept[ConfigException.WrongType] { - conf.getInt("tooPositive") - } - assertTrue(ep.getMessage.contains("range")) - } +import org.ekrich.config._ +class ConfigFactoryJvmTest extends TestUtils { @Test def test01Getting(): Unit = { val conf = ConfigFactory.load("test01") @@ -1410,203 +840,4 @@ class ConfigTest extends TestUtils { checkSerializable(resolved) } } - - @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/jvm/src/test/scala/org/ekrich/config/impl/ConfigSubstitutionTest.scala b/sconfig/jvm/src/test/scala/org/ekrich/config/impl/ConfigSubstitutionTest.scala index 7516179b..a0e4e2e0 100644 --- a/sconfig/jvm/src/test/scala/org/ekrich/config/impl/ConfigSubstitutionTest.scala +++ b/sconfig/jvm/src/test/scala/org/ekrich/config/impl/ConfigSubstitutionTest.scala @@ -6,7 +6,6 @@ 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._ @@ -55,729 +54,6 @@ class ConfigSubstitutionTest extends TestUtils { """) } - @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( """ @@ -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 + ) + } +}