diff --git a/tests/functional/replication/test_updating_blob_with_empty_string_stops_replication.py b/tests/functional/replication/test_updating_blob_with_empty_string_stops_replication.py index 59b0b417..73f6df54 100644 --- a/tests/functional/replication/test_updating_blob_with_empty_string_stops_replication.py +++ b/tests/functional/replication/test_updating_blob_with_empty_string_stops_replication.py @@ -46,12 +46,17 @@ NOTES: We use 'assert' only at the final point of test, with printing detalization about encountered problem(s). During all previous steps, we only store unexpected output to variables, e.g.: out_main = capsys.readouterr().out etc. - [18.07.2023] pzotov ENABLED execution of on Linux when ServerMode = Classic after letter from dimitr 13-JUL-2023 12:58. See https://github.com/FirebirdSQL/firebird/commit/9aaeab2d4b414f06dabba37e4ebd32587acd5dc0 - Checked on 5.0.0.1017, 4.0.3.2925 - both SS and CS. + [22.12.2023] pzotov + Refactored: make test more robust when it can not remove some files from and folders. + This can occurs because engine opens / file every 10 seconds and check whether new segments must be applied. + Because of this, attempt to drop this file exactly at that moment causes on Windows "PermissionError: [WinError 32]". + This error must NOT propagate and interrupt entire test. Rather, we must only to log name of file that can not be dropped. + + Checked on Windows, 6.0.0.193, 5.0.0.1304, 4.0.5.3042 (SS/CS for all). """ import os import shutil @@ -90,17 +95,28 @@ def cleanup_folder(p): # Used for cleanup and when replication must be reset # in case when any error occurred during test execution. assert os.path.dirname(p) != p, f"@@@ ABEND @@@ CAN NOT operate in the file system root directory. Check your code!" + for root, dirs, files in os.walk(p): for f in files: - os.unlink(os.path.join(root, f)) + # ::: NB ::: 22.12.2023. + # We have to expect that attempt to delete of GUID and (maybe) archived segments can FAIL with + # PermissionError: [WinError 32] The process cannot ... used by another process: /path/to/{GUID} + # Also, we have to skip exception if file (segment) was just deleted by engine + try: + Path(root +'/' + f).unlink(missing_ok = True) + except PermissionError as x: + pass + for d in dirs: - shutil.rmtree(os.path.join(root, d)) - return len(os.listdir(p)) + shutil.rmtree(os.path.join(root, d), ignore_errors = True) + + return os.listdir(p) #-------------------------------------------- def reset_replication(act_db_main, act_db_repl, db_main_file, db_repl_file): out_reset = '' + failed_shutdown_db_map = {} # K = 'db_main', 'db_repl'; V = error that occurred when we attempted to change DB state to full shutdown (if it occurred) with act_db_main.connect_server() as srv: @@ -120,22 +136,36 @@ def reset_replication(act_db_main, act_db_repl, db_main_file, db_repl_file): # try: srv.database.shutdown(database = f, mode = ShutdownMode.FULL, method = ShutdownMethod.FORCED, timeout = 0) - except DatabaseError as e: - out_reset += e.__str__() - # REMOVE db file from disk: - ########################### - os.unlink(f) + # REMOVE db file from disk: we can safely assume that this can be done because DB in full shutdown state. + ########################### + os.unlink(f) + except DatabaseError as e: + failed_shutdown_db_map[ f ] = e.__str__() + # Clean folders repl_journal and repl_archive: remove all files from there. + # NOTE: test must NOT raise unrecoverable error if some of files in these folders can not be deleted. + # Rather, this must be displayed as diff and test must be considered as just failed. for p in (repl_jrn_sub_dir,repl_arc_sub_dir): - if cleanup_folder(repl_root_path / p) > 0: - out_reset += f"Directory {str(p)} remains non-empty.\n" + + remained_files = cleanup_folder(repl_root_path/p) - if out_reset == '': - for a in (act_db_main,act_db_repl): - d = a.db.db_path + if remained_files: + out_reset += '\n'.join( (f"Directory '{str(repl_root_path/p)}' remains non-empty. Could not delete file(s):", '\n'.join(remained_files)) ) + # xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + # xxx r e c r e a t e d b _ m a i n a n d d b _ r e p l xxx + # xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + for a in (act_db_main,act_db_repl): + d = a.db.db_path + failed_shutdown_msg = failed_shutdown_db_map.get( str(d), '' ) + if failed_shutdown_msg: + # we could NOT change state of this database to full shutdown --> we must NOT recreate it. + # Accumulate error messages in OUT arg (for displaying as diff): + # + out_reset += '\n'.join( failed_shutdown_msg ) + else: try: dbx = create_database(str(d), user = a.db.user) dbx.close() @@ -151,8 +181,9 @@ def reset_replication(act_db_main, act_db_repl, db_main_file, db_repl_file): con.commit() except DatabaseError as e: out_reset += e.__str__() - + # Must remain EMPTY: + #################### return out_reset #-------------------------------------------- @@ -396,12 +427,16 @@ def test_1(act_db_main: Action, act_db_repl: Action, capsys): # print('Problem(s) detected:') if out_prep.strip(): - print('out_prep:\n', out_prep) + print('out_prep:') + print(out_prep) if out_main.strip(): - print('out_main:\n', out_main) + print('out_main:') + print(out_main) if out_drop.strip(): - print('out_drop:\n', out_drop) + print('out_drop:') + print(out_drop) if out_reset.strip(): - print('out_reset:\n', out_reset) + print('out_reset:') + print(out_reset) assert '' == capsys.readouterr().out