Skip to content

Commit 03f637e

Browse files
authored
Merge pull request #7 from wwu-cs/feat/testing-git-services
Implement Git and Testing Services
2 parents 8dd3209 + 909868d commit 03f637e

14 files changed

Lines changed: 711 additions & 177 deletions

package.json

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
"engines": {
77
"vscode": "^1.95.0"
88
},
9+
"extensionDependencies": [
10+
"vscode.git"
11+
],
912
"categories": [
1013
"Other"
1114
],
@@ -42,7 +45,7 @@
4245
"viewsContainers": {
4346
"activitybar": [
4447
{
45-
"id": "reactWebview",
48+
"id": "submittySidebar",
4649
"title": "Submitty",
4750
"icon": "media/duck.png"
4851
}
@@ -63,13 +66,6 @@
6366
"name": "Sidebar",
6467
"icon": "media/duck.png"
6568
}
66-
],
67-
"reactWebview": [
68-
{
69-
"type": "webview",
70-
"id": "reactWebview",
71-
"name": "React Webview"
72-
}
7369
]
7470
}
7571
},

src/extension.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,25 @@ import * as vscode from 'vscode';
22
import { SidebarProvider } from './sidebarProvider';
33
import ReactWebview from './reactWebview';
44
import { ExtensionContextUtil } from './util/extensionContextUtil';
5+
import { ApiService } from './services/apiService';
6+
import { TestingService } from './services/testingService';
57

68
export function activate(context: vscode.ExtensionContext) {
7-
ExtensionContextUtil.getExtensionContext(context)
8-
const sidebarProvider = new SidebarProvider(context);
9+
ExtensionContextUtil.getExtensionContext(context);
10+
const apiService = ApiService.getInstance(context, '');
11+
const testingService = new TestingService(context, apiService);
12+
const sidebarProvider = new SidebarProvider(context, testingService);
913

1014
context.subscriptions.push(
1115
vscode.window.registerWebviewViewProvider('submittyWebview', sidebarProvider)
1216
);
1317

14-
context.subscriptions.push(
15-
vscode.window.registerWebviewViewProvider(
16-
ReactWebview.viewType,
17-
ReactWebview.getInstance(context.extensionUri),
18-
)
19-
);
18+
// context.subscriptions.push(
19+
// vscode.window.registerWebviewViewProvider(
20+
// ReactWebview.viewType,
21+
// ReactWebview.getInstance(context.extensionUri),
22+
// )
23+
// );
2024
}
2125

2226
export function deactivate() { }
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
export interface AutoGraderDetails {
2+
status: string
3+
data: AutoGraderDetailsData
4+
}
5+
6+
export interface AutoGraderDetailsData {
7+
is_queued: boolean
8+
queue_position: number
9+
is_grading: boolean
10+
has_submission: boolean
11+
autograding_complete: boolean
12+
has_active_version: boolean
13+
highest_version: number
14+
total_points: number
15+
total_percent: number
16+
test_cases: TestCase[]
17+
}
18+
19+
export interface TestCase {
20+
name: string
21+
details: string
22+
is_extra_credit: boolean
23+
points_available: number
24+
has_extra_results: boolean
25+
points_received: number
26+
testcase_message: string
27+
autochecks: Autocheck[]
28+
}
29+
30+
export interface Autocheck {
31+
description: string
32+
messages: Message[]
33+
diff_viewer: Record<string, string>
34+
expected: string
35+
actual: string
36+
}
37+
38+
export interface Message {
39+
message: string
40+
type: string
41+
}
42+

src/interfaces/Courses.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export interface Course {
2+
semester: string;
3+
title: string;
4+
display_name: string;
5+
display_semester: string;
6+
user_group: number;
7+
registration_section: string;
8+
}

src/interfaces/Gradables.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export interface Gradable {
2+
id: string
3+
title: string
4+
instructions_url: string
5+
gradeable_type: string
6+
syllabus_bucket: string
7+
section: number
8+
section_name: string
9+
due_date: DueDate
10+
vcs_repository: string
11+
vcs_subdirectory: string
12+
}
13+
14+
export interface DueDate {
15+
date: string
16+
timezone_type: number
17+
timezone: string
18+
}

