Skip to content

Commit

Permalink
Add copy to ReportNode API (#3303)
Browse files Browse the repository at this point in the history
* Add copy to ReportNode API
* Add unit test
* Renaming copy -> addCopy
* Add documentation
* Improve coverage

Signed-off-by: Florian Dupuy <florian.dupuy@rte-france.com>
  • Loading branch information
flo-dup authored Feb 20, 2025
1 parent 54e0963 commit f522552
Show file tree
Hide file tree
Showing 10 changed files with 248 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,13 @@ default String getMessage() {
*/
void include(ReportNode reportRoot);

/**
* Copy the given <code>ReportNode</code> and inserts the resulting <code>ReportNode</code> as a child of current <code>ReportNode</code>.
*
* @param reportNode the <code>ReportNode</code> to copy into the children of current <code>ReportNode</code>
*/
void addCopy(ReportNode reportNode);

/**
* Serialize the current report node
* @param generator the jsonGenerator to use for serialization
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,14 @@
import com.powsybl.commons.PowsyblException;
import com.powsybl.commons.ref.RefChain;
import com.powsybl.commons.ref.RefObj;
import org.apache.commons.io.IOUtils;
import org.apache.commons.text.StringSubstitutor;

import java.io.IOException;
import java.io.StringWriter;
import java.io.UncheckedIOException;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Stream;
Expand Down Expand Up @@ -151,15 +155,15 @@ public ReportNodeAdder newReportNode() {
}

@Override
public void include(ReportNode reportNode) {
if (!(reportNode instanceof ReportNodeImpl reportNodeImpl)) {
public void include(ReportNode reportRoot) {
if (!(reportRoot instanceof ReportNodeImpl reportNodeImpl)) {
throw new PowsyblException("Cannot mix implementations of ReportNode, included reportNode should be/extend ReportNodeImpl");
}
if (!reportNodeImpl.isRoot) {
throw new PowsyblException("Cannot include non-root reportNode");
}
if (reportNode == this) {
throw new PowsyblException("Cannot add a reportNode in itself");
if (getTreeContext() == reportNodeImpl.getTreeContext()) {
throw new PowsyblException("The given reportNode cannot be included as it is the root of the reportNode");
}

reportNodeImpl.unroot();
Expand All @@ -169,6 +173,24 @@ public void include(ReportNode reportNode) {
reportNodeImpl.treeContext.setRef(treeContext);
}

@Override
public void addCopy(ReportNode reportNode) {
var om = new ObjectMapper().registerModule(new ReportNodeJsonModule());
var sw = new StringWriter();

try {
om.writeValue(sw, reportNode);
} catch (IOException e) {
throw new UncheckedIOException(e);
}

ReportNodeImpl copiedReportNode = (ReportNodeImpl) ReportNodeDeserializer.read(IOUtils.toInputStream(sw.toString(), StandardCharsets.UTF_8));
children.add(copiedReportNode);

getTreeContext().merge(copiedReportNode.getTreeContext());
copiedReportNode.treeContext.setRef(treeContext);
}

private void unroot() {
this.isRoot = false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,19 @@ public ReportNodeAdder newReportNode() {

@Override
public TreeContext getTreeContext() {
return null;
return TreeContext.NO_OP;
}

@Override
public void include(ReportNode reportRoot) {
// No-op
}

@Override
public void addCopy(ReportNode reportNode) {
// No-op
}

@Override
public String getMessageKey() {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
*/
public interface TreeContext {

TreeContext NO_OP = new TreeContextNoOp();

/**
* Get the dictionary of message templates indexed by their key.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* Copyright (c) 2025, 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 java.time.format.DateTimeFormatter;
import java.util.Map;

/**
* @author Florian Dupuy {@literal <florian.dupuy at rte-france.com>}
*/
public class TreeContextNoOp implements TreeContext {

@Override
public Map<String, String> getDictionary() {
return Map.of();
}

@Override
public DateTimeFormatter getTimestampFormatter() {
return null;
}

@Override
public boolean isTimestampAdded() {
return false;
}

@Override
public void merge(TreeContext treeContext) {
// no-op
}
}
29 changes: 27 additions & 2 deletions commons/src/test/java/com/powsybl/commons/report/NoOpTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@
*/
package com.powsybl.commons.report;

import com.powsybl.commons.test.AbstractSerDeTest;
import com.powsybl.commons.test.ComparisonUtils;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.io.StringWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.Optional;

Expand All @@ -19,7 +23,7 @@
/**
* @author Florian Dupuy {@literal <florian.dupuy at rte-france.com>}
*/
class NoOpTest {
class NoOpTest extends AbstractSerDeTest {

@Test
void test() throws IOException {
Expand All @@ -39,6 +43,7 @@ void test() throws IOException {
.withUntypedValue("untyped_boolean", true)
.withUntypedValue("untyped_string", "vl1")
.withSeverity(TypedValue.TRACE_SEVERITY)
.withSeverity("Custom severity")
.add();
assertEquals(Collections.emptyList(), root.getChildren());
assertNotEquals(ReportNode.NO_OP, reportNode);
Expand All @@ -48,12 +53,32 @@ void test() throws IOException {
assertNull(reportNode.getMessageTemplate());
assertNull(reportNode.getMessageKey());

reportNode.include(ReportNode.newRootReportNode().withMessageTemplate("k", "Real root reportNode").build());
ReportNode reportNodeImplRoot = ReportNode.newRootReportNode().withMessageTemplate("k", "Real root reportNode").build();
reportNode.include(reportNodeImplRoot);
assertEquals(Collections.emptyList(), reportNode.getChildren());

root.addCopy(reportNodeImplRoot);
assertEquals(Collections.emptyList(), root.getChildren());

StringWriter sw = new StringWriter();
reportNode.print(sw);
assertEquals("", sw.toString());

Path serializedReport = tmpDir.resolve("tmp.json");
ReportNodeSerializer.write(root, serializedReport);
ComparisonUtils.assertTxtEquals(getClass().getResourceAsStream("/testReportNodeNoOp.json"), Files.newInputStream(serializedReport));
}

@Test
void testTreeContextNoOp() {
assertEquals(0, TreeContextNoOp.NO_OP.getDictionary().size());
assertNull(TreeContextNoOp.NO_OP.getTimestampFormatter());
assertFalse(TreeContextNoOp.NO_OP.isTimestampAdded());

TreeContextImpl treeContext = new TreeContextImpl();
treeContext.addDictionaryEntry("key", "value");
TreeContextNoOp.NO_OP.merge(treeContext);
assertEquals(0, TreeContextNoOp.NO_OP.getDictionary().size());
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,30 +134,34 @@ void testInclude() throws IOException {
.withMessageTemplate("rootTemplate", "Root message")
.build();

PowsyblException e1 = assertThrows(PowsyblException.class, () -> root.include(root));
assertEquals("Cannot add a reportNode in itself", e1.getMessage());

ReportNode otherRoot = ReportNode.newRootReportNode()
.withMessageTemplate("includedRoot", "Included root message")
.build();
otherRoot.newReportNode()
ReportNode otherRootChild = otherRoot.newReportNode()
.withMessageTemplate("includedChild", "Included child message")
.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(((ReportNodeImpl) root).getTreeContext(), ((ReportNodeImpl) otherRoot).getTreeContextRef().get());

// Other root is not root anymore and can therefore not be added again
PowsyblException e2 = assertThrows(PowsyblException.class, () -> root.include(otherRoot));
assertEquals("Cannot include non-root reportNode", e2.getMessage());
PowsyblException e4 = assertThrows(PowsyblException.class, () -> root.include(otherRoot));
assertEquals("Cannot include non-root reportNode", e4.getMessage());

ReportNode child = root.newReportNode()
.withMessageTemplate("child", "Child message")
.add();
PowsyblException e3 = assertThrows(PowsyblException.class, () -> root.include(child));
assertEquals("Cannot include non-root reportNode", e3.getMessage());
PowsyblException e5 = assertThrows(PowsyblException.class, () -> root.include(child));
assertEquals("Cannot include non-root reportNode", e5.getMessage());

ReportNode yetAnotherRoot = ReportNode.newRootReportNode()
.withMessageTemplate("newRootAboveAll", "New root above all reportNodes")
Expand All @@ -169,6 +173,85 @@ void testInclude() throws IOException {
roundTripTest(yetAnotherRoot, ReportNodeSerializer::write, ReportNodeDeserializer::read, "/testIncludeReportNode.json");
}

@Test
void testAddCopy() throws IOException {
ReportNode root = ReportNode.newRootReportNode()
.withMessageTemplate("root", "Root message with value ${value}")
.withTypedValue("value", 2.3203, "ROOT_VALUE")
.build();
root.newReportNode()
.withMessageTemplate("existingChild", "Child message")
.add();

ReportNode otherRoot = ReportNode.newRootReportNode()
.withMessageTemplate("otherRoot", "Root message containing node to copy")
.withTypedValue("value", -915.3, "ROOT_VALUE")
.build();
otherRoot.newReportNode()
.withMessageTemplate("childNotCopied", "Child message")
.add();
ReportNode childToCopy = otherRoot.newReportNode()
.withMessageTemplate("childToCopy", "Child message with inherited value ${value}")
.add();
childToCopy.newReportNode()
.withMessageTemplate("grandChild", "Grandchild message")
.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(((ReportNodeImpl) 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, ((ReportNodeImpl) 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()
.withMessageTemplate("root", "Root message with value ${value}")
.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", "Another child")
.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"));
Expand Down
30 changes: 30 additions & 0 deletions commons/src/test/resources/testCopyReportNode.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"version" : "2.1",
"dictionaries" : {
"default" : {
"childNotCopied" : "Child message",
"childToCopy" : "Child message with inherited value ${value}",
"existingChild" : "Child message",
"grandChild" : "Grandchild message",
"otherRoot" : "Root message containing node to copy",
"root" : "Root message with value ${value}"
}
},
"reportRoot" : {
"messageKey" : "root",
"values" : {
"value" : {
"value" : 2.3203,
"type" : "ROOT_VALUE"
}
},
"children" : [ {
"messageKey" : "existingChild"
}, {
"messageKey" : "childToCopy",
"children" : [ {
"messageKey" : "grandChild"
} ]
} ]
}
}
7 changes: 7 additions & 0 deletions commons/src/test/resources/testReportNodeNoOp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"version" : "2.1",
"dictionaries" : {
"default" : { }
},
"reportRoot" : { }
}
15 changes: 15 additions & 0 deletions docs/user/functional_logs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,21 @@ Both API share methods to provide the message template and the typed values:
- `withTypedValue(key, value, type)`,
- `withSeverity(severity)`.

## Merging ReportNodes

### Include
An `include` method is provided in the API in order to fully insert a given root `ReportNode` as a child of another `ReportNode`.
The given root `ReportNode` is becoming non-root after this operation.
This was meant for including the serialized reports obtained from another process.

### AddCopy
An `addCopy` method is provided in the API to partly insert a `ReportNode`: unlike `include`, the given node does not need to be root.
The given `ReportNode` is copied and inserted as a child of the `ReportNode`.

Two known limitations of this method:
1. the inherited values of copied `ReportNode` are not kept,
2. the resulting dictionary contains all the keys from the copied `ReportNode` tree, even the ones from non-copied `ReportNode`s.

## Example
```java
ReportNode root = ReportNode.newRootReportNode()
Expand Down

0 comments on commit f522552

Please sign in to comment.