--- a/environment/mapper.go Mon Oct 02 11:21:46 2017 -0500
+++ b/environment/mapper.go Mon Oct 02 12:06:42 2017 -0500
@@ -41,6 +41,12 @@
+// Map will return the value matching a KEY=VAL pair in the given environment. +func Map(key string, env []string) string { + mapper := envMapper{env} // Mapper will replace ${TEMPLATE} patterns in the string with the KEY=VAL pairs
// in the given environment. This function will replace patterns recursively, so
// if VAL has the form ${TEMPLATE}, it will be replaced again.
--- a/state/state.go Mon Oct 02 11:21:46 2017 -0500
+++ b/state/state.go Mon Oct 02 12:06:42 2017 -0500
@@ -21,7 +21,6 @@
@@ -30,6 +29,8 @@
"bitbucket.org/rw_grim/convey/workspace"
+const ExpansionLimit = 100 Workspace workspace.Workspace
@@ -69,19 +70,42 @@
// MapSlice calls SliceMapper on the given environment, but also checks to
// see if the variable in the env parameter can be expanded into a list.
func (st *State) MapSlice(env, fullEnv []string) ([]string, error) {
+ // Protect ourselves against a weird infinite expansion. This can + // happen if something occurs like X => A,$Y; Y => B,$X. This is + // never a useful case - a high expansion limit should catch this + // without ever hitting a practical edge. + for i := 0; i < ExpansionLimit; i++ { + next, err := st.expandSlice(prev, fullEnv) - // TODO - make iterative
+ // If we haven't made a change, return the final result. + if isSame(next, prev) { + return nil, fmt.Errorf("hit limit while expanding '%s'", env) +func (st *State) expandSlice(env, fullEnv []string) ([]string, error) { for _, data := range env {
expanded, err := st.expand(data, fullEnv)
- clean = append(clean, expanded...)
+ all = append(all, expanded...)
+ return removeDuplicates(all), nil // expand will attempt to expand any variables stored on the state expand
@@ -96,12 +120,7 @@
for _, name := range getNames(data) {
if delimiter, ok := st.getDelimiter(name); ok {
- val, err := environment.Mapper("$"+name, fullEnv)
- expansions[name] = strings.SplitN(val, delimiter, -1)
+ expansions[name] = strings.SplitN(environment.Map(name, fullEnv), delimiter, -1) @@ -119,8 +138,8 @@
// Construct the actual values. Product gives us a map of all possible
- // combinations of expanded values. For each one, overwrite the vars
- // in the full env and apply the map.
+ // combinations of expanded values. For each one, map using ONLY the + // expanded vars. Additional mapping will apply on the next iteration. for _, m := range product(expansions) {
@@ -129,7 +148,7 @@
env = append(env, fmt.Sprintf("%s=%s", k, v))
- val, err := environment.Mapper(data, environment.Merge(fullEnv, env))
+ val, err := environment.Mapper(data, env) @@ -184,95 +203,3 @@
-var re1 = regexp.MustCompile("\\$[A-Za-z0-9_]+")
-var re2 = regexp.MustCompile("\\${[^}]+}")
-// getNames extracts all variables with the format $var or ${var} from
-func getNames(env string) []string {
- for _, match := range re1.FindAllString(env, -1) {
- names = append(names, match[1:])
- for _, match := range re2.FindAllString(env, -1) {
- // Remove leading ${ and trailing }
- names = append(names, match[2:len(match)-1])
-// product will return the cartesian product of the input map. For
-// example, if the input map is A => 1, 2, 3; B => 4, 5, 6, then
-// the list of outputs will contain the following:
-func product(expansions map[string][]string) []map[string]string {
- indices = make([]int, len(expansions))
- maps = []map[string]string{}
- if len(expansions) == 0 {
- // Create a stable order of keys - this will determine our
- // iteration order (in the above [A B] is the list as the
- // index for B will increase more frequently than A).
- for key := range expansions {
- order = append(order, key)
- // The index map starts out as [0 0 ... 0] and will increase
- // in the following order:
- // Once the first index (which increases the most slowly)
- // exceeds the length of that slice, we've seen everything.
- for indices[0] < len(expansions[order[0]]) {
- // Create a single element from the current state
- snapshot := map[string]string{}
- for i, j := range indices {
- snapshot[order[i]] = expansions[order[i]][j]
- maps = append(maps, snapshot)
- // Advance the index map. Start from the end of the
- // index map and increment by one. If we end up rolling
- // over from n - 1 to 0 (where n is the length of the
- // associated slice), then we also flip the index
- // immediately to the left.
- for j := len(indices) - 1; j >= 0; j-- {
- if j == 0 || indices[j] < len(expansions[order[j]]) {
--- a/state/state_test.go Mon Oct 02 11:21:46 2017 -0500
+++ b/state/state_test.go Mon Oct 02 12:06:42 2017 -0500
@@ -47,27 +47,28 @@
- Expect(mapEnv("$X")).To(Equal([]string{"$X"}))
- Expect(mapEnv("$FOO")).To(Equal([]string{"BAR"}))
+ Expect(mapEnv("$X")).To(ConsistOf([]string{"$X"})) + Expect(mapEnv("$FOO")).To(ConsistOf([]string{"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("$X")).To(ConsistOf([]string{"A", "B", "C"})) + Expect(mapEnv("$FOO")).To(ConsistOf([]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"}))
+ Expect(mapEnv("$X")).To(ConsistOf([]string{"A", "B", "C"})) + Expect(mapEnv("$FOO")).To(ConsistOf([]string{"BAR"})) + Expect(mapEnv("$BAR")).To(ConsistOf([]string{"B;A;R", "B;A;Z"})) Expect(st.PopExtendFrame()).To(BeNil())
Expect(st.PopExtendFrame()).To(BeNil())
- Expect(mapEnv("$X")).To(Equal([]string{"$X"}))
- Expect(mapEnv("$FOO")).To(Equal([]string{"BAR"}))
+ Expect(mapEnv("$X")).To(ConsistOf([]string{"$X"})) + Expect(mapEnv("$FOO")).To(ConsistOf([]string{"BAR"})) func (e *stateSuite) TestMapSliceComplex(t sweet.T) {
st.Environment = []string{"FOO=BAR"}
+ st.PushExtendFrame([]string{"X=A;B;C", "Y=D;E;F"}, []string{"X", "Y"}, ";") mapEnv := func(val string) []string {
mapped, err := st.MapSlice([]string{val}, st.Environment)
@@ -75,29 +76,64 @@
- st.PushExtendFrame([]string{"X=A;B;C"}, []string{"X"}, ";")
- st.PushExtendFrame([]string{"Y=D;E;F"}, []string{"Y"}, ";")
- Expect(mapEnv("BONK")).To(Equal([]string{"BONK"}))
- Expect(mapEnv("$FOO")).To(Equal([]string{"BAR"}))
+ Expect(mapEnv("BONK")).To(ConsistOf([]string{"BONK"})) + Expect(mapEnv("$FOO")).To(ConsistOf([]string{"BAR"})) - Expect(mapEnv("$X")).To(Equal([]string{"A", "B", "C"}))
- Expect(mapEnv("$Y")).To(Equal([]string{"D", "E", "F"}))
+ Expect(mapEnv("$X")).To(ConsistOf([]string{"A", "B", "C"})) + Expect(mapEnv("$Y")).To(ConsistOf([]string{"D", "E", "F"})) - Expect(mapEnv("x${X}x")).To(Equal([]string{"xAx", "xBx", "xCx"}))
- Expect(mapEnv("x${Y}x")).To(Equal([]string{"xDx", "xEx", "xFx"}))
+ Expect(mapEnv("x${X}x")).To(ConsistOf([]string{"xAx", "xBx", "xCx"})) + Expect(mapEnv("x${Y}x")).To(ConsistOf([]string{"xDx", "xEx", "xFx"})) - Expect(mapEnv("$X/$Y")).To(Equal([]string{
+ Expect(mapEnv("$X/$Y")).To(ConsistOf([]string{ +func (e *stateSuite) TestMapSliceIterative(t sweet.T) { + st.Environment = []string{"FOO=BAR"} + st.PushExtendFrame([]string{"X=A;x$Y", "Y=B;y$Z", "Z=C;D;E"}, []string{"X", "Y", "Z"}, ";") + mapEnv := func(val string) []string { + mapped, err := st.MapSlice([]string{val}, st.Environment) + Expect(err).To(BeNil()) + Expect(mapEnv("$X")).To(ConsistOf([]string{"A", "xB", "xyC", "xyD", "xyE"})) +func (e *stateSuite) TestMapSliceIndirect(t sweet.T) { + st.Environment = []string{"FOO=BAR"} + st.PushExtendFrame([]string{"X=$Y", "Y=A;$Z;C", "Z=B;$W", "W=D"}, []string{"X", "Y", "Z"}, ";") + mapEnv := func(val string) []string { + mapped, err := st.MapSlice([]string{val}, st.Environment) + Expect(err).To(BeNil()) + Expect(mapEnv("$X")).To(ConsistOf([]string{"A", "B", "C", "D"})) +func (e *stateSuite) TestMapSliceRecursive(t sweet.T) { + st.Environment = []string{"FOO=BAR"} + st.PushExtendFrame([]string{"X=A;x$Y", "Y=B;y$Z", "Z=C;D;$X"}, []string{"X", "Y", "Z"}, ";") + _, err := st.MapSlice([]string{"$X"}, st.Environment) + Expect(err).NotTo(BeNil()) + Expect(err.Error()).To(ContainSubstring("hit limit")) func (e *stateSuite) TestPopExtendFrameSize(t sweet.T) {
st.PushExtendFrame(nil, nil, "")