lifecycle/phase/detector_test.go

2051 lines
66 KiB
Go

package phase_test
import (
"errors"
"io"
"reflect"
"strings"
"sync"
"testing"
apexlog "github.com/apex/log"
"github.com/apex/log/handlers/memory"
"github.com/golang/mock/gomock"
"github.com/google/go-cmp/cmp"
"github.com/sclevine/spec"
"github.com/sclevine/spec/report"
"github.com/buildpacks/lifecycle/api"
"github.com/buildpacks/lifecycle/buildpack"
"github.com/buildpacks/lifecycle/log"
"github.com/buildpacks/lifecycle/phase"
"github.com/buildpacks/lifecycle/phase/testmock"
"github.com/buildpacks/lifecycle/platform"
"github.com/buildpacks/lifecycle/platform/files"
h "github.com/buildpacks/lifecycle/testhelpers"
)
func TestDetector(t *testing.T) {
spec.Run(t, "Detector", testDetector, spec.Report(report.Terminal{}))
}
func testDetector(t *testing.T, when spec.G, it spec.S) {
var (
mockController *gomock.Controller
apiVerifier *testmock.MockBuildpackAPIVerifier
configHandler *testmock.MockConfigHandler
dirStore *testmock.MockDirStore
logger log.LoggerHandlerWithLevel
detectorFactory *phase.HermeticFactory
)
it.Before(func() {
mockController = gomock.NewController(t)
apiVerifier = testmock.NewMockBuildpackAPIVerifier(mockController)
configHandler = testmock.NewMockConfigHandler(mockController)
dirStore = testmock.NewMockDirStore(mockController)
logger = log.NewDefaultLogger(io.Discard)
detectorFactory = phase.NewHermeticFactory(
api.Platform.Latest(),
apiVerifier,
configHandler,
dirStore,
)
// Mock ReadSystem for Platform API >= 0.15 (returns empty by default)
if api.Platform.Latest().AtLeast("0.15") {
configHandler.EXPECT().ReadSystem(gomock.Any(), gomock.Any()).Return(files.System{}, nil).AnyTimes()
}
})
it.After(func() {
mockController.Finish()
})
when("#NewDetector", func() {
it.Before(func() {
configHandler.EXPECT().ReadAnalyzed("some-analyzed-path", gomock.Any()).Return(files.Analyzed{}, nil).AnyTimes()
})
it("configures the detector", func() {
order := buildpack.Order{
buildpack.Group{Group: []buildpack.GroupElement{{ID: "A", Version: "v1"}}},
}
configHandler.EXPECT().ReadOrder("some-order-path").Return(order, nil, nil)
t.Log("verifies buildpack apis")
bpA1 := &buildpack.BpDescriptor{WithAPI: "0.2"}
dirStore.EXPECT().Lookup(buildpack.KindBuildpack, "A", "v1").Return(bpA1, nil)
apiVerifier.EXPECT().VerifyBuildpackAPI(buildpack.KindBuildpack, "A@v1", "0.2", logger)
detector, err := detectorFactory.NewDetector(platform.LifecycleInputs{
AnalyzedPath: "some-analyzed-path",
AppDir: "some-app-dir",
BuildConfigDir: "some-build-config-dir",
OrderPath: "some-order-path",
PlatformDir: "some-platform-dir",
}, logger)
h.AssertNil(t, err)
h.AssertEq(t, detector.AppDir, "some-app-dir")
h.AssertEq(t, detector.BuildConfigDir, "some-build-config-dir")
h.AssertNotNil(t, detector.DirStore)
h.AssertEq(t, detector.HasExtensions, false)
h.AssertEq(t, detector.Order, order)
h.AssertEq(t, detector.PlatformDir, "some-platform-dir")
_, ok := detector.Resolver.(*phase.DefaultDetectResolver)
h.AssertEq(t, ok, true)
h.AssertNotNil(t, detector.Runs)
})
when("Platform API >= 0.15", func() {
it("reads and merges system buildpacks", func() {
configHandler = testmock.NewMockConfigHandler(mockController)
detectorFactory015 := phase.NewHermeticFactory(
api.MustParse("0.15"),
apiVerifier,
configHandler,
dirStore,
)
order := buildpack.Order{
buildpack.Group{Group: []buildpack.GroupElement{{ID: "A", Version: "v1"}}},
}
system := files.System{
Pre: files.SystemBuildpacks{
Buildpacks: []files.SystemBuildpack{
{ID: "pre-bp", Version: "0.5.0"},
},
},
Post: files.SystemBuildpacks{
Buildpacks: []files.SystemBuildpack{
{ID: "post-bp", Version: "9.0.0"},
},
},
}
configHandler.EXPECT().ReadAnalyzed("some-analyzed-path", gomock.Any()).Return(files.Analyzed{}, nil)
configHandler.EXPECT().ReadOrder("some-order-path").Return(order, nil, nil)
configHandler.EXPECT().ReadSystem("some-system-path", gomock.Any()).Return(system, nil)
t.Log("verifies buildpack apis")
preBp := &buildpack.BpDescriptor{WithAPI: "0.10"}
dirStore.EXPECT().Lookup(buildpack.KindBuildpack, "pre-bp", "0.5.0").Return(preBp, nil)
apiVerifier.EXPECT().VerifyBuildpackAPI(buildpack.KindBuildpack, "pre-bp@0.5.0", "0.10", logger)
bpA1 := &buildpack.BpDescriptor{WithAPI: "0.10"}
dirStore.EXPECT().Lookup(buildpack.KindBuildpack, "A", "v1").Return(bpA1, nil)
apiVerifier.EXPECT().VerifyBuildpackAPI(buildpack.KindBuildpack, "A@v1", "0.10", logger)
postBp := &buildpack.BpDescriptor{WithAPI: "0.10"}
dirStore.EXPECT().Lookup(buildpack.KindBuildpack, "post-bp", "9.0.0").Return(postBp, nil)
apiVerifier.EXPECT().VerifyBuildpackAPI(buildpack.KindBuildpack, "post-bp@9.0.0", "0.10", logger)
detector, err := detectorFactory015.NewDetector(platform.LifecycleInputs{
AnalyzedPath: "some-analyzed-path",
AppDir: "some-app-dir",
BuildConfigDir: "some-build-config-dir",
OrderPath: "some-order-path",
SystemPath: "some-system-path",
PlatformDir: "some-platform-dir",
}, logger)
h.AssertNil(t, err)
t.Log("system buildpacks are prepended and appended to the order")
h.AssertEq(t, len(detector.Order), 1)
h.AssertEq(t, len(detector.Order[0].Group), 3)
h.AssertEq(t, detector.Order[0].Group[0].ID, "pre-bp")
h.AssertEq(t, detector.Order[0].Group[0].Version, "0.5.0")
h.AssertEq(t, detector.Order[0].Group[1].ID, "A")
h.AssertEq(t, detector.Order[0].Group[1].Version, "v1")
h.AssertEq(t, detector.Order[0].Group[2].ID, "post-bp")
h.AssertEq(t, detector.Order[0].Group[2].Version, "9.0.0")
})
})
when("Platform API < 0.15", func() {
it("does not read system buildpacks", func() {
detectorFactory014 := phase.NewHermeticFactory(
api.MustParse("0.14"),
apiVerifier,
configHandler,
dirStore,
)
order := buildpack.Order{
buildpack.Group{Group: []buildpack.GroupElement{{ID: "A", Version: "v1"}}},
}
configHandler.EXPECT().ReadOrder("some-order-path").Return(order, nil, nil)
// NOTE: ReadSystem should NOT be called for Platform API < 0.15
t.Log("verifies buildpack apis")
bpA1 := &buildpack.BpDescriptor{WithAPI: "0.10"}
dirStore.EXPECT().Lookup(buildpack.KindBuildpack, "A", "v1").Return(bpA1, nil)
apiVerifier.EXPECT().VerifyBuildpackAPI(buildpack.KindBuildpack, "A@v1", "0.10", logger)
detector, err := detectorFactory014.NewDetector(platform.LifecycleInputs{
AnalyzedPath: "some-analyzed-path",
AppDir: "some-app-dir",
BuildConfigDir: "some-build-config-dir",
OrderPath: "some-order-path",
SystemPath: "some-system-path",
PlatformDir: "some-platform-dir",
}, logger)
h.AssertNil(t, err)
t.Log("system buildpacks are not merged")
h.AssertEq(t, len(detector.Order), 1)
h.AssertEq(t, len(detector.Order[0].Group), 1)
h.AssertEq(t, detector.Order[0].Group[0].ID, "A")
})
})
when("there are extensions", func() {
it("prepends the extensions order to the buildpacks order", func() {
orderBp := buildpack.Order{
buildpack.Group{Group: []buildpack.GroupElement{{ID: "A", Version: "v1"}}},
buildpack.Group{Group: []buildpack.GroupElement{{ID: "B", Version: "v1"}}},
}
orderExt := buildpack.Order{
buildpack.Group{Group: []buildpack.GroupElement{{ID: "C", Version: "v1", Extension: true}}},
buildpack.Group{Group: []buildpack.GroupElement{{ID: "D", Version: "v1", Extension: true}}},
}
expectedOrder := buildpack.Order{
buildpack.Group{
Group: []buildpack.GroupElement{
{OrderExtensions: buildpack.Order{
buildpack.Group{Group: []buildpack.GroupElement{{ID: "C", Version: "v1", Extension: true, Optional: true}}},
buildpack.Group{Group: []buildpack.GroupElement{{ID: "D", Version: "v1", Extension: true, Optional: true}}},
}},
{ID: "A", Version: "v1"},
},
},
buildpack.Group{
Group: []buildpack.GroupElement{
{OrderExtensions: buildpack.Order{
buildpack.Group{Group: []buildpack.GroupElement{{ID: "C", Version: "v1", Extension: true, Optional: true}}},
buildpack.Group{Group: []buildpack.GroupElement{{ID: "D", Version: "v1", Extension: true, Optional: true}}},
}},
{ID: "B", Version: "v1"},
},
},
}
configHandler.EXPECT().ReadOrder("some-order-path").Return(orderBp, orderExt, nil)
t.Log("verifies buildpack apis")
bpA1 := &buildpack.BpDescriptor{WithAPI: "some-api-version"}
bpB1 := &buildpack.BpDescriptor{WithAPI: "some-api-version"}
extC1 := &buildpack.BpDescriptor{WithAPI: "some-other-api-version"}
extD1 := &buildpack.BpDescriptor{WithAPI: "some-other-api-version"}
dirStore.EXPECT().Lookup(buildpack.KindBuildpack, "A", "v1").Return(bpA1, nil)
apiVerifier.EXPECT().VerifyBuildpackAPI(buildpack.KindBuildpack, "A@v1", "some-api-version", logger)
dirStore.EXPECT().Lookup(buildpack.KindBuildpack, "B", "v1").Return(bpB1, nil)
apiVerifier.EXPECT().VerifyBuildpackAPI(buildpack.KindBuildpack, "B@v1", "some-api-version", logger)
dirStore.EXPECT().Lookup(buildpack.KindExtension, "C", "v1").Return(extC1, nil)
apiVerifier.EXPECT().VerifyBuildpackAPI(buildpack.KindExtension, "C@v1", "some-other-api-version", logger)
dirStore.EXPECT().Lookup(buildpack.KindExtension, "D", "v1").Return(extD1, nil)
apiVerifier.EXPECT().VerifyBuildpackAPI(buildpack.KindExtension, "D@v1", "some-other-api-version", logger)
detector, err := detectorFactory.NewDetector(platform.LifecycleInputs{
AnalyzedPath: "some-analyzed-path",
AppDir: "some-app-dir",
BuildConfigDir: "some-build-config-dir",
OrderPath: "some-order-path",
PlatformDir: "some-platform-dir",
}, logger)
h.AssertNil(t, err)
h.AssertEq(t, detector.AppDir, "some-app-dir")
h.AssertNotNil(t, detector.DirStore)
h.AssertEq(t, detector.HasExtensions, true)
h.AssertEq(t, detector.Order, expectedOrder)
h.AssertEq(t, detector.PlatformDir, "some-platform-dir")
_, ok := detector.Resolver.(*phase.DefaultDetectResolver)
h.AssertEq(t, ok, true)
h.AssertNotNil(t, detector.Runs)
})
})
})
when(".Detect", func() {
var (
detector *phase.Detector
executor *testmock.MockDetectExecutor
resolver *testmock.MockDetectResolver
)
it.Before(func() {
configHandler.EXPECT().ReadAnalyzed("some-analyzed-path", gomock.Any()).Return(files.Analyzed{}, nil).AnyTimes()
configHandler.EXPECT().ReadOrder("some-order-path").Return(buildpack.Order{}, buildpack.Order{}, nil)
var err error
detector, err = detectorFactory.NewDetector(platform.LifecycleInputs{
AnalyzedPath: "some-analyzed-path",
AppDir: "some-app-dir",
BuildConfigDir: "some-build-config-dir",
OrderPath: "some-order-path",
PlatformDir: "some-platform-dir",
}, logger)
h.AssertNil(t, err)
// override factory-provided services
executor = testmock.NewMockDetectExecutor(mockController)
resolver = testmock.NewMockDetectResolver(mockController)
detector.Executor = executor
detector.Resolver = resolver
})
it("provides detect inputs to each group element", func() {
bpA1 := &buildpack.BpDescriptor{
Buildpack: buildpack.BpInfo{BaseInfo: buildpack.BaseInfo{ID: "A", Version: "v1"}},
}
dirStore.EXPECT().LookupBp("A", "v1").Return(bpA1, nil).AnyTimes()
executor.EXPECT().Detect(bpA1, gomock.Any(), gomock.Any()).Do(
func(_ buildpack.Descriptor, inputs buildpack.DetectInputs, _ log.Logger) buildpack.DetectOutputs {
h.AssertEq(t, inputs.AppDir, detector.AppDir)
h.AssertEq(t, inputs.BuildConfigDir, detector.BuildConfigDir)
h.AssertEq(t, inputs.PlatformDir, detector.PlatformDir)
return buildpack.DetectOutputs{}
})
group := []buildpack.GroupElement{
{ID: "A", Version: "v1", Optional: true},
}
resolver.EXPECT().Resolve(group, detector.Runs)
detector.Order = buildpack.Order{{Group: group}}
_, _, _ = detector.Detect()
})
it("passes through the CNB_TARGET_* env vars", func() {
bpA1 := &buildpack.BpDescriptor{
Buildpack: buildpack.BpInfo{BaseInfo: buildpack.BaseInfo{ID: "A", Version: "v1"}},
}
detector.AnalyzeMD = files.Analyzed{RunImage: &files.RunImage{TargetMetadata: &files.TargetMetadata{OS: "linux", Arch: "amd64"}}}
dirStore.EXPECT().LookupBp("A", "v1").Return(bpA1, nil).AnyTimes()
executor.EXPECT().Detect(bpA1, gomock.Any(), gomock.Any()).Do(
func(_ buildpack.Descriptor, inputs buildpack.DetectInputs, _ log.Logger) buildpack.DetectOutputs {
h.AssertContains(t, inputs.TargetEnv, "CNB_TARGET_ARCH=amd64")
h.AssertContains(t, inputs.TargetEnv, "CNB_TARGET_OS=linux")
return buildpack.DetectOutputs{}
})
group := []buildpack.GroupElement{
{ID: bpA1.Buildpack.ID, Version: bpA1.Buildpack.Version, API: bpA1.WithAPI, Optional: true},
}
resolver.EXPECT().Resolve(group, detector.Runs)
detector.Order = buildpack.Order{{Group: group}}
_, _, _ = detector.Detect()
})
it("expands order-containing buildpack IDs", func() {
// This test doesn't use gomock.InOrder() because each call to Detect() happens in a go func.
// The order that other calls are written in is the order that they happen in.
bpE1 := &buildpack.BpDescriptor{
Buildpack: buildpack.BpInfo{BaseInfo: buildpack.BaseInfo{ID: "E", Version: "v1"}},
Order: []buildpack.Group{
{
Group: []buildpack.GroupElement{
{ID: "A", Version: "v1"},
{ID: "F", Version: "v1"},
{ID: "B", Version: "v1"},
},
},
},
}
dirStore.EXPECT().LookupBp("E", "v1").Return(bpE1, nil).AnyTimes()
bpA1 := &buildpack.BpDescriptor{
Buildpack: buildpack.BpInfo{BaseInfo: buildpack.BaseInfo{ID: "A", Version: "v1"}},
}
dirStore.EXPECT().LookupBp("A", "v1").Return(bpA1, nil).AnyTimes()
bpF1 := &buildpack.BpDescriptor{
Buildpack: buildpack.BpInfo{BaseInfo: buildpack.BaseInfo{ID: "F", Version: "v1"}},
Order: []buildpack.Group{
{Group: []buildpack.GroupElement{
{ID: "C", Version: "v1"},
}},
{Group: []buildpack.GroupElement{
{ID: "G", Version: "v1", Optional: true},
}},
{Group: []buildpack.GroupElement{
{ID: "D", Version: "v1"},
}},
},
}
dirStore.EXPECT().LookupBp("F", "v1").Return(bpF1, nil).AnyTimes()
bpC1 := &buildpack.BpDescriptor{
Buildpack: buildpack.BpInfo{BaseInfo: buildpack.BaseInfo{ID: "C", Version: "v1"}},
}
dirStore.EXPECT().LookupBp("C", "v1").Return(bpC1, nil).AnyTimes()
bpB1 := &buildpack.BpDescriptor{
Buildpack: buildpack.BpInfo{BaseInfo: buildpack.BaseInfo{ID: "B", Version: "v1"}},
}
dirStore.EXPECT().LookupBp("B", "v1").Return(bpB1, nil).AnyTimes()
bpG1 := &buildpack.BpDescriptor{
Buildpack: buildpack.BpInfo{BaseInfo: buildpack.BaseInfo{ID: "G", Version: "v1"}},
Order: []buildpack.Group{
{
Group: []buildpack.GroupElement{
{ID: "A", Version: "v2"},
{ID: "B", Version: "v2"},
},
},
{
Group: []buildpack.GroupElement{
{ID: "C", Version: "v2"},
{ID: "D", Version: "v2"},
},
},
},
}
dirStore.EXPECT().LookupBp("G", "v1").Return(bpG1, nil).AnyTimes()
bpB2 := &buildpack.BpDescriptor{
Buildpack: buildpack.BpInfo{BaseInfo: buildpack.BaseInfo{ID: "B", Version: "v2"}},
}
dirStore.EXPECT().LookupBp("B", "v2").Return(bpB2, nil).AnyTimes()
bpC2 := &buildpack.BpDescriptor{
Buildpack: buildpack.BpInfo{BaseInfo: buildpack.BaseInfo{ID: "C", Version: "v2"}},
}
dirStore.EXPECT().LookupBp("C", "v2").Return(bpC2, nil).AnyTimes()
bpD2 := &buildpack.BpDescriptor{
Buildpack: buildpack.BpInfo{BaseInfo: buildpack.BaseInfo{ID: "D", Version: "v2"}},
}
dirStore.EXPECT().LookupBp("D", "v2").Return(bpD2, nil).AnyTimes()
bpD1 := &buildpack.BpDescriptor{
Buildpack: buildpack.BpInfo{BaseInfo: buildpack.BaseInfo{ID: "D", Version: "v1"}},
}
dirStore.EXPECT().LookupBp("D", "v1").Return(bpD1, nil).AnyTimes()
executor.EXPECT().Detect(bpA1, gomock.Any(), gomock.Any())
executor.EXPECT().Detect(bpC1, gomock.Any(), gomock.Any())
executor.EXPECT().Detect(bpB1, gomock.Any(), gomock.Any())
firstGroup := []buildpack.GroupElement{
{ID: "A", Version: "v1"},
{ID: "C", Version: "v1"},
{ID: "B", Version: "v1"},
}
firstResolve := resolver.EXPECT().Resolve(
firstGroup,
detector.Runs,
).Return(
[]buildpack.GroupElement{},
[]files.BuildPlanEntry{},
phase.ErrFailedDetection,
)
// bpA1 already done
executor.EXPECT().Detect(bpB2, gomock.Any(), gomock.Any())
secondGroup := []buildpack.GroupElement{
{ID: "A", Version: "v1"},
{ID: "B", Version: "v2"},
}
secondResolve := resolver.EXPECT().Resolve(
secondGroup,
detector.Runs,
).Return(
[]buildpack.GroupElement{},
[]files.BuildPlanEntry{},
phase.ErrFailedDetection,
).After(firstResolve)
// bpA1 already done
executor.EXPECT().Detect(bpC2, gomock.Any(), gomock.Any())
executor.EXPECT().Detect(bpD2, gomock.Any(), gomock.Any())
// bpB1 already done
thirdGroup := []buildpack.GroupElement{
{ID: "A", Version: "v1"},
{ID: "C", Version: "v2"},
{ID: "D", Version: "v2"},
{ID: "B", Version: "v1"},
}
thirdResolve := resolver.EXPECT().Resolve(
thirdGroup,
detector.Runs,
).Return(
[]buildpack.GroupElement{},
[]files.BuildPlanEntry{},
phase.ErrFailedDetection,
).After(secondResolve)
// bpA1 already done
// bpB1 already done
fourthGroup := []buildpack.GroupElement{
{ID: "A", Version: "v1"},
{ID: "B", Version: "v1"},
}
fourthResolve := resolver.EXPECT().Resolve(
fourthGroup,
detector.Runs,
).Return(
[]buildpack.GroupElement{},
[]files.BuildPlanEntry{},
phase.ErrFailedDetection,
).After(thirdResolve)
// bpA1 already done
executor.EXPECT().Detect(bpD1, gomock.Any(), gomock.Any())
// bpB1 already done
fifthGroup := []buildpack.GroupElement{
{ID: "A", Version: "v1"},
{ID: "D", Version: "v1"},
{ID: "B", Version: "v1"},
}
resolver.EXPECT().Resolve(
fifthGroup,
detector.Runs,
).Return(
[]buildpack.GroupElement{},
[]files.BuildPlanEntry{},
phase.ErrFailedDetection,
).After(fourthResolve)
order := buildpack.Order{
{Group: []buildpack.GroupElement{{ID: "E", Version: "v1"}}},
}
detector.Order = order
_, _, err := detector.Detect()
if err, ok := err.(*buildpack.Error); !ok || err.Type != buildpack.ErrTypeFailedDetection {
t.Fatalf("Unexpected error:\n%s\n", err)
}
})
it("selects the first passing group", func() {
// This test doesn't use gomock.InOrder() because each call to Detect() happens in a go func.
// The order that other calls are written in is the order that they happen in.
bpE1 := &buildpack.BpDescriptor{
Buildpack: buildpack.BpInfo{BaseInfo: buildpack.BaseInfo{ID: "E", Version: "v1"}},
Order: []buildpack.Group{
{
Group: []buildpack.GroupElement{
{ID: "A", Version: "v1"},
{ID: "F", Version: "v1"},
{ID: "B", Version: "v1"},
},
},
},
}
dirStore.EXPECT().LookupBp("E", "v1").Return(bpE1, nil).AnyTimes()
bpA1 := &buildpack.BpDescriptor{
Buildpack: buildpack.BpInfo{BaseInfo: buildpack.BaseInfo{ID: "A", Version: "v1", Homepage: "Buildpack A Homepage"}}, // homepage added intentionally
}
dirStore.EXPECT().LookupBp("A", "v1").Return(bpA1, nil).AnyTimes()
bpF1 := &buildpack.BpDescriptor{
Buildpack: buildpack.BpInfo{BaseInfo: buildpack.BaseInfo{ID: "F", Version: "v1"}},
Order: []buildpack.Group{
{Group: []buildpack.GroupElement{
{ID: "C", Version: "v1"},
}},
{Group: []buildpack.GroupElement{
{ID: "G", Version: "v1", Optional: true},
}},
{Group: []buildpack.GroupElement{
{ID: "D", Version: "v1"},
}},
},
}
dirStore.EXPECT().LookupBp("F", "v1").Return(bpF1, nil).AnyTimes()
bpC1 := &buildpack.BpDescriptor{
Buildpack: buildpack.BpInfo{BaseInfo: buildpack.BaseInfo{ID: "C", Version: "v1"}},
}
dirStore.EXPECT().LookupBp("C", "v1").Return(bpC1, nil).AnyTimes()
bpB1 := &buildpack.BpDescriptor{
Buildpack: buildpack.BpInfo{BaseInfo: buildpack.BaseInfo{ID: "B", Version: "v1"}},
}
dirStore.EXPECT().LookupBp("B", "v1").Return(bpB1, nil).AnyTimes()
bpG1 := &buildpack.BpDescriptor{
Buildpack: buildpack.BpInfo{BaseInfo: buildpack.BaseInfo{ID: "G", Version: "v1"}},
Order: []buildpack.Group{
{
Group: []buildpack.GroupElement{
{ID: "A", Version: "v2"},
{ID: "B", Version: "v2"},
},
},
{
Group: []buildpack.GroupElement{
{ID: "C", Version: "v2"},
{ID: "D", Version: "v2"},
},
},
},
}
dirStore.EXPECT().LookupBp("G", "v1").Return(bpG1, nil).AnyTimes()
bpB2 := &buildpack.BpDescriptor{
Buildpack: buildpack.BpInfo{BaseInfo: buildpack.BaseInfo{ID: "B", Version: "v2"}},
}
dirStore.EXPECT().LookupBp("B", "v2").Return(bpB2, nil).AnyTimes()
bpC2 := &buildpack.BpDescriptor{
Buildpack: buildpack.BpInfo{BaseInfo: buildpack.BaseInfo{ID: "C", Version: "v2"}},
}
dirStore.EXPECT().LookupBp("C", "v2").Return(bpC2, nil).AnyTimes()
bpD2 := &buildpack.BpDescriptor{
Buildpack: buildpack.BpInfo{BaseInfo: buildpack.BaseInfo{ID: "D", Version: "v2"}},
}
dirStore.EXPECT().LookupBp("D", "v2").Return(bpD2, nil).AnyTimes()
executor.EXPECT().Detect(bpA1, gomock.Any(), gomock.Any())
executor.EXPECT().Detect(bpC1, gomock.Any(), gomock.Any())
executor.EXPECT().Detect(bpB1, gomock.Any(), gomock.Any())
firstGroup := []buildpack.GroupElement{
{ID: "A", Version: "v1", Homepage: "Buildpack A Homepage"}, // resolver receives homepage
{ID: "C", Version: "v1"},
{ID: "B", Version: "v1"},
}
firstResolve := resolver.EXPECT().Resolve(
firstGroup,
detector.Runs,
).Return(
[]buildpack.GroupElement{},
[]files.BuildPlanEntry{},
phase.ErrFailedDetection,
)
// bpA1 already done
executor.EXPECT().Detect(bpB2, gomock.Any(), gomock.Any())
secondGroup := []buildpack.GroupElement{
{ID: "A", Version: "v1", Homepage: "Buildpack A Homepage"},
{ID: "B", Version: "v2"},
}
secondResolve := resolver.EXPECT().Resolve(
secondGroup,
detector.Runs,
).Return(
[]buildpack.GroupElement{},
[]files.BuildPlanEntry{},
phase.ErrFailedDetection,
).After(firstResolve)
// bpA1 already done
executor.EXPECT().Detect(bpC2, gomock.Any(), gomock.Any())
executor.EXPECT().Detect(bpD2, gomock.Any(), gomock.Any())
// bpB1 already done
thirdGroup := []buildpack.GroupElement{
{ID: "A", Version: "v1", Homepage: "Buildpack A Homepage"},
{ID: "C", Version: "v2"},
{ID: "D", Version: "v2"},
{ID: "B", Version: "v1"},
}
thirdResolve := resolver.EXPECT().Resolve(
thirdGroup,
detector.Runs,
).Return(
[]buildpack.GroupElement{},
[]files.BuildPlanEntry{},
phase.ErrFailedDetection,
).After(secondResolve)
// bpA1 already done
// bpB1 already done
fourthGroup := []buildpack.GroupElement{
{ID: "A", Version: "v1", Homepage: "Buildpack A Homepage"},
{ID: "B", Version: "v1"},
}
resolver.EXPECT().Resolve(
fourthGroup,
detector.Runs,
).Return(
fourthGroup,
[]files.BuildPlanEntry{},
nil,
).After(thirdResolve)
order := buildpack.Order{
{Group: []buildpack.GroupElement{{ID: "E", Version: "v1"}}},
}
detector.Order = order
group, plan, err := detector.Detect()
if err != nil {
t.Fatalf("Unexpected error:\n%s\n", err)
}
if s := cmp.Diff(group, buildpack.Group{
Group: []buildpack.GroupElement{
{ID: "A", Version: "v1", Homepage: "Buildpack A Homepage"},
{ID: "B", Version: "v1"},
},
}); s != "" {
t.Fatalf("Unexpected group:\n%s\n", s)
}
if !hasEntries(plan.Entries, []files.BuildPlanEntry(nil)) {
t.Fatalf("Unexpected entries:\n%+v\n", plan.Entries)
}
})
it("updates detect runs for each buildpack", func() {
bpA1 := &buildpack.BpDescriptor{
Buildpack: buildpack.BpInfo{BaseInfo: buildpack.BaseInfo{ID: "A", Version: "v1"}},
}
dirStore.EXPECT().LookupBp("A", "v1").Return(bpA1, nil).AnyTimes()
executor.EXPECT().Detect(bpA1, gomock.Any(), gomock.Any()).Return(buildpack.DetectOutputs{
BuildPlan: buildpack.BuildPlan{
PlanSections: buildpack.PlanSections{
Requires: []buildpack.Require{{Name: "some-dep"}},
Provides: []buildpack.Provide{{Name: "some-dep"}},
},
Or: []buildpack.PlanSections{
{
Requires: []buildpack.Require{{Name: "some-other-dep"}},
Provides: []buildpack.Provide{{Name: "some-other-dep"}},
},
},
},
Output: []byte("detect out: A@v1\ndetect err: A@v1"),
Code: 0,
})
bpB1 := &buildpack.BpDescriptor{
Buildpack: buildpack.BpInfo{BaseInfo: buildpack.BaseInfo{ID: "B", Version: "v1"}},
}
dirStore.EXPECT().LookupBp("B", "v1").Return(bpB1, nil).AnyTimes()
bpBerror := errors.New("some-error")
executor.EXPECT().Detect(bpB1, gomock.Any(), gomock.Any()).Return(buildpack.DetectOutputs{
Output: []byte("detect out: B@v1\ndetect err: B@v1"),
Code: 100,
Err: bpBerror,
})
group := []buildpack.GroupElement{
{ID: "A", Version: "v1"},
{ID: "B", Version: "v1"},
}
resolver.EXPECT().Resolve(group, detector.Runs).Return(group, []files.BuildPlanEntry{}, nil)
detector.Order = buildpack.Order{{Group: group}}
_, _, err := detector.Detect()
if err != nil {
t.Fatalf("Unexpected error:\n%s\n", err)
}
bpARun, ok := detector.Runs.Load("Buildpack A@v1")
if !ok {
t.Fatalf("missing detection of '%s'", "A@v1")
}
if s := cmp.Diff(bpARun, buildpack.DetectOutputs{
BuildPlan: buildpack.BuildPlan{
PlanSections: buildpack.PlanSections{
Requires: []buildpack.Require{{Name: "some-dep"}},
Provides: []buildpack.Provide{{Name: "some-dep"}},
},
Or: []buildpack.PlanSections{
{
Requires: []buildpack.Require{{Name: "some-other-dep"}},
Provides: []buildpack.Provide{{Name: "some-other-dep"}},
},
},
},
Output: []byte("detect out: A@v1\ndetect err: A@v1"),
Code: 0,
Err: nil,
}); s != "" {
t.Fatalf("Unexpected detect run:\n%s\n", s)
}
bpBRun, ok := detector.Runs.Load("Buildpack B@v1")
if !ok {
t.Fatalf("missing detection of '%s'", "B@v1")
}
if s := cmp.Diff(bpBRun, buildpack.DetectOutputs{
Output: []byte("detect out: B@v1\ndetect err: B@v1"),
Code: 100,
Err: bpBerror,
}, cmp.Comparer(errors.Is)); s != "" {
t.Fatalf("Unexpected detect run:\n%s\n", s)
}
})
it("preserves 'optional'", func() {
bpA1 := &buildpack.BpDescriptor{
Buildpack: buildpack.BpInfo{BaseInfo: buildpack.BaseInfo{ID: "A", Version: "v1"}},
}
dirStore.EXPECT().LookupBp("A", "v1").Return(bpA1, nil).AnyTimes()
executor.EXPECT().Detect(bpA1, gomock.Any(), gomock.Any())
group := []buildpack.GroupElement{
{ID: "A", Version: "v1", Optional: true},
}
resolver.EXPECT().Resolve(group, detector.Runs)
detector.Order = buildpack.Order{{Group: group}}
_, _, _ = detector.Detect()
})
when("resolve errors", func() {
when("with buildpack error", func() {
it("returns a buildpack error", func() {
bpA1 := &buildpack.BpDescriptor{
Buildpack: buildpack.BpInfo{BaseInfo: buildpack.BaseInfo{ID: "A", Version: "v1"}},
}
dirStore.EXPECT().LookupBp("A", "v1").Return(bpA1, nil).AnyTimes()
executor.EXPECT().Detect(bpA1, gomock.Any(), gomock.Any())
group := []buildpack.GroupElement{
{ID: "A", Version: "v1"},
}
resolver.EXPECT().Resolve(group, detector.Runs).Return(
[]buildpack.GroupElement{},
[]files.BuildPlanEntry{},
phase.ErrBuildpack,
)
detector.Order = buildpack.Order{{Group: group}}
_, _, err := detector.Detect()
if err, ok := err.(*buildpack.Error); !ok || err.Type != buildpack.ErrTypeBuildpack {
t.Fatalf("Unexpected error:\n%s\n", err)
}
})
})
when("with detect error", func() {
it("returns a detect error", func() {
bpA1 := &buildpack.BpDescriptor{
Buildpack: buildpack.BpInfo{BaseInfo: buildpack.BaseInfo{ID: "A", Version: "v1"}},
}
dirStore.EXPECT().LookupBp("A", "v1").Return(bpA1, nil).AnyTimes()
executor.EXPECT().Detect(bpA1, gomock.Any(), gomock.Any())
group := []buildpack.GroupElement{
{ID: "A", Version: "v1"},
}
resolver.EXPECT().Resolve(group, detector.Runs).Return(
[]buildpack.GroupElement{},
[]files.BuildPlanEntry{},
phase.ErrFailedDetection,
)
detector.Order = buildpack.Order{{Group: group}}
_, _, err := detector.Detect()
if err, ok := err.(*buildpack.Error); !ok || err.Type != buildpack.ErrTypeFailedDetection {
t.Fatalf("Unexpected error:\n%s\n", err)
}
})
})
})
when("target resolution", func() {
it("totally works if the constraints are met", func() {
detector.AnalyzeMD.RunImage = &files.RunImage{
TargetMetadata: &files.TargetMetadata{
OS: "MacOS",
Arch: "ARM64",
Distro: &files.OSDistro{Name: "MacOS", Version: "snow cheetah"},
},
}
bpA1 := &buildpack.BpDescriptor{
Buildpack: buildpack.BpInfo{BaseInfo: buildpack.BaseInfo{ID: "A", Version: "v1"}},
Targets: []buildpack.TargetMetadata{
{Arch: "P6", ArchVariant: "Pentium Pro", OS: "Win95",
Distros: []buildpack.OSDistro{
{Name: "Sceens 95", Version: "OSR1"}, {Name: "Sceens 95", Version: "OSR2.5"}}},
{Arch: "ARM64", OS: "MacOS", Distros: []buildpack.OSDistro{{Name: "MacOS", Version: "snow cheetah"}}}},
}
dirStore.EXPECT().LookupBp("A", "v1").Return(bpA1, nil).AnyTimes()
executor.EXPECT().Detect(bpA1, gomock.Any(), gomock.Any())
group := []buildpack.GroupElement{
{ID: "A", Version: "v1"},
}
// the most meaningful assertion in this test is that `group` is the first argument to Resolve, meaning that the buildpack matched.
resolver.EXPECT().Resolve(group, detector.Runs).Return(
[]buildpack.GroupElement{},
[]files.BuildPlanEntry{},
nil,
)
detector.Order = buildpack.Order{{Group: group}}
_, _, err := detector.Detect()
h.AssertNil(t, err)
})
it("was born to be wildcard compliant", func() {
detector.AnalyzeMD.RunImage = &files.RunImage{
TargetMetadata: &files.TargetMetadata{
OS: "MacOS",
Arch: "ARM64",
Distro: &files.OSDistro{Name: "MacOS", Version: "snow cheetah"},
},
}
bpA1 := &buildpack.BpDescriptor{
Buildpack: buildpack.BpInfo{BaseInfo: buildpack.BaseInfo{ID: "A", Version: "v1"}},
Targets: []buildpack.TargetMetadata{
{Arch: "", OS: ""}},
}
dirStore.EXPECT().LookupBp("A", "v1").Return(bpA1, nil).AnyTimes()
executor.EXPECT().Detect(bpA1, gomock.Any(), gomock.Any())
group := []buildpack.GroupElement{
{ID: "A", Version: "v1"},
}
// the most meaningful assertion in this test is that `group` is the first argument to Resolve, meaning that the buildpack matched.
resolver.EXPECT().Resolve(group, detector.Runs).Return(
[]buildpack.GroupElement{},
[]files.BuildPlanEntry{},
nil,
)
detector.Order = buildpack.Order{{Group: group}}
_, _, err := detector.Detect()
h.AssertNil(t, err)
})
when("there is a composite buildpack", func() {
it("totally works if the constraints are met", func() {
detector.AnalyzeMD.RunImage = &files.RunImage{
TargetMetadata: &files.TargetMetadata{
OS: "MacOS",
Arch: "ARM64",
Distro: &files.OSDistro{Name: "MacOS", Version: "snow cheetah"},
},
}
bpF1 := &buildpack.BpDescriptor{
Buildpack: buildpack.BpInfo{BaseInfo: buildpack.BaseInfo{ID: "F", Version: "v1"}},
Order: []buildpack.Group{
{Group: []buildpack.GroupElement{
{ID: "A", Version: "v1"},
}},
},
}
dirStore.EXPECT().LookupBp("F", "v1").Return(bpF1, nil).AnyTimes()
bpA1 := &buildpack.BpDescriptor{
Buildpack: buildpack.BpInfo{BaseInfo: buildpack.BaseInfo{ID: "A", Version: "v1"}},
Targets: []buildpack.TargetMetadata{
{Arch: "P6", ArchVariant: "Pentium Pro", OS: "Win95",
Distros: []buildpack.OSDistro{
{Name: "Sceens 95", Version: "OSR1"}, {Name: "Sceens 95", Version: "OSR2.5"}}},
{Arch: "ARM64", OS: "MacOS", Distros: []buildpack.OSDistro{{Name: "MacOS", Version: "snow cheetah"}}}},
}
dirStore.EXPECT().LookupBp("A", "v1").Return(bpA1, nil).AnyTimes()
executor.EXPECT().Detect(bpA1, gomock.Any(), gomock.Any())
expectedGroup := []buildpack.GroupElement{
{ID: "A", Version: "v1"},
}
// the most meaningful assertion in this test is that `expectedGroup` is the first argument to Resolve, meaning that the buildpack matched.
resolver.EXPECT().Resolve(expectedGroup, detector.Runs).Return(
[]buildpack.GroupElement{},
[]files.BuildPlanEntry{},
nil,
)
detector.Order = buildpack.Order{{Group: []buildpack.GroupElement{
{ID: "F", Version: "v1"},
}}}
_, _, err := detector.Detect()
h.AssertNil(t, err)
})
})
it("errors if the buildpacks don't share that target arch/os", func() {
detector.AnalyzeMD.RunImage = &files.RunImage{
TargetMetadata: &files.TargetMetadata{
OS: "MacOS",
Arch: "ARM64",
Distro: &files.OSDistro{Name: "MacOS", Version: "some kind of big cat"},
},
}
bpA1 := &buildpack.BpDescriptor{
Buildpack: buildpack.BpInfo{BaseInfo: buildpack.BaseInfo{ID: "A", Version: "v1"}},
Targets: []buildpack.TargetMetadata{
{Arch: "P6", ArchVariant: "Pentium Pro", OS: "Win95",
Distros: []buildpack.OSDistro{
{Name: "Sceens 95", Version: "OSR1"}, {Name: "Sceens 95", Version: "OSR2.5"}}},
{Arch: "Pentium M", OS: "Win98",
Distros: []buildpack.OSDistro{{Name: "Screens 2000", Version: "Server"}}},
},
}
dirStore.EXPECT().LookupBp("A", "v1").Return(bpA1, nil).AnyTimes()
resolver.EXPECT().Resolve(gomock.Any(), gomock.Any()).DoAndReturn(
func(done []buildpack.GroupElement, detectRuns *sync.Map) ([]buildpack.GroupElement, []files.BuildPlanEntry, error) {
h.AssertEq(t, len(done), 1)
val, ok := detectRuns.Load("Buildpack A@v1")
h.AssertEq(t, ok, true)
outs := val.(buildpack.DetectOutputs)
h.AssertEq(t, outs.Code, -1)
h.AssertStringContains(t, outs.Err.Error(), `unable to satisfy target os/arch constraints; run image: {"os":"MacOS","arch":"ARM64","distro":{"name":"MacOS","version":"some kind of big cat"}}, buildpack: [{"os":"Win95","arch":"P6","arch-variant":"Pentium Pro","distros":[{"name":"Sceens 95","version":"OSR1"},{"name":"Sceens 95","version":"OSR2.5"}]},{"os":"Win98","arch":"Pentium M","distros":[{"name":"Screens 2000","version":"Server"}]}]`)
return []buildpack.GroupElement{}, []files.BuildPlanEntry{}, nil
})
group := []buildpack.GroupElement{
{ID: "A", Version: "v1"},
}
detector.Order = buildpack.Order{{Group: group}}
_, _, err := detector.Detect() // even though the returns from this are directly from the mock above, if we don't check the returns the linter declares we've done it wrong and fails on the lack of assertions.
h.AssertNil(t, err)
})
})
when("there are extensions", func() {
it("selects the first passing group", func() {
// This test doesn't use gomock.InOrder() because each call to Detect() happens in a go func.
// The order that other calls are written in is the order that they happen in.
bpA1 := &buildpack.BpDescriptor{
Buildpack: buildpack.BpInfo{BaseInfo: buildpack.BaseInfo{ID: "A", Version: "v1"}},
}
dirStore.EXPECT().LookupBp("A", "v1").Return(bpA1, nil).AnyTimes()
bpB1 := &buildpack.BpDescriptor{
Buildpack: buildpack.BpInfo{BaseInfo: buildpack.BaseInfo{ID: "B", Version: "v1"}},
}
dirStore.EXPECT().LookupBp("B", "v1").Return(bpB1, nil).AnyTimes()
extA1 := &buildpack.ExtDescriptor{
Extension: buildpack.ExtInfo{BaseInfo: buildpack.BaseInfo{ID: "A", Version: "v1"}},
}
dirStore.EXPECT().LookupExt("A", "v1").Return(extA1, nil).AnyTimes()
extB1 := &buildpack.ExtDescriptor{
Extension: buildpack.ExtInfo{BaseInfo: buildpack.BaseInfo{ID: "B", Version: "v1"}},
}
dirStore.EXPECT().LookupExt("B", "v1").Return(extB1, nil).AnyTimes()
executor.EXPECT().Detect(extA1, gomock.Any(), gomock.Any())
executor.EXPECT().Detect(bpA1, gomock.Any(), gomock.Any())
firstGroup := []buildpack.GroupElement{
{ID: "A", Version: "v1", Extension: true, Optional: true},
{ID: "A", Version: "v1"},
}
firstResolve := resolver.EXPECT().Resolve(
firstGroup,
detector.Runs,
).Return(
[]buildpack.GroupElement{},
[]files.BuildPlanEntry{},
phase.ErrFailedDetection,
)
executor.EXPECT().Detect(extB1, gomock.Any(), gomock.Any())
// bpA1 already done
secondGroup := []buildpack.GroupElement{
{ID: "B", Version: "v1", Extension: true, Optional: true},
{ID: "A", Version: "v1"},
}
secondResolve := resolver.EXPECT().Resolve(
secondGroup,
detector.Runs,
).Return(
[]buildpack.GroupElement{},
[]files.BuildPlanEntry{},
phase.ErrFailedDetection,
).After(firstResolve)
// bpA1 already done
thirdGroup := []buildpack.GroupElement{
{ID: "A", Version: "v1"},
}
thirdResolve := resolver.EXPECT().Resolve(
thirdGroup,
detector.Runs,
).Return(
[]buildpack.GroupElement{},
[]files.BuildPlanEntry{},
phase.ErrFailedDetection,
).After(secondResolve)
// extA1 already done
executor.EXPECT().Detect(bpB1, gomock.Any(), gomock.Any())
fourthGroup := []buildpack.GroupElement{
{ID: "A", Version: "v1", Extension: true, Optional: true},
{ID: "B", Version: "v1"},
}
fourthResolve := resolver.EXPECT().Resolve(
fourthGroup,
detector.Runs,
).Return(
[]buildpack.GroupElement{},
[]files.BuildPlanEntry{},
phase.ErrFailedDetection,
).After(thirdResolve)
// extB1 already done
// bpB1 already done
fifthGroup := []buildpack.GroupElement{
{ID: "B", Version: "v1", Extension: true, Optional: true},
{ID: "B", Version: "v1"},
}
resolver.EXPECT().Resolve(
fifthGroup,
detector.Runs,
).Return(
[]buildpack.GroupElement{
{ID: "B", Version: "v1", Extension: true}, // optional removed
{ID: "B", Version: "v1"},
},
[]files.BuildPlanEntry{},
nil,
).After(fourthResolve)
orderBp := buildpack.Order{
{Group: []buildpack.GroupElement{{ID: "A", Version: "v1"}}},
{Group: []buildpack.GroupElement{{ID: "B", Version: "v1"}}},
}
orderExt := buildpack.Order{
{Group: []buildpack.GroupElement{{ID: "A", Version: "v1"}}},
{Group: []buildpack.GroupElement{{ID: "B", Version: "v1"}}},
}
detector.Order = phase.PrependExtensions(orderBp, orderExt)
group, _, err := detector.Detect()
h.AssertNil(t, err)
h.AssertEq(t, group.Group, []buildpack.GroupElement{{ID: "B", Version: "v1"}})
h.AssertEq(t, group.GroupExtensions, []buildpack.GroupElement{{ID: "B", Version: "v1"}})
})
})
})
when(".Resolve", func() {
var (
resolver *phase.DefaultDetectResolver
logHandler *memory.Handler
logger *log.DefaultLogger
)
it.Before(func() {
logHandler = memory.New()
logger = &log.DefaultLogger{Logger: &apexlog.Logger{Handler: logHandler}}
resolver = phase.NewDefaultDetectResolver(logger)
})
it("fails if the group is empty", func() {
_, _, err := resolver.Resolve([]buildpack.GroupElement{}, &sync.Map{})
if err != phase.ErrFailedDetection {
t.Fatalf("Unexpected error:\n%s\n", err)
}
if s := cmp.Diff(h.AllLogs(logHandler),
"======== Results ========\n"+
"fail: no viable buildpacks in group\n",
); s != "" {
t.Fatalf("Unexpected log:\n%s\n", s)
}
})
it("fails if the group has no viable buildpacks, even if no required buildpacks fail", func() {
group := []buildpack.GroupElement{
{ID: "A", Version: "v1", Optional: true},
{ID: "B", Version: "v1", Optional: true},
}
detectRuns := &sync.Map{}
detectRuns.Store("Buildpack A@v1", buildpack.DetectOutputs{
Code: 100,
})
detectRuns.Store("Buildpack B@v1", buildpack.DetectOutputs{
Code: 100,
})
_, _, err := resolver.Resolve(group, detectRuns)
if err != phase.ErrFailedDetection {
t.Fatalf("Unexpected error:\n%s\n", err)
}
if s := h.AllLogs(logHandler); !strings.HasSuffix(s,
"======== Results ========\n"+
"skip: A@v1\n"+
"skip: B@v1\n"+
"fail: no viable buildpacks in group\n",
) {
t.Fatalf("Unexpected log:\n%s\n", s)
}
})
when("there are extensions", func() {
it("fails if the group has no viable buildpacks, even if no required buildpacks fail", func() {
group := []buildpack.GroupElement{
{ID: "A", Version: "v1", Optional: true},
{ID: "B", Version: "v1", Extension: true, Optional: true},
}
detectRuns := &sync.Map{}
detectRuns.Store("Buildpack A@v1", buildpack.DetectOutputs{
Code: 100,
})
detectRuns.Store("Extension B@v1", buildpack.DetectOutputs{
Code: 0,
})
_, _, err := resolver.Resolve(group, detectRuns)
if err != phase.ErrFailedDetection {
t.Fatalf("Unexpected error:\n%s\n", err)
}
if s := h.AllLogs(logHandler); !strings.HasSuffix(s,
"======== Results ========\n"+
"skip: A@v1\n"+
"pass: B@v1\n"+
"fail: no viable buildpacks in group\n",
) {
t.Fatalf("Unexpected log:\n%s\n", s)
}
})
})
it("fails with specific error if any bp detect fails in an unexpected way", func() {
group := []buildpack.GroupElement{
{ID: "A", Version: "v1", Optional: false},
{ID: "B", Version: "v1", Optional: false},
}
detectRuns := &sync.Map{}
detectRuns.Store("Buildpack A@v1", buildpack.DetectOutputs{
Code: 0,
})
detectRuns.Store("Buildpack B@v1", buildpack.DetectOutputs{
Code: 127,
})
_, _, err := resolver.Resolve(group, detectRuns)
if err != phase.ErrBuildpack {
t.Fatalf("Unexpected error:\n%s\n", err)
}
if s := h.AllLogs(logHandler); !strings.HasSuffix(s,
"======== Results ========\n"+
"pass: A@v1\n"+
"err: B@v1 (127)\n",
) {
t.Fatalf("Unexpected log:\n%s\n", s)
}
})
when("log output", func() {
it.Before(func() {
h.AssertNil(t, logger.SetLevel("info"))
})
it("outputs detect pass and fail as debug level", func() {
group := []buildpack.GroupElement{
{ID: "A", Version: "v1", Optional: false},
{ID: "B", Version: "v1", Optional: false},
}
detectRuns := &sync.Map{}
detectRuns.Store("Buildpack A@v1", buildpack.DetectOutputs{
Code: 0,
})
detectRuns.Store("Buildpack B@v1", buildpack.DetectOutputs{
Code: 100,
})
_, _, err := resolver.Resolve(group, detectRuns)
if err != phase.ErrFailedDetection {
t.Fatalf("Unexpected error:\n%s\n", err)
}
if s := h.AllLogs(logHandler); s != "" {
t.Fatalf("Unexpected log:\n%s\n", s)
}
})
it("outputs detect errors as info level", func() {
group := []buildpack.GroupElement{
{ID: "A", Version: "v1", Optional: false},
{ID: "B", Version: "v1", Optional: false},
}
detectRuns := &sync.Map{}
detectRuns.Store("Buildpack A@v1", buildpack.DetectOutputs{
Code: 0,
})
detectRuns.Store("Buildpack B@v1", buildpack.DetectOutputs{
Output: []byte("detect out: B@v1\ndetect err: B@v1"),
Code: 127,
})
_, _, err := resolver.Resolve(group, detectRuns)
if err != phase.ErrBuildpack {
t.Fatalf("Unexpected error:\n%s\n", err)
}
if s := h.AllLogs(logHandler); !strings.HasSuffix(s,
"======== Output: B@v1 ========\n"+
"detect out: B@v1\n"+
"detect err: B@v1\n"+
"err: B@v1 (127)\n",
) {
t.Fatalf("Unexpected log:\n%s\n", s)
}
})
})
it("returns a build plan with matched dependencies", func() {
group := []buildpack.GroupElement{
{ID: "A", Version: "v1", Homepage: "Buildpack A Homepage"},
{ID: "C", Version: "v2"},
{ID: "D", Version: "v2"},
{ID: "B", Version: "v1"},
}
detectRuns := &sync.Map{}
detectRuns.Store("Buildpack A@v1", buildpack.DetectOutputs{
BuildPlan: buildpack.BuildPlan{
PlanSections: buildpack.PlanSections{
Provides: []buildpack.Provide{
{Name: "dep1"},
{Name: "dep2"},
},
Requires: []buildpack.Require{
{Name: "dep2"},
},
},
},
})
detectRuns.Store("Buildpack B@v1", buildpack.DetectOutputs{
BuildPlan: buildpack.BuildPlan{
PlanSections: buildpack.PlanSections{
Requires: []buildpack.Require{
{Name: "dep1"},
{Name: "dep2"},
},
},
},
})
detectRuns.Store("Buildpack C@v2", buildpack.DetectOutputs{
BuildPlan: buildpack.BuildPlan{
PlanSections: buildpack.PlanSections{
Provides: []buildpack.Provide{
{Name: "dep1"},
{Name: "dep2"},
},
},
},
})
detectRuns.Store("Buildpack D@v2", buildpack.DetectOutputs{
BuildPlan: buildpack.BuildPlan{
PlanSections: buildpack.PlanSections{
Provides: []buildpack.Provide{
{Name: "dep2"},
},
Requires: []buildpack.Require{
{Name: "dep1"},
{Name: "dep2"},
},
},
},
})
found, entries, err := resolver.Resolve(group, detectRuns)
if err != nil {
t.Fatalf("Unexpected error:\n%s\n", err)
}
if s := cmp.Diff(found, group); s != "" {
t.Fatalf("Unexpected group:\n%s\n", s)
}
if !hasEntries(entries, []files.BuildPlanEntry{
{
Providers: []buildpack.GroupElement{
{ID: "A", Version: "v1"},
{ID: "C", Version: "v2"},
},
Requires: []buildpack.Require{{Name: "dep1"}, {Name: "dep1"}},
},
{
Providers: []buildpack.GroupElement{
{ID: "A", Version: "v1"},
{ID: "C", Version: "v2"},
{ID: "D", Version: "v2"},
},
Requires: []buildpack.Require{{Name: "dep2"}, {Name: "dep2"}, {Name: "dep2"}},
},
}) {
t.Fatalf("Unexpected entries:\n%+v\n", entries)
}
if s := h.AllLogs(logHandler); !strings.HasSuffix(s,
"======== Results ========\n"+
"pass: A@v1\n"+
"pass: C@v2\n"+
"pass: D@v2\n"+
"pass: B@v1\n"+
"Resolving plan... (try #1)\n"+
"A v1\n"+
"C v2\n"+
"D v2\n"+
"B v1\n",
) {
t.Fatalf("Unexpected log:\n%s\n", s)
}
})
it("fails if all requires are not provided first", func() {
group := []buildpack.GroupElement{
{ID: "A", Version: "v1", Optional: true},
{ID: "B", Version: "v1"},
{ID: "C", Version: "v1"},
}
detectRuns := &sync.Map{}
detectRuns.Store("Buildpack A@v1", buildpack.DetectOutputs{
BuildPlan: buildpack.BuildPlan{
PlanSections: buildpack.PlanSections{
Provides: []buildpack.Provide{
{Name: "dep1"},
},
},
},
Code: 100,
})
detectRuns.Store("Buildpack B@v1", buildpack.DetectOutputs{
BuildPlan: buildpack.BuildPlan{
PlanSections: buildpack.PlanSections{
Requires: []buildpack.Require{
{Name: "dep1"},
},
},
},
})
detectRuns.Store("Buildpack C@v1", buildpack.DetectOutputs{
BuildPlan: buildpack.BuildPlan{
PlanSections: buildpack.PlanSections{
Provides: []buildpack.Provide{
{Name: "dep1"},
},
Requires: []buildpack.Require{
{Name: "dep1"},
},
},
},
})
_, _, err := resolver.Resolve(group, detectRuns)
if err != phase.ErrFailedDetection {
t.Fatalf("Unexpected error:\n%s\n", err)
}
if s := h.AllLogs(logHandler); !strings.HasSuffix(s,
"======== Results ========\n"+
"skip: A@v1\n"+
"pass: B@v1\n"+
"pass: C@v1\n"+
"Resolving plan... (try #1)\n"+
"fail: B@v1 requires dep1\n",
) {
t.Fatalf("Unexpected log:\n%s\n", s)
}
})
it("fails if all provides are not required after", func() {
group := []buildpack.GroupElement{
{ID: "A", Version: "v1"},
{ID: "B", Version: "v1"},
{ID: "C", Version: "v1", Optional: true},
}
detectRuns := &sync.Map{}
detectRuns.Store("Buildpack A@v1", buildpack.DetectOutputs{
BuildPlan: buildpack.BuildPlan{
PlanSections: buildpack.PlanSections{
Provides: []buildpack.Provide{
{Name: "dep1"},
},
Requires: []buildpack.Require{
{Name: "dep1"},
},
},
},
})
detectRuns.Store("Buildpack B@v1", buildpack.DetectOutputs{
BuildPlan: buildpack.BuildPlan{
PlanSections: buildpack.PlanSections{
Provides: []buildpack.Provide{
{Name: "dep1"},
},
},
},
})
detectRuns.Store("Buildpack C@v1", buildpack.DetectOutputs{
BuildPlan: buildpack.BuildPlan{
PlanSections: buildpack.PlanSections{
Requires: []buildpack.Require{
{Name: "dep1"},
},
},
},
Code: 100,
})
_, _, err := resolver.Resolve(group, detectRuns)
if err != phase.ErrFailedDetection {
t.Fatalf("Unexpected error:\n%s\n", err)
}
if s := h.AllLogs(logHandler); !strings.HasSuffix(s,
"======== Results ========\n"+
"pass: A@v1\n"+
"pass: B@v1\n"+
"skip: C@v1\n"+
"Resolving plan... (try #1)\n"+
"fail: B@v1 provides unused dep1\n",
) {
t.Fatalf("Unexpected log:\n%s\n", s)
}
})
it("succeeds if unmet provides/requires are optional", func() {
group := []buildpack.GroupElement{
{ID: "A", Version: "v1", Optional: true},
{ID: "B", Version: "v1"},
{ID: "C", Version: "v1", Optional: true},
}
detectRuns := &sync.Map{}
detectRuns.Store("Buildpack A@v1", buildpack.DetectOutputs{
BuildPlan: buildpack.BuildPlan{
PlanSections: buildpack.PlanSections{
Requires: []buildpack.Require{
{Name: "dep-missing"},
},
},
},
})
detectRuns.Store("Buildpack B@v1", buildpack.DetectOutputs{
BuildPlan: buildpack.BuildPlan{
PlanSections: buildpack.PlanSections{
Provides: []buildpack.Provide{
{Name: "dep-present"},
},
Requires: []buildpack.Require{
{Name: "dep-present"},
},
},
},
})
detectRuns.Store("Buildpack C@v1", buildpack.DetectOutputs{
BuildPlan: buildpack.BuildPlan{
PlanSections: buildpack.PlanSections{
Provides: []buildpack.Provide{
{Name: "dep-missing"},
},
},
},
})
found, entries, err := resolver.Resolve(group, detectRuns)
if err != nil {
t.Fatalf("Unexpected error:\n%s\n", err)
}
if s := cmp.Diff(found, []buildpack.GroupElement{
{ID: "B", Version: "v1"},
}); s != "" {
t.Fatalf("Unexpected group:\n%s\n", s)
}
if !hasEntries(entries, []files.BuildPlanEntry{
{
Providers: []buildpack.GroupElement{{ID: "B", Version: "v1"}},
Requires: []buildpack.Require{{Name: "dep-present"}},
},
}) {
t.Fatalf("Unexpected entries:\n%+v\n", entries)
}
if s := h.AllLogs(logHandler); !strings.HasSuffix(s,
"======== Results ========\n"+
"pass: A@v1\n"+
"pass: B@v1\n"+
"pass: C@v1\n"+
"Resolving plan... (try #1)\n"+
"skip: A@v1 requires dep-missing\n"+
"skip: C@v1 provides unused dep-missing\n"+
"1 of 3 buildpacks participating\n"+
"B v1\n",
) {
t.Fatalf("Unexpected log:\n%s\n", s)
}
})
it("falls back to alternate build plans", func() {
group := []buildpack.GroupElement{
{ID: "A", Version: "v1", Optional: true, Homepage: "Buildpack A Homepage"},
{ID: "B", Version: "v1", Optional: true},
{ID: "C", Version: "v1"},
{ID: "D", Version: "v1", Optional: true},
}
detectRuns := &sync.Map{}
detectRuns.Store("Buildpack A@v1", buildpack.DetectOutputs{
BuildPlan: buildpack.BuildPlan{
PlanSections: buildpack.PlanSections{
Provides: []buildpack.Provide{
{Name: "dep2-missing"},
},
},
Or: []buildpack.PlanSections{
{
Provides: []buildpack.Provide{
{Name: "dep1-present"},
},
},
},
},
})
detectRuns.Store("Buildpack B@v1", buildpack.DetectOutputs{
BuildPlan: buildpack.BuildPlan{
PlanSections: buildpack.PlanSections{
Requires: []buildpack.Require{
{Name: "dep3-missing"},
},
},
Or: []buildpack.PlanSections{
{
Requires: []buildpack.Require{
{Name: "dep1-present"},
},
},
},
},
})
detectRuns.Store("Buildpack C@v1", buildpack.DetectOutputs{
BuildPlan: buildpack.BuildPlan{
PlanSections: buildpack.PlanSections{
Provides: []buildpack.Provide{
{Name: "dep5-missing"},
},
Requires: []buildpack.Require{
{Name: "dep4-missing"},
},
},
Or: []buildpack.PlanSections{
{
Provides: []buildpack.Provide{
{Name: "dep6-present"},
},
Requires: []buildpack.Require{
{Name: "dep6-present"},
},
},
},
},
})
detectRuns.Store("Buildpack D@v1", buildpack.DetectOutputs{
BuildPlan: buildpack.BuildPlan{
PlanSections: buildpack.PlanSections{
Provides: []buildpack.Provide{
{Name: "dep8-missing"},
},
Requires: []buildpack.Require{
{Name: "dep7-missing"},
},
},
Or: []buildpack.PlanSections{
{
Provides: []buildpack.Provide{
{Name: "dep10-missing"},
},
Requires: []buildpack.Require{
{Name: "dep9-missing"},
},
},
},
},
})
found, entries, err := resolver.Resolve(group, detectRuns)
if err != nil {
t.Fatalf("Unexpected error:\n%s\n", err)
}
if s := cmp.Diff(found, []buildpack.GroupElement{
{ID: "A", Version: "v1", Homepage: "Buildpack A Homepage"},
{ID: "B", Version: "v1"},
{ID: "C", Version: "v1"},
}); s != "" {
t.Fatalf("Unexpected group:\n%s\n", s)
}
if !hasEntries(entries, []files.BuildPlanEntry{
{
Providers: []buildpack.GroupElement{{ID: "A", Version: "v1"}},
Requires: []buildpack.Require{{Name: "dep1-present"}},
},
{
Providers: []buildpack.GroupElement{{ID: "C", Version: "v1"}},
Requires: []buildpack.Require{{Name: "dep6-present"}},
},
}) {
t.Fatalf("Unexpected entries:\n%+v\n", entries)
}
if s := h.AllLogs(logHandler); !strings.HasSuffix(s,
"Resolving plan... (try #16)\n"+
"skip: D@v1 requires dep9-missing\n"+
"skip: D@v1 provides unused dep10-missing\n"+
"3 of 4 buildpacks participating\n"+
"A v1\n"+
"B v1\n"+
"C v1\n",
) {
t.Fatalf("Unexpected log:\n%s\n", s)
}
})
})
when("execution environment filtering", func() {
var (
detector *phase.Detector
executor *testmock.MockDetectExecutor
resolver *testmock.MockDetectResolver
)
it.Before(func() {
configHandler.EXPECT().ReadAnalyzed("some-analyzed-path", gomock.Any()).Return(files.Analyzed{}, nil).AnyTimes()
configHandler.EXPECT().ReadOrder("some-order-path").Return(buildpack.Order{}, buildpack.Order{}, nil)
var err error
detector, err = detectorFactory.NewDetector(platform.LifecycleInputs{
AnalyzedPath: "some-analyzed-path",
AppDir: "some-app-dir",
BuildConfigDir: "some-build-config-dir",
OrderPath: "some-order-path",
PlatformDir: "some-platform-dir",
ExecEnv: "test",
}, logger)
h.AssertNil(t, err)
// override factory-provided services
executor = testmock.NewMockDetectExecutor(mockController)
resolver = testmock.NewMockDetectResolver(mockController)
detector.Executor = executor
detector.Resolver = resolver
detector.PlatformAPI = api.MustParse("0.15") // Enable execution environment filtering
})
when("Platform API < 0.15", func() {
it("does not filter buildpacks based on execution environment", func() {
detector.PlatformAPI = api.MustParse("0.14")
detector.ExecEnv = "test"
bpA1 := &buildpack.BpDescriptor{
WithAPI: "0.12",
Buildpack: buildpack.BpInfo{BaseInfo: buildpack.BaseInfo{ID: "A", Version: "v1", ExecEnv: []string{"production"}}},
}
dirStore.EXPECT().LookupBp("A", "v1").Return(bpA1, nil).AnyTimes()
executor.EXPECT().Detect(bpA1, gomock.Any(), gomock.Any()).Return(buildpack.DetectOutputs{})
resolver.EXPECT().Resolve(gomock.Any(), detector.Runs).Do(
func(group []buildpack.GroupElement, _ *sync.Map) {
h.AssertEq(t, len(group), 1)
h.AssertEq(t, group[0].ID, "A")
h.AssertEq(t, group[0].Version, "v1")
})
detector.Order = buildpack.Order{{Group: []buildpack.GroupElement{
{ID: "A", Version: "v1"},
}}}
_, _, _ = detector.Detect()
})
})
when("buildpack API < 0.12", func() {
it("ignores exec-env field and allows buildpack to run", func() {
detector.ExecEnv = "test"
bpA1 := &buildpack.BpDescriptor{
WithAPI: "0.11", // API < 0.12
Buildpack: buildpack.BpInfo{BaseInfo: buildpack.BaseInfo{ID: "A", Version: "v1", ExecEnv: []string{"production"}}},
}
dirStore.EXPECT().LookupBp("A", "v1").Return(bpA1, nil).AnyTimes()
executor.EXPECT().Detect(bpA1, gomock.Any(), gomock.Any()).Return(buildpack.DetectOutputs{})
resolver.EXPECT().Resolve(gomock.Any(), detector.Runs).Do(
func(group []buildpack.GroupElement, _ *sync.Map) {
h.AssertEq(t, len(group), 1)
h.AssertEq(t, group[0].ID, "A")
h.AssertEq(t, group[0].Version, "v1")
})
detector.Order = buildpack.Order{{Group: []buildpack.GroupElement{
{ID: "A", Version: "v1"},
}}}
_, _, _ = detector.Detect()
})
})
when("buildpack API >= 0.12", func() {
when("buildpack has no exec-env specified", func() {
it("allows buildpack to run in any execution environment", func() {
detector.ExecEnv = "test"
bpA1 := &buildpack.BpDescriptor{
WithAPI: "0.12",
Buildpack: buildpack.BpInfo{BaseInfo: buildpack.BaseInfo{ID: "A", Version: "v1"}}, // No ExecEnv specified
}
dirStore.EXPECT().LookupBp("A", "v1").Return(bpA1, nil).AnyTimes()
executor.EXPECT().Detect(bpA1, gomock.Any(), gomock.Any()).Return(buildpack.DetectOutputs{})
resolver.EXPECT().Resolve(gomock.Any(), detector.Runs).Do(
func(group []buildpack.GroupElement, _ *sync.Map) {
h.AssertEq(t, len(group), 1)
h.AssertEq(t, group[0].ID, "A")
h.AssertEq(t, group[0].Version, "v1")
})
detector.Order = buildpack.Order{{Group: []buildpack.GroupElement{
{ID: "A", Version: "v1"},
}}}
_, _, _ = detector.Detect()
})
})
when("buildpack supports all execution environments with '*'", func() {
it("allows buildpack to run", func() {
detector.ExecEnv = "test"
bpA1 := &buildpack.BpDescriptor{
WithAPI: "0.12",
Buildpack: buildpack.BpInfo{BaseInfo: buildpack.BaseInfo{ID: "A", Version: "v1", ExecEnv: []string{"*"}}},
}
dirStore.EXPECT().LookupBp("A", "v1").Return(bpA1, nil).AnyTimes()
executor.EXPECT().Detect(bpA1, gomock.Any(), gomock.Any()).Return(buildpack.DetectOutputs{})
resolver.EXPECT().Resolve(gomock.Any(), detector.Runs).Do(
func(group []buildpack.GroupElement, _ *sync.Map) {
h.AssertEq(t, len(group), 1)
h.AssertEq(t, group[0].ID, "A")
h.AssertEq(t, group[0].Version, "v1")
})
detector.Order = buildpack.Order{{Group: []buildpack.GroupElement{
{ID: "A", Version: "v1"},
}}}
_, _, _ = detector.Detect()
})
})
when("buildpack supports the current execution environment", func() {
it("allows buildpack to run", func() {
detector.ExecEnv = "test"
bpA1 := &buildpack.BpDescriptor{
WithAPI: "0.12",
Buildpack: buildpack.BpInfo{BaseInfo: buildpack.BaseInfo{ID: "A", Version: "v1", ExecEnv: []string{"production", "test"}}},
}
dirStore.EXPECT().LookupBp("A", "v1").Return(bpA1, nil).AnyTimes()
executor.EXPECT().Detect(bpA1, gomock.Any(), gomock.Any()).Return(buildpack.DetectOutputs{})
resolver.EXPECT().Resolve(gomock.Any(), detector.Runs).Do(
func(group []buildpack.GroupElement, _ *sync.Map) {
h.AssertEq(t, len(group), 1)
h.AssertEq(t, group[0].ID, "A")
h.AssertEq(t, group[0].Version, "v1")
})
detector.Order = buildpack.Order{{Group: []buildpack.GroupElement{
{ID: "A", Version: "v1"},
}}}
_, _, _ = detector.Detect()
})
})
when("buildpack does not support the current execution environment", func() {
it("skips the buildpack", func() {
detector.ExecEnv = "test"
bpA1 := &buildpack.BpDescriptor{
WithAPI: "0.12",
Buildpack: buildpack.BpInfo{BaseInfo: buildpack.BaseInfo{ID: "A", Version: "v1", ExecEnv: []string{"production"}}},
}
dirStore.EXPECT().LookupBp("A", "v1").Return(bpA1, nil).AnyTimes()
// executor.Detect should NOT be called for this buildpack
bpB1 := &buildpack.BpDescriptor{
WithAPI: "0.12",
Buildpack: buildpack.BpInfo{BaseInfo: buildpack.BaseInfo{ID: "B", Version: "v1", ExecEnv: []string{"test"}}},
}
dirStore.EXPECT().LookupBp("B", "v1").Return(bpB1, nil).AnyTimes()
executor.EXPECT().Detect(bpB1, gomock.Any(), gomock.Any()).Return(buildpack.DetectOutputs{})
resolver.EXPECT().Resolve(gomock.Any(), detector.Runs).Do(
func(group []buildpack.GroupElement, _ *sync.Map) {
h.AssertEq(t, len(group), 1)
h.AssertEq(t, group[0].ID, "B")
h.AssertEq(t, group[0].Version, "v1")
})
detector.Order = buildpack.Order{{Group: []buildpack.GroupElement{
{ID: "A", Version: "v1"}, // This gets skipped
{ID: "B", Version: "v1"}, // This gets processed
}}}
_, _, _ = detector.Detect()
})
})
when("optional buildpack does not support the current execution environment", func() {
it("skips the optional buildpack without affecting the group", func() {
detector.ExecEnv = "test"
bpA1 := &buildpack.BpDescriptor{
WithAPI: "0.12",
Buildpack: buildpack.BpInfo{BaseInfo: buildpack.BaseInfo{ID: "A", Version: "v1", ExecEnv: []string{"production"}}},
}
dirStore.EXPECT().LookupBp("A", "v1").Return(bpA1, nil).AnyTimes()
// executor.Detect should NOT be called for this buildpack
bpB1 := &buildpack.BpDescriptor{
WithAPI: "0.12",
Buildpack: buildpack.BpInfo{BaseInfo: buildpack.BaseInfo{ID: "B", Version: "v1", ExecEnv: []string{"test"}}},
}
dirStore.EXPECT().LookupBp("B", "v1").Return(bpB1, nil).AnyTimes()
executor.EXPECT().Detect(bpB1, gomock.Any(), gomock.Any()).Return(buildpack.DetectOutputs{})
resolver.EXPECT().Resolve(gomock.Any(), detector.Runs).Do(
func(group []buildpack.GroupElement, _ *sync.Map) {
h.AssertEq(t, len(group), 1)
h.AssertEq(t, group[0].ID, "B")
h.AssertEq(t, group[0].Version, "v1")
})
detector.Order = buildpack.Order{{Group: []buildpack.GroupElement{
{ID: "A", Version: "v1", Optional: true}, // Optional, gets skipped
{ID: "B", Version: "v1"}, // This gets processed
}}}
_, _, _ = detector.Detect()
})
})
})
when("extension filtering", func() {
it("filters extensions based on execution environment", func() {
detector.ExecEnv = "test"
extA1 := &buildpack.ExtDescriptor{
WithAPI: "0.12",
Extension: buildpack.ExtInfo{BaseInfo: buildpack.BaseInfo{ID: "A", Version: "v1", ExecEnv: []string{"production"}}},
}
dirStore.EXPECT().LookupExt("A", "v1").Return(extA1, nil).AnyTimes()
// executor.Detect should NOT be called for this extension
extB1 := &buildpack.ExtDescriptor{
WithAPI: "0.12",
Extension: buildpack.ExtInfo{BaseInfo: buildpack.BaseInfo{ID: "B", Version: "v1", ExecEnv: []string{"test"}}},
}
dirStore.EXPECT().LookupExt("B", "v1").Return(extB1, nil).AnyTimes()
executor.EXPECT().Detect(extB1, gomock.Any(), gomock.Any()).Return(buildpack.DetectOutputs{})
resolver.EXPECT().Resolve(gomock.Any(), detector.Runs).Do(
func(group []buildpack.GroupElement, _ *sync.Map) {
h.AssertEq(t, len(group), 1)
h.AssertEq(t, group[0].ID, "B")
h.AssertEq(t, group[0].Version, "v1")
h.AssertEq(t, group[0].Extension, true)
h.AssertEq(t, group[0].Optional, true)
})
detector.Order = buildpack.Order{{Group: []buildpack.GroupElement{
{ID: "A", Version: "v1", Extension: true, Optional: true}, // This gets skipped
{ID: "B", Version: "v1", Extension: true, Optional: true}, // This gets processed
}}}
_, _, _ = detector.Detect()
})
})
})
when("#PrependExtensions", func() {
it("prepends the extensions order to each group in the buildpacks order", func() {
orderBp := buildpack.Order{
buildpack.Group{Group: []buildpack.GroupElement{{ID: "A", Version: "v1"}}},
buildpack.Group{Group: []buildpack.GroupElement{{ID: "B", Version: "v1"}}},
}
orderExt := buildpack.Order{
buildpack.Group{Group: []buildpack.GroupElement{{ID: "C", Version: "v1"}}},
buildpack.Group{Group: []buildpack.GroupElement{{ID: "D", Version: "v1"}}},
}
expectedOrderExt := buildpack.Order{
buildpack.Group{Group: []buildpack.GroupElement{{ID: "C", Version: "v1", Extension: true, Optional: true}}},
buildpack.Group{Group: []buildpack.GroupElement{{ID: "D", Version: "v1", Extension: true, Optional: true}}},
}
newOrder := phase.PrependExtensions(orderBp, orderExt)
t.Log("returns the modified order")
if s := cmp.Diff(newOrder, buildpack.Order{
buildpack.Group{
Group: []buildpack.GroupElement{
{OrderExtensions: expectedOrderExt},
{ID: "A", Version: "v1"},
},
},
buildpack.Group{
Group: []buildpack.GroupElement{
{OrderExtensions: expectedOrderExt},
{ID: "B", Version: "v1"},
},
},
}); s != "" {
t.Fatalf("Unexpected:\n%s\n", s)
}
t.Log("does not modify the originally provided order")
if s := cmp.Diff(orderBp, buildpack.Order{
buildpack.Group{Group: []buildpack.GroupElement{{ID: "A", Version: "v1"}}},
buildpack.Group{Group: []buildpack.GroupElement{{ID: "B", Version: "v1"}}},
}); s != "" {
t.Fatalf("Unexpected:\n%s\n", s)
}
})
when("the extensions order is empty", func() {
it("returns the originally provided order", func() {
orderBp := buildpack.Order{
buildpack.Group{Group: []buildpack.GroupElement{{ID: "A", Version: "v1"}}},
buildpack.Group{Group: []buildpack.GroupElement{{ID: "B", Version: "v1"}}},
}
newOrder := phase.PrependExtensions(orderBp, nil)
if s := cmp.Diff(newOrder, buildpack.Order{
buildpack.Group{Group: []buildpack.GroupElement{{ID: "A", Version: "v1"}}},
buildpack.Group{Group: []buildpack.GroupElement{{ID: "B", Version: "v1"}}},
}); s != "" {
t.Fatalf("Unexpected:\n%s\n", s)
}
})
})
})
}
func hasEntry(l []files.BuildPlanEntry, entry files.BuildPlanEntry) bool {
for _, e := range l {
if reflect.DeepEqual(e, entry) {
return true
}
}
return false
}
func hasEntries(a, b []files.BuildPlanEntry) bool {
if len(a) != len(b) {
return false
}
for _, e := range a {
if !hasEntry(b, e) {
return false
}
}
return true
}