SecurityAnalysisExecutionHandlersTest.java

/**
 * Copyright (c) 2019, 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.security.distributed;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.io.ByteSource;
import com.google.common.jimfs.Configuration;
import com.google.common.jimfs.Jimfs;
import com.powsybl.computation.*;
import com.powsybl.contingency.Contingency;
import com.powsybl.contingency.ContingencyContext;
import com.powsybl.iidm.network.LimitType;
import com.powsybl.iidm.network.VariantManagerConstants;
import com.powsybl.iidm.network.test.EurostagTutorialExample1Factory;
import com.powsybl.loadflow.LoadFlowResult;
import com.powsybl.security.*;
import com.powsybl.action.Action;
import com.powsybl.action.SwitchAction;
import com.powsybl.security.condition.TrueCondition;
import com.powsybl.security.converter.JsonSecurityAnalysisResultExporter;
import com.powsybl.security.execution.SecurityAnalysisExecutionInput;
import com.powsybl.security.limitreduction.LimitReduction;
import com.powsybl.security.results.PostContingencyResult;
import com.powsybl.security.strategy.OperatorStrategy;
import org.apache.commons.lang3.SystemUtils;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.junit.jupiter.api.Assertions.*;

/**
 * @author Sylvain Leclerc {@literal <sylvain.leclerc at rte-france.com>}
 */
class SecurityAnalysisExecutionHandlersTest {

    private FileSystem fileSystem;
    private Path workingDir;

    @BeforeEach
    void createFileSystem() {
        fileSystem = Jimfs.newFileSystem(Configuration.unix());
        workingDir = fileSystem.getPath("/work");
    }

    @AfterEach
    void closeFileSystem() throws IOException {
        fileSystem.close();
    }

    @Test
    void forwardedBeforeWithPartialInput() throws IOException {
        SecurityAnalysisExecutionInput input = new SecurityAnalysisExecutionInput();
        input.setParameters(new SecurityAnalysisParameters());
        input.setNetworkVariant(EurostagTutorialExample1Factory.create(), VariantManagerConstants.INITIAL_VARIANT_ID);

        ExecutionHandler<SecurityAnalysisReport> handler = SecurityAnalysisExecutionHandlers.forwarded(input);

        List<CommandExecution> commandExecutions = handler.before(workingDir);

        assertEquals(1, commandExecutions.size());
        CommandExecution commandExecution = commandExecutions.get(0);
        assertEquals(1, commandExecution.getExecutionCount());
        SimpleCommand command = (SimpleCommand) commandExecution.getCommand();
        assertNotNull(command);

        String expectedDefaultProgram = SystemUtils.IS_OS_WINDOWS ? "itools.bat" : "itools";
        assertEquals(expectedDefaultProgram, command.getProgram());
        List<String> args = command.getArgs(0);
        assertThat(args).first().isEqualTo("security-analysis");
        assertThat(args.subList(1, args.size()))
                .containsExactlyInAnyOrder("--case-file=/work/network.xiidm",
                        "--parameters-file=/work/parameters.json",
                        "--output-file=/work/result.json",
                        "--output-format=JSON");

        assertThat(workingDir.resolve("network.xiidm")).exists();
        assertThat(workingDir.resolve("parameters.json")).exists();
    }

    @Test
    void forwardedAfter() throws IOException {

        try (Writer writer = Files.newBufferedWriter(workingDir.resolve("result.json"))) {
            new JsonSecurityAnalysisResultExporter().export(SecurityAnalysisResult.empty(), writer);
        }

        ExecutionHandler<SecurityAnalysisReport> handler = SecurityAnalysisExecutionHandlers.forwarded(new SecurityAnalysisExecutionInput());
        SecurityAnalysisReport report = handler.after(workingDir, new DefaultExecutionReport(workingDir));
        SecurityAnalysisResult result = report.getResult();

        assertNotNull(result);
        assertSame(LoadFlowResult.ComponentResult.Status.CONVERGED, result.getPreContingencyResult().getStatus());
        assertTrue(result.getPreContingencyLimitViolationsResult().getLimitViolations().isEmpty());
        assertTrue(result.getPostContingencyResults().isEmpty());
    }

