mirror of
https://github.com/StarFleetCPTN/GoMFT.git
synced 2026-09-08 15:41:20 +02:00
- Integrated new components for job scheduling, including a cron-based scheduler and improved job execution logic. - Refactored job executor to utilize interfaces for better dependency management and testing. - Enhanced notification system with a new Notifier interface, allowing for flexible notification service integration. - Added comprehensive unit tests for job executor, scheduler, and notification components to ensure reliability and correctness. - Updated metadata handling to support new database interfaces, improving testability and maintainability.
527 lines
17 KiB
Go
527 lines
17 KiB
Go
package scheduler
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"strings" // Added import
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/robfig/cron/v3"
|
|
"github.com/starfleetcptn/gomft/internal/db"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// --- Mock Implementations ---
|
|
|
|
// Mock JobExecutorDB
|
|
var _ JobExecutorDB = (*mockJobExecutorDB)(nil)
|
|
|
|
type mockJobExecutorDB struct {
|
|
mu sync.Mutex
|
|
FirstFunc func(dest interface{}, conds ...interface{}) *gorm.DB
|
|
GetConfigsForJobFunc func(jobID uint) ([]db.TransferConfig, error)
|
|
UpdateJobStatusFunc func(job *db.Job) error
|
|
CreateJobHistoryFunc func(history *db.JobHistory) error
|
|
|
|
// Store calls/data
|
|
firstCalledWithDest interface{}
|
|
firstCalledWithConds []interface{}
|
|
configsForJobID uint
|
|
updatedJobStatus *db.Job
|
|
createdHistory *db.JobHistory
|
|
}
|
|
|
|
func (m *mockJobExecutorDB) First(dest interface{}, conds ...interface{}) *gorm.DB {
|
|
m.mu.Lock()
|
|
m.firstCalledWithDest = dest
|
|
m.firstCalledWithConds = conds
|
|
m.mu.Unlock()
|
|
if m.FirstFunc != nil {
|
|
return m.FirstFunc(dest, conds...)
|
|
}
|
|
// Default: Simulate job found by populating dest
|
|
if job, ok := dest.(*db.Job); ok && len(conds) > 0 {
|
|
if jobID, ok := conds[0].(uint); ok {
|
|
job.ID = jobID
|
|
job.Name = fmt.Sprintf("Mock Job %d", jobID)
|
|
job.ConfigIDs = "1,2" // Default config IDs
|
|
enabled := true
|
|
job.Enabled = &enabled
|
|
return &gorm.DB{Error: nil} // Success
|
|
}
|
|
}
|
|
return &gorm.DB{Error: gorm.ErrRecordNotFound} // Default not found
|
|
}
|
|
|
|
func (m *mockJobExecutorDB) GetConfigsForJob(jobID uint) ([]db.TransferConfig, error) {
|
|
m.mu.Lock()
|
|
m.configsForJobID = jobID
|
|
m.mu.Unlock()
|
|
if m.GetConfigsForJobFunc != nil {
|
|
return m.GetConfigsForJobFunc(jobID)
|
|
}
|
|
// Default: return some mock configs
|
|
return []db.TransferConfig{
|
|
{ID: 1, Name: "Config 1"}, // Corrected initialization
|
|
{ID: 2, Name: "Config 2"}, // Corrected initialization
|
|
}, nil
|
|
}
|
|
|
|
func (m *mockJobExecutorDB) UpdateJobStatus(job *db.Job) error {
|
|
m.mu.Lock()
|
|
m.updatedJobStatus = job // Store last updated job
|
|
m.mu.Unlock()
|
|
if m.UpdateJobStatusFunc != nil {
|
|
return m.UpdateJobStatusFunc(job)
|
|
}
|
|
return nil // Default success
|
|
}
|
|
|
|
func (m *mockJobExecutorDB) CreateJobHistory(history *db.JobHistory) error {
|
|
m.mu.Lock()
|
|
m.createdHistory = history // Store last created history
|
|
m.mu.Unlock()
|
|
if m.CreateJobHistoryFunc != nil {
|
|
return m.CreateJobHistoryFunc(history)
|
|
}
|
|
history.ID = 999 // Assign mock ID
|
|
return nil // Default success
|
|
}
|
|
|
|
func (m *mockJobExecutorDB) Reset() {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.firstCalledWithDest = nil
|
|
m.firstCalledWithConds = nil
|
|
m.configsForJobID = 0
|
|
m.updatedJobStatus = nil
|
|
m.createdHistory = nil
|
|
}
|
|
|
|
// Mock JobExecutorCron
|
|
var _ JobExecutorCron = (*mockJobExecutorCron)(nil)
|
|
|
|
type mockJobExecutorCron struct {
|
|
mu sync.Mutex
|
|
EntryFunc func(id cron.EntryID) cron.Entry
|
|
|
|
// Store calls
|
|
entryCalledWithID cron.EntryID
|
|
}
|
|
|
|
func (m *mockJobExecutorCron) Entry(id cron.EntryID) cron.Entry {
|
|
m.mu.Lock()
|
|
m.entryCalledWithID = id
|
|
m.mu.Unlock()
|
|
if m.EntryFunc != nil {
|
|
return m.EntryFunc(id)
|
|
}
|
|
// Default: return a basic entry with a future next run time
|
|
return cron.Entry{
|
|
ID: id,
|
|
Next: time.Now().Add(1 * time.Hour),
|
|
}
|
|
}
|
|
func (m *mockJobExecutorCron) Reset() {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.entryCalledWithID = 0
|
|
}
|
|
|
|
// Mock JobExecutorTransferExecutor
|
|
var _ JobExecutorTransferExecutor = (*mockJobExecutorTransferExecutor)(nil)
|
|
|
|
type mockJobExecutorTransferExecutor struct {
|
|
mu sync.Mutex
|
|
ExecuteConfigTransferFunc func(job db.Job, config db.TransferConfig, history *db.JobHistory)
|
|
|
|
// Store calls
|
|
executeConfigTransferCalls []map[string]interface{}
|
|
}
|
|
|
|
func (m *mockJobExecutorTransferExecutor) executeConfigTransfer(job db.Job, config db.TransferConfig, history *db.JobHistory) {
|
|
m.mu.Lock()
|
|
m.executeConfigTransferCalls = append(m.executeConfigTransferCalls, map[string]interface{}{
|
|
"job": job, "config": config, "history": history,
|
|
})
|
|
m.mu.Unlock()
|
|
if m.ExecuteConfigTransferFunc != nil {
|
|
m.ExecuteConfigTransferFunc(job, config, history)
|
|
}
|
|
// Default: Do nothing, just record the call
|
|
}
|
|
func (m *mockJobExecutorTransferExecutor) Reset() {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.executeConfigTransferCalls = nil
|
|
}
|
|
|
|
// Mock JobExecutorNotifier
|
|
var _ JobExecutorNotifier = (*mockJobExecutorNotifier)(nil)
|
|
|
|
type mockJobExecutorNotifier struct {
|
|
mu sync.Mutex
|
|
SendNotificationsFunc func(job *db.Job, history *db.JobHistory, config *db.TransferConfig)
|
|
|
|
// Store calls
|
|
sendNotificationsCalls []map[string]interface{}
|
|
}
|
|
|
|
func (m *mockJobExecutorNotifier) SendNotifications(job *db.Job, history *db.JobHistory, config *db.TransferConfig) {
|
|
m.mu.Lock()
|
|
m.sendNotificationsCalls = append(m.sendNotificationsCalls, map[string]interface{}{
|
|
"job": job, "history": history, "config": config,
|
|
})
|
|
m.mu.Unlock()
|
|
if m.SendNotificationsFunc != nil {
|
|
m.SendNotificationsFunc(job, history, config)
|
|
}
|
|
}
|
|
func (m *mockJobExecutorNotifier) Reset() {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.sendNotificationsCalls = nil
|
|
}
|
|
|
|
// --- Test Setup ---
|
|
|
|
type testJobExecutorComponents struct {
|
|
db *mockJobExecutorDB
|
|
logger *Logger
|
|
logBuf *bytes.Buffer
|
|
cron *mockJobExecutorCron
|
|
transfer *mockJobExecutorTransferExecutor
|
|
notifier *mockJobExecutorNotifier
|
|
executor *JobExecutor
|
|
jobsMap map[uint]cron.EntryID
|
|
jobMutex *sync.Mutex
|
|
}
|
|
|
|
func setupTestJobExecutor() testJobExecutorComponents {
|
|
dbMock := &mockJobExecutorDB{}
|
|
logger, logBuf := newTestLogger(LogLevelDebug)
|
|
cronMock := &mockJobExecutorCron{}
|
|
transferMock := &mockJobExecutorTransferExecutor{}
|
|
notifierMock := &mockJobExecutorNotifier{}
|
|
jobsMap := make(map[uint]cron.EntryID)
|
|
var jobMutex sync.Mutex
|
|
|
|
executor := NewJobExecutor(dbMock, logger, cronMock, jobsMap, &jobMutex, transferMock, notifierMock)
|
|
|
|
return testJobExecutorComponents{
|
|
db: dbMock,
|
|
logger: logger,
|
|
logBuf: logBuf,
|
|
cron: cronMock,
|
|
transfer: transferMock,
|
|
notifier: notifierMock,
|
|
executor: executor,
|
|
jobsMap: jobsMap,
|
|
jobMutex: &jobMutex,
|
|
}
|
|
}
|
|
|
|
// --- Tests ---
|
|
|
|
func TestExecuteJob_Success(t *testing.T) {
|
|
comps := setupTestJobExecutor()
|
|
defer comps.logger.Close()
|
|
|
|
testJobID := uint(1)
|
|
testCronEntryID := cron.EntryID(10)
|
|
comps.jobsMap[testJobID] = testCronEntryID // Simulate job being scheduled
|
|
|
|
// Configure mocks
|
|
comps.db.GetConfigsForJobFunc = func(jobID uint) ([]db.TransferConfig, error) {
|
|
if jobID != testJobID {
|
|
t.Errorf("GetConfigsForJob called with wrong jobID: got %d, want %d", jobID, testJobID)
|
|
}
|
|
// Return configs in a different order than job.ConfigIDs to test ordering logic
|
|
return []db.TransferConfig{
|
|
{ID: 2, Name: "Config 2"}, // Corrected initialization
|
|
{ID: 1, Name: "Config 1"}, // Corrected initialization
|
|
{ID: 3, Name: "Config 3 (Not in Job Order)"}, // Corrected initialization
|
|
}, nil
|
|
}
|
|
// Ensure the job returned by First has the expected ConfigIDs order
|
|
comps.db.FirstFunc = func(dest interface{}, conds ...interface{}) *gorm.DB {
|
|
if job, ok := dest.(*db.Job); ok {
|
|
job.ID = testJobID
|
|
job.Name = "Test Job Success"
|
|
job.ConfigIDs = "1,2" // Explicit order
|
|
enabled := true
|
|
job.Enabled = &enabled
|
|
return &gorm.DB{Error: nil}
|
|
}
|
|
return &gorm.DB{Error: gorm.ErrRecordNotFound}
|
|
}
|
|
|
|
// Execute the job
|
|
comps.executor.executeJob(testJobID)
|
|
|
|
// Assertions
|
|
// 1. DB calls
|
|
comps.db.mu.Lock()
|
|
if comps.db.firstCalledWithDest == nil {
|
|
t.Error("DB First was not called")
|
|
}
|
|
if comps.db.configsForJobID != testJobID {
|
|
t.Errorf("GetConfigsForJob not called with correct jobID: got %d, want %d", comps.db.configsForJobID, testJobID)
|
|
}
|
|
if comps.db.updatedJobStatus == nil {
|
|
t.Error("DB UpdateJobStatus was not called")
|
|
} else if comps.db.updatedJobStatus.LastRun == nil {
|
|
t.Error("LastRun time was not updated")
|
|
} else if comps.db.updatedJobStatus.NextRun == nil {
|
|
t.Error("NextRun time was not updated")
|
|
}
|
|
if comps.db.createdHistory == nil {
|
|
t.Error("DB CreateJobHistory was not called")
|
|
}
|
|
comps.db.mu.Unlock()
|
|
|
|
// 2. Cron calls
|
|
comps.cron.mu.Lock()
|
|
if comps.cron.entryCalledWithID != testCronEntryID {
|
|
t.Errorf("Cron Entry not called with correct entryID: got %d, want %d", comps.cron.entryCalledWithID, testCronEntryID)
|
|
}
|
|
comps.cron.mu.Unlock()
|
|
|
|
// 3. Notifier calls (via processConfiguration -> transferExecutor)
|
|
comps.notifier.mu.Lock()
|
|
// Expect one call per configuration processed (1, 2, then 3)
|
|
if len(comps.notifier.sendNotificationsCalls) != 3 { // Expect 3 calls now
|
|
t.Errorf("Expected 3 calls to SendNotifications, got %d", len(comps.notifier.sendNotificationsCalls))
|
|
}
|
|
comps.notifier.mu.Unlock()
|
|
|
|
// 4. TransferExecutor calls
|
|
comps.transfer.mu.Lock()
|
|
if len(comps.transfer.executeConfigTransferCalls) != 3 { // Expect 3 calls now
|
|
t.Errorf("Expected 3 calls to executeConfigTransfer, got %d", len(comps.transfer.executeConfigTransferCalls))
|
|
} else {
|
|
// Check order (1, 2, then 3)
|
|
call1 := comps.transfer.executeConfigTransferCalls[0]
|
|
call2 := comps.transfer.executeConfigTransferCalls[1]
|
|
call3 := comps.transfer.executeConfigTransferCalls[2]
|
|
if cfg1, ok := call1["config"].(db.TransferConfig); !ok || cfg1.ID != 1 {
|
|
t.Errorf("Expected first transfer call for config ID 1, got %+v", call1["config"])
|
|
}
|
|
if cfg2, ok := call2["config"].(db.TransferConfig); !ok || cfg2.ID != 2 {
|
|
t.Errorf("Expected second transfer call for config ID 2, got %+v", call2["config"])
|
|
}
|
|
if cfg3, ok := call3["config"].(db.TransferConfig); !ok || cfg3.ID != 3 {
|
|
t.Errorf("Expected third transfer call for config ID 3, got %+v", call3["config"])
|
|
}
|
|
}
|
|
comps.transfer.mu.Unlock()
|
|
|
|
// 5. Logs
|
|
logOutput := comps.logBuf.String()
|
|
// Check for specific log messages in order
|
|
expectedLogs := []string{
|
|
fmt.Sprintf("Starting execution of job %d", testJobID),
|
|
"Processing job 1 with 3 configurations in specified order", // Uses total configs found
|
|
"Execution order 1/3: Config ID 1", // Uses total configs found
|
|
"Processing configuration 1 (1/3) for job 1", // Log from processConfiguration
|
|
"Execution order 2/3: Config ID 2", // Uses total configs found
|
|
"Processing configuration 2 (2/3) for job 1", // Log from processConfiguration
|
|
"Execution order 3/3: Config ID 3", // Uses total configs found
|
|
"Processing configuration 3 (3/3) for job 1", // Log from processConfiguration for extra config
|
|
fmt.Sprintf("Next run time for job %d", testJobID),
|
|
}
|
|
for _, expectedLog := range expectedLogs {
|
|
if !strings.Contains(logOutput, expectedLog) {
|
|
t.Errorf("Expected log message containing %q not found in output:\n%s", expectedLog, logOutput)
|
|
}
|
|
}
|
|
// Removed extra closing brace
|
|
}
|
|
|
|
func TestExecuteJob_JobNotFound(t *testing.T) {
|
|
comps := setupTestJobExecutor()
|
|
defer comps.logger.Close()
|
|
testJobID := uint(5)
|
|
|
|
// Configure mocks
|
|
comps.db.FirstFunc = func(dest interface{}, conds ...interface{}) *gorm.DB {
|
|
return &gorm.DB{Error: gorm.ErrRecordNotFound} // Simulate job not found
|
|
}
|
|
|
|
comps.executor.executeJob(testJobID)
|
|
|
|
// Assertions
|
|
logOutput := comps.logBuf.String()
|
|
if !strings.Contains(logOutput, fmt.Sprintf("Error loading job %d: record not found", testJobID)) {
|
|
t.Errorf("Expected 'Error loading job' log message not found in output:\n%s", logOutput)
|
|
}
|
|
// Ensure other dependent functions were not called
|
|
comps.db.mu.Lock()
|
|
if comps.db.configsForJobID != 0 {
|
|
t.Error("GetConfigsForJob should not have been called")
|
|
}
|
|
comps.db.mu.Unlock()
|
|
comps.transfer.mu.Lock()
|
|
if len(comps.transfer.executeConfigTransferCalls) > 0 {
|
|
t.Error("executeConfigTransfer should not have been called")
|
|
}
|
|
comps.transfer.mu.Unlock()
|
|
}
|
|
|
|
func TestExecuteJob_ConfigLoadError(t *testing.T) {
|
|
comps := setupTestJobExecutor()
|
|
defer comps.logger.Close()
|
|
testJobID := uint(6)
|
|
dbErr := errors.New("db connection failed")
|
|
|
|
// Configure mocks
|
|
comps.db.GetConfigsForJobFunc = func(jobID uint) ([]db.TransferConfig, error) {
|
|
return nil, dbErr // Simulate error loading configs
|
|
}
|
|
|
|
comps.executor.executeJob(testJobID)
|
|
|
|
// Assertions
|
|
logOutput := comps.logBuf.String()
|
|
if !strings.Contains(logOutput, fmt.Sprintf("Error loading configurations for job %d: %v", testJobID, dbErr)) {
|
|
t.Errorf("Expected 'Error loading configurations' log message not found in output:\n%s", logOutput)
|
|
}
|
|
comps.transfer.mu.Lock()
|
|
if len(comps.transfer.executeConfigTransferCalls) > 0 {
|
|
t.Error("executeConfigTransfer should not have been called")
|
|
}
|
|
comps.transfer.mu.Unlock()
|
|
}
|
|
|
|
func TestExecuteJob_NoConfigs(t *testing.T) {
|
|
comps := setupTestJobExecutor()
|
|
defer comps.logger.Close()
|
|
testJobID := uint(7)
|
|
|
|
// Configure mocks
|
|
comps.db.GetConfigsForJobFunc = func(jobID uint) ([]db.TransferConfig, error) {
|
|
return []db.TransferConfig{}, nil // Simulate empty config list
|
|
}
|
|
|
|
comps.executor.executeJob(testJobID)
|
|
|
|
// Assertions
|
|
logOutput := comps.logBuf.String()
|
|
if !strings.Contains(logOutput, fmt.Sprintf("Error: job %d has no associated configurations", testJobID)) {
|
|
t.Errorf("Expected 'no associated configurations' log message not found in output:\n%s", logOutput)
|
|
}
|
|
comps.transfer.mu.Lock()
|
|
if len(comps.transfer.executeConfigTransferCalls) > 0 {
|
|
t.Error("executeConfigTransfer should not have been called")
|
|
}
|
|
comps.transfer.mu.Unlock()
|
|
}
|
|
|
|
func TestProcessConfiguration_Success(t *testing.T) {
|
|
comps := setupTestJobExecutor()
|
|
defer comps.logger.Close()
|
|
|
|
job := db.Job{ID: 1}
|
|
config := db.TransferConfig{ID: 10, Name: "Process Test"} // Corrected initialization
|
|
index := 1
|
|
totalConfigs := 1
|
|
|
|
comps.executor.processConfiguration(&job, &config, index, totalConfigs)
|
|
|
|
// Assertions
|
|
// 1. DB CreateJobHistory called
|
|
comps.db.mu.Lock()
|
|
if comps.db.createdHistory == nil {
|
|
t.Fatal("CreateJobHistory was not called")
|
|
}
|
|
if comps.db.createdHistory.JobID != job.ID {
|
|
t.Errorf("CreateJobHistory called with wrong JobID: got %d, want %d", comps.db.createdHistory.JobID, job.ID)
|
|
}
|
|
if comps.db.createdHistory.ConfigID != config.ID {
|
|
t.Errorf("CreateJobHistory called with wrong ConfigID: got %d, want %d", comps.db.createdHistory.ConfigID, config.ID)
|
|
}
|
|
if comps.db.createdHistory.Status != "running" {
|
|
t.Errorf("CreateJobHistory called with wrong Status: got %q, want 'running'", comps.db.createdHistory.Status)
|
|
}
|
|
comps.db.mu.Unlock()
|
|
|
|
// 2. Notifier SendNotifications called
|
|
comps.notifier.mu.Lock()
|
|
if len(comps.notifier.sendNotificationsCalls) != 1 {
|
|
t.Fatalf("Expected 1 call to SendNotifications, got %d", len(comps.notifier.sendNotificationsCalls))
|
|
}
|
|
callArgs := comps.notifier.sendNotificationsCalls[0]
|
|
if !reflect.DeepEqual(callArgs["job"], &job) {
|
|
t.Errorf("SendNotifications called with wrong job: got %+v, want %+v", callArgs["job"], &job)
|
|
}
|
|
// Compare history partially as StartTime is dynamic
|
|
if histArg, ok := callArgs["history"].(*db.JobHistory); !ok || histArg.JobID != job.ID || histArg.ConfigID != config.ID || histArg.Status != "running" {
|
|
t.Errorf("SendNotifications called with wrong history: got %+v", callArgs["history"])
|
|
}
|
|
if !reflect.DeepEqual(callArgs["config"], &config) {
|
|
t.Errorf("SendNotifications called with wrong config: got %+v, want %+v", callArgs["config"], &config)
|
|
}
|
|
comps.notifier.mu.Unlock()
|
|
|
|
// 3. TransferExecutor executeConfigTransfer called
|
|
comps.transfer.mu.Lock()
|
|
if len(comps.transfer.executeConfigTransferCalls) != 1 {
|
|
t.Fatalf("Expected 1 call to executeConfigTransfer, got %d", len(comps.transfer.executeConfigTransferCalls))
|
|
}
|
|
transferCallArgs := comps.transfer.executeConfigTransferCalls[0]
|
|
// Need to compare job/config by value as they are passed by value to transferExecutor
|
|
if !reflect.DeepEqual(transferCallArgs["job"], job) {
|
|
t.Errorf("executeConfigTransfer called with wrong job: got %+v, want %+v", transferCallArgs["job"], job)
|
|
}
|
|
if !reflect.DeepEqual(transferCallArgs["config"], config) {
|
|
t.Errorf("executeConfigTransfer called with wrong config: got %+v, want %+v", transferCallArgs["config"], config)
|
|
}
|
|
// Compare history partially
|
|
if histArg, ok := transferCallArgs["history"].(*db.JobHistory); !ok || histArg.JobID != job.ID || histArg.ConfigID != config.ID || histArg.Status != "running" {
|
|
t.Errorf("executeConfigTransfer called with wrong history: got %+v", transferCallArgs["history"])
|
|
}
|
|
comps.transfer.mu.Unlock()
|
|
}
|
|
|
|
func TestProcessConfiguration_HistoryError(t *testing.T) {
|
|
comps := setupTestJobExecutor()
|
|
defer comps.logger.Close()
|
|
|
|
job := db.Job{ID: 1}
|
|
config := db.TransferConfig{ID: 10, Name: "History Error Test"} // Corrected initialization
|
|
index := 1
|
|
totalConfigs := 1
|
|
dbErr := errors.New("failed to create history")
|
|
|
|
// Configure mock
|
|
comps.db.CreateJobHistoryFunc = func(history *db.JobHistory) error {
|
|
return dbErr
|
|
}
|
|
|
|
comps.executor.processConfiguration(&job, &config, index, totalConfigs)
|
|
|
|
// Assertions
|
|
// 1. Check log for error
|
|
logOutput := comps.logBuf.String()
|
|
if !strings.Contains(logOutput, fmt.Sprintf("Error creating job history for job %d, config %d: %v", job.ID, config.ID, dbErr)) {
|
|
t.Errorf("Expected 'Error creating job history' log message not found in output:\n%s", logOutput)
|
|
}
|
|
|
|
// 2. Ensure Notifier and TransferExecutor were NOT called
|
|
comps.notifier.mu.Lock()
|
|
if len(comps.notifier.sendNotificationsCalls) > 0 {
|
|
t.Error("SendNotifications should not have been called after history error")
|
|
}
|
|
comps.notifier.mu.Unlock()
|
|
|
|
comps.transfer.mu.Lock()
|
|
if len(comps.transfer.executeConfigTransferCalls) > 0 {
|
|
t.Error("executeConfigTransfer should not have been called after history error")
|
|
}
|
|
comps.transfer.mu.Unlock()
|
|
}
|