Introduce Midori.SpeedDial and unit test

Fixes: https://bugs.launchpad.net/midori/+bug/1038634
This commit is contained in:
Christian Dywan 2012-08-31 23:47:03 +02:00
parent 287fc53da9
commit db0ae6ab60
6 changed files with 312 additions and 276 deletions

View file

@ -1578,92 +1578,6 @@ signal_handler (int signal_id)
} }
#endif #endif
static GKeyFile*
speeddial_new_from_file (const gchar* config,
GError** error)
{
GKeyFile* key_file = g_key_file_new ();
gchar* config_file = g_build_filename (config, "speeddial", NULL);
guint i = 0;
gchar* json_content;
gsize json_length;
GString* script;
JSGlobalContextRef js_context;
gchar* keyfile;
gchar* thumb_dir;
gchar** tiles;
if (g_key_file_load_from_file (key_file, config_file, G_KEY_FILE_NONE, error))
{
g_free (config_file);
return key_file;
}
katze_assign (config_file, g_build_filename (config, "speeddial.json", NULL));
if (!g_file_get_contents (config_file, &json_content, &json_length, NULL))
{
katze_assign (json_content, g_strdup ("'{}'"));
json_length = strlen ("'{}'");
}
script = g_string_sized_new (json_length);
g_string_append (script, "var json = JSON.parse (");
g_string_append_len (script, json_content, json_length);
g_string_append (script, "); "
"var keyfile = '';"
"for (var i in json['shortcuts']) {"
"var tile = json['shortcuts'][i];"
"keyfile += '[Dial ' + tile['id'].substring (1) + ']\\n'"
" + 'uri=' + tile['href'] + '\\n'"
" + 'img=' + tile['img'] + '\\n'"
" + 'title=' + tile['title'] + '\\n\\n';"
"} "
"var columns = json['width'] ? json['width'] : 3;"
"var rows = json['shortcuts'] ? json['shortcuts'].length / columns : 0;"
"keyfile += '[settings]\\n'"
" + 'columns=' + columns + '\\n'"
" + 'rows=' + (rows > 3 ? rows : 3) + '\\n\\n';"
"keyfile;");
g_free (json_content);
js_context = JSGlobalContextCreateInGroup (NULL, NULL);
keyfile = sokoke_js_script_eval (js_context, script->str, NULL);
JSGlobalContextRelease (js_context);
g_string_free (script, TRUE);
g_key_file_load_from_data (key_file, keyfile, -1, 0, NULL);
g_free (keyfile);
tiles = g_key_file_get_groups (key_file, NULL);
thumb_dir = g_build_path (G_DIR_SEPARATOR_S, midori_paths_get_cache_dir (), "thumbnails", NULL);
if (!g_file_test (thumb_dir, G_FILE_TEST_EXISTS))
katze_mkdir_with_parents (thumb_dir, 0700);
g_free (thumb_dir);
while (tiles[i] != NULL)
{
gsize sz;
gchar* uri = g_key_file_get_string (key_file, tiles[i], "uri", NULL);
gchar* img = g_key_file_get_string (key_file, tiles[i], "img", NULL);
if (img != NULL && (uri && *uri && *uri != '#'))
{
guchar* decoded = g_base64_decode (img, &sz);
gchar* thumb_path = sokoke_build_thumbnail_path (uri);
g_file_set_contents (thumb_path, (gchar*)decoded, sz, NULL);
g_free (thumb_path);
g_free (decoded);
}
g_free (img);
g_free (uri);
g_key_file_remove_key (key_file, tiles[i], "img", NULL);
i++;
}
g_strfreev (tiles);
katze_assign (config_file, g_build_filename (config, "speeddial", NULL));
sokoke_key_file_save_to_file (key_file, config_file, NULL);
g_free (config_file);
return key_file;
}
static void static void
midori_soup_session_block_uris_cb (SoupSession* session, midori_soup_session_block_uris_cb (SoupSession* session,
SoupMessage* msg, SoupMessage* msg,
@ -1970,7 +1884,7 @@ main (int argc,
gchar** extensions; gchar** extensions;
MidoriWebSettings* settings; MidoriWebSettings* settings;
gchar* config_file; gchar* config_file;
GKeyFile* speeddial; MidoriSpeedDial* dial;
MidoriStartup load_on_startup; MidoriStartup load_on_startup;
KatzeArray* search_engines; KatzeArray* search_engines;
KatzeArray* bookmarks; KatzeArray* bookmarks;
@ -2476,8 +2390,8 @@ main (int argc,
} }
midori_startup_timer ("History read: \t%f"); midori_startup_timer ("History read: \t%f");
error = NULL; katze_assign (config_file, g_build_filename (config, "speeddial", NULL));
speeddial = speeddial_new_from_file (config, &error); dial = midori_speed_dial_new (config_file, NULL);
/* In case of errors */ /* In case of errors */
if (error_messages->len) if (error_messages->len)
@ -2594,7 +2508,7 @@ main (int argc,
"trash", trash, "trash", trash,
"search-engines", search_engines, "search-engines", search_engines,
"history", history, "history", history,
"speed-dial", speeddial, "speed-dial", dial->keyfile,
NULL); NULL);
g_object_unref (history); g_object_unref (history);
g_object_unref (search_engines); g_object_unref (search_engines);
@ -2657,7 +2571,7 @@ main (int argc,
} }
g_object_unref (settings); g_object_unref (settings);
g_key_file_free (speeddial); g_object_unref (dial);
g_object_unref (app); g_object_unref (app);
g_free (config_file); g_free (config_file);
return 0; return 0;

