unittest/shell_script_tester.cc (1,646 lines of code) (raw):
/*
* Copyright (c) 2015, 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/shell_script_tester.h"
#include <mysql_version.h>
#include <chrono>
#include <condition_variable>
#include <iostream>
#include <set>
#include <string>
#include <thread>
#include <utility>
#include "modules/adminapi/common/server_features.h"
#include "mysqlshdk/libs/textui/textui.h"
#include "mysqlshdk/libs/utils/strformat.h"
#include "mysqlshdk/shellcore/shell_console.h"
#include "shellcore/interrupt_handler.h"
#include "shellcore/ishell_core.h"
#include "src/mysqlsh/cmdline_shell.h"
#include "utils/process_launcher.h"
#include "utils/utils_file.h"
#include "utils/utils_general.h"
#include "utils/utils_lexing.h"
#include "utils/utils_path.h"
#include "utils/utils_string.h"
using namespace shcore;
extern "C" const char *g_test_home;
extern bool g_generate_validation_file;
extern int g_test_script_timeout;
extern std::set<int> g_break;
// Helper class to capture GTest partial results (the stuff generated by
// ADD_TEST_FAILURE() and so on)
class TestResultCatcher : public ::testing::EmptyTestEventListener {
public:
TestResultCatcher(std::stringstream *stream) : m_stream(stream) {}
void OnTestPartResult(const ::testing::TestPartResult &result) override {
// If the test part succeeded, we don't need to do anything.
if (result.type() == ::testing::TestPartResult::kSuccess) return;
*m_stream << "\033[35m\033[1m################################### vv BEGIN "
"FAILURE vv ###################################\033[0m\n";
*m_stream << result.message() << "\n";
*m_stream << "\033[35m\033[1m################################### ^^ END "
"FAILURE ^^ ###################################\033[0m\n";
}
private:
std::stringstream *m_stream;
};
//-----------------------------------------------------------------------------
static bool do_print(void * /* udata */, const char *s) {
printf("%s", s);
fflush(stdout);
return true;
}
static shcore::Prompt_result do_prompt(
void * /* udata */, const char *prompt,
const shcore::prompt::Prompt_options & /*options*/,
std::string *ret_input) {
printf("%s", prompt);
fflush(stdout);
std::cin >> *ret_input;
return shcore::Prompt_result::Ok;
}
namespace mysqlsh {
class Test_debugger {
public:
enum class Action { Skip_execute, Continue, Abort };
Test_debugger() : m_deleg(this, do_print, do_prompt, nullptr, nullptr) {
m_console.reset(new mysqlsh::Shell_console(&m_deleg));
}
void enable(bool step) {
println("Enabling...");
m_enabled = true;
m_stepping = step;
}
void on_test_begin(Shell_core_test_wrapper *test) { m_test = test; }
void on_reset_shell(std::shared_ptr<mysqlsh::Command_line_shell> shell) {
m_shell = shell;
}
Action will_execute(const std::string &source, int lnum,
const std::string &code) {
if (m_main_source.empty()) m_main_source = source;
// Line containing //BREAK in the script will enter cmd loop
if (m_enabled) {
if (m_stepping) {
if (source != m_main_source && m_skip_include) {
// skip over include files
return Action::Continue;
}
m_stepping = false;
return interact();
} else if (shcore::str_strip(code) == "//BREAK" ||
shcore::str_strip(code) == "#BREAK") {
println("//BREAK hit");
return interact();
} else if (source == m_main_source && g_break.count(lnum) > 0) {
println("Breaking at requested line " + std::to_string(lnum));
return interact();
}
}
return Action::Continue;
}
Action on_validate_fail(const std::string &chunk_id) {
if (m_exit_on_test_error) exit(1);
if (m_enabled) {
println("Validation '" + chunk_id + "' failed");
return interact(true);
} else if (g_test_fail_early) {
return Action::Abort;
}
println(
"\033[35m\033[1m#######################################################"
"##############################\033[0m");
return Action::Continue;
}
void did_execute(int /* lnum */, const std::string & /* code */) {}
Action did_execute_test_failure() {
if (m_exit_on_test_error) exit(1);
if (m_enabled) {
println("Test failures found");
return interact();
} else if (g_test_fail_early) {
return Action::Abort;
}
return Action::Continue;
}
void did_throw(int /* lnum */, const std::string & /* code */) {
if (m_enabled && m_break_on_throw) interact();
}
private:
void println(const std::string &s) {
puts(mysqlshdk::textui::bold("TDB: " + s).c_str());
}
bool debug() { return true; }
bool handle_command(const std::string &cmd, bool *exit_interactive) {
if (cmd == "\\next") {
m_stepping = true;
m_skip_include = true;
*exit_interactive = true;
return true;
} else if (cmd == "\\step") {
m_stepping = true;
m_skip_include = false;
*exit_interactive = true;
return true;
} else if (cmd == "\\dhelp") {
std::cout << "TDB Help\n"
<< "--------\n"
<< "\\next line (skip INCLUDE)\n"
<< "\\step line (enter INCLUDE)\n"
<< "\\cont continue execution\n"
<< "\\abort let test fail (when handling validation failures)\n"
<< "\\quit exit\n"
<< "\n";
return true;
}
return false;
}
Action interact(bool abortable = false) {
if (m_first_interact) {
if (abortable)
println(
"Entering interactive loop... \\cont to continue, ^D or \\abort to "
"exit. \\dhelp for debugger help");
else
println(
"Entering interactive loop... \\cont or ^D to continue. \\dhelp "
"for debugger help");
m_first_interact = false;
}
// save stdout and stderr contents
std::string std_err = m_test->output_handler.std_err;
std::string std_out = m_test->output_handler.std_out;
Action result = do_interact(abortable);
if (result == Action::Continue)
println("Continuing...");
else
println("Aborting...");
m_test->output_handler.std_err = std_err;
m_test->output_handler.std_out = std_out;
return result;
}
#define CTRL_C_STR "\003"
Action do_interact(bool abortable = false) {
Action result = abortable ? Action::Abort : Action::Continue;
bool interrupted = false;
bool exit_interactive = false;
std::string cmd;
while (!exit_interactive) {
char *tmp = mysqlsh::Command_line_shell::readline(
makebold(shell()->prompt()).c_str());
if (tmp && strcmp(tmp, CTRL_C_STR) != 0) {
cmd = tmp;
free(tmp);
} else {
if (tmp) {
if (strcmp(tmp, CTRL_C_STR) == 0) interrupted = true;
free(tmp);
}
if (interrupted) {
shell()->clear_input();
interrupted = false;
continue;
}
break;
}
if (cmd == "") {
// re-execute last command
cmd = m_last_cmd;
} else {
m_last_cmd = cmd;
}
if (cmd == "\\cont") {
result = Action::Continue;
break;
} else if (cmd == "\\abort" && abortable) {
result = Action::Abort;
break;
} else if (cmd == "\\quit") {
exit(1);
}
if (!handle_command(cmd, &exit_interactive)) shell()->process_line(cmd);
}
return result;
}
private:
bool m_enabled = false;
bool m_break_on_throw = false;
bool m_stepping = false;
bool m_skip_include = false;
bool m_exit_on_test_error = false;
bool m_first_interact = true;
Shell_core_test_wrapper *m_test = nullptr;
std::string m_last_cmd;
std::string m_main_source;
std::shared_ptr<mysqlsh::Shell_console> m_console;
shcore::Interpreter_delegate m_deleg;
std::weak_ptr<mysqlsh::Command_line_shell> m_shell;
std::shared_ptr<mysqlsh::Command_line_shell> shell() {
return m_shell.lock();
}
};
} // namespace mysqlsh
mysqlsh::Test_debugger *g_tdb = nullptr;
void init_tdb() {
if (!g_tdb) g_tdb = new mysqlsh::Test_debugger();
}
void enable_tdb(bool step) {
init_tdb();
g_tdb->enable(step);
}
void fini_tdb() {
delete g_tdb;
g_tdb = nullptr;
}
//-----------------------------------------------------------------------------
class Timeout {
public:
explicit Timeout(Shell_script_tester *owner) : _owner(owner) { start(); }
~Timeout() { stop(); }
bool did_timeout() const { return _timed_out; }
private:
Shell_script_tester *_owner;
std::thread _thread;
std::condition_variable _wait_cond;
std::mutex _cond_mutex;
bool _stop = false;
bool _timed_out = false;
void check() {
using namespace std::chrono_literals; // NOLINT:build/namespace
// wait for the timeout amount then abort the test
std::unique_lock<std::mutex> lock(_cond_mutex);
if (!_wait_cond.wait_for(lock, g_test_script_timeout * 1s,
[this]() { return _stop; })) {
_timed_out = true;
// timedout
puts("TIMEOUT!");
// there's no locking to protect this, but the test already timedout
// anyway
_owner->output_handler.flush_debug_log();
puts("Aborting test after timeout");
auto intr = shcore::current_interrupt(true);
if (intr) intr->interrupt();
}
}
void start() {
_stop = false;
_thread = std::thread([this]() { check(); });
}
void stop() {
{
std::unique_lock<std::mutex> lock(_cond_mutex);
if (_stop) return;
_stop = true;
}
_wait_cond.notify_all();
_thread.join();
}
};
//-----------------------------------------------------------------------------
Shell_script_tester::Shell_script_tester() {
init_tdb();
// Default home folder for scripts
_shell_scripts_home = shcore::path::join_path(g_test_home, "scripts");
_new_format = false;
}
void Shell_script_tester::SetUp() {
Crud_test_wrapper::SetUp();
if (_options->trace_protocol) {
// Redirect cout
_cout_backup = std::cout.rdbuf();
std::cout.rdbuf(_cout.rdbuf());
}
g_tdb->on_test_begin(this);
}
void Shell_script_tester::TearDown() {
if (!_skip_sandbox_check) {
// check for leftover sandboxes
for (int i = 0; i < tests::sandbox::k_num_ports; i++) {
EXPECT_FALSE(shcore::is_folder(
testutil->get_sandbox_path(_mysql_sandbox_ports[i])))
<< "Sandbox left behind port=" << _mysql_sandbox_ports[i];
}
}
Crud_test_wrapper::TearDown();
if (_options->trace_protocol) {
// Restore old cout.
std::cout.rdbuf(_cout_backup);
}
g_tdb->on_test_begin(this);
}
void Shell_script_tester::reset_shell() {
Crud_test_wrapper::reset_shell();
g_tdb->on_reset_shell(_interactive_shell);
}
void Shell_script_tester::set_config_folder(const std::string &name) {
// Custom home folder for scripts
_shell_scripts_home = shcore::path::join_path(g_test_home, "scripts", name);
// Currently hardcoded since scripts are on the shell repo
// but can easily be updated to be setup on an ENV VAR so
// the scripts can by dynamically imported from the dev-api docs
// with the sctract tool Jan is working on
_scripts_home = shcore::path::join_path(_shell_scripts_home, "scripts");
}
void Shell_script_tester::set_setup_script(const std::string &name) {
// if name is an absolute path, join_path will just return name
_setup_script = shcore::path::join_path(_shell_scripts_home, "setup", name);
}
size_t find_token(const std::string &source, const std::string &find,
const std::string &is_not, size_t start_pos) {
size_t ret_val = std::string::npos;
while (true) {
size_t found = source.find(find, start_pos);
size_t proto = source.find(is_not, start_pos);
if (found != std::string::npos && found == proto)
start_pos = proto + is_not.size();
else {
ret_val = found;
break;
}
}
return ret_val;
}
std::string Shell_script_tester::resolve_string(const std::string &source) {
std::string updated(source);
auto std_out_backup = std::move(output_handler.std_out);
size_t start = find_token(updated, "<<<", "<<<< RECEIVE", 0);
size_t end;
while (start != std::string::npos) {
bool strip_trailing_newline = false;
end = find_token(updated, ">>>", ">>>> SEND", start);
if (end == std::string::npos)
throw std::logic_error("Unterminated <<< in test");
//<<<"fooo"\>>>\n is stripped into fooo (without the trailing newline)
if (updated.compare(end - 1, 5, "\\>>>\n") == 0) {
strip_trailing_newline = true;
--end;
}
std::string token = updated.substr(start + 3, end - start - 3);
// This will make the variable to be printed on the stdout
output_handler.wipe_out();
std::string value;
// If the token was registered in C++ uses it
if (_output_tokens.count(token)) {
value = _output_tokens[token];
} else {
// If not, we use whatever is defined on the scripting language
execute_internal(token);
value = output_handler.internal_std_out;
}
// strip the trailing newline added by execute, but not all whitespace
if (str_endswith(value, "\n")) value = value.substr(0, value.size() - 1);
if (strip_trailing_newline) {
updated.replace(start, end - start + 5, value);
} else {
updated.replace(start, end - start + 3, value);
}
start = find_token(updated, "<<<", "<<<< RECEIVE", 0);
}
output_handler.std_out = std::move(std_out_backup);
return updated;
}
bool Shell_script_tester::validate_line_by_line(const std::string &context,
const std::string &chunk_id,
const std::string &stream,
const std::string &expected,
const std::string &actual,
int srcline, int valline) {
std::string new_expected(expected);
std::vector<std::string> expected_lines;
bool changed = false;
// Takes this as the posibility of the presense of conditional lines.
size_t end_pos = expected.find("?{}");
if (end_pos != std::string::npos) {
expected_lines = shcore::split_string(expected, "\n");
// Lines in the format of ?{<condition>} might be the start/end of a
// conditional Section of expectations, the section ends on a equal to ?{}
for (size_t index = 0; index < expected_lines.size(); index++) {
std::string line = expected_lines[index];
if (!line.empty() && line[0] == '?') {
auto size = line.size();
if (size > 2 && line[1] == '{' && line[2] != '*' &&
line[size - 1] == '}') {
std::string condition = line.substr(2, size - 3);
// Empty condition is simply ignored
if (!condition.empty()) {
expected_lines.erase(expected_lines.begin() + index);
// If condition not satisfied deletes all the lines in the middle
// until the closing tag is found
bool erase = !context_enabled(condition);
while (expected_lines.size() > index &&
expected_lines[index] != "?{}") {
if (erase)
expected_lines.erase(expected_lines.begin() + index);
else
index++;
}
expected_lines.erase(expected_lines.begin() + index);
changed = true;
// Goes back on the index so the new current line is analyzed as
// well
index--;
}
}
}
}
}
if (changed) new_expected = shcore::str_join(expected_lines, "\n");
return check_multiline_expect(context + "@" + chunk_id, stream, new_expected,
actual, srcline, valline);
}
bool Shell_script_tester::validate(const std::string &context,
const std::string &chunk_id, bool optional) {
std::string original_std_out = output_handler.std_out;
std::string original_std_err = output_handler.std_err;
size_t out_position = 0;
size_t err_position = 0;
std::string validation_id = _chunks[chunk_id].def->validation_id;
if (_chunk_validations.find(validation_id) != _chunk_validations.end()) {
bool expect_failures = false;
// Identifies the validations to be done based on the context
std::vector<std::shared_ptr<Validation>> validations;
for (const auto &val : _chunk_validations[validation_id]) {
bool enabled = false;
try {
enabled = context_enabled(val->def->context);
if (val->def->stream == "PROTOCOL" && !_options->trace_protocol) {
ADD_FAILURE_AT("validation file", val->def->linenum)
<< "ERROR TESTING PROTOCOL: Protocol tracing is disabled."
<< "\n"
<< "\tCHUNK: " << val->def->line << "\n";
}
} catch (const std::invalid_argument &e) {
ADD_FAILURE_AT("validation file", val->def->linenum)
<< "ERROR EVALUATING VALIDATION CONTEXT: " << e.what() << "\n"
<< "\tCHUNK: " << val->def->line << "\n";
}
if (enabled) {
validations.push_back(val);
if (!val->expected_error.empty()) expect_failures = true;
} else {
if (output_handler.internal_std_err.empty()) {
SKIP_VALIDATION(val->def->line);
} else {
SKIP_VALIDATION(val->def->line + ": " +
output_handler.internal_std_err);
}
}
}
std::string full_statement;
// The validations will be performed ONLY if the context is enabled
for (size_t valindex = 0; valindex < validations.size(); valindex++) {
// Validation goes against validation code
if (!validations[valindex]->code.empty()) {
// Before cleaning up, prints any error found on the script execution
if (valindex == 0 && !original_std_err.empty()) {
ADD_FAILURE_AT(_chunks[chunk_id].source.c_str(),
_chunks[chunk_id].code[0].first)
<< makered("\tUnexpected Error: " + original_std_err) << "\n";
output_handler.wipe_all();
return false;
}
output_handler.wipe_all();
_cout.str("");
_cout.clear();
std::string backup = _custom_context;
full_statement.append(validations[valindex]->code);
_custom_context += "[" + full_statement + "]";
execute(validations[valindex]->code);
_custom_context = backup;
if (_interactive_shell->input_state() == shcore::Input_state::Ok)
full_statement.clear();
else
full_statement.append("\n");
original_std_err = output_handler.std_err;
original_std_out = output_handler.std_out;
out_position = 0;
err_position = 0;
output_handler.wipe_all();
_cout.str("");
_cout.clear();
}
// Validates unexpected error
if (!expect_failures && !original_std_err.empty()) {
ADD_FAILURE_AT(_chunks[chunk_id].source.c_str(),
_chunks[chunk_id].code[0].first)
<< "while executing chunk: " + _chunks[chunk_id].def->line << "\n"
<< makered("\tUnexpected Error: ") << original_std_err << "\n";
output_handler.wipe_all();
return false;
}
// Validates expected output if any
if (!validations[valindex]->expected_output.empty()) {
std::string out = validations[valindex]->expected_output;
out = resolve_string(out);
if (out != "*") {
if (validations[valindex]->def->validation ==
ValidationType::Simple) {
auto matched =
multi_value_compare(out, original_std_out, false, out_position,
nullptr, &out_position);
if (!matched) {
if (out_position == 0) {
ADD_FAILURE_AT(_chunks[chunk_id].source.c_str(),
_chunks[chunk_id].code[0].first)
<< "while executing chunk: " + _chunks[chunk_id].def->line
<< "\nwith validation at "
<< validations[valindex]->def->linenum << "\n"
<< makeyellow("\tSTDOUT missing: ") << out << "\n"
<< makeyellow("\tSTDOUT actual: ") + original_std_out
<< "\n";
} else {
ADD_FAILURE_AT(_chunks[chunk_id].source.c_str(),
_chunks[chunk_id].code[0].first)
<< "while executing chunk: " + _chunks[chunk_id].def->line
<< "\nwith validation at "
<< validations[valindex]->def->linenum << "\n"
<< makeyellow("\tSTDOUT missing: ") << out << "\n"
<< makeyellow("\tSTDOUT actual: ") +
original_std_out.substr(out_position)
<< "\n"
<< makeyellow("\tSTDOUT original: ") + original_std_out
<< "\n";
}
output_handler.wipe_all();
return false;
}
} else {
SCOPED_TRACE(_chunks[chunk_id].source);
if (!validate_line_by_line(
context, chunk_id, "STDOUT", out,
validations[valindex]->def->stream == "PROTOCOL"
? _cout.str()
: original_std_out,
_chunks[chunk_id].code[0].first,
validations[valindex]->def->linenum)) {
output_handler.wipe_all();
return false;
}
}
}
}
// Validates unexpected output if any
if (!validations[valindex]->unexpected_output.empty()) {
std::string out = validations[valindex]->unexpected_output;
out = resolve_string(out);
bool matched = false;
if (validations[valindex]->def->stream == "PROTOCOL")
matched = _cout.str().find(out) != std::string::npos;
else
matched = multi_value_compare(out, original_std_out, false);
if (matched) {
ADD_FAILURE_AT(_chunks[chunk_id].source.c_str(),
_chunks[chunk_id].code[0].first)
<< "while executing chunk: " + _chunks[chunk_id].def->line << "\n"
<< "with validation at " << validations[valindex]->def->linenum
<< "\n"
<< makeyellow("\tSTDOUT unexpected: ") << out << "\n"
<< makeyellow("\tSTDOUT actual: ") << original_std_out << "\n";
output_handler.wipe_all();
return false;
}
}
// Validates expected error if any
if (!validations[valindex]->expected_error.empty()) {
std::string error = validations[valindex]->expected_error;
error = resolve_string(error);
if (error != "*") {
if (validations[valindex]->def->validation ==
ValidationType::Simple) {
bool matched =
multi_value_compare(error, original_std_err, false,
err_position, nullptr, &err_position);
if (!matched) {
if (err_position == 0) {
ADD_FAILURE_AT(_chunks[chunk_id].source.c_str(),
_chunks[chunk_id].code[0].first)
<< "while executing chunk: " + _chunks[chunk_id].def->line
<< "\n"
<< "with validation at "
<< validations[valindex]->def->linenum << "\n"
<< makeyellow("\tSTDERR missing: ") + error << "\n"
<< makeyellow("\tSTDERR actual: ") + original_std_err
<< "\n";
} else {
ADD_FAILURE_AT(_chunks[chunk_id].source.c_str(),
_chunks[chunk_id].code[0].first)
<< "while executing chunk: " + _chunks[chunk_id].def->line
<< "\n"
<< "with validation at "
<< validations[valindex]->def->linenum << "\n"
<< makeyellow("\tSTDERR missing: ") + error << "\n"
<< makeyellow("\tSTDERR actual: ") +
original_std_err.substr(err_position)
<< "\n"
<< makeyellow("\tSTDERR original: ") + original_std_err
<< "\n";
}
output_handler.wipe_all();
return false;
}
} else {
SCOPED_TRACE(_chunks[chunk_id].source);
if (!validate_line_by_line(context, chunk_id, "STDERR", error,
original_std_err,
_chunks[chunk_id].code[0].first,
validations[valindex]->def->linenum)) {
output_handler.wipe_all();
return false;
}
}
}
}
}
if (!optional && validations.empty()) {
ADD_FAILURE_AT(_chunks[chunk_id].source.c_str(),
_chunks[chunk_id].code[0].first)
<< makered("MISSING VALIDATIONS FOR CHUNK ")
<< _chunks[chunk_id].def->line << "\n"
<< makeyellow("\tSTDOUT: ") << original_std_out << "\n"
<< makeyellow("\tSTDERR: ") << original_std_err << "\n";
return false;
}
output_handler.wipe_all();
_cout.str("");
_cout.clear();
} else {
// There were errors
if (!original_std_err.empty()) {
ADD_FAILURE_AT(_chunks[chunk_id].source.c_str(),
_chunks[chunk_id].code[0].first)
<< "while executing chunk: " + _chunks[chunk_id].def->line << "\n"
<< makered("\tUnexpected Error: ") + original_std_err << "\n";
} else if (!optional && _chunks.find(chunk_id) != _chunks.end()) {
// The error is that there are no validations
ADD_FAILURE_AT(_chunks[chunk_id].source.c_str(),
_chunks[chunk_id].code[0].first)
<< makered("MISSING VALIDATIONS FOR CHUNK ")
<< _chunks[chunk_id].def->line << "\n"
<< makeyellow("\tSTDOUT: ") << original_std_out << "\n"
<< makeyellow("\tSTDERR: ") << original_std_err << "\n";
}
output_handler.wipe_all();
_cout.str("");
_cout.clear();
}
return true;
}
void Shell_script_tester::validate_interactive(const std::string &script) {
_filename = script;
try {
if (output_handler.passwords.size() || output_handler.prompts.size()) {
std::cerr << "Starting with " << output_handler.passwords.size()
<< " password prompts and " << output_handler.prompts.size()
<< " regular prompts queued\n";
}
execute_script(script, true);
if (testutil->test_skipped()) {
SKIP_TEST(testutil->test_skip_reason());
}
} catch (std::exception &e) {
std::string error = e.what();
FAIL() << makered("Unexpected exception executing test script: ")
<< e.what() << "\n";
}
}
static bool is_identifier_assignment(const std::string &s) {
std::string::size_type p = 0, pp;
p = mysqlshdk::utils::span_keyword(s, p);
if (p == 0) return false;
p = mysqlshdk::utils::span_spaces(s, p);
if (s[p] != '=') return false;
++p;
pp = mysqlshdk::utils::span_spaces(s, p);
pp = mysqlshdk::utils::span_keyword(s, pp);
if (pp == p) return false;
if (s[pp] == ';') ++pp;
if (pp < s.size()) return false;
return true;
}
static std::string find_in_parent_dir(std::string dir,
const std::string &file) {
while (!dir.empty() && dir != "/" && dir != "\\") {
std::string path = shcore::path::join_path(dir, file);
fprintf(stderr, "CHECK %s\n", path.c_str());
if (shcore::path_exists(path)) return path;
dir = shcore::path::dirname(dir);
}
return "";
}
bool Shell_script_tester::load_source_chunks(const std::string &path,
std::istream &stream,
const std::string &prefix) {
std::string last_id;
auto last_chunk = [this, &last_id]() { return &_chunks.at(last_id); };
int linenum = 0;
bool ret_val = true;
bool last_chunk_enabled = true;
while (ret_val && !stream.eof()) {
std::string line;
std::getline(stream, line);
linenum++;
line = str_rstrip(line, "\r\n");
std::shared_ptr<Chunk_definition> chunk_def = load_chunk_definition(line);
if (chunk_def) {
if (!prefix.empty()) {
chunk_def->id = prefix + chunk_def->id;
chunk_def->validation_id = prefix + chunk_def->validation_id;
}
// Full Script Context Validation is supported by defining context
// validation at the __global__ scope.
// The way to do it is with an anonymous chunk with the context
// validation i.e.
// #@ {<context validation>}
if (chunk_def->id.empty()) {
if ((last_id.empty() || last_id == prefix + "__global__") &&
!context_enabled(chunk_def->context)) {
if (output_handler.internal_std_err.empty()) {
ADD_SKIPPED_TEST(line);
} else {
ADD_SKIPPED_TEST(line + ": " + output_handler.internal_std_err);
}
ret_val = false;
}
} else {
chunk_def->linenum = linenum;
last_id = chunk_def->id;
// Starts the new chunk
{
Chunk_t chunk;
chunk.source = path;
chunk.def = chunk_def;
// Every chunk will now be added depending on the context condition
last_chunk_enabled = add_source_chunk(path, chunk);
}
}
// Handle include files... we make them look like chunks even if they
// aren't to make it obvious the previous chunk is over
if (str_beginswith(chunk_def->id, "INCLUDE ")) {
// Syntax:
//@ INCLUDE file.inc
//@ INCLUDE tag file.inc
auto tokens = str_split(line, " ", -1, true);
if (tokens.size() > 2) {
std::string include = tokens[2];
std::string tag;
if (tokens.size() > 3) {
tag = tokens[2];
include = tokens[3];
}
// Try in the same dir as the test
std::string inc_path =
shcore::path::join_path(shcore::path::dirname(path), include);
auto load = [this, include, tag](const std::string &ipath) {
if (!ipath.empty()) {
std::ifstream inc_stream(ipath.c_str());
if (!inc_stream.fail()) {
std::string namespc =
std::get<0>(shcore::path::split_extension(include));
if (!tag.empty()) namespc = tag + "::" + namespc;
load_source_chunks(include, inc_stream, namespc + "::");
return true;
}
}
return false;
};
if (!load(inc_path)) {
// Try in the setup dir for the language
std::string lang =
std::get<1>(shcore::path::split_extension(path)).substr(1);
if (!load(find_in_parent_dir(
shcore::path::dirname(path),
shcore::path::join_path("setup_" + lang, include)))) {
ADD_FAILURE_AT(path.c_str(), linenum - 1)
<< makered("Could not load include file " + inc_path) << "\n";
}
}
} else {
ADD_FAILURE_AT(path.c_str(), linenum - 1)
<< makered("Invalid INCLUDE directive") << "\n";
}
}
} else if (last_chunk_enabled) {
// Only adds the lines that are NO snippet specifier
if (line.find("//! [") != 0) {
if (last_id.empty()) {
// Add __global__ at the 1st line we find
Chunk_t chunk;
chunk.source = path;
chunk.def->id = last_id = prefix + "__global__";
chunk.def->validation_id = prefix + "__global__";
chunk.def->validation = ValidationType::Optional;
chunk.code.push_back({linenum, line});
add_source_chunk(path, chunk);
} else {
// To simplify validation error handling and reporting, we limit
// what can appear in an INCLUDE chunk to:
// - comments
// - empty space
// - identifier assignments (x = y)
if (str_beginswith(last_chunk()->def->id, "INCLUDE ")) {
if (!line.empty() && !str_beginswith(line, "//") &&
!is_identifier_assignment(line)) {
ADD_FAILURE_AT(path.c_str(), linenum - 1)
<< makered("No code allowed after an //@ INCLUDE directive")
<< "\n";
}
}
// If the chunk was not added because of the context, it's lines
// are simply ignored
last_chunk()->code.push_back({linenum, line});
}
}
}
}
return ret_val;
}
bool Shell_script_tester::add_source_chunk(const std::string &path,
const Chunk_t &chunk) {
bool enabled = true;
try {
enabled = context_enabled(chunk.def->context);
} catch (const std::exception &err) {
// NO OP: If evaluating the context fails, it might be a context that is
// set with the script execution so we continue considering it enabled.
// Errors on this validation are ignored
}
if (enabled) {
if (_chunks.find(chunk.def->id) == _chunks.end()) {
_chunks[chunk.def->id] = chunk;
// normalize Windows paths
_chunks[chunk.def->id].source = str_replace(chunk.source, "\\", "/");
_chunk_order.push_back(chunk.def->id);
} else {
ADD_FAILURE_AT(path.c_str(), chunk.def->linenum - 1)
<< makered("REDEFINITION OF CHUNK: \"") + chunk.source << ":"
<< chunk.def->line << "\"\n"
<< "\tInitially defined at " << _chunks[chunk.def->id].source << ":"
<< (_chunks[chunk.def->id].code[0].first - 1) << "\n";
}
} else {
_skipped_chunks.insert(chunk.def->id);
if (output_handler.internal_std_err.empty()) {
SKIP_CHUNK(chunk.def->line);
} else {
SKIP_CHUNK(chunk.def->line + ": " + output_handler.internal_std_err);
}
}
return enabled;
}
void Shell_script_tester::add_validation(
const std::shared_ptr<Chunk_definition> &chunk_def,
const std::vector<std::string> &source, const std::string &sep) {
if (source.size() == 3) {
if (_chunk_validations.find(chunk_def->id) == _chunk_validations.end())
_chunk_validations[chunk_def->id] = Chunk_validations();
auto val = std::shared_ptr<Validation>(new Validation(source, chunk_def));
_chunk_validations[chunk_def->id].push_back(val);
} else {
std::string text(makered("WRONG VALIDATION FORMAT FOR CHUNK ") +
chunk_def->line);
text += "\nLine: " + shcore::str_join(source, sep);
SCOPED_TRACE(text.c_str());
ADD_FAILURE();
}
}
/**
* Process the given line to determine if it is a chunk definition.
* @param line The line to be processed.
*
* @returns The chunk definition if the line is in the right format.
*/
std::shared_ptr<Chunk_definition> Shell_script_tester::load_chunk_definition(
const std::string &line) {
std::shared_ptr<Chunk_definition> ret_val;
if (line.find(get_chunk_token()) == 0) {
std::string chunk_id;
std::string validation_id;
std::string chunk_context;
ValidationType val_type = ValidationType::Simple;
std::string stream;
chunk_id = line.substr(get_chunk_token().size());
if (chunk_id[0] == '#') {
if (chunk_id.size() > 1 && chunk_id[1] == ' ')
chunk_id = chunk_id.substr(2);
else
chunk_id = chunk_id.substr(1);
}
// Identifies the version for the chunk expectations
// If no version is specified assigns '*'
auto start = chunk_id.find("{");
auto end = chunk_id.find_last_of("}");
if (start != std::string::npos && end != std::string::npos && start < end) {
chunk_context = chunk_id.substr(start + 1, end - start - 1);
chunk_id = chunk_id.substr(0, start);
}
// Identifies the validation type and the stream if applicable
if (chunk_id.find("<OUT>") == 0) {
stream = "OUT";
chunk_id = chunk_id.substr(5);
chunk_id = str_strip(chunk_id);
val_type = ValidationType::Multiline;
} else if (chunk_id.find("<ERR>") == 0) {
stream = "ERR";
chunk_id = chunk_id.substr(5);
chunk_id = str_strip(chunk_id);
val_type = ValidationType::Multiline;
} else if (chunk_id.find("<PROTOCOL>") == 0) {
stream = "PROTOCOL";
chunk_id = chunk_id.substr(10);
chunk_id = str_strip(chunk_id);
val_type = ValidationType::Multiline;
} else if (chunk_id.find("<>") == 0) {
stream = "";
chunk_id = chunk_id.substr(2);
chunk_id = str_strip(chunk_id);
val_type = ValidationType::Optional;
}
// Identifies the version for the chunk expectations
// If no version is specified assigns '*'
start = chunk_id.find("[USE:");
end = chunk_id.find("]", start);
if (start != std::string::npos && end != std::string::npos) {
validation_id = chunk_id.substr(start + 5, end - start - 5);
chunk_id = chunk_id.substr(0, start);
} else {
validation_id = chunk_id;
}
chunk_id = str_strip(chunk_id);
validation_id = str_strip(validation_id);
ret_val.reset(new Chunk_definition());
ret_val->line = line;
ret_val->id = chunk_id;
ret_val->context = chunk_context;
ret_val->validation = val_type;
ret_val->stream = stream;
ret_val->validation_id = validation_id;
}
return ret_val;
}
void Shell_script_tester::load_validations(const std::string &path) {
std::ifstream file(path.c_str());
std::vector<std::string> lines;
bool skip_chunk = false;
_chunk_validations.clear();
bool chunk_verification = !_chunk_order.empty();
size_t chunk_index = 0;
Chunk_t *current_chunk = &_chunks[_chunk_order[chunk_index]];
if (g_generate_validation_file) chunk_verification = false;
int line_no = 0;
if (!file.fail()) {
std::shared_ptr<Chunk_definition> current_val_def;
std::shared_ptr<Chunk_definition> new_val_def;
while (!file.eof()) {
std::string line;
std::getline(file, line);
line_no++;
line = shcore::str_rstrip(line);
// If a new chunk definition is found
// Adds the accumulated validations to the previous chunk
new_val_def = load_chunk_definition(line);
if (new_val_def) {
new_val_def->linenum = line_no;
skip_chunk = false;
// Adds the previous validations
if (current_val_def && lines.size()) {
std::string value = multiline(lines);
value = shcore::str_rstrip(value);
if (current_val_def->stream == "OUT" ||
current_val_def->stream == "PROTOCOL")
add_validation(current_val_def, {"", value, ""});
else if (current_val_def->stream == "ERR")
add_validation(current_val_def, {"", "", value});
current_val_def = new_val_def;
lines.clear();
} else {
current_val_def = new_val_def;
}
// When the script is loaded in chunks, the validations should come in
// the same order the chunks were loaded
if (chunk_verification) {
// Ensures the found validation is for a valid chunk
if (_chunks.find(current_val_def->id) == _chunks.end() &&
!g_generate_validation_file) {
// The error is ONLY if the script chunk was not skipped
if (std::find(_skipped_chunks.begin(), _skipped_chunks.end(),
current_val_def->id) == _skipped_chunks.end()) {
ADD_FAILURE_AT(path.c_str(), line_no)
<< makered("FOUND VALIDATION FOR UNEXISTING CHUNK ")
<< current_val_def->line << "\n"
<< "\tLINE: " << line_no << "\n";
}
skip_chunk = true;
continue;
}
bool match = current_val_def->id == current_chunk->def->id;
// If the new validation no longer match the current chunk, steps
// to the next chunk
if (!match) {
chunk_index++;
current_chunk = &_chunks[_chunk_order[chunk_index]];
match = current_val_def->id == current_chunk->def->id;
}
bool optional = current_chunk->is_validation_optional();
bool reference =
current_chunk->def->id != current_chunk->def->validation_id;
while ((optional || reference) && !match &&
chunk_index < _chunk_order.size()) {
if (reference) {
auto index =
_chunk_validations.find(current_chunk->def->validation_id);
if (index == _chunk_validations.end()) {
ADD_FAILURE_AT(path.c_str(), line_no)
<< makered("CHUNK REFERENCES AN UNEXISTING VALIDATION")
<< current_chunk->source << ":" << current_chunk->def->line
<< "\n"
<< "\tLINE: " << line_no << "\n";
}
}
chunk_index++;
current_chunk = &_chunks[_chunk_order[chunk_index]];
optional = current_chunk->is_validation_optional();
reference =
current_chunk->def->id != current_chunk->def->validation_id;
match = current_val_def->id == current_chunk->def->id;
}
if (!optional && !match && !g_generate_validation_file) {
ADD_FAILURE_AT(path.c_str(), line_no)
<< makered("EXPECTED VALIDATIONS FOR CHUNK ")
<< current_chunk->source << ":" << current_chunk->def->line
<< "\n"
<< "INSTEAD FOUND FOR CHUNK " << current_val_def->line << "\n"
<< "\tLINE: " << line_no << "\n";
skip_chunk = true;
continue;
}
if (chunk_index >= _chunk_order.size() &&
!g_generate_validation_file) {
ADD_FAILURE_AT(path.c_str(), line_no)
<< makered("UNEXPECTED VALIDATIONS FOR CHUNK ")
<< current_val_def->line << "\n"
<< "\tLINE: " << line_no << "\n";
skip_chunk = true;
}
}
// If the new chunk is wrong, ignores it
// if (!skip_chunk)
// current_chunk = new_val_def;
} else {
if (!skip_chunk && current_val_def) {
if (current_val_def->validation != ValidationType::Multiline) {
// When processing single line validations, lines as comments are
// ignored
if (!shcore::str_beginswith(line.c_str(),
get_comment_token().c_str())) {
line = str_strip(line);
if (!line.empty()) {
std::vector<std::string> tokens;
// parse each line as:
// <sep>stdout<sep>stderr<sep>
// where <sep> can be any single char, such as |
std::string sep = line.substr(0, 1);
tokens = split_string(line, sep, false);
add_validation(current_val_def, tokens, sep);
}
}
} else {
lines.push_back(line);
}
}
}
}
// Adds final formatted value if any
if (current_val_def &&
current_val_def->validation == ValidationType::Multiline) {
std::string value = multiline(lines);
value = str_rstrip(value);
if (current_val_def->stream == "OUT" ||
current_val_def->stream == "PROTOCOL")
add_validation(current_val_def, {"", value, ""});
else if (current_val_def->stream == "ERR")
add_validation(current_val_def, {"", "", value});
}
file.close();
} else {
if (_new_format) {
std::string text("Unable to locate validation script: " + path);
SCOPED_TRACE(text.c_str());
ADD_FAILURE();
}
}
}
void Shell_script_tester::execute_script(const std::string &path,
bool in_chunks, bool is_pre_script) {
// If no path is provided then executes the setup script
std::string script(path.empty() ? _setup_script
: is_pre_script ? PRE_SCRIPT(path)
: _new_format ? NEW_TEST_SCRIPT(path)
: TEST_SCRIPT(path));
std::ifstream stream(script.c_str());
if (!stream.fail()) {
TestResultCatcher catcher(&output_handler.full_output);
// Capture GTest output if we're not tracing, so that it can be dumped
// together with the test trace at the end.
if (g_test_trace_scripts == 0)
testing::UnitTest::GetInstance()->listeners().Append(&catcher);
shcore::on_leave_scope cleaner([&catcher]() {
if (g_test_trace_scripts == 0)
testing::UnitTest::GetInstance()->listeners().Release(&catcher);
});
// When it is a test script, preprocesses it so the
// right execution scenario is in place
if (!path.empty()) {
if (!is_pre_script) {
// Processes independent preprocessing file
std::string pre_script = PRE_SCRIPT(path);
std::ifstream pre_stream(pre_script.c_str());
if (!pre_stream.fail()) {
pre_stream.close();
_custom_context = "Preprocessing";
execute_script(path, false, true);
}
}
// Preprocesses the test file itself
_custom_context = "Setup";
process_setup(stream);
}
// Process the file
if (in_chunks) {
_options->interactive = true;
if (load_source_chunks(script, stream)) {
if (!_chunks.empty()) {
// Loads the validations
load_validations(_new_format ? VAL_SCRIPT(path)
: VALIDATION_SCRIPT(path));
} else {
ADD_SKIPPED_TEST("All test chunks were skipped.");
}
}
// Abort the script processing if something went wrong on the validation
// loading
if (testutil->test_skipped() && ::testing::Test::HasFailure()) return;
std::ofstream ofile;
if (g_generate_validation_file) {
std::string vfile_name = VALIDATION_SCRIPT(path);
if (!shcore::is_file(vfile_name)) {
ofile.open(vfile_name, std::ofstream::out | std::ofstream::trunc);
} else {
vfile_name.append(".new");
ofile.open(vfile_name, std::ofstream::out | std::ofstream::trunc);
}
}
bool skip_until_cleanup = false;
for (size_t index = 0; index < _chunk_order.size(); index++) {
// Prints debugging information
_cout.str("");
_cout.clear();
if (str_beginswith(_chunk_order[index], "INCLUDE ")) {
std::string chunk_log = _chunk_order[index];
std::string splitter(chunk_log.length(), '=');
output_handler.debug_print(makelblue(splitter));
output_handler.debug_print(makelblue(chunk_log));
output_handler.debug_print(makelblue(splitter));
} else {
std::string chunk_log = "CHUNK: " + _chunk_order[index];
std::string splitter(chunk_log.length(), '-');
output_handler.debug_print(makeyellow(splitter));
output_handler.debug_print(makeyellow(chunk_log));
output_handler.debug_print(makeyellow(splitter));
}
// Gets the chunks for the next id
auto &chunk = _chunks[_chunk_order[index]];
bool enabled;
try {
enabled = context_enabled(chunk.def->context);
if (enabled && skip_until_cleanup) {
if (_chunk_order[index] == "Cleanup") {
skip_until_cleanup = false;
} else {
output_handler.debug_print("Chunk skipped...");
enabled = false;
}
}
} catch (const std::exception &e) {
ADD_FAILURE_AT(chunk.source.c_str(), chunk.code[0].first)
<< makered("ERROR EVALUATING CONTEXT: ") << e.what() << "\n"
<< "\tCHUNK: " << chunk.def->line << "\n";
break;
}
// Executes the file line by line
if (enabled) {
_custom_context = "while executing chunk \"" + chunk.def->line +
"\" at " + chunk.source + ":" +
std::to_string(chunk.def->linenum);
set_scripting_context();
auto &code = chunk.code;
std::string full_statement;
for (size_t chunk_item = 0; chunk_item < code.size(); chunk_item++) {
std::string line(code[chunk_item].second);
full_statement.append(line);
// Execution context is at line (statement actually) level
_custom_context = chunk.source + "@[" + _chunk_order[index] + "][" +
std::to_string(chunk.code[chunk_item].first) +
":" + full_statement + "]";
// There's chance to do preprocessing
pre_process_line(path, &line);
if (testutil)
testutil->set_test_execution_context(
chunk.source.c_str(), code[chunk_item].first, this);
if (g_tdb->will_execute(chunk.source, chunk.code[chunk_item].first,
line) ==
mysqlsh::Test_debugger::Action::Skip_execute) {
continue;
}
try {
std::unique_ptr<Timeout> timeout;
if (g_test_script_timeout > 0)
timeout = std::make_unique<Timeout>(this);
execute(chunk.code[chunk_item].first, line);
if (timeout && timeout->did_timeout()) {
ADD_FAILURE() << "line took too long: " << line
<< "\nSkipping the rest of the script...";
skip_until_cleanup = true;
}
} catch (...) {
g_tdb->did_throw(chunk.code[chunk_item].first, line);
throw;
}
if (testutil->test_skipped()) return;
if (_interactive_shell->input_state() == shcore::Input_state::Ok)
full_statement.clear();
else
full_statement.append("\n");
g_tdb->did_execute(chunk.code[chunk_item].first, line);
if (::testing::Test::HasFailure()) {
if (g_tdb->did_execute_test_failure() ==
mysqlsh::Test_debugger::Action::Abort)
FAIL();
}
}
execute("");
if (g_generate_validation_file) {
// Only saves the data if the chunk is not a reference
if (chunk.def->id == chunk.def->validation_id) {
if (_options->trace_protocol) {
std::string protocol_text = _cout.str();
if (!protocol_text.empty()) {
ofile << get_chunk_token() << "<PROTOCOL> "
<< _chunk_order[index] << std::endl;
ofile << protocol_text << std::endl;
}
}
if (!output_handler.std_out.empty()) {
ofile << get_chunk_token() << "<OUT> " << _chunk_order[index]
<< std::endl;
ofile << output_handler.std_out << std::endl;
}
if (!output_handler.std_err.empty()) {
ofile << get_chunk_token() << "<ERR> " << _chunk_order[index]
<< std::endl;
ofile << output_handler.std_err << std::endl;
}
}
output_handler.wipe_all();
_cout.str("");
_cout.clear();
} else {
// Validation contexts is at chunk level
_custom_context =
path + "@[" + _chunk_order[index] + " validation]";
if (!validate(path, _chunk_order[index],
chunk.is_validation_optional())) {
if (g_tdb->on_validate_fail(_chunk_order[index]) ==
mysqlsh::Test_debugger::Action::Abort) {
FAIL();
}
} else {
output_handler.wipe_debug_log();
}
}
} else {
_skipped_chunks.insert(chunk.def->id);
if (output_handler.internal_std_err.empty()) {
SKIP_CHUNK(chunk.def->line);
} else {
SKIP_CHUNK(chunk.def->line + ": " +
output_handler.internal_std_err);
}
}
}
if (g_generate_validation_file) {
ofile.close();
}
} else { // !in_chunks
_options->interactive = false;
// Loads the validations, the validation is to exclude
// - Loading validations for a pre_script
// - Loading validations for a setup script (path empty)
if (!is_pre_script && !path.empty())
load_validations(_new_format ? VAL_SCRIPT(path)
: VALIDATION_SCRIPT(path));
// Abort the script processing if something went wrong on the validation
// loading
if (testutil->test_skipped() && ::testing::Test::HasFailure()) return;
// Processes the script
_interactive_shell->process_stream(stream, script, {}, true);
// When path is empty it is processing a setup script
// If an error is found it will be printed here
if (path.empty() || is_pre_script) {
if (!output_handler.std_err.empty()) {
SCOPED_TRACE(output_handler.std_err);
std::string text("Setup Script: " + _setup_script);
SCOPED_TRACE(text.c_str());
ADD_FAILURE();
}
output_handler.wipe_all();
_cout.str("");
_cout.clear();
} else {
// If processing a tets script, performs the validations over it
_options->interactive = true;
if (!validate(script)) {
if (g_test_fail_early) {
// Failure logs are printed on the fly in debug mode
FAIL();
}
} else {
output_handler.wipe_debug_log();
}
}
}
if (::testing::Test::HasFailure() && g_test_trace_scripts == 0) {
std::cerr << makeredbg("----------vvvv Failure Log Begin vvvv----------")
<< std::endl;
output_handler.flush_debug_log();
std::cerr << makeredbg("----------^^^^ Failure Log End ^^^^------------")
<< std::endl;
}
stream.close();
} else {
std::string text("Unable to open test script: " + script);
SCOPED_TRACE(text.c_str());
ADD_FAILURE();
}
}
// Searches for // Assumpsions: comment, if found, creates the __assumptions__
// array And processes the _assumption_script
void Shell_script_tester::process_setup(std::istream &stream) {
bool done = false;
while (!done && !stream.eof()) {
std::string line;
std::getline(stream, line);
if (line.find(get_assumptions_token()) == 0) {
// Removes the assumptions header and parses the rest
std::vector<std::string> tokens;
tokens = split_string(line, ":", true);
// Now parses the real assumptions
std::string assumptions = tokens[1];
tokens = split_string(assumptions, ",", true);
// Now quotes the assumptions
for (size_t index = 0; index < tokens.size(); index++) {
tokens[index] = str_strip(tokens[index]);
tokens[index] = "'" + tokens[index] + "'";
}
// Creates an assumptions array to be processed on the setup script
std::string code = get_variable_prefix() + "__assumptions__ = [" +
str_join(tokens, ",") + "];";
execute(code);
if (_setup_script.empty())
throw std::logic_error(
"A setup script must be specified when there are assumptions on "
"the tested scripts.");
else
execute_script(); // Executes the active setup script
} else {
done = true;
}
}
// Once the assumptions are processed, rewinds the read position
// To the beggining of the script
stream.clear(); // To clean up the eof flag in case it was set
stream.seekg(0, stream.beg);
}
void Shell_script_tester::validate_batch(const std::string &script) {
_filename = script;
execute_script(script, false);
}
void Shell_script_tester::def_var(const std::string &var,
const std::string &value) {
std::string code = get_variable_prefix() + var + " = " + value;
exec_and_out_equals(code);
}
void Shell_script_tester::def_string_var_from_env(const std::string &var,
const std::string &env_var) {
const char *variable =
getenv(env_var.empty() ? var.c_str() : env_var.c_str());
if (variable) {
def_var(var, shcore::str_format("'%s'", variable));
}
}
void Shell_script_tester::def_numeric_var_from_env(const std::string &var,
const std::string &env_var) {
const char *variable =
getenv(env_var.empty() ? var.c_str() : env_var.c_str());
if (variable) {
def_var(var, variable);
}
}
void Shell_script_tester::set_defaults() {
output_handler.wipe_all();
_cout.str("");
_cout.clear();
Crud_test_wrapper::set_defaults();
std::string test_mode;
switch (g_test_recording_mode) {
case mysqlshdk::db::replay::Mode::Direct:
test_mode = "direct";
break;
case mysqlshdk::db::replay::Mode::Record:
test_mode = "record";
break;
case mysqlshdk::db::replay::Mode::Replay:
test_mode = "replay";
break;
}
std::string code =
get_variable_prefix() + "__test_execution_mode = '" + test_mode + "'";
exec_and_out_equals(code);
code = get_variable_prefix() + "__package_year = '" PACKAGE_YEAR "'";
exec_and_out_equals(code);
code = get_variable_prefix() + "__mysh_full_version = '" +
std::string(MYSH_FULL_VERSION) + "'";
exec_and_out_equals(code);
code = get_variable_prefix() + "__mysh_version = '" +
std::string(MYSH_VERSION) + "'";
exec_and_out_equals(code);
code = get_variable_prefix() + "__mysh_version_no_extra = '" +
mysqlshdk::utils::Version(MYSH_VERSION).get_base() + "'";
exec_and_out_equals(code);
int64_t version_num =
mysqlshdk::utils::Version(MYSH_VERSION).get_major() * 10000 +
mysqlshdk::utils::Version(MYSH_VERSION).get_minor() * 100 +
mysqlshdk::utils::Version(MYSH_VERSION).get_patch();
code = get_variable_prefix() +
"__mysh_version_num = " + std::to_string(version_num);
exec_and_out_equals(code);
code = get_variable_prefix() + "__version = '" +
_target_server_version.get_base() + "'";
exec_and_out_equals(code);
code = get_variable_prefix() + "__version_full = '" +
_target_server_version.get_full() + "'";
exec_and_out_equals(code);
version_num = _target_server_version.get_major() * 10000 +
_target_server_version.get_minor() * 100 +
_target_server_version.get_patch();
code =
get_variable_prefix() + "__version_num = " + std::to_string(version_num);
exec_and_out_equals(code);
// Set terminology related variables
if (version_num > 80025) {
def_var("__replica_keyword", "'replica'");
def_var("__replica_keyword_capital", "'Replica'");
def_var("__source_keyword", "'source'");
def_var("__source_keyword_capital", "'Source'");
} else {
def_var("__replica_keyword", "'slave'");
def_var("__replica_keyword_capital", "'Slave'");
def_var("__source_keyword", "'master'");
def_var("__source_keyword_capital", "'Master'");
}
def_var("__mysqluripwd", "''");
def_var("__os_type", "'" + shcore::to_string(shcore::get_os_type()) + "'");
def_var("__machine_type", "'" + shcore::get_machine_type() + "'");
def_var("__test_data_path",
shcore::quote_string(shcore::path::join_path(g_test_home, "data", ""),
'\'') +
";");
def_var("__test_home_path", shcore::quote_string(g_test_home, '\'') + ";");
if (_target_server_version >= mysqlshdk::utils::Version(8, 0, 21)) {
def_var("__default_gr_expel_timeout", "5");
def_var("__default_gr_auto_rejoin_tries", "3");
} else {
def_var("__default_gr_expel_timeout", "0");
def_var("__default_gr_auto_rejoin_tries", "0");
}
if (mysqlsh::dba::supports_paxos_single_leader(_target_server_version)) {
def_var("__default_gr_paxos_single_leader", "'OFF'");
}
def_var("__user_config_path",
shcore::quote_string(shcore::get_user_config_path(), '\''));
#ifdef PYTHON_DEPS
def_var("__python_deps", "1");
#else
def_var("__python_deps", "0");
#endif
#ifdef NDEBUG
// TODO(.) - remove __dbug_off and replace all uses with __dbug
def_var("__dbug_off", "1");
// dbug tests should only run in direct mode, so that traces aren't affected
// by different code branches being taken
def_var("__dbug", !_replaying && !_recording ? "1==1" : "0==1");
#else
def_var("__dbug_off", "0");
def_var("__dbug", "0==1");
#endif
// Variables for OCI Tests
def_string_var_from_env("OCI_CONFIG_HOME");
def_string_var_from_env("OCI_COMPARTMENT_ID");
def_string_var_from_env("OS_NAMESPACE");
def_string_var_from_env("OS_BUCKET_NAME");
// Variables for MDS Tests
def_string_var_from_env("MDS_URI");
auto set_env_if_missing = [](const char *var, const char *value) {
if (!getenv(var)) {
shcore::setenv(var, value);
}
};
// Simple LDAP Authentication Variables
if (getenv("LDAP_SIMPLE_SERVER_HOST")) {
set_env_if_missing("LDAP_SIMPLE_BIND_BASE_DN", "dc=my-domain,dc=com");
}
def_string_var_from_env("LDAP_SIMPLE_SERVER_HOST");
def_string_var_from_env("LDAP_SIMPLE_SERVER_PORT");
def_string_var_from_env("LDAP_SIMPLE_BIND_BASE_DN");
def_string_var_from_env("LDAP_SIMPLE_USER");
def_string_var_from_env("LDAP_SIMPLE_PWD");
def_string_var_from_env("LDAP_SIMPLE_AUTH_STRING");
// LDAP SASL Authentication Variables
if (getenv("LDAP_SASL_SERVER_HOST")) {
set_env_if_missing("LDAP_SASL_BIND_BASE_DN", "dc=my-domain,dc=com");
set_env_if_missing("LDAP_SASL_GROUP_SEARCH_FILTER",
"(|(&(objectClass=posixGroup)(memberUid={UA}))(&("
"objectClass=group)(member={UD})))");
}
def_string_var_from_env("LDAP_SASL_SERVER_HOST");
def_string_var_from_env("LDAP_SASL_SERVER_PORT");
def_string_var_from_env("LDAP_SASL_BIND_BASE_DN");
def_string_var_from_env("LDAP_SASL_USER");
def_string_var_from_env("LDAP_SASL_PWD");
def_string_var_from_env("LDAP_SASL_GROUP_SEARCH_FILTER");
// LDAP Kerberos Authentication Variables
if (getenv("LDAP_KERBEROS_SERVER_HOST")) {
set_env_if_missing("LDAP_KERBEROS_BIND_BASE_DN",
"CN=users,DC=mtr,DC=local");
set_env_if_missing("LDAP_KERBEROS_USER_SEARCH_ATTR", "sAMAccountName");
set_env_if_missing("LDAP_KERBEROS_BIND_ROOT_DN",
"CN=test2,CN=Users,DC=mtr,DC=local");
set_env_if_missing("LDAP_KERBEROS_GROUP_SEARCH_FILTER",
"(&(objectClass=group)(member={UD}))");
}
def_string_var_from_env("LDAP_KERBEROS_SERVER_HOST");
def_string_var_from_env("LDAP_KERBEROS_SERVER_PORT");
def_string_var_from_env("LDAP_KERBEROS_BIND_BASE_DN");
def_string_var_from_env("LDAP_KERBEROS_USER_SEARCH_ATTR");
def_string_var_from_env("LDAP_KERBEROS_BIND_ROOT_DN");
def_string_var_from_env("LDAP_KERBEROS_BIND_ROOT_PWD");
def_string_var_from_env("LDAP_KERBEROS_USER");
def_string_var_from_env("LDAP_KERBEROS_PWD");
def_string_var_from_env("LDAP_KERBEROS_AUTH_STRING");
def_string_var_from_env("LDAP_KERBEROS_GROUP_SEARCH_FILTER");
def_string_var_from_env("LDAP_KERBEROS_DOMAIN");
// Kerberos Authentication Variables
def_string_var_from_env("KERBEROS_USER");
def_string_var_from_env("KERBEROS_PWD");
def_string_var_from_env("KERBEROS_DOMAIN");
// OCI Authentication Variables
def_string_var_from_env("OCI_AUTH_URI");
if (getenv("OCI_AUTH_CONFIG_FILE")) {
def_string_var_from_env("OCI_AUTH_CONFIG_FILE");
} else {
def_var("OCI_AUTH_CONFIG_FILE", "''");
}
if (getenv("OCI_AUTH_PROFILE")) {
def_string_var_from_env("OCI_AUTH_PROFILE");
} else {
def_var("OCI_AUTH_PROFILE", "'DEFAULT'");
}
if (getenv("OCI_AUTH_POSITIVE_TESTS")) {
def_numeric_var_from_env("OCI_AUTH_POSITIVE_TESTS");
} else {
def_var("OCI_AUTH_POSITIVE_TESTS",
getenv("OCI_AUTH_CONFIG_FILE") ? "1" : "0");
}
// Variables for AWS Tests
def_string_var_from_env("MYSQLSH_S3_BUCKET_NAME");
def_string_var_from_env("MYSQLSH_AWS_SHARED_CREDENTIALS_FILE");
def_string_var_from_env("MYSQLSH_AWS_CONFIG_FILE");
def_string_var_from_env("MYSQLSH_AWS_PROFILE");
def_string_var_from_env("MYSQLSH_AWS_REGION");
def_string_var_from_env("MYSQLSH_S3_ENDPOINT_OVERRIDE");
def_var("__libmysql_version_id",
shcore::str_format("'%d'", LIBMYSQL_VERSION_ID));
}
void Shell_js_script_tester::set_defaults() {
_interactive_shell->process_line("\\js");
Shell_script_tester::set_defaults();
#ifdef HAVE_JS
exec_and_out_equals("var __have_javascript = true");
#else
exec_and_out_equals("var __have_javascript = false");
#endif
}
void Shell_py_script_tester::set_defaults() {
_interactive_shell->process_line("\\py");
Shell_script_tester::set_defaults();
#ifdef HAVE_JS
exec_and_out_equals("__have_javascript = True");
#else
exec_and_out_equals("__have_javascript = False");
#endif
}
std::string Shell_script_tester::get_current_mode_command() {
switch (_interactive_shell->shell_context()->interactive_mode()) {
case shcore::IShell_core::Mode::SQL:
return "\\sql";
case shcore::IShell_core::Mode::JavaScript:
return "\\js";
case shcore::IShell_core::Mode::Python:
return "\\py";
case shcore::IShell_core::Mode::None:
return "";
}
return "";
}
void Shell_script_tester::set_scripting_context() {
std::string current = get_current_mode_command();
std::string required = get_switch_mode_command();
std::string context = context_identifier();
context = shcore::str_replace(context, "'", "\\'");
std::string code = get_variable_prefix() += "__test_context = '";
code += context + "';";
if (current != required) {
execute_internal(required);
execute_internal(code);
execute_internal(current);
} else {
execute_internal(code);
}
}
void Shell_script_tester::execute_setup() {
// No need to process setup scripts line by line
#ifdef _WIN32
std::ifstream s(shcore::utf8_to_wide(_setup_script));
#else
std::ifstream s(_setup_script.c_str());
#endif
if (!s.fail()) {
// The return value now depends on the stream processing
_interactive_shell->process_stream(s, _setup_script, {}, true);
s.close();
} else {
std::string text("Unable to open test script: " + _setup_script);
SCOPED_TRACE(text.c_str());
ADD_FAILURE();
}
}
// Append option to the end of the given config file.
void Shell_script_tester::add_to_cfg_file(const std::string &cfgfile_path,
const std::string &option) {
std::ofstream cfgfile(cfgfile_path, std::ios_base::app);
cfgfile << option << std::endl;
cfgfile.close();
}
// Delete lines with the option from the given config file.
void Shell_script_tester::remove_from_cfg_file(const std::string &cfgfile_path,
const std::string &option) {
std::string new_cfgfile_path = cfgfile_path + ".new";
std::ofstream new_cfgfile(new_cfgfile_path);
std::ifstream cfgfile(cfgfile_path);
std::string line;
while (std::getline(cfgfile, line)) {
if (line.find(option) != 0) new_cfgfile << line << std::endl;
}
cfgfile.close();
new_cfgfile.close();
std::remove(cfgfile_path.c_str());
std::rename(new_cfgfile_path.c_str(), cfgfile_path.c_str());
}
// Check whether openssl executable is accessible via PATH
bool Shell_script_tester::has_openssl_binary() {
const char *argv[] = {"openssl", "version", nullptr};
shcore::Process_launcher p(argv);
p.start();
std::string s = p.read_line();
if (p.wait() == 0) {
return shcore::str_beginswith(s, "OpenSSL");
}
return false;
}
bool Shell_script_tester::context_enabled(const std::string &context) {
bool ret_val = true;
std::string code(context);
if (!context.empty()) {
size_t function_pos = code.find("VER(");
while (function_pos != std::string::npos) {
size_t version_pos = code.find_first_of("0123456789", function_pos);
size_t closing_pos = code.find(")", version_pos);
if (version_pos == std::string::npos || closing_pos == std::string::npos)
throw std::invalid_argument("Invalid syntax for VER(#.#.#) macro.");
std::string old_func =
code.substr(function_pos, closing_pos - function_pos + 1);
std::string op = shcore::str_strip(
code.substr(function_pos + 4, version_pos - function_pos - 4));
std::string ver = shcore::str_strip(
code.substr(version_pos, closing_pos - version_pos));
std::string fname =
shcore::get_member_name("versionCheck", get_naming_style());
std::string new_func =
"testutil." + fname + "(__version, '" + op + "', '" + ver + "')";
code = shcore::str_replace(code, old_func, new_func);
function_pos = code.find("VER(");
}
function_pos = code.find("DEF(");
while (function_pos != std::string::npos) {
size_t closing_pos = code.find(")", function_pos);
if (closing_pos == std::string::npos)
throw std::invalid_argument("Invalid syntax for DEF(variable) macro.");
std::string old_func =
code.substr(function_pos, closing_pos - function_pos + 1);
std::string variable = shcore::str_strip(
code.substr(function_pos + 4, closing_pos - function_pos - 4));
std::string new_func = get_if_def(variable);
code = shcore::str_replace(code, old_func, new_func);
function_pos = code.find("DEF(");
}
output_handler.wipe_out();
std::string value;
execute_internal(code);
value = output_handler.internal_std_out;
value = str_strip(value);
if (value == "true" || value == "True") {
ret_val = true;
} else if (value == "false" || value == "False") {
ret_val = false;
} else {
if (!output_handler.internal_std_err.empty()) {
throw std::invalid_argument(output_handler.internal_std_err);
} else {
throw std::invalid_argument(
"Context does not evaluate to a boolean "
"expression");
}
}
}
return ret_val;
}
void Shell_script_tester::execute(int location, const std::string &code) {
// save location in test script that is being currently executed
_current_entry_point = context_identifier();
try {
Crud_test_wrapper::execute(location, code);
_current_entry_point.clear();
} catch (...) {
_current_entry_point.clear();
throw;
}
}
void Shell_script_tester::execute(const std::string &code) {
// save location in test script that is being currently executed
_current_entry_point = context_identifier();
try {
Crud_test_wrapper::execute(code);
_current_entry_point.clear();
} catch (...) {
_current_entry_point.clear();
throw;
}
}