Repository: scionproto/scion
Pull request #3622 (2020-01-21 08:12:01)

Use /topology endpoint for topology reload tests
Also:
- Make overlay.Type json "unmarshallable"
- Make common.IFIDType json friendlier
- Make topology.RWToplology json "unmarshallable"
- Remove old topology tests

<!-- Reviewable:start -->
---
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/scionproto/scion/3622)
<!-- Reviewable:end -->



Patch:
diff --git a/.buildkite/pipeline.sh b/.buildkite/pipeline.sh
index ffe495d24..2c32ad9dd 100755
--- a/.buildkite/pipeline.sh
+++ b/.buildkite/pipeline.sh
@@ -24,13 +24,18 @@ gen_acceptance() {
 
 # gen_bazel_acceptance generates steps for bazel tests in acceptance folder.
 gen_bazel_acceptance() {
-    for test in $(bazel query 'kind(sh_test, //acceptance/...)' 2>/dev/null); do
+    for test in $(bazel query 'kind(test, //acceptance/...)' 2>/dev/null); do
         # test has the format //acceptance/<name>:<name>_test
         name=$(echo $test | cut -d ':' -f 1)
         name=${name#'//acceptance/'}
         echo "  - label: \":bazel: Acceptance: $name\""
         echo "    command:"
-        echo "      - bazel test $test"
+        if [[ "$test" =~ "go" ]]; then
+            # for go tests add verbose flag.
+            echo "      - bazel test $test --test_arg=-test.v"
+        else
+            echo "      - bazel test $test"
+        fi
         echo "    key: ${name}_acceptance"
         echo "    artifact_paths:"
         echo "      - \"artifacts.out/**/*\""
diff --git a/Makefile b/Makefile
index 1904b8be6..7c31efcc5 100644
--- a/Makefile
+++ b/Makefile
@@ -38,7 +38,7 @@ mocks:
 	./tools/gomocks
 
 gazelle:
-	bazel run //:gazelle -- update -mode=$(GAZELLE_MODE) -index=false -external=external -exclude go/vendor -exclude docker/_build ./go
+	bazel run //:gazelle -- update -mode=$(GAZELLE_MODE) -index=false -external=external -exclude go/vendor -exclude docker/_build ./go ./acceptance
 
 setcap:
 	tools/setcap cap_net_admin,cap_net_raw+ep $(BRACCEPT)
diff --git a/acceptance/sig_failover/BUILD.bazel b/acceptance/sig_failover/BUILD.bazel
index 0e9092275..4f280b105 100644
--- a/acceptance/sig_failover/BUILD.bazel
+++ b/acceptance/sig_failover/BUILD.bazel
@@ -18,7 +18,11 @@ container_image(
     name = "dispatcher1",
     base = "//docker/perapp:dispatcher_go_debug",
     files = ["testdata/1-ff00_0_110/dispatcher/disp.toml"],
-    entrypoint = ["/app/godispatcher", "-config", "/disp.toml"],
+    entrypoint = [
+        "/app/godispatcher",
+        "-config",
+        "/disp.toml",
+    ],
     stamp = True,
 )
 
@@ -26,7 +30,11 @@ container_image(
     name = "dispatcher2",
     base = "//docker/perapp:dispatcher_go_debug",
     files = ["testdata/1-ff00_0_111/dispatcher/disp.toml"],
-    entrypoint = ["/app/godispatcher", "-config", "/disp.toml"],
+    entrypoint = [
+        "/app/godispatcher",
+        "-config",
+        "/disp.toml",
+    ],
     stamp = True,
 )
 
@@ -38,7 +46,11 @@ container_image(
         "testdata/1-ff00_0_110/sig/sig.toml",
         "testdata/1-ff00_0_110/sig/fake_sciond.json",
     ],
-    entrypoint = ["/app/sig", "-config", "/sig.toml"],
+    entrypoint = [
+        "/app/sig",
+        "-config",
+        "/sig.toml",
+    ],
     stamp = True,
 )
 
@@ -50,6 +62,10 @@ container_image(
         "testdata/1-ff00_0_111/sig/sig.toml",
         "testdata/1-ff00_0_111/sig/fake_sciond.json",
     ],
-    entrypoint = ["/app/sig", "-config", "/sig.toml"],
+    entrypoint = [
+        "/app/sig",
+        "-config",
+        "/sig.toml",
+    ],
     stamp = True,
 )
diff --git a/acceptance/sig_short_exp_time/BUILD.bazel b/acceptance/sig_short_exp_time/BUILD.bazel
index c3a318b5b..1b52f2310 100644
--- a/acceptance/sig_short_exp_time/BUILD.bazel
+++ b/acceptance/sig_short_exp_time/BUILD.bazel
@@ -18,7 +18,11 @@ container_image(
     name = "dispatcher1",
     base = "//docker/perapp:dispatcher_go_debug",
     files = ["testdata/1-ff00_0_110/dispatcher/disp.toml"],
-    entrypoint = ["/app/godispatcher", "-config", "/disp.toml"],
+    entrypoint = [
+        "/app/godispatcher",
+        "-config",
+        "/disp.toml",
+    ],
     stamp = True,
 )
 
@@ -26,7 +30,11 @@ container_image(
     name = "dispatcher2",
     base = "//docker/perapp:dispatcher_go_debug",
     files = ["testdata/1-ff00_0_111/dispatcher/disp.toml"],
-    entrypoint = ["/app/godispatcher", "-config", "/disp.toml"],
+    entrypoint = [
+        "/app/godispatcher",
+        "-config",
+        "/disp.toml",
+    ],
     stamp = True,
 )
 
@@ -38,7 +46,11 @@ container_image(
         "testdata/1-ff00_0_110/sig/sig.toml",
         "testdata/1-ff00_0_110/sig/fake_sciond.json",
     ],
-    entrypoint = ["/app/sig", "-config", "/sig.toml"],
+    entrypoint = [
+        "/app/sig",
+        "-config",
+        "/sig.toml",
+    ],
     stamp = True,
 )
 
@@ -50,6 +62,10 @@ container_image(
         "testdata/1-ff00_0_111/sig/sig.toml",
         "testdata/1-ff00_0_111/sig/fake_sciond.json",
     ],
-    entrypoint = ["/app/sig", "-config", "/sig.toml"],
+    entrypoint = [
+        "/app/sig",
+        "-config",
+        "/sig.toml",
+    ],
     stamp = True,
 )
