| | |
| | | import tempfile |
| | | import unittest |
| | | import json |
| | | import struct |
| | | import zipfile |
| | | from unittest import mock |
| | | from http import HTTPStatus |
| | | from urllib.parse import quote |
| | |
| | | SPEC.loader.exec_module(MODULE) |
| | | |
| | | |
| | | def write_test_npz(path: Path, point_count: int) -> None: |
| | | """Create just enough valid NPY headers for the console's dependency-free validator.""" |
| | | def npy_header(shape: tuple[int, int]) -> bytes: |
| | | text = repr({"descr": "<f8", "fortran_order": False, "shape": shape}).encode("ascii") |
| | | padding = (-((10 + len(text) + 1) % 16)) % 16 |
| | | header = text + (b" " * padding) + b"\n" |
| | | return b"\x93NUMPY\x01\x00" + struct.pack("<H", len(header)) + header |
| | | |
| | | with zipfile.ZipFile(path, "w", compression=zipfile.ZIP_DEFLATED) as archive: |
| | | archive.writestr("xyz.npy", npy_header((point_count, 3))) |
| | | archive.writestr("las_rgb.npy", npy_header((point_count, 3))) |
| | | |
| | | |
| | | class WorkbenchConsoleHandlerTests(unittest.TestCase): |
| | | def test_run_deletion_plan_rejects_unknown_capability_and_path_traversal(self) -> None: |
| | | with tempfile.TemporaryDirectory() as temp_dir: |
| | | root = Path(temp_dir) |
| | | with self.assertRaises(MODULE.ApiError): |
| | | MODULE.run_deletion_plan(root, "not-a-capability", "run-1") |
| | | with self.assertRaises(MODULE.ApiError): |
| | | MODULE.run_deletion_plan(root, "02-semantic-mapping", "../run-1") |
| | | |
| | | def test_run_deletion_plan_protects_non_console_baseline(self) -> None: |
| | | with tempfile.TemporaryDirectory() as temp_dir: |
| | | root = Path(temp_dir) |
| | | artifact = root / "shared" / "outputs" / "02-semantic-mapping" / "validation-baseline" |
| | | artifact.mkdir(parents=True) |
| | | (artifact / "run_metadata.json").write_text(json.dumps({"capability": "02-semantic-mapping", "images": []}), encoding="utf-8") |
| | | plan = MODULE.run_deletion_plan(root, "02-semantic-mapping", "validation-baseline") |
| | | self.assertFalse(plan["removable"]) |
| | | self.assertEqual(plan["outputDirectories"], []) |
| | | |
| | | def test_console_run_deletion_removes_only_discovered_owned_directories(self) -> None: |
| | | with tempfile.TemporaryDirectory() as temp_dir: |
| | | root = Path(temp_dir) |
| | | run_id = "semantic-test-run" |
| | | output = root / "shared" / "outputs" / "02-semantic-mapping" / "runs" / run_id |
| | | raw = root / "shared" / "data" / "raw" / "02-semantic-mapping" / "runs" / run_id |
| | | processed = root / "shared" / "data" / "processed" / "02-semantic-mapping" / run_id |
| | | sibling = root / "shared" / "outputs" / "02-semantic-mapping" / "runs" / "keep-me" |
| | | external = root / "baseData" / "keep.jpg" |
| | | for directory in (output, raw, processed, sibling, external.parent): |
| | | directory.mkdir(parents=True, exist_ok=True) |
| | | (output / "run_metadata.json").write_text(json.dumps({"capability": "02-semantic-mapping", "images": [], "created_at": "2026-08-28T00:00:00Z"}), encoding="utf-8") |
| | | external.write_bytes(b"external") |
| | | plan = MODULE.run_deletion_plan(root, "02-semantic-mapping", run_id) |
| | | self.assertTrue(plan["removable"]) |
| | | self.assertEqual(plan["outputDirectories"], [f"shared/outputs/02-semantic-mapping/runs/{run_id}"]) |
| | | self.assertEqual(plan["rawDirectories"], [f"shared/data/raw/02-semantic-mapping/runs/{run_id}"]) |
| | | self.assertEqual(plan["processedDirectories"], [f"shared/data/processed/02-semantic-mapping/{run_id}"]) |
| | | removed = MODULE.delete_console_run(root, "02-semantic-mapping", run_id) |
| | | self.assertEqual(removed["runId"], run_id) |
| | | self.assertFalse(output.exists()) |
| | | self.assertFalse(raw.exists()) |
| | | self.assertFalse(processed.exists()) |
| | | self.assertTrue(sibling.is_dir()) |
| | | self.assertEqual(external.read_bytes(), b"external") |
| | | |
| | | def make_handler(self) -> MODULE.WorkbenchConsoleHandler: |
| | | handler = object.__new__(MODULE.WorkbenchConsoleHandler) |
| | | handler.directory = str(ROOT) |
| | |
| | | def test_large_tiff_upload_limits_allow_one_gibibyte_files(self) -> None: |
| | | self.assertEqual(MODULE.MAX_FILE_BYTES, 1024 * 1024 * 1024) |
| | | self.assertEqual(MODULE.MAX_REQUEST_BYTES, 3072 * 1024 * 1024) |
| | | |
| | | def test_photo_reconstruction_timeout_supports_long_cpu_runs(self) -> None: |
| | | self.assertEqual(MODULE.PHOTO_RECONSTRUCTION_TIMEOUT, 86_400) |
| | | |
| | | def test_binary_change_upload_preserves_original_bytes(self) -> None: |
| | | with tempfile.TemporaryDirectory() as temp_dir: |
| | |
| | | with self.assertRaisesRegex(MODULE.ApiError, "没有点记录"): |
| | | MODULE.validate_pointcloud_model_input(empty_las) |
| | | |
| | | def test_photo_reconstruction_request_requires_three_to_thirty_photos(self) -> None: |
| | | def test_photo_reconstruction_request_requires_three_to_one_thousand_photos(self) -> None: |
| | | handler = self.make_handler() |
| | | with self.assertRaisesRegex(MODULE.ApiError, "at least three"): |
| | | handler.create_photo_reconstruction_run({"photos": []}) |
| | |
| | | handler.create_photo_reconstruction_run({"photos": [{}] * (MODULE.MAX_PHOTO_RECONSTRUCTION_IMAGES_PER_RUN + 1)}) |
| | | with self.assertRaisesRegex(MODULE.ApiError, "true or false"): |
| | | handler.create_photo_reconstruction_run({"photos": [{}, {}, {}], "usePositionPriors": "yes"}) |
| | | |
| | | def test_photo_reconstruction_job_reads_progress_without_exposing_path(self) -> None: |
| | | with tempfile.TemporaryDirectory() as temp_dir: |
| | | progress_path = Path(temp_dir) / "progress.json" |
| | | progress_path.write_text(json.dumps({"percent": 68, "stage": "dense_fusion", "message": "running", "inputImages": 18, "estimate": True}), encoding="utf-8") |
| | | with MODULE.PHOTO_RECONSTRUCTION_JOBS_LOCK: |
| | | MODULE.PHOTO_RECONSTRUCTION_JOBS["progress-test"] = {"id": "progress-test", "runId": "run", "status": "running", "stage": "dense_mvs", "inputImages": 18, "progressPath": str(progress_path)} |
| | | try: |
| | | job = MODULE.photo_reconstruction_job("progress-test") |
| | | finally: |
| | | with MODULE.PHOTO_RECONSTRUCTION_JOBS_LOCK: |
| | | MODULE.PHOTO_RECONSTRUCTION_JOBS.pop("progress-test", None) |
| | | self.assertEqual(job["progress"]["percent"], 68) |
| | | self.assertNotIn("progressPath", job) |
| | | |
| | | def test_binary_anomaly_upload_decodes_chinese_file_name(self) -> None: |
| | | with tempfile.TemporaryDirectory() as temp_dir: |
| | |
| | | with mock.patch.object(MODULE.subprocess, "run", return_value=subprocess.CompletedProcess([], 0, '{"cuda": false, "torch": "2.11.0+cu128"}\n', "")): |
| | | with self.assertRaisesRegex(MODULE.ApiError, "CUDA was requested"): |
| | | MODULE.pointcloud_execution_environment(root, "cuda") |
| | | |
| | | def test_explicit_cpu_execution_never_probes_cuda(self) -> None: |
| | | with tempfile.TemporaryDirectory() as temp_dir: |
| | | root = Path(temp_dir) |
| | | cpu_python = root / ".venvs" / MODULE.OBJECT_DETECTION_CPU_ENVIRONMENT / "Scripts" / "python.exe" |
| | | cpu_python.parent.mkdir(parents=True) |
| | | cpu_python.write_bytes(b"fixed-cpu-interpreter") |
| | | with mock.patch.object(MODULE.subprocess, "run") as probe: |
| | | execution = MODULE.object_detection_execution_environment(root, "cpu") |
| | | probe.assert_not_called() |
| | | self.assertEqual(execution["requestedDevice"], "cpu") |
| | | self.assertEqual(execution["device"], "cpu") |
| | | self.assertFalse(execution["fallbackUsed"]) |
| | | |
| | | def test_auto_cpu_fallback_records_the_probe_reason(self) -> None: |
| | | with tempfile.TemporaryDirectory() as temp_dir: |
| | | root = Path(temp_dir) |
| | | cpu_python = root / ".venvs" / MODULE.CHANGE_DETECTION_CPU_ENVIRONMENT / "Scripts" / "python.exe" |
| | | gpu_python = root / ".venvs" / MODULE.CHANGE_DETECTION_GPU_ENVIRONMENT / "Scripts" / "python.exe" |
| | | cpu_python.parent.mkdir(parents=True) |
| | | gpu_python.parent.mkdir(parents=True) |
| | | cpu_python.write_bytes(b"fixed-cpu-interpreter") |
| | | gpu_python.write_bytes(b"fixed-gpu-interpreter") |
| | | with mock.patch.object(MODULE.subprocess, "run", return_value=subprocess.CompletedProcess([], 0, '{"cuda": false, "torch": "2.11.0+cu128"}\n', "")): |
| | | execution = MODULE.change_detection_execution_environment(root, "auto") |
| | | self.assertTrue(execution["fallbackUsed"]) |
| | | self.assertIn("cannot use CUDA", execution["fallbackReason"]) |
| | | |
| | | def test_object_detection_execution_uses_fixed_gpu_environment_when_probe_succeeds(self) -> None: |
| | | with tempfile.TemporaryDirectory() as temp_dir: |
| | |
| | | validation = next(item for item in runs if item["id"] == "validation-normal-20260821-v2") |
| | | self.assertTrue(validation["artifactRoot"].startswith("shared/outputs/05-3d-pointcloud/")) |
| | | |
| | | def test_multiview_annotation_source_requires_complete_ordered_contract(self) -> None: |
| | | with tempfile.TemporaryDirectory() as temp_dir: |
| | | root = Path(temp_dir) |
| | | artifact = root / "shared" / "outputs" / "05-3d-pointcloud" / "multiview-feature-valid" |
| | | artifact.mkdir(parents=True) |
| | | cloud = artifact / "multiview-annotation-source.ply" |
| | | cloud.write_text( |
| | | "ply\nformat ascii 1.0\nelement vertex 2\nproperty float x\nproperty float y\nproperty float z\nend_header\n0 0 0\n1 1 1\n", |
| | | encoding="ascii", |
| | | ) |
| | | dataset = artifact / "multiview-point-features.npz" |
| | | write_test_npz(dataset, 2) |
| | | (artifact / "run_metadata.json").write_text(json.dumps({ |
| | | "capability": "05-3d-pointcloud", |
| | | "artifacts": {"annotation_source": cloud.name, "feature_dataset": dataset.name}, |
| | | "annotation_source": { |
| | | "schema_version": 1, |
| | | "kind": "multiview_photo_feature_fusion", |
| | | "point_cloud": cloud.name, |
| | | "feature_dataset": dataset.name, |
| | | "point_count": 2, |
| | | "point_cloud_sha256": MODULE.file_sha256(cloud), |
| | | "feature_dataset_sha256": MODULE.file_sha256(dataset), |
| | | }, |
| | | }), encoding="utf-8") |
| | | |
| | | sources = MODULE.pointcloud_annotation_sources(root) |
| | | self.assertEqual(len(sources), 1) |
| | | self.assertEqual(sources[0]["pointCount"], 2) |
| | | self.assertIn("多视角照片特征融合", sources[0]["sourceKind"]) |
| | | |
| | | handler = object.__new__(MODULE.WorkbenchConsoleHandler) |
| | | handler.directory = str(root) |
| | | annotation = handler.create_pointcloud_annotation({"sourceId": sources[0]["id"], "labels": [[1, 15]]}) |
| | | self.assertEqual(annotation["labelCount"], 1) |
| | | execution = {"python": "fixed-python", "device": "cpu", "environment": "05-3d-pointcloud", "torchVersion": "2.13.0+cpu", "requestedDevice": "cpu", "fallbackUsed": False, "fallbackReason": None} |
| | | with mock.patch.object(MODULE, "pointcloud_execution_environment", return_value=execution), mock.patch.object(MODULE.threading, "Thread") as thread: |
| | | thread.return_value.start.return_value = None |
| | | job = handler.create_pointcloud_training_run({"annotationId": annotation["id"], "device": "cpu"}) |
| | | self.assertEqual(job["trainer"], "multiview_local_attention_baseline") |
| | | self.assertTrue(job["artifactRoot"] if "artifactRoot" in job else True) |
| | | self.assertEqual(thread.call_args.kwargs["args"][-1], "multiview_local_attention_baseline") |
| | | |
| | | dataset.unlink() |
| | | self.assertEqual(MODULE.pointcloud_annotation_sources(root), []) |
| | | |
| | | def test_pointcloud_request_requires_allowlisted_files(self) -> None: |
| | | handler = self.make_handler() |
| | | with self.assertRaisesRegex(MODULE.ApiError, "PLY, PCD, XYZ, LAS, or LAZ"): |
| | |
| | | with self.assertRaisesRegex(MODULE.ApiError, "Unsupported file type"): |
| | | handler.create_pointcloud_run({"pointClouds": [{"name": "unsafe.exe", "content": "eA=="}]}) |
| | | |
| | | def test_annotation_taxonomy_adds_custom_las_class_and_preserves_used_code(self) -> None: |
| | | with tempfile.TemporaryDirectory() as temp_dir: |
| | | root = Path(temp_dir) |
| | | handler = object.__new__(MODULE.WorkbenchConsoleHandler) |
| | | handler.directory = str(root) |
| | | defaults = MODULE.annotation_classes(root) |
| | | self.assertEqual({item["code"] for item in defaults}, {1, 2, 5, 6, 15, 16}) |
| | | created = handler.create_pointcloud_annotation_class({"key": "transformer", "label": "变压器", "color": [30, 144, 255]}) |
| | | self.assertEqual(created["code"], 17) |
| | | self.assertTrue(MODULE.annotation_classes_path(root).is_file()) |
| | | revision = root / "shared" / "outputs" / "05-3d-pointcloud" / "annotations" / "annotation-used" |
| | | revision.mkdir(parents=True) |
| | | (revision / "annotation.json").write_text(json.dumps({"schema_version": 1, "labels": [[0, 17]]}), encoding="utf-8") |
| | | with self.assertRaisesRegex(MODULE.ApiError, "used by a saved annotation"): |
| | | handler.delete_pointcloud_annotation_class(17) |
| | | (revision / "annotation.json").unlink() |
| | | self.assertEqual(handler.delete_pointcloud_annotation_class(17), 17) |
| | | self.assertNotIn(17, {item["code"] for item in MODULE.annotation_classes(root)}) |
| | | |
| | | def test_annotation_taxonomy_rejects_invalid_custom_metadata(self) -> None: |
| | | with tempfile.TemporaryDirectory() as temp_dir: |
| | | handler = object.__new__(MODULE.WorkbenchConsoleHandler) |
| | | handler.directory = temp_dir |
| | | with self.assertRaisesRegex(MODULE.ApiError, "lowercase English"): |
| | | handler.create_pointcloud_annotation_class({"key": "Transformer", "label": "变压器", "color": [30, 144, 255]}) |
| | | with self.assertRaisesRegex(MODULE.ApiError, "RGB"): |
| | | handler.create_pointcloud_annotation_class({"key": "transformer", "label": "变压器", "color": [999, 1, 1]}) |
| | | |
| | | def test_preview_only_annotation_source_is_discovered_without_geometry_artifacts(self) -> None: |
| | | with tempfile.TemporaryDirectory() as temp_dir: |
| | | root = Path(temp_dir) |
| | | run_id = "annotation-source-preview-only" |
| | | artifact = root / "shared" / "outputs" / "05-3d-pointcloud" / "runs" / run_id |
| | | artifact.mkdir(parents=True) |
| | | preview = artifact / "upload.annotation-source.ply" |
| | | preview.write_text( |
| | | "ply\nformat binary_little_endian 1.0\nelement vertex 2\nproperty float x\nproperty float y\nproperty float z\nproperty uchar red\nproperty uchar green\nproperty uchar blue\nend_header\n", |
| | | encoding="ascii", |
| | | ) |
| | | (artifact / "run_metadata.json").write_text(json.dumps({ |
| | | "capability": "05-3d-pointcloud", "annotation_source_job": True, |
| | | "annotation_source": {"schema_version": 1, "file": preview.name, "point_count": 2, "sha256": MODULE.file_sha256(preview)}, |
| | | "input": {"file": "upload.ply", "has_rgb": False}, |
| | | }), encoding="utf-8") |
| | | sources = MODULE.pointcloud_annotation_sources(root) |
| | | self.assertEqual(len(sources), 1) |
| | | self.assertEqual(sources[0]["id"], f"{run_id}:{preview.name}") |
| | | self.assertFalse(sources[0]["sourceHasRgb"]) |
| | | self.assertEqual(MODULE.pointcloud_annotation_source_deletion_plan(root, sources[0]["id"])["outputDirectories"], 1) |
| | | |
| | | def test_texture_baked_annotation_source_is_discovered_at_capability_root(self) -> None: |
| | | with tempfile.TemporaryDirectory() as temp_dir: |
| | | root = Path(temp_dir) |
| | | run_id = "texture-baked-preview" |
| | | artifact = root / "shared" / "outputs" / "05-3d-pointcloud" / run_id |
| | | artifact.mkdir(parents=True) |
| | | preview = artifact / "texture-baked-annotation-source.ply" |
| | | preview.write_text( |
| | | "ply\nformat binary_little_endian 1.0\nelement vertex 1\nproperty float x\nproperty float y\nproperty float z\nend_header\n", |
| | | encoding="ascii", |
| | | ) |
| | | (artifact / "run_metadata.json").write_text(json.dumps({ |
| | | "capability": "05-3d-pointcloud", "annotation_source_job": True, |
| | | "annotation_source": {"schema_version": 1, "file": preview.name, "point_count": 1, "sha256": MODULE.file_sha256(preview)}, |
| | | "input": {"file": "Model_0.zip", "has_rgb": True}, |
| | | }), encoding="utf-8") |
| | | sources = MODULE.pointcloud_annotation_sources(root) |
| | | self.assertEqual([item["id"] for item in sources], [f"{run_id}:{preview.name}"]) |
| | | self.assertTrue(sources[0]["sourceHasRgb"]) |
| | | |
| | | def test_annotation_source_removal_deletes_only_its_complete_local_chain(self) -> None: |
| | | with tempfile.TemporaryDirectory() as temp_dir: |
| | | root = Path(temp_dir) |
| | | run_id = "annotation-source-test" |
| | | artifact = root / "shared" / "outputs" / "05-3d-pointcloud" / "runs" / run_id |
| | | artifact.mkdir(parents=True) |
| | | annotation_cloud = artifact / "block.semantic-annotation-source.ply" |
| | | annotation_cloud.write_bytes(b"annotation-preview") |
| | | (artifact / "preview.png").write_bytes(b"preview") |
| | | (artifact / "vector.geojson").write_text('{"type":"FeatureCollection","features":[]}', encoding="utf-8") |
| | | (artifact / "run_metadata.json").write_text(json.dumps({"capability": "05-3d-pointcloud", "created_at": "2026-08-28T00:00:00+00:00", "annotation_source_job": True, "point_clouds": [{"file": "block.las", "preview_file": "preview.png", "vector_file": "vector.geojson", "semantic_annotation_source_point_cloud": annotation_cloud.name, "semantic_preview_points": 1}]}), encoding="utf-8") |
| | | source_id = f"{run_id}:{annotation_cloud.name}" |
| | | revision = root / "shared" / "outputs" / "05-3d-pointcloud" / "annotations" / "annotation-test" |
| | | revision.mkdir(parents=True) |
| | | annotation_path = revision / "annotation.json" |
| | | annotation_path.write_text(json.dumps({"schema_version": 1, "id": "annotation-test", "source_id": source_id, "labels": [[0, 5]]}), encoding="utf-8") |
| | | training = root / "shared" / "outputs" / "05-3d-pointcloud" / "training-runs" / "model-test" |
| | | training.mkdir(parents=True) |
| | | model = training / "model.pt" |
| | | model.write_bytes(b"weights") |
| | | (training / "metrics.json").write_text(json.dumps({"annotation": str(annotation_path)}), encoding="utf-8") |
| | | inference = root / "shared" / "outputs" / "05-3d-pointcloud" / "model-inference-runs" / "inference-test" |
| | | inference.mkdir(parents=True) |
| | | (inference / "run_metadata.json").write_text(json.dumps({"model": {"path": str(model)}}), encoding="utf-8") |
| | | raw = root / "shared" / "data" / "raw" / "05-3d-pointcloud" / "annotation-source-runs" / run_id |
| | | processed = root / "shared" / "data" / "processed" / "05-3d-pointcloud" / "annotation-source-runs" / run_id |
| | | raw.mkdir(parents=True); processed.mkdir(parents=True) |
| | | (raw / "block.las").write_bytes(b"raw"); (processed / "block.las").write_bytes(b"processed") |
| | | external = root / "baseData" / "block.las"; external.parent.mkdir(); external.write_bytes(b"must-remain") |
| | | |
| | | handler = object.__new__(MODULE.WorkbenchConsoleHandler) |
| | | handler.directory = str(root) |
| | | plan = MODULE.pointcloud_annotation_source_deletion_plan(root, source_id) |
| | | self.assertEqual((plan["rawDirectories"], plan["processedDirectories"], plan["annotationRevisions"], plan["trainingRuns"], plan["inferenceRuns"]), (1, 1, 1, 1, 1)) |
| | | result = handler.delete_pointcloud_annotation_source(source_id) |
| | | self.assertEqual(result["removed"]["trainingRuns"], 1) |
| | | self.assertFalse(artifact.exists()); self.assertFalse(raw.exists()); self.assertFalse(processed.exists()) |
| | | self.assertFalse(revision.exists()); self.assertFalse(training.exists()); self.assertFalse(inference.exists()) |
| | | self.assertEqual(external.read_bytes(), b"must-remain") |
| | | |
| | | def test_semantic_model_discovery_requires_complete_training_artifacts(self) -> None: |
| | | with tempfile.TemporaryDirectory() as temp_dir: |
| | | root = Path(temp_dir) |