From 4b8fdd0fbecb5cad61e5c2a943d55117fd8bd9b9 Mon Sep 17 00:00:00 2001 From: Paul Beckingham Date: Sat, 27 Jun 2015 13:48:42 -0400 Subject: [PATCH 01/20] ISO8601: Removed non-extended forms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Removed support for non-extended forms, which is approximately half of the formats. These include: YYYYMMDD YYYYWww YYYYWwwD hhmmѕsZ hhmmZ hhZ and combinations thereof. Essentially all forms that contains run-on sequences of integers, without separators. These removed forms will still be supported via rc.dateformat. - Removed unsupported forms from iso8601d.t.cpp. - Removed unsupported forms from datetime-negative.t, and corrected the tests that now succeed. --- src/ISO8601.cpp | 245 ++------------------------------------- src/ISO8601.h | 6 - test/datetime-negative.t | 48 -------- test/iso8601d.t.cpp | 105 +---------------- 4 files changed, 8 insertions(+), 396 deletions(-) diff --git a/src/ISO8601.cpp b/src/ISO8601.cpp index 277a84aeb..e985fd19a 100644 --- a/src/ISO8601.cpp +++ b/src/ISO8601.cpp @@ -61,16 +61,9 @@ void ISO8601d::ambiguity (bool value) // | date-ext 'T' time-ext offset-ext # Specified TZ // | date-ext 'T' time-ext # Local // | date-ext # Local -// | date 'T' time 'Z' -// | date 'T' time offset-ext -// | date 'T' time -// | date // | time-ext 'Z' // | time-ext offset-ext Not needed // | time-ext -// | time 'Z' -// | time offset -// | time // ; // // date-ext ::= ±YYYYY-MM-DD Νot needed @@ -83,37 +76,14 @@ void ISO8601d::ambiguity (bool value) // | YYYY-Www // ; // -// date ::= ±YYYYYMMDD Νot needed -// | ±YYYYYWwwD Νot needed -// | ±YYYYYWww Νot needed -// | ±YYYYYDDD Νot needed -// | ±YYYYYMM Νot needed -// | ±YYYYY Νot needed -// | ±YYY Νot needed -// | YYYYMMDD Ambiguous (number) -// | YYYYWwwD -// | YYYYWww -// | YYYYDDD Ambiguous (number) -// | YYYY-MM -// | YYYY Ambiguous (number) -// | YY Ambiguous (number) -// ; -// // time-ext ::= hh:mm:ss[,ss] // | hh:mm[,mm] // | hh[,hh] Ambiguous (number) // ; // -// time ::= hhmmss[,ss] Ambiguous (number) -// | hhmm[,mm] Ambiguous (number) -// | hh[,hh] Ambiguous (number) -// ; -// // time-utc-ext ::= hh:mm[:ss] 'Z' ; -// time-utc ::= hh[mm[ss]] 'Z' ; // // offset-ext ::= ±hh[:mm] ; -// offset ::= ±hh[mm] ; // // Not yet supported: // @@ -136,12 +106,7 @@ bool ISO8601d::parse (const std::string& input, std::string::size_type& start) parse_date_ext (n) || parse_time_utc_ext (n) || parse_time_off_ext (n) || - parse_date_time (n) || - parse_date (n, _ambiguity) || - parse_time_utc (n) || - parse_time_off (n) || - parse_time_ext (n) || // Time last, as it is the most permissive. - parse_time (n, _ambiguity)) + parse_time_ext (n)) // Time last, as it is the most permissive. { // Check the values and determine time_t. if (validate ()) @@ -214,47 +179,6 @@ bool ISO8601d::parse_date_time_ext (Nibbler& n) return false; } -//////////////////////////////////////////////////////////////////////////////// -// date 'T' time 'Z' -// date 'T' time offset -// date 'T' time -bool ISO8601d::parse_date_time (Nibbler& n) -{ - Nibbler backup (n); - if (parse_date (n, true)) - { - if (n.skip ('T') && - parse_time (n, true)) - { - if (n.skip ('Z')) - { - _utc = true; - if (!Lexer::isDigit (n.next ())) - return true; - } - else if (parse_off (n)) - { - if (!Lexer::isDigit (n.next ())) - return true; - } - - if (!Lexer::isDigit (n.next ())) - return true; - } - - // Restore date - _year = 0; - _month = 0; - _week = 0; - _weekday = 0; - _julian = 0; - _day = 0; - } - - n = backup; - return false; -} - //////////////////////////////////////////////////////////////////////////////// // YYYY-MM-DD // YYYY-DDD @@ -303,64 +227,6 @@ bool ISO8601d::parse_date_ext (Nibbler& n) return false; } -//////////////////////////////////////////////////////////////////////////////// -// YYYYMMDD Ambiguous (number) -// YYYYWwwD -// YYYYWww -// YYYYDDD Ambiguous (number) -// YYYY-MM -bool ISO8601d::parse_date (Nibbler& n, bool ambiguous) -{ - Nibbler backup (n); - int year; - if (n.getDigit4 (year)) - { - int month; - if (n.skip ('W')) - { - int week; - if (n.getDigit2 (week)) - { - _week = week; - - int day; - if (n.getDigit (day)) - _weekday = day; - - _year = year; - if (!Lexer::isDigit (n.next ())) - return true; - } - } - else if (n.skip ('-')) - { - if (n.getDigit2 (_month)) - { - _year = year; - if (!Lexer::isDigit (n.next ())) - return true; - } - } - else if (n.getDigit4 (month)) - { - _year = year; - _month = month / 100; - _day = month % 100; - if (!Lexer::isDigit (n.next ())) - return true; - } - else if (ambiguous && n.getDigit3 (_julian)) - { - _year = year; - if (!Lexer::isDigit (n.next ())) - return true; - } - } - - n = backup; - return false; -} - //////////////////////////////////////////////////////////////////////////////// // ±hh[:mm] bool ISO8601d::parse_off_ext (Nibbler& n) @@ -394,37 +260,7 @@ bool ISO8601d::parse_off_ext (Nibbler& n) } //////////////////////////////////////////////////////////////////////////////// -// ±hh[mm] -bool ISO8601d::parse_off (Nibbler& n) -{ - Nibbler backup (n); - std::string sign; - if (n.getN (1, sign)) - { - if (sign == "+" || sign == "-") - { - int offset; - int hh; - if (n.getDigit2 (hh)) - { - offset = hh * 3600; - int mm; - if (n.getDigit2 (mm)) - offset += mm * 60; - - _offset = (sign == "-") ? -offset : offset; - if (!Lexer::isDigit (n.next ())) - return true; - } - } - } - - n = backup; - return false; -} - -//////////////////////////////////////////////////////////////////////////////// -// hh[:mm[:ss]] +// hh:mm[:ss] bool ISO8601d::parse_time_ext (Nibbler& n) { Nibbler backup (n); @@ -433,20 +269,15 @@ bool ISO8601d::parse_time_ext (Nibbler& n) int mm; int ss; if (n.getDigit2 (hh) && - !n.getDigit (mm)) + n.skip (':') && + n.getDigit2 (mm)) { - seconds = hh * 3600; + seconds = (hh * 3600) + (mm * 60); if (n.skip (':') && - n.getDigit2 (mm) && - !n.getDigit (ss)) + n.getDigit2 (ss)) { - seconds += mm * 60; - - if (n.skip (':') && - n.getDigit2 (ss)) - seconds += ss; - + seconds += ss; _seconds = seconds; return true; } @@ -463,35 +294,6 @@ bool ISO8601d::parse_time_ext (Nibbler& n) return false; } -//////////////////////////////////////////////////////////////////////////////// -// hhmm[ss] -bool ISO8601d::parse_time (Nibbler& n, bool ambiguous) -{ - if (!ambiguous) - return false; - - Nibbler backup (n); - int seconds = 0; - int hh; - int mm; - if (n.getDigit2 (hh) && - n.getDigit2 (mm)) - { - seconds = hh * 3600 + mm * 60; - - int ss; - if (n.getDigit2 (ss)) - seconds += ss; - - _seconds = seconds; - if (!Lexer::isDigit (n.next ())) - return true; - } - - n = backup; - return false; -} - //////////////////////////////////////////////////////////////////////////////// // time-ext 'Z' bool ISO8601d::parse_time_utc_ext (Nibbler& n) @@ -509,23 +311,6 @@ bool ISO8601d::parse_time_utc_ext (Nibbler& n) return false; } -//////////////////////////////////////////////////////////////////////////////// -// time 'Z' -bool ISO8601d::parse_time_utc (Nibbler& n) -{ - n.save (); - if (parse_time (n, true) && - n.skip ('Z')) - { - _utc = true; - if (!Lexer::isDigit (n.next ())) - return true; - } - - n.restore (); - return false; -} - //////////////////////////////////////////////////////////////////////////////// // time-ext offset-ext bool ISO8601d::parse_time_off_ext (Nibbler& n) @@ -542,22 +327,6 @@ bool ISO8601d::parse_time_off_ext (Nibbler& n) return false; } -//////////////////////////////////////////////////////////////////////////////// -// time offset -bool ISO8601d::parse_time_off (Nibbler& n) -{ - Nibbler backup (n); - if (parse_time (n, true) && - parse_off (n)) - { - if (!Lexer::isDigit (n.next ())) - return true; - } - - n = backup; - return false; -} - //////////////////////////////////////////////////////////////////////////////// // Using Zeller's Congruence. int ISO8601d::dayOfWeek (int year, int month, int day) diff --git a/src/ISO8601.h b/src/ISO8601.h index ca26732da..1d9d5f0dd 100644 --- a/src/ISO8601.h +++ b/src/ISO8601.h @@ -45,17 +45,11 @@ public: private: bool parse_date_time_ext (Nibbler&); - bool parse_date_time (Nibbler&); bool parse_date_ext (Nibbler&); - bool parse_date (Nibbler&, bool); bool parse_off_ext (Nibbler&); - bool parse_off (Nibbler&); bool parse_time_ext (Nibbler&); - bool parse_time (Nibbler&, bool); bool parse_time_utc_ext (Nibbler&); - bool parse_time_utc (Nibbler&); bool parse_time_off_ext (Nibbler&); - bool parse_time_off (Nibbler&); int dayOfWeek (int, int, int); bool validate (); void resolve (); diff --git a/test/datetime-negative.t b/test/datetime-negative.t index d85417e41..4ece6e812 100755 --- a/test/datetime-negative.t +++ b/test/datetime-negative.t @@ -103,38 +103,6 @@ class TestIncorrectDate(BaseDateTimeNegativeTest): def test_set_incorrect_datetime_day_two_hundred_in_YYYY_WwwD(self): self.assertInvalidDatetimeFormat('2014-W24200') - @unittest.expectedFailure - def test_set_incorrect_datetime_week_with_the_number_zero_in_YYYYWww(self): - self.assertInvalidDatetimeFormat('2014W00') - - def test_set_incorrect_datetime_overflow_in_week_in_YYYYWww(self): - self.assertInvalidDatetimeFormat('2014W54') - - @unittest.expectedFailure - def test_set_incorrect_datetime_week_zero_in_YYYYWwwD(self): - self.assertInvalidDatetimeFormat('2014W001') - - @unittest.expectedFailure - def test_set_incorrect_datetime_fifth_day_of_week_zero_in_YYYYWwwD(self): - self.assertInvalidDatetimeFormat('2014W005') - - def test_set_incorrect_datetime_overflow_week_in_YYYYWwwD(self): - self.assertInvalidDatetimeFormat('2014W541') - - def test_set_incorrect_datetime_huge_overflow_week_in_YYYYWwwD(self): - self.assertInvalidDatetimeFormat('2014W991') - - @unittest.expectedFailure - def test_set_incorrect_datetime_day_zero_in_YYYYWwwD(self): - self.assertInvalidDatetimeFormat('2014W240') - - def test_set_incorrect_datetime_day_eight_in_YYYYWwwD(self): - self.assertInvalidDatetimeFormat('2014W248') - - def test_set_incorrect_datetime_day_two_hundred_in_YYYYWwwD(self): - self.assertInvalidDatetimeFormat('2014W24200') - - @unittest.expectedFailure def test_set_incorrect_datetime_month_zero_in_YYYY_MM(self): self.assertInvalidDatetimeFormat('2014-00') @@ -211,18 +179,15 @@ class TestIncorrectTime(BaseDateTimeNegativeTest): def test_set_incorrect_datetime_negative_minutes_in_hh_mmZ(self): self.assertInvalidDatetimeFormat('12:-12Z') - @unittest.expectedFailure def test_set_incorrect_datetime_hour_overflow_in_hh_mm_plus_hh_mm(self): self.assertInvalidDatetimeFormat('24:00+01:00') def test_set_incorrect_datetime_huge_hour_overflow_in_hh_mm_plus_hh_mm(self): self.assertInvalidDatetimeFormat('99:00+01:00') - @unittest.expectedFailure def test_set_incorrect_datetime_minute_overflow_in_hh_mm_plus_hh_mm(self): self.assertInvalidDatetimeFormat('12:60+01:00') - @unittest.expectedFailure def test_set_incorrect_datetime_huge_minute_overflow_in_hh_mm_plus_hh_mm(self): self.assertInvalidDatetimeFormat('12:99+01:00') @@ -241,18 +206,15 @@ class TestIncorrectTime(BaseDateTimeNegativeTest): def test_set_incorrect_datetime_negative_minutes_in_hh_mm_plus_hh_mm(self): self.assertInvalidDatetimeFormat('12:-12+01:00') - @unittest.expectedFailure def test_set_incorrect_datetime_hour_overflow_in_hh_mm_minus_hh_mm(self): self.assertInvalidDatetimeFormat('24:00-01:00') def test_set_incorrect_datetime_huge_hour_overflow_in_hh_mm_minus_hh_mm(self): self.assertInvalidDatetimeFormat('99:00-01:00') - @unittest.expectedFailure def test_set_incorrect_datetime_minute_overflow_in_hh_mm_minus_hh_mm(self): self.assertInvalidDatetimeFormat('12:60-01:00') - @unittest.expectedFailure def test_set_incorrect_datetime_huge_minute_overflow_in_hh_mm_minus_hh_mm(self): self.assertInvalidDatetimeFormat('12:99-01:00') @@ -312,7 +274,6 @@ class TestIncorrectTime(BaseDateTimeNegativeTest): def test_set_incorrect_datetime_negative_minutes_in_hh_mm_ss(self): self.assertInvalidDatetimeFormat('12:-12:12') - @unittest.expectedFailure def test_set_incorrect_datetime_negative_seconds_in_hh_mm_ss(self): self.assertInvalidDatetimeFormat('12:12:-12') @@ -445,7 +406,6 @@ class TestIncorrectTime(BaseDateTimeNegativeTest): def test_set_incorrect_datetime_negative_minutes_in_hh_mm_ss_minus_hh_mm(self): self.assertInvalidDatetimeFormat('12:-12:12-01:00') - @unittest.expectedFailure def test_set_incorrect_datetime_negative_seconds_in_hh_mm_ss_minus_hh_mm(self): self.assertInvalidDatetimeFormat('12:12:-12-01:00') @@ -505,22 +465,18 @@ class TestIncorrectTime(BaseDateTimeNegativeTest): def test_set_incorrect_datetime_invalid_negative_offset_length_in_hh_mm_ss(self): self.assertInvalidDatetimeFormat('12:12:12-3:2') - @unittest.expectedFailure def test_set_incorrect_datetime_invalid_hour_positive_offset_in_hh_mm(self): self.assertInvalidDatetimeFormat('12:12+13:00') - @unittest.expectedFailure def test_set_incorrect_datetime_invalid_medium_hour_positive_offset_in_hh_mm(self): self.assertInvalidDatetimeFormat('12:12+24:00') def test_set_incorrect_datetime_invalid_huge_hour_positive_offset_in_hh_mm(self): self.assertInvalidDatetimeFormat('12:12+99:00') - @unittest.expectedFailure def test_set_incorrect_datetime_invalid_minute_positive_offset_in_hh_mm(self): self.assertInvalidDatetimeFormat('12:12+03:60') - @unittest.expectedFailure def test_set_incorrect_datetime_invalid_huge_minute_positive_offset_in_hh_mm(self): self.assertInvalidDatetimeFormat('12:12+03:99') @@ -533,22 +489,18 @@ class TestIncorrectTime(BaseDateTimeNegativeTest): def test_set_incorrect_datetime_invalid_positive_offset_length_in_hh_mm(self): self.assertInvalidDatetimeFormat('12:12+3:2') - @unittest.expectedFailure def test_set_incorrect_datetime_invalid_hour_negative_offset_in_hh_mm(self): self.assertInvalidDatetimeFormat('12:12-13:00') - @unittest.expectedFailure def test_set_incorrect_datetime_invalid_medium_hour_negative_offset_in_hh_mm(self): self.assertInvalidDatetimeFormat('12:12-24:00') def test_set_incorrect_datetime_invalid_huge_hour_negative_offset_in_hh_mm(self): self.assertInvalidDatetimeFormat('12:12-99:00') - @unittest.expectedFailure def test_set_incorrect_datetime_invalid_minute_negative_offset_in_hh_mm(self): self.assertInvalidDatetimeFormat('12:12-03:60') - @unittest.expectedFailure def test_set_incorrect_datetime_invalid_huge_minute_negative_offset_in_hh_mm(self): self.assertInvalidDatetimeFormat('12:12-03:99') diff --git a/test/iso8601d.t.cpp b/test/iso8601d.t.cpp index 6ef71ff7b..3437dd1ae 100644 --- a/test/iso8601d.t.cpp +++ b/test/iso8601d.t.cpp @@ -33,9 +33,6 @@ Context context; -#define AMBIGUOUS // Include ambiguous forms -#undef AMBIGUOUS // Exclude ambiguous forms - //////////////////////////////////////////////////////////////////////////////// void testParse ( UnitTest& t, @@ -74,11 +71,7 @@ void testParse ( //////////////////////////////////////////////////////////////////////////////// int main (int argc, char** argv) { - UnitTest t (1214 -#ifdef AMBIGUOUS - + 48 -#endif - ); + UnitTest t (734); ISO8601d iso; std::string::size_type start = 0; @@ -139,7 +132,6 @@ int main (int argc, char** argv) int hms = (12 * 3600) + (34 * 60) + 56; // The time 12:34:56 in seconds. int hm = (12 * 3600) + (34 * 60); // The time 12:34:00 in seconds. - int h = (12 * 3600); // The time 12:00:00 in seconds. int z = 3600; // TZ offset. int ld = local_s > hms ? 86400 : 0; // Local extra day if now > hms. @@ -155,31 +147,12 @@ int main (int argc, char** argv) // input i Year Mo Wk WD Jul Da Secs TZ UTC time_t testParse (t, "12:34:56Z", 9, 0, 0, 0, 0, 0, 0, hms, 0, true, utc+hms+ud ); testParse (t, "12:34Z", 6, 0, 0, 0, 0, 0, 0, hm, 0, true, utc+hm+ud ); - testParse (t, "12Z", 3, 0, 0, 0, 0, 0, 0, h, 0, true, utc+h+ud ); testParse (t, "12:34:56+01:00", 14, 0, 0, 0, 0, 0, 0, hms, 3600, false, utc+hms-z+ud ); testParse (t, "12:34:56+01", 11, 0, 0, 0, 0, 0, 0, hms, 3600, false, utc+hms-z+ud ); testParse (t, "12:34+01:00", 11, 0, 0, 0, 0, 0, 0, hm, 3600, false, utc+hm-z+ud ); testParse (t, "12:34+01", 8, 0, 0, 0, 0, 0, 0, hm, 3600, false, utc+hm-z+ud ); -// testParse (t, "12+01:00", 8, 0, 0, 0, 0, 0, 0, h, 3600, false, utc+h-z+ud ); -// testParse (t, "12+01", 5, 0, 0, 0, 0, 0, 0, h, 3600, false, utc+h-z+ud ); testParse (t, "12:34:56", 8, 0, 0, 0, 0, 0, 0, hms, 0, false, local+hms+ld ); testParse (t, "12:34", 5, 0, 0, 0, 0, 0, 0, hm, 0, false, local+hm+ld ); -#ifdef AMBIGUOUS - testParse (t, "12", 2, 0, 0, 0, 0, 0, 0, h, 0, false, local+h+ld ); -#endif - - // time - // input i Year Mo Wk WD Jul Da Secs TZ UTC time_t - testParse (t, "123456Z", 7, 0, 0, 0, 0, 0, 0, hms, 0, true, utc+hms+ud ); - testParse (t, "1234Z", 5, 0, 0, 0, 0, 0, 0, hm, 0, true, utc+hm+ud ); - testParse (t, "123456+0100", 11, 0, 0, 0, 0, 0, 0, hms, 3600, false, utc+hms-z+ud ); - testParse (t, "123456+01", 9, 0, 0, 0, 0, 0, 0, hms, 3600, false, utc+hms-z+ud ); - testParse (t, "1234+0100", 9, 0, 0, 0, 0, 0, 0, hm, 3600, false, utc+hm-z+ud ); - testParse (t, "1234+01", 7, 0, 0, 0, 0, 0, 0, hm, 3600, false, utc+hm-z+ud ); -// testParse (t, "12+0100", 7, 0, 0, 0, 0, 0, 0, h, 3600, false, utc+h-z+ud ); -#ifdef AMBIGUOUS - testParse (t, "123456", 6, 0, 0, 0, 0, 0, 0, hms, 0, false, local+hms+ld ); -#endif // datetime-ext // input i Year Mo Wk WD Jul Da Secs TZ UTC time_t @@ -239,82 +212,6 @@ int main (int argc, char** argv) testParse (t, "2013-W49T12:34-01:00", 20, 2013, 0, 49, 0, 0, 0, hm, -3600, false, utc1+hm+z ); testParse (t, "2013-W49T12:34-01", 17, 2013, 0, 49, 0, 0, 0, hm, -3600, false, utc1+hm+z ); - // datetime -#ifdef AMBIGUOUS - testParse (t, "20131206", 8, 2013, 12, 0, 0, 0, 6, 0, 0, false, local6 ); -#endif - testParse (t, "2013W495", 8, 2013, 0, 49, 5, 0, 0, 0, 0, false, local6 ); - testParse (t, "2013W49", 7, 2013, 0, 49, 0, 0, 0, 0, 0, false, local1 ); -#ifdef AMBIGUOUS - testParse (t, "2013340", 7, 2013, 0, 0, 0, 340, 0, 0, 0, false, local6 ); -#endif - testParse (t, "2013-12", 7, 2013, 12, 0, 0, 0, 0, 0, 0, false, local1 ); - - testParse (t, "20131206T123456", 15, 2013, 12, 0, 0, 0, 6, hms, 0, false, local6+hms); -// testParse (t, "20131206T12", 11, 2013, 12, 0, 0, 0, 6, h, 0, false, local6+h ); - testParse (t, "2013W495T123456", 15, 2013, 0, 49, 5, 0, 0, hms, 0, false, local6+hms); -// testParse (t, "2013W495T12", 11, 2013, 0, 49, 5, 0, 0, h, 0, false, local6+h ); - testParse (t, "2013W49T123456", 14, 2013, 0, 49, 0, 0, 0, hms, 0, false, local1+hms); -// testParse (t, "2013W49T12", 10, 2013, 0, 49, 0, 0, 0, h, 0, false, local1+h ); - testParse (t, "2013340T123456", 14, 2013, 0, 0, 0, 340, 0, hms, 0, false, local6+hms); -// testParse (t, "2013340T12", 10, 2013, 0, 0, 0, 340, 0, h, 0, false, local6+h ); - testParse (t, "2013-12T1234", 12, 2013, 12, 0, 0, 0, 0, hm, 0, false, local1+hm ); -// testParse (t, "2013-12T12", 10, 2013, 12, 0, 0, 0, 0, h, 0, false, local1+h ); - - testParse (t, "20131206T123456Z", 16, 2013, 12, 0, 0, 0, 6, hms, 0, true, utc6+hms ); -// testParse (t, "20131206T12Z", 12, 2013, 12, 0, 0, 0, 6, h, 0, true, utc6+h ); - testParse (t, "2013W495T123456Z", 16, 2013, 0, 49, 5, 0, 0, hms, 0, true, utc6+hms ); -// testParse (t, "2013W495T12Z", 12, 2013, 0, 49, 5, 0, 0, h, 0, true, utc6+h ); - testParse (t, "2013W49T123456Z", 15, 2013, 0, 49, 0, 0, 0, hms, 0, true, utc1+hms ); -// testParse (t, "2013W49T12Z", 11, 2013, 0, 49, 0, 0, 0, h, 0, true, utc1+h ); - testParse (t, "2013340T123456Z", 15, 2013, 0, 0, 0, 340, 0, hms, 0, true, utc6+hms ); -// testParse (t, "2013340T12Z", 11, 2013, 0, 0, 0, 340, 0, h, 0, true, utc6+h ); - testParse (t, "2013-12T123456Z", 15, 2013, 12, 0, 0, 0, 0, hms, 0, true, utc1+hms ); - // testParse (t, "2013-12T12Z", 11, 2013, 12, 0, 0, 0, 0, h, 0, true, utc1+h ); - - testParse (t, "20131206T123456+0100", 20, 2013, 12, 0, 0, 0, 6, hms, 3600, false, utc6+hms-z); - testParse (t, "20131206T123456+01", 18, 2013, 12, 0, 0, 0, 6, hms, 3600, false, utc6+hms-z); - testParse (t, "20131206T123456-0100", 20, 2013, 12, 0, 0, 0, 6, hms, -3600, false, utc6+hms+z); - testParse (t, "20131206T123456-01", 18, 2013, 12, 0, 0, 0, 6, hms, -3600, false, utc6+hms+z); -// testParse (t, "20131206T12+0100", 16, 2013, 12, 0, 0, 0, 6, h, 3600, false, utc6+h-z ); -// testParse (t, "20131206T12+01", 14, 2013, 12, 0, 0, 0, 6, h, 3600, false, utc6+h-z ); -// testParse (t, "20131206T12-0100", 16, 2013, 12, 0, 0, 0, 6, h, -3600, false, utc6+h+z ); -// testParse (t, "20131206T12-01", 14, 2013, 12, 0, 0, 0, 6, h, -3600, false, utc6+h+z ); - testParse (t, "2013W495T123456+0100", 20, 2013, 0, 49, 5, 0, 0, hms, 3600, false, utc6+hms-z); - testParse (t, "2013W495T123456+01", 18, 2013, 0, 49, 5, 0, 0, hms, 3600, false, utc6+hms-z); - testParse (t, "2013W495T123456-0100", 20, 2013, 0, 49, 5, 0, 0, hms, -3600, false, utc6+hms+z); - testParse (t, "2013W495T123456-01", 18, 2013, 0, 49, 5, 0, 0, hms, -3600, false, utc6+hms+z); -// testParse (t, "2013W495T12+0100", 16, 2013, 0, 49, 5, 0, 0, h, 3600, false, utc6+h-z ); -// testParse (t, "2013W495T12+01", 14, 2013, 0, 49, 5, 0, 0, h, 3600, false, utc6+h-z ); -// testParse (t, "2013W495T12-0100", 16, 2013, 0, 49, 5, 0, 0, h, -3600, false, utc6+h+z ); -// testParse (t, "2013W495T12-01", 14, 2013, 0, 49, 5, 0, 0, h, -3600, false, utc6+h+z ); - testParse (t, "2013W49T123456+0100", 19, 2013, 0, 49, 0, 0, 0, hms, 3600, false, utc1+hms-z); - testParse (t, "2013W49T123456+01", 17, 2013, 0, 49, 0, 0, 0, hms, 3600, false, utc1+hms-z); - testParse (t, "2013W49T123456-0100", 19, 2013, 0, 49, 0, 0, 0, hms, -3600, false, utc1+hms+z); - testParse (t, "2013W49T123456-01", 17, 2013, 0, 49, 0, 0, 0, hms, -3600, false, utc1+hms+z); -// testParse (t, "2013W49T12+0100", 15, 2013, 0, 49, 0, 0, 0, h, 3600, false, utc1+h-z ); -// testParse (t, "2013W49T12+01", 13, 2013, 0, 49, 0, 0, 0, h, 3600, false, utc1+h-z ); -// testParse (t, "2013W49T12-0100", 15, 2013, 0, 49, 0, 0, 0, h, -3600, false, utc1+h+z ); -// testParse (t, "2013W49T12-01", 13, 2013, 0, 49, 0, 0, 0, h, -3600, false, utc1+h+z ); - testParse (t, "2013340T123456+0100", 19, 2013, 0, 0, 0, 340, 0, hms, 3600, false, utc6+hms-z); - testParse (t, "2013340T123456+01", 17, 2013, 0, 0, 0, 340, 0, hms, 3600, false, utc6+hms-z); - testParse (t, "2013340T123456-0100", 19, 2013, 0, 0, 0, 340, 0, hms, -3600, false, utc6+hms+z); - testParse (t, "2013340T123456-01", 17, 2013, 0, 0, 0, 340, 0, hms, -3600, false, utc6+hms+z); -// testParse (t, "2013340T12+0100", 15, 2013, 0, 0, 0, 340, 0, h, 3600, false, utc6+h-z ); -// testParse (t, "2013340T12+01", 13, 2013, 0, 0, 0, 340, 0, h, 3600, false, utc6+h-z ); -// testParse (t, "2013340T12-0100", 15, 2013, 0, 0, 0, 340, 0, h, -3600, false, utc6+h+z ); -// testParse (t, "2013340T12-01", 13, 2013, 0, 0, 0, 340, 0, h, -3600, false, utc6+h+z ); - testParse (t, "2013-12T123456+0100", 19, 2013, 12, 0, 0, 0, 0, hms, 3600, false, utc1+hms-z); - testParse (t, "2013-12T123456+01", 17, 2013, 12, 0, 0, 0, 0, hms, 3600, false, utc1+hms-z); - testParse (t, "2013-12T123456-0100", 19, 2013, 12, 0, 0, 0, 0, hms, -3600, false, utc1+hms+z); - testParse (t, "2013-12T123456-01", 17, 2013, 12, 0, 0, 0, 0, hms, -3600, false, utc1+hms+z); -// testParse (t, "2013-12T12+0100", 15, 2013, 12, 0, 0, 0, 0, h, 3600, false, utc1+h-z ); -// testParse (t, "2013-12T12+01", 13, 2013, 12, 0, 0, 0, 0, h, 3600, false, utc1+h-z ); -// testParse (t, "2013-12T12-0100", 15, 2013, 12, 0, 0, 0, 0, h, -3600, false, utc1+h+z ); -// testParse (t, "2013-12T12-01", 13, 2013, 12, 0, 0, 0, 0, h, -3600, false, utc1+h+z ); - - // TODO Test validation of individual values. - return 0; } From 98855dc19cab8ed52124f988662a8d011d13211e Mon Sep 17 00:00:00 2001 From: Paul Beckingham Date: Sat, 27 Jun 2015 14:14:36 -0400 Subject: [PATCH 02/20] Docs: Updated NEWS, task.1 with date changes --- NEWS | 5 +++++ doc/man/task.1.in | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/NEWS b/NEWS index 2308a6052..9b6209c6c 100644 --- a/NEWS +++ b/NEWS @@ -12,6 +12,11 @@ Newly deprecated features in Taskwarrior 2.4.5 Removed features in 2.4.5 - The script 'context' was removed, now that context is a core feature. + - Non­extended forms of ISO-8601 date/time support is removed. This means + that 'YYYYMMDD' is no longer supported, but 'YYYY-MM-DD' is. For times, + 'hhmmss' is no longer supported, but 'hh:mm:ss' is. The non-enxtended + forms all contain sequences of digits that make the identification of + IDs, UUIDs, and various date/time formats problematic. Known Issues diff --git a/doc/man/task.1.in b/doc/man/task.1.in index 98cf9e74e..07386f5a2 100644 --- a/doc/man/task.1.in +++ b/doc/man/task.1.in @@ -905,7 +905,7 @@ task ... due:7/14/2008 .TP ISO-8601 -task ... due:20130314T223000Z +task ... due:2013-03-14T22:30:00Z .TP Relative wording From 14c95dcb3b748679d1173e0fa1975c1461c7ab36 Mon Sep 17 00:00:00 2001 From: Paul Beckingham Date: Sat, 27 Jun 2015 14:20:16 -0400 Subject: [PATCH 03/20] Tests: Removed Lexer tests for unsupported ISO date formats --- test/lexer.t.cpp | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/test/lexer.t.cpp b/test/lexer.t.cpp index 5e136deea..0e873c7f8 100644 --- a/test/lexer.t.cpp +++ b/test/lexer.t.cpp @@ -36,7 +36,7 @@ Context context; //////////////////////////////////////////////////////////////////////////////// int main (int argc, char** argv) { - UnitTest t (801); + UnitTest t (795); std::vector > tokens; std::string token; @@ -131,7 +131,7 @@ int main (int argc, char** argv) t.is (tokens[15].first, "'€'", "tokens[15] = \\u20ac --> ''€''"); t.is (Lexer::typeName (tokens[15].second), "string", "tokens[15] = string"); - // Test for ISO-8601 dates (favoring dates in ambiguous cases). + // Test for numbers that are no longer ISO-8601 dates. Lexer l3 ("1 12 123 1234 12345 123456 1234567 12345678"); l3.ambiguity (true); tokens.clear (); @@ -145,21 +145,21 @@ int main (int argc, char** argv) t.is (tokens[0].first, "1", "tokens[0] == '1'"); t.is ((int) tokens[0].second, (int) Lexer::Type::number, "tokens[0] == Type::number"); t.is (tokens[1].first, "12", "tokens[1] == '12'"); - t.is ((int) tokens[1].second, (int) Lexer::Type::date, "tokens[1] == Type::date"); + t.is ((int) tokens[1].second, (int) Lexer::Type::number, "tokens[1] == Type::date"); t.is (tokens[2].first, "123", "tokens[2] == '123'"); t.is ((int) tokens[2].second, (int) Lexer::Type::number, "tokens[2] == Type::number"); // 70 t.is (tokens[3].first, "1234", "tokens[3] == '1234'"); - t.is ((int) tokens[3].second, (int) Lexer::Type::date, "tokens[3] == Type::date"); + t.is ((int) tokens[3].second, (int) Lexer::Type::number, "tokens[3] == Type::date"); t.is (tokens[4].first, "12345", "tokens[4] == '12345'"); t.is ((int) tokens[4].second, (int) Lexer::Type::number, "tokens[4] == Type::number"); t.is (tokens[5].first, "123456", "tokens[5] == '123456'"); - t.is ((int) tokens[5].second, (int) Lexer::Type::date, "tokens[5] == Type::date"); + t.is ((int) tokens[5].second, (int) Lexer::Type::number, "tokens[5] == Type::date"); t.is (tokens[6].first, "1234567", "tokens[6] == '1234567'"); t.is ((int) tokens[6].second, (int) Lexer::Type::number, "tokens[6] == Type::number"); t.is (tokens[7].first, "12345678", "tokens[7] == '12345678'"); t.is ((int) tokens[7].second, (int) Lexer::Type::number, "tokens[7] == Type::number"); // 80 - // Test for ISO-8601 dates (favoring numbers in ambiguous cases). + // Test for numbers that are no longer ISO-8601 dates. Lexer l4 ("1 12 123 1234 12345 123456 1234567 12345678"); l4.ambiguity (false); tokens.clear (); @@ -340,7 +340,6 @@ int main (int argc, char** argv) // Date { "2015-W01", { { "2015-W01", Lexer::Type::date }, NO, NO, NO, NO }, }, { "2015-02-17", { { "2015-02-17", Lexer::Type::date }, NO, NO, NO, NO }, }, - { "20131129T225800Z", { { "20131129T225800Z", Lexer::Type::date }, NO, NO, NO, NO }, }, { "2013-11-29T22:58:00Z", { { "2013-11-29T22:58:00Z", Lexer::Type::date }, NO, NO, NO, NO }, }, // Duration From 7425c8f2ae9052dc707bd8d7d6d8f96d2a7f5638 Mon Sep 17 00:00:00 2001 From: Paul Beckingham Date: Sat, 27 Jun 2015 14:35:07 -0400 Subject: [PATCH 04/20] Test: Removed useless tests - The args.1.t script contained tests that weren't testing what they claimed to test, and are now removed. --- test/args.1.t | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/test/args.1.t b/test/args.1.t index 7532d9cd4..4c12269c7 100755 --- a/test/args.1.t +++ b/test/args.1.t @@ -42,7 +42,6 @@ class TestIDPosition(TestCase): self.t(("add", "one")) self.t(("add", "two")) - self.t(("add", "three")) def test_id(self): """Test id before and after command""" @@ -50,21 +49,12 @@ class TestIDPosition(TestCase): self.assertIn("one", out) self.assertIn("two", out) - self.assertIn("three", out) code, out, err = self.t(("1", "done")) self.assertIn("Completed 1 task.", out) - filter = "rc.allow.empty.filter:yes" - code, out, err = self.t.runError((filter, "done", "2")) - self.assertIn("Command prevented from running.", err) - self.assertNotIn("Completed 1 task.", out) - - filter = "rc.allow.empty.filter:no" - code, out, err = self.t.runError((filter, "done", "2")) - self.assertIn("You did not specify a filter, and with the " - "'allow.empty.filter' value, no action is taken.", err) - self.assertNotIn("Completed 1 task.", out) + code, out, err = self.t(("done", "2")) + self.assertIn("Completed 1 task.", out) if __name__ == "__main__": from simpletap import TAPTestRunner From b67b2ccc963327beaa6c029aef73b034d429408b Mon Sep 17 00:00:00 2001 From: Paul Beckingham Date: Sat, 27 Jun 2015 15:53:24 -0400 Subject: [PATCH 05/20] CLI2: Added ::addContextFilter, and supporting processing --- src/CLI2.cpp | 94 ++++++++++++++++++++------------------ src/CLI2.h | 4 +- src/Filter.cpp | 5 +- src/commands/CmdCustom.cpp | 3 -- 4 files changed, 54 insertions(+), 52 deletions(-) diff --git a/src/CLI2.cpp b/src/CLI2.cpp index 0f2f36b99..0487280c2 100644 --- a/src/CLI2.cpp +++ b/src/CLI2.cpp @@ -414,51 +414,6 @@ void CLI2::analyze () context.debug (dump ("CLI2::analyze end")); } -/* -//////////////////////////////////////////////////////////////////////////////// -// There are situations where a context filter is applied. This method -// determines whether one applies, and if so, applies it. Disqualifiers include: -// - filter contains ID or UUID -void CLI2::addContextFilter () -{ - // Detect if any context is set, and bail out if not - std::string contextName = context.config.get ("context"); - if (contextName == "") - { - context.debug ("No context applied."); - return; - } - - // Detect if UUID or ID is set, and bail out - for (auto& a : _args) - { - // TODO This looks wrong. - if (a.hasTag ("FILTER") && - a.hasTag ("ATTRIBUTE") && - ! a.hasTag ("TERMINATED") && - ! a.hasTag ("WORD") && - (a.attribute ("raw") == "id" || a.attribute ("raw") == "uuid")) - { - context.debug (format ("UUID/ID lexeme found '{1}', not applying context.", a.attribute ("raw"))); - return; - } - } - - // Apply context - context.debug ("Applying context: " + contextName); - std::string contextFilter = context.config.get ("context." + contextName); - - if (contextFilter == "") - context.debug ("Context '" + contextName + "' not defined."); - else - { - addRawFilter ("( " + contextFilter + " )"); - if (context.verbose ("context")) - context.footnote (format ("Context '{1}' set. Use 'task context none' to remove.", contextName)); - } -} -*/ - //////////////////////////////////////////////////////////////////////////////// // Process raw string. void CLI2::addFilter (const std::string& arg) @@ -480,6 +435,52 @@ void CLI2::addFilter (const std::string& arg) analyze (); } +//////////////////////////////////////////////////////////////////////////////// +// There are situations where a context filter is applied. This method +// determines whether one applies, and if so, applies it. Disqualifiers include: +// - filter contains ID or UUID +void CLI2::addContextFilter () +{ + // Detect if any context is set, and bail out if not + std::string contextName = context.config.get ("context"); + if (contextName == "") + { + context.debug ("No context applied."); + return; + } + +/* + // Detect if UUID or ID is set, and bail out + for (auto& a : _args) + { + // TODO This is needed, but the parsing is not yet complete, so the logic + // below is not valid. + if (a.hasTag ("FILTER") && + a.hasTag ("ATTRIBUTE") && + ! a.hasTag ("TERMINATED") && + ! a.hasTag ("WORD") && + (a.attribute ("raw") == "id" || a.attribute ("raw") == "uuid")) + { + context.debug (format ("UUID/ID lexeme found '{1}', not applying context.", a.attribute ("raw"))); + return; + } + } +*/ + + // Apply context + context.debug ("Applying context: " + contextName); + std::string contextFilter = context.config.get ("context." + contextName); + + if (contextFilter == "") + context.debug ("Context '" + contextName + "' not defined."); + else + { + addFilter (contextFilter); + if (context.verbose ("context")) + context.footnote (format ("Context '{1}' set. Use 'task context none' to remove.", contextName)); + } +} + //////////////////////////////////////////////////////////////////////////////// // Parse the command line, identifiying filter components, expanding syntactic // sugar as necessary. @@ -489,6 +490,9 @@ void CLI2::prepareFilter (bool applyContext) _id_ranges.clear (); _uuid_list.clear (); + if (applyContext) + addContextFilter (); + // Classify FILTER and MODIFICATION args, based on CMD and READCMD/WRITECMD. bool changes = false; bool foundCommand = false; diff --git a/src/CLI2.h b/src/CLI2.h index 3948ec5ea..4e768fb4c 100644 --- a/src/CLI2.h +++ b/src/CLI2.h @@ -73,10 +73,8 @@ public: void add (const std::string&); void analyze (); -/* - void addContextFilter (); -*/ void addFilter (const std::string& arg); + void addContextFilter (); void prepareFilter (bool applyContext = true); const std::vector getWords (bool filtered = true); bool canonicalize (std::string&, const std::string&, const std::string&) const; diff --git a/src/Filter.cpp b/src/Filter.cpp index a83aacbec..c2221f6a2 100644 --- a/src/Filter.cpp +++ b/src/Filter.cpp @@ -73,7 +73,8 @@ void Filter::subset (const std::vector & input, std::vector & output context.timer_filter.start (); _startCount = (int) input.size (); -// context.cli2.prepareFilter (applyContext); + context.cli2.prepareFilter (applyContext); + std::vector > precompiled; for (auto& a : context.cli2._args) if (a.hasTag ("FILTER")) @@ -118,6 +119,8 @@ void Filter::subset (std::vector & output, bool applyContext /* = true */) { context.timer_filter.start (); + context.cli2.prepareFilter (applyContext); + std::vector > precompiled; for (auto& a : context.cli2._args) if (a.hasTag ("FILTER")) diff --git a/src/commands/CmdCustom.cpp b/src/commands/CmdCustom.cpp index da672e099..e2b9f63ce 100644 --- a/src/commands/CmdCustom.cpp +++ b/src/commands/CmdCustom.cpp @@ -81,10 +81,7 @@ int CmdCustom::execute (std::string& output) // Add the report filter to any existing filter. if (reportFilter != "") - { context.cli2.addFilter (reportFilter); - context.cli2.prepareFilter (); - } // Apply filter. handleRecurrence (); From 37e41effdef1c409135af51598a616917ebad57f Mon Sep 17 00:00:00 2001 From: Paul Beckingham Date: Sat, 27 Jun 2015 15:59:02 -0400 Subject: [PATCH 06/20] L10N: Removed 'permanently' from deletion notification - Deletion is not really permanent (thanks to smemsh). --- src/l10n/eng-USA.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/l10n/eng-USA.h b/src/l10n/eng-USA.h index f0e15cdad..bce55af81 100644 --- a/src/l10n/eng-USA.h +++ b/src/l10n/eng-USA.h @@ -372,7 +372,7 @@ #define STRING_CMD_UDAS_ORPHANS "{1} Orphan UDAs" #define STRING_CMD_DELETE_USAGE "Deletes the specified task" -#define STRING_CMD_DELETE_CONFIRM "Permanently delete task {1} '{2}'?" +#define STRING_CMD_DELETE_CONFIRM "Delete task {1} '{2}'?" #define STRING_CMD_DELETE_TASK "Deleting task {1} '{2}'." #define STRING_CMD_DELETE_TASK_R "Deleting recurring task {1} '{2}'." #define STRING_CMD_DELETE_CONFIRM_R "This is a recurring task. Do you want to delete all pending recurrences of this same task?" From 9ece20d635dd50fbf69274f614144bf4c6d02941 Mon Sep 17 00:00:00 2001 From: Paul Beckingham Date: Sat, 27 Jun 2015 16:12:30 -0400 Subject: [PATCH 07/20] Test: Changed tests that depended on 'Permanently' being part of the feedback --- test/confirmation.t | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/test/confirmation.t b/test/confirmation.t index 29cc5ca14..17748741b 100755 --- a/test/confirmation.t +++ b/test/confirmation.t @@ -56,49 +56,49 @@ qx{../src/task rc:$rc add foo 2>&1} for 1..10; # Test the various forms of "Yes". my $output = qx{echo "Yes" | ../src/task rc:$rc 1 del 2>&1}; -like ($output, qr/Permanently delete task 1 'foo'\? \(yes\/no\)/, 'confirmation - Yes works'); +like ($output, qr/Delete task 1 'foo'\? \(yes\/no\)/, 'confirmation - Yes works'); unlike ($output, qr/Task not deleted\./, 'confirmation - Yes works'); $output = qx{echo "ye" | ../src/task rc:$rc 2 del 2>&1}; -like ($output, qr/Permanently delete task 2 'foo'\? \(yes\/no\)/, 'confirmation - ye works'); +like ($output, qr/Delete task 2 'foo'\? \(yes\/no\)/, 'confirmation - ye works'); unlike ($output, qr/Task not deleted\./, 'confirmation - ye works'); $output = qx{echo "y" | ../src/task rc:$rc 3 del 2>&1}; -like ($output, qr/Permanently delete task 3 'foo'\? \(yes\/no\)/, 'confirmation - y works'); +like ($output, qr/Delete task 3 'foo'\? \(yes\/no\)/, 'confirmation - y works'); unlike ($output, qr/Task not deleted\./, 'confirmation - y works'); $output = qx{echo "YES" | ../src/task rc:$rc 4 del 2>&1}; -like ($output, qr/Permanently delete task 4 'foo'\? \(yes\/no\)/, 'confirmation - YES works'); +like ($output, qr/Delete task 4 'foo'\? \(yes\/no\)/, 'confirmation - YES works'); unlike ($output, qr/Task not deleted\./, 'confirmation - YES works'); # 10 $output = qx{echo "YE" | ../src/task rc:$rc 5 del 2>&1}; -like ($output, qr/Permanently delete task 5 'foo'\? \(yes\/no\)/, 'confirmation - YE works'); +like ($output, qr/Delete task 5 'foo'\? \(yes\/no\)/, 'confirmation - YE works'); unlike ($output, qr/Task not deleted\./, 'confirmation - YE works'); $output = qx{echo "Y" | ../src/task rc:$rc 6 del 2>&1}; -like ($output, qr/Permanently delete task 6 'foo'\? \(yes\/no\)/, 'confirmation - Y works'); +like ($output, qr/Delete task 6 'foo'\? \(yes\/no\)/, 'confirmation - Y works'); unlike ($output, qr/Task not deleted\./, 'confirmation - Y works'); # Test the various forms of "no". $output = qx{echo "no" | ../src/task rc:$rc 7 del 2>&1}; -like ($output, qr/Permanently delete task 7 'foo'\? \(yes\/no\)/, 'confirmation - no works'); +like ($output, qr/Delete task 7 'foo'\? \(yes\/no\)/, 'confirmation - no works'); like ($output, qr/Task not deleted\./, 'confirmation - no works'); $output = qx{echo "n" | ../src/task rc:$rc 7 del 2>&1}; -like ($output, qr/Permanently delete task 7 'foo'\? \(yes\/no\)/, 'confirmation - n works'); +like ($output, qr/Delete task 7 'foo'\? \(yes\/no\)/, 'confirmation - n works'); like ($output, qr/Task not deleted\./, 'confirmation - n works'); $output = qx{echo "NO" | ../src/task rc:$rc 7 del 2>&1}; -like ($output, qr/Permanently delete task 7 'foo'\? \(yes\/no\)/, 'confirmation - NO works'); +like ($output, qr/Delete task 7 'foo'\? \(yes\/no\)/, 'confirmation - NO works'); like ($output, qr/Task not deleted\./, 'confirmation - NO works'); # 20 $output = qx{echo "N" | ../src/task rc:$rc 7 del 2>&1}; -like ($output, qr/Permanently delete task 7 'foo'\? \(yes\/no\)/, 'confirmation - N works'); +like ($output, qr/Delete task 7 'foo'\? \(yes\/no\)/, 'confirmation - N works'); like ($output, qr/Task not deleted\./, 'confirmation - N works'); # Test newlines. $output = qx{cat response.txt | ../src/task rc:$rc 7 del 2>&1}; -like ($output, qr/(Permanently delete task 7 'foo'\? \(yes\/no\)) \1 \1/, 'confirmation - \n re-prompt works'); # 43 +like ($output, qr/(Delete task 7 'foo'\? \(yes\/no\)) \1 \1/, 'confirmation - \n re-prompt works'); # 43 # Cleanup. unlink qw(pending.data completed.data undo.data backlog.data response.txt), $rc; From 299ecad49f40b97314e8bc34f5fe0f7db26d1c10 Mon Sep 17 00:00:00 2001 From: Paul Beckingham Date: Sat, 27 Jun 2015 17:13:48 -0400 Subject: [PATCH 08/20] Tests: Converted to Python --- test/feature.891.t | 250 ++++++++++----------------------------------- 1 file changed, 54 insertions(+), 196 deletions(-) diff --git a/test/feature.891.t b/test/feature.891.t index 064b8e86d..ccc4fdcad 100755 --- a/test/feature.891.t +++ b/test/feature.891.t @@ -1,205 +1,63 @@ -#! /usr/bin/env perl -################################################################################ -## -## Copyright 2006 - 2015, Paul Beckingham, Federico Hernandez. -## -## Permission is hereby granted, free of charge, to any person obtaining a copy -## of this software and associated documentation files (the "Software"), to deal -## in the Software without restriction, including without limitation the rights -## to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -## copies of the Software, and to permit persons to whom the Software is -## furnished to do so, subject to the following conditions: -## -## The above copyright notice and this permission notice shall be included -## in all copies or substantial portions of the Software. -## -## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -## OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -## FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -## THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -## LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -## OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -## SOFTWARE. -## -## http://www.opensource.org/licenses/mit-license.php -## -################################################################################ +#!/usr/bin/env python2.7 +# -*- coding: utf-8 -*- +############################################################################### +# +# Copyright 2006 - 2015, Paul Beckingham, Federico Hernandez. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +# http://www.opensource.org/licenses/mit-license.php +# +############################################################################### -use strict; -use warnings; -use Time::Local; -use Test::More tests => 34; +import sys +import os +import unittest +# Ensure python finds the local simpletap module +sys.path.append(os.path.dirname(os.path.abspath(__file__))) -# Ensure environment has no influence. -delete $ENV{'TASKDATA'}; -delete $ENV{'TASKRC'}; +from basetest import Task, TestCase -use File::Basename; -my $ut = basename ($0); -my $rc = $ut . '.rc'; -# Create the rc file. -if (open my $fh, '>', $rc) -{ - print $fh "data.location=.\n", - "confirmation=off\n"; - close $fh; -} +class TestFeature891(TestCase): + @classmethod + def setUp(self): + self.t = Task() + self.t(("add", "one")) + self.t(("add", "two")) -# Feature 891: UUID filter should be uuid.endswith by default -# Create some example data directly. This is so that we have complete control -# over the UUID. -if (open my $fh, '>', 'pending.data') -{ - my $timeA = timegm (00,00,12,22,11,2008); - my $timeB = timegm (00,00,12,17,03,2009); - print $fh <&1}; -my ($uuid) = $output =~ /UUID\s+(\S{36})/ms; + # TODO This should fail because a 7-character UUID is not a UUID, but + # instead it blindly does nothing, and succeeds. + #code, out, err = self.t((self.uuid[0:6], "list")) -$output = qx{../src/task rc:$rc $uuid list 2>&1}; -like ($output, qr/one/, "Found with $uuid"); - -my ($short) = $uuid =~ /^(.{35})/; -$output = qx{../src/task rc:$rc $short list 2>&1}; -like ($output, qr/one/, "Found with $short"); - -($short) = $uuid =~ /^(.{34})/; -$output = qx{../src/task rc:$rc $short list 2>&1}; -like ($output, qr/one/, "Found with $short"); - -($short) = $uuid =~ /^(.{33})/; -$output = qx{../src/task rc:$rc $short list 2>&1}; -like ($output, qr/one/, "Found with $short"); - -($short) = $uuid =~ /^(.{32})/; -$output = qx{../src/task rc:$rc $short list 2>&1}; -like ($output, qr/one/, "Found with $short"); - -($short) = $uuid =~ /^(.{31})/; -$output = qx{../src/task rc:$rc $short list 2>&1}; -like ($output, qr/one/, "Found with $short"); - -($short) = $uuid =~ /^(.{30})/; -$output = qx{../src/task rc:$rc $short list 2>&1}; -like ($output, qr/one/, "Found with $short"); - -($short) = $uuid =~ /^(.{29})/; -$output = qx{../src/task rc:$rc $short list 2>&1}; -like ($output, qr/one/, "Found with $short"); - -($short) = $uuid =~ /^(.{28})/; -$output = qx{../src/task rc:$rc $short list 2>&1}; -like ($output, qr/one/, "Found with $short"); - -($short) = $uuid =~ /^(.{27})/; -$output = qx{../src/task rc:$rc $short list 2>&1}; -like ($output, qr/one/, "Found with $short"); - -($short) = $uuid =~ /^(.{26})/; -$output = qx{../src/task rc:$rc $short list 2>&1}; -like ($output, qr/one/, "Found with $short"); - -($short) = $uuid =~ /^(.{25})/; -$output = qx{../src/task rc:$rc $short list 2>&1}; -like ($output, qr/one/, "Found with $short"); - -($short) = $uuid =~ /^(.{24})/; -$output = qx{../src/task rc:$rc $short list 2>&1}; -like ($output, qr/one/, "Found with $short"); - -($short) = $uuid =~ /^(.{23})/; -$output = qx{../src/task rc:$rc $short list 2>&1}; -like ($output, qr/one/, "Found with $short"); - -($short) = $uuid =~ /^(.{22})/; -$output = qx{../src/task rc:$rc $short list 2>&1}; -like ($output, qr/one/, "Found with $short"); - -($short) = $uuid =~ /^(.{21})/; -$output = qx{../src/task rc:$rc $short list 2>&1}; -like ($output, qr/one/, "Found with $short"); - -($short) = $uuid =~ /^(.{20})/; -$output = qx{../src/task rc:$rc $short list 2>&1}; -like ($output, qr/one/, "Found with $short"); - -($short) = $uuid =~ /^(.{19})/; -$output = qx{../src/task rc:$rc $short list 2>&1}; -like ($output, qr/one/, "Found with $short"); - -($short) = $uuid =~ /^(.{18})/; -$output = qx{../src/task rc:$rc $short list 2>&1}; -like ($output, qr/one/, "Found with $short"); - -($short) = $uuid =~ /^(.{17})/; -$output = qx{../src/task rc:$rc $short list 2>&1}; -like ($output, qr/one/, "Found with $short"); - -($short) = $uuid =~ /^(.{16})/; -$output = qx{../src/task rc:$rc $short list 2>&1}; -like ($output, qr/one/, "Found with $short"); - -($short) = $uuid =~ /^(.{15})/; -$output = qx{../src/task rc:$rc $short list 2>&1}; -like ($output, qr/one/, "Found with $short"); - -($short) = $uuid =~ /^(.{14})/; -$output = qx{../src/task rc:$rc $short list 2>&1}; -like ($output, qr/one/, "Found with $short"); - -($short) = $uuid =~ /^(.{13})/; -$output = qx{../src/task rc:$rc $short list 2>&1}; -like ($output, qr/one/, "Found with $short"); - -($short) = $uuid =~ /^(.{12})/; -$output = qx{../src/task rc:$rc $short list 2>&1}; -like ($output, qr/one/, "Found with $short"); - -($short) = $uuid =~ /^(.{11})/; -$output = qx{../src/task rc:$rc $short list 2>&1}; -like ($output, qr/one/, "Found with $short"); - -($short) = $uuid =~ /^(.{10})/; -$output = qx{../src/task rc:$rc $short list 2>&1}; -like ($output, qr/one/, "Found with $short"); - -($short) = $uuid =~ /^(.{9})/; -$output = qx{../src/task rc:$rc $short list 2>&1}; -like ($output, qr/one/, "Found with $short"); - -($short) = $uuid =~ /^(.{8})/; -$output = qx{../src/task rc:$rc $short list 2>&1}; -like ($output, qr/one/, "Found with $short"); - -($short) = $uuid =~ /^(.{7})/; -$output = qx{../src/task rc:$rc $short list 2>&1}; -unlike ($output, qr/one/, "Not found with $short"); - -($short) = $uuid =~ /^(.{6})/; -$output = qx{../src/task rc:$rc $short list 2>&1}; -unlike ($output, qr/one/, "Not found with $short"); - -($short) = $uuid =~ /^(.{5})/; -$output = qx{../src/task rc:$rc $short list 2>&1}; -unlike ($output, qr/one/, "Not found with $short"); - -($short) = $uuid =~ /^(.{4})/; -$output = qx{../src/task rc:$rc $short list 2>&1}; -unlike ($output, qr/one/, "Not found with $short"); - -($short) = $uuid =~ /^(.{3})/; -$output = qx{../src/task rc:$rc $short list 2>&1}; -unlike ($output, qr/one/, "Not found with $short"); - -# Cleanup. -unlink qw(pending.data completed.data undo.data backlog.data), $rc; -exit 0; +if __name__ == "__main__": + from simpletap import TAPTestRunner + unittest.main(testRunner=TAPTestRunner()) +# vim: ai sts=4 et sw=4 From 1824a542f6ada6180440622f994e9c52f0ce8d9b Mon Sep 17 00:00:00 2001 From: Paul Beckingham Date: Sat, 27 Jun 2015 17:35:15 -0400 Subject: [PATCH 09/20] CLI2: Obey the terminator and skip Lexing --- src/CLI2.cpp | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/CLI2.cpp b/src/CLI2.cpp index 0487280c2..8676bd2ff 100644 --- a/src/CLI2.cpp +++ b/src/CLI2.cpp @@ -340,15 +340,28 @@ void CLI2::lexArguments () { // Note: Starts interating at index 1, because ::handleArg0 has already // processed it. + bool terminated = false; for (unsigned int i = 1; i < _original_args.size (); ++i) { - std::string lexeme; - Lexer::Type type; - Lexer lex (_original_args[i]); - lex.ambiguity (false); + if (_original_args[i] == "--") + { + terminated = true; + _args.push_back (A2 (_original_args[i], Lexer::Type::separator)); + } + else if (terminated) + { + _args.push_back (A2 (_original_args[i], Lexer::Type::word)); + } + else + { + std::string lexeme; + Lexer::Type type; + Lexer lex (_original_args[i]); + lex.ambiguity (false); - while (lex.token (lexeme, type)) - _args.push_back (A2 (lexeme, type)); + while (lex.token (lexeme, type)) + _args.push_back (A2 (lexeme, type)); + } } if (context.config.getInteger ("debug.parser") >= 3) From f96a42d8b03cc2f019126f58b958aa4815340a8e Mon Sep 17 00:00:00 2001 From: Paul Beckingham Date: Sat, 27 Jun 2015 18:38:24 -0400 Subject: [PATCH 10/20] Feature: Error on virtual tag modification attempt - An attempt to add or remove a virtual tag is now an error (thanks to Scott M). --- ChangeLog | 2 +- NEWS | 1 + doc/man/task.1.in | 8 +++++--- src/Task.cpp | 5 ++++- src/commands/CmdInfo.cpp | 1 + src/feedback.cpp | 35 +++++++++++++++++++++++++++++++++++ src/l10n/deu-DEU.h | 1 + src/l10n/eng-USA.h | 1 + src/l10n/epo-RUS.h | 1 + src/l10n/esp-ESP.h | 1 + src/l10n/fra-FRA.h | 1 + src/l10n/ita-ITA.h | 1 + src/l10n/jpn-JPN.h | 1 + src/l10n/pol-POL.h | 1 + src/l10n/por-PRT.h | 1 + src/main.h | 1 + 16 files changed, 57 insertions(+), 5 deletions(-) diff --git a/ChangeLog b/ChangeLog index 37ba7d0e3..634528877 100644 --- a/ChangeLog +++ b/ChangeLog @@ -24,7 +24,7 @@ and enabling more flexible use of the function. - Enable "task sync" support by default. "cmake -DENABLE_SYNC=OFF" allows disabling it and building Taskwarrior without libgnutls available. - +- An attempt to add or remove a virtual tag is now an error (thanks to Scott M). ------ current release --------------------------- diff --git a/NEWS b/NEWS index 9b6209c6c..b4ecd6851 100644 --- a/NEWS +++ b/NEWS @@ -2,6 +2,7 @@ New Features in Taskwarrior 2.4.5 - The active context, if one is set, is now identified in "task context list" + - It is an error to attempt and add or remove of a virtual tag. New commands in Taskwarrior 2.4.5 diff --git a/doc/man/task.1.in b/doc/man/task.1.in index 07386f5a2..325bc4d39 100644 --- a/doc/man/task.1.in +++ b/doc/man/task.1.in @@ -504,9 +504,10 @@ Shows a report of aggregated task status by project. .TP .B task tags -Show a list of all tags used. Any special tags used are highlighted. Note that +Show a list of all tags used. Any special tags used are highlighted. Note that virtual tags are not listed - they don't really exist, and are just a convenient -notation for other task metadata. +notation for other task metadata. It is an error to attempt to add or remove a +virtual tag. .TP .B task timesheet [weeks] @@ -666,7 +667,8 @@ are: DELETED Matches if the task has deleted status You can use +BLOCKED to filter blocked tasks, or -BLOCKED for unblocked tasks. -Similarly, -BLOCKED is equivalent to +UNBLOCKED. +Similarly, -BLOCKED is equivalent to +UNBLOCKED. It is an error to attempt to +add or remove a virtual tag. .TP .B project: diff --git a/src/Task.cpp b/src/Task.cpp index c518684bd..7e35548a3 100644 --- a/src/Task.cpp +++ b/src/Task.cpp @@ -1092,6 +1092,7 @@ bool Task::hasTag (const std::string& tag) const { // Synthetic tags - dynamically generated, but do not occupy storage space. // Note: This list must match that in CmdInfo::execute. + // Note: This list must match that in ::feedback_reserved_tags. if (tag == "BLOCKED") return is_blocked; if (tag == "UNBLOCKED") return !is_blocked; if (tag == "BLOCKING") return is_blocking; @@ -2094,11 +2095,13 @@ void Task::modify (modType type, bool text_required /* = false */) else if (a._lextype == Lexer::Type::tag) { std::string tag = a.attribute ("name"); + feedback_reserved_tags (tag); + if (a.attribute ("sign") == "+") { context.debug (label + "tags <-- add '" + tag + "'"); addTag (tag); - feedback_special_tags ((*this), tag); + feedback_special_tags (*this, tag); } else { diff --git a/src/commands/CmdInfo.cpp b/src/commands/CmdInfo.cpp index ccd0ab2a9..259396f72 100644 --- a/src/commands/CmdInfo.cpp +++ b/src/commands/CmdInfo.cpp @@ -303,6 +303,7 @@ int CmdInfo::execute (std::string& output) // Virtual tags. { // Note: This list must match that in Task::hasTag. + // Note: This list must match that in ::feedback_reserved_tags. std::string virtualTags = ""; if (task.hasTag ("ACTIVE")) virtualTags += "ACTIVE "; if (task.hasTag ("ANNOTATED")) virtualTags += "ANNOTATED "; diff --git a/src/feedback.cpp b/src/feedback.cpp index 5f4d1d6b6..7bbb7966e 100644 --- a/src/feedback.cpp +++ b/src/feedback.cpp @@ -351,6 +351,41 @@ void feedback_affected (const std::string& effect, const Task& task) } } +//////////////////////////////////////////////////////////////////////////////// +// Implements feedback and error when adding a reserved tag name. +void feedback_reserved_tags (const std::string& tag) +{ + // Note: This list must match that in Task::hasTag. + // Note: This list must match that in CmdInfo::execute. + if (tag == "BLOCKED" || + tag == "UNBLOCKED" || + tag == "BLOCKING" || + tag == "READY" || + tag == "DUE" || + tag == "DUETODAY" || + tag == "TODAY" || + tag == "YESTERDAY" || + tag == "TOMORROW" || + tag == "OVERDUE" || + tag == "WEEK" || + tag == "MONTH" || + tag == "YEAR" || + tag == "ACTIVE" || + tag == "SCHEDULED" || + tag == "CHILD" || + tag == "UNTIL" || + tag == "ANNOTATED" || + tag == "TAGGED" || + tag == "PARENT" || + tag == "WAITING" || + tag == "PENDING" || + tag == "COMPLETED" || + tag == "DELETED") + { + throw format (STRING_FEEDBACK_TAG_VIRTUAL, tag); + } +} + //////////////////////////////////////////////////////////////////////////////// // Implements feedback when adding special tags to a task. void feedback_special_tags (const Task& task, const std::string& tag) diff --git a/src/l10n/deu-DEU.h b/src/l10n/deu-DEU.h index 2e33b07dd..a64746e84 100644 --- a/src/l10n/deu-DEU.h +++ b/src/l10n/deu-DEU.h @@ -792,6 +792,7 @@ #define STRING_FEEDBACK_TAG_NONAG "Das besondere Schlagwort 'nonag' verhindert Nachfragen, wenn diese Aufgabe geändert wird." #define STRING_FEEDBACK_TAG_NOCAL "Das besondere Schlagwort 'nocal' verhindert, dass diese Aufgabe im 'calendar'-Report erscheint." #define STRING_FEEDBACK_TAG_NEXT "Das besondere Schlagwort 'next' erhöht die Dringlichkeit dieser Aufgabe, sodass sie im 'next'-Report erscheint." +#define STRING_FEEDBACK_TAG_VIRTUAL "Virtual tags (including '{1}') are reserved and may not be added or removed." #define STRING_FEEDBACK_UNBLOCKED "Aufgabe {1} '{2}' entsperrt." #define STRING_FEEDBACK_EXPIRED "Aufgabe {1} '{2}' ist abgelaufen und wurde gelöscht." #define STRING_FEEDBACK_BACKLOG "Lokale Änderungen. Datenabgleich erforderlich." diff --git a/src/l10n/eng-USA.h b/src/l10n/eng-USA.h index bce55af81..85d528cd1 100644 --- a/src/l10n/eng-USA.h +++ b/src/l10n/eng-USA.h @@ -792,6 +792,7 @@ #define STRING_FEEDBACK_TAG_NONAG "The 'nonag' special tag will prevent nagging when this task is modified." #define STRING_FEEDBACK_TAG_NOCAL "The 'nocal' special tag will keep this task off the 'calendar' report." #define STRING_FEEDBACK_TAG_NEXT "The 'next' special tag will boost the urgency of this task so it appears on the 'next' report." +#define STRING_FEEDBACK_TAG_VIRTUAL "Virtual tags (including '{1}') are reserved and may not be added or removed." #define STRING_FEEDBACK_UNBLOCKED "Unblocked {1} '{2}'." #define STRING_FEEDBACK_EXPIRED "Task {1} '{2}' expired and was deleted." #define STRING_FEEDBACK_BACKLOG "There are local changes. Sync required." diff --git a/src/l10n/epo-RUS.h b/src/l10n/epo-RUS.h index 1aa4a64f4..01c637f56 100644 --- a/src/l10n/epo-RUS.h +++ b/src/l10n/epo-RUS.h @@ -792,6 +792,7 @@ #define STRING_FEEDBACK_TAG_NONAG "Speciala etikedo 'nonag' antaŭmalebligitos molestojn, kiam oni modifus tiun taskon." #define STRING_FEEDBACK_TAG_NOCAL "Speciala etikedo 'nocal' ekskluzivos tiun taskon ĉe raporto 'calendar'." #define STRING_FEEDBACK_TAG_NEXT "Speciala etikedo 'next' pligrandigos la urĝecon de tiu tasko por ke ĝi aperus ĉe raporto 'next'." +#define STRING_FEEDBACK_TAG_VIRTUAL "Virtual tags (including '{1}') are reserved and may not be added or removed." #define STRING_FEEDBACK_UNBLOCKED "Malblokis {1} '{2}'." #define STRING_FEEDBACK_EXPIRED "Tasko {1} '{2}' fortempiĝis do estis viŝata." #define STRING_FEEDBACK_BACKLOG "Estas lokaj ŝanĝoj. Sinkronigo devita." diff --git a/src/l10n/esp-ESP.h b/src/l10n/esp-ESP.h index 1ec86747a..1da9e809d 100644 --- a/src/l10n/esp-ESP.h +++ b/src/l10n/esp-ESP.h @@ -804,6 +804,7 @@ #define STRING_FEEDBACK_TAG_NONAG "La marca especial 'nonag' evitará el recuerdo fastidioso cuando la tarea sea modificada." #define STRING_FEEDBACK_TAG_NOCAL "La marca especial 'nocal' mantendrá esta tarea fuera del informe 'calendar'." #define STRING_FEEDBACK_TAG_NEXT "La etiqueta especial 'next' aumentará la urgencia de esta tarea para que aparezca en el informe 'next'." +#define STRING_FEEDBACK_TAG_VIRTUAL "Virtual tags (including '{1}') are reserved and may not be added or removed." #define STRING_FEEDBACK_UNBLOCKED "Desbloqueada {1} '{2}'." #define STRING_FEEDBACK_EXPIRED "La tarea {1} '{2}' caducó y fue eliminada." #define STRING_FEEDBACK_BACKLOG "Hay modificaciones locales. Se require una sincronización." diff --git a/src/l10n/fra-FRA.h b/src/l10n/fra-FRA.h index 7495ffb8c..5497f40a0 100644 --- a/src/l10n/fra-FRA.h +++ b/src/l10n/fra-FRA.h @@ -792,6 +792,7 @@ #define STRING_FEEDBACK_TAG_NONAG "The 'nonag' special tag will prevent nagging when this task is modified." #define STRING_FEEDBACK_TAG_NOCAL "The 'nocal' special tag will keep this task off the 'calendar' report." #define STRING_FEEDBACK_TAG_NEXT "The 'next' special tag will boost the urgency of this task so it appears on the 'next' report." +#define STRING_FEEDBACK_TAG_VIRTUAL "Virtual tags (including '{1}') are reserved and may not be added or removed." #define STRING_FEEDBACK_UNBLOCKED "Unblocked {1} '{2}'." #define STRING_FEEDBACK_EXPIRED "Tâche {1} '{2}' a expiré et a été supprimée." #define STRING_FEEDBACK_BACKLOG "Il y a des changements locaux. Synchronisation requise." diff --git a/src/l10n/ita-ITA.h b/src/l10n/ita-ITA.h index 0008652f0..5c67c592a 100644 --- a/src/l10n/ita-ITA.h +++ b/src/l10n/ita-ITA.h @@ -791,6 +791,7 @@ #define STRING_FEEDBACK_TAG_NONAG "Il tag speciale 'nonag' eviterà problemi quando il task è modificato." #define STRING_FEEDBACK_TAG_NOCAL "Il tag speciale 'nocal' manterrà il task fuori dal report 'calendar'." #define STRING_FEEDBACK_TAG_NEXT "Il tag speciale 'next' aumenterà l'urgenza di questo task in modo che appaia nel report 'next'." +#define STRING_FEEDBACK_TAG_VIRTUAL "Virtual tags (including '{1}') are reserved and may not be added or removed." #define STRING_FEEDBACK_UNBLOCKED "Sbloccato {1} '{2}'." #define STRING_FEEDBACK_EXPIRED "Il task {1} '{2}' è scaduto ed è stato eliminato" #define STRING_FEEDBACK_BACKLOG "There are local changes. Sync required." diff --git a/src/l10n/jpn-JPN.h b/src/l10n/jpn-JPN.h index f5dd4c6fc..0ad641db4 100644 --- a/src/l10n/jpn-JPN.h +++ b/src/l10n/jpn-JPN.h @@ -792,6 +792,7 @@ #define STRING_FEEDBACK_TAG_NONAG "The 'nonag' special tag will prevent nagging when this task is modified." #define STRING_FEEDBACK_TAG_NOCAL "The 'nocal' special tag will keep this task off the 'calendar' report." #define STRING_FEEDBACK_TAG_NEXT "The 'next' special tag will boost the urgency of this task so it appears on the 'next' report." +#define STRING_FEEDBACK_TAG_VIRTUAL "Virtual tags (including '{1}') are reserved and may not be added or removed." #define STRING_FEEDBACK_UNBLOCKED "Unblocked {1} '{2}'." #define STRING_FEEDBACK_EXPIRED "Task {1} '{2}' expired and was deleted." #define STRING_FEEDBACK_BACKLOG "There are local changes. Sync required." diff --git a/src/l10n/pol-POL.h b/src/l10n/pol-POL.h index 634e40604..25e0739bb 100644 --- a/src/l10n/pol-POL.h +++ b/src/l10n/pol-POL.h @@ -792,6 +792,7 @@ #define STRING_FEEDBACK_TAG_NONAG "Specjalny tag 'nonag' uchroni przed upierdliwością kiedy zadanie jest modyfikowane." #define STRING_FEEDBACK_TAG_NOCAL "Specjalny tag 'nocal' spowoduje nie dodawanie zadania do kalendarza." #define STRING_FEEDBACK_TAG_NEXT "Specjalny tag 'next' podniesie pilność tego zadania co spowoduje wyświetlenie go w raporcie 'next'." +#define STRING_FEEDBACK_TAG_VIRTUAL "Virtual tags (including '{1}') are reserved and may not be added or removed." #define STRING_FEEDBACK_UNBLOCKED "Odblokowane {1} '{2}'." #define STRING_FEEDBACK_EXPIRED "Zadanie {1} '{2}' jest przedawnione i zostało usunięte." #define STRING_FEEDBACK_BACKLOG "Wykryto lokalne zmiany. Wymagana synchronizacja." diff --git a/src/l10n/por-PRT.h b/src/l10n/por-PRT.h index 47496bb7e..32a8d7d41 100644 --- a/src/l10n/por-PRT.h +++ b/src/l10n/por-PRT.h @@ -792,6 +792,7 @@ #define STRING_FEEDBACK_TAG_NONAG "A marca especial 'nonag' irá prevenir avisos quando a tarefa é modificada." #define STRING_FEEDBACK_TAG_NOCAL "A marca especial 'nocal' irá manter esta tarefa ausente do relatório de 'calendário'." #define STRING_FEEDBACK_TAG_NEXT "A marca especial 'next' irá aumentar a urgência desta tarefa de modo a que apareça no relatório 'next'." +#define STRING_FEEDBACK_TAG_VIRTUAL "Virtual tags (including '{1}') are reserved and may not be added or removed." #define STRING_FEEDBACK_UNBLOCKED "Desbloqueada {1} '{2}'." #define STRING_FEEDBACK_EXPIRED "Tarefa {1} '{2}' expirou e foi eliminada." #define STRING_FEEDBACK_BACKLOG "Há modificações locais. Necessário sincronizar (sync)." diff --git a/src/main.h b/src/main.h index 3b0bc281f..93d4d4901 100644 --- a/src/main.h +++ b/src/main.h @@ -67,6 +67,7 @@ std::string renderAttribute (const std::string&, const std::string&, const std:: void feedback_affected (const std::string&); void feedback_affected (const std::string&, int); void feedback_affected (const std::string&, const Task&); +void feedback_reserved_tags (const std::string&); void feedback_special_tags (const Task&, const std::string&); void feedback_unblocked (const Task&); void feedback_backlog (); From e0291d35e4eca75694b92cc7f7d02216c19b2fc0 Mon Sep 17 00:00:00 2001 From: Paul Beckingham Date: Sat, 27 Jun 2015 20:48:03 -0400 Subject: [PATCH 11/20] Test: Corrected test --- test/feature.print.empty.columns.t | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/feature.print.empty.columns.t b/test/feature.print.empty.columns.t index 4113ea583..64291e497 100755 --- a/test/feature.print.empty.columns.t +++ b/test/feature.print.empty.columns.t @@ -42,13 +42,13 @@ if (open my $fh, '>', 'bug.rc') } # Feature: variable to control printing of empty columns -qx{../src/task rc:bug.rc add sample desc 2>&1}; +qx{../src/task rc:bug.rc add /sample/ desc 2>&1}; qx{../src/task rc:bug.rc add withP project:house 2>&1}; -my $output = qx{../src/task test sample rc:bug.rc 2>&1}; +my $output = qx{../src/task test /sample/ rc:bug.rc 2>&1}; unlike ($output, qr/Project/, 'empty \'project\' column is not printed by default'); -$output = qx{../src/task test sample rc.print.empty.columns:yes rc:bug.rc 2>&1}; +$output = qx{../src/task test /sample/ rc.print.empty.columns:yes rc:bug.rc 2>&1}; like ($output, qr/Project/, 'empty \'project\' column is printed if rc.print.empty.columns:yes'); $output = qx{../src/task test rc:bug.rc 2>&1}; From 358223a6b192d3d336ee0a460a0fdbe408c52e5c Mon Sep 17 00:00:00 2001 From: Paul Beckingham Date: Sat, 27 Jun 2015 21:08:24 -0400 Subject: [PATCH 12/20] Filter: Corrected implementation of ::safety --- src/Filter.cpp | 23 ++++++++++------------- src/Filter.h | 2 +- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/Filter.cpp b/src/Filter.cpp index c2221f6a2..c64d64f11 100644 --- a/src/Filter.cpp +++ b/src/Filter.cpp @@ -182,7 +182,7 @@ void Filter::subset (std::vector & output, bool applyContext /* = true */) } else { - safety (precompiled.size ()); + safety (); context.timer_filter.stop (); for (auto& task : context.tdb2.pending.get_tasks ()) @@ -261,7 +261,7 @@ bool Filter::pendingOnly () //////////////////////////////////////////////////////////////////////////////// // Disaster avoidance mechanism. If a WRITECMD has no filter, then it can cause // all tasks to be modified. This is usually not intended. -void Filter::safety (unsigned int terms) +void Filter::safety () { for (auto& a : context.cli2._args) { @@ -269,19 +269,16 @@ void Filter::safety (unsigned int terms) { if (a.hasTag ("WRITECMD")) { - if (terms) - { - if (! context.config.getBoolean ("allow.empty.filter")) - throw std::string (STRING_TASK_SAFETY_ALLOW); + if (! context.config.getBoolean ("allow.empty.filter")) + throw std::string (STRING_TASK_SAFETY_ALLOW); - // If user is willing to be asked, this can be avoided. - if (context.config.getBoolean ("confirmation") && - confirm (STRING_TASK_SAFETY_VALVE)) - return; + // If user is willing to be asked, this can be avoided. + if (context.config.getBoolean ("confirmation") && + confirm (STRING_TASK_SAFETY_VALVE)) + return; - // Sounds the alarm. - throw std::string (STRING_TASK_SAFETY_FAIL); - } + // Sounds the alarm. + throw std::string (STRING_TASK_SAFETY_FAIL); } // CMD was found. diff --git a/src/Filter.h b/src/Filter.h index 194f40dc9..e2c4c3d44 100644 --- a/src/Filter.h +++ b/src/Filter.h @@ -43,7 +43,7 @@ public: void subset (const std::vector &, std::vector &, bool applyContext = true); void subset (std::vector &, bool applyContext = true); bool pendingOnly (); - void safety (unsigned int); + void safety (); private: int _startCount; From 4820bde41ec7f53426a3548327d8b75a663acfbd Mon Sep 17 00:00:00 2001 From: Paul Beckingham Date: Sat, 27 Jun 2015 23:08:56 -0400 Subject: [PATCH 13/20] CmdCalc: Eliminated temporary storage --- src/commands/CmdCalc.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/commands/CmdCalc.cpp b/src/commands/CmdCalc.cpp index f114ebf81..dd3680ceb 100644 --- a/src/commands/CmdCalc.cpp +++ b/src/commands/CmdCalc.cpp @@ -63,8 +63,7 @@ int CmdCalc::execute (std::string& output) // Compile all the args into one expression. std::string expression; - std::vector words = context.cli2.getWords (); - for (auto& word : words) + for (auto& word : context.cli2.getWords ()) expression += word + " "; // Evaluate according to preference. From 8cc75693a07fef7032a30e828b68a5fe62b5b3b5 Mon Sep 17 00:00:00 2001 From: Paul Beckingham Date: Sat, 27 Jun 2015 23:34:03 -0400 Subject: [PATCH 14/20] CLI2: Some args must avoid lexing - This now include rc:xxx and rc.xxx. --- src/CLI2.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/CLI2.cpp b/src/CLI2.cpp index 8676bd2ff..2f39ff1a7 100644 --- a/src/CLI2.cpp +++ b/src/CLI2.cpp @@ -343,15 +343,27 @@ void CLI2::lexArguments () bool terminated = false; for (unsigned int i = 1; i < _original_args.size (); ++i) { + // The terminator itself is captured. if (_original_args[i] == "--") { terminated = true; _args.push_back (A2 (_original_args[i], Lexer::Type::separator)); } + + // Any arguments that are after the terminator are captured as words. else if (terminated) { _args.push_back (A2 (_original_args[i], Lexer::Type::word)); } + + // rc: and rc.[:=] argumenst are captured whole. + else if (_original_args[i].substr (0, 3) == "rc:" || + _original_args[i].substr (0, 3) == "rc.") + { + _args.push_back (A2 (_original_args[i], Lexer::Type::pair)); + } + + // Everything else gets lexed. else { std::string lexeme; From 94b4f2bfba40c5604bf6e2cd0dbd739e8c9b5301 Mon Sep 17 00:00:00 2001 From: Paul Beckingham Date: Sat, 27 Jun 2015 23:42:24 -0400 Subject: [PATCH 15/20] Test: Corrected test --- test/caseless.t | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/caseless.t b/test/caseless.t index a6b0f7fd1..0d212836e 100755 --- a/test/caseless.t +++ b/test/caseless.t @@ -84,17 +84,17 @@ $output = qx{../src/task rc:caseless.rc info 1 2>&1}; like ($output, qr/four five six/, 'one two three\nfour FIVE six -> /five/five/ = caseless succeed'); # Description filter. -$output = qx{../src/task rc:caseless.rc rc.search.case.sensitive:yes ls One 2>&1}; -unlike ($output, qr/one two three/, 'one two three\nfour five six -> ls One = fail'); +$output = qx{../src/task rc:caseless.rc rc.search.case.sensitive:yes ls /One/ 2>&1}; +unlike ($output, qr/one two three/, 'one two three\nfour five six -> ls /One/ = fail'); -$output = qx{../src/task rc:caseless.rc rc.search.case.sensitive:no ls One 2>&1}; +$output = qx{../src/task rc:caseless.rc rc.search.case.sensitive:no ls /One/ 2>&1}; like ($output, qr/one two three/, 'one two three\nfour five six -> ls One caseless = succeed'); -$output = qx{../src/task rc:caseless.rc rc.search.case.sensitive:yes ls Five 2>&1}; -unlike ($output, qr/four five six/, 'one two three\nfour five six -> ls Five = fail'); +$output = qx{../src/task rc:caseless.rc rc.search.case.sensitive:yes ls /Five/ 2>&1}; +unlike ($output, qr/four five six/, 'one two three\nfour five six -> ls /Five/ = fail'); -$output = qx{../src/task rc:caseless.rc rc.search.case.sensitive:no ls Five 2>&1}; -like ($output, qr/four five six/, 'one two three\nfour five six -> ls Five caseless = succeed'); +$output = qx{../src/task rc:caseless.rc rc.search.case.sensitive:no ls /Five/ 2>&1}; +like ($output, qr/four five six/, 'one two three\nfour five six -> ls /Five/ caseless = succeed'); # Annotation filter. $output = qx{../src/task rc:caseless.rc rc.search.case.sensitive:yes ls description.contains:Three 2>&1}; From fd8e61607675f338830a9a867a6aeec7749c52b6 Mon Sep 17 00:00:00 2001 From: Paul Beckingham Date: Sat, 27 Jun 2015 23:47:32 -0400 Subject: [PATCH 16/20] Test: Corrected test --- test/tw-1469.t | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/tw-1469.t b/test/tw-1469.t index b158e0de4..e9dad0e67 100755 --- a/test/tw-1469.t +++ b/test/tw-1469.t @@ -45,7 +45,7 @@ class Test1469(TestCase): def test_implicit_search_sensitive_regex(self): """Implicit search, case sensitive, regex """ - code, out, err = self.t(('list', 'möbel', + code, out, err = self.t(('list', '/möbel/', 'rc.search.case.sensitive=yes', 'rc.regex=on')) self.assertEqual(0, code, "Exit code was non-zero ({0})".format(code)) @@ -54,7 +54,7 @@ class Test1469(TestCase): def test_implicit_search_sensitive_noregex(self): """Implicit search, case sensitive, no regex """ - code, out, err = self.t(('list', 'möbel', + code, out, err = self.t(('list', '/möbel/', 'rc.search.case.sensitive=yes', 'rc.regex=off')) self.assertEqual(0, code, "Exit code was non-zero ({0})".format(code)) @@ -64,7 +64,7 @@ class Test1469(TestCase): @unittest.skipIf('CYGWIN' in platform.system(), 'Skipping regex case-insensitive test for Cygwin') def test_implicit_search_insensitive_regex(self): """Implicit search, case insensitive, regex """ - code, out, err = self.t(('list', 'möbel', + code, out, err = self.t(('list', '/möbel/', 'rc.search.case.sensitive=no', 'rc.regex=on')) self.assertEqual(0, code, @@ -74,7 +74,7 @@ class Test1469(TestCase): def test_implicit_search_insensitive_noregex(self): """Implicit search, case insensitive, no regex """ - code, out, err = self.t(('list', 'möbel', + code, out, err = self.t(('list', '/möbel/', 'rc.search.case.sensitive=no', 'rc.regex=off')) self.assertEqual(0, code, "Exit code was non-zero ({0})".format(code)) From 239cf2d848d83fb2e359450ba79ce086874c5494 Mon Sep 17 00:00:00 2001 From: Paul Beckingham Date: Sat, 27 Jun 2015 23:54:45 -0400 Subject: [PATCH 17/20] Test: Corrected test --- test/filter.t | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/test/filter.t b/test/filter.t index 651f1c144..3ac29bba2 100755 --- a/test/filter.t +++ b/test/filter.t @@ -269,8 +269,8 @@ class TestFilter(TestCase): @unittest.expectedFailure def test_regex_list_project(self): - """filter - rc.regex:on list project:/[A-Z]/""" - code, out, err = self.t(("rc.regex:on", "list", "project:/[A-Z]/")) + """filter - rc.regex:on list project~[A-Z]""" + code, out, err = self.t(("rc.regex:on", "list", "project~[A-Z]")) self.assertIn("one", out) self.assertIn("two", out) @@ -280,10 +280,9 @@ class TestFilter(TestCase): self.assertNotIn("six", out) self.assertNotIn("seven", out) - @unittest.expectedFailure def test_regex_list_project_any(self): - """filter - rc.regex:on list project:.""" - code, out, err = self.t(("rc.regex:on", "list", "project:.")) + """filter - rc.regex:on list project~.""" + code, out, err = self.t(("rc.regex:on", "list", "project~.")) self.assertIn("one", out) self.assertIn("two", out) From 050aad49f2c3b048ef095f8ebc738042aa3765b0 Mon Sep 17 00:00:00 2001 From: Paul Beckingham Date: Sun, 28 Jun 2015 00:15:53 -0400 Subject: [PATCH 18/20] Test: Corrected test --- test/args.1.t | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/test/args.1.t b/test/args.1.t index 4c12269c7..91c370ea2 100755 --- a/test/args.1.t +++ b/test/args.1.t @@ -36,25 +36,29 @@ from basetest import Task, TestCase class TestIDPosition(TestCase): - def setUp(self): - """Executed before each test in the class""" - self.t = Task() + @classmethod + def setUpClass(cls): + """Executed once before any test in the class""" + cls.t = Task() - self.t(("add", "one")) - self.t(("add", "two")) - - def test_id(self): - """Test id before and after command""" - code, out, err = self.t(("list",)) + cls.t(("add", "one")) + cls.t(("add", "two")) + def test_id_read_cmd(self): + """Test id before and after read command""" + code, out, err = self.t(("1", "info")) self.assertIn("one", out) - self.assertIn("two", out) + self.assertNotIn("two", out) - code, out, err = self.t(("1", "done")) - self.assertIn("Completed 1 task.", out) + code, out, err = self.t(("info", "1")) + self.assertIn("one", out) + self.assertNotIn("two", out) + + def test_id_write_cmd(self): + """Test id before write command""" + code, out, err = self.t(("2", "done")) + self.assertIn("Completed task 2", out) - code, out, err = self.t(("done", "2")) - self.assertIn("Completed 1 task.", out) if __name__ == "__main__": from simpletap import TAPTestRunner From ba65fa67b1be47b3460f8a631215b29938c98327 Mon Sep 17 00:00:00 2001 From: Paul Beckingham Date: Sun, 28 Jun 2015 00:33:17 -0400 Subject: [PATCH 19/20] CLI2: ::aliasExpansion now properly observes and propagates TERMINATED args --- src/CLI2.cpp | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/CLI2.cpp b/src/CLI2.cpp index 2f39ff1a7..1e765017d 100644 --- a/src/CLI2.cpp +++ b/src/CLI2.cpp @@ -353,7 +353,9 @@ void CLI2::lexArguments () // Any arguments that are after the terminator are captured as words. else if (terminated) { - _args.push_back (A2 (_original_args[i], Lexer::Type::word)); + A2 word (_original_args[i], Lexer::Type::word); + word.tag ("TERMINATED"); + _args.push_back (word); } // rc: and rc.[:=] argumenst are captured whole. @@ -740,13 +742,15 @@ void CLI2::aliasExpansion () for (auto& i : _args) { raw = i.attribute ("raw"); - if (_aliases.find (raw) != _aliases.end ()) + if (i.hasTag ("TERMINATED")) + { + reconstructed.push_back (i); + } + else if (_aliases.find (raw) != _aliases.end ()) { for (auto& l : Lexer::split (_aliases[raw])) { A2 a (l, Lexer::Type::word); - a.tag ("ALIAS"); - a.tag ("LEX"); reconstructed.push_back (a); } @@ -754,15 +758,25 @@ void CLI2::aliasExpansion () changes = true; } else + { reconstructed.push_back (i); + } } _args = reconstructed; std::vector reconstructedOriginals; + bool terminated = false; for (auto& i : _original_args) { - if (_aliases.find (i) != _aliases.end ()) + if (i == "--") + terminated = true; + + if (terminated) + { + reconstructedOriginals.push_back (i); + } + else if (_aliases.find (i) != _aliases.end ()) { for (auto& l : Lexer::split (_aliases[i])) reconstructedOriginals.push_back (l); @@ -771,7 +785,9 @@ void CLI2::aliasExpansion () changes = true; } else + { reconstructedOriginals.push_back (i); + } } _original_args = reconstructedOriginals; From 86ed23234875bc32c6dab9b8f4c26f370e881538 Mon Sep 17 00:00:00 2001 From: Paul Beckingham Date: Sun, 28 Jun 2015 12:35:06 -0400 Subject: [PATCH 20/20] Lexer: Added ::wasQuoted to determine original quote state --- src/Lexer.cpp | 9 +++++++++ src/Lexer.h | 25 +++++++++++++------------ test/lexer.t.cpp | 10 ++++++++-- 3 files changed, 30 insertions(+), 14 deletions(-) diff --git a/src/Lexer.cpp b/src/Lexer.cpp index 2642d56c9..856218ecc 100644 --- a/src/Lexer.cpp +++ b/src/Lexer.cpp @@ -324,6 +324,15 @@ void Lexer::dequote (std::string& input) } } +//////////////////////////////////////////////////////////////////////////////// +bool Lexer::wasQuoted (const std::string& input) +{ + if (input.find_first_of (" \t()") != std::string::npos) + return true; + + return false; +} + //////////////////////////////////////////////////////////////////////////////// bool Lexer::isEOS () const { diff --git a/src/Lexer.h b/src/Lexer.h index 2bacdd727..bb83f461b 100644 --- a/src/Lexer.h +++ b/src/Lexer.h @@ -63,18 +63,19 @@ public: // Static helpers. static const std::string typeName (const Lexer::Type&); - static bool isWhitespace (int); - static bool isAlpha (int); - static bool isDigit (int); - static bool isHexDigit (int); - static bool isIdentifierStart (int); - static bool isIdentifierNext (int); - static bool isSingleCharOperator (int); - static bool isDoubleCharOperator (int, int, int); - static bool isTripleCharOperator (int, int, int, int); - static bool isBoundary (int, int); - static bool isPunctuation (int); - static void dequote (std::string&); + static bool isWhitespace (int); + static bool isAlpha (int); + static bool isDigit (int); + static bool isHexDigit (int); + static bool isIdentifierStart (int); + static bool isIdentifierNext (int); + static bool isSingleCharOperator (int); + static bool isDoubleCharOperator (int, int, int); + static bool isTripleCharOperator (int, int, int, int); + static bool isBoundary (int, int); + static bool isPunctuation (int); + static void dequote (std::string&); + static bool wasQuoted (const std::string&); // Helpers. bool isEOS () const; diff --git a/test/lexer.t.cpp b/test/lexer.t.cpp index 0e873c7f8..698cb5840 100644 --- a/test/lexer.t.cpp +++ b/test/lexer.t.cpp @@ -36,7 +36,7 @@ Context context; //////////////////////////////////////////////////////////////////////////////// int main (int argc, char** argv) { - UnitTest t (795); + UnitTest t (799); std::vector > tokens; std::string token; @@ -71,7 +71,7 @@ int main (int argc, char** argv) t.ok (Lexer::isWhitespace (0x205F), "U+205F isWhitespace"); t.ok (Lexer::isWhitespace (0x3000), "U+3000 isWhitespace"); - // static bool Lexer::isBoundary(int, int); + // static bool Lexer::isBoundary (int, int); t.ok (Lexer::isBoundary (' ', 'a'), "' ' --> 'a' = isBoundary"); t.ok (Lexer::isBoundary ('a', ' '), "'a' --> ' ' = isBoundary"); t.ok (Lexer::isBoundary (' ', '+'), "' ' --> '+' = isBoundary"); @@ -80,6 +80,12 @@ int main (int argc, char** argv) t.ok (Lexer::isBoundary ('(', '('), "'(' --> '(' = isBoundary"); t.notok (Lexer::isBoundary ('r', 'd'), "'r' --> 'd' = isBoundary"); + // static bool Lexer::wasQuoted (const std::string&); + t.notok (Lexer::wasQuoted (""), "'' --> !wasQuoted"); + t.notok (Lexer::wasQuoted ("foo"), "'foo' --> !wasQuoted"); + t.ok (Lexer::wasQuoted ("a b"), "'a b' --> wasQuoted"); + t.ok (Lexer::wasQuoted ("(a)"), "'(a)' --> wasQuoted"); + // Should result in no tokens. Lexer l0 (""); t.notok (l0.token (token, type), "'' --> no tokens");