Files
sudo/plugins/python/regress/testhelpers.c
Robert Manner 27de7dd24d plugins/python: only deinit interpreters when sudo unlinks the plugin
This only happens when sudo unloads the last python plugin.
The reason doing so is because there are some python modules which
does not support importing them again after destroying the interpreter
which has imported them previously.

Another solution would be to just leak the interpreters (let the kernel
free up), but then there might be some python resources like open files
would not get cleaned up correctly if the plugin is badly written.

Tests are meant to test the scenario sudo does, so I have modified them
to generally do not unlink but only a few times (~per plugin type) so it
does not use 48 interpreters (one gets started on every plugin->open) and
it is visible at least which type of plugin fails deinit if there is an
error.
2020-02-19 11:48:16 -07:00

272 lines
8.2 KiB
C

/*
* SPDX-License-Identifier: ISC
*
* Copyright (c) 2020 Robert Manner <robert.manner@oneidentity.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
/*
* This is an open source non-commercial project. Dear PVS-Studio, please check it.
* PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
*/
#include "testhelpers.h"
const char *sudo_conf_developer_mode = TESTDATA_DIR "sudo.conf.developer_mode";
const char *sudo_conf_normal_mode = TESTDATA_DIR "sudo.conf.normal_mode";
struct TestData data;
static void
clean_output(char *output)
{
// we replace some output which otherwise would be test run dependant
str_replace_in_place(output, MAX_OUTPUT, data.tmp_dir, TEMP_PATH_TEMPLATE);
if (data.tmp_dir2)
str_replace_in_place(output, MAX_OUTPUT, data.tmp_dir2, TEMP_PATH_TEMPLATE "2");
str_replace_in_place(output, MAX_OUTPUT, SRC_DIR, "SRC_DIR");
}
const char *
expected_path(const char *format, ...)
{
static char expected_output_file[PATH_MAX];
int count = snprintf(expected_output_file, PATH_MAX, TESTDATA_DIR);
char *filename = expected_output_file + count;
va_list args;
va_start(args, format);
vsprintf(filename, format, args);
va_end(args);
return expected_output_file;
}
char **
create_str_array(size_t count, ...)
{
va_list args;
va_start(args, count);
char ** result = calloc(count, sizeof(char *));
for (size_t i = 0; i < count; ++i) {
const char *str = va_arg(args, char *);
result[i] = (str == NULL ? NULL : strdup(str));
}
va_end(args);
return result;
}
int
is_update(void)
{
static int result = -1;
if (result < 0) {
const char *update = getenv("UPDATE_TESTDATA");
result = (update && strcmp(update, "1") == 0) ? 1 : 0;
}
return result;
}
int
verify_content(char *actual_content, const char *reference_path)
{
clean_output(actual_content);
if (is_update()) {
VERIFY_TRUE(fwriteall(reference_path, actual_content));
} else {
char expected_output[MAX_OUTPUT] = "";
if (!freadall(reference_path, expected_output, sizeof(expected_output))) {
printf("Error: Missing test data at '%s'\n", reference_path);
return false;
}
VERIFY_STR(actual_content, expected_output);
}
return true;
}
int
verify_file(const char *actual_dir, const char *actual_file_name, const char *reference_path)
{
char actual_path[PATH_MAX];
snprintf(actual_path, sizeof(actual_path), "%s/%s", actual_dir, actual_file_name);
char actual_str[MAX_OUTPUT];
if (!freadall(actual_path, actual_str, sizeof(actual_str))) {
printf("Expected that file '%s' gets created, but it was not\n", actual_path);
return false;
}
int rc = verify_content(actual_str, reference_path);
return rc;
}
int
fake_conversation(int num_msgs, const struct sudo_conv_message msgs[],
struct sudo_conv_reply replies[], struct sudo_conv_callback *callback)
{
(void) callback;
snprintf_append(data.conv_str, MAX_OUTPUT, "Question count: %d\n", num_msgs);
for (int i = 0; i < num_msgs; ++i) {
const struct sudo_conv_message *msg = &msgs[i];
snprintf_append(data.conv_str, MAX_OUTPUT, "Question %d: <<%s>> (timeout: %d, msg_type=%d)\n",
i, msg->msg, msg->timeout, msg->msg_type);
if (data.conv_replies[i] == NULL)
return 1; // simulates user interruption (conversation error)
replies[i].reply = strdup(data.conv_replies[i]);
}
return 0; // simulate user answered just fine
}
int
fake_conversation_with_suspend(int num_msgs, const struct sudo_conv_message msgs[],
struct sudo_conv_reply replies[], struct sudo_conv_callback *callback)
{
if (callback != NULL) {
callback->on_suspend(SIGTSTP, callback->closure);
callback->on_resume(SIGCONT, callback->closure);
}
return fake_conversation(num_msgs, msgs, replies, callback);
}
int
fake_printf(int msg_type, const char *fmt, ...)
{
int rc = -1;
va_list args;
va_start(args, fmt);
char *output = NULL;
switch(msg_type) {
case SUDO_CONV_INFO_MSG:
output = data.stdout_str;
break;
case SUDO_CONV_ERROR_MSG:
output = data.stderr_str;
break;
default:
break;
}
if (output)
rc = vsnprintf_append(output, MAX_OUTPUT, fmt, args);
va_end(args);
return rc;
}
int
verify_log_lines(const char *reference_path)
{
char stored_path[PATH_MAX];
snprintf(stored_path, sizeof(stored_path), "%s/%s", data.tmp_dir, "debug.log");
FILE *file = fopen(stored_path, "rb");
if (file == NULL) {
printf("Failed to open file '%s'\n", stored_path);
return false;
}
char line[1024] = "";
char stored_str[MAX_OUTPUT] = "";
while(fgets(line, sizeof(line), file) != NULL) {
const char *line_data = strstr(line, "] "); // this skips the timestamp and pid at the beginning
VERIFY_NOT_NULL(line_data); // malformed log line
line_data += 2;
char *line_end = strstr(line_data, " object at "); // this skips checking the pointer hex
if (line_end)
sprintf(line_end, " object>\n");
VERIFY_TRUE(strlen(stored_str) + strlen(line_data) + 1 < sizeof(stored_str)); // we have enough space in buffer
strcat(stored_str, line_data);
}
clean_output(stored_str);
VERIFY_TRUE(verify_content(stored_str, reference_path));
return true;
}
int
verify_str_set(char **actual_set, char **expected_set, const char *actual_variable_name)
{
VERIFY_NOT_NULL(actual_set);
VERIFY_NOT_NULL(expected_set);
int actual_len = str_array_count(actual_set);
int expected_len = str_array_count(expected_set);
int matches = false;
if (actual_len == expected_len) {
int actual_pos = 0;
for (; actual_pos < actual_len; ++actual_pos) {
char *actual_item = actual_set[actual_pos];
int expected_pos = 0;
for (; expected_pos < expected_len; ++expected_pos) {
if (strcmp(actual_item, expected_set[expected_pos]) == 0)
break;
}
if (expected_pos == expected_len) {
// matching item was not found
break;
}
}
matches = (actual_pos == actual_len);
}
if (!matches) {
char actual_set_str[MAX_OUTPUT] = "";
char expected_set_str[MAX_OUTPUT] = "";
str_array_snprint(actual_set_str, MAX_OUTPUT, actual_set, actual_len);
str_array_snprint(expected_set_str, MAX_OUTPUT, expected_set, expected_len);
VERIFY_PRINT_MSG("%s", actual_variable_name, actual_set_str, "expected",
expected_set_str, "expected to contain the same elements as");
return false;
}
return true;
}
int
mock_python_datetime_now(const char *plugin_name, const char *date_str)
{
char *cmd = NULL;
asprintf(&cmd, "import %s\n" // the plugin has its own submodule
"from datetime import datetime\n" // store the real datetime
"import time\n"
"from unittest.mock import Mock\n"
"%s.datetime = Mock()\n" // replace plugin's datetime
"%s.datetime.now = lambda: datetime.strptime('%s', '%%Y-%%m-%%dT%%H:%%M:%%S')\n",
plugin_name, plugin_name, plugin_name, date_str);
VERIFY_PTR_NE(cmd, NULL);
VERIFY_INT(PyRun_SimpleString(cmd), 0);
free(cmd);
return true;
}