ReportNodeTest.java
/**
* Copyright (c) 2024, RTE (http://www.rte-france.com)
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
* SPDX-License-Identifier: MPL-2.0
*/
package com.powsybl.commons.report;
import com.powsybl.commons.PowsyblException;
import com.powsybl.commons.test.AbstractSerDeTest;
import com.powsybl.commons.test.ComparisonUtils;
import com.powsybl.commons.test.PowsyblCoreTestReportResourceBundle;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Locale;
import java.util.Optional;
import static com.powsybl.commons.test.PowsyblCoreTestReportResourceBundle.TEST_BASE_NAME;
import static org.junit.jupiter.api.Assertions.*;
/**
* @author Florian Dupuy {@literal <florian.dupuy at rte-france.com>}
*/
class ReportNodeTest extends AbstractSerDeTest {
private static final String ALL_VALUES_MESSAGE_FORMATTED = """
Root message
doubleUntyped: 4.3
doubleTyped: 4.4
floatUntyped: -1.5
floatTyped: 0.6
intUntyped: 4
intTyped: -2
longUntyped: 5
longTyped: -3
booleanUntyped: true
booleanTyped: false
stringUntyped: value
stringTyped: filename
severity: INFO""";
@Test
void testValues() throws IOException {
ReportNode root = ReportNode.newRootReportNode()
.withResourceBundles(TEST_BASE_NAME)
.withMessageTemplate("rootTemplate")
.withUntypedValue("doubleUntyped", 4.3)
.withTypedValue("doubleTyped", 4.4, TypedValue.ACTIVE_POWER)
.withUntypedValue("floatUntyped", -1.5f)
.withTypedValue("floatTyped", 0.6f, TypedValue.IMPEDANCE)
.withUntypedValue("intUntyped", 4)
.withTypedValue("intTyped", -2, "count")
.withUntypedValue("longUntyped", 5L)
.withTypedValue("longTyped", -3L, "count")
.withUntypedValue("booleanUntyped", true)
.withTypedValue("booleanTyped", false, "protected")
.withUntypedValue("stringUntyped", "value")
.withTypedValue("stringTyped", "filename", TypedValue.FILENAME)
.withSeverity(TypedValue.INFO_SEVERITY)
.build();
assertEquals(ALL_VALUES_MESSAGE_FORMATTED, root.getMessage());
ReportNode child = root.newReportNode()
.withMessageTemplate("child")
.withSeverity("Very important custom severity")
.add();
assertEquals("Child message with parent value filename and own severity 'Very important custom severity'", child.getMessage());
assertEquals(1, root.getChildren().size());
roundTripTest(root, ReportNodeSerializer::write, ReportNodeDeserializer::read, "/testValuesReportNode.json");
}
@Test
void testPostponedValues() throws IOException {
ReportNode root = ReportNode.newRootReportNode()
.withResourceBundles(TEST_BASE_NAME)
.withMessageTemplate("rootTemplate")
.build();
ReportNode child = root.newReportNode()
.withMessageTemplate("child")
.withSeverity("Overridden custom severity")
.add();
assertEquals("Child message with parent value ${stringTyped} and own severity 'Overridden custom severity'", child.getMessage());
// postponed values added
root.addUntypedValue("doubleUntyped", 4.3)
.addTypedValue("doubleTyped", 4.4, TypedValue.ACTIVE_POWER)
.addUntypedValue("floatUntyped", -1.5f)
.addTypedValue("floatTyped", 0.6f, TypedValue.IMPEDANCE)
.addUntypedValue("intUntyped", 4)
.addTypedValue("intTyped", -2, "count")
.addUntypedValue("longUntyped", 5L)
.addTypedValue("longTyped", -3L, "count")
.addUntypedValue("booleanUntyped", true)
.addTypedValue("booleanTyped", false, "protected")
.addUntypedValue("stringUntyped", "value")
.addTypedValue("stringTyped", "filename", TypedValue.FILENAME)
.addSeverity(TypedValue.INFO_SEVERITY);
assertEquals(ALL_VALUES_MESSAGE_FORMATTED, root.getMessage());
// child reportNode also inherits the postponed added values
assertEquals("Child message with parent value filename and own severity 'Overridden custom severity'", child.getMessage());
// postponed overriding severity
child.addSeverity("Very important custom severity");
assertEquals("Child message with parent value filename and own severity 'Very important custom severity'", child.getMessage());
Path report = tmpDir.resolve("report.json");
ReportNodeSerializer.write(root, report);
ComparisonUtils.assertTxtEquals(getClass().getResourceAsStream("/testValuesReportNode.json"), Files.readString(report));
}
@Test
void testInclude() throws IOException {
ReportNode root = ReportNode.newRootReportNode()
.withResourceBundles(TEST_BASE_NAME)
.withMessageTemplate("simpleRootTemplate")
.build();
ReportNode otherRoot = ReportNode.newRootReportNode()
.withResourceBundles(TEST_BASE_NAME)
.withMessageTemplate("includedRoot")
.build();
ReportNode otherRootChild = otherRoot.newReportNode()
.withMessageTemplate("includedChild")
.add();
PowsyblException e1 = assertThrows(PowsyblException.class, () -> root.include(ReportNode.NO_OP));
PowsyblException e2 = assertThrows(PowsyblException.class, () -> root.include(root));
PowsyblException e3 = assertThrows(PowsyblException.class, () -> otherRootChild.include(otherRoot));
assertEquals("Cannot mix implementations of ReportNode, included reportNode should be/extend ReportNodeImpl", e1.getMessage());
assertEquals("The given reportNode cannot be included as it is the root of the reportNode", e2.getMessage());
assertEquals("The given reportNode cannot be included as it is the root of the reportNode", e3.getMessage());
root.include(otherRoot);
assertEquals(1, root.getChildren().size());
assertEquals(otherRoot, root.getChildren().get(0));
assertEquals(root.getTreeContext(), ((ReportNodeImpl) otherRoot).getTreeContextRef().get());
// Other root is not root anymore and can therefore not be added again
PowsyblException e4 = assertThrows(PowsyblException.class, () -> root.include(otherRoot));
assertEquals("Cannot include non-root reportNode", e4.getMessage());
ReportNode child = root.newReportNode()
.withMessageTemplate("simpleChild")
.add();
PowsyblException e5 = assertThrows(PowsyblException.class, () -> root.include(child));
assertEquals("Cannot include non-root reportNode", e5.getMessage());
ReportNode yetAnotherRoot = ReportNode.newRootReportNode()
.withResourceBundles(TEST_BASE_NAME)
.withMessageTemplate("newRootAboveAll")
.build();
yetAnotherRoot.include(root);
assertEquals(root.getTreeContext(), ((ReportNodeImpl) yetAnotherRoot).getTreeContextRef().get());
assertEquals(root.getTreeContext(), ((ReportNodeImpl) otherRoot).getTreeContextRef().get());
roundTripTest(yetAnotherRoot, ReportNodeSerializer::write, ReportNodeDeserializer::read, "/testIncludeReportNode.json");
}
@Test
void testAddCopy() throws IOException {
ReportNode root = ReportNode.newRootReportNode()
.withResourceBundles(TEST_BASE_NAME)
.withMessageTemplate("rootWithValue")
.withTypedValue("value", 2.3203, "ROOT_VALUE")
.build();
root.newReportNode()
.withMessageTemplate("existingChild")
.add();
ReportNode otherRoot = ReportNode.newRootReportNode()
.withResourceBundles(TEST_BASE_NAME)
.withMessageTemplate("otherRoot")
.withTypedValue("value", -915.3, "ROOT_VALUE")
.build();
otherRoot.newReportNode()
.withMessageTemplate("childNotCopied")
.add();
ReportNode childToCopy = otherRoot.newReportNode()
.withMessageTemplate("childToCopy")
.add();
childToCopy.newReportNode()
.withMessageTemplate("grandChild")
.add();
root.addCopy(childToCopy);
assertEquals(2, otherRoot.getChildren().size()); // the copied message is not removed
assertEquals(2, root.getChildren().size());
ReportNode childCopied = root.getChildren().get(1);
assertEquals(childToCopy.getMessageKey(), childCopied.getMessageKey());
assertEquals(root.getTreeContext(), ((ReportNodeImpl) childCopied).getTreeContextRef().get());
// Two limitations of copy current implementation, due to the current ReportNode serialization
// 1. the inherited values are not kept
assertNotEquals(childToCopy.getMessage(), childCopied.getMessage());
// 2. the dictionary contains all the keys from the copied reportNode tree (even the ones from non-copied reportNodes)
assertEquals(6, root.getTreeContext().getDictionary().size());
Path serializedReport = tmpDir.resolve("tmp.json");
ReportNodeSerializer.write(root, serializedReport);
ComparisonUtils.assertTxtEquals(getClass().getResourceAsStream("/testCopyReportNode.json"), Files.newInputStream(serializedReport));
}
@Test
void testAddCopyCornerCases() {
ReportNode root = ReportNode.newRootReportNode()
.withResourceBundles(TEST_BASE_NAME)
.withMessageTemplate("rootWithValue")
.withTypedValue("value", 2.3203, "ROOT_VALUE")
.build();
// Corner case: copying oneself
// there's no limitation on this with current implementation
// this leads to: root
// |___ root
root.addCopy(root);
assertEquals(root.getMessage(), root.getChildren().get(0).getMessage());
// Corner case: copying an ancestor
// there's also no limitation on this with current implementation
// this leads to: root
// |___ root
// |___ rootChild
// |___root
// |___ root
// |___ rootChild
ReportNode rootChild = root.newReportNode()
.withMessageTemplate("rootChild")
.add();
rootChild.addCopy(root);
ReportNode rootGrandChild = rootChild.getChildren().get(0);
ReportNode rootGreatGrandChild1 = rootGrandChild.getChildren().get(0);
ReportNode rootGreatGrandChild2 = rootGrandChild.getChildren().get(1);
assertEquals(root.getMessage(), rootGrandChild.getMessage());
assertEquals(root.getMessage(), rootGreatGrandChild1.getMessage());
assertEquals(rootChild.getMessage(), rootGreatGrandChild2.getMessage());
}
@Test
void testDictionaryEnd() {
ReportNode report = ReportNodeDeserializer.read(getClass().getResourceAsStream("/testDictionaryEnd.json"));
assertEquals("Root message", report.getMessage());
assertEquals("Child message", report.getChildren().get(0).getMessage());
}
@Test
void testTimestamps() {
DateTimeFormatter defaultDateTimeFormatter = DateTimeFormatter.ofPattern(
ReportConstants.DEFAULT_TIMESTAMP_PATTERN, ReportConstants.DEFAULT_LOCALE);
ReportNode root1 = ReportNode.newRootReportNode()
.withResourceBundles(TEST_BASE_NAME)
.withMessageTemplate("rootTemplate")
.build();
assertHasNoTimeStamp(root1);
// No locale set and no timestamp pattern set
ReportNode child1 = root1.newReportNode()
.withMessageTemplate("child")
.withTimestamp()
.add();
assertHasTimeStamp(child1, defaultDateTimeFormatter);
Path report = tmpDir.resolve("report");
ReportNodeSerializer.write(root1, report);
ReportNode rootRead = ReportNodeDeserializer.read(report);
assertHasNoTimeStamp(rootRead);
assertHasTimeStamp(rootRead.getChildren().get(0), defaultDateTimeFormatter);
// Default timestamp pattern set but no locale set
String customPattern1 = "dd MMMM yyyy HH:mm:ss XXX";
DateTimeFormatter customPatternFormatter = DateTimeFormatter.ofPattern(customPattern1, ReportConstants.DEFAULT_LOCALE);
ReportNode root2 = ReportNode.newRootReportNode()
.withResourceBundles(TEST_BASE_NAME)
.withDefaultTimestampPattern(customPattern1)
.withTimestamp()
.withMessageTemplate("rootTemplate")
.build();
assertHasTimeStamp(root2, customPatternFormatter);
// Child does not inherit timestamp enabled (but still contains the value as inherited!)
ReportNode child2 = root2.newReportNode()
.withMessageTemplate("child")
.add();
assertHasNoTimeStamp(child2);
// Both default timestamp pattern and locale set
Locale customLocale = Locale.ITALIAN;
DateTimeFormatter customPatternAndLocaleFormatter1 = DateTimeFormatter.ofPattern(customPattern1, customLocale);
ReportNode root3 = ReportNode.newRootReportNode()
.withResourceBundles(TEST_BASE_NAME)
.withLocale(customLocale)
.withDefaultTimestampPattern(customPattern1)
.withTimestamp()
.withMessageTemplate("rootTemplate")
.build();
assertHasTimeStamp(root3, customPatternAndLocaleFormatter1);
// Child inherits timestamp pattern and locale
ReportNode child3 = root3.newReportNode()
.withMessageTemplate("simpleChild")
.withTimestamp()
.add();
assertHasTimeStamp(child3, customPatternAndLocaleFormatter1);
// Child might override timestamp pattern
String customPattern2 = "eeee dd MMMM yyyy HH:mm:ss XXX";
DateTimeFormatter customPatternAndLocaleFormatter2 = DateTimeFormatter.ofPattern(customPattern2, customLocale);
ReportNode child4 = root3.newReportNode()
.withMessageTemplate("simpleChild")
.withTimestamp(customPattern2)
.add();
assertHasTimeStamp(child4, customPatternAndLocaleFormatter2);
// with Locale set but no timestamp pattern
DateTimeFormatter noPatternAndLocaleFormatter = DateTimeFormatter.ofPattern(ReportConstants.DEFAULT_TIMESTAMP_PATTERN, customLocale);
ReportNode root4 = ReportNode.newRootReportNode()
.withResourceBundles(TEST_BASE_NAME)
.withLocale(customLocale)
.withMessageTemplate("simpleRootTemplate")
.withTimestamp()
.build();
assertHasTimeStamp(root4, noPatternAndLocaleFormatter);
}
@Test
void testMissingKey() {
// Without giving a locale
ReportNode report1 = ReportNode.newRootReportNode()
.withResourceBundles(PowsyblCoreTestReportResourceBundle.TEST_BASE_NAME, PowsyblCoreReportResourceBundle.BASE_NAME)
.withMessageTemplate("unknown.key")
.build();
// translation should fall back to default properties as the key is not defined in the reports_en_US.properties
assertEquals("Cannot find message template with key: 'unknown.key'", report1.getMessage());
// With Locale.FRENCH
ReportNode report2 = ReportNode.newRootReportNode()
.withResourceBundles(PowsyblCoreTestReportResourceBundle.TEST_BASE_NAME, PowsyblCoreReportResourceBundle.BASE_NAME)
.withLocale(Locale.FRENCH)
.withMessageTemplate("unknown.key")
.build();
// translation should fall back to default properties as the key is not defined in the reports_en_US.properties
assertEquals("Template de message non trouv�� pour la cl�� 'unknown.key'", report2.getMessage());
}
@Test
void testLocaleAndi18n() {
// Without giving a locale => default one is en_US
ReportNode rootReportEnglish = ReportNode.newRootReportNode()
.withResourceBundles(PowsyblCoreTestReportResourceBundle.TEST_BASE_NAME, PowsyblCoreReportResourceBundle.BASE_NAME)
.withMessageTemplateProvider(new BundleMessageTemplateProvider(TEST_BASE_NAME))
.withMessageTemplate("rootWithValue")
.withUntypedValue("value", 4)
.build();
// translation should fall back to default properties as the key is not defined in the reports_en_US.properties
assertEquals("Root message with value 4", rootReportEnglish.getMessage());
assertEquals(Locale.US, rootReportEnglish.getTreeContext().getLocale());
// With french locale
ReportNode rootReportFrench = ReportNode.newRootReportNode()
.withResourceBundles(TEST_BASE_NAME)
.withLocale(Locale.FRENCH)
.withMessageTemplate("rootWithValue")
.withUntypedValue("value", 4)
.build();
// translation should be from the reports_fr.properties file
assertEquals("Message racine avec la valeur 4", rootReportFrench.getMessage());
assertEquals(Locale.FRENCH, rootReportFrench.getTreeContext().getLocale());
// Test giving the specific France locale
ReportNode rootReportFrance = ReportNode.newRootReportNode()
.withResourceBundles(TEST_BASE_NAME)
.withLocale(Locale.FRANCE)
.withMessageTemplate("rootWithValue")
.withUntypedValue("value", 4)
.build();
// translation should be from the reports_fr.properties file as the key is not defined in the reports_fr_FR.properties
assertEquals("Message racine avec la valeur 4", rootReportFrance.getMessage());
assertEquals(Locale.FRANCE, rootReportFrance.getTreeContext().getLocale());
}
@Test
void testAllBundlesFromClasspath() {
ReportNode root = ReportNode.newRootReportNode()
.withAllResourceBundlesFromClasspath()
.withMessageTemplate("core.iidm.modification.voltageLevelRemoved")
.withTypedValue("vlId", "vl1", TypedValue.ID)
.build();
assertEquals("Voltage level vl1 removed", root.getMessage());
ReportNode child = root.newReportNode()
.withMessageTemplate("simpleChild")
.add();
assertEquals("Child message", child.getMessage());
}
@Test
void testMissingBundleName() {
ReportNodeBuilder reportNodeBuilder = ReportNode.newRootReportNode();
PowsyblException e = assertThrows(PowsyblException.class, reportNodeBuilder::withResourceBundles);
assertEquals("bundleBaseNames must not be empty", e.getMessage());
}
private static void assertHasNoTimeStamp(ReportNode root1) {
assertFalse(root1.getValues().containsKey(ReportConstants.TIMESTAMP_KEY));
}
private static void assertHasTimeStamp(ReportNode root, DateTimeFormatter dateTimeFormatter) {
Optional<TypedValue> timestamp = root.getValue(ReportConstants.TIMESTAMP_KEY);
assertTrue(timestamp.isPresent());
assertInstanceOf(String.class, timestamp.get().getValue());
try {
ZonedDateTime.parse((String) timestamp.get().getValue(), dateTimeFormatter);
} catch (DateTimeParseException e) {
fail();
}
}
}