--- a/intrinsic/extend.go Sun Oct 01 21:04:31 2017 -0500
+++ b/intrinsic/extend.go Sun Oct 01 21:31:03 2017 -0500
@@ -45,10 +45,12 @@
// While extending, certain environment variables can be expanded into
// lists. We store this meta information as a stack on the state, which
// is passed into the inner task execute method (and so on).
- st.PushExtendFrame(e.Environment, e.Expand, e.ExpandDelimiter)
- defer st.PopExtendFrame()
- return e.InnerTask.Execute(name, logger, env, st)
+ return e.InnerTask.Execute(name, logger, env, st.WrapWithExpandableEnv( func (e *Extend) New() tasks.Task {
--- a/state/state.go Sun Oct 01 21:04:31 2017 -0500
+++ b/state/state.go Sun Oct 01 21:31:03 2017 -0500
@@ -18,7 +18,6 @@
@@ -38,23 +37,25 @@
TaskTimeout time.Duration
- ExtendFrames []extendFrame
DetachedContainers []string
+ // States have the ability to "wrap" another one without + // changing the underlying state. This is used by the + // extends intrinsic in order to modify the stat without + // requiring unique access to the state object during the + // execution of the extended task. A past implementation + // had modified the stack directly, but that causes an + // extended task to clobber other tasks in concurrent mode. + expandableDelimiter string -type extendFrame struct {
-var errEmptyExtendStack = errors.New("empty extend stack")
func (st *State) Valid() error {
if val := os.Getenv("SSH_AUTH_SOCK"); val == "" {
@@ -88,46 +89,45 @@
// GetDelimiter returns the highest (outermost extend task) delimiter registered
// with a given expandable environment variable. Returns true if found by name.
func (st *State) getDelimiter(name string) (string, bool) {
- for i := len(st.ExtendFrames) - 1; i >= 0; i-- {
- frame := st.ExtendFrames[i]
- for _, expandable := range frame.expandable {
- if expandable == name {
- return frame.delimiter, true
+ for _, expandable := range st.expandables { + if expandable == name { + return st.expandableDelimiter, true
+ return st.parent.getDelimiter(name) -// PushExtendFrame will store the set of expandable environment variables
-// and the current set of environments which are restored on pop. These frames
-// are used to map a slice within an extended task.
-func (st *State) PushExtendFrame(env, expandable []string, delimiter string) {
- st.ExtendFrames = append(st.ExtendFrames, extendFrame{
- expandable: expandable,
- stashedEnv: st.Environment,
+// WrapWithExpandableEnv will create a shallow clone of the state with a reference +// to the current state as "parent" with a modified environment. This creates a local +// stack of states which do not interfere with other goroutines. A pop operation is +// the same as ignoring the wrapped values and using the underlying state. This stack +// is used to map a slice within an extended task. +func (st *State) WrapWithExpandableEnv(env, expandable []string, delimiter string) *State { // Merge the environment into this map, but do NOT override anything that
// is currently in the state's enviornment - this has higher precedence.
- st.Environment = environment.Merge(env, st.Environment)
+ env = environment.Merge(env, st.Environment) -// PopExtendFrame will remove one frame from the top of the stack.
-func (st *State) PopExtendFrame() error {
- idx := len(st.ExtendFrames) - 1
- return errEmptyExtendStack
+ Workspace: st.Workspace, + KeepWorkspace: st.KeepWorkspace, + ForceSequential: st.ForceSequential, + EnableSSHAgent: st.EnableSSHAgent, + TaskTimeout: st.TaskTimeout, + DockerConfig: st.DockerConfig, + CPUShares: st.CPUShares, + DetachedContainers: st.DetachedContainers, + expandables: expandable, + expandableDelimiter: delimiter,
- last := st.ExtendFrames[idx]
- st.ExtendFrames = st.ExtendFrames[:idx]
- st.Environment = last.stashedEnv
// getName returns the name of the variable `var` if the string is of the
--- a/state/state_test.go Sun Oct 01 21:04:31 2017 -0500
+++ b/state/state_test.go Sun Oct 01 21:31:03 2017 -0500
@@ -38,59 +38,46 @@
func (e *stateSuite) TestMap(t sweet.T) {
- st.Environment = []string{"FOO=BAR"}
- mapEnv := func(val string) []string {
+ mapEnv := func(st *State, val string) []string { mapped, err := st.MapSlice([]string{val}, st.Environment)
- Expect(mapEnv("$X")).To(Equal([]string{"$X"}))
- Expect(mapEnv("$FOO")).To(Equal([]string{"BAR"}))
+ st1.Environment = []string{"FOO=BAR"} - st.PushExtendFrame([]string{"X=A;B;C"}, []string{"X"}, ";")
- Expect(mapEnv("$X")).To(Equal([]string{"A", "B", "C"}))
- Expect(mapEnv("$FOO")).To(Equal([]string{"BAR"}))
+ Expect(mapEnv(st1, "$X")).To(Equal([]string{"$X"})) + Expect(mapEnv(st1, "$FOO")).To(Equal([]string{"BAR"})) - st.PushExtendFrame([]string{"BAR=B;A;R::B;A;Z", "FOO=SAME"}, []string{"BAR"}, "::")
- Expect(mapEnv("$X")).To(Equal([]string{"A", "B", "C"}))
- Expect(mapEnv("$FOO")).To(Equal([]string{"BAR"}))
- Expect(mapEnv("$BAR")).To(Equal([]string{"B;A;R", "B;A;Z"}))
+ st2 := st1.WrapWithExpandableEnv([]string{"X=A;B;C"}, []string{"X"}, ";") + Expect(mapEnv(st2, "$X")).To(Equal([]string{"A", "B", "C"})) + Expect(mapEnv(st2, "$FOO")).To(Equal([]string{"BAR"})) - Expect(st.PopExtendFrame()).To(BeNil())
- Expect(st.PopExtendFrame()).To(BeNil())
- Expect(mapEnv("$X")).To(Equal([]string{"$X"}))
- Expect(mapEnv("$FOO")).To(Equal([]string{"BAR"}))
+ st3 := st2.WrapWithExpandableEnv([]string{"BAR=B;A;R::B;A;Z", "FOO=SAME"}, []string{"BAR"}, "::") + Expect(mapEnv(st3, "$X")).To(Equal([]string{"A", "B", "C"})) + Expect(mapEnv(st3, "$FOO")).To(Equal([]string{"BAR"})) + Expect(mapEnv(st3, "$BAR")).To(Equal([]string{"B;A;R", "B;A;Z"})) -func (e *stateSuite) TestPopExtendFrameSize(t sweet.T) {
- st.PushExtendFrame(nil, nil, "")
- Expect(st.ExtendFrames).To(HaveLen(1))
- st.PushExtendFrame(nil, nil, "")
- Expect(st.ExtendFrames).To(HaveLen(2))
- Expect(st.PopExtendFrame()).To(BeNil())
- Expect(st.ExtendFrames).To(HaveLen(1))
- Expect(st.PopExtendFrame()).To(BeNil())
- Expect(st.ExtendFrames).To(HaveLen(0))
+func (e *stateSuite) TestWrapParent(t sweet.T) { + st1.WrapWithExpandableEnv(nil, nil, "") + Expect(st1.parent).To(BeNil()) + st2 := st1.WrapWithExpandableEnv(nil, nil, "") + Expect(st2.parent).To(BeIdenticalTo(st1)) -func (e *stateSuite) TestPopExtendFrameRestoresEnv(t sweet.T) {
- st.Environment = []string{"FOO=BAR", "BAR=BAZ"}
- st.PushExtendFrame([]string{"FOO=BONK", "BAZ=BONK"}, nil, "")
- Expect(st.Environment).To(HaveLen(3))
- Expect(st.Environment).To(ConsistOf([]string{"FOO=BAR", "BAR=BAZ", "BAZ=BONK"}))
- Expect(st.Environment).To(HaveLen(2))
- Expect(st.Environment).To(ConsistOf([]string{"FOO=BAR", "BAR=BAZ"}))
+func (e *stateSuite) TestWrapWithExpandableEnvMap(t sweet.T) { + st1.Environment = []string{"FOO=BAR", "BAR=BAZ"} + Expect(st1.Environment).To(HaveLen(2)) + Expect(st1.Environment).To(ConsistOf([]string{"FOO=BAR", "BAR=BAZ"})) -func (e *stateSuite) TestPopExtendFrameEmpty(t sweet.T) {
- Expect(st.PopExtendFrame()).To(Equal(errEmptyExtendStack))
+ st2 := st1.WrapWithExpandableEnv([]string{"FOO=BONK", "BAZ=BONK"}, nil, "") + Expect(st2.Environment).To(HaveLen(3)) + Expect(st2.Environment).To(ConsistOf([]string{"FOO=BAR", "BAR=BAZ", "BAZ=BONK"})) func (e *stateSuite) TestGetName(t sweet.T) {