Added new macro features. resolve_value + default: key. Added raises to prevent CliGen features from being used outside of a already parsed state, and worked on specs. Will be working on specs later on as well to finish covering regex & redo timeparse & relative_operations
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
require "../spec_helper"
|
||||
|
||||
@[CliGen::CommandInfo(description: "Abc", singleton_init: true)]
|
||||
class MyCmd < CliGen::Command
|
||||
|
||||
argument(abcdef : String = "123",
|
||||
description: "This is a thing"
|
||||
)
|
||||
argument(ghijkl : Int32 = 666,
|
||||
description: "The number of the devil"
|
||||
)
|
||||
argument(badvar : String,
|
||||
description: "This will fail on init"
|
||||
)
|
||||
|
||||
def main
|
||||
puts "abc"
|
||||
end
|
||||
|
||||
def get_badvar
|
||||
resolve_value badvar, default: "was unset"
|
||||
end
|
||||
end
|
||||
|
||||
@[CliGen::CommandInfo(description: "Def", parent: ::MyCmd, singleton_init: true)]
|
||||
class MySubCmd < CliGen::Command
|
||||
argument(dfdfdf : String = "abc",
|
||||
description: "ALKJSDFLSKDFJ"
|
||||
)
|
||||
def main
|
||||
resolve_value abcdef
|
||||
end
|
||||
end
|
||||
|
||||
@[CliGen::CommandInfo(description: "Def", parent: ::MySubCmd, singleton_init: true)]
|
||||
class MySubSubCmd < CliGen::Command
|
||||
def main
|
||||
resolve_value ghijkl
|
||||
end
|
||||
|
||||
def main2
|
||||
resolve_value badvar, default: "unset"
|
||||
end
|
||||
end
|
||||
|
||||
def get_handler_for(cls : String)
|
||||
if handler = CliGen::App.get.all_commands.find(&.meta.cls.== cls)
|
||||
handler
|
||||
else
|
||||
raise "ERROR"
|
||||
end
|
||||
end
|
||||
|
||||
describe CliGen::Command do
|
||||
before_each do
|
||||
ENV.delete("MYCMD_BADVAR")
|
||||
end
|
||||
|
||||
describe "#resolve_value" do
|
||||
it "does work 1 level deep" do
|
||||
MySubCmd.new.main.should eq("123")
|
||||
end
|
||||
|
||||
it "works 2 levels deep" do
|
||||
MySubSubCmd.new.main.should eq(666)
|
||||
end
|
||||
|
||||
it "If a argument is without a default value it will fall back to the default" do
|
||||
MySubSubCmd.new.main2.should eq("unset")
|
||||
end
|
||||
|
||||
it "will return the provided value if the Flag(T) has an ENVVAR to match" do
|
||||
ENV["MYCMD_BADVAR"]="ABC"
|
||||
MySubSubCmd.new.main2.should eq("ABC")
|
||||
end
|
||||
|
||||
it "works on instance variables" do
|
||||
a = "the cake was a lie"
|
||||
ENV["MYCMD_BADVAR"] = a
|
||||
handler = get_handler_for("MyCmd")
|
||||
MyCmd.new(handler: handler).get_badvar.should eq(a)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#initialize" do
|
||||
it "will raise CliGen::MissingRequiredFlagError on handler initialize if no flag value is set for badvar" do
|
||||
handler = get_handler_for("MyCmd")
|
||||
expect_raises(CliGen::MissingRequiredFlagError) do
|
||||
MyCmd.new(handler: handler)
|
||||
end
|
||||
end
|
||||
|
||||
it "will not raise if badvar is set via ENV VAR" do
|
||||
ENV["MYCMD_BADVAR"]="the cake was a lie"
|
||||
handler = get_handler_for("MyCmd")
|
||||
MyCmd.new(handler: handler).get_badvar.should eq("the cake was a lie")
|
||||
end
|
||||
end
|
||||
|
||||
describe ".get" do
|
||||
it "will raise CliGen::AppNotProcessedError if a user attempts to use .get before CliGen::App has processed commandline arguments" do
|
||||
expect_raises(CliGen::AppNotProcessedError) do
|
||||
MySubCmd.get
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -104,21 +104,21 @@ describe CliGen::Flag do
|
||||
before_each { ENV.delete("TEST") }
|
||||
|
||||
describe "#check!" do
|
||||
it "will throw CliGen::ReservedFlagError if -h is used as short" do
|
||||
expect_raises(CliGen::ReservedFlagError) do
|
||||
make_flag(type: Bool, short: "-h", long: "--not-help").check!
|
||||
it "will throw CliGen::ConfigurationError if a short is provided as a long" do
|
||||
expect_raises(CliGen::ConfigurationError) do
|
||||
make_flag(type: Bool, short: nil, long: "-n").check!
|
||||
end
|
||||
end
|
||||
|
||||
it "will throw CliGen::ReservedFlagError if --help is used in long" do
|
||||
expect_raises(CliGen::ReservedFlagError) do
|
||||
make_flag(type: Bool, short: "-b", long: "--help TOPIC").check!
|
||||
it "will throw CliGen::ConfigurationError if a long is provided as a short" do
|
||||
expect_raises(CliGen::ConfigurationError) do
|
||||
make_flag(type: Bool, short: "--b", long: "--help TOPIC").check!
|
||||
end
|
||||
end
|
||||
|
||||
it "will throw CliGen::ReservedFlagError if --help is used as long_key" do
|
||||
expect_raises(CliGen::ReservedFlagError) do
|
||||
make_flag(type: Bool, short: "-b", long: "--help").check!
|
||||
it "will throw CliGen::ConfigurationError if long flag does not have enough characters" do
|
||||
expect_raises(CliGen::ConfigurationError) do
|
||||
make_flag(type: Bool, short: "-b", long: "--h").check!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,344 @@
|
||||
require "../spec_helper"
|
||||
|
||||
|
||||
macro get_match(a, b, &work)
|
||||
match = CliGen::Regex::{{a}}.match({{b}}).not_nil!
|
||||
{{work.body}}
|
||||
end
|
||||
|
||||
macro does_match(a, b)
|
||||
CliGen::Regex::{{a}}.matches?({{b}}).should be_true
|
||||
end
|
||||
|
||||
macro doesnt_match(a, b)
|
||||
CliGen::Regex::{{a}}.matches?({{b}}).should be_false
|
||||
end
|
||||
|
||||
describe CliGen::Regex do
|
||||
describe "FLAG_REGEX" do
|
||||
it "will match LONG (--long) format" do
|
||||
does_match(FLAG_REGEX, "--long")
|
||||
end
|
||||
|
||||
it "will match a SHORT (-s) format" do
|
||||
does_match(FLAG_REGEX, "-s")
|
||||
end
|
||||
|
||||
it "will not match a LONG with an arg (--long=abc)" do
|
||||
doesnt_match(FLAG_REGEX, "--long=abc")
|
||||
end
|
||||
|
||||
it "will not match a SHORT with an arg (-s=abc)" do
|
||||
doesnt_match(FLAG_REGEX, "-s=abc")
|
||||
end
|
||||
end
|
||||
|
||||
describe "FLAG_WITH_ARG" do
|
||||
it "will match a LONG with arg without quotes (--long=arg)" do
|
||||
get_match(FLAG_WITH_ARG, "--long=arg") do
|
||||
match["flag"].should eq("--long")
|
||||
match["arg"].should eq("arg")
|
||||
end
|
||||
end
|
||||
|
||||
it "will match a LONG with arg with quotes (--long=\"arg\")" do
|
||||
get_match(FLAG_WITH_ARG, "--long=\"arg\"") do
|
||||
match["flag"].should eq("--long")
|
||||
match["arg"].should eq("arg")
|
||||
end
|
||||
end
|
||||
|
||||
it "will match a LONG with arg with quotes and spaces in the arg (--long=\"arg abcd ef\")" do
|
||||
get_match(FLAG_WITH_ARG, "--long=\"arg abcd ef\"") do
|
||||
match["flag"].should eq("--long")
|
||||
match["arg"].should eq("arg abcd ef")
|
||||
end
|
||||
end
|
||||
|
||||
it "will match a LONG with arg without quotes and with spaces in the arg (--long=arg abcd ef)" do
|
||||
get_match(FLAG_WITH_ARG, "--long=arg abcd ef") do
|
||||
match["flag"].should eq("--long")
|
||||
match["arg"].should eq("arg abcd ef")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "FLAG_MULTIPLE_SHORT" do
|
||||
it "does match a valid multi-short flag (-abcdef)" do
|
||||
does_match(FLAG_MULTIPLE_SHORT, "-abcdef")
|
||||
end
|
||||
|
||||
it "does not match a singleshort flag (-a)" do
|
||||
doesnt_match(FLAG_MULTIPLE_SHORT, "-a")
|
||||
end
|
||||
|
||||
{% for int in (0..9) %}
|
||||
it "does not match a multi-short flag with a digit in it (-abc{{int}}) as -{{int}} is not a valid short" do
|
||||
doesnt_match(FLAG_MULTIPLE_SHORT, "-abc{{int}}")
|
||||
end
|
||||
{% end %}
|
||||
|
||||
it "does not match a long flag (--long)" do
|
||||
doesnt_match(FLAG_MULTIPLE_SHORT, "--long")
|
||||
end
|
||||
end
|
||||
|
||||
describe "FLAG_LONG" do
|
||||
it "matches a valid long flag (--long)" do
|
||||
does_match(FLAG_LONG, "--long")
|
||||
end
|
||||
|
||||
it "does not match a short flag (-s)" do
|
||||
doesnt_match(FLAG_LONG, "-s")
|
||||
end
|
||||
|
||||
it "doesn't match a long flag with an arg" do
|
||||
doesnt_match(FLAG_LONG, "--long=abc")
|
||||
end
|
||||
end
|
||||
|
||||
describe "FLAG_SHORT" do
|
||||
it "matches a valid short flag (-s)" do
|
||||
does_match(FLAG_SHORT, "-s")
|
||||
end
|
||||
|
||||
{% for int in (0..9) %}
|
||||
it "does not match a digit flag (-{{int}})" do
|
||||
doesnt_match(FLAG_SHORT, "-{{int}}")
|
||||
end
|
||||
{% end %}
|
||||
end
|
||||
|
||||
describe "TIMEZONE" do
|
||||
{% for sign in %w[ - + ] %}
|
||||
{% for i in [ 2, 4, 7 ] %}
|
||||
{% for j in [ 0, 7, 59 ] %}
|
||||
{% format = "%s%02d%02d".id %}
|
||||
it "will match a valid offset ({{format}})" % [ {{sign}}, {{i}}, {{j}} ] do
|
||||
does_match(TIMEZONE, {{format.stringify}} % [ {{sign}}, {{i}}, {{j}} ])
|
||||
end
|
||||
{% end %}
|
||||
{% end %}
|
||||
{% end %}
|
||||
|
||||
it "won't accept any offsets above \"+2359\"" do
|
||||
doesnt_match(TIMEZONE, "-2400")
|
||||
end
|
||||
|
||||
it "won't accept any invalid minute values (+2361)" do
|
||||
doesnt_match(TIMEZONE, "-2361")
|
||||
end
|
||||
|
||||
it "able to parse fields out of the offset (-2330)" do
|
||||
get_match(TIMEZONE, "-2330") do
|
||||
match["offset_sign"].should eq("-")
|
||||
match["offset_hour"].should eq("23")
|
||||
match["offset_minute"].should eq("30")
|
||||
match["timezone"].should eq("-2330")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "TIME" do
|
||||
it "should be able to match a valid time (22:23:24) and parse the fields" do
|
||||
does_match(TIME, "22:23:24")
|
||||
get_match(TIME, "22:23:24") do
|
||||
match["hour"].should eq("22")
|
||||
match["minute"].should eq("23")
|
||||
match["second"].should eq("24")
|
||||
match["time"].should eq("22:23:24")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "DATE" do
|
||||
it "should be able to match a valid date (2026-08-10) and parse the fields" do
|
||||
does_match(DATE, "2026-08-10")
|
||||
get_match(DATE, "2026-08-10") do
|
||||
match["date"].should eq("2026-08-10")
|
||||
match["year"].should eq("2026")
|
||||
match["month"].should eq("08")
|
||||
match["day"].should eq("10")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "EPOCH" do
|
||||
it "should match a valid epoch time, based on the pattern, and parse it's fields (@1788748305)" do
|
||||
does_match(EPOCH, "@1788748305")
|
||||
get_match(EPOCH, "@1788748305") do
|
||||
match["epoch"].should eq("1788748305")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "RELATIVE" do
|
||||
it "should match a valid operation" do
|
||||
{% for sign in %w[ - + ].map(&.id) %}
|
||||
{% for token in %w[ day days week weeks hour hours month months second seconds year years ].map(&.id) %}
|
||||
does_match(RELATIVE, "{{sign}}1 {{token}}")
|
||||
{% end %}
|
||||
{% end %}
|
||||
end
|
||||
it "should not match invalid units (taco, tuesday, misfit, really?)" do
|
||||
tokens = %w[ taco tuesday misfit really? ]
|
||||
tokens.each do |token|
|
||||
doesnt_match(RELATIVE, "+1 #{token}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "RELATIVE_OPERATION" do
|
||||
it "should match a valid operation and we can parse the fields" do
|
||||
{% for sign in %w[ - + ].map(&.id) %}
|
||||
{% for int in %w[ 1 3 10 ].map(&.id) %}
|
||||
{% for token in %w[ day days week weeks hour hours month months second seconds year years ].map(&.id) %}
|
||||
get_match(RELATIVE_OPERATION, "{{sign}}{{int}} {{token}}") do
|
||||
match["sign"].should eq("{{sign}}")
|
||||
match["quantity"].should eq("{{int}}")
|
||||
match["unit"].should eq("{{token}}")
|
||||
end
|
||||
{% end %}
|
||||
{% end %}
|
||||
{% end %}
|
||||
end
|
||||
end
|
||||
|
||||
describe "INPUT_DATE_FULL" do
|
||||
it "will match a bounded full date with an offset (2024-08-13 22:23:24 -0700)" do
|
||||
get_match(INPUT_DATE_FULL, "2024-08-13 22:23:24 -0700") do
|
||||
match["date"].should eq("2024-08-13")
|
||||
match["year"].should eq("2024")
|
||||
match["month"].should eq("08")
|
||||
match["day"].should eq("13")
|
||||
match["time"].should eq("22:23:24")
|
||||
match["hour"].should eq("22")
|
||||
match["minute"].should eq("23")
|
||||
match["second"].should eq("24")
|
||||
match["timezone"].should eq("-0700")
|
||||
match["offset_sign"].should eq("-")
|
||||
match["offset_hour"].should eq("07")
|
||||
match["offset_minute"].should eq("00")
|
||||
end
|
||||
end
|
||||
|
||||
it "will match a bounded full date without an offset (2024-08-13 22:23:24)" do
|
||||
get_match(INPUT_DATE_FULL, "2024-08-13 22:23:24") do
|
||||
match["date"].should eq("2024-08-13")
|
||||
match["year"].should eq("2024")
|
||||
match["month"].should eq("08")
|
||||
match["day"].should eq("13")
|
||||
match["time"].should eq("22:23:24")
|
||||
match["hour"].should eq("22")
|
||||
match["minute"].should eq("23")
|
||||
match["second"].should eq("24")
|
||||
match["timezone"]?.should eq(nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "INPUT_DATE_SIMPLE" do
|
||||
it "will match a bounded simple date without an offset (2024-08-13)" do
|
||||
get_match(INPUT_DATE_SIMPLE, "2024-08-13") do
|
||||
match["date"].should eq("2024-08-13")
|
||||
match["year"].should eq("2024")
|
||||
match["month"].should eq("08")
|
||||
match["day"].should eq("13")
|
||||
match["timezone"]?.should eq(nil)
|
||||
end
|
||||
end
|
||||
|
||||
it "will match a bounded simple date with an offset (2024-08-13 -0700)" do
|
||||
get_match(INPUT_DATE_SIMPLE, "2024-08-13 -0700") do
|
||||
match["date"].should eq("2024-08-13")
|
||||
match["year"].should eq("2024")
|
||||
match["month"].should eq("08")
|
||||
match["day"].should eq("13")
|
||||
match["timezone"].should eq("-0700")
|
||||
match["offset_sign"].should eq("-")
|
||||
match["offset_hour"].should eq("07")
|
||||
match["offset_minute"].should eq("00")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "INPUT_DATE_EPOCH" do
|
||||
it "will match a bounded epoch time without an offset (@1788748305)" do
|
||||
get_match(INPUT_DATE_EPOCH, "@1788748305") do
|
||||
match["epoch"].should eq("1788748305")
|
||||
match["timezone"]?.should eq(nil)
|
||||
end
|
||||
end
|
||||
|
||||
it "will match a bounded simple date with an offset (@1788748305 -0700)" do
|
||||
get_match(INPUT_DATE_EPOCH, "@1788748305 -0700") do
|
||||
match["epoch"].should eq("1788748305")
|
||||
match["timezone"].should eq("-0700")
|
||||
match["offset_sign"].should eq("-")
|
||||
match["offset_hour"].should eq("07")
|
||||
match["offset_minute"].should eq("00")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
describe "INPUT_RELATIVE_OPERATIONS" do
|
||||
it "matches a single relative operation without timzone (+1 day)" do
|
||||
get_match(INPUT_RELATIVE_OPERATIONS, "+1 day") do
|
||||
match["operations"].should eq("+1 day")
|
||||
match["timezone"]?.should be_nil
|
||||
end
|
||||
end
|
||||
|
||||
it "matches a single relative operation with timzone (+1 day -0700)" do
|
||||
get_match(INPUT_RELATIVE_OPERATIONS, "+1 day -0700") do
|
||||
match["operations"].should eq("+1 day")
|
||||
match["timezone"].should eq("-0700")
|
||||
match["offset_sign"].should eq("-")
|
||||
match["offset_hour"].should eq("07")
|
||||
match["offset_minute"].should eq("00")
|
||||
end
|
||||
end
|
||||
|
||||
it "matches multiple relative operations without timzone (+1 day -2 years)" do
|
||||
get_match(INPUT_RELATIVE_OPERATIONS, "+1 day -2 years") do
|
||||
match["operations"].should eq("+1 day -2 years")
|
||||
match["timezone"]?.should be_nil
|
||||
end
|
||||
end
|
||||
|
||||
it "matches multiple relative operations without timzone (+1 day -2 years -0700)" do
|
||||
get_match(INPUT_RELATIVE_OPERATIONS, "+1 day -2 years -0700") do
|
||||
match["operations"].should eq("+1 day -2 years")
|
||||
match["timezone"].should eq("-0700")
|
||||
match["offset_sign"].should eq("-")
|
||||
match["offset_hour"].should eq("07")
|
||||
match["offset_minute"].should eq("00")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "FLOAT" do
|
||||
it "will match a traditional float (1.1)" do
|
||||
does_match(FLOAT, "1.1")
|
||||
end
|
||||
|
||||
it "will match an int (1)" do
|
||||
does_match(FLOAT, "1")
|
||||
end
|
||||
|
||||
it "will match a negative float (-1.1)" do
|
||||
does_match(FLOAT, "-1.1")
|
||||
end
|
||||
|
||||
it "will match a negative int (-1)" do
|
||||
does_match(FLOAT, "-1")
|
||||
end
|
||||
end
|
||||
|
||||
describe "INT" do
|
||||
end
|
||||
|
||||
describe "UINT" do
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,102 @@
|
||||
require "../spec_helper"
|
||||
|
||||
def get_ops(raw : String)
|
||||
CliGen::Timeparse::RelativeOperation.get_operations(raw)
|
||||
end
|
||||
|
||||
describe CliGen::Timeparse::RelativeOperation do
|
||||
describe ".get_operations" do
|
||||
it "will correctly parse a single relative operation & provide an array of 1" do
|
||||
op = "+1 seconds"
|
||||
ops = get_ops(op)
|
||||
ops.size.should eq(1)
|
||||
my_op = ops.first
|
||||
my_op.sign.should eq(1)
|
||||
my_op.quantity.should eq(1)
|
||||
my_op.unit.should eq(CliGen::Timeparse::OperationUnit::SECOND)
|
||||
end
|
||||
|
||||
it "will correctly parse multiple relative operations & provide an array of 2" do
|
||||
op = "+1 seconds -1 minute"
|
||||
ops = get_ops(op)
|
||||
ops.size.should eq(2)
|
||||
|
||||
op1 = ops.shift
|
||||
op1.sign.should eq(1)
|
||||
op1.quantity.should eq(1)
|
||||
op1.unit.should eq(CliGen::Timeparse::OperationUnit::SECOND)
|
||||
|
||||
op2 = ops.shift
|
||||
op2.sign.should eq(-1)
|
||||
op2.quantity.should eq(1)
|
||||
op2.unit.should eq(CliGen::Timeparse::OperationUnit::MINUTE)
|
||||
end
|
||||
|
||||
it "will parse every unit in both singular and plural form" do
|
||||
{
|
||||
"year" => CliGen::Timeparse::OperationUnit::YEAR,
|
||||
"month" => CliGen::Timeparse::OperationUnit::MONTH,
|
||||
"week" => CliGen::Timeparse::OperationUnit::WEEK,
|
||||
"day" => CliGen::Timeparse::OperationUnit::DAY,
|
||||
"hour" => CliGen::Timeparse::OperationUnit::HOUR,
|
||||
"minute" => CliGen::Timeparse::OperationUnit::MINUTE,
|
||||
"second" => CliGen::Timeparse::OperationUnit::SECOND,
|
||||
}.each do |word, unit|
|
||||
[word, "#{word}s"].each do |form|
|
||||
ops = get_ops("+1 #{form}")
|
||||
ops.size.should eq(1)
|
||||
ops.first.unit.should eq(unit)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "will correctly parse a negative sign and a multi-digit quantity" do
|
||||
op = get_ops("-42 days").first
|
||||
op.sign.should eq(-1)
|
||||
op.quantity.should eq(42)
|
||||
op.unit.should eq(CliGen::Timeparse::OperationUnit::DAY)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#apply" do
|
||||
it "will apply a single operation correctly" do
|
||||
time = Time.local
|
||||
atime = time + 2.minute
|
||||
get_ops("+2 minutes").each{|op| time = op.apply(time)}
|
||||
time.should eq(atime)
|
||||
end
|
||||
|
||||
it "will apply multiple operations correctly" do
|
||||
time = Time.local
|
||||
atime = time + 2.minute - 3.minute + 2.year
|
||||
get_ops("+2 minutes -3 minutes +2 years").each{|op| time = op.apply(time)}
|
||||
time.should eq(atime)
|
||||
end
|
||||
|
||||
it "will apply each Time::Span unit correctly" do
|
||||
time = Time.utc(2026, 4, 24, 10, 20, 30)
|
||||
get_ops("+3 weeks").first.apply(time).should eq(time + 3.weeks)
|
||||
get_ops("+3 days").first.apply(time).should eq(time + 3.days)
|
||||
get_ops("+3 hours").first.apply(time).should eq(time + 3.hours)
|
||||
get_ops("+3 minutes").first.apply(time).should eq(time + 3.minutes)
|
||||
get_ops("+3 seconds").first.apply(time).should eq(time + 3.seconds)
|
||||
end
|
||||
|
||||
it "will apply MONTH and YEAR as calendar spans, not fixed durations" do
|
||||
# Jan 31 + 1 month clamps to Feb 28 - a fixed 30.days span would give Mar 02
|
||||
jan31 = Time.utc(2026, 1, 31, 12, 0, 0)
|
||||
get_ops("+1 month").first.apply(jan31).should eq(Time.utc(2026, 2, 28, 12, 0, 0))
|
||||
get_ops("+1 year").first.apply(jan31).should eq(Time.utc(2027, 1, 31, 12, 0, 0))
|
||||
|
||||
# leap year: Feb 29 2028 exists, so +2 years from 2026-02-28 stays on the 28th
|
||||
feb28 = Time.utc(2026, 2, 28, 12, 0, 0)
|
||||
get_ops("+2 years").first.apply(feb28).should eq(Time.utc(2028, 2, 28, 12, 0, 0))
|
||||
end
|
||||
|
||||
it "will apply a negative operation correctly" do
|
||||
time = Time.utc(2026, 3, 15, 8, 0, 0)
|
||||
get_ops("-1 month").first.apply(time).should eq(Time.utc(2026, 2, 15, 8, 0, 0))
|
||||
get_ops("-10 days").first.apply(time).should eq(time - 10.days)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,139 @@
|
||||
require "../spec_helper"
|
||||
|
||||
module CliGen::Timeparse
|
||||
def self.test_get_location(raw : String)
|
||||
get_location(raw)
|
||||
end
|
||||
end
|
||||
|
||||
describe CliGen::Timeparse do
|
||||
describe ".parse" do
|
||||
it "accepts time in %Y-%m-%d format" do
|
||||
time = "2026-04-24"
|
||||
a = ::Time.parse_local(time, "%Y-%m-%d")
|
||||
CliGen::Timeparse.parse(time).should eq(a)
|
||||
end
|
||||
|
||||
it "accepts time in %Y-%m-%d %z format and will respect the provided offset" do
|
||||
time = "2026-04-24 -0900"
|
||||
a = ::Time.parse!(time, "%Y-%m-%d %z")
|
||||
CliGen::Timeparse.parse(time).should eq(a)
|
||||
end
|
||||
|
||||
it "strips surrounding whitespace before parsing" do
|
||||
a = ::Time.parse_local("2026-04-24", "%Y-%m-%d")
|
||||
CliGen::Timeparse.parse(" 2026-04-24 ").should eq(a)
|
||||
end
|
||||
|
||||
it "accepts time in %Y-%m-%d %H:%M:%S format and defaults to the local timezone" do
|
||||
time = "2026-04-24 10:20:30"
|
||||
a = ::Time.parse(time, "%Y-%m-%d %H:%M:%S", location: ::Time::Location.local)
|
||||
CliGen::Timeparse.parse(time).should eq(a)
|
||||
end
|
||||
|
||||
it "accepts time in %Y-%m-%d %H:%M:%S %z format and will respect the provided offset" do
|
||||
time = "2026-04-24 10:20:30 -0900"
|
||||
a = ::Time.parse!(time, "%Y-%m-%d %H:%M:%S %z")
|
||||
CliGen::Timeparse.parse(time).should eq(a)
|
||||
end
|
||||
|
||||
it "accepts epoch time in the @%s format and will default to UTC" do
|
||||
time = "@1788742653"
|
||||
a = ::Time.unix(time.lchop.to_i)
|
||||
CliGen::Timeparse.parse(time).should eq(a)
|
||||
end
|
||||
|
||||
it "accepts epoch time in the @%s %z format and will set the timezone to match the offset provided" do
|
||||
time = "@1788742653 -0700"
|
||||
a = ::Time.unix(time.lchop.split(" ").first.to_i).in(Time::Location.fixed(-1 * (7 * 3600)))
|
||||
CliGen::Timeparse.parse(time).should eq(a)
|
||||
end
|
||||
|
||||
it "accepts a single relative operation and will return the time" do
|
||||
time = CliGen::Timeparse.parse("+10 minutes")
|
||||
b_time = Time.local + 9.minute
|
||||
a_time = Time.local + 11.minute
|
||||
|
||||
(b_time..a_time).includes?(time).should be_true
|
||||
end
|
||||
|
||||
it "accepts a single relative operation + offset and will return the time" do
|
||||
time = CliGen::Timeparse.parse("+10 minutes -0900")
|
||||
b_time = Time.local + 9.minute
|
||||
a_time = Time.local + 11.minute
|
||||
(b_time..a_time).includes?(time).should be_true
|
||||
time.location.should eq(Time::Location.fixed("-0900", -1 * (9 * 3600)))
|
||||
end
|
||||
|
||||
it "accepts multiple relative operations and will return the time" do
|
||||
time = CliGen::Timeparse.parse("+20 minutes +2 minute")
|
||||
b_time = Time.local + 21.minute
|
||||
a_time = Time.local + 23.minute
|
||||
(b_time..a_time).includes?(time).should be_true
|
||||
end
|
||||
|
||||
it "accepts a multiple relative operation + offset and will return the time" do
|
||||
time = CliGen::Timeparse.parse("+10 minutes +2 minutes -0900")
|
||||
b_time = Time.local + 11.minute
|
||||
a_time = Time.local + 13.minute
|
||||
(b_time..a_time).includes?(time).should be_true
|
||||
time.location.should eq(Time::Location.fixed("-0900", -1 * (9 * 3600)))
|
||||
end
|
||||
end
|
||||
|
||||
describe ".parse error handling" do
|
||||
it "raises CliGen::TimeParseError when the input matches no known format" do
|
||||
expect_raises(CliGen::TimeParseError) do
|
||||
CliGen::Timeparse.parse("not-a-date")
|
||||
end
|
||||
end
|
||||
|
||||
it "includes the offending input in the unknown-format message" do
|
||||
ex = expect_raises(CliGen::TimeParseError) do
|
||||
CliGen::Timeparse.parse("nonsense")
|
||||
end
|
||||
ex.message.to_s.should contain("nonsense")
|
||||
end
|
||||
|
||||
it "raises CliGen::TimeParseError for a shape-valid but out-of-range month/day" do
|
||||
expect_raises(CliGen::TimeParseError) do
|
||||
CliGen::Timeparse.parse("2026-13-45")
|
||||
end
|
||||
end
|
||||
|
||||
it "raises CliGen::TimeParseError for an out-of-range hour" do
|
||||
expect_raises(CliGen::TimeParseError) do
|
||||
CliGen::Timeparse.parse("2026-01-15 25:00:00")
|
||||
end
|
||||
end
|
||||
|
||||
it "raises CliGen::TimeParseError for a day that does not exist in that month" do
|
||||
expect_raises(CliGen::TimeParseError) do
|
||||
CliGen::Timeparse.parse("2026-02-30")
|
||||
end
|
||||
end
|
||||
|
||||
# Guards the catch-all rescue: this path raises OverflowError, not ArgumentError.
|
||||
# Narrowing the rescue back to specific stdlib types would let it escape
|
||||
# App#handle_command_raises and reach the user as a stack trace.
|
||||
it "raises CliGen::TimeParseError for an epoch large enough to overflow" do
|
||||
expect_raises(CliGen::TimeParseError) do
|
||||
CliGen::Timeparse.parse("@99999999999999999999")
|
||||
end
|
||||
end
|
||||
|
||||
it "raises CliGen::TimeParseError rather than an offset error for an out-of-range timezone" do
|
||||
expect_raises(CliGen::TimeParseError) do
|
||||
CliGen::Timeparse.parse("2026-01-15 -9999")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe ".get_location" do
|
||||
it "correctly generates an offest" do
|
||||
a = "-0900"
|
||||
b = Time::Location.fixed(a, (-1) * ((9 * 3600) + (0 * 60)))
|
||||
CliGen::Timeparse.test_get_location(a).should eq(b)
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user