Repository: argoproj/argo (go)

Issue:
argo get/watch should list parallel entries in order, not alphabetize
Checklist:

 * [x] I've included the version.
 * [x] I've included reproduction steps.
 * [x] I've included the workflow YAML.
 * [x] I've included the logs.
 
**What happened**:

When running any parallel workflow (`withParam`, `withSequence`, or `withItems`), the fanned out entries are displayed in alphabetical order of the step names. If there are 10 or fewer, this order matches the standard order, since the names are all prepended with digits. But if you go past 10, the alphabetical order and numerical order no longer agree: 10-19 come before 1, 20-29 come before 2, and so on.

**What you expected to happen**:

They should instead be ordered by the numbers in those step names. In addition to the aesthetic benefits, this would also make it a lot easier to see exactly how many parallel entries there are -- just look at the number of the last one (and add one).

**How to reproduce it (as minimally and precisely as possible)**:

Run any parallel workflow with fan out greater than 10. Here's an example:

```
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: many-items-
spec:
  entrypoint: parallel-sleep
  templates:
  - name: parallel-sleep
    steps:
    - - name: sleep
        template: sleep
        withItems:
        - zero
        - one
        - two
        - three
        - four
        - five
        - six
        - seven
        - eight
        - nine
        - ten
        - eleven
        - twelve

  - name: sleep
    container:
      image: alpine:latest
      command: [sh, -c, sleep 10]
```

In `argo watch` or `argo get`, this is represented as follows:
```
STEP                     TEMPLATE        PODNAME                      DURATION  MESSAGE
 ✔ many-items-bp774      parallel-sleep
 └-·-✔ sleep(0:zero)     sleep           many-items-bp774-1191717986  18s
   ├-✔ sleep(10:ten)     sleep           many-items-bp774-1715531170  25s
   ├-✔ sleep(11:eleven)  sleep           many-items-bp774-2634080843  23s
   ├-✔ sleep(12:twelve)  sleep           many-items-bp774-153115818   24s
   ├-✔ sleep(1:one)      sleep           many-items-bp774-3071867483  20s
   ├-✔ sleep(2:two)      sleep           many-items-bp774-2507206310  18s
   ├-✔ sleep(3:three)    sleep           many-items-bp774-4054797277  25s
   ├-✔ sleep(4:four)     sleep           many-items-bp774-383720484   20s
   ├-✔ sleep(5:five)     sleep           many-items-bp774-3297451019  20s
   ├-✔ sleep(6:six)      sleep           many-items-bp774-1930420580  25s
   ├-✔ sleep(7:seven)    sleep           many-items-bp774-3209167226  20s
   ├-✔ sleep(8:eight)    sleep           many-items-bp774-1893091255  21s
   └-✔ sleep(9:nine)     sleep           many-items-bp774-243241999   25s
```
Just glancing at the bottom, you'd think that only 10 entries ran, but the last three are misordered.

This is on Argo v2.8.1.

---
<!-- Issue Author: Don't delete this message to encourage other users to support your issue! -->
**Message from the maintainers**:

If you are impacted by this bug please add a 👍 reaction to this issue! We often sort issues this way to know what to prioritize.


Pull request description:
fix(cli): Sort expanded nodes by index. Closes #3145
Closes: #3145

Patch:
diff --git a/cmd/argo/commands/get.go b/cmd/argo/commands/get.go
index 3e76da8e9..79f239be9 100644
--- a/cmd/argo/commands/get.go
+++ b/cmd/argo/commands/get.go
@@ -1,6 +1,7 @@
 package commands
 
 import (
+	"bytes"
 	"encoding/json"
 	"fmt"
 	"log"
@@ -18,6 +19,7 @@ import (
 	"github.com/argoproj/argo/cmd/argo/commands/client"
 	workflowpkg "github.com/argoproj/argo/pkg/apiclient/workflow"
 	wfv1 "github.com/argoproj/argo/pkg/apis/workflow/v1alpha1"
+	argoutil "github.com/argoproj/argo/util"
 	"github.com/argoproj/argo/util/printer"
 	"github.com/argoproj/argo/workflow/util"
 )
@@ -99,65 +101,66 @@ func printWorkflow(wf *wfv1.Workflow, getArgs getFlags) {
 		outBytes, _ := yaml.Marshal(wf)
 		fmt.Print(string(outBytes))
 	case "wide", "":
-		printWorkflowHelper(wf, getArgs)
+		fmt.Print(printWorkflowHelper(wf, getArgs))
 	default:
 		log.Fatalf("Unknown output format: %s", getArgs.output)
 	}
 }
 
