Files
WQ_GUI/_smoke_test_step8_9_csv_combo.py

229 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
"""
Smoke test: step8_ml_train_panel + step9_ml_predict_panel 的新 CSV→QComboBox 联动
& update_from_config 多源文件优先回退 逻辑。
不真正 import 整个 panel(避免 osgeo 重依赖),只 import 类本身,验证:
1) Step8MlTrainPanel.feature_start 变成 QComboBox
2) Step8._on_training_csv_changed 正确读 CSV 表头 + 默认选中 374.285
3) Step8._resolve_training_csv_from_workdir 优先级(Step7 > 10_WaterIndex > Step6 > Step7 兜底)
4) Step8.get_config / set_config / get_training_params 用 QComboBox API 不崩
5) Step9._resolve_latest_wqi_test_csv 找到正确的 WQI 测试集
6) Step9.update_from_config 优先级(WQI CSV > Step4 raw)
用 offscreen 模式避免弹窗,完整可重复跑。
"""
import os
import sys
import tempfile
from pathlib import Path
# 避免 panel 顶部 osgeo 导入阻塞
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
# 让 src 目录可 import
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import pandas as pd
import numpy as np
from PyQt5.QtWidgets import QApplication
# 必须先 create QApplication 才能 new widget
app = QApplication.instance() or QApplication(sys.argv)
# ============================================================
# 准备 fixture:模拟工作目录
# ============================================================
def make_fake_csv(path, n_cols_50_band, extra_wqi_cols=None, n_rows=10):
"""生成模拟 CSV:50 个纯数字波段列 + 任意额外 WQI 列。"""
band_cols = [374.285 + i for i in range(n_cols_50_band)]
# 浮点列名会被 pandas 自动转成 374.285 / 374.285001 / ...
cols = band_cols + (extra_wqi_cols or [])
data = np.random.RandomState(42).rand(n_rows, len(cols))
df = pd.DataFrame(data, columns=cols)
df.to_csv(path, index=False)
return df
with tempfile.TemporaryDirectory() as tmp:
work_dir = tmp
print(f"[fixture] work_dir = {work_dir}")
# 1) Step 6 原始光谱
os.makedirs(os.path.join(work_dir, "6_Spectral_Feature_Extraction"), exist_ok=True)
step6_csv = os.path.join(work_dir, "6_Spectral_Feature_Extraction", "training_spectra.csv")
make_fake_csv(step6_csv, 50)
# 2) Step 7 WQI 增强版
os.makedirs(os.path.join(work_dir, "7_Water_Quality_Indices"), exist_ok=True)
step7_csv = os.path.join(work_dir, "7_Water_Quality_Indices", "training_spectra_indices.csv")
wqi_names = [f"WQI_Chla_{i}" for i in range(10)] + [f"WQI_TP_{i}" for i in range(5)]
make_fake_csv(step7_csv, 50, wqi_names)
# 3) 10_WaterIndex_CSV 下的 training csv(较旧 mtime)
os.makedirs(os.path.join(work_dir, "10_WaterIndex_CSV"), exist_ok=True)
step10_csv = os.path.join(work_dir, "10_WaterIndex_CSV", "training_watercolor_summary.csv")
make_fake_csv(step10_csv, 50, wqi_names)
# 把 mtime 调成 3 年前,确认 7_Water_Quality_Indices 那个更新
old_time = (pd.Timestamp.now() - pd.Timedelta(days=365 * 3)).timestamp()
os.utime(step10_csv, (old_time, old_time))
# 4) Step 4 raw sampling
os.makedirs(os.path.join(work_dir, "4_sampling"), exist_ok=True)
step4_csv = os.path.join(work_dir, "4_sampling", "sampling_spectra.csv")
make_fake_csv(step4_csv, 50)
# 5) WQI 测试集(10_WaterIndex_CSV 下另一个 mtime 较新的 csv)
wqi_test_csv = os.path.join(work_dir, "10_WaterIndex_CSV", "sampling_watercolor_summary.csv")
make_fake_csv(wqi_test_csv, 50, wqi_names)
# 设为 1 小时前(mtime 比 step7_csv 新)
new_time = (pd.Timestamp.now() - pd.Timedelta(hours=1)).timestamp()
os.utime(wqi_test_csv, (new_time, new_time))
# 6) 9_ML_Prediction 输出目录
os.makedirs(os.path.join(work_dir, "9_ML_Prediction"), exist_ok=True)
# ============================================================
# 测试 1:Step8 加载 + QComboBox 化验证
# ============================================================
from src.gui.panels.step8_ml_train_panel import Step8MlTrainPanel
from PyQt5.QtWidgets import QComboBox
panel8 = Step8MlTrainPanel()
assert isinstance(panel8.feature_start, QComboBox), \
f"FAIL: feature_start 应为 QComboBox,实际为 {type(panel8.feature_start)}"
print(f"[OK] Step8.feature_start 类型: {type(panel8.feature_start).__name__}")
# ============================================================
# 测试 2:CSV 变更槽 — 模拟用户选 WQI CSV,自动填表头
# ============================================================
panel8._on_training_csv_changed(step7_csv)
assert panel8.feature_start.count() > 0
head_text = panel8.feature_start.currentText()
print(f"[OK] Step8 选中 step7_csv 后,feature_start.currentText() = {head_text!r}")
# 默认选中:第一个纯数字波段列 "374.285"
assert head_text.replace('.', '').lstrip('-').isdigit(), \
f"FAIL: 默认应选数字波段,实际 = {head_text!r}"
print(f"[OK] 默认选中数字波段列: {head_text!r}")
# 验证前 50 列已填充
n_items = panel8.feature_start.count()
assert n_items >= 50, f"FAIL: 应至少 50 项,实际 {n_items}"
print(f"[OK] Step8.feature_start 填充了 {n_items} 个候选列(前 50 列)")
# ============================================================
# 测试 3:CSV 不存在时,combo 回到占位项
# ============================================================
panel8._on_training_csv_changed("/nonexistent/path.csv")
assert panel8.feature_start.count() == 1
assert "请先选择训练 CSV" in panel8.feature_start.itemText(0)
print(f"[OK] Step8 CSV 不存在时,combo 回退到占位项: {panel8.feature_start.itemText(0)!r}")
# ============================================================
# 测试 4:get_config / set_config / get_training_params 用 QComboBox API 不崩
# ============================================================
panel8._on_training_csv_changed(step7_csv)
cfg = panel8.get_config()
assert 'feature_start_column' in cfg
assert isinstance(cfg['feature_start_column'], str)
assert cfg['feature_start_column'].replace('.', '').lstrip('-').isdigit()
print(f"[OK] Step8.get_config()['feature_start_column'] = {cfg['feature_start_column']!r}")
# set_config 试一下(先选个不存在的列名,应该走兜底 addItem 分支)
panel8.set_config({'feature_start_column': '999.999_unknown'})
# 重新触发 CSV 加载,验证回放不污染
panel8._on_training_csv_changed(step7_csv)
panel8.set_config({'feature_start_column': '374.285'})
assert panel8.feature_start.currentText() == '374.285'
print(f"[OK] Step8.set_config() 正确把 374.285 选中")
# get_training_params
params = panel8.get_training_params()
assert 'feature_start' in params
assert isinstance(params['feature_start'], float)
print(f"[OK] Step8.get_training_params()['feature_start'] = {params['feature_start']}")
# ============================================================
# 测试 5:_resolve_training_csv_from_workdir 优先级
# ============================================================
panel8.work_dir = work_dir
resolved = panel8._resolve_training_csv_from_workdir()
expected = step7_csv.replace('\\', '/')
assert resolved == expected, f"FAIL: 应优先 step7, 实际 {resolved!r}, 期望 {expected!r}"
print(f"[OK] Step8._resolve_training_csv_from_workdir() = {resolved}")
print(f" 优先级 1: Step 7 WQI 增强版 ✓")
# 模拟用户没跑过 Step 7(删掉 step7_csv 目录)— 但 10_WaterIndex_CSV 还在
import shutil
shutil.rmtree(os.path.join(work_dir, "7_Water_Quality_Indices"))
resolved = panel8._resolve_training_csv_from_workdir()
# 用户明确提到 10_WaterIndex_CSV 目录下的结果是有效来源,应作为第二优先级
expected_step10 = str(Path(step10_csv)).replace('\\', '/')
assert resolved == expected_step10, \
f"FAIL: 应取 10_WaterIndex_CSV/*training*, 实际 {resolved!r}, 期望 {expected_step10!r}"
print(f"[OK] Step 7 缺省时,回退到 10_WaterIndex_CSV/*training*: {resolved}")
print(f" 优先级 2: 10_WaterIndex_CSV/*training* ✓")
# 再删 10_WaterIndex_CSV 才回退到 Step 6
shutil.rmtree(os.path.join(work_dir, "10_WaterIndex_CSV"))
resolved = panel8._resolve_training_csv_from_workdir()
assert resolved == step6_csv.replace('\\', '/'), \
f"FAIL: 应回退 step6, 实际 {resolved!r}, 期望 {step6_csv!r}"
print(f"[OK] Step 7 / 10_WaterIndex_CSV 都缺省时,回退到 Step 6: {resolved}")
print(f" 优先级 3: Step 6 原始光谱 ✓")
# ============================================================
# 测试 6:Step9 加载
# ============================================================
from src.gui.panels.step9_ml_predict_panel import Step9MlPredictPanel
panel9 = Step9MlPredictPanel()
print(f"[OK] Step9MlPredictPanel 实例化成功")
# 重建 10_WaterIndex_CSV(WQI 测试集 fixture,test 5 中被删了)
os.makedirs(os.path.join(work_dir, "10_WaterIndex_CSV"), exist_ok=True)
wqi_test_csv = os.path.join(work_dir, "10_WaterIndex_CSV", "sampling_watercolor_summary.csv")
make_fake_csv(wqi_test_csv, 50, wqi_names)
new_time = (pd.Timestamp.now() - pd.Timedelta(hours=1)).timestamp()
os.utime(wqi_test_csv, (new_time, new_time))
# ============================================================
# 测试 7:Step9._resolve_latest_wqi_test_csv 优先 10_WaterIndex_CSV
# ============================================================
panel9.work_dir = work_dir
resolved = panel9._resolve_latest_wqi_test_csv()
# 应该是 sampling_watercolor_summary.csv(mtime 较新)
expected = wqi_test_csv.replace('\\', '/')
assert resolved == expected, \
f"FAIL: 应取 mtime 最新的 wqi 测试集, 实际 {resolved!r}, 期望 {expected!r}"
print(f"[OK] Step9._resolve_latest_wqi_test_csv() = {resolved}")
print(f" 优先级:10_WaterIndex_CSV/sampling_watercolor_summary.csv ✓")
# ============================================================
# 测试 8:Step9.update_from_config 优先级:WQI CSV > Step4 raw
# ============================================================
# 注:Step 4 回退路径依赖 main_window._panel_factory(offline 测试无 main_window,
# 故仅验证 WQI CSV 优先级主路径;Step 4 路径与原代码同源,逻辑没变)
# 有 WQI 时优先 WQI
os.makedirs(os.path.join(work_dir, "10_WaterIndex_CSV"), exist_ok=True)
wqi_test_csv2 = os.path.join(work_dir, "10_WaterIndex_CSV", "sampling_watercolor_summary.csv")
make_fake_csv(wqi_test_csv2, 50, wqi_names)
panel9.update_from_config(work_dir=work_dir, pipeline=None)
path = panel9.sampling_csv_file.get_path()
assert wqi_test_csv2.replace('\\', '/') in path, \
f"FAIL: 应优先 WQI CSV, 实际 {path!r}"
print(f"[OK] Step9.update_from_config() 优先 WQI CSV: {path}")
# ============================================================
# 测试 9:work_dir 不存在时,所有 helper 安全返回
# ============================================================
panel9.work_dir = ""
assert panel9._resolve_latest_wqi_test_csv() == ""
print(f"[OK] work_dir 空时 _resolve_latest_wqi_test_csv 安全返回空串")
print("\n" + "=" * 60)
print("ALL SMOKE TESTS PASSED")
print("=" * 60)