Skip to content

Commit 61f9798

Browse files
authored
Integrate 1920 Source Format and Grader (#363)
* Modified autograding_errors to autograding_summary * integrate autograder and new xml format * updated schema * small refactor * Renamed template to solutionTemplate for viewer * Removed redundant comments * Added :score as a requirement for :testcase * Removed redundant test and comment * Removed overriding of status in lambda_worker * Added more assertions to testcase test * Small refactor * Update lib/cadet/jobs/autograder/lambda_worker.ex Guard parse_response Co-Authored-By: Julius Putra Tanu Setiaji <indocomsoft@gmail.com> * Removed redundant pipeline * Added test for when lambda crashes * Shifted comments * Set parse_response to private * Added comment for clarity * Refactored functions
1 parent 02b7ec0 commit 61f9798

24 files changed

+489
-127
lines changed

lib/cadet/assessments/answer.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ defmodule Cadet.Assessments.Answer do
1717
field(:xp, :integer, default: 0)
1818
field(:xp_adjustment, :integer, default: 0)
1919
field(:autograding_status, AutogradingStatus, default: :none)
20-
field(:autograding_errors, {:array, :map}, default: [])
20+
field(:autograding_results, {:array, :map}, default: [])
2121
field(:answer, :map)
2222
field(:type, QuestionType, virtual: true)
2323
field(:comment, :string)
@@ -56,7 +56,7 @@ defmodule Cadet.Assessments.Answer do
5656
@spec autograding_changeset(%__MODULE__{} | Ecto.Changeset.t(), map()) :: Ecto.Changeset.t()
5757
def autograding_changeset(answer, params) do
5858
answer
59-
|> cast(params, ~w(grade adjustment xp autograding_status autograding_errors)a)
59+
|> cast(params, ~w(grade adjustment xp autograding_status autograding_results)a)
6060
|> validate_xp_grade_adjustment_total()
6161
end
6262

lib/cadet/assessments/autograding_status.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ defenum(Cadet.Assessments.Answer.AutogradingStatus, :autograding_status, [
44
:none,
55
:processing,
66
:success,
7+
# note that :failed refers to the autograder failing due to system errors
78
:failed
89
])

lib/cadet/assessments/question_types/programming_question.ex

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,27 @@ defmodule Cadet.Assessments.QuestionTypes.ProgrammingQuestion do
44
"""
55
use Cadet, :model
66

7+
alias Cadet.Assessments.QuestionTypes.Testcase
8+
79
@primary_key false
810
embedded_schema do
911
field(:content, :string)
10-
field(:solution_template, :string)
12+
field(:prepend, :string, default: "")
13+
field(:template, :string)
14+
field(:postpend, :string, default: "")
1115
field(:solution, :string)
12-
field(:autograder, {:array, :string})
16+
embeds_many(:public, Testcase)
17+
embeds_many(:private, Testcase)
1318
end
1419

15-
@required_fields ~w(content solution_template)a
16-
@optional_fields ~w(solution autograder)a
20+
@required_fields ~w(content template)a
21+
@optional_fields ~w(solution prepend postpend)a
1722

1823
def changeset(question, params \\ %{}) do
1924
question
2025
|> cast(params, @required_fields ++ @optional_fields)
26+
|> cast_embed(:public, with: &Testcase.changeset/2)
27+
|> cast_embed(:private, with: &Testcase.changeset/2)
2128
|> validate_required(@required_fields)
2229
end
2330
end
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
defmodule Cadet.Assessments.QuestionTypes.Testcase do
2+
@moduledoc """
3+
The Assessments.QuestionTypes.Testcase entity represents a public/private testcase.
4+
"""
5+
use Cadet, :model
6+
7+
@primary_key false
8+
embedded_schema do
9+
field(:program, :string)
10+
field(:answer, :string)
11+
field(:score, :integer)
12+
end
13+
14+
@required_fields ~w(program answer score)a
15+
16+
def changeset(question, params \\ %{}) do
17+
question
18+
|> cast(params, @required_fields)
19+
|> validate_required(@required_fields)
20+
|> validate_number(:score, greater_than_or_equal_to: 0)
21+
end
22+
end

lib/cadet/jobs/autograder/lambda_worker.ex

Lines changed: 36 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,9 @@ defmodule Cadet.Autograder.LambdaWorker do
2525
|> ExAws.Lambda.invoke(lambda_params, %{})
2626
|> ExAws.request!()
2727

28-
# If the lambda crashes, results are in the format of:
29-
# %{"errorMessage" => "${message}"}
30-
if is_map(response) do
31-
raise inspect(response)
32-
else
33-
result =
34-
response
35-
|> parse_response(lambda_params)
36-
|> Map.put(:status, :success)
28+
result = parse_response(response)
3729

38-
Que.add(ResultStoreWorker, %{answer_id: answer.id, result: result})
39-
end
30+
Que.add(ResultStoreWorker, %{answer_id: answer.id, result: result})
4031
end
4132

4233
def on_failure(%{answer: answer = %Answer{}, question: %Question{}}, error) do
@@ -55,8 +46,17 @@ defmodule Cadet.Autograder.LambdaWorker do
5546
result: %{
5647
grade: 0,
5748
status: :failed,
58-
errors: [
59-
%{"systemError" => "Autograder runtime error. Please contact a system administrator"}
49+
result: [
50+
%{
51+
"resultType" => "error",
52+
"errors" => [
53+
%{
54+
"errorType" => "systemError",
55+
"errorMessage" =>
56+
"Autograder runtime error. Please contact a system administrator"
57+
}
58+
]
59+
}
6060
]
6161
}
6262
}
@@ -75,8 +75,11 @@ defmodule Cadet.Autograder.LambdaWorker do
7575
)
7676

7777
%{
78-
graderPrograms: question_content["autograder"],
79-
studentProgram: answer.answer["code"],
78+
prependProgram: Map.get(question_content, "prepend", ""),
79+
studentProgram: Map.get(answer.answer, "code"),
80+
postpendProgram: Map.get(question_content, "postpend", ""),
81+
testcases:
82+
Map.get(question_content, "public", []) ++ Map.get(question_content, "private", []),
8083
library: %{
8184
chapter: question.grading_library.chapter,
8285
external: upcased_name_external,
@@ -85,23 +88,24 @@ defmodule Cadet.Autograder.LambdaWorker do
8588
}
8689
end
8790

88-
def parse_response(response, %{graderPrograms: grader_programs}) do
89-
response
90-
|> Enum.zip(grader_programs)
91-
|> Enum.reduce(
92-
%{grade: 0, errors: []},
93-
fn {result, grader_program}, %{grade: grade, errors: errors} ->
94-
if result["resultType"] == "pass" do
95-
%{grade: grade + result["grade"], errors: errors}
96-
else
97-
error_result = %{
98-
grader_program: grader_program,
99-
errors: result["errors"]
91+
defp parse_response(response) when is_map(response) do
92+
# If the lambda crashes, results are in the format of:
93+
# %{"errorMessage" => "${message}"}
94+
if Map.has_key?(response, "errorMessage") do
95+
%{
96+
grade: 0,
97+
status: :failed,
98+
result: [
99+
%{
100+
"resultType" => "error",
101+
"errors" => [
102+
%{"errorType" => "systemError", "errorMessage" => response["errorMessage"]}
103+
]
100104
}
101-
102-
%{grade: grade, errors: errors ++ [error_result]}
103-
end
104-
end
105-
)
105+
]
106+
}
107+
else
108+
%{grade: response["totalScore"], result: response["results"], status: :success}
109+
end
106110
end
107111
end

lib/cadet/jobs/autograder/result_store_worker.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ defmodule Cadet.Autograder.ResultStoreWorker do
6161
grade: result.grade,
6262
xp: xp,
6363
autograding_status: status,
64-
autograding_errors: result.errors
64+
autograding_results: result.result
6565
}
6666

6767
answer

lib/cadet/jobs/xml_parser.ex

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -247,15 +247,31 @@ defmodule Cadet.Updater.XMLParser do
247247
end
248248

249249
defp process_question_entity_by_type(entity, "programming") do
250-
entity
251-
|> xpath(
252-
~x"."e,
253-
content: ~x"./TEXT/text()" |> transform_by(&process_charlist/1),
254-
solution_template: ~x"./SNIPPET/TEMPLATE/text()" |> transform_by(&process_charlist/1),
255-
solution: ~x"./SNIPPET/SOLUTION/text()" |> transform_by(&process_charlist/1),
256-
autograder:
257-
~x"./SNIPPET/GRADER/text()"l
258-
|> transform_by(&Enum.map(&1, fn charlist -> process_charlist(charlist) end))
250+
Map.merge(
251+
entity
252+
|> xpath(
253+
~x"."e,
254+
content: ~x"./TEXT/text()" |> transform_by(&process_charlist/1),
255+
prepend: ~x"./SNIPPET/PREPEND/text()" |> transform_by(&process_charlist/1),
256+
template: ~x"./SNIPPET/TEMPLATE/text()" |> transform_by(&process_charlist/1),
257+
postpend: ~x"./SNIPPET/POSTPEND/text()" |> transform_by(&process_charlist/1),
258+
solution: ~x"./SNIPPET/SOLUTION/text()" |> transform_by(&process_charlist/1)
259+
),
260+
entity
261+
|> xmap(
262+
public: [
263+
~x"./SNIPPET/TESTCASES/PUBLIC"l,
264+
score: ~x"./@score"oi,
265+
answer: ~x"./@answer" |> transform_by(&process_charlist/1),
266+
program: ~x"./text()" |> transform_by(&process_charlist/1)
267+
],
268+
private: [
269+
~x"./SNIPPET/TESTCASES/PRIVATE"l,
270+
score: ~x"./@score"oi,
271+
answer: ~x"./@answer" |> transform_by(&process_charlist/1),
272+
program: ~x"./text()" |> transform_by(&process_charlist/1)
273+
]
274+
)
259275
)
260276
end
261277

lib/cadet_web/controllers/assessments_controller.ex

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,9 +193,22 @@ defmodule CadetWeb.AssessmentsController do
193193
"The library used for this question"
194194
)
195195

196-
solutionTemplate(:string, "Solution template for programming questions")
196+
prepend(:string, "Prepend program for programming questions")
197+
198+
template(:string, "Solution template for programming questions")
199+
200+
postpend(:string, "Postpend program for programming questions")
201+
202+
testcases(
203+
Schema.new do
204+
type(:array)
205+
items(Schema.ref(:Testcase))
206+
end,
207+
"Testcase programs for programming questions"
208+
)
197209

198210
grader(Schema.ref(:GraderInfo))
211+
199212
gradedAt(:string, "Last graded at", format: "date-time", required: false)
200213

201214
xp(:integer, "Final XP given to this question. Only provided for students.")
@@ -212,6 +225,18 @@ defmodule CadetWeb.AssessmentsController do
212225
"The max xp for this question",
213226
required: true
214227
)
228+
229+
autogradingStatus(
230+
:string,
231+
"One of none/processing/success/failed"
232+
)
233+
234+
autogradingResults(
235+
Schema.new do
236+
type(:array)
237+
items(Schema.ref(:AutogradingResult))
238+
end
239+
)
215240
end
216241
end,
217242
MCQChoice:
@@ -261,6 +286,22 @@ defmodule CadetWeb.AssessmentsController do
261286
"The external library for this question"
262287
)
263288
end
289+
end,
290+
Testcase:
291+
swagger_schema do
292+
properties do
293+
answer(:string)
294+
score(:integer)
295+
program(:string)
296+
end
297+
end,
298+
AutogradingResult:
299+
swagger_schema do
300+
properties do
301+
resultType(:string, "One of pass/fail/error")
302+
expected(:string)
303+
actual(:string)
304+
end
264305
end
265306
}
266307
end

lib/cadet_web/views/assessments_view.ex

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,30 +128,77 @@ defmodule CadetWeb.AssessmentsView do
128128
grader: grader_builder(grader),
129129
gradedAt: graded_at_builder(grader),
130130
xp: &((&1.xp || 0) + (&1.xp_adjustment || 0)),
131-
grade: &((&1.grade || 0) + (&1.adjustment || 0))
131+
grade: &((&1.grade || 0) + (&1.adjustment || 0)),
132+
autogradingStatus: :autograding_status,
133+
autogradingResults: build_results(%{results: answer.autograding_results})
132134
})
133135
end
134136

135-
def build_choice(%{choice: choice}) do
137+
defp build_results(%{results: results}) do
138+
case results do
139+
nil -> nil
140+
_ -> &Enum.map(&1.autograding_results, fn result -> build_result(result) end)
141+
end
142+
end
143+
144+
defp build_result(result) do
145+
transform_map_for_view(result, %{
146+
resultType: "resultType",
147+
expected: "expected",
148+
actual: "actual",
149+
errorType: "errorType",
150+
errors: build_errors(result["errors"])
151+
})
152+
end
153+
154+
defp build_errors(errors) do
155+
case errors do
156+
nil -> nil
157+
_ -> &Enum.map(&1["errors"], fn error -> build_error(error) end)
158+
end
159+
end
160+
161+
defp build_error(error) do
162+
transform_map_for_view(error, %{
163+
errorType: "errorType",
164+
line: "line",
165+
location: "location",
166+
errorLine: "errorLine",
167+
errorExplanation: "errorExplanation"
168+
})
169+
end
170+
171+
defp build_choice(choice) do
136172
transform_map_for_view(choice, %{
137173
id: "choice_id",
138174
content: "content",
139175
hint: "hint"
140176
})
141177
end
142178

179+
defp build_testcase(testcase) do
180+
transform_map_for_view(testcase, %{
181+
answer: "answer",
182+
score: "score",
183+
program: "program"
184+
})
185+
end
186+
143187
defp build_question_content_by_type(%{question: %{question: question, type: question_type}}) do
144188
case question_type do
145189
:programming ->
146190
transform_map_for_view(question, %{
147191
content: "content",
148-
solutionTemplate: "solution_template"
192+
prepend: "prepend",
193+
solutionTemplate: "template",
194+
postpend: "postpend",
195+
testcases: &Enum.map(&1["public"], fn testcase -> build_testcase(testcase) end)
149196
})
150197

151198
:mcq ->
152199
transform_map_for_view(question, %{
153200
content: "content",
154-
choices: &Enum.map(&1["choices"], fn choice -> build_choice(%{choice: choice}) end)
201+
choices: &Enum.map(&1["choices"], fn choice -> build_choice(choice) end)
155202
})
156203
end
157204
end
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
defmodule Cadet.Repo.Migrations.AlterAnswersTableAutogradingErrors do
2+
use Ecto.Migration
3+
4+
def change do
5+
rename(table(:answers), :autograding_errors, to: :autograding_results)
6+
end
7+
end

schema.png

-130 KB
Loading

test/cadet/assessments/assessments_test.exs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ defmodule Cadet.AssessmentsTest do
3232
library: build(:library),
3333
question: %{
3434
content: Faker.Pokemon.name(),
35-
solution_template: Faker.Lorem.Shakespeare.as_you_like_it(),
35+
prepend: "",
36+
template: Faker.Lorem.Shakespeare.as_you_like_it(),
37+
postpend: "",
3638
solution: Faker.Lorem.Shakespeare.hamlet()
3739
}
3840
},

0 commit comments

Comments
 (0)