Commit 406ec145 authored by Ondrej Kozina's avatar Ondrej Kozina Committed by Milan Broz

Add unit tests for low level io helpers.

parent c27b42e4
......@@ -14,7 +14,8 @@ TESTS = api-test \
keyring-compat-test \
luks2-validation-test \
luks2-integrity-test \
vectors-test
vectors-test \
blockwise-compat
if VERITYSETUP
TESTS += verity-compat-test
......@@ -59,9 +60,10 @@ EXTRA_DIST = compatimage.img.bz2 compatv10image.img.bz2 \
keyring-test \
keyring-compat-test \
integrity-compat-test \
cryptsetup-valg-supps valg.sh valg-api.sh
cryptsetup-valg-supps valg.sh valg-api.sh \
blockwise-compat
CLEANFILES = cryptsetup-tst* valglog*
CLEANFILES = cryptsetup-tst* valglog* *-fail-*.log
clean-local:
-rm -rf tcrypt-images luks1-images luks2-images conversion_imgs luks2_valid_hdr.img
......@@ -86,7 +88,13 @@ vectors_test_LDFLAGS = $(AM_LDFLAGS) -static
vectors_test_CFLAGS = $(AM_CFLAGS) -I$(top_srcdir)/lib/crypto_backend/ @CRYPTO_CFLAGS@
vectors_test_CPPFLAGS = $(AM_CPPFLAGS) -include config.h
check_PROGRAMS = api-test api-test-2 differ vectors-test
unit_utils_io_SOURCES = unit-utils-io.c
unit_utils_io_LDADD = ../libutils_io.la
unit_utils_io_LDFLAGS = $(AM_LDFLAGS) -static
unit_utils_io_CFLAGS = $(AM_CFLAGS) -I$(top_srcdir)/lib
unit_utils_io_CPPFLAGS = $(AM_CPPFLAGS) -include config.h
check_PROGRAMS = api-test api-test-2 differ vectors-test unit-utils-io
conversion_imgs:
@tar xJf conversion_imgs.tar.xz
......
#!/bin/bash
BW_UNIT=./unit-utils-io
STRACE=strace
cleanup() {
if [ -d "$MNT_DIR" ] ; then
umount -f $MNT_DIR 2>/dev/null
rmdir $MNT_DIR 2>/dev/null
fi
rmmod scsi_debug 2>/dev/null
}
fail()
{
if [ -n "$1" ] ; then echo "FAIL $1" ; else echo "FAIL" ; fi
cleanup
exit 100
}
fail_count()
{
echo "[WRONG]"
FAILS=$((FAILS+1))
}
skip()
{
echo "TEST SKIPPED: $1"
cleanup
exit 0
}
add_device() {
modprobe scsi_debug $@
if [ $? -ne 0 ] ; then
echo "This kernel seems to not support proper scsi_debug module, test skipped."
exit 77
fi
DEV=$(grep -l -e scsi_debug /sys/block/*/device/model | cut -f4 -d /)
DEV="/dev/$DEV"
[ -b $DEV ] || fail "Cannot find $DEV."
}
RUN() {
local _res=$1
shift
local _dev=$1
shift
local _fn=$1
shift
case "$_res" in
P)
echo -n "Testing $_fn with params $@ [should PASS]..."
$BW_UNIT $_dev $_fn $@
if [ $? -ne 0 ]; then
fail_count
test -z "$STRACE" || $STRACE -o ./$BW_UNIT-fail-$FAILS-should-pass.log $BW_UNIT $_dev $_fn $@ 2> /dev/null
else
echo "[OK]"
fi
;;
F)
echo -n "Testing $_fn with params $@ [should FAIL]..."
$BW_UNIT $_dev $_fn $@ 2> /dev/null
if [ $? -eq 0 ]; then
fail_count
test -z "$STRACE" || $STRACE -o ./$BW_UNIT-fail-$FAILS-should-fail.log $BW_UNIT $_dev $_fn $@ 2> /dev/null
else
echo "[OK]"
fi
;;
*)
fail "Internal test error"
;;
esac
}
run_all() {
# buffer io support only blocksize aligned ios
# device/file fn_name length
RUN "P" $DEV read_buffer $BSIZE
RUN "P" $DEV read_buffer $((2*BSIZE))
RUN "F" $DEV read_buffer $((BSIZE-1))
RUN "F" $DEV read_buffer $((BSIZE+1))
RUN "P" $DEV read_buffer 0
RUN "P" $DEV write_buffer $BSIZE
RUN "P" $DEV write_buffer $((2*BSIZE))
RUN "F" $DEV write_buffer $((BSIZE-1))
RUN "F" $DEV write_buffer $((BSIZE+1))
RUN "F" $DEV write_buffer 0
# basic blockwise functions
# device/file fn_name length bsize
RUN "P" $DEV read_blockwise 0 $BSIZE
RUN "P" $DEV read_blockwise $((BSIZE)) $BSIZE
RUN "P" $DEV read_blockwise $((BSIZE-1)) $BSIZE
RUN "P" $DEV read_blockwise $((BSIZE+1)) $BSIZE
RUN "P" $DEV read_blockwise $((DEVSIZE)) $BSIZE
RUN "P" $DEV read_blockwise $((DEVSIZE-1)) $BSIZE
RUN "F" $DEV read_blockwise $((DEVSIZE+1)) $BSIZE
RUN "P" $DEV write_blockwise 0 $BSIZE
RUN "P" $DEV write_blockwise $((BSIZE)) $BSIZE
RUN "P" $DEV write_blockwise $((BSIZE-1)) $BSIZE
RUN "P" $DEV write_blockwise $((BSIZE+1)) $BSIZE
RUN "P" $DEV write_blockwise $((DEVSIZE)) $BSIZE
RUN "P" $DEV write_blockwise $((DEVSIZE-1)) $BSIZE
RUN "F" $DEV write_blockwise $((DEVSIZE+1)) $BSIZE
# seek variant blockwise functions
# device/file fn_name length bsize offset
RUN "P" $DEV read_lseek_blockwise 0 $BSIZE 0
RUN "P" $DEV read_lseek_blockwise 0 $BSIZE 1
RUN "P" $DEV read_lseek_blockwise 0 $BSIZE $((DEVSIZE))
# length = 0 is significant here
RUN "P" $DEV read_lseek_blockwise 0 $BSIZE $((DEVSIZE+1))
# begining of device
RUN "P" $DEV read_lseek_blockwise 1 $BSIZE 0
RUN "P" $DEV read_lseek_blockwise 1 $BSIZE 1
RUN "P" $DEV read_lseek_blockwise 1 $BSIZE $((BSIZE-1))
RUN "P" $DEV read_lseek_blockwise 1 $BSIZE $((BSIZE/2))
# somewhere in the 'middle'
RUN "P" $DEV read_lseek_blockwise 1 $BSIZE $BSIZE
RUN "P" $DEV read_lseek_blockwise 1 $BSIZE $((BSIZE+1))
RUN "P" $DEV read_lseek_blockwise 1 $BSIZE $((2*BSIZE-1))
RUN "P" $DEV read_lseek_blockwise 1 $BSIZE $((BSIZE+BSIZE/2-1))
# cross-sector tests
RUN "P" $DEV read_lseek_blockwise 2 $BSIZE $((BSIZE-1))
RUN "P" $DEV read_lseek_blockwise $((BSIZE+1)) $BSIZE $((BSIZE-1))
RUN "P" $DEV read_lseek_blockwise $((BSIZE+2)) $BSIZE $((BSIZE-1))
RUN "P" $DEV read_lseek_blockwise 2 $BSIZE $((2*BSIZE-1))
RUN "P" $DEV read_lseek_blockwise $((BSIZE+1)) $BSIZE $((2*BSIZE-1))
RUN "P" $DEV read_lseek_blockwise $((BSIZE+2)) $BSIZE $((2*BSIZE-1))
# including one whole sector
RUN "P" $DEV read_lseek_blockwise $((BSIZE+2)) $BSIZE $((BSIZE))
RUN "P" $DEV read_lseek_blockwise $((2*BSIZE)) $BSIZE $((BSIZE+1))
RUN "P" $DEV read_lseek_blockwise $((2*BSIZE)) $BSIZE $((BSIZE-1))
RUN "P" $DEV read_lseek_blockwise $((BSIZE+2)) $BSIZE $((BSIZE-1))
RUN "P" $DEV read_lseek_blockwise $((2*BSIZE)) $BSIZE $((BSIZE+1))
RUN "P" $DEV read_lseek_blockwise $((3*BSIZE-2)) $BSIZE $((BSIZE+1))
# hiting exaclty the sector boundary
RUN "P" $DEV read_lseek_blockwise $((BSIZE-1)) $BSIZE 1
RUN "P" $DEV read_lseek_blockwise $((BSIZE-1)) $BSIZE $((BSIZE+1))
RUN "P" $DEV read_lseek_blockwise $((BSIZE+1)) $BSIZE $((BSIZE-1))
RUN "P" $DEV read_lseek_blockwise $((BSIZE+1)) $BSIZE $((2*BSIZE-1))
# device end
RUN "P" $DEV read_lseek_blockwise 1 $BSIZE $((DEVSIZE-1))
RUN "P" $DEV read_lseek_blockwise $((BSIZE-1)) $BSIZE $((DEVSIZE-BSIZE+1))
RUN "P" $DEV read_lseek_blockwise $((BSIZE)) $BSIZE $((DEVSIZE-BSIZE))
RUN "P" $DEV read_lseek_blockwise $((BSIZE+1)) $BSIZE $((DEVSIZE-BSIZE-1))
# this must fail on both device and file
RUN "F" $DEV read_lseek_blockwise 1 $BSIZE $((DEVSIZE))
RUN "F" $DEV read_lseek_blockwise $((BSIZE-1)) $BSIZE $((DEVSIZE-BSIZE+2))
RUN "F" $DEV read_lseek_blockwise $((BSIZE)) $BSIZE $((DEVSIZE-BSIZE+1))
RUN "F" $DEV read_lseek_blockwise $((BSIZE+1)) $BSIZE $((DEVSIZE-BSIZE))
RUN "P" $DEV write_lseek_blockwise 0 $BSIZE 0
# TODO: this may pass but must not write a byte (write(0) is undefined).
# Test it with underlying dm-error or phony read/write syscalls.
# Skipping read is optimization.
# HINT: currently it performs useless write and read as well
RUN "P" $DEV write_lseek_blockwise 0 $BSIZE 1
RUN "P" $DEV write_lseek_blockwise 0 $BSIZE $BSIZE
# begining of device
RUN "P" $DEV write_lseek_blockwise 1 $BSIZE 0
RUN "P" $DEV write_lseek_blockwise 1 $BSIZE 1
RUN "P" $DEV write_lseek_blockwise 1 $BSIZE $((BSIZE-1))
RUN "P" $DEV write_lseek_blockwise 1 $BSIZE $((BSIZE/2))
# somewhere in the 'middle'
RUN "P" $DEV write_lseek_blockwise 1 $BSIZE $BSIZE
RUN "P" $DEV write_lseek_blockwise 1 $BSIZE $((BSIZE+1))
RUN "P" $DEV write_lseek_blockwise 1 $BSIZE $((2*BSIZE-1))
RUN "P" $DEV write_lseek_blockwise 1 $BSIZE $((BSIZE+BSIZE/2-1))
# cross-sector tests
RUN "P" $DEV write_lseek_blockwise 2 $BSIZE $((BSIZE-1))
RUN "P" $DEV write_lseek_blockwise $((BSIZE+1)) $BSIZE $((BSIZE-1))
RUN "P" $DEV write_lseek_blockwise $((BSIZE+2)) $BSIZE $((BSIZE-1))
RUN "P" $DEV write_lseek_blockwise 2 $BSIZE $((2*BSIZE-1))
RUN "P" $DEV write_lseek_blockwise $((BSIZE+1)) $BSIZE $((2*BSIZE-1))
RUN "P" $DEV write_lseek_blockwise $((BSIZE+2)) $BSIZE $((2*BSIZE-1))
# including one whole sector
RUN "P" $DEV write_lseek_blockwise $((BSIZE+2)) $BSIZE $((BSIZE))
RUN "P" $DEV write_lseek_blockwise $((2*BSIZE)) $BSIZE $((BSIZE+1))
RUN "P" $DEV write_lseek_blockwise $((2*BSIZE)) $BSIZE $((BSIZE-1))
RUN "P" $DEV write_lseek_blockwise $((BSIZE+2)) $BSIZE $((BSIZE-1))
RUN "P" $DEV write_lseek_blockwise $((2*BSIZE)) $BSIZE $((BSIZE+1))
RUN "P" $DEV write_lseek_blockwise $((3*BSIZE-2)) $BSIZE $((BSIZE+1))
# hiting exaclty the sector boundary
RUN "P" $DEV write_lseek_blockwise $((BSIZE-1)) $BSIZE 1
RUN "P" $DEV write_lseek_blockwise $((BSIZE-1)) $BSIZE $((BSIZE+1))
RUN "P" $DEV write_lseek_blockwise $((BSIZE+1)) $BSIZE $((BSIZE-1))
RUN "P" $DEV write_lseek_blockwise $((BSIZE+1)) $BSIZE $((2*BSIZE-1))
# device end
RUN "P" $DEV write_lseek_blockwise 1 $BSIZE $((DEVSIZE-1))
RUN "P" $DEV write_lseek_blockwise $((BSIZE-1)) $BSIZE $((DEVSIZE-BSIZE+1))
RUN "P" $DEV write_lseek_blockwise $((BSIZE)) $BSIZE $((DEVSIZE-BSIZE))
RUN "P" $DEV write_lseek_blockwise $((BSIZE+1)) $BSIZE $((DEVSIZE-BSIZE-1))
# this must fail on device, but pass on file (which is unfortunate and maybe design mistake)
RUN "F" $DEV write_lseek_blockwise 1 $BSIZE $((DEVSIZE))
RUN "F" $DEV write_lseek_blockwise $((BSIZE-1)) $BSIZE $((DEVSIZE-BSIZE+2))
RUN "F" $DEV write_lseek_blockwise $((BSIZE)) $BSIZE $((DEVSIZE-BSIZE+1))
RUN "F" $DEV write_lseek_blockwise $((BSIZE+1)) $BSIZE $((DEVSIZE-BSIZE))
}
which $STRACE > /dev/null 2>&1 || unset STRACE
test -x $BW_UNIT || skip "Run \"make `basename $BW_UNIT`\" first"
FAILS=0
DEVSIZEMB=2
DEVSIZE=$((DEVSIZEMB*1024*1024))
DEVBSIZE=512
BSIZE=$DEVBSIZE
EXP=0
echo "System PAGE_SIZE=`getconf PAGE_SIZE`"
echo "# Create classic 512B drive"
echo "# (logical_block_size=$DEVBSIZE, physical_block_size=$((DEVBSIZE*(1<<EXP))))"
add_device dev_size_mb=$DEVSIZEMB sector_size=$DEVBSIZE physblk_exp=$EXP num_tgts=1
run_all
#TODO: create fs on top of device and repeat
cleanup
EXP=3
echo "# Create desktop-class 4K drive"
echo "# (logical_block_size=$DEVBSIZE, physical_block_size=$((DEVBSIZE*(1<<EXP))))"
add_device dev_size_mb=$DEVSIZEMB physblk_exp=$EXP sector_size=$DEVBSIZE num_tgts=1
run_all
#TODO: create fs on top of device and repeat
BSIZE=$((DEVBSIZE*(1<<EXP)))
run_all
#TODO: create fs on top of device and repeat
cleanup
DEVBSIZE=4096
BSIZE=$DEVBSIZE
EXP=0
echo "# Create enterprise-class 4K drive"
echo "# (logical_block_size=$DEVBSIZE, physical_block_size=$((DEVBSIZE*(1<<EXP))))"
add_device dev_size_mb=$DEVSIZEMB physblk_exp=$EXP sector_size=$DEVBSIZE num_tgts=1
run_all
cleanup
test $FAILS -eq 0 || fail "($FAILS wrong result(s) in total)"
#add_device dev_size_mb=2 sector_size=512 physblk_exp=3
#cleanup
#add_device dev_size_mb=2 sector_size=4096
#cleanup
/*
* simple unit test for utils_io.c (blockwise low level functions)
*
* Copyright (C) 2018 Red Hat, Inc. All rights reserved.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include "utils_io.h"
enum fn_enum {
READ_BUFFER = 0,
WRITE_BUFFER,
READ_BLOCKWISE,
WRITE_BLOCKWISE,
READ_LSEEK_BLOCKWISE,
WRITE_LSEEK_BLOCKWISE
} test_fn;
char *test_file;
size_t test_bsize;
size_t test_alignment;
size_t test_length;
off_t test_offset; //FIXME: check for proper 64bit support (and test it!)
size_t test_mem_alignment = 4096;
static int test_read_buffer(void)
{
void *buffer = NULL;
int fd = -1;
ssize_t ret = -EINVAL;
//printf("Entering test_read_buffer\n");
if (posix_memalign(&buffer, test_mem_alignment, test_length)) {
fprintf(stderr, "Failed to allocate aligned buffer.\n");
goto out;
}
fd = open(test_file, O_RDONLY | O_DIRECT);
if (fd < 0) {
fprintf(stderr, "Failed to open %s.\n", test_file);
goto out;
}
ret = read_buffer(fd, buffer, test_length);
if (ret < 0)
goto out;
ret = (size_t) ret == test_length ? 0 : -EIO;
out:
if (fd >= 0)
close(fd);
free(buffer);
return ret;
}
static int test_write_buffer(void)
{
void *buffer = NULL;
int fd = -1;
ssize_t ret = -EINVAL;
//printf("Entering test_write_buffer\n");
if (posix_memalign(&buffer, test_mem_alignment, test_length)) {
fprintf(stderr, "Failed to allocate aligned buffer.\n");
goto out;
}
fd = open(test_file, O_WRONLY | O_DIRECT);
if (fd < 0) {
fprintf(stderr, "Failed to open %s.\n", test_file);
goto out;
}
ret = write_buffer(fd, buffer, test_length);
if (ret < 0)
goto out;
return (size_t) ret == test_length ? 0 : -EIO;
out:
if (fd >= 0)
close(fd);
free(buffer);
return ret;
}
static int test_read_blockwise(void)
{
void *buffer = NULL;
int fd = -1;
ssize_t ret = -EINVAL;
//printf("Entering test_read_blockwise ");
//printf("test_bsize: %zu, test_length: %zu\n", test_bsize, test_length);
if (posix_memalign(&buffer, test_mem_alignment, test_length)) {
fprintf(stderr, "Failed to allocate aligned buffer.\n");
goto out;
}
fd = open(test_file, O_RDONLY | O_DIRECT);
if (fd < 0) {
fprintf(stderr, "Failed to open %s.\n", test_file);
goto out;
}
ret = read_blockwise(fd, test_bsize, test_mem_alignment, buffer, test_length);
if (ret < 0)
goto out;
ret = (size_t) ret == test_length ? 0 : -EIO;
out:
if (fd >= 0)
close(fd);
free(buffer);
return ret;
}
static int test_write_blockwise(void)
{
void *buffer = NULL;
int fd = -1;
ssize_t ret = -EINVAL;
//printf("Entering test_write_blockwise\n");
if (posix_memalign(&buffer, test_mem_alignment, test_length)) {
fprintf(stderr, "Failed to allocate aligned buffer.\n");
goto out;
}
fd = open(test_file, O_RDWR | O_DIRECT);
if (fd < 0) {
fprintf(stderr, "Failed to open %s.\n", test_file);
goto out;
}
ret = write_blockwise(fd, test_bsize, test_mem_alignment, buffer, test_length);
if (ret < 0)
goto out;
ret = (size_t) ret == test_length ? 0 : -EIO;
out:
if (fd >= 0)
close(fd);
free(buffer);
return ret;
}
static int test_read_lseek_blockwise(void)
{
void *buffer = NULL;
int fd = -1;
ssize_t ret = -EINVAL;
//printf("Entering test_read_lseek_blockwise\n");
if (posix_memalign(&buffer, test_mem_alignment, test_length)) {
fprintf(stderr, "Failed to allocate aligned buffer.\n");
goto out;
}
fd = open(test_file, O_RDONLY | O_DIRECT);
if (fd < 0) {
fprintf(stderr, "Failed to open %s.\n", test_file);
goto out;
}
ret = read_lseek_blockwise(fd, test_bsize, test_mem_alignment, buffer, test_length, test_offset);
if (ret < 0)
goto out;
ret = (size_t) ret == test_length ? 0 : -EIO;
out:
if (fd >= 0)
close(fd);
free(buffer);
return ret;
}
static int test_write_lseek_blockwise(void)
{
void *buffer = NULL;
int fd = -1;
ssize_t ret = -EINVAL;
//printf("Entering test_write_lseek_blockwise\n");
if (posix_memalign(&buffer, test_mem_alignment, test_length)) {
fprintf(stderr, "Failed to allocate aligned buffer.\n");
goto out;
}
fd = open(test_file, O_RDWR | O_DIRECT);
if (fd < 0) {
fprintf(stderr, "Failed to open %s.\n", test_file);
goto out;
}
ret = write_lseek_blockwise(fd, test_bsize, test_mem_alignment, buffer, test_length, test_offset);
if (ret < 0)
goto out;
ret = (size_t) ret == test_length ? 0 : -EIO;
out:
if (fd >= 0)
close(fd);
free(buffer);
return ret;
}
static void usage(void)
{
fprintf(stderr, "Use:\tunit-utils-io file/device blockwise_fn length [bsize] [offset].\n");
}
static int parse_input_params(int argc, char **argv)
{
struct stat st;
if (argc < 4) {
usage();
return 1;
}
if (stat(argv[1], &st)) {
fprintf(stderr, "File/device %s is missing?\n", argv[1]);
return 1;
}
test_file = argv[1];
if (sscanf(argv[3], "%zu", &test_length) != 1)
return 1;
if (argc >= 5 && sscanf(argv[4], "%zu", &test_bsize) != 1)
return 1;
if (argc >= 6 && sscanf(argv[5], "%llu", &test_offset) != 1)
return 1;
if (!strcmp(argv[2], "read_buffer"))
test_fn = READ_BUFFER;
else if (!strcmp(argv[2], "write_buffer"))
test_fn = WRITE_BUFFER;
else if (!strcmp(argv[2], "read_blockwise")) {
if (argc < 5) {
usage();
return 1;
}
test_fn = READ_BLOCKWISE;
} else if (!strcmp(argv[2], "write_blockwise")) {
if (argc < 5) {
usage();
return 1;
}
test_fn = WRITE_BLOCKWISE;
} else if (!strcmp(argv[2], "read_lseek_blockwise")) {
if (argc < 6) {
usage();
return 1;
}
test_fn = READ_LSEEK_BLOCKWISE;
} else if (!strcmp(argv[2], "write_lseek_blockwise")) {
if (argc < 6) {
usage();
return 1;
}
test_fn = WRITE_LSEEK_BLOCKWISE;
} else {
usage();
return 1;
}
/* printf("function '%s': length %zu", argv[2], test_length);
if (argc >= 5)
printf(", bsize %zu", test_bsize);
if (argc >= 6)
printf(", offset %llu", test_offset);
printf("\n"); */
return 0;
}
int main(int argc, char **argv)
{
long ps;
int r = EXIT_FAILURE;
if (parse_input_params(argc, argv))
return r;
ps = sysconf(_SC_PAGESIZE);
if (ps > 0)
test_mem_alignment = (size_t)ps;
switch (test_fn) {
case READ_BUFFER:
r = test_read_buffer();
break;
case WRITE_BUFFER:
r = test_write_buffer();
break;
case READ_BLOCKWISE:
r = test_read_blockwise();
break;
case WRITE_BLOCKWISE:
r = test_write_blockwise();
break;
case READ_LSEEK_BLOCKWISE:
r = test_read_lseek_blockwise();
break;
case WRITE_LSEEK_BLOCKWISE:
r = test_write_lseek_blockwise();
break;
default :
fprintf(stderr, "Internal test error.\n");
return r;
}
return r == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment