Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@

### 1.0.4-dev

* `lobster-codebeamer`:
- Added `baseline_id` support for cbQL queries. When set, the tool queries
items at the specified Codebeamer baseline revision.
- `baseline_id` combined with `import_tagged` or a numeric `import_query`
now raises an error instead of being silently ignored (ISO 26262 TCL3
qualification support).

* `trlc bazel dep`: update to trlc==2.0.5

* Fixed wrong links in [README](README.md).
Expand Down
17 changes: 17 additions & 0 deletions documentation/manual-lobster_codebeamer.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,23 @@ Then invoke the `lobster-codebeamer` tool like so:
```bash
$ lobster-codebeamer --import-query 4776335 --out system-requirements.lobster
```

### Querying at a Baseline

To restrict results to the state of a specific Codebeamer baseline, add `baseline_id`
to the YAML config file:

```yaml
import_query: "tracker.id IN (29782591) AND status IN ('Approved')"
baseline_id: 407126303
```

> **Important:** `baseline_id` is only applied when `import_query` is a **cbQL query
> string**. When `import_query` is a numeric report ID or `import_tagged` (a path to
> an existing LOBSTER artifact for tag-based import, see
> [Importing only tagged requirements](#importing-only-tagged-requirements)) is set,
> `lobster-codebeamer` exits with an error (exit code 1).

### Importing only tagged requirements

If you are not interested in a completeness check, or your
Expand Down
30 changes: 30 additions & 0 deletions lobster/tools/codebeamer/codebeamer.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ class SupportedConfigKeys(Enum):
RETRY_ERROR_CODES = "retry_error_codes"
IMPORT_TAGGED = "import_tagged"
IMPORT_QUERY = "import_query"
BASELINE_ID = "baseline_id"
VERIFY_SSL = "verify_ssl"
PAGE_SIZE = "page_size"
REFS = "refs"
Expand Down Expand Up @@ -231,6 +232,8 @@ def get_query(cb_config: Config, query: Union[int, str]):
elif isinstance(query, str):
url = (f"{cb_config.base}/items/query?page={page_id}"
f"&pageSize={cb_config.page_size}&queryString={query}")
if cb_config.baseline_id is not None:
url += f"&baselineId={cb_config.baseline_id}"
data = query_cb_single(cb_config, url)
if len(data) != 4:
raise MismatchException(
Expand Down Expand Up @@ -505,6 +508,7 @@ def parse_config_data(data: dict) -> Config:
references=ensure_list(data.get(SupportedConfigKeys.REFS.value, [])),
import_tagged=data.get(SupportedConfigKeys.IMPORT_TAGGED.value),
import_query=data.get(SupportedConfigKeys.IMPORT_QUERY.value),
baseline_id=data.get(SupportedConfigKeys.BASELINE_ID.value),
verify_ssl=data.get(SupportedConfigKeys.VERIFY_SSL.value, True),
page_size=data.get(SupportedConfigKeys.PAGE_SIZE.value, 100),
schema=data.get(SupportedConfigKeys.SCHEMA.value, "Requirement"),
Expand Down Expand Up @@ -532,6 +536,30 @@ def parse_config_data(data: dict) -> Config:
raise KeyError(f"{SupportedConfigKeys.CB_ROOT.value} must start with https://, "
f"but value is {config.cb_auth_conf.root}.")

if config.baseline_id is not None:
if config.import_tagged:
raise KeyError(
f"The keys {SupportedConfigKeys.BASELINE_ID.value} and "
f"{SupportedConfigKeys.IMPORT_TAGGED.value} are both present "
f"in the configuration, but they are mutually exclusive!"
)
if config.import_query and not isinstance(config.import_query, str):
raise KeyError(
f"The key {SupportedConfigKeys.BASELINE_ID.value} is only "
f"allowed if {SupportedConfigKeys.IMPORT_QUERY.value} is a "
f"cbQL query string, not a numeric report ID!"
)
try:
config.baseline_id = int(config.baseline_id)
except (TypeError, ValueError) as exc:
raise ValueError(
f"{SupportedConfigKeys.BASELINE_ID.value} must be a positive integer."
) from exc
if config.baseline_id <= 0:
raise ValueError(
f"{SupportedConfigKeys.BASELINE_ID.value} must be a positive integer."
)
Comment thread
iceman-muc marked this conversation as resolved.

return config


Expand Down Expand Up @@ -571,6 +599,8 @@ def _run_impl(self, options: argparse.Namespace) -> int:
)
except ValueError as value_error:
self._print_error(value_error)
except KeyError as key_error:
self._print_error(key_error)
except LOBSTER_Error as lobster_error:
self._print_error(lobster_error)

Expand Down
1 change: 1 addition & 0 deletions lobster/tools/codebeamer/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class Config:
references: dict
import_tagged: str
import_query: Union[str, int]
baseline_id: Optional[int]
verify_ssl: bool
page_size: int
schema: str
Expand Down
5 changes: 5 additions & 0 deletions manual/tools/codebeamer.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ Config
references=[],
import_tagged=None,
import_query=1234, # report ID or cbQL query string
baseline_id=None,
verify_ssl=True,
page_size=100,
schema="requirement", # or "implementation" / "activity"
Expand All @@ -57,6 +58,7 @@ Config
- ``references`` (List[str]): Names of Codebeamer fields whose referenced items should be traced (converted to ``req`` tags).
- ``import_tagged`` (str | None): Path to an existing LOBSTER artifact whose unresolved ``req`` references define item IDs to import.
- ``import_query`` (int | str | None): Report ID (int) or cbQL query string used to fetch items directly.
- ``baseline_id`` (int | None): Codebeamer baseline ID to query against. **Only allowed when** ``import_query`` **is a cbQL query string**. Raises an error if combined with ``import_tagged`` or a numeric ``import_query``.
- ``verify_ssl`` (bool): Whether to verify TLS certificates; set ``True`` in production for security.
- ``page_size`` (int): Pagination size for REST queries; a trade-off between round trips and response size (default typically 100).
- ``schema`` (str): Target schema type (``requirement``, ``implementation``, ``activity``) controlling class/namespace mapping.
Expand Down Expand Up @@ -122,3 +124,6 @@ Error Conditions
- Absent both ``import_query`` & ``import_tagged`` → ``KeyError``.
- ``num_request_retry <= 0`` → ``ValueError``.
- Unrecognised ``schema`` → ``KeyError``.
- ``baseline_id`` is not a positive integer → ``ValueError``.
- ``baseline_id`` combined with ``import_tagged`` → ``KeyError``.
- ``baseline_id`` combined with numeric ``import_query`` → ``KeyError``.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
@dataclass
class ConfigFileData:
import_query: Optional[Union[int, str]] = None
import_tagged: Optional[str] = None
baseline_id: Optional[int] = None
root: Optional[str] = None
token: Optional[str] = None
out: Optional[str] = None
Expand All @@ -31,6 +33,8 @@ def append_if_not_none(key, value):
data[key] = value

append_if_not_none("import_query", self.import_query)
append_if_not_none("import_tagged", self.import_tagged)
append_if_not_none("baseline_id", self.baseline_id)
append_if_not_none("root", self.root)
append_if_not_none("token", self.token)
append_if_not_none("out", self.out)
Expand Down
133 changes: 133 additions & 0 deletions tests_system/lobster_codebeamer/test_baseline_id.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import json
import unittest
from flask import Response
from tests_system.lobster_codebeamer.lobster_codebeamer_system_test_case_base import (
LobsterCodebeamerSystemTestCaseBase)
from tests_system.asserter import Asserter
from tests_system.lobster_codebeamer.lobster_codebeamer_asserter import (
LobsterCodebeamerAsserter)
from tests_system.lobster_codebeamer.mock_server_setup import get_mock_app


class LobsterCodebeamerBaselineIdTest(LobsterCodebeamerSystemTestCaseBase):
"""System tests for baseline_id validation and usage."""

@classmethod
def setUpClass(cls):
cls.codebeamer_flask = get_mock_app()

def setUp(self):
super().setUp()
self.codebeamer_flask.reset()
self._test_runner = self.create_test_runner()
self._test_runner.config_file_data.verify_ssl = False

def test_baseline_id_with_import_tagged_raises_error(self):
"""Ensure baseline_id combined with import_tagged exits with error."""
cfg = self._test_runner.config_file_data
cfg.set_default_root_token_out(self.codebeamer_flask.port)
cfg.import_tagged = "some_file.lobster"
cfg.baseline_id = 12345

completed_process = self._test_runner.run_tool_test()
asserter = Asserter(self, completed_process, self._test_runner)
asserter.assertExitCode(1)
asserter.assertStdErrText(
"lobster-codebeamer: 'The keys baseline_id and import_tagged "
"are both present in the configuration, but they are mutually "
"exclusive!'\n"
)

def test_baseline_id_with_numeric_import_query_raises_error(self):
"""Ensure baseline_id combined with a numeric import_query exits
with error."""
cfg = self._test_runner.config_file_data
cfg.set_default_root_token_out(self.codebeamer_flask.port)
cfg.import_query = 9999
cfg.baseline_id = 12345

completed_process = self._test_runner.run_tool_test()
asserter = Asserter(self, completed_process, self._test_runner)
asserter.assertExitCode(1)
asserter.assertStdErrText(
"lobster-codebeamer: 'The key baseline_id is only allowed if "
"import_query is a cbQL query string, not a numeric report ID!'\n"
)

def test_baseline_id_negative_raises_error(self):
"""Ensure a negative baseline_id exits with error."""
cfg = self._test_runner.config_file_data
cfg.set_default_root_token_out(self.codebeamer_flask.port)
cfg.import_query = "tracker.id IN (123)"
cfg.baseline_id = -1

completed_process = self._test_runner.run_tool_test()
asserter = Asserter(self, completed_process, self._test_runner)
asserter.assertExitCode(1)
asserter.assertStdErrText(
"lobster-codebeamer: baseline_id must be a positive integer.\n"
)

def test_baseline_id_zero_raises_error(self):
"""Ensure baseline_id of 0 exits with error."""
cfg = self._test_runner.config_file_data
cfg.set_default_root_token_out(self.codebeamer_flask.port)
cfg.import_query = "tracker.id IN (123)"
cfg.baseline_id = 0

completed_process = self._test_runner.run_tool_test()
asserter = Asserter(self, completed_process, self._test_runner)
asserter.assertExitCode(1)
asserter.assertStdErrText(
"lobster-codebeamer: baseline_id must be a positive integer.\n"
)

def test_baseline_id_with_cbql_query_succeeds(self):
"""Ensure baseline_id with a cbQL string query works and appends
baselineId to the URL."""
cfg = self._test_runner.config_file_data
cfg.set_default_root_token_out(self.codebeamer_flask.port)
cfg.import_query = "tracker.id IN (123)"
cfg.baseline_id = 407126303

response_data = {
'page': 1,
'pageSize': 1,
'total': 1,
'items': [
{
'id': 5,
'name': 'Requirement 5: Dynamic name',
'description': 'Dynamic description',
'status': {
'id': 5,
'name': 'Status 5',
'type': 'ChoiceOptionReference'
},
'tracker': {
'id': 5,
'name': 'Tracker_Name_5',
'type': 'TrackerReference',
},
'version': 1,
}
]
}
self.codebeamer_flask.responses = [
Response(json.dumps(response_data), status=200),
]
self._test_runner.declare_output_file(
self._data_directory / cfg.out)

completed_process = self._test_runner.run_tool_test()
asserter = Asserter(self, completed_process, self._test_runner)
asserter.assertExitCode(0)

# Verify the baselineId parameter was included in the request URL
self.assertEqual(len(self.codebeamer_flask.received_requests), 1)
request_url = self.codebeamer_flask.received_requests[0]["url"]
self.assertIn("baselineId=407126303", request_url)


if __name__ == "__main__":
unittest.main()
31 changes: 31 additions & 0 deletions tests_unit/lobster_codebeamer/test_codebeamer.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ def setUp(self):
references=None,
import_tagged=None,
import_query=None,
baseline_id=None,
verify_ssl=None,
page_size=10,
schema="Requirement",
Expand Down Expand Up @@ -118,6 +119,36 @@ def test_get_query_with_query(self, mock_query_cb_single):
self.assertEqual(result[0].location.tracker, item_data[0]["tracker"]["id"])
self.assertEqual(result[0].location.version, item_data[0]["version"])

@patch('lobster.tools.codebeamer.codebeamer.query_cb_single')
def test_get_query_with_query_and_baseline_id(self, mock_query_cb_single):
"""baseline_id is appended to the URL when import_query is a cbQL string."""
self._mock_cb_config.baseline_id = 407126303
mock_query = "tracker.id IN (29782591)"
mock_query_cb_single.return_value = {
"page": 1, "pageSize": 10, "total": 0, "items": []
}
get_query(self._mock_cb_config, mock_query)
called_url = mock_query_cb_single.call_args[0][1]
self.assertIn("baselineId=407126303", called_url)

@patch('lobster.tools.codebeamer.codebeamer.query_cb_single')
def test_get_query_with_ID_and_baseline_id(self, mock_query_cb_single):
"""baseline_id is NOT appended to the URL when import_query is a report ID."""
self._mock_cb_config.baseline_id = 407126303
mock_query = 171619121
item_data = [{"item": {
"id": 1, "name": "x", "version": 1,
"tracker": {"id": 1},
"categories": [{"name": "Requirement"}],
"status": {"name": "Draft"},
}}]
mock_query_cb_single.return_value = {
"page": 1, "pageSize": 10, "total": 1, "items": item_data
}
get_query(self._mock_cb_config, mock_query)
called_url = mock_query_cb_single.call_args[0][1]
self.assertNotIn("baselineId", called_url)

@patch('lobster.tools.codebeamer.codebeamer.query_cb_single')
def test_get_query_with_invalid_data(self, mock_query_cb_single):
query_id = 789
Expand Down
Loading