MarginCalculationToolTest.java

/**
 * 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.dynawo.margincalculation.tool;

import com.powsybl.commons.test.ComparisonUtils;
import com.powsybl.dynawo.contingency.results.FailedCriterion;
import com.powsybl.dynawo.contingency.results.ScenarioResult;
import com.powsybl.dynawo.contingency.results.Status;
import com.powsybl.dynawo.margincalculation.MarginCalculationProvider;
import com.powsybl.dynawo.margincalculation.results.LoadIncreaseResult;
import com.powsybl.dynawo.margincalculation.results.MarginCalculationResult;
import com.powsybl.tools.Command;
import com.powsybl.tools.CommandLineTools;
import com.powsybl.tools.Tool;
import com.powsybl.tools.test.AbstractToolTest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.MockedConstruction;
import org.mockito.Mockito;

import java.io.IOException;
import java.nio.file.Files;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;

/**
 * @author Laurent Issertial {@literal <laurent.issertial at rte-france.com>}
 */
class MarginCalculationToolTest extends AbstractToolTest {

    private static final MockedConstruction.MockInitializer<MarginCalculationProvider> MOCK_INITIALIZER =
            (mock, context) -> {
                when(mock.getName()).thenReturn("Mock");
                when(mock.run(any(), any(), any(), any(), any(), any()))
                        .thenReturn(CompletableFuture.completedFuture(new MarginCalculationResult(List.of(
                                new LoadIncreaseResult(100, Status.CRITERIA_NON_RESPECTED,
                                        Collections.emptyList(),
                                        List.of(new FailedCriterion("failed", 10),
                                                new FailedCriterion("failed2", 20))),
                                new LoadIncreaseResult(50, Status.DIVERGENCE,
                                        List.of(new ScenarioResult("Disconnect line", Status.CRITERIA_NON_RESPECTED,
                                                        List.of(new FailedCriterion("Sc failed", 10),
                                                                new FailedCriterion("Sc failed2", 20))),
                                                new ScenarioResult("Disconnect gen", Status.CONVERGENCE))),
                                new LoadIncreaseResult(25, Status.CONVERGENCE)
                        ))));
            };

    private final MarginCalculationTool tool = new MarginCalculationTool();

    @BeforeEach
    @Override
    public void setUp() throws Exception {
        super.setUp();
        Files.copy(Objects.requireNonNull(getClass().getResourceAsStream("/empty_network.xiidm")), fileSystem.getPath("/network.xiidm"));
        Files.createFile(fileSystem.getPath("/dynamicModels.groovy"));
        Files.createFile(fileSystem.getPath("/contingencies.groovy"));
        Files.createFile(fileSystem.getPath("/loadsVariations.json"));
    }

    @Override
    protected Iterable<Tool> getTools() {
        return Collections.singleton(tool);
    }

    @Override
    @Test
    public void assertCommand() {
        Command command = tool.getCommand();
        assertCommand(command, "margin-calculation", 9, 4);
        assertEquals("Computation", command.getTheme());
        assertEquals("Run margin calculation", command.getDescription());
        assertNull(command.getUsageFooter());
        assertOption(command.getOptions(), "case-file", true, true);
        assertOption(command.getOptions(), "dynamic-models-file", true, true);
        assertOption(command.getOptions(), "contingencies-file", true, true);
        assertOption(command.getOptions(), "load-variations-file", true, true);
        assertOption(command.getOptions(), "parameters-file", false, true);
        assertOption(command.getOptions(), "output-file", false, true);
        assertOption(command.getOptions(), "output-log-file", false, true);
        assertOption(command.getOptions(), "import-parameters", false, true);
        assertOption(command.getOptions(), "I", false, true);
    }

    @Test
    void testMarginCalculation() {
        try (MockedConstruction<MarginCalculationProvider> provider = Mockito.mockConstruction(MarginCalculationProvider.class, MOCK_INITIALIZER)) {

            String expectedOut = """
                    Loading network '/network.xiidm'
                    Margin Calculation Tool
                    Margin calculation results:
                    +------------+------------------------+---------------------+----------------------+-----------------+------------------------+------------------------------+--------------------------------+
                    | Load level | Status                 | Failed criteria     | Failed criteria time | Scenarios       | Scenarios Status       | Scenarios failed criteria    | Scenarios failed criteria time |
                    +------------+------------------------+---------------------+----------------------+-----------------+------------------------+------------------------------+--------------------------------+
                    | 100.000    | CRITERIA_NON_RESPECTED | Failed criteria (2) |                      |                 |                        |                              |                                |
                    |            |                        | failed              | 10.0000              |                 |                        |                              |                                |
                    |            |                        | failed2             | 20.0000              |                 |                        |                              |                                |
                    | 50.0000    | DIVERGENCE             |                     |                      | Scenarios (2)   |                        |                              |                                |
                    |            |                        |                     |                      | Disconnect line | CRITERIA_NON_RESPECTED | Scenario failed criteria (2) |                                |
                    |            |                        |                     |                      |                 |                        | Sc failed                    | 10.0000                        |
                    |            |                        |                     |                      |                 |                        | Sc failed2                   | 20.0000                        |
                    |            |                        |                     |                      | Disconnect gen  | CONVERGENCE            |                              |                                |
                    | 25.0000    | CONVERGENCE            |                     |                      |                 |                        |                              |                                |
                    +------------+------------------------+---------------------+----------------------+-----------------+------------------------+------------------------------+--------------------------------+
                    """;
            assertCommandSuccessful(new String[]{"margin-calculation",
                "--case-file", "/network.xiidm",
                "--dynamic-models-file", "/dynamicModels.groovy",
                "--contingencies-file", "/contingencies.groovy",
                "--load-variations-file", "/loadsVariations.json"}, expectedOut);
        }
    }

