Skip to content

Commit 692e68f

Browse files
author
Ryosuke Yabuki
authored
Merge pull request #1008 from launchableinc/timestamp-option
To support for importing historical data
2 parents 7cfad1a + b9acf19 commit 692e68f

File tree

8 files changed

+173
-14
lines changed

8 files changed

+173
-14
lines changed

launchable/commands/helper.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import datetime
12
from time import time
23
from typing import Optional, Sequence, Tuple
34

@@ -64,6 +65,7 @@ def find_or_create_session(
6465
is_no_build: bool = False,
6566
lineage: Optional[str] = None,
6667
test_suite: Optional[str] = None,
68+
timestamp: Optional[datetime.datetime] = None,
6769
) -> Optional[str]:
6870
"""Determine the test session ID to be used.
6971
@@ -134,6 +136,7 @@ def find_or_create_session(
134136
is_no_build=is_no_build,
135137
lineage=lineage,
136138
test_suite=test_suite,
139+
timestamp=timestamp,
137140
)
138141
return read_session(saved_build_name)
139142

launchable/commands/record/build.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import datetime
12
import os
23
import re
34
import sys
@@ -11,7 +12,7 @@
1112

1213
from ...utils import subprocess
1314
from ...utils.authentication import get_org_workspace
14-
from ...utils.click import KEY_VALUE
15+
from ...utils.click import DATETIME_WITH_TZ, KEY_VALUE
1516
from ...utils.launchable_client import LaunchableClient
1617
from ...utils.session import clean_session_files, write_build
1718
from .commit import commit
@@ -96,13 +97,20 @@
9697
'lineage',
9798
hidden=True,
9899
)
100+
@click.option(
101+
'--timestamp',
102+
'timestamp',
103+
help='Used to overwrite the build time when importing historical data. Note: Format must be `YYYY-MM-DDThh:mm:ssTZD` or `YYYY-MM-DDThh:mm:ss` (local timezone applied)', # noqa: E501
104+
type=DATETIME_WITH_TZ,
105+
default=None,
106+
)
99107
@click.pass_context
100108
def build(
101109
ctx: click.core.Context, build_name: str, source: List[str],
102110
max_days: int, no_submodules: bool, no_commit_collection: bool, scrub_pii: bool,
103111
commits: Sequence[Tuple[str, str]],
104112
links: Sequence[Tuple[str, str]],
105-
branches: Sequence[str], lineage: str):
113+
branches: Sequence[str], lineage: str, timestamp: Optional[datetime.datetime]):
106114

107115
if "/" in build_name or "%2f" in build_name.lower():
108116
sys.exit("--name must not contain a slash and an encoded slash")
@@ -326,7 +334,8 @@ def compute_links():
326334
'commitHash': w.commit_hash,
327335
'branchName': w.branch or ""
328336
} for w in ws],
329-
"links": compute_links()
337+
"links": compute_links(),
338+
"timestamp": timestamp.isoformat() if timestamp else None,
330339
}
331340

332341
res = client.request("post", "builds", payload=payload)

launchable/commands/record/session.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import datetime
12
import os
23
import re
34
import sys
@@ -6,6 +7,7 @@
67

78
import click
89

10+
from launchable.utils.click import DATETIME_WITH_TZ
911
from launchable.utils.link import LinkKind, capture_link
1012
from launchable.utils.tracking import Tracking, TrackingClient
1113

@@ -98,6 +100,13 @@ def _validate_session_name(ctx, param, value):
98100
type=str,
99101
metavar='TEST_SUITE',
100102
)
103+
@click.option(
104+
'--timestamp',
105+
'timestamp',
106+
help='Used to overwrite the session time when importing historical data. Note: Format must be `YYYY-MM-DDThh:mm:ssTZD` or `YYYY-MM-DDThh:mm:ss` (local timezone applied)', # noqa: E501
107+
type=DATETIME_WITH_TZ,
108+
default=None,
109+
)
101110
@click.pass_context
102111
def session(
103112
ctx: click.core.Context,
@@ -111,6 +120,7 @@ def session(
111120
session_name: Optional[str] = None,
112121
lineage: Optional[str] = None,
113122
test_suite: Optional[str] = None,
123+
timestamp: Optional[datetime.datetime] = None,
114124
):
115125
"""
116126
print_session is for backward compatibility.
@@ -166,6 +176,7 @@ def session(
166176
"noBuild": is_no_build,
167177
"lineage": lineage,
168178
"testSuite": test_suite,
179+
"timestamp": timestamp.isoformat() if timestamp else None,
169180
}
170181

171182
_links = capture_link(os.environ)

launchable/commands/record/tests.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from launchable.utils.tracking import Tracking, TrackingClient
1717

