sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] 02/02: Consolidation of the policy about handling of errors during statistics computation.
Date Sat, 29 Feb 2020 17:31:59 GMT
This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git

commit 5b6f88a31ef863a44f9a98b1f2614531d549b90f
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Sat Feb 29 17:06:26 2020 +0100

    Consolidation of the policy about handling of errors during statistics computation.
---
 .../java/org/apache/sis/image/AnnotatedImage.java  | 165 ++++++++++++++-------
 .../java/org/apache/sis/image/ImageOperations.java |  56 +++++--
 .../java/org/apache/sis/image/PlanarImage.java     |  15 ++
 .../org/apache/sis/image/StatisticsCalculator.java |  25 +++-
 .../sis/internal/coverage/j2d/TileOpExecutor.java  |   2 +-
 .../apache/sis/image/StatisticsCalculatorTest.java |  50 ++++++-
 .../java/org/apache/sis/util/logging/Logging.java  |   4 +-
 .../java/org/apache/sis/test/LoggingWatcher.java   |   5 +-
 8 files changed, 243 insertions(+), 79 deletions(-)

diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/AnnotatedImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/AnnotatedImage.java
index 86bbf28..d3f9d85 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/AnnotatedImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/AnnotatedImage.java
@@ -28,7 +28,9 @@ import java.awt.image.SampleModel;
 import java.awt.image.RenderedImage;
 import java.awt.image.Raster;
 import java.awt.image.WritableRaster;
+import java.awt.image.ImagingOpException;
 import org.apache.sis.util.ArraysExt;
+import org.apache.sis.util.logging.Logging;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.internal.system.Modules;
 import org.apache.sis.internal.coverage.j2d.TileOpExecutor;
