Skip to main content

subversion/
error.rs

1use subversion_sys::svn_error_t;
2
3/// Categorizes the kind of error that occurred based on SVN error code ranges.
4///
5/// This enum provides a way to programmatically distinguish between different
6/// error categories without parsing error messages. The categories correspond
7/// to SVN's internal error code organization.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
9pub enum ErrorCategory {
10    /// Malformed input or argument errors (125000-129999)
11    BadInput,
12    /// XML parsing/generation errors (130000-134999)
13    Xml,
14    /// I/O errors (135000-139999)
15    Io,
16    /// Stream-related errors (140000-144999)
17    Stream,
18    /// Node (file/directory) errors (145000-149999)
19    Node,
20    /// Entry-related errors (150000-154999)
21    Entry,
22    /// Working copy errors (155000-159999)
23    WorkingCopy,
24    /// Filesystem backend errors (160000-164999)
25    Filesystem,
26    /// Repository errors (165000-169999)
27    Repository,
28    /// Repository access layer errors (170000-174999)
29    RepositoryAccess,
30    /// DAV protocol errors (175000-179999)
31    RaDav,
32    /// Local repository access errors (180000-184999)
33    RaLocal,
34    /// Diff algorithm errors (185000-189999)
35    Svndiff,
36    /// Apache module errors (190000-194999)
37    ApacheMod,
38    /// Client operation errors (195000-199999)
39    Client,
40    /// Miscellaneous errors including cancellation (200000-204999)
41    Misc,
42    /// Command-line client errors (205000-209999)
43    CommandLine,
44    /// SVN protocol errors (210000-214999)
45    RaSvn,
46    /// Authentication errors (215000-219999)
47    Authentication,
48    /// Authorization errors (220000-224999)
49    Authorization,
50    /// Diff operation errors (225000-229999)
51    Diff,
52    /// Serf/HTTP errors (230000-234999)
53    RaSerf,
54    /// Internal malfunction errors (235000-239999)
55    Malfunction,
56    /// X.509 certificate errors (240000-244999)
57    X509,
58    /// Unknown or APR error
59    Other,
60}
61
62// Errors are a bit special; they own their own pool, so don't need to use PooledPtr
63/// Represents a Subversion error.
64///
65/// SVN errors can form chains where each error points to a child error that provides
66/// more context. The lifetime parameter tracks ownership of the error chain:
67///
68/// - `Error<'static>` owns its error pointer and will free the entire chain on drop
69/// - `Error<'a>` borrows from another error's chain and shares the pointer without owning it
70///
71/// # Examples
72///
73/// Creating a simple error:
74/// ```
75/// use subversion::Error;
76///
77/// let err = Error::from_message("Something went wrong");
78/// ```
79///
80/// Checking error details:
81/// ```
82/// # use subversion::Error;
83/// # let err = Error::from_message("Something went wrong");
84/// println!("Error code: {}", err.code());
85/// println!("Error message: {}", err.message());
86/// println!("Error category: {:?}", err.category());
87/// ```
88///
89/// Traversing an error chain:
90/// ```
91/// # use subversion::Error;
92/// # let err = Error::from_message("Something went wrong");
93/// let mut current = Some(&err);
94/// while let Some(e) = current {
95///     println!("Error: {}", e.message());
96///     current = e.child().as_ref();
97/// }
98/// ```
99pub struct Error<'a> {
100    ptr: *mut svn_error_t,
101    owns_ptr: bool,
102    _phantom: std::marker::PhantomData<&'a ()>,
103}
104
105unsafe impl Send for Error<'_> {}
106
107impl Error<'static> {
108    /// Creates a new error with the given status, optional child error, and message.
109    pub fn new(status: apr::Status, child: Option<Error<'static>>, msg: &str) -> Self {
110        let msg = std::ffi::CString::new(msg).unwrap();
111        let child = child
112            .map(|mut e| unsafe { e.detach() })
113            .unwrap_or(std::ptr::null_mut());
114        let err = unsafe { subversion_sys::svn_error_create(status as i32, child, msg.as_ptr()) };
115        Self {
116            ptr: err,
117            owns_ptr: true,
118            _phantom: std::marker::PhantomData,
119        }
120    }
121
122    /// Creates a new error with a raw APR/SVN status code.
123    ///
124    /// Use this when you need SVN-specific error codes (like `SVN_ERR_CANCELLED`)
125    /// that cannot be represented by `apr::Status`.
126    pub fn with_raw_status(status: i32, child: Option<Error<'static>>, msg: &str) -> Self {
127        let msg = std::ffi::CString::new(msg).unwrap();
128        let child = child
129            .map(|mut e| unsafe { e.detach() })
130            .unwrap_or(std::ptr::null_mut());
131        let err = unsafe { subversion_sys::svn_error_create(status, child, msg.as_ptr()) };
132        Self {
133            ptr: err,
134            owns_ptr: true,
135            _phantom: std::marker::PhantomData,
136        }
137    }
138
139    /// Creates a new error from a string message.
140    pub fn from_message(msg: &str) -> Error<'static> {
141        Self::new(apr::Status::from(1), None, msg)
142    }
143
144    /// Creates an error from a raw SVN error pointer, or Ok if null.
145    pub fn from_raw(err: *mut svn_error_t) -> Result<(), Error<'static>> {
146        if err.is_null() {
147            Ok(())
148        } else {
149            Err(Error {
150                ptr: err,
151                owns_ptr: true,
152                _phantom: std::marker::PhantomData,
153            })
154        }
155    }
156}
157
158impl<'a> Error<'a> {
159    /// Wraps a raw SVN error pointer without taking ownership.
160    ///
161    /// The caller remains responsible for freeing `err`; the returned `Error`
162    /// will NOT call `svn_error_clear` on drop.
163    ///
164    /// # Safety
165    ///
166    /// `err` must be a valid, non-null pointer that outlives the returned `Error<'a>`.
167    pub(crate) unsafe fn from_ptr_borrowed(err: *mut svn_error_t) -> Error<'a> {
168        debug_assert!(!err.is_null());
169        Error {
170            ptr: err,
171            owns_ptr: false,
172            _phantom: std::marker::PhantomData,
173        }
174    }
175}
176
177impl<'a> Error<'a> {
178    /// Gets the APR error status code.
179    ///
180    /// Note: SVN-specific error codes (like `SVN_ERR_CANCELLED`) are mapped to
181    /// `apr::Status::General` because they fall outside the standard APR status range.
182    /// Use [`raw_apr_err()`](Self::raw_apr_err) when you need to distinguish SVN error codes.
183    pub fn apr_err(&self) -> apr::Status {
184        unsafe { (*self.ptr).apr_err }.into()
185    }
186
187    /// Gets the raw APR/SVN error status code as an integer.
188    ///
189    /// Unlike [`apr_err()`](Self::apr_err), this preserves the full error code
190    /// including SVN-specific codes (e.g. `SVN_ERR_CANCELLED = 200015`).
191    pub fn raw_apr_err(&self) -> i32 {
192        unsafe { (*self.ptr).apr_err }
193    }
194
195    /// Gets the mutable raw pointer to the error.
196    pub fn as_mut_ptr(&mut self) -> *mut svn_error_t {
197        self.ptr
198    }
199
200    /// Gets the raw pointer to the error.
201    pub fn as_ptr(&self) -> *const svn_error_t {
202        self.ptr
203    }
204
205    /// Gets the line number where the error occurred.
206    pub fn line(&self) -> i64 {
207        unsafe { (*self.ptr).line.into() }
208    }
209
210    /// Gets the file name where the error occurred.
211    pub fn file(&self) -> Option<&str> {
212        unsafe {
213            let file = (*self.ptr).file;
214            if file.is_null() {
215                None
216            } else {
217                Some(std::ffi::CStr::from_ptr(file).to_str().unwrap())
218            }
219        }
220    }
221
222    /// Gets the file and line location where the error occurred.
223    pub fn location(&self) -> Option<(&str, i64)> {
224        self.file().map(|f| (f, self.line()))
225    }
226
227    /// Gets the child error, if any.
228    ///
229    /// The returned error has the same lifetime as this error (both are part of the same error chain).
230    /// The returned error does not own its pointer - the parent error owns the entire chain.
231    pub fn child(&self) -> Option<Error<'a>> {
232        unsafe {
233            let child = (*self.ptr).child;
234            if child.is_null() {
235                None
236            } else {
237                Some(Error {
238                    ptr: child,
239                    owns_ptr: false,
240                    _phantom: std::marker::PhantomData,
241                })
242            }
243        }
244    }
245
246    /// Gets the error message.
247    pub fn message(&self) -> Option<&str> {
248        unsafe {
249            let message = (*self.ptr).message;
250            if message.is_null() {
251                None
252            } else {
253                Some(std::ffi::CStr::from_ptr(message).to_str().unwrap())
254            }
255        }
256    }
257
258    /// Finds an error in the chain with the given status code.
259    ///
260    /// The returned error borrows from this error's chain and does not own its pointer.
261    pub fn find_cause(&self, status: apr::Status) -> Option<Error<'a>> {
262        unsafe {
263            let err = subversion_sys::svn_error_find_cause(self.ptr, status as i32);
264            if err.is_null() {
265                None
266            } else {
267                Some(Error {
268                    ptr: err,
269                    owns_ptr: false,
270                    _phantom: std::marker::PhantomData,
271                })
272            }
273        }
274    }
275
276    /// Removes tracing information from the error.
277    ///
278    /// The returned error borrows from this error's chain and does not own its pointer.
279    pub fn purge_tracing(&self) -> Error<'_> {
280        unsafe {
281            Error {
282                ptr: subversion_sys::svn_error_purge_tracing(self.ptr),
283                owns_ptr: false,
284                _phantom: std::marker::PhantomData,
285            }
286        }
287    }
288
289    /// Detaches the error, returning the raw pointer and preventing cleanup.
290    ///
291    /// # Safety
292    ///
293    /// The caller assumes responsibility for managing the returned pointer's lifetime
294    /// and ensuring it is properly freed using Subversion's error handling functions.
295    pub unsafe fn detach(&mut self) -> *mut svn_error_t {
296        let err = self.ptr;
297        self.ptr = std::ptr::null_mut();
298        err
299    }
300
301    /// Converts the error into a raw pointer, consuming self without cleanup.
302    ///
303    /// # Safety
304    ///
305    /// The caller assumes responsibility for managing the returned pointer's lifetime
306    /// and ensuring it is properly freed using Subversion's error handling functions.
307    pub unsafe fn into_raw(self) -> *mut svn_error_t {
308        let err = self.ptr;
309        std::mem::forget(self);
310        err
311    }
312
313    /// Gets the best available error message from the error chain.
314    pub fn best_message(&self) -> String {
315        let mut buf = [0; 1024];
316        unsafe {
317            let ret = subversion_sys::svn_err_best_message(self.ptr, buf.as_mut_ptr(), buf.len());
318            std::ffi::CStr::from_ptr(ret).to_string_lossy().into_owned()
319        }
320    }
321
322    /// Collect all messages from the error chain
323    pub fn full_message(&self) -> String {
324        let mut messages = Vec::new();
325        let mut current = self.ptr;
326
327        unsafe {
328            while !current.is_null() {
329                let msg = (*current).message;
330                if !msg.is_null() {
331                    let msg_str = std::ffi::CStr::from_ptr(msg).to_string_lossy();
332                    if !msg_str.is_empty() {
333                        messages.push(msg_str.into_owned());
334                    }
335                }
336                current = (*current).child;
337            }
338        }
339
340        if messages.is_empty() {
341            self.best_message()
342        } else {
343            messages.join(": ")
344        }
345    }
346
347    /// Returns the error category based on the SVN error code.
348    ///
349    /// This allows programmatic handling of different error types without
350    /// parsing error messages.
351    ///
352    /// # Example
353    ///
354    /// ```no_run
355    /// # use subversion::error::ErrorCategory;
356    /// # fn example() -> Result<(), subversion::Error> {
357    /// let mut ctx = subversion::client::Context::new()?;
358    /// match ctx.checkout("https://svn.example.com/repo", "/tmp/wc", None, true) {
359    ///     Ok(_) => println!("Success"),
360    ///     Err(e) => match e.category() {
361    ///         ErrorCategory::Authentication => println!("Authentication required"),
362    ///         ErrorCategory::Authorization => println!("Permission denied"),
363    ///         ErrorCategory::Io => println!("I/O error occurred"),
364    ///         _ => println!("Other error: {}", e),
365    ///     }
366    /// }
367    /// # Ok(())
368    /// # }
369    /// ```
370    pub fn category(&self) -> ErrorCategory {
371        use subversion_sys::*;
372        // Get the raw apr_status_t value directly, not the apr::Status enum discriminant
373        let code = unsafe { (*self.ptr).apr_err as u32 };
374        let category_size = SVN_ERR_CATEGORY_SIZE;
375
376        match code {
377            c if c >= SVN_ERR_BAD_CATEGORY_START
378                && c < SVN_ERR_BAD_CATEGORY_START + category_size =>
379            {
380                ErrorCategory::BadInput
381            }
382            c if c >= SVN_ERR_XML_CATEGORY_START
383                && c < SVN_ERR_XML_CATEGORY_START + category_size =>
384            {
385                ErrorCategory::Xml
386            }
387            c if c >= SVN_ERR_IO_CATEGORY_START
388                && c < SVN_ERR_IO_CATEGORY_START + category_size =>
389            {
390                ErrorCategory::Io
391            }
392            c if c >= SVN_ERR_STREAM_CATEGORY_START
393                && c < SVN_ERR_STREAM_CATEGORY_START + category_size =>
394            {
395                ErrorCategory::Stream
396            }
397            c if c >= SVN_ERR_NODE_CATEGORY_START
398                && c < SVN_ERR_NODE_CATEGORY_START + category_size =>
399            {
400                ErrorCategory::Node
401            }
402            c if c >= SVN_ERR_ENTRY_CATEGORY_START
403                && c < SVN_ERR_ENTRY_CATEGORY_START + category_size =>
404            {
405                ErrorCategory::Entry
406            }
407            c if c >= SVN_ERR_WC_CATEGORY_START
408                && c < SVN_ERR_WC_CATEGORY_START + category_size =>
409            {
410                ErrorCategory::WorkingCopy
411            }
412            c if c >= SVN_ERR_FS_CATEGORY_START
413                && c < SVN_ERR_FS_CATEGORY_START + category_size =>
414            {
415                ErrorCategory::Filesystem
416            }
417            c if c >= SVN_ERR_REPOS_CATEGORY_START
418                && c < SVN_ERR_REPOS_CATEGORY_START + category_size =>
419            {
420                ErrorCategory::Repository
421            }
422            c if c >= SVN_ERR_RA_CATEGORY_START
423                && c < SVN_ERR_RA_CATEGORY_START + category_size =>
424            {
425                ErrorCategory::RepositoryAccess
426            }
427            c if c >= SVN_ERR_RA_DAV_CATEGORY_START
428                && c < SVN_ERR_RA_DAV_CATEGORY_START + category_size =>
429            {
430                ErrorCategory::RaDav
431            }
432            c if c >= SVN_ERR_RA_LOCAL_CATEGORY_START
433                && c < SVN_ERR_RA_LOCAL_CATEGORY_START + category_size =>
434            {
435                ErrorCategory::RaLocal
436            }
437            c if c >= SVN_ERR_SVNDIFF_CATEGORY_START
438                && c < SVN_ERR_SVNDIFF_CATEGORY_START + category_size =>
439            {
440                ErrorCategory::Svndiff
441            }
442            c if c >= SVN_ERR_APMOD_CATEGORY_START
443                && c < SVN_ERR_APMOD_CATEGORY_START + category_size =>
444            {
445                ErrorCategory::ApacheMod
446            }
447            c if c >= SVN_ERR_CLIENT_CATEGORY_START
448                && c < SVN_ERR_CLIENT_CATEGORY_START + category_size =>
449            {
450                ErrorCategory::Client
451            }
452            c if c >= SVN_ERR_MISC_CATEGORY_START
453                && c < SVN_ERR_MISC_CATEGORY_START + category_size =>
454            {
455                ErrorCategory::Misc
456            }
457            c if c >= SVN_ERR_CL_CATEGORY_START
458                && c < SVN_ERR_CL_CATEGORY_START + category_size =>
459            {
460                ErrorCategory::CommandLine
461            }
462            c if c >= SVN_ERR_RA_SVN_CATEGORY_START
463                && c < SVN_ERR_RA_SVN_CATEGORY_START + category_size =>
464            {
465                ErrorCategory::RaSvn
466            }
467            c if c >= SVN_ERR_AUTHN_CATEGORY_START
468                && c < SVN_ERR_AUTHN_CATEGORY_START + category_size =>
469            {
470                ErrorCategory::Authentication
471            }
472            c if c >= SVN_ERR_AUTHZ_CATEGORY_START
473                && c < SVN_ERR_AUTHZ_CATEGORY_START + category_size =>
474            {
475                ErrorCategory::Authorization
476            }
477            c if c >= SVN_ERR_DIFF_CATEGORY_START
478                && c < SVN_ERR_DIFF_CATEGORY_START + category_size =>
479            {
480                ErrorCategory::Diff
481            }
482            c if c >= SVN_ERR_RA_SERF_CATEGORY_START
483                && c < SVN_ERR_RA_SERF_CATEGORY_START + category_size =>
484            {
485                ErrorCategory::RaSerf
486            }
487            c if c >= SVN_ERR_MALFUNC_CATEGORY_START
488                && c < SVN_ERR_MALFUNC_CATEGORY_START + category_size =>
489            {
490                ErrorCategory::Malfunction
491            }
492            c if c >= SVN_ERR_X509_CATEGORY_START
493                && c < SVN_ERR_X509_CATEGORY_START + category_size =>
494            {
495                ErrorCategory::X509
496            }
497            _ => ErrorCategory::Other,
498        }
499    }
500}
501
502/// Gets the symbolic name for an error status code.
503pub fn symbolic_name(status: apr::Status) -> Option<&'static str> {
504    unsafe {
505        let name = subversion_sys::svn_error_symbolic_name(status as i32);
506        if name.is_null() {
507            None
508        } else {
509            Some(std::ffi::CStr::from_ptr(name).to_str().unwrap())
510        }
511    }
512}
513
514/// Gets a human-readable error string for a status code.
515pub fn strerror(status: apr::Status) -> Option<&'static str> {
516    let mut buf = [0; 1024];
517    unsafe {
518        let name = subversion_sys::svn_strerror(status as i32, buf.as_mut_ptr(), buf.len());
519        if name.is_null() {
520            None
521        } else {
522            Some(std::ffi::CStr::from_ptr(name).to_str().unwrap())
523        }
524    }
525}
526
527impl Clone for Error<'static> {
528    fn clone(&self) -> Self {
529        unsafe {
530            Error {
531                ptr: subversion_sys::svn_error_dup(self.ptr),
532                owns_ptr: true,
533                _phantom: std::marker::PhantomData,
534            }
535        }
536    }
537}
538
539impl Drop for Error<'_> {
540    fn drop(&mut self) {
541        // Only free if we own the pointer and it's non-null
542        if self.owns_ptr && !self.ptr.is_null() {
543            unsafe { subversion_sys::svn_error_clear(self.ptr) }
544        }
545    }
546}
547
548impl std::fmt::Debug for Error<'_> {
549    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
550        writeln!(
551            f,
552            "{}:{}: {}",
553            self.file().unwrap_or("<unspecified>"),
554            self.line(),
555            self.message().unwrap_or("<no message>")
556        )?;
557        let mut n = self.child();
558        while let Some(err) = n {
559            writeln!(
560                f,
561                "{}:{}: {}",
562                err.file().unwrap_or("<unspecified>"),
563                err.line(),
564                err.message().unwrap_or("<no message>")
565            )?;
566            n = err.child();
567        }
568        Ok(())
569    }
570}
571
572impl std::fmt::Display for Error<'_> {
573    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
574        write!(f, "{}", self.full_message())
575    }
576}
577
578impl std::error::Error for Error<'_> {}
579
580impl From<std::io::Error> for Error<'static> {
581    fn from(err: std::io::Error) -> Self {
582        Error::new(apr::Status::from(err.kind()), None, &err.to_string())
583    }
584}
585
586impl From<Error<'_>> for std::io::Error {
587    fn from(err: Error) -> Self {
588        let errno = err.apr_err().raw_os_error();
589        errno.map_or(
590            std::io::Error::other(err.message().unwrap_or("Unknown error")),
591            std::io::Error::from_raw_os_error,
592        )
593    }
594}
595
596impl From<std::ffi::NulError> for Error<'static> {
597    fn from(err: std::ffi::NulError) -> Self {
598        Error::from_message(&format!("Null byte in string: {}", err))
599    }
600}
601
602impl From<std::str::Utf8Error> for Error<'static> {
603    fn from(err: std::str::Utf8Error) -> Self {
604        Error::from_message(&format!("UTF-8 encoding error: {}", err))
605    }
606}
607
608#[cfg(test)]
609mod tests {
610    use super::*;
611
612    #[test]
613    fn test_error_chain_formatting() {
614        // Create a chain of errors
615        let child_err = Error::from_message("Child error");
616        let parent_err = Error::new(apr::Status::from(1), Some(child_err), "Parent error");
617
618        let full_msg = parent_err.full_message();
619        assert!(full_msg.contains("Parent error"));
620        assert!(full_msg.contains("Child error"));
621        assert!(full_msg.contains(": ")); // Check for separator
622    }
623
624    #[test]
625    fn test_single_error_message() {
626        let err = Error::from_message("Single error");
627        assert_eq!(err.message(), Some("Single error"));
628
629        let full_msg = err.full_message();
630        assert!(full_msg.contains("Single error"));
631    }
632
633    #[test]
634    fn test_error_display() {
635        let err = Error::from_message("Display test error");
636        let display_str = format!("{}", err);
637        assert!(display_str.contains("Display test error"));
638    }
639
640    #[test]
641    fn test_error_from_raw_null() {
642        Error::from_raw(std::ptr::null_mut()).unwrap();
643    }
644
645    #[test]
646    fn test_error_category() {
647        use subversion_sys::*;
648
649        // Test various error category codes - create errors directly via C API
650        let io_err_ptr = unsafe {
651            subversion_sys::svn_error_create(
652                SVN_ERR_IO_CATEGORY_START as i32,
653                std::ptr::null_mut(),
654                b"I/O error\0".as_ptr() as *const i8,
655            )
656        };
657        let io_err = Error {
658            ptr: io_err_ptr,
659            owns_ptr: true,
660            _phantom: std::marker::PhantomData,
661        };
662        assert_eq!(io_err.category(), ErrorCategory::Io);
663
664        let auth_err_ptr = unsafe {
665            subversion_sys::svn_error_create(
666                SVN_ERR_AUTHN_CATEGORY_START as i32,
667                std::ptr::null_mut(),
668                b"Auth error\0".as_ptr() as *const i8,
669            )
670        };
671        let auth_err = Error {
672            ptr: auth_err_ptr,
673            owns_ptr: true,
674            _phantom: std::marker::PhantomData,
675        };
676        assert_eq!(auth_err.category(), ErrorCategory::Authentication);
677
678        let authz_err_ptr = unsafe {
679            subversion_sys::svn_error_create(
680                SVN_ERR_AUTHZ_CATEGORY_START as i32,
681                std::ptr::null_mut(),
682                b"Authz error\0".as_ptr() as *const i8,
683            )
684        };
685        let authz_err = Error {
686            ptr: authz_err_ptr,
687            owns_ptr: true,
688            _phantom: std::marker::PhantomData,
689        };
690        assert_eq!(authz_err.category(), ErrorCategory::Authorization);
691
692        let wc_err_ptr = unsafe {
693            subversion_sys::svn_error_create(
694                SVN_ERR_WC_CATEGORY_START as i32,
695                std::ptr::null_mut(),
696                b"WC error\0".as_ptr() as *const i8,
697            )
698        };
699        let wc_err = Error {
700            ptr: wc_err_ptr,
701            owns_ptr: true,
702            _phantom: std::marker::PhantomData,
703        };
704        assert_eq!(wc_err.category(), ErrorCategory::WorkingCopy);
705
706        let repos_err_ptr = unsafe {
707            subversion_sys::svn_error_create(
708                SVN_ERR_REPOS_CATEGORY_START as i32,
709                std::ptr::null_mut(),
710                b"Repos error\0".as_ptr() as *const i8,
711            )
712        };
713        let repos_err = Error {
714            ptr: repos_err_ptr,
715            owns_ptr: true,
716            _phantom: std::marker::PhantomData,
717        };
718        assert_eq!(repos_err.category(), ErrorCategory::Repository);
719
720        let misc_err_ptr = unsafe {
721            subversion_sys::svn_error_create(
722                SVN_ERR_MISC_CATEGORY_START as i32,
723                std::ptr::null_mut(),
724                b"Misc error\0".as_ptr() as *const i8,
725            )
726        };
727        let misc_err = Error {
728            ptr: misc_err_ptr,
729            owns_ptr: true,
730            _phantom: std::marker::PhantomData,
731        };
732        assert_eq!(misc_err.category(), ErrorCategory::Misc);
733    }
734
735    #[test]
736    fn test_error_location_returns_value() {
737        // Test that Error::location() returns actual location info when available
738        use subversion_sys::*;
739
740        // Create an error with location information
741        // Use a static string to avoid memory management issues with CString
742        static TEST_FILE: &[u8] = b"test_file.c\0";
743
744        let err_ptr = unsafe {
745            let err = svn_error_create(
746                SVN_ERR_IO_CATEGORY_START as i32,
747                std::ptr::null_mut(),
748                b"Test error\0".as_ptr() as *const i8,
749            );
750            // SVN errors typically have file/line information set by internal macros
751            // We simulate this by setting them directly using a static string
752            (*err).file = TEST_FILE.as_ptr() as *const i8;
753            (*err).line = 42;
754            err
755        };
756
757        let err = Error {
758            ptr: err_ptr,
759            owns_ptr: true,
760            _phantom: std::marker::PhantomData,
761        };
762
763        // Verify location() returns the correct file and line
764        let location = err.location();
765        assert!(
766            location.is_some(),
767            "Error with file/line should have location"
768        );
769        let (file, line) = location.unwrap();
770        assert_eq!(file, "test_file.c", "File name should match");
771        assert_eq!(line, 42, "Line number should match");
772    }
773
774    #[test]
775    fn test_error_category_boundary_conditions() {
776        // Test all error category ranges with boundary conditions to catch mutations
777        // that modify range checks (>=, <, +, -, &&, ||)
778        use subversion_sys::*;
779
780        // Helper to create an error with a specific error code
781        let make_error = |code: u32| -> Error<'static> {
782            let err_ptr = unsafe {
783                svn_error_create(
784                    code as i32,
785                    std::ptr::null_mut(),
786                    b"Test\0".as_ptr() as *const i8,
787                )
788            };
789            Error {
790                ptr: err_ptr,
791                owns_ptr: true,
792                _phantom: std::marker::PhantomData,
793            }
794        };
795
796        let category_size = SVN_ERR_CATEGORY_SIZE;
797
798        // Test BadInput category (first category)
799        assert_eq!(
800            make_error(SVN_ERR_BAD_CATEGORY_START).category(),
801            ErrorCategory::BadInput,
802            "Start of BadInput range"
803        );
804        assert_eq!(
805            make_error(SVN_ERR_BAD_CATEGORY_START + category_size - 1).category(),
806            ErrorCategory::BadInput,
807            "End of BadInput range"
808        );
809        assert_eq!(
810            make_error(SVN_ERR_BAD_CATEGORY_START - 1).category(),
811            ErrorCategory::Other,
812            "Just before BadInput range"
813        );
814
815        // Test Xml category
816        assert_eq!(
817            make_error(SVN_ERR_XML_CATEGORY_START).category(),
818            ErrorCategory::Xml,
819            "Start of Xml range"
820        );
821        assert_eq!(
822            make_error(SVN_ERR_XML_CATEGORY_START + category_size - 1).category(),
823            ErrorCategory::Xml,
824            "End of Xml range"
825        );
826        assert_eq!(
827            make_error(SVN_ERR_XML_CATEGORY_START + category_size).category(),
828            ErrorCategory::Io,
829            "Just after Xml range should be Io"
830        );
831
832        // Test Io category
833        assert_eq!(
834            make_error(SVN_ERR_IO_CATEGORY_START).category(),
835            ErrorCategory::Io,
836            "Start of Io range"
837        );
838        assert_eq!(
839            make_error(SVN_ERR_IO_CATEGORY_START + category_size - 1).category(),
840            ErrorCategory::Io,
841            "End of Io range"
842        );
843
844        // Test Stream category
845        assert_eq!(
846            make_error(SVN_ERR_STREAM_CATEGORY_START).category(),
847            ErrorCategory::Stream,
848            "Start of Stream range"
849        );
850        assert_eq!(
851            make_error(SVN_ERR_STREAM_CATEGORY_START + category_size - 1).category(),
852            ErrorCategory::Stream,
853            "End of Stream range"
854        );
855
856        // Test Node category
857        assert_eq!(
858            make_error(SVN_ERR_NODE_CATEGORY_START).category(),
859            ErrorCategory::Node,
860            "Start of Node range"
861        );
862        assert_eq!(
863            make_error(SVN_ERR_NODE_CATEGORY_START + category_size - 1).category(),
864            ErrorCategory::Node,
865            "End of Node range"
866        );
867
868        // Test Entry category
869        assert_eq!(
870            make_error(SVN_ERR_ENTRY_CATEGORY_START).category(),
871            ErrorCategory::Entry,
872            "Start of Entry range"
873        );
874        assert_eq!(
875            make_error(SVN_ERR_ENTRY_CATEGORY_START + category_size - 1).category(),
876            ErrorCategory::Entry,
877            "End of Entry range"
878        );
879
880        // Test WorkingCopy category
881        assert_eq!(
882            make_error(SVN_ERR_WC_CATEGORY_START).category(),
883            ErrorCategory::WorkingCopy,
884            "Start of WorkingCopy range"
885        );
886        assert_eq!(
887            make_error(SVN_ERR_WC_CATEGORY_START + category_size - 1).category(),
888            ErrorCategory::WorkingCopy,
889            "End of WorkingCopy range"
890        );
891
892        // Test Filesystem category
893        assert_eq!(
894            make_error(SVN_ERR_FS_CATEGORY_START).category(),
895            ErrorCategory::Filesystem,
896            "Start of Filesystem range"
897        );
898        assert_eq!(
899            make_error(SVN_ERR_FS_CATEGORY_START + category_size - 1).category(),
900            ErrorCategory::Filesystem,
901            "End of Filesystem range"
902        );
903
904        // Test Repository category
905        assert_eq!(
906            make_error(SVN_ERR_REPOS_CATEGORY_START).category(),
907            ErrorCategory::Repository,
908            "Start of Repository range"
909        );
910        assert_eq!(
911            make_error(SVN_ERR_REPOS_CATEGORY_START + category_size - 1).category(),
912            ErrorCategory::Repository,
913            "End of Repository range"
914        );
915
916        // Test RepositoryAccess category
917        assert_eq!(
918            make_error(SVN_ERR_RA_CATEGORY_START).category(),
919            ErrorCategory::RepositoryAccess,
920            "Start of RepositoryAccess range"
921        );
922        assert_eq!(
923            make_error(SVN_ERR_RA_CATEGORY_START + category_size - 1).category(),
924            ErrorCategory::RepositoryAccess,
925            "End of RepositoryAccess range"
926        );
927
928        // Test RaDav category
929        assert_eq!(
930            make_error(SVN_ERR_RA_DAV_CATEGORY_START).category(),
931            ErrorCategory::RaDav,
932            "Start of RaDav range"
933        );
934        assert_eq!(
935            make_error(SVN_ERR_RA_DAV_CATEGORY_START + category_size - 1).category(),
936            ErrorCategory::RaDav,
937            "End of RaDav range"
938        );
939
940        // Test RaLocal category
941        assert_eq!(
942            make_error(SVN_ERR_RA_LOCAL_CATEGORY_START).category(),
943            ErrorCategory::RaLocal,
944            "Start of RaLocal range"
945        );
946        assert_eq!(
947            make_error(SVN_ERR_RA_LOCAL_CATEGORY_START + category_size - 1).category(),
948            ErrorCategory::RaLocal,
949            "End of RaLocal range"
950        );
951
952        // Test Svndiff category
953        assert_eq!(
954            make_error(SVN_ERR_SVNDIFF_CATEGORY_START).category(),
955            ErrorCategory::Svndiff,
956            "Start of Svndiff range"
957        );
958        assert_eq!(
959            make_error(SVN_ERR_SVNDIFF_CATEGORY_START + category_size - 1).category(),
960            ErrorCategory::Svndiff,
961            "End of Svndiff range"
962        );
963
964        // Test ApacheMod category
965        assert_eq!(
966            make_error(SVN_ERR_APMOD_CATEGORY_START).category(),
967            ErrorCategory::ApacheMod,
968            "Start of ApacheMod range"
969        );
970        assert_eq!(
971            make_error(SVN_ERR_APMOD_CATEGORY_START + category_size - 1).category(),
972            ErrorCategory::ApacheMod,
973            "End of ApacheMod range"
974        );
975
976        // Test Client category
977        assert_eq!(
978            make_error(SVN_ERR_CLIENT_CATEGORY_START).category(),
979            ErrorCategory::Client,
980            "Start of Client range"
981        );
982        assert_eq!(
983            make_error(SVN_ERR_CLIENT_CATEGORY_START + category_size - 1).category(),
984            ErrorCategory::Client,
985            "End of Client range"
986        );
987
988        // Test Misc category
989        assert_eq!(
990            make_error(SVN_ERR_MISC_CATEGORY_START).category(),
991            ErrorCategory::Misc,
992            "Start of Misc range"
993        );
994        assert_eq!(
995            make_error(SVN_ERR_MISC_CATEGORY_START + category_size - 1).category(),
996            ErrorCategory::Misc,
997            "End of Misc range"
998        );
999
1000        // Test CommandLine category
1001        assert_eq!(
1002            make_error(SVN_ERR_CL_CATEGORY_START).category(),
1003            ErrorCategory::CommandLine,
1004            "Start of CommandLine range"
1005        );
1006        assert_eq!(
1007            make_error(SVN_ERR_CL_CATEGORY_START + category_size - 1).category(),
1008            ErrorCategory::CommandLine,
1009            "End of CommandLine range"
1010        );
1011
1012        // Test RaSvn category
1013        assert_eq!(
1014            make_error(SVN_ERR_RA_SVN_CATEGORY_START).category(),
1015            ErrorCategory::RaSvn,
1016            "Start of RaSvn range"
1017        );
1018        assert_eq!(
1019            make_error(SVN_ERR_RA_SVN_CATEGORY_START + category_size - 1).category(),
1020            ErrorCategory::RaSvn,
1021            "End of RaSvn range"
1022        );
1023
1024        // Test Authentication category
1025        assert_eq!(
1026            make_error(SVN_ERR_AUTHN_CATEGORY_START).category(),
1027            ErrorCategory::Authentication,
1028            "Start of Authentication range"
1029        );
1030        assert_eq!(
1031            make_error(SVN_ERR_AUTHN_CATEGORY_START + category_size - 1).category(),
1032            ErrorCategory::Authentication,
1033            "End of Authentication range"
1034        );
1035
1036        // Test Authorization category
1037        assert_eq!(
1038            make_error(SVN_ERR_AUTHZ_CATEGORY_START).category(),
1039            ErrorCategory::Authorization,
1040            "Start of Authorization range"
1041        );
1042        assert_eq!(
1043            make_error(SVN_ERR_AUTHZ_CATEGORY_START + category_size - 1).category(),
1044            ErrorCategory::Authorization,
1045            "End of Authorization range"
1046        );
1047
1048        // Test Diff category
1049        assert_eq!(
1050            make_error(SVN_ERR_DIFF_CATEGORY_START).category(),
1051            ErrorCategory::Diff,
1052            "Start of Diff range"
1053        );
1054        assert_eq!(
1055            make_error(SVN_ERR_DIFF_CATEGORY_START + category_size - 1).category(),
1056            ErrorCategory::Diff,
1057            "End of Diff range"
1058        );
1059
1060        // Test RaSerf category
1061        assert_eq!(
1062            make_error(SVN_ERR_RA_SERF_CATEGORY_START).category(),
1063            ErrorCategory::RaSerf,
1064            "Start of RaSerf range"
1065        );
1066        assert_eq!(
1067            make_error(SVN_ERR_RA_SERF_CATEGORY_START + category_size - 1).category(),
1068            ErrorCategory::RaSerf,
1069            "End of RaSerf range"
1070        );
1071
1072        // Test Malfunction category
1073        assert_eq!(
1074            make_error(SVN_ERR_MALFUNC_CATEGORY_START).category(),
1075            ErrorCategory::Malfunction,
1076            "Start of Malfunction range"
1077        );
1078        assert_eq!(
1079            make_error(SVN_ERR_MALFUNC_CATEGORY_START + category_size - 1).category(),
1080            ErrorCategory::Malfunction,
1081            "End of Malfunction range"
1082        );
1083
1084        // Test X509 category (last category)
1085        assert_eq!(
1086            make_error(SVN_ERR_X509_CATEGORY_START).category(),
1087            ErrorCategory::X509,
1088            "Start of X509 range"
1089        );
1090        assert_eq!(
1091            make_error(SVN_ERR_X509_CATEGORY_START + category_size - 1).category(),
1092            ErrorCategory::X509,
1093            "End of X509 range"
1094        );
1095        assert_eq!(
1096            make_error(SVN_ERR_X509_CATEGORY_START + category_size).category(),
1097            ErrorCategory::Other,
1098            "Just after X509 range"
1099        );
1100
1101        // Test Other category (out of all ranges)
1102        assert_eq!(
1103            make_error(0).category(),
1104            ErrorCategory::Other,
1105            "Zero should be Other"
1106        );
1107        assert_eq!(
1108            make_error(1000).category(),
1109            ErrorCategory::Other,
1110            "Small values should be Other"
1111        );
1112        assert_eq!(
1113            make_error(300000).category(),
1114            ErrorCategory::Other,
1115            "Values beyond all categories should be Other"
1116        );
1117    }
1118
1119    #[test]
1120    fn test_error_best_message_returns_actual_message() {
1121        // Test that best_message() returns the actual error message, not "xyzzy" or empty string
1122        use subversion_sys::*;
1123
1124        let err_ptr = unsafe {
1125            svn_error_create(
1126                SVN_ERR_IO_CATEGORY_START as i32,
1127                std::ptr::null_mut(),
1128                b"Specific error message\0".as_ptr() as *const i8,
1129            )
1130        };
1131        let err = Error {
1132            ptr: err_ptr,
1133            owns_ptr: true,
1134            _phantom: std::marker::PhantomData,
1135        };
1136
1137        let msg = err.best_message();
1138        assert!(!msg.is_empty(), "best_message should not be empty");
1139        assert_ne!(msg, "xyzzy", "best_message should not be 'xyzzy'");
1140        assert_eq!(
1141            msg, "Specific error message",
1142            "best_message should return exact message, got '{}'",
1143            msg
1144        );
1145    }
1146
1147    #[test]
1148    fn test_error_child_returns_none_when_no_child() {
1149        // Test that child() returns None when there is no child error
1150        use subversion_sys::*;
1151
1152        let err_ptr = unsafe {
1153            svn_error_create(
1154                SVN_ERR_IO_CATEGORY_START as i32,
1155                std::ptr::null_mut(),
1156                b"Error without child\0".as_ptr() as *const i8,
1157            )
1158        };
1159
1160        let err = Error {
1161            ptr: err_ptr,
1162            owns_ptr: true,
1163            _phantom: std::marker::PhantomData,
1164        };
1165
1166        // Test that child() returns None when there is no child
1167        let child = err.child();
1168        assert!(
1169            child.is_none(),
1170            "child() should return None when no child exists"
1171        );
1172    }
1173
1174    #[test]
1175    fn test_error_find_cause_returns_none_for_non_matching_status() {
1176        // Test that find_cause() returns None when no error matches the requested status
1177        use subversion_sys::*;
1178
1179        // Create an error with a specific status
1180        let err_ptr = unsafe {
1181            svn_error_create(
1182                SVN_ERR_IO_CATEGORY_START as i32,
1183                std::ptr::null_mut(),
1184                b"Test error\0".as_ptr() as *const i8,
1185            )
1186        };
1187        let err = Error {
1188            ptr: err_ptr,
1189            owns_ptr: true,
1190            _phantom: std::marker::PhantomData,
1191        };
1192
1193        // Create another error with a different status to use for searching
1194        let different_err_ptr = unsafe {
1195            svn_error_create(
1196                (SVN_ERR_CLIENT_CATEGORY_START + 100) as i32,
1197                std::ptr::null_mut(),
1198                b"Different error\0".as_ptr() as *const i8,
1199            )
1200        };
1201        let different_err = Error {
1202            ptr: different_err_ptr,
1203            owns_ptr: true,
1204            _phantom: std::marker::PhantomData,
1205        };
1206        let different_status = different_err.apr_err();
1207        // Detach so it doesn't get cleaned up before we use the status
1208        std::mem::forget(different_err);
1209
1210        // Search for a status that won't be found in the first error
1211        let found = err.find_cause(different_status);
1212
1213        assert!(
1214            found.is_none(),
1215            "find_cause() should return None when status doesn't match any error in chain"
1216        );
1217
1218        // Clean up the different_err
1219        unsafe {
1220            subversion_sys::svn_error_clear(different_err_ptr);
1221        }
1222    }
1223
1224    #[test]
1225    fn test_error_child_returns_actual_child() {
1226        // Test that child() returns the actual child error when present, not None
1227        use subversion_sys::*;
1228
1229        let child_err_ptr = unsafe {
1230            svn_error_create(
1231                SVN_ERR_IO_CATEGORY_START as i32,
1232                std::ptr::null_mut(),
1233                b"Child error\0".as_ptr() as *const i8,
1234            )
1235        };
1236
1237        let parent_err_ptr = unsafe {
1238            svn_error_create(
1239                SVN_ERR_CLIENT_CATEGORY_START as i32,
1240                child_err_ptr,
1241                b"Parent error\0".as_ptr() as *const i8,
1242            )
1243        };
1244
1245        let parent_err = Error {
1246            ptr: parent_err_ptr,
1247            owns_ptr: true,
1248            _phantom: std::marker::PhantomData,
1249        };
1250
1251        // Test that child() returns Some when there is a child
1252        let child = parent_err.child();
1253        assert!(
1254            child.is_some(),
1255            "child() should return Some when child exists"
1256        );
1257
1258        let child_err = child.unwrap();
1259        assert_eq!(
1260            child_err.category(),
1261            ErrorCategory::Io,
1262            "Child error should have Io category"
1263        );
1264        assert!(
1265            child_err.message().unwrap().contains("Child error"),
1266            "Child error should have correct message"
1267        );
1268    }
1269
1270    #[test]
1271    fn test_error_find_cause_returns_matching_error() {
1272        // Test that find_cause() returns the error with matching status, not None
1273        // We create an error chain and verify find_cause can locate specific errors
1274        let child_err = Error::from_message("Child error");
1275        let parent_status = apr::Status::from(12345);
1276        let parent_err = Error::new(parent_status, Some(child_err), "Parent error");
1277
1278        // find_cause should find itself when searching for its own status
1279        let found = parent_err.find_cause(parent_status);
1280        assert!(
1281            found.is_some(),
1282            "find_cause() should find error with matching status"
1283        );
1284
1285        let found_err = found.unwrap();
1286        assert_eq!(
1287            found_err.apr_err(),
1288            parent_status,
1289            "Found error should have correct status"
1290        );
1291    }
1292
1293    #[test]
1294    fn test_error_as_ptr_returns_actual_pointer() {
1295        // Test that as_ptr() returns the actual pointer, not Default::default() (null)
1296        use subversion_sys::*;
1297
1298        let err_ptr = unsafe {
1299            svn_error_create(
1300                SVN_ERR_IO_CATEGORY_START as i32,
1301                std::ptr::null_mut(),
1302                b"Test\0".as_ptr() as *const i8,
1303            )
1304        };
1305
1306        let err = Error {
1307            ptr: err_ptr,
1308            owns_ptr: true,
1309            _phantom: std::marker::PhantomData,
1310        };
1311
1312        let ptr = err.as_ptr();
1313        assert!(!ptr.is_null(), "as_ptr() should return non-null pointer");
1314        assert_eq!(
1315            ptr, err_ptr,
1316            "as_ptr() should return the actual error pointer"
1317        );
1318    }
1319
1320    #[test]
1321    fn test_error_as_mut_ptr_returns_actual_pointer() {
1322        // Test that as_mut_ptr() returns the actual pointer, not Default::default() (null)
1323        use subversion_sys::*;
1324
1325        let err_ptr = unsafe {
1326            svn_error_create(
1327                SVN_ERR_IO_CATEGORY_START as i32,
1328                std::ptr::null_mut(),
1329                b"Test\0".as_ptr() as *const i8,
1330            )
1331        };
1332
1333        let mut err = Error {
1334            ptr: err_ptr,
1335            owns_ptr: true,
1336            _phantom: std::marker::PhantomData,
1337        };
1338
1339        let ptr = err.as_mut_ptr();
1340        assert!(
1341            !ptr.is_null(),
1342            "as_mut_ptr() should return non-null pointer"
1343        );
1344        assert_eq!(
1345            ptr, err_ptr,
1346            "as_mut_ptr() should return the actual error pointer"
1347        );
1348    }
1349
1350    #[test]
1351    fn test_symbolic_name_returns_actual_names() {
1352        // Test that symbolic_name() returns actual error names for errors we create,
1353        // not "xyzzy", "", or always None
1354
1355        // Create an actual error and get its status code
1356        let err = Error::from_message("Test error");
1357        let status = err.apr_err();
1358
1359        // Get the symbolic name for this error
1360        let name = symbolic_name(status);
1361
1362        // For an actual error we created, symbolic_name should return a valid name or None
1363        // We can't assert Some because not all error codes have symbolic names,
1364        // but if it returns Some, it must be valid
1365        if let Some(name_str) = name {
1366            assert!(
1367                !name_str.is_empty(),
1368                "Symbolic name should not be empty if returned"
1369            );
1370            assert_ne!(name_str, "xyzzy", "Symbolic name should not be 'xyzzy'");
1371            assert!(
1372                name_str.starts_with("SVN_"),
1373                "Symbolic name should start with SVN_, got: {}",
1374                name_str
1375            );
1376        }
1377
1378        // Test that the function doesn't panic with various inputs
1379        let _ = symbolic_name(0.into());
1380        let _ = symbolic_name(999999.into());
1381    }
1382
1383    #[test]
1384    fn test_strerror_returns_actual_error_strings() {
1385        // Test that strerror() returns actual error strings for errors we create,
1386        // not "xyzzy", "", or always None
1387
1388        // Create an actual error and get its status code
1389        let err = Error::from_message("Test error");
1390        let status = err.apr_err();
1391
1392        // Get the error string for this error
1393        let err_str = strerror(status);
1394
1395        // strerror MUST return Some for our created error, not None
1396        // This catches the mutation that always returns None
1397        assert!(
1398            err_str.is_some(),
1399            "strerror() must return Some for a valid SVN error code, got None"
1400        );
1401
1402        let err_msg = err_str.unwrap();
1403        assert!(
1404            !err_msg.is_empty(),
1405            "Error string should not be empty if returned"
1406        );
1407        assert_ne!(err_msg, "xyzzy", "Error string should not be 'xyzzy'");
1408        assert!(
1409            err_msg.len() > 2,
1410            "Error string should be substantive, got: {}",
1411            err_msg
1412        );
1413
1414        // Test that the function doesn't panic with various inputs
1415        // (these may or may not return Some, so we just check they don't panic)
1416        let _ = strerror(0.into());
1417        let _ = strerror(999999.into());
1418    }
1419
1420    #[test]
1421    fn test_error_category_off_by_one_and_midrange() {
1422        // Test off-by-one boundary conditions and mid-range values to catch operator mutations
1423        // This catches mutations like >= -> <, < -> ==, + -> -, && -> ||
1424        use subversion_sys::*;
1425
1426        let make_error = |code: u32| -> Error<'static> {
1427            let err_ptr = unsafe {
1428                svn_error_create(
1429                    code as i32,
1430                    std::ptr::null_mut(),
1431                    b"Test\0".as_ptr() as *const i8,
1432                )
1433            };
1434            Error {
1435                ptr: err_ptr,
1436                owns_ptr: true,
1437                _phantom: std::marker::PhantomData,
1438            }
1439        };
1440
1441        let category_size = SVN_ERR_CATEGORY_SIZE;
1442
1443        // For each category, test: START-1, START, START+1, MID, END-1, END, END+1
1444        let categories = vec![
1445            (
1446                SVN_ERR_BAD_CATEGORY_START,
1447                ErrorCategory::BadInput,
1448                "BadInput",
1449            ),
1450            (SVN_ERR_XML_CATEGORY_START, ErrorCategory::Xml, "Xml"),
1451            (SVN_ERR_IO_CATEGORY_START, ErrorCategory::Io, "Io"),
1452            (
1453                SVN_ERR_STREAM_CATEGORY_START,
1454                ErrorCategory::Stream,
1455                "Stream",
1456            ),
1457            (SVN_ERR_NODE_CATEGORY_START, ErrorCategory::Node, "Node"),
1458            (SVN_ERR_ENTRY_CATEGORY_START, ErrorCategory::Entry, "Entry"),
1459            (
1460                SVN_ERR_WC_CATEGORY_START,
1461                ErrorCategory::WorkingCopy,
1462                "WorkingCopy",
1463            ),
1464            (
1465                SVN_ERR_FS_CATEGORY_START,
1466                ErrorCategory::Filesystem,
1467                "Filesystem",
1468            ),
1469            (
1470                SVN_ERR_REPOS_CATEGORY_START,
1471                ErrorCategory::Repository,
1472                "Repository",
1473            ),
1474            (
1475                SVN_ERR_RA_CATEGORY_START,
1476                ErrorCategory::RepositoryAccess,
1477                "RepositoryAccess",
1478            ),
1479            (SVN_ERR_RA_DAV_CATEGORY_START, ErrorCategory::RaDav, "RaDav"),
1480            (
1481                SVN_ERR_RA_LOCAL_CATEGORY_START,
1482                ErrorCategory::RaLocal,
1483                "RaLocal",
1484            ),
1485            (
1486                SVN_ERR_SVNDIFF_CATEGORY_START,
1487                ErrorCategory::Svndiff,
1488                "Svndiff",
1489            ),
1490            (
1491                SVN_ERR_APMOD_CATEGORY_START,
1492                ErrorCategory::ApacheMod,
1493                "ApacheMod",
1494            ),
1495            (
1496                SVN_ERR_CLIENT_CATEGORY_START,
1497                ErrorCategory::Client,
1498                "Client",
1499            ),
1500            (SVN_ERR_MISC_CATEGORY_START, ErrorCategory::Misc, "Misc"),
1501            (
1502                SVN_ERR_CL_CATEGORY_START,
1503                ErrorCategory::CommandLine,
1504                "CommandLine",
1505            ),
1506            (SVN_ERR_RA_SVN_CATEGORY_START, ErrorCategory::RaSvn, "RaSvn"),
1507            (
1508                SVN_ERR_AUTHN_CATEGORY_START,
1509                ErrorCategory::Authentication,
1510                "Authentication",
1511            ),
1512            (
1513                SVN_ERR_AUTHZ_CATEGORY_START,
1514                ErrorCategory::Authorization,
1515                "Authorization",
1516            ),
1517            (SVN_ERR_DIFF_CATEGORY_START, ErrorCategory::Diff, "Diff"),
1518            (
1519                SVN_ERR_RA_SERF_CATEGORY_START,
1520                ErrorCategory::RaSerf,
1521                "RaSerf",
1522            ),
1523            (
1524                SVN_ERR_MALFUNC_CATEGORY_START,
1525                ErrorCategory::Malfunction,
1526                "Malfunction",
1527            ),
1528            (SVN_ERR_X509_CATEGORY_START, ErrorCategory::X509, "X509"),
1529        ];
1530
1531        for (start, expected_cat, name) in categories {
1532            // Test START (should be in category)
1533            assert_eq!(
1534                make_error(start).category(),
1535                expected_cat,
1536                "{}: START should be in category",
1537                name
1538            );
1539
1540            // Test START + 1 (should be in category, catches >= -> > mutation)
1541            assert_eq!(
1542                make_error(start + 1).category(),
1543                expected_cat,
1544                "{}: START+1 should be in category",
1545                name
1546            );
1547
1548            // Test mid-range (should be in category)
1549            let mid = start + category_size / 2;
1550            assert_eq!(
1551                make_error(mid).category(),
1552                expected_cat,
1553                "{}: MID should be in category",
1554                name
1555            );
1556
1557            // Test END - 2 (should be in category)
1558            assert_eq!(
1559                make_error(start + category_size - 2).category(),
1560                expected_cat,
1561                "{}: END-2 should be in category",
1562                name
1563            );
1564
1565            // Test END - 1 (should be in category, catches < -> <= mutation)
1566            assert_eq!(
1567                make_error(start + category_size - 1).category(),
1568                expected_cat,
1569                "{}: END-1 (last valid) should be in category",
1570                name
1571            );
1572
1573            // Test END (should NOT be in category, catches < -> <= mutation)
1574            assert_ne!(
1575                make_error(start + category_size).category(),
1576                expected_cat,
1577                "{}: END should NOT be in category",
1578                name
1579            );
1580
1581            // Test START - 1 (should NOT be in category for most, catches >= -> > mutation)
1582            // Skip for first category since START-1 might underflow
1583            if start > 1000 {
1584                assert_ne!(
1585                    make_error(start - 1).category(),
1586                    expected_cat,
1587                    "{}: START-1 should NOT be in category",
1588                    name
1589                );
1590            }
1591        }
1592
1593        // Test value way below all categories
1594        assert_eq!(
1595            make_error(100).category(),
1596            ErrorCategory::Other,
1597            "Value below all categories should be Other"
1598        );
1599
1600        // Test value way above all categories
1601        assert_eq!(
1602            make_error(500000).category(),
1603            ErrorCategory::Other,
1604            "Value above all categories should be Other"
1605        );
1606
1607        // Test between categories (just after BadInput, should be Xml or Other)
1608        let between = SVN_ERR_BAD_CATEGORY_START + category_size;
1609        let between_cat = make_error(between).category();
1610        assert_ne!(
1611            between_cat,
1612            ErrorCategory::BadInput,
1613            "Value just after BadInput should not be BadInput"
1614        );
1615    }
1616
1617    #[test]
1618    fn test_raw_apr_err_preserves_svn_error_codes() {
1619        // SVN error codes like SVN_ERR_CANCELLED (200015) are not standard APR
1620        // status codes and get mapped to General by apr::Status::from().
1621        // raw_apr_err() must preserve the original code.
1622        let cancelled_code = subversion_sys::svn_errno_t_SVN_ERR_CANCELLED as i32;
1623        let err = Error::with_raw_status(cancelled_code, None, "cancelled");
1624
1625        assert_eq!(err.raw_apr_err(), cancelled_code);
1626        // apr_err() loses the distinction — both map to General
1627        assert_eq!(err.apr_err(), apr::Status::General);
1628    }
1629
1630    #[test]
1631    fn test_with_raw_status_creates_distinguishable_errors() {
1632        let cancelled_code = subversion_sys::svn_errno_t_SVN_ERR_CANCELLED as i32;
1633        let fs_not_found_code = subversion_sys::svn_errno_t_SVN_ERR_FS_NOT_FOUND as i32;
1634
1635        let err1 = Error::with_raw_status(cancelled_code, None, "cancelled");
1636        let err2 = Error::with_raw_status(fs_not_found_code, None, "not found");
1637
1638        // apr_err() would return General for both — indistinguishable
1639        assert_eq!(err1.apr_err(), err2.apr_err());
1640        // raw_apr_err() preserves the difference
1641        assert_ne!(err1.raw_apr_err(), err2.raw_apr_err());
1642        assert_eq!(err1.raw_apr_err(), cancelled_code);
1643        assert_eq!(err2.raw_apr_err(), fs_not_found_code);
1644    }
1645
1646    #[test]
1647    fn test_with_raw_status_message_and_child() {
1648        let child = Error::from_message("child error");
1649        let parent = Error::with_raw_status(200015, Some(child), "parent error");
1650
1651        assert_eq!(parent.message(), Some("parent error"));
1652        let full = parent.full_message();
1653        assert!(full.contains("parent error"));
1654        assert!(full.contains("child error"));
1655    }
1656}