From abd32dc60ae8946c7473bb3c90b0cb7a205ca23c Mon Sep 17 00:00:00 2001 From: Thomas ten Cate Date: Wed, 25 Jan 2023 11:44:16 +0100 Subject: [PATCH] Implement basic Array functionality --- godot-core/src/builtin/arrays.rs | 458 +++++++++++++++++- godot-core/src/builtin/variant/impls.rs | 1 + godot-core/src/builtin/variant/mod.rs | 2 +- .../src/builtin/variant/variant_traits.rs | 2 +- itest/rust/src/array_test.rs | 254 ++++++++++ itest/rust/src/lib.rs | 2 + 6 files changed, 708 insertions(+), 11 deletions(-) create mode 100644 itest/rust/src/array_test.rs diff --git a/godot-core/src/builtin/arrays.rs b/godot-core/src/builtin/arrays.rs index 0fdbfba7e..f7798f63f 100644 --- a/godot-core/src/builtin/arrays.rs +++ b/godot-core/src/builtin/arrays.rs @@ -6,12 +6,30 @@ use godot_ffi as sys; -use crate::builtin::{inner, FromVariant, Variant}; +use crate::builtin::{inner, FromVariant, ToVariant, Variant, VariantConversionError}; +use std::fmt; use std::marker::PhantomData; use sys::types::*; use sys::{ffi_methods, interface_fn, GodotFfi}; -impl_builtin_stub!(Array, OpaqueArray); +/// Godot's `Array` type. +/// +/// This is a variant array, meaning it contains `Variant`s which may be of different types even +/// within the same array. +/// +/// Unlike GDScript, all indices and sizes are unsigned, so negative indices are not supported. +/// +/// # Safety +/// +/// Usage is safe if the `Array` is used on a single thread only. Concurrent reads on different +/// threads are also safe, but any writes must be externally synchronized. The Rust compiler will +/// enforce this as long as you use only Rust threads, but it cannot protect against concurrent +/// modification on other threads (e.g. created through GDScript). +#[repr(C)] +pub struct Array { + opaque: sys::types::OpaqueArray, +} + impl_builtin_stub!(PackedByteArray, OpaquePackedByteArray); impl_builtin_stub!(PackedColorArray, OpaquePackedColorArray); impl_builtin_stub!(PackedFloat32Array, OpaquePackedFloat32Array); @@ -45,23 +63,441 @@ impl_builtin_froms!(PackedVector2Array; Array => packed_vector2_array_from_array impl_builtin_froms!(PackedVector3Array; Array => packed_vector3_array_from_array); impl Array { - pub fn get(&self, index: i64) -> Option { + fn from_opaque(opaque: sys::types::OpaqueArray) -> Self { + Self { opaque } + } +} + +// This impl relies on `InnerArray` which is not (yet) available in unit tests +#[cfg(not(any(gdext_test, doctest)))] +impl Array { + /// Constructs an empty `Array`. + pub fn new() -> Self { + Self::default() + } + + /// Returns the number of elements in the array. Equivalent of `size()` in Godot. + pub fn len(&self) -> usize { + self.as_inner().size().try_into().unwrap() + } + + /// Returns `true` if the array is empty. + pub fn is_empty(&self) -> bool { + self.as_inner().is_empty() + } + + /// Returns a 32-bit integer hash value representing the array and its contents. + /// + /// Note: Arrays with equal content will always produce identical hash values. However, the + /// reverse is not true. Returning identical hash values does not imply the arrays are equal, + /// because different arrays can have identical hash values due to hash collisions. + pub fn hash(&self) -> u32 { + // The GDExtension interface only deals in `i64`, but the engine's own `hash()` function + // actually returns `uint32_t`. + self.as_inner().hash().try_into().unwrap() + } + + /// Converts this array to a strongly typed Rust vector. If the conversion from `Variant` fails + /// for any element, an error is returned. + pub fn try_to_vec(&self) -> Result, VariantConversionError> { + let len = self.len(); + let mut vec = Vec::with_capacity(len); + let ptr = self.ptr(0); + for offset in 0..len.try_into().unwrap() { + // SAFETY: Arrays are stored contiguously in memory, so we can use pointer arithmetic + // instead of going through `array_operator_index_const` for every index. + let element = unsafe { T::try_from_variant(&*ptr.offset(offset))? }; + vec.push(element); + } + Ok(vec) + } + + /// Implements iteration over an `Array` by reference, but returns (cheap) copies of the + /// `Variant`s in the array. + pub fn iter(&self) -> ArrayIterator<'_> { + ArrayIterator { + start: self.ptr(0), + len: self.len(), + next_idx: 0, + _phantom: PhantomData, + } + } + + /// Clears the array, removing all elements. + pub fn clear(&mut self) { + self.as_inner().clear(); + } + + /// Resizes the array to contain a different number of elements. If the new size is smaller, + /// elements are removed from the end. If the new size is larger, new elements are set to + /// [`Variant::nil()`]. + pub fn resize(&mut self, size: usize) { + self.as_inner().resize(size.try_into().unwrap()); + } + + // TODO: find out why this segfaults (even on an empty array, regardless of deep = true/false) + // /// Returns a deep copy of the array. All nested arrays and dictionaries are duplicated and + // /// will not be shared with the original array. Note that any `Object`-derived elements will + // /// still be shallow copied. + // /// + // /// To create a shallow copy, use `clone()` instead. + // pub fn duplicate_deep(&self) -> Self { + // self.as_inner().duplicate(true) + // } + + /// Returns the value at the specified index as a `Variant`. To convert to a specific type, use + /// the available conversion methods on `Variant`, such as [`Variant::try_to`] or + /// [`Variant::to`]. + /// + /// Panics if `index` is out of bounds. + pub fn get(&self, index: usize) -> Variant { + let ptr = self.ptr(index); + // SAFETY: `ptr` just verified that the index is not out of bounds. + unsafe { (*ptr).clone() } + } + + /// Returns the value at the specified index as a `Variant`. To convert to a specific type, use + /// the available conversion methods on `Variant`, such as [`Variant::try_to`] or + /// [`Variant::to`]. + /// + /// Panics if `index` is out of bounds. + /// + /// # Safety + /// + /// The returned reference is invalidated if the same array is mutated through another + /// reference. + pub unsafe fn get_mut(&mut self, index: usize) -> &mut Variant { + let ptr = self.ptr_mut(index); + // SAFETY: `ptr_mut` just checked that the index is not out of bounds. + unsafe { &mut *ptr } + } + + /// Returns the first element in the array, or `None` if the array is empty. Equivalent of + /// `front()` in GDScript. + pub fn first(&self) -> Option { + (!self.is_empty()).then(|| self.as_inner().front()) + } + + /// Returns the last element in the array, or `None` if the array is empty. Equivalent of + /// `back()` in GDScript. + pub fn last(&self) -> Option { + (!self.is_empty()).then(|| self.as_inner().back()) + } + + /// Finds the index of an existing value in a sorted array using binary search. Equivalent of + /// `bsearch` in GDScript. + /// + /// If the value is not present in the array, returns the insertion index that would maintain + /// sorting order. + /// + /// Calling `binary_search` on an unsorted array results in unspecified behavior. + pub fn binary_search(&self, value: Variant) -> usize { + self.as_inner().bsearch(value, true).try_into().unwrap() + } + + /// Returns the number of times a value is in the array. + pub fn count(&self, value: Variant) -> usize { + self.as_inner().count(value).try_into().unwrap() + } + + /// Returns `true` if the array contains the given value. Equivalent of `has` in GDScript. + pub fn contains(&self, value: Variant) -> bool { + self.as_inner().has(value) + } + + /// Searches the array for the first occurrence of a value and returns its index, or `None` if + /// not found. Starts searching at index `from`; pass `None` to search the entire array. + pub fn find(&self, value: Variant, from: Option) -> Option { + let from = from.unwrap_or(0).try_into().unwrap(); + let index = self.as_inner().find(value, from); + if index >= 0 { + Some(index.try_into().unwrap()) + } else { + None + } + } + + /// Searches the array backwards for the last occurrence of a value and returns its index, or + /// `None` if not found. Starts searching at index `from`; pass `None` to search the entire + /// array. + pub fn rfind(&self, value: Variant, from: Option) -> Option { + let from = from.map(|from| from.try_into().unwrap()).unwrap_or(-1); + let index = self.as_inner().rfind(value, from); + // It's not documented, but `rfind` returns -1 if not found. + if index >= 0 { + Some(index.try_into().unwrap()) + } else { + None + } + } + + /// Returns the minimum value contained in the array if all elements are of comparable types. + /// If the elements can't be compared or the array is empty, `None` is returned. + pub fn min(&self) -> Option { + let min = self.as_inner().min(); + (!min.is_nil()).then_some(min) + } + + /// Returns the maximum value contained in the array if all elements are of comparable types. + /// If the elements can't be compared or the array is empty, `None` is returned. + pub fn max(&self) -> Option { + let max = self.as_inner().max(); + (!max.is_nil()).then_some(max) + } + + /// Returns a random element from the array, or `None` if it is empty. + pub fn pick_random(&self) -> Option { + (!self.is_empty()).then(|| self.as_inner().pick_random()) + } + + /// Sets the value at the specified index as a `Variant`. To convert a specific type (which + /// implements `ToVariant`) to a variant, call [`ToVariant::to_variant`] on it. + /// + /// Panics if `index` is out of bounds. + pub fn set(&mut self, index: usize, value: Variant) { + let ptr_mut = self.ptr_mut(index); + // SAFETY: `ptr_mut` just checked that the index is not out of bounds. unsafe { - let ptr = (interface_fn!(array_operator_index))(self.sys(), index) as *mut Variant; - if ptr.is_null() { - return None; - } - Some((*ptr).clone()) + *ptr_mut = value; + } + } + + /// Appends an element to the end of the array. Equivalent of `append` and `push_back` in + /// GDScript. + pub fn push(&mut self, value: Variant) { + self.as_inner().push_back(value); + } + + /// Adds an element at the beginning of the array. See also `push`. + /// + /// Note: On large arrays, this method is much slower than `push` as it will move all the + /// array's elements. The larger the array, the slower `push_front` will be. + pub fn push_front(&mut self, value: Variant) { + self.as_inner().push_front(value); + } + + /// Removes and returns the last element of the array. Returns `None` if the array is empty. + /// Equivalent of `pop_back` in GDScript. + pub fn pop(&mut self) -> Option { + (!self.is_empty()).then(|| self.as_inner().pop_back()) + } + + /// Removes and returns the first element of the array. Returns `None` if the array is empty. + /// + /// Note: On large arrays, this method is much slower than `pop` as it will move all the + /// array's elements. The larger the array, the slower `pop_front` will be. + pub fn pop_front(&mut self) -> Option { + (!self.is_empty()).then(|| self.as_inner().pop_front()) + } + + /// Inserts a new element at a given index in the array. The index must be valid, or at the end + /// of the array (`index == len()`). + /// + /// Note: On large arrays, this method is much slower than `push` as it will move all the + /// array's elements after the inserted element. The larger the array, the slower `insert` will + /// be. + pub fn insert(&mut self, index: usize, value: Variant) { + let len = self.len(); + if index > len { + panic!( + "Array insertion index {} is out of bounds: length is {}", + index, len + ); + } + self.as_inner().insert(index.try_into().unwrap(), value); + } + + /// Removes and returns the element at the specified index. Equivalent of `pop_at` in GDScript. + /// + /// On large arrays, this method is much slower than `pop_back` as it will move all the array's + /// elements after the removed element. The larger the array, the slower `remove` will be. + /// + /// Panics if `index` is out of bounds. + pub fn remove(&mut self, index: usize) -> Variant { + self.check_bounds(index); + self.as_inner().pop_at(index.try_into().unwrap()) + } + + /// Removes the first occurrence of a value from the array. If the value does not exist in the + /// array, nothing happens. To remove an element by index, use `remove` instead. + /// + /// On large arrays, this method is much slower than `pop_back` as it will move all the array's + /// elements after the removed element. The larger the array, the slower `remove` will be. + pub fn erase(&mut self, value: Variant) { + self.as_inner().erase(value); + } + + /// Assigns the given value to all elements in the array. This can be used together with + /// `resize` to create an array with a given size and initialized elements. + pub fn fill(&mut self, value: Variant) { + self.as_inner().fill(value); + } + + /// Appends another array at the end of this array. Equivalent of `append_array` in GDScript. + pub fn extend_array(&mut self, other: Array) { + self.as_inner().append_array(other); + } + + /// Reverses the order of the elements in the array. + pub fn reverse(&mut self) { + self.as_inner().reverse(); + } + + /// Sorts the array. + /// + /// Note: The sorting algorithm used is not + /// [stable](https://en.wikipedia.org/wiki/Sorting_algorithm#Stability). This means that values + /// considered equal may have their order changed when using `sort_unstable`. + pub fn sort_unstable(&mut self) { + self.as_inner().sort(); + } + + /// Shuffles the array such that the items will have a random order. This method uses the + /// global random number generator common to methods such as `randi`. Call `randomize` to + /// ensure that a new seed will be used each time if you want non-reproducible shuffling. + pub fn shuffle(&mut self) { + self.as_inner().shuffle(); + } + + /// Wraps negative indices to count from the end of the array, because the GDExtension API + /// doesn't do this. + /// + /// Panics if `index` is out of bounds. + fn check_bounds(&self, index: usize) { + let len = self.len(); + if index >= len { + panic!("Array index {} is out of bounds: length is {}", index, len); + } + } + + fn ptr(&self, index: usize) -> *const Variant { + if self.is_empty() { + std::ptr::null() + } else { + self.check_bounds(index); + // SAFETY: We just checked that the index is not out of bounds. + let ptr = unsafe { + (interface_fn!(array_operator_index_const))(self.sys(), index.try_into().unwrap()) + as *const Variant + }; + assert!(!ptr.is_null()); + ptr + } + } + + fn ptr_mut(&self, index: usize) -> *mut Variant { + if self.is_empty() { + std::ptr::null_mut() + } else { + self.check_bounds(index); + // SAFETY: We just checked that the index is not out of bounds. + let ptr = unsafe { + (interface_fn!(array_operator_index))(self.sys(), index.try_into().unwrap()) + as *mut Variant + }; + assert!(!ptr.is_null()); + ptr } } - #[cfg(not(any(gdext_test, doctest)))] #[doc(hidden)] pub fn as_inner(&self) -> inner::InnerArray { inner::InnerArray::from_outer(self) } } +/// Creates an `Array` from the given Rust array. Each element is converted to a `Variant`. +#[cfg(not(any(gdext_test, doctest)))] +impl From<&[T; N]> for Array { + fn from(arr: &[T; N]) -> Self { + Self::from(&arr[..]) + } +} + +/// Creates an `Array` from the given slice. Each element is converted to a `Variant`. +#[cfg(not(any(gdext_test, doctest)))] +impl From<&[T]> for Array { + fn from(slice: &[T]) -> Self { + let mut array = Self::new(); + let len = slice.len(); + if len == 0 { + return array; + } + array.resize(len); + let ptr = array.ptr_mut(0); + for (i, element) in slice.iter().enumerate() { + // SAFETY: The array contains exactly `len` elements, stored contiguously in memory. + unsafe { + *ptr.offset(i.try_into().unwrap()) = element.to_variant(); + } + } + array + } +} + +/// Creates an `Array` from an iterator. Each element is converted to a `Variant`. +#[cfg(not(any(gdext_test, doctest)))] +impl FromIterator for Array { + fn from_iter>(iter: I) -> Self { + let mut array = Array::new(); + array.extend(iter); + array + } +} + +/// Extends an `Array` with the contents of an iterator. Each element is converted to a `Variant`. +#[cfg(not(any(gdext_test, doctest)))] +impl Extend for Array { + fn extend>(&mut self, iter: I) { + // Unfortunately the GDExtension API does not offer the equivalent of `Vec::reserve`. + // Otherwise we could use it to pre-allocate based on `iter.size_hint()`. + // + // A faster implementation using `resize()` and direct pointer writes might still be + // possible. + for item in iter.into_iter() { + self.push(item.to_variant()); + } + } +} + +/// See [`Array::iter()`]. +#[cfg(not(any(gdext_test, doctest)))] +impl<'a> IntoIterator for &'a Array { + type Item = Variant; + type IntoIter = ArrayIterator<'a>; + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +pub struct ArrayIterator<'a> { + start: *const Variant, + len: usize, + next_idx: usize, + _phantom: PhantomData<&'a Array>, +} + +impl<'a> Iterator for ArrayIterator<'a> { + type Item = Variant; + + fn next(&mut self) -> Option { + if self.next_idx < self.len { + let offset = self.next_idx.try_into().unwrap(); + self.next_idx += 1; + unsafe { Some((*self.start.offset(offset)).clone()) } + } else { + None + } + } +} + +impl fmt::Debug for Array { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // Going through `Variant` because there doesn't seem to be a direct way. + write!(f, "{:?}", self.to_variant().stringify()) + } +} + impl_builtin_traits! { for Array { Default => array_construct_default; @@ -70,6 +506,10 @@ impl_builtin_traits! { } } +impl GodotFfi for Array { + ffi_methods! { type sys::GDExtensionTypePtr = *mut Opaque; .. } +} + #[repr(C)] pub struct TypedArray { opaque: OpaqueArray, diff --git a/godot-core/src/builtin/variant/impls.rs b/godot-core/src/builtin/variant/impls.rs index e90195e03..a89aa48e1 100644 --- a/godot-core/src/builtin/variant/impls.rs +++ b/godot-core/src/builtin/variant/impls.rs @@ -141,6 +141,7 @@ mod impls { impl_variant_traits!(Color, color_to_variant, color_from_variant, Color); impl_variant_traits!(GodotString, string_to_variant, string_from_variant, String); impl_variant_traits!(StringName, string_name_to_variant, string_name_from_variant, StringName); + impl_variant_traits!(Array, array_to_variant, array_from_variant, Array); impl_variant_traits!(i64, int_to_variant, int_from_variant, Int, GDEXTENSION_METHOD_ARGUMENT_METADATA_INT_IS_INT64); diff --git a/godot-core/src/builtin/variant/mod.rs b/godot-core/src/builtin/variant/mod.rs index ed3b6e047..72ccb2d9c 100644 --- a/godot-core/src/builtin/variant/mod.rs +++ b/godot-core/src/builtin/variant/mod.rs @@ -101,7 +101,7 @@ impl Variant { } #[allow(unused_mut)] - fn stringify(&self) -> GodotString { + pub(crate) fn stringify(&self) -> GodotString { let mut result = GodotString::new(); unsafe { interface_fn!(variant_stringify)(self.var_sys(), result.string_sys()); diff --git a/godot-core/src/builtin/variant/variant_traits.rs b/godot-core/src/builtin/variant/variant_traits.rs index 8ead0287f..bb2f0d53e 100644 --- a/godot-core/src/builtin/variant/variant_traits.rs +++ b/godot-core/src/builtin/variant/variant_traits.rs @@ -40,7 +40,7 @@ pub trait ToVariant { // ---------------------------------------------------------------------------------------------------------------------------------------------- -#[derive(Debug)] +#[derive(Debug, Eq, PartialEq)] pub struct VariantConversionError; /*pub enum VariantConversionError { /// Variant type does not match expected type diff --git a/itest/rust/src/array_test.rs b/itest/rust/src/array_test.rs new file mode 100644 index 000000000..348b77866 --- /dev/null +++ b/itest/rust/src/array_test.rs @@ -0,0 +1,254 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +use crate::{expect_panic, itest}; +use godot::builtin::{Array, FromVariant, GodotString, ToVariant}; + +pub fn run() -> bool { + let mut ok = true; + ok &= array_default(); + ok &= array_new(); + ok &= array_from_iterator(); + ok &= array_from(); + ok &= array_try_to_vec(); + ok &= array_into_iterator(); + ok &= array_clone(); + // ok &= array_duplicate_deep(); + ok &= array_hash(); + ok &= array_get(); + ok &= array_first_last(); + ok &= array_binary_search(); + ok &= array_find(); + ok &= array_rfind(); + ok &= array_min_max(); + ok &= array_set(); + ok &= array_push_pop(); + ok &= array_insert(); + ok &= array_extend(); + ok &= array_reverse(); + ok &= array_sort(); + ok &= array_shuffle(); + ok +} + +#[itest] +fn array_default() { + assert_eq!(Array::default().len(), 0); +} + +#[itest] +fn array_new() { + assert_eq!(Array::new().len(), 0); +} + +#[itest] +fn array_from_iterator() { + let array = Array::from_iter([1, 2]); + + assert_eq!(array.len(), 2); + assert_eq!(array.get(0), 1.to_variant()); + assert_eq!(array.get(1), 2.to_variant()); +} + +#[itest] +fn array_from() { + let array = Array::from(&[1, 2]); + + assert_eq!(array.len(), 2); + assert_eq!(array.get(0), 1.to_variant()); + assert_eq!(array.get(1), 2.to_variant()); +} + +#[itest] +fn array_try_to_vec() { + let array = Array::from(&[1, 2]); + assert_eq!(array.try_to_vec::(), Ok(vec![1, 2])); +} + +#[itest] +fn array_into_iterator() { + let array = Array::from(&[1, 2]); + let mut iter = array.into_iter(); + assert_eq!(iter.next(), Some(1.to_variant())); + assert_eq!(iter.next(), Some(2.to_variant())); + assert_eq!(iter.next(), None); +} + +#[itest] +fn array_clone() { + let subarray = Array::from(&[2, 3]); + let array = Array::from(&[1.to_variant(), subarray.to_variant()]); + #[allow(clippy::redundant_clone)] + let mut clone = array.clone(); + Array::try_from_variant(unsafe { clone.get_mut(1) }) + .unwrap() + .set(0, 4.to_variant()); + assert_eq!(subarray.get(0), 4.to_variant()); +} + +#[itest] +fn array_hash() { + let array = Array::from(&[1, 2]); + // Just testing that it converts successfully from i64 to u32. + array.hash(); +} + +// TODO: enable once the implementation no longer segfaults +// #[itest] +// fn array_duplicate_deep() { +// let subarray = Array::from(&[2, 3]); +// let array = Array::from(&[1.to_variant(), subarray.to_variant()]); +// let mut clone = array.duplicate_deep(); +// Array::try_from_variant(unsafe { clone.get_mut(1) }).unwrap().set(0, 4.to_variant()); +// assert_eq!(subarray.get(0), 3.to_variant()); +// } + +#[itest] +fn array_get() { + let array = Array::from(&[1, 2]); + + assert_eq!(array.get(0), 1.to_variant()); + assert_eq!(array.get(1), 2.to_variant()); + expect_panic("Array index 2 out of bounds: length is 2", || { + array.get(2); + }); +} + +#[itest] +fn array_first_last() { + let array = Array::from(&[1, 2]); + + assert_eq!(array.first(), Some(1.to_variant())); + assert_eq!(array.last(), Some(2.to_variant())); + + let empty_array = Array::new(); + + assert_eq!(empty_array.first(), None); + assert_eq!(empty_array.last(), None); +} + +#[itest] +fn array_binary_search() { + let array = Array::from(&[1, 2]); + + assert_eq!(array.binary_search(0.to_variant()), 0); + assert_eq!(array.binary_search(1.to_variant()), 0); + assert_eq!(array.binary_search(1.5f64.to_variant()), 1); + assert_eq!(array.binary_search(2.to_variant()), 1); + assert_eq!(array.binary_search(3.to_variant()), 2); +} + +#[itest] +fn array_find() { + let array = Array::from(&[1, 2, 1]); + + assert_eq!(array.find(0.to_variant(), None), None); + assert_eq!(array.find(1.to_variant(), None), Some(0)); + assert_eq!(array.find(1.to_variant(), Some(1)), Some(2)); +} + +#[itest] +fn array_rfind() { + let array = Array::from(&[1, 2, 1]); + + assert_eq!(array.rfind(0.to_variant(), None), None); + assert_eq!(array.rfind(1.to_variant(), None), Some(2)); + assert_eq!(array.rfind(1.to_variant(), Some(1)), Some(0)); +} + +#[itest] +fn array_min_max() { + let int_array = Array::from(&[1, 2]); + + assert_eq!(int_array.min(), Some(1.to_variant())); + assert_eq!(int_array.max(), Some(2.to_variant())); + + let uncomparable_array = Array::from(&[1.to_variant(), GodotString::from("two").to_variant()]); + + assert_eq!(uncomparable_array.min(), None); + assert_eq!(uncomparable_array.max(), None); + + let empty_array = Array::new(); + + assert_eq!(empty_array.min(), None); + assert_eq!(empty_array.max(), None); +} + +#[itest] +fn array_pick_random() { + assert_eq!(Array::new().pick_random(), None); + assert_eq!(Array::from(&[1]).pick_random(), Some(1.to_variant())); +} + +#[itest] +fn array_set() { + let mut array = Array::from(&[1, 2]); + + array.set(0, 3.to_variant()); + assert_eq!(array.get(0), 3.to_variant()); + + expect_panic("Array index 2 out of bounds: length is 2", move || { + array.set(2, 4.to_variant()); + }); +} + +#[itest] +fn array_push_pop() { + let mut array = Array::from(&[1, 2]); + + array.push(3.to_variant()); + assert_eq!(array.pop(), Some(3.to_variant())); + + array.push_front(4.to_variant()); + assert_eq!(array.pop_front(), Some(4.to_variant())); + + assert_eq!(array.pop(), Some(2.to_variant())); + assert_eq!(array.pop_front(), Some(1.to_variant())); + + assert_eq!(array.pop(), None); + assert_eq!(array.pop_front(), None); +} + +#[itest] +fn array_insert() { + let mut array = Array::from(&[1, 2]); + + array.insert(0, 3.to_variant()); + assert_eq!(array.try_to_vec::().unwrap(), vec![3, 1, 2]); + + array.insert(3, 4.to_variant()); + assert_eq!(array.try_to_vec::().unwrap(), vec![3, 1, 2, 4]); +} + +#[itest] +fn array_extend() { + let mut array = Array::from(&[1, 2]); + let other = Array::from(&[3, 4]); + array.extend_array(other); + assert_eq!(array.try_to_vec::().unwrap(), vec![1, 2, 3, 4]); +} + +#[itest] +fn array_sort() { + let mut array = Array::from(&[2, 1]); + array.sort_unstable(); + assert_eq!(array.try_to_vec::().unwrap(), vec![1, 2]); +} + +#[itest] +fn array_reverse() { + let mut array = Array::from(&[1, 2]); + array.reverse(); + assert_eq!(array.try_to_vec::().unwrap(), vec![2, 1]); +} + +#[itest] +fn array_shuffle() { + // Since the output is random, we just test that it doesn't crash. + let mut array = Array::from(&[1i64]); + array.shuffle(); + assert_eq!(array.try_to_vec::().unwrap(), vec![1]); +} diff --git a/itest/rust/src/lib.rs b/itest/rust/src/lib.rs index 4983b8863..669df563a 100644 --- a/itest/rust/src/lib.rs +++ b/itest/rust/src/lib.rs @@ -12,6 +12,7 @@ use godot::init::{gdextension, ExtensionLibrary}; use godot::test::itest; use std::panic::UnwindSafe; +mod array_test; mod base_test; mod builtin_test; mod enum_test; @@ -36,6 +37,7 @@ fn run_tests() -> bool { ok &= object_test::run(); ok &= singleton_test::run(); ok &= string_test::run(); + ok &= array_test::run(); ok &= utilities_test::run(); ok &= variant_test::run(); ok &= virtual_methods_test::run();