    @Test
    void testMarginCalculationWithOutputFile() throws IOException {
        try (MockedConstruction<MarginCalculationProvider> provider = Mockito.mockConstruction(MarginCalculationProvider.class, MOCK_INITIALIZER)) {

            String expectedOut = """
                    Loading network '/network.xiidm'
                    Margin Calculation Tool
                    Writing results to 'outputTest.json'
                    """;
            assertCommandSuccessful(new String[]{"margin-calculation",
                "--case-file", "/network.xiidm",
                "--dynamic-models-file", "/dynamicModels.groovy",
                "--contingencies-file", "/contingencies.groovy",
                "--load-variations-file", "/loadsVariations.json",
                "--output-file", "outputTest.json"}, expectedOut);
            ComparisonUtils.assertTxtEquals(Objects.requireNonNull(getClass().getResourceAsStream("/tool_result.json")),
                    Files.newInputStream(fileSystem.getPath("outputTest.json")));
        }
    }

    @Test
    void testMarginCalculationWithLogFile() throws IOException {
        try (MockedConstruction<MarginCalculationProvider> provider = Mockito.mockConstruction(MarginCalculationProvider.class, MOCK_INITIALIZER)) {

            String expectedOut = """
                    Loading network '/network.xiidm'
                    Writing logs to 'outputTest.log'
                    Margin calculation results:
                    +------------+------------------------+---------------------+----------------------+-----------------+------------------------+------------------------------+--------------------------------+
                    | Load level | Status                 | Failed criteria     | Failed criteria time | Scenarios       | Scenarios Status       | Scenarios failed criteria    | Scenarios failed criteria time |
                    +------------+------------------------+---------------------+----------------------+-----------------+------------------------+------------------------------+--------------------------------+
                    | 100.000    | CRITERIA_NON_RESPECTED | Failed criteria (2) |                      |                 |                        |                              |                                |
                    |            |                        | failed              | 10.0000              |                 |                        |                              |                                |
                    |            |                        | failed2             | 20.0000              |                 |                        |                              |                                |
                    | 50.0000    | DIVERGENCE             |                     |                      | Scenarios (2)   |                        |                              |                                |
                    |            |                        |                     |                      | Disconnect line | CRITERIA_NON_RESPECTED | Scenario failed criteria (2) |                                |
                    |            |                        |                     |                      |                 |                        | Sc failed                    | 10.0000                        |
                    |            |                        |                     |                      |                 |                        | Sc failed2                   | 20.0000                        |
                    |            |                        |                     |                      | Disconnect gen  | CONVERGENCE            |                              |                                |
                    | 25.0000    | CONVERGENCE            |                     |                      |                 |                        |                              |                                |
                    +------------+------------------------+---------------------+----------------------+-----------------+------------------------+------------------------------+--------------------------------+
                    """;
            String expectedOutputFile = "Margin Calculation Tool\n";
            assertCommandSuccessful(new String[]{"margin-calculation",
                "--case-file", "/network.xiidm",
                "--dynamic-models-file", "/dynamicModels.groovy",
                "--contingencies-file", "/contingencies.groovy",
                "--load-variations-file", "/loadsVariations.json",
                "--output-log-file", "outputTest.log"}, expectedOut);
            ComparisonUtils.assertTxtEquals(expectedOutputFile, Files.newInputStream(fileSystem.getPath("outputTest.log")));
        }
    }

    @Test
    void testWrongNetwork() {
        try (MockedConstruction<MarginCalculationProvider> provider = Mockito.mockConstruction(MarginCalculationProvider.class, MOCK_INITIALIZER)) {
            assertCommandErrorMatch(new String[]{"margin-calculation",
                "--case-file", "/wrong_network.xiidm",
                "--dynamic-models-file", "/dynamicModels.groovy",
                "--contingencies-file", "/contingencies.groovy",
                "--load-variations-file", "/loadsVariations.json"},
                CommandLineTools.EXECUTION_ERROR_STATUS,
                "File /wrong_network.xiidm does not exist");
        }
    }
}