Merge pull request #1281 from gtardif/compose_unittests

More Local Compose unit tests
This commit is contained in:
Guillaume Tardif 2021-02-12 12:12:57 +01:00 committed by GitHub
commit f52bdc54d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 2129 additions and 64 deletions

View File

@ -33,12 +33,12 @@ import (
) )
// NewComposeService create a local implementation of the compose.Service API // NewComposeService create a local implementation of the compose.Service API
func NewComposeService(apiClient *client.Client) compose.Service { func NewComposeService(apiClient client.APIClient) compose.Service {
return &composeService{apiClient: apiClient} return &composeService{apiClient: apiClient}
} }
type composeService struct { type composeService struct {
apiClient *client.Client apiClient client.APIClient
} }
func (s *composeService) Up(ctx context.Context, project *types.Project, options compose.UpOptions) error { func (s *composeService) Up(ctx context.Context, project *types.Project, options compose.UpOptions) error {

View File

@ -70,20 +70,6 @@ func (containers Containers) filter(predicate containerPredicate) Containers {
return filtered return filtered
} }
// split return Containers with elements to match and those not to match predicate
func (containers Containers) split(predicate containerPredicate) (Containers, Containers) {
var right Containers
var left Containers
for _, c := range containers {
if predicate(c) {
right = append(right, c)
} else {
left = append(left, c)
}
}
return right, left
}
func (containers Containers) names() []string { func (containers Containers) names() []string {
var names []string var names []string
for _, c := range containers { for _, c := range containers {

View File

@ -64,9 +64,7 @@ func (s *composeService) Create(ctx context.Context, project *types.Project, opt
var observedState Containers var observedState Containers
observedState, err = s.apiClient.ContainerList(ctx, moby.ContainerListOptions{ observedState, err = s.apiClient.ContainerList(ctx, moby.ContainerListOptions{
Filters: filters.NewArgs( Filters: filters.NewArgs(projectFilter(project.Name)),
projectFilter(project.Name),
),
All: true, All: true,
}) })
if err != nil { if err != nil {

View File

@ -57,27 +57,23 @@ func (s *composeService) Down(ctx context.Context, projectName string, options c
} }
err = InReverseDependencyOrder(ctx, options.Project, func(c context.Context, service types.ServiceConfig) error { err = InReverseDependencyOrder(ctx, options.Project, func(c context.Context, service types.ServiceConfig) error {
serviceContainers, others := containers.split(isService(service.Name)) serviceContainers := containers.filter(isService(service.Name))
err := s.removeContainers(ctx, w, serviceContainers) err := s.removeContainers(ctx, w, serviceContainers)
containers = others
return err return err
}) })
if err != nil { if err != nil {
return err return err
} }
if options.RemoveOrphans && len(containers) > 0 { orphans := containers.filter(isNotService(options.Project.ServiceNames()...))
err := s.removeContainers(ctx, w, containers) if options.RemoveOrphans && len(orphans) > 0 {
err := s.removeContainers(ctx, w, orphans)
if err != nil { if err != nil {
return err return err
} }
} }
networks, err := s.apiClient.NetworkList(ctx, moby.NetworkListOptions{ networks, err := s.apiClient.NetworkList(ctx, moby.NetworkListOptions{Filters: filters.NewArgs(projectFilter(projectName))})
Filters: filters.NewArgs(
projectFilter(projectName),
),
})
if err != nil { if err != nil {
return err return err
} }
@ -137,13 +133,15 @@ func (s *composeService) removeContainers(ctx context.Context, w progress.Writer
return eg.Wait() return eg.Wait()
} }
func (s *composeService) projectFromContainerLabels(ctx context.Context, projectName string) (*types.Project, error) { func projectFilterListOpt(projectName string) moby.ContainerListOptions {
containers, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{ return moby.ContainerListOptions{
Filters: filters.NewArgs( Filters: filters.NewArgs(projectFilter(projectName)),
projectFilter(projectName),
),
All: true, All: true,
}) }
}
func (s *composeService) projectFromContainerLabels(ctx context.Context, projectName string) (*types.Project, error) {
containers, err := s.apiClient.ContainerList(ctx, projectFilterListOpt(projectName))
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -0,0 +1,83 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"testing"
"github.com/golang/mock/gomock"
"gotest.tools/v3/assert"
apitypes "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/compose-cli/api/compose"
"github.com/docker/compose-cli/local/mocks"
)
func TestDown(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
api := mocks.NewMockAPIClient(mockCtrl)
tested.apiClient = api
ctx := context.Background()
api.EXPECT().ContainerList(ctx, projectFilterListOpt(testProject)).Return(
[]apitypes.Container{testContainer("service1", "123"), testContainer("service1", "456"), testContainer("service2", "789"), testContainer("service_orphan", "321")}, nil).Times(2)
api.EXPECT().ContainerStop(ctx, "123", nil).Return(nil)
api.EXPECT().ContainerStop(ctx, "456", nil).Return(nil)
api.EXPECT().ContainerStop(ctx, "789", nil).Return(nil)
api.EXPECT().ContainerRemove(ctx, "123", apitypes.ContainerRemoveOptions{Force: true}).Return(nil)
api.EXPECT().ContainerRemove(ctx, "456", apitypes.ContainerRemoveOptions{Force: true}).Return(nil)
api.EXPECT().ContainerRemove(ctx, "789", apitypes.ContainerRemoveOptions{Force: true}).Return(nil)
api.EXPECT().NetworkList(ctx, apitypes.NetworkListOptions{Filters: filters.NewArgs(projectFilter(testProject))}).Return([]apitypes.NetworkResource{{ID: "myProject_default"}}, nil)
api.EXPECT().NetworkRemove(ctx, "myProject_default").Return(nil)
err := tested.Down(ctx, testProject, compose.DownOptions{})
assert.NilError(t, err)
}
func TestDownRemoveOrphans(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
api := mocks.NewMockAPIClient(mockCtrl)
tested.apiClient = api
ctx := context.Background()
api.EXPECT().ContainerList(ctx, projectFilterListOpt(testProject)).Return(
[]apitypes.Container{testContainer("service1", "123"), testContainer("service2", "789"), testContainer("service_orphan", "321")}, nil).Times(2)
api.EXPECT().ContainerStop(ctx, "123", nil).Return(nil)
api.EXPECT().ContainerStop(ctx, "789", nil).Return(nil)
api.EXPECT().ContainerStop(ctx, "321", nil).Return(nil)
api.EXPECT().ContainerRemove(ctx, "123", apitypes.ContainerRemoveOptions{Force: true}).Return(nil)
api.EXPECT().ContainerRemove(ctx, "789", apitypes.ContainerRemoveOptions{Force: true}).Return(nil)
api.EXPECT().ContainerRemove(ctx, "321", apitypes.ContainerRemoveOptions{Force: true}).Return(nil)
api.EXPECT().NetworkList(ctx, apitypes.NetworkListOptions{Filters: filters.NewArgs(projectFilter(testProject))}).Return([]apitypes.NetworkResource{{ID: "myProject_default"}}, nil)
api.EXPECT().NetworkRemove(ctx, "myProject_default").Return(nil)
err := tested.Down(ctx, testProject, compose.DownOptions{RemoveOrphans: true})
assert.NilError(t, err)
}

View File

@ -0,0 +1,92 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"testing"
"github.com/golang/mock/gomock"
"gotest.tools/v3/assert"
"github.com/compose-spec/compose-go/types"
apitypes "github.com/docker/docker/api/types"
"github.com/docker/compose-cli/api/compose"
"github.com/docker/compose-cli/local/mocks"
)
const testProject = "testProject"
var tested = composeService{}
func TestKillAll(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
api := mocks.NewMockAPIClient(mockCtrl)
tested.apiClient = api
project := types.Project{Name: testProject, Services: []types.ServiceConfig{testService("service1"), testService("service2")}}
ctx := context.Background()
api.EXPECT().ContainerList(ctx, projectFilterListOpt(testProject)).Return(
[]apitypes.Container{testContainer("service1", "123"), testContainer("service1", "456"), testContainer("service2", "789")}, nil)
api.EXPECT().ContainerKill(anyCancellableContext(), "123", "").Return(nil)
api.EXPECT().ContainerKill(anyCancellableContext(), "456", "").Return(nil)
api.EXPECT().ContainerKill(anyCancellableContext(), "789", "").Return(nil)
err := tested.Kill(ctx, &project, compose.KillOptions{})
assert.NilError(t, err)
}
func TestKillSignal(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
api := mocks.NewMockAPIClient(mockCtrl)
tested.apiClient = api
project := types.Project{Name: testProject, Services: []types.ServiceConfig{testService("service1")}}
ctx := context.Background()
api.EXPECT().ContainerList(ctx, projectFilterListOpt(testProject)).Return([]apitypes.Container{testContainer("service1", "123")}, nil)
api.EXPECT().ContainerKill(anyCancellableContext(), "123", "SIGTERM").Return(nil)
err := tested.Kill(ctx, &project, compose.KillOptions{Signal: "SIGTERM"})
assert.NilError(t, err)
}
func testService(name string) types.ServiceConfig {
return types.ServiceConfig{Name: name}
}
func testContainer(service string, id string) apitypes.Container {
return apitypes.Container{
ID: id,
Names: []string{id},
Labels: containerLabels(service),
}
}
func containerLabels(service string) map[string]string {
return map[string]string{serviceLabel: service, configFilesLabel: "testdata/docker-compose.yml", workingDirLabel: "testdata", projectLabel: testProject}
}
func anyCancellableContext() gomock.Matcher {
ctxWithCancel, cancel := context.WithCancel(context.Background())
cancel()
return gomock.AssignableToTypeOf(ctxWithCancel)
}

View File

@ -84,3 +84,23 @@ func combinedStatus(statuses []string) string {
} }
return result return result
} }
func groupContainerByLabel(containers []moby.Container, labelName string) (map[string][]moby.Container, []string, error) {
containersByLabel := map[string][]moby.Container{}
keys := []string{}
for _, c := range containers {
label, ok := c.Labels[labelName]
if !ok {
return nil, nil, fmt.Errorf("No label %q set on container %q of compose project", labelName, c.ID)
}
labelContainers, ok := containersByLabel[label]
if !ok {
labelContainers = []moby.Container{}
keys = append(keys, label)
}
labelContainers = append(labelContainers, c)
containersByLabel[label] = labelContainers
}
sort.Strings(keys)
return containersByLabel, keys, nil
}

View File

@ -19,7 +19,6 @@ package compose
import ( import (
"context" "context"
"fmt" "fmt"
"sort"
moby "github.com/docker/docker/api/types" moby "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
@ -30,9 +29,7 @@ import (
func (s *composeService) Ps(ctx context.Context, projectName string, options compose.PsOptions) ([]compose.ContainerSummary, error) { func (s *composeService) Ps(ctx context.Context, projectName string, options compose.PsOptions) ([]compose.ContainerSummary, error) {
containers, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{ containers, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{
Filters: filters.NewArgs( Filters: filters.NewArgs(projectFilter(projectName)),
projectFilter(projectName),
),
All: options.All, All: options.All,
}) })
if err != nil { if err != nil {
@ -83,23 +80,3 @@ func (s *composeService) Ps(ctx context.Context, projectName string, options com
} }
return summary, eg.Wait() return summary, eg.Wait()
} }
func groupContainerByLabel(containers []moby.Container, labelName string) (map[string][]moby.Container, []string, error) {
containersByLabel := map[string][]moby.Container{}
keys := []string{}
for _, c := range containers {
label, ok := c.Labels[labelName]
if !ok {
return nil, nil, fmt.Errorf("No label %q set on container %q of compose project", labelName, c.ID)
}
labelContainers, ok := containersByLabel[label]
if !ok {
labelContainers = []moby.Container{}
keys = append(keys, label)
}
labelContainers = append(labelContainers, c)
containersByLabel[label] = labelContainers
}
sort.Strings(keys)
return containersByLabel, keys, nil
}

94
local/compose/ps_test.go Normal file
View File

@ -0,0 +1,94 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"testing"
"github.com/golang/mock/gomock"
"gotest.tools/v3/assert"
apitypes "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/compose-cli/api/compose"
"github.com/docker/compose-cli/local/mocks"
)
func TestPs(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
api := mocks.NewMockAPIClient(mockCtrl)
tested.apiClient = api
ctx := context.Background()
listOpts := apitypes.ContainerListOptions{Filters: filters.NewArgs(projectFilter(testProject)), All: false}
c1, inspect1 := containerDetails("service1", "123", "Running", "healthy")
c2, inspect2 := containerDetails("service1", "456", "Running", "")
c2.Ports = []apitypes.Port{{PublicPort: 80, PrivatePort: 90, IP: "localhost"}}
c3, inspect3 := containerDetails("service2", "789", "Running", "")
api.EXPECT().ContainerList(ctx, listOpts).Return([]apitypes.Container{c1, c2, c3}, nil)
api.EXPECT().ContainerInspect(anyCancellableContext(), "123").Return(inspect1, nil)
api.EXPECT().ContainerInspect(anyCancellableContext(), "456").Return(inspect2, nil)
api.EXPECT().ContainerInspect(anyCancellableContext(), "789").Return(inspect3, nil)
containers, err := tested.Ps(ctx, testProject, compose.PsOptions{})
expected := []compose.ContainerSummary{
{ID: "123", Name: "123", Project: testProject, Service: "service1", State: "Running", Health: "healthy", Publishers: nil},
{ID: "456", Name: "456", Project: testProject, Service: "service1", State: "Running", Health: "", Publishers: []compose.PortPublisher{{URL: "localhost:80", TargetPort: 90, PublishedPort: 80}}},
{ID: "789", Name: "789", Project: testProject, Service: "service2", State: "Running", Health: "", Publishers: nil},
}
assert.NilError(t, err)
assert.DeepEqual(t, containers, expected)
}
func TestPsAll(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
api := mocks.NewMockAPIClient(mockCtrl)
tested.apiClient = api
ctx := context.Background()
listOpts := apitypes.ContainerListOptions{Filters: filters.NewArgs(projectFilter(testProject)), All: true}
c1, inspect1 := containerDetails("service1", "123", "Running", "healthy")
c2, inspect2 := containerDetails("service1", "456", "Stopped", "")
api.EXPECT().ContainerList(ctx, listOpts).Return([]apitypes.Container{c1, c2}, nil)
api.EXPECT().ContainerInspect(anyCancellableContext(), "123").Return(inspect1, nil)
api.EXPECT().ContainerInspect(anyCancellableContext(), "456").Return(inspect2, nil)
containers, err := tested.Ps(ctx, testProject, compose.PsOptions{All: true})
expected := []compose.ContainerSummary{
{ID: "123", Name: "123", Project: testProject, Service: "service1", State: "Running", Health: "healthy", Publishers: nil},
{ID: "456", Name: "456", Project: testProject, Service: "service1", State: "Stopped", Health: "", Publishers: nil},
}
assert.NilError(t, err)
assert.DeepEqual(t, containers, expected)
}
func containerDetails(service string, id string, status string, health string) (apitypes.Container, apitypes.ContainerJSON) {
container := apitypes.Container{
ID: id,
Names: []string{"/" + id},
Labels: containerLabels(service),
State: status,
}
inspect := apitypes.ContainerJSON{ContainerJSONBase: &apitypes.ContainerJSONBase{State: &apitypes.ContainerState{Status: status, Health: &apitypes.Health{Status: health}}}}
return container, inspect
}

View File

@ -0,0 +1,5 @@
services:
service1:
image: nginx
service2:
image: mysql

View File

@ -27,7 +27,7 @@ import (
func TestCascadeStop(t *testing.T) { func TestCascadeStop(t *testing.T) {
c := NewParallelE2eCLI(t, binDir) c := NewParallelE2eCLI(t, binDir)
const projectName = "compose-e2e-logs" const projectName = "e2e-cascade-stop"
t.Run("abort-on-container-exit", func(t *testing.T) { t.Run("abort-on-container-exit", func(t *testing.T) {
res := c.RunDockerOrExitError("compose", "-f", "./fixtures/cascade-stop-test/compose.yaml", "--project-name", projectName, "up", "--abort-on-container-exit") res := c.RunDockerOrExitError("compose", "-f", "./fixtures/cascade-stop-test/compose.yaml", "--project-name", projectName, "up", "--abort-on-container-exit")

View File

@ -101,9 +101,9 @@ func TestLocalBackendRun(t *testing.T) {
fields := strings.Fields(line) fields := strings.Fields(line)
if fields[0] == nginxID { if fields[0] == nginxID {
nginxFound = true nginxFound = true
assert.Equal(t, fields[1], "nginx") assert.Equal(t, fields[1], "nginx", res.Combined())
assert.Equal(t, fields[2], "/docker-entrypoint.sh") assert.Equal(t, fields[2], "/docker-entrypoint.sh", res.Combined())
assert.Equal(t, fields[len(fields)-1], "0.0.0.0:85->80/tcp") assert.Equal(t, fields[len(fields)-1], "0.0.0.0:85->80/tcp", res.Combined())
} }
} }
assert.Assert(t, nginxFound, res.Stdout()) assert.Assert(t, nginxFound, res.Stdout())

File diff suppressed because it is too large Load Diff