Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Plagiarism check: Fix display of escaped characters #6123

Merged
merged 15 commits into from
Feb 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ spotless {
java {
target project.fileTree(project.rootDir) {
include "**/*.java"
exclude "**/src/main/java/de/tum/in/www1/artemis/service/connectors/BambooService.java", "**/src/test/resources/test-data/repository-export/EncodingISO_8559_1.java", "**/node_modules/**", "**/out/**", "**/repos/**", "**/build/**", "**/src/main/generated/**", "**/src/main/resources/templates/**"
exclude "**/src/main/java/de/tum/in/www1/artemis/service/connectors/BambooService.java", "**/src/test/resources/test-data/repository-export/EncodingISO_8559_1.java", "**/node_modules/**", "**/out/**", "**/repos/**", "**/repos-download/**", "**/build/**", "**/src/main/generated/**", "**/src/main/resources/templates/**"
}
importOrderFile "artemis-spotless.importorder"
eclipse("4.19.0").configFile "artemis-spotless-style.xml"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

import javax.persistence.Entity;

import org.apache.commons.lang3.ArrayUtils;

import de.jplag.JPlagResult;
import de.tum.in.www1.artemis.domain.Exercise;
import de.tum.in.www1.artemis.domain.plagiarism.PlagiarismComparison;
Expand Down Expand Up @@ -31,11 +29,7 @@ public void convertJPlagResult(JPlagResult result, Exercise exercise) {
this.comparisons.add(comparison);
}
this.duration = result.getDuration();
// NOTE: there seems to be an issue in JPlag 4.0 that the similarity distribution is reversed, either in the implementation or in the documentation.
// we use it like this: 0: [0% - 10%), 1: [10% - 20%), 2: [20% - 30%), ..., 9: [90% - 100%] so we reverse it
var similarityDistribution = result.getSimilarityDistribution();
ArrayUtils.reverse(similarityDistribution);
this.setSimilarityDistribution(similarityDistribution);
this.setSimilarityDistribution(result.getSimilarityDistribution());
this.setExercise(exercise);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -345,10 +345,11 @@ private List<Repository> downloadRepositories(ProgrammingExercise programmingExe
// Used for sending progress notifications
var topic = plagiarismWebsocketService.getProgrammingExercisePlagiarismCheckTopic(programmingExercise.getId());

int maxRepositories = participations.size() + 1;
List<Repository> downloadedRepositories = new ArrayList<>();
participations.parallelStream().forEach(participation -> {
try {
var progressMessage = "Downloading repositories: " + (downloadedRepositories.size() + 1) + "/" + participations.size();
var progressMessage = "Downloading repositories: " + (downloadedRepositories.size() + 1) + "/" + maxRepositories;
plagiarismWebsocketService.notifyInstructorAboutPlagiarismState(topic, PlagiarismCheckState.RUNNING, List.of(progressMessage));

Repository repo = gitService.getOrCheckoutRepositoryForJPlag(participation, targetPath);
Expand All @@ -363,6 +364,9 @@ private List<Repository> downloadRepositories(ProgrammingExercise programmingExe

// clone the template repo
try {
var progressMessage = "Downloading repositories: " + maxRepositories + "/" + maxRepositories;
plagiarismWebsocketService.notifyInstructorAboutPlagiarismState(topic, PlagiarismCheckState.RUNNING, List.of(progressMessage));

Repository templateRepo = gitService.getOrCheckoutRepository(programmingExercise.getTemplateParticipation(), targetPath);
gitService.resetToOriginHead(templateRepo); // start with clean state
downloadedRepositories.add(templateRepo);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ export class TextSubmissionViewerComponent implements OnChanges {
this.repositoryService.getFile(file, domain).subscribe({
next: ({ fileContent }) => {
this.loading = false;
this.fileContent = this.insertMatchTokens(escape(fileContent));
this.fileContent = this.insertMatchTokens(fileContent);
},
error: () => {
this.loading = false;
Expand All @@ -201,47 +201,67 @@ export class TextSubmissionViewerComponent implements OnChanges {
return this.matches.has(file);
}

insertToken(text: string, token: string, position: number) {
// prevent negative values because slice does not handle them as we would wish
if (position < 0) {
position = 0;
insertMatchTokens(fileContent: string): string {
const matches = this.getMatchesForCurrentFile()
.filter((match) => match.from && match.to)
.sort((m1, m2) => {
const lines = m1.from.line - m2.from.line;
if (lines === 0) {
return m1.from.column - m2.from.column;
}
return lines;
});

if (!matches.length) {
return escape(fileContent);
}
return [text.slice(0, position), token, text.slice(position)].join('');
}

insertMatchTokens(fileContent: string) {
const rows = fileContent.split('\n');
const matches = this.getMatchesForCurrentFile();
const offsets = new Array(rows.length).fill(0);
let result = '';

matches.forEach((match) => {
if (!match.from) {
captureException(new Error('"from" is not defined in insertMatchTokens'));
return;
}
if (!match.to) {
captureException(new Error('"to" is not defined in insertMatchTokens'));
return;
}
for (let i = 0; i < matches[0].from.line - 1; i++) {
result += escape(rows[i]) + '\n';
}
result += escape(rows[matches[0].from.line - 1].slice(0, matches[0].from.column - 1));

for (let i = 0; i < matches.length; i++) {
const match = matches[i];

const idxLineFrom = match.from.line - 1;
const idxLineTo = match.to.line - 1;

const idxColumnFrom = match.from.column - 1 + offsets[idxLineFrom];
const idxColumnFrom = match.from.column - 1;
const idxColumnTo = match.to.column + match.to.length - 1;

if (rows[idxLineFrom]) {
rows[idxLineFrom] = this.insertToken(rows[idxLineFrom], this.tokenStart, idxColumnFrom);
offsets[idxLineFrom] += this.tokenStart.length;
}
result += this.tokenStart;

const idxColumnTo = match.to.column + match.to.length - 1 + offsets[idxLineTo];
if (idxLineFrom === idxLineTo) {
result += escape(rows[idxLineFrom].slice(idxColumnFrom, idxColumnTo)) + this.tokenEnd;
} else {
result += escape(rows[idxLineFrom].slice(idxColumnFrom));
for (let j = idxLineFrom + 1; j < idxLineTo; j++) {
result += '\n' + escape(rows[j]);
}
result += '\n' + escape(rows[idxLineTo].slice(0, idxColumnTo)) + this.tokenEnd;
}

if (rows[idxLineTo]) {
rows[idxLineTo] = this.insertToken(rows[idxLineTo], this.tokenEnd, idxColumnTo);
offsets[idxLineTo] += this.tokenEnd.length;
// escape everything up until the next match (or the end of the string if there is no more match)
if (i === matches.length - 1) {
result += escape(rows[idxLineTo].slice(idxColumnTo));
for (let j = idxLineTo + 1; j < rows.length; j++) {
result += '\n' + escape(rows[j]);
}
} else if (matches[i + 1].from.line === match.to.line) {
Strohgelaender marked this conversation as resolved.
Show resolved Hide resolved
result += escape(rows[idxLineTo].slice(idxColumnTo, matches[i + 1].from.column - 1));
} else {
result += escape(rows[idxLineTo].slice(idxColumnTo)) + '\n';
for (let j = idxLineTo + 1; j < matches[i + 1].from.line - 1; j++) {
result += escape(rows[j]) + '\n';
}
result += escape(rows[matches[i + 1].from.line - 1].slice(0, matches[i + 1].from.column - 1));
}
});
}

return rows.join('\n');
return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -149,33 +149,58 @@ describe('Text Submission Viewer Component', () => {
expect(comp.currentFile).toEqual(fileName);
});

it('inserts a token', () => {
const base = 'This is a test';
const token = '<token>';
const position = 4;
const expectedResult = 'This<token> is a test';
it('should insert match tokens', () => {
const mockMatches = [
{
from: {
column: 1,
line: 1,
length: 5,
} as TextSubmissionElement,
to: {
column: 13,
line: 1,
length: 5,
} as TextSubmissionElement,
},
{
from: {
column: 1,
line: 2,
length: 10,
} as TextSubmissionElement,
to: {
column: 23,
line: 2,
length: 5,
} as TextSubmissionElement,
},
];
jest.spyOn(comp, 'getMatchesForCurrentFile').mockReturnValue(mockMatches);

const fileContent = `Lorem ipsum dolor sit amet.\nConsetetur sadipscing elitr.`;
const expectedFileContent = `<span class="plagiarism-match">Lorem ipsum dolor</span> sit amet.\n<span class="plagiarism-match">Consetetur sadipscing elitr</span>.`;

const result = comp.insertToken(base, token, position);
const updatedFileContent = comp.insertMatchTokens(fileContent);

expect(result).toEqual(expectedResult);
expect(updatedFileContent).toEqual(expectedFileContent);
});

it('appends a token', () => {
const base = 'This is a test';
const token = '<token>';
const position = 20;
const expectedResult = 'This is a test<token>';
it('should escape the text if no matches are present', () => {
jest.spyOn(comp, 'getMatchesForCurrentFile').mockReturnValue([]);
const fileContent = 'Lorem ipsum dolor sit amet.\n<test>';
const expectedFileContent = 'Lorem ipsum dolor sit amet.\n&lt;test&gt;';

const result = comp.insertToken(base, token, position);
const updatedFileContent = comp.insertMatchTokens(fileContent);

expect(result).toEqual(expectedResult);
expect(updatedFileContent).toEqual(expectedFileContent);
});

it('should insert match tokens', () => {
it('should escape and insert tokens', () => {
const mockMatches = [
{
from: {
column: 1,
column: 6,
line: 1,
length: 5,
} as TextSubmissionElement,
Expand All @@ -199,9 +224,47 @@ describe('Text Submission Viewer Component', () => {
},
];
jest.spyOn(comp, 'getMatchesForCurrentFile').mockReturnValue(mockMatches);
const fileContent = 'Lorem ipsum <fake-token>dolor sit amet.\n<test> test text for inserting tokens';
const expectedFileContent =
'Lorem<span class="plagiarism-match"> ipsum &lt;fake</span>-token&gt;dolor sit amet.\n' +
'<span class="plagiarism-match">&lt;test&gt; test text for insert</span>ing tokens';

const fileContent = `Lorem ipsum dolor sit amet.\nConsetetur sadipscing elitr.`;
const expectedFileContent = `<span class="plagiarism-match">Lorem ipsum dolor</span> sit amet.\n<span class="plagiarism-match">Consetetur sadipscing elitr</span>.`;
const updatedFileContent = comp.insertMatchTokens(fileContent);

expect(updatedFileContent).toEqual(expectedFileContent);
});

it('should insert tokens for multiple matches in one line', () => {
const mockMatches = [
{
from: {
column: 20,
line: 1,
length: 10,
} as TextSubmissionElement,
to: {
column: 30,
line: 1,
length: 5,
} as TextSubmissionElement,
},
{
from: {
column: 1,
line: 1,
length: 5,
} as TextSubmissionElement,
to: {
column: 5,
line: 1,
length: 5,
} as TextSubmissionElement,
},
];
jest.spyOn(comp, 'getMatchesForCurrentFile').mockReturnValue(mockMatches);
const fileContent = 'Lorem ipsum <fake-token>dolor sit amet.';
// TODO double check that, this result seems wrong
const expectedFileContent = '<span class="plagiarism-match">Lorem ips</span>um &lt;fake-t<span class="plagiarism-match">oken&gt;dolor sit </span>amet.';

const updatedFileContent = comp.insertMatchTokens(fileContent);

Expand Down