diff --git a/.github/workflows/complete-e2e.yml b/.github/workflows/complete-e2e.yml index 332cc834e1..40cec33bc8 100644 --- a/.github/workflows/complete-e2e.yml +++ b/.github/workflows/complete-e2e.yml @@ -43,7 +43,7 @@ jobs: node-version: "18" - name: Build Assembly - run: mvn -Pwith-report-viewer -DskipTests clean package assembly:single + run: mvn -DskipTests clean package assembly:single - name: Rename Jar run: mv cli/target/jplag-*-jar-with-dependencies.jar cli/target/jplag.jar diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 9629427c80..ab2bb989cd 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -7,6 +7,7 @@ on: - "**/pom.xml" - "**.java" - "**.g4" + - "report-viewer/**" pull_request: types: [opened, synchronize, reopened] paths: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 3fcc4b7e3c..f71b7250c2 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -38,6 +38,19 @@ jobs: with: node-version: "18" + - name: Set version of Report Viewer + shell: bash + run: | + VERSION=$(grep "" pom.xml | grep -oPm1 "(?<=)[^-|<]+") + MAJOR=$(echo $VERSION | cut -d '.' -f 1) + MINOR=$(echo $VERSION | cut -d '.' -f 2) + PATCH=$(echo $VERSION | cut -d '.' -f 3) + json=$(cat report-viewer/src/version.json) + json=$(echo "$json" | jq --arg MAJOR "$MAJOR" --arg MINOR "$MINOR" --arg PATCH "$PATCH" '.report_viewer_version |= { "major": $MAJOR | tonumber, "minor": $MINOR | tonumber, "patch": $PATCH | tonumber }') + echo "$json" > report-viewer/src/version.json + echo "Version of Report Viewer:" + cat report-viewer/src/version.json + - name: Build JPlag run: mvn -Pwith-report-viewer -U -B clean package assembly:single diff --git a/.github/workflows/report-viewer.yml b/.github/workflows/report-viewer.yml deleted file mode 100644 index e045ea06d0..0000000000 --- a/.github/workflows/report-viewer.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Report Viewer Deployment Workflow - -on: - workflow_dispatch: # Use this to dispatch from the Actions Tab - push: - branches: - - main - -jobs: - build-and-deploy: - runs-on: ubuntu-latest - steps: - - name: Checkout ๐Ÿ›Ž๏ธ - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 - with: - node-version: "18" - - - name: Set version of Report Viewer - shell: bash - run: | - VERSION=$(grep "" pom.xml | grep -oPm1 "(?<=)[^-|<]+") - MAJOR=$(echo $VERSION | cut -d '.' -f 1) - MINOR=$(echo $VERSION | cut -d '.' -f 2) - PATCH=$(echo $VERSION | cut -d '.' -f 3) - json=$(cat report-viewer/src/version.json) - json=$(echo "$json" | jq --arg MAJOR "$MAJOR" --arg MINOR "$MINOR" --arg PATCH "$PATCH" '.report_viewer_version |= { "major": $MAJOR | tonumber, "minor": $MINOR | tonumber, "patch": $PATCH | tonumber }') - echo "$json" > report-viewer/src/version.json - echo "Version of Report Viewer:" - cat report-viewer/src/version.json - - - name: Install and Build ๐Ÿ”ง - working-directory: report-viewer - run: | - npm install - npm run build-prod - - - name: Deploy ๐Ÿš€ - uses: JamesIves/github-pages-deploy-action@v4.6.3 - with: - branch: gh-pages - folder: report-viewer/dist diff --git a/cli/src/main/java/de/jplag/cli/options/CliOptions.java b/cli/src/main/java/de/jplag/cli/options/CliOptions.java index 73f95f91bc..748a8f6c8b 100644 --- a/cli/src/main/java/de/jplag/cli/options/CliOptions.java +++ b/cli/src/main/java/de/jplag/cli/options/CliOptions.java @@ -54,7 +54,7 @@ public class CliOptions implements Runnable { public String resultFile = "results"; @Option(names = {"-M", "--mode"}, description = "The mode of JPlag. One of: ${COMPLETION-CANDIDATES} (default: ${DEFAULT_VALUE})") - public JPlagMode mode = JPlagMode.RUN; + public JPlagMode mode = JPlagMode.RUN_AND_VIEW; @Option(names = {"--normalize"}, description = "Activate the normalization of tokens. Supported for languages: Java, C++.") public boolean normalize = false; diff --git a/cli/src/test/java/de/jplag/cli/DebugTest.java b/cli/src/test/java/de/jplag/cli/DebugTest.java new file mode 100644 index 0000000000..a6f22f74c0 --- /dev/null +++ b/cli/src/test/java/de/jplag/cli/DebugTest.java @@ -0,0 +1,27 @@ +package de.jplag.cli; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +import de.jplag.cli.test.CliArgument; +import de.jplag.cli.test.CliTest; +import de.jplag.exceptions.ExitException; +import de.jplag.options.JPlagOptions; + +class DebugTest extends CliTest { + @Test + void testDefaultDebug() throws IOException, ExitException { + JPlagOptions options = runCliForOptions(); + assertFalse(options.debugParser()); + } + + @Test + void testSetDebug() throws IOException, ExitException { + JPlagOptions options = runCliForOptions(args -> args.with(CliArgument.DEBUG, true)); + assertTrue(options.debugParser()); + } +} diff --git a/cli/src/test/java/de/jplag/cli/ExcludeFileTest.java b/cli/src/test/java/de/jplag/cli/ExcludeFileTest.java new file mode 100644 index 0000000000..e2cb116699 --- /dev/null +++ b/cli/src/test/java/de/jplag/cli/ExcludeFileTest.java @@ -0,0 +1,29 @@ +package de.jplag.cli; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +import de.jplag.cli.test.CliArgument; +import de.jplag.cli.test.CliTest; +import de.jplag.exceptions.ExitException; +import de.jplag.options.JPlagOptions; + +class ExcludeFileTest extends CliTest { + private static final String EXCLUDE_FILE_NAME = "exclusions"; + + @Test + void testNoDefaultExcludeFile() throws IOException, ExitException { + JPlagOptions options = runCliForOptions(); + assertNull(options.exclusionFileName()); + } + + @Test + void testSetExcludeFile() throws IOException, ExitException { + JPlagOptions options = runCliForOptions(args -> args.with(CliArgument.EXCLUDE_FILES, EXCLUDE_FILE_NAME)); + assertEquals(EXCLUDE_FILE_NAME, options.exclusionFileName()); + } +} diff --git a/cli/src/test/java/de/jplag/cli/LogLevelTest.java b/cli/src/test/java/de/jplag/cli/LogLevelTest.java new file mode 100644 index 0000000000..9994de5fa9 --- /dev/null +++ b/cli/src/test/java/de/jplag/cli/LogLevelTest.java @@ -0,0 +1,28 @@ +package de.jplag.cli; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; +import org.slf4j.event.Level; + +import de.jplag.cli.test.CliArgument; +import de.jplag.cli.test.CliTest; +import de.jplag.exceptions.ExitException; + +class LogLevelTest extends CliTest { + private static final Level DEFAULT_LOG_LEVEL = Level.INFO; + + @Test + void testDefaultLogLevel() throws IOException, ExitException { + Level level = runCliForLogLevel(); + assertEquals(DEFAULT_LOG_LEVEL, level); + } + + @Test + void testSetLogLevel() throws IOException, ExitException { + Level level = runCliForLogLevel(args -> args.with(CliArgument.LOG_LEVEL, Level.ERROR.name())); + assertEquals(Level.ERROR, level); + } +} diff --git a/cli/src/test/java/de/jplag/cli/ResultFileTest.java b/cli/src/test/java/de/jplag/cli/ResultFileTest.java new file mode 100644 index 0000000000..888d166fd3 --- /dev/null +++ b/cli/src/test/java/de/jplag/cli/ResultFileTest.java @@ -0,0 +1,68 @@ +package de.jplag.cli; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +import org.junit.jupiter.api.Test; + +import de.jplag.cli.test.CliArgument; +import de.jplag.cli.test.CliTest; +import de.jplag.exceptions.ExitException; + +class ResultFileTest extends CliTest { + private static final String DEFAULT_RESULT_FILE = "results.zip"; + private static final String TEST_RESULT_FILE = "customResults.zip"; + private static final String TEST_RESULT_FILE_WITH_AVOIDANCE = "customResults(1).zip"; + private static final String TEST_RESULT_FILE_WITHOUT_ZIP = "customResults"; + + @Test + void testDefaultResultFolder() throws IOException, ExitException { + String targetPath = runCliForTargetPath(); + assertEquals(DEFAULT_RESULT_FILE, targetPath); + } + + @Test + void testSetResultFolder() throws IOException, ExitException { + String targetPath = runCliForTargetPath(args -> args.with(CliArgument.RESULT_FILE, TEST_RESULT_FILE)); + assertEquals(TEST_RESULT_FILE, targetPath); + } + + @Test + void testSetResultFolderWithoutZip() throws IOException, ExitException { + String targetPath = runCliForTargetPath(args -> args.with(CliArgument.RESULT_FILE, TEST_RESULT_FILE_WITHOUT_ZIP)); + assertEquals(TEST_RESULT_FILE, targetPath); + } + + @Test + void testResultFileOverrideAvoidance() throws IOException, ExitException { + File testDir = Files.createTempDirectory("JPlagResultFileTest").toFile(); + File targetFile = new File(testDir, TEST_RESULT_FILE); + File expectedTargetFile = new File(testDir, TEST_RESULT_FILE_WITH_AVOIDANCE); + targetFile.createNewFile(); + + String actualTargetPath = runCliForTargetPath(args -> args.with(CliArgument.RESULT_FILE, targetFile.getAbsolutePath())); + + targetFile.delete(); + testDir.delete(); + + assertEquals(expectedTargetFile.getAbsolutePath(), actualTargetPath); + } + + @Test + void testResultFileOverwrite() throws IOException, ExitException { + File testDir = Files.createTempDirectory("JPlagResultFileTest").toFile(); + File targetFile = new File(testDir, TEST_RESULT_FILE); + targetFile.createNewFile(); + + String actualTargetPath = runCliForTargetPath( + args -> args.with(CliArgument.RESULT_FILE, targetFile.getAbsolutePath()).with(CliArgument.OVERWRITE_RESULT_FILE, true)); + + targetFile.delete(); + testDir.delete(); + + assertEquals(targetFile.getAbsolutePath(), actualTargetPath); + } +} diff --git a/cli/src/test/java/de/jplag/cli/SubdirectoryTest.java b/cli/src/test/java/de/jplag/cli/SubdirectoryTest.java new file mode 100644 index 0000000000..e2d319551f --- /dev/null +++ b/cli/src/test/java/de/jplag/cli/SubdirectoryTest.java @@ -0,0 +1,29 @@ +package de.jplag.cli; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +import de.jplag.cli.test.CliArgument; +import de.jplag.cli.test.CliTest; +import de.jplag.exceptions.ExitException; +import de.jplag.options.JPlagOptions; + +class SubdirectoryTest extends CliTest { + private static final String TEST_SUBDIRECTORY = "dir"; + + @Test + void testDefaultSubdirectory() throws IOException, ExitException { + JPlagOptions options = runCliForOptions(); + assertNull(options.subdirectoryName()); + } + + @Test + void testSetSubdirectory() throws IOException, ExitException { + JPlagOptions options = runCliForOptions(args -> args.with(CliArgument.SUBDIRECTORY, TEST_SUBDIRECTORY)); + assertEquals(TEST_SUBDIRECTORY, options.subdirectoryName()); + } +} diff --git a/cli/src/test/java/de/jplag/cli/SuffixesTest.java b/cli/src/test/java/de/jplag/cli/SuffixesTest.java new file mode 100644 index 0000000000..ab095224b9 --- /dev/null +++ b/cli/src/test/java/de/jplag/cli/SuffixesTest.java @@ -0,0 +1,30 @@ +package de.jplag.cli; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import de.jplag.cli.test.CliArgument; +import de.jplag.cli.test.CliTest; +import de.jplag.exceptions.ExitException; +import de.jplag.options.JPlagOptions; + +class SuffixesTest extends CliTest { + private static final List JAVA_SUFFIXES = List.of(".java", ".JAVA"); + private static final List CUSTOM_SUFFIXES = List.of(".j", ".jva"); + + @Test + void testDefaultSuffixes() throws IOException, ExitException { + JPlagOptions options = runCliForOptions(); + assertEquals(JAVA_SUFFIXES, options.fileSuffixes()); + } + + @Test + void testSetSuffixes() throws IOException, ExitException { + JPlagOptions options = runCliForOptions(args -> args.with(CliArgument.SUFFIXES, CUSTOM_SUFFIXES.toArray(new String[0]))); + assertEquals(CUSTOM_SUFFIXES, options.fileSuffixes()); + } +} diff --git a/cli/src/test/java/de/jplag/cli/test/CliArgument.java b/cli/src/test/java/de/jplag/cli/test/CliArgument.java index 6ac22a524c..3b90977ab4 100644 --- a/cli/src/test/java/de/jplag/cli/test/CliArgument.java +++ b/cli/src/test/java/de/jplag/cli/test/CliArgument.java @@ -28,4 +28,10 @@ public record CliArgument(String name, boolean isPositional) { public static CliArgument RESULT_FILE = new CliArgument<>("r", false); public static CliArgument OVERWRITE_RESULT_FILE = new CliArgument<>("overwrite", false); + + public static CliArgument LOG_LEVEL = new CliArgument<>("log-level", false); + public static CliArgument DEBUG = new CliArgument<>("d", false); + + public static CliArgument SUBDIRECTORY = new CliArgument<>("subdirectory", false); + public static CliArgument EXCLUDE_FILES = new CliArgument<>("x", false); } diff --git a/cli/src/test/java/de/jplag/cli/test/CliResult.java b/cli/src/test/java/de/jplag/cli/test/CliResult.java index 4978a5195e..7051a6061f 100644 --- a/cli/src/test/java/de/jplag/cli/test/CliResult.java +++ b/cli/src/test/java/de/jplag/cli/test/CliResult.java @@ -1,6 +1,8 @@ package de.jplag.cli.test; +import org.slf4j.event.Level; + import de.jplag.options.JPlagOptions; -public record CliResult(JPlagOptions jPlagOptions, String targetPath) { +public record CliResult(JPlagOptions jPlagOptions, String targetPath, Level logLevel) { } diff --git a/cli/src/test/java/de/jplag/cli/test/CliTest.java b/cli/src/test/java/de/jplag/cli/test/CliTest.java index d26a1fb477..bff7528a60 100644 --- a/cli/src/test/java/de/jplag/cli/test/CliTest.java +++ b/cli/src/test/java/de/jplag/cli/test/CliTest.java @@ -11,9 +11,11 @@ import org.junit.jupiter.api.BeforeEach; import org.mockito.MockedStatic; import org.mockito.Mockito; +import org.slf4j.event.Level; import de.jplag.JPlagResult; import de.jplag.cli.*; +import de.jplag.cli.logger.CollectedLogger; import de.jplag.cli.picocli.CliInputHandler; import de.jplag.exceptions.ExitException; import de.jplag.options.JPlagOptions; @@ -105,6 +107,29 @@ protected String runCliForTargetPath(Consumer additionalOpti return runCli(additionalOptionsBuilder).targetPath(); } + /** + * Runs the cli + * @return The log level set by the cli + * @throws ExitException If JPlag throws an exception + * @throws IOException If JPlag throws an exception + * @see #runCli() + */ + protected Level runCliForLogLevel() throws IOException, ExitException { + return runCli().logLevel(); + } + + /** + * Runs the cli using custom options + * @param additionalOptionsBuilder May modify the {@link CliArgumentBuilder} object to set custom options for this run. + * @return The log level set by the cli + * @throws ExitException If JPlag throws an exception + * @throws IOException If JPlag throws an exception + * @see #runCli() + */ + protected Level runCliForLogLevel(Consumer additionalOptionsBuilder) throws IOException, ExitException { + return runCli(additionalOptionsBuilder).logLevel(); + } + /** * Runs the cli * @return The options returned by the cli @@ -129,7 +154,7 @@ protected CliResult runCli(Consumer additionalOptionsBuilder String targetPath = (String) getWritableFileMethod.invoke(cli); - return new CliResult(optionsBuilder.buildOptions(), targetPath); + return new CliResult(optionsBuilder.buildOptions(), targetPath, CollectedLogger.getLogLevel()); } catch (IllegalAccessException | InvocationTargetException e) { Assumptions.abort("Could not access private field in CLI for test."); return null; // will not be executed diff --git a/core/pom.xml b/core/pom.xml index 2fa264d420..a55478e084 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -56,7 +56,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.8.0 + 3.10.0 src/main/java;target/generated-sources/annotations diff --git a/core/src/main/java/de/jplag/reporting/jsonfactory/BaseCodeReportWriter.java b/core/src/main/java/de/jplag/reporting/jsonfactory/BaseCodeReportWriter.java index e5217330dd..1f4a9a279d 100644 --- a/core/src/main/java/de/jplag/reporting/jsonfactory/BaseCodeReportWriter.java +++ b/core/src/main/java/de/jplag/reporting/jsonfactory/BaseCodeReportWriter.java @@ -66,9 +66,11 @@ private BaseCodeMatch convertToBaseCodeMatch(Submission submission, Match match, List tokens = submission.getTokenList().subList(takeLeft ? match.startOfFirst() : match.startOfSecond(), (takeLeft ? match.endOfFirst() : match.endOfSecond()) + 1); - Comparator lineComparator = Comparator.comparingInt(Token::getLine); - Token start = tokens.stream().min(lineComparator).orElseThrow(); - Token end = tokens.stream().max(lineComparator).orElseThrow(); + Comparator lineStartComparator = Comparator.comparingInt(Token::getLine).thenComparingInt(Token::getColumn); + Comparator lineEndComparator = Comparator.comparingInt(Token::getLine) + .thenComparingInt((Token t) -> t.getColumn() + t.getLength()); + Token start = tokens.stream().min(lineStartComparator).orElseThrow(); + Token end = tokens.stream().max(lineEndComparator).orElseThrow(); CodePosition startPosition = new CodePosition(start.getLine(), start.getColumn() - 1, takeLeft ? match.startOfFirst() : match.startOfSecond()); diff --git a/core/src/main/java/de/jplag/reporting/jsonfactory/ComparisonReportWriter.java b/core/src/main/java/de/jplag/reporting/jsonfactory/ComparisonReportWriter.java index f8732dac44..35ef87de59 100644 --- a/core/src/main/java/de/jplag/reporting/jsonfactory/ComparisonReportWriter.java +++ b/core/src/main/java/de/jplag/reporting/jsonfactory/ComparisonReportWriter.java @@ -99,12 +99,14 @@ private Match convertMatchToReportMatch(JPlagComparison comparison, de.jplag.Mat List tokensFirst = comparison.firstSubmission().getTokenList().subList(match.startOfFirst(), match.endOfFirst() + 1); List tokensSecond = comparison.secondSubmission().getTokenList().subList(match.startOfSecond(), match.endOfSecond() + 1); - Comparator lineComparator = Comparator.comparingInt(Token::getLine).thenComparingInt(Token::getColumn); - - Token startOfFirst = tokensFirst.stream().min(lineComparator).orElseThrow(); - Token endOfFirst = tokensFirst.stream().max(lineComparator).orElseThrow(); - Token startOfSecond = tokensSecond.stream().min(lineComparator).orElseThrow(); - Token endOfSecond = tokensSecond.stream().max(lineComparator).orElseThrow(); + Comparator lineStartComparator = Comparator.comparingInt(Token::getLine).thenComparingInt(Token::getColumn); + Comparator lineEndComparator = Comparator.comparingInt(Token::getLine) + .thenComparingInt((Token t) -> t.getColumn() + t.getLength()); + + Token startOfFirst = tokensFirst.stream().min(lineStartComparator).orElseThrow(); + Token endOfFirst = tokensFirst.stream().max(lineEndComparator).orElseThrow(); + Token startOfSecond = tokensSecond.stream().min(lineStartComparator).orElseThrow(); + Token endOfSecond = tokensSecond.stream().max(lineEndComparator).orElseThrow(); String firstFileName = FilePathUtil.getRelativeSubmissionPath(startOfFirst.getFile(), comparison.firstSubmission(), submissionToIdFunction) .toString(); diff --git a/core/src/test/java/de/jplag/reporting/jsonfactory/ReportTokenPositionTestTest.java b/core/src/test/java/de/jplag/reporting/jsonfactory/ReportTokenPositionTestTest.java new file mode 100644 index 0000000000..d186863e98 --- /dev/null +++ b/core/src/test/java/de/jplag/reporting/jsonfactory/ReportTokenPositionTestTest.java @@ -0,0 +1,150 @@ +package de.jplag.reporting.jsonfactory; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.nio.file.Path; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import de.jplag.JPlagComparison; +import de.jplag.JPlagResult; +import de.jplag.Match; +import de.jplag.Submission; +import de.jplag.Token; +import de.jplag.options.JPlagOptions; +import de.jplag.reporting.FilePathUtil; +import de.jplag.reporting.reportobject.model.BaseCodeMatch; +import de.jplag.reporting.reportobject.model.CodePosition; +import de.jplag.reporting.reportobject.model.ComparisonReport; + +class ReportTokenPositionTestTest { + + @Test + void testCorrectTokenPositionsInComparisonReport() { + JPlagResult result = mock(JPlagResult.class); + JPlagOptions mockOptions = createMockOptions(); + when(result.getOptions()).thenReturn(mockOptions); + JPlagComparison comparison = mock(JPlagComparison.class); + String firstID = "first"; + String secondID = "second"; + Submission firstSubmission = createMockSubmission(firstID); + Submission secondSubmission = createMockSubmission(secondID); + when(comparison.firstSubmission()).thenReturn(firstSubmission); + when(comparison.secondSubmission()).thenReturn(secondSubmission); + Match mockMatch = createMockMatch(0, 2, 0, 1); + when(comparison.matches()).thenReturn(List.of(mockMatch)); + + when(result.getComparisons(1)).thenReturn(List.of(comparison)); + + TestableReportWriter resultWriter = new TestableReportWriter(); + Map> comparisonReportOutput; + + try (MockedStatic mockedFilePathUtil = Mockito.mockStatic(FilePathUtil.class)) { + mockedFilePathUtil.when(() -> FilePathUtil.getRelativeSubmissionPath(any(), any(), any())).thenReturn(Path.of("file.java")); + comparisonReportOutput = new ComparisonReportWriter(Submission::getName, resultWriter).writeComparisonReports(result); + } + ComparisonReport comparisonReport = (ComparisonReport) resultWriter.getJsonEntry(Path.of(comparisonReportOutput.get(firstID).get(secondID))); + + assertEquals(1, comparisonReport.matches().size()); + + CodePosition startInFirst = comparisonReport.matches().get(0).startInFirst(); + CodePosition endInFirst = comparisonReport.matches().get(0).endInFirst(); + CodePosition startInSecond = comparisonReport.matches().get(0).startInSecond(); + CodePosition endInSecond = comparisonReport.matches().get(0).endInSecond(); + + assertEquals(1, startInFirst.lineNumber()); + assertEquals(0, startInFirst.column()); + assertEquals(0, startInFirst.tokenListIndex()); + + assertEquals(2, endInFirst.lineNumber()); + assertEquals(10, endInFirst.column()); + assertEquals(2, endInFirst.tokenListIndex()); + + assertEquals(1, startInSecond.lineNumber()); + assertEquals(0, startInSecond.column()); + assertEquals(0, startInSecond.tokenListIndex()); + + assertEquals(2, endInSecond.lineNumber()); + assertEquals(10, endInSecond.column()); + assertEquals(1, endInSecond.tokenListIndex()); + } + + @Test + void testCorrectTokenPositionsInBasecodeReport() { + JPlagResult result = mock(JPlagResult.class); + JPlagOptions mockOptions = createMockOptions(); + when(result.getOptions()).thenReturn(mockOptions); + JPlagComparison comparison = mock(JPlagComparison.class); + String submissionID = "first"; + String basecodeID = "basecode"; + Submission submission = createMockSubmission(submissionID); + Submission baseCodeSubmission = createMockSubmission(basecodeID); + when(comparison.firstSubmission()).thenReturn(submission); + when(comparison.secondSubmission()).thenReturn(baseCodeSubmission); + Match mockMatch = createMockMatch(0, 2, 0, 1); + when(comparison.matches()).thenReturn(List.of(mockMatch)); + when(submission.getBaseCodeComparison()).thenReturn(comparison); + + when(result.getComparisons(1)).thenReturn(List.of(comparison)); + + TestableReportWriter resultWriter = new TestableReportWriter(); + try (MockedStatic mockedFilePathUtil = Mockito.mockStatic(FilePathUtil.class)) { + mockedFilePathUtil.when(() -> FilePathUtil.getRelativeSubmissionPath(any(), any(), any())).thenReturn(Path.of("file.java")); + new BaseCodeReportWriter(Submission::getName, resultWriter).writeBaseCodeReport(result); + } + + List baseCodeMatches = (List) resultWriter.getJsonEntry(Path.of("basecode", submissionID + ".json")); + + assertEquals(1, baseCodeMatches.size()); + + CodePosition start = baseCodeMatches.get(0).start(); + CodePosition end = baseCodeMatches.get(0).end(); + + assertEquals(1, start.lineNumber()); + assertEquals(0, start.column()); + assertEquals(0, start.tokenListIndex()); + + assertEquals(2, end.lineNumber()); + assertEquals(10, end.column()); + assertEquals(2, end.tokenListIndex()); + } + + JPlagOptions createMockOptions() { + JPlagOptions options = mock(JPlagOptions.class); + when(options.maximumNumberOfComparisons()).thenReturn(1); + return options; + } + + Submission createMockSubmission(String name) { + Submission submission = mock(Submission.class); + when(submission.getName()).thenReturn(name); + List tokens = List.of(createMockToken(1, 1, 10), createMockToken(2, 1, 10), createMockToken(2, 3, 2), createMockToken(2, 10, 2)); + when(submission.getTokenList()).thenReturn(tokens); + return submission; + } + + Match createMockMatch(int startOfFirst, int endOfFirst, int startOfSecond, int endOfSecond) { + Match match = mock(Match.class); + when(match.startOfFirst()).thenReturn(startOfFirst); + when(match.endOfFirst()).thenReturn(endOfFirst); + when(match.startOfSecond()).thenReturn(startOfSecond); + when(match.endOfSecond()).thenReturn(endOfSecond); + return match; + } + + Token createMockToken(int line, int column, int length) { + Token token = mock(Token.class); + when(token.getLine()).thenReturn(line); + when(token.getColumn()).thenReturn(column); + when(token.getLength()).thenReturn(length); + return token; + } + +} diff --git a/core/src/test/java/de/jplag/reporting/jsonfactory/TestableReportWriter.java b/core/src/test/java/de/jplag/reporting/jsonfactory/TestableReportWriter.java new file mode 100644 index 0000000000..77362106fd --- /dev/null +++ b/core/src/test/java/de/jplag/reporting/jsonfactory/TestableReportWriter.java @@ -0,0 +1,25 @@ +package de.jplag.reporting.jsonfactory; + +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; + +import de.jplag.reporting.reportobject.writer.DummyResultWriter; + +public class TestableReportWriter extends DummyResultWriter { + + public final Map jsonEntries; + + public TestableReportWriter() { + jsonEntries = new HashMap<>(); + } + + @Override + public void addJsonEntry(Object jsonContent, Path path) { + jsonEntries.put(path, jsonContent); + } + + public Object getJsonEntry(Path path) { + return jsonEntries.get(path); + } +} diff --git a/docs/1.-How-to-Use-JPlag.md b/docs/1.-How-to-Use-JPlag.md index a6e52ff669..8702a97065 100644 --- a/docs/1.-How-to-Use-JPlag.md +++ b/docs/1.-How-to-Use-JPlag.md @@ -6,8 +6,8 @@ JPlag can be used via the Command Line Interface by executing the JAR file. Example: `java -jar jplag.jar path/to/the/submissions` The language can either be set with the -l parameter or as a subcommand. If both a subcommand and the -l option are specified, the subcommand will take priority. -When using the subcommand language specific arguments can be set. -A list of language specific options can be obtained by requesting the help page of a subcommand (e.g. "jplag java -h"). +When using the subcommand, language-specific arguments can be set. +A list of language-specific options can be obtained by requesting the help page of a subcommand (e.g., "jplag java -h"). The following arguments can be used to control JPlag: ``` @@ -86,7 +86,7 @@ Subcommands (supported languages): *Note that the [legacy CLI](https://github.com/jplag/jplag/blob/legacy/README.md) is varying slightly.* ## Using JPlag programmatically -The new API makes it easy to integrate JPlag's plagiarism detection into external Java projects. +The API makes it easy to integrate JPlag's plagiarism detection into external Java projects. **Example:** @@ -110,16 +110,16 @@ try { ## Report File Generation -After a JPlag run a zipped result report is automatically created. +After a JPlag run, a zipped result report is automatically created. The target location of the report can be specified with the `-r` flag. If the `-r` is not specified, the location defaults `result.zip`. Specifying the `-r` flag with a path `/path/to/desiredFolder` results in the report being created as `/path/to/desiredFolder.zip`. -Unless there is an error during the zipping process, the report will always be zipped. If the zipping process fails, the report will be available as unzipped under the specified location. +The report will always be zipped unless there is an error during the zipping process. If the zipping process fails, the report will be available in an unzipped version under the specified location. ## Viewing Reports -The newest version of the report viewer is always accessible at https://jplag.github.io/JPlag/. Simply drop your `result.zip` folder on the page to start inspecting the results of your JPlag run. Your submissions will neither be uploaded to a server nor stored permanently. They are saved in the application as long as you view them. Once you refresh the page, all information will be erased. +The newest version of the report viewer is always accessible at https://jplag.github.io/JPlag/. Drop your `result.zip` folder on the page to start inspecting the results of your JPlag run. Your submissions will neither be uploaded to a server nor stored permanently. They are saved in the application as long as you view them. Once you refresh the page, all information will be erased. ## Basic Concepts @@ -127,7 +127,7 @@ The newest version of the report viewer is always accessible at https://jplag.gi This section explains some fundamental concepts about JPlag that make it easier to understand and use. * **Root directory:** This is the directory in which JPlag will scan for submissions. -* **Submissions:** Submissions contain the source code that JPlag will parse and compare. They have to be direct children of the root directory and can either be single files or directories. +* **Submissions:** Submissions contain the source code JPlag will parse and compare. They have to be direct children of the root directory and can either be single files or directories. ### Single-file submissions @@ -140,7 +140,7 @@ This section explains some fundamental concepts about JPlag that make it easier ### Directory submissions -JPlag will read submission directories recursively, so they can contain multiple (nested) source code files. +JPlag will read submission directories recursively, thus they can contain multiple (nested) source code files. ``` /path/to/root-directory @@ -155,7 +155,7 @@ JPlag will read submission directories recursively, so they can contain multiple โ””โ”€โ”€ Utils.java ``` -If you want JPlag to scan only one specific subdirectory of the submissions for source code files (e.g. `src`), can configure that with the argument `-S`: +If you want JPlag to scan only one specific subdirectory of the submissions for source code files (e.g., `src`), you can configure that with the argument `-s`: ``` /path/to/root-directory @@ -173,7 +173,7 @@ If you want JPlag to scan only one specific subdirectory of the submissions for ### Base Code -The base code is a special kind of submission. It is the template that all other submissions are based on. JPlag will ignore all matches between two submissions, where the matches are also part of the base code. Like any other submission, the base code has to be a single file or directory in the root directory. +The base code is a special kind of submission. It is the template that all other submissions are based on. JPlag will ignore all matches between two submissions where the matches are also part of the base code. Like any other submission, the base code has to be a single file or directory in the root directory. ``` /path/to/root-directory @@ -186,7 +186,7 @@ The base code is a special kind of submission. It is the template that all other โ””โ”€โ”€ Solution.java ``` -In this example, students have to solve a given problem by implementing the `run` method in the template below. Because they are not supposed to modify the `main` function, it will be identical for each student. +In this example, students must solve a problem by implementing the `run` method in the template below. Because they are not supposed to modify the `main` function, it will be identical for each student. ```java // BaseCode/Solution.java @@ -204,15 +204,15 @@ public class Solution { } ``` -To prevent JPlag from detecting similarities in the `main` function (and other parts of the template), we can instruct JPlag to ignore matches with the given base code by providing the `--bc=` option. +To prevent JPlag from detecting similarities in the `main` function (and other parts of the template), we can instruct JPlag to ignore matches with the given base code by providing the `-bc=` option. The `` in the example above is `BaseCode`. ### Multiple Root Directories -* You can run JPlag with multiple root directories, JPlag compares submissions from all of them +* You can run JPlag with multiple root directories; JPlag compares submissions from all of them * JPlag distinguishes between old and new root directories ** Submissions in new root directories are checked amongst themselves and against submissions from other root directories ** Submissions in old root directories are only checked against submissions from other new root directories -* You need at least one new root directory to run JPlag +* You need at least one (new) root directory to run JPlag This allows you to check submissions against those of previous years: ``` diff --git a/docs/2.-Supported-Languages.md b/docs/2.-Supported-Languages.md index c89bb8ee39..69bbcfc5a0 100644 --- a/docs/2.-Supported-Languages.md +++ b/docs/2.-Supported-Languages.md @@ -1,11 +1,11 @@ -JPlag currently supports Java, C, C++, C#, Go, Kotlin, Python, R, Rust, Scala, Swift, and Scheme. Additionally, it has primitive support for text and prototypical support for EMF metamodels. A detailed list, including the supported language versions can be found in the [project readme](https://github.com/jplag/JPlag/blob/main/README.md#supported-languages). +JPlag currently supports Java, C, C++, C#, Go, Kotlin, Python, R, Rust, Scala, Swift, Javascript, Typescript, LLVM IR and Scheme. Additionally, it has primitive support for text and prototypical support for EMF metamodels. A detailed list, including the supported language versions, can be found in the [project readme](https://github.com/jplag/JPlag/blob/main/README.md#supported-languages). -The language modules differ in their maturity due to their age and different usage frequencies. +The language modules differ in maturity due to their age and differing usage frequencies. Thus, each frontend has a state label: -- `mature`: This module is tried and tested, as well as up to date with a current language version. -- `beta`: This module is relatively new and up to date. However, it is not as well tested. **Feedback welcome!** -- `alpha`: This module is very new and not yet finished. Use with caution! +- `mature`: This module is tried and tested and is (somewhat) up to date with a current language version. +- `beta`: This module is relatively new and (somewhat) up to date. However, it has not been tested as well. **Feedback welcome!** +- `alpha`: This module is very new and has not yet been finished. Use with caution! - `legacy`: This module is from JPlag legacy (pre-v3.0.0) and may only support outdated language versions. It needs an update. -- `unknown`: It is very much unclear in which state this module is. +- `unknown`: It is very unclear which state this module is in. All language modules can be found [here](https://github.com/jplag/JPlag/tree/master/languages). diff --git a/docs/3.-Contributing-to-JPlag.md b/docs/3.-Contributing-to-JPlag.md index de0647876f..d60d334088 100644 --- a/docs/3.-Contributing-to-JPlag.md +++ b/docs/3.-Contributing-to-JPlag.md @@ -1,7 +1,7 @@ We're happy to incorporate all improvements to JPlag into this codebase. Feel free to fork the project and send pull requests. If you are new to JPlag, maybe check the [good first issues](https://github.com/jplag/jplag/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). -Please try to make well-documented and clear structured submissions: +Please try to make well-documented and clearly structured submissions: * All artifacts (code, comments...) should be in English * Please avoid abbreviations! * Make use of JavaDoc to document classes and public methods @@ -9,19 +9,20 @@ Please try to make well-documented and clear structured submissions: * Eclipse/IntelliJ users can use it directly * It can always be applied via maven with `mvn spotless:apply` * Use well-explained pull requests to propose your features -* When re-using code from other projects mark them accordingly and make sure their license allows the re-use +* When re-using code from other projects, mark them accordingly and make sure their license allows the re-use * Your changes should always improve the code quality of the codebase, especially when working on older components -* Your git messages should be concise but more importantly descriptive +* Your git messages should be concise but, more importantly, descriptive * Ensure your git history is clean, or else your PR may get squashed while merging +* When creating a PR, make sure to provide a detailed description of you changes and what purpose they serve ## Building from sources 1. Download or clone the code from this repository. 2. Run `mvn clean package` from the root of the repository to compile and build all submodules. - Run `mvn clean package assembly:single` instead if you need the full jar which includes all dependencies. + Run `mvn clean package assembly:single` instead if you need the full jar, which includes all dependencies. 5. You will find the generated JARs in the subdirectory `jplag.cli/target`. ### Git hooks -The repository contains a pre-commit hook, that prevents commits if fail spotless. +The repository contains a pre-commit hook that prevents commits if they fail spotless. To set up the hooks, call `git config --local core.hooksPath gitHooks/hooks` once within your local repository. diff --git a/docs/4.-Adding-New-Languages.md b/docs/4.-Adding-New-Languages.md index a4dbf71f00..1cfbb9563d 100644 --- a/docs/4.-Adding-New-Languages.md +++ b/docs/4.-Adding-New-Languages.md @@ -1,11 +1,11 @@ -# JPlag Frontend Design +# JPlag Language Module Design -To add support for a new language to JPlag, a JPlag frontend needs to be created for that specific language. The core purpose of a frontend is to transform each submission to a list of _Tokens_, an abstraction of the content of the submission files independent of the language of the submissions.
+To add support for a new language to JPlag, a JPlag language module needs to be created for that specific language. The core purpose of a language module is to transform each submission into a list of _Tokens_, an abstraction of the content of the submission files independent of the language of the submissions.
The token lists of the different submissions are then passed on to a comparison algorithm that checks the token lists for matching sequences. ## How are submissions represented? โ€” Notion of _Token_ -In the context of JPlag, a Token does not represent a lexical unit, as identifiers, keywords or operators. Instead, Tokens represent syntactic entities, like statements, or control structures. More than one token might be needed to represent the nested structure of a statement or expression in a linear token list. +In the context of JPlag, a Token does not represent a lexical unit, as identifiers, keywords or operators. Instead, Tokens represent syntactic entities, like statements or control structures. More than one token might be needed to represent the nested structure of a statement or expression in a linear token list. ```java class MyClass extends SuperClass { private String name; } @@ -18,12 +18,12 @@ Each comment is intended to represent one token. From this example in Java, you may be able to see the following things: - a class declaration is represented by three tokens of different _types_: `CLASS_DECLARATION`, `CLASS_BODY_BEGIN` and `CLASS_BODY_END` - a token is associated with a _position_ in a code file. - - the abstraction is incomplete, many details of the code are omitted. The original code cannot be reconstructed from the token list, but its structure can. + - the abstraction is incomplete, and many details of the code are omitted. The original code cannot be reconstructed from the token list, but its structure can. A few more points about Tokens in JPlag: - a token list contains the Tokens from _all files of one submission_. For that reason, Tokens save the _filename_ of their origin in addition to their position. - Token types are represented by the `TokenType` interface which has to be adapted for each language individually. - - For brevity, each token type is also associated with a String description, usually shorter than their name. Looking at the String representations used in existing frontends, you may recognize a kind of convention about how they are formed. The example above uses the full names of token types. + - For brevity, each token type is also associated with a String description, usually shorter than their name. Looking at the String representations used in existing language modules, you may recognize a kind of convention about how they are formed. The example above uses the full names of token types. ## How does the transformation work? @@ -31,7 +31,7 @@ Here is an outline of the transformation process. - each submitted file is _parsed_. The result is a set of ASTs for each submission. - each AST is now _traversed_ depth-first. The nodes of the AST represent the grammatical units of the language. - upon entering and exiting a node, Tokens can be created that match the type of the node. They are added to the current token list. - - for block-type nodes like bodies of classes or if expressions, the point of entry and exit correspond to the respective `BEGIN` and `END` token types. If done correctly, the token list should contain balanced pairs of matching `BEGIN` and `END` tokens. + - for block-type nodes like bodies of classes or if expressions, the point of entry and exit corresponds to the respective `BEGIN` and `END` token types. If done correctly, the token list should contain balanced pairs of matching `BEGIN` and `END` tokens. ```java @Override @@ -57,20 +57,20 @@ public void enterClassDeclaration(ClassBodyContext context) { addToken(token); } ``` -The way the traversal works and how you can interact with the process depends on the parser technology used. In the example above, **ANTLR-generated parsers** were used, as was in most of the current JPlag frontends. We recommend to use ANTLR for any new frontend. +The way the traversal works and how you can interact with the process depends on the parser technology used. In the example above, **ANTLR-generated parsers** were used, as was in most of the current JPlag language modules. We recommend using ANTLR for any new language module. If a hard-coded (as opposed to dynamically generated) parser library is available for your language, it may make sense to use it. An implementation of the visitor pattern for the resulting AST should be included. -# Frontend Structure +# Language Module Structure -A frontend consists of these parts: +A language module consists of these parts: | Component/Class | Superclass | Function | How to get there | |-----------------------------------------|---------------------------|--------------------------------------------------|-------------------------------------------------------------| -| Language class | `de.jplag.Language` | access point for the frontend | copy with small adjustments | +| Language class | `de.jplag.Language` | access point for the language module | copy with small adjustments | | `pom.xml` | - | Maven submodule descriptor | copy with small adjustments;
add dependencies for parser | -| `README.md` | - | documentation for the frontend | copy for consistent structure; adjust from there | -| TokenType class | `de.jplag.TokenType` | contains the language-specific token types | **implement new** | +| `README.md` | - | documentation for the language module | copy for consistent structure; adjust from there | +| TokenType class | `de.jplag.TokenType` | contains the language-specific token types | **implement new** | | | | | | Lexer and Parser | - | transform code into AST | depends on technology | | ParserAdapter class | `de.jplag.AbstractParser` | sets up Parser and calls Traverser | depends on technology | @@ -86,21 +86,21 @@ For example, if ANTLR is used, the setup is as follows: | TraverserListener class | `ParseTreeListener` (ANTLR) | creates tokens when called | **implement new** | | ParserAdapter class | `de.jplag.AbstractParser` | sets up Parser and calls Traverser | copy with small adjustments | -As the table shows, much of a frontend can be reused, especially when using ANTLR. The only parts left to implement specifically for each frontend are +As the table shows, much of a language module can be reused, especially when using ANTLR. The only parts left to implement specifically for each language module are - the ParserAdapter (for custom parsers) - the TokenTypes, and - the TraverserListener. **Note** for parser libraries other than ANTLR: - It should still be rather easy to implement the ParserAdapter from the library documentation. - - Instead of using a listener pattern, the library may require you to do the token extraction in a _Visitor subclass_. In that case, there is only one method call per element, called e.g. `traverseClassDeclaration`. The advantage of this version is that the traversal of the subtrees can be controlled freely. See the Scala frontend for an example. + - Instead of using a listener pattern, the library may require you to do the token extraction in a _Visitor subclass_. In that case, there is only one method call per element, called e.g. `traverseClassDeclaration`. The advantage of this version is that the traversal of the subtrees can be controlled freely. See the Scala language module for an example. ### Basic procedure outline ```mermaid flowchart LR JPlag -->|"parse(files)"| Language - subgraph frontend[LanguageFrontend] + subgraph module[LanguageModule] Language -->|"parse(files)"| ParserAdapter ParserAdapter -->|"parse(files)"| Parser -.->|ASTs| ParserAdapter ParserAdapter -->|"walk(ASTs)"| Traverser @@ -110,49 +110,37 @@ flowchart LR end ``` -Note: In existing frontends, the token list is managed by the ParserAdapter, and from there it is returned to the +Note: In existing language modules, the token list is managed by the ParserAdapter, and from there it is returned to the Language class and then to JPlag. ### Integration into JPlag -The following adjustments have to be made beyond creating the frontend submodule itself: +The following adjustments have to be made beyond creating the language module submodule itself: - Register the submodule in the aggregator POM for every build profile. ```xml - + ... - jplag.frontend.my-frontend + newlanguage/module> ... ``` -- Add a dependency from the aggregator module to the new frontend -- Add a dependency from the jplag module to the new frontend -```xml - - - - ... - - de.jplag - jplag.frontend.my-frontend - ${revision} - - ... - -``` +- Add a dependency for the new language module to the coverage-report module +- Add a dependency for the new language module to the cli module +- Update the documentation in the main readme file and the `docs` folder -That's it! The new frontend should now be usable as described in the main README. The name of the frontend used with the CLI `-l` option is the `IDENTIFIER` set in the Language class. +That's it! The new language module should now be usable as described in the main README. The name of the language module used with the CLI `-l` option is the `IDENTIFIER` set in the Language class. # Token Selection -Apart from extracting the tokens correctly, the task of deciding which syntactical elements should be assigned a token is the essential part when designing a frontend.
+Apart from extracting the tokens correctly, the task of deciding which syntactical elements should be assigned a token is an essential part of designing a language module.
This guideline is solely based on experience and intuition โ€“ this "worked well" so far. More research might hint towards a more systematic process of token selection. The goal of the abstraction is to create a token list that is - _accurate_: a fair representation of the code as input to the comparison algorithm - _consistent per se_: insensitive to small changes in the code that might obfuscate plagiarism; constructs are represented equally throughout the file - - _consistent_ with the output of other trusted frontendsโ€”only to the extent that their respective languages are comparable, naturally. + - _consistent_ with the output of other trusted language modules โ€” only to the extent that their respective languages are comparable, naturally. To create a set of tokens in line with these objectives, we offer the tips below. @@ -222,7 +210,7 @@ Note: If lists of the same type are nested, the end of the inner list may become # Token Extraction -The token extraction is the most time-consuming part of the frontend design. +The token extraction is the most time-consuming part of the language module design. How difficult it is is largely dependent on the underlying **grammar** of the parser. This article deals with the implementation of the listener which is called at every stage of traversal of the AST. The examples center around tokenizing the Java language, using a grammar written in ANTLR4. @@ -424,9 +412,9 @@ options.sensitivity.getValue(); You should pass the options to your parser if neccesary. -# Frontend Test +# Language Module Test -To check the output of your frontend against the input, the `TokenPrinter` can be helpful. The `TokenPrinter` prints the input line by line, and the tokens of each line below it. +To check the output of your language module against the input, the `TokenPrinter` can be helpful. The `TokenPrinter` prints the input line by line, and the tokens of each line below it. ```java 10 public class Example { @@ -447,11 +435,11 @@ To check the output of your frontend against the input, the `TokenPrinter` can b 15 } |}CLASS ``` -To test a frontend, set up a JUnit test class where the `TokenPrinter` prints the output of the `parse` method of the frontend. Read through the output and check whether the `List` satisfies the given requirements. +To test a language module, set up a JUnit test class where the `TokenPrinter` prints the output of the `parse` method of the language module. Read through the output and check whether the `List` satisfies the given requirements. ### Test files -The frontend should be tested with 'authentic' sample code as well as a 'complete' test file that covers all syntactic elements that the frontend should take into account. If you are using an ANTLR parser, such a complete test file may be included in the parser test files in the ANTLRv4 Grammar Repository. +The language module should be tested with 'authentic' sample code as well as a 'complete' test file that covers all syntactic elements that the language module should take into account. If you are using an ANTLR parser, such a complete test file may be included in the parser test files in the ANTLRv4 Grammar Repository. ### Sanity check suggestions @@ -461,7 +449,7 @@ The frontend should be tested with 'authentic' sample code as well as a 'complet - The token list represents the input code with an acceptable coverage โ€”how that can be measured and what coverage is acceptable depends on the language. One approach would be line coverage, e.g. 90 percent of code lines should contain a token. -- There are no `TokenTypes` that can never be produced by the frontend for any input. +- There are no `TokenTypes` that can never be produced by the language module for any input. - Put another way, the complete test code produces a token list that contains every type of token. ### Writing tests using the test api @@ -510,7 +498,32 @@ protected File getTestFileLocation() { } ``` +## Testing token positions + +The precise position of a token can be relevant for the visualization in the report viewer. To make sure the token positions are extracted correctly language modules should include some tests for that. + +Writing such tests can be done using a specific syntax in the test sources directly. +Such a file can look like this: +```java +>class Test { +> int test; +$ | J_VARDEF 8 +>} +``` + +Every line that is prefixed with '>' will be interpreted as a line of test source code. + +Every line starting with '$' contains information about one expected token. The token is expected in the first source line above this one. +The '|' marks the column the token should be in. It is followed by one space, then the name of the token (The name of the enum constant). +Finally, separated by another space is the length of the token. +A single file may contain any number of tokens. +The test will verify that at least one token with those exact properties exists. + +These test files have to be added in the TestDataCollector. Put all test files in a single directory and specify it through collector.addTokenPositionTests(""). +If the directory is in the default location for test files, a relative path is enough, otherwise a full path has to be specified. + + # Adding code highlighting to the report-viewer To ensure your language gets properly registered and its code is correctly highlighted in the report-viewer: -1) Add your language to the `ParserLanguage` enum in 'src/model/Language.ts'. As the value for the entry use its frontend name. -2) Add your language to the switch-case in `src/utils/CodeHighlighter.ts` and return the correct [highlight.js name](https://github.com/highlightjs/highlight.js/blob/main/SUPPORTED_LANGUAGES.md). If your language is not supported by default, also register the language here. \ No newline at end of file +1) Add your language to the `ParserLanguage` enum in 'src/model/Language.ts'. As the value for the entry use its language module name. +2) Add your language to the switch-case in `src/utils/CodeHighlighter.ts` and return the correct [highlight.js name](https://github.com/highlightjs/highlight.js/blob/main/SUPPORTED_LANGUAGES.md). If your language is not supported by default, also register the language here. diff --git a/docs/5.-End-to-End-Testing.md b/docs/5.-End-to-End-Testing.md index 35bfd882c5..be2cbd59da 100644 --- a/docs/5.-End-to-End-Testing.md +++ b/docs/5.-End-to-End-Testing.md @@ -1,433 +1 @@ -## Basics The basic structure of the end-to-end testing module is discussed in the [corresponding readme file](https://github.com/jplag/JPlag/blob/master/endtoend-testing/README.md). - -## Rationale behind the test data -To be able to create the test data, some examples from science, which have addressed the concealment of plagiarism, were used to ensure the greatest possible coverage of the JPlag functionality. -Here, the changes were split as finely as possible and applied to the levies. -The following elaborations were used for this: -- [Detecting source code plagiarism on introductory programming course assignments using a bytecode approach](https://ieeexplore.ieee.org/abstract/document/7910274) -- [Detecting Disguised Plagiarism](https://arxiv.org/abs/1711.02149) - -These elaborations provide basic ideas on how a modification of the plagiarized source code can look like or be adapted. -These code adaptations refer to a wide range of changes starting from -adding/removing comments to architectural changes in the deliverables. - -- (1) Inserting comments or empty lines -- (2) Changing variable names or function names -- (3) Insertion of unnecessary or changed code lines -- (4) Changing the program flow (statements and functions must be independent from each other) - - (1) Variable declaration at the beginning of the program - - (2) Combining declarations of variables - - (3) Reuse of the same variable for other functions -- (5) Changing control structures - - (1) for(...) to while(...) - - (2) if(...) to switch-case -- (6) Modification of expressions - - (1) (X < Y) to !(X >= Y) and ++x to x = x + 1 -- (7) Splitting and merging statements - - (1) x = getSomeValue(); y = x- z; to y = (getSomeValue() - Z; -- (8) Inserting unnecessary casts - -These changes were now transferred to a base class and thus the plagiarism was created. The named base class was provided with the individual changes. The numbers in the list shown above are intended for the traceability of the test data. Here the test data filenames were named with the respective changes. Example: SortAlgo4d1 contains the changes "Variable declaration at the beginning of the program". If several points are combined, this is separated by "_" e.g.: SortAlgo1_3 contains "(1) Inserting comments or empty lines" and "(3) Insertion of unnecessary or changed code lines". - -The following code examples show how these changes affect the program code and also how the detection of JPLag behaves. -All the code examples shown and more can be found at [testdata-resources-SortAlgo](https://github.com/jplag/JPlag/tree/main/endtoend-testing/src/test/resources/data/sortAlgo). - -### (1) Inserting comments or empty lines - -Adding empty lines or comments affects the normalization of the output. If the End-To-End tests fail with these changes, it means that something has changed in the normalization, e.g. removing empty lines or recognizing comments no longer works. - -In the following, the modified base class looks like this: - -Original: -``` java - - public void BubbleSortWithoutRecursion(Integer arr[]) { - for(int i = arr.length; i > 1 ; i--) { -``` - -Plagiarized: -``` java -/* - - Unnecessary comment - */ - public void BubbleSortWithoutRecursion(Integer arr[]) { - //Unnecessary comment - for(int i = arr.length; i > 1 ; i--) { -``` - -As expected, the resulting outputs have a match of 100% (JPLag result): - -``` json -"SortAlgo-SortAlgo1" : { - "minimal_similarity" : 100.0, - "maximum_similarity" : 100.0, - "matched_token_number" : 56 - }, -``` - -### (2) Changing variable names or function names - -Changing variable names and function names has, like point 1, also the goal of detecting adjustments in the normalization level. -If the End-To-End tests fail with these changes, it means that something has changed in the normalization, e.g. creating constants function and variable names. - -Orginal: - -``` java - private final void swap(T[] arr, int i, int j) { - T t = arr[i]; - arr[i] = arr[j]; - arr[j] = t; - } -``` - -Plagiarized: - -``` java - private final void paws(T[] otherArr, int i, int j) { - T t = otherArr[i]; - otherArr[i] = otherArr[j]; - otherArr[j] = t; - } -``` - -As expected, the resulting outputs have a match of 100% (JPLag result): - -``` json -"SortAlgo-SortAlgo2" : { - "minimal_similarity" : 100.0, - "maximum_similarity" : 100.0, - "matched_token_number" : 56 - }, -``` - -### (3) Insertion of unnecessary or changed code lines - -In contrast to points 1 and 2, adding unnecessary code lines reduces the token generation. This has the consequence that the recognition can no longer be 100% sure whether plagiarism is present or not. The failure of the end-to-end tests in these cases means that either the tokens have been adjusted, the normalization has changed the function separation or something has changed in the minimum token numbers. This can be easily seen by running the end-to-end tests in different options. this will be shown in the next result examples. - -Original: -``` java - private final void swap(T[] arr, int i, int j) { - T t = arr[i]; - arr[i] = arr[j]; - arr[j] = t; - } -``` -Plagiarized: -``` java -private final void swap(T[] arr, int i, int j) { - var tempVar1 = 0; - if (true) { - T t = arr[i]; - arr[i] = arr[j]; - arr[j] = t; - var tempVar2 = 0; - tempVar2++; - tempVar2 = tempVar2 + 1; - } - } -``` - -The results for the recognition already allow first recognition changes. Here the change of the `minimum_token_match` also has an effect on the result, which was not the case with (1) and (2). - -``` json -[{"options" : { - "minimum_token_match" : 1 - }, -"SortAlgo-SortAlgo3" : { - "minimal_similarity" : 81.159424, - "maximum_similarity" : 100.0, - "matched_token_number" : 56 - }, -}] -``` - -``` json -[{"options" : { - "minimum_token_match" : 15 - }, -"SortAlgo-SortAlgo3" : { - "minimal_similarity" : 57.971016, - "maximum_similarity" : 71.42857, - "matched_token_number" : 40 - }, -}] -``` - -### (4) Changing the program flow (statements and functions must be independent from each other) - -This subitem breaks down into three more change methods to maintain fine granularity: -- (1) Variable declaration at the beginning of the program -```java -public class SortAlgo4d1 { - private int firstCounter; - private int arrayLenght; - private int swapVarI; - private int swapVarJ; - -``` - -- (2) Combining declarations of variables -``` java -public class SortAlgo4d2 { - private int firstCounter,swapVarJ,arrayLenght ,swapVarI; -``` - -- (3) Reuse of the same variable for other functions -``` java -public class SortAlgo4d3 { - private int firstCounterAndArrayLenghtAndswapVarJ ,swapVarI; -``` - -The adjustments to the program flow with the previous instantiation of the variables were also made: -Original: -``` java - if (n == 1) - { - return; - } -``` - -Plagiarized: -``` java - firstCounter = n; - if (firstCounter == 1) { - return; - } -``` - -The results of the individual adjustment are as follows: -```json - "SortAlgo-SortAlgo4d1" : { - "minimal_similarity" : 87.30159, - "maximum_similarity" : 98.21429, - "matched_token_number" : 55 - }, - "SortAlgo-SortAlgo4d2" : { - "minimal_similarity" : 87.5, - "maximum_similarity" : 100.0, - "matched_token_number" : 56 - }, - "SortAlgo-SortAlgo4d3" : { - "minimal_similarity" : 90.32258, - "maximum_similarity" : 100.0, - "matched_token_number" : 56 - }, -``` - -### (5) Changing control structures - -The change of the control structure in the program also indicates a change of the token generation in case of faulty tests. In contrast to (4), however, these are specially designed for other tokens that are made for if, else, ... structures. - -These changes were made to the SortAlgo test data in a plagiarized form. - -Original: -``` java - public void BubbleSortRecursion(Integer arr[], int n) { - if (n == 1) - { - return; - } - - for (int i = 0; i < n - 1; i++) - { - if (arr[i] > arr[i + 1]) - { - swap(arr, i , i+1); - } - } - BubbleSortRecursion(arr, n - 1); - } -``` - -Plagiarized: -``` java - public void BubbleSortRecursion(Integer arr[], int n) { - switch (n) { - case 1: - return; - } - - int i = 0; - while(i < n-1) - { - var tempBool = arr[i] > arr[i + 1]; - if (tempBool) { - swap(arr, i, i + 1); - } - i++; - } - - BubbleSortRecursion(arr, n - 1); - } -``` - -Here it is remarkable which affects the adjustment of the `minimum_token_match` has on the recognition of the plagiarism. -Changes of the token generation as well as the `minimum_token_match` have an effect on this kind of End-To-End test. - -``` json - "options" : { - "minimum_token_match" : 1 - }, - "tests" : { - "SortAlgo-SortAlgo5" : { - "minimal_similarity" : 82.14286, - "maximum_similarity" : 82.14286, - "matched_token_number" : 46 - }, -``` - -``` json - "options" : { - "minimum_token_match" : 15 - }, - "tests" : { - "SortAlgo-SortAlgo5" : { - "minimal_similarity" : 0.0, - "maximum_similarity" : 0.0, - "matched_token_number" : 0 - }, -``` - -### (6) Modification of expressions -Changing the order of compare also changes the order of the program flow which is difficult to determine the exact effect of plagiarism. Here the statements (X < Y) to !(X >= Y) and ++x to x = x + 1 are changed. Since the syntax should be recognized however as expression, the pure change of the expression has little effect on their plagiarism recognition. - -Orginal: -``` java - public void BubbleSortRecursion(Integer arr[], int n) { - if (n == 1) - { - return; - } - - for (int i = 0; i < n - 1; i++) - { - if (arr[i] > arr[i + 1]) - { - swap(arr, i , i+1); - } - } - BubbleSortRecursion(arr, n - 1); - } -``` - -Plagiarized: -``` java -public void BubbleSortRecursion(Integer arr[], int n) { - if (n != 1) - { - for (int i = 0; !(i >= (n - 1));) - { - if (!(arr[i] <= arr[i + 1])) - { - swap(arr, i , i+1); - } - i = i + 1; - } - BubbleSortRecursion(arr, n - 1); - } - else - { - return; - } - } -``` - -Results: -``` json - { - "options" : { - "minimum_token_match" : 1 - }, - "SortAlgo-SortAlgo6" : { - "minimal_similarity" : 83.58209, - "maximum_similarity" : 100.0, - "matched_token_number" : 56 - }, -``` - -``` json - "options" : { - "minimum_token_match" : 15 - }, - "SortAlgo-SortAlgo6" : { - "minimal_similarity" : 43.28358, - "maximum_similarity" : 51.785713, - "matched_token_number" : 29 - }, -``` - -### (7) Splitting and merging statements -The merging or splitting of statements results in changing the token for the respective plagiarism detection. -Here code lines are either fetched from functions or stored in functions like `x = getSomeValue(); y = x- z;` to `y = (getSomeValue() - Z`. - -Original: -``` java -[...] - swap(arr, i , i+1); -[...] - if (arr[innerCounter] > arr[innerCounter + 1]) { -[...] -``` - -Plagiarized: -``` java -[...] - swap(arr, i, add(i , 1)); -[...] - if (arr[innerCounter] > arr[add(innerCounter , 1)]) { -[...] - private int add(int value1, int value2) - { - return value1 + value2; - } - - private int subtract(int value1, int value2) - { - return value1 - value2; - } -``` - -Results: -``` json - "options" : { - "minimum_token_match" : 1 - }, - "tests" : { - "SortAlgo-SortAlgo7" : { - "minimal_similarity" : 76.712326, - "maximum_similarity" : 100.0, - "matched_token_number" : 56 - }, -``` - -``` json - "options" : { - "minimum_token_match" : 15 - }, - "tests" : { - "SortAlgo-SortAlgo7" : { - "minimal_similarity" : 49.315067, - "maximum_similarity" : 64.28571, - "matched_token_number" : 36 - }, -``` - -## Summary - -The results and the test coverage of the end-to-end tests strongly depend on the tested plagiarisms. It is also important to use and test many different options of the API offered by JPlag, as these have a direct influence on the detection and are therefore also important for the change detection. - -To summarize -- (1) and (2) test normalization level -- (3) to (7) the token generation level - -If a result differs only in the options, it is very likely that the change is in the configuration of the `minimum_token_match`. -This means that if Option1 does not change in the result of the detection, but the result in Option2 does, this is the basis of the `minimum_token_match`. -``` -java: (1)SortAlgo-SortAlgo5 --> passed -java: (15)SortAlgo-SortAlgo5 --> failed -``` - - - diff --git a/docs/6.-Report-File-Generation.md b/docs/6.-Report-File-Generation.md index 406ce95cc9..3753993d6d 100644 --- a/docs/6.-Report-File-Generation.md +++ b/docs/6.-Report-File-Generation.md @@ -62,7 +62,7 @@ The `overview.json` contains a map that associates a submission id to its displa For internal use in the report viewer use only(!) the submission id. Whenever the name of a submission has to be displayed in the report viewer, the id has to be resolved to its display name first. The report viewer's vuex store provides a getter for this resolution. ### JPlag -At the beginning of report generation a map and a function that associate a JPlag `Submission` to a submission id is built. Whenever you reference a submission in a report viewer DTO use this map/function to resolve the submission to its id. +At the beginning of report generation a map and a function that associates a JPlag `Submission` to a submission id is built. Whenever you reference a submission in a report viewer DTO use this map/function to resolve the submission to its id. ## Adding and displaying new attributes from JPlagResult diff --git a/docs/7.-Clustering-of-Submissions.md b/docs/7.-Clustering-of-Submissions.md index be1aee66a5..0875cf0dc6 100644 --- a/docs/7.-Clustering-of-Submissions.md +++ b/docs/7.-Clustering-of-Submissions.md @@ -1,6 +1,6 @@ ## Clustering Usage -By default, JPlag is configured to perform a clustering of the submissions. +By default, JPlag is configured to cluster the submissions. The clustering partitions the set of submissions into groups of similar submissions. The found clusters can be used candidates for _potentially_ colluding groups. Each cluster has a strength score, that measures how _suspicious_ the cluster is compared to other clusters. @@ -20,18 +20,18 @@ __The clustering is designed to work out-of-the-box for running within the magni | General | Enable | Controls whether the clustering is run at all. | `true` | | General | Algorithm | Which clustering algorithm to use.
Agglomerative Clustering
Agglomerative Clustering iteratively merges similar submissions bottom up. It usually requires manual tuning for its parameters to yield helpful clusters.
Spectral Clustering
Spectral Clustering is combined with Bayesian Optimization to execute the k-Means clustering algorithm multiple times, hopefully finding a \"good\" clustering automatically. Its default parameters should work O.K. in most cases.
| Agglomerative Clustering | | General | Metric | The similarity score between submissions to use during clustering. Each score is expressed in terms of the size of the submissions `A` and `B` and the size of their matched intersection `A โˆฉ B`.
AVG (aka. Dice's coefficient)
`AVG = 2 * (A โˆฉ B) / (A + B)`
MAX (aka. overlap coefficient)
`MAX = (A โˆฉ B) / min(A, B)` Compared to MAX, this prevents obfuscation when a collaborator bloats his submission with unrelated code.
MIN (_deprecated_)
`MIN = (A โˆฉ B) / max(A, B)`
INTERSECTION (_experimental_)
`INTERSECTION = A โˆฉ B`
| AVG | -| Spectral | Bandwidth | For Spectral Clustering, Baysian Optimization is used to determine a fitting number of clusters. If a good clustering result is found during the search, numbers of clusters that differ by something in range of the bandwidth are also expected to good. Low values result in more exploration of the search space, high values in more exploitation of known results. | 20.0 | +| Spectral | Bandwidth | For Spectral Clustering, Bayesian Optimization determines a fitting number of clusters. If a good clustering result is found during the search, numbers of clusters that differ by something in range of the bandwidth are also expected to good. Low values result in more search space exploration, and high values result in more exploitation of known results. | 20.0 | | Spectral | Noise | The result of each k-Means run in the search for good clusterings is random. The noise level models the variance in the \"worth\" of these results. It also acts as a regularization constant. | 0.0025 | -| Spectral | Min-Runs | Minimum number of k-Means executions for spectral clustering. With these initial runs clustering sizes are explored. | 5 | -| Spectral | Max-Runs | Maximum number of k-Means executions during spectral clustering. Any execution after the initial (min-) runs tries to balance between exploration of unknown clustering sizes and exploitation of clustering sizes known as good. | 50 | +| Spectral | Min-Runs | Minimum number of k-Means executions for spectral clustering. With these initial runs, clustering sizes are explored. | 5 | +| Spectral | Max-Runs | Maximum number of k-Means executions during spectral clustering. Any execution after the initial (min-) runs tries to balance between exploring unknown clustering sizes and exploiting clustering sizes known as good. | 50 | | Spectral | K-Means Iterations | Maximum number of iterations during each execution of the k-Means algorithm. | 200 | -| Agglomerative | Threshold | Only clusters with an inter-cluster-similarity greater than this threshold are merged during agglomerative clustering. | 0.2 | +| Agglomerative | Threshold | Only clusters with an inter-cluster similarity greater than this threshold are merged during agglomerative clustering. | 0.2 | | Agglomerative | inter-cluster-similarity | How to measure the similarity of two clusters during agglomerative clustering.
MIN (aka. complete-linkage)
Clusters are merged if all their submissions are similar.
MAX (aka. single-linkage)
Clusters are merged if there is a similar submission in both.
AVERAGE (aka. average-linkage)
Clusters are merged if their submissions are similar on average.
| AVERAGE | -| Preprocessing | Pre-Processor | How the similarities are preprocessed prior to clustering. Spectral Clustering will probably not have good results without it.
None
No preprocessing.
Cumulative Distribution Function (CDF)
Before clustering, the value of the cumulative distribution function of all similarities is estimated. The similarities are multiplied with these estimates. This has the effect of suppressing similarities that are low compared to other similarities.
Percentile
Any similarity smaller than the given percentile will be suppressed during clustering.
Threshold
Any similarity smaller than the given threshold will be suppressed during clustering.
| CDF | +| Preprocessing | Pre-Processor | How the similarities are preprocessed before clustering. Spectral Clustering will probably not have good results without it.
None
No preprocessing.
Cumulative Distribution Function (CDF)
Before clustering, the value of the cumulative distribution function of all similarities is estimated. The similarities are multiplied by these estimates. This has the effect of suppressing low similarities compared to other similarities.
Percentile
Any similarity smaller than the given percentile will be suppressed during clustering.
Threshold
Any similarity smaller than the given threshold will be suppressed during clustering.
| CDF | ## Clustering Architecture -All clustering related classes are contained within the `de.jplag.clustering(.*)` packages in the core project. +All clustering-related classes are contained within the core project's `de.jplag.clustering(.*)` packages. The central idea behind the structure of clustering is the ease of use: To use the clustering calling code should only ever interact with the `ClusteringOptions`, `ClusteringFactory`, and `ClusteringResult` classes: @@ -85,7 +85,7 @@ New clustering algorithms and preprocessors can be implemented using the `Generi ### Integration Tests -There are integration tests for the Spectral Clustering to verify, that a least in the case of two known sets of similarities the groups known to be colluders are found. However, these are considered to be sensitive data. The datasets are not available to the public and these tests can only be run by maintainers with access. +There are integration tests for the Spectral Clustering to verify that, at least in the case of two known sets of similarities, the groups known to be colluders are found. However, these are considered to be sensitive data. The datasets are not available to the public and these tests can only be run by maintainers with access. To run these tests the contents of the [PseudonymizedReports](https://github.com/jplag/PseudonymizedReports) repository must added in the folder `jplag/src/test/resources/de/jplag/PseudonymizedReports`. diff --git a/docs/Home.md b/docs/Home.md index d6388bd543..94cedf59c1 100644 --- a/docs/Home.md +++ b/docs/Home.md @@ -3,9 +3,9 @@

## What is JPlag -JPlag finds pairwise similarities among a set of multiple programs. It can reliably detect software plagiarism and collusion in software development. All similarities are calculated locally, and no source code or plagiarism results are ever uploaded to the internet. JPlag supports a large number of programming and modeling languages. JPlag does not merely compare bytes of text but is aware of programming language syntax and program structure and hence is robust against many kinds of attempts to disguise similarities (_obfusction_) between plagiarized files. +JPlag finds pairwise similarities among a set of multiple programs. It can reliably detect software plagiarism and collusion in software development. All similarities are calculated locally; no source code or plagiarism results are ever uploaded online. JPlag supports a large number of programming and modeling languages. JPlag does not merely compare bytes of text but is aware of programming language syntax and program structure and hence is robust against many kinds of attempts to disguise similarities (_obfusction_) between plagiarized files. -JPlag is typically used to detect and thus discourage the unallowed copying of student exercise programs in programming education. But in principle, it can also be used to detect stolen software parts among large amounts of source text or modules that have been duplicated (and only slightly modified). JPlag has already played a part in several intellectual property cases where it has been successfully used by expert witnesses. +JPlag is typically used to detect and thus discourage the unallowed copying of student exercise programs in programming education. However, in principle, it can also detect stolen software parts among large amounts of source text or modules that have been duplicated (and only slightly modified). JPlag has already played a part in several intellectual property cases where expert witnesses have successfully used it. **Just to make it clear**: JPlag does not compare to the internet! It is designed to find similarities among the student solutions, which is usually sufficient for computer programs. @@ -16,7 +16,7 @@ JPlag is typically used to detect and thus discourage the unallowed copying of s * ๐Ÿคฉ [Give us Feedback in a **short (<5 min) survey**](https://docs.google.com/forms/d/e/1FAIpQLSckqUlXhIlJ-H2jtu2VmGf_mJt4hcnHXaDlwhpUL3XG1I8UYw/viewform?usp=sf_link) ## History -Originally, JPlag was developed in 1996 by Guido Mahlpohl and others at the chair of Prof. Walter Tichy at Karlsruhe Institute of Technology (KIT). It was first documented in a [Tech Report](https://publikationen.bibliothek.kit.edu/542000) in 2000 and later more formally in the [Journal of Universal Computer Science](http://www.ipd.kit.edu/tichy/uploads/publikationen/16/finding_plagiarisms_among_a_set_of_progr_638847.pdf). Since 2015 JPlag is hosted here on GitHub. After 25 years of its creation, JPlag is still used frequently in many universities in different countries around the world. +Originally, JPlag was developed in 1996 by Guido Mahlpohl and others at the chair of Prof. Walter Tichy at Karlsruhe Institute of Technology (KIT). It was first documented in a [Tech Report](https://publikationen.bibliothek.kit.edu/542000) in 2000 and later more formally in the [Journal of Universal Computer Science](http://www.ipd.kit.edu/tichy/uploads/publikationen/16/finding_plagiarisms_among_a_set_of_progr_638847.pdf). Since 2015 JPlag is hosted here on GitHub. After 25 years of its creation, JPlag is still used frequently in many universities in different countries worldwide. ## Download JPlag Download the latest version of JPlag [here](https://github.com/jplag/jplag/releases). If you encounter bugs or other issues, please report them [here](https://github.com/jplag/jplag/issues). diff --git a/language-testutils/src/test/java/de/jplag/testutils/LanguageModuleTest.java b/language-testutils/src/test/java/de/jplag/testutils/LanguageModuleTest.java index 36d997f7be..f659ba2b22 100644 --- a/language-testutils/src/test/java/de/jplag/testutils/LanguageModuleTest.java +++ b/language-testutils/src/test/java/de/jplag/testutils/LanguageModuleTest.java @@ -14,6 +14,7 @@ import java.util.Collection; import java.util.List; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; @@ -32,6 +33,7 @@ import de.jplag.testutils.datacollector.TestData; import de.jplag.testutils.datacollector.TestDataCollector; import de.jplag.testutils.datacollector.TestSourceIgnoredLinesCollector; +import de.jplag.testutils.datacollector.TokenPositionTestData; /** * Base class for language module tests. Automatically adds all common tests types for jplag languages. @@ -174,7 +176,7 @@ final List testTokensContainedData() { final void testTokenSequence(TestDataCollector.TokenListTest test) throws ParsingException, IOException { List actual = extractTokenTypes(test.data()); List expected = new ArrayList<>(test.tokens()); - if (expected.get(expected.size() - 1) != SharedTokenType.FILE_END) { + if (expected.getLast() != SharedTokenType.FILE_END) { expected.add(SharedTokenType.FILE_END); } assertTokensMatch(expected, actual, "Extracted token from " + test.data().describeTestSource() + " does not match expected sequence."); @@ -196,6 +198,45 @@ final List testTokenSequenceData() { return ignoreEmptyTestType(this.collector.getTokenSequenceTest()); } + /** + * Tests if the tokens specified for the token position tests are present in the sources + * @param testData The specifications of the expected tokens and the test source + * @throws ParsingException If the parsing fails + * @throws IOException If IO operations fail. If this happens, that should be unrelated to the test itself. + */ + @ParameterizedTest + @MethodSource("getTokenPositionTestData") + @DisplayName("Tests if the extracted tokens contain the tokens specified in the test files.") + final void testTokenPositions(TokenPositionTestData testData) throws ParsingException, IOException { + List extractedTokens = parseTokens(testData); + List failedTokens = new ArrayList<>(); + + for (TokenPositionTestData.TokenData expectedToken : testData.getExpectedTokens()) { + TokenType expectedType = this.languageTokens.stream().filter(type -> type.toString().equals(expectedToken.typeName())).findFirst() + .orElseThrow(() -> new IOException(String.format("The token type %s does not exist.", expectedToken.typeName()))); + + if (extractedTokens.stream().noneMatch(token -> token.getType() == expectedType && token.getLine() == expectedToken.lineNumber() + && token.getColumn() == expectedToken.columnNumber() && token.getLength() == expectedToken.length())) { + failedTokens.add(expectedToken); + } + } + + if (!failedTokens.isEmpty()) { + String failureDescriptors = String.join(System.lineSeparator(), + failedTokens.stream().map( + token -> token.typeName() + " at (" + token.lineNumber() + ":" + token.columnNumber() + ") with length " + token.length()) + .toList()); + fail("Some tokens weren't extracted with the correct properties:" + System.lineSeparator() + failureDescriptors); + } + } + + /** + * @return All token positions tests that are configured + */ + final List getTokenPositionTestData() { + return ignoreEmptyTestType(this.collector.getTokenPositionTestData()); + } + /** * Tests all configured test sources for a monotone order of tokens * @param data The test source @@ -231,8 +272,7 @@ final void testMonotoneTokenOrder(TestData data) throws ParsingException, IOExce final void testTokenSequencesEndsWithFileEnd(TestData data) throws ParsingException, IOException { List tokens = parseTokens(data); - assertEquals(SharedTokenType.FILE_END, tokens.get(tokens.size() - 1).getType(), - "Last token in " + data.describeTestSource() + " is not file end."); + assertEquals(SharedTokenType.FILE_END, tokens.getLast().getType(), "Last token in " + data.describeTestSource() + " is not file end."); } /** @@ -251,6 +291,11 @@ final void collectTestData() { collectTestData(this.collector); } + @AfterAll + final void deleteTemporaryFiles() { + TemporaryFileHolder.deleteTemporaryFiles(); + } + private List parseTokens(TestData source) throws ParsingException, IOException { List tokens = source.parseTokens(this.language); logger.info(TokenPrinter.printTokens(tokens)); diff --git a/language-testutils/src/test/java/de/jplag/testutils/TemporaryFileHolder.java b/language-testutils/src/test/java/de/jplag/testutils/TemporaryFileHolder.java new file mode 100644 index 0000000000..98a95fb7fb --- /dev/null +++ b/language-testutils/src/test/java/de/jplag/testutils/TemporaryFileHolder.java @@ -0,0 +1,20 @@ +package de.jplag.testutils; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +/** + * Stores all temporary files that are created for a {@link LanguageModuleTest} and provides the option to delete them + */ +public class TemporaryFileHolder { + public static List temporaryFiles = new ArrayList<>(); + + /** + * Deletes all temporary files that have been created up to this point + */ + public static void deleteTemporaryFiles() { + temporaryFiles.forEach(File::delete); + temporaryFiles.clear(); + } +} diff --git a/language-testutils/src/test/java/de/jplag/testutils/datacollector/InlineTestData.java b/language-testutils/src/test/java/de/jplag/testutils/datacollector/InlineTestData.java index 8d93f7e156..6a87110234 100644 --- a/language-testutils/src/test/java/de/jplag/testutils/datacollector/InlineTestData.java +++ b/language-testutils/src/test/java/de/jplag/testutils/datacollector/InlineTestData.java @@ -8,6 +8,7 @@ import de.jplag.Language; import de.jplag.ParsingException; import de.jplag.Token; +import de.jplag.testutils.TemporaryFileHolder; import de.jplag.util.FileUtils; /** @@ -25,7 +26,7 @@ public List parseTokens(Language language) throws ParsingException, IOExc File file = File.createTempFile("testSource", language.suffixes()[0]); FileUtils.write(file, this.testData); List tokens = language.parse(Collections.singleton(file)); - file.delete(); + TemporaryFileHolder.temporaryFiles.add(file); return tokens; } diff --git a/language-testutils/src/test/java/de/jplag/testutils/datacollector/TestDataCollector.java b/language-testutils/src/test/java/de/jplag/testutils/datacollector/TestDataCollector.java index d5a929d06f..9b45e8d8b6 100644 --- a/language-testutils/src/test/java/de/jplag/testutils/datacollector/TestDataCollector.java +++ b/language-testutils/src/test/java/de/jplag/testutils/datacollector/TestDataCollector.java @@ -1,10 +1,13 @@ package de.jplag.testutils.datacollector; import java.io.File; +import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -18,6 +21,7 @@ public class TestDataCollector { private final List tokenCoverageData; private final List containedTokenData; private final List tokenSequenceTest; + private final List tokenPositionTestData; private final List allTestData; @@ -34,6 +38,7 @@ public TestDataCollector(File testFileLocation) { this.tokenCoverageData = new ArrayList<>(); this.containedTokenData = new ArrayList<>(); this.tokenSequenceTest = new ArrayList<>(); + this.tokenPositionTestData = new ArrayList<>(); this.allTestData = new ArrayList<>(); } @@ -73,6 +78,28 @@ public TestDataContext inlineSource(String... sources) { return new TestDataContext(data); } + /** + * Adds all files from the given directory for token position tests. The sources can still be used for other tests, + * using the returned {@link TestDataContext} + * @param directoryName The name of the directory containing the token position tests. + * @return The context containing the added sources + * @throws IOException If the files cannot be read + */ + public TestDataContext addTokenPositionTests(String directoryName) { + File directory = new File(this.testFileLocation, directoryName); + Set allTestsInDirectory = new HashSet<>(); + for (File file : Objects.requireNonNull(directory.listFiles())) { + try { + TokenPositionTestData data = new TokenPositionTestData(file); + allTestsInDirectory.add(data); + this.tokenPositionTestData.add(data); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + return new TestDataContext(allTestsInDirectory); + } + /** * @return The test data that should be checked for source coverage */ @@ -101,6 +128,10 @@ public List getTokenSequenceTest() { return Collections.unmodifiableList(tokenSequenceTest); } + public List getTokenPositionTestData() { + return Collections.unmodifiableList(this.tokenPositionTestData); + } + /** * @return The list of all test data */ diff --git a/language-testutils/src/test/java/de/jplag/testutils/datacollector/TokenPositionTestData.java b/language-testutils/src/test/java/de/jplag/testutils/datacollector/TokenPositionTestData.java new file mode 100644 index 0000000000..d46288041a --- /dev/null +++ b/language-testutils/src/test/java/de/jplag/testutils/datacollector/TokenPositionTestData.java @@ -0,0 +1,92 @@ +package de.jplag.testutils.datacollector; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import de.jplag.Language; +import de.jplag.ParsingException; +import de.jplag.Token; +import de.jplag.testutils.TemporaryFileHolder; +import de.jplag.util.FileUtils; + +/** + * Test sources with token information Reads token position test specifications form a file and provides the token + * information for tests. The sources can be used as regular test sources. + */ +public class TokenPositionTestData implements TestData { + private final List sourceLines; + private final List expectedTokens; + + private final String descriptor; + + /** + * @param testFile The file containing the test specifications + * @throws IOException If the file cannot be read + */ + public TokenPositionTestData(File testFile) throws IOException { + this.sourceLines = new ArrayList<>(); + this.expectedTokens = new ArrayList<>(); + this.descriptor = "(Token position file: " + testFile.getName() + ")"; + this.readFile(testFile); + } + + private void readFile(File testFile) throws IOException { + List testFileLines = FileUtils.readFileContent(testFile).lines().toList(); + int currentLine = 0; + + for (String sourceLine : testFileLines) { + if (sourceLine.charAt(0) == '>') { + this.sourceLines.add(sourceLine.substring(1)); + currentLine++; + } + + if (sourceLine.charAt(0) == '$') { + int column = sourceLine.indexOf('|'); + String[] tokenDescriptionParts = sourceLine.split(" ", 0); + + String typeName = tokenDescriptionParts[tokenDescriptionParts.length - 2]; + int length = Integer.parseInt(tokenDescriptionParts[tokenDescriptionParts.length - 1]); + this.expectedTokens.add(new TokenData(typeName, currentLine, column, length)); + } + } + } + + @Override + public List parseTokens(Language language) throws ParsingException, IOException { + File file = File.createTempFile("testSource", language.suffixes()[0]); + FileUtils.write(file, String.join(System.lineSeparator(), sourceLines)); + List tokens = language.parse(Collections.singleton(file)); + TemporaryFileHolder.temporaryFiles.add(file); + return tokens; + } + + @Override + public String[] getSourceLines() { + return this.sourceLines.toArray(new String[0]); + } + + @Override + public String describeTestSource() { + return this.descriptor; + } + + /** + * @return A list of the expected tokens for this test source + */ + public List getExpectedTokens() { + return expectedTokens; + } + + /** + * Information about a single token + * @param typeName The name of the token type + * @param lineNumber The line the token is in (1 based) + * @param columnNumber The column the token is in (1 based) + * @param length The length of the token + */ + public record TokenData(String typeName, int lineNumber, int columnNumber, int length) { + } +} diff --git a/languages/c/src/main/java/de/jplag/c/CLanguage.java b/languages/c/src/main/java/de/jplag/c/CLanguage.java index ba99dbf498..c55dbf60b3 100644 --- a/languages/c/src/main/java/de/jplag/c/CLanguage.java +++ b/languages/c/src/main/java/de/jplag/c/CLanguage.java @@ -12,6 +12,7 @@ @MetaInfServices(de.jplag.Language.class) public class CLanguage implements Language { + private static final String NAME = "C"; private static final String IDENTIFIER = "c"; private final Scanner scanner; // c code is scanned not parsed @@ -27,7 +28,7 @@ public String[] suffixes() { @Override public String getName() { - return "C Scanner"; + return NAME; } @Override diff --git a/languages/cpp/src/main/java/de/jplag/cpp/CPPLanguage.java b/languages/cpp/src/main/java/de/jplag/cpp/CPPLanguage.java index c08e53dce1..b76dfc823e 100644 --- a/languages/cpp/src/main/java/de/jplag/cpp/CPPLanguage.java +++ b/languages/cpp/src/main/java/de/jplag/cpp/CPPLanguage.java @@ -10,6 +10,7 @@ */ @MetaInfServices(Language.class) public class CPPLanguage extends AbstractAntlrLanguage { + private static final String NAME = "C++"; private static final String IDENTIFIER = "cpp"; public CPPLanguage() { @@ -23,7 +24,7 @@ public String[] suffixes() { @Override public String getName() { - return "C++ Parser"; + return NAME; } @Override diff --git a/languages/csharp/src/main/java/de/jplag/csharp/CSharpLanguage.java b/languages/csharp/src/main/java/de/jplag/csharp/CSharpLanguage.java index aeeb53728f..67d24c60aa 100644 --- a/languages/csharp/src/main/java/de/jplag/csharp/CSharpLanguage.java +++ b/languages/csharp/src/main/java/de/jplag/csharp/CSharpLanguage.java @@ -9,7 +9,7 @@ */ @MetaInfServices(de.jplag.Language.class) public class CSharpLanguage extends AbstractAntlrLanguage { - private static final String NAME = "C# 6 Parser"; + private static final String NAME = "C#"; private static final String IDENTIFIER = "csharp"; private static final String[] FILE_ENDINGS = new String[] {".cs", ".CS"}; private static final int DEFAULT_MIN_TOKEN_MATCH = 8; diff --git a/languages/golang/src/main/java/de/jplag/golang/GoLanguage.java b/languages/golang/src/main/java/de/jplag/golang/GoLanguage.java index e14926b43e..cb2d08eace 100644 --- a/languages/golang/src/main/java/de/jplag/golang/GoLanguage.java +++ b/languages/golang/src/main/java/de/jplag/golang/GoLanguage.java @@ -6,7 +6,7 @@ @MetaInfServices(de.jplag.Language.class) public class GoLanguage extends AbstractAntlrLanguage { - private static final String NAME = "Go Parser"; + private static final String NAME = "Go"; private static final String IDENTIFIER = "go"; private static final int DEFAULT_MIN_TOKEN_MATCH = 8; private static final String[] FILE_EXTENSIONS = {".go"}; diff --git a/languages/java/src/main/java/de/jplag/java/JavaLanguage.java b/languages/java/src/main/java/de/jplag/java/JavaLanguage.java index a941dda67f..79d32f502c 100644 --- a/languages/java/src/main/java/de/jplag/java/JavaLanguage.java +++ b/languages/java/src/main/java/de/jplag/java/JavaLanguage.java @@ -14,6 +14,7 @@ */ @MetaInfServices(de.jplag.Language.class) public class JavaLanguage implements de.jplag.Language { + private static final String NAME = "Java"; private static final String IDENTIFIER = "java"; private final Parser parser; @@ -29,7 +30,7 @@ public String[] suffixes() { @Override public String getName() { - return "Javac based AST plugin"; + return NAME; } @Override diff --git a/languages/java/src/test/java/de/jplag/java/JavaLanguageTest.java b/languages/java/src/test/java/de/jplag/java/JavaLanguageTest.java index e54955781a..a4f89d9bb0 100644 --- a/languages/java/src/test/java/de/jplag/java/JavaLanguageTest.java +++ b/languages/java/src/test/java/de/jplag/java/JavaLanguageTest.java @@ -73,6 +73,8 @@ protected void collectTestData(TestDataCollector collector) { collector.testFile("AnonymousVariables.java").testTokenSequence(J_CLASS_BEGIN, J_METHOD_BEGIN, J_VARDEF, J_IF_BEGIN, J_IF_END, J_METHOD_END, J_CLASS_END); + + collector.addTokenPositionTests("tokenPositions"); } @Override diff --git a/languages/java/src/test/resources/de/jplag/java/tokenPositions/VarDef_1.java b/languages/java/src/test/resources/de/jplag/java/tokenPositions/VarDef_1.java new file mode 100644 index 0000000000..276f17351b --- /dev/null +++ b/languages/java/src/test/resources/de/jplag/java/tokenPositions/VarDef_1.java @@ -0,0 +1,4 @@ +>class Test { +> int test; +$ | J_VARDEF 8 +>} diff --git a/languages/kotlin/src/main/java/de/jplag/kotlin/KotlinLanguage.java b/languages/kotlin/src/main/java/de/jplag/kotlin/KotlinLanguage.java index 487effaa24..9b7678681e 100644 --- a/languages/kotlin/src/main/java/de/jplag/kotlin/KotlinLanguage.java +++ b/languages/kotlin/src/main/java/de/jplag/kotlin/KotlinLanguage.java @@ -10,7 +10,7 @@ @MetaInfServices(de.jplag.Language.class) public class KotlinLanguage extends AbstractAntlrLanguage { - private static final String NAME = "Kotlin Parser"; + private static final String NAME = "Kotlin"; private static final String IDENTIFIER = "kotlin"; private static final int DEFAULT_MIN_TOKEN_MATCH = 8; private static final String[] FILE_EXTENSIONS = {".kt"}; diff --git a/languages/llvmir/src/main/java/de/jplag/llvmir/LLVMIRLanguage.java b/languages/llvmir/src/main/java/de/jplag/llvmir/LLVMIRLanguage.java index 846a047e68..0d9ad7f150 100644 --- a/languages/llvmir/src/main/java/de/jplag/llvmir/LLVMIRLanguage.java +++ b/languages/llvmir/src/main/java/de/jplag/llvmir/LLVMIRLanguage.java @@ -11,7 +11,7 @@ @MetaInfServices(Language.class) public class LLVMIRLanguage extends AbstractAntlrLanguage { - private static final String NAME = "LLVMIR Parser"; + private static final String NAME = "LLVM IR"; private static final String IDENTIFIER = "llvmir"; private static final int DEFAULT_MIN_TOKEN_MATCH = 70; private static final String[] FILE_EXTENSIONS = {".ll"}; diff --git a/languages/python-3/src/main/java/de/jplag/python3/PythonLanguage.java b/languages/python-3/src/main/java/de/jplag/python3/PythonLanguage.java index 3df6587284..2a70481276 100644 --- a/languages/python-3/src/main/java/de/jplag/python3/PythonLanguage.java +++ b/languages/python-3/src/main/java/de/jplag/python3/PythonLanguage.java @@ -6,7 +6,7 @@ @MetaInfServices(de.jplag.Language.class) public class PythonLanguage extends AbstractAntlrLanguage { - + private static final String NAME = "Python"; private static final String IDENTIFIER = "python3"; public PythonLanguage() { @@ -20,7 +20,7 @@ public String[] suffixes() { @Override public String getName() { - return "Python3 Parser"; + return NAME; } @Override diff --git a/languages/rlang/src/main/java/de/jplag/rlang/RLanguage.java b/languages/rlang/src/main/java/de/jplag/rlang/RLanguage.java index d09e23b722..98b0171bf9 100644 --- a/languages/rlang/src/main/java/de/jplag/rlang/RLanguage.java +++ b/languages/rlang/src/main/java/de/jplag/rlang/RLanguage.java @@ -15,7 +15,7 @@ @MetaInfServices(de.jplag.Language.class) public class RLanguage implements de.jplag.Language { - private static final String NAME = "R Parser"; + private static final String NAME = "R"; private static final String IDENTIFIER = "rlang"; private static final int DEFAULT_MIN_TOKEN_MATCH = 8; private static final String[] FILE_EXTENSION = {".R", ".r"}; diff --git a/languages/rlang/src/main/java/de/jplag/rlang/RParserAdapter.java b/languages/rlang/src/main/java/de/jplag/rlang/RParserAdapter.java index e3c3aa6bce..e0ee215f75 100644 --- a/languages/rlang/src/main/java/de/jplag/rlang/RParserAdapter.java +++ b/languages/rlang/src/main/java/de/jplag/rlang/RParserAdapter.java @@ -83,8 +83,8 @@ private void parseFile(File file) throws ParsingException { /** * Adds a new {@link Token} to the current token list. * @param type the type of the new {@link Token} - * @param line the line of the Token in the current file - * @param start the start column of the Token in the line + * @param line the lineNumber of the Token in the current file + * @param start the start column of the Token in the lineNumber * @param length the length of the Token */ /* package-private */ void addToken(TokenType type, int line, int start, int length) { diff --git a/languages/rust/src/main/java/de/jplag/rust/RustLanguage.java b/languages/rust/src/main/java/de/jplag/rust/RustLanguage.java index 50f0826e04..1a5cfa4f81 100644 --- a/languages/rust/src/main/java/de/jplag/rust/RustLanguage.java +++ b/languages/rust/src/main/java/de/jplag/rust/RustLanguage.java @@ -16,7 +16,7 @@ public class RustLanguage implements de.jplag.Language { protected static final String[] FILE_EXTENSIONS = {".rs"}; - private static final String NAME = "Rust Language Module"; + private static final String NAME = "Rust"; private static final String IDENTIFIER = "rust"; private static final int MINIMUM_TOKEN_MATCH = 8; diff --git a/languages/scala/src/main/scala/de/jplag/scala/ScalaLanguage.scala b/languages/scala/src/main/scala/de/jplag/scala/ScalaLanguage.scala index 424b0f7334..bf226a5ced 100644 --- a/languages/scala/src/main/scala/de/jplag/scala/ScalaLanguage.scala +++ b/languages/scala/src/main/scala/de/jplag/scala/ScalaLanguage.scala @@ -14,7 +14,7 @@ class ScalaLanguage extends de.jplag.Language { override def suffixes: Array[String] = fileExtensions - override def getName = "Scala parser" + override def getName = "Scala" override def getIdentifier = "scala" diff --git a/languages/scheme/src/main/java/de/jplag/scheme/SchemeLanguage.java b/languages/scheme/src/main/java/de/jplag/scheme/SchemeLanguage.java index 0ebbf4ef97..13e0c89f72 100644 --- a/languages/scheme/src/main/java/de/jplag/scheme/SchemeLanguage.java +++ b/languages/scheme/src/main/java/de/jplag/scheme/SchemeLanguage.java @@ -12,6 +12,7 @@ @MetaInfServices(de.jplag.Language.class) public class SchemeLanguage implements de.jplag.Language { + private static final String NAME = "Scheme"; private static final String IDENTIFIER = "scheme"; private final de.jplag.scheme.Parser parser; @@ -26,7 +27,7 @@ public String[] suffixes() { @Override public String getName() { - return "SchemeR4RS Parser [basic markup]"; + return NAME; } @Override diff --git a/languages/scxml/src/main/java/de/jplag/scxml/ScxmlLanguage.java b/languages/scxml/src/main/java/de/jplag/scxml/ScxmlLanguage.java index ec6316f4dd..ba4af3e8eb 100644 --- a/languages/scxml/src/main/java/de/jplag/scxml/ScxmlLanguage.java +++ b/languages/scxml/src/main/java/de/jplag/scxml/ScxmlLanguage.java @@ -26,7 +26,7 @@ public class ScxmlLanguage implements de.jplag.Language { */ public static final String VIEW_FILE_SUFFIX = ".scxmlview"; - private static final String NAME = "SCXML (Statechart XML)"; + private static final String NAME = "SCXML"; private static final String IDENTIFIER = "scxml"; private static final int DEFAULT_MIN_TOKEN_MATCH = 6; diff --git a/languages/swift/src/main/java/de/jplag/swift/SwiftLanguage.java b/languages/swift/src/main/java/de/jplag/swift/SwiftLanguage.java index 87e13269fa..82a56d11e4 100644 --- a/languages/swift/src/main/java/de/jplag/swift/SwiftLanguage.java +++ b/languages/swift/src/main/java/de/jplag/swift/SwiftLanguage.java @@ -17,7 +17,7 @@ public class SwiftLanguage implements de.jplag.Language { private static final String IDENTIFIER = "swift"; - private static final String NAME = "Swift Parser"; + private static final String NAME = "Swift"; private static final int DEFAULT_MIN_TOKEN_MATCH = 8; private static final String[] FILE_EXTENSIONS = {".swift"}; private final SwiftParserAdapter parserAdapter; diff --git a/languages/text/src/main/java/de/jplag/text/NaturalLanguage.java b/languages/text/src/main/java/de/jplag/text/NaturalLanguage.java index 5727130097..057c06e56e 100644 --- a/languages/text/src/main/java/de/jplag/text/NaturalLanguage.java +++ b/languages/text/src/main/java/de/jplag/text/NaturalLanguage.java @@ -18,6 +18,7 @@ public class NaturalLanguage implements de.jplag.Language { private static final String IDENTIFIER = "text"; + private static final String NAME = "Text (naive)"; private final ParserAdapter parserAdapter; public NaturalLanguage() { @@ -31,7 +32,7 @@ public String[] suffixes() { @Override public String getName() { - return "Text Parser (naive)"; + return NAME; } @Override diff --git a/languages/text/src/main/java/de/jplag/text/ParserAdapter.java b/languages/text/src/main/java/de/jplag/text/ParserAdapter.java index c3d1ccc831..09f15e15b9 100644 --- a/languages/text/src/main/java/de/jplag/text/ParserAdapter.java +++ b/languages/text/src/main/java/de/jplag/text/ParserAdapter.java @@ -51,7 +51,7 @@ public List parse(Set files) throws ParsingException { private void parseFile(File file) throws ParsingException { this.currentFile = file; this.currentLine = 1; // lines start at 1 - this.currentLineBreakIndex = 0; + this.currentLineBreakIndex = -1; String content = readFile(file); int lastTokenEnd = 0; CoreDocument coreDocument = pipeline.processToCoreDocument(content); diff --git a/languages/typescript/src/main/java/de/jplag/typescript/TypeScriptLanguage.java b/languages/typescript/src/main/java/de/jplag/typescript/TypeScriptLanguage.java index 9fa5ad514c..8dda2b3486 100644 --- a/languages/typescript/src/main/java/de/jplag/typescript/TypeScriptLanguage.java +++ b/languages/typescript/src/main/java/de/jplag/typescript/TypeScriptLanguage.java @@ -11,6 +11,7 @@ public class TypeScriptLanguage extends AbstractAntlrLanguage { private static final String IDENTIFIER = "typescript"; + private static final String NAME = "TypeScript"; private final TypeScriptLanguageOptions options = new TypeScriptLanguageOptions(); @Override @@ -20,7 +21,7 @@ public String[] suffixes() { @Override public String getName() { - return "Typescript Parser"; + return NAME; } @Override diff --git a/pom.xml b/pom.xml index 5fed5943cf..6310f00a32 100644 --- a/pom.xml +++ b/pom.xml @@ -80,7 +80,7 @@ 2.7.7 4.13.2 - 2.36.0 + 2.37.0 2.30.0 2.37.0 3.20.200 @@ -168,7 +168,7 @@ org.mockito mockito-core - 5.12.0 + 5.13.0 test @@ -240,7 +240,7 @@ org.apache.maven.plugins maven-surefire-plugin - 3.4.0 + 3.5.0 org.jacoco @@ -274,7 +274,7 @@ org.apache.maven.plugins maven-deploy-plugin - 3.1.2 + 3.1.3 @@ -331,7 +331,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.8.0 + 3.10.0 attach-javadocs diff --git a/report-viewer/.eslintrc.cjs b/report-viewer/.eslintrc.cjs index dd9e51bf7f..894ad63d50 100644 --- a/report-viewer/.eslintrc.cjs +++ b/report-viewer/.eslintrc.cjs @@ -14,7 +14,7 @@ module.exports = { }, plugins: ['@typescript-eslint', 'vue'], rules: { - 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', + 'no-console': ['error', { allow: ['warn', 'error', 'info'] }], 'no-restricted-exports': ['error', { restrictDefaultExports: { direct: true } }], 'vue/no-setup-props-reactivity-loss': 'error' }, diff --git a/report-viewer/README.md b/report-viewer/README.md index c39bf4f8ef..f1d633c300 100644 --- a/report-viewer/README.md +++ b/report-viewer/README.md @@ -1,37 +1,52 @@ # JPlag Report Viewer -The JPlag Report Viewer is a Vue 3 + Typescript standalone application that can be used to display the JSON files generated by the JPlag reporting. The application requires Node.js and npm to be installed on the system. +The JPlag Report Viewer is a web application that can be used to display the zip file generated by JPlag. -Before the first run execute: - -- Install necessary dependencies by running `npm install` in the /report-viewer folder. -- Start the application by running the `npm run dev` command in the /report-viewer folder. -- The report viewer is now accessible in your browser under http://localhost:8080/ ## Project setup +The application requires Node.js and npm to be installed on the system. ``` npm install ``` -### Compiles and hot-reloads for development +### Run the development server ``` npm run dev ``` -### Compiles and minifies for production +### Compile and build +There are different ways to build the report-viewer. + +The report viewer will be built and packaged with the cli in a jar file if built with the `with-report-viewer` profile: +``` +mvn -Pwith-report-viewer clean package assembly:single +``` + +To build it in the standard way, without any base url, run: ``` npm run build ``` -### Lints and fixes files +For production builds (for example to host on GitHub Pages in a repository called `JPlag`) run: ``` -npm run lint +npm run build:prod ``` +When hosting this build it will need to be accessible under `yourdomain.tld/JPlag/`. -### Format files with prettier +To build the demo version run: ``` -npm run format +npm run build:demo ``` +Similar to the production build, this build will have `demo` as its base url. -### Customize configuration -See [Configuration Reference](https://cli.vuejs.org/config/). + +## Contributing + +We're happy to incorporate all improvements to JPlag into this codebase. Feel free to fork the project and send pull requests. Please consider our guidelines for contributions. + +Before committing please run the following commands to ensure that the code is properly formatted and linted. +``` +npm run format +npm run lint +``` +This can also be done automatically by the precommit hooks. They get automatically installed when running `npm install`. diff --git a/report-viewer/package-lock.json b/report-viewer/package-lock.json index 8872d2f015..cb6c3ad395 100644 --- a/report-viewer/package-lock.json +++ b/report-viewer/package-lock.json @@ -12,12 +12,12 @@ "@fortawesome/free-brands-svg-icons": "^6.6.0", "@fortawesome/free-solid-svg-icons": "^6.6.0", "@fortawesome/vue-fontawesome": "^3.0.8", - "chart.js": "^4.4.3", + "chart.js": "^4.4.4", "chartjs-chart-graph": "^4.3.1", "chartjs-plugin-datalabels": "^2.2.0", "highlight.js": "^11.10.0", "jszip": "^3.10.0", - "pinia": "^2.1.7", + "pinia": "^2.2.2", "slash": "^5.1.0", "vue": "^3.4.38", "vue-chartjs": "^5.3.1", @@ -30,7 +30,7 @@ "@playwright/test": "^1.46.0", "@rushstack/eslint-patch": "^1.10.4", "@types/jsdom": "^21.1.7", - "@types/node": "^22.4.2", + "@types/node": "^22.5.1", "@vitejs/plugin-vue": "^5.1.2", "@vue/eslint-config-prettier": "^9.0.0", "@vue/eslint-config-typescript": "^13.0.0", @@ -46,8 +46,8 @@ "prettier": "^3.3.3", "prettier-plugin-tailwindcss": "^0.6.6", "tailwindcss": "^3.4.6", - "typescript": "^5.5.3", - "vite": "^5.3.4", + "typescript": "^5.5.4", + "vite": "^5.4.2", "vitest": "^2.0.5", "vue-tsc": "^2.0.29" } @@ -896,9 +896,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.13.2.tgz", - "integrity": "sha512-3XFIDKWMFZrMnao1mJhnOT1h2g0169Os848NhhmGweEcfJ4rCi+3yMCOLG4zA61rbJdkcrM/DjVZm9Hg5p5w7g==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.1.tgz", + "integrity": "sha512-2thheikVEuU7ZxFXubPDOtspKn1x0yqaYQwvALVtEcvFhMifPADBrgRPyHV0TF3b+9BgvgjgagVyvA/UqPZHmg==", "cpu": [ "arm" ], @@ -909,9 +909,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.13.2.tgz", - "integrity": "sha512-GdxxXbAuM7Y/YQM9/TwwP+L0omeE/lJAR1J+olu36c3LqqZEBdsIWeQ91KBe6nxwOnb06Xh7JS2U5ooWU5/LgQ==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.1.tgz", + "integrity": "sha512-t1lLYn4V9WgnIFHXy1d2Di/7gyzBWS8G5pQSXdZqfrdCGTwi1VasRMSS81DTYb+avDs/Zz4A6dzERki5oRYz1g==", "cpu": [ "arm64" ], @@ -922,9 +922,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.13.2.tgz", - "integrity": "sha512-mCMlpzlBgOTdaFs83I4XRr8wNPveJiJX1RLfv4hggyIVhfB5mJfN4P8Z6yKh+oE4Luz+qq1P3kVdWrCKcMYrrA==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.1.tgz", + "integrity": "sha512-AH/wNWSEEHvs6t4iJ3RANxW5ZCK3fUnmf0gyMxWCesY1AlUj8jY7GC+rQE4wd3gwmZ9XDOpL0kcFnCjtN7FXlA==", "cpu": [ "arm64" ], @@ -935,9 +935,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.13.2.tgz", - "integrity": "sha512-yUoEvnH0FBef/NbB1u6d3HNGyruAKnN74LrPAfDQL3O32e3k3OSfLrPgSJmgb3PJrBZWfPyt6m4ZhAFa2nZp2A==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.1.tgz", + "integrity": "sha512-dO0BIz/+5ZdkLZrVgQrDdW7m2RkrLwYTh2YMFG9IpBtlC1x1NPNSXkfczhZieOlOLEqgXOFH3wYHB7PmBtf+Bg==", "cpu": [ "x64" ], @@ -948,9 +948,22 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.13.2.tgz", - "integrity": "sha512-GYbLs5ErswU/Xs7aGXqzc3RrdEjKdmoCrgzhJWyFL0r5fL3qd1NPcDKDowDnmcoSiGJeU68/Vy+OMUluRxPiLQ==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.1.tgz", + "integrity": "sha512-sWWgdQ1fq+XKrlda8PsMCfut8caFwZBmhYeoehJ05FdI0YZXk6ZyUjWLrIgbR/VgiGycrFKMMgp7eJ69HOF2pQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.1.tgz", + "integrity": "sha512-9OIiSuj5EsYQlmwhmFRA0LRO0dRRjdCVZA3hnmZe1rEwRk11Jy3ECGGq3a7RrVEZ0/pCsYWx8jG3IvcrJ6RCew==", "cpu": [ "arm" ], @@ -961,9 +974,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.13.2.tgz", - "integrity": "sha512-L1+D8/wqGnKQIlh4Zre9i4R4b4noxzH5DDciyahX4oOz62CphY7WDWqJoQ66zNR4oScLNOqQJfNSIAe/6TPUmQ==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.1.tgz", + "integrity": "sha512-0kuAkRK4MeIUbzQYu63NrJmfoUVicajoRAL1bpwdYIYRcs57iyIV9NLcuyDyDXE2GiZCL4uhKSYAnyWpjZkWow==", "cpu": [ "arm64" ], @@ -974,9 +987,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.13.2.tgz", - "integrity": "sha512-tK5eoKFkXdz6vjfkSTCupUzCo40xueTOiOO6PeEIadlNBkadH1wNOH8ILCPIl8by/Gmb5AGAeQOFeLev7iZDOA==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.1.tgz", + "integrity": "sha512-/6dYC9fZtfEY0vozpc5bx1RP4VrtEOhNQGb0HwvYNwXD1BBbwQ5cKIbUVVU7G2d5WRE90NfB922elN8ASXAJEA==", "cpu": [ "arm64" ], @@ -987,11 +1000,11 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.13.2.tgz", - "integrity": "sha512-zvXvAUGGEYi6tYhcDmb9wlOckVbuD+7z3mzInCSTACJ4DQrdSLPNUeDIcAQW39M3q6PDquqLWu7pnO39uSMRzQ==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.1.tgz", + "integrity": "sha512-ltUWy+sHeAh3YZ91NUsV4Xg3uBXAlscQe8ZOXRCVAKLsivGuJsrkawYPUEyCV3DYa9urgJugMLn8Z3Z/6CeyRQ==", "cpu": [ - "ppc64le" + "ppc64" ], "dev": true, "optional": true, @@ -1000,9 +1013,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.13.2.tgz", - "integrity": "sha512-C3GSKvMtdudHCN5HdmAMSRYR2kkhgdOfye4w0xzyii7lebVr4riCgmM6lRiSCnJn2w1Xz7ZZzHKuLrjx5620kw==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.1.tgz", + "integrity": "sha512-BggMndzI7Tlv4/abrgLwa/dxNEMn2gC61DCLrTzw8LkpSKel4o+O+gtjbnkevZ18SKkeN3ihRGPuBxjaetWzWg==", "cpu": [ "riscv64" ], @@ -1013,9 +1026,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.13.2.tgz", - "integrity": "sha512-l4U0KDFwzD36j7HdfJ5/TveEQ1fUTjFFQP5qIt9gBqBgu1G8/kCaq5Ok05kd5TG9F8Lltf3MoYsUMw3rNlJ0Yg==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.1.tgz", + "integrity": "sha512-z/9rtlGd/OMv+gb1mNSjElasMf9yXusAxnRDrBaYB+eS1shFm6/4/xDH1SAISO5729fFKUkJ88TkGPRUh8WSAA==", "cpu": [ "s390x" ], @@ -1026,9 +1039,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.13.2.tgz", - "integrity": "sha512-xXMLUAMzrtsvh3cZ448vbXqlUa7ZL8z0MwHp63K2IIID2+DeP5iWIT6g1SN7hg1VxPzqx0xZdiDM9l4n9LRU1A==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.1.tgz", + "integrity": "sha512-kXQVcWqDcDKw0S2E0TmhlTLlUgAmMVqPrJZR+KpH/1ZaZhLSl23GZpQVmawBQGVhyP5WXIsIQ/zqbDBBYmxm5w==", "cpu": [ "x64" ], @@ -1039,9 +1052,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.13.2.tgz", - "integrity": "sha512-M/JYAWickafUijWPai4ehrjzVPKRCyDb1SLuO+ZyPfoXgeCEAlgPkNXewFZx0zcnoIe3ay4UjXIMdXQXOZXWqA==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.1.tgz", + "integrity": "sha512-CbFv/WMQsSdl+bpX6rVbzR4kAjSSBuDgCqb1l4J68UYsQNalz5wOqLGYj4ZI0thGpyX5kc+LLZ9CL+kpqDovZA==", "cpu": [ "x64" ], @@ -1052,9 +1065,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.13.2.tgz", - "integrity": "sha512-2YWwoVg9KRkIKaXSh0mz3NmfurpmYoBBTAXA9qt7VXk0Xy12PoOP40EFuau+ajgALbbhi4uTj3tSG3tVseCjuA==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.1.tgz", + "integrity": "sha512-3Q3brDgA86gHXWHklrwdREKIrIbxC0ZgU8lwpj0eEKGBQH+31uPqr0P2v11pn0tSIxHvcdOWxa4j+YvLNx1i6g==", "cpu": [ "arm64" ], @@ -1065,9 +1078,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.13.2.tgz", - "integrity": "sha512-2FSsE9aQ6OWD20E498NYKEQLneShWes0NGMPQwxWOdws35qQXH+FplabOSP5zEe1pVjurSDOGEVCE2agFwSEsw==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.1.tgz", + "integrity": "sha512-tNg+jJcKR3Uwe4L0/wY3Ro0H+u3nrb04+tcq1GSYzBEmKLeOQF2emk1whxlzNqb6MMrQ2JOcQEpuuiPLyRcSIw==", "cpu": [ "ia32" ], @@ -1078,9 +1091,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.2.tgz", - "integrity": "sha512-7h7J2nokcdPePdKykd8wtc8QqqkqxIrUz7MHj6aNr8waBRU//NLDVnNjQnqQO6fqtjrtCdftpbTuOKAyrAQETQ==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.1.tgz", + "integrity": "sha512-xGiIH95H1zU7naUyTKEyOA/I0aexNMUdO9qRv0bLKN3qu25bBdrxZHqA3PTJ24YNN/GdMzG4xkDcd/GvjuhfLg==", "cpu": [ "x64" ], @@ -1130,9 +1143,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "22.4.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.4.2.tgz", - "integrity": "sha512-nAvM3Ey230/XzxtyDcJ+VjvlzpzoHwLsF7JaDRfoI0ytO0mVheerNmM45CtA0yOILXwXXxOrcUWH3wltX+7PSw==", + "version": "22.5.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.1.tgz", + "integrity": "sha512-KkHsxej0j9IW1KKOOAA/XBA0z08UFSrRQHErzEfA3Vgq57eXIMYboIlHJuYIfd+lwCQjtKqUu3UnmKbtUc9yRw==", "dev": true, "dependencies": { "undici-types": "~6.19.2" @@ -2063,9 +2076,9 @@ } }, "node_modules/chart.js": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.3.tgz", - "integrity": "sha512-qK1gkGSRYcJzqrrzdR6a+I0vQ4/R+SoODXyAjscQ/4mzuNzySaMCd+hyVxitSY1+L2fjPD1Gbn+ibNqRmwQeLw==", + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.4.tgz", + "integrity": "sha512-emICKGBABnxhMjUjlYRR12PmOXhJ2eJjEHL2/dZlWjxRAZT1D8xplLFq5M0tMQK8ja+wBS/tuVEJB5C6r7VxJA==", "dependencies": { "@kurkle/color": "^0.3.0" }, @@ -5852,9 +5865,9 @@ } }, "node_modules/rollup": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.13.2.tgz", - "integrity": "sha512-MIlLgsdMprDBXC+4hsPgzWUasLO9CE4zOkj/u6j+Z6j5A4zRY+CtiXAdJyPtgCsc42g658Aeh1DlrdVEJhsL2g==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.1.tgz", + "integrity": "sha512-ZnYyKvscThhgd3M5+Qt3pmhO4jIRR5RGzaSovB6Q7rGNrK5cUncrtLmcTTJVSdcKXyZjW8X8MB0JMSuH9bcAJg==", "dev": true, "dependencies": { "@types/estree": "1.0.5" @@ -5867,21 +5880,22 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.13.2", - "@rollup/rollup-android-arm64": "4.13.2", - "@rollup/rollup-darwin-arm64": "4.13.2", - "@rollup/rollup-darwin-x64": "4.13.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.13.2", - "@rollup/rollup-linux-arm64-gnu": "4.13.2", - "@rollup/rollup-linux-arm64-musl": "4.13.2", - "@rollup/rollup-linux-powerpc64le-gnu": "4.13.2", - "@rollup/rollup-linux-riscv64-gnu": "4.13.2", - "@rollup/rollup-linux-s390x-gnu": "4.13.2", - "@rollup/rollup-linux-x64-gnu": "4.13.2", - "@rollup/rollup-linux-x64-musl": "4.13.2", - "@rollup/rollup-win32-arm64-msvc": "4.13.2", - "@rollup/rollup-win32-ia32-msvc": "4.13.2", - "@rollup/rollup-win32-x64-msvc": "4.13.2", + "@rollup/rollup-android-arm-eabi": "4.21.1", + "@rollup/rollup-android-arm64": "4.21.1", + "@rollup/rollup-darwin-arm64": "4.21.1", + "@rollup/rollup-darwin-x64": "4.21.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.21.1", + "@rollup/rollup-linux-arm-musleabihf": "4.21.1", + "@rollup/rollup-linux-arm64-gnu": "4.21.1", + "@rollup/rollup-linux-arm64-musl": "4.21.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.21.1", + "@rollup/rollup-linux-riscv64-gnu": "4.21.1", + "@rollup/rollup-linux-s390x-gnu": "4.21.1", + "@rollup/rollup-linux-x64-gnu": "4.21.1", + "@rollup/rollup-linux-x64-musl": "4.21.1", + "@rollup/rollup-win32-arm64-msvc": "4.21.1", + "@rollup/rollup-win32-ia32-msvc": "4.21.1", + "@rollup/rollup-win32-x64-msvc": "4.21.1", "fsevents": "~2.3.2" } }, @@ -6754,9 +6768,9 @@ } }, "node_modules/typescript": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", - "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", "devOptional": true, "bin": { "tsc": "bin/tsc", @@ -6861,14 +6875,14 @@ } }, "node_modules/vite": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.4.tgz", - "integrity": "sha512-Cw+7zL3ZG9/NZBB8C+8QbQZmR54GwqIz+WMI4b3JgdYJvX+ny9AjJXqkGQlDXSXRP9rP0B4tbciRMOVEKulVOA==", + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.2.tgz", + "integrity": "sha512-dDrQTRHp5C1fTFzcSaMxjk6vdpKvT+2/mIdE07Gw2ykehT49O0z/VHS3zZ8iV/Gh8BJJKHWOe5RjaNrW5xf/GA==", "dev": true, "dependencies": { "esbuild": "^0.21.3", - "postcss": "^8.4.39", - "rollup": "^4.13.0" + "postcss": "^8.4.41", + "rollup": "^4.20.0" }, "bin": { "vite": "bin/vite.js" @@ -6887,6 +6901,7 @@ "less": "*", "lightningcss": "^1.21.0", "sass": "*", + "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" @@ -6904,6 +6919,9 @@ "sass": { "optional": true }, + "sass-embedded": { + "optional": true + }, "stylus": { "optional": true }, diff --git a/report-viewer/package.json b/report-viewer/package.json index 7133e1d166..d30193a854 100644 --- a/report-viewer/package.json +++ b/report-viewer/package.json @@ -23,12 +23,12 @@ "@fortawesome/free-brands-svg-icons": "^6.6.0", "@fortawesome/free-solid-svg-icons": "^6.6.0", "@fortawesome/vue-fontawesome": "^3.0.8", - "chart.js": "^4.4.3", + "chart.js": "^4.4.4", "chartjs-chart-graph": "^4.3.1", "chartjs-plugin-datalabels": "^2.2.0", "highlight.js": "^11.10.0", "jszip": "^3.10.0", - "pinia": "^2.1.7", + "pinia": "^2.2.2", "slash": "^5.1.0", "vue": "^3.4.38", "vue-chartjs": "^5.3.1", @@ -41,7 +41,7 @@ "@playwright/test": "^1.46.0", "@rushstack/eslint-patch": "^1.10.4", "@types/jsdom": "^21.1.7", - "@types/node": "^22.4.2", + "@types/node": "^22.5.1", "@vitejs/plugin-vue": "^5.1.2", "@vue/eslint-config-prettier": "^9.0.0", "@vue/eslint-config-typescript": "^13.0.0", @@ -57,8 +57,8 @@ "prettier": "^3.3.3", "prettier-plugin-tailwindcss": "^0.6.6", "tailwindcss": "^3.4.6", - "typescript": "^5.5.3", - "vite": "^5.3.4", + "typescript": "^5.5.4", + "vite": "^5.4.2", "vitest": "^2.0.5", "vue-tsc": "^2.0.29" } diff --git a/report-viewer/src/components/ClusterGraph.vue b/report-viewer/src/components/ClusterGraph.vue index b20d02b674..3d9686f9c6 100644 --- a/report-viewer/src/components/ClusterGraph.vue +++ b/report-viewer/src/components/ClusterGraph.vue @@ -83,7 +83,7 @@ const hoverableEdges = computed(() => { const firstIndex = keys.value.indexOf(key) const secondIndex = keys.value.indexOf(match.matchedWith) if (firstIndex == -1 || secondIndex == -1) { - console.log(`Could not find index for ${key} or ${match.matchedWith}`) + console.warn(`Could not find index for ${key} or ${match.matchedWith}`) } if (firstIndex < secondIndex) { edges.push({ diff --git a/report-viewer/src/components/ComparisonTableFilter.vue b/report-viewer/src/components/ComparisonTableFilter.vue index 99a381b75e..b2e47a29b7 100644 --- a/report-viewer/src/components/ComparisonTableFilter.vue +++ b/report-viewer/src/components/ComparisonTableFilter.vue @@ -83,7 +83,7 @@ const searchStringValue = computed({ } for (const submissionId of store().getSubmissionIds) { - const submissionParts = submissionId.toLowerCase().split(/ +/g) + const submissionParts = store().submissionDisplayName(submissionId).toLowerCase().split(/ +/g) if (submissionParts.every((part) => searchParts.includes(part))) { store().state.anonymous.delete(submissionId) } diff --git a/report-viewer/src/components/ComparisonsTable.vue b/report-viewer/src/components/ComparisonsTable.vue index 9af99fe2cf..37703b6476 100644 --- a/report-viewer/src/components/ComparisonsTable.vue +++ b/report-viewer/src/components/ComparisonsTable.vue @@ -236,9 +236,9 @@ function getFilteredComparisons(comparisons: ComparisonListElement[]) { return comparisons.filter((c) => { // name search - const id1 = c.firstSubmissionId.toLowerCase() - const id2 = c.secondSubmissionId.toLowerCase() - if (searches.some((s) => id1.includes(s) || id2.includes(s))) { + const name1 = store().submissionDisplayName(c.firstSubmissionId).toLowerCase() + const name2 = store().submissionDisplayName(c.secondSubmissionId).toLowerCase() + if (searches.some((s) => name1.includes(s) || name2.includes(s))) { return true } diff --git a/report-viewer/src/components/fileDisplaying/CodePanel.vue b/report-viewer/src/components/fileDisplaying/CodePanel.vue index f71b737b43..5da443d081 100644 --- a/report-viewer/src/components/fileDisplaying/CodePanel.vue +++ b/report-viewer/src/components/fileDisplaying/CodePanel.vue @@ -137,7 +137,6 @@ function collapse() { } function expand() { - console.log('expand') collapsed.value = false } diff --git a/report-viewer/src/components/fileDisplaying/FilesContainer.vue b/report-viewer/src/components/fileDisplaying/FilesContainer.vue index ede1c03ba0..9c03e3e9d7 100644 --- a/report-viewer/src/components/fileDisplaying/FilesContainer.vue +++ b/report-viewer/src/components/fileDisplaying/FilesContainer.vue @@ -179,11 +179,9 @@ const tokenCount = computed(() => { function scrollTo(file: string, line: number) { const fileIndex = Array.from(props.files).findIndex((f) => f.fileName === file) if (fileIndex !== -1) { - console.log(fileIndex) codePanels.value[fileIndex].expand() nextTick(() => { if (!scrollContainer.value) { - console.log('null') return } const childToScrollTo = codePanels.value[fileIndex].getLineRect(line) as DOMRect diff --git a/report-viewer/src/model/factories/BaseFactory.ts b/report-viewer/src/model/factories/BaseFactory.ts index 54908b2149..52f4633681 100644 --- a/report-viewer/src/model/factories/BaseFactory.ts +++ b/report-viewer/src/model/factories/BaseFactory.ts @@ -22,8 +22,6 @@ export class BaseFactory { return await (await this.getLocalFile(`/files/${path}`)).text() } else if (store().state.zipModeUsed) { return this.getFileFromStore(path) - } else if (store().state.singleModeUsed) { - return store().state.singleFillRawContent } else if (await this.useLocalZipMode()) { await new ZipFileHandler().handleFile(await this.getLocalFile(this.zipFileName)) store().setLoadingType('zip') diff --git a/report-viewer/src/model/factories/ComparisonFactory.ts b/report-viewer/src/model/factories/ComparisonFactory.ts index 0b4b774c3d..9305ad630e 100644 --- a/report-viewer/src/model/factories/ComparisonFactory.ts +++ b/report-viewer/src/model/factories/ComparisonFactory.ts @@ -97,7 +97,7 @@ export class ComparisonFactory extends BaseFactory { }) } } catch (e) { - console.log(e) + console.error(e) } } diff --git a/report-viewer/src/model/fileHandling/JsonFileHandler.ts b/report-viewer/src/model/fileHandling/JsonFileHandler.ts deleted file mode 100644 index 9c8b8d736f..0000000000 --- a/report-viewer/src/model/fileHandling/JsonFileHandler.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { store } from '@/stores/store' -import { FileHandler } from './FileHandler' - -/** - * Class for handling single json files. - */ -export class JsonFileHandler extends FileHandler { - public async handleFile(file: Blob) { - const content = await file.text() - const json = JSON.parse(content) - - store().setSingleFileRawContent(content) - if (!json['submission_folder_path']) { - throw new Error(`Invalid JSON: File is not an overview file.`) - } - } -} diff --git a/report-viewer/src/model/fileHandling/ZipFileHandler.ts b/report-viewer/src/model/fileHandling/ZipFileHandler.ts index b7754937a9..7aa604d010 100644 --- a/report-viewer/src/model/fileHandling/ZipFileHandler.ts +++ b/report-viewer/src/model/fileHandling/ZipFileHandler.ts @@ -8,7 +8,7 @@ import { FileHandler } from './FileHandler' */ export class ZipFileHandler extends FileHandler { public async handleFile(file: Blob) { - console.log('Start handling zip file and storing necessary data...') + console.info('Start handling zip file and storing necessary data...') return jszip.loadAsync(file).then(async (zip) => { for (const originalFileName of Object.keys(zip.files)) { const unixFileName = slash(originalFileName) diff --git a/report-viewer/src/stores/state.ts b/report-viewer/src/stores/state.ts index b1f031c87f..d12bdac29f 100644 --- a/report-viewer/src/stores/state.ts +++ b/report-viewer/src/stores/state.ts @@ -28,14 +28,6 @@ export interface State { * Indicates whether zip mode is used. */ zipModeUsed: boolean - /** - * Indicates whether single file mode is used. - */ - singleModeUsed: boolean - /** - * Files string if single mode is used. - */ - singleFillRawContent: string fileIdToDisplayName: Map submissionIdsToComparisonFileName: Map> diff --git a/report-viewer/src/stores/store.ts b/report-viewer/src/stores/store.ts index 5a721662e9..e722cbbd21 100644 --- a/report-viewer/src/stores/store.ts +++ b/report-viewer/src/stores/store.ts @@ -18,9 +18,6 @@ const store = defineStore('store', { // Mode that was used to load the files localModeUsed: false, zipModeUsed: false, - singleModeUsed: false, - // only used in single mode - singleFillRawContent: '', fileIdToDisplayName: new Map(), uploadedFileName: '' }, @@ -133,8 +130,6 @@ const store = defineStore('store', { submissions: {}, localModeUsed: false, zipModeUsed: false, - singleModeUsed: false, - singleFillRawContent: '', fileIdToDisplayName: new Map(), uploadedFileName: '' } @@ -201,17 +196,9 @@ const store = defineStore('store', { * Sets the loading type * @param payload Type used to input JPlag results */ - setLoadingType(loadingType: 'zip' | 'local' | 'single') { + setLoadingType(loadingType: 'zip' | 'local') { this.state.localModeUsed = loadingType == 'local' this.state.zipModeUsed = loadingType == 'zip' - this.state.singleModeUsed = loadingType == 'single' - }, - /** - * Sets the raw content of the single file mode - * @param payload Raw content of the single file mode - */ - setSingleFileRawContent(payload: string) { - this.state.singleFillRawContent = payload }, /** * Switches whether darkMode is being used for the UI diff --git a/report-viewer/src/version.json b/report-viewer/src/version.json index d03da50710..9134088500 100644 --- a/report-viewer/src/version.json +++ b/report-viewer/src/version.json @@ -1,6 +1,6 @@ { "report_viewer_version": { - "major": 5, + "major": 0, "minor": 0, "patch": 0 }, diff --git a/report-viewer/src/viewWrapper/InformationViewWrapper.vue b/report-viewer/src/viewWrapper/InformationViewWrapper.vue index fc9e16ef05..a939749582 100644 --- a/report-viewer/src/viewWrapper/InformationViewWrapper.vue +++ b/report-viewer/src/viewWrapper/InformationViewWrapper.vue @@ -36,5 +36,5 @@ OverviewFactory.getOverview() OptionsFactory.getCliOptions() .then((o) => (cliOptions.value = o)) - .catch((error) => console.log('Could not load full options.', error)) + .catch((error) => console.error('Could not load full options.', error)) diff --git a/report-viewer/src/views/FileUploadView.vue b/report-viewer/src/views/FileUploadView.vue index c0f3598da3..02191f8e49 100644 --- a/report-viewer/src/views/FileUploadView.vue +++ b/report-viewer/src/views/FileUploadView.vue @@ -40,10 +40,7 @@ Continue with local files - -
- -
+

{{ getErrorText() }}

For more details check the console.

@@ -62,7 +59,6 @@ import Button from '@/components/ButtonComponent.vue' import VersionInfoComponent from '@/components/VersionInfoComponent.vue' import LoadingCircle from '@/components/LoadingCircle.vue' import { ZipFileHandler } from '@/model/fileHandling/ZipFileHandler' -import { JsonFileHandler } from '@/model/fileHandling/JsonFileHandler' import { BaseFactory } from '@/model/factories/BaseFactory' store().clearStore() @@ -111,21 +107,6 @@ function navigateToOverview() { }) } -/** - * Handles a json file on drop. It read the file and passes the file string to next window. - * @param file The json file to handle - */ -async function handleJsonFile(file: Blob) { - try { - await new JsonFileHandler().handleFile(file) - } catch (e) { - registerError(e as Error, 'upload') - return - } - store().setLoadingType('single') - navigateToOverview() -} - /** * Handles a file on drop. It determines the file type and passes it to the corresponding handler. * @param file File to handle @@ -140,8 +121,6 @@ async function handleFile(file: Blob) { store().setLoadingType('zip') await new ZipFileHandler().handleFile(file) return navigateToOverview() - case 'application/json': - return await handleJsonFile(file) default: throw new Error(`Unknown MIME type '${file.type}'`) } @@ -236,4 +215,8 @@ onErrorCaptured((error) => { registerError(error, 'unknown') return false }) + +if (exampleFiles.value) { + continueWithLocal() +} diff --git a/report-viewer/tests/unit/model/factories/ComparisonFactory.test.ts b/report-viewer/tests/unit/model/factories/ComparisonFactory.test.ts index eabfc58eac..a918f43edb 100644 --- a/report-viewer/tests/unit/model/factories/ComparisonFactory.test.ts +++ b/report-viewer/tests/unit/model/factories/ComparisonFactory.test.ts @@ -1,55 +1,59 @@ -import { vi, it, beforeAll, describe, expect } from 'vitest' +import { it, beforeEach, describe, expect } from 'vitest' import validNew from './ValidComparison.json' import { ComparisonFactory } from '@/model/factories/ComparisonFactory' import { store } from '@/stores/store' import { MetricType } from '@/model/MetricType' - -const store = { - state: { - localModeUsed: false, - zipModeUsed: true, - singleModeUsed: false, - files: {} - }, - getComparisonFileName: (id1: string, id2: string) => { - return `${id1}-${id2}.json` - }, - filesOfSubmission: (name: string) => { - return [ - { - fileName: `${name}/Structure.java`, - value: '' - }, - { - fileName: `${name}/Submission.java`, - value: '' - } - ] - }, - getSubmissionFile: (id: string, name: string) => { - return { - fileName: name, - submissionId: id, - matchedTokenCount: 0, - displayName: name - } - } -} +import { setActivePinia, createPinia } from 'pinia' describe('Test JSON to Comparison', () => { - beforeAll(() => { - vi.mock('@/stores/store', () => ({ - store: vi.fn(() => { - return store - }) - })) + beforeEach(() => { + setActivePinia(createPinia()) + store().setLoadingType('zip') }) it('Post 5.0', async () => { - store.state.files['root1-root2.json'] = JSON.stringify(validNew) + store().state.files['root1-root2.json'] = JSON.stringify(validNew) + store().state.submissionIdsToComparisonFileName.set( + 'root1', + new Map([['root2', 'root1-root2.json']]) + ) + store().state.submissionIdsToComparisonFileName.set( + 'root2', + new Map([['root1', 'root1-root2.json']]) + ) + store().state.submissions['root1'] = new Map() + store().state.submissions['root1'].set('root1/Structure.java', { + fileName: 'root1/Structure.java', + value: '', + submissionId: 'root1', + matchedTokenCount: 0, + displayName: 'Structure.java' + }) + store().state.submissions['root1'].set('root1/Submission.java', { + fileName: 'root1/Submission.java', + value: '', + submissionId: 'root1', + matchedTokenCount: 0, + displayName: 'Submission.java' + }) + store().state.submissions['root2'] = new Map() + store().state.submissions['root2'].set('root2/Structure.java', { + fileName: 'root2/Structure.java', + value: '', + submissionId: 'root2', + matchedTokenCount: 0, + displayName: 'Structure.java' + }) + store().state.submissions['root2'].set('root2/Submission.java', { + fileName: 'root2/Submission.java', + value: '', + submissionId: 'root2', + matchedTokenCount: 0, + displayName: 'Submission.java' + }) const result = await ComparisonFactory.getComparison( - store.getComparisonFileName('root1', 'root2') + store().getComparisonFileName('root1', 'root2') ) expect(result).toBeDefined() diff --git a/report-viewer/tests/unit/model/factories/OptionsFactory.test.ts b/report-viewer/tests/unit/model/factories/OptionsFactory.test.ts index b6719152c9..5cbc82bee9 100644 --- a/report-viewer/tests/unit/model/factories/OptionsFactory.test.ts +++ b/report-viewer/tests/unit/model/factories/OptionsFactory.test.ts @@ -1,30 +1,19 @@ -import { beforeAll, describe, it, vi, expect } from 'vitest' +import { beforeEach, describe, it, expect } from 'vitest' import { OptionsFactory } from '@/model/factories/OptionsFactory' import { ParserLanguage } from '@/model/Language' import { MetricType } from '@/model/MetricType' import validOptions from './ValidOptions.json' - -const store = { - state: { - localModeUsed: false, - zipModeUsed: true, - singleModeUsed: false, - files: { - 'options.json': JSON.stringify(validOptions) - } - } -} +import { setActivePinia, createPinia } from 'pinia' +import { store } from '@/stores/store' describe('Test JSON to Options', async () => { - beforeAll(() => { - vi.mock('@/stores/store', () => ({ - store: vi.fn(() => { - return store - }) - })) + beforeEach(() => { + setActivePinia(createPinia()) + store().setLoadingType('zip') }) it('Test Valid JSON', async () => { + store().state.files['options.json'] = JSON.stringify(validOptions) const result = await OptionsFactory.getCliOptions() expect(result).toEqual({ diff --git a/report-viewer/tests/unit/model/factories/OverviewFactory.test.ts b/report-viewer/tests/unit/model/factories/OverviewFactory.test.ts index 6104668ce3..d5f9fb21dd 100644 --- a/report-viewer/tests/unit/model/factories/OverviewFactory.test.ts +++ b/report-viewer/tests/unit/model/factories/OverviewFactory.test.ts @@ -1,43 +1,25 @@ -import { beforeAll, describe, expect, it, vi } from 'vitest' +import { beforeAll, describe, expect, it, vi, beforeEach } from 'vitest' import { OverviewFactory } from '@/model/factories/OverviewFactory' import { MetricType } from '@/model/MetricType' import { Distribution } from '@/model/Distribution' import { ParserLanguage } from '@/model/Language' import validNew from './ValidOverview.json' import outdated from './OutdatedOverview.json' - -const store = { - state: { - localModeUsed: false, - zipModeUsed: true, - singleModeUsed: false, - files: {} - }, - saveSubmissionNames: (map) => { - expect(map.has('A')).toBeTruthy() - expect(map.has('B')).toBeTruthy() - expect(map.has('C')).toBeTruthy() - expect(map.has('D')).toBeTruthy() - }, - saveComparisonFileLookup: (map) => { - expect(map.has('A')).toBeTruthy() - expect(map.has('B')).toBeTruthy() - } -} +import { setActivePinia, createPinia } from 'pinia' +import { store } from '@/stores/store' describe('Test JSON to Overview', () => { beforeAll(() => { - vi.mock('@/stores/store', () => ({ - store: vi.fn(() => { - return store - }) - })) - vi.spyOn(global.window, 'alert').mockImplementation(() => {}) }) + beforeEach(() => { + setActivePinia(createPinia()) + store().setLoadingType('zip') + }) + it('Post 5.0', async () => { - store.state.files['overview.json'] = JSON.stringify(validNew) + store().state.files['overview.json'] = JSON.stringify(validNew) expect(await OverviewFactory.getOverview()).toEqual({ _submissionFolderPath: ['files'], @@ -143,7 +125,7 @@ describe('Test JSON to Overview', () => { describe('Outdated JSON to Overview', () => { it('Outdated version', async () => { - store.state.files['overview.json'] = JSON.stringify(outdated) + store().state.files['overview.json'] = JSON.stringify(outdated) expect(() => OverviewFactory.getOverview()).rejects.toThrowError() }) }) diff --git a/report-viewer/tests/unit/model/factories/ValidComparison.json b/report-viewer/tests/unit/model/factories/ValidComparison.json index 57453ca7b4..2698402a20 100644 --- a/report-viewer/tests/unit/model/factories/ValidComparison.json +++ b/report-viewer/tests/unit/model/factories/ValidComparison.json @@ -7,8 +7,8 @@ }, "matches": [ { - "firstFile": "root2\\Structure.java", - "secondFile": "root1\\Structure.java", + "firstFile": "root1\\Structure.java", + "secondFile": "root2\\Structure.java", "startInFirst": { "line": 1, "column": 1, @@ -32,8 +32,8 @@ "tokens": 139 }, { - "firstFile": "root2\\Submission.java", - "secondFile": "root1\\Submission.java", + "firstFile": "root1\\Submission.java", + "secondFile": "root2\\Submission.java", "startInFirst": { "line": 129, "column": 1, @@ -57,8 +57,8 @@ "tokens": 34 }, { - "firstFile": "root2\\Submission.java", - "secondFile": "root1\\Submission.java", + "firstFile": "root1\\Submission.java", + "secondFile": "root2\\Submission.java", "startInFirst": { "line": 165, "column": 1, @@ -82,8 +82,8 @@ "tokens": 33 }, { - "firstFile": "root2\\Submission.java", - "secondFile": "root1\\Submission.java", + "firstFile": "root1\\Submission.java", + "secondFile": "root2\\Submission.java", "startInFirst": { "line": 112, "column": 1,