DynamicSecurityAnalysisToolTest.java

/**
 * Copyright (c) 2017, 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 com.powsybl.security.dynamic.tools;

import com.google.auto.service.AutoService;
import com.google.common.io.ByteSource;
import com.powsybl.commons.PowsyblException;
import com.powsybl.commons.io.FileUtil;
import com.powsybl.commons.io.table.TableFormatterConfig;
import com.powsybl.computation.ComputationException;
import com.powsybl.computation.ComputationExceptionBuilder;
import com.powsybl.computation.ComputationManager;
import com.powsybl.contingency.ContingenciesProvider;
import com.powsybl.dynamicsimulation.DynamicModelsSupplier;
import com.powsybl.dynamicsimulation.groovy.DynamicSimulationSupplierFactory;
import com.powsybl.iidm.network.ImportersLoaderList;
import com.powsybl.iidm.network.Network;
import com.powsybl.security.LimitViolationFilter;
import com.powsybl.security.LimitViolationType;
import com.powsybl.security.SecurityAnalysisReport;
import com.powsybl.security.distributed.ExternalSecurityAnalysisConfig;
import com.powsybl.security.dynamic.*;
import com.powsybl.security.dynamic.execution.DynamicSecurityAnalysisExecutionBuilder;
import com.powsybl.security.dynamic.execution.DynamicSecurityAnalysisExecutionInput;
import com.powsybl.security.preprocessor.SecurityAnalysisPreprocessor;
import com.powsybl.security.preprocessor.SecurityAnalysisPreprocessorFactory;
import com.powsybl.security.tools.SecurityAnalysisToolConstants;
import com.powsybl.tools.Command;
import com.powsybl.tools.Tool;
import com.powsybl.tools.ToolOptions;
import com.powsybl.tools.ToolRunningContext;
import com.powsybl.tools.test.AbstractToolTest;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.Options;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.function.Supplier;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

/**
 * @author Mathieu Bague {@literal <mathieu.bague at rte-france.com>}
 * @author Laurent Issertial {@literal <laurent.issertial at rte-france.com>}
 */
class DynamicSecurityAnalysisToolTest extends AbstractToolTest {

    private static final String OUTPUT_LOG_FILENAME = "out.zip";

    private static final String DYNAMIC_MODEL_FILENAME = "dynamicModel";

    private DynamicSecurityAnalysisTool tool;
    private ByteSource dynamicModels;
    private DynamicModelsSupplier dynamicModelsSupplier;

    @Override
    @BeforeEach
    public void setUp() throws Exception {
        super.setUp();
        tool = new DynamicSecurityAnalysisTool();
        Files.createFile(fileSystem.getPath("network.xml"));
        dynamicModels = FileUtil.asByteSource(Files.write(fileSystem.getPath(DYNAMIC_MODEL_FILENAME), "test".getBytes()));
        dynamicModelsSupplier = DynamicSimulationSupplierFactory.createDynamicModelsSupplier(dynamicModels.openBufferedStream(), "DynamicSecurityAnalysisToolProviderMock");
    }

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

    @Override
    public void assertCommand() {
        Command command = tool.getCommand();
        Options options = command.getOptions();
        assertCommand(command, "dynamic-security-analysis", 15, 2);
        assertOption(options, "case-file", true, true);
        assertOption(options, "dynamic-models-file", true, true);
        assertOption(options, "parameters-file", false, true);
        assertOption(options, "limit-types", false, true);
        assertOption(options, "output-file", false, true);
        assertOption(options, "output-format", false, true);
        assertOption(options, "contingencies-file", false, true);
        assertOption(options, "with-extensions", false, true);
        assertOption(options, "task-count", false, true);
        assertOption(options, "task", false, true);
        assertOption(options, "external", false, false);
        assertOption(options, "log-file", false, true);
        assertOption(options, "monitoring-file", false, true);
    }

    @Test
    void test() {
        assertCommand();
    }

    private static CommandLine mockCommandLine(Map<String, String> options, Set<String> flags) {
        CommandLine cli = mock(CommandLine.class);
        when(cli.hasOption(anyString())).thenReturn(false);
        when(cli.getOptionValue(anyString())).thenReturn(null);
        options.forEach((k, v) -> {
            when(cli.getOptionValue(k)).thenReturn(v);
            when(cli.hasOption(k)).thenReturn(true);
        });
        flags.forEach(f -> when(cli.hasOption(f)).thenReturn(true));
        when(cli.getOptionProperties(anyString())).thenReturn(new Properties());
        return cli;
    }

    private ToolOptions emptyOptions() {
        return mockOptions(Collections.emptyMap());
    }

    private ToolOptions mockOptions(Map<String, String> options) {
        return mockOptions(options, Collections.emptySet());
    }

    private ToolOptions mockOptions(Map<String, String> options, Set<String> flags) {
        return new ToolOptions(mockCommandLine(options, flags), fileSystem);
    }

