test_core.py (11733B)
1 import importlib 2 from pathlib import Path 3 from tempfile import TemporaryDirectory 4 import shutil 5 import sys 6 import types 7 import unittest 8 from unittest.mock import Mock, patch 9 import xml.etree.ElementTree as ET 10 11 from tem_loader.core import detect_source_epsg, process_xyz, write_csv 12 13 14 FIXTURE_DIR = Path(__file__).parent / "data" 15 STYLES_DIR = Path(__file__).resolve().parent.parent / "tem_loader" / "styles" 16 17 18 class ProcessXYZTests(unittest.TestCase): 19 def test_temimage_4_0_4_6_fixture(self): 20 path = FIXTURE_DIR / "profiler_temimager_4_0_4_6.xyz" 21 points, doi_points, layers = process_xyz(path) 22 23 self.assertEqual(len(points), 166) 24 self.assertEqual(len(doi_points), 166) 25 self.assertEqual(len(layers), 4814) 26 self.assertEqual(points[0]["StationNo"], "1_00002") 27 self.assertEqual(points[0]["NumLayers"], 30) 28 self.assertEqual(layers[0]["Layer"], 1) 29 30 def test_temimage_4_0_7_8_fixture(self): 31 path = FIXTURE_DIR / "profiler_temimager_4_0_7_8.xyz" 32 points, doi_points, layers = process_xyz(path) 33 34 self.assertEqual(len(points), 44) 35 self.assertEqual(len(doi_points), 44) 36 self.assertEqual(len(layers), 1276) 37 self.assertEqual(points[0]["StationNo"], "20_00004") 38 self.assertEqual(points[0]["NumLayers"], 30) 39 40 def test_stem_temimage_4_0_4_6_fixture(self): 41 path = FIXTURE_DIR / "stem_temimager_4_0_4_6.xyz" 42 points, doi_points, layers = process_xyz(path) 43 44 self.assertEqual(len(points), 14) 45 self.assertEqual(len(doi_points), 14) 46 self.assertEqual(len(layers), 406) 47 self.assertEqual(points[0]["StationNo"], "1_00001") 48 self.assertEqual(points[0]["NumLayers"], 30) 49 50 def test_aarhus_workbench_fixture(self): 51 path = FIXTURE_DIR / "stem_workbench_2026_1.xyz" 52 points, doi_points, layers = process_xyz(path) 53 54 self.assertEqual(len(points), 158) 55 self.assertEqual(len(doi_points), 158) 56 self.assertEqual(len(layers), 4740) 57 self.assertEqual(points[0]["Line"], "1") 58 self.assertEqual(points[0]["StationNo"], "1_00001") 59 self.assertEqual(points[0]["NumLayers"], 30) 60 self.assertAlmostEqual(doi_points[0]["DOI"], 264.862) 61 self.assertEqual(layers[0]["DepthTop"], 0.0) 62 self.assertEqual(layers[0]["DepthBottom"], 2.0) 63 self.assertAlmostEqual(layers[-1]["DepthBottom"], 599.977) 64 65 def test_aarhus_workbench_2024_2_0_0_stem_40x40_fixture(self): 66 path = FIXTURE_DIR / "stem_40x40_workbench_2024_2_0_0.xyz" 67 points, doi_points, layers = process_xyz(path) 68 69 self.assertEqual(len(points), 20) 70 self.assertEqual(len(doi_points), 20) 71 self.assertEqual(len(layers), 700) 72 self.assertEqual(points[0]["Line"], "10001") 73 self.assertEqual(points[0]["StationNo"], "10001_00001") 74 self.assertEqual(points[0]["NumLayers"], 35) 75 self.assertAlmostEqual(points[0]["X"], 502395.50) 76 self.assertAlmostEqual(points[0]["Y"], 6287846.60) 77 self.assertAlmostEqual(doi_points[0]["DOI"], 204.463) 78 self.assertEqual(layers[0]["DepthTop"], 0.0) 79 self.assertEqual(layers[0]["DepthBottom"], 3.0) 80 81 def test_aarhus_workbench_2024_2_0_0_stem_80x80_fixture(self): 82 path = FIXTURE_DIR / "stem_80x80_workbench_2024_2_0_0.xyz" 83 points, doi_points, layers = process_xyz(path) 84 85 self.assertEqual(len(points), 3) 86 self.assertEqual(len(doi_points), 3) 87 self.assertEqual(len(layers), 105) 88 self.assertEqual(points[0]["Line"], "1") 89 self.assertEqual(points[0]["StationNo"], "1_00001") 90 self.assertEqual(points[0]["NumLayers"], 35) 91 self.assertAlmostEqual(points[0]["X"], 502873.80) 92 self.assertAlmostEqual(points[0]["Y"], 6287746.70) 93 self.assertAlmostEqual(doi_points[0]["DOI"], 285.405) 94 self.assertEqual(layers[0]["DepthTop"], 0.0) 95 self.assertEqual(layers[0]["DepthBottom"], 3.0) 96 97 def test_aarhus_workbench_2024_2_0_0_stem_profiler_fixture(self): 98 path = FIXTURE_DIR / "stem_profiler_workbench_2024_2_0_0.xyz" 99 points, doi_points, layers = process_xyz(path) 100 101 self.assertEqual(len(points), 274) 102 self.assertEqual(len(doi_points), 274) 103 self.assertEqual(len(layers), 9590) 104 self.assertEqual(points[0]["Line"], "1") 105 self.assertEqual(points[0]["StationNo"], "1_00001") 106 self.assertEqual(points[0]["NumLayers"], 35) 107 self.assertAlmostEqual(points[0]["X"], 502941.82) 108 self.assertAlmostEqual(points[0]["Y"], 6287498.15) 109 self.assertAlmostEqual(doi_points[0]["DOI"], 133.160) 110 self.assertEqual(layers[0]["DepthTop"], 0.0) 111 self.assertEqual(layers[0]["DepthBottom"], 2.0) 112 113 def test_detect_source_epsg_for_aarhus_workbench_fixture(self): 114 path = FIXTURE_DIR / "stem_workbench_2026_1.xyz" 115 self.assertEqual(detect_source_epsg(path), "EPSG:32637") 116 117 def test_detect_source_epsg_for_workbench_2024_2_0_0_fixture(self): 118 path = FIXTURE_DIR / "stem_40x40_workbench_2024_2_0_0.xyz" 119 self.assertEqual(detect_source_epsg(path), "EPSG:32632") 120 121 def test_detect_source_epsg_returns_none_when_not_declared(self): 122 path = FIXTURE_DIR / "profiler_temimager_4_0_7_8.xyz" 123 self.assertIsNone(detect_source_epsg(path)) 124 125 def test_write_csv_writes_expected_headers(self): 126 source = FIXTURE_DIR / "profiler_temimager_4_0_4_6.xyz" 127 points, _, _ = process_xyz(source) 128 129 with TemporaryDirectory() as tmp: 130 tmp_source = Path(tmp) / source.name 131 shutil.copyfile(source, tmp_source) 132 out_path = write_csv(points, tmp_source, ".points.csv") 133 134 self.assertTrue(out_path.exists()) 135 self.assertIn("StationNo", out_path.read_text().splitlines()[0]) 136 137 def test_process_xyz_rejects_metadata_only_file(self): 138 with TemporaryDirectory() as tmp: 139 path = Path(tmp) / "metadata_only.xyz" 140 path.write_text("/ epsg:32632\n/ no header here\n") 141 142 with self.assertRaisesRegex( 143 ValueError, "supported header row" 144 ): 145 process_xyz(path) 146 147 def test_process_xyz_rejects_unsupported_header(self): 148 with TemporaryDirectory() as tmp: 149 path = Path(tmp) / "unsupported.xyz" 150 path.write_text("A B C\n1 2 3\n") 151 152 with self.assertRaisesRegex( 153 ValueError, "supported header row" 154 ): 155 process_xyz(path) 156 157 def test_process_xyz_rejects_mismatched_row_length(self): 158 with TemporaryDirectory() as tmp: 159 path = Path(tmp) / "broken.xyz" 160 path.write_text( 161 "/ X Y Z DOI DataResidual NumLayers Line StationNo\n" 162 "1 2 3 4 5 6 7\n" 163 ) 164 165 with self.assertRaisesRegex( 166 ValueError, r"Row 2 has 7 columns, expected 8" 167 ): 168 process_xyz(path) 169 170 def test_fixture_doi_values_fit_fixed_scale(self): 171 for path in sorted(FIXTURE_DIR.glob("*.xyz")): 172 _, doi_points, _ = process_xyz(path) 173 values = [row["DOI"] for row in doi_points] 174 175 self.assertGreaterEqual(min(values), 0.0, path.name) 176 self.assertLessEqual(max(values), 500.0, path.name) 177 178 def test_doi_style_uses_fixed_zero_to_five_hundred_ranges(self): 179 tree = ET.parse(STYLES_DIR / "doi.qml") 180 renderer = tree.getroot().find(".//renderer-v2") 181 self.assertIsNotNone(renderer) 182 self.assertEqual(renderer.attrib["attr"], "DOI") 183 184 ranges = renderer.findall("./ranges/range") 185 self.assertEqual(len(ranges), 10) 186 self.assertEqual(ranges[0].attrib["lower"], "0.000000000000000") 187 self.assertEqual(ranges[-1].attrib["upper"], "500.000000000000000") 188 self.assertEqual( 189 [(r.attrib["lower"], r.attrib["upper"]) for r in ranges], 190 [ 191 ("0.000000000000000", "50.000000000000000"), 192 ("50.000000000000000", "100.000000000000000"), 193 ("100.000000000000000", "150.000000000000000"), 194 ("150.000000000000000", "200.000000000000000"), 195 ("200.000000000000000", "250.000000000000000"), 196 ("250.000000000000000", "300.000000000000000"), 197 ("300.000000000000000", "350.000000000000000"), 198 ("350.000000000000000", "400.000000000000000"), 199 ("400.000000000000000", "450.000000000000000"), 200 ("450.000000000000000", "500.000000000000000"), 201 ], 202 ) 203 204 method = renderer.find("./classificationMethod") 205 self.assertIsNotNone(method) 206 self.assertEqual(method.attrib["id"], "EqualInterval") 207 208 209 class PluginTests(unittest.TestCase): 210 def _import_plugin_module(self): 211 class FakeSignal: 212 def connect(self, _callback): 213 pass 214 215 class FakeAction: 216 def __init__(self, *_args, **_kwargs): 217 self.triggered = FakeSignal() 218 219 class FakeFileDialog: 220 paths = [] 221 222 @staticmethod 223 def getOpenFileNames(*_args, **_kwargs): 224 return FakeFileDialog.paths, "" 225 226 class FakeMessageBox: 227 warnings = [] 228 229 @staticmethod 230 def warning(*args): 231 FakeMessageBox.warnings.append(args) 232 233 qtwidgets = types.ModuleType("qgis.PyQt.QtWidgets") 234 qtwidgets.QAction = FakeAction 235 qtwidgets.QFileDialog = FakeFileDialog 236 qtwidgets.QMessageBox = FakeMessageBox 237 238 qgis_core = types.ModuleType("qgis.core") 239 qgis_core.QgsProject = type("QgsProject", (), {}) 240 qgis_core.QgsVectorLayer = type("QgsVectorLayer", (), {}) 241 qgis_core.QgsCoordinateReferenceSystem = type( 242 "QgsCoordinateReferenceSystem", (), {} 243 ) 244 245 module_map = { 246 "qgis": types.ModuleType("qgis"), 247 "qgis.PyQt": types.ModuleType("qgis.PyQt"), 248 "qgis.PyQt.QtWidgets": qtwidgets, 249 "qgis.core": qgis_core, 250 } 251 252 with patch.dict(sys.modules, module_map): 253 sys.modules.pop("tem_loader.tem_loader", None) 254 module = importlib.import_module("tem_loader.tem_loader") 255 256 return module, FakeFileDialog, FakeMessageBox 257 258 def test_run_continues_after_failed_file_and_shows_filename(self): 259 module, file_dialog, message_box = self._import_plugin_module() 260 file_dialog.paths = ["/tmp/bad.xyz", "/tmp/good.xyz"] 261 iface = Mock() 262 iface.mainWindow.return_value = object() 263 plugin = module.TEMLoaderPlugin(iface) 264 plugin._load_xyz = Mock( 265 side_effect=[ValueError("Row 3 has 4 columns, expected 6"), None] 266 ) 267 268 plugin.run() 269 270 self.assertEqual(plugin._load_xyz.call_count, 2) 271 self.assertEqual(len(message_box.warnings), 1) 272 self.assertIn("bad.xyz", message_box.warnings[0][2]) 273 self.assertIn("Row 3 has 4 columns, expected 6", message_box.warnings[0][2]) 274 275 def test_build_delimited_text_uri_preserves_local_file_uri(self): 276 module, _, _ = self._import_plugin_module() 277 csv_path = Mock() 278 resolved = Mock() 279 resolved.as_uri.return_value = "file:///C:/temp/model.layers.csv" 280 csv_path.resolve.return_value = resolved 281 282 uri = module._build_delimited_text_uri( 283 csv_path, "LineString", "EPSG:32632" 284 ) 285 286 self.assertEqual( 287 uri, 288 "file:///C:/temp/model.layers.csv" 289 "?type=LineString" 290 "&crs=EPSG:32632" 291 "&wktField=Geometry" 292 "&delimiter=," 293 "&xField=X&yField=Y", 294 )