veyron/services/mgmt/node/impl: separate out state management types and routines
into a new source file; add unit tests.

Change-Id: I840a6b77a2fde0eefd4d0106bf8ab6fa38332be7
diff --git a/services/mgmt/node/impl/app_invoker.go b/services/mgmt/node/impl/app_invoker.go
index c81e079..871a78e 100644
--- a/services/mgmt/node/impl/app_invoker.go
+++ b/services/mgmt/node/impl/app_invoker.go
@@ -106,106 +106,6 @@
 	"veyron2/vlog"
 )
 
-// installationState describes the states that an installation can be in at any
-// time.
-type installationState int
-
-const (
-	active installationState = iota
-	uninstalled
-)
-
-// String returns the name that will be used to encode the state as a file name
-// in the installation's dir.
-func (s installationState) String() string {
-	switch s {
-	case active:
-		return "active"
-	case uninstalled:
-		return "uninstalled"
-	default:
-		return "unknown"
-	}
-}
-
-func installationStateIs(installationDir string, state installationState) bool {
-	if _, err := os.Stat(filepath.Join(installationDir, state.String())); err != nil {
-		return false
-	}
-	return true
-}
-
-func transitionInstallation(installationDir string, initial, target installationState) error {
-	return transitionState(installationDir, initial, target)
-}
-
-func initializeInstallation(installationDir string, initial installationState) error {
-	return initializeState(installationDir, initial)
-}
-
-// instanceState describes the states that an instance can be in at any time.
-type instanceState int
-
-const (
-	starting instanceState = iota
-	started
-	suspending
-	suspended
-	stopping
-	stopped
-)
-
-// String returns the name that will be used to encode the state as a file name
-// in the instance's dir.
-func (s instanceState) String() string {
-	switch s {
-	case starting:
-		return "starting"
-	case started:
-		return "started"
-	case suspending:
-		return "suspending"
-	case suspended:
-		return "suspended"
-	case stopping:
-		return "stopping"
-	case stopped:
-		return "stopped"
-	default:
-		return "unknown"
-	}
-}
-
-func transitionInstance(instanceDir string, initial, target instanceState) error {
-	return transitionState(instanceDir, initial, target)
-}
-
-func initializeInstance(instanceDir string, initial instanceState) error {
-	return initializeState(instanceDir, initial)
-}
-
-func transitionState(dir string, initial, target fmt.Stringer) error {
-	initialState := filepath.Join(dir, initial.String())
-	targetState := filepath.Join(dir, target.String())
-	if err := os.Rename(initialState, targetState); err != nil {
-		if os.IsNotExist(err) {
-			return errInvalidOperation
-		}
-		vlog.Errorf("Rename(%v, %v) failed: %v", initialState, targetState, err) // Something went really wrong.
-		return errOperationFailed
-	}
-	return nil
-}
-
-func initializeState(dir string, initial fmt.Stringer) error {
-	initialStatus := filepath.Join(dir, initial.String())
-	if err := ioutil.WriteFile(initialStatus, []byte("status"), 0600); err != nil {
-		vlog.Errorf("WriteFile(%v) failed: %v", initialStatus, err)
-		return errOperationFailed
-	}
-	return nil
-}
-
 // instanceInfo holds state about a running instance.
 type instanceInfo struct {
 	AppCycleMgrName string
diff --git a/services/mgmt/node/impl/app_state.go b/services/mgmt/node/impl/app_state.go
new file mode 100644
index 0000000..744dcb5
--- /dev/null
+++ b/services/mgmt/node/impl/app_state.go
@@ -0,0 +1,110 @@
+package impl
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+
+	"veyron2/vlog"
+)
+
+// installationState describes the states that an installation can be in at any
+// time.
+type installationState int
+
+const (
+	active installationState = iota
+	uninstalled
+)
+
+// String returns the name that will be used to encode the state as a file name
+// in the installation's dir.
+func (s installationState) String() string {
+	switch s {
+	case active:
+		return "active"
+	case uninstalled:
+		return "uninstalled"
+	default:
+		return "unknown"
+	}
+}
+
+func installationStateIs(installationDir string, state installationState) bool {
+	if _, err := os.Stat(filepath.Join(installationDir, state.String())); err != nil {
+		return false
+	}
+	return true
+}
+
+func transitionInstallation(installationDir string, initial, target installationState) error {
+	return transitionState(installationDir, initial, target)
+}
+
+func initializeInstallation(installationDir string, initial installationState) error {
+	return initializeState(installationDir, initial)
+}
+
+// instanceState describes the states that an instance can be in at any time.
+type instanceState int
+
+const (
+	starting instanceState = iota
+	started
+	suspending
+	suspended
+	stopping
+	stopped
+)
+
+// String returns the name that will be used to encode the state as a file name
+// in the instance's dir.
+func (s instanceState) String() string {
+	switch s {
+	case starting:
+		return "starting"
+	case started:
+		return "started"
+	case suspending:
+		return "suspending"
+	case suspended:
+		return "suspended"
+	case stopping:
+		return "stopping"
+	case stopped:
+		return "stopped"
+	default:
+		return "unknown"
+	}
+}
+
+func transitionInstance(instanceDir string, initial, target instanceState) error {
+	return transitionState(instanceDir, initial, target)
+}
+
+func initializeInstance(instanceDir string, initial instanceState) error {
+	return initializeState(instanceDir, initial)
+}
+
+func transitionState(dir string, initial, target fmt.Stringer) error {
+	initialState := filepath.Join(dir, initial.String())
+	targetState := filepath.Join(dir, target.String())
+	if err := os.Rename(initialState, targetState); err != nil {
+		if os.IsNotExist(err) {
+			return errInvalidOperation
+		}
+		vlog.Errorf("Rename(%v, %v) failed: %v", initialState, targetState, err) // Something went really wrong.
+		return errOperationFailed
+	}
+	return nil
+}
+
+func initializeState(dir string, initial fmt.Stringer) error {
+	initialStatus := filepath.Join(dir, initial.String())
+	if err := ioutil.WriteFile(initialStatus, []byte("status"), 0600); err != nil {
+		vlog.Errorf("WriteFile(%v) failed: %v", initialStatus, err)
+		return errOperationFailed
+	}
+	return nil
+}
diff --git a/services/mgmt/node/impl/app_state_test.go b/services/mgmt/node/impl/app_state_test.go
new file mode 100644
index 0000000..0fd8c2d
--- /dev/null
+++ b/services/mgmt/node/impl/app_state_test.go
@@ -0,0 +1,70 @@
+package impl
+
+import (
+	"io/ioutil"
+	"os"
+	"testing"
+)
+
+// TestInstallationState verifies the state transition logic for app installations.
+func TestInstallationState(t *testing.T) {
+	dir, err := ioutil.TempDir("", "installation")
+	if err != nil {
+		t.Fatalf("Failed to create temp dir: %v", err)
+	}
+	defer os.RemoveAll(dir)
+	// Uninitialized state.
+	if transitionInstallation(dir, active, uninstalled) == nil {
+		t.Fatalf("transitionInstallation should have failed")
+	}
+	if isActive, isUninstalled := installationStateIs(dir, active), installationStateIs(dir, uninstalled); isActive || isUninstalled {
+		t.Fatalf("isActive, isUninstalled = %t, %t (expected false, false)", isActive, isUninstalled)
+	}
+	// Initialize.
+	if err := initializeInstallation(dir, active); err != nil {
+		t.Fatalf("initializeInstallation failed: %v", err)
+	}
+	if !installationStateIs(dir, active) {
+		t.Fatalf("Installation state expected to be %v", active)
+	}
+	if err := transitionInstallation(dir, active, uninstalled); err != nil {
+		t.Fatalf("transitionInstallation failed: %v", err)
+	}
+	if !installationStateIs(dir, uninstalled) {
+		t.Fatalf("Installation state expected to be %v", uninstalled)
+	}
+	// Invalid transition: wrong initial state.
+	if transitionInstallation(dir, active, uninstalled) == nil {
+		t.Fatalf("transitionInstallation should have failed")
+	}
+	if !installationStateIs(dir, uninstalled) {
+		t.Fatalf("Installation state expected to be %v", uninstalled)
+	}
+}
+
+// TestInstanceState verifies the state transition logic for app instances.
+func TestInstanceState(t *testing.T) {
+	dir, err := ioutil.TempDir("", "instance")
+	if err != nil {
+		t.Fatalf("Failed to create temp dir: %v", err)
+	}
+	defer os.RemoveAll(dir)
+	// Uninitialized state.
+	if transitionInstance(dir, starting, started) == nil {
+		t.Fatalf("transitionInstance should have failed")
+	}
+	// Initialize.
+	if err := initializeInstance(dir, suspending); err != nil {
+		t.Fatalf("initializeInstance failed: %v", err)
+	}
+	if err := transitionInstance(dir, suspending, suspended); err != nil {
+		t.Fatalf("transitionInstance failed: %v", err)
+	}
+	// Invalid transition: wrong initial state.
+	if transitionInstance(dir, suspending, suspended) == nil {
+		t.Fatalf("transitionInstance should have failed")
+	}
+	if err := transitionInstance(dir, suspended, stopped); err != nil {
+		t.Fatalf("transitionInstance failed: %v", err)
+	}
+}