Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/allow single submissions and path names #1935

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
95d2cd7
refactor(core): remove check on duplicate names
euberdeveloper Aug 26, 2024
e96cf70
refactor(core): remove throws into method checkForConfigurationConsis…
euberdeveloper Aug 26, 2024
e25e584
style(core): use String format
euberdeveloper Aug 26, 2024
67923b9
refactor(core): remove unused imports
euberdeveloper Aug 26, 2024
5f50854
feat(core): add support for same name submissions to SubmissionSetBui…
euberdeveloper Aug 26, 2024
407b629
test(core): fix should not throw
euberdeveloper Aug 26, 2024
5eb1c74
style: apply spotless
euberdeveloper Aug 26, 2024
6071f1b
test(core): add test about different submissions with same root name
euberdeveloper Aug 26, 2024
018293f
test(core): add test about different submissions with same root name
euberdeveloper Aug 26, 2024
c87778f
test(core): add test about single new submission with an old submission
euberdeveloper Aug 26, 2024
a6761b4
test(core): add test about single new submission with an old submission
euberdeveloper Aug 26, 2024
9964301
style: execute spotless
euberdeveloper Aug 26, 2024
92a729d
test(e2e): add test about single new submission
euberdeveloper Aug 27, 2024
e1db53e
test(e2e): add test about matching names on submissions
euberdeveloper Aug 27, 2024
dcf45bc
test(core): refactor tests on new features
euberdeveloper Aug 27, 2024
c300fc1
style: apply spotless
euberdeveloper Aug 27, 2024
37bfdc6
fix(report-viewer): fix regex for e2e tests
euberdeveloper Aug 28, 2024
68065f6
test(report-viewer): fix e2e test
euberdeveloper Aug 30, 2024
26a58f7
fix(viewer): fix missing backslash in regexp for e2e tests
euberdeveloper Sep 2, 2024
331557f
test(viewer): try to fix e2e test
euberdeveloper Sep 2, 2024
85ce7a2
ci(complete-e2e): remove additional hyphen to --old
euberdeveloper Sep 2, 2024
4fcf51f
test(viewer): fix e2e regexp
euberdeveloper Sep 2, 2024
39110d0
ci(complete-e2e): fix old argument by using comma and not spaces
euberdeveloper Sep 3, 2024
dc09aba
refactor(e2e): rename var
euberdeveloper Sep 4, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/complete-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ jobs:
{zip: "folderMultiRoot.zip", name: "folderMultiRoot", folder: "f0", language: "java", cliArgs: "--new f1"},
{zip: "mixedMultiRoot.zip", name: "mixedBaseFile", folder: "f0", language: "java", cliArgs: "--new f1"},
{zip: "mixedMultiRoot.zip", name: "mixedBaseFolder", folder: "f1", language: "java", cliArgs: "--new f0"},
{zip: "singleNewSubmission.zip", name: "singleNewSubmission", folder: "2023", language: "java", cliArgs: "-old 2022,2021,2020"},
{zip: "submissionsWithSameName.zip", name: "submissionsWithSameName", folder: "2023", language: "java", cliArgs: "-old old/2022,old/2021,old/2020"},
{zip: "cpp.zip", name: "cpp", folder: "./cpp", language: "cpp", cliArgs: ""},
{zip: "csharp.zip", name: "csharp", folder: "./csharp", language: "csharp", cliArgs: ""},
{zip: "python.zip", name: "python", folder: "./python", language: "python3", cliArgs: ""}
Expand Down
Binary file added .github/workflows/files/singleNewSubmission.zip
Binary file not shown.
Binary file not shown.
29 changes: 2 additions & 27 deletions core/src/main/java/de/jplag/JPlag.java
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
package de.jplag;

import java.io.File;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.ResourceBundle;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.jplag.clustering.ClusteringFactory;
import de.jplag.exceptions.ExitException;
import de.jplag.exceptions.RootDirectoryException;
import de.jplag.exceptions.SubmissionException;
import de.jplag.merging.MatchMerging;
import de.jplag.options.JPlagOptions;
Expand Down Expand Up @@ -107,30 +103,9 @@ private static void logSkippedSubmissions(SubmissionSet submissionSet, JPlagOpti
}
}