    @Test
    void forwardedBeforeWithCompleteInput() throws IOException {
        Action action = new SwitchAction("action", "switch", false);
        OperatorStrategy strategy = new OperatorStrategy("strat", ContingencyContext.specificContingency("cont"), new TrueCondition(), List.of("action"));
        LimitReduction limitReduction = new LimitReduction(LimitType.CURRENT, 0.9);

        SecurityAnalysisExecutionInput input = new SecurityAnalysisExecutionInput()
                .setParameters(new SecurityAnalysisParameters())
                .setNetworkVariant(EurostagTutorialExample1Factory.create(), VariantManagerConstants.INITIAL_VARIANT_ID)
                .setContingenciesSource(ByteSource.wrap("contingencies definition".getBytes(StandardCharsets.UTF_8)))
                .addResultExtensions(ImmutableList.of("ext1", "ext2"))
                .addViolationTypes(ImmutableList.of(LimitViolationType.CURRENT))
                .setActions(List.of(action))
                .setOperatorStrategies(List.of(strategy))
                .setLimitReductions(List.of(limitReduction));
        ExecutionHandler<SecurityAnalysisReport> handler = SecurityAnalysisExecutionHandlers.forwarded(input, 12);

        Path workingDir = fileSystem.getPath("/work");
        List<CommandExecution> commandExecutions = handler.before(workingDir);
        SimpleCommand command = (SimpleCommand) commandExecutions.get(0).getCommand();
        List<String> args = command.getArgs(0);
        assertThat(args.subList(1, args.size()))
                .containsExactlyInAnyOrder("--case-file=/work/network.xiidm",
                        "--parameters-file=/work/parameters.json",
                        "--output-file=/work/result.json",
                        "--output-format=JSON",
                        "--contingencies-file=/work/contingencies.groovy",
                        "--actions-file=/work/actions.json",
                        "--strategies-file=/work/strategies.json",
                        "--limit-reductions-file=/work/limit-reductions.json",
                        "--with-extensions=ext1,ext2",
                        "--limit-types=CURRENT",
                        "--task-count=12");

        assertThat(workingDir.resolve("network.xiidm")).exists();
        assertThat(workingDir.resolve("parameters.json")).exists();
        assertThat(workingDir.resolve("contingencies.groovy")).exists();
        assertThat(workingDir.resolve("strategies.json")).exists();
        assertThat(workingDir.resolve("actions.json")).exists();
        assertThat(workingDir.resolve("limit-reductions.json")).exists();
    }

    @Test
    void distributedBefore() throws IOException {
        SecurityAnalysisExecutionInput input = new SecurityAnalysisExecutionInput()
                .setParameters(new SecurityAnalysisParameters())
                .setNetworkVariant(EurostagTutorialExample1Factory.create(), VariantManagerConstants.INITIAL_VARIANT_ID)
                .setContingenciesSource(ByteSource.wrap("contingencies definition".getBytes(StandardCharsets.UTF_8)))
                .addResultExtensions(ImmutableList.of("ext1", "ext2"))
                .addViolationTypes(ImmutableList.of(LimitViolationType.CURRENT));
        ExecutionHandler<SecurityAnalysisReport> handler = SecurityAnalysisExecutionHandlers.distributed(input, 3);

        List<CommandExecution> commandExecutions = handler.before(workingDir);
        SimpleCommand command = (SimpleCommand) commandExecutions.get(0).getCommand();
        List<String> args = command.getArgs(0);
        assertThat(command.getArgs(0).subList(1, args.size()))
                .containsExactlyInAnyOrder("--case-file=/work/network.xiidm",
                        "--parameters-file=/work/parameters.json",
                        "--output-file=/work/task_0_result.json",
                        "--output-format=JSON",
                        "--contingencies-file=/work/contingencies.groovy",
                        "--with-extensions=ext1,ext2",
                        "--limit-types=CURRENT",
                        "--task=1/3");

        assertThat(command.getArgs(1).subList(1, args.size()))
                .containsExactlyInAnyOrder("--case-file=/work/network.xiidm",
                        "--parameters-file=/work/parameters.json",
                        "--output-file=/work/task_1_result.json",
                        "--output-format=JSON",
                        "--contingencies-file=/work/contingencies.groovy",
                        "--with-extensions=ext1,ext2",
                        "--limit-types=CURRENT",
                        "--task=2/3");
    }

