blob: eb28d0415e762d7c20d54e3167196c1c1fd916ea [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 java.io.Serializable;
import java.util.List;
import java.util.Map;
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.TestConfigurationChanger;
import org.apache.ignite.internal.configuration.asm.ConfigurationAsmGenerator;
import org.apache.ignite.internal.configuration.storage.TestConfigurationStorage;
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;
import static java.lang.String.format;
import static org.apache.ignite.configuration.annotation.ConfigurationType.LOCAL;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.aMapWithSize;
import static org.hamcrest.Matchers.anEmptyMap;
import static org.hamcrest.Matchers.hasEntry;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertThrows;
/** Test for named list nodes. */
public class NamedListNodeTest {
/** Root that has a single named list. */
@ConfigurationRoot(rootName = "a")
public static class AConfigurationSchema {
/** */
@NamedConfigValue
public BConfigurationSchema b;
}
/** Named list element node that in inself contains another named list. */
@Config
public static class BConfigurationSchema {
/** Every named list element node must have at least one configuration field that is not named list. */
@Value(hasDefault = true)
public String c = "foo";
@NamedConfigValue
public BConfigurationSchema b;
}
/** Runtime implementations generator. */
private static ConfigurationAsmGenerator cgen;
/** Test configuration storage. */
private TestConfigurationStorage storage;
/** Test configuration changer. */
private TestConfigurationChanger changer;
/** Instantiates {@link #cgen}. */
@BeforeAll
public static void beforeAll() {
cgen = new ConfigurationAsmGenerator();
}
/** Nullifies {@link #cgen} to prevent memory leak from having runtime ClassLoader accessible from GC root. */
@AfterAll
public static void afterAll() {
cgen = null;
}
/** */
@BeforeEach
public void before() {
storage = new TestConfigurationStorage(LOCAL);
changer = new TestConfigurationChanger(cgen, List.of(AConfiguration.KEY), Map.of(), storage, List.of());
changer.start();
}
/** */
@AfterEach
public void after() {
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 = (AConfiguration)cgen.instantiateCfg(AConfiguration.KEY, changer);
// Create values on several layers at the same time. They all should have <order> = 0.
a.b().change(b -> b.create("X", x -> x.changeB(xb -> xb.create("Z0", z0 -> {})))).get();
String xId = ((NamedListNode<?>)a.b().value()).internalId("X");
String z0Id = ((NamedListNode<?>)a.b().get("X").b().value()).internalId("Z0");
Map<String, Serializable> storageValues = storage.readAll().values();
assertThat(
storageValues,
is(Matchers.<Map<String, Serializable>>allOf(
aMapWithSize(6),
hasEntry(format("a.b.%s.c", xId), "foo"),
hasEntry(format("a.b.%s.<order>", xId), 0),
hasEntry(format("a.b.%s.<name>", xId), "X"),
hasEntry(format("a.b.%s.b.%s.c", xId, z0Id), "foo"),
hasEntry(format("a.b.%s.b.%s.<order>", xId, z0Id), 0),
hasEntry(format("a.b.%s.b.%s.<name>", xId, z0Id), "Z0")
))
);
BConfiguration x = a.b().get("X");
// Append new key. It should have <order> = 1.
x.b().change(xb -> xb.create("Z5", z5 -> {})).get();
String z5Id = ((NamedListNode<?>)a.b().get("X").b().value()).internalId("Z5");
storageValues = storage.readAll().values();
assertThat(
storageValues,
is(Matchers.<Map<String, Serializable>>allOf(
aMapWithSize(9),
hasEntry(format("a.b.%s.c", xId), "foo"),
hasEntry(format("a.b.%s.<order>", xId), 0),
hasEntry(format("a.b.%s.<name>", xId), "X"),
hasEntry(format("a.b.%s.b.%s.c", xId, z0Id), "foo"),
hasEntry(format("a.b.%s.b.%s.<order>", xId, z0Id), 0),
hasEntry(format("a.b.%s.b.%s.<name>", xId, z0Id), "Z0"),
hasEntry(format("a.b.%s.b.%s.c", xId, z5Id), "foo"),
hasEntry(format("a.b.%s.b.%s.<order>", xId, z5Id), 1),
hasEntry(format("a.b.%s.b.%s.<name>", xId, z5Id), "Z5")
))
);
// Insert new key somewhere in the middle. Index of Z5 should be updated to 2.
x.b().change(xb -> xb.create(1, "Z2", z2 -> {})).get();
String z2Id = ((NamedListNode<?>)a.b().get("X").b().value()).internalId("Z2");
storageValues = storage.readAll().values();
assertThat(
storageValues,
is(Matchers.<Map<String, Serializable>>allOf(
aMapWithSize(12),
hasEntry(format("a.b.%s.c", xId), "foo"),
hasEntry(format("a.b.%s.<order>", xId), 0),
hasEntry(format("a.b.%s.<name>", xId), "X"),
hasEntry(format("a.b.%s.b.%s.c", xId, z0Id), "foo"),
hasEntry(format("a.b.%s.b.%s.<order>", xId, z0Id), 0),
hasEntry(format("a.b.%s.b.%s.<name>", xId, z0Id), "Z0"),
hasEntry(format("a.b.%s.b.%s.c", xId, z2Id), "foo"),
hasEntry(format("a.b.%s.b.%s.<order>", xId, z2Id), 1),
hasEntry(format("a.b.%s.b.%s.<name>", xId, z2Id), "Z2"),
hasEntry(format("a.b.%s.b.%s.c", xId, z5Id), "foo"),
hasEntry(format("a.b.%s.b.%s.<order>", xId, z5Id), 2),
hasEntry(format("a.b.%s.b.%s.<name>", xId, z5Id), "Z5")
))
);
// Insert new key somewhere in the middle. Indexes of Z3 and Z5 should be updated to 2 and 3.
x.b().change(xb -> xb.createAfter("Z2", "Z3", z3 -> {})).get();
String z3Id = ((NamedListNode<?>)a.b().get("X").b().value()).internalId("Z3");
storageValues = storage.readAll().values();
assertThat(
storageValues,
is(Matchers.<Map<String, Serializable>>allOf(
aMapWithSize(15),
hasEntry(format("a.b.%s.c", xId), "foo"),
hasEntry(format("a.b.%s.<order>", xId), 0),
hasEntry(format("a.b.%s.<name>", xId), "X"),
hasEntry(format("a.b.%s.b.%s.c", xId, z0Id), "foo"),
hasEntry(format("a.b.%s.b.%s.<order>", xId, z0Id), 0),
hasEntry(format("a.b.%s.b.%s.<name>", xId, z0Id), "Z0"),
hasEntry(format("a.b.%s.b.%s.c", xId, z2Id), "foo"),
hasEntry(format("a.b.%s.b.%s.<order>", xId, z2Id), 1),
hasEntry(format("a.b.%s.b.%s.<name>", xId, z2Id), "Z2"),
hasEntry(format("a.b.%s.b.%s.c", xId, z3Id), "foo"),
hasEntry(format("a.b.%s.b.%s.<order>", xId, z3Id), 2),
hasEntry(format("a.b.%s.b.%s.<name>", xId, z3Id), "Z3"),
hasEntry(format("a.b.%s.b.%s.c", xId, z5Id), "foo"),
hasEntry(format("a.b.%s.b.%s.<order>", xId, z5Id), 3),
hasEntry(format("a.b.%s.b.%s.<name>", xId, z5Id), "Z5")
))
);
// Delete keys from the middle. Indexes of Z3 should be updated to 1.
x.b().change(xb -> xb.delete("Z2").delete("Z5")).get();
storageValues = storage.readAll().values();
assertThat(
storageValues,
is(Matchers.<Map<String, Serializable>>allOf(
aMapWithSize(9),
hasEntry(format("a.b.%s.c", xId), "foo"),
hasEntry(format("a.b.%s.<order>", xId), 0),
hasEntry(format("a.b.%s.<name>", xId), "X"),
hasEntry(format("a.b.%s.b.%s.c", xId, z0Id), "foo"),
hasEntry(format("a.b.%s.b.%s.<order>", xId, z0Id), 0),
hasEntry(format("a.b.%s.b.%s.<name>", xId, z0Id), "Z0"),
hasEntry(format("a.b.%s.b.%s.c", xId, z3Id), "foo"),
hasEntry(format("a.b.%s.b.%s.<order>", xId, z3Id), 1),
hasEntry(format("a.b.%s.b.%s.<name>", xId, z3Id), "Z3")
))
);
// Delete keys from the middle. Indexes of Z3 should be updated to 1.
x.b().change(xb -> xb.rename("Z0", "Z1")).get();
storageValues = storage.readAll().values();
assertThat(
storageValues,
is(Matchers.<Map<String, Serializable>>allOf(
aMapWithSize(9),
hasEntry(format("a.b.%s.c", xId), "foo"),
hasEntry(format("a.b.%s.<order>", xId), 0),
hasEntry(format("a.b.%s.<name>", xId), "X"),
hasEntry(format("a.b.%s.b.%s.c", xId, z0Id), "foo"),
hasEntry(format("a.b.%s.b.%s.<order>", xId, z0Id), 0),
hasEntry(format("a.b.%s.b.%s.<name>", xId, z0Id), "Z1"),
hasEntry(format("a.b.%s.b.%s.c", xId, z3Id), "foo"),
hasEntry(format("a.b.%s.b.%s.<order>", xId, z3Id), 1),
hasEntry(format("a.b.%s.b.%s.<name>", xId, z3Id), "Z3")
))
);
// Delete values on several layers simultaneously. Storage must be empty after that.
a.b().change(b -> b.delete("X")).get();
assertThat(storage.readAll().values(), is(anEmptyMap()));
}
/** Tests exceptions described in methods signatures. */
@Test
public void errors() throws Exception {
var b = new NamedListNode<>("name", () -> cgen.instantiateNode(BConfigurationSchema.class));
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.createAfter(null, "Z", z -> {}));
assertThrows(NullPointerException.class, () -> b.createAfter("X", 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));
assertThrows(NullPointerException.class, () -> b.createAfter("X", "Z", null));
// Already existing keys.
assertThrows(IllegalArgumentException.class, () -> b.create("X", x -> {}));
assertThrows(IllegalArgumentException.class, () -> b.create(0, "X", x -> {}));
assertThrows(IllegalArgumentException.class, () -> b.createAfter("X", "Y", y -> {}));
assertThrows(IllegalArgumentException.class, () -> b.rename("X", "Y"));
// Nonexistent preceding key.
assertThrows(IllegalArgumentException.class, () -> b.createAfter("A", "Z", z -> {}));
// 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");
}
}