From dd333ac166d2c8a17560b4826e9bc3fc9815d031 Mon Sep 17 00:00:00 2001 From: Alex | Kronox Date: Thu, 7 Nov 2024 16:52:28 +0100 Subject: [PATCH 01/11] include new metrics in report --- .../java/de/jplag/options/SimilarityMetric.java | 13 ++++++++++++- .../jsonfactory/ComparisonReportWriter.java | 12 ++++++++++-- .../reporting/reportobject/mapper/MetricMapper.java | 6 +++++- .../de/jplag/endtoend/model/ExpectedResult.java | 1 + 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/de/jplag/options/SimilarityMetric.java b/core/src/main/java/de/jplag/options/SimilarityMetric.java index 0f38cb83d8..08d00f78d0 100644 --- a/core/src/main/java/de/jplag/options/SimilarityMetric.java +++ b/core/src/main/java/de/jplag/options/SimilarityMetric.java @@ -3,12 +3,23 @@ import java.util.function.ToDoubleFunction; import de.jplag.JPlagComparison; +import de.jplag.Match; public enum SimilarityMetric implements ToDoubleFunction { AVG("average similarity", JPlagComparison::similarity), MIN("minimum similarity", JPlagComparison::minimalSimilarity), MAX("maximal similarity", JPlagComparison::maximalSimilarity), - INTERSECTION("matched tokens", it -> (double) it.getNumberOfMatchedTokens()); + INTERSECTION("matched tokens", it -> (double) it.getNumberOfMatchedTokens()), + SYMMETRIC("symmetric similarity", it -> { + int divisor = it.firstSubmission().getNumberOfTokens() + it.secondSubmission().getNumberOfTokens(); + if (divisor != 0) { + return 2.0 * it.getNumberOfMatchedTokens() / divisor; + } else { + return .0; + } + }), + LONGEST_MATCH("number of tokens in the longest match", it -> it.matches().stream().mapToInt(Match::length).max().orElse(0)), + OVERALL("Sum of both submission lengths", it -> it.firstSubmission().getNumberOfTokens() + it.secondSubmission().getNumberOfTokens()); private final ToDoubleFunction similarityFunction; private final String description; 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 35ef87de59..832289a477 100644 --- a/core/src/main/java/de/jplag/reporting/jsonfactory/ComparisonReportWriter.java +++ b/core/src/main/java/de/jplag/reporting/jsonfactory/ComparisonReportWriter.java @@ -2,6 +2,7 @@ import java.nio.file.Path; import java.util.Comparator; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -57,13 +58,20 @@ private void writeComparisons(List comparisons) { String secondSubmissionId = submissionToIdFunction.apply(comparison.secondSubmission()); String fileName = generateComparisonName(firstSubmissionId, secondSubmissionId); addToLookUp(firstSubmissionId, secondSubmissionId, fileName); - var comparisonReport = new ComparisonReport(firstSubmissionId, secondSubmissionId, - Map.of(SimilarityMetric.AVG.name(), comparison.similarity(), SimilarityMetric.MAX.name(), comparison.maximalSimilarity()), + var comparisonReport = new ComparisonReport(firstSubmissionId, secondSubmissionId, createSimilarityMap(comparison), convertMatchesToReportMatches(comparison), comparison.similarityOfFirst(), comparison.similarityOfSecond()); resultWriter.addJsonEntry(comparisonReport, Path.of(fileName)); } } + private Map createSimilarityMap(JPlagComparison comparison) { + Map result = new HashMap<>(); + for (SimilarityMetric metric : SimilarityMetric.values()) { + result.put(metric.name(), metric.applyAsDouble(comparison)); + } + return result; + } + private void addToLookUp(String firstSubmissionId, String secondSubmissionId, String fileName) { writeToMap(secondSubmissionId, firstSubmissionId, fileName); writeToMap(firstSubmissionId, secondSubmissionId, fileName); 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 0868cf20ac..890f0ff374 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 @@ -47,6 +47,10 @@ public List getTopComparisons(JPlagResult result) { } private Map getComparisonMetricMap(JPlagComparison comparison) { - return Map.of(SimilarityMetric.AVG.name(), comparison.similarity(), SimilarityMetric.MAX.name(), comparison.maximalSimilarity()); + Map metricMap = new HashMap<>(); + for (SimilarityMetric metric : SimilarityMetric.values()) { + metricMap.put(metric.name(), metric.applyAsDouble(comparison)); + } + return metricMap; } } diff --git a/endtoend-testing/src/main/java/de/jplag/endtoend/model/ExpectedResult.java b/endtoend-testing/src/main/java/de/jplag/endtoend/model/ExpectedResult.java index ff2408adb5..10b2b8dc47 100644 --- a/endtoend-testing/src/main/java/de/jplag/endtoend/model/ExpectedResult.java +++ b/endtoend-testing/src/main/java/de/jplag/endtoend/model/ExpectedResult.java @@ -24,6 +24,7 @@ public double getSimilarityForMetric(SimilarityMetric metric) { case MIN -> resultSimilarityMinimum(); case MAX -> resultSimilarityMaximum(); case INTERSECTION -> resultMatchedTokenNumber(); + default -> throw new IllegalArgumentException(String.format("Similarity metric %s not supported for end to end tests", metric.name())); }; } From af988a1699a0c108f0216266efb900d057a0c1a8 Mon Sep 17 00:00:00 2001 From: Alex | Kronox Date: Thu, 7 Nov 2024 21:09:11 +0100 Subject: [PATCH 02/11] update report viewer to new metric system --- .../src/components/ComparisonTableFilter.vue | 16 +- .../src/components/ComparisonsTable.vue | 48 +++-- .../DistributionDiagram.vue | 4 +- .../DistributionDiagramOptions.vue | 5 +- .../optionsSelectors/MetricSelector.vue | 14 +- report-viewer/src/model/Comparison.ts | 5 +- .../src/model/ComparisonListElement.ts | 4 +- report-viewer/src/model/MetricType.ts | 164 +++++++++++++++--- report-viewer/src/model/Overview.ts | 6 +- .../src/model/factories/ComparisonFactory.ts | 10 +- .../src/model/factories/OptionsFactory.ts | 10 +- .../src/model/factories/OverviewFactory.ts | 10 +- .../src/model/ui/DistributionChartConfig.ts | 4 +- report-viewer/src/stores/state.ts | 4 +- report-viewer/src/stores/store.ts | 6 +- report-viewer/src/views/ClusterView.vue | 10 +- report-viewer/src/views/ComparisonView.vue | 10 +- report-viewer/src/views/InformationView.vue | 5 +- 18 files changed, 239 insertions(+), 96 deletions(-) diff --git a/report-viewer/src/components/ComparisonTableFilter.vue b/report-viewer/src/components/ComparisonTableFilter.vue index b2e47a29b7..5e35d13b59 100644 --- a/report-viewer/src/components/ComparisonTableFilter.vue +++ b/report-viewer/src/components/ComparisonTableFilter.vue @@ -45,7 +45,7 @@ import ToolTipComponent from './ToolTipComponent.vue' import ButtonComponent from './ButtonComponent.vue' import OptionsSelector from './optionsSelectors/OptionsSelectorComponent.vue' import { store } from '@/stores/store' -import { MetricType, metricToolTips } from '@/model/MetricType' +import { MetricJsonIdentifier, MetricTypes } from '@/model/MetricType' import type { ToolTipLabel } from '@/model/ui/ToolTip' const props = defineProps({ @@ -93,7 +93,9 @@ const searchStringValue = computed({ function changeSortingMetric(index: number) { store().uiState.comparisonTableSortingMetric = - index < tableSortingMetricOptions.length ? tableSortingMetricOptions[index] : MetricType.AVERAGE + index < tableSortingMetricOptions.length + ? tableSortingMetricOptions[index].identifier + : MetricJsonIdentifier.AVERAGE_SIMILARITY store().uiState.comparisonTableClusterSorting = tableSortingOptions.value[index] == 'Cluster' } @@ -101,15 +103,17 @@ function getSortingMetric() { if (store().uiState.comparisonTableClusterSorting && props.enableClusterSorting) { return tableSortingOptions.value.indexOf('Cluster') } - return tableSortingMetricOptions.indexOf(store().uiState.comparisonTableSortingMetric) + return tableSortingMetricOptions.findIndex( + (m) => m.identifier == store().uiState.comparisonTableSortingMetric + ) } -const tableSortingMetricOptions = [MetricType.AVERAGE, MetricType.MAXIMUM] +const tableSortingMetricOptions = MetricTypes.METRIC_LIST const tableSortingOptions = computed(() => { const options: (ToolTipLabel | string)[] = tableSortingMetricOptions.map((metric) => { return { - displayValue: metricToolTips[metric].longName, - tooltip: metricToolTips[metric].tooltip + displayValue: metric.longName, + tooltip: metric.tooltip } }) if (props.enableClusterSorting) { diff --git a/report-viewer/src/components/ComparisonsTable.vue b/report-viewer/src/components/ComparisonsTable.vue index d1856c7708..4fdf1cf307 100644 --- a/report-viewer/src/components/ComparisonsTable.vue +++ b/report-viewer/src/components/ComparisonsTable.vue @@ -21,12 +21,12 @@ @@ -34,12 +34,12 @@ @@ -102,10 +102,18 @@
- {{ (item.similarities[MetricType.AVERAGE] * 100).toFixed(2) }}% + {{ + MetricTypes.AVERAGE_SIMILARITY.format( + item.similarities[MetricTypes.AVERAGE_SIMILARITY.shortName] + ) + }}
- {{ (item.similarities[MetricType.MAXIMUM] * 100).toFixed(2) }}% + {{ + MetricTypes.MAXIMUM_SIMILARITY.format( + item.similarities[MetricTypes.MAXIMUM_SIMILARITY.shortName] + ) + }}
@@ -175,7 +183,7 @@ import { library } from '@fortawesome/fontawesome-svg-core' import { faUserGroup } from '@fortawesome/free-solid-svg-icons' import { generateHues } from '@/utils/ColorUtils' import ToolTipComponent from './ToolTipComponent.vue' -import { MetricType, metricToolTips } from '@/model/MetricType' +import { MetricJsonIdentifier, MetricTypes } from '@/model/MetricType' import NameElement from './NameElement.vue' import ComparisonTableFilter from './ComparisonTableFilter.vue' @@ -251,28 +259,32 @@ function getFilteredComparisons(comparisons: ComparisonListElement[]) { } // metric search - const searchPerMetric: Record = { - [MetricType.AVERAGE]: [], - [MetricType.MAXIMUM]: [] - } + const searchPerMetric: Record = {} as Record< + MetricJsonIdentifier, + string[] + > + MetricTypes.METRIC_JSON_IDENTIFIERS.forEach((m) => { + searchPerMetric[m] = [] + }) metricSearches.forEach((s) => { const regexResult = /^(?:(avg|max):)([<>]=?[0-9]+%?$)/.exec(s) if (regexResult) { const metricName = regexResult[1] - let metric = MetricType.AVERAGE - for (const m of [MetricType.AVERAGE, MetricType.MAXIMUM]) { - if (metricToolTips[m].shortName.toLowerCase() == metricName) { + let metric = MetricTypes.AVERAGE_SIMILARITY + for (const m of MetricTypes.METRIC_LIST) { + if (m.shortName.toLowerCase() == metricName) { metric = m break } } - searchPerMetric[metric].push(regexResult[2]) + searchPerMetric[metric.identifier].push(regexResult[2]) } else { - searchPerMetric[MetricType.AVERAGE].push(s) - searchPerMetric[MetricType.MAXIMUM].push(s) + MetricTypes.METRIC_JSON_IDENTIFIERS.forEach((m) => { + searchPerMetric[m].push(s) + }) } }) - for (const metric of [MetricType.AVERAGE, MetricType.MAXIMUM]) { + for (const metric of MetricTypes.METRIC_JSON_IDENTIFIERS) { for (const search of searchPerMetric[metric]) { const regexResult = /([<>]=?)([0-9]+)%?/.exec(search)! const operator = regexResult[1] diff --git a/report-viewer/src/components/distributionDiagram/DistributionDiagram.vue b/report-viewer/src/components/distributionDiagram/DistributionDiagram.vue index 65ee66ed85..1e762854a2 100644 --- a/report-viewer/src/components/distributionDiagram/DistributionDiagram.vue +++ b/report-viewer/src/components/distributionDiagram/DistributionDiagram.vue @@ -18,16 +18,16 @@ import { Chart, registerables } from 'chart.js' import ChartDataLabels from 'chartjs-plugin-datalabels' import { graphColors } from '@/utils/ColorUtils' import type { Distribution } from '@/model/Distribution' -import { MetricType } from '@/model/MetricType' import { store } from '@/stores/store' import DistributionDiagramOptions from './DistributionDiagramOptions.vue' +import type { MetricJsonIdentifier } from '@/model/MetricType' Chart.register(...registerables) Chart.register(ChartDataLabels) const props = defineProps({ distributions: { - type: Object as PropType>, + type: Object as PropType>, required: true }, xScale: { diff --git a/report-viewer/src/components/distributionDiagram/DistributionDiagramOptions.vue b/report-viewer/src/components/distributionDiagram/DistributionDiagramOptions.vue index ffb52474b3..81a2b24b26 100644 --- a/report-viewer/src/components/distributionDiagram/DistributionDiagramOptions.vue +++ b/report-viewer/src/components/distributionDiagram/DistributionDiagramOptions.vue @@ -7,7 +7,8 @@ title="Metric:" :defaultSelected="store().uiState.distributionChartConfig.metric" @selection-changed=" - (metric: MetricType) => (store().uiState.distributionChartConfig.metric = metric) + (metric: MetricJsonIdentifier) => + (store().uiState.distributionChartConfig.metric = metric) " /> diff --git a/report-viewer/src/model/Comparison.ts b/report-viewer/src/model/Comparison.ts index 1c84d7c4f5..df51f8a932 100644 --- a/report-viewer/src/model/Comparison.ts +++ b/report-viewer/src/model/Comparison.ts @@ -1,7 +1,6 @@ import type { Match } from './Match' import type { SubmissionFile } from '@/model/File' import { MatchInSingleFile } from './MatchInSingleFile' -import type { MetricType } from './MetricType' /** * Comparison model used by the ComparisonView @@ -9,7 +8,7 @@ import type { MetricType } from './MetricType' export class Comparison { private readonly _firstSubmissionId: string private readonly _secondSubmissionId: string - private readonly _similarities: Record + private readonly _similarities: Record private readonly _filesOfFirstSubmission: SubmissionFile[] private readonly _filesOfSecondSubmission: SubmissionFile[] private readonly _allMatches: Array @@ -19,7 +18,7 @@ export class Comparison { constructor( firstSubmissionId: string, secondSubmissionId: string, - similarities: Record, + similarities: Record, filesOfFirstSubmission: SubmissionFile[], filesOfSecondSubmission: SubmissionFile[], allMatches: Array, diff --git a/report-viewer/src/model/ComparisonListElement.ts b/report-viewer/src/model/ComparisonListElement.ts index 4c7b767dcf..8a98128b27 100644 --- a/report-viewer/src/model/ComparisonListElement.ts +++ b/report-viewer/src/model/ComparisonListElement.ts @@ -1,4 +1,4 @@ -import type { MetricType } from './MetricType' +import type { MetricJsonIdentifier } from './MetricType' /** * Comparison model used by the Comparison Table in Overview. Only the needed attributes to display are included. @@ -16,6 +16,6 @@ export type ComparisonListElement = { id: number firstSubmissionId: string secondSubmissionId: string - similarities: Record + similarities: Record clusterIndex: number } diff --git a/report-viewer/src/model/MetricType.ts b/report-viewer/src/model/MetricType.ts index 5fc4897be1..9dfa7c7732 100644 --- a/report-viewer/src/model/MetricType.ts +++ b/report-viewer/src/model/MetricType.ts @@ -1,28 +1,148 @@ -/** - * This enum maps the metric type to the index they have in the generated JSON and respectively in the store. - */ -export enum MetricType { - AVERAGE = 'AVG', - MAXIMUM = 'MAX' +export enum MetricJsonIdentifier { + AVERAGE_SIMILARITY = 'AVG', + MAXIMUM_SIMILARITY = 'MAX', + MINIMUM_SIMILARITY = 'MIN', + INTERSECTION = 'INTERSECTION', + SYMMETRIC = 'SYMMETRIC', + LONGEST_MATCH = 'LONGEST_MATCH', + OVERALL = 'OVERALL' } -export type MetricToolTipData = { - longName: string - shortName: string - tooltip: string +export abstract class MetricType { + private readonly _shortName: string + private readonly _longName: string + private readonly _tooltip: string + private readonly _identifier: MetricJsonIdentifier + + constructor( + shortName: string, + longName: string, + tooltip: string, + identifier: MetricJsonIdentifier + ) { + this._shortName = shortName + this._longName = longName + this._tooltip = tooltip + this._identifier = identifier + } + + get shortName() { + return this._shortName + } + + get longName() { + return this._longName + } + + get tooltip() { + return this._tooltip + } + + get identifier() { + return this._identifier + } + + abstract format(value: number): string } -export const metricToolTips: Record = { - [MetricType.AVERAGE]: { - longName: 'Average Similarity', - shortName: 'AVG', - tooltip: - 'The average similarity of the two files.\nA high similarity indicates that the programms work in a similar way.' - }, - [MetricType.MAXIMUM]: { - longName: 'Maximum Similarity', - shortName: 'MAX', - tooltip: - 'The maximum similarity of the two files.\nUseful if programms are very different in size.' +class IdentityMetricType extends MetricType { + constructor( + shortName: string, + longName: string, + tooltip: string, + identifier: MetricJsonIdentifier + ) { + super(shortName, longName, tooltip, identifier) + } + + format(value: number): string { + return value.toString() + } +} + +class PercentageMetricType extends MetricType { + constructor( + shortName: string, + longName: string, + tooltip: string, + identifier: MetricJsonIdentifier + ) { + super(shortName, longName, tooltip, identifier) + } + + format(value: number): string { + return `${(value * 100).toFixed(2)}%` + } +} + +export namespace MetricTypes { + export const AVERAGE_SIMILARITY = new PercentageMetricType( + 'AVG', + 'Average Similarity', + 'The average similarity of the two files.\nA high similarity indicates that the programs work in a similar way.', + MetricJsonIdentifier.AVERAGE_SIMILARITY + ) + export const MAXIMUM_SIMILARITY = new PercentageMetricType( + 'MAX', + 'Maximum Similarity', + 'The maximum similarity of the two files.\nUseful if programs are very different in size.', + MetricJsonIdentifier.MAXIMUM_SIMILARITY + ) + export const MINIMUM_SIMILARITY = new PercentageMetricType( + 'MIN', + 'Minimum Similarity', + 'The minimum similarity of the two files.', + MetricJsonIdentifier.MINIMUM_SIMILARITY + ) + export const INTERSECTION = new IdentityMetricType( + 'INTER', + 'Matched Tokens', + 'The number of tokens that are matched between the two files.', + MetricJsonIdentifier.INTERSECTION + ) + export const SYMMETRIC = new PercentageMetricType( + 'SYM', + 'Symmetric Similarity', + 'A symmetric similarity measure.', + MetricJsonIdentifier.SYMMETRIC + ) + export const LONGEST_MATCH = new IdentityMetricType( + 'LONG', + 'Longest Match', + 'The number of tokens in the longest match.', + MetricJsonIdentifier.LONGEST_MATCH + ) + export const OVERALL = new IdentityMetricType( + 'ALL', + 'Overall', + 'Sum of both submission lengths.', + MetricJsonIdentifier.OVERALL + ) + + export const METRIC_LIST: MetricType[] = [ + AVERAGE_SIMILARITY, + MAXIMUM_SIMILARITY, + MINIMUM_SIMILARITY, + INTERSECTION, + SYMMETRIC, + LONGEST_MATCH, + OVERALL + ] + + export const METRIC_MAP: Record = {} as Record< + MetricJsonIdentifier, + MetricType + > + for (const metric of METRIC_LIST) { + METRIC_MAP[metric.identifier] = metric } + export const METRIC_JSON_IDENTIFIERS = [ + MetricJsonIdentifier.AVERAGE_SIMILARITY, + MetricJsonIdentifier.MAXIMUM_SIMILARITY, + MetricJsonIdentifier.MINIMUM_SIMILARITY, + MetricJsonIdentifier.INTERSECTION, + MetricJsonIdentifier.SYMMETRIC, + MetricJsonIdentifier.LONGEST_MATCH, + MetricJsonIdentifier.OVERALL + ] } diff --git a/report-viewer/src/model/Overview.ts b/report-viewer/src/model/Overview.ts index 56841636f2..16e91862ba 100644 --- a/report-viewer/src/model/Overview.ts +++ b/report-viewer/src/model/Overview.ts @@ -1,7 +1,7 @@ import type { Distribution } from './Distribution' import type { Cluster } from '@/model/Cluster' import type { ComparisonListElement } from './ComparisonListElement' -import type { MetricType } from './MetricType' +import type { MetricJsonIdentifier } from './MetricType' import type { Language } from './Language' /** @@ -16,7 +16,7 @@ export class Overview { private readonly _dateOfExecution: string private readonly _durationOfExecution: number private readonly _topComparisons: Array - private readonly _distributions: Record + private readonly _distributions: Record private readonly _clusters: Array private readonly _totalComparisons: number @@ -29,7 +29,7 @@ export class Overview { dateOfExecution: string, durationOfExecution: number, topComparisons: Array, - distributions: Record, + distributions: Record, clusters: Array, totalComparisons: number ) { diff --git a/report-viewer/src/model/factories/ComparisonFactory.ts b/report-viewer/src/model/factories/ComparisonFactory.ts index 9305ad630e..b872af43c0 100644 --- a/report-viewer/src/model/factories/ComparisonFactory.ts +++ b/report-viewer/src/model/factories/ComparisonFactory.ts @@ -4,7 +4,7 @@ import { store } from '@/stores/store' import { getMatchColorCount } from '@/utils/ColorUtils' import slash from 'slash' import { BaseFactory } from './BaseFactory' -import { MetricType } from '../MetricType' +import { MetricJsonIdentifier } from '../MetricType' import type { SubmissionFile } from '../File' /** @@ -66,10 +66,12 @@ export class ComparisonFactory extends BaseFactory { ) } - private static extractSimilarities(json: Record): Record { - const similarities = {} as Record + private static extractSimilarities( + json: Record + ): Record { + const similarities = {} as Record for (const [key, value] of Object.entries(json)) { - similarities[key as MetricType] = value + similarities[key as MetricJsonIdentifier] = value } return similarities } diff --git a/report-viewer/src/model/factories/OptionsFactory.ts b/report-viewer/src/model/factories/OptionsFactory.ts index 3cd5a30bd5..346e70840e 100644 --- a/report-viewer/src/model/factories/OptionsFactory.ts +++ b/report-viewer/src/model/factories/OptionsFactory.ts @@ -1,6 +1,6 @@ import type { CliClusterOptions, CliMergingOptions, CliOptions } from '../CliOptions' import { ParserLanguage } from '../Language' -import { MetricType } from '../MetricType' +import { MetricJsonIdentifier, MetricTypes } from '../MetricType' import { BaseFactory } from './BaseFactory' export class OptionsFactory extends BaseFactory { @@ -9,6 +9,7 @@ export class OptionsFactory extends BaseFactory { } private static extractOptions(json: Record): CliOptions { + const similarityMetricIdentifier = json['similarity_metric'] as MetricJsonIdentifier return { language: json['language'] as ParserLanguage, minTokenMatch: json['min_token_match'] as number, @@ -18,7 +19,10 @@ export class OptionsFactory extends BaseFactory { subDirectoryName: (json['subdirectory_name'] as string) ?? '', fileSuffixes: json['file_suffixes'] as string[], exclusionFileName: (json['exclusion_file_name'] as string) ?? '', - similarityMetric: json['similarity_metric'] as MetricType, + similarityMetric: + MetricTypes.METRIC_LIST.find( + (metric) => metric.identifier === similarityMetricIdentifier + ) ?? MetricTypes.AVERAGE_SIMILARITY, similarityThreshold: json['similarity_threshold'] as number, maxNumberComparisons: json['max_comparisons'] as number, clusterOptions: this.extractClusterOptions(json['cluster'] as Record), @@ -29,7 +33,7 @@ export class OptionsFactory extends BaseFactory { private static extractClusterOptions(json: Record): CliClusterOptions { return { enabled: json['enabled'] as boolean, - similarityMetric: MetricType.AVERAGE, + similarityMetric: MetricTypes.AVERAGE_SIMILARITY, spectralBandwidth: json['spectral_bandwidth'] as number, spectralGaussianProcessVariance: json['spectral_gaussian_variance'] as number, spectralMinRuns: json['spectral_min_runs'] as number, diff --git a/report-viewer/src/model/factories/OverviewFactory.ts b/report-viewer/src/model/factories/OverviewFactory.ts index e388353e1b..1ff3aa795d 100644 --- a/report-viewer/src/model/factories/OverviewFactory.ts +++ b/report-viewer/src/model/factories/OverviewFactory.ts @@ -5,7 +5,7 @@ import { store } from '@/stores/store' import { Version, minimalReportVersion, reportViewerVersion } from '../Version' import { getLanguageParser } from '../Language' import { Distribution } from '../Distribution' -import { MetricType } from '../MetricType' +import { MetricJsonIdentifier } from '../MetricType' import { BaseFactory } from './BaseFactory' /** @@ -59,10 +59,10 @@ export class OverviewFactory extends BaseFactory { private static extractDistributions( json: Record> - ): Record { - const distributions = {} as Record + ): Record { + const distributions = {} as Record for (const [key, value] of Object.entries(json)) { - distributions[key as MetricType] = new Distribution(value as Array) + distributions[key as MetricJsonIdentifier] = new Distribution(value as Array) } return distributions } @@ -79,7 +79,7 @@ export class OverviewFactory extends BaseFactory { id: counter, firstSubmissionId: topComparison.first_submission as string, secondSubmissionId: topComparison.second_submission as string, - similarities: topComparison.similarities as Record + similarities: topComparison.similarities as Record } comparisons.push({ ...comparison, diff --git a/report-viewer/src/model/ui/DistributionChartConfig.ts b/report-viewer/src/model/ui/DistributionChartConfig.ts index cc260dec0d..3a67e0e08c 100644 --- a/report-viewer/src/model/ui/DistributionChartConfig.ts +++ b/report-viewer/src/model/ui/DistributionChartConfig.ts @@ -1,11 +1,11 @@ import type { BucketOptions } from '../Distribution' -import type { MetricType } from '../MetricType' +import type { MetricJsonIdentifier } from '../MetricType' /** * Configuration for the distribution chart. */ export interface DistributionChartConfig { - metric: MetricType + metric: MetricJsonIdentifier xScale: 'linear' | 'logarithmic' bucketCount: BucketOptions } diff --git a/report-viewer/src/stores/state.ts b/report-viewer/src/stores/state.ts index d12bdac29f..81b7a1bf44 100644 --- a/report-viewer/src/stores/state.ts +++ b/report-viewer/src/stores/state.ts @@ -1,5 +1,5 @@ import type { SubmissionFile } from '@/model/File' -import type { MetricType } from '@/model/MetricType' +import type { MetricJsonIdentifier } from '@/model/MetricType' import type { DistributionChartConfig } from '@/model/ui/DistributionChartConfig' import type { FileSortingOptions } from '@/model/ui/FileSortingOptions' @@ -39,7 +39,7 @@ export interface State { export interface UIState { useDarkMode: boolean - comparisonTableSortingMetric: MetricType + comparisonTableSortingMetric: MetricJsonIdentifier comparisonTableClusterSorting: boolean distributionChartConfig: DistributionChartConfig fileSorting: FileSortingOptions diff --git a/report-viewer/src/stores/store.ts b/report-viewer/src/stores/store.ts index e722cbbd21..3d61200c98 100644 --- a/report-viewer/src/stores/store.ts +++ b/report-viewer/src/stores/store.ts @@ -1,6 +1,6 @@ import { defineStore } from 'pinia' import type { State, UIState } from './state' -import { MetricType } from '@/model/MetricType' +import { MetricJsonIdentifier } from '@/model/MetricType' import type { SubmissionFile, File } from '@/model/File' import { FileSortingOptions } from '@/model/ui/FileSortingOptions' @@ -23,10 +23,10 @@ const store = defineStore('store', { }, uiState: { useDarkMode: window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches, - comparisonTableSortingMetric: MetricType.AVERAGE, + comparisonTableSortingMetric: MetricJsonIdentifier.AVERAGE_SIMILARITY, comparisonTableClusterSorting: false, distributionChartConfig: { - metric: MetricType.AVERAGE, + metric: MetricJsonIdentifier.AVERAGE_SIMILARITY, xScale: 'linear', bucketCount: 10 }, diff --git a/report-viewer/src/views/ClusterView.vue b/report-viewer/src/views/ClusterView.vue index 06044eca31..1c079dff16 100644 --- a/report-viewer/src/views/ClusterView.vue +++ b/report-viewer/src/views/ClusterView.vue @@ -95,7 +95,7 @@ import Container from '@/components/ContainerComponent.vue' import TextInformation from '@/components/TextInformation.vue' import type { Cluster } from '@/model/Cluster' import type { ClusterListElement, ClusterListElementMember } from '@/model/ClusterListElement' -import { MetricType } from '@/model/MetricType' +import { MetricType, MetricTypes } from '@/model/MetricType' import type { Overview } from '@/model/Overview' import { computed, ref, onErrorCaptured, type PropType, type Ref } from 'vue' import { redirectOnError } from '@/router' @@ -135,7 +135,7 @@ const comparisonTableOptions = [ tooltip: 'Comparisons between the cluster members\nand other submissions.' } ] -const usedMetric = MetricType.AVERAGE +const usedMetric: MetricType = MetricTypes.AVERAGE_SIMILARITY const comparisons = computed(() => props.overview.topComparisons.filter( @@ -147,7 +147,7 @@ const comparisons = computed(() => let counter = 0 comparisons.value - .sort((a, b) => b.similarities[usedMetric] - a.similarities[usedMetric]) + .sort((a, b) => b.similarities[usedMetric.identifier] - a.similarities[usedMetric.identifier]) .forEach((c) => { c.sortingPlace = counter++ c.id = counter @@ -164,7 +164,7 @@ const relatedComparisons = computed(() => ) counter = 0 relatedComparisons.value - .sort((a, b) => b.similarities[usedMetric] - a.similarities[usedMetric]) + .sort((a, b) => b.similarities[usedMetric.identifier] - a.similarities[usedMetric.identifier]) .forEach((c) => { c.sortingPlace = counter++ c.id = counter @@ -177,7 +177,7 @@ for (const member of props.cluster.members) { .forEach((c) => { membersComparisons.push({ matchedWith: c.firstSubmissionId === member ? c.secondSubmissionId : c.firstSubmissionId, - similarity: c.similarities[usedMetric] + similarity: c.similarities[usedMetric.identifier] }) }) clusterMemberList.set(member, membersComparisons) diff --git a/report-viewer/src/views/ComparisonView.vue b/report-viewer/src/views/ComparisonView.vue index e0a0860db3..756311bc88 100644 --- a/report-viewer/src/views/ComparisonView.vue +++ b/report-viewer/src/views/ComparisonView.vue @@ -24,9 +24,11 @@
- {{ (comparison.similarities[MetricType.AVERAGE] * 100).toFixed(2) }}% + {{ + MetricTypes.AVERAGE_SIMILARITY.format( + comparison.similarities[MetricTypes.AVERAGE_SIMILARITY.identifier] + ) + }} {{ - metricToolTips[options.similarityMetric].longName + options.similarityMetric.longName }} {{ options.similarityThreshold @@ -41,7 +41,7 @@

Clustering:

{{ - metricToolTips[options.clusterOptions.similarityMetric].longName + options.clusterOptions.similarityMetric.longName }} {{ options.clusterOptions.algorithm @@ -135,7 +135,6 @@ import { Overview } from '@/model/Overview' import { onErrorCaptured, type PropType } from 'vue' import { redirectOnError } from '@/router' import type { CliOptions } from '@/model/CliOptions' -import { metricToolTips } from '@/model/MetricType' defineProps({ overview: { From 549c6e4a7a94a648482a3defee3a6cecb08a6398 Mon Sep 17 00:00:00 2001 From: Alex | Kronox Date: Fri, 8 Nov 2024 10:36:52 +0100 Subject: [PATCH 03/11] fix core unit tests --- .../reportobject/mapper/MetricMapperTest.java | 40 ++++++++++++++++--- 1 file changed, 34 insertions(+), 6 deletions(-) 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 9493bd5653..2b98fe04b6 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 @@ -9,6 +9,7 @@ import java.util.List; import java.util.Map; +import de.jplag.Match; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -48,14 +49,16 @@ public void test_getDistributions() { public void test_getTopComparisons() { // given 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)); + comparison(submission("1", 22), submission("2", 30), .7, .8, .5, new int[]{9,3,1}), + comparison(submission("3", 202), submission("4", 134), .3, .9, .01, new int[]{1,15,23,3})); // when List result = metricMapper.getTopComparisons(jPlagResult); // then Assertions.assertEquals( - List.of(new TopComparison("1", "2", Map.of("AVG", .7, "MAX", .8)), new TopComparison("3", "4", Map.of("AVG", .3, "MAX", .9))), + List.of(new TopComparison("1", "2", Map.of("AVG", .7, "MAX", .8, "MIN", .5, "LONGEST_MATCH", 9.0, "INTERSECTION", 13.0, "SYMMETRIC", .5, "OVERALL", 52.0)), + new TopComparison("3", "4", Map.of("AVG", .3, "MAX", .9, "MIN", .01, "LONGEST_MATCH", 23.0, "INTERSECTION", 42.0, "SYMMETRIC", .25, "OVERALL", 336.0))), result); } @@ -64,14 +67,23 @@ private int[] distribution(List expectedDistribution) { return distribution.stream().mapToInt(Integer::intValue).toArray(); } + private CreateSubmission submission(String name, int tokenCount) { + return new CreateSubmission(name, tokenCount); + } + private CreateSubmission submission(String name) { - return new CreateSubmission(name); + return submission(name, 0); + } + + private Comparison comparison(CreateSubmission submission1, CreateSubmission submission2, double similarity, double maxSimilarity, double minSimilarity, int[] matchLengths) { + return new Comparison(submission1, submission2, similarity, maxSimilarity, minSimilarity, matchLengths); } private Comparison comparison(CreateSubmission submission1, CreateSubmission submission2, double similarity, double maxSimilarity) { - return new Comparison(submission1, submission2, similarity, maxSimilarity); + return comparison(submission1, submission2, similarity, maxSimilarity, 0, new int[0]); } + private JPlagResult createJPlagResult(int[] avgDistribution, int[] maxDistribution, Comparison... createComparisonsDto) { JPlagResult jPlagResult = mock(JPlagResult.class); doReturn(Arrays.stream(avgDistribution).boxed().toList()).when(jPlagResult).calculateDistributionFor(SimilarityMetric.AVG); @@ -85,14 +97,20 @@ private JPlagResult createJPlagResult(int[] avgDistribution, int[] maxDistributi for (Comparison comparisonDto : createComparisonsDto) { Submission submission1 = mock(Submission.class); doReturn(comparisonDto.submission1.name).when(submission1).getName(); + doReturn(comparisonDto.submission1.tokenCount).when(submission1).getNumberOfTokens(); Submission submission2 = mock(Submission.class); doReturn(comparisonDto.submission2.name).when(submission2).getName(); + doReturn(comparisonDto.submission2.tokenCount).when(submission2).getNumberOfTokens(); JPlagComparison mockedComparison = mock(JPlagComparison.class); doReturn(submission1).when(mockedComparison).firstSubmission(); doReturn(submission2).when(mockedComparison).secondSubmission(); doReturn(comparisonDto.similarity).when(mockedComparison).similarity(); doReturn(comparisonDto.maxSimilarity).when(mockedComparison).maximalSimilarity(); + doReturn(comparisonDto.minSimilarity).when(mockedComparison).minimalSimilarity(); + List matches = createMockMatchList(comparisonDto.matchLengths); + doReturn(matches).when(mockedComparison).matches(); + doReturn(matches.stream().mapToInt(Match::length).sum()).when(mockedComparison).getNumberOfMatchedTokens(); comparisonList.add(mockedComparison); } @@ -100,10 +118,20 @@ private JPlagResult createJPlagResult(int[] avgDistribution, int[] maxDistributi return jPlagResult; } - private record Comparison(CreateSubmission submission1, CreateSubmission submission2, double similarity, double maxSimilarity) { + private List createMockMatchList(int[] matchLengths) { + List matches = new ArrayList<>(); + for (int l : matchLengths) { + Match m = mock(Match.class); + doReturn(l).when(m).length(); + matches.add(m); + } + return matches; + } + + private record Comparison(CreateSubmission submission1, CreateSubmission submission2, double similarity, double maxSimilarity, double minSimilarity, int[] matchLengths) { } - private record CreateSubmission(String name) { + private record CreateSubmission(String name, int tokenCount) { } } \ No newline at end of file From ab247860169c8eb62433fcae838d625ff4a59827 Mon Sep 17 00:00:00 2001 From: Alex | Kronox Date: Fri, 8 Nov 2024 11:33:24 +0100 Subject: [PATCH 04/11] adjust report viewer unit tests --- .../comparisonTable/ComparisonTable.test.ts | 60 ++++++++-------- .../ComparisonTableFilter.test.ts | 24 +++++-- .../optionsSelectors/MetricSelector.test.ts | 14 ++-- .../model/factories/ComparisonFactory.test.ts | 12 +++- .../model/factories/OptionsFactory.test.ts | 6 +- .../model/factories/OverviewFactory.test.ts | 72 +++++++++++++++---- .../unit/model/factories/ValidComparison.json | 8 ++- .../unit/model/factories/ValidOverview.json | 24 +++++-- 8 files changed, 150 insertions(+), 70 deletions(-) diff --git a/report-viewer/tests/unit/components/comparisonTable/ComparisonTable.test.ts b/report-viewer/tests/unit/components/comparisonTable/ComparisonTable.test.ts index 2866e79a4f..c0970e35f1 100644 --- a/report-viewer/tests/unit/components/comparisonTable/ComparisonTable.test.ts +++ b/report-viewer/tests/unit/components/comparisonTable/ComparisonTable.test.ts @@ -3,7 +3,7 @@ import { flushPromises, mount } from '@vue/test-utils' import { describe, it, vi, expect } from 'vitest' import { createTestingPinia } from '@pinia/testing' import { store } from '@/stores/store' -import { MetricType } from '@/model/MetricType.ts' +import { MetricJsonIdentifier } from '@/model/MetricType.ts' import { router } from '@/router' import OptionsSelector from '@/components/optionsSelectors/OptionsSelectorComponent.vue' import OptionComponent from '@/components/optionsSelectors/OptionComponent.vue' @@ -19,8 +19,8 @@ describe('ComparisonTable', async () => { firstSubmissionId: 'A', secondSubmissionId: 'B', similarities: { - [MetricType.AVERAGE]: 1, - [MetricType.MAXIMUM]: 0.5 + [MetricJsonIdentifier.AVERAGE_SIMILARITY]: 1, + [MetricJsonIdentifier.MAXIMUM_SIMILARITY]: 0.5 }, clusterIndex: -1 }, @@ -30,8 +30,8 @@ describe('ComparisonTable', async () => { firstSubmissionId: 'C', secondSubmissionId: 'D', similarities: { - [MetricType.AVERAGE]: 0.5, - [MetricType.MAXIMUM]: 1 + [MetricJsonIdentifier.AVERAGE_SIMILARITY]: 0.5, + [MetricJsonIdentifier.MAXIMUM_SIMILARITY]: 1 }, clusterIndex: -1 } @@ -68,8 +68,8 @@ describe('ComparisonTable', async () => { firstSubmissionId: 'A', secondSubmissionId: 'B', similarities: { - [MetricType.AVERAGE]: 0.3, - [MetricType.MAXIMUM]: 0.5 + [MetricJsonIdentifier.AVERAGE_SIMILARITY]: 0.3, + [MetricJsonIdentifier.MAXIMUM_SIMILARITY]: 0.5 }, clusterIndex: -1 }, @@ -79,8 +79,8 @@ describe('ComparisonTable', async () => { firstSubmissionId: 'C', secondSubmissionId: 'D', similarities: { - [MetricType.AVERAGE]: 0.5, - [MetricType.MAXIMUM]: 1 + [MetricJsonIdentifier.AVERAGE_SIMILARITY]: 0.5, + [MetricJsonIdentifier.MAXIMUM_SIMILARITY]: 1 }, clusterIndex: -1 }, @@ -90,8 +90,8 @@ describe('ComparisonTable', async () => { firstSubmissionId: 'E', secondSubmissionId: 'F', similarities: { - [MetricType.AVERAGE]: 0.3, - [MetricType.MAXIMUM]: 0.1 + [MetricJsonIdentifier.AVERAGE_SIMILARITY]: 0.3, + [MetricJsonIdentifier.MAXIMUM_SIMILARITY]: 0.1 }, clusterIndex: -1 }, @@ -101,8 +101,8 @@ describe('ComparisonTable', async () => { firstSubmissionId: 'H', secondSubmissionId: 'G', similarities: { - [MetricType.AVERAGE]: 0.9, - [MetricType.MAXIMUM]: 0.2 + [MetricJsonIdentifier.AVERAGE_SIMILARITY]: 0.9, + [MetricJsonIdentifier.MAXIMUM_SIMILARITY]: 0.2 }, clusterIndex: -1 } @@ -152,8 +152,8 @@ describe('ComparisonTable', async () => { firstSubmissionId: 'A', secondSubmissionId: 'B', similarities: { - [MetricType.AVERAGE]: 0.3, - [MetricType.MAXIMUM]: 0.5 + [MetricJsonIdentifier.AVERAGE_SIMILARITY]: 0.3, + [MetricJsonIdentifier.MAXIMUM_SIMILARITY]: 0.5 }, clusterIndex: -1 }, @@ -163,8 +163,8 @@ describe('ComparisonTable', async () => { firstSubmissionId: 'C', secondSubmissionId: 'D', similarities: { - [MetricType.AVERAGE]: 0.4, - [MetricType.MAXIMUM]: 1 + [MetricJsonIdentifier.AVERAGE_SIMILARITY]: 0.4, + [MetricJsonIdentifier.MAXIMUM_SIMILARITY]: 1 }, clusterIndex: -1 }, @@ -174,8 +174,8 @@ describe('ComparisonTable', async () => { firstSubmissionId: 'E', secondSubmissionId: 'F', similarities: { - [MetricType.AVERAGE]: 0.3, - [MetricType.MAXIMUM]: 0.1 + [MetricJsonIdentifier.AVERAGE_SIMILARITY]: 0.3, + [MetricJsonIdentifier.MAXIMUM_SIMILARITY]: 0.1 }, clusterIndex: -1 }, @@ -185,8 +185,8 @@ describe('ComparisonTable', async () => { firstSubmissionId: 'H', secondSubmissionId: 'G', similarities: { - [MetricType.AVERAGE]: 0.9, - [MetricType.MAXIMUM]: 0.2 + [MetricJsonIdentifier.AVERAGE_SIMILARITY]: 0.9, + [MetricJsonIdentifier.MAXIMUM_SIMILARITY]: 0.2 }, clusterIndex: -1 } @@ -249,8 +249,8 @@ describe('ComparisonTable', async () => { firstSubmissionId: 'A', secondSubmissionId: 'B', similarities: { - [MetricType.AVERAGE]: 0.3, - [MetricType.MAXIMUM]: 0.5 + [MetricJsonIdentifier.AVERAGE_SIMILARITY]: 0.3, + [MetricJsonIdentifier.MAXIMUM_SIMILARITY]: 0.5 }, clusterIndex: 0 }, @@ -260,8 +260,8 @@ describe('ComparisonTable', async () => { firstSubmissionId: 'C', secondSubmissionId: 'D', similarities: { - [MetricType.AVERAGE]: 0.5, - [MetricType.MAXIMUM]: 1 + [MetricJsonIdentifier.AVERAGE_SIMILARITY]: 0.5, + [MetricJsonIdentifier.MAXIMUM_SIMILARITY]: 1 }, clusterIndex: 1 }, @@ -271,8 +271,8 @@ describe('ComparisonTable', async () => { firstSubmissionId: 'E', secondSubmissionId: 'F', similarities: { - [MetricType.AVERAGE]: 0.3, - [MetricType.MAXIMUM]: 0.1 + [MetricJsonIdentifier.AVERAGE_SIMILARITY]: 0.3, + [MetricJsonIdentifier.MAXIMUM_SIMILARITY]: 0.1 }, clusterIndex: 2 }, @@ -282,8 +282,8 @@ describe('ComparisonTable', async () => { firstSubmissionId: 'H', secondSubmissionId: 'G', similarities: { - [MetricType.AVERAGE]: 0.9, - [MetricType.MAXIMUM]: 0.2 + [MetricJsonIdentifier.AVERAGE_SIMILARITY]: 0.9, + [MetricJsonIdentifier.MAXIMUM_SIMILARITY]: 0.2 }, clusterIndex: -1 } @@ -329,7 +329,7 @@ describe('ComparisonTable', async () => { expect(displayedComparisonsMaxSorted[2].firstSubmissionId).toBe('H') expect(displayedComparisonsMaxSorted[3].firstSubmissionId).toBe('E') - await metricOptions[2].trigger('click') + await metricOptions[7].trigger('click') await flushPromises() // Test sorting by cluster diff --git a/report-viewer/tests/unit/components/comparisonTable/ComparisonTableFilter.test.ts b/report-viewer/tests/unit/components/comparisonTable/ComparisonTableFilter.test.ts index 2b11ea6a5d..c6752cb13c 100644 --- a/report-viewer/tests/unit/components/comparisonTable/ComparisonTableFilter.test.ts +++ b/report-viewer/tests/unit/components/comparisonTable/ComparisonTableFilter.test.ts @@ -3,7 +3,7 @@ import { flushPromises, mount } from '@vue/test-utils' import { describe, it, vi, expect } from 'vitest' import { createTestingPinia } from '@pinia/testing' import { store } from '@/stores/store' -import { MetricType } from '@/model/MetricType.ts' +import { MetricJsonIdentifier } from '@/model/MetricType.ts' import ButtonComponent from '@/components/ButtonComponent.vue' import OptionsSelector from '@/components/optionsSelectors/OptionsSelectorComponent.vue' import OptionComponent from '@/components/optionsSelectors/OptionComponent.vue' @@ -41,21 +41,31 @@ describe('ComparisonTableFilter', async () => { expect(wrapper.text()).toContain('Cluster') const options = wrapper.getComponent(OptionsSelector).findAllComponents(OptionComponent) - expectHighlighting(0) await options[1].trigger('click') - expect(store().uiState.comparisonTableSortingMetric).toBe(MetricType.MAXIMUM) + expect(store().uiState.comparisonTableSortingMetric).toBe( + MetricJsonIdentifier.MAXIMUM_SIMILARITY + ) expect(store().uiState.comparisonTableClusterSorting).toBeFalsy() expectHighlighting(1) - await options[2].trigger('click') - expect(store().uiState.comparisonTableSortingMetric).toBe(MetricType.AVERAGE) + await options[4].trigger('click') + expect(store().uiState.comparisonTableSortingMetric).toBe(MetricJsonIdentifier.SYMMETRIC) + expect(store().uiState.comparisonTableClusterSorting).toBeFalsy() + expectHighlighting(4) + + await options[7].trigger('click') + expect(store().uiState.comparisonTableSortingMetric).toBe( + MetricJsonIdentifier.AVERAGE_SIMILARITY + ) expect(store().uiState.comparisonTableClusterSorting).toBeTruthy() - expectHighlighting(2) + expectHighlighting(7) await options[0].trigger('click') - expect(store().uiState.comparisonTableSortingMetric).toBe(MetricType.AVERAGE) + expect(store().uiState.comparisonTableSortingMetric).toBe( + MetricJsonIdentifier.AVERAGE_SIMILARITY + ) expect(store().uiState.comparisonTableClusterSorting).toBeFalsy() expectHighlighting(0) diff --git a/report-viewer/tests/unit/components/optionsSelectors/MetricSelector.test.ts b/report-viewer/tests/unit/components/optionsSelectors/MetricSelector.test.ts index 304100bf13..2bf1437191 100644 --- a/report-viewer/tests/unit/components/optionsSelectors/MetricSelector.test.ts +++ b/report-viewer/tests/unit/components/optionsSelectors/MetricSelector.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest' import { mount } from '@vue/test-utils' import MetricSelector from '@/components/optionsSelectors/MetricSelector.vue' -import { MetricType } from '@/model/MetricType' +import { MetricJsonIdentifier, MetricTypes } from '@/model/MetricType' describe('OptionSelectorComponent', () => { it('renders all options', async () => { @@ -12,20 +12,22 @@ describe('OptionSelectorComponent', () => { }) expect(wrapper.text()).toContain('Test:') - expect(wrapper.text()).toContain('Average Similarity') - expect(wrapper.text()).toContain('Maximum Similarity') + for (const metric of MetricTypes.METRIC_LIST) { + expect(wrapper.text()).toContain(metric.longName) + } }) it('renders given metrics only', async () => { const wrapper = mount(MetricSelector, { props: { title: 'Test:', - metrics: [MetricType.AVERAGE] + metrics: [MetricJsonIdentifier.AVERAGE_SIMILARITY, MetricJsonIdentifier.MINIMUM_SIMILARITY] } }) expect(wrapper.text()).toContain('Test:') expect(wrapper.text()).toContain('Average Similarity') + expect(wrapper.text()).toContain('Minimum Similarity') expect(wrapper.text()).not.toContain('Maximum Similarity') }) @@ -40,6 +42,8 @@ describe('OptionSelectorComponent', () => { expect(wrapper.emitted('selectionChanged')).toBeTruthy() expect(wrapper.emitted('selectionChanged')?.length).toBe(1) - expect(wrapper.emitted('selectionChanged')?.[0]).toEqual([MetricType.MAXIMUM]) + expect(wrapper.emitted('selectionChanged')?.[0]).toEqual([ + MetricJsonIdentifier.MAXIMUM_SIMILARITY + ]) }) }) diff --git a/report-viewer/tests/unit/model/factories/ComparisonFactory.test.ts b/report-viewer/tests/unit/model/factories/ComparisonFactory.test.ts index a918f43edb..24d6ce8e7e 100644 --- a/report-viewer/tests/unit/model/factories/ComparisonFactory.test.ts +++ b/report-viewer/tests/unit/model/factories/ComparisonFactory.test.ts @@ -2,7 +2,7 @@ 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' +import { MetricJsonIdentifier } from '@/model/MetricType' import { setActivePinia, createPinia } from 'pinia' describe('Test JSON to Comparison', () => { @@ -59,8 +59,14 @@ describe('Test JSON to Comparison', () => { expect(result).toBeDefined() expect(result.firstSubmissionId).toBe('root1') expect(result.secondSubmissionId).toBe('root2') - expect(result.similarities[MetricType.AVERAGE]).toBe(0.45) - expect(result.similarities[MetricType.MAXIMUM]).toBe(0.5) + expect(result.similarities[MetricJsonIdentifier.AVERAGE_SIMILARITY]).toBe(0.45) + expect(result.similarities[MetricJsonIdentifier.MAXIMUM_SIMILARITY]).toBe(0.5) + expect(result.similarities[MetricJsonIdentifier.MINIMUM_SIMILARITY]).toBe(0.4) + expect(result.similarities[MetricJsonIdentifier.INTERSECTION]).toBe(229) + expect(result.similarities[MetricJsonIdentifier.SYMMETRIC]).toBe(0.5) + expect(result.similarities[MetricJsonIdentifier.LONGEST_MATCH]).toBe(139) + expect(result.similarities[MetricJsonIdentifier.OVERALL]).toBe(916) + expect(result.filesOfFirstSubmission).toBeDefined() expect(result.filesOfSecondSubmission).toBeDefined() expect(result.allMatches.length).toBe(4) diff --git a/report-viewer/tests/unit/model/factories/OptionsFactory.test.ts b/report-viewer/tests/unit/model/factories/OptionsFactory.test.ts index 5cbc82bee9..4041c99235 100644 --- a/report-viewer/tests/unit/model/factories/OptionsFactory.test.ts +++ b/report-viewer/tests/unit/model/factories/OptionsFactory.test.ts @@ -1,7 +1,7 @@ import { beforeEach, describe, it, expect } from 'vitest' import { OptionsFactory } from '@/model/factories/OptionsFactory' import { ParserLanguage } from '@/model/Language' -import { MetricType } from '@/model/MetricType' +import { MetricTypes } from '@/model/MetricType' import validOptions from './ValidOptions.json' import { setActivePinia, createPinia } from 'pinia' import { store } from '@/stores/store' @@ -25,12 +25,12 @@ describe('Test JSON to Options', async () => { subDirectoryName: 'src/', fileSuffixes: ['.java', '.JAVA'], exclusionFileName: 'ex.txt', - similarityMetric: MetricType.AVERAGE, + similarityMetric: MetricTypes.AVERAGE_SIMILARITY, similarityThreshold: 0.0, maxNumberComparisons: 500, clusterOptions: { enabled: true, - similarityMetric: MetricType.AVERAGE, + similarityMetric: MetricTypes.AVERAGE_SIMILARITY, spectralBandwidth: 20.0, spectralGaussianProcessVariance: 0.0025000000000000005, spectralMinRuns: 5, diff --git a/report-viewer/tests/unit/model/factories/OverviewFactory.test.ts b/report-viewer/tests/unit/model/factories/OverviewFactory.test.ts index d5f9fb21dd..8871147541 100644 --- a/report-viewer/tests/unit/model/factories/OverviewFactory.test.ts +++ b/report-viewer/tests/unit/model/factories/OverviewFactory.test.ts @@ -1,6 +1,6 @@ import { beforeAll, describe, expect, it, vi, beforeEach } from 'vitest' import { OverviewFactory } from '@/model/factories/OverviewFactory' -import { MetricType } from '@/model/MetricType' +import { MetricJsonIdentifier } from '@/model/MetricType' import { Distribution } from '@/model/Distribution' import { ParserLanguage } from '@/model/Language' import validNew from './ValidOverview.json' @@ -34,8 +34,13 @@ describe('Test JSON to Overview', () => { firstSubmissionId: 'A', secondSubmissionId: 'C', similarities: { - [MetricType.AVERAGE]: 0.9960435212660732, - [MetricType.MAXIMUM]: 0.9960435212660732 + [MetricJsonIdentifier.AVERAGE_SIMILARITY]: 0.9960435212660732, + [MetricJsonIdentifier.MAXIMUM_SIMILARITY]: 0.9960435212660732, + [MetricJsonIdentifier.MINIMUM_SIMILARITY]: 0.99, + [MetricJsonIdentifier.SYMMETRIC]: 0.98, + [MetricJsonIdentifier.LONGEST_MATCH]: 13, + [MetricJsonIdentifier.INTERSECTION]: 32, + [MetricJsonIdentifier.OVERALL]: 100 }, sortingPlace: 0, id: 1, @@ -45,8 +50,13 @@ describe('Test JSON to Overview', () => { firstSubmissionId: 'D', secondSubmissionId: 'A', similarities: { - [MetricType.AVERAGE]: 0.751044776119403, - [MetricType.MAXIMUM]: 0.947289156626506 + [MetricJsonIdentifier.AVERAGE_SIMILARITY]: 0.751044776119403, + [MetricJsonIdentifier.MAXIMUM_SIMILARITY]: 0.947289156626506, + [MetricJsonIdentifier.MINIMUM_SIMILARITY]: 0.5, + [MetricJsonIdentifier.SYMMETRIC]: 0.8, + [MetricJsonIdentifier.LONGEST_MATCH]: 4, + [MetricJsonIdentifier.INTERSECTION]: 12, + [MetricJsonIdentifier.OVERALL]: 133 }, sortingPlace: 1, id: 2, @@ -56,8 +66,13 @@ describe('Test JSON to Overview', () => { firstSubmissionId: 'D', secondSubmissionId: 'C', similarities: { - [MetricType.AVERAGE]: 0.751044776119403, - [MetricType.MAXIMUM]: 0.947289156626506 + [MetricJsonIdentifier.AVERAGE_SIMILARITY]: 0.751044776119403, + [MetricJsonIdentifier.MAXIMUM_SIMILARITY]: 0.947289156626506, + [MetricJsonIdentifier.MINIMUM_SIMILARITY]: 0.46, + [MetricJsonIdentifier.SYMMETRIC]: 0.78, + [MetricJsonIdentifier.LONGEST_MATCH]: 12, + [MetricJsonIdentifier.INTERSECTION]: 12, + [MetricJsonIdentifier.OVERALL]: 98 }, sortingPlace: 2, id: 3, @@ -67,8 +82,13 @@ describe('Test JSON to Overview', () => { firstSubmissionId: 'B', secondSubmissionId: 'D', similarities: { - [MetricType.AVERAGE]: 0.28322981366459626, - [MetricType.MAXIMUM]: 0.8085106382978723 + [MetricJsonIdentifier.AVERAGE_SIMILARITY]: 0.28322981366459626, + [MetricJsonIdentifier.MAXIMUM_SIMILARITY]: 0.8085106382978723, + [MetricJsonIdentifier.MINIMUM_SIMILARITY]: 0.1, + [MetricJsonIdentifier.SYMMETRIC]: 0.456, + [MetricJsonIdentifier.LONGEST_MATCH]: 5, + [MetricJsonIdentifier.INTERSECTION]: 6, + [MetricJsonIdentifier.OVERALL]: 32 }, sortingPlace: 3, id: 4, @@ -78,8 +98,13 @@ describe('Test JSON to Overview', () => { firstSubmissionId: 'B', secondSubmissionId: 'A', similarities: { - [MetricType.AVERAGE]: 0.2378472222222222, - [MetricType.MAXIMUM]: 0.9716312056737588 + [MetricJsonIdentifier.AVERAGE_SIMILARITY]: 0.2378472222222222, + [MetricJsonIdentifier.MAXIMUM_SIMILARITY]: 0.9716312056737588, + [MetricJsonIdentifier.MINIMUM_SIMILARITY]: 0.05, + [MetricJsonIdentifier.SYMMETRIC]: 0.23, + [MetricJsonIdentifier.LONGEST_MATCH]: 7, + [MetricJsonIdentifier.INTERSECTION]: 9, + [MetricJsonIdentifier.OVERALL]: 34 }, sortingPlace: 4, id: 5, @@ -89,8 +114,13 @@ describe('Test JSON to Overview', () => { firstSubmissionId: 'B', secondSubmissionId: 'C', similarities: { - [MetricType.AVERAGE]: 0.2378472222222222, - [MetricType.MAXIMUM]: 0.9716312056737588 + [MetricJsonIdentifier.AVERAGE_SIMILARITY]: 0.2378472222222222, + [MetricJsonIdentifier.MAXIMUM_SIMILARITY]: 0.9716312056737588, + [MetricJsonIdentifier.MINIMUM_SIMILARITY]: 0.06, + [MetricJsonIdentifier.SYMMETRIC]: 0.235, + [MetricJsonIdentifier.LONGEST_MATCH]: 3, + [MetricJsonIdentifier.INTERSECTION]: 6, + [MetricJsonIdentifier.OVERALL]: 134 }, sortingPlace: 5, id: 6, @@ -98,13 +128,25 @@ describe('Test JSON to Overview', () => { } ], _distributions: { - [MetricType.MAXIMUM]: new Distribution([ + [MetricJsonIdentifier.MAXIMUM_SIMILARITY]: new Distribution([ 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 Distribution([ + [MetricJsonIdentifier.AVERAGE_SIMILARITY]: new Distribution([ + 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 + ]), + [MetricJsonIdentifier.MINIMUM_SIMILARITY]: new Distribution([ + 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, 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, 3 + ]), + [MetricJsonIdentifier.SYMMETRIC]: new Distribution([ 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, diff --git a/report-viewer/tests/unit/model/factories/ValidComparison.json b/report-viewer/tests/unit/model/factories/ValidComparison.json index 2698402a20..5e3ac80647 100644 --- a/report-viewer/tests/unit/model/factories/ValidComparison.json +++ b/report-viewer/tests/unit/model/factories/ValidComparison.json @@ -3,7 +3,13 @@ "id2": "root2", "similarities": { "AVG": 0.45, - "MAX": 0.5 + "MAX": 0.5, + "MIN": 0.4, + "LONGEST_MATCH": 139, + "INTERSECTION": 229, + "SYMMETRIC": 0.5, + "OVERALL": 916 + }, "matches": [ { diff --git a/report-viewer/tests/unit/model/factories/ValidOverview.json b/report-viewer/tests/unit/model/factories/ValidOverview.json index 1ddfd238ef..5b0843d22a 100644 --- a/report-viewer/tests/unit/model/factories/ValidOverview.json +++ b/report-viewer/tests/unit/model/factories/ValidOverview.json @@ -28,38 +28,50 @@ 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 + ], + "MIN": [ + 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, 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, 3 + ], + "SYMMETRIC": [ + 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 } + "similarities": { "AVG": 0.9960435212660732, "MAX": 0.9960435212660732, "MIN": 0.99, "SYMMETRIC": 0.98, "LONGEST_MATCH": 13.0, "INTERSECTION": 32.0, "OVERALL": 100.0 } }, { "first_submission": "D", "second_submission": "A", - "similarities": { "AVG": 0.751044776119403, "MAX": 0.947289156626506 } + "similarities": { "AVG": 0.751044776119403, "MAX": 0.947289156626506, "MIN": 0.5, "SYMMETRIC": 0.8, "LONGEST_MATCH": 4.0, "INTERSECTION": 12.0, "OVERALL": 133.0 } }, { "first_submission": "D", "second_submission": "C", - "similarities": { "AVG": 0.751044776119403, "MAX": 0.947289156626506 } + "similarities": { "AVG": 0.751044776119403, "MAX": 0.947289156626506, "MIN": 0.46, "SYMMETRIC": 0.78, "LONGEST_MATCH": 12.0, "INTERSECTION": 12.0, "OVERALL": 98.0 } }, { "first_submission": "B", "second_submission": "D", - "similarities": { "AVG": 0.28322981366459626, "MAX": 0.8085106382978723 } + "similarities": { "AVG": 0.28322981366459626, "MAX": 0.8085106382978723, "MIN": 0.1, "SYMMETRIC": 0.456, "LONGEST_MATCH": 5.0, "INTERSECTION": 6.0, "OVERALL": 32.0 } }, { "first_submission": "B", "second_submission": "A", - "similarities": { "AVG": 0.2378472222222222, "MAX": 0.9716312056737588 } + "similarities": { "AVG": 0.2378472222222222, "MAX": 0.9716312056737588, "MIN": 0.05, "SYMMETRIC": 0.23, "LONGEST_MATCH": 7.0, "INTERSECTION": 9.0, "OVERALL": 34.0 } }, { "first_submission": "B", "second_submission": "C", - "similarities": { "AVG": 0.2378472222222222, "MAX": 0.9716312056737588 } + "similarities": { "AVG": 0.2378472222222222, "MAX": 0.9716312056737588, "MIN": 0.06, "SYMMETRIC": 0.235, "LONGEST_MATCH": 3.0, "INTERSECTION": 6.0, "OVERALL": 134.0 } } ], "clusters": [ From c56a514a3319719407eee5ddb07aca056515acd9 Mon Sep 17 00:00:00 2001 From: Alex | Kronox Date: Fri, 8 Nov 2024 11:45:29 +0100 Subject: [PATCH 05/11] only show distribution for percentage based metrics --- .../distributionDiagram/DistributionDiagramOptions.vue | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/report-viewer/src/components/distributionDiagram/DistributionDiagramOptions.vue b/report-viewer/src/components/distributionDiagram/DistributionDiagramOptions.vue index 81a2b24b26..6d9e8c1d5f 100644 --- a/report-viewer/src/components/distributionDiagram/DistributionDiagramOptions.vue +++ b/report-viewer/src/components/distributionDiagram/DistributionDiagramOptions.vue @@ -10,6 +10,7 @@ (metric: MetricJsonIdentifier) => (store().uiState.distributionChartConfig.metric = metric) " + :metrics="metricOptions" /> From 3648b7678010405c6e97e0b6c099d137b7454a16 Mon Sep 17 00:00:00 2001 From: Alex | Kronox Date: Fri, 8 Nov 2024 11:46:43 +0100 Subject: [PATCH 06/11] fix spottless --- .../reportobject/mapper/MetricMapperTest.java | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) 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 2b98fe04b6..7faa55fbb3 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 @@ -9,12 +9,12 @@ import java.util.List; import java.util.Map; -import de.jplag.Match; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import de.jplag.JPlagComparison; import de.jplag.JPlagResult; +import de.jplag.Match; import de.jplag.Submission; import de.jplag.options.JPlagOptions; import de.jplag.options.SimilarityMetric; @@ -49,16 +49,18 @@ public void test_getDistributions() { public void test_getTopComparisons() { // given JPlagResult jPlagResult = createJPlagResult(distribution(EXPECTED_AVG_DISTRIBUTION), distribution(EXPECTED_MAX_DISTRIBUTION), - comparison(submission("1", 22), submission("2", 30), .7, .8, .5, new int[]{9,3,1}), - comparison(submission("3", 202), submission("4", 134), .3, .9, .01, new int[]{1,15,23,3})); + comparison(submission("1", 22), submission("2", 30), .7, .8, .5, new int[] {9, 3, 1}), + comparison(submission("3", 202), submission("4", 134), .3, .9, .01, new int[] {1, 15, 23, 3})); // when List result = metricMapper.getTopComparisons(jPlagResult); // then - Assertions.assertEquals( - List.of(new TopComparison("1", "2", Map.of("AVG", .7, "MAX", .8, "MIN", .5, "LONGEST_MATCH", 9.0, "INTERSECTION", 13.0, "SYMMETRIC", .5, "OVERALL", 52.0)), - new TopComparison("3", "4", Map.of("AVG", .3, "MAX", .9, "MIN", .01, "LONGEST_MATCH", 23.0, "INTERSECTION", 42.0, "SYMMETRIC", .25, "OVERALL", 336.0))), + Assertions.assertEquals(List.of( + new TopComparison("1", "2", + Map.of("AVG", .7, "MAX", .8, "MIN", .5, "LONGEST_MATCH", 9.0, "INTERSECTION", 13.0, "SYMMETRIC", .5, "OVERALL", 52.0)), + new TopComparison("3", "4", + Map.of("AVG", .3, "MAX", .9, "MIN", .01, "LONGEST_MATCH", 23.0, "INTERSECTION", 42.0, "SYMMETRIC", .25, "OVERALL", 336.0))), result); } @@ -75,7 +77,8 @@ private CreateSubmission submission(String name) { return submission(name, 0); } - private Comparison comparison(CreateSubmission submission1, CreateSubmission submission2, double similarity, double maxSimilarity, double minSimilarity, int[] matchLengths) { + private Comparison comparison(CreateSubmission submission1, CreateSubmission submission2, double similarity, double maxSimilarity, + double minSimilarity, int[] matchLengths) { return new Comparison(submission1, submission2, similarity, maxSimilarity, minSimilarity, matchLengths); } @@ -83,7 +86,6 @@ private Comparison comparison(CreateSubmission submission1, CreateSubmission sub return comparison(submission1, submission2, similarity, maxSimilarity, 0, new int[0]); } - private JPlagResult createJPlagResult(int[] avgDistribution, int[] maxDistribution, Comparison... createComparisonsDto) { JPlagResult jPlagResult = mock(JPlagResult.class); doReturn(Arrays.stream(avgDistribution).boxed().toList()).when(jPlagResult).calculateDistributionFor(SimilarityMetric.AVG); @@ -128,7 +130,8 @@ private List createMockMatchList(int[] matchLengths) { return matches; } - private record Comparison(CreateSubmission submission1, CreateSubmission submission2, double similarity, double maxSimilarity, double minSimilarity, int[] matchLengths) { + private record Comparison(CreateSubmission submission1, CreateSubmission submission2, double similarity, double maxSimilarity, + double minSimilarity, int[] matchLengths) { } private record CreateSubmission(String name, int tokenCount) { From 7820ef4e6e4d43f016c9fa5972ded8601917060f Mon Sep 17 00:00:00 2001 From: Alex | Kronox Date: Fri, 8 Nov 2024 13:15:26 +0100 Subject: [PATCH 07/11] add selection for secondary metric in table --- .../src/components/ComparisonTableFilter.vue | 18 ++++++++++++++++++ .../src/components/ComparisonsTable.vue | 17 +++++++++++------ report-viewer/src/stores/state.ts | 1 + report-viewer/src/stores/store.ts | 1 + 4 files changed, 31 insertions(+), 6 deletions(-) diff --git a/report-viewer/src/components/ComparisonTableFilter.vue b/report-viewer/src/components/ComparisonTableFilter.vue index 5e35d13b59..a605b0630a 100644 --- a/report-viewer/src/components/ComparisonTableFilter.vue +++ b/report-viewer/src/components/ComparisonTableFilter.vue @@ -35,6 +35,14 @@ :labels="tableSortingOptions" @selection-changed="(index: number) => changeSortingMetric(index)" /> +
@@ -47,6 +55,7 @@ import OptionsSelector from './optionsSelectors/OptionsSelectorComponent.vue' import { store } from '@/stores/store' import { MetricJsonIdentifier, MetricTypes } from '@/model/MetricType' import type { ToolTipLabel } from '@/model/ui/ToolTip' +import MetricSelector from './optionsSelectors/MetricSelector.vue' const props = defineProps({ searchString: { @@ -122,6 +131,15 @@ const tableSortingOptions = computed(() => { return options }) +const secondaryMetricOptions = [ + MetricJsonIdentifier.MAXIMUM_SIMILARITY, + MetricJsonIdentifier.MINIMUM_SIMILARITY, + MetricJsonIdentifier.SYMMETRIC, + MetricJsonIdentifier.INTERSECTION, + MetricJsonIdentifier.LONGEST_MATCH, + MetricJsonIdentifier.OVERALL +] + /** * Sets the anonymous set to empty if it is full or adds all submission ids to it if it is not full */ diff --git a/report-viewer/src/components/ComparisonsTable.vue b/report-viewer/src/components/ComparisonsTable.vue index 4fdf1cf307..dc1cf9a94c 100644 --- a/report-viewer/src/components/ComparisonsTable.vue +++ b/report-viewer/src/components/ComparisonsTable.vue @@ -34,12 +34,17 @@ @@ -104,15 +109,15 @@
{{ MetricTypes.AVERAGE_SIMILARITY.format( - item.similarities[MetricTypes.AVERAGE_SIMILARITY.shortName] + item.similarities[MetricTypes.AVERAGE_SIMILARITY.identifier] ) }}
{{ - MetricTypes.MAXIMUM_SIMILARITY.format( - item.similarities[MetricTypes.MAXIMUM_SIMILARITY.shortName] - ) + MetricTypes.METRIC_MAP[ + store().uiState.comparisonTableSecondaryMetric + ].format(item.similarities[store().uiState.comparisonTableSecondaryMetric]) }}
diff --git a/report-viewer/src/stores/state.ts b/report-viewer/src/stores/state.ts index 81b7a1bf44..92b8f50f7f 100644 --- a/report-viewer/src/stores/state.ts +++ b/report-viewer/src/stores/state.ts @@ -40,6 +40,7 @@ export interface State { export interface UIState { useDarkMode: boolean comparisonTableSortingMetric: MetricJsonIdentifier + comparisonTableSecondaryMetric: MetricJsonIdentifier comparisonTableClusterSorting: boolean distributionChartConfig: DistributionChartConfig fileSorting: FileSortingOptions diff --git a/report-viewer/src/stores/store.ts b/report-viewer/src/stores/store.ts index 3d61200c98..0d109e9bbf 100644 --- a/report-viewer/src/stores/store.ts +++ b/report-viewer/src/stores/store.ts @@ -24,6 +24,7 @@ const store = defineStore('store', { uiState: { useDarkMode: window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches, comparisonTableSortingMetric: MetricJsonIdentifier.AVERAGE_SIMILARITY, + comparisonTableSecondaryMetric: MetricJsonIdentifier.MAXIMUM_SIMILARITY, comparisonTableClusterSorting: false, distributionChartConfig: { metric: MetricJsonIdentifier.AVERAGE_SIMILARITY, From 7f1b63e98a00160dd9a4dcbb79815db6be1c7a97 Mon Sep 17 00:00:00 2001 From: Alex | Kronox Date: Mon, 25 Nov 2024 15:43:46 +0100 Subject: [PATCH 08/11] adjust names --- .../src/components/ComparisonTableFilter.vue | 2 +- report-viewer/src/model/MetricType.ts | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/report-viewer/src/components/ComparisonTableFilter.vue b/report-viewer/src/components/ComparisonTableFilter.vue index a605b0630a..c980f85888 100644 --- a/report-viewer/src/components/ComparisonTableFilter.vue +++ b/report-viewer/src/components/ComparisonTableFilter.vue @@ -30,7 +30,7 @@ Date: Thu, 28 Nov 2024 07:51:52 +0100 Subject: [PATCH 09/11] update report viewer tests --- report-viewer/tests/e2e/Cluster.spec.ts | 2 +- report-viewer/tests/e2e/Comparison.spec.ts | 5 +++-- report-viewer/tests/e2e/Distribution.spec.ts | 2 +- .../unit/components/optionsSelectors/MetricSelector.test.ts | 6 +++--- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/report-viewer/tests/e2e/Cluster.spec.ts b/report-viewer/tests/e2e/Cluster.spec.ts index 008561c7a0..63958ebaa3 100644 --- a/report-viewer/tests/e2e/Cluster.spec.ts +++ b/report-viewer/tests/e2e/Cluster.spec.ts @@ -46,6 +46,6 @@ function compareTableRow( similarityMAX: number ) { expect(table).toContain( - `${row}${id1}${id2}${similarityAVG.toFixed(2)}% ${similarityMAX.toFixed(2)}%` + `${row}${id1}${id2}${similarityAVG.toFixed(2)}%${similarityMAX.toFixed(2)}%` ) } diff --git a/report-viewer/tests/e2e/Comparison.spec.ts b/report-viewer/tests/e2e/Comparison.spec.ts index 173537d687..21e413fd99 100644 --- a/report-viewer/tests/e2e/Comparison.spec.ts +++ b/report-viewer/tests/e2e/Comparison.spec.ts @@ -6,14 +6,15 @@ test('Test comparison table and comparsion view', async ({ page }) => { await uploadFile('progpedia-report.zip', page) - const comparisonContainer = page.getByText('Hide AllSort By') + const comparisonContainer = page.getByText('Hide AllSorting Metric') // check for elements in average similarity table await page.getByPlaceholder('Filter/Unhide Comparisons').fill('Purple') const comparisonTableAverageSorted = await page.getByText(/Cluster[0-9]/).textContent() expect(comparisonTableAverageSorted).toContain('100Purple FishBeige Dog') - await comparisonContainer.getByText('Maximum Similarity', { exact: true }).click() + const sortingMetricSelector = comparisonContainer.getByText('Sorting Metric:Average') + await sortingMetricSelector.getByText('Maximum', { exact: true }).click() // check for elements in maximum similarity table await page.getByPlaceholder('Filter/Unhide Comparisons').fill('Blue') const comparisonTableMaxSorted = await page.getByText(/Cluster[0-9]/).textContent() diff --git a/report-viewer/tests/e2e/Distribution.spec.ts b/report-viewer/tests/e2e/Distribution.spec.ts index f9935ac61a..4fe7275984 100644 --- a/report-viewer/tests/e2e/Distribution.spec.ts +++ b/report-viewer/tests/e2e/Distribution.spec.ts @@ -35,7 +35,7 @@ async function selectOptions(page: Page, options: string[]) { function getTestCombinations() { const options = [ - ['Average Similarity', 'Maximum Similarity'], + ['Average', 'Maximum'], ['Linear', 'Logarithmic'], ['10', '20', '25', '50', '100'] ] diff --git a/report-viewer/tests/unit/components/optionsSelectors/MetricSelector.test.ts b/report-viewer/tests/unit/components/optionsSelectors/MetricSelector.test.ts index 2bf1437191..3188ac7bea 100644 --- a/report-viewer/tests/unit/components/optionsSelectors/MetricSelector.test.ts +++ b/report-viewer/tests/unit/components/optionsSelectors/MetricSelector.test.ts @@ -26,9 +26,9 @@ describe('OptionSelectorComponent', () => { }) expect(wrapper.text()).toContain('Test:') - expect(wrapper.text()).toContain('Average Similarity') - expect(wrapper.text()).toContain('Minimum Similarity') - expect(wrapper.text()).not.toContain('Maximum Similarity') + expect(wrapper.text()).toContain('Average') + expect(wrapper.text()).toContain('Minimum') + expect(wrapper.text()).not.toContain('Maximum') }) it('switch selection', async () => { From bcbd7dbb5a3a5db7856ebc849c08c06ad09bfee9 Mon Sep 17 00:00:00 2001 From: Alex | Kronox Date: Thu, 28 Nov 2024 08:12:54 +0100 Subject: [PATCH 10/11] include basecode in symmetric similarity --- core/src/main/java/de/jplag/JPlagComparison.java | 13 +++++++++++++ .../java/de/jplag/options/SimilarityMetric.java | 9 +-------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/core/src/main/java/de/jplag/JPlagComparison.java b/core/src/main/java/de/jplag/JPlagComparison.java index 37aa0c7ad3..2fa03cef0e 100644 --- a/core/src/main/java/de/jplag/JPlagComparison.java +++ b/core/src/main/java/de/jplag/JPlagComparison.java @@ -54,6 +54,19 @@ public final double similarity() { return 2 * similarity(divisorA + divisorB); } + /** + * @return A symmetric similarity in interval [0, 1]. O means no similarity, 1 means maximum similarity. + */ + public final double symmetricSimilarity() { + boolean subtractBaseCode = firstSubmission.hasBaseCodeMatches() && secondSubmission.hasBaseCodeMatches(); + int divisorA = firstSubmission.getSimilarityDivisor(subtractBaseCode); + int divisorB = secondSubmission.getSimilarityDivisor(subtractBaseCode); + if (divisorA + divisorB == 0) { + return 0.0; + } + return 2.0 * getNumberOfMatchedTokens() / (divisorA + divisorB); + } + /** * @return Similarity of the first submission in interval [0, 1]. O means no similarity, 1 means maximum similarity. */ diff --git a/core/src/main/java/de/jplag/options/SimilarityMetric.java b/core/src/main/java/de/jplag/options/SimilarityMetric.java index 08d00f78d0..4cefb9ff70 100644 --- a/core/src/main/java/de/jplag/options/SimilarityMetric.java +++ b/core/src/main/java/de/jplag/options/SimilarityMetric.java @@ -10,14 +10,7 @@ public enum SimilarityMetric implements ToDoubleFunction { MIN("minimum similarity", JPlagComparison::minimalSimilarity), MAX("maximal similarity", JPlagComparison::maximalSimilarity), INTERSECTION("matched tokens", it -> (double) it.getNumberOfMatchedTokens()), - SYMMETRIC("symmetric similarity", it -> { - int divisor = it.firstSubmission().getNumberOfTokens() + it.secondSubmission().getNumberOfTokens(); - if (divisor != 0) { - return 2.0 * it.getNumberOfMatchedTokens() / divisor; - } else { - return .0; - } - }), + SYMMETRIC("symmetric similarity", JPlagComparison::symmetricSimilarity), LONGEST_MATCH("number of tokens in the longest match", it -> it.matches().stream().mapToInt(Match::length).max().orElse(0)), OVERALL("Sum of both submission lengths", it -> it.firstSubmission().getNumberOfTokens() + it.secondSubmission().getNumberOfTokens()); From c69818a0337b19e74d3626903c9c1095ab743f77 Mon Sep 17 00:00:00 2001 From: Alex | Kronox Date: Thu, 28 Nov 2024 08:26:27 +0100 Subject: [PATCH 11/11] fix core tests --- .../reportobject/mapper/MetricMapperTest.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) 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 7faa55fbb3..afd334110a 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 @@ -49,8 +49,8 @@ public void test_getDistributions() { public void test_getTopComparisons() { // given JPlagResult jPlagResult = createJPlagResult(distribution(EXPECTED_AVG_DISTRIBUTION), distribution(EXPECTED_MAX_DISTRIBUTION), - comparison(submission("1", 22), submission("2", 30), .7, .8, .5, new int[] {9, 3, 1}), - comparison(submission("3", 202), submission("4", 134), .3, .9, .01, new int[] {1, 15, 23, 3})); + comparison(submission("1", 22), submission("2", 30), .7, .8, .5, .5, new int[] {9, 3, 1}), + comparison(submission("3", 202), submission("4", 134), .3, .9, .01, .25, new int[] {1, 15, 23, 3})); // when List result = metricMapper.getTopComparisons(jPlagResult); @@ -78,12 +78,12 @@ private CreateSubmission submission(String name) { } private Comparison comparison(CreateSubmission submission1, CreateSubmission submission2, double similarity, double maxSimilarity, - double minSimilarity, int[] matchLengths) { - return new Comparison(submission1, submission2, similarity, maxSimilarity, minSimilarity, matchLengths); + double minSimilarity, double symSimilarity, int[] matchLengths) { + return new Comparison(submission1, submission2, similarity, maxSimilarity, minSimilarity, symSimilarity, matchLengths); } private Comparison comparison(CreateSubmission submission1, CreateSubmission submission2, double similarity, double maxSimilarity) { - return comparison(submission1, submission2, similarity, maxSimilarity, 0, new int[0]); + return comparison(submission1, submission2, similarity, maxSimilarity, 0, 0, new int[0]); } private JPlagResult createJPlagResult(int[] avgDistribution, int[] maxDistribution, Comparison... createComparisonsDto) { @@ -110,6 +110,7 @@ private JPlagResult createJPlagResult(int[] avgDistribution, int[] maxDistributi doReturn(comparisonDto.similarity).when(mockedComparison).similarity(); doReturn(comparisonDto.maxSimilarity).when(mockedComparison).maximalSimilarity(); doReturn(comparisonDto.minSimilarity).when(mockedComparison).minimalSimilarity(); + doReturn(comparisonDto.symSimilarity).when(mockedComparison).symmetricSimilarity(); List matches = createMockMatchList(comparisonDto.matchLengths); doReturn(matches).when(mockedComparison).matches(); doReturn(matches.stream().mapToInt(Match::length).sum()).when(mockedComparison).getNumberOfMatchedTokens(); @@ -131,7 +132,7 @@ private List createMockMatchList(int[] matchLengths) { } private record Comparison(CreateSubmission submission1, CreateSubmission submission2, double similarity, double maxSimilarity, - double minSimilarity, int[] matchLengths) { + double minSimilarity, double symSimilarity, int[] matchLengths) { } private record CreateSubmission(String name, int tokenCount) {