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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,8 @@ The value `beautiful` will expire 10 seconds from the moment it's stored.
If you try to retrieve an expired value with `Cache Retrieve Value` it will return `None` like it
would if it was never stored.

The default retention is 3600 seconds (1 hour).
The default retention is 3600 seconds (1 hour). You can change this default when importing the
library.

### Changing the cache file path

Expand Down
43 changes: 25 additions & 18 deletions src/CacheLibrary/CacheLibrary.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from contextlib import contextmanager
from datetime import datetime, timedelta
from pathlib import Path
from typing import Any, TypeAlias, TypedDict
from typing import Any, Literal, TypeAlias, TypedDict

from pabot.pabotlib import PabotLib
from robot.api import logger
Expand All @@ -28,7 +28,6 @@ class CacheEntry(TypedDict):

CacheContents: TypeAlias = dict[CacheKey, CacheEntry]


KwName: TypeAlias = str
KwArgs: TypeAlias = Any

Expand Down Expand Up @@ -95,14 +94,17 @@ def __init__(
self,
file_path: str = "robotframework-cache.json",
file_size_warning_bytes: int = 500000,
default_expire_in_seconds: int = 3600,
) -> None:
"""
| `file_path` | Path to the cache file. Relative to where Robot Frameworks working directory |
| `file_size_warning_bytes` | Log warning when the cache exceeds this size |
| `file_path` | Path to the cache file. Relative to where Robot Frameworks working directory |
| `file_size_warning_bytes` | Log warning when the cache exceeds this size |
| `default_expire_in_seconds=3600` | After how many seconds should a stored value expire. Can be overwritten when a value is stored |
""" # noqa: D205, E501
self.pabotlib = PabotLib()
self.file_path = Path(file_path)
self.file_size_warning_bytes = file_size_warning_bytes
self.default_expire_in_seconds = default_expire_in_seconds

@keyword
def cache_retrieve_value(self, key: CacheKey) -> CacheValue | None:
Expand Down Expand Up @@ -138,7 +140,7 @@ def cache_store_value(
self,
key: CacheKey,
value: CacheValue,
expire_in_seconds: int = 3600,
expire_in_seconds: int | Literal["default"] = "default",
) -> None:
"""
Store a value in the cache.
Expand All @@ -153,26 +155,29 @@ def cache_store_value(
- Dictionary
- List

| `key` | Name of the value to be stored |
| `value` | Value to be stored |
| `expire_in_seconds=3600` | After how many seconds the value should expire |
| `key` | Name of the value to be stored |
| `value` | Value to be stored |
| `expire_in_seconds=default` | After how many seconds the value should expire |

= Examples =

== Basic usage ==

Store a value in the cache

| Cache Store Value user-session ${session_token}
| Cache Store Value user-session ${session_token}

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

== Control expiration ==

Store a value in the cache and set it to expire in 1 minute

| Cache Store Value user-session ${session_token} expire_in_seconds=60
| Cache Store Value user-session ${session_token} expire_in_seconds=60
"""
if expire_in_seconds == "default":
expire_in_seconds = self.default_expire_in_seconds

with self._lock("cachelib-edit"):
cache = self._open_cache_file()

Expand All @@ -197,7 +202,7 @@ def cache_remove_value(self, key: CacheKey) -> None:

Remove a value from the cache

