@@ -198,6 +198,25 @@ pub fn patch_module_cargo_to_local_bindings(module_dir: &Path) -> Result<()> {
198198 Ok ( ( ) )
199199}
200200
201+ fn copy_dir_contents ( src : & Path , dst : & Path ) -> Result < ( ) > {
202+ fs:: create_dir_all ( dst) . with_context ( || format ! ( "Failed to create {}" , dst. display( ) ) ) ?;
203+ for entry in fs:: read_dir ( src) . with_context ( || format ! ( "Failed to read {}" , src. display( ) ) ) ? {
204+ let entry = entry?;
205+ let src_path = entry. path ( ) ;
206+ let dst_path = dst. join ( entry. file_name ( ) ) ;
207+ let file_type = entry. file_type ( ) ?;
208+ if file_type. is_dir ( ) {
209+ copy_dir_contents ( & src_path, & dst_path) ?;
210+ } else if file_type. is_file ( ) {
211+ fs:: copy ( & src_path, & dst_path)
212+ . with_context ( || format ! ( "Failed to copy {} to {}" , src_path. display( ) , dst_path. display( ) ) ) ?;
213+ } else {
214+ bail ! ( "Unsupported fixture entry type at {}" , src_path. display( ) ) ;
215+ }
216+ }
217+ Ok ( ( ) )
218+ }
219+
201220/// Returns the shared target directory for smoketest module builds.
202221///
203222/// All tests share this directory to cache compiled dependencies. The warmup step
@@ -448,6 +467,8 @@ pub struct Smoketest {
448467 /// The SpacetimeDB server guard (stops server on drop).
449468 /// None when running against a remote server.
450469 pub guard : Option < SpacetimeDbGuard > ,
470+ /// Owns a copied fixture data directory, if this smoketest was started from one.
471+ _data_dir_fixture : Option < tempfile:: TempDir > ,
451472 /// Temporary directory containing the module project.
452473 pub project_dir : tempfile:: TempDir ,
453474 /// Additional features for the spacetimedb bindings dependency.
@@ -718,13 +739,19 @@ impl<'a> SubscribeBuilder<'a> {
718739pub struct SmoketestBuilder {
719740 module_code : Option < String > ,
720741 precompiled_module : Option < String > ,
742+ data_dir_fixture : Option < DataDirFixture > ,
721743 bindings_features : Vec < String > ,
722744 extra_deps : String ,
723745 autopublish : bool ,
724746 pg_port : Option < u16 > ,
725747 server_url_override : Option < String > ,
726748}
727749
750+ struct DataDirFixture {
751+ path : PathBuf ,
752+ database_identity : String ,
753+ }
754+
728755impl Default for SmoketestBuilder {
729756 fn default ( ) -> Self {
730757 Self :: new ( )
@@ -737,6 +764,7 @@ impl SmoketestBuilder {
737764 Self {
738765 module_code : None ,
739766 precompiled_module : None ,
767+ data_dir_fixture : None ,
740768 bindings_features : vec ! [ "unstable" . to_string( ) ] ,
741769 extra_deps : String :: new ( ) ,
742770 autopublish : true ,
@@ -750,6 +778,18 @@ impl SmoketestBuilder {
750778 self
751779 }
752780
781+ /// Starts the local server from a copy of a persisted standalone data directory fixture.
782+ ///
783+ /// The fixture directory is copied to a temporary directory before startup so tests can
784+ /// freely mutate it. Tests using this should normally also call `autopublish(false)`.
785+ pub fn data_dir_fixture ( mut self , path : impl AsRef < Path > , database_identity : impl Into < String > ) -> Self {
786+ self . data_dir_fixture = Some ( DataDirFixture {
787+ path : path. as_ref ( ) . to_path_buf ( ) ,
788+ database_identity : database_identity. into ( ) ,
789+ } ) ;
790+ self
791+ }
792+
753793 /// Enables the PostgreSQL wire protocol on the specified port.
754794 pub fn pg_port ( mut self , port : u16 ) -> Self {
755795 self . pg_port = Some ( port) ;
@@ -822,20 +862,45 @@ impl SmoketestBuilder {
822862 let _ = ensure_binaries_built ( ) ;
823863 let build_start = Instant :: now ( ) ;
824864
865+ let fixture_identity = self
866+ . data_dir_fixture
867+ . as_ref ( )
868+ . map ( |fixture| fixture. database_identity . clone ( ) ) ;
869+
825870 // Check if we're running against a remote server
826- let ( guard, server_url) = if let Some ( url) = self . server_url_override {
871+ let ( guard, server_url, data_dir_fixture) = if let Some ( fixture) = self . data_dir_fixture . as_ref ( ) {
872+ if self . server_url_override . is_some ( ) || remote_server_url ( ) . is_some ( ) {
873+ panic ! ( "data_dir_fixture requires a local server managed by the smoketest harness" ) ;
874+ }
875+
876+ let temp_dir = tempfile:: tempdir ( ) . expect ( "Failed to create temp data fixture directory" ) ;
877+ copy_dir_contents ( & fixture. path , temp_dir. path ( ) ) . unwrap_or_else ( |err| {
878+ panic ! (
879+ "failed to copy data dir fixture from {} to {}: {err:#}" ,
880+ fixture. path. display( ) ,
881+ temp_dir. path( ) . display( )
882+ )
883+ } ) ;
884+
885+ let guard = timed ! (
886+ "server spawn from data dir fixture" ,
887+ SpacetimeDbGuard :: spawn_with_data_dir( temp_dir. path( ) . to_path_buf( ) , self . pg_port)
888+ ) ;
889+ let url = guard. host_url . clone ( ) ;
890+ ( Some ( guard) , url, Some ( temp_dir) )
891+ } else if let Some ( url) = self . server_url_override {
827892 eprintln ! ( "[REMOTE] Using explicit server URL: {}" , url) ;
828- ( None , url)
893+ ( None , url, None )
829894 } else if let Some ( remote_url) = remote_server_url ( ) {
830895 eprintln ! ( "[REMOTE] Using remote server: {}" , remote_url) ;
831- ( None , remote_url)
896+ ( None , remote_url, None )
832897 } else {
833898 let guard = timed ! (
834899 "server spawn" ,
835900 SpacetimeDbGuard :: spawn_in_temp_data_dir_with_pg_port( self . pg_port)
836901 ) ;
837902 let url = guard. host_url . clone ( ) ;
838- ( Some ( guard) , url)
903+ ( Some ( guard) , url, None )
839904 } ;
840905
841906 let project_dir = tempfile:: tempdir ( ) . expect ( "Failed to create temp project directory" ) ;
@@ -863,8 +928,9 @@ impl SmoketestBuilder {
863928 let config_path = project_dir. path ( ) . join ( "config.toml" ) ;
864929 let mut smoketest = Smoketest {
865930 guard,
931+ _data_dir_fixture : data_dir_fixture,
866932 project_dir,
867- database_identity : None ,
933+ database_identity : fixture_identity ,
868934 server_url,
869935 config_path,
870936 module_name,
0 commit comments