-func printWorkflowHelper(wf *wfv1.Workflow, getArgs getFlags) {
+func printWorkflowHelper(wf *wfv1.Workflow, getArgs getFlags) string {
 	const fmtStr = "%-20s %v\n"
-	fmt.Printf(fmtStr, "Name:", wf.ObjectMeta.Name)
-	fmt.Printf(fmtStr, "Namespace:", wf.ObjectMeta.Namespace)
+	out := ""
+	out += fmt.Sprintf(fmtStr, "Name:", wf.ObjectMeta.Name)
+	out += fmt.Sprintf(fmtStr, "Namespace:", wf.ObjectMeta.Namespace)
 	serviceAccount := wf.Spec.ServiceAccountName
 	if serviceAccount == "" {
 		serviceAccount = "default"
 	}
-	fmt.Printf(fmtStr, "ServiceAccount:", serviceAccount)
-	fmt.Printf(fmtStr, "Status:", printer.WorkflowStatus(wf))
+	out += fmt.Sprintf(fmtStr, "ServiceAccount:", serviceAccount)
+	out += fmt.Sprintf(fmtStr, "Status:", printer.WorkflowStatus(wf))
 	if wf.Status.Message != "" {
-		fmt.Printf(fmtStr, "Message:", wf.Status.Message)
+		out += fmt.Sprintf(fmtStr, "Message:", wf.Status.Message)
 	}
 	if len(wf.Status.Conditions) > 0 {
-		fmt.Print(wf.Status.Conditions.DisplayString(fmtStr, workflowConditionIconMap))
+		out += wf.Status.Conditions.DisplayString(fmtStr, workflowConditionIconMap)
 	}
-	fmt.Printf(fmtStr, "Created:", humanize.Timestamp(wf.ObjectMeta.CreationTimestamp.Time))
+	out += fmt.Sprintf(fmtStr, "Created:", humanize.Timestamp(wf.ObjectMeta.CreationTimestamp.Time))
 	if !wf.Status.StartedAt.IsZero() {
-		fmt.Printf(fmtStr, "Started:", humanize.Timestamp(wf.Status.StartedAt.Time))
+		out += fmt.Sprintf(fmtStr, "Started:", humanize.Timestamp(wf.Status.StartedAt.Time))
 	}
 	if !wf.Status.FinishedAt.IsZero() {
-		fmt.Printf(fmtStr, "Finished:", humanize.Timestamp(wf.Status.FinishedAt.Time))
+		out += fmt.Sprintf(fmtStr, "Finished:", humanize.Timestamp(wf.Status.FinishedAt.Time))
 	}
 	if !wf.Status.StartedAt.IsZero() {
-		fmt.Printf(fmtStr, "Duration:", humanize.RelativeDuration(wf.Status.StartedAt.Time, wf.Status.FinishedAt.Time))
+		out += fmt.Sprintf(fmtStr, "Duration:", humanize.RelativeDuration(wf.Status.StartedAt.Time, wf.Status.FinishedAt.Time))
 	}
 	if !wf.Status.ResourcesDuration.IsZero() {
-		fmt.Printf(fmtStr, "ResourcesDuration:", wf.Status.ResourcesDuration)
+		out += fmt.Sprintf(fmtStr, "ResourcesDuration:", wf.Status.ResourcesDuration)
 	}
 
 	if len(wf.Spec.Arguments.Parameters) > 0 {
-		fmt.Printf(fmtStr, "Parameters:", "")
+		out += fmt.Sprintf(fmtStr, "Parameters:", "")
 		for _, param := range wf.Spec.Arguments.Parameters {
 			if param.Value == nil {
 				continue
 			}
-			fmt.Printf(fmtStr, "  "+param.Name+":", *param.Value)
+			out += fmt.Sprintf(fmtStr, "  "+param.Name+":", *param.Value)
 		}
 	}
 	if wf.Status.Outputs != nil {
 		if len(wf.Status.Outputs.Parameters) > 0 {
-			fmt.Printf(fmtStr, "Output Parameters:", "")
+			out += fmt.Sprintf(fmtStr, "Output Parameters:", "")
 			for _, param := range wf.Status.Outputs.Parameters {
-				fmt.Printf(fmtStr, "  "+param.Name+":", *param.Value)
+				out += fmt.Sprintf(fmtStr, "  "+param.Name+":", *param.Value)
 			}
 		}
 		if len(wf.Status.Outputs.Artifacts) > 0 {
-			fmt.Printf(fmtStr, "Output Artifacts:", "")
+			out += fmt.Sprintf(fmtStr, "Output Artifacts:", "")
 			for _, art := range wf.Status.Outputs.Artifacts {
 				if art.S3 != nil {
-					fmt.Printf(fmtStr, "  "+art.Name+":", art.S3.String())
+					out += fmt.Sprintf(fmtStr, "  "+art.Name+":", art.S3.String())
 				} else if art.Artifactory != nil {
-					fmt.Printf(fmtStr, "  "+art.Name+":", art.Artifactory.String())
+					out += fmt.Sprintf(fmtStr, "  "+art.Name+":", art.Artifactory.String())
 				}
 			}
 		}
@@ -169,8 +172,9 @@ func printWorkflowHelper(wf *wfv1.Workflow, getArgs getFlags) {
 		printTree = false
 	}
 	if printTree {
-		w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
-		fmt.Println()
+		writerBuffer := new(bytes.Buffer)
+		w := tabwriter.NewWriter(writerBuffer, 0, 0, 2, ' ', 0)
+		out += "\n"
 		// apply a dummy FgDefault format to align tab writer with the rest of the columns
 		if getArgs.output == "wide" {
 			_, _ = fmt.Fprintf(w, "%s\tTEMPLATE\tPODNAME\tDURATION\tARTIFACTS\tMESSAGE\tRESOURCESDURATION\tNODENAME\n", ansiFormat("STEP", FgDefault))
@@ -194,7 +198,9 @@ func printWorkflowHelper(wf *wfv1.Workflow, getArgs getFlags) {
 			onExitRoot.renderNodes(w, wf, 0, " ", " ", getArgs)
 		}
 		_ = w.Flush()
+		out += writerBuffer.String()
 	}
+	return out
 }
 
 type nodeInfoInterface interface {
@@ -268,6 +274,17 @@ func insertSorted(wf *wfv1.Workflow, sortedArray []renderNode, item renderNode)
 			// get some consistent printing
 			insertName := item.getNodeStatus(wf).DisplayName
 			equalName := existingItem.getNodeStatus(wf).DisplayName
+
+			// If they are both elements of a list (e.g. withParams, withSequence, etc.) order by index number instead of
+			// alphabetical order
+			insertIndex := argoutil.RecoverIndexFromNodeName(insertName)
+			equalIndex := argoutil.RecoverIndexFromNodeName(equalName)
+			if insertIndex >= 0 && equalIndex >= 0 {
+				if insertIndex < equalIndex {
+					break
+				}
+			}
+
 			if insertName < equalName {
 				break
 			}
diff --git a/cmd/argo/commands/watch.go b/cmd/argo/commands/watch.go
index 1d8f1547d..dda0d1352 100644
--- a/cmd/argo/commands/watch.go
+++ b/cmd/argo/commands/watch.go
@@ -81,5 +81,5 @@ func printWorkflowStatus(wf *wfv1.Workflow, getArgs getFlags) {
 	errors.CheckError(err)
 	print("\033[H\033[2J")
 	print("\033[0;0H")
-	printWorkflowHelper(wf, getArgs)
+	fmt.Print(printWorkflowHelper(wf, getArgs))
 }
diff --git a/docs/rest-api.md b/docs/rest-api.md
index cea98e001..3ad6ef5b0 100644
--- a/docs/rest-api.md
+++ b/docs/rest-api.md
@@ -18,7 +18,7 @@ curl -H "Authorization: $token" http://localhost:2746/api/v1/workflows/argo
 To view the API:
  
 1. Open [https://editor.swagger.io/](https://editor.swagger.io/)
-2. Copy and paste ../api/argo-server/swagger.json
+2. Copy and paste `../api/openapi-spec/swagger.json`
 
 ## Classic API
 
diff --git a/util/util.go b/util/util.go
index bc07029dd..963823288 100644
--- a/util/util.go
+++ b/util/util.go
@@ -2,6 +2,8 @@ package util
 
 import (
 	"io/ioutil"
+	"strconv"
+	"strings"
 
 	log "github.com/sirupsen/logrus"
 	apiv1 "k8s.io/api/core/v1"
@@ -75,3 +77,16 @@ func MergeParameters(params ...[]wfv1.Parameter) []wfv1.Parameter {
 	}
 	return resultParams
 }
+
+func RecoverIndexFromNodeName(name string) int {
+	startIndex := strings.Index(name, "(")
+	endIndex := strings.Index(name, ":")
+	if startIndex < 0 || endIndex < 0 {
+		return -1
+	}
+	out, err := strconv.Atoi(name[startIndex+1 : endIndex])
+	if err != nil {
+		return -1
+	}
+	return out
+}
diff --git a/workflow/controller/operator.go b/workflow/controller/operator.go
index 2d9e82201..39f966e1c 100644
--- a/workflow/controller/operator.go
+++ b/workflow/controller/operator.go
@@ -2321,7 +2321,7 @@ func processItem(fstTmpl *fasttemplate.Template, name string, index int, item wf
 	switch item.Type {
 	case wfv1.String, wfv1.Number, wfv1.Bool:
 		replaceMap["item"] = fmt.Sprintf("%v", item)
-		newName = fmt.Sprintf("%s(%d:%v)", name, index, item)
+		newName = generateNodeName(name, index, item)
 	case wfv1.Map:
 		// Handle the case when withItems is a list of maps.
 		// vals holds stringified versions of the map items which are incorporated as part of the step name.
@@ -2342,14 +2342,14 @@ func processItem(fstTmpl *fasttemplate.Template, name string, index int, item wf
 
 		// sort the values so that the name is deterministic
 		sort.Strings(vals)
-		newName = fmt.Sprintf("%s(%d:%v)", name, index, strings.Join(vals, ","))
+		newName = generateNodeName(name, index, strings.Join(vals, ","))
 	case wfv1.List:
 		byteVal, err := json.Marshal(item.ListVal)
 		if err != nil {
 			return "", errors.InternalWrapError(err)
 		}
 		replaceMap["item"] = string(byteVal)
-		newName = fmt.Sprintf("%s(%d:%v)", name, index, item.ListVal)
+		newName = generateNodeName(name, index, item.ListVal)
 	default:
 		return "", errors.Errorf(errors.CodeBadRequest, "withItems[%d] expected string, number, list, or map. received: %v", index, item)
 	}
@@ -2364,6 +2364,14 @@ func processItem(fstTmpl *fasttemplate.Template, name string, index int, item wf
 	return newName, nil
 }
 
+func generateNodeName(name string, index int, desc interface{}) string {
+	newName := fmt.Sprintf("%s(%d:%v)", name, index, desc)
+	if out := util.RecoverIndexFromNodeName(newName); out != index {
+		panic(fmt.Sprintf("unrecoverable digit in generateName; wanted '%d' and got '%d'", index, out))
+	}
+	return newName
+}
+
 func expandSequence(seq *wfv1.Sequence) ([]wfv1.Item, error) {
 	var start, end int
 	var err error


Test patch:
diff --git a/cmd/argo/commands/get_test.go b/cmd/argo/commands/get_test.go
index aee85f38f..76594d974 100644
--- a/cmd/argo/commands/get_test.go
+++ b/cmd/argo/commands/get_test.go
@@ -7,6 +7,7 @@ import (
 	"text/tabwriter"
 	"time"
 
+	"github.com/ghodss/yaml"
 	"github.com/stretchr/testify/assert"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
@@ -103,3 +104,246 @@ func TestStatusToNodeFieldSelector(t *testing.T) {
 	one := statusToNodeFieldSelector("Running")
 	assert.Equal(t, "phase=Running", one)
 }
+
+var indexTest = `apiVersion: argoproj.io/v1alpha1
+kind: Workflow
+metadata:
+  creationTimestamp: "2020-06-02T16:04:21Z"
+  generateName: many-items-
+  generation: 32
+  labels:
+    workflows.argoproj.io/completed: "true"
+    workflows.argoproj.io/phase: Succeeded
+  name: many-items-z26lj
+  namespace: argo
+  resourceVersion: "5102"
+  selfLink: /apis/argoproj.io/v1alpha1/namespaces/argo/workflows/many-items-z26lj
+  uid: d21f092a-f659-4300-bd69-983a9912a379
+spec:
+  arguments: {}
+  entrypoint: parallel-sleep
+  templates:
+  - arguments: {}
+    inputs: {}
+    metadata: {}
+    name: parallel-sleep
+    outputs: {}
+    steps:
+    - - arguments: {}
+        name: sleep
+        template: sleep
+        withItems:
+        - zero
+        - one
+        - two
+        - three
+        - four
+        - five
+        - six
+        - seven
+        - eight
+        - nine
+        - ten
+        - eleven
+        - twelve
+  - arguments: {}
+    container:
+      command:
+      - sh
+      - -c
+      - sleep 10
+      image: alpine:latest
+      name: ""
+      resources: {}
+    inputs: {}
+    metadata: {}
+    name: sleep
+    outputs: {}
+status:
+  conditions:
+  - status: "True"
+    type: Completed
+  finishedAt: "2020-06-02T16:05:01Z"
+  nodes:
+    many-items-z26lj:
+      children:
+      - many-items-z26lj-1414877240
+      displayName: many-items-z26lj
+      finishedAt: "2020-06-02T16:05:01Z"
+      id: many-items-z26lj
+      name: many-items-z26lj
+      outboundNodes:
+      - many-items-z26lj-1939921510
+      - many-items-z26lj-2156977535
+      - many-items-z26lj-3409403178
+      - many-items-z26lj-1774150289
+      - many-items-z26lj-3491220632
+      - many-items-z26lj-1942531647
+      - many-items-z26lj-3178865096
+      - many-items-z26lj-3031375822
+      - many-items-z26lj-753834747
+      - many-items-z26lj-2619926859
+      - many-items-z26lj-1052882686
+      - many-items-z26lj-3011405271
+      - many-items-z26lj-3126938806
+      phase: Succeeded
+      startedAt: "2020-06-02T16:04:21Z"
+      templateName: parallel-sleep
+      type: Steps
+    many-items-z26lj-753834747:
+      boundaryID: many-items-z26lj
+      displayName: sleep(8:eight)
+      finishedAt: "2020-06-02T16:04:42Z"
+      id: many-items-z26lj-753834747
+      name: many-items-z26lj[0].sleep(8:eight)
+      startedAt: "2020-06-02T16:04:21Z"
+      templateName: sleep
+      type: Pod
+    many-items-z26lj-1052882686:
+      boundaryID: many-items-z26lj
+      displayName: sleep(10:ten)
+      finishedAt: "2020-06-02T16:04:45Z"
+      id: many-items-z26lj-1052882686
+      name: many-items-z26lj[0].sleep(10:ten)
+      startedAt: "2020-06-02T16:04:22Z"
+      templateName: sleep
+      type: Pod
+    many-items-z26lj-1414877240:
+      boundaryID: many-items-z26lj
+      children:
+      - many-items-z26lj-1939921510
+      - many-items-z26lj-2156977535
+      - many-items-z26lj-3409403178
+      - many-items-z26lj-1774150289
+      - many-items-z26lj-3491220632
+      - many-items-z26lj-1942531647
+      - many-items-z26lj-3178865096
+      - many-items-z26lj-3031375822
+      - many-items-z26lj-753834747
+      - many-items-z26lj-2619926859
+      - many-items-z26lj-1052882686
+      - many-items-z26lj-3011405271
+      - many-items-z26lj-3126938806
+      displayName: '[0]'
+      finishedAt: "2020-06-02T16:05:01Z"
+      id: many-items-z26lj-1414877240
+      name: many-items-z26lj[0]
+      phase: Succeeded
+      startedAt: "2020-06-02T16:04:21Z"
+      templateName: parallel-sleep
+      type: StepGroup
+    many-items-z26lj-1774150289:
+      boundaryID: many-items-z26lj
+      displayName: sleep(3:three)
+      finishedAt: "2020-06-02T16:04:54Z"
+      id: many-items-z26lj-1774150289
+      name: many-items-z26lj[0].sleep(3:three)
+      startedAt: "2020-06-02T16:04:21Z"
+      templateName: sleep
+      type: Pod
+    many-items-z26lj-1939921510:
+      boundaryID: many-items-z26lj
+      displayName: sleep(0:zero)
+      finishedAt: "2020-06-02T16:04:48Z"
+      id: many-items-z26lj-1939921510
+      name: many-items-z26lj[0].sleep(0:zero)
+      startedAt: "2020-06-02T16:04:21Z"
+      templateName: sleep
+      type: Pod
+    many-items-z26lj-1942531647:
+      boundaryID: many-items-z26lj
+      displayName: sleep(5:five)
+      finishedAt: "2020-06-02T16:04:47Z"
+      id: many-items-z26lj-1942531647
+      name: many-items-z26lj[0].sleep(5:five)
+      phase: Succeeded
+      startedAt: "2020-06-02T16:04:21Z"
+      templateName: sleep
+      type: Pod
+    many-items-z26lj-2156977535:
+      boundaryID: many-items-z26lj
+      displayName: sleep(1:one)
+      finishedAt: "2020-06-02T16:04:53Z"
+      id: many-items-z26lj-2156977535
+      name: many-items-z26lj[0].sleep(1:one)
+      startedAt: "2020-06-02T16:04:21Z"
+      templateName: sleep
+      type: Pod
+    many-items-z26lj-2619926859:
+      boundaryID: many-items-z26lj
+      displayName: sleep(9:nine)
+      finishedAt: "2020-06-02T16:04:40Z"
+      id: many-items-z26lj-2619926859
+      name: many-items-z26lj[0].sleep(9:nine)
+      startedAt: "2020-06-02T16:04:21Z"
+      templateName: sleep
+      type: Pod
+    many-items-z26lj-3011405271:
+      boundaryID: many-items-z26lj
+      displayName: sleep(11:eleven)
+      finishedAt: "2020-06-02T16:04:44Z"
+      id: many-items-z26lj-3011405271
+      name: many-items-z26lj[0].sleep(11:eleven)
+      startedAt: "2020-06-02T16:04:22Z"
+      templateName: sleep
+      type: Pod
+    many-items-z26lj-3031375822:
+      boundaryID: many-items-z26lj
+      displayName: sleep(7:seven)
+      finishedAt: "2020-06-02T16:04:57Z"
+      id: many-items-z26lj-3031375822
+      name: many-items-z26lj[0].sleep(7:seven)
+      startedAt: "2020-06-02T16:04:21Z"
+      templateName: sleep
+      type: Pod
+    many-items-z26lj-3126938806:
+      boundaryID: many-items-z26lj
+      displayName: sleep(12:twelve)
+      finishedAt: "2020-06-02T16:04:59Z"
+      id: many-items-z26lj-3126938806
+      name: many-items-z26lj[0].sleep(12:twelve)
+      startedAt: "2020-06-02T16:04:22Z"
+      templateName: sleep
+      type: Pod
+    many-items-z26lj-3178865096:
+      boundaryID: many-items-z26lj
+      displayName: sleep(6:six)
+      finishedAt: "2020-06-02T16:04:56Z"
+      id: many-items-z26lj-3178865096
+      name: many-items-z26lj[0].sleep(6:six)
+      startedAt: "2020-06-02T16:04:21Z"
+      templateName: sleep
+      type: Pod
+    many-items-z26lj-3409403178:
+      boundaryID: many-items-z26lj
+      displayName: sleep(2:two)
+      finishedAt: "2020-06-02T16:04:51Z"
+      id: many-items-z26lj-3409403178
+      name: many-items-z26lj[0].sleep(2:two)
+      startedAt: "2020-06-02T16:04:21Z"
+      templateName: sleep
+      type: Pod
+    many-items-z26lj-3491220632:
+      boundaryID: many-items-z26lj
+      displayName: sleep(4:four)
+      finishedAt: "2020-06-02T16:04:50Z"
+      id: many-items-z26lj-3491220632
+      name: many-items-z26lj[0].sleep(4:four)
+      phase: Succeeded
+      startedAt: "2020-06-02T16:04:21Z"
+      templateName: sleep
+      type: Pod
+  phase: Succeeded
+  startedAt: "2020-06-02T16:04:21Z"
+`
+
+func TestIndexOrdering(t *testing.T) {
+	var wf wfv1.Workflow
+	err := yaml.Unmarshal([]byte(indexTest), &wf)
+	if assert.NoError(t, err) {
+		assert.Contains(t, printWorkflowHelper(&wf, getFlags{}), `         
+   ├- sleep(9:nine)     sleep           many-items-z26lj-2619926859  19s         
+   ├- sleep(10:ten)     sleep           many-items-z26lj-1052882686  23s         
+   ├- sleep(11:eleven)  sleep           many-items-z26lj-3011405271  22s`)
+	}
+}
diff --git a/test/e2e/functional_test.go b/test/e2e/functional_test.go
index 75485e969..1537d46f7 100644
--- a/test/e2e/functional_test.go
+++ b/test/e2e/functional_test.go
@@ -456,7 +456,7 @@ func (s *FunctionalSuite) TestDAGDepends() {
 		Workflow("@functional/dag-depends.yaml").
 		When().
 		SubmitWorkflow().
-		WaitForWorkflow(30 * time.Second).
+		WaitForWorkflow(45 * time.Second).
 		Then().
 		ExpectWorkflow(func(t *testing.T, _ *metav1.ObjectMeta, status *wfv1.WorkflowStatus) {
 			assert.Equal(t, wfv1.NodeFailed, status.Phase)
diff --git a/util/util_test.go b/util/util_test.go
index 8aeb1c21a..1fad3b1dd 100644
--- a/util/util_test.go
+++ b/util/util_test.go
@@ -49,3 +49,12 @@ func TestMergeParameters(t *testing.T) {
 	})
 
 }
+
+func TestRecoverIndexFromNodeName(t *testing.T) {
+	out := RecoverIndexFromNodeName("sleep(10:ten)")
+	assert.Equal(t, 10, out)
+	out = RecoverIndexFromNodeName("sleep(17:[foobar]])")
+	assert.Equal(t, 17, out)
+	out = RecoverIndexFromNodeName("sleep(1:a;skldfja)")
+	assert.Equal(t, 1, out)
+}
diff --git a/workflow/controller/operator_test.go b/workflow/controller/operator_test.go
index 21743bdb2..a098b3bd5 100644
--- a/workflow/controller/operator_test.go
+++ b/workflow/controller/operator_test.go
@@ -3694,6 +3694,18 @@ func TestNoOnExitWhenSkipped(t *testing.T) {
 	assert.Nil(t, woc.getNodeByName("B.onExit"))
 }
 
+func TestGenerateNodeName(t *testing.T) {
+	assert.Equal(t, "sleep(10:ten)", generateNodeName("sleep", 10, "ten"))
+	assert.Equal(t, `sleep(10:[{"foo":"bar"}])`, generateNodeName("sleep", 10, []wfv1.ItemValue{{
+		Type:   wfv1.Map,
+		MapVal: map[string]string{"foo": "bar"},
+	}}))
+	assert.Equal(t, `sleep(10:[10])`, generateNodeName("sleep", 10, []wfv1.ItemValue{{
+		Type:   wfv1.Number,
+		NumVal: "10",
+	}}))
+}
+
 // This tests that we don't wait a backoff if it would exceed the maxDuration anyway.
 func TestPanicMetric(t *testing.T) {
 	wf := unmarshalWF(noOnExitWhenSkipped)