github.com/ystia/yorc/v4@v4.3.0/plugin/operation_test.go (about)

     1  // Copyright 2018 Bull S.A.S. Atos Technologies - Bull, Rue Jean Jaures, B.P.68, 78340, Les Clayes-sous-Bois, France.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package plugin
    16  
    17  import (
    18  	"context"
    19  	"errors"
    20  	"testing"
    21  	"time"
    22  
    23  	plugin "github.com/hashicorp/go-plugin"
    24  	"github.com/stretchr/testify/assert"
    25  	"github.com/stretchr/testify/require"
    26  
    27  	"github.com/ystia/yorc/v4/config"
    28  	"github.com/ystia/yorc/v4/events"
    29  	"github.com/ystia/yorc/v4/prov"
    30  )
    31  
    32  type mockOperationExecutor struct {
    33  	execoperationCalled            bool
    34  	execAsyncOperationCalled       bool
    35  	ctx                            context.Context
    36  	conf                           config.Configuration
    37  	taskID, deploymentID, nodeName string
    38  	operation                      prov.Operation
    39  	contextCancelled               bool
    40  	lof                            events.LogOptionalFields
    41  }
    42  
    43  func (m *mockOperationExecutor) ExecAsyncOperation(ctx context.Context, conf config.Configuration, taskID, deploymentID, nodeName string, operation prov.Operation, stepName string) (*prov.Action, time.Duration, error) {
    44  	m.execAsyncOperationCalled = true
    45  	m.ctx = ctx
    46  	m.conf = conf
    47  	m.taskID = taskID
    48  	m.deploymentID = deploymentID
    49  	m.nodeName = nodeName
    50  	m.operation = operation
    51  	m.lof, _ = events.FromContext(ctx)
    52  
    53  	go func() {
    54  		<-m.ctx.Done()
    55  		m.contextCancelled = true
    56  	}()
    57  	if m.deploymentID == "TestCancel" {
    58  		<-m.ctx.Done()
    59  	}
    60  	if m.deploymentID == "TestFailure" {
    61  		return nil, 0, NewRPCError(errors.New("a failure occurred during plugin exec async operation"))
    62  	}
    63  	action := prov.Action{
    64  		ID:         "testID",
    65  		ActionType: "testActionType",
    66  		AsyncOperation: prov.AsyncOperation{
    67  			DeploymentID: deploymentID,
    68  			TaskID:       taskID,
    69  			StepName:     stepName,
    70  			NodeName:     nodeName,
    71  			Operation:    operation,
    72  		},
    73  		Data: map[string]string{
    74  			"testDataKey": "testDataValue",
    75  		},
    76  	}
    77  	return &action, 5 * time.Second, nil
    78  }
    79  
    80  func (m *mockOperationExecutor) ExecOperation(ctx context.Context, conf config.Configuration, taskID, deploymentID, nodeName string, operation prov.Operation) error {
    81  	m.execoperationCalled = true
    82  	m.ctx = ctx
    83  	m.conf = conf
    84  	m.taskID = taskID
    85  	m.deploymentID = deploymentID
    86  	m.nodeName = nodeName
    87  	m.operation = operation
    88  	m.lof, _ = events.FromContext(ctx)
    89  
    90  	go func() {
    91  		<-m.ctx.Done()
    92  		m.contextCancelled = true
    93  	}()
    94  	if m.deploymentID == "TestCancel" {
    95  		<-m.ctx.Done()
    96  	}
    97  	if m.deploymentID == "TestFailure" {
    98  		return NewRPCError(errors.New("a failure occurred during plugin exec operation"))
    99  	}
   100  	return nil
   101  }
   102  
   103  func createMockOperationExecutorClient(t *testing.T) (*mockOperationExecutor, *plugin.RPCClient) {
   104  	mock := new(mockOperationExecutor)
   105  	client, _ := plugin.TestPluginRPCConn(
   106  		t,
   107  		map[string]plugin.Plugin{
   108  			OperationPluginName: &OperationPlugin{F: func() prov.OperationExecutor {
   109  				return mock
   110  			}},
   111  		},
   112  		nil)
   113  	return mock, client
   114  }
   115  
   116  func setupExecOperationTestEnv(t *testing.T) (*mockOperationExecutor, *plugin.RPCClient,
   117  	prov.OperationExecutor, prov.Operation, events.LogOptionalFields, context.Context) {
   118  
   119  	mock, client := createMockOperationExecutorClient(t)
   120  
   121  	raw, err := client.Dispense(OperationPluginName)
   122  	require.Nil(t, err)
   123  
   124  	plugin := raw.(prov.OperationExecutor)
   125  	op := prov.Operation{
   126  		Name:                   "myOps",
   127  		ImplementationArtifact: "tosca.artifacts.Implementation.Bash",
   128  		RelOp: prov.RelationshipOperation{
   129  			IsRelationshipOperation: true,
   130  			RequirementIndex:        "1",
   131  			TargetNodeName:          "AnotherNode",
   132  		},
   133  	}
   134  	lof := events.LogOptionalFields{
   135  		events.WorkFlowID:    "testWF",
   136  		events.InterfaceName: "delegate",
   137  		events.OperationName: "myTest",
   138  	}
   139  	ctx := events.NewContext(context.Background(), lof)
   140  
   141  	return mock, client, plugin, op, lof, ctx
   142  
   143  }
   144  func TestOperationExecutorExecOperation(t *testing.T) {
   145  	t.Parallel()
   146  	mock, client, plugin, op, lof, ctx := setupExecOperationTestEnv(t)
   147  	defer client.Close()
   148  	err := plugin.ExecOperation(
   149  		ctx,
   150  		config.Configuration{Consul: config.Consul{Address: "test", Datacenter: "testdc"}},
   151  		"TestTaskID", "TestDepID", "TestNodeName", op)
   152  	require.Nil(t, err)
   153  	require.True(t, mock.execoperationCalled)
   154  	require.Equal(t, "test", mock.conf.Consul.Address)
   155  	require.Equal(t, "testdc", mock.conf.Consul.Datacenter)
   156  	require.Equal(t, "TestTaskID", mock.taskID)
   157  	require.Equal(t, "TestDepID", mock.deploymentID)
   158  	require.Equal(t, "TestNodeName", mock.nodeName)
   159  	require.Equal(t, op, mock.operation)
   160  	assert.Equal(t, lof, mock.lof)
   161  }
   162  
   163  func TestOperationExecutorExecOperationWithFailure(t *testing.T) {
   164  	t.Parallel()
   165  	mock, client, plugin, op, _, ctx := setupExecOperationTestEnv(t)
   166  	defer client.Close()
   167  	err := plugin.ExecOperation(
   168  		ctx,
   169  		config.Configuration{Consul: config.Consul{Address: "test", Datacenter: "testdc"}},
   170  		"TestTaskID", "TestFailure", "TestNodeName", op)
   171  	require.True(t, mock.execoperationCalled)
   172  	require.Error(t, err, "An error was expected during executing plugin operation")
   173  	require.EqualError(t, err, "a failure occurred during plugin exec operation")
   174  }
   175  
   176  func TestOperationExecutorExecOperationWithCancel(t *testing.T) {
   177  	t.Parallel()
   178  	mock, client := createMockOperationExecutorClient(t)
   179  	defer client.Close()
   180  
   181  	raw, err := client.Dispense(OperationPluginName)
   182  	require.Nil(t, err)
   183  
   184  	plugin := raw.(prov.OperationExecutor)
   185  	lof := events.LogOptionalFields{
   186  		events.WorkFlowID:    "testWF",
   187  		events.InterfaceName: "delegate",
   188  		events.OperationName: "myTest",
   189  	}
   190  	ctx := events.NewContext(context.Background(), lof)
   191  	ctx, cancelF := context.WithCancel(ctx)
   192  	go func() {
   193  		err = plugin.ExecOperation(
   194  			ctx,
   195  			config.Configuration{Consul: config.Consul{Address: "test", Datacenter: "testdc"}},
   196  			"TestTaskID", "TestCancel", "TestNodeName", prov.Operation{})
   197  		require.Nil(t, err)
   198  	}()
   199  	cancelF()
   200  	// Wait for cancellation signal to be dispatched
   201  	time.Sleep(50 * time.Millisecond)
   202  	require.True(t, mock.contextCancelled, "Context not cancelled")
   203  }
   204  
   205  func TestOperationGetSupportedArtifactTypes(t *testing.T) {
   206  	mock := new(mockOperationExecutor)
   207  	client, _ := plugin.TestPluginRPCConn(
   208  		t,
   209  		map[string]plugin.Plugin{
   210  			OperationPluginName: &OperationPlugin{
   211  				F: func() prov.OperationExecutor {
   212  					return mock
   213  				},
   214  				SupportedTypes: []string{"tosca.my.types", "test"}},
   215  		},
   216  		nil)
   217  	defer client.Close()
   218  	raw, err := client.Dispense(OperationPluginName)
   219  	require.Nil(t, err)
   220  	plugin := raw.(OperationExecutor)
   221  
   222  	supportedTypes, err := plugin.GetSupportedArtifactTypes()
   223  	require.Nil(t, err)
   224  	require.Len(t, supportedTypes, 2)
   225  	require.Contains(t, supportedTypes, "tosca.my.types")
   226  	require.Contains(t, supportedTypes, "test")
   227  }
   228  
   229  func TestOperationExecutorExecAsyncOperation(t *testing.T) {
   230  	t.Parallel()
   231  	mock, client, plugin, op, lof, ctx := setupExecOperationTestEnv(t)
   232  	defer client.Close()
   233  	action, interval, err := plugin.ExecAsyncOperation(
   234  		ctx,
   235  		config.Configuration{Consul: config.Consul{Address: "test", Datacenter: "testdc"}},
   236  		"TestTaskID", "TestDepID", "TestNodeName", op, "TestStepName")
   237  	require.NoError(t, err, "Failed to call plugin ExecAsyncOperation")
   238  	require.True(t, mock.execAsyncOperationCalled)
   239  	require.Equal(t, "TestTaskID", action.AsyncOperation.TaskID)
   240  	require.Equal(t, "testDataValue", action.Data["testDataKey"])
   241  	require.Equal(t, "TestNodeName", action.AsyncOperation.NodeName)
   242  	require.Equal(t, op, action.AsyncOperation.Operation)
   243  	require.Equal(t, 5*time.Second, interval)
   244  	assert.Equal(t, lof, mock.lof)
   245  }
   246  
   247  func TestOperationExecutorExecAsyncOperationWithFailure(t *testing.T) {
   248  	t.Parallel()
   249  	mock, client, plugin, op, _, ctx := setupExecOperationTestEnv(t)
   250  	defer client.Close()
   251  	_, _, err := plugin.ExecAsyncOperation(
   252  		ctx,
   253  		config.Configuration{Consul: config.Consul{Address: "test", Datacenter: "testdc"}},
   254  		"TestTaskID", "TestFailure", "TestNodeName", op, "TestStepName")
   255  	require.True(t, mock.execAsyncOperationCalled)
   256  	require.Error(t, err, "An error was expected calling plugin ExecAsyncOperation")
   257  	require.EqualError(t, err, "a failure occurred during plugin exec async operation")
   258  }
   259  
   260  func TestOperationExecutorExecAsyncOperationWithCancel(t *testing.T) {
   261  	t.Parallel()
   262  	mock, client := createMockOperationExecutorClient(t)
   263  	defer client.Close()
   264  
   265  	raw, err := client.Dispense(OperationPluginName)
   266  	require.Nil(t, err)
   267  
   268  	plugin := raw.(prov.OperationExecutor)
   269  	lof := events.LogOptionalFields{
   270  		events.WorkFlowID:    "testWF",
   271  		events.InterfaceName: "delegate",
   272  		events.OperationName: "myTest",
   273  	}
   274  	ctx := events.NewContext(context.Background(), lof)
   275  	ctx, cancelF := context.WithCancel(ctx)
   276  	go func() {
   277  		_, _, err := plugin.ExecAsyncOperation(
   278  			ctx,
   279  			config.Configuration{Consul: config.Consul{Address: "test", Datacenter: "testdc"}},
   280  			"TestTaskID", "TestCancel", "TestNodeName", prov.Operation{}, "TestStepName")
   281  		require.Nil(t, err)
   282  	}()
   283  	cancelF()
   284  	// Wait for cancellation signal to be dispatched
   285  	time.Sleep(50 * time.Millisecond)
   286  	require.True(t, mock.contextCancelled, "Context not cancelled")
   287  }