Skip to content

Commit

Permalink
install: fix update of tt master
Browse files Browse the repository at this point in the history
It was not possible to update tt using `tt install tt master` checked
that a binary file existed for a branch and rejected to reinstall if so.

After the patch `tt install` updates a tt if commit hashes of the
installed tt and a last commit on the branch are different.

Closes #854
  • Loading branch information
themilchenko authored and psergee committed Jul 29, 2024
1 parent a06a5d7 commit c8f2df1
Show file tree
Hide file tree
Showing 6 changed files with 285 additions and 6 deletions.
24 changes: 23 additions & 1 deletion cli/cmdcontext/cmdcontext.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"os/exec"
"strings"

"github.com/tarantool/tt/cli/util"
"github.com/tarantool/tt/cli/version"
"github.com/tarantool/tt/lib/integrity"
)
Expand Down Expand Up @@ -61,6 +62,26 @@ func (tntCli *TarantoolCli) GetVersion() (version.Version, error) {
return tntCli.version, nil
}

// GetTtVersion returns version of Tt provided by its executable path.
func GetTtVersion(pathToBin string) (version.Version, error) {
if !util.IsRegularFile(pathToBin) {
return version.Version{}, fmt.Errorf("file %q not found", pathToBin)
}

output, err := exec.Command(pathToBin, "--self", "version",
"--commit").Output()
if err != nil {
return version.Version{}, fmt.Errorf("failed to get tt version: %s", err)
}

ttVersion, err := version.ParseTt(string(output))
if err != nil {
return version.Version{}, err
}

return ttVersion, nil
}

