From a173cbe41083410a51411e871f7e604c7ebb4774 Mon Sep 17 00:00:00 2001 From: Dmitriy Gertsog Date: Mon, 23 Sep 2024 18:26:00 +0300 Subject: [PATCH] tt: add support set delimiter in console MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The “\set delimiter [marker]” command is handled by the `tt` utility, simulating the behavior of a similar command for the Tarantool console. Closes #727 --- CHANGELOG.md | 1 + cli/connect/commands.go | 23 +++++++- cli/connect/console.go | 3 +- cli/connect/const.go | 3 ++ cli/connect/input.go | 23 +++++++- cli/connect/input_test.go | 68 +++++++++++++++++++++++- test/integration/connect/test_connect.py | 67 +++++++++++++++++++++++ 7 files changed, 182 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f3f0d2ae3..ead52e26c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. cluster config (3.0) or cartridge orchestrator. ### Fixed +- Command `\set delimiter [marker]` works correctly and don't hangs `tt` console. - `tt log -f` crash on removing log directory. diff --git a/cli/connect/commands.go b/cli/connect/commands.go index a1d3be164..f248f9dce 100644 --- a/cli/connect/commands.go +++ b/cli/connect/commands.go @@ -160,7 +160,7 @@ func (command argSetCmdDecorator) Aliases() []string { // Run checks that there is one allowed argument and runs the command. func (command argSetCmdDecorator) Run(console *Console, cmd string, args []string) (string, error) { - if len(args) != 1 || !find(command.sorted, args[0]) { + if len(command.sorted) > 0 && (len(args) != 1 || !find(command.sorted, args[0])) { return "", fmt.Errorf("the command expects one of: %s", strings.Join(command.sorted, ", ")) } @@ -361,6 +361,19 @@ func setTableColumnWidthMaxFunc(console *Console, return "", nil } +// setDelimiterMarker apply delimiter to the Console object. +func setDelimiterMarker(console *Console, cmd string, args []string) (string, error) { + switch len(args) { + case 0: + console.delimiter = "" + case 1: + console.delimiter = args[0] + default: + return "", fmt.Errorf("the command expects zero or single argument") + } + return "", nil +} + // switchNextFormatFunc switches to a next output format. func switchNextFormatFunc(console *Console, cmd string, args []string) (string, error) { console.format = (1 + console.format) % formatter.FormatsAmount @@ -453,6 +466,14 @@ var cmdInfos = []cmdInfo{ ), ), }, + cmdInfo{ + Short: setDelimiter + " ", + Long: "set expression delimiter", + Cmd: newArgSetCmdDecorator( + newBaseCmd([]string{setDelimiter}, setDelimiterMarker), + []string{}, + ), + }, cmdInfo{ Short: setTableColumnWidthMaxShort + " ", Long: "set max column width for table/ttable", diff --git a/cli/connect/console.go b/cli/connect/console.go index b0b043d4b..7cd24b696 100644 --- a/cli/connect/console.go +++ b/cli/connect/console.go @@ -65,6 +65,7 @@ type Console struct { executor func(in string) completer func(in prompt.Document) []prompt.Suggest validators map[Language]ValidateCloser + delimiter string prompt *prompt.Prompt } @@ -197,7 +198,7 @@ func getExecutor(console *Console) func(string) { var completed bool validator := console.validators[console.language] - console.input, completed = AddStmtPart(console.input, in, validator) + console.input, completed = AddStmtPart(console.input, in, console.delimiter, validator) if !completed { console.livePrefixEnabled = true return diff --git a/cli/connect/const.go b/cli/connect/const.go index 7ba07dc89..2005d381a 100644 --- a/cli/connect/const.go +++ b/cli/connect/const.go @@ -19,6 +19,9 @@ const setTableDialect = "\\set table_format" // width for tables. const setTableColumnWidthMaxLong = "\\set table_column_width" +// setDelimiter set a custom expression delimiter for Tarantool console. +const setDelimiter = "\\set delimiter" + // setTableColumnWidthMaxShort is a short command to set a maximum columnt // width for tables. const setTableColumnWidthMaxShort = "\\xw" diff --git a/cli/connect/input.go b/cli/connect/input.go index eabfac3ac..f8355370e 100644 --- a/cli/connect/input.go +++ b/cli/connect/input.go @@ -3,6 +3,7 @@ package connect import ( "fmt" "strings" + "unicode" lua "github.com/yuin/gopher-lua" ) @@ -86,9 +87,25 @@ func (v SQLValidator) Close() error { return nil } +// cleanupDelimiter checks if the statement ends with the string `delim`. If yes, it removes it. +// Returns true if the delimiter has been removed. +func cleanupDelimiter(stmt string, delim string) (string, bool) { + if delim == "" { + return stmt, true + } + no_space := strings.TrimRightFunc(stmt, func(r rune) bool { + return unicode.IsSpace(r) + }) + no_delim := strings.TrimSuffix(no_space, delim) + if len(no_space) > len(no_delim) { + return no_delim, true + } + return stmt, false +} + // AddStmtPart adds a new part of the statement. It returns a result statement // and true if the statement is already completed. -func AddStmtPart(stmt, part string, validator Validator) (string, bool) { +func AddStmtPart(stmt, part, delim string, validator Validator) (string, bool) { if stmt == "" { trimmed := strings.TrimSpace(part) if trimmed != "" { @@ -98,5 +115,7 @@ func AddStmtPart(stmt, part string, validator Validator) (string, bool) { stmt += "\n" + part } - return stmt, validator.Validate(stmt) + var hasDelim bool + stmt, hasDelim = cleanupDelimiter(stmt, delim) + return stmt, hasDelim && validator.Validate(stmt) } diff --git a/cli/connect/input_test.go b/cli/connect/input_test.go index 4e2e8ed1c..462cf4da2 100644 --- a/cli/connect/input_test.go +++ b/cli/connect/input_test.go @@ -149,7 +149,7 @@ func TestAddStmtPart(t *testing.T) { } t.Run(name, func(t *testing.T) { validator.ret = c - result, completed := AddStmtPart(stmt, part, validator) + result, completed := AddStmtPart(stmt, part, "", validator) assert.Equal(t, expected, result) assert.Equal(t, c, completed) assert.Equal(t, expected, validator.in) @@ -178,7 +178,71 @@ func TestAddStmtPart_luaValidator(t *testing.T) { stmt := "" for _, part := range parts { var completed bool - stmt, completed = AddStmtPart(stmt, part.str, validator) + stmt, completed = AddStmtPart(stmt, part.str, "", validator) + + assert.Equal(t, part.expected, stmt) + assert.Equal(t, part.completed, completed) + } +} + +func TestAddStmtPart_luaValidator_Delimiter(t *testing.T) { + validator := NewLuaValidator() + defer validator.Close() + + parts := []struct { + str string + expected string + delim string + completed bool + }{ + { + " ", + "", + "", + true, + }, + { + "for i = 1,10 do ; ", + "for i = 1,10 do ", + ";", + false, + }, + { + " print(x)", + "for i = 1,10 do \n print(x)", + "", + false, + }, + { + " local j = 5
", + "for i = 1,10 do \n print(x)\n local j = 5", + "
", + false, + }, + { + "", + "for i = 1,10 do \n print(x)\n local j = 5\n", + ";", + false, + }, + { + " ", + "for i = 1,10 do \n print(x)\n local j = 5\n\n ", + "", + false, + }, + { + "end\t*** ", + "for i = 1,10 do \n print(x)\n local j = 5\n\n \nend\t", + "***", + true, + }, + } + + stmt := "" + for _, part := range parts { + var completed bool + stmt, completed = AddStmtPart(stmt, part.str, part.delim, validator) assert.Equal(t, part.expected, stmt) assert.Equal(t, part.completed, completed) diff --git a/test/integration/connect/test_connect.py b/test/integration/connect/test_connect.py index 05391cb7f..821fea15e 100644 --- a/test/integration/connect/test_connect.py +++ b/test/integration/connect/test_connect.py @@ -4,6 +4,7 @@ import shutil import subprocess import tempfile +from pathlib import Path import psutil import pytest @@ -233,6 +234,7 @@ def test_connect_and_get_commands_outputs(tt_cmd, tmpdir_with_cfg): \\set table_format -- set table format default, jira or markdown \\set graphics -- disables/enables pseudographics for table modes \\set table_column_width -- set max column width for table/ttable + \\set delimiter -- set expression delimiter \\xw -- set max column width for table/ttable \\x -- switches output format cyclically \\x[l,t,T,y] -- set output format lua, table, ttable or yaml @@ -253,6 +255,8 @@ def test_connect_and_get_commands_outputs(tt_cmd, tmpdir_with_cfg): commands["\\set graphics false"] = "" commands["\\set graphics true"] = "" commands["\\set table_column_width 1"] = "" + commands["\\set delimiter ;"] = "" + commands["\\set delimiter"] = "" commands["\\xw 1"] = "" commands["\\x"] = "" commands["\\xl"] = "" @@ -331,6 +335,7 @@ def test_connect_and_get_commands_errors(tt_cmd, tmpdir_with_cfg): commands["\\set graphics arg"] = "⨯ the command expects one boolean" commands["\\set table_column_width"] = "⨯ the command expects one unsigned number" commands["\\set table_column_width arg"] = "⨯ the command expects one unsigned number" + commands["\\set delimiter arg arg"] = "⨯ the command expects zero or single argument" commands["\\xw"] = "⨯ the command expects one unsigned number" commands["\\xw arg"] = "⨯ the command expects one unsigned number" commands["\\x arg"] = "⨯ the command does not expect arguments" @@ -2595,3 +2600,65 @@ def test_connect_to_cluster_app(tt_cmd): # Stop the Instance. stop_app(tt_cmd, tmpdir, app_name) shutil.rmtree(tmpdir) + + +@pytest.mark.parametrize( + "instance, opts, ready_file", + ( + ("test_app", None, Path(run_path, "test_app", control_socket)), + ( + "localhost:3013", + {"-u": "test", "-p": "password"}, + Path("ready"), + ), + ), +) +def test_set_delimiter( + tt_cmd, tmpdir_with_cfg, instance: str, opts: None | dict, ready_file: Path +): + input = """local a=1 +a = a + 1 +return a +""" + delimiter = "
" + tmpdir = Path(tmpdir_with_cfg) + + # The test application file. + test_app_path = Path(__file__).parent / "test_localhost_app" / "test_app.lua" + # Copy test data into temporary directory. + copy_data(tmpdir, [test_app_path]) + + # Start an instance. + start_app(tt_cmd, tmpdir, "test_app") + # Check for start. + file = wait_file(tmpdir / "test_app" / ready_file.parent, ready_file.name, []) + assert file != "" + + # Without delimiter should get an error. + ret, output = try_execute_on_instance( + tt_cmd, + tmpdir, + instance, + opts=opts, + stdin=input, + ) + assert ret + assert "attempt to perform arithmetic on global" in output + + # With delimiter expecting correct responses. + input = f"\\set delimiter {delimiter}\n{input}{delimiter}\n" + ret, output = try_execute_on_instance( + tt_cmd, tmpdir, instance, opts=opts, stdin=input + ) + assert ret + assert ( + output + == """--- +- 2 +... + +""" + ) + + # Stop the Instance. + stop_app(tt_cmd, tmpdir, "test_app")