github.com/hashicorp/packer@v1.14.3/command/build_parallel_test.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: BUSL-1.1
     3  
     4  package command
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"path/filepath"
    10  	"sync"
    11  	"testing"
    12  
    13  	"github.com/hashicorp/hcl/v2/hcldec"
    14  
    15  	"golang.org/x/sync/errgroup"
    16  
    17  	packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
    18  	"github.com/hashicorp/packer/builder/file"
    19  	"github.com/hashicorp/packer/packer"
    20  	"github.com/hashicorp/packer/provisioner/sleep"
    21  )
    22  
    23  // NewParallelTestBuilder will return a New ParallelTestBuilder that will
    24  // unlock after `runs` builds
    25  func NewParallelTestBuilder(runs int) *ParallelTestBuilder {
    26  	pb := &ParallelTestBuilder{}
    27  	pb.wg.Add(runs)
    28  	return pb
    29  }
    30  
    31  // The ParallelTestBuilder's first run will lock
    32  type ParallelTestBuilder struct {
    33  	wg sync.WaitGroup
    34  }
    35  
    36  func (b *ParallelTestBuilder) ConfigSpec() hcldec.ObjectSpec { return nil }
    37  
    38  func (b *ParallelTestBuilder) Prepare(raws ...interface{}) ([]string, []string, error) {
    39  	return nil, nil, nil
    40  }
    41  
    42  func (b *ParallelTestBuilder) Run(ctx context.Context, ui packersdk.Ui, hook packersdk.Hook) (packersdk.Artifact, error) {
    43  	ui.Say("building")
    44  	b.wg.Done()
    45  	return nil, nil
    46  }
    47  
    48  // LockedBuilder won't run until unlock is called
    49  type LockedBuilder struct{ unlock chan interface{} }
    50  
    51  func (b *LockedBuilder) ConfigSpec() hcldec.ObjectSpec { return nil }
    52  
    53  func (b *LockedBuilder) Prepare(raws ...interface{}) ([]string, []string, error) {
    54  	return nil, nil, nil
    55  }
    56  
    57  func (b *LockedBuilder) Run(ctx context.Context, ui packersdk.Ui, hook packersdk.Hook) (packersdk.Artifact, error) {
    58  	ui.Say("locking build")
    59  	select {
    60  	case <-b.unlock:
    61  	case <-ctx.Done():
    62  		return nil, ctx.Err()
    63  	}
    64  	return nil, nil
    65  }
    66  
    67  // testMetaFile creates a Meta object that includes a file builder
    68  func testMetaParallel(t *testing.T, builder *ParallelTestBuilder, locked *LockedBuilder) Meta {
    69  	var out, err bytes.Buffer
    70  	return Meta{
    71  		CoreConfig: &packer.CoreConfig{
    72  			Components: packer.ComponentFinder{
    73  				PluginConfig: &packer.PluginConfig{
    74  					Builders: packer.MapOfBuilder{
    75  						"parallel-test": func() (packersdk.Builder, error) { return builder, nil },
    76  						"file":          func() (packersdk.Builder, error) { return &file.Builder{}, nil },
    77  						"lock":          func() (packersdk.Builder, error) { return locked, nil },
    78  					},
    79  					Provisioners: packer.MapOfProvisioner{
    80  						"sleep": func() (packersdk.Provisioner, error) { return &sleep.Provisioner{}, nil },
    81  					},
    82  				},
    83  			},
    84  		},
    85  		Ui: &packersdk.BasicUi{
    86  			Writer:      &out,
    87  			ErrorWriter: &err,
    88  		},
    89  	}
    90  }
    91  
    92  func TestBuildParallel_1(t *testing.T) {
    93  	// testfile has 6 builds, with first one locks 'forever', other builds
    94  	// should go through.
    95  	b := NewParallelTestBuilder(5)
    96  	locked := &LockedBuilder{unlock: make(chan interface{})}
    97  
    98  	c := &BuildCommand{
    99  		Meta: testMetaParallel(t, b, locked),
   100  	}
   101  
   102  	args := []string{
   103  		"-parallel-builds=10",
   104  		filepath.Join(testFixture("parallel"), "1lock-5wg.json"),
   105  	}
   106  
   107  	wg := errgroup.Group{}
   108  
   109  	wg.Go(func() error {
   110  		if code := c.Run(args); code != 0 {
   111  			fatalCommand(t, c.Meta)
   112  		}
   113  		return nil
   114  	})
   115  
   116  	b.wg.Wait()          // ran 5 times
   117  	close(locked.unlock) // unlock locking one
   118  	wg.Wait()            // wait for termination
   119  }
   120  
   121  func TestBuildParallel_2(t *testing.T) {
   122  	// testfile has 6 builds, 2 of them lock 'forever', other builds
   123  	// should go through.
   124  	b := NewParallelTestBuilder(4)
   125  	locked := &LockedBuilder{unlock: make(chan interface{})}
   126  
   127  	c := &BuildCommand{
   128  		Meta: testMetaParallel(t, b, locked),
   129  	}
   130  
   131  	args := []string{
   132  		"-parallel-builds=3",
   133  		filepath.Join(testFixture("parallel"), "2lock-4wg.json"),
   134  	}
   135  
   136  	wg := errgroup.Group{}
   137  
   138  	wg.Go(func() error {
   139  		if code := c.Run(args); code != 0 {
   140  			fatalCommand(t, c.Meta)
   141  		}
   142  		return nil
   143  	})
   144  
   145  	b.wg.Wait()          // ran 4 times
   146  	close(locked.unlock) // unlock locking one
   147  	wg.Wait()            // wait for termination
   148  }
   149  
   150  func TestBuildParallel_Timeout(t *testing.T) {
   151  	// testfile has 6 builds, 1 of them locks 'forever', one locks and times
   152  	// out other builds should go through.
   153  	b := NewParallelTestBuilder(4)
   154  	locked := &LockedBuilder{unlock: make(chan interface{})}
   155  
   156  	c := &BuildCommand{
   157  		Meta: testMetaParallel(t, b, locked),
   158  	}
   159  
   160  	args := []string{
   161  		"-parallel-builds=3",
   162  		filepath.Join(testFixture("parallel"), "2lock-timeout.json"),
   163  	}
   164  
   165  	wg := errgroup.Group{}
   166  
   167  	wg.Go(func() error {
   168  		if code := c.Run(args); code == 0 {
   169  			fatalCommand(t, c.Meta)
   170  		}
   171  		return nil
   172  	})
   173  
   174  	b.wg.Wait()          // ran 4 times
   175  	close(locked.unlock) // unlock locking one
   176  	wg.Wait()            // wait for termination
   177  }