    @Test
    void parseInputs() throws IOException {
        ToolOptions options = emptyOptions();

        DynamicSecurityAnalysisExecutionInput input = new DynamicSecurityAnalysisExecutionInput();
        tool.updateInput(options, input);
        assertThat(input.getDynamicModelsSource()).isNull();
        assertThat(input.getViolationTypes()).isEmpty();
        assertThat(input.getResultExtensions()).isEmpty();
        assertThat(input.getContingenciesSource()).isNotPresent();

        options = mockOptions(Map.of(SecurityAnalysisToolConstants.LIMIT_TYPES_OPTION, "HIGH_VOLTAGE,CURRENT"));
        tool.updateInput(options, input);
        assertThat(input.getViolationTypes()).containsExactly(LimitViolationType.CURRENT, LimitViolationType.HIGH_VOLTAGE);

        options = mockOptions(Map.of(SecurityAnalysisToolConstants.WITH_EXTENSIONS_OPTION, "ext1,ext2"));
        tool.updateInput(options, input);
        assertThat(input.getResultExtensions()).containsExactly("ext1", "ext2");

        parseFile(input, DynamicSecurityAnalysisToolConstants.DYNAMIC_MODELS_FILE_OPTION, "dynamicModels", input::getDynamicModelsSource);
        parseOptionalFile(input, SecurityAnalysisToolConstants.CONTINGENCIES_FILE_OPTION, "contingencies", input::getContingenciesSource);
    }

    void parseOptionalFile(DynamicSecurityAnalysisExecutionInput input, String optionName, String fileName, Supplier<Optional<ByteSource>> getSource) throws IOException {
        ToolOptions invalidOptions = mockOptions(Map.of(optionName, fileName));
        assertThatIllegalArgumentException().isThrownBy(() -> tool.updateInput(invalidOptions, input));

        Files.write(fileSystem.getPath(fileName), "test".getBytes());
        ToolOptions options = mockOptions(Map.of(optionName, fileName));
        tool.updateInput(options, input);
        assertThat(getSource.get()).isPresent();
        if (getSource.get().isPresent()) {
            assertEquals("test", new String(getSource.get().get().read()));
        } else {
            fail();
        }
    }

    ToolOptions parseFile(DynamicSecurityAnalysisExecutionInput input, String optionName, String fileName, Supplier<ByteSource> getSource) throws IOException {
        ToolOptions invalidOptions = mockOptions(Map.of(optionName, fileName));
        assertThatIllegalArgumentException().isThrownBy(() -> tool.updateInput(invalidOptions, input));

        Files.write(fileSystem.getPath(fileName), "test".getBytes());
        ToolOptions options = mockOptions(Map.of(optionName, fileName));
        tool.updateInput(options, input);
        assertNotNull(getSource.get());
        assertEquals("test", new String(getSource.get().read()));
        return options;
    }

    @Test
    void buildPreprocessedInput() {
        DynamicSecurityAnalysisExecutionInput executionInput = new DynamicSecurityAnalysisExecutionInput()
                .setDynamicModelsSource(dynamicModels)
                .setNetworkVariant(mock(Network.class), "")
                .setParameters(new DynamicSecurityAnalysisParameters());

        SecurityAnalysisPreprocessor preprocessor = mock(SecurityAnalysisPreprocessor.class);
        SecurityAnalysisPreprocessorFactory factory = mock(SecurityAnalysisPreprocessorFactory.class);
        when(factory.newPreprocessor(any())).thenReturn(preprocessor);

        DynamicSecurityAnalysisInput input = DynamicSecurityAnalysisTool.buildPreprocessedInput(executionInput, "", LimitViolationFilter::new, factory);

        assertSame(executionInput.getParameters(), input.getParameters());
        assertSame(executionInput.getNetworkVariant(), input.getNetworkVariant());

        verify(factory, times(0)).newPreprocessor(any());

        executionInput.setContingenciesSource(ByteSource.empty());
        DynamicSecurityAnalysisTool.buildPreprocessedInput(executionInput, "", LimitViolationFilter::new, factory);

        verify(factory, times(1)).newPreprocessor(any());
        verify(preprocessor, times(1)).preprocess(any());
    }

    @Test
    void readNetwork() throws IOException {
        ToolRunningContext context = new ToolRunningContext(mock(PrintStream.class), mock(PrintStream.class), fileSystem,
                mock(ComputationManager.class), mock(ComputationManager.class));
        CommandLine cli = mockCommandLine(Map.of("case-file", "network.xml"), Collections.emptySet());
        Network network = DynamicSecurityAnalysisTool.readNetwork(cli, context, new ImportersLoaderList(new NetworkImporterMock()));
        assertNotNull(network);
    }