    @Test
    void distributedBeforeWithLog() throws IOException {
        SecurityAnalysisExecutionInput input = new SecurityAnalysisExecutionInput()
                .setParameters(new SecurityAnalysisParameters())
                .setNetworkVariant(EurostagTutorialExample1Factory.create(), VariantManagerConstants.INITIAL_VARIANT_ID)
                .setContingenciesSource(ByteSource.wrap("contingencies definition".getBytes(StandardCharsets.UTF_8)))
                .setWithLogs(true);
        ExecutionHandler<SecurityAnalysisReport> handler = SecurityAnalysisExecutionHandlers.distributed(input, 3);

        List<CommandExecution> commandExecutions = handler.before(workingDir);
        SimpleCommand command = (SimpleCommand) commandExecutions.get(0).getCommand();
        List<String> args = command.getArgs(0);
        assertThat(command.getArgs(0).subList(1, args.size()))
                .containsExactlyInAnyOrder("--case-file=/work/network.xiidm",
                        "--parameters-file=/work/parameters.json",
                        "--output-file=/work/task_0_result.json",
                        "--output-format=JSON",
                        "--contingencies-file=/work/contingencies.groovy",
                        "--task=1/3",
                        "--log-file=/work/logs_0.zip");

        assertThat(command.getArgs(1).subList(1, args.size()))
                .containsExactlyInAnyOrder("--case-file=/work/network.xiidm",
                        "--parameters-file=/work/parameters.json",
                        "--output-file=/work/task_1_result.json",
                        "--output-format=JSON",
                        "--contingencies-file=/work/contingencies.groovy",
                        "--task=2/3",
                        "--log-file=/work/logs_1.zip");
    }

    @Test
    void forwardedBeforeWithLog() throws IOException {
        SecurityAnalysisExecutionInput input = new SecurityAnalysisExecutionInput()
                .setParameters(new SecurityAnalysisParameters())
                .setNetworkVariant(EurostagTutorialExample1Factory.create(), VariantManagerConstants.INITIAL_VARIANT_ID)
                .setContingenciesSource(ByteSource.wrap("contingencies definition".getBytes(StandardCharsets.UTF_8)))
                .setWithLogs(true);
        ExecutionHandler<SecurityAnalysisReport> handler = SecurityAnalysisExecutionHandlers.forwarded(input);

        List<CommandExecution> commandExecutions = handler.before(workingDir);
        SimpleCommand command = (SimpleCommand) commandExecutions.get(0).getCommand();
        List<String> args = command.getArgs(0);
        assertThat(command.getArgs(0).subList(1, args.size()))
                .containsExactlyInAnyOrder("--case-file=/work/network.xiidm",
                        "--parameters-file=/work/parameters.json",
                        "--output-file=/work/result.json",
                        "--output-format=JSON",
                        "--contingencies-file=/work/contingencies.groovy",
                        "--log-file=/work/logs.zip");
    }

    private static SecurityAnalysisResult resultForContingency(String id) {
        return new SecurityAnalysisResult(LimitViolationsResult.empty(), LoadFlowResult.ComponentResult.Status.CONVERGED,
                Collections.singletonList(new PostContingencyResult(new Contingency(id), PostContingencyComputationStatus.CONVERGED,
                        LimitViolationsResult.empty())));
    }

