blob: 9357f05b5594ecad23399f03cb65eb6c83965739 [file] [log] [blame]
package testutil
import (
"context"
"fmt"
"time"
"github.com/apache/airavata/scheduler/adapters"
"github.com/apache/airavata/scheduler/core/domain"
ports "github.com/apache/airavata/scheduler/core/port"
)
// TestDataBuilder provides a fluent interface for creating test data
type TestDataBuilder struct {
repo ports.RepositoryPort
db *adapters.PostgresAdapter
}
// NewTestDataBuilder creates a new test data builder
func NewTestDataBuilder(db *adapters.PostgresAdapter) *TestDataBuilder {
repo := adapters.NewRepository(db)
return &TestDataBuilder{
repo: repo,
db: db,
}
}
// UserBuilder builds user test data
type UserBuilder struct {
builder *TestDataBuilder
user *domain.User
}
// ProjectBuilder builds project test data
type ProjectBuilder struct {
builder *TestDataBuilder
project *domain.Project
}
// ComputeResourceBuilder builds compute resource test data
type ComputeResourceBuilder struct {
builder *TestDataBuilder
resource *domain.ComputeResource
}
// StorageResourceBuilder builds storage resource test data
type StorageResourceBuilder struct {
builder *TestDataBuilder
resource *domain.StorageResource
}
// CredentialBuilder builds credential test data
type CredentialBuilder struct {
builder *TestDataBuilder
credential *domain.Credential
}
// ExperimentBuilder builds experiment test data
type ExperimentBuilder struct {
builder *TestDataBuilder
experiment *domain.Experiment
}
// TaskBuilder builds task test data
type TaskBuilder struct {
builder *TestDataBuilder
task *domain.Task
}
// WorkerBuilder builds worker test data
type WorkerBuilder struct {
builder *TestDataBuilder
worker *domain.Worker
}
// User methods
func (tdb *TestDataBuilder) CreateUser(username, email string, isAdmin bool) *UserBuilder {
user := &domain.User{
ID: fmt.Sprintf("user-%d", time.Now().UnixNano()),
Username: username,
Email: email,
FullName: username,
IsActive: true,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
// Set admin status in metadata
if user.Metadata == nil {
user.Metadata = make(map[string]interface{})
}
user.Metadata["isAdmin"] = isAdmin
return &UserBuilder{
builder: tdb,
user: user,
}
}
// ID returns the user ID
func (ub *UserBuilder) ID() string {
return ub.user.ID
}
// WithID sets the user ID
func (ub *UserBuilder) WithID(id string) *UserBuilder {
ub.user.ID = id
return ub
}
// Build persists the user and returns it
func (ub *UserBuilder) Build() (*domain.User, error) {
if err := ub.builder.repo.CreateUser(context.Background(), ub.user); err != nil {
return nil, fmt.Errorf("failed to create user: %w", err)
}
return ub.user, nil
}
func (ub *UserBuilder) WithEmail(email string) *UserBuilder {
ub.user.Email = email
return ub
}
func (ub *UserBuilder) WithAdmin(isAdmin bool) *UserBuilder {
// Note: User model doesn't have IsAdmin field, using metadata instead
if ub.user.Metadata == nil {
ub.user.Metadata = make(map[string]interface{})
}
ub.user.Metadata["isAdmin"] = isAdmin
return ub
}
// Project methods
func (ub *UserBuilder) CreateProject(name, description string) *ProjectBuilder {
project := &domain.Project{
ID: fmt.Sprintf("project-%d", time.Now().UnixNano()),
Name: name,
Description: description,
OwnerID: ub.user.ID,
IsActive: true,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
return &ProjectBuilder{
builder: ub.builder,
project: project,
}
}
// CreateProject creates a project with a specific user ID
func (tdb *TestDataBuilder) CreateProject(name, description, userID string) *ProjectBuilder {
project := &domain.Project{
ID: fmt.Sprintf("project-%d", time.Now().UnixNano()),
Name: name,
Description: description,
OwnerID: userID,
IsActive: true,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
return &ProjectBuilder{
builder: tdb,
project: project,
}
}
func (pb *ProjectBuilder) WithID(id string) *ProjectBuilder {
pb.project.ID = id
return pb
}
func (pb *ProjectBuilder) WithName(name string) *ProjectBuilder {
pb.project.Name = name
return pb
}
func (pb *ProjectBuilder) Build() (*domain.Project, error) {
if err := pb.builder.repo.CreateProject(context.Background(), pb.project); err != nil {
return nil, fmt.Errorf("failed to create project: %w", err)
}
return pb.project, nil
}
func (pb *ProjectBuilder) WithDescription(description string) *ProjectBuilder {
pb.project.Description = description
return pb
}
// ComputeResource methods
func (ub *UserBuilder) CreateComputeResource(name, resourceType, endpoint string) (*ComputeResourceBuilder, error) {
resource := &domain.ComputeResource{
ID: fmt.Sprintf("compute-%d", time.Now().UnixNano()),
Name: name,
Type: domain.ComputeResourceType(resourceType),
Endpoint: endpoint,
Status: domain.ResourceStatusActive,
CostPerHour: 1.0,
MaxWorkers: 10,
CurrentWorkers: 0,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
Metadata: make(map[string]interface{}),
}
if err := ub.builder.repo.CreateComputeResource(context.Background(), resource); err != nil {
return nil, fmt.Errorf("failed to create compute resource: %w", err)
}
return &ComputeResourceBuilder{
builder: ub.builder,
resource: resource,
}, nil
}
func (crb *ComputeResourceBuilder) WithID(id string) *ComputeResourceBuilder {
crb.resource.ID = id
return crb
}
func (crb *ComputeResourceBuilder) WithType(resourceType string) *ComputeResourceBuilder {
crb.resource.Type = domain.ComputeResourceType(resourceType)
return crb
}
func (crb *ComputeResourceBuilder) WithEndpoint(endpoint string) *ComputeResourceBuilder {
crb.resource.Endpoint = endpoint
return crb
}
func (crb *ComputeResourceBuilder) WithStatus(status string) *ComputeResourceBuilder {
crb.resource.Status = domain.ResourceStatus(status)
return crb
}
func (crb *ComputeResourceBuilder) WithMetadata(key string, value interface{}) *ComputeResourceBuilder {
if crb.resource.Metadata == nil {
crb.resource.Metadata = make(map[string]interface{})
}
crb.resource.Metadata[key] = value
return crb
}
func (crb *ComputeResourceBuilder) Build() (*domain.ComputeResource, error) {
if err := crb.builder.repo.UpdateComputeResource(context.Background(), crb.resource); err != nil {
return nil, fmt.Errorf("failed to update compute resource: %w", err)
}
return crb.resource, nil
}
// StorageResource methods
func (ub *UserBuilder) CreateStorageResource(name, resourceType, endpoint string) (*StorageResourceBuilder, error) {
capacity := int64(1000000000) // 1GB
resource := &domain.StorageResource{
ID: fmt.Sprintf("storage-%d", time.Now().UnixNano()),
Name: name,
Type: domain.StorageResourceType(resourceType),
Endpoint: endpoint,
OwnerID: "test-user",
Status: domain.ResourceStatusActive,
TotalCapacity: &capacity,
UsedCapacity: nil,
AvailableCapacity: &capacity,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
Metadata: make(map[string]interface{}),
}
if err := ub.builder.repo.CreateStorageResource(context.Background(), resource); err != nil {
return nil, fmt.Errorf("failed to create storage resource: %w", err)
}
return &StorageResourceBuilder{
builder: ub.builder,
resource: resource,
}, nil
}
func (srb *StorageResourceBuilder) WithID(id string) *StorageResourceBuilder {
srb.resource.ID = id
return srb
}
func (srb *StorageResourceBuilder) WithType(resourceType string) *StorageResourceBuilder {
srb.resource.Type = domain.StorageResourceType(resourceType)
return srb
}
func (srb *StorageResourceBuilder) WithEndpoint(endpoint string) *StorageResourceBuilder {
srb.resource.Endpoint = endpoint
return srb
}
func (srb *StorageResourceBuilder) WithStatus(status string) *StorageResourceBuilder {
srb.resource.Status = domain.ResourceStatus(status)
return srb
}
func (srb *StorageResourceBuilder) WithMetadata(key string, value interface{}) *StorageResourceBuilder {
if srb.resource.Metadata == nil {
srb.resource.Metadata = make(map[string]interface{})
}
srb.resource.Metadata[key] = value
return srb
}
func (srb *StorageResourceBuilder) Build() (*domain.StorageResource, error) {
if err := srb.builder.repo.UpdateStorageResource(context.Background(), srb.resource); err != nil {
return nil, fmt.Errorf("failed to update storage resource: %w", err)
}
return srb.resource, nil
}
// Credential methods
func (ub *UserBuilder) CreateCredential(name, credentialType string, data []byte) (*CredentialBuilder, error) {
// For now, we'll create a simple credential object without persisting to database
// since credentials are now stored in OpenBao
credential := &domain.Credential{
ID: fmt.Sprintf("cred-%d", time.Now().UnixNano()),
Name: name,
Type: domain.CredentialType(credentialType),
OwnerID: ub.user.ID,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
return &CredentialBuilder{
builder: ub.builder,
credential: credential,
}, nil
}
// CreateSSHCredential creates an SSH key credential in vault
func (ub *UserBuilder) CreateSSHCredential(name string, privateKey []byte) (*CredentialBuilder, error) {
return ub.CreateCredential(name, string(domain.CredentialTypeSSHKey), privateKey)
}
func (cb *CredentialBuilder) WithID(id string) *CredentialBuilder {
cb.credential.ID = id
return cb
}
func (cb *CredentialBuilder) WithName(name string) *CredentialBuilder {
cb.credential.Name = name
return cb
}
func (cb *CredentialBuilder) WithType(credentialType string) *CredentialBuilder {
cb.credential.Type = domain.CredentialType(credentialType)
return cb
}
func (cb *CredentialBuilder) WithData(data []byte) *CredentialBuilder {
// Note: Data is now stored in OpenBao, not in the credential object
return cb
}
func (cb *CredentialBuilder) Build() (*domain.Credential, error) {
// For now, we'll just return the credential object without persisting to database
// since credentials are now stored in OpenBao
return cb.credential, nil
}
// Experiment methods
func (pb *ProjectBuilder) CreateExperiment(name, description, commandTemplate string) *ExperimentBuilder {
experiment := &domain.Experiment{
ID: fmt.Sprintf("exp-%d", time.Now().UnixNano()),
Name: name,
Description: description,
ProjectID: pb.project.ID,
OwnerID: pb.project.OwnerID,
Status: domain.ExperimentStatusCreated,
CommandTemplate: commandTemplate,
OutputPattern: "output_{task_id}.txt",
Parameters: []domain.ParameterSet{},
Requirements: &domain.ResourceRequirements{},
Constraints: &domain.ExperimentConstraints{},
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
Metadata: make(map[string]interface{}),
}
return &ExperimentBuilder{
builder: pb.builder,
experiment: experiment,
}
}
func (eb *ExperimentBuilder) WithID(id string) *ExperimentBuilder {
eb.experiment.ID = id
return eb
}
func (eb *ExperimentBuilder) WithName(name string) *ExperimentBuilder {
eb.experiment.Name = name
return eb
}
func (eb *ExperimentBuilder) WithStatus(status string) *ExperimentBuilder {
eb.experiment.Status = domain.ExperimentStatus(status)
return eb
}
func (eb *ExperimentBuilder) WithCommandTemplate(template string) *ExperimentBuilder {
eb.experiment.CommandTemplate = template
return eb
}
func (eb *ExperimentBuilder) WithParameters(parameters []domain.ParameterSet) *ExperimentBuilder {
eb.experiment.Parameters = parameters
return eb
}
func (eb *ExperimentBuilder) WithRequirements(requirements *domain.ResourceRequirements) *ExperimentBuilder {
eb.experiment.Requirements = requirements
return eb
}
func (eb *ExperimentBuilder) WithConstraints(constraints *domain.ExperimentConstraints) *ExperimentBuilder {
eb.experiment.Constraints = constraints
return eb
}
func (eb *ExperimentBuilder) WithMetadata(key string, value interface{}) *ExperimentBuilder {
if eb.experiment.Metadata == nil {
eb.experiment.Metadata = make(map[string]interface{})
}
eb.experiment.Metadata[key] = value
return eb
}
func (eb *ExperimentBuilder) Build() (*domain.Experiment, error) {
if err := eb.builder.repo.CreateExperiment(context.Background(), eb.experiment); err != nil {
return nil, fmt.Errorf("failed to create experiment: %w", err)
}
return eb.experiment, nil
}
// Task methods
func (eb *ExperimentBuilder) CreateTask(command string) *TaskBuilder {
task := &domain.Task{
ID: fmt.Sprintf("task-%d", time.Now().UnixNano()),
ExperimentID: eb.experiment.ID,
Status: domain.TaskStatusQueued,
Command: command,
InputFiles: []domain.FileMetadata{},
OutputFiles: []domain.FileMetadata{},
RetryCount: 0,
MaxRetries: 3,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
return &TaskBuilder{
builder: eb.builder,
task: task,
}
}
func (tb *TaskBuilder) WithID(id string) *TaskBuilder {
tb.task.ID = id
return tb
}
func (tb *TaskBuilder) WithStatus(status string) *TaskBuilder {
tb.task.Status = domain.TaskStatus(status)
return tb
}
func (tb *TaskBuilder) WithCommand(command string) *TaskBuilder {
tb.task.Command = command
return tb
}
func (tb *TaskBuilder) WithWorkerID(workerID string) *TaskBuilder {
tb.task.WorkerID = workerID
return tb
}
func (tb *TaskBuilder) WithInputFiles(files []domain.FileMetadata) *TaskBuilder {
tb.task.InputFiles = files
return tb
}
func (tb *TaskBuilder) WithOutputFiles(files []domain.FileMetadata) *TaskBuilder {
tb.task.OutputFiles = files
return tb
}
func (tb *TaskBuilder) Build() (*domain.Task, error) {
if err := tb.builder.repo.CreateTask(context.Background(), tb.task); err != nil {
return nil, fmt.Errorf("failed to create task: %w", err)
}
return tb.task, nil
}
// Worker methods
func (crb *ComputeResourceBuilder) CreateWorker(experimentID string, walltime time.Duration) *WorkerBuilder {
worker := &domain.Worker{
ID: fmt.Sprintf("worker-%d", time.Now().UnixNano()),
ComputeResourceID: crb.resource.ID,
ExperimentID: experimentID,
Status: domain.WorkerStatusIdle,
Walltime: walltime,
WalltimeRemaining: walltime,
LastHeartbeat: time.Now(),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
return &WorkerBuilder{
builder: crb.builder,
worker: worker,
}
}
func (wb *WorkerBuilder) WithID(id string) *WorkerBuilder {
wb.worker.ID = id
return wb
}
func (wb *WorkerBuilder) WithStatus(status string) *WorkerBuilder {
wb.worker.Status = domain.WorkerStatus(status)
return wb
}
func (wb *WorkerBuilder) WithWalltime(walltime time.Duration) *WorkerBuilder {
wb.worker.Walltime = walltime
wb.worker.WalltimeRemaining = walltime
return wb
}
func (wb *WorkerBuilder) WithWalltimeRemaining(remaining time.Duration) *WorkerBuilder {
wb.worker.WalltimeRemaining = remaining
return wb
}
func (wb *WorkerBuilder) WithLastHeartbeat(heartbeat time.Time) *WorkerBuilder {
wb.worker.LastHeartbeat = heartbeat
return wb
}
func (wb *WorkerBuilder) Build() (*domain.Worker, error) {
if err := wb.builder.repo.CreateWorker(context.Background(), wb.worker); err != nil {
return nil, fmt.Errorf("failed to create worker: %w", err)
}
return wb.worker, nil
}
// Convenience methods for common test scenarios
func (tdb *TestDataBuilder) CreateUserWithProject(username, email, projectName string) (*domain.User, *domain.Project, error) {
userBuilder := tdb.CreateUser(username, email, false)
user, err := userBuilder.Build()
if err != nil {
return nil, nil, err
}
projectBuilder := userBuilder.CreateProject(projectName, "Test project")
project, err := projectBuilder.Build()
if err != nil {
return nil, nil, err
}
return user, project, nil
}
func (tdb *TestDataBuilder) CreateExperimentWithTasks(userID, projectID, experimentName string, numTasks int) (*domain.Experiment, []*domain.Task, error) {
// Get project
project, err := tdb.repo.GetProjectByID(context.Background(), projectID)
if err != nil {
return nil, nil, err
}
// Create experiment
experimentBuilder := (&ProjectBuilder{builder: tdb, project: project}).CreateExperiment(experimentName, "Test experiment", "echo test")
experiment, err := experimentBuilder.Build()
if err != nil {
return nil, nil, err
}
// Create tasks
var tasks []*domain.Task
for i := 0; i < numTasks; i++ {
taskBuilder := experimentBuilder.CreateTask(fmt.Sprintf("echo task_%d", i))
task, err := taskBuilder.Build()
if err != nil {
return nil, nil, err
}
tasks = append(tasks, task)
}
return experiment, tasks, nil
}