Skip to content

Commit e1c924b

Browse files
[Crane: crane-migration-python-to-go-full-apm-cli-rewrite] Iteration 6: Port internal/deps -- dependency graph and lockfile types
Ported core data structures from src/apm_cli/deps/dependency_graph.py and src/apm_cli/deps/lockfile.py into internal/deps/: - DependencyNode, CircularRef, ConflictInfo, FlatDependencyMap, DependencyTree, DependencyGraph - LockedDependency (with to_dict/from_dict parity, port validation, sorted fields) - InstalledPackage 15 new TestParity* tests added. parity_passing: 75 -> 90, score 0.2483 -> 0.2980. Run: https://github.com/githubnext/apm/actions/runs/26290002076 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 2b76b0b commit e1c924b

3 files changed

Lines changed: 758 additions & 0 deletions

File tree

internal/deps/deps_test.go

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
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

Comments
 (0)