""" Graders for Git Conflict Resolution Environment. Each grader scores a resolved set of files from 0.0 to 1.0. """ import ast import sys import types import importlib import tempfile import os CONFLICT_MARKERS = ["<<<<<<<", "=======", ">>>>>>>"] def has_conflict_markers(content: str) -> bool: return any(marker in content for marker in CONFLICT_MARKERS) def load_module_from_string(source: str, module_name: str = "resolved"): """Dynamically load a Python module from a source string.""" module = types.ModuleType(module_name) exec(compile(source, module_name, "exec"), module.__dict__) return module def score_easy(resolved_files: dict) -> tuple[float, str]: """ Score the easy task: calculate_discount must handle both the order_total > 100 and is_premium discounts together. Returns (score 0.0-1.0, feedback string). """ if "store.py" not in resolved_files: return 0.0, "Missing store.py in resolved files." content = resolved_files["store.py"] if has_conflict_markers(content): return 0.0, "File still contains conflict markers." try: mod = load_module_from_string(content) except SyntaxError as e: return 0.0, f"Syntax error in resolved file: {e}" except Exception as e: return 0.0, f"Error loading module: {e}" if not hasattr(mod, "calculate_discount"): return 0.0, "Function calculate_discount not found." fn = mod.calculate_discount test_cases = [ ((50, False), 50.0, "No discount for small non-premium order"), ((150, False), 135.0, "10% discount for large non-premium order"), ((50, True), 40.0, "20% discount for small premium order"), ((150, True), 105.0, "30% combined discount for large premium order"), ] passed = 0 feedback_lines = [] for args, expected, description in test_cases: try: result = fn(*args) if abs(result - expected) < 0.01: passed += 1 else: feedback_lines.append(f"FAIL [{description}]: got {result}, expected {expected}") except Exception as e: feedback_lines.append(f"ERROR [{description}]: {e}") score = passed / len(test_cases) feedback = f"Passed {passed}/{len(test_cases)} tests." if feedback_lines: feedback += " Issues: " + "; ".join(feedback_lines) return round(score, 2), feedback def score_medium(resolved_files: dict) -> tuple[float, str]: """ Score the medium task: process_order must validate inputs, log, and compute loyalty points. """ if "orders.py" not in resolved_files: return 0.0, "Missing orders.py in resolved files." content = resolved_files["orders.py"] if has_conflict_markers(content): return 0.0, "File still contains conflict markers." try: mod = load_module_from_string(content) except SyntaxError as e: return 0.0, f"Syntax error: {e}" except Exception as e: return 0.0, f"Error loading module: {e}" if not hasattr(mod, "process_order"): return 0.0, "Function process_order not found." fn = mod.process_order passed = 0 total = 4 feedback_lines = [] # Test 1: normal order with loyalty points try: result = fn(1, ["apple"], 50) if (result.get("loyalty_points") == 500 and result.get("status") == "processed"): passed += 1 else: feedback_lines.append(f"FAIL [loyalty points]: got {result}") except Exception as e: feedback_lines.append(f"ERROR [normal order]: {e}") # Test 2: fractional total try: result = fn(2, ["book", "pen"], 30.5) if result.get("loyalty_points") == 305: passed += 1 else: feedback_lines.append(f"FAIL [fractional total]: loyalty_points={result.get('loyalty_points')}") except Exception as e: feedback_lines.append(f"ERROR [fractional total]: {e}") # Test 3: empty items raises ValueError try: fn(1, [], 50) feedback_lines.append("FAIL [empty items]: should raise ValueError") except ValueError: passed += 1 except Exception as e: feedback_lines.append(f"FAIL [empty items]: raised {type(e).__name__} instead of ValueError") # Test 4: zero total raises ValueError try: fn(1, ["x"], 0) feedback_lines.append("FAIL [zero total]: should raise ValueError") except ValueError: passed += 1 except Exception as e: feedback_lines.append(f"FAIL [zero total]: raised {type(e).__name__} instead of ValueError") score = passed / total feedback = f"Passed {passed}/{total} tests." if feedback_lines: feedback += " Issues: " + "; ".join(feedback_lines) return round(score, 2), feedback def score_hard(resolved_files: dict) -> tuple[float, str]: """ Score the hard task: all three files must work together. User must have both role and session_token. Auth and API must be consistent. """ required = ["models.py", "auth.py", "api.py"] for f in required: if f not in resolved_files: return 0.0, f"Missing {f} in resolved files." if has_conflict_markers(resolved_files[f]): return 0.0, f"{f} still contains conflict markers." # Write files to a temp dir so they can import each other with tempfile.TemporaryDirectory() as tmpdir: for fname, content in resolved_files.items(): with open(os.path.join(tmpdir, fname), "w") as f: f.write(content) sys.path.insert(0, tmpdir) try: # Clear cached modules for mod_name in ["models", "auth", "api"]: if mod_name in sys.modules: del sys.modules[mod_name] import models as mod_models import auth as mod_auth import api as mod_api except Exception as e: sys.path.pop(0) return 0.0, f"Import error: {e}" finally: sys.path.pop(0) passed = 0 total = 7 feedback_lines = [] # Test 1: User has role attribute try: u = mod_models.User(1, "test") assert hasattr(u, "role") passed += 1 except Exception as e: feedback_lines.append(f"FAIL [user has role]: {e}") # Test 2: User has session_token attribute try: u = mod_models.User(1, "test") assert hasattr(u, "session_token") passed += 1 except Exception as e: feedback_lines.append(f"FAIL [user has session_token]: {e}") # Test 3: make_admin works try: u = mod_auth.login(1, "test") u = mod_auth.make_admin(u) assert u.is_admin() passed += 1 except Exception as e: feedback_lines.append(f"FAIL [make_admin]: {e}") # Test 4: logout clears session try: u = mod_auth.login(1, "test") u = mod_auth.logout(u) assert not u.has_session() passed += 1 except Exception as e: feedback_lines.append(f"FAIL [logout]: {e}") # Test 5: api login returns user with session try: u = mod_api.handle_request("login", 1, "test") assert u is not None and u.has_session() passed += 1 except Exception as e: feedback_lines.append(f"FAIL [api login]: {e}") # Test 6: api make_admin returns admin user try: u = mod_api.handle_request("make_admin", 1, "test") assert u is not None and u.is_admin() passed += 1 except Exception as e: feedback_lines.append(f"FAIL [api make_admin]: {e}") # Test 7: api logout returns user with no session try: u = mod_api.handle_request("logout", 1, "test") assert u is not None and not u.has_session() passed += 1 except Exception as e: feedback_lines.append(f"FAIL [api logout]: {e}") score = passed / total feedback = f"Passed {passed}/{total} tests." if feedback_lines: feedback += " Issues: " + "; ".join(feedback_lines) return round(score, 2), feedback GRADERS = { "easy": score_easy, "medium": score_medium, "hard": score_hard, } def grade(task_id: str, resolved_files: dict) -> tuple[float, str]: if task_id not in GRADERS: return 0.0, f"Unknown task: {task_id}" return GRADERS[task_id](resolved_files)