diff --git a/core/src/main/java/de/jplag/JPlagResult.java b/core/src/main/java/de/jplag/JPlagResult.java index 8b4d123d9..2b1aabd32 100644 --- a/core/src/main/java/de/jplag/JPlagResult.java +++ b/core/src/main/java/de/jplag/JPlagResult.java @@ -23,7 +23,7 @@ public class JPlagResult { private final int[] similarityDistribution; // 10-element array representing the similarity distribution of the detected matches. private List> clusteringResult; - private final int SIMILARITY_DISTRIBUTION_SIZE = 10; + private final int SIMILARITY_DISTRIBUTION_SIZE = 100; public JPlagResult(List comparisons, SubmissionSet submissions, long durationInMillis, JPlagOptions options) { // sort by similarity (descending) 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 5b3594979..242d09138 100644 --- a/core/src/main/java/de/jplag/reporting/jsonfactory/ComparisonReportWriter.java +++ b/core/src/main/java/de/jplag/reporting/jsonfactory/ComparisonReportWriter.java @@ -12,6 +12,7 @@ import de.jplag.JPlagResult; import de.jplag.Submission; import de.jplag.Token; +import de.jplag.options.SimilarityMetric; import de.jplag.reporting.FilePathUtil; import de.jplag.reporting.reportobject.model.ComparisonReport; import de.jplag.reporting.reportobject.model.Match; @@ -55,7 +56,8 @@ private void writeComparisons(String path, List comparisons) { String secondSubmissionId = submissionToIdFunction.apply(comparison.secondSubmission()); String fileName = generateComparisonName(firstSubmissionId, secondSubmissionId); addToLookUp(firstSubmissionId, secondSubmissionId, fileName); - var comparisonReport = new ComparisonReport(firstSubmissionId, secondSubmissionId, comparison.similarity(), + var comparisonReport = new ComparisonReport(firstSubmissionId, secondSubmissionId, + Map.of(SimilarityMetric.AVG.name(), comparison.similarity(), SimilarityMetric.MAX.name(), comparison.maximalSimilarity()), convertMatchesToReportMatches(comparison)); fileWriter.saveAsJSON(comparisonReport, path, fileName); }); diff --git a/core/src/main/java/de/jplag/reporting/reportobject/ReportObjectFactory.java b/core/src/main/java/de/jplag/reporting/reportobject/ReportObjectFactory.java index 1815c55e2..0f8f142da 100644 --- a/core/src/main/java/de/jplag/reporting/reportobject/ReportObjectFactory.java +++ b/core/src/main/java/de/jplag/reporting/reportobject/ReportObjectFactory.java @@ -33,7 +33,6 @@ import de.jplag.reporting.jsonfactory.ToDiskWriter; import de.jplag.reporting.reportobject.mapper.ClusteringResultMapper; import de.jplag.reporting.reportobject.mapper.MetricMapper; -import de.jplag.reporting.reportobject.model.Metric; import de.jplag.reporting.reportobject.model.OverviewReport; import de.jplag.reporting.reportobject.model.SubmissionFileIndex; import de.jplag.reporting.reportobject.model.Version; @@ -175,7 +174,7 @@ private void writeOverview(JPlagResult result, String path) { int totalComparisons = result.getAllComparisons().size(); int numberOfMaximumComparisons = result.getOptions().maximumNumberOfComparisons(); - int shownComparisons = totalComparisons > numberOfMaximumComparisons ? numberOfMaximumComparisons : totalComparisons; + int shownComparisons = Math.min(totalComparisons, numberOfMaximumComparisons); int missingComparisons = totalComparisons > numberOfMaximumComparisons ? (totalComparisons - numberOfMaximumComparisons) : 0; logger.info("Total Comparisons: {}. Comparisons in Report: {}. Omitted Comparisons: {}.", totalComparisons, shownComparisons, missingComparisons); @@ -190,7 +189,8 @@ private void writeOverview(JPlagResult result, String path) { result.getOptions().minimumTokenMatch(), // matchSensitivity getDate(),// dateOfExecution result.getDuration(), // executionTime - getMetrics(result),// metrics + MetricMapper.getDistributions(result), // distribution + new MetricMapper(submissionToIdFunction).getTopComparisons(result),// topComparisons clusteringResultMapper.map(result), // clusters totalComparisons); // totalComparisons @@ -220,16 +220,6 @@ private Set getSubmissions(List comparisons) { return submissions; } - /** - * Gets the used metrics in a JPlag comparison. As Max Metric is included in every JPlag run, this always include Max - * Metric. - * @return A list contains Metric DTOs. - */ - private List getMetrics(JPlagResult result) { - MetricMapper metricMapper = new MetricMapper(submissionToIdFunction); - return List.of(metricMapper.getAverageMetric(result), metricMapper.getMaxMetric(result)); - } - private String getDate() { SimpleDateFormat dateFormat = new SimpleDateFormat("dd/MM/yy"); Date date = new Date(); diff --git a/core/src/main/java/de/jplag/reporting/reportobject/mapper/MetricMapper.java b/core/src/main/java/de/jplag/reporting/reportobject/mapper/MetricMapper.java index 0b8a819d4..bdad683a0 100644 --- a/core/src/main/java/de/jplag/reporting/reportobject/mapper/MetricMapper.java +++ b/core/src/main/java/de/jplag/reporting/reportobject/mapper/MetricMapper.java @@ -3,16 +3,14 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.Comparator; import java.util.List; +import java.util.Map; import java.util.function.Function; import de.jplag.JPlagComparison; import de.jplag.JPlagResult; -import de.jplag.Messages; import de.jplag.Submission; import de.jplag.options.SimilarityMetric; -import de.jplag.reporting.reportobject.model.Metric; import de.jplag.reporting.reportobject.model.TopComparison; /** @@ -25,40 +23,35 @@ public MetricMapper(Function submissionToIdFunction) { this.submissionToIdFunction = submissionToIdFunction; } - public Metric getAverageMetric(JPlagResult result) { - return new Metric(SimilarityMetric.AVG.name(), convertDistribution(result.getSimilarityDistribution()), - getTopComparisons(getComparisons(result)), Messages.getString("SimilarityMetric.Avg.Description")); + /** + * Generates a map of all distributions + * @param result Result containing distributions + * @return Map with key as name of metric and value as distribution + */ + public static Map> getDistributions(JPlagResult result) { + return Map.of(SimilarityMetric.AVG.name(), convertDistribution(result.getSimilarityDistribution()), SimilarityMetric.MAX.name(), + convertDistribution(result.getMaxSimilarityDistribution())); } - public Metric getMaxMetric(JPlagResult result) { - return new Metric(SimilarityMetric.MAX.name(), convertDistribution(result.getMaxSimilarityDistribution()), - getMaxSimilarityTopComparisons(getComparisons(result)), Messages.getString("SimilarityMetric.Max.Description")); + /** + * Generates a List of the top comparisons + * @param result Result containing comparisons + * @return List of top comparisons with similarities in all metrics + */ + public List getTopComparisons(JPlagResult result) { + return result.getComparisons(result.getOptions().maximumNumberOfComparisons()).stream() + .map(comparison -> new TopComparison(submissionToIdFunction.apply(comparison.firstSubmission()), + submissionToIdFunction.apply(comparison.secondSubmission()), getComparisonMetricMap(comparison))) + .toList(); } - private List getComparisons(JPlagResult result) { - int maxNumberOfComparisons = result.getOptions().maximumNumberOfComparisons(); - return result.getComparisons(maxNumberOfComparisons); + private Map getComparisonMetricMap(JPlagComparison comparison) { + return Map.of(SimilarityMetric.AVG.name(), comparison.similarity(), SimilarityMetric.MAX.name(), comparison.maximalSimilarity()); } - private List convertDistribution(int[] array) { + private static List convertDistribution(int[] array) { List list = new ArrayList<>(Arrays.stream(array).boxed().toList()); Collections.reverse(list); return list; } - - private List getTopComparisons(List comparisons, Function similarityExtractor) { - return comparisons.stream().sorted(Comparator.comparing(similarityExtractor).reversed()) - .map(comparison -> new TopComparison(submissionToIdFunction.apply(comparison.firstSubmission()), - submissionToIdFunction.apply(comparison.secondSubmission()), similarityExtractor.apply(comparison))) - .toList(); - } - - private List getTopComparisons(List comparisons) { - return getTopComparisons(comparisons, JPlagComparison::similarity); - } - - private List getMaxSimilarityTopComparisons(List comparisons) { - return getTopComparisons(comparisons, JPlagComparison::maximalSimilarity); - } - } diff --git a/core/src/main/java/de/jplag/reporting/reportobject/model/ComparisonReport.java b/core/src/main/java/de/jplag/reporting/reportobject/model/ComparisonReport.java index 00b243fbd..5b8d2fc4b 100644 --- a/core/src/main/java/de/jplag/reporting/reportobject/model/ComparisonReport.java +++ b/core/src/main/java/de/jplag/reporting/reportobject/model/ComparisonReport.java @@ -1,6 +1,7 @@ package de.jplag.reporting.reportobject.model; import java.util.List; +import java.util.Map; import com.fasterxml.jackson.annotation.JsonProperty; @@ -8,10 +9,10 @@ * ReportViewer DTO for the comparison of two submissions. * @param firstSubmissionId id of the first submission * @param secondSubmissionId id of the second submission - * @param similarity average similarity. between 0.0 and 1.0. + * @param similarities map of metric names and corresponding similarities. between 0.0 and 1.0. * @param matches the list of matches found in the comparison of the two submissions */ public record ComparisonReport(@JsonProperty("id1") String firstSubmissionId, @JsonProperty("id2") String secondSubmissionId, - @JsonProperty("similarity") double similarity, @JsonProperty("matches") List matches) { + @JsonProperty("similarities") Map similarities, @JsonProperty("matches") List matches) { } diff --git a/core/src/main/java/de/jplag/reporting/reportobject/model/OverviewReport.java b/core/src/main/java/de/jplag/reporting/reportobject/model/OverviewReport.java index b4651dbe9..fa7da7673 100644 --- a/core/src/main/java/de/jplag/reporting/reportobject/model/OverviewReport.java +++ b/core/src/main/java/de/jplag/reporting/reportobject/model/OverviewReport.java @@ -31,7 +31,9 @@ public record OverviewReport( @JsonProperty("execution_time") long executionTime, - @JsonProperty("metrics") List metrics, + @JsonProperty("distributions") Map> distributions, + + @JsonProperty("top_comparisons") List topComparisons, @JsonProperty("clusters") List clusters, diff --git a/core/src/main/java/de/jplag/reporting/reportobject/model/TopComparison.java b/core/src/main/java/de/jplag/reporting/reportobject/model/TopComparison.java index 7dfa1339f..ba4a54da8 100644 --- a/core/src/main/java/de/jplag/reporting/reportobject/model/TopComparison.java +++ b/core/src/main/java/de/jplag/reporting/reportobject/model/TopComparison.java @@ -1,7 +1,9 @@ package de.jplag.reporting.reportobject.model; +import java.util.Map; + import com.fasterxml.jackson.annotation.JsonProperty; public record TopComparison(@JsonProperty("first_submission") String firstSubmission, @JsonProperty("second_submission") String secondSubmission, - @JsonProperty("similarity") double similarity) { + @JsonProperty("similarities") Map similarities) { } diff --git a/core/src/test/java/de/jplag/BaseCodeTest.java b/core/src/test/java/de/jplag/BaseCodeTest.java index bd307dc43..193482b8b 100644 --- a/core/src/test/java/de/jplag/BaseCodeTest.java +++ b/core/src/test/java/de/jplag/BaseCodeTest.java @@ -46,7 +46,7 @@ protected void verifyResults(JPlagResult result) { assertEquals(2, result.getNumberOfSubmissions()); assertEquals(1, result.getAllComparisons().size()); assertEquals(1, result.getAllComparisons().get(0).matches().size()); - assertEquals(1, result.getSimilarityDistribution()[8]); + assertEquals(1, result.getSimilarityDistribution()[81]); assertEquals(0.8125, result.getAllComparisons().get(0).similarity(), DELTA); } @@ -94,7 +94,7 @@ protected void verifySimpleSubdirectoryDuplicate(JPlagResult result, int submiss assertEquals(submissions, result.getNumberOfSubmissions()); assertEquals(comparisons, result.getAllComparisons().size()); assertEquals(1, result.getAllComparisons().get(0).matches().size()); - assertEquals(1, result.getSimilarityDistribution()[9]); + assertEquals(1, result.getSimilarityDistribution()[94]); assertEquals(0.9473, result.getAllComparisons().get(0).similarity(), DELTA); } diff --git a/core/src/test/java/de/jplag/BasicFunctionalityTest.java b/core/src/test/java/de/jplag/BasicFunctionalityTest.java index 4bf4bf347..0c88ce7ea 100644 --- a/core/src/test/java/de/jplag/BasicFunctionalityTest.java +++ b/core/src/test/java/de/jplag/BasicFunctionalityTest.java @@ -14,6 +14,8 @@ */ class BasicFunctionalityTest extends TestBase { + private static int DISTRIBUTION_INDEX = 66; + @Test @DisplayName("test submissions that contain obvious plagiarism") void testSimpleDuplicate() throws ExitException { @@ -22,14 +24,15 @@ void testSimpleDuplicate() throws ExitException { assertEquals(2, result.getNumberOfSubmissions()); assertEquals(1, result.getAllComparisons().size()); assertEquals(1, result.getAllComparisons().get(0).matches().size()); - assertEquals(1, result.getSimilarityDistribution()[6]); + assertEquals(1, result.getSimilarityDistribution()[DISTRIBUTION_INDEX]); assertEquals(0.666, result.getAllComparisons().get(0).similarity(), DELTA); } @Test @DisplayName("test submissions with a custom minimum token match") void testWithMinTokenMatch() throws ExitException { - var expectedDistribution = new int[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 1}; + var expectedDistribution = new int[100]; + expectedDistribution[96] = 1; JPlagResult result = runJPlag("SimpleDuplicate", it -> it.withMinimumTokenMatch(4)); assertEquals(2, result.getNumberOfSubmissions()); @@ -90,7 +93,7 @@ void testSingleFileSubmisssions() throws ExitException { assertEquals(2, result.getNumberOfSubmissions()); assertEquals(1, result.getAllComparisons().size()); - assertEquals(1, result.getSimilarityDistribution()[6]); + assertEquals(1, result.getSimilarityDistribution()[DISTRIBUTION_INDEX]); assertEquals(0.666, result.getAllComparisons().get(0).similarity(), DELTA); var matches = result.getAllComparisons().get(0).matches(); diff --git a/core/src/test/java/de/jplag/reporting/reportobject/mapper/MetricMapperTest.java b/core/src/test/java/de/jplag/reporting/reportobject/mapper/MetricMapperTest.java index 1aedbefa8..6419fda3b 100644 --- a/core/src/test/java/de/jplag/reporting/reportobject/mapper/MetricMapperTest.java +++ b/core/src/test/java/de/jplag/reporting/reportobject/mapper/MetricMapperTest.java @@ -7,6 +7,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -18,43 +19,42 @@ import de.jplag.reporting.reportobject.model.TopComparison; public class MetricMapperTest { - private static final List EXPECTED_DISTRIBUTION = List.of(29, 23, 19, 17, 13, 11, 7, 5, 3, 2); + private static final List EXPECTED_AVG_DISTRIBUTION = List.of(1, 0, 0, 2, 3, 15, 5, 2, 16, 5, 2, 18, 3, 21, 2, 1, 5, 0, 14, 32, 25, 4, 2, + 12, 3, 2, 5, 5, 0, 5, 1, 5, 2, 5, 4, 5, 3, 5, 18, 21, 30, 4, 3, 10, 2, 3, 17, 28, 4, 10, 2, 4, 3, 0, 2, 20, 4, 0, 19, 5, 25, 9, 4, 18, 1, + 1, 1, 0, 31, 15, 35, 38, 40, 43, 45, 49, 50, 50, 50, 53, 60, 71, 73, 74, 80, 83, 87, 93, 95, 99, 102, 105, 106, 110, 113, 113, 117, 117, + 122, 124); + private static final List EXPECTED_MAX_DISTRIBUTION = List.of(130, 129, 124, 116, 114, 110, 110, 108, 103, 101, 99, 97, 96, 92, 82, 81, + 70, 67, 64, 63, 59, 56, 52, 50, 50, 50, 49, 47, 43, 5, 6, 11, 4, 2, 3, 20, 37, 5, 0, 2, 33, 30, 19, 4, 5, 24, 40, 6, 3, 9, 2, 3, 18, 3, 5, + 1, 4, 1, 0, 0, 5, 5, 14, 5, 42, 4, 18, 0, 0, 10, 4, 3, 17, 33, 4, 4, 3, 4, 39, 0, 20, 2, 4, 9, 0, 5, 0, 8, 23, 4, 2, 39, 3, 4, 1, 0, 3, + 33, 2, 1); private final MetricMapper metricMapper = new MetricMapper(Submission::getName); @Test - public void test_getAverageMetric() { + public void test_getDistributions() { // given - JPlagResult jPlagResult = createJPlagResult(MockMetric.AVG, distribution(EXPECTED_DISTRIBUTION), - comparison(submission("1"), submission("2"), .7), comparison(submission("3"), submission("4"), .3)); + JPlagResult jPlagResult = createJPlagResult(distribution(EXPECTED_AVG_DISTRIBUTION), distribution(EXPECTED_MAX_DISTRIBUTION), + comparison(submission("1"), submission("2"), .7, .8), comparison(submission("3"), submission("4"), .3, .9)); + // when - var result = metricMapper.getAverageMetric(jPlagResult); + Map> result = MetricMapper.getDistributions(jPlagResult); // then - Assertions.assertEquals("AVG", result.name()); - Assertions.assertIterableEquals(EXPECTED_DISTRIBUTION, result.distribution()); - Assertions.assertEquals(List.of(new TopComparison("1", "2", .7), new TopComparison("3", "4", .3)), result.topComparisons()); - Assertions.assertEquals( - "Average of both program coverages. This is the default similarity which" - + " works in most cases: Matches with a high average similarity indicate that the programs work " + "in a very similar way.", - result.description()); + Assertions.assertEquals(Map.of("AVG", EXPECTED_AVG_DISTRIBUTION, "MAX", EXPECTED_MAX_DISTRIBUTION), result); } @Test - public void test_getMaxMetric() { + public void test_getTopComparisons() { // given - JPlagResult jPlagResult = createJPlagResult(MockMetric.MAX, distribution(EXPECTED_DISTRIBUTION), - comparison(submission("00"), submission("01"), .7), comparison(submission("10"), submission("11"), .3)); + JPlagResult jPlagResult = createJPlagResult(distribution(EXPECTED_AVG_DISTRIBUTION), distribution(EXPECTED_MAX_DISTRIBUTION), + comparison(submission("1"), submission("2"), .7, .8), comparison(submission("3"), submission("4"), .3, .9)); + // when - var result = metricMapper.getMaxMetric(jPlagResult); + List result = metricMapper.getTopComparisons(jPlagResult); // then - Assertions.assertEquals("MAX", result.name()); - Assertions.assertIterableEquals(EXPECTED_DISTRIBUTION, result.distribution()); - Assertions.assertEquals(List.of(new TopComparison("00", "01", .7), new TopComparison("10", "11", .3)), result.topComparisons()); Assertions.assertEquals( - "Maximum of both program coverages. This ranking is especially useful if the programs are very " - + "different in size. This can happen when dead code was inserted to disguise the origin of the plagiarized program.", - result.description()); + List.of(new TopComparison("1", "2", Map.of("AVG", .7, "MAX", .8)), new TopComparison("3", "4", Map.of("AVG", .3, "MAX", .9))), + result); } private int[] distribution(List expectedDistribution) { @@ -67,19 +67,14 @@ private CreateSubmission submission(String name) { return new CreateSubmission(name); } - private Comparison comparison(CreateSubmission submission1, CreateSubmission submission2, double similarity) { - return new Comparison(submission1, submission2, similarity); + private Comparison comparison(CreateSubmission submission1, CreateSubmission submission2, double similarity, double maxSimilarity) { + return new Comparison(submission1, submission2, similarity, maxSimilarity); } - private JPlagResult createJPlagResult(MockMetric metricToMock, int[] distribution, Comparison... createComparisonsDto) { + private JPlagResult createJPlagResult(int[] avgDistribution, int[] maxDistribution, Comparison... createComparisonsDto) { JPlagResult jPlagResult = mock(JPlagResult.class); - - if (metricToMock.equals(MockMetric.AVG)) { - doReturn(distribution).when(jPlagResult).getSimilarityDistribution(); - } else if (metricToMock.equals(MockMetric.MAX)) { - doReturn(distribution).when(jPlagResult).getMaxSimilarityDistribution(); - - } + doReturn(avgDistribution).when(jPlagResult).getSimilarityDistribution(); + doReturn(maxDistribution).when(jPlagResult).getMaxSimilarityDistribution(); JPlagOptions options = mock(JPlagOptions.class); doReturn(createComparisonsDto.length).when(options).maximumNumberOfComparisons(); @@ -95,11 +90,8 @@ private JPlagResult createJPlagResult(MockMetric metricToMock, int[] distributio JPlagComparison mockedComparison = mock(JPlagComparison.class); doReturn(submission1).when(mockedComparison).firstSubmission(); doReturn(submission2).when(mockedComparison).secondSubmission(); - if (metricToMock.equals(MockMetric.AVG)) { - doReturn(comparisonDto.similarity).when(mockedComparison).similarity(); - } else if (metricToMock.equals(MockMetric.MAX)) { - doReturn(comparisonDto.similarity).when(mockedComparison).maximalSimilarity(); - } + doReturn(comparisonDto.similarity).when(mockedComparison).similarity(); + doReturn(comparisonDto.maxSimilarity).when(mockedComparison).maximalSimilarity(); comparisonList.add(mockedComparison); } @@ -107,15 +99,10 @@ private JPlagResult createJPlagResult(MockMetric metricToMock, int[] distributio return jPlagResult; } - private enum MockMetric { - MAX, - AVG - } - - private record Comparison(CreateSubmission submission1, CreateSubmission submission2, double similarity) { + private record Comparison(CreateSubmission submission1, CreateSubmission submission2, double similarity, double maxSimilarity) { } private record CreateSubmission(String name) { } -} +} \ No newline at end of file diff --git a/report-viewer/src/components/ComparisonsTable.vue b/report-viewer/src/components/ComparisonsTable.vue index 400b67943..9ef0ce566 100644 --- a/report-viewer/src/components/ComparisonsTable.vue +++ b/report-viewer/src/components/ComparisonsTable.vue @@ -79,8 +79,12 @@
-
{{ (item.averageSimilarity * 100).toFixed(2) }}%
-
{{ (item.maximumSimilarity * 100).toFixed(2) }}%
+
+ {{ (item.similarities[MetricType.AVERAGE] * 100).toFixed(2) }}% +
+
+ {{ (item.similarities[MetricType.MAXIMUM] * 100).toFixed(2) }}% +
@@ -133,6 +137,7 @@ import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' import { library } from '@fortawesome/fontawesome-svg-core' import { faUserGroup } from '@fortawesome/free-solid-svg-icons' import { generateColors } from '@/utils/ColorUtils' +import MetricType from '@/model/MetricType' library.add(faUserGroup) diff --git a/report-viewer/src/components/DistributionDiagram.vue b/report-viewer/src/components/DistributionDiagram.vue index e161b860a..3cc12413c 100644 --- a/report-viewer/src/components/DistributionDiagram.vue +++ b/report-viewer/src/components/DistributionDiagram.vue @@ -23,7 +23,7 @@ const props = defineProps({ } }) -const maxVal = ref(Math.max(...props.distribution.distribution)) +const maxVal = ref(Math.max(...props.distribution.splitIntoTenBuckets())) const labels = [ '91-100%', '81-90%', @@ -51,7 +51,7 @@ const chartData = ref({ datasets: [ { ...dataSetStyle.value, - data: props.distribution.distribution + data: props.distribution.splitIntoTenBuckets() } ] }) @@ -109,12 +109,12 @@ watch( datasets: [ { ...dataSetStyle.value, - data: val.distribution + data: val.splitIntoTenBuckets() } ] } - maxVal.value = Math.max(...val.distribution) + maxVal.value = Math.max(...val.splitIntoTenBuckets()) options.value.scales.x.suggestedMax = maxVal.value + 5 } ) diff --git a/report-viewer/src/model/Comparison.ts b/report-viewer/src/model/Comparison.ts index 53b9bc6ff..36f1b7dca 100644 --- a/report-viewer/src/model/Comparison.ts +++ b/report-viewer/src/model/Comparison.ts @@ -1,6 +1,7 @@ import type { Match } from './Match' import type { SubmissionFile } from './SubmissionFile' import type { MatchInSingleFile } from './MatchInSingleFile' +import type MetricType from './MetricType' /** * Comparison model used by the ComparisonView @@ -8,22 +9,27 @@ import type { MatchInSingleFile } from './MatchInSingleFile' export class Comparison { private readonly _firstSubmissionId: string private readonly _secondSubmissionId: string - private readonly _similarity: number + private readonly _similarities: Record + private _filesOfFirstSubmission: Map + private _filesOfSecondSubmission: Map + private _allMatches: Array - constructor(firstSubmissionId: string, secondSubmissionId: string, similarity: number) { + constructor( + firstSubmissionId: string, + secondSubmissionId: string, + similarities: Record, + filesOfFirstSubmission: Map, + filesOfSecondSubmission: Map, + allMatches: Array + ) { this._firstSubmissionId = firstSubmissionId this._secondSubmissionId = secondSubmissionId - this._similarity = similarity - this._filesOfFirstSubmission = new Map() - this._filesOfSecondSubmission = new Map() - this._colors = [] - this._allMatches = [] - this._matchesInFirstSubmission = new Map() - this._matchesInSecondSubmissions = new Map() + this._similarities = similarities + this._filesOfFirstSubmission = filesOfFirstSubmission + this._filesOfSecondSubmission = filesOfSecondSubmission + this._allMatches = allMatches } - private _filesOfFirstSubmission: Map - /** * @return Map of all files of the first submission */ @@ -31,16 +37,6 @@ export class Comparison { return this._filesOfFirstSubmission } - /** - * Set the files of the first submission - * @param value Map to set to - */ - set filesOfFirstSubmission(value: Map) { - this._filesOfFirstSubmission = value - } - - private _filesOfSecondSubmission: Map - /** * @return Map of all files of the second submission */ @@ -48,33 +44,6 @@ export class Comparison { return this._filesOfSecondSubmission } - /** - * Set the files of the second submission - * @param value Map to set to - */ - set filesOfSecondSubmission(value: Map) { - this._filesOfSecondSubmission = value - } - - private _colors: Array - - /** - * @return Array of all colors used to display the matches - */ - get colors(): Array { - return this._colors - } - - /** - * Set the colors used to display the matches - * @param value Colors to set to - */ - set colors(value: Array) { - this._colors = value - } - - private _allMatches: Array - /** * @return Array of all matches */ @@ -82,46 +51,18 @@ export class Comparison { return this._allMatches } - /** - * Set the array of all matches - * @param value Matches to set to - */ - set allMatches(value: Array) { - this._allMatches = value - } - - private _matchesInFirstSubmission: Map> - /** * @return Map of all matches in the first submission */ get matchesInFirstSubmission(): Map> { - return this._matchesInFirstSubmission - } - - /** - * Set the matches in the first submission - * @param value Matches in the first submission to set to - */ - set matchesInFirstSubmission(value: Map>) { - this._matchesInFirstSubmission = value + return this.groupMatchesByFileName(1) } - private _matchesInSecondSubmissions: Map> - /** * @return Map of all matches in the second submission */ get matchesInSecondSubmissions(): Map> { - return this._matchesInSecondSubmissions - } - - /** - * Set the matches in the second submission - * @param value Matches in the first submission to set to - */ - set matchesInSecondSubmissions(value: Map>) { - this._matchesInSecondSubmissions = value + return this.groupMatchesByFileName(2) } /** @@ -141,7 +82,41 @@ export class Comparison { /** * @return Similarity of the two submissions */ - get similarity() { - return this._similarity + get similarities() { + return this._similarities + } + + private groupMatchesByFileName(index: 1 | 2): Map> { + const acc = new Map>() + this.allMatches.forEach((val) => { + const name = index === 1 ? val.firstFile : val.secondFile + + if (!acc.get(name)) { + acc.set(name, []) + } + + if (index === 1) { + const newVal: MatchInSingleFile = { + start: val.startInFirst, + end: val.endInFirst, + linked_panel: 2, + linked_file: val.secondFile, + linked_line: val.startInSecond, + color: val.color + } + acc.get(name)?.push(newVal) + } else { + const newVal: MatchInSingleFile = { + start: val.startInSecond, + end: val.endInSecond, + linked_panel: 1, + linked_file: val.firstFile, + linked_line: val.startInFirst, + color: val.color + } + acc.get(name)?.push(newVal) + } + }) + return acc } } diff --git a/report-viewer/src/model/ComparisonListElement.ts b/report-viewer/src/model/ComparisonListElement.ts index 815e6c9c1..60f4fb483 100644 --- a/report-viewer/src/model/ComparisonListElement.ts +++ b/report-viewer/src/model/ComparisonListElement.ts @@ -1,3 +1,5 @@ +import type MetricType from './MetricType' + /** * Comparison model used by the Comparison Table in Overview. Only the needed attributes to display are included. * For full comparison model see Comparison.ts @@ -13,6 +15,5 @@ export type ComparisonListElement = { id: number firstSubmissionId: string secondSubmissionId: string - averageSimilarity: number - maximumSimilarity: number + similarities: Record } diff --git a/report-viewer/src/model/Distribution.ts b/report-viewer/src/model/Distribution.ts index d05544b53..622f334a5 100644 --- a/report-viewer/src/model/Distribution.ts +++ b/report-viewer/src/model/Distribution.ts @@ -1,11 +1,12 @@ -export default class Distribution { - private readonly _distribution: Array +export default abstract class Distribution { + protected readonly _distribution: number[] - constructor(distribution: Array) { + constructor(distribution: number[]) { this._distribution = distribution } - get distribution(): Array { - return this._distribution - } + /** + * Returns the distribution summed at every tenth percentile + */ + public abstract splitIntoTenBuckets(): number[] } diff --git a/report-viewer/src/model/HundredValueDistribution.ts b/report-viewer/src/model/HundredValueDistribution.ts new file mode 100644 index 000000000..465cf88f9 --- /dev/null +++ b/report-viewer/src/model/HundredValueDistribution.ts @@ -0,0 +1,22 @@ +import Distribution from './Distribution' + +/** + * This class represents he JPlag Distribution of a metric. + * It is composed of 100 values, each representing the sum of the values of the metric in the corresponding percentile. + */ +export default class HundredValueDistribution extends Distribution { + constructor(distribution: number[]) { + super(distribution) + } + + /** + * Returns the distribution summed at every tenth percentile + */ + public splitIntoTenBuckets(): number[] { + const tenValueArray = new Array(10).fill(0) + for (let i = 99; i >= 0; i--) { + tenValueArray[Math.floor(i / 10)] += this._distribution[i] + } + return tenValueArray + } +} diff --git a/report-viewer/src/model/Metric.ts b/report-viewer/src/model/Metric.ts deleted file mode 100644 index b6e7d6374..000000000 --- a/report-viewer/src/model/Metric.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { ComparisonListElement } from './ComparisonListElement' - -/** - * Metric used in the Jplag Comparison - * @property metricName - Name of the metric. - * @property description - Description of the metric. - * @property metricThreshold - Threshold of the metric. - * @property distribution - Distribution of the metric. - * @property comparisons - Comparisons of the metric. - */ -export type Metric = { - metricName: string - description: string - metricThreshold: number - distribution: Array - comparisons: Array -} diff --git a/report-viewer/src/model/MetricType.ts b/report-viewer/src/model/MetricType.ts index dfb93c9f0..16f5c7439 100644 --- a/report-viewer/src/model/MetricType.ts +++ b/report-viewer/src/model/MetricType.ts @@ -2,8 +2,8 @@ * This enum maps the metric type to the index they have in the generated JSON and respectivly in the store. */ enum MetricType { - AVERAGE = 0, - MAXIMUM = 1 + AVERAGE = 'AVG', + MAXIMUM = 'MAX' } export default MetricType diff --git a/report-viewer/src/model/Overview.ts b/report-viewer/src/model/Overview.ts index a59e2ccd0..4ee8d9ba0 100644 --- a/report-viewer/src/model/Overview.ts +++ b/report-viewer/src/model/Overview.ts @@ -1,6 +1,7 @@ import type Distribution from './Distribution' import type { Cluster } from '@/model/Cluster' import type { ComparisonListElement } from './ComparisonListElement' +import type MetricType from './MetricType' /** * Model of the Overview file generated by JPlag @@ -11,11 +12,10 @@ export class Overview { private readonly _language: string private readonly _fileExtensions: Array private readonly _matchSensitivity: number - private readonly _submissionIdsToComparisonFileName: Map> private readonly _dateOfExecution: string private readonly _durationOfExecution: number private readonly _topComparisons: Array - private readonly _distributions: Array + private readonly _distributions: Record private readonly _clusters: Array private readonly _totalComparisons: number @@ -28,10 +28,9 @@ export class Overview { dateOfExecution: string, durationOfExecution: number, topComparisons: Array, - distributions: Array, + distributions: Record, clusters: Array, - totalComparisons: number, - submissionIdsToComparisonFileName: Map> + totalComparisons: number ) { this._submissionFolderPath = submissionFolderPath this._baseCodeFolderPath = baseCodeFolderPath @@ -43,17 +42,9 @@ export class Overview { this._topComparisons = topComparisons this._distributions = distributions this._clusters = clusters - this._submissionIdsToComparisonFileName = submissionIdsToComparisonFileName this._totalComparisons = totalComparisons } - /** - * @return Map of submission ids to the name of the comparison file and their content - */ - get submissionIdsToComparisonFileName() { - return this._submissionIdsToComparisonFileName - } - /** * @return Path to the folder containing the submissions */ diff --git a/report-viewer/src/model/TenValueDistribution.ts b/report-viewer/src/model/TenValueDistribution.ts new file mode 100644 index 000000000..3b8e244a1 --- /dev/null +++ b/report-viewer/src/model/TenValueDistribution.ts @@ -0,0 +1,16 @@ +import Distribution from './Distribution' + +/** + * This class represents he JPlag Distribution of a metric. + * It is composed of 10 values, each representing the sum of the values of the metric in the corresponding percentiles. + */ +/** @deprecated since 5.0.0. Use the new format with {@link HundredValueDistribution} */ +export default class TenValueDistribution extends Distribution { + constructor(distribution: number[]) { + super(distribution) + } + + public splitIntoTenBuckets(): number[] { + return this._distribution + } +} diff --git a/report-viewer/src/model/factories/BaseFactory.ts b/report-viewer/src/model/factories/BaseFactory.ts new file mode 100644 index 000000000..2c31ccfc5 --- /dev/null +++ b/report-viewer/src/model/factories/BaseFactory.ts @@ -0,0 +1,50 @@ +import store from '@/stores/store' + +/** + * This class provides some basic functionality for the factories. + */ +export class BaseFactory { + /** + * Returns the content of a file through the stored loading type. + * @param path - Path to the file + * @return Content of the file + * @throws Error if the file could not be found + */ + protected static getFile(path: string): string { + if (store().state.localModeUsed) { + return this.getLocalFile(path) + } else if (store().state.zipModeUsed) { + const index = Object.keys(store().state.files).find((name) => name.endsWith(path)) + if (index == undefined) { + throw new Error(`Could not find ${path} in zip file.`) + } + const file = store().state.files[index] + if (file == undefined) { + throw new Error(`Could not load ${path}.`) + } + return file + } else if (store().state.singleModeUsed) { + return store().state.singleFillRawContent + } + + throw new Error('No loading type specified') + } + + /** + * Returns the content of a file from the local files. + * @param path - Path to the file + * @return Content of the file + * @throws Error if the file could not be found + */ + protected static getLocalFile(path: string): string { + const request = new XMLHttpRequest() + request.open('GET', `/files/${path}`, false) + request.send() + + if (request.status == 200) { + return request.response + } else { + throw new Error(`Could not find ${path} in local files.`) + } + } +} diff --git a/report-viewer/src/model/factories/ComparisonFactory.ts b/report-viewer/src/model/factories/ComparisonFactory.ts index a4eac9e5d..afd0c4d5f 100644 --- a/report-viewer/src/model/factories/ComparisonFactory.ts +++ b/report-viewer/src/model/factories/ComparisonFactory.ts @@ -5,50 +5,20 @@ import type { MatchInSingleFile } from '../MatchInSingleFile' import store from '@/stores/store' import { generateColors } from '@/utils/ColorUtils' import slash from 'slash' +import { BaseFactory } from './BaseFactory' +import MetricType from '../MetricType' /** * Factory class for creating Comparison objects */ -export class ComparisonFactory { +export class ComparisonFactory extends BaseFactory { public static getComparison(id1: string, id2: string) { - console.log('Generating comparison {%s} - {%s}...', id1, id2) - let comparison = new Comparison('', '', 0) - - //getting the comparison file based on the used mode (zip, local, single) - if (store().state.localModeUsed) { - const request = new XMLHttpRequest() - request.open('GET', `/files/${store().getComparisonFileName(id1, id2)}`, false) - request.send() - - if (request.status == 200) { - ComparisonFactory.loadSubmissionFilesFromLocal(id1) - ComparisonFactory.loadSubmissionFilesFromLocal(id2) - try { - comparison = ComparisonFactory.extractComparison(JSON.parse(request.response)) - } catch (e) { - throw new Error('Comparison file not found') - } - } else { - throw new Error('Comparison file not found') - } - } else if (store().state.zipModeUsed) { - const comparisonFile = store().getComparisonFileForSubmissions(id1, id2) - if (comparisonFile) { - comparison = ComparisonFactory.extractComparison(JSON.parse(comparisonFile)) - } else { - throw new Error('Comparison file not found') - } - } else if (store().state.singleModeUsed) { - try { - comparison = ComparisonFactory.extractComparison( - JSON.parse(store().state.singleFillRawContent) - ) - } catch (e) { - store().clearStore() - throw new Error('No Comparison files given!') - } + const filePath = store().getComparisonFileName(id1, id2) + if (!filePath) { + throw new Error('Comparison file not specified') } - return comparison + + return this.extractComparison(JSON.parse(this.getFile(filePath))) } /** @@ -56,11 +26,17 @@ export class ComparisonFactory { * @param json the json object */ private static extractComparison(json: Record): Comparison { - const filesOfFirstSubmission = store().filesOfSubmission(json.id1 as string) - const filesOfSecondSubmission = store().filesOfSubmission(json.id2 as string) + const firstSubmissionId = json.id1 as string + const secondSubmissionId = json.id2 as string + if (store().state.localModeUsed) { + this.loadSubmissionFilesFromLocal(firstSubmissionId) + this.loadSubmissionFilesFromLocal(json.id2 as string) + } + const filesOfFirstSubmission = store().filesOfSubmission(firstSubmissionId) + const filesOfSecondSubmission = store().filesOfSubmission(secondSubmissionId) - const filesOfFirstConverted = this.convertToFilesByName(filesOfFirstSubmission) - const filesOfSecondConverted = this.convertToFilesByName(filesOfSecondSubmission) + const filesOfFirstConverted = this.convertToSubmissionFileList(filesOfFirstSubmission) + const filesOfSecondConverted = this.convertToSubmissionFileList(filesOfSecondSubmission) const matches = json.matches as Array> @@ -70,25 +46,46 @@ export class ComparisonFactory { const colors = generateColors(matches.length, matchSaturation, matchLightness, matchAlpha) const coloredMatches = matches.map((match, index) => this.mapMatch(match, colors[index])) - const matchesInFirst = this.groupMatchesByFileName(coloredMatches, 1) - const matchesInSecond = this.groupMatchesByFileName(coloredMatches, 2) - - const comparison = new Comparison( - json.id1 as string, - json.id2 as string, - json.similarity as number + return new Comparison( + firstSubmissionId, + secondSubmissionId, + this.extractSimilarities(json), + filesOfFirstConverted, + filesOfSecondConverted, + coloredMatches ) - comparison.filesOfFirstSubmission = filesOfFirstConverted - comparison.filesOfSecondSubmission = filesOfSecondConverted - comparison.colors = colors - comparison.allMatches = coloredMatches - comparison.matchesInFirstSubmission = matchesInFirst - comparison.matchesInSecondSubmissions = matchesInSecond + } - return comparison + private static extractSimilarities(json: Record): Record { + if (json.similarities) { + return this.extractSimilaritiesFromMap(json.similarities as Record) + } else if (json.similarity) { + return this.extractSimilaritiesFromSingleValue(json.similarity as number) + } + throw new Error('No similarities found in comparison file') } - private static convertToFilesByName( + /** @deprecated since 5.0.0. Use the new format with {@link extractSimilaritiesFromMap} */ + private static extractSimilaritiesFromSingleValue( + avgSimilarity: number + ): Record { + return { + [MetricType.AVERAGE]: avgSimilarity, + [MetricType.MAXIMUM]: Number.NaN + } + } + + private static extractSimilaritiesFromMap( + similarityMap: Record + ): Record { + const similarities = {} as Record + for (const [key, value] of Object.entries(similarityMap)) { + similarities[key as MetricType] = value + } + return similarities + } + + private static convertToSubmissionFileList( files: Array<{ name: string; value: string }> ): Map { const map = new Map() @@ -107,74 +104,26 @@ export class ComparisonFactory { return map } - private static groupMatchesByFileName( - matches: Array, - index: number - ): Map> { - const acc = new Map>() - matches.forEach((val) => { - const name = index === 1 ? (val.firstFile as string) : (val.secondFile as string) - - if (!acc.get(name)) { - acc.set(name, []) - } - - if (index === 1) { - const newVal: MatchInSingleFile = { - start: val.startInFirst as number, - end: val.endInFirst as number, - linked_panel: 2, - linked_file: val.secondFile as string, - linked_line: val.startInSecond as number, - color: val.color as string - } - acc.get(name)?.push(newVal) - } else { - const newVal: MatchInSingleFile = { - start: val.startInSecond as number, - end: val.endInSecond as number, - linked_panel: 1, - linked_file: val.firstFile as string, - linked_line: val.startInFirst as number, - color: val.color as string - } - acc.get(name)?.push(newVal) - } - }) - return acc - } - private static getSubmissionFileListFromLocal(submissionId: string): string[] { - const request = new XMLHttpRequest() - request.open('GET', `/files/submissionFileIndex.json`, false) - request.send() - if (request.status == 200) { - return JSON.parse(request.response).submission_file_indexes[submissionId].map( - (file: string) => slash(file) - ) - } else { - return [] - } + return JSON.parse(this.getLocalFile(`submissionFileIndex.json`)).submission_file_indexes[ + submissionId + ].map((file: string) => slash(file)) } private static loadSubmissionFilesFromLocal(submissionId: string) { - const request = new XMLHttpRequest() - const fileList = ComparisonFactory.getSubmissionFileListFromLocal(submissionId) - for (const file of fileList) { - request.open('GET', `/files/files/${file}`, false) - request.overrideMimeType('text/plain') - request.send() - if (request.status == 200) { + try { + const fileList = this.getSubmissionFileListFromLocal(submissionId) + for (const filePath of fileList) { store().saveSubmissionFile({ name: submissionId, file: { - fileName: slash(file), - data: request.response + fileName: slash(filePath), + data: this.getLocalFile(`files/${filePath}`) } }) - } else { - console.log('Error loading file: ' + file) } + } catch (e) { + console.log(e) } } diff --git a/report-viewer/src/model/factories/OverviewFactory.ts b/report-viewer/src/model/factories/OverviewFactory.ts index 849aa4957..5ea185584 100644 --- a/report-viewer/src/model/factories/OverviewFactory.ts +++ b/report-viewer/src/model/factories/OverviewFactory.ts @@ -5,46 +5,136 @@ import store from '@/stores/store' import type { Version } from '../Version' import versionJson from '@/version.json' import Distribution from '../Distribution' +import MetricType from '../MetricType' +import { BaseFactory } from './BaseFactory' +import HundredValueDistribution from '../HundredValueDistribution' +import TenValueDistribution from '../TenValueDistribution' -export class OverviewFactory { +/** + * Factory class for creating Overview objects + */ +export class OverviewFactory extends BaseFactory { static reportViewerVersion: Version = versionJson['report_viewer_version'] !== undefined ? versionJson['report_viewer_version'] : { major: -1, minor: -1, patch: -1 } + /** + * Gets the overview file based on the used mode (zip, local, single). + */ + public static getOverview(): Overview { + return this.extractOverview(JSON.parse(this.getFile('overview.json'))) + } + /** * Creates an overview object from a json object created by by JPlag * @param json the json object */ private static extractOverview(json: Record): Overview { - const versionField = json.jplag_version as Record - const jplagVersion: Version = { - major: versionField.major, - minor: versionField.minor, - patch: versionField.patch - } - + const jplagVersion = this.extractVersion(json) OverviewFactory.compareVersions(jplagVersion, this.reportViewerVersion) const submissionFolder = json.submission_folder_path as Array - const baseCodeFolder = '' + const baseCodeFolder = json.base_code_folder_path as string const language = json.language as string const fileExtensions = json.file_extensions as Array const matchSensitivity = json.match_sensitivity as number - const jsonSubmissions = json.submission_id_to_display_name as Map - const map = new Map(Object.entries(jsonSubmissions)) const dateOfExecution = json.date_of_execution as string const duration = json.execution_time as number as number - const clusters = [] as Array const totalComparisons = json.total_comparisons as number - const distributions = [] as Array + this.saveIdToDisplayNameMap(json) + this.saveComparisonFilesLookup(json) + + return new Overview( + submissionFolder, + baseCodeFolder, + language, + fileExtensions, + matchSensitivity, + dateOfExecution, + duration, + this.extractTopComparisons(json), + this.extractDistributions(json), + this.extractClusters(json), + totalComparisons + ) + } + + private static extractVersion(json: Record): Version { + const versionField = json.jplag_version as Record + return { + major: versionField.major, + minor: versionField.minor, + patch: versionField.patch + } + } + + private static extractDistributions( + json: Record + ): Record { + if (json.distributions) { + return this.extractDistributionsFromMap(json.distributions as Record>) + } else if (json.metrics) { + return this.extractDistributionsFromMetrics(json.metrics as Array>) + } + throw new Error('No distributions found') + } + + private static extractDistributionsFromMap( + distributionsMap: Record> + ): Record { + const distributions = {} as Record + for (const [key, value] of Object.entries(distributionsMap)) { + distributions[key as MetricType] = new HundredValueDistribution(value as Array) + } + return distributions + } + + /** @deprecated since 5.0.0. Use the new format with {@link extractDistributionsFromMap} */ + private static extractDistributionsFromMetrics( + metrics: Array> + ): Record { + return { + [MetricType.AVERAGE]: new TenValueDistribution(metrics[0].distribution as Array), + [MetricType.MAXIMUM]: new TenValueDistribution(metrics[1].distribution as Array) + } + } + + private static extractTopComparisons( + json: Record + ): Array { + if (json.top_comparisons) { + return this.extractTopComparisonsFromMap( + json.top_comparisons as Array> + ) + } else if (json.metrics) { + return this.extractTopComparisonsFromMetrics(json.metrics as Array>) + } + throw new Error('No top comparisons found') + } + + private static extractTopComparisonsFromMap(jsonComparisons: Array>) { + const comparisons = [] as Array + let counter = 0 + for (const topComparison of jsonComparisons) { + comparisons.push({ + sortingPlace: counter++, + id: counter, + firstSubmissionId: topComparison.first_submission as string, + secondSubmissionId: topComparison.second_submission as string, + similarities: topComparison.similarities as Record + }) + } + return comparisons + } + + /** @deprecated since 5.0.0. Use the new format with {@link extractTopComparisonsFromMap} */ + private static extractTopComparisonsFromMetrics(metrics: Array>) { const averageSimilarities: Map = new Map() const comparisons = [] as Array - const metrics = json.metrics as Array as Array> - // Average - distributions.push(new Distribution(metrics[0].distribution as Array)) + // Save the average similarities in a temporary map to combine them with the max similarities later for (const comparison of metrics[0].topComparisons as Array>) { averageSimilarities.set( (comparison.first_submission as string) + '-' + (comparison.second_submission as string), @@ -52,8 +142,7 @@ export class OverviewFactory { ) } - // Max - distributions.push(new Distribution(metrics[1].distribution as Array)) + // Extract the max similarities and combine them with the average similarities let counter = 0 for (const comparison of metrics[1].topComparisons as Array>) { const avg = averageSimilarities.get( @@ -64,90 +153,51 @@ export class OverviewFactory { id: counter, firstSubmissionId: comparison.first_submission as string, secondSubmissionId: comparison.second_submission as string, - averageSimilarity: avg as number, - maximumSimilarity: comparison.similarity as number + similarities: { + [MetricType.AVERAGE]: avg as number, + [MetricType.MAXIMUM]: comparison.similarity as number + } }) } - store().saveSubmissionNames(map) + return comparisons + } - if (json.clusters) { - ;(json.clusters as Array).forEach((jsonCluster) => { - const cluster = jsonCluster as Record - const newCluster: Cluster = { - averageSimilarity: cluster.average_similarity as number, - strength: cluster.strength as number, - members: cluster.members as Array - } - clusters.push(newCluster) + private static extractClusters(json: Record): Array { + if (!json.clusters) { + return [] + } + + const clusters = [] as Array + for (const jsonCluster of json.clusters as Array>) { + clusters.push({ + averageSimilarity: jsonCluster.average_similarity as number, + strength: jsonCluster.strength as number, + members: jsonCluster.members as Array }) } + return clusters + } - OverviewFactory.saveSubmissionsToComparisonNameMap(json) - return new Overview( - submissionFolder, - baseCodeFolder, - language, - fileExtensions, - matchSensitivity, - dateOfExecution, - duration, - comparisons, - distributions, - clusters, - totalComparisons, - new Map() - ) + private static saveIdToDisplayNameMap(json: Record) { + const jsonSubmissions = json.submission_id_to_display_name as Map + const map = new Map(Object.entries(jsonSubmissions)) + + store().saveSubmissionNames(map) } - /** - * Gets the overview file based on the used mode (zip, local, single). - */ - public static getOverview(): Overview { - console.log('Generating overview...') - let temp!: Overview - //Gets the overview file based on the used mode (zip, local, single). - if (store().state.localModeUsed) { - const request = new XMLHttpRequest() - request.open('GET', '/files/overview.json', false) - request.send() - - if (request.status == 200) { - temp = OverviewFactory.extractOverview(JSON.parse(request.response)) - } else { - throw new Error('Could not find overview.json in folder.') - } - } else if (store().state.zipModeUsed) { - console.log('Start finding overview.json in state...') - const index = Object.keys(store().state.files).find((name) => name.endsWith('overview.json')) - const overviewFile = - index != undefined - ? store().state.files[index] - : console.log('Could not find overview.json') - - if (overviewFile === undefined) { - console.log('Could not find overview.json') - return new Overview( - [], - '', - '', - [], - 0, - '', - 0, - [], - [], - [], - 0, - new Map>() - ) - } - const overviewJson = JSON.parse(overviewFile) - temp = OverviewFactory.extractOverview(overviewJson) - } else if (store().state.singleModeUsed) { - temp = OverviewFactory.extractOverview(JSON.parse(store().state.singleFillRawContent)) + private static saveComparisonFilesLookup(json: Record) { + const submissionIdsToComparisonName = json.submission_ids_to_comparison_file_name as Map< + string, + Map + > + const test: Array> = Object.entries(submissionIdsToComparisonName) + const comparisonMap = new Map>() + for (const [key, value] of test) { + comparisonMap.set(key as string, new Map(Object.entries(value as object))) } - return temp + + store().saveComparisonFileLookup(comparisonMap) } /** @@ -204,18 +254,4 @@ export class OverviewFactory { sessionStorage.setItem('versionAlert', 'true') } } - - private static saveSubmissionsToComparisonNameMap(json: Record) { - const submissionIdsToComparisonName = json.submission_ids_to_comparison_file_name as Map< - string, - Map - > - const test: Array> = Object.entries(submissionIdsToComparisonName) - const comparisonMap = new Map>() - for (const [key, value] of test) { - comparisonMap.set(key as string, new Map(Object.entries(value as object))) - } - - store().saveComparisonFileLookup(comparisonMap) - } } diff --git a/report-viewer/src/views/ClusterView.vue b/report-viewer/src/views/ClusterView.vue index b0faca718..c47cdf038 100644 --- a/report-viewer/src/views/ClusterView.vue +++ b/report-viewer/src/views/ClusterView.vue @@ -30,6 +30,7 @@ import Container from '@/components/ContainerComponent.vue' import TextInformation from '@/components/TextInformation.vue' import type { ClusterListElement, ClusterListElementMember } from '@/model/ClusterListElement' import type { ComparisonListElement } from '@/model/ComparisonListElement' +import MetricType from '@/model/MetricType' import { OverviewFactory } from '@/model/factories/OverviewFactory' const props = defineProps({ @@ -43,6 +44,7 @@ const overview = OverviewFactory.getOverview() const cluster = overview.clusters[props.clusterIndex] const comparisons = [] as Array const clusterMemberList = new Map() as ClusterListElementMember +const usedMetric = MetricType.AVERAGE function getComparisonFor(id1: string, id2: string) { return overview.topComparisons.find( @@ -62,7 +64,7 @@ for (let i = 0; i < cluster.members.length; i++) { } let counter = 0 comparisons - .sort((a, b) => b.averageSimilarity - a.averageSimilarity) + .sort((a, b) => b.similarities[usedMetric] - a.similarities[usedMetric]) .forEach((c) => { c.sortingPlace = counter++ c.id = counter @@ -75,7 +77,7 @@ for (const member of cluster.members) { .forEach((c) => { membersComparisons.push({ matchedWith: c.firstSubmissionId === member ? c.secondSubmissionId : c.firstSubmissionId, - similarity: c.averageSimilarity + similarity: c.similarities[usedMetric] }) }) clusterMemberList.set(member, membersComparisons) diff --git a/report-viewer/src/views/ComparisonView.vue b/report-viewer/src/views/ComparisonView.vue index 2f046364a..5b51b8047 100644 --- a/report-viewer/src/views/ComparisonView.vue +++ b/report-viewer/src/views/ComparisonView.vue @@ -21,7 +21,7 @@
{{ (comparison.similarity * 100).toFixed(2) }}%{{ (comparison.similarities[MetricType.AVERAGE] * 100).toFixed(2) }}%
@@ -60,7 +62,9 @@ b.maximumSimilarity - a.maximumSimilarity) - } else { - comparisons.sort((a, b) => b.averageSimilarity - a.averageSimilarity) - } + comparisons.sort( + (a, b) => + b.similarities[comparisonTableSortingMetric.value] - + a.similarities[comparisonTableSortingMetric.value] + ) let index = 0 comparisons.forEach((c) => { c.sortingPlace = index++ @@ -172,12 +176,12 @@ function changeAnnoymousForAll() { const selectedDistributionDiagramMetric = ref(MetricType.AVERAGE) -/** - * Switch between metrics - * @param metric Metric to switch to - */ -function selectDistributionDiagramMetric(metric: number) { - selectedDistributionDiagramMetric.value = metric +function getMetricFromNumber(metric: number) { + if (metric == 0) { + return MetricType.AVERAGE + } else { + return MetricType.MAXIMUM + } } const hasMoreSubmissionPaths = overview.submissionFolderPath.length > 1 diff --git a/report-viewer/tests/unit/model/Distribution.test.ts b/report-viewer/tests/unit/model/Distribution.test.ts new file mode 100644 index 000000000..1d8b6fa65 --- /dev/null +++ b/report-viewer/tests/unit/model/Distribution.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from 'vitest' +import Distribution from '@/model/Distribution' +import TenValueDistribution from '@/model/TenValueDistribution' +import HundredValueDistribution from '@/model/HundredValueDistribution' + +describe('Distribution', () => { + it.each([ + new TenValueDistribution([0, 0, 0, 0, 0, 0, 26, 13209, 58955, 5231]), + new HundredValueDistribution([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 2, 1, 0, 1, 2, 12, 8, 31, 61, 168, 273, 493, 923, 1544, 2244, 3163, 4309, 5373, 6343, 7177, + 7445, 7292, 7023, 6130, 5091, 4056, 3025, 2052, 1442, 869, 470, 225, 109, 42, 15, 7, 0 + ]) + ])('get in 10 Buckets', (distribution: Distribution) => { + expect(distribution.splitIntoTenBuckets()).toEqual([0, 0, 0, 0, 0, 0, 26, 13209, 58955, 5231]) + }) +}) diff --git a/report-viewer/tests/unit/model/factories/ComparisonFactory.test.ts b/report-viewer/tests/unit/model/factories/ComparisonFactory.test.ts new file mode 100644 index 000000000..7a126d76d --- /dev/null +++ b/report-viewer/tests/unit/model/factories/ComparisonFactory.test.ts @@ -0,0 +1,74 @@ +import { vi, it, beforeAll, describe, expect } from 'vitest' +import validNew from './ValidNewComparison.json' +import validOld from './ValidOldComparison.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 [ + { + name: `${name}/Structure.java`, + value: '' + }, + { + name: `${name}/Submission.java`, + value: '' + } + ] + } +} + +describe('Test JSON to Comparison', () => { + beforeAll(() => { + vi.mock('@/stores/store', () => ({ + default: vi.fn(() => { + return store + }) + })) + }) + + it('Post 5.0', () => { + store.state.files['root1-root2.json'] = JSON.stringify(validNew) + + const result = ComparisonFactory.getComparison('root1', 'root2') + + expect(result).toBeDefined() + expect(result.firstSubmissionId).toBe('root1') + expect(result.secondSubmissionId).toBe('root2') + expect(result.similarities[MetricType.AVERAGE]).toBe(0.6900452488687783) + expect(result.similarities[MetricType.MAXIMUM]).toBe(0.9936000000000001) + expect(result.filesOfFirstSubmission).toBeDefined() + expect(result.filesOfSecondSubmission).toBeDefined() + expect(result.allMatches.length).toBe(4) + expect(result.matchesInFirstSubmission.size).toBe(2) + expect(result.matchesInSecondSubmissions.size).toBe(2) + }) + + it('Pre 5.0', () => { + store.state.files['root1-root2.json'] = JSON.stringify(validOld) + + const result = ComparisonFactory.getComparison('root1', 'root2') + + expect(result).toBeDefined() + expect(result.firstSubmissionId).toBe('root1') + expect(result.secondSubmissionId).toBe('root2') + expect(result.similarities[MetricType.AVERAGE]).toBe(0.6900452488687783) + expect(result.similarities[MetricType.MAXIMUM]).toBe(Number.NaN) + expect(result.filesOfFirstSubmission).toBeDefined() + expect(result.filesOfSecondSubmission).toBeDefined() + expect(result.allMatches.length).toBe(4) + expect(result.matchesInFirstSubmission.size).toBe(2) + expect(result.matchesInSecondSubmissions.size).toBe(2) + }) +}) diff --git a/report-viewer/tests/unit/model/factories/OverviewFactory.test.ts b/report-viewer/tests/unit/model/factories/OverviewFactory.test.ts new file mode 100644 index 000000000..5385d697a --- /dev/null +++ b/report-viewer/tests/unit/model/factories/OverviewFactory.test.ts @@ -0,0 +1,177 @@ +import { beforeAll, describe, expect, it, vi } from 'vitest' +import { OverviewFactory } from '@/model/factories/OverviewFactory' +import MetricType from '@/model/MetricType' +import HundredValueDistribution from '@/model/HundredValueDistribution' +import TenValueDistribution from '@/model/TenValueDistribution' +import validNew from './ValidNewOverview.json' +import validOld from './ValidOldOverview.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() + } +} + +describe('Test JSON to Overview', () => { + beforeAll(() => { + vi.mock('@/stores/store', () => ({ + default: vi.fn(() => { + return store + }) + })) + + vi.spyOn(global.window, 'alert').mockImplementation(() => {}) + }) + + it('Post 5.0', () => { + store.state.files['overview.json'] = JSON.stringify(validNew) + + expect(OverviewFactory.getOverview()).toEqual({ + _submissionFolderPath: ['files'], + _baseCodeFolderPath: '', + _language: 'Javac based AST plugin', + _fileExtensions: ['.java', '.JAVA'], + _matchSensitivity: 9, + _dateOfExecution: '12/07/23', + _durationOfExecution: 12, + _topComparisons: [ + { + firstSubmissionId: 'A', + secondSubmissionId: 'C', + similarities: { + [MetricType.AVERAGE]: 0.9960435212660732, + [MetricType.MAXIMUM]: 0.9960435212660732 + }, + sortingPlace: 0, + id: 1 + }, + { + firstSubmissionId: 'D', + secondSubmissionId: 'A', + similarities: { + [MetricType.AVERAGE]: 0.751044776119403, + [MetricType.MAXIMUM]: 0.947289156626506 + }, + sortingPlace: 1, + id: 2 + }, + { + firstSubmissionId: 'D', + secondSubmissionId: 'C', + similarities: { + [MetricType.AVERAGE]: 0.751044776119403, + [MetricType.MAXIMUM]: 0.947289156626506 + }, + sortingPlace: 2, + id: 3 + }, + { + firstSubmissionId: 'B', + secondSubmissionId: 'D', + similarities: { + [MetricType.AVERAGE]: 0.28322981366459626, + [MetricType.MAXIMUM]: 0.8085106382978723 + }, + sortingPlace: 3, + id: 4 + }, + { + firstSubmissionId: 'B', + secondSubmissionId: 'A', + similarities: { + [MetricType.AVERAGE]: 0.2378472222222222, + [MetricType.MAXIMUM]: 0.9716312056737588 + }, + sortingPlace: 4, + id: 5 + }, + { + firstSubmissionId: 'B', + secondSubmissionId: 'C', + similarities: { + [MetricType.AVERAGE]: 0.2378472222222222, + [MetricType.MAXIMUM]: 0.9716312056737588 + }, + sortingPlace: 5, + id: 6 + } + ], + _distributions: { + [MetricType.MAXIMUM]: new HundredValueDistribution([ + 1, 0, 2, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ]), + [MetricType.AVERAGE]: new HundredValueDistribution([ + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ]) + }, + _clusters: [ + { + averageSimilarity: 94.746956, + strength: 0.0, + members: ['C', 'A', 'B', 'D'] + } + ], + _totalComparisons: 6 + }) + }) + + it('Pre 5.0', () => { + store.state.files['overview.json'] = JSON.stringify(validOld) + expect(OverviewFactory.getOverview()).toEqual({ + _submissionFolderPath: ['test'], + _baseCodeFolderPath: '', + _language: 'Javac based AST plugin', + _fileExtensions: ['.java', '.JAVA'], + _matchSensitivity: 9, + _dateOfExecution: '12/07/23', + _durationOfExecution: 34, + _topComparisons: [ + { + firstSubmissionId: 'A', + secondSubmissionId: 'B', + similarities: { + [MetricType.AVERAGE]: 0.6900452488687783, + [MetricType.MAXIMUM]: 0.9457364341085271 + }, + sortingPlace: 0, + id: 1 + }, + { + firstSubmissionId: 'C', + secondSubmissionId: 'D', + similarities: { + [MetricType.AVERAGE]: 0.6954045248868778, + [MetricType.MAXIMUM]: 0.83500530023 + }, + sortingPlace: 1, + id: 2 + } + ], + _distributions: { + [MetricType.AVERAGE]: new TenValueDistribution([0, 0, 0, 1, 0, 0, 0, 0, 0, 0]), + [MetricType.MAXIMUM]: new TenValueDistribution([1, 0, 0, 0, 0, 0, 0, 0, 0, 0]) + }, + _clusters: [], + _totalComparisons: 6 + }) + }) +}) diff --git a/report-viewer/tests/unit/model/factories/ValidNewComparison.json b/report-viewer/tests/unit/model/factories/ValidNewComparison.json new file mode 100644 index 000000000..fe8533d49 --- /dev/null +++ b/report-viewer/tests/unit/model/factories/ValidNewComparison.json @@ -0,0 +1,48 @@ +{ + "id1": "root1", + "id2": "root2", + "similarities": { + "AVG": 0.6900452488687783, + "MAX": 0.9936000000000001 + }, + "matches": [ + { + "file1": "root2\\Structure.java", + "file2": "root1\\Structure.java", + "start1": 3, + "end1": 120, + "start2": 1, + "end2": 118, + "tokens": 139 + }, + { + "file1": "root2\\Submission.java", + "file2": "root1\\Submission.java", + "start1": 129, + "end1": 155, + "start2": 134, + "end2": 160, + "tokens": 34 + }, + { + "file1": "root2\\Submission.java", + "file2": "root1\\Submission.java", + "start1": 165, + "end1": 194, + "start2": 173, + "end2": 203, + "tokens": 33 + }, + + { + "file1": "root2\\Submission.java", + "file2": "root1\\Submission.java", + "start1": 112, + "end1": 127, + "start2": 116, + "end2": 132, + "tokens": 23 + } + ] + } + \ No newline at end of file diff --git a/report-viewer/tests/unit/model/factories/ValidNewOverview.json b/report-viewer/tests/unit/model/factories/ValidNewOverview.json new file mode 100644 index 000000000..8fb36cc51 --- /dev/null +++ b/report-viewer/tests/unit/model/factories/ValidNewOverview.json @@ -0,0 +1,79 @@ +{ + "jplag_version": { "major": 0, "minor": 0, "patch": 0 }, + "submission_folder_path": ["files"], + "base_code_folder_path": "", + "language": "Javac based AST plugin", + "file_extensions": [".java", ".JAVA"], + "submission_id_to_display_name": { "A": "A", "B": "B", "C": "C", "D": "D" }, + "submission_ids_to_comparison_file_name": { + "A": { "B": "B-A.json", "C": "A-C.json", "D": "D-A.json" }, + "B": { "A": "B-A.json", "C": "B-C.json", "D": "B-D.json" }, + "C": { "A": "A-C.json", "B": "B-C.json", "D": "D-C.json" }, + "D": { "A": "D-A.json", "B": "B-D.json", "C": "D-C.json" } + }, + "failed_submission_names": [], + "excluded_files": [], + "match_sensitivity": 9, + "date_of_execution": "12/07/23", + "execution_time": 12, + "distributions": { + "MAX": [ + 1, 0, 2, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ], + "AVG": [ + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ] + }, + "top_comparisons": [ + { + "first_submission": "A", + "second_submission": "C", + "similarities": { "AVG": 0.9960435212660732, "MAX": 0.9960435212660732 } + }, + { + "first_submission": "D", + "second_submission": "A", + "similarities": { "AVG": 0.751044776119403, "MAX": 0.947289156626506 } + }, + { + "first_submission": "D", + "second_submission": "C", + "similarities": { "AVG": 0.751044776119403, "MAX": 0.947289156626506 } + }, + { + "first_submission": "B", + "second_submission": "D", + "similarities": { "AVG": 0.28322981366459626, "MAX": 0.8085106382978723 } + }, + { + "first_submission": "B", + "second_submission": "A", + "similarities": { "AVG": 0.2378472222222222, "MAX": 0.9716312056737588 } + }, + { + "first_submission": "B", + "second_submission": "C", + "similarities": { "AVG": 0.2378472222222222, "MAX": 0.9716312056737588 } + } + ], + "clusters": [ + { + "average_similarity": 94.746956, + "strength": 0.0, + "members": [ + "C", + "A", + "B", + "D" + ] + } + ], + "total_comparisons": 6 + } + \ No newline at end of file diff --git a/report-viewer/tests/unit/model/factories/ValidOldComparison.json b/report-viewer/tests/unit/model/factories/ValidOldComparison.json new file mode 100644 index 000000000..3f2394af2 --- /dev/null +++ b/report-viewer/tests/unit/model/factories/ValidOldComparison.json @@ -0,0 +1,45 @@ +{ + "id1": "root1", + "id2": "root2", + "similarity": 0.6900452488687783, + "matches": [ + { + "file1": "root2\\Structure.java", + "file2": "root1\\Structure.java", + "start1": 3, + "end1": 120, + "start2": 1, + "end2": 118, + "tokens": 139 + }, + { + "file1": "root2\\Submission.java", + "file2": "root1\\Submission.java", + "start1": 129, + "end1": 155, + "start2": 134, + "end2": 160, + "tokens": 34 + }, + { + "file1": "root2\\Submission.java", + "file2": "root1\\Submission.java", + "start1": 165, + "end1": 194, + "start2": 173, + "end2": 203, + "tokens": 33 + }, + + { + "file1": "root2\\Submission.java", + "file2": "root1\\Submission.java", + "start1": 112, + "end1": 127, + "start2": 116, + "end2": 132, + "tokens": 23 + } + ] + } + \ No newline at end of file diff --git a/report-viewer/tests/unit/model/factories/ValidOldOverview.json b/report-viewer/tests/unit/model/factories/ValidOldOverview.json new file mode 100644 index 000000000..981fca99f --- /dev/null +++ b/report-viewer/tests/unit/model/factories/ValidOldOverview.json @@ -0,0 +1,56 @@ +{ + "jplag_version": { "major": 4, "minor": 2, "patch": 0 }, + "submission_folder_path": ["test"], + "base_code_folder_path": "", + "language": "Javac based AST plugin", + "file_extensions": [".java", ".JAVA"], + "submission_id_to_display_name": { "A": "A", "B": "B", "C": "C", "D": "D" }, + "submission_ids_to_comparison_file_name": { + "A": { "B": "B-A.json" }, + "B": { "A": "B-A.json" } + }, + "failed_submission_names": [], + "excluded_files": [], + "match_sensitivity": 9, + "date_of_execution": "12/07/23", + "execution_time": 34, + "metrics": [ + { + "name": "AVG", + "distribution": [0, 0, 0, 1, 0, 0, 0, 0, 0, 0], + "topComparisons": [ + { + "first_submission": "A", + "second_submission": "B", + "similarity": 0.6900452488687783 + }, + { + "first_submission": "C", + "second_submission": "D", + "similarity": 0.6954045248868778 + } + ], + "description": "Average of both program coverages. This is the default similarity which works in most cases: Matches with a high average similarity indicate that the programs work in a very similar way." + }, + { + "name": "MAX", + "distribution": [1, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "topComparisons": [ + { + "first_submission": "A", + "second_submission": "B", + "similarity": 0.9457364341085271 + }, + { + "first_submission": "C", + "second_submission": "D", + "similarity": 0.83500530023 + } + ], + "description": "Maximum of both program coverages. This ranking is especially useful if the programs are very different in size. This can happen when dead code was inserted to disguise the origin of the plagiarized program." + } + ], + "clusters": [], + "total_comparisons": 6 + } + \ No newline at end of file diff --git a/report-viewer/vitest.config.ts b/report-viewer/vitest.config.ts index 1a9b1be61..b96e93fb8 100644 --- a/report-viewer/vitest.config.ts +++ b/report-viewer/vitest.config.ts @@ -6,5 +6,10 @@ export default defineConfig({ environment: 'jsdom', exclude: [...configDefaults.exclude, 'tests/e2e/*'], root: fileURLToPath(new URL('./', import.meta.url)) + }, + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)) + } } })