|
| 1 | +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. |
| 2 | +// |
| 3 | +// Licensed under the Apache License, Version 2.0 (the "License"). You may |
| 4 | +// not use this file except in compliance with the License. A copy of the |
| 5 | +// License is located at |
| 6 | +// |
| 7 | +// http://aws.amazon.com/apache2.0/ |
| 8 | +// |
| 9 | +// or in the "license" file accompanying this file. This file is distributed |
| 10 | +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either |
| 11 | +// express or implied. See the License for the specific language governing |
| 12 | +// permissions and limitations under the License. |
| 13 | + |
| 14 | +package testutil |
| 15 | + |
| 16 | +import ( |
| 17 | + "context" |
| 18 | + "errors" |
| 19 | + "fmt" |
| 20 | + mocksvcsdkapi "github.com/aws-controllers-k8s/applicationautoscaling-controller/test/mocks/aws-sdk-go/applicationautoscaling" |
| 21 | + acktypes "github.com/aws-controllers-k8s/runtime/pkg/types" |
| 22 | + "github.com/stretchr/testify/assert" |
| 23 | + "github.com/stretchr/testify/mock" |
| 24 | + "path/filepath" |
| 25 | + "strings" |
| 26 | + "testing" |
| 27 | +) |
| 28 | + |
| 29 | +var RecoverPanicString = "\t--- PANIC ON ERROR:" |
| 30 | + |
| 31 | +// TestSuiteRunner runs given test suite config with the help of delegate supplied to it |
| 32 | +type TestSuiteRunner struct { |
| 33 | + TestSuite *TestSuite |
| 34 | + Delegate TestRunnerDelegate |
| 35 | +} |
| 36 | + |
| 37 | +// fixtureContext is runtime context for test scenario given fixture. |
| 38 | +type fixtureContext struct { |
| 39 | + desired acktypes.AWSResource |
| 40 | + latest acktypes.AWSResource |
| 41 | + mocksdkapi *mocksvcsdkapi.ApplicationAutoScalingAPI |
| 42 | + resourceManager acktypes.AWSResourceManager |
| 43 | +} |
| 44 | + |
| 45 | +//TODO: remove if no longer used |
| 46 | +// expectContext is runtime context for test scenario expectation fixture. |
| 47 | +type expectContext struct { |
| 48 | + latest acktypes.AWSResource |
| 49 | + err error |
| 50 | +} |
| 51 | + |
| 52 | +// TestRunnerDelegate provides interface for custom resource tests to implement. |
| 53 | +// TestSuiteRunner depends on it to run tests for custom resource. |
| 54 | +type TestRunnerDelegate interface { |
| 55 | + ResourceDescriptor() acktypes.AWSResourceDescriptor |
| 56 | + Equal(desired acktypes.AWSResource, latest acktypes.AWSResource) bool // remove it when ResourceDescriptor.Delta() is available |
| 57 | + YamlEqual(expected string, actual acktypes.AWSResource) bool // new |
| 58 | + ResourceManager(*mocksvcsdkapi.ApplicationAutoScalingAPI) acktypes.AWSResourceManager |
| 59 | + EmptyServiceAPIOutput(apiName string) (interface{}, error) |
| 60 | + GoTestRunner() *testing.T |
| 61 | +} |
| 62 | + |
| 63 | +// RunTests runs the tests from the test suite |
| 64 | +func (runner *TestSuiteRunner) RunTests() { |
| 65 | + if runner.TestSuite == nil || runner.Delegate == nil { |
| 66 | + panic(errors.New("failed to run test suite")) |
| 67 | + } |
| 68 | + |
| 69 | + for _, test := range runner.TestSuite.Tests { |
| 70 | + fmt.Printf("Starting test: %s\n", test.Name) |
| 71 | + for _, scenario := range test.Scenarios { |
| 72 | + runner.startScenario(scenario) |
| 73 | + } |
| 74 | + fmt.Printf("Test: %s completed.\n", test.Name) |
| 75 | + } |
| 76 | +} |
| 77 | + |
| 78 | +// Wrapper for running test scenarios that catches any panics thrown. |
| 79 | +func (runner *TestSuiteRunner) startScenario (scenario TestScenario) { |
| 80 | + t := runner.Delegate.GoTestRunner() |
| 81 | + t.Run(scenario.Name, func(t *testing.T) { |
| 82 | + defer func() { |
| 83 | + if r := recover(); r != nil { |
| 84 | + fmt.Println(RecoverPanicString, r) |
| 85 | + t.Fail() |
| 86 | + } |
| 87 | + }() |
| 88 | + fmt.Printf("Running test scenario: %s\n", scenario.Name) |
| 89 | + fixtureCxt := runner.setupFixtureContext(&scenario.Fixture) |
| 90 | + runner.runTestScenario(t, scenario.Name, fixtureCxt, scenario.UnitUnderTest, &scenario.Expect) |
| 91 | + }) |
| 92 | +} |
| 93 | + |
| 94 | +// runTestScenario runs given test scenario which is expressed as: given fixture context, unit to test, expected fixture context. |
| 95 | +func (runner *TestSuiteRunner) runTestScenario(t *testing.T, scenarioName string, fixtureCxt *fixtureContext, unitUnderTest string, expectation *Expect) { |
| 96 | + rm := fixtureCxt.resourceManager |
| 97 | + assert := assert.New(t) |
| 98 | + |
| 99 | + var actual acktypes.AWSResource = nil |
| 100 | + var err error = nil |
| 101 | + switch unitUnderTest { |
| 102 | + case "ReadOne": |
| 103 | + actual, err = rm.ReadOne(context.Background(), fixtureCxt.desired) |
| 104 | + case "Create": |
| 105 | + actual, err = rm.Create(context.Background(), fixtureCxt.desired) |
| 106 | + case "Update": |
| 107 | + delta := runner.Delegate.ResourceDescriptor().Delta(fixtureCxt.desired, fixtureCxt.latest) |
| 108 | + actual, err = rm.Update(context.Background(), fixtureCxt.desired, fixtureCxt.latest, delta) |
| 109 | + case "Delete": |
| 110 | + err = rm.Delete(context.Background(), fixtureCxt.desired) |
| 111 | + default: |
| 112 | + panic(errors.New(fmt.Sprintf("unit under test: %s not supported", unitUnderTest))) |
| 113 | + } |
| 114 | + runner.assertExpectations(assert, expectation, actual, err) |
| 115 | +} |
| 116 | + |
| 117 | +/* assertExpectations validates the actual outcome against the expected outcome. |
| 118 | +There are two components to the expected outcome, corresponding to the return values of the resource manager's CRUD operation: |
| 119 | + 1) the actual return value of type AWSResource ("expect.latest_state" in test_suite.yaml) |
| 120 | + 2) the error ("expect.error" in test_suite.yaml) |
| 121 | +With each of these components, there are three possibilities in test_suite.yaml, which are interpreted as follows: |
| 122 | + 1) the key does not exist, or was provided with no value: no explicit expectations, don't assert anything |
| 123 | + 2) the key was provided with value "nil": explicit expectation; assert that the error or return value is nil |
| 124 | + 3) the key was provided with value other than "nil": explicit expectation; assert that the value matches the |
| 125 | + expected value |
| 126 | +However, if neither expect.latest_state nor error are provided, assertExpectations will fail the test case. |
| 127 | +*/ |
| 128 | +func (runner *TestSuiteRunner) assertExpectations(assert *assert.Assertions, expectation *Expect, actual acktypes.AWSResource, err error) { |
| 129 | + if expectation.LatestState == "" && expectation.Error == "" { |
| 130 | + fmt.Println("Invalid test case: no expectation given for either latest_state or error") |
| 131 | + assert.True(false) |
| 132 | + return |
| 133 | + } |
| 134 | + |
| 135 | + // expectation exists for at least one of LatestState and Error; assert results independently |
| 136 | + if expectation.LatestState == "nil" { |
| 137 | + assert.Nil(actual) |
| 138 | + } else if expectation.LatestState != "" { |
| 139 | + expectedLatest := runner.loadAWSResource(expectation.LatestState) |
| 140 | + assert.NotNil(actual) |
| 141 | + |
| 142 | + delta := runner.Delegate.ResourceDescriptor().Delta(expectedLatest, actual) |
| 143 | + assert.Equal(0, len(delta.Differences)) |
| 144 | + if len(delta.Differences) > 0 { |
| 145 | + fmt.Println("Unexpected differences:") |
| 146 | + for _, difference := range delta.Differences { |
| 147 | + fmt.Printf("Path: %v, expected: %v, actual: %v\n", difference.Path, difference.A, difference.B) |
| 148 | + fmt.Printf("See expected differences below:\n") |
| 149 | + } |
| 150 | + } |
| 151 | + |
| 152 | + // Check that the yaml files are equivalent. |
| 153 | + // This makes it easier to make changes to unit test cases. |
| 154 | + assert.True(runner.Delegate.YamlEqual(expectation.LatestState, actual)) |
| 155 | + // Delta only contains `Spec` differences. Thus, we need Delegate.Equal to compare `Status`. |
| 156 | + assert.True(runner.Delegate.Equal(expectedLatest, actual)) |
| 157 | + } |
| 158 | + |
| 159 | + if expectation.Error == "nil" { |
| 160 | + assert.Nil(err) |
| 161 | + } else if expectation.Error != "" { |
| 162 | + expectedError := errors.New(expectation.Error) |
| 163 | + assert.NotNil(err) |
| 164 | + |
| 165 | + assert.Equal(expectedError.Error(), err.Error()) |
| 166 | + } |
| 167 | +} |
| 168 | + |
| 169 | +// setupFixtureContext provides runtime context for test scenario given fixture. |
| 170 | +func (runner *TestSuiteRunner) setupFixtureContext(fixture *Fixture) *fixtureContext { |
| 171 | + if fixture == nil { |
| 172 | + return nil |
| 173 | + } |
| 174 | + var cxt = fixtureContext{} |
| 175 | + if fixture.DesiredState != "" { |
| 176 | + cxt.desired = runner.loadAWSResource(fixture.DesiredState) |
| 177 | + } |
| 178 | + if fixture.LatestState != "" { |
| 179 | + cxt.latest = runner.loadAWSResource(fixture.LatestState) |
| 180 | + } |
| 181 | + mocksdkapi := &mocksvcsdkapi.ApplicationAutoScalingAPI{} |
| 182 | + for _, serviceApi := range fixture.ServiceAPIs { |
| 183 | + if serviceApi.Operation != "" { |
| 184 | + |
| 185 | + if serviceApi.ServiceAPIError != nil { |
| 186 | + mockError := CreateAWSError(*serviceApi.ServiceAPIError) |
| 187 | + mocksdkapi.On(serviceApi.Operation, mock.Anything, mock.Anything).Return(nil, mockError) |
| 188 | + } else if serviceApi.Operation != "" && serviceApi.Output != "" { |
| 189 | + var outputObj, err = runner.Delegate.EmptyServiceAPIOutput(serviceApi.Operation) |
| 190 | + apiOutputFixturePath := append([]string{"testdata"}, strings.Split(serviceApi.Output, "/")...) |
| 191 | + LoadFromFixture(filepath.Join(apiOutputFixturePath...), outputObj) |
| 192 | + mocksdkapi.On(serviceApi.Operation, mock.Anything, mock.Anything).Return(outputObj, nil) |
| 193 | + if err != nil { |
| 194 | + panic(err) |
| 195 | + } |
| 196 | + } else if serviceApi.ServiceAPIError == nil && serviceApi.Output == "" { |
| 197 | + // Default case for no defined output fixture or error. |
| 198 | + mocksdkapi.On(serviceApi.Operation, mock.Anything, mock.Anything).Return(nil, nil) |
| 199 | + } |
| 200 | + } |
| 201 | + } |
| 202 | + cxt.mocksdkapi = mocksdkapi |
| 203 | + cxt.resourceManager = runner.Delegate.ResourceManager(mocksdkapi) |
| 204 | + return &cxt |
| 205 | +} |
| 206 | + |
| 207 | +// loadAWSResource loads AWSResource from the supplied fixture file. |
| 208 | +func (runner *TestSuiteRunner) loadAWSResource(resourceFixtureFilePath string) acktypes.AWSResource { |
| 209 | + if resourceFixtureFilePath == "" { |
| 210 | + panic(errors.New(fmt.Sprintf("resourceFixtureFilePath not specified"))) |
| 211 | + } |
| 212 | + var rd = runner.Delegate.ResourceDescriptor() |
| 213 | + ro := rd.EmptyRuntimeObject() |
| 214 | + path := append([]string{"testdata"}, strings.Split(resourceFixtureFilePath, "/")...) |
| 215 | + LoadFromFixture(filepath.Join(path...), ro) |
| 216 | + return rd.ResourceFromRuntimeObject(ro) |
| 217 | +} |
0 commit comments