Skip to content

Commit

Permalink
add test fixtures (GoogleCloudPlatform#100)
Browse files Browse the repository at this point in the history
* add test fixtures

* PR feedback
  • Loading branch information
hopkiw authored Apr 15, 2021
1 parent b958692 commit 1815ed5
Show file tree
Hide file tree
Showing 3 changed files with 500 additions and 11 deletions.
122 changes: 122 additions & 0 deletions imagetest/fixtures.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// Copyright 2021 Google LLC
//
// 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 imagetest

import (
"fmt"

"github.com/GoogleCloudPlatform/compute-image-tools/daisy"
)

const (
createVMsStepName = "create-vms"
createDisksStepName = "create-disks"
successMatch = "FINISHED-TEST"
)

// TestVM is a test VM.
type TestVM struct {
name string
testWorkflow *TestWorkflow
instance *daisy.Instance
}

// AddMetadata adds the specified key:value pair to metadata during VM creation.
func (t *TestVM) AddMetadata(key, value string) {
createVMStep := t.testWorkflow.wf.Steps[createVMsStepName]
for _, vm := range createVMStep.CreateInstances.Instances {
if vm.Name == t.name {
if vm.Metadata == nil {
vm.Metadata = make(map[string]string)
}
vm.Metadata[key] = value
return
}
}
}

// RunTests runs only the named tests on the testVM.
//
// From go help test:
// -run regexp
// Run only those tests and examples matching the regular expression.
// For tests, the regular expression is split by unbracketed slash (/)
// characters into a sequence of regular expressions, and each part
// of a test's identifier must match the corresponding element in
// the sequence, if any. Note that possible parents of matches are
// run too, so that -run=X/Y matches and runs and reports the result
// of all tests matching X, even those without sub-tests matching Y,
// because it must run them to look for those sub-tests.
func (t *TestVM) RunTests(runtest string) {
t.AddMetadata("_test_run", runtest)
}

// SetShutdownScript sets the `shutdown-script` metadata key for a VM.
func (t *TestVM) SetShutdownScript(script string) {
t.AddMetadata("shutdown-script", script)
}

// SetStartupScript sets the `startup-script` metadata key for a VM.
func (t *TestVM) SetStartupScript(script string) {
t.AddMetadata("startup-script", script)
}

// Reboot stops the VM, waits for it to shutdown, then starts it again. Your
// test package must handle being run twice.
func (t *TestVM) Reboot() error {
// Grab the wait step that was added with CreateTestVM.
waitStep, ok := t.testWorkflow.wf.Steps["wait-"+t.name]
if !ok {
return fmt.Errorf("wait-%s step missing", t.name)
}

stopInstancesStep, err := t.testWorkflow.addStopStep(t.name, t.name)
if err != nil {
return err
}

if err := t.testWorkflow.wf.AddDependency(stopInstancesStep, waitStep); err != nil {
return err
}

waitStopStep, err := t.testWorkflow.addWaitStep("stopped-"+t.name, t.name, true)
if err != nil {
return err
}

if err := t.testWorkflow.wf.AddDependency(waitStopStep, stopInstancesStep); err != nil {
return err
}

startInstancesStep, err := t.testWorkflow.addStartStep(t.name, t.name)
if err != nil {
return err
}

if err := t.testWorkflow.wf.AddDependency(startInstancesStep, waitStopStep); err != nil {
return err
}

waitStartedStep, err := t.testWorkflow.addWaitStep("started-"+t.name, t.name, false)
if err != nil {
return err
}

if err := t.testWorkflow.wf.AddDependency(waitStartedStep, startInstancesStep); err != nil {
return err
}

return nil
}
177 changes: 167 additions & 10 deletions imagetest/testworkflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,17 @@ import (
"cloud.google.com/go/storage"
"github.com/GoogleCloudPlatform/compute-image-tools/daisy"
"github.com/GoogleCloudPlatform/guest-test-infra/imagetest/utils"
"google.golang.org/api/compute/v1"
)

var (
client *storage.Client
)

const (
testBinariesPath = "/out"
testWrapperPath = testBinariesPath + "/wrapper"
baseGCSPath = "gs://gcp-guest-test-outputs/cloud_image_tests/"
createVMsStepName = "create-vms"
testBinariesPath = "/out"
testWrapperPath = testBinariesPath + "/wrapper"
baseGCSPath = "gs://gcp-guest-test-outputs/cloud_image_tests/"
)

// TestWorkflow defines a test workflow which creates at least one test VM.
Expand All @@ -48,12 +48,169 @@ type TestWorkflow struct {
// ShortImage will be only the final component of Image, used for naming.
ShortImage string
// destination for workflow outputs in GCS.
destination string
gcsPath string
skipped bool
skippedMessage string
wf *daisy.Workflow
}

// SingleVMTest configures one VM running tests.
func SingleVMTest(t *TestWorkflow) error {
_, err := t.CreateTestVM("vm")
return err
}

// Skip marks a test workflow to be skipped.
func (t *TestWorkflow) Skip(message string) {
t.skipped = true
t.skippedMessage = message
}

// SkippedMessage returns the skip reason message for the workflow.
func (t *TestWorkflow) SkippedMessage() string {
return t.skippedMessage
}

// CreateTestVM creates the necessary steps to create a VM with the specified name to the workflow.
func (t *TestWorkflow) CreateTestVM(name string) (*TestVM, error) {
// TODO: more robust name validation.
name = strings.ReplaceAll(name, "_", "-")

createDisksStep, err := t.appendCreateDisksStep(name)
if err != nil {
return nil, err
}

// createDisksStep doesn't depend on any other steps.

createVMStep, err := t.appendCreateVMStep(name)
if err != nil {
return nil, err
}

if err := t.wf.AddDependency(createVMStep, createDisksStep); err != nil {
return nil, err
}

waitStep, err := t.addWaitStep(name, name, false)
if err != nil {
return nil, err
}

if err := t.wf.AddDependency(waitStep, createVMStep); err != nil {
return nil, err
}

return &TestVM{name: name, testWorkflow: t}, nil
}

func (t *TestWorkflow) appendCreateVMStep(name string) (*daisy.Step, error) {
attachedDisk := &compute.AttachedDisk{Source: name}

instance := &daisy.Instance{}
instance.StartupScript = "wrapper"
instance.Name = name
instance.Scopes = append(instance.Scopes, "https://www.googleapis.com/auth/devstorage.read_write")
instance.Disks = append(instance.Disks, attachedDisk)

instance.Metadata = make(map[string]string)
instance.Metadata["_test_vmname"] = name
instance.Metadata["_test_package_url"] = "${SOURCESPATH}/testpackage"
instance.Metadata["_test_results_url"] = fmt.Sprintf("${OUTSPATH}/%s.txt", name)

createInstances := &daisy.CreateInstances{}
createInstances.Instances = append(createInstances.Instances, instance)

createVMStep, ok := t.wf.Steps[createVMsStepName]
if ok {
// append to existing step.
createVMStep.CreateInstances.Instances = append(createVMStep.CreateInstances.Instances, instance)
} else {
var err error
createVMStep, err = t.wf.NewStep(createVMsStepName)
if err != nil {
return nil, err
}
createVMStep.CreateInstances = createInstances
}

return createVMStep, nil
}

func (t *TestWorkflow) appendCreateDisksStep(diskname string) (*daisy.Step, error) {
bootdisk := &daisy.Disk{}
bootdisk.Name = diskname
bootdisk.SourceImage = t.Image

createDisks := &daisy.CreateDisks{bootdisk}

createDisksStep, ok := t.wf.Steps[createDisksStepName]
if ok {
// append to existing step.
*createDisksStep.CreateDisks = append(*createDisksStep.CreateDisks, bootdisk)
} else {
var err error
createDisksStep, err = t.wf.NewStep(createDisksStepName)
if err != nil {
return nil, err
}
createDisksStep.CreateDisks = createDisks
}

return createDisksStep, nil
}

func (t *TestWorkflow) addWaitStep(stepname, vmname string, stopped bool) (*daisy.Step, error) {
serialOutput := &daisy.SerialOutput{}
serialOutput.Port = 1
serialOutput.SuccessMatch = successMatch

instanceSignal := &daisy.InstanceSignal{}
instanceSignal.Name = vmname
instanceSignal.Stopped = stopped

// Waiting for stop and waiting for success match are mutually exclusive.
if !stopped {
instanceSignal.SerialOutput = serialOutput
}

waitForInstances := &daisy.WaitForInstancesSignal{instanceSignal}

waitStep, err := t.wf.NewStep("wait-" + stepname)
if err != nil {
return nil, err
}
waitStep.WaitForInstancesSignal = waitForInstances

return waitStep, nil
}

func (t *TestWorkflow) addStopStep(stepname, vmname string) (*daisy.Step, error) {
stopInstances := &daisy.StopInstances{}
stopInstances.Instances = append(stopInstances.Instances, vmname)

stopInstancesStep, err := t.wf.NewStep("stop-" + stepname)
if err != nil {
return nil, err
}
stopInstancesStep.StopInstances = stopInstances

return stopInstancesStep, nil
}

func (t *TestWorkflow) addStartStep(stepname, vmname string) (*daisy.Step, error) {
startInstances := &daisy.StartInstances{}
startInstances.Instances = append(startInstances.Instances, vmname)

startInstancesStep, err := t.wf.NewStep("start-" + stepname)
if err != nil {
return nil, err
}
startInstancesStep.StartInstances = startInstances

return startInstancesStep, nil
}

// finalizeWorkflows adds the final necessary data to each workflow for it to
// be able to run, including the final copy-objects step.
func finalizeWorkflows(tests []*TestWorkflow, zone, project string) error {
Expand All @@ -63,8 +220,8 @@ func finalizeWorkflows(tests []*TestWorkflow, zone, project string) error {
return fmt.Errorf("found nil workflow in finalize")
}

ts.destination = fmt.Sprintf("%s/%s/%s/%s", baseGCSPath, run, ts.Name, ts.ShortImage)
ts.wf.GCSPath = ts.destination
ts.gcsPath = fmt.Sprintf("%s/%s/%s/%s", baseGCSPath, run, ts.Name, ts.ShortImage)
ts.wf.GCSPath = ts.gcsPath

ts.wf.DisableGCSLogging()
ts.wf.DisableCloudLogging()
Expand All @@ -76,10 +233,10 @@ func finalizeWorkflows(tests []*TestWorkflow, zone, project string) error {
ts.wf.Sources["wrapper"] = testWrapperPath
ts.wf.Sources["testpackage"] = fmt.Sprintf("%s/%s.test", testBinariesPath, ts.Name)

// add a final copy-objects step which copies the daisy-outs-path directory to ts.destination + /outs
// add a final copy-objects step which copies the daisy-outs-path directory to ts.gcsPath + /outs
copyGCSObject := daisy.CopyGCSObject{}
copyGCSObject.Source = "${OUTSPATH}/" // Trailing slash apparently crucial.
copyGCSObject.Destination = ts.destination + "/outs"
copyGCSObject.Destination = ts.gcsPath + "/outs"
copyGCSObjects := &daisy.CopyGCSObjects{copyGCSObject}
copyStep, err := ts.wf.NewStep("copy-objects")
if err != nil {
Expand Down Expand Up @@ -220,7 +377,7 @@ func runTestWorkflow(ctx context.Context, test *TestWorkflow) testResult {
res.testWorkflow = test
if test.skipped {
res.skipped = true
res.err = fmt.Errorf("Test suite was skipped")
res.err = fmt.Errorf("Test suite was skipped with message: %q", res.testWorkflow.SkippedMessage())
return res
}
fmt.Printf("runTestWorkflow: running %s on %s (ID %s)\n", test.Name, test.ShortImage, test.wf.ID())
Expand Down
Loading

0 comments on commit 1815ed5

Please sign in to comment.