Updates to the affinity router test framework as
well as bug fixes to the affinity router found by
the test framework.

Change-Id: I90e6baa9e9ee11bd8034498b8651e9e14512e528
diff --git a/afrouter/afrouter/affinity-router.go b/afrouter/afrouter/affinity-router.go
index 443a55e..30d1982 100644
--- a/afrouter/afrouter/affinity-router.go
+++ b/afrouter/afrouter/affinity-router.go
@@ -20,6 +20,7 @@
 import (
 	"fmt"
 	"errors"
+	"regexp"
 	"strconv"
 	"io/ioutil"
 	"google.golang.org/grpc"
@@ -28,6 +29,11 @@
 	pb "github.com/golang/protobuf/protoc-gen-go/descriptor"
 )
 
+const (
+	PKG_MTHD_PKG int = 1
+	PKG_MTHD_MTHD int = 2
+)
+
 type AffinityRouter struct {
 	name string
 	routerType int // TODO: This is probably not needed 
@@ -45,6 +51,7 @@
 func newAffinityRouter(rconf *RouterConfig, config *RouteConfig) (Router,error) {
 	var err error = nil
 	var rtrn_err bool = false
+	var pkg_re *regexp.Regexp = regexp.MustCompile(`^(\.[^.]+\.)(.+)$`)
 	// Validate the configuration
 
 	// A name must exist
@@ -150,8 +157,14 @@
 						// Determine if this is a method we're supposed to be processing.
 						if needMethod(*m.Name, config) == true {
 							log.Debugf("Enabling method '%s'",*m.Name)
+							pkg_methd := pkg_re.FindStringSubmatch(*m.InputType)
+							if pkg_methd == nil {
+								log.Errorf("Regular expression didn't match input type '%s'",*m.InputType)
+								rtrn_err = true
+							}
 							// The input type has the package name prepended to it. Remove it.
-							in := (*m.InputType)[len(rconf.ProtoPackage)+2:]
+							//in := (*m.InputType)[len(rconf.ProtoPackage)+2:]
+							in := pkg_methd[PKG_MTHD_MTHD]
 							dr.methodMap[*m.Name], ok = msgs[key{in, config.RouteField}]
 							if ok == false {
 								log.Errorf("Method '%s' has no field named '%s' in it's parameter message '%s'",
@@ -243,7 +256,7 @@
 func (r AffinityRouter) decodeProtoField(payload []byte, fieldId byte) (string, error) {
 	idx :=0
 	b := proto.NewBuffer([]byte{})
-	b.DebugPrint("The Buffer", payload)
+	//b.DebugPrint("The Buffer", payload)
 	for { // Find the route selector field
 		log.Debugf("Decoding afinity value attributeNumber: %d from %v at index %d", fieldId, payload, idx)
 		log.Debugf("Attempting match with payload: %d, methodTable: %d", payload[idx], fieldId)
@@ -305,6 +318,7 @@
 			}
 			// Not a south affinity binding method, proceed with north affinity binding.
 			if selector,err := r.decodeProtoField(sl.payload, r.methodMap[sl.mthdSlice[REQ_METHOD]]); err == nil {
+				log.Debugf("Establishing affinity for selector: %s", selector)
 				if rtrn,ok := r.affinity[selector]; ok {
 					return rtrn
 				} else {
diff --git a/afrouter/afrouter/api.go b/afrouter/afrouter/api.go
index 7f7ba54..5766e8d 100644
--- a/afrouter/afrouter/api.go
+++ b/afrouter/afrouter/api.go
@@ -20,8 +20,9 @@
 import (
 	"net"
 	"fmt"
-	"strconv"
 	"errors"
+	"runtime"
+	"strconv"
 	"google.golang.org/grpc"
 	"golang.org/x/net/context"
 	"github.com/opencord/voltha-go/common/log"
@@ -217,6 +218,10 @@
 	return &pb.Result{Success:true,Error:""},nil
 }
 
+func (aa ArouterApi) GetGoroutineCount(ctx context.Context, in *pb.Empty) (*pb.Count, error) {
+	return &pb.Count{Count:uint32(runtime.NumGoroutine())}, nil
+}
+
 func (aa *ArouterApi) serve() {
 	// Start a serving thread
 	go func() {
diff --git a/afrouter/afrouter/method-router.go b/afrouter/afrouter/method-router.go
index e4824da..379bf11 100644
--- a/afrouter/afrouter/method-router.go
+++ b/afrouter/afrouter/method-router.go
@@ -36,10 +36,12 @@
 func newMethodRouter(config *RouterConfig) (Router, error) {
 	mr := MethodRouter{name:config.Name,service:config.ProtoService,mthdRt:make(map[string]map[string]Router)}
 	mr.mthdRt[NoMeta] = make(map[string]Router) // For routes not needing metadata (all expcept binding at this time)
+	log.Debugf("Processing MethodRouter config %v", *config)
 	if len(config.Routes) == 0 {
 		return nil, errors.New(fmt.Sprintf("Router %s must have at least one route", config.Name))
 	}
 	for _,rtv := range config.Routes {
+		//log.Debugf("Processing route: %v",rtv)
 		var idx1 string
 		r,err := newSubRouter(config, &rtv)
 		if err != nil {
@@ -61,24 +63,24 @@
 				return r, nil
 			} else {
 				log.Debugf("Setting router '%s' for single method '%s'",r.Name(),rtv.Methods[0])
-				if _,ok := mr.mthdRt[""][rtv.Methods[0]]; ok == false {
+				if _,ok := mr.mthdRt[idx1][rtv.Methods[0]]; ok == false {
 					mr.mthdRt[idx1][rtv.Methods[0]] = r
 				} else {
 					err := errors.New(fmt.Sprintf("Attempt to define method %s for 2 routes: %s & %s", rtv.Methods[0],
 						r.Name(), mr.mthdRt[idx1][rtv.Methods[0]].Name()))
-					log.Debug(err)
+					log.Error(err)
 					return mr, err
 				}
 			}
 		default:
 			for _,m := range rtv.Methods {
-				log.Debugf("Setting router '%s' for method '%s'",r.Name(),m)
+				log.Debugf("Processing Method %s", m)
 				if _,ok := mr.mthdRt[idx1][m]; ok == false {
+					log.Debugf("Setting router '%s' for method '%s'",r.Name(),m)
 					mr.mthdRt[idx1][m] = r
 				} else {
-					err := errors.New(fmt.Sprintf("Attempt to define method %s for 2 routes: %s & %s", rtv.Methods[0],
-						r.Name(), mr.mthdRt[idx1][m].Name()))
-					log.Debug(err)
+					err := errors.New(fmt.Sprintf("Attempt to define method %s for 2 routes: %s & %s", m, r.Name(), mr.mthdRt[idx1][m].Name()))
+					log.Error(err)
 					return mr, err
 				}
 			}
diff --git a/docker/Dockerfile.arouterTest b/docker/Dockerfile.arouterTest
index 5fe1dc2..302c7b6 100644
--- a/docker/Dockerfile.arouterTest
+++ b/docker/Dockerfile.arouterTest
@@ -27,6 +27,7 @@
 # Compile protobuf files
 # Repeat here even if it's done in the base
 RUN sh /src/protos/build_protos.sh /src/protos
+#RUN find / -name "*.pb.go" -print
 
 #ADD /src/protos/voltha.pb test/afrouter/suites
 # Build rw_core
@@ -35,7 +36,7 @@
 RUN cp /src/protos/voltha.pb tests/afrouter/suites
 RUN cd tests/afrouter && go build -o /src/afrouterTest
 RUN mkdir /src/tests
-RUN cd tests/afrouter/suites && /src/afrouterTest -config main.json
+RUN cd tests/afrouter/suites && /src/afrouterTest -config main.json -logLevel 1
 RUN mkdir /src/temp
 RUN cp -r tests/afrouter/tests /src/temp
 #RUN cd tests/afrouter/tester && go build -o /src/tests
@@ -55,9 +56,10 @@
 COPY --from=build-env /src/arouter.json /app/
 COPY --from=build-env /src/arouter_test.json /app/
 COPY --from=build-env /src/protos/voltha.pb /app/
-COPY --from=build-env /src/suites /app
+COPY --from=build-env /src/suites/ /app/suites/
 COPY --from=build-env /src/tests /app
 COPY --from=build-env /src/temp/ /app/src/
+COPY --from=build-env /go/src/github.com/opencord/voltha-go/protos/ /app/protos/
 WORKDIR /app
 
 #CMD cd /app && ./arouter -config config/arouter.voltha2.json
diff --git a/protos/afrouter.proto b/protos/afrouter.proto
index 903e5cc..7702841 100644
--- a/protos/afrouter.proto
+++ b/protos/afrouter.proto
@@ -7,13 +7,22 @@
 package afrouter;
 
 service Configuration {
-    rpc SetConnection (Conn) returns (Result) {}
+	rpc SetConnection (Conn) returns (Result) {}
 	rpc SetAffinity(Affinity) returns (Result) {}
+	rpc GetGoroutineCount(Empty) returns (Count) {}
+}
+
+message Empty {
+}
+
+message Count {
+	uint32 count = 1;
 }
 
 message Result {
 	bool success = 1;
 	string error = 2;
+	string info = 3;
 }
 
 message Conn {
diff --git a/tests/afrouter/arouter_test.json b/tests/afrouter/arouter_test.json
index 0829737..d58f26d 100644
--- a/tests/afrouter/arouter_test.json
+++ b/tests/afrouter/arouter_test.json
@@ -30,9 +30,10 @@
 					"backend_cluster":"vcore",
 					"_COMMENT":"Methods are naturally southbound affinity binding unless otherwise specified below",
 					"methods":[ "CreateDevice",
-								"GetCoreInstance",
 								"EnableLogicalDevicePort",
 								"DisableLogicalDevicePort",
+								"UpdateLogicalDeviceFlowTable",
+								"UpdateLogicalDeviceFlowGroupTable",
 								"EnableDevice",
 								"DisableDevice",
 								"RebootDevice",
@@ -59,6 +60,15 @@
 					]
 				},
 				{
+					"name":"control",
+		   			"type":"round_robin",
+		   			"association":"round_robin",
+					"backend_cluster":"vcore",
+					"methods":[ 
+								"UpdateLogLevel"
+					]
+				},
+				{
 					"name":"read_only",
 		   			"type":"round_robin",
 		   			"association":"round_robin",
@@ -79,8 +89,17 @@
 								"GetDeviceGroup",
 								"GetLogicalDevice",
 								"GetAlarmFilter",
+								"ListLogicalDevices",
 								"ListLogicalDevicePorts",
-								"GetLogicalDevicePort"
+								"GetLogicalDevicePort",
+								"GetVoltha",
+								"ListCoreInstances",
+								"ListAdapters",
+								"ListDeviceIds",
+								"ListDeviceTypes",
+								"ListDeviceGroups",
+								"ListAlarmFilters",
+								"GetCoreInstance"
 					]
 				},
 				{
diff --git a/tests/afrouter/suites/main.json b/tests/afrouter/suites/main.json
index a1bc09c..594bc8a 100644
--- a/tests/afrouter/suites/main.json
+++ b/tests/afrouter/suites/main.json
@@ -2,6 +2,7 @@
 "__COMMENT":"Top level test driver file",
 "suites": [
 	"test1",
-	"test2"
-]
+	"test2",
+	"test3"
+	]
 }
diff --git a/tests/afrouter/suites/test1.json b/tests/afrouter/suites/test1.json
index 9e749ac..77cc0ec 100644
--- a/tests/afrouter/suites/test1.json
+++ b/tests/afrouter/suites/test1.json
@@ -1,16 +1,26 @@
 {
 	"environment":{
-		"cmdLine":"afrouter -config arouter_test.json",
+		"cmdLine":"afrouter -logLevel 1 -config arouter_test.json",
 		"protoFiles": [
 			{
 				"importPath":"github.com/opencord/voltha-go/protos/",
 				"service":"VolthaService",
 				"package":"voltha"
+			},
+			{
+				"importPath":"github.com/opencord/voltha-go/protos/",
+				"service":"Configuration",
+				"package":"afrouter"
+			}
+		],
+		"Junk": [
+			{
+				"importPath":"github.com/opencord/voltha-go/protos/",
+				"service":"Configuration",
+				"package":"afrouter"
 			}
 		],
 		"imports": [
-			 "github.com/golang/protobuf/ptypes/empty",
-			 "github.com/opencord/voltha-go/protos/openflow_13"
 		],
 		"protoDesc":"voltha.pb",
 		"protoSubst": [
@@ -19,13 +29,28 @@
 				"to":"empty.Empty"
 			}
 		],
-		"clients": [
-			{
-				"name":"client",
-				"port":"5000"
-			}
-		],
-		"servers": [
+		"clients": {
+			"imports": [
+				 "github.com/golang/protobuf/ptypes/empty",
+				 "github.com/opencord/voltha-go/protos/openflow_13"
+			],
+			"endpoints": [
+				{
+					"name":"client",
+					"port":"5000"
+				},
+				{
+					"name":"stats",
+					"port":"55554"
+				}
+			]
+		},
+		"servers": {
+			"imports": [
+				 "github.com/golang/protobuf/ptypes/empty",
+				 "github.com/opencord/voltha-go/protos/openflow_13"
+			],
+			"endpoints": [
 			{
 				"name":"core11",
 				"port":"5011"
@@ -49,8 +74,21 @@
 			{
 				"name":"core32",
 				"port":"5032"
+			},
+			{
+				"name":"roCore1",
+				"port":"5001"
+			},
+			{
+				"name":"roCore2",
+				"port":"5002"
+			},
+			{
+				"name":"roCore3",
+				"port":"5003"
 			}
-		]
+			]
+		}
 	},
 	"tests":[
 		{
@@ -150,6 +188,19 @@
 					]
 				}
 			]
+		},
+		{
+			"name":"Get goroutine count",
+			"infoOnly":true,
+			"send": {
+				"client":"stats",
+				"method":"GetGoroutineCount",
+				"param":"{}",
+				"meta": [ ],
+				"expect":"{Count:39}",
+				"expectMeta": [ ]
+			},
+			"servers": [ ]
 		}
 	]
 }
diff --git a/tests/afrouter/suites/test2.go b/tests/afrouter/suites/test2.go
index fc0332e..80e3af3 100644
--- a/tests/afrouter/suites/test2.go
+++ b/tests/afrouter/suites/test2.go
@@ -33,28 +33,37 @@
 	//pb "github.com/golang/protobuf/protoc-gen-go/descriptor"
 )
 
+type suite struct {
+	CrTests []test
+	GetTests[]test
+}
+
 type test struct {
 	Core int
+	SerNo int
 }
 
+const SUITE_LEN = 55000
+//const SUITE_LEN = 100
+
 func main() {
 
-	var ary []test
+	var ary suite
 
 	// Setup logging
 	if _, err := log.SetDefaultLogger(log.JSON, 0, nil); err != nil {
 		log.With(log.Fields{"error": err}).Fatal("Cannot setup logging")
 	}
 
-	for i :=0; i<10000; i++ {
-		
-		ary = append(ary,test{Core:(i%3)+1})
+	for i :=0; i<SUITE_LEN; i++ {
+
+		ary.CrTests = append(ary.CrTests,test{Core:(i%3)+1, SerNo:i})
+		ary.GetTests = append(ary.GetTests,test{Core:(i%3)+1, SerNo:i+SUITE_LEN})
 	}
 
 	// Load the template to execute
 	t := template.Must(template.New("").ParseFiles("./test2.tmpl.json"))
 	if f,err := os.Create("test2.json"); err == nil {
-		_=f
 		defer f.Close()
 		if err := t.ExecuteTemplate(f, "test2.tmpl.json", ary); err != nil {
 			log.Errorf("Unable to execute template for test2.tmpl.json: %v", err)
diff --git a/tests/afrouter/suites/test2.tmpl.json b/tests/afrouter/suites/test2.tmpl.json
index fc8ef1c..c8d040e 100644
--- a/tests/afrouter/suites/test2.tmpl.json
+++ b/tests/afrouter/suites/test2.tmpl.json
@@ -1,16 +1,20 @@
 {
 	"environment":{
-		"cmdLine":"afrouter -config arouter_test.json -grpclog",
+		"cmdLine":"afrouter -config arouter_test.json -logLevel 1",
 		"protoFiles": [
 			{
 				"importPath":"github.com/opencord/voltha-go/protos/",
 				"service":"VolthaService",
 				"package":"voltha"
+			},
+			{
+				"importPath":"github.com/opencord/voltha-go/protos/",
+				"service":"Configuration",
+				"package":"afrouter"
 			}
 		],
 		"imports": [
-			 "github.com/golang/protobuf/ptypes/empty",
-			 "github.com/opencord/voltha-go/protos/openflow_13"
+			 "github.com/golang/protobuf/ptypes/empty"
 		],
 		"protoDesc":"voltha.pb",
 		"protoSubst": [
@@ -19,13 +23,28 @@
 				"to":"empty.Empty"
 			}
 		],
-		"clients": [
-			{
-				"name":"client",
-				"port":"5000"
-			}
-		],
-		"servers": [
+		"clients": {
+			"imports": [
+				 "github.com/golang/protobuf/ptypes/empty",
+				 "github.com/opencord/voltha-go/protos/openflow_13"
+			],
+			"endpoints": [
+				{
+					"name":"client",
+					"port":"5000"
+				},
+				{
+					"name":"stats",
+					"port":"55554"
+				}
+			]
+		},
+		"servers": {
+			"imports": [
+				 "github.com/golang/protobuf/ptypes/empty",
+				 "github.com/opencord/voltha-go/protos/openflow_13"
+			],
+			"endpoints": [
 			{
 				"name":"core11",
 				"port":"5011"
@@ -50,10 +69,11 @@
 				"name":"core32",
 				"port":"5032"
 			}
-		]
+			]
+		}
 	},
 	"tests":[
-		{{range $k,$v := .}}
+		{{range $k,$v := .CrTests}}
 		{
 			"name":"Test CreateDevice",
 			"send": {
@@ -69,7 +89,7 @@
 					"meta": [
 						{
 							"key":"voltha_serial_number",
-							"value":"{{$k}}"
+							"value":"{{$v.SerNo}}"
 						}
 					]
 				},
@@ -78,7 +98,41 @@
 					"meta": [
 						{
 							"key":"voltha_serial_number",
-							"value":"{{$k}}"
+							"value":"{{$v.SerNo}}"
+						}
+					]
+				}
+			]
+		},
+		{{end}}
+		{{range $k,$v := .GetTests}}
+		{
+			"name":"Test EnableDevice",
+			"send": {
+				"client":"client",
+				"_method":"GetDevice",
+				"method":"EnableDevice",
+				"param":"{Id:\"abcd1234{{$k}}\"}",
+				"expect":"{}",
+				"_expect":"{Id:\"abcd1234{{$k}}\",Type:\"simulated_olt\"}",
+				"_meta":""
+			},
+			"servers": [
+				{
+					"name":"core{{$v.Core}}1",
+					"meta": [
+						{
+							"key":"voltha_serial_number",
+							"value":"{{$v.SerNo}}"
+						}
+					]
+				},
+				{
+					"name":"core{{$v.Core}}2",
+					"meta": [
+						{
+							"key":"voltha_serial_number",
+							"value":"{{$v.SerNo}}"
 						}
 					]
 				}
@@ -100,7 +154,7 @@
 					"meta": [
 						{
 							"key":"voltha_serial_number",
-							"value":"10000"
+							"value":"110000"
 						}
 					]
 				},
@@ -109,7 +163,7 @@
 					"meta": [
 						{
 							"key":"voltha_serial_number",
-							"value":"10000"
+							"value":"110000"
 						}
 					]
 				}
@@ -141,7 +195,7 @@
 					"meta": [
 						{
 							"key":"voltha_serial_number",
-							"value":"10001"
+							"value":"110001"
 						},
 						{
 							"key":"voltha_backend_name",
@@ -150,6 +204,19 @@
 					]
 				}
 			]
+		},
+		{
+			"_COMMENT":"If this test case fails, there could be a goroutine leak",
+			"name":"Get goroutine count",
+			"send": {
+				"client":"stats",
+				"method":"GetGoroutineCount",
+				"param":"{}",
+				"meta": [ ],
+				"expect":"{Count:39}",
+				"expectMeta": [ ]
+			},
+			"servers": [ ]
 		}
 	]
 }
diff --git a/tests/afrouter/suites/test3.go b/tests/afrouter/suites/test3.go
new file mode 100644
index 0000000..88b4382
--- /dev/null
+++ b/tests/afrouter/suites/test3.go
@@ -0,0 +1,418 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+
+ * 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 main
+
+import (
+	"os"
+	"fmt"
+	//"flag"
+	//"path"
+	//"bufio"
+	"errors"
+	//"os/exec"
+	//"strconv"
+	"io/ioutil"
+	//"encoding/json"
+	"text/template"
+	//"github.com/golang/protobuf/proto"
+	"github.com/opencord/voltha-go/common/log"
+	//pb "github.com/golang/protobuf/protoc-gen-go/descriptor"
+)
+
+// This test suite validates that the different method types get routed
+// properly. That is, rw methods to the rw cores and ro methods to the
+// ro cores
+type test struct {
+	Name string
+	Core int
+	SerNo int
+	Method string
+	Param string
+	Expect string
+}
+
+type tests struct {
+	RoTests []test
+	RwTests []test
+	CtlTests []test
+}
+
+var roMethods []test = []test{
+    {
+		Name: "Test GetVoltha",
+		Method: "GetVoltha", // rpc GetVoltha(google.protobuf.Empty) returns(Voltha)
+		Param: "{}",
+		Expect:`{Version:\"abcdef\"}`,
+	},
+	/*
+    {
+		Name: "Test ListCoreInstances",
+		Method: "ListCoreInstances", // rpc ListCoreInstances(google.protobuf.Empty) returns(CoreInstances)
+		Param: "{}",
+		Expect:  `{Items:[]*voltha.CoreInstance{&voltha.CoreInstance{InstanceId:\"ABC\",Health:&voltha.HealthStatus{State:0}}}}`,
+	},
+	{
+		Name: "Test GetCoreInstance",
+		Method: "GetCoreInstance", // rpc GetCoreInstance(ID) returns(CoreInstance)
+		Param: `{Id:\"arou8390\"}`,
+		Expect: `{InstanceId:\"arou8390\", Health:&voltha.HealthStatus{State:0}}`,
+	},*/
+	{
+		Name: "Test ListAdapters",
+		Method: "ListAdapters", // rpc ListAdapters(google.protobuf.Empty) returns(Adapters)
+		Param: "{}",
+		Expect: `{Items:[]*voltha.Adapter{&voltha.Adapter{Id:\"ABC\", Vendor:\"afrouterTest\", Version:\"Version 1.0\"}}}`,
+	},
+	{
+		Name: "Test ListLogicalDevices",
+		Method: "ListLogicalDevices", // rpc ListLogicalDevices(google.protobuf.Empty) returns(LogicalDevices)
+		Param: "{}",
+		Expect:`{Items:[]*voltha.LogicalDevice{&voltha.LogicalDevice{Id:\"LDevId\", DatapathId:64, RootDeviceId:\"Root\"}}}`,
+	},
+	{
+		Name: "Test GetLogicalDevice",
+		Method: "GetLogicalDevice", // rpc GetLogicalDevice(ID) returns(LogicalDevice)
+		Param: `{Id:\"ABC123XYZ\"}`,
+		Expect:`{Id:\"ABC123XYZ\", DatapathId:64, RootDeviceId:\"Root-of:ABC123XYZ\"}`,
+	},
+	{
+		Name: "Test ListLogicalDevicePorts",
+		Method: "ListLogicalDevicePorts", // rpc ListLogicalDevicePorts(ID) returns(LogicalPorts)
+		Param: `{Id:\"ABC123XYZ\"}`,
+		Expect:`{Items:[]*voltha.LogicalPort{&voltha.LogicalPort{Id:\"PortId-1\", DeviceId:\"ABC12XYZ\", DevicePortNo:64, RootPort:true},&voltha.LogicalPort{Id:\"PortId-2\", DeviceId:\"ABC12XYZ\", DevicePortNo:64, RootPort:false}}}`,
+	},
+	{
+		Name: "Test GetLogicalDevicePort",
+		Method: "GetLogicalDevicePort", // rpc GetLogicalDevicePort(LogicalPortId) returns(LogicalPort)
+		Param: `{Id:\"PortId-1\"}`,
+		Expect:`{Id:\"PortId-1\", DeviceId:\"ABC12XYZ\", DevicePortNo:64, RootPort:true}`,
+	},
+	{
+		Name: "Test ListLogicalDeviceFlows",
+		Method: "ListLogicalDeviceFlows", // rpc ListLogicalDeviceFlows(ID) returns(openflow_13.Flows)
+		Param: `{Id:\"Flow-ABC123XYZ\"}`,
+		Expect:`{Items:[]*openflow_13.OfpFlowStats{&openflow_13.OfpFlowStats{Id:1, TableId:2, DurationSec:12, Priority:0},&openflow_13.OfpFlowStats{Id:1, TableId:2, DurationSec:12, Priority:0}}}`,
+	},
+	{
+		Name: "Test ListLogicalDeviceFlowGroups",
+		Method: "ListLogicalDeviceFlowGroups", // rpc ListLogicalDeviceFlowGroups(ID) returns(openflow_13.FlowGroups)
+		Param: `{Id:\"Flow-ABC123XYZ\"}`,
+		Expect:`{Items:[]*openflow_13.OfpGroupEntry{&openflow_13.OfpGroupEntry{Desc:&openflow_13.OfpGroupDesc{GroupId:12}, Stats:&openflow_13.OfpGroupStats{GroupId:1, RefCount:1, PacketCount:3}}}}`,
+	},
+	{
+		Name: "Test ListDevices",
+		Method: "ListDevices", // rpc ListDevices(google.protobuf.Empty) returns(Devices)
+		Param: `{}`,
+		Expect:`{Items:[]*voltha.Device{&voltha.Device{Id:\"ABC123XYZ\", Type:\"SomeDeviceType\", Root:false, ParentId:\"ZYX321CBA\"},&voltha.Device{Id:\"ZYX321CBA\", Type:\"SomeDeviceType\", Root:true, ParentId:\"ROOT\"}}}`,
+	},
+	{
+		Name: "Test ListDeviceIds",
+		Method: "ListDeviceIds", // rpc ListDeviceIds(google.protobuf.Empty) returns(IDs)
+		Param: `{}`,
+		Expect:`{Items:[]*voltha.ID{&voltha.ID{Id:\"ABC123XYZ\"},&voltha.ID{Id:\"ZYX321CBA\"}}}`,
+	},
+	{
+		Name: "Test GetDevice",
+		Method: "GetDevice", // rpc GetDevice(ID) returns(Device)
+		Param: `{Id:\"ZYX321CBA\"}`,
+		Expect:`{Id:\"ABC123XYZ\", Type:\"SomeDeviceType\", Root:false, ParentId:\"ZYX321CBA\"}`,
+	},
+	{
+		Name: "Test GetImageDownloadStatus",
+		Method: "GetImageDownloadStatus", // rpc GetImageDownloadStatus(ImageDownload) returns(ImageDownload)
+		Param: `{Id:\"Image-ZYX321CBA\"}`,
+		Expect:`{Id:\"Image-ABC123XYZ\", Name:\"SomeName\", Url:\"Some URL\", Crc:123456}`,
+	},
+	{
+		Name: "Test GetImageDownload",
+		Method: "GetImageDownload", // rpc GetImageDownload(ImageDownload) returns(ImageDownload)
+		Param: `{Id:\"Image-ZYX321CBA\"}`,
+		Expect:`{Id:\"Image-ABC123XYZ\", Name:\"SomeName\", Url:\"Some URL\", Crc:123456}`,
+	},
+	{
+		Name: "Test ListImageDownloads",
+		Method: "ListImageDownloads", // rpc ListImageDownloads(ID) returns(ImageDownloads)
+		Param: `{Id:\"ZYX321CBA\"}`,
+		Expect:`{Items:[]*voltha.ImageDownload{&voltha.ImageDownload{Id:\"Image1-ABC123XYZ\", Name:\"SomeName\", Url:\"Some URL\", Crc:123456}, &voltha.ImageDownload{Id:\"Image2-ABC123XYZ\", Name:\"SomeName\", Url:\"Some other URL\", Crc:654321}}}`,
+	},
+	{
+		Name: "Test ListDevicePorts",
+		Method: "ListDevicePorts", // rpc ListDevicePorts(ID) returns(Ports)
+		Param: `{}`,
+		Expect:`{Items:[]*voltha.Port{&voltha.Port{PortNo:100000, Label:\"Port one hundred thousand\", DeviceId:\"ZYX321CBA\"},&voltha.Port{PortNo:100001, Label:\"Port one hundred thousand and one\", DeviceId:\"ZYX321CBA\"}}}`,
+	},
+	{
+		Name: "Test ListDevicePmConfigs",
+		Method: "ListDevicePmConfigs", // rpc ListDevicePmConfigs(ID) returns(PmConfigs)
+		Param: `{}`,
+		Expect:`{Id:\"ABC123XYZ\", DefaultFreq: 10000, Grouped:false, FreqOverride:false, Metrics:[]*voltha.PmConfig{&voltha.PmConfig{Name:\"Speed\", Enabled: true, SampleFreq:10000}, &voltha.PmConfig{Name:\"Errors\", Enabled: true, SampleFreq:10000}}}`,
+	},
+	{
+		Name: "Test ListDeviceFlows",
+		Method: "ListDeviceFlows", // rpc ListDeviceFlows(ID) returns(openflow_13.Flows)
+		Param: `{Id:\"Flow-ABC123XYZ\"}`,
+		Expect:`{Items:[]*openflow_13.OfpFlowStats{&openflow_13.OfpFlowStats{Id:1, TableId:2, DurationSec:12, Priority:0},&openflow_13.OfpFlowStats{Id:1, TableId:2, DurationSec:12, Priority:0}}}`,
+	},
+	{
+		Name: "Test ListDeviceFlowGroups",
+		Method: "ListDeviceFlowGroups", // rpc ListDeviceFlowGroups(ID) returns(openflow_13.FlowGroups)
+		Param: `{Id:\"Flow-ABC123XYZ\"}`,
+		Expect:`{Items:[]*openflow_13.OfpGroupEntry{&openflow_13.OfpGroupEntry{Desc:&openflow_13.OfpGroupDesc{GroupId:12}, Stats:&openflow_13.OfpGroupStats{GroupId:1, RefCount:1, PacketCount:3}}}}`,
+	},
+	{
+		Name: "Test ListDeviceTypes",
+		Method: "ListDeviceTypes", // rpc ListDeviceTypes(google.protobuf.Empty) returns(DeviceTypes)
+		Param: `{}`,
+		Expect:`{Items:[]*voltha.DeviceType{&voltha.DeviceType{Id:\"ABC123XYZ\", VendorId:\"Ciena\", Adapter:\"SAOS\"},&voltha.DeviceType{Id:\"ZYX321CBA\", VendorId:\"Ciena\", Adapter:\"SAOS-Tibit\"}}}`,
+	},
+	{
+		Name: "Test GetDeviceType",
+		Method: "GetDeviceType", // rpc GetDeviceType(ID) returns(DeviceType)
+		Param: `{Id:\"ZYX321CBA\"}`,
+		Expect:`{Id:\"ZYX321CBA\", VendorId:\"Ciena\", Adapter:\"SAOS-Tibit\"}`,
+	},
+	{
+		Name: "Test ListDeviceGroups",
+		Method: "ListDeviceGroups", // rpc ListDeviceGroups(google.protobuf.Empty) returns(DeviceGroups)
+		Param: `{}`,
+		Expect:`{Items:[]*voltha.DeviceGroup{&voltha.DeviceGroup{Id:\"group-ABC123XYZ\", LogicalDevices: []*voltha.LogicalDevice{&voltha.LogicalDevice{Id:\"LDevId\", DatapathId:64, RootDeviceId:\"Root\"}}, Devices: []*voltha.Device{&voltha.Device{Id:\"ABC123XYZ\", Type:\"SomeDeviceType\", Root:false, ParentId:\"ZYX321CBA\"},&voltha.Device{Id:\"ZYX321CBA\", Type:\"SomeDeviceType\", Root:true, ParentId:\"ROOT\"}}}}}`,
+	},
+	{
+		Name: "Test GetDeviceGroup",
+		Method: "GetDeviceGroup", // rpc GetDeviceGroup(ID) returns(DeviceGroup)
+		Param: `{Id:\"ZYX321CBA\"}`,
+		Expect:`{Id:\"group-ABC123XYZ\", LogicalDevices: []*voltha.LogicalDevice{&voltha.LogicalDevice{Id:\"LDevId\", DatapathId:64, RootDeviceId:\"Root\"}}, Devices: []*voltha.Device{&voltha.Device{Id:\"ABC123XYZ\", Type:\"SomeDeviceType\", Root:false, ParentId:\"ZYX321CBA\"},&voltha.Device{Id:\"ZYX321CBA\", Type:\"SomeDeviceType\", Root:true, ParentId:\"ROOT\"}}}`,
+	},
+	{
+		Name: "Test ListAlarmFilters",
+		Method: "ListAlarmFilters", // rpc ListAlarmFilters(google.protobuf.Empty) returns(AlarmFilters)
+		Param: `{}`,
+		Expect:`{Filters:[]*voltha.AlarmFilter{&voltha.AlarmFilter{Id:\"ABC123XYZ\", Rules: []*voltha.AlarmFilterRule{&voltha.AlarmFilterRule{Value:\"Rule Value\"}}}}}`,
+	},
+	{
+		Name: "Test GetAlarmFilter",
+		Method: "GetAlarmFilter", // rpc GetAlarmFilter(ID) returns(AlarmFilter)
+		Param: `{Id:\"ABC123XYZ\"}`,
+		Expect:`{Id:\"ABC123XYZ\", Rules: []*voltha.AlarmFilterRule{&voltha.AlarmFilterRule{Value:\"Rule Value\"}}}`,
+	},
+	{
+		Name: "Test GetImages",
+		Method: "GetImages", // rpc GetImages(ID) returns(Images)
+		Param: `{Id:\"ZYX321CBA\"}`,
+		Expect:`{Image: []*voltha.Image{&voltha.Image{Name:\"Image1\", Version: \"0.1\", Hash:\"123@\", InstallDatetime:\"Yesterday\", IsActive:true, IsCommitted:true, IsValid:false},&voltha.Image{Name:\"Image2\", Version: \"0.2\", Hash:\"ABC@\", InstallDatetime:\"Today\", IsActive:false, IsCommitted:false, IsValid:false}}}`,
+	},
+}
+
+var ctlMethods []test = []test {
+	{
+		Name: "Test UpdateLogLevel",
+		Method: "UpdateLogLevel", // rpc UpdateLogLevel(Logging) returns(google.protobuf.Empty)
+		Param: `{Level: 0, PackageName:\"abc123\"}`,
+		Expect: "{}",
+	},
+}
+
+var rwMethods []test = []test{
+	{
+		Name: "Test EnableLogicalDevicePort",
+		Method: "EnableLogicalDevicePort", // rpc EnableLogicalDevicePort(LogicalPortId) returns(google.protobuf.Empty)
+		Param: `{Id:\"ZYX321CBA\", PortId:\"100,000\"}`,
+		Expect:`{}`,
+	},
+	{
+		Name: "Test DisableLogicalDevicePort",
+		Method: "DisableLogicalDevicePort", // rpc DisableLogicalDevicePort(LogicalPortId) returns(google.protobuf.Empty)
+		Param: `{Id:\"ABC123XYZ\", PortId:\"100,000\"}`,
+		Expect:`{}`,
+	},
+	{
+		Name: "Test UpdateLogicalDeviceFlowTable",
+		Method: "UpdateLogicalDeviceFlowTable", // rpc UpdateLogicalDeviceFlowTable(openflow_13.FlowTableUpdate)
+		Param: `{Id:\"XYZ123ABC\", FlowMod:&openflow_13.OfpFlowMod{Cookie:10, CookieMask:255, TableId:10000, Command:openflow_13.OfpFlowModCommand_OFPFC_ADD}}`,
+		Expect:`{}`,
+	},
+	{
+		Name: "Test UpdateLogicalDeviceFlowGroupTable",
+		Method: "UpdateLogicalDeviceFlowGroupTable", // rpc UpdateLogicalDeviceFlowGroupTable(openflow_13.FlowGroupTableUpdate)
+		Param: `{Id:\"ZYX123ABC\", GroupMod:&openflow_13.OfpGroupMod{Command:openflow_13.OfpGroupModCommand_OFPGC_ADD, Type:openflow_13.OfpGroupType_OFPGT_INDIRECT, GroupId:255}}`,
+		Expect:`{}`,
+	},
+	//"ReconcileDevices", // rpc ReconcileDevices(IDs) returns(google.protobuf.Empty)
+	{
+		Name: "Test CreateDevice",
+		Method: "CreateDevice", // rpc CreateDevice(Device) returns(Device)
+		Param: `{Type:\"simulated_OLT\"}`,
+		Expect:`{Id:\"ZYX321ABC\", Type:\"\"}`,
+	},
+	{
+		Name: "Test EnableDevice",
+		Method: "EnableDevice", // rpc EnableDevice(ID) returns(google.protobuf.Empty)
+		Param: `{Id:\"XYZ321ABC\"}`,
+		Expect:`{}`,
+	},
+	{
+		Name: "Test DisableDevice",
+		Method: "DisableDevice", // rpc DisableDevice(ID) returns(google.protobuf.Empty)
+		Param: `{Id:\"XYZ123CBA\"}`,
+		Expect:`{}`,
+	},
+	{
+		Name: "Test RebootDevice",
+		Method: "RebootDevice", // rpc RebootDevice(ID) returns(google.protobuf.Empty)
+		Param: `{Id:\"ZYX123CBA\"}`,
+		Expect:`{}`,
+	},
+	{
+		Name: "Test DeleteDevice",
+		Method: "DeleteDevice", // rpc DeleteDevice(ID) returns(google.protobuf.Empty)
+		Param: `{Id:\"CBA123XYZ\"}`,
+		Expect:`{}`,
+	},
+	{
+		Name: "Test DownloadImage",
+		Method: "DownloadImage", // rpc DownloadImage(ImageDownload) returns(OperationResp)
+		Param: `{Id:\"CBA321XYZ\", Name:\"Image-1\", Crc: 54321}`,
+		Expect:`{Code:voltha.OperationResp_OPERATION_SUCCESS, AdditionalInfo:\"It worked!\"}`,
+	},
+	{
+		Name: "Test CancelImageDownload",
+		Method: "CancelImageDownload", // rpc CancelImageDownload(ImageDownload) returns(OperationResp)
+		Param: `{Id:\"CBA321ZYX\", Name:\"Image-1\", Crc: 54321}`,
+		Expect:`{Code:voltha.OperationResp_OPERATION_SUCCESS, AdditionalInfo:\"It worked!\"}`,
+	},
+	{
+		Name: "Test ActivateImageUpdate",
+		Method: "ActivateImageUpdate", // rpc ActivateImageUpdate(ImageDownload) returns(OperationResp)
+		Param: `{Id:\"ABC321ZYX\", Name:\"Image-1\", Crc: 54321}`,
+		Expect:`{Code:voltha.OperationResp_OPERATION_FAILURE, AdditionalInfo:\"It bombed!\"}`,
+	},
+	{
+		Name: "Test RevertImageUpdate",
+		Method: "RevertImageUpdate", // rpc RevertImageUpdate(ImageDownload) returns(OperationResp)
+		Param: `{Id:\"ABC123ZYX\", Name:\"Image-1\", Crc: 54321}`,
+		Expect:`{Code:voltha.OperationResp_OPERATION_FAILURE, AdditionalInfo:\"It bombed!\"}`,
+	},
+	{
+		Name: "Test UpdateDevicePmConfigs",
+		Method: "UpdateDevicePmConfigs", // rpc UpdateDevicePmConfigs(voltha.PmConfigs) returns(google.protobuf.Empty)
+		Param: `{Id:\"CBA123ZYX\", DefaultFreq:1000000, Grouped: false, FreqOverride: false}`,
+		Expect:`{}`,
+	},
+	{
+		Name: "Test CreateAlarmFilter",
+		Method: "CreateAlarmFilter", // rpc CreateAlarmFilter(AlarmFilter) returns(AlarmFilter)
+		Param: `{Id:\"abc123xyz\", Rules:[]*voltha.AlarmFilterRule{&voltha.AlarmFilterRule{Key:voltha.AlarmFilterRuleKey_type, Value:\"Type man, it's the type!\"}, &voltha.AlarmFilterRule{Key:voltha.AlarmFilterRuleKey_category, Value:\"Category yeah!\"}}}`,
+		Expect:`{Id:\"abc123xyz\", Rules: []*voltha.AlarmFilterRule{&voltha.AlarmFilterRule{Key:voltha.AlarmFilterRuleKey_type, Value:\"Type man, it's the type!\"}, &voltha.AlarmFilterRule{Key:voltha.AlarmFilterRuleKey_category, Value:\"Category yeah!\"}}}`,
+	},
+	{
+		Name: "Test UpdateAlarmFilter",
+		Method: "UpdateAlarmFilter", // rpc UpdateAlarmFilter(AlarmFilter) returns(AlarmFilter)
+		Param: `{Id:\"ABC321XYZ\", Rules:[]*voltha.AlarmFilterRule{&voltha.AlarmFilterRule{Key:voltha.AlarmFilterRuleKey_type, Value:\"Type man, it's the type!\"}, &voltha.AlarmFilterRule{Key:voltha.AlarmFilterRuleKey_category, Value:\"Category yeah!\"}}}`,
+		Expect:`{Id:\"ABC321XYZ\", Rules: []*voltha.AlarmFilterRule{&voltha.AlarmFilterRule{Key:voltha.AlarmFilterRuleKey_type, Value:\"Type man, it's the type!\"}, &voltha.AlarmFilterRule{Key:voltha.AlarmFilterRuleKey_category, Value:\"Category yeah!\"}}}`,
+	},
+	{
+		Name: "Test DeleteAlarmFilter",
+		Method: "DeleteAlarmFilter", // rpc DeleteAlarmFilter(ID) returns(google.protobuf.Empty)
+		Param: `{Id:\"acb123xyz\"}`,
+		Expect:`{}`,
+	},
+	{
+		Name: "Test SelfTest",
+		Method: "SelfTest", // rpc SelfTest(ID) returns(SelfTestResponse)
+		Param: `{Id:\"bac123xyz\"}`,
+		Expect:`{Result:voltha.SelfTestResponse_UNKNOWN_ERROR}`,
+	},
+} /*
+//    "StreamPacketsOut", // rpc StreamPacketsOut(stream openflow_13.PacketOut)
+//    "ReceivePacketsIn", // rpc ReceivePacketsIn(google.protobuf.Empty)
+//    "ReceiveChangeEvents", // rpc ReceiveChangeEvents(google.protobuf.Empty)
+    "Subscribe ", // rpc Subscribe (OfAgentSubscriber) returns (OfAgentSubscriber)
+
+}
+*/
+
+func dumpFile(fileNm string) error {
+	// Dump the generated file for debugging purposes
+	if c,err := ioutil.ReadFile(fileNm); err == nil {
+		fmt.Print(string(c))
+	} else {
+		e := errors.New(fmt.Sprintf("Could not read the file '%s', %v", fileNm, err))
+		return e
+	}
+	return nil
+}
+
+func main() {
+
+	//var rwAry []test
+	//var roAry []test
+	var serNo int = 0
+
+	// Setup logging
+	if _, err := log.SetDefaultLogger(log.JSON, 0, nil); err != nil {
+		log.With(log.Fields{"error": err}).Fatal("Cannot setup logging")
+	}
+
+	core := 0
+	for  k,_ := range roMethods {
+		roMethods[k].SerNo = serNo
+		roMethods[k].Core = (core%3) + 1
+		log.Infof("Processing method %s, serNo: %d", roMethods[k].Method, roMethods[k].SerNo)
+		serNo++
+		core++
+	}
+
+	// New round robin starts here because of the different route
+	core = 0
+	for  k,_ := range rwMethods {
+		rwMethods[k].SerNo = serNo
+		rwMethods[k].Core = (core%3) + 1
+		log.Infof("Processing method %s, serNo: %d", rwMethods[k].Method, rwMethods[k].SerNo)
+		serNo++
+		core++
+	}
+
+	// New round robin starts here because of the different route
+	core = 0
+	for  k,_ := range ctlMethods {
+		ctlMethods[k].SerNo = serNo
+		ctlMethods[k].Core = (core%3) + 1
+		log.Infof("Processing method %s, serNo: %d", ctlMethods[k].Method, ctlMethods[k].SerNo)
+		serNo++
+		core++
+	}
+
+	tsts := tests{RoTests: roMethods, RwTests: rwMethods, CtlTests: ctlMethods}
+
+	t := template.Must(template.New("").ParseFiles("./test3.tmpl.json"))
+	if f,err := os.Create("test3.json"); err == nil {
+		_=f
+		defer f.Close()
+		if err := t.ExecuteTemplate(f, "test3.tmpl.json", tsts); err != nil {
+			log.Errorf("Unable to execute template for test3.tmpl.json: %v", err)
+		}
+	} else {
+		log.Errorf("Couldn't create file test3.json: %v", err)
+	}
+
+	// Dump the generated file for debugging purposes
+	//if err := dumpFile("test3.json"); err != nil {
+	//	log.Error(err)
+	//}
+
+
+}
diff --git a/tests/afrouter/suites/test3.json b/tests/afrouter/suites/test3.json
deleted file mode 100644
index 2d3d7ad..0000000
--- a/tests/afrouter/suites/test3.json
+++ /dev/null
@@ -1,87 +0,0 @@
-{
-	"environment":{
-		"cmdLine":"afrouter -config arouter_test.json",
-		"protoFiles": [
-			{
-				"importPath":"github.com/opencord/voltha-go/protos/",
-				"service":"VolthaService",
-				"package":"voltha"
-			}
-		],
-		"imports": [
-			 "github.com/golang/protobuf/ptypes/empty",
-			 "github.com/opencord/voltha-go/protos/openflow_13"
-		],
-		"protoDesc":"voltha.pb",
-		"protoSubst": [
-			{
-				"from":"google.protobuf.Empty",
-				"to":"empty.Empty"
-			}
-		],
-		"clients": [
-			{
-				"name":"client",
-				"port":"5000"
-			}
-		],
-		"servers": [
-			{
-				"name":"core11",
-				"port":"5011"
-			},
-			{
-				"name":"core12",
-				"port":"5012"
-			},
-			{
-				"name":"core21",
-				"port":"5021"
-			},
-			{
-				"name":"core22",
-				"port":"5022"
-			},
-			{
-				"name":"core31",
-				"port":"5031"
-			},
-			{
-				"name":"core32",
-				"port":"5032"
-			}
-		]
-	},
-	"tests":[
-		{
-			"send": {
-				"client":"client",
-				"method":"CreateDevice",
-				"param":"json struct",
-				"_meta":""
-			},
-			"servers": [
-				{
-					"name":"server11",
-					"meta": [
-						{
-							"key":"voltha_serial_number",
-							"value":"1"
-						}
-					],
-					"param":"mirror_client"
-				},
-				{
-					"name":"server12",
-					"meta": [
-						{
-							"key":"voltha_serial_number",
-							"value":"1"
-						}
-					],
-					"param":"mirror_client"
-				}
-			]
-		}
-	]
-}
diff --git a/tests/afrouter/suites/test3.tmpl.json b/tests/afrouter/suites/test3.tmpl.json
new file mode 100644
index 0000000..b3118ff
--- /dev/null
+++ b/tests/afrouter/suites/test3.tmpl.json
@@ -0,0 +1,235 @@
+{
+	"environment":{
+		"cmdLine":"afrouter -config arouter_test.json -logLevel 1",
+		"protoFiles": [
+			{
+				"importPath":"github.com/opencord/voltha-go/protos/",
+				"service":"VolthaService",
+				"package":"voltha"
+			},
+			{
+				"importPath":"github.com/opencord/voltha-go/protos/",
+				"service":"Configuration",
+				"package":"afrouter"
+			}
+		],
+		"imports": [
+			"github.com/golang/protobuf/ptypes/empty",
+			"github.com/opencord/voltha-go/protos/openflow_13"
+		],
+		"protoDesc":"voltha.pb",
+		"protoSubst": [
+			{
+				"from":"google.protobuf.Empty",
+				"to":"empty.Empty"
+			}
+		],
+		"clients": {
+			"imports": [
+				 "github.com/golang/protobuf/ptypes/empty",
+				 "github.com/opencord/voltha-go/protos/openflow_13"
+			],
+			"endpoints": [
+				{
+					"name":"client",
+					"port":"5000"
+				},
+				{
+					"name":"stats",
+					"port":"55554"
+				}
+			]
+		},
+		"servers": {
+			"imports": [
+				 "github.com/golang/protobuf/ptypes/empty",
+				 "github.com/opencord/voltha-go/protos/openflow_13"
+			],
+			"endpoints": [
+			{
+				"name":"core11",
+				"port":"5011"
+			},
+			{
+				"name":"core12",
+				"port":"5012"
+			},
+			{
+				"name":"core21",
+				"port":"5021"
+			},
+			{
+				"name":"core22",
+				"port":"5022"
+			},
+			{
+				"name":"core31",
+				"port":"5031"
+			},
+			{
+				"name":"core32",
+				"port":"5032"
+			},
+			{
+				"name":"roCore1",
+				"port":"5001"
+			},
+			{
+				"name":"roCore2",
+				"port":"5002"
+			},
+			{
+				"name":"roCore3",
+				"port":"5003"
+			}
+			]
+		}
+	},
+	"tests":[
+		{{range $k,$v := .RoTests}}
+		{
+			"name":"{{$v.Name}}",
+			"send": {
+				"client":"client",
+				"method":"{{$v.Method}}",
+				"param":"{{$v.Param}}",
+				"expect":"{{$v.Expect}}",
+				"_meta":""
+			},
+			"servers": [
+				{
+					"name":"core{{$v.Core}}1",
+					"meta": [
+						{
+							"key":"voltha_serial_number",
+							"value":"{{$v.SerNo}}"
+						}
+					]
+				},
+				{
+					"name":"core{{$v.Core}}2",
+					"meta": [
+						{
+							"key":"voltha_serial_number",
+							"value":"{{$v.SerNo}}"
+						}
+					]
+				}
+			]
+		},
+		{{end}}
+		{{range $k,$v := .RwTests}}
+		{
+			"name":"{{$v.Name}}",
+			"send": {
+				"client":"client",
+				"method":"{{$v.Method}}",
+				"param":"{{$v.Param}}",
+				"expect":"{{$v.Expect}}",
+				"_meta":""
+			},
+			"servers": [
+				{
+					"name":"core{{$v.Core}}1",
+					"meta": [
+						{
+							"key":"voltha_serial_number",
+							"value":"{{$v.SerNo}}"
+						}
+					]
+				},
+				{
+					"name":"core{{$v.Core}}2",
+					"meta": [
+						{
+							"key":"voltha_serial_number",
+							"value":"{{$v.SerNo}}"
+						}
+					]
+				}
+			]
+		},
+		{{end}}
+		{{range $k,$v := .CtlTests}}
+		{
+			"name":"{{$v.Name}}",
+			"send": {
+				"client":"client",
+				"method":"{{$v.Method}}",
+				"param":"{{$v.Param}}",
+				"expect":"{{$v.Expect}}",
+				"_meta":""
+			},
+			"servers": [
+				{
+					"name":"core{{$v.Core}}1",
+					"meta": [
+						{
+							"key":"voltha_serial_number",
+							"value":"{{$v.SerNo}}"
+						}
+					]
+				},
+				{
+					"name":"core{{$v.Core}}2",
+					"meta": [
+						{
+							"key":"voltha_serial_number",
+							"value":"{{$v.SerNo}}"
+						}
+					]
+				}
+			]
+		},
+		{{end}}
+		{
+			"name":"Test Subscribe",
+			"send": {
+				"client":"client",
+				"method":"Subscribe",
+				"param":"{OfagentId:\"Agent007\"}",
+				"meta": [
+					{
+						"key":"voltha_backend_name",
+						"value":""
+					}
+				],
+				"expect":"{OfagentId:\"Agent007\",VolthaId:\"core11\"}",
+				"expectMeta": [
+					{
+						"key":"voltha_backend_name",
+						"value":"vcore1"
+					}
+				]
+			},
+			"servers": [
+				{
+					"name":"core11",
+					"meta": [
+						{
+							"key":"voltha_serial_number",
+							"value":"44"
+						},
+						{
+							"key":"voltha_backend_name",
+							"value":""
+						}
+					]
+				}
+			]
+		},
+		{
+			"_COMMENT":"If this test case fails, there could be a goroutine leak",
+			"name":"Get goroutine count",
+			"send": {
+				"client":"stats",
+				"method":"GetGoroutineCount",
+				"param":"{}",
+				"meta": [ ],
+				"expect":"{Count:39}",
+				"expectMeta": [ ]
+			},
+			"servers": [ ]
+		}
+	]
+}
diff --git a/tests/afrouter/templates/client.go b/tests/afrouter/templates/client.go
index 8cd3330..db34efd 100644
--- a/tests/afrouter/templates/client.go
+++ b/tests/afrouter/templates/client.go
@@ -99,7 +99,7 @@
 			// value.
 			if resS,err := json.Marshal(res); err == nil {
 				if string(resS) != expect {
-					resFile.testLog("Unexpected result returned\n")
+					stats.testLog("Unexpected result returned expected '%s' got '%s'\n", expect, string(resS))
 					return errors.New("Unexpected result on method {{.Name}}")
 				}
 			} else {
@@ -109,11 +109,11 @@
 			for k,v := range expectMeta {
 				if rv,ok := hdr[k]; ok == true {
 					if rv[0] != v[0] {
-						resFile.testLog("Mismatch on returned metadata for key '%s' expected '%s' and got '%s'\n", k, v, rv)
+						stats.testLog("Mismatch on returned metadata for key '%s' expected '%s' and got '%s'\n", k, v, rv)
 						err = errors.New("Failure on returned metadata")
 					}
 				} else {
-					resFile.testLog("Returned metadata missing key '%s'; expected value '%s' at that key\n", k, v)
+					stats.testLog("Returned metadata missing key '%s'; expected value '%s' at that key\n", k, v)
 					err = errors.New("Failure on returned metadata")
 				}
 			}
diff --git a/tests/afrouter/templates/main.go b/tests/afrouter/templates/main.go
index c2fddb9..0361460 100644
--- a/tests/afrouter/templates/main.go
+++ b/tests/afrouter/templates/main.go
@@ -21,74 +21,15 @@
 
 import (
 	"os"
-	"fmt"
 	"time"
 	"os/exec"
 	"strings"
 	"context"
-	slog "log"
-	"io/ioutil"
-	"encoding/json"
-	"google.golang.org/grpc/grpclog"
+	//slog "log"
+	//"google.golang.org/grpc/grpclog"
 	"github.com/opencord/voltha-go/common/log"
 )
 
-type TestCase struct {
-	Title string `json:"title"`
-	Result bool `json:"result"`
-	Info []string `json:"info"`
-}
-
-type TestSuite struct {
-	Name string `json:"name"`
-	TestCases []TestCase `json:"testCases"`
-}
-
-type TestRun struct {
-	TestSuites []TestSuite
-}
-
-var resFile *tstLog
-type tstLog struct {
-	fp * os.File
-	fn string
-}
-
-func (tr * tstLog) testLog(format string, a ...interface{}) {
-	var err error
-	if tr.fp == nil {
-		if tr.fp, err = os.OpenFile(tr.fn, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644); err != nil {
-			log.Errorf("Could not append to the results file")
-			tr.fp = nil
-		}
-	}
-	if tr.fp != nil {
-		tr.fp.Write([]byte(fmt.Sprintf(format, a...)))
-	}
-}
-
-func (tr * tstLog) testLogOnce(format string, a ...interface{}) {
-	var err error
-	if tr.fp == nil {
-		if tr.fp, err = os.OpenFile(tr.fn, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644); err != nil {
-			log.Errorf("Could not append to the results file")
-			tr.fp = nil
-		}
-	}
-	if tr.fp != nil {
-		tr.fp.Write([]byte(fmt.Sprintf(format, a...)))
-	}
-	tr.fp.Close()
-	tr.fp = nil
-}
-
-func (tr * tstLog) close() {
-	if tr.fp != nil {
-		tr.fp.Close()
-	}
-}
-
-
 func startSut(cmdStr string) (*exec.Cmd, context.CancelFunc, error) {
 	var err error = nil
 
@@ -109,58 +50,35 @@
 
 func cleanUp(cmd *exec.Cmd, cncl context.CancelFunc) {
 	cncl()
-	cmd.Wait() // This seems to hang
-	// Give the child processes time to terminate
-	//time.Sleep(1 * time.Second)
+	cmd.Wait()
 }
 
-func readStats(stats * TestRun) () {
-	// Check if the  stats file exists
-	if _,err := os.Stat("stats.json"); err != nil {
-		// Nothing to do, just return
-		return
-	}
-	// The file is there, read it an unmarshal it into the stats struct
-	if statBytes, err := ioutil.ReadFile("stats.json"); err != nil {
-		log.Error(err)
-		return
-	} else if err := json.Unmarshal(statBytes, stats); err != nil {
-		log.Error(err)
-		return
-	}
-}
-
-func writeStats(stats * TestRun) () {
-	// Check if the  stats file exists
-	// The file is there, read it an unmarshal it into the stats struct
-	if statBytes, err := json.MarshalIndent(stats, "","    "); err != nil {
-		log.Error(err)
-		return
-	} else if err := ioutil.WriteFile("stats.json.new", statBytes, 0644); err != nil {
-		log.Error(err)
-		return
-	}
-	os.Rename("stats.json", "stats.json~")
-	os.Rename("stats.json.new", "stats.json")
-}
-var stats TestRun
-
 func main() {
 	var err error
 
 	// Setup logging
-	if _, err = log.SetDefaultLogger(log.JSON, 0, nil); err != nil {
+	if _, err = log.SetDefaultLogger(log.JSON, 1, nil); err != nil {
 		log.With(log.Fields{"error": err}).Fatal("Cannot setup logging")
 	}
 	defer log.CleanUp()
 
-	readStats(&stats)
+	if len(os.Args) < 2 {
+		log.Fatalf("Stat file name parameter missing for %s. Aborting...", os.Args[0])
+	} else {
+		statFn = os.Args[1]
+	}
 
+	if stats,err = readStats(statFn); err != nil {
+		log.Error(err)
+		return
+	}
+	defer writeStats(statFn, stats)
 
-	grpclog.SetLogger(slog.New(os.Stderr, "grpc: ", slog.LstdFlags))
+	// Add a stat entry for this run
 
-	resFile = &tstLog{fn:os.Args[1]}
-	defer resFile.close()
+	stats.appendNew()
+	tsIdx :=  len(stats.TestSuites) - 1
+	stats.TestSuites[tsIdx].Name = os.Args[0]
 
 
 	// Initialize the servers
@@ -186,7 +104,8 @@
 
 	// Run all the test cases now
 	log.Infof("Executing tests")
+
+	//log.Infof("Stats struct: %v", stats)
 	runTests()
-	writeStats(&stats)
 
 }
diff --git a/tests/afrouter/templates/runAll.go b/tests/afrouter/templates/runAll.go
index 03cdab1..5b02376 100644
--- a/tests/afrouter/templates/runAll.go
+++ b/tests/afrouter/templates/runAll.go
@@ -28,44 +28,6 @@
 	"github.com/opencord/voltha-go/common/log"
 )
 
-type tstLog struct {
-	fp * os.File
-	fn string
-}
-
-func (tr * tstLog) testLog(format string, a ...interface{}) {
-	var err error
-	if tr.fp == nil {
-		if tr.fp, err = os.OpenFile(tr.fn, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644); err != nil {
-			log.Errorf("Could not append to the results file")
-			tr.fp = nil
-		}
-	}
-	if tr.fp != nil {
-		tr.fp.Write([]byte(fmt.Sprintf(format, a...)))
-	}
-}
-
-func (tr * tstLog) testLogOnce(format string, a ...interface{}) {
-	var err error
-	if tr.fp == nil {
-		if tr.fp, err = os.OpenFile(tr.fn, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644); err != nil {
-			log.Errorf("Could not append to the results file")
-			tr.fp = nil
-		}
-	}
-	if tr.fp != nil {
-		tr.fp.Write([]byte(fmt.Sprintf(format, a...)))
-	}
-	tr.fp.Close()
-	tr.fp = nil
-}
-
-func (tr * tstLog) close() {
-	if tr.fp != nil {
-		tr.fp.Close()
-	}
-}
 
 func main() {
 	var cmd *exec.Cmd
@@ -77,17 +39,23 @@
 
 	defer log.CleanUp()
 
+	statFn = "stats.json"
+
 	log.Info("Running tests")
 	if err:= os.Chdir(os.Args[1]); err != nil {
 		log.Error("Could not change directory to %s: %v", os.Args[1], err)
 	}
-	tl := &tstLog{fn:"results.txt"}
+
+	if err := initStats(statFn); err != nil {
+		log.Error(err)
+		return
+	}
+
 	{{range .}}
 	cmdStr =  "./"+"{{.}}"+".e"
-	tl.testLogOnce("Running test suite '%s'\n", cmdStr[2:])
 
 	log.Infof("Running test suite %s",cmdStr)
-	cmd = exec.Command(cmdStr, "results.txt")
+	cmd = exec.Command(cmdStr, statFn)
 	cmd.Stdin = os.Stdin
 	cmd.Stdout = os.Stdout
 	cmd.Stderr = os.Stderr
@@ -96,10 +64,36 @@
 	}
 	{{end}}
 	// Open the results file and output it.
-	if resFile, err := ioutil.ReadFile("results.txt"); err == nil {
+	if s,err := readStats(statFn); err != nil {
+		log.Error(err)
+		return
+	} else {
+		stats = s
+	}
+
+	//log.Infof("Stats are: %v", stats)
+	if resFile, err := ioutil.ReadFile(statFn); err == nil {
 		fmt.Println(string(resFile))
 	} else {
-		log.Error("Could not load the results file 'results.txt'")
+		log.Error("Could not load the stats file 'stats.json'")
+	}
+	fmt.Println("Test result summary")
+	for _,v := range stats.TestSuites {
+		fmt.Printf("Test suite: %s\n", v.Name[2:len(v.Name)-2])
+		pass := 0
+		fail := 0
+		total := 0
+		for _,v1 := range v.TestCases {
+			total++
+			if v1.Result == true {
+				pass++
+			} else {
+				fail++
+			}
+		}
+		fmt.Printf("\tTotal test cases: %d\n", total)
+		fmt.Printf("\t\tTotal passed test cases: %d\n", pass)
+		fmt.Printf("\t\tTotal failed test cases: %d\n", fail)
 	}
 	log.Info("Tests complete")
 }
diff --git a/tests/afrouter/templates/runTests.go b/tests/afrouter/templates/runTests.go
index 61ba5eb..ac58220 100644
--- a/tests/afrouter/templates/runTests.go
+++ b/tests/afrouter/templates/runTests.go
@@ -18,6 +18,7 @@
 package main
 
 import (
+{{if .HasFuncs}}
 	"fmt"
 	"time"
 	"errors"
@@ -26,14 +27,22 @@
 	//"golang.org/x/net/context"
 	"google.golang.org/grpc/metadata"
 	"github.com/opencord/voltha-go/common/log"
+{{end}}
 	{{range .Imports}}
-	_ "{{.}}"
+	{{if .Used}}
+	"{{.Package}}"
+	{{end}}
 	{{end}}
 	{{range .ProtoImports}}
+	{{if .Used}}
 	{{.Short}} "{{.Package}}"
 	{{end}}
+	{{end}}
 )
 
+
+{{if .FileNum}}
+{{else}}
 var glCtx context.Context
 
 func resetChannels() {
@@ -54,70 +63,148 @@
 	}
 }
 
-func runTests() {
-	{{range $k,$v := .Tests}}
-	if err := test{{$k}}(); err == nil {
-		resFile.testLog("\tTest Successful\n")
-	} else {
-		resFile.testLog("\tTest Failed\n")
-	}
-	//resetChannels()
-	{{end}}
+type testData struct {
+	function func(int, string, string, []string,[]string,[]string,
+				  map[string][]string,interface{}, interface{}) error
+	testNum int
+	testName string
+	sendClient string
+	srvrs []string
+	sendMeta []string
+	expectMeta []string // Send Meta
+	srvMeta map[string][]string
+	parm interface{}
+	ret interface{}
 }
 
-{{range $k,$v := .Tests}}
-func test{{$k}}() error {
+func addTestSlot(stats * TestRun) {
+	tsIdx := len(stats.TestSuites) - 1
+	stats.TestSuites[tsIdx].TestCases =
+			append(stats.TestSuites[tsIdx].TestCases, TestCase{Info:[]string{}})
+}
+{{end}}
+
+{{if .FileNum}}
+func runTests{{.FileNum}}() {
+{{else}}
+func runTests() {
+{{end}}
+	tsIdx := len(stats.TestSuites) - 1
+	tests := []testData {
+	{{$ofs := .Offset}}
+	{{range $k,$v := .Tests}}
+	testData {
+		{{$v.Send.Method}}_test,
+		{{$k}} + {{$ofs}},
+		"{{$v.Name}}",
+		"{{$v.Send.Client}}",
+		[]string{ {{range $sk,$sv := $v.Srvr}} "{{$sv.Name}}",{{end}} },
+		[]string{ {{range $mk,$mv := $v.Send.MetaData}}"{{$mv.Key}}","{{$mv.Val}}",{{end}} },
+		[]string{ {{range $mk,$mv := $v.Send.ExpectMeta}}"{{$mv.Key}}","{{$mv.Val}}",{{end}} },
+		map[string][]string {
+		{{range $sk,$sv := $v.Srvr}}
+			"{{$sv.Name}}":[]string {
+			{{range $mk, $mv := $sv.Meta}}
+				 "{{$mv.Key}}","{{$mv.Val}}",
+			{{end}}
+			},
+		{{end}}
+		},
+		&{{$v.Send.ParamType}}{{$v.Send.Param}},
+		&{{$v.Send.ExpectType}}{{$v.Send.Expect}},
+	},
+	{{end}}
+	}
+
+	for _,v := range tests {
+		addTestSlot(stats)
+		stats.TestSuites[tsIdx].TestCases[v.testNum].Title = v.testName
+		if err := v.function(
+			v.testNum,
+			v.testName,
+			v.sendClient,
+			v.srvrs,
+			v.sendMeta,
+			v.expectMeta,
+			v.srvMeta,
+			v.parm,
+			v.ret); err != nil {
+			stats.TestSuites[tsIdx].TestCases[v.testNum].Result = false
+		} else {
+			stats.TestSuites[tsIdx].TestCases[v.testNum].Result = true
+		}
+	}
+	{{if .FileNum}}
+	{{else}}
+	{{range $k,$v := .RunTestsCallList}}
+	{{$v}}()
+	{{end}}
+	{{end}}
+	return
+	//resetChannels()
+}
+
+{{range $k,$v := .Funcs }}
+{{if $v.CodeGenerated}}
+{{else}}
+func {{$k}}_test(testNum int, testName string, sendClient string, srvrs []string,
+				 sendMeta []string, expectMeta []string, srvrMeta map[string][]string,
+				 parm interface{}, ret interface{}) error {
+
 	var rtrn error = nil
 	var cancel context.CancelFunc
+	var repl *reply
 
+	log.Debug("Running Test %d",testNum)
 	glCtx, cancel = context.WithTimeout(context.Background(), 900*time.Millisecond)
 	defer cancel()
-	// Announce the test being run
-	resFile.testLog("******************** Running test case ({{$k}}): {{$v.Name}}\n")
-	// Acquire the client used to run the test
-	cl := clients["{{$v.Send.Client}}"]
+
+	cl := clients[sendClient]
 	// Create the server's reply data structure
-	repl := &reply{repl:&{{$v.Send.ExpectType}}{{$v.Send.Expect}}}
-	// Send the reply data structure to each of the servers
-	{{range $s := .Srvr}}
-	if servers["{{$s.Name}}"] == nil {
-		err := errors.New("Server '{{$s.Name}}' is nil")
-		log.Error(err)
-		return err
+	switch r := ret.(type) {
+	case *{{$v.ReturnType}}:
+		repl = &reply{repl:r}
+	default:
+		log.Errorf("Invalid type in call to {{$k}}_test expecting {{$v.ReturnType}} got %T", ret)
 	}
-	// Start a go routine to send the the reply data to the
-	// server. The go routine blocks until the server picks
-	// up the data or the timeout is exceeded.
-	go func (ctx context.Context) {
-		select {
-		case servers["{{$s.Name}}"].replyData <- repl:
-		case <-ctx.Done():
-			rtrn := errors.New("Could not provide server {{$s.Name}} with reply data")
-			log.Error(rtrn)
-			resFile.testLog("%s\n", rtrn.Error())
+	// Send the reply data structure to each of the servers
+	for _,v := range srvrs {
+		if servers[v] == nil {
+			err := errors.New(fmt.Sprintf("Server %s is nil", v))
+			log.Error(err)
+			return err
 		}
-	}(glCtx)
-	{{end}}
+		// Start a go routine to send the the reply data to the
+		// server. The go routine blocks until the server picks
+		// up the data or the timeout is exceeded.
+		go func (ctx context.Context, srv string) {
+			select {
+			case servers[srv].replyData <- repl:
+			case <-ctx.Done():
+				rtrn := errors.New(fmt.Sprintf("Could not provide server %s with reply data",srv))
+				log.Error(rtrn)
+				stats.testLog("%s\n", rtrn.Error())
+			}
+		}(glCtx,v)
+	}
 
 	// Now call the RPC with the data provided
 	if expct,err := json.Marshal(repl.repl); err != nil {
-		log.Errorf("Marshaling the reply for test {{$v.Name}}: %v",err)
+		log.Errorf("Marshaling the reply for test %s: %v",testName, err)
 	} else {
 		// Create the context for the call
 		ctx := context.Background()
-		{{range $m := $v.Send.MetaData}}
-		ctx = metadata.AppendToOutgoingContext(ctx, "{{$m.Key}}", "{{$m.Val}}")
-		{{end}}
+		for i:=0; i<len(sendMeta); i += 2 {
+			ctx = metadata.AppendToOutgoingContext(ctx, sendMeta[i], sendMeta[i+1])
+		}
 		var md map[string]string = make(map[string]string)
-		{{range $m := $v.Send.ExpectMeta}}
-			md["{{$m.Key}}"] = "{{$m.Val}}"
-		{{end}}
+		for i:=0; i<len(expectMeta); i+=2 {
+			md[expectMeta[i]] = expectMeta[i+1]
+		}
 		expectMd := metadata.New(md)
-		if err := cl.send("{{$v.Send.Method}}", ctx,
-							&{{$v.Send.ParamType}}{{$v.Send.Param}},
-							string(expct), expectMd); err != nil {
-			log.Errorf("Test case {{$v.Name}} failed!: %v", err)
-
+		if err := cl.send("{{$k}}", ctx, parm, string(expct), expectMd); err != nil {
+			log.Errorf("Test case %s failed!: %v", testName, err)
+			rtrn = err
 		}
 	}
 
@@ -125,39 +212,38 @@
 	var s *serverCtl
 	var payload string
 	var i *incoming
-	if pld, err := json.Marshal(&{{$v.Send.ParamType}}{{$v.Send.Param}}); err != nil {
-		log.Errorf("Marshaling paramter for test {{$v.Name}}: %v", err)
+	if pld, err := json.Marshal(parm); err != nil {
+		log.Errorf("Marshaling paramter for test %s: %v", testName, err)
 	} else {
 		payload = string(pld)
 	}
-	{{range $s := .Srvr}}
-	s = servers["{{$s.Name}}"]
-	// Oddly sometimes the data isn't in the channel yet when we come to read it.
-	select {
-	case i = <-s.incmg:
-		if i.payload != payload {
-			rtrn = errors.New(fmt.Sprintf("Mismatched payload expected '%s', got '%s'", payload, i.payload))
-			log.Error(rtrn.Error())
-			resFile.testLog("%s\n", rtrn.Error())
-		}
-		{{range $m := $s.Meta}}
-		if mv,ok := i.meta["{{$m.Key}}"]; ok == true {
-			if "{{$m.Val}}" != mv[0] {
-				rtrn=errors.New(fmt.Sprintf("Mismatched metadata on server '%s' expected '%s', got '%s'", "{{$s.Name}}", "{{$m.Val}}", mv[0]))
+	for _,v := range srvrs {
+		s = servers[v]
+		// Oddly sometimes the data isn't in the channel yet when we come to read it.
+		select {
+		case i = <-s.incmg:
+			if i.payload != payload {
+				rtrn = errors.New(fmt.Sprintf("Mismatched payload expected '%s', got '%s'", payload, i.payload))
 				log.Error(rtrn.Error())
-				resFile.testLog("%s\n", rtrn.Error())
+				stats.testLog("%s\n", rtrn.Error())
 			}
+			for j:=0; j<len(srvrMeta[v]); j+=2 {
+				if mv,ok := i.meta[srvrMeta[v][j]]; ok == true {
+					if srvrMeta[v][j+1] != mv[0] {
+						rtrn=errors.New(fmt.Sprintf("Mismatched metadata on server '%s' expected '%s', got '%s'", srvrMeta[v][j], srvrMeta[v][j+1], mv[0]))
+						log.Error(rtrn.Error())
+						stats.testLog("%s\n", rtrn.Error())
+					}
+				}
+			}
+		case <-glCtx.Done():
+			rtrn = errors.New(fmt.Sprintf("Timeout: no response data available for server %s", testName))
+			stats.testLog("%s\n", rtrn.Error())
+			log.Error(rtrn)
 		}
-		{{end}}
-	case <-glCtx.Done():
-		rtrn = errors.New("Timeout: no response data available for server {{$s.Name}}")
-		resFile.testLog("%s\n", rtrn.Error())
-		log.Error(rtrn)
 	}
-	{{end}}
 
 	return rtrn
 }
 {{end}}
-
-
+{{end}}
diff --git a/tests/afrouter/templates/server.go b/tests/afrouter/templates/server.go
index ffea3d5..0f9ed48 100644
--- a/tests/afrouter/templates/server.go
+++ b/tests/afrouter/templates/server.go
@@ -123,7 +123,7 @@
 {{else}}
 func  (ts {{$.Name}}TestServer) {{.Name}}(ctx context.Context, in *{{.Param}}) (*{{.Rtrn}}, error) {
 	var r * incoming = &incoming{}
-	log.Debug("Serving {{$.Name}}")
+	//log.Debug("Serving {{$.Name}}")
 	// Read the metadata
 	if md,ok := metadata.FromIncomingContext(ctx); ok == false {
 		log.Error("Getting matadata during call to {{.Name}}")
diff --git a/tests/afrouter/templates/stats.go b/tests/afrouter/templates/stats.go
new file mode 100644
index 0000000..958bea7
--- /dev/null
+++ b/tests/afrouter/templates/stats.go
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+
+ * 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.
+ */
+// The template for the tester.
+// This template is filled in by the
+// test driver based on the configuration.
+
+package main
+
+import (
+	"os"
+	"fmt"
+	//slog "log"
+	"io/ioutil"
+	"encoding/json"
+	//"google.golang.org/grpc/grpclog"
+	"github.com/opencord/voltha-go/common/log"
+)
+
+type TestCase struct {
+	Title string `json:"title"`
+	Result bool `json:"passed"`
+	Info []string `json:"info"`
+}
+
+type TestSuite struct {
+	Name string `json:"name"`
+	Info []string `json:"info"`
+	TestCases []TestCase `json:"testCases"`
+}
+
+type TestRun struct {
+	TestSuites []TestSuite
+}
+
+func (tr * TestRun) testLog(format string, a ...interface{}) {
+
+	tridx := len(tr.TestSuites) - 1
+	tcidx := len(tr.TestSuites[tridx].TestCases) - 1
+
+	tr.TestSuites[tridx].TestCases[tcidx].Info =
+			append(tr.TestSuites[tridx].TestCases[tcidx].Info, fmt.Sprintf(format, a...))
+
+}
+
+func (tr * TestRun) suiteLog(format string, a ...interface{}) {
+
+	tridx := len(tr.TestSuites) - 1
+
+	tr.TestSuites[tridx].Info =
+			append(tr.TestSuites[tridx].Info, fmt.Sprintf(format, a...))
+
+}
+
+func readStats(statFn string) (*TestRun, error) {
+	// Check if the  stats file exists
+	if _,err := os.Stat(statFn); err != nil {
+		// Nothing to do, just return
+		return &TestRun{}, nil
+	}
+	rtrn := &TestRun{}
+	// The file is there, read it an unmarshal it into the stats struct
+	if statBytes, err := ioutil.ReadFile(statFn); err != nil {
+		log.Error(err)
+		return nil, err
+	} else if err := json.Unmarshal(statBytes, rtrn); err != nil {
+		log.Error(err)
+		return nil, err
+	}
+	return rtrn, nil
+}
+
+func writeStats(statFn string, stats * TestRun) error {
+	// Check if the  stats file exists
+	// The file is there, read it an unmarshal it into the stats struct
+	if statBytes, err := json.MarshalIndent(stats, "","    "); err != nil {
+		log.Error(err)
+		return err
+	} else if err := ioutil.WriteFile(statFn+".new", statBytes, 0644); err != nil {
+		log.Error(err)
+		return err
+	}
+	if err := os.Rename(statFn, statFn+"~"); err != nil {
+		log.Error(err)
+		return err
+	}
+	if err := os.Rename(statFn+".new", statFn); err != nil {
+		log.Error(err)
+		return err
+	}
+	return nil
+}
+
+func (tr * TestRun) appendNew() {
+	tr.TestSuites = append(tr.TestSuites, TestSuite{})
+}
+
+func initStats(statFn string) error {
+	s := &TestRun{}
+
+	if statBytes, err := json.MarshalIndent(s, "","    "); err != nil {
+		log.Error(err)
+		return err
+	} else if err := ioutil.WriteFile(statFn, statBytes, 0644); err != nil {
+		log.Error(err)
+		return err
+	}
+
+	return nil
+}
+
+
+var statFn string
+var stats *TestRun
diff --git a/tests/afrouter/tester.go b/tests/afrouter/tester.go
index f3d975e..fafbbbe 100644
--- a/tests/afrouter/tester.go
+++ b/tests/afrouter/tester.go
@@ -22,8 +22,10 @@
 	"fmt"
 	"flag"
 	"path"
+	"math"
 	//"bufio"
 	"errors"
+	"regexp"
 	"os/exec"
 	"strconv"
 	"io/ioutil"
@@ -34,6 +36,8 @@
 	pb "github.com/golang/protobuf/protoc-gen-go/descriptor"
 )
 
+const MAX_CT = 500
+
 type TestConfig struct {
 	configFile *string
 	logLevel *int
@@ -47,6 +51,11 @@
 	Port string `json:"port"`
 }
 
+type EndpointList struct {
+	Imports []string `json:"imports"`
+	Endpoints []Connection `json:"endPoints"`
+}
+
 type ProtoFile struct {
 	ImportPath string `json:"importPath"`
 	Service string `json:"service"`
@@ -62,8 +71,8 @@
 	Command string `json:"cmdLine"`
 	ProtoFiles []ProtoFile `json:"protoFiles"`
 	ProtoDesc string `json:"protoDesc"`
-	Clients []Connection `json:"clients"`
-	Servers []Connection `json:"servers"`
+	Clients EndpointList `json:"clients"`
+	Servers EndpointList `json:"servers"`
 	Imports []string `json:"imports"`
 	ProtoSubsts []ProtoSubst `json:"protoSubst"`
 }
@@ -89,6 +98,7 @@
 
 type Test struct {
 	Name string `json:"name"`
+	InfoOnly bool `json:"infoOnly"`
 	Send Rpc `json:"send"`
 	Servers []Server `json:"servers"`
 }
@@ -102,6 +112,7 @@
 	Service string
 	Short string
 	Package string
+	Used bool
 }
 
 type SendItem struct {
@@ -117,14 +128,32 @@
 
 type TestCase struct {
 	Name string
+	InfoOnly bool
 	Send SendItem
 	Srvr []Server
 }
 
+type MethodTypes struct {
+	ParamType string
+	ReturnType string
+	CodeGenerated bool
+}
+
+type Import struct {
+	Package string
+	Used bool
+}
+
 type TestList struct {
 	ProtoImports []ProtoImport
-	Imports []string
+	Imports []Import
 	Tests []TestCase
+	Funcs map[string]MethodTypes
+	RunTestsCallList []string
+	FileNum int
+	//NextFile int
+	HasFuncs bool
+	Offset int
 }
 
 type ClientConfig struct {
@@ -309,16 +338,19 @@
 						t *template.Template) error {
 	var servers []ServerConfig
 
-	for k,v := range ts.Env.Servers {
+	for k,v := range ts.Env.Servers.Endpoints {
 		log.Infof("Generating the code for server[%d]: %s", k, v.Name)
-		sc := &ServerConfig{Name:v.Name, Port:v.Port, Ct:k, Imports:ts.Env.Imports}
+		sc := &ServerConfig{Name:v.Name, Port:v.Port, Ct:k, Imports:ts.Env.Servers.Imports,
+							Methods:make(map[string]*mthd)}
 		for k1,v1 := range ts.Env.ProtoFiles {
 			imp := &ProtoImport{Short:"pb"+strconv.Itoa(k1),
 								Package:v1.ImportPath+v1.Package,
-								Service:v1.Service}
+								Service:v1.Service,
+								Used:true}
 			imp = &ProtoImport{Short:v1.Package,
 								Package:v1.ImportPath+v1.Package,
-								Service:v1.Service}
+								Service:v1.Service,
+								Used:true}
 			sc.ProtoImports = append(sc.ProtoImports, *imp)
 			// Compile the template from the file
 			log.Debugf("Proto substs: %v", ts.Env.ProtoSubsts)
@@ -329,7 +361,10 @@
 				return err
 			} else {
 				//Generate all the function calls required by the 
-				sc.Methods = mthds
+				for k,v := range mthds {
+					sc.Methods[k] = v
+				}
+				//sc.Methods = mthds
 			}
 		}
 		log.Debugf("Server: %v", *sc)
@@ -361,12 +396,10 @@
 func generateClients(conf *TestConfig, suiteDir string, ts * TestSuite,
 						t *template.Template) error {
 	var clients []ClientConfig
-	for k,v := range ts.Env.Clients {
+	for k,v := range ts.Env.Clients.Endpoints {
 		log.Infof("Generating the code for client[%d]: %s", k, v.Name)
-		cc := &ClientConfig{Name:v.Name, Port:v.Port, Ct:k, Imports:ts.Env.Imports}
-		// TODO: This loop makes no sense, the only proto map that would remain
-		// after this loop is the last one loaded. Fix this to load the map
-		// for all services not just the last one.
+		cc := &ClientConfig{Name:v.Name, Port:v.Port, Ct:k, Imports:ts.Env.Clients.Imports,
+							Methods:make(map[string]*mthd)}
 		for _,v1 := range ts.Env.ProtoFiles {
 			imp := &ProtoImport{Short:v1.Package,
 								Package:v1.ImportPath+v1.Package,
@@ -380,12 +413,14 @@
 						ts.Env.ProtoDesc, v1.Package, v1.Service)
 				return err
 			} else {
-				//Generate all the function calls required by the 
-				cc.Methods = mthds
+				// Add to the known methods
+				for k,v := range mthds {
+					cc.Methods[k] = v
+				}
 			}
 		}
 		clients = append(clients, *cc)
-		if f,err := os.Create(suiteDir+"/"+v.Name+".go"); err == nil {
+		if f,err := os.Create(suiteDir+"/"+v.Name+"_client.go"); err == nil {
 			_=f
 			defer f.Close()
 			if err := t.ExecuteTemplate(f, "client.go", cc); err != nil {
@@ -393,7 +428,7 @@
 				return err
 			}
 		} else {
-			log.Errorf("Couldn't create file %s : %v", suiteDir+"/client.go", err)
+			log.Errorf("Couldn't create file %s : %v", suiteDir+"/"+v.Name+"_client.go", err)
 			return err
 		}
 	}
@@ -409,28 +444,117 @@
 }
 
 func serverExists(srvr string, ts * TestSuite) bool {
-	for _,v := range ts.Env.Servers {
+	for _,v := range ts.Env.Servers.Endpoints {
 		if v.Name == srvr {
 			return true
 		}
 	}
 	return false
 }
+
+func getPackageList(tests []TestCase) map[string]struct{} {
+	var rtrn map[string]struct{} = make(map[string]struct{})
+	var p string
+	var e string
+	r := regexp.MustCompile(`^([^.]+)\..*$`)
+	for _,v := range tests {
+		pa := r.FindStringSubmatch(v.Send.ParamType)
+		if len(pa) == 2 {
+			p = pa[1]
+		} else {
+			log.Errorf("Internal error regexp returned %v", pa)
+		}
+		ea := r.FindStringSubmatch(v.Send.ExpectType)
+		if len(ea) == 2 {
+			e = ea[1]
+		} else {
+			log.Errorf("Internal error regexp returned %v", pa)
+		}
+		if _,ok := rtrn[p]; ok == false {
+			rtrn[p] = struct{}{}
+		}
+		if _,ok := rtrn[e]; ok == false {
+			rtrn[e] = struct{}{}
+		}
+	}
+	return rtrn
+}
+
+func fixupProtoImports(protoImports []ProtoImport, used map[string]struct{}) []ProtoImport {
+	var rtrn []ProtoImport
+	//log.Infof("Updating imports %v, using %v", protoImports, used)
+	for _,v := range protoImports {
+		//log.Infof("Looking for package %s", v.Package)
+		if _,ok := used[v.Short]; ok == true {
+			rtrn = append(rtrn, ProtoImport {
+				Service: v.Service,
+				Short: v.Short,
+				Package: v.Package,
+				Used: true,
+			})
+		} else {
+			rtrn = append(rtrn, ProtoImport {
+				Service: v.Service,
+				Short: v.Short,
+				Package: v.Package,
+				Used: false,
+			})
+		}
+	}
+	//log.Infof("After update %v", rtrn)
+	return rtrn
+}
+
+func fixupImports(imports []Import, used map[string]struct{}) []Import {
+	var rtrn []Import
+	var p string
+	re := regexp.MustCompile(`^.*/([^/]+)$`)
+	//log.Infof("Updating imports %v, using %v", protoImports, used)
+	for _,v := range imports {
+		//log.Infof("Looking for match in %s", v.Package)
+		pa := re.FindStringSubmatch(v.Package)
+		if len(pa) == 2 {
+			p = pa[1]
+		} else {
+			log.Errorf("Substring match failed, regexp returned %v", pa)
+		}
+		//log.Infof("Looking for package %s", v.Package)
+		if _,ok := used[p]; ok == true {
+			rtrn = append(rtrn, Import {
+				Package: v.Package,
+				Used: true,
+			})
+		} else {
+			rtrn = append(rtrn, Import {
+				Package: v.Package,
+				Used: false,
+			})
+		}
+	}
+	//log.Infof("After update %v", rtrn)
+	return rtrn
+}
+
+
 func generateTestCases(conf *TestConfig, suiteDir string, ts * TestSuite,
 						t *template.Template) error {
 	var mthdMap map[string]*mthd
+	mthdMap = make(map[string]*mthd)
 	// Generate the test cases
 	log.Info("Generating the test cases: runTests.go")
-	tc := &TestList{Imports:ts.Env.Imports}
+	tc := &TestList{Funcs:make(map[string]MethodTypes),
+					FileNum:0, HasFuncs: false,
+					Offset:0}
+	for _,v := range ts.Env.Imports {
+		tc.Imports = append(tc.Imports,Import{Package:v, Used:true})
+	}
 
 	// Load the proto descriptor file
-	// TODO: This loop makes no sense, the only proto map that would remain
-	// after this loop is the last one loaded. Fix this to load the map
-	// for all services not just the last one.
 	for _,v := range ts.Env.ProtoFiles {
 		imp := &ProtoImport{Short:v.Package,
 							Package:v.ImportPath+v.Package,
-							Service:v.Service}
+							Service:v.Service,
+							Used:true}
 		tc.ProtoImports = append(tc.ProtoImports, *imp)
 		// Compile the template from the file
 		log.Debugf("Proto substs: %v", ts.Env.ProtoSubsts)
@@ -440,14 +564,59 @@
 					ts.Env.ProtoDesc, v.Package, v.Service)
 			return err
 		} else {
-			mthdMap = mthds
+			// Add to the known methods
+			for k,v := range mthds {
+				mthdMap[k] = v
+			}
 		}
 	}
+	// Since the input file can possibly be a template with loops that
+	// make multiple calls to exactly the same method with potentially
+	// different parameters it is best to try to optimize for that case.
+	// Creating a function for each method used will greatly reduce the
+	// code size and repetition. In the case where a method is used only
+	// once the resulting code will be bigger but this is an acceptable
+	// tradeoff to allow huge suites that call the same method repeatedly
+	// to test for leaks.
+
+	// The go compiler has trouble compiling files that are too large. In order
+	// to mitigate that, It's necessary to create more smaller files than the
+	// single one large one while making sure that the functions for the distinct
+	// methods are only defined once in one of the files.
+
+	// Yet another hiccup with the go compiler, it doesn't like deep function
+	// nesting meaning that each runTests function can't call the next without
+	// eventually blowing the stack check. In order to work around this, the
+	// first runTests function needs to call all the others in sequence to
+	// keep the stack depth constant.
+
+	// Counter limiting the number of tests to output to each file
+	maxCt := MAX_CT
+
+	// Get the list of distinct methods
+	//for _,v := range ts.Tests {
+	//	if _,ok := tc.Funcs[v.Send.Method]; ok == false {
+	//		tc.Funcs[v.Send.Method] = MethodTypes{ParamType:mthdMap[v.Send.Method].Param,
+	//											  ReturnType:mthdMap[v.Send.Method].Rtrn}
+	//	}
+	//}
+	for i:=1; i<int(math.Ceil(float64(len(ts.Tests))/float64(MAX_CT))); i++ {
+		tc.RunTestsCallList = append(tc.RunTestsCallList, "runTests"+strconv.Itoa(i))
+	}
+
 	// Create the test data structure for the template
 	for _,v := range ts.Tests {
 		var test TestCase
 
+		if _,ok := tc.Funcs[v.Send.Method]; ok == false {
+			tc.Funcs[v.Send.Method] = MethodTypes{ParamType:mthdMap[v.Send.Method].Param,
+												  ReturnType:mthdMap[v.Send.Method].Rtrn,
+												  CodeGenerated:false}
+			tc.HasFuncs = true
+		}
+
 		test.Name = v.Name
+		test.InfoOnly = v.InfoOnly
 		test.Send.Client = v.Send.Client
 		test.Send.Method = v.Send.Method
 		test.Send.Param = v.Send.Param
@@ -471,15 +640,63 @@
 			test.Srvr = append(test.Srvr, srvr)
 		}
 		tc.Tests = append(tc.Tests, test)
+		if maxCt--; maxCt == 0 {
+			// Get the list of proto pacakges required for this file
+			pkgs := getPackageList(tc.Tests)
+			// Adjust the proto import data accordingly
+			tc.ProtoImports = fixupProtoImports(tc.ProtoImports, pkgs)
+			tc.Imports = fixupImports(tc.Imports, pkgs)
+			//log.Infof("The packages needed are: %v", pkgs)
+			if f,err := os.Create(suiteDir+"/runTests"+strconv.Itoa(tc.FileNum)+".go"); err == nil {
+				if err := t.ExecuteTemplate(f, "runTests.go", tc); err != nil {
+						log.Errorf("Unable to execute template for runTests.go: %v", err)
+				}
+				f.Close()
+			} else {
+				log.Errorf("Couldn't create file %s : %v",
+						suiteDir+"/runTests"+strconv.Itoa(tc.FileNum)+".go", err)
+			}
+			tc.FileNum++
+			//tc.NextFile++
+			maxCt = MAX_CT
+			// Mark the functions as generated.
+			tc.Tests = []TestCase{}
+			for k,v := range tc.Funcs {
+				tc.Funcs[k] = MethodTypes{ParamType:v.ParamType,
+										 ReturnType:v.ReturnType,
+										 CodeGenerated:true}
+			}
+			tc.HasFuncs = false
+			tc.Offset += MAX_CT
+			//tmp,_ := strconv.Atoi(tc.Offset)
+			//tc.Offset = strconv.Itoa(tmp+500)
+		}
 	}
-	if f,err := os.Create(suiteDir+"/runTests.go"); err == nil {
+	//tc.NextFile = 0
+	// Get the list of proto pacakges required for this file
+	pkgs := getPackageList(tc.Tests)
+	// Adjust the proto import data accordingly
+	tc.ProtoImports = fixupProtoImports(tc.ProtoImports, pkgs)
+	tc.Imports = fixupImports(tc.Imports, pkgs)
+	//log.Infof("The packages needed are: %v", pkgs)
+	if f,err := os.Create(suiteDir+"/runTests"+strconv.Itoa(tc.FileNum)+".go"); err == nil {
 		if err := t.ExecuteTemplate(f, "runTests.go", tc); err != nil {
 				log.Errorf("Unable to execute template for runTests.go: %v", err)
 		}
 		f.Close()
 	} else {
-		log.Errorf("Couldn't create file %s : %v", suiteDir+"/runTests.go", err)
+		log.Errorf("Couldn't create file %s : %v",
+				suiteDir+"/runTests"+strconv.Itoa(tc.FileNum)+".go", err)
 	}
+
+	//if f,err := os.Create(suiteDir+"/runTests.go"); err == nil {
+	//	if err := t.ExecuteTemplate(f, "runTests.go", tc); err != nil {
+	//			log.Errorf("Unable to execute template for runTests.go: %v", err)
+	//	}
+	//	f.Close()
+	//} else {
+	//	log.Errorf("Couldn't create file %s : %v", suiteDir+"/runTests.go", err)
+	//}
 	return nil
 }
 
@@ -497,13 +714,14 @@
 		ts := &TestSuite{}
 		ts.loadSuite(v)
 		log.Debugf("Suite %s: %v", v, ts)
-		log.Info("Processing test suite %s", v)
+		log.Infof("Processing test suite %s", v)
 
 		t := template.Must(template.New("").ParseFiles("../templates/server.go",
 														"../templates/serverInit.go",
 														"../templates/client.go",
 														"../templates/clientInit.go",
 														"../templates/runTests.go",
+														"../templates/stats.go",
 														"../templates/main.go"))
 		// Create a directory for he source code for this test suite
 		if err := os.Mkdir(suiteDir, 0777); err != nil {
@@ -537,6 +755,17 @@
 			log.Errorf("Couldn't create file %s : %v", suiteDir+"/main.go", err)
 		}
 
+		log.Infof("Copying over common modules")
+		if f,err := os.Create(suiteDir+"/stats.go"); err == nil {
+			if err := t.ExecuteTemplate(f, "stats.go", ts.Env); err != nil {
+					log.Errorf("Unable to execute template for stats.go: %v", err)
+			}
+			f.Close()
+		} else {
+			log.Errorf("Couldn't create file %s : %v", suiteDir+"/stats.go", err)
+		}
+
+
 		log.Infof("Compiling test suite: %s in directory %s", v, suiteDir)
 		if err := os.Chdir(suiteDir); err != nil {
 			log.Errorf("Could not change to directory '%s':%v",suiteDir, err)
@@ -562,15 +791,24 @@
 		log.Errorf("Unable to create directory 'driver':%v\n", err)
 		return err
 	}
-	t := template.Must(template.New("").ParseFiles("../templates/runAll.go"))
+	t := template.Must(template.New("").ParseFiles("../templates/runAll.go",
+					"../templates/stats.go"))
 	if f,err := os.Create(srcDir+"/runAll.go"); err == nil {
 		if err := t.ExecuteTemplate(f, "runAll.go", conf.Suites); err != nil {
-				log.Errorf("Unable to execute template for main.go: %v", err)
+				log.Errorf("Unable to execute template for runAll.go: %v", err)
 		}
 		f.Close()
 	} else {
 		log.Errorf("Couldn't create file %s : %v", srcDir+"/runAll.go", err)
 	}
+	if f,err := os.Create(srcDir+"/stats.go"); err == nil {
+		if err := t.ExecuteTemplate(f, "stats.go", conf.Suites); err != nil {
+				log.Errorf("Unable to execute template for stats.go: %v", err)
+		}
+		f.Close()
+	} else {
+		log.Errorf("Couldn't create file %s : %v", srcDir+"/stats.go", err)
+	}
 
 	// Compile the test driver file
 	log.Info("Compiling the test driver")