From e4330cf42dc058a15337cd2a34872880a09f7913 Mon Sep 17 00:00:00 2001 From: reevafaisal Date: Tue, 12 Nov 2024 14:13:21 -0500 Subject: [PATCH] added grading module - still buggy --- src/haz3lschool/GradePrelude.re | 10 + src/haz3lschool/Tutorial.re | 61 +++- src/haz3lschool/TutorialGradescope.re | 133 +++++++++ src/haz3lschool/TutorialGrading.re | 145 +++++++++ src/haz3lweb/SlideContent.re | 4 +- src/haz3lweb/Store.re | 2 +- src/haz3lweb/TutorialGrading.re | 415 ++++++++++++++++++++++++++ src/haz3lweb/view/TestView.re | 24 ++ src/haz3lweb/view/TutorialMode.re | 51 +++- 9 files changed, 829 insertions(+), 16 deletions(-) create mode 100644 src/haz3lschool/TutorialGradescope.re create mode 100644 src/haz3lschool/TutorialGrading.re create mode 100644 src/haz3lweb/TutorialGrading.re diff --git a/src/haz3lschool/GradePrelude.re b/src/haz3lschool/GradePrelude.re index a45b34fa15..9b20655790 100644 --- a/src/haz3lschool/GradePrelude.re +++ b/src/haz3lschool/GradePrelude.re @@ -4,6 +4,16 @@ module ExerciseEnv = { let output_header = Exercise.output_header_grading; }; +module TutorialEnv = { + type node = unit; + let default = (); + let output_header = Tutorial.output_header_grading; +}; + +module Tutorial = Tutorial.D(TutorialEnv); + +module GradingT = TutorialGrading.D(TutorialEnv); + module Exercise = Exercise.F(ExerciseEnv); module Grading = Grading.F(ExerciseEnv); diff --git a/src/haz3lschool/Tutorial.re b/src/haz3lschool/Tutorial.re index 719842c7f1..8292af2764 100644 --- a/src/haz3lschool/Tutorial.re +++ b/src/haz3lschool/Tutorial.re @@ -64,13 +64,33 @@ module D = (TutorialEnv: TutorialEnv) => { p.title; }; - let find_key_opt = (key, specs: list(p('code))) => { - specs |> Util.ListUtil.findi_opt(spec => key_of(spec) == key); + // let find_key_opt = (key, specs: list(p('code))) => { + // specs |> Util.ListUtil.findi_opt(spec => key_of(spec) == key); + // }; + + let find_key_opt = + (key, specs: list(p('code))): option((string, p('code))) => { + let rec loop = remaining_specs => { + switch (remaining_specs) { + | [] => None + | [spec, ...rest] => + if (key_of(spec) == key) { + Some + ((key_of(spec), spec)); // Return as soon as we find a match + } else { + loop( + rest // Continue searching in the remaining specs + ); + } + }; + }; + loop(specs); // Start looping over `specs` }; [@deriving (show({with_path: false}), sexp, yojson)] type pos = | YourImpl + | YourTestsValidation | HiddenTests; [@deriving (show({with_path: false}), sexp, yojson)] @@ -114,6 +134,7 @@ module D = (TutorialEnv: TutorialEnv) => { ({pos, eds, _}) => switch (pos) { | YourImpl => eds.your_impl + | YourTestsValidation => eds.hidden_tests.tests | HiddenTests => eds.hidden_tests.tests }; @@ -126,6 +147,7 @@ module D = (TutorialEnv: TutorialEnv) => { your_impl: editor, }, } + | YourTestsValidation | HiddenTests => { ...state, eds: { @@ -143,7 +165,7 @@ module D = (TutorialEnv: TutorialEnv) => { eds.hidden_tests.tests, ]; - let editor_positions = [YourImpl, HiddenTests]; + let editor_positions = [YourImpl, HiddenTests, YourTestsValidation]; let positioned_editors = state => List.combine(editor_positions, editors(state)); @@ -151,8 +173,8 @@ module D = (TutorialEnv: TutorialEnv) => { let idx_of_pos = (pos, p: p('code)) => switch (pos) { | YourImpl => 0 - - | HiddenTests => 0 + List.length(p.hidden_tests.tests) // NEED TO FIGURE OUT HOW TO ACTUALLY MAKE THIS WORK + | YourTestsValidation => 1 + | HiddenTests => 1 + List.length(p.hidden_tests.tests) }; let pos_of_idx = (p: p('code), idx: int) => @@ -242,6 +264,7 @@ module D = (TutorialEnv: TutorialEnv) => { let visible_in = (pos, ~instructor_mode) => { switch (pos) { | YourImpl => true + | YourTestsValidation => true | HiddenTests => instructor_mode }; }; @@ -323,6 +346,7 @@ module D = (TutorialEnv: TutorialEnv) => { // }; type stitched('a) = { + test_validation: 'a, // prelude + correct_impl + your_tests user_impl: 'a, // prelude + your_impl instructor: 'a, // prelude + correct_impl + hidden_tests.tests // TODO only needs to run in instructor mode hidden_tests: 'a, @@ -369,12 +393,18 @@ module D = (TutorialEnv: TutorialEnv) => { let user_impl_term = { eds.your_impl |> term_of |> wrap_filter(FilterAction.Step); }; + let test_validation_term = eds.hidden_tests.tests |> term_of; // No combining of your_impl_term with hidden_tests let hidden_tests_term = EditorUtil.append_exp(user_impl_term, term_of(eds.hidden_tests.tests)); - {user_impl: user_impl_term, instructor, hidden_tests: hidden_tests_term}; + { + user_impl: user_impl_term, + instructor, + test_validation: test_validation_term, + hidden_tests: hidden_tests_term, + }; }; let stitch_term = Core.Memo.general(stitch_term); @@ -395,6 +425,7 @@ module D = (TutorialEnv: TutorialEnv) => { }; let instructor = mk(t.instructor); { + test_validation: mk(t.test_validation), user_impl: mk(t.user_impl), instructor, hidden_tests: mk(t.hidden_tests), @@ -427,18 +458,20 @@ module D = (TutorialEnv: TutorialEnv) => { let key_for_statics = (state: state): string => switch (state.pos) { | YourImpl => user_impl_key + | YourTestsValidation => test_validation_key | HiddenTests => hidden_tests_key }; let spliced_elabs = (settings: CoreSettings.t, state: state) : list((ModelResults.key, Elaborator.Elaboration.t)) => { - let {user_impl, instructor, hidden_tests} = + let {test_validation, user_impl, instructor, hidden_tests} = stitch_static(settings, stitch_term(state)); let elab = (s: Editor.CachedStatics.t): Elaborator.Elaboration.t => { d: Interface.elaborate(~settings, s.info_map, s.term), }; [ + (test_validation_key, elab(test_validation)), (user_impl_key, elab(user_impl)), (instructor_key, elab(instructor)), (hidden_tests_key, elab(hidden_tests)), @@ -467,6 +500,7 @@ module D = (TutorialEnv: TutorialEnv) => { (state: state, s: stitched(DynamicsItem.t)): Editor.CachedStatics.t => switch (state.pos) { | YourImpl => s.user_impl.statics + | YourTestsValidation => s.test_validation.statics | HiddenTests => s.hidden_tests.statics }; @@ -481,7 +515,7 @@ module D = (TutorialEnv: TutorialEnv) => { ) : stitched(DynamicsItem.t) => { let { - // test_validation, + test_validation, user_impl, // user_tests, // prelude, @@ -497,6 +531,11 @@ module D = (TutorialEnv: TutorialEnv) => { ModelResults.lookup(results, key) |> Option.value(~default=ModelResult.NoElab) }; + let test_validation = + DynamicsItem.{ + statics: test_validation, + result: result_of(test_validation_key), + }; let user_impl = DynamicsItem.{statics: user_impl, result: result_of(user_impl_key)}; @@ -510,7 +549,7 @@ module D = (TutorialEnv: TutorialEnv) => { result: result_of(hidden_tests_key), }; { - // test_validation, + test_validation, user_impl, // user_tests, instructor, @@ -532,7 +571,7 @@ module D = (TutorialEnv: TutorialEnv) => { } else if (settings.statics) { let t = stitch_static(settings, stitch_term(state)); { - // test_validation: DynamicsItem.statics_only(t.test_validation), + test_validation: DynamicsItem.statics_only(t.test_validation), user_impl: DynamicsItem.statics_only(t.user_impl), // user_tests: DynamicsItem.statics_only(t.user_tests), instructor: DynamicsItem.statics_only(t.instructor), @@ -542,7 +581,7 @@ module D = (TutorialEnv: TutorialEnv) => { }; } else { { - // test_validation: DynamicsItem.empty, + test_validation: DynamicsItem.empty, user_impl: DynamicsItem.empty, // user_tests: DynamicsItem.empty, instructor: DynamicsItem.empty, diff --git a/src/haz3lschool/TutorialGradescope.re b/src/haz3lschool/TutorialGradescope.re new file mode 100644 index 0000000000..a0ba449e94 --- /dev/null +++ b/src/haz3lschool/TutorialGradescope.re @@ -0,0 +1,133 @@ +open Haz3lcore; +open Util; + +// open Haz3lschool; +open Core; + +open Specs; +open GradePrelude.Tutorial; +open GradePrelude.GradingT; + +[@deriving (sexp, yojson)] +type item = { + max: int, + percentage, + src: string, +}; + +let item_to_summary = (name, {max, percentage, src}) => + Printf.sprintf( + "%s: %.1f/%.1f\n\n", + name, + percentage *. float_of_int(max), + float_of_int(max), + ) + ++ ( + if (String.equal(src, "")) { + ""; + } else { + "Source Code:\n\n" ++ src ++ "\n\n"; + } + ); + +[@deriving (sexp, yojson)] +type report = { + summary: string, + overall: score, +}; + +[@deriving (sexp, yojson)] +type section = { + name: string, + report, +}; + +[@deriving (sexp, yojson)] +type chapter = list(section); + +module Main = { + let settings = CoreSettings.on; /* Statics and Dynamics on */ + let name_to_tutorial_export = path => { + let yj = Yojson.Safe.from_file(path); + switch (yj) { + | `Assoc(l) => + let sch = List.Assoc.find_exn(~equal=String.(==), l, "school"); + switch (sch) { + | `String(sch) => + let exercise_export = sch |> deserialize_exercise_export; + { + cur_exercise: exercise_export.cur_exercise, + exercise_data: exercise_export.exercise_data // Ensure this is list((key, persistent_state)) + }; + | _ => failwith("School is not a string") + }; + | _ => failwith("Json without school key") + }; + }; + let gen_grading_report = exercise => { + let zipper_pp = zipper => { + Printer.pretty_print(zipper); + }; + let model_results = + spliced_elabs(settings, exercise) + |> ModelResults.init_eval + |> ModelResults.run_pending(~settings); + let stitched_dynamics = + stitch_dynamic(settings, exercise, Some(model_results)); + let grading_report = exercise.eds |> GradingReport.mk(~stitched_dynamics); + let details = grading_report; + + let impl_grading = { + max: 100, // Set fixed maximum score + src: exercise.eds.your_impl.state.zipper |> zipper_pp, + percentage: ImplGradingReport.percentage(details.impl_grading_report), + }; + + let overall = grading_report |> GradingReport.overall_score; + let (a, b) = overall; + let summary = + Printf.sprintf("Overall: %.1f/%.1f\n\n", a, b) + ++ item_to_summary("Impl Grading", impl_grading); + {summary, overall}; + }; + + let create_section = (item): section => { + // Separate `key` and `persistent_state` within the function + let key = fst(item); + let persistent_state = snd(item); + + switch (find_key_opt(key, specs)) { + | Some((name, spec)) => + // Unpersist the state for the exercise + let exercise = + unpersist_state( + persistent_state, + ~settings, + ~spec, + ~instructor_mode=true, + ); + + // Generate the grading report for this exercise + let report = gen_grading_report(exercise); + + // Return a `section` record + {name, report}; + | None => failwith("Invalid spec") + }; + }; + + let run = () => { + let hw_path = Sys.get_argv()[1]; + let hw = name_to_tutorial_export(hw_path); + + let export_chapter: list(section) = + List.map(~f=item => create_section(item), hw.exercise_data); + + export_chapter + |> yojson_of_chapter + |> Yojson.Safe.pretty_to_string + |> print_endline; + }; +}; + +Main.run(); diff --git a/src/haz3lschool/TutorialGrading.re b/src/haz3lschool/TutorialGrading.re new file mode 100644 index 0000000000..bcf3368660 --- /dev/null +++ b/src/haz3lschool/TutorialGrading.re @@ -0,0 +1,145 @@ +open Haz3lcore; +open Util; + +module D = (TutorialEnv: Tutorial.TutorialEnv) => { + open Tutorial.D(TutorialEnv); + + [@deriving (show({with_path: false}), sexp, yojson)] + type percentage = float; + [@deriving (show({with_path: false}), sexp, yojson)] + type points = float; + [@deriving (show({with_path: false}), sexp, yojson)] + type score = (points, points); + + let score_of_percent = (percent, max_points) => { + let max_points = float_of_int(max_points); + (percent *. max_points, max_points); + }; + + module ImplGradingReport = { + type t = { + hints: list(string), + test_results: option(TestResults.t), + hinted_results: list((TestStatus.t, string)), + }; + + let mk = (~hints: list(string), ~test_results: option(TestResults.t)): t => { + let hinted_results = + switch (test_results) { + | Some(test_results) => + let statuses = test_results.statuses; + Util.ListUtil.zip_defaults( + statuses, + hints, + Haz3lcore.TestStatus.Indet, + "No hint available.", + ); + + | None => + Util.ListUtil.zip_defaults( + [], + hints, + Haz3lcore.TestStatus.Indet, + "Exercise configuration error: Hint without a test.", + ) + }; + {hints, test_results, hinted_results}; + }; + + let total = (report: t) => List.length(report.hinted_results); + let num_passed = (report: t) => { + report.hinted_results + |> List.find_all(((status, _)) => status == TestStatus.Pass) + |> List.length; + }; + + let percentage = (report: t): float => { + let passed = float_of_int(num_passed(report)); + let total = float_of_int(total(report)); + if (total == 0.0) { + 0.0; // Avoid division by zero + } else { + 100.0 *. (passed /. total); // Return percentage as a float + }; + }; + + let test_summary_str = (test_results: TestResults.t) => { + TestResults.result_summary_str( + ~n=test_results.total, + ~p=test_results.failing, + ~q=test_results.unfinished, + ~n_str="test", + ~ns_str="tests", + ~p_str="failing", + ~q_str="indeterminate", + ~r_str="valid", + ); + }; + }; + + module GradingReport = { + type t = { + // point_distribution, + // test_validation_report: TestValidationReport.t, + // mutation_testing_report: MutationTestingReport.t, + // syntax_report: SyntaxReport.t, + impl_grading_report: ImplGradingReport.t, + }; + + let mk = (eds: eds, ~stitched_dynamics: stitched(DynamicsItem.t)) => { + // point_distribution: eds.point_distribution, + // test_validation_report: + // TestValidationReport.mk( + // eds, + // ModelResult.test_results(stitched_dynamics.test_validation.result), + // ), + // mutation_testing_report: + // MutationTestingReport.mk( + // ~test_validation=stitched_dynamics.test_validation, + // ~hidden_bugs_state=eds.hidden_bugs, + // ~hidden_bugs=stitched_dynamics.hidden_bugs, + // ), + // syntax_report: + // SyntaxReport.mk(~your_impl=eds.your_impl, ~tests=eds.syntax_tests), + impl_grading_report: + ImplGradingReport.mk( + ~hints=eds.hidden_tests.hints, + ~test_results= + ModelResult.test_results(stitched_dynamics.hidden_tests.result), + ), + }; + + let overall_score = + ( + { + // point_distribution, + // test_validation_report, + // mutation_testing_report, + // syntax_report, + impl_grading_report, + _, + }: t, + ) + : score => { + // let (tv_points, tv_max) = + // score_of_percent( + // TestValidationReport.percentage(test_validation_report), + // point_distribution.test_validation, + // ); + // let (mt_points, mt_max) = + // score_of_percent( + // MutationTestingReport.percentage(mutation_testing_report), + // point_distribution.mutation_testing, + // ); + let max_impl_grading = 100; + let (ig_points, ig_max) = + score_of_percent( + ImplGradingReport.percentage(impl_grading_report), + max_impl_grading, + ); + let total_points = ig_points; + let max_points = ig_max; + (total_points, max_points); + }; + }; +}; diff --git a/src/haz3lweb/SlideContent.re b/src/haz3lweb/SlideContent.re index 7d2788cee7..64fb87922d 100644 --- a/src/haz3lweb/SlideContent.re +++ b/src/haz3lweb/SlideContent.re @@ -23,10 +23,10 @@ let em = content => span(~attrs=[Attr.class_("em")], [text(content)]); let get_content = fun - | Tutorial("Expressive Programming", _) => + | Tutorial("Programming Expressively", _) => Some( slide( - "Expressive Programming", + "Programming Expressively", [ p([ text( diff --git a/src/haz3lweb/Store.re b/src/haz3lweb/Store.re index 11510a2844..594b1e6856 100644 --- a/src/haz3lweb/Store.re +++ b/src/haz3lweb/Store.re @@ -401,7 +401,7 @@ module Tutorial = { let tutorial_state: Tutorial.state = { pos: YourImpl, eds: { - title: status.title, + title: "", description: status.description, your_impl: your_impl_editor, hidden_tests: { diff --git a/src/haz3lweb/TutorialGrading.re b/src/haz3lweb/TutorialGrading.re new file mode 100644 index 0000000000..a1ef89fdac --- /dev/null +++ b/src/haz3lweb/TutorialGrading.re @@ -0,0 +1,415 @@ +open Virtual_dom.Vdom; +// open Util; +open Node; + +include Haz3lschool.TutorialGrading.D(Tutorial.TutorialEnv); + +let score_view = ((earned: points, max: points)) => { + div( + ~attrs=[ + Attr.classes([ + "test-percent", + Float.equal(earned, max) ? "all-pass" : "some-fail", + ]), + ], + [text(Printf.sprintf("%.1f / %.1f pts", earned, max))], + ); +}; + +let percentage_view = (p: percentage) => { + div( + ~attrs=[ + Attr.classes([ + "test-percent", + Float.equal(p, 1.) ? "all-pass" : "some-fail", + ]), + ], + [text(Printf.sprintf("%.0f%%", 100. *. p))], + ); +}; + +module TestValidationReport = { + include ImplGradingReport; + // let textual_summary = (report: t) => { + // switch (report.test_results) { + // | None => [Node.text("No test results")] + // | Some(test_results) => [ + // { + // let total_tests = test_results.total; + // // let required = report.required; + // let num_tests_message = + // total_tests >= required + // ? "at least " ++ string_of_int(required) + // : string_of_int(test_results.total) + // ++ " of " + // ++ string_of_int(report.required); + // text( + // "Entered " + // ++ num_tests_message + // ++ " tests. " + // ++ test_summary_str(test_results), + // ); + // }, + // ] + // }; + // }; + + let view = (~inject, report: t, max_points: int) => { + Cell.report_footer_view([ + div( + ~attrs=[Attr.classes(["test-summary"])], + [ + div( + ~attrs=[Attr.class_("test-text")], + [score_view(score_of_percent(percentage(report), max_points))], + // @ textual_summary(report), + ), + ] + @ Option.to_list( + report.test_results + |> Option.map(test_results => + TestView.test_bar( + ~inject, + ~test_results, + YourTestsValidation, + ) + ), + ), + ), + ]); + }; +}; + +// module MutationTestingReport = { +// // include MutationTestingReport; +// open Haz3lcore; + +// let summary_message = (~score, ~total, ~found): Node.t => +// div( +// ~attrs=[Attr.classes(["test-text"])], +// [score_view(score), text(summary_str(~total, ~found))], +// ); + +// let bar = (~inject, instances) => +// div( +// ~attrs=[Attr.classes(["test-bar"])], +// List.mapi( +// (id, (status, _)) => +// div( +// ~attrs=[ +// Attr.classes(["segment", TestStatus.to_string(status)]), +// Attr.on_click( +// //TODO: wire up test ids +// TestView.jump_to_test(~inject, HiddenBugs(id), Id.invalid), +// ), +// ], +// [], +// ), +// instances, +// ), +// ); + +// let summary = (~inject, ~report, ~max_points) => { +// let total = List.length(report.results); +// let found = +// List.length( +// List.filter(((x: TestStatus.t, _)) => x == Pass, report.results), +// ); +// let status_class = total == found ? "Pass" : "Fail"; +// div( +// ~attrs=[ +// Attr.classes([ +// "cell-item", +// "test-summary", +// "cell-report", +// status_class, +// ]), +// ], +// [ +// summary_message( +// ~score=score_of_percent(percentage(report), max_points), +// ~total, +// ~found, +// ), +// bar(~inject, report.results), +// ], +// ); +// }; + +// let individual_report = (id, ~inject, ~hint: string, ~status: TestStatus.t) => +// div( +// ~attrs=[ +// Attr.classes(["test-report"]), +// //TODO: wire up test ids +// Attr.on_click( +// TestView.jump_to_test(~inject, HiddenBugs(id), Id.invalid), +// ), +// ], +// [ +// div( +// ~attrs=[ +// Attr.classes([ +// "test-id", +// "Test" ++ TestStatus.to_string(status), +// ]), +// ], +// /* NOTE: prints lexical index, not unique id */ +// [text(string_of_int(id + 1))], +// ), +// // TestView.test_instance_view(~font_metrics, instance), +// ] +// @ [ +// div( +// ~attrs=[ +// Attr.classes([ +// "test-hint", +// "test-instance", +// TestStatus.to_string(status), +// ]), +// ], +// [text(hint)], +// ), +// ], +// ); + +// let individual_reports = (~inject, coverage_results) => +// div( +// coverage_results +// |> List.mapi((i, (status, hint)) => +// individual_report(i, ~inject, ~hint, ~status) +// ), +// ); + +// let view = (~inject, report: t, max_points: int) => +// if (max_points == 0) { +// Node.div([]); +// } else { +// Cell.panel( +// ~classes=["test-panel"], +// [ +// Cell.caption( +// "Mutation Testing", +// ~rest=": Your Tests vs. Buggy Implementations (hidden)", +// ), +// individual_reports(~inject, report.results), +// ], +// ~footer=Some(summary(~inject, ~report, ~max_points)), +// ); +// }; +// }; + +// module SyntaxReport = { +// include SyntaxReport; +// let individual_report = (i: int, hint: string, status: bool) => { +// let result_string = status ? "Pass" : "Indet"; + +// div( +// ~attrs=[Attr.classes(["test-report"])], +// [ +// div( +// ~attrs=[Attr.classes(["test-id", "Test" ++ result_string])], +// [text(string_of_int(i + 1))], +// ), +// ] +// @ [ +// div( +// ~attrs=[ +// Attr.classes(["test-hint", "test-instance", result_string]), +// ], +// [text(hint)], +// ), +// ], +// ); +// }; + +// let individual_reports = (hinted_results: list((bool, string))) => { +// div( +// hinted_results +// |> List.mapi((i, (status, hint)) => +// individual_report(i, hint, status) +// ), +// ); +// }; + +// let view = (syntax_report: t) => { +// Cell.panel( +// ~classes=["test-panel"], +// [ +// Cell.caption( +// "Syntax Validation", +// ~rest= +// ": Does your implementation satisfy the syntactic requirements?", +// ), +// individual_reports(syntax_report.hinted_results), +// ], +// ~footer= +// Some( +// Cell.report_footer_view([ +// div( +// ~attrs=[Attr.classes(["test-summary"])], +// [ +// div( +// ~attrs=[Attr.class_("test-text")], +// [ +// percentage_view(syntax_report.percentage), +// text( +// " of the Implementation Validation points will be earned", +// ), +// ], +// ), +// ], +// ), +// ]), +// ), +// ); +// }; +// }; + +module ImplGradingReport = { + open Haz3lcore; + include ImplGradingReport; + let textual_summary = (report: t) => { + switch (report.test_results) { + | None => [Node.text("No test results")] + | Some(test_results) => [ + { + text(test_summary_str(test_results)); + }, + ] + }; + }; + + // let summary = (~inject, ~report, ~max_points) => { + // let percentage = percentage(report); + // let score = score_of_percent(percentage); + // let total = total(report); + // let num_passed = num_passed(report); + // let status_class = total == num_passed ? "Pass" : "Fail"; + // div( + // ~attrs= + // Attr.classes([ + // "cell-item", + // "test-summary", + // "cell-report", + // status_class, + // ]), + // [ + // summary_message( + // ~score, + // ~total, + // ~found=num_passed, + // ), + // bar(~inject, report.results), + // ], + // ); + // }; + + let individual_report = (i, ~inject, ~hint: string, ~status, (id, _)) => + div( + ~attrs=[ + Attr.classes(["test-report"]), + Attr.on_click( + TestView.jump_to_test_tutorial(~inject, Tutorial.HiddenTests, id), + ), + ], + [ + div( + ~attrs=[ + Attr.classes([ + "test-id", + "Test" ++ TestStatus.to_string(status), + ]), + ], + /* NOTE: prints lexical index, not unique id */ + [text(string_of_int(i + 1))], + ), + // TestView.test_instance_view(~font_metrics, instance), + ] + @ [ + div( + ~attrs=[ + Attr.classes([ + "test-hint", + "test-instance", + TestStatus.to_string(status), + ]), + ], + [text(hint)], + ), + ], + ); + + let individual_reports = (~inject, ~report) => { + switch (report.test_results) { + | Some(test_results) + when + List.length(test_results.test_map) + == List.length(report.hinted_results) => + /* NOTE: This condition will be false when evaluation crashes, + * for example due to a stack overflow, which may occur in normal operation */ + div( + report.hinted_results + |> List.mapi((i, (status, hint)) => + individual_report( + i, + ~inject, + ~hint, + ~status, + List.nth(test_results.test_map, i), + ) + ), + ) + | _ => div([]) + }; + }; + + let view = (~inject, ~report: t, ~max_points: int) => { + Cell.panel( + ~classes=["cell-item", "panel", "test-panel"], + [ + Cell.caption( + "Implementation Grading", + ~rest=": Hidden Tests vs. Your Implementation", + ), + individual_reports(~inject, ~report), + ], + ~footer= + Some( + Cell.report_footer_view([ + div( + ~attrs=[Attr.classes(["test-summary"])], + [ + div( + ~attrs=[Attr.class_("test-text")], + [ + score_view( + score_of_percent(percentage(report), max_points), + ), + ] + @ textual_summary(report), + ), + ] + @ Option.to_list( + report.test_results + |> Option.map(test_results => + TestView.test_bar_tutorial( + ~inject, + ~test_results, + Tutorial.HiddenTests, + ) + ), + ), + ), + ]), + ), + ); + }; +}; + +module GradingReport = { + include GradingReport; + + let view_overall_score = (report: t) => { + score_view(overall_score(report)); + }; +}; diff --git a/src/haz3lweb/view/TestView.re b/src/haz3lweb/view/TestView.re index 1b01158c56..95e31a1aca 100644 --- a/src/haz3lweb/view/TestView.re +++ b/src/haz3lweb/view/TestView.re @@ -37,6 +37,12 @@ let jump_to_test = (~inject, pos, id, _) => { Effect.bind(effect1, ~f=_result1 => effect2); }; +let jump_to_test_tutorial = (~inject, pos, id, _) => { + let effect1 = inject(Update.SwitchTutEditor(pos)); + let effect2 = inject(Update.PerformAction(Jump(TileId(id)))); + Effect.bind(effect1, ~f=_result1 => effect2); +}; + let test_report_view = ( ~settings, @@ -117,12 +123,30 @@ let test_bar_segment = (~inject, pos, (id, reports)) => { ); }; +let test_bar_segment_tutorial = (~inject, pos: Tutorial.pos, (id, reports)) => { + let status = reports |> TestMap.joint_status |> TestStatus.to_string; + div( + ~attrs=[ + clss(["segment", status]), + Attr.on_click(jump_to_test_tutorial(~inject, pos, id)), + ], + [], + ); +}; + let test_bar = (~inject, ~test_results: TestResults.t, pos) => div( ~attrs=[Attr.class_("test-bar")], List.map(test_bar_segment(~inject, pos), test_results.test_map), ); +let test_bar_tutorial = + (~inject, ~test_results: TestResults.t, pos: Tutorial.pos) => + div( + ~attrs=[Attr.class_("test-bar")], + List.map(test_bar_segment_tutorial(~inject, pos), test_results.test_map), + ); + // result_summary_str and test_summary_str have been moved to haz3lcore/TestResults.re let percent_view = (n: int, p: int): Node.t => { diff --git a/src/haz3lweb/view/TutorialMode.re b/src/haz3lweb/view/TutorialMode.re index 6b46245b35..b926073518 100644 --- a/src/haz3lweb/view/TutorialMode.re +++ b/src/haz3lweb/view/TutorialMode.re @@ -25,6 +25,7 @@ let view = ~settings: Settings.t, ~tutorial, ~results, + // ~stitched_dynamics, ~highlights, ) => { // editor : Editor.t, @@ -38,7 +39,7 @@ let view = settings.core.dynamics ? Some(results) : None, ); let { - // test_validation, + test_validation, user_impl, // user_tests, // prelude, @@ -47,6 +48,10 @@ let view = hidden_tests: _, }: Tutorial.stitched(Tutorial.DynamicsItem.t) = stitched_dynamics; + let grading_report = + TutorialGrading.GradingReport.mk(eds, ~stitched_dynamics); + let score_view = + TutorialGrading.GradingReport.view_overall_score(grading_report); let editor_view = ( @@ -106,7 +111,49 @@ let view = ), ); - [title_view] @ render_cells(settings, [your_impl_view, hidden_tests_view]); + // let your_tests_view = + // Always( + // editor_view( + // YourTestsValidation, + // ~caption="Test Validation", + // ~subcaption=": Your Tests vs. Correct Implementation", + // ~editor=eds.hidden_tests.tests, + // ~di=test_validation, + // ~footer=[] // TutorialGrading.TestValidationReport.view( + // // ~inject, + // // grading_report.test_validation_report, + // // grading_report.point_distribution.test_validation, + // // ), + // ), + // ); + + let impl_validation_view = + Always( + editor_view( + YourTestsValidation, + ~caption="Implementation Validation", + ~subcaption=": Hidden Tests vs. Your Implementation", + ~editor=eds.your_impl, + ~di=test_validation, + ~footer=[ + Cell.test_report_footer_view( + ~inject, + ~test_results=ModelResult.test_results(test_validation.result), + ), + ], + ), + ); + + [score_view, title_view] + @ render_cells( + settings, + [ + your_impl_view, + hidden_tests_view, + // your_tests_view, + impl_validation_view, + ], + ); }; let reset_button = inject =>