11import os
2+ import tempfile
23import unittest
34from unittest .mock import patch
45
@@ -317,7 +318,7 @@ class TestFileDownloads(unittest.TestCase):
317318 @patch ("evaluation_function.evaluation.download_files" )
318319 def test_demo_mode_can_read_downloaded_file (self , mock_download ):
319320 mock_download .side_effect = _stub_download ({"data.csv" : "1,2,3" })
320- params = {"mode" : "demo" , "files" : [{"url" : "https://example.com/k" , "filename " : "data.csv" }]}
321+ params = {"mode" : "demo" , "files" : [{"url" : "https://example.com/k" , "name " : "data.csv" }]}
321322 result = evaluation_function ("print(open('data.csv').read())" , None , params ).to_dict ()
322323
323324 self .assertIn ("1,2,3" , result ["feedback" ])
@@ -327,7 +328,7 @@ def test_io_test_downloads_once_for_all_tests(self, mock_download):
327328 mock_download .side_effect = _stub_download ({"data.csv" : "42" })
328329 params = {
329330 "mode" : "io_test" ,
330- "files" : [{"url" : "https://example.com/k" , "filename " : "data.csv" }],
331+ "files" : [{"url" : "https://example.com/k" , "name " : "data.csv" }],
331332 "tests" : [_test ("" , "42\n " ), _test ("" , "42\n " )],
332333 }
333334 result = evaluation_function ("print(open('data.csv').read())" , None , params ).to_dict ()
@@ -341,7 +342,7 @@ def test_answer_code_receives_same_files(self, mock_download):
341342 params = {
342343 "mode" : "io_test" ,
343344 "use_answer_as_expected_output" : True ,
344- "files" : [{"url" : "https://example.com/k" , "filename " : "data.csv" }],
345+ "files" : [{"url" : "https://example.com/k" , "name " : "data.csv" }],
345346 "tests" : [{"input" : "" }],
346347 }
347348 code = "print(open('data.csv').read())"
@@ -352,15 +353,15 @@ def test_answer_code_receives_same_files(self, mock_download):
352353 @patch ("evaluation_function.evaluation.download_files" )
353354 def test_missing_file_reported_as_warning (self , mock_download ):
354355 mock_download .return_value = ["File 'data.csv' could not be found." ]
355- params = {"mode" : "demo" , "files" : [{"url" : "https://example.com/k" , "filename " : "data.csv" }]}
356+ params = {"mode" : "demo" , "files" : [{"url" : "https://example.com/k" , "name " : "data.csv" }]}
356357 result = evaluation_function ("print('hi')" , None , params ).to_dict ()
357358
358359 self .assertIn ("could not be found" , result ["feedback" ])
359360
360361 @patch ("evaluation_function.evaluation.download_files" )
361362 def test_import_of_uploaded_module (self , mock_download ):
362363 mock_download .side_effect = _stub_download ({"helper.py" : "def square(n):\n return n * n\n " })
363- params = {"mode" : "demo" , "files" : [{"url" : "https://example.com/k" , "filename " : "helper.py" }]}
364+ params = {"mode" : "demo" , "files" : [{"url" : "https://example.com/k" , "name " : "helper.py" }]}
364365 result = evaluation_function ("import helper\n print(helper.square(4))" , None , params ).to_dict ()
365366
366367 self .assertIn ("16" , result ["feedback" ])
@@ -371,36 +372,97 @@ def test_no_files_param_no_download_call(self):
371372 mock_download .assert_not_called ()
372373
373374
375+ class TestMalformedFileSpec (unittest .TestCase ):
376+
377+ def test_legacy_filename_key_does_not_crash (self ):
378+ # Reproduces the real-world crash report shape: a client sending the
379+ # old/wrong "filename" key instead of "name". Must not crash.
380+ params = {
381+ "mode" : "demo" ,
382+ "files" : [{
383+ "url" : "https://example.com/k" ,
384+ "filename" : "score_utils.py" ,
385+ "type" : "text/x-python-script" ,
386+ "size" : 237 ,
387+ }],
388+ }
389+ result = evaluation_function ("print('hi')" , None , params ).to_dict ()
390+
391+ self .assertIn ("hi" , result ["feedback" ])
392+ self .assertIn ("missing" , result ["feedback" ].lower ())
393+
394+
395+ class TestUnexpectedExceptionHandling (unittest .TestCase ):
396+
397+ @patch ("evaluation_function.evaluation._run_code" )
398+ def test_unexpected_exception_during_evaluation_is_caught (self , mock_run ):
399+ mock_run .side_effect = RuntimeError ("boom" )
400+
401+ result = evaluation_function ("print('hi')" , None , {"mode" : "demo" }).to_dict (include_test_data = True )
402+
403+ self .assertFalse (result ["is_correct" ])
404+ self .assertIn ("error" , result ["tags" ])
405+
406+ @patch ("evaluation_function.evaluation.download_files" )
407+ def test_exception_in_download_files_becomes_error_result (self , mock_download ):
408+ # Simulates a bug in download_files() itself (defense-in-depth,
409+ # independent of the s3_files.py validation fix).
410+ mock_download .side_effect = KeyError ("name" )
411+ params = {"mode" : "demo" , "files" : [{"url" : "https://example.com/k" , "name" : "data.csv" }]}
412+
413+ result = evaluation_function ("print('hi')" , None , params ).to_dict (include_test_data = True )
414+
415+ self .assertFalse (result ["is_correct" ])
416+ self .assertIn ("error" , result ["tags" ])
417+
418+ def test_files_dir_cleaned_up_even_on_exception (self ):
419+ created_dirs = []
420+ real_mkdtemp = tempfile .mkdtemp
421+
422+ def tracking_mkdtemp (* args , ** kwargs ):
423+ d = real_mkdtemp (* args , ** kwargs )
424+ created_dirs .append (d )
425+ return d
426+
427+ with patch ("evaluation_function.evaluation.download_files" , side_effect = RuntimeError ("boom" )), \
428+ patch ("evaluation_function.evaluation.tempfile.mkdtemp" , side_effect = tracking_mkdtemp ):
429+ params = {"mode" : "demo" , "files" : [{"url" : "https://example.com/k" , "name" : "data.csv" }]}
430+ evaluation_function ("print('hi')" , None , params )
431+
432+ self .assertTrue (created_dirs )
433+ self .assertFalse (os .path .exists (created_dirs [0 ]))
434+
435+
374436class TestFileAccessSandbox (unittest .TestCase ):
375437
376438 @patch ("evaluation_function.evaluation.download_files" )
377439 def test_read_downloaded_file_succeeds (self , mock_download ):
378440 mock_download .side_effect = _stub_download ({"data.csv" : "hello" })
379- params = {"mode" : "demo" , "files" : [{"url" : "https://example.com/k" , "filename " : "data.csv" }]}
441+ params = {"mode" : "demo" , "files" : [{"url" : "https://example.com/k" , "name " : "data.csv" }]}
380442 result = evaluation_function ("print(open('data.csv').read())" , None , params ).to_dict ()
381443
382444 self .assertIn ("hello" , result ["feedback" ])
383445
384446 @patch ("evaluation_function.evaluation.download_files" )
385447 def test_write_mode_to_provided_file_blocked (self , mock_download ):
386448 mock_download .side_effect = _stub_download ({"data.csv" : "hello" })
387- params = {"mode" : "demo" , "files" : [{"url" : "https://example.com/k" , "filename " : "data.csv" }]}
449+ params = {"mode" : "demo" , "files" : [{"url" : "https://example.com/k" , "name " : "data.csv" }]}
388450 result = evaluation_function ("open('data.csv', 'w')" , None , params ).to_dict ()
389451
390452 self .assertIn ("read-only" , result ["feedback" ])
391453
392454 @patch ("evaluation_function.evaluation.download_files" )
393455 def test_write_new_file_in_run_dir_blocked (self , mock_download ):
394456 mock_download .side_effect = _stub_download ({"data.csv" : "hello" })
395- params = {"mode" : "demo" , "files" : [{"url" : "https://example.com/k" , "filename " : "data.csv" }]}
457+ params = {"mode" : "demo" , "files" : [{"url" : "https://example.com/k" , "name " : "data.csv" }]}
396458 result = evaluation_function ("open('output.txt', 'w')" , None , params ).to_dict ()
397459
398460 self .assertIn ("read-only" , result ["feedback" ])
399461
400462 @patch ("evaluation_function.evaluation.download_files" )
401463 def test_pathlib_read_respects_sandbox (self , mock_download ):
402464 mock_download .side_effect = _stub_download ({"data.csv" : "world" })
403- params = {"mode" : "demo" , "files" : [{"url" : "https://example.com/k" , "filename " : "data.csv" }]}
465+ params = {"mode" : "demo" , "files" : [{"url" : "https://example.com/k" , "name " : "data.csv" }]}
404466 code = "from pathlib import Path\n print(Path('data.csv').read_text())"
405467 result = evaluation_function (code , None , params ).to_dict ()
406468
@@ -409,7 +471,7 @@ def test_pathlib_read_respects_sandbox(self, mock_download):
409471 @patch ("evaluation_function.evaluation.download_files" )
410472 def test_pathlib_write_respects_sandbox (self , mock_download ):
411473 mock_download .side_effect = _stub_download ({"data.csv" : "world" })
412- params = {"mode" : "demo" , "files" : [{"url" : "https://example.com/k" , "filename " : "data.csv" }]}
474+ params = {"mode" : "demo" , "files" : [{"url" : "https://example.com/k" , "name " : "data.csv" }]}
413475 code = "from pathlib import Path\n Path('data.csv').write_text('nope')"
414476 result = evaluation_function (code , None , params ).to_dict ()
415477
0 commit comments