app/services/course/assessment/question/programming/cpp/cpp_package_service.rb
# frozen_string_literal: true
class Course::Assessment::Question::Programming::Cpp::CppPackageService < \
Course::Assessment::Question::Programming::LanguagePackageService
def submission_templates
[
{
filename: 'template.c',
content: @test_params[:submission] || ''
}
]
end
def generate_package(old_attachment)
return nil if @test_params.blank?
@tmp_dir = Dir.mktmpdir
@old_meta = old_attachment.present? ? extract_meta(old_attachment, nil) : nil
data_files_to_keep = old_attachment.present? ? find_data_files_to_keep(old_attachment) : []
@meta = generate_meta(data_files_to_keep)
return nil if @meta == @old_meta
@attachment = generate_zip_file(data_files_to_keep)
FileUtils.remove_entry @tmp_dir if @tmp_dir.present?
@attachment
end
def extract_meta(attachment, template_files)
return @meta if @meta.present? && attachment == @attachment
# attachment will be nil if the question is not autograded, in that case the meta data will be
# generated from the template files in the database.
return generate_non_autograded_meta(template_files) if attachment.nil?
extract_autograded_meta(attachment)
end
private
def extract_autograded_meta(attachment)
attachment.open(binmode: true) do |temporary_file|
package = Course::Assessment::ProgrammingPackage.new(temporary_file)
meta = package.meta_file
@old_meta = meta.present? ? JSON.parse(meta) : nil
ensure
next unless package
temporary_file.close
end
end
def generate_non_autograded_meta(template_files)
meta = default_meta
return meta if template_files.blank?
# For cpp editor, there is only a single submission template file.
meta[:submission] = template_files.first.content
meta.as_json
end
def extract_from_package(package, new_data_filenames, data_files_to_delete)
data_files_to_keep = []
if @old_meta.present?
package.unzip_file @tmp_dir
@old_meta['data_files'].try(:each) do |file|
next if data_files_to_delete.try(:include?, (file['filename']))
# new files overrides old ones
next if new_data_filenames.include?(file['filename'])
data_files_to_keep.append(File.new(File.join(@tmp_dir, file['filename'])))
end
end
data_files_to_keep
end
def find_data_files_to_keep(attachment)
new_filenames = (@test_params[:data_files] || []).reject(&:nil?).map(&:original_filename)
attachment.open(binmode: true) do |temporary_file|
package = Course::Assessment::ProgrammingPackage.new(temporary_file)
return extract_from_package(package, new_filenames, @test_params[:data_files_to_delete])
ensure
next unless package
temporary_file.close
end
end
def generate_zip_file(data_files_to_keep)
tmp = Tempfile.new(['package', '.zip'])
autograde_include_path = get_file_path('cpp_autograde_include.cc')
autograde_pre_path = get_file_path('cpp_autograde_pre.cc')
autograde_post_path = get_file_path('cpp_autograde_post.cc')
makefile_path = get_file_path('cpp_makefile')
Zip::OutputStream.open(tmp.path) do |zip|
# Create solution directory with template file
zip.put_next_entry 'solution/'
zip.put_next_entry 'solution/template.c'
zip.print @test_params[:solution]
zip.print "\n"
# Create submission directory with template file
zip.put_next_entry 'submission/'
zip.put_next_entry 'submission/template.c'
zip.print @test_params[:submission]
zip.print "\n"
# Create tests directory with prepend, append and autograde files
zip.put_next_entry 'tests/'
zip.put_next_entry 'tests/append.cc'
zip.print "\n"
zip.print @test_params[:append]
zip.print "\n"
zip.put_next_entry 'tests/prepend.cc'
zip.print "\n"
zip.print File.read(autograde_include_path)
zip.print "\n"
zip.print @test_params[:prepend]
zip.print "\n"
zip.print File.read(autograde_pre_path)
zip.print "\n"
zip.put_next_entry 'tests/autograde.cc'
[:public, :private, :evaluation].each do |test_type|
zip_test_files(test_type, zip)
end
zip.print "\n"
zip.print File.read(autograde_post_path)
# Creates Makefile
zip.put_next_entry 'Makefile'
zip.print File.read(makefile_path)
zip.put_next_entry '.meta'
zip.print @meta.to_json
end
Zip::File.open(tmp.path) do |zip|
@test_params[:data_files].try(:each) do |file|
next if file.nil?
zip.add(file.original_filename, file.tempfile.path)
end
data_files_to_keep.each do |file|
zip.add(File.basename(file.path), file.path)
end
end
tmp
end
# Retrieves the absolute path of the file specified
#
# @param [String] filename The filename of the file to get the path of
def get_file_path(filename)
File.join(__dir__, filename).freeze
end
def zip_test_files(test_type, zip) # rubocop:disable Metrics/AbcSize
tests = @test_params[:test_cases]
tests[test_type]&.each&.with_index(1) do |test, index|
# String types should be displayed with quotes, other types will be converted to string
# with the str method.
expected = string?(test[:expected]) ? test[:expected].inspect : (test[:expected]).to_s
hint = test[:hint].blank? ? String(nil) : "RecordProperty(\"hint\", #{test[:hint].inspect})"
test_fn = <<-CPP
TEST(Autograder, test_#{test_type}_#{format('%<index>02i', index: index)}) {
RecordProperty("expression", #{test[:expression].inspect});
custom_evaluation(#{test[:expected]}, #{test[:expression]});
#{hint};
}
CPP
zip.print test_fn
end
end
def get_data_files_meta(data_files_to_keep, new_data_files)
data_files = []
new_data_files.each do |file|
sha256 = Digest::SHA256.file(file.tempfile).hexdigest
data_files.append(filename: file.original_filename, size: file.tempfile.size, hash: sha256)
end
data_files_to_keep.each do |file|
sha256 = Digest::SHA256.file(file).hexdigest
data_files.append(filename: File.basename(file.path), size: file.size, hash: sha256)
end
data_files.sort_by { |file| file[:filename].downcase }
end
def generate_meta(data_files_to_keep)
meta = default_meta
[:submission, :solution, :prepend, :append].each { |field| meta[field] = @test_params[field] }
new_data_files = (@test_params[:data_files] || []).reject(&:nil?)
meta[:data_files] = get_data_files_meta(data_files_to_keep, new_data_files)
[:public, :private, :evaluation].each do |test_type|
meta[:test_cases][test_type] = @test_params[:test_cases][test_type] || []
end
meta.as_json
end
end