TimeSeriesStoreUtilsTest.java

/*
 * Copyright (c) 2021, 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.metrix.mapping.timeseries;

import com.google.common.collect.ImmutableSortedSet;
import com.google.common.jimfs.Configuration;
import com.google.common.jimfs.Jimfs;
import com.powsybl.timeseries.*;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.io.Writer;
import java.nio.ByteBuffer;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.function.IntFunction;

import static com.powsybl.metrix.mapping.AbstractCompareTxt.compareStreamTxt;
import static com.powsybl.metrix.mapping.timeseries.FileSystemTimeSeriesStore.ExistingFilePolicy.APPEND;
import static com.powsybl.metrix.mapping.timeseries.TimeSeriesStoreUtil.isNotVersioned;
import static com.powsybl.metrix.mapping.timeseries.TimeSeriesStoreUtil.toTable;
import static com.powsybl.timeseries.TimeSeries.DEFAULT_VERSION_NUMBER_FOR_UNVERSIONED_TIMESERIES;
import static org.junit.jupiter.api.Assertions.*;

/**
 * @author Paul Bui-Quang {@literal <paul.buiquang at rte-france.com>}
 */
class TimeSeriesStoreUtilsTest {
    private FileSystem fileSystem;

    @BeforeEach
    public void setUp() {
        this.fileSystem = Jimfs.newFileSystem(Configuration.unix());
    }

    @AfterEach
    public void tearDown() throws IOException {
        this.fileSystem.close();
    }

    @Test
    void export() throws IOException {
        Path output = fileSystem.getPath("output.csv");

        Instant now = Instant.ofEpochMilli(978303600000L);
        RegularTimeSeriesIndex index = RegularTimeSeriesIndex.create(now, now.plus(2, ChronoUnit.HOURS), Duration.ofHours(1));
        StoredDoubleTimeSeries ts1 = TimeSeries.createDouble("ts1", index, 1d, 2d, 3d);
        StoredDoubleTimeSeries ts2 = TimeSeries.createDouble("ts2", index, 1d, 3d, 5d);
        ReadOnlyTimeSeriesStore store = new ReadOnlyTimeSeriesStoreCache(ts1, ts2);

        try (Writer writer = Files.newBufferedWriter(output)) {
            TimeSeriesStoreUtil.writeCsv(store, writer, ';', ZoneId.of(ZoneOffset.UTC.getId()), ImmutableSortedSet.of(1), ImmutableSortedSet.of("ts1", "ts2"));
        } catch (IOException e) {
            fail();
        }

        try (InputStream expected = getClass().getResourceAsStream("/expected/simpleExport.csv")) {
            try (InputStream actual = Files.newInputStream(output)) {
                compareStreamTxt(expected, actual);
            } catch (UncheckedIOException ex) {
                fail();
            }
        }
    }

    @Test
    void notVersionedSingleNumberTest() {
        assertTrue(isNotVersioned(Set.of(DEFAULT_VERSION_NUMBER_FOR_UNVERSIONED_TIMESERIES)));
        assertFalse(isNotVersioned(Set.of(1)));
        assertFalse(isNotVersioned(Set.of(DEFAULT_VERSION_NUMBER_FOR_UNVERSIONED_TIMESERIES, 1)));
    }

    @Test
    void testToTable() throws IOException {
        // TimeSeries index
        Instant now = Instant.ofEpochMilli(978303600000L);
        RegularTimeSeriesIndex index = RegularTimeSeriesIndex.create(now, now.plus(2, ChronoUnit.HOURS), Duration.ofHours(1));

        // TimeSeries
        StoredDoubleTimeSeries ts1 = TimeSeries.createDouble("ts1", index, 1d, 2d, 3d);
        StringTimeSeries ts3 = TimeSeries.createString("ts3", index, "a", "b", "c");

        // TimeSeries metadata
        TimeSeriesMetadata ts1Metadata = new TimeSeriesMetadata("ts1", TimeSeriesDataType.DOUBLE, index);
        TimeSeriesMetadata ts2Metadata = new TimeSeriesMetadata("ts2", TimeSeriesDataType.STRING, index);

        // TimeSeriesStore
        Path resDir = Files.createDirectory(fileSystem.getPath("/tmp"));
        FileSystemTimeSeriesStore tsStore = new FileSystemTimeSeriesStore(resDir);
        tsStore.importTimeSeries(List.of(ts1, ts3), 1, APPEND);
        tsStore.importTimeSeries(List.of(ts1, ts3), 2, APPEND);

        // Versions
        NavigableSet<Integer> versions = new TreeSet<>(List.of(1, 2));

        // Useful variables
        List<String> fileNames = new ArrayList<>();
        TimeSeriesTable table;
        IntFunction<ByteBuffer> byteBufferAllocator = size -> {
            String fileName = "csv_export_" + UUID.randomUUID();
            ByteBuffer buffer = MmapByteBufferService.INSTANCE.create(fileName, size);
            fileNames.add(fileName);
            return buffer;
        };

        // Case 1: one DoubleTimeSeries
        table = toTable(tsStore, byteBufferAllocator, versions, List.of(ts1Metadata), Set.of("ts1"), new HashSet<>());
        assertEquals(index, table.getTableIndex());
        assertIterableEquals(List.of("ts1"), table.getTimeSeriesNames());
        assertEquals(0, table.getDoubleTimeSeriesIndex("ts1"));
        assertEquals(1d, table.getDoubleValue(1, 0, 0));
        assertEquals(2d, table.getDoubleValue(1, 0, 1));
        assertEquals(3d, table.getDoubleValue(1, 0, 2));
        assertEquals(1, fileNames.size());

        // Case 2: one StringTimeSeries
        table = toTable(tsStore, byteBufferAllocator, versions, List.of(ts2Metadata), new HashSet<>(), Set.of("ts3"));
        assertEquals(index, table.getTableIndex());
        assertIterableEquals(List.of("ts3"), table.getTimeSeriesNames());
        assertEquals(0, table.getStringTimeSeriesIndex("ts3"));
        assertEquals("a", table.getStringValue(1, 0, 0));
        assertEquals("b", table.getStringValue(1, 0, 1));
        assertEquals("c", table.getStringValue(1, 0, 2));
        assertEquals(2, fileNames.size());
    }

