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

Programming exercises: Add visualization of test case errors #9213

Merged
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
c6b7629
added template for advanced errorfiltering
az108 Aug 13, 2024
e4b0294
removed example table and added real exercise data
az108 Aug 14, 2024
cbf61d9
table works for exercises, tests missing
az108 Aug 15, 2024
189e343
added client side tests
az108 Aug 15, 2024
9ea9a24
fixed client side tests
az108 Aug 15, 2024
656361b
Merge branch 'develop' into feature/programming-exercises/add-advance…
az108 Aug 15, 2024
ddcc330
implemented code rabbit feedback
az108 Aug 15, 2024
240ad84
Merge remote-tracking branch 'origin/feature/programming-exercises/ad…
az108 Aug 15, 2024
46dea01
implemented code rabbit feedback
az108 Aug 15, 2024
cd01d6a
code coverage
az108 Aug 15, 2024
5b53635
code coverage
az108 Aug 15, 2024
5e2246e
code coverage
az108 Aug 15, 2024
68ba2cb
implemented feedback
az108 Aug 16, 2024
a77800a
Merge branch 'develop' into feature/programming-exercises/add-advance…
az108 Aug 16, 2024
2a7a9e2
implemented feedback
az108 Aug 16, 2024
c2104de
Merge remote-tracking branch 'origin/feature/programming-exercises/ad…
az108 Aug 16, 2024
62be8c9
implemented feedback
az108 Aug 16, 2024
ce673ab
small adjustment to failing test
az108 Aug 16, 2024
6d32b02
small adjustment to failing test
az108 Aug 16, 2024
737ea02
small adjustment to failing test
az108 Aug 16, 2024
f256248
adjusted performance even more and added query
az108 Aug 17, 2024
fc9ea87
adjusted performance even more and implemented feedback
az108 Aug 17, 2024
6bf690b
scss removed for now
az108 Aug 17, 2024
231ed5d
coderabbit
az108 Aug 17, 2024
957b592
server style
az108 Aug 17, 2024
016336b
optimized again
az108 Aug 18, 2024
c87c056
fixed client test
az108 Aug 18, 2024
4b22236
coderabbit
az108 Aug 18, 2024
cf9064b
tests
az108 Aug 18, 2024
4505ef6
tests
az108 Aug 18, 2024
aaec498
moved interface
az108 Aug 18, 2024
3511e79
fixed import
az108 Aug 18, 2024
99602bc
feedback implemented
az108 Aug 18, 2024
cced3ab
feedback implemented
az108 Aug 18, 2024
50795d7
feedback implemented
az108 Aug 18, 2024
faf33cc
feedback Markus/Ramona implemented
az108 Aug 19, 2024
d5fa0f7
feedback Ramona/Markus implemented
az108 Aug 20, 2024
90d0b8a
feedback Ramona/Markus implemented
az108 Aug 20, 2024
21bc58f
client test fix
az108 Aug 20, 2024
44360d6
server test fix
az108 Aug 20, 2024
586134c
Merge branch 'develop' into feature/programming-exercises/add-advance…
az108 Aug 20, 2024
6d18aa4
folder update
az108 Aug 20, 2024
acbb22a
Merge remote-tracking branch 'origin/feature/programming-exercises/ad…
az108 Aug 20, 2024
793332f
folder name update
az108 Aug 20, 2024
30f99d4
Merge branch 'develop' into feature/programming-exercises/add-advance…
az108 Aug 20, 2024
38f4e6d
removed class from html
az108 Aug 20, 2024
ffb7060
Merge remote-tracking branch 'origin/feature/programming-exercises/ad…
az108 Aug 20, 2024
1178d8d
removed isAtLeastEditor from component
az108 Aug 20, 2024
f7a9e57
removed isAtLeastEditor from component
az108 Aug 20, 2024
9079493
test adjusted
az108 Aug 20, 2024
0f40fbd
updated performance feedback
az108 Aug 25, 2024
4205190
service tests updated
az108 Aug 25, 2024
1336f1e
server style
az108 Aug 25, 2024
9ab8f79
Merge branch 'develop' into feature/programming-exercises/add-advance…
az108 Aug 25, 2024
3f67553
adjusted performance even more
az108 Aug 25, 2024
d13ee4d
adjusted performance even more
az108 Aug 25, 2024
0ba085f
Merge remote-tracking branch 'origin/feature/programming-exercises/ad…
az108 Aug 25, 2024
e295a19
adjusted performance even more
az108 Aug 25, 2024
aca6ffa
fixed calculation
az108 Aug 25, 2024
ff84548
feedback implemented
az108 Aug 25, 2024
978acee
Merge branch 'develop' into feature/programming-exercises/add-advance…
az108 Aug 28, 2024
ed8e1a6
server style
az108 Aug 28, 2024
9c0d094
Merge remote-tracking branch 'origin/feature/programming-exercises/ad…
az108 Aug 28, 2024
d2970f6
translation file
az108 Aug 29, 2024
d461a88
Merge branch 'develop' into feature/programming-exercises/add-advance…
MarkusPaulsen Sep 3, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,23 @@ <h3 class="fw-medium" jhiTranslate="artemisApp.programmingExercise.configureGrad
<div class="tab-item submission-policy" (click)="selectTab('submission-policy')" [ngClass]="activeTab === 'submission-policy' ? 'active' : ''">
<b>Submission Policy</b>
</div>
<div class="tab-item test-analysis" (click)="selectTab('test-analysis')" [ngClass]="activeTab === 'test-analysis' ? 'active' : ''">
<b>Test Analysis</b>
</div>
az108 marked this conversation as resolved.
Show resolved Hide resolved
</div>
<ng-template>
<div></div>
</ng-template>
<div class="d-flex align-items-center">
@if (activeTab !== 'submission-policy') {
@if (activeTab !== 'submission-policy' && activeTab !== 'test-analysis') {
az108 marked this conversation as resolved.
Show resolved Hide resolved
<jhi-programming-exercise-configure-grading-status
[exerciseIsReleasedAndHasResults]="isReleasedAndHasResults"
[hasUnsavedTestCaseChanges]="hasUnsavedChanges"
[hasUnsavedCategoryChanges]="!!changedCategoryIds.length"
[hasUpdatedGradingConfig]="hasUpdatedGradingConfig"
/>
}
@if (programmingExercise.isAtLeastInstructor) {
@if (programmingExercise.isAtLeastInstructor && activeTab !== 'test-analysis') {
<jhi-programming-exercise-configure-grading-actions
[exercise]="programmingExercise"
[hasUpdatedGradingConfig]="hasUpdatedGradingConfig"
Expand Down Expand Up @@ -256,5 +259,12 @@ <h2 class="mb-5 fw-medium">
</div>
}
</div>
<div class="grading-body-container mt-3">
@if (activeTab === 'test-analysis') {
<div>
<jhi-testcase-analysis [exerciseTitle]="programmingExercise.title" />
</div>
az108 marked this conversation as resolved.
Show resolved Hide resolved
}
</div>
}
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const DefaultFieldValues: { [key: string]: number } = {
[EditableField.MAX_PENALTY]: 0,
};

export type GradingTab = 'test-cases' | 'code-analysis' | 'submission-policy';
export type GradingTab = 'test-cases' | 'code-analysis' | 'submission-policy' | 'test-analysis';
az108 marked this conversation as resolved.
Show resolved Hide resolved
az108 marked this conversation as resolved.
Show resolved Hide resolved

export type Table = 'testCases' | 'codeAnalysis';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { SubmissionPolicyUpdateModule } from 'app/exercises/shared/submission-po
import { ProgrammingExerciseGradingTasksTableComponent } from 'app/exercises/programming/manage/grading/tasks/programming-exercise-grading-tasks-table.component';
import { BarChartModule } from '@swimlane/ngx-charts';
import { ProgrammingExerciseTaskComponent } from 'app/exercises/programming/manage/grading/tasks/programming-exercise-task/programming-exercise-task.component';
import { TestcaseAnalysisComponent } from 'app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component';

@NgModule({
imports: [
Expand All @@ -42,6 +43,7 @@ import { ProgrammingExerciseTaskComponent } from 'app/exercises/programming/mana
ProgrammingExerciseGradingTableActionsComponent,
ProgrammingExerciseGradingSubmissionPolicyConfigurationActionsComponent,
ProgrammingExerciseGradingTasksTableComponent,
TestcaseAnalysisComponent,
TestCasePassedBuildsChartComponent,
CategoryIssuesChartComponent,
TestCaseDistributionChartComponent,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<div class="container">
<h2>{{ 'artemisApp.programmingExercise.configureGrading.testAnalysis.title' | artemisTranslate: { exerciseTitle: exerciseTitle } }}</h2>
az108 marked this conversation as resolved.
Show resolved Hide resolved
<table class="table">
<thead>
<tr>
<th scope="col" jhiTranslate="artemisApp.programmingExercise.configureGrading.testAnalysis.occurrence"></th>
<th scope="col" jhiTranslate="artemisApp.programmingExercise.configureGrading.testAnalysis.testCaseFeedback"></th>
<th scope="col" jhiTranslate="artemisApp.programmingExercise.configureGrading.testAnalysis.task"></th>
<th scope="col" jhiTranslate="artemisApp.programmingExercise.configureGrading.testAnalysis.testcase"></th>
<th scope="col" jhiTranslate="artemisApp.programmingExercise.configureGrading.testAnalysis.errorCategory"></th>
</tr>
</thead>
<tbody class="table-group-divider">
@for (item of feedbacks; track item) {
<tr>
<td class="text-center">{{ item.count }} ({{ getRelativeCount(item.count) | number: '1.0-0' }}%)</td>
az108 marked this conversation as resolved.
Show resolved Hide resolved
<td>{{ item.detailText }}</td>
<td class="text-center">{{ item.task }}</td>
<td>{{ item.testcase }}</td>
<td>Student Error</td>
<!-- This is a placeholder, will be covered in follow up PRs -->
</tr>
}
</tbody>
</table>
<div>{{ 'artemisApp.programmingExercise.configureGrading.testAnalysis.totalItems' | artemisTranslate: { count: feedbacks.length } }}</div>
az108 marked this conversation as resolved.
Show resolved Hide resolved
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.container {
margin: 20px;
}

h2 {
margin-bottom: 20px;
}
az108 marked this conversation as resolved.
Show resolved Hide resolved

.table {
width: 100%;
az108 marked this conversation as resolved.
Show resolved Hide resolved
margin-bottom: 1rem;
color: #212529;
az108 marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { Component, Input, OnInit } from '@angular/core';
az108 marked this conversation as resolved.
Show resolved Hide resolved
import { Feedback } from 'app/entities/feedback.model';
import { ProgrammingExerciseTaskService } from 'app/exercises/programming/manage/grading/tasks/programming-exercise-task.service';
import { ResultService } from 'app/exercises/shared/result/result.service';
import { Participation } from 'app/entities/participation/participation.model';
import { ParticipationService } from 'app/exercises/shared/participation/participation.service';
import { ProgrammingExerciseTask } from 'app/exercises/programming/manage/grading/tasks/programming-exercise-task';
import { ProgrammingExerciseTestCase } from 'app/entities/programming-exercise-test-case.model';

// Define the structure for FeedbackDetail
az108 marked this conversation as resolved.
Show resolved Hide resolved
type FeedbackDetail = {
count: number;
detailText: string;
testcase: string;
task: number;
};

@Component({
selector: 'jhi-testcase-analysis',
templateUrl: './testcase-analysis.component.html',
styleUrls: ['./testcase-analysis.component.scss'],
})
az108 marked this conversation as resolved.
Show resolved Hide resolved
export class TestcaseAnalysisComponent implements OnInit {
@Input() exerciseTitle?: string;
participation: Participation[] = [];
tasks: ProgrammingExerciseTask[] = [];
feedbacks: FeedbackDetail[] = [];

constructor(
private participationService: ParticipationService,
private resultService: ResultService,
private programmingExerciseTaskService: ProgrammingExerciseTaskService,
) {}

ngOnInit(): void {
if (this.programmingExerciseTaskService.exercise.id != undefined) {
// Find all participations for the programming exercise and instantiate the feedbacks array with the FeedbackDetail structure
this.participationService.findAllParticipationsByExercise(this.programmingExerciseTaskService.exercise.id, true).subscribe((participationsResponse) => {
this.participation = participationsResponse.body ?? [];
this.loadFeedbacks(this.participation);
});
}
// Load tasks for the programming exercise
this.tasks = this.programmingExerciseTaskService.updateTasks();
}
az108 marked this conversation as resolved.
Show resolved Hide resolved

loadFeedbacks(participations: Participation[]): void {
// Iterate over all participations, get feedback details for each result, and filter them for negative feedback
participations.forEach((participation) => {
participation.results?.forEach((result) => {
this.resultService.getFeedbackDetailsForResult(participation.id!, result).subscribe((response) => {
const feedbackArray = response.body ?? [];
const negativeFeedbackArray = feedbackArray.filter((feedback) => !feedback.positive); // Filter out positive feedback
this.saveFeedbacks(negativeFeedbackArray); // Save only negative feedback
});
});
});
az108 marked this conversation as resolved.
Show resolved Hide resolved
}
az108 marked this conversation as resolved.
Show resolved Hide resolved

saveFeedbacks(feedbackArray: Feedback[]): void {
// Iterate over all feedback and save them in the feedbacks array
// If a feedback with the corresponding testcase already exists in the list, then the count is incremented; otherwise, a new FeedbackDetail is added
feedbackArray.forEach((feedback) => {
const feedbackText = feedback.detailText ?? '';
const existingFeedback = this.feedbacks.find((f) => f.detailText === feedbackText && f.testcase === feedback.testCase?.testName);
if (existingFeedback) {
existingFeedback.count += 1; // Increment count if feedback already exists
} else {
const task = this.findTaskIndexForTestCase(feedback.testCase); // Find the task index for the test case
this.feedbacks.push(<FeedbackDetail>{
count: 1,
detailText: feedback.detailText ?? '',
testcase: feedback.testCase?.testName,
task: task,
});
}
});
this.feedbacks.sort((a, b) => b.count - a.count); // Sort feedback by count in descending order
}
az108 marked this conversation as resolved.
Show resolved Hide resolved

findTaskIndexForTestCase(testCase?: ProgrammingExerciseTestCase): number | undefined {
az108 marked this conversation as resolved.
Show resolved Hide resolved
if (!testCase) {
return undefined;
}
// Find the index of the task and add 1 to it (to make it a 1-based index), if 0 is returned then no element was found
return this.tasks.findIndex((task) => task.testCases.some((tc) => tc.testName === testCase.testName)) + 1;
az108 marked this conversation as resolved.
Show resolved Hide resolved
}

// Used to calculate the relative occurrence of a feedback
getRelativeCount(count: number): number {
return (this.participation.length > 0 ? count / this.participation.length : 0) * 100;
}
}
9 changes: 9 additions & 0 deletions src/main/webapp/i18n/de/programmingExercise.json
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,15 @@
"testType": "Type",
"passedPercent": "Bestanden %"
},
"testAnalysis": {
"title": "Error Analyse für {{exerciseTitle}}",
az108 marked this conversation as resolved.
Show resolved Hide resolved
"occurrence": "Häufigkeit",
"testCaseFeedback": "Test Fall Feedback",
az108 marked this conversation as resolved.
Show resolved Hide resolved
"task": "Task",
az108 marked this conversation as resolved.
Show resolved Hide resolved
"testcase": "Test Fall",
"errorCategory": "Error Kategorie",
az108 marked this conversation as resolved.
Show resolved Hide resolved
"totalItems": "Insgesamt {{count}} items"
az108 marked this conversation as resolved.
Show resolved Hide resolved
},
"help": {
"name": "Aufgabennamen werden fett geschrieben, während Testnamen normal sind. Ob es ein Aufgabenname oder Testname ist hängt davon ab, ob die Reihe eine Aufgabe oder einen Test darstellt.",
"state": "Gibt an, ob Issues in dieser Kategorie den Studierenden angezeigt und bewertet werden sollen.",
Expand Down
9 changes: 9 additions & 0 deletions src/main/webapp/i18n/en/programmingExercise.json
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,15 @@
"testType": "Type",
"passedPercent": "Passed %"
},
"testAnalysis": {
"title": "Error Analysis for {{exerciseTitle}}",
"occurrence": "Occurrency",
"testCaseFeedback": "Test Case Feedback",
"task": "Task",
"testcase": "Testcase",
"errorCategory": "Error Category",
"totalItems": "Total {{count}} items"
az108 marked this conversation as resolved.
Show resolved Hide resolved
},
az108 marked this conversation as resolved.
Show resolved Hide resolved
"help": {
"name": "Task names are written in bold whereas Test names are normal. Task or test name depending on whether the row is a task or test.",
"state": "Determines whether issues in this category should be shown to the students and used for grading.",
Expand Down
az108 marked this conversation as resolved.
Show resolved Hide resolved
az108 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { of } from 'rxjs';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
az108 marked this conversation as resolved.
Show resolved Hide resolved
import { MockTranslateService } from '../../../helpers/mocks/service/mock-translate.service';
import { ArtemisTestModule } from '../../../test.module';
import { TestcaseAnalysisComponent } from 'app/exercises/programming/manage/grading/testcase-analysis/testcase-analysis.component';
import { ParticipationService } from 'app/exercises/shared/participation/participation.service';
import { ResultService } from 'app/exercises/shared/result/result.service';
import { ProgrammingExerciseTaskService } from 'app/exercises/programming/manage/grading/tasks/programming-exercise-task.service';
import { Participation } from 'app/entities/participation/participation.model';
import { Feedback } from 'app/entities/feedback.model';
import { ProgrammingExerciseTask } from 'app/exercises/programming/manage/grading/tasks/programming-exercise-task';
import { ProgrammingExerciseTestCase } from 'app/entities/programming-exercise-test-case.model';
import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe';
import { ButtonComponent } from 'app/shared/components/button.component';
import { MockComponent, MockPipe } from 'ng-mocks';
import { HttpResponse } from '@angular/common/http';

describe('TestcaseAnalysisComponent', () => {
let component: TestcaseAnalysisComponent;
let fixture: ComponentFixture<TestcaseAnalysisComponent>;
let participationService: ParticipationService;
let resultService: ResultService;

const participationMock: Participation[] = [
{
id: 1,
results: [{ id: 1 }],
},
] as Participation[];

const feedbackMock: Feedback[] = [
{
text: 'Test feedback 1',
positive: false,
detailText: 'Test feedback 1 detail',
testCase: { testName: 'test1' } as ProgrammingExerciseTestCase,
},
{
text: 'Test feedback 2',
positive: false,
detailText: 'Test feedback 2 detail',
testCase: { testName: 'test2' } as ProgrammingExerciseTestCase,
},
] as Feedback[];

const tasksMock: ProgrammingExerciseTask[] = [
{ id: 1, taskName: 'Task 1', testCases: [{ testName: 'test1' } as ProgrammingExerciseTestCase] },
{ id: 2, taskName: 'Task 2', testCases: [{ testName: 'test2' } as ProgrammingExerciseTestCase] },
] as ProgrammingExerciseTask[];

const participationResponseMock = new HttpResponse({ body: participationMock });
const feedbackResponseMock = new HttpResponse({ body: feedbackMock });

beforeEach(() => {
const mockProgrammingExerciseTaskService = {
exercise: { id: 1 }, // Mock the exercise with an id
updateTasks: jest.fn().mockReturnValue(tasksMock),
};

TestBed.configureTestingModule({
imports: [ArtemisTestModule, TranslateModule.forRoot()],
declarations: [TestcaseAnalysisComponent, MockPipe(ArtemisTranslatePipe), MockComponent(ButtonComponent)],
providers: [
{ provide: TranslateService, useClass: MockTranslateService },
ParticipationService,
ResultService,
{ provide: ProgrammingExerciseTaskService, useValue: mockProgrammingExerciseTaskService },
],
}).compileComponents();

fixture = TestBed.createComponent(TestcaseAnalysisComponent);
component = fixture.componentInstance;
participationService = TestBed.inject(ParticipationService);
resultService = TestBed.inject(ResultService);

jest.spyOn(participationService, 'findAllParticipationsByExercise').mockReturnValue(of(participationResponseMock));
jest.spyOn(resultService, 'getFeedbackDetailsForResult').mockReturnValue(of(feedbackResponseMock));
});

it('should initialize and load feedbacks correctly', () => {
component.ngOnInit();
fixture.detectChanges();

expect(participationService.findAllParticipationsByExercise).toHaveBeenCalled();
expect(resultService.getFeedbackDetailsForResult).toHaveBeenCalled();
expect(component.participation).toEqual(participationMock);
expect(component.feedbacks).toHaveLength(2);
expect(component.feedbacks[0].detailText).toBe('Test feedback 1 detail');
});

it('should save feedbacks and sort them by count', () => {
component.saveFeedbacks(feedbackMock);

expect(component.feedbacks).toHaveLength(2);
expect(component.feedbacks[0].count).toBe(1);
expect(component.feedbacks[1].count).toBe(1);
expect(component.feedbacks[0].detailText).toBe('Test feedback 1 detail');
});

it('should find task index for a given test case', () => {
component.tasks = tasksMock;
const index = component.findTaskIndexForTestCase({ testName: 'test1' } as ProgrammingExerciseTestCase);
expect(index).toBe(1);

const zeroIndex = component.findTaskIndexForTestCase({ testName: 'test3' } as ProgrammingExerciseTestCase);
expect(zeroIndex).toBe(0);
});

it('should calculate relative count correctly', () => {
component.participation = participationMock;
const relativeCount = component.getRelativeCount(1);
expect(relativeCount).toBe(100);

const zeroRelativeCount = component.getRelativeCount(0);
expect(zeroRelativeCount).toBe(0);
});
});
Loading