View file

@ -1234,75 +1234,27 @@ midori_view_save_as_cb (GtkWidget* menuitem,
midori_browser_save_uri (browser, MIDORI_VIEW (view), uri); midori_browser_save_uri (browser, MIDORI_VIEW (view), uri);
} }
static gchar*
midori_browser_speed_dial_get_next_free_slot (MidoriView* view)
{
MidoriBrowser* browser = midori_browser_get_for_widget (GTK_WIDGET (view));
GKeyFile* key_file;
guint slot_count = 0, slot = 1, i;
gchar** groups;
g_object_get (browser, "speed-dial", &key_file, NULL);
groups = g_key_file_get_groups (key_file, NULL);
for (i = 0; groups[i]; i++)
{
if (g_key_file_has_key (key_file, groups[i], "uri", NULL))
slot_count++;
}
while (slot <= slot_count)
{
gchar* dial_id = g_strdup_printf ("Dial %d", slot);
if (!g_key_file_has_group (key_file, dial_id))
{
g_free (dial_id);
return g_strdup_printf ("s%d", slot);
}
g_free (dial_id);
slot++;
}
return g_strdup_printf ("s%d", slot_count + 1);
}
static void static void
midori_browser_add_speed_dial (MidoriBrowser* browser) midori_browser_add_speed_dial (MidoriBrowser* browser)
{ {
GdkPixbuf* img; GdkPixbuf* img;
GtkWidget* view = midori_browser_get_current_tab (browser); GtkWidget* view = midori_browser_get_current_tab (browser);
gchar* uri = g_strdup (midori_view_get_display_uri (MIDORI_VIEW (view))); gchar* slot_id = midori_speed_dial_get_next_free_slot_fk (browser->speeddial);
gchar* title = g_strdup (midori_view_get_display_title (MIDORI_VIEW (view))); gchar* uri;
gchar* slot_id = midori_browser_speed_dial_get_next_free_slot (MIDORI_VIEW (view)); gchar* title;
if (slot_id == NULL) if (slot_id == NULL)
{
g_free (uri);
g_free (title);
return; return;
}
uri = g_strdup (midori_view_get_display_uri (MIDORI_VIEW (view)));
title = g_strdup (midori_view_get_display_title (MIDORI_VIEW (view)));
if ((img = midori_view_get_snapshot (MIDORI_VIEW (view), 240, 160))) if ((img = midori_view_get_snapshot (MIDORI_VIEW (view), 240, 160)))
{ {
GKeyFile* key_file;
gchar* dial_id = g_strdup_printf ("Dial %s", slot_id + 1); gchar* dial_id = g_strdup_printf ("Dial %s", slot_id + 1);
gchar* file_path = sokoke_build_thumbnail_path (uri); midori_speed_dial_add_fk (dial_id, uri, title, img, browser->speeddial);
gchar* thumb_dir = g_build_path (G_DIR_SEPARATOR_S, midori_paths_get_cache_dir (), "thumbnails", NULL);
g_object_get (browser, "speed-dial", &key_file, NULL);
g_key_file_set_string (key_file, dial_id, "uri", uri);
g_key_file_set_string (key_file, dial_id, "title", title);
if (!g_file_test (thumb_dir, G_FILE_TEST_EXISTS))
katze_mkdir_with_parents (thumb_dir, 0700);
gdk_pixbuf_save (img, file_path, "png", NULL, "compression", "7", NULL);
midori_view_save_speed_dial_config (MIDORI_VIEW (view), key_file);
g_object_unref (img);
g_free (file_path);
g_free (thumb_dir);
g_free (dial_id); g_free (dial_id);
midori_view_save_speed_dial_config (MIDORI_VIEW (view), browser->speeddial);
g_object_unref (img);
} }
g_free (uri); g_free (uri);
g_free (title); g_free (title);

View file

@ -0,0 +1,232 @@
/*
Copyright (C) 2011-2012 Christian Dywan <christian@twotoats.de>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
See the file COPYING for the full license text.
*/
namespace Katze {
extern static string mkdir_with_parents (string pathname, int mode);
}
namespace Sokoke {
extern static string js_script_eval (void* ctx, string script, void* error);
extern static string build_thumbnail_path (string uri);
}
namespace Midori {
public class SpeedDial : GLib.Object {
public GLib.KeyFile keyfile;
public SpeedDial (string filename, string? fallback = null) {
keyfile = new GLib.KeyFile ();
try {
keyfile.load_from_file (filename, GLib.KeyFileFlags.NONE);
}
catch (GLib.Error io_error) {
string json;
size_t len;
try {
FileUtils.get_contents (fallback ?? (filename + ".json"),
out json, out len);
}
catch (GLib.Error fallback_error) {
json = "'{}'";
len = 4;
}
var script = new StringBuilder.sized (len);
script.append ("var json = JSON.parse (");
script.append_len (json, (ssize_t)len);
script.append ("""
);
var keyfile = '';
for (var i in json['shortcuts']) {
var tile = json['shortcuts'][i];
keyfile += '[Dial ' + tile['id'].substring (1) + ']\n'
+ 'uri=' + tile['href'] + '\n'
+ 'img=' + tile['img'] + '\n'
+ 'title=' + tile['title'] + '\n\n';
}
var columns = json['width'] ? json['width'] : 3;
var rows = json['shortcuts'] ? json['shortcuts'].length / columns : 0;
keyfile += '[settings]\n'
+ 'columns=' + columns + '\n'
+ 'rows=' + (rows > 3 ? rows : 3) + '\n\n';
keyfile;
""");
try {
keyfile.load_from_data (
Sokoke.js_script_eval (null, script.str, null),
-1, 0);
}
catch (GLib.Error eval_error) {
GLib.critical ("Failed to parse %s as speed dial JSON: %s",
fallback ?? (filename + ".json"), eval_error.message);
}
Katze.mkdir_with_parents (
Path.build_path (Path.DIR_SEPARATOR_S,
Environment.get_user_cache_dir (),
"midori", "thumbnails"), 0700);
foreach (string tile in keyfile.get_groups ()) {
try {
string img = keyfile.get_string (tile, "img");
string uri = keyfile.get_string (tile, "uri");
if (img != null && uri[0] != '\0' && uri[0] != '#') {
uchar[] decoded = Base64.decode (img);
FileUtils.set_data (Sokoke.build_thumbnail_path (uri), decoded);
}
keyfile.remove_key (tile, "img");
}
catch (GLib.Error img_error) {
/* img and uri can be missing */
}
}
}
}
public static string get_next_free_slot_fk (KeyFile keyfile) {
uint slot_count = 0;
foreach (string tile in keyfile.get_groups ()) {
try {
if (keyfile.has_key (tile, "uri"))
slot_count++;
}
catch (KeyFileError error) { }
}
uint slot = 1;
while (slot <= slot_count) {
string tile = "Dial %u".printf (slot);
if (!keyfile.has_group (tile))
return "s%u".printf (slot);
slot++;
}
return "s%u".printf (slot_count + 1);
}
public static void add_fk (string id, string uri, string title, Gdk.Pixbuf img, KeyFile keyfile) {
keyfile.set_string (id, "uri", uri);
keyfile.set_string (id, "title", title);
Katze.mkdir_with_parents (Path.build_path (Path.DIR_SEPARATOR_S,
Paths.get_cache_dir (), "thumbnails"), 0700);
string filename = Sokoke.build_thumbnail_path (uri);
try {
img.save (filename, "png", null, "compression", "7", null);
}
catch (Error error) {
critical ("Failed to save speed dial thumbnail: %s", error.message);
}
}
public static string? get_html_fk (KeyFile? keyfile,
bool close_buttons_left, GLib.Object view, bool load_missing) throws Error {
string? head = null;
string filename = Paths.get_res_filename ("speeddial-head.html");
if (keyfile != null
&& FileUtils.get_contents (filename, out head, null)) {
string header = head.replace ("{title}", _("Speed Dial")).
replace ("{click_to_add}", _("Click to add a shortcut")).
replace ("{enter_shortcut_address}", _("Enter shortcut address")).
replace ("{enter_shortcut_name}", _("Enter shortcut title")).
replace ("{are_you_sure}", _("Are you sure you want to delete this shortcut?"));
var markup = new StringBuilder (header);
uint slot_count = 1;
foreach (string tile in keyfile.get_groups ()) {
try {
if (keyfile.has_key (tile, "uri"))
slot_count++;
}
catch (KeyFileError error) { }
}
/* Try to guess the best X by X grid size */
uint grid_index = 3;
while ((grid_index * grid_index) < slot_count)
grid_index++;
/* Percent width size of one slot */
uint slot_size = (100 / grid_index);
/* No editing in private/ app mode or without scripts */
markup.append_printf (
"%s<style>.cross { display:none }</style>%s" +
"<style> div.shortcut { height: %d%%; width: %d%%; }</style>\n",
Paths.is_readonly () ? "" : "<noscript>",
Paths.is_readonly () ? "" : "</noscript>",
slot_size + 1, slot_size - 4);
/* Combined width of slots should always be less than 100%.
* Use half of the remaining percentage as a margin size */
uint div_factor;
if (slot_size * grid_index >= 100 && grid_index > 4)
div_factor = 8;
else
div_factor = 2;
uint margin = (100 - ((slot_size - 4) * grid_index)) / div_factor;
if (margin > 9)
margin = margin % 10;
markup.append_printf (
"<style> body { overflow:hidden } #content { margin-left: %u%%; }</style>", margin);
if (close_buttons_left)
markup.append_printf (
"<style>.cross { left: -14px }</style>");
foreach (string tile in keyfile.get_groups ()) {
try {
string uri = keyfile.get_string (tile, "uri");
if (uri != null && uri.str ("://") != null && tile.has_prefix ("Dial ")) {
string title = keyfile.get_string (tile, "title");
string thumb_filename = Sokoke.build_thumbnail_path (uri);
uint slot = tile.substring (5, -1).to_int ();
string encoded;
try {
uint8[] thumb;
FileUtils.get_data (thumb_filename, out thumb);
encoded = Base64.encode (thumb);
}
catch (FileError error) {
encoded = null;
if (load_missing)
/* FIXME: midori_view_speed_dial_get_thumb (view, tile, uri); */
critical ("FIXME midori_view_speed_dial_get_thumb");
}
markup.append_printf ("""
<div class="shortcut" id="s%u"><div class="preview">
<a class="cross" href="#" onclick='clearShortcut("s%u");'></a>
<a href="%s"><img src="data:image/png;base64,%s" title='%s'></a>
</div><div class="title" onclick='renameShortcut("s%u");'>%s</div></div>
""",
slot, slot, uri, encoded ?? "", title, slot, title ?? "");
}
else if (tile != "settings")
keyfile.remove_group (tile);
}
catch (KeyFileError error) { }
}
markup.append_printf ("""
<div class="shortcut" id="s%u"><div class="preview new">
<a class="add" href="#" onclick='return getAction("s%u");'></a>
</div><div class="title">%s</div></div>
""",
slot_count + 1, slot_count + 1, _("Click to add a shortcut"));
markup.append_printf ("</div>\n</body>\n</html>\n");
return markup.str;
}
return null;
}
}
}

View file

@ -4230,134 +4230,10 @@ prepare_speed_dial_html (MidoriView* view,
gboolean load_missing) gboolean load_missing)
{ {
MidoriBrowser* browser = midori_browser_get_for_widget (GTK_WIDGET (view)); MidoriBrowser* browser = midori_browser_get_for_widget (GTK_WIDGET (view));
GKeyFile* key_file; GKeyFile* key_file = katze_object_get_object (browser, "speed-dial");
GString* markup = NULL; return midori_speed_dial_get_html_fk (key_file,
guint slot_count = 1, i, grid_index = 3, slot_size; katze_object_get_boolean (view->settings, "close-buttons-left"),
guint margin, div_factor; G_OBJECT (view), load_missing, NULL);
gchar* speed_dial_head;
gchar* file_path;
gchar** groups;
g_object_get (browser, "speed-dial", &key_file, NULL);
file_path = midori_paths_get_res_filename ("speeddial-head.html");
if (key_file != NULL
&& g_access (file_path, F_OK) == 0
&& g_file_get_contents (file_path, &speed_dial_head, NULL, NULL))
{
gchar* header = sokoke_replace_variables (speed_dial_head,
"{title}", _("Speed Dial"),
"{click_to_add}", _("Click to add a shortcut"),
"{enter_shortcut_address}", _("Enter shortcut address"),
"{enter_shortcut_name}", _("Enter shortcut title"),
"{are_you_sure}", _("Are you sure you want to delete this shortcut?"),
NULL);
markup = g_string_new (header);
g_free (speed_dial_head);
g_free (file_path);
g_free (header);
}
else
{
g_free (file_path);
return NULL;
}
groups = g_key_file_get_groups (key_file, NULL);
for (i = 0; groups[i]; i++)
{
if (g_key_file_has_key (key_file, groups[i], "uri", NULL))
slot_count++;
}
/* try to guess the best X by X grid size */
while ((grid_index * grid_index) < slot_count)
grid_index++;
/* percent width size of one slot */
slot_size = (100 / grid_index);
/* No editing in private/ app mode or without scripts */
g_string_append_printf (markup,
"%s<style>.cross { display:none }</style>%s"
"<style> div.shortcut { height: %d%%; width: %d%%; }</style>\n",
midori_paths_is_readonly () ? "" : "<noscript>",
midori_paths_is_readonly () ? "" : "</noscript>",
slot_size + 1, slot_size - 4);
/* Combined width of slots should always be less than 100%.
* Use half of the remaining percentage as a margin size */
if (slot_size * grid_index >= 100 && grid_index > 4)
div_factor = 8;
else
div_factor = 2;
margin = (100 - ((slot_size - 4) * grid_index)) / div_factor;
if (margin > 9)
margin = margin % 10;
g_string_append_printf (markup,
"<style> body { overflow:hidden } #content { margin-left: %d%%; }</style>",
margin);
if (katze_object_get_boolean (view->settings, "close-buttons-left"))
g_string_append_printf (markup,
"<style>.cross { left: -14px }</style>");
for (i = 0; groups[i]; i++)
{
gchar* uri = g_key_file_get_string (key_file, groups[i], "uri", NULL);
if (uri && strstr (uri, "://"))
{
gchar* title = g_key_file_get_string (key_file, groups[i], "title", NULL);
gchar* thumb_file = sokoke_build_thumbnail_path (uri);
gchar* encoded;
guint slot = atoi (groups[i] + strlen ("Dial "));
if (g_access (thumb_file, F_OK) == 0)
{
gsize sz;
gchar* thumb_content;
g_file_get_contents (thumb_file, &thumb_content, &sz, NULL);
encoded = g_base64_encode ((guchar*)thumb_content, sz);
g_free (thumb_content);
}
else
{
encoded = NULL;
if (load_missing)
midori_view_speed_dial_get_thumb (view, groups[i], uri);
}
g_free (thumb_file);
g_string_append_printf (markup,
"<div class=\"shortcut\" id=\"s%d\"><div class=\"preview\">"
"<a class=\"cross\" href=\"#\" onclick='clearShortcut(\"s%d\");'></a>"
"<a href=\"%s\"><img src=\"data:image/png;base64,%s\" title='%s'></a>"
"</div><div class=\"title\" onclick='renameShortcut(\"s%d\");'>%s</div></div>\n",
slot, slot, uri, encoded ? encoded : "", title, slot, title ? title : "");
g_free (title);
g_free (encoded);
}
else if (strcmp (groups[i], "settings"))
g_key_file_remove_group (key_file, groups[i], NULL);
g_free (uri);
}
g_strfreev (groups);
g_string_append_printf (markup,
"<div class=\"shortcut\" id=\"s%d\"><div class=\"preview new\">"
"<a class=\"add\" href=\"#\" onclick='return getAction(\"s%d\");'></a>"
"</div><div class=\"title\">%s</div></div>\n",
slot_count + 1, slot_count + 1, _("Click to add a shortcut"));
g_string_append_printf (markup,
"</div>\n</body>\n</html>\n");
return g_string_free (markup, FALSE);
} }

View file

@ -64,15 +64,18 @@ sokoke_js_script_eval (JSContextRef js_context,
const gchar* script, const gchar* script,
gchar** exception) gchar** exception)
{ {
JSGlobalContextRef temporary_context = NULL;
gchar* value; gchar* value;
JSStringRef js_value_string; JSStringRef js_value_string;
JSStringRef js_script; JSStringRef js_script;
JSValueRef js_exception = NULL; JSValueRef js_exception = NULL;
JSValueRef js_value; JSValueRef js_value;
g_return_val_if_fail (js_context, FALSE);
g_return_val_if_fail (script, FALSE); g_return_val_if_fail (script, FALSE);
if (!js_context)
js_context = temporary_context = JSGlobalContextCreateInGroup (NULL, NULL);
js_script = JSStringCreateWithUTF8CString (script); js_script = JSStringCreateWithUTF8CString (script);
js_value = JSEvaluateScript (js_context, js_script, js_value = JSEvaluateScript (js_context, js_script,
JSContextGetGlobalObject (js_context), NULL, 0, &js_exception); JSContextGetGlobalObject (js_context), NULL, 0, &js_exception);
@ -91,12 +94,16 @@ sokoke_js_script_eval (JSContextRef js_context,
g_free (value); g_free (value);
} }
JSStringRelease (js_message); JSStringRelease (js_message);
if (temporary_context)
JSGlobalContextRelease (temporary_context);
return NULL; return NULL;
} }
js_value_string = JSValueToStringCopy (js_context, js_value, NULL); js_value_string = JSValueToStringCopy (js_context, js_value, NULL);
value = sokoke_js_string_utf8 (js_value_string); value = sokoke_js_string_utf8 (js_value_string);
JSStringRelease (js_value_string); JSStringRelease (js_value_string);
if (temporary_context)
JSGlobalContextRelease (temporary_context);
return value; return value;
} }

