blob: 2b067a3b3974260d28d36338cef8aedc94592dab [file] [log] [blame]
package store
import (
"context"
"fmt"
"os"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.skia.org/infra/go/testutils/unittest"
"go.skia.org/infra/machine/go/machine"
"go.skia.org/infra/machine/go/machineserver/config"
)
func TestMachineToStoreDescription_NoDimensions(t *testing.T) {
unittest.SmallTest(t)
d := machine.NewDescription()
m := machineDescriptionToStoreDescription(d)
assert.Equal(t, storeDescription{
Mode: d.Mode,
LastUpdated: d.LastUpdated,
MachineDescription: d,
}, m)
}
func TestMachineToStoreDescription_WithDimensions(t *testing.T) {
unittest.SmallTest(t)
d := machine.NewDescription()
d.Dimensions[machine.DimOS] = []string{"Android"}
d.Dimensions[machine.DimDeviceType] = []string{"sailfish"}
d.Dimensions[machine.DimQuarantined] = []string{"Device sailfish too hot."}
m := machineDescriptionToStoreDescription(d)
assert.Equal(t, storeDescription{
OS: []string{"Android"},
DeviceType: []string{"sailfish"},
Quarantined: []string{"Device sailfish too hot."},
Mode: d.Mode,
LastUpdated: d.LastUpdated,
MachineDescription: d,
}, m)
}
func TestMachineToStoreDescription_WithPowerCycle(t *testing.T) {
unittest.SmallTest(t)
d := machine.NewDescription()
d.Dimensions[machine.DimOS] = []string{"Android"}
d.PowerCycle = true
m := machineDescriptionToStoreDescription(d)
assert.Equal(t, storeDescription{
OS: []string{"Android"},
Mode: d.Mode,
LastUpdated: d.LastUpdated,
MachineDescription: d,
PowerCycle: true,
}, m)
}
func setupForTest(t *testing.T) (context.Context, config.InstanceConfig) {
require.NotEmpty(t, os.Getenv("FIRESTORE_EMULATOR_HOST"), "This test requires the firestore emulator.")
cfg := config.InstanceConfig{
Store: config.Store{
Project: "test-project",
Instance: fmt.Sprintf("test-%s", uuid.New()),
},
}
ctx := context.Background()
return ctx, cfg
}
func TestNew(t *testing.T) {
unittest.LargeTest(t)
ctx, cfg := setupForTest(t)
_, err := New(ctx, true, cfg)
require.NoError(t, err)
}
func TestUpdate_CanUpdateEvenIfDescriptionDoesntExist(t *testing.T) {
unittest.LargeTest(t)
ctx, cfg := setupForTest(t)
store, err := New(ctx, true, cfg)
require.NoError(t, err)
called := false
err = store.Update(ctx, "skia-rpi2-rack2-shelf1-001", func(previous machine.Description) machine.Description {
assert.Equal(t, machine.ModeAvailable, previous.Mode)
ret := previous.Copy()
ret.Mode = machine.ModeMaintenance
called = true
return ret
})
require.NoError(t, err)
assert.True(t, called)
assert.Equal(t, int64(1), store.updateCounter.Get())
assert.Equal(t, int64(0), store.updateDataToErrorCounter.Get())
snap, err := store.machinesCollection.Doc("skia-rpi2-rack2-shelf1-001").Get(ctx)
require.NoError(t, err)
var storedDescription machine.Description
err = snap.DataTo(&storedDescription)
require.NoError(t, err)
assert.Equal(t, machine.ModeMaintenance, storedDescription.Mode)
assert.NoError(t, store.firestoreClient.Close())
}
func TestUpdate_CanUpdateIfDescriptionExists(t *testing.T) {
unittest.LargeTest(t)
ctx, cfg := setupForTest(t)
store, err := New(ctx, true, cfg)
require.NoError(t, err)
// First write a Description.
err = store.Update(ctx, "skia-rpi2-rack2-shelf1-001", func(previous machine.Description) machine.Description {
ret := previous.Copy()
ret.Mode = machine.ModeMaintenance
ret.Dimensions[machine.DimOS] = []string{"Android"}
return ret
})
require.NoError(t, err)
// Now confirm we get the Description we previously wrote on the next update.
err = store.Update(ctx, "skia-rpi2-rack2-shelf1-001", func(previous machine.Description) machine.Description {
assert.Equal(t, machine.ModeMaintenance, previous.Mode)
assert.Equal(t, []string{"Android"}, previous.Dimensions["os"])
assert.Empty(t, previous.Dimensions[machine.DimDeviceType])
ret := previous.Copy()
ret.Mode = machine.ModeAvailable
return ret
})
require.NoError(t, err)
assert.NoError(t, store.firestoreClient.Close())
}
func TestWatch_StartWatchBeforeMachineExists(t *testing.T) {
unittest.LargeTest(t)
ctx, cfg := setupForTest(t)
store, err := New(ctx, true, cfg)
require.NoError(t, err)
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// First add the watch.
ch := store.Watch(ctx, "skia-rpi2-rack2-shelf1-001")
// Then create the document.
err = store.Update(ctx, "skia-rpi2-rack2-shelf1-001", func(previous machine.Description) machine.Description {
ret := previous.Copy()
ret.Mode = machine.ModeMaintenance
return ret
})
require.NoError(t, err)
// Wait for first description.
m := <-ch
assert.Equal(t, machine.ModeMaintenance, m.Mode)
assert.Equal(t, int64(1), store.watchReceiveSnapshotCounter.Get())
assert.Equal(t, int64(0), store.watchDataToErrorCounter.Get())
assert.NoError(t, store.firestoreClient.Close())
}
func TestWatch_StartWatchAfterMachineExists(t *testing.T) {
unittest.LargeTest(t)
ctx, cfg := setupForTest(t)
store, err := New(ctx, true, cfg)
require.NoError(t, err)
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// First create the document.
err = store.Update(ctx, "skia-rpi2-rack2-shelf1-001", func(previous machine.Description) machine.Description {
ret := previous.Copy()
ret.Mode = machine.ModeMaintenance
ret.Dimensions[machine.DimOS] = []string{"Android"}
ret.Annotation.Message = "Hello World!"
return ret
})
require.NoError(t, err)
// Then add the watch.
ch := store.Watch(ctx, "skia-rpi2-rack2-shelf1-001")
// Wait for first description.
m := <-ch
assert.Equal(t, machine.ModeMaintenance, m.Mode)
assert.Equal(t, machine.SwarmingDimensions{
machine.DimID: {"skia-rpi2-rack2-shelf1-001"},
machine.DimOS: {"Android"},
}, m.Dimensions)
assert.Equal(t, "Hello World!", m.Annotation.Message)
assert.NoError(t, store.firestoreClient.Close())
}
func TestWatch_IsCancellable(t *testing.T) {
unittest.LargeTest(t)
ctx, cfg := setupForTest(t)
store, err := New(ctx, true, cfg)
require.NoError(t, err)
ctx, cancel := context.WithCancel(ctx)
// First create the document.
err = store.Update(ctx, "skia-rpi2-rack2-shelf1-001", func(previous machine.Description) machine.Description {
ret := previous.Copy()
ret.Mode = machine.ModeMaintenance
return ret
})
require.NoError(t, err)
// Then add the watch.
ch := store.Watch(ctx, "skia-rpi2-rack2-shelf1-001")
cancel()
// The test passes if we get past this loop since that means the channel was closed.
for range ch {
}
assert.NoError(t, store.firestoreClient.Close())
}
func TestList_Success(t *testing.T) {
unittest.LargeTest(t)
ctx, cfg := setupForTest(t)
store, err := New(ctx, true, cfg)
require.NoError(t, err)
// List on an empty collection is OK.
descriptions, err := store.List(ctx)
require.NoError(t, err)
assert.Len(t, descriptions, 0)
// Add a single description.
err = store.Update(ctx, "skia-rpi2-rack2-shelf1-001", func(previous machine.Description) machine.Description {
assert.Equal(t, machine.ModeAvailable, previous.Mode)
ret := previous.Copy()
ret.Mode = machine.ModeMaintenance
ret.Dimensions["foo"] = []string{"bar", "baz"}
return ret
})
require.NoError(t, err)
// Confirm it appears in the list.
descriptions, err = store.List(ctx)
require.NoError(t, err)
assert.Len(t, descriptions, 1)
assert.Equal(t, machine.SwarmingDimensions{
"foo": {"bar", "baz"},
machine.DimID: {"skia-rpi2-rack2-shelf1-001"},
}, descriptions[0].Dimensions)
// Add a second description.
err = store.Update(ctx, "skia-rpi2-rack2-shelf1-002", func(previous machine.Description) machine.Description {
assert.Equal(t, machine.ModeAvailable, previous.Mode)
ret := previous.Copy()
ret.Mode = machine.ModeMaintenance
ret.Dimensions["foo"] = []string{"quux"}
return ret
})
// Confirm they both show up in the list.
descriptions, err = store.List(ctx)
require.NoError(t, err)
assert.Len(t, descriptions, 2)
}
func TestWatchForDeletablePods_Success(t *testing.T) {
unittest.LargeTest(t)
ctx, cfg := setupForTest(t)
store, err := New(ctx, true, cfg)
require.NoError(t, err)
ctx, cancel := context.WithCancel(ctx)
defer cancel()
defer store.watchForDeletablePodsDataToErrorCounter.Reset()
defer store.watchForDeletablePodsReceiveSnapshotCounter.Reset()
// First add the watch.
ch := store.WatchForDeletablePods(ctx)
const podName = "rpi-swarming-123456-987"
// Then create the document.
err = store.Update(ctx, "skia-rpi2-rack2-shelf1-001", func(previous machine.Description) machine.Description {
ret := previous.Copy()
ret.Mode = machine.ModeMaintenance
ret.PodName = podName
ret.ScheduledForDeletion = podName
ret.RunningSwarmingTask = false
return ret
})
require.NoError(t, err)
// Wait for first pod name.
m := <-ch
assert.Equal(t, podName, m)
assert.Equal(t, int64(1), store.watchForDeletablePodsReceiveSnapshotCounter.Get())
assert.Equal(t, int64(0), store.watchForDeletablePodsDataToErrorCounter.Get())
assert.NoError(t, store.firestoreClient.Close())
}
func TestWatchForDeletablePods_OnlyMatchesTheRightMachines(t *testing.T) {
unittest.LargeTest(t)
ctx, cfg := setupForTest(t)
store, err := New(ctx, true, cfg)
require.NoError(t, err)
ctx, cancel := context.WithCancel(ctx)
defer cancel()
defer store.watchForDeletablePodsDataToErrorCounter.Reset()
defer store.watchForDeletablePodsReceiveSnapshotCounter.Reset()
// First add the watch.
ch := store.WatchForDeletablePods(ctx)
const podName = "rpi-swarming-123456-987"
// Add some changes that should not match the query.
err = store.Update(ctx, "skia-rpi2-rack4-shelf2-001", func(previous machine.Description) machine.Description {
ret := previous.Copy()
ret.Mode = machine.ModeMaintenance
ret.PodName = podName + "-should-not-match"
ret.ScheduledForDeletion = ""
ret.RunningSwarmingTask = false
return ret
})
require.NoError(t, err)
err = store.Update(ctx, "skia-rpi2-rack4-shelf2-001", func(previous machine.Description) machine.Description {
ret := previous.Copy()
ret.Mode = machine.ModeMaintenance
ret.PodName = podName + "-should-not-match"
ret.ScheduledForDeletion = podName
ret.RunningSwarmingTask = true
return ret
})
require.NoError(t, err)
// Then create a machine with a deletable pod.
err = store.Update(ctx, "skia-rpi2-rack2-shelf1-001", func(previous machine.Description) machine.Description {
ret := previous.Copy()
ret.Mode = machine.ModeMaintenance
ret.PodName = podName
ret.ScheduledForDeletion = podName
ret.RunningSwarmingTask = false
return ret
})
require.NoError(t, err)
// Wait for first pod name.
m := <-ch
assert.Equal(t, podName, m)
assert.Equal(t, int64(1), store.watchForDeletablePodsReceiveSnapshotCounter.Get())
assert.Equal(t, int64(0), store.watchForDeletablePodsDataToErrorCounter.Get())
assert.NoError(t, store.firestoreClient.Close())
}
func TestWatchForDeletablePods_IsCancellable(t *testing.T) {
unittest.LargeTest(t)
ctx, cfg := setupForTest(t)
store, err := New(ctx, true, cfg)
require.NoError(t, err)
ctx, cancel := context.WithCancel(ctx)
defer store.watchForDeletablePodsDataToErrorCounter.Reset()
defer store.watchForDeletablePodsReceiveSnapshotCounter.Reset()
// First add the watch.
ch := store.WatchForDeletablePods(ctx)
const podName = "rpi-swarming-123456-987"
// Then create the document.
err = store.Update(ctx, "skia-rpi2-rack2-shelf1-001", func(previous machine.Description) machine.Description {
ret := previous.Copy()
ret.Mode = machine.ModeMaintenance
ret.PodName = podName
ret.ScheduledForDeletion = podName
ret.RunningSwarmingTask = false
return ret
})
require.NoError(t, err)
cancel()
// The test passes if we get past this loop since that means the channel was closed.
for range ch {
}
assert.NoError(t, store.firestoreClient.Close())
}
func TestWatchForPowerCycle_Success(t *testing.T) {
unittest.LargeTest(t)
ctx, cfg := setupForTest(t)
store, err := New(ctx, true, cfg)
require.NoError(t, err)
ctx, cancel := context.WithCancel(ctx)
defer cancel()
defer store.watchForPowerCycleDataToErrorCounter.Reset()
defer store.watchForPowerCycleReceiveSnapshotCounter.Reset()
// First add the watch.
ch := store.WatchForPowerCycle(ctx)
const machineName = "skia-rpi2-rack2-shelf1-001"
// Then create the document.
err = store.Update(ctx, machineName, func(previous machine.Description) machine.Description {
ret := previous.Copy()
ret.Mode = machine.ModeMaintenance
ret.RunningSwarmingTask = false
ret.PowerCycle = true
return ret
})
require.NoError(t, err)
// Wait for first pod name.
m := <-ch
assert.Equal(t, machineName, m)
assert.Equal(t, int64(1), store.watchForPowerCycleReceiveSnapshotCounter.Get())
assert.Equal(t, int64(0), store.watchForPowerCycleDataToErrorCounter.Get())
assert.NoError(t, store.firestoreClient.Close())
}
func TestWatchForPowerCycle_OnlyMatchesTheRightMachines(t *testing.T) {
unittest.LargeTest(t)
ctx, cfg := setupForTest(t)
store, err := New(ctx, true, cfg)
require.NoError(t, err)
ctx, cancel := context.WithCancel(ctx)
defer cancel()
defer store.watchForPowerCycleDataToErrorCounter.Reset()
defer store.watchForPowerCycleReceiveSnapshotCounter.Reset()
// First add the watch.
ch := store.WatchForPowerCycle(ctx)
// Add some machines that don't match the query.
err = store.Update(ctx, "skia-rpi2-rack4-shelf2-013", func(previous machine.Description) machine.Description {
ret := previous.Copy()
ret.Mode = machine.ModeMaintenance
ret.RunningSwarmingTask = true
ret.PowerCycle = true
return ret
})
require.NoError(t, err)
err = store.Update(ctx, "skia-rpi2-rack4-shelf1-021", func(previous machine.Description) machine.Description {
ret := previous.Copy()
ret.Mode = machine.ModeMaintenance
ret.RunningSwarmingTask = false
ret.PowerCycle = false
return ret
})
require.NoError(t, err)
// Now add a machine that will match the query.
const machineName = "skia-rpi2-rack2-shelf1-001"
// Then create the document.
err = store.Update(ctx, machineName, func(previous machine.Description) machine.Description {
ret := previous.Copy()
ret.Mode = machine.ModeMaintenance
ret.RunningSwarmingTask = false
ret.PowerCycle = true
return ret
})
require.NoError(t, err)
// Wait for first pod name.
m := <-ch
assert.Equal(t, machineName, m)
assert.Equal(t, int64(1), store.watchForPowerCycleReceiveSnapshotCounter.Get())
assert.Equal(t, int64(0), store.watchForPowerCycleDataToErrorCounter.Get())
assert.NoError(t, store.firestoreClient.Close())
}
func TestWatchForPowerCycle_ISCancellable(t *testing.T) {
unittest.LargeTest(t)
ctx, cfg := setupForTest(t)
store, err := New(ctx, true, cfg)
require.NoError(t, err)
ctx, cancel := context.WithCancel(ctx)
// First add the watch.
ch := store.WatchForPowerCycle(ctx)
const machineName = "skia-rpi2-rack2-shelf1-001"
// Then create the document.
err = store.Update(ctx, machineName, func(previous machine.Description) machine.Description {
ret := previous.Copy()
ret.Mode = machine.ModeMaintenance
ret.RunningSwarmingTask = false
ret.PowerCycle = true
return ret
})
require.NoError(t, err)
cancel()
// The test passes if we get past this loop since that means the channel was closed.
for range ch {
}
assert.NoError(t, store.firestoreClient.Close())
}