    @Test
    void testRunWithLog() throws Exception {
        try (ByteArrayOutputStream bout = new ByteArrayOutputStream();
             ByteArrayOutputStream berr = new ByteArrayOutputStream();
             PrintStream out = new PrintStream(bout);
             PrintStream err = new PrintStream(berr);
             ComputationManager cm = mock(ComputationManager.class)) {
            CommandLine cl = mockCommandLine(Map.of("case-file", "network.xml",
                    DynamicSecurityAnalysisToolConstants.DYNAMIC_MODELS_FILE_OPTION, DYNAMIC_MODEL_FILENAME,
                    SecurityAnalysisToolConstants.OUTPUT_LOG_OPTION, OUTPUT_LOG_FILENAME),
                    Set.of("skip-postproc"));

            ToolRunningContext context = new ToolRunningContext(out, err, fileSystem, cm, cm);

            DynamicSecurityAnalysisExecutionBuilder builderRun = new DynamicSecurityAnalysisExecutionBuilder(ExternalSecurityAnalysisConfig::new,
                    "DynamicSecurityAnalysisToolProviderMock",
                    (executionInput, providerName) -> new DynamicSecurityAnalysisInput(executionInput.getNetworkVariant(), dynamicModelsSupplier));

            // Check runWithLog execution
            tool.run(cl, context, builderRun,
                    new ImportersLoaderList(new NetworkImporterMock()),
                    TableFormatterConfig::new);
            // Check log-file creation
            Path logPath = context.getFileSystem().getPath(OUTPUT_LOG_FILENAME);
            assertTrue(Files.exists(logPath));
            // Need to clean for next test
            Files.delete(logPath);

            // Check run execution
            when(cl.hasOption("log-file")).thenReturn(false);

            tool.run(cl, context, builderRun,
                    new ImportersLoaderList(new NetworkImporterMock()),
                    TableFormatterConfig::new);

            // Check no log-file creation
            assertFalse(Files.exists(logPath));

            // exception happens
            DynamicSecurityAnalysisExecutionBuilder builderException = new DynamicSecurityAnalysisExecutionBuilder(ExternalSecurityAnalysisConfig::new,
                    "DynamicSecurityAnalysisToolExceptionProviderMock",
                    (executionInput, providerName) -> new DynamicSecurityAnalysisInput(executionInput.getNetworkVariant(), dynamicModelsSupplier));

            ImportersLoaderList importers = new ImportersLoaderList(new NetworkImporterMock());
            try {
                tool.run(cl, context, builderException,
                        importers,
                        TableFormatterConfig::new);
                fail();
            } catch (CompletionException exception) {
                assertInstanceOf(ComputationException.class, exception.getCause());
                assertEquals("outLog", ((ComputationException) exception.getCause()).getOutLogs().get("out"));
                assertEquals("errLog", ((ComputationException) exception.getCause()).getErrLogs().get("err"));
            }
        }
    }

    @Test
    void testRunWithBuilderCreation() throws Exception {

        try (ByteArrayOutputStream bout = new ByteArrayOutputStream();
             ByteArrayOutputStream berr = new ByteArrayOutputStream();
             PrintStream out = new PrintStream(bout);
             PrintStream err = new PrintStream(berr);
             ComputationManager cm = mock(ComputationManager.class)) {

            CommandLine cl = mockCommandLine(Map.of("case-file", "network.xml",
                    DynamicSecurityAnalysisToolConstants.DYNAMIC_MODELS_FILE_OPTION, "groovy",
                    SecurityAnalysisToolConstants.OUTPUT_LOG_OPTION, OUTPUT_LOG_FILENAME), Set.of("skip-postproc"));

            ToolRunningContext context = new ToolRunningContext(out, err, fileSystem, cm, cm);

            PowsyblException e = assertThrows(PowsyblException.class, () -> tool.run(cl, context));
            assertTrue(e.getMessage().startsWith("Property ContingenciesProviderFactory is not set"));
        }
    }

    @AutoService(DynamicSecurityAnalysisProvider.class)
    public static class DynamicSecurityAnalysisExceptionProviderMock implements DynamicSecurityAnalysisProvider {

        @Override
        public CompletableFuture<SecurityAnalysisReport> run(Network network, String workingVariantId, DynamicModelsSupplier dynamicModelsSupplier, ContingenciesProvider contingenciesProvider, DynamicSecurityAnalysisRunParameters runParameters) {
            ComputationExceptionBuilder ceb = new ComputationExceptionBuilder(new RuntimeException("test"));
            ceb.addOutLog("out", "outLog")
                    .addErrLog("err", "errLog");
            ComputationException computationException = ceb.build();
            throw new CompletionException(computationException);
        }

        @Override
        public String getName() {
            return "DynamicSecurityAnalysisToolExceptionProviderMock";
        }

        @Override
        public String getVersion() {
            return "1.0";
        }
    }
}