diff --git a/acceptance/topo_common/BUILD.bazel b/acceptance/topo_common/BUILD.bazel
new file mode 100644
index 000000000..ed3b37e1e
--- /dev/null
+++ b/acceptance/topo_common/BUILD.bazel
@@ -0,0 +1,36 @@
+filegroup(
+    name = "topology",
+    srcs = ["topology.json"],
+    visibility = ["//visibility:public"],
+)
+
+filegroup(
+    name = "invalid_reloads",
+    srcs = [
+        ":topology_invalid_ia",
+        ":topology_invalid_attributes",
+        ":topology_invalid_mtu",
+    ],
+    visibility = ["//visibility:public"],
+)
+
+genrule(
+    name = "topology_invalid_ia",
+    srcs = ["topology.json"],
+    outs = ["topology_invalid_ia.json"],
+    cmd = "jq '.ISD_AS = \"1-ff00:0:111\"' $(location :topology.json) > $@",
+)
+
+genrule(
+    name = "topology_invalid_attributes",
+    srcs = ["topology.json"],
+    outs = ["topology_invalid_attributes.json"],
+    cmd = "jq '.Attributes = [\"core\"]' $(location :topology.json) > $@",
+)
+
+genrule(
+    name = "topology_invalid_mtu",
+    srcs = ["topology.json"],
+    outs = ["topology_invalid_mtu.json"],
+    cmd = "jq '.MTU = 42' $(location :topology.json) > $@",
+)
diff --git a/acceptance/topo_common/topology.json b/acceptance/topo_common/topology.json
new file mode 100644
index 000000000..fe79eca97
--- /dev/null
+++ b/acceptance/topo_common/topology.json
@@ -0,0 +1,121 @@
+{
+  "MTU": 1400,
+  "BorderRouters": {
+    "br1-ff00_0_110-1": {
+      "Interfaces": {
+        "1": {
+          "MTU": 1280,
+          "RemoteOverlay": {
+            "Addr": "127.0.0.5",
+            "OverlayPort": 50000
+          },
+          "Bandwidth": 1000,
+          "ISD_AS": "1-ff00:0:111",
+          "PublicOverlay": {
+            "Addr": "127.0.0.4",
+            "OverlayPort": 50000
+          },
+          "LinkTo": "CHILD",
+          "Overlay": "UDP/IPv4"
+        }
+      },
+      "InternalAddrs": {
+        "IPv4": {
+          "PublicOverlay": {
+            "Addr": "127.0.0.9",
+            "OverlayPort": 31008
+          }
+        }
+      },
+      "CtrlAddr": {
+        "IPv4": {
+          "Public": {
+            "Addr": "127.0.0.9",
+            "L4Port": 31006
+          }
+        }
+      }
+    },
+    "br1-ff00_0_110-2": {
+      "Interfaces": {
+        "2": {
+          "MTU": 1472,
+          "RemoteOverlay": {
+            "Addr": "127.0.0.7",
+            "OverlayPort": 50000
+          },
+          "Bandwidth": 500,
+          "ISD_AS": "1-ff00:0:112",
+          "PublicOverlay": {
+            "Addr": "127.0.0.6",
+            "OverlayPort": 50000
+          },
+          "LinkTo": "CHILD",
+          "Overlay": "UDP/IPv4"
+        }
+      },
+      "InternalAddrs": {
+        "IPv4": {
+          "PublicOverlay": {
+            "Addr": "127.0.0.10",
+            "OverlayPort": 31012
+          }
+        }
+      },
+      "CtrlAddr": {
+        "IPv4": {
+          "Public": {
+            "Addr": "127.0.0.10",
+            "L4Port": 31010
+          }
+        }
+      }
+    }
+  },
+  "ColibriService": {},
+  "DiscoveryService": {},
+  "PathService": {
+    "ps1-ff00_0_110-1": {
+      "Addrs": {
+        "IPv4": {
+          "Public": {
+            "Addr": "127.0.0.13",
+            "L4Port": 31004
+          }
+        }
+      }
+    }
+  },
+  "Attributes": [
+    "authoritative",
+    "core",
+    "issuing",
+    "voting"
+  ],
+  "ISD_AS": "1-ff00:0:110",
+  "CertificateService": {
+    "cs1-ff00_0_110-1": {
+      "Addrs": {
+        "IPv4": {
+          "Public": {
+            "Addr": "127.0.0.12",
+            "L4Port": 31002
+          }
+        }
+      }
+    }
+  },
+  "BeaconService": {
+    "bs1-ff00_0_110-1": {
+      "Addrs": {
+        "IPv4": {
+          "Public": {
+            "Addr": "127.0.0.11",
+            "L4Port": 31000
+          }
+        }
+      }
+    }
+  },
+  "Overlay": "UDP/IPv4"
+}
diff --git a/acceptance/topo_ps_reload/BUILD.bazel b/acceptance/topo_ps_reload/BUILD.bazel
new file mode 100644
index 000000000..673d7d960
--- /dev/null
+++ b/acceptance/topo_ps_reload/BUILD.bazel
@@ -0,0 +1,67 @@
+load("@io_bazel_rules_docker//container:container.bzl", "container_image")
+load("@io_bazel_rules_go//go:def.bzl", "go_test")
+
+go_test(
+    name = "go_default_test",
+    srcs = ["reload_test.go"],
+    data = [
+        ":dispatcher",
+        ":path_srv",
+        "docker-compose.yml",
+        "//acceptance/topo_common:topology",
+        "//acceptance/topo_common:invalid_reloads",
+        "testdata/topology_reload.json",
+        ":invalid_changed_ip",
+        ":invalid_changed_port",
+    ],
+    deps = [
+        "//go/lib/topology:go_default_library",
+        "@com_github_stretchr_testify//assert:go_default_library",
+        "@com_github_stretchr_testify//require:go_default_library",
+    ],
+)
+
+container_image(
+    name = "dispatcher",
+    base = "//docker/perapp:dispatcher_go_debug",
+    files = ["testdata/disp.toml"],
+    entrypoint = [
+        "/app/godispatcher",
+        "-config",
+        "/disp.toml",
+    ],
+    stamp = True,
+)
+
+container_image(
+    name = "path_srv",
+    base = "//docker/perapp:path_srv_debug",
+    files = [
+        "testdata/ps.toml",
+        "//acceptance/topo_common:topology",
+        "//acceptance/topo_common:invalid_reloads",
+        "testdata/topology_reload.json",
+        ":invalid_changed_ip",
+        ":invalid_changed_port",
+    ],
+    entrypoint = [
+        "/app/path_srv",
+        "-config",
+        "/ps.toml",
+    ],
+    stamp = True,
+)
+
+genrule(
+    name = "invalid_changed_ip",
+    srcs = ["//acceptance/topo_common:topology"],
+    outs = ["topology_invalid_changed_ip.json"],
+    cmd = "jq '.PathService[].Addrs.IPv4.Public.Addr = \"242.42.42.2\"' $(location //acceptance/topo_common:topology) > $@",
+)
+
+genrule(
+    name = "invalid_changed_port",
+    srcs = ["//acceptance/topo_common:topology"],
+    outs = ["topology_invalid_changed_port.json"],
+    cmd = "jq '.PathService[].Addrs.IPv4.Public.L4Port = 42424' $(location //acceptance/topo_common:topology) > $@",
+)
diff --git a/acceptance/topo_ps_reload/docker-compose.yml b/acceptance/topo_ps_reload/docker-compose.yml
new file mode 100644
index 000000000..981a5ee32
--- /dev/null
+++ b/acceptance/topo_ps_reload/docker-compose.yml
@@ -0,0 +1,26 @@
+networks:
+  bridge1:
+    driver: bridge
+    driver_opts:
+      com.docker.network.bridge.name: bridge1
+    ipam:
+      config:
+      - subnet: 242.253.100.0/24
+services:
+  topo_ps_reload_dispatcher:
+    container_name: topo_ps_reload_dispatcher
+    image: bazel/acceptance/topo_ps_reload:dispatcher
+    networks:
+      bridge1:
+        ipv4_address: 242.253.100.2
+    volumes:
+    - vol_topo_ps_reload_disp:/run/shm/dispatcher:rw
+  topo_ps_reload_path_srv:
+    container_name: topo_ps_reload_path_srv
+    image: bazel/acceptance/topo_ps_reload:path_srv
+    volumes:
+    - vol_topo_ps_reload_disp:/run/shm/dispatcher:ro
+    network_mode: service:topo_ps_reload_dispatcher
+version: '2.4'
+volumes:
+  vol_topo_ps_reload_disp: null
diff --git a/acceptance/topo_sd_reload/BUILD.bazel b/acceptance/topo_sd_reload/BUILD.bazel
new file mode 100644
index 000000000..00f3d83ac
--- /dev/null
+++ b/acceptance/topo_sd_reload/BUILD.bazel
@@ -0,0 +1,49 @@
+load("@io_bazel_rules_docker//container:container.bzl", "container_image")
+load("@io_bazel_rules_go//go:def.bzl", "go_test")
+
+go_test(
+    name = "go_default_test",
+    srcs = ["reload_test.go"],
+    data = [
+        ":dispatcher",
+        ":sciond",
+        ":docker-compose.yml",
+        "//acceptance/topo_common:topology",
+        "//acceptance/topo_common:invalid_reloads",
+        "testdata/topology_reload.json",
+    ],
+    deps = [
+        "//go/lib/topology:go_default_library",
+        "@com_github_stretchr_testify//assert:go_default_library",
+        "@com_github_stretchr_testify//require:go_default_library",
+    ],
+)
+
+container_image(
+    name = "dispatcher",
+    base = "//docker/perapp:dispatcher_go_debug",
+    files = ["testdata/disp.toml"],
+    entrypoint = [
+        "/app/godispatcher",
+        "-config",
+        "/disp.toml",
+    ],
+    stamp = True,
+)
+
+container_image(
+    name = "sciond",
+    base = "//docker/perapp:sciond_debug",
+    files = [
+        "testdata/sd.toml",
+        "//acceptance/topo_common:topology",
+        "//acceptance/topo_common:invalid_reloads",
+        "testdata/topology_reload.json",
+    ],
+    entrypoint = [
+        "/app/sciond",
+        "-config",
+        "/sd.toml",
+    ],
+    stamp = True,
+)
diff --git a/acceptance/topo_sd_reload/docker-compose.yml b/acceptance/topo_sd_reload/docker-compose.yml
new file mode 100644
index 000000000..dec59bb47
--- /dev/null
+++ b/acceptance/topo_sd_reload/docker-compose.yml
@@ -0,0 +1,26 @@
+networks:
+  bridge1:
+    driver: bridge
+    driver_opts:
+      com.docker.network.bridge.name: bridge1
+    ipam:
+      config:
+      - subnet: 242.254.100.0/24
+services:
+  topo_sd_reload_dispatcher:
+    container_name: topo_sd_reload_dispatcher
+    image: bazel/acceptance/topo_sd_reload:dispatcher
+    networks:
+      bridge1:
+        ipv4_address: 242.254.100.2
+    volumes:
+    - vol_topo_sd_reload_disp:/run/shm/dispatcher:rw
+  topo_sd_reload_sciond:
+    container_name: topo_sd_reload_sciond
+    image: bazel/acceptance/topo_sd_reload:sciond
+    volumes:
+    - vol_topo_sd_reload_disp:/run/shm/dispatcher:ro
+    network_mode: service:topo_sd_reload_dispatcher
+version: '2.4'
+volumes:
+  vol_topo_sd_reload_disp: null
diff --git a/go/lib/common/BUILD.bazel b/go/lib/common/BUILD.bazel
index dc7f645fd..df07f1583 100644
--- a/go/lib/common/BUILD.bazel
+++ b/go/lib/common/BUILD.bazel
@@ -28,5 +28,6 @@ go_test(
     deps = [
         "@com_github_smartystreets_goconvey//convey:go_default_library",
         "@com_github_stretchr_testify//assert:go_default_library",
+        "@com_github_stretchr_testify//require:go_default_library",
     ],
 )
