use crate::abi::structs::{_xmlDoc, _xmlNode, _xmlXPathObject};
use crate::abi::types::xmlChar;
use crate::xml::xpath::types::XPathValue;
use std::collections::HashMap;
use std::os::raw::c_void;
pub type XPathFunction = fn(&mut XPathContext, &[XPathValue]) -> Result<XPathValue, String>;
pub type VarLookupFunc =
unsafe extern "C" fn(*mut c_void, *const xmlChar, *const xmlChar) -> *mut _xmlXPathObject;
pub type FuncLookupFunc =
unsafe extern "C" fn(*mut c_void, *const xmlChar, *const xmlChar) -> *mut c_void;
#[derive(Debug, Clone)]
pub struct XPathContext {
pub document: *mut _xmlDoc,
pub context_node: *mut _xmlNode,
pub context_position: i32,
pub context_size: i32,
pub variables: HashMap<String, XPathValue>,
pub namespaces: HashMap<String, String>,
pub functions: HashMap<String, XPathFunction>,
pub error: Option<String>,
pub proximity_position: i32,
pub context_list: Vec<*mut _xmlNode>,
pub recursion_depth: u32,
pub var_lookup_func: Option<VarLookupFunc>,
pub var_lookup_data: *mut c_void,
pub func_lookup_func: Option<FuncLookupFunc>,
pub func_lookup_data: *mut c_void,
}
impl XPathContext {
pub fn new(doc: *mut _xmlDoc) -> Self {
Self {
document: doc,
context_node: std::ptr::null_mut(),
context_position: 1,
context_size: 1,
variables: HashMap::new(),
namespaces: HashMap::new(),
functions: HashMap::new(),
error: None,
proximity_position: 1,
context_list: Vec::new(),
recursion_depth: 0,
var_lookup_func: None,
var_lookup_data: std::ptr::null_mut(),
func_lookup_func: None,
func_lookup_data: std::ptr::null_mut(),
}
}
pub fn set_context_node(&mut self, node: *mut _xmlNode) {
self.context_node = node;
if node.is_null() {
self.context_list.clear();
self.context_position = 1;
self.context_size = 1;
self.proximity_position = 1;
} else {
self.context_list = vec![node];
self.context_position = 1;
self.context_size = 1;
self.proximity_position = 1;
}
}
pub fn set_context_list(&mut self, nodes: Vec<*mut _xmlNode>) {
self.context_size = nodes.len() as i32;
self.context_list = nodes;
self.context_position = 1;
self.proximity_position = 1;
}
pub fn resolve_variable(&self, name: &str) -> Option<XPathValue> {
if let Some(value) = self.variables.get(name) {
return Some(value.clone());
}
if let Some(lookup) = self.var_lookup_func {
let c_name: Vec<xmlChar> = name.bytes().collect();
let result = unsafe { lookup(self.var_lookup_data, std::ptr::null(), c_name.as_ptr()) };
if !result.is_null() {
unsafe {
let _ = result; }
}
}
None
}
pub fn resolve_namespace(&self, prefix: &str) -> Option<String> {
if let Some(uri) = self.namespaces.get(prefix) {
return Some(uri.clone());
}
let mut current = self.context_node;
while !current.is_null() {
unsafe {
let mut ns = (*current).nsDef;
while !ns.is_null() {
let ns_prefix = (*ns).prefix;
let ns_href = (*ns).href;
let prefix_matches = if ns_prefix.is_null() {
prefix.is_empty()
} else {
let mut len = 0;
while *ns_prefix.add(len) != 0 {
len += 1;
}
let slice = std::slice::from_raw_parts(ns_prefix, len);
slice == prefix.as_bytes()
};
if prefix_matches {
let mut len = 0;
while *ns_href.add(len) != 0 {
len += 1;
}
let slice = std::slice::from_raw_parts(ns_href, len);
return Some(String::from_utf8_lossy(slice).into_owned());
}
ns = (*ns).next;
}
}
unsafe {
current = (*current).parent;
}
}
None
}
pub fn lookup_function(&self, name: &str) -> Option<XPathFunction> {
if let Some(func) = self.functions.get(name) {
return Some(*func);
}
if let Some(lookup) = self.func_lookup_func {
let c_name: Vec<xmlChar> = name.bytes().collect();
let _result =
unsafe { lookup(self.func_lookup_data, std::ptr::null(), c_name.as_ptr()) };
}
None
}
pub fn register_function(&mut self, name: &str, func: XPathFunction) {
self.functions.insert(name.to_string(), func);
}
pub fn register_variable(&mut self, name: &str, value: XPathValue) {
self.variables.insert(name.to_string(), value);
}
pub fn register_namespace(&mut self, prefix: &str, uri: &str) {
self.namespaces.insert(prefix.to_string(), uri.to_string());
}
pub fn set_error(&mut self, msg: &str) {
self.error = Some(msg.to_string());
}
pub fn clear_error(&mut self) {
self.error = None;
}
pub fn push_recursion(&mut self) -> Result<(), String> {
const MAX_RECURSION_DEPTH: u32 = 1000;
if self.recursion_depth >= MAX_RECURSION_DEPTH {
return Err(
"XPath evaluation recursion depth exceeded (infinite recursion?)".to_string(),
);
}
self.recursion_depth += 1;
Ok(())
}
pub fn pop_recursion(&mut self) {
assert!(
self.recursion_depth > 0,
"unbalanced pop_recursion: recursion_depth is already 0"
);
self.recursion_depth -= 1;
}
pub fn has_context_node(&self) -> bool {
!self.context_node.is_null()
}
pub fn reset(&mut self) {
self.context_node = std::ptr::null_mut();
self.context_position = 1;
self.context_size = 1;
self.error = None;
self.proximity_position = 1;
self.context_list.clear();
self.recursion_depth = 0;
}
pub fn position(&self) -> i32 {
self.proximity_position
}
pub fn last(&self) -> i32 {
self.context_size
}
pub fn advance_position(&mut self) {
self.proximity_position += 1;
self.context_position = self.proximity_position;
}
pub fn reset_position(&mut self) {
self.proximity_position = 1;
self.context_position = 1;
}
}
impl Default for XPathContext {
fn default() -> Self {
Self::new(std::ptr::null_mut())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::xml::xpath::ast::Expr;
use crate::xml::xpath::types::NodeSet;
unsafe fn create_test_doc() -> *mut _xmlDoc {
let layout = std::alloc::Layout::new::<_xmlDoc>();
let ptr = std::alloc::alloc_zeroed(layout) as *mut _xmlDoc;
assert!(!ptr.is_null(), "failed to allocate test document");
ptr
}
unsafe fn create_test_node() -> *mut _xmlNode {
let layout = std::alloc::Layout::new::<_xmlNode>();
let ptr = std::alloc::alloc_zeroed(layout) as *mut _xmlNode;
assert!(!ptr.is_null(), "failed to allocate test node");
ptr
}
unsafe fn free_test_doc(doc: *mut _xmlDoc) {
if !doc.is_null() {
let layout = std::alloc::Layout::new::<_xmlDoc>();
std::alloc::dealloc(doc as *mut u8, layout);
}
}
unsafe fn free_test_node(node: *mut _xmlNode) {
if !node.is_null() {
let layout = std::alloc::Layout::new::<_xmlNode>();
std::alloc::dealloc(node as *mut u8, layout);
}
}
#[test]
fn test_new_context() {
let ctx = XPathContext::new(std::ptr::null_mut());
assert!(ctx.document.is_null());
assert!(ctx.context_node.is_null());
assert_eq!(ctx.context_position, 1);
assert_eq!(ctx.context_size, 1);
assert!(ctx.variables.is_empty());
assert!(ctx.namespaces.is_empty());
assert!(ctx.functions.is_empty());
assert!(ctx.error.is_none());
assert_eq!(ctx.proximity_position, 1);
assert!(ctx.context_list.is_empty());
assert_eq!(ctx.recursion_depth, 0);
assert!(ctx.var_lookup_func.is_none());
assert!(ctx.var_lookup_data.is_null());
assert!(ctx.func_lookup_func.is_none());
assert!(ctx.func_lookup_data.is_null());
}
#[test]
fn test_default_context() {
let ctx = XPathContext::default();
assert!(ctx.document.is_null());
assert_eq!(ctx.context_position, 1);
}
#[test]
fn test_new_with_doc() {
unsafe {
let doc = create_test_doc();
let ctx = XPathContext::new(doc);
assert_eq!(ctx.document, doc);
free_test_doc(doc);
}
}
#[test]
fn test_set_context_node_non_null() {
unsafe {
let node = create_test_node();
let mut ctx = XPathContext::new(std::ptr::null_mut());
ctx.set_context_node(node);
assert_eq!(ctx.context_node, node);
assert_eq!(ctx.context_position, 1);
assert_eq!(ctx.context_size, 1);
assert_eq!(ctx.proximity_position, 1);
assert_eq!(ctx.context_list.len(), 1);
assert_eq!(ctx.context_list[0], node);
free_test_node(node);
}
}
#[test]
fn test_set_context_node_null() {
let mut ctx = XPathContext::new(std::ptr::null_mut());
let sentinel = 1 as *mut _xmlNode;
ctx.context_list = vec![sentinel];
ctx.context_position = 5;
ctx.context_size = 5;
ctx.proximity_position = 5;
ctx.set_context_node(std::ptr::null_mut());
assert!(ctx.context_node.is_null());
assert!(ctx.context_list.is_empty());
assert_eq!(ctx.context_position, 1);
assert_eq!(ctx.context_size, 1);
assert_eq!(ctx.proximity_position, 1);
}
#[test]
fn test_set_context_list() {
unsafe {
let node1 = create_test_node();
let node2 = create_test_node();
let nodes = vec![node1, node2];
let mut ctx = XPathContext::new(std::ptr::null_mut());
ctx.set_context_list(nodes.clone());
assert_eq!(ctx.context_list.len(), 2);
assert_eq!(ctx.context_size, 2);
assert_eq!(ctx.context_position, 1);
assert_eq!(ctx.proximity_position, 1);
free_test_node(node1);
free_test_node(node2);
}
}
#[test]
fn test_set_context_list_empty() {
let mut ctx = XPathContext::new(std::ptr::null_mut());
ctx.set_context_list(vec![]);
assert!(ctx.context_list.is_empty());
assert_eq!(ctx.context_size, 0);
assert_eq!(ctx.context_position, 1);
}
#[test]
fn test_register_and_resolve_variable() {
let mut ctx = XPathContext::new(std::ptr::null_mut());
ctx.register_variable("foo", XPathValue::String("bar".to_string()));
let result = ctx.resolve_variable("foo");
assert!(result.is_some());
assert_eq!(result.unwrap().as_string(), "bar");
}
#[test]
fn test_resolve_unknown_variable() {
let ctx = XPathContext::new(std::ptr::null_mut());
assert!(ctx.resolve_variable("nonexistent").is_none());
}
#[test]
fn test_register_variable_number() {
let mut ctx = XPathContext::new(std::ptr::null_mut());
ctx.register_variable("pi", XPathValue::Number(3.14159));
let result = ctx.resolve_variable("pi");
assert!(result.is_some());
let val = result.unwrap();
assert!((val.as_number() - 3.14159).abs() < 1e-10);
}
#[test]
fn test_register_variable_boolean() {
let mut ctx = XPathContext::new(std::ptr::null_mut());
ctx.register_variable("flag", XPathValue::Boolean(true));
let result = ctx.resolve_variable("flag");
assert!(result.is_some());
assert!(result.unwrap().as_boolean());
}
#[test]
fn test_register_variable_nodeset() {
let mut ctx = XPathContext::new(std::ptr::null_mut());
let ns = NodeSet::new();
ctx.register_variable("nodes", XPathValue::NodeSet(ns));
let result = ctx.resolve_variable("nodes");
assert!(result.is_some());
assert!(matches!(result.unwrap(), XPathValue::NodeSet(_)));
}
#[test]
fn test_variable_overwrite() {
let mut ctx = XPathContext::new(std::ptr::null_mut());
ctx.register_variable("x", XPathValue::Number(1.0));
ctx.register_variable("x", XPathValue::Number(2.0));
let result = ctx.resolve_variable("x");
assert!(result.is_some());
assert!((result.unwrap().as_number() - 2.0).abs() < 1e-10);
}
#[test]
fn test_register_and_resolve_namespace() {
let mut ctx = XPathContext::new(std::ptr::null_mut());
ctx.register_namespace("xslt", "http://www.w3.org/1999/XSL/Transform");
let result = ctx.resolve_namespace("xslt");
assert!(result.is_some());
assert_eq!(result.unwrap(), "http://www.w3.org/1999/XSL/Transform");
}
#[test]
fn test_resolve_unknown_namespace() {
let ctx = XPathContext::new(std::ptr::null_mut());
assert!(ctx.resolve_namespace("unknown").is_none());
}
#[test]
fn test_register_default_namespace() {
let mut ctx = XPathContext::new(std::ptr::null_mut());
ctx.register_namespace("", "http://example.com/default");
let result = ctx.resolve_namespace("");
assert!(result.is_some());
assert_eq!(result.unwrap(), "http://example.com/default");
}
#[test]
fn test_namespace_overwrite() {
let mut ctx = XPathContext::new(std::ptr::null_mut());
ctx.register_namespace("a", "http://example.com/1");
ctx.register_namespace("a", "http://example.com/2");
let result = ctx.resolve_namespace("a");
assert_eq!(result.unwrap(), "http://example.com/2");
}
#[test]
fn test_register_and_lookup_function() {
fn test_func(_ctx: &mut XPathContext, _args: &[XPathValue]) -> Result<XPathValue, String> {
Ok(XPathValue::String("test".to_string()))
}
let mut ctx = XPathContext::new(std::ptr::null_mut());
ctx.register_function("test:func", test_func);
let result = ctx.lookup_function("test:func");
assert!(result.is_some());
}
#[test]
fn test_lookup_unknown_function() {
let ctx = XPathContext::new(std::ptr::null_mut());
assert!(ctx.lookup_function("nonexistent").is_none());
}
#[test]
fn test_function_overwrite() {
fn func_a(_ctx: &mut XPathContext, _args: &[XPathValue]) -> Result<XPathValue, String> {
Ok(XPathValue::String("a".to_string()))
}
fn func_b(_ctx: &mut XPathContext, _args: &[XPathValue]) -> Result<XPathValue, String> {
Ok(XPathValue::String("b".to_string()))
}
let mut ctx = XPathContext::new(std::ptr::null_mut());
ctx.register_function("f", func_a);
ctx.register_function("f", func_b);
let result = ctx.lookup_function("f");
assert!(result.is_some());
if let Some(f) = result {
let mut tmp_ctx = XPathContext::new(std::ptr::null_mut());
let value = f(&mut tmp_ctx, &[]).unwrap();
assert_eq!(value.as_string(), "b");
}
}
#[test]
fn test_set_and_get_error() {
let mut ctx = XPathContext::new(std::ptr::null_mut());
assert!(ctx.error.is_none());
ctx.set_error("something went wrong");
assert_eq!(ctx.error.as_deref(), Some("something went wrong"));
}
#[test]
fn test_clear_error() {
let mut ctx = XPathContext::new(std::ptr::null_mut());
ctx.set_error("an error");
assert!(ctx.error.is_some());
ctx.clear_error();
assert!(ctx.error.is_none());
}
#[test]
fn test_error_overwrite() {
let mut ctx = XPathContext::new(std::ptr::null_mut());
ctx.set_error("first error");
ctx.set_error("second error");
assert_eq!(ctx.error.as_deref(), Some("second error"));
}
#[test]
fn test_push_pop_recursion() {
let mut ctx = XPathContext::new(std::ptr::null_mut());
assert_eq!(ctx.recursion_depth, 0);
assert!(ctx.push_recursion().is_ok());
assert_eq!(ctx.recursion_depth, 1);
ctx.pop_recursion();
assert_eq!(ctx.recursion_depth, 0);
}
#[test]
fn test_recursion_depth_limit() {
let mut ctx = XPathContext::new(std::ptr::null_mut());
for _ in 0..1000 {
assert!(ctx.push_recursion().is_ok());
}
assert_eq!(ctx.recursion_depth, 1000);
let result = ctx.push_recursion();
assert!(result.is_err());
assert!(result.unwrap_err().contains("recursion depth exceeded"));
for _ in 0..1000 {
ctx.pop_recursion();
}
assert_eq!(ctx.recursion_depth, 0);
}
#[test]
#[should_panic(expected = "unbalanced pop_recursion")]
fn test_pop_recursion_underflow() {
let mut ctx = XPathContext::new(std::ptr::null_mut());
ctx.pop_recursion(); }
#[test]
fn test_recursion_nesting() {
let mut ctx = XPathContext::new(std::ptr::null_mut());
assert!(ctx.push_recursion().is_ok());
assert!(ctx.push_recursion().is_ok());
assert!(ctx.push_recursion().is_ok());
assert_eq!(ctx.recursion_depth, 3);
ctx.pop_recursion();
assert_eq!(ctx.recursion_depth, 2);
ctx.pop_recursion();
assert_eq!(ctx.recursion_depth, 1);
ctx.pop_recursion();
assert_eq!(ctx.recursion_depth, 0);
}
#[test]
fn test_position_and_last() {
let mut ctx = XPathContext::new(std::ptr::null_mut());
assert_eq!(ctx.position(), 1);
assert_eq!(ctx.last(), 1);
}
#[test]
fn test_advance_position() {
let mut ctx = XPathContext::new(std::ptr::null_mut());
ctx.advance_position();
assert_eq!(ctx.position(), 2);
assert_eq!(ctx.proximity_position, 2);
assert_eq!(ctx.context_position, 2);
}
#[test]
fn test_reset_position() {
let mut ctx = XPathContext::new(std::ptr::null_mut());
ctx.advance_position();
ctx.advance_position();
ctx.advance_position();
assert_eq!(ctx.position(), 4);
ctx.reset_position();
assert_eq!(ctx.position(), 1);
assert_eq!(ctx.context_position, 1);
}
#[test]
fn test_position_with_context_list() {
unsafe {
let node1 = create_test_node();
let node2 = create_test_node();
let node3 = create_test_node();
let nodes = vec![node1, node2, node3];
let mut ctx = XPathContext::new(std::ptr::null_mut());
ctx.set_context_list(nodes);
assert_eq!(ctx.last(), 3);
assert_eq!(ctx.position(), 1);
ctx.advance_position();
assert_eq!(ctx.position(), 2);
ctx.advance_position();
assert_eq!(ctx.position(), 3);
free_test_node(node1);
free_test_node(node2);
free_test_node(node3);
}
}
#[test]
fn test_has_context_node() {
let mut ctx = XPathContext::new(std::ptr::null_mut());
assert!(!ctx.has_context_node());
unsafe {
let node = create_test_node();
ctx.set_context_node(node);
assert!(ctx.has_context_node());
free_test_node(node);
}
}
#[test]
fn test_reset() {
let mut ctx = XPathContext::new(std::ptr::null_mut());
ctx.set_error("test error");
ctx.proximity_position = 5;
ctx.context_position = 5;
ctx.context_size = 10;
ctx.recursion_depth = 3;
unsafe {
let sentinel = 1 as *mut _xmlNode;
ctx.context_list = vec![sentinel];
}
ctx.register_variable("x", XPathValue::Number(42.0));
ctx.register_namespace("p", "http://example.com/ns");
fn dummy(_: &mut XPathContext, _: &[XPathValue]) -> Result<XPathValue, String> {
Ok(XPathValue::Boolean(true))
}
ctx.register_function("f", dummy);
ctx.reset();
assert!(ctx.context_node.is_null());
assert_eq!(ctx.context_position, 1);
assert_eq!(ctx.context_size, 1);
assert_eq!(ctx.proximity_position, 1);
assert!(ctx.error.is_none());
assert!(ctx.context_list.is_empty());
assert_eq!(ctx.recursion_depth, 0);
assert!(ctx.resolve_variable("x").is_some());
assert!(ctx.resolve_namespace("p").is_some());
assert!(ctx.lookup_function("f").is_some());
}
#[test]
fn test_callback_fields_default_to_none() {
let ctx = XPathContext::new(std::ptr::null_mut());
assert!(ctx.var_lookup_func.is_none());
assert!(ctx.var_lookup_data.is_null());
assert!(ctx.func_lookup_func.is_none());
assert!(ctx.func_lookup_data.is_null());
}
#[test]
fn test_set_callback_fields() {
let mut ctx = XPathContext::new(std::ptr::null_mut());
unsafe extern "C" fn dummy_var_lookup(
_data: *mut c_void,
_ns: *const xmlChar,
_name: *const xmlChar,
) -> *mut _xmlXPathObject {
std::ptr::null_mut()
}
unsafe extern "C" fn dummy_func_lookup(
_data: *mut c_void,
_ns: *const xmlChar,
_name: *const xmlChar,
) -> *mut c_void {
std::ptr::null_mut()
}
let data_ptr = &mut 42u32 as *mut u32 as *mut c_void;
ctx.var_lookup_func = Some(dummy_var_lookup);
ctx.var_lookup_data = data_ptr;
ctx.func_lookup_func = Some(dummy_func_lookup);
ctx.func_lookup_data = data_ptr;
assert!(ctx.var_lookup_func.is_some());
assert!(!ctx.var_lookup_data.is_null());
assert!(ctx.func_lookup_func.is_some());
assert!(!ctx.func_lookup_data.is_null());
}
#[test]
fn test_context_clone() {
let mut ctx = XPathContext::new(std::ptr::null_mut());
ctx.register_variable("x", XPathValue::Number(10.0));
ctx.register_namespace("ns", "http://example.com/ns");
ctx.set_error("clone test");
let cloned = ctx.clone();
assert_eq!(cloned.document, ctx.document);
assert_eq!(cloned.context_node, ctx.context_node);
assert_eq!(cloned.context_position, ctx.context_position);
assert_eq!(cloned.context_size, ctx.context_size);
assert_eq!(cloned.error, ctx.error);
let var = cloned.resolve_variable("x");
assert!(var.is_some());
assert!((var.unwrap().as_number() - 10.0).abs() < 1e-10);
let ns = cloned.resolve_namespace("ns");
assert!(ns.is_some());
assert_eq!(ns.unwrap(), "http://example.com/ns");
}
#[test]
fn test_context_debug_format() {
let ctx = XPathContext::new(std::ptr::null_mut());
let debug_str = format!("{:?}", ctx);
assert!(debug_str.contains("context_position"));
assert!(debug_str.contains("context_size"));
assert!(debug_str.contains("recursion_depth"));
}
#[test]
fn test_context_size_zero() {
let mut ctx = XPathContext::new(std::ptr::null_mut());
ctx.set_context_list(vec![]);
assert_eq!(ctx.last(), 0);
assert_eq!(ctx.position(), 1);
}
#[test]
fn test_multiple_advancements() {
let mut ctx = XPathContext::new(std::ptr::null_mut());
for i in 1..=10 {
assert_eq!(ctx.position(), i);
ctx.advance_position();
}
assert_eq!(ctx.position(), 11);
}
#[test]
fn test_register_multiple_variables() {
let mut ctx = XPathContext::new(std::ptr::null_mut());
ctx.register_variable("a", XPathValue::Number(1.0));
ctx.register_variable("b", XPathValue::String("two".to_string()));
ctx.register_variable("c", XPathValue::Boolean(true));
assert_eq!(ctx.variables.len(), 3);
assert_eq!(ctx.resolve_variable("a").unwrap().as_number(), 1.0);
assert_eq!(ctx.resolve_variable("b").unwrap().as_string(), "two");
assert!(ctx.resolve_variable("c").unwrap().as_boolean());
}
#[test]
fn test_register_multiple_namespaces() {
let mut ctx = XPathContext::new(std::ptr::null_mut());
ctx.register_namespace("a", "http://example.com/a");
ctx.register_namespace("b", "http://example.com/b");
ctx.register_namespace("c", "http://example.com/c");
assert_eq!(ctx.namespaces.len(), 3);
assert_eq!(ctx.resolve_namespace("a").unwrap(), "http://example.com/a");
assert_eq!(ctx.resolve_namespace("b").unwrap(), "http://example.com/b");
assert_eq!(ctx.resolve_namespace("c").unwrap(), "http://example.com/c");
}
#[test]
fn test_register_multiple_functions() {
fn f1(_: &mut XPathContext, _: &[XPathValue]) -> Result<XPathValue, String> {
Ok(XPathValue::Number(1.0))
}
fn f2(_: &mut XPathContext, _: &[XPathValue]) -> Result<XPathValue, String> {
Ok(XPathValue::Number(2.0))
}
fn f3(_: &mut XPathContext, _: &[XPathValue]) -> Result<XPathValue, String> {
Ok(XPathValue::Number(3.0))
}
let mut ctx = XPathContext::new(std::ptr::null_mut());
ctx.register_function("f1", f1);
ctx.register_function("f2", f2);
ctx.register_function("f3", f3);
assert_eq!(ctx.functions.len(), 3);
}
}