diff --git a/AUTHORS b/AUTHORS index 4c0b256d1..c358b1c4b 100644 --- a/AUTHORS +++ b/AUTHORS @@ -28,4 +28,5 @@ Main Contributers: - Johannes Bao - https://github.com/jo-bao - Julian Hohmann - https://github.com/julianhohmann - Simon Huette - https://github.com/SimonHuette - - Pierre Petersmeier - http://github.com/pierrepetersmeier + - Pierre Petersmeier - https://github.com/pierrepetersmeier + - Philipp Schmelter - https://github.com/PhilippSchmelter diff --git a/CHANGELOG.md b/CHANGELOG.md index 5416aa3b7..20b0d65de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,6 +75,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Enhance `TimeSeriesSource` with method to retrieve all time keys after a given key [#543](https://github.com/ie3-institute/PowerSystemDataModel/issues/543) - Enhance `WeatherSource` with method to retrieve all time keys after a given key [#572](https://github.com/ie3-institute/PowerSystemDataModel/issues/572) +- Added explicit handling for cases where no weather data is received from any source [#554](https://github.com/ie3-institute/PowerSystemDataModel/issues/554) - Adding timeseries for voltage values [#1128](https://github.com/ie3-institute/PowerSystemDataModel/issues/1128) - Added Staudt to list of reviewers [#1190](https://github.com/ie3-institute/PowerSystemDataModel/issues/1190) - Extend ValidationUtils for validating ThermalGrids [#1216](https://github.com/ie3-institute/PowerSystemDataModel/issues/1216) diff --git a/src/main/java/edu/ie3/datamodel/exceptions/NoDataException.java b/src/main/java/edu/ie3/datamodel/exceptions/NoDataException.java new file mode 100644 index 000000000..cf8aa747e --- /dev/null +++ b/src/main/java/edu/ie3/datamodel/exceptions/NoDataException.java @@ -0,0 +1,21 @@ +/* + * © 2024. TU Dortmund University, + * Institute of Energy Systems, Energy Efficiency and Energy Economics, + * Research group Distribution grid planning and operation +*/ +package edu.ie3.datamodel.exceptions; + +/** + * Exception that should be used whenever no data is received{@link + * edu.ie3.datamodel.io.source.DataSource} + */ +public class NoDataException extends Exception { + + public NoDataException(final String message) { + super(message); + } + + public NoDataException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/edu/ie3/datamodel/io/source/WeatherSource.java b/src/main/java/edu/ie3/datamodel/io/source/WeatherSource.java index 94fdbd733..aee3e0158 100644 --- a/src/main/java/edu/ie3/datamodel/io/source/WeatherSource.java +++ b/src/main/java/edu/ie3/datamodel/io/source/WeatherSource.java @@ -5,6 +5,7 @@ */ package edu.ie3.datamodel.io.source; +import edu.ie3.datamodel.exceptions.NoDataException; import edu.ie3.datamodel.exceptions.SourceException; import edu.ie3.datamodel.exceptions.ValidationException; import edu.ie3.datamodel.io.factory.timeseries.TimeBasedWeatherValueData; @@ -55,14 +56,14 @@ public void validate() throws ValidationException { // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- public abstract Map> getWeather( - ClosedInterval timeInterval) throws SourceException; + ClosedInterval timeInterval) throws SourceException, NoDataException; public abstract Map> getWeather( ClosedInterval timeInterval, Collection coordinates) - throws SourceException; + throws SourceException, NoDataException; - public abstract Optional> getWeather( - ZonedDateTime date, Point coordinate) throws SourceException; + public abstract TimeBasedValue getWeather(ZonedDateTime date, Point coordinate) + throws SourceException, NoDataException; public abstract Map> getTimeKeysAfter(ZonedDateTime time) throws SourceException; diff --git a/src/main/java/edu/ie3/datamodel/io/source/couchbase/CouchbaseWeatherSource.java b/src/main/java/edu/ie3/datamodel/io/source/couchbase/CouchbaseWeatherSource.java index e8a2d1583..00761284b 100644 --- a/src/main/java/edu/ie3/datamodel/io/source/couchbase/CouchbaseWeatherSource.java +++ b/src/main/java/edu/ie3/datamodel/io/source/couchbase/CouchbaseWeatherSource.java @@ -10,6 +10,7 @@ import com.couchbase.client.java.json.JsonObject; import com.couchbase.client.java.kv.GetResult; import com.couchbase.client.java.query.QueryResult; +import edu.ie3.datamodel.exceptions.NoDataException; import edu.ie3.datamodel.io.connectors.CouchbaseConnector; import edu.ie3.datamodel.io.factory.timeseries.TimeBasedWeatherValueData; import edu.ie3.datamodel.io.factory.timeseries.TimeBasedWeatherValueFactory; @@ -105,7 +106,7 @@ public Optional> getSourceFields() { @Override public Map> getWeather( - ClosedInterval timeInterval) { + ClosedInterval timeInterval) throws NoDataException { logger.warn( "By not providing coordinates you are forcing couchbase to check all possible coordinates one by one." + " This is not very performant. Please consider providing specific coordinates instead."); @@ -114,7 +115,18 @@ public Map> getWeather( @Override public Map> getWeather( - ClosedInterval timeInterval, Collection coordinates) { + ClosedInterval timeInterval, Collection coordinates) + throws NoDataException { + + List invalidCoordinates = + coordinates.stream() + .filter(coordinate -> idCoordinateSource.getId(coordinate).isEmpty()) + .toList(); + + if (!invalidCoordinates.isEmpty()) { + throw new NoDataException("No data for given coordinates: " + invalidCoordinates); + } + HashMap> coordinateToTimeSeries = new HashMap<>(); for (Point coordinate : coordinates) { Optional coordinateId = idCoordinateSource.getId(coordinate); @@ -126,7 +138,8 @@ public Map> getWeather( try { jsonWeatherInputs = queryResult.rowsAsObject(); } catch (DecodingFailureException ex) { - logger.error("Querying weather inputs failed!", ex); + throw new NoDataException( + "Failed to decode weather data for coordinate " + coordinate, ex); } if (jsonWeatherInputs != null && !jsonWeatherInputs.isEmpty()) { Set> weatherInputs = @@ -138,32 +151,52 @@ public Map> getWeather( new IndividualTimeSeries<>(weatherInputs); coordinateToTimeSeries.put(coordinate, weatherTimeSeries); } - } else logger.warn("Unable to match coordinate {} to a coordinate ID", coordinate); + } } return coordinateToTimeSeries; } @Override - public Optional> getWeather(ZonedDateTime date, Point coordinate) { + public TimeBasedValue getWeather(ZonedDateTime date, Point coordinate) + throws NoDataException { Optional coordinateId = idCoordinateSource.getId(coordinate); if (coordinateId.isEmpty()) { - logger.warn("Unable to match coordinate {} to a coordinate ID", coordinate); - return Optional.empty(); + logger.error("Unable to match coordinate {} to a coordinate ID", coordinate); + throw new NoDataException("No coordinate ID found for the given point: " + coordinate); } try { CompletableFuture futureResult = connector.get(generateWeatherKey(date, coordinateId.get())); GetResult getResult = futureResult.join(); JsonObject jsonWeatherInput = getResult.contentAsObject(); - return toTimeBasedWeatherValue(jsonWeatherInput); + return toTimeBasedWeatherValue(jsonWeatherInput) + .orElseThrow( + () -> + new NoDataException( + "No valid weather data found for the given date and coordinate.")); } catch (DecodingFailureException ex) { - logger.error("Decoding to TimeBasedWeatherValue failed!", ex); - return Optional.empty(); + throw new NoDataException( + "Failed to decode weather data for coordinate " + coordinate + " and date " + date, ex); } catch (DocumentNotFoundException ex) { - return Optional.empty(); + throw new NoDataException( + "Weather document not found for coordinate " + coordinate + " and date " + date, ex); } catch (CompletionException ex) { - if (ex.getCause() instanceof DocumentNotFoundException) return Optional.empty(); - else throw ex; + Throwable cause = ex.getCause(); + if (cause instanceof DocumentNotFoundException) { + throw new NoDataException( + "Weather document not found in completion stage for coordinate " + + coordinate + + " and date " + + date, + cause); + } else { + throw new NoDataException( + "Unexpected completion exception while retrieving weather data for coordinate " + + coordinate + + " and date " + + date, + ex); + } } } diff --git a/src/main/java/edu/ie3/datamodel/io/source/csv/CsvWeatherSource.java b/src/main/java/edu/ie3/datamodel/io/source/csv/CsvWeatherSource.java index 241442725..45035808d 100644 --- a/src/main/java/edu/ie3/datamodel/io/source/csv/CsvWeatherSource.java +++ b/src/main/java/edu/ie3/datamodel/io/source/csv/CsvWeatherSource.java @@ -8,6 +8,7 @@ import static edu.ie3.datamodel.utils.validation.UniquenessValidationUtils.checkWeatherUniqueness; import edu.ie3.datamodel.exceptions.DuplicateEntitiesException; +import edu.ie3.datamodel.exceptions.NoDataException; import edu.ie3.datamodel.exceptions.SourceException; import edu.ie3.datamodel.exceptions.ValidationException; import edu.ie3.datamodel.io.connectors.CsvFileConnector; @@ -90,25 +91,63 @@ public Optional> getSourceFields() { @Override public Map> getWeather( - ClosedInterval timeInterval) { - return trimMapToInterval(coordinateToTimeSeries, timeInterval); + ClosedInterval timeInterval) throws NoDataException { + + Map> result = + trimMapToInterval(coordinateToTimeSeries, timeInterval); + + if (result == null || result.isEmpty()) { + throw new NoDataException( + "No weather data found for the given time interval: " + timeInterval); + } + + return result; } @Override public Map> getWeather( - ClosedInterval timeInterval, Collection coordinates) { + ClosedInterval timeInterval, Collection coordinates) + throws NoDataException { + + List invalidCoordinates = + coordinates.stream() + .filter(coordinate -> !coordinateToTimeSeries.containsKey(coordinate)) + .toList(); + + if (!invalidCoordinates.isEmpty()) { + throw new NoDataException("No data for given coordinates: " + invalidCoordinates); + } + Map> filteredMap = coordinateToTimeSeries.entrySet().stream() .filter(entry -> coordinates.contains(entry.getKey())) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - return trimMapToInterval(filteredMap, timeInterval); + + Map> result = + trimMapToInterval(filteredMap, timeInterval); + + if (result == null || result.isEmpty()) { + throw new NoDataException( + "No weather data found for the given time interval: " + timeInterval); + } + return result; } @Override - public Optional> getWeather(ZonedDateTime date, Point coordinate) { + public TimeBasedValue getWeather(ZonedDateTime date, Point coordinate) + throws NoDataException { IndividualTimeSeries timeSeries = coordinateToTimeSeries.get(coordinate); - if (timeSeries == null) return Optional.empty(); - return timeSeries.getTimeBasedValue(date); + + if (timeSeries == null) { + throw new NoDataException("No weather data found for the given coordinate: " + coordinate); + } + + return timeSeries + .getTimeBasedValue(date) + .orElseThrow( + () -> + new NoDataException( + "No weather data found for the given coordinate: " + coordinate)); } @Override diff --git a/src/main/java/edu/ie3/datamodel/io/source/influxdb/InfluxDbWeatherSource.java b/src/main/java/edu/ie3/datamodel/io/source/influxdb/InfluxDbWeatherSource.java index 2a23660f5..85f0a2d2a 100644 --- a/src/main/java/edu/ie3/datamodel/io/source/influxdb/InfluxDbWeatherSource.java +++ b/src/main/java/edu/ie3/datamodel/io/source/influxdb/InfluxDbWeatherSource.java @@ -5,6 +5,7 @@ */ package edu.ie3.datamodel.io.source.influxdb; +import edu.ie3.datamodel.exceptions.NoDataException; import edu.ie3.datamodel.io.connectors.InfluxDbConnector; import edu.ie3.datamodel.io.factory.timeseries.TimeBasedWeatherValueData; import edu.ie3.datamodel.io.factory.timeseries.TimeBasedWeatherValueFactory; @@ -59,7 +60,7 @@ public Optional> getSourceFields() { @Override public Map> getWeather( - ClosedInterval timeInterval) { + ClosedInterval timeInterval) throws NoDataException { try (InfluxDB session = connector.getSession()) { String query = createQueryStringForTimeInterval(timeInterval); QueryResult queryResult = session.query(new Query(query)); @@ -67,6 +68,10 @@ public Map> getWeather( optTimeBasedValueStream(queryResult); Set> timeBasedValues = filterEmptyOptionals(optValues).collect(Collectors.toSet()); + if (timeBasedValues.isEmpty()) { + throw new NoDataException( + "No weather data found for the given time interval: " + timeInterval); + } Map>> coordinateToValues = timeBasedValues.stream() .collect( @@ -81,10 +86,22 @@ public Map> getWeather( @Override public Map> getWeather( - ClosedInterval timeInterval, Collection coordinates) { + ClosedInterval timeInterval, Collection coordinates) + throws NoDataException { if (coordinates == null) return getWeather(timeInterval); Map> coordinatesToId = coordinates.stream().collect(Collectors.toMap(point -> point, idCoordinateSource::getId)); + + List invalidCoordinates = + coordinatesToId.entrySet().stream() + .filter(entry -> entry.getValue().isEmpty()) + .map(Map.Entry::getKey) + .toList(); + + if (!invalidCoordinates.isEmpty()) { + throw new NoDataException("No data for given coordinates: " + invalidCoordinates); + } + HashMap> coordinateToTimeSeries = new HashMap<>(); try (InfluxDB session = connector.getSession()) { for (Map.Entry> entry : coordinatesToId.entrySet()) { @@ -97,9 +114,11 @@ public Map> getWeather( optTimeBasedValueStream(queryResult); Set> timeBasedValues = filterEmptyOptionals(optValues).collect(Collectors.toSet()); - IndividualTimeSeries timeSeries = - new IndividualTimeSeries<>(timeBasedValues); - coordinateToTimeSeries.put(entry.getKey(), timeSeries); + if (!timeBasedValues.isEmpty()) { + IndividualTimeSeries timeSeries = + new IndividualTimeSeries<>(timeBasedValues); + coordinateToTimeSeries.put(entry.getKey(), timeSeries); + } } } } @@ -107,15 +126,25 @@ public Map> getWeather( } @Override - public Optional> getWeather(ZonedDateTime date, Point coordinate) { + public TimeBasedValue getWeather(ZonedDateTime date, Point coordinate) + throws NoDataException { Optional coordinateId = idCoordinateSource.getId(coordinate); if (coordinateId.isEmpty()) { - return Optional.empty(); + throw new NoDataException("No coordinate ID found for the given point: " + coordinate); } + try (InfluxDB session = connector.getSession()) { String query = createQueryStringForCoordinateAndTime(date, coordinateId.get()); QueryResult queryResult = session.query(new Query(query)); - return filterEmptyOptionals(optTimeBasedValueStream(queryResult)).findFirst(); + return filterEmptyOptionals(optTimeBasedValueStream(queryResult)) + .findFirst() + .orElseThrow( + () -> + new NoDataException( + "No weather data available for the given date " + + date + + " and coordinate " + + coordinate)); } } @@ -167,10 +196,10 @@ public List getTimeKeysAfter(ZonedDateTime time, Point coordinate * @return weather data for the specified time and coordinate */ public IndividualTimeSeries getWeather( - ClosedInterval timeInterval, Point coordinate) { + ClosedInterval timeInterval, Point coordinate) throws NoDataException { Optional coordinateId = idCoordinateSource.getId(coordinate); if (coordinateId.isEmpty()) { - return new IndividualTimeSeries<>(UUID.randomUUID(), Collections.emptySet()); + throw new NoDataException("No data for given coordinates: " + coordinate); } try (InfluxDB session = connector.getSession()) { String query = diff --git a/src/main/java/edu/ie3/datamodel/io/source/sql/SqlWeatherSource.java b/src/main/java/edu/ie3/datamodel/io/source/sql/SqlWeatherSource.java index 358169694..8b962b1f2 100644 --- a/src/main/java/edu/ie3/datamodel/io/source/sql/SqlWeatherSource.java +++ b/src/main/java/edu/ie3/datamodel/io/source/sql/SqlWeatherSource.java @@ -7,6 +7,7 @@ import static edu.ie3.datamodel.io.source.sql.SqlDataSource.createBaseQueryString; +import edu.ie3.datamodel.exceptions.NoDataException; import edu.ie3.datamodel.exceptions.SourceException; import edu.ie3.datamodel.io.connectors.SqlConnector; import edu.ie3.datamodel.io.factory.timeseries.TimeBasedWeatherValueFactory; @@ -92,7 +93,7 @@ public Optional> getSourceFields() { @Override public Map> getWeather( - ClosedInterval timeInterval) throws SourceException { + ClosedInterval timeInterval) throws NoDataException, SourceException { List> timeBasedValues = buildTimeBasedValues( weatherFactory, @@ -102,22 +103,33 @@ public Map> getWeather( ps.setTimestamp(1, Timestamp.from(timeInterval.getLower().toInstant())); ps.setTimestamp(2, Timestamp.from(timeInterval.getUpper().toInstant())); })); + if (timeBasedValues.isEmpty()) { + throw new NoDataException( + "No weather data found for the given time interval: " + timeInterval); + } return mapWeatherValuesToPoints(timeBasedValues); } @Override public Map> getWeather( ClosedInterval timeInterval, Collection coordinates) - throws SourceException { + throws SourceException, NoDataException { + + List invalidCoordinates = + coordinates.stream() + .filter(coordinate -> idCoordinateSource.getId(coordinate).isEmpty()) + .toList(); + + if (!invalidCoordinates.isEmpty()) { + log.warn("Unable to match coordinates {} to coordinate IDs", invalidCoordinates); + throw new NoDataException("No data for given coordinates: " + invalidCoordinates); + } + Set coordinateIds = coordinates.stream() .map(idCoordinateSource::getId) .flatMap(Optional::stream) .collect(Collectors.toSet()); - if (coordinateIds.isEmpty()) { - log.warn("Unable to match coordinates to coordinate ID"); - return Collections.emptyMap(); - } List> timeBasedValues = buildTimeBasedValues( @@ -136,12 +148,12 @@ public Map> getWeather( } @Override - public Optional> getWeather(ZonedDateTime date, Point coordinate) - throws SourceException { + public TimeBasedValue getWeather(ZonedDateTime date, Point coordinate) + throws SourceException, NoDataException { Optional coordinateId = idCoordinateSource.getId(coordinate); if (coordinateId.isEmpty()) { log.warn("Unable to match coordinate {} to a coordinate ID", coordinate); - return Optional.empty(); + throw new NoDataException("No coordinate ID found for the given point: " + coordinate); } List> timeBasedValues = @@ -154,10 +166,12 @@ public Optional> getWeather(ZonedDateTime date, Poi ps.setTimestamp(2, Timestamp.from(date.toInstant())); })); - if (timeBasedValues.isEmpty()) return Optional.empty(); + if (timeBasedValues.isEmpty()) + throw new NoDataException( + "No weather data found for the given date " + date + " and coordinate " + coordinate); if (timeBasedValues.size() > 1) log.warn("Retrieved more than one result value, using the first"); - return Optional.of(timeBasedValues.get(0)); + return timeBasedValues.get(0); } @Override diff --git a/src/test/groovy/edu/ie3/datamodel/io/source/couchbase/CouchbaseWeatherSourceCosmoIT.groovy b/src/test/groovy/edu/ie3/datamodel/io/source/couchbase/CouchbaseWeatherSourceCosmoIT.groovy index a032cb5bc..b8301fec2 100644 --- a/src/test/groovy/edu/ie3/datamodel/io/source/couchbase/CouchbaseWeatherSourceCosmoIT.groovy +++ b/src/test/groovy/edu/ie3/datamodel/io/source/couchbase/CouchbaseWeatherSourceCosmoIT.groovy @@ -5,6 +5,7 @@ */ package edu.ie3.datamodel.io.source.couchbase +import edu.ie3.datamodel.exceptions.NoDataException import edu.ie3.datamodel.io.connectors.CouchbaseConnector import edu.ie3.datamodel.io.factory.timeseries.CosmoTimeBasedWeatherValueFactory import edu.ie3.datamodel.models.timeseries.individual.IndividualTimeSeries @@ -13,6 +14,7 @@ import edu.ie3.datamodel.models.value.WeatherValue import edu.ie3.test.common.CosmoWeatherTestData import edu.ie3.test.helper.TestContainerHelper import edu.ie3.test.helper.WeatherSourceTestHelper +import edu.ie3.util.geo.GeoUtils import edu.ie3.util.interval.ClosedInterval import org.locationtech.jts.geom.Point import org.testcontainers.couchbase.BucketDefinition @@ -90,8 +92,8 @@ class CouchbaseWeatherSourceCosmoIT extends Specification implements TestContain def optTimeBasedValue = source.getWeather(CosmoWeatherTestData.TIME_15H, CosmoWeatherTestData.COORDINATE_193186) then: - optTimeBasedValue.present - equalsIgnoreUUID(optTimeBasedValue.get(), expectedTimeBasedValue) + optTimeBasedValue != null + equalsIgnoreUUID(optTimeBasedValue, expectedTimeBasedValue) } def "A CouchbaseWeatherSource can read multiple time series values for multiple coordinates"() { @@ -164,4 +166,35 @@ class CouchbaseWeatherSourceCosmoIT extends Specification implements TestContain ] actual.get(CosmoWeatherTestData.COORDINATE_193187) == [CosmoWeatherTestData.TIME_16H] } + + def "A CouchbaseWeatherSource throws NoDataException for invalid coordinate"() { + given: + def invalidCoordinate = GeoUtils.buildPoint(999d, 999d) + + when: + source.getWeather(CosmoWeatherTestData.TIME_15H, invalidCoordinate) + + then: + def ex = thrown(NoDataException) + ex.message.contains("No coordinate ID found for the given point") + ex.message.contains(invalidCoordinate.toString()) + } + + def "A CouchbaseWeatherSource throws NoDataException for mixed valid and invalid coordinates"() { + given: + def validCoordinate = CosmoWeatherTestData.COORDINATE_193186 + def invalidCoordinate = GeoUtils.buildPoint(999d, 999d) + def timeInterval = new ClosedInterval(CosmoWeatherTestData.TIME_15H, CosmoWeatherTestData.TIME_17H) + + when: + source.getWeather(timeInterval, [ + validCoordinate, + invalidCoordinate + ]) + + then: + def ex = thrown(NoDataException) + ex.message.contains("No data for given coordinates") + ex.message.contains(invalidCoordinate.toString()) + } } diff --git a/src/test/groovy/edu/ie3/datamodel/io/source/couchbase/CouchbaseWeatherSourceIconIT.groovy b/src/test/groovy/edu/ie3/datamodel/io/source/couchbase/CouchbaseWeatherSourceIconIT.groovy index bf3f272f0..61f125542 100644 --- a/src/test/groovy/edu/ie3/datamodel/io/source/couchbase/CouchbaseWeatherSourceIconIT.groovy +++ b/src/test/groovy/edu/ie3/datamodel/io/source/couchbase/CouchbaseWeatherSourceIconIT.groovy @@ -5,6 +5,7 @@ */ package edu.ie3.datamodel.io.source.couchbase +import edu.ie3.datamodel.exceptions.NoDataException import edu.ie3.datamodel.io.connectors.CouchbaseConnector import edu.ie3.datamodel.io.factory.timeseries.IconTimeBasedWeatherValueFactory import edu.ie3.datamodel.models.timeseries.individual.IndividualTimeSeries @@ -13,6 +14,7 @@ import edu.ie3.test.common.IconWeatherTestData import edu.ie3.test.helper.TestContainerHelper import edu.ie3.test.helper.WeatherSourceTestHelper import edu.ie3.util.TimeUtil +import edu.ie3.util.geo.GeoUtils import edu.ie3.util.interval.ClosedInterval import org.testcontainers.couchbase.BucketDefinition import org.testcontainers.couchbase.CouchbaseContainer @@ -90,8 +92,8 @@ class CouchbaseWeatherSourceIconIT extends Specification implements TestContaine def optTimeBasedValue = source.getWeather(IconWeatherTestData.TIME_15H, IconWeatherTestData.COORDINATE_67775) then: - optTimeBasedValue.present - equalsIgnoreUUID(optTimeBasedValue.get(), expectedTimeBasedValue) + optTimeBasedValue != null + equalsIgnoreUUID(optTimeBasedValue, expectedTimeBasedValue) } def "A CouchbaseWeatherSource can read multiple time series values for multiple coordinates"() { @@ -161,4 +163,35 @@ class CouchbaseWeatherSourceIconIT extends Specification implements TestContaine ] actual.get(IconWeatherTestData.COORDINATE_67776) == [IconWeatherTestData.TIME_16H] } + + def "A CouchbaseWeatherSource throws NoDataException for invalid coordinate"() { + given: + def invalidCoordinate = GeoUtils.buildPoint(999d, 999d) + + when: + source.getWeather(IconWeatherTestData.TIME_15H, invalidCoordinate) + + then: + def ex = thrown(NoDataException) + ex.message.contains("No coordinate ID found for the given point") + ex.message.contains(invalidCoordinate.toString()) + } + + def "A CouchbaseWeatherSource throws NoDataException for mixed valid and invalid coordinates"() { + given: + def validCoordinate = IconWeatherTestData.COORDINATE_67775 + def invalidCoordinate = GeoUtils.buildPoint(999d, 999d) + def timeInterval = new ClosedInterval(IconWeatherTestData.TIME_15H, IconWeatherTestData.TIME_17H) + + when: + source.getWeather(timeInterval, [ + validCoordinate, + invalidCoordinate + ]) + + then: + def ex = thrown(NoDataException) + ex.message.contains("No data for given coordinates") + ex.message.contains(invalidCoordinate.toString()) + } } diff --git a/src/test/groovy/edu/ie3/datamodel/io/source/csv/CsvWeatherSourceCosmoTest.groovy b/src/test/groovy/edu/ie3/datamodel/io/source/csv/CsvWeatherSourceCosmoTest.groovy index cc5e709f2..07cf7461d 100644 --- a/src/test/groovy/edu/ie3/datamodel/io/source/csv/CsvWeatherSourceCosmoTest.groovy +++ b/src/test/groovy/edu/ie3/datamodel/io/source/csv/CsvWeatherSourceCosmoTest.groovy @@ -49,8 +49,8 @@ class CsvWeatherSourceCosmoTest extends Specification implements CsvTestDataMeta def optTimeBasedValue = source.getWeather(CosmoWeatherTestData.TIME_15H, CosmoWeatherTestData.COORDINATE_193186) then: - optTimeBasedValue.present - equalsIgnoreUUID(optTimeBasedValue.get(), expectedTimeBasedValue) + optTimeBasedValue != null + equalsIgnoreUUID(optTimeBasedValue, expectedTimeBasedValue) } def "A CsvWeatherSource can read multiple time series values for multiple coordinates"() { @@ -80,7 +80,6 @@ class CsvWeatherSourceCosmoTest extends Specification implements CsvTestDataMeta equalsIgnoreUUID(coordinateToTimeSeries.get(CosmoWeatherTestData.COORDINATE_193187), timeSeries193187) } - def "A CsvWeatherSource can read all weather data in a given time interval"() { given: def timeInterval = new ClosedInterval(CosmoWeatherTestData.TIME_15H, CosmoWeatherTestData.TIME_17H) diff --git a/src/test/groovy/edu/ie3/datamodel/io/source/csv/CsvWeatherSourceIconTest.groovy b/src/test/groovy/edu/ie3/datamodel/io/source/csv/CsvWeatherSourceIconTest.groovy index 7d90727b2..7e3f22f8e 100644 --- a/src/test/groovy/edu/ie3/datamodel/io/source/csv/CsvWeatherSourceIconTest.groovy +++ b/src/test/groovy/edu/ie3/datamodel/io/source/csv/CsvWeatherSourceIconTest.groovy @@ -5,6 +5,7 @@ */ package edu.ie3.datamodel.io.source.csv +import edu.ie3.datamodel.exceptions.NoDataException import edu.ie3.datamodel.io.factory.timeseries.IconTimeBasedWeatherValueFactory import edu.ie3.datamodel.io.naming.FileNamingStrategy import edu.ie3.datamodel.io.source.IdCoordinateSource @@ -43,8 +44,8 @@ class CsvWeatherSourceIconTest extends Specification implements CsvTestDataMeta, def optTimeBasedValue = source.getWeather(IconWeatherTestData.TIME_15H, IconWeatherTestData.COORDINATE_67775) then: - optTimeBasedValue.present - equalsIgnoreUUID(optTimeBasedValue.get(), expectedTimeBasedValue) + optTimeBasedValue != null + equalsIgnoreUUID(optTimeBasedValue, expectedTimeBasedValue) } def "A CsvWeatherSource can read multiple time series values for multiple coordinates"() { @@ -295,4 +296,48 @@ class CsvWeatherSourceIconTest extends Specification implements CsvTestDataMeta, ] actual.get(IconWeatherTestData.COORDINATE_67776) == [IconWeatherTestData.TIME_16H] } + + def "A CsvWeatherSource throws NoDataException when no weather data is found for coordinate"() { + given: + def unknownCoordinate = GeoUtils.DEFAULT_GEOMETRY_FACTORY.createPoint(new Coordinate(0.0, 0.0)) + + when: + source.getWeather(IconWeatherTestData.TIME_15H, unknownCoordinate) + + then: + def ex = thrown(NoDataException) + ex.message.contains("No weather data found for the given coordinate") + ex.message.contains(unknownCoordinate.toString()) + } + + def "A CsvWeatherSource throws NoDataException when no weather data is found for coordinate at specific time"() { + given: + def futureTime = IconWeatherTestData.TIME_17H.plusHours(10) + + when: + source.getWeather(futureTime, IconWeatherTestData.COORDINATE_67775) + + then: + def ex = thrown(NoDataException) + ex.message.contains("No weather data found for the given coordinate") + ex.message.contains(IconWeatherTestData.COORDINATE_67775.toString()) + } + + def "A CsvWeatherSource throws NoDataException for mixed valid and invalid coordinates"() { + given: + def validCoordinate = IconWeatherTestData.COORDINATE_67775 + def invalidCoordinate = GeoUtils.DEFAULT_GEOMETRY_FACTORY.createPoint(new Coordinate(999d, 999d)) + def timeInterval = new ClosedInterval(IconWeatherTestData.TIME_15H, IconWeatherTestData.TIME_17H) + + when: + source.getWeather(timeInterval, [ + validCoordinate, + invalidCoordinate + ]) + + then: + def ex = thrown(NoDataException) + ex.message.contains("No data for given coordinates") + ex.message.contains(invalidCoordinate.toString()) + } } diff --git a/src/test/groovy/edu/ie3/datamodel/io/source/influxdb/InfluxDbWeatherSourceCosmoIT.groovy b/src/test/groovy/edu/ie3/datamodel/io/source/influxdb/InfluxDbWeatherSourceCosmoIT.groovy index d2b212a2a..28e6a7d81 100644 --- a/src/test/groovy/edu/ie3/datamodel/io/source/influxdb/InfluxDbWeatherSourceCosmoIT.groovy +++ b/src/test/groovy/edu/ie3/datamodel/io/source/influxdb/InfluxDbWeatherSourceCosmoIT.groovy @@ -5,6 +5,7 @@ */ package edu.ie3.datamodel.io.source.influxdb +import edu.ie3.datamodel.exceptions.NoDataException import edu.ie3.datamodel.io.connectors.InfluxDbConnector import edu.ie3.datamodel.io.factory.timeseries.CosmoTimeBasedWeatherValueFactory import edu.ie3.datamodel.models.timeseries.individual.IndividualTimeSeries @@ -65,8 +66,8 @@ class InfluxDbWeatherSourceCosmoIT extends Specification implements TestContaine def optTimeBasedValue = source.getWeather(CosmoWeatherTestData.TIME_15H, CosmoWeatherTestData.COORDINATE_193186) then: - optTimeBasedValue.present - equalsIgnoreUUID(optTimeBasedValue.get(), expectedTimeBasedValue) + optTimeBasedValue != null + equalsIgnoreUUID(optTimeBasedValue, expectedTimeBasedValue) } def "An InfluxDbWeatherSource can read multiple time series values for multiple coordinates"() { @@ -125,13 +126,12 @@ class InfluxDbWeatherSourceCosmoIT extends Specification implements TestContaine equalsIgnoreUUID(coordinateToTimeSeries.get(CosmoWeatherTestData.COORDINATE_193188).entries, timeseries_193188.entries) } - def "An InfluxDbWeatherSource will return an equivalent to 'empty' when being unable to map a coordinate to its ID"() { + def "An InfluxDbWeatherSource will throw NoDataException when being unable to map a coordinate to its ID"() { given: def validCoordinate = CosmoWeatherTestData.COORDINATE_193186 def invalidCoordinate = GeoUtils.buildPoint(7d, 48d) def time = CosmoWeatherTestData.TIME_15H def timeInterval = new ClosedInterval(CosmoWeatherTestData.TIME_15H, CosmoWeatherTestData.TIME_17H) - def emptyTimeSeries = new IndividualTimeSeries(UUID.randomUUID(), Collections.emptySet()) def timeseries_193186 = new IndividualTimeSeries(null, [ new TimeBasedValue(CosmoWeatherTestData.TIME_15H, CosmoWeatherTestData.WEATHER_VALUE_193186_15H), @@ -139,19 +139,32 @@ class InfluxDbWeatherSourceCosmoIT extends Specification implements TestContaine new TimeBasedValue(CosmoWeatherTestData.TIME_17H, CosmoWeatherTestData.WEATHER_VALUE_193186_17H) ] as Set) - when: - def coordinateAtDate = source.getWeather(time, invalidCoordinate) - def coordinateInInterval = source.getWeather(timeInterval, invalidCoordinate) - def coordinatesToTimeSeries = source.getWeather(timeInterval, [ + when: "requesting weather for an invalid coordinate at a specific date" + source.getWeather(time, invalidCoordinate) + + then: "NoDataException is thrown" + def ex1 = thrown(NoDataException) + ex1.message.contains("No coordinate ID found for the given point") + ex1.message.contains(invalidCoordinate.toString()) + + when: "requesting weather for an invalid coordinate in a time interval" + source.getWeather(timeInterval, invalidCoordinate) + + then: "NoDataException is thrown" + def ex2 = thrown(NoDataException) + ex2.message.contains("No data for given coordinates") + ex2.message.contains(invalidCoordinate.toString()) + + when: "requesting weather for mixed valid and invalid coordinates" + source.getWeather(timeInterval, [ validCoordinate, invalidCoordinate ]) - then: - coordinateAtDate == Optional.empty() - equalsIgnoreUUID(coordinateInInterval, emptyTimeSeries) - coordinatesToTimeSeries.keySet() == [validCoordinate].toSet() - equalsIgnoreUUID(coordinatesToTimeSeries.get(validCoordinate), timeseries_193186) + then: "NoDataException is thrown" + def ex3 = thrown(NoDataException) + ex3.message.contains("No data for given coordinates") + ex3.message.contains(invalidCoordinate.toString()) } def "A InfluxDbWeatherSource returns all time keys after a given time key correctly"() { diff --git a/src/test/groovy/edu/ie3/datamodel/io/source/influxdb/InfluxDbWeatherSourceIconIT.groovy b/src/test/groovy/edu/ie3/datamodel/io/source/influxdb/InfluxDbWeatherSourceIconIT.groovy index ce3a6dfcb..5bee62a39 100644 --- a/src/test/groovy/edu/ie3/datamodel/io/source/influxdb/InfluxDbWeatherSourceIconIT.groovy +++ b/src/test/groovy/edu/ie3/datamodel/io/source/influxdb/InfluxDbWeatherSourceIconIT.groovy @@ -5,6 +5,7 @@ */ package edu.ie3.datamodel.io.source.influxdb +import edu.ie3.datamodel.exceptions.NoDataException import edu.ie3.datamodel.io.connectors.InfluxDbConnector import edu.ie3.datamodel.io.factory.timeseries.IconTimeBasedWeatherValueFactory import edu.ie3.datamodel.models.timeseries.individual.IndividualTimeSeries @@ -63,8 +64,8 @@ class InfluxDbWeatherSourceIconIT extends Specification implements WeatherSource def optTimeBasedValue = source.getWeather(IconWeatherTestData.TIME_15H , IconWeatherTestData.COORDINATE_67775) then: - optTimeBasedValue.present - equalsIgnoreUUID(optTimeBasedValue.get(), expectedTimeBasedValue) + optTimeBasedValue != null + equalsIgnoreUUID(optTimeBasedValue, expectedTimeBasedValue) } def "An InfluxDbWeatherSource can read multiple time series values for multiple coordinates"() { @@ -118,13 +119,12 @@ class InfluxDbWeatherSourceIconIT extends Specification implements WeatherSource equalsIgnoreUUID(coordinateToTimeSeries.get(IconWeatherTestData.COORDINATE_67776).entries, timeseries67776.entries) } - def "An InfluxDbWeatherSource will return an equivalent to 'empty' when being unable to map a coordinate to its ID"() { + def "An InfluxDbWeatherSource will throw NoDataException when being unable to map a coordinate to its ID"() { given: def validCoordinate = IconWeatherTestData.COORDINATE_67775 def invalidCoordinate = GeoUtils.buildPoint(7d, 48d) def time = IconWeatherTestData.TIME_15H def timeInterval = new ClosedInterval(IconWeatherTestData.TIME_15H , IconWeatherTestData.TIME_17H) - def emptyTimeSeries = new IndividualTimeSeries(UUID.randomUUID(), Collections.emptySet()) def timeseries67775 = new IndividualTimeSeries(null, [ new TimeBasedValue(IconWeatherTestData.TIME_15H, IconWeatherTestData.WEATHER_VALUE_67775_15H), @@ -132,19 +132,32 @@ class InfluxDbWeatherSourceIconIT extends Specification implements WeatherSource new TimeBasedValue(IconWeatherTestData.TIME_17H, IconWeatherTestData.WEATHER_VALUE_67775_17H) ] as Set) - when: - def coordinateAtDate = source.getWeather(time, invalidCoordinate) - def coordinateInInterval = source.getWeather(timeInterval, invalidCoordinate) - def coordinatesToTimeSeries = source.getWeather(timeInterval, [ + when: "requesting weather for an invalid coordinate at a specific date" + source.getWeather(time, invalidCoordinate) + + then: "NoDataException is thrown" + def ex1 = thrown(NoDataException) + ex1.message.contains("No coordinate ID found for the given point") + ex1.message.contains(invalidCoordinate.toString()) + + when: "requesting weather for an invalid coordinate in a time interval" + source.getWeather(timeInterval, invalidCoordinate) + + then: "NoDataException is thrown" + def ex2 = thrown(NoDataException) + ex2.message.contains("No data for given coordinates") + ex2.message.contains(invalidCoordinate.toString()) + + when: "requesting weather for mixed valid and invalid coordinates" + source.getWeather(timeInterval, [ validCoordinate, invalidCoordinate ]) - then: - coordinateAtDate == Optional.empty() - equalsIgnoreUUID(coordinateInInterval, emptyTimeSeries) - coordinatesToTimeSeries.keySet() == [validCoordinate].toSet() - equalsIgnoreUUID(coordinatesToTimeSeries.get(validCoordinate), timeseries67775) + then: "NoDataException is thrown" + def ex3 = thrown(NoDataException) + ex3.message.contains("No data for given coordinates") + ex3.message.contains(invalidCoordinate.toString()) } def "The InfluxDbWeatherSource returns all time keys after a given time key correctly"() { diff --git a/src/test/groovy/edu/ie3/datamodel/io/source/sql/SqlWeatherSourceCosmoIT.groovy b/src/test/groovy/edu/ie3/datamodel/io/source/sql/SqlWeatherSourceCosmoIT.groovy index 1a7e0a0d3..954e1f54b 100644 --- a/src/test/groovy/edu/ie3/datamodel/io/source/sql/SqlWeatherSourceCosmoIT.groovy +++ b/src/test/groovy/edu/ie3/datamodel/io/source/sql/SqlWeatherSourceCosmoIT.groovy @@ -5,6 +5,7 @@ */ package edu.ie3.datamodel.io.source.sql +import edu.ie3.datamodel.exceptions.NoDataException import edu.ie3.datamodel.io.connectors.SqlConnector import edu.ie3.datamodel.io.factory.timeseries.CosmoTimeBasedWeatherValueFactory import edu.ie3.datamodel.models.timeseries.individual.IndividualTimeSeries @@ -56,16 +57,21 @@ class SqlWeatherSourceCosmoIT extends Specification implements TestContainerHelp def optTimeBasedValue = source.getWeather(CosmoWeatherTestData.TIME_15H, CosmoWeatherTestData.COORDINATE_193186) then: - optTimeBasedValue.present - equalsIgnoreUUID(optTimeBasedValue.get(), expectedTimeBasedValue ) + optTimeBasedValue != null + equalsIgnoreUUID(optTimeBasedValue, expectedTimeBasedValue ) } - def "A SqlWeatherSource returns nothing for an invalid coordinate"() { + def "A SqlWeatherSource throws NoDataException for an invalid coordinate"() { + given: + def invalidCoordinate = GeoUtils.buildPoint(89d, 88d) + when: - def optTimeBasedValue = source.getWeather(CosmoWeatherTestData.TIME_15H, GeoUtils.buildPoint(89d, 88d)) + source.getWeather(CosmoWeatherTestData.TIME_15H, invalidCoordinate) then: - optTimeBasedValue.empty + def ex = thrown(NoDataException) + ex.message.contains("No coordinate ID found for the given point") + ex.message.contains(invalidCoordinate.toString()) } def "A SqlWeatherSource can read multiple time series values for multiple coordinates"() { @@ -95,7 +101,7 @@ class SqlWeatherSourceCosmoIT extends Specification implements TestContainerHelp equalsIgnoreUUID(coordinateToTimeSeries.get(CosmoWeatherTestData.COORDINATE_193187), timeSeries193187) } - def "A SqlWeatherSource returns nothing for invalid coordinates"() { + def "A SqlWeatherSource throws NoDataException for invalid coordinates"() { given: def coordinates = [ GeoUtils.buildPoint(89d, 88d), @@ -104,10 +110,12 @@ class SqlWeatherSourceCosmoIT extends Specification implements TestContainerHelp def timeInterval = new ClosedInterval(CosmoWeatherTestData.TIME_16H, CosmoWeatherTestData.TIME_17H) when: - Map> coordinateToTimeSeries = source.getWeather(timeInterval, coordinates) + source.getWeather(timeInterval, coordinates) then: - coordinateToTimeSeries.keySet().empty + def ex = thrown(NoDataException) + ex.message.contains("No data for given coordinates") + ex.message.contains(coordinates.toString()) } def "A SqlWeatherSource can read all weather data in a given time interval"() { @@ -155,4 +163,22 @@ class SqlWeatherSourceCosmoIT extends Specification implements TestContainerHelp ] actual.get(CosmoWeatherTestData.COORDINATE_193187) == [CosmoWeatherTestData.TIME_16H] } + + def "A SqlWeatherSource throws NoDataException for mixed valid and invalid coordinates"() { + given: + def validCoordinate = CosmoWeatherTestData.COORDINATE_193186 + def invalidCoordinate = GeoUtils.buildPoint(999d, 999d) + def timeInterval = new ClosedInterval(CosmoWeatherTestData.TIME_15H, CosmoWeatherTestData.TIME_17H) + + when: + source.getWeather(timeInterval, [ + validCoordinate, + invalidCoordinate + ]) + + then: + def ex = thrown(NoDataException) + ex.message.contains("No data for given coordinates") + ex.message.contains(invalidCoordinate.toString()) + } } diff --git a/src/test/groovy/edu/ie3/datamodel/io/source/sql/SqlWeatherSourceIconIT.groovy b/src/test/groovy/edu/ie3/datamodel/io/source/sql/SqlWeatherSourceIconIT.groovy index 65cdb72d9..d8194d1cb 100644 --- a/src/test/groovy/edu/ie3/datamodel/io/source/sql/SqlWeatherSourceIconIT.groovy +++ b/src/test/groovy/edu/ie3/datamodel/io/source/sql/SqlWeatherSourceIconIT.groovy @@ -54,8 +54,8 @@ class SqlWeatherSourceIconIT extends Specification implements TestContainerHelpe when: def optTimeBasedValue = source.getWeather(IconWeatherTestData.TIME_15H, IconWeatherTestData.COORDINATE_67775) then: - optTimeBasedValue.present - equalsIgnoreUUID(optTimeBasedValue.get(), expectedTimeBasedValue ) + optTimeBasedValue != null + equalsIgnoreUUID(optTimeBasedValue, expectedTimeBasedValue ) } def "A NativeSqlWeatherSource can read multiple timeseries values for multiple coordinates"() {