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

Code before:
			Cluster: ver.String(),
			Server:  offsetMinor(ver, 1).String(),
			Storage: ver.String(),
		})
		AssertProcessLogs(t, epc.Procs[i], "The server is ready to downgrade")
	}

	t.Log("Cluster is ready for downgrade")
}

func DowngradeCancel(t *testing.T, epc *EtcdProcessCluster, ver *semver.Version) {
	t.Logf("etcdctl downgrade cancel")
	c := epc.Etcdctl()
	testutils.ExecuteWithTimeout(t, 20*time.Second, func() {
		err := c.DowngradeCancel(context.TODO())
		require.NoError(t, err)
	})

	t.Log("Downgrade cancelled, validating if cluster is in the right state")
	for i := 0; i < len(epc.Procs); i++ {
		ValidateVersion(t, epc.Cfg, epc.Procs[i], version.Versions{
			Cluster: ver.String(),
			Server:  ver.String(),
			Storage: ver.String(),
		})
	}

	t.Log("Cluster downgrade cancellation is completed")
}

func DowngradeUpgradeMembers(t *testing.T, lg *zap.Logger, clus *EtcdProcessCluster, numberOfMembersToChange int, currentVersion, targetVersion *semver.Version) error {
	if lg == nil {
		lg = clus.lg
	}
	isDowngrade := targetVersion.LessThan(*currentVersion)
	opString := "upgrading"
	newExecPath := BinPath.Etcd
	if isDowngrade {
		opString = "downgrading"
		newExecPath = BinPath.EtcdLastRelease
	}
	membersToChange := rand.Perm(len(clus.Procs))[:numberOfMembersToChange]
	lg.Info(fmt.Sprintf("Test %s members", opString), zap.Any("members", membersToChange))

	// Need to wait health interval for cluster to prepare for downgrade/upgrade
	time.Sleep(etcdserver.HealthInterval)

	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)

Review comment (suggestion):
downgrade cancelling itself doesn't care about the version.  Users may cancel the downgrade anytime during the process, the members may have different versions. I think it's the test cases' responsibility to check the the versions based on their specific scenarios.



Code after:
			Cluster: ver.String(),
			Server:  offsetMinor(ver, 1).String(),
			Storage: ver.String(),
		})
		AssertProcessLogs(t, epc.Procs[i], "The server is ready to downgrade")
	}

	t.Log("Cluster is ready for downgrade")
}

func DowngradeCancel(t *testing.T, epc *EtcdProcessCluster) {
	t.Logf("etcdctl downgrade cancel")
	c := epc.Etcdctl()
	testutils.ExecuteWithTimeout(t, 20*time.Second, func() {
		err := c.DowngradeCancel(context.TODO())
		require.NoError(t, err)
	})

	t.Log("Cluster downgrade cancellation is completed")
}

func DowngradeUpgradeMembers(t *testing.T, lg *zap.Logger, clus *EtcdProcessCluster, numberOfMembersToChange int, currentVersion, targetVersion *semver.Version) error {
	if lg == nil {
		lg = clus.lg
	}
	isDowngrade := targetVersion.LessThan(*currentVersion)
	opString := "upgrading"
	newExecPath := BinPath.Etcd
	if isDowngrade {
		opString = "downgrading"
		newExecPath = BinPath.EtcdLastRelease
	}
	membersToChange := rand.Perm(len(clus.Procs))[:numberOfMembersToChange]
	lg.Info(fmt.Sprintf("Test %s members", opString), zap.Any("members", membersToChange))

	// Need to wait health interval for cluster to prepare for downgrade/upgrade
	time.Sleep(etcdserver.HealthInterval)

	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 {