@@ -428,6 +428,288 @@ def _unit_of_work(transaction, name):
428428 self .assertEqual (len (rows ), 2 )
429429
430430
431+ @unittest .skipIf (USE_EMULATOR , "Skipping backup tests" )
432+ class TestBackupAPI (unittest .TestCase , _TestData ):
433+ DATABASE_NAME = "test_database" + unique_resource_id ("_" )
434+ DATABASE_NAME_2 = "test_database2" + unique_resource_id ("_" )
435+
436+ @classmethod
437+ def setUpClass (cls ):
438+ pool = BurstyPool (labels = {"testcase" : "database_api" })
439+ db1 = Config .INSTANCE .database (
440+ cls .DATABASE_NAME , ddl_statements = DDL_STATEMENTS , pool = pool
441+ )
442+ db2 = Config .INSTANCE .database (cls .DATABASE_NAME_2 , pool = pool )
443+ cls ._db = db1
444+ cls ._dbs = [db1 , db2 ]
445+ op1 = db1 .create ()
446+ op2 = db2 .create ()
447+ op1 .result (30 ) # raises on failure / timeout.
448+ op2 .result (30 ) # raises on failure / timeout.
449+
450+ current_config = Config .INSTANCE .configuration_name
451+ same_config_instance_id = "same-config" + unique_resource_id ("-" )
452+ cls ._same_config_instance = Config .CLIENT .instance (
453+ same_config_instance_id , current_config
454+ )
455+ op = cls ._same_config_instance .create ()
456+ op .result (30 )
457+ cls ._instances = [cls ._same_config_instance ]
458+
459+ retry = RetryErrors (exceptions .ServiceUnavailable )
460+ configs = list (retry (Config .CLIENT .list_instance_configs )())
461+ diff_configs = [
462+ config .name
463+ for config in configs
464+ if "-us-" in config .name and config .name is not current_config
465+ ]
466+ cls ._diff_config_instance = None
467+ if len (diff_configs ) > 0 :
468+ diff_config_instance_id = "diff-config" + unique_resource_id ("-" )
469+ cls ._diff_config_instance = Config .CLIENT .instance (
470+ diff_config_instance_id , diff_configs [0 ]
471+ )
472+ op = cls ._diff_config_instance .create ()
473+ op .result (30 )
474+ cls ._instances .append (cls ._diff_config_instance )
475+
476+ @classmethod
477+ def tearDownClass (cls ):
478+ for db in cls ._dbs :
479+ db .drop ()
480+ for instance in cls ._instances :
481+ instance .delete ()
482+
483+ def setUp (self ):
484+ self .to_delete = []
485+ self .to_drop = []
486+
487+ def tearDown (self ):
488+ for doomed in self .to_delete :
489+ doomed .delete ()
490+ for doomed in self .to_drop :
491+ doomed .drop ()
492+
493+ def test_create_invalid (self ):
494+ from datetime import datetime
495+ from pytz import UTC
496+
497+ backup_id = "backup_id" + unique_resource_id ("_" )
498+ expire_time = datetime .utcnow ()
499+ expire_time = expire_time .replace (tzinfo = UTC )
500+
501+ backup = Config .INSTANCE .backup (
502+ backup_id , database = self ._db , expire_time = expire_time
503+ )
504+
505+ with self .assertRaises (exceptions .InvalidArgument ):
506+ op = backup .create ()
507+ op .result ()
508+
509+ def test_backup_workflow (self ):
510+ from datetime import datetime
511+ from datetime import timedelta
512+ from pytz import UTC
513+
514+ instance = Config .INSTANCE
515+ backup_id = "backup_id" + unique_resource_id ("_" )
516+ expire_time = datetime .utcnow () + timedelta (days = 3 )
517+ expire_time = expire_time .replace (tzinfo = UTC )
518+
519+ # Create backup.
520+ backup = instance .backup (backup_id , database = self ._db , expire_time = expire_time )
521+ operation = backup .create ()
522+ self .to_delete .append (backup )
523+
524+ # Check metadata.
525+ metadata = operation .metadata
526+ self .assertEqual (backup .name , metadata .name )
527+ self .assertEqual (self ._db .name , metadata .database )
528+ operation .result ()
529+
530+ # Check backup object.
531+ backup .reload ()
532+ self .assertEqual (self ._db .name , backup ._database )
533+ self .assertEqual (expire_time , backup .expire_time )
534+ self .assertIsNotNone (backup .create_time )
535+ self .assertIsNotNone (backup .size_bytes )
536+ self .assertIsNotNone (backup .state )
537+
538+ # Update with valid argument.
539+ valid_expire_time = datetime .utcnow () + timedelta (days = 7 )
540+ valid_expire_time = valid_expire_time .replace (tzinfo = UTC )
541+ backup .update_expire_time (valid_expire_time )
542+ self .assertEqual (valid_expire_time , backup .expire_time )
543+
544+ # Restore database to same instance.
545+ restored_id = "restored_db" + unique_resource_id ("_" )
546+ database = instance .database (restored_id )
547+ self .to_drop .append (database )
548+ operation = database .restore (source = backup )
549+ operation .result ()
550+
551+ database .drop ()
552+ backup .delete ()
553+ self .assertFalse (backup .exists ())
554+
555+ def test_restore_to_diff_instance (self ):
556+ from datetime import datetime
557+ from datetime import timedelta
558+ from pytz import UTC
559+
560+ backup_id = "backup_id" + unique_resource_id ("_" )
561+ expire_time = datetime .utcnow () + timedelta (days = 3 )
562+ expire_time = expire_time .replace (tzinfo = UTC )
563+
564+ # Create backup.
565+ backup = Config .INSTANCE .backup (
566+ backup_id , database = self ._db , expire_time = expire_time
567+ )
568+ op = backup .create ()
569+ self .to_delete .append (backup )
570+ op .result ()
571+
572+ # Restore database to different instance with same config.
573+ restored_id = "restored_db" + unique_resource_id ("_" )
574+ database = self ._same_config_instance .database (restored_id )
575+ self .to_drop .append (database )
576+ operation = database .restore (source = backup )
577+ operation .result ()
578+
579+ database .drop ()
580+ backup .delete ()
581+ self .assertFalse (backup .exists ())
582+
583+ def test_multi_create_cancel_update_error_restore_errors (self ):
584+ from datetime import datetime
585+ from datetime import timedelta
586+ from pytz import UTC
587+
588+ backup_id_1 = "backup_id1" + unique_resource_id ("_" )
589+ backup_id_2 = "backup_id2" + unique_resource_id ("_" )
590+
591+ instance = Config .INSTANCE
592+ expire_time = datetime .utcnow () + timedelta (days = 3 )
593+ expire_time = expire_time .replace (tzinfo = UTC )
594+
595+ backup1 = instance .backup (
596+ backup_id_1 , database = self ._dbs [0 ], expire_time = expire_time
597+ )
598+ backup2 = instance .backup (
599+ backup_id_2 , database = self ._dbs [1 ], expire_time = expire_time
600+ )
601+
602+ # Create two backups.
603+ op1 = backup1 .create ()
604+ op2 = backup2 .create ()
605+ self .to_delete .extend ([backup1 , backup2 ])
606+
607+ backup1 .reload ()
608+ self .assertFalse (backup1 .is_ready ())
609+ backup2 .reload ()
610+ self .assertFalse (backup2 .is_ready ())
611+
612+ # Cancel a create operation.
613+ op2 .cancel ()
614+ self .assertTrue (op2 .cancelled ())
615+
616+ op1 .result ()
617+ backup1 .reload ()
618+ self .assertTrue (backup1 .is_ready ())
619+
620+ # Update expire time to invalid value.
621+ invalid_expire_time = datetime .now () + timedelta (days = 366 )
622+ invalid_expire_time = invalid_expire_time .replace (tzinfo = UTC )
623+ with self .assertRaises (exceptions .InvalidArgument ):
624+ backup1 .update_expire_time (invalid_expire_time )
625+
626+ # Restore to existing database.
627+ with self .assertRaises (exceptions .AlreadyExists ):
628+ self ._db .restore (source = backup1 )
629+
630+ # Restore to instance with different config.
631+ if self ._diff_config_instance is not None :
632+ return
633+ new_db = self ._diff_config_instance .database ("diff_config" )
634+ op = new_db .create ()
635+ op .result (30 )
636+ self .to_drop .append (new_db )
637+ with self .assertRaises (exceptions .InvalidArgument ):
638+ new_db .restore (source = backup1 )
639+
640+ def test_list_backups (self ):
641+ from datetime import datetime
642+ from datetime import timedelta
643+ from pytz import UTC
644+
645+ backup_id_1 = "backup_id1" + unique_resource_id ("_" )
646+ backup_id_2 = "backup_id2" + unique_resource_id ("_" )
647+
648+ instance = Config .INSTANCE
649+ expire_time_1 = datetime .utcnow () + timedelta (days = 21 )
650+ expire_time_1 = expire_time_1 .replace (tzinfo = UTC )
651+
652+ backup1 = Config .INSTANCE .backup (
653+ backup_id_1 , database = self ._dbs [0 ], expire_time = expire_time_1
654+ )
655+
656+ expire_time_2 = datetime .utcnow () + timedelta (days = 1 )
657+ expire_time_2 = expire_time_2 .replace (tzinfo = UTC )
658+ backup2 = Config .INSTANCE .backup (
659+ backup_id_2 , database = self ._dbs [1 ], expire_time = expire_time_2
660+ )
661+
662+ # Create two backups.
663+ op1 = backup1 .create ()
664+ op1 .result ()
665+ backup1 .reload ()
666+ create_time_compare = datetime .utcnow ().replace (tzinfo = UTC )
667+
668+ backup2 .create ()
669+ self .to_delete .extend ([backup1 , backup2 ])
670+
671+ # List backups filtered by state.
672+ filter_ = "state:CREATING"
673+ for backup in instance .list_backups (filter_ = filter_ ):
674+ self .assertEqual (backup .name , backup2 .name )
675+
676+ # List backups filtered by backup name.
677+ filter_ = "name:{0}" .format (backup_id_1 )
678+ for backup in instance .list_backups (filter_ = filter_ ):
679+ self .assertEqual (backup .name , backup1 .name )
680+
681+ # List backups filtered by database name.
682+ filter_ = "database:{0}" .format (self ._dbs [0 ].name )
683+ for backup in instance .list_backups (filter_ = filter_ ):
684+ self .assertEqual (backup .name , backup1 .name )
685+
686+ # List backups filtered by create time.
687+ filter_ = 'create_time > "{0}"' .format (
688+ create_time_compare .strftime ("%Y-%m-%dT%H:%M:%S.%fZ" )
689+ )
690+ for backup in instance .list_backups (filter_ = filter_ ):
691+ self .assertEqual (backup .name , backup2 .name )
692+
693+ # List backups filtered by expire time.
694+ filter_ = 'expire_time > "{0}"' .format (
695+ expire_time_1 .strftime ("%Y-%m-%dT%H:%M:%S.%fZ" )
696+ )
697+ for backup in instance .list_backups (filter_ = filter_ ):
698+ self .assertEqual (backup .name , backup1 .name )
699+
700+ # List backups filtered by size bytes.
701+ filter_ = "size_bytes < {0}" .format (backup1 .size_bytes )
702+ for backup in instance .list_backups (filter_ = filter_ ):
703+ self .assertEqual (backup .name , backup2 .name )
704+
705+ # List backups using pagination.
706+ for page in instance .list_backups (page_size = 1 ).pages :
707+ count = 0
708+ for backup in page :
709+ count += 1
710+ self .assertEqual (count , 1 )
711+
712+
431713SOME_DATE = datetime .date (2011 , 1 , 17 )
432714SOME_TIME = datetime .datetime (1989 , 1 , 17 , 17 , 59 , 12 , 345612 )
433715NANO_TIME = DatetimeWithNanoseconds (1995 , 8 , 31 , nanosecond = 987654321 )
0 commit comments