652 lines
20 KiB
Rust
652 lines
20 KiB
Rust
//! Integration tests for Runtime repository
|
|
//!
|
|
//! Tests cover CRUD operations, specialized queries, constraints,
|
|
//! enum handling, timestamps, and edge cases.
|
|
|
|
use attune_common::repositories::runtime::{
|
|
CreateRuntimeInput, RuntimeRepository, UpdateRuntimeInput,
|
|
};
|
|
use attune_common::repositories::{Create, Delete, FindById, FindByRef, List, Patch, Update};
|
|
use serde_json::json;
|
|
use sqlx::PgPool;
|
|
use std::collections::hash_map::DefaultHasher;
|
|
use std::hash::{Hash, Hasher};
|
|
use std::sync::atomic::{AtomicU64, Ordering};
|
|
|
|
mod helpers;
|
|
use helpers::create_test_pool;
|
|
|
|
// Global counter for unique IDs across all tests
|
|
static GLOBAL_COUNTER: AtomicU64 = AtomicU64::new(0);
|
|
|
|
/// Test fixture for creating unique runtime data
|
|
struct RuntimeFixture {
|
|
sequence: AtomicU64,
|
|
test_id: String,
|
|
}
|
|
|
|
impl RuntimeFixture {
|
|
fn new(test_name: &str) -> Self {
|
|
let global_count = GLOBAL_COUNTER.fetch_add(1, Ordering::SeqCst);
|
|
let timestamp = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.unwrap()
|
|
.as_nanos();
|
|
|
|
// Create unique test ID from test name, timestamp, and global counter
|
|
let mut hasher = DefaultHasher::new();
|
|
test_name.hash(&mut hasher);
|
|
timestamp.hash(&mut hasher);
|
|
global_count.hash(&mut hasher);
|
|
let hash = hasher.finish();
|
|
|
|
let test_id = format!("test_{}_{:x}", global_count, hash);
|
|
|
|
Self {
|
|
sequence: AtomicU64::new(0),
|
|
test_id,
|
|
}
|
|
}
|
|
|
|
fn unique_ref(&self, prefix: &str) -> String {
|
|
let seq = self.sequence.fetch_add(1, Ordering::SeqCst);
|
|
format!("{}_{}_ref_{}", prefix, self.test_id, seq)
|
|
}
|
|
|
|
fn create_input(&self, ref_suffix: &str) -> CreateRuntimeInput {
|
|
let seq = self.sequence.fetch_add(1, Ordering::SeqCst);
|
|
let name = format!("test_runtime_{}_{}", ref_suffix, seq);
|
|
let r#ref = format!("{}.{}", self.test_id, name);
|
|
|
|
CreateRuntimeInput {
|
|
r#ref,
|
|
pack: None,
|
|
pack_ref: None,
|
|
description: Some(format!("Test runtime {}", seq)),
|
|
name,
|
|
aliases: vec![],
|
|
distributions: json!({
|
|
"linux": { "supported": true, "versions": ["ubuntu20.04", "ubuntu22.04"] },
|
|
"darwin": { "supported": true, "versions": ["12", "13"] }
|
|
}),
|
|
installation: Some(json!({
|
|
"method": "pip",
|
|
"packages": ["requests", "pyyaml"]
|
|
})),
|
|
execution_config: json!({
|
|
"interpreter": {
|
|
"binary": "python3",
|
|
"args": ["-u"],
|
|
"file_extension": ".py"
|
|
}
|
|
}),
|
|
auto_detected: false,
|
|
detection_config: json!({}),
|
|
}
|
|
}
|
|
|
|
fn create_minimal_input(&self, ref_suffix: &str) -> CreateRuntimeInput {
|
|
let seq = self.sequence.fetch_add(1, Ordering::SeqCst);
|
|
let name = format!("minimal_{}_{}", ref_suffix, seq);
|
|
let r#ref = format!("{}.{}", self.test_id, name);
|
|
|
|
CreateRuntimeInput {
|
|
r#ref,
|
|
pack: None,
|
|
pack_ref: None,
|
|
description: None,
|
|
name,
|
|
aliases: vec![],
|
|
distributions: json!({}),
|
|
installation: None,
|
|
execution_config: json!({
|
|
"interpreter": {
|
|
"binary": "/bin/bash",
|
|
"args": [],
|
|
"file_extension": ".sh"
|
|
}
|
|
}),
|
|
auto_detected: false,
|
|
detection_config: json!({}),
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn setup_db() -> PgPool {
|
|
create_test_pool()
|
|
.await
|
|
.expect("Failed to create test pool")
|
|
}
|
|
|
|
// ============================================================================
|
|
// Basic CRUD Tests
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
#[ignore = "integration test — requires database"]
|
|
async fn test_create_runtime() {
|
|
let pool = setup_db().await;
|
|
let fixture = RuntimeFixture::new("create_runtime");
|
|
let input = fixture.create_input("basic");
|
|
|
|
let runtime = RuntimeRepository::create(&pool, input.clone())
|
|
.await
|
|
.expect("Failed to create runtime");
|
|
|
|
assert!(runtime.id > 0);
|
|
assert_eq!(runtime.r#ref, input.r#ref);
|
|
assert_eq!(runtime.pack, input.pack);
|
|
assert_eq!(runtime.pack_ref, input.pack_ref);
|
|
assert_eq!(runtime.description, input.description);
|
|
assert_eq!(runtime.name, input.name);
|
|
assert_eq!(runtime.distributions, input.distributions);
|
|
assert_eq!(runtime.installation, input.installation);
|
|
assert!(runtime.created > chrono::Utc::now() - chrono::Duration::seconds(5));
|
|
assert!(runtime.updated > chrono::Utc::now() - chrono::Duration::seconds(5));
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore = "integration test — requires database"]
|
|
async fn test_create_runtime_minimal() {
|
|
let pool = setup_db().await;
|
|
let fixture = RuntimeFixture::new("create_runtime_minimal");
|
|
let input = fixture.create_minimal_input("minimal");
|
|
|
|
let runtime = RuntimeRepository::create(&pool, input.clone())
|
|
.await
|
|
.expect("Failed to create minimal runtime");
|
|
|
|
assert!(runtime.id > 0);
|
|
assert_eq!(runtime.r#ref, input.r#ref);
|
|
assert_eq!(runtime.description, None);
|
|
assert_eq!(runtime.pack, None);
|
|
assert_eq!(runtime.pack_ref, None);
|
|
assert_eq!(runtime.installation, None);
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore = "integration test — requires database"]
|
|
async fn test_find_runtime_by_id() {
|
|
let pool = setup_db().await;
|
|
let fixture = RuntimeFixture::new("find_by_id");
|
|
let input = fixture.create_input("findable");
|
|
|
|
let created = RuntimeRepository::create(&pool, input)
|
|
.await
|
|
.expect("Failed to create runtime");
|
|
|
|
let found = RuntimeRepository::find_by_id(&pool, created.id)
|
|
.await
|
|
.expect("Failed to find runtime")
|
|
.expect("Runtime not found");
|
|
|
|
assert_eq!(found.id, created.id);
|
|
assert_eq!(found.r#ref, created.r#ref);
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore = "integration test — requires database"]
|
|
async fn test_find_runtime_by_id_not_found() {
|
|
let pool = setup_db().await;
|
|
|
|
let result = RuntimeRepository::find_by_id(&pool, 999999999)
|
|
.await
|
|
.expect("Query should succeed");
|
|
|
|
assert!(result.is_none());
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore = "integration test — requires database"]
|
|
async fn test_find_runtime_by_ref() {
|
|
let pool = setup_db().await;
|
|
let fixture = RuntimeFixture::new("find_by_ref");
|
|
let input = fixture.create_input("reftest");
|
|
|
|
let created = RuntimeRepository::create(&pool, input.clone())
|
|
.await
|
|
.expect("Failed to create runtime");
|
|
|
|
let found = RuntimeRepository::find_by_ref(&pool, &input.r#ref)
|
|
.await
|
|
.expect("Failed to find runtime")
|
|
.expect("Runtime not found");
|
|
|
|
assert_eq!(found.id, created.id);
|
|
assert_eq!(found.r#ref, created.r#ref);
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore = "integration test — requires database"]
|
|
async fn test_find_runtime_by_ref_not_found() {
|
|
let pool = setup_db().await;
|
|
|
|
let result = RuntimeRepository::find_by_ref(&pool, "nonexistent.ref.999999")
|
|
.await
|
|
.expect("Query should succeed");
|
|
|
|
assert!(result.is_none());
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore = "integration test — requires database"]
|
|
async fn test_list_runtimes() {
|
|
let pool = setup_db().await;
|
|
let fixture = RuntimeFixture::new("list_runtimes");
|
|
|
|
let input1 = fixture.create_input("list1");
|
|
let input2 = fixture.create_input("list2");
|
|
|
|
let created1 = RuntimeRepository::create(&pool, input1)
|
|
.await
|
|
.expect("Failed to create runtime 1");
|
|
let created2 = RuntimeRepository::create(&pool, input2)
|
|
.await
|
|
.expect("Failed to create runtime 2");
|
|
|
|
let list = RuntimeRepository::list(&pool)
|
|
.await
|
|
.expect("Failed to list runtimes");
|
|
|
|
assert!(list.len() >= 2);
|
|
assert!(list.iter().any(|r| r.id == created1.id));
|
|
assert!(list.iter().any(|r| r.id == created2.id));
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore = "integration test — requires database"]
|
|
async fn test_update_runtime() {
|
|
let pool = setup_db().await;
|
|
let fixture = RuntimeFixture::new("update_runtime");
|
|
let input = fixture.create_input("update");
|
|
|
|
let created = RuntimeRepository::create(&pool, input)
|
|
.await
|
|
.expect("Failed to create runtime");
|
|
|
|
let update_input = UpdateRuntimeInput {
|
|
description: Some(Patch::Set("Updated description".to_string())),
|
|
name: Some("updated_name".to_string()),
|
|
distributions: Some(json!({
|
|
"linux": { "supported": false }
|
|
})),
|
|
installation: Some(Patch::Set(json!({
|
|
"method": "npm"
|
|
}))),
|
|
execution_config: None,
|
|
..Default::default()
|
|
};
|
|
|
|
let updated = RuntimeRepository::update(&pool, created.id, update_input.clone())
|
|
.await
|
|
.expect("Failed to update runtime");
|
|
|
|
assert_eq!(updated.id, created.id);
|
|
assert_eq!(updated.description, Some("Updated description".to_string()));
|
|
assert_eq!(updated.name, update_input.name.unwrap());
|
|
assert_eq!(updated.distributions, update_input.distributions.unwrap());
|
|
assert_eq!(updated.installation, Some(json!({ "method": "npm" })));
|
|
assert!(updated.updated > created.updated);
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore = "integration test — requires database"]
|
|
async fn test_update_runtime_partial() {
|
|
let pool = setup_db().await;
|
|
let fixture = RuntimeFixture::new("update_partial");
|
|
let input = fixture.create_input("partial");
|
|
|
|
let created = RuntimeRepository::create(&pool, input)
|
|
.await
|
|
.expect("Failed to create runtime");
|
|
|
|
let update_input = UpdateRuntimeInput {
|
|
description: Some(Patch::Set("Only description changed".to_string())),
|
|
name: None,
|
|
distributions: None,
|
|
installation: None,
|
|
execution_config: None,
|
|
..Default::default()
|
|
};
|
|
|
|
let updated = RuntimeRepository::update(&pool, created.id, update_input.clone())
|
|
.await
|
|
.expect("Failed to update runtime");
|
|
|
|
assert_eq!(
|
|
updated.description,
|
|
Some("Only description changed".to_string())
|
|
);
|
|
assert_eq!(updated.name, created.name);
|
|
assert_eq!(updated.distributions, created.distributions);
|
|
assert_eq!(updated.installation, created.installation);
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore = "integration test — requires database"]
|
|
async fn test_update_runtime_empty() {
|
|
let pool = setup_db().await;
|
|
let fixture = RuntimeFixture::new("update_empty");
|
|
let input = fixture.create_input("empty");
|
|
|
|
let created = RuntimeRepository::create(&pool, input)
|
|
.await
|
|
.expect("Failed to create runtime");
|
|
|
|
let update_input = UpdateRuntimeInput::default();
|
|
|
|
let result = RuntimeRepository::update(&pool, created.id, update_input)
|
|
.await
|
|
.expect("Failed to update runtime");
|
|
|
|
// Should return existing entity unchanged
|
|
assert_eq!(result.id, created.id);
|
|
assert_eq!(result.description, created.description);
|
|
assert_eq!(result.name, created.name);
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore = "integration test — requires database"]
|
|
async fn test_delete_runtime() {
|
|
let pool = setup_db().await;
|
|
let fixture = RuntimeFixture::new("delete_runtime");
|
|
let input = fixture.create_input("deletable");
|
|
|
|
let created = RuntimeRepository::create(&pool, input)
|
|
.await
|
|
.expect("Failed to create runtime");
|
|
|
|
let deleted = RuntimeRepository::delete(&pool, created.id)
|
|
.await
|
|
.expect("Failed to delete runtime");
|
|
|
|
assert!(deleted);
|
|
|
|
let found = RuntimeRepository::find_by_id(&pool, created.id)
|
|
.await
|
|
.expect("Query should succeed");
|
|
|
|
assert!(found.is_none());
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore = "integration test — requires database"]
|
|
async fn test_delete_runtime_not_found() {
|
|
let pool = setup_db().await;
|
|
|
|
let deleted = RuntimeRepository::delete(&pool, 999999999)
|
|
.await
|
|
.expect("Delete should succeed");
|
|
|
|
assert!(!deleted);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Specialized Query Tests
|
|
// ============================================================================
|
|
|
|
// #[tokio::test]
|
|
// async fn test_find_by_type_action() {
|
|
// // RuntimeType and find_by_type no longer exist
|
|
// }
|
|
|
|
// #[tokio::test]
|
|
// async fn test_find_by_type_sensor() {
|
|
// // RuntimeType and find_by_type no longer exist
|
|
// }
|
|
|
|
#[tokio::test]
|
|
#[ignore = "integration test — requires database"]
|
|
async fn test_find_by_pack() {
|
|
let pool = setup_db().await;
|
|
let fixture = RuntimeFixture::new("find_by_pack");
|
|
|
|
// Create a pack first
|
|
use attune_common::repositories::pack::{CreatePackInput, PackRepository};
|
|
|
|
let pack_input = CreatePackInput {
|
|
r#ref: fixture.unique_ref("testpack"),
|
|
label: "Test Pack".to_string(),
|
|
description: Some("Pack for runtime testing".to_string()),
|
|
version: "1.0.0".to_string(),
|
|
conf_schema: json!({}),
|
|
config: json!({}),
|
|
meta: json!({
|
|
"author": "Test Author",
|
|
"email": "test@example.com"
|
|
}),
|
|
tags: vec!["test".to_string()],
|
|
runtime_deps: vec![],
|
|
dependencies: vec![],
|
|
is_standard: false,
|
|
installers: json!({}),
|
|
};
|
|
|
|
let pack = PackRepository::create(&pool, pack_input)
|
|
.await
|
|
.expect("Failed to create pack");
|
|
|
|
// Create runtimes with and without pack association
|
|
let mut input1 = fixture.create_input("with_pack1");
|
|
input1.pack = Some(pack.id);
|
|
input1.pack_ref = Some(pack.r#ref.clone());
|
|
|
|
let mut input2 = fixture.create_input("with_pack2");
|
|
input2.pack = Some(pack.id);
|
|
input2.pack_ref = Some(pack.r#ref.clone());
|
|
|
|
let input3 = fixture.create_input("without_pack");
|
|
|
|
let created1 = RuntimeRepository::create(&pool, input1)
|
|
.await
|
|
.expect("Failed to create runtime 1");
|
|
let created2 = RuntimeRepository::create(&pool, input2)
|
|
.await
|
|
.expect("Failed to create runtime 2");
|
|
let _created3 = RuntimeRepository::create(&pool, input3)
|
|
.await
|
|
.expect("Failed to create runtime 3");
|
|
|
|
let pack_runtimes = RuntimeRepository::find_by_pack(&pool, pack.id)
|
|
.await
|
|
.expect("Failed to find by pack");
|
|
|
|
assert_eq!(pack_runtimes.len(), 2);
|
|
assert!(pack_runtimes.iter().any(|r| r.id == created1.id));
|
|
assert!(pack_runtimes.iter().any(|r| r.id == created2.id));
|
|
assert!(pack_runtimes.iter().all(|r| r.pack == Some(pack.id)));
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore = "integration test — requires database"]
|
|
async fn test_find_by_pack_empty() {
|
|
let pool = setup_db().await;
|
|
|
|
let runtimes = RuntimeRepository::find_by_pack(&pool, 999999999)
|
|
.await
|
|
.expect("Failed to find by pack");
|
|
|
|
assert_eq!(runtimes.len(), 0);
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore = "integration test — requires database"]
|
|
async fn test_runtime_created_successfully() {
|
|
let pool = setup_db().await;
|
|
let fixture = RuntimeFixture::new("created_test");
|
|
let input = fixture.create_input("created");
|
|
|
|
let runtime = RuntimeRepository::create(&pool, input)
|
|
.await
|
|
.expect("Failed to create runtime");
|
|
|
|
let found = RuntimeRepository::find_by_id(&pool, runtime.id)
|
|
.await
|
|
.expect("Failed to find runtime")
|
|
.expect("Runtime not found");
|
|
|
|
assert_eq!(found.id, runtime.id);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Edge Cases and Constraints
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
#[ignore = "integration test — requires database"]
|
|
async fn test_duplicate_ref_fails() {
|
|
let pool = setup_db().await;
|
|
let fixture = RuntimeFixture::new("duplicate_ref");
|
|
let input = fixture.create_input("duplicate");
|
|
|
|
RuntimeRepository::create(&pool, input.clone())
|
|
.await
|
|
.expect("Failed to create first runtime");
|
|
|
|
let result = RuntimeRepository::create(&pool, input).await;
|
|
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore = "integration test — requires database"]
|
|
async fn test_json_fields() {
|
|
let pool = setup_db().await;
|
|
let fixture = RuntimeFixture::new("json_fields");
|
|
let input = fixture.create_input("json_test");
|
|
|
|
let runtime = RuntimeRepository::create(&pool, input.clone())
|
|
.await
|
|
.expect("Failed to create runtime");
|
|
|
|
assert_eq!(runtime.distributions, input.distributions);
|
|
assert_eq!(runtime.installation, input.installation);
|
|
|
|
// Verify JSON structure
|
|
assert_eq!(runtime.distributions["linux"]["supported"], json!(true));
|
|
assert!(runtime.installation.is_some());
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore = "integration test — requires database"]
|
|
async fn test_empty_json_distributions() {
|
|
let pool = setup_db().await;
|
|
let fixture = RuntimeFixture::new("empty_json");
|
|
let mut input = fixture.create_input("empty");
|
|
input.distributions = json!({});
|
|
input.installation = None;
|
|
|
|
let runtime = RuntimeRepository::create(&pool, input)
|
|
.await
|
|
.expect("Failed to create runtime");
|
|
|
|
assert_eq!(runtime.distributions, json!({}));
|
|
assert_eq!(runtime.installation, None);
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore = "integration test — requires database"]
|
|
async fn test_list_ordering() {
|
|
let pool = setup_db().await;
|
|
let fixture = RuntimeFixture::new("list_ordering");
|
|
|
|
let mut input1 = fixture.create_input("z_last");
|
|
input1.r#ref = format!("{}.zzz", fixture.test_id);
|
|
|
|
let mut input2 = fixture.create_input("a_first");
|
|
input2.r#ref = format!("{}.aaa", fixture.test_id);
|
|
|
|
let mut input3 = fixture.create_input("m_middle");
|
|
input3.r#ref = format!("{}.mmm", fixture.test_id);
|
|
|
|
RuntimeRepository::create(&pool, input1)
|
|
.await
|
|
.expect("Failed to create runtime 1");
|
|
RuntimeRepository::create(&pool, input2)
|
|
.await
|
|
.expect("Failed to create runtime 2");
|
|
RuntimeRepository::create(&pool, input3)
|
|
.await
|
|
.expect("Failed to create runtime 3");
|
|
|
|
let list = RuntimeRepository::list(&pool)
|
|
.await
|
|
.expect("Failed to list runtimes");
|
|
|
|
// Find our test runtimes in the list
|
|
let test_runtimes: Vec<_> = list
|
|
.iter()
|
|
.filter(|r| r.r#ref.contains(&fixture.test_id))
|
|
.collect();
|
|
|
|
assert_eq!(test_runtimes.len(), 3);
|
|
|
|
// Verify they are sorted by ref
|
|
for i in 0..test_runtimes.len() - 1 {
|
|
assert!(test_runtimes[i].r#ref <= test_runtimes[i + 1].r#ref);
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore = "integration test — requires database"]
|
|
async fn test_timestamps() {
|
|
let pool = setup_db().await;
|
|
let fixture = RuntimeFixture::new("timestamps");
|
|
let input = fixture.create_input("timestamped");
|
|
|
|
let before = chrono::Utc::now();
|
|
let runtime = RuntimeRepository::create(&pool, input)
|
|
.await
|
|
.expect("Failed to create runtime");
|
|
let after = chrono::Utc::now();
|
|
|
|
assert!(runtime.created >= before);
|
|
assert!(runtime.created <= after);
|
|
assert!(runtime.updated >= before);
|
|
assert!(runtime.updated <= after);
|
|
assert_eq!(runtime.created, runtime.updated);
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore = "integration test — requires database"]
|
|
async fn test_update_changes_timestamp() {
|
|
let pool = setup_db().await;
|
|
let fixture = RuntimeFixture::new("timestamp_update");
|
|
let input = fixture.create_input("ts");
|
|
|
|
let runtime = RuntimeRepository::create(&pool, input)
|
|
.await
|
|
.expect("Failed to create runtime");
|
|
|
|
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
|
|
|
let update_input = UpdateRuntimeInput {
|
|
description: Some(Patch::Set("Updated".to_string())),
|
|
..Default::default()
|
|
};
|
|
|
|
let updated = RuntimeRepository::update(&pool, runtime.id, update_input)
|
|
.await
|
|
.expect("Failed to update runtime");
|
|
|
|
assert_eq!(updated.created, runtime.created);
|
|
assert!(updated.updated > runtime.updated);
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore = "integration test — requires database"]
|
|
async fn test_pack_ref_without_pack_id() {
|
|
let pool = setup_db().await;
|
|
let fixture = RuntimeFixture::new("pack_ref_only");
|
|
let mut input = fixture.create_input("packref");
|
|
input.pack = None;
|
|
input.pack_ref = Some("some.pack.ref".to_string());
|
|
|
|
let runtime = RuntimeRepository::create(&pool, input.clone())
|
|
.await
|
|
.expect("Failed to create runtime");
|
|
|
|
assert_eq!(runtime.pack, None);
|
|
assert_eq!(runtime.pack_ref, input.pack_ref);
|
|
}
|