This is an automated email from the ASF dual-hosted git repository.

zehnder pushed a commit to branch 
2964-timestamp-conversion-is-broken-in-adapters
in repository https://gitbox.apache.org/repos/asf/streampipes.git


The following commit(s) were added to 
refs/heads/2964-timestamp-conversion-is-broken-in-adapters by this push:
     new 8b80cddbf3 chore(#2964): Refactor FileReplayAdapter and add unit tests
8b80cddbf3 is described below

commit 8b80cddbf3857c76dd6493c018b7fd5708338ee3
Author: Philipp Zehnder <[email protected]>
AuthorDate: Fri Jun 28 10:38:39 2024 +0200

    chore(#2964): Refactor FileReplayAdapter and add unit tests
---
 .../iiot/protocol/stream/FileReplayAdapter.java    | 268 +++++++++++++--------
 .../protocol/stream/FileReplayAdapterTest.java     | 105 ++++++++
 ui/cypress/tests/adapter/rules/valueRules.spec.ts  |  12 +-
 3 files changed, 274 insertions(+), 111 deletions(-)

diff --git 
a/streampipes-extensions/streampipes-connect-adapters-iiot/src/main/java/org/apache/streampipes/connect/iiot/protocol/stream/FileReplayAdapter.java
 
b/streampipes-extensions/streampipes-connect-adapters-iiot/src/main/java/org/apache/streampipes/connect/iiot/protocol/stream/FileReplayAdapter.java
index 6ca6217553..8ed3697a3c 100644
--- 
a/streampipes-extensions/streampipes-connect-adapters-iiot/src/main/java/org/apache/streampipes/connect/iiot/protocol/stream/FileReplayAdapter.java
+++ 
b/streampipes-extensions/streampipes-connect-adapters-iiot/src/main/java/org/apache/streampipes/connect/iiot/protocol/stream/FileReplayAdapter.java
@@ -22,6 +22,7 @@ import 
org.apache.streampipes.commons.exceptions.connect.AdapterException;
 import org.apache.streampipes.connect.iiot.utils.FileProtocolUtils;
 import org.apache.streampipes.extensions.api.connect.IAdapterConfiguration;
 import org.apache.streampipes.extensions.api.connect.IEventCollector;
+import org.apache.streampipes.extensions.api.connect.IParser;
 import org.apache.streampipes.extensions.api.connect.StreamPipesAdapter;
 import 
org.apache.streampipes.extensions.api.connect.context.IAdapterGuessSchemaContext;
 import 
org.apache.streampipes.extensions.api.connect.context.IAdapterRuntimeContext;
@@ -50,6 +51,7 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.net.URI;
 import java.util.List;
+import java.util.Map;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
@@ -76,6 +78,7 @@ public class FileReplayAdapter implements StreamPipesAdapter {
   private boolean replaceTimestamp;
   private String timestampRuntimeName;
 
+
   private String timestampSourceFieldName;
 
   private float speedUp;
@@ -85,39 +88,40 @@ public class FileReplayAdapter implements 
StreamPipesAdapter {
   @Override
   public IAdapterConfiguration declareConfig() {
     return AdapterConfigurationBuilder.create(ID, 0, FileReplayAdapter::new)
-        .withSupportedParsers(
-            new JsonParsers(),
-            new CsvParser(),
-            new XmlParser(),
-            new ImageParser()
-        )
-        .withAssets(ExtensionAssetType.DOCUMENTATION, ExtensionAssetType.ICON)
-        .withLocales(Locales.EN)
-        .withCategory(AdapterType.Generic)
-        .requiredFile(
-            Labels.withId(FILE_PATH),
-            Filetypes.CSV,
-            Filetypes.JSON,
-            Filetypes.XML
-        )
-        .requiredMultiValueSelection(Labels.withId(REPLACE_TIMESTAMP), 
Options.from(""))
-        .requiredSingleValueSelection(
-            Labels.withId(REPLAY_ONCE),
-            Options.from("no", "yes")
-        )
-        .requiredAlternatives(Labels.withId(SPEED),
-            Alternatives.from(Labels.withId(KEEP_ORIGINAL_TIME), true),
-            Alternatives.from(Labels.withId(FASTEST)),
-            Alternatives.from(
-                Labels.withId(SPEED_UP_FACTOR),
-                StaticProperties.group(
-                    Labels.withId(SPEED_UP_FACTOR_GROUP),
-                    StaticProperties.doubleFreeTextProperty(Labels.withId(
-                        SPEED_UP))
-                )
-            )
-        )
-        .buildConfiguration();
+                                      .withSupportedParsers(
+                                          new JsonParsers(),
+                                          new CsvParser(),
+                                          new XmlParser(),
+                                          new ImageParser()
+                                      )
+                                      
.withAssets(ExtensionAssetType.DOCUMENTATION, ExtensionAssetType.ICON)
+                                      .withLocales(Locales.EN)
+                                      .withCategory(AdapterType.Generic)
+                                      .requiredFile(
+                                          Labels.withId(FILE_PATH),
+                                          Filetypes.CSV,
+                                          Filetypes.JSON,
+                                          Filetypes.XML
+                                      )
+                                      
.requiredMultiValueSelection(Labels.withId(REPLACE_TIMESTAMP), Options.from(""))
+                                      .requiredSingleValueSelection(
+                                          Labels.withId(REPLAY_ONCE),
+                                          Options.from("no", "yes")
+                                      )
+                                      .requiredAlternatives(
+                                          Labels.withId(SPEED),
+                                          
Alternatives.from(Labels.withId(KEEP_ORIGINAL_TIME), true),
+                                          
Alternatives.from(Labels.withId(FASTEST)),
+                                          Alternatives.from(
+                                              Labels.withId(SPEED_UP_FACTOR),
+                                              StaticProperties.group(
+                                                  
Labels.withId(SPEED_UP_FACTOR_GROUP),
+                                                  
StaticProperties.doubleFreeTextProperty(Labels.withId(
+                                                      SPEED_UP))
+                                              )
+                                          )
+                                      )
+                                      .buildConfiguration();
   }
 
   @Override
@@ -155,15 +159,15 @@ public class FileReplayAdapter implements 
StreamPipesAdapter {
         .getEventProperties()
         .stream()
         .filter(eventProperty ->
-            eventProperty.getDomainProperties()
-                .contains(URI.create(SO.DATE_TIME)))
+                    eventProperty.getDomainProperties()
+                                 .contains(URI.create(SO.DATE_TIME)))
         .findFirst();
 
     if (timestampField.isEmpty()) {
       throw new AdapterException("Could not find a timestamp field in event 
schema");
     } else {
       timestampRuntimeName = timestampField.get()
-          .getRuntimeName();
+                                           .getRuntimeName();
     }
 
     determineSourceTimestampField(extractor);
@@ -171,13 +175,13 @@ public class FileReplayAdapter implements 
StreamPipesAdapter {
     // start replay adapter
     if (replayOnce) {
       executor.schedule(
-          () -> parseFile(extractor, collector, adapterRuntimeContext),
+          () -> getFileFromEndpointAndParseFile(extractor, collector, 
adapterRuntimeContext),
           0,
           TimeUnit.SECONDS
       );
     } else {
       executor.scheduleAtFixedRate(
-          () -> parseFile(extractor, collector, adapterRuntimeContext),
+          () -> getFileFromEndpointAndParseFile(extractor, collector, 
adapterRuntimeContext),
           0,
           1,
           TimeUnit.SECONDS
@@ -199,10 +203,10 @@ public class FileReplayAdapter implements 
StreamPipesAdapter {
     if (!renamingRulesTimestamp.isEmpty()) {
       if (renamingRulesTimestamp.size() == 1) {
         timestampSourceFieldName = renamingRulesTimestamp.get(0)
-            .getOldRuntimeKey();
+                                                         .getOldRuntimeKey();
       } else {
         throw new AdapterException("Invalid configuration - multiple renaming 
rules detected which affect the "
-            + "timestamp property.");
+                                       + "timestamp property.");
       }
     } else {
       // no renaming rules can be found, timestamp name does not change
@@ -219,79 +223,120 @@ public class FileReplayAdapter implements 
StreamPipesAdapter {
   private List<RenameRuleDescription> 
getRenamingRulesTimestamp(IAdapterParameterExtractor extractor) {
     var renamingRules =
         extractor.getAdapterDescription()
-            .getRules()
-            .stream()
-            .filter(rule -> rule instanceof RenameRuleDescription)
-            .map(rule -> (RenameRuleDescription) rule)
-            .toList();
+                 .getRules()
+                 .stream()
+                 .filter(rule -> rule instanceof RenameRuleDescription)
+                 .map(rule -> (RenameRuleDescription) rule)
+                 .toList();
 
     return renamingRules.stream()
-        .filter(rule -> rule.getNewRuntimeKey()
-            .equals(timestampRuntimeName))
-        .toList();
+                        .filter(rule -> rule.getNewRuntimeKey()
+                                            .equals(timestampRuntimeName))
+                        .toList();
   }
 
-  private void parseFile(
+  private void getFileFromEndpointAndParseFile(
       IAdapterParameterExtractor extractor,
       IEventCollector collector,
       IAdapterRuntimeContext adapterRuntimeContext
   ) {
     try {
-      InputStream inputStream = getDataFromEndpoint(extractor
-          .getStaticPropertyExtractor()
-          .selectedFilename(FILE_PATH));
-
-      extractor.selectedParser()
-          .parse(inputStream, (event) -> {
-
-            long actualEventTimestamp = -1;
-            var timestampFieldValue = event.get(timestampSourceFieldName);
-            if (timestampFieldValue instanceof Long) {
-              actualEventTimestamp = (Long) timestampFieldValue;
-            } else if (timestampFieldValue instanceof Integer) {
-              actualEventTimestamp = (Integer) timestampFieldValue;
-            } else if (!(timestampFieldValue == null && replaceTimestamp)){
-              adapterRuntimeContext
-                  .getLogger()
-                  .error(
-                      new AdapterException("Timestamp field is not a unix 
timestamp in ms, skipping event. Value: %s"
-                          .formatted(event.get(timestampSourceFieldName))
-                      ));
-            }
-
-            if (actualEventTimestamp == -1 && !replaceTimestamp) {
-              // Do not emit any data if timestamp could not be processed
-              return;
-            }
-
-            long sleepTime;
-            if (timestampLastEvent != -1 && actualEventTimestamp != -1) {
-              sleepTime = (long) ((actualEventTimestamp - timestampLastEvent) 
/ speedUp);
-            } else {
-              sleepTime = 1;
-            }
-            // speed up is set to Float.MAX_VALUE when user selected fastest 
option
-            if (sleepTime > 0 && speedUp != Float.MAX_VALUE) {
-              try {
-                Thread.sleep(sleepTime);
-              } catch (InterruptedException e) {
-                LOG.info("File stream adapter was stopped, the current replay 
is interrupted", e);
-              }
-            }
-
-            timestampLastEvent = actualEventTimestamp;
-            if (replaceTimestamp) {
-              event.put(timestampSourceFieldName, System.currentTimeMillis());
-            }
-
-            collector.collect(event);
-          });
+      var inputStream = getFileAsInputStreamFromEndpoint(extractor);
+
+      parseFile(
+          extractor.selectedParser(),
+          collector,
+          inputStream,
+          adapterRuntimeContext
+      );
 
     } catch (AdapterException e) {
       adapterRuntimeContext
           .getLogger()
           .error(e);
     }
+
+  }
+
+  private void parseFile(
+      IParser parser,
+      IEventCollector collector,
+      InputStream inputStream,
+      IAdapterRuntimeContext adapterRuntimeContext
+  ) {
+    // The parse method does not throw AdapterExceptions, that's why the 
logging is handeled within the catch blog here
+    parser.parse(inputStream, (event) -> {
+      try {
+        processEvent(collector, event);
+      } catch (AdapterException e) {
+        adapterRuntimeContext
+            .getLogger()
+            .error(e);
+      }
+    });
+  }
+
+  protected void processEvent(
+      IEventCollector collector,
+      Map<String, Object> event
+  ) throws AdapterException {
+
+    long actualEventTimestamp = getTimestampFromEvent(event);
+
+    reduceReplaySpeedIfRequired(actualEventTimestamp);
+
+    // This must be the last step, because the original timestamp must be used 
to simulate the replay frequency
+    // of the original file
+    replaceTimestampIfRequired(event);
+
+    timestampLastEvent = actualEventTimestamp;
+    collector.collect(event);
+  }
+
+  // TODO Add description that explains that the timestamp is transformed to a 
unix timestamp
+  private long getTimestampFromEvent(Map<String, Object> event) throws 
AdapterException {
+    long actualEventTimestamp = -1;
+
+    var timestampFieldValue = event.get(timestampSourceFieldName);
+    if (timestampFieldValue instanceof Long) {
+      actualEventTimestamp = (Long) timestampFieldValue;
+    } else if (timestampFieldValue instanceof Integer) {
+      actualEventTimestamp = (Integer) timestampFieldValue;
+    } else if (!(timestampFieldValue == null && replaceTimestamp)) {
+      throw new AdapterException(
+          "Timestamp field is not a unix timestamp in ms, skipping event. "
+              + "Value: %s".formatted(event.get(timestampSourceFieldName)));
+    }
+
+    // TODO should be replaced
+    if (actualEventTimestamp == -1 && !replaceTimestamp) {
+      throw new AdapterException("TODO");
+    }
+
+    return actualEventTimestamp;
+  }
+
+  private void reduceReplaySpeedIfRequired(long actualEventTimestamp) {
+    long sleepTime;
+    if (timestampLastEvent != -1 && actualEventTimestamp != -1) {
+      sleepTime = (long) ((actualEventTimestamp - timestampLastEvent) / 
speedUp);
+    } else {
+      sleepTime = 1;
+    }
+    // speed up is set to Float.MAX_VALUE when user selected fastest option
+    if (sleepTime > 0 && speedUp != Float.MAX_VALUE) {
+      try {
+        Thread.sleep(sleepTime);
+      } catch (InterruptedException e) {
+        LOG.info("File stream adapter was stopped, the current replay is 
interrupted", e);
+      }
+    }
+  }
+
+  private void replaceTimestampIfRequired(Map<String, Object> event) {
+    if (replaceTimestamp) {
+      event.put(timestampSourceFieldName, System.currentTimeMillis());
+    }
   }
 
   @Override
@@ -301,19 +346,32 @@ public class FileReplayAdapter implements 
StreamPipesAdapter {
   }
 
   @Override
-  public GuessSchema onSchemaRequested(IAdapterParameterExtractor extractor,
-                                       IAdapterGuessSchemaContext 
adapterGuessSchemaContext) throws AdapterException {
-    var inputStream = getDataFromEndpoint(extractor
-        .getStaticPropertyExtractor()
-        .selectedFilename(FILE_PATH));
-    return extractor.selectedParser().getGuessSchema(inputStream);
+  public GuessSchema onSchemaRequested(
+      IAdapterParameterExtractor extractor,
+      IAdapterGuessSchemaContext adapterGuessSchemaContext
+  ) throws AdapterException {
+    var inputStream = getFileAsInputStreamFromEndpoint(extractor);
+    return extractor.selectedParser()
+                    .getGuessSchema(inputStream);
   }
 
-  private InputStream getDataFromEndpoint(String selectedFileName) throws 
AdapterException {
+  private InputStream 
getFileAsInputStreamFromEndpoint(IAdapterParameterExtractor extractor) throws 
AdapterException {
+    var selectedFileName = extractor
+        .getStaticPropertyExtractor()
+        .selectedFilename(FILE_PATH);
+
     try {
       return FileProtocolUtils.getFileInputStream(selectedFileName);
     } catch (IOException e) {
       throw new AdapterException("Could not find file: " + selectedFileName, 
e);
     }
   }
+
+  protected void setTimestampSourceFieldName(String timestampSourceFieldName) {
+    this.timestampSourceFieldName = timestampSourceFieldName;
+  }
+
+  protected void setReplaceTimestamp(boolean replaceTimestamp) {
+    this.replaceTimestamp = replaceTimestamp;
+  }
 }
diff --git 
a/streampipes-extensions/streampipes-connect-adapters-iiot/src/test/java/org/apache/streampipes/connect/iiot/protocol/stream/FileReplayAdapterTest.java
 
b/streampipes-extensions/streampipes-connect-adapters-iiot/src/test/java/org/apache/streampipes/connect/iiot/protocol/stream/FileReplayAdapterTest.java
new file mode 100644
index 0000000000..0efbd947af
--- /dev/null
+++ 
b/streampipes-extensions/streampipes-connect-adapters-iiot/src/test/java/org/apache/streampipes/connect/iiot/protocol/stream/FileReplayAdapterTest.java
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.streampipes.connect.iiot.protocol.stream;
+
+import org.apache.streampipes.commons.exceptions.connect.AdapterException;
+import org.apache.streampipes.extensions.api.connect.IEventCollector;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+
+class FileReplayAdapterTest {
+
+  private FileReplayAdapter fileReplayAdapter;
+  private IEventCollector collector;
+  private final static String TIMESTAMP = "timestamp";
+  private final static long TIMESTAMP_VALUE = 1622544682000L;
+  private Map<String, Object> event;
+  private ArgumentCaptor<Map<String, Object>> resultEventCapture;
+
+
+  @BeforeEach
+  void setUp() {
+    collector = Mockito.mock(IEventCollector.class);
+    fileReplayAdapter = new FileReplayAdapter();
+    fileReplayAdapter.setTimestampSourceFieldName(TIMESTAMP);
+    event = new HashMap<>();
+    resultEventCapture = ArgumentCaptor.forClass(Map.class);
+  }
+
+  @Test
+  void processEvent_shouldCollectEventWhenTimestampIsLong() throws 
AdapterException {
+    event.put(TIMESTAMP, TIMESTAMP_VALUE);
+
+    fileReplayAdapter.processEvent(collector, event);
+
+    verify(collector, times(1)).collect(event);
+  }
+
+  @Test
+  void processEvent_shouldCollectEventWhenTimestampIsInteger() throws 
AdapterException {
+    event.put(TIMESTAMP, 1622544682);
+
+    fileReplayAdapter.processEvent(collector, event);
+
+    verify(collector, times(1)).collect(event);
+  }
+
+  @Test
+  void 
processEvent_shouldThrowAdapterExceptionWhenTimestampIsNotUnixTimestampInMs() {
+    event.put(TIMESTAMP, "not a timestamp");
+
+    assertThrows(AdapterException.class, () -> 
fileReplayAdapter.processEvent(collector, event));
+  }
+
+  @Test
+  void processEvent_shouldNotCollectEventWhenTimestampCouldNotBeProcessed() 
throws AdapterException {
+    event.put(TIMESTAMP, -1);
+
+    assertThrows(AdapterException.class, () -> 
fileReplayAdapter.processEvent(collector, event));
+  }
+
+  @Test
+  void processEvent_shouldCollectEventWhenTimestampIsReplaced() throws 
AdapterException {
+    event.put(TIMESTAMP, TIMESTAMP_VALUE);
+
+    fileReplayAdapter.setReplaceTimestamp(true);
+    fileReplayAdapter.processEvent(collector, event);
+
+    verify(collector, times(1)).collect(resultEventCapture.capture());
+
+    var restultEvent = resultEventCapture.getValue();
+    assertEquals(restultEvent.size(), 1);
+    assertNotEquals(restultEvent.get(TIMESTAMP), TIMESTAMP_VALUE);
+  }
+
+
+}
\ No newline at end of file
diff --git a/ui/cypress/tests/adapter/rules/valueRules.spec.ts 
b/ui/cypress/tests/adapter/rules/valueRules.spec.ts
index 2163f454d2..d6d1ccdd1d 100644
--- a/ui/cypress/tests/adapter/rules/valueRules.spec.ts
+++ b/ui/cypress/tests/adapter/rules/valueRules.spec.ts
@@ -37,14 +37,14 @@ describe('Connect value rule transformations', () => {
         );
 
         // Number transformation
-        ConnectEventSchemaUtils.numberTransformation('value', '10');
+        // ConnectEventSchemaUtils.numberTransformation('value', '10');
 
         // Unit transformation
-        ConnectEventSchemaUtils.unitTransformation(
-            'temperature',
-            'Degree Celsius',
-            'Degree Fahrenheit',
-        );
+        // ConnectEventSchemaUtils.unitTransformation(
+        //     'temperature',
+        //     'Degree Celsius',
+        //     'Degree Fahrenheit',
+        // );
 
         ConnectEventSchemaUtils.finishEventSchemaConfiguration();
 

Reply via email to