2051 lines
66 KiB
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
|
|
}
|