@@ -40,10 +42,13 @@ import org.apache.sis.internal.coverage.j2d.TileOpExecutor;
  * image except {@link #getSources()} and the methods for getting the property names or values.
  *
  * <p>The name of the computed property is given by {@link #getComputedPropertyName()}.
- * In addition this method automatically creates another property with the same name
- * and {@value #ERRORS_SUFFIX} suffix. That property will contain a {@link LogRecord}
- * with the exception that occurred during tile computations, if any.
- * The computation results are cached by this class.</p>
+ * If an exception is thrown during calculation and {@link #failOnException} is {@code false},
+ * then {@code AnnotatedImage} automatically creates another property with the same name
and
+ * {@value #WARNINGS_SUFFIX} suffix. That property will contain the exception encapsulated
+ * in a {@link LogRecord} in order to retain additional information such as the instant when
+ * the first error occurred.</p>
+ *
+ * <p>The computation results are cached by this class.</p>
  *
  * <div class="note"><b>Design note:</b>
  * most non-abstract methods are final because {@link PixelIterator} (among others) relies
@@ -57,8 +62,10 @@ import org.apache.sis.internal.coverage.j2d.TileOpExecutor;
 abstract class AnnotatedImage implements RenderedImage {
     /**
      * The suffix to add to property name for errors that occurred during computation.
+     * A property with suffix is automatically created if an exception is thrown during
+     * computation and {@link #failOnException} is {@code false}.
      */
-    public static final String ERRORS_SUFFIX = ".errors";
+    public static final String WARNINGS_SUFFIX = ".warnings";
 
     /**
      * The source image from which to compute the property.
@@ -72,27 +79,35 @@ abstract class AnnotatedImage implements RenderedImage {
     private Object result;
 
     /**
-     * The errors that occurred while computing the result, or {@code null} if none
-     * or not yet determined.
+     * The errors that occurred while computing the result, or {@code null} if none or not
yet determined.
+     * This field is never set if {@link #failOnException} is {@code true}.
      */
     private LogRecord errors;
 
     /**
      * Whether parallel execution is authorized for the {@linkplain #source} image.
+     * If {@code true}, then {@link RenderedImage#getTile(int, int)} implementation should
be concurrent.
      */
     private final boolean parallel;
 
     /**
+     * Whether errors occurring during computation should be propagated instead than wrapped
in a {@link LogRecord}.
+     */
+    private final boolean failOnException;
+
+    /**
      * Creates a new annotated image wrapping the given image.
      * The annotations are the additional properties computed by the subclass.
      *
-     * @param  source    the image to wrap for adding properties (annotations).
-     * @parma  parallel  whether parallel execution is authorized.
+     * @param  source           the image to wrap for adding properties (annotations).
+     * @param  parallel         whether parallel execution is authorized.
+     * @param  failOnException  whether errors occurring during computation should be propagated.
      */
-    protected AnnotatedImage(final RenderedImage source, final boolean parallel) {
-        this.source   = source;
-        this.parallel = parallel;
-        result = Image.UndefinedProperty;
+    protected AnnotatedImage(final RenderedImage source, final boolean parallel, final boolean
failOnException) {
+        this.source          = source;
+        this.parallel        = parallel;
+        this.failOnException = failOnException;
+        this.result          = Image.UndefinedProperty;
     }
 
     /**
@@ -109,17 +124,6 @@ abstract class AnnotatedImage implements RenderedImage {
     }
 
     /**
-     * If the properties should be computed on a subset of the tiles,
-     * the pixel coordinates of the region intersecting those tiles.
-     * The default implementation returns {@code null}.
-     *
-     * @return pixel coordinates of the region of interest, or {@code null} for the whole
image.
-     */
-    protected Rectangle getAreaOfInterest() {
-        return null;
-    }
-
-    /**
      * Returns the name of the property which is computed by this image.
      *
      * @return name of property computed by this image. Shall not be null.
@@ -129,14 +133,20 @@ abstract class AnnotatedImage implements RenderedImage {
     /**
      * Returns an array of names recognized by {@link #getProperty(String)}.
      * The default implementation returns the {@linkplain #source} properties names
-     * followed by {@link #getComputedPropertyName()} and the error property name.
+     * followed by {@link #getComputedPropertyName()}. If that property has already
+     * been computed and an error occurred, then the names returned by this method
+     * will include the property name with {@value #WARNINGS_SUFFIX} suffix.
      *
      * @return all recognized property names.
      */
     @Override
     public String[] getPropertyNames() {
-        final String name = getComputedPropertyName();
-        return ArraysExt.concatenate(source.getPropertyNames(), new String[] {name, name
+ ERRORS_SUFFIX});
+        final String[] names = new String[(errors != null) ? 2 : 1];
+        names[0] = getComputedPropertyName();
+        if (errors != null) {
+            names[1] = names[0] + WARNINGS_SUFFIX;
+        }
+        return ArraysExt.concatenate(source.getPropertyNames(), names);
     }
 
     /**
@@ -145,17 +155,18 @@ abstract class AnnotatedImage implements RenderedImage {
      *
      * @param  cn    name of the computed property.
      * @param  name  the property name to test.
-     * @return whether {@code name} is {@code cn} + {@value #ERRORS_SUFFIX}.
+     * @return whether {@code name} is {@code cn} + {@value #WARNINGS_SUFFIX}.
      */
     private static boolean isErrorProperty(final String cn, final String name) {
-        return name.length() == cn.length() + ERRORS_SUFFIX.length() &&
-                    name.startsWith(cn) && name.endsWith(ERRORS_SUFFIX);
+        return name.length() == cn.length() + WARNINGS_SUFFIX.length() &&
+                    name.startsWith(cn) && name.endsWith(WARNINGS_SUFFIX);
     }
 
     /**
      * Gets a property from this image or from its source. If the given name is for the property
      * to be computed by this class and if that property has not been computed before, then
this
-     * method invokes {@link #computeProperty()} and caches the result.
+     * method invokes {@link #computeProperty(Rectangle)} with a {@code null} "area of interest"
+     * argument value. This {@code computeProperty(…)} result will be cached.
      *
      * @param  name  name of the property to get.
      * @return the property for the given name (may be {@code null}).
@@ -168,8 +179,12 @@ abstract class AnnotatedImage implements RenderedImage {
             if (isProperty || isErrorProperty(cn, name)) {
                 synchronized (this) {
                     if (result == Image.UndefinedProperty) try {
-                        result = computeProperty();
+                        result = computeProperty(null);
                     } catch (Exception e) {
+                        if (failOnException) {
+                            throw (ImagingOpException) new ImagingOpException(
+                                    Errors.format(Errors.Keys.CanNotCompute_1, cn)).initCause(e);
+                        }
                         result = null;
                         if (errors != null) {
                             errors.getThrown().addSuppressed(e);
@@ -184,7 +199,7 @@ abstract class AnnotatedImage implements RenderedImage {
                             setError(record);
                         }
                     }
-                    return isProperty ? result : errors;
+                    return isProperty ? cloneProperty(cn, result) : errors;
                 }
             }
         }
@@ -192,16 +207,13 @@ abstract class AnnotatedImage implements RenderedImage {
     }
 
     /**
-     * Invoked by {@link TileOpExecutor} if an error occurred while processing tiles.
-     * Can also be invoked by {@link #getProperty(String)} directly.
-     * This method shall be invoked at most once.
+     * Invoked by {@link TileOpExecutor} if an error occurred during calculation on a tiles.
+     * Can also be invoked by {@link #getProperty(String)} directly if the error occurred
+     * outside {@link TileOpExecutor}. This method shall be invoked at most once.
      *
      * @param  record  a description of the error that occurred.
      */
-    private synchronized void setError(final LogRecord record) {
-        if (errors != null) {
-            throw new IllegalStateException();      // Should never happen.
-        }
+    private void setError(final LogRecord record) {
         /*
          * Complete record with source identification as if the error occurred from
          * above `getProperty(String)` method (this is always the case, indirectly).
@@ -209,52 +221,84 @@ abstract class AnnotatedImage implements RenderedImage {
         record.setSourceClassName(AnnotatedImage.class.getCanonicalName());
         record.setSourceMethodName("getProperty");
         record.setLoggerName(Modules.RASTER);
-        errors = record;
+        synchronized (this) {
+            if (errors == null) {
+                errors = record;
+            } else {
+                throw new IllegalStateException();      // If it happens, this is a bug in
thie AnnotatedImage class.
+            }
+        }
+    }
+
+    /**
+     * If an error occurred, logs the message. The log record is cleared by this method call
+     * and will no longer be reported, unless the property is recomputed.
+     *
+     * @param  classe  the class to report as the source of the logging message.
+     * @param  method  the method to report as the source of the logging message.
+     */
+    final void logAndClearError(final Class<?> classe, String method) {
+        final LogRecord record;
+        synchronized (this) {
+            record = errors;
+            errors = null;
+        }
+        if (record != null) {
+            Logging.log(classe, method, record);
+        }
     }
 
     /**
      * Invoked when the property needs to be computed. If the property can not be computed,
      * then the result will be {@code null} and the exception thrown by this method will
be
-     * wrapped in a property of the same name with the {@value #ERRORS_SUFFIX} suffix.
+     * wrapped in a property of the same name with the {@value #WARNINGS_SUFFIX} suffix.
      *
      * <p>The default implementation makes the following choice:</p>
      * <ul class="verbose">
-     *   <li>If {@link #parallel} is {@code true} and the {@linkplain #getAreaOfInterest()
area of interest}
-     *       covers at least two tiles and {@link #collector()} returned a non-null value,
then this method
-     *       distributes the calculation of many threads using the functions provided by
the collector.
+     *   <li>If {@link #parallel} is {@code true}, {@link #collector()} returns a non-null
value
+     *       and the area of interest covers at least two tiles, then this method distributes
+     *       calculation on many threads using the functions provided by the collector.
      *       See {@link #collector()} Javadoc for more information.</li>
-     *   <li>Otherwise this method delegates to {@link #computeSequentially()}.</li>
+     *   <li>Otherwise this method delegates to {@link #computeSequentially(Rectangle)}.</li>
      * </ul>
      *
-     * @return the property value (may be {@code null}).
+     * The {@code areaOfInterest} argument is {@code null} by default, which means to calculate
+     * the property on all tiles. This argument exists for allowing subclasses to override
this
+     * method and invoke {@code super.computeProperty(…)} with a sub-region to compute.
+     *
+     * @param  areaOfInterest  pixel coordinates of the region of interest, or {@code null}
for the whole image.
+     * @return the computed property value. Note that {@code null} is a valid result.
      * @throws Exception if an error occurred while computing the property.
      */
-    protected Object computeProperty() throws Exception {
+    protected Object computeProperty(final Rectangle areaOfInterest) throws Exception {
         if (parallel) {
-            final TileOpExecutor executor = new TileOpExecutor(source, getAreaOfInterest());
+            final TileOpExecutor executor = new TileOpExecutor(source, areaOfInterest);
             if (executor.isMultiTiled()) {
                 final Collector<? super Raster,?,?> collector = collector();
                 if (collector != null) {
-                    return executor.executeOnReadable(source, collector(), this::setError);
+                    return executor.executeOnReadable(source, collector(), failOnException
? null : this::setError);
                 }
             }
         }
-        return computeSequentially();
+        return computeSequentially(areaOfInterest);
     }
 
     /**
      * Invoked when the property needs to be computed sequentially (all computations in current
thread).
      * If the property can not be computed, then the result will be {@code null} and the
exception thrown
-     * by this method will be wrapped in a property of the same name with the {@value #ERRORS_SUFFIX}
suffix.
+     * by this method will be wrapped in a property of the same name with the {@value #WARNINGS_SUFFIX}
suffix.
      *
      * <p>This method is invoked when this class does not support parallel execution
({@link #collector()}
      * returned {@code null}), or when it is not worth to parallelize (image has only one
tile), or when
      * the {@linkplain #source} image may be non-thread safe ({@link #parallel} is {@code
false}).</p>
      *
-     * @return the property value (may be {@code null}).
+     * @param  areaOfInterest  pixel coordinates of the region of interest, or {@code null}
for the whole image.
+     *         This is the argument given to {@link #computeProperty(Rectangle)} and can
usually be ignored
+     *         (because always {@code null}) if that method has not been overridden.
+     * @return the computed property value. Note that {@code null} is a valid result.
      * @throws Exception if an error occurred while computing the property.
      */
-    protected abstract Object computeSequentially() throws Exception;
+    protected abstract Object computeSequentially(Rectangle areaOfInterest) throws Exception;
 
     /**
      * Returns the function to execute for computing the property value, together with other
required functions
@@ -291,6 +335,19 @@ abstract class AnnotatedImage implements RenderedImage {
         return null;
     }
 
+    /**
+     * Invoked when a property of the given name has been requested and that property is
cached.
+     * If the property is mutable, subclasses may want to clone it before to return it to
users.
+     * The default implementation returns {@code value} unchanged.
+     *
+     * @param  name   the property name.
+     * @param  value  the property value.
+     * @return the property value to give to user.
+     */
+    protected Object cloneProperty(final String name, final Object value) {
+        return value;
+    }
+
     /** Delegates to the wrapped image. */
     @Override public final ColorModel     getColorModel()            {return source.getColorModel();}
     @Override public final SampleModel    getSampleModel()           {return source.getSampleModel();}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/ImageOperations.java b/core/sis-feature/src/main/java/org/apache/sis/image/ImageOperations.java
index 71bae50..c7ff008 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/ImageOperations.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/ImageOperations.java
@@ -16,34 +16,53 @@
  */
 package org.apache.sis.image;
 
+import java.util.logging.LogRecord;
 import java.awt.image.RenderedImage;
+import java.awt.image.ImagingOpException;
 import org.apache.sis.math.Statistics;
 import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.internal.system.Modules;
 
 
 /**
  * A predefined set of operations on images as convenience methods.
+ * Operations can be executed in parallel if applied on image with a thread-safe
+ * and concurrent implementation of {@link RenderedImage#getTile(int, int)}.
+ * Otherwise the same operations can be executed sequentially in the caller thread.
+ * Errors during calculation can either be propagated as an {@link ImagingOpException}
+ * (in which case no result is available), or notified as a {@link LogRecord}
+ * (in which case partial results may be available).
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.1
  * @since   1.1
  * @module
  */
-public final class ImageOperations {
+public class ImageOperations {
     /**
      * The set of operations with default configuration. Operations executed by this instance
      * will be multi-threaded if possible, and failures to compute a value cause an exception
      * to be thrown.
      */
-    public static final ImageOperations DEFAULT = new ImageOperations(true);
+    public static final ImageOperations PARALLEL = new ImageOperations(true, true);
 
     /**
      * The set of operations where all executions are constrained to a single thread.
      * Only the caller thread is used, with no parallelization. Sequential operations
      * may be useful for processing {@link RenderedImage} that may not be thread-safe.
-     * The error handling policy is the same than {@link #DEFAULT}.
+     * The error handling policy is the same than {@link #PARALLEL}.
      */
-    public static final ImageOperations SEQUENTIAL = new ImageOperations(false);
+    public static final ImageOperations SEQUENTIAL = new ImageOperations(false, true);
+
+    /**
+     * The set of operations executed without throwing an exception in case of failure.
+     * Instead the warnings are logged. Whether the operations are executed in parallel
+     * or not is implementation dependent.
+     *
+     * <p>Users should prefer {@link #PARALLEL} or {@link #SEQUENTIAL} in most cases
since the use
+     * of {@code LENIENT} may cause errors to be unnoticed (not everyone read log messages).</p>
+     */
+    public static final ImageOperations LENIENT = new ImageOperations(true, false);
 
     /**
      * Whether the operations can be executed in parallel.
@@ -51,26 +70,43 @@ public final class ImageOperations {
     private final boolean parallel;
 
     /**
+     * Whether errors occurring during computation should be propagated instead than wrapped
in a {@link LogRecord}.
+     */
+    private final boolean failOnException;
+
+    /**
      * Creates a new set of image operations.
      *
-     * @param  parallel  whether the operations can be executed in parallel.
+     * @param  parallel         whether the operations can be executed in parallel.
+     * @param  failOnException  whether errors occurring during computation should be propagated.
+     */
+    public ImageOperations(final boolean parallel, final boolean failOnException) {
+        this.parallel        = parallel;
+        this.failOnException = failOnException;
+    }
+
+    /**
+     * Whether the operations can be executed in parallel for the specified image.
+     * Should be a method overridden by {@link #LENIENT}, but for this simple need
+     * it is not yet worth to do sub-classing.
      */
-    private ImageOperations(final boolean parallel) {
-        this.parallel = parallel;
+    private boolean parallel(final RenderedImage source) {
+        return (this == LENIENT) ? source.getClass().getName().startsWith(Modules.CLASSNAME_PREFIX)
: parallel;
     }
 
     /**
-     * Returns statistics on all bands of the given image.
+     * Returns statistics (minimum, maximum, mean, standard deviation) on each bands of the
given image.
      *
      * @param  source  the image for which to compute statistics.
      * @return the statistics of sample values in each band.
+     * @throws ImagingOpException if an error occurred during calculation and {@code failOnException}
is {@code true}.
      */
     public Statistics[] statistics(final RenderedImage source) {
         ArgumentChecks.ensureNonNull("source", source);
-        final StatisticsCalculator calculator = new StatisticsCalculator(source, parallel);
+        final StatisticsCalculator calculator = new StatisticsCalculator(source, parallel(source),
failOnException);
         final Object property = calculator.getProperty(StatisticsCalculator.PROPERTY_NAME);
+        calculator.logAndClearError(ImageOperations.class, "statistics");
         if (property instanceof Statistics[]) {
-            // TODO: check error condition.
             return (Statistics[]) property;
         }
         return null;
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/PlanarImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/PlanarImage.java
index 6ee5141..7dd5393 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/PlanarImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/PlanarImage.java
@@ -173,6 +173,21 @@ public abstract class PlanarImage implements RenderedImage {
     }
 
     /**
+     * Returns the image location (<var>x</var>, <var>y</var>) and
image size (<var>width</var>, <var>height</var>).
+     * This is a convenience method encapsulating the results of 4 method calls in a single
object.
+     *
+     * @return the image location and image size as a new rectangle.
+     *
+     * @see #getMinX()
+     * @see #getMinY()
+     * @see #getWidth()
+     * @see #getHeight()
+     */
+    public Rectangle getBounds() {
+        return ImageUtilities.getBounds(this);
+    }
+
+    /**
      * Returns the minimum <var>x</var> coordinate (inclusive) of this image.
      *
      * <p>Default implementation returns zero.
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/StatisticsCalculator.java
b/core/sis-feature/src/main/java/org/apache/sis/image/StatisticsCalculator.java
index 12b3ebc..32a98ad 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/StatisticsCalculator.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/StatisticsCalculator.java
@@ -16,6 +16,7 @@
  */
 package org.apache.sis.image;
 
+import java.awt.Rectangle;
 import java.awt.image.Raster;
 import java.awt.image.RenderedImage;
 import java.awt.image.ImagingOpException;
@@ -43,11 +44,12 @@ final class StatisticsCalculator extends AnnotatedImage {
     /**
      * Creates a new calculator.
      *
-     * @param  image     the image for which to compute statistics.
-     * @parma  parallel  whether parallel execution is authorized.
+     * @param  image            the image for which to compute statistics.
+     * @param  parallel         whether parallel execution is authorized.
+     * @param  failOnException  whether errors occurring during computation should be propagated.
      */
-    StatisticsCalculator(final RenderedImage image, final boolean parallel) {
-        super(image, parallel);
+    StatisticsCalculator(final RenderedImage image, final boolean parallel, final boolean
failOnException) {
+        super(image, parallel, failOnException);
     }
 
     /**
@@ -109,11 +111,24 @@ final class StatisticsCalculator extends AnnotatedImage {
      * not worth to parallelize (image has only one tile), or when the source image may be
non-thread safe.
      */
     @Override
-    protected Object computeSequentially() {
+    protected Object computeSequentially(Rectangle areaOfInterest) {
         return computeSequentially(source);
     }
 
     /**
+     * Invoked when a property of the given name has been requested and that property is
cached.
+     * The property should be cloned before to be returned to the user in order to protect
this image state.
+     */
+    @Override
+    protected Object cloneProperty(final String name, final Object value) {
+        final Statistics[] result = ((Statistics[]) value).clone();
+        for (int i=0; i<result.length; i++) {
+            result[i] = result[i].clone();
+        }
+        return result;
+    }
+
+    /**
      * Returns the function to execute for parallel computation of statistics,
      * together with other required functions (supplier of accumulator, combiner, finisher).
      */
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/TileOpExecutor.java
b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/TileOpExecutor.java
index 3d21ada..4e8ce47 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/TileOpExecutor.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/TileOpExecutor.java
@@ -664,7 +664,7 @@ public class TileOpExecutor {
             synchronized (this) {
                 if (errors == null) {
                     errors = Resources.forLocale((Locale) null).getLogRecord(Level.WARNING,
-                             Resources.Keys.CanNotUpdateTile_2, indices.tx, indices.ty);
+                             Resources.Keys.CanNotProcessTile_2, indices.tx, indices.ty);
                     errors.setThrown(ex);
                 } else {
                     errors.getThrown().addSuppressed(ex);
diff --git a/core/sis-feature/src/test/java/org/apache/sis/image/StatisticsCalculatorTest.java
b/core/sis-feature/src/test/java/org/apache/sis/image/StatisticsCalculatorTest.java
index 1bdc0a3..7be7cc1 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/image/StatisticsCalculatorTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/image/StatisticsCalculatorTest.java
@@ -18,11 +18,16 @@ package org.apache.sis.image;
 
 import java.util.Random;
 import java.awt.image.DataBuffer;
+import java.awt.image.ImagingOpException;
+import org.apache.sis.internal.system.Modules;
 import org.apache.sis.math.Statistics;
+import org.apache.sis.test.LoggingWatcher;
+import org.apache.sis.util.logging.Logging;
 import org.apache.sis.test.TestCase;
+import org.junit.Rule;
 import org.junit.Test;
 
-import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.*;
 
 
 /**
@@ -43,6 +48,12 @@ public final strictfp class StatisticsCalculatorTest extends TestCase {
     private static final int TILE_WIDTH = 5, TILE_HEIGHT = 3;
 
     /**
+     * Intercepts log records for verifying them.
+     */
+    @Rule
+    public final LoggingWatcher loggings = new LoggingWatcher(Logging.getLogger(Modules.RASTER));
+
+    /**
      * Creates a dummy image for testing purpose. This image will contain many small tiles
      * of two bands. The first bands has deterministic values and the second band contains
      * random values.
@@ -71,7 +82,7 @@ public final strictfp class StatisticsCalculatorTest extends TestCase {
     public void testParallelExecution() {
         final TiledImageMock image = createImage();
         final Statistics[] expected = StatisticsCalculator.computeSequentially(image);
-        final Statistics[] actual = ImageOperations.DEFAULT.statistics(image);
+        final Statistics[] actual = ImageOperations.PARALLEL.statistics(image);
         for (int i=0; i<expected.length; i++) {
             final Statistics e = expected[i];
             final Statistics a = actual  [i];
@@ -79,16 +90,43 @@ public final strictfp class StatisticsCalculatorTest extends TestCase
{
             assertEquals("maximum", e.maximum(), a.maximum(), STRICT);
             assertEquals("sum",     e.sum(),     a.sum(),     STRICT);
         }
+        loggings.assertNoUnexpectedLog();
     }
 
     /**
-     * Tests with random failures.
+     * Tests with random failures propagated as exceptions.
      */
     @Test
     public void testWithFailures() {
         final TiledImageMock image = createImage();
-        image.failRandomly(new Random());
-        final Statistics[] stats = ImageOperations.DEFAULT.statistics(image);
-        // TODO: clarify the policy on error handling.
+        image.failRandomly(new Random(-8739538736973900203L));
+        try {
+            ImageOperations.PARALLEL.statistics(image);
+            fail("Expected ImagingOpException.");
+        } catch (ImagingOpException e) {
+            final String message = e.getMessage();
+            assertTrue(message, message.contains("statistics"));
+            assertNotNull("Expected a cause.", e.getCause());
+        }
+        loggings.assertNoUnexpectedLog();
+    }
+
+    /**
+     * Tests with random failures that are logged.
+     */
+    @Test
+    public void testWithLoggings() {
+        final TiledImageMock image = createImage();
+        image.failRandomly(new Random(8004277484984714811L));
+        final Statistics[] stats = ImageOperations.LENIENT.statistics(image);
+        for (final Statistics a : stats) {
+            assertTrue(a.count() > 0);
+        }
+        /*
+         * Verifies that a logging message has been emitted because of the errors.
+         * All errors (there is many) should have been consolidated in a single record.
+         */
+        loggings.assertNextLogContains(/* no keywords we could rely on. */);
+        loggings.assertNoUnexpectedLog();
     }
 }
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/logging/Logging.java b/core/sis-utility/src/main/java/org/apache/sis/util/logging/Logging.java
index 9539899..5be09a9 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/logging/Logging.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/logging/Logging.java
@@ -200,8 +200,8 @@ public final class Logging extends Static {
      *   <li>{@linkplain Logger#log(LogRecord) Log} the modified record.</li>
      * </ul>
      *
-     * @param  classe  the class for which to obtain a logger.
-     * @param  method  the name of the method which is logging a record.
+     * @param  classe  the class to report as the source of the logging message.
+     * @param  method  the method to report as the source of the logging message.
      * @param  record  the record to log.
      */
     public static void log(final Class<?> classe, String method, final LogRecord record)
{
diff --git a/core/sis-utility/src/test/java/org/apache/sis/test/LoggingWatcher.java b/core/sis-utility/src/test/java/org/apache/sis/test/LoggingWatcher.java
index 0c025a3..e4a8c9e 100644
--- a/core/sis-utility/src/test/java/org/apache/sis/test/LoggingWatcher.java
+++ b/core/sis-utility/src/test/java/org/apache/sis/test/LoggingWatcher.java
@@ -125,6 +125,9 @@ public final strictfp class LoggingWatcher extends TestWatcher implements
Filter
     /**
      * Invoked (indirectly) when a tested method has emitted a log message.
      * This method adds the logging message to the {@link #messages} list.
+     *
+     * @param  record  the intercepted log record.
+     * @return {@code true} if verbose mode, or {@code false} is quiet mode.
      */
     @Override
     public final boolean isLoggable(final LogRecord record) {
@@ -169,7 +172,7 @@ public final strictfp class LoggingWatcher extends TestWatcher implements
Filter
         final String message = messages.remove();
         for (final String word : keywords) {
             if (!message.contains(word)) {
-                fail("Expected the logging message to contains the “"+ word + "” word
but got:\n" + message);
+                fail("Expected the logging message to contains the “" + word + "” word
but got:\n" + message);
             }
         }
     }


Mime
View raw message