Files
WQ_GUI/_smoke_test_step8_9_csv_combo.py

229 lines
11 KiB
Python
Raw Permalink 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):
"""生成模拟 CSV50 个纯数字波段列 + 任意额外 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)
# ============================================================
# 测试 1Step8 加载 + 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__}")
# ============================================================
# 测试 2CSV 变更槽 — 模拟用户选 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 列)")
# ============================================================
# 测试 3CSV 不存在时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}")
# ============================================================
# 测试 4get_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 原始光谱 ✓")
# ============================================================
# 测试 6Step9 加载
# ============================================================
from src.gui.panels.step9_ml_predict_panel import Step9MlPredictPanel
panel9 = Step9MlPredictPanel()
print(f"[OK] Step9MlPredictPanel 实例化成功")
# 重建 10_WaterIndex_CSVWQI 测试集 fixturetest 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))
# ============================================================
# 测试 7Step9._resolve_latest_wqi_test_csv 优先 10_WaterIndex_CSV
# ============================================================
panel9.work_dir = work_dir
resolved = panel9._resolve_latest_wqi_test_csv()
# 应该是 sampling_watercolor_summary.csvmtime 较新)
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 ✓")
# ============================================================
# 测试 8Step9.update_from_config 优先级WQI CSV > Step4 raw
# ============================================================
# 注Step 4 回退路径依赖 main_window._panel_factoryoffline 测试无 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}")
# ============================================================
# 测试 9work_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)