From 854c390f99fd8c5dc03ffdc08d15d34b5264da3a Mon Sep 17 00:00:00 2001 From: Martin Vehovsky Date: Sun, 12 Apr 2020 23:17:29 +0200 Subject: [PATCH 1/2] few basic unit tests for DefaultProgressBarRenderer --- .../DefaultProgressBarRenderer.java | 11 ++-- .../me/tongfei/progressbar/ProgressState.java | 2 +- .../DefaultProgressBarRendererTest.java | 61 +++++++++++++++++++ 3 files changed, 68 insertions(+), 6 deletions(-) create mode 100644 src/test/java/me/tongfei/progressbar/DefaultProgressBarRendererTest.java diff --git a/src/main/java/me/tongfei/progressbar/DefaultProgressBarRenderer.java b/src/main/java/me/tongfei/progressbar/DefaultProgressBarRenderer.java index 3dd1730..4873b75 100644 --- a/src/main/java/me/tongfei/progressbar/DefaultProgressBarRenderer.java +++ b/src/main/java/me/tongfei/progressbar/DefaultProgressBarRenderer.java @@ -6,6 +6,7 @@ /** * Default progress bar renderer (see {@link ProgressBarRenderer}). + * * @author Tongfei Chen * @since 0.8.0 */ @@ -33,7 +34,7 @@ public class DefaultProgressBarRenderer implements ProgressBarRenderer { // Number of full blocks private int progressIntegralPart(ProgressState progress, int length) { - return (int)(progress.getNormalizedProgress() * length); + return (int) (progress.getNormalizedProgress() * length); } private int progressFractionalPart(ProgressState progress, int length) { @@ -78,10 +79,10 @@ public String render(ProgressState progress, int maxLength) { String prefix = progress.task + " " + percentage(progress) + " " + style.leftBracket; int maxSuffixLength = Math.max(maxLength - prefix.length(), 0); - String speedString = isSpeedShown ? speed(progress, elapsed) : ""; + String speedString = isSpeedShown ? " " + speed(progress, elapsed) : ""; String suffix = style.rightBracket + " " + ratio(progress) + " (" - + Util.formatDuration(elapsed) + " / " + eta(progress, elapsed) + ") " - + speedString + progress.extraMessage; + + Util.formatDuration(elapsed) + " / " + eta(progress, elapsed) + ")" + + speedString + (progress.extraMessage.isEmpty() ? "" : " " + progress.extraMessage); // trim excessive suffix if (suffix.length() > maxSuffixLength) suffix = suffix.substring(0, maxSuffixLength); @@ -93,7 +94,7 @@ public String render(ProgressState progress, int maxLength) { // case of indefinite progress bars if (progress.indefinite) { - int pos = (int)(progress.current % length); + int pos = (int) (progress.current % length); sb.append(Util.repeat(style.space, pos)); sb.append(style.block); sb.append(Util.repeat(style.space, length - pos - 1)); diff --git a/src/main/java/me/tongfei/progressbar/ProgressState.java b/src/main/java/me/tongfei/progressbar/ProgressState.java index 8df285d..9d81711 100644 --- a/src/main/java/me/tongfei/progressbar/ProgressState.java +++ b/src/main/java/me/tongfei/progressbar/ProgressState.java @@ -45,7 +45,7 @@ synchronized void stepTo(long n) { } synchronized void setExtraMessage(String msg) { - extraMessage = msg; + extraMessage = msg.trim(); } String getTask() { diff --git a/src/test/java/me/tongfei/progressbar/DefaultProgressBarRendererTest.java b/src/test/java/me/tongfei/progressbar/DefaultProgressBarRendererTest.java new file mode 100644 index 0000000..abc5c4a --- /dev/null +++ b/src/test/java/me/tongfei/progressbar/DefaultProgressBarRendererTest.java @@ -0,0 +1,61 @@ +package me.tongfei.progressbar; + +import java.text.DecimalFormat; +import java.time.Instant; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class DefaultProgressBarRendererTest { + + public final String taskName = "Test"; + private final int maxLength = 80; + private final ProgressBarStyle style = ProgressBarStyle.ASCII; + + @Test + public void basic() { + ProgressState state = new ProgressState(taskName, 100); + ProgressBarRenderer renderer = new DefaultProgressBarRenderer(style, "", 1, false, null); + + state.stepBy(10); + state.startTime = Instant.now(); + String result = renderer.render(state, maxLength); + + String expected = taskName + " 10% [====> ] 10/100 (0:00:00 / 0:00:00)"; + assertEquals(expected, result); + + // assert blank extra message + state.setExtraMessage(" "); + state.startTime = Instant.now(); + result = renderer.render(state, maxLength); + assertEquals(expected, result); + } + + @Test + public void withSpeedBasicDecimalFormat() { + ProgressState state = new ProgressState(taskName, 10240); + ProgressBarRenderer renderer = new DefaultProgressBarRenderer(style, "Mb", 1024, true, new DecimalFormat("#")); + + state.stepBy(2048); + state.startTime = Instant.now().minusSeconds(1); + String result = renderer.render(state, maxLength); + + String expected = taskName + " 20% [======> ] 2/10Mb (0:00:01 / 0:00:03) 2Mb/s"; + assertEquals(expected, result); + } + + @Test + public void withSpeedAndDecimalFormat() { + ProgressState state = new ProgressState(taskName, 10240); + ProgressBarRenderer renderer = new DefaultProgressBarRenderer(style, "Mb", 1024, true, new DecimalFormat("#.#")); + + state.stepBy(2048); + state.setExtraMessage("downloading.."); + state.startTime = Instant.now().minusSeconds(10); + String result = renderer.render(state, maxLength); + + String expected = taskName + " 20% [===> ] 2/10Mb (0:00:10 / 0:00:39) 0.2Mb/s downloading.."; + assertEquals(expected, result); + } +} \ No newline at end of file From e76db795d323a16016a258e8682f9346b67796ad Mon Sep 17 00:00:00 2001 From: Martin Vehovsky Date: Mon, 13 Apr 2020 23:39:02 +0200 Subject: [PATCH 2/2] initial implementation for child ProgressBar --- .../ConsoleProgressBarConsumer.java | 5 +- .../DefaultProgressBarRenderer.java | 76 ++++++++++------ .../DelegatingProgressBarConsumer.java | 6 +- ...InteractiveConsoleProgressBarConsumer.java | 24 +++++- .../me/tongfei/progressbar/ProgressBar.java | 43 ++++------ .../progressbar/ProgressBarConsumer.java | 20 +---- .../progressbar/ProgressBarRenderer.java | 4 +- .../tongfei/progressbar/ProgressBarStyle.java | 14 ++- .../me/tongfei/progressbar/ProgressState.java | 83 ++++++++++-------- .../progressbar/ProgressStateImmutable.java | 86 +++++++++++++++++++ .../tongfei/progressbar/ProgressThread.java | 24 +++--- .../DefaultProgressBarRendererTest.java | 22 ++--- .../progressbar/MockProgressBarBuilder.java | 4 + 13 files changed, 274 insertions(+), 137 deletions(-) create mode 100644 src/main/java/me/tongfei/progressbar/ProgressStateImmutable.java diff --git a/src/main/java/me/tongfei/progressbar/ConsoleProgressBarConsumer.java b/src/main/java/me/tongfei/progressbar/ConsoleProgressBarConsumer.java index 62a1715..cadd376 100644 --- a/src/main/java/me/tongfei/progressbar/ConsoleProgressBarConsumer.java +++ b/src/main/java/me/tongfei/progressbar/ConsoleProgressBarConsumer.java @@ -1,6 +1,7 @@ package me.tongfei.progressbar; import java.io.PrintStream; +import java.util.List; import static me.tongfei.progressbar.TerminalUtils.MOVE_CURSOR_TO_LINE_START; @@ -26,8 +27,8 @@ public int getMaxProgressLength() { } @Override - public void accept(String str) { - out.print(MOVE_CURSOR_TO_LINE_START + str); + public void accept(List str) { + out.print(MOVE_CURSOR_TO_LINE_START + str.get(0)); } @Override diff --git a/src/main/java/me/tongfei/progressbar/DefaultProgressBarRenderer.java b/src/main/java/me/tongfei/progressbar/DefaultProgressBarRenderer.java index 4873b75..a586b0c 100644 --- a/src/main/java/me/tongfei/progressbar/DefaultProgressBarRenderer.java +++ b/src/main/java/me/tongfei/progressbar/DefaultProgressBarRenderer.java @@ -1,8 +1,8 @@ package me.tongfei.progressbar; import java.text.DecimalFormat; -import java.time.Duration; -import java.time.Instant; +import java.util.LinkedList; +import java.util.List; /** * Default progress bar renderer (see {@link ProgressBarRenderer}). @@ -12,11 +12,11 @@ */ public class DefaultProgressBarRenderer implements ProgressBarRenderer { - private ProgressBarStyle style; - private String unitName; - private long unitSize; - private boolean isSpeedShown; - private DecimalFormat speedFormat; + private final ProgressBarStyle style; + private final String unitName; + private final long unitSize; + private final boolean isSpeedShown; + private final DecimalFormat speedFormat; DefaultProgressBarRenderer( ProgressBarStyle style, @@ -33,55 +33,81 @@ public class DefaultProgressBarRenderer implements ProgressBarRenderer { } // Number of full blocks - private int progressIntegralPart(ProgressState progress, int length) { + private int progressIntegralPart(ProgressStateImmutable progress, int length) { return (int) (progress.getNormalizedProgress() * length); } - private int progressFractionalPart(ProgressState progress, int length) { + private int progressFractionalPart(ProgressStateImmutable progress, int length) { double p = progress.getNormalizedProgress() * length; double fraction = (p - Math.floor(p)) * style.fractionSymbols.length(); return (int) Math.floor(fraction); } - private String eta(ProgressState progress, Duration elapsed) { + private String eta(ProgressStateImmutable progress) { if (progress.max <= 0 || progress.indefinite) return "?"; + else if (!progress.getChildren().isEmpty()) return Util.formatDuration(progress.eta()); else if (progress.current == 0) return "?"; - else return Util.formatDuration( - elapsed.dividedBy(progress.current).multipliedBy(progress.max - progress.current) - ); + else return Util.formatDuration(progress.eta()); } - private String percentage(ProgressState progress) { + private String percentage(ProgressStateImmutable progress) { String res; if (progress.max <= 0 || progress.indefinite) res = "? %"; - else res = String.valueOf((int) Math.floor(100.0 * progress.current / progress.max)) + "%"; + else res = progress.progress() + "%"; return Util.repeat(' ', 4 - res.length()) + res; } - private String ratio(ProgressState progress) { + private String ratio(ProgressStateImmutable progress) { String m = progress.indefinite ? "?" : String.valueOf(progress.max / unitSize); String c = String.valueOf(progress.current / unitSize); return Util.repeat(' ', m.length() - c.length()) + c + "/" + m + unitName; } - private String speed(ProgressState progress, Duration elapsed) { - if (elapsed.getSeconds() == 0) return "?" + unitName + "/s"; - double speed = (double) progress.current / elapsed.getSeconds(); + private String speed(ProgressStateImmutable progress) { + if (progress.elapsed.getSeconds() == 0) return "?" + unitName + "/s"; + double speed = progress.speed(); double speedWithUnit = speed / unitSize; return speedFormat.format(speedWithUnit) + unitName + "/s"; } - public String render(ProgressState progress, int maxLength) { + public List render(ProgressState progress, int maxLength) { + return render(progress.getState(), maxLength, 0, false); + } + + private List render(ProgressStateImmutable progress, int maxLength, int subChild, boolean isLast) { + LinkedList result = new LinkedList<>(); + List children = progress.getChildren(); + for (ProgressStateImmutable childState : children) { + List render = render(childState, maxLength, subChild + 1, children.get(children.size() - 1) == childState); + result.addAll(render); + } + + result.add(0, doRender(progress, maxLength, subChild, isLast)); + return result; + } + + private String doRender(ProgressStateImmutable progress, int maxLength, int subChild, boolean isLast) { + + String prefix = ""; + for (int i = 0; i < subChild; i++) { + if (i == subChild - 1) { + if (isLast) { + prefix += style.upRight; + } else { + prefix += style.verticalRight; + } + } else { + prefix += style.vertical; + } + } - Instant currTime = Instant.now(); - Duration elapsed = Duration.between(progress.startTime, currTime); + prefix += progress.task + " " + percentage(progress) + " " + style.leftBracket; - String prefix = progress.task + " " + percentage(progress) + " " + style.leftBracket; int maxSuffixLength = Math.max(maxLength - prefix.length(), 0); - String speedString = isSpeedShown ? " " + speed(progress, elapsed) : ""; + String speedString = isSpeedShown ? " " + speed(progress) : ""; String suffix = style.rightBracket + " " + ratio(progress) + " (" - + Util.formatDuration(elapsed) + " / " + eta(progress, elapsed) + ")" + + Util.formatDuration(progress.elapsed) + " / " + eta(progress) + ")" + speedString + (progress.extraMessage.isEmpty() ? "" : " " + progress.extraMessage); // trim excessive suffix if (suffix.length() > maxSuffixLength) diff --git a/src/main/java/me/tongfei/progressbar/DelegatingProgressBarConsumer.java b/src/main/java/me/tongfei/progressbar/DelegatingProgressBarConsumer.java index 80aa1ba..72c3f30 100644 --- a/src/main/java/me/tongfei/progressbar/DelegatingProgressBarConsumer.java +++ b/src/main/java/me/tongfei/progressbar/DelegatingProgressBarConsumer.java @@ -1,9 +1,11 @@ package me.tongfei.progressbar; +import java.util.List; import java.util.function.Consumer; /** * Progress bar consumer that delegates the progress bar handling to a custom {@link Consumer}. + * * @author Alex Peelman * @since 0.8.0 */ @@ -27,8 +29,8 @@ public int getMaxProgressLength() { } @Override - public void accept(String str) { - this.consumer.accept(str); + public void accept(List strs) { + strs.forEach(this.consumer); } @Override diff --git a/src/main/java/me/tongfei/progressbar/InteractiveConsoleProgressBarConsumer.java b/src/main/java/me/tongfei/progressbar/InteractiveConsoleProgressBarConsumer.java index afd1c67..cfdde68 100644 --- a/src/main/java/me/tongfei/progressbar/InteractiveConsoleProgressBarConsumer.java +++ b/src/main/java/me/tongfei/progressbar/InteractiveConsoleProgressBarConsumer.java @@ -1,6 +1,8 @@ package me.tongfei.progressbar; import java.io.PrintStream; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; import static me.tongfei.progressbar.TerminalUtils.*; @@ -13,22 +15,38 @@ public class InteractiveConsoleProgressBarConsumer extends ConsoleProgressBarCon private boolean initialized = false; int position = 1; + int childOffset = 1; public InteractiveConsoleProgressBarConsumer(PrintStream out) { super(out); } @Override - public void accept(String str) { + public void accept(List str) { if (!initialized) { TerminalUtils.filterActiveConsumers(InteractiveConsoleProgressBarConsumer.class).forEach(c -> c.position++); TerminalUtils.activeConsumers.add(this); - out.println(MOVE_CURSOR_TO_LINE_START + str); + out.println(MOVE_CURSOR_TO_LINE_START + str.get(0)); initialized = true; return; } + if (str.size() > childOffset) { + int offset = str.size() - childOffset; + childOffset += offset; + position += offset; + AtomicReference takeWhile = new AtomicReference<>(true); + TerminalUtils.filterActiveConsumers(InteractiveConsoleProgressBarConsumer.class).forEach(consumer -> { + if (consumer == this) takeWhile.set(false); + if (takeWhile.get()) consumer.position += offset; + }); + for (int i = 0; i < offset; i++) { + out.println(MOVE_CURSOR_TO_LINE_START); + } + } - out.print(moveCursorUp(position) + str + moveCursorDown(position)); + for (int i = str.size() - 1; i >= 0; i--) { + out.print(moveCursorUp(position - i) + str.get(i) + moveCursorDown(position - i)); + } } @Override diff --git a/src/main/java/me/tongfei/progressbar/ProgressBar.java b/src/main/java/me/tongfei/progressbar/ProgressBar.java index cb8eabd..22434fa 100644 --- a/src/main/java/me/tongfei/progressbar/ProgressBar.java +++ b/src/main/java/me/tongfei/progressbar/ProgressBar.java @@ -8,7 +8,6 @@ import java.io.InputStream; import java.io.PrintStream; import java.text.DecimalFormat; -import java.time.Instant; import java.util.Iterator; import java.util.Spliterator; import java.util.concurrent.ExecutionException; @@ -27,7 +26,7 @@ public class ProgressBar implements AutoCloseable { private ProgressState progress; - private ProgressThread target; + private final ProgressThread target; private ScheduledFuture scheduled; /** @@ -113,21 +112,13 @@ public ProgressBar( ProgressBarConsumer consumer ) { this.progress = new ProgressState(task, initialMax); - this.target = new ProgressThread(progress, renderer, updateIntervalMillis, consumer); - // starts the progress bar upon construction - progress.startTime = Instant.now(); + this.target = new ProgressThread(this.progress, renderer, consumer); scheduled = Util.executor.scheduleAtFixedRate(target, 0, updateIntervalMillis, TimeUnit.MILLISECONDS); } - /** - * Starts this progress bar. - * @deprecated Please use the Java try-with-resource pattern instead. - */ - @Deprecated - public ProgressBar start() { - progress.startTime = Instant.now(); - scheduled = Util.executor.scheduleAtFixedRate(target, 0, target.updateInterval, TimeUnit.MILLISECONDS); - return this; + private ProgressBar(ProgressState progress, ProgressThread target) { + this.progress = progress; + this.target = target; } /** @@ -162,10 +153,10 @@ public ProgressBar step() { */ public ProgressBar maxHint(long n) { if (n < 0) - progress.setAsIndefinite(); + progress.setIndefinite(true); else { - progress.setAsDefinite(); - progress.maxHint(n); + progress.setIndefinite(false); + progress.setMax(n); } return this; } @@ -178,8 +169,7 @@ public ProgressBar maxHint(long n) { public ProgressBar stop() { try { close(); - } - catch (Exception e) { /* ignored, for backward compatibility */ } + } catch (Exception e) { /* ignored, for backward compatibility */ } return this; } @@ -209,32 +199,32 @@ public ProgressBar setExtraMessage(String msg) { return this; } - /** + /** * Returns the current progress. */ public long getCurrent() { - return progress.getCurrent(); + return progress.getState().getCurrent(); } /** * Returns the maximum value of this progress bar. */ public long getMax() { - return progress.getMax(); + return progress.getState().getMax(); } /** * Returns the name of this task. */ public String getTask() { - return progress.getTask(); + return progress.getState().getTask(); } /** * Returns the extra message at the end of the progress bar. */ public String getExtraMessage() { - return progress.getExtraMessage(); + return progress.getState().getExtraMessage(); } // STATIC WRAPPER METHODS @@ -353,4 +343,9 @@ public static > Stream wrap(S stream, ProgressB return StreamSupport.stream(sp, stream.isParallel()); } + public ProgressBar createChild(String task, long initialMax) { + ProgressState progress = new ProgressState(task, initialMax); + this.progress.addChild(progress); + return new ProgressBar(progress, target); + } } diff --git a/src/main/java/me/tongfei/progressbar/ProgressBarConsumer.java b/src/main/java/me/tongfei/progressbar/ProgressBarConsumer.java index 7db7667..14d5849 100644 --- a/src/main/java/me/tongfei/progressbar/ProgressBarConsumer.java +++ b/src/main/java/me/tongfei/progressbar/ProgressBarConsumer.java @@ -1,5 +1,6 @@ package me.tongfei.progressbar; +import java.util.List; import java.util.function.Consumer; /** @@ -8,7 +9,7 @@ * @author Alex Peelman * @author Tongfei Chen */ -public interface ProgressBarConsumer extends Consumer, Appendable, AutoCloseable { +public interface ProgressBarConsumer extends Consumer>, AutoCloseable { /** * Returns the maximum length allowed for the rendered form of a progress bar. @@ -19,22 +20,7 @@ public interface ProgressBarConsumer extends Consumer, Appendable, AutoC * Accepts a rendered form of a progress bar, e.g., prints to a specified stream. * @param rendered Rendered form of a progress bar, a string */ - void accept(String rendered); - - default ProgressBarConsumer append(CharSequence csq) { - accept(csq.toString()); - return this; - } - - default ProgressBarConsumer append(CharSequence csq, int start, int end) { - accept(csq.subSequence(start, end).toString()); - return this; - } - - default ProgressBarConsumer append(char c) { - accept(String.valueOf(c)); - return this; - } + void accept(List rendered); void close(); diff --git a/src/main/java/me/tongfei/progressbar/ProgressBarRenderer.java b/src/main/java/me/tongfei/progressbar/ProgressBarRenderer.java index c794b64..b1f73bb 100644 --- a/src/main/java/me/tongfei/progressbar/ProgressBarRenderer.java +++ b/src/main/java/me/tongfei/progressbar/ProgressBarRenderer.java @@ -1,5 +1,7 @@ package me.tongfei.progressbar; +import java.util.List; + /** * Renders a {@link ProgressState} into a string. * @author Tongfei Chen @@ -14,6 +16,6 @@ public interface ProgressBarRenderer { * @param maxLength The maximum length as dictated by the consumer * @return Rendered string to be consumed by the consumer */ - String render(ProgressState progress, int maxLength); + List render(ProgressState progress, int maxLength); } diff --git a/src/main/java/me/tongfei/progressbar/ProgressBarStyle.java b/src/main/java/me/tongfei/progressbar/ProgressBarStyle.java index 59dadf7..89cbf00 100644 --- a/src/main/java/me/tongfei/progressbar/ProgressBarStyle.java +++ b/src/main/java/me/tongfei/progressbar/ProgressBarStyle.java @@ -7,13 +7,13 @@ */ public enum ProgressBarStyle { - COLORFUL_UNICODE_BLOCK("\r", "\u001b[33m│", "│\u001b[0m", '█', ' ', " ▏▎▍▌▋▊▉"), + COLORFUL_UNICODE_BLOCK("\r", "\u001b[33m│", "│\u001b[0m", '█', ' ', " ▏▎▍▌▋▊▉", " ┣━ ", " ┗━ ", " ┃ "), /** Use Unicode block characters to draw the progress bar. */ - UNICODE_BLOCK("\r", "│", "│", '█', ' ', " ▏▎▍▌▋▊▉"), + UNICODE_BLOCK("\r", "│", "│", '█', ' ', " ▏▎▍▌▋▊▉", " ┣━ ", " ┗━ ", " ┃ "), /** Use only ASCII characters to draw the progress bar. */ - ASCII("\r", "[", "]", '=', ' ', ">"); + ASCII("\r", "[", "]", '=', ' ', ">", " |- ", " '- ", " | "); String refreshPrompt; String leftBracket; @@ -21,14 +21,20 @@ public enum ProgressBarStyle { char block; char space; String fractionSymbols; + String verticalRight; + String upRight; + String vertical; - ProgressBarStyle(String refreshPrompt, String leftBracket, String rightBracket, char block, char space, String fractionSymbols) { + ProgressBarStyle(String refreshPrompt, String leftBracket, String rightBracket, char block, char space, String fractionSymbols, String verticalRight, String upRight, String vertical) { this.refreshPrompt = refreshPrompt; this.leftBracket = leftBracket; this.rightBracket = rightBracket; this.block = block; this.space = space; this.fractionSymbols = fractionSymbols; + this.verticalRight = verticalRight; + this.upRight = upRight; + this.vertical = vertical; } } diff --git a/src/main/java/me/tongfei/progressbar/ProgressState.java b/src/main/java/me/tongfei/progressbar/ProgressState.java index 9d81711..8d1efe6 100644 --- a/src/main/java/me/tongfei/progressbar/ProgressState.java +++ b/src/main/java/me/tongfei/progressbar/ProgressState.java @@ -1,6 +1,13 @@ package me.tongfei.progressbar; +import java.time.Duration; import java.time.Instant; +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; + +import static java.util.Collections.emptyList; + /** * Encapsulates the internal states of a progress bar. @@ -9,12 +16,15 @@ */ class ProgressState { - String task; - boolean indefinite = false; - long current = 0; - long max = 0; - Instant startTime = null; - String extraMessage = ""; + private final String task; + private boolean indefinite = false; + private long current = 0; + private long max = 0; + private Instant startTime = Instant.now(); + private String extraMessage = ""; + private final List children = new LinkedList<>(); + private Duration elapsed; + private boolean done = false; ProgressState(String task, long initialMax) { this.task = task; @@ -22,52 +32,49 @@ class ProgressState { if (initialMax < 0) indefinite = true; } - synchronized void setAsDefinite() { - indefinite = false; + synchronized ProgressStateImmutable getState() { + List childrenStates = emptyList(); + if (!children.isEmpty()) { + childrenStates = children.stream().map(ProgressState::getState).collect(Collectors.toList()); + stepTo(childrenStates.stream().filter(ProgressStateImmutable::isDone).count()); + } + if (!done) { + elapsed = Duration.between(startTime, Instant.now()); + } + return new ProgressStateImmutable(task, indefinite, current, max, startTime, extraMessage, elapsed, childrenStates); } - synchronized void setAsIndefinite() { - indefinite = true; + synchronized void addChild(ProgressState child) { + children.add(child); + max = children.size(); } - synchronized void maxHint(long n) { - max = n; + synchronized void setIndefinite(boolean indefinite) { + this.indefinite = indefinite; } - synchronized void stepBy(long n) { - current += n; - if (current > max) max = current; - } - - synchronized void stepTo(long n) { - current = n; - if (current > max) max = current; + synchronized void setMax(long max) { + this.max = max; } synchronized void setExtraMessage(String msg) { extraMessage = msg.trim(); } - String getTask() { - return task; + synchronized void setStartTime(Instant startTime) { + this.startTime = startTime; } - synchronized String getExtraMessage() { - return extraMessage; - } - - synchronized long getCurrent() { - return current; - } - - synchronized long getMax() { - return max; + synchronized void stepBy(long n) { + stepTo(current + n); } - // The progress, normalized to range [0, 1]. - synchronized double getNormalizedProgress() { - if (max <= 0) return 0.0; - else return ((double)current) / max; + synchronized void stepTo(long n) { + current = n; + if (current > max) max = current; + if (current == max) { + elapsed = Duration.between(startTime, Instant.now()); + done = true; + } } - -} +} \ No newline at end of file diff --git a/src/main/java/me/tongfei/progressbar/ProgressStateImmutable.java b/src/main/java/me/tongfei/progressbar/ProgressStateImmutable.java new file mode 100644 index 0000000..30b607c --- /dev/null +++ b/src/main/java/me/tongfei/progressbar/ProgressStateImmutable.java @@ -0,0 +1,86 @@ +package me.tongfei.progressbar; + +import java.time.Duration; +import java.time.Instant; +import java.util.List; + +class ProgressStateImmutable { + final String task; + final boolean indefinite; + final long current; + final long max; + final Instant startTime; + final String extraMessage; + final List children; + final Duration elapsed; + + public ProgressStateImmutable(String task, boolean indefinite, long current, long max, Instant startTime, String extraMessage, Duration elapsed, List children) { + this.task = task; + this.indefinite = indefinite; + this.current = current; + this.max = max; + this.startTime = startTime; + this.extraMessage = extraMessage; + this.children = children; + this.elapsed = elapsed; + } + + int progress() { + if (!children.isEmpty()) { + return (int) children.stream().mapToInt(ProgressStateImmutable::progress).average().orElse(0); + } + return (int) Math.floor(100.0 * current / max); + } + + // The progress, normalized to range [0, 1]. + double getNormalizedProgress() { + if (!children.isEmpty()) + return children.stream().mapToDouble(ProgressStateImmutable::getNormalizedProgress).average().orElse(0); + if (max <= 0) return 0.0; + else return ((double) current) / max; + } + + Duration eta() { + if (!children.isEmpty()) + return children.stream().map(ProgressStateImmutable::eta).max(Duration::compareTo).orElse(Duration.ZERO); + return elapsed.dividedBy(current).multipliedBy(max - current); + } + + double speed() { + if (!children.isEmpty()) + return children.stream().mapToDouble(ProgressStateImmutable::speed).average().orElse(0); + return (double) current / elapsed.getSeconds(); + } + + boolean isDone() { + return current == max; + } + + public String getTask() { + return task; + } + + public boolean isIndefinite() { + return indefinite; + } + + public long getCurrent() { + return current; + } + + public long getMax() { + return max; + } + + public Instant getStartTime() { + return startTime; + } + + public String getExtraMessage() { + return extraMessage; + } + + public List getChildren() { + return children; + } +} \ No newline at end of file diff --git a/src/main/java/me/tongfei/progressbar/ProgressThread.java b/src/main/java/me/tongfei/progressbar/ProgressThread.java index c685e47..9d1a7af 100644 --- a/src/main/java/me/tongfei/progressbar/ProgressThread.java +++ b/src/main/java/me/tongfei/progressbar/ProgressThread.java @@ -1,5 +1,7 @@ package me.tongfei.progressbar; +import java.util.List; + /** * @author Tongfei Chen * @since 0.5.0 @@ -8,24 +10,21 @@ class ProgressThread implements Runnable { private ProgressState progress; private ProgressBarRenderer renderer; - long updateInterval; private ProgressBarConsumer consumer; private boolean active = true; ProgressThread( ProgressState progress, ProgressBarRenderer renderer, - long updateInterval, ProgressBarConsumer consumer ) { this.progress = progress; this.renderer = renderer; - this.updateInterval = updateInterval; this.consumer = consumer; } private void refresh() { - String rendered = renderer.render(progress, consumer.getMaxProgressLength()); + List rendered = renderer.render(progress, consumer.getMaxProgressLength()); consumer.accept(rendered); } @@ -35,14 +34,19 @@ public void setActive(boolean active) { @Override public void run() { - if (!active) { + try { + if (!active) { + refresh(); + consumer.close(); + TerminalUtils.closeTerminal(); + return; + } + refresh(); - consumer.close(); - TerminalUtils.closeTerminal(); - return; + } catch (Exception e) { + //FIXME: ensure exception in renderer/consumer is not swallowed. What to do really?? + e.printStackTrace(); } - - refresh(); } } diff --git a/src/test/java/me/tongfei/progressbar/DefaultProgressBarRendererTest.java b/src/test/java/me/tongfei/progressbar/DefaultProgressBarRendererTest.java index abc5c4a..e21e625 100644 --- a/src/test/java/me/tongfei/progressbar/DefaultProgressBarRendererTest.java +++ b/src/test/java/me/tongfei/progressbar/DefaultProgressBarRendererTest.java @@ -19,29 +19,29 @@ public void basic() { ProgressBarRenderer renderer = new DefaultProgressBarRenderer(style, "", 1, false, null); state.stepBy(10); - state.startTime = Instant.now(); - String result = renderer.render(state, maxLength); + state.setStartTime(Instant.now()); + String result = renderer.render(state, maxLength).get(0); String expected = taskName + " 10% [====> ] 10/100 (0:00:00 / 0:00:00)"; assertEquals(expected, result); // assert blank extra message state.setExtraMessage(" "); - state.startTime = Instant.now(); - result = renderer.render(state, maxLength); + state.setStartTime(Instant.now()); + result = renderer.render(state, maxLength).get(0); assertEquals(expected, result); } @Test public void withSpeedBasicDecimalFormat() { - ProgressState state = new ProgressState(taskName, 10240); + ProgressState state = new ProgressState(taskName, 102400); ProgressBarRenderer renderer = new DefaultProgressBarRenderer(style, "Mb", 1024, true, new DecimalFormat("#")); - state.stepBy(2048); - state.startTime = Instant.now().minusSeconds(1); - String result = renderer.render(state, maxLength); + state.stepBy(20480); + state.setStartTime(Instant.now().minusSeconds(10)); + String result = renderer.render(state, maxLength).get(0); - String expected = taskName + " 20% [======> ] 2/10Mb (0:00:01 / 0:00:03) 2Mb/s"; + String expected = taskName + " 20% [======> ] 20/100Mb (0:00:10 / 0:00:39) 2Mb/s"; assertEquals(expected, result); } @@ -52,8 +52,8 @@ public void withSpeedAndDecimalFormat() { state.stepBy(2048); state.setExtraMessage("downloading.."); - state.startTime = Instant.now().minusSeconds(10); - String result = renderer.render(state, maxLength); + state.setStartTime(Instant.now().minusSeconds(10)); + String result = renderer.render(state, maxLength).get(0); String expected = taskName + " 20% [===> ] 2/10Mb (0:00:10 / 0:00:39) 0.2Mb/s downloading.."; assertEquals(expected, result); diff --git a/src/test/java/me/tongfei/progressbar/MockProgressBarBuilder.java b/src/test/java/me/tongfei/progressbar/MockProgressBarBuilder.java index 54f333b..eee7313 100644 --- a/src/test/java/me/tongfei/progressbar/MockProgressBarBuilder.java +++ b/src/test/java/me/tongfei/progressbar/MockProgressBarBuilder.java @@ -8,10 +8,14 @@ public class MockProgressBarBuilder extends ProgressBarBuilder { + public final String taskName = "Test"; private final ByteArrayOutputStream out = new ByteArrayOutputStream(); public MockProgressBarBuilder() { try { + setTaskName(taskName); + setStyle(ProgressBarStyle.ASCII); + setUpdateIntervalMillis(100); setConsumer(new InteractiveConsoleProgressBarConsumer(new PrintStream(out, true, UTF_8.name()))); } catch (UnsupportedEncodingException e) { throw new RuntimeException("This should never happen!");