1818
from ...testpath import FilePathNormalizer, TestPathComponent, unparse_test_path
19-
from ...utils.click import KEY_VALUE
19+
from ...utils.click import DATETIME_WITH_TZ, KEY_VALUE
2020
from ...utils.exceptions import InvalidJUnitXMLException
2121
from ...utils.launchable_client import LaunchableClient
2222
from ...utils.logger import Logger
@@ -159,6 +159,13 @@ def _validate_group(ctx, param, value):
159159
type=str,
160160
metavar='TEST_SUITE',
161161
)
162+
@click.option(
163+
'--timestamp',
164+
'timestamp',
165+
help='Used to overwrite the test executed times when importing historical data. Note: Format must be `YYYY-MM-DDThh:mm:ssTZD` or `YYYY-MM-DDThh:mm:ss` (local timezone applied)', # noqa: E501
166+
type=DATETIME_WITH_TZ,
167+
default=None,
168+
)
162169
@click.pass_context
163170
def tests(
164171
context: click.core.Context,
@@ -177,6 +184,7 @@ def tests(
177184
session_name: Optional[str] = None,
178185
lineage: Optional[str] = None,
179186
test_suite: Optional[str] = None,
187+
timestamp: Optional[datetime.datetime] = None,
180188
):
181189
logger = Logger()
182190

@@ -236,6 +244,7 @@ def tests(
236244
links=links,
237245
lineage=lineage,
238246
test_suite=test_suite,
247+
timestamp=timestamp,
239248
tracking_client=tracking_client))
240249
build_name = read_build()
241250
record_start_at = get_record_start_at(session_id, client)
@@ -404,8 +413,13 @@ def report(self, junit_report_file: str):
404413
ctime = datetime.datetime.fromtimestamp(
405414
os.path.getctime(junit_report_file))
406415

407-
if not self.is_allow_test_before_build and not self.is_no_build and (
408-
self.check_timestamp and ctime.timestamp() < record_start_at.timestamp()):
416+
if (
417+
not self.is_allow_test_before_build # nlqa: W503
418+
and not self.is_no_build # noqa: W503
419+
and timestamp is None # noqa: W503
420+
and self.check_timestamp # noqa: W503
421+
and ctime.timestamp() < record_start_at.timestamp() # noqa: W503
422+
):
409423
format = "%Y-%m-%d %H:%M:%S"
410424
logger.warning("skip: {} is too old to report. start_record_at: {} file_created_at: {}".format(
411425
junit_report_file, record_start_at.strftime(format), ctime.strftime(format)))
@@ -437,6 +451,10 @@ def testcases(reports: List[str]) -> Generator[CaseEventType, None, None]:
437451
if len(tc.get('testPath', [])) == 0:
438452
continue
439453

454+
# Set specific time for importing historical data
455+
if timestamp is not None:
456+
tc["createdAt"] = timestamp.isoformat()
457+
440458
yield tc
441459

442460
except Exception as e:

launchable/utils/click.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
from typing import Dict, Optional, Tuple
44

55
import click
6+
import dateutil.parser
67
from click import ParamType
8+
from dateutil.tz import tzlocal
79

810
# click.Group has the notion of hidden commands but it doesn't allow us to easily add
911
# the same command under multiple names and hide all but one.
@@ -94,10 +96,25 @@ def convert(self, value: str, param: Optional[click.core.Parameter], ctx: Option
9496
self.fail("Expected fraction like 1/2 but got '{}'".format(value), param, ctx)
9597

9698

99+
class DateTimeWithTimezoneType(ParamType):
100+
name = "datetime"
101+
102+
def convert(self, value: str, param: Optional[click.core.Parameter], ctx: Optional[click.core.Context]):
103+
104+
try:
105+
dt = dateutil.parser.parse(value)
106+
if dt.tzinfo is None:
107+
return dt.replace(tzinfo=tzlocal())
108+
return dt
109+
except ValueError:
110+
self.fail("Expected datetime like 2023-10-01T12:00:00 but got '{}'".format(value), param, ctx)
111+
112+
97113
PERCENTAGE = PercentageType()
98114
DURATION = DurationType()
99115
FRACTION = FractionType()
100116
KEY_VALUE = KeyValueType()
117+
DATETIME_WITH_TZ = DateTimeWithTimezoneType()
101118

102119
# Can the output deal with Unicode emojis?
103120
try:

tests/commands/record/test_build.py

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ def test_submodule(self, mock_check_output):
5959
"branchName": ""
6060
},
6161
],
62-
"links": []
62+
"links": [],
63+
"timestamp": None
6364
}, payload)
6465

6566
self.assertEqual(read_build(), self.build_name)
@@ -93,7 +94,8 @@ def test_no_submodule(self, mock_check_output):
9394
"branchName": ""
9495
},
9596
],
96-
"links": []
97+
"links": [],
98+
"timestamp": None
9799
}, payload)
98100

99101
self.assertEqual(read_build(), self.build_name)
@@ -124,7 +126,8 @@ def test_no_git_directory(self):
124126
"branchName": "",
125127
},
126128
],
127-
"links": []
129+
"links": [],
130+
"timestamp": None
128131
}, payload)
129132

130133
self.assertEqual(read_build(), self.build_name)
@@ -153,7 +156,8 @@ def test_commit_option_and_build_option(self):
153156
"branchName": ""
154157
},
155158
],
156-
"links": []
159+
"links": [],
160+
'timestamp': None
157161
}, payload)
158162
responses.calls.reset()
159163

@@ -182,7 +186,8 @@ def test_commit_option_and_build_option(self):
182186
"branchName": "feature-xxx"
183187
},
184188
],
185-
"links": []
189+
"links": [],
190+
"timestamp": None
186191
}, payload)
187192
responses.calls.reset()
188193

@@ -211,7 +216,8 @@ def test_commit_option_and_build_option(self):
211216
"branchName": ""
212217
},
213218
],
214-
"links": []
219+
"links": [],
220+
"timestamp": None
215221
}, payload)
216222
responses.calls.reset()
217223
self.assertIn("Invalid repository name B in a --branch option. ", result.output)
@@ -250,7 +256,8 @@ def test_commit_option_and_build_option(self):
250256
"branchName": "feature-yyy"
251257
},
252258
],
253-
"links": []
259+
"links": [],
260+
"timestamp": None
254261
}, payload)
255262
responses.calls.reset()
256263

@@ -260,3 +267,42 @@ def test_build_name_validation(self):
260267

261268
result = self.cli("record", "build", "--no-commit-collection", "--name", "foo%2Fhoge")
262269
self.assert_exit_code(result, 1)
270+
271+
# make sure the output of git-submodule is properly parsed
272+
@responses.activate
273+
@mock.patch.dict(os.environ, {"LAUNCHABLE_TOKEN": CliTestCase.launchable_token})
274+
# to tests on GitHub Actions
275+
@mock.patch.dict(os.environ, {"GITHUB_ACTIONS": ""})
276+
@mock.patch.dict(os.environ, {"GITHUB_PULL_REQUEST_URL": ""})
277+
@mock.patch('launchable.utils.subprocess.check_output')
278+
def test_with_timestamp(self, mock_check_output):
279+
self.assertEqual(read_build(), None)
280+
result = self.cli(
281+
"record",
282+
"build",
283+
"--no-commit-collection",
284+
"--commit",
285+
"repo=abc12",
286+
"--name",
287+
self.build_name,
288+
'--timestamp',
289+
"2025-01-23 12:34:56Z")
290+
self.assert_success(result)
291+
292+
payload = json.loads(responses.calls[0].request.body.decode())
293+
self.assert_json_orderless_equal(
294+
{
295+
"buildNumber": "123",
296+
"lineage": None,
297+
"commitHashes": [
298+
{
299+
"repositoryName": "repo",
300+
"commitHash": "abc12",
301+
"branchName": ""
302+
},
303+
],
304+
"links": [],
305+
"timestamp": "2025-01-23T12:34:56+00:00"
306+
}, payload)
307+
308+
self.assertEqual(read_build(), self.build_name)

tests/commands/record/test_session.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ def test_run_session_without_flavor(self):
3434
"noBuild": False,
3535
"lineage": None,
3636
"testSuite": None,
37+
"timestamp": None,
3738
}, payload)
3839

3940
@responses.activate
@@ -58,6 +59,7 @@ def test_run_session_with_flavor(self):
5859
"noBuild": False,
5960
"lineage": None,
6061
"testSuite": None,
62+
"timestamp": None,
6163
}, payload)
6264

6365
result = self.cli("record", "session", "--build", self.build_name, "--flavor", "only-key")
@@ -82,6 +84,7 @@ def test_run_session_with_observation(self):
8284
"noBuild": False,
8385
"lineage": None,
8486
"testSuite": None,
87+
"timestamp": None,
8588
}, payload)
8689

8790
@responses.activate
@@ -120,6 +123,7 @@ def test_run_session_with_session_name(self):
120123
"noBuild": False,
121124
"lineage": None,
122125
"testSuite": None,
126+
"timestamp": None,
123127
}, payload)
124128

125129
@responses.activate
@@ -140,6 +144,7 @@ def test_run_session_with_lineage(self):
140144
"noBuild": False,
141145
"lineage": "example-lineage",
142146
"testSuite": None,
147+
"timestamp": None,
143148
}, payload)
144149

145150
@responses.activate
@@ -160,4 +165,26 @@ def test_run_session_with_test_suite(self):
160165
"noBuild": False,
161166
"lineage": None,
162167
"testSuite": "example-test-suite",
168+
"timestamp": None,
169+
}, payload)
170+
171+
@responses.activate
172+
@mock.patch.dict(os.environ, {
173+
"LAUNCHABLE_TOKEN": CliTestCase.launchable_token,
174+
'LANG': 'C.UTF-8',
175+
}, clear=True)
176+
def test_run_session_with_timestamp(self):
177+
result = self.cli("record", "session", "--build", self.build_name,
178+
"--timestamp", "2023-10-01T12:00:00Z")
179+
self.assert_success(result)
180+
181+
payload = json.loads(responses.calls[0].request.body.decode())
182+
self.assert_json_orderless_equal({
183+
"flavors": {},
184+
"isObservation": False,
185+
"links": [],
186+
"noBuild": False,
187+
"lineage": None,
188+
"testSuite": None,
189+
"timestamp": "2023-10-01T12:00:00+00:00",
163190
}, payload)

0 commit comments

Comments
 (0)