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

Code before:
		if err != nil {
			return err
		}
	}

	t.Log("Waiting health interval to make sure the leader propagates version to new processes")
	time.Sleep(etcdserver.HealthInterval)

	lg.Info("Validating versions")
	clusterVersion := targetVersion
	if !isDowngrade {
		// if there is any member using old version binary, the clusterVersion should be old version.
		for _, member := range clus.Procs {
			mVersion, err := GetVersionFromBinary(member.Config().ExecPath)
			if err != nil {
				return fmt.Errorf("failed to get version from member %s binary: %w", member.Config().Name, err)
			}

			if mVersion.LessThan(*clusterVersion) {
				clusterVersion = mVersion
				break
			}
		}

		if downgradeEnabled {
			clusterVersion = currentVersion
		}
	}

	for _, memberID := range membersToChange {
		member := clus.Procs[memberID]
		ValidateVersion(t, clus.Cfg, member, version.Versions{
			Cluster: clusterVersion.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 {

Review comment (bug):
```suggestion
	if !isDowngrade {
		if downgradeEnabled {
			// If the downgrade isn't cancelled yet, then the cluster
			// version will always stay at the lower version, no matter
			// what's the binary version of each member.
			clusterVersion = currentVersion
		} else {
			// If the downgrade has already been cancelled, then the
			// cluster version is the minimal server version.
			minVer, err := clus.MinServerVersion()
			if err != nil {
				return fmt.Errorf("failed to get min server version: %w", err)
			}
			clusterVersion = minVer
		}
	}	
```

Also add a method below,

```
func (epc *EtcdProcessCluster) MinServerVersion() (*semver.Version, error) {
	var minVersion *semver.Version
	for _, member := range epc.Procs {
		ver, err := GetVersionFromBinary(member.Config().ExecPath)
		if err != nil {
			return nil, fmt.Errorf("failed to get version from member %s binary: %w", member.Config().Name, err)
		}

		if minVersion == nil || ver.LessThan(*minVersion) {
			minVersion = ver
		}
	}
	return minVersion, nil
}
```

Code after:
		if err != nil {
			return err
		}
	}

	t.Log("Waiting health interval to make sure the leader propagates version to new processes")
	time.Sleep(etcdserver.HealthInterval)

	lg.Info("Validating versions")
	clusterVersion := targetVersion
	if !isDowngrade {
		if downgradeEnabled {
			// If the downgrade isn't cancelled yet, then the cluster
			// version will always stay at the lower version, no matter
			// what's the binary version of each member.
			clusterVersion = currentVersion
		} else {
			// If the downgrade has already been cancelled, then the
			// cluster version is the minimal server version.
			minVer, err := clus.MinServerVersion()
			if err != nil {
				return fmt.Errorf("failed to get min server version: %w", err)
			}
			clusterVersion = minVer
		}
	}

	for _, memberID := range membersToChange {
		member := clus.Procs[memberID]
		ValidateVersion(t, clus.Cfg, member, version.Versions{
			Cluster: clusterVersion.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)