Przeglądaj źródła

Merge pull request #624 from jplag/parallel-comparison-default

Enable parallel comparison strategy by default
Timur Sağlam 4 lat temu
rodzic
commit
8a1f7ae23d

+ 0 - 1
README.md

@@ -81,7 +81,6 @@ named arguments:
   -n N             The maximum number of comparisons that will be shown in the  generated report, if set to -1 all comparisons will be shown
                         (default: 30)
   -r R             Name of the directory in which the comparison results will be stored (default: result)
-  -c               {normal,parallel} Comparison mode used to compare the programs (default: normal)
 ```
 
 ### Java API

+ 1 - 9
cli/src/main/java/de/jplag/CLI.java

@@ -15,7 +15,6 @@ import static de.jplag.CommandLineArgument.CLUSTER_SPECTRAL_KMEANS_ITERATIONS;
 import static de.jplag.CommandLineArgument.CLUSTER_SPECTRAL_MAX_RUNS;
 import static de.jplag.CommandLineArgument.CLUSTER_SPECTRAL_MIN_RUNS;
 import static de.jplag.CommandLineArgument.CLUSTER_SPECTRAL_NOISE;
-import static de.jplag.CommandLineArgument.COMPARISON_MODE;
 import static de.jplag.CommandLineArgument.DEBUG;
 import static de.jplag.CommandLineArgument.EXCLUDE_FILE;
 import static de.jplag.CommandLineArgument.LANGUAGE;
@@ -52,7 +51,6 @@ import de.jplag.logger.CollectedLoggerFactory;
 import de.jplag.options.JPlagOptions;
 import de.jplag.options.Verbosity;
 import de.jplag.reporting.reportobject.ReportObjectFactory;
-import de.jplag.strategy.ComparisonMode;
 
 /**
  * Command line interface class, allows using via command line.
@@ -151,18 +149,12 @@ public final class CLI {
         addAllMultiValueArgument(OLD_DIRECTORY.getListFrom(namespace), oldSubmissionDirectories);
 
         var language = LanguageLoader.getLanguage(LANGUAGE.getFrom(namespace)).orElseThrow();
-        var comparisonModeOptional = ComparisonMode.fromName(COMPARISON_MODE.getFrom(namespace));
-        if (comparisonModeOptional.isEmpty()) {
-            logger.warn("Unknown comparison mode, using default mode!");
-        }
-        var comparisonMode = comparisonModeOptional.orElse(JPlagOptions.DEFAULT_COMPARISON_MODE);
-
         ClusteringOptions clusteringOptions = getClusteringOptions(namespace);
 
         return new JPlagOptions(language, MIN_TOKEN_MATCH.getFrom(namespace), submissionDirectories, oldSubmissionDirectories,
                 BASE_CODE.getFrom(namespace), SUBDIRECTORY.getFrom(namespace), Arrays.stream(fileSuffixes).toList(), EXCLUDE_FILE.getFrom(namespace),
                 JPlagOptions.DEFAULT_SIMILARITY_METRIC, SIMILARITY_THRESHOLD.getFrom(namespace), SHOWN_COMPARISONS.getFrom(namespace),
-                clusteringOptions, comparisonMode, Verbosity.fromOption(VERBOSITY.getFrom(namespace)), DEBUG.getFrom(namespace));
+                clusteringOptions, Verbosity.fromOption(VERBOSITY.getFrom(namespace)), DEBUG.getFrom(namespace));
     }
 
     private static ClusteringOptions getClusteringOptions(Namespace namespace) {

+ 0 - 3
cli/src/main/java/de/jplag/CommandLineArgument.java

@@ -2,7 +2,6 @@ package de.jplag;
 
 import static de.jplag.CLI.ADVANCED_GROUP;
 import static de.jplag.CLI.CLUSTERING_GROUP_NAME;
-import static de.jplag.options.JPlagOptions.DEFAULT_COMPARISON_MODE;
 import static de.jplag.options.JPlagOptions.DEFAULT_SHOWN_COMPARISONS;
 import static de.jplag.options.JPlagOptions.DEFAULT_SIMILARITY_THRESHOLD;
 import static net.sourceforge.argparse4j.impl.Arguments.append;
@@ -25,7 +24,6 @@ import de.jplag.clustering.ClusteringAlgorithm;
 import de.jplag.clustering.ClusteringOptions;
 import de.jplag.clustering.algorithm.InterClusterSimilarity;
 import de.jplag.options.SimilarityMetric;
-import de.jplag.strategy.ComparisonMode;
 
 /**
  * Command line arguments for the JPlag CLI. Each argument is defined through an enumeral.
@@ -51,7 +49,6 @@ public enum CommandLineArgument {
     SIMILARITY_THRESHOLD(new Builder("-m", Double.class).defaultsTo(DEFAULT_SIMILARITY_THRESHOLD).argumentGroup(ADVANCED_GROUP)),
     SHOWN_COMPARISONS(new Builder("-n", Integer.class).defaultsTo(DEFAULT_SHOWN_COMPARISONS)),
     RESULT_FOLDER(new Builder("-r", String.class).defaultsTo("result")),
-    COMPARISON_MODE(new Builder("-c", String.class).defaultsTo(DEFAULT_COMPARISON_MODE.getName()).choices(ComparisonMode.allNames())),
     CLUSTER_DISABLE(new Builder("--cluster-skip", Boolean.class).argumentGroup(CLUSTERING_GROUP_NAME).action(Arguments.storeTrue())),
     CLUSTER_ALGORITHM(
             new Builder("--cluster-alg", ClusteringAlgorithm.class).argumentGroup(CLUSTERING_GROUP_NAME)

+ 0 - 43
cli/src/test/java/de/jplag/cli/ComparisonModeTest.java

@@ -1,43 +0,0 @@
-package de.jplag.cli;
-
-import static com.github.stefanbirkner.systemlambda.SystemLambda.catchSystemExit;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-import org.junit.jupiter.api.Test;
-
-import de.jplag.CommandLineArgument;
-import de.jplag.options.JPlagOptions;
-import de.jplag.strategy.ComparisonMode;
-
-class ComparisonModeTest extends CommandLineInterfaceTest {
-
-    @Test
-    void testDefaultMode() {
-        buildOptionsFromCLI(CURRENT_DIRECTORY);
-        assertEquals(JPlagOptions.DEFAULT_COMPARISON_MODE, options.comparisonMode());
-    }
-
-    @Test
-    void testInvalidMode() throws Exception {
-        String argument = buildArgument(CommandLineArgument.COMPARISON_MODE, "Test'); DROP TABLE STUDENTS; --");
-        int statusCode = catchSystemExit(() -> buildOptionsFromCLI(argument, CURRENT_DIRECTORY));
-        assertEquals(1, statusCode);
-    }
-
-    @Test
-    void testNormalMode() {
-        ComparisonMode mode = ComparisonMode.NORMAL;
-        String argument = buildArgument(CommandLineArgument.COMPARISON_MODE, mode.getName());
-        buildOptionsFromCLI(argument, CURRENT_DIRECTORY);
-        assertEquals(mode, options.comparisonMode());
-    }
-
-    @Test
-    void testParallelMode() {
-        ComparisonMode mode = ComparisonMode.PARALLEL;
-        String argument = buildArgument(CommandLineArgument.COMPARISON_MODE, mode.getName());
-        buildOptionsFromCLI(argument, CURRENT_DIRECTORY);
-        assertEquals(mode, options.comparisonMode());
-    }
-
-}

+ 14 - 19
core/src/main/java/de/jplag/GreedyStringTiling.java

@@ -202,13 +202,8 @@ public class GreedyStringTiling {
     }
 
     private SubsequenceHashLookupTable subsequenceHashLookupTableForSubmission(Submission submission, Set<Integer> markedIndexes) {
-        if (cachedHashLookupTables.containsKey(submission)) {
-            return cachedHashLookupTables.get(submission);
-        }
-        SubsequenceHashLookupTable lookupTable = new SubsequenceHashLookupTable(minimumMatchLength, tokenValueListFromSubmission(submission),
-                markedIndexes);
-        cachedHashLookupTables.put(submission, lookupTable);
-        return lookupTable;
+        return cachedHashLookupTables.computeIfAbsent(submission,
+                (key -> new SubsequenceHashLookupTable(minimumMatchLength, tokenValueListFromSubmission(key), markedIndexes)));
     }
 
     /**
@@ -216,17 +211,17 @@ public class GreedyStringTiling {
      * @param submission The submission from which to convert the tokens.
      */
     private int[] tokenValueListFromSubmission(Submission submission) {
-        if (cachedTokenValueLists.containsKey(submission)) {
-            return cachedTokenValueLists.get(submission);
-        }
-        List<Token> tokens = submission.getTokenList();
-        int[] tokenValueList = new int[tokens.size()];
-        for (int i = 0; i < tokens.size(); i++) {
-            TokenType type = tokens.get(i).getType();
-            tokenTypeValues.putIfAbsent(type, tokenTypeValues.size());
-            tokenValueList[i] = tokenTypeValues.get(type);
-        }
-        cachedTokenValueLists.put(submission, tokenValueList);
-        return tokenValueList;
+        return cachedTokenValueLists.computeIfAbsent(submission, (key -> {
+            List<Token> tokens = key.getTokenList();
+            int[] tokenValueList = new int[tokens.size()];
+            for (int i = 0; i < tokens.size(); i++) {
+                TokenType type = tokens.get(i).getType();
+                synchronized (tokenTypeValues) {
+                    tokenTypeValues.putIfAbsent(type, tokenTypeValues.size());
+                }
+                tokenValueList[i] = tokenTypeValues.get(type);
+            }
+            return tokenValueList;
+        }));
     }
 }

+ 2 - 12
core/src/main/java/de/jplag/JPlag.java

@@ -7,9 +7,7 @@ import de.jplag.clustering.ClusteringFactory;
 import de.jplag.exceptions.ExitException;
 import de.jplag.exceptions.SubmissionException;
 import de.jplag.options.JPlagOptions;
-import de.jplag.strategy.ComparisonMode;
 import de.jplag.strategy.ComparisonStrategy;
-import de.jplag.strategy.NormalComparisonStrategy;
 import de.jplag.strategy.ParallelComparisonStrategy;
 
 /**
@@ -22,7 +20,6 @@ public class JPlag {
 
     private final Language language;
     private final ComparisonStrategy comparisonStrategy;
-    private final GreedyStringTiling coreAlgorithm; // Contains the comparison logic.
 
     /**
      * Creates and initializes a JPlag instance, parameterized by a set of options.
@@ -30,9 +27,9 @@ public class JPlag {
      */
     public JPlag(JPlagOptions options) {
         this.options = options;
-        coreAlgorithm = new GreedyStringTiling(options);
         language = this.options.language();
-        comparisonStrategy = initializeComparisonStrategy(options.comparisonMode());
+        GreedyStringTiling coreAlgorithm = new GreedyStringTiling(options);
+        comparisonStrategy = new ParallelComparisonStrategy(options, coreAlgorithm);
     }
 
     /**
@@ -59,11 +56,4 @@ public class JPlag {
 
         return result;
     }
-
-    private ComparisonStrategy initializeComparisonStrategy(final ComparisonMode comparisonMode) {
-        return switch (comparisonMode) {
-            case NORMAL -> new NormalComparisonStrategy(options, coreAlgorithm);
-            case PARALLEL -> new ParallelComparisonStrategy(options, coreAlgorithm);
-        };
-    }
 }

+ 2 - 2
core/src/main/java/de/jplag/JPlagResult.java

@@ -26,12 +26,12 @@ public class JPlagResult {
     private final int SIMILARITY_DISTRIBUTION_SIZE = 10;
 
     public JPlagResult(List<JPlagComparison> comparisons, SubmissionSet submissions, long durationInMillis, JPlagOptions options) {
-        this.comparisons = comparisons;
+        // sort comparisons by percentage (descending)
+        this.comparisons = comparisons.stream().sorted((first, second) -> Double.compare(second.similarity(), first.similarity())).toList();
         this.submissions = submissions;
         this.durationInMillis = durationInMillis;
         this.options = options;
         similarityDistribution = calculateSimilarityDistribution(comparisons);
-        comparisons.sort((first, second) -> Double.compare(second.similarity(), first.similarity())); // Sort by percentage (descending).
     }
 
     /**

+ 17 - 28
core/src/main/java/de/jplag/options/JPlagOptions.java

@@ -1,7 +1,6 @@
 package de.jplag.options;
 
 import static de.jplag.options.Verbosity.LONG;
-import static de.jplag.strategy.ComparisonMode.NORMAL;
 
 import java.io.BufferedReader;
 import java.io.FileReader;
@@ -21,7 +20,6 @@ import org.slf4j.LoggerFactory;
 import de.jplag.JPlag;
 import de.jplag.Language;
 import de.jplag.clustering.ClusteringOptions;
-import de.jplag.strategy.ComparisonMode;
 
 /**
  * This record defines the options to configure {@link JPlag}.
@@ -44,16 +42,14 @@ import de.jplag.strategy.ComparisonMode;
  * @param maximumNumberOfComparisons The maximum number of comparisons that will be shown in the generated report. If
  * set to {@link #SHOW_ALL_COMPARISONS} all comparisons will be shown.
  * @param clusteringOptions Clustering options
- * @param comparisonMode Determines which strategy to use for the comparison of submissions.
  * @param verbosity Level of output verbosity.
  * @param debugParser If true, submissions that cannot be parsed will be stored in a separate directory.
  */
 public record JPlagOptions(Language language, Integer minimumTokenMatch, List<String> submissionDirectories, List<String> oldSubmissionDirectories,
         String baseCodeSubmissionName, String subdirectoryName, List<String> fileSuffixes, String exclusionFileName,
         SimilarityMetric similarityMetric, double similarityThreshold, int maximumNumberOfComparisons, ClusteringOptions clusteringOptions,
-        ComparisonMode comparisonMode, Verbosity verbosity, boolean debugParser) {
+        Verbosity verbosity, boolean debugParser) {
 
-    public static final ComparisonMode DEFAULT_COMPARISON_MODE = NORMAL;
     public static final double DEFAULT_SIMILARITY_THRESHOLD = 0;
     public static final int DEFAULT_SHOWN_COMPARISONS = 30;
     public static final int SHOW_ALL_COMPARISONS = 0;
@@ -64,15 +60,14 @@ public record JPlagOptions(Language language, Integer minimumTokenMatch, List<St
 
     public JPlagOptions(Language language, List<String> submissionDirectories, List<String> oldSubmissionDirectories) {
         this(language, null, submissionDirectories, oldSubmissionDirectories, null, null, null, null, DEFAULT_SIMILARITY_METRIC,
-                DEFAULT_SIMILARITY_THRESHOLD, DEFAULT_SHOWN_COMPARISONS, new ClusteringOptions(), DEFAULT_COMPARISON_MODE, null, false);
+                DEFAULT_SIMILARITY_THRESHOLD, DEFAULT_SHOWN_COMPARISONS, new ClusteringOptions(), null, false);
     }
 
     public JPlagOptions(Language language, Integer minimumTokenMatch, List<String> submissionDirectories, List<String> oldSubmissionDirectories,
             String baseCodeSubmissionName, String subdirectoryName, List<String> fileSuffixes, String exclusionFileName,
             SimilarityMetric similarityMetric, double similarityThreshold, int maximumNumberOfComparisons, ClusteringOptions clusteringOptions,
-            ComparisonMode comparisonMode, Verbosity verbosity, boolean debugParser) {
+            Verbosity verbosity, boolean debugParser) {
         this.language = language;
-        this.comparisonMode = comparisonMode;
         this.debugParser = debugParser;
         this.fileSuffixes = fileSuffixes == null || fileSuffixes.isEmpty() ? null : Collections.unmodifiableList(fileSuffixes);
         this.similarityThreshold = normalizeSimilarityThreshold(similarityThreshold);
@@ -91,91 +86,85 @@ public record JPlagOptions(Language language, Integer minimumTokenMatch, List<St
     public JPlagOptions withLanguageOption(Language language) {
         return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionName,
                 subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons,
-                clusteringOptions, comparisonMode, verbosity, debugParser);
-    }
-
-    public JPlagOptions withComparisonMode(ComparisonMode comparisonMode) {
-        return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionName,
-                subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons,
-                clusteringOptions, comparisonMode, verbosity, debugParser);
+                clusteringOptions, verbosity, debugParser);
     }
 
     public JPlagOptions withDebugParser(boolean debugParser) {
         return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionName,
                 subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons,
-                clusteringOptions, comparisonMode, verbosity, debugParser);
+                clusteringOptions, verbosity, debugParser);
     }
 
     public JPlagOptions withFileSuffixes(List<String> fileSuffixes) {
         return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionName,
                 subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons,
-                clusteringOptions, comparisonMode, verbosity, debugParser);
+                clusteringOptions, verbosity, debugParser);
     }
 
     public JPlagOptions withSimilarityThreshold(double similarityThreshold) {
         return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionName,
                 subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons,
-                clusteringOptions, comparisonMode, verbosity, debugParser);
+                clusteringOptions, verbosity, debugParser);
     }
 
     public JPlagOptions withMaximumNumberOfComparisons(int maximumNumberOfComparisons) {
         return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionName,
                 subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons,
-                clusteringOptions, comparisonMode, verbosity, debugParser);
+                clusteringOptions, verbosity, debugParser);
     }
 
     public JPlagOptions withSimilarityMetric(SimilarityMetric similarityMetric) {
         return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionName,
                 subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons,
-                clusteringOptions, comparisonMode, verbosity, debugParser);
+                clusteringOptions, verbosity, debugParser);
     }
 
     public JPlagOptions withMinimumTokenMatch(Integer minimumTokenMatch) {
         return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionName,
                 subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons,
-                clusteringOptions, comparisonMode, verbosity, debugParser);
+                clusteringOptions, verbosity, debugParser);
     }
 
     public JPlagOptions withExclusionFileName(String exclusionFileName) {
         return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionName,
                 subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons,
-                clusteringOptions, comparisonMode, verbosity, debugParser);
+                clusteringOptions, verbosity, debugParser);
     }
 
     public JPlagOptions withSubmissionDirectories(List<String> submissionDirectories) {
         return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionName,
                 subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons,
-                clusteringOptions, comparisonMode, verbosity, debugParser);
+                clusteringOptions, verbosity, debugParser);
     }
 
     public JPlagOptions withOldSubmissionDirectories(List<String> oldSubmissionDirectories) {
         return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionName,
                 subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons,
-                clusteringOptions, comparisonMode, verbosity, debugParser);
+                clusteringOptions, verbosity, debugParser);
     }
 
     public JPlagOptions withBaseCodeSubmissionName(String baseCodeSubmissionName) {
         return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionName,
                 subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons,
-                clusteringOptions, comparisonMode, verbosity, debugParser);
+                clusteringOptions, verbosity, debugParser);
     }
 
     public JPlagOptions withSubdirectoryName(String subdirectoryName) {
         return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionName,
                 subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons,
-                clusteringOptions, comparisonMode, verbosity, debugParser);
+                clusteringOptions, verbosity, debugParser);
     }
 
     public JPlagOptions withVerbosity(Verbosity verbosity) {
         return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionName,
                 subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons,
-                clusteringOptions, comparisonMode, verbosity, debugParser);
+                clusteringOptions, verbosity, debugParser);
     }
 
     public JPlagOptions withClusteringOptions(ClusteringOptions clusteringOptions) {
         return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionName,
                 subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons,
-                clusteringOptions, comparisonMode, verbosity, debugParser);
+                clusteringOptions, verbosity, debugParser);
     }
 
     public boolean hasBaseCode() {

+ 6 - 13
core/src/main/java/de/jplag/strategy/AbstractComparisonStrategy.java

@@ -44,7 +44,7 @@ public abstract class AbstractComparisonStrategy implements ComparisonStrategy {
     /**
      * Compares two submissions and optionally returns the results if similarity is high enough.
      */
-    protected Optional<JPlagComparison> compareSubmissions(Submission first, Submission second, boolean withBaseCode) {
+    protected Optional<JPlagComparison> compareSubmissions(Submission first, Submission second) {
         JPlagComparison comparison = greedyStringTiling.compare(first, second);
         logger.info("Comparing {}-{}: {}", first.getName(), second.getName(), comparison.similarity());
 
@@ -59,19 +59,12 @@ public abstract class AbstractComparisonStrategy implements ComparisonStrategy {
      */
     protected static List<SubmissionTuple> buildComparisonTuples(List<Submission> submissions) {
         List<SubmissionTuple> tuples = new ArrayList<>();
+        List<Submission> validSubmissions = submissions.stream().filter(s -> s.getTokenList() != null).toList();
 
-        for (int i = 0; i < (submissions.size() - 1); i++) {
-            Submission first = submissions.get(i);
-            if (first.getTokenList() == null) {
-                continue;
-            }
-
-            for (int j = (i + 1); j < submissions.size(); j++) {
-                Submission second = submissions.get(j);
-                if (second.getTokenList() == null) {
-                    continue;
-                }
-
+        for (int i = 0; i < (validSubmissions.size() - 1); i++) {
+            Submission first = validSubmissions.get(i);
+            for (int j = (i + 1); j < validSubmissions.size(); j++) {
+                Submission second = validSubmissions.get(j);
                 if (first.isNew() || second.isNew()) {
                     tuples.add(new SubmissionTuple(first, second));
                 }

+ 0 - 52
core/src/main/java/de/jplag/strategy/ComparisonMode.java

@@ -1,52 +0,0 @@
-package de.jplag.strategy;
-
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Optional;
-
-/**
- * The comparison mode determines which {@link ComparisonStrategy} is used to compare submissions.
- */
-public enum ComparisonMode {
-    NORMAL("Normal comparison (sequential)"),
-    PARALLEL("Faster comparison (parallel)");
-
-    private final String name;
-    private final String description;
-
-    ComparisonMode(String description) {
-        this.description = description;
-        name = toString().toLowerCase();
-    }
-
-    /**
-     * @return the specifier for the comparison mode.
-     */
-    public String getName() {
-        return name;
-    }
-
-    /**
-     * The textual description of the comparison mode.
-     * @return a description of the comparison mode
-     */
-    public String getDescription() {
-        return description;
-    }
-
-    /**
-     * @return a collections of all mode names.
-     */
-    public static Collection<String> allNames() {
-        return Arrays.stream(values()).map(ComparisonMode::getName).toList();
-    }
-
-    /**
-     * Retrieves a comparison mode from a specific name.
-     * @param name is that name.
-     * @return the optional mode or nothing if no name matches.
-     */
-    public static Optional<ComparisonMode> fromName(String name) {
-        return Arrays.stream(values()).filter(it -> it.name.equals(name)).findFirst();
-    }
-}

+ 0 - 38
core/src/main/java/de/jplag/strategy/NormalComparisonStrategy.java

@@ -1,38 +0,0 @@
-package de.jplag.strategy;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import de.jplag.GreedyStringTiling;
-import de.jplag.JPlagComparison;
-import de.jplag.JPlagResult;
-import de.jplag.Submission;
-import de.jplag.SubmissionSet;
-import de.jplag.options.JPlagOptions;
-
-public class NormalComparisonStrategy extends AbstractComparisonStrategy {
-
-    public NormalComparisonStrategy(JPlagOptions options, GreedyStringTiling greedyStringTiling) {
-        super(options, greedyStringTiling);
-    }
-
-    @Override
-    public JPlagResult compareSubmissions(SubmissionSet submissionSet) {
-        boolean withBaseCode = submissionSet.hasBaseCode();
-        if (withBaseCode) {
-            compareSubmissionsToBaseCode(submissionSet);
-        }
-
-        List<Submission> submissions = submissionSet.getSubmissions();
-        long timeBeforeStartInMillis = System.currentTimeMillis();
-        List<JPlagComparison> comparisons = new ArrayList<>();
-
-        for (SubmissionTuple tuple : buildComparisonTuples(submissions)) {
-            compareSubmissions(tuple.left(), tuple.right(), withBaseCode).ifPresent(comparisons::add);
-        }
-
-        long durationInMillis = System.currentTimeMillis() - timeBeforeStartInMillis;
-        return new JPlagResult(comparisons, submissionSet, durationInMillis, options);
-    }
-
-}

+ 5 - 106
core/src/main/java/de/jplag/strategy/ParallelComparisonStrategy.java

@@ -1,43 +1,21 @@
 package de.jplag.strategy;
 
-import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import java.util.Optional;
 
 import de.jplag.GreedyStringTiling;
 import de.jplag.JPlagComparison;
 import de.jplag.JPlagResult;
-import de.jplag.Submission;
 import de.jplag.SubmissionSet;
 import de.jplag.options.JPlagOptions;
 
 /**
- * Strategy for the parallel comparison of submissions. Uses all available cores and compares in a non-blocking manner.
+ * Strategy for the parallel comparison of submissions. Uses all available cores.
  * @author Timur Saglam
  */
 public class ParallelComparisonStrategy extends AbstractComparisonStrategy {
-    private static final Logger logger = LoggerFactory.getLogger("JPlag");
-
-    private static final int TIMEOUT_IN_SECONDS = 5;
-    private final ConcurrentMap<String, Lock> submissionLocks;
-    private ExecutorService threadPool;
-    private final List<JPlagComparison> comparisons;
-    private int successfulComparisons;
-
     public ParallelComparisonStrategy(JPlagOptions options, GreedyStringTiling greedyStringTiling) {
         super(options, greedyStringTiling);
-        submissionLocks = new ConcurrentHashMap<>();
-        comparisons = Collections.synchronizedList(new ArrayList<>());
     }
 
     @Override
@@ -48,91 +26,12 @@ public class ParallelComparisonStrategy extends AbstractComparisonStrategy {
         if (withBaseCode) {
             compareSubmissionsToBaseCode(submissionSet);
         }
-        threadPool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
-        comparisons.clear();
-        submissionLocks.clear();
-        successfulComparisons = 0;
 
-        // Parallel compare:
-        List<Submission> submissions = submissionSet.getSubmissions();
-        List<SubmissionTuple> tuples = buildComparisonTuples(submissions);
-        Collections.shuffle(tuples); // Reduces how often submission pairs must be re-submitted
-        for (SubmissionTuple tuple : tuples) {
-            threadPool.execute(compareTuple(tuple, withBaseCode));
-        }
+        List<SubmissionTuple> tuples = buildComparisonTuples(submissionSet.getSubmissions());
+        List<JPlagComparison> comparisons = tuples.stream().parallel().map(tuple -> compareSubmissions(tuple.left(), tuple.right()))
+                .flatMap(Optional::stream).toList();
 
-        // Ensure termination:
-        while (successfulComparisons < tuples.size()) {
-            try {
-                Thread.sleep(5);
-            } catch (InterruptedException e) {
-                logger.error(e.getMessage(), e);
-                Thread.currentThread().interrupt();
-            }
-        }
-
-        // Clean up and return result:
-        shutdownThreadPool();
         long durationInMillis = System.currentTimeMillis() - timeBeforeStartInMillis;
         return new JPlagResult(comparisons, submissionSet, durationInMillis, options);
     }
-
-    /**
-     * Creates a runnable which compares a submission tuple. If the submissions are locked, the runnable is re-submitted.
-     * @param tuple contains the submissions to compare.
-     * @param withBaseCode specifies if base code is used.
-     * @return the runnable for parallel use.
-     */
-    private Runnable compareTuple(SubmissionTuple tuple, boolean withBaseCode) {
-        return new Runnable() {
-            @Override
-            public void run() {
-                Lock leftLock = getOrCreateLock(tuple.left().getName());
-                Lock rightLock = getOrCreateLock(tuple.right().getName());
-                boolean hasLeft = leftLock.tryLock();
-                boolean hasRight = hasLeft && rightLock.tryLock();
-                try {
-                    if (hasLeft && hasRight) { // both locks acquired!
-                        compareSubmissions(tuple.left(), tuple.right(), withBaseCode).ifPresent(comparisons::add);
-                        synchronized (this) {
-                            successfulComparisons++;
-                        }
-                    } else {
-                        threadPool.execute(this); // re-submit runnable, as at least one submission is locked.
-                    }
-                } finally {
-                    if (hasRight) {
-                        rightLock.unlock();
-                    }
-                    if (hasLeft) {
-                        leftLock.unlock();
-                    }
-                }
-            }
-        };
-    }
-
-    /**
-     * @return a lock for a given key. If it does not exist, it is created in a thread-safe manner.
-     */
-    private Lock getOrCreateLock(String key) {
-        submissionLocks.putIfAbsent(key, new ReentrantLock()); // atomic operation
-        return submissionLocks.get(key);
-    }
-
-    /**
-     * Shuts down the thread pool and awaits termination
-     */
-    private void shutdownThreadPool() {
-        threadPool.shutdown();
-        try {
-            if (!threadPool.awaitTermination(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS)) {
-                throw new IllegalStateException("Parallel comparison calculation timed out!");
-            }
-        } catch (InterruptedException e) {
-            logger.error(e.getMessage(), e);
-            Thread.currentThread().interrupt();
-        }
-    }
-
 }

+ 0 - 1
core/src/main/resources/de/jplag/messages.properties

@@ -1,5 +1,4 @@
 CommandLineArgument.BaseCode=Path of the directory containing the base code (common framework used in all submissions)
-CommandLineArgument.ComparisonMode=Comparison mode used to compare the programs
 CommandLineArgument.Debug=Debug parser. Non-parsable files will be stored
 CommandLineArgument.Suffixes=comma-separated list of all filename suffixes that are included
 CommandLineArgument.ExcludeFile=All files named in this file will be ignored in the comparison (line-separated list)

+ 0 - 161
core/src/test/java/de/jplag/NormalComparisonTest.java

@@ -1,161 +0,0 @@
-package de.jplag;
-
-import static org.junit.jupiter.api.Assertions.assertArrayEquals;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-
-import java.util.List;
-import java.util.Optional;
-
-import org.junit.jupiter.api.Test;
-
-import de.jplag.exceptions.BasecodeException;
-import de.jplag.exceptions.ExitException;
-
-class NormalComparisonTest extends TestBase {
-
-    /**
-     * The simple duplicate contains obvious plagiarism.
-     */
-    @Test
-    void testSimpleDuplicate() throws ExitException {
-        JPlagResult result = runJPlagWithDefaultOptions("SimpleDuplicate");
-
-        assertEquals(2, result.getNumberOfSubmissions());
-        assertEquals(1, result.getAllComparisons().size());
-        assertEquals(1, result.getAllComparisons().get(0).matches().size());
-        assertEquals(1, result.getSimilarityDistribution()[3]);
-        assertEquals(62.07, result.getAllComparisons().get(0).similarity(), 0.1);
-    }
-
-    /**
-     * The simple duplicate with a custom min token match.
-     */
-    @Test
-    void testWithMinTokenMatch() throws ExitException {
-        var expectedDistribution = new int[] {1, 0, 0, 0, 0, 0, 0, 0, 0, 0};
-        JPlagResult result = runJPlag("SimpleDuplicate", it -> it.withMinimumTokenMatch(5));
-
-        assertEquals(2, result.getNumberOfSubmissions());
-        assertEquals(1, result.getAllComparisons().size());
-        assertEquals(2, result.getAllComparisons().get(0).matches().size());
-        assertArrayEquals(expectedDistribution, result.getSimilarityDistribution());
-        assertEquals(96.55, result.getAllComparisons().get(0).similarity(), 0.1);
-    }
-
-    /**
-     * The classes in no duplicate have nearly nothing in common.
-     */
-    @Test
-    void testNoDuplicate() throws ExitException {
-        JPlagResult result = runJPlagWithDefaultOptions("NoDuplicate");
-
-        assertEquals(3, result.getNumberOfSubmissions());
-        assertEquals(3, result.getAllComparisons().size());
-
-        result.getAllComparisons().forEach(comparison -> assertEquals(0, comparison.similarity(), 0.1));
-    }
-
-    /**
-     * This case is more complex and consists out of 5 submissions with different plagiarism. A is the original code (coming
-     * from an older JPlag version) B is a partial copy of that code C is a full copy of that code D is dumb plagiarism,
-     * e.g., changed variable names, additional unneeded code, ... E is just a Hello World Java errorConsumer
-     */
-    @Test
-    void testPartialPlagiarism() throws ExitException {
-        JPlagResult result = runJPlagWithDefaultOptions("PartialPlagiarism");
-
-        assertEquals(5, result.getNumberOfSubmissions());
-        assertEquals(10, result.getAllComparisons().size());
-
-        // All comparisons with E shall have no matches
-        result.getAllComparisons().stream()
-                .filter(comparison -> comparison.secondSubmission().getName().equals("E") || comparison.firstSubmission().getName().equals("E"))
-                .forEach(comparison -> assertEquals(0, comparison.similarity(), DELTA));
-
-        // Hard coded assertions on selected comparisons
-        assertEquals(24.6, getSelectedPercent(result, "A", "B"), 0.1);
-        assertEquals(99.7, getSelectedPercent(result, "A", "C"), 0.1);
-        assertEquals(77.9, getSelectedPercent(result, "A", "D"), 0.1);
-        assertEquals(24.6, getSelectedPercent(result, "B", "C"), 0.1);
-        assertEquals(28.3, getSelectedPercent(result, "B", "D"), 0.1);
-        assertEquals(77.9, getSelectedPercent(result, "C", "D"), 0.1);
-
-        // More detailed assertions for the plagiarism in A-D
-        var biggestMatch = getSelectedComparison(result, "A", "D");
-        assertEquals(96.4, biggestMatch.get().maximalSimilarity(), 0.1);
-        assertEquals(65.3, biggestMatch.get().minimalSimilarity(), 0.1);
-        assertEquals(12, biggestMatch.get().matches().size());
-
-    }
-
-    // TODO SH: Methods like this should be moved to the API and also should accept wildcards
-    private double getSelectedPercent(JPlagResult result, String nameA, String nameB) {
-        return getSelectedComparison(result, nameA, nameB).map(JPlagComparison::similarity).orElse(-1.0);
-    }
-
-    private Optional<JPlagComparison> getSelectedComparison(JPlagResult result, String nameA, String nameB) {
-        return result.getAllComparisons().stream()
-                .filter(comparison -> comparison.firstSubmission().getName().equals(nameA) && comparison.secondSubmission().getName().equals(nameB)
-                        || comparison.firstSubmission().getName().equals(nameB) && comparison.secondSubmission().getName().equals(nameA))
-                .findFirst();
-    }
-
-    @Test
-    void testMultiRootDirNoBasecode() throws ExitException {
-        List<String> paths = List.of(getBasePath("basecode"), getBasePath("SimpleDuplicate")); // 3 + 2 submissions.
-        JPlagResult result = runJPlag(paths, it -> it);
-        assertEquals(5, result.getNumberOfSubmissions());
-    }
-
-    @Test
-    void testMultiRootDirSeparateBasecode() throws ExitException {
-        String basecodePath = getBasePath("basecode-base");
-        List<String> paths = List.of(getBasePath("basecode"), getBasePath("SimpleDuplicate")); // 3 + 2 submissions.
-        JPlagResult result = runJPlag(paths, it -> it.withBaseCodeSubmissionName(basecodePath));
-        assertEquals(5, result.getNumberOfSubmissions());
-    }
-
-    @Test
-    public void testMultiRootDirBasecodeInSubmissionDir() throws ExitException {
-        String basecodePath = getBasePath("basecode", "base");
-        List<String> paths = List.of(getBasePath("basecode"), getBasePath("SimpleDuplicate")); // 2 + 2 submissions.
-        JPlagResult result = runJPlag(paths, it -> it.withBaseCodeSubmissionName(basecodePath));
-        assertEquals(4, result.getNumberOfSubmissions());
-    }
-
-    @Test
-    public void testMultiRootDirBasecodeName() {
-        List<String> paths = List.of(getBasePath("basecode"), getBasePath("SimpleDuplicate"));
-        String basecodePath = "base"; // Should *not* find basecode/base
-        assertThrows(BasecodeException.class, () -> runJPlag(paths, it -> it.withBaseCodeSubmissionName(basecodePath)));
-    }
-
-    @Test
-    public void testDisjunctNewAndOldRootDirectories() throws ExitException {
-        List<String> newDirectories = List.of(getBasePath("SimpleDuplicate")); // 2 submissions
-        List<String> oldDirectories = List.of(getBasePath("basecode")); // 3 submissions
-        JPlagResult result = runJPlag(newDirectories, oldDirectories, it -> it);
-        int numberOfExpectedComparison = 1 + 3 * 2;
-        assertEquals(numberOfExpectedComparison, result.getAllComparisons().size());
-    }
-
-    @Test
-    void testOverlappingNewAndOldDirectoriesOverlap() throws ExitException {
-        List<String> newDirectories = List.of(getBasePath("SimpleDuplicate")); // 2 submissions
-        List<String> oldDirectories = List.of(getBasePath("SimpleDuplicate"));
-        JPlagResult result = runJPlag(newDirectories, oldDirectories, it -> it);
-        int numberOfExpectedComparison = 1;
-        assertEquals(numberOfExpectedComparison, result.getAllComparisons().size());
-    }
-
-    @Test
-    void testBasecodeInOldDirectory() throws ExitException {
-        String basecodePath = getBasePath("basecode", "base");
-        List<String> newDirectories = List.of(getBasePath("SimpleDuplicate")); // 2 submissions
-        List<String> oldDirectories = List.of(getBasePath("basecode")); // 3 - 1 submissions
-        JPlagResult result = runJPlag(newDirectories, oldDirectories, it -> it.withBaseCodeSubmissionName(basecodePath));
-        int numberOfExpectedComparison = 1 + 2 * 2;
-        assertEquals(numberOfExpectedComparison, result.getAllComparisons().size());
-    }
-}

+ 83 - 13
core/src/test/java/de/jplag/ParallelComparisonTest.java

@@ -1,28 +1,25 @@
 package de.jplag;
 
-import static de.jplag.strategy.ComparisonMode.PARALLEL;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 
+import java.util.List;
 import java.util.Optional;
 
 import org.junit.jupiter.api.Test;
 
+import de.jplag.exceptions.BasecodeException;
 import de.jplag.exceptions.ExitException;
-import de.jplag.strategy.ParallelComparisonStrategy;
 
-/**
- * Currently just a copy of {@link NormalComparisonTest} but for the {@link ParallelComparisonStrategy}. // TODO TS:
- * de-duplicate this
- * @author Timur Saglam
- */
 public class ParallelComparisonTest extends TestBase {
 
     /**
      * The simple duplicate contains obvious plagiarism.
      */
     @Test
-    public void testSimpleDuplicate() throws ExitException {
-        JPlagResult result = runJPlag("SimpleDuplicate", it -> it.withComparisonMode(PARALLEL));
+    void testSimpleDuplicate() throws ExitException {
+        JPlagResult result = runJPlagWithDefaultOptions("SimpleDuplicate");
 
         assertEquals(2, result.getNumberOfSubmissions());
         assertEquals(1, result.getAllComparisons().size());
@@ -31,12 +28,27 @@ public class ParallelComparisonTest extends TestBase {
         assertEquals(62.07, result.getAllComparisons().get(0).similarity(), DELTA);
     }
 
+    /**
+     * The simple duplicate with a custom min token match.
+     */
+    @Test
+    void testWithMinTokenMatch() throws ExitException {
+        var expectedDistribution = new int[] {1, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+        JPlagResult result = runJPlag("SimpleDuplicate", it -> it.withMinimumTokenMatch(5));
+
+        assertEquals(2, result.getNumberOfSubmissions());
+        assertEquals(1, result.getAllComparisons().size());
+        assertEquals(2, result.getAllComparisons().get(0).matches().size());
+        assertArrayEquals(expectedDistribution, result.getSimilarityDistribution());
+        assertEquals(96.55, result.getAllComparisons().get(0).similarity(), DELTA);
+    }
+
     /**
      * The classes in no duplicate have nearly nothing in common.
      */
     @Test
-    public void testNoDuplicate() throws ExitException {
-        JPlagResult result = runJPlag("NoDuplicate", it -> it.withComparisonMode(PARALLEL));
+    void testNoDuplicate() throws ExitException {
+        JPlagResult result = runJPlagWithDefaultOptions("NoDuplicate");
 
         assertEquals(3, result.getNumberOfSubmissions());
         assertEquals(3, result.getAllComparisons().size());
@@ -50,8 +62,8 @@ public class ParallelComparisonTest extends TestBase {
      * e.g., changed variable names, additional unneeded code, ... E is just a Hello World Java errorConsumer
      */
     @Test
-    public void testPartialPlagiarism() throws ExitException {
-        JPlagResult result = runJPlag("PartialPlagiarism", it -> it.withComparisonMode(PARALLEL));
+    void testPartialPlagiarism() throws ExitException {
+        JPlagResult result = runJPlagWithDefaultOptions("PartialPlagiarism");
 
         assertEquals(5, result.getNumberOfSubmissions());
         assertEquals(10, result.getAllComparisons().size());
@@ -87,4 +99,62 @@ public class ParallelComparisonTest extends TestBase {
                         || comparison.firstSubmission().getName().equals(nameB) && comparison.secondSubmission().getName().equals(nameA))
                 .findFirst();
     }
+
+    @Test
+    void testMultiRootDirNoBasecode() throws ExitException {
+        List<String> paths = List.of(getBasePath("basecode"), getBasePath("SimpleDuplicate")); // 3 + 2 submissions.
+        JPlagResult result = runJPlag(paths, it -> it);
+        assertEquals(5, result.getNumberOfSubmissions());
+    }
+
+    @Test
+    void testMultiRootDirSeparateBasecode() throws ExitException {
+        String basecodePath = getBasePath("basecode-base");
+        List<String> paths = List.of(getBasePath("basecode"), getBasePath("SimpleDuplicate")); // 3 + 2 submissions.
+        JPlagResult result = runJPlag(paths, it -> it.withBaseCodeSubmissionName(basecodePath));
+        assertEquals(5, result.getNumberOfSubmissions());
+    }
+
+    @Test
+    void testMultiRootDirBasecodeInSubmissionDir() throws ExitException {
+        String basecodePath = getBasePath("basecode", "base");
+        List<String> paths = List.of(getBasePath("basecode"), getBasePath("SimpleDuplicate")); // 2 + 2 submissions.
+        JPlagResult result = runJPlag(paths, it -> it.withBaseCodeSubmissionName(basecodePath));
+        assertEquals(4, result.getNumberOfSubmissions());
+    }
+
+    @Test
+    void testMultiRootDirBasecodeName() {
+        List<String> paths = List.of(getBasePath("basecode"), getBasePath("SimpleDuplicate"));
+        String basecodePath = "base"; // Should *not* find basecode/base
+        assertThrows(BasecodeException.class, () -> runJPlag(paths, it -> it.withBaseCodeSubmissionName(basecodePath)));
+    }
+
+    @Test
+    void testDisjunctNewAndOldRootDirectories() throws ExitException {
+        List<String> newDirectories = List.of(getBasePath("SimpleDuplicate")); // 2 submissions
+        List<String> oldDirectories = List.of(getBasePath("basecode")); // 3 submissions
+        JPlagResult result = runJPlag(newDirectories, oldDirectories, it -> it);
+        int numberOfExpectedComparison = 1 + 3 * 2;
+        assertEquals(numberOfExpectedComparison, result.getAllComparisons().size());
+    }
+
+    @Test
+    void testOverlappingNewAndOldDirectoriesOverlap() throws ExitException {
+        List<String> newDirectories = List.of(getBasePath("SimpleDuplicate")); // 2 submissions
+        List<String> oldDirectories = List.of(getBasePath("SimpleDuplicate"));
+        JPlagResult result = runJPlag(newDirectories, oldDirectories, it -> it);
+        int numberOfExpectedComparison = 1;
+        assertEquals(numberOfExpectedComparison, result.getAllComparisons().size());
+    }
+
+    @Test
+    void testBasecodeInOldDirectory() throws ExitException {
+        String basecodePath = getBasePath("basecode", "base");
+        List<String> newDirectories = List.of(getBasePath("SimpleDuplicate")); // 2 submissions
+        List<String> oldDirectories = List.of(getBasePath("basecode")); // 3 - 1 submissions
+        JPlagResult result = runJPlag(newDirectories, oldDirectories, it -> it.withBaseCodeSubmissionName(basecodePath));
+        int numberOfExpectedComparison = 1 + 2 * 2;
+        assertEquals(numberOfExpectedComparison, result.getAllComparisons().size());
+    }
 }