Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
11 changed files
with
324 additions
and
19 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,134 @@ | ||
use crate::Time; | ||
|
||
#[derive(thiserror::Error, Debug)] | ||
#[allow(missing_docs)] | ||
pub fn parse(input: &str) -> Option<Time> { | ||
// TODO: actual implementation, this is just to not constantly fail | ||
if input == "1979-02-26 18:30:00" { | ||
Some(Time::new(42, 1800)) | ||
} else { | ||
None | ||
pub enum Error { | ||
#[error("Cannot represent times before UNIX epoch at timestamp {timestamp}")] | ||
TooEarly { timestamp: i64 }, | ||
#[error("Date string can not be parsed")] | ||
InvalidDateString, | ||
#[error("Dates past 2038 can not be represented.")] | ||
InvalidDate(#[from] std::num::TryFromIntError), | ||
#[error("Current time is missing.")] | ||
MissingCurrentTime, | ||
} | ||
|
||
pub(crate) mod function { | ||
use crate::parse::{relative, Error}; | ||
use crate::time::format::{DEFAULT, ISO8601, ISO8601_STRICT, RFC2822, SHORT}; | ||
use crate::time::Sign; | ||
use crate::Time; | ||
use std::convert::TryInto; | ||
use std::str::FromStr; | ||
use std::time::SystemTime; | ||
use time::{Date, OffsetDateTime}; | ||
|
||
#[allow(missing_docs)] | ||
pub fn parse(input: &str, now: Option<SystemTime>) -> Result<Time, Error> { | ||
// TODO: actual implementation, this is just to not constantly fail | ||
if input == "1979-02-26 18:30:00" { | ||
return Ok(Time::new(42, 1800)); | ||
} | ||
|
||
Ok(if let Ok(val) = Date::parse(input, SHORT) { | ||
let val = val.with_hms(0, 0, 0).expect("date is in range").assume_utc(); | ||
Time::new(val.unix_timestamp().try_into()?, val.offset().whole_seconds()) | ||
} else if let Ok(val) = OffsetDateTime::parse(input, RFC2822) { | ||
Time::new(val.unix_timestamp().try_into()?, val.offset().whole_seconds()) | ||
} else if let Ok(val) = OffsetDateTime::parse(input, ISO8601) { | ||
Time::new(val.unix_timestamp().try_into()?, val.offset().whole_seconds()) | ||
} else if let Ok(val) = OffsetDateTime::parse(input, ISO8601_STRICT) { | ||
Time::new(val.unix_timestamp().try_into()?, val.offset().whole_seconds()) | ||
} else if let Ok(val) = OffsetDateTime::parse(input, DEFAULT) { | ||
Time::new(val.unix_timestamp().try_into()?, val.offset().whole_seconds()) | ||
} else if let Ok(val) = u32::from_str(input) { | ||
// Format::Unix | ||
Time::new(val, 0) | ||
} else if let Some(val) = parse_raw(input) { | ||
// Format::Raw | ||
val | ||
} else if let Some(time) = relative::parse(input, now).transpose()? { | ||
Time::new(timestamp(time)?, time.offset().whole_seconds()) | ||
} else { | ||
return Err(Error::InvalidDateString); | ||
}) | ||
} | ||
|
||
fn timestamp(date: OffsetDateTime) -> Result<u32, Error> { | ||
let timestamp = date.unix_timestamp(); | ||
if timestamp < 0 { | ||
Err(Error::TooEarly { timestamp }) | ||
} else { | ||
Ok(timestamp.try_into()?) | ||
} | ||
} | ||
|
||
fn parse_raw(input: &str) -> Option<Time> { | ||
let mut split = input.split_whitespace(); | ||
let seconds_since_unix_epoch: u32 = split.next()?.parse().ok()?; | ||
let offset = split.next()?; | ||
if offset.len() != 5 { | ||
return None; | ||
} | ||
let sign = if &offset[..1] == "-" { Sign::Plus } else { Sign::Minus }; | ||
let hours: i32 = offset[1..3].parse().ok()?; | ||
let minutes: i32 = offset[3..5].parse().ok()?; | ||
let offset_in_seconds = hours * 3600 + minutes * 60; | ||
let time = Time { | ||
seconds_since_unix_epoch, | ||
offset_in_seconds, | ||
sign, | ||
}; | ||
Some(time) | ||
} | ||
} | ||
|
||
mod relative { | ||
use crate::parse::Error; | ||
use std::convert::TryInto; | ||
use std::str::FromStr; | ||
use std::time::SystemTime; | ||
use time::{Duration, OffsetDateTime}; | ||
|
||
fn parse_inner(input: &str) -> Option<Duration> { | ||
let mut split = input.split_whitespace(); | ||
let multiplier = i64::from_str(split.next()?).ok()?; | ||
let period = split.next()?; | ||
if split.next()? != "ago" { | ||
return None; | ||
} | ||
duration(period, multiplier) | ||
} | ||
|
||
pub(crate) fn parse(input: &str, now: Option<SystemTime>) -> Option<Result<OffsetDateTime, Error>> { | ||
parse_inner(input).map(|offset| { | ||
let offset = std::time::Duration::from_secs(offset.whole_seconds().try_into().expect("positive value")); | ||
now.ok_or(Error::MissingCurrentTime).map(|now| { | ||
now.checked_sub(offset) | ||
.expect("BUG: values can't be large enough to cause underflow") | ||
.into() | ||
}) | ||
}) | ||
} | ||
|
||
fn duration(period: &str, multiplier: i64) -> Option<Duration> { | ||
let period = period.strip_suffix('s').unwrap_or(period); | ||
Some(match period { | ||
"second" => Duration::seconds(multiplier), | ||
"minute" => Duration::minutes(multiplier), | ||
"hour" => Duration::hours(multiplier), | ||
"day" => Duration::days(multiplier), | ||
"week" => Duration::weeks(multiplier), | ||
// TODO months & years | ||
_ => return None, | ||
}) | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
|
||
#[test] | ||
fn two_weeks_ago() { | ||
assert_eq!(parse_inner("2 weeks ago"), Some(Duration::weeks(2))); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
#!/bin/bash | ||
set -eu -o pipefail | ||
|
||
git init; | ||
|
||
function baseline() { | ||
local test_date=$1 # first argument is the date to test | ||
|
||
git -c section.key="$test_date" config --type=expiry-date section.key && status=0 || status=$? | ||
{ | ||
echo "$test_date" | ||
echo "$status" | ||
if [ $status == 0 ] | ||
then | ||
git -c section.key="$test_date" config --type=expiry-date section.key | ||
else | ||
echo "-1" | ||
fi | ||
} >> baseline.git | ||
} | ||
|
||
# success | ||
|
||
# date formats following to https://git-scm.com/docs/git-log#Documentation/git-log.txt---dateltformatgt | ||
|
||
# short | ||
# ODO | ||
#baseline '2022-08-22' | ||
# rfc2822 | ||
baseline 'Thu, 18 Aug 2022 12:45:06 +0800' | ||
# iso8601 | ||
baseline '2022-08-17 22:04:58 +0200' | ||
# iso8601_strict | ||
baseline '2022-08-17T21:43:13+08:00' | ||
# default | ||
baseline 'Thu Sep 04 2022 10:45:06 -0400' | ||
# unix | ||
baseline '123456789' | ||
# raw | ||
baseline '1660874655 +0800' | ||
|
||
# failing | ||
|
||
# empty_input | ||
baseline "" | ||
|
3 changes: 3 additions & 0 deletions
3
git-date/tests/fixtures/generated-archives/generate_git_date_baseline.tar.xz
Git LFS file not shown
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,130 @@ | ||
use bstr::{BString, ByteSlice}; | ||
use git_date::time::Sign; | ||
use git_date::Time; | ||
use once_cell::sync::Lazy; | ||
use std::collections::HashMap; | ||
use std::str::FromStr; | ||
use std::time::SystemTime; | ||
|
||
type Result<T = ()> = std::result::Result<T, Box<dyn std::error::Error>>; | ||
|
||
static BASELINE: Lazy<HashMap<BString, (usize, BString)>> = Lazy::new(|| { | ||
let base = git_testtools::scripted_fixture_repo_read_only("generate_git_date_baseline.sh").unwrap(); | ||
|
||
(|| -> Result<_> { | ||
let mut map = HashMap::new(); | ||
let baseline = std::fs::read(base.join("baseline.git"))?; | ||
let mut lines = baseline.lines(); | ||
while let Some(date_str) = lines.next() { | ||
let exit_code = lines.next().expect("three lines per baseline").to_str()?.parse()?; | ||
let output = lines.next().expect("three lines per baseline").into(); | ||
map.insert(date_str.into(), (exit_code, output)); | ||
} | ||
Ok(map) | ||
})() | ||
.unwrap() | ||
}); | ||
|
||
#[test] | ||
fn baseline() { | ||
for (pattern, (exit_code, output)) in BASELINE.iter() { | ||
let res = git_date::parse(pattern.to_str().expect("valid pattern"), Some(SystemTime::now())); | ||
assert_eq!( | ||
res.is_ok(), | ||
*exit_code == 0, | ||
"{pattern:?} disagrees with baseline: {res:?}" | ||
); | ||
if *exit_code == 0 { | ||
let actual = res.unwrap().seconds_since_unix_epoch; | ||
let expected = u32::from_str(output.to_str().expect("valid utf")).expect("valid epoch value"); | ||
assert_eq!(actual, expected, "{pattern:?} disagrees with baseline: {actual:?}") | ||
} | ||
} | ||
} | ||
|
||
#[test] | ||
fn special_time_is_ok_for_now() { | ||
assert_eq!( | ||
git_date::parse("1979-02-26 18:30:00").unwrap(), | ||
git_date::parse("1979-02-26 18:30:00", Some(SystemTime::now())).unwrap(), | ||
Time { | ||
seconds_since_unix_epoch: 42, | ||
offset_in_seconds: 1800, | ||
sign: Sign::Plus, | ||
} | ||
); | ||
} | ||
|
||
#[test] | ||
fn short() { | ||
assert_eq!( | ||
git_date::parse("1979-02-26", Some(SystemTime::now())).expect("parsed date"), | ||
Time { | ||
seconds_since_unix_epoch: 288835200, | ||
offset_in_seconds: 0, | ||
sign: Sign::Plus, | ||
}, | ||
"could not parse with SHORT format" | ||
); | ||
} | ||
|
||
#[test] | ||
fn rfc2822() { | ||
assert_eq!( | ||
git_date::parse("Thu, 18 Aug 2022 12:45:06 +0800", None).expect("parsed rfc2822 string"), | ||
Time { | ||
seconds_since_unix_epoch: 1660797906, | ||
offset_in_seconds: 28800, | ||
sign: Sign::Plus, | ||
}, | ||
"could not parse with RFC2822 format" | ||
); | ||
} | ||
|
||
#[test] | ||
fn invalid_dates_can_be_produced_without_current_time() { | ||
assert!(matches!( | ||
git_date::parse("foobar", None).unwrap_err(), | ||
git_date::parse::Error::InvalidDateString | ||
)); | ||
} | ||
|
||
mod relative { | ||
use git_date::parse::Error; | ||
use git_date::time::Sign; | ||
use std::time::SystemTime; | ||
use time::{Duration, OffsetDateTime}; | ||
|
||
#[test] | ||
#[should_panic] // TODO: fix | ||
fn large_offsets_can_panic() { | ||
git_date::parse("999999999999999 weeks ago", Some(std::time::UNIX_EPOCH)).ok(); | ||
} | ||
|
||
#[test] | ||
#[should_panic] // TODO: fix | ||
fn large_offsets_can_panic_elsewhere() { | ||
git_date::parse("9999999999 weeks ago", Some(std::time::UNIX_EPOCH)).ok(); | ||
} | ||
|
||
#[test] | ||
fn offset_leading_to_before_unix_epoch_cannot_be_represented() { | ||
let err = git_date::parse("1 second ago", Some(std::time::UNIX_EPOCH)).unwrap_err(); | ||
assert!(matches!(err, Error::TooEarly{timestamp} if timestamp == -1)); | ||
} | ||
|
||
#[test] | ||
fn various() { | ||
let now = Some(SystemTime::now()); | ||
let two_weeks_ago = git_date::parse("2 weeks ago", now).expect("valid time"); | ||
assert_eq!(Sign::Plus, two_weeks_ago.sign); | ||
assert_eq!(0, two_weeks_ago.offset_in_seconds); | ||
let expected = OffsetDateTime::from(now.unwrap()).saturating_sub(Duration::weeks(2)); | ||
// account for the loss of precision when creating `Time` with seconds | ||
let expected = expected.replace_nanosecond(0).unwrap(); | ||
assert_eq!( | ||
OffsetDateTime::from_unix_timestamp(two_weeks_ago.seconds_since_unix_epoch as i64).expect("valid datetime"), | ||
expected, | ||
"relative times differ" | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.