use omnilua::{Lua, LuaVersion, Value};
const ALL: [LuaVersion; 5] = [
LuaVersion::V51,
LuaVersion::V52,
LuaVersion::V53,
LuaVersion::V54,
LuaVersion::V55,
];
fn eval_str(version: LuaVersion, code: &str) -> Vec<u8> {
let lua = Lua::new_versioned(version);
match lua.load(code).eval::<Value>() {
Ok(Value::String(s)) => s
.as_bytes()
.unwrap_or_else(|e| panic!("string bytes under {version:?} for `{code}`: {e:?}"))
.to_vec(),
Ok(other) => panic!("`{code}` under {version:?} returned {other:?}, expected a string"),
Err(e) => panic!("eval of `{code}` failed under {version:?}: {e:?}"),
}
}
fn eval_int(version: LuaVersion, code: &str) -> i64 {
let lua = Lua::new_versioned(version);
match lua.load(code).eval::<Value>() {
Ok(Value::Integer(i)) => i,
Ok(Value::Number(n)) if n.fract() == 0.0 => n as i64,
Ok(other) => panic!("`{code}` under {version:?} returned {other:?}, expected an integer"),
Err(e) => panic!("eval of `{code}` failed under {version:?}: {e:?}"),
}
}
fn assert_true(version: LuaVersion, code: &str) {
let lua = Lua::new_versioned(version);
match lua.load(code).eval::<Value>() {
Ok(Value::Boolean(true)) => {}
Ok(other) => panic!("`{code}` under {version:?} returned {other:?}, expected true"),
Err(e) => panic!("eval of `{code}` failed under {version:?}: {e:?}"),
}
}
fn eval_err(version: LuaVersion, code: &str) -> String {
let lua = Lua::new_versioned(version);
match lua.load(code).eval::<Value>() {
Ok(v) => panic!("expected error under {version:?} for `{code}`, got {v:?}"),
Err(e) => e.message_lossy(),
}
}
#[test]
fn date_utc_table_at_epoch_zero() {
for v in ALL {
assert_eq!(eval_int(v, "return os.date('!*t', 0).year"), 1970, "{v:?}");
assert_eq!(eval_int(v, "return os.date('!*t', 0).month"), 1, "{v:?}");
assert_eq!(eval_int(v, "return os.date('!*t', 0).day"), 1, "{v:?}");
assert_eq!(eval_int(v, "return os.date('!*t', 0).hour"), 0, "{v:?}");
assert_eq!(eval_int(v, "return os.date('!*t', 0).min"), 0, "{v:?}");
assert_eq!(eval_int(v, "return os.date('!*t', 0).sec"), 0, "{v:?}");
assert_eq!(eval_int(v, "return os.date('!*t', 0).wday"), 5, "{v:?}");
assert_eq!(eval_int(v, "return os.date('!*t', 0).yday"), 1, "{v:?}");
}
}
#[test]
fn date_utc_iso_strings_at_fixed_epochs() {
for v in ALL {
assert_eq!(
eval_str(v, "return os.date('!%Y-%m-%dT%H:%M:%S', 0)"),
b"1970-01-01T00:00:00",
"{v:?}"
);
assert_eq!(
eval_str(v, "return os.date('!%Y-%m-%dT%H:%M:%S', 1000000000)"),
b"2001-09-09T01:46:40",
"{v:?}"
);
assert_eq!(
eval_str(v, "return os.date('!%Y-%m-%dT%H:%M:%S', 1700000000)"),
b"2023-11-14T22:13:20",
"{v:?}"
);
assert_eq!(
eval_str(v, "return os.date('!%Y-%m-%d %H:%M:%S', -1)"),
b"1969-12-31 23:59:59",
"{v:?}"
);
assert_eq!(
eval_str(v, "return os.date('!%Y-%m-%d', -100000000)"),
b"1966-10-31",
"{v:?}"
);
}
}
#[test]
fn date_utc_english_names_are_c_locale() {
for v in ALL {
assert_eq!(
eval_str(v, "return os.date('!%a|%A|%b|%B', 1700000000)"),
b"Tue|Tuesday|Nov|November",
"{v:?}"
);
}
}
#[test]
fn date_utc_numeric_specifiers() {
for v in ALL {
assert_eq!(
eval_str(v, "return os.date('!%y|%C|%m|%d|%e|%H|%I|%M|%S|%j|%w|%u', 1700000000)"),
b"23|20|11|14|14|22|10|13|20|318|2|2",
"{v:?}"
);
assert_eq!(
eval_str(v, "return os.date('!%F|%T|%R|%D', 1700000000)"),
b"2023-11-14|22:13:20|22:13|11/14/23",
"{v:?}"
);
}
}
#[test]
fn date_literal_passthrough_and_percent_escape() {
for v in ALL {
assert_eq!(eval_str(v, "return os.date('!ab%Ycd', 0)"), b"ab1970cd", "{v:?}");
assert_eq!(eval_str(v, "return os.date('!%%', 0)"), b"%", "{v:?}");
assert_eq!(eval_str(v, "return os.date('')"), b"", "{v:?}");
assert_eq!(eval_str(v, "return os.date('!')"), b"", "{v:?}");
assert_eq!(eval_str(v, "return os.date('!\\0\\0')"), b"\0\0", "{v:?}");
}
}
#[test]
fn time_local_round_trip_is_host_independent() {
let probe = "\
local function rt(t) return os.time(os.date('*t', t)) == t end \
return rt(0) and rt(1000) and rt(1000000000) and rt(1700000000) and rt(0x7fffffff)";
for v in ALL {
assert_true(v, probe);
}
}
#[test]
fn time_default_fields_normalize_into_epoch() {
let probe = "\
local a = os.time{year=2000, month=1, day=1} \
local b = os.time{year=2000, month=1, day=1, hour=12, min=0, sec=0} \
local c = os.time{year=2023, month=13, day=1, hour=0, min=0, sec=0} \
local d = os.time{year=2024, month=1, day=1, hour=0, min=0, sec=0} \
return a == b and c == d";
for v in ALL {
assert_true(v, probe);
}
}
#[test]
fn time_missing_field_names_first_unread_required_field_crossversion() {
for v in [LuaVersion::V51, LuaVersion::V52, LuaVersion::V53] {
assert!(
eval_err(v, "return os.time({})").contains("field 'day' missing in date table"),
"{v:?}: {}",
eval_err(v, "return os.time({})")
);
}
for v in [LuaVersion::V54, LuaVersion::V55] {
assert!(
eval_err(v, "return os.time({})").contains("field 'year' missing in date table"),
"{v:?}: {}",
eval_err(v, "return os.time({})")
);
}
for v in [LuaVersion::V54, LuaVersion::V55] {
assert!(
eval_err(v, "return os.time({year=2000})")
.contains("field 'month' missing in date table"),
"{v:?}"
);
}
}
#[test]
fn time_non_integer_field_is_unchecked_pre_5_3_crossversion() {
let code = "return os.time({year=1000, month=1, day=1, hour='x'})";
for v in [LuaVersion::V51, LuaVersion::V52] {
let lua = Lua::new_versioned(v);
match lua.load(code).eval::<Value>() {
Ok(Value::Integer(_)) | Ok(Value::Number(_)) => {}
other => panic!("{v:?}: expected a number (no type-check), got {other:?}"),
}
}
for v in [LuaVersion::V53, LuaVersion::V54, LuaVersion::V55] {
assert!(
eval_err(v, code).contains("field 'hour' is not an integer"),
"{v:?}: {}",
eval_err(v, code)
);
}
let code2 = "return os.time({year=1000, month=1, day=1, hour=1.5})";
for v in [LuaVersion::V51, LuaVersion::V52] {
let lua = Lua::new_versioned(v);
assert!(
matches!(
lua.load(code2).eval::<Value>(),
Ok(Value::Integer(_)) | Ok(Value::Number(_))
),
"{v:?}: fractional hour must not raise pre-5.3"
);
}
for v in [LuaVersion::V53, LuaVersion::V54, LuaVersion::V55] {
assert!(eval_err(v, code2).contains("field 'hour' is not an integer"), "{v:?}");
}
}
#[test]
fn time_out_of_bound_field_is_unchecked_pre_5_3_crossversion() {
let code = "return os.time({year=0, month=1, day=2^32})";
for v in [LuaVersion::V51, LuaVersion::V52] {
let lua = Lua::new_versioned(v);
assert!(
matches!(
lua.load(code).eval::<Value>(),
Ok(Value::Integer(_)) | Ok(Value::Number(_))
),
"{v:?}: huge day must not raise pre-5.3 (no bounds check)"
);
}
for v in [LuaVersion::V53, LuaVersion::V54, LuaVersion::V55] {
assert!(
eval_err(v, code).contains("field 'day' is out-of-bound"),
"{v:?}: {}",
eval_err(v, code)
);
}
}
#[test]
fn date_invalid_specifier_is_unvalidated_on_5_1_crossversion() {
for v in [LuaVersion::V51] {
let lua = Lua::new_versioned(v);
assert!(
lua.load("return os.date('!%')").eval::<Value>().is_ok(),
"5.1 must not raise on a bare trailing %"
);
let lua = Lua::new_versioned(v);
assert!(
lua.load("return os.date('!%9')").eval::<Value>().is_ok(),
"5.1 must not raise on an unknown specifier"
);
}
for v in [LuaVersion::V52, LuaVersion::V53, LuaVersion::V54, LuaVersion::V55] {
assert!(
eval_err(v, "return os.date('!%')").contains("invalid conversion specifier"),
"{v:?}"
);
assert!(
eval_err(v, "return os.date('!%9')").contains("invalid conversion specifier"),
"{v:?}"
);
}
}