This is an automated email from the ASF dual-hosted git repository. ggregory pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/commons-lang.git
commit da246244aebf91571b0689e3dee97fce0ad52b23 Author: Gary Gregory <garydgreg...@gmail.com> AuthorDate: Sun Sep 22 08:11:40 2024 -0400 Add StopWatch.run([Failable]Runnable) and get([Failable]Supplier) --- pom.xml | 6 ++ src/changes/changes.xml | 1 + .../org/apache/commons/lang3/time/StopWatch.java | 102 +++++++++++++++++-- .../apache/commons/lang3/time/StopWatchTest.java | 109 +++++++++++++++------ 4 files changed, 179 insertions(+), 39 deletions(-) diff --git a/pom.xml b/pom.xml index cd18b14ca..496eff197 100644 --- a/pom.xml +++ b/pom.xml @@ -82,6 +82,12 @@ <version>5.4.0</version> <scope>test</scope> </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-core</artifactId> + <version>4.11.0</version> + <scope>test</scope> + </dependency> <!-- For Javadoc links --> <dependency> <groupId>org.apache.commons</groupId> diff --git a/src/changes/changes.xml b/src/changes/changes.xml index b9952c966..17afee8a0 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -54,6 +54,7 @@ The <action> type attribute can be add,update,fix,remove. <action type="fix" dev="ggregory" due-to="OSS-Fuzz, Gary Gregory">Rewrite ClassUtils.getClass(...) without recursion to avoid StackOverflowError on very long inputs. OSS-Fuzz Issue 42522972: apache-commons-text:StringSubstitutorInterpolatorFuzzer: Security exception in org.apache.commons.lang3.ClassUtils.getClass.</action> <!-- ADD --> <action type="add" dev="ggregory" due-to="Gary Gregory">Add Strings and refactor StringUtils.</action> + <action type="add" dev="ggregory" due-to="Gary Gregory">Add StopWatch.run([Failable]Runnable) and get([Failable]Supplier).</action> <!-- UPDATE --> <action type="update" dev="ggregory" due-to="Gary Gregory, Dependabot">Bump org.apache.commons:commons-parent from 73 to 75 #1267, #1277.</action> </release> diff --git a/src/main/java/org/apache/commons/lang3/time/StopWatch.java b/src/main/java/org/apache/commons/lang3/time/StopWatch.java index 6164c9932..5b81fa94c 100644 --- a/src/main/java/org/apache/commons/lang3/time/StopWatch.java +++ b/src/main/java/org/apache/commons/lang3/time/StopWatch.java @@ -21,10 +21,12 @@ import java.time.Duration; import java.time.Instant; import java.util.Objects; import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.function.FailableConsumer; import org.apache.commons.lang3.function.FailableRunnable; +import org.apache.commons.lang3.function.FailableSupplier; /** * {@link StopWatch} provides a convenient API for timings. @@ -286,6 +288,23 @@ public class StopWatch { return DurationFormatUtils.formatDurationHMS(getTime()); } + /** + * Delegates to {@link Supplier#get()} while recording the duration of the call. + * + * @param <T> the type of results supplied by this supplier. + * @param supplier The supplier to {@link Supplier#get()}. + * @return a result from the given Supplier. + * @since 3.18.0 + */ + public <T> T get(final Supplier<T> supplier) { + startResume(); + try { + return supplier.get(); + } finally { + suspend(); + } + } + /** * Gets the Duration on the StopWatch. * @@ -322,14 +341,16 @@ public class StopWatch { * @since 3.0 */ public long getNanoTime() { - if (runningState == State.STOPPED || runningState == State.SUSPENDED) { + switch (runningState) { + case STOPPED: + case SUSPENDED: return stopTimeNanos - startTimeNanos; - } - if (runningState == State.UNSTARTED) { + case UNSTARTED: return 0; - } - if (runningState == State.RUNNING) { + case RUNNING: return System.nanoTime() - startTimeNanos; + default: + break; } throw new IllegalStateException("Illegal running state has occurred."); } @@ -443,6 +464,25 @@ public class StopWatch { return stopInstant.toEpochMilli(); } + /** + * Delegates to {@link FailableSupplier#get()} while recording the duration of the call. + * + * @param <T> the type of results supplied by this supplier. + * @param <E> The kind of thrown exception or error. + * @param supplier The supplier to {@link Supplier#get()}. + * @return a result from the given Supplier. + * @throws Throwable if the supplier fails. + * @since 3.18.0 + */ + public <T, E extends Throwable> T getT(final FailableSupplier<T, E> supplier) throws Throwable { + startResume(); + try { + return supplier.get(); + } finally { + suspend(); + } + } + /** * Gets the time on the StopWatch. * @@ -544,6 +584,38 @@ public class StopWatch { runningState = State.RUNNING; } + /** + * Delegates to {@link Runnable#run()} while recording the duration of the call. + * + * @param runnable The runnable to {@link Runnable#run()}. + * @since 3.18.0 + */ + public void run(final Runnable runnable) { + startResume(); + try { + runnable.run(); + } finally { + suspend(); + } + } + + /** + * Delegates to {@link FailableRunnable#run()} while recording the duration of the call. + * + * @param <E> The kind of {@link Throwable}. + * @param runnable The runnable to {@link FailableRunnable#run()}. + * @throws Throwable Thrown by {@link FailableRunnable#run()}. + * @since 3.18.0 + */ + public <E extends Throwable> void runT(final FailableRunnable<E> runnable) throws Throwable { + startResume(); + try { + runnable.run(); + } finally { + suspend(); + } + } + /** * Splits the time. * @@ -563,7 +635,7 @@ public class StopWatch { } /** - * Starts the StopWatch. + * Starts this StopWatch. * * <p> * This method starts a new timing session, clearing any previous values. @@ -584,7 +656,18 @@ public class StopWatch { } /** - * Stops the StopWatch. + * Starts or resumes this StopWatch. + */ + private void startResume() { + if (isStopped()) { + start(); + } else if (isSuspended()) { + resume(); + } + } + + /** + * Stops this StopWatch. * * <p> * This method ends a new timing session, allowing the time to be retrieved. @@ -604,7 +687,7 @@ public class StopWatch { } /** - * Suspends the StopWatch for later resumption. + * Suspends this StopWatch for later resumption. * * <p> * This method suspends the watch until it is resumed. The watch will not include time between the suspend and resume calls in the total time. @@ -656,7 +739,7 @@ public class StopWatch { } /** - * Removes a split. + * Removes the split. * * <p> * This method clears the stop time. The start time is unaffected, enabling timing from the original start point to continue. @@ -671,4 +754,5 @@ public class StopWatch { splitState = SplitState.UNSPLIT; } + } diff --git a/src/test/java/org/apache/commons/lang3/time/StopWatchTest.java b/src/test/java/org/apache/commons/lang3/time/StopWatchTest.java index 04986fba3..48f06ed44 100644 --- a/src/test/java/org/apache/commons/lang3/time/StopWatchTest.java +++ b/src/test/java/org/apache/commons/lang3/time/StopWatchTest.java @@ -27,9 +27,11 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.io.IOException; import java.time.Duration; import java.time.Instant; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import org.apache.commons.lang3.AbstractLangTest; import org.apache.commons.lang3.ThreadUtils; @@ -50,17 +52,13 @@ public class StopWatchTest extends AbstractLangTest { /** * <p> - * Creates a suspended StopWatch object which appears to have elapsed for the requested amount of time in - * nanoseconds. + * Creates a suspended StopWatch object which appears to have elapsed for the requested amount of time in nanoseconds. * <p> * <p> * * <pre> * // Create a mock StopWatch with a time of 2:59:01.999 - * final long nanos = TimeUnit.HOURS.toNanos(2) - * + TimeUnit.MINUTES.toNanos(59) - * + TimeUnit.SECONDS.toNanos(1) - * + TimeUnit.MILLISECONDS.toNanos(999); + * final long nanos = TimeUnit.HOURS.toNanos(2) + TimeUnit.MINUTES.toNanos(59) + TimeUnit.SECONDS.toNanos(1) + TimeUnit.MILLISECONDS.toNanos(999); * final StopWatch watch = createMockStopWatch(nanos); * </pre> * @@ -100,42 +98,31 @@ public class StopWatchTest extends AbstractLangTest { @Test public void testBadStates() { final StopWatch watch = new StopWatch(); - assertThrows(IllegalStateException.class, watch::stop, - "Calling stop on an unstarted StopWatch should throw an exception. "); + assertThrows(IllegalStateException.class, watch::stop, "Calling stop on an unstarted StopWatch should throw an exception. "); - assertThrows(IllegalStateException.class, watch::suspend, - "Calling suspend on an unstarted StopWatch should throw an exception. "); + assertThrows(IllegalStateException.class, watch::suspend, "Calling suspend on an unstarted StopWatch should throw an exception. "); - assertThrows(IllegalStateException.class, watch::split, - "Calling split on a non-running StopWatch should throw an exception. "); + assertThrows(IllegalStateException.class, watch::split, "Calling split on a non-running StopWatch should throw an exception. "); - assertThrows(IllegalStateException.class, watch::unsplit, - "Calling unsplit on an unsplit StopWatch should throw an exception. "); + assertThrows(IllegalStateException.class, watch::unsplit, "Calling unsplit on an unsplit StopWatch should throw an exception. "); - assertThrows(IllegalStateException.class, watch::resume, - "Calling resume on an unsuspended StopWatch should throw an exception. "); + assertThrows(IllegalStateException.class, watch::resume, "Calling resume on an unsuspended StopWatch should throw an exception. "); watch.start(); - assertThrows(IllegalStateException.class, watch::start, - "Calling start on a started StopWatch should throw an exception. "); + assertThrows(IllegalStateException.class, watch::start, "Calling start on a started StopWatch should throw an exception. "); - assertThrows(IllegalStateException.class, watch::unsplit, - "Calling unsplit on an unsplit StopWatch should throw an exception. "); + assertThrows(IllegalStateException.class, watch::unsplit, "Calling unsplit on an unsplit StopWatch should throw an exception. "); - assertThrows(IllegalStateException.class, watch::getSplitTime, - "Calling getSplitTime on an unsplit StopWatch should throw an exception. "); + assertThrows(IllegalStateException.class, watch::getSplitTime, "Calling getSplitTime on an unsplit StopWatch should throw an exception. "); - assertThrows(IllegalStateException.class, watch::getSplitDuration, - "Calling getSplitTime on an unsplit StopWatch should throw an exception. "); + assertThrows(IllegalStateException.class, watch::getSplitDuration, "Calling getSplitTime on an unsplit StopWatch should throw an exception. "); - assertThrows(IllegalStateException.class, watch::resume, - "Calling resume on an unsuspended StopWatch should throw an exception. "); + assertThrows(IllegalStateException.class, watch::resume, "Calling resume on an unsuspended StopWatch should throw an exception. "); watch.stop(); - assertThrows(IllegalStateException.class, watch::start, - "Calling start on a stopped StopWatch should throw an exception as it needs to be reset. "); + assertThrows(IllegalStateException.class, watch::start, "Calling start on a stopped StopWatch should throw an exception as it needs to be reset. "); } @Test @@ -197,6 +184,33 @@ public class StopWatchTest extends AbstractLangTest { assertThat("formatTime", formatTime, not(startsWith(MESSAGE))); } + @Test + public void testGet() throws Throwable { + final StopWatch watch = new StopWatch(); + final AtomicInteger i = new AtomicInteger(); + assertEquals(1, watch.get(i::incrementAndGet)); + assertEquals(2, watch.getT(i::incrementAndGet)); + final IOException e = assertThrows(IOException.class, () -> watch.getT(this::throwIOException)); + assertEquals("A", e.getMessage()); + // test state + assertTrue(watch.isSuspended()); + assertEquals(3, watch.get(() -> { + assertTrue(watch.isStarted()); + return i.incrementAndGet(); + })); + assertTrue(watch.isSuspended()); + final long nanos1 = watch.getDuration().toNanos(); + assertTrue(nanos1 >= 0); + // test state + assertTrue(watch.isSuspended()); + assertEquals(4, watch.getT(() -> { + assertTrue(watch.isStarted()); + return i.incrementAndGet(); + })); + assertTrue(watch.isSuspended()); + assertTrue(watch.getDuration().toNanos() >= nanos1); + } + @Test public void testGetDuration() throws InterruptedException { final StopWatch watch = new StopWatch(); @@ -301,6 +315,37 @@ public class StopWatchTest extends AbstractLangTest { assertThat("stopWatch.toSplitString", stopWatch.toSplitString(), startsWith(MESSAGE)); } + @Test + public void testRun() throws Throwable { + final StopWatch watch = new StopWatch(); + final AtomicInteger i = new AtomicInteger(); + watch.run(i::incrementAndGet); + assertEquals(1, i.get()); + watch.runT(i::incrementAndGet); + assertEquals(2, i.get()); + final IOException e = assertThrows(IOException.class, () -> watch.runT(this::throwIOException)); + assertEquals("A", e.getMessage()); + // test state + assertTrue(watch.isSuspended()); + watch.run(() -> { + assertTrue(watch.isStarted()); + i.incrementAndGet(); + }); + assertEquals(3, i.get()); + assertTrue(watch.isSuspended()); + final long nanos1 = watch.getDuration().toNanos(); + assertTrue(nanos1 > 0); + // test state + assertTrue(watch.isSuspended()); + watch.runT(() -> { + assertTrue(watch.isStarted()); + i.incrementAndGet(); + }); + assertEquals(4, i.get()); + assertTrue(watch.isSuspended()); + assertTrue(watch.getDuration().toNanos() >= nanos1); + } + @Test public void testSimple() throws InterruptedException { final StopWatch watch = StopWatch.createStarted(); @@ -421,14 +466,14 @@ public class StopWatchTest extends AbstractLangTest { assertTrue(totalTimeFromNanos >= sleepMillisX2, () -> String.format("totalTimeFromNanos %s >= sleepMillisX2 %s", totalTimeFromNanos, sleepMillisX2)); assertTrue(totalDuration.compareTo(Duration.ofMillis(sleepMillisX2)) >= 0, () -> String.format("totalDuration >= sleepMillisX2", totalDuration, sleepMillisX2)); - ; + // Be lenient for slow running builds final long testTooLongMillis = sleepMillis * 100; assertTrue(totalTimeFromNanos < testTooLongMillis, () -> String.format("totalTimeFromNanos %s < testTooLongMillis %s", totalTimeFromNanos, testTooLongMillis)); assertTrue(totalDuration.compareTo(Duration.ofMillis(testTooLongMillis)) < 0, () -> String.format("totalDuration %s < testTooLongMillis %s", totalDuration, testTooLongMillis)); - ; + } @Test @@ -471,4 +516,8 @@ public class StopWatchTest extends AbstractLangTest { final String splitStr = watch.toString(); assertEquals(SPLIT_CLOCK_STR_LEN + MESSAGE.length() + 1, splitStr.length(), "Formatted split string not the correct length"); } + + private int throwIOException() throws IOException { + throw new IOException("A"); + } }