Merge remote-tracking branch 'origin/gpt' into q3-bayes
This commit is contained in:
@@ -211,13 +211,12 @@ class SimulatorVirtualCapTests(unittest.TestCase):
|
||||
simulator = LocalSimulator([], robot_id="T001", config=CONFIG)
|
||||
simulator.entered = True
|
||||
simulator.virtual_time_s = CONFIG.field.max_virtual_duration_s
|
||||
status, body = simulator.handle(
|
||||
"/measure", {"arena_id": "default", "robot_id": "T001",
|
||||
"request_id": "x1",
|
||||
"position": {"x": 0.0, "y": 0.0}, "channel": 1})
|
||||
self.assertEqual(status, 200)
|
||||
self.assertFalse(body["accepted"])
|
||||
self.assertEqual(body["virtual_time_s"], 0)
|
||||
from mcm26b.sim import SimulatorClosed
|
||||
with self.assertRaises(SimulatorClosed):
|
||||
simulator.handle(
|
||||
"/measure", {"arena_id": "default", "robot_id": "T001",
|
||||
"request_id": "x1",
|
||||
"position": {"x": 0.0, "y": 0.0}, "channel": 1})
|
||||
|
||||
|
||||
class AbortFinalizationTests(unittest.TestCase):
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
"""审核报告的独立反例:时限、原始协议、并发和量化。"""
|
||||
import json
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from dataclasses import replace
|
||||
from threading import Event
|
||||
|
||||
import pytest
|
||||
|
||||
from mcm26b.config import ProblemConfig
|
||||
from mcm26b.sim import LocalSimulator, LocalSimTransport, SimulatorClosed, Source
|
||||
|
||||
|
||||
def payload(id="e", **extra):
|
||||
return dict(arena_id="default", robot_id="2026001", request_id=id, **extra)
|
||||
|
||||
|
||||
def measure(id="m", x=0, y=0):
|
||||
return payload(id, position=dict(x=x, y=y), channel=1)
|
||||
|
||||
|
||||
def test_late_enter_and_deadline():
|
||||
now = [1400.25]
|
||||
sim = LocalSimulator([], clock=lambda: now[0], wall_clock=lambda: 1234.5,
|
||||
window_started_at=0)
|
||||
response = sim.handle("/enter", payload())[1]
|
||||
assert response["remaining_real_duration_s"] == 99
|
||||
assert response["real_timestamp_ms"] == 1234500
|
||||
now[0] = 1500
|
||||
for path, body in [("/measure", measure()), ("/exit", payload("x"))]:
|
||||
with pytest.raises(SimulatorClosed):
|
||||
sim.handle(path, body)
|
||||
assert sim.virtual_time_s == 0
|
||||
|
||||
|
||||
def test_program_deadline_and_subsecond_enter():
|
||||
now = [100]
|
||||
sim = LocalSimulator([], clock=lambda: now[0], window_started_at=0)
|
||||
assert sim.handle("/enter", payload())[1]["remaining_real_duration_s"] == 1200
|
||||
now[0] = 1300
|
||||
with pytest.raises(SimulatorClosed):
|
||||
sim.handle("/measure", measure())
|
||||
late = LocalSimulator([], clock=lambda: 1499.5, window_started_at=0)
|
||||
assert late.handle("/enter", payload())[1]["remaining_real_duration_s"] == 0
|
||||
expired = LocalSimulator([], clock=lambda: 1500, window_started_at=0)
|
||||
with pytest.raises(SimulatorClosed):
|
||||
expired.handle("/enter", payload())
|
||||
|
||||
|
||||
def test_stress_mode_is_explicit():
|
||||
config = ProblemConfig()
|
||||
config = replace(config, field=replace(config.field, max_real_duration_s=1800))
|
||||
official = LocalSimulator([], config=config, clock=lambda: 0)
|
||||
stress = LocalSimulator([], config=config, clock=lambda: 0, stress_mode=True)
|
||||
assert official.handle("/enter", payload())[1]["remaining_real_duration_s"] == 1200
|
||||
assert stress.handle("/enter", payload())[1]["remaining_real_duration_s"] == 1800
|
||||
|
||||
|
||||
def test_virtual_crossing_action_finishes_then_connection_closes():
|
||||
sim = LocalSimulator([])
|
||||
sim.handle("/enter", payload())
|
||||
assert sim.handle("/measure", measure(x=1800000))[1]["virtual_time_s"] == 360005
|
||||
for path, body in [("/measure", measure("m2")), ("/exit", payload("x"))]:
|
||||
with pytest.raises(SimulatorClosed):
|
||||
sim.handle(path, body)
|
||||
assert not sim.exited
|
||||
|
||||
|
||||
def test_microseconds_accumulate_and_signed_zero_is_same_location():
|
||||
sim = LocalSimulator([Source(1, 1000, 0, 1500)])
|
||||
sim.handle("/enter", payload())
|
||||
a = sim.handle("/measure", measure("a", 0.0))[1]
|
||||
b = sim.handle("/measure", measure("b", -0.0))[1]
|
||||
assert a["svd_deg"] == b["svd_deg"]
|
||||
for i in range(100):
|
||||
point = 1 if i % 2 == 0 else 0
|
||||
sim.handle("/measure", measure(str(i), point, point))
|
||||
assert sim.virtual_time_s == 10 + 100 * 5282843 / 1000000
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bad", ["e\n", "e\u200b", "e\u0085", "e\ud800"])
|
||||
def test_identifier_rejection_does_not_consume_id(bad):
|
||||
sim = LocalSimulator([])
|
||||
assert sim.handle("/enter", payload(bad))[0] == 400
|
||||
assert sim._idempotency == {}
|
||||
assert sim.handle("/enter", payload())[1]["accepted"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("body,status", [
|
||||
(b'{"request_id":"a","request_id":"b"}', 400),
|
||||
(b'{"position":{"x":1,"x":2}}', 400),
|
||||
(b'{}' + b' ' * 65535, 413),
|
||||
(b'\xef\xbb\xbf{}', 400), (b'\xff', 400), (b'{', 400),
|
||||
(b'[]', 400), (b'{"x":NaN}', 400), (b'{"x":Infinity}', 400),
|
||||
(b'{"a":' + b'[' * 16 + b'0' + b']' * 16 + b'}', 400),
|
||||
], ids=["duplicate-root", "duplicate-nested", "oversize", "bom", "utf8",
|
||||
"syntax", "non-object", "nan", "infinity", "depth"])
|
||||
def test_raw_json(body, status):
|
||||
sim = LocalSimulator([])
|
||||
response = LocalSimTransport(sim).post("http://local/enter", body,
|
||||
{"Content-Type": "application/json"}, None)
|
||||
assert response.status == status
|
||||
assert json.loads(response.body)["accepted"] is False
|
||||
assert not sim.entered
|
||||
|
||||
|
||||
@pytest.mark.parametrize("method,path,headers,status", [
|
||||
("POST", "/enter?x=1", {"Content-Type": "application/json"}, 404),
|
||||
("POST", "/enter?", {"Content-Type": "application/json"}, 404),
|
||||
("POST", "/enter/", {"Content-Type": "application/json"}, 404),
|
||||
("GET", "/enter", {"Content-Type": "application/json"}, 405),
|
||||
("POST", "/enter", {"Content-Type": "text/plain"}, 415),
|
||||
("POST", "/enter", {"Content-Type": "application/json;foo=bar"}, 415),
|
||||
("POST", "/enter", {"Content-Type": "application/json", "Content-Encoding": "gzip"}, 415),
|
||||
("POST", "/enter", {"content-type": "Application/JSON; charset=UTF-8"}, 200),
|
||||
])
|
||||
def test_http_boundary(method, path, headers, status):
|
||||
transport = LocalSimTransport(LocalSimulator([]))
|
||||
response = transport.request(method, "http://local" + path,
|
||||
json.dumps(payload()).encode(), headers)
|
||||
assert response.status == status
|
||||
|
||||
|
||||
def test_concurrent_registration_and_action_finishing_after_deadline():
|
||||
now = [0]
|
||||
sim = LocalSimulator([], clock=lambda: now[0])
|
||||
sim.handle("/enter", payload())
|
||||
started, release = Event(), Event()
|
||||
observe = sim._observe
|
||||
calls = []
|
||||
|
||||
def blocking_observe(pos, channel):
|
||||
calls.append(1)
|
||||
started.set()
|
||||
assert release.wait(5)
|
||||
return observe(pos, channel)
|
||||
|
||||
sim._observe = blocking_observe
|
||||
with ThreadPoolExecutor(max_workers=2) as pool:
|
||||
first = pool.submit(sim.handle, "/measure", measure())
|
||||
try:
|
||||
assert started.wait(5)
|
||||
assert sim.handle("/measure", measure("other"))[0] == 409
|
||||
assert sim.handle("/measure", measure(x=1))[0] == 409
|
||||
waiting = Event()
|
||||
completed = sim._pending["done"]
|
||||
|
||||
class ObservedEvent:
|
||||
def wait(self):
|
||||
waiting.set()
|
||||
return completed.wait(5)
|
||||
|
||||
def set(self):
|
||||
completed.set()
|
||||
|
||||
sim._pending["done"] = ObservedEvent()
|
||||
duplicate = pool.submit(sim.handle, "/measure", measure())
|
||||
assert waiting.wait(5)
|
||||
finally:
|
||||
release.set()
|
||||
assert first.result(timeout=5) == duplicate.result(timeout=5)
|
||||
assert calls == [1]
|
||||
assert sim.virtual_time_s == 5
|
||||
# 执行中跨过现实截止仍返回结果,后续新请求断连。
|
||||
sim._observe = lambda pos, channel: (now.__setitem__(0, 1200) or observe(pos, channel))
|
||||
assert sim.handle("/measure", measure("last"))[1]["accepted"]
|
||||
with pytest.raises(SimulatorClosed):
|
||||
sim.handle("/exit", payload("exit"))
|
||||
|
||||
|
||||
def test_raw_size_and_depth_boundaries_are_inclusive():
|
||||
sim = LocalSimulator([])
|
||||
transport = LocalSimTransport(sim)
|
||||
headers = {"Content-Type": "application/json"}
|
||||
deep = b'{"extra":' + b'[' * 15 + b'0' + b']' * 15 + b'}'
|
||||
# 深度16合法,因缺少公共字段得到400,补公共字段后应为业务拒绝200。
|
||||
body = json.dumps({**payload(), **json.loads(deep)}).encode()
|
||||
assert transport.post("http://local/enter", body, headers, None).status == 200
|
||||
body = json.dumps(payload()).encode()
|
||||
response = transport.post("http://local/enter", body.ljust(65536), headers, None)
|
||||
assert json.loads(response.body)["accepted"]
|
||||
|
||||
|
||||
def test_huge_numeric_fields_return_bad_request():
|
||||
sim = LocalSimulator([])
|
||||
sim.handle("/enter", payload())
|
||||
huge = 10 ** 400
|
||||
assert sim.handle("/measure", measure(x=huge))[0] == 400
|
||||
body = measure()
|
||||
body["channel"] = huge
|
||||
assert sim.handle("/measure", body)[0] == 400
|
||||
assert sim.handle("/measure", measure())[1]["accepted"]
|
||||
|
||||
|
||||
def test_explicit_exit_closes_new_requests():
|
||||
sim = LocalSimulator([])
|
||||
sim.handle("/enter", payload())
|
||||
assert sim.handle("/exit", payload("exit"))[1]["exit_reason"] == "user_exit"
|
||||
assert sim.closed_reason == "user_exit"
|
||||
with pytest.raises(SimulatorClosed):
|
||||
sim.handle("/measure", measure())
|
||||
Reference in New Issue
Block a user