From 67dea2e2564e2064282cd899d07dc645c6915717 Mon Sep 17 00:00:00 2001 From: rtblast70 Date: Thu, 18 Nov 2021 10:28:47 -0800 Subject: [PATCH 1/8] submit_01 - Added functionality to parse and store (java) student test cases into db - Migrated student_test_cases table - CodingPromptAnswer now has many student_test_cases --- app/jobs/code_worker.rb | 40 ++++++++++++++++++- app/models/coding_prompt.rb | 2 +- app/models/coding_prompt_answer.rb | 16 ++++++++ app/models/student_test_case.rb | 6 +++ ...0211112005057_create_student_test_cases.rb | 9 +++++ db/schema.rb | 8 +++- 6 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 app/models/student_test_case.rb create mode 100644 db/migrate/20211112005057_create_student_test_cases.rb diff --git a/app/jobs/code_worker.rb b/app/jobs/code_worker.rb index febd1813..b5c547c5 100644 --- a/app/jobs/code_worker.rb +++ b/app/jobs/code_worker.rb @@ -15,11 +15,45 @@ class CodeWorker # workers. workers 2 # 10 + def self.parse_java(string) + testList = [] + flag = false + sourceList = (string.split("/"))[1].split("*") + sourceList.each do |elem| + if elem.include? "@" + if elem.include? "@test" + flag = true + else + flag = false + end + elsif flag && elem.include?("->") + temp = elem.split("->") + #check elements here + testList.append([temp[0][/\(([^()]*)\)/, 1], temp[1].strip]) + end + end + return testList + end + + # ------------------------------------------------------------- + def self.parse_attempt(answer_text, language) + case language + when 'Java' + return parse_java(answer_text) + when 'Ruby' + return nil + when 'Python' + return nil + when 'C++' + return nil + end + end + + # ------------------------------------------------------------- def perform(attempt_id) ActiveRecord::Base.connection_pool.with_connection do start_time = Time.now - attempt = Attempt.find(attempt_id) exv = attempt.exercise_version prompt = exv.prompts.first.specific @@ -66,6 +100,10 @@ def perform(attempt_id) end FileUtils.cp(prompt.test_file_name, attempt_dir) File.write(attempt_dir + '/' + prompt.class_name + '.' + lang, code_body) + + # compile and load student tests into DB + + answer.create_student_tests!(answer_text, language, current_attempt) # Run static checks result = nil diff --git a/app/models/coding_prompt.rb b/app/models/coding_prompt.rb index cb58423f..d7381ea1 100644 --- a/app/models/coding_prompt.rb +++ b/app/models/coding_prompt.rb @@ -127,7 +127,7 @@ def regenerate_tests #~ Private instance methods ................................................. - private + private # ------------------------------------------------------------- def set_defaults diff --git a/app/models/coding_prompt_answer.rb b/app/models/coding_prompt_answer.rb index 8a03d848..52d31f18 100644 --- a/app/models/coding_prompt_answer.rb +++ b/app/models/coding_prompt_answer.rb @@ -19,6 +19,7 @@ class CodingPromptAnswer < ActiveRecord::Base #~ Relationships ............................................................ acts_as :prompt_answer + has_many :student_test_cases has_many :test_case_results, #-> { includes :test_case }, -> { order('test_case_id ASC').includes(:test_case) }, @@ -35,6 +36,21 @@ class CodingPromptAnswer < ActiveRecord::Base #~ Instance methods ......................................................... + # ------------------------------------------------------------- + def create_student_tests!(answer_text, language, id) + testList = CodeWorker.parse_attempt(answer_text, language) + testList.each do |test| + tc = StudentTestCase.new( + input: test[0], + expected_output: test[1], + coding_prompt_answer_id: self.id + ) + unless tc.save + puts "error saving test case: #{tc.errors.full_messages.to_s}" + end + end + end + # ------------------------------------------------------------- def execute_static_tests result = nil diff --git a/app/models/student_test_case.rb b/app/models/student_test_case.rb new file mode 100644 index 00000000..86cdf33c --- /dev/null +++ b/app/models/student_test_case.rb @@ -0,0 +1,6 @@ +class StudentTestCase < ActiveRecord::Base + + # Relationships + belongs_to :coding_prompt_answer + +end \ No newline at end of file diff --git a/db/migrate/20211112005057_create_student_test_cases.rb b/db/migrate/20211112005057_create_student_test_cases.rb new file mode 100644 index 00000000..9994f783 --- /dev/null +++ b/db/migrate/20211112005057_create_student_test_cases.rb @@ -0,0 +1,9 @@ +class CreateStudentTestCases < ActiveRecord::Migration + def change + create_table :student_test_cases do |t| + t.string :input + t.string :expected_output + t.references :coding_prompt_answer + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 4b8ce986..5be47144 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20201216191207) do +ActiveRecord::Schema.define(version: 20211112005057) do create_table "active_admin_comments", force: :cascade do |t| t.string "namespace", limit: 255 @@ -472,6 +472,12 @@ add_index "student_extensions", ["user_id"], name: "index_student_extensions_on_user_id", using: :btree add_index "student_extensions", ["workout_offering_id"], name: "index_student_extensions_on_workout_offering_id", using: :btree + create_table "student_test_cases", force: :cascade do |t| + t.string "input", limit: 255 + t.string "expected_output", limit: 255 + t.integer "coding_prompt_answer_id", limit: 4 + end + create_table "tag_user_scores", force: :cascade do |t| t.integer "user_id", limit: 4, null: false t.integer "experience", limit: 4, default: 0 From 94bd7bafaef4f248fe136e733339a734b971dace Mon Sep 17 00:00:00 2001 From: rtblast70 Date: Fri, 18 Feb 2022 15:36:14 -0800 Subject: [PATCH 2/8] student tests 1 - student_test_cases created and saved to db - created student_test_case_results - add column reference_solution to coding_prompts --- app/jobs/code_worker.rb | 39 +- app/models/coding_prompt.rb | 1 + app/models/coding_prompt_answer.rb | 25 +- app/models/student_test_case_result.rb | 6 + app/representers/coding_prompt_representer.rb | 1 + ...dd_reference_solution_to_coding_prompts.rb | 5 + ...180629_create_student_test_case_results.rb | 11 + db/schema.rb | 22 +- docker-compose.yml | 4 + example_exercise.yaml | 1 + install.sh | 1010 +++++++++++++++++ spec/factories/exercises.rb | 8 + usr/resources/config.rb | 6 +- 13 files changed, 1121 insertions(+), 18 deletions(-) create mode 100644 app/models/student_test_case_result.rb create mode 100644 db/migrate/20211119220458_add_reference_solution_to_coding_prompts.rb create mode 100644 db/migrate/20220120180629_create_student_test_case_results.rb create mode 100644 install.sh diff --git a/app/jobs/code_worker.rb b/app/jobs/code_worker.rb index b5c547c5..59981e70 100644 --- a/app/jobs/code_worker.rb +++ b/app/jobs/code_worker.rb @@ -63,7 +63,7 @@ def perform(attempt_id) answer_text = answer.answer answer_lines = answer_text ? answer_text.count("\n") : 0 if !prompt.wrapper_code.blank? - code_body = prompt.wrapper_code.sub(/\b___\b/, answer_text) + code_body = prompt.wrapper_code.sub(/\b___\b/, answer_text) # student's answer if $` # Want pre_lines to be a count of the number of lines preceding # the one the match is on, so use count() instead of lines() here @@ -85,7 +85,8 @@ def perform(attempt_id) term_name = term ? term.slug : 'no-term' # compile and evaluate the attempt in a temporary location - attempt_dir = "usr/attempts/active/#{current_attempt}" + working_dir = "usr/attempts/active/#{current_attempt}" ######## replicate this code but with inst soln + attempt_dir = "#{working_dir}/attempt" # puts "DIRECTORY",attempt_dir,"DIRECTORY" FileUtils.mkdir_p(attempt_dir) if !Dir[attempt_dir].empty? @@ -99,11 +100,36 @@ def perform(attempt_id) prompt.regenerate_tests end FileUtils.cp(prompt.test_file_name, attempt_dir) - File.write(attempt_dir + '/' + prompt.class_name + '.' + lang, code_body) + File.write(attempt_dir + '/' + prompt.class_name + '.' + lang, code_body) ### # compile and load student tests into DB - answer.create_student_tests!(answer_text, language, current_attempt) + answer.parse_student_tests!(answer_text, language, current_attempt) #rename + + #run against inst soln + + ref_dir = "#{working_dir}/reference" + ref_body = prompt.wrapper_code.sub(/\b___\b/, prompt.reference_solution) + + FileUtils.mkdir_p(ref_dir) + if !Dir[ref_dir].empty? + puts 'WARNING, OVERWRITING EXISTING DIRECTORY = ' + ref_dir + FileUtils.remove_dir(ref_dir, true) + FileUtils.mkdir_p(ref_dir) + end + if !File.exist?(prompt.test_file_name) + # Workaround for bug in correctly pre-generating test file + # on exercise creation. If it doesn't exist, force regeneration + answer.regenerate_tests + end + FileUtils.cp(prompt.test_file_name, ref_dir) + puts prompt.test_file_name + File.write(ref_dir + '/' + prompt.class_name + '.' + lang, ref_body) ### + + ref_lines = ref_body.count("\n") + execute_javatest( + prompt.class_name, ref_dir, pre_lines, ref_lines) + # Run static checks result = nil @@ -176,8 +202,9 @@ def perform(attempt_id) attempt.feedback_ready = true # clean up log and class files that were generated during testing + cleanup_files = Dir.glob([ - "#{attempt_dir}/*.class", + "#{working_dir}/**/*.class", "#{attempt_dir}/*.log", "#{attempt_dir}/reports/TEST-*.csv", "#{attempt_dir}/__pycache__/*.pyc", @@ -198,7 +225,7 @@ def perform(attempt_id) # move the attempt to permanent storage term_dir = "usr/attempts/#{term_name}/" FileUtils.mkdir_p(term_dir) # create the term_dir if it doesn't exist - FileUtils.mv(attempt_dir, term_dir) + FileUtils.mv(working_dir, term_dir) # calculate various time values. all times are in ms time_taken = (Time.now - attempt.submit_time) * 1000 diff --git a/app/models/coding_prompt.rb b/app/models/coding_prompt.rb index d7381ea1..8200e1ff 100644 --- a/app/models/coding_prompt.rb +++ b/app/models/coding_prompt.rb @@ -122,6 +122,7 @@ def regenerate_tests end # Default, if none of above cases return generate_CSV_tests(test_file_name) + puts 'regentests' + test_file_name end end diff --git a/app/models/coding_prompt_answer.rb b/app/models/coding_prompt_answer.rb index 52d31f18..a4583a43 100644 --- a/app/models/coding_prompt_answer.rb +++ b/app/models/coding_prompt_answer.rb @@ -33,11 +33,18 @@ class CodingPromptAnswer < ActiveRecord::Base # answer all prompts, and that would constitute an empty answer. We # want to allow that, so do not add validations preventing it. + def prompt_dir + 'usr/resources/' + self.language + '/tests/' + self.id.to_s + end + def test_file_name + prompt_dir + '/' + self.class_name + 'Test.' + + Exercise.extension_of(self.language) + end #~ Instance methods ......................................................... # ------------------------------------------------------------- - def create_student_tests!(answer_text, language, id) + def parse_student_tests!(answer_text, language, id) testList = CodeWorker.parse_attempt(answer_text, language) testList.each do |test| tc = StudentTestCase.new( @@ -49,6 +56,22 @@ def create_student_tests!(answer_text, language, id) puts "error saving test case: #{tc.errors.full_messages.to_s}" end end + #generate_CSV_tests(test_file_name) + end + + def generate_CSV_tests(file_name) #refactor + lang = self.language + tests = '' + self.test_cases.only_dynamic.each do |test_case| + tests << test_case.to_code(lang) + end + body = File.read('usr/resources/' + lang + '/' + lang + + 'BaseTestFile.' + Exercise.extension_of(lang)) + File.write(file_name, body % { + tests: tests, + method_name: self.method_name, + class_name: self.class_name + }) end # ------------------------------------------------------------- diff --git a/app/models/student_test_case_result.rb b/app/models/student_test_case_result.rb new file mode 100644 index 00000000..50dc5a5d --- /dev/null +++ b/app/models/student_test_case_result.rb @@ -0,0 +1,6 @@ +class StudentTestCaseResult < ActiveRecord::Base + + # Relationships + belongs_to :coding_prompt_answer + +end \ No newline at end of file diff --git a/app/representers/coding_prompt_representer.rb b/app/representers/coding_prompt_representer.rb index f541b570..fc364963 100644 --- a/app/representers/coding_prompt_representer.rb +++ b/app/representers/coding_prompt_representer.rb @@ -10,6 +10,7 @@ class CodingPromptRepresenter < Representable::Decorator property :class_name property :method_name property :starter_code + property :reference_solution property :wrapper_code property :test_script, as: :tests diff --git a/db/migrate/20211119220458_add_reference_solution_to_coding_prompts.rb b/db/migrate/20211119220458_add_reference_solution_to_coding_prompts.rb new file mode 100644 index 00000000..96e885d1 --- /dev/null +++ b/db/migrate/20211119220458_add_reference_solution_to_coding_prompts.rb @@ -0,0 +1,5 @@ +class AddReferenceSolutionToCodingPrompts < ActiveRecord::Migration + def change + add_column :coding_prompts, :reference_solution, :text + end +end diff --git a/db/migrate/20220120180629_create_student_test_case_results.rb b/db/migrate/20220120180629_create_student_test_case_results.rb new file mode 100644 index 00000000..8509aa2d --- /dev/null +++ b/db/migrate/20220120180629_create_student_test_case_results.rb @@ -0,0 +1,11 @@ +class CreateStudentTestCaseResults < ActiveRecord::Migration + def change + #drop_table :student_test_case_results + create_table :student_test_case_results do |t| + t.integer :test_case_id + t.string :feedback + t.boolean :pass + t.references :coding_prompt_answer + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 5be47144..c9f391b4 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20211112005057) do +ActiveRecord::Schema.define(version: 20220120180629) do create_table "active_admin_comments", force: :cascade do |t| t.string "namespace", limit: 255 @@ -87,12 +87,13 @@ create_table "coding_prompts", force: :cascade do |t| t.datetime "created_at" t.datetime "updated_at" - t.string "class_name", limit: 255 - t.text "wrapper_code", limit: 65535, null: false - t.text "test_script", limit: 65535, null: false - t.string "method_name", limit: 255 - t.text "starter_code", limit: 65535 - t.boolean "hide_examples", default: false, null: false + t.string "class_name", limit: 255 + t.text "wrapper_code", limit: 65535, null: false + t.text "test_script", limit: 65535, null: false + t.string "method_name", limit: 255 + t.text "starter_code", limit: 65535 + t.boolean "hide_examples", default: false, null: false + t.text "reference_solution", limit: 65535 end create_table "course_enrollments", force: :cascade do |t| @@ -472,6 +473,13 @@ add_index "student_extensions", ["user_id"], name: "index_student_extensions_on_user_id", using: :btree add_index "student_extensions", ["workout_offering_id"], name: "index_student_extensions_on_workout_offering_id", using: :btree + create_table "student_test_case_results", force: :cascade do |t| + t.integer "test_case_id", limit: 4 + t.string "feedback", limit: 255 + t.boolean "pass" + t.integer "coding_prompt_answer_id", limit: 4 + end + create_table "student_test_cases", force: :cascade do |t| t.string "input", limit: 255 t.string "expected_output", limit: 255 diff --git a/docker-compose.yml b/docker-compose.yml index 3294a3fd..e98022a9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,9 @@ version: '3.8' services: db_dev: + platform: linux/x86_64 image: mysql:5.7 + command: mysqld --max-connections=500 environment: MYSQL_ROOT_PASSWORD: root MYSQL_USER: codeworkout @@ -12,7 +14,9 @@ services: volumes: - dbdata:/var/lib/mysql:delegated db_test: + platform: linux/x86_64 image: mysql:5.7 + command: mysqld --max-connections=500 environment: MYSQL_ROOT_PASSWORD: root MYSQL_USER: codeworkout diff --git a/example_exercise.yaml b/example_exercise.yaml index 2d20403e..4d83ce93 100644 --- a/example_exercise.yaml +++ b/example_exercise.yaml @@ -73,6 +73,7 @@ { ___ } + #reference_solution wrapper_code: | public class Month { diff --git a/install.sh b/install.sh new file mode 100644 index 00000000..e4b4712e --- /dev/null +++ b/install.sh @@ -0,0 +1,1010 @@ +#!/bin/bash +set -u + +abort() { + printf "%s\n" "$@" + exit 1 +} + +if [ -z "${BASH_VERSION:-}" ] +then + abort "Bash is required to interpret this script." +fi + +# Check if script is run non-interactively (e.g. CI) +# If it is run non-interactively we should not prompt for passwords. +if [[ ! -t 0 || -n "${CI-}" ]] +then + NONINTERACTIVE=1 +fi + +# First check OS. +OS="$(uname)" +if [[ "${OS}" == "Linux" ]] +then + HOMEBREW_ON_LINUX=1 +elif [[ "${OS}" != "Darwin" ]] +then + abort "Homebrew is only supported on macOS and Linux." +fi + +# Required installation paths. To install elsewhere (which is unsupported) +# you can untar https://github.com/Homebrew/brew/tarball/master +# anywhere you like. +if [[ -z "${HOMEBREW_ON_LINUX-}" ]] +then + UNAME_MACHINE="$(/usr/bin/uname -m)" + + if [[ "${UNAME_MACHINE}" == "arm64" ]] + then + # On ARM macOS, this script installs to /opt/homebrew only + HOMEBREW_PREFIX="/opt/homebrew" + HOMEBREW_REPOSITORY="${HOMEBREW_PREFIX}" + else + # On Intel macOS, this script installs to /usr/local only + HOMEBREW_PREFIX="/usr/local" + HOMEBREW_REPOSITORY="${HOMEBREW_PREFIX}/Homebrew" + fi + HOMEBREW_CACHE="${HOME}/Library/Caches/Homebrew" + HOMEBREW_CORE_DEFAULT_GIT_REMOTE="https://github.com/Homebrew/homebrew-core" + + STAT_FLAG="-f" + PERMISSION_FORMAT="%A" + CHOWN="/usr/sbin/chown" + CHGRP="/usr/bin/chgrp" + GROUP="admin" + TOUCH="/usr/bin/touch" +else + UNAME_MACHINE="$(uname -m)" + + # On Linux, it installs to /home/linuxbrew/.linuxbrew if you have sudo access + # and ~/.linuxbrew (which is unsupported) if run interactively. + HOMEBREW_PREFIX_DEFAULT="/home/linuxbrew/.linuxbrew" + HOMEBREW_CACHE="${HOME}/.cache/Homebrew" + HOMEBREW_CORE_DEFAULT_GIT_REMOTE="https://github.com/Homebrew/linuxbrew-core" + + STAT_FLAG="--printf" + PERMISSION_FORMAT="%a" + CHOWN="/bin/chown" + CHGRP="/bin/chgrp" + GROUP="$(id -gn)" + TOUCH="/bin/touch" +fi +HOMEBREW_BREW_DEFAULT_GIT_REMOTE="https://github.com/Homebrew/brew" + +# Use remote URLs of Homebrew repositories from environment if set. +HOMEBREW_BREW_GIT_REMOTE="${HOMEBREW_BREW_GIT_REMOTE:-"${HOMEBREW_BREW_DEFAULT_GIT_REMOTE}"}" +HOMEBREW_CORE_GIT_REMOTE="${HOMEBREW_CORE_GIT_REMOTE:-"${HOMEBREW_CORE_DEFAULT_GIT_REMOTE}"}" +# The URLs with and without the '.git' suffix are the same Git remote. Do not prompt. +if [[ "${HOMEBREW_BREW_GIT_REMOTE}" == "${HOMEBREW_BREW_DEFAULT_GIT_REMOTE}.git" ]] +then + HOMEBREW_BREW_GIT_REMOTE="${HOMEBREW_BREW_DEFAULT_GIT_REMOTE}" +fi +if [[ "${HOMEBREW_CORE_GIT_REMOTE}" == "${HOMEBREW_CORE_DEFAULT_GIT_REMOTE}.git" ]] +then + HOMEBREW_CORE_GIT_REMOTE="${HOMEBREW_CORE_DEFAULT_GIT_REMOTE}" +fi +export HOMEBREW_{BREW,CORE}_GIT_REMOTE + +# TODO: bump version when new macOS is released or announced +MACOS_NEWEST_UNSUPPORTED="12.0" +# TODO: bump version when new macOS is released +MACOS_OLDEST_SUPPORTED="10.14" + +# For Homebrew on Linux +REQUIRED_RUBY_VERSION=2.6 # https://github.com/Homebrew/brew/pull/6556 +REQUIRED_GLIBC_VERSION=2.13 # https://docs.brew.sh/Homebrew-on-Linux#requirements +REQUIRED_CURL_VERSION=7.41.0 # HOMEBREW_MINIMUM_CURL_VERSION in brew.sh in Homebrew/brew +REQUIRED_GIT_VERSION=2.7.0 # HOMEBREW_MINIMUM_GIT_VERSION in brew.sh in Homebrew/brew + +# no analytics during installation +export HOMEBREW_NO_ANALYTICS_THIS_RUN=1 +export HOMEBREW_NO_ANALYTICS_MESSAGE_OUTPUT=1 + +# string formatters +if [[ -t 1 ]] +then + tty_escape() { printf "\033[%sm" "$1"; } +else + tty_escape() { :; } +fi +tty_mkbold() { tty_escape "1;$1"; } +tty_underline="$(tty_escape "4;39")" +tty_blue="$(tty_mkbold 34)" +tty_red="$(tty_mkbold 31)" +tty_bold="$(tty_mkbold 39)" +tty_reset="$(tty_escape 0)" + +unset HAVE_SUDO_ACCESS # unset this from the environment + +have_sudo_access() { + if [[ ! -x "/usr/bin/sudo" ]] + then + return 1 + fi + + local -a args + if [[ -n "${SUDO_ASKPASS-}" ]] + then + args=("-A") + elif [[ -n "${NONINTERACTIVE-}" ]] + then + args=("-n") + fi + + if [[ -z "${HAVE_SUDO_ACCESS-}" ]] + then + if [[ -n "${args[*]-}" ]] + then + SUDO="/usr/bin/sudo ${args[*]}" + else + SUDO="/usr/bin/sudo" + fi + if [[ -n "${NONINTERACTIVE-}" ]] + then + # Don't add quotes around ${SUDO} here + ${SUDO} -l mkdir &>/dev/null + else + ${SUDO} -v && ${SUDO} -l mkdir &>/dev/null + fi + HAVE_SUDO_ACCESS="$?" + fi + + if [[ -z "${HOMEBREW_ON_LINUX-}" ]] && [[ "${HAVE_SUDO_ACCESS}" -ne 0 ]] + then + abort "Need sudo access on macOS (e.g. the user ${USER} needs to be an Administrator)!" + fi + + return "${HAVE_SUDO_ACCESS}" +} + +shell_join() { + local arg + printf "%s" "$1" + shift + for arg in "$@" + do + printf " " + printf "%s" "${arg// /\ }" + done +} + +chomp() { + printf "%s" "${1/"$'\n'"/}" +} + +ohai() { + printf "${tty_blue}==>${tty_bold} %s${tty_reset}\n" "$(shell_join "$@")" +} + +warn() { + printf "${tty_red}Warning${tty_reset}: %s\n" "$(chomp "$1")" +} + +execute() { + if ! "$@" + then + abort "$(printf "Failed during: %s" "$(shell_join "$@")")" + fi +} + +execute_sudo() { + local -a args=("$@") + if have_sudo_access + then + if [[ -n "${SUDO_ASKPASS-}" ]] + then + args=("-A" "${args[@]}") + fi + ohai "/usr/bin/sudo" "${args[@]}" + execute "/usr/bin/sudo" "${args[@]}" + else + ohai "${args[@]}" + execute "${args[@]}" + fi +} + +getc() { + local save_state + save_state="$(/bin/stty -g)" + /bin/stty raw -echo + IFS='' read -r -n 1 -d '' "$@" + /bin/stty "${save_state}" +} + +ring_bell() { + # Use the shell's audible bell. + if [[ -t 1 ]] + then + printf "\a" + fi +} + +wait_for_user() { + local c + echo + echo "Press RETURN to continue or any other key to abort" + getc c + # we test for \r and \n because some stuff does \r instead + if ! [[ "${c}" == $'\r' || "${c}" == $'\n' ]] + then + exit 1 + fi +} + +major_minor() { + echo "${1%%.*}.$( + x="${1#*.}" + echo "${x%%.*}" + )" +} + +version_gt() { + [[ "${1%.*}" -gt "${2%.*}" ]] || [[ "${1%.*}" -eq "${2%.*}" && "${1#*.}" -gt "${2#*.}" ]] +} +version_ge() { + [[ "${1%.*}" -gt "${2%.*}" ]] || [[ "${1%.*}" -eq "${2%.*}" && "${1#*.}" -ge "${2#*.}" ]] +} +version_lt() { + [[ "${1%.*}" -lt "${2%.*}" ]] || [[ "${1%.*}" -eq "${2%.*}" && "${1#*.}" -lt "${2#*.}" ]] +} + +should_install_command_line_tools() { + if [[ -n "${HOMEBREW_ON_LINUX-}" ]] + then + return 1 + fi + + if version_gt "${macos_version}" "10.13" + then + ! [[ -e "/Library/Developer/CommandLineTools/usr/bin/git" ]] + else + ! [[ -e "/Library/Developer/CommandLineTools/usr/bin/git" ]] || + ! [[ -e "/usr/include/iconv.h" ]] + fi +} + +get_permission() { + stat "${STAT_FLAG}" "${PERMISSION_FORMAT}" "$1" +} + +user_only_chmod() { + [[ -d "$1" ]] && [[ "$(get_permission "$1")" != 75[0145] ]] +} + +exists_but_not_writable() { + [[ -e "$1" ]] && ! [[ -r "$1" && -w "$1" && -x "$1" ]] +} + +get_owner() { + stat "${STAT_FLAG}" "%u" "$1" +} + +file_not_owned() { + [[ "$(get_owner "$1")" != "$(id -u)" ]] +} + +get_group() { + stat "${STAT_FLAG}" "%g" "$1" +} + +file_not_grpowned() { + [[ " $(id -G "${USER}") " != *" $(get_group "$1") "* ]] +} + +# Please sync with 'test_ruby()' in 'Library/Homebrew/utils/ruby.sh' from Homebrew/brew repository. +test_ruby() { + if [[ ! -x "$1" ]] + then + return 1 + fi + + "$1" --enable-frozen-string-literal --disable=gems,did_you_mean,rubyopt -rrubygems -e \ + "abort if Gem::Version.new(RUBY_VERSION.to_s.dup).to_s.split('.').first(2) != \ + Gem::Version.new('${REQUIRED_RUBY_VERSION}').to_s.split('.').first(2)" 2>/dev/null +} + +test_curl() { + if [[ ! -x "$1" ]] + then + return 1 + fi + + local curl_version_output curl_name_and_version + curl_version_output="$("$1" --version 2>/dev/null)" + curl_name_and_version="${curl_version_output%% (*}" + version_ge "$(major_minor "${curl_name_and_version##* }")" "$(major_minor "${REQUIRED_CURL_VERSION}")" +} + +test_git() { + if [[ ! -x "$1" ]] + then + return 1 + fi + + local git_version_output + git_version_output="$("$1" --version 2>/dev/null)" + version_ge "$(major_minor "${git_version_output##* }")" "$(major_minor "${REQUIRED_GIT_VERSION}")" +} + +# Search given executable in PATH (remove dependency for `which` command) +which() { + # Alias to Bash built-in command `type -P` + type -P "$@" +} + +# Search PATH for the specified program that satisfies Homebrew requirements +# function which is set above +# shellcheck disable=SC2230 +find_tool() { + if [[ $# -ne 1 ]] + then + return 1 + fi + + local executable + while read -r executable + do + if "test_$1" "${executable}" + then + echo "${executable}" + break + fi + done < <(which -a "$1") +} + +no_usable_ruby() { + [[ -z "$(find_tool ruby)" ]] +} + +outdated_glibc() { + local glibc_version + glibc_version="$(ldd --version | head -n1 | grep -o '[0-9.]*$' | grep -o '^[0-9]\+\.[0-9]\+')" + version_lt "${glibc_version}" "${REQUIRED_GLIBC_VERSION}" +} + +if [[ -n "${HOMEBREW_ON_LINUX-}" ]] && no_usable_ruby && outdated_glibc +then + abort "$( + cat <<-EOFABORT +Homebrew requires Ruby ${REQUIRED_RUBY_VERSION} which was not found on your system. +Homebrew portable Ruby requires Glibc version ${REQUIRED_GLIBC_VERSION} or newer, +and your Glibc version is too old. +See ${tty_underline}https://docs.brew.sh/Homebrew-on-Linux#requirements${tty_reset} +Install Ruby ${REQUIRED_RUBY_VERSION} and add its location to your PATH. +EOFABORT + )" +fi + +# USER isn't always set so provide a fall back for the installer and subprocesses. +if [[ -z "${USER-}" ]] +then + USER="$(chomp "$(id -un)")" + export USER +fi + +# Invalidate sudo timestamp before exiting (if it wasn't active before). +if [[ -x /usr/bin/sudo ]] && ! /usr/bin/sudo -n -v 2>/dev/null +then + trap '/usr/bin/sudo -k' EXIT +fi + +# Things can fail later if `pwd` doesn't exist. +# Also sudo prints a warning message for no good reason +cd "/usr" || exit 1 + +####################################################################### script +if ! command -v git >/dev/null +then + abort "$( + cat </dev/null +then + abort "$( + cat </dev/null + then + ohai "Select the Homebrew installation directory" + echo "- ${tty_bold}Enter your password${tty_reset} to install to ${tty_underline}${HOMEBREW_PREFIX_DEFAULT}${tty_reset} (${tty_bold}recommended${tty_reset})" + echo "- ${tty_bold}Press Control-D${tty_reset} to install to ${tty_underline}${HOME}/.linuxbrew${tty_reset}" + echo "- ${tty_bold}Press Control-C${tty_reset} to cancel installation" + fi + if have_sudo_access + then + HOMEBREW_PREFIX="${HOMEBREW_PREFIX_DEFAULT}" + else + HOMEBREW_PREFIX="${HOME}/.linuxbrew" + fi + trap - SIGINT + fi + HOMEBREW_REPOSITORY="${HOMEBREW_PREFIX}/Homebrew" +fi +HOMEBREW_CORE="${HOMEBREW_REPOSITORY}/Library/Taps/homebrew/homebrew-core" + +if [[ "${EUID:-${UID}}" == "0" ]] +then + # Allow Azure Pipelines/GitHub Actions/Docker/Concourse/Kubernetes to do everything as root (as it's normal there) + if ! [[ -f /proc/1/cgroup ]] || + ! grep -E "azpl_job|actions_job|docker|garden|kubepods" -q /proc/1/cgroup + then + abort "Don't run this as root!" + fi +fi + +if [[ -d "${HOMEBREW_PREFIX}" && ! -x "${HOMEBREW_PREFIX}" ]] +then + abort "$( + cat </dev/null || return + + # we do it in four steps to avoid merge errors when reinstalling + execute "git" "init" "-q" + + # "git remote add" will fail if the remote is defined in the global config + execute "git" "config" "remote.origin.url" "${HOMEBREW_BREW_GIT_REMOTE}" + execute "git" "config" "remote.origin.fetch" "+refs/heads/*:refs/remotes/origin/*" + + # ensure we don't munge line endings on checkout + execute "git" "config" "core.autocrlf" "false" + + execute "git" "fetch" "--force" "origin" + execute "git" "fetch" "--force" "--tags" "origin" + + execute "git" "reset" "--hard" "origin/master" + + if [[ "${HOMEBREW_REPOSITORY}" != "${HOMEBREW_PREFIX}" ]] + then + if [[ "${HOMEBREW_REPOSITORY}" == "${HOMEBREW_PREFIX}/Homebrew" ]] + then + execute "ln" "-sf" "../Homebrew/bin/brew" "${HOMEBREW_PREFIX}/bin/brew" + else + abort "The Homebrew/brew repository should be placed in the Homebrew prefix directory." + fi + fi + + if [[ ! -d "${HOMEBREW_CORE}" ]] + then + ohai "Tapping homebrew/core" + ( + execute "/bin/mkdir" "-p" "${HOMEBREW_CORE}" + cd "${HOMEBREW_CORE}" >/dev/null || return + + execute "git" "init" "-q" + execute "git" "config" "remote.origin.url" "${HOMEBREW_CORE_GIT_REMOTE}" + execute "git" "config" "remote.origin.fetch" "+refs/heads/*:refs/remotes/origin/*" + execute "git" "config" "core.autocrlf" "false" + execute "git" "fetch" "--force" "origin" "refs/heads/master:refs/remotes/origin/master" + execute "git" "remote" "set-head" "origin" "--auto" >/dev/null + execute "git" "reset" "--hard" "origin/master" + + cd "${HOMEBREW_REPOSITORY}" >/dev/null || return + ) || exit 1 + fi + + execute "${HOMEBREW_PREFIX}/bin/brew" "update" "--force" "--quiet" +) || exit 1 + +if [[ ":${PATH}:" != *":${HOMEBREW_PREFIX}/bin:"* ]] +then + warn "${HOMEBREW_PREFIX}/bin is not in your PATH. + Instructions on how to configure your shell for Homebrew + can be found in the 'Next steps' section below." +fi + +ohai "Installation successful!" +echo + +ring_bell + +# Use an extra newline and bold to avoid this being missed. +ohai "Homebrew has enabled anonymous aggregate formulae and cask analytics." +echo "$( + cat </dev/null || return + execute "git" "config" "--replace-all" "homebrew.analyticsmessage" "true" + execute "git" "config" "--replace-all" "homebrew.caskanalyticsmessage" "true" +) || exit 1 + +ohai "Next steps:" +case "${SHELL}" in + */bash*) + if [[ -r "${HOME}/.bash_profile" ]] + then + shell_profile="${HOME}/.bash_profile" + else + shell_profile="${HOME}/.profile" + fi + ;; + */zsh*) + shell_profile="${HOME}/.zprofile" + ;; + *) + shell_profile="${HOME}/.profile" + ;; +esac +if [[ "${UNAME_MACHINE}" == "arm64" ]] || [[ -n "${HOMEBREW_ON_LINUX-}" ]] +then + cat <> ${shell_profile} + eval "\$(${HOMEBREW_PREFIX}/bin/brew shellenv)" +EOS +fi +if [[ -n "${non_default_repos}" ]] +then + plural="" + if [[ "${#additional_shellenv_commands[@]}" -gt 1 ]] + then + plural="s" + fi + echo "- Run these commands in your terminal to add the non-default Git remote${plural} for ${non_default_repos}:" + printf " echo '%s' >> ${shell_profile}\n" "${additional_shellenv_commands[@]}" + printf " %s\n" "${additional_shellenv_commands[@]}" +fi + +echo "- Run \`brew help\` to get started" +echo "- Further documentation: " +echo " ${tty_underline}https://docs.brew.sh${tty_reset}" + +if [[ -n "${HOMEBREW_ON_LINUX-}" ]] +then + echo "- Install the Homebrew dependencies if you have sudo access:" + + if [[ -x "$(command -v apt-get)" ]] + then + echo " sudo apt-get install build-essential" + elif [[ -x "$(command -v yum)" ]] + then + echo " sudo yum groupinstall 'Development Tools'" + elif [[ -x "$(command -v pacman)" ]] + then + echo " sudo pacman -S base-devel" + elif [[ -x "$(command -v apk)" ]] + then + echo " sudo apk add build-base" + fi + + cat < Date: Wed, 16 Mar 2022 18:37:04 -0700 Subject: [PATCH 3/8] test case refactor + testcase_helper.rb module --- app/helpers/testcase_helper.rb | 99 ++++++++++++++++++++++++++++++ app/jobs/code_worker.rb | 13 ++-- app/models/coding_prompt.rb | 24 ++------ app/models/coding_prompt_answer.rb | 16 ----- app/models/student_test_case.rb | 2 + app/models/test_case.rb | 64 ------------------- 6 files changed, 110 insertions(+), 108 deletions(-) create mode 100644 app/helpers/testcase_helper.rb diff --git a/app/helpers/testcase_helper.rb b/app/helpers/testcase_helper.rb new file mode 100644 index 00000000..1c31b4ba --- /dev/null +++ b/app/helpers/testcase_helper.rb @@ -0,0 +1,99 @@ +module TestcaseHelper + + # if called from coding_prompt.rb, cp_answer is nil + def generate_CSV_tests(file_name, coding_prompt, cp_answer) + lang = coding_prompt.language + tests = '' + if cp_answer.nil? + loc = coding_prompt.test_cases.only_dynamic + else + loc = cp_answer.student_test_cases + end + loc.each do |test_case| + tests << self.to_code(lang, test_case) + end + body = File.read('usr/resources/' + lang + '/' + lang + + 'BaseTestFile.' + Exercise.extension_of(lang)) + File.write(file_name, body % { + tests: tests, + method_name: coding_prompt.method_name, + class_name: coding_prompt.class_name + }) + end + + # moved from test_case.rb and student_test_case.rb + def to_code(language, test_case) + inp = test_case.input + if !inp.blank? + # TODO: need to fix this to handle nested parens appropriately + inp.gsub!(/map\([^()]*\)/) do |map_expr| + map_expr.split(/"/).map.with_index{ |x, i| + if i % 2 == 0 + x.gsub(/\s*=(>?)\s*/, ', ') + else + x + end + }.join('"') + end + end + if test_case.is_a?(StudentTestCase) + coding_prompt = test_case.coding_prompt_answer.actable.prompt.specific + TEST_METHOD_TEMPLATES[language] % { + id: test_case.id.to_s, + method_name: coding_prompt.method_name, + class_name: coding_prompt.class_name, + input: inp, + expected_output: test_case.expected_output, + negative_feedback: 'check your understanding', + } + else + TEST_METHOD_TEMPLATES[language] % { + id: test_case.id.to_s, + method_name: test_case.coding_prompt.method_name, + class_name: test_case.coding_prompt.class_name, + input: inp, + expected_output: test_case.expected_output, + negative_feedback: test_case.negative_feedback, + array: ((test_case.expected_output.start_with?('new ') && + test_case.expected_output.include?('[]')) || + test_case.expected_output.start_with?('array(')) ? 'Array' : '' + } + end + end + + + # heredoc + # moved from test_case.rb and student_test_case.rb + TEST_METHOD_TEMPLATES = { + 'Ruby' => < < < <?)\s*/, ', ') - else - x - end - }.join('"') - end - end - TEST_METHOD_TEMPLATES[language] % { - id: self.id.to_s, - method_name: coding_prompt.method_name, - class_name: coding_prompt.class_name, - input: inp, - expected_output: self.expected_output, - negative_feedback: self.negative_feedback, - array: ((self.expected_output.start_with?('new ') && - self.expected_output.include?('[]')) || - self.expected_output.start_with?('array(')) ? 'Array' : '' - } - end - - # ------------------------------------------------------------- def parse_description_specifier(desc) desc.strip! if !desc.blank? @@ -358,38 +328,4 @@ def to_choices(str, regex) str.sub(regex, '').tr(',', ' ').strip.split(/\s+/).uniq.join('|') end - - TEST_METHOD_TEMPLATES = { - 'Ruby' => < < < < Date: Tue, 29 Mar 2022 11:25:23 -0700 Subject: [PATCH 4/8] test case refactor edits TestcaseHelper -> TestCaseHelper testcase_helper.rb -> test_case_helper.rb cp_answer param in generate_CSV_tests defaults to nil --- app/helpers/{testcase_helper.rb => test_case_helper.rb} | 6 +++--- app/jobs/code_worker.rb | 2 +- app/models/coding_prompt.rb | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) rename app/helpers/{testcase_helper.rb => test_case_helper.rb} (97%) diff --git a/app/helpers/testcase_helper.rb b/app/helpers/test_case_helper.rb similarity index 97% rename from app/helpers/testcase_helper.rb rename to app/helpers/test_case_helper.rb index 1c31b4ba..7d75caa1 100644 --- a/app/helpers/testcase_helper.rb +++ b/app/helpers/test_case_helper.rb @@ -1,7 +1,7 @@ -module TestcaseHelper +module TestCaseHelper # if called from coding_prompt.rb, cp_answer is nil - def generate_CSV_tests(file_name, coding_prompt, cp_answer) + def generate_CSV_tests(file_name, coding_prompt, cp_answer = nil) lang = coding_prompt.language tests = '' if cp_answer.nil? @@ -96,4 +96,4 @@ def test%{id}(self): } CPP_TEST } -end \ No newline at end of file +end diff --git a/app/jobs/code_worker.rb b/app/jobs/code_worker.rb index 0f86f8b6..c8943297 100644 --- a/app/jobs/code_worker.rb +++ b/app/jobs/code_worker.rb @@ -8,7 +8,7 @@ class CodeWorker include SuckerPunch::Job - include TestcaseHelper + include TestCaseHelper # Reducing to 2 workers, since it seems that each puma process will # have its own job queue and its own set of sucker punch worker threads. # We'll get parallelism through puma processes instead of sucker punch diff --git a/app/models/coding_prompt.rb b/app/models/coding_prompt.rb index aaf16d0f..75933074 100644 --- a/app/models/coding_prompt.rb +++ b/app/models/coding_prompt.rb @@ -23,7 +23,7 @@ # gem). # class CodingPrompt < ActiveRecord::Base - include TestcaseHelper + include TestCaseHelper #~ Relationships ............................................................ @@ -122,7 +122,7 @@ def regenerate_tests end end # Default, if none of above cases return - generate_CSV_tests(test_file_name, self, nil) + generate_CSV_tests(test_file_name, self) puts 'regentests' + test_file_name end end @@ -175,7 +175,7 @@ def parse_tests end # Default, if none of above cases return parse_CSV_tests(self.test_script) - generate_CSV_tests(test_file_name, self, nil) + generate_CSV_tests(test_file_name, self) end end From 61555f2ca63b41367fe3213328be973ddb739e02 Mon Sep 17 00:00:00 2001 From: rtblast70 Date: Thu, 5 May 2022 11:50:30 -0700 Subject: [PATCH 5/8] basic student test case display --- app/jobs/code_worker.rb | 6 ++++++ app/models/coding_prompt_answer.rb | 1 + app/models/student_test_case.rb | 30 +++++++++++++++++++++++++- app/models/student_test_case_result.rb | 5 +++++ app/views/sse/_ajax_feedback.html.haml | 16 ++++++++++++++ 5 files changed, 57 insertions(+), 1 deletion(-) diff --git a/app/jobs/code_worker.rb b/app/jobs/code_worker.rb index c8943297..e218007d 100644 --- a/app/jobs/code_worker.rb +++ b/app/jobs/code_worker.rb @@ -127,6 +127,12 @@ def perform(attempt_id) execute_javatest( prompt.class_name, ref_dir, pre_lines, ref_lines) + CSV.foreach(ref_dir + '/results.csv') do |line| + # find test id + test_id = line[2][/\d+$/].to_i + test_case = answer.student_test_cases.where(id: test_id).first + tc_score = test_case.record_result(answer, line) + end # Run static checks result = nil diff --git a/app/models/coding_prompt_answer.rb b/app/models/coding_prompt_answer.rb index 329c4a17..f85955b4 100644 --- a/app/models/coding_prompt_answer.rb +++ b/app/models/coding_prompt_answer.rb @@ -20,6 +20,7 @@ class CodingPromptAnswer < ActiveRecord::Base acts_as :prompt_answer has_many :student_test_cases + has_many :student_test_case_results has_many :test_case_results, #-> { includes :test_case }, -> { order('test_case_id ASC').includes(:test_case) }, diff --git a/app/models/student_test_case.rb b/app/models/student_test_case.rb index e11ae6f3..a0d4b339 100644 --- a/app/models/student_test_case.rb +++ b/app/models/student_test_case.rb @@ -3,6 +3,34 @@ class StudentTestCase < ActiveRecord::Base # Relationships belongs_to :coding_prompt_answer - #refactored to testcase + def record_result(answer, test_results_array) + tcr = StudentTestCaseResult.new( + test_case_id: self, + coding_prompt_answer: answer, + pass: (test_results_array.length == 8 && test_results_array[7].to_i == 1) + ) + # This logic is somewhat Java-specific, and needs to be refactored + # to better support other languages. + if !test_results_array[5].blank? + exception_name = test_results_array[6] #.sub(/^.*\./, '') + if !(['AssertionFailedError', + 'AssertionError', + 'ComparisonFailure', + 'ReflectionSupportError'].include?(exception_name) || + (exception_name == 'Exception' && + test_results_array[6].start_with?('test timed out'))) || + test_results_array[6].blank? || + "null" == test_results_array[6] + tcr.feedback = exception_name + puts(tcr.feedback) + end + end + tcr.save! + end + + def display_description(pass = true) + result = self.feedback + result + end end \ No newline at end of file diff --git a/app/models/student_test_case_result.rb b/app/models/student_test_case_result.rb index 50dc5a5d..b5ec54b7 100644 --- a/app/models/student_test_case_result.rb +++ b/app/models/student_test_case_result.rb @@ -2,5 +2,10 @@ class StudentTestCaseResult < ActiveRecord::Base # Relationships belongs_to :coding_prompt_answer + belongs_to :student_test_case + + def display_description + student_test_case.display_description(self.pass) + end end \ No newline at end of file diff --git a/app/views/sse/_ajax_feedback.html.haml b/app/views/sse/_ajax_feedback.html.haml index 44ca3973..b16951fe 100644 --- a/app/views/sse/_ajax_feedback.html.haml +++ b/app/views/sse/_ajax_feedback.html.haml @@ -50,6 +50,22 @@ %tbody - has_hidden = false - hidden_pass = true + %tr + %td Your test cases + - answer.student_test_case_results(true).each do |tcr| + %tr + - if tcr.pass + %td.pass + %i.fa.fa-check-square + - else + %td.fail + %i.fa.fa-minus-square + %td + -# = tcr.display_description + - if !tcr.pass + .out= tcr.feedback + %tr + %td Instructor test cases - answer.test_case_results(true).each do |tcr| - if tcr.test_case.hidden? - has_hidden = true From 01cbbdab283f4d27885f4075d721a71e88f245db Mon Sep 17 00:00:00 2001 From: rtblast70 Date: Tue, 17 May 2022 10:39:47 -0700 Subject: [PATCH 6/8] student test case display description+cosmetics --- app/jobs/code_worker.rb | 3 ++- app/models/coding_prompt.rb | 1 + app/models/coding_prompt_answer.rb | 1 + app/models/student_test_case.rb | 9 ++++----- app/models/student_test_case_result.rb | 9 +++++---- app/views/sse/_ajax_feedback.html.haml | 8 +++----- ...220506035419_add_description_to_student_test_cases.rb | 5 +++++ db/schema.rb | 3 ++- 8 files changed, 23 insertions(+), 16 deletions(-) create mode 100644 db/migrate/20220506035419_add_description_to_student_test_cases.rb diff --git a/app/jobs/code_worker.rb b/app/jobs/code_worker.rb index e218007d..fc32e500 100644 --- a/app/jobs/code_worker.rb +++ b/app/jobs/code_worker.rb @@ -27,9 +27,10 @@ def self.parse_java(string) flag = false end elsif flag && elem.include?("->") + temp = elem.split("->") #check elements here - testList.append([temp[0][/\(([^()]*)\)/, 1], temp[1].strip]) + testList.append([temp[0][/\(([^()]*)\)/, 1], temp[1].strip, elem]) end end return testList diff --git a/app/models/coding_prompt.rb b/app/models/coding_prompt.rb index 75933074..707bae03 100644 --- a/app/models/coding_prompt.rb +++ b/app/models/coding_prompt.rb @@ -295,6 +295,7 @@ def parse_JUnit_tests end end tc.parse_description_specifier(desc) + puts(tc.description) # look for "example" tag in comments or attribute if comment =~ /example\s*:\s*true\s*(?:\*\/\s*)?$/i || attrs =~ /@Example\b/ diff --git a/app/models/coding_prompt_answer.rb b/app/models/coding_prompt_answer.rb index f85955b4..c479078d 100644 --- a/app/models/coding_prompt_answer.rb +++ b/app/models/coding_prompt_answer.rb @@ -51,6 +51,7 @@ def parse_student_tests!(answer_text, language, id) tc = StudentTestCase.new( input: test[0], expected_output: test[1], + description: test[2], coding_prompt_answer_id: self.id ) unless tc.save diff --git a/app/models/student_test_case.rb b/app/models/student_test_case.rb index a0d4b339..83012353 100644 --- a/app/models/student_test_case.rb +++ b/app/models/student_test_case.rb @@ -2,10 +2,11 @@ class StudentTestCase < ActiveRecord::Base # Relationships belongs_to :coding_prompt_answer + has_one :student_test_case_result def record_result(answer, test_results_array) tcr = StudentTestCaseResult.new( - test_case_id: self, + test_case_id: self.id, coding_prompt_answer: answer, pass: (test_results_array.length == 8 && test_results_array[7].to_i == 1) ) @@ -22,15 +23,13 @@ def record_result(answer, test_results_array) test_results_array[6].blank? || "null" == test_results_array[6] tcr.feedback = exception_name - puts(tcr.feedback) end end tcr.save! end - def display_description(pass = true) - result = self.feedback - result + def display_description() + self.description end end \ No newline at end of file diff --git a/app/models/student_test_case_result.rb b/app/models/student_test_case_result.rb index b5ec54b7..c5ebdfd6 100644 --- a/app/models/student_test_case_result.rb +++ b/app/models/student_test_case_result.rb @@ -1,11 +1,12 @@ class StudentTestCaseResult < ActiveRecord::Base # Relationships - belongs_to :coding_prompt_answer - belongs_to :student_test_case - + belongs_to :coding_prompt_answer, inverse_of: :student_test_case_results + belongs_to :student_test_case, class_name: "StudentTestCase", + foreign_key: :test_case_id, inverse_of: :student_test_case_result + def display_description - student_test_case.display_description(self.pass) + student_test_case.display_description() end end \ No newline at end of file diff --git a/app/views/sse/_ajax_feedback.html.haml b/app/views/sse/_ajax_feedback.html.haml index b16951fe..c13cda0b 100644 --- a/app/views/sse/_ajax_feedback.html.haml +++ b/app/views/sse/_ajax_feedback.html.haml @@ -51,7 +51,7 @@ - has_hidden = false - hidden_pass = true %tr - %td Your test cases + %td{colspan: 2} Your test cases - answer.student_test_case_results(true).each do |tcr| %tr - if tcr.pass @@ -61,11 +61,9 @@ %td.fail %i.fa.fa-minus-square %td - -# = tcr.display_description - - if !tcr.pass - .out= tcr.feedback + = tcr.display_description %tr - %td Instructor test cases + %td{colspan: 2} Instructor test cases - answer.test_case_results(true).each do |tcr| - if tcr.test_case.hidden? - has_hidden = true diff --git a/db/migrate/20220506035419_add_description_to_student_test_cases.rb b/db/migrate/20220506035419_add_description_to_student_test_cases.rb new file mode 100644 index 00000000..b99054ac --- /dev/null +++ b/db/migrate/20220506035419_add_description_to_student_test_cases.rb @@ -0,0 +1,5 @@ +class AddDescriptionToStudentTestCases < ActiveRecord::Migration + def change + add_column :student_test_cases, :description, :text + end +end diff --git a/db/schema.rb b/db/schema.rb index c9f391b4..ea686888 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20220120180629) do +ActiveRecord::Schema.define(version: 20220506035419) do create_table "active_admin_comments", force: :cascade do |t| t.string "namespace", limit: 255 @@ -484,6 +484,7 @@ t.string "input", limit: 255 t.string "expected_output", limit: 255 t.integer "coding_prompt_answer_id", limit: 4 + t.text "description", limit: 65535 end create_table "tag_user_scores", force: :cascade do |t| From d9a310306890d7083bc04622328d1a5af2e750ce Mon Sep 17 00:00:00 2001 From: rtblast70 Date: Thu, 19 May 2022 13:15:56 -0700 Subject: [PATCH 7/8] minor commenting --- app/jobs/code_worker.rb | 5 ++--- app/models/student_test_case.rb | 3 +-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/app/jobs/code_worker.rb b/app/jobs/code_worker.rb index fc32e500..4c715325 100644 --- a/app/jobs/code_worker.rb +++ b/app/jobs/code_worker.rb @@ -104,11 +104,10 @@ def perform(attempt_id) File.write(attempt_dir + '/' + prompt.class_name + '.' + lang, code_body) ### # compile and load student tests into DB - answer.parse_student_tests!(answer_text, language, current_attempt) #rename - #run against inst soln - + # run against inst soln + # creating reference directory ref_dir = "#{working_dir}/reference" ref_body = prompt.wrapper_code.sub(/\b___\b/, prompt.reference_solution) diff --git a/app/models/student_test_case.rb b/app/models/student_test_case.rb index 83012353..af5c8c42 100644 --- a/app/models/student_test_case.rb +++ b/app/models/student_test_case.rb @@ -10,8 +10,7 @@ def record_result(answer, test_results_array) coding_prompt_answer: answer, pass: (test_results_array.length == 8 && test_results_array[7].to_i == 1) ) - # This logic is somewhat Java-specific, and needs to be refactored - # to better support other languages. + if !test_results_array[5].blank? exception_name = test_results_array[6] #.sub(/^.*\./, '') if !(['AssertionFailedError', From ea5a5871f196f42e89a5d2d49046f809b3a51dbb Mon Sep 17 00:00:00 2001 From: rtblast70 Date: Sun, 29 May 2022 17:44:10 -0700 Subject: [PATCH 8/8] minor edits --- app/jobs/code_worker.rb | 20 ++++++++++++-------- app/models/coding_prompt.rb | 1 - app/models/coding_prompt_answer.rb | 2 +- db/schema.rb | 15 +++++++++++++-- usr/resources/config.rb | 2 +- 5 files changed, 27 insertions(+), 13 deletions(-) diff --git a/app/jobs/code_worker.rb b/app/jobs/code_worker.rb index 4c715325..c0caf966 100644 --- a/app/jobs/code_worker.rb +++ b/app/jobs/code_worker.rb @@ -15,11 +15,13 @@ class CodeWorker # workers. workers 2 # 10 - def self.parse_java(string) - testList = [] + # grabs student-written tests from answer text + # String -> List[[]] + def self.get_tests_from_javadoc(answer_text) + test_list = [] flag = false - sourceList = (string.split("/"))[1].split("*") - sourceList.each do |elem| + source_list = (answer_text.split("/"))[1].split("*") + source_list.each do |elem| if elem.include? "@" if elem.include? "@test" flag = true @@ -30,17 +32,19 @@ def self.parse_java(string) temp = elem.split("->") #check elements here - testList.append([temp[0][/\(([^()]*)\)/, 1], temp[1].strip, elem]) + test_list.append([temp[0][/\(([^()]*)\)/, 1], temp[1].strip, elem]) end end - return testList + return test_list end # ------------------------------------------------------------- - def self.parse_attempt(answer_text, language) + # directs parsing to appropriate method + # String, String -> List[[]] + def self.get_tests_from_answer_text(answer_text, language) case language when 'Java' - return parse_java(answer_text) + return get_tests_from_javadoc(answer_text) when 'Ruby' return nil when 'Python' diff --git a/app/models/coding_prompt.rb b/app/models/coding_prompt.rb index f8a09594..e2c3a845 100644 --- a/app/models/coding_prompt.rb +++ b/app/models/coding_prompt.rb @@ -126,7 +126,6 @@ def regenerate_tests end # Default, if none of above cases return generate_CSV_tests(test_file_name, self) - puts 'regentests' + test_file_name end end diff --git a/app/models/coding_prompt_answer.rb b/app/models/coding_prompt_answer.rb index c479078d..314e5a65 100644 --- a/app/models/coding_prompt_answer.rb +++ b/app/models/coding_prompt_answer.rb @@ -46,7 +46,7 @@ def test_file_name # ------------------------------------------------------------- def parse_student_tests!(answer_text, language, id) - testList = CodeWorker.parse_attempt(answer_text, language) + testList = CodeWorker.get_tests_from_answer_text(answer_text, language) testList.each do |test| tc = StudentTestCase.new( input: test[0], diff --git a/db/schema.rb b/db/schema.rb index aa0402c1..9d98572d 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -217,6 +217,7 @@ end add_index "exercise_versions", ["creator_id"], name: "exercise_versions_creator_id_fk", using: :btree + add_index "exercise_versions", ["exercise_id"], name: "index_exercise_versions_on_exercise_id", using: :btree add_index "exercise_versions", ["irt_data_id"], name: "exercise_versions_irt_data_id_fk", using: :btree add_index "exercise_versions", ["stem_id"], name: "index_exercise_versions_on_stem_id", using: :btree @@ -255,6 +256,7 @@ t.integer "exercise_collection_id", limit: 4 end + add_index "exercises", ["current_version_id"], name: "index_exercises_on_current_version_id", using: :btree add_index "exercises", ["exercise_collection_id"], name: "index_exercises_on_exercise_collection_id", using: :btree add_index "exercises", ["exercise_family_id"], name: "index_exercises_on_exercise_family_id", using: :btree add_index "exercises", ["external_id"], name: "index_exercises_on_external_id", unique: true, using: :btree @@ -346,7 +348,7 @@ add_index "lms_instances", ["url"], name: "index_lms_instances_on_url", unique: true, using: :btree create_table "lms_types", force: :cascade do |t| - t.string "name", limit: 255, null: false + t.string "name", limit: 255, default: "", null: false t.datetime "created_at" t.datetime "updated_at" end @@ -366,7 +368,7 @@ create_table "lti_workouts", force: :cascade do |t| t.integer "workout_id", limit: 4 - t.string "lms_assignment_id", limit: 255, null: false + t.string "lms_assignment_id", limit: 255, default: "", null: false t.datetime "created_at" t.datetime "updated_at" t.integer "lms_instance_id", limit: 4 @@ -411,6 +413,7 @@ add_index "ownerships", ["exercise_version_id"], name: "index_ownerships_on_exercise_version_id", using: :btree add_index "ownerships", ["filename"], name: "index_ownerships_on_filename", using: :btree + add_index "ownerships", ["resource_file_id"], name: "index_ownerships_on_resource_file_id", using: :btree create_table "prompt_answers", force: :cascade do |t| t.integer "attempt_id", limit: 4 @@ -437,6 +440,7 @@ end add_index "prompts", ["actable_id"], name: "index_prompts_on_actable_id", using: :btree + add_index "prompts", ["exercise_version_id"], name: "index_prompts_on_exercise_version_id", using: :btree add_index "prompts", ["irt_data_id"], name: "prompts_irt_data_id_fk", using: :btree create_table "resource_files", force: :cascade do |t| @@ -577,6 +581,8 @@ t.boolean "hidden", default: false, null: false end + add_index "test_cases", ["coding_prompt_id"], name: "index_test_cases_on_coding_prompt_id", using: :btree + create_table "time_zones", force: :cascade do |t| t.string "name", limit: 255 t.string "zone", limit: 255 @@ -747,6 +753,7 @@ add_foreign_key "courses", "organizations", name: "courses_organization_id_fk" add_foreign_key "exercise_owners", "exercises", name: "exercise_owners_exercise_id_fk" add_foreign_key "exercise_owners", "users", column: "owner_id", name: "exercise_owners_owner_id_fk" + add_foreign_key "exercise_versions", "exercises", name: "exercise_versions_exercise_id_fk" add_foreign_key "exercise_versions", "irt_data", column: "irt_data_id", name: "exercise_versions_irt_data_id_fk" add_foreign_key "exercise_versions", "stems", name: "exercise_versions_stem_id_fk" add_foreign_key "exercise_versions", "users", column: "creator_id", name: "exercise_versions_creator_id_fk" @@ -755,13 +762,16 @@ add_foreign_key "exercise_workouts", "exercises", name: "exercise_workouts_exercise_id_fk" add_foreign_key "exercise_workouts", "workouts", name: "exercise_workouts_workout_id_fk" add_foreign_key "exercises", "exercise_families", name: "exercises_exercise_family_id_fk" + add_foreign_key "exercises", "exercise_versions", column: "current_version_id", name: "exercises_current_version_id_fk" add_foreign_key "exercises", "irt_data", column: "irt_data_id", name: "exercises_irt_data_id_fk" add_foreign_key "identities", "users", name: "identities_user_id_fk" add_foreign_key "lms_instances", "lms_types", name: "lms_instances_lms_type_id_fk" add_foreign_key "lti_workouts", "lms_instances" add_foreign_key "ownerships", "exercise_versions" + add_foreign_key "ownerships", "resource_files" add_foreign_key "prompt_answers", "attempts", name: "prompt_answers_attempt_id_fk" add_foreign_key "prompt_answers", "prompts", name: "prompt_answers_prompt_id_fk" + add_foreign_key "prompts", "exercise_versions", name: "prompts_exercise_version_id_fk" add_foreign_key "prompts", "irt_data", column: "irt_data_id", name: "prompts_irt_data_id_fk" add_foreign_key "resource_files", "users", name: "resource_files_user_id_fk" add_foreign_key "student_extensions", "users", name: "student_extensions_user_id_fk" @@ -770,6 +780,7 @@ add_foreign_key "test_case_results", "coding_prompt_answers", name: "test_case_results_coding_prompt_answer_id_fk" add_foreign_key "test_case_results", "test_cases", name: "test_case_results_test_case_id_fk" add_foreign_key "test_case_results", "users", name: "test_case_results_user_id_fk" + add_foreign_key "test_cases", "coding_prompts", name: "test_cases_coding_prompt_id_fk" add_foreign_key "users", "global_roles", name: "users_global_role_id_fk" add_foreign_key "users", "time_zones", name: "users_time_zone_id_fk" add_foreign_key "users", "workout_scores", column: "current_workout_score_id", name: "users_current_workout_score_id_fk" diff --git a/usr/resources/config.rb b/usr/resources/config.rb index f019b86e..680ee8e6 100644 --- a/usr/resources/config.rb +++ b/usr/resources/config.rb @@ -28,7 +28,7 @@ def self.which_path(cmd) '-Dattempt_dir=%{attempt_dir} ' \ '-Dbasedir=. ' \ '-l ant.log ' \ - '-f ../../../../../usr/resources/Java/build.xml', + '-f ../../../../usr/resources/Java/build.xml', # daemon_url: "http://localhost:8080/javadaemon/cr?dir=%{attempt_dir}" }, cpp: {