| Cache Remove Value user-session
| Cache Remove Value user-session
"""
with self._lock("cachelib-edit"):
cache = self._open_cache_file()
Expand All @@ -223,7 +228,7 @@ def run_keyword_and_cache_output(
self,
keyword: KwName,
*args: KwArgs,
expire_in_seconds: int = 3600,
expire_in_seconds: int | Literal["default"] = "default",
) -> CacheValue:
"""
If possible, return the keyword's output from cache.
Expand All @@ -235,33 +240,34 @@ def run_keyword_and_cache_output(
to create incorrect caching behavior. You can easily create two keyword calls that
functionally do the same thing, but are considered different for caching purposes.

| `keyword` | The keyword that to be run |
| `*args` | Arguments send to the keyword |
| `expire_in_seconds=3600` | After how many seconds the value should expire |
| `keyword` | The keyword that to be run |
| `*args` | Arguments send to the keyword |
| `expire_in_seconds=default` | After how many seconds the value should expire |

= Examples =

== Basic usage ==

Wrap a keyword with Run Keyword And Cache Output to cache its output.

| ${session_token} = Run Keyword And Cache Output Get API Session Token
| ${session_token} = Run Keyword And Cache Output Get API Session Token

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

== With keyword arguments ==

Wrap a keyword that requires arguments.

| ${user_session_token} = Run Keyword And Cache Output Login User ${username} ${password}
| ${user_session_token} = Run Keyword And Cache Output Login User ${username} ${password}

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

== Control expiration ==

Wrap a keyword that requires arguments and set it to expire in 1 minute

| ${user_session_token} = Run Keyword And Cache Output Login User ${username} ${password} expire_in_seconds=60
| ${user_session_token} = Run Keyword And Cache Output
| ... Login User ${username} ${password} expire_in_seconds=60

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

Expand Down Expand Up @@ -316,6 +322,7 @@ def _open_cache_file(self) -> CacheContents:
cache_contents[key] = entry

self.pabotlib.set_parallel_value_for_key(self.parallel_value_key, cache_contents)

self._store_json_file(self.file_path, cache_contents)
return cache_contents

Expand Down
8 changes: 4 additions & 4 deletions tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ def test(c: Context):
test_integration_sync,
test_integration_parallel_suite_level,
test_integration_parallel_test_level,
test_acceptance_parallel_test_level,
test_acceptance_parallel_test_level,
test_acceptance_sync,
test_acceptance_parallel_suite_level,
test_acceptance_parallel_test_level,
),
)
Expand Down Expand Up @@ -159,8 +159,8 @@ def test_acceptance_parallel_test_level(c: Context):
test_integration_parallel_suite_level,
test_integration_parallel_test_level,
test_acceptance,
test_acceptance_parallel_test_level,
test_acceptance_parallel_test_level,
test_acceptance_sync,
test_acceptance_parallel_suite_level,
test_acceptance_parallel_test_level,
)
ns.configure(
Expand Down
99 changes: 71 additions & 28 deletions test/acceptance/run.robot
Original file line number Diff line number Diff line change
@@ -1,89 +1,132 @@
*** Settings ***
Library Process
Library FakerLibrary
Library OperatingSystem
Library Collections
Library Collections
Library DateTime
Library OperatingSystem
Library Process
Library FakerLibrary


*** Variables ***
${DEFAULT_EXPIRES_IN_SECONDS} ${3600}


*** Test Cases ***
A01 Fetches cache from file
${file_cache_path} = Set Variable robocache-A01.json
${file_cache_path} = Set Variable robocache-A01.json

${cache} = Create Dictionary some-string-value=Lorum ipsum dolor sit amet conscuer
${cache} = Create Dictionary some-string-value=Lorum ipsum dolor sit amet conscuer
Create Cache File With Content ${file_cache_path} ${cache}

Run Test File With Robot A01.robot

[Teardown] Remove File ${file_cache_path}

A02 Creates a new cache file if it does not exist
${file_cache_path} = Set Variable robocache-A02.json
${file_cache_path} = Set Variable robocache-A02.json

Run Test File With Robot A02.robot

File Should Exist ${file_cache_path}
${contents} = Get File ${file_cache_path}
${contents} = Get File ${file_cache_path}
Should Contain ${contents} "some-value": {
Should Contain ${contents} "value": "Hello, world!"

[Teardown] Remove File ${file_cache_path}

A03 Resets the cache file when adding and the cache file is not json
${file_cache_path} = Set Variable robocache-A03.json
${file_cache_path} = Set Variable robocache-A03.json

Create File ${file_cache_path} <this> is #not# json!

Run Test File With Robot A03.robot

File Should Exist ${file_cache_path}
${contents} = Get File ${file_cache_path}
${contents} = Get File ${file_cache_path}
Should Contain ${contents} "some-value": {
Should Contain ${contents} "value": "Hello, world!"

[Teardown] Remove File ${file_cache_path}

A04 Resets the cache file when fetching and the cache file is not json
${file_cache_path} = Set Variable robocache-A04.json
${file_cache_path} = Set Variable robocache-A04.json

Create File ${file_cache_path} <this> is #not# json!

Run Test File With Robot A04.robot

File Should Exist ${file_cache_path}
${contents} = Get File ${file_cache_path}
${contents} = Get File ${file_cache_path}
Should Be Equal ${contents} \{\}

[Teardown] Remove File ${file_cache_path}

A05 Removes expired values from the cache file
${file_cache_path} = Set Variable robocache-A05.json

${cache} = Create Dictionary expired_value=123
Create Cache File With Content ${file_cache_path} ${cache}
... expiration=1970-01-01T00:00:00.000000

Run Test File With Robot A05.robot

File Should Exist ${file_cache_path}
${contents} = Get File ${file_cache_path}
Should Be Equal ${contents} \{\}

[Teardown] Remove File ${file_cache_path}

A06 Overwrites default expiration time during import
${file_cache_path} = Set Variable robocache-A06.json

Run Test File With Robot A06.robot

File Should Exist ${file_cache_path}
${contents} = Get File ${file_cache_path}
${cache_content} = Evaluate json.loads('${contents}') modules=json

${expires_date} = Set Variable ${cache_content['foo']['expires']}
${expires_date} = DateTime.Convert Date ${expires_date}
${now} = DateTime.Get Current Date
${expires_in} = DateTime.Subtract Date From Date ${expires_date} ${now} result_format=number

Should Be True
... ${{ ${expires_in} > ${DEFAULT_EXPIRES_IN_SECONDS} }}
... msg=Expiration time should be greater than default expiration time | Expected ${expires_in} to be greater than ${DEFAULT_EXPIRES_IN_SECONDS}

[Teardown] Remove File ${file_cache_path}


*** Keywords ***
Create Cache File With Content
[Arguments] ${file_name} ${key_value_pairs}
${future_date} = Evaluate
... (datetime.datetime.now() + datetime.timedelta(days=1)).isoformat()
... modules=datetime

${cache_entries} = Create Dictionary
FOR ${key} ${value} IN &{key_value_pairs}
${entry} = Create Dictionary
[Arguments] ${file_name} ${key_value_pairs} ${expiration}=${None}
IF '${expiration}' == '${None}'
${expiration} = Evaluate
... (datetime.datetime.now() + datetime.timedelta(days=1)).isoformat()
... modules=datetime
END

${cache_entries} = Create Dictionary
FOR ${key} ${value} IN &{key_value_pairs}
${entry} = Create Dictionary
... value=${value}
... expires=${future_date}
... expires=${expiration}
Set To Dictionary ${cache_entries} ${key}=${entry}
END

${cache_file_content} = Evaluate json.dumps(${cache_entries}) modules=json
${cache_file_content} = Evaluate json.dumps(${cache_entries}) modules=json
Create File ${file_name} ${cache_file_content}


Run Test File With Robot
[Arguments] ${path}
${path} = Normalize Path ${CURDIR}/test/${path}
${path} = Normalize Path ${CURDIR}/test/${path}

${result} = Run Process
${result} = Run Process
... uv run robot --output NONE --log NONE --report NONE ${path}
... shell=${True}

IF ${result.rc} != 0
log ${result.stdout} level=WARN
log ${result.stderr} level=ERROR
IF ${result.rc} != 0
log ${result.stdout} level=WARN
log ${result.stderr} level=ERROR

Fail Acceptance test failed. Details above.
END
8 changes: 8 additions & 0 deletions test/acceptance/test/A05.robot
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
*** Settings ***
Library CacheLibrary robocache-A05.json


*** Test Cases ***
Fetches data from file
${value} = Cache Retrieve Value expired_value
Should Be Equal ${value} ${None}
7 changes: 7 additions & 0 deletions test/acceptance/test/A06.robot
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
*** Settings ***
Library CacheLibrary robocache-A06.json default_expire_in_seconds=86400


*** Test Cases ***
Fetches data from file
Cache Store Value foo bar
Loading