mirror of
https://github.com/FirebirdSQL/firebird-qa.git
synced 2025-01-22 13:33:07 +01:00
375 lines
19 KiB
Python
375 lines
19 KiB
Python
#coding:utf-8
|
|
|
|
"""
|
|
ID: issue-6492
|
|
ISSUE: 6492
|
|
TITLE: A number of errors when database name is longer than 255 symbols
|
|
DESCRIPTION:
|
|
From ticket title one may to conclude that we have ability to really create databases with full length (path + file name)
|
|
more than 255 characters, but it is not so.
|
|
Actually, on Windows and Linux one can not create files with length more than 255 characters.
|
|
Moreover, FB has its own, more strict limitation: command CONNECT '<database_name>' can not operate with files with length
|
|
more than (253 - <P>) symbols, including enclosing quotes, where <P> is length of used protocol prefix: 'inet://', 'localhost:'.
|
|
|
|
Test verifies ability to create DB with max possible length (i.e. which allows to run 'CONNECT ...' command) and does that
|
|
for all protocols that are supported by current OS (Linux + local, inet; Windows: local, inet, xnet).
|
|
For each used protocol we try to perform several operations using gfix utility and after this - fbsvcmgr.
|
|
|
|
After each operation we check mon$database and 'construct' string based on values from this table that must be returned
|
|
as tail of 'Attributes' line in the 'gstat -h' command. These lines must be equal.
|
|
One need to pay attantion that when database is in 'backup-lock' state then changes in its attributes can not be seen
|
|
in the output of 'gstat -h'. Because of this, we do not change any attributes between 'alter database begin/end backup'.
|
|
Also, one need to take in account that we can not check full ability of 'nbackup -L' command for databases which names
|
|
are maximal possible, because adding of suffix '.delta' will cause exceeding max possible length = 253 characters.
|
|
Because of this, test uses 'alter database begin add difference file with name = '<database_path_name.dif>' (i.e. its has
|
|
the same length and main DB file).
|
|
|
|
NOTES:
|
|
[09.03.2020] pzotov // old comments, to be deleted later.
|
|
STDOUT-logs of backup, restore and gstat currently (09-mar-2020) have only truncated name (~235...241 chars).
|
|
This may change in the future if FB developers will decide to resolve this problem.
|
|
|
|
For L=259 we must see in backup log following phrase:
|
|
gbak:text for attribute 7 is too large in put_asciz(), truncating to 255 bytes
|
|
- but currently this is not checked here.
|
|
[09.02.2022] pcisar
|
|
Fails on Windows10 / 4.0.1 with:
|
|
"CreateFile (create)" operation for file "..."
|
|
-Error while trying to create file
|
|
-System can't find specified path
|
|
Variant with 255 chars fails in init script, while 259 chars variant fails in database fixture while
|
|
db creation.
|
|
On national windows with OS i/o error messages in locale.getpreferredencoding(), it may fail while
|
|
reading stderr from isql. But using io_enc=locale.getpreferredencoding() will show the message.
|
|
[23.09.2022] pzotov
|
|
1. Database header (that is produced by 'gstat -h' command) contains in its 1st line full path + name of DB file,
|
|
enclosed into double quotes. When length of path+filename is 243 or greater, this string begins look as 'cuted',
|
|
i.e. ellipsis will be shown at the end of this name (instead of extension and closed double quote).
|
|
This means that we have to be aware about applying regexp during parsing gstat output: final quote may miss!
|
|
2. Currently one can not use 'act.svccmgr()' calls because of need to specify different protocols when check fbsvcmgr:
|
|
fbsvcmgr service_mgr ...
|
|
fbsvcmgr inet://localhost:service_mgr ...
|
|
fbsvcmgr xnet://service_mgr ...
|
|
Because of this, subprocess.run() is used to invoke fbsvcmgr
|
|
|
|
Checked on Windows and Linux, 4.0.1.2692 (SS/CS), 5.0.0.736 (SS/CS).
|
|
JIRA: CORE-6248
|
|
FBTEST: bugs.core_6248
|
|
"""
|
|
import os
|
|
import subprocess
|
|
import locale
|
|
import re
|
|
import time
|
|
import platform
|
|
from pathlib import Path
|
|
#from difflib import unified_diff
|
|
#from firebird.driver import SrvRepairFlag
|
|
|
|
import pytest
|
|
from firebird.qa import *
|
|
|
|
db = db_factory()
|
|
act = python_act('db', substitutions = [('[\t ]+', ' ')])
|
|
|
|
# We have to limit length of temp_file with 255 characters.
|
|
# CentOS-7: OSError: [Errno 36] File name too long:
|
|
tmp_file = temp_file( ('0123456789' * 26)[:255] )
|
|
|
|
#-----------------------------------------------
|
|
def check_db_hdr_info(act: Action, db_file_chk:Path, interested_patterns, capsys):
|
|
|
|
# 1. Obtain attributes from mon$database: get page buffers, 'build' attributes row and get sweep interval.
|
|
# These values will be displayed in the form of three separate LINES, without column names.
|
|
# Content of this output must be equal to gstat filtered values, with exception of leading spaces:
|
|
sql_txt = f"""
|
|
set list on;
|
|
|
|
-- Make connect using local protocol.
|
|
-- NOTE: 'command error' raises here if length of '{db_file_chk}' (including qutes!) greater than 255.
|
|
connect '{db_file_chk}' user {act.db.user};
|
|
|
|
select
|
|
'Page buffers ' || mon$page_buffers as " "
|
|
,'Attributes ' || iif(trim(attr_list) = '', '', substring(attr_list from 2))
|
|
as " "
|
|
,'Sweep interval: ' || mon$sweep_interval as " "
|
|
from (
|
|
select
|
|
mon$page_buffers
|
|
,mon$forced_writes
|
|
,mon$backup_state
|
|
,mon$reserve_space
|
|
,mon$shutdown_mode
|
|
,mon$read_only
|
|
,mon$replica_mode
|
|
,mon$sweep_interval
|
|
,iif(mon$forced_writes = 1, ', force write', '')
|
|
|| iif(mon$reserve_space = 0, ', no reserve', '')
|
|
|| decode(mon$backup_state, 2, ', merge', 1, ', backup lock', '')
|
|
|| decode(mon$shutdown_mode, 3, 'full shutdown', 2, ', single-user maintenance', 1, ', multi-user maintenance', '')
|
|
-- !! NEED TRIM() !! otherwise 10 spaces will be inserted if mon$read_only=0.
|
|
-- Discussed with Vladet al, letters since 23.09.2022 10:57.
|
|
|| trim(iif(mon$read_only<>0, ', read only', ''))
|
|
|| decode(mon$replica_mode, 2, ', read-write replica', 1, ', read-only replica', '')
|
|
as attr_list
|
|
from mon$database
|
|
);
|
|
commit;
|
|
"""
|
|
# Example of output:
|
|
# Page buffers 3791
|
|
# Attributes force write, no reserve, single-user maintenance, read only, read-write replica
|
|
# Sweep interval: 5678
|
|
|
|
act.isql(switches = ['-q'], input = sql_txt, connect_db=False, credentials = False, combine_output = True, io_enc = locale.getpreferredencoding())
|
|
expected_attr_from_mon_db = act.stdout
|
|
|
|
#------------------------------------------------------
|
|
|
|
# 2. Run 'gstat -h', filtering its output and compare with data that was obtained from mon$database.
|
|
# NOTE: database name with backslashes (on Windows) must be checked without regexp work, only via 'IN'.
|
|
# On Winows databases are created in UPPER form, so we have to remove case sensitivity.
|
|
act.gstat(switches=['-h', db_file_chk], connect_db = False, io_enc = locale.getpreferredencoding())
|
|
db_guid = ''
|
|
db_found = ''
|
|
db_cuted = ('Database "' + str(db_file_chk) + '"').lower()[:250]
|
|
for line in act.stdout.split('\n'):
|
|
if act.match_any(line, interested_patterns):
|
|
if 'Database GUID' in line:
|
|
db_guid = line
|
|
elif db_cuted in line.lower():
|
|
print(db_cuted)
|
|
db_found = 1
|
|
else:
|
|
print(line)
|
|
|
|
if db_found:
|
|
act.expected_stdout = f"""
|
|
{db_cuted}
|
|
{expected_attr_from_mon_db}
|
|
"""
|
|
act.stdout = capsys.readouterr().out
|
|
assert act.clean_stdout == act.clean_expected_stdout
|
|
act.reset()
|
|
else:
|
|
print('Cuted DB name:',db_cuted)
|
|
for line in act.stdout.split('\n'):
|
|
print('gstat output: ',line)
|
|
assert db_found,'COULD NOT FIND NAME OF DATABASE IN THE GSTAT HEADER'
|
|
# 3. Return GUID of database (can be compared after b/r with GUID of restored database: they always must differ):
|
|
return db_guid
|
|
|
|
#-----------------------------------------------
|
|
@pytest.mark.version('>=4.0')
|
|
def test_1(act: Action, tmp_file:Path, capsys):
|
|
|
|
#################
|
|
# ### ACHTUNG ###
|
|
#################
|
|
# DO NOT include ending double quote into the database name pattern!
|
|
# String with database name is CUTED OFF when length of full path + filename is 243 and above:
|
|
# 243 --> Database "C:\TEMP\...\01234567890<...>012345.F...
|
|
# 244 --> Database "C:\TEMP\...\01234567890<...>0123456....
|
|
# 245 --> Database "C:\TEMP\...\01234567890<...>01234567...
|
|
# 246 ... 255 -- same as for 245. NB: for N=244 line differs from all others.
|
|
# All these (cuted) strings have length = 254 bytes and do NOT contain ending double quote.
|
|
# Because of this, we must include this character into the pattern only as OPTIONAL, i.e.: |'Database\s+"\S+(")?'|
|
|
#
|
|
interested_patterns = ( 'Database\s+"\S+(")?', '[\t ]*Attributes([\t ]+\w+)?', '[\t ]*Page buffers([\t ]+\d+)', '[\t ]*Sweep interval(:)?([\t ]+\d+)', 'Database GUID')
|
|
interested_patterns = [re.compile(p, re.IGNORECASE) for p in interested_patterns]
|
|
protocol_list = ('', 'inet://', 'xnet://') if os.name == 'nt' else ('', 'inet://',)
|
|
|
|
full_str = str(tmp_file.absolute())
|
|
|
|
for utility in ('gfix', 'fbsvcmgr'):
|
|
for protocol_prefix in protocol_list:
|
|
|
|
# NB: most strict limit for DB filename length origins from isql 'CONNECT' command:
|
|
# 'command error' raises there if length of '{db_file_chk}' (including qutes!) greater than 255.
|
|
# Because of this, we can not operate with files with length of full name greater than 253 bytes.
|
|
#
|
|
db_file_len = 253 - len(protocol_prefix)
|
|
|
|
db_file_chk = Path((full_str[:db_file_len-4] + '.fdb').lower())
|
|
db_file_dif = Path(os.path.splitext(db_file_chk)[0] + '.dif')
|
|
db_file_fbk = Path(os.path.splitext(db_file_chk)[0] + '.fbk')
|
|
|
|
db_file_dsn = ''
|
|
svc_call_starting_part = []
|
|
if utility == 'gfix':
|
|
db_file_dsn = protocol_prefix + str(db_file_chk)
|
|
else:
|
|
db_file_dsn = db_file_chk
|
|
fb_svc_name = protocol_prefix + 'service_mgr'
|
|
svc_call_starting_part = [ act.vars['fbsvcmgr'], fb_svc_name, '-user', act.db.user, '-password', act.db.password ]
|
|
|
|
sql_txt = f"""
|
|
set list on;
|
|
create database '{db_file_dsn}' user {act.db.user} password '{act.db.password}';
|
|
select lower(mon$database_name) as mon_db_name from mon$database;
|
|
select lower(rdb$get_context('SYSTEM', 'DB_NAME')) as ctx_db_name from mon$database;
|
|
commit;
|
|
"""
|
|
|
|
act.expected_stdout = f"""
|
|
MON_DB_NAME {db_file_chk}
|
|
CTX_DB_NAME {db_file_chk}
|
|
"""
|
|
|
|
act.isql(switches = ['-q'], input = sql_txt, connect_db=False, credentials = False, combine_output = True, io_enc = locale.getpreferredencoding())
|
|
assert act.clean_stdout == act.clean_expected_stdout
|
|
act.reset()
|
|
|
|
svc_retcode = 0
|
|
if utility == 'gfix':
|
|
act.gfix(switches=['-buffers', '3791', db_file_dsn], combine_output = True, io_enc = locale.getpreferredencoding())
|
|
else:
|
|
svc_retcode = (subprocess.run( svc_call_starting_part + ['action_properties', 'prp_page_buffers', '3791', 'dbname', db_file_chk], stderr = subprocess.STDOUT)).returncode
|
|
|
|
assert '' == act.stdout and svc_retcode == 0
|
|
act.reset()
|
|
|
|
|
|
if utility == 'gfix':
|
|
act.gfix(switches=['-write','sync', db_file_dsn], combine_output = True, io_enc = locale.getpreferredencoding())
|
|
else:
|
|
svc_retcode = (subprocess.run( svc_call_starting_part + ['action_properties', 'prp_write_mode', 'prp_wm_sync', 'dbname', db_file_chk], stderr = subprocess.STDOUT)).returncode
|
|
|
|
assert '' == act.stdout and svc_retcode == 0
|
|
act.reset()
|
|
|
|
|
|
if utility == 'gfix':
|
|
act.gfix(switches=['-housekeeping','5678', db_file_dsn], combine_output = True, io_enc = locale.getpreferredencoding())
|
|
else:
|
|
svc_retcode = (subprocess.run( svc_call_starting_part + ['action_properties', 'prp_sweep_interval', '5678', 'dbname', db_file_chk], stderr = subprocess.STDOUT)).returncode
|
|
|
|
assert '' == act.stdout and svc_retcode == 0
|
|
act.reset()
|
|
|
|
|
|
if utility == 'gfix':
|
|
act.gfix(switches=['-use','full', db_file_dsn], combine_output = True, io_enc = locale.getpreferredencoding())
|
|
else:
|
|
svc_retcode = (subprocess.run( svc_call_starting_part + ['action_properties', 'prp_reserve_space', 'prp_res_use_full', 'dbname', db_file_chk], stderr = subprocess.STDOUT)).returncode
|
|
|
|
assert '' == act.stdout and svc_retcode == 0
|
|
act.reset()
|
|
|
|
|
|
if utility == 'gfix':
|
|
act.gfix(switches=['-sweep', db_file_dsn], combine_output = True, io_enc = locale.getpreferredencoding())
|
|
else:
|
|
svc_retcode = (subprocess.run( svc_call_starting_part + ['action_repair', 'rpr_sweep_db', 'dbname', db_file_chk], stderr = subprocess.STDOUT)).returncode
|
|
|
|
assert '' == act.stdout and svc_retcode == 0
|
|
act.reset()
|
|
|
|
if act.is_version('>=4'):
|
|
if utility == 'gfix':
|
|
act.gfix(switches=['-replica','read_write', db_file_dsn], combine_output = True, io_enc = locale.getpreferredencoding())
|
|
else:
|
|
svc_retcode = (subprocess.run( svc_call_starting_part + ['action_properties', 'prp_replica_mode', 'prp_rm_readwrite', 'dbname', db_file_chk], stderr = subprocess.STDOUT)).returncode
|
|
|
|
assert '' == act.stdout and svc_retcode == 0
|
|
act.reset()
|
|
|
|
|
|
sql_txt = f"""
|
|
-- Make connect using local protocol.
|
|
-- NOTE: 'command error' raises here if length of '{db_file_chk}' (including qutes!) greater than 255.
|
|
connect '{db_file_chk}' user {act.db.user};
|
|
alter database add difference file '{db_file_dif}';
|
|
alter database begin backup;
|
|
alter database set linger to 100;
|
|
"""
|
|
act.isql(switches = ['-q'], input = sql_txt, connect_db=False, credentials = False, combine_output = True, io_enc = locale.getpreferredencoding())
|
|
assert act.clean_stdout == act.clean_expected_stdout
|
|
act.reset()
|
|
_ = check_db_hdr_info(act, db_file_chk, interested_patterns, capsys)
|
|
|
|
|
|
sql_txt = f"""
|
|
-- Make connect using local protocol.
|
|
-- NOTE: 'command error' raises here if length of '{db_file_chk}' (including qutes!) greater than 255.
|
|
connect '{db_file_chk}' user {act.db.user};
|
|
alter database set linger to 0;
|
|
alter database end backup;
|
|
"""
|
|
act.isql(switches = ['-q'], input = sql_txt, connect_db=False, credentials = False, combine_output = True, io_enc = locale.getpreferredencoding())
|
|
assert act.clean_stdout == act.clean_expected_stdout
|
|
act.reset()
|
|
|
|
|
|
if utility == 'gfix':
|
|
act.gfix(switches=['-mode','read_only', db_file_dsn], combine_output = True, io_enc = locale.getpreferredencoding())
|
|
else:
|
|
svc_retcode = (subprocess.run( svc_call_starting_part + ['action_properties', 'prp_access_mode', 'prp_am_readonly', 'dbname', db_file_chk], stderr = subprocess.STDOUT)).returncode
|
|
|
|
assert '' == act.stdout and svc_retcode == 0
|
|
act.reset()
|
|
_ = check_db_hdr_info(act, db_file_chk, interested_patterns, capsys)
|
|
|
|
|
|
if utility == 'gfix':
|
|
act.gfix(switches=['-shut','single', '-at', '20', db_file_dsn], combine_output = True, io_enc = locale.getpreferredencoding())
|
|
else:
|
|
svc_retcode = (subprocess.run( svc_call_starting_part + ['action_properties', 'prp_shutdown_mode', 'prp_sm_single', 'prp_deny_new_attachments', '20', 'dbname', db_file_chk], stderr = subprocess.STDOUT)).returncode
|
|
|
|
assert '' == act.stdout and svc_retcode == 0
|
|
act.reset()
|
|
|
|
src_guid = check_db_hdr_info(act, db_file_chk, interested_patterns, capsys)
|
|
|
|
|
|
if utility == 'gfix':
|
|
act.gfix(switches=['-online', db_file_dsn], combine_output = True, io_enc = locale.getpreferredencoding())
|
|
else:
|
|
svc_retcode = (subprocess.run( svc_call_starting_part + ['action_properties', 'prp_online_mode', 'prp_sm_normal', 'dbname', db_file_chk], stderr = subprocess.STDOUT)).returncode
|
|
|
|
assert '' == act.stdout and svc_retcode == 0
|
|
act.reset()
|
|
|
|
|
|
if utility == 'gfix':
|
|
act.gfix(switches=['-v', '-full', db_file_dsn], combine_output = True, io_enc = locale.getpreferredencoding())
|
|
else:
|
|
svc_retcode = (subprocess.run( svc_call_starting_part + ['action_repair', 'rpr_validate_db', 'rpr_full', 'dbname', db_file_chk], stderr = subprocess.STDOUT)).returncode
|
|
|
|
assert '' == act.stdout and svc_retcode == 0
|
|
act.reset()
|
|
|
|
|
|
if utility == 'gfix':
|
|
act.gbak(switches=['-b', db_file_dsn, db_file_fbk], combine_output = True, io_enc = locale.getpreferredencoding())
|
|
else:
|
|
svc_retcode = (subprocess.run( svc_call_starting_part + ['action_backup', 'dbname', db_file_chk, 'bkp_file', db_file_fbk], stderr = subprocess.STDOUT)).returncode
|
|
|
|
assert '' == act.stdout and svc_retcode == 0
|
|
act.reset()
|
|
|
|
|
|
if utility == 'gfix':
|
|
act.gbak(switches=['-rep', db_file_fbk, db_file_dsn], combine_output = True, io_enc = locale.getpreferredencoding())
|
|
else:
|
|
svc_retcode = (subprocess.run( svc_call_starting_part + ['action_restore', 'dbname', db_file_chk, 'bkp_file', db_file_fbk, 'res_replace' ], stderr = subprocess.STDOUT)).returncode
|
|
|
|
assert '' == act.stdout and svc_retcode == 0
|
|
act.reset()
|
|
|
|
new_guid = check_db_hdr_info(act, db_file_chk, interested_patterns, capsys)
|
|
|
|
|
|
print('GUID changed ? ==> ', src_guid != new_guid)
|
|
act.expected_stdout = """
|
|
GUID changed ? ==> True
|
|
"""
|
|
act.stdout = capsys.readouterr().out
|
|
assert act.clean_stdout == act.clean_expected_stdout
|
|
act.reset()
|
|
|
|
for f in (db_file_chk,db_file_dif,db_file_fbk):
|
|
f.unlink(missing_ok=True)
|
|
|