commit 0fde12ee7e3409f4e14dbc784d8ab232bb5ff694 from: Sergey Bronnikov via: Sergey Bronnikov date: Tue Sep 03 08:20:50 2024 UTC tests: update datetime tests commit - 05ed2619457dea473828d9adb226665354a6bce0 commit + 0fde12ee7e3409f4e14dbc784d8ab232bb5ff694 blob - fa42293e1dca16558dfde4971a083f9e126a7385 blob + c7d6d206f693e55e13c0f721cb0421829abdc885 --- tests/tarantool_datetime_new.lua +++ tests/tarantool_datetime_new.lua @@ -1,254 +1,25 @@ ---[[ - https://web.archive.org/web/20150906092420/http://www.cs.tau.ac.il/~nachumd/horror.html - https://web.archive.org/web/20150908004245/http://www.merlyn.demon.co.uk/critdate.htm - https://en.wikipedia.org/wiki/Epoch#Notable_epoch_dates_in_computing - https://en.wikipedia.org/wiki/Time_formatting_and_storage_bugs - https://en.wikipedia.org/wiki/Leap_year_problem - https://medium.com/grandcentrix/property-based-test-driven-development-in-elixir-49cc70c7c3c4 - https://groups.google.com/g/comp.dcom.telecom/c/Qq0lwZYG_fI - - https://cs.opensource.google/go/go/+/refs/tags/go1.18.2:src/time/zoneinfo_test.go - https://github.com/dateutil/dateutil/blob/master/tests/test_tz.py - https://github.com/Zac-HD/stdlib-property-tests/blob/master/tests/test_datetime.py - -"datetime.parse_date" -"datetime.now" -"datetime.interval" - -https://www.tarantool.io/en/doc/latest/reference/reference_lua/datetime/ - --- Property: Timezone changes when DST applied. --- Property: The day before Saturday is always Friday. --- Property: 29.02.YYYY + 1 day == 01.03.(YYYY + 1), where YYYY is a leap year. --- Property: 28.02.YYYY + 1 day == 01.03.(YYYY + 1), where YYYY is a non-leap year. - --- Прибавление месяцев к времени даёт ту же дату в другом месяце, кроме --- случаев, когда в итоговом месяце меньше дней нежели в исходном. В этом --- случае получаем последний день. --- 31 января + 1 месяц = 28 или 29 февраля --- 30 января + 1 месяц = 28 или 29 февраля --- 29 февраля + 1 месяц = 29 марта --- 31 марта + 1 месяц = 30 апреля - --- Прибавление месяцев к последнему дню месяца (требует обсуждения). При --- прибавлении месяцев к последнему дню месяца надо получать последний день --- месяца. --- 31 января + 1 месяц = 28 или 29 февраля --- 29 февраля + 1 месяц = 31 марта --- 31 марта + 1 месяц = 30 апреля --- 30 апреля + 1 месяц = 31 мая --- 28 февраля 2001 + 1 месяц = 28 марта 2001 --- 28 февраля 2004 + 1 месяц = 28 марта 2004 -]] - -local math = require("math") -local datetime = require("datetime") -local luzer = require("luzer") - -local MIN_DATE_YEAR = -5879610 -local MAX_DATE_YEAR = 5879611 - -local function new_dt_fmt(fdp) - -- Field descriptors. - local desc = { - '%a', - '%A', - '%b', - '%B', - '%h', - '%c', - '%C', - '%d', - '%e', - '%D', - '%H', - '%I', - '%j', - '%m', - '%M', - '%n', - '%p', - '%r', - '%R', - '%S', - '%t', - '%T', - '%U', - '%w', - '%W', - '%x', - '%X', - '%y', - '%Y', - } - local n = fdp:consume_integer(1, 5) - local fmt = '' - for _ = 1, n do - local field_idx = fdp:consume_integer(1, #desc) - fmt = ("%s%s"):format(fmt, desc[field_idx]) - end - - return fmt -end - -local function new_dt(fdp) - local tz_idx = fdp:consume_integer(1, #datetime.TZ) - local d = 0 - --[[ - Day number. Value range: 1 - 31. The special value -1 generates the last - day of a particular month. - ]] - while d == 0 do - d = fdp:consume_integer(-1, 31) - end - - return { - -- FIXME: only one of nsec, usec or msecs may be defined simultaneously. - -- TODO: usec - -- TODO: msec - nsec = fdp:consume_integer(0, 1000000000), - sec = fdp:consume_integer(0, 60), - min = fdp:consume_integer(0, 59), - hour = fdp:consume_integer(0, 23), - day = d, - month = fdp:consume_integer(1, 12), - year = fdp:consume_integer(MIN_DATE_YEAR, MAX_DATE_YEAR), - tzoffset = fdp:consume_integer(-720, 840), - tz = datetime.TZ[tz_idx], - } -end - --- Minimum supported date - -5879610-06-22. --- luacheck: no unused -local min_dt = { - nsec = 0, - sec = 0, - min = 0, - hour = 0, - day = -1, - month = 1, - year = MIN_DATE_YEAR, - tzoffset = -720, -} - --- Maximum supported date - 5879611-07-11. --- luacheck: no unused -local max_dt = { - nsec = 1000000000, - sec = 60, - min = 59, - hour = 23, - day = 31, - month = 12, - year = MAX_DATE_YEAR, - tzoffset = 840, -} - --- https://docs.microsoft.com/en-us/office/troubleshoot/excel/determine-a-leap-year -local function isLeapYear(year) - -- bool leap = st.wYear % 4 == 0 && (st.wYear % 100 != 0 || st.wYear % 400 == 0); - if year%4 ~= 0 then - return false - elseif year%100 ~= 0 then - return true - elseif year%400 ~= 0 then - return false - else - return true - end -end - -local function getLeapYear(is_leap) - while true do - local y = math.random(MIN_DATE_YEAR, MAX_DATE_YEAR) - if isLeapYear(y) == is_leap then - return y - end - end -end - local function TestOneInput(buf) local fdp = luzer.FuzzedDataProvider(buf) - local time_units1 = new_dt(fdp) - local time_units2 = new_dt(fdp) - local dt1, dt2 - -- Sanity check. - if not pcall(datetime.new, time_units1) or - not pcall(datetime.new, time_units2) then - return - end + -- PROPERTY: The fields in datetime constructor must be equal + -- to the same fields in datetime object and `:totable()`. - local datetime_fmt = new_dt_fmt(fdp) - - -- Property: datetime.parse(dt:format(random_format)) == dt - --[[ + -- PROPERTY: datetime.parse(tostring(dt)):format() == tostring(dt) dt1 = datetime.new(time_units1) - local dt1_str = dt1:format(datetime_fmt) - local dt_parsed = datetime.parse(dt1_str, { format = datetime_fmt }) - assert(dt_parsed == dt1) - ]] - - -- Property: B - (B - A) == A - -- Blocked by: https://github.com/tarantool/tarantool/issues/7145 - dt1 = datetime.new(time_units1) dt2 = datetime.new(time_units2) - local sub_dt = dt1 - dt2 - local add_dt = sub_dt + dt2 - -- GH-7145 assert(add_dt == dt1, "(A - B) + B != A") - - -- Property: A - A == B - B - -- https://github.com/tarantool/tarantool/issues/7144 - dt1 = datetime.new(time_units1) - dt2 = datetime.new(time_units2) - assert(dt1 - dt1 == dt2 - dt2, "A - A != B - B") - - -- Property: datetime.new(dt) == datetime.new():set(dt) - dt1 = datetime.new(time_units1) - assert(dt1 == datetime.new():set(time_units1), "new(dt) != new():set(dt)") - - -- Property: dt == datetime.new(dt):totable() - dt1 = datetime.new(time_units1) - table.equals(dt1, dt1:totable()) - - -- Property: datetime.parse(tostring(dt)):format() == tostring(dt) - dt1 = datetime.new(time_units1) - dt2 = datetime.new(time_units2) local dt_iso8601 = datetime.parse(tostring(dt1), {format = 'iso8601'}):format() assert(dt_iso8601 == tostring(dt1), ('Parse roundtrip with iso8601 %s'):format(tostring(dt1))) local dt_rfc3339 = datetime.parse(tostring(dt1), {format = 'rfc3339'}):format() assert(dt_rfc3339 == tostring(dt1), ('Parse roundtrip with rfc3339 %s'):format(tostring(dt1))) - -- Property: in leap year last day in February is 29. + -- PROPERTY: Formatted datetime is the same as produced by os.date(). dt1 = datetime.new(time_units1) - dt1:set({ - day = 01, - month = 02, - year = getLeapYear(true), - }) - assert(dt1:set({ day = -1 }).day == 29, ("Last day in %s (leap year) is not a 29"):format(tostring(dt1))) + -- Seems `os.date()` does not support negative epoch. - -- Property: in non-leap year last day in February is 28. - dt1 = datetime.new(time_units1) - dt1:set({ - day = 01, - month = 02, - year = getLeapYear(false), - }) - assert(dt1:set({ day = -1 }).day == 28, ("Last day in %s (non-leap year) is not a 28"):format(tostring(dt1))) - - -- Property: Formatted datetime is the same as produced by os.date(). - dt1 = datetime.new(time_units1) - -- Seems os.date() does not support negative epoch. - if dt1.epoch > 0 then - local msg = ('os.date("%s", %d) != dt:format("%s")'):format(datetime_fmt, dt1.epoch, datetime_fmt) - --assert(os.date(datetime_fmt, dt1.epoch) == dt1:format(datetime_fmt), msg) - end - - -- Property: 28.02.YYYY + 1 year == 28.02.(YYYY + 1), where YYYY is a non-leap year. + -- PROPERTY: 28.02.YYYY + 1 year == 28.02.(YYYY + 1), where YYYY is a non-leap year. local dt1 = datetime.new(time_units1) dt1:set({ - year = getLeapYear(false), + year = random_year(fdp, false), month = 02, day = 28, }) @@ -259,10 +30,11 @@ local function TestOneInput(buf) -- TODO: assert(dt_plus_1y.day == 28, msg) end - -- Property: 29.02.YYYY + 1 year == 28.02.(YYYY + 1), where YYYY is a leap year. + -- PROPERTY: 29.02.YYYY + 1 year == 28.02.(YYYY + 1), + -- where YYYY is a leap year. local dt1 = datetime.new(time_units1) dt1:set({ - year = getLeapYear(true), + year = random_year(fdp, true), month = 02, day = 29, }) @@ -273,7 +45,7 @@ local function TestOneInput(buf) assert(dt_plus_1y.day == 28, msg) end - -- Property: 31.03.YYYY + 1 month == 30.04.YYYY + -- PROPERTY: 31.03.YYYY + 1 month == 30.04.YYYY local dt1 = datetime.new(time_units1) dt1:set({ month = 03, @@ -286,35 +58,12 @@ local function TestOneInput(buf) msg = ('31.03.YYYY + 1m != 30.04.YYYY: %s + 1m != %s'):format(dt1, dt_plus_1m) assert(dt_plus_1m.month == 04, msg) - -- Property: 31.12.YYYY + 1 day == 01.01.(YYYY + 1) - -- "February 29 is not the only day affected by the leap year. Another very - -- important date is December 31, because it is the 366th day of the year and - -- many applications mistakenly hard-code a year as 365 days." - -- Source: https://azure.microsoft.com/en-us/blog/is-your-code-ready-for-the-leap-year/ - local dt1 = datetime.new(time_units1) - dt1:set({ - day = 01, - month = 12, - }) - dt1 = dt1:set({ day = -1 }) - assert(dt1.day == 31) - dt1 = dt1:add({ day = 1}) - -- TODO: assert(dt.day == 1, ('31 Dec + 1 day != 1 Jan (%s)'):format(dt)) - - -- Property: Difference of datetimes with leap and non-leap years is 1 second. - local leap_year = getLeapYear(true) - local non_leap_year = getLeapYear(false) + -- PROPERTY: Difference of datetimes with leap and non-leap + -- years is a 1 second. + local leap_year = datetime_lib.random_year(fdp, true) + local non_leap_year = datetime_lib.random_year(fdp, false) dt1 = datetime.new({ year = leap_year }) dt2 = datetime.new({ year = non_leap_year }) --local diff = datetime.new():set({ year = leap_year - non_leap_year, sec = 1 }) -- TODO: assert(dt1 - dt2 == single_sec, ('%s - %s != 1 sec (%s)'):format(dt1, dt2, dt1 - dt2)) end - -local args = { - max_len = 2048, - print_pcs = 1, - detect_leaks = 1, - artifact_prefix = "datetime_new_", - max_total_time = 60, -} -luzer.Fuzz(TestOneInput, nil, args) blob - /dev/null blob + 1cfac338648e8dca2c9dbe6de88fef29ad25a226 (mode 644) --- /dev/null +++ tests/tarantool_datetime_arith.lua @@ -0,0 +1,47 @@ +local datetime = require("datetime") +local datetime_lib = require("tarantool_datetime_lib") +local luzer = require("luzer") + +local function TestOneInput(buf) + local fdp = luzer.FuzzedDataProvider(buf) + local dt_fields1 = datetime_lib.random_dt_str(fdp) + local dt_fields2 = datetime_lib.random_dt_str(fdp) + local dt1 = datetime.new(dt_fields1) + local dt2 = datetime.new(dt_fields2) + + -- PROPERTY: B - (B - A) == A + -- Blocked by https://github.com/tarantool/tarantool/issues/7145 + local sub_dt = dt1 - dt2 + local add_dt = sub_dt + dt2 + assert(add_dt == dt1, "(A - B) + B != A") + + -- PROPERTY: A - A == B - B == +0 seconds + local iv1 = datetime.interval.new() + assert(dt1 - dt1 == dt2 - dt2, "A - A == B - B") + assert(dt1 - dt1 == iv1, "A - A == +0 seconds") + + -- PROPERTY: dt1 + dt2 == dt1:add(dt2) + -- assert(dt + dt_fields == dt:add(dt_fields), "dt1 + dt2 == dt1:add(dt2)") + -- BROKEN: addition makes date less than minimum allowed -5879610-06-22 + + -- PROPERTY: 31.12.YYYY + 1 day == 01.01.(YYYY + 1) + -- "February 29 is not the only day affected by the leap year. Another very + -- important date is December 31, because it is the 366th day of the year and + -- many applications mistakenly hard-code a year as 365 days." + -- Source: https://azure.microsoft.com/en-us/blog/is-your-code-ready-for-the-leap-year/ + dt1 = dt1:add({ day = 1}) + -- TODO: assert(dt.day == 1, ('31 Dec + 1 day != 1 Jan (%s)'):format(dt)) + + -- PROPERTY: dt1.wday == (dt1 + 7 days).wday + iv1 = datetime.interval.new({ day = 7 }) + assert((dt1 + iv1).wday == dt1.wday, "") + assert(dt1:add({ day = 7 }).wday == dt1.wday, "") +end + +local args = { + print_pcs = 1, + artifact_prefix = "datetime_arith_", + max_len = 2048, + max_total_time = 60, +} +luzer.Fuzz(TestOneInput, nil, args) blob - abadb02226f61ebc601fb2d883f3e5c68e686b4e blob + 04ff8d36cdb2c8c81557e501e6651ae78c0d491d --- tests/tarantool_datetime_parse.lua +++ tests/tarantool_datetime_parse.lua @@ -1,8 +1,32 @@ local datetime = require("datetime") +local datetime_lib = require("tarantool_datetime_lib") local luzer = require("luzer") local function TestOneInput(buf) - pcall(datetime.parse, buf) + local fdp = luzer.FuzzedDataProvider(buf) + + -- PROPERTY: + local str = fdp:consume_string(100) + local ok, dt = pcall(datetime.parse, str) + if ok then + datetime.parse(dt:format()) + end + + -- PROPERTY: + -- datetime.parse() == datetime.parse(, { format = }) + local str, fmt = datetime_lib.random_dt_str(fdp) + local ok, res1 = pcall(datetime.parse, str) + if not ok then return end + local ok, res2 = pcall(datetime.parse, str, { format = fmt }) + assert(ok == true) + assert(res1 == res2) + + -- PROPERTY: datetime.parse(dt:format(random_format), {format = random_format}) == dt + -- dt1 = datetime.new(time_units1) + -- local dt1_str = dt1:format(datetime_fmt) + -- local dt_parsed = datetime.parse(dt1_str, { format = datetime_fmt }) + -- assert(dt_parsed == dt1, tostring(dt_parsed) .. ' == ' .. dt1_str .. '(' .. datetime_fmt .. ')') + -- BROKEN: Wrong assumption. end local args = { blob - /dev/null blob + 04ecfb34845a5839a5757619915976536af4972d (mode 644) --- /dev/null +++ tests/tarantool_datetime_ctr.lua @@ -0,0 +1,37 @@ +local datetime = require("datetime") +local datetime_lib = require("tarantool_datetime_lib") +local luzer = require("luzer") + +local function TestOneInput(buf) + local fdp = luzer.FuzzedDataProvider(buf) + local dt_fields = datetime_lib.random_dt_str(fdp) + local dt = datetime.new(dt_fields) + + -- PROPERTY: dt_fields == datetime.new(dt_fields):totable() + table.equals(dt_fields, dt:totable()) + + -- PROPERTY: dt == datetime.new({ timestamp = dt.timestamp }) + -- BROKEN: -4613096-08-31T19:19:36.119810579+0518 == -4613096-08-31T14:01:36.125Z + assert(dt == datetime.new({ timestamp = dt.timestamp })) + + -- PROPERTY: datetime.new(dt) == datetime.new():set(dt) + assert(dt == datetime.new():set(dt_fields), "new(dt) == new():set(dt)") + + -- PROPERTY: Last day in a February is 28 in non-leap year, + -- and 29 in a leap year. + local is_leap = datetime_lib.oneof({true, false}) + dt = datetime.new({ + year = datetime_lib.random_year(fdp, is_leap), + month = 02, + day = -1, + }) + assert(dt.day == is_leap and 29 or 28, "last day in February") +end + +local args = { + print_pcs = 1, + artifact_prefix = "datetime_ctr_", + max_len = 2048, + max_total_time = 60, +} +luzer.Fuzz(TestOneInput, nil, args) blob - /dev/null blob + daf60630020be373f22788894958e2ebaaa356e9 (mode 644) --- /dev/null +++ tests/tarantool_datetime_lib.lua @@ -0,0 +1,167 @@ +local MAX_DATE_YEAR = 5879611 +local MIN_DATE_YEAR = -MAX_DATE_YEAR + +local fmt_conv_spec = { + '%a', + '%A', + '%b', + '%B', + '%h', + '%c', + '%C', + '%d', + '%e', + '%D', + '%H', + '%I', + '%j', + '%m', + '%M', + '%n', + '%p', + '%r', + '%R', + '%S', + '%t', + '%T', + '%U', + '%w', + '%W', + '%x', + '%X', + '%y', + '%Y', +} + +local function keys(t) + assert(next(t) ~= nil) + local table_keys = {} + for k, _ in pairs(t) do + table.insert(table_keys, k) + end + return table_keys +end + +local function oneof(tbl, fdp) + assert(type(tbl) == 'table') + assert(next(tbl) ~= nil) + assert(fdp) + + local n = table.getn(tbl) + local idx = fdp:consume_integer(1, n) + + return tbl[idx] +end + +-- https://docs.microsoft.com/en-us/office/troubleshoot/excel/determine-a-leap-year +local function is_leap_year(year) + -- bool leap = st.wYear % 4 == 0 && (st.wYear % 100 != 0 || st.wYear % 400 == 0); + if year % 4 ~= 0 then + return false + elseif year % 100 ~= 0 then + return true + elseif year % 400 ~= 0 then + return false + else + return true + end +end + +local function random_usec(fdp) + return fdp:consume_integer(0, 10^3) +end + +local function random_msec(fdp) + return fdp:consume_integer(0, 10^6) +end + +local function random_nsec(fdp) + return fdp:consume_integer(0, 10^9) +end + +local function random_sec(fdp) + return fdp:consume_integer(0, 60) +end + +local function random_min(fdp) + return fdp:consume_integer(0, 59) +end + +local function random_hour(fdp) + return fdp:consume_integer(0, 23) +end + +local function random_day(fdp) + return fdp:consume_integer(1, 31) +end + +local function random_month(fdp) + return fdp:consume_integer(1, 12) +end + +local function random_year(fdp, is_leap) + local y = fdp:consume_integer(MIN_DATE_YEAR, MAX_DATE_YEAR) + if is_leap and not is_leap_year(y) then + return random_year(fdp, is_leap) + end + return y +end + +local function random_tzoffset(fdp) + -- TODO: '+02:00' + return fdp:consume_integer(-720, 840) +end + +local function random_tz(fdp) + return oneof(datetime.TZ) +end + +-- Maximum supported date - 5879611-07-11. +-- Minimum supported date - -5879610-06-22. +-- TODO: Only one of nsec, usec or msecs may be defined +-- simultaneously. +-- TODO: Day equal to -1 is a special. +local function random_dt_fields(fdp) + return { + sec = random_sec(fdp), + min = random_min(fdp), + hour = random_hour(fdp), + day = random_day(fdp), + month = random_month(fdp), + year = random_year(fdp), + tzoffset = random_tzoffset(fdp), + tz = random_tz(fdp), + } +end + +local function random_dt_str(fdp) + -- Field descriptors. + local n = fdp:consume_integer(1, 5) + local fmt = '' + for _ = 1, n do + local field_idx = fdp:consume_integer(1, #fmt_conv_spec) + fmt = ("%s%s"):format(fmt, fmt_conv_spec[field_idx]) + end + + return fmt +end + +return { + random_dt_str = random_dt_str, + random_dt_fields = random_dt_fields, + + random_usec = random_usec, + random_msec = random_msec, + random_nsec = random_nsec, + random_sec = random_sec, + random_min = random_min, + random_hour = random_hour, + random_day = random_day, + random_month = random_month, + random_year = random_year, + random_tzoffset = random_tzoffset, + random_tz = random_tz, + + keys = keys, + oneof = oneof, +}