Skip to content

Commit

Permalink
Merge pull request #1680 from jplag/feature/idleBars
Browse files Browse the repository at this point in the history
Added idle bars
  • Loading branch information
tsaglam authored May 3, 2024
2 parents 2312b70 + 2efba00 commit 649a595
Show file tree
Hide file tree
Showing 8 changed files with 231 additions and 31 deletions.
4 changes: 2 additions & 2 deletions cli/src/main/java/de/jplag/cli/CLI.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@

import de.jplag.JPlag;
import de.jplag.JPlagResult;
import de.jplag.cli.logger.CliProgressBarProvider;
import de.jplag.cli.logger.CollectedLoggerFactory;
import de.jplag.cli.logger.TongfeiProgressBarProvider;
import de.jplag.cli.picocli.CliInputHandler;
import de.jplag.exceptions.ExitException;
import de.jplag.logging.ProgressBarLogger;
Expand Down Expand Up @@ -45,7 +45,7 @@ public void executeCli() throws ExitException, IOException {
logger.debug("Your version of JPlag is {}", JPlag.JPLAG_VERSION);

if (!this.inputHandler.parse()) {
ProgressBarLogger.setProgressBarProvider(new TongfeiProgressBarProvider());
ProgressBarLogger.setProgressBarProvider(new CliProgressBarProvider());

switch (this.inputHandler.getCliOptions().mode) {
case RUN -> runJPlag();
Expand Down
26 changes: 26 additions & 0 deletions cli/src/main/java/de/jplag/cli/logger/CliProgressBarProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package de.jplag.cli.logger;

import de.jplag.logging.ProgressBar;
import de.jplag.logging.ProgressBarProvider;
import de.jplag.logging.ProgressBarType;

import me.tongfei.progressbar.ProgressBarBuilder;
import me.tongfei.progressbar.ProgressBarStyle;

/**
* A ProgressBar provider, that used the tongfei progress bar library underneath, to show progress bars on the cli.
*/
public class CliProgressBarProvider implements ProgressBarProvider {
@Override
public ProgressBar initProgressBar(ProgressBarType type, int totalSteps) {
if (type.isIdleBar()) {
IdleBar idleBar = new IdleBar(type.getDefaultText());
idleBar.start();
return idleBar;
} else {
me.tongfei.progressbar.ProgressBar progressBar = new ProgressBarBuilder().setTaskName(type.getDefaultText()).setInitialMax(totalSteps)
.setStyle(ProgressBarStyle.ASCII).build();
return new TongfeiProgressBar(progressBar);
}
}
}
103 changes: 103 additions & 0 deletions cli/src/main/java/de/jplag/cli/logger/IdleBar.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package de.jplag.cli.logger;

import java.io.IOException;
import java.io.PrintStream;

import org.apache.commons.lang3.time.DurationFormatUtils;
import org.jline.terminal.Terminal;
import org.jline.terminal.TerminalBuilder;

import de.jplag.logging.ProgressBar;

/**
* Prints an idle progress bar, that does not count upwards.
*/
public class IdleBar implements ProgressBar {
private final PrintStream output;

private final Thread runner;

private long startTime;
private final String text;
private int length;

private int currentPos;
private int currentDirection;

private boolean running = false;

public IdleBar(String text) {
this.output = System.out;
this.runner = new Thread(this::run);
this.length = 50;
this.currentDirection = -1;
this.currentPos = 0;
this.text = text;
try {
Terminal terminal = TerminalBuilder.terminal();
this.length = Math.min(terminal.getWidth() / 2, terminal.getWidth() - 50);
terminal.close();
} catch (IOException ignore) {
// ignore exceptions here. If we cannot access the terminal, we guess a width
}
if (this.length < 10) {
this.length = 10;
}
}

public void start() {
this.startTime = System.currentTimeMillis();
this.running = true;
this.runner.start();
}

@Override
public void dispose() {
this.running = false;
try {
this.runner.join();
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
}
this.output.println();
}

private void run() {
while (running) {
this.output.print('\r');
this.output.print(printLine());
if (currentPos == 0 || currentPos == length - 1) {
currentDirection *= -1;
}
try {
Thread.sleep(200);
} catch (InterruptedException ignore) {
Thread.currentThread().interrupt();
}
currentPos += currentDirection;
}
}

private String printLine() {
StringBuilder line = new StringBuilder();
line.append(this.text).append(' ');

line.append('<');
line.append(" ".repeat(Math.max(0, currentPos)));
line.append("<+>");
line.append(" ".repeat(Math.max(0, length - currentPos - 1)));
line.append('>');

long timeRunning = System.currentTimeMillis() - this.startTime;
line.append(' ');
String duration = DurationFormatUtils.formatDuration(timeRunning, "H:mm:ss");
line.append(duration);

return line.toString();
}

@Override
public void step(int number) {
// does nothing, because the idle bar has no steps
}
}

This file was deleted.

70 changes: 70 additions & 0 deletions cli/src/test/java/de/jplag/cli/logger/IdleBarTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package de.jplag.cli.logger;

import java.io.ByteArrayOutputStream;
import java.io.PrintStream;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

class IdleBarTest {
private static final String TEST_BAR_TEXT = "Test";
private static final long IDLE_BAR_ANIMATION_DELAY = 200;

private static final int TARGET_FRAME_NUMBER = 5;

/**
* Tests if the output of the idle bar looks plausible
*/
@Test
void testIdleBarPlausible() {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
PrintStream oldSystemOut = System.out;
System.setOut(new PrintStream(outputStream));

IdleBar idleBar = new IdleBar(TEST_BAR_TEXT);
idleBar.start();
while (outputStream.toString().split("\\r").length <= TARGET_FRAME_NUMBER) {
Thread.yield();
}

idleBar.dispose();
System.setOut(oldSystemOut);

String result = outputStream.toString();
String[] animationFrames = result.substring(1).split("\\r");

String firstFrame = animationFrames[0];
int numberOfSpaces = firstFrame.lastIndexOf('>') - firstFrame.indexOf('<') - 3 - 1;
for (int i = 0; i < TARGET_FRAME_NUMBER; i++) {
checkIdleBarOutput(animationFrames[i], i, numberOfSpaces);
}
}

/**
* Checks that the given string matches the expected content of an animation frame
* @param output The animation frame
* @param frameIndex The index of the frame
* @param numberOfSpaces The number of spaces within the bar
*/
private void checkIdleBarOutput(String output, int frameIndex, int numberOfSpaces) {
int pass = frameIndex / numberOfSpaces;
int offset = frameIndex % numberOfSpaces;
if (pass % 2 == 1) {
offset = numberOfSpaces - offset;
}

String expectedOutput = TEST_BAR_TEXT + ' ' + '<' + " ".repeat(offset) + "<+>" + " ".repeat(numberOfSpaces - offset) + '>';

int endOfPredictableOutput = output.lastIndexOf(' ');
String predictableOutput = output.substring(0, endOfPredictableOutput);
String time = output.substring(endOfPredictableOutput + 1).trim();

Assertions.assertEquals(expectedOutput, predictableOutput);
Assertions.assertTrue(time.matches("[0-9]:[0-9]{2}:[0-9]{2}"), "Invalid format for time");

String[] timeParts = time.split(":");
int seconds = Integer.parseInt(timeParts[0]) * 60 * 60 + Integer.parseInt(timeParts[1]) * 60 + Integer.parseInt(timeParts[2]);
int expectedTime = (int) ((IDLE_BAR_ANIMATION_DELAY * frameIndex) / 1000);
Assertions.assertTrue(Math.abs(seconds - expectedTime) < 1, "Frame time of by more than one second");
}
}
11 changes: 9 additions & 2 deletions core/src/main/java/de/jplag/clustering/ClusteringFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
import de.jplag.JPlagComparison;
import de.jplag.Submission;
import de.jplag.clustering.algorithm.GenericClusteringAlgorithm;
import de.jplag.logging.ProgressBar;
import de.jplag.logging.ProgressBarLogger;
import de.jplag.logging.ProgressBarType;

/**
* Runs the clustering according to an options object.
Expand Down Expand Up @@ -42,6 +45,8 @@ public static List<ClusteringResult<Submission>> getClusterings(Collection<JPlag
logger.info(CLUSTERING_PARAMETERS, options.algorithm(), options.preprocessor());
}

ProgressBar progressBar = ProgressBarLogger.createProgressBar(ProgressBarType.CLUSTERING, 0);

// init algorithm
GenericClusteringAlgorithm clusteringAlgorithm = options.algorithm().create(options);

Expand All @@ -61,6 +66,8 @@ public static List<ClusteringResult<Submission>> getClusterings(Collection<JPlag

// remove bad clusters
result = removeBadClusters(result);

progressBar.dispose();
logClusters(result);

return List.of(result);
Expand All @@ -74,15 +81,15 @@ private static ClusteringResult<Submission> removeBadClusters(final ClusteringRe
private static void logClusters(ClusteringResult<Submission> result) {
var clusters = new ArrayList<>(result.getClusters());
clusters.sort((first, second) -> Double.compare(second.getAverageSimilarity(), first.getAverageSimilarity()));
logger.info(CLUSTERING_RESULT, clusters.size());
logger.trace(CLUSTERING_RESULT, clusters.size());
clusters.forEach(ClusteringFactory::logCluster);
}

private static void logCluster(Cluster<Submission> cluster) {
String members = membersToString(cluster.getMembers());
String similarity = String.format(SIMILARITY_FORMAT, cluster.getAverageSimilarity() * 100);
String strength = String.format(STRENGTH_FORMAT, cluster.getCommunityStrength());
logger.info(CLUSTER_PATTERN, similarity, strength, cluster.getMembers().size(), members);
logger.trace(CLUSTER_PATTERN, similarity, strength, cluster.getMembers().size(), members);
}

private static String membersToString(Collection<Submission> members) {
Expand Down
6 changes: 5 additions & 1 deletion core/src/main/java/de/jplag/logging/ProgressBarLogger.java
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,11 @@ private static class DummyBar implements ProgressBar {

public DummyBar(ProgressBarType type, int totalSteps) {
this.currentStep = 0;
logger.info("{} ({})", type.getDefaultText(), totalSteps);
if (type.isIdleBar()) {
logger.info("{} - started", type.getDefaultText());
} else {
logger.info("{} ({})", type.getDefaultText(), totalSteps);
}
}

@Override
Expand Down
22 changes: 16 additions & 6 deletions core/src/main/java/de/jplag/logging/ProgressBarType.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,19 @@
* The available processes. Used as a hint for the ui, which step JPlag is currently performing.
*/
public enum ProgressBarType {
LOADING("Loading Submissions "),
PARSING("Parsing Submissions "),
COMPARING("Comparing Submissions"),
MATCH_MERGING("Merging matched subsequences "),
TOKEN_STRING_NORMALIZATION("Normalizing Token Sequence");
LOADING("Loading Submissions ", false),
PARSING("Parsing Submissions ", false),
COMPARING("Comparing Submissions", false),
MATCH_MERGING("Merging matched subsequences ", false),
TOKEN_STRING_NORMALIZATION("Normalizing Token Sequence", false),
CLUSTERING("Finding clusters ", true);

private final String defaultText;
private final boolean isIdleBar;

ProgressBarType(String defaultText) {
ProgressBarType(String defaultText, boolean isIdleBar) {
this.defaultText = defaultText;
this.isIdleBar = isIdleBar;
}

/**
Expand All @@ -22,4 +25,11 @@ public enum ProgressBarType {
public String getDefaultText() {
return defaultText;
}

/**
* @return True, if this bar should be rendered as an idle bar instead.
*/
public boolean isIdleBar() {
return isIdleBar;
}
}

0 comments on commit 649a595

Please sign in to comment.