From 0fc149c00eb87fe05d2acfc644e18b2a85b9b2f6 Mon Sep 17 00:00:00 2001 From: adi-herwana-nus Date: Sun, 2 Aug 2026 02:53:07 +0800 Subject: [PATCH] fix(codaveri): fix crash on import question with empty data files --- .../c_sharp/c_sharp_package_service.rb | 21 ---- .../go/go_package_service.rb | 21 ---- .../java/java_package_service.rb | 22 +--- .../java_script_package_service.rb | 21 ---- .../language_package_service.rb | 34 ++++++ .../python/python_package_service.rb | 21 ---- .../r/r_package_service.rb | 21 ---- .../rust/rust_package_service.rb | 21 ---- .../type_script_package_service.rb | 21 ---- ...tion_template_codaveri_empty_data_file.zip | Bin 0 -> 4041 bytes .../language_package_service_spec.rb | 115 ++++++++++++++++++ 11 files changed, 154 insertions(+), 164 deletions(-) create mode 100644 spec/fixtures/course/programming_question_template_codaveri_empty_data_file.zip create mode 100644 spec/services/course/assessment/question/programming_codaveri/language_package_service_spec.rb diff --git a/app/services/course/assessment/question/programming_codaveri/c_sharp/c_sharp_package_service.rb b/app/services/course/assessment/question/programming_codaveri/c_sharp/c_sharp_package_service.rb index 16c93ae6f30..2360882dbe0 100644 --- a/app/services/course/assessment/question/programming_codaveri/c_sharp/c_sharp_package_service.rb +++ b/app/services/course/assessment/question/programming_codaveri/c_sharp/c_sharp_package_service.rb @@ -97,27 +97,6 @@ def extract_supporting_solution_files end end - # Extracts filename and content of a data file and append it to the - # [:additionalFiles] array for the problem management API request body. - # - # @param [Pathname] pathname The pathname of the file. - # @param [String] content The content of the file. - def extract_supporting_file(filename, content) - supporting_file_object = default_codaveri_data_file_template - - supporting_file_object[:type] = 'internal' # 'external' s3 upload not yet implemented by codaveri - supporting_file_object[:path] = filename.to_s - if content.force_encoding('UTF-8').valid_encoding? - supporting_file_object[:content] = content - supporting_file_object[:encoding] = 'utf8' - else - supporting_file_object[:content] = Base64.strict_encode64(content) - supporting_file_object[:encoding] = 'base64' - end - - @data_files.append(supporting_file_object) - end - # Extracts test cases from the built dummy reports and append all the test cases to the # [:IOTestcases] array for the problem management API request body. def extract_test_cases # rubocop:disable Metrics/AbcSize diff --git a/app/services/course/assessment/question/programming_codaveri/go/go_package_service.rb b/app/services/course/assessment/question/programming_codaveri/go/go_package_service.rb index 4156f3dd069..aadc11cf9e9 100644 --- a/app/services/course/assessment/question/programming_codaveri/go/go_package_service.rb +++ b/app/services/course/assessment/question/programming_codaveri/go/go_package_service.rb @@ -97,27 +97,6 @@ def extract_supporting_solution_files end end - # Extracts filename and content of a data file and append it to the - # [:additionalFiles] array for the problem management API request body. - # - # @param [Pathname] pathname The pathname of the file. - # @param [String] content The content of the file. - def extract_supporting_file(filename, content) - supporting_file_object = default_codaveri_data_file_template - - supporting_file_object[:type] = 'internal' # 'external' s3 upload not yet implemented by codaveri - supporting_file_object[:path] = filename.to_s - if content.force_encoding('UTF-8').valid_encoding? - supporting_file_object[:content] = content - supporting_file_object[:encoding] = 'utf8' - else - supporting_file_object[:content] = Base64.strict_encode64(content) - supporting_file_object[:encoding] = 'base64' - end - - @data_files.append(supporting_file_object) - end - # Extracts test cases from the built dummy reports and append all the test cases to the # [:IOTestcases] array for the problem management API request body. def extract_test_cases # rubocop:disable Metrics/AbcSize diff --git a/app/services/course/assessment/question/programming_codaveri/java/java_package_service.rb b/app/services/course/assessment/question/programming_codaveri/java/java_package_service.rb index ca782a0e72c..44809f559e2 100644 --- a/app/services/course/assessment/question/programming_codaveri/java/java_package_service.rb +++ b/app/services/course/assessment/question/programming_codaveri/java/java_package_service.rb @@ -103,23 +103,11 @@ def extract_supporting_tests_files end end - def extract_supporting_file(filename, content) - supporting_file_object = default_codaveri_data_file_template - - supporting_file_object[:type] = 'internal' # 'external' s3 upload not yet implemented by codaveri - supporting_file_object[:path] = filename.to_s - # TODO: remove filename.to_s.downcase.end_with?('.java') check - # For now, only plaintext files that require compiling (e.g. *.java) will use 'utf8' ecoding - # Pending Codaveri 'utf8' encoding support for all plaintext files in compiled languages - if content.force_encoding('UTF-8').valid_encoding? && filename.to_s.downcase.end_with?('.java') - supporting_file_object[:content] = content - supporting_file_object[:encoding] = 'utf8' - else - supporting_file_object[:content] = Base64.strict_encode64(content) - supporting_file_object[:encoding] = 'base64' - end - - @data_files.append(supporting_file_object) + # TODO: remove filename.to_s.downcase.end_with?('.java') check + # For now, only plaintext files that require compiling (e.g. *.java) will use 'utf8' ecoding + # Pending Codaveri 'utf8' encoding support for all plaintext files in compiled languages + def utf8_encodable?(filename, utf8_content) + super && filename.to_s.downcase.end_with?('.java') end def extract_template diff --git a/app/services/course/assessment/question/programming_codaveri/java_script/java_script_package_service.rb b/app/services/course/assessment/question/programming_codaveri/java_script/java_script_package_service.rb index c5382efd647..1293ead1815 100644 --- a/app/services/course/assessment/question/programming_codaveri/java_script/java_script_package_service.rb +++ b/app/services/course/assessment/question/programming_codaveri/java_script/java_script_package_service.rb @@ -97,27 +97,6 @@ def extract_supporting_solution_files end end - # Extracts filename and content of a data file and append it to the - # [:additionalFiles] array for the problem management API request body. - # - # @param [Pathname] pathname The pathname of the file. - # @param [String] content The content of the file. - def extract_supporting_file(filename, content) - supporting_file_object = default_codaveri_data_file_template - - supporting_file_object[:type] = 'internal' # 'external' s3 upload not yet implemented by codaveri - supporting_file_object[:path] = filename.to_s - if content.force_encoding('UTF-8').valid_encoding? - supporting_file_object[:content] = content - supporting_file_object[:encoding] = 'utf8' - else - supporting_file_object[:content] = Base64.strict_encode64(content) - supporting_file_object[:encoding] = 'base64' - end - - @data_files.append(supporting_file_object) - end - # Extracts test cases from the built dummy reports and append all the test cases to the # [:IOTestcases] array for the problem management API request body. def extract_test_cases # rubocop:disable Metrics/AbcSize diff --git a/app/services/course/assessment/question/programming_codaveri/language_package_service.rb b/app/services/course/assessment/question/programming_codaveri/language_package_service.rb index 3bc81d09294..88775832ccb 100644 --- a/app/services/course/assessment/question/programming_codaveri/language_package_service.rb +++ b/app/services/course/assessment/question/programming_codaveri/language_package_service.rb @@ -61,6 +61,40 @@ def process_evaluator private + # Extracts filename and content of a data file and append it to the + # [:additionalFiles] array for the problem management API request body. + # + # @param [Pathname] filename The pathname of the file. + # @param [String] content The content of the file. + def extract_supporting_file(filename, content) + supporting_file_object = default_codaveri_data_file_template + + supporting_file_object[:type] = 'internal' # 'external' s3 upload not yet implemented by codaveri + supporting_file_object[:path] = filename.to_s + # `content` is read straight out of the zip and may be frozen (rubyzip returns a frozen empty + # string literal for zero-byte entries), so tag the encoding on a copy rather than in place. + utf8_content = content.dup.force_encoding('UTF-8') + if utf8_encodable?(filename, utf8_content) + supporting_file_object[:content] = utf8_content + supporting_file_object[:encoding] = 'utf8' + else + supporting_file_object[:content] = Base64.strict_encode64(content) + supporting_file_object[:encoding] = 'base64' + end + + @data_files.append(supporting_file_object) + end + + # Whether a supporting file may be sent to Codaveri as plaintext 'utf8' rather than 'base64'. + # Concrete services may narrow this further; see the Java package service. + # + # @param [Pathname] filename The pathname of the file. + # @param [String] utf8_content The content of the file, tagged as UTF-8. + # @return [Boolean] + def utf8_encodable?(_filename, utf8_content) + utf8_content.valid_encoding? + end + # Defines the default solution template as indicated in the Codevari API problem management spec. # # @return [Hash] diff --git a/app/services/course/assessment/question/programming_codaveri/python/python_package_service.rb b/app/services/course/assessment/question/programming_codaveri/python/python_package_service.rb index 714a83fb060..4b7acfb2910 100644 --- a/app/services/course/assessment/question/programming_codaveri/python/python_package_service.rb +++ b/app/services/course/assessment/question/programming_codaveri/python/python_package_service.rb @@ -97,27 +97,6 @@ def extract_supporting_solution_files end end - # Extracts filename and content of a data file and append it to the - # [:additionalFiles] array for the problem management API request body. - # - # @param [Pathname] pathname The pathname of the file. - # @param [String] content The content of the file. - def extract_supporting_file(filename, content) - supporting_file_object = default_codaveri_data_file_template - - supporting_file_object[:type] = 'internal' # 'external' s3 upload not yet implemented by codaveri - supporting_file_object[:path] = filename.to_s - if content.force_encoding('UTF-8').valid_encoding? - supporting_file_object[:content] = content - supporting_file_object[:encoding] = 'utf8' - else - supporting_file_object[:content] = Base64.strict_encode64(content) - supporting_file_object[:encoding] = 'base64' - end - - @data_files.append(supporting_file_object) - end - # Extracts test cases from 'autograde.py' and append all the test cases to the # [:resources][0][:exprTestcases] array for the problem management API request body. def extract_test_cases diff --git a/app/services/course/assessment/question/programming_codaveri/r/r_package_service.rb b/app/services/course/assessment/question/programming_codaveri/r/r_package_service.rb index 999a0fc737c..fc41378b9d5 100644 --- a/app/services/course/assessment/question/programming_codaveri/r/r_package_service.rb +++ b/app/services/course/assessment/question/programming_codaveri/r/r_package_service.rb @@ -97,27 +97,6 @@ def extract_supporting_solution_files end end - # Extracts filename and content of a data file and append it to the - # [:additionalFiles] array for the problem management API request body. - # - # @param [Pathname] pathname The pathname of the file. - # @param [String] content The content of the file. - def extract_supporting_file(filename, content) - supporting_file_object = default_codaveri_data_file_template - - supporting_file_object[:type] = 'internal' # 'external' s3 upload not yet implemented by codaveri - supporting_file_object[:path] = filename.to_s - if content.force_encoding('UTF-8').valid_encoding? - supporting_file_object[:content] = content - supporting_file_object[:encoding] = 'utf8' - else - supporting_file_object[:content] = Base64.strict_encode64(content) - supporting_file_object[:encoding] = 'base64' - end - - @data_files.append(supporting_file_object) - end - # Extracts test cases from the built dummy reports and append all the test cases to the # [:IOTestcases] array for the problem management API request body. def extract_test_cases # rubocop:disable Metrics/AbcSize diff --git a/app/services/course/assessment/question/programming_codaveri/rust/rust_package_service.rb b/app/services/course/assessment/question/programming_codaveri/rust/rust_package_service.rb index 4824130cfee..eae0a8cf35c 100644 --- a/app/services/course/assessment/question/programming_codaveri/rust/rust_package_service.rb +++ b/app/services/course/assessment/question/programming_codaveri/rust/rust_package_service.rb @@ -97,27 +97,6 @@ def extract_supporting_solution_files end end - # Extracts filename and content of a data file and append it to the - # [:additionalFiles] array for the problem management API request body. - # - # @param [Pathname] pathname The pathname of the file. - # @param [String] content The content of the file. - def extract_supporting_file(filename, content) - supporting_file_object = default_codaveri_data_file_template - - supporting_file_object[:type] = 'internal' # 'external' s3 upload not yet implemented by codaveri - supporting_file_object[:path] = filename.to_s - if content.force_encoding('UTF-8').valid_encoding? - supporting_file_object[:content] = content - supporting_file_object[:encoding] = 'utf8' - else - supporting_file_object[:content] = Base64.strict_encode64(content) - supporting_file_object[:encoding] = 'base64' - end - - @data_files.append(supporting_file_object) - end - # Extracts test cases from the built dummy reports and append all the test cases to the # [:IOTestcases] array for the problem management API request body. def extract_test_cases # rubocop:disable Metrics/AbcSize diff --git a/app/services/course/assessment/question/programming_codaveri/type_script/type_script_package_service.rb b/app/services/course/assessment/question/programming_codaveri/type_script/type_script_package_service.rb index 8b81504fd73..0a54601c220 100644 --- a/app/services/course/assessment/question/programming_codaveri/type_script/type_script_package_service.rb +++ b/app/services/course/assessment/question/programming_codaveri/type_script/type_script_package_service.rb @@ -97,27 +97,6 @@ def extract_supporting_solution_files end end - # Extracts filename and content of a data file and append it to the - # [:additionalFiles] array for the problem management API request body. - # - # @param [Pathname] pathname The pathname of the file. - # @param [String] content The content of the file. - def extract_supporting_file(filename, content) - supporting_file_object = default_codaveri_data_file_template - - supporting_file_object[:type] = 'internal' # 'external' s3 upload not yet implemented by codaveri - supporting_file_object[:path] = filename.to_s - if content.force_encoding('UTF-8').valid_encoding? - supporting_file_object[:content] = content - supporting_file_object[:encoding] = 'utf8' - else - supporting_file_object[:content] = Base64.strict_encode64(content) - supporting_file_object[:encoding] = 'base64' - end - - @data_files.append(supporting_file_object) - end - # Extracts test cases from the built dummy reports and append all the test cases to the # [:IOTestcases] array for the problem management API request body. def extract_test_cases # rubocop:disable Metrics/AbcSize diff --git a/spec/fixtures/course/programming_question_template_codaveri_empty_data_file.zip b/spec/fixtures/course/programming_question_template_codaveri_empty_data_file.zip new file mode 100644 index 0000000000000000000000000000000000000000..4b06fa054c4d621c8a7d2a802aec26abe399dd83 GIT binary patch literal 4041 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