// CliCtx - CLI context. Contains flags passed when starting
// Tarantool CLI and some other parameters.
type CliCtx struct {
Expand All @@ -85,7 +106,8 @@ type CliCtx struct {
TarantoolCli TarantoolCli
// IntegrityCheck is a public key used for integrity check.
IntegrityCheck string
// This flag disables searching of other tt versions to run instead of the current one.
// This flag disables searching of other tt versions to run
// instead of the current one.
IsSelfExec bool
// NoPrompt flag needs to skip cli interaction using default behavior.
NoPrompt bool
Expand Down
77 changes: 77 additions & 0 deletions cli/cmdcontext/cmdcontext_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cmdcontext

import (
"fmt"
"os"
"path/filepath"
"testing"
Expand Down Expand Up @@ -79,3 +80,79 @@ exit 1`),
assert.ErrorContains(t, err, "failed to get tarantool version: exit status 1")
assert.Equal(t, version.Version{}, tntVersion)
}

func TestTtCli_GetVersion(t *testing.T) {
tmpDir := t.TempDir()

type testCase struct {
name string
versionToCheck string
expectedVer version.Version
isErr bool
expectedErrMsg string
}

cases := []testCase{
{
name: "basic",
versionToCheck: "2.3.1.f7cc1de\n",
expectedVer: version.Version{
Major: 2,
Minor: 3,
Patch: 1,
Hash: "f7cc1de",
Str: "2.3.1.f7cc1de",
},
isErr: false,
expectedErrMsg: "",
},
{
name: "parse error",
versionToCheck: "2.w.1.f7cc1de",
expectedVer: version.Version{},
isErr: true,
expectedErrMsg: `strconv.ParseUint: parsing "w": invalid syntax`,
},
{
name: "no dots in version",
versionToCheck: "2131f7cc1de",
expectedVer: version.Version{},
isErr: true,
expectedErrMsg: fmt.Sprintf(`failed to parse version "2131f7cc1de\n":` +
` format is not valid`),
},
{
name: "version does not match",
versionToCheck: "2.1.3.1.f7cc1de",
expectedVer: version.Version{},
isErr: true,
expectedErrMsg: fmt.Sprintf(`the version of "2.1.3.1" does not match` +
` <major>.<minor>.<patch> format`),
},
{
name: "hash does not match",
versionToCheck: "2.1.3.f7cc1de_",
expectedVer: version.Version{},
isErr: true,
expectedErrMsg: `hash "f7cc1de_" has a wrong format`,
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
err := os.WriteFile(filepath.Join(tmpDir, "tt.sh"),
[]byte(fmt.Sprintf(`#!/bin/bash
echo "%s"`, tc.versionToCheck)),
0755)
require.NoError(t, err)

ttVersion, err := GetTtVersion(filepath.Join(tmpDir, "tt.sh"))
if tc.isErr {
require.EqualError(t, err, tc.expectedErrMsg)
} else {
require.NoError(t, err)
}
require.Equal(t, tc.expectedVer, ttVersion)
})
}
}
38 changes: 33 additions & 5 deletions cli/install/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -549,16 +549,38 @@ func installTt(binDir string, installCtx InstallCtx, distfiles string) error {
}

// Check if that version is already installed.
// If it is installed, check if the newest version exists.
if !installCtx.Reinstall {
log.Infof("Checking existing...")
if util.IsRegularFile(filepath.Join(binDir, versionStr)) {
log.Infof("%s version of tt already exists, updating symlink...", versionStr)
err := util.CreateSymlink(versionStr, filepath.Join(binDir, "tt"), true)
pathToBin := filepath.Join(binDir, versionStr)
if util.IsRegularFile(pathToBin) {
isBinExecutable, err := util.IsExecOwner(pathToBin)
if err != nil {
return err
}
log.Infof("Done")
return err

isUpdatePossible, err := isUpdatePossible(installCtx,
pathToBin,
search.ProgramTt,
ttVersion,
distfiles,
isBinExecutable)
if err != nil {
return err
}

if !isUpdatePossible {
log.Infof("%s version of tt already exists, updating symlink...", versionStr)
err := util.CreateSymlink(versionStr, filepath.Join(binDir, search.ProgramTt), true)
if err != nil {
return err
}
log.Infof("Done")
return nil
}
log.Info("Found newest commit of tt in master")
// Reduce the case to a reinstallation.
installCtx.Reinstall = true
}
}

Expand Down Expand Up @@ -1234,6 +1256,12 @@ func isUpdatePossible(installCtx InstallCtx,
"of an installed %s", program)
}
curBinHash = binVersion.Hash[1:]
} else if program == search.ProgramTt {
ttVer, err := cmdcontext.GetTtVersion(pathToBin)
if err != nil {
return false, err
}
curBinHash = ttVer.Hash
}
}
}
Expand Down
47 changes: 47 additions & 0 deletions cli/version/version_tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package version
import (
"fmt"
"regexp"
"strings"

"github.com/tarantool/tt/cli/util"
)
Expand Down Expand Up @@ -135,6 +136,52 @@ func Parse(verStr string) (Version, error) {
return version, nil
}

// ParseTt parses a tt version string with format '<major>.<minor>.<patch>.<hash>'
// and return the version value it represents.
func ParseTt(verStr string) (Version, error) {
verToParse := strings.Trim(verStr, "\n")
sepIndex := strings.LastIndex(verToParse, ".")
if sepIndex == -1 {
return Version{}, fmt.Errorf("failed to parse version %q: format is not valid", verStr)
}

verStr = verToParse[:sepIndex]
numVersions := strings.Split(verStr, ".")
if len(numVersions) != 3 {
return Version{}, fmt.Errorf("the version of %q does not match"+
" <major>.<minor>.<patch> format", verStr)
}

var err error
ttVersion := Version{}

ttVersion.Major, err = util.AtoiUint64(numVersions[0])
if err != nil {
return Version{}, err
}
ttVersion.Minor, err = util.AtoiUint64(numVersions[1])
if err != nil {
return Version{}, err
}
ttVersion.Patch, err = util.AtoiUint64(numVersions[2])
if err != nil {
return Version{}, err
}

hashStr := verToParse[sepIndex+1:]
isHashValid, err := util.IsValidCommitHash(hashStr)
if err != nil {
return Version{}, err
}
if !isHashValid {
return Version{}, fmt.Errorf("hash %q has a wrong format", hashStr)
}
ttVersion.Hash = hashStr
ttVersion.Str = verToParse

return ttVersion, nil
}

// VersionSlice attaches the methods of sort.Interface to []Version, sorting from oldest to newest.
type VersionSlice []Version

Expand Down
68 changes: 68 additions & 0 deletions cli/version/version_tools_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,3 +217,71 @@ func TestParseVersion(t *testing.T) {
}
}
}

func TestParseTt(t *testing.T) {
type testCase struct {
name string
inputVer string
expectedVer Version
isErr bool
expectedErrMsg string
}

cases := []testCase{
{
name: "basic",
inputVer: "2.3.1.f7cc1de\n",
expectedVer: Version{
Major: 2,
Minor: 3,
Patch: 1,
Hash: "f7cc1de",
Str: "2.3.1.f7cc1de",
},
isErr: false,
expectedErrMsg: "",
},
{
name: "parse error",
inputVer: "2.w.1.f7cc1de",
expectedVer: Version{},
isErr: true,
expectedErrMsg: `strconv.ParseUint: parsing "w": invalid syntax`,
},
{
name: "no dots in version",
inputVer: "2131f7cc1de",
expectedVer: Version{},
isErr: true,
expectedErrMsg: fmt.Sprintf(`failed to parse version "2131f7cc1de":` +
` format is not valid`),
},
{
name: "version does not match",
inputVer: "2.1.3.1.f7cc1de",
expectedVer: Version{},
isErr: true,
expectedErrMsg: fmt.Sprintf(`the version of "2.1.3.1" does not match` +
` <major>.<minor>.<patch> format`),
},
{
name: "hash does not match",
inputVer: "2.1.3.f7cc1de_",
expectedVer: Version{},
isErr: true,
expectedErrMsg: `hash "f7cc1de_" has a wrong format`,
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
resVer, err := ParseTt(tc.inputVer)
if tc.isErr {
assert.EqualError(t, err, tc.expectedErrMsg)
} else {
assert.NoError(t, err)
}
assert.Equal(t, resVer, tc.expectedVer)
})
}
}
37 changes: 37 additions & 0 deletions test/integration/install/test_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -700,3 +700,40 @@ def test_install_tarantool_fetch_latest_version(
is True else None)
assert instance_process_rc == 0
assert expected_install_msg in output


@pytest.mark.slow
@pytest.mark.parametrize("active_symlink, expected_install_msg, is_interactive", [
("tt_master", "Found newest commit of tt in master", True),
("tt_master", "tt_master version of tt already exists, updating symlink...", False),
("tt_v1.2.3", "Found newest commit of tt in master", True),
])
def test_install_tt_fetch_latest_version(active_symlink,
expected_install_msg,
is_interactive,
tt_cmd,
tmp_path):
config_path = os.path.join(tmp_path, config_name)
# Create test config
with open(config_path, 'w') as f:
f.write(f"env:\n bin_dir: {tmp_path}\n inc_dir:\n")

# Copying built executable to bin directory,
# renaming it to master and change symlink
# to emulate that this is master version.
shutil.copy(tt_cmd, os.path.join(tmp_path, "tt_master"))
shutil.copy(tt_cmd, os.path.join(tmp_path, "tt_v1.2.3"))
os.symlink(os.path.join(tmp_path, active_symlink),
os.path.join(tmp_path, "tt"))

install_cmd = [tt_cmd, "--cfg", config_path]
if not is_interactive:
install_cmd.append("--no-prompt")
install_cmd.extend(["install", "tt", "master"])
instance_process_rc, install_output = run_command_and_get_output(
install_cmd,
cwd=tmp_path,
input="y\n" if is_interactive
is True else None)
assert instance_process_rc == 0
assert expected_install_msg in install_output

0 comments on commit c8f2df1

Please sign in to comment.