blob: 7c334618a5f528496eceb60a47b27de627d75aee [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.tree;
import static java.lang.String.format;
import static org.apache.ignite.configuration.annotation.ConfigurationType.LOCAL;
import static org.apache.ignite.internal.testframework.matchers.CompletableFutureMatcher.willBe;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.aMapWithSize;
import static org.hamcrest.Matchers.anEmptyMap;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasEntry;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
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.ConfigurationTreeGenerator;
import org.apache.ignite.internal.configuration.TestConfigurationChanger;
import org.apache.ignite.internal.configuration.storage.Data;
import org.apache.ignite.internal.configuration.storage.TestConfigurationStorage;
import org.apache.ignite.internal.configuration.util.ConfigurationUtil;
import org.apache.ignite.internal.configuration.validation.TestConfigurationValidator;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/** Test for named list nodes. */
public class NamedListNodeTest {
/** Root that has a single named list. */
@ConfigurationRoot(rootName = "a")
public static class FirstConfigurationSchema {
@NamedConfigValue
public SecondConfigurationSchema second;
}
/** Named list element node that contains another named list. */
@Config
public static class SecondConfigurationSchema {
/** Every named list element node must have at least one configuration field that is not named list. */
@Value(hasDefault = true)
public String str = "foo";
@NamedConfigValue
public ThirdConfigurationSchema third;
}
/** Simple configuration schema. */
@Config
public static class ThirdConfigurationSchema {
/** Integer value. */
@Value(hasDefault = true)
public int intVal = 1;
}
/** Runtime implementations generator. */
private static ConfigurationTreeGenerator cgen;
/** Test configuration storage. */
private TestConfigurationStorage storage;
/** Test configuration changer. */
private TestConfigurationChanger changer;
/** Instantiates {@link #cgen}. */
@BeforeAll
public static void beforeAll() {
cgen = new ConfigurationTreeGenerator(FirstConfiguration.KEY);
}
/** Nullifies {@link #cgen} to prevent memory leak from having runtime ClassLoader accessible from GC root. */
@AfterAll
public static void afterAll() {
cgen.close();
}
/**
* Before each.
*/
@BeforeEach
public void before() {
storage = new TestConfigurationStorage(LOCAL);
changer = new TestConfigurationChanger(
List.of(FirstConfiguration.KEY),
storage,
cgen,
new TestConfigurationValidator()
);
changer.start();
}
/**
* After each.
*/
@AfterEach
public void after() throws Exception {
changer.stop();
}
/**
* Tests that there are no unnecessary {@code <order>} values in the storage after all basic named list operations.
*
* @throws Exception If failed.
*/
@Test
public void storageData() throws Exception {
// Manually instantiate configuration instance.
var a = (FirstConfiguration) cgen.instantiateCfg(FirstConfiguration.KEY, changer);
// Create values on several layers at the same time. They all should have <order> = 0.
a.second().change(b -> b.create("X", x -> x.changeThird(xb -> xb.create("Z0", z0 -> {})))).get();
UUID x0Id = ((NamedListNode<?>) a.second().value()).internalId("X");
UUID z0Id = ((NamedListNode<?>) a.second().get("X").third().value()).internalId("Z0");
CompletableFuture<Map<String, ? extends Serializable>> storageValues = storage.readDataOnRecovery().thenApply(Data::values);
assertThat(
storageValues,
willBe(Matchers.<Map<String, ? extends Serializable>>allOf(
aMapWithSize(8),
hasEntry(format("a.second.<ids>.X"), x0Id),
hasEntry(format("a.second.%s.str", x0Id), "foo"),
hasEntry(format("a.second.%s.<order>", x0Id), 0),
hasEntry(format("a.second.%s.<name>", x0Id), "X"),
hasEntry(format("a.second.%s.third.<ids>.Z0", x0Id), z0Id),
hasEntry(format("a.second.%s.third.%s.intVal", x0Id, z0Id), 1),
hasEntry(format("a.second.%s.third.%s.<order>", x0Id, z0Id), 0),
hasEntry(format("a.second.%s.third.%s.<name>", x0Id, z0Id), "Z0")
))
);
SecondConfiguration x = a.second().get("X");
// Append new key. It should have <order> = 1.
x.third().change(xb -> xb.create("Z5", z5 -> {})).get();
UUID z5Id = ((NamedListNode<?>) a.second().get("X").third().value()).internalId("Z5");
storageValues = storage.readDataOnRecovery().thenApply(Data::values);
assertThat(
storageValues,
willBe(Matchers.<Map<String, ? extends Serializable>>allOf(
aMapWithSize(12),
hasEntry(format("a.second.<ids>.X"), x0Id),
hasEntry(format("a.second.%s.str", x0Id), "foo"),
hasEntry(format("a.second.%s.<order>", x0Id), 0),
hasEntry(format("a.second.%s.<name>", x0Id), "X"),
hasEntry(format("a.second.%s.third.<ids>.Z0", x0Id), z0Id),
hasEntry(format("a.second.%s.third.<ids>.Z5", x0Id), z5Id),
hasEntry(format("a.second.%s.third.%s.intVal", x0Id, z0Id), 1),
hasEntry(format("a.second.%s.third.%s.<order>", x0Id, z0Id), 0),
hasEntry(format("a.second.%s.third.%s.<name>", x0Id, z0Id), "Z0"),
hasEntry(format("a.second.%s.third.%s.intVal", x0Id, z5Id), 1),
hasEntry(format("a.second.%s.third.%s.<order>", x0Id, z5Id), 1),
hasEntry(format("a.second.%s.third.%s.<name>", x0Id, z5Id), "Z5")
))
);
// Insert new key somewhere in the middle. Index of Z5 should be updated to 2.
x.third().change(xb -> xb.create(1, "Z2", z2 -> {})).get();
UUID z2Id = ((NamedListNode<?>) a.second().get("X").third().value()).internalId("Z2");
storageValues = storage.readDataOnRecovery().thenApply(Data::values);
assertThat(
storageValues,
willBe(Matchers.<Map<String, ? extends Serializable>>allOf(
aMapWithSize(16),
hasEntry(format("a.second.<ids>.X"), x0Id),
hasEntry(format("a.second.%s.str", x0Id), "foo"),
hasEntry(format("a.second.%s.<order>", x0Id), 0),
hasEntry(format("a.second.%s.<name>", x0Id), "X"),
hasEntry(format("a.second.%s.third.<ids>.Z0", x0Id), z0Id),
hasEntry(format("a.second.%s.third.<ids>.Z2", x0Id), z2Id),
hasEntry(format("a.second.%s.third.<ids>.Z5", x0Id), z5Id),
hasEntry(format("a.second.%s.third.%s.intVal", x0Id, z0Id), 1),
hasEntry(format("a.second.%s.third.%s.<order>", x0Id, z0Id), 0),
hasEntry(format("a.second.%s.third.%s.<name>", x0Id, z0Id), "Z0"),
hasEntry(format("a.second.%s.third.%s.intVal", x0Id, z2Id), 1),
hasEntry(format("a.second.%s.third.%s.<order>", x0Id, z2Id), 1),
hasEntry(format("a.second.%s.third.%s.<name>", x0Id, z2Id), "Z2"),
hasEntry(format("a.second.%s.third.%s.intVal", x0Id, z5Id), 1),
hasEntry(format("a.second.%s.third.%s.<order>", x0Id, z5Id), 2),
hasEntry(format("a.second.%s.third.%s.<name>", x0Id, z5Id), "Z5")
))
);
// Insert new key somewhere in the middle. Indexes of Z3 and Z5 should be updated to 2 and 3.
x.third().change(xb -> xb.createAfter("Z2", "Z3", z3 -> {})).get();
UUID z3Id = ((NamedListNode<?>) a.second().get("X").third().value()).internalId("Z3");
storageValues = storage.readDataOnRecovery().thenApply(Data::values);
assertThat(
storageValues,
willBe(Matchers.<Map<String, ? extends Serializable>>allOf(
aMapWithSize(20),
hasEntry(format("a.second.<ids>.X"), x0Id),
hasEntry(format("a.second.%s.str", x0Id), "foo"),
hasEntry(format("a.second.%s.<order>", x0Id), 0),
hasEntry(format("a.second.%s.<name>", x0Id), "X"),
hasEntry(format("a.second.%s.third.<ids>.Z0", x0Id), z0Id),
hasEntry(format("a.second.%s.third.<ids>.Z2", x0Id), z2Id),
hasEntry(format("a.second.%s.third.<ids>.Z3", x0Id), z3Id),
hasEntry(format("a.second.%s.third.<ids>.Z5", x0Id), z5Id),
hasEntry(format("a.second.%s.third.%s.intVal", x0Id, z0Id), 1),
hasEntry(format("a.second.%s.third.%s.<order>", x0Id, z0Id), 0),
hasEntry(format("a.second.%s.third.%s.<name>", x0Id, z0Id), "Z0"),
hasEntry(format("a.second.%s.third.%s.intVal", x0Id, z2Id), 1),
hasEntry(format("a.second.%s.third.%s.<order>", x0Id, z2Id), 1),
hasEntry(format("a.second.%s.third.%s.<name>", x0Id, z2Id), "Z2"),
hasEntry(format("a.second.%s.third.%s.intVal", x0Id, z3Id), 1),
hasEntry(format("a.second.%s.third.%s.<order>", x0Id, z3Id), 2),
hasEntry(format("a.second.%s.third.%s.<name>", x0Id, z3Id), "Z3"),
hasEntry(format("a.second.%s.third.%s.intVal", x0Id, z5Id), 1),
hasEntry(format("a.second.%s.third.%s.<order>", x0Id, z5Id), 3),
hasEntry(format("a.second.%s.third.%s.<name>", x0Id, z5Id), "Z5")
))
);
// Delete keys from the middle. Indexes of Z3 should be updated to 1.
x.third().change(xb -> xb.delete("Z2").delete("Z5")).get();
storageValues = storage.readDataOnRecovery().thenApply(Data::values);
assertThat(
storageValues,
willBe(Matchers.<Map<String, ? extends Serializable>>allOf(
aMapWithSize(12),
hasEntry(format("a.second.<ids>.X"), x0Id),
hasEntry(format("a.second.%s.str", x0Id), "foo"),
hasEntry(format("a.second.%s.<order>", x0Id), 0),
hasEntry(format("a.second.%s.<name>", x0Id), "X"),
hasEntry(format("a.second.%s.third.<ids>.Z0", x0Id), z0Id),
hasEntry(format("a.second.%s.third.<ids>.Z3", x0Id), z3Id),
hasEntry(format("a.second.%s.third.%s.intVal", x0Id, z0Id), 1),
hasEntry(format("a.second.%s.third.%s.<order>", x0Id, z0Id), 0),
hasEntry(format("a.second.%s.third.%s.<name>", x0Id, z0Id), "Z0"),
hasEntry(format("a.second.%s.third.%s.intVal", x0Id, z3Id), 1),
hasEntry(format("a.second.%s.third.%s.<order>", x0Id, z3Id), 1),
hasEntry(format("a.second.%s.third.%s.<name>", x0Id, z3Id), "Z3")
))
);
// Delete keys from the middle. Indexes of Z3 should be updated to 1.
x.third().change(xb -> xb.rename("Z0", "Z1")).get();
storageValues = storage.readDataOnRecovery().thenApply(Data::values);
assertThat(
storageValues,
willBe(Matchers.<Map<String, ? extends Serializable>>allOf(
aMapWithSize(12),
hasEntry(format("a.second.<ids>.X"), x0Id),
hasEntry(format("a.second.%s.str", x0Id), "foo"),
hasEntry(format("a.second.%s.<order>", x0Id), 0),
hasEntry(format("a.second.%s.<name>", x0Id), "X"),
hasEntry(format("a.second.%s.third.<ids>.Z1", x0Id), z0Id),
hasEntry(format("a.second.%s.third.<ids>.Z3", x0Id), z3Id),
hasEntry(format("a.second.%s.third.%s.intVal", x0Id, z0Id), 1),
hasEntry(format("a.second.%s.third.%s.<order>", x0Id, z0Id), 0),
hasEntry(format("a.second.%s.third.%s.<name>", x0Id, z0Id), "Z1"),
hasEntry(format("a.second.%s.third.%s.intVal", x0Id, z3Id), 1),
hasEntry(format("a.second.%s.third.%s.<order>", x0Id, z3Id), 1),
hasEntry(format("a.second.%s.third.%s.<name>", x0Id, z3Id), "Z3")
))
);
// Delete values on several layers simultaneously. Storage must be empty after that.
a.second().change(b -> b.delete("X")).get();
assertThat(storage.readDataOnRecovery().thenApply(Data::values), willBe(anEmptyMap()));
}
/** Tests exceptions described in methods signatures. */
@Test
public void errors() throws Exception {
var b = new NamedListNode<>("name", NamedListNodeTest::instantiateChildNode, null);
b.create("X", x -> {
}).create("Y", y -> {
});
// NPE in keys.
assertThrows(NullPointerException.class, () -> b.create(null, z -> {
}));
assertThrows(NullPointerException.class, () -> b.createOrUpdate(null, z -> {
}));
assertThrows(NullPointerException.class, () -> b.create(0, null, z -> {
}));
assertThrows(NullPointerException.class, () -> b.rename(null, "Z"));
assertThrows(NullPointerException.class, () -> b.rename("X", null));
assertThrows(NullPointerException.class, () -> b.delete(null));
// NPE in closures.
assertThrows(NullPointerException.class, () -> b.create("Z", null));
assertThrows(NullPointerException.class, () -> b.createOrUpdate("Z", null));
assertThrows(NullPointerException.class, () -> b.create(0, "Z", null));
// Already existing keys.
assertThrows(IllegalArgumentException.class, () -> b.create("X", x -> {
}));
assertThrows(IllegalArgumentException.class, () -> b.create(0, "X", x -> {
}));
assertThrows(IllegalArgumentException.class, () -> b.rename("X", "Y"));
// Wrong indexes.
assertThrows(IndexOutOfBoundsException.class, () -> b.create(-1, "Z", z -> {
}));
assertThrows(IndexOutOfBoundsException.class, () -> b.create(3, "Z", z -> {
}));
// Nonexisting key.
assertThrows(IllegalArgumentException.class, () -> b.rename("A", "Z"));
// Operations after delete.
b.delete("X");
assertThrows(IllegalArgumentException.class, () -> b.create("X", x -> {
}));
assertThrows(IllegalArgumentException.class, () -> b.create(0, "X", x -> {
}));
assertThrows(IllegalArgumentException.class, () -> b.rename("X", "Z"));
assertThrows(IllegalArgumentException.class, () -> b.rename("Y", "X"));
// Deletion of nonexistent elements doesn't break anything.
b.delete("X");
b.delete("Y");
}
/**
* Tests the {@link NamedListNode#update} method.
*/
@Test
public void testUpdate() {
var list = new NamedListNode<SecondChange>("name", NamedListNodeTest::instantiateChildNode, null);
list.create("foo", ch -> ch.changeStr("bar"));
assertThat(list.get("foo").str(), is(equalTo("bar")));
list.update("foo", ch -> ch.changeStr("baz"));
assertThat(list.get("foo").str(), is(equalTo("baz")));
list.delete("foo");
// updating a removed key should throw
assertThrows(IllegalArgumentException.class, () -> list.update("foo", ch -> {}));
}
@Test
public void testUpdateErrors() {
var list = new NamedListNode<SecondChange>("name", NamedListNodeTest::instantiateChildNode, null);
assertThrows(NullPointerException.class, () -> list.update(null, ch -> {}));
assertThrows(NullPointerException.class, () -> list.update("foo", null));
// updating a non existent key should throw
assertThrows(IllegalArgumentException.class, () -> list.update("wrong", ch -> {}));
}
@Test
void testCreateAfterErrors() {
var list = new NamedListNode<>("name", NamedListNodeTest::instantiateChildNode, null);
list
.create("X", x -> {})
.create("Y", y -> {});
assertThrows(NullPointerException.class, () -> list.createAfter(null, "Z", z -> {}));
assertThrows(NullPointerException.class, () -> list.createAfter("X", null, z -> {}));
assertThrows(NullPointerException.class, () -> list.createAfter("X", "Z", null));
// inserting an existing key should throw
assertThrows(IllegalArgumentException.class, () -> list.createAfter("X", "Y", y -> {}));
// inserting after a missing key should throw
assertThrows(IllegalArgumentException.class, () -> list.createAfter("A", "Z", z -> {}));
// inserting after a removed key should throw
assertThrows(IllegalArgumentException.class, () -> list.delete("X").createAfter("X", "foo", foo -> {}));
}
@Test
public void makeImmutable() {
var list = new NamedListNode<>("name", NamedListNodeTest::instantiateChildNode, null);
list.makeImmutable();
assertThrows(AssertionError.class, () -> list.construct("elem", ConfigurationUtil.EMPTY_CFG_SRC, true));
assertThrows(AssertionError.class, () -> list.setInternalId("elem", UUID.randomUUID()));
assertThrows(AssertionError.class, () -> list.reorderKeys(List.of()));
assertThrows(AssertionError.class, () -> list.create("elem", elem -> {}));
assertThrows(AssertionError.class, () -> list.create(0, "elem", elem -> {}));
assertThrows(AssertionError.class, () -> list.createAfter("foo", "elem", elem -> {}));
assertThrows(AssertionError.class, () -> list.createOrUpdate("elem", elem -> {}));
assertThrows(AssertionError.class, () -> list.delete("elem"));
assertThrows(AssertionError.class, () -> list.forceDelete("elem"));
// Copy is always mutable.
list.copy().create("elem", elem -> {});
}
private static InnerNode instantiateChildNode() {
return cgen.instantiateNode(SecondConfigurationSchema.class);
}
}