v0.2.0: semaphore timeouts, error logging, dead code removal, parallel exports

Critical:
- Replace DISPATCH_TIME_FOREVER with 120s/30s timeouts in ObjC
- Log failed asset IDs and error messages in cmdExport/backupTree
- Show failed count in export summaries

Cleanup:
- Remove legacy Bridge methods (ExportAlbumPreviews, ExportAlbumOriginals, BackupAll)
- Remove legacy ObjC functions and C stub equivalents
- Remove photos.go delegates (package-level pass-throughs)
- Remove InterpretExportResult (only used by legacy methods)
- Clean up mockBridge fields (rename Fn2 -> Fn)
- Fix rc race condition in main_main.go (atomic.Int32)
- Remove unused variables (_ = grandTotal, _ = sig)

Design:
- Fix resolveAlbumID: ListAlbums first (cheap), then direct ID
- Unify Cloud type: Asset.Cloud string (was bool)
- Extract shared export logic into exportAssets/exportOne
- Add worker pool for parallel exports (3 workers when assets >= 4)
- Fix backupTree progress bar counter and directory prefix

Robustness:
- Add nil checks for stringWithUTF8String: in ObjC
- Log directory creation errors in ensure_directory (ObjC)

Quality:
- Add go vet and -race flag to Makefile test target
- Add ADR for performSelector cloudIdentifier decision
- Add sync comments between Go/ObjC sanitizePathComponent
- Add package-level doc comment
- Add tests: partial failure, skipped album, album-not-found message
This commit is contained in:
Ein Anderssono
2026-06-11 21:12:47 +02:00
parent b460c68641
commit 85eaa3ea37
14 changed files with 274 additions and 651 deletions
-164
View File
@@ -209,45 +209,6 @@ func TestParseTreeJSON(t *testing.T) {
}
}
func TestInterpretExportResult(t *testing.T) {
tests := []struct {
name string
rc int
wantN int
wantErr bool
errMsg string
}{
{name: "success", rc: 5, wantN: 5},
{name: "zero exports", rc: 0, wantN: 0},
{name: "single export", rc: 1, wantN: 1},
{name: "invalid args", rc: -1, wantErr: true, errMsg: "invalid arguments or directory creation failed"},
{name: "mkdir failed", rc: -2, wantErr: true, errMsg: "could not create output directory"},
{name: "album not found", rc: -3, wantErr: true, errMsg: "album not found"},
{name: "all exports failed", rc: -4, wantErr: true, errMsg: "all exports failed"},
{name: "cancelled", rc: -5, wantErr: true, errMsg: "cancelled"},
{name: "unknown error", rc: -99, wantErr: true, errMsg: "unknown error (code -99)"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
n, err := InterpretExportResult(tt.rc)
if (err != nil) != tt.wantErr {
t.Errorf("InterpretExportResult() error = %v, wantErr %v", err, tt.wantErr)
return
}
if tt.wantErr {
if err.Error() != tt.errMsg {
t.Errorf("InterpretExportResult() error = %q, want %q", err.Error(), tt.errMsg)
}
return
}
if n != tt.wantN {
t.Errorf("InterpretExportResult() = %d, want %d", n, tt.wantN)
}
})
}
}
func TestAlbumsResponseUnmarshal(t *testing.T) {
raw := `{"albums":[{"id":"x","title":"Y"}]}`
var resp AlbumsResponse
@@ -299,68 +260,6 @@ func TestCgoBridgeImplementsBridge(t *testing.T) {
var _ Bridge = &CgoBridge{}
}
func TestPackageLevelDelegatesToDefaultBridge(t *testing.T) {
SetTestAccessRC(0)
SetTestAlbumsJSON(`{"albums":[{"id":"p1","title":"PAlbum"}]}`)
SetTestAssetsJSON(`{"assets":[{"id":"pa1","filename":"IMG_1001.JPG"}],"total":1}`)
SetTestTreeJSON(`{"collections":[{"id":"f1","name":"Trips","kind":"folder","children":[{"id":"a1","name":"Venice","kind":"album"}]}]}`)
SetTestExportRC(3)
SetTestExportOriginalsRC(2)
SetTestBackupAllRC(5)
if err := RequestAccess(); err != nil {
t.Fatal(err)
}
albums, err := ListAlbums()
if err != nil {
t.Fatal(err)
}
if len(albums) != 1 || albums[0].ID != "p1" {
t.Errorf("got %+v", albums)
}
assets, _, err := ListAssets("p1")
if err != nil {
t.Fatal(err)
}
if len(assets) != 1 || assets[0].ID != "pa1" || assets[0].Filename != "IMG_1001.JPG" {
t.Errorf("got %+v", assets)
}
tree, err := ListTree()
if err != nil {
t.Fatal(err)
}
if len(tree) != 1 || tree[0].ID != "f1" || tree[0].Name != "Trips" || len(tree[0].Children) != 1 || tree[0].Children[0].ID != "a1" || tree[0].Children[0].Name != "Venice" {
t.Errorf("got %+v", tree)
}
n, err := ExportAlbumPreviews("p1", "/tmp/x", 1024)
if err != nil {
t.Fatal(err)
}
if n != 3 {
t.Errorf("got %d", n)
}
n, err = ExportAlbumOriginals("p1", "/tmp/x")
if err != nil {
t.Fatal(err)
}
if n != 2 {
t.Errorf("got %d", n)
}
n, err = BackupAll("/tmp/x", 1024, false)
if err != nil {
t.Fatal(err)
}
if n != 5 {
t.Errorf("got %d", n)
}
}
func TestCgoBridgeListAlbumsViaStub(t *testing.T) {
SetTestAlbumsJSON(`{"albums":[{"id":"abc","title":"TestAlbum"}]}`)
bridge := &CgoBridge{}
@@ -425,69 +324,6 @@ func TestCgoBridgeListTreeNil(t *testing.T) {
}
}
func TestCgoBridgeExportViaStub(t *testing.T) {
SetTestExportRC(7)
bridge := &CgoBridge{}
n, err := bridge.ExportAlbumPreviews("id", "/tmp/out", 512)
if err != nil {
t.Fatal(err)
}
if n != 7 {
t.Errorf("got %d", n)
}
}
func TestCgoBridgeExportErrorViaStub(t *testing.T) {
SetTestExportRC(-3)
bridge := &CgoBridge{}
_, err := bridge.ExportAlbumPreviews("id", "/tmp/out", 512)
if err == nil || err.Error() != "album not found" {
t.Errorf("got %v", err)
}
}
func TestCgoBridgeExportOriginalsViaStub(t *testing.T) {
SetTestExportOriginalsRC(6)
bridge := &CgoBridge{}
n, err := bridge.ExportAlbumOriginals("id", "/tmp/out")
if err != nil {
t.Fatal(err)
}
if n != 6 {
t.Errorf("got %d", n)
}
}
func TestCgoBridgeExportOriginalsErrorViaStub(t *testing.T) {
SetTestExportOriginalsRC(-4)
bridge := &CgoBridge{}
_, err := bridge.ExportAlbumOriginals("id", "/tmp/out")
if err == nil || err.Error() != "all exports failed" {
t.Errorf("got %v", err)
}
}
func TestCgoBridgeBackupAllViaStub(t *testing.T) {
SetTestBackupAllRC(8)
bridge := &CgoBridge{}
n, err := bridge.BackupAll("/tmp/out", 1024, false)
if err != nil {
t.Fatal(err)
}
if n != 8 {
t.Errorf("got %d", n)
}
}
func TestCgoBridgeBackupAllErrorViaStub(t *testing.T) {
SetTestBackupAllRC(-2)
bridge := &CgoBridge{}
_, err := bridge.BackupAll("/tmp/out", 1024, true)
if err == nil || err.Error() != "could not create output directory" {
t.Errorf("got %v", err)
}
}
func TestCgoBridgeRequestAccessGranted(t *testing.T) {
SetTestAccessRC(0)
bridge := &CgoBridge{}