diff --git a/app/commands/exercise/representation/create_search_index_document.rb b/app/commands/exercise/representation/create_search_index_document.rb index 70f9da8abc..940ba275e4 100644 --- a/app/commands/exercise/representation/create_search_index_document.rb +++ b/app/commands/exercise/representation/create_search_index_document.rb @@ -25,6 +25,7 @@ def call num_solutions: representation.num_published_solutions, code: published_iteration.submission.files.map(&:content) || [], max_reputation:, + tags:, exercise: { id: solution.exercise.id, slug: solution.exercise.slug, @@ -61,5 +62,20 @@ def max_reputation ).maximum(:reputation).to_i end + def tags + return [] if last_analyzed_submission_representation.nil? + + last_analyzed_submission_representation.submission.analysis.tags + end + + memoize + def last_analyzed_submission_representation + representation. + submission_representations. + joins(submission: :analysis). + where(submission: { analysis_status: :completed }). + last + end + attr_reader :solution, :published_iteration end diff --git a/app/commands/exercise/update_tags.rb b/app/commands/exercise/update_tags.rb new file mode 100644 index 0000000000..d428267be3 --- /dev/null +++ b/app/commands/exercise/update_tags.rb @@ -0,0 +1,20 @@ +class Exercise::UpdateTags + include Mandate + + initialize_with :exercise + + def call = exercise.update(tags:) + + private + def tags + solution_tags = Solution::Tag.where(exercise:).distinct.pluck(:tag) + existing_tags = Exercise::Tag.where(exercise:).where(tag: solution_tags).select(:id, :tag).to_a + exercise_tags = existing_tags.map(&:tag) + + new_tags = (solution_tags - exercise_tags).map do |tag| + Exercise::Tag.find_or_create_by!(tag:, exercise:) + end + + existing_tags + new_tags + end +end diff --git a/app/commands/solution/publish_iteration.rb b/app/commands/solution/publish_iteration.rb index aab2301f09..8c2d5a941a 100644 --- a/app/commands/solution/publish_iteration.rb +++ b/app/commands/solution/publish_iteration.rb @@ -6,6 +6,7 @@ class Solution::PublishIteration def call solution.update!(published_iteration: iteration) + Solution::UpdateTags.(solution) Solution::UpdatePublishedExerciseRepresentation.(solution) Solution::UpdateSnippet.(solution) Solution::UpdateNumLoc.(solution) diff --git a/app/commands/solution/unpublish.rb b/app/commands/solution/unpublish.rb index 1f7238b1aa..9cc9c202c7 100644 --- a/app/commands/solution/unpublish.rb +++ b/app/commands/solution/unpublish.rb @@ -5,6 +5,7 @@ class Solution::Unpublish def call solution.update!(published_iteration_id: nil, published_at: nil) + Solution::UpdateTags.(solution) Solution::UpdatePublishedExerciseRepresentation.(solution) Solution::UpdateSnippet.(solution) Solution::UpdateNumLoc.(solution) diff --git a/app/commands/solution/update_tags.rb b/app/commands/solution/update_tags.rb new file mode 100644 index 0000000000..aa73cf377a --- /dev/null +++ b/app/commands/solution/update_tags.rb @@ -0,0 +1,28 @@ +class Solution::UpdateTags + include Mandate + + initialize_with :solution + + def call + solution.update(tags:) + Exercise::UpdateTags.(solution.exercise) + end + + private + def tags + return [] if latest_analysis.nil? + + analysis_tags = latest_analysis.tags + existing_tags = Solution::Tag.where(solution:).where(tag: analysis_tags).select(:id, :tag).to_a + solution_tags = existing_tags.map(&:tag) + + new_tags = (analysis_tags - solution_tags).map do |tag| + Solution::Tag.find_or_create_by!(tag:, solution:) + end + + existing_tags + new_tags + end + + memoize + def latest_analysis = solution.latest_published_iteration_submission&.analysis +end diff --git a/app/commands/submission/analysis/process.rb b/app/commands/submission/analysis/process.rb index 2293a5f3db..2e3f761caa 100644 --- a/app/commands/submission/analysis/process.rb +++ b/app/commands/submission/analysis/process.rb @@ -9,7 +9,8 @@ def call analysis = submission.create_analysis!( tooling_job_id: tooling_job.id, ops_status: tooling_job.execution_status.to_i, - data: + data:, + tags_data: ) begin @@ -39,6 +40,7 @@ def handle_ops_error! def handle_completed! submission.analysis_completed! + Solution::UpdateTags.(submission.solution) end memoize @@ -50,7 +52,20 @@ def submission def data res = JSON.parse(tooling_job.execution_output['analysis.json']) res.is_a?(Hash) ? res.symbolize_keys : {} - rescue StandardError + rescue StandardError => e + Bugsnag.notify(e) + {} + end + + memoize + def tags_data + tags_json = tooling_job.execution_output['tags.json'] + return {} if tags_json.empty? + + res = JSON.parse(tags_json) + res.is_a?(Hash) ? res.symbolize_keys : {} + rescue StandardError => e + Bugsnag.notify(e) {} end end diff --git a/app/commands/submission/representation/process.rb b/app/commands/submission/representation/process.rb index 5415e80038..5e87c0945d 100644 --- a/app/commands/submission/representation/process.rb +++ b/app/commands/submission/representation/process.rb @@ -32,7 +32,8 @@ def submission memoize def ast tooling_job.execution_output['representation.txt'] - rescue StandardError + rescue StandardError => e + Bugsnag.notify(e) nil end @@ -40,15 +41,20 @@ def ast def mapping res = JSON.parse(tooling_job.execution_output['mapping.json']) res.is_a?(Hash) ? res.symbolize_keys : {} - rescue StandardError + rescue StandardError => e + Bugsnag.notify(e) {} end memoize def metadata - res = JSON.parse(tooling_job.execution_output['representation.json']) + representation_json = tooling_job.execution_output['representation.json'] + return {} if representation_json.empty? + + res = JSON.parse(representation_json) res.is_a?(Hash) ? res.symbolize_keys : {} - rescue StandardError + rescue StandardError => e + Bugsnag.notify(e) {} end diff --git a/app/commands/submission/test_run/process.rb b/app/commands/submission/test_run/process.rb index 347b972eec..b81cd5be8e 100644 --- a/app/commands/submission/test_run/process.rb +++ b/app/commands/submission/test_run/process.rb @@ -111,7 +111,8 @@ def broadcast!(test_run) def results res = JSON.parse(tooling_job.execution_output['results.json'], allow_invalid_unicode: true) res.is_a?(Hash) ? res.symbolize_keys : {} - rescue StandardError + rescue StandardError => e + Bugsnag.notify(e) {} end diff --git a/app/models/exercise.rb b/app/models/exercise.rb index 7f9853b493..06fb6b4741 100644 --- a/app/models/exercise.rb +++ b/app/models/exercise.rb @@ -19,6 +19,7 @@ class Exercise < ApplicationRecord has_many :representations, dependent: :destroy has_many :community_videos, dependent: :destroy has_many :site_updates, dependent: :destroy + has_many :tags, dependent: :destroy has_many :approaches, class_name: "Exercise::Approach", diff --git a/app/models/exercise/tag.rb b/app/models/exercise/tag.rb new file mode 100644 index 0000000000..df22b54ca4 --- /dev/null +++ b/app/models/exercise/tag.rb @@ -0,0 +1,14 @@ +class Exercise::Tag < ApplicationRecord + extend Mandate::Memoize + + belongs_to :exercise + + memoize + def category = tag.split(':').first + + memoize + def name = tag.split(':').second + + memoize + def to_s = "#{category.titleize}: #{name.titleize}" +end diff --git a/app/models/solution.rb b/app/models/solution.rb index bae61057f8..7c5562349d 100644 --- a/app/models/solution.rb +++ b/app/models/solution.rb @@ -40,6 +40,8 @@ class Solution < ApplicationRecord has_many :mentor_discussions, class_name: "Mentor::Discussion", dependent: :destroy has_many :mentors, through: :mentor_discussions + has_many :tags, dependent: :destroy + scope :completed, -> { where.not(completed_at: nil) } scope :not_completed, -> { where(completed_at: nil) } diff --git a/app/models/solution/tag.rb b/app/models/solution/tag.rb new file mode 100644 index 0000000000..97b97facb2 --- /dev/null +++ b/app/models/solution/tag.rb @@ -0,0 +1,21 @@ +class Solution::Tag < ApplicationRecord + extend Mandate::Memoize + + belongs_to :solution + belongs_to :exercise + belongs_to :user + + before_validation on: :create do + self.exercise_id = solution.exercise_id unless exercise_id + self.user_id = solution.user_id unless user_id + end + + memoize + def category = tag.split(':').first + + memoize + def name = tag.split(':').second + + memoize + def to_s = "#{category.titleize}: #{name.titleize}" +end diff --git a/app/models/submission/analysis.rb b/app/models/submission/analysis.rb index acd58fab25..5d0e53d968 100644 --- a/app/models/submission/analysis.rb +++ b/app/models/submission/analysis.rb @@ -3,6 +3,7 @@ class Submission::Analysis < ApplicationRecord include HasToolingJob serialize :data, JSON + serialize :tags_data, JSON belongs_to :submission belongs_to :track @@ -54,9 +55,11 @@ def num_comments_by_type end end - def summary - data[:summary].presence - end + memoize + def summary = data[:summary].presence + + memoize + def tags = tags_data[:tags].to_a memoize def comments @@ -96,9 +99,10 @@ def comment_blocks end memoize - def data - HashWithIndifferentAccess.new(super) - end + def data = HashWithIndifferentAccess.new(super) + + memoize + def tags_data = HashWithIndifferentAccess.new(super) def analyzer_repo = "#{submission.track.slug}-analyzer" diff --git a/db/migrate/20231013123032_add_tags_to_submission_analyses.rb b/db/migrate/20231013123032_add_tags_to_submission_analyses.rb new file mode 100644 index 0000000000..c91e49b8e5 --- /dev/null +++ b/db/migrate/20231013123032_add_tags_to_submission_analyses.rb @@ -0,0 +1,7 @@ +class AddTagsToSubmissionAnalyses < ActiveRecord::Migration[7.0] + def change + return if Rails.env.production? + + add_column :submission_analyses, :tags_data, :text, null: true + end +end diff --git a/db/migrate/20231017072611_create_solution_tags.rb b/db/migrate/20231017072611_create_solution_tags.rb new file mode 100644 index 0000000000..77e373299e --- /dev/null +++ b/db/migrate/20231017072611_create_solution_tags.rb @@ -0,0 +1,17 @@ +class CreateSolutionTags < ActiveRecord::Migration[7.0] + def change + return if Rails.env.production? + + create_table :solution_tags do |t| + t.references :solution, null: false, foreign_key: true + t.references :exercise, null: false, foreign_key: true + t.references :user, null: false, foreign_key: true + + t.string :tag, null: false + + t.index %i[solution_id tag], unique: true + + t.timestamps + end + end +end diff --git a/db/migrate/20231017101049_create_exercise_tags.rb b/db/migrate/20231017101049_create_exercise_tags.rb new file mode 100644 index 0000000000..f6f252fb1b --- /dev/null +++ b/db/migrate/20231017101049_create_exercise_tags.rb @@ -0,0 +1,16 @@ +class CreateExerciseTags < ActiveRecord::Migration[7.0] + def change + return if Rails.env.production? + + create_table :exercise_tags do |t| + t.references :exercise, null: false, foreign_key: true + + t.string :tag, null: false + t.boolean :filterable, null: false, default: true + + t.index %i[exercise_id tag], unique: true + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index f3c28b53f8..f6804bcb4e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2023_10_04_120817) do +ActiveRecord::Schema[7.0].define(version: 2023_10_17_101049) do create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| t.string "name", null: false t.string "record_type", null: false @@ -368,6 +368,16 @@ t.index ["uuid"], name: "index_exercise_representations_on_uuid", unique: true end + create_table "exercise_tags", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| + t.string "tag", null: false + t.boolean "filterable", default: true, null: false + t.bigint "exercise_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["exercise_id", "tag"], name: "index_exercise_tags_on_exercise_id_and_tag", unique: true + t.index ["exercise_id"], name: "index_exercise_tags_on_exercise_id" + end + create_table "exercise_taught_concepts", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| t.bigint "exercise_id", null: false t.bigint "track_concept_id", null: false @@ -852,6 +862,19 @@ t.index ["user_id"], name: "index_solution_stars_on_user_id" end + create_table "solution_tags", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| + t.string "tag", null: false + t.bigint "solution_id", null: false + t.bigint "exercise_id", null: false + t.bigint "user_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["exercise_id"], name: "index_solution_tags_on_exercise_id" + t.index ["solution_id", "tag"], name: "index_solution_tags_on_solution_id_and_tag", unique: true + t.index ["solution_id"], name: "index_solution_tags_on_solution_id" + t.index ["user_id"], name: "index_solution_tags_on_user_id" + end + create_table "solutions", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| t.bigint "user_id", null: false t.string "uuid", null: false @@ -940,6 +963,7 @@ t.datetime "updated_at", null: false t.integer "num_comments", limit: 1, default: 0, null: false t.bigint "track_id" + t.text "tags_data" t.index ["submission_id"], name: "index_submission_analyses_on_submission_id" t.index ["track_id", "id"], name: "index_submission_analyses_on_track_id_and_id" t.index ["track_id", "num_comments"], name: "index_submission_analyses_on_track_id_and_num_comments" @@ -1541,6 +1565,7 @@ add_foreign_key "exercise_representations", "tracks" add_foreign_key "exercise_representations", "users", column: "feedback_author_id" add_foreign_key "exercise_representations", "users", column: "feedback_editor_id" + add_foreign_key "exercise_tags", "exercises" add_foreign_key "exercise_taught_concepts", "exercises" add_foreign_key "exercise_taught_concepts", "track_concepts" add_foreign_key "exercises", "tracks" @@ -1574,6 +1599,9 @@ add_foreign_key "site_updates", "tracks" add_foreign_key "site_updates", "users", column: "author_id" add_foreign_key "solution_comments", "solutions" + add_foreign_key "solution_tags", "exercises" + add_foreign_key "solution_tags", "solutions" + add_foreign_key "solution_tags", "users" add_foreign_key "solutions", "exercise_representations", column: "published_exercise_representation_id" add_foreign_key "solutions", "exercises" add_foreign_key "solutions", "iterations", column: "published_iteration_id" diff --git a/test/commands/exercise/representation/create_search_index_document_test.rb b/test/commands/exercise/representation/create_search_index_document_test.rb index 01491559f8..62198648d9 100644 --- a/test/commands/exercise/representation/create_search_index_document_test.rb +++ b/test/commands/exercise/representation/create_search_index_document_test.rb @@ -60,6 +60,7 @@ class Exercise::Representation::CreateSearchIndexDocumentTest < ActiveSupport::T num_loc: solution.num_loc, num_solutions: num_published_solutions, max_reputation: 0, + tags: [], code: [content], exercise: { id: solution.exercise.id, @@ -156,4 +157,45 @@ class Exercise::Representation::CreateSearchIndexDocumentTest < ActiveSupport::T assert Exercise::Representation::CreateSearchIndexDocument.(representation) end end + + test "uses tags from last analyzed submission" do + exercise = create :practice_exercise + representation = create(:exercise_representation, exercise:) + + solution_1 = create :practice_solution, :published, exercise:, + published_exercise_representation: representation, + published_iteration_head_tests_status: :passed + create(:iteration, solution: solution_1) + submission_1 = create(:submission, solution: solution_1, analysis_status: :queued) + submission_1_tags = ["construct:throw", "paradigm:object-oriented"] + create(:submission_representation, submission: submission_1, ast_digest: representation.ast_digest) + create(:submission_analysis, submission: submission_1, tags_data: { tags: submission_1_tags }) + + actual_tags = Exercise::Representation::CreateSearchIndexDocument.(representation)[:tags] + assert_empty actual_tags + + solution_2 = create :practice_solution, :published, exercise:, + published_exercise_representation: representation, + published_iteration_head_tests_status: :passed + create(:iteration, solution: solution_2) + submission_2 = create(:submission, solution: solution_2, analysis_status: :completed) + submission_2_tags = ["construct:if", "paradigm:functional"] + create(:submission_representation, submission: submission_2, ast_digest: representation.ast_digest) + create(:submission_analysis, submission: submission_2, tags_data: { tags: submission_2_tags }) + + actual_tags = Exercise::Representation::CreateSearchIndexDocument.(representation)[:tags] + assert_equal submission_2_tags, actual_tags + + solution_3 = create :practice_solution, :published, exercise:, + published_exercise_representation: representation, + published_iteration_head_tests_status: :passed + create(:iteration, solution: solution_3) + submission_3 = create(:submission, solution: solution_3, analysis_status: :completed) + submission_3_tags = ["construct:while-loop", "paradigm:imperative"] + create(:submission_representation, submission: submission_3, ast_digest: representation.ast_digest) + create(:submission_analysis, submission: submission_3, tags_data: { tags: submission_3_tags }) + + actual_tags = Exercise::Representation::CreateSearchIndexDocument.(representation)[:tags] + assert_equal submission_3_tags, actual_tags + end end diff --git a/test/commands/exercise/update_tags_test.rb b/test/commands/exercise/update_tags_test.rb new file mode 100644 index 0000000000..4c2f8bb9bd --- /dev/null +++ b/test/commands/exercise/update_tags_test.rb @@ -0,0 +1,22 @@ +require "test_helper" + +class Exercise::UpdateTagsTest < ActiveSupport::TestCase + test "update tags" do + exercise = create :practice_exercise + other_exercise = create :practice_exercise + existing_tag_to_remove = create(:exercise_tag, tag: 'uses:date.add_days', exercise:) + existing_tag_to_keep = create(:exercise_tag, tag: 'construct:exception', exercise:) + solution_tag_to_keep = create(:solution_tag, tag: existing_tag_to_keep.tag, exercise:) + solution_tag_to_add = create(:solution_tag, tag: 'paradigm:object-oriented', exercise:) + + # Sanity check: tag should still be remove as this is linked to another exercise + create(:solution_tag, tag: existing_tag_to_remove.tag, exercise: other_exercise) + + assert_equal [existing_tag_to_remove.tag, existing_tag_to_keep.tag], exercise.reload.tags.order(:id).pluck(:tag) + + Exercise::UpdateTags.(exercise) + + assert_equal [solution_tag_to_keep.tag, solution_tag_to_add.tag], exercise.reload.tags.order(:id).pluck(:tag) + assert_raises ActiveRecord::RecordNotFound, &proc { existing_tag_to_remove.reload } + end +end diff --git a/test/commands/solution/publish_iteration_test.rb b/test/commands/solution/publish_iteration_test.rb index aafd6ff1a5..c0b200a011 100644 --- a/test/commands/solution/publish_iteration_test.rb +++ b/test/commands/solution/publish_iteration_test.rb @@ -72,4 +72,19 @@ class Solution::PublishIterationTest < ActiveSupport::TestCase Solution::Publish.(solution, user_track, nil) end + + test "updates tags" do + track = create :track + user = create :user + exercise = create(:concept_exercise, track:) + user_track = create(:user_track, user:, track:) + + create(:exercise_representation, exercise:) + solution = create(:concept_solution) + create(:iteration, submission: create(:submission, solution:)) + + Solution::UpdateTags.expects(:call).with(solution) + + Solution::Publish.(solution, user_track, nil) + end end diff --git a/test/commands/solution/unpublish_test.rb b/test/commands/solution/unpublish_test.rb index 675ed7bb40..b50a1cd5cc 100644 --- a/test/commands/solution/unpublish_test.rb +++ b/test/commands/solution/unpublish_test.rb @@ -55,4 +55,12 @@ class Solution::UnpublishTest < ActiveSupport::TestCase Solution::UpdatePublishedExerciseRepresentation.expects(:call).with(solution) Solution::Unpublish.(solution) end + + test "calls out to update tags" do + exercise = create(:concept_exercise) + solution = create(:concept_solution, :published, exercise:) + + Solution::UpdateTags.expects(:call).with(solution) + Solution::Unpublish.(solution) + end end diff --git a/test/commands/solution/update_tags_test.rb b/test/commands/solution/update_tags_test.rb new file mode 100644 index 0000000000..31c8ea0acd --- /dev/null +++ b/test/commands/solution/update_tags_test.rb @@ -0,0 +1,36 @@ +require "test_helper" + +class Solution::UpdateTagsTest < ActiveSupport::TestCase + test "update tags" do + exercise = create :practice_exercise + representation = create(:exercise_representation, exercise:) + + solution = create :practice_solution, :published, exercise:, + published_exercise_representation: representation, + published_iteration_head_tests_status: :passed + iteration = create(:iteration, solution:) + submission = create(:submission, solution:, iteration:, analysis_status: :completed) + submission_tags = ["construct:throw", "paradigm:object-oriented"] + create(:submission_representation, submission:, ast_digest: representation.ast_digest) + create(:submission_analysis, submission:, tags_data: { tags: submission_tags }) + + solution_tag_to_remove = create(:solution_tag, tag: 'uses:date.add_days', solution:) + solution_tag_to_keep = create(:solution_tag, tag: submission_tags.first, solution:) + + assert_equal [solution_tag_to_remove.tag, solution_tag_to_keep.tag], solution.reload.tags.order(:id).pluck(:tag) + + Solution::UpdateTags.(solution) + + assert_equal [solution_tag_to_keep.tag, submission_tags.second], solution.reload.tags.order(:id).pluck(:tag) + assert_raises ActiveRecord::RecordNotFound, &proc { solution_tag_to_remove.reload } + end + + test "update exercise tags" do + exercise = create :practice_exercise + solution = create(:practice_solution, exercise:) + + Exercise::UpdateTags.expects(:call).with(exercise) + + Solution::UpdateTags.(solution) + end +end diff --git a/test/commands/submission/analysis/process_test.rb b/test/commands/submission/analysis/process_test.rb index 75c8c1dbf6..47a12003f8 100644 --- a/test/commands/submission/analysis/process_test.rb +++ b/test/commands/submission/analysis/process_test.rb @@ -6,8 +6,10 @@ class Submission::Analysis::ProcessTest < ActiveSupport::TestCase ops_status = 200 comments = [{ 'foo' => 'bar' }] data = { 'comments' => comments } + tags = ["construct:while-loop", "paradigm:logic"] + tags_data = { 'tags' => tags } - job = create_analyzer_job!(submission, execution_status: ops_status, data:) + job = create_analyzer_job!(submission, execution_status: ops_status, data:, tags_data:) Submission::Analysis::Process.(job) analysis = submission.reload.analysis @@ -15,6 +17,7 @@ class Submission::Analysis::ProcessTest < ActiveSupport::TestCase assert_equal job.id, analysis.tooling_job_id assert_equal ops_status, analysis.ops_status assert_equal data, analysis.send(:data) + assert_equal tags_data, analysis.send(:tags_data) end test "handle ops error" do @@ -56,4 +59,16 @@ class Submission::Analysis::ProcessTest < ActiveSupport::TestCase job = create_analyzer_job!(submission, execution_status: 200, data:) Submission::Analysis::Process.(job) end + + test "updates tags" do + solution = create :practice_solution + submission = create(:submission, solution:) + create(:iteration, submission:) + data = { 'comments' => [] } + + Solution::UpdateTags.expects(:call).with(submission.solution) + + job = create_analyzer_job!(submission, execution_status: 200, data:) + Submission::Analysis::Process.(job) + end end diff --git a/test/factories/exercise/tags.rb b/test/factories/exercise/tags.rb new file mode 100644 index 0000000000..5d9cfb17e4 --- /dev/null +++ b/test/factories/exercise/tags.rb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :exercise_tag, class: 'Exercise::Tag' do + tag { "paradigm:functional" } + exercise { create :practice_exercise } + end +end diff --git a/test/factories/solution/tags.rb b/test/factories/solution/tags.rb new file mode 100644 index 0000000000..e8ad049b9d --- /dev/null +++ b/test/factories/solution/tags.rb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :solution_tag, class: 'Solution::Tag' do + tag { "paradigm:functional" } + solution { create :practice_solution } + end +end diff --git a/test/factories/submission/analyses.rb b/test/factories/submission/analyses.rb index 04fd4c3c2c..573d61356b 100644 --- a/test/factories/submission/analyses.rb +++ b/test/factories/submission/analyses.rb @@ -11,6 +11,12 @@ } end + tags_data do + { + tags: [] + } + end + trait :with_comments do data do { @@ -19,5 +25,13 @@ } end end + + trait :with_tags do + tags_data do + { + tags: ["construct:if", "paradigm:functional"] + } + end + end end end diff --git a/test/models/exercise/representation_test.rb b/test/models/exercise/representation_test.rb index d09cf80cd9..67598fe1d3 100644 --- a/test/models/exercise/representation_test.rb +++ b/test/models/exercise/representation_test.rb @@ -89,8 +89,6 @@ class Exercise::RepresentationTest < ActiveSupport::TestCase submission_representation = create(:submission_representation, submission: create(:submission, exercise:), ast_digest:) - p submission_representation.exercise_id_and_ast_digest_idx_cache - p representation.exercise_id_and_ast_digest_idx_cache assert_equal [submission_representation], representation.reload.submission_representations diff --git a/test/models/exercise/tag_test.rb b/test/models/exercise/tag_test.rb new file mode 100644 index 0000000000..b1d9ec065e --- /dev/null +++ b/test/models/exercise/tag_test.rb @@ -0,0 +1,18 @@ +require "test_helper" + +class Exercise::TagTest < ActiveSupport::TestCase + test "category" do + tag = create :exercise_tag, tag: 'construct:for-loop' + assert_equal "construct", tag.category + end + + test "name" do + tag = create :exercise_tag, tag: 'construct:for-loop' + assert_equal "for-loop", tag.name + end + + test "to_s" do + tag = create :exercise_tag, tag: 'construct:for-loop' + assert_equal "Construct: For Loop", tag.to_s + end +end diff --git a/test/models/solution/tag_test.rb b/test/models/solution/tag_test.rb new file mode 100644 index 0000000000..bf62a8df92 --- /dev/null +++ b/test/models/solution/tag_test.rb @@ -0,0 +1,31 @@ +require "test_helper" + +class Solution::TagTest < ActiveSupport::TestCase + test "wired correctly" do + user = create :user + track = create :track + exercise = create(:practice_exercise, track:) + solution = create(:practice_solution, exercise:, user:) + + solution_tag = create(:solution_tag, solution:) + + assert_equal solution, solution_tag.solution + assert_equal exercise, solution_tag.exercise + assert_equal user, solution_tag.user + end + + test "category" do + tag = create :solution_tag, tag: 'construct:for-loop' + assert_equal "construct", tag.category + end + + test "name" do + tag = create :solution_tag, tag: 'construct:for-loop' + assert_equal "for-loop", tag.name + end + + test "to_s" do + tag = create :solution_tag, tag: 'construct:for-loop' + assert_equal "Construct: For Loop", tag.to_s + end +end diff --git a/test/models/submission/analysis_test.rb b/test/models/submission/analysis_test.rb index 52d5546ba5..0afad88acf 100644 --- a/test/models/submission/analysis_test.rb +++ b/test/models/submission/analysis_test.rb @@ -24,6 +24,17 @@ class Submission::AnalysisTest < ActiveSupport::TestCase assert_nil analysis.summary end + test "tags is empty if nil" do + analysis = create :submission_analysis, tags_data: { tags: nil } + assert_empty analysis.tags + end + + test "tags returns tags from tags_data" do + tags = ["construct:if", "paradigm:functional"] + analysis = create :submission_analysis, tags_data: { tags: } + assert_equal tags, analysis.tags + end + test "comments doesn't raise" do TestHelpers.use_website_copy_test_repo! Github::Issue::Open.expects(:call) diff --git a/test/test_helper.rb b/test/test_helper.rb index 8332a0fd1d..5fd2ae25dd 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -261,9 +261,10 @@ def create_representer_job!(submission, execution_status: nil, ast: nil, mapping ) end - def create_analyzer_job!(submission, execution_status: nil, data: nil) + def create_analyzer_job!(submission, execution_status: nil, data: nil, tags_data: nil) execution_output = { - "analysis.json" => data&.to_json + "analysis.json" => data&.to_json, + "tags.json" => tags_data&.to_json } create_tooling_job!( submission,