diff --git a/build.gradle b/build.gradle index d3a0a9dacbe0..8e60cc1af9f4 100644 --- a/build.gradle +++ b/build.gradle @@ -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" diff --git a/src/main/java/de/tum/in/www1/artemis/domain/plagiarism/text/TextPlagiarismResult.java b/src/main/java/de/tum/in/www1/artemis/domain/plagiarism/text/TextPlagiarismResult.java index c9b6167b7a4b..57ad6a6efdc1 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/plagiarism/text/TextPlagiarismResult.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/plagiarism/text/TextPlagiarismResult.java @@ -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; @@ -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); } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/plagiarism/ProgrammingPlagiarismDetectionService.java b/src/main/java/de/tum/in/www1/artemis/service/plagiarism/ProgrammingPlagiarismDetectionService.java index 5b4fdb8231ea..14918cee4195 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/plagiarism/ProgrammingPlagiarismDetectionService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/plagiarism/ProgrammingPlagiarismDetectionService.java @@ -345,10 +345,11 @@ private List downloadRepositories(ProgrammingExercise programmingExe // Used for sending progress notifications var topic = plagiarismWebsocketService.getProgrammingExercisePlagiarismCheckTopic(programmingExercise.getId()); + int maxRepositories = participations.size() + 1; List 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); @@ -363,6 +364,9 @@ private List 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); diff --git a/src/main/webapp/app/exercises/shared/plagiarism/plagiarism-split-view/text-submission-viewer/text-submission-viewer.component.ts b/src/main/webapp/app/exercises/shared/plagiarism/plagiarism-split-view/text-submission-viewer/text-submission-viewer.component.ts index cd5ea339ba5a..7381c79470b8 100644 --- a/src/main/webapp/app/exercises/shared/plagiarism/plagiarism-split-view/text-submission-viewer/text-submission-viewer.component.ts +++ b/src/main/webapp/app/exercises/shared/plagiarism/plagiarism-split-view/text-submission-viewer/text-submission-viewer.component.ts @@ -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; @@ -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) { + 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; } } diff --git a/src/test/javascript/spec/component/plagiarism/text-submission-viewer.component.spec.ts b/src/test/javascript/spec/component/plagiarism/text-submission-viewer.component.spec.ts index 042ab9830648..699d50ced723 100644 --- a/src/test/javascript/spec/component/plagiarism/text-submission-viewer.component.spec.ts +++ b/src/test/javascript/spec/component/plagiarism/text-submission-viewer.component.spec.ts @@ -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 = ''; - const position = 4; - const expectedResult = 'This 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 = `Lorem ipsum dolor sit amet.\nConsetetur sadipscing elitr.`; - 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 = ''; - const position = 20; - const expectedResult = 'This is a test'; + it('should escape the text if no matches are present', () => { + jest.spyOn(comp, 'getMatchesForCurrentFile').mockReturnValue([]); + const fileContent = 'Lorem ipsum dolor sit amet.\n'; + const expectedFileContent = 'Lorem ipsum dolor sit amet.\n<test>'; - 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, @@ -199,9 +224,47 @@ describe('Text Submission Viewer Component', () => { }, ]; jest.spyOn(comp, 'getMatchesForCurrentFile').mockReturnValue(mockMatches); + const fileContent = 'Lorem ipsum dolor sit amet.\n test text for inserting tokens'; + const expectedFileContent = + 'Lorem ipsum <fake-token>dolor sit amet.\n' + + '<test> test text for inserting tokens'; - const fileContent = `Lorem ipsum dolor sit amet.\nConsetetur sadipscing elitr.`; - const expectedFileContent = `Lorem ipsum dolor sit amet.\nConsetetur sadipscing elitr.`; + 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 dolor sit amet.'; + // TODO double check that, this result seems wrong + const expectedFileContent = 'Lorem ipsum <fake-token>dolor sit amet.'; const updatedFileContent = comp.insertMatchTokens(fileContent);