timewarrior/test/export.t
Scott Mcdermott bf26176243 Allow specification of intervals to the export command
This patch checks if intervals are given on cli to 'timew export', and
if so will filter only those numbered IDs out from the db.  This lets
the user that already knows the interval(s) they want to know about, to
ask for only those, without parsing the whole thing (similar to how we
can do this for taskwarrior IDs).

If both intervals and other filters -- time range or tags -- are given,
this is considered an error.  There would seem to be little use to AND
or OR tags/ranges with IDs because anyone that knew IDs to request would
already know those IDs met their requirement.

Fixes #510

Code additions from @lauft PR notes (thanks!):

- factor out 'filtering' so we can do only one call to getTracked()
- simplify (tag || range) to .empty(), which already checks both
- error message phrasing

Signed-off-by: Scott Mcdermott <scott@smemsh.net>
2022-12-28 22:13:35 +01:00

231 lines
10 KiB
Python
Executable file

#!/usr/bin/env python3
###############################################################################
#
# Copyright 2016 - 2021, Thomas Lauf, 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.
#
# https://www.opensource.org/licenses/mit-license.php
#
###############################################################################
import os
import unittest
from datetime import datetime, timedelta
import sys
# Ensure python finds the local simpletap module
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from basetest import Timew, TestCase
class TestExport(TestCase):
def setUp(self):
"""Executed before each test in the class"""
self.t = Timew()
def test_trivial_export(self):
"""Test trivial export"""
code, out, err = self.t("export")
self.assertIn("[\n]\n", out)
def test_fixed_id_export(self):
"""Give specific IDs on CLI"""
self.t("track 2022-12-10T00:00:00Z - 2022-12-10T01:00:00Z")
self.t("track 2022-12-10T01:00:00Z - 2022-12-10T02:00:00Z")
self.t("track 2022-12-10T02:00:00Z - 2022-12-10T03:00:00Z")
self.t("track 2022-12-10T04:00:00Z - 2022-12-10T05:00:00Z")
j = self.t.export("@1 @4")
self.assertEqual(len(j), 2)
self.assertClosedInterval(j[0], expectedStart="20221210T000000Z", expectedId=4)
self.assertClosedInterval(j[1], expectedStart="20221210T040000Z", expectedId=1)
def test_single_unobstructed_interval(self):
"""Single unobstructed interval"""
now_utc = datetime.now().utcnow()
one_hour_before_utc = now_utc - timedelta(hours=1)
self.t("track {:%Y-%m-%dT%H:%M:%S}Z - {:%Y-%m-%dT%H:%M:%S}Z foo".format(one_hour_before_utc, now_utc))
j = self.t.export()
self.assertEqual(len(j), 1)
self.assertClosedInterval(j[0],
expectedStart=one_hour_before_utc,
expectedEnd=now_utc,
expectedTags=["foo"])
def test_changing_exclusion_does_not_change_flattened_intervals(self):
"""Changing exclusions does not change flattened intervals"""
now = datetime.now()
now_utc = now.utcnow()
two_hours_before = now - timedelta(hours=2)
three_hours_before = now - timedelta(hours=3)
four_hours_before = now - timedelta(hours=4)
one_hour_before_utc = now_utc - timedelta(hours=1)
three_hours_before_utc = now_utc - timedelta(hours=3)
four_hours_before_utc = now_utc - timedelta(hours=4)
five_hours_before_utc = now_utc - timedelta(hours=5)
self.t.configure_exclusions((four_hours_before.time(), three_hours_before.time()))
self.t("track {:%Y-%m-%dT%H:%M:%S}Z - {:%Y-%m-%dT%H:%M:%S}Z foo".format(five_hours_before_utc, one_hour_before_utc))
j = self.t.export()
self.assertEqual(len(j), 2)
self.assertClosedInterval(j[0],
description="interval before exclusion (before change)",
expectedStart="{:%Y%m%dT%H%M%S}Z".format(five_hours_before_utc),
expectedEnd="{:%Y%m%dT%H%M%S}Z".format(four_hours_before_utc),
expectedTags=["foo"])
self.assertClosedInterval(j[1],
description="interval after exclusion (before change)",
expectedStart="{:%Y%m%dT%H%M%S}Z".format(three_hours_before_utc),
expectedEnd="{:%Y%m%dT%H%M%S}Z".format(one_hour_before_utc),
expectedTags=["foo"])
self.t.configure_exclusions((three_hours_before.time(), two_hours_before.time()))
j = self.t.export()
self.assertEqual(len(j), 2)
self.assertClosedInterval(j[0],
description="interval before exclusion (after change)",
expectedStart="{:%Y%m%dT%H%M%S}Z".format(five_hours_before_utc),
expectedEnd="{:%Y%m%dT%H%M%S}Z".format(four_hours_before_utc),
expectedTags=["foo"])
self.assertClosedInterval(j[1],
description="interval after exclusion (after change)",
expectedStart="{:%Y%m%dT%H%M%S}Z".format(three_hours_before_utc),
expectedEnd="{:%Y%m%dT%H%M%S}Z".format(one_hour_before_utc),
expectedTags=["foo"])
def test_changing_exclusion_does_change_open_interval(self):
"""Changing exclusions does change open interval"""
now = datetime.now()
now_utc = now.utcnow()
two_hours_before = now - timedelta(hours=2)
three_hours_before = now - timedelta(hours=3)
four_hours_before = now - timedelta(hours=4)
two_hours_before_utc = now_utc - timedelta(hours=2)
three_hours_before_utc = now_utc - timedelta(hours=3)
four_hours_before_utc = now_utc - timedelta(hours=4)
five_hours_before_utc = now_utc - timedelta(hours=5)
self.t.configure_exclusions((four_hours_before.time(), three_hours_before.time()))
self.t("start {:%Y-%m-%dT%H:%M:%S}Z foo".format(five_hours_before_utc))
j = self.t.export()
self.assertEqual(len(j), 2)
self.assertClosedInterval(j[0],
description="interval before exclusion (before change)",
expectedStart="{:%Y%m%dT%H%M%S}Z".format(five_hours_before_utc),
expectedEnd="{:%Y%m%dT%H%M%S}Z".format(four_hours_before_utc),
expectedTags=["foo"])
self.assertOpenInterval(j[1],
description="interval after exclusion (before change)",
expectedStart="{:%Y%m%dT%H%M%S}Z".format(three_hours_before_utc),
expectedTags=["foo"])
self.t.configure_exclusions((three_hours_before.time(), two_hours_before.time()))
j = self.t.export()
self.assertEqual(len(j), 2)
self.assertClosedInterval(j[0],
description="interval before exclusion (after change)",
expectedStart="{:%Y%m%dT%H%M%S}Z".format(five_hours_before_utc),
expectedEnd="{:%Y%m%dT%H%M%S}Z".format(three_hours_before_utc),
expectedTags=["foo"])
self.assertOpenInterval(j[1],
description="interval after exclusion (after change)",
expectedStart="{:%Y%m%dT%H%M%S}Z".format(two_hours_before_utc),
expectedTags=["foo"])
def test_export_with_tag_with_spaces(self):
"""Interval with tag with spaces"""
now_utc = datetime.now().utcnow()
one_hour_before_utc = now_utc - timedelta(hours=1)
self.t("track {:%Y-%m-%dT%H:%M:%S}Z - {:%Y-%m-%dT%H:%M:%S}Z \"tag with spaces\"".format(one_hour_before_utc, now_utc))
j = self.t.export()
self.assertEqual(len(j), 1)
self.assertClosedInterval(j[0], expectedTags=["tag with spaces"])
def test_export_with_tag_with_quote(self):
"""Interval with tag with quote"""
now_utc = datetime.now().utcnow()
one_hour_before_utc = now_utc - timedelta(hours=1)
self.t("track {:%Y-%m-%dT%H:%M:%S}Z - {:%Y-%m-%dT%H:%M:%S}Z \"tag with \\\"quote\"".format(one_hour_before_utc, now_utc))
j = self.t.export()
self.assertEqual(len(j), 1)
self.assertClosedInterval(j[0], expectedTags=["tag with \"quote"])
def test_non_contiguous_with_tag_filter(self):
"""Export with tag filter"""
self.t("track Tag1 2017-03-09T08:43:08 - 2017-03-09T09:38:15")
self.t("track Tag2 2017-03-09T11:38:39 - 2017-03-09T11:45:35")
self.t("track Tag1 Tag3 2017-03-09T11:46:21 - 2017-03-09T12:00:17")
self.t("track Tag2 Tag4 2017-03-09T12:01:49 - 2017-03-09T12:28:46")
j = self.t.export("Tag1")
self.assertEqual(len(j), 2)
self.assertClosedInterval(j[0],
expectedId=4,
expectedTags=["Tag1"])
self.assertClosedInterval(j[1],
expectedId=2,
expectedTags=["Tag1", "Tag3"])
def test_export_with_intersecting_filter(self):
"""Export with filter that is contained by interval"""
self.t("track Tag1 2021-02-01T00:00:00 - 2021-03-01T00:00:00")
self.t("track Tag2 2021-03-01T00:00:00 - 2021-04-01T00:00:00")
# Pass a filter to export that is contained within the above intervals
# and check that it picks up the containing interval
j = self.t.export("2021-02-02 - 2021-02-03")
self.assertEqual(len(j), 1)
self.assertClosedInterval(j[0],
expectedId=2,
expectedTags=["Tag1"])
if __name__ == "__main__":
from simpletap import TAPTestRunner
unittest.main(testRunner=TAPTestRunner())