|
4 | 4 | "encoding/json" |
5 | 5 | "os" |
6 | 6 | "path/filepath" |
| 7 | + "strings" |
7 | 8 | "testing" |
8 | 9 |
|
9 | 10 | "github.com/mertcikla/tld/cmd" |
@@ -33,6 +34,20 @@ func TestAnalyzeCmd_WatchPipelineWritesYAML(t *testing.T) { |
33 | 34 | t.Fatalf("symbol %q (%s) has no placement", element.Name, ref) |
34 | 35 | } |
35 | 36 | } |
| 37 | + |
| 38 | + stdout, stderr, err = cmd.RunCmd(t, dir, "analyze", repoDir, "--data-dir", dataDir, "--embedding-provider", "none") |
| 39 | + if err != nil { |
| 40 | + t.Fatalf("second analyze: %v\nstdout: %s\nstderr: %s", err, stdout, stderr) |
| 41 | + } |
| 42 | + ws, err = workspace.Load(dir) |
| 43 | + if err != nil { |
| 44 | + t.Fatal(err) |
| 45 | + } |
| 46 | + for _, element := range ws.Elements { |
| 47 | + if element.Kind == "file" && strings.HasPrefix(element.FilePath, ".tld/") { |
| 48 | + t.Fatalf("generated workspace YAML should not be scanned as source: %+v", element) |
| 49 | + } |
| 50 | + } |
36 | 51 | } |
37 | 52 |
|
38 | 53 | func TestAnalyzeCmd_RuntimeArtifactsUseArchitectureView(t *testing.T) { |
@@ -108,15 +123,34 @@ spec: |
108 | 123 | if err != nil { |
109 | 124 | t.Fatal(err) |
110 | 125 | } |
111 | | - if countKind(ws, "function") != 0 || countKind(ws, "file") != 0 || countKind(ws, "folder") != 0 { |
112 | | - t.Fatalf("architecture view should hide code/file noise: %+v", ws.Elements) |
| 126 | + architectureRef := refByElementName(ws, "Architecture") |
| 127 | + structuralRef := refByElementName(ws, "Structural") |
| 128 | + repositoryRef := refByKind(ws, "repository") |
| 129 | + if architectureRef == "" || structuralRef == "" || repositoryRef == "" { |
| 130 | + t.Fatalf("missing repository sections: %+v", ws.Elements) |
| 131 | + } |
| 132 | + if !hasPlacementParent(ws, architectureRef, repositoryRef) || !hasPlacementParent(ws, structuralRef, repositoryRef) { |
| 133 | + t.Fatalf("architecture and structural sections should be siblings under repository: %+v", ws.Elements) |
113 | 134 | } |
114 | 135 | for _, name := range []string{"alpha", "beta", "cache", "External traffic"} { |
115 | | - if refByElementName(ws, name) == "" { |
| 136 | + ref := refByElementNameWithParent(ws, name, architectureRef) |
| 137 | + if ref == "" { |
116 | 138 | t.Fatalf("missing architecture element %q in %+v", name, ws.Elements) |
117 | 139 | } |
118 | 140 | } |
119 | | - if !connectorByElementNames(ws, "alpha", "beta") || !connectorByElementNames(ws, "alpha", "cache") || !connectorByElementNames(ws, "External traffic", "alpha") { |
| 141 | + deployRef := refByElementName(ws, "deploy") |
| 142 | + if deployRef == "" || !hasPlacementParent(ws, deployRef, structuralRef) { |
| 143 | + t.Fatalf("top-level structural folder should be under Structural, ref=%q elements=%+v", deployRef, ws.Elements) |
| 144 | + } |
| 145 | + if ref := refByElementName(ws, "topology.yaml"); ref == "" || !hasPlacementParent(ws, ref, deployRef) { |
| 146 | + t.Fatalf("structural file should be under its folder view, ref=%q elements=%+v", ref, ws.Elements) |
| 147 | + } |
| 148 | + alphaRef := refByElementNameWithParent(ws, "alpha", architectureRef) |
| 149 | + topologyRef := refByElementName(ws, "topology.yaml") |
| 150 | + if alphaRef == "" || topologyRef == "" || !ws.Elements[alphaRef].HasView || !hasPlacementParent(ws, topologyRef, alphaRef) { |
| 151 | + t.Fatalf("bound architecture component should own a deep-dive view with structural targets: alpha=%q topology=%q %+v", alphaRef, topologyRef, ws.Elements) |
| 152 | + } |
| 153 | + if !connectorByElementNamesInParent(ws, "alpha", "beta", architectureRef) || !connectorByElementNamesInParent(ws, "alpha", "cache", architectureRef) || !connectorByElementNamesInParent(ws, "External traffic", "alpha", architectureRef) { |
120 | 154 | t.Fatalf("missing expected architecture connectors: %+v", ws.Connectors) |
121 | 155 | } |
122 | 156 | for _, connector := range ws.Connectors { |
@@ -160,8 +194,139 @@ services: |
160 | 194 | if !connectorByElementNames(ws, "worker", "api") { |
161 | 195 | t.Fatalf("expected env endpoint connector from worker to api, got %+v", ws.Connectors) |
162 | 196 | } |
163 | | - if countKind(ws, "function") != 0 { |
164 | | - t.Fatalf("architecture evidence should suppress symbol-level output, got %+v", ws.Elements) |
| 197 | + architectureRef := refByElementName(ws, "Architecture") |
| 198 | + structuralRef := refByElementName(ws, "Structural") |
| 199 | + if architectureRef == "" || structuralRef == "" { |
| 200 | + t.Fatalf("missing repository sections: %+v", ws.Elements) |
| 201 | + } |
| 202 | + for _, name := range []string{"worker", "api"} { |
| 203 | + ref := refByElementNameWithParent(ws, name, architectureRef) |
| 204 | + if ref == "" { |
| 205 | + t.Fatalf("architecture element %q should be under Architecture, ref=%q placements=%+v", name, ref, ws.Elements[ref]) |
| 206 | + } |
| 207 | + } |
| 208 | + if ref := refByElementName(ws, "Main"); ref == "" { |
| 209 | + t.Fatalf("structural symbol should still be materialized: %+v", ws.Elements) |
| 210 | + } else if !hasPlacementParent(ws, ref, refByElementName(ws, "main.go")) { |
| 211 | + t.Fatalf("symbol should remain nested under its file view: %+v", ws.Elements[ref].Placements) |
| 212 | + } |
| 213 | + if ref := refByElementName(ws, "main.go"); ref == "" || !hasPlacementParent(ws, ref, structuralRef) { |
| 214 | + t.Fatalf("top-level structural file should be under Structural, ref=%q elements=%+v", ref, ws.Elements) |
| 215 | + } |
| 216 | +} |
| 217 | + |
| 218 | +func TestAnalyzeCmd_CrossRepositoryArchitectureLinksReuseStructuralElements(t *testing.T) { |
| 219 | + dir := t.TempDir() |
| 220 | + dataDir := t.TempDir() |
| 221 | + cmd.MustInitWorkspace(t, dir) |
| 222 | + sourceRepo := filepath.Join(dir, "source-repo") |
| 223 | + cmd.InitGitRepo(t, sourceRepo, "modules/cart/main.go", "package main\nfunc ServeCart() {}\n") |
| 224 | + runtimeRepo := filepath.Join(dir, "runtime-repo") |
| 225 | + cmd.InitGitRepo(t, runtimeRepo, "deploy/cart.yaml", ` |
| 226 | +apiVersion: apps/v1 |
| 227 | +kind: Deployment |
| 228 | +metadata: |
| 229 | + name: cart |
| 230 | +spec: |
| 231 | + template: |
| 232 | + spec: |
| 233 | + containers: |
| 234 | + - name: app |
| 235 | + image: example/cart |
| 236 | + env: |
| 237 | + - name: PEER |
| 238 | + value: "cache:6379" |
| 239 | +--- |
| 240 | +apiVersion: v1 |
| 241 | +kind: Service |
| 242 | +metadata: |
| 243 | + name: cache |
| 244 | +spec: |
| 245 | + ports: |
| 246 | + - port: 6379 |
| 247 | +`) |
| 248 | + |
| 249 | + stdout, stderr, err := cmd.RunCmd(t, dir, "analyze", sourceRepo, "--data-dir", dataDir, "--embedding-provider", "none") |
| 250 | + if err != nil { |
| 251 | + t.Fatalf("analyze source: %v\nstdout: %s\nstderr: %s", err, stdout, stderr) |
| 252 | + } |
| 253 | + stdout, stderr, err = cmd.RunCmd(t, dir, "analyze", runtimeRepo, "--data-dir", dataDir, "--embedding-provider", "none") |
| 254 | + if err != nil { |
| 255 | + t.Fatalf("analyze runtime: %v\nstdout: %s\nstderr: %s", err, stdout, stderr) |
| 256 | + } |
| 257 | + ws, err := workspace.Load(dir) |
| 258 | + if err != nil { |
| 259 | + t.Fatal(err) |
| 260 | + } |
| 261 | + runtimeRef := refByElementName(ws, "runtime-repo") |
| 262 | + architectureRef := refByElementNameWithParent(ws, "Architecture", runtimeRef) |
| 263 | + cartArchRef := refByElementNameWithParent(ws, "cart", architectureRef) |
| 264 | + cartFolderRef := refByKindAndFilePath(ws, "folder", "modules/cart") |
| 265 | + if runtimeRef == "" || architectureRef == "" || cartArchRef == "" || cartFolderRef == "" { |
| 266 | + t.Fatalf("missing cross-repo test elements: runtime=%q architecture=%q cartArch=%q cartFolder=%q elements=%+v", runtimeRef, architectureRef, cartArchRef, cartFolderRef, ws.Elements) |
| 267 | + } |
| 268 | + if !ws.Elements[cartArchRef].HasView || !hasPlacementParent(ws, cartFolderRef, cartArchRef) { |
| 269 | + t.Fatalf("runtime architecture component should deep-link to source repo structural folder: arch=%+v folder=%+v", ws.Elements[cartArchRef], ws.Elements[cartFolderRef]) |
| 270 | + } |
| 271 | + if placementCount(ws, cartFolderRef) < 2 { |
| 272 | + t.Fatalf("source structural folder should remain in its original structural view and be reused in runtime deep-dive view: %+v", ws.Elements[cartFolderRef]) |
| 273 | + } |
| 274 | +} |
| 275 | + |
| 276 | +func TestAnalyzeCmd_PrunesDisconnectedArchitectureComponents(t *testing.T) { |
| 277 | + dir := t.TempDir() |
| 278 | + dataDir := t.TempDir() |
| 279 | + cmd.MustInitWorkspace(t, dir) |
| 280 | + repoDir := filepath.Join(dir, "runtime-app") |
| 281 | + cmd.InitGitRepo(t, repoDir, "main.go", "package main\nfunc Main() {}\n") |
| 282 | + writeAnalyzeTestFile(t, repoDir, "deploy/topology.yaml", ` |
| 283 | +apiVersion: apps/v1 |
| 284 | +kind: Deployment |
| 285 | +metadata: |
| 286 | + name: connected |
| 287 | +spec: |
| 288 | + template: |
| 289 | + spec: |
| 290 | + containers: |
| 291 | + - name: app |
| 292 | + image: example/connected:latest |
| 293 | + env: |
| 294 | + - name: PEER |
| 295 | + value: "target:9090" |
| 296 | +--- |
| 297 | +apiVersion: v1 |
| 298 | +kind: Service |
| 299 | +metadata: |
| 300 | + name: target |
| 301 | +spec: |
| 302 | + ports: |
| 303 | + - port: 9090 |
| 304 | +--- |
| 305 | +apiVersion: v1 |
| 306 | +kind: Service |
| 307 | +metadata: |
| 308 | + name: isolated |
| 309 | +spec: |
| 310 | + ports: |
| 311 | + - port: 9999 |
| 312 | +`) |
| 313 | + |
| 314 | + stdout, stderr, err := cmd.RunCmd(t, dir, "analyze", repoDir, "--data-dir", dataDir, "--embedding-provider", "none") |
| 315 | + if err != nil { |
| 316 | + t.Fatalf("analyze: %v\nstdout: %s\nstderr: %s", err, stdout, stderr) |
| 317 | + } |
| 318 | + ws, err := workspace.Load(dir) |
| 319 | + if err != nil { |
| 320 | + t.Fatal(err) |
| 321 | + } |
| 322 | + architectureRef := refByElementName(ws, "Architecture") |
| 323 | + for ref, element := range ws.Elements { |
| 324 | + if element.Name == "isolated" && hasPlacementParent(ws, ref, architectureRef) { |
| 325 | + t.Fatalf("disconnected architecture element should be pruned from Architecture: %+v", ws.Elements) |
| 326 | + } |
| 327 | + } |
| 328 | + if !connectorByElementNamesInParent(ws, "connected", "target", architectureRef) { |
| 329 | + t.Fatalf("expected connected architecture edge, got %+v", ws.Connectors) |
165 | 330 | } |
166 | 331 | } |
167 | 332 |
|
@@ -276,6 +441,68 @@ func refByElementName(ws *workspace.Workspace, name string) string { |
276 | 441 | return "" |
277 | 442 | } |
278 | 443 |
|
| 444 | +func refByElementNameWithParent(ws *workspace.Workspace, name, parentRef string) string { |
| 445 | + for ref, element := range ws.Elements { |
| 446 | + if element.Name == name && hasPlacementParent(ws, ref, parentRef) { |
| 447 | + return ref |
| 448 | + } |
| 449 | + } |
| 450 | + return "" |
| 451 | +} |
| 452 | + |
| 453 | +func refByKind(ws *workspace.Workspace, kind string) string { |
| 454 | + for ref, element := range ws.Elements { |
| 455 | + if element.Kind == kind { |
| 456 | + return ref |
| 457 | + } |
| 458 | + } |
| 459 | + return "" |
| 460 | +} |
| 461 | + |
| 462 | +func refByKindAndFilePath(ws *workspace.Workspace, kind, filePath string) string { |
| 463 | + for ref, element := range ws.Elements { |
| 464 | + if element.Kind == kind && element.FilePath == filePath { |
| 465 | + return ref |
| 466 | + } |
| 467 | + } |
| 468 | + return "" |
| 469 | +} |
| 470 | + |
| 471 | +func hasPlacementParent(ws *workspace.Workspace, ref, parentRef string) bool { |
| 472 | + element := ws.Elements[ref] |
| 473 | + if element == nil { |
| 474 | + return false |
| 475 | + } |
| 476 | + for _, placement := range element.Placements { |
| 477 | + if placement.ParentRef == parentRef { |
| 478 | + return true |
| 479 | + } |
| 480 | + } |
| 481 | + return false |
| 482 | +} |
| 483 | + |
| 484 | +func placementCount(ws *workspace.Workspace, ref string) int { |
| 485 | + element := ws.Elements[ref] |
| 486 | + if element == nil { |
| 487 | + return 0 |
| 488 | + } |
| 489 | + return len(element.Placements) |
| 490 | +} |
| 491 | + |
| 492 | +func connectorByElementNamesInParent(ws *workspace.Workspace, sourceName, targetName, parentRef string) bool { |
| 493 | + sourceRef := refByElementNameWithParent(ws, sourceName, parentRef) |
| 494 | + targetRef := refByElementNameWithParent(ws, targetName, parentRef) |
| 495 | + if sourceRef == "" || targetRef == "" { |
| 496 | + return false |
| 497 | + } |
| 498 | + for _, connector := range ws.Connectors { |
| 499 | + if connector.Source == sourceRef && connector.Target == targetRef { |
| 500 | + return true |
| 501 | + } |
| 502 | + } |
| 503 | + return false |
| 504 | +} |
| 505 | + |
279 | 506 | func connectorByElementNames(ws *workspace.Workspace, sourceName, targetName string) bool { |
280 | 507 | sourceRef := refByElementName(ws, sourceName) |
281 | 508 | targetRef := refByElementName(ws, targetName) |
|
0 commit comments