You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
gnucash/libgnucash/backend/xml/test/gtest-load-save-files.cpp

331 lines
12 KiB

/********************************************************************\
* This program is free software; you can redistribute it and/or *
* modify it under the terms of the GNU General Public License as *
* published by the Free Software Foundation; either version 2 of *
* the License, or (at your option) any later version. *
* *
* 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 for more details. *
* *
* You should have received a copy of the GNU General Public License*
* along with this program; if not, contact: *
* *
* Free Software Foundation Voice: +1-617-542-5942 *
* 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 *
* Boston, MA 02110-1301, USA gnu@gnu.org *
* *
\********************************************************************/
#include <glib.h>
#include <glib/gfileutils.h>
#include <glib/gstdio.h>
#include <config.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <dirent.h>
#include <errno.h>
#include <string.h>
#include <zlib.h>
#include <cstdlib>
#include <functional>
#include <memory>
#include <string>
#include <vector>
#include <cashobjects.h>
#include <TransLog.h>
#include <gnc-engine.h>
#include <gnc-prefs.h>
#include <gnc-uri-utils.h>
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wcpp"
#include <gtest/gtest.h>
#pragma GCC diagnostic pop
#include <unittest-support.h>
#include "../gnc-backend-xml.h"
#include "../io-gncxml-v2.h"
#define GNC_LIB_NAME "gncmod-backend-xml"
#define GNC_LIB_REL_PATH "xml"
static std::vector<unsigned char> read_file (std::string filename)
{
gchar *contents;
gsize length = 0;
if (g_file_get_contents (filename.c_str (), &contents, &length, nullptr))
{
std::vector<unsigned char> data(length);
memcpy (data.data (), contents, length);
g_free (contents);
return data;
}
else
{
return {};
}
}
static bool
compare_files (std::string filename1, std::string filename2)
{
auto contents1 = read_file (filename1);
auto contents2 = read_file (filename2);
if (contents1.size () > 0 && contents1.size () == contents2.size ()
&& !memcmp(contents1.data (), contents2.data (), contents1.size ())) {
return true;
} else {
ADD_FAILURE() << "compare_files: " << filename1 << " and " << filename2 << " are different";
return false;
}
}
static bool
decompress_file (std::string filename, const std::vector<unsigned char> &in, std::vector<unsigned char> &out)
{
/* 037 0213 are the header id bytes for a gzipped file. */
if (in.size () < 2 || in[0] != 037 || in[1] != 0213)
{
ADD_FAILURE() << "decompress_file: " << filename << " is not compressed";
return false;
}
z_stream stream{};
stream.next_in = const_cast<unsigned char*>(in.data ());
stream.avail_in = in.size ();
stream.zalloc = Z_NULL;
stream.zfree = Z_NULL;
stream.opaque = Z_NULL;
stream.next_out = out.data ();
stream.avail_out = out.size ();
/* "add 16 to decode only the gzip format" */
int ret = inflateInit2 (&stream, 16 + MAX_WBITS);
if (ret != Z_OK)
{
ADD_FAILURE() << "decompress_file: " << filename << " could not be uncompressed (inflateInit): " << ret;
return false;
}
ret = inflate (&stream, Z_NO_FLUSH);
if (ret != Z_STREAM_END)
{
ADD_FAILURE() << "decompress_file: " << filename << " could not be uncompressed (inflate "
<< in.size () << " into " << out.size() << "): " << ret << " " << stream.msg
<< " (avail_in " << stream.avail_in << " avail_out " << stream.avail_out << ")";
inflateEnd (&stream);
return false;
}
ret = inflateEnd (&stream);
if (ret != Z_OK)
{
ADD_FAILURE() << "decompress_file: " << filename << " could not be uncompressed (inflateEnd): " << ret;
return false;
}
if (stream.avail_in)
{
ADD_FAILURE() << "decompress_file: " << filename << " has unused compressed data: " << stream.avail_in;
return false;
}
out.resize (stream.avail_out);
return true;
}
static bool
compare_compressed_files (std::string uncompressed_filename1, std::string compressed_filename2)
{
auto uncompressed_contents1 = read_file (uncompressed_filename1);
auto compressed_contents2 = read_file (compressed_filename2);
/* Allow some space to grow beyond the expected size */
std::vector<unsigned char> uncompressed_contents2(uncompressed_contents1.size () * 2);
if (!decompress_file (compressed_filename2, compressed_contents2, uncompressed_contents2))
return false;
if (uncompressed_contents1.size () > 0
&& uncompressed_contents1.size () != uncompressed_contents2.size ())
{
ADD_FAILURE() << "compare_compressed_files: " << uncompressed_filename1
<< " and " << compressed_filename2 << " are different sizes or empty ("
<< uncompressed_contents1.size () << " and "
<< uncompressed_contents2.size () << ")";
return false;
}
if (!memcmp(uncompressed_contents1.data (), uncompressed_contents2.data (), uncompressed_contents1.size ())) {
return true;
} else {
ADD_FAILURE() << "compare_compressed_files: " << uncompressed_filename1
<< " and " << compressed_filename2 << " are different";
return false;
}
}
/* The original file is used for comparisons. The file will be different when
* there are future changes in the GnuCash output and needs to be updated if
* that happens.
*
* Using the same file each time also checks that nothing in the file will swap
* between two stable states (bug 746937).
*/
class LoadSaveFiles : public testing::TestWithParam<std::string>
{
public:
static void SetUpTestSuite ()
{
g_setenv ("GNC_UNINSTALLED", "1", TRUE);
qof_init ();
cashobjects_register ();
ASSERT_TRUE(qof_load_backend_library (GNC_LIB_REL_PATH, GNC_LIB_NAME)) << "loading gnc-backend-xml GModule failed";
xaccLogDisable ();
}
static void TearDownTestSuite ()
{
qof_close ();
}
};
#define QOF_SESSION_CHECKED_CALL(_function, _session, ...) \
do { \
_function (_session.get (), ## __VA_ARGS__); \
ASSERT_EQ (qof_session_get_error (_session.get ()), 0) << #_function \
<< " (" << #_session << ".get (), " << #__VA_ARGS__ << "): " << qof_session_get_error (_session.get ()) \
<< " \"" << qof_session_get_error_message (_session.get ()) << "\""; \
} while (0)
TEST_P(LoadSaveFiles, test_file)
{
auto filename = GetParam();
auto base_url = gnc_uri_normalize_uri (filename.c_str (), FALSE);
/* Verify that we can write a compressed version of the original file that
* has the original content when uncompressed.
*/
auto new_compressed_file = filename + "-test-compressed~";
/* Verify that we can read a compressed file and write an uncompressed file
* that has the original content.
*/
auto compressed_url = gnc_uri_normalize_uri (new_compressed_file.c_str (), FALSE);
auto new_uncompressed_file = filename + "-test-uncompressed~";
auto uncompressed_url = gnc_uri_normalize_uri (new_uncompressed_file.c_str (), FALSE);
const char *logdomain = "backend.xml";
GLogLevelFlags loglevel = static_cast<decltype (loglevel)>
(G_LOG_LEVEL_WARNING);
TestErrorStruct check = { loglevel, const_cast<char*> (logdomain), nullptr };
g_log_set_handler (logdomain, loglevel,
(GLogFunc)test_checked_handler, &check);
{
auto load_uncompressed_session = std::shared_ptr<QofSession>{qof_session_new (qof_book_new ()), qof_session_destroy};
QOF_SESSION_CHECKED_CALL(qof_session_begin, load_uncompressed_session, base_url, SESSION_READ_ONLY);
QOF_SESSION_CHECKED_CALL(qof_session_load, load_uncompressed_session, nullptr);
auto save_compressed_session = std::shared_ptr<QofSession>{qof_session_new (nullptr), qof_session_destroy};
g_unlink (new_compressed_file.c_str ());
g_unlink ((new_compressed_file + ".LCK").c_str ());
QOF_SESSION_CHECKED_CALL(qof_session_begin, save_compressed_session, compressed_url, SESSION_NEW_OVERWRITE);
qof_event_suspend ();
qof_session_swap_data (load_uncompressed_session.get (), save_compressed_session.get ());
qof_book_mark_session_dirty (qof_session_get_book (save_compressed_session.get ()));
qof_event_resume ();
qof_session_end (load_uncompressed_session.get ());
gnc_prefs_set_file_save_compressed (TRUE);
QOF_SESSION_CHECKED_CALL(qof_session_save, save_compressed_session, nullptr);
qof_session_end (save_compressed_session.get ());
}
if (!compare_compressed_files (filename, new_compressed_file))
return;
{
auto load_compressed_session = std::shared_ptr<QofSession>{qof_session_new (qof_book_new ()), qof_session_destroy};
QOF_SESSION_CHECKED_CALL(qof_session_begin, load_compressed_session, compressed_url, SESSION_READ_ONLY);
QOF_SESSION_CHECKED_CALL(qof_session_load, load_compressed_session, nullptr);
auto save_uncompressed_session = std::shared_ptr<QofSession>{qof_session_new (nullptr), qof_session_destroy};
g_unlink (new_uncompressed_file.c_str ());
g_unlink ((new_uncompressed_file + ".LCK").c_str ());
QOF_SESSION_CHECKED_CALL(qof_session_begin, save_uncompressed_session, uncompressed_url, SESSION_NEW_OVERWRITE);
qof_event_suspend ();
qof_session_swap_data (load_compressed_session.get (), save_uncompressed_session.get ());
qof_book_mark_session_dirty (qof_session_get_book (save_uncompressed_session.get ()));
qof_event_resume ();
qof_session_end (load_compressed_session.get ());
gnc_prefs_set_file_save_compressed (FALSE);
QOF_SESSION_CHECKED_CALL(qof_session_save, save_uncompressed_session, nullptr);
qof_session_end (save_uncompressed_session.get ());
}
g_free (base_url);
g_free (compressed_url);
g_free (uncompressed_url);
if (!compare_files (filename, new_uncompressed_file))
return;
}
std::vector<std::string> ListTestCases ();
INSTANTIATE_TEST_SUITE_P(
LoadSaveFilesDir,
LoadSaveFiles,
testing::ValuesIn (ListTestCases ()));
std::vector<std::string> ListTestCases ()
{
std::vector<std::string> files;
const char *location = g_getenv ("GNC_TEST_FILES");
if (!location)
location = "test-files/load-save";
std::shared_ptr<GDir> dir{g_dir_open (location, 0, nullptr), g_dir_close};
if (dir)
{
const gchar *entry;
while ((entry = g_dir_read_name (dir.get ())) != nullptr)
{
if (g_str_has_suffix (entry, ".gnucash"))
{
std::shared_ptr<gchar> to_open{g_build_filename (location, entry, (gchar*)nullptr), g_free};
if (!g_file_test (to_open.get (), G_FILE_TEST_IS_DIR))
files.push_back (to_open.get());
}
}
}
else
{
ADD_FAILURE() << "unable to open directory " << location;
}
EXPECT_FALSE(files.empty()) << "no files found in " << location;
return files;
}