    @Test
    void testToTableExceptions() throws IOException {
        // TimeSeries indexes
        Instant now = Instant.ofEpochMilli(978303600000L);
        RegularTimeSeriesIndex index = RegularTimeSeriesIndex.create(now, now.plus(2, ChronoUnit.HOURS), Duration.ofHours(1));
        RegularTimeSeriesIndex otherIndex = RegularTimeSeriesIndex.create(now, now.plus(1, ChronoUnit.HOURS), Duration.ofMinutes(30));
        InfiniteTimeSeriesIndex infiniteIndex = new InfiniteTimeSeriesIndex();

        // TimeSeries
        StoredDoubleTimeSeries ts1 = TimeSeries.createDouble("ts1", index, 1d, 2d, 3d);
        StoredDoubleTimeSeries ts2 = TimeSeries.createDouble("ts2", infiniteIndex, 1d, 5d);
        StoredDoubleTimeSeries ts4 = TimeSeries.createDouble("ts4", otherIndex, 1d, 3d, 5d);

        // TimeSeries metadata
        TimeSeriesMetadata ts1Metadata = new TimeSeriesMetadata("ts1", TimeSeriesDataType.DOUBLE, index);
        TimeSeriesMetadata ts2Metadata = new TimeSeriesMetadata("ts3", TimeSeriesDataType.DOUBLE, infiniteIndex);
        TimeSeriesMetadata ts4Metadata = new TimeSeriesMetadata("ts4", TimeSeriesDataType.DOUBLE, otherIndex);

        // TimeSeriesStore
        Path resDir = Files.createDirectory(fileSystem.getPath("/tmp"));
        FileSystemTimeSeriesStore tsStore = new FileSystemTimeSeriesStore(resDir);
        tsStore.importTimeSeries(List.of(ts1, ts2, ts4), 1, APPEND);
        tsStore.importTimeSeries(List.of(ts1, ts2, ts4), 2, APPEND);

        // Versions
        NavigableSet<Integer> versions = new TreeSet<>(List.of(1, 2));

        // Useful variable
        List<String> fileNames = new ArrayList<>();

        IntFunction<ByteBuffer> byteBufferAllocator = size -> {
            String fileName = "csv_export_" + UUID.randomUUID();
            ByteBuffer buffer = MmapByteBufferService.INSTANCE.create(fileName, size);
            fileNames.add(fileName);
            return buffer;
        };

        // Case 1: different indexes
        List<TimeSeriesMetadata> timeSeriesMetadataListCase1 = List.of(ts1Metadata, ts4Metadata);
        Set<String> doubleTimeSeriesNamesCase1 = Set.of("ts1", "ts4");
        Set<String> stringTimeSeriesNamesCase1 = new HashSet<>();
        TimeSeriesException timeSeriesException = assertThrows(TimeSeriesException.class,
            () -> toTable(tsStore, byteBufferAllocator, versions, timeSeriesMetadataListCase1, doubleTimeSeriesNamesCase1, stringTimeSeriesNamesCase1));
        assertEquals("Impossible to write CSV because index is not unique", timeSeriesException.getMessage());

        // Case 2: Infinite index
        List<TimeSeriesMetadata> timeSeriesMetadataListCase2 = List.of(ts1Metadata, ts2Metadata);
        Set<String> doubleTimeSeriesNamesCase2 = Set.of("ts1", "ts2");
        Set<String> stringTimeSeriesNamesCase2 = new HashSet<>();
        UnsupportedOperationException unsupportedOperationException = assertThrows(UnsupportedOperationException.class,
            () -> toTable(tsStore, byteBufferAllocator, versions, timeSeriesMetadataListCase2, doubleTimeSeriesNamesCase2, stringTimeSeriesNamesCase2));
        assertEquals("Not yet implemented", unsupportedOperationException.getMessage());
        assertEquals(1, fileNames.size()); // a file name was created since the exception comes late in the process
    }
}