unittest/shell_history_t.cc (1,195 lines of code) (raw):
/*
* Copyright (c) 2017, 2024, Oracle and/or its affiliates.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License, version 2.0,
* as published by the Free Software Foundation.
*
* This program is designed to work with certain software (including
* but not limited to OpenSSL) that is licensed under separate terms,
* as designated in a particular file or component or in included license
* documentation. The authors of MySQL hereby grant you an additional
* permission to link the program and your derivative works with the
* separately licensed software that they have either included with
* the program or referenced in the documentation.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
* the GNU General Public License, version 2.0, for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "unittest/gprod_clean.h"
#include "unittest/gtest_clean.h"
#include "unittest/test_utils/mocks/gmock_clean.h"
#ifndef _WIN32
#include <sys/stat.h>
#endif
#include "ext/linenoise-ng/include/linenoise.h"
#include "modules/mod_shell.h"
#include "modules/mod_shell_options.h"
#include "mysqlshdk/libs/utils/utils_file.h"
#include "mysqlshdk/libs/utils/utils_general.h"
#include "mysqlshdk/libs/utils/utils_string.h"
#include "src/mysqlsh/cmdline_shell.h"
#include "unittest/test_utils.h"
namespace mysqlsh {
class Shell_history : public ::testing::Test {
protected:
#ifdef HAVE_JS
const std::string to_scripting = "\\js";
#else
const std::string to_scripting = "\\py";
#endif
public:
Shell_history()
: _options_file(
Shell_core_test_wrapper::get_options_file_name("history_test")),
m_handler(&m_capture, print_capture, print_capture, print_capture) {}
void SetUp() override {
remove(_options_file.c_str());
linenoiseHistoryFree();
}
protected:
void enable_capture() { current_console()->add_print_handler(&m_handler); }
void disable_capture() {
current_console()->remove_print_handler(&m_handler);
}
std::string _options_file;
std::string m_capture;
shcore::Interpreter_print_handler m_handler;
private:
static bool print_capture(void *cdata, const char *text) {
std::string *m_capture = static_cast<std::string *>(cdata);
m_capture->append(text).append("\n");
return true;
}
};
TEST_F(Shell_history, check_history_sql_not_connected) {
// Start in SQL mode, do not connect to MySQL. This will ensure no statement
// gets executed. However, we promise that all user input will be recorded
// in the history - no matther what all user input shall be recorded.
char *args[] = {const_cast<char *>("ut"), const_cast<char *>("--sql"),
nullptr};
mysqlsh::Command_line_shell shell(
std::make_shared<Shell_options>(2, args, _options_file));
EXPECT_EQ(0, linenoiseHistorySize());
shell.process_line("select 1;");
EXPECT_EQ(1, linenoiseHistorySize());
EXPECT_STREQ("select 1;", linenoiseHistoryLine(0));
SKIP_TEST("Bug in history handling of empty commands");
// Bug
shell.process_line("select 2;;");
EXPECT_EQ(2, linenoiseHistorySize());
EXPECT_STREQ("select 1;", linenoiseHistoryLine(0));
EXPECT_STREQ("select 2;", linenoiseHistoryLine(1));
}
TEST_F(Shell_history, check_password_history_linenoise) {
// TS_HM#6 in SQL mode, commands that match the glob patterns IDENTIFIED,
// PASSWORD or any pattern specified in the
// shell.options["history.sql.ignorePattern"] option will be skipped from
// being added to the history.
// TS_CV#5 shell.options["history.sql.ignorePattern"]=string skip string from
// history.
const auto &server_uri = shell_test_server_uri();
char *args[] = {const_cast<char *>("ut"),
const_cast<char *>(server_uri.c_str()), nullptr};
mysqlsh::Command_line_shell shell(
std::make_shared<Shell_options>(2, args, _options_file));
shell._history.set_limit(100);
auto coptions = shcore::get_connection_options("root@localhost");
coptions.set_scheme("mysql");
coptions.set_password("");
coptions.set_port(atoi(getenv("MYSQL_PORT")));
shell.connect(coptions, false);
EXPECT_EQ("*IDENTIFIED*:*PASSWORD*",
shell.get_options()->get("history.sql.ignorePattern").descr());
EXPECT_EQ(0, linenoiseHistorySize());
// \sql command should be filtered according to SQL mode rules
EXPECT_EQ(0, linenoiseHistorySize());
shell.process_line("\\sql select 1;");
EXPECT_EQ(1, linenoiseHistorySize());
EXPECT_STREQ("\\sql select 1;", linenoiseHistoryLine(0));
// ensure certain words don't appear in history for more than 1 iteration
shell.process_line("\\sql set password = 'secret' then fail;");
EXPECT_EQ(2, linenoiseHistorySize());
EXPECT_STREQ("\\sql select 1;", linenoiseHistoryLine(0));
// 1st time added it should be there, but not after another one is added
EXPECT_STREQ("\\sql set password = 'secret' then fail;",
linenoiseHistoryLine(1));
shell.process_line("\\sql create user foo@bar identified by 'secret';");
EXPECT_EQ(2, linenoiseHistorySize());
EXPECT_STREQ("\\sql select 1;", linenoiseHistoryLine(0));
EXPECT_STREQ("\\sql create user foo@bar identified by 'secret';",
linenoiseHistoryLine(1));
shell.process_line("\\sql alter user foo@bar set password='secret';");
EXPECT_EQ(2, linenoiseHistorySize());
EXPECT_STREQ("\\sql select 1;", linenoiseHistoryLine(0));
EXPECT_STREQ("\\sql alter user foo@bar set password='secret';",
linenoiseHistoryLine(1));
shell.process_line("\\sql secret;");
EXPECT_EQ(2, linenoiseHistorySize());
EXPECT_STREQ("\\sql select 1;", linenoiseHistoryLine(0));
EXPECT_STREQ("\\sql secret;", linenoiseHistoryLine(1));
shell.process_line("\\sql drop user foo@bar;");
// TS_CV#9
shell.process_line("\\sql");
EXPECT_EQ(0, linenoiseHistorySize());
shell.process_line("select 1;");
EXPECT_EQ(1, linenoiseHistorySize());
EXPECT_STREQ("select 1;", linenoiseHistoryLine(0));
// ensure certain words don't appear in history for more than 1 iteration
shell.process_line("set password = 'secret' then fail;");
EXPECT_EQ(2, linenoiseHistorySize());
EXPECT_STREQ("select 1;", linenoiseHistoryLine(0));
// 1st time added it should be there, but not after another one is added
EXPECT_STREQ("set password = 'secret' then fail;", linenoiseHistoryLine(1));
shell.process_line("create user foo@bar identified by 'secret';");
EXPECT_EQ(2, linenoiseHistorySize());
EXPECT_STREQ("select 1;", linenoiseHistoryLine(0));
EXPECT_STREQ("create user foo@bar identified by 'secret';",
linenoiseHistoryLine(1));
shell.process_line("alter user foo@bar set password='secret';");
EXPECT_EQ(2, linenoiseHistorySize());
EXPECT_STREQ("select 1;", linenoiseHistoryLine(0));
EXPECT_STREQ("alter user foo@bar set password='secret';",
linenoiseHistoryLine(1));
shell.process_line("secret;");
EXPECT_EQ(2, linenoiseHistorySize());
EXPECT_STREQ("select 1;", linenoiseHistoryLine(0));
EXPECT_STREQ("secret;", linenoiseHistoryLine(1));
// set filter directly
shell.set_sql_safe_for_logging("*SECret*");
shell.process_line("top secret;");
EXPECT_EQ(3, linenoiseHistorySize());
EXPECT_STREQ("select 1;", linenoiseHistoryLine(0));
EXPECT_STREQ("secret;", linenoiseHistoryLine(1));
EXPECT_STREQ("top secret;", linenoiseHistoryLine(2));
shell.process_line("top;");
EXPECT_EQ(3, linenoiseHistorySize());
EXPECT_STREQ("top;", linenoiseHistoryLine(2));
#ifdef HAVE_JS
std::string print_stmt = "println('secret');";
#else
std::string print_stmt = "print('secret')";
#endif
// SQL filter only applies to SQL mode
shell.process_line(to_scripting);
shell.process_line(print_stmt);
EXPECT_EQ(1, linenoiseHistorySize());
EXPECT_STREQ(print_stmt.c_str(), linenoiseHistoryLine(0));
#ifdef HAVE_JS
shell.process_line("\\py");
shell.process_line("print('secret')");
EXPECT_EQ(1, linenoiseHistorySize());
EXPECT_STREQ("print('secret')", linenoiseHistoryLine(0));
#endif
// unset filter via shell options
mysqlsh::Options opts(shell.get_options());
opts.set_member("history.sql.ignorePattern", shcore::Value(""));
shell.process_line("top secret;");
EXPECT_EQ(2, linenoiseHistorySize());
EXPECT_STREQ("top secret;", linenoiseHistoryLine(1));
// TS_CV#6
// shell.options["history.sql.ignorePattern"]=string:string:string:string with
// multiple strings separated by a colon (:) works correctly
// TS_CV#7
// TS_CV#8
shell.process_line(to_scripting);
shell.process_line(
"shell.options['history.sql.ignorePattern'] = '*bla*:*ble*';");
EXPECT_STREQ("shell.options['history.sql.ignorePattern'] = '*bla*:*ble*';",
linenoiseHistoryLine(0));
shell.process_line("\\sql");
shell.process_line("select 'bga';");
shell.process_line("select 'bge';");
shell.process_line("select 'aablaaa';");
shell.process_line("select 'aaBLEaa';");
shell.process_line("select 'bla';");
shell.process_line("select '*bla*';");
shell.process_line("select 'bgi';");
EXPECT_STREQ("select 'bga';", linenoiseHistoryLine(0));
EXPECT_STREQ("select 'bge';", linenoiseHistoryLine(1));
EXPECT_STREQ("select 'bgi';", linenoiseHistoryLine(2));
shell.process_line(to_scripting);
shell.process_line("shell.options['history.sql.ignorePattern'] = 'set;';");
shell.process_line("\\sql");
shell.process_line("\\history clear");
EXPECT_EQ(1, linenoiseHistorySize());
shell.process_line("bar;");
EXPECT_EQ(2, linenoiseHistorySize());
EXPECT_STREQ("bar;", linenoiseHistoryLine(1));
shell.process_line("set;");
EXPECT_EQ(3, linenoiseHistorySize());
EXPECT_STREQ("bar;", linenoiseHistoryLine(1));
EXPECT_STREQ("set;", linenoiseHistoryLine(2));
shell.process_line("xset;");
EXPECT_EQ(3, linenoiseHistorySize());
EXPECT_STREQ("xset;", linenoiseHistoryLine(2));
shell.process_line(to_scripting);
shell.process_line("shell.options['history.sql.ignorePattern'] = '*';");
shell.process_line("\\sql");
shell.process_line("\\history clear");
EXPECT_EQ(1, linenoiseHistorySize());
shell.process_line("a;");
EXPECT_EQ(1, linenoiseHistorySize());
shell.process_line("hello world;");
EXPECT_EQ(1, linenoiseHistorySize());
shell.process_line("*;");
EXPECT_EQ(1, linenoiseHistorySize());
shell.process_line(";");
EXPECT_EQ(1, linenoiseHistorySize());
shell.process_line(to_scripting);
shell.process_line("shell.options['history.sql.ignorePattern'] = '**';");
shell.process_line("\\sql");
shell.process_line("\\history clear");
EXPECT_EQ(1, linenoiseHistorySize());
shell.process_line("a;");
EXPECT_EQ(1, linenoiseHistorySize());
shell.process_line("hello world;");
EXPECT_EQ(1, linenoiseHistorySize());
shell.process_line("*;");
EXPECT_EQ(1, linenoiseHistorySize());
shell.process_line(";");
EXPECT_EQ(1, linenoiseHistorySize());
shell.process_line(to_scripting);
shell.process_line("shell.options['history.sql.ignorePattern'] = '?';");
shell.process_line("\\sql");
shell.process_line("\\history clear");
EXPECT_EQ(1, linenoiseHistorySize());
shell.process_line(";");
EXPECT_EQ(2, linenoiseHistorySize());
shell.process_line("a;");
EXPECT_EQ(2, linenoiseHistorySize());
shell.process_line("aa;");
EXPECT_EQ(3, linenoiseHistorySize());
shell.process_line(to_scripting);
shell.process_line("shell.options['history.sql.ignorePattern'] = '?*';");
shell.process_line("\\sql");
shell.process_line("\\history clear");
EXPECT_EQ(1, linenoiseHistorySize());
shell.process_line("?;");
EXPECT_EQ(1, linenoiseHistorySize());
shell.process_line("a;");
EXPECT_EQ(1, linenoiseHistorySize());
shell.process_line("aa;");
EXPECT_EQ(1, linenoiseHistorySize());
shell.process_line(to_scripting);
shell.process_line("shell.options['history.sql.ignorePattern'] = 'a?b?c*';");
shell.process_line("\\sql");
shell.process_line("\\history clear");
EXPECT_EQ(1, linenoiseHistorySize());
shell.process_line("abc;");
EXPECT_EQ(2, linenoiseHistorySize());
shell.process_line("abxc;");
EXPECT_EQ(3, linenoiseHistorySize());
shell.process_line("xaxbxc;");
EXPECT_EQ(4, linenoiseHistorySize());
shell.process_line("axbxcx;");
EXPECT_EQ(5, linenoiseHistorySize());
shell.process_line("axbxc;");
EXPECT_EQ(5, linenoiseHistorySize());
shell.process_line("axbxcdddeefg;");
EXPECT_EQ(5, linenoiseHistorySize());
shell.process_line("a b c;");
EXPECT_EQ(5, linenoiseHistorySize());
}
TEST_F(Shell_history, history_ignore_wildcard_questionmark) {
// WL#10446 TS_CV#8 - check wildcards, * is covered elsewhere, ? is covered
// here
linenoiseHistoryFree();
// NOTE: resizing it may cause troubles with \history dump indexing
// because we do not call the approriate API
linenoiseHistorySetMaxLen(100);
char *args[] = {const_cast<char *>("ut"), const_cast<char *>("--sql"),
nullptr};
mysqlsh::Command_line_shell shell(
std::make_shared<Shell_options>(2, args, _options_file));
// ? = match exactly one
EXPECT_EQ(0, linenoiseHistorySize());
shell.process_line(to_scripting);
shell.process_line(
"shell.options['history.sql.ignorePattern'] = '?ELECT 1;'");
shell.process_line("\\sql");
EXPECT_EQ(0, linenoiseHistorySize());
shell.process_line("SELECT 1;");
EXPECT_EQ(1, linenoiseHistorySize());
shell.process_line("ELECT 1;");
EXPECT_EQ(1, linenoiseHistorySize());
shell.process_line(to_scripting);
shell.process_line(
"shell.options['history.sql.ignorePattern'] = '?? ??;:?\?'");
shell.process_line("\\sql");
EXPECT_EQ(0, linenoiseHistorySize());
shell.process_line("AA BB;");
EXPECT_EQ(1, linenoiseHistorySize());
shell.process_line("A;");
EXPECT_EQ(1, linenoiseHistorySize());
shell.process_line(" A\n ;");
EXPECT_EQ(1, linenoiseHistorySize());
}
TEST_F(Shell_history, history_set_option) {
// All user input shall be caputered in the history
//
// cmdline_shell.cc catches asssorted "events" that are generated upon user
// input To test the requirement we need to try to trick base_shell.cc and
// find events that are not properly handled or even missing ones. Most other
// tests check the event SN_STATEMENT_EXECUTED.
linenoiseHistoryFree();
mysqlsh::Command_line_shell shell(
std::make_shared<Shell_options>(0, nullptr, _options_file));
// SN_SHELL_OPTION_CHANGED
shell.process_line(
"shell.options['history.sql.ignorePattern'] = '*SELECT 1*';");
EXPECT_EQ(1, linenoiseHistorySize());
EXPECT_STREQ("shell.options['history.sql.ignorePattern'] = '*SELECT 1*';",
linenoiseHistoryLine(0));
// And now another "plain assignment" from a user perspective
shell.process_line("a = 1;");
EXPECT_EQ(2, linenoiseHistorySize());
EXPECT_STREQ("a = 1;", linenoiseHistoryLine(1));
}
TEST_F(Shell_history, history_ignore_pattern_js) {
mysqlsh::Command_line_shell shell(
std::make_shared<Shell_options>(0, nullptr, _options_file));
shell.process_line("shell.options['history.sql.ignorePattern'] = '*SELECT*'");
shell.process_line("// SELECT");
EXPECT_STREQ("// SELECT", linenoiseHistoryLine(1));
}
TEST_F(Shell_history, history_ignore_pattern_py) {
char *args[] = {const_cast<char *>("ut"), const_cast<char *>("--py"),
nullptr};
mysqlsh::Command_line_shell shell(
std::make_shared<Shell_options>(2, args, _options_file));
shell.process_line(
"setattr(shell.options, 'history.sql.ignorePattern', "
"'WHAT A PROPERTY');");
EXPECT_EQ(1, linenoiseHistorySize());
EXPECT_STREQ(
"setattr(shell.options, 'history.sql.ignorePattern', 'WHAT A PROPERTY');",
linenoiseHistoryLine(0));
// Second issue - comments not recorded in history
shell.process_line("# WHAT A PROPERTY NAME");
EXPECT_EQ(2, linenoiseHistorySize());
EXPECT_STREQ("# WHAT A PROPERTY NAME", linenoiseHistoryLine(1));
}
TEST_F(Shell_history, history_linenoise) {
// Test cases covered here:
// TS_CLE#1 Commands executed by the user in the shell are saved to the
// command history
// TS_HM#1 only commands interactively typed by the user in the shell prompt
// are saved to history file: ~/.mysqlsh/history file
mysqlsh::Command_line_shell shell(
std::make_shared<Shell_options>(0, nullptr, _options_file));
const std::string hist_file = shell.history_file();
shcore::delete_file(hist_file);
{
EXPECT_NO_THROW(shell.load_state());
EXPECT_EQ(0, linenoiseHistorySize());
shell.process_line("print(1);");
EXPECT_EQ(1, linenoiseHistorySize());
shell.process_line("print(2);");
EXPECT_EQ(2, linenoiseHistorySize());
shell.process_line("print(3);");
EXPECT_EQ(3, linenoiseHistorySize());
EXPECT_STREQ("print(1);", linenoiseHistoryLine(0));
EXPECT_STREQ("print(2);", linenoiseHistoryLine(1));
EXPECT_STREQ("print(3);", linenoiseHistoryLine(2));
mysqlsh::Options opt(shell.get_options());
// TS_CV#10
// autosave off by default
EXPECT_FALSE(opt.get_member("history.autoSave").as_bool());
// check history autosave
shell.save_state();
EXPECT_FALSE(shcore::is_file(hist_file));
opt.set_member("history.autoSave", shcore::Value::True());
shell.save_state();
EXPECT_TRUE(shcore::is_file(hist_file));
// TS_CV#1 shell.options["history.maxSize"]=number sets the max number of
// entries to store in the shell history file
// TS_CV#2 (is not a requirement)
opt.set_member("history.maxSize", shcore::Value(1));
EXPECT_EQ(1, shell._history.size());
EXPECT_STREQ("print(3);", linenoiseHistoryLine(0));
// TS_CV#3 shell.options["history.maxSize"]=number, if the number of history
// entries exceeds the configured maximum, the oldest entries will be
// removed.
shell.process_line("print(4);");
EXPECT_EQ(1, shell._history.size());
EXPECT_STREQ("print(4);", linenoiseHistoryLine(0));
enable_capture();
m_capture.clear();
shell.process_line("\\help history");
// TS_SC#1
m_capture.clear();
shell.process_line("\\history");
EXPECT_EQ(" 5 \\help history\n\n", m_capture);
m_capture.clear();
// TS_SC#3
shell.process_line("\\history del 6");
EXPECT_EQ("", m_capture);
m_capture.clear();
shell.process_line("\\history");
EXPECT_EQ(" 1 \\history del 6\n\n", m_capture);
m_capture.clear();
shell.process_line("print(5);");
m_capture.clear();
shell.process_line("\\history 1");
EXPECT_EQ("Invalid options for \\history. See \\help history for syntax.\n",
m_capture);
m_capture.clear();
shell.process_line("\\history 1 x");
EXPECT_EQ("Invalid options for \\history. See \\help history for syntax.\n",
m_capture);
m_capture.clear();
shell.process_line("\\history x 1");
EXPECT_EQ("Invalid options for \\history. See \\help history for syntax.\n",
m_capture);
m_capture.clear();
shell.process_line("\\history clear 1");
EXPECT_EQ("\\history clear does not take any parameters\n", m_capture);
m_capture.clear();
shell.process_line("\\history del");
EXPECT_EQ("\\history delete requires entry number to be deleted\n",
m_capture);
m_capture.clear();
shell.process_line("\\history del 50");
EXPECT_EQ("Invalid history entry: 50 - valid range is 8-8\n", m_capture);
m_capture.clear();
shell.process_line("\\history 0 -1 -1");
EXPECT_EQ("Invalid options for \\history. See \\help history for syntax.\n",
m_capture);
// TS_SC#4 - cancelled
m_capture.clear();
// TS_SC#5
shell.process_line("\\history clear");
EXPECT_EQ("", m_capture);
m_capture.clear();
shell.process_line("\\history");
EXPECT_EQ(" 1 \\history clear\n\n", m_capture);
EXPECT_EQ(1, shell._history.size());
shell.load_state();
EXPECT_EQ(1, shell._history.size());
EXPECT_STREQ("print(3);", linenoiseHistoryLine(0));
// check no history
// TS_CV#4 shell.options["history.maxSize"]=0 , no history will be saved and
// old history entries will also be deleted when the shell exits.
// Note: linenoise history includes 1 history entry for the "current" line
// being edited. The current line will be cleared from history as soon as
// Enter is pressed. However we can't emulate keypress that from here, so
// the "current" line will be stuck in the history until the next command.
// This makes this test for something different from what we want, but
// there's no better way
m_capture.clear();
shell.process_line("shell.options['history.maxSize']=0;\n");
EXPECT_EQ(0, shell._history.size());
m_capture.clear();
shell.process_line("\\history\n");
EXPECT_EQ("", m_capture);
EXPECT_STREQ(NULL, linenoiseHistoryLine(1));
m_capture.clear();
// TS_SC#2
shell.process_line("\\history save\n");
EXPECT_EQ("Command history file saved with 0 entries.\n\n", m_capture);
std::string hdata;
shcore::load_text_file(hist_file, hdata, false);
EXPECT_EQ("", hdata);
}
shcore::delete_file(hist_file);
}
TEST_F(Shell_history, check_help_shows_history) {
// We should have one test that checks the output of \h for compleness
// TODO(ulf) (Remove my [Ulf] note when checked): I don't mind if we have one
// central UT for \h or every command we introduce has it's own tests.
// My preference is a global one but I'm not the one to maintain the UTs so
// I'll just do something and let the owners of the UTs decide.
mysqlsh::Command_line_shell shell(
std::make_shared<Shell_options>(0, nullptr, _options_file));
{
// Expecting the Shell mention \history in \help
enable_capture();
m_capture.clear();
shell.process_line("\\help");
EXPECT_TRUE(
strstr(m_capture.c_str(),
"\\history View and edit command line history."));
}
}
TEST_F(Shell_history, history_autosave_int) {
mysqlsh::Command_line_shell shell(
std::make_shared<Shell_options>(0, nullptr, _options_file));
shcore::delete_file(shell.history_file());
{
mysqlsh::Options opt(shell.get_options());
// Expecting the Shell to cast to boolean true if value is 1 or 0
opt.set_member("history.autoSave", shcore::Value(1));
EXPECT_TRUE(opt.get_member("history.autoSave").as_bool());
// Expecting the Shell to print history.autoSave = true not 101
enable_capture();
m_capture.clear();
shell.process_line("print(shell.options)");
// we perform automatic type conversion on set
EXPECT_TRUE(strstr(m_capture.c_str(), "\"history.autoSave\": true"));
}
}
#ifdef HAVE_JS
TEST_F(Shell_history, check_history_source_js) {
// WL#10446 says \source shall no add entries to the history
// Only history entry shall the \source itself
const auto &server_uri = shell_test_server_uri();
char *args[] = {const_cast<char *>("ut"), const_cast<char *>("--js"),
const_cast<char *>(server_uri.c_str()), nullptr};
mysqlsh::Command_line_shell shell(
std::make_shared<Shell_options>(3, args, _options_file));
shell._history.set_limit(10);
std::ofstream of;
of.open("test_source.js");
of << "print(1);\n";
of << "print(2);\n";
of.close();
EXPECT_NO_THROW(shell.load_state());
EXPECT_EQ(0, linenoiseHistorySize());
{
enable_capture();
shell.process_line("\\source test_source.js");
EXPECT_EQ(1, linenoiseHistorySize());
EXPECT_EQ("1\n2\n", m_capture);
}
shcore::delete_file("test_source.js");
}
TEST_F(Shell_history, check_history_source_js_nonl_interactive) {
// WL#10446 says \source shall no add entries to the history
// Only history entry shall the \source itself
// BUG#30765725 SHELL PYTHON HISTORY INCLUDING LAST LINE OF SCRIPTS
// If script doesn't have newline at the end of the file, then last statement
// is not executed.
const auto &server_uri = shell_test_server_uri();
char *args[] = {const_cast<char *>("ut"), const_cast<char *>("--js"),
const_cast<char *>("--interactive=full"),
const_cast<char *>(server_uri.c_str()), nullptr};
mysqlsh::Command_line_shell shell(
std::make_shared<Shell_options>(4, args, _options_file));
shell._history.set_limit(10);
std::ofstream of;
of.open("test_source_nonl.js");
of << "print(`line1\nline2\nline3`)";
of.close();
EXPECT_NO_THROW(shell.load_state());
EXPECT_EQ(0, linenoiseHistorySize());
{
enable_capture();
shell.process_line("\\source test_source_nonl.js");
EXPECT_EQ(1, linenoiseHistorySize());
EXPECT_EQ(std::string{"\\source test_source_nonl.js"},
std::string(linenoiseHistoryLine(0)));
EXPECT_EQ(R"*(mysql-js>
print(`line1
->
line2
->
line3`)
->
line1
line2
line3
)*",
m_capture);
}
shcore::delete_file("test_source_nonl.js");
}
#endif
TEST_F(Shell_history, check_history_source_py) {
// WL#10446 says \source shall no add entries to the history
// Only history entry shall the \source itself
const auto &server_uri = shell_test_server_uri();
char *args[] = {const_cast<char *>("ut"), const_cast<char *>("--py"),
const_cast<char *>(server_uri.c_str()), nullptr};
mysqlsh::Command_line_shell shell(
std::make_shared<Shell_options>(3, args, _options_file));
shell._history.set_limit(10);
std::ofstream of;
of.open("test_source.py");
of << "print(1)\n";
of << "print(2)\n";
of.close();
EXPECT_NO_THROW(shell.load_state());
EXPECT_EQ(0, linenoiseHistorySize());
{
enable_capture();
shell.process_line("\\source test_source.py");
EXPECT_EQ(1, linenoiseHistorySize());
EXPECT_EQ("1\n\n\n2\n\n\n", m_capture);
}
shcore::delete_file("test_source.py");
}
TEST_F(Shell_history, check_history_source_py_nonl_interactive) {
// WL#10446 says \source shall no add entries to the history
// Only history entry shall the \source itself
// BUG#30765725 SHELL PYTHON HISTORY INCLUDING LAST LINE OF SCRIPTS
// If script doesn't have newline at the end of the file, then last statement
// is not executed.
const auto &server_uri = shell_test_server_uri();
char *args[] = {const_cast<char *>("ut"), const_cast<char *>("--py"),
const_cast<char *>("--interactive=full"),
const_cast<char *>(server_uri.c_str()), nullptr};
mysqlsh::Command_line_shell shell(
std::make_shared<Shell_options>(4, args, _options_file));
shell._history.set_limit(10);
std::ofstream of;
of.open("test_source_nonl.py");
of << "print('''line1\nline2\nline3''')";
of.close();
EXPECT_NO_THROW(shell.load_state());
EXPECT_EQ(0, linenoiseHistorySize());
{
enable_capture();
shell.process_line("\\source test_source_nonl.py");
EXPECT_EQ(1, linenoiseHistorySize());
EXPECT_EQ(std::string{"\\source test_source_nonl.py"},
std::string(linenoiseHistoryLine(0)));
EXPECT_EQ(R"*(mysql-py>
print('''line1
->
line2
->
line3''')
line1
line2
line3
mysql-py>
)*",
m_capture);
}
shcore::delete_file("test_source_nonl.py");
}
TEST_F(Shell_history, check_history_source_py_nonl_continuedstate_interactive) {
// WL#10446 says \source shall no add entries to the history
// Only history entry shall the \source itself
// BUG#30765725 SHELL PYTHON HISTORY INCLUDING LAST LINE OF SCRIPTS
// If script doesn't have newline at the end of the file, then last statement
// is not executed.
const auto &server_uri = shell_test_server_uri();
char *args[] = {const_cast<char *>("ut"), const_cast<char *>("--py"),
const_cast<char *>("--interactive=full"),
const_cast<char *>(server_uri.c_str()), nullptr};
mysqlsh::Command_line_shell shell(
std::make_shared<Shell_options>(4, args, _options_file));
shell._history.set_limit(10);
std::ofstream of;
of.open("test_source_nonl.py");
// of << "print('''line1\nline2\nline3";
of << "l = ['line1', 'line2', 'line3']\nfor s in l:\n print(s";
of.close();
EXPECT_NO_THROW(shell.load_state());
EXPECT_EQ(0, linenoiseHistorySize());
{
enable_capture();
shell.process_line("\\source test_source_nonl.py");
EXPECT_EQ(shell.input_state(), shcore::Input_state::Ok);
EXPECT_EQ(1, linenoiseHistorySize());
EXPECT_EQ(std::string{"\\source test_source_nonl.py"},
std::string(linenoiseHistoryLine(0)));
EXPECT_THAT(m_capture, ::testing::HasSubstr("SyntaxError"));
}
shcore::delete_file("test_source_nonl.py");
}
TEST_F(Shell_history, check_history_overflow_del) {
// See if the history numbering still works for users when the history
// overflows, entries are dropped and renumbering might take place.
{
mysqlsh::Command_line_shell shell(
std::make_shared<Shell_options>(0, nullptr, _options_file));
shell._history.set_limit(3);
EXPECT_NO_THROW(shell.load_state());
EXPECT_EQ(0, shell._history.size());
shell.process_line("// 1");
// Actual history is now:
// 1 // 1
shell.process_line("// 2");
// Actual history is now:
// 1 // 1
// 2 // 2
shell.process_line("// 3");
// Actual history is now:
// 1 // 1
// 2 // 2
// 3 // 3
EXPECT_EQ(3, shell._history.size());
enable_capture();
// Note: history entries are added AFTER they are executed
// that means if \history is called, it will print the stack up the
// last command before \history itself
// Now we print the entries and offsets to be used with history del
// Did we just pop index 0 off the stack by pushing \\history?
// A: Yes, but the pop only happens after the history is printed
shell.process_line("\\history");
EXPECT_EQ(" 1 // 1\n\n 2 // 2\n\n 3 // 3\n\n", m_capture);
EXPECT_EQ(3, shell._history.size());
// Actual history is now:
// 2 // 2
// 3 // 3
// 4 \history
// Delete entry 2 // 2
shell.process_line("\\history del 2");
// Actual history is now:
// 3 // 3
// 4 \history
// 5 \history del 2
m_capture.clear();
shell.process_line("\\history");
// Actual history is now:
// 4 \history
// 5 \history del 2
// 6 \history
EXPECT_EQ(" 3 // 3\n\n 4 \\history\n\n 5 \\history del 2\n\n",
m_capture);
EXPECT_EQ(3, shell._history.size());
}
}
TEST_F(Shell_history, history_management) {
mysqlsh::Command_line_shell shell(
std::make_shared<Shell_options>(0, nullptr, _options_file));
enable_capture();
shell.process_line("\\js\n");
shell.process_line("\\history clear\n");
shell.process_line("shell.options['history.maxSize']=10;\n");
shcore::create_file("test_file.js", "println('test1');\nprintln('test2');\n");
std::string histfile = shell.history_file();
shcore::delete_file(histfile);
// TS_HM#2 History File does not add commands that are executed indirectly or
// internally to the history (example: \source command is executed)
shell.process_line("println('hello')\n");
shell.process_line("\\source test_file.js\n");
shell.process_line("\\history save\n");
std::string hist;
shcore::load_text_file(histfile, hist, false);
EXPECT_TRUE(hist.find("'hello'") != std::string::npos);
EXPECT_FALSE(hist.find("'test1'") != std::string::npos);
EXPECT_FALSE(hist.find("'test2'") != std::string::npos);
// TS_HM#3 History File is protected from Other USERS
#ifndef _WIN32
struct stat st;
EXPECT_EQ(0, stat(histfile.c_str(), &st));
EXPECT_EQ(S_IRUSR | S_IWUSR, st.st_mode & S_IRWXU);
EXPECT_EQ(0, st.st_mode & S_IRWXG);
EXPECT_EQ(0, st.st_mode & S_IRWXO);
shcore::delete_file(histfile);
mode_t omode = umask(0);
shell.process_line("\\history save\n");
EXPECT_EQ(0, stat(histfile.c_str(), &st));
EXPECT_EQ(S_IRUSR | S_IWUSR, st.st_mode & S_IRWXU);
EXPECT_EQ(0, st.st_mode & S_IRWXG);
EXPECT_EQ(0, st.st_mode & S_IRWXO);
shcore::delete_file(histfile);
umask(0022);
shell.process_line("\\history save\n");
EXPECT_EQ(0, stat(histfile.c_str(), &st));
EXPECT_EQ(S_IRUSR | S_IWUSR, st.st_mode & S_IRWXU);
EXPECT_EQ(0, st.st_mode & S_IRWXG);
EXPECT_EQ(0, st.st_mode & S_IRWXO);
shcore::delete_file(histfile);
umask(omode);
#endif
// TS_HM#4 History File, If the command has multiple lines, the newlines are
// stripped
shell.process_line("if (123)\nbar=456;\n");
shell.process_line("\\history save\n");
shcore::load_text_file(histfile, hist, false);
EXPECT_TRUE(hist.find("if (123) bar=456") != std::string::npos);
shell.process_line("\\history clear\n");
// TS_HM#5 History File If the command is the same as the previous one to the
// one executed previously (case sensitive), it wont be duplicated.
shell.process_line("println('bar');");
shell.process_line("println('foo');");
shell.process_line("println('foo');");
m_capture.clear();
shell.process_line("\\history");
EXPECT_EQ(
" 1 \\history clear\n\n"
" 2 println('bar');\n\n"
" 3 println('foo');\n\n",
m_capture);
m_capture.clear();
// ensure ordering after an ignored duplicate item continues sequential
shell.process_line("\\history");
EXPECT_EQ(
" 1 \\history clear\n\n"
" 2 println('bar');\n\n"
" 3 println('foo');\n\n"
" 4 \\history\n\n",
m_capture);
// TS_HM#7 if the history file can't be read or written to, it will log an
// error message and skip the read/write operation.
shcore::delete_file(histfile);
shcore::ensure_dir_exists(histfile.c_str());
m_capture.clear();
shell.process_line("\\history save");
auto base = "Could not save command history to";
#ifdef _WIN32
auto specific = histfile + ": Permission denied";
#else
auto specific = histfile + ": Is a directory";
#endif
auto base_found = m_capture.find(base) != std::string::npos;
auto specific_found = m_capture.find(specific) != std::string::npos;
if (!base_found || !specific_found) {
SCOPED_TRACE(specific);
SCOPED_TRACE(base);
SCOPED_TRACE("Expected:");
SCOPED_TRACE(m_capture);
SCOPED_TRACE("Actual:");
FAIL();
}
#ifndef _WIN32
EXPECT_EQ(0, chmod(histfile.c_str(), 0));
m_capture.clear();
EXPECT_NO_THROW(shell.load_state());
base = "Could not load command history from";
specific = histfile + ": Permission denied";
base_found = m_capture.find(base) != std::string::npos;
specific_found = m_capture.find(specific) != std::string::npos;
if (!base_found || !specific_found) {
SCOPED_TRACE(specific);
SCOPED_TRACE(base);
SCOPED_TRACE("Expected:");
SCOPED_TRACE(m_capture);
SCOPED_TRACE("Actual:");
FAIL();
}
#endif
shcore::remove_directory(histfile, false);
}
TEST_F(Shell_history, history_sizes) {
// We use a secondary history list to provide entry numbers that do not
// change when the list contents changes so that \history del works.
// This test shall cover internal list management (grow, shrink, overflow).
// No crash is good enough.
// See also src/mysqlsh/history.cc|h
mysqlsh::Command_line_shell shell(
std::make_shared<Shell_options>(0, nullptr, _options_file));
enable_capture();
shell.process_line("shell.options['history.maxSize'] = 4;");
shell.process_line("print(1);");
shell.process_line("print(2);");
shell.process_line("print(3);");
shell.process_line("print(4);");
m_capture.clear();
shell.process_line("\\history");
EXPECT_EQ(4, shell._history.size());
// NOTE: Always use shell._history.size() to check history size, otherwise
// you will get different results when test cases are ran on UTs vs manually
EXPECT_EQ(
" 2 print(1);\n\n"
" 3 print(2);\n\n"
" 4 print(3);\n\n"
" 5 print(4);\n\n",
m_capture);
shell.process_line("shell.options['history.maxSize'] = 3;");
shell.process_line("\\history");
shell.process_line("print(5);");
shell.process_line("print(6);");
EXPECT_EQ(3, shell._history.size());
shell.process_line("shell.options['history.maxSize'] = 0;");
shell.process_line("print(7);");
shell.process_line("\\history");
shell.process_line("print(77);");
EXPECT_EQ(0, shell._history.size());
shell.process_line("shell.options['history.maxSize'] = 1;");
shell.process_line("print(8);");
shell.process_line("\\history");
shell.process_line("print(88);");
EXPECT_EQ(1, shell._history.size());
shell.process_line("shell.options['history.maxSize'] = 2;");
shell.process_line("print(9);");
shell.process_line("\\history");
shell.process_line("print(99);");
EXPECT_EQ(2, shell._history.size());
shell.process_line("shell.options['history.maxSize'] = 0;");
shell.process_line("\\history");
EXPECT_EQ(0, shell._history.size());
// no crash is good enough so far
// attempt deleting entry that is not supposed to exist
shell.process_line("shell.options['history.maxSize'] = 3;");
shell.process_line("\\history clear");
shell.process_line("print(42);");
shell.process_line("\\history del 3");
// ensure numbering continues normally
shell.process_line("print(42);");
m_capture.clear();
shell.process_line("\\history");
EXPECT_EQ(
" 2 print(42);\n\n"
" 3 \\history del 3\n\n"
" 4 print(42);\n\n",
m_capture);
}
TEST_F(Shell_history, history_del_invisible_entry) {
// See also TEST_F(Shell_history, history_sizes)
mysqlsh::Command_line_shell shell(
std::make_shared<Shell_options>(0, nullptr, _options_file));
enable_capture();
// Trivial and should be covered elsewhere already
shell.process_line("\\history del 10-1");
EXPECT_TRUE(strstr(m_capture.c_str(), "Invalid"));
// History has now one command and it will get the index 1
// Can we access the invisible implementation dependent index 2?
m_capture.clear();
shell.process_line("\\history del 2");
EXPECT_TRUE(strstr(m_capture.c_str(), "Invalid"));
}
TEST_F(Shell_history, history_source_history) {
// Generate a history, save it, load and execute saved history
// using \source. \source shall not add any executed commands to history
// but preserve the history state from after save.
mysqlsh::Command_line_shell shell(
std::make_shared<Shell_options>(0, nullptr, _options_file));
enable_capture();
shell.process_line("session");
shell.process_line("dba");
shell.process_line("\\history save");
std::string histfile = shell.history_file();
std::string line = "\\source " + histfile;
shell.process_line(line);
shell.process_line("\\history");
EXPECT_EQ(5, shell._history.size());
EXPECT_EQ(
"Command history file saved with 2 entries.\n\n"
" 1 session\n\n"
" 2 dba\n\n"
" 3 \\history save\n\n"
" 4 " +
line + "\n\n",
m_capture);
shcore::delete_file(histfile);
}
TEST_F(Shell_history, history_del_range) {
mysqlsh::Command_line_shell shell(
std::make_shared<Shell_options>(0, nullptr, _options_file));
enable_capture();
shell.process_line("session");
shell.process_line("dba");
shell.process_line("mysql");
shell.process_line("mysqlx");
shell.process_line("shell");
shell.process_line("util");
EXPECT_EQ(6, shell._history.size());
// valid range
shell.process_line("\\history del 1-3");
shell.process_line("\\history");
EXPECT_EQ(5, shell._history.size());
EXPECT_EQ(
" 4 mysqlx\n\n"
" 5 shell\n\n"
" 6 util\n\n"
" 7 \\history del 1-3\n\n",
m_capture);
// invalid range: using former entries
m_capture.clear();
shell.process_line("\\history del 1-3");
EXPECT_EQ("Invalid history range: 1-3 - valid range is 4-8\n", m_capture);
// lower bound bigger than upper bound
m_capture.clear();
shell.process_line("\\history del 7-4");
EXPECT_EQ("Invalid history range 7-4. Last item must be greater than first\n",
m_capture);
shell.process_line("\\history clear");
shell.process_line("session");
shell.process_line("dba");
m_capture.clear();
shell.process_line("\\history del 1 - 3");
// Not sure if we want to give an error here or be gentle and accept space
EXPECT_EQ("\\history delete requires entry number to be deleted\n",
m_capture);
}
TEST_F(Shell_history, history_entry_number_reset) {
// Numbering shall only be reset when Shell is restarted
// or when \\history clear is called
mysqlsh::Command_line_shell shell(
std::make_shared<Shell_options>(0, nullptr, _options_file));
enable_capture();
shell.process_line("session");
shell.process_line("dba");
shell.process_line("util");
shell.process_line("\\history");
EXPECT_EQ(4, shell._history.size());
EXPECT_EQ(
" 1 session\n\n"
" 2 dba\n\n"
" 3 util\n\n",
m_capture);
m_capture.clear();
shell.process_line("\\history clear");
shell.process_line("\\history");
// This should get number 4, if numbering was not reset
EXPECT_EQ(" 1 \\history clear\n\n", m_capture);
EXPECT_EQ(2, shell._history.size());
}
TEST_F(Shell_history, history_delete_range) {
#define LOAD_HISTORY(data) \
shcore::create_file("testhistory", shcore::str_join(data, "\n")); \
shell._history.load("testhistory");
#define CHECK_DELRANGE(range, expected_init) \
{ \
std::vector<std::string> expected(expected_init); \
std::vector<std::string> dump; \
SCOPED_TRACE(range); \
shell.process_line("\\history del " range); \
shell._history.dump([&dump](const std::string &s) { dump.push_back(s); }); \
for (std::vector<std::string>::size_type i = 0; i < expected.size(); \
++i) { \
EXPECT_TRUE(i < dump.size()); \
EXPECT_EQ(expected[i], shcore::str_strip(dump[i])); \
} \
EXPECT_EQ(expected.size(), dump.size()); \
}
mysqlsh::Command_line_shell shell(
std::make_shared<Shell_options>(0, nullptr, _options_file));
enable_capture();
using strv = std::vector<std::string>;
shell._history.set_limit(10);
// range of 1
m_capture.clear();
LOAD_HISTORY(strv({"one", "two", "three", "four"}));
CHECK_DELRANGE(
"1-1", strv({"2 two", "3 three", "4 four", "5 \\history del 1-1"}));
EXPECT_TRUE(m_capture.empty());
m_capture.clear();
LOAD_HISTORY(strv({"one", "two", "three", "four"}));
CHECK_DELRANGE(
"2-2", strv({"1 one", "3 three", "4 four", "5 \\history del 2-2"}));
EXPECT_TRUE(m_capture.empty());
LOAD_HISTORY(strv({"one", "two", "three", "four"}));
CHECK_DELRANGE(
"4-4", strv({"1 one", "2 two", "3 three", "4 \\history del 4-4"}));
EXPECT_TRUE(m_capture.empty());
LOAD_HISTORY(strv({"one", "two", "three", "four"}));
CHECK_DELRANGE("5-5", strv({"1 one", "2 two", "3 three", "4 four",
"5 \\history del 5-5"}));
EXPECT_EQ("Invalid history range: 5-5 - valid range is 1-4\n", m_capture);
// range of 2
m_capture.clear();
LOAD_HISTORY(strv({"one", "two", "three", "four"}));
CHECK_DELRANGE("1-2", strv({"3 three", "4 four", "5 \\history del 1-2"}));
EXPECT_TRUE(m_capture.empty());
// (continuing) start outside, end inside
m_capture.clear();
CHECK_DELRANGE("1-3", strv({"3 three", "4 four", "5 \\history del 1-2",
"6 \\history del 1-3"}));
EXPECT_EQ("Invalid history range: 1-3 - valid range is 3-5\n", m_capture);
m_capture.clear();
LOAD_HISTORY(strv({"one", "two", "three", "four"}));
CHECK_DELRANGE("2-3", strv({"1 one", "4 four", "5 \\history del 2-3"}));
EXPECT_TRUE(m_capture.empty());
// shell._history.dump([](const std::string &s) { std::cout << s << "\n"; });
// inverted range
m_capture.clear();
LOAD_HISTORY(strv({"one", "two", "three", "four"}));
CHECK_DELRANGE("2-1", strv({"1 one", "2 two", "3 three", "4 four",
"5 \\history del 2-1"}));
EXPECT_EQ("Invalid history range 2-1. Last item must be greater than first\n",
m_capture);
// start inside, end outside
m_capture.clear();
LOAD_HISTORY(strv({"one", "two", "three", "four"}));
CHECK_DELRANGE("3-5", strv({"1 one", "2 two", "3 \\history del 3-5"}));
// start outside, end outside
m_capture.clear();
LOAD_HISTORY(strv({"one", "two", "three", "four"}));
CHECK_DELRANGE(
"1-1", strv({"2 two", "3 three", "4 four", "5 \\history del 1-1"}));
CHECK_DELRANGE("1-5", strv({"2 two", "3 three", "4 four",
"5 \\history del 1-1", "6 \\history del 1-5"}));
EXPECT_EQ("Invalid history range: 1-5 - valid range is 2-5\n", m_capture);
// outside to end
m_capture.clear();
LOAD_HISTORY(strv({"one", "two", "three", "four"}));
CHECK_DELRANGE(
"1-1", strv({"2 two", "3 three", "4 four", "5 \\history del 1-1"}));
CHECK_DELRANGE("1-", strv({"2 two", "3 three", "4 four",
"5 \\history del 1-1", "6 \\history del 1-"}));
EXPECT_EQ("Invalid history range: 1- - valid range is 2-5\n", m_capture);
m_capture.clear();
LOAD_HISTORY(strv({"one", "two", "three", "four"}));
CHECK_DELRANGE("5-", strv({"1 one", "2 two", "3 three", "4 four",
"5 \\history del 5-"}));
EXPECT_EQ("Invalid history range: 5- - valid range is 1-4\n", m_capture);
// middle to end
m_capture.clear();
LOAD_HISTORY(strv({"one", "two", "three", "four"}));
CHECK_DELRANGE("3-", strv({"1 one", "2 two", "3 \\history del 3-"}));
EXPECT_TRUE(m_capture.empty());
// last to end
m_capture.clear();
LOAD_HISTORY(strv({"one", "two", "three", "four"}));
CHECK_DELRANGE("4-",
strv({"1 one", "2 two", "3 three", "4 \\history del 4-"}));
EXPECT_TRUE(m_capture.empty());
m_capture.clear();
LOAD_HISTORY(strv({"one", "two", "three", "four"}));
CHECK_DELRANGE("5-", strv({"1 one", "2 two", "3 three", "4 four",
"5 \\history del 5-"}));
EXPECT_EQ("Invalid history range: 5- - valid range is 1-4\n", m_capture);
m_capture.clear();
LOAD_HISTORY(strv({"one", "two", "three"}));
CHECK_DELRANGE("-2", strv({"1 one", "2 \\history del -2"}));
EXPECT_TRUE(m_capture.empty());
m_capture.clear();
LOAD_HISTORY(strv({"one", "two", "three"}));
CHECK_DELRANGE("-5", strv({"1 \\history del -5"}));
EXPECT_TRUE(m_capture.empty());
m_capture.clear();
LOAD_HISTORY(strv({"one", "two", "three", "four"}));
CHECK_DELRANGE("3",
strv({"1 one", "2 two", "4 four", "5 \\history del 3"}));
CHECK_DELRANGE("-3", strv({"1 one", "2 two", "3 \\history del -3"}));
EXPECT_TRUE(m_capture.empty());
// invalid
m_capture.clear();
LOAD_HISTORY(strv({"one", "two", "three", "four"}));
CHECK_DELRANGE("-1-2", strv({"1 one", "2 two", "3 three", "4 four",
"5 \\history del -1-2"}));
EXPECT_EQ(
"\\history delete range argument needs to be in format first-last\n",
m_capture);
// small history size
m_capture.clear();
LOAD_HISTORY(strv({"one", "two", "three"}));
shell._history.set_limit(3);
CHECK_DELRANGE("3", strv({"1 one", "2 two", "3 \\history del 3"}));
EXPECT_TRUE(m_capture.empty());
m_capture.clear();
LOAD_HISTORY(strv({"one", "two", "three"}));
shell._history.set_limit(3);
CHECK_DELRANGE("1", strv({"2 two", "3 three", "4 \\history del 1"}));
EXPECT_EQ("", m_capture);
m_capture.clear();
LOAD_HISTORY(strv({"one", "two"}));
shell._history.set_limit(2);
CHECK_DELRANGE("2", strv({"1 one", "2 \\history del 2"}));
EXPECT_TRUE(m_capture.empty());
m_capture.clear();
LOAD_HISTORY(strv({"one", "two"}));
shell._history.set_limit(2);
CHECK_DELRANGE("1", strv({"2 two", "3 \\history del 1"}));
EXPECT_EQ("", m_capture);
m_capture.clear();
LOAD_HISTORY(strv({"one"}));
shell._history.set_limit(1);
CHECK_DELRANGE("1", strv({"1 \\history del 1"}));
EXPECT_TRUE(m_capture.empty());
m_capture.clear();
shell._history.clear();
shell._history.set_limit(0);
CHECK_DELRANGE("1", strv({}));
EXPECT_EQ("The history is already empty\n", m_capture);
m_capture.clear();
CHECK_DELRANGE("0", strv({}));
EXPECT_EQ("The history is already empty\n", m_capture);
// load and shrink
m_capture.clear();
LOAD_HISTORY(strv({"one", "two", "three"}));
shell._history.set_limit(1);
CHECK_DELRANGE("0", strv({"2 \\history del 0"}));
EXPECT_EQ("Invalid history entry: 0 - valid range is 1-1\n", m_capture);
#undef CHECK_DELRANGE
shcore::delete_file("testhistory");
}
TEST_F(Shell_history, history_numbering) {
mysqlsh::Command_line_shell shell(
std::make_shared<Shell_options>(0, nullptr, _options_file));
enable_capture();
using strv = std::vector<std::string>;
shell._history.set_limit(10);
#define CHECK_NUMBERING_ADD(line, expected) \
{ \
strv dump; \
SCOPED_TRACE(line); \
shell.process_line(line); \
EXPECT_TRUE(m_capture.empty()); \
shell._history.dump([&dump](const std::string &s) { dump.push_back(s); }); \
for (strv::size_type i = 0; i < dump.size(); ++i) { \
EXPECT_TRUE(i < expected.size()); \
EXPECT_EQ(expected[i], shcore::str_strip(dump[i])); \
} \
EXPECT_EQ(expected.size(), dump.size()); \
}
// Test sequential numbering of history
CHECK_NUMBERING_ADD("session", (strv{"1 session"}));
CHECK_NUMBERING_ADD("dba", (strv{"1 session", "2 dba"}));
CHECK_NUMBERING_ADD("util", (strv{"1 session", "2 dba", "3 util"}));
// Must reset to 0 on clear
shell._history.clear();
CHECK_NUMBERING_ADD("mysqlx", (strv{"1 mysqlx"}));
// Must reset on load
shcore::create_file("testhistory", "mysql\nmysqlx\n");
shell._history.load("testhistory");
CHECK_NUMBERING_ADD("shell", (strv{"1 mysql", "2 mysqlx", "3 shell"}));
// Adding a duplicate item ignores the duplicate
shell._history.clear();
CHECK_NUMBERING_ADD("session", (strv{"1 session"}));
CHECK_NUMBERING_ADD("dba", (strv{"1 session", "2 dba"}));
CHECK_NUMBERING_ADD("dba", (strv{"1 session", "2 dba"}));
CHECK_NUMBERING_ADD("util", (strv{"1 session", "2 dba", "3 util"}));
CHECK_NUMBERING_ADD("dba",
(strv{"1 session", "2 dba", "3 util", "4 dba"}));
// Continued sequential numbering after filling up history
shell._history.clear();
shell._history.set_limit(3);
CHECK_NUMBERING_ADD("session", (strv{"1 session"}));
CHECK_NUMBERING_ADD("dba", (strv{"1 session", "2 dba"}));
CHECK_NUMBERING_ADD("util", (strv{"1 session", "2 dba", "3 util"}));
CHECK_NUMBERING_ADD("mysql", (strv{"2 dba", "3 util", "4 mysql"}));
CHECK_NUMBERING_ADD("mysqlx", (strv{"3 util", "4 mysql", "5 mysqlx"}));
// Delete 1st id
shell._history.clear();
shell._history.set_limit(10);
CHECK_NUMBERING_ADD("session", (strv{"1 session"}));
CHECK_NUMBERING_ADD("dba", (strv{"1 session", "2 dba"}));
CHECK_NUMBERING_ADD("\\history del 1",
(strv{"2 dba", "3 \\history del 1"}));
// Deleting an id that was already deleted is a no-op
shell._history.clear();
shell._history.set_limit(10);
CHECK_NUMBERING_ADD("session", (strv{"1 session"}));
CHECK_NUMBERING_ADD("dba", (strv{"1 session", "2 dba"}));
CHECK_NUMBERING_ADD("util", (strv{"1 session", "2 dba", "3 util"}));
CHECK_NUMBERING_ADD("mysql",
(strv{"1 session", "2 dba", "3 util", "4 mysql"}));
CHECK_NUMBERING_ADD(
"\\history del 3",
(strv{"1 session", "2 dba", "4 mysql", "5 \\history del 3"}));
CHECK_NUMBERING_ADD("shell", (strv{"1 session", "2 dba", "4 mysql",
"5 \\history del 3", "6 shell"}));
shell.process_line("\\history del 3");
CHECK_NUMBERING_ADD(
"\\history del 3",
(strv{"1 session", "2 dba", "4 mysql", "5 \\history del 3",
"6 shell", "7 \\history del 3"}));
// Deleting the item at the limit
shell._history.clear();
shell._history.set_limit(4);
CHECK_NUMBERING_ADD("session", (strv{"1 session"}));
CHECK_NUMBERING_ADD("dba", (strv{"1 session", "2 dba"}));
CHECK_NUMBERING_ADD("util", (strv{"1 session", "2 dba", "3 util"}));
CHECK_NUMBERING_ADD("mysql",
(strv{"1 session", "2 dba", "3 util", "4 mysql"}));
CHECK_NUMBERING_ADD(
"\\history del 4",
(strv{"1 session", "2 dba", "3 util", "4 \\history del 4"}));
shell._history.clear();
shell._history.set_limit(4);
CHECK_NUMBERING_ADD("session", (strv{"1 session"}));
CHECK_NUMBERING_ADD("dba", (strv{"1 session", "2 dba"}));
CHECK_NUMBERING_ADD("util", (strv{"1 session", "2 dba", "3 util"}));
CHECK_NUMBERING_ADD("mysql",
(strv{"1 session", "2 dba", "3 util", "4 mysql"}));
m_capture.clear();
shell.process_line("\\history del 5");
EXPECT_EQ("Invalid history entry: 5 - valid range is 1-4\n", m_capture);
#undef CHECK_NUMBERING
shcore::delete_file("testhistory");
}
TEST_F(Shell_history, never_filter_latest) {
mysqlsh::Command_line_shell shell(
std::make_shared<Shell_options>(0, nullptr, _options_file));
shell._history.set_limit(4);
// Bug #28749037 COMMAND HISTORY SHOULD KEEP ALL HISTORY ENTRIES FOR AT
// LEAST ONE ENTRY
shell.process_line("\\sql");
shell._history.clear();
shell.process_line("select 1;");
shell.process_line("set password='secret';");
EXPECT_EQ(2, linenoiseHistorySize());
EXPECT_STREQ("select 1;", linenoiseHistoryLine(0));
EXPECT_STREQ("set password='secret';", linenoiseHistoryLine(1));
// this should clear the set password line
shell._history.save("testhistory");
std::string data = shcore::get_text_file("testhistory", false);
EXPECT_EQ("select 1;\n", data);
shcore::delete_file("testhistory");
EXPECT_EQ(1, linenoiseHistorySize());
EXPECT_STREQ("select 1;", linenoiseHistoryLine(0));
shell.process_line("set password='secret';");
// this should clear the set password line again
shell.process_line("select 2;");
EXPECT_EQ(2, linenoiseHistorySize());
EXPECT_STREQ("select 1;", linenoiseHistoryLine(0));
EXPECT_STREQ("select 2;", linenoiseHistoryLine(1));
}
TEST_F(Shell_history, history_split_by_mode) {
std::string sql_history_file;
std::string scripting_history_file;
{
char *args[] = {const_cast<char *>("ut"), const_cast<char *>("--sql"),
const_cast<char *>("--interactive"), nullptr};
mysqlsh::Command_line_shell shell(
std::make_shared<Shell_options>(2, args, _options_file));
sql_history_file = shell.history_file();
EXPECT_EQ(0, linenoiseHistorySize());
shell.get_options()->set("history.autoSave", shcore::Value::True());
shell.process_line("select 1;\n");
EXPECT_EQ(1, linenoiseHistorySize());
shell.process_line(to_scripting);
scripting_history_file = shell.history_file();
EXPECT_EQ(0, linenoiseHistorySize());
shell.process_line("\\status\n");
EXPECT_EQ(1, linenoiseHistorySize());
shell.process_line("\\sql");
EXPECT_EQ(2, linenoiseHistorySize());
EXPECT_STREQ(to_scripting.c_str(), linenoiseHistoryLine(1));
shell.process_line(to_scripting);
EXPECT_EQ(2, linenoiseHistorySize());
EXPECT_STREQ("\\sql", linenoiseHistoryLine(1));
}
shcore::delete_file(sql_history_file);
shcore::delete_file(scripting_history_file);
}
TEST_F(Shell_history, migrate_old_history) {
int history_size = 0;
char *args[] = {const_cast<char *>("ut"), const_cast<char *>("--sql"),
nullptr};
{
mysqlsh::Command_line_shell shell(
std::make_shared<Shell_options>(2, args, _options_file));
EXPECT_FALSE(shcore::is_file(shell.history_file()));
shell.process_line("select 1;\n");
history_size = linenoiseHistorySize();
shell.process_line("\\history save\n");
const auto hist_file = shell.history_file();
ASSERT_TRUE(shcore::is_file(hist_file));
shcore::rename_file(hist_file, hist_file.substr(0, hist_file.rfind('.')));
}
mysqlsh::Command_line_shell shell(
std::make_shared<Shell_options>(2, args, _options_file));
shell.load_state();
EXPECT_EQ(history_size, linenoiseHistorySize());
EXPECT_TRUE(
shcore::is_file(shell.history_file(shcore::IShell_core::Mode::SQL)));
shcore::delete_file(shell.history_file(shcore::IShell_core::Mode::SQL));
#ifdef HAVE_JS
shell.process_line("\\js\n");
EXPECT_EQ(history_size, linenoiseHistorySize());
EXPECT_TRUE(shcore::is_file(
shell.history_file(shcore::IShell_core::Mode::JavaScript)));
shcore::delete_file(
shell.history_file(shcore::IShell_core::Mode::JavaScript));
#endif
#ifdef HAVE_PYTHON
shell.process_line("\\py\n");
EXPECT_EQ(history_size, linenoiseHistorySize());
EXPECT_TRUE(
shcore::is_file(shell.history_file(shcore::IShell_core::Mode::Python)));
shcore::delete_file(shell.history_file(shcore::IShell_core::Mode::Python));
#endif
}
TEST_F(Shell_history, get_entry) {
mysqlsh::Command_line_shell shell(
std::make_shared<Shell_options>(0, nullptr, _options_file));
const auto &history = shell._history;
EXPECT_EQ("", history.get_entry(5));
shell.process_line("a = 1");
EXPECT_EQ("a = 1", history.get_entry(history.first_entry()));
EXPECT_EQ("a = 1", history.get_entry(history.last_entry()));
shell.process_line("b = 2");
EXPECT_EQ("a = 1", history.get_entry(history.first_entry()));
EXPECT_EQ("b = 2", history.get_entry(history.last_entry()));
shell.process_line("c = 3");
EXPECT_EQ("a = 1", history.get_entry(history.first_entry()));
EXPECT_EQ("c = 3", history.get_entry(history.last_entry()));
}
} // namespace mysqlsh