mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 13:33:42 +08:00
80fbb4f6db
Register vehicle controls, planner modes, UI actions and Galaxy services after their providers exist. Wire the shared runtime entrypoints and cover their combined behavior. Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com> Co-authored-by: Daniel Koepping <elkoled@gmail.com> Co-authored-by: Danny <dannyirwin31@yahoo.com> Co-authored-by: DimitriChrysafis <Dimitri.chrysafis@gmail.com> Co-authored-by: Dom <168790843+firestar5683@users.noreply.github.com> Co-authored-by: Harald Schäfer <harald.the.engineer@gmail.com> Co-authored-by: Kacper Rączy <gfw.kra@gmail.com> Co-authored-by: Prabhaav Pillai <prabhaav@msu.edu> Co-authored-by: dirwin31 <83434411+dirwin31@users.noreply.github.com> Co-authored-by: firestarsdog <229254897+firestarsdog@users.noreply.github.com> Co-authored-by: stef <19478336+stefpi@users.noreply.github.com> Co-authored-by: whoisdomi <dcantu83@gmail.com> Co-authored-by: Intelli <contact@intelli.software> Co-authored-by: haraschax <6804392+haraschax@users.noreply.github.com> Co-authored-by: prabhaavp <prabhaav@msu.edu>
507 lines
20 KiB
Python
507 lines
20 KiB
Python
"""Plots projection, bounded worker, actual IPC and authenticated HTTP."""
|
|
|
|
import http.client
|
|
import json
|
|
from pathlib import Path
|
|
from types import SimpleNamespace as NS
|
|
import tempfile
|
|
import threading
|
|
import time
|
|
import unittest
|
|
import uuid
|
|
from unittest import mock
|
|
|
|
from openpilot.cereal import messaging
|
|
from openpilot.starpilot.galaxy.access import GalaxyAccessOwner
|
|
from openpilot.starpilot.galaxy.plots import MessagingPlotsReader, PlotReading, Plots, extract, finite
|
|
from openpilot.starpilot.galaxy.server import make_server
|
|
|
|
|
|
def controls(kind='torqueState', **fields):
|
|
state = NS(which=lambda: kind, torqueState=NS(p=0.2, i=0.1, d=0.0, f=0.3, desiredLateralAccel=0.4, actualLateralAccel=0.35), pidState=NS(p=0.2, i=0.1, f=0.3))
|
|
return NS(
|
|
lateralControlState=state,
|
|
active=True,
|
|
longControlState='pid',
|
|
vPid=12.0,
|
|
desiredCurvature=0.01,
|
|
curvature=0.009,
|
|
aTarget=0.6,
|
|
upAccelCmd=0.2,
|
|
uiAccelCmd=0.1,
|
|
ufAccelCmd=0.3,
|
|
**fields,
|
|
)
|
|
|
|
|
|
def pose(valid=True):
|
|
return NS(velocityDevice=NS(valid=valid, x=12.0), accelerationDevice=NS(valid=valid, x=0.5))
|
|
|
|
|
|
class PlotsTest(unittest.TestCase):
|
|
def test_projection_sources_and_missing_values(self):
|
|
read = extract(
|
|
controls(),
|
|
pose(),
|
|
controls_fresh=True,
|
|
pose_fresh=True,
|
|
controls_valid=True,
|
|
pose_valid=True,
|
|
state=NS(active=True),
|
|
state_fresh=True,
|
|
plan=NS(aTarget=0.6),
|
|
plan_fresh=True,
|
|
car_control=NS(longActive=True),
|
|
car_control_fresh=True,
|
|
)
|
|
self.assertEqual((read['lateralSource'], read['longitudinalSource'], read['lateralTermsSource']), ('torqueState', 'aTarget', 'torqueState'))
|
|
self.assertEqual((read['desiredLateralAccel'], read['actualLongitudinalAccel']), (0.4, 0.5))
|
|
aol = extract(
|
|
controls(),
|
|
pose(),
|
|
controls_fresh=True,
|
|
pose_fresh=True,
|
|
controls_valid=True,
|
|
pose_valid=True,
|
|
state=NS(active=False),
|
|
state_fresh=True,
|
|
car_control=NS(latActive=True, longActive=False),
|
|
car_control_fresh=True,
|
|
)
|
|
self.assertEqual((aol['controlsActive'], aol['lateralControlActive'], aol['longitudinalControlActive']), (False, True, False))
|
|
long_only = extract(
|
|
controls(),
|
|
pose(),
|
|
controls_fresh=True,
|
|
pose_fresh=True,
|
|
controls_valid=True,
|
|
pose_valid=True,
|
|
state=NS(active=True),
|
|
state_fresh=True,
|
|
car_control=NS(latActive=False, longActive=True),
|
|
car_control_fresh=True,
|
|
)
|
|
self.assertEqual((long_only['controlsActive'], long_only['lateralControlActive'], long_only['longitudinalControlActive']), (True, False, True))
|
|
fallback = extract(
|
|
controls(kind='pidState'),
|
|
pose(False),
|
|
controls_fresh=True,
|
|
pose_fresh=True,
|
|
controls_valid=True,
|
|
pose_valid=True,
|
|
state=NS(active=True),
|
|
state_fresh=True,
|
|
car_state=NS(vEgo=12.0),
|
|
car_state_fresh=True,
|
|
)
|
|
self.assertEqual(fallback['lateralSource'], 'curvature')
|
|
self.assertEqual(fallback['speedSource'], 'carState')
|
|
self.assertEqual(fallback['lateralTermsSource'], 'pidState')
|
|
self.assertIsNone(fallback['lateralD'])
|
|
self.assertIsNone(fallback['actualLongitudinalAccel'])
|
|
self.assertEqual(fallback['desiredLateralAccel'], 1.44)
|
|
stale = extract(controls(), pose(), controls_fresh=False, pose_fresh=True, controls_valid=True, pose_valid=True, state=NS(active=True), state_fresh=True)
|
|
self.assertIsNone(stale['desiredLateralAccel'])
|
|
self.assertIsNone(stale['controlsActive'])
|
|
self.assertIsNone(finite(float('nan')))
|
|
self.assertIsNone(finite(float('inf')))
|
|
self.assertIsNone(finite(10**400))
|
|
corrupt = controls()
|
|
corrupt.lateralControlState.torqueState.actualLateralAccel = float('nan')
|
|
output = extract(
|
|
corrupt,
|
|
pose(),
|
|
controls_fresh=True,
|
|
pose_fresh=True,
|
|
controls_valid=True,
|
|
pose_valid=True,
|
|
state=NS(active=True),
|
|
state_fresh=True,
|
|
plan=NS(aTarget=0.6),
|
|
plan_fresh=True,
|
|
car_control=NS(longActive=True),
|
|
car_control_fresh=True,
|
|
)
|
|
self.assertEqual(output['lateralSource'], 'curvature')
|
|
self.assertTrue(
|
|
all(
|
|
value is None or isinstance(value, float)
|
|
for key, value in output.items()
|
|
if key in ('desiredLateralAccel', 'actualLateralAccel', 'actualLongitudinalAccel')
|
|
)
|
|
)
|
|
|
|
def test_worker_idle_and_reconnect(self):
|
|
readers = []
|
|
|
|
class Reader:
|
|
def __init__(self):
|
|
self.closed = False
|
|
readers.append(self)
|
|
|
|
def sample(self, now):
|
|
return PlotReading({'controlsFresh': True, 'speedMps': 1.0}, 0.0, (1,))
|
|
|
|
def close(self):
|
|
self.closed = True
|
|
|
|
source = Plots(reader_factory=Reader)
|
|
with mock.patch('openpilot.starpilot.galaxy.plots.SAMPLE_INTERVAL_S', 0.01), mock.patch('openpilot.starpilot.galaxy.plots.CLIENT_IDLE_S', 0.06):
|
|
try:
|
|
source.snapshot()
|
|
deadline = time.monotonic() + 1
|
|
while source.snapshot()['state'] != 'current' and time.monotonic() < deadline:
|
|
time.sleep(0.005)
|
|
self.assertEqual(source.snapshot()['state'], 'current')
|
|
time.sleep(0.1)
|
|
# The last request above has expired; its worker must release IPC before the next one.
|
|
self.assertTrue(readers[0].closed)
|
|
source.snapshot()
|
|
deadline = time.monotonic() + 1
|
|
while len(readers) < 2 and time.monotonic() < deadline:
|
|
time.sleep(0.005)
|
|
self.assertGreaterEqual(len(readers), 2)
|
|
finally:
|
|
source.close()
|
|
self.assertTrue(readers[-1].closed)
|
|
|
|
def test_producer_session_changes_across_process_instances(self):
|
|
first = Plots(reader_factory=lambda: None)
|
|
second = Plots(reader_factory=lambda: None)
|
|
self.assertRegex(first.session_id, r'^[0-9a-f]{16}$')
|
|
self.assertNotEqual(first.session_id, second.session_id)
|
|
first.close()
|
|
second.close()
|
|
|
|
def test_sample_age_and_boot_status_do_not_renew_without_reader_data(self):
|
|
now = [100.0]
|
|
|
|
class Reader:
|
|
def sample(self, timestamp):
|
|
return PlotReading({'controlsFresh': True, 'speedMps': 1.0}, 0.0, (1,))
|
|
|
|
def close(self):
|
|
pass
|
|
|
|
source = Plots(reader_factory=Reader, clock=lambda: now[0])
|
|
with (
|
|
mock.patch('openpilot.starpilot.galaxy.plots.SAMPLE_INTERVAL_S', 5.0),
|
|
mock.patch('openpilot.starpilot.galaxy.plots.boot_stabilizing', return_value=True),
|
|
):
|
|
try:
|
|
source.snapshot()
|
|
deadline = time.monotonic() + 1
|
|
while source.snapshot()['state'] != 'current' and time.monotonic() < deadline:
|
|
time.sleep(0.005)
|
|
self.assertTrue(source.snapshot()['bootStabilizing'])
|
|
now[0] += 1.6
|
|
aged = source.snapshot()
|
|
self.assertEqual(aged['state'], 'stale')
|
|
self.assertIsNone(aged['values'])
|
|
self.assertGreaterEqual(aged['sampleAgeSeconds'], 1.5)
|
|
finally:
|
|
source.close()
|
|
|
|
def test_publisher_age_budget_survives_worker_and_http_delay(self):
|
|
now = [100.0]
|
|
|
|
class Reader:
|
|
def sample(self, timestamp):
|
|
return PlotReading({'controlsFresh': True, 'speedMps': 1.0}, 1.4, (1,))
|
|
|
|
def close(self):
|
|
pass
|
|
|
|
source = Plots(reader_factory=Reader, clock=lambda: now[0])
|
|
with mock.patch('openpilot.starpilot.galaxy.plots.SAMPLE_INTERVAL_S', 5.0):
|
|
try:
|
|
source.snapshot()
|
|
deadline = time.monotonic() + 1
|
|
while source.snapshot()['state'] != 'current' and time.monotonic() < deadline:
|
|
time.sleep(0.005)
|
|
initial = source.snapshot()
|
|
self.assertEqual(initial['state'], 'current')
|
|
self.assertGreaterEqual(initial['sampleAgeSeconds'], 1.4)
|
|
self.assertLessEqual(initial['sampleAgeSeconds'], 1.401)
|
|
now[0] += 0.09
|
|
self.assertEqual(source.snapshot()['state'], 'current')
|
|
now[0] += 0.11
|
|
aged = source.snapshot()
|
|
self.assertEqual(aged['state'], 'stale')
|
|
self.assertIsNone(aged['values'])
|
|
self.assertGreaterEqual(aged['sampleAgeSeconds'], 1.6)
|
|
self.assertLessEqual(aged['sampleAgeSeconds'], 1.601)
|
|
finally:
|
|
source.close()
|
|
|
|
def test_repeated_source_sample_does_not_renew_age_or_history(self):
|
|
now = [100.0]
|
|
sampled = threading.Event()
|
|
|
|
class Reader:
|
|
def sample(self, timestamp):
|
|
sampled.set()
|
|
return PlotReading({'controlsFresh': True, 'speedMps': 1.0}, max(0.0, now[0] - 98.6), (1,))
|
|
|
|
def close(self):
|
|
pass
|
|
|
|
source = Plots(reader_factory=Reader, clock=lambda: now[0])
|
|
with mock.patch('openpilot.starpilot.galaxy.plots.SAMPLE_INTERVAL_S', 0.01):
|
|
try:
|
|
source.snapshot()
|
|
deadline = time.monotonic() + 1
|
|
while source.snapshot()['state'] != 'current' and time.monotonic() < deadline:
|
|
time.sleep(0.005)
|
|
initial = source.snapshot()
|
|
self.assertEqual(initial['sampleIndex'], 1)
|
|
self.assertGreaterEqual(initial['sampleAgeSeconds'], 1.4)
|
|
self.assertLessEqual(initial['sampleAgeSeconds'], 1.401)
|
|
sampled.clear()
|
|
now[0] += 0.2
|
|
self.assertTrue(sampled.wait(1))
|
|
deadline = time.monotonic() + 1
|
|
while source.snapshot()['sampleAgeSeconds'] < 1.6 and time.monotonic() < deadline:
|
|
time.sleep(0.005)
|
|
aged = source.snapshot()
|
|
self.assertEqual(aged['sampleIndex'], 1)
|
|
self.assertEqual(aged['state'], 'stale')
|
|
self.assertIsNone(aged['values'])
|
|
self.assertGreaterEqual(aged['sampleAgeSeconds'], 1.6)
|
|
finally:
|
|
source.close()
|
|
|
|
def test_resume_rearms_only_after_new_producer_events(self):
|
|
services = ('controlsState', 'deviceMotion', 'selfdriveState', 'longitudinalPlan', 'carControl', 'carState')
|
|
|
|
class Cached:
|
|
def __init__(self):
|
|
self.seen = dict.fromkeys(services, True)
|
|
self.valid = dict.fromkeys(services, True)
|
|
self.recv_time = dict.fromkeys(services, 100.05)
|
|
self.logMonoTime = dict.fromkeys(services, 100_050_000_000)
|
|
self.data = {
|
|
'controlsState': controls(),
|
|
'deviceMotion': pose(),
|
|
'selfdriveState': NS(active=True),
|
|
'longitudinalPlan': NS(aTarget=0.6),
|
|
'carControl': NS(latActive=True, longActive=True),
|
|
'carState': NS(vEgo=12.0),
|
|
}
|
|
|
|
def update(self, timeout):
|
|
self.asserted_timeout = timeout
|
|
|
|
def __getitem__(self, key):
|
|
return self.data[key]
|
|
|
|
reader = object.__new__(MessagingPlotsReader)
|
|
cached = Cached()
|
|
reader.started_ns = 100_000_000_000
|
|
reader.boot_offset_ns = 1_000_000_000
|
|
with (
|
|
mock.patch.object(reader, 'sm', cached, create=True),
|
|
mock.patch.object(time, 'CLOCK_BOOTTIME', 7, create=True),
|
|
mock.patch('openpilot.starpilot.galaxy.plots.paired_boot_offset_ns', side_effect=[1_000_000_000, 1_090_000_000, 1_090_000_000]),
|
|
mock.patch('openpilot.starpilot.galaxy.plots.time.monotonic', side_effect=[100.1, 100.1, 100.21]),
|
|
mock.patch('openpilot.starpilot.galaxy.plots.time.monotonic_ns', return_value=100_100_000_000),
|
|
):
|
|
self.assertTrue(reader.sample(0).values['controlsFresh'])
|
|
self.assertFalse(reader.sample(0).values['controlsFresh'])
|
|
for service in services:
|
|
cached.recv_time[service] = 100.2
|
|
cached.logMonoTime[service] = 100_200_000_000
|
|
self.assertTrue(reader.sample(0).values['controlsFresh'])
|
|
|
|
def test_reader_preserves_original_event_age_on_repeated_ipc_sample(self):
|
|
services = ('controlsState', 'deviceMotion', 'selfdriveState', 'longitudinalPlan', 'carControl', 'carState')
|
|
|
|
class Cached:
|
|
def __init__(self):
|
|
self.seen = dict.fromkeys(services, True)
|
|
self.valid = dict.fromkeys(services, True)
|
|
self.recv_time = dict.fromkeys(services, 98.6)
|
|
self.logMonoTime = dict.fromkeys(services, 98_600_000_000)
|
|
self.data = {
|
|
'controlsState': controls(),
|
|
'deviceMotion': pose(),
|
|
'selfdriveState': NS(active=True),
|
|
'longitudinalPlan': NS(aTarget=0.6),
|
|
'carControl': NS(latActive=True, longActive=True),
|
|
'carState': NS(vEgo=12.0),
|
|
}
|
|
|
|
def update(self, timeout):
|
|
pass
|
|
|
|
def __getitem__(self, key):
|
|
return self.data[key]
|
|
|
|
reader = object.__new__(MessagingPlotsReader)
|
|
cached = Cached()
|
|
reader.started_ns = 98_000_000_000
|
|
reader.boot_offset_ns = 1_000_000_000
|
|
with (
|
|
mock.patch.object(reader, 'sm', cached, create=True),
|
|
mock.patch.object(time, 'CLOCK_BOOTTIME', 7, create=True),
|
|
mock.patch('openpilot.starpilot.galaxy.plots.paired_boot_offset_ns', return_value=1_000_000_000),
|
|
mock.patch('openpilot.starpilot.galaxy.plots.time.monotonic', side_effect=[100.0, 100.2]),
|
|
):
|
|
first = reader.sample(0)
|
|
second = reader.sample(0)
|
|
self.assertTrue(first.values['controlsFresh'])
|
|
self.assertAlmostEqual(first.source_age_seconds, 1.4)
|
|
self.assertEqual(second.signature, (0,) * len(services))
|
|
self.assertFalse(second.values['controlsFresh'])
|
|
self.assertIsNone(second.source_age_seconds)
|
|
|
|
def test_typed_ipc_through_authenticated_http(self):
|
|
messaging.set_fake_prefix('galaxy_plots_http_' + uuid.uuid4().hex)
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
owner = GalaxyAccessOwner(Path(directory) / 'access')
|
|
owner.configure('password123', lambda: True)
|
|
source = Plots()
|
|
server = make_server(port=0, owner=owner, plots=source)
|
|
server_thread = threading.Thread(target=server.serve_forever, kwargs={'poll_interval': 0.01}, daemon=True)
|
|
server_thread.start()
|
|
publisher = messaging.PubMaster(['controlsState', 'selfdriveState', 'deviceMotion', 'longitudinalPlan', 'carControl'])
|
|
|
|
def request(path, method='GET', body=None, headers=None):
|
|
# Exercise the password-backed forwarded path.
|
|
connection = http.client.HTTPConnection('127.0.0.1', server.server_port, timeout=2)
|
|
try:
|
|
connection.request(method, path, body=body, headers={'Forwarded': 'for=203.0.113.8', **(headers or {})})
|
|
response = connection.getresponse()
|
|
return response.status, json.loads(response.read()), dict(response.getheaders())
|
|
finally:
|
|
connection.close()
|
|
|
|
try:
|
|
self.assertEqual(request('/api/plots/live')[0], 401)
|
|
self.assertIsNone(source.worker)
|
|
origin = f'http://127.0.0.1:{server.server_port}'
|
|
status, _, headers = request('/api/auth/login', 'POST', json.dumps({'password': 'password123'}), {'Content-Type': 'application/json', 'Origin': origin})
|
|
self.assertEqual(status, 200)
|
|
cookie = headers['Set-Cookie'].split(';', 1)[0]
|
|
self.assertEqual(request('/api/plots/live', headers={'Cookie': cookie})[1]['state'], 'stale')
|
|
control = messaging.new_message('controlsState', valid=True)
|
|
control.controlsState.lateralControlState.init('torqueState')
|
|
control.controlsState.lateralControlState.torqueState.desiredLateralAccel = 0.42
|
|
control.controlsState.lateralControlState.torqueState.actualLateralAccel = 0.37
|
|
state = messaging.new_message('selfdriveState', valid=True)
|
|
state.selfdriveState.active = True
|
|
motion = messaging.new_message('deviceMotion', valid=True)
|
|
motion.deviceMotion.velocityDevice.valid = True
|
|
motion.deviceMotion.velocityDevice.x = 12.0
|
|
motion.deviceMotion.accelerationDevice.valid = True
|
|
motion.deviceMotion.accelerationDevice.x = 0.5
|
|
plan = messaging.new_message('longitudinalPlan', valid=True)
|
|
plan.longitudinalPlan.aTarget = 0.6
|
|
car_control = messaging.new_message('carControl', valid=True)
|
|
car_control.carControl.longActive = True
|
|
car_control.carControl.latActive = True
|
|
deadline = time.monotonic() + 2
|
|
result = None
|
|
while time.monotonic() < deadline:
|
|
for name, event in [
|
|
('controlsState', control),
|
|
('selfdriveState', state),
|
|
('deviceMotion', motion),
|
|
('longitudinalPlan', plan),
|
|
('carControl', car_control),
|
|
]:
|
|
event.clear_write_flag()
|
|
publisher.send(name, event)
|
|
_, result, _ = request('/api/plots/live', headers={'Cookie': cookie})
|
|
if (
|
|
result['state'] == 'current'
|
|
and result['values']['poseFresh']
|
|
and result['values']['desiredLongitudinalAccel'] is not None
|
|
and result['values']['longitudinalControlActive']
|
|
):
|
|
break
|
|
time.sleep(0.02)
|
|
assert result is not None
|
|
self.assertEqual(result['state'], 'current')
|
|
self.assertEqual(result['values']['desiredLateralAccel'], 0.42)
|
|
self.assertEqual(result['values']['lateralSource'], 'torqueState')
|
|
self.assertEqual(result['values']['actualLongitudinalAccel'], 0.5)
|
|
self.assertEqual(result['values']['desiredLongitudinalAccel'], 0.6)
|
|
self.assertTrue(result['values']['longitudinalControlActive'])
|
|
self.assertTrue(result['values']['lateralControlActive'])
|
|
finally:
|
|
server.shutdown()
|
|
server_thread.join(2)
|
|
server.server_close()
|
|
del publisher
|
|
messaging.delete_fake_prefix()
|
|
|
|
def test_authenticated_http_lazy_and_late_revoke(self):
|
|
class Paused:
|
|
def __init__(self):
|
|
self.calls = 0
|
|
self.entered = threading.Event()
|
|
self.release = threading.Event()
|
|
self.closed = False
|
|
|
|
def snapshot(self):
|
|
self.calls += 1
|
|
self.entered.set()
|
|
self.release.wait(2)
|
|
return {'schemaVersion': 1, 'state': 'stale', 'sampleIndex': 0, 'sampleAgeSeconds': None, 'values': None, 'error': ''}
|
|
|
|
def close(self):
|
|
self.closed = True
|
|
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
owner = GalaxyAccessOwner(Path(directory) / 'access')
|
|
plots = Paused()
|
|
server = make_server(port=0, owner=owner, plots=plots)
|
|
worker = threading.Thread(target=server.serve_forever, kwargs={'poll_interval': 0.01}, daemon=True)
|
|
worker.start()
|
|
|
|
def request(path, method='GET', body=None, headers=None):
|
|
conn = http.client.HTTPConnection('127.0.0.1', server.server_port, timeout=3)
|
|
try:
|
|
conn.request(method, path, body=body, headers={'Forwarded': 'for=203.0.113.8', **(headers or {})})
|
|
response = conn.getresponse()
|
|
return response.status, response.read(), dict(response.getheaders())
|
|
finally:
|
|
conn.close()
|
|
|
|
try:
|
|
self.assertEqual(request('/api/plots/live')[0], 503)
|
|
owner.configure('password123', lambda: True)
|
|
self.assertEqual(request('/api/plots/live')[0], 401)
|
|
self.assertEqual(plots.calls, 0)
|
|
origin = f'http://127.0.0.1:{server.server_port}'
|
|
status, _, headers = request('/api/auth/login', 'POST', json.dumps({'password': 'password123'}), {'Content-Type': 'application/json', 'Origin': origin})
|
|
self.assertEqual(status, 200)
|
|
cookie = headers['Set-Cookie'].split(';', 1)[0]
|
|
plots.release.set()
|
|
status, body, headers = request('/api/plots/live', headers={'Cookie': cookie})
|
|
self.assertEqual(status, 200)
|
|
self.assertEqual(json.loads(body)['state'], 'stale')
|
|
self.assertEqual(headers['Cache-Control'], 'no-store')
|
|
plots.release.clear()
|
|
plots.entered.clear()
|
|
delayed = []
|
|
request_thread = threading.Thread(target=lambda: delayed.append(request('/api/plots/live', headers={'Cookie': cookie})))
|
|
request_thread.start()
|
|
self.assertTrue(plots.entered.wait(1))
|
|
self.assertEqual(request('/api/auth/logout', 'POST', '{}', {'Content-Type': 'application/json', 'Origin': origin, 'Cookie': cookie})[0], 200)
|
|
plots.release.set()
|
|
request_thread.join(2)
|
|
self.assertEqual(delayed[0][0], 401)
|
|
self.assertNotIn(b'"sampleIndex"', delayed[0][1])
|
|
finally:
|
|
plots.release.set()
|
|
server.shutdown()
|
|
worker.join(2)
|
|
server.server_close()
|
|
self.assertTrue(plots.closed)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|