diff --git a/src/main/java/org/gridsuite/modification/dto/CompositeModificationInfos.java b/src/main/java/org/gridsuite/modification/dto/CompositeModificationInfos.java index 96ea4838..72aec97c 100644 --- a/src/main/java/org/gridsuite/modification/dto/CompositeModificationInfos.java +++ b/src/main/java/org/gridsuite/modification/dto/CompositeModificationInfos.java @@ -8,6 +8,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonTypeName; +import com.powsybl.commons.report.ReportNode; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.NoArgsConstructor; @@ -33,6 +34,10 @@ @ModificationErrorTypeName("COMPOSITE_MODIFICATION_ERROR") public class CompositeModificationInfos extends ModificationInfos { + @Schema(description = "composite modification name") + @JsonInclude(JsonInclude.Include.NON_NULL) + private String name; + @Schema(description = "composite modification list") @JsonInclude(JsonInclude.Include.NON_NULL) private List modifications; @@ -41,4 +46,12 @@ public class CompositeModificationInfos extends ModificationInfos { public AbstractModification toModification() { return new CompositeModification(this); } + + @Override + public ReportNode createSubReportNode(ReportNode reportNode) { + return reportNode.newReportNode() + .withMessageTemplate("network.modification.composite.apply") + .withUntypedValue("modificationName", getName()) + .add(); + } } diff --git a/src/main/java/org/gridsuite/modification/dto/ModificationsToCopyInfos.java b/src/main/java/org/gridsuite/modification/dto/ModificationsToCopyInfos.java new file mode 100644 index 00000000..7b2ff6f3 --- /dev/null +++ b/src/main/java/org/gridsuite/modification/dto/ModificationsToCopyInfos.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2026, 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/. + */ + +package org.gridsuite.modification.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.UUID; + +/** + * @author Mathieu Deharbe + */ +@AllArgsConstructor +@NoArgsConstructor +@Getter +@Builder +public class ModificationsToCopyInfos { + private UUID uuid; + + // only useful when the operation is the import of a composite as a complete composite + private String compositeName; +} + diff --git a/src/main/java/org/gridsuite/modification/modifications/CompositeModification.java b/src/main/java/org/gridsuite/modification/modifications/CompositeModification.java index 8feacfcd..e28cf926 100644 --- a/src/main/java/org/gridsuite/modification/modifications/CompositeModification.java +++ b/src/main/java/org/gridsuite/modification/modifications/CompositeModification.java @@ -7,20 +7,46 @@ package org.gridsuite.modification.modifications; import com.powsybl.commons.report.ReportNode; +import com.powsybl.commons.report.TypedValue; import com.powsybl.iidm.network.Network; import org.gridsuite.modification.dto.CompositeModificationInfos; +import org.gridsuite.modification.report.NetworkModificationReportResourceBundle; + +import static org.gridsuite.modification.modifications.byfilter.AbstractModificationByAssignment.VALUE_KEY_ERROR_MESSAGE; /** * @author Ghazwa Rehili */ public class CompositeModification extends AbstractModification { + private final CompositeModificationInfos compositeModificationInfos; + public CompositeModification(CompositeModificationInfos compositeModificationInfos) { + this.compositeModificationInfos = compositeModificationInfos; } @Override public void apply(Network network, ReportNode subReportNode) { - throw new UnsupportedOperationException(); + compositeModificationInfos.getModifications().forEach( + modif -> { + ReportNode modifNode = modif.createSubReportNode(subReportNode); + try { + AbstractModification modification = modif.toModification(); + modification.check(network); + modification.apply(network, modifNode); + } catch (Exception e) { + // in case of error in a network modification, the composite modification doesn't interrupt its execution : + // the following modifications will be carried out + modifNode.newReportNode() + .withResourceBundles(NetworkModificationReportResourceBundle.BASE_NAME) + .withMessageTemplate("network.modification.composite.exception.report") + .withUntypedValue("modificationName", modif.toModification().getName()) + .withUntypedValue(VALUE_KEY_ERROR_MESSAGE, e.getMessage()) + .withSeverity(TypedValue.ERROR_SEVERITY) + .add(); + } + } + ); } @Override diff --git a/src/main/resources/org/gridsuite/modification/reports.properties b/src/main/resources/org/gridsuite/modification/reports.properties index bcff3703..1825b1e9 100644 --- a/src/main/resources/org/gridsuite/modification/reports.properties +++ b/src/main/resources/org/gridsuite/modification/reports.properties @@ -192,6 +192,8 @@ network.modification.noLimitSetSelectedOnSide1 = No limit set selected on side 1 network.modification.noLimitSetSelectedOnSide2 = No limit set selected on side 2 network.modification.limitSetAbsentOnSide1 = limit set '${selectedOperationalLimitsGroup}' on side 1 does not exist network.modification.limitSetAbsentOnSide2 = limit set '${selectedOperationalLimitsGroup}' on side 2 does not exist +network.modification.composite.apply = Composite modification : '${modificationName}' +network.modification.composite.exception.report = Cannot execute ${modificationName} : ${errorMessage} network.modification.applicabilityChanged = limit set ${operationalLimitsGroupName} applicability changed to ${applicability} network.modification.limits = Limits network.modification.activeLimitsSets = Active limits sets diff --git a/src/test/java/org/gridsuite/modification/modifications/CompositeModificationsTest.java b/src/test/java/org/gridsuite/modification/modifications/CompositeModificationsTest.java index ac004b93..bef09d9d 100644 --- a/src/test/java/org/gridsuite/modification/modifications/CompositeModificationsTest.java +++ b/src/test/java/org/gridsuite/modification/modifications/CompositeModificationsTest.java @@ -6,18 +6,24 @@ */ package org.gridsuite.modification.modifications; +import com.powsybl.commons.PowsyblException; +import com.powsybl.commons.report.ReportNode; +import com.powsybl.iidm.network.Generator; import com.powsybl.iidm.network.LoadType; import com.powsybl.iidm.network.Network; import org.gridsuite.modification.ModificationType; -import org.gridsuite.modification.dto.CompositeModificationInfos; -import org.gridsuite.modification.dto.ModificationInfos; +import org.gridsuite.modification.dto.*; +import org.gridsuite.modification.report.NetworkModificationReportResourceBundle; import org.gridsuite.modification.utils.ModificationCreation; import org.gridsuite.modification.utils.NetworkCreation; +import org.junit.jupiter.api.Test; + import java.util.List; import java.util.UUID; -import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.gridsuite.modification.utils.TestUtils.*; +import static org.junit.jupiter.api.Assertions.*; /** * @author Ghazwa Rehili @@ -27,13 +33,64 @@ class CompositeModificationsTest extends AbstractNetworkModificationTest { @Override public void checkModification() { + // nothing to check here } - @Override - public void testApply() throws Exception { + @Test + void checkCompositeExecutionDepth() { + Network network = getNetwork(); + CompositeModificationInfos compositeModificationInfos = (CompositeModificationInfos) buildModification(); + + // checks that the sub sub sub netmod is executed at the right depth + ReportNode report = compositeModificationInfos.createSubReportNode(ReportNode.newRootReportNode() + .withResourceBundles(NetworkModificationReportResourceBundle.BASE_NAME) + .withMessageTemplate("test") + .build()); + CompositeModification netmod = (CompositeModification) compositeModificationInfos.toModification(); + assertDoesNotThrow(() -> netmod.apply(network, report)); + assertLogMessageAtDepth( + "Generator with id=idGenerator modified :", + "network.modification.generatorModification", + report, + 4 + ); + assertLogMessageAtDepth( + "Composite modification : 'sub sub composite'", + "network.modification.composite.apply", + report, + 2 + ); + } + + @Test + void checkCompositeExecutionErrorHandling() { + Network network = getNetwork(); CompositeModificationInfos compositeModificationInfos = (CompositeModificationInfos) buildModification(); - compositeModificationInfos.getModifications().forEach(modificationInfos -> modificationInfos.toModification().apply(getNetwork())); - assertAfterNetworkModificationApplication(); + + ReportNode report = compositeModificationInfos.createSubReportNode(ReportNode.newRootReportNode() + .withResourceBundles(NetworkModificationReportResourceBundle.BASE_NAME) + .withMessageTemplate("test") + .build()); + // regular throwing exception netmod + GeneratorCreation throwingExceptionNetMod = (GeneratorCreation) buildThrowingModification().toModification(); + assertThrows(PowsyblException.class, () -> throwingExceptionNetMod.apply(network)); + // but doesn't throw once inside a composite modification + compositeModificationInfos.setModifications(List.of(buildThrowingModification())); + CompositeModification netmodContainingError = (CompositeModification) compositeModificationInfos.toModification(); + assertDoesNotThrow(() -> netmodContainingError.apply(network, report)); + // but the thrown message is inside the report : + assertLogMessageWithoutRank( + "Cannot execute GeneratorCreation : GENERATOR_ALREADY_EXISTS : idGenerator", + "network.modification.composite.exception.report", + report + ); + + } + + private GeneratorCreationInfos buildThrowingModification() { + return ModificationCreation.getCreationGenerator( + "v1", "idGenerator", "nameGenerator", "1B", "v2load", "LOAD", "v1" + ); } @Override @@ -44,11 +101,34 @@ protected Network createNetwork(UUID networkUuid) { @Override protected ModificationInfos buildModification() { List modifications = List.of( - ModificationCreation.getCreationGenerator("v1", "idGenerator", "nameGenerator", "1B", "v2load", "LOAD", - "v1"), + CompositeModificationInfos.builder() + .name("sub composite 1") + .modifications( + List.of( + ModificationCreation.getModificationGenerator("idGenerator", "other idGenerator name"), + // this should throw an error but not stop the execution of the composite modification and all the other content + buildThrowingModification() + ) + ).build(), + ModificationCreation.getModificationGenerator("idGenerator", "new idGenerator name"), ModificationCreation.getCreationLoad("v1", "idLoad", "nameLoad", "1.1", LoadType.UNDEFINED), - ModificationCreation.getCreationBattery("v1", "idBattery", "nameBattry", "1.1")); + ModificationCreation.getCreationBattery("v1", "idBattery", "nameBattery", "1.1"), + // test of a composite modification inside a composite modification inside a composite modification + CompositeModificationInfos.builder() + .name("sub composite 2") + .modifications( + List.of( + CompositeModificationInfos.builder() + .name("sub sub composite") + .modifications( + List.of(ModificationCreation.getModificationGenerator("idGenerator", "other idGenerator name again")) + ).build(), + ModificationCreation.getModificationGenerator("idGenerator", "even newer idGenerator name") + ) + ).build() + ); return CompositeModificationInfos.builder() + .name("main composite") .modifications(modifications) .stashed(false) .build(); @@ -56,7 +136,9 @@ protected ModificationInfos buildModification() { @Override protected void assertAfterNetworkModificationApplication() { - assertNotNull(getNetwork().getGenerator("idGenerator")); + Generator gen = getNetwork().getGenerator("idGenerator"); + assertNotNull(gen); + assertEquals("even newer idGenerator name", gen.getOptionalName().orElseThrow()); assertNotNull(getNetwork().getLoad("idLoad")); assertNotNull(getNetwork().getBattery("idBattery")); } diff --git a/src/test/java/org/gridsuite/modification/utils/TestUtils.java b/src/test/java/org/gridsuite/modification/utils/TestUtils.java index 52c73bdb..bf93c9e0 100644 --- a/src/test/java/org/gridsuite/modification/utils/TestUtils.java +++ b/src/test/java/org/gridsuite/modification/utils/TestUtils.java @@ -58,6 +58,12 @@ public static void assertLogNthMessage(String expectedMessage, String reportKey, assertEquals(expectedMessage, message.get().trim()); } + public static void assertLogMessageAtDepth(String expectedMessage, String reportKey, ReportNode reportNode, int depth) { + Optional message = getMessageFromReporterAtDepth(reportKey, reportNode, 1, depth); + assertTrue(message.isPresent()); + assertEquals(expectedMessage, message.get().trim()); + } + public static void assertLogMessage(String expectedMessage, String reportKey, ReportNode reportNode) { assertLogNthMessage(expectedMessage, reportKey, reportNode, 1); } @@ -85,6 +91,28 @@ private static boolean assertMessageFoundFromReporter(String expectedMessage, St return foundInSubReporters; } + private static Optional getMessageFromReporterAtDepth(String reportKey, ReportNode reporterModel, int currentDepth, int expectedDepth) { + Optional message = Optional.empty(); + + Iterator reportersIterator = reporterModel.getChildren().iterator(); + while (message.isEmpty() && reportersIterator.hasNext() && currentDepth < expectedDepth) { + message = getMessageFromReporterAtDepth(reportKey, reportersIterator.next(), currentDepth + 1, expectedDepth); + } + + Iterator reportsIterator = reporterModel.getChildren().iterator(); + while (message.isEmpty() && reportsIterator.hasNext()) { + ReportNode report = reportsIterator.next(); + if (currentDepth == expectedDepth && report.getMessageKey().equals(reportKey)) { + message = Optional.of(formatReportMessage(report, reporterModel)); + } + } + + return message; + } + + /** + * @param rank order position inside reporterModel + */ private static Optional getMessageFromReporter(String reportKey, ReportNode reporterModel, int rank) { Optional message = Optional.empty();