// Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"). You may not // use this file except in compliance with the License. A copy of the // License is located at // // http://aws.amazon.com/apache2.0/ // // or in the "license" file accompanying this file. This file 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 task import ( "testing" "time" "github.com/aws/amazon-ssm-agent/agent/mocks/times" "github.com/stretchr/testify/assert" ) type InnerFunc func() // TestCase is a structure for testing functions that run other functions. // The outer function is the function under test and we want to make sure // that the inner function is called in the right sequence type TestCase struct { Clock *times.MockedClock CancelFlag *ChanneledCancelFlag CancelWaitMillis time.Duration InnerFunctionPanics bool innerFunction InnerFunc testFunctionWrapper func() innerFunctionState chan bool testFunctionState chan bool t *testing.T } // TestProcessNoCancel tests process on a job that exits before cancel. func TestProcessNoCancel(t *testing.T) { testCase := createTestCaseForProcess(t) // job ends then "process" ends testCase.assertInnerFunctionCompletes() testCase.assertTestMethodCompletes() testCase.assertExpectations() } // TestProcessCancelGraceful tests process on a job that gets canceled and exits before cancel timeout. func TestProcessCancelGraceful(t *testing.T) { testCase := createTestCaseForProcess(t) testCase.setupClock() // start job then send cancel signal testCase.assertInnerFunctionStarts() testCase.cancelJob() // job terminates before timeout testCase.assertInnerFunctionCompletes() // "process" exits normally even though the timeout hasn't happened testCase.assertTestMethodCompletes() // "timeout" the clock even though everything has already exited testCase.timeoutCancelWait() testCase.assertExpectations() } // TestProcessCancelGraceful tests process on a job that gets canceled and fails to exit before cancel timeout. func TestProcessCancelAbandon(t *testing.T) { testCase := createTestCaseForProcess(t) testCase.setupClock() // start job then send cancel signal testCase.assertInnerFunctionStarts() testCase.cancelJob() // timeout the "clock" before the job ends testCase.timeoutCancelWait() // check that we have exited from "process" with job still running testCase.assertTestMethodCompletes() // check that the job is still running (abandoned) testCase.assertInnerFunctionCompletes() testCase.assertExpectations() } // TestRunJob tests the runJob method. func TestRunJob(t *testing.T) { testRunJob(t, false) testRunJob(t, true) } func testRunJob(t *testing.T, innerFunctionPanics bool) { // create buffered channel for job done notification doneChannel := make(chan struct{}, 1) testCase := createTestCaseForRunJob(t, doneChannel, innerFunctionPanics) // see that job starts testCase.assertInnerFunctionStarts() if !innerFunctionPanics { testCase.assertInnerFunctionCompletes() } // check that runJob sends signal and exits assert.NotNil(t, <-doneChannel) testCase.assertTestMethodCompletes() testCase.assertExpectations() } func createTestCase(t *testing.T, innerFunctionPanics bool) TestCase { // create job jobState := make(chan bool) job := func() { jobState <- true if innerFunctionPanics { panic("panic") } jobState <- false } return TestCase{ Clock: times.NewMockedClock(), CancelFlag: NewChanneledCancelFlag(), CancelWaitMillis: time.Duration(100 * time.Millisecond), InnerFunctionPanics: innerFunctionPanics, innerFunction: job, innerFunctionState: jobState, testFunctionState: make(chan bool), t: t, } } func createTestCaseForProcess(t *testing.T) TestCase { testCase := createTestCase(t, false) testMethod := func() { job := func(CancelFlag) { testCase.innerFunction() } process(logger, job, testCase.CancelFlag, testCase.CancelWaitMillis, testCase.Clock) } testCase.startTestMethod(testMethod) return testCase } func createTestCaseForRunJob(t *testing.T, doneChannel chan struct{}, innerFunctionPanics bool) TestCase { testCase := createTestCase(t, innerFunctionPanics) testMethod := func() { runJob(logger, testCase.innerFunction, doneChannel) } testCase.startTestMethod(testMethod) return testCase } func (testCase *TestCase) startTestMethod(testMethod func()) { go func() { testCase.testFunctionState <- true testMethod() testCase.testFunctionState <- false }() assert.True(testCase.t, <-testCase.testFunctionState) } func (testCase *TestCase) assertInnerFunctionStarts() { assert.True(testCase.t, <-testCase.innerFunctionState) } func (testCase *TestCase) assertInnerFunctionCompletes() { res := <-testCase.innerFunctionState if res { assert.False(testCase.t, <-testCase.innerFunctionState) } } func (testCase *TestCase) assertTestMethodCompletes() { assert.False(testCase.t, <-testCase.testFunctionState) } func (testCase *TestCase) cancelJob() { testCase.CancelFlag.Set(Canceled) } func (testCase *TestCase) setupClock() { testCase.Clock.On("After", testCase.CancelWaitMillis).Return(testCase.Clock.AfterChannel) } func (testCase *TestCase) timeoutCancelWait() { testCase.Clock.AfterChannel <- time.Now() } func (testCase *TestCase) assertExpectations() { testCase.Clock.AssertExpectations(testCase.t) }