Fix for bug #6266 "Invalid DATETIME value is not handled properly".

In server we assume that datetime values stored in MYSQL_TIME struct
are normalized (and year is not greater than 9999), so we should 
perform range checks in all places then we convert something to
MYSQL_TIME. 
parent 75e322e2
...@@ -58,14 +58,15 @@ void init_time(void); ...@@ -58,14 +58,15 @@ void init_time(void);
my_time_t my_time_t
my_system_gmt_sec(const MYSQL_TIME *t, long *my_timezone, bool *in_dst_time_gap); my_system_gmt_sec(const MYSQL_TIME *t, long *my_timezone, bool *in_dst_time_gap);
void set_zero_time(MYSQL_TIME *tm); void set_zero_time(MYSQL_TIME *tm, enum enum_mysql_timestamp_type time_type);
/* /*
Required buffer length for my_time_to_str, my_date_to_str, Required buffer length for my_time_to_str, my_date_to_str,
my_datetime_to_str and TIME_to_string functions. Note, that the my_datetime_to_str and TIME_to_string functions. Note, that the
caller is still responsible to check that given TIME structure caller is still responsible to check that given TIME structure
has values in valid ranges, otherwise size of the buffer could has values in valid ranges, otherwise size of the buffer could
be not enough. be not enough. We also rely on the fact that even wrong values
sent using binary protocol fit in this buffer.
*/ */
#define MAX_DATE_STRING_REP_LENGTH 30 #define MAX_DATE_STRING_REP_LENGTH 30
......
...@@ -33,6 +33,18 @@ enum enum_mysql_timestamp_type ...@@ -33,6 +33,18 @@ enum enum_mysql_timestamp_type
}; };
/*
Structure which is used to represent datetime values inside MySQL.
We assume that values in this structure are normalized, i.e. year <= 9999,
month <= 12, day <= 31, hour <= 23, hour <= 59, hour <= 59. Many functions
in server such as my_system_gmt_sec() or make_time() family of functions
rely on this (actually now usage of make_*() family relies on a bit weaker
restriction). Also functions that produce MYSQL_TIME as result ensure this.
There is one exception to this rule though if this structure holds time
value (time_type == MYSQL_TIMESTAMP_TIME) days and hour member can hold
bigger values.
*/
typedef struct st_mysql_time typedef struct st_mysql_time
{ {
unsigned int year, month, day, hour, minute, second; unsigned int year, month, day, hour, minute, second;
......
...@@ -3256,11 +3256,12 @@ static void read_binary_time(MYSQL_TIME *tm, uchar **pos) ...@@ -3256,11 +3256,12 @@ static void read_binary_time(MYSQL_TIME *tm, uchar **pos)
tm->hour+= tm->day*24; tm->hour+= tm->day*24;
tm->day= 0; tm->day= 0;
} }
tm->time_type= MYSQL_TIMESTAMP_TIME;
*pos+= length; *pos+= length;
} }
else else
set_zero_time(tm); set_zero_time(tm, MYSQL_TIMESTAMP_TIME);
tm->time_type= MYSQL_TIMESTAMP_TIME;
} }
static void read_binary_datetime(MYSQL_TIME *tm, uchar **pos) static void read_binary_datetime(MYSQL_TIME *tm, uchar **pos)
...@@ -3285,12 +3286,12 @@ static void read_binary_datetime(MYSQL_TIME *tm, uchar **pos) ...@@ -3285,12 +3286,12 @@ static void read_binary_datetime(MYSQL_TIME *tm, uchar **pos)
else else
tm->hour= tm->minute= tm->second= 0; tm->hour= tm->minute= tm->second= 0;
tm->second_part= (length > 7) ? (ulong) sint4korr(to+7) : 0; tm->second_part= (length > 7) ? (ulong) sint4korr(to+7) : 0;
tm->time_type= MYSQL_TIMESTAMP_DATETIME;
*pos+= length; *pos+= length;
} }
else else
set_zero_time(tm); set_zero_time(tm, MYSQL_TIMESTAMP_DATETIME);
tm->time_type= MYSQL_TIMESTAMP_DATETIME;
} }
static void read_binary_date(MYSQL_TIME *tm, uchar **pos) static void read_binary_date(MYSQL_TIME *tm, uchar **pos)
...@@ -3307,12 +3308,12 @@ static void read_binary_date(MYSQL_TIME *tm, uchar **pos) ...@@ -3307,12 +3308,12 @@ static void read_binary_date(MYSQL_TIME *tm, uchar **pos)
tm->hour= tm->minute= tm->second= 0; tm->hour= tm->minute= tm->second= 0;
tm->second_part= 0; tm->second_part= 0;
tm->neg= 0; tm->neg= 0;
tm->time_type= MYSQL_TIMESTAMP_DATE;
*pos+= length; *pos+= length;
} }
else else
set_zero_time(tm); set_zero_time(tm, MYSQL_TIMESTAMP_DATE);
tm->time_type= MYSQL_TIMESTAMP_DATE;
} }
......
...@@ -97,13 +97,15 @@ select * from t1 where a is null or b is null; ...@@ -97,13 +97,15 @@ select * from t1 where a is null or b is null;
a b a b
drop table t1; drop table t1;
create table t1 (t datetime); create table t1 (t datetime);
insert into t1 values (20030102030460),(20030102036301),(20030102240401),(20030132030401),(20031302030460); insert into t1 values (20030102030460),(20030102036301),(20030102240401),
(20030132030401),(20031302030401),(100001202030401);
Warnings: Warnings:
Warning 1265 Data truncated for column 't' at row 1 Warning 1265 Data truncated for column 't' at row 1
Warning 1265 Data truncated for column 't' at row 2 Warning 1265 Data truncated for column 't' at row 2
Warning 1265 Data truncated for column 't' at row 3 Warning 1265 Data truncated for column 't' at row 3
Warning 1265 Data truncated for column 't' at row 4 Warning 1265 Data truncated for column 't' at row 4
Warning 1265 Data truncated for column 't' at row 5 Warning 1265 Data truncated for column 't' at row 5
Warning 1265 Data truncated for column 't' at row 6
select * from t1; select * from t1;
t t
0000-00-00 00:00:00 0000-00-00 00:00:00
...@@ -111,14 +113,18 @@ t ...@@ -111,14 +113,18 @@ t
0000-00-00 00:00:00 0000-00-00 00:00:00
0000-00-00 00:00:00 0000-00-00 00:00:00
0000-00-00 00:00:00 0000-00-00 00:00:00
0000-00-00 00:00:00
delete from t1; delete from t1;
insert into t1 values ("20030102030460"),("20030102036301"),("20030102240401"),("20030132030401"),("20031302030460"); insert into t1 values
("2003-01-02 03:04:60"),("2003-01-02 03:63:01"),("2003-01-02 24:04:01"),
("2003-01-32 03:04:01"),("2003-13-02 03:04:01"), ("10000-12-02 03:04:00");
Warnings: Warnings:
Warning 1264 Data truncated; out of range for column 't' at row 1 Warning 1264 Data truncated; out of range for column 't' at row 1
Warning 1264 Data truncated; out of range for column 't' at row 2 Warning 1264 Data truncated; out of range for column 't' at row 2
Warning 1264 Data truncated; out of range for column 't' at row 3 Warning 1264 Data truncated; out of range for column 't' at row 3
Warning 1264 Data truncated; out of range for column 't' at row 4 Warning 1264 Data truncated; out of range for column 't' at row 4
Warning 1264 Data truncated; out of range for column 't' at row 5 Warning 1264 Data truncated; out of range for column 't' at row 5
Warning 1264 Data truncated; out of range for column 't' at row 6
select * from t1; select * from t1;
t t
0000-00-00 00:00:00 0000-00-00 00:00:00
...@@ -126,6 +132,7 @@ t ...@@ -126,6 +132,7 @@ t
0000-00-00 00:00:00 0000-00-00 00:00:00
0000-00-00 00:00:00 0000-00-00 00:00:00
0000-00-00 00:00:00 0000-00-00 00:00:00
0000-00-00 00:00:00
delete from t1; delete from t1;
insert into t1 values ("0000-00-00 00:00:00 some trailer"),("2003-01-01 00:00:00 some trailer"); insert into t1 values ("0000-00-00 00:00:00 some trailer"),("2003-01-01 00:00:00 some trailer");
Warnings: Warnings:
......
...@@ -77,10 +77,13 @@ drop table t1; ...@@ -77,10 +77,13 @@ drop table t1;
# warnings (for both strings and numbers) # warnings (for both strings and numbers)
# #
create table t1 (t datetime); create table t1 (t datetime);
insert into t1 values (20030102030460),(20030102036301),(20030102240401),(20030132030401),(20031302030460); insert into t1 values (20030102030460),(20030102036301),(20030102240401),
(20030132030401),(20031302030401),(100001202030401);
select * from t1; select * from t1;
delete from t1; delete from t1;
insert into t1 values ("20030102030460"),("20030102036301"),("20030102240401"),("20030132030401"),("20031302030460"); insert into t1 values
("2003-01-02 03:04:60"),("2003-01-02 03:63:01"),("2003-01-02 24:04:01"),
("2003-01-32 03:04:01"),("2003-13-02 03:04:01"), ("10000-12-02 03:04:00");
select * from t1; select * from t1;
delete from t1; delete from t1;
insert into t1 values ("0000-00-00 00:00:00 some trailer"),("2003-01-01 00:00:00 some trailer"); insert into t1 values ("0000-00-00 00:00:00 some trailer"),("2003-01-01 00:00:00 some trailer");
......
...@@ -343,7 +343,8 @@ str_to_datetime(const char *str, uint length, MYSQL_TIME *l_time, ...@@ -343,7 +343,8 @@ str_to_datetime(const char *str, uint length, MYSQL_TIME *l_time,
(l_time->month || l_time->day)) (l_time->month || l_time->day))
l_time->year+= (l_time->year < YY_PART_YEAR ? 2000 : 1900); l_time->year+= (l_time->year < YY_PART_YEAR ? 2000 : 1900);
if (number_of_fields < 3 || l_time->month > 12 || if (number_of_fields < 3 ||
l_time->year > 9999 || l_time->month > 12 ||
l_time->day > 31 || l_time->hour > 23 || l_time->day > 31 || l_time->hour > 23 ||
l_time->minute > 59 || l_time->second > 59 || l_time->minute > 59 || l_time->second > 59 ||
(!(flags & TIME_FUZZY_DATE) && (l_time->month == 0 || l_time->day == 0))) (!(flags & TIME_FUZZY_DATE) && (l_time->month == 0 || l_time->day == 0)))
...@@ -720,10 +721,10 @@ my_system_gmt_sec(const MYSQL_TIME *t, long *my_timezone, bool *in_dst_time_gap) ...@@ -720,10 +721,10 @@ my_system_gmt_sec(const MYSQL_TIME *t, long *my_timezone, bool *in_dst_time_gap)
/* Set MYSQL_TIME structure to 0000-00-00 00:00:00.000000 */ /* Set MYSQL_TIME structure to 0000-00-00 00:00:00.000000 */
void set_zero_time(MYSQL_TIME *tm) void set_zero_time(MYSQL_TIME *tm, enum enum_mysql_timestamp_type time_type)
{ {
bzero((void*) tm, sizeof(*tm)); bzero((void*) tm, sizeof(*tm));
tm->time_type= MYSQL_TIMESTAMP_NONE; tm->time_type= time_type;
} }
......
...@@ -4086,6 +4086,10 @@ int Field_datetime::store(longlong nr) ...@@ -4086,6 +4086,10 @@ int Field_datetime::store(longlong nr)
void Field_datetime::store_time(TIME *ltime,timestamp_type type) void Field_datetime::store_time(TIME *ltime,timestamp_type type)
{ {
longlong tmp; longlong tmp;
/*
We don't perform range checking here since values stored in TIME
structure always fit into DATETIME range.
*/
if (type == MYSQL_TIMESTAMP_DATE || type == MYSQL_TIMESTAMP_DATETIME) if (type == MYSQL_TIMESTAMP_DATE || type == MYSQL_TIMESTAMP_DATETIME)
tmp=((ltime->year*10000L+ltime->month*100+ltime->day)*LL(1000000)+ tmp=((ltime->year*10000L+ltime->month*100+ltime->day)*LL(1000000)+
(ltime->hour*10000L+ltime->minute*100+ltime->second)); (ltime->hour*10000L+ltime->minute*100+ltime->second));
......
...@@ -837,6 +837,21 @@ void Item_param::set_double(double d) ...@@ -837,6 +837,21 @@ void Item_param::set_double(double d)
} }
/*
Set parameter value from TIME value.
SYNOPSIS
set_time()
tm - datetime value to set (time_type is ignored)
type - type of datetime value
max_length_arg - max length of datetime value as string
NOTE
If we value to be stored is not normalized, zero value will be stored
instead and proper warning will be produced. This function relies on
the fact that even wrong value sent over binary protocol fits into
MAX_DATE_STRING_REP_LENGTH buffer.
*/
void Item_param::set_time(TIME *tm, timestamp_type type, uint32 max_length_arg) void Item_param::set_time(TIME *tm, timestamp_type type, uint32 max_length_arg)
{ {
DBUG_ENTER("Item_param::set_time"); DBUG_ENTER("Item_param::set_time");
...@@ -844,6 +859,17 @@ void Item_param::set_time(TIME *tm, timestamp_type type, uint32 max_length_arg) ...@@ -844,6 +859,17 @@ void Item_param::set_time(TIME *tm, timestamp_type type, uint32 max_length_arg)
value.time= *tm; value.time= *tm;
value.time.time_type= type; value.time.time_type= type;
if (value.time.year > 9999 || value.time.month > 12 ||
value.time.day > 31 ||
type != MYSQL_TIMESTAMP_TIME && value.time.hour > 23 ||
value.time.minute > 59 || value.time.second > 59)
{
char buff[MAX_DATE_STRING_REP_LENGTH];
uint length= my_TIME_to_str(&value.time, buff);
make_truncated_value_warning(current_thd, buff, length, type);
set_zero_time(&value.time, MYSQL_TIMESTAMP_ERROR);
}
state= TIME_VALUE; state= TIME_VALUE;
maybe_null= 0; maybe_null= 0;
max_length= max_length_arg; max_length= max_length_arg;
......
...@@ -349,12 +349,6 @@ static void set_param_time(Item_param *param, uchar **pos, ulong len) ...@@ -349,12 +349,6 @@ static void set_param_time(Item_param *param, uchar **pos, ulong len)
tm.neg= (bool) to[0]; tm.neg= (bool) to[0];
day= (uint) sint4korr(to+1); day= (uint) sint4korr(to+1);
/*
Note, that though ranges of hour, minute and second are not checked
here we rely on them being < 256: otherwise
we'll get buffer overflow in make_{date,time} functions,
which are called when time value is converted to string.
*/
tm.hour= (uint) to[5] + day * 24; tm.hour= (uint) to[5] + day * 24;
tm.minute= (uint) to[6]; tm.minute= (uint) to[6];
tm.second= (uint) to[7]; tm.second= (uint) to[7];
...@@ -369,7 +363,7 @@ static void set_param_time(Item_param *param, uchar **pos, ulong len) ...@@ -369,7 +363,7 @@ static void set_param_time(Item_param *param, uchar **pos, ulong len)
tm.day= tm.year= tm.month= 0; tm.day= tm.year= tm.month= 0;
} }
else else
set_zero_time(&tm); set_zero_time(&tm, MYSQL_TIMESTAMP_TIME);
param->set_time(&tm, MYSQL_TIMESTAMP_TIME, param->set_time(&tm, MYSQL_TIMESTAMP_TIME,
MAX_TIME_WIDTH * MY_CHARSET_BIN_MB_MAXLEN); MAX_TIME_WIDTH * MY_CHARSET_BIN_MB_MAXLEN);
*pos+= length; *pos+= length;
...@@ -388,11 +382,6 @@ static void set_param_datetime(Item_param *param, uchar **pos, ulong len) ...@@ -388,11 +382,6 @@ static void set_param_datetime(Item_param *param, uchar **pos, ulong len)
tm.year= (uint) sint2korr(to); tm.year= (uint) sint2korr(to);
tm.month= (uint) to[2]; tm.month= (uint) to[2];
tm.day= (uint) to[3]; tm.day= (uint) to[3];
/*
Note, that though ranges of hour, minute and second are not checked
here we rely on them being < 256: otherwise
we'll get buffer overflow in make_{date,time} functions.
*/
if (length > 4) if (length > 4)
{ {
tm.hour= (uint) to[4]; tm.hour= (uint) to[4];
...@@ -405,7 +394,7 @@ static void set_param_datetime(Item_param *param, uchar **pos, ulong len) ...@@ -405,7 +394,7 @@ static void set_param_datetime(Item_param *param, uchar **pos, ulong len)
tm.second_part= (length > 7) ? (ulong) sint4korr(to+7) : 0; tm.second_part= (length > 7) ? (ulong) sint4korr(to+7) : 0;
} }
else else
set_zero_time(&tm); set_zero_time(&tm, MYSQL_TIMESTAMP_DATETIME);
param->set_time(&tm, MYSQL_TIMESTAMP_DATETIME, param->set_time(&tm, MYSQL_TIMESTAMP_DATETIME,
MAX_DATETIME_WIDTH * MY_CHARSET_BIN_MB_MAXLEN); MAX_DATETIME_WIDTH * MY_CHARSET_BIN_MB_MAXLEN);
*pos+= length; *pos+= length;
...@@ -419,11 +408,7 @@ static void set_param_date(Item_param *param, uchar **pos, ulong len) ...@@ -419,11 +408,7 @@ static void set_param_date(Item_param *param, uchar **pos, ulong len)
if (length >= 4) if (length >= 4)
{ {
uchar *to= *pos; uchar *to= *pos;
/*
Note, that though ranges of hour, minute and second are not checked
here we rely on them being < 256: otherwise
we'll get buffer overflow in make_{date,time} functions.
*/
tm.year= (uint) sint2korr(to); tm.year= (uint) sint2korr(to);
tm.month= (uint) to[2]; tm.month= (uint) to[2];
tm.day= (uint) to[3]; tm.day= (uint) to[3];
...@@ -433,7 +418,7 @@ static void set_param_date(Item_param *param, uchar **pos, ulong len) ...@@ -433,7 +418,7 @@ static void set_param_date(Item_param *param, uchar **pos, ulong len)
tm.neg= 0; tm.neg= 0;
} }
else else
set_zero_time(&tm); set_zero_time(&tm, MYSQL_TIMESTAMP_DATE);
param->set_time(&tm, MYSQL_TIMESTAMP_DATE, param->set_time(&tm, MYSQL_TIMESTAMP_DATE,
MAX_DATE_WIDTH * MY_CHARSET_BIN_MB_MAXLEN); MAX_DATE_WIDTH * MY_CHARSET_BIN_MB_MAXLEN);
*pos+= length; *pos+= length;
......
...@@ -11088,6 +11088,139 @@ static void test_bug6096() ...@@ -11088,6 +11088,139 @@ static void test_bug6096()
} }
/*
Test of basic checks that are performed in server for components
of MYSQL_TIME parameters.
*/
static void test_datetime_ranges()
{
const char *stmt_text;
int rc, i;
MYSQL_STMT *stmt;
MYSQL_BIND bind[6];
MYSQL_TIME tm[6];
myheader("test_datetime_ranges");
stmt_text= "drop table if exists t1";
rc= mysql_real_query(mysql, stmt_text, strlen(stmt_text));
myquery(rc);
stmt_text= "create table t1 (year datetime, month datetime, day datetime, "
"hour datetime, min datetime, sec datetime)";
rc= mysql_real_query(mysql, stmt_text, strlen(stmt_text));
myquery(rc);
stmt= mysql_simple_prepare(mysql,
"INSERT INTO t1 VALUES (?, ?, ?, ?, ?, ?)");
check_stmt(stmt);
verify_param_count(stmt, 6);
bzero(bind, sizeof(bind));
for (i= 0; i < 6; i++)
{
bind[i].buffer_type= MYSQL_TYPE_DATETIME;
bind[i].buffer= &tm[i];
}
rc= mysql_stmt_bind_param(stmt, bind);
check_execute(stmt, rc);
tm[0].year= 2004; tm[0].month= 11; tm[0].day= 10;
tm[0].hour= 12; tm[0].minute= 30; tm[0].second= 30;
tm[0].second_part= 0; tm[0].neg= 0;
tm[5]= tm[4]= tm[3]= tm[2]= tm[1]= tm[0];
tm[0].year= 10000; tm[1].month= 13; tm[2].day= 32;
tm[3].hour= 24; tm[4].minute= 60; tm[5].second= 60;
rc= mysql_stmt_execute(stmt);
check_execute(stmt, rc);
DIE_UNLESS(mysql_warning_count(mysql) != 6);
verify_col_data("t1", "year", "0000-00-00 00:00:00");
verify_col_data("t1", "month", "0000-00-00 00:00:00");
verify_col_data("t1", "day", "0000-00-00 00:00:00");
verify_col_data("t1", "hour", "0000-00-00 00:00:00");
verify_col_data("t1", "min", "0000-00-00 00:00:00");
verify_col_data("t1", "sec", "0000-00-00 00:00:00");
mysql_stmt_close(stmt);
stmt_text= "delete from t1";
rc= mysql_real_query(mysql, stmt_text, strlen(stmt_text));
myquery(rc);
stmt= mysql_simple_prepare(mysql, "INSERT INTO t1 (year, month, day) "
"VALUES (?, ?, ?)");
check_stmt(stmt);
verify_param_count(stmt, 3);
/*
We reuse contents of bind and tm arrays left from previous part of test.
*/
for (i= 0; i < 3; i++)
bind[i].buffer_type= MYSQL_TYPE_DATE;
rc= mysql_stmt_bind_param(stmt, bind);
check_execute(stmt, rc);
rc= mysql_stmt_execute(stmt);
check_execute(stmt, rc);
DIE_UNLESS(mysql_warning_count(mysql) != 3);
verify_col_data("t1", "year", "0000-00-00 00:00:00");
verify_col_data("t1", "month", "0000-00-00 00:00:00");
verify_col_data("t1", "day", "0000-00-00 00:00:00");
mysql_stmt_close(stmt);
stmt_text= "drop table t1";
rc= mysql_real_query(mysql, stmt_text, strlen(stmt_text));
myquery(rc);
stmt_text= "create table t1 (day_ovfl time, day time, hour time, min time, sec time)";
rc= mysql_real_query(mysql, stmt_text, strlen(stmt_text));
myquery(rc);
stmt= mysql_simple_prepare(mysql,
"INSERT INTO t1 VALUES (?, ?, ?, ?, ?)");
check_stmt(stmt);
verify_param_count(stmt, 5);
/*
Again we reuse what we can from previous part of test.
*/
for (i= 0; i < 5; i++)
bind[i].buffer_type= MYSQL_TYPE_TIME;
rc= mysql_stmt_bind_param(stmt, bind);
check_execute(stmt, rc);
tm[0].year= 0; tm[0].month= 0; tm[0].day= 10;
tm[0].hour= 12; tm[0].minute= 30; tm[0].second= 30;
tm[0].second_part= 0; tm[0].neg= 0;
tm[4]= tm[3]= tm[2]= tm[1]= tm[0];
tm[0].day= 35; tm[1].day= 34; tm[2].hour= 30; tm[3].minute= 60; tm[4].second= 60;
rc= mysql_stmt_execute(stmt);
check_execute(stmt, rc);
DIE_UNLESS(mysql_warning_count(mysql) != 2);
verify_col_data("t1", "day_ovfl", "838:59:59");
verify_col_data("t1", "day", "828:30:30");
verify_col_data("t1", "hour", "270:30:30");
verify_col_data("t1", "min", "00:00:00");
verify_col_data("t1", "sec", "00:00:00");
mysql_stmt_close(stmt);
stmt_text= "drop table t1";
rc= mysql_real_query(mysql, stmt_text, strlen(stmt_text));
myquery(rc);
}
/* /*
Read and parse arguments and MySQL options from my.cnf Read and parse arguments and MySQL options from my.cnf
*/ */
...@@ -11404,6 +11537,8 @@ int main(int argc, char **argv) ...@@ -11404,6 +11537,8 @@ int main(int argc, char **argv)
test_bug6046(); /* NATURAL JOIN transformation works in PS */ test_bug6046(); /* NATURAL JOIN transformation works in PS */
test_bug6081(); /* test of mysql_create_db()/mysql_rm_db() */ test_bug6081(); /* test of mysql_create_db()/mysql_rm_db() */
test_bug6096(); /* max_length for numeric columns */ test_bug6096(); /* max_length for numeric columns */
test_datetime_ranges(); /* Test if basic checks are performed for
components of MYSQL_TIME parameters */
/* /*
XXX: PLEASE RUN THIS PROGRAM UNDER VALGRIND AND VERIFY THAT YOUR TEST XXX: PLEASE RUN THIS PROGRAM UNDER VALGRIND AND VERIFY THAT YOUR TEST
DOESN'T CONTAIN WARNINGS/ERRORS BEFORE YOU PUSH. DOESN'T CONTAIN WARNINGS/ERRORS BEFORE YOU PUSH.
......
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