diff --git a/CHANGELOG.md b/CHANGELOG.md index 778778c242..dcb3726570 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] + +### Added +- Implement SQL source for primary data [#34](https://github.com/ie3-institute/simona/issues/34) + ### Changed - Improving code readability in EvcsAgent by moving FreeLotsRequest to separate methods - Re-organizing test resources into their respective packages [#105](https://github.com/ie3-institute/simona/issues/105) diff --git a/build.gradle b/build.gradle index 52f34f8ff7..a5045c941f 100644 --- a/build.gradle +++ b/build.gradle @@ -30,6 +30,8 @@ ext { tscfgVersion = '0.9.997' scapegoatVersion = '1.4.12' + testContainerVersion = '0.40.3' + scriptsLocation = 'gradle' + File.separator + 'scripts' + File.separator // location of script plugins } @@ -107,6 +109,10 @@ dependencies { testImplementation group: 'org.pegdown', name: 'pegdown', version: '1.6.0' testImplementation "com.typesafe.akka:akka-testkit_${scalaVersion}:${akkaVersion}" // akka testkit + // testcontainers + testImplementation "com.dimafeng:testcontainers-scala-scalatest_${scalaVersion}:${testContainerVersion}" + testImplementation "com.dimafeng:testcontainers-scala-postgresql_${scalaVersion}:${testContainerVersion}" + /* --- Scala libs --- */ /* CORE Scala */ implementation "org.scala-lang:scala-library:${scalaBinaryVersion}" diff --git a/gradle/scripts/tscfg.gradle b/gradle/scripts/tscfg.gradle index 7f430ff1f7..ce1fe4dbe9 100644 --- a/gradle/scripts/tscfg.gradle +++ b/gradle/scripts/tscfg.gradle @@ -15,7 +15,7 @@ task genConfigClass { args = [ "build/tscfg-${tscfgVersion}.jar", "--spec", - "src/main/resources/config/simona-config-template.conf", + "src/main/resources/config/config-template.conf", "--scala", "--durations", "--pn", diff --git a/src/main/resources/config/config-template.conf b/src/main/resources/config/config-template.conf index 8e6952e8f0..9c36b25cf5 100644 --- a/src/main/resources/config/config-template.conf +++ b/src/main/resources/config/config-template.conf @@ -101,9 +101,7 @@ simona.input.primary = { jdbcUrl: string userName: string password: string - weatherTableName: string schemaName: string | "public" - timeColumnName: string timePattern: string | "yyyy-MM-dd'T'HH:mm:ss[.S[S][S]]'Z'" # default pattern from PSDM:TimeBasedSimpleValueFactory } #@optional @@ -150,9 +148,9 @@ simona.input.weather.datasource = { jdbcUrl: string userName: string password: string - weatherTableName: string + tableName: string schemaName: string | "public" - timeColumnName: string + timePattern: string | "yyyy-MM-dd'T'HH:mm:ss[.S[S][S]]'Z'" # default pattern from PSDM:TimeBasedSimpleValueFactory } #@optional couchbaseParams = { diff --git a/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala b/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala index 6283055bbc..93e9b4931b 100644 --- a/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala +++ b/src/main/scala/edu/ie3/simona/config/SimonaConfig.scala @@ -1,5 +1,5 @@ /* - * © 2021. TU Dortmund University, + * © 2022. TU Dortmund University, * Institute of Energy Systems, Energy Efficiency and Energy Economics, * Research group Distribution grid planning and operation */ @@ -908,10 +908,8 @@ object SimonaConfig { jdbcUrl: java.lang.String, password: java.lang.String, schemaName: java.lang.String, - timeColumnName: java.lang.String, timePattern: java.lang.String, - userName: java.lang.String, - weatherTableName: java.lang.String + userName: java.lang.String ) object SqlParams { def apply( @@ -925,14 +923,10 @@ object SimonaConfig { schemaName = if (c.hasPathOrNull("schemaName")) c.getString("schemaName") else "public", - timeColumnName = - $_reqStr(parentPath, c, "timeColumnName", $tsCfgValidator), timePattern = if (c.hasPathOrNull("timePattern")) c.getString("timePattern") else "yyyy-MM-dd'T'HH:mm:ss[.S[S][S]]'Z'", - userName = $_reqStr(parentPath, c, "userName", $tsCfgValidator), - weatherTableName = - $_reqStr(parentPath, c, "weatherTableName", $tsCfgValidator) + userName = $_reqStr(parentPath, c, "userName", $tsCfgValidator) ) } private def $_reqStr( @@ -1277,9 +1271,9 @@ object SimonaConfig { jdbcUrl: java.lang.String, password: java.lang.String, schemaName: java.lang.String, - timeColumnName: java.lang.String, - userName: java.lang.String, - weatherTableName: java.lang.String + tableName: java.lang.String, + timePattern: java.lang.String, + userName: java.lang.String ) object SqlParams { def apply( @@ -1293,11 +1287,12 @@ object SimonaConfig { schemaName = if (c.hasPathOrNull("schemaName")) c.getString("schemaName") else "public", - timeColumnName = - $_reqStr(parentPath, c, "timeColumnName", $tsCfgValidator), - userName = $_reqStr(parentPath, c, "userName", $tsCfgValidator), - weatherTableName = - $_reqStr(parentPath, c, "weatherTableName", $tsCfgValidator) + tableName = + $_reqStr(parentPath, c, "tableName", $tsCfgValidator), + timePattern = + if (c.hasPathOrNull("timePattern")) c.getString("timePattern") + else "yyyy-MM-dd'T'HH:mm:ss[.S[S][S]]'Z'", + userName = $_reqStr(parentPath, c, "userName", $tsCfgValidator) ) } private def $_reqStr( diff --git a/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceProxy.scala b/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceProxy.scala index 010c0525dd..f46af75f53 100644 --- a/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceProxy.scala +++ b/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceProxy.scala @@ -9,9 +9,12 @@ package edu.ie3.simona.service.primary import akka.actor.{Actor, ActorRef, PoisonPill, Props} import edu.ie3.datamodel.io.csv.CsvIndividualTimeSeriesMetaInformation import edu.ie3.datamodel.io.naming.FileNamingStrategy -import edu.ie3.datamodel.io.naming.timeseries.ColumnScheme +import edu.ie3.datamodel.io.naming.timeseries.IndividualTimeSeriesMetaInformation import edu.ie3.datamodel.io.source.TimeSeriesMappingSource -import edu.ie3.datamodel.io.source.csv.CsvTimeSeriesMappingSource +import edu.ie3.datamodel.io.source.csv.{ + CsvTimeSeriesMappingSource, + CsvTimeSeriesTypeSource +} import edu.ie3.datamodel.models.value.Value import edu.ie3.simona.config.SimonaConfig import edu.ie3.simona.config.SimonaConfig.Simona.Input.Primary.CsvParams @@ -51,7 +54,6 @@ import java.time.ZonedDateTime import java.util.UUID import scala.Option.when import scala.jdk.CollectionConverters._ -import scala.jdk.OptionConverters._ import scala.util.{Failure, Success, Try} /** This actor has information on which models can be replaced by precalculated @@ -137,19 +139,27 @@ case class PrimaryServiceProxy( ).filter(_.isDefined).flatten.headOption match { case Some(CsvParams(csvSep, folderPath, _)) => // TODO: Configurable file naming strategy + val fileNamingStrategy = new FileNamingStrategy() val mappingSource = new CsvTimeSeriesMappingSource( csvSep, folderPath, - new FileNamingStrategy() + fileNamingStrategy + ) + val typeSource = new CsvTimeSeriesTypeSource( + csvSep, + folderPath, + fileNamingStrategy ) val modelToTimeSeries = mappingSource.getMapping.asScala.toMap + val timeSeriesMetaInformation = + typeSource.getTimeSeriesMetaInformation.asScala.toMap + val timeSeriesToSourceRef = modelToTimeSeries.values .to(LazyList) .distinct .flatMap { timeSeriesUuid => - mappingSource - .timeSeriesMetaInformation(timeSeriesUuid) - .toScala match { + timeSeriesMetaInformation + .get(timeSeriesUuid) match { case Some(metaInformation) => val columnScheme = metaInformation.getColumnScheme /* Only register those entries, that meet the supported column schemes */ @@ -157,7 +167,7 @@ case class PrimaryServiceProxy( PrimaryServiceWorker.supportedColumnSchemes .contains(columnScheme) ) { - timeSeriesUuid -> SourceRef(columnScheme, None) + timeSeriesUuid -> SourceRef(metaInformation, None) } case None => log.warning( @@ -251,14 +261,12 @@ case class PrimaryServiceProxy( /* There is yet a worker apparent. Register the requesting actor. The worker will reply to the original * requesting actor. */ worker ! WorkerRegistrationMessage(requestingActor) - case Some(SourceRef(columnScheme, None)) => + case Some(SourceRef(metaInformation, None)) => /* There is NO worker apparent, yet. Spin one off. */ initializeWorker( - columnScheme, - timeSeriesUuid, + metaInformation, stateData.simulationStart, - stateData.primaryConfig, - stateData.mappingSource + stateData.primaryConfig ) match { case Success(workerRef) => /* Forward the registration request. The worker will reply about successful registration or not. */ @@ -289,33 +297,28 @@ case class PrimaryServiceProxy( /** Instantiate a new [[PrimaryServiceWorker]] and send initialization * information * - * @param columnScheme - * Scheme of the data to expect + * @param metaInformation + * Meta information (including column scheme) of the time series + * @param simulationStart + * The time of the simulation start * @param primaryConfig * Configuration for the primary config - * @param mappingSource - * Source for time series mapping, that might deliver additional - * information for the source initialization * @return * The [[ActorRef]] to the worker */ protected def initializeWorker( - columnScheme: ColumnScheme, - timeSeriesUuid: UUID, + metaInformation: IndividualTimeSeriesMetaInformation, simulationStart: ZonedDateTime, - primaryConfig: PrimaryConfig, - mappingSource: TimeSeriesMappingSource + primaryConfig: PrimaryConfig ): Try[ActorRef] = { val workerRef = classToWorkerRef( - columnScheme.getValueClass, - timeSeriesUuid.toString, - simulationStart + metaInformation.getColumnScheme.getValueClass, + metaInformation.getUuid.toString ) toInitData( - primaryConfig, - mappingSource, - timeSeriesUuid, - simulationStart + metaInformation, + simulationStart, + primaryConfig ) match { case Success(initData) => scheduler ! ScheduleTriggerMessage( @@ -341,8 +344,6 @@ case class PrimaryServiceProxy( * Class of the values to provide later on * @param timeSeriesUuid * uuid of the time series the actor processes - * @param simulationStart - * Wall clock time of first instant in simulation * @tparam V * Type of the class to provide * @return @@ -350,33 +351,29 @@ case class PrimaryServiceProxy( */ protected def classToWorkerRef[V <: Value]( valueClass: Class[V], - timeSeriesUuid: String, - simulationStart: ZonedDateTime + timeSeriesUuid: String ): ActorRef = { import edu.ie3.simona.actor.SimonaActorNaming._ context.system.simonaActorOf( - PrimaryServiceWorker.props(scheduler, valueClass, simulationStart), + PrimaryServiceWorker.props(scheduler, valueClass), timeSeriesUuid ) } /** Building proper init data for the worker * - * @param primaryConfig - * Configuration for primary sources - * @param mappingSource - * Source to get mapping information about time series - * @param timeSeriesUuid - * Unique identifier for the time series + * @param metaInformation + * Meta information (including column scheme) of the time series * @param simulationStart - * Wall clock time of the first instant in simulation + * The time of the simulation start + * @param primaryConfig + * Configuration for the primary config * @return */ private def toInitData( - primaryConfig: PrimaryConfig, - mappingSource: TimeSeriesMappingSource, - timeSeriesUuid: UUID, - simulationStart: ZonedDateTime + metaInformation: IndividualTimeSeriesMetaInformation, + simulationStart: ZonedDateTime, + primaryConfig: PrimaryConfig ): Try[InitPrimaryServiceStateData] = primaryConfig match { case PrimaryConfig( @@ -385,29 +382,27 @@ case class PrimaryServiceProxy( None, None ) => - /* The mapping and actual data sources are from csv. At first, get the file name of the file to read. */ - Try(mappingSource.timeSeriesMetaInformation(timeSeriesUuid).get) - .flatMap { - /* Time series meta information could be successfully obtained */ - case csvMetaData: CsvIndividualTimeSeriesMetaInformation => - Success( - CsvInitPrimaryServiceStateData( - timeSeriesUuid, - simulationStart, - csvSep, - directoryPath, - csvMetaData.getFullFilePath, - new FileNamingStrategy(), - timePattern - ) + /* The actual data sources are from csv. Meta information have to match */ + metaInformation match { + case csvMetaData: CsvIndividualTimeSeriesMetaInformation => + Success( + CsvInitPrimaryServiceStateData( + csvMetaData.getUuid, + simulationStart, + csvSep, + directoryPath, + csvMetaData.getFullFilePath, + new FileNamingStrategy(), + timePattern ) - case invalidMetaData => - Failure( - new InitializationException( - s"Expected '${classOf[CsvIndividualTimeSeriesMetaInformation]}', but got '$invalidMetaData'." - ) + ) + case invalidMetaData => + Failure( + new InitializationException( + s"Expected '${classOf[CsvIndividualTimeSeriesMetaInformation]}', but got '$invalidMetaData'." ) - } + ) + } case unsupported => Failure( new InitializationException( @@ -489,14 +484,14 @@ object PrimaryServiceProxy { /** Giving reference to the target time series and source worker. * - * @param columnScheme - * Column scheme of the time series to get + * @param metaInformation + * Meta information (including column scheme) of the time series * @param worker - * Optional reference to a yet existing worker providing information on - * that time series + * Optional reference to an already existing worker providing information + * on that time series */ final case class SourceRef( - columnScheme: ColumnScheme, + metaInformation: IndividualTimeSeriesMetaInformation, worker: Option[ActorRef] ) diff --git a/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceWorker.scala b/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceWorker.scala index ec50abf716..7d25630cac 100644 --- a/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceWorker.scala +++ b/src/main/scala/edu/ie3/simona/service/primary/PrimaryServiceWorker.scala @@ -7,14 +7,17 @@ package edu.ie3.simona.service.primary import akka.actor.{ActorRef, Props} +import edu.ie3.datamodel.io.connectors.SqlConnector import edu.ie3.datamodel.io.factory.timeseries.TimeBasedSimpleValueFactory -import edu.ie3.datamodel.io.naming.FileNamingStrategy import edu.ie3.datamodel.io.naming.timeseries.ColumnScheme +import edu.ie3.datamodel.io.naming.{DatabaseNamingStrategy, FileNamingStrategy} import edu.ie3.datamodel.io.source.TimeSeriesSource import edu.ie3.datamodel.io.source.csv.CsvTimeSeriesSource +import edu.ie3.datamodel.io.source.sql.SqlTimeSeriesSource import edu.ie3.datamodel.models.value.Value import edu.ie3.simona.agent.participant.data.Data.PrimaryData import edu.ie3.simona.agent.participant.data.Data.PrimaryData.RichValue +import edu.ie3.simona.config.SimonaConfig.Simona.Input.Primary.SqlParams import edu.ie3.simona.exceptions.InitializationException import edu.ie3.simona.exceptions.WeatherServiceException.InvalidRegistrationRequestException import edu.ie3.simona.ontology.messages.SchedulerMessage @@ -24,11 +27,11 @@ import edu.ie3.simona.service.ServiceStateData.{ InitializeServiceStateData, ServiceActivationBaseStateData } -import edu.ie3.simona.service.{ServiceStateData, SimonaService} import edu.ie3.simona.service.primary.PrimaryServiceWorker.{ PrimaryServiceInitializedStateData, ProvidePrimaryDataMessage } +import edu.ie3.simona.service.{ServiceStateData, SimonaService} import edu.ie3.simona.util.TickUtil.{RichZonedDateTime, TickLong} import edu.ie3.util.scala.collection.immutable.SortedDistinctSeq @@ -40,8 +43,7 @@ import scala.util.{Failure, Success, Try} final case class PrimaryServiceWorker[V <: Value]( override protected val scheduler: ActorRef, - valueClass: Class[V], - private implicit val startDateTime: ZonedDateTime + valueClass: Class[V] ) extends SimonaService[PrimaryServiceInitializedStateData[V]](scheduler) { /** Initialize the actor with the given information. Try to figure out the @@ -61,68 +63,99 @@ final case class PrimaryServiceWorker[V <: Value]( PrimaryServiceInitializedStateData[V], Option[Seq[SchedulerMessage.ScheduleTriggerMessage]] ) - ] = initServiceData match { - case PrimaryServiceWorker.CsvInitPrimaryServiceStateData( - timeSeriesUuid, - simulationStart, - csvSep, - directoryPath, - filePath, - fileNamingStrategy, - timePattern - ) => - /* Got the right data. Attempt to set up a source and acquire information */ - implicit val startDateTime: ZonedDateTime = simulationStart + ] = { + (initServiceData match { + case PrimaryServiceWorker.CsvInitPrimaryServiceStateData( + timeSeriesUuid, + simulationStart, + csvSep, + directoryPath, + filePath, + fileNamingStrategy, + timePattern + ) => + Try { + /* Set up source and acquire information */ + val factory = new TimeBasedSimpleValueFactory(valueClass, timePattern) + val source = new CsvTimeSeriesSource( + csvSep, + directoryPath, + fileNamingStrategy, + timeSeriesUuid, + filePath, + valueClass, + factory + ) + (source, simulationStart) + } - Try { - /* Set up source and acquire information */ - val factory = new TimeBasedSimpleValueFactory(valueClass, timePattern) - val source = new CsvTimeSeriesSource( - csvSep, - directoryPath, - fileNamingStrategy, - timeSeriesUuid, - filePath, - valueClass, - factory - ) - /* This seems not to be very efficient, but it is as efficient as possible. The getter method points to a - * final attribute within the source implementation. */ - val (maybeNextTick, furtherActivationTicks) = SortedDistinctSeq( - source.getTimeSeries.getEntries.asScala - .filter { timeBasedValue => - val dateTime = timeBasedValue.getTime - dateTime.isEqual(simulationStart) || dateTime.isAfter( - simulationStart - ) - } - .map(timeBasedValue => timeBasedValue.getTime.toTick) - .toSeq - .sorted - ).pop + case PrimaryServiceWorker.SqlInitPrimaryServiceStateData( + timeSeriesUuid: UUID, + simulationStart: ZonedDateTime, + sqlParams: SqlParams, + namingStrategy: DatabaseNamingStrategy + ) => + Try { + val factory = + new TimeBasedSimpleValueFactory(valueClass, sqlParams.timePattern) - /* Set up the state data and determine the next activation tick. */ - val initializedStateData = - PrimaryServiceInitializedStateData( - maybeNextTick, - furtherActivationTicks, - simulationStart, - source + val sqlConnector = new SqlConnector( + sqlParams.jdbcUrl, + sqlParams.userName, + sqlParams.password ) - val triggerMessage = - ServiceActivationBaseStateData.tickToScheduleTriggerMessages( - maybeNextTick, - self + + val source = new SqlTimeSeriesSource( + sqlConnector, + sqlParams.schemaName, + namingStrategy, + timeSeriesUuid, + valueClass, + factory + ) + + (source, simulationStart) + } + + case unsupported => + /* Got the wrong init data */ + Failure( + new InitializationException( + s"Provided init data '${unsupported.getClass.getSimpleName}' for primary service are invalid!" ) - (initializedStateData, triggerMessage) - } - case unsupported => - /* Got the wrong init data */ - Failure( - new InitializationException( - s"Provided init data '${unsupported.getClass.getSimpleName}' for primary service are invalid!" ) - ) + }).map { case (source, simulationStart) => + implicit val startDateTime: ZonedDateTime = simulationStart + + val (maybeNextTick, furtherActivationTicks) = SortedDistinctSeq( + // Note: The whole data set is used here, which might be inefficient depending on the source implementation. + source.getTimeSeries.getEntries.asScala + .filter { timeBasedValue => + val dateTime = timeBasedValue.getTime + dateTime.isEqual(simulationStart) || dateTime.isAfter( + simulationStart + ) + } + .map(timeBasedValue => timeBasedValue.getTime.toTick) + .toSeq + .sorted + ).pop + + /* Set up the state data and determine the next activation tick. */ + val initializedStateData = + PrimaryServiceInitializedStateData( + maybeNextTick, + furtherActivationTicks, + simulationStart, + source + ) + val triggerMessage = + ServiceActivationBaseStateData.tickToScheduleTriggerMessages( + maybeNextTick, + self + ) + (initializedStateData, triggerMessage) + } } /** Handle a request to register for information from this service @@ -289,7 +322,7 @@ final case class PrimaryServiceWorker[V <: Value]( } } -case object PrimaryServiceWorker { +object PrimaryServiceWorker { /** List of supported column schemes aka. column schemes, that belong to * primary data @@ -303,10 +336,9 @@ case object PrimaryServiceWorker { def props[V <: Value]( scheduler: ActorRef, - valueClass: Class[V], - simulationStart: ZonedDateTime + valueClass: Class[V] ): Props = - Props(new PrimaryServiceWorker(scheduler, valueClass, simulationStart)) + Props(new PrimaryServiceWorker(scheduler, valueClass)) /** Abstract class pattern for specific [[InitializeServiceStateData]]. * Different implementations are needed, because the [[PrimaryServiceProxy]] @@ -348,6 +380,25 @@ case object PrimaryServiceWorker { timePattern: String ) extends InitPrimaryServiceStateData + /** Specific implementation of [[InitPrimaryServiceStateData]], if the source + * to use utilizes an SQL database. + * + * @param timeSeriesUuid + * Unique identifier of the time series to read + * @param simulationStart + * Wall clock time of the beginning of simulation time + * @param sqlParams + * Parameters regarding SQL connection and table selection + * @param databaseNamingStrategy + * Strategy of naming database entities, such as tables + */ + final case class SqlInitPrimaryServiceStateData( + override val timeSeriesUuid: UUID, + override val simulationStart: ZonedDateTime, + sqlParams: SqlParams, + databaseNamingStrategy: DatabaseNamingStrategy + ) extends InitPrimaryServiceStateData + /** Class carrying the state of a fully initialized [[PrimaryServiceWorker]] * * @param maybeNextActivationTick diff --git a/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala b/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala index ded598932e..5f0947bbae 100644 --- a/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala +++ b/src/main/scala/edu/ie3/simona/service/weather/WeatherSourceWrapper.scala @@ -328,7 +328,7 @@ private[weather] object WeatherSourceWrapper extends LazyLogging { sqlConnector, idCoordinateSource, sqlParams.schemaName, - sqlParams.weatherTableName, + sqlParams.tableName, buildFactory(timestampPattern, scheme) ) logger.info( diff --git a/src/main/scala/edu/ie3/simona/util/ConfigUtil.scala b/src/main/scala/edu/ie3/simona/util/ConfigUtil.scala index b4703c2873..d4ee521750 100644 --- a/src/main/scala/edu/ie3/simona/util/ConfigUtil.scala +++ b/src/main/scala/edu/ie3/simona/util/ConfigUtil.scala @@ -321,11 +321,7 @@ object ConfigUtil { logger.info( "Password for SQL weather source is empty. This is allowed, but not common. Please check if this an intended setting." ) - if (sql.timeColumnName.isEmpty) - throw new InvalidConfigParameterException( - "Time column for SQL weather source cannot be empty" - ) - if (sql.weatherTableName.isEmpty) + if (sql.tableName.isEmpty) throw new InvalidConfigParameterException( "Weather table name for SQL weather source cannot be empty" ) diff --git a/src/test/resources/edu/ie3/simona/service/primary/timeseries/time_series_p.sql b/src/test/resources/edu/ie3/simona/service/primary/timeseries/time_series_p.sql new file mode 100644 index 0000000000..79beaf5e70 --- /dev/null +++ b/src/test/resources/edu/ie3/simona/service/primary/timeseries/time_series_p.sql @@ -0,0 +1,21 @@ +CREATE TABLE public.time_series_p +( + uuid uuid PRIMARY KEY, + time_series uuid NOT NULL, + time timestamp with time zone NOT NULL, + p double precision NOT NULL +) + WITHOUT OIDS + TABLESPACE pg_default; + +CREATE INDEX time_series_p_series_id ON time_series_p USING hash (time_series); + +CREATE UNIQUE INDEX time_series_p_series_time ON time_series_p USING btree (time_series, time); + +INSERT INTO + public.time_series_p (uuid, time_series, time, p) +VALUES +('0245d599-9a5c-4c32-9613-5b755fac8ca0', '9185b8c1-86ba-4a16-8dea-5ac898e8caa5', '2020-01-01 00:00:00+0', 1000.0), +('a5e27652-9024-4a93-9d2a-590fbc3ab5a1', '9185b8c1-86ba-4a16-8dea-5ac898e8caa5', '2020-01-01 00:15:00+0', 1250.0), +('b4a2b3e0-7215-431b-976e-d8b41c7bc71b', 'b669e4bf-a351-4067-860d-d5f224b62247', '2020-01-01 00:00:00+0', 50.0), +('1c8f072c-c833-47da-a3e9-5f4d305ab926', 'b669e4bf-a351-4067-860d-d5f224b62247', '2020-01-01 00:15:00+0', 100.0); diff --git a/src/test/resources/edu/ie3/simona/service/primary/timeseries/time_series_pqh.sql b/src/test/resources/edu/ie3/simona/service/primary/timeseries/time_series_pqh.sql new file mode 100644 index 0000000000..8bd3a48908 --- /dev/null +++ b/src/test/resources/edu/ie3/simona/service/primary/timeseries/time_series_pqh.sql @@ -0,0 +1,21 @@ +CREATE TABLE public.time_series_pqh +( + uuid uuid PRIMARY KEY, + time_series uuid NOT NULL, + time timestamp with time zone NOT NULL, + p double precision NOT NULL, + q double precision NOT NULL, + heat_demand double precision NOT NULL +) + WITHOUT OIDS + TABLESPACE pg_default; + +CREATE INDEX time_series_pqh_series_id ON time_series_pqh USING hash (time_series); + +CREATE UNIQUE INDEX time_series_pqh_series_time ON time_series_pqh USING btree (time_series, time); + +INSERT INTO + public.time_series_pqh (uuid, time_series, time, p, q, heat_demand) +VALUES +('661ac594-47f0-4442-8d82-bbeede5661f7', '46be1e57-e4ed-4ef7-95f1-b2b321cb2047', '2020-01-01 00:00:00+0', 1000.0, 329.0, 8.0), +('5adcd6c5-a903-433f-b7b5-5fe669a3ed30', '46be1e57-e4ed-4ef7-95f1-b2b321cb2047', '2020-01-01 00:15:00+0', 1250.0, 411.0, 12.0); diff --git a/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceProxySpec.scala b/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceProxySpec.scala index 94a27323b9..8ab191fc3b 100644 --- a/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceProxySpec.scala +++ b/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceProxySpec.scala @@ -10,8 +10,9 @@ import akka.actor.{ActorRef, ActorSystem, PoisonPill} import akka.testkit.{TestActorRef, TestProbe} import akka.util.Timeout import com.typesafe.config.ConfigFactory +import edu.ie3.datamodel.io.csv.CsvIndividualTimeSeriesMetaInformation import edu.ie3.datamodel.io.naming.FileNamingStrategy -import edu.ie3.datamodel.io.naming.timeseries.ColumnScheme +import edu.ie3.datamodel.io.naming.timeseries.IndividualTimeSeriesMetaInformation import edu.ie3.datamodel.io.source.TimeSeriesMappingSource import edu.ie3.datamodel.io.source.csv.CsvTimeSeriesMappingSource import edu.ie3.datamodel.models.value.{SValue, Value} @@ -33,22 +34,23 @@ import edu.ie3.simona.ontology.messages.SchedulerMessage.{ ScheduleTriggerMessage, TriggerWithIdMessage } +import edu.ie3.simona.ontology.messages.services.ServiceMessage.RegistrationResponseMessage.RegistrationFailedMessage import edu.ie3.simona.ontology.messages.services.ServiceMessage.{ PrimaryServiceRegistrationMessage, WorkerRegistrationMessage } -import edu.ie3.simona.ontology.messages.services.ServiceMessage.RegistrationResponseMessage.RegistrationFailedMessage import edu.ie3.simona.ontology.trigger.Trigger.InitializeServiceTrigger -import edu.ie3.simona.service.primary.PrimaryServiceWorker.{ - CsvInitPrimaryServiceStateData, - InitPrimaryServiceStateData -} import edu.ie3.simona.service.primary.PrimaryServiceProxy.{ InitPrimaryServiceProxyStateData, PrimaryServiceStateData, SourceRef } +import edu.ie3.simona.service.primary.PrimaryServiceWorker.{ + CsvInitPrimaryServiceStateData, + InitPrimaryServiceStateData +} import edu.ie3.simona.test.common.AgentSpec +import edu.ie3.simona.test.common.input.TimeSeriesTestData import edu.ie3.util.TimeUtil import org.scalatest.PartialFunctionValues import org.scalatest.prop.TableDrivenPropertyChecks @@ -57,8 +59,8 @@ import java.nio.file.Paths import java.time.ZonedDateTime import java.util.concurrent.TimeUnit import java.util.{Objects, UUID} -import scala.util.{Failure, Success, Try} import scala.concurrent.ExecutionContext.Implicits.global +import scala.util.{Failure, Success, Try} class PrimaryServiceProxySpec extends AgentSpec( @@ -72,7 +74,8 @@ class PrimaryServiceProxySpec ) ) with TableDrivenPropertyChecks - with PartialFunctionValues { + with PartialFunctionValues + with TimeSeriesTestData { // this works both on Windows and Unix systems val baseDirectoryPath: String = Paths .get( @@ -103,30 +106,19 @@ class PrimaryServiceProxySpec baseDirectoryPath, fileNamingStrategy ) - val workerId: String = - "PrimaryService_3fbfaa97-cff4-46d4-95ba-a95665e87c26" + val workerId: String = "PrimaryService_" + uuidPq val modelUuid: UUID = UUID.fromString("c7ebcc6c-55fc-479b-aa6b-6fa82ccac6b8") - val timeSeriesUuid: UUID = - UUID.fromString("3fbfaa97-cff4-46d4-95ba-a95665e87c26") val simulationStart: ZonedDateTime = TimeUtil.withDefaults.toZonedDateTime("2021-03-17 13:14:00") val proxyStateData: PrimaryServiceStateData = PrimaryServiceStateData( Map( - UUID.fromString("b86e95b0-e579-4a80-a534-37c7a470a409") -> UUID - .fromString("9185b8c1-86ba-4a16-8dea-5ac898e8caa5"), - modelUuid -> UUID.fromString("3fbfaa97-cff4-46d4-95ba-a95665e87c26"), - UUID.fromString("90a96daa-012b-4fea-82dc-24ba7a7ab81c") -> UUID - .fromString("3fbfaa97-cff4-46d4-95ba-a95665e87c26") + UUID.fromString("b86e95b0-e579-4a80-a534-37c7a470a409") -> uuidP, + modelUuid -> uuidPq, + UUID.fromString("90a96daa-012b-4fea-82dc-24ba7a7ab81c") -> uuidPq ), Map( - UUID.fromString("9185b8c1-86ba-4a16-8dea-5ac898e8caa5") -> SourceRef( - ColumnScheme.ACTIVE_POWER, - None - ), - UUID.fromString("3fbfaa97-cff4-46d4-95ba-a95665e87c26") -> SourceRef( - ColumnScheme.APPARENT_POWER, - None - ) + uuidP -> SourceRef(metaP, None), + uuidPq -> SourceRef(metaPq, None) ), simulationStart, validPrimaryConfig, @@ -210,13 +202,13 @@ class PrimaryServiceProxySpec None, None, None, - Some(SqlParams("", "", "", "", "", "", "")) + Some(SqlParams("", "", "", "", "")) ) val exception = intercept[InvalidConfigParameterException]( PrimaryServiceProxy.checkConfig(maliciousConfig) ) - exception.getMessage shouldBe "Invalid configuration 'SqlParams(,,,,,,)' for a time series source.\nAvailable types:\n\tcsv" + exception.getMessage shouldBe "Invalid configuration 'SqlParams(,,,,)' for a time series source.\nAvailable types:\n\tcsv" } "fails on invalid time pattern" in { @@ -282,7 +274,7 @@ class PrimaryServiceProxySpec None, None, None, - Some(SqlParams("", "", "", "", "", "", "")) + Some(SqlParams("", "", "", "", "")) ) proxy invokePrivate prepareStateData( @@ -293,7 +285,7 @@ class PrimaryServiceProxySpec fail("Building state data with missing config should fail") case Failure(exception) => exception.getClass shouldBe classOf[IllegalArgumentException] - exception.getMessage shouldBe "Unsupported config for mapping source: 'SqlParams(,,,,,,)'" + exception.getMessage shouldBe "Unsupported config for mapping source: 'SqlParams(,,,,)'" } } @@ -312,24 +304,13 @@ class PrimaryServiceProxySpec ) ) => modelToTimeSeries shouldBe Map( - UUID.fromString("b86e95b0-e579-4a80-a534-37c7a470a409") -> UUID - .fromString("9185b8c1-86ba-4a16-8dea-5ac898e8caa5"), - UUID.fromString("c7ebcc6c-55fc-479b-aa6b-6fa82ccac6b8") -> UUID - .fromString("3fbfaa97-cff4-46d4-95ba-a95665e87c26"), - UUID.fromString("90a96daa-012b-4fea-82dc-24ba7a7ab81c") -> UUID - .fromString("3fbfaa97-cff4-46d4-95ba-a95665e87c26") + UUID.fromString("b86e95b0-e579-4a80-a534-37c7a470a409") -> uuidP, + UUID.fromString("c7ebcc6c-55fc-479b-aa6b-6fa82ccac6b8") -> uuidPq, + UUID.fromString("90a96daa-012b-4fea-82dc-24ba7a7ab81c") -> uuidPq ) timeSeriesToSourceRef shouldBe Map( - UUID - .fromString("9185b8c1-86ba-4a16-8dea-5ac898e8caa5") -> SourceRef( - ColumnScheme.ACTIVE_POWER, - None - ), - UUID - .fromString("3fbfaa97-cff4-46d4-95ba-a95665e87c26") -> SourceRef( - ColumnScheme.APPARENT_POWER, - None - ) + uuidP -> SourceRef(metaP, None), + uuidPq -> SourceRef(metaPq, None) ) simulationStart shouldBe this.simulationStart primaryConfig shouldBe validPrimaryConfig @@ -372,8 +353,7 @@ class PrimaryServiceProxySpec val workerRef = proxy invokePrivate classToWorkerRef( testClass, - workerId, - simulationStart + workerId ) Objects.nonNull(workerRef) shouldBe true @@ -385,12 +365,15 @@ class PrimaryServiceProxySpec val toInitData = PrivateMethod[Try[InitPrimaryServiceStateData]]( Symbol("toInitData") ) + val metaInformation = new CsvIndividualTimeSeriesMetaInformation( + metaPq, + "its_pq_" + uuidPq + ) proxy invokePrivate toInitData( - validPrimaryConfig, - mappingSource, - timeSeriesUuid, - simulationStart + metaInformation, + simulationStart, + validPrimaryConfig ) match { case Success( CsvInitPrimaryServiceStateData( @@ -403,11 +386,11 @@ class PrimaryServiceProxySpec timePattern ) ) => - actualTimeSeriesUuid shouldBe timeSeriesUuid + actualTimeSeriesUuid shouldBe uuidPq actualSimulationStart shouldBe simulationStart actualCsvSep shouldBe csvSep directoryPath shouldBe baseDirectoryPath - filePath shouldBe "its_pq_3fbfaa97-cff4-46d4-95ba-a95665e87c26" + filePath shouldBe metaInformation.getFullFilePath classOf[FileNamingStrategy].isAssignableFrom( fileNamingStrategy.getClass ) shouldBe true @@ -429,13 +412,10 @@ class PrimaryServiceProxySpec None, None ) - proxy invokePrivate initializeWorker( - ColumnScheme.APPARENT_POWER, - timeSeriesUuid, + metaPq, simulationStart, - maliciousPrimaryConfig, - mappingSource + maliciousPrimaryConfig ) match { case Failure(exception) => /* Check the exception */ @@ -468,35 +448,32 @@ class PrimaryServiceProxySpec TestActorRef(new PrimaryServiceProxy(scheduler.ref, simulationStart) { override protected def classToWorkerRef[V <: Value]( valueClass: Class[V], - timeSeriesUuid: String, - simulationStart: ZonedDateTime + timeSeriesUuid: String ): ActorRef = testProbe.ref // needs to be overwritten as to make it available to the private method tester @SuppressWarnings(Array("NoOpOverride")) override protected def initializeWorker( - columnScheme: ColumnScheme, - timeSeriesUuid: UUID, + metaInformation: IndividualTimeSeriesMetaInformation, simulationStart: ZonedDateTime, - primaryConfig: PrimaryConfig, - mappingSource: TimeSeriesMappingSource + primaryConfig: PrimaryConfig ): Try[ActorRef] = super.initializeWorker( - columnScheme, - timeSeriesUuid, + metaInformation, simulationStart, - primaryConfig, - mappingSource + primaryConfig ) }) val fakeProxy: PrimaryServiceProxy = fakeProxyRef.underlyingActor + val metaInformation = new CsvIndividualTimeSeriesMetaInformation( + metaPq, + "its_pq_" + uuidPq + ) fakeProxy invokePrivate initializeWorker( - ColumnScheme.APPARENT_POWER, - timeSeriesUuid, + metaInformation, simulationStart, - validPrimaryConfig, - mappingSource + validPrimaryConfig ) match { case Success(workerRef) => /* Check, if expected init message has been sent */ @@ -515,11 +492,11 @@ class PrimaryServiceProxySpec ), actorToBeScheduled ) => - actualTimeSeriesUuid shouldBe timeSeriesUuid + actualTimeSeriesUuid shouldBe uuidPq actualSimulationStart shouldBe simulationStart actualCsvSep shouldBe csvSep directoryPath shouldBe baseDirectoryPath - filePath shouldBe "its_pq_3fbfaa97-cff4-46d4-95ba-a95665e87c26" + filePath shouldBe metaInformation.getFullFilePath classOf[FileNamingStrategy].isAssignableFrom( fileNamingStrategy.getClass ) shouldBe true @@ -556,7 +533,7 @@ class PrimaryServiceProxySpec "work otherwise" in { proxy invokePrivate updateStateData( proxyStateData, - timeSeriesUuid, + uuidPq, self ) match { case PrimaryServiceStateData( @@ -568,16 +545,8 @@ class PrimaryServiceProxySpec ) => modelToTimeSeries shouldBe proxyStateData.modelToTimeSeries timeSeriesToSourceRef shouldBe Map( - UUID - .fromString("9185b8c1-86ba-4a16-8dea-5ac898e8caa5") -> SourceRef( - ColumnScheme.ACTIVE_POWER, - None - ), - UUID - .fromString("3fbfaa97-cff4-46d4-95ba-a95665e87c26") -> SourceRef( - ColumnScheme.APPARENT_POWER, - Some(self) - ) + uuidP -> SourceRef(metaP, None), + uuidPq -> SourceRef(metaPq, Some(self)) ) simulationStart shouldBe proxyStateData.simulationStart primaryConfig shouldBe proxyStateData.primaryConfig @@ -595,7 +564,7 @@ class PrimaryServiceProxySpec proxy invokePrivate handleCoveredModel( modelUuid, - timeSeriesUuid, + uuidPq, maliciousStateData, self ) @@ -605,13 +574,13 @@ class PrimaryServiceProxySpec "forward the registration request, if worker is already known" in { val adaptedStateData = proxyStateData.copy( timeSeriesToSourceRef = Map( - timeSeriesUuid -> SourceRef(ColumnScheme.APPARENT_POWER, Some(self)) + uuidPq -> SourceRef(metaPq, Some(self)) ) ) proxy invokePrivate handleCoveredModel( modelUuid, - timeSeriesUuid, + uuidPq, adaptedStateData, self ) @@ -630,7 +599,7 @@ class PrimaryServiceProxySpec proxy invokePrivate handleCoveredModel( modelUuid, - timeSeriesUuid, + uuidPq, maliciousStateData, self ) @@ -643,11 +612,9 @@ class PrimaryServiceProxySpec val fakeProxyRef = TestActorRef(new PrimaryServiceProxy(self, simulationStart) { override protected def initializeWorker( - columnScheme: ColumnScheme, - timeSeriesUuid: UUID, + metaInformation: IndividualTimeSeriesMetaInformation, simulationStart: ZonedDateTime, - primaryConfig: PrimaryConfig, - mappingSource: TimeSeriesMappingSource + primaryConfig: PrimaryConfig ): Try[ActorRef] = Success(probe.ref) // needs to be overwritten as to make it available to the private method tester @@ -669,7 +636,7 @@ class PrimaryServiceProxySpec fakeProxy invokePrivate handleCoveredModel( modelUuid, - timeSeriesUuid, + uuidPq, proxyStateData, self ) @@ -693,11 +660,9 @@ class PrimaryServiceProxySpec val fakeProxyRef = TestActorRef(new PrimaryServiceProxy(self, simulationStart) { override protected def initializeWorker( - columnScheme: ColumnScheme, - timeSeriesUuid: UUID, + metaInformation: IndividualTimeSeriesMetaInformation, simulationStart: ZonedDateTime, - primaryConfig: PrimaryConfig, - mappingSource: TimeSeriesMappingSource + primaryConfig: PrimaryConfig ): Try[ActorRef] = Success(probe.ref) }) diff --git a/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceWorkerSpec.scala b/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceWorkerSpec.scala index 53556678fa..2a7e0d816c 100644 --- a/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceWorkerSpec.scala +++ b/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceWorkerSpec.scala @@ -37,6 +37,7 @@ import edu.ie3.simona.service.primary.PrimaryServiceWorker.{ } import edu.ie3.simona.service.primary.PrimaryServiceWorkerSpec.WrongInitPrimaryServiceStateData import edu.ie3.simona.test.common.AgentSpec +import edu.ie3.simona.test.common.input.TimeSeriesTestData import edu.ie3.util.TimeUtil import edu.ie3.util.quantities.PowerSystemUnits import edu.ie3.util.scala.collection.immutable.SortedDistinctSeq @@ -56,7 +57,8 @@ class PrimaryServiceWorkerSpec |akka.loglevel="OFF" """.stripMargin) ) - ) { + ) + with TimeSeriesTestData { // this works both on Windows and Unix systems val baseDirectoryPath: String = Paths .get( @@ -68,15 +70,12 @@ class PrimaryServiceWorkerSpec ) .toString - private val simulationStart = - TimeUtil.withDefaults.toZonedDateTime("2020-01-01 00:00:00") - val validInitData: CsvInitPrimaryServiceStateData = CsvInitPrimaryServiceStateData( - timeSeriesUuid = UUID.fromString("9185b8c1-86ba-4a16-8dea-5ac898e8caa5"), + timeSeriesUuid = uuidP, csvSep = ";", directoryPath = baseDirectoryPath, - filePath = "its_p_9185b8c1-86ba-4a16-8dea-5ac898e8caa5", + filePath = "its_p_" + uuidP, fileNamingStrategy = new FileNamingStrategy(), simulationStart = TimeUtil.withDefaults.toZonedDateTime("2020-01-01 00:00:00"), @@ -88,8 +87,7 @@ class PrimaryServiceWorkerSpec TestActorRef( new PrimaryServiceWorker[PValue]( self, - classOf[PValue], - simulationStart + classOf[PValue] ) ) val service = serviceRef.underlyingActor @@ -106,13 +104,12 @@ class PrimaryServiceWorkerSpec "fail, if pointed to the wrong file" in { val maliciousInitData = CsvInitPrimaryServiceStateData( - timeSeriesUuid = - UUID.fromString("3fbfaa97-cff4-46d4-95ba-a95665e87c26"), + timeSeriesUuid = uuidPq, simulationStart = TimeUtil.withDefaults.toZonedDateTime("2020-01-01 00:00:00"), csvSep = ";", directoryPath = baseDirectoryPath, - filePath = "its_pq_3fbfaa97-cff4-46d4-95ba-a95665e87c26", + filePath = "its_pq_" + uuidPq, fileNamingStrategy = new FileNamingStrategy(), timePattern = TimeUtil.withDefaults.getDtfPattern ) @@ -200,8 +197,8 @@ class PrimaryServiceWorkerSpec ";", baseDirectoryPath, new FileNamingStrategy(), - UUID.fromString("9185b8c1-86ba-4a16-8dea-5ac898e8caa5"), - "its_p_9185b8c1-86ba-4a16-8dea-5ac898e8caa5", + uuidP, + "its_p_" + uuidP, classOf[PValue], new TimeBasedSimpleValueFactory[PValue](classOf[PValue]) ), diff --git a/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceWorkerSqlIT.scala b/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceWorkerSqlIT.scala new file mode 100644 index 0000000000..1609809225 --- /dev/null +++ b/src/test/scala/edu/ie3/simona/service/primary/PrimaryServiceWorkerSqlIT.scala @@ -0,0 +1,204 @@ +/* + * © 2022. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.service.primary + +import akka.actor.ActorSystem +import akka.testkit.{TestActorRef, TestProbe} +import com.dimafeng.testcontainers.{ForAllTestContainer, PostgreSQLContainer} +import com.typesafe.config.ConfigFactory +import edu.ie3.datamodel.io.naming.DatabaseNamingStrategy +import edu.ie3.datamodel.models.StandardUnits +import edu.ie3.datamodel.models.value.{HeatAndSValue, PValue} +import edu.ie3.simona.agent.participant.data.Data.PrimaryData.{ + ActivePower, + ApparentPowerAndHeat +} +import edu.ie3.simona.config.SimonaConfig.Simona.Input.Primary.SqlParams +import edu.ie3.simona.ontology.messages.SchedulerMessage.{ + CompletionMessage, + ScheduleTriggerMessage, + TriggerWithIdMessage +} +import edu.ie3.simona.ontology.messages.services.ServiceMessage.RegistrationResponseMessage.RegistrationSuccessfulMessage +import edu.ie3.simona.ontology.messages.services.ServiceMessage.WorkerRegistrationMessage +import edu.ie3.simona.ontology.trigger.Trigger.{ + ActivityStartTrigger, + InitializeServiceTrigger +} +import edu.ie3.simona.service.primary.PrimaryServiceWorker.{ + ProvidePrimaryDataMessage, + SqlInitPrimaryServiceStateData +} +import edu.ie3.simona.test.common.AgentSpec +import edu.ie3.simona.test.common.input.TimeSeriesTestData +import edu.ie3.simona.test.helper.TestContainerHelper +import edu.ie3.util.TimeUtil +import org.scalatest.BeforeAndAfterAll +import org.scalatest.prop.TableDrivenPropertyChecks +import tech.units.indriya.quantity.Quantities + +class PrimaryServiceWorkerSqlIT + extends AgentSpec( + ActorSystem( + "PrimaryServiceWorkerSqlIT", + ConfigFactory + .parseString(""" + |akka.loglevel="OFF" + """.stripMargin) + ) + ) + with ForAllTestContainer + with BeforeAndAfterAll + with TableDrivenPropertyChecks + with TimeSeriesTestData + with TestContainerHelper { + + override val container: PostgreSQLContainer = PostgreSQLContainer( + "postgres:14.2" + ) + + private val simulationStart = + TimeUtil.withDefaults.toZonedDateTime("2020-01-01 00:00:00") + + private val schemaName = "public" + + override protected def beforeAll(): Unit = { + // Copy sql import scripts into docker + val sqlImportFile = getMountableFile("timeseries/") + container.copyFileToContainer(sqlImportFile, "/home/") + + Iterable("time_series_p.sql", "time_series_pqh.sql") + .foreach { file => + val res = container.execInContainer("psql", "-Utest", "-f/home/" + file) + res.getStderr shouldBe empty + } + } + + override protected def afterAll(): Unit = { + container.stop() + container.close() + } + + "A primary service actor with SQL source" should { + "initialize and send out data when activated" in { + val scheduler = TestProbe("scheduler") + + val cases = Table( + ( + "service", + "uuid", + "firstTick", + "firstData", + "maybeNextTick" + ), + ( + PrimaryServiceWorker.props( + scheduler.ref, + classOf[HeatAndSValue] + ), + uuidPqh, + 0L, + ApparentPowerAndHeat( + Quantities.getQuantity(1000.0d, StandardUnits.ACTIVE_POWER_IN), + Quantities.getQuantity(329.0d, StandardUnits.REACTIVE_POWER_IN), + Quantities.getQuantity(8000.0, StandardUnits.HEAT_DEMAND_PROFILE) + ), + Some(900L) + ), + ( + PrimaryServiceWorker.props( + scheduler.ref, + classOf[PValue] + ), + uuidP, + 0L, + ActivePower( + Quantities.getQuantity(1000.0d, StandardUnits.ACTIVE_POWER_IN) + ), + Some(900L) + ) + ) + + forAll(cases) { + ( + service, + uuid, + firstTick, + firstData, + maybeNextTick + ) => + val serviceRef = TestActorRef(service) + + val initData = SqlInitPrimaryServiceStateData( + uuid, + simulationStart, + SqlParams( + jdbcUrl = container.jdbcUrl, + userName = container.username, + password = container.password, + schemaName = schemaName, + timePattern = "yyyy-MM-dd HH:mm:ss" + ), + new DatabaseNamingStrategy() + ) + + val triggerId1 = 1L + + scheduler.send( + serviceRef, + TriggerWithIdMessage( + InitializeServiceTrigger(initData), + triggerId1, + serviceRef + ) + ) + + scheduler.expectMsg( + CompletionMessage( + triggerId1, + Some( + List( + ScheduleTriggerMessage( + ActivityStartTrigger(firstTick), + serviceRef + ) + ) + ) + ) + ) + + val participant = TestProbe() + + participant.send( + serviceRef, + WorkerRegistrationMessage(participant.ref) + ) + participant.expectMsg(RegistrationSuccessfulMessage(Some(firstTick))) + + val triggerId2 = 2L + + scheduler.send( + serviceRef, + TriggerWithIdMessage( + ActivityStartTrigger(firstTick), + triggerId2, + serviceRef + ) + ) + + scheduler.expectMsgType[CompletionMessage] + + val dataMsg = participant.expectMsgType[ProvidePrimaryDataMessage] + dataMsg.tick shouldBe firstTick + dataMsg.data shouldBe firstData + dataMsg.nextDataTick shouldBe maybeNextTick + + scheduler.expectNoMessage() + } + } + } +} diff --git a/src/test/scala/edu/ie3/simona/test/common/input/TimeSeriesTestData.scala b/src/test/scala/edu/ie3/simona/test/common/input/TimeSeriesTestData.scala new file mode 100644 index 0000000000..64d674e3ad --- /dev/null +++ b/src/test/scala/edu/ie3/simona/test/common/input/TimeSeriesTestData.scala @@ -0,0 +1,39 @@ +/* + * © 2022. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.test.common.input + +import edu.ie3.datamodel.io.naming.timeseries.{ + ColumnScheme, + IndividualTimeSeriesMetaInformation +} + +import java.util.UUID + +trait TimeSeriesTestData { + protected val uuidP: UUID = + UUID.fromString("9185b8c1-86ba-4a16-8dea-5ac898e8caa5") + protected val uuidPq: UUID = + UUID.fromString("3fbfaa97-cff4-46d4-95ba-a95665e87c26") + protected val uuidPqh: UUID = + UUID.fromString("46be1e57-e4ed-4ef7-95f1-b2b321cb2047") + + protected val metaP: IndividualTimeSeriesMetaInformation = + new IndividualTimeSeriesMetaInformation( + uuidP, + ColumnScheme.ACTIVE_POWER + ) + protected val metaPq: IndividualTimeSeriesMetaInformation = + new IndividualTimeSeriesMetaInformation( + uuidPq, + ColumnScheme.APPARENT_POWER + ) + protected val metaPqh: IndividualTimeSeriesMetaInformation = + new IndividualTimeSeriesMetaInformation( + uuidPqh, + ColumnScheme.APPARENT_POWER_AND_HEAT_DEMAND + ) +} diff --git a/src/test/scala/edu/ie3/simona/test/helper/TestContainerHelper.scala b/src/test/scala/edu/ie3/simona/test/helper/TestContainerHelper.scala new file mode 100644 index 0000000000..f58786aa6d --- /dev/null +++ b/src/test/scala/edu/ie3/simona/test/helper/TestContainerHelper.scala @@ -0,0 +1,35 @@ +/* + * © 2022. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation + */ + +package edu.ie3.simona.test.helper + +import akka.testkit.TestException +import org.testcontainers.utility.MountableFile + +import java.nio.file.Paths + +trait TestContainerHelper { + + /** Retrieve resource with the class' resource loader. In contrast to + * [[org.testcontainers.utility.MountableFile#forClasspathResource(java.lang.String, java.lang.Integer)]], + * this also works with paths relative to the current class (i.e. without + * leading '/'). + * @param resource + * the resource directory or file path + * @return + * a MountableFile to use with test containers + */ + def getMountableFile(resource: String): MountableFile = { + Option(getClass.getResource(resource)) + .map(url => Paths.get(url.toURI)) + .map(MountableFile.forHostPath) + .getOrElse( + throw TestException( + "Resource '" + resource + "' was not found from " + getClass.toString + ) + ) + } +}