diff options
| -rw-r--r-- | grasp-audit/src/fixtures.rs | 113 |
1 files changed, 81 insertions, 32 deletions
diff --git a/grasp-audit/src/fixtures.rs b/grasp-audit/src/fixtures.rs index 6014343..23cea37 100644 --- a/grasp-audit/src/fixtures.rs +++ b/grasp-audit/src/fixtures.rs | |||
| @@ -36,6 +36,8 @@ | |||
| 36 | use crate::{AuditClient, AuditMode}; | 36 | use crate::{AuditClient, AuditMode}; |
| 37 | use anyhow::{Context, Result}; | 37 | use anyhow::{Context, Result}; |
| 38 | use nostr_sdk::prelude::Event; | 38 | use nostr_sdk::prelude::Event; |
| 39 | use std::collections::HashMap; | ||
| 40 | use std::sync::{Arc, Mutex}; | ||
| 39 | 41 | ||
| 40 | /// Deterministic commit hash used in RepoState fixtures (Owner variant) | 42 | /// Deterministic commit hash used in RepoState fixtures (Owner variant) |
| 41 | /// This is the hash produced by creating a commit with: | 43 | /// This is the hash produced by creating a commit with: |
| @@ -197,18 +199,27 @@ impl From<AuditMode> for ContextMode { | |||
| 197 | pub struct TestContext<'a> { | 199 | pub struct TestContext<'a> { |
| 198 | client: &'a AuditClient, | 200 | client: &'a AuditClient, |
| 199 | mode: ContextMode, | 201 | mode: ContextMode, |
| 202 | /// Per-TestContext cache for Isolated mode | ||
| 203 | /// This cache ensures fixture dependencies work within a single test | ||
| 204 | /// while maintaining isolation between tests. | ||
| 205 | /// In Shared mode, this cache is not used - we use the client's cache instead. | ||
| 206 | local_cache: Arc<Mutex<HashMap<FixtureKind, Event>>>, | ||
| 200 | } | 207 | } |
| 201 | 208 | ||
| 202 | impl<'a> TestContext<'a> { | 209 | impl<'a> TestContext<'a> { |
| 203 | /// Create a new test context | 210 | /// Create a new test context |
| 204 | /// | 211 | /// |
| 205 | /// The context mode is automatically determined from the client's audit config. | 212 | /// The context mode is automatically determined from the client's audit config. |
| 206 | /// The fixture cache is borrowed from the client, enabling natural sharing: | 213 | /// In Isolated mode, fixtures are cached per-TestContext to maintain fixture |
| 207 | /// - Same client = shared cache (CLI mode behavior) | 214 | /// dependencies within a test while ensuring isolation between tests. |
| 208 | /// - Different clients = isolated caches (test mode behavior) | 215 | /// In Shared mode, the client's cache is used for cross-test fixture sharing. |
| 209 | pub fn new(client: &'a AuditClient) -> Self { | 216 | pub fn new(client: &'a AuditClient) -> Self { |
| 210 | let mode = ContextMode::from(client.config.mode); | 217 | let mode = ContextMode::from(client.config.mode); |
| 211 | Self { client, mode } | 218 | Self { |
| 219 | client, | ||
| 220 | mode, | ||
| 221 | local_cache: Arc::new(Mutex::new(HashMap::new())), | ||
| 222 | } | ||
| 212 | } | 223 | } |
| 213 | 224 | ||
| 214 | /// Create a test context with explicit mode override | 225 | /// Create a test context with explicit mode override |
| @@ -216,7 +227,11 @@ impl<'a> TestContext<'a> { | |||
| 216 | /// This is useful for testing the context itself or for advanced use cases | 227 | /// This is useful for testing the context itself or for advanced use cases |
| 217 | /// where you want to override the default mode behavior. | 228 | /// where you want to override the default mode behavior. |
| 218 | pub fn with_mode(client: &'a AuditClient, mode: ContextMode) -> Self { | 229 | pub fn with_mode(client: &'a AuditClient, mode: ContextMode) -> Self { |
| 219 | Self { client, mode } | 230 | Self { |
| 231 | client, | ||
| 232 | mode, | ||
| 233 | local_cache: Arc::new(Mutex::new(HashMap::new())), | ||
| 234 | } | ||
| 220 | } | 235 | } |
| 221 | 236 | ||
| 222 | /// Get a fixture, creating it if needed based on mode | 237 | /// Get a fixture, creating it if needed based on mode |
| @@ -323,17 +338,29 @@ impl<'a> TestContext<'a> { | |||
| 323 | /// This is a helper method that avoids async recursion by not going | 338 | /// This is a helper method that avoids async recursion by not going |
| 324 | /// through get_fixture. It handles the repo specifically. | 339 | /// through get_fixture. It handles the repo specifically. |
| 325 | /// | 340 | /// |
| 326 | /// Uses client's fixture_cache for caching - in Shared mode this enables | 341 | /// Caching strategy: |
| 327 | /// cross-test reuse when the same client is used. | 342 | /// - **Isolated mode**: Uses per-TestContext local_cache to maintain fixture |
| 328 | /// In Isolated mode, the cache is bypassed to ensure fresh fixtures. | 343 | /// dependencies within a single test, while ensuring isolation between tests. |
| 344 | /// - **Shared mode**: Uses client's fixture_cache for cross-test reuse. | ||
| 329 | async fn get_or_create_repo(&self) -> Result<Event> { | 345 | async fn get_or_create_repo(&self) -> Result<Event> { |
| 330 | // Only check client's cache in Shared mode | 346 | // Check the appropriate cache based on mode |
| 331 | // In Isolated mode, we always create fresh fixtures | 347 | match self.mode { |
| 332 | if self.mode == ContextMode::Shared { | 348 | ContextMode::Isolated => { |
| 333 | let cache = self.client.fixture_cache().lock().unwrap(); | 349 | // In Isolated mode, use local TestContext cache |
| 334 | if let Some(event) = cache.get(&FixtureKind::ValidRepo) { | 350 | // This ensures fixture dependencies work within a single test |
| 335 | tracing::debug!("get_or_create_repo() found in client cache (Shared mode)"); | 351 | let cache = self.local_cache.lock().unwrap(); |
| 336 | return Ok(event.clone()); | 352 | if let Some(event) = cache.get(&FixtureKind::ValidRepo) { |
| 353 | tracing::debug!("get_or_create_repo() found in local cache (Isolated mode)"); | ||
| 354 | return Ok(event.clone()); | ||
| 355 | } | ||
| 356 | } | ||
| 357 | ContextMode::Shared => { | ||
| 358 | // In Shared mode, use client's cache for cross-test sharing | ||
| 359 | let cache = self.client.fixture_cache().lock().unwrap(); | ||
| 360 | if let Some(event) = cache.get(&FixtureKind::ValidRepo) { | ||
| 361 | tracing::debug!("get_or_create_repo() found in client cache (Shared mode)"); | ||
| 362 | return Ok(event.clone()); | ||
| 363 | } | ||
| 337 | } | 364 | } |
| 338 | } | 365 | } |
| 339 | 366 | ||
| @@ -359,29 +386,45 @@ impl<'a> TestContext<'a> { | |||
| 359 | self.client.send_event(repo.clone()).await | 386 | self.client.send_event(repo.clone()).await |
| 360 | .with_context(|| "Failed to send repo announcement to relay")?; | 387 | .with_context(|| "Failed to send repo announcement to relay")?; |
| 361 | 388 | ||
| 362 | // Store in client's cache only in Shared mode | 389 | // Store in the appropriate cache based on mode |
| 363 | // In Isolated mode, we don't cache to ensure test isolation | 390 | match self.mode { |
| 364 | if self.mode == ContextMode::Shared { | 391 | ContextMode::Isolated => { |
| 365 | let mut cache = self.client.fixture_cache().lock().unwrap(); | 392 | // Store in local cache for within-test fixture dependencies |
| 366 | cache.insert(FixtureKind::ValidRepo, repo.clone()); | 393 | let mut cache = self.local_cache.lock().unwrap(); |
| 367 | tracing::debug!("get_or_create_repo() stored in client cache ({} entries)", cache.len()); | 394 | cache.insert(FixtureKind::ValidRepo, repo.clone()); |
| 395 | tracing::debug!("get_or_create_repo() stored in local cache ({} entries)", cache.len()); | ||
| 396 | } | ||
| 397 | ContextMode::Shared => { | ||
| 398 | // Store in client cache for cross-test sharing | ||
| 399 | let mut cache = self.client.fixture_cache().lock().unwrap(); | ||
| 400 | cache.insert(FixtureKind::ValidRepo, repo.clone()); | ||
| 401 | tracing::debug!("get_or_create_repo() stored in client cache ({} entries)", cache.len()); | ||
| 402 | } | ||
| 368 | } | 403 | } |
| 369 | 404 | ||
| 370 | Ok(repo) | 405 | Ok(repo) |
| 371 | } | 406 | } |
| 372 | 407 | ||
| 373 | /// Get or create a RepoWithIssue, with mode-aware caching via the client. | 408 | /// Get or create a RepoWithIssue, with mode-aware caching. |
| 374 | /// Returns the issue event (repo is already sent/cached via get_or_create_repo). | 409 | /// Returns the issue event (repo is already sent/cached via get_or_create_repo). |
| 375 | async fn get_or_create_issue(&self) -> Result<Event> { | 410 | async fn get_or_create_issue(&self) -> Result<Event> { |
| 376 | // Only check client's cache in Shared mode | 411 | // Check the appropriate cache based on mode |
| 377 | if self.mode == ContextMode::Shared { | 412 | match self.mode { |
| 378 | let cache = self.client.fixture_cache().lock().unwrap(); | 413 | ContextMode::Isolated => { |
| 379 | if let Some(event) = cache.get(&FixtureKind::RepoWithIssue) { | 414 | let cache = self.local_cache.lock().unwrap(); |
| 380 | return Ok(event.clone()); | 415 | if let Some(event) = cache.get(&FixtureKind::RepoWithIssue) { |
| 416 | return Ok(event.clone()); | ||
| 417 | } | ||
| 418 | } | ||
| 419 | ContextMode::Shared => { | ||
| 420 | let cache = self.client.fixture_cache().lock().unwrap(); | ||
| 421 | if let Some(event) = cache.get(&FixtureKind::RepoWithIssue) { | ||
| 422 | return Ok(event.clone()); | ||
| 423 | } | ||
| 381 | } | 424 | } |
| 382 | } | 425 | } |
| 383 | 426 | ||
| 384 | // Get or create repo (reuses cached via client) | 427 | // Get or create repo (reuses cached via appropriate cache) |
| 385 | let repo = self.get_or_create_repo().await?; | 428 | let repo = self.get_or_create_repo().await?; |
| 386 | 429 | ||
| 387 | // Create the issue | 430 | // Create the issue |
| @@ -395,10 +438,16 @@ impl<'a> TestContext<'a> { | |||
| 395 | // Send it | 438 | // Send it |
| 396 | self.client.send_event(issue.clone()).await?; | 439 | self.client.send_event(issue.clone()).await?; |
| 397 | 440 | ||
| 398 | // Store in client's cache only in Shared mode | 441 | // Store in the appropriate cache based on mode |
| 399 | if self.mode == ContextMode::Shared { | 442 | match self.mode { |
| 400 | let mut cache = self.client.fixture_cache().lock().unwrap(); | 443 | ContextMode::Isolated => { |
| 401 | cache.insert(FixtureKind::RepoWithIssue, issue.clone()); | 444 | let mut cache = self.local_cache.lock().unwrap(); |
| 445 | cache.insert(FixtureKind::RepoWithIssue, issue.clone()); | ||
| 446 | } | ||
| 447 | ContextMode::Shared => { | ||
| 448 | let mut cache = self.client.fixture_cache().lock().unwrap(); | ||
| 449 | cache.insert(FixtureKind::RepoWithIssue, issue.clone()); | ||
| 450 | } | ||
| 402 | } | 451 | } |
| 403 | 452 | ||
| 404 | Ok(issue) | 453 | Ok(issue) |