testit-cli 2.10.1.post580__tar.gz → 2.10.2.post580__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. {testit_cli-2.10.1.post580/src/testit_cli.egg-info → testit_cli-2.10.2.post580}/PKG-INFO +82 -1
  2. testit_cli-2.10.2.post580/README.md +113 -0
  3. {testit_cli-2.10.1.post580 → testit_cli-2.10.2.post580}/setup.py +1 -1
  4. {testit_cli-2.10.1.post580 → testit_cli-2.10.2.post580}/src/testit_cli/apiclient.py +20 -2
  5. {testit_cli-2.10.1.post580 → testit_cli-2.10.2.post580}/src/testit_cli/click_commands.py +27 -3
  6. {testit_cli-2.10.1.post580 → testit_cli-2.10.2.post580}/src/testit_cli/converter.py +4 -2
  7. {testit_cli-2.10.1.post580 → testit_cli-2.10.2.post580}/src/testit_cli/models/config.py +4 -0
  8. {testit_cli-2.10.1.post580 → testit_cli-2.10.2.post580}/src/testit_cli/models/testrun.py +2 -1
  9. testit_cli-2.10.2.post580/src/testit_cli/models/testrun_link.py +10 -0
  10. {testit_cli-2.10.1.post580 → testit_cli-2.10.2.post580}/src/testit_cli/service.py +27 -1
  11. testit_cli-2.10.2.post580/src/testit_cli/testrun_metadata.py +131 -0
  12. {testit_cli-2.10.1.post580 → testit_cli-2.10.2.post580/src/testit_cli.egg-info}/PKG-INFO +82 -1
  13. {testit_cli-2.10.1.post580 → testit_cli-2.10.2.post580}/src/testit_cli.egg-info/SOURCES.txt +4 -1
  14. testit_cli-2.10.2.post580/tests/test_testrun_metadata.py +151 -0
  15. testit_cli-2.10.1.post580/README.md +0 -32
  16. {testit_cli-2.10.1.post580 → testit_cli-2.10.2.post580}/LICENSE +0 -0
  17. {testit_cli-2.10.1.post580 → testit_cli-2.10.2.post580}/setup.cfg +0 -0
  18. {testit_cli-2.10.1.post580 → testit_cli-2.10.2.post580}/src/testit_cli/__init__.py +0 -0
  19. {testit_cli-2.10.1.post580 → testit_cli-2.10.2.post580}/src/testit_cli/__main__.py +0 -0
  20. {testit_cli-2.10.1.post580 → testit_cli-2.10.2.post580}/src/testit_cli/autotests_filter.py +0 -0
  21. {testit_cli-2.10.1.post580 → testit_cli-2.10.2.post580}/src/testit_cli/dir_worker.py +0 -0
  22. {testit_cli-2.10.1.post580 → testit_cli-2.10.2.post580}/src/testit_cli/file_worker.py +0 -0
  23. {testit_cli-2.10.1.post580 → testit_cli-2.10.2.post580}/src/testit_cli/filter_factory.py +0 -0
  24. {testit_cli-2.10.1.post580 → testit_cli-2.10.2.post580}/src/testit_cli/html_escape_utils.py +0 -0
  25. {testit_cli-2.10.1.post580 → testit_cli-2.10.2.post580}/src/testit_cli/http_retry.py +0 -0
  26. {testit_cli-2.10.1.post580 → testit_cli-2.10.2.post580}/src/testit_cli/importer.py +0 -0
  27. {testit_cli-2.10.1.post580 → testit_cli-2.10.2.post580}/src/testit_cli/logger.py +0 -0
  28. {testit_cli-2.10.1.post580 → testit_cli-2.10.2.post580}/src/testit_cli/models/__init__.py +0 -0
  29. {testit_cli-2.10.1.post580 → testit_cli-2.10.2.post580}/src/testit_cli/models/mode.py +0 -0
  30. {testit_cli-2.10.1.post580 → testit_cli-2.10.2.post580}/src/testit_cli/models/status_type.py +0 -0
  31. {testit_cli-2.10.1.post580 → testit_cli-2.10.2.post580}/src/testit_cli/models/testcase.py +0 -0
  32. {testit_cli-2.10.1.post580 → testit_cli-2.10.2.post580}/src/testit_cli/parser.py +0 -0
  33. {testit_cli-2.10.1.post580 → testit_cli-2.10.2.post580}/src/testit_cli/service_factory.py +0 -0
  34. {testit_cli-2.10.1.post580 → testit_cli-2.10.2.post580}/src/testit_cli/validation.py +0 -0
  35. {testit_cli-2.10.1.post580 → testit_cli-2.10.2.post580}/src/testit_cli.egg-info/dependency_links.txt +0 -0
  36. {testit_cli-2.10.1.post580 → testit_cli-2.10.2.post580}/src/testit_cli.egg-info/entry_points.txt +0 -0
  37. {testit_cli-2.10.1.post580 → testit_cli-2.10.2.post580}/src/testit_cli.egg-info/requires.txt +0 -0
  38. {testit_cli-2.10.1.post580 → testit_cli-2.10.2.post580}/src/testit_cli.egg-info/top_level.txt +0 -0
  39. {testit_cli-2.10.1.post580 → testit_cli-2.10.2.post580}/tests/test_click.py +0 -0
  40. {testit_cli-2.10.1.post580 → testit_cli-2.10.2.post580}/tests/test_service.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: testit-cli
3
- Version: 2.10.1.post580
3
+ Version: 2.10.2.post580
4
4
  Summary: This tool is the command line wrapper of Test IT allowing you to upload the test results in real time to Test IT
5
5
  Home-page: https://pypi.org/project/testit-cli/
6
6
  Author: Integration team
@@ -63,3 +63,84 @@ You can see more information in [official documentation](https://docs.testit.sof
63
63
  1. For current versions, see the releases tab.
64
64
  2. Starting with 5.2, we have added a TMS postscript, which means that the utility is compatible with a specific enterprise version.
65
65
  3. If you are in doubt about which version to use, check with the support staff. support@yoonion.ru
66
+
67
+ ## Test run tags and links
68
+
69
+ You can attach **test run** tags and links (for example a CI job URL) via CLI flags or environment variables.
70
+ They are applied when the test run is **created**, or merged into an **existing** run as early as possible (not only after results upload finishes).
71
+
72
+ | Intent | CLI | Env |
73
+ |--------|-----|-----|
74
+ | Tags | `--testruntags` / `-trt` | `TMS_TEST_RUN_TAGS` |
75
+ | Links | `--testrunlinks` / `-trl` | `TMS_TEST_RUN_LINKS` |
76
+
77
+ Supported commands: `testrun create`, `results import`, `results upload`.
78
+
79
+ ### Tags format
80
+
81
+ Comma-separated list or JSON array:
82
+
83
+ ```bash
84
+ --testruntags "smoke,nightly"
85
+ --testruntags '["smoke","nightly"]'
86
+ ```
87
+
88
+ ### Links format
89
+
90
+ JSON array of objects. `url` is required; `title`, `description`, and `type` are optional.
91
+
92
+ **Link types for a test run:**
93
+
94
+ - `Related`
95
+ - `BlockedBy`
96
+ - `Defect`
97
+ - `Issue`
98
+ - `Requirement`
99
+ - `Repository`
100
+
101
+ If `type` is omitted, `Related` is used.
102
+
103
+ ```bash
104
+ --testrunlinks '[{"url":"https://gitlab.example.com/group/project/-/jobs/12345","title":"CI Job","type":"Related"}]'
105
+ ```
106
+
107
+ ### Examples
108
+
109
+ Create a test run with tags and a CI job link (visible while the run is still In progress):
110
+
111
+ ```bash
112
+ testit testrun create \
113
+ --url "$TMS_URL" \
114
+ --token "$TMS_TOKEN" \
115
+ --project-id "$TMS_PROJECT_ID" \
116
+ --testruntags "smoke,ci" \
117
+ --testrunlinks "[{\"url\":\"$CI_JOB_URL\",\"title\":\"CI Job\",\"type\":\"Related\"}]" \
118
+ --output ./testrun.id
119
+ ```
120
+
121
+ Same via env vars:
122
+
123
+ ```bash
124
+ export TMS_TEST_RUN_TAGS='smoke,ci'
125
+ export TMS_TEST_RUN_LINKS="[{\"url\":\"$CI_JOB_URL\",\"title\":\"CI Job\",\"type\":\"Related\"}]"
126
+ testit testrun create -u "$TMS_URL" -t "$TMS_TOKEN" -pi "$TMS_PROJECT_ID" -o ./testrun.id
127
+ ```
128
+
129
+ Merge tags/links into an existing run when uploading results:
130
+
131
+ ```bash
132
+ testit results upload \
133
+ --url "$TMS_URL" \
134
+ --token "$TMS_TOKEN" \
135
+ --configuration-id "$TMS_CONFIGURATION_ID" \
136
+ --testrun-id "$TMS_TEST_RUN_ID" \
137
+ --testruntags "nightly" \
138
+ --testrunlinks "[{\"url\":\"$CI_JOB_URL\",\"title\":\"CI Job\",\"type\":\"Related\"}]" \
139
+ --results ./results
140
+ ```
141
+
142
+ Notes:
143
+
144
+ - Test run tags/links are independent from autotest/result tags and links.
145
+ - On an existing run, new tags/links are **merged** (existing values are kept; duplicates by tag name / link URL are skipped).
146
+ - Empty or omitted values mean “do not change tags/links”.
@@ -0,0 +1,113 @@
1
+ # Test IT CLI
2
+
3
+ [![Release
4
+ Status](https://img.shields.io/pypi/v/testit-cli?style=plastic)](https://pypi.python.org/pypi/testit-cli)
5
+ [![Downloads](https://img.shields.io/pypi/dm/testit-cli?style=plastic)](https://pypi.python.org/pypi/testit-cli)
6
+
7
+ This tool is the command line wrapper of Test IT allowing you to upload the test results in real time to Test IT.
8
+ You can see more information in [official documentation](https://docs.testit.software/user-guide/integrations/cli.html)
9
+
10
+ ## Compatibility
11
+
12
+ | Test IT | Test IT CLI |
13
+ |---------|----------------|
14
+ | 3.5+ | 0.1 |
15
+ | 4.4 | 1.0 |
16
+ | 4.5 | 1.2 |
17
+ | 4.6 | 1.5 |
18
+ | 5.0 | 2.0 |
19
+ | 5.2 | 2.1 |
20
+ | 5.2.2 | 2.2.1.post522 |
21
+ | 5.2.3 | 2.3.0.post523 |
22
+ | 5.3 | 2.4.5.post530 |
23
+ | 5.4 | 2.4.11.post540 |
24
+ | 5.5 | 2.7.2.post550 |
25
+ | 5.6 | 2.8.1.post560 |
26
+ | 5.7 | 2.9.1.post570 |
27
+ | 5.8 | 2.10.0.post580 |
28
+ | Cloud | 2.8.0 + |
29
+
30
+ 1. For current versions, see the releases tab.
31
+ 2. Starting with 5.2, we have added a TMS postscript, which means that the utility is compatible with a specific enterprise version.
32
+ 3. If you are in doubt about which version to use, check with the support staff. support@yoonion.ru
33
+
34
+ ## Test run tags and links
35
+
36
+ You can attach **test run** tags and links (for example a CI job URL) via CLI flags or environment variables.
37
+ They are applied when the test run is **created**, or merged into an **existing** run as early as possible (not only after results upload finishes).
38
+
39
+ | Intent | CLI | Env |
40
+ |--------|-----|-----|
41
+ | Tags | `--testruntags` / `-trt` | `TMS_TEST_RUN_TAGS` |
42
+ | Links | `--testrunlinks` / `-trl` | `TMS_TEST_RUN_LINKS` |
43
+
44
+ Supported commands: `testrun create`, `results import`, `results upload`.
45
+
46
+ ### Tags format
47
+
48
+ Comma-separated list or JSON array:
49
+
50
+ ```bash
51
+ --testruntags "smoke,nightly"
52
+ --testruntags '["smoke","nightly"]'
53
+ ```
54
+
55
+ ### Links format
56
+
57
+ JSON array of objects. `url` is required; `title`, `description`, and `type` are optional.
58
+
59
+ **Link types for a test run:**
60
+
61
+ - `Related`
62
+ - `BlockedBy`
63
+ - `Defect`
64
+ - `Issue`
65
+ - `Requirement`
66
+ - `Repository`
67
+
68
+ If `type` is omitted, `Related` is used.
69
+
70
+ ```bash
71
+ --testrunlinks '[{"url":"https://gitlab.example.com/group/project/-/jobs/12345","title":"CI Job","type":"Related"}]'
72
+ ```
73
+
74
+ ### Examples
75
+
76
+ Create a test run with tags and a CI job link (visible while the run is still In progress):
77
+
78
+ ```bash
79
+ testit testrun create \
80
+ --url "$TMS_URL" \
81
+ --token "$TMS_TOKEN" \
82
+ --project-id "$TMS_PROJECT_ID" \
83
+ --testruntags "smoke,ci" \
84
+ --testrunlinks "[{\"url\":\"$CI_JOB_URL\",\"title\":\"CI Job\",\"type\":\"Related\"}]" \
85
+ --output ./testrun.id
86
+ ```
87
+
88
+ Same via env vars:
89
+
90
+ ```bash
91
+ export TMS_TEST_RUN_TAGS='smoke,ci'
92
+ export TMS_TEST_RUN_LINKS="[{\"url\":\"$CI_JOB_URL\",\"title\":\"CI Job\",\"type\":\"Related\"}]"
93
+ testit testrun create -u "$TMS_URL" -t "$TMS_TOKEN" -pi "$TMS_PROJECT_ID" -o ./testrun.id
94
+ ```
95
+
96
+ Merge tags/links into an existing run when uploading results:
97
+
98
+ ```bash
99
+ testit results upload \
100
+ --url "$TMS_URL" \
101
+ --token "$TMS_TOKEN" \
102
+ --configuration-id "$TMS_CONFIGURATION_ID" \
103
+ --testrun-id "$TMS_TEST_RUN_ID" \
104
+ --testruntags "nightly" \
105
+ --testrunlinks "[{\"url\":\"$CI_JOB_URL\",\"title\":\"CI Job\",\"type\":\"Related\"}]" \
106
+ --results ./results
107
+ ```
108
+
109
+ Notes:
110
+
111
+ - Test run tags/links are independent from autotest/result tags and links.
112
+ - On an existing run, new tags/links are **merged** (existing values are kept; duplicates by tag name / link URL are skipped).
113
+ - Empty or omitted values mean “do not change tags/links”.
@@ -1,6 +1,6 @@
1
1
  from setuptools import find_packages, setup
2
2
 
3
- VERSION = "2.10.1.post580"
3
+ VERSION = "2.10.2.post580"
4
4
 
5
5
  setup(
6
6
  name='testit-cli',
@@ -55,9 +55,20 @@ class ApiClient:
55
55
  self.__projects_api = ProjectsApi(api_client=client)
56
56
  self.__workflows_api = WorkflowsApi(api_client=client)
57
57
 
58
- def create_test_run(self, project_id: str, name: str) -> TestRun:
58
+ def create_test_run(
59
+ self,
60
+ project_id: str,
61
+ name: str,
62
+ tags: typing.Optional[typing.List[str]] = None,
63
+ links: typing.Optional[typing.List] = None,
64
+ ) -> TestRun:
59
65
  """Function creates test run and returns test run id."""
60
- model = CreateEmptyRequest(project_id=project_id, name=name)
66
+ create_kwargs = {"project_id": project_id, "name": name}
67
+ if tags:
68
+ create_kwargs["tags"] = tags
69
+ if links:
70
+ create_kwargs["links"] = links
71
+ model = CreateEmptyRequest(**create_kwargs)
61
72
  model = HtmlEscapeUtils.escape_html_in_object(model)
62
73
  logging.debug(f"Creating test run with model: {model}")
63
74
 
@@ -67,6 +78,13 @@ class ApiClient:
67
78
  )
68
79
 
69
80
  logging.info(f'Created new testrun (ID: {test_run.id})')
81
+ if tags or links:
82
+ logging.info(
83
+ "Applied tags/links on create for testrun (ID: %s): tags=%s links=%s",
84
+ test_run.id,
85
+ tags,
86
+ links,
87
+ )
70
88
  logging.debug(f"Test run created: {test_run}")
71
89
 
72
90
  return Converter.test_run_v2_get_model_to_test_run(test_run)
@@ -4,6 +4,7 @@ from itertools import chain
4
4
 
5
5
  from .models.config import Config
6
6
  from .service_factory import ServiceFactory
7
+ from .testrun_metadata import parse_testrun_links, parse_testrun_tags
7
8
  from .validation import validate_uuid, validate_url
8
9
 
9
10
 
@@ -61,11 +62,24 @@ class ExtraArgsForOption(click.Option):
61
62
  return retval
62
63
 
63
64
 
65
+ _TESTRUN_TAGS_HELP = (
66
+ "Test run tags: comma-separated or JSON array "
67
+ "(smoke,nightly or [\"smoke\",\"nightly\"])"
68
+ )
69
+ _TESTRUN_LINKS_HELP = (
70
+ "Test run links as JSON array "
71
+ '([{"url":"https://ci.example/jobs/1","title":"CI Job","type":"Related"}]). '
72
+ "Types: Related, BlockedBy, Defect, Issue, Requirement, Repository"
73
+ )
74
+
75
+
64
76
  @results.command("upload")
65
77
  @click.option("-u", "--url", type=str, envvar='TMS_URL', required=True, help="Set url address of the Test IT instance (https://demo.testit.software)", callback=validate_url)
66
78
  @click.option("-t", "--token", type=str, envvar='TMS_TOKEN', required=True, help="Set API token (T2lKd2pLZGI4WHRhaVZUejNl)")
67
79
  @click.option("-ci", "--configuration-id", type=str, envvar='TMS_CONFIGURATION_ID', required=True, help="Set configuration id (15dbb164-c1aa-4cbf-830c-8c01ae14f4fb)", callback=validate_uuid)
68
80
  @click.option("-ti", "--testrun-id", type=str, envvar='TMS_TEST_RUN_ID', required=True, help="Set test run id (3802f329-190c-4617-8bb0-2c3696abeb8f)", callback=validate_uuid)
81
+ @click.option("-trt", "--testruntags", type=str, envvar='TMS_TEST_RUN_TAGS', default=None, help=_TESTRUN_TAGS_HELP)
82
+ @click.option("-trl", "--testrunlinks", type=str, envvar='TMS_TEST_RUN_LINKS', default=None, help=_TESTRUN_LINKS_HELP)
69
83
  @click.option("-s", "--separator", type=str, help="Separate the classname value in the results into namespace and classname (.)", default=None)
70
84
  @click.option("-ns", "--namespace", type=str, help="Set namespace (NameSpace01)", default=None)
71
85
  @click.option("-cn", "--classname", type=str, help="Set classname (ClassName01)", default=None)
@@ -74,13 +88,15 @@ class ExtraArgsForOption(click.Option):
74
88
  @click.option("-d", "--debug", is_flag=True, help="Set debug logs")
75
89
  @click.option("-dcv", "--disable-cert-validation", is_flag=True, help="Disables certificate validation")
76
90
  @click.option("-iff", "--ignore-flaky-failure", is_flag=True, help="Ignore status flakyFailure in results")
77
- def upload_results(url, token, configuration_id, testrun_id, separator, namespace, classname, results, debug, attachments, disable_cert_validation, ignore_flaky_failure):
91
+ def upload_results(url, token, configuration_id, testrun_id, testruntags, testrunlinks, separator, namespace, classname, results, debug, attachments, disable_cert_validation, ignore_flaky_failure):
78
92
  """Uploading results from different streams"""
79
93
  config = Config(
80
94
  url=url,
81
95
  token=token,
82
96
  configuration_id=configuration_id,
83
97
  testrun_id=testrun_id,
98
+ testrun_tags=parse_testrun_tags(testruntags),
99
+ testrun_links=parse_testrun_links(testrunlinks),
84
100
  separator=separator,
85
101
  namespace=namespace,
86
102
  classname=classname,
@@ -102,6 +118,8 @@ def upload_results(url, token, configuration_id, testrun_id, separator, namespac
102
118
  @click.option("-ci", "--configuration-id", type=str, envvar='TMS_CONFIGURATION_ID', required=True, help="Set configuration id (15dbb164-c1aa-4cbf-830c-8c01ae14f4fb)", callback=validate_uuid)
103
119
  @click.option("-ti", "--testrun-id", type=str, envvar='TMS_TEST_RUN_ID', help="Set test run id (3802f329-190c-4617-8bb0-2c3696abeb8f)", default=None, callback=validate_uuid)
104
120
  @click.option("-tn", "--testrun-name", type=str, envvar='TMS_TEST_RUN_NAME', help="Set test run name (TestRun01)", default=None)
121
+ @click.option("-trt", "--testruntags", type=str, envvar='TMS_TEST_RUN_TAGS', default=None, help=_TESTRUN_TAGS_HELP)
122
+ @click.option("-trl", "--testrunlinks", type=str, envvar='TMS_TEST_RUN_LINKS', default=None, help=_TESTRUN_LINKS_HELP)
105
123
  @click.option("-s", "--separator", type=str, help="Separate the classname value in the results into namespace and classname (.)", default=None)
106
124
  @click.option("-ns", "--namespace", type=str, help="Set namespace (NameSpace01)", default=None)
107
125
  @click.option("-cn", "--classname", type=str, help="Set classname (ClassName01)", default=None)
@@ -110,7 +128,7 @@ def upload_results(url, token, configuration_id, testrun_id, separator, namespac
110
128
  @click.option("-d", "--debug", is_flag=True, help="Set debug logs")
111
129
  @click.option("-dcv", "--disable-cert-validation", is_flag=True, help="Disables certificate validation")
112
130
  @click.option("-iff", "--ignore-flaky-failure", is_flag=True, help="Ignore status flakyFailure in results")
113
- def import_results(url, token, project_id, configuration_id, testrun_id, testrun_name, separator, namespace, classname, results, debug,
131
+ def import_results(url, token, project_id, configuration_id, testrun_id, testrun_name, testruntags, testrunlinks, separator, namespace, classname, results, debug,
114
132
  attachments, disable_cert_validation, ignore_flaky_failure):
115
133
  """Uploading the first test results"""
116
134
  if testrun_id is not None and testrun_name is not None:
@@ -123,6 +141,8 @@ def import_results(url, token, project_id, configuration_id, testrun_id, testrun
123
141
  configuration_id=configuration_id,
124
142
  testrun_id=testrun_id,
125
143
  testrun_name=testrun_name,
144
+ testrun_tags=parse_testrun_tags(testruntags),
145
+ testrun_links=parse_testrun_links(testrunlinks),
126
146
  separator=separator,
127
147
  namespace=namespace,
128
148
  classname=classname,
@@ -148,17 +168,21 @@ def testrun():
148
168
  @click.option("-t", "--token", type=str, envvar='TMS_TOKEN', required=True, help="Set API token (T2lKd2pLZGI4WHRhaVZUejNl)")
149
169
  @click.option("-pi", "--project-id", type=str, envvar='TMS_PROJECT_ID', required=True, help="Set project id (5236eb3f-7c05-46f9-a609-dc0278896464)", callback=validate_uuid)
150
170
  @click.option("-tn", "--testrun-name", type=str, envvar='TMS_TEST_RUN_NAME', help="Set test run name (TestRun01)", default=None)
171
+ @click.option("-trt", "--testruntags", type=str, envvar='TMS_TEST_RUN_TAGS', default=None, help=_TESTRUN_TAGS_HELP)
172
+ @click.option("-trl", "--testrunlinks", type=str, envvar='TMS_TEST_RUN_LINKS', default=None, help=_TESTRUN_LINKS_HELP)
151
173
  @click.option("-o", "--output", type=str, required=True, help="Set file path for output (FILE)")
152
174
  @click.option("-a", "--attachments", multiple=True, type=list, help="Path to attachments for test run (multiple)", default=[], cls=ExtraArgsForOption)
153
175
  @click.option("-d", "--debug", is_flag=True, help="Set debug logs")
154
176
  @click.option("-dcv", "--disable-cert-validation", is_flag=True, help="Disables certificate validation")
155
- def create_test_run(url, token, project_id, testrun_name, output, debug, attachments, disable_cert_validation):
177
+ def create_test_run(url, token, project_id, testrun_name, testruntags, testrunlinks, output, debug, attachments, disable_cert_validation):
156
178
  """Creating a new test run"""
157
179
  config = Config(
158
180
  url=url,
159
181
  token=token,
160
182
  project_id=project_id,
161
183
  testrun_name=testrun_name,
184
+ testrun_tags=parse_testrun_tags(testruntags),
185
+ testrun_links=parse_testrun_links(testrunlinks),
162
186
  is_debug=debug,
163
187
  output=output,
164
188
  paths_to_attachments=list(chain.from_iterable(attachments)),
@@ -137,7 +137,8 @@ class Converter:
137
137
  description=test_run_model.description,
138
138
  launch_source=test_run_model.launch_source,
139
139
  attachments=cls.attachment_models_to_attachment_put_models(test_run_model.attachments),
140
- links=cls.link_models_to_link_put_models(test_run_model.links)
140
+ links=cls.link_models_to_link_put_models(test_run_model.links),
141
+ tags=list(test_run_model.tags) if test_run_model.tags else [],
141
142
  )
142
143
 
143
144
  @classmethod
@@ -199,5 +200,6 @@ class Converter:
199
200
  description=test_run.description,
200
201
  launch_source=test_run.launch_source,
201
202
  attachments=test_run.attachments,
202
- links=test_run.links
203
+ links=test_run.links,
204
+ tags=test_run.tags,
203
205
  )
@@ -1,6 +1,8 @@
1
1
  from dataclasses import dataclass, field
2
2
  import typing
3
3
 
4
+ from .testrun_link import TestRunLink
5
+
4
6
 
5
7
  @dataclass
6
8
  class Config:
@@ -15,6 +17,8 @@ class Config:
15
17
  configuration_ids: typing.Optional[list[str]] = None
16
18
  testrun_id: typing.Optional[str] = None
17
19
  testrun_name: typing.Optional[str] = None
20
+ testrun_tags: typing.Optional[list[str]] = None
21
+ testrun_links: typing.Optional[list[TestRunLink]] = None
18
22
  separator: typing.Optional[str] = None
19
23
  namespace: typing.Optional[str] = None
20
24
  classname: typing.Optional[str] = None
@@ -1,4 +1,4 @@
1
- from dataclasses import dataclass
1
+ from dataclasses import dataclass, field
2
2
  from typing import List
3
3
  from testit_api_client.model.assign_attachment_api_model import AssignAttachmentApiModel
4
4
  from testit_api_client.model.link_put_model import LinkPutModel
@@ -14,3 +14,4 @@ class TestRun:
14
14
  launch_source: str
15
15
  attachments: List[AssignAttachmentApiModel]
16
16
  links: List[LinkPutModel]
17
+ tags: List[str] = field(default_factory=list)
@@ -0,0 +1,10 @@
1
+ from dataclasses import dataclass
2
+ from typing import Optional
3
+
4
+
5
+ @dataclass
6
+ class TestRunLink:
7
+ url: str
8
+ title: Optional[str] = None
9
+ description: Optional[str] = None
10
+ link_type: Optional[str] = None
@@ -13,6 +13,7 @@ from .apiclient import ApiClient
13
13
  from .dir_worker import DirWorker
14
14
  from .file_worker import FileWorker
15
15
  from .importer import Importer
16
+ from .testrun_metadata import merge_links, merge_tags, to_create_link_models, to_link_put_models
16
17
 
17
18
 
18
19
  class Service:
@@ -72,9 +73,32 @@ class Service:
72
73
  def __create_test_run(self) -> TestRun:
73
74
  return self.__api_client.create_test_run(
74
75
  self.__config.project_id,
75
- self.__config.testrun_name
76
+ self.__config.testrun_name,
77
+ tags=self.__config.testrun_tags,
78
+ links=to_create_link_models(self.__config.testrun_links),
76
79
  )
77
80
 
81
+ def __merge_configured_metadata(self, test_run: TestRun) -> None:
82
+ test_run.tags = merge_tags(test_run.tags, self.__config.testrun_tags)
83
+ test_run.links = merge_links(
84
+ test_run.links,
85
+ to_link_put_models(self.__config.testrun_links),
86
+ )
87
+
88
+ def __apply_testrun_tags_and_links(self, test_run: TestRun) -> TestRun:
89
+ if not self.__config.testrun_tags and not self.__config.testrun_links:
90
+ return test_run
91
+
92
+ self.__merge_configured_metadata(test_run)
93
+ self.__api_client.update_test_run(test_run)
94
+ logging.info(
95
+ "Merged tags/links into testrun (ID: %s): tags=%s links=%s",
96
+ test_run.id,
97
+ self.__config.testrun_tags,
98
+ self.__config.testrun_links,
99
+ )
100
+ return test_run
101
+
78
102
  def __upload_attachments(self) -> list[AttachmentPutModel]:
79
103
  files = []
80
104
 
@@ -94,6 +118,7 @@ class Service:
94
118
  else:
95
119
  test_run = self.__api_client.get_test_run(self.__config.testrun_id)
96
120
  self.__config.project_id = test_run.project_id
121
+ test_run = self.__apply_testrun_tags_and_links(test_run)
97
122
 
98
123
  logging.info("Sending test results to Test IT ...")
99
124
 
@@ -104,6 +129,7 @@ class Service:
104
129
  logging.info("Successfully sent test results")
105
130
 
106
131
  def __update_test_run_with_attachments(self, test_run: TestRun) -> None:
132
+ self.__merge_configured_metadata(test_run)
107
133
  attachments: typing.List[AssignAttachmentApiModel] = Converter.attachment_put_models_to_assign_attachments(
108
134
  self.__upload_attachments())
109
135
  test_run.attachments.extend(attachments)
@@ -0,0 +1,131 @@
1
+ """Parse and merge test run tags/links (config → API)."""
2
+ import json
3
+ import logging
4
+ from typing import List, Optional
5
+
6
+ from testit_api_client.model.create_link_api_model import CreateLinkApiModel
7
+ from testit_api_client.model.link_put_model import LinkPutModel
8
+ from testit_api_client.model.link_type import LinkType
9
+
10
+ from .models.testrun_link import TestRunLink
11
+
12
+ LINK_TYPES = ("Related", "BlockedBy", "Defect", "Issue", "Requirement", "Repository")
13
+
14
+
15
+ def parse_testrun_tags(raw: Optional[str]) -> Optional[List[str]]:
16
+ if raw is None or not str(raw).strip():
17
+ return None
18
+ text = str(raw).strip()
19
+ if text.startswith("["):
20
+ try:
21
+ data = json.loads(text)
22
+ except json.JSONDecodeError as exc:
23
+ logging.warning("Invalid test run tags JSON: %s", exc)
24
+ return None
25
+ if not isinstance(data, list):
26
+ logging.warning("Test run tags JSON must be an array of strings")
27
+ return None
28
+ tags = [str(item).strip() for item in data if str(item).strip()]
29
+ return tags or None
30
+ tags = [part.strip() for part in text.split(",") if part.strip()]
31
+ return tags or None
32
+
33
+
34
+ def parse_testrun_links(raw: Optional[str]) -> Optional[List[TestRunLink]]:
35
+ if raw is None or not str(raw).strip():
36
+ return None
37
+ try:
38
+ data = json.loads(str(raw).strip())
39
+ except json.JSONDecodeError as exc:
40
+ logging.warning("Invalid test run links JSON: %s", exc)
41
+ return None
42
+ if not isinstance(data, list):
43
+ logging.warning("Test run links JSON must be an array of objects")
44
+ return None
45
+ links = []
46
+ for item in data:
47
+ if not isinstance(item, dict):
48
+ logging.warning("Skipping invalid test run link entry: %s", item)
49
+ continue
50
+ url = item.get("url")
51
+ if not url or not str(url).strip():
52
+ logging.warning("Skipping test run link without url: %s", item)
53
+ continue
54
+ link_type = item.get("type")
55
+ if link_type is not None and str(link_type) not in LINK_TYPES:
56
+ logging.warning(
57
+ "Unknown link type %r; allowed: %s. Using Related.",
58
+ link_type,
59
+ ", ".join(LINK_TYPES),
60
+ )
61
+ link_type = "Related"
62
+ links.append(
63
+ TestRunLink(
64
+ url=str(url).strip(),
65
+ title=item.get("title"),
66
+ description=item.get("description"),
67
+ link_type=str(link_type) if link_type is not None else None,
68
+ )
69
+ )
70
+ return links or None
71
+
72
+
73
+ def _to_link_type(value: Optional[str]) -> LinkType:
74
+ name = value or "Related"
75
+ try:
76
+ return LinkType(name)
77
+ except Exception:
78
+ return LinkType("Related")
79
+
80
+
81
+ def to_create_link_models(links: Optional[List[TestRunLink]]) -> Optional[List[CreateLinkApiModel]]:
82
+ if not links:
83
+ return None
84
+ return [
85
+ CreateLinkApiModel(
86
+ url=link.url,
87
+ title=link.title,
88
+ description=link.description,
89
+ type=_to_link_type(link.link_type),
90
+ has_info=False,
91
+ )
92
+ for link in links
93
+ ]
94
+
95
+
96
+ def to_link_put_models(links: Optional[List[TestRunLink]]) -> List[LinkPutModel]:
97
+ if not links:
98
+ return []
99
+ return [
100
+ LinkPutModel(
101
+ url=link.url,
102
+ title=link.title,
103
+ description=link.description,
104
+ type=_to_link_type(link.link_type),
105
+ has_info=False,
106
+ )
107
+ for link in links
108
+ ]
109
+
110
+
111
+ def merge_tags(existing: Optional[List[str]], incoming: Optional[List[str]]) -> List[str]:
112
+ result = list(existing or [])
113
+ seen = set(result)
114
+ for tag in incoming or []:
115
+ if tag not in seen:
116
+ result.append(tag)
117
+ seen.add(tag)
118
+ return result
119
+
120
+
121
+ def merge_links(
122
+ existing: Optional[List[LinkPutModel]],
123
+ incoming: Optional[List[LinkPutModel]],
124
+ ) -> List[LinkPutModel]:
125
+ result = list(existing or [])
126
+ seen = {link.url for link in result}
127
+ for link in incoming or []:
128
+ if link.url not in seen:
129
+ result.append(link)
130
+ seen.add(link.url)
131
+ return result
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: testit-cli
3
- Version: 2.10.1.post580
3
+ Version: 2.10.2.post580
4
4
  Summary: This tool is the command line wrapper of Test IT allowing you to upload the test results in real time to Test IT
5
5
  Home-page: https://pypi.org/project/testit-cli/
6
6
  Author: Integration team
@@ -63,3 +63,84 @@ You can see more information in [official documentation](https://docs.testit.sof
63
63
  1. For current versions, see the releases tab.
64
64
  2. Starting with 5.2, we have added a TMS postscript, which means that the utility is compatible with a specific enterprise version.
65
65
  3. If you are in doubt about which version to use, check with the support staff. support@yoonion.ru
66
+
67
+ ## Test run tags and links
68
+
69
+ You can attach **test run** tags and links (for example a CI job URL) via CLI flags or environment variables.
70
+ They are applied when the test run is **created**, or merged into an **existing** run as early as possible (not only after results upload finishes).
71
+
72
+ | Intent | CLI | Env |
73
+ |--------|-----|-----|
74
+ | Tags | `--testruntags` / `-trt` | `TMS_TEST_RUN_TAGS` |
75
+ | Links | `--testrunlinks` / `-trl` | `TMS_TEST_RUN_LINKS` |
76
+
77
+ Supported commands: `testrun create`, `results import`, `results upload`.
78
+
79
+ ### Tags format
80
+
81
+ Comma-separated list or JSON array:
82
+
83
+ ```bash
84
+ --testruntags "smoke,nightly"
85
+ --testruntags '["smoke","nightly"]'
86
+ ```
87
+
88
+ ### Links format
89
+
90
+ JSON array of objects. `url` is required; `title`, `description`, and `type` are optional.
91
+
92
+ **Link types for a test run:**
93
+
94
+ - `Related`
95
+ - `BlockedBy`
96
+ - `Defect`
97
+ - `Issue`
98
+ - `Requirement`
99
+ - `Repository`
100
+
101
+ If `type` is omitted, `Related` is used.
102
+
103
+ ```bash
104
+ --testrunlinks '[{"url":"https://gitlab.example.com/group/project/-/jobs/12345","title":"CI Job","type":"Related"}]'
105
+ ```
106
+
107
+ ### Examples
108
+
109
+ Create a test run with tags and a CI job link (visible while the run is still In progress):
110
+
111
+ ```bash
112
+ testit testrun create \
113
+ --url "$TMS_URL" \
114
+ --token "$TMS_TOKEN" \
115
+ --project-id "$TMS_PROJECT_ID" \
116
+ --testruntags "smoke,ci" \
117
+ --testrunlinks "[{\"url\":\"$CI_JOB_URL\",\"title\":\"CI Job\",\"type\":\"Related\"}]" \
118
+ --output ./testrun.id
119
+ ```
120
+
121
+ Same via env vars:
122
+
123
+ ```bash
124
+ export TMS_TEST_RUN_TAGS='smoke,ci'
125
+ export TMS_TEST_RUN_LINKS="[{\"url\":\"$CI_JOB_URL\",\"title\":\"CI Job\",\"type\":\"Related\"}]"
126
+ testit testrun create -u "$TMS_URL" -t "$TMS_TOKEN" -pi "$TMS_PROJECT_ID" -o ./testrun.id
127
+ ```
128
+
129
+ Merge tags/links into an existing run when uploading results:
130
+
131
+ ```bash
132
+ testit results upload \
133
+ --url "$TMS_URL" \
134
+ --token "$TMS_TOKEN" \
135
+ --configuration-id "$TMS_CONFIGURATION_ID" \
136
+ --testrun-id "$TMS_TEST_RUN_ID" \
137
+ --testruntags "nightly" \
138
+ --testrunlinks "[{\"url\":\"$CI_JOB_URL\",\"title\":\"CI Job\",\"type\":\"Related\"}]" \
139
+ --results ./results
140
+ ```
141
+
142
+ Notes:
143
+
144
+ - Test run tags/links are independent from autotest/result tags and links.
145
+ - On an existing run, new tags/links are **merged** (existing values are kept; duplicates by tag name / link URL are skipped).
146
+ - Empty or omitted values mean “do not change tags/links”.
@@ -17,6 +17,7 @@ src/testit_cli/logger.py
17
17
  src/testit_cli/parser.py
18
18
  src/testit_cli/service.py
19
19
  src/testit_cli/service_factory.py
20
+ src/testit_cli/testrun_metadata.py
20
21
  src/testit_cli/validation.py
21
22
  src/testit_cli.egg-info/PKG-INFO
22
23
  src/testit_cli.egg-info/SOURCES.txt
@@ -30,5 +31,7 @@ src/testit_cli/models/mode.py
30
31
  src/testit_cli/models/status_type.py
31
32
  src/testit_cli/models/testcase.py
32
33
  src/testit_cli/models/testrun.py
34
+ src/testit_cli/models/testrun_link.py
33
35
  tests/test_click.py
34
- tests/test_service.py
36
+ tests/test_service.py
37
+ tests/test_testrun_metadata.py
@@ -0,0 +1,151 @@
1
+ from unittest.mock import Mock
2
+
3
+ import pytest
4
+ from testit_api_client.model.link_put_model import LinkPutModel
5
+ from testit_api_client.model.link_type import LinkType
6
+
7
+ from src.testit_cli.models.testrun import TestRun
8
+ from src.testit_cli.models.testrun_link import TestRunLink
9
+ from src.testit_cli.service import Service
10
+ from src.testit_cli.testrun_metadata import (
11
+ merge_links,
12
+ merge_tags,
13
+ parse_testrun_links,
14
+ parse_testrun_tags,
15
+ to_create_link_models,
16
+ )
17
+
18
+
19
+ @pytest.mark.parametrize(
20
+ "raw, expected",
21
+ [
22
+ (None, None),
23
+ ("", None),
24
+ (" ", None),
25
+ ("smoke, nightly", ["smoke", "nightly"]),
26
+ ('["smoke", "nightly"]', ["smoke", "nightly"]),
27
+ ("smoke", ["smoke"]),
28
+ ],
29
+ )
30
+ def test_parse_testrun_tags(raw, expected):
31
+ assert parse_testrun_tags(raw) == expected
32
+
33
+
34
+ def test_parse_testrun_tags_invalid_json_array():
35
+ assert parse_testrun_tags("[") is None
36
+
37
+
38
+ def test_parse_testrun_links_ok():
39
+ raw = (
40
+ '[{"url":"https://ci.example/jobs/1","title":"CI Job","type":"Related"},'
41
+ '{"url":"https://ci.example/jobs/2"}]'
42
+ )
43
+ links = parse_testrun_links(raw)
44
+ assert links == [
45
+ TestRunLink(
46
+ url="https://ci.example/jobs/1",
47
+ title="CI Job",
48
+ description=None,
49
+ link_type="Related",
50
+ ),
51
+ TestRunLink(
52
+ url="https://ci.example/jobs/2",
53
+ title=None,
54
+ description=None,
55
+ link_type=None,
56
+ ),
57
+ ]
58
+
59
+
60
+ @pytest.mark.parametrize("raw", [None, "", "not-json", "{}", '[{"title":"x"}]'])
61
+ def test_parse_testrun_links_invalid(raw):
62
+ assert parse_testrun_links(raw) is None
63
+
64
+
65
+ def test_merge_tags_and_links_dedupe():
66
+ assert merge_tags(["a"], ["a", "b"]) == ["a", "b"]
67
+ related = LinkType("Related")
68
+ existing = [LinkPutModel(url="https://a", type=related, has_info=False)]
69
+ incoming = [
70
+ LinkPutModel(url="https://a", type=related, has_info=False),
71
+ LinkPutModel(url="https://b", type=related, has_info=False),
72
+ ]
73
+ merged = merge_links(existing, incoming)
74
+ assert [link.url for link in merged] == ["https://a", "https://b"]
75
+
76
+
77
+ def test_to_create_link_models_defaults_type():
78
+ models = to_create_link_models([TestRunLink(url="https://ci.example/1")])
79
+ assert models is not None
80
+ assert len(models) == 1
81
+ assert models[0].url == "https://ci.example/1"
82
+ assert str(models[0].type) == "Related"
83
+
84
+
85
+ def test_create_test_run_passes_tags_and_links():
86
+ api_client = Mock()
87
+ api_client.create_test_run.return_value = TestRun(
88
+ id="run-1",
89
+ project_id="proj-1",
90
+ state="InProgress",
91
+ name="Run",
92
+ description="",
93
+ launch_source="",
94
+ attachments=[],
95
+ links=[],
96
+ tags=["smoke"],
97
+ )
98
+ api_client.upload_attachments.return_value = []
99
+ config = Mock(
100
+ project_id="proj-1",
101
+ testrun_name="Run",
102
+ testrun_tags=["smoke"],
103
+ testrun_links=[TestRunLink(url="https://ci.example/1", title="CI", link_type="Related")],
104
+ paths_to_attachments=[],
105
+ output="out.txt",
106
+ )
107
+ service = Service(config, api_client, Mock(), Mock(), Mock())
108
+ service._Service__write_to_output = Mock()
109
+
110
+ service.create_test_run()
111
+
112
+ kwargs = api_client.create_test_run.call_args.kwargs
113
+ assert kwargs["tags"] == ["smoke"]
114
+ assert kwargs["links"] is not None
115
+ assert kwargs["links"][0].url == "https://ci.example/1"
116
+
117
+
118
+ def test_upload_results_merges_tags_and_links_early():
119
+ related = LinkType("Related")
120
+ existing = TestRun(
121
+ id="run-1",
122
+ project_id="proj-1",
123
+ state="InProgress",
124
+ name="Run",
125
+ description="",
126
+ launch_source="",
127
+ attachments=[],
128
+ links=[LinkPutModel(url="https://existing", type=related, has_info=False)],
129
+ tags=["ui"],
130
+ )
131
+ api_client = Mock()
132
+ api_client.get_test_run.return_value = existing
133
+ api_client.upload_attachments.return_value = []
134
+ parser = Mock()
135
+ parser.read_file.return_value = []
136
+ importer = Mock()
137
+ config = Mock(
138
+ testrun_id="run-1",
139
+ testrun_tags=["smoke"],
140
+ testrun_links=[TestRunLink(url="https://ci.example/1")],
141
+ paths_to_attachments=[],
142
+ project_id=None,
143
+ )
144
+ service = Service(config, api_client, parser, importer, Mock())
145
+
146
+ service.upload_results()
147
+
148
+ assert api_client.update_test_run.call_count >= 1
149
+ first_update = api_client.update_test_run.call_args_list[0].args[0]
150
+ assert first_update.tags == ["ui", "smoke"]
151
+ assert [link.url for link in first_update.links] == ["https://existing", "https://ci.example/1"]
@@ -1,32 +0,0 @@
1
- # Test IT CLI
2
-
3
- [![Release
4
- Status](https://img.shields.io/pypi/v/testit-cli?style=plastic)](https://pypi.python.org/pypi/testit-cli)
5
- [![Downloads](https://img.shields.io/pypi/dm/testit-cli?style=plastic)](https://pypi.python.org/pypi/testit-cli)
6
-
7
- This tool is the command line wrapper of Test IT allowing you to upload the test results in real time to Test IT.
8
- You can see more information in [official documentation](https://docs.testit.software/user-guide/integrations/cli.html)
9
-
10
- ## Compatibility
11
-
12
- | Test IT | Test IT CLI |
13
- |---------|----------------|
14
- | 3.5+ | 0.1 |
15
- | 4.4 | 1.0 |
16
- | 4.5 | 1.2 |
17
- | 4.6 | 1.5 |
18
- | 5.0 | 2.0 |
19
- | 5.2 | 2.1 |
20
- | 5.2.2 | 2.2.1.post522 |
21
- | 5.2.3 | 2.3.0.post523 |
22
- | 5.3 | 2.4.5.post530 |
23
- | 5.4 | 2.4.11.post540 |
24
- | 5.5 | 2.7.2.post550 |
25
- | 5.6 | 2.8.1.post560 |
26
- | 5.7 | 2.9.1.post570 |
27
- | 5.8 | 2.10.0.post580 |
28
- | Cloud | 2.8.0 + |
29
-
30
- 1. For current versions, see the releases tab.
31
- 2. Starting with 5.2, we have added a TMS postscript, which means that the utility is compatible with a specific enterprise version.
32
- 3. If you are in doubt about which version to use, check with the support staff. support@yoonion.ru