Skip to content

Commit 7f70083

Browse files
ncduy0303adi-herwana-nus
authored andcommitted
feat(programming-language-support): add c# 5.0, js 22.16, go 1.16, rust 1.68, ts 5.8
1 parent 497960f commit 7f70083

File tree

45 files changed

+2599
-35
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+2599
-35
lines changed

Gemfile.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ GIT
2222

2323
GIT
2424
remote: https://github.com/Coursemology/polyglot
25-
revision: 7cf1c55b530fd50950144d24524b0647f2d98f76
25+
revision: bf38cf4d4e8a4ee732c604fc27db2263257a23ec
2626
specs:
27-
coursemology-polyglot (0.4.1)
27+
coursemology-polyglot (0.4.2)
2828
activesupport (>= 4.2)
2929

3030
GIT

app/controllers/concerns/codaveri_language_concern.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ def programming_language_map
2929
Coursemology::Polyglot::Language::Java::Java21 => {
3030
language: 'java',
3131
version: '21.0'
32+
},
33+
Coursemology::Polyglot::Language::CSharp::CSharp5Point0 => {
34+
language: 'csharp',
35+
version: '5.0.201'
3236
}
3337
}
3438
end

app/services/course/assessment/question/codaveri_problem_generation_service.rb

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,27 @@
11
# frozen_string_literal: true
2-
class Course::Assessment::Question::CodaveriProblemGenerationService
2+
class Course::Assessment::Question::CodaveriProblemGenerationService # rubocop:disable Metrics/ClassLength
33
POLL_INTERVAL_SECONDS = 2
44
MAX_POLL_RETRIES = 1000
55

6+
LANGUAGE_FILENAME_MAPPING = {
7+
'python' => 'main.py',
8+
'r' => 'main.R',
9+
'javascript' => 'main.js',
10+
'csharp' => 'main.cs',
11+
'go' => 'main.go',
12+
'rust' => 'main.rs',
13+
'typescript' => 'main.ts'
14+
}.freeze
15+
16+
LANGUAGE_TESTCASE_TYPE_MAPPING = {
17+
'r' => 'IO',
18+
'javascript' => 'IO',
19+
'csharp' => 'IO',
20+
'go' => 'IO',
21+
'rust' => 'IO',
22+
'typescript' => 'IO'
23+
}.freeze
24+
625
def codaveri_generate_problem
726
response_status, response_body, generation_id = send_problem_generation_request
827
poll_count = 0
@@ -23,7 +42,7 @@ def codaveri_generate_problem
2342

2443
private
2544

26-
def initialize(assessment, params, language, version)
45+
def initialize(assessment, params, language, version) # rubocop:disable Metrics/AbcSize
2746
custom_prompt = params[:custom_prompt].to_s || ''
2847
@payload = {
2948
userId: assessment.creator_id.to_s,
@@ -71,15 +90,15 @@ def initialize(assessment, params, language, version)
7190
end
7291

7392
def generate_payload_file_name(codaveri_language, file_content)
74-
return 'main.py' if codaveri_language == 'python'
75-
return 'main.R' if codaveri_language == 'r'
93+
return LANGUAGE_FILENAME_MAPPING[codaveri_language] if LANGUAGE_FILENAME_MAPPING.key?(codaveri_language)
7694

7795
match = file_content&.match(/\bclass\s+(\w+)\s*\{/)
7896
match ? "#{match[1]}.java" : 'Main.java'
7997
end
8098

8199
def generate_payload_testcases_type(codaveri_language)
82-
codaveri_language == 'r' ? 'IO' : 'expression'
100+
# New languages supported by Codaveri only allow IO test cases.
101+
LANGUAGE_TESTCASE_TYPE_MAPPING.fetch(codaveri_language, 'expression')
83102
end
84103

85104
def generate_payload_io_test_case(test_case, visibility, index)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
prepare:
2+
3+
compile: submission/template.cs tests/prepend.cs tests/append.cs
4+
cat tests/prepend.cs submission/template.cs tests/append.cs > answer.cs
5+
6+
public:
7+
echo "Not Implemented"
8+
9+
private:
10+
echo "Not Implemented"
11+
12+
evaluation:
13+
echo "Not Implemented"
14+
15+
solution: solution.cs
16+
echo "Not Implemented"
17+
18+
solution.cs: solution/template.cs tests/prepend.cs tests/append.cs
19+
cat tests/prepend.cs solution/template.cs tests/append.cs > solution.cs
20+
21+
clean:
22+
rm -f answer.cs
23+
rm -f report.xml
24+
rm -f solution.cs
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
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
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
prepare:
2+
3+
compile: submission/template.go tests/prepend.go tests/append.go
4+
cat tests/prepend.go submission/template.go tests/append.go > answer.go
5+
6+
public:
7+
echo "Not Implemented"
8+
9+
private:
10+
echo "Not Implemented"
11+
12+
evaluation:
13+
echo "Not Implemented"
14+
15+
solution: solution.go
16+
echo "Not Implemented"
17+
18+
solution.go: solution/template.go tests/prepend.go tests/append.go
19+
cat tests/prepend.go solution/template.go tests/append.go > solution.go
20+
21+
clean:
22+
rm -f answer.go
23+
rm -f report.xml
24+
rm -f solution.go

0 commit comments

Comments
 (0)