blob: 4c3bb254204e1e469fcd33b81a76854326832b92 [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main_test
import (
"bytes"
"fmt"
"io"
"math/rand"
"runtime"
"strings"
"sync"
"testing"
"time"
"v.io/v23"
"v.io/v23/context"
"v.io/v23/naming"
"v.io/v23/services/device"
"v.io/x/lib/cmdline"
cmd_device "v.io/x/ref/services/device/device"
"v.io/x/ref/services/internal/servicetest"
"v.io/x/ref/test"
)
func simplePrintHandler(entry cmd_device.GlobResult, _ *context.T, stdout, _ io.Writer) error {
fmt.Fprintf(stdout, "%v\n", entry)
return nil
}
func errOnInstallationsHandler(entry cmd_device.GlobResult, _ *context.T, stdout, _ io.Writer) error {
if entry.Kind == cmd_device.ApplicationInstallationObject {
return fmt.Errorf("handler complete failure")
}
fmt.Fprintf(stdout, "%v\n", entry)
return nil
}
// newEnforceFullParallelismHandler returns a handler that should be invoked in
// parallel n times.
func newEnforceFullParallelismHandler(t *testing.T, n int) cmd_device.GlobHandler {
// The WaitGroup is used to verify parallel execution: each run of the
// handler decrements the counter, and then waits for the counter to
// reach zero. If any of the runs of the handler were sequential, the
// counter would never reach zero since a deadlock would ensue. A
// timeout protects against the deadlock to ensure the test fails fast.
var wg sync.WaitGroup
wg.Add(n)
return func(entry cmd_device.GlobResult, ctx *context.T, stdout, stderr io.Writer) error {
wg.Done()
simplePrintHandler(entry, ctx, stdout, stderr)
waitDoneCh := make(chan struct{})
go func() {
wg.Wait()
close(waitDoneCh)
}()
select {
case <-waitDoneCh:
case <-time.After(5 * time.Second):
t.Errorf("Timed out waiting for WaitGroup. Potential parallelism issue.")
}
return nil
}
}
// maybeGosched flips a coin to decide where to call Gosched. It's used to
// shuffle up the order of handler execution a bit (to prevent the goroutines
// spawned by the glob library from always executing in a fixed order, e.g. the
// order in which they're created).
func maybeGosched() {
if rand.Intn(2) == 0 {
runtime.Gosched()
}
}
// newEnforceKindParallelismHandler returns a handler that should be invoked
// nInstallations times in parallel for installations, then nInstances times in
// parallel for instances, then nDevices times in parallel for device service
// objects.
func newEnforceKindParallelismHandler(t *testing.T, nInstallations, nInstances, nDevices int) cmd_device.GlobHandler {
// Each of these handlers ensures parallelism within each kind
// (installations, instances, device objects).
hInstallations := newEnforceFullParallelismHandler(t, nInstallations)
hInstances := newEnforceFullParallelismHandler(t, nInstances)
hDevices := newEnforceFullParallelismHandler(t, nDevices)
// These channels are used to verify that all installation handlers must
// execute before all instance handlers, which in turn must execute
// before all device handlers.
instancesCh := make(chan struct{}, nInstances)
devicesCh := make(chan struct{}, nDevices)
return func(entry cmd_device.GlobResult, ctx *context.T, stdout, stderr io.Writer) error {
maybeGosched()
switch entry.Kind {
case cmd_device.ApplicationInstallationObject:
select {
case <-instancesCh:
t.Errorf("Instance before installation")
case <-devicesCh:
t.Errorf("Device before installation")
default:
}
return hInstallations(entry, ctx, stdout, stderr)
case cmd_device.ApplicationInstanceObject:
select {
case <-devicesCh:
t.Errorf("Device before instance")
default:
}
instancesCh <- struct{}{}
return hInstances(entry, ctx, stdout, stderr)
case cmd_device.DeviceServiceObject:
devicesCh <- struct{}{}
return hDevices(entry, ctx, stdout, stderr)
}
t.Errorf("Unknown entry: %v", entry.Kind)
return nil
}
}
// newEnforceNoParallelismHandler returns a handler meant to be invoked sequentially
// for each of the suffixes contained in the expected slice.
func newEnforceNoParallelismHandler(t *testing.T, n int, expected []string) cmd_device.GlobHandler {
if n != len(expected) {
t.Errorf("Test invariant broken: %d != %d", n, len(expected))
}
orderedSuffixes := make(chan string, n)
for _, e := range expected {
orderedSuffixes <- e
}
return func(entry cmd_device.GlobResult, ctx *context.T, stdout, stderr io.Writer) error {
maybeGosched()
_, suffix := naming.SplitAddressName(entry.Name)
expect := <-orderedSuffixes
if suffix != expect {
t.Errorf("Expected %s, got %s", expect, suffix)
}
simplePrintHandler(entry, ctx, stdout, stderr)
return nil
}
}
// TestObjectKindInvariant ensures that the object kind enum and list are in
// sync and have not been inadvertently updated independently of each other.
func TestObjectKindInvariant(t *testing.T) {
if len(cmd_device.ObjectKinds) != int(cmd_device.SentinelObjectKind) {
t.Errorf("Broken invariant: mismatching number of object kinds")
}
}
// TestGlob tests the internals of the globbing support for the device tool.
func TestGlob(t *testing.T) {
ctx, shutdown := test.V23Init()
defer shutdown()
tapes := servicetest.NewTapeMap()
rootTape := tapes.ForSuffix("")
ctx, server, err := v23.WithNewDispatchingServer(ctx, "", newDispatcher(t, tapes))
if err != nil {
t.Fatalf("NewServer failed: %v", err)
}
endpoint := server.Status().Endpoints[0]
appName := naming.JoinAddressName(endpoint.String(), "app")
allGlobArgs := []string{"glob1", "glob2"}
allGlobResponses := []GlobResponse{
{results: []string{"app/3", "app/4", "app/6", "app/5", "app/9", "app/7"}},
{results: []string{"app/2", "app/1", "app/8"}},
}
allStatusResponses := map[string][]interface{}{
"app/1": []interface{}{instanceRunning},
"app/2": []interface{}{installationUninstalled},
"app/3": []interface{}{instanceUpdating},
"app/4": []interface{}{installationActive},
"app/5": []interface{}{instanceNotRunning},
"app/6": []interface{}{deviceService},
"app/7": []interface{}{installationActive},
"app/8": []interface{}{deviceUpdating},
"app/9": []interface{}{instanceUpdating},
}
outLine := func(suffix string, s device.Status) string {
r, err := cmd_device.NewGlobResult(appName+"/"+suffix, s)
if err != nil {
t.Errorf("NewGlobResult failed: %v", err)
return ""
}
return fmt.Sprintf("%v", *r)
}
var (
runningIstc1Out = outLine("1", instanceRunning)
uninstalledIstl2Out = outLine("2", installationUninstalled)
updatingIstc3Out = outLine("3", instanceUpdating)
activeIstl4Out = outLine("4", installationActive)
notRunningIstc5Out = outLine("5", instanceNotRunning)
devc6Out = outLine("6", deviceService)
activeIstl7Out = outLine("7", installationActive)
updatingDevc8Out = outLine("8", deviceUpdating)
updatingIstc9Out = outLine("9", instanceUpdating)
)
noParallelismHandler := newEnforceNoParallelismHandler(t, len(allStatusResponses), []string{"app/2", "app/4", "app/7", "app/1", "app/3", "app/5", "app/9", "app/6", "app/8"})
for _, c := range []struct {
handler cmd_device.GlobHandler
globResponses []GlobResponse
statusResponses map[string][]interface{}
gs cmd_device.GlobSettings
globPatterns []string
expectedStdout string
expectedStderr string
expectedError string
}{
// Verifies output is correct and in the expected order (first
// installations, then instances, then device services).
{
simplePrintHandler,
allGlobResponses,
allStatusResponses,
cmd_device.GlobSettings{},
allGlobArgs,
joinLines(uninstalledIstl2Out, activeIstl4Out, activeIstl7Out, runningIstc1Out, updatingIstc3Out, notRunningIstc5Out, updatingIstc9Out, devc6Out, updatingDevc8Out),
"",
"",
},
// Verifies that full parallelism runs all the handlers
// simultaneously.
{
newEnforceFullParallelismHandler(t, len(allStatusResponses)),
allGlobResponses,
allStatusResponses,
cmd_device.GlobSettings{HandlerParallelism: cmd_device.FullParallelism},
allGlobArgs,
joinLines(uninstalledIstl2Out, activeIstl4Out, activeIstl7Out, runningIstc1Out, updatingIstc3Out, notRunningIstc5Out, updatingIstc9Out, devc6Out, updatingDevc8Out),
"",
"",
},
// Verifies that "by kind" parallelism runs all installation
// handlers in parallel, then all instance handlers in parallel,
// then all device service handlers in parallel.
{
newEnforceKindParallelismHandler(t, 3, 4, 2),
allGlobResponses,
allStatusResponses,
cmd_device.GlobSettings{HandlerParallelism: cmd_device.KindParallelism},
allGlobArgs,
joinLines(uninstalledIstl2Out, activeIstl4Out, activeIstl7Out, runningIstc1Out, updatingIstc3Out, notRunningIstc5Out, updatingIstc9Out, devc6Out, updatingDevc8Out),
"",
"",
},
// Verifies that the no parallelism option runs all handlers
// sequentially.
{
noParallelismHandler,
allGlobResponses,
allStatusResponses,
cmd_device.GlobSettings{HandlerParallelism: cmd_device.NoParallelism},
allGlobArgs,
joinLines(uninstalledIstl2Out, activeIstl4Out, activeIstl7Out, runningIstc1Out, updatingIstc3Out, notRunningIstc5Out, updatingIstc9Out, devc6Out, updatingDevc8Out),
"",
"",
},
// Verifies "only instances" filter.
{
simplePrintHandler,
allGlobResponses,
allStatusResponses,
cmd_device.GlobSettings{OnlyInstances: true},
allGlobArgs,
joinLines(runningIstc1Out, updatingIstc3Out, notRunningIstc5Out, updatingIstc9Out),
"",
"",
},
// Verifies "only installations" filter.
{
simplePrintHandler,
allGlobResponses,
allStatusResponses,
cmd_device.GlobSettings{OnlyInstallations: true},
allGlobArgs,
joinLines(uninstalledIstl2Out, activeIstl4Out, activeIstl7Out),
"",
"",
},
// Verifies "instance state" filter.
{
simplePrintHandler,
allGlobResponses,
allStatusResponses,
cmd_device.GlobSettings{InstanceStateFilter: cmd_device.InstanceStates(device.InstanceStateUpdating)},
allGlobArgs,
joinLines(uninstalledIstl2Out, activeIstl4Out, activeIstl7Out, updatingIstc3Out, updatingIstc9Out, devc6Out, updatingDevc8Out),
"",
"",
},
// Verifies "instance state" filter with more than 1 state.
{
simplePrintHandler,
allGlobResponses,
allStatusResponses,
cmd_device.GlobSettings{InstanceStateFilter: cmd_device.InstanceStates(device.InstanceStateUpdating, device.InstanceStateRunning)},
allGlobArgs,
joinLines(uninstalledIstl2Out, activeIstl4Out, activeIstl7Out, runningIstc1Out, updatingIstc3Out, updatingIstc9Out, devc6Out, updatingDevc8Out),
"",
"",
},
// Verifies "instance state" filter with excluded state.
{
simplePrintHandler,
allGlobResponses,
allStatusResponses,
cmd_device.GlobSettings{InstanceStateFilter: cmd_device.ExcludeInstanceStates(device.InstanceStateUpdating)},
allGlobArgs,
joinLines(uninstalledIstl2Out, activeIstl4Out, activeIstl7Out, runningIstc1Out, notRunningIstc5Out, devc6Out, updatingDevc8Out),
"",
"",
},
// Verifies "instance state" filter with more than 1 excluded state.
{
simplePrintHandler,
allGlobResponses,
allStatusResponses,
cmd_device.GlobSettings{InstanceStateFilter: cmd_device.ExcludeInstanceStates(device.InstanceStateUpdating, device.InstanceStateRunning)},
allGlobArgs,
joinLines(uninstalledIstl2Out, activeIstl4Out, activeIstl7Out, notRunningIstc5Out, devc6Out, updatingDevc8Out),
"",
"",
},
// Verifies "installation state" filter.
{
simplePrintHandler,
allGlobResponses,
allStatusResponses,
cmd_device.GlobSettings{InstallationStateFilter: cmd_device.InstallationStates(device.InstallationStateActive)},
allGlobArgs,
joinLines(activeIstl4Out, activeIstl7Out, runningIstc1Out, updatingIstc3Out, notRunningIstc5Out, updatingIstc9Out, devc6Out, updatingDevc8Out),
"",
"",
},
// Verifies "installation state" filter with more than 1 state.
{
simplePrintHandler,
allGlobResponses,
allStatusResponses,
cmd_device.GlobSettings{InstallationStateFilter: cmd_device.InstallationStates(device.InstallationStateActive, device.InstallationStateUninstalled)},
allGlobArgs,
joinLines(uninstalledIstl2Out, activeIstl4Out, activeIstl7Out, runningIstc1Out, updatingIstc3Out, notRunningIstc5Out, updatingIstc9Out, devc6Out, updatingDevc8Out),
"",
"",
},
// Verifies "installation state" filter with excluded state.
{
simplePrintHandler,
allGlobResponses,
allStatusResponses,
cmd_device.GlobSettings{InstallationStateFilter: cmd_device.ExcludeInstallationStates(device.InstallationStateActive)},
allGlobArgs,
joinLines(uninstalledIstl2Out, runningIstc1Out, updatingIstc3Out, notRunningIstc5Out, updatingIstc9Out, devc6Out, updatingDevc8Out),
"",
"",
},
// Verifies "installation state" filter with more than 1 excluded state.
{
simplePrintHandler,
allGlobResponses,
allStatusResponses,
cmd_device.GlobSettings{InstallationStateFilter: cmd_device.ExcludeInstallationStates(device.InstallationStateActive, device.InstallationStateUninstalled)},
allGlobArgs,
joinLines(runningIstc1Out, updatingIstc3Out, notRunningIstc5Out, updatingIstc9Out, devc6Out, updatingDevc8Out),
"",
"",
},
// Verifies "installation state" filter + "only installations" filter.
{
simplePrintHandler,
allGlobResponses,
allStatusResponses,
cmd_device.GlobSettings{
InstallationStateFilter: cmd_device.InstallationStates(device.InstallationStateActive),
OnlyInstallations: true,
},
allGlobArgs,
joinLines(activeIstl4Out, activeIstl7Out),
"",
"",
},
// Verifies "installation state" filter + "only instances" filter.
{
simplePrintHandler,
allGlobResponses,
allStatusResponses,
cmd_device.GlobSettings{
InstallationStateFilter: cmd_device.InstallationStates(device.InstallationStateActive),
OnlyInstances: true,
},
allGlobArgs,
joinLines(runningIstc1Out, updatingIstc3Out, notRunningIstc5Out, updatingIstc9Out),
"",
"",
},
// Verifies "installation state" filter + "instance state" filter.
{
simplePrintHandler,
allGlobResponses,
allStatusResponses,
cmd_device.GlobSettings{
InstanceStateFilter: cmd_device.InstanceStates(device.InstanceStateRunning),
InstallationStateFilter: cmd_device.InstallationStates(device.InstallationStateUninstalled),
},
allGlobArgs,
joinLines(uninstalledIstl2Out, runningIstc1Out, devc6Out, updatingDevc8Out),
"",
"",
},
// Verifies "only instances" filter + "only installations" filter -- no results.
{
simplePrintHandler,
allGlobResponses,
allStatusResponses,
cmd_device.GlobSettings{
OnlyInstallations: true,
OnlyInstances: true,
},
allGlobArgs,
"",
"",
"no objects found",
},
// No glob arguments.
{
simplePrintHandler,
allGlobResponses,
allStatusResponses,
cmd_device.GlobSettings{},
[]string{},
"",
"",
"no objects found",
},
// No glob results.
{
simplePrintHandler,
make([]GlobResponse, 2),
allStatusResponses,
cmd_device.GlobSettings{},
allGlobArgs,
"",
"",
"no objects found",
},
// Error in glob.
{
simplePrintHandler,
[]GlobResponse{{results: []string{"app/3", "app/4"}}, {err: fmt.Errorf("glob utter failure")}},
allStatusResponses,
cmd_device.GlobSettings{},
[]string{"glob", "glob"},
joinLines(activeIstl4Out, updatingIstc3Out),
fmt.Sprintf("Glob(%v) returned an error for %v: device.test:\"\".__Glob: Internal error: glob utter failure", naming.JoinAddressName(endpoint.String(), "glob"), naming.JoinAddressName(endpoint.String(), "")),
"",
},
// Error in status.
{
simplePrintHandler,
[]GlobResponse{{results: []string{"app/4", "app/3"}}, {results: []string{"app/1", "app/2"}}},
map[string][]interface{}{
"app/1": []interface{}{instanceRunning},
"app/2": []interface{}{fmt.Errorf("status miserable failure")},
"app/3": []interface{}{instanceUpdating},
"app/4": []interface{}{installationActive},
},
cmd_device.GlobSettings{},
allGlobArgs,
joinLines(activeIstl4Out, runningIstc1Out, updatingIstc3Out),
fmt.Sprintf("Status(%v) failed: device.test:<rpc.Client>\"%v\".Status: Error: status miserable failure", appName+"/2", appName+"/2"),
"",
},
// Error in handler.
{
errOnInstallationsHandler,
[]GlobResponse{{results: []string{"app/4", "app/3"}}, {results: []string{"app/1", "app/2"}}},
map[string][]interface{}{
"app/1": []interface{}{instanceRunning},
"app/2": []interface{}{installationUninstalled},
"app/3": []interface{}{instanceUpdating},
"app/4": []interface{}{installationActive},
},
cmd_device.GlobSettings{},
allGlobArgs,
joinLines(runningIstc1Out, updatingIstc3Out),
joinLines(
fmt.Sprintf("ERROR for \"%v\": handler complete failure.", appName+"/2"),
fmt.Sprintf("ERROR for \"%v\": handler complete failure.", appName+"/4")),
"encountered a total of 2 error(s)",
},
} {
tapes.Rewind()
var rootTapeResponses []interface{}
for _, r := range c.globResponses {
rootTapeResponses = append(rootTapeResponses, r)
}
rootTape.SetResponses(rootTapeResponses...)
for n, r := range c.statusResponses {
tapes.ForSuffix(n).SetResponses(r...)
}
var stdout, stderr bytes.Buffer
env := &cmdline.Env{Stdout: &stdout, Stderr: &stderr}
var args []string
for _, p := range c.globPatterns {
args = append(args, naming.JoinAddressName(endpoint.String(), p))
}
err := cmd_device.Run(ctx, env, args, c.handler, c.gs)
if err != nil {
if expected, got := c.expectedError, err.Error(); expected != got {
t.Errorf("Unexpected error. Got: %v. Expected: %v.", got, expected)
}
} else if c.expectedError != "" {
t.Errorf("Expected an error (%v) but got none.", c.expectedError)
}
if expected, got := c.expectedStdout, strings.TrimSpace(stdout.String()); got != expected {
t.Errorf("Unexpected stdout. Got:\n%v\nExpected:\n%v\n", got, expected)
}
if expected, got := c.expectedStderr, strings.TrimSpace(stderr.String()); got != expected {
t.Errorf("Unexpected stderr. Got:\n%v\nExpected:\n%v\n", got, expected)
}
}
}