Object expiration test case (#47)
* Object expiration test case has been added * Common steps have been separated from object test cases * Timeouts have been moved to pre-declared variables
This commit is contained in:
parent
1276f2cdfc
commit
7a4ca9b7cc
9 changed files with 156 additions and 108 deletions
|
@ -81,6 +81,7 @@ The following UserScenarios and testcases are available for execution:
|
||||||
* object_simple.robot
|
* object_simple.robot
|
||||||
* object_storagegroup_simple.robot
|
* object_storagegroup_simple.robot
|
||||||
* object_storagegroup_complex.robot
|
* object_storagegroup_complex.robot
|
||||||
|
* object_expiration.robot
|
||||||
* payment
|
* payment
|
||||||
* withdraw.robot
|
* withdraw.robot
|
||||||
* services
|
* services
|
||||||
|
|
|
@ -182,20 +182,37 @@ def get_eacl(private_key: str, cid: str):
|
||||||
raise Exception("command '{}' return with error (code {}): {}".format(e.cmd, e.returncode, e.output))
|
raise Exception("command '{}' return with error (code {}): {}".format(e.cmd, e.returncode, e.output))
|
||||||
|
|
||||||
|
|
||||||
|
@keyword('Get Epoch')
|
||||||
|
def get_epoch(private_key: str):
|
||||||
|
cmd = (
|
||||||
|
f'{NEOFS_CLI_EXEC} --rpc-endpoint {NEOFS_ENDPOINT} --key {private_key} '
|
||||||
|
f'netmap epoch'
|
||||||
|
)
|
||||||
|
logger.info(f"Cmd: {cmd}")
|
||||||
|
try:
|
||||||
|
complProc = subprocess.run(cmd, check=True, universal_newlines=True,
|
||||||
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=150, shell=True)
|
||||||
|
output = complProc.stdout
|
||||||
|
logger.info(f"Output: {output}")
|
||||||
|
return int(output)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
raise Exception(f"command '{e.cmd}' return with error (code {e.returncode}): {e.output}")
|
||||||
|
|
||||||
@keyword('Set eACL')
|
@keyword('Set eACL')
|
||||||
def set_eacl(private_key: str, cid: str, eacl: str, add_keys: str = ""):
|
def set_eacl(private_key: str, cid: str, eacl: str, add_keys: str = ""):
|
||||||
file_path = TEMP_DIR + eacl
|
file_path = TEMP_DIR + eacl
|
||||||
|
cmd = (
|
||||||
Cmd = (
|
|
||||||
f'{NEOFS_CLI_EXEC} --rpc-endpoint {NEOFS_ENDPOINT} --key {private_key} '
|
f'{NEOFS_CLI_EXEC} --rpc-endpoint {NEOFS_ENDPOINT} --key {private_key} '
|
||||||
f'container set-eacl --cid {cid} --table {file_path} {add_keys}'
|
f'container set-eacl --cid {cid} --table {file_path} {add_keys}'
|
||||||
)
|
)
|
||||||
logger.info("Cmd: %s" % Cmd)
|
logger.info(f"Cmd: {cmd}")
|
||||||
complProc = subprocess.run(Cmd, check=True, universal_newlines=True,
|
try:
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=150, shell=True)
|
complProc = subprocess.run(cmd, check=True, universal_newlines=True,
|
||||||
output = complProc.stdout
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=150, shell=True)
|
||||||
logger.info("Output: %s" % output)
|
output = complProc.stdout
|
||||||
|
logger.info(f"Output: {output}")
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
raise Exception(f"command '{e.cmd}' return with error (code {e.returncode}): {e.output}")
|
||||||
|
|
||||||
@keyword('Form BearerToken file')
|
@keyword('Form BearerToken file')
|
||||||
def form_bearertoken_file(private_key: str, cid: str, file_name: str, eacl_oper_list,
|
def form_bearertoken_file(private_key: str, cid: str, file_name: str, eacl_oper_list,
|
||||||
|
@ -851,6 +868,7 @@ def put_object(private_key: str, path: str, cid: str, bearer: str, user_headers:
|
||||||
|
|
||||||
if user_headers:
|
if user_headers:
|
||||||
user_headers = f"--attributes {user_headers}"
|
user_headers = f"--attributes {user_headers}"
|
||||||
|
|
||||||
if bearer:
|
if bearer:
|
||||||
bearer = f"--bearer {TEMP_DIR}{bearer}"
|
bearer = f"--bearer {TEMP_DIR}{bearer}"
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,44 @@
|
||||||
*** Variables ***
|
*** Variables ***
|
||||||
${FILE_USR_HEADER} = key1=1,key2=abc
|
${FILE_USR_HEADER} = key1=1,key2=abc
|
||||||
${FILE_USR_HEADER_OTH} = key1=2
|
${FILE_USR_HEADER_OTH} = key1=2
|
||||||
${UNEXIST_OID} = 256ZZZZZZZZZZZZZZZZAAAAAAAAAAAAAAAAAAAAAAAAA
|
${UNEXIST_OID} = B2DKvkHnLnPvapbDgfpU1oVUPuXQo5LTfKVxmNDZXQff
|
||||||
|
${TRANSFER_AMOUNT} = 15
|
||||||
|
${DEPOSIT_AMOUNT} = 10
|
||||||
|
|
||||||
*** Keywords ***
|
*** Keywords ***
|
||||||
|
|
||||||
|
Payment operations
|
||||||
|
${WALLET} = Init wallet
|
||||||
|
Generate wallet ${WALLET}
|
||||||
|
${ADDR} = Dump Address ${WALLET}
|
||||||
|
${PRIV_KEY} = Dump PrivKey ${WALLET} ${ADDR}
|
||||||
|
${TX} = Transfer Mainnet Gas ${MAINNET_WALLET_PATH} ${DEF_WALLET_ADDR} ${ADDR} ${TRANSFER_AMOUNT}
|
||||||
|
|
||||||
|
Wait Until Keyword Succeeds ${BASENET_WAIT_TIME} ${BASENET_BLOCK_TIME}
|
||||||
|
... Transaction accepted in block ${TX}
|
||||||
|
Get Transaction ${TX}
|
||||||
|
Expected Mainnet Balance ${ADDR} ${TRANSFER_AMOUNT}
|
||||||
|
|
||||||
|
${SCRIPT_HASH} = Get ScriptHash ${PRIV_KEY}
|
||||||
|
|
||||||
|
${TX_DEPOSIT} = NeoFS Deposit ${WALLET} ${ADDR} ${SCRIPT_HASH} ${DEPOSIT_AMOUNT}
|
||||||
|
Wait Until Keyword Succeeds ${BASENET_WAIT_TIME} ${BASENET_BLOCK_TIME}
|
||||||
|
... Transaction accepted in block ${TX_DEPOSIT}
|
||||||
|
Get Transaction ${TX_DEPOSIT}
|
||||||
|
|
||||||
|
${BALANCE} = Wait Until Keyword Succeeds ${NEOFS_EPOCH_TIMEOUT} ${MORPH_BLOCK_TIME}
|
||||||
|
... Expected Balance ${PRIV_KEY} 0 ${DEPOSIT_AMOUNT}
|
||||||
|
|
||||||
|
Set Global Variable ${PRIV_KEY} ${PRIV_KEY}
|
||||||
|
Set Global Variable ${ADDR} ${ADDR}
|
||||||
|
|
||||||
|
|
||||||
|
Prepare container
|
||||||
|
${CID} = Create container ${PRIV_KEY}
|
||||||
|
Container Existing ${PRIV_KEY} ${CID}
|
||||||
|
|
||||||
|
Wait Until Keyword Succeeds ${NEOFS_EPOCH_TIMEOUT} ${MORPH_BLOCK_TIME}
|
||||||
|
... Expected Balance ${PRIV_KEY} ${DEPOSIT_AMOUNT} ${NEOFS_CREATE_CONTAINER_GAS_FEE}
|
||||||
|
|
||||||
|
Set Global Variable ${CID} ${CID}
|
||||||
|
|
|
@ -12,31 +12,8 @@ NeoFS Complex Object Operations
|
||||||
[Tags] Object NeoFS NeoCLI
|
[Tags] Object NeoFS NeoCLI
|
||||||
[Timeout] 20 min
|
[Timeout] 20 min
|
||||||
|
|
||||||
${WALLET} = Init wallet
|
Payment operations
|
||||||
Generate wallet ${WALLET}
|
Prepare container
|
||||||
${ADDR} = Dump Address ${WALLET}
|
|
||||||
${PRIV_KEY} = Dump PrivKey ${WALLET} ${ADDR}
|
|
||||||
${TX} = Transfer Mainnet Gas wallets/wallet.json ${DEF_WALLET_ADDR} ${ADDR} 15
|
|
||||||
Wait Until Keyword Succeeds 1 min 15 sec
|
|
||||||
... Transaction accepted in block ${TX}
|
|
||||||
Get Transaction ${TX}
|
|
||||||
Expected Mainnet Balance ${ADDR} 15
|
|
||||||
|
|
||||||
${SCRIPT_HASH} = Get ScriptHash ${PRIV_KEY}
|
|
||||||
|
|
||||||
${TX_DEPOSIT} = NeoFS Deposit ${WALLET} ${ADDR} ${SCRIPT_HASH} 10
|
|
||||||
Wait Until Keyword Succeeds 1 min 15 sec
|
|
||||||
... Transaction accepted in block ${TX_DEPOSIT}
|
|
||||||
Get Transaction ${TX_DEPOSIT}
|
|
||||||
|
|
||||||
${BALANCE} = Wait Until Keyword Succeeds 5 min 1 min
|
|
||||||
... Expected Balance ${PRIV_KEY} 0 10
|
|
||||||
|
|
||||||
${CID} = Create container ${PRIV_KEY}
|
|
||||||
Container Existing ${PRIV_KEY} ${CID}
|
|
||||||
|
|
||||||
Wait Until Keyword Succeeds 2 min 30 sec
|
|
||||||
... Expected Balance ${PRIV_KEY} 10 -1e-08
|
|
||||||
|
|
||||||
${FILE} = Generate file of bytes ${COMPLEX_OBJ_SIZE}
|
${FILE} = Generate file of bytes ${COMPLEX_OBJ_SIZE}
|
||||||
${FILE_HASH} = Get file hash ${FILE}
|
${FILE_HASH} = Get file hash ${FILE}
|
||||||
|
|
73
robot/testsuites/integration/object/object_expiration.robot
Normal file
73
robot/testsuites/integration/object/object_expiration.robot
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
*** Settings ***
|
||||||
|
Variables ../../../variables/common.py
|
||||||
|
|
||||||
|
Library ../${RESOURCES}/neofs.py
|
||||||
|
Library ../${RESOURCES}/payment_neogo.py
|
||||||
|
Resource common_steps_object.robot
|
||||||
|
|
||||||
|
|
||||||
|
*** Test cases ***
|
||||||
|
NeoFS Simple Object Operations
|
||||||
|
[Documentation] Testcase to validate NeoFS object expiration option.
|
||||||
|
[Tags] Object NeoFS NeoCLI
|
||||||
|
[Timeout] 20 min
|
||||||
|
|
||||||
|
Payment operations
|
||||||
|
Prepare container
|
||||||
|
|
||||||
|
${FILE} = Generate file of bytes ${SIMPLE_OBJ_SIZE}
|
||||||
|
${FILE_HASH} = Get file hash ${FILE}
|
||||||
|
|
||||||
|
${EPOCH} = Get Epoch ${PRIV_KEY}
|
||||||
|
|
||||||
|
${EPOCH_PRE} = Evaluate ${EPOCH}-1
|
||||||
|
${EPOCH_NEXT} = Evaluate ${EPOCH}+1
|
||||||
|
${EPOCH_POST} = Evaluate ${EPOCH}+1000
|
||||||
|
|
||||||
|
# Failed on attempt to create epoch from the past
|
||||||
|
Run Keyword And Expect Error *
|
||||||
|
... Put object ${PRIV_KEY} ${FILE} ${CID} ${EMPTY} __NEOFS__EXPIRATION_EPOCH=${EPOCH_PRE}
|
||||||
|
|
||||||
|
# Put object with different expiration epoch numbers (current, next, and from the distant future)
|
||||||
|
${OID_CUR} = Put object ${PRIV_KEY} ${FILE} ${CID} ${EMPTY} __NEOFS__EXPIRATION_EPOCH=${EPOCH}
|
||||||
|
${OID_NXT} = Put object ${PRIV_KEY} ${FILE} ${CID} ${EMPTY} __NEOFS__EXPIRATION_EPOCH=${EPOCH_NEXT}
|
||||||
|
${OID_PST} = Put object ${PRIV_KEY} ${FILE} ${CID} ${EMPTY} __NEOFS__EXPIRATION_EPOCH=${EPOCH_POST}
|
||||||
|
|
||||||
|
# Check objects for existence
|
||||||
|
Get object ${PRIV_KEY} ${CID} ${OID_CUR} ${EMPTY} file_read_cur
|
||||||
|
Get object ${PRIV_KEY} ${CID} ${OID_NXT} ${EMPTY} file_read_nxt
|
||||||
|
Get object ${PRIV_KEY} ${CID} ${OID_PST} ${EMPTY} file_read_pst
|
||||||
|
|
||||||
|
# Wait one epoch to check that expired objects (OID_CUR) will be removed
|
||||||
|
Sleep ${NEOFS_EPOCH_TIMEOUT}
|
||||||
|
|
||||||
|
Run Keyword And Expect Error *
|
||||||
|
... Get object ${PRIV_KEY} ${CID} ${OID_CUR} ${EMPTY} file_read
|
||||||
|
|
||||||
|
# Check that correct object with expiration in the future is existed
|
||||||
|
Get object ${PRIV_KEY} ${CID} ${OID_NXT} ${EMPTY} file_read
|
||||||
|
Get object ${PRIV_KEY} ${CID} ${OID_PST} ${EMPTY} file_read_pst
|
||||||
|
|
||||||
|
# Wait one more epoch to check that expired object (OID_NXT) will be removed
|
||||||
|
Sleep ${NEOFS_EPOCH_TIMEOUT}
|
||||||
|
|
||||||
|
Run Keyword And Expect Error *
|
||||||
|
... Get object ${PRIV_KEY} ${CID} ${OID_NXT} ${EMPTY} file_read
|
||||||
|
|
||||||
|
# Check that correct object with expiration in the distant future is existed
|
||||||
|
Get object ${PRIV_KEY} ${CID} ${OID_PST} ${EMPTY} file_read_pst
|
||||||
|
|
||||||
|
[Teardown] Cleanup
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
*** Keywords ***
|
||||||
|
|
||||||
|
Cleanup
|
||||||
|
Cleanup Files
|
||||||
|
Get Docker Logs object_expiration
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -10,33 +10,10 @@ Resource common_steps_object.robot
|
||||||
NeoFS Simple Object Operations
|
NeoFS Simple Object Operations
|
||||||
[Documentation] Testcase to validate NeoFS operations with simple object.
|
[Documentation] Testcase to validate NeoFS operations with simple object.
|
||||||
[Tags] Object NeoFS NeoCLI
|
[Tags] Object NeoFS NeoCLI
|
||||||
[Timeout] 20 min
|
[Timeout] 10 min
|
||||||
|
|
||||||
${WALLET} = Init wallet
|
Payment operations
|
||||||
Generate wallet ${WALLET}
|
Prepare container
|
||||||
${ADDR} = Dump Address ${WALLET}
|
|
||||||
${PRIV_KEY} = Dump PrivKey ${WALLET} ${ADDR}
|
|
||||||
${TX} = Transfer Mainnet Gas wallets/wallet.json ${DEF_WALLET_ADDR} ${ADDR} 15
|
|
||||||
Wait Until Keyword Succeeds 1 min 15 sec
|
|
||||||
... Transaction accepted in block ${TX}
|
|
||||||
Get Transaction ${TX}
|
|
||||||
Expected Mainnet Balance ${ADDR} 15
|
|
||||||
|
|
||||||
${SCRIPT_HASH} = Get ScriptHash ${PRIV_KEY}
|
|
||||||
|
|
||||||
${TX_DEPOSIT} = NeoFS Deposit ${WALLET} ${ADDR} ${SCRIPT_HASH} 10
|
|
||||||
Wait Until Keyword Succeeds 1 min 15 sec
|
|
||||||
... Transaction accepted in block ${TX_DEPOSIT}
|
|
||||||
Get Transaction ${TX_DEPOSIT}
|
|
||||||
|
|
||||||
${BALANCE} = Wait Until Keyword Succeeds 5 min 1 min
|
|
||||||
... Expected Balance ${PRIV_KEY} 0 10
|
|
||||||
|
|
||||||
${CID} = Create container ${PRIV_KEY}
|
|
||||||
Container Existing ${PRIV_KEY} ${CID}
|
|
||||||
|
|
||||||
Wait Until Keyword Succeeds 2 min 30 sec
|
|
||||||
... Expected Balance ${PRIV_KEY} 10 -1e-08
|
|
||||||
|
|
||||||
${FILE} = Generate file of bytes ${SIMPLE_OBJ_SIZE}
|
${FILE} = Generate file of bytes ${SIMPLE_OBJ_SIZE}
|
||||||
${FILE_HASH} = Get file hash ${FILE}
|
${FILE_HASH} = Get file hash ${FILE}
|
||||||
|
|
|
@ -12,31 +12,11 @@ NeoFS Complex Storagegroup
|
||||||
[Tags] Object NeoFS NeoCLI
|
[Tags] Object NeoFS NeoCLI
|
||||||
[Timeout] 20 min
|
[Timeout] 20 min
|
||||||
|
|
||||||
${WALLET} = Init wallet
|
Payment operations
|
||||||
Generate wallet ${WALLET}
|
Create container
|
||||||
${ADDR} = Dump Address ${WALLET}
|
|
||||||
${PRIV_KEY} = Dump PrivKey ${WALLET} ${ADDR}
|
|
||||||
${TX} = Transfer Mainnet Gas wallets/wallet.json ${DEF_WALLET_ADDR} ${ADDR} 15
|
|
||||||
Wait Until Keyword Succeeds 1 min 15 sec
|
|
||||||
... Transaction accepted in block ${TX}
|
|
||||||
Get Transaction ${TX}
|
|
||||||
Expected Mainnet Balance ${ADDR} 15
|
|
||||||
|
|
||||||
${SCRIPT_HASH} = Get ScriptHash ${PRIV_KEY}
|
${FILE_S} = Generate file of bytes ${COMPLEX_OBJ_SIZE}
|
||||||
|
${FILE_HASH_S} = Get file hash ${FILE_S}
|
||||||
${TX_DEPOSIT} = NeoFS Deposit ${WALLET} ${ADDR} ${SCRIPT_HASH} 10
|
|
||||||
Wait Until Keyword Succeeds 1 min 15 sec
|
|
||||||
... Transaction accepted in block ${TX_DEPOSIT}
|
|
||||||
Get Transaction ${TX_DEPOSIT}
|
|
||||||
|
|
||||||
${BALANCE} = Wait Until Keyword Succeeds 5 min 1 min
|
|
||||||
... Expected Balance ${PRIV_KEY} 0 10
|
|
||||||
|
|
||||||
${CID} = Create container ${PRIV_KEY}
|
|
||||||
Container Existing ${PRIV_KEY} ${CID}
|
|
||||||
|
|
||||||
${FILE_S} = Generate file of bytes ${COMPLEX_OBJ_SIZE}
|
|
||||||
${FILE_HASH_S} = Get file hash ${FILE_S}
|
|
||||||
|
|
||||||
|
|
||||||
# Put two Simple Object
|
# Put two Simple Object
|
||||||
|
|
|
@ -12,31 +12,11 @@ NeoFS Simple Storagegroup
|
||||||
[Tags] Object NeoFS NeoCLI
|
[Tags] Object NeoFS NeoCLI
|
||||||
[Timeout] 20 min
|
[Timeout] 20 min
|
||||||
|
|
||||||
${WALLET} = Init wallet
|
Payment operations
|
||||||
Generate wallet ${WALLET}
|
Create container
|
||||||
${ADDR} = Dump Address ${WALLET}
|
|
||||||
${PRIV_KEY} = Dump PrivKey ${WALLET} ${ADDR}
|
|
||||||
${TX} = Transfer Mainnet Gas wallets/wallet.json ${DEF_WALLET_ADDR} ${ADDR} 15
|
|
||||||
Wait Until Keyword Succeeds 1 min 15 sec
|
|
||||||
... Transaction accepted in block ${TX}
|
|
||||||
Get Transaction ${TX}
|
|
||||||
Expected Mainnet Balance ${ADDR} 15
|
|
||||||
|
|
||||||
${SCRIPT_HASH} = Get ScriptHash ${PRIV_KEY}
|
${FILE_S} = Generate file of bytes ${SIMPLE_OBJ_SIZE}
|
||||||
|
${FILE_HASH_S} = Get file hash ${FILE_S}
|
||||||
${TX_DEPOSIT} = NeoFS Deposit ${WALLET} ${ADDR} ${SCRIPT_HASH} 10
|
|
||||||
Wait Until Keyword Succeeds 1 min 15 sec
|
|
||||||
... Transaction accepted in block ${TX_DEPOSIT}
|
|
||||||
Get Transaction ${TX_DEPOSIT}
|
|
||||||
|
|
||||||
${BALANCE} = Wait Until Keyword Succeeds 5 min 1 min
|
|
||||||
... Expected Balance ${PRIV_KEY} 0 10
|
|
||||||
|
|
||||||
${CID} = Create container ${PRIV_KEY}
|
|
||||||
Container Existing ${PRIV_KEY} ${CID}
|
|
||||||
|
|
||||||
${FILE_S} = Generate file of bytes ${SIMPLE_OBJ_SIZE}
|
|
||||||
${FILE_HASH_S} = Get file hash ${FILE_S}
|
|
||||||
|
|
||||||
|
|
||||||
# Put two Simple Object
|
# Put two Simple Object
|
||||||
|
|
|
@ -9,9 +9,14 @@ CERT="%s/../../ca" % ROOT
|
||||||
# in case when test is run from root in docker
|
# in case when test is run from root in docker
|
||||||
ABSOLUTE_FILE_PATH="/robot/testsuites/integration"
|
ABSOLUTE_FILE_PATH="/robot/testsuites/integration"
|
||||||
# Price of the contract Deposit/Withdraw execution:
|
# Price of the contract Deposit/Withdraw execution:
|
||||||
|
MAINNET_WALLET_PATH = "wallets/wallet.json"
|
||||||
NEOFS_CONTRACT_DEPOSIT_GAS_FEE = 0.1679897
|
NEOFS_CONTRACT_DEPOSIT_GAS_FEE = 0.1679897
|
||||||
NEOFS_CONTRACT_WITHDRAW_GAS_FEE = 0.0382514
|
NEOFS_CONTRACT_WITHDRAW_GAS_FEE = 0.0382514
|
||||||
|
NEOFS_CREATE_CONTAINER_GAS_FEE = -1e-08
|
||||||
NEOFS_EPOCH_TIMEOUT = "5min"
|
NEOFS_EPOCH_TIMEOUT = "5min"
|
||||||
|
BASENET_BLOCK_TIME = "15s"
|
||||||
|
BASENET_WAIT_TIME = "1min"
|
||||||
|
MORPH_BLOCK_TIME = "1s"
|
||||||
NEOFS_CONTRACT_CACHE_TIMEOUT = "30s"
|
NEOFS_CONTRACT_CACHE_TIMEOUT = "30s"
|
||||||
NEOFS_IR_WIF = "KxyjQ8eUa4FHt3Gvioyt1Wz29cTUrE4eTqX3yFSk1YFCsPL8uNsY"
|
NEOFS_IR_WIF = "KxyjQ8eUa4FHt3Gvioyt1Wz29cTUrE4eTqX3yFSk1YFCsPL8uNsY"
|
||||||
NEOFS_SN_WIF = "Kwk6k2eC3L3QuPvD8aiaNyoSXgQ2YL1bwS5CP1oKoA9waeAze97s"
|
NEOFS_SN_WIF = "Kwk6k2eC3L3QuPvD8aiaNyoSXgQ2YL1bwS5CP1oKoA9waeAze97s"
|
||||||
|
|
Loading…
Reference in a new issue