package main import ( "encoding/json" "io" "os" "path/filepath" "testing" ) func TestRunRestoresPair(t *testing.T) { root := t.TempDir() input := filepath.Join(root, "bundle") output := filepath.Join(root, "out") writeTestFile(t, filepath.Join(input, "data", "scene_master", "m.txt"), "master") writeTestFile(t, filepath.Join(input, "data", "scene_slave", "s.txt"), "slave") writeTestFile(t, filepath.Join(input, "orbit", "master.txt"), "master orbit") doc := pairsDocument{ Schema: "dinsar_source_bundle_pairs.v1", Pairs: []pair{{ PairID: "pair_0001", TaskName: "Task_20250101_20250113", TaskAlias: "Task_20250101_20250113", MasterData: "data/scene_master", SlaveData: "data/scene_slave", MasterOrbit: "orbit/master.txt", SlaveOrbit: "orbit/missing.txt", MasterImagingDate: "20250101", SlaveImagingDate: "20250113", TimeBaselineDays: 12, }}, } writeTestJSON(t, filepath.Join(input, pairsFileName), doc) result, err := run(config{inputRoot: input, outputRoot: output, skipExisting: true}) if err != nil { t.Fatalf("run failed: %v", err) } if result.report.Restored != 1 { t.Fatalf("restored = %d, want 1", result.report.Restored) } if len(result.report.Warnings) != 1 { t.Fatalf("warnings = %d, want 1", len(result.report.Warnings)) } for _, path := range []string{ filepath.Join(output, "Task_20250101_20250113", "master", "m.txt"), filepath.Join(output, "Task_20250101_20250113", "slave", "s.txt"), filepath.Join(output, "Task_20250101_20250113", "orbit", "master.txt"), filepath.Join(output, "Task_20250101_20250113", pairMetaName), filepath.Join(output, reportFileName), filepath.Join(output, logFileName), } { if _, err := os.Stat(path); err != nil { t.Fatalf("expected %s to exist: %v", path, err) } } } func TestRunRestoresOriginalDataAndOrbitNames(t *testing.T) { root := t.TempDir() input := filepath.Join(root, "bundle") output := filepath.Join(root, "out") masterName := "LT1B_MONO_SYC_STRIP1_019663_E131.0_N44.8_20251010_SLC_HH_S2A_0000898998" slaveName := "LT1A_MONO_SYC_STRIP1_019664_E131.0_N44.8_20251022_SLC_HH_S2A_0000899001" masterBundleName := "scene_e1e6ef658c_" + masterName slaveBundleName := "scene_abc123def4_" + slaveName writeTestFile(t, filepath.Join(input, "data", masterBundleName, "scene_e1e6ef658c_"+masterName+".tiff"), "master") writeTestFile(t, filepath.Join(input, "data", slaveBundleName, "scene_abc123def4_"+slaveName+".tiff"), "slave") writeTestFile(t, filepath.Join(input, "orbit", "orbit_0123abcd_LT1A_GpsData_GAS_C_20230508.txt"), "master orbit") writeTestFile(t, filepath.Join(input, "orbit", "orbit_abcd1234_LT1B_GpsData_GAS_C_20230508.txt"), "slave orbit") writeTestJSON(t, filepath.Join(input, pairsFileName), pairsDocument{ Pairs: []pair{{ PairID: "pair_0001", TaskName: "Task_20251010_20251022", MasterData: filepath.ToSlash(filepath.Join("data", masterBundleName)), SlaveData: filepath.ToSlash(filepath.Join("data", slaveBundleName)), MasterOrbit: "orbit/orbit_0123abcd_LT1A_GpsData_GAS_C_20230508.txt", SlaveOrbit: "orbit/orbit_abcd1234_LT1B_GpsData_GAS_C_20230508.txt", }}, }) result, err := run(config{inputRoot: input, outputRoot: output, skipExisting: true, logWriter: io.Discard}) if err != nil { t.Fatalf("run failed: %v", err) } if result.report.Restored != 1 { t.Fatalf("restored = %d, want 1", result.report.Restored) } task := filepath.Join(output, "Task_20251010_20251022") for _, path := range []string{ filepath.Join(task, "master", masterName+".tiff"), filepath.Join(task, "slave", slaveName+".tiff"), filepath.Join(task, "orbit", "LT1A_GpsData_GAS_C_20230508.txt"), filepath.Join(task, "orbit", "LT1B_GpsData_GAS_C_20230508.txt"), } { if _, err := os.Stat(path); err != nil { t.Fatalf("expected %s to exist: %v", path, err) } } for _, path := range []string{ filepath.Join(task, "master", masterBundleName), filepath.Join(task, "slave", slaveBundleName), filepath.Join(task, "master", "scene_e1e6ef658c_"+masterName+".tiff"), filepath.Join(task, "slave", "scene_abc123def4_"+slaveName+".tiff"), filepath.Join(task, "orbit", "orbit_0123abcd_LT1A_GpsData_GAS_C_20230508.txt"), filepath.Join(task, "orbit", "orbit_abcd1234_LT1B_GpsData_GAS_C_20230508.txt"), } { if _, err := os.Stat(path); !os.IsNotExist(err) { t.Fatalf("expected %s to be absent, stat err=%v", path, err) } } } func TestRunCollapsesNestedPrefixedDataRoot(t *testing.T) { root := t.TempDir() input := filepath.Join(root, "bundle") output := filepath.Join(root, "out") originalName := "LT1B_MONO_SYC_STRIP1_019663_E131.0_N44.8_20251010_SLC_HH_S2A_0000898998" bundleName := "scene_e1e6ef658c_" + originalName writeTestFile(t, filepath.Join(input, "data", bundleName, bundleName, "m.txt"), "master") writeTestFile(t, filepath.Join(input, "data", "scene_abc123def4_slave", "s.txt"), "slave") writeTestJSON(t, filepath.Join(input, pairsFileName), pairsDocument{ Pairs: []pair{{ PairID: "pair_0001", TaskName: "Task_20251010_20251022", MasterData: filepath.ToSlash(filepath.Join("data", bundleName)), SlaveData: "data/scene_abc123def4_slave", }}, }) if _, err := run(config{inputRoot: input, outputRoot: output, skipExisting: true, logWriter: io.Discard}); err != nil { t.Fatalf("run failed: %v", err) } task := filepath.Join(output, "Task_20251010_20251022") if _, err := os.Stat(filepath.Join(task, "master", "m.txt")); err != nil { t.Fatalf("expected nested data root to be collapsed: %v", err) } if _, err := os.Stat(filepath.Join(task, "master", originalName)); !os.IsNotExist(err) { t.Fatalf("expected duplicated nested data root to be absent, stat err=%v", err) } } func TestRunSkipsExistingCompleteTask(t *testing.T) { root := t.TempDir() input := filepath.Join(root, "bundle") output := filepath.Join(root, "out") task := filepath.Join(output, "Task_20250101_20250113") writeTestFile(t, filepath.Join(input, "data", "scene_master", "m.txt"), "master") writeTestFile(t, filepath.Join(input, "data", "scene_slave", "s.txt"), "slave") writeTestFile(t, filepath.Join(task, "master", "old.txt"), "old master") writeTestFile(t, filepath.Join(task, "slave", "old.txt"), "old slave") writeTestJSON(t, filepath.Join(input, pairsFileName), pairsDocument{ Pairs: []pair{{ PairID: "pair_0001", TaskName: "Task_20250101_20250113", MasterData: "data/scene_master", SlaveData: "data/scene_slave", }}, }) result, err := run(config{inputRoot: input, outputRoot: output, skipExisting: true}) if err != nil { t.Fatalf("run failed: %v", err) } if result.report.Skipped != 1 { t.Fatalf("skipped = %d, want 1", result.report.Skipped) } if result.report.Restored != 0 { t.Fatalf("restored = %d, want 0", result.report.Restored) } } func TestRunLimitContinuesAfterExistingTasks(t *testing.T) { root := t.TempDir() input := filepath.Join(root, "bundle") output := filepath.Join(root, "out") var pairs []pair for i := 1; i <= 4; i++ { taskName := "Task_20250101_202501" + string(rune('0'+i)) masterDir := filepath.Join(input, "data", "scene_master_"+string(rune('0'+i))) slaveDir := filepath.Join(input, "data", "scene_slave_"+string(rune('0'+i))) writeTestFile(t, filepath.Join(masterDir, "m.txt"), "master") writeTestFile(t, filepath.Join(slaveDir, "s.txt"), "slave") pairs = append(pairs, pair{ PairID: "pair_000" + string(rune('0'+i)), TaskName: taskName, MasterData: filepath.ToSlash(filepath.Join("data", "scene_master_"+string(rune('0'+i)))), SlaveData: filepath.ToSlash(filepath.Join("data", "scene_slave_"+string(rune('0'+i)))), }) } writeTestJSON(t, filepath.Join(input, pairsFileName), pairsDocument{Pairs: pairs}) first, err := run(config{inputRoot: input, outputRoot: output, skipExisting: true, limit: 2, logWriter: io.Discard}) if err != nil { t.Fatalf("first run failed: %v", err) } if first.report.Restored != 2 { t.Fatalf("first restored = %d, want 2", first.report.Restored) } second, err := run(config{inputRoot: input, outputRoot: output, skipExisting: true, limit: 2, logWriter: io.Discard}) if err != nil { t.Fatalf("second run failed: %v", err) } if second.report.Restored != 2 { t.Fatalf("second restored = %d, want 2", second.report.Restored) } if second.report.Skipped != 2 { t.Fatalf("second skipped = %d, want 2", second.report.Skipped) } for _, p := range pairs { if _, err := os.Stat(filepath.Join(output, p.TaskName, pairMetaName)); err != nil { t.Fatalf("expected %s metadata to exist: %v", p.TaskName, err) } } } func TestRunCheckReportsMissingSlaveData(t *testing.T) { root := t.TempDir() input := filepath.Join(root, "bundle") writeTestFile(t, filepath.Join(input, "data", "scene_master", "m.txt"), "master") writeTestJSON(t, filepath.Join(input, pairsFileName), pairsDocument{ Pairs: []pair{{ PairID: "pair_0001", TaskName: "Task_20250101_20250113", MasterData: "data/scene_master", SlaveData: "data/scene_missing", }}, }) result, err := runCheck(config{inputRoot: input, logWriter: io.Discard}) if err != nil { t.Fatalf("runCheck failed: %v", err) } if result.report.Failed != 1 { t.Fatalf("failed = %d, want 1", result.report.Failed) } if result.report.Valid != 0 { t.Fatalf("valid = %d, want 0", result.report.Valid) } } func TestRunCheckTreatsMissingOrbitAsWarning(t *testing.T) { root := t.TempDir() input := filepath.Join(root, "bundle") writeTestFile(t, filepath.Join(input, "data", "scene_master", "m.txt"), "master") writeTestFile(t, filepath.Join(input, "data", "scene_slave", "s.txt"), "slave") writeTestJSON(t, filepath.Join(input, pairsFileName), pairsDocument{ Pairs: []pair{{ PairID: "pair_0001", TaskName: "Task_20250101_20250113", MasterData: "data/scene_master", SlaveData: "data/scene_slave", MasterOrbit: "orbit/missing.txt", }}, }) result, err := runCheck(config{inputRoot: input, logWriter: io.Discard}) if err != nil { t.Fatalf("runCheck failed: %v", err) } if result.report.Failed != 0 { t.Fatalf("failed = %d, want 0", result.report.Failed) } if result.report.Valid != 1 { t.Fatalf("valid = %d, want 1", result.report.Valid) } if len(result.report.Warnings) != 1 { t.Fatalf("warnings = %d, want 1", len(result.report.Warnings)) } } func TestRunCheckAllowsExtraPairMetadata(t *testing.T) { root := t.TempDir() input := filepath.Join(root, "bundle") writeTestFile(t, filepath.Join(input, "data", "scene_master", "m.txt"), "master") writeTestFile(t, filepath.Join(input, "data", "scene_slave", "s.txt"), "slave") writeTestFile(t, filepath.Join(input, "orbit", "master.txt"), "master orbit") writeTestFile(t, filepath.Join(input, "orbit", "slave.txt"), "slave orbit") writeTestFile(t, filepath.Join(input, pairsFileName), `{ "schema": "dinsar_source_bundle_pairs.v1", "exported_at": "2026-05-11T07:20:15Z", "pairs": [ { "pair_id": "pair_0001", "identity_key": "uid:test", "task_name": "Task_20250101_20250113", "task_alias": "Task_20250101_20250113", "pair_key": "lt1_test", "scene_pair_uid": "test", "master_data": "data/scene_master", "slave_data": "data/scene_slave", "master_orbit": "orbit/master.txt", "slave_orbit": "orbit/slave.txt", "master_source_path": "D:\\source\\master", "slave_source_path": "D:\\source\\slave", "scene_center_distance_meters": 123.4 } ] }`) result, err := runCheck(config{inputRoot: input, logWriter: io.Discard}) if err != nil { t.Fatalf("runCheck failed: %v", err) } if result.report.Failed != 0 { t.Fatalf("failed = %d, want 0; errors=%v", result.report.Failed, result.report.Errors) } if result.report.Valid != 1 { t.Fatalf("valid = %d, want 1", result.report.Valid) } } func TestParseFlagsRejectsSkipExistingWithOverwrite(t *testing.T) { _, err := parseFlags([]string{"--input", "in", "--output", "out", "--skip-existing", "--overwrite"}) if err == nil { t.Fatal("expected error") } } func TestParseFlagsAllowsOverwriteByItself(t *testing.T) { cfg, err := parseFlags([]string{"--input", "in", "--output", "out", "--overwrite"}) if err != nil { t.Fatalf("unexpected error: %v", err) } if cfg.skipExisting { t.Fatal("skipExisting = true, want false") } } func TestSafeJoinRejectsEscapingPath(t *testing.T) { if _, err := safeJoin(t.TempDir(), "../outside"); err == nil { t.Fatal("expected escaping path error") } } func writeTestFile(t *testing.T, path string, content string) { t.Helper() if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { t.Fatal(err) } if err := os.WriteFile(path, []byte(content), 0644); err != nil { t.Fatal(err) } } func writeTestJSON(t *testing.T, path string, value any) { t.Helper() if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { t.Fatal(err) } bytes, err := json.MarshalIndent(value, "", " ") if err != nil { t.Fatal(err) } if err := os.WriteFile(path, bytes, 0644); err != nil { t.Fatal(err) } }