    @Test
    void distributedAfter() throws IOException {
        JsonSecurityAnalysisResultExporter exporter = new JsonSecurityAnalysisResultExporter();
        try (Writer writer = Files.newBufferedWriter(workingDir.resolve("task_0_result.json"))) {
            exporter.export(resultForContingency("c1"), writer);
        }

        SecurityAnalysisExecutionInput input = new SecurityAnalysisExecutionInput();

        ExecutionHandler<SecurityAnalysisReport> handler3 = SecurityAnalysisExecutionHandlers.distributed(input, 2);
        assertThatExceptionOfType(ComputationException.class).isThrownBy(() -> {
            Command cmd = Mockito.mock(Command.class);
            handler3.after(workingDir, new DefaultExecutionReport(workingDir, Collections.singletonList(new ExecutionError(cmd, 0, 42))));
        })
            .withMessageContaining("An error occurred during security analysis command execution")
            .withStackTraceContaining("Error during the execution in directory  /work exit codes: Task 0 : 42");

        try (Writer writer = Files.newBufferedWriter(workingDir.resolve("task_1_result.json"))) {
            exporter.export(resultForContingency("c2"), writer);
        }

        ExecutionHandler<SecurityAnalysisReport> handler = SecurityAnalysisExecutionHandlers.distributed(input, 2);
        SecurityAnalysisReport report = handler.after(workingDir, new DefaultExecutionReport(workingDir));
        SecurityAnalysisResult result = report.getResult();

        assertNotNull(result);
        assertSame(LoadFlowResult.ComponentResult.Status.CONVERGED, result.getPreContingencyResult().getStatus());
        assertTrue(result.getPreContingencyLimitViolationsResult().getLimitViolations().isEmpty());
        assertEquals(2, result.getPostContingencyResults().size());
        assertEquals("c1", result.getPostContingencyResults().get(0).getContingency().getId());
        assertEquals("c2", result.getPostContingencyResults().get(1).getContingency().getId());
    }

    private static Set<String> getFileNamesFromZip(byte[] bytes) throws IOException {
        Set<String> foundNames = new HashSet<>();
        try (ZipInputStream zip = new ZipInputStream(new ByteArrayInputStream(bytes))) {

            ZipEntry entry = zip.getNextEntry();
            while (entry != null) {
                foundNames.add(entry.getName());
                entry = zip.getNextEntry();
            }
        }
        return foundNames;
    }

