Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:

steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/docs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6

- name: Setup Pages
uses: actions/configure-pages@v5
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ dev = [
"invoke (>=2.2.1)",
# Required for faker <6.0.0
"setuptools (>=80.0.0,<81.0.0)",
"robotframework-retryfailed>=0.2.0",
]

[tool.uv.build-backend]
Expand Down
57 changes: 51 additions & 6 deletions src/CacheLibrary/CacheLibrary.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from contextlib import contextmanager
from datetime import datetime, timedelta
from pathlib import Path
from typing import Any, Literal, TypeAlias
from typing import Any, Literal, TypeAlias, cast

from pabot.pabotlib import PabotLib
from robot.api import logger
Expand Down Expand Up @@ -158,7 +158,7 @@ def cache_retrieve_value(self, key: CacheKey) -> CacheValue | None:
def cache_retrieve_value_from_collection(
self,
key: CacheKey,
pick: Literal["first", "last", "random"] = "first",
pick: Literal["first", "last", "random", "parallel process"] = "first",
remove_value: bool = True, # noqa: FBT001, FBT002
) -> CacheValue | None:
"""
Expand All @@ -167,16 +167,35 @@ def cache_retrieve_value_from_collection(
Will return a single value from a collection stored in the cache, or `None` if there is no
value.

| `key` | Name of the collection |
| `pick=first` | How to pick a value from the collection. Can be 'first', 'last', or 'random' |
| `remove_value=True` | Should the value be removed from the collection |
| `key` | Name of the collection |
| `pick=first` | How to pick a value from the collection. Can be 'first', 'last', 'random', or 'parallel process' |
| `remove_value=True` | Should the value be removed from the collection |

= Pick strategies =

- `first`: pick the first value
- `last`: pick the last value
- `random`: pick a random value
- `parallel process`: pick a value unique per parallel process.

== Pick strategy `parallel process` ==

With `parallel process`, each parallel process gets its own fixed value, preventing multiple
processes from using the same value at the same time. Within a process, the same value is
returned on each call.

Designed to be used with `remove_value=False` and Pabot. When not using Pabot,
`parallel process` behaves the same as `first`.

= Examples =

== Basic usage ==

Retrieve the first value from a cached collection.

The value is automatically removed from the collection. This way you will receive a new
value every time you use this keyword.

| ${user} = Cache Retrieve Value From Collection user-accounts

--------------------
Expand All @@ -187,6 +206,18 @@ def cache_retrieve_value_from_collection(
collection.

| ${user} = Cache Retrieve Value From Collection user-accounts pick=random remove_value=${False}

--------------------

== Pick parallel process value ==

Ensure each parallel process uses a different user from a cached collection of users. This
could be useful in the following scenario:

In the application under test, a user can only have a logged in session in one browser at a
time. If you log in from a second browser, the user is logged out in the first browser.

| ${user} = Cache Retrieve Value From Collection user-accounts pick=parallel process remove_value=${False}
""" # noqa: E501
cache = self.cache_file.get()
cache = self._ensure_complete_cache(cache)["COLLECTION"]
Expand All @@ -211,8 +242,13 @@ def cache_retrieve_value_from_collection(
index = -1
elif pick == "random":
index = random.randint(0, len(values) - 1) # noqa: S311
elif pick == "parallel process":
index = self._pick_strategy_parallel_process()
else:
msg = f"Unexpected pick '{pick}'. Expected one of 'first', 'last', or 'random'."
msg = (
f"Unexpected pick '{pick}'. "
"Expected one of 'first', 'last', 'random', or 'parallel process'."
)
raise ValueError(msg)

value = values[index]
Expand All @@ -221,6 +257,15 @@ def cache_retrieve_value_from_collection(

return value

def _pick_strategy_parallel_process(self) -> int:
index = BuiltIn().get_variable_value("${PABOTEXECUTIONPOOLID}", 0)
try:
index = int(cast(str | int, index))
except: # noqa: E722
index = 0

return index

@keyword(tags=["value"])
def cache_store_value(
self,
Expand Down
8 changes: 5 additions & 3 deletions tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,19 +98,21 @@ def test_integration(c: Context):
@task
def test_integration_sync(c: Context):
print("Integration tests: Synchronous")
c.run("uv run robot test/integration")
c.run("uv run robot --listener RetryFailed test/integration")


@task
def test_integration_parallel_suite_level(c: Context):
print("Integration tests: Parallel, Suite level split")
c.run("uv run pabot --pabotlib test/integration")
c.run("uv run pabot --pabotlib --listener RetryFailed test/integration")


@task
def test_integration_parallel_test_level(c: Context):
print("Integration tests: Parallel, Test level split")
c.run("uv run pabot --testlevelsplit --pabotlib test/integration")
c.run(
"uv run pabot --testlevelsplit --pabotlib --listener RetryFailed test/integration",
)


@task
Expand Down
72 changes: 72 additions & 0 deletions test/integration/run-multi-value.robot
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,23 @@ ${ITERATIONS} 50
... float
... list
... dict
@{FIXED_INCREMENTAL_COLLECTION}
... 111
... 222
... 333
... 444
... 555
... 666
... 777
... 888
... 999
... 000
... aaa
... bbb
... ccc
... ddd
... eee
... fff


*** Test Cases ***
Expand Down Expand Up @@ -232,6 +249,36 @@ Store and retrieve random list data
Should Be Equal ${retrieved} ${value_set}[${i}]
END

Pick strategy Pabot - thread 1
[Setup] Acquire Lock pick_strat_pabot
[Template] Test Template Pabot Pick Strategy
${None}
[Teardown] Release Lock pick_strat_pabot

Pick strategy Pabot - thread 2
[Setup] Acquire Lock pick_strat_pabot
[Template] Test Template Pabot Pick Strategy
${None}
[Teardown] Release Lock pick_strat_pabot

Pick strategy Pabot - thread 3
[Setup] Acquire Lock pick_strat_pabot
[Template] Test Template Pabot Pick Strategy
${None}
[Teardown] Release Lock pick_strat_pabot

Pick strategy Pabot - thread 4
[Setup] Acquire Lock pick_strat_pabot
[Template] Test Template Pabot Pick Strategy
${None}
[Teardown] Release Lock pick_strat_pabot

Pick strategy Pabot - thread 5
[Setup] Acquire Lock pick_strat_pabot
[Template] Test Template Pabot Pick Strategy
${None}
[Teardown] Release Lock pick_strat_pabot


*** Keywords ***
Generate Complex Collection
Expand Down Expand Up @@ -269,3 +316,28 @@ Generate Complex Collection
END

RETURN ${collection}

Test Template Pabot Pick Strategy
[Arguments] ${_}
Cache Store Collection fixed-strings @{FIXED_INCREMENTAL_COLLECTION}
${pabot_thread_id} = Get Variable Value ${PABOTEXECUTIONPOOLID} 0
${expected_value} = Get From List ${FIXED_INCREMENTAL_COLLECTION} ${pabot_thread_id}

${other_tread_id} = Get Parallel Value For Key pick_strat_pabot_thread
${other_tread_expected_value} = Get Parallel Value For Key pick_strat_pabot_value
IF '${other_tread_expected_value}' == '${EMPTY}'
Set Parallel Value For Key pick_strat_pabot_thread ${pabot_thread_id}
Set Parallel Value For Key pick_strat_pabot_value ${expected_value}
ELSE IF ${pabot_thread_id} != ${other_tread_id}
Should Not Be Equal As Strings ${expected_value} ${other_tread_expected_value}
ELSE
Should Be Equal As Strings ${expected_value} ${other_tread_expected_value}
END

FOR ${_} IN RANGE ${5}
${retrieved} = Cache Retrieve Value From Collection
... fixed-strings
... pick=parallel process
... remove_value=False
Should Be Equal ${retrieved} ${expected_value}
END
7 changes: 6 additions & 1 deletion test/integration/run.robot
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,12 @@ Remove stored data
Should Be Equal ${postRemove} ${None}

Store with expiration time
Cache Store Value random-data amazing data expire_in_seconds=1
[Documentation]
... Test relies on timing within the test. This timing can be messed up by locks aquired by
... other tests. Retry stabilizes the run.
... The retry is _not_ hiding a race condition in this situation.
[Tags] test:retry(1)
Cache Store Value random-data amazing data expire_in_seconds=2
${preSleep} = Cache Retrieve Value random-data
Sleep 2s
${postSleep} = Cache Retrieve Value random-data
Expand Down
14 changes: 14 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading