diff --git a/configure b/configure index 22673bc4..51d2ab58 100755 --- a/configure +++ b/configure @@ -1,7 +1,7 @@ #!/bin/bash OPTS=`getopt -o "h" --long \ -help,fetch-only,no-debug,disable-fast-vapi,\ +help,fetch-only,no-debug,disable-fast-vapi,with-tests,\ enable-plugin:,disable-plugin:,\ prefix:,program-prefix:,exec-prefix:,lib-suffix:,\ bindir:,libdir:,includedir:,datadir:,\ @@ -16,6 +16,7 @@ eval set -- "$OPTS" PREFIX=${PREFIX:-/usr/local} ENABLED_PLUGINS= DISABLED_PLUGINS= +BUILD_TESTS= DISABLE_FAST_VAPI= LIB_SUFFIX= NO_DEBUG= @@ -51,6 +52,7 @@ Configuration: feature. fast-vapi mode is slower when doing clean builds, but faster when doing incremental builds (during development). + --with-tests Also build tests. Plugin configuration: --enable-plugin=PLUGIN Enable compilation of plugin PLUGIN. @@ -108,6 +110,7 @@ while true; do --disable-fast-vapi ) DISABLE_FAST_VAPI=yes; shift ;; --no-debug ) NO_DEBUG=yes; shift ;; --fetch-only ) FETCH_ONLY=yes; shift ;; + --with-tests ) BUILD_TESTS=yes; shift ;; # Autotools paths --program-prefix ) PREFIX="$2"; shift; shift ;; --exec-prefix ) EXEC_PREFIX="$2"; shift; shift ;; @@ -238,6 +241,7 @@ cmake -G "$cmake_type" \ -DCMAKE_INSTALL_PREFIX="$PREFIX" \ -DENABLED_PLUGINS="$ENABLED_PLUGINS" \ -DDISABLED_PLUGINS="$DISABLED_PLUGINS" \ + -DBUILD_TESTS="$BUILD_TESTS" \ -DVALA_EXECUTABLE="$VALAC" \ -DCMAKE_VALA_FLAGS="$VALACFLAGS" \ -DDISABLE_FAST_VAPI="$DISABLE_FAST_VAPI" \ diff --git a/xmpp-vala/CMakeLists.txt b/xmpp-vala/CMakeLists.txt index 779540cc..2e2287a6 100644 --- a/xmpp-vala/CMakeLists.txt +++ b/xmpp-vala/CMakeLists.txt @@ -85,3 +85,21 @@ set_target_properties(xmpp-vala PROPERTIES VERSION 0.1 SOVERSION 0) install(TARGETS xmpp-vala ${TARGET_INSTALL}) install(FILES ${CMAKE_BINARY_DIR}/exports/xmpp-vala.vapi ${CMAKE_BINARY_DIR}/exports/xmpp-vala.deps DESTINATION ${VAPI_INSTALL_DIR}) install(FILES ${CMAKE_BINARY_DIR}/exports/xmpp-vala.h DESTINATION ${INCLUDE_INSTALL_DIR}) + +if(BUILD_TESTS) + vala_precompile(ENGINE_TEST_VALA_C + SOURCES + "tests/common.vala" + "tests/testcase.vala" + + "tests/stanza.vala" + CUSTOM_VAPIS + ${CMAKE_BINARY_DIR}/exports/xmpp-vala_internal.vapi + PACKAGES + ${ENGINE_PACKAGES} + ) + + add_definitions(${VALA_CFLAGS}) + add_executable(xmpp-vala-test ${ENGINE_TEST_VALA_C}) + target_link_libraries(xmpp-vala-test xmpp-vala ${SIGNAL_PROTOCOL_PACKAGES}) +endif(BUILD_TESTS) diff --git a/xmpp-vala/src/core/namespace_state.vala b/xmpp-vala/src/core/namespace_state.vala index e71607fa..b55812b4 100644 --- a/xmpp-vala/src/core/namespace_state.vala +++ b/xmpp-vala/src/core/namespace_state.vala @@ -1,68 +1,80 @@ using Gee; namespace Xmpp.Core { + public class NamespaceState { private HashMap uri_to_name = new HashMap (); private HashMap name_to_uri = new HashMap (); public string current_ns_uri; - public NamespaceState () { + private NamespaceState parent; + + public NamespaceState() { add_assoc(XMLNS_URI, "xmlns"); - add_assoc("http://www.w3.org/XML/1998/namespace", "xml"); - current_ns_uri = "http://www.w3.org/XML/1998/namespace"; + add_assoc(XML_URI, "xml"); + current_ns_uri = XML_URI; } - public NamespaceState.for_stanza () { + public NamespaceState.for_stanza() { this(); add_assoc("http://etherx.jabber.org/streams", "stream"); current_ns_uri = "jabber:client"; } - public NamespaceState.copy (NamespaceState old) { + private NamespaceState.copy(NamespaceState old) { foreach (string key in old.uri_to_name.keys) { add_assoc(key, old.uri_to_name[key]); } set_current(old.current_ns_uri); } - public NamespaceState.with_assoc (NamespaceState old, string ns_uri, string name) { + private NamespaceState.with_parent(NamespaceState parent) { + this.copy(parent); + this.parent = parent; + } + + public NamespaceState.with_assoc(NamespaceState old, string ns_uri, string name) { this.copy(old); add_assoc(ns_uri, name); } - public NamespaceState.with_current (NamespaceState old, string current_ns_uri) { + public NamespaceState.with_current(NamespaceState old, string current_ns_uri) { this.copy(old); set_current(current_ns_uri); } - public void add_assoc (string ns_uri, string name) { + public void add_assoc(string ns_uri, string name) { name_to_uri[name] = ns_uri; uri_to_name[ns_uri] = name; } - public void set_current (string current_ns_uri) { + public void set_current(string current_ns_uri) { this.current_ns_uri = current_ns_uri; } - public string find_name (string ns_uri) throws XmlError { + public string find_name(string ns_uri) throws XmlError { if (uri_to_name.has_key(ns_uri)) { return uri_to_name[ns_uri]; } throw new XmlError.NS_DICT_ERROR(@"NS URI $ns_uri not found."); } - public string find_uri (string name) throws XmlError { + public string find_uri(string name) throws XmlError { if (name_to_uri.has_key(name)) { return name_to_uri[name]; } throw new XmlError.NS_DICT_ERROR(@"NS name $name not found."); } - public NamespaceState clone() { - return new NamespaceState.copy(this); + public NamespaceState push() { + return new NamespaceState.with_parent(this); } - public string to_string () { + public NamespaceState pop() { + return parent; + } + + public string to_string() { StringBuilder sb = new StringBuilder (); sb.append ("NamespaceState{"); foreach (string key in uri_to_name.keys) { @@ -77,4 +89,5 @@ public class NamespaceState { return sb.str; } } -} \ No newline at end of file + +} diff --git a/xmpp-vala/src/core/stanza_attribute.vala b/xmpp-vala/src/core/stanza_attribute.vala index f751c801..ea776110 100644 --- a/xmpp-vala/src/core/stanza_attribute.vala +++ b/xmpp-vala/src/core/stanza_attribute.vala @@ -20,6 +20,13 @@ public class StanzaAttribute : StanzaEntry { this.val = val; } + public bool equals(StanzaAttribute other) { + if (other.ns_uri != ns_uri) return false; + if (other.name != name) return false; + if (other.val != val) return false; + return true; + } + internal string printf(string fmt, bool no_ns = false, string? ns_name = null) { if (no_ns) { return fmt.printf(name, (!)val); diff --git a/xmpp-vala/src/core/stanza_node.vala b/xmpp-vala/src/core/stanza_node.vala index 67b8db35..341e67ba 100644 --- a/xmpp-vala/src/core/stanza_node.vala +++ b/xmpp-vala/src/core/stanza_node.vala @@ -300,6 +300,25 @@ public class StanzaNode : StanzaEntry { return this; } + public bool equals(StanzaNode other) { + if (other.name != name) return false; + if (other.val != val) return false; + if (name == "#text") return true; + if (other.ns_uri != ns_uri) return false; + + if (other.sub_nodes.size != sub_nodes.size) return false; + for (int i = 0; i < sub_nodes.size; i++) { + if (!other.sub_nodes[i].equals(sub_nodes[i])) return false; + } + + if (other.attributes.size != attributes.size) return false; + for (int i = 0; i < attributes.size; i++) { + if (!other.attributes[i].equals(attributes[i])) return false; + } + + return true; + } + private const string TAG_START_BEGIN_FORMAT = "%s<{%s}:%s"; private const string TAG_START_EMPTY_END = " />\n"; private const string TAG_START_CONTENT_END = ">\n"; @@ -358,12 +377,13 @@ public class StanzaNode : StanzaEntry { public string to_xml(NamespaceState? state = null) throws XmlError { NamespaceState my_state = state ?? new NamespaceState.for_stanza(); if (name == "#text") return val == null ? "" : (!)encoded_val; + my_state = my_state.push(); foreach (var xmlns in get_attributes_by_ns_uri (XMLNS_URI)) { if (xmlns.val == null) continue; if (xmlns.name == "xmlns") { - my_state = new NamespaceState.with_current(my_state, (!)xmlns.val); + my_state.set_current((!)xmlns.val); } else { - my_state = new NamespaceState.with_assoc(my_state, (!)xmlns.val, xmlns.name); + my_state.add_assoc((!)xmlns.val, xmlns.name); } } var sb = new StringBuilder(); @@ -391,6 +411,7 @@ public class StanzaNode : StanzaEntry { } } } + my_state = my_state.pop(); return sb.str; } } diff --git a/xmpp-vala/src/core/stanza_reader.vala b/xmpp-vala/src/core/stanza_reader.vala index dd284fa6..f4b900d1 100644 --- a/xmpp-vala/src/core/stanza_reader.vala +++ b/xmpp-vala/src/core/stanza_reader.vala @@ -1,7 +1,9 @@ using Gee; namespace Xmpp.Core { + public const string XMLNS_URI = "http://www.w3.org/2000/xmlns/"; +public const string XML_URI = "http://www.w3.org/XML/1998/namespace"; public const string JABBER_URI = "jabber:client"; public errordomain XmlError { @@ -85,7 +87,7 @@ public class StanzaReader { private string read_until_ws() throws XmlError { var res = new StringBuilder(); var what = peek_single(); - while(!is_ws(what)) { + while (!is_ws(what)) { res.append_c(read_single()); what = peek_single(); } @@ -95,7 +97,7 @@ public class StanzaReader { private string read_until_char_or_ws(char x, char y = 0) throws XmlError { var res = new StringBuilder(); var what = peek_single(); - while(what != x && what != y && !is_ws(what)) { + while (what != x && what != y && !is_ws(what)) { res.append_c(read_single()); what = peek_single(); } @@ -105,11 +107,11 @@ public class StanzaReader { private string read_until_char(char x) throws XmlError { var res = new StringBuilder(); var what = peek_single(); - while(what != x) { + while (what != x) { res.append_c(read_single()); what = peek_single(); } - return res.str; + return res.str; } private StanzaAttribute read_attribute() throws XmlError { @@ -169,7 +171,7 @@ public class StanzaReader { eof = true; skip_single(); res.name = read_until_char_or_ws('>'); - while(peek_single() != '>') { + while (peek_single() != '>') { skip_single(); } skip_single(); @@ -184,7 +186,7 @@ public class StanzaReader { res.attributes.add(read_attribute()); skip_until_non_ws(); } - if (read_single() == '/' || res.pseudo ) { + if (read_single() == '/' || res.pseudo) { res.has_nodes = false; skip_single(); } else { @@ -215,8 +217,8 @@ public class StanzaReader { } } - public StanzaNode read_stanza_node(NamespaceState? baseNs = null) throws XmlError { - ns_state = baseNs ?? new NamespaceState.for_stanza(); + public StanzaNode read_stanza_node() throws XmlError { + ns_state = ns_state.push(); var res = read_node_start(); if (res.has_nodes) { bool finishNodeSeen = false; @@ -238,8 +240,7 @@ public class StanzaReader { } finishNodeSeen = true; } else { - res.sub_nodes.add(read_stanza_node(ns_state.clone())); - ns_state = baseNs ?? new NamespaceState.for_stanza(); + res.sub_nodes.add(read_stanza_node()); } } else { res.sub_nodes.add(read_text_node()); @@ -247,16 +248,18 @@ public class StanzaReader { } while (!finishNodeSeen); if (res.sub_nodes.size == 0) res.has_nodes = false; } + ns_state = ns_state.pop(); return res; } - public StanzaNode read_node(NamespaceState? baseNs = null) throws XmlError { + public StanzaNode read_node() throws XmlError { skip_until_non_ws(); if (peek_single() == '<') { - return read_stanza_node(baseNs ?? new NamespaceState.for_stanza()); + return read_stanza_node(); } else { return read_text_node(); } } } + } diff --git a/xmpp-vala/src/core/xmpp_stream.vala b/xmpp-vala/src/core/xmpp_stream.vala index eb6ffaa5..a8201a22 100644 --- a/xmpp-vala/src/core/xmpp_stream.vala +++ b/xmpp-vala/src/core/xmpp_stream.vala @@ -11,7 +11,7 @@ public errordomain IOStreamError { } public class XmppStream { - private static string NS_URI = "http://etherx.jabber.org/streams"; + public const string NS_URI = "http://etherx.jabber.org/streams"; public string remote_name; public XmppLog log = new XmppLog(); diff --git a/xmpp-vala/tests/common.vala b/xmpp-vala/tests/common.vala new file mode 100644 index 00000000..b393ba79 --- /dev/null +++ b/xmpp-vala/tests/common.vala @@ -0,0 +1,93 @@ +namespace Xmpp.Test { + +int main(string[] args) { + GLib.Test.init(ref args); + GLib.Test.set_nonfatal_assertions(); + TestSuite.get_root().add_suite(new Xmpp.Test.StanzaTest().get_suite()); + return GLib.Test.run(); +} + +bool fail_if(bool exp, string? reason = null) { + if (exp) { + if (reason != null) GLib.Test.message(reason); + GLib.Test.fail(); + return true; + } + return false; +} + +void fail_if_reached(string? reason = null) { + fail_if(true, reason); +} + +delegate void ErrorFunc() throws Error; + +void fail_if_not_error_code(ErrorFunc func, int expectedCode, string? reason = null) { + try { + func(); + fail_if_reached(@"$(reason + ": " ?? "")no error thrown"); + } catch (Error e) { + fail_if_not_eq_int(e.code, expectedCode, @"$(reason + ": " ?? "")catched unexpected error"); + } +} + +bool fail_if_not(bool exp, string? reason = null) { + return fail_if(!exp, reason); +} + +bool fail_if_eq_int(int left, int right, string? reason = null) { + return fail_if(left == right, @"$(reason + ": " ?? "")$left == $right"); +} + +bool fail_if_not_eq_node(Core.StanzaNode left, Core.StanzaNode right, string? reason = null) { + if (fail_if_not_eq_str(left.name, right.name, @"$(reason + ": " ?? "")name mismatch")) return true; + if (fail_if_not_eq_str(left.val, right.val, @"$(reason + ": " ?? "")val mismatch")) return true; + if (left.name == "#text") return false; + if (fail_if_not_eq_str(left.ns_uri, right.ns_uri, @"$(reason + ": " ?? "")ns_uri mismatch")) return true; + if (fail_if_not_eq_int(left.sub_nodes.size, right.sub_nodes.size, @"$(reason + ": " ?? "")sub node count mismatch")) return true; + if (fail_if_not_eq_int(left.attributes.size, right.attributes.size, @"$(reason + ": " ?? "")attributes count mismatch")) return true; + for (var i = 0; i < left.sub_nodes.size; i++) { + if (fail_if_not_eq_node(left.sub_nodes[i], right.sub_nodes[i], @"$(reason + ": " ?? "")$(i+1)th subnode mismatch")) return true; + } + for (var i = 0; i < left.attributes.size; i++) { + if (fail_if_not_eq_attr(left.attributes[i], right.attributes[i], @"$(reason + ": " ?? "")$(i+1)th attribute mismatch")) return true; + } + return false; +} + +bool fail_if_not_eq_attr(Core.StanzaAttribute left, Core.StanzaAttribute right, string? reason = null) { + if (fail_if_not_eq_str(left.name, right.name, @"$(reason + ": " ?? "")name mismatch")) return true; + if (fail_if_not_eq_str(left.val, right.val, @"$(reason + ": " ?? "")val mismatch")) return true; + if (fail_if_not_eq_str(left.ns_uri, right.ns_uri, @"$(reason + ": " ?? "")ns_uri mismatch")) return true; + return false; +} + +bool fail_if_not_eq_int(int left, int right, string? reason = null) { + return fail_if_not(left == right, @"$(reason + ": " ?? "")$left != $right"); +} + +bool fail_if_not_eq_str(string? left, string? right, string? reason = null) { + bool nullcheck = (left == null || right == null) && (left != null && right != null); + if (left == null) left = "(null)"; + if (right == null) right = "(null)"; + return fail_if_not(!nullcheck && left == right, @"$(reason + ": " ?? "")$left != $right"); +} + +bool fail_if_not_eq_uint8_arr(uint8[] left, uint8[] right, string? reason = null) { + if (fail_if_not_eq_int(left.length, right.length, @"$(reason + ": " ?? "")array length not equal")) return true; + return fail_if_not_eq_str(Base64.encode(left), Base64.encode(right), reason); +} + +bool fail_if_not_zero_int(int zero, string? reason = null) { + return fail_if_not_eq_int(zero, 0, reason); +} + +bool fail_if_zero_int(int zero, string? reason = null) { + return fail_if_eq_int(zero, 0, reason); +} + +bool fail_if_null(void* what, string? reason = null) { + return fail_if(what == null || (size_t)what == 0, reason); +} + +} diff --git a/xmpp-vala/tests/stanza.vala b/xmpp-vala/tests/stanza.vala new file mode 100644 index 00000000..1662c9bc --- /dev/null +++ b/xmpp-vala/tests/stanza.vala @@ -0,0 +1,105 @@ +using Xmpp.Core; + +namespace Xmpp.Test { + +class StanzaTest : Gee.TestCase { + public StanzaTest() { + base("Stanza"); + + add_test("node_one", test_node_one); + add_test("typical_stream", test_typical_stream); + add_test("ack_stream", test_ack_stream); + } + + private void test_node_one() { + var node1 = new StanzaNode.build("test", "ns1_uri") + .add_self_xmlns() + .put_attribute("ns2", "ns2_uri", XMLNS_URI) + .put_attribute("bla", "blub") + .put_node(new StanzaNode.build("testaa", "ns2_uri") + .put_attribute("ns3", "ns3_uri", XMLNS_URI)) + .put_node(new StanzaNode.build("testbb", "ns3_uri") + .add_self_xmlns()); + + var xml1 = node1.to_xml(); + var node2 = new StanzaReader.for_string(xml1).read_node(); + fail_if_not(node1.equals(node2)); + fail_if_not_eq_str(node1.to_string(), node2.to_string()); + } + + private void test_typical_stream() { + var stream = """ + + + + I'll send a friar with speed, to Mantua, with my letters to thy lord. + + + """; + var root_node_cmp = new StanzaNode.build("stream", "http://etherx.jabber.org/streams") + .put_attribute("to", "example.com") + .put_attribute("xmlns", "jabber:client") + .put_attribute("stream", "http://etherx.jabber.org/streams", XMLNS_URI) + .put_attribute("version", "1.0"); + var node_cmp = new StanzaNode.build("message") + .put_attribute("from", "laurence@example.net/churchyard") + .put_attribute("to", "juliet@example.com") + .put_attribute("lang", "en", XML_URI) + .put_node(new StanzaNode.build("body") + .put_node(new StanzaNode.text("I'll send a friar with speed, to Mantua, with my letters to thy lord."))); + + var reader = new StanzaReader.for_string(stream); + fail_if_not_eq_node(root_node_cmp, reader.read_root_node()); + fail_if_not_eq_node(node_cmp, reader.read_node()); + reader.read_node(); + fail_if_not_error_code(() => reader.read_node(), 3, "end of stream should be reached"); + } + + private void test_ack_stream() { + var stream = """ + + + + + + + + + + + """; + var root_node_cmp = new StanzaNode.build("stream", "http://etherx.jabber.org/streams") + .put_attribute("to", "example.com") + .put_attribute("xmlns", "jabber:client") + .put_attribute("stream", "http://etherx.jabber.org/streams", XMLNS_URI) + .put_attribute("ack", "http://jabber.org/protocol/ack", XMLNS_URI) + .put_attribute("version", "1.0"); + var node_cmp = new StanzaNode.build("features", XmppStream.NS_URI) + .put_node(new StanzaNode.build("ack", "http://jabber.org/protocol/ack")) + .put_node(new StanzaNode.build("bind", "urn:ietf:params:xml:ns:xmpp-bind") + .add_self_xmlns() + .put_node(new StanzaNode.build("required", "urn:ietf:params:xml:ns:xmpp-bind"))); + var node2_cmp = new StanzaNode.build("r", "http://jabber.org/protocol/ack"); + + var reader = new StanzaReader.for_string(stream); + fail_if_not_eq_node(root_node_cmp, reader.read_root_node()); + fail_if_not_eq_node(node_cmp, reader.read_node()); + fail_if_not_eq_node(node2_cmp, reader.read_node()); + reader.read_node(); + fail_if_not_error_code(() => reader.read_node(), 3, "end of stream should be reached"); + } + +} + +} diff --git a/xmpp-vala/tests/testcase.vala b/xmpp-vala/tests/testcase.vala new file mode 100644 index 00000000..dd1fdefd --- /dev/null +++ b/xmpp-vala/tests/testcase.vala @@ -0,0 +1,80 @@ +/* testcase.vala + * + * Copyright (C) 2009 Julien Peeters + * + * 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. + + * This library 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 + * Lesser General Public License for more details. + + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Author: + * Julien Peeters + */ + +public abstract class Gee.TestCase : Object { + + private GLib.TestSuite suite; + private Adaptor[] adaptors = new Adaptor[0]; + + public delegate void TestMethod (); + + public TestCase (string name) { + this.suite = new GLib.TestSuite (name); + } + + public void add_test (string name, owned TestMethod test) { + var adaptor = new Adaptor (name, (owned)test, this); + this.adaptors += adaptor; + + this.suite.add (new GLib.TestCase (adaptor.name, + adaptor.set_up, + adaptor.run, + adaptor.tear_down )); + } + + public virtual void set_up () { + } + + public virtual void tear_down () { + } + + public GLib.TestSuite get_suite () { + return this.suite; + } + + private class Adaptor { + [CCode (notify = false)] + public string name { get; private set; } + private TestMethod test; + private TestCase test_case; + + public Adaptor (string name, + owned TestMethod test, + TestCase test_case) { + this.name = name; + this.test = (owned)test; + this.test_case = test_case; + } + + public void set_up (void* fixture) { + this.test_case.set_up (); + } + + public void run (void* fixture) { + this.test (); + } + + public void tear_down (void* fixture) { + this.test_case.tear_down (); + } + } +}