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 }