    @Test
    void distributedAfterWithLogs() throws IOException {
        JsonSecurityAnalysisResultExporter exporter = new JsonSecurityAnalysisResultExporter();

        Set<String> expectedLogs = ImmutableSet.of("logs_0.zip",
                "security-analysis-task_0.out",
                "security-analysis-task_0.err",
                "logs_1.zip",
                "security-analysis-task_1.out",
                "security-analysis-task_1.err");
        for (String logFileName : expectedLogs) {
            Files.writeString(workingDir.resolve(logFileName), "logs");
        }

        SecurityAnalysisExecutionInput input = new SecurityAnalysisExecutionInput()
                .setWithLogs(true);
        ExecutionHandler<SecurityAnalysisReport> handler2 = SecurityAnalysisExecutionHandlers.distributed(input, 2);
        try {
            handler2.after(workingDir, new DefaultExecutionReport(workingDir));
            fail();
        } catch (ComputationException ce) {
            assertEquals("logs", ce.getErrLogs().get("security-analysis-task_0.err"));
            assertEquals("logs", ce.getErrLogs().get("security-analysis-task_1.err"));
            assertEquals("logs", ce.getOutLogs().get("security-analysis-task_0.out"));
            assertEquals("logs", ce.getOutLogs().get("security-analysis-task_1.out"));
        }

        try {
            Command cmd = Mockito.mock(Command.class);
            handler2.after(workingDir, new DefaultExecutionReport(workingDir, Collections.singletonList(new ExecutionError(cmd, 0, 42))));
            fail();
        } catch (Exception e) {
            // ignored
            assertInstanceOf(ComputationException.class, e);
        }

        try (Writer writer = Files.newBufferedWriter(workingDir.resolve("task_0_result.json"))) {
            exporter.export(resultForContingency("c1"), writer);
        }

        try (Writer writer = Files.newBufferedWriter(workingDir.resolve("task_1_result.json"))) {
            exporter.export(resultForContingency("c2"), writer);
        }
        ExecutionHandler<SecurityAnalysisReport> handler = SecurityAnalysisExecutionHandlers.distributed(input, 2);

        SecurityAnalysisReport report = handler.after(workingDir, new DefaultExecutionReport(workingDir));
        SecurityAnalysisResult result = report.getResult();

        assertNotNull(result);
        assertSame(LoadFlowResult.ComponentResult.Status.CONVERGED, result.getPreContingencyResult().getStatus());
        assertTrue(result.getPreContingencyLimitViolationsResult().getLimitViolations().isEmpty());
        assertEquals(2, result.getPostContingencyResults().size());
        assertEquals("c1", result.getPostContingencyResults().get(0).getContingency().getId());
        assertEquals("c2", result.getPostContingencyResults().get(1).getContingency().getId());

        byte[] logBytes = report.getLogBytes()
                .orElseThrow(IllegalStateException::new);
        Set<String> foundNames = getFileNamesFromZip(logBytes);
        assertEquals(expectedLogs, foundNames);
    }

    @Test
    void forwardedAfterWithLogs() throws IOException {
        JsonSecurityAnalysisResultExporter exporter = new JsonSecurityAnalysisResultExporter();

        Set<String> expectedLogs = ImmutableSet.of("logs.zip",
                "security-analysis.out",
                "security-analysis.err");

        for (String logFileName : expectedLogs) {
            Files.writeString(workingDir.resolve(logFileName), "logs");
        }

        SecurityAnalysisExecutionInput input = new SecurityAnalysisExecutionInput()
                .setWithLogs(true);

        ExecutionHandler<SecurityAnalysisReport> handler2 = SecurityAnalysisExecutionHandlers.forwarded(input, 2);

        assertThatExceptionOfType(ComputationException.class)
                .isThrownBy(() -> handler2.after(workingDir, new DefaultExecutionReport(workingDir)))
                .withStackTraceContaining("NoSuchFile")
                .withStackTraceContaining("result.json")
                .satisfies(ce -> {
                    assertEquals("logs", ce.getErrLogs().get("security-analysis.err"));
                    assertEquals("logs", ce.getOutLogs().get("security-analysis.out"));
                });

        ExecutionHandler<SecurityAnalysisReport> handler = SecurityAnalysisExecutionHandlers.forwarded(input, 2);

        try (Writer writer = Files.newBufferedWriter(workingDir.resolve("result.json"))) {
            exporter.export(resultForContingency("c1"), writer);
        }
        SecurityAnalysisReport report = handler.after(workingDir, new DefaultExecutionReport(workingDir));
        SecurityAnalysisResult result = report.getResult();

        assertNotNull(result);
        assertSame(LoadFlowResult.ComponentResult.Status.CONVERGED, result.getPreContingencyResult().getStatus());
        assertTrue(result.getPreContingencyLimitViolationsResult().getLimitViolations().isEmpty());
        assertEquals(1, result.getPostContingencyResults().size());
        assertEquals("c1", result.getPostContingencyResults().get(0).getContingency().getId());

        assertTrue(report.getLogBytes().isPresent());

        byte[] logBytes = report.getLogBytes()
                .orElseThrow(IllegalStateException::new);
        Set<String> foundNames = getFileNamesFromZip(logBytes);
        assertEquals(expectedLogs, foundNames);
    }

}