diff --git a/go/lib/common/defs.go b/go/lib/common/defs.go
index aba5e682a..10aa653a3 100644
--- a/go/lib/common/defs.go
+++ b/go/lib/common/defs.go
@@ -18,6 +18,7 @@ package common
 import (
 	"reflect"
 	"strconv"
+	"strings"
 )
 
 const (
@@ -42,15 +43,21 @@ const (
 	CPService = "Control Plane Service"
 )
 
-// Interface ID
+// IFIDType is the type for interface IDs.
 type IFIDType uint64
 
 func (ifid IFIDType) String() string {
 	return strconv.FormatUint(uint64(ifid), 10)
 }
 
+// UnmarshalJSON unmarshals the JSON data into the IfID.
+func (ifid *IFIDType) UnmarshalJSON(data []byte) error {
+	return ifid.UnmarshalText(data)
+}
+
+// UnmarshalText unmarshals the text into the IfID.
 func (ifid *IFIDType) UnmarshalText(text []byte) error {
-	i, err := strconv.ParseUint(string(text), 10, 64)
+	i, err := strconv.ParseUint(strings.ReplaceAll(string(text), "\"", ""), 10, 64)
 	if err != nil {
 		return err
 	}
diff --git a/go/lib/snet/snet.go b/go/lib/snet/snet.go
index bda1b22db..2ed8cfe46 100644
--- a/go/lib/snet/snet.go
+++ b/go/lib/snet/snet.go
@@ -144,7 +144,7 @@ func (n *SCIONNetwork) Listen(ctx context.Context, network string, listen *net.U
 		return nil, serrors.New("nil listen addr not supported")
 	}
 	if listen.IP == nil {
-		return nil, serrors.New("nil listen IP no supported")
+		return nil, serrors.New("nil listen IP not supported")
 	}
 	if listen.IP.IsUnspecified() {
 		return nil, serrors.New("unspecified listen IP not supported")
diff --git a/go/lib/topology/overlay/BUILD.bazel b/go/lib/topology/overlay/BUILD.bazel
index 6a24f772b..a72906b90 100644
--- a/go/lib/topology/overlay/BUILD.bazel
+++ b/go/lib/topology/overlay/BUILD.bazel
@@ -1,4 +1,4 @@
-load("@io_bazel_rules_go//go:def.bzl", "go_library")
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
 
 go_library(
     name = "go_default_library",
@@ -7,3 +7,13 @@ go_library(
     visibility = ["//visibility:public"],
     deps = ["//go/lib/common:go_default_library"],
 )
+
+go_test(
+    name = "go_default_test",
+    srcs = ["defs_test.go"],
+    embed = [":go_default_library"],
+    deps = [
+        "@com_github_stretchr_testify//assert:go_default_library",
+        "@com_github_stretchr_testify//require:go_default_library",
+    ],
+)
diff --git a/go/lib/topology/overlay/defs.go b/go/lib/topology/overlay/defs.go
index f1be3a501..ece7421b9 100644
--- a/go/lib/topology/overlay/defs.go
+++ b/go/lib/topology/overlay/defs.go
@@ -1,4 +1,5 @@
 // Copyright 2017 ETH Zurich
+// Copyright 2020 ETH Zurich, Anapaya Systems
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -69,6 +70,19 @@ func TypeFromString(s string) (Type, error) {
 	}
 }
 
+func (ot *Type) UnmarshalJSON(data []byte) error {
+	var strVal string
+	if err := json.Unmarshal(data, &strVal); err != nil {
+		return err
+	}
+	t, err := TypeFromString(strVal)
+	if err != nil {
+		return err
+	}
+	*ot = t
+	return nil
+}
+
 func (ot Type) MarshalJSON() ([]byte, error) {
 	return json.Marshal(ot.String())
 }
diff --git a/go/lib/topology/topology.go b/go/lib/topology/topology.go
index b48125e0b..f4e8555aa 100644
--- a/go/lib/topology/topology.go
+++ b/go/lib/topology/topology.go
@@ -119,7 +119,7 @@ type (
 	// the underlay to be used for contacting said service.
 	TopoAddr struct {
 		SCIONAddress    *net.UDPAddr
-		UnderlayAddress net.Addr
+		UnderlayAddress *net.UDPAddr
 	}
 )
 
@@ -423,7 +423,7 @@ func (i IFInfo) String() string {
 // FIXME(scrye): This should be removed; applications should not need to look into the underlay
 // concrete type.
 func (a *TopoAddr) UnderlayAddr() *net.UDPAddr {
-	return a.UnderlayAddress.(*net.UDPAddr)
+	return a.UnderlayAddress
 }
 
 func (a *TopoAddr) String() string {
@@ -440,7 +440,7 @@ func (a *TopoAddr) copy() *TopoAddr {
 			IP:   append(a.SCIONAddress.IP[:0:0], a.SCIONAddress.IP...),
 			Port: a.SCIONAddress.Port,
 		},
-		UnderlayAddress: toUDPAddr(a.UnderlayAddress.(*net.UDPAddr)),
+		UnderlayAddress: toUDPAddr(a.UnderlayAddress),
 	}
 }
 
diff --git a/scion.sh b/scion.sh
index 69c5766e6..9083b2e92 100755
--- a/scion.sh
+++ b/scion.sh
@@ -291,7 +291,7 @@ go_lint() {
     local TMPDIR=$(mktemp -d /tmp/scion-lint.XXXXXXX)
     local LOCAL_DIRS="$(find go/* -maxdepth 0 -type d | grep -v vendor)"
     # Find go files to lint, excluding generated code. For linelen and misspell.
-    find go -type f -iname '*.go' \
+    find go acceptance -type f -iname '*.go' \
       -a '!' -ipath 'go/proto/structs.gen.go' \
       -a '!' -ipath 'go/proto/*.capnp.go' \
       -a '!' -ipath '*mock_*' \
@@ -304,12 +304,13 @@ go_lint() {
     lint_step "impi"
     # Skip CGO (https://github.com/pavius/impi/issues/5) files.
     $TMPDIR/impi --local github.com/scionproto/scion --scheme stdThirdPartyLocal --skip '/c\.go$' --skip 'mock_' --skip 'go/proto/.*\.capnp\.go' --skip 'go/proto/structs.gen.go' ./go/... || ret=1
+    $TMPDIR/impi --local github.com/scionproto/scion --scheme stdThirdPartyLocal ./acceptance/... || ret=1
     lint_step "gofmt"
     # TODO(sustrik): At the moment there are no bazel rules for gofmt.
     # See: https://github.com/bazelbuild/rules_go/issues/511
     # Instead we'll just run the commands from Go SDK directly.
     GOSDK=$(bazel info output_base)/external/go_sdk/bin
-    out=$($GOSDK/gofmt -d -s $LOCAL_DIRS);
+    out=$($GOSDK/gofmt -d -s $LOCAL_DIRS ./acceptance);
     if [ -n "$out" ]; then echo "$out"; ret=1; fi
     lint_step "linelen (lll)"
     out=$($TMPDIR/lll -w 4 -l 100 --files -e '`comment:"|`ini:"|https?:' < $TMPDIR/gofiles.list);
@@ -317,7 +318,7 @@ go_lint() {
     lint_step "misspell"
     xargs -a $TMPDIR/gofiles.list $TMPDIR/misspell -error || ret=1
     lint_step "ineffassign"
-    $TMPDIR/ineffassign -exclude ineffassign.json go || ret=1
+    $TMPDIR/ineffassign -exclude ineffassign.json go acceptance || ret=1
     lint_step "bazel"
     make gazelle GAZELLE_MODE=diff || ret=1
     # Clean up the binaries


Test patch:
diff --git a/acceptance/sig_failover/test b/acceptance/sig_failover/test
index a056d3a9b..a6fad34b2 100755
--- a/acceptance/sig_failover/test
+++ b/acceptance/sig_failover/test
@@ -92,6 +92,6 @@ for FILE in sig1-ff00_0_110.log sig1-ff00_0_111.log disp_1-ff00_0_110.log disp_1
     docker-compose -f acceptance/sig_failover/docker-compose.yml run log_exporter cat /share/logs/$FILE > $OUTPUT_DIR/logs/$FILE
 done
 
-docker-compose -f acceptance/sig_failover/docker-compose.yml down
+docker-compose -f acceptance/sig_failover/docker-compose.yml down -v
 
 exit $RC
diff --git a/acceptance/sig_short_exp_time/test b/acceptance/sig_short_exp_time/test
index ce0d53113..ecdb73a98 100755
--- a/acceptance/sig_short_exp_time/test
+++ b/acceptance/sig_short_exp_time/test
@@ -101,6 +101,6 @@ for FILE in sig1-ff00_0_110.log sig1-ff00_0_111.log disp_1-ff00_0_110.log disp_1
     docker-compose -f acceptance/sig_short_exp_time/docker-compose.yml run log_exporter cat /share/logs/$FILE > $OUTPUT_DIR/logs/$FILE
 done
 
-docker-compose -f acceptance/sig_short_exp_time/docker-compose.yml down
+docker-compose -f acceptance/sig_short_exp_time/docker-compose.yml down -v
 
 exit $RC
diff --git a/acceptance/topo_invalid_reloads_acceptance/test b/acceptance/topo_invalid_reloads_acceptance/test
deleted file mode 100755
index ef63aa088..000000000
--- a/acceptance/topo_invalid_reloads_acceptance/test
+++ /dev/null
@@ -1,84 +0,0 @@
-#!/bin/bash
-
-# Whenever immutable fields in the topology file of an infra element is
-# changed, if the process receives a SIGHUP it will fail to reload the config.
-
-. acceptance/common.sh
-
-TEST_NAME="topo_invalid_reloads"
-TEST_TOPOLOGY="topology/Tiny.topo"
-
-IA=${IA:-1-ff00:0:112}
-IA_FILE="$(ia_file $IA)"
-AS_FILE="$(as_file $IA)"
-
-TOPO="gen/ISD1/AS$AS_FILE/topology.json"
-
-test_setup() {
-    set -e
-    ./scion.sh topology -c $TEST_TOPOLOGY -d -t
-    sed -i '/\[logging\.file\]/a FlushInterval = 1' gen/ISD1/*/*/*.toml
-    ./tools/dc start scion_ps$IA_FILE-1 scion_sd$IA_FILE
-    docker_status
-}
-
-test_run() {
-    set -e
-    cp "gen/ISD1/AS$AS_FILE/ps$IA_FILE-1/topology.json" "$TOPO"
-    test_ps_immutable "ps$IA_FILE-1" "gen/ISD1/AS$AS_FILE/ps$IA_FILE-1/topology.json" "PS"
-    test_immutable "sd$IA_FILE" "gen/ISD1/AS$AS_FILE/endhost/topology.json" "SD"
-
-}
-
-test_ps_immutable() {
-    test_immutable $1 $2 "$3"
-    jq '.PathService[].Addrs.IPv4.Public.Addr = "242.42.42.42"' $TOPO | sponge $2
-    check_no_reload $1 4 "$3: Addr"
-    jq '.PathService[].Addrs.IPv4.Public.L4Port = 42424' $TOPO | sponge $2
-    check_no_reload $1 5 "$3: L4Port"
-}
-
-test_immutable() {
-    jq '.ISD_AS = "1-ff00:0:111"' $TOPO | sponge $2
-    check_no_reload "$1" 1 "$3: ISD_AS"
-    jq '.Attributes = ["core"] | del( .BorderRouters[].Interfaces )' $TOPO | sponge $2
-    check_no_reload "$1" 2 "$3: Attributes"
-    jq '.MTU = 42' $TOPO | sponge $2
-    check_no_reload "$1" 3 "$3: MTU"
-}
-
-check_no_reload() {
-    ./tools/dc scion kill -s HUP scion_"$1"
-    sleep 2
-    local errors=$( grep "Unable to set topology" "logs/$1.log" | wc -l )
-    if [ "$errors" != $2 ]; then
-        echo "FAIL: Successful reload should not be possible. step=( $3 ) expErrCount=$2 actErrCount=$errors "
-        return 1
-    fi
-}
-
-print_help() {
-    echo
-	cat <<-_EOF
-	    $PROGRAM name
-	        return the name of this test
-	    $PROGRAM setup
-	        execute only the setup phase.
-	    $PROGRAM run
-	        execute only the run phase.
-	    $PROGRAM teardown
-	        execute only the teardown phase.
-	_EOF
-}
-
-PROGRAM=`basename "$0"`
-COMMAND="$1"
-
-case "$COMMAND" in
-    name)
-        echo $TEST_NAME ;;
-    setup|run|teardown)
-        "test_$COMMAND" ;;
-    *) print_help; exit 1 ;;
-esac
-
diff --git a/acceptance/topo_ps_reload/reload_test.go b/acceptance/topo_ps_reload/reload_test.go
new file mode 100644
index 000000000..cb7028291
--- /dev/null
+++ b/acceptance/topo_ps_reload/reload_test.go
@@ -0,0 +1,145 @@
+// Copyright 2019 Anapaya Systems
+//
+// 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 reload_test
+
+import (
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"os"
+	"os/exec"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/scionproto/scion/go/lib/topology"
+)
+
+func TestPSTopoReload(t *testing.T) {
+	setupTest(t)
+	defer teardownTest(t)
+
+	// first load the topo files to memory for comparison.
+	origTopo, err := topology.RWTopologyFromJSONFile("../topo_common/topology.json")
+	assert.NoError(t, err, "Loading origTopo failed")
+	reloadTopo, err := topology.RWTopologyFromJSONFile("testdata/topology_reload.json")
+	assert.NoError(t, err, "Loading reloadTopo failed")
+
+	// check initial topo matches expected.
+	checkTopology(t, origTopo)
+
+	// try invalid topos first.
+	invalidFiles := []string{
+		"/topology_invalid_ia.json",
+		"/topology_invalid_attributes.json",
+		"/topology_invalid_mtu.json",
+		"/topology_invalid_changed_ip.json",
+		"/topology_invalid_changed_port.json",
+	}
+	for _, invalidFile := range invalidFiles {
+		t.Logf("loading %s", invalidFile)
+		loadTopo(t, invalidFile)
+		checkTopology(t, origTopo)
+	}
+
+	// now try to load a valid one.
+	loadTopo(t, "/topology_reload.json")
+	checkTopology(t, reloadTopo)
+}
+
+func setupTest(t *testing.T) {
+	// first load the docker images from bazel into the docker deamon, the
+	// scripts are in the same folder as this test runs in bazel.
+	mustExec(t, "dispatcher")
+	mustExec(t, "path_srv")
+	// now start the docker containers
+	mustExec(t, "docker-compose", "-f", "docker-compose.yml", "up",
+		"-d", "topo_ps_reload_dispatcher", "topo_ps_reload_path_srv")
+	// wait a bit to make sure the containers are ready.
+	time.Sleep(time.Second / 2)
+	t.Log("Test setup done")
+	mustExec(t, "docker-compose", "-f", "docker-compose.yml", "ps")
+}
+
+func teardownTest(t *testing.T) {
+	defer mustExec(t, "docker-compose", "-f", "docker-compose.yml", "down", "-v")
+
+	outdir, exists := os.LookupEnv("TEST_UNDECLARED_OUTPUTS_DIR")
+	require.True(t, exists, "TEST_UNDECLARED_OUTPUTS_DIR must be defined")
+	require.NoError(t, os.MkdirAll(fmt.Sprintf("%s/logs", outdir), os.ModePerm|os.ModeDir))
+	// collect logs
+	for service, file := range map[string]string{
+		"topo_ps_reload_dispatcher": "disp.log",
+		"topo_ps_reload_path_srv":   "path_srv.log",
+	} {
+		cmd := exec.Command("docker-compose", "-f", "docker-compose.yml", "logs", "--no-color",
+			service)
+		logFileName := fmt.Sprintf("%s/logs/%s", outdir, file)
+		logFile, err := os.Create(logFileName)
+		if err != nil {
+			t.Logf("Failed to create logfile %s for %s", logFileName, service)
+			continue
+		}
+		cmd.Stdout = logFile
+		if err = cmd.Run(); err != nil {
+			t.Logf("Failed to read log for service %s: %v\n", service, err)
+		}
+	}
+}
+
+func loadTopo(t *testing.T, name string) {
+	t.Helper()
+
+	mustExec(t, "docker-compose", "-f", "docker-compose.yml", "exec", "-T",
+		"topo_ps_reload_path_srv", "mv", name, "/topology.json")
+	mustExec(t, "docker-compose", "-f", "docker-compose.yml", "kill", "-s", "SIGHUP",
+		"topo_ps_reload_path_srv")
+}
+
+func mustExec(t *testing.T, name string, arg ...string) {
+	t.Helper()
+
+	cmd := exec.Command(name, arg...)
+	output, err := cmd.Output()
+	t.Logf("%s %v\n%s\n", name, arg, string(output))
+	require.NoError(t, err, "Failed to run %s %v: %v\n%s", name, arg, err, string(output))
+}
+
+func checkTopology(t *testing.T, expectedTopo *topology.RWTopology) {
+	t.Helper()
+
+	eJSON, err := json.Marshal(expectedTopo)
+	require.NoError(t, err)
+	actualTopo := fetchTopologyFromEndpoint(t, "http://242.253.100.2:30453/topology")
+	aJSON, err := json.Marshal(actualTopo)
+	require.NoError(t, err)
+	assert.Equal(t, eJSON, aJSON)
+}
+
+func fetchTopologyFromEndpoint(t *testing.T, url string) *topology.RWTopology {
+	t.Helper()
+
+	resp, err := http.Get(url)
+	require.NoError(t, err)
+	defer resp.Body.Close()
+	body, err := ioutil.ReadAll(resp.Body)
+	require.NoError(t, err)
+	var topo *topology.RWTopology
+	require.NoError(t, json.Unmarshal(body, &topo))
+	return topo
+}
diff --git a/acceptance/topo_ps_reload/testdata/disp.toml b/acceptance/topo_ps_reload/testdata/disp.toml
new file mode 100644
index 000000000..f38528293
--- /dev/null
+++ b/acceptance/topo_ps_reload/testdata/disp.toml
@@ -0,0 +1,8 @@
+[dispatcher]
+ID = "disp_1-ff00_0_110"
+
+[features]
+AllowRunAsRoot = true
+
+[logging.console]
+Level = "trace"
diff --git a/acceptance/topo_ps_reload/testdata/ps.toml b/acceptance/topo_ps_reload/testdata/ps.toml
new file mode 100644
index 000000000..09a060669
--- /dev/null
+++ b/acceptance/topo_ps_reload/testdata/ps.toml
@@ -0,0 +1,18 @@
+[general]
+ReconnectToDispatcher = true
+ConfigDir = "/"
+ID = "ps1-ff00_0_110-1"
+
+[trustDB]
+Connection = "ps1-ff00_0_110-1.trust.db"
+Backend = "sqlite"
+
+[logging.console]
+Level = "trace"
+
+[ps.pathDB]
+Connection = "ps1-ff00_0_110-1.path.db"
+Backend = "sqlite"
+
+[metrics]
+Prometheus = "242.253.100.2:30453"
diff --git a/acceptance/topo_ps_reload/testdata/topology_reload.json b/acceptance/topo_ps_reload/testdata/topology_reload.json
new file mode 100644
index 000000000..1c8d2c839
--- /dev/null
+++ b/acceptance/topo_ps_reload/testdata/topology_reload.json
@@ -0,0 +1,121 @@
+{
+  "MTU": 1400,
+  "BorderRouters": {
+    "br1-ff00_0_110-1": {
+      "Interfaces": {
+        "1": {
+          "MTU": 1280,
+          "RemoteOverlay": {
+            "Addr": "127.0.1.5",
+            "OverlayPort": 50000
+          },
+          "Bandwidth": 1000,
+          "ISD_AS": "1-ff00:0:111",
+          "PublicOverlay": {
+            "Addr": "127.0.1.4",
+            "OverlayPort": 50000
+          },
+          "LinkTo": "CHILD",
+          "Overlay": "UDP/IPv4"
+        }
+      },
+      "InternalAddrs": {
+        "IPv4": {
+          "PublicOverlay": {
+            "Addr": "127.0.1.9",
+            "OverlayPort": 31008
+          }
+        }
+      },
+      "CtrlAddr": {
+        "IPv4": {
+          "Public": {
+            "Addr": "127.0.1.9",
+            "L4Port": 31006
+          }
+        }
+      }
+    },
+    "br1-ff00_0_110-2": {
+      "Interfaces": {
+        "2": {
+          "MTU": 1472,
+          "RemoteOverlay": {
+            "Addr": "127.0.1.7",
+            "OverlayPort": 50000
+          },
+          "Bandwidth": 500,
+          "ISD_AS": "1-ff00:0:112",
+          "PublicOverlay": {
+            "Addr": "127.0.1.6",
+            "OverlayPort": 50000
+          },
+          "LinkTo": "CHILD",
+          "Overlay": "UDP/IPv4"
+        }
+      },
+      "InternalAddrs": {
+        "IPv4": {
+          "PublicOverlay": {
+            "Addr": "127.0.1.10",
+            "OverlayPort": 31012
+          }
+        }
+      },
+      "CtrlAddr": {
+        "IPv4": {
+          "Public": {
+            "Addr": "127.0.1.10",
+            "L4Port": 31010
+          }
+        }
+      }
+    }
+  },
+  "ColibriService": {},
+  "DiscoveryService": {},
+  "PathService": {
+    "ps1-ff00_0_110-1": {
+      "Addrs": {
+        "IPv4": {
+          "Public": {
+            "Addr": "127.0.0.13",
+            "L4Port": 31004
+          }
+        }
+      }
+    }
+  },
+  "Attributes": [
+    "authoritative",
+    "core",
+    "issuing",
+    "voting"
+  ],
+  "ISD_AS": "1-ff00:0:110",
+  "CertificateService": {
+    "cs1-ff00_0_110-1": {
+      "Addrs": {
+        "IPv4": {
+          "Public": {
+            "Addr": "127.0.1.12",
+            "L4Port": 31002
+          }
+        }
+      }
+    }
+  },
+  "BeaconService": {
+    "bs1-ff00_0_110-1": {
+      "Addrs": {
+        "IPv4": {
+          "Public": {
+            "Addr": "127.0.1.11",
+            "L4Port": 31000
+          }
+        }
+      }
+    }
+  },
+  "Overlay": "UDP/IPv4"
+}
diff --git a/acceptance/topo_ps_reloads_br_acceptance/test b/acceptance/topo_ps_reloads_br_acceptance/test
deleted file mode 100755
index 0d581eba6..000000000
--- a/acceptance/topo_ps_reloads_br_acceptance/test
+++ /dev/null
@@ -1,64 +0,0 @@
-#!/bin/bash
-
-# Whenever the BR's internal address in the topology file of a PS server is
-# changed, if the PS process receives a SIGHUP it will reload the config and
-# use the new internal address as next-hop when doing path segment
-# synchronization with other core PSes.
-
-. acceptance/common.sh
-
-TEST_NAME="topo_ps_reloads_br"
-TEST_TOPOLOGY="topology/Tiny.topo"
-
-SRC_IA=${SRC_IA:-1-ff00:0:112}
-SRC_IA_FILE="$(ia_file $SRC_IA)"
-SRC_AS_FILE="$(as_file $SRC_IA)"
-DST_IA=${DST_IA:-1-ff00:0:111}
-
-test_setup() {
-    set -e
-    ./scion.sh topology -c $TEST_TOPOLOGY -d -t
-    sed -i '/\[logging\.file\]/a FlushInterval = 1' gen/ISD1/*/ps*/ps.toml
-    ./scion.sh run nobuild
-    ./tools/dc start tester_$SRC_IA_FILE
-    docker_status
-}
-
-test_run() {
-    set -e
-    local topo_file="gen/ISD1/AS$SRC_AS_FILE/ps$SRC_IA_FILE-1/topology.json"
-    jq '.BorderRouters[].InternalAddrs.IPv4.PublicOverlay = {Addr: "127.42.42.42", OverlayPort: 39999}' $topo_file | sponge $topo_file
-    ./tools/dc scion kill -s HUP scion_ps"$SRC_IA_FILE"-1
-    sqlite3 gen-cache/ps"$SRC_IA_FILE"-1.path.db "delete from NextQuery;"
-    sleep 6
-    ./tools/dc exec_tester $SRC_IA_FILE bin/showpaths -srcIA $SRC_IA -dstIA $DST_IA -sciondFromIA || true
-    sleep 2
-    grep -q "\[TRACE\] \[Acceptance\] .*overlay=127\.42\.42\.42" "logs/ps$SRC_IA_FILE-1.log" || \
-        fail "Request to 127.42.42.42 not found in logs"
-}
-
-print_help() {
-    echo
-	cat <<-_EOF
-	    $PROGRAM name
-	        return the name of this test
-	    $PROGRAM setup
-	        execute only the setup phase.
-	    $PROGRAM run
-	        execute only the run phase.
-	    $PROGRAM teardown
-	        execute only the teardown phase.
-	_EOF
-}
-
-PROGRAM=`basename "$0"`
-COMMAND="$1"
-
-case "$COMMAND" in
-    name)
-        echo $TEST_NAME ;;
-    setup|run|teardown)
-        "test_$COMMAND" ;;
-    *) print_help; exit 1 ;;
-esac
-
diff --git a/acceptance/topo_ps_reloads_cs_acceptance_disabled/test b/acceptance/topo_ps_reloads_cs_acceptance_disabled/test
deleted file mode 100755
index 00bd83c78..000000000
--- a/acceptance/topo_ps_reloads_cs_acceptance_disabled/test
+++ /dev/null
@@ -1,63 +0,0 @@
-#!/bin/bash
-
-# Whenever the CS addresses in the topology file of a PS server are changed, if
-# the PS process receives a SIGHUP it will reload the config and push future
-# crypto to the new address.
-
-. acceptance/common.sh
-
-TEST_NAME="topo_ps_reloads_cs"
-TEST_TOPOLOGY="topology/Tiny.topo"
-
-IA=${IA:-1-ff00:0:112}
-IA_FILE="$(ia_file $IA)"
-AS_FILE="$(as_file $IA)"
-
-test_setup() {
-    set -e
-    ./scion.sh topology -c $TEST_TOPOLOGY -d -t
-    sed -i -e '/\[logging\.file\]/a FlushInterval = 1'\
-            -e '/\[ps\]/a\CryptoSyncInterval="1s"' gen/ISD1/*/ps*/ps.toml
-    ./scion.sh run nobuild
-    docker_status
-}
-
-test_run() {
-    set -e
-    # Wait for the PS to be populated with crypto following beaconing
-    sleep 2
-    local topo_file="gen/ISD1/AS$AS_FILE/ps$IA_FILE-1/topology.json"
-    jq '.CertificateService[].Addrs.IPv4.Public = {Addr: "127.42.42.42", L4Port: 39999}' $topo_file | sponge $topo_file
-    ./tools/dc scion kill -s HUP scion_ps$IA_FILE-1
-    # Wait for 1 (PS crypto push interval) + 1 (logging flush interval)
-    # + 1  to guarantee a push is seen
-    sleep 3
-    grep -q "\[TRACE\] \[Acceptance\] .*overlay=127\.42\.42\.42" "logs/ps$IA_FILE-1.log" || \
-        fail "Push request to 127.42.42.42 not found in logs"
-}
-
-print_help() {
-    echo
-	cat <<-_EOF
-	    $PROGRAM name
-	        return the name of this test
-	    $PROGRAM setup
-	        execute only the setup phase.
-	    $PROGRAM run
-	        execute only the run phase.
-	    $PROGRAM teardown
-	        execute only the teardown phase.
-	_EOF
-}
-
-PROGRAM=`basename "$0"`
-COMMAND="$1"
-
-case "$COMMAND" in
-    name)
-        echo $TEST_NAME ;;
-    setup|run|teardown)
-        "test_$COMMAND" ;;
-    *) print_help; exit 1 ;;
-esac
-
diff --git a/acceptance/topo_sd_reload/reload_test.go b/acceptance/topo_sd_reload/reload_test.go
new file mode 100644
index 000000000..3ecb1b8c8
--- /dev/null
+++ b/acceptance/topo_sd_reload/reload_test.go
@@ -0,0 +1,143 @@
+// Copyright 2019 Anapaya Systems
+//
+// 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 reload_test
+
+import (
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"os"
+	"os/exec"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/scionproto/scion/go/lib/topology"
+)
+
+func TestSDTopoReload(t *testing.T) {
+	setupTest(t)
+	defer teardownTest(t)
+
+	// first load the topo files to memory for comparison.
+	origTopo, err := topology.RWTopologyFromJSONFile("../topo_common/topology.json")
+	assert.NoError(t, err, "Loading origTopo failed")
+	reloadTopo, err := topology.RWTopologyFromJSONFile("testdata/topology_reload.json")
+	assert.NoError(t, err, "Loading reloadTopo failed")
+
+	// check initial topo matches expected.
+	checkTopology(t, origTopo)
+
+	// try invalid topos first.
+	invalidFiles := []string{
+		"/topology_invalid_ia.json",
+		"/topology_invalid_attributes.json",
+		"/topology_invalid_mtu.json",
+	}
+	for _, invalidFile := range invalidFiles {
+		t.Logf("loading %s", invalidFile)
+		loadTopo(t, invalidFile)
+		checkTopology(t, origTopo)
+	}
+
+	// now try to load a valid one.
+	loadTopo(t, "/topology_reload.json")
+	checkTopology(t, reloadTopo)
+}
+
+func setupTest(t *testing.T) {
+	// first load the docker images from bazel into the docker deamon, the
+	// scripts are in the same folder as this test runs in bazel.
+	mustExec(t, "dispatcher")
+	mustExec(t, "sciond")
+	// now start the docker containers
+	mustExec(t, "docker-compose", "-f", "docker-compose.yml", "up",
+		"-d", "topo_sd_reload_dispatcher", "topo_sd_reload_sciond")
+	// wait a bit to make sure the containers are ready.
+	time.Sleep(time.Second / 2)
+	t.Log("Test setup done")
+	mustExec(t, "docker-compose", "-f", "docker-compose.yml", "ps")
+}
+
+func teardownTest(t *testing.T) {
+	defer mustExec(t, "docker-compose", "-f", "docker-compose.yml", "down", "-v")
+
+	outdir, exists := os.LookupEnv("TEST_UNDECLARED_OUTPUTS_DIR")
+	require.True(t, exists, "TEST_UNDECLARED_OUTPUTS_DIR must be defined")
+	require.NoError(t, os.MkdirAll(fmt.Sprintf("%s/logs", outdir), os.ModePerm|os.ModeDir))
+	// collect logs
+	for service, file := range map[string]string{
+		"topo_sd_reload_dispatcher": "disp.log",
+		"topo_sd_reload_sciond":     "sciond.log",
+	} {
+		cmd := exec.Command("docker-compose", "-f", "docker-compose.yml", "logs", "--no-color",
+			service)
+		logFileName := fmt.Sprintf("%s/logs/%s", outdir, file)
+		logFile, err := os.Create(logFileName)
+		if err != nil {
+			t.Logf("Failed to create logfile %s for %s", logFileName, service)
+			continue
+		}
+		cmd.Stdout = logFile
+		if err = cmd.Run(); err != nil {
+			t.Logf("Failed to read log for service %s: %v\n", service, err)
+		}
+	}
+}
+
+func loadTopo(t *testing.T, name string) {
+	t.Helper()
+
+	mustExec(t, "docker-compose", "-f", "docker-compose.yml", "exec", "-T",
+		"topo_sd_reload_sciond", "mv", name, "/topology.json")
+	mustExec(t, "docker-compose", "-f", "docker-compose.yml", "kill", "-s", "SIGHUP",
+		"topo_sd_reload_sciond")
+}
+
+func mustExec(t *testing.T, name string, arg ...string) {
+	t.Helper()
+
+	cmd := exec.Command(name, arg...)
+	output, err := cmd.Output()
+	t.Logf("%s %v\n%s\n", name, arg, string(output))
+	require.NoError(t, err, "Failed to run %s %v: %v\n%s", name, arg, err, string(output))
+}
+
+func checkTopology(t *testing.T, expectedTopo *topology.RWTopology) {
+	t.Helper()
+
+	eJSON, err := json.Marshal(expectedTopo)
+	require.NoError(t, err)
+	actualTopo := fetchTopologyFromEndpoint(t, "http://242.254.100.2:30455/topology")
+	aJSON, err := json.Marshal(actualTopo)
+	require.NoError(t, err)
+	assert.Equal(t, eJSON, aJSON)
+}
+
+func fetchTopologyFromEndpoint(t *testing.T, url string) *topology.RWTopology {
+	t.Helper()
+
+	resp, err := http.Get(url)
+	require.NoError(t, err)
+	defer resp.Body.Close()
+	body, err := ioutil.ReadAll(resp.Body)
+	require.NoError(t, err)
+	var topo *topology.RWTopology
+	require.NoError(t, json.Unmarshal(body, &topo))
+	return topo
+}
diff --git a/acceptance/topo_sd_reload/testdata/disp.toml b/acceptance/topo_sd_reload/testdata/disp.toml
new file mode 100644
index 000000000..f38528293
--- /dev/null
+++ b/acceptance/topo_sd_reload/testdata/disp.toml
@@ -0,0 +1,8 @@
+[dispatcher]
+ID = "disp_1-ff00_0_110"
+
+[features]
+AllowRunAsRoot = true
+
+[logging.console]
+Level = "trace"
diff --git a/acceptance/topo_sd_reload/testdata/sd.toml b/acceptance/topo_sd_reload/testdata/sd.toml
new file mode 100644
index 000000000..c035f79fc
--- /dev/null
+++ b/acceptance/topo_sd_reload/testdata/sd.toml
@@ -0,0 +1,21 @@
+[general]
+ReconnectToDispatcher = true
+ConfigDir = "/"
+ID = "sd1-ff00_0_110"
+
+[trustDB]
+Connection = "sd1-ff00_0_110.trust.db"
+Backend = "sqlite"
+
+[logging.console]
+Level = "trace"
+
+[sd]
+Public = "[242.254.100.2]:0"
+
+[sd.pathDB]
+Connection = "sd1-ff00_0_110.path.db"
+Backend = "sqlite"
+
+[metrics]
+Prometheus = "242.254.100.2:30455"
diff --git a/acceptance/topo_sd_reload/testdata/topology_reload.json b/acceptance/topo_sd_reload/testdata/topology_reload.json
new file mode 100644
index 000000000..1a758dd5c
--- /dev/null
+++ b/acceptance/topo_sd_reload/testdata/topology_reload.json
@@ -0,0 +1,121 @@
+{
+  "MTU": 1400,
+  "BorderRouters": {
+    "br1-ff00_0_110-1": {
+      "Interfaces": {
+        "1": {
+          "MTU": 1280,
+          "RemoteOverlay": {
+            "Addr": "127.0.1.5",
+            "OverlayPort": 50000
+          },
+          "Bandwidth": 1000,
+          "ISD_AS": "1-ff00:0:111",
+          "PublicOverlay": {
+            "Addr": "127.0.1.4",
+            "OverlayPort": 50000
+          },
+          "LinkTo": "CHILD",
+          "Overlay": "UDP/IPv4"
+        }
+      },
+      "InternalAddrs": {
+        "IPv4": {
+          "PublicOverlay": {
+            "Addr": "127.0.1.9",
+            "OverlayPort": 31008
+          }
+        }
+      },
+      "CtrlAddr": {
+        "IPv4": {
+          "Public": {
+            "Addr": "127.0.1.9",
+            "L4Port": 31006
+          }
+        }
+      }
+    },
+    "br1-ff00_0_110-2": {
+      "Interfaces": {
+        "2": {
+          "MTU": 1472,
+          "RemoteOverlay": {
+            "Addr": "127.0.1.7",
+            "OverlayPort": 50000
+          },
+          "Bandwidth": 500,
+          "ISD_AS": "1-ff00:0:112",
+          "PublicOverlay": {
+            "Addr": "127.0.1.6",
+            "OverlayPort": 50000
+          },
+          "LinkTo": "CHILD",
+          "Overlay": "UDP/IPv4"
+        }
+      },
+      "InternalAddrs": {
+        "IPv4": {
+          "PublicOverlay": {
+            "Addr": "127.0.1.10",
+            "OverlayPort": 31012
+          }
+        }
+      },
+      "CtrlAddr": {
+        "IPv4": {
+          "Public": {
+            "Addr": "127.0.1.10",
+            "L4Port": 31010
+          }
+        }
+      }
+    }
+  },
+  "ColibriService": {},
+  "DiscoveryService": {},
+  "PathService": {
+    "ps1-ff00_0_110-1": {
+      "Addrs": {
+        "IPv4": {
+          "Public": {
+            "Addr": "127.0.1.13",
+            "L4Port": 31004
+          }
+        }
+      }
+    }
+  },
+  "Attributes": [
+    "authoritative",
+    "core",
+    "issuing",
+    "voting"
+  ],
+  "ISD_AS": "1-ff00:0:110",
+  "CertificateService": {
+    "cs1-ff00_0_110-1": {
+      "Addrs": {
+        "IPv4": {
+          "Public": {
+            "Addr": "127.0.1.12",
+            "L4Port": 31002
+          }
+        }
+      }
+    }
+  },
+  "BeaconService": {
+    "bs1-ff00_0_110-1": {
+      "Addrs": {
+        "IPv4": {
+          "Public": {
+            "Addr": "127.0.1.11",
+            "L4Port": 31000
+          }
+        }
+      }
+    }
+  },
+  "Overlay": "UDP/IPv4"
+}
diff --git a/acceptance/topo_sd_reloads_br_acceptance/test b/acceptance/topo_sd_reloads_br_acceptance/test
deleted file mode 100755
index a2fa9e394..000000000
--- a/acceptance/topo_sd_reloads_br_acceptance/test
+++ /dev/null
@@ -1,63 +0,0 @@
-#!/bin/bash
-
-# Whenever the BR's internal address in the topology file of a SD server is
-# changed, if the SD process receives a SIGHUP it will reload the config and
-# use the new internal address as next-hop in the replies to path requests from
-# clients.
-
-. acceptance/common.sh
-
-TEST_NAME="topo_sd_reloads_br"
-TEST_TOPOLOGY="topology/Tiny.topo"
-
-SRC_IA=${SRC_IA:-1-ff00:0:112}
-SRC_IA_FILE="$(ia_file $SRC_IA)"
-SRC_AS_FILE="$(as_file $SRC_IA)"
-DST_IA=${DST_IA:-1-ff00:0:110}
-
-test_setup() {
-    set -e
-    ./scion.sh topology -c $TEST_TOPOLOGY -d -t
-    sed -i '/\[logging\.file\]/a FlushInterval = 1' gen/ISD1/*/endhost/sd.toml
-    ./scion.sh run nobuild
-    ./tools/dc start tester_$SRC_IA_FILE
-    docker_status
-}
-
-test_run() {
-    set -e
-    local topo_file="gen/ISD1/AS$SRC_AS_FILE/endhost/topology.json"
-    jq '.BorderRouters[].InternalAddrs.IPv4.PublicOverlay = {Addr: "127.42.42.42", OverlayPort: 39999}' $topo_file | sponge $topo_file
-    ./tools/dc scion kill -s HUP scion_sd$SRC_IA_FILE
-    sleep 6
-    ./tools/dc exec_tester $SRC_IA_FILE bin/showpaths -srcIA $SRC_IA -dstIA $DST_IA -sciondFromIA || true
-    sleep 2
-    grep -q "NextHop=\[127\.42\.42\.42\]:39999" "logs/sd$SRC_IA_FILE.log" || \
-        fail "SD Path reply with next-hop 127.42.42.42:39999 not found in logs"
-}
-
-print_help() {
-    echo
-	cat <<-_EOF
-	    $PROGRAM name
-	        return the name of this test
-	    $PROGRAM setup
-	        execute only the setup phase.
-	    $PROGRAM run
-	        execute only the run phase.
-	    $PROGRAM teardown
-	        execute only the teardown phase.
-	_EOF
-}
-
-PROGRAM=`basename "$0"`
-COMMAND="$1"
-
-case "$COMMAND" in
-    name)
-        echo $TEST_NAME ;;
-    setup|run|teardown)
-        "test_$COMMAND" ;;
-    *) print_help; exit 1 ;;
-esac
-
diff --git a/acceptance/topo_sd_reloads_cs_acceptance_disabled/test b/acceptance/topo_sd_reloads_cs_acceptance_disabled/test
deleted file mode 100755
index c4aba732d..000000000
--- a/acceptance/topo_sd_reloads_cs_acceptance_disabled/test
+++ /dev/null
@@ -1,64 +0,0 @@
-#!/bin/bash
-
-# Whenever the CS addresses in the topology file of a SD server are changed, if
-# the SD process receives a SIGHUP it will reload the config and get future
-# certificate chains from the new address.
-
-. acceptance/common.sh
-
-TEST_NAME="topo_sd_reloads_cs"
-TEST_TOPOLOGY="topology/Tiny.topo"
-
-SRC_IA=${SRC_IA:-1-ff00:0:112}
-SRC_IA_FILE="$(ia_file $SRC_IA)"
-SRC_AS_FILE="$(as_file $SRC_IA)"
-DST_IA=${DST_IA:-1-ff00:0:110}
-
-test_setup() {
-    set -e
-    ./scion.sh topology -c $TEST_TOPOLOGY -d -t
-    sed -i '/\[logging\.file\]/a FlushInterval = 1' gen/ISD1/*/endhost/sd.toml
-    ./scion.sh run nobuild
-    ./tools/dc start tester_$SRC_IA_FILE
-    docker_status
-}
-
-test_run() {
-    set -e
-    ./tools/dc exec_tester $SRC_IA_FILE bin/showpaths -srcIA $SRC_IA -dstIA $DST_IA -sciondFromIA || true
-    local topo_file="gen/ISD1/AS$SRC_AS_FILE/endhost/topology.json"
-    jq '.CertificateService[].Addrs.IPv4.Public = {Addr: "127.42.42.42", L4Port: 39999}' $topo_file | sponge $topo_file
-    ./tools/dc scion kill -s HUP scion_sd$SRC_IA_FILE
-    sqlite3 gen-cache/sd$SRC_IA_FILE.trust.db "delete from chains; delete from LeafCerts;"
-    sleep 1
-    ./tools/dc exec_tester $SRC_IA_FILE bin/showpaths -srcIA $SRC_IA -dstIA $DST_IA -sciondFromIA -refresh || true
-    sleep 2
-    grep -q "\[TRACE\] \[Acceptance\] .*overlay=127\.42\.42\.42" "logs/sd$SRC_IA_FILE.log" || \
-        fail "Request to 127.42.42.42 not found in logs"
-}
-
-print_help() {
-    echo
-	cat <<-_EOF
-	    $PROGRAM name
-	        return the name of this test
-	    $PROGRAM setup
-	        execute only the setup phase.
-	    $PROGRAM run
-	        execute only the run phase.
-	    $PROGRAM teardown
-	        execute only the teardown phase.
-	_EOF
-}
-
-PROGRAM=`basename "$0"`
-COMMAND="$1"
-
-case "$COMMAND" in
-    name)
-        echo $TEST_NAME ;;
-    setup|run|teardown)
-        "test_$COMMAND" ;;
-    *) print_help; exit 1 ;;
-esac
-
diff --git a/acceptance/topo_sd_reloads_ps_acceptance/test b/acceptance/topo_sd_reloads_ps_acceptance/test
deleted file mode 100755
index 6ee91afec..000000000
--- a/acceptance/topo_sd_reloads_ps_acceptance/test
+++ /dev/null
@@ -1,63 +0,0 @@
-#!/bin/bash
-
-# Whenever the PS addresses in the topology file of a SD server are changed, if
-# the SD process receives a SIGHUP it will reload the config and service future
-# client requests for paths by querying the new PS addresses.
-
-. acceptance/common.sh
-
-TEST_NAME="topo_reloading"
-TEST_TOPOLOGY="topology/Tiny.topo"
-
-SRC_IA=${SRC_IA:-1-ff00:0:112}
-SRC_IA_FILE="$(ia_file $SRC_IA)"
-SRC_AS_FILE="$(as_file $SRC_IA)"
-DST_IA=${DST_IA:-1-ff00:0:110}
-
-test_setup() {
-    set -e
-    ./scion.sh topology -c $TEST_TOPOLOGY -d -t
-    sed -i '/\[logging\.file\]/a FlushInterval = 1' gen/ISD1/*/endhost/sd.toml
-    ./scion.sh run nobuild
-    ./tools/dc start tester_$SRC_IA_FILE
-    docker_status
-}
-
-test_run() {
-    set -e
-    ./tools/dc exec_tester $SRC_IA_FILE bin/showpaths -srcIA $SRC_IA -dstIA $DST_IA -sciondFromIA || true
-    local topo_file="gen/ISD1/AS$SRC_AS_FILE/endhost/topology.json"
-    jq '.PathService[].Addrs.IPv4.Public = {Addr: "127.42.42.42", L4Port: 39999}' $topo_file | sponge $topo_file
-    ./tools/dc scion kill -s HUP scion_sd$SRC_IA_FILE
-    sleep 1
-    ./tools/dc exec_tester $SRC_IA_FILE bin/showpaths -srcIA $SRC_IA -dstIA $DST_IA -sciondFromIA -refresh || true
-    sleep 2
-    grep -q "\[TRACE\] \[Acceptance\] .*overlay=127\.42\.42\.42" "logs/sd$SRC_IA_FILE.log" || \
-        fail "Request to 127.42.42.42 not found in logs"
-}
-
-print_help() {
-    echo
-	cat <<-_EOF
-	    $PROGRAM name
-	        return the name of this test
-	    $PROGRAM setup
-	        execute only the setup phase.
-	    $PROGRAM run
-	        execute only the run phase.
-	    $PROGRAM teardown
-	        execute only the teardown phase.
-	_EOF
-}
-
-PROGRAM=`basename "$0"`
-COMMAND="$1"
-
-case "$COMMAND" in
-    name)
-        echo $TEST_NAME ;;
-    setup|run|teardown)
-        "test_$COMMAND" ;;
-    *) print_help; exit 1 ;;
-esac
-
diff --git a/go/lib/common/defs_test.go b/go/lib/common/defs_test.go
index 33efd0ca3..749d6191c 100644
--- a/go/lib/common/defs_test.go
+++ b/go/lib/common/defs_test.go
@@ -15,9 +15,11 @@
 package common_test
 
 import (
+	"encoding/json"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 
 	"github.com/scionproto/scion/go/lib/common"
 )
@@ -38,3 +40,30 @@ func TestTypeOf(t *testing.T) {
 		})
 	}
 }
+
+func TestIFIDTypeUnmarshalJSON(t *testing.T) {
+	t.Run("Simple Value", func(t *testing.T) {
+		type exampleStruct struct {
+			IfID common.IFIDType `json:"if_id"`
+		}
+		j := `{"if_id": 5}`
+		var f exampleStruct
+		require.NoError(t, json.Unmarshal([]byte(j), &f))
+		assert.Equal(t, exampleStruct{IfID: 5}, f)
+	})
+	t.Run("Map keys", func(t *testing.T) {
+		type exampleStruct struct {
+			IfMap map[common.IFIDType]string `json:"if_map"`
+		}
+		j := `{"if_map": {"5": "foo"}}`
+		var f exampleStruct
+		require.NoError(t, json.Unmarshal([]byte(j), &f))
+		assert.Equal(t, exampleStruct{IfMap: map[common.IFIDType]string{5: "foo"}}, f)
+	})
+}
+
+func TestIFIDTypeUnmarshalText(t *testing.T) {
+	var id common.IFIDType
+	assert.NoError(t, id.UnmarshalText([]byte("1")))
+	assert.Equal(t, common.IFIDType(1), id)
+}
diff --git a/go/lib/topology/overlay/defs_test.go b/go/lib/topology/overlay/defs_test.go
new file mode 100644
index 000000000..5548c71fb
--- /dev/null
+++ b/go/lib/topology/overlay/defs_test.go
@@ -0,0 +1,34 @@
+// Copyright 2020 Anapaya Systems
+//
+// 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 overlay_test
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/scionproto/scion/go/lib/topology/overlay"
+)
+
+func TestTypeUnmarshalJSON(t *testing.T) {
+	type exampleStruct struct {
+		Type overlay.Type `json:"type"`
+	}
+	var e exampleStruct
+	require.NoError(t, json.Unmarshal([]byte(`{"type": "UDP/IPv4"}`), &e))
+	assert.Equal(t, exampleStruct{Type: overlay.UDPIPv4}, e)
+}