|
| 1 | +# frozen_string_literal: true |
| 2 | +class Course::Assessment::Question::Programming::CSharp::CSharpPackageService < # rubocop:disable Metrics/ClassLength |
| 3 | + Course::Assessment::Question::Programming::LanguagePackageService |
| 4 | + def submission_templates |
| 5 | + [ |
| 6 | + { |
| 7 | + filename: 'template.cs', |
| 8 | + content: @test_params[:submission] || '' |
| 9 | + } |
| 10 | + ] |
| 11 | + end |
| 12 | + |
| 13 | + def generate_package(old_attachment) |
| 14 | + return nil if @test_params.blank? |
| 15 | + |
| 16 | + @tmp_dir = Dir.mktmpdir |
| 17 | + @old_meta = old_attachment.present? ? extract_meta(old_attachment, nil) : nil |
| 18 | + data_files_to_keep = old_attachment.present? ? find_data_files_to_keep(old_attachment) : [] |
| 19 | + @meta = generate_meta(data_files_to_keep) |
| 20 | + |
| 21 | + return nil if @meta == @old_meta |
| 22 | + |
| 23 | + @attachment = generate_zip_file(data_files_to_keep) |
| 24 | + FileUtils.remove_entry @tmp_dir if @tmp_dir.present? |
| 25 | + @attachment |
| 26 | + end |
| 27 | + |
| 28 | + def extract_meta(attachment, template_files) |
| 29 | + return @meta if @meta.present? && attachment == @attachment |
| 30 | + |
| 31 | + # attachment will be nil if the question is not autograded, in that case the meta data will be |
| 32 | + # generated from the template files in the database. |
| 33 | + return generate_non_autograded_meta(template_files) if attachment.nil? |
| 34 | + |
| 35 | + extract_autograded_meta(attachment) |
| 36 | + end |
| 37 | + |
| 38 | + private |
| 39 | + |
| 40 | + def extract_autograded_meta(attachment) |
| 41 | + attachment.open(binmode: true) do |temporary_file| |
| 42 | + package = Course::Assessment::ProgrammingPackage.new(temporary_file) |
| 43 | + meta = package.meta_file |
| 44 | + @old_meta = meta.present? ? JSON.parse(meta) : nil |
| 45 | + ensure |
| 46 | + next unless package |
| 47 | + |
| 48 | + temporary_file.close |
| 49 | + end |
| 50 | + end |
| 51 | + |
| 52 | + def generate_non_autograded_meta(template_files) |
| 53 | + meta = default_meta |
| 54 | + |
| 55 | + return meta if template_files.blank? |
| 56 | + |
| 57 | + # For python editor, there is only a single submission template file. |
| 58 | + meta[:submission] = template_files.first.content |
| 59 | + |
| 60 | + meta.as_json |
| 61 | + end |
| 62 | + |
| 63 | + def extract_from_package(package, new_data_filenames, data_files_to_delete) |
| 64 | + data_files_to_keep = [] |
| 65 | + |
| 66 | + if @old_meta.present? |
| 67 | + package.unzip_file @tmp_dir |
| 68 | + |
| 69 | + @old_meta['data_files']&.each do |file| |
| 70 | + next if data_files_to_delete.try(:include?, file['filename']) |
| 71 | + # new files overrides old ones |
| 72 | + next if new_data_filenames.include?(file['filename']) |
| 73 | + |
| 74 | + data_files_to_keep.append(File.new(File.join(@tmp_dir, file['filename']))) |
| 75 | + end |
| 76 | + end |
| 77 | + |
| 78 | + data_files_to_keep |
| 79 | + end |
| 80 | + |
| 81 | + def find_data_files_to_keep(attachment) |
| 82 | + new_filenames = (@test_params[:data_files] || []).reject(&:nil?).map(&:original_filename) |
| 83 | + |
| 84 | + attachment.open(binmode: true) do |temporary_file| |
| 85 | + package = Course::Assessment::ProgrammingPackage.new(temporary_file) |
| 86 | + return extract_from_package(package, new_filenames, @test_params[:data_files_to_delete]) |
| 87 | + ensure |
| 88 | + next unless package |
| 89 | + |
| 90 | + temporary_file.close |
| 91 | + end |
| 92 | + end |
| 93 | + |
| 94 | + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength |
| 95 | + def generate_zip_file(data_files_to_keep) |
| 96 | + tmp = Tempfile.new(['package', '.zip']) |
| 97 | + makefile_path = get_file_path('c_sharp_makefile') |
| 98 | + |
| 99 | + Zip::OutputStream.open(tmp.path) do |zip| |
| 100 | + # Create solution directory with template file |
| 101 | + zip.put_next_entry 'solution/' |
| 102 | + zip.put_next_entry 'solution/template.cs' |
| 103 | + zip.print @test_params[:solution] |
| 104 | + zip.print "\n" |
| 105 | + |
| 106 | + # Create submission directory with template file |
| 107 | + zip.put_next_entry 'submission/' |
| 108 | + zip.put_next_entry 'submission/template.cs' |
| 109 | + zip.print @test_params[:submission] |
| 110 | + zip.print "\n" |
| 111 | + |
| 112 | + # Create tests directory with prepend, append and autograde files |
| 113 | + zip.put_next_entry 'tests/' |
| 114 | + zip.put_next_entry 'tests/append.cs' |
| 115 | + zip.print "\n" |
| 116 | + zip.print @test_params[:append] |
| 117 | + zip.print "\n" |
| 118 | + |
| 119 | + zip.put_next_entry 'tests/prepend.cs' |
| 120 | + zip.print @test_params[:prepend] |
| 121 | + zip.print "\n" |
| 122 | + |
| 123 | + [:public, :private, :evaluation].each do |test_type| |
| 124 | + zip_test_files(test_type, zip) |
| 125 | + end |
| 126 | + |
| 127 | + # Creates Makefile |
| 128 | + zip.put_next_entry 'Makefile' |
| 129 | + zip.print File.read(makefile_path) |
| 130 | + |
| 131 | + zip.put_next_entry '.meta' |
| 132 | + zip.print @meta.to_json |
| 133 | + end |
| 134 | + |
| 135 | + Zip::File.open(tmp.path) do |zip| |
| 136 | + @test_params[:data_files]&.each do |file| |
| 137 | + next if file.nil? |
| 138 | + |
| 139 | + zip.add(file.original_filename, file.tempfile.path) |
| 140 | + end |
| 141 | + |
| 142 | + data_files_to_keep.each do |file| |
| 143 | + zip.add(File.basename(file.path), file.path) |
| 144 | + end |
| 145 | + end |
| 146 | + |
| 147 | + tmp |
| 148 | + end |
| 149 | + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength |
| 150 | + |
| 151 | + # Retrieves the absolute path of the file specified |
| 152 | + # |
| 153 | + # @param [String] filename The filename of the file to get the path of |
| 154 | + def get_file_path(filename) |
| 155 | + File.join(__dir__, filename).freeze |
| 156 | + end |
| 157 | + |
| 158 | + def zip_test_files(test_type, zip) |
| 159 | + # Create a dummy report to pass test cases to DB/Codaveri |
| 160 | + tests = @test_params[:test_cases] |
| 161 | + return unless tests[test_type]&.count&.> 0 |
| 162 | + |
| 163 | + zip.put_next_entry "report-#{test_type}.xml" |
| 164 | + zip.print build_dummy_report(test_type, tests[test_type]) |
| 165 | + end |
| 166 | + |
| 167 | + def build_dummy_report(test_type, test_cases) |
| 168 | + Course::Assessment::ProgrammingTestCaseReportBuilder.build_dummy_report(test_type, test_cases, '.cs') |
| 169 | + end |
| 170 | + |
| 171 | + def get_data_files_meta(data_files_to_keep, new_data_files) |
| 172 | + data_files = [] |
| 173 | + |
| 174 | + new_data_files.each do |file| |
| 175 | + sha256 = Digest::SHA256.file(file.tempfile).hexdigest |
| 176 | + data_files.append(filename: file.original_filename, size: file.tempfile.size, hash: sha256) |
| 177 | + end |
| 178 | + |
| 179 | + data_files_to_keep.each do |file| |
| 180 | + sha256 = Digest::SHA256.file(file).hexdigest |
| 181 | + data_files.append(filename: File.basename(file.path), size: file.size, hash: sha256) |
| 182 | + end |
| 183 | + |
| 184 | + data_files.sort_by { |file| file[:filename].downcase } |
| 185 | + end |
| 186 | + |
| 187 | + def generate_meta(data_files_to_keep) |
| 188 | + meta = default_meta |
| 189 | + |
| 190 | + [:submission, :solution, :prepend, :append].each { |field| meta[field] = @test_params[field] } |
| 191 | + |
| 192 | + new_data_files = (@test_params[:data_files] || []).reject(&:nil?) |
| 193 | + meta[:data_files] = get_data_files_meta(data_files_to_keep, new_data_files) |
| 194 | + |
| 195 | + [:public, :private, :evaluation].each do |test_type| |
| 196 | + meta[:test_cases][test_type] = @test_params[:test_cases][test_type] || [] |
| 197 | + end |
| 198 | + |
| 199 | + meta.as_json |
| 200 | + end |
| 201 | +end |
0 commit comments