55
tests/speeddial.vala Normal file
View file

@ -0,0 +1,55 @@
/*
Copyright (C) 2012 Christian Dywan <christian@twotoasts.de>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
See the file COPYING for the full license text.
*/
string get_test_file (string contents) {
string file;
int fd = FileUtils.open_tmp ("speeddialXXXXXX", out file);
FileUtils.set_contents (file, contents, -1);
FileUtils.close (fd);
return file;
}
namespace Katze {
extern static string assert_str_equal (string input, string result, string expected);
}
static void speeddial_load () {
string data = get_test_file ("""
[Dial 1]
uri=http://example.com
title=Example
[settings]
columns=3
rows=3
""");
string json = get_test_file ("""
'{"shortcuts":[{"id":"s1","href":"http://example.com","title":"Example","img":"a2F0emU="}]}'
""");
var dial_data = new Midori.SpeedDial (data, "");
var dial_json = new Midori.SpeedDial ("", json);
FileUtils.remove (data);
FileUtils.remove (json);
Katze.assert_str_equal (json, dial_data.keyfile.to_data (), dial_json.keyfile.to_data ());
Katze.assert_str_equal (json, Midori.SpeedDial.get_next_free_slot_fk (dial_data.keyfile), "s2");
Katze.assert_str_equal (json, Midori.SpeedDial.get_next_free_Slot_fk (dial_json), "s2");
}
void main (string[] args) {
string temporary_cache = DirUtils.make_tmp ("cacheXXXXXX");
Environment.set_variable ("XDG_CACHE_HOME", temporary_cache, true);
Test.init (ref args);
Test.add_func ("/speeddial/load", speeddial_load);
Test.run ();
DirUtils.remove (temporary_cache);
}