| // Copyright 2026 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package project |
| |
| import ( |
| "errors" |
| "reflect" |
| "sync" |
| "testing" |
| |
| "go.fuchsia.dev/jiri" |
| ) |
| |
| func TestGroupProjectsByDepth(t *testing.T) { |
| rootPath := "/root" |
| projects := []Project{ |
| {Name: "p1", Path: "/root/p1"}, |
| {Name: "p2", Path: "/root/p1/p2"}, |
| {Name: "p3", Path: "/root/p3"}, |
| {Name: "p4", Path: "/root/p3/p4/p5"}, |
| } |
| |
| groups, depths, err := groupProjectsByDepth(projects, rootPath) |
| if err != nil { |
| t.Fatalf("groupProjectsByDepth failed: %v", err) |
| } |
| |
| expectedDepths := []int{1, 2, 3} |
| if !reflect.DeepEqual(depths, expectedDepths) { |
| t.Errorf("Expected depths %v, got %v", expectedDepths, depths) |
| } |
| |
| expectedGroups := map[int][]Project{ |
| 1: { |
| {Name: "p1", Path: "/root/p1"}, |
| {Name: "p3", Path: "/root/p3"}, |
| }, |
| 2: { |
| {Name: "p2", Path: "/root/p1/p2"}, |
| }, |
| 3: { |
| {Name: "p4", Path: "/root/p3/p4/p5"}, |
| }, |
| } |
| |
| if len(groups) != len(expectedGroups) { |
| t.Errorf("Expected %d groups, got %d", len(expectedGroups), len(groups)) |
| } |
| |
| for depth, expectedProjs := range expectedGroups { |
| projs, ok := groups[depth] |
| if !ok { |
| t.Errorf("Expected group for depth %d, but none found", depth) |
| continue |
| } |
| if len(projs) != len(expectedProjs) { |
| t.Errorf("Expected %d projects for depth %d, got %d", len(expectedProjs), depth, len(projs)) |
| continue |
| } |
| for i, expectedProj := range expectedProjs { |
| if projs[i].Name != expectedProj.Name || projs[i].Path != expectedProj.Path { |
| t.Errorf("Expected project %v at index %d of depth %d, got %v", expectedProj, i, depth, projs[i]) |
| } |
| } |
| } |
| } |
| |
| func TestRunProjectsParallel(t *testing.T) { |
| jirix := &jiri.X{} |
| jirix.Jobs = 2 |
| |
| projects := []Project{ |
| {Name: "p1"}, |
| {Name: "p2"}, |
| {Name: "p3"}, |
| } |
| |
| // Test successful run |
| var mu sync.Mutex |
| var processed []string |
| err := runProjectsParallel(jirix, projects, func(p Project) error { |
| mu.Lock() |
| processed = append(processed, p.Name) |
| mu.Unlock() |
| return nil |
| }) |
| if err != nil { |
| t.Fatalf("runProjectsParallel failed: %v", err) |
| } |
| |
| if len(processed) != 3 { |
| t.Errorf("Expected 3 projects to be processed, got %v", processed) |
| } |
| |
| // Test error collection |
| err = runProjectsParallel(jirix, projects, func(p Project) error { |
| if p.Name == "p2" || p.Name == "p3" { |
| return errors.New("error: " + p.Name) |
| } |
| return nil |
| }) |
| if err == nil { |
| t.Fatalf("Expected runProjectsParallel to fail, but it succeeded") |
| } |
| |
| errMsg := err.Error() |
| if errMsg != "error: p2\nerror: p3" && errMsg != "error: p3\nerror: p2" { |
| t.Errorf("Unexpected error message: %q", errMsg) |
| } |
| } |
| |
| func TestRunProjectsParallelWithDepth(t *testing.T) { |
| jirix := &jiri.X{} |
| jirix.Jobs = 2 |
| |
| rootPath := "/root" |
| projects := []Project{ |
| {Name: "p1", Path: "/root/p1"}, |
| {Name: "p2", Path: "/root/p1/p2"}, |
| {Name: "p3", Path: "/root/p3"}, |
| {Name: "p4", Path: "/root/p3/p4/p5"}, |
| } |
| |
| // Test normal order (shallower to deeper) |
| var mu sync.Mutex |
| var processed []string |
| err := runProjectsParallelWithDepth(jirix, projects, rootPath, false /*reverse*/, func(p Project) error { |
| mu.Lock() |
| processed = append(processed, p.Name) |
| mu.Unlock() |
| return nil |
| }) |
| if err != nil { |
| t.Fatalf("runProjectsParallelWithDepth failed: %v", err) |
| } |
| |
| // We expect p1 and p3 (depth 1) to be processed before p2 (depth 2), which is processed before p4 (depth 3). |
| p1Index, p2Index, p3Index, p4Index := -1, -1, -1, -1 |
| for i, name := range processed { |
| switch name { |
| case "p1": |
| p1Index = i |
| case "p2": |
| p2Index = i |
| case "p3": |
| p3Index = i |
| case "p4": |
| p4Index = i |
| } |
| } |
| |
| if p1Index > p2Index || p3Index > p2Index { |
| t.Errorf("Expected p1/p3 to be processed before p2. Order: %v", processed) |
| } |
| if p2Index > p4Index { |
| t.Errorf("Expected p2 to be processed before p4. Order: %v", processed) |
| } |
| |
| // Test reverse order (deeper to shallower) |
| processed = nil |
| err = runProjectsParallelWithDepth(jirix, projects, rootPath, true /*reverse*/, func(p Project) error { |
| mu.Lock() |
| processed = append(processed, p.Name) |
| mu.Unlock() |
| return nil |
| }) |
| if err != nil { |
| t.Fatalf("runProjectsParallelWithDepth failed: %v", err) |
| } |
| |
| // We expect p4 (depth 3) to be processed before p2 (depth 2), which is processed before p1/p3 (depth 1). |
| for i, name := range processed { |
| switch name { |
| case "p1": |
| p1Index = i |
| case "p2": |
| p2Index = i |
| case "p3": |
| p3Index = i |
| case "p4": |
| p4Index = i |
| } |
| } |
| |
| if p4Index > p2Index { |
| t.Errorf("Expected p4 to be processed before p2 (reverse). Order: %v", processed) |
| } |
| if p2Index > p1Index || p2Index > p3Index { |
| t.Errorf("Expected p2 to be processed before p1/p3 (reverse). Order: %v", processed) |
| } |
| } |