commit - 05ed2619457dea473828d9adb226665354a6bce0
commit + 0fde12ee7e3409f4e14dbc784d8ab232bb5ff694
blob - fa42293e1dca16558dfde4971a083f9e126a7385
blob + c7d6d206f693e55e13c0f721cb0421829abdc885
--- tests/tarantool_datetime_new.lua
+++ tests/tarantool_datetime_new.lua
---[[
- 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,
})
-- 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,
})
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,
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
+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
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(<buf>) == datetime.parse(<buf>, { format = <fmt> })
+ 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
+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
+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,
+}