VOL-2086 unit tests for arouter.go;
Set exit status on errors

Change-Id: Ic395f454ed7e1d7ebc9b429b3c44cb0a2e785311
diff --git a/cmd/arouter/arouter.go b/cmd/arouter/arouter.go
index cfd0282..9415362 100644
--- a/cmd/arouter/arouter.go
+++ b/cmd/arouter/arouter.go
@@ -27,17 +27,19 @@
 	"os"
 )
 
-func main() {
+// startup arouter, return exit status as an integer
+func startup() int {
 
 	conf, err := afrouter.ParseCmd()
 	if err != nil {
 		fmt.Printf("Error: %v\n", err)
-		return
+		return 1
 	}
 
 	// Setup logging
 	if _, err := log.SetDefaultLogger(log.JSON, *conf.LogLevel, log.Fields{"instanceId": conf.InstanceID}); err != nil {
 		log.With(log.Fields{"error": err}).Fatal("Cannot setup logging")
+		return 1
 	}
 
 	defer func() {
@@ -52,14 +54,14 @@
 	if *conf.DisplayVersionOnly {
 		fmt.Println("VOLTHA API Server (afrouter)")
 		fmt.Println(version.VersionInfo.String("  "))
-		return
+		return 0
 	}
 
 	// Parse the config file
 	err = conf.LoadConfig()
 	if err != nil {
 		log.Error(err)
-		return
+		return 1
 	}
 	log.With(log.Fields{"config": *conf}).Debug("Configuration loaded")
 
@@ -72,18 +74,29 @@
 	err = afrouter.InitExitHandler()
 	if err != nil {
 		log.Errorf("Failed to initialize exit handler, exiting: %v", err)
-		return
+		return 1
 	}
 
 	// Create the affinity router proxy...
 	if ap, err := afrouter.NewArouterProxy(conf); err != nil {
 		log.Errorf("Failed to create the arouter proxy, exiting:%v", err)
-		return
+		return 1
 		// and start it.
 		// This function never returns unless an error
 		// occurs or a signal is caught.
+	} else if *conf.DryRun {
+		// Do nothing
 	} else if err := ap.ListenAndServe(); err != nil {
 		log.Errorf("Exiting on error %v", err)
+		return 1
 	}
 
+	return 0
+}
+
+func main() {
+	status := startup()
+	if status != 0 {
+		os.Exit(status)
+	}
 }
diff --git a/cmd/arouter/arouter_test.go b/cmd/arouter/arouter_test.go
new file mode 100644
index 0000000..26c6d4e
--- /dev/null
+++ b/cmd/arouter/arouter_test.go
@@ -0,0 +1,244 @@
+/*
+ * 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.
+ */
+
+// This file implements an exit handler that tries to shut down all the
+// running servers before finally exiting. There are 2 triggers to this
+// clean exit thread: signals and an exit channel.
+
+package main
+
+import (
+	"fmt"
+	"github.com/phayes/freeport"
+	"github.com/stretchr/testify/assert"
+	"io/ioutil"
+	"os"
+	"testing"
+)
+
+// Generate a configuration, ensure any ports are randomly chosen free ports
+func MakeConfig() (string, error) {
+	freePort, err := freeport.GetFreePort()
+	if err != nil {
+		return "", err
+	}
+
+	config := fmt.Sprintf(`{
+	"api": {
+		"_comment": "If this isn't defined then no api is available for dynamic configuration and queries",
+		"address": "localhost",
+		"port": %d
+	}
+ }`, freePort)
+
+	return config, nil
+}
+
+// run the function fp() and return its return value and stdout
+func CaptureStdout(fp func() int) (int, string, error) {
+	origStdout := os.Stdout
+
+	// log.Cleanup() will call Sync on sys.stdout, and that doesn't
+	// work on pipes. Instead of creating a pipe, write the output
+	// to a file, then read that file back in.
+	f, err := ioutil.TempFile("", "arouter.json")
+	if err != nil {
+		return 0, "", err
+	}
+
+	// Make sure the file is closed and deleted on exit
+	defer func() { f.Close(); os.Remove(f.Name()) }()
+
+	// reassign stdout to the file, ensure it will be restored on exit
+	os.Stdout = f
+	defer func() { os.Stdout = origStdout }()
+
+	status := fp()
+
+	// read back the contents of the tempfile
+	_, err = f.Seek(0, 0)
+	if err != nil {
+		return 0, "", err
+	}
+	out := make([]byte, 16384)
+	numRead, err := f.Read(out)
+	if err != nil {
+		return 0, "", err
+	}
+
+	return status, string(out[:numRead]), nil
+}
+
+// Test output of "--version" command
+func TestStartupVersionOnly(t *testing.T) {
+	oldArgs := os.Args
+	defer func() { os.Args = oldArgs }()
+
+	config, err := MakeConfig()
+	assert.Nil(t, err)
+
+	f, err := ioutil.TempFile("", "arouter.json")
+	assert.Nil(t, err)
+	_, err = f.WriteString(config)
+	assert.Nil(t, err)
+	f.Close()
+
+	defer func() { os.Remove(f.Name()) }()
+
+	os.Args = []string{os.Args[0], "--version", "--config", f.Name()}
+
+	status, s, err := CaptureStdout(startup)
+	assert.Nil(t, err)
+
+	assert.Equal(t, 0, status)
+
+	expected := `VOLTHA API Server (afrouter)
+  Version:      unknown-version
+  GoVersion:    unknown-goversion
+  VCS Ref:      unknown-vcsref
+  VCS Dirty:    unknown-vcsdirty
+  Built:        unknown-buildtime
+  OS/Arch:      unknown-os/unknown-arch
+
+`
+	assert.Equal(t, expected, s)
+}
+
+func TestStartupMissingConfigFile(t *testing.T) {
+	oldArgs := os.Args
+	defer func() { os.Args = oldArgs }()
+
+	config, err := MakeConfig()
+	assert.Nil(t, err)
+
+	f, err := ioutil.TempFile("", "arouter.json")
+	assert.Nil(t, err)
+	_, err = f.WriteString(config)
+	assert.Nil(t, err)
+	f.Close()
+
+	defer func() { os.Remove(f.Name()) }()
+
+	os.Args = []string{os.Args[0], "--config", "doesnotexist"}
+
+	// The Voltha logger will write messages to stdout
+
+	status, s, err := CaptureStdout(startup)
+	assert.Nil(t, err)
+
+	assert.Equal(t, 1, status)
+
+	assert.Contains(t, s, "open doesnotexist: no such file or directory")
+}
+
+func TestStartupDryRun(t *testing.T) {
+	oldArgs := os.Args
+	defer func() { os.Args = oldArgs }()
+
+	config, err := MakeConfig()
+	assert.Nil(t, err)
+
+	f, err := ioutil.TempFile("", "arouter.json")
+	assert.Nil(t, err)
+	_, err = f.WriteString(config)
+	assert.Nil(t, err)
+	f.Close()
+
+	defer func() { os.Remove(f.Name()) }()
+
+	os.Args = []string{os.Args[0], "--dry-run", "--config", f.Name()}
+
+	status, s, err := CaptureStdout(startup)
+	assert.Nil(t, err)
+
+	assert.Equal(t, 0, status)
+
+	assert.Contains(t, s, "Configuration loaded")
+}
+
+func TestStartupDryRunGrpcLog(t *testing.T) {
+	oldArgs := os.Args
+	defer func() { os.Args = oldArgs }()
+
+	config, err := MakeConfig()
+	assert.Nil(t, err)
+
+	f, err := ioutil.TempFile("", "arouter.json")
+	assert.Nil(t, err)
+	_, err = f.WriteString(config)
+	assert.Nil(t, err)
+	f.Close()
+
+	defer func() { os.Remove(f.Name()) }()
+
+	os.Args = []string{os.Args[0], "--dry-run", "--grpclog", "--config", f.Name()}
+
+	status, s, err := CaptureStdout(startup)
+	assert.Nil(t, err)
+
+	assert.Equal(t, 0, status)
+
+	assert.Contains(t, s, "Configuration loaded")
+}
+
+// An unknown command-line option should produce an error
+func TestStartupBadCommandLine(t *testing.T) {
+	oldArgs := os.Args
+	defer func() { os.Args = oldArgs }()
+
+	config, err := MakeConfig()
+	assert.Nil(t, err)
+
+	f, err := ioutil.TempFile("", "arouter.json")
+	assert.Nil(t, err)
+	_, err = f.WriteString(config)
+	assert.Nil(t, err)
+	f.Close()
+
+	defer func() { os.Remove(f.Name()) }()
+
+	os.Args = []string{os.Args[0], "--dry-run", "--badoption", "--config", f.Name()}
+
+	status, s, err := CaptureStdout(startup)
+	assert.Nil(t, err)
+
+	assert.Equal(t, 1, status)
+
+	assert.Contains(t, s, "Error: Error parsing the command line")
+}
+
+// A config file with invalid contents should cause logging output of the error
+func TestStartupBadConfigFile(t *testing.T) {
+	oldArgs := os.Args
+	defer func() { os.Args = oldArgs }()
+
+	f, err := ioutil.TempFile("", "arouter.json")
+	assert.Nil(t, err)
+	_, err = f.WriteString("this is not proper json")
+	assert.Nil(t, err)
+	f.Close()
+
+	defer func() { os.Remove(f.Name()) }()
+
+	os.Args = []string{os.Args[0], "--dry-run", "--config", f.Name()}
+
+	status, s, err := CaptureStdout(startup)
+	assert.Nil(t, err)
+
+	assert.Equal(t, 1, status)
+
+	assert.Contains(t, s, "invalid character")
+}
diff --git a/internal/pkg/afrouter/config.go b/internal/pkg/afrouter/config.go
index 5dccf32..c9bef79 100644
--- a/internal/pkg/afrouter/config.go
+++ b/internal/pkg/afrouter/config.go
@@ -40,6 +40,7 @@
 	config.LogLevel = cmdParse.Int("logLevel", 0, "The log level for the affinity router")
 	config.GrpcLog = cmdParse.Bool("grpclog", false, "Enable GRPC logging")
 	config.DisplayVersionOnly = cmdParse.Bool("version", false, "Print version information and exit")
+	config.DryRun = cmdParse.Bool("dry-run", false, "Verify config file, but exit before starting to serve requests")
 
 	err := cmdParse.Parse(os.Args[1:])
 	if err != nil {
@@ -65,6 +66,7 @@
 	LogLevel           *int
 	GrpcLog            *bool
 	DisplayVersionOnly *bool
+	DryRun             *bool
 	Servers            []ServerConfig         `json:"servers"`
 	Ports              PortConfig             `json:"ports"`
 	ServerCertificates ServerCertConfig       `json:"serverCertificates"`