Skip to content

Commit 5c7d6c5

Browse files
fix(JavaProgrammingTestCaseReport): correctly escape/unescape CDATA attributes
1 parent 1513f15 commit 5c7d6c5

3 files changed

Lines changed: 338 additions & 3 deletions

File tree

lib/autoload/course/assessment/java/java_programming_test_case_report.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -221,8 +221,8 @@ def messages
221221
# @return [String]
222222
def get_test_case_metadata(attribute_name)
223223
attribute = @test_case.search("./attributes/attribute[@name=#{attribute_name.inspect}]")
224-
if attribute.present? && attribute.children[1]
225-
attribute.children[1].text
224+
if attribute.present? && attribute.children[1] && attribute.children[1].text.present?
225+
attribute.children[1].text.gsub('
', "\n").gsub('&', '&')
226226
else
227227
''
228228
end
@@ -234,7 +234,7 @@ def get_test_case_metadata(attribute_name)
234234
# @param [String] report The report XML to parse.
235235
# rubocop: disable Lint/MissingSuper
236236
def initialize(report)
237-
report = report.gsub("\n", '
') if report
237+
report = report.gsub('&', '&').gsub("\n", '
') if report
238238
@report = Nokogiri::XML::DocumentFragment.parse(report)
239239
end
240240
# rubocop: enable Lint/MissingSuper
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<testng-results ignored="0" total="6" passed="4" failed="2" skipped="0">
3+
<reporter-output>
4+
</reporter-output>
5+
<suite started-at="2026-04-23T09:06:55Z" name="AllTests" finished-at="2026-04-23T09:06:56Z" duration-ms="396">
6+
<groups>
7+
<group name="public">
8+
<method signature="Autograder.test_public_01()[pri:0, instance:Autograder@6c1a5b54]" name="test_public_01" class="Autograder"/>
9+
<method signature="Autograder.test_public_02()[pri:0, instance:Autograder@6c1a5b54]" name="test_public_02" class="Autograder"/>
10+
<method signature="Autograder.test_public_03()[pri:0, instance:Autograder@6c1a5b54]" name="test_public_03" class="Autograder"/>
11+
<method signature="Autograder.test_public_04()[pri:0, instance:Autograder@6c1a5b54]" name="test_public_04" class="Autograder"/>
12+
<method signature="Autograder.test_public_05()[pri:0, instance:Autograder@6c1a5b54]" name="test_public_05" class="Autograder"/>
13+
<method signature="Autograder.test_public_06()[pri:0, instance:Autograder@6c1a5b54]" name="test_public_06" class="Autograder"/>
14+
</group> <!-- public -->
15+
</groups>
16+
<test started-at="2026-04-23T09:06:55Z" name="tests" finished-at="2026-04-23T09:06:56Z" duration-ms="396">
17+
<class name="Autograder">
18+
<test-method signature="test_public_01()[pri:0, instance:Autograder@6c1a5b54]" started-at="2026-04-23T09:06:56Z" name="test_public_01" finished-at="2026-04-23T09:06:56Z" duration-ms="30" status="PASS">
19+
<reporter-output>
20+
</reporter-output>
21+
<attributes>
22+
<attribute name="output">
23+
<![CDATA[aaaa]]>
24+
</attribute> <!-- output -->
25+
<attribute name="expression">
26+
<![CDATA[EmojiChecker.containsEmoji("aaaa")]]>
27+
</attribute> <!-- expression -->
28+
<attribute name="expected">
29+
<![CDATA[aaaa]]>
30+
</attribute> <!-- expected -->
31+
<attribute name="hint">
32+
<![CDATA[1]]>
33+
</attribute> <!-- hint -->
34+
</attributes>
35+
</test-method> <!-- test_public_01 -->
36+
<test-method signature="test_public_02()[pri:0, instance:Autograder@6c1a5b54]" started-at="2026-04-23T09:06:56Z" name="test_public_02" finished-at="2026-04-23T09:06:56Z" duration-ms="1" status="PASS">
37+
<reporter-output>
38+
</reporter-output>
39+
<attributes>
40+
<attribute name="output">
41+
<![CDATA[Hello :) ]]>
42+
</attribute> <!-- output -->
43+
<attribute name="expression">
44+
<![CDATA[EmojiChecker.containsEmoji("Hello 🙂 ")]]>
45+
</attribute> <!-- expression -->
46+
<attribute name="expected">
47+
<![CDATA[Hello :) ]]>
48+
</attribute> <!-- expected -->
49+
<attribute name="hint">
50+
<![CDATA[2]]>
51+
</attribute> <!-- hint -->
52+
</attributes>
53+
</test-method> <!-- test_public_02 -->
54+
<test-method signature="test_public_03()[pri:0, instance:Autograder@6c1a5b54]" started-at="2026-04-23T09:06:56Z" name="test_public_03" finished-at="2026-04-23T09:06:56Z" duration-ms="5" status="FAIL">
55+
<exception class="java.lang.AssertionError">
56+
<message>
57+
<![CDATA[expected [ 8-)
58+
coolman] but found [ 😎
59+
coolman]]]>
60+
</message>
61+
<full-stacktrace>
62+
<![CDATA[java.lang.AssertionError: expected [ 8-)
63+
coolman] but found [ 😎
64+
coolman]
65+
at org.testng.Assert.fail(Assert.java:93)
66+
at org.testng.Assert.failNotEquals(Assert.java:512)
67+
at org.testng.Assert.assertEqualsImpl(Assert.java:134)
68+
at org.testng.Assert.assertEquals(Assert.java:115)
69+
at org.testng.Assert.assertEquals(Assert.java:178)
70+
at Autograder.expectEquals(Unknown Source)
71+
at Autograder.test_public_03(Unknown Source)
72+
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
73+
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
74+
at org.testng.internal.MethodInvocationHelper.invokeMethod(MethodInvocationHelper.java:108)
75+
at org.testng.internal.Invoker.invokeMethod(Invoker.java:661)
76+
at org.testng.internal.Invoker.invokeTestMethod(Invoker.java:869)
77+
at org.testng.internal.Invoker.invokeTestMethods(Invoker.java:1193)
78+
at org.testng.internal.TestMethodWorker.invokeTestMethods(TestMethodWorker.java:126)
79+
at org.testng.internal.TestMethodWorker.run(TestMethodWorker.java:109)
80+
at org.testng.TestRunner.privateRun(TestRunner.java:744)
81+
at org.testng.TestRunner.run(TestRunner.java:602)
82+
at org.testng.SuiteRunner.runTest(SuiteRunner.java:380)
83+
at org.testng.SuiteRunner.runSequentially(SuiteRunner.java:375)
84+
at org.testng.SuiteRunner.privateRun(SuiteRunner.java:340)
85+
at org.testng.SuiteRunner.run(SuiteRunner.java:289)
86+
at org.testng.SuiteRunnerWorker.runSuite(SuiteRunnerWorker.java:52)
87+
at org.testng.SuiteRunnerWorker.run(SuiteRunnerWorker.java:86)
88+
at org.testng.TestNG.runSuitesSequentially(TestNG.java:1301)
89+
at org.testng.TestNG.runSuitesLocally(TestNG.java:1226)
90+
at org.testng.TestNG.runSuites(TestNG.java:1144)
91+
at org.testng.TestNG.run(TestNG.java:1115)
92+
at RunTests.main(Unknown Source)
93+
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
94+
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
95+
at org.apache.tools.ant.taskdefs.ExecuteJava.run(ExecuteJava.java:218)
96+
at org.apache.tools.ant.taskdefs.ExecuteJava.execute(ExecuteJava.java:155)
97+
at org.apache.tools.ant.taskdefs.Java.run(Java.java:891)
98+
at org.apache.tools.ant.taskdefs.Java.executeJava(Java.java:231)
99+
at org.apache.tools.ant.taskdefs.Java.executeJava(Java.java:135)
100+
at org.apache.tools.ant.taskdefs.Java.execute(Java.java:108)
101+
at org.apache.tools.ant.UnknownElement.execute(UnknownElement.java:299)
102+
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
103+
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
104+
at org.apache.tools.ant.dispatch.DispatchUtils.execute(DispatchUtils.java:99)
105+
at org.apache.tools.ant.Task.perform(Task.java:350)
106+
at org.apache.tools.ant.Target.execute(Target.java:449)
107+
at org.apache.tools.ant.Target.performTasks(Target.java:470)
108+
at org.apache.tools.ant.Project.executeSortedTargets(Project.java:1401)
109+
at org.apache.tools.ant.Project.executeTarget(Project.java:1374)
110+
at org.apache.tools.ant.helper.DefaultExecutor.executeTargets(DefaultExecutor.java:41)
111+
at org.apache.tools.ant.Project.executeTargets(Project.java:1264)
112+
at org.apache.tools.ant.Main.runBuild(Main.java:818)
113+
at org.apache.tools.ant.Main.startAnt(Main.java:223)
114+
at org.apache.tools.ant.launch.Launcher.run(Launcher.java:284)
115+
at org.apache.tools.ant.launch.Launcher.main(Launcher.java:101)
116+
]]>
117+
</full-stacktrace>
118+
</exception> <!-- java.lang.AssertionError -->
119+
<reporter-output>
120+
</reporter-output>
121+
<attributes>
122+
<attribute name="output">
123+
<![CDATA[ 😎
124+
coolman]]>
125+
</attribute> <!-- output -->
126+
<attribute name="expression">
127+
<![CDATA[EmojiChecker.containsEmoji(" 😎 \ncoolman")]]>
128+
</attribute> <!-- expression -->
129+
<attribute name="expected">
130+
<![CDATA[ 8-)
131+
coolman]]>
132+
</attribute> <!-- expected -->
133+
<attribute name="hint">
134+
<![CDATA[3]]>
135+
</attribute> <!-- hint -->
136+
</attributes>
137+
</test-method> <!-- test_public_03 -->
138+
<test-method signature="test_public_04()[pri:0, instance:Autograder@6c1a5b54]" started-at="2026-04-23T09:06:56Z" name="test_public_04" finished-at="2026-04-23T09:06:56Z" duration-ms="2" status="FAIL">
139+
<exception class="java.lang.AssertionError">
140+
<message>
141+
<![CDATA[expected [I <3 Java] but found [I ❤️ Java]]]>
142+
</message>
143+
<full-stacktrace>
144+
<![CDATA[java.lang.AssertionError: expected [I <3 Java] but found [I ❤️ Java]
145+
at org.testng.Assert.fail(Assert.java:93)
146+
at org.testng.Assert.failNotEquals(Assert.java:512)
147+
at org.testng.Assert.assertEqualsImpl(Assert.java:134)
148+
at org.testng.Assert.assertEquals(Assert.java:115)
149+
at org.testng.Assert.assertEquals(Assert.java:178)
150+
at Autograder.expectEquals(Unknown Source)
151+
at Autograder.test_public_04(Unknown Source)
152+
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
153+
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
154+
at org.testng.internal.MethodInvocationHelper.invokeMethod(MethodInvocationHelper.java:108)
155+
at org.testng.internal.Invoker.invokeMethod(Invoker.java:661)
156+
at org.testng.internal.Invoker.invokeTestMethod(Invoker.java:869)
157+
at org.testng.internal.Invoker.invokeTestMethods(Invoker.java:1193)
158+
at org.testng.internal.TestMethodWorker.invokeTestMethods(TestMethodWorker.java:126)
159+
at org.testng.internal.TestMethodWorker.run(TestMethodWorker.java:109)
160+
at org.testng.TestRunner.privateRun(TestRunner.java:744)
161+
at org.testng.TestRunner.run(TestRunner.java:602)
162+
at org.testng.SuiteRunner.runTest(SuiteRunner.java:380)
163+
at org.testng.SuiteRunner.runSequentially(SuiteRunner.java:375)
164+
at org.testng.SuiteRunner.privateRun(SuiteRunner.java:340)
165+
at org.testng.SuiteRunner.run(SuiteRunner.java:289)
166+
at org.testng.SuiteRunnerWorker.runSuite(SuiteRunnerWorker.java:52)
167+
at org.testng.SuiteRunnerWorker.run(SuiteRunnerWorker.java:86)
168+
at org.testng.TestNG.runSuitesSequentially(TestNG.java:1301)
169+
at org.testng.TestNG.runSuitesLocally(TestNG.java:1226)
170+
at org.testng.TestNG.runSuites(TestNG.java:1144)
171+
at org.testng.TestNG.run(TestNG.java:1115)
172+
at RunTests.main(Unknown Source)
173+
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
174+
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
175+
at org.apache.tools.ant.taskdefs.ExecuteJava.run(ExecuteJava.java:218)
176+
at org.apache.tools.ant.taskdefs.ExecuteJava.execute(ExecuteJava.java:155)
177+
at org.apache.tools.ant.taskdefs.Java.run(Java.java:891)
178+
at org.apache.tools.ant.taskdefs.Java.executeJava(Java.java:231)
179+
at org.apache.tools.ant.taskdefs.Java.executeJava(Java.java:135)
180+
at org.apache.tools.ant.taskdefs.Java.execute(Java.java:108)
181+
at org.apache.tools.ant.UnknownElement.execute(UnknownElement.java:299)
182+
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
183+
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
184+
at org.apache.tools.ant.dispatch.DispatchUtils.execute(DispatchUtils.java:99)
185+
at org.apache.tools.ant.Task.perform(Task.java:350)
186+
at org.apache.tools.ant.Target.execute(Target.java:449)
187+
at org.apache.tools.ant.Target.performTasks(Target.java:470)
188+
at org.apache.tools.ant.Project.executeSortedTargets(Project.java:1401)
189+
at org.apache.tools.ant.Project.executeTarget(Project.java:1374)
190+
at org.apache.tools.ant.helper.DefaultExecutor.executeTargets(DefaultExecutor.java:41)
191+
at org.apache.tools.ant.Project.executeTargets(Project.java:1264)
192+
at org.apache.tools.ant.Main.runBuild(Main.java:818)
193+
at org.apache.tools.ant.Main.startAnt(Main.java:223)
194+
at org.apache.tools.ant.launch.Launcher.run(Launcher.java:284)
195+
at org.apache.tools.ant.launch.Launcher.main(Launcher.java:101)
196+
]]>
197+
</full-stacktrace>
198+
</exception> <!-- java.lang.AssertionError -->
199+
<reporter-output>
200+
</reporter-output>
201+
<attributes>
202+
<attribute name="output">
203+
<![CDATA[I ❤️ Java]]>
204+
</attribute> <!-- output -->
205+
<attribute name="expression">
206+
<![CDATA[EmojiChecker.containsEmoji("I ❤️ Java")]]>
207+
</attribute> <!-- expression -->
208+
<attribute name="expected">
209+
<![CDATA[I <3 Java]]>
210+
</attribute> <!-- expected -->
211+
<attribute name="hint">
212+
<![CDATA[4]]>
213+
</attribute> <!-- hint -->
214+
</attributes>
215+
</test-method> <!-- test_public_04 -->
216+
<test-method signature="test_public_05()[pri:0, instance:Autograder@6c1a5b54]" started-at="2026-04-23T09:06:56Z" name="test_public_05" finished-at="2026-04-23T09:06:56Z" duration-ms="0" status="PASS">
217+
<reporter-output>
218+
</reporter-output>
219+
<attributes>
220+
<attribute name="output">
221+
<![CDATA[fake&#10;real
222+
fake&#10;end]]>
223+
</attribute> <!-- output -->
224+
<attribute name="expression">
225+
<![CDATA[EmojiChecker.containsEmoji("fake&#10;real\nfake&#10;end")]]>
226+
</attribute> <!-- expression -->
227+
<attribute name="expected">
228+
<![CDATA[fake&#10;real
229+
fake&#10;end]]>
230+
</attribute> <!-- expected -->
231+
<attribute name="hint">
232+
<![CDATA[5]]>
233+
</attribute> <!-- hint -->
234+
</attributes>
235+
</test-method> <!-- test_public_05 -->
236+
<test-method signature="test_public_06()[pri:0, instance:Autograder@6c1a5b54]" started-at="2026-04-23T09:06:56Z" name="test_public_06" finished-at="2026-04-23T09:06:56Z" duration-ms="1" status="PASS">
237+
<reporter-output>
238+
</reporter-output>
239+
<attributes>
240+
<attribute name="output">
241+
<![CDATA[hehehe")]]]]><![CDATA[></message>]]>
242+
</attribute> <!-- output -->
243+
<attribute name="expression">
244+
<![CDATA[EmojiChecker.containsEmoji("hehehe\")]]]]><![CDATA[></message>")]]>
245+
</attribute> <!-- expression -->
246+
<attribute name="expected">
247+
<![CDATA[hehehe")]]]]><![CDATA[></message>]]>
248+
</attribute> <!-- expected -->
249+
</attributes>
250+
</test-method> <!-- test_public_06 -->
251+
</class> <!-- Autograder -->
252+
</test> <!-- tests -->
253+
</suite> <!-- AllTests -->
254+
</testng-results>

spec/libraries/course/assessment/java/java_programming_test_case_report_spec.rb

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,4 +218,85 @@
218218
end
219219
end
220220
end
221+
222+
context 'when given a report with newlines and special characters in attributes' do
223+
let(:report_path) do
224+
File.join(Rails.root, 'spec/fixtures/course/programming_java_test_report_newlines.xml')
225+
end
226+
227+
let(:parsed_report) do
228+
Course::Assessment::Java::JavaProgrammingTestCaseReport.new(File.read(report_path))
229+
end
230+
subject { parsed_report }
231+
232+
describe '#test_cases' do
233+
it 'returns all the test cases in the report' do
234+
expect(subject.test_cases.length).to eq(6)
235+
end
236+
end
237+
238+
describe Course::Assessment::Java::JavaProgrammingTestCaseReport::TestCase do
239+
let(:test_cases) { parsed_report.test_cases }
240+
241+
context 'when the attribute value contains a real newline inside CDATA' do
242+
# test_public_03: output/expected span multiple lines in the XML
243+
let(:test_case) { test_cases[2] }
244+
subject { test_case }
245+
246+
describe '#output' do
247+
it 'preserves the newline as a newline character' do
248+
expect(subject.output).to eq(" \u{1F60E} \ncoolman")
249+
end
250+
end
251+
252+
describe '#expected' do
253+
it 'preserves the newline as a newline character' do
254+
expect(subject.expected).to eq(" 8-) \ncoolman")
255+
end
256+
end
257+
end
258+
259+
context 'when the attribute value contains literal &#10; text alongside real newlines' do
260+
# test_public_05: the core bug — &#10; must survive as text, not become a newline
261+
let(:test_case) { test_cases[4] }
262+
subject { test_case }
263+
264+
describe '#output' do
265+
it 'keeps literal &#10; as text and real newlines as newline characters' do
266+
expect(subject.output).to eq("fake&#10;real\nfake&#10;end")
267+
end
268+
end
269+
270+
describe '#expected' do
271+
it 'keeps literal &#10; as text and real newlines as newline characters' do
272+
expect(subject.expected).to eq("fake&#10;real\nfake&#10;end")
273+
end
274+
end
275+
end
276+
277+
context 'when the attribute value contains ]]> requiring CDATA section splitting' do
278+
# test_public_06: ]]> is encoded as ]]]]><![CDATA[> across two CDATA sections
279+
let(:test_case) { test_cases[5] }
280+
subject { test_case }
281+
282+
describe '#output' do
283+
it 'reassembles the split CDATA sections into the original string' do
284+
expect(subject.output).to eq('hehehe")]]></message>')
285+
end
286+
end
287+
288+
describe '#expected' do
289+
it 'reassembles the split CDATA sections into the original string' do
290+
expect(subject.expected).to eq('hehehe")]]></message>')
291+
end
292+
end
293+
294+
describe '#expression' do
295+
it 'reassembles the split CDATA sections into the original string' do
296+
expect(subject.expression).to eq('EmojiChecker.containsEmoji("hehehe\")]]></message>")')
297+
end
298+
end
299+
end
300+
end
301+
end
221302
end

0 commit comments

Comments
 (0)