upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/grasp-audit
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-11-28 05:31:08 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-11-28 05:31:08 +0000
commit37bd77778041a3350f8bfb65deb9dd6d1803e058 (patch)
treec2b5818d26e30ee775cd9450259a77fc454c5644 /grasp-audit
parentc45a425606776e937f4429402030aed8f2ab7436 (diff)
audit: future test shared / isolation fixes
Diffstat (limited to 'grasp-audit')
-rw-r--r--grasp-audit/src/fixtures.rs113
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 @@
36use crate::{AuditClient, AuditMode}; 36use crate::{AuditClient, AuditMode};
37use anyhow::{Context, Result}; 37use anyhow::{Context, Result};
38use nostr_sdk::prelude::Event; 38use nostr_sdk::prelude::Event;
39use std::collections::HashMap;
40use 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 {
197pub struct TestContext<'a> { 199pub 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
202impl<'a> TestContext<'a> { 209impl<'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)