Repository: etcd-io/etcd
File: tests/framework/e2e/downgrade.go (Go)

Code before:
	newExecPath := BinPath.Etcd
	if isDowngrade {
		opString = "downgrading"
		newExecPath = BinPath.EtcdLastRelease
	}

	for _, memberID := range membersToChange {
		member := clus.Procs[memberID]
		if member.Config().ExecPath == newExecPath {
			return fmt.Errorf("member:%s is already running with the %s target binary - %s", member.Config().Name, opString, member.Config().ExecPath)
		}
		lg.Info(fmt.Sprintf("%s member", opString), zap.String("member", member.Config().Name))
		if err := member.Stop(); err != nil {
			return err
		}
		member.Config().ExecPath = newExecPath
		lg.Info("Restarting member", zap.String("member", member.Config().Name))
		err := member.Start(context.TODO())
		if err != nil {
			return err
		}
	}
	lg.Info("Validating versions")
	for _, memberID := range membersToChange {
		member := clus.Procs[memberID]
		if isDowngrade {
			ValidateVersion(t, clus.Cfg, member, version.Versions{
				Cluster: targetVersion.String(),
				Server:  targetVersion.String(),
			})
		} else {
			ValidateVersion(t, clus.Cfg, member, version.Versions{
				Cluster: targetVersion.String(),
				Server:  targetVersion.String(),
			})
		}
	}
	return nil
}

func ValidateMemberVersions(t *testing.T, epc *EtcdProcessCluster, expect []*version.Versions) {
	for i := 0; i < len(epc.Procs); i++ {
		ValidateVersion(t, epc.Cfg, epc.Procs[i], *expect[i])
	}
	t.Log("Cluster member version validation after downgrade cancellation is completed")
}

func ValidateVersion(t *testing.T, cfg *EtcdProcessClusterConfig, member EtcdProcess, expect version.Versions) {
	testutils.ExecuteWithTimeout(t, 1*time.Minute, func() {
		for {
			result, err := getMemberVersionByCurl(cfg, member)

Review comment (suggestion):
Let's just merge the validation cases, no?
```
	for _, memberID := range membersToChange {
		member := clus.Procs[memberID]
		ValidateVersion(t, clus.Cfg, member, version.Versions{
			Cluster: targetVersion.String(),
			Server:  targetVersion.String(),
		})
	}
```

Code after:
	newExecPath := BinPath.Etcd
	if isDowngrade {
		opString = "downgrading"
		newExecPath = BinPath.EtcdLastRelease
	}

	for _, memberID := range membersToChange {
		member := clus.Procs[memberID]
		if member.Config().ExecPath == newExecPath {
			return fmt.Errorf("member:%s is already running with the %s target binary - %s", member.Config().Name, opString, member.Config().ExecPath)
		}
		lg.Info(fmt.Sprintf("%s member", opString), zap.String("member", member.Config().Name))
		if err := member.Stop(); err != nil {
			return err
		}
		member.Config().ExecPath = newExecPath
		lg.Info("Restarting member", zap.String("member", member.Config().Name))
		err := member.Start(context.TODO())
		if err != nil {
			return err
		}
	}
	lg.Info("Validating versions")
	for _, memberID := range membersToChange {
		member := clus.Procs[memberID]
		ValidateVersion(t, clus.Cfg, member, version.Versions{
			Cluster: targetVersion.String(),
			Server:  targetVersion.String(),
		})
	}
	return nil
}

func ValidateMemberVersions(t *testing.T, epc *EtcdProcessCluster, expect []*version.Versions) {
	for i := 0; i < len(epc.Procs); i++ {
		ValidateVersion(t, epc.Cfg, epc.Procs[i], *expect[i])
	}
	t.Log("Cluster member version validation after downgrade cancellation is completed")
}

func ValidateVersion(t *testing.T, cfg *EtcdProcessClusterConfig, member EtcdProcess, expect version.Versions) {
	testutils.ExecuteWithTimeout(t, 1*time.Minute, func() {
		for {
			result, err := getMemberVersionByCurl(cfg, member)
			if err != nil {
				cfg.Logger.Warn("failed to get member version and retrying", zap.Error(err), zap.String("member", member.Config().Name))
				time.Sleep(time.Second)
				continue
			}
			cfg.Logger.Info("Comparing versions", zap.String("member", member.Config().Name), zap.Any("got", result), zap.Any("want", expect))
			if err := compareMemberVersion(expect, result); err != nil {