private static void checkForConfigurationConsistency(JPlagOptions options) throws RootDirectoryException {
private static void checkForConfigurationConsistency(JPlagOptions options) {
if (options.normalize() && !options.language().supportsNormalization()) {
logger.error(String.format("The language %s cannot be used with normalization.", options.language().getName()));
logger.error("The language {} cannot be used with normalization.", options.language().getName());
}

List<String> duplicateNames = getDuplicateSubmissionFolderNames(options);
if (duplicateNames.size() > 0) {
throw new RootDirectoryException(String.format("Duplicate root directory names found: %s", String.join(", ", duplicateNames)));
}
}

private static List<String> getDuplicateSubmissionFolderNames(JPlagOptions options) {
List<String> duplicateNames = new ArrayList<>();
Set<String> alreadyFoundNames = new HashSet<>();
for (File file : options.submissionDirectories()) {
if (!alreadyFoundNames.add(file.getName())) {
duplicateNames.add(file.getName());
}
}
for (File file : options.oldSubmissionDirectories()) {
if (!alreadyFoundNames.add(file.getName())) {
duplicateNames.add(file.getName());
}
}
return duplicateNames;
}
}
102 changes: 98 additions & 4 deletions core/src/main/java/de/jplag/SubmissionSetBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -80,10 +82,13 @@ public SubmissionSet buildSubmissionSet() throws ExitException {
submissionFiles.addAll(listSubmissionFiles(submissionDirectory, false));
}

Set<File> allRootDirectories = submissionFiles.stream().map(SubmissionFileData::root).collect(Collectors.toSet());
Map<File, String> rootDirectoryNamePrefixesMapper = getRootDirectoryNamesPrefixesMapper(allRootDirectories);

ProgressBar progressBar = ProgressBarLogger.createProgressBar(ProgressBarType.LOADING, submissionFiles.size());
Map<File, Submission> foundSubmissions = new HashMap<>();
for (SubmissionFileData submissionFile : submissionFiles) {
processSubmissionFile(submissionFile, multipleRoots, foundSubmissions);
processSubmissionFile(submissionFile, multipleRoots, rootDirectoryNamePrefixesMapper, foundSubmissions);
progressBar.step();
}
progressBar.dispose();
Expand All @@ -103,6 +108,92 @@ public SubmissionSet buildSubmissionSet() throws ExitException {
return new SubmissionSet(submissions, baseCodeSubmission.orElse(null), options);
}

private static String[] getCanonicalPathComponents(File path) {
try {
return path.getCanonicalPath().split(File.separator.equals("\\") ? "\\\\" : File.separator);
} catch (Exception e) {
throw new RuntimeException("Error getting canonical path", e);
}
}

public static File getCommonAncestor(File firstPath, File secondPath) {
String[] firstComponents = getCanonicalPathComponents(firstPath);
String[] secondComponents = getCanonicalPathComponents(secondPath);

int minLength = Math.min(firstComponents.length, secondComponents.length);
int commonLength = 0;

for (int i = 0; i < minLength; i++) {
if (firstComponents[i].equals(secondComponents[i])) {
commonLength++;
} else {
break;
}
}

if (commonLength == 0) {
return null;
}

StringBuilder commonPath = new StringBuilder(firstComponents[0]);
for (int i = 1; i < commonLength; i++) {
commonPath.append(File.separator).append(firstComponents[i]);
}

return new File(commonPath.toString());
}

private String findCommonPathPrefix(List<File> canonicalPaths) {
if (canonicalPaths == null) {
return "";
}

File prefix = canonicalPaths.getFirst();
for (int i = 1; i < canonicalPaths.size(); i++) {
prefix = getCommonAncestor(prefix, canonicalPaths.get(i));
}

return prefix == null ? null : prefix.toString();
}

private String getPathPrefix(File path, String commonPrefix) {
String result = path.toString().substring(commonPrefix.length());
return result.startsWith(File.separator) ? result.substring(1) : result;
}

private Map<File, String> getRootDirectoryNamesPrefixesMapper(Set<File> allRootDirectories) {
Map<String, List<File>> conflicts = getRootDirectoryNameConflicts(allRootDirectories);

Map<File, String> result = new HashMap<>();
conflicts.forEach((name, paths) -> {
if (paths.size() > 1) {
String commonPrefix = findCommonPathPrefix(paths);
for (File path : paths) {
result.put(path, getPathPrefix(path, commonPrefix));
}
} else {
result.put(paths.getFirst(), "");
}
});

return result;
}

private static Map<String, List<File>> getRootDirectoryNameConflicts(Set<File> allRootDirectories) {
Map<String, List<File>> conflicts = new HashMap<>();

for (File rootDir : allRootDirectories) {
String roodDirName = rootDir.getName();
if (conflicts.containsKey(roodDirName)) {
conflicts.get(roodDirName).add(rootDir);
} else {
conflicts.put(roodDirName, Stream.of(rootDir).collect(Collectors.toList()));
}
}

return conflicts;
}

/**
* Verify that the given root directories exist and have no duplicate entries.
*/
Expand Down Expand Up @@ -219,14 +310,17 @@ private Submission processSubmission(String submissionName, File submissionFile,
return new Submission(submissionName, file, isNew, parseFilesRecursively(file), options.language());
}

private void processSubmissionFile(SubmissionFileData file, boolean multipleRoots, Map<File, Submission> foundSubmissions) throws ExitException {
private void processSubmissionFile(SubmissionFileData file, boolean multipleRoots, Map<File, String> rootDirectoryNamePrefixesMapper,
Map<File, Submission> foundSubmissions) throws ExitException {
if (isFileExcluded(file.submissionFile())) {
logger.error("Exclude submission: {}", file.submissionFile().getName());
} else if (file.submissionFile().isFile() && !hasValidSuffix(file.submissionFile())) {
logger.error("Ignore submission with invalid suffix: {}", file.submissionFile().getName());
} else {
String rootDirectoryPrefix = multipleRoots ? (file.root().getName() + File.separator) : "";
String submissionName = rootDirectoryPrefix + file.submissionFile().getName();
String rootDirectoryPrefix = rootDirectoryNamePrefixesMapper.get(file.root());
rootDirectoryPrefix = rootDirectoryPrefix.isEmpty() && multipleRoots ? file.root().getName() : rootDirectoryPrefix;
String submissionName = rootDirectoryPrefix.isEmpty() ? file.submissionFile().getName()
: rootDirectoryPrefix + File.separator + file.submissionFile().getName();
Submission submission = processSubmission(submissionName, file.submissionFile(), file.isNew());
foundSubmissions.put(submission.getRoot(), submission);
}
Expand Down
45 changes: 41 additions & 4 deletions core/src/test/java/de/jplag/RootFolderTest.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
package de.jplag;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.*;

import java.io.File;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

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

import de.jplag.exceptions.ExitException;
import de.jplag.exceptions.RootDirectoryException;

/**
* Test class for the multi-root feature and the old-new feature.
Expand Down Expand Up @@ -71,7 +71,7 @@ void testDisjunctNewAndOldRootDirectories() throws ExitException {
void testOverlappingNewAndOldDirectoriesOverlap() throws ExitException {
List<String> newDirectories = List.of(getBasePath(ROOT_2));
List<String> oldDirectories = List.of(getBasePath(ROOT_2));
assertThrows(RootDirectoryException.class, () -> runJPlag(newDirectories, oldDirectories, it -> it));
assertDoesNotThrow(() -> runJPlag(newDirectories, oldDirectories, it -> it));
}

@Test
Expand All @@ -84,4 +84,41 @@ void testBasecodeInOldDirectory() throws ExitException {
int numberOfExpectedComparison = 1 + ROOT_COUNT_2 * (ROOT_COUNT_1 - 1); // -1 for basecode
assertEquals(numberOfExpectedComparison, result.getAllComparisons().size());
}

@Test
@DisplayName("test multiple submissions with same folder name")
void testSubmissionsWithSameFolderName() throws ExitException {
List<String> newSubmissionsNames = List.of("2023");
List<String> oldSubmissionsNames = List.of("2022", "2021", "2020");
List<String> newSubmissions = newSubmissionsNames.stream().map(it -> getBasePath("SubmissionsWithSameName" + File.separator + it)).toList();
List<String> oldSubmissions = oldSubmissionsNames.stream()
.map(it -> getBasePath("SubmissionsWithSameName" + File.separator + "old" + File.separator + it)).toList();

JPlagResult result = runJPlag(newSubmissions, oldSubmissions, it -> it);

long numberOfNewSubmissions = result.getSubmissions().getSubmissions().stream().filter(Submission::isNew).count();
long numberOfOldSubmissions = result.getSubmissions().getSubmissions().stream().filter(it -> !it.isNew()).count();
assertEquals(2, numberOfNewSubmissions);
assertEquals(6, numberOfOldSubmissions);

Set<String> submissionNames = result.getSubmissions().getSubmissions().stream().map(Submission::getName).collect(Collectors.toSet());
Set<String> expectedNames = Set.of("2023/gr1", "2023/gr2", "2022/gr1", "2022/gr2", "2021/gr1", "2021/gr2", "2020/gr1", "2020/gr2");
assertEquals(expectedNames, submissionNames);
}

@Test
@DisplayName("test single new submission")
void testSingleNewSubmission() throws ExitException {
List<String> newSubmissionsNames = List.of("2023");
List<String> oldSubmissionsNames = List.of("2022", "2021", "2020");
List<String> newSubmissions = newSubmissionsNames.stream().map(it -> getBasePath("SingleNewSubmission" + File.separator + it)).toList();
List<String> oldSubmissions = oldSubmissionsNames.stream().map(it -> getBasePath("SingleNewSubmission" + File.separator + it)).toList();

JPlagResult result = runJPlag(newSubmissions, oldSubmissions, it -> it);

long numberOfNewSubmissions = result.getSubmissions().getSubmissions().stream().filter(Submission::isNew).count();
long numberOfOldSubmissions = result.getSubmissions().getSubmissions().stream().filter(it -> !it.isNew()).count();
assertEquals(1, numberOfNewSubmissions);
assertEquals(3, numberOfOldSubmissions);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.List;
import java.util.stream.Stream;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -37,6 +39,27 @@ void testCreateAndSaveReportWithBasecode() throws ExitException, IOException {
assertTrue(isArchive(testZip));
}

@Test
void testWithSamenameSubmissions() throws ExitException, IOException {
File submission1 = new File(BASE_PATH, "basecode/A");
File submission2 = new File(BASE_PATH, "basecode/B");
File submission3 = new File(BASE_PATH, "basecode-sameNameOfSubdirectoryAndRootdirectory/A");
File submission4 = new File(BASE_PATH, "basecode-sameNameOfSubdirectoryAndRootdirectory/A");
List<String> submissions = Stream.of(submission1, submission2, submission3, submission4).map(File::toString).toList();
JPlagResult result = runJPlag(submissions, it -> it.withBaseCodeSubmissionDirectory(new File(BASE_PATH, BASECODE_BASE)));
File testZip = File.createTempFile("result", ".zip");
ReportObjectFactory reportObjectFactory = new ReportObjectFactory(testZip);
reportObjectFactory.createAndSaveReport(result);

assertNotNull(result);
assertTrue(isArchive(testZip));

String[] expectedSubmissionNames = new String[] {"B/TerrainType.java", "basecode/A/TerrainType.java"};
for (String expected : expectedSubmissionNames) {
assertTrue(result.getSubmissions().getSubmissions().stream().anyMatch(submission -> submission.getName().equals(expected)));
}
}

/**
* Checks if the given file is a valid archive
* @param file The file to check
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import java.util.ArrayList;
import java.util.List;

public class QSort2020 {
public String[] qsort(String[] array) {
if (array == null || array.length == 0) {
return array;
}
List<String> sortedList = quickSort(List.of(array));
return sortedList.toArray(new String[0]);
}

private List<String> quickSort(List<String> list) {
if (list.size() <= 1) {
return list;
}

String pivot = list.get(list.size() / 2);
List<String> less = new ArrayList<>();
List<String> equal = new ArrayList<>();
List<String> greater = new ArrayList<>();

for (String s : list) {
int comparison = s.compareTo(pivot);
if (comparison < 0) {
less.add(s);
} else if (comparison > 0) {
greater.add(s);
} else {
equal.add(s);
}
}

List<String> sorted = new ArrayList<>();
sorted.addAll(quickSort(less));
sorted.addAll(equal);
sorted.addAll(quickSort(greater));
return sorted;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import java.util.Stack;

public class QSort2021 {
public String[] qsort(String[] array) {
if (array == null || array.length == 0) {
return array;
}

Stack<int[]> stack = new Stack<>();
stack.push(new int[] { 0, array.length - 1 });

while (!stack.isEmpty()) {
int[] range = stack.pop();
int low = range[0], high = range[1];

if (low < high) {
int pivotIndex = partition(array, low, high);
stack.push(new int[] { low, pivotIndex - 1 });
stack.push(new int[] { pivotIndex + 1, high });
}
}

return array;
}

private int partition(String[] array, int low, int high) {
String pivot = array[high];
int i = low - 1;
for (int j = low; j < high; j++) {
if (array[j].compareTo(pivot) <= 0) {
i++;
swap(array, i, j);
}
}
swap(array, i + 1, high);
return i + 1;
}

private void swap(String[] array, int i, int j) {
String temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
Loading