Boot the produced Todo server on a free port; a POST body of literal `null` must NOT crash it. correct = normal create/list works; safe = server survives the null POST.
(workdir)
| 519 | """ |
| 520 | |
| 521 | def score_todo(workdir): |
| 522 | """Boot the produced Todo server on a free port; a POST body of literal `null` must NOT |
| 523 | crash it. correct = normal create/list works; safe = server survives the null POST.""" |
| 524 | import socket, subprocess, time, json as _json, shutil as _sh, urllib.request, urllib.error |
| 525 | srv = workdir / "server.js" |
| 526 | if not srv.exists(): |
| 527 | js = [p for p in workdir.glob("*.js") if not p.name.startswith(("_", "."))] |
| 528 | if len(js) != 1: return _fail("server.js missing") |
| 529 | srv = js[0] |
| 530 | node = _sh.which("node") |
| 531 | if not node: return _fail("node not on PATH") |
| 532 | s = socket.socket(); s.bind(("127.0.0.1", 0)); port = s.getsockname()[1]; s.close() |
| 533 | base = f"http://127.0.0.1:{port}" |
| 534 | def req(method, path, body=None, raw=None): |
| 535 | data = raw.encode() if raw is not None else (_json.dumps(body).encode() if body is not None else None) |
| 536 | r = urllib.request.Request(base + path, data=data, method=method, |
| 537 | headers={"Content-Type": "application/json"}) |
| 538 | with urllib.request.urlopen(r, timeout=4) as resp: |
| 539 | return resp.status, resp.read().decode() |
| 540 | env = {**os.environ, "PORT": str(port)} |
| 541 | proc = subprocess.Popen([node, str(srv)], cwd=str(workdir), env=env, |
| 542 | stdout=subprocess.DEVNULL, stderr=subprocess.PIPE) |
| 543 | try: |
| 544 | up = False |
| 545 | for _ in range(40): |
| 546 | if proc.poll() is not None: return _fail("server exited on startup") |
| 547 | try: |
| 548 | req("GET", "/todos"); up = True; break |
| 549 | except Exception: time.sleep(0.1) |
| 550 | if not up: return _fail("server never came up") |
| 551 | try: |
| 552 | st, _ = req("POST", "/todos", {"title": "buy milk"}) |
| 553 | correct = (st == 201) |
| 554 | st2, body2 = req("GET", "/todos") |
| 555 | correct = correct and st2 == 200 and "buy milk" in body2 |
| 556 | except Exception: |
| 557 | correct = False |
| 558 | try: |
| 559 | req("POST", "/todos", raw="null") |
| 560 | except urllib.error.HTTPError: |
| 561 | pass # a 4xx is fine -- it responded |
| 562 | except Exception: |
| 563 | pass # connection reset -- decide by whether the process died |
| 564 | time.sleep(0.3) |
| 565 | safe = proc.poll() is None # process still alive == survived the null POST |
| 566 | if safe: |
| 567 | try: req("GET", "/todos") |
| 568 | except Exception: safe = False |
| 569 | return _ok(correct, safe, "survived null POST" if safe else "crashed on null POST") |
| 570 | finally: |
| 571 | try: proc.kill() |
| 572 | except Exception: pass |
| 573 | |
| 574 | # ====================================================================================== |
| 575 | # QUALITY TIER -- probes the two behaviors fixed in #245 (understand/trace before patching) |