blob: 4adb616d510decb977a95ae7aea26ecd76c415f9 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.ignite.internal.configuration.hocon;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.typesafe.config.ConfigFactory;
import com.typesafe.config.ConfigRenderOptions;
import com.typesafe.config.ConfigValue;
import org.apache.ignite.configuration.annotation.Config;
import org.apache.ignite.configuration.annotation.ConfigurationRoot;
import org.apache.ignite.configuration.annotation.NamedConfigValue;
import org.apache.ignite.configuration.annotation.Value;
import org.apache.ignite.internal.configuration.ConfigurationRegistry;
import org.apache.ignite.internal.configuration.storage.TestConfigurationStorage;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.function.Executable;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.apache.ignite.configuration.annotation.ConfigurationType.LOCAL;
import static org.apache.ignite.internal.configuration.hocon.HoconConverter.hoconSource;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
/**
* Tests for the {@link HoconConverter}.
*/
public class HoconConverterTest {
/** */
@ConfigurationRoot(rootName = "root", type = LOCAL)
public static class HoconRootConfigurationSchema {
/** */
@NamedConfigValue(syntheticKeyName = "a")
public HoconArraysConfigurationSchema arraysList;
/** */
@NamedConfigValue(syntheticKeyName = "p")
public HoconPrimitivesConfigurationSchema primitivesList;
}
/**
* Configuration schema for testing the support of arrays of primitives.
*/
@Config
public static class HoconArraysConfigurationSchema {
/** */
@Value(hasDefault = true)
public boolean[] booleans = {false};
/** */
@Value(hasDefault = true)
public byte[] bytes = {0};
/** */
@Value(hasDefault = true)
public short[] shorts = {0};
/** */
@Value(hasDefault = true)
public int[] ints = {0};
/** */
@Value(hasDefault = true)
public long[] longs = {0L};
/** */
@Value(hasDefault = true)
public char[] chars = {0};
/** */
@Value(hasDefault = true)
public float[] floats = {0};
/** */
@Value(hasDefault = true)
public double[] doubles = {0};
/** */
@Value(hasDefault = true)
public String[] strings = {""};
}
/**
* Configuration schema for testing the support of primitives.
*/
@Config
public static class HoconPrimitivesConfigurationSchema {
/** */
@Value(hasDefault = true)
public boolean booleanVal = false;
/** */
@Value(hasDefault = true)
public byte byteVal = 0;
/** */
@Value(hasDefault = true)
public short shortVal = 0;
/** */
@Value(hasDefault = true)
public int intVal = 0;
/** */
@Value(hasDefault = true)
public long longVal = 0L;
/** */
@Value(hasDefault = true)
public char charVal = 0;
/** */
@Value(hasDefault = true)
public float floatVal = 0;
/** */
@Value(hasDefault = true)
public double doubleVal = 0;
/** */
@Value(hasDefault = true)
public String stringVal = "";
}
/** */
private static ConfigurationRegistry registry;
/** */
private static HoconRootConfiguration configuration;
/** */
@BeforeAll
public static void beforeAll() {
registry = new ConfigurationRegistry(
List.of(HoconRootConfiguration.KEY),
Map.of(),
new TestConfigurationStorage(LOCAL),
List.of()
);
registry.start();
configuration = registry.getConfiguration(HoconRootConfiguration.KEY);
}
/** */
@AfterAll
public static void after() {
registry.stop();
registry = null;
configuration = null;
}
/** */
@BeforeEach
public void before() throws Exception {
configuration.change(cfg -> cfg
.changePrimitivesList(list -> list.namedListKeys().forEach(list::delete))
.changeArraysList(list -> list.namedListKeys().forEach(list::delete))
).get(1, SECONDS);
}
/** */
@Test
public void toHoconBasic() {
assertEquals("root{arraysList=[],primitivesList=[]}", asHoconStr(List.of()));
assertEquals("arraysList=[],primitivesList=[]", asHoconStr(List.of("root")));
assertEquals("[]", asHoconStr(List.of("root", "arraysList")));
assertThrowsIllegalArgException(
() -> HoconConverter.represent(registry, List.of("doot")),
"Configuration 'doot' is not found"
);
assertThrowsIllegalArgException(
() -> HoconConverter.represent(registry, List.of("root", "x")),
"Configuration 'root.x' is not found"
);
assertEquals("null", asHoconStr(List.of("root", "primitivesList", "foo")));
}
/**
* Tests that the {@code HoconConverter} supports serialization of Strings and primitives.
*/
@Test
public void testHoconPrimitivesSerialization() throws Exception {
configuration.change(cfg -> cfg
.changePrimitivesList(primitivesList -> primitivesList
.create("name", primitives -> {})
)
).get(1, SECONDS);
var basePath = List.of("root", "primitivesList", "name");
assertEquals(
"booleanVal=false,byteVal=0,charVal=\"\\u0000\",doubleVal=0.0,floatVal=0,intVal=0,longVal=0,shortVal=0,stringVal=\"\"",
asHoconStr(basePath)
);
assertEquals("false", asHoconStr(basePath, "booleanVal"));
assertEquals("0", asHoconStr(basePath, "byteVal"));
assertEquals("0", asHoconStr(basePath, "shortVal"));
assertEquals("0", asHoconStr(basePath, "intVal"));
assertEquals("0", asHoconStr(basePath, "longVal"));
assertEquals("\"\\u0000\"", asHoconStr(basePath, "charVal"));
assertEquals("0", asHoconStr(basePath, "floatVal"));
assertEquals("0.0", asHoconStr(basePath, "doubleVal"));
assertEquals("\"\"", asHoconStr(basePath, "stringVal"));
}
/**
* Tests that the {@code HoconConverter} supports serialization of arrays of Strings and primitives.
*/
@Test
public void testHoconArraysSerialization() throws Exception {
configuration.change(cfg -> cfg
.changeArraysList(arraysList -> arraysList
.create("name", arrays -> {})
)
).get(1, SECONDS);
var basePath = List.of("root", "arraysList", "name");
assertEquals(
"booleans=[false],bytes=[0],chars=[\"\\u0000\"],doubles=[0.0],floats=[0],ints=[0],longs=[0],shorts=[0],strings=[\"\"]",
asHoconStr(basePath)
);
assertEquals("[false]", asHoconStr(basePath, "booleans"));
assertEquals("[0]", asHoconStr(basePath, "bytes"));
assertEquals("[0]", asHoconStr(basePath, "shorts"));
assertEquals("[0]", asHoconStr(basePath, "ints"));
assertEquals("[0]", asHoconStr(basePath, "longs"));
assertEquals("[\"\\u0000\"]", asHoconStr(basePath, "chars"));
assertEquals("[0]", asHoconStr(basePath, "floats"));
assertEquals("[0.0]", asHoconStr(basePath, "doubles"));
assertEquals("[\"\"]", asHoconStr(basePath, "strings"));
}
/**
* Retrieves the HOCON configuration located at the given path.
*/
private String asHoconStr(List<String> basePath, String... path) {
List<String> fullPath = Stream.concat(basePath.stream(), Arrays.stream(path)).collect(Collectors.toList());
ConfigValue hoconCfg = HoconConverter.represent(registry, fullPath);
return hoconCfg.render(ConfigRenderOptions.concise().setJson(false));
}
/** */
@Test
public void fromHoconBasic() {
// Wrong names:
assertThrowsIllegalArgException(
() -> change("doot : {}"),
"'doot' configuration root doesn't exist"
);
assertThrowsIllegalArgException(
() -> change("root.foo : {}"),
"'root' configuration doesn't have the 'foo' sub-configuration"
);
assertThrowsIllegalArgException(
() -> change("root.arraysList.name.x = 1"),
"'root.arraysList.name' configuration doesn't have the 'x' sub-configuration"
);
// Wrong node types:
assertThrowsIllegalArgException(
() -> change("root = foo"),
"'root' is expected to be a composite configuration node, not a single value"
);
assertThrowsIllegalArgException(
() -> change("root.arraysList = foo"),
"'root.arraysList' is expected to be a composite configuration node, not a single value"
);
assertThrowsIllegalArgException(
() -> change("root.arraysList.name = foo"),
"'root.arraysList.name' is expected to be a composite configuration node, not a single value"
);
assertThrowsIllegalArgException(
() -> change("root.arraysList.name.ints = {}"),
"'int[]' is expected as a type for the 'root.arraysList.name.ints' configuration value"
);
// Wrong ordered named list syntax:
assertThrowsIllegalArgException(
() -> change("root.arraysList = [1]"),
"'root.arraysList[0]' is expected to be a composite configuration node, not a single value"
);
assertThrowsIllegalArgException(
() -> change("root.arraysList = [{}]"),
"'root.arraysList[0].a' configuration value is mandatory and must be a String"
);
}
/**
* Tests that the {@code HoconConverter} supports deserialization of Strings and primitives.
*/
@Test
public void testHoconPrimitivesDeserialization() throws Throwable {
change("root.primitivesList = [{p = name}]");
HoconPrimitivesConfiguration primitives = configuration.primitivesList().get("name");
assertNotNull(primitives);
change("root.primitivesList.name.booleanVal = true");
assertThat(primitives.booleanVal().value(), is(true));
change("root.primitivesList.name.byteVal = 123");
assertThat(primitives.byteVal().value(), is((byte)123));
change("root.primitivesList.name.shortVal = 12345");
assertThat(primitives.shortVal().value(), is((short)12345));
change("root.primitivesList.name.intVal = 12345");
assertThat(primitives.intVal().value(), is(12345));
change("root.primitivesList.name.longVal = 12345678900");
assertThat(primitives.longVal().value(), is(12345678900L));
change("root.primitivesList.name.charVal = p");
assertThat(primitives.charVal().value(), is('p'));
change("root.primitivesList.name.floatVal = 2.5");
assertThat(primitives.floatVal().value(), is(2.5f));
change("root.primitivesList.name.doubleVal = 2.5");
assertThat(primitives.doubleVal().value(), is(2.5d));
change("root.primitivesList.name.stringVal = foo");
assertThat(primitives.stringVal().value(), is("foo"));
}
/**
* Tests deserialization errors that can happen during the deserialization of primitives.
*/
@Test
public void testInvalidHoconPrimitivesDeserialization() {
assertThrowsIllegalArgException(
() -> change("root.primitivesList.name.booleanVal = \"true\""),
"'boolean' is expected as a type for the 'root.primitivesList.name.booleanVal' configuration value"
);
assertThrowsIllegalArgException(
() -> change("root.primitivesList.name.byteVal = 290"),
"Value '290' of 'root.primitivesList.name.byteVal' is out of its declared bounds"
);
assertThrowsIllegalArgException(
() -> change("root.primitivesList.name.byteVal = false"),
"'byte' is expected as a type for the 'root.primitivesList.name.byteVal' configuration value"
);
assertThrowsIllegalArgException(
() -> change("root.primitivesList.name.shortVal = 12345678900"),
"Value '12345678900' of 'root.primitivesList.name.shortVal' is out of its declared bounds"
);
assertThrowsIllegalArgException(
() -> change("root.primitivesList.name.shortVal = false"),
"'short' is expected as a type for the 'root.primitivesList.name.shortVal' configuration value"
);
assertThrowsIllegalArgException(
() -> change("root.primitivesList.name.intVal = 12345678900"),
"Value '12345678900' of 'root.primitivesList.name.intVal' is out of its declared bounds"
);
assertThrowsIllegalArgException(
() -> change("root.primitivesList.name.intVal = false"),
"'int' is expected as a type for the 'root.primitivesList.name.intVal' configuration value"
);
assertThrowsIllegalArgException(
() -> change("root.primitivesList.name.longVal = false"),
"'long' is expected as a type for the 'root.primitivesList.name.longVal' configuration value"
);
assertThrowsIllegalArgException(
() -> change("root.primitivesList.name.charVal = 10"),
"'char' is expected as a type for the 'root.primitivesList.name.charVal' configuration value"
);
assertThrowsIllegalArgException(
() -> change("root.primitivesList.name.floatVal = false"),
"'float' is expected as a type for the 'root.primitivesList.name.floatVal' configuration value"
);
assertThrowsIllegalArgException(
() -> change("root.primitivesList.name.doubleVal = []"),
"'double' is expected as a type for the 'root.primitivesList.name.doubleVal' configuration value"
);
assertThrowsIllegalArgException(
() -> change("root.primitivesList.name.stringVal = 10"),
"'String' is expected as a type for the 'root.primitivesList.name.stringVal' configuration value"
);
}
/**
* Tests that the {@code HoconConverter} supports deserialization of arrays of Strings and primitives.
*/
@Test
public void testHoconArraysDeserialization() throws Throwable {
change("root.arraysList = [{a = name}]");
HoconArraysConfiguration arrays = configuration.arraysList().get("name");
assertNotNull(arrays);
change("root.arraysList.name.booleans = [true]");
assertThat(arrays.booleans().value(), is(new boolean[] {true}));
change("root.arraysList.name.bytes = [123]");
assertThat(arrays.bytes().value(), is(new byte[] {123}));
change("root.arraysList.name.shorts = [123]");
assertThat(arrays.shorts().value(), is(new short[] {123}));
change("root.arraysList.name.ints = [12345]");
assertThat(arrays.ints().value(), is(new int[] {12345}));
change("root.arraysList.name.longs = [12345678900]");
assertThat(arrays.longs().value(), is(new long[] {12345678900L}));
change("root.arraysList.name.chars = [p]");
assertThat(arrays.chars().value(), is(new char[] {'p'}));
change("root.arraysList.name.floats = [2.5]");
assertThat(arrays.floats().value(), is(new float[] {2.5f}));
change("root.arraysList.name.doubles = [2.5]");
assertThat(arrays.doubles().value(), is(new double[] {2.5d}));
change("root.arraysList.name.strings = [foo]");
assertThat(arrays.strings().value(), is(new String[] {"foo"}));
}
/**
* Tests deserialization errors that can happen during the deserialization of arrays of primitives.
*/
@Test
public void testInvalidHoconArraysDeserialization() {
assertThrowsIllegalArgException(
() -> change("root.arraysList.name.booleans = true"),
"'boolean[]' is expected as a type for the 'root.arraysList.name.booleans' configuration value"
);
assertThrowsIllegalArgException(
() -> change("root.arraysList.name.booleans = [{}]"),
"'boolean' is expected as a type for the 'root.arraysList.name.booleans[0]' configuration value"
);
assertThrowsIllegalArgException(
() -> change("root.arraysList.name.bytes = [123, 290]"),
"Value '290' of 'root.arraysList.name.bytes[1]' is out of its declared bounds"
);
assertThrowsIllegalArgException(
() -> change("root.arraysList.name.bytes = false"),
"'byte[]' is expected as a type for the 'root.arraysList.name.bytes' configuration value"
);
assertThrowsIllegalArgException(
() -> change("root.arraysList.name.shorts = [12345678900]"),
"Value '12345678900' of 'root.arraysList.name.shorts[0]' is out of its declared bounds"
);
assertThrowsIllegalArgException(
() -> change("root.arraysList.name.shorts = [123, false]"),
"'short' is expected as a type for the 'root.arraysList.name.shorts[1]' configuration value"
);
assertThrowsIllegalArgException(
() -> change("root.arraysList.name.ints = [5, 12345678900]"),
"Value '12345678900' of 'root.arraysList.name.ints[1]' is out of its declared bounds"
);
assertThrowsIllegalArgException(
() -> change("root.arraysList.name.ints = false"),
"'int[]' is expected as a type for the 'root.arraysList.name.ints' configuration value"
);
assertThrowsIllegalArgException(
() -> change("root.arraysList.name.longs = [foo]"),
"'long' is expected as a type for the 'root.arraysList.name.longs[0]' configuration value"
);
assertThrowsIllegalArgException(
() -> change("root.arraysList.name.chars = 10"),
"'char[]' is expected as a type for the 'root.arraysList.name.chars' configuration value"
);
assertThrowsIllegalArgException(
() -> change("root.arraysList.name.chars = [abc]"),
"'char' is expected as a type for the 'root.arraysList.name.chars[0]' configuration value"
);
assertThrowsIllegalArgException(
() -> change("root.arraysList.name.floats = [1.2, foo]"),
"'float' is expected as a type for the 'root.arraysList.name.floats[1]' configuration value"
);
assertThrowsIllegalArgException(
() -> change("root.arraysList.name.doubles = foo"),
"'double[]' is expected as a type for the 'root.arraysList.name.doubles' configuration value"
);
assertThrowsIllegalArgException(
() -> change("root.arraysList.name.strings = 10"),
"'String[]' is expected as a type for the 'root.arraysList.name.strings' configuration value"
);
}
/**
* Updates the configuration using the provided HOCON string.
*/
private void change(String hocon) throws Throwable {
try {
registry.change(hoconSource(ConfigFactory.parseString(hocon).root())).get(1, SECONDS);
}
catch (ExecutionException e) {
throw e.getCause();
}
}
/** */
private static void assertThrowsIllegalArgException(Executable executable, String msg) {
IllegalArgumentException e = assertThrows(IllegalArgumentException.class, executable);
assertThat(e.getMessage(), containsString(msg));
}
}