update server
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import types
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
MODULE_PATH = Path(__file__).resolve().parents[1] / "core" / "device_heartbeat.py"
|
||||
SPEC = importlib.util.spec_from_file_location("device_heartbeat_under_test", MODULE_PATH)
|
||||
HEARTBEAT = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(HEARTBEAT)
|
||||
|
||||
|
||||
class DeviceHeartbeatTests(unittest.TestCase):
|
||||
def test_sends_current_device_id_to_server(self):
|
||||
calls = []
|
||||
requests_module = types.ModuleType("requests")
|
||||
|
||||
class Response:
|
||||
def raise_for_status(self):
|
||||
return None
|
||||
|
||||
def json(self):
|
||||
return {"success": True, "lastSeenAt": "2026-07-28T00:00:00.000Z"}
|
||||
|
||||
def post(url, json, timeout):
|
||||
calls.append((url, json, timeout))
|
||||
return Response()
|
||||
|
||||
requests_module.post = post
|
||||
api_module = types.ModuleType("api")
|
||||
api_module.data_record_url = "https://server.example/api"
|
||||
api_module.the_folder = "company/line"
|
||||
with patch.dict(sys.modules, {"requests": requests_module, "api": api_module}):
|
||||
timestamp = HEARTBEAT.heartbeat_device(timeout=7)
|
||||
|
||||
self.assertEqual(timestamp, "2026-07-28T00:00:00.000Z")
|
||||
self.assertEqual(calls, [("https://server.example/api", {
|
||||
"type": "deviceHeartbeat", "deviceId": "company/line"
|
||||
}, 7)])
|
||||
@@ -0,0 +1,157 @@
|
||||
import importlib.util
|
||||
import csv
|
||||
import io
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import types
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
MODULE_PATH = Path(__file__).resolve().parents[1] / "core" / "identification_config.py"
|
||||
SPEC = importlib.util.spec_from_file_location(
|
||||
"identification_config_under_test", MODULE_PATH
|
||||
)
|
||||
IDENTIFICATION_CONFIG = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(IDENTIFICATION_CONFIG)
|
||||
validate_identification_config = IDENTIFICATION_CONFIG.validate_identification_config
|
||||
parse_identification_config_csv = IDENTIFICATION_CONFIG.parse_identification_config_csv
|
||||
download_identification_config = IDENTIFICATION_CONFIG.download_identification_config
|
||||
|
||||
|
||||
VALID_CONFIG = {
|
||||
"q_in_val": 50.0,
|
||||
"dt": 0.1,
|
||||
"n_order": 6,
|
||||
"t_c": 2.5,
|
||||
"levels": [10, 20, 30, 40, 50, 60, 70, 80],
|
||||
"dead_area": 240.0,
|
||||
"xa_full": 1000.0,
|
||||
"V_val": 5.0,
|
||||
"repeat": 2,
|
||||
}
|
||||
|
||||
|
||||
def config_csv(config):
|
||||
output = io.StringIO(newline="")
|
||||
writer = csv.writer(output)
|
||||
writer.writerow(("parameter", "value"))
|
||||
for key in (
|
||||
"q_in_val", "dt", "n_order", "t_c", "levels", "dead_area",
|
||||
"xa_full", "V_val", "repeat"):
|
||||
value = config[key]
|
||||
if key == "levels":
|
||||
value = ",".join(str(item) for item in value)
|
||||
writer.writerow((key, value))
|
||||
return output.getvalue()
|
||||
|
||||
|
||||
class IdentificationConfigTests(unittest.TestCase):
|
||||
def test_accepts_and_normalizes_valid_config(self):
|
||||
result = validate_identification_config(VALID_CONFIG)
|
||||
self.assertEqual(result["repeat"], 2)
|
||||
self.assertEqual(result["levels"], [
|
||||
10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0
|
||||
])
|
||||
|
||||
def test_rejects_missing_field(self):
|
||||
config = dict(VALID_CONFIG)
|
||||
config.pop("repeat")
|
||||
with self.assertRaisesRegex(ValueError, "缺少"):
|
||||
validate_identification_config(config)
|
||||
|
||||
def test_parses_parameter_value_csv(self):
|
||||
result = parse_identification_config_csv(config_csv(VALID_CONFIG))
|
||||
self.assertEqual(result, VALID_CONFIG)
|
||||
|
||||
def test_rejects_non_power_of_two_levels(self):
|
||||
config = dict(VALID_CONFIG, levels=[10, 20, 30])
|
||||
with self.assertRaisesRegex(ValueError, "2 的整数次幂"):
|
||||
validate_identification_config(config)
|
||||
|
||||
def test_rejects_symbol_period_shorter_than_sample_period(self):
|
||||
config = dict(VALID_CONFIG, dt=0.1, t_c=0.05)
|
||||
with self.assertRaisesRegex(ValueError, "t_c 必须大于等于 dt"):
|
||||
validate_identification_config(config)
|
||||
|
||||
def test_rejects_travel_scan_above_xa_full(self):
|
||||
config = dict(VALID_CONFIG, xa_full=999.0)
|
||||
with self.assertRaisesRegex(ValueError, "1000"):
|
||||
validate_identification_config(config)
|
||||
|
||||
def test_rejects_dead_area_at_or_above_xa_full(self):
|
||||
config = dict(VALID_CONFIG, dead_area=1000.0)
|
||||
with self.assertRaisesRegex(ValueError, "dead_area"):
|
||||
validate_identification_config(config)
|
||||
|
||||
def test_download_requests_customer_config_and_validates_it(self):
|
||||
calls = []
|
||||
|
||||
class FakeResponse:
|
||||
def __init__(self, body=None, text=None):
|
||||
self.body = body
|
||||
self.text = text
|
||||
|
||||
def raise_for_status(self):
|
||||
return None
|
||||
|
||||
def json(self):
|
||||
return self.body
|
||||
|
||||
requests_module = types.ModuleType("requests")
|
||||
requests_module.RequestException = Exception
|
||||
|
||||
def post(url, json, timeout):
|
||||
calls.append(("post", url, json, timeout))
|
||||
return FakeResponse({"success": True, "url": "https://temp/config"})
|
||||
|
||||
def get(url, timeout):
|
||||
calls.append(("get", url, timeout))
|
||||
return FakeResponse(text=config_csv(VALID_CONFIG))
|
||||
|
||||
requests_module.post = post
|
||||
requests_module.get = get
|
||||
api_module = types.ModuleType("api")
|
||||
api_module.data_record_url = "https://cloud/data_record"
|
||||
api_module.the_folder = "客户A"
|
||||
|
||||
with patch.dict(sys.modules, {
|
||||
"requests": requests_module,
|
||||
"api": api_module,
|
||||
}):
|
||||
result = download_identification_config(timeout=7)
|
||||
|
||||
self.assertEqual(result["repeat"], 2)
|
||||
self.assertEqual(calls[0], (
|
||||
"post",
|
||||
"https://cloud/data_record",
|
||||
{"type": "getIdentificationConfig", "deviceId": "客户A"},
|
||||
7,
|
||||
))
|
||||
self.assertEqual(calls[1], ("get", "https://temp/config", 7))
|
||||
|
||||
def test_download_reports_cloud_rejection(self):
|
||||
class FakeResponse:
|
||||
def raise_for_status(self):
|
||||
return None
|
||||
|
||||
def json(self):
|
||||
return {"success": False, "errMsg": "配置不存在"}
|
||||
|
||||
requests_module = types.ModuleType("requests")
|
||||
requests_module.RequestException = Exception
|
||||
requests_module.post = lambda *args, **kwargs: FakeResponse()
|
||||
api_module = types.ModuleType("api")
|
||||
api_module.data_record_url = "https://cloud/data_record"
|
||||
api_module.the_folder = "客户A"
|
||||
|
||||
with patch.dict(sys.modules, {
|
||||
"requests": requests_module,
|
||||
"api": api_module,
|
||||
}):
|
||||
with self.assertRaisesRegex(ValueError, "配置不存在"):
|
||||
download_identification_config()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,85 @@
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import types
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
MODULE_PATH = (
|
||||
Path(__file__).resolve().parents[1] / "core" / "identification_feedback.py"
|
||||
)
|
||||
SPEC = importlib.util.spec_from_file_location(
|
||||
"identification_feedback_under_test", MODULE_PATH
|
||||
)
|
||||
FEEDBACK = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(FEEDBACK)
|
||||
|
||||
|
||||
class FakeResponse:
|
||||
def __init__(self, body):
|
||||
self.body = body
|
||||
|
||||
def raise_for_status(self):
|
||||
return None
|
||||
|
||||
def json(self):
|
||||
return self.body
|
||||
|
||||
|
||||
class IdentificationFeedbackTests(unittest.TestCase):
|
||||
def call_with_response(self, response_body, callback):
|
||||
calls = []
|
||||
requests_module = types.ModuleType("requests")
|
||||
|
||||
def post(url, json, timeout):
|
||||
calls.append((url, json, timeout))
|
||||
return FakeResponse(response_body)
|
||||
|
||||
requests_module.post = post
|
||||
api_module = types.ModuleType("api")
|
||||
api_module.data_record_url = "https://cloud/data_record"
|
||||
api_module.the_folder = "客户A"
|
||||
with patch.dict(sys.modules, {
|
||||
"requests": requests_module,
|
||||
"api": api_module,
|
||||
}):
|
||||
value = callback()
|
||||
return value, calls
|
||||
|
||||
def test_registers_uploaded_csv(self):
|
||||
_, calls = self.call_with_response(
|
||||
{"success": True},
|
||||
lambda: FEEDBACK.register_identification_result("result.csv", 7),
|
||||
)
|
||||
self.assertEqual(calls[0][1], {
|
||||
"type": "registerIdentificationResult",
|
||||
"deviceId": "客户A",
|
||||
"runId": "result.csv",
|
||||
"fileName": "result.csv",
|
||||
})
|
||||
|
||||
def test_pending_feedback_returns_none(self):
|
||||
value, _ = self.call_with_response(
|
||||
{"success": True, "ready": False},
|
||||
lambda: FEEDBACK.get_identification_feedback("result.csv"),
|
||||
)
|
||||
self.assertIsNone(value)
|
||||
|
||||
def test_feedback_returns_only_zero_or_one(self):
|
||||
for result in (0, 1):
|
||||
value, _ = self.call_with_response(
|
||||
{"success": True, "ready": True, "result": result},
|
||||
lambda: FEEDBACK.get_identification_feedback("result.csv"),
|
||||
)
|
||||
self.assertEqual(value, result)
|
||||
|
||||
with self.assertRaisesRegex(ValueError, "0 或 1"):
|
||||
self.call_with_response(
|
||||
{"success": True, "ready": True, "result": 2},
|
||||
lambda: FEEDBACK.get_identification_feedback("result.csv"),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,153 @@
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import types
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
MODULE_PATH = Path(__file__).resolve().parents[1] / "core" / "identification.py"
|
||||
|
||||
|
||||
def load_identification_module():
|
||||
get_v = types.ModuleType("get_V")
|
||||
get_v.measure_volume = lambda *args, **kwargs: None
|
||||
ind_collector = types.ModuleType("ind_collector")
|
||||
ind_collector.collect_data_with_prbs = lambda *args, **kwargs: {}
|
||||
api = types.ModuleType("api")
|
||||
api.base_url = "https://cloud.example"
|
||||
api.data_record_url = "https://cloud.example/data_record"
|
||||
api.the_folder = "customer-a"
|
||||
requests = types.ModuleType("requests")
|
||||
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"identification_under_test", MODULE_PATH
|
||||
)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
with patch.dict(sys.modules, {
|
||||
"get_V": get_v,
|
||||
"ind_collector": ind_collector,
|
||||
"api": api,
|
||||
"requests": requests,
|
||||
}):
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
IDENTIFICATION = load_identification_module()
|
||||
|
||||
|
||||
class FakeClock:
|
||||
def __init__(self):
|
||||
self.now = 0.0
|
||||
|
||||
def monotonic(self):
|
||||
self.now += 0.001
|
||||
return self.now
|
||||
|
||||
def sleep(self, duration):
|
||||
self.now += max(0.0, duration)
|
||||
|
||||
|
||||
class FakeConnectionManager:
|
||||
def __init__(self):
|
||||
self.distance = 0
|
||||
|
||||
def set_motor_position(self, distance):
|
||||
self.distance = int(distance)
|
||||
return True
|
||||
|
||||
def read_pressure(self):
|
||||
return self.distance / 100.0
|
||||
|
||||
|
||||
class InitialTravelScanTests(unittest.TestCase):
|
||||
def test_uploads_distance_and_pressure_json_without_time_fields(self):
|
||||
manager = IDENTIFICATION.IdentificationManager()
|
||||
manager._identifying = True
|
||||
captured = {}
|
||||
|
||||
def capture_upload(body, filename, folder):
|
||||
captured.update(body=body, filename=filename, folder=folder)
|
||||
return True
|
||||
|
||||
manager._upload_to_cos = capture_upload
|
||||
clock = FakeClock()
|
||||
with patch.object(IDENTIFICATION.time, "monotonic", clock.monotonic), \
|
||||
patch.object(IDENTIFICATION.time, "sleep", clock.sleep):
|
||||
payload = manager._run_initial_travel_scan(
|
||||
FakeConnectionManager()
|
||||
)
|
||||
|
||||
records = payload["stable_pressures"]
|
||||
expected_distances = list(range(1000, -1, -100))
|
||||
self.assertEqual(
|
||||
[record["distance"] for record in records], expected_distances
|
||||
)
|
||||
self.assertEqual(
|
||||
[record["pressure"] for record in records],
|
||||
[distance / 100.0 for distance in expected_distances],
|
||||
)
|
||||
self.assertTrue(captured["filename"].endswith(".json"))
|
||||
self.assertEqual(captured["folder"], "customer-a/ind_data")
|
||||
self.assertEqual(json.loads(captured["body"]), payload)
|
||||
self.assertTrue(all(
|
||||
set(record) == {"distance", "pressure"} for record in records
|
||||
))
|
||||
|
||||
def test_identification_uploads_collector_csv_and_notifies_filename(self):
|
||||
manager = IDENTIFICATION.IdentificationManager()
|
||||
manager._run_initial_travel_scan = lambda conn_mgr: {}
|
||||
uploaded = {}
|
||||
callbacks = []
|
||||
csv_data = b"t,u,p,q_in,V\n0.0,10.0,20.0,50.0,5.0\n"
|
||||
csv_filename = "identification_data_test.csv"
|
||||
|
||||
manager._upload_to_cos = lambda content, filename, folder: (
|
||||
uploaded.update(
|
||||
content=content, filename=filename, folder=folder
|
||||
) or True
|
||||
)
|
||||
manager.set_identification_upload_callback(
|
||||
lambda success, filename, error:
|
||||
callbacks.append((success, filename, error))
|
||||
)
|
||||
|
||||
class ConnectedManager:
|
||||
def is_connected(self):
|
||||
return True
|
||||
|
||||
collector_result = {
|
||||
"success": True,
|
||||
"csv_data": csv_data,
|
||||
"filename": csv_filename,
|
||||
}
|
||||
with patch.object(
|
||||
IDENTIFICATION, "collect_data_with_prbs",
|
||||
return_value=collector_result):
|
||||
started = manager.start_identification(
|
||||
conn_mgr=ConnectedManager(),
|
||||
running_flag_check=lambda: False,
|
||||
q_in_val=50.0,
|
||||
dt=0.1,
|
||||
n_order=6,
|
||||
t_c=2.5,
|
||||
levels=[10, 20, 30, 40, 50, 60, 70, 80],
|
||||
dead_area=240.0,
|
||||
xa_full=1000.0,
|
||||
V_val=5.0,
|
||||
repeat=2,
|
||||
)
|
||||
manager._task_thread.join(timeout=2)
|
||||
|
||||
self.assertTrue(started)
|
||||
self.assertFalse(manager._task_thread.is_alive())
|
||||
self.assertEqual(uploaded["content"], csv_data)
|
||||
self.assertEqual(uploaded["filename"], csv_filename)
|
||||
self.assertEqual(uploaded["folder"], "customer-a/ind_data")
|
||||
self.assertEqual(callbacks, [(True, csv_filename, None)])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,122 @@
|
||||
"""Tests for the company/production-line license protocol."""
|
||||
|
||||
import base64
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
import types
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
LICENSE_SPEC = importlib.util.spec_from_file_location(
|
||||
"license_utils_under_test", ROOT / "license_utils.py"
|
||||
)
|
||||
LICENSE = importlib.util.module_from_spec(LICENSE_SPEC)
|
||||
LICENSE_SPEC.loader.exec_module(LICENSE)
|
||||
|
||||
|
||||
class FakePublicKey:
|
||||
def verify(self, *args, **kwargs):
|
||||
return None
|
||||
|
||||
|
||||
def license_file(payload):
|
||||
payload_b64 = base64.b64encode(json.dumps(payload).encode()).decode()
|
||||
handle = tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", delete=False)
|
||||
handle.write(f"{payload_b64}|{base64.b64encode(b'signature').decode()}")
|
||||
handle.close()
|
||||
return handle.name
|
||||
|
||||
|
||||
NEW_LICENSE = {
|
||||
"license_id": "license-123",
|
||||
"customer": "Sample Co",
|
||||
"company_id": "company-123",
|
||||
"production_line_id": "line-123",
|
||||
"device_id": "sample-co/line-1",
|
||||
"issued": "2026-01-01 00:00",
|
||||
"expiry": "2099-01-01 00:00",
|
||||
"features": "*",
|
||||
}
|
||||
|
||||
|
||||
class LicenseProtocolTests(unittest.TestCase):
|
||||
def verify_payload(self, payload):
|
||||
path = license_file(payload)
|
||||
self.addCleanup(os.unlink, path)
|
||||
with patch.object(LICENSE, "_load_public_key", return_value=FakePublicKey()):
|
||||
return LICENSE.verify_license(path)
|
||||
|
||||
def test_new_license_returns_company_and_line_identifiers(self):
|
||||
self.assertEqual(self.verify_payload(NEW_LICENSE), NEW_LICENSE)
|
||||
|
||||
def test_old_license_remains_valid(self):
|
||||
legacy = {
|
||||
"customer": "Legacy Customer",
|
||||
"issued": "2026-01-01",
|
||||
"expiry": "2099-01-01",
|
||||
"features": "*",
|
||||
}
|
||||
self.assertEqual(self.verify_payload(legacy), legacy)
|
||||
|
||||
def test_new_license_rejects_missing_organization_identifier(self):
|
||||
invalid = dict(NEW_LICENSE)
|
||||
invalid.pop("production_line_id")
|
||||
with self.assertRaisesRegex(ValueError, "production_line_id"):
|
||||
self.verify_payload(invalid)
|
||||
|
||||
def test_new_license_rejects_unsafe_device_id(self):
|
||||
invalid = dict(NEW_LICENSE, device_id="sample-co/../line-1")
|
||||
with self.assertRaisesRegex(ValueError, "device_id"):
|
||||
self.verify_payload(invalid)
|
||||
|
||||
def test_online_active_status_is_accepted(self):
|
||||
class Response:
|
||||
def raise_for_status(self):
|
||||
return None
|
||||
|
||||
def json(self):
|
||||
return {"success": True, "valid": True, "status": "active",
|
||||
"licenseId": NEW_LICENSE["license_id"]}
|
||||
|
||||
with patch.object(LICENSE.requests, "post", return_value=Response()):
|
||||
LICENSE.validate_license_online(NEW_LICENSE)
|
||||
|
||||
def test_online_invalid_statuses_are_rejected(self):
|
||||
for status in ("revoked", "expired", "device_mismatch"):
|
||||
with self.subTest(status=status):
|
||||
class Response:
|
||||
def raise_for_status(self):
|
||||
return None
|
||||
|
||||
def json(self):
|
||||
return {"success": True, "valid": False, "status": status}
|
||||
|
||||
with patch.object(LICENSE.requests, "post", return_value=Response()):
|
||||
with self.assertRaisesRegex(LICENSE.ExpiredError, status):
|
||||
LICENSE.validate_license_online(NEW_LICENSE)
|
||||
|
||||
def test_online_network_failure_is_allowed_within_offline_grace(self):
|
||||
LICENSE._last_online_success_monotonic = LICENSE._time_module.monotonic()
|
||||
with patch.object(LICENSE.requests, "post",
|
||||
side_effect=LICENSE.requests.ConnectionError("offline")):
|
||||
LICENSE.validate_license_online(NEW_LICENSE)
|
||||
|
||||
def test_api_rejects_environment_device_id_mismatch(self):
|
||||
fake_license_utils = types.ModuleType("license_utils")
|
||||
fake_license_utils.get_verified_license = lambda: dict(NEW_LICENSE)
|
||||
api_spec = importlib.util.spec_from_file_location("api_under_test", ROOT / "api.py")
|
||||
api_module = importlib.util.module_from_spec(api_spec)
|
||||
with patch.dict(os.environ, {"REINLOOP_DEVICE_ID": "other/line"}, clear=False), \
|
||||
patch.dict(sys.modules, {"license_utils": fake_license_utils}):
|
||||
with self.assertRaisesRegex(RuntimeError, "不一致"):
|
||||
api_spec.loader.exec_module(api_module)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,218 @@
|
||||
import json
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
import types
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
MODULE_PATH = Path(__file__).resolve().parents[1] / "core" / "volume_config.py"
|
||||
SPEC = importlib.util.spec_from_file_location("volume_config_under_test", MODULE_PATH)
|
||||
VOLUME_CONFIG = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(VOLUME_CONFIG)
|
||||
load_volume_config = VOLUME_CONFIG.load_volume_config
|
||||
validate_volume_config = VOLUME_CONFIG.validate_volume_config
|
||||
create_volume_config_request = VOLUME_CONFIG.create_volume_config_request
|
||||
poll_volume_config_request = VOLUME_CONFIG.poll_volume_config_request
|
||||
acknowledge_volume_config_request = VOLUME_CONFIG.acknowledge_volume_config_request
|
||||
|
||||
|
||||
VALID_CONFIG = {
|
||||
"q_in_val": 50.0, "dt": 0.05, "p_max": 200.0,
|
||||
"fit_low": 50.0, "fit_high": 150.0, "T_delta": 30.0,
|
||||
"xa_full": 1000.0, "num_runs": 3,
|
||||
}
|
||||
|
||||
|
||||
class VolumeConfigTests(unittest.TestCase):
|
||||
def write_config(self, directory, config):
|
||||
path = Path(directory) / "volume.json"
|
||||
path.write_text(json.dumps(config), encoding="utf-8")
|
||||
return path
|
||||
|
||||
def test_load_valid_config(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
result = load_volume_config(self.write_config(directory, VALID_CONFIG))
|
||||
self.assertEqual(result["num_runs"], 3)
|
||||
self.assertEqual(result["xa_full"], 1000.0)
|
||||
|
||||
def test_rejects_missing_field(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
config = dict(VALID_CONFIG)
|
||||
config.pop("dt")
|
||||
with self.assertRaisesRegex(ValueError, "缺少"):
|
||||
load_volume_config(self.write_config(directory, config))
|
||||
|
||||
def test_rejects_invalid_range(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
config = dict(VALID_CONFIG, fit_high=40.0)
|
||||
with self.assertRaisesRegex(ValueError, "fit_low"):
|
||||
load_volume_config(self.write_config(directory, config))
|
||||
|
||||
def test_rejects_zero_flow(self):
|
||||
with self.assertRaisesRegex(ValueError, "q_in_val 必须大于 0"):
|
||||
validate_volume_config(dict(VALID_CONFIG, q_in_val=0))
|
||||
|
||||
def test_customer_creates_exactly_one_request_instruction(self):
|
||||
calls = []
|
||||
|
||||
class FakeResponse:
|
||||
def raise_for_status(self):
|
||||
return None
|
||||
|
||||
def json(self):
|
||||
return {
|
||||
"success": True,
|
||||
"requestId": "request-1",
|
||||
"expiresAtMs": 123456,
|
||||
}
|
||||
|
||||
requests_module = types.ModuleType("requests")
|
||||
|
||||
def post(url, json, timeout):
|
||||
calls.append((url, json, timeout))
|
||||
return FakeResponse()
|
||||
|
||||
requests_module.post = post
|
||||
api_module = types.ModuleType("api")
|
||||
api_module.data_record_url = "https://cloud/data_record"
|
||||
api_module.the_folder = "客户A"
|
||||
|
||||
with patch.dict(sys.modules, {
|
||||
"requests": requests_module,
|
||||
"api": api_module,
|
||||
}):
|
||||
result = create_volume_config_request(timeout=7)
|
||||
|
||||
self.assertEqual(result, {
|
||||
"request_id": "request-1",
|
||||
"expires_at_ms": 123456,
|
||||
})
|
||||
self.assertEqual(calls, [(
|
||||
"https://cloud/data_record",
|
||||
{"type": "createVolumeConfigRequest", "deviceId": "客户A"},
|
||||
7,
|
||||
)])
|
||||
|
||||
def test_pending_request_does_not_download_a_file(self):
|
||||
calls = []
|
||||
|
||||
class FakeResponse:
|
||||
def raise_for_status(self):
|
||||
return None
|
||||
|
||||
def json(self):
|
||||
return {"success": True, "ready": False, "expired": False}
|
||||
|
||||
requests_module = types.ModuleType("requests")
|
||||
requests_module.post = lambda *args, **kwargs: (
|
||||
calls.append(("post", kwargs["json"])) or FakeResponse()
|
||||
)
|
||||
requests_module.get = lambda *args, **kwargs: calls.append(("get", args[0]))
|
||||
api_module = types.ModuleType("api")
|
||||
api_module.data_record_url = "https://cloud/data_record"
|
||||
api_module.the_folder = "客户A"
|
||||
|
||||
with patch.dict(sys.modules, {
|
||||
"requests": requests_module,
|
||||
"api": api_module,
|
||||
}):
|
||||
result = poll_volume_config_request("request-1")
|
||||
|
||||
self.assertEqual(result, {"ready": False, "expired": False})
|
||||
self.assertEqual(calls, [("post", {
|
||||
"type": "getVolumeConfigRequest",
|
||||
"deviceId": "客户A",
|
||||
"requestId": "request-1",
|
||||
})])
|
||||
|
||||
def test_ready_request_downloads_and_validates_json(self):
|
||||
class FakeResponse:
|
||||
def __init__(self, body):
|
||||
self.body = body
|
||||
|
||||
def raise_for_status(self):
|
||||
return None
|
||||
|
||||
def json(self):
|
||||
return self.body
|
||||
|
||||
requests_module = types.ModuleType("requests")
|
||||
requests_module.post = lambda *args, **kwargs: FakeResponse({
|
||||
"success": True,
|
||||
"ready": True,
|
||||
"expired": False,
|
||||
"url": "https://temp/volume.json",
|
||||
})
|
||||
requests_module.get = lambda *args, **kwargs: FakeResponse(
|
||||
dict(VALID_CONFIG)
|
||||
)
|
||||
api_module = types.ModuleType("api")
|
||||
api_module.data_record_url = "https://cloud/data_record"
|
||||
api_module.the_folder = "客户A"
|
||||
|
||||
with patch.dict(sys.modules, {
|
||||
"requests": requests_module,
|
||||
"api": api_module,
|
||||
}):
|
||||
result = poll_volume_config_request("request-1")
|
||||
|
||||
self.assertTrue(result["ready"])
|
||||
self.assertEqual(result["config"], VALID_CONFIG)
|
||||
|
||||
def test_create_request_reports_server_rejection(self):
|
||||
class FakeResponse:
|
||||
def raise_for_status(self):
|
||||
return None
|
||||
|
||||
def json(self):
|
||||
return {"success": False, "errMsg": "尚未配置"}
|
||||
|
||||
requests_module = types.ModuleType("requests")
|
||||
requests_module.post = lambda *args, **kwargs: FakeResponse()
|
||||
api_module = types.ModuleType("api")
|
||||
api_module.data_record_url = "https://cloud/data_record"
|
||||
api_module.the_folder = "客户A"
|
||||
|
||||
with patch.dict(sys.modules, {
|
||||
"requests": requests_module,
|
||||
"api": api_module,
|
||||
}):
|
||||
with self.assertRaisesRegex(ValueError, "尚未配置"):
|
||||
create_volume_config_request()
|
||||
|
||||
def test_acknowledges_the_same_request_for_cleanup(self):
|
||||
calls = []
|
||||
|
||||
class FakeResponse:
|
||||
def raise_for_status(self):
|
||||
return None
|
||||
|
||||
def json(self):
|
||||
return {"success": True, "deleted": 1}
|
||||
|
||||
requests_module = types.ModuleType("requests")
|
||||
requests_module.post = lambda *args, **kwargs: (
|
||||
calls.append(kwargs["json"]) or FakeResponse()
|
||||
)
|
||||
api_module = types.ModuleType("api")
|
||||
api_module.data_record_url = "https://cloud/data_record"
|
||||
api_module.the_folder = "客户A"
|
||||
|
||||
with patch.dict(sys.modules, {
|
||||
"requests": requests_module,
|
||||
"api": api_module,
|
||||
}):
|
||||
acknowledge_volume_config_request("request-1")
|
||||
|
||||
self.assertEqual(calls, [{
|
||||
"type": "ackVolumeConfigRequest",
|
||||
"deviceId": "客户A",
|
||||
"requestId": "request-1",
|
||||
}])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user