Repository: compose-spec/compose-go
Pull request #548 (2024-01-24 22:02:57)

fix: handle more complex fields during merge
Improve the overall merge process for specific attribute format and uniqueness for lists and sequences

closes https://github.com/docker/compose/issues/11417

logopk: I have noticed problems with duplicate IP subnets. docker-compose version 2.24.2 and 2.24.3

```
  default:
    driver: bridge
    ipam:
     config:
       - subnet: 172.18.0.0/29  
```
override:
```
  default:
    driver: bridge
    ipam:
     config:
       - subnet: 172.23.0.0/29
```

docker-compose config shows

```
  default:
    name: apache_default
    driver: bridge
    ipam:
      config:
        - subnet: 172.18.0.0/29
        - subnet: 172.23.0.0/29
```

Error message on up is:

```
 ✘ Network apache_default  Error                                                                                                                                  0.0s 
failed to create network apache_default: Error response from daemon: bridge driver doesn't support multiple subnets
```

This was not a problem so far. config shows the overridden subnet and up starts the container in this subnet

```
  default:
    name: apache_default
    driver: bridge
    ipam:
      config:
      - subnet: 172.23.0.0/29
```

if it would be the same subnet in both files it would not be merged:
```
  default:
    name: apache_default
    driver: bridge
    ipam:
      config:
        - subnet: 172.22.0.0/29
        - subnet: 172.22.0.0/29
```

leading to a more bizarre error:

```
 ✘ Network apache_default  Error                                                                                                                                  0.0s 
failed to create network apache_default: Error response from daemon: Pool overlaps with other one on this address space
```