src/interfaces/Responses.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Course } from "./Courses";
2+
import { Gradable } from "./Gradables";
3+
4+
5+
export interface ApiResponse<T> {
6+
status: string;
7+
data: T;
8+
message?: string;
9+
}
10+
11+
export type CourseResponse = ApiResponse<{
12+
unarchived_courses: Course[];
13+
dropped_courses: Course[];
14+
}>;
15+
16+
export type LoginResponse = ApiResponse<{
17+
token: string;
18+
}>;
19+
20+
export type GradableResponse = ApiResponse<Gradable[]>;

src/reactWebview.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class ReactWebview implements WebviewViewProvider {
2828

2929
public resolveWebviewView(webviewView: WebviewView, _context: WebviewViewResolveContext, _token: CancellationToken): void | Thenable<void> {
3030
this._view = webviewView;
31-
31+
3232
this._view.webview.options = {
3333
enableScripts: true,
3434
localResourceRoots: [this._extensionUri]
@@ -147,4 +147,4 @@ class ReactWebview implements WebviewViewProvider {
147147
}
148148
}
149149

150-
export default ReactWebview;
150+
export default ReactWebview;

src/services/apiClient.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ export class ApiClient {
2121
// Add any request logging or modification here
2222
return config;
2323
},
24-
(error) => {
25-
return Promise.reject(error);
24+
(error: Error) => {
25+
return Promise.reject(new Error(error.message || 'Request failed'));
2626
}
2727
);
2828

@@ -31,9 +31,9 @@ export class ApiClient {
3131
(response) => {
3232
return response;
3333
},
34-
(error) => {
34+
(error: Error) => {
3535
// Handle common errors here
36-
return Promise.reject(error);
36+
return Promise.reject(new Error(error.message || 'Response failed'));
3737
}
3838
);
3939
}

src/services/apiService.ts

Lines changed: 95 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -3,31 +3,9 @@
33
import * as vscode from 'vscode';
44
import { ApiClient } from './apiClient';
55

6-
interface Course {
7-
semester: string;
8-
title: string;
9-
display_name: string;
10-
display_semester: string;
11-
user_group: number;
12-
registration_section: string;
13-
}
14-
15-
interface ApiResponse {
16-
status: string;
17-
data: {
18-
unarchived_courses: Course[];
19-
dropped_courses: Course[];
20-
};
21-
message?: string;
22-
}
23-
24-
interface LoginResponse {
25-
status: string;
26-
data: {
27-
token: string;
28-
};
29-
message?: string;
30-
}
6+
import { CourseResponse, LoginResponse, GradableResponse } from '../interfaces/Responses';
7+
import { AutoGraderDetails } from '../interfaces/AutoGraderDetails';
8+
319

3210
export class ApiService {
3311
private client: ApiClient;
@@ -70,48 +48,117 @@ export class ApiService {
7048
}
7149
}
7250

51+
async fetchMe(): Promise<any> {
52+
try {
53+
const response = await this.client.get<any>('/api/me');
54+
return response.data;
55+
} catch (error: any) {
56+
throw new Error(error.response?.data?.message || 'Failed to fetch me.');
57+
}
58+
}
59+
60+
7361
/**
7462
* Fetch all courses for the authenticated user
7563
*/
76-
async fetchCourses(token: string): Promise<ApiResponse> {
64+
async fetchCourses(token?: string): Promise<CourseResponse> {
7765
try {
78-
const response = await this.client.get<ApiResponse>('/api/courses', {
79-
headers: {
80-
Authorization: token,
81-
},
82-
});
66+
const response = await this.client.get<CourseResponse>('/api/courses');
8367
return response.data;
8468
} catch (error: any) {
8569
console.error('Error fetching courses:', error);
8670
throw new Error(error.response?.data?.message || 'Failed to fetch courses.');
8771
}
8872
}
8973

74+
async fetchGradables(courseId: string, term: string): Promise<GradableResponse> {
75+
try {
76+
const url = `/api/${term}/${courseId}/gradeables`;
77+
const response = await this.client.get<GradableResponse>(url);
78+
return response.data;
79+
} catch (error: any) {
80+
console.error('Error fetching gradables:', error);
81+
throw new Error(error.response?.data?.message || 'Failed to fetch gradables.');
82+
}
83+
}
84+
9085
/**
9186
* Fetch grade details for a specific homework assignment
9287
*/
93-
async fetchGradeDetails(hw: string): Promise<any> {
94-
// Hardcoded grade details
95-
return {
96-
score: '25/40',
97-
tests: [
98-
{ name: 'Test 1', passed: true },
99-
{ name: 'Test 2', passed: false },
100-
{ name: 'Test 3', passed: true },
101-
{ name: 'Test 4', passed: false },
102-
]
103-
};
88+
async fetchGradeDetails(term: string, courseId: string, gradeableId: string): Promise<AutoGraderDetails> {
89+
try {
90+
const response = await this.client.get<AutoGraderDetails>(`/api/${term}/${courseId}/gradeable/${gradeableId}/values`);
91+
return response.data;
92+
} catch (error: any) {
93+
console.error('Error fetching grade details:', error);
94+
throw new Error(error.response?.data?.message || 'Failed to fetch grade details.');
95+
}
10496
}
10597

98+
/**
99+
* Poll fetchGradeDetails until autograding_complete is true and test_cases has data.
100+
* @param intervalMs Delay between requests (default 2000)
101+
* @param timeoutMs Stop after this many ms (default 300000 = 5 min); 0 = no timeout
102+
* @returns The final AutoGraderDetails with complete data
103+
*/
104+
async pollGradeDetailsUntilComplete(
105+
term: string,
106+
courseId: string,
107+
gradeableId: string,
108+
options?: { intervalMs?: number; timeoutMs?: number; token?: vscode.CancellationToken }
109+
): Promise<AutoGraderDetails> {
110+
const intervalMs = options?.intervalMs ?? 2000;
111+
const timeoutMs = options?.timeoutMs ?? 300000;
112+
const token = options?.token;
113+
const deadline = timeoutMs > 0 ? Date.now() + timeoutMs : 0;
114+
115+
const isComplete = (res: AutoGraderDetails): boolean =>
116+
res?.data?.autograding_complete === true &&
117+
Array.isArray(res.data.test_cases) &&
118+
res.data.test_cases.length > 0;
119+
120+
for (;;) {
121+
if (token?.isCancellationRequested) {
122+
throw new Error('Cancelled');
123+
}
124+
if (deadline > 0 && Date.now() >= deadline) {
125+
throw new Error('Autograding did not complete within the timeout.');
126+
}
127+
128+
const result = await this.fetchGradeDetails(term, courseId, gradeableId);
129+
if (isComplete(result)) {
130+
return result;
131+
}
132+
133+
await new Promise((r) => setTimeout(r, intervalMs));
134+
}
135+
}
136+
137+
async submitVCSGradable(term: string, courseId: string, gradeableId: string): Promise<any> {
138+
try {
139+
// git_repo_id is literally not used, but is required by the API *ugh*
140+
const url = `/api/${term}/${courseId}/gradeable/${gradeableId}/upload?vcs_upload=true&git_repo_id=true`;
141+
const response = await this.client.post<any>(url);
142+
return response.data;
143+
} catch (error: any) {
144+
console.error('Error submitting VCS gradable:', error);
145+
throw new Error(error.response?.data?.message || 'Failed to submit VCS gradable.');
146+
}
147+
}
148+
149+
106150
/**
107151
* Fetch previous attempts for a specific homework assignment
108152
*/
109-
async fetchPreviousAttempts(hw: string): Promise<any[]> {
110-
// Hardcoded previous attempts
111-
return [
112-
{ score: '15/40', tests: [{ name: 'Test 1', passed: false }, { name: 'Test 2', passed: false }, { name: 'Test 3', passed: true }, { name: 'Test 4', passed: false }] },
113-
{ score: '25/40', tests: [{ name: 'Test 1', passed: true }, { name: 'Test 2', passed: false }, { name: 'Test 3', passed: true }, { name: 'Test 4', passed: false }] }
114-
];
153+
async fetchPreviousAttempts(term: string, courseId: string, gradeableId: string): Promise<any[]> {
154+
try {
155+
const url = `/api/${term}/${courseId}/gradeable/${gradeableId}/attempts`;
156+
const response = await this.client.get<any>(url);
157+
return response.data;
158+
} catch (error: any) {
159+
console.error('Error fetching previous attempts:', error);
160+
throw new Error(error.response?.data?.message || 'Failed to fetch previous attempts.');
161+
}
115162
}
116163

117164
static getInstance(context: vscode.ExtensionContext, apiBaseUrl: string): ApiService {

0 commit comments

Comments
 (0)