mirror of
https://github.com/asterinas/asterinas.git
synced 2025-06-13 23:36:48 +00:00
Add ktest cases
This commit is contained in:
parent
79b0866259
commit
63831dc2df
330
kernel/comps/systree/src/test.rs
Normal file
330
kernel/comps/systree/src/test.rs
Normal file
@ -0,0 +1,330 @@
|
|||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
use alloc::{
|
||||||
|
borrow::Cow,
|
||||||
|
string::{String, ToString},
|
||||||
|
sync::{Arc, Weak},
|
||||||
|
vec::Vec,
|
||||||
|
};
|
||||||
|
use core::{any::Any, fmt::Debug};
|
||||||
|
|
||||||
|
use ostd::{
|
||||||
|
mm::{FallibleVmRead, FallibleVmWrite, VmReader, VmWriter},
|
||||||
|
prelude::ktest,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
Error, Result, SysAttrFlags, SysAttrSet, SysAttrSetBuilder, SysBranchNode, SysBranchNodeFields,
|
||||||
|
SysNode, SysNodeId, SysNodeType, SysObj, SysStr, SysSymlink, SysTree,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct DeviceNode {
|
||||||
|
fields: SysBranchNodeFields<dyn SysObj>,
|
||||||
|
self_ref: Weak<Self>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DeviceNode {
|
||||||
|
fn new(name: &str) -> Arc<Self> {
|
||||||
|
let mut builder = SysAttrSetBuilder::new();
|
||||||
|
builder
|
||||||
|
.add(Cow::Borrowed("model"), SysAttrFlags::CAN_READ)
|
||||||
|
.add(Cow::Borrowed("vendor"), SysAttrFlags::CAN_READ)
|
||||||
|
.add(
|
||||||
|
Cow::Borrowed("status"),
|
||||||
|
SysAttrFlags::CAN_READ | SysAttrFlags::CAN_WRITE,
|
||||||
|
);
|
||||||
|
|
||||||
|
let attrs = builder.build().expect("Failed to build attribute set");
|
||||||
|
let name_owned: SysStr = name.to_string().into();
|
||||||
|
let fields = SysBranchNodeFields::new(name_owned, attrs);
|
||||||
|
|
||||||
|
Arc::new_cyclic(|weak_self| DeviceNode {
|
||||||
|
fields,
|
||||||
|
self_ref: weak_self.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SysObj for DeviceNode {
|
||||||
|
fn as_any(&self) -> &dyn Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn arc_as_node(&self) -> Option<Arc<dyn SysNode>> {
|
||||||
|
self.self_ref
|
||||||
|
.upgrade()
|
||||||
|
.map(|arc_self| arc_self as Arc<dyn SysNode>)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn arc_as_branch(&self) -> Option<Arc<dyn SysBranchNode>> {
|
||||||
|
self.self_ref
|
||||||
|
.upgrade()
|
||||||
|
.map(|arc_self| arc_self as Arc<dyn SysBranchNode>)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn id(&self) -> &SysNodeId {
|
||||||
|
self.fields.id()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn type_(&self) -> SysNodeType {
|
||||||
|
SysNodeType::Branch
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> SysStr {
|
||||||
|
self.fields.name().to_string().into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SysNode for DeviceNode {
|
||||||
|
fn node_attrs(&self) -> &SysAttrSet {
|
||||||
|
self.fields.attr_set()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_attr(&self, name: &str, writer: &mut VmWriter) -> Result<usize> {
|
||||||
|
// Check if attribute exists
|
||||||
|
if !self.fields.attr_set().contains(name) {
|
||||||
|
return Err(Error::AttributeError);
|
||||||
|
}
|
||||||
|
|
||||||
|
let attr = self.fields.attr_set().get(name).unwrap();
|
||||||
|
// Check if attribute is readable
|
||||||
|
if !attr.flags().contains(SysAttrFlags::CAN_READ) {
|
||||||
|
return Err(Error::PermissionDenied);
|
||||||
|
}
|
||||||
|
let value = match name {
|
||||||
|
"model" => "MyDevice",
|
||||||
|
"vendor" => "ExampleVendor",
|
||||||
|
"status" => "online",
|
||||||
|
_ => "",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Write the value to the provided writer
|
||||||
|
writer
|
||||||
|
.write_fallible(&mut (value.as_bytes()).into())
|
||||||
|
.map_err(|_| Error::AttributeError)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_attr(&self, name: &str, reader: &mut VmReader) -> Result<usize> {
|
||||||
|
// Get attribute and check if it exists
|
||||||
|
let attr = self
|
||||||
|
.fields
|
||||||
|
.attr_set()
|
||||||
|
.get(name)
|
||||||
|
.ok_or(Error::AttributeError)?;
|
||||||
|
|
||||||
|
// Check if attribute is writable
|
||||||
|
if !attr.flags().contains(SysAttrFlags::CAN_WRITE) {
|
||||||
|
return Err(Error::PermissionDenied);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read new value from the provided reader
|
||||||
|
let mut buffer = [0u8; 256];
|
||||||
|
let mut writer = VmWriter::from(&mut buffer[..]);
|
||||||
|
let read_len = reader
|
||||||
|
.read_fallible(&mut writer)
|
||||||
|
.map_err(|_| Error::AttributeError)?;
|
||||||
|
|
||||||
|
Ok(read_len)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SysBranchNode for DeviceNode {
|
||||||
|
fn visit_child_with(&self, name: &str, f: &mut dyn FnMut(Option<&dyn SysNode>)) {
|
||||||
|
let children = self.fields.children.read();
|
||||||
|
children
|
||||||
|
.get(name)
|
||||||
|
.map(|child| {
|
||||||
|
child
|
||||||
|
.arc_as_node()
|
||||||
|
.map(|node| f(Some(node.as_ref())))
|
||||||
|
.unwrap_or_else(|| f(None))
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| f(None));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_children_with(&self, min_id: u64, f: &mut dyn FnMut(&Arc<dyn SysObj>) -> Option<()>) {
|
||||||
|
let children = self.fields.children.read();
|
||||||
|
for child in children
|
||||||
|
.values()
|
||||||
|
.filter(|child| child.id().as_u64() >= min_id)
|
||||||
|
{
|
||||||
|
if f(child).is_none() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn child(&self, name: &str) -> Option<Arc<dyn SysObj>> {
|
||||||
|
let children = self.fields.children.read();
|
||||||
|
children.get(name).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn children(&self) -> Vec<Arc<dyn SysObj>> {
|
||||||
|
let children = self.fields.children.read();
|
||||||
|
children.values().cloned().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn count_children(&self) -> usize {
|
||||||
|
self.fields.children.read().len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct SymlinkNode {
|
||||||
|
id: SysNodeId,
|
||||||
|
name: SysStr,
|
||||||
|
target: String,
|
||||||
|
self_ref: Weak<Self>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SymlinkNode {
|
||||||
|
fn new(name: &str, target: &str) -> Arc<Self> {
|
||||||
|
Arc::new_cyclic(|weak_self| SymlinkNode {
|
||||||
|
id: SysNodeId::new(),
|
||||||
|
name: name.to_string().into(),
|
||||||
|
target: target.to_string(),
|
||||||
|
self_ref: weak_self.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SysObj for SymlinkNode {
|
||||||
|
fn as_any(&self) -> &dyn Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn arc_as_symlink(&self) -> Option<Arc<dyn SysSymlink>> {
|
||||||
|
self.self_ref
|
||||||
|
.upgrade()
|
||||||
|
.map(|arc_self| arc_self as Arc<dyn SysSymlink>)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn id(&self) -> &SysNodeId {
|
||||||
|
&self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn type_(&self) -> SysNodeType {
|
||||||
|
SysNodeType::Symlink
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> SysStr {
|
||||||
|
self.name.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SysSymlink for SymlinkNode {
|
||||||
|
fn target_path(&self) -> &str {
|
||||||
|
&self.target
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ktest]
|
||||||
|
fn systree_singleton() {
|
||||||
|
// Get the SysTree singleton
|
||||||
|
let sys_tree = SysTree::new();
|
||||||
|
|
||||||
|
// Access the root node
|
||||||
|
let root = sys_tree.root();
|
||||||
|
|
||||||
|
// Check if root node exists
|
||||||
|
assert!(root.is_root());
|
||||||
|
assert_eq!(root.name(), "");
|
||||||
|
assert_eq!(root.type_(), SysNodeType::Leaf);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ktest]
|
||||||
|
fn node_hierarchy() {
|
||||||
|
// Create device node hierarchy
|
||||||
|
let root_device = DeviceNode::new("root_device");
|
||||||
|
|
||||||
|
// Add child nodes
|
||||||
|
{
|
||||||
|
let child1 = DeviceNode::new("child1");
|
||||||
|
let child2 = DeviceNode::new("child2");
|
||||||
|
root_device
|
||||||
|
.fields
|
||||||
|
.children
|
||||||
|
.write()
|
||||||
|
.insert(Cow::Borrowed("child1"), child1);
|
||||||
|
root_device
|
||||||
|
.fields
|
||||||
|
.children
|
||||||
|
.write()
|
||||||
|
.insert(Cow::Borrowed("child2"), child2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify number of child nodes
|
||||||
|
assert_eq!(root_device.fields.children.read().len(), 2);
|
||||||
|
|
||||||
|
// Get specific child node
|
||||||
|
let child = root_device.child("child1").unwrap();
|
||||||
|
assert_eq!(child.name(), "child1");
|
||||||
|
|
||||||
|
// Traverse all child nodes
|
||||||
|
let all_children: Vec<_> = root_device.children();
|
||||||
|
assert_eq!(all_children.len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ktest]
|
||||||
|
fn attributes() {
|
||||||
|
let device = DeviceNode::new("test_device");
|
||||||
|
|
||||||
|
// Read read-only attribute
|
||||||
|
let model = device.show_attr("model").unwrap();
|
||||||
|
assert_eq!(model, "MyDevice");
|
||||||
|
|
||||||
|
// Read read-write attribute
|
||||||
|
let status = device.show_attr("status").unwrap();
|
||||||
|
assert_eq!(status, "online");
|
||||||
|
|
||||||
|
// Modify read-write attribute
|
||||||
|
let len = device.store_attr("status", "offline").unwrap();
|
||||||
|
assert_eq!(len, 7);
|
||||||
|
|
||||||
|
// Attempt to modify read-only attribute (should fail)
|
||||||
|
let result = device.store_attr("model", "NewModel");
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ktest]
|
||||||
|
fn symlinks() {
|
||||||
|
let device = DeviceNode::new("device");
|
||||||
|
|
||||||
|
// Create symlink pointing to device
|
||||||
|
let symlink = SymlinkNode::new("device_link", "/sys/devices/device");
|
||||||
|
|
||||||
|
// Verify symlink attributes
|
||||||
|
assert_eq!(symlink.type_(), SysNodeType::Symlink);
|
||||||
|
assert_eq!(symlink.target_path(), "/sys/devices/device");
|
||||||
|
|
||||||
|
// Add symlink to device tree
|
||||||
|
{
|
||||||
|
device
|
||||||
|
.fields
|
||||||
|
.children
|
||||||
|
.write()
|
||||||
|
.insert(Cow::Borrowed("device_link"), symlink.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify symlink was added correctly
|
||||||
|
let symlink_obj = device.child("device_link").unwrap();
|
||||||
|
let symlink_node = symlink_obj.arc_as_symlink().unwrap();
|
||||||
|
assert_eq!(symlink_node.target_path(), "/sys/devices/device");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ktest]
|
||||||
|
fn error_handling() {
|
||||||
|
let device = DeviceNode::new("error_test");
|
||||||
|
|
||||||
|
// Attempt to access non-existent attribute
|
||||||
|
let result = device.show_attr("nonexistent");
|
||||||
|
match result {
|
||||||
|
Err(Error::AttributeError) => (),
|
||||||
|
_ => panic!("Failed to handle non-existent attribute error"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to access non-existent child node
|
||||||
|
let child = device.child("nonexistent");
|
||||||
|
assert!(child.is_none());
|
||||||
|
}
|
641
kernel/src/fs/sysfs/test.rs
Normal file
641
kernel/src/fs/sysfs/test.rs
Normal file
@ -0,0 +1,641 @@
|
|||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
use alloc::{
|
||||||
|
borrow::Cow,
|
||||||
|
collections::BTreeMap,
|
||||||
|
format,
|
||||||
|
string::{String, ToString},
|
||||||
|
sync::{Arc, Weak},
|
||||||
|
vec,
|
||||||
|
vec::Vec,
|
||||||
|
};
|
||||||
|
use core::{any::Any, fmt::Debug};
|
||||||
|
|
||||||
|
use ostd::{
|
||||||
|
mm::{FallibleVmRead, FallibleVmWrite, VmReader, VmWriter},
|
||||||
|
prelude::ktest,
|
||||||
|
sync::RwLock,
|
||||||
|
};
|
||||||
|
use systree::{
|
||||||
|
init_for_ktest, singleton as systree_singleton, Error as SysTreeError, Result as SysTreeResult,
|
||||||
|
SysAttrFlags, SysAttrSet, SysAttrSetBuilder, SysBranchNode, SysBranchNodeFields, SysNode,
|
||||||
|
SysNodeId, SysNodeType, SysNormalNodeFields, SysObj, SysStr, SysSymlink, SysTree,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
fs::{
|
||||||
|
sysfs::fs::SysFs,
|
||||||
|
utils::{DirentVisitor, FileSystem, InodeMode, InodeType},
|
||||||
|
},
|
||||||
|
time::clocks::init_for_ktest as time_init_for_ktest,
|
||||||
|
Result,
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Mock SysTree Components ---
|
||||||
|
// Sysfs acts as a view layer over the systree component.
|
||||||
|
// These mocks simulate the systree interface (SysNode, SysBranchNode, etc.)
|
||||||
|
|
||||||
|
// Refactor MockLeafNode to use SysNormalNodeFields
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct MockLeafNode {
|
||||||
|
fields: SysNormalNodeFields,
|
||||||
|
data: RwLock<BTreeMap<String, String>>, // Store attribute data
|
||||||
|
self_ref: Weak<Self>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MockLeafNode {
|
||||||
|
fn new(name: &str, read_attrs: &[&str], write_attrs: &[&str]) -> Arc<Self> {
|
||||||
|
let name_owned: SysStr = name.to_string().into(); // Convert to owned SysStr
|
||||||
|
|
||||||
|
let mut builder = SysAttrSetBuilder::new();
|
||||||
|
let mut data = BTreeMap::new();
|
||||||
|
for &attr_name in read_attrs {
|
||||||
|
builder.add(Cow::Owned(attr_name.to_string()), SysAttrFlags::CAN_READ);
|
||||||
|
data.insert(attr_name.to_string(), format!("val_{}", attr_name)); // Initial value
|
||||||
|
}
|
||||||
|
for &attr_name in write_attrs {
|
||||||
|
builder.add(
|
||||||
|
Cow::Owned(attr_name.to_string()),
|
||||||
|
SysAttrFlags::CAN_READ | SysAttrFlags::CAN_WRITE,
|
||||||
|
);
|
||||||
|
data.insert(attr_name.to_string(), format!("val_{}", attr_name)); // Initial value
|
||||||
|
}
|
||||||
|
|
||||||
|
let attrs = builder.build().expect("Failed to build attribute set");
|
||||||
|
let fields = SysNormalNodeFields::new(name_owned, attrs);
|
||||||
|
|
||||||
|
Arc::new_cyclic(|weak_self| MockLeafNode {
|
||||||
|
fields,
|
||||||
|
data: RwLock::new(data),
|
||||||
|
self_ref: weak_self.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SysObj for MockLeafNode {
|
||||||
|
fn as_any(&self) -> &dyn Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
fn arc_as_node(&self) -> Option<Arc<dyn SysNode>> {
|
||||||
|
self.self_ref
|
||||||
|
.upgrade()
|
||||||
|
.map(|arc_self| arc_self as Arc<dyn SysNode>)
|
||||||
|
}
|
||||||
|
fn id(&self) -> &SysNodeId {
|
||||||
|
self.fields.id()
|
||||||
|
}
|
||||||
|
fn type_(&self) -> SysNodeType {
|
||||||
|
SysNodeType::Leaf
|
||||||
|
}
|
||||||
|
fn name(&self) -> SysStr {
|
||||||
|
Cow::Owned(self.fields.name().to_string()) // Convert to Cow::Owned
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SysNode for MockLeafNode {
|
||||||
|
fn node_attrs(&self) -> &SysAttrSet {
|
||||||
|
self.fields.attr_set()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_attr(&self, name: &str, writer: &mut VmWriter) -> SysTreeResult<usize> {
|
||||||
|
let attr = self
|
||||||
|
.fields
|
||||||
|
.attr_set()
|
||||||
|
.get(name)
|
||||||
|
.ok_or(SysTreeError::AttributeError)?;
|
||||||
|
if !attr.flags().contains(SysAttrFlags::CAN_READ) {
|
||||||
|
return Err(SysTreeError::PermissionDenied);
|
||||||
|
}
|
||||||
|
let data = self.data.read();
|
||||||
|
let value = data.get(name).ok_or(SysTreeError::AttributeError)?; // Should exist if in attrs
|
||||||
|
let bytes = value.as_bytes();
|
||||||
|
writer
|
||||||
|
.write_fallible(&mut (&bytes[..]).into())
|
||||||
|
.map_err(|_| SysTreeError::AttributeError)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_attr(&self, name: &str, reader: &mut VmReader) -> SysTreeResult<usize> {
|
||||||
|
let attr = self
|
||||||
|
.fields
|
||||||
|
.attr_set()
|
||||||
|
.get(name)
|
||||||
|
.ok_or(SysTreeError::AttributeError)?;
|
||||||
|
if !attr.flags().contains(SysAttrFlags::CAN_WRITE) {
|
||||||
|
return Err(SysTreeError::PermissionDenied);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut buffer = [0u8; 1024]; // Max write size for test
|
||||||
|
let mut writer = VmWriter::from(&mut buffer[..]);
|
||||||
|
let read_len = reader
|
||||||
|
.read_fallible(&mut writer)
|
||||||
|
.map_err(|_| SysTreeError::AttributeError)?;
|
||||||
|
|
||||||
|
let new_value = String::from_utf8_lossy(&buffer[..read_len]).to_string();
|
||||||
|
|
||||||
|
let mut data = self.data.write();
|
||||||
|
data.insert(name.to_string(), new_value);
|
||||||
|
|
||||||
|
Ok(read_len)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refactor MockBranchNode to use SysBranchNodeFields
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct MockBranchNode {
|
||||||
|
fields: SysBranchNodeFields<dyn SysObj>,
|
||||||
|
self_ref: Weak<Self>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MockBranchNode {
|
||||||
|
fn new(name: &str) -> Arc<Self> {
|
||||||
|
let name_owned: SysStr = name.to_string().into(); // Convert to owned SysStr
|
||||||
|
|
||||||
|
let mut builder = SysAttrSetBuilder::new();
|
||||||
|
builder.add(Cow::Borrowed("branch_attr"), SysAttrFlags::CAN_READ);
|
||||||
|
let attrs = builder
|
||||||
|
.build()
|
||||||
|
.expect("Failed to build branch attribute set");
|
||||||
|
|
||||||
|
let fields = SysBranchNodeFields::new(name_owned, attrs);
|
||||||
|
|
||||||
|
Arc::new_cyclic(|weak_self| MockBranchNode {
|
||||||
|
fields,
|
||||||
|
self_ref: weak_self.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_child(&self, child: Arc<dyn SysObj>) {
|
||||||
|
self.fields.add_child(child).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SysObj for MockBranchNode {
|
||||||
|
fn as_any(&self) -> &dyn Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
fn arc_as_node(&self) -> Option<Arc<dyn SysNode>> {
|
||||||
|
self.self_ref
|
||||||
|
.upgrade()
|
||||||
|
.map(|arc_self| arc_self as Arc<dyn SysNode>)
|
||||||
|
}
|
||||||
|
fn arc_as_branch(&self) -> Option<Arc<dyn SysBranchNode>> {
|
||||||
|
self.self_ref
|
||||||
|
.upgrade()
|
||||||
|
.map(|arc_self| arc_self as Arc<dyn SysBranchNode>)
|
||||||
|
}
|
||||||
|
fn id(&self) -> &SysNodeId {
|
||||||
|
self.fields.id()
|
||||||
|
}
|
||||||
|
fn type_(&self) -> SysNodeType {
|
||||||
|
SysNodeType::Branch
|
||||||
|
}
|
||||||
|
fn name(&self) -> SysStr {
|
||||||
|
Cow::Owned(self.fields.name().to_string()) // Convert to Cow::Owned
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SysNode for MockBranchNode {
|
||||||
|
fn node_attrs(&self) -> &SysAttrSet {
|
||||||
|
self.fields.attr_set()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_attr(&self, name: &str, writer: &mut VmWriter) -> SysTreeResult<usize> {
|
||||||
|
let attr = self
|
||||||
|
.fields
|
||||||
|
.attr_set()
|
||||||
|
.get(name)
|
||||||
|
.ok_or(SysTreeError::AttributeError)?;
|
||||||
|
if !attr.flags().contains(SysAttrFlags::CAN_READ) {
|
||||||
|
return Err(SysTreeError::PermissionDenied);
|
||||||
|
}
|
||||||
|
let value = match name {
|
||||||
|
"branch_attr" => "branch_value",
|
||||||
|
_ => return Err(SysTreeError::AttributeError),
|
||||||
|
};
|
||||||
|
let bytes = value.as_bytes();
|
||||||
|
writer
|
||||||
|
.write_fallible(&mut (&bytes[..]).into())
|
||||||
|
.map_err(|_| SysTreeError::AttributeError)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_attr(&self, name: &str, _reader: &mut VmReader) -> SysTreeResult<usize> {
|
||||||
|
let attr = self
|
||||||
|
.fields
|
||||||
|
.attr_set()
|
||||||
|
.get(name)
|
||||||
|
.ok_or(SysTreeError::AttributeError)?;
|
||||||
|
if !attr.flags().contains(SysAttrFlags::CAN_WRITE) {
|
||||||
|
return Err(SysTreeError::PermissionDenied);
|
||||||
|
}
|
||||||
|
// No writable attrs in this mock for now
|
||||||
|
Err(SysTreeError::AttributeError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SysBranchNode for MockBranchNode {
|
||||||
|
fn visit_child_with(&self, name: &str, f: &mut dyn FnMut(Option<&dyn SysNode>)) {
|
||||||
|
self.fields
|
||||||
|
.children
|
||||||
|
.read()
|
||||||
|
.get(name)
|
||||||
|
.map(|child| {
|
||||||
|
child
|
||||||
|
.arc_as_node()
|
||||||
|
.map(|node| f(Some(node.as_ref())))
|
||||||
|
.unwrap_or_else(|| f(None));
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| f(None));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_children_with(&self, min_id: u64, f: &mut dyn FnMut(&Arc<dyn SysObj>) -> Option<()>) {
|
||||||
|
let children = self.fields.children.read();
|
||||||
|
for child in children
|
||||||
|
.values()
|
||||||
|
.filter(|child| child.id().as_u64() >= min_id)
|
||||||
|
{
|
||||||
|
if f(child).is_none() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn child(&self, name: &str) -> Option<Arc<dyn SysObj>> {
|
||||||
|
let children = self.fields.children.read();
|
||||||
|
children.get(name).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn children(&self) -> Vec<Arc<dyn SysObj>> {
|
||||||
|
self.fields.children.read().values().cloned().collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock Symlink
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct MockSymlinkNode {
|
||||||
|
id: SysNodeId,
|
||||||
|
name: SysStr,
|
||||||
|
target: String,
|
||||||
|
self_ref: Weak<Self>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MockSymlinkNode {
|
||||||
|
fn new(name: &str, target: &str) -> Arc<Self> {
|
||||||
|
Arc::new_cyclic(|weak_self| MockSymlinkNode {
|
||||||
|
id: SysNodeId::new(),
|
||||||
|
name: name.to_string().into(),
|
||||||
|
target: target.to_string(),
|
||||||
|
self_ref: weak_self.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SysObj for MockSymlinkNode {
|
||||||
|
fn as_any(&self) -> &dyn Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
fn arc_as_symlink(&self) -> Option<Arc<dyn SysSymlink>> {
|
||||||
|
self.self_ref
|
||||||
|
.upgrade()
|
||||||
|
.map(|arc_self| arc_self as Arc<dyn SysSymlink>)
|
||||||
|
}
|
||||||
|
fn id(&self) -> &SysNodeId {
|
||||||
|
&self.id
|
||||||
|
}
|
||||||
|
fn type_(&self) -> SysNodeType {
|
||||||
|
SysNodeType::Symlink
|
||||||
|
}
|
||||||
|
fn name(&self) -> SysStr {
|
||||||
|
self.name.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SysSymlink for MockSymlinkNode {
|
||||||
|
fn target_path(&self) -> &str {
|
||||||
|
&self.target
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Test Setup ---
|
||||||
|
|
||||||
|
// Create a mock SysTree instance populated with mock nodes.
|
||||||
|
fn create_mock_systree_instance() -> &'static Arc<SysTree> {
|
||||||
|
time_init_for_ktest();
|
||||||
|
init_for_ktest();
|
||||||
|
// Create nodes
|
||||||
|
let root = systree_singleton().root();
|
||||||
|
let branch1 = MockBranchNode::new("branch1");
|
||||||
|
let leaf1 = MockLeafNode::new("leaf1", &["r_attr1"], &["rw_attr1"]);
|
||||||
|
let leaf2 = MockLeafNode::new("leaf2", &["r_attr2"], &[]);
|
||||||
|
let symlink1 = MockSymlinkNode::new("link1", "../branch1/leaf1");
|
||||||
|
|
||||||
|
// Build hierarchy - ignore Result since this is test setup
|
||||||
|
let _ = branch1.add_child(leaf1.clone() as Arc<dyn SysObj>);
|
||||||
|
let _ = root.add_child(branch1.clone() as Arc<dyn SysObj>);
|
||||||
|
let _ = root.add_child(leaf2.clone() as Arc<dyn SysObj>);
|
||||||
|
let _ = root.add_child(symlink1.clone() as Arc<dyn SysObj>);
|
||||||
|
|
||||||
|
systree_singleton()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize a SysFs instance using the mock systree.
|
||||||
|
fn init_sysfs_with_mock_tree() -> Arc<SysFs> {
|
||||||
|
let _ = create_mock_systree_instance();
|
||||||
|
SysFs::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ktest]
|
||||||
|
fn test_sysfs_root_lookup() {
|
||||||
|
// Setup: Create SysFs instance backed by the mock systree
|
||||||
|
let sysfs = init_sysfs_with_mock_tree();
|
||||||
|
let root_inode = sysfs.root_inode(); // Get the sysfs root inode
|
||||||
|
|
||||||
|
// Verification: Check that the sysfs root inode corresponds to the mock systree root
|
||||||
|
|
||||||
|
assert_eq!(root_inode.type_(), InodeType::Dir);
|
||||||
|
|
||||||
|
// Lookup existing branch
|
||||||
|
let branch1_inode = root_inode.lookup("branch1").expect("Lookup branch1 failed");
|
||||||
|
assert_eq!(branch1_inode.type_(), InodeType::Dir);
|
||||||
|
|
||||||
|
// Lookup existing leaf (represented as Dir in sysfs)
|
||||||
|
let leaf2_inode = root_inode.lookup("leaf2").expect("Lookup leaf2 failed");
|
||||||
|
assert_eq!(leaf2_inode.type_(), InodeType::Dir);
|
||||||
|
|
||||||
|
// Lookup existing symlink
|
||||||
|
let link1_inode = root_inode.lookup("link1").expect("Lookup link1 failed");
|
||||||
|
assert_eq!(link1_inode.type_(), InodeType::SymLink);
|
||||||
|
|
||||||
|
// Lookup non-existent
|
||||||
|
let result = root_inode.lookup("nonexistent");
|
||||||
|
assert!(result.is_err());
|
||||||
|
|
||||||
|
// Lookup "."
|
||||||
|
let self_inode = root_inode.lookup(".").expect("Lookup . failed");
|
||||||
|
assert_eq!(self_inode.ino(), root_inode.ino());
|
||||||
|
|
||||||
|
// Lookup ".." from root
|
||||||
|
let parent_inode = root_inode.lookup("..").expect("Lookup .. failed");
|
||||||
|
assert_eq!(parent_inode.ino(), root_inode.ino()); // Parent of root is root
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ktest]
|
||||||
|
fn test_sysfs_branch_lookup() {
|
||||||
|
let sysfs = init_sysfs_with_mock_tree();
|
||||||
|
let root_inode = sysfs.root_inode();
|
||||||
|
// Action: Lookup a branch node within sysfs
|
||||||
|
let branch1_inode = root_inode.lookup("branch1").unwrap();
|
||||||
|
|
||||||
|
// Verification: Check lookups within the sysfs branch inode,
|
||||||
|
// ensuring they correctly reflect the children and attributes of the underlying mock systree branch node.
|
||||||
|
|
||||||
|
// Lookup existing leaf inside branch
|
||||||
|
let leaf1_inode = branch1_inode.lookup("leaf1").expect("Lookup leaf1 failed");
|
||||||
|
assert_eq!(leaf1_inode.type_(), InodeType::Dir); // Leaf nodes are Dirs
|
||||||
|
|
||||||
|
// Lookup branch attribute
|
||||||
|
let attr_inode = branch1_inode
|
||||||
|
.lookup("branch_attr")
|
||||||
|
.expect("Lookup branch_attr failed");
|
||||||
|
assert_eq!(attr_inode.type_(), InodeType::File);
|
||||||
|
|
||||||
|
// Lookup non-existent inside branch
|
||||||
|
let result = branch1_inode.lookup("nonexistent_leaf");
|
||||||
|
assert!(result.is_err());
|
||||||
|
|
||||||
|
// Lookup "."
|
||||||
|
let self_inode = branch1_inode.lookup(".").expect("Lookup . failed");
|
||||||
|
assert_eq!(self_inode.ino(), branch1_inode.ino());
|
||||||
|
|
||||||
|
// Lookup ".."
|
||||||
|
let parent_inode = branch1_inode.lookup("..").expect("Lookup .. failed");
|
||||||
|
assert_eq!(parent_inode.ino(), root_inode.ino());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ktest]
|
||||||
|
fn test_sysfs_leaf_lookup() {
|
||||||
|
let sysfs = init_sysfs_with_mock_tree();
|
||||||
|
let root_inode = sysfs.root_inode();
|
||||||
|
// Action: Lookup a leaf node (represented as a directory in sysfs)
|
||||||
|
let leaf1_inode = root_inode
|
||||||
|
.lookup("branch1")
|
||||||
|
.unwrap()
|
||||||
|
.lookup("leaf1")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Verification: Check lookups within the sysfs leaf directory,
|
||||||
|
// ensuring they correctly reflect the attributes of the underlying mock systree leaf node.
|
||||||
|
|
||||||
|
assert_eq!(leaf1_inode.type_(), InodeType::Dir); // Leaf node itself is Dir
|
||||||
|
|
||||||
|
// Lookup existing readable attribute
|
||||||
|
let r_attr_inode = leaf1_inode
|
||||||
|
.lookup("r_attr1")
|
||||||
|
.expect("Lookup r_attr1 failed");
|
||||||
|
assert_eq!(r_attr_inode.type_(), InodeType::File);
|
||||||
|
|
||||||
|
// Lookup existing read-write attribute
|
||||||
|
let rw_attr_inode = leaf1_inode
|
||||||
|
.lookup("rw_attr1")
|
||||||
|
.expect("Lookup rw_attr1 failed");
|
||||||
|
assert_eq!(rw_attr_inode.type_(), InodeType::File);
|
||||||
|
|
||||||
|
// Lookup non-existent attribute
|
||||||
|
let result = leaf1_inode.lookup("nonexistent_attr");
|
||||||
|
assert!(result.is_err());
|
||||||
|
|
||||||
|
// Lookup "."
|
||||||
|
let self_inode = leaf1_inode.lookup(".").expect("Lookup . failed");
|
||||||
|
assert_eq!(self_inode.ino(), leaf1_inode.ino());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ktest]
|
||||||
|
fn test_sysfs_read_attr() {
|
||||||
|
let sysfs = init_sysfs_with_mock_tree();
|
||||||
|
let root_inode = sysfs.root_inode();
|
||||||
|
let leaf1_dir_inode = root_inode
|
||||||
|
.lookup("branch1")
|
||||||
|
.unwrap()
|
||||||
|
.lookup("leaf1")
|
||||||
|
.unwrap();
|
||||||
|
// Action: Lookup the sysfs file corresponding to a systree attribute
|
||||||
|
let r_attr_inode = leaf1_dir_inode.lookup("r_attr1").unwrap();
|
||||||
|
|
||||||
|
// Verification: Read the sysfs file and check if the content matches
|
||||||
|
// the data provided by the underlying mock systree node's read_attr method.
|
||||||
|
|
||||||
|
let mut buf = [0u8; 64];
|
||||||
|
let mut writer = VmWriter::from(&mut buf[..]).to_fallible();
|
||||||
|
let bytes_read = r_attr_inode
|
||||||
|
.read_at(0, &mut writer)
|
||||||
|
.expect("read_at failed");
|
||||||
|
|
||||||
|
assert!(bytes_read > 0);
|
||||||
|
let content = core::str::from_utf8(&buf[..bytes_read]).unwrap();
|
||||||
|
assert_eq!(content, "val_r_attr1");
|
||||||
|
|
||||||
|
// Reading a directory should fail (expect EINVAL as per inode.rs)
|
||||||
|
let mut writer = VmWriter::from(&mut buf[..]).to_fallible(); // Reset writer
|
||||||
|
let result = leaf1_dir_inode.read_at(0, &mut writer);
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ktest]
|
||||||
|
fn test_sysfs_write_attr() {
|
||||||
|
let sysfs = init_sysfs_with_mock_tree();
|
||||||
|
let root_inode = sysfs.root_inode();
|
||||||
|
let leaf1_dir_inode = root_inode
|
||||||
|
.lookup("branch1")
|
||||||
|
.unwrap()
|
||||||
|
.lookup("leaf1")
|
||||||
|
.unwrap();
|
||||||
|
// Action: Lookup sysfs files for attributes
|
||||||
|
let rw_attr_inode = leaf1_dir_inode.lookup("rw_attr1").unwrap();
|
||||||
|
let r_attr_inode = leaf1_dir_inode.lookup("r_attr1").unwrap();
|
||||||
|
|
||||||
|
// Verification: Write to the sysfs files and check if the operation
|
||||||
|
// is correctly delegated to the underlying mock systree node's write_attr method,
|
||||||
|
// respecting read/write permissions derived from SysAttrFlags.
|
||||||
|
|
||||||
|
// Write to rw_attr1
|
||||||
|
let new_val = "new_value";
|
||||||
|
let mut reader = VmReader::from(new_val.as_bytes()).to_fallible();
|
||||||
|
let bytes_written = rw_attr_inode
|
||||||
|
.write_at(0, &mut reader)
|
||||||
|
.expect("write_at failed");
|
||||||
|
assert_eq!(bytes_written, new_val.len());
|
||||||
|
|
||||||
|
// Read back to verify
|
||||||
|
let mut buf = [0u8; 64];
|
||||||
|
let mut writer = VmWriter::from(&mut buf[..]).to_fallible();
|
||||||
|
let bytes_read = rw_attr_inode
|
||||||
|
.read_at(0, &mut writer)
|
||||||
|
.expect("read_at failed");
|
||||||
|
let content = core::str::from_utf8(&buf[..bytes_read]).unwrap();
|
||||||
|
assert_eq!(content, new_val);
|
||||||
|
|
||||||
|
// Write to r_attr1 (should fail - EIO expected from underlying PermissionDenied)
|
||||||
|
let mut reader = VmReader::from("attempt_write".as_bytes()).to_fallible();
|
||||||
|
let result = r_attr_inode.write_at(0, &mut reader);
|
||||||
|
assert!(result.is_err());
|
||||||
|
|
||||||
|
// Writing to a directory should fail (expect EINVAL as per inode.rs)
|
||||||
|
let mut reader = VmReader::from("attempt_write".as_bytes()).to_fallible();
|
||||||
|
let result = leaf1_dir_inode.write_at(0, &mut reader);
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ktest]
|
||||||
|
fn test_sysfs_read_link() {
|
||||||
|
let sysfs = init_sysfs_with_mock_tree();
|
||||||
|
let root_inode = sysfs.root_inode();
|
||||||
|
// Action: Lookup the sysfs symlink corresponding to a systree symlink node
|
||||||
|
let link1_inode = root_inode.lookup("link1").unwrap();
|
||||||
|
|
||||||
|
// Verification: Read the sysfs symlink and check if the target path matches
|
||||||
|
// the path provided by the underlying mock systree symlink node's target_path method.
|
||||||
|
|
||||||
|
let target = link1_inode.read_link().expect("read_link failed");
|
||||||
|
assert_eq!(target, "../branch1/leaf1");
|
||||||
|
|
||||||
|
// read_link on non-symlink should fail (expect EINVAL as per inode.rs)
|
||||||
|
let branch1_inode = root_inode.lookup("branch1").unwrap();
|
||||||
|
let result = branch1_inode.read_link();
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper for readdir tests
|
||||||
|
struct TestDirentVisitor {
|
||||||
|
entries: Vec<(String, u64, InodeType)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DirentVisitor for TestDirentVisitor {
|
||||||
|
fn visit(&mut self, name: &str, ino: u64, type_: InodeType, _next_offset: usize) -> Result<()> {
|
||||||
|
self.entries.push((name.to_string(), ino, type_));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ktest]
|
||||||
|
fn test_sysfs_readdir_leaf() {
|
||||||
|
let sysfs = init_sysfs_with_mock_tree();
|
||||||
|
let root_inode = sysfs.root_inode();
|
||||||
|
let leaf1_inode = root_inode
|
||||||
|
.lookup("branch1")
|
||||||
|
.unwrap()
|
||||||
|
.lookup("leaf1")
|
||||||
|
.unwrap(); // The sysfs dir for the leaf node
|
||||||
|
let mut visitor = TestDirentVisitor { entries: vec![] };
|
||||||
|
|
||||||
|
// Action: Read directory entries from the sysfs directory representing a systree leaf node
|
||||||
|
|
||||||
|
let mut offset = 0;
|
||||||
|
loop {
|
||||||
|
// Pass offset as usize
|
||||||
|
let result = leaf1_inode.readdir_at(offset, &mut visitor);
|
||||||
|
match result {
|
||||||
|
Ok(next_offset) => {
|
||||||
|
if next_offset == offset || next_offset == 0 {
|
||||||
|
// Check if no progress or end
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
offset = next_offset;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
panic!("readdir_at failed unexpectedly: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut names: Vec<_> = visitor.entries.iter().map(|(n, _, _)| n.clone()).collect();
|
||||||
|
names.sort();
|
||||||
|
|
||||||
|
assert!(names.contains(&".".to_string()));
|
||||||
|
assert!(names.contains(&"..".to_string()));
|
||||||
|
assert!(names.contains(&"r_attr1".to_string()));
|
||||||
|
assert!(names.contains(&"rw_attr1".to_string()));
|
||||||
|
|
||||||
|
for (name, _, type_) in &visitor.entries {
|
||||||
|
match name.as_str() {
|
||||||
|
"." | ".." => assert_eq!(*type_, InodeType::Dir),
|
||||||
|
"r_attr1" | "rw_attr1" => assert_eq!(*type_, InodeType::File),
|
||||||
|
_ => panic!("Unexpected entry: {}", name),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ktest]
|
||||||
|
fn test_sysfs_mode_permissions() {
|
||||||
|
let sysfs = init_sysfs_with_mock_tree();
|
||||||
|
let root_inode = sysfs.root_inode();
|
||||||
|
let leaf1_dir_inode = root_inode
|
||||||
|
.lookup("branch1")
|
||||||
|
.unwrap()
|
||||||
|
.lookup("leaf1")
|
||||||
|
.unwrap();
|
||||||
|
let r_attr_inode = leaf1_dir_inode.lookup("r_attr1").unwrap(); // Sysfs file for read-only attr
|
||||||
|
let rw_attr_inode = leaf1_dir_inode.lookup("rw_attr1").unwrap(); // Sysfs file for read-write attr
|
||||||
|
|
||||||
|
// Verification: Check that the default mode (permissions) of the sysfs files/dirs
|
||||||
|
// correctly reflects the SysAttrFlags of the underlying systree attributes/nodes.
|
||||||
|
// Also test that set_mode works on the sysfs inode.
|
||||||
|
|
||||||
|
// Check default modes based on SysAttrFlags
|
||||||
|
let r_mode = r_attr_inode.mode().unwrap();
|
||||||
|
assert!(r_mode.contains(InodeMode::S_IRUSR | InodeMode::S_IRGRP | InodeMode::S_IROTH)); // 0o444
|
||||||
|
assert!(!r_mode.contains(InodeMode::S_IWUSR | InodeMode::S_IWGRP | InodeMode::S_IWOTH)); // Not 0o222
|
||||||
|
|
||||||
|
let rw_mode = rw_attr_inode.mode().unwrap();
|
||||||
|
assert!(rw_mode.contains(InodeMode::S_IRUSR | InodeMode::S_IRGRP | InodeMode::S_IROTH)); // 0o444
|
||||||
|
assert!(rw_mode.contains(InodeMode::S_IWUSR | InodeMode::S_IWGRP | InodeMode::S_IWOTH)); // 0o222
|
||||||
|
|
||||||
|
// Test set_mode
|
||||||
|
let new_mode = InodeMode::from_bits_truncate(0o600); // rw-------
|
||||||
|
rw_attr_inode.set_mode(new_mode).expect("set_mode failed");
|
||||||
|
assert_eq!(rw_attr_inode.mode().unwrap(), new_mode);
|
||||||
|
|
||||||
|
// Directories should have default mode (e.g., 0o555)
|
||||||
|
let leaf1_mode = leaf1_dir_inode.mode().unwrap();
|
||||||
|
assert!(leaf1_mode.contains(InodeMode::S_IRUSR | InodeMode::S_IXUSR)); // Read/execute for user
|
||||||
|
assert!(leaf1_mode.contains(InodeMode::S_IRGRP | InodeMode::S_IXGRP)); // Read/execute for group
|
||||||
|
assert!(leaf1_mode.contains(InodeMode::S_IROTH | InodeMode::S_IXOTH)); // Read/execute for other
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user