Patch:
diff --git a/go.mod b/go.mod
index 97be758..8f29e4e 100644
--- a/go.mod
+++ b/go.mod
@@ -8,6 +8,7 @@ require (
 	github.com/docker/go-units v0.5.0
 	github.com/google/go-cmp v0.5.9
 	github.com/mattn/go-shellwords v1.0.12
+	github.com/mitchellh/copystructure v1.2.0
 	github.com/mitchellh/mapstructure v1.5.0
 	github.com/opencontainers/go-digest v1.0.0
 	github.com/pkg/errors v0.9.1
@@ -22,7 +23,6 @@ require (
 
 require (
 	github.com/davecgh/go-spew v1.1.1 // indirect
-	github.com/mitchellh/copystructure v1.2.0 // indirect
 	github.com/mitchellh/reflectwalk v1.0.2 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
diff --git a/override/merge.go b/override/merge.go
index 2196d79..2d2d754 100644
--- a/override/merge.go
+++ b/override/merge.go
@@ -39,15 +39,28 @@ type merger func(any, any, tree.Path) (any, error)
 var mergeSpecials = map[tree.Path]merger{}
 
 func init() {
-	mergeSpecials["services.*.build.args"] = mergeToSequence
+	mergeSpecials["networks.*.ipam.config"] = mergeIPAMConfig
+	mergeSpecials["services.*.annotations"] = mergeToSequence
 	mergeSpecials["services.*.build"] = mergeBuild
-	mergeSpecials["services.*.depends_on"] = mergeDependsOn
-	mergeSpecials["services.*.logging"] = mergeLogging
-	mergeSpecials["services.*.networks"] = mergeNetworks
+	mergeSpecials["services.*.build.args"] = mergeToSequence
+	mergeSpecials["services.*.build.additional_contexts"] = mergeToSequence
+	mergeSpecials["services.*.build.labels"] = mergeToSequence
 	mergeSpecials["services.*.command"] = override
+	mergeSpecials["services.*.depends_on"] = mergeDependsOn
+	mergeSpecials["services.*.deploy.labels"] = mergeToSequence
+	mergeSpecials["services.*.dns"] = mergeToSequence
+	mergeSpecials["services.*.dns_opt"] = mergeToSequence
+	mergeSpecials["services.*.dns_search"] = mergeToSequence
 	mergeSpecials["services.*.entrypoint"] = override
-	mergeSpecials["services.*.healthcheck.test"] = override
+	mergeSpecials["services.*.env_file"] = mergeToSequence
 	mergeSpecials["services.*.environment"] = mergeToSequence
+	mergeSpecials["services.*.extra_hosts"] = mergeToSequence
+	mergeSpecials["services.*.healthcheck.test"] = override
+	mergeSpecials["services.*.labels"] = mergeToSequence
+	mergeSpecials["services.*.logging"] = mergeLogging
+	mergeSpecials["services.*.networks"] = mergeNetworks
+	mergeSpecials["services.*.sysctls"] = mergeToSequence
+	mergeSpecials["services.*.tmpfs"] = mergeToSequence
 	mergeSpecials["services.*.ulimits.*"] = mergeUlimit
 }
 
@@ -171,6 +184,8 @@ func convertIntoSequence(value any) []any {
 		return seq
 	case []any:
 		return v
+	case string:
+		return []any{v}
 	}
 	return nil
 }
@@ -183,6 +198,41 @@ func mergeUlimit(_ any, o any, p tree.Path) (any, error) {
 	return o, nil
 }
 
+func mergeIPAMConfig(c any, o any, path tree.Path) (any, error) {
+	var ipamConfigs []any
+	for _, original := range c.([]any) {
+		right := convertIntoMapping(original, nil)
+		for _, override := range o.([]any) {
+			left := convertIntoMapping(override, nil)
+			if left["subnet"] != right["subnet"] {
+				// check if left is already in ipamConfigs, add it if not and continue with the next config
+				if !slices.ContainsFunc(ipamConfigs, func(a any) bool {
+					return a.(map[string]any)["subnet"] == left["subnet"]
+				}) {
+					ipamConfigs = append(ipamConfigs, left)
+					continue
+				}
+			}
+			merged, err := mergeMappings(right, left, path)
+			if err != nil {
+				return nil, err
+			}
+			// find index of potential previous config with the same subnet in ipamConfigs
+			indexIfExist := slices.IndexFunc(ipamConfigs, func(a any) bool {
+				return a.(map[string]any)["subnet"] == merged["subnet"]
+			})
+			// if a previous config is already in ipamConfigs, replace it
+			if indexIfExist >= 0 {
+				ipamConfigs[indexIfExist] = merged
+			} else {
+				// or add the new config to ipamConfigs
+				ipamConfigs = append(ipamConfigs, merged)
+			}
+		}
+	}
+	return ipamConfigs, nil
+}
+
 func convertIntoMapping(a any, defaultValue any) map[string]any {
 	switch v := a.(type) {
 	case map[string]any:
diff --git a/override/uncity.go b/override/uncity.go
index 432bb9c..d8f29b5 100644
--- a/override/uncity.go
+++ b/override/uncity.go
@@ -31,13 +31,36 @@ type indexer func(any, tree.Path) (string, error)
 var unique = map[tree.Path]indexer{}
 
 func init() {
-	unique["services.*.environment"] = keyValueIndexer
+	unique["networks.*.labels"] = keyValueIndexer
+	unique["networks.*.ipam.options"] = keyValueIndexer
+	unique["services.*.annotations"] = keyValueIndexer
 	unique["services.*.build.args"] = keyValueIndexer
-	unique["services.*.volumes"] = volumeIndexer
-	unique["services.*.expose"] = exposeIndexer
-	unique["services.*.secrets"] = mountIndexer("/run/secrets")
+	unique["services.*.build.additional_contexts"] = keyValueIndexer
+	unique["services.*.build.extra_hosts"] = keyValueIndexer
+	unique["services.*.build.platform"] = keyValueIndexer
+	unique["services.*.build.tags"] = keyValueIndexer
+	unique["services.*.build.labels"] = keyValueIndexer
+	unique["services.*.cap_add"] = keyValueIndexer
+	unique["services.*.cap_drop"] = keyValueIndexer
 	unique["services.*.configs"] = mountIndexer("")
+	unique["services.*.deploy.labels"] = keyValueIndexer
+	unique["services.*.dns"] = keyValueIndexer
+	unique["services.*.dns_opt"] = keyValueIndexer
+	unique["services.*.dns_search"] = keyValueIndexer
+	unique["services.*.environment"] = keyValueIndexer
+	unique["services.*.env_file"] = envFileIndexer
+	unique["services.*.expose"] = exposeIndexer
+	unique["services.*.extra_hosts"] = keyValueIndexer
+	unique["services.*.labels"] = keyValueIndexer
+	unique["services.*.links"] = keyValueIndexer
+	unique["services.*.networks.*.aliases"] = keyValueIndexer
+	unique["services.*.networks.*.link_local_ips"] = keyValueIndexer
 	unique["services.*.ports"] = portIndexer
+	unique["services.*.profiles"] = keyValueIndexer
+	unique["services.*.secrets"] = mountIndexer("/run/secrets")
+	unique["services.*.sysctls"] = keyValueIndexer
+	unique["services.*.tmpfs"] = keyValueIndexer
+	unique["services.*.volumes"] = volumeIndexer
 }
 
 // EnforceUnicity removes redefinition of elements declared in a sequence
@@ -169,3 +192,13 @@ func portIndexer(y any, p tree.Path) (string, error) {
 	}
 	return "", nil
 }
+
+func envFileIndexer(y any, _ tree.Path) (string, error) {
+	switch value := y.(type) {
+	case string:
+		return value, nil
+	case map[string]any:
+		return value["path"].(string), nil
+	}
+	return "", nil
+}


Test patch:
diff --git a/override/merge_annotations_test.go b/override/merge_annotations_test.go
new file mode 100644
index 0000000..e425483
--- /dev/null
+++ b/override/merge_annotations_test.go
@@ -0,0 +1,114 @@
+/*
+   Copyright 2020 The Compose Specification 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 override
+
+import (
+	"testing"
+)
+
+func TestMergeAnnotationsSequence(t *testing.T) {
+	assertMergeYaml(t, `
+services:
+  test:
+    image: foo
+    annotations:
+      - FOO=BAR
+`, `
+services:
+  test:
+    annotations:
+      - QIX=ZOT
+      - EMPTY=
+      - NIL
+`, `
+services:
+  test:
+    image: foo
+    annotations:
+      - FOO=BAR
+      - QIX=ZOT
+      - EMPTY=
+      - NIL
+`)
+}
+
+func TestMergeAnnotationsMapping(t *testing.T) {
+	assertMergeYaml(t, `
+services:
+  test:
+    image: foo
+    annotations:
+      FOO: BAR
+`, `
+services:
+  test:
+    annotations:
+      EMPTY: ""
+      NIL: null
+      QIX: ZOT
+`, `
+services:
+  test:
+    image: foo
+    annotations:
+      - FOO=BAR
+      - EMPTY=
+      - NIL
+      - QIX=ZOT
+`)
+}
+
+func TestMergeAnnotationsMixed(t *testing.T) {
+	assertMergeYaml(t, `
+services:
+  test:
+    image: foo
+    annotations:
+      FOO: BAR
+`, `
+services:
+  test:
+    annotations:
+      - QIX=ZOT
+`, `
+services:
+  test:
+    image: foo
+    annotations:
+      - FOO=BAR
+      - QIX=ZOT
+`)
+}
+
+func TestMergeAnnotationsNumbers(t *testing.T) {
+	assertMergeYaml(t, `
+services:
+  test:
+    annotations:
+      FOO: 1
+`, `
+services:
+  test:
+    annotations:
+      FOO: 3
+`, `
+services:
+  test:
+    annotations:
+      - FOO=3
+`)
+}
diff --git a/override/merge_args_test.go b/override/merge_args_test.go
deleted file mode 100644
index fde07dc..0000000
--- a/override/merge_args_test.go
+++ /dev/null
@@ -1,131 +0,0 @@
-/*
-   Copyright 2020 The Compose Specification 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 override
-
-import (
-	"testing"
-)
-
-func Test_mergeYamlArgsSequence(t *testing.T) {
-	assertMergeYaml(t, `
-services:
-  test:
-    build:
-      context: .
-      args:
-        - FOO=BAR
-`, `
-services:
-  test:
-    build:
-      context: .
-      args:
-        - GIT_COMMIT=cdc3b19
-        - EMPTY=
-        - NIL
-`, `
-services:
-  test:
-    build:
-      context: .
-      args:
-        - FOO=BAR
-        - GIT_COMMIT=cdc3b19
-        - EMPTY=
-        - NIL
-`)
-}
-
-func Test_mergeYamlArgsMapping(t *testing.T) {
-	assertMergeYaml(t, `
-services:
-  test:
-    build:
-      context: .
-      args:
-        FOO: BAR
-`, `
-services:
-  test:
-    build:
-      context: .
-      args:
-        EMPTY: ""
-        NIL: null
-        QIX: ZOT
-`, `
-services:
-  test:
-    build:
-      context: .
-      args:
-       - FOO=BAR
-       - EMPTY=
-       - NIL
-       - QIX=ZOT
-`)
-}
-
-func Test_mergeYamlArgsMixed(t *testing.T) {
-	assertMergeYaml(t, `
-services:
-  test:
-    build:
-      context: .
-      args:
-        FOO: BAR
-`, `
-services:
-  test:
-    build:
-      args:
-        - QIX=ZOT
-`, `
-services:
-  test:
-    build:
-      context: .
-      args:
-        - FOO=BAR
-        - QIX=ZOT
-`)
-}
-
-func Test_mergeYamlArgsNumber(t *testing.T) {
-	assertMergeYaml(t, `
-services:
-  test:
-    build:
-      context: .
-      args:
-        FOO: 1
-`, `
-services:
-  test:
-    build:
-      context: .
-      args:
-        FOO: 3
-`, `
-services:
-  test:
-    build:
-      context: .
-      args:
-       - FOO=3
-`)
-}
diff --git a/override/merge_build_test.go b/override/merge_build_test.go
new file mode 100644
index 0000000..8f0c057
--- /dev/null
+++ b/override/merge_build_test.go
@@ -0,0 +1,197 @@
+/*
+   Copyright 2020 The Compose Specification 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 override
+
+import (
+	"testing"
+)
+
+func Test_mergeYamlBuildSequence(t *testing.T) {
+	assertMergeYaml(t, `
+services:
+  test:
+    build:
+      context: .
+      args:
+        - FOO=BAR
+      additional_contexts:
+        - resources=/path/to/resources
+        - app=docker-image://my-app:latest
+      platform: 
+        - linux/amd64
+      tags:
+        - "myimage:mytag"
+      extra_hosts:
+        - "somehost=162.242.195.82"
+        - "otherhost=50.31.209.229"
+`, `
+services:
+  test:
+    build:
+      context: .
+      args:
+        - GIT_COMMIT=cdc3b19
+        - EMPTY=
+        - NIL
+      additional_contexts:
+        - source=https://github.com/myuser/project.git
+      platform:
+        - "linux/arm64"
+        - "linux/amd64"
+        - "unsupported/unsupported"
+      tags:
+        - "myimage:mytag"
+        - "registry/username/myrepos:my-other-tag"
+      extra_hosts:
+        - "otherhost=50.31.209.230"
+        - "myhostv6=::1"
+`, `
+services:
+  test:
+    build:
+      context: .
+      args:
+        - FOO=BAR
+        - GIT_COMMIT=cdc3b19
+        - EMPTY=
+        - NIL
+      additional_contexts:
+        - resources=/path/to/resources
+        - app=docker-image://my-app:latest
+        - source=https://github.com/myuser/project.git
+      platform:
+        - "linux/amd64"
+        - "linux/arm64"
+        - "unsupported/unsupported"
+      tags:
+        - "myimage:mytag"
+        - "registry/username/myrepos:my-other-tag"
+      extra_hosts:
+        - "somehost=162.242.195.82"
+        - "otherhost=50.31.209.230"
+        - "myhostv6=::1"
+`)
+}
+
+func Test_mergeYamlArgsMapping(t *testing.T) {
+	assertMergeYaml(t, `
+services:
+  test:
+    build:
+      context: .
+      args:
+        FOO: BAR
+      additional_contexts:
+        source: https://github.com/myuser/project.git
+`, `
+services:
+  test:
+    build:
+      context: .
+      args:
+        EMPTY: ""
+        NIL: null
+        QIX: ZOT
+      additional_contexts:
+        app: docker-image://my-app:latest
+        resources: /path/to/resources
+`, `
+services:
+  test:
+    build:
+      context: .
+      args:
+       - FOO=BAR
+       - EMPTY=
+       - NIL
+       - QIX=ZOT
+      additional_contexts:
+        - source=https://github.com/myuser/project.git
+        - app=docker-image://my-app:latest
+        - resources=/path/to/resources
+`)
+}
+
+func Test_mergeYamlArgsMixed(t *testing.T) {
+	assertMergeYaml(t, `
+services:
+  test:
+    build:
+      context: .
+      args:
+        FOO: BAR
+      additional_contexts:
+        - resources=/path/to/resources
+        - app=docker-image://my-app:latest
+      platform: linux/amd64
+      tags:
+        - "myimage:mytag"
+`, `
+services:
+  test:
+    build:
+      args:
+        - QIX=ZOT
+      additional_contexts:
+        app: docker-image://new-app:latest
+        source: https://github.com/myuser/project.git
+      tags:
+        - "registry/username/myrepos:my-other-tag"
+
+`, `
+services:
+  test:
+    build:
+      context: .
+      args:
+        - FOO=BAR
+        - QIX=ZOT
+      additional_contexts:
+        - resources=/path/to/resources
+        - app=docker-image://new-app:latest
+        - source=https://github.com/myuser/project.git
+      platform: linux/amd64
+      tags:
+        - "myimage:mytag"
+        - "registry/username/myrepos:my-other-tag"
+`)
+}
+
+func Test_mergeYamlArgsNumber(t *testing.T) {
+	assertMergeYaml(t, `
+services:
+  test:
+    build:
+      context: .
+      args:
+        FOO: 1
+`, `
+services:
+  test:
+    build:
+      context: .
+      args:
+        FOO: 3
+`, `
+services:
+  test:
+    build:
+      context: .
+      args:
+       - FOO=3
+`)
+}
diff --git a/override/merge_cap_test.go b/override/merge_cap_test.go
new file mode 100644
index 0000000..bf7f5c5
--- /dev/null
+++ b/override/merge_cap_test.go
@@ -0,0 +1,56 @@
+/*
+   Copyright 2020 The Compose Specification 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 override
+
+import (
+	"testing"
+)
+
+func TestMergeCAPSequence(t *testing.T) {
+	assertMergeYaml(t, `
+services:
+  test:
+    image: foo
+    cap_add:
+      - CAP_BPF
+      - CAP_CHOWN
+    cap_drop:
+      - NET_ADMIN
+      - SYS_ADMIN
+`, `
+services:
+  test:
+    cap_add:
+      - CAP_KILL
+      - CAP_CHOWN
+    cap_drop:
+      - NET_ADMIN
+      - CAP_FOWNER
+`, `
+services:
+  test:
+    image: foo
+    cap_add:
+      - CAP_BPF
+      - CAP_CHOWN
+      - CAP_KILL
+    cap_drop:
+      - NET_ADMIN
+      - SYS_ADMIN
+      - CAP_FOWNER
+`)
+}
diff --git a/override/merge_dns_test.go b/override/merge_dns_test.go
new file mode 100644
index 0000000..5f51037
--- /dev/null
+++ b/override/merge_dns_test.go
@@ -0,0 +1,96 @@
+/*
+   Copyright 2020 The Compose Specification 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 override
+
+import (
+	"testing"
+)
+
+func Test_mergeYamlDNSSequence(t *testing.T) {
+	assertMergeYaml(t, `
+services:
+  test:
+    image: foo
+    dns:
+      - 8.8.8.8
+    dns_opt:
+      - use-vc
+    dns_search:
+      - dc1.example.com
+`, `
+services:
+  test:
+    dns:
+      - 9.9.9.9
+    dns_opt:
+      - no-tld-query
+    dns_search:
+      - dc2.example.com
+`, `
+services:
+  test:
+    image: foo
+    dns:
+      - 8.8.8.8
+      - 9.9.9.9
+    dns_opt:
+      - use-vc
+      - no-tld-query
+    dns_search:
+      - dc1.example.com
+      - dc2.example.com
+`)
+}
+
+func Test_mergeYamlDNSMixed(t *testing.T) {
+	assertMergeYaml(t, `
+services:
+  test:
+    image: foo
+    dns:
+      - 8.8.8.8
+      - 10.10.10.10
+      - 9.9.9.9
+    dns_opt:
+      - use-vc
+      - no-tld-query
+    dns_search:
+      - dc1.example.com
+      - dc2.example.com
+`, `
+services:
+  test:
+    dns: 9.9.9.9
+    dns_opt:
+      - no-tld-query
+    dns_search: dc2.example.com
+`, `
+services:
+  test:
+    image: foo
+    dns:
+      - 8.8.8.8
+      - 10.10.10.10
+      - 9.9.9.9
+    dns_opt:
+      - use-vc
+      - no-tld-query
+    dns_search:
+      - dc1.example.com
+      - dc2.example.com
+`)
+}
diff --git a/override/merge_env_file_test.go b/override/merge_env_file_test.go
new file mode 100644
index 0000000..2e16ab0
--- /dev/null
+++ b/override/merge_env_file_test.go
@@ -0,0 +1,120 @@
+/*
+   Copyright 2020 The Compose Specification 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 override
+
+import (
+	"testing"
+)
+
+func TestMergeEnvFilesSequence(t *testing.T) {
+	assertMergeYaml(t, `
+services:
+  test:
+    image: foo
+    env_file:
+      - foo.env
+`, `
+services:
+  test:
+    env_file:
+      - bar.env
+      - baz.env
+`, `
+services:
+  test:
+    image: foo
+    env_file:
+      - foo.env
+      - bar.env
+      - baz.env
+`)
+}
+
+func TestMergeEnvFilesString(t *testing.T) {
+	assertMergeYaml(t, `
+services:
+  test:
+    image: foo
+    env_file: foo.env
+`, `
+services:
+  test:
+    env_file: bar.env
+`, `
+services:
+  test:
+    image: foo
+    env_file:
+      - foo.env
+      - bar.env
+`)
+}
+
+func TestMergeEnvFilesMixed(t *testing.T) {
+	l := `
+services:
+  test:
+    env_file:
+      - bar.env
+      - foo.env
+`
+
+	r := `
+services:
+  test:
+    env_file: foo.env
+`
+
+	t.Run("SequenceThenString", func(t *testing.T) {
+		assertMergeYaml(t, r, l, `
+services:
+  test:
+    env_file:
+      - foo.env
+      - bar.env
+`)
+	})
+
+	t.Run("StringThenSequence", func(t *testing.T) {
+		assertMergeYaml(t, l, r, `
+services:
+  test:
+    env_file:
+      - bar.env
+      - foo.env
+`)
+	})
+
+	r = `
+services:
+  test:
+    env_file: 
+      - path: foo.env
+        required: true
+`
+	t.Run("Unicity between string and mapping", func(t *testing.T) {
+		assertMergeYaml(t, l, r, `
+services:
+  test:
+    env_file:
+      - bar.env
+      - path: foo.env
+        required: true
+
+`)
+	})
+}
diff --git a/override/merge_extra_hosts_test.go b/override/merge_extra_hosts_test.go
new file mode 100644
index 0000000..d5cc859
--- /dev/null
+++ b/override/merge_extra_hosts_test.go
@@ -0,0 +1,87 @@
+/*
+   Copyright 2020 The Compose Specification 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 override
+
+import (
+	"testing"
+)
+
+func TestMergeExtraHostsSequence(t *testing.T) {
+	assertMergeYaml(t, `
+services:
+  test:
+    image: foo
+    extra_hosts:
+      - example.com=1.2.3.4
+`, `
+services:
+  test:
+    extra_hosts:
+      - localhost=127.0.0.1
+`, `
+services:
+  test:
+    image: foo
+    extra_hosts:
+      - example.com=1.2.3.4
+      - localhost=127.0.0.1
+`)
+}
+
+func TestMergeExtraHostsMapping(t *testing.T) {
+	assertMergeYaml(t, `
+services:
+  test:
+    image: foo
+    extra_hosts:
+      "example.com": "1.2.3.4"
+`, `
+services:
+  test:
+    extra_hosts:
+      "localhost": "127.0.0.1"
+`, `
+services:
+  test:
+    image: foo
+    extra_hosts:
+      - example.com=1.2.3.4
+      - localhost=127.0.0.1
+`)
+}
+
+func TestMergeExtraHostsMixed(t *testing.T) {
+	assertMergeYaml(t, `
+services:
+  test:
+    image: foo
+    extra_hosts:
+      "example.com": "1.2.3.4"
+`, `
+services:
+  test:
+    extra_hosts:
+      - localhost=127.0.0.1
+`, `
+services:
+  test:
+    image: foo
+    extra_hosts:
+      - example.com=1.2.3.4
+      - localhost=127.0.0.1
+`)
+}
diff --git a/override/merge_labels_test.go b/override/merge_labels_test.go
new file mode 100644
index 0000000..b37e517
--- /dev/null
+++ b/override/merge_labels_test.go
@@ -0,0 +1,324 @@
+/*
+   Copyright 2020 The Compose Specification 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 override
+
+import (
+	"testing"
+)
+
+func Test_mergeYamlLabelsSequence(t *testing.T) {
+	assertMergeYaml(t, `
+services:
+  test:
+    image: foo
+    labels:
+      - FOO=BAR
+`, `
+services:
+  test:
+    labels:
+      - QIX=ZOT
+      - EMPTY=
+      - NIL
+`, `
+services:
+  test:
+    image: foo
+    labels:
+      - FOO=BAR
+      - QIX=ZOT
+      - EMPTY=
+      - NIL
+`)
+}
+
+func Test_mergeYamlLabelsMapping(t *testing.T) {
+	assertMergeYaml(t, `
+services:
+  test:
+    image: foo
+    labels:
+      FOO: BAR
+`, `
+services:
+  test:
+    labels:
+      EMPTY: ""
+      NIL: null
+      QIX: ZOT
+`, `
+services:
+  test:
+    image: foo
+    labels:
+      - FOO=BAR
+      - EMPTY=
+      - NIL
+      - QIX=ZOT
+`)
+}
+
+func Test_mergeYamlLabelsMixed(t *testing.T) {
+	assertMergeYaml(t, `
+services:
+  test:
+    image: foo
+    labels:
+      FOO: BAR
+`, `
+services:
+  test:
+    labels:
+      - QIX=ZOT
+`, `
+services:
+  test:
+    image: foo
+    labels:
+      - FOO=BAR
+      - QIX=ZOT
+`)
+}
+
+func Test_mergeYamlLabelsNumber(t *testing.T) {
+	assertMergeYaml(t, `
+services:
+  test:
+    labels:
+      FOO: 1
+`, `
+services:
+  test:
+    labels:
+      FOO: 3
+`, `
+services:
+  test:
+    labels:
+      - FOO=3
+`)
+}
+
+func Test_mergeYamlDeployLabelsSequence(t *testing.T) {
+	assertMergeYaml(t, `
+services:
+  test:
+    image: foo
+    deploy:
+      labels:
+        - FOO=BAR
+`, `
+services:
+  test:
+    deploy:
+      labels:
+        - QIX=ZOT
+        - EMPTY=
+        - NIL
+`, `
+services:
+  test:
+    image: foo
+    deploy:
+      labels:
+        - FOO=BAR
+        - QIX=ZOT
+        - EMPTY=
+        - NIL
+`)
+}
+
+func Test_mergeYamlDeployLabelsMapping(t *testing.T) {
+	assertMergeYaml(t, `
+services:
+  test:
+    image: foo
+    deploy:
+      labels:
+        FOO: BAR
+`, `
+services:
+  test:
+    deploy:
+      labels:
+        EMPTY: ""
+        NIL: null
+        QIX: ZOT
+`, `
+services:
+  test:
+    image: foo
+    deploy:
+      labels:
+        - FOO=BAR
+        - EMPTY=
+        - NIL
+        - QIX=ZOT
+`)
+}
+
+func Test_mergeYamlDeployLabelsMixed(t *testing.T) {
+	assertMergeYaml(t, `
+services:
+  test:
+    image: foo
+    deploy:
+      labels:
+        FOO: BAR
+`, `
+services:
+  test:
+    deploy:
+      labels:
+        - QIX=ZOT
+`, `
+services:
+  test:
+    image: foo
+    deploy:
+      labels:
+        - FOO=BAR
+        - QIX=ZOT
+`)
+}
+
+func Test_mergeYamlDeployLabelsNumber(t *testing.T) {
+	assertMergeYaml(t, `
+services:
+  test:
+    deploy:
+      labels:
+        FOO: 1
+`, `
+services:
+  test:
+    deploy:
+      labels:
+        FOO: 3
+`, `
+services:
+  test:
+    deploy:
+      labels:
+        - FOO=3
+`)
+}
+
+func Test_mergeYamlBuildLabelsSequence(t *testing.T) {
+	assertMergeYaml(t, `
+services:
+  test:
+    image: foo
+    build:
+      labels:
+        - FOO=BAR
+`, `
+services:
+  test:
+    build:
+      labels:
+        - QIX=ZOT
+        - EMPTY=
+        - NIL
+`, `
+services:
+  test:
+    image: foo
+    build:
+      labels:
+        - FOO=BAR
+        - QIX=ZOT
+        - EMPTY=
+        - NIL
+`)
+}
+
+func Test_mergeYamlBuildLabelsMapping(t *testing.T) {
+	assertMergeYaml(t, `
+services:
+  test:
+    image: foo
+    build:
+      labels:
+        FOO: BAR
+`, `
+services:
+  test:
+    build:
+      labels:
+        EMPTY: ""
+        NIL: null
+        QIX: ZOT
+`, `
+services:
+  test:
+    image: foo
+    build:
+      labels:
+        - FOO=BAR
+        - EMPTY=
+        - NIL
+        - QIX=ZOT
+`)
+}
+
+func Test_mergeYamlBuildLabelsMixed(t *testing.T) {
+	assertMergeYaml(t, `
+services:
+  test:
+    image: foo
+    build:
+      labels:
+        FOO: BAR
+`, `
+services:
+  test:
+    build:
+      labels:
+        - QIX=ZOT
+`, `
+services:
+  test:
+    image: foo
+    build:
+      labels:
+        - FOO=BAR
+        - QIX=ZOT
+`)
+}
+
+func Test_mergeYamlBuildLabelsNumber(t *testing.T) {
+	assertMergeYaml(t, `
+services:
+  test:
+    build:
+      labels:
+        FOO: 1
+`, `
+services:
+  test:
+    build:
+      labels:
+        FOO: 3
+`, `
+services:
+  test:
+    build:
+      labels:
+        - FOO=3
+`)
+}
diff --git a/override/merge_links_test.go b/override/merge_links_test.go
new file mode 100644
index 0000000..6a6e5c8
--- /dev/null
+++ b/override/merge_links_test.go
@@ -0,0 +1,44 @@
+/*
+   Copyright 2020 The Compose Specification 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 override
+
+import (
+	"testing"
+)
+
+func TestMergeExtraLinksSequence(t *testing.T) {
+	assertMergeYaml(t, `
+services:
+  test:
+    image: foo
+    links:
+      - service1
+      - service2
+`, `
+services:
+  test:
+    links:
+      - service2=alias1
+`, `
+services:
+  test:
+    image: foo
+    links:
+      - service1
+      - service2=alias1
+`)
+}
diff --git a/override/merge_mem_test.go b/override/merge_mem_test.go
new file mode 100644
index 0000000..8fe1507
--- /dev/null
+++ b/override/merge_mem_test.go
@@ -0,0 +1,49 @@
+/*
+   Copyright 2020 The Compose Specification 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 override
+
+import (
+	"testing"
+)
+
+func TestMemLimitMixed(t *testing.T) {
+	l := `
+services:
+  test:
+    mem_limit: '256m'
+`
+	r := `
+services:
+  test:
+    mem_limit: 1
+`
+	t.Run("StringThenInt", func(t *testing.T) {
+		assertMergeYaml(t, r, l, `
+services:
+  test:
+    mem_limit: 256m
+`)
+	})
+
+	t.Run("IntThenString", func(t *testing.T) {
+		assertMergeYaml(t, l, r, `
+services:
+  test:
+    mem_limit: 1
+`)
+	})
+}
diff --git a/override/merge_networks_test.go b/override/merge_networks_test.go
new file mode 100644
index 0000000..4603a7a
--- /dev/null
+++ b/override/merge_networks_test.go
@@ -0,0 +1,228 @@
+/*
+   Copyright 2020 The Compose Specification 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 override
+
+import (
+	"testing"
+)
+
+func Test_mergeYamlServiceNetworkSequence(t *testing.T) {
+	assertMergeYaml(t, `
+services:
+  test:
+    image: foo
+    networks:
+      - front-network
+      - back-network
+`, `
+services:
+  test:
+    networks:
+      - front-network
+`, `
+services:
+  test:
+    image: foo
+    networks:
+      front-network:
+      back-network:
+`)
+}
+
+func Test_mergeYamlServiceNetworksMapping(t *testing.T) {
+	assertMergeYaml(t, `
+services:
+  test:
+    image: foo
+    networks:
+      network1:
+        aliases:
+          - alias1
+          - alias2
+        link_local_ips:
+          - 57.123.22.11
+          - 57.123.22.13
+      network2:
+        aliases:
+          - alias1
+          - alias3
+`, `
+services:
+  test:
+    networks:
+      network1:
+        aliases:
+          - alias3
+          - alias1
+        link_local_ips:
+          - 57.123.22.12
+          - 57.123.22.13
+`, `
+services:
+  test:
+    image: foo
+    networks:
+      network1:
+        aliases:
+          - alias1
+          - alias2
+          - alias3
+        link_local_ips:
+          - 57.123.22.11
+          - 57.123.22.13
+          - 57.123.22.12
+      network2:
+        aliases:
+          - alias1
+          - alias3
+`)
+}
+
+func Test_mergeYamlServiceNetworksMixed(t *testing.T) {
+	assertMergeYaml(t, `
+services:
+  test:
+    image: foo
+    networks:
+      - front-network
+      - back-network
+      - network1
+`, `
+services:
+  test:
+    image: foo
+    networks:
+      network1:
+        aliases:
+          - alias1
+          - alias2
+        link_local_ips:
+          - 57.123.22.11
+          - 57.123.22.13
+      network2:
+        aliases:
+          - alias1
+          - alias3
+`, `
+services:
+  test:
+    image: foo
+    networks:
+      front-network:
+      back-network:
+      network1:
+        aliases:
+          - alias1
+          - alias2
+        link_local_ips:
+          - 57.123.22.11
+          - 57.123.22.13
+      network2:
+        aliases:
+          - alias1
+          - alias3
+`)
+}
+
+func Test_mergeYamlNetworks(t *testing.T) {
+	assertMergeYaml(t, `
+services:
+  test:
+    image: foo
+networks:
+  network1:
+    ipam:
+      config:
+        - subnet: 172.28.0.0/16
+          ip_range: 172.28.5.0/24
+          gateway: 172.28.5.254
+          aux_addresses:
+            host1: 172.28.1.5
+            host2: 172.28.1.6
+            host3: 172.28.1.7
+      options:
+        foo: bar
+        baz: "0"
+    labels:
+      com.example.description: "Financial transaction network"
+      com.example.department: "Finance"
+      com.example.label-with-empty-value: ""
+`, `
+services:
+  test:
+    image: foo
+networks:
+  network1:
+    ipam:
+      config:
+        - subnet: 172.28.0.0/16
+          ip_range: 172.28.5.1/24
+          gateway: 172.28.5.254
+          aux_addresses:
+            host1: 172.28.1.5
+            host2: 172.28.1.4
+            host4: 172.28.1.10
+        - subnet: 172.28.10.0/16
+          ip_range: 172.28.10.1/24
+          gateway: 172.28.10.254
+          aux_addresses:
+            host1: 172.28.10.5
+            host2: 172.28.10.4
+            host3: 172.28.10.10
+      options:
+        bar: foo
+        baz: "0"
+    labels:
+      com.example.description: "Financial transaction network"
+      com.example.department-new: "New"
+      com.example.label-with-empty-value: ""
+  network2:
+`, `
+services:
+  test:
+    image: foo
+networks:
+  network1:
+    ipam:
+      config:
+        - subnet: 172.28.0.0/16
+          ip_range: 172.28.5.1/24
+          gateway: 172.28.5.254
+          aux_addresses:
+            host1: 172.28.1.5
+            host2: 172.28.1.4
+            host3: 172.28.1.7
+            host4: 172.28.1.10
+        - subnet: 172.28.10.0/16
+          ip_range: 172.28.10.1/24
+          gateway: 172.28.10.254
+          aux_addresses:
+            host1: 172.28.10.5
+            host2: 172.28.10.4
+            host3: 172.28.10.10
+      options:
+        foo: bar
+        bar: foo
+        baz: "0"
+    labels:
+      com.example.description: "Financial transaction network"
+      com.example.department: "Finance"
+      com.example.label-with-empty-value: ""
+      com.example.department-new: "New"
+  network2:
+`)
+}
diff --git a/override/merge_profiles_test.go b/override/merge_profiles_test.go
new file mode 100644
index 0000000..b3f8e65
--- /dev/null
+++ b/override/merge_profiles_test.go
@@ -0,0 +1,46 @@
+/*
+   Copyright 2020 The Compose Specification 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 override
+
+import (
+	"testing"
+)
+
+func TestMergeProfilesUnicity(t *testing.T) {
+	assertMergeYaml(t, `
+services:
+  test:
+    image: foo
+    profiles:
+      - profile1
+      - profile2
+`, `
+services:
+  test:
+    profiles:
+      - profile2
+      - profile3
+`, `
+services:
+  test:
+    image: foo
+    profiles:
+      - profile1
+      - profile2
+      - profile3
+`)
+}
diff --git a/override/merge_sysctls_test.go b/override/merge_sysctls_test.go
new file mode 100644
index 0000000..f4c27c7
--- /dev/null
+++ b/override/merge_sysctls_test.go
@@ -0,0 +1,106 @@
+/*
+   Copyright 2020 The Compose Specification 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 override
+
+import (
+	"testing"
+)
+
+func TestMergeSysctlsSequence(t *testing.T) {
+	assertMergeYaml(t, `
+services:
+  test:
+    image: foo
+    sysctls:
+      - net.ipv6.conf.all.disable_ipv6=1
+`, `
+services:
+  test:
+    sysctls:
+      - net.ipv4.tcp_keepalive_time=300
+`, `
+services:
+  test:
+    image: foo
+    sysctls:
+      - net.ipv6.conf.all.disable_ipv6=1
+      - net.ipv4.tcp_keepalive_time=300
+`)
+}
+
+func TestMergeSysctlsMapping(t *testing.T) {
+	assertMergeYaml(t, `
+services:
+  test:
+    image: foo
+    sysctls:
+      "net.ipv6.conf.all.disable_ipv6": 1
+`, `
+services:
+  test:
+    sysctls:
+      "net.ipv4.tcp_keepalive_time": "300"
+`, `
+services:
+  test:
+    image: foo
+    sysctls:
+      - net.ipv6.conf.all.disable_ipv6=1
+      - net.ipv4.tcp_keepalive_time=300
+`)
+}
+
+func TestMergeSysctlsMixed(t *testing.T) {
+	assertMergeYaml(t, `
+services:
+  test:
+    image: foo
+    sysctls:
+      "net.ipv6.conf.all.disable_ipv6": 1
+`, `
+services:
+  test:
+    sysctls:
+      - net.ipv4.tcp_keepalive_time=300
+`, `
+services:
+  test:
+    image: foo
+    sysctls:
+      - net.ipv6.conf.all.disable_ipv6=1
+      - net.ipv4.tcp_keepalive_time=300
+`)
+}
+
+func TestMergeSysctlsNumbers(t *testing.T) {
+	assertMergeYaml(t, `
+services:
+  test:
+    sysctls:
+      "net.ipv4.tcp_keepalive_time": 1
+`, `
+services:
+  test:
+    sysctls:
+      "net.ipv4.tcp_keepalive_time": 3
+`, `
+services:
+  test:
+    sysctls:
+      - net.ipv4.tcp_keepalive_time=3
+`)
+}
diff --git a/override/merge_test.go b/override/merge_test.go
index 3fb97fa..e218646 100644
--- a/override/merge_test.go
+++ b/override/merge_test.go
@@ -44,22 +44,24 @@ services:
     scale: 2
 `
 
-	got, err := Merge(unmarshall(t, right), unmarshall(t, left))
+	got, err := Merge(unmarshal(t, right), unmarshal(t, left))
 	assert.NilError(t, err)
-	assert.DeepEqual(t, got, unmarshall(t, expected))
+	assert.DeepEqual(t, got, unmarshal(t, expected))
 }
 
 func assertMergeYaml(t *testing.T, right string, left string, want string) {
-	got, err := Merge(unmarshall(t, right), unmarshall(t, left))
+	t.Helper()
+	got, err := Merge(unmarshal(t, right), unmarshal(t, left))
 	assert.NilError(t, err)
 	got, err = EnforceUnicity(got)
 	assert.NilError(t, err)
-	assert.DeepEqual(t, got, unmarshall(t, want))
+	assert.DeepEqual(t, got, unmarshal(t, want))
 }
 
-func unmarshall(t *testing.T, s string) map[string]any {
+func unmarshal(t *testing.T, s string) map[string]any {
+	t.Helper()
 	var val map[string]any
 	err := yaml.Unmarshal([]byte(s), &val)
-	assert.NilError(t, err)
+	assert.NilError(t, err, s)
 	return val
 }
diff --git a/override/merge_tmpfs_test.go b/override/merge_tmpfs_test.go
new file mode 100644
index 0000000..1e421d7
--- /dev/null
+++ b/override/merge_tmpfs_test.go
@@ -0,0 +1,105 @@
+/*
+   Copyright 2020 The Compose Specification 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 override
+
+import (
+	"testing"
+)
+
+func TestMergeTmpfsSequence(t *testing.T) {
+	assertMergeYaml(t, `
+services:
+  test:
+    image: foo
+    tmpfs:
+      - /foo
+`, `
+services:
+  test:
+    tmpfs:
+      - /bar
+      - /baz
+      - /foo
+`, `
+services:
+  test:
+    image: foo
+    tmpfs:
+      - /foo
+      - /bar
+      - /baz
+`)
+}
+
+func TestMergeTmpfsString(t *testing.T) {
+	assertMergeYaml(t, `
+services:
+  test:
+    image: foo
+    tmpfs: /foo
+`, `
+services:
+  test:
+    tmpfs: /bar
+`, `
+services:
+  test:
+    image: foo
+    tmpfs:
+      - /foo
+      - /bar
+`)
+}
+
+func TestMergeTmpfsMixed(t *testing.T) {
+	l := `
+services:
+  test:
+    tmpfs:
+      - /bar
+      - /foo
+`
+
+	r := `
+services:
+  test:
+    image: foo
+    tmpfs: /foo
+`
+
+	t.Run("SequenceThenString", func(t *testing.T) {
+		assertMergeYaml(t, r, l, `
+services:
+  test:
+    image: foo
+    tmpfs:
+      - /foo
+      - /bar
+`)
+	})
+
+	t.Run("StringThenSequence", func(t *testing.T) {
+		assertMergeYaml(t, l, r, `
+services:
+  test:
+    image: foo
+    tmpfs:
+      - /bar
+      - /foo
+`)
+	})
+}
diff --git a/override/merge_volumes_test.go b/override/merge_volumes_test.go
new file mode 100644
index 0000000..887c738
--- /dev/null
+++ b/override/merge_volumes_test.go
@@ -0,0 +1,49 @@
+/*
+   Copyright 2020 The Compose Specification 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 override
+
+import (
+	"testing"
+)
+
+func TestMergeVolumes(t *testing.T) {
+	assertMergeYaml(t, `
+services:
+  test:
+    image: foo
+    volumes:
+     - a:/a
+     - type: bind
+       source: ./b
+       target: /b
+`, `
+services:
+  test:
+    volumes:
+      - c:/c
+`, `
+services:
+  test:
+    image: foo
+    volumes:
+      - a:/a
+      - type: bind
+        source: ./b
+        target: /b
+      - c:/c
+`)
+}
diff --git a/override/uncity_test.go b/override/uncity_test.go
index 239a1eb..7d4a62d 100644
--- a/override/uncity_test.go
+++ b/override/uncity_test.go
@@ -133,7 +133,7 @@ services:
 }
 
 func assertUnicity(t *testing.T, before string, expected string) {
-	got, err := EnforceUnicity(unmarshall(t, before))
+	got, err := EnforceUnicity(unmarshal(t, before))
 	assert.NilError(t, err)
-	assert.DeepEqual(t, got, unmarshall(t, expected))
+	assert.DeepEqual(t, got, unmarshal(t, expected))
 }