Files
oak-editor/crates/oakcommon/tests/ffi_xmlutils.rs
T
Mike-Solar 18ff60f147 feat(engine): clip move, clip effect_input, mandatory static FFmpeg
- oakengine_sequence_move_clip implemented for real (oaktimeline
  TrackMoveBlockCommand; fixes the graph-ownership/gap-anchor/ripple
  trim bugs the stub was hiding); same-track via the frozen C ABI,
  cross-track supported by the module command
- oaknode clip blocks now declare a tex_in texture input and set
  effect_input to it, so timeline clips can host effect chains; facade
  test covers effect insert/remove on a real clip
- oakffmpeg-link: FFMPEG_DIR is now mandatory with a clear panic (a
  Homebrew upgrade left the system ffmpeg .pc pointing at a deleted
  dav1d Cellar path, breaking links); reads a git-ignored workspace
  .env for IDEs that cannot inject env vars (RustRover); links the C++
  stdlib for C++ codec libs (svt-av1)
- oakengine re-exports oaknode so tests share one crate instance;
  it_node uses the direct instance's value type where it calls the
  module FFI (the --workspace dev-dependency feature split builds
  oaknode twice)
2026-08-11 23:04:48 +08:00

762 lines
21 KiB
Rust

// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// 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 3 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, see <http://www.gnu.org/licenses/>.
//! FFI-level integration tests for the C-ABI XML streaming exports in
//! `oakcommon::ffi::xmlutils`, asserted against the C++ oracle
//! `src/common/c_api/xmlutils.cpp`.
//!
//! `ffi::error` and `ffi::error_abi` are documented placeholder modules
//! (`include/common/error.h` exposes no functions; the `OAKCOMMON_OK` /
//! `OAKCOMMON_E_*` constants live in `crate::error`), so the only callable
//! surface here is `xmlutils`. The error constants its exports return are
//! pinned in `error_constants_returned_by_xmlutils_match_header`; the full
//! set is already asserted in `contract.rs`.
//!
//! The contract under test (each point matches the C++ oracle):
//! - exports take a `CHandle` by value and never release it;
//! - two-stage string getters return the required size (NUL included) and
//! only copy when the buffer is large enough — they never truncate;
//! - out-of-range attribute indexes report `OAKCOMMON_E_NOT_FOUND`;
//! - a malformed document still yields a usable handle whose `has_error`
//! flag reads back 1 and whose navigation reports no start elements.
use std::ffi::{c_char, CString};
use oakcommon::error::{OAKCOMMON_E_INVALID, OAKCOMMON_E_NOT_FOUND, OAKCOMMON_OK};
use oakcommon::ffi::xmlutils::*;
use oakcommon::handle::{CHandle, OAKCOMMON_ABI_VERSION};
/// Shared sample document: two attributes on the root plus a nested text
/// element. The writer test sequence reproduces this exact document.
const DOC: &str = r#"<root a="1" b="two"><child>text here</child></root>"#;
/// Convert a string slice to a NUL-terminated C string for FFI inputs.
fn to_cstring(s: &str) -> CString {
CString::new(s).expect("test string must not contain NUL")
}
/// Cheap struct copy: `CHandle` is neither `Clone` nor `Copy`, but every
/// getter takes it by value. Rebuilding from the same fields duplicates
/// only the handle value — the box stays alive as long as the original
/// handle lives, and the getters never release.
fn dup(h: &CHandle) -> CHandle {
CHandle {
ctx: h.ctx,
addref: h.addref,
release: h.release,
abi_version: h.abi_version,
}
}
/// Drive a two-stage string getter against the C++ `copy_string`
/// convention: a null-buffer size query, a short buffer that must stay
/// untouched (no truncation), an exact-fit copy with its NUL, and an
/// oversized copy with the tail untouched.
fn assert_two_stage_getter(getter: impl Fn(*mut c_char, i32) -> i32, expected: &str) {
let required = (expected.len() + 1) as i32;
// Size query: a null buffer returns the required size, NUL included.
assert_eq!(getter(std::ptr::null_mut(), 0), required);
// Short buffer: too small, so nothing is written to it.
let short_size = (required - 1).max(0);
let mut short = vec![0xABu8; short_size as usize];
assert_eq!(
getter(short.as_mut_ptr() as *mut c_char, short_size),
required
);
assert!(
short.iter().all(|&b| b == 0xAB),
"short buffer must stay untouched"
);
// Exact fit: payload followed by a NUL.
let mut exact = vec![0xCDu8; required as usize];
assert_eq!(
getter(exact.as_mut_ptr() as *mut c_char, required),
required
);
assert_eq!(&exact[..expected.len()], expected.as_bytes());
assert_eq!(exact[expected.len()], 0);
// Oversized: payload and NUL written, tail left as initialized.
let mut big = vec![0u8; (required + 8) as usize];
assert_eq!(
getter(big.as_mut_ptr() as *mut c_char, required + 8),
required
);
assert_eq!(&big[..expected.len()], expected.as_bytes());
assert_eq!(big[expected.len()], 0);
assert!(big[(required + 1) as usize..].iter().all(|&b| b == 0));
}
// ---- Reader: handle lifecycle ----
/// `init` with a null data pointer yields an empty handle.
#[test]
fn init_returns_null_handle_for_null_data() {
let h = oakcommon_xml_reader_init(std::ptr::null());
assert!(h.is_null());
assert!(h.ctx.is_null());
assert!(h.addref.is_none());
assert!(h.release.is_none());
}
/// `init` over valid data yields a stamped, non-empty handle.
#[test]
fn init_creates_stamped_handle() {
let h = oakcommon_xml_reader_init(to_cstring(DOC).as_ptr());
assert!(!h.is_null());
assert_eq!(h.abi_version, OAKCOMMON_ABI_VERSION);
assert!(h.addref.is_some());
assert!(h.release.is_some());
}
/// `free` nullifies the handle, is idempotent, and tolerates a null
/// pointer.
#[test]
fn free_nullifies_and_is_idempotent() {
let mut h = oakcommon_xml_reader_init(to_cstring(DOC).as_ptr());
assert!(!h.is_null());
oakcommon_xml_reader_free(&mut h);
assert!(h.is_null());
// A second free of the now-empty handle is safe.
oakcommon_xml_reader_free(&mut h);
assert!(h.is_null());
// Freeing a null pointer is safe.
oakcommon_xml_reader_free(std::ptr::null_mut());
}
// ---- Reader: navigation ----
/// `read_next_start_element` writes 1 while a start element is found and
/// 0 once the document is exhausted (CPP-PARITY: an end element and the
/// end of the document both report 0, not an error).
#[test]
fn read_next_start_element_reports_found() {
let r = oakcommon_xml_reader_init(to_cstring(DOC).as_ptr());
let mut found = -1i32;
assert_eq!(
oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found),
OAKCOMMON_OK
);
assert_eq!(found, 1); // root
assert_eq!(
oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found),
OAKCOMMON_OK
);
assert_eq!(found, 1); // child
assert_eq!(
oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found),
OAKCOMMON_OK
);
assert_eq!(found, 0); // child's end element
assert_eq!(
oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found),
OAKCOMMON_OK
);
assert_eq!(found, 0); // root's end element
}
/// A null reader handle or a null `found` out-param is `E_INVALID`.
#[test]
fn read_next_start_element_rejects_null_args() {
let r = oakcommon_xml_reader_init(to_cstring(DOC).as_ptr());
let mut found = 0i32;
assert_eq!(
oakcommon_xml_reader_read_next_start_element(CHandle::null(), &mut found),
OAKCOMMON_E_INVALID
);
assert_eq!(
oakcommon_xml_reader_read_next_start_element(dup(&r), std::ptr::null_mut()),
OAKCOMMON_E_INVALID
);
}
// ---- Reader: two-stage string getters ----
/// `name` is a two-stage getter over the current element's name; a null
/// handle is `E_INVALID`.
#[test]
fn name_two_stage_getter() {
let r = oakcommon_xml_reader_init(to_cstring(DOC).as_ptr());
let mut found = 0i32;
assert_eq!(
oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found),
OAKCOMMON_OK
);
assert_eq!(found, 1);
assert_two_stage_getter(
|buf, size| oakcommon_xml_reader_name(dup(&r), buf, size),
"root",
);
assert_eq!(
oakcommon_xml_reader_name(CHandle::null(), std::ptr::null_mut(), 0),
OAKCOMMON_E_INVALID
);
}
/// Before any token is read the name is the empty string (size 1).
#[test]
fn name_is_empty_before_any_read() {
let r = oakcommon_xml_reader_init(to_cstring(DOC).as_ptr());
assert_eq!(
oakcommon_xml_reader_name(dup(&r), std::ptr::null_mut(), 0),
1
);
}
/// `read_element_text` is a two-stage getter and caches its result, so a
/// second read returns the same text even though the stream was consumed
/// by the first (CPP-PARITY with the C++ `XmlReaderState::cached_text`).
#[test]
fn read_element_text_two_stage_with_cache() {
let r = oakcommon_xml_reader_init(to_cstring(DOC).as_ptr());
let mut found = 0i32;
assert_eq!(
oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found),
OAKCOMMON_OK
);
assert_eq!(
oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found),
OAKCOMMON_OK
);
assert_eq!(found, 1);
assert_two_stage_getter(
|buf, size| oakcommon_xml_reader_read_element_text(dup(&r), buf, size),
"text here",
);
assert_two_stage_getter(
|buf, size| oakcommon_xml_reader_read_element_text(dup(&r), buf, size),
"text here",
);
}
/// When not on a start element the text is empty (CPP-PARITY: the C++
/// reader returns an empty string).
#[test]
fn read_element_text_not_on_start_element_is_empty() {
let r = oakcommon_xml_reader_init(to_cstring(DOC).as_ptr());
let mut found = 0i32;
assert_eq!(
oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found),
OAKCOMMON_OK
);
assert_eq!(
oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found),
OAKCOMMON_OK
);
assert_eq!(
oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found),
OAKCOMMON_OK
);
assert_eq!(found, 0);
assert_eq!(
oakcommon_xml_reader_read_element_text(dup(&r), std::ptr::null_mut(), 0),
1
);
assert_eq!(
oakcommon_xml_reader_read_element_text(CHandle::null(), std::ptr::null_mut(), 0),
OAKCOMMON_E_INVALID
);
}
/// `skip_current_element` consumes the current element and its subtree; a
/// null handle is `E_INVALID`.
#[test]
fn skip_current_element_skips_subtree() {
let r = oakcommon_xml_reader_init(to_cstring(DOC).as_ptr());
let mut found = 0i32;
assert_eq!(
oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found),
OAKCOMMON_OK
);
assert_eq!(found, 1);
assert_eq!(
oakcommon_xml_reader_skip_current_element(dup(&r)),
OAKCOMMON_OK
);
assert_eq!(
oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found),
OAKCOMMON_OK
);
assert_eq!(found, 0);
assert_eq!(
oakcommon_xml_reader_skip_current_element(CHandle::null()),
OAKCOMMON_E_INVALID
);
}
// ---- Reader: attributes ----
/// `attribute_count` reports 0 before any token and the real count on a
/// start element; a null handle or a null count out-param is `E_INVALID`.
#[test]
fn attribute_count_reports_attributes() {
let r = oakcommon_xml_reader_init(to_cstring(DOC).as_ptr());
let mut count = -1i32;
assert_eq!(
oakcommon_xml_reader_attribute_count(dup(&r), &mut count),
OAKCOMMON_OK
);
assert_eq!(count, 0);
let mut found = 0i32;
assert_eq!(
oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found),
OAKCOMMON_OK
);
assert_eq!(found, 1);
assert_eq!(
oakcommon_xml_reader_attribute_count(dup(&r), &mut count),
OAKCOMMON_OK
);
assert_eq!(count, 2);
assert_eq!(
oakcommon_xml_reader_attribute_count(CHandle::null(), &mut count),
OAKCOMMON_E_INVALID
);
assert_eq!(
oakcommon_xml_reader_attribute_count(dup(&r), std::ptr::null_mut()),
OAKCOMMON_E_INVALID
);
}
/// `attribute_name` / `attribute_value` are two-stage getters over the
/// attributes of the current start element, in document order.
#[test]
fn attribute_name_and_value_two_stage() {
let r = oakcommon_xml_reader_init(to_cstring(DOC).as_ptr());
let mut found = 0i32;
assert_eq!(
oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found),
OAKCOMMON_OK
);
assert_eq!(found, 1);
assert_two_stage_getter(
|buf, size| oakcommon_xml_reader_attribute_name(dup(&r), 0, buf, size),
"a",
);
assert_two_stage_getter(
|buf, size| oakcommon_xml_reader_attribute_value(dup(&r), 0, buf, size),
"1",
);
assert_two_stage_getter(
|buf, size| oakcommon_xml_reader_attribute_name(dup(&r), 1, buf, size),
"b",
);
assert_two_stage_getter(
|buf, size| oakcommon_xml_reader_attribute_value(dup(&r), 1, buf, size),
"two",
);
}
/// An out-of-range attribute index is `E_NOT_FOUND` (CPP-PARITY), while a
/// null handle is `E_INVALID`.
#[test]
fn attribute_out_of_range_is_not_found() {
let r = oakcommon_xml_reader_init(to_cstring(DOC).as_ptr());
let mut found = 0i32;
assert_eq!(
oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found),
OAKCOMMON_OK
);
assert_eq!(found, 1);
assert_eq!(
oakcommon_xml_reader_attribute_name(dup(&r), 2, std::ptr::null_mut(), 0),
OAKCOMMON_E_NOT_FOUND
);
assert_eq!(
oakcommon_xml_reader_attribute_name(dup(&r), -1, std::ptr::null_mut(), 0),
OAKCOMMON_E_NOT_FOUND
);
assert_eq!(
oakcommon_xml_reader_attribute_value(dup(&r), 2, std::ptr::null_mut(), 0),
OAKCOMMON_E_NOT_FOUND
);
assert_eq!(
oakcommon_xml_reader_attribute_name(CHandle::null(), 0, std::ptr::null_mut(), 0),
OAKCOMMON_E_INVALID
);
}
// ---- Reader: error reporting ----
/// `has_error` is 0 for a well-formed document and 1 for a malformed one;
/// a malformed document still navigates safely and reports no start
/// elements (CPP-PARITY: parse failures surface lazily through
/// `has_error`, never at init time).
#[test]
fn has_error_flags_malformed_documents() {
let mut err = -1i32;
let r = oakcommon_xml_reader_init(to_cstring(DOC).as_ptr());
assert_eq!(
oakcommon_xml_reader_has_error(dup(&r), &mut err),
OAKCOMMON_OK
);
assert_eq!(err, 0);
let bad = oakcommon_xml_reader_init(to_cstring("<a></b>").as_ptr());
assert!(!bad.is_null());
assert_eq!(
oakcommon_xml_reader_has_error(dup(&bad), &mut err),
OAKCOMMON_OK
);
assert_eq!(err, 1);
let mut found = -1i32;
assert_eq!(
oakcommon_xml_reader_read_next_start_element(dup(&bad), &mut found),
OAKCOMMON_OK
);
assert_eq!(found, 0);
let empty = oakcommon_xml_reader_init(to_cstring("").as_ptr());
assert!(!empty.is_null());
assert_eq!(
oakcommon_xml_reader_has_error(dup(&empty), &mut err),
OAKCOMMON_OK
);
assert_eq!(err, 1);
assert_eq!(
oakcommon_xml_reader_has_error(CHandle::null(), &mut err),
OAKCOMMON_E_INVALID
);
assert_eq!(
oakcommon_xml_reader_has_error(dup(&r), std::ptr::null_mut()),
OAKCOMMON_E_INVALID
);
}
// ---- Writer: handle lifecycle ----
/// `writer_init` yields a stamped handle; `free` nullifies it, is
/// idempotent, and tolerates a null pointer.
#[test]
fn writer_init_free_and_idempotent() {
let mut w = oakcommon_xml_writer_init();
assert!(!w.is_null());
assert_eq!(w.abi_version, OAKCOMMON_ABI_VERSION);
assert!(w.addref.is_some());
assert!(w.release.is_some());
oakcommon_xml_writer_free(&mut w);
assert!(w.is_null());
oakcommon_xml_writer_free(&mut w);
assert!(w.is_null());
oakcommon_xml_writer_free(std::ptr::null_mut());
}
// ---- Writer: operations ----
/// Every writer export rejects a null handle with `E_INVALID`, and the
/// string-taking exports reject null strings.
#[test]
fn writer_rejects_null_arguments() {
let w = oakcommon_xml_writer_init();
assert_eq!(
oakcommon_xml_writer_write_start_element(CHandle::null(), to_cstring("a").as_ptr()),
OAKCOMMON_E_INVALID
);
assert_eq!(
oakcommon_xml_writer_write_attribute(
CHandle::null(),
to_cstring("a").as_ptr(),
to_cstring("b").as_ptr(),
),
OAKCOMMON_E_INVALID
);
assert_eq!(
oakcommon_xml_writer_write_characters(CHandle::null(), to_cstring("x").as_ptr()),
OAKCOMMON_E_INVALID
);
assert_eq!(
oakcommon_xml_writer_write_text_element(
CHandle::null(),
to_cstring("a").as_ptr(),
to_cstring("b").as_ptr(),
),
OAKCOMMON_E_INVALID
);
assert_eq!(
oakcommon_xml_writer_write_end_element(CHandle::null()),
OAKCOMMON_E_INVALID
);
assert_eq!(
oakcommon_xml_writer_write_end_document(CHandle::null()),
OAKCOMMON_E_INVALID
);
assert_eq!(
oakcommon_xml_writer_output(CHandle::null(), std::ptr::null_mut(), 0),
OAKCOMMON_E_INVALID
);
assert_eq!(
oakcommon_xml_writer_write_start_element(dup(&w), std::ptr::null()),
OAKCOMMON_E_INVALID
);
assert_eq!(
oakcommon_xml_writer_write_attribute(dup(&w), std::ptr::null(), to_cstring("b").as_ptr()),
OAKCOMMON_E_INVALID
);
assert_eq!(
oakcommon_xml_writer_write_attribute(dup(&w), to_cstring("a").as_ptr(), std::ptr::null()),
OAKCOMMON_E_INVALID
);
assert_eq!(
oakcommon_xml_writer_write_characters(dup(&w), std::ptr::null()),
OAKCOMMON_E_INVALID
);
assert_eq!(
oakcommon_xml_writer_write_text_element(
dup(&w),
std::ptr::null(),
to_cstring("b").as_ptr()
),
OAKCOMMON_E_INVALID
);
assert_eq!(
oakcommon_xml_writer_write_text_element(
dup(&w),
to_cstring("a").as_ptr(),
std::ptr::null()
),
OAKCOMMON_E_INVALID
);
}
/// The writer sequence builds the exact sample document, and `output` is a
/// two-stage getter over it.
#[test]
fn writer_builds_document_and_output_two_stage() {
let w = oakcommon_xml_writer_init();
assert_eq!(
oakcommon_xml_writer_write_start_element(dup(&w), to_cstring("root").as_ptr()),
OAKCOMMON_OK
);
assert_eq!(
oakcommon_xml_writer_write_attribute(
dup(&w),
to_cstring("a").as_ptr(),
to_cstring("1").as_ptr()
),
OAKCOMMON_OK
);
assert_eq!(
oakcommon_xml_writer_write_attribute(
dup(&w),
to_cstring("b").as_ptr(),
to_cstring("two").as_ptr()
),
OAKCOMMON_OK
);
assert_eq!(
oakcommon_xml_writer_write_text_element(
dup(&w),
to_cstring("child").as_ptr(),
to_cstring("text here").as_ptr(),
),
OAKCOMMON_OK
);
assert_eq!(
oakcommon_xml_writer_write_end_element(dup(&w)),
OAKCOMMON_OK
);
assert_eq!(
oakcommon_xml_writer_write_end_document(dup(&w)),
OAKCOMMON_OK
);
assert_two_stage_getter(
|buf, size| oakcommon_xml_writer_output(dup(&w), buf, size),
DOC,
);
}
/// The writer's output round-trips through the reader.
#[test]
fn writer_output_round_trips_through_reader() {
let w = oakcommon_xml_writer_init();
assert_eq!(
oakcommon_xml_writer_write_start_element(dup(&w), to_cstring("root").as_ptr()),
OAKCOMMON_OK
);
assert_eq!(
oakcommon_xml_writer_write_attribute(
dup(&w),
to_cstring("a").as_ptr(),
to_cstring("1").as_ptr()
),
OAKCOMMON_OK
);
assert_eq!(
oakcommon_xml_writer_write_attribute(
dup(&w),
to_cstring("b").as_ptr(),
to_cstring("two").as_ptr()
),
OAKCOMMON_OK
);
assert_eq!(
oakcommon_xml_writer_write_text_element(
dup(&w),
to_cstring("child").as_ptr(),
to_cstring("text here").as_ptr(),
),
OAKCOMMON_OK
);
assert_eq!(
oakcommon_xml_writer_write_end_element(dup(&w)),
OAKCOMMON_OK
);
let mut out = vec![0u8; 64];
let needed = oakcommon_xml_writer_output(dup(&w), out.as_mut_ptr() as *mut c_char, 64);
assert_eq!(needed, (DOC.len() + 1) as i32);
assert_eq!(&out[..DOC.len()], DOC.as_bytes());
assert_eq!(out[DOC.len()], 0);
let r = oakcommon_xml_reader_init(out.as_ptr() as *const c_char);
assert!(!r.is_null());
let mut found = 0i32;
assert_eq!(
oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found),
OAKCOMMON_OK
);
assert_eq!(found, 1);
assert_two_stage_getter(
|buf, size| oakcommon_xml_reader_name(dup(&r), buf, size),
"root",
);
assert_eq!(
oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found),
OAKCOMMON_OK
);
assert_eq!(found, 1);
assert_two_stage_getter(
|buf, size| oakcommon_xml_reader_read_element_text(dup(&r), buf, size),
"text here",
);
assert_eq!(
oakcommon_xml_reader_read_next_start_element(dup(&r), &mut found),
OAKCOMMON_OK
);
assert_eq!(found, 0);
}
/// Documented no-op writer operations (attribute without an open start
/// tag, end element on an empty stack, end document with nothing open)
/// still return `OK`.
#[test]
fn writer_noop_operations_return_ok() {
let w = oakcommon_xml_writer_init();
assert_eq!(
oakcommon_xml_writer_write_attribute(
dup(&w),
to_cstring("a").as_ptr(),
to_cstring("b").as_ptr()
),
OAKCOMMON_OK
);
assert_eq!(
oakcommon_xml_writer_write_end_element(dup(&w)),
OAKCOMMON_OK
);
assert_eq!(
oakcommon_xml_writer_write_end_document(dup(&w)),
OAKCOMMON_OK
);
assert_eq!(
oakcommon_xml_writer_write_characters(dup(&w), to_cstring("x").as_ptr()),
OAKCOMMON_OK
);
assert_two_stage_getter(
|buf, size| oakcommon_xml_writer_output(dup(&w), buf, size),
"x",
);
}
/// An empty element with attributes serializes as `<a k="v"/>`.
#[test]
fn writer_self_closing_empty_element() {
let w = oakcommon_xml_writer_init();
assert_eq!(
oakcommon_xml_writer_write_start_element(dup(&w), to_cstring("a").as_ptr()),
OAKCOMMON_OK
);
assert_eq!(
oakcommon_xml_writer_write_attribute(
dup(&w),
to_cstring("k").as_ptr(),
to_cstring("v").as_ptr()
),
OAKCOMMON_OK
);
assert_eq!(
oakcommon_xml_writer_write_end_element(dup(&w)),
OAKCOMMON_OK
);
assert_two_stage_getter(
|buf, size| oakcommon_xml_writer_output(dup(&w), buf, size),
r#"<a k="v"/>"#,
);
}
/// Text and attribute values are escaped for the five predefined XML
/// entities.
#[test]
fn writer_escapes_text_and_attributes() {
let w = oakcommon_xml_writer_init();
assert_eq!(
oakcommon_xml_writer_write_text_element(
dup(&w),
to_cstring("e").as_ptr(),
to_cstring("hi & bye <there>").as_ptr(),
),
OAKCOMMON_OK
);
assert_eq!(
oakcommon_xml_writer_write_start_element(dup(&w), to_cstring("a").as_ptr()),
OAKCOMMON_OK
);
assert_eq!(
oakcommon_xml_writer_write_attribute(
dup(&w),
to_cstring("q").as_ptr(),
to_cstring("x\"y&z").as_ptr(),
),
OAKCOMMON_OK
);
assert_eq!(
oakcommon_xml_writer_write_end_element(dup(&w)),
OAKCOMMON_OK
);
assert_two_stage_getter(
|buf, size| oakcommon_xml_writer_output(dup(&w), buf, size),
r#"<e>hi &amp; bye &lt;there&gt;</e><a q="x&quot;y&amp;z"/>"#,
);
}
// ---- error / error_abi ----
/// The constants the xmlutils exports return are defined in `crate::error`
/// and match `include/common/error.h` (the `ffi::error` / `ffi::error_abi`
/// modules only point here). The full set is pinned in `contract.rs`.
#[test]
fn error_constants_returned_by_xmlutils_match_header() {
assert_eq!(OAKCOMMON_OK, 0);
assert_eq!(OAKCOMMON_E_INVALID, -10001);
assert_eq!(OAKCOMMON_E_NOT_FOUND, -10004);
}