|
| 1 | +package deps |
| 2 | + |
| 3 | +import ( |
| 4 | + "strings" |
| 5 | + "testing" |
| 6 | +) |
| 7 | + |
| 8 | +// TestParityDependencyNodeGetID mirrors DependencyNode.get_id() parity. |
| 9 | +func TestParityDependencyNodeGetID(t *testing.T) { |
| 10 | + n := &DependencyNode{RepoURL: "https://github.com/owner/repo"} |
| 11 | + if got := n.GetID(); got != "https://github.com/owner/repo" { |
| 12 | + t.Errorf("GetID no-ref: got %q", got) |
| 13 | + } |
| 14 | + |
| 15 | + n2 := &DependencyNode{RepoURL: "https://github.com/owner/repo", Reference: "main"} |
| 16 | + if got := n2.GetID(); got != "https://github.com/owner/repo#main" { |
| 17 | + t.Errorf("GetID with-ref: got %q", got) |
| 18 | + } |
| 19 | +} |
| 20 | + |
| 21 | +// TestParityDependencyNodeAncestorChain mirrors DependencyNode.get_ancestor_chain(). |
| 22 | +func TestParityDependencyNodeAncestorChain(t *testing.T) { |
| 23 | + root := &DependencyNode{RepoURL: "root"} |
| 24 | + mid := &DependencyNode{RepoURL: "mid", Parent: root} |
| 25 | + leaf := &DependencyNode{RepoURL: "leaf", Parent: mid} |
| 26 | + |
| 27 | + if got := leaf.GetAncestorChain(); got != "root > mid > leaf" { |
| 28 | + t.Errorf("ancestor chain: got %q", got) |
| 29 | + } |
| 30 | +} |
| 31 | + |
| 32 | +// TestParityCircularRefString mirrors CircularRef.__str__(). |
| 33 | +func TestParityCircularRefString(t *testing.T) { |
| 34 | + cr := &CircularRef{CyclePath: []string{"a", "b", "c"}, DetectedAtDepth: 2} |
| 35 | + got := cr.String() |
| 36 | + if !strings.Contains(got, "a -> b -> c -> a") { |
| 37 | + t.Errorf("circular ref string: got %q", got) |
| 38 | + } |
| 39 | + |
| 40 | + // Single element -- no arrow appended |
| 41 | + cr2 := &CircularRef{CyclePath: []string{"a"}} |
| 42 | + got2 := cr2.String() |
| 43 | + if !strings.Contains(got2, "a") { |
| 44 | + t.Errorf("single circular ref: got %q", got2) |
| 45 | + } |
| 46 | + |
| 47 | + // Empty |
| 48 | + cr3 := &CircularRef{} |
| 49 | + got3 := cr3.String() |
| 50 | + if !strings.Contains(got3, "empty path") { |
| 51 | + t.Errorf("empty circular ref: got %q", got3) |
| 52 | + } |
| 53 | +} |
| 54 | + |
| 55 | +// TestParityFlatDependencyMapFirstWins verifies first-wins conflict semantics. |
| 56 | +func TestParityFlatDependencyMapFirstWins(t *testing.T) { |
| 57 | + f := NewFlatDependencyMap() |
| 58 | + f.AddDependency("https://github.com/owner/repo", "main") |
| 59 | + f.AddDependency("https://github.com/owner/repo", "v1.0") // should not overwrite |
| 60 | + |
| 61 | + if got := f.Dependencies["https://github.com/owner/repo"]; got != "main" { |
| 62 | + t.Errorf("first-wins: expected main, got %q", got) |
| 63 | + } |
| 64 | + if f.TotalDependencies() != 1 { |
| 65 | + t.Errorf("total: expected 1, got %d", f.TotalDependencies()) |
| 66 | + } |
| 67 | +} |
| 68 | + |
| 69 | +// TestParityFlatDependencyMapInstallOrder mirrors install_order list. |
| 70 | +func TestParityFlatDependencyMapInstallOrder(t *testing.T) { |
| 71 | + f := NewFlatDependencyMap() |
| 72 | + f.AddDependency("a", "main") |
| 73 | + f.AddDependency("b", "main") |
| 74 | + f.AddDependency("c", "main") |
| 75 | + |
| 76 | + order := f.GetInstallationList() |
| 77 | + if len(order) != 3 || order[0] != "a" || order[1] != "b" || order[2] != "c" { |
| 78 | + t.Errorf("install order: got %v", order) |
| 79 | + } |
| 80 | +} |
| 81 | + |
| 82 | +// TestParityDependencyTreeMaxDepth mirrors DependencyTree.max_depth. |
| 83 | +func TestParityDependencyTreeMaxDepth(t *testing.T) { |
| 84 | + tree := NewDependencyTree("root") |
| 85 | + tree.AddNode(&DependencyNode{RepoURL: "a", Depth: 1}) |
| 86 | + tree.AddNode(&DependencyNode{RepoURL: "b", Depth: 3}) |
| 87 | + tree.AddNode(&DependencyNode{RepoURL: "c", Depth: 2}) |
| 88 | + |
| 89 | + if tree.MaxDepth != 3 { |
| 90 | + t.Errorf("max_depth: expected 3, got %d", tree.MaxDepth) |
| 91 | + } |
| 92 | +} |
| 93 | + |
| 94 | +// TestParityDependencyTreeHasDependency mirrors DependencyTree.has_dependency(). |
| 95 | +func TestParityDependencyTreeHasDependency(t *testing.T) { |
| 96 | + tree := NewDependencyTree("root") |
| 97 | + tree.AddNode(&DependencyNode{RepoURL: "https://github.com/owner/repo", Depth: 1}) |
| 98 | + |
| 99 | + if !tree.HasDependency("https://github.com/owner/repo") { |
| 100 | + t.Error("should find existing dep") |
| 101 | + } |
| 102 | + if tree.HasDependency("https://github.com/other/repo") { |
| 103 | + t.Error("should not find missing dep") |
| 104 | + } |
| 105 | +} |
| 106 | + |
| 107 | +// TestParityDependencyGraphIsValid mirrors DependencyGraph.is_valid(). |
| 108 | +func TestParityDependencyGraphIsValid(t *testing.T) { |
| 109 | + g := NewDependencyGraph("root") |
| 110 | + if !g.IsValid() { |
| 111 | + t.Error("empty graph should be valid") |
| 112 | + } |
| 113 | + |
| 114 | + g.AddError("some error") |
| 115 | + if g.IsValid() { |
| 116 | + t.Error("graph with errors should be invalid") |
| 117 | + } |
| 118 | + |
| 119 | + g2 := NewDependencyGraph("root2") |
| 120 | + g2.AddCircularDependency(CircularRef{CyclePath: []string{"a", "b"}}) |
| 121 | + if g2.IsValid() { |
| 122 | + t.Error("graph with circular dep should be invalid") |
| 123 | + } |
| 124 | +} |
| 125 | + |
| 126 | +// TestParityDependencyGraphSummaryKeys mirrors DependencyGraph.get_summary() keys. |
| 127 | +func TestParityDependencyGraphSummaryKeys(t *testing.T) { |
| 128 | + g := NewDependencyGraph("my-pkg") |
| 129 | + g.Flattened.AddDependency("dep1", "main") |
| 130 | + summary := g.GetSummary() |
| 131 | + |
| 132 | + expected := []string{ |
| 133 | + "root_package", "total_dependencies", "max_depth", |
| 134 | + "has_circular_dependencies", "circular_count", |
| 135 | + "has_conflicts", "conflict_count", |
| 136 | + "has_errors", "error_count", "is_valid", |
| 137 | + } |
| 138 | + for _, k := range expected { |
| 139 | + if _, ok := summary[k]; !ok { |
| 140 | + t.Errorf("summary missing key %q", k) |
| 141 | + } |
| 142 | + } |
| 143 | + if summary["root_package"] != "my-pkg" { |
| 144 | + t.Errorf("root_package: got %v", summary["root_package"]) |
| 145 | + } |
| 146 | + if summary["total_dependencies"].(int) != 1 { |
| 147 | + t.Errorf("total_dependencies: got %v", summary["total_dependencies"]) |
| 148 | + } |
| 149 | +} |
| 150 | + |
| 151 | +// TestParityLockedDependencyGetUniqueKey mirrors LockedDependency.get_unique_key(). |
| 152 | +func TestParityLockedDependencyGetUniqueKey(t *testing.T) { |
| 153 | + // Normal dep |
| 154 | + ld := &LockedDependency{RepoURL: "https://github.com/owner/repo", Depth: 1} |
| 155 | + if got := ld.GetUniqueKey(); got != "https://github.com/owner/repo" { |
| 156 | + t.Errorf("normal dep key: got %q", got) |
| 157 | + } |
| 158 | + |
| 159 | + // Local dep |
| 160 | + ld2 := &LockedDependency{RepoURL: "https://github.com/owner/repo", Source: "local", LocalPath: "./local/pkg"} |
| 161 | + if got := ld2.GetUniqueKey(); got != "./local/pkg" { |
| 162 | + t.Errorf("local dep key: got %q", got) |
| 163 | + } |
| 164 | + |
| 165 | + // Virtual dep |
| 166 | + ld3 := &LockedDependency{ |
| 167 | + RepoURL: "https://github.com/owner/mono", |
| 168 | + IsVirtual: true, |
| 169 | + VirtualPath: "packages/sub", |
| 170 | + } |
| 171 | + if got := ld3.GetUniqueKey(); got != "https://github.com/owner/mono/packages/sub" { |
| 172 | + t.Errorf("virtual dep key: got %q", got) |
| 173 | + } |
| 174 | +} |
| 175 | + |
| 176 | +// TestParityLockedDependencyToMap mirrors LockedDependency.to_dict() behavior. |
| 177 | +func TestParityLockedDependencyToMap(t *testing.T) { |
| 178 | + ld := &LockedDependency{ |
| 179 | + RepoURL: "https://github.com/owner/repo", |
| 180 | + ResolvedCommit: "abc1234", |
| 181 | + Depth: 2, |
| 182 | + IsDev: true, |
| 183 | + } |
| 184 | + m := ld.ToMap() |
| 185 | + if m["repo_url"] != "https://github.com/owner/repo" { |
| 186 | + t.Errorf("repo_url: got %v", m["repo_url"]) |
| 187 | + } |
| 188 | + if m["resolved_commit"] != "abc1234" { |
| 189 | + t.Errorf("resolved_commit: got %v", m["resolved_commit"]) |
| 190 | + } |
| 191 | + if m["depth"] != 2 { |
| 192 | + t.Errorf("depth: got %v", m["depth"]) |
| 193 | + } |
| 194 | + if m["is_dev"] != true { |
| 195 | + t.Errorf("is_dev: got %v", m["is_dev"]) |
| 196 | + } |
| 197 | + // depth==1 should be omitted (default) |
| 198 | + ld2 := &LockedDependency{RepoURL: "x", Depth: 1} |
| 199 | + m2 := ld2.ToMap() |
| 200 | + if _, ok := m2["depth"]; ok { |
| 201 | + t.Error("depth==1 should be omitted from map") |
| 202 | + } |
| 203 | +} |
| 204 | + |
| 205 | +// TestParityLockedDependencyDeployedFilesSorted mirrors sorted deployed_files. |
| 206 | +func TestParityLockedDependencyDeployedFilesSorted(t *testing.T) { |
| 207 | + ld := &LockedDependency{ |
| 208 | + RepoURL: "x", |
| 209 | + DeployedFiles: []string{"z.md", "a.md", "m.md"}, |
| 210 | + } |
| 211 | + m := ld.ToMap() |
| 212 | + files := m["deployed_files"].([]string) |
| 213 | + if files[0] != "a.md" || files[1] != "m.md" || files[2] != "z.md" { |
| 214 | + t.Errorf("deployed_files not sorted: %v", files) |
| 215 | + } |
| 216 | +} |
| 217 | + |
| 218 | +// TestParityLockedDependencyFromMap mirrors LockedDependency.from_dict(). |
| 219 | +func TestParityLockedDependencyFromMap(t *testing.T) { |
| 220 | + data := map[string]interface{}{ |
| 221 | + "repo_url": "https://github.com/owner/repo", |
| 222 | + "resolved_commit": "deadbeef", |
| 223 | + "depth": float64(2), |
| 224 | + "is_dev": true, |
| 225 | + "port": float64(7999), |
| 226 | + } |
| 227 | + ld := LockedDependencyFromMap(data) |
| 228 | + if ld.RepoURL != "https://github.com/owner/repo" { |
| 229 | + t.Errorf("repo_url: got %q", ld.RepoURL) |
| 230 | + } |
| 231 | + if ld.ResolvedCommit != "deadbeef" { |
| 232 | + t.Errorf("resolved_commit: got %q", ld.ResolvedCommit) |
| 233 | + } |
| 234 | + if ld.Depth != 2 { |
| 235 | + t.Errorf("depth: got %d", ld.Depth) |
| 236 | + } |
| 237 | + if !ld.IsDev { |
| 238 | + t.Error("is_dev should be true") |
| 239 | + } |
| 240 | + if ld.Port != 7999 { |
| 241 | + t.Errorf("port: got %d", ld.Port) |
| 242 | + } |
| 243 | +} |
| 244 | + |
| 245 | +// TestParityLockedDependencyPortValidation mirrors port range validation in from_dict(). |
| 246 | +func TestParityLockedDependencyPortValidation(t *testing.T) { |
| 247 | + // Invalid port (out of range) should be ignored |
| 248 | + data := map[string]interface{}{ |
| 249 | + "repo_url": "x", |
| 250 | + "port": float64(99999), |
| 251 | + } |
| 252 | + ld := LockedDependencyFromMap(data) |
| 253 | + if ld.Port != 0 { |
| 254 | + t.Errorf("out-of-range port should be 0, got %d", ld.Port) |
| 255 | + } |
| 256 | + |
| 257 | + // Valid port |
| 258 | + data2 := map[string]interface{}{ |
| 259 | + "repo_url": "x", |
| 260 | + "port": float64(443), |
| 261 | + } |
| 262 | + ld2 := LockedDependencyFromMap(data2) |
| 263 | + if ld2.Port != 443 { |
| 264 | + t.Errorf("valid port: got %d", ld2.Port) |
| 265 | + } |
| 266 | +} |
| 267 | + |
| 268 | +// TestParityInstalledPackageFields checks InstalledPackage fields exist. |
| 269 | +func TestParityInstalledPackageFields(t *testing.T) { |
| 270 | + ip := InstalledPackage{ |
| 271 | + RepoURL: "https://github.com/owner/repo", |
| 272 | + Reference: "main", |
| 273 | + ResolvedCommit: "abc1234", |
| 274 | + Depth: 1, |
| 275 | + IsDev: false, |
| 276 | + } |
| 277 | + if ip.RepoURL == "" { |
| 278 | + t.Error("RepoURL should be set") |
| 279 | + } |
| 280 | + if ip.Depth != 1 { |
| 281 | + t.Errorf("Depth: got %d", ip.Depth) |
| 282 | + } |
| 283 | +} |
0 commit comments