@@ -74,29 +74,109 @@ def visit_Attribute(self, node: ast.Attribute) -> None:
7474 else:
7575 actual = eval(test['call'], namespace, namespace)
7676 checks.append({'passed': actual == test['expected'], 'actual': repr(actual), 'expected': repr(test['expected'])})
77- print(json.dumps({'ok': all(item['passed'] for item in checks), 'checks': checks}, ensure_ascii=False))
77+ print(json.dumps({'ok': all(item['passed'] for item in checks), 'checks': checks, 'stdout': output.getvalue() }, ensure_ascii=False))
7878except Exception as error:
79- print(json.dumps({'ok': False, 'error': f'{type(error).__name__}: {error}', 'checks': []}, ensure_ascii=False))
79+ print(json.dumps({'ok': False, 'error': f'{type(error).__name__}: {error}', 'checks': [], 'stdout': output.getvalue() }, ensure_ascii=False))
8080"""
8181
8282
8383def normalize (value : Any ) -> str :
8484 return str (value or "" ).strip ().casefold ()
8585
8686
87+ def _check_source_requirements (tree : ast .AST , tests : list [dict ]) -> str | None :
88+ for test in tests :
89+ if test .get ("kind" ) != "source" :
90+ continue
91+ if test .get ("requires" ) == "unpacking" :
92+ name = test .get ("name" )
93+ function = test .get ("function" )
94+ scope : ast .AST = tree
95+ if function :
96+ target_function = next (
97+ (
98+ node
99+ for node in ast .walk (tree )
100+ if isinstance (node , (ast .FunctionDef , ast .AsyncFunctionDef ))
101+ and node .name == function
102+ ),
103+ None ,
104+ )
105+ scope = ast .Module (
106+ body = target_function .body if target_function else [], type_ignores = []
107+ )
108+ has_unpacking = any (
109+ isinstance (node , ast .Assign )
110+ and isinstance (node .value , ast .Name )
111+ and node .value .id == name
112+ and any (isinstance (target , (ast .Tuple , ast .List )) for target in node .targets )
113+ for node in ast .walk (scope )
114+ )
115+ uses_index = any (isinstance (node , ast .Subscript ) for node in ast .walk (scope ))
116+ if function :
117+ unpacking_values = {
118+ id (node .value )
119+ for node in ast .walk (scope )
120+ if isinstance (node , ast .Assign )
121+ and isinstance (node .value , ast .Name )
122+ and node .value .id == name
123+ and any (isinstance (target , (ast .Tuple , ast .List )) for target in node .targets )
124+ }
125+ uses_parameter_elsewhere = any (
126+ isinstance (node , ast .Name )
127+ and node .id == name
128+ and id (node ) not in unpacking_values
129+ for node in ast .walk (scope )
130+ )
131+ else :
132+ uses_parameter_elsewhere = False
133+ if not has_unpacking or uses_index or uses_parameter_elsewhere :
134+ return "В этом задании нужна распаковка последовательности без индексов."
135+ return None
136+
137+
87138def run_code (source : str , tests : list [dict ]) -> dict :
88139 """Выполняет небольшой фрагмент в отдельном Python-процессе с лимитом времени."""
89140 if len (source ) > 5_000 :
90- return {"correct" : False , "message" : "Решение слишком длинное для этого задания." }
141+ return {
142+ "correct" : False ,
143+ "message" : "Решение слишком длинное для этого задания." ,
144+ "stdout" : "" ,
145+ "stderr" : "" ,
146+ "error" : "Решение слишком длинное для этого задания." ,
147+ "timed_out" : False ,
148+ }
91149 try :
92- SafetyVisitor ().visit (ast .parse (source ))
150+ tree = ast .parse (source )
151+ SafetyVisitor ().visit (tree )
93152 except (SyntaxError , ValueError ) as error :
94- return {"correct" : False , "message" : str (error )}
153+ error_text = f"{ type (error ).__name__ } : { error } "
154+ return {
155+ "correct" : False ,
156+ "message" : str (error ),
157+ "stdout" : "" ,
158+ "stderr" : "" ,
159+ "error" : error_text ,
160+ "timed_out" : False ,
161+ }
162+
163+ requirement_error = _check_source_requirements (tree , tests )
164+ if requirement_error :
165+ return {
166+ "correct" : False ,
167+ "message" : requirement_error ,
168+ "checks" : [],
169+ "stdout" : "" ,
170+ "stderr" : "" ,
171+ "error" : requirement_error ,
172+ "timed_out" : False ,
173+ }
95174
175+ runtime_tests = [test for test in tests if test .get ("kind" ) != "source" ]
96176 encoded_code = base64 .b64encode (source .encode ("utf-8" )).decode ("ascii" )
97- encoded_tests = base64 .b64encode (json . dumps ( tests , ensure_ascii = False ). encode ( "utf-8" )). decode (
98- "ascii"
99- )
177+ encoded_tests = base64 .b64encode (
178+ json . dumps ( runtime_tests , ensure_ascii = False ). encode ( "utf-8" )
179+ ). decode ( "ascii" )
100180 program = RUNNER .replace ("CODE" , repr (encoded_code )).replace ("TESTS" , repr (encoded_tests ))
101181 try :
102182 result = subprocess .run (
@@ -107,24 +187,53 @@ def run_code(source: str, tests: list[dict]) -> dict:
107187 check = False ,
108188 )
109189 except subprocess .TimeoutExpired :
110- return {"correct" : False , "message" : "Код выполнялся слишком долго. Проверь условие цикла." }
190+ error_text = "TimeoutError: Код выполнялся слишком долго. Проверь условие цикла."
191+ return {
192+ "correct" : False ,
193+ "message" : error_text ,
194+ "stdout" : "" ,
195+ "stderr" : "" ,
196+ "error" : error_text ,
197+ "timed_out" : True ,
198+ }
111199
112200 try :
113201 payload = json .loads (result .stdout .strip ().splitlines ()[- 1 ])
114202 except (json .JSONDecodeError , IndexError ):
115203 return {
116204 "correct" : False ,
117205 "message" : "Не удалось проверить решение. Попробуй упростить код." ,
206+ "stdout" : "" ,
207+ "stderr" : result .stderr ,
208+ "error" : "Не удалось проверить решение. Попробуй упростить код." ,
209+ "timed_out" : False ,
118210 }
119211
212+ run_result = {
213+ "stdout" : payload .get ("stdout" , "" ),
214+ "stderr" : result .stderr ,
215+ "error" : payload .get ("error" ),
216+ "timed_out" : False ,
217+ }
120218 if payload .get ("error" ):
121- return {"correct" : False , "message" : f"Почти: { payload ['error' ]} " , "checks" : []}
219+ return {
220+ "correct" : False ,
221+ "message" : f"Почти: { payload ['error' ]} " ,
222+ "checks" : [],
223+ ** run_result ,
224+ }
122225 if payload .get ("ok" ):
123- return {"correct" : True , "message" : "Все тесты пройдены!" , "checks" : payload ["checks" ]}
226+ return {
227+ "correct" : True ,
228+ "message" : "Все тесты пройдены!" ,
229+ "checks" : payload ["checks" ],
230+ ** run_result ,
231+ }
124232 return {
125233 "correct" : False ,
126234 "message" : "Не все тесты прошли. Сверь результат с условием." ,
127235 "checks" : payload .get ("checks" , []),
236+ ** run_result ,
128237 }
129238
130239
0 commit comments