Skip to content

Commit 7840a92

Browse files
BenjaminGuzmanndeloof
authored andcommitted
Added tests to viz subcommand
Signed-off-by: Benjamín Guzmán <[email protected]>
1 parent 3751c30 commit 7840a92

File tree

8 files changed

+478
-108
lines changed

8 files changed

+478
-108
lines changed

cmd/compose/alpha.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func alphaCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
3434
cmd.AddCommand(
3535
watchCommand(p, backend),
3636
dryRunRedirectCommand(p),
37-
vizCommand(p),
37+
vizCommand(p, backend),
3838
)
3939
return cmd
4040
}

cmd/compose/viz.go

Lines changed: 11 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,9 @@ import (
2020
"context"
2121
"fmt"
2222
"os"
23-
"strconv"
2423
"strings"
2524

26-
"github.com/compose-spec/compose-go/types"
25+
"github.com/docker/compose/v2/pkg/api"
2726
"github.com/spf13/cobra"
2827
)
2928

@@ -35,10 +34,7 @@ type vizOptions struct {
3534
indentationStr string
3635
}
3736

38-
// maps a service with the services it depends on
39-
type vizGraph map[*types.ServiceConfig][]*types.ServiceConfig
40-
41-
func vizCommand(p *ProjectOptions) *cobra.Command {
37+
func vizCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
4238
opts := vizOptions{
4339
ProjectOptions: p,
4440
}
@@ -54,7 +50,7 @@ func vizCommand(p *ProjectOptions) *cobra.Command {
5450
return err
5551
}),
5652
RunE: Adapt(func(ctx context.Context, args []string) error {
57-
return runViz(ctx, &opts)
53+
return runViz(ctx, backend, &opts)
5854
}),
5955
}
6056

@@ -66,118 +62,26 @@ func vizCommand(p *ProjectOptions) *cobra.Command {
6662
return cmd
6763
}
6864

69-
func runViz(_ context.Context, opts *vizOptions) error {
65+
func runViz(ctx context.Context, backend api.Service, opts *vizOptions) error {
7066
_, _ = fmt.Fprintln(os.Stderr, "viz command is EXPERIMENTAL")
7167
project, err := opts.ToProject(nil)
7268
if err != nil {
7369
return err
7470
}
7571

7672
// build graph
77-
graph := make(vizGraph)
78-
for i, serviceConfig := range project.Services {
79-
serviceConfigPtr := &project.Services[i]
80-
graph[serviceConfigPtr] = make([]*types.ServiceConfig, 0, len(serviceConfig.DependsOn))
81-
for dependencyName := range serviceConfig.DependsOn {
82-
// no error should be returned since dependencyName should exist
83-
dependency, _ := project.GetService(dependencyName)
84-
graph[serviceConfigPtr] = append(graph[serviceConfigPtr], &dependency)
85-
}
86-
}
73+
graphStr, _ := backend.Viz(ctx, project, api.VizOptions{
74+
IncludeNetworks: opts.includeNetworks,
75+
IncludePorts: opts.includePorts,
76+
IncludeImageName: opts.includeImageName,
77+
Indentation: opts.indentationStr,
78+
})
8779

88-
// build graphviz graph
89-
var graphBuilder strings.Builder
90-
graphBuilder.WriteString("digraph " + project.Name + " {\n")
91-
graphBuilder.WriteString(opts.indentationStr + "layout=dot;\n")
92-
addNodes(&graphBuilder, graph, opts)
93-
graphBuilder.WriteByte('\n')
94-
addEdges(&graphBuilder, graph, opts)
95-
graphBuilder.WriteString("}\n")
96-
97-
fmt.Println(graphBuilder.String())
80+
fmt.Println(graphStr)
9881

9982
return nil
10083
}
10184

102-
// addNodes adds the corresponding graphviz representation of all the nodes in the given graph to the graphBuilder
103-
// returns the same graphBuilder
104-
func addNodes(graphBuilder *strings.Builder, graph vizGraph, opts *vizOptions) *strings.Builder {
105-
for serviceNode := range graph {
106-
// write:
107-
// "service name" [style="filled" label<<font point-size="15">service name</font>
108-
graphBuilder.WriteString(opts.indentationStr)
109-
writeQuoted(graphBuilder, serviceNode.Name)
110-
graphBuilder.WriteString(" [style=\"filled\" label=<<font point-size=\"15\">")
111-
graphBuilder.WriteString(serviceNode.Name)
112-
graphBuilder.WriteString("</font>")
113-
114-
if opts.includeNetworks && len(serviceNode.Networks) > 0 {
115-
graphBuilder.WriteString("<font point-size=\"10\">")
116-
graphBuilder.WriteString("<br/><br/><b>Networks:</b>")
117-
for _, networkName := range serviceNode.NetworksByPriority() {
118-
graphBuilder.WriteString("<br/>")
119-
graphBuilder.WriteString(networkName)
120-
}
121-
graphBuilder.WriteString("</font>")
122-
}
123-
124-
if opts.includePorts && len(serviceNode.Ports) > 0 {
125-
graphBuilder.WriteString("<font point-size=\"10\">")
126-
graphBuilder.WriteString("<br/><br/><b>Ports:</b>")
127-
for _, portConfig := range serviceNode.Ports {
128-
graphBuilder.WriteString("<br/>")
129-
if len(portConfig.HostIP) > 0 {
130-
graphBuilder.WriteString(portConfig.HostIP)
131-
graphBuilder.WriteByte(':')
132-
}
133-
graphBuilder.WriteString(portConfig.Published)
134-
graphBuilder.WriteByte(':')
135-
graphBuilder.WriteString(strconv.Itoa(int(portConfig.Target)))
136-
graphBuilder.WriteString(" (")
137-
graphBuilder.WriteString(portConfig.Protocol)
138-
graphBuilder.WriteString(", ")
139-
graphBuilder.WriteString(portConfig.Mode)
140-
graphBuilder.WriteString(")")
141-
}
142-
graphBuilder.WriteString("</font>")
143-
}
144-
145-
if opts.includeImageName {
146-
graphBuilder.WriteString("<font point-size=\"10\">")
147-
graphBuilder.WriteString("<br/><br/><b>Image:</b><br/>")
148-
graphBuilder.WriteString(serviceNode.Image)
149-
graphBuilder.WriteString("</font>")
150-
}
151-
152-
graphBuilder.WriteString(">];\n")
153-
}
154-
155-
return graphBuilder
156-
}
157-
158-
// addEdges adds the corresponding graphviz representation of all edges in the given graph to the graphBuilder
159-
// returns the same graphBuilder
160-
func addEdges(graphBuilder *strings.Builder, graph vizGraph, opts *vizOptions) *strings.Builder {
161-
for parent, children := range graph {
162-
for _, child := range children {
163-
graphBuilder.WriteString(opts.indentationStr)
164-
writeQuoted(graphBuilder, parent.Name)
165-
graphBuilder.WriteString(" -> ")
166-
writeQuoted(graphBuilder, child.Name)
167-
graphBuilder.WriteString(";\n")
168-
}
169-
}
170-
171-
return graphBuilder
172-
}
173-
174-
// writeQuoted writes "str" to builder
175-
func writeQuoted(builder *strings.Builder, str string) {
176-
builder.WriteByte('"')
177-
builder.WriteString(str)
178-
builder.WriteByte('"')
179-
}
180-
18185
// preferredIndentationStr returns a single string given the indentation preference
18286
func preferredIndentationStr(size int, useSpace bool) (string, error) {
18387
if size < 0 {

cmd/compose/viz_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
Copyright 2020 Docker Compose CLI authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package compose
18+
19+
import (
20+
"fmt"
21+
"testing"
22+
23+
"github.com/stretchr/testify/assert"
24+
)
25+
26+
func TestPreferredIndentationStr(t *testing.T) {
27+
type args struct {
28+
size int
29+
useSpace bool
30+
}
31+
tests := []struct {
32+
name string
33+
args args
34+
want string
35+
wantErr bool
36+
}{
37+
{
38+
name: "should return '\\t\\t'",
39+
args: args{
40+
size: 2,
41+
useSpace: false,
42+
},
43+
want: "\t\t",
44+
wantErr: false,
45+
},
46+
{
47+
name: "should return ' '",
48+
args: args{
49+
size: 4,
50+
useSpace: true,
51+
},
52+
want: " ",
53+
wantErr: false,
54+
},
55+
{
56+
name: "should return ''",
57+
args: args{
58+
size: 0,
59+
useSpace: false,
60+
},
61+
want: "",
62+
wantErr: false,
63+
},
64+
{
65+
name: "should return ''",
66+
args: args{
67+
size: 0,
68+
useSpace: true,
69+
},
70+
want: "",
71+
wantErr: false,
72+
},
73+
{
74+
name: "should throw error because indentation size < 0",
75+
args: args{
76+
size: -1,
77+
useSpace: false,
78+
},
79+
want: "",
80+
wantErr: true,
81+
},
82+
}
83+
for _, tt := range tests {
84+
t.Run(tt.name, func(t *testing.T) {
85+
got, err := preferredIndentationStr(tt.args.size, tt.args.useSpace)
86+
if tt.wantErr && assert.NotNilf(t, err, fmt.Sprintf("preferredIndentationStr(%v, %v)", tt.args.size, tt.args.useSpace)) {
87+
return
88+
}
89+
assert.Equalf(t, tt.want, got, "preferredIndentationStr(%v, %v)", tt.args.size, tt.args.useSpace)
90+
})
91+
}
92+
}

pkg/api/api.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,19 @@ type Service interface {
8282
DryRunMode(ctx context.Context, dryRun bool) (context.Context, error)
8383
// Watch services' development context and sync/notify/rebuild/restart on changes
8484
Watch(ctx context.Context, project *types.Project, services []string, options WatchOptions) error
85+
// Viz generates a graphviz graph of the project services
86+
Viz(ctx context.Context, project *types.Project, options VizOptions) (string, error)
87+
}
88+
89+
type VizOptions struct {
90+
// IncludeNetworks if true, network names a container is attached to should appear in the graph node
91+
IncludeNetworks bool
92+
// IncludePorts if true, ports a container exposes should appear in the graph node
93+
IncludePorts bool
94+
// IncludeImageName if true, name of the image used to create a container should appear in the graph node
95+
IncludeImageName bool
96+
// Indentation string to be used to indent graphviz code, e.g. "\t", " "
97+
Indentation string
8598
}
8699

87100
// WatchOptions group options of the Watch API

pkg/api/proxy.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ type ServiceProxy struct {
5353
WatchFn func(ctx context.Context, project *types.Project, services []string, options WatchOptions) error
5454
MaxConcurrencyFn func(parallel int)
5555
DryRunModeFn func(ctx context.Context, dryRun bool) (context.Context, error)
56+
VizFn func(ctx context.Context, project *types.Project, options VizOptions) (string, error)
5657
interceptors []Interceptor
5758
}
5859

@@ -93,6 +94,7 @@ func (s *ServiceProxy) WithService(service Service) *ServiceProxy {
9394
s.WatchFn = service.Watch
9495
s.MaxConcurrencyFn = service.MaxConcurrency
9596
s.DryRunModeFn = service.DryRunMode
97+
s.VizFn = service.Viz
9698
return s
9799
}
98100

@@ -323,6 +325,14 @@ func (s *ServiceProxy) Watch(ctx context.Context, project *types.Project, servic
323325
return s.WatchFn(ctx, project, services, options)
324326
}
325327

328+
// Viz implements Viz interface
329+
func (s *ServiceProxy) Viz(ctx context.Context, project *types.Project, options VizOptions) (string, error) {
330+
if s.VizFn == nil {
331+
return "", ErrNotImplemented
332+
}
333+
return s.VizFn(ctx, project, options)
334+
}
335+
326336
func (s *ServiceProxy) MaxConcurrency(i int) {
327337
s.MaxConcurrencyFn(i)
328338
}

0 commit comments

Comments
 (0)