Simplify SeedSigner OS builder path

This commit is contained in:
kdmukai
2025-12-26 13:41:32 -06:00
parent eed16f73d8
commit d2914c33fb
4 changed files with 382 additions and 304 deletions
+149 -130
View File
@@ -40,52 +40,70 @@ class Version(Singleton):
around in the main code) and so that the version data is only determined once per
runtime.
On SeedSigner OS:
* Version data is written to `src/seedsigner/version.json` during the build
process via tools/write_versionfile.py.
* version_name: copied from the command used to build the SeedSigner OS image:
--app-branch (the target git branch OR tag) or
--app-commit-id (target commit hash)
* version_fork: the git repo owner/fork name (e.g. "SeedSigner") targeted by the
build process.
* version_timestamp: the last git commit time for the target branch, tag, or
commit hash.
* commit_hash: the short commit hash for the specified build target.
SeedSigner OS lifecycle:
* The SeedSigner OS build process runs the tools/write_versionfile.py script to
generate version.json.
* The version.json file is included in the SeedSigner OS image.
The version data is fetched differently depending on the environment:
In SeedSigner OS:
* Version data is read exclusively from version.json at runtime.
In the SeedSigner OS builder:
- Note: the build process relies on `git` being installed so we leverage it here.
* version_name:
* read dynamically from `git` shell calls to retrieve, in order:
* Current git branch name
* Current git tag name
* Current git short commit hash
* version_fork:
* the git repo owner parsed out of the remote url.
* https://github.com/SeedSigner/seedsigner.git -> "SeedSigner"
* Read dynamically from shell `git` call to check the remote "origin" URL.
* short_commit_hash:
* the commit hash for the current branch / tag / detached HEAD.
* Read dynamically from shell `git` call.
* version_timestamp:
* the last git commit time for the current branch / tag / detached HEAD.
* Read dynamically from shell `git` call.
In local dev:
* version_name: read dynamically from a few possible sources. In order:
* SEEDSIGNER_VERSION_NAME env var, if available.
* Shell `git` calls (e.g. `git branch --show-current`).
* Directly parsing the .git/HEAD file and possibly .git/refs/tags.
* Note: we avoid reading from the version.json file as it may be out of date
and could lead to confusion.
* version_fork: read dynamically from:
* Shell `git` call to check the remote "origin" URL.
* Parse the .git/config for the remote "origin" URL.
- Similar process as for the SeedSigner OS builder, but with additional fallbacks and
a different method for determining version_timestamp.
- If a local version.json is present, it will be ignored; it may be out of date and
could lead to confusion.
* version_name:
* `git` shell calls + directly parsing the .git/HEAD file and .git/refs/tags
when necessary.
* version_fork:
* `git` shell call + parse the .git/config for the remote "origin" URL.
* commit_hash:
* Shell `git` call + parse the .git/HEAD file and
.git/refs/heads/<branch_name> when necessary.
* version_timestamp: determined by scanning the src/ directory for the most
recently modified python file.
* commit_hash: read dynamically from:
* Shell `git` call.
* Parse the .git/HEAD file and possibly .git/refs/heads/<branch_name>.
In Github Actions CI:
* version_name: read from GITHUB_REF_NAME or GITHUB_SHA env vars.
* version_fork: TODO
* version_timestamp: TODO
* commit_hash: TODO
* version_name: read from GITHUB_REF_NAME env var.
* version_fork: read from GITHUB_REPOSITORY_OWNER env var.
* version_timestamp: Shell `git` call to get the last commit time for the current
branch/tag/commit.
* commit_hash: read from GITHUB_SHA env var.
This class defines the limited methods that are meant to be publicly accessible
across the SeedSigner codebase.
The misc utility functions in `VersionUtils` were explicitly isolated because they
should NOT be used elsewhere in the codebase.
The utility functions in `VersionUtils` were explicitly isolated because they should
NOT be used elsewhere in the codebase.
TODO: Should `VersionUtils` be an internal class within `Version` to further signal
that it is not to be used externally?
TODO: Also pull git log history and display it in its own View?
"""
_version_name: str = None
_version_fork: str = None
_version_timestamp: datetime = None
_short_commit_hash: str = None
_version_timestamp: datetime = None
@classmethod
@@ -114,27 +132,31 @@ class Version(Singleton):
return cls.get_instance()._version_fork
@classmethod
def get_version_timestamp(cls) -> datetime | None:
return cls.get_instance()._version_timestamp
@classmethod
def get_short_commit_hash(cls) -> str | None:
return cls.get_instance()._short_commit_hash
@classmethod
def get_version_timestamp(cls) -> datetime | None:
return cls.get_instance()._version_timestamp
@classmethod
@not_allowed_in_seedsigner_os
def override_data(cls, version_name, version_fork, version_timestamp, short_commit_hash):
def override_data(cls, **kwargs):
"""
Only used by the screenshot generator.
"""
instance = cls.get_instance()
instance._version_name = version_name
instance._version_fork = version_fork
instance._version_timestamp = version_timestamp
instance._short_commit_hash = short_commit_hash
if VersionUtils.VERSIONFILE_ATTR__NAME in kwargs:
instance._version_name = kwargs[VersionUtils.VERSIONFILE_ATTR__NAME]
if VersionUtils.VERSIONFILE_ATTR__FORK in kwargs:
instance._version_fork = kwargs[VersionUtils.VERSIONFILE_ATTR__FORK]
if VersionUtils.VERSIONFILE_ATTR__SHORT_COMMIT_HASH in kwargs:
instance._short_commit_hash = kwargs[VersionUtils.VERSIONFILE_ATTR__SHORT_COMMIT_HASH]
if VersionUtils.VERSIONFILE_ATTR__TIMESTAMP in kwargs:
instance._version_timestamp = kwargs[VersionUtils.VERSIONFILE_ATTR__TIMESTAMP]
@classmethod
@@ -143,11 +165,12 @@ class Version(Singleton):
return {
VersionUtils.VERSIONFILE_ATTR__NAME: instance._version_name,
VersionUtils.VERSIONFILE_ATTR__FORK: instance._version_fork,
VersionUtils.VERSIONFILE_ATTR__SHORT_COMMIT_HASH: instance._short_commit_hash,
VersionUtils.VERSIONFILE_ATTR__TIMESTAMP: instance._version_timestamp.isoformat() if instance._version_timestamp else None,
VersionUtils.VERSIONFILE_ATTR__COMMIT_HASH: instance._short_commit_hash,
}
class VersionUtils:
""" *********************************************************************************
Not meant to be used elsewhere in the SeedSigner codebase (aside from
@@ -168,7 +191,7 @@ class VersionUtils:
* One external http GET to github to get the most recent release tag.
********************************************************************************* """
ENV_VAR__SEEDSIGNER_OS_BUILDER__VERSION_NAME = "SEEDSIGNER_VERSION_NAME"
ENV_VAR__IS_SEEDSIGNER_OS_BUILDER = "SEEDSIGNER_OS_BUILDER"
ENV_VAR__GITHUB_ACTIONS__IS_CI = "CI"
ENV_VAR__GITHUB_ACTIONS__REF_NAME = "GITHUB_REF_NAME"
ENV_VAR__GITHUB_ACTIONS__SHA = "GITHUB_SHA"
@@ -177,8 +200,8 @@ class VersionUtils:
VERSIONFILE__FILENAME = "version.json"
VERSIONFILE_ATTR__NAME = "name"
VERSIONFILE_ATTR__FORK = "fork"
VERSIONFILE_ATTR__SHORT_COMMIT_HASH = "short_commit_hash"
VERSIONFILE_ATTR__TIMESTAMP = "timestamp"
VERSIONFILE_ATTR__COMMIT_HASH = "commit_hash"
""" *************************************************************************************
@@ -211,18 +234,10 @@ class VersionUtils:
else:
raise Exception("Could not determine version from Github Actions env vars.")
elif VersionUtils._is_seedsigner_os_builder_env():
# In the SeedSigner OS build environment, get the version name from env var.
# Note: This get_version_name call will never fail because the env var that
# provides the version name is what defines whether we're in the SeedSigner OS
# builder in the first place.
version_name = VersionUtils._get_version_name_from_seedsigner_os_builder_env_var()
return VersionUtils._prefix_version_name(version_name)
else:
# In local dev, we try the following methods in order:
# In the SeedSigner OS builder we know that we'll have the `git` shell
# commands available. We add extra fallbacks for local dev.
for get_version_name_method in [
VersionUtils._get_version_name_from_seedsigner_os_builder_env_var,
VersionUtils._get_version_name_from_git_shell,
VersionUtils._get_version_name_from_git_HEAD,
]:
@@ -252,12 +267,9 @@ class VersionUtils:
# In Github Actions CI, try to get the version name from env vars
return VersionUtils._get_version_fork_from_github_actions_env_vars()
elif VersionUtils._is_seedsigner_os_builder_env():
# In the SeedSigner OS build environment `git` shell call should be available
return VersionUtils._get_version_fork_from_git_shell()
else:
# In local dev we try to access the current git state via:
# In the SeedSigner OS builder we know that we'll have the `git` shell
# commands available. We add extra fallbacks for local dev.
for get_version_fork_method in [
VersionUtils._get_version_fork_from_git_shell,
VersionUtils._get_version_fork_from_git_config,
@@ -268,6 +280,45 @@ class VersionUtils:
return None
@classmethod
def get_short_commit_hash(cls) -> str | None:
"""
Returns the short commit hash string.
Will be None if the local dev system has no git state available or if it only has
the .git/HEAD but is currently on a branch (not a tag or specific commit).
"""
if Settings.HOSTNAME == Settings.SEEDSIGNER_OS:
return VersionUtils._get_short_commit_hash_from_version_file()
full_commit_hash = None
if VersionUtils.is_github_actions_ci():
# In Github Actions CI the "SHA" env var should always be available
full_commit_hash = VersionUtils._get_full_commit_hash_from_github_actions_env_vars()
else:
# In the SeedSigner OS builder we know that we'll have the `git` shell
# commands available. We add extra fallbacks for local dev.
for get_full_commit_hash_method in [
VersionUtils._get_full_commit_hash_from_git_shell,
VersionUtils._get_full_commit_hash_from_git_HEAD,
]:
full_commit_hash = get_full_commit_hash_method()
if full_commit_hash is not None:
break
if full_commit_hash is None:
# `git` shell calls didn't work and HEAD might be on a branch, so dig
# deeper into the .git files to look up the commit hash via the branch
# name.
branch_name, expect_full_commit_hash_to_be_none = VersionUtils._read_git_HEAD_file()
if branch_name:
full_commit_hash = VersionUtils._get_full_commit_hash_from_git_refs_heads(branch_name)
if full_commit_hash is not None:
return full_commit_hash[:7]
@classmethod
def get_version_timestamp(cls) -> datetime:
"""
@@ -290,7 +341,10 @@ class VersionUtils:
return VersionUtils._get_version_timestamp_from_git_shell()
elif VersionUtils._is_seedsigner_os_builder_env():
# In the SeedSigner OS build environment `git` shell call should be available
# In the SeedSigner OS builder we know that we'll have the `git` shell
# commands available.
# Note: This is the only piece of version data that behaves differently
# between the SeedSigner OS builder and local dev.
return VersionUtils._get_version_timestamp_from_git_shell()
else:
@@ -299,48 +353,10 @@ class VersionUtils:
return VersionUtils._get_last_modified_timestamp_from_src_files()
@classmethod
def get_short_commit_hash(cls) -> str | None:
"""
Returns the short commit hash string.
Will be None if the local dev system has no git state available or if it only has
the .git/HEAD but is currently on a branch (not a tag or specific commit).
"""
if Settings.HOSTNAME == Settings.SEEDSIGNER_OS:
return VersionUtils._get_short_commit_hash_from_version_file()
full_commit_hash = None
if VersionUtils.is_github_actions_ci():
# In Github Actions CI the "SHA" env var should always be available
full_commit_hash = VersionUtils._get_full_commit_hash_from_github_actions_env_vars()
elif VersionUtils._is_seedsigner_os_builder_env():
# In the SeedSigner OS build environment `git` shell call should be available
full_commit_hash = VersionUtils._get_full_commit_hash_from_git_shell()
else:
# In local dev we try to access the current git state via:
for get_full_commit_hash_method in [
VersionUtils._get_full_commit_hash_from_git_shell,
VersionUtils._get_full_commit_hash_from_git_HEAD,
]:
full_commit_hash = get_full_commit_hash_method()
if full_commit_hash is not None:
break
if full_commit_hash is None:
# `git` shell calls didn't work and HEAD might be on a branch, so dig
# deeper into the .git files to look up the commit hash via the branch
# name.
branch_name, expect_full_commit_hash_to_be_none = VersionUtils._read_git_HEAD_file()
if branch_name:
full_commit_hash = VersionUtils._get_full_commit_hash_from_git_refs_heads(branch_name)
if full_commit_hash is not None:
return full_commit_hash[:7]
""" *************************************************************************************
Misc internal utility functions.
************************************************************************************* """
@classmethod
def _prefix_version_name(cls, version_name: str) -> str:
"""
@@ -353,6 +369,7 @@ class VersionUtils:
return version_name
""" *************************************************************************************
Reading data from the version.json file.
************************************************************************************* """
@@ -398,6 +415,16 @@ class VersionUtils:
return version_data.get(cls.VERSIONFILE_ATTR__FORK)
@classmethod
def _get_short_commit_hash_from_version_file(cls) -> str | None:
"""
Attempts to read the version.json and return the version commit hash.
"""
version_data = cls._read_version_file()
if version_data:
return version_data.get(cls.VERSIONFILE_ATTR__SHORT_COMMIT_HASH)
@classmethod
def _get_version_timestamp_from_version_file(cls) -> str | None:
"""
@@ -408,16 +435,6 @@ class VersionUtils:
return version_data.get(cls.VERSIONFILE_ATTR__TIMESTAMP)
@classmethod
def _get_short_commit_hash_from_version_file(cls) -> str | None:
"""
Attempts to read the version.json and return the version commit hash.
"""
version_data = cls._read_version_file()
if version_data:
return version_data.get(cls.VERSIONFILE_ATTR__COMMIT_HASH)
""" *************************************************************************************
Utilities used in the SeedSigner OS build environment and writing the version.json file.
@@ -425,20 +442,10 @@ class VersionUtils:
@classmethod
def _is_seedsigner_os_builder_env(cls) -> bool:
"""
Simple check to see if we're running in the SeedSigner OS build environment.
The `ENV_VAR__IS_SEEDSIGNER_OS_BUILDER` env var is set during the SeedSigner OS
build process.
"""
return os.getenv(cls.ENV_VAR__SEEDSIGNER_OS_BUILDER__VERSION_NAME) is not None
@classmethod
def _get_version_name_from_seedsigner_os_builder_env_var(cls) -> str | None:
"""
Primarily used during the SeedSigner OS build process to set the version name via env var.
This env var can also be set manually in local dev when needed.
e.g. SEEDSIGNER_VERSION_NAME=some_name python main.py
"""
return os.getenv(cls.ENV_VAR__SEEDSIGNER_OS_BUILDER__VERSION_NAME)
return os.getenv(cls.ENV_VAR__IS_SEEDSIGNER_OS_BUILDER) is not None
@@ -478,14 +485,14 @@ class VersionUtils:
************************************************************************************* """
@classmethod
@not_allowed_in_seedsigner_os
def _get_version_name_from_git_shell_branch(cls) -> str | None:
def _get_branch_name_from_git_shell(cls) -> str | None:
branch_name = os.popen("git branch --show-current 2> /dev/null").read()
return branch_name.strip() if branch_name else None
@classmethod
@not_allowed_in_seedsigner_os
def _get_version_name_from_git_shell_tag(cls) -> str | None:
def _get_tag_name_from_git_shell(cls) -> str | None:
# Only return a value if the current commit exactly corresponds with a tag.
# (`--points-at` defaults to the current HEAD)
tag_name = os.popen(f"git tag --points-at 2> /dev/null").read()
@@ -496,22 +503,34 @@ class VersionUtils:
@not_allowed_in_seedsigner_os
def _get_full_commit_hash_from_git_shell(cls) -> str | None:
"""
Attempts to get the current git commit hash via shell `git` commands.
Attempts to get the current full commit hash via shell `git` commands.
"""
commit_hash = os.popen("git rev-parse HEAD").read()
return commit_hash.strip() if commit_hash else None
@classmethod
@not_allowed_in_seedsigner_os
def _get_short_commit_hash_from_git_shell(cls) -> str | None:
"""
Attempts to get the current short commit hash via shell `git` commands.
"""
commit_hash = cls._get_full_commit_hash_from_git_shell()
return commit_hash[:7] if commit_hash else None
@classmethod
@not_allowed_in_seedsigner_os
def _get_version_name_from_git_shell(cls) -> str | None:
"""
Attempts to get the version name via shell `git` commands.
Note: If we have to fall back to the commit hash, we use the short version.
"""
return (
cls._get_version_name_from_git_shell_branch() or
cls._get_version_name_from_git_shell_tag() or
cls._get_full_commit_hash_from_git_shell()
cls._get_branch_name_from_git_shell() or
cls._get_tag_name_from_git_shell() or
cls._get_short_commit_hash_from_git_shell()
)
@@ -747,7 +766,7 @@ class VersionUtils:
# Shouldn't be possible
raise Exception("No python source files found in src/ directory")
return datetime.fromtimestamp(last_modified)
return datetime.fromtimestamp(last_modified).astimezone(timezone.utc).replace(tzinfo=None)
except Exception as e:
# Catch and log any unexpected errors but this isn't a mission-critical
+20 -12
View File
@@ -197,16 +197,19 @@ def generate_screenshots(locale):
value=SettingsConstants.OPTION__ENABLED
)
# Initialize the Version data to the most recent release
(version_name, version_timestamp) = VersionUtils._fetch_latest_seedsigner_release_tag()
if not version_name or not version_timestamp:
raise Exception("Could not fetch latest release version from GitHub")
Version.override_data(
version_name=version_name,
version_fork="SeedSigner", # main repo; screenshot should hide fork and commit hash
version_timestamp=version_timestamp,
short_commit_hash="abcd1234" # dummy value should be ignored
)
def reset_version_most_recent_release():
# Initialize the Version data to the most recent release
(version_name, version_timestamp) = VersionUtils._fetch_latest_seedsigner_release_tag()
if not version_name or not version_timestamp:
raise Exception("Could not fetch latest release version from GitHub")
new_values = {
VersionUtils.VERSIONFILE_ATTR__NAME: version_name,
VersionUtils.VERSIONFILE_ATTR__FORK: "SeedSigner", # main repo; screenshot should hide fork and commit hash
VersionUtils.VERSIONFILE_ATTR__TIMESTAMP: version_timestamp,
VersionUtils.VERSIONFILE_ATTR__SHORT_COMMIT_HASH: "abcd1234" # dummy value should be ignored
}
Version.override_data(**new_values)
reset_version_most_recent_release()
# Automatically populate all Settings options Views
settings_views_list = []
@@ -314,14 +317,19 @@ def generate_screenshots(locale):
def reset_version_to_local_git_state_cb():
# Normally, directly manipulating the singleton's internal instance is not
# allowed, but the screnshot generator is an atypical use case.
# allowed, but the screnshot generator is a special exception.
Version._instance = None
def reset_version_to_most_recent_release_cb():
reset_version_most_recent_release()
screenshot_sections = {
"Main Menu Views": [
ScreenshotConfig(OpeningSplashView, dict(force_partner_logos=True)),
ScreenshotConfig(OpeningSplashView, dict(force_partner_logos=False), screenshot_name="OpeningSplashView_no_partner_logos"),
ScreenshotConfig(OpeningSplashView, dict(force_partner_logos=True), run_before=reset_version_to_local_git_state_cb, screenshot_name="OpeningSplashView_current_git_state"),
ScreenshotConfig(MainMenuView),
ScreenshotConfig(MainMenuView, screenshot_name='MainMenuView_SDCardStateChangeToast_removed', toast_thread=SDCardStateChangeToastManagerThread(action=MicroSD.ACTION__REMOVED, activation_delay=0, duration=0)),
ScreenshotConfig(MainMenuView, screenshot_name='MainMenuView_SDCardStateChangeToast_inserted', toast_thread=SDCardStateChangeToastManagerThread(action=MicroSD.ACTION__INSERTED, activation_delay=0, duration=0)),
@@ -447,7 +455,7 @@ def generate_screenshots(locale):
"Settings Views": settings_views_list + [
ScreenshotConfig(settings_views.IOTestView),
ScreenshotConfig(settings_views.DonateView),
ScreenshotConfig(settings_views.VersionView),
ScreenshotConfig(settings_views.VersionView, run_before=reset_version_to_most_recent_release_cb),
ScreenshotConfig(settings_views.VersionView, run_before=reset_version_to_local_git_state_cb, screenshot_name="VersionView_current_git_state"),
ScreenshotConfig(settings_views.SettingsIngestSettingsQRView, dict(data=settingsqr_data_persistent), screenshot_name="SettingsIngestSettingsQRView_persistent"),
ScreenshotConfig(settings_views.SettingsIngestSettingsQRView, dict(data=settingsqr_data_not_persistent), screenshot_name="SettingsIngestSettingsQRView_not_persistent"),
+193 -129
View File
@@ -29,8 +29,8 @@ TEST__FULL_COMMIT_HASH = "c5efda306c60877191013a6093d92cd0bfcccec8"
TEST__VERSION_DICT = {
VersionUtils.VERSIONFILE_ATTR__NAME: VersionUtils._prefix_version_name(TEST__VERSION_NAME),
VersionUtils.VERSIONFILE_ATTR__FORK: TEST__VERSION_FORK,
VersionUtils.VERSIONFILE_ATTR__SHORT_COMMIT_HASH: TEST__SHORT_COMMIT_HASH,
VersionUtils.VERSIONFILE_ATTR__TIMESTAMP: TEST__VERSION_TIMESTAMP.isoformat(),
VersionUtils.VERSIONFILE_ATTR__COMMIT_HASH: TEST__SHORT_COMMIT_HASH,
}
# Mimic result of reading from version.json
@@ -68,7 +68,7 @@ class VersionBaseTest(BaseTest):
assumptions when running locally and when actually running in CI. Any test that
needs to simulate being in CI can override this env var as needed.
"""
with mock.patch.dict(os.environ, {VersionUtils.ENV_VAR__GITHUB_ACTIONS__IS_CI: "false"}):
with patch.dict(os.environ, {VersionUtils.ENV_VAR__GITHUB_ACTIONS__IS_CI: "false"}):
yield
@@ -193,7 +193,7 @@ class TestVersionUtils_BasicCalls(VersionBaseTest):
# If we don't provide the SeedSigner OS or Github Actions CI env vars, mock out
# the `git` shell commands via `mock_popen`, and mock out all .git/ file parsing,
# then the method should return the fallback warning note as the version name.
with mock.patch("builtins.open", side_effect=FileNotFoundError):
with patch("builtins.open", side_effect=FileNotFoundError):
result = VersionUtils.get_version_name()
assert "not detected" in result
@@ -206,7 +206,7 @@ class TestVersionUtils_BasicCalls(VersionBaseTest):
# If we don't provide the SeedSigner OS or Github Actions CI env vars, mock out
# the `git` shell commands via `mock_popen`, and mock out all .git/ file parsing,
# then the method should return None.
with mock.patch("builtins.open", side_effect=FileNotFoundError):
with patch("builtins.open", side_effect=FileNotFoundError):
VersionUtils.get_version_fork() is None
@@ -218,7 +218,7 @@ class TestVersionUtils_BasicCalls(VersionBaseTest):
# If we don't provide the SeedSigner OS or Github Actions CI env vars, mock out
# the `git` shell commands via `mock_popen`, and mock out all .git/ file parsing,
# then the method should return None.
with mock.patch("builtins.open", side_effect=FileNotFoundError):
with patch("builtins.open", side_effect=FileNotFoundError):
VersionUtils.get_short_commit_hash() is None
@@ -238,21 +238,20 @@ class TestVersionUtils_VersionFile(VersionBaseTest):
with patch("seedsigner.models.settings.Settings.HOSTNAME", Settings.SEEDSIGNER_OS):
assert VersionUtils.get_version_name() == VersionUtils._prefix_version_name(TEST__VERSION_NAME)
assert VersionUtils.get_version_fork() == TEST__VERSION_FORK
assert VersionUtils.get_version_timestamp() == TEST__VERSION_TIMESTAMP
assert VersionUtils.get_short_commit_hash() == TEST__SHORT_COMMIT_HASH
assert VersionUtils.get_version_timestamp() == TEST__VERSION_TIMESTAMP
# Expect errors if keys are missing from version.json
with mock.patch("builtins.open", mock.mock_open(read_data="{'some_key':'some_value'}")):
with patch("builtins.open", mock.mock_open(read_data="{'some_key':'some_value'}")):
with pytest.raises(Exception):
VersionUtils.get_version_name()
# This time it's the timestamp that's missing
with mock.patch("builtins.open", mock.mock_open(read_data=str({VersionUtils.VERSIONFILE_ATTR__NAME: TEST__VERSION_NAME}).replace("'", '"'))):
with patch("builtins.open", mock.mock_open(read_data=str({VersionUtils.VERSIONFILE_ATTR__NAME: TEST__VERSION_NAME}).replace("'", '"'))):
with pytest.raises(Exception):
VersionUtils.get_version_timestamp()
def test__read_version_file(self):
"""
Low-level test for reading the version.json file.
@@ -265,8 +264,8 @@ class TestVersionUtils_VersionFile(VersionBaseTest):
assert version_data is not None
assert version_data[VersionUtils.VERSIONFILE_ATTR__NAME] == VersionUtils._prefix_version_name(TEST__VERSION_NAME)
assert version_data[VersionUtils.VERSIONFILE_ATTR__FORK] == TEST__VERSION_FORK
assert version_data[VersionUtils.VERSIONFILE_ATTR__SHORT_COMMIT_HASH] == TEST__SHORT_COMMIT_HASH
assert version_data[VersionUtils.VERSIONFILE_ATTR__TIMESTAMP] == TEST__VERSION_TIMESTAMP.isoformat()
assert version_data[VersionUtils.VERSIONFILE_ATTR__COMMIT_HASH] == TEST__SHORT_COMMIT_HASH
def test__read_version_file__missing(self):
@@ -277,23 +276,14 @@ class TestVersionUtils_VersionFile(VersionBaseTest):
# The upstream dependent calls should also return None
assert VersionUtils._get_version_name_from_version_file() is None
assert VersionUtils._get_version_fork_from_version_file() is None
assert VersionUtils._get_version_timestamp_from_version_file() is None
assert VersionUtils._get_short_commit_hash_from_version_file() is None
assert VersionUtils._get_version_timestamp_from_version_file() is None
# Gracefully handle other unexpected exceptions
with patch("builtins.open", side_effect=Exception("Unexpected error")):
assert VersionUtils._read_version_file() is None
def test__get_version_name_from_seedsigner_os_builder_env_var(self):
assert os.environ.get(VersionUtils.ENV_VAR__SEEDSIGNER_OS_BUILDER__VERSION_NAME) is None
assert VersionUtils._get_version_name_from_seedsigner_os_builder_env_var() is None
with mock.patch.dict(os.environ, {VersionUtils.ENV_VAR__SEEDSIGNER_OS_BUILDER__VERSION_NAME: TEST__VERSION_NAME}):
result = VersionUtils._get_version_name_from_seedsigner_os_builder_env_var()
assert result == TEST__VERSION_NAME
class TestVersionUtils_GithubActions(VersionBaseTest):
def test_github_actions_env_vars(self):
@@ -305,9 +295,9 @@ class TestVersionUtils_GithubActions(VersionBaseTest):
pulled from the appropriate env vars.
"""
# CI uses some limited `git` shell calls; mock out the associated calls.
with mock.patch("seedsigner.helpers.version.VersionUtils._get_version_timestamp_from_git_shell", return_value=TEST__VERSION_TIMESTAMP):
with patch("seedsigner.helpers.version.VersionUtils._get_version_timestamp_from_git_shell", return_value=TEST__VERSION_TIMESTAMP):
# When running CI on a branch
with mock.patch.dict(os.environ, {
with patch.dict(os.environ, {
VersionUtils.ENV_VAR__GITHUB_ACTIONS__IS_CI: "true",
VersionUtils.ENV_VAR__GITHUB_ACTIONS__REF_NAME: TEST__VERSION_BRANCH,
VersionUtils.ENV_VAR__GITHUB_ACTIONS__SHA: TEST__FULL_COMMIT_HASH,
@@ -319,7 +309,7 @@ class TestVersionUtils_GithubActions(VersionBaseTest):
assert VersionUtils.get_version_timestamp() == TEST__VERSION_TIMESTAMP
# When running CI on a semantic tag
with mock.patch.dict(os.environ, {
with patch.dict(os.environ, {
VersionUtils.ENV_VAR__GITHUB_ACTIONS__IS_CI: "true",
VersionUtils.ENV_VAR__GITHUB_ACTIONS__REF_NAME: TEST__SEMANTIC_TAG,
}):
@@ -327,7 +317,7 @@ class TestVersionUtils_GithubActions(VersionBaseTest):
assert VersionUtils.get_version_name() == f"v{TEST__SEMANTIC_TAG}"
# When running CI on a generic tag
with mock.patch.dict(os.environ, {
with patch.dict(os.environ, {
VersionUtils.ENV_VAR__GITHUB_ACTIONS__IS_CI: "true",
VersionUtils.ENV_VAR__GITHUB_ACTIONS__REF_NAME: TEST__VERSION_TAG,
}):
@@ -337,7 +327,7 @@ class TestVersionUtils_GithubActions(VersionBaseTest):
# When running CI on a commit (detached HEAD) with no REF_NAME, the
# version_name should be the short commit hash.
# TODO: I don't think this scenario ever happens.
with mock.patch.dict(os.environ, {
with patch.dict(os.environ, {
VersionUtils.ENV_VAR__GITHUB_ACTIONS__IS_CI: "true",
VersionUtils.ENV_VAR__GITHUB_ACTIONS__REF_NAME: "",
VersionUtils.ENV_VAR__GITHUB_ACTIONS__SHA: TEST__FULL_COMMIT_HASH,
@@ -348,7 +338,7 @@ class TestVersionUtils_GithubActions(VersionBaseTest):
# raise error.
# Note: This scenario definitely would never happen. Just trying to get to
# 100% test coverage.
with mock.patch.dict(os.environ, {
with patch.dict(os.environ, {
VersionUtils.ENV_VAR__GITHUB_ACTIONS__IS_CI: "true",
VersionUtils.ENV_VAR__GITHUB_ACTIONS__REF_NAME: "",
VersionUtils.ENV_VAR__GITHUB_ACTIONS__SHA: "",
@@ -365,13 +355,13 @@ class TestVersionUtils_GithubActions(VersionBaseTest):
Note: Have to mock a value for ALL scenario variations here because this test will
actually run in Github Actions so the env var will already be present!
"""
with mock.patch.dict(os.environ, {VersionUtils.ENV_VAR__GITHUB_ACTIONS__IS_CI: "true"}):
with patch.dict(os.environ, {VersionUtils.ENV_VAR__GITHUB_ACTIONS__IS_CI: "true"}):
assert VersionUtils.is_github_actions_ci() is True
with mock.patch.dict(os.environ, {VersionUtils.ENV_VAR__GITHUB_ACTIONS__IS_CI: "false"}):
with patch.dict(os.environ, {VersionUtils.ENV_VAR__GITHUB_ACTIONS__IS_CI: "false"}):
assert VersionUtils.is_github_actions_ci() is False
with mock.patch.dict(os.environ, {VersionUtils.ENV_VAR__GITHUB_ACTIONS__IS_CI: "1"}):
with patch.dict(os.environ, {VersionUtils.ENV_VAR__GITHUB_ACTIONS__IS_CI: "1"}):
assert VersionUtils.is_github_actions_ci() is False
@@ -381,7 +371,7 @@ class TestVersionUtils_GithubActions(VersionBaseTest):
env vars when set.
"""
# Need to signal that we're in a GitHub Actions CI environment
with mock.patch.dict(os.environ, {
with patch.dict(os.environ, {
VersionUtils.ENV_VAR__GITHUB_ACTIONS__IS_CI: "true",
VersionUtils.ENV_VAR__GITHUB_ACTIONS__REF_NAME: "",
VersionUtils.ENV_VAR__GITHUB_ACTIONS__SHA: "",
@@ -389,12 +379,12 @@ class TestVersionUtils_GithubActions(VersionBaseTest):
assert VersionUtils._get_version_name_from_github_actions_env_vars() is None
# REF_NAME should be passed straight through
with mock.patch.dict(os.environ, {VersionUtils.ENV_VAR__GITHUB_ACTIONS__REF_NAME: TEST__VERSION_NAME}):
with patch.dict(os.environ, {VersionUtils.ENV_VAR__GITHUB_ACTIONS__REF_NAME: TEST__VERSION_NAME}):
result = VersionUtils._get_version_name_from_github_actions_env_vars()
assert result == TEST__VERSION_NAME
# Unlikely scenario: no REF_NAME but SHA is set; should return short commit hash
with mock.patch.dict(os.environ, {
with patch.dict(os.environ, {
VersionUtils.ENV_VAR__GITHUB_ACTIONS__REF_NAME: "",
VersionUtils.ENV_VAR__GITHUB_ACTIONS__SHA: TEST__FULL_COMMIT_HASH,
}):
@@ -410,30 +400,40 @@ class TestVersionUtils_SeedSignerOSBuilder(VersionBaseTest):
since all the downstream helper methods are tested in detail elsewhere.
"""
# Simulate running in the SeedSigner OS build environment
with mock.patch.dict(os.environ, {VersionUtils.ENV_VAR__SEEDSIGNER_OS_BUILDER__VERSION_NAME: TEST__VERSION_NAME}):
with patch.dict(os.environ, {VersionUtils.ENV_VAR__IS_SEEDSIGNER_OS_BUILDER: "1"}):
# SeedSigner OS builder uses some limited `git` shell calls; mock out the
# associated calls.
with mock.patch.multiple(
with patch.multiple(
"seedsigner.helpers.version.VersionUtils",
_get_version_timestamp_from_git_shell=Mock(return_value=TEST__VERSION_TIMESTAMP),
_get_version_name_from_git_shell=Mock(return_value=TEST__VERSION_NAME),
_get_version_fork_from_git_shell=Mock(return_value=TEST__VERSION_FORK),
_get_full_commit_hash_from_git_shell=Mock(return_value=TEST__FULL_COMMIT_HASH),
_get_version_timestamp_from_git_shell=Mock(return_value=TEST__VERSION_TIMESTAMP),
):
assert VersionUtils.get_version_name() == VersionUtils._prefix_version_name(TEST__VERSION_NAME)
assert VersionUtils.get_version_fork() == TEST__VERSION_FORK
assert VersionUtils.get_short_commit_hash() == TEST__SHORT_COMMIT_HASH
assert VersionUtils.get_version_timestamp() == TEST__VERSION_TIMESTAMP
# Meanwhile, mock out the timestamp from source files and ensure it's NOT used
with patch.object(
VersionUtils,
"_get_last_modified_timestamp_from_src_files",
Mock(return_value=datetime.now())
):
assert VersionUtils.get_version_name() == VersionUtils._prefix_version_name(TEST__VERSION_NAME)
assert VersionUtils.get_version_fork() == TEST__VERSION_FORK
assert VersionUtils.get_short_commit_hash() == TEST__SHORT_COMMIT_HASH
assert VersionUtils.get_version_timestamp() == TEST__VERSION_TIMESTAMP
# Verify that the source file timestamp method was NOT called
VersionUtils._get_last_modified_timestamp_from_src_files.assert_not_called()
def test_is_seedsigner_os_builder_env(self):
"""
is_seedsigner_os_builder_env should return True only when the
ENV_VAR__SEEDSIGNER_OS_BUILDER__VERSION_NAME env var is set.
ENV_VAR__IS_SEEDSIGNER_OS_BUILDER env var is set.
"""
with mock.patch.dict(os.environ, {}, clear=True):
with patch.dict(os.environ, {}, clear=True):
assert VersionUtils._is_seedsigner_os_builder_env() is False
with mock.patch.dict(os.environ, {VersionUtils.ENV_VAR__SEEDSIGNER_OS_BUILDER__VERSION_NAME: TEST__VERSION_NAME}):
with patch.dict(os.environ, {VersionUtils.ENV_VAR__IS_SEEDSIGNER_OS_BUILDER: "1"}):
assert VersionUtils._is_seedsigner_os_builder_env() is True
@@ -450,17 +450,29 @@ class TestVersionUtils_GitShell(VersionBaseTest):
Note that in local dev we use the last modified timestamp from the source files
rather than from git.
"""
with mock.patch.multiple(
with patch.multiple(
"seedsigner.helpers.version.VersionUtils",
_get_version_name_from_git_shell=Mock(return_value=TEST__VERSION_BRANCH),
_get_version_fork_from_git_shell=Mock(return_value=TEST__VERSION_FORK),
_get_last_modified_timestamp_from_src_files=Mock(return_value=TEST__VERSION_TIMESTAMP),
_get_full_commit_hash_from_git_shell=Mock(return_value=TEST__SHORT_COMMIT_HASH),
_get_last_modified_timestamp_from_src_files=Mock(return_value=TEST__VERSION_TIMESTAMP),
):
assert VersionUtils.get_version_name() == TEST__VERSION_BRANCH
assert VersionUtils.get_version_fork() == TEST__VERSION_FORK
assert VersionUtils.get_version_timestamp() == TEST__VERSION_TIMESTAMP
assert VersionUtils.get_short_commit_hash() == TEST__SHORT_COMMIT_HASH
# Mock out the git shell call that retrieves the timestamp.
with patch(
"seedsigner.helpers.version.VersionUtils._get_version_timestamp_from_git_shell",
return_value=None
) as mock_timestamp_from_git_shell:
# Verify that we're using the local dev code path
assert VersionUtils.is_github_actions_ci() is False
assert VersionUtils._is_seedsigner_os_builder_env() is False
assert VersionUtils.get_version_name() == TEST__VERSION_BRANCH
assert VersionUtils.get_version_fork() == TEST__VERSION_FORK
assert VersionUtils.get_short_commit_hash() == TEST__SHORT_COMMIT_HASH
assert VersionUtils.get_version_timestamp() == TEST__VERSION_TIMESTAMP
# Verify that the git shell timestamp method was NOT called
mock_timestamp_from_git_shell.assert_not_called()
def test__get_version_name_from_git_shell(self):
@@ -474,34 +486,36 @@ class TestVersionUtils_GitShell(VersionBaseTest):
assert result is None
# If we're on a branch, should return the branch name
with mock.patch.multiple(
with patch.multiple(
"seedsigner.helpers.version.VersionUtils",
_get_version_name_from_git_shell_branch=Mock(return_value=TEST__VERSION_BRANCH),
_get_version_name_from_git_shell_tag=Mock(return_value=TEST__VERSION_TAG),
_get_branch_name_from_git_shell=Mock(return_value=TEST__VERSION_BRANCH),
_get_tag_name_from_git_shell=Mock(return_value=TEST__VERSION_TAG),
_get_full_commit_hash_from_git_shell=Mock(return_value=TEST__FULL_COMMIT_HASH),
):
result = VersionUtils._get_version_name_from_git_shell()
assert result == TEST__VERSION_BRANCH
# If we're on a tag, the detached HEAD state wipes out the branch name
with mock.patch.multiple(
with patch.multiple(
"seedsigner.helpers.version.VersionUtils",
_get_version_name_from_git_shell_branch=Mock(return_value=None),
_get_version_name_from_git_shell_tag=Mock(return_value=TEST__VERSION_TAG),
_get_branch_name_from_git_shell=Mock(return_value=None),
_get_tag_name_from_git_shell=Mock(return_value=TEST__VERSION_TAG),
_get_full_commit_hash_from_git_shell=Mock(return_value=TEST__FULL_COMMIT_HASH),
):
result = VersionUtils._get_version_name_from_git_shell()
assert result == TEST__VERSION_TAG
# Similarly, if we're detached at a specific commit hash
with mock.patch.multiple(
# Similarly, if we're detached at a specific commit hash.
# Note: when falling back to the commit hash, we're expecting the SHORT commit
# hash.
with patch.multiple(
"seedsigner.helpers.version.VersionUtils",
_get_version_name_from_git_shell_branch=Mock(return_value=None),
_get_version_name_from_git_shell_tag=Mock(return_value=None),
_get_branch_name_from_git_shell=Mock(return_value=None),
_get_tag_name_from_git_shell=Mock(return_value=None),
_get_full_commit_hash_from_git_shell=Mock(return_value=TEST__FULL_COMMIT_HASH),
):
result = VersionUtils._get_version_name_from_git_shell()
assert result == TEST__FULL_COMMIT_HASH
assert result == TEST__SHORT_COMMIT_HASH
def test__get_version_fork_from_git_shell(self, mock_popen: Mock):
@@ -509,12 +523,32 @@ class TestVersionUtils_GitShell(VersionBaseTest):
Test that _get_version_fork_from_git_shell returns the expected repo owner from
the remote url.
"""
remote_url = "https://github.com/SeedSigner/seedsigner.git"
expected_fork = "SeedSigner"
for owner in ["SeedSigner", "seedsigner", "some-user"]:
remote_url = f"https://github.com/{owner}/seedsigner.git"
mock_popen.return_value.read.return_value = remote_url
result = VersionUtils._get_version_fork_from_git_shell()
assert result == expected_fork
mock_popen.return_value.read.return_value = remote_url
result = VersionUtils._get_version_fork_from_git_shell()
assert result == owner
def test__get_full_commit_hash_from_git_shell(self, mock_popen: Mock):
"""
Test that _get_full_commit_hash_from_git_shell returns the expected full commit hash.
"""
mock_popen.return_value.read.return_value = TEST__FULL_COMMIT_HASH
result = VersionUtils._get_full_commit_hash_from_git_shell()
assert result == TEST__FULL_COMMIT_HASH
def test__get_short_commit_hash_from_git_shell(self, mock_popen: Mock):
"""
Test that _get_short_commit_hash_from_git_shell returns the expected short commit hash.
"""
mock_popen.return_value.read.return_value = TEST__FULL_COMMIT_HASH
result = VersionUtils._get_short_commit_hash_from_git_shell()
assert result == TEST__SHORT_COMMIT_HASH
def test__get_version_timestamp_from_git_shell(self, mock_popen: Mock):
@@ -540,16 +574,6 @@ class TestVersionUtils_GitShell(VersionBaseTest):
assert VersionUtils._get_version_timestamp_from_git_shell() == expected_datetime
def test__get_full_commit_hash_from_git_shell(self, mock_popen: Mock):
"""
Test that _get_full_commit_hash_from_git_shell returns the expected short commit hash.
"""
mock_popen.return_value.read.return_value = TEST__SHORT_COMMIT_HASH
result = VersionUtils._get_full_commit_hash_from_git_shell()
assert result == TEST__SHORT_COMMIT_HASH
class TestVersionUtils_DotGitFiles(VersionBaseTest):
def test_local_dev_with_dot_git_dir_parsing(self, mock_popen: Mock):
@@ -567,37 +591,50 @@ class TestVersionUtils_DotGitFiles(VersionBaseTest):
"""
# If we're on a branch, getting the commit hash requires the
# .git/refs/heads/<branch> file.
with mock.patch.multiple(
with patch.multiple(
"seedsigner.helpers.version.VersionUtils",
_read_git_HEAD_file=Mock(return_value=(TEST__VERSION_BRANCH, None)),
_get_full_commit_hash_from_git_refs_heads=Mock(return_value=TEST__FULL_COMMIT_HASH),
_get_version_fork_from_git_config=Mock(return_value=TEST__VERSION_FORK),
_get_last_modified_timestamp_from_src_files=Mock(return_value=TEST__VERSION_TIMESTAMP),
):
assert VersionUtils.get_version_name() == TEST__VERSION_BRANCH
assert VersionUtils.get_version_fork() == TEST__VERSION_FORK
assert VersionUtils.get_version_timestamp() == TEST__VERSION_TIMESTAMP
assert VersionUtils.get_short_commit_hash() == TEST__SHORT_COMMIT_HASH
# Mock out the git shell call that retrieves the timestamp.
with patch(
"seedsigner.helpers.version.VersionUtils._get_version_timestamp_from_git_shell",
return_value=None
) as mock_timestamp_from_git_shell:
# Verify that we're using the local dev code path
assert VersionUtils.is_github_actions_ci() is False
assert VersionUtils._is_seedsigner_os_builder_env() is False
# If we're on a tag (detached HEAD), getting the commit hash requires
# checking the refs/tags for a matching tag.
with mock.patch.multiple(
"seedsigner.helpers.version.VersionUtils",
_read_git_HEAD_file=Mock(return_value=(None, TEST__FULL_COMMIT_HASH)),
_get_matching_tag_from_git_refs_tags=Mock(return_value=TEST__VERSION_TAG),
):
assert VersionUtils.get_version_name() == TEST__VERSION_TAG
assert VersionUtils.get_short_commit_hash() == TEST__SHORT_COMMIT_HASH
assert VersionUtils.get_version_name() == TEST__VERSION_BRANCH
assert VersionUtils.get_version_fork() == TEST__VERSION_FORK
assert VersionUtils.get_version_timestamp() == TEST__VERSION_TIMESTAMP
assert VersionUtils.get_short_commit_hash() == TEST__SHORT_COMMIT_HASH
# If we're on a commit hash (detached HEAD) with no matching tag, the
# version name should be the short commit hash.
with mock.patch.multiple(
"seedsigner.helpers.version.VersionUtils",
_read_git_HEAD_file=Mock(return_value=(None, TEST__FULL_COMMIT_HASH)),
_get_matching_tag_from_git_refs_tags=Mock(return_value=None),
):
assert VersionUtils.get_version_name() == TEST__SHORT_COMMIT_HASH
assert VersionUtils.get_short_commit_hash() == TEST__SHORT_COMMIT_HASH
# If we're on a tag (detached HEAD), getting the commit hash requires
# checking the refs/tags for a matching tag.
with patch.multiple(
"seedsigner.helpers.version.VersionUtils",
_read_git_HEAD_file=Mock(return_value=(None, TEST__FULL_COMMIT_HASH)),
_get_matching_tag_from_git_refs_tags=Mock(return_value=TEST__VERSION_TAG),
):
assert VersionUtils.get_version_name() == TEST__VERSION_TAG
assert VersionUtils.get_short_commit_hash() == TEST__SHORT_COMMIT_HASH
# If we're on a commit hash (detached HEAD) with no matching tag, the
# version name should be the short commit hash.
with patch.multiple(
"seedsigner.helpers.version.VersionUtils",
_read_git_HEAD_file=Mock(return_value=(None, TEST__FULL_COMMIT_HASH)),
_get_matching_tag_from_git_refs_tags=Mock(return_value=None),
):
assert VersionUtils.get_version_name() == TEST__SHORT_COMMIT_HASH
assert VersionUtils.get_short_commit_hash() == TEST__SHORT_COMMIT_HASH
# Verify that through all of this that the git shell timestamp method was
# NOT called.
mock_timestamp_from_git_shell.assert_not_called()
def test__get_dot_git_dir(self):
@@ -677,17 +714,17 @@ class TestVersionUtils_DotGitFiles(VersionBaseTest):
# We already tested _read_git_HEAD_file() above, so just mock scenarios here.
# On a branch:
with mock.patch.object(VersionUtils, '_read_git_HEAD_file', return_value=(TEST__VERSION_BRANCH, None)):
with patch.object(VersionUtils, '_read_git_HEAD_file', return_value=(TEST__VERSION_BRANCH, None)):
assert VersionUtils._get_version_name_from_git_HEAD() == TEST__VERSION_BRANCH
# Detached HEAD at commit hash, with matching tag
with mock.patch.object(VersionUtils, '_read_git_HEAD_file', return_value=(None, TEST__FULL_COMMIT_HASH)):
with mock.patch.object(VersionUtils, '_get_matching_tag_from_git_refs_tags', return_value=TEST__VERSION_NAME):
with patch.object(VersionUtils, '_read_git_HEAD_file', return_value=(None, TEST__FULL_COMMIT_HASH)):
with patch.object(VersionUtils, '_get_matching_tag_from_git_refs_tags', return_value=TEST__VERSION_NAME):
assert VersionUtils._get_version_name_from_git_HEAD() == TEST__VERSION_NAME
# Detached HEAD at commit hash, no matching tag; returns the short commit hash
with mock.patch.object(VersionUtils, '_read_git_HEAD_file', return_value=(None, TEST__FULL_COMMIT_HASH)):
with mock.patch.object(VersionUtils, '_get_matching_tag_from_git_refs_tags', return_value=None):
with patch.object(VersionUtils, '_read_git_HEAD_file', return_value=(None, TEST__FULL_COMMIT_HASH)):
with patch.object(VersionUtils, '_get_matching_tag_from_git_refs_tags', return_value=None):
assert VersionUtils._get_version_name_from_git_HEAD() == TEST__SHORT_COMMIT_HASH
@@ -695,7 +732,7 @@ class TestVersionUtils_DotGitFiles(VersionBaseTest):
# _get_commit_hash_from_git_HEAD is a trivial convenience function that relies on
# _read_git_HEAD_file which we've already tested. Just verify the expected outputs
# here.
with mock.patch.object(VersionUtils, '_read_git_HEAD_file', return_value=(None, TEST__SHORT_COMMIT_HASH)):
with patch.object(VersionUtils, '_read_git_HEAD_file', return_value=(None, TEST__SHORT_COMMIT_HASH)):
assert VersionUtils._get_full_commit_hash_from_git_HEAD() == TEST__SHORT_COMMIT_HASH
@@ -759,7 +796,7 @@ class TestVersionUtils_DotGitFiles(VersionBaseTest):
[next_section]
next_key = next_value
"""
with mock.patch("builtins.open", mock.mock_open(read_data=git_config)):
with patch("builtins.open", mock.mock_open(read_data=git_config)):
assert VersionUtils._get_version_fork_from_git_config() == expected_fork
# Missing origin section should return None
@@ -769,7 +806,7 @@ class TestVersionUtils_DotGitFiles(VersionBaseTest):
[next_section]
next_key = next_value
"""
with mock.patch("builtins.open", mock.mock_open(read_data=git_config_no_origin)):
with patch("builtins.open", mock.mock_open(read_data=git_config_no_origin)):
assert VersionUtils._get_version_fork_from_git_config() is None
# Handle malformed origin section (can't find url) should return None
@@ -779,11 +816,11 @@ class TestVersionUtils_DotGitFiles(VersionBaseTest):
[next_section]
next_key = next_value
"""
with mock.patch("builtins.open", mock.mock_open(read_data=git_config_malformed_origin)):
with patch("builtins.open", mock.mock_open(read_data=git_config_malformed_origin)):
assert VersionUtils._get_version_fork_from_git_config() is None
# If the git config file is missing, should return None
with mock.patch("builtins.open", side_effect=FileNotFoundError):
with patch("builtins.open", side_effect=FileNotFoundError):
assert VersionUtils._get_version_fork_from_git_config() is None
@@ -802,12 +839,13 @@ class TestVersionUtils_DotGitFiles(VersionBaseTest):
assert timestamp < datetime.now() + timedelta(days=30)
# Now mock out os.path.getmtime to force all files to have a known timestamp
expected_timestamp = datetime(2025, 12, 23, 0, 0, 0)
with mock.patch("os.path.getmtime", return_value=expected_timestamp.timestamp()):
assert VersionUtils._get_last_modified_timestamp_from_src_files() == expected_timestamp
local_timestamp = datetime(2025, 12, 23, 0, 0, 0)
with patch("os.path.getmtime", return_value=local_timestamp.timestamp()):
# Note that the result will be converted to UTC with no tzinfo
assert VersionUtils._get_last_modified_timestamp_from_src_files() == local_timestamp.astimezone(timezone.utc).replace(tzinfo=None)
# Mock out os.walk() to simulate no .py files found
with mock.patch("os.walk", return_value=[]):
with patch("os.walk", return_value=[]):
assert VersionUtils._get_last_modified_timestamp_from_src_files() is None
@@ -833,7 +871,7 @@ class TestVersionUtils_Misc(VersionBaseTest):
fake_response.status = 200
fake_response.read.return_value = json.dumps(latest_releases_response_dict).encode('utf-8')
with mock.patch("urllib.request.urlopen", return_value=fake_response):
with patch("urllib.request.urlopen", return_value=fake_response):
release_tag, release_timestamp = VersionUtils._fetch_latest_seedsigner_release_tag()
# mock_popen returns empty string, which mimics not having local git data to
@@ -859,13 +897,13 @@ class TestVersionUtils_Misc(VersionBaseTest):
# Should gracefully handle HTTP errors
fake_error_response = Mock()
fake_error_response.status = 404
with mock.patch("urllib.request.urlopen", return_value=fake_error_response):
with patch("urllib.request.urlopen", return_value=fake_error_response):
release_tag, release_timestamp = VersionUtils._fetch_latest_seedsigner_release_tag()
assert release_tag is None
assert release_timestamp is None
# And other exceptions
with mock.patch("urllib.request.urlopen", side_effect=Exception("Network error")):
with patch("urllib.request.urlopen", side_effect=Exception("Network error")):
release_tag, release_timestamp = VersionUtils._fetch_latest_seedsigner_release_tag()
assert release_tag is None
assert release_timestamp is None
@@ -884,7 +922,7 @@ class TestVersion(VersionBaseTest):
with patch("seedsigner.models.settings.Settings.HOSTNAME", Settings.SEEDSIGNER_OS):
Version.get_version_name() == TEST__VERSION_DICT[VersionUtils.VERSIONFILE_ATTR__NAME]
Version.get_version_fork() == TEST__VERSION_DICT[VersionUtils.VERSIONFILE_ATTR__FORK]
Version.get_short_commit_hash() == TEST__VERSION_DICT[VersionUtils.VERSIONFILE_ATTR__COMMIT_HASH]
Version.get_short_commit_hash() == TEST__VERSION_DICT[VersionUtils.VERSIONFILE_ATTR__SHORT_COMMIT_HASH]
Version.get_version_timestamp() == TEST__VERSION_TIMESTAMP
@@ -893,17 +931,12 @@ class TestVersion(VersionBaseTest):
Test that we can override the version data via the Version.override_version_data()
method.
"""
override_name = "v9.9.9-test"
override_fork = "TestFork"
override_commit_hash = "abcd123"
override_timestamp = datetime(2030, 1, 1, 0, 0, 0)
# Initially the version data is pulled from the usual sources
self.write_test_version_file()
with patch("seedsigner.models.settings.Settings.HOSTNAME", Settings.SEEDSIGNER_OS):
assert Version.get_version_name() == TEST__VERSION_DICT[VersionUtils.VERSIONFILE_ATTR__NAME]
assert Version.get_version_fork() == TEST__VERSION_DICT[VersionUtils.VERSIONFILE_ATTR__FORK]
assert Version.get_short_commit_hash() == TEST__VERSION_DICT[VersionUtils.VERSIONFILE_ATTR__COMMIT_HASH]
assert Version.get_short_commit_hash() == TEST__VERSION_DICT[VersionUtils.VERSIONFILE_ATTR__SHORT_COMMIT_HASH]
assert Version.get_version_timestamp() == TEST__VERSION_TIMESTAMP
# While we're in the mocked SeedSigner OS environment, verify that the
@@ -911,19 +944,33 @@ class TestVersion(VersionBaseTest):
with pytest.raises(NotAllowedInSeedSignerOS):
Version.override_data()
override_name = "v9.9.9-test"
override_fork = "TestFork"
override_commit_hash = "abcd123"
override_timestamp = datetime(2030, 1, 1, 0, 0, 0)
override_dict = {
VersionUtils.VERSIONFILE_ATTR__NAME: override_name,
VersionUtils.VERSIONFILE_ATTR__FORK: override_fork,
VersionUtils.VERSIONFILE_ATTR__SHORT_COMMIT_HASH: override_commit_hash,
VersionUtils.VERSIONFILE_ATTR__TIMESTAMP: override_timestamp,
}
# No longer in the mocked SeedSigner OS environment; should be allowed now.
Version.override_data(
version_name=override_name,
version_fork=override_fork,
short_commit_hash=override_commit_hash,
version_timestamp=override_timestamp,
)
Version.override_data(**override_dict)
assert Version.get_version_name() == override_name
assert Version.get_version_fork() == override_fork
assert Version.get_short_commit_hash() == override_commit_hash
assert Version.get_version_timestamp() == override_timestamp
# No overrides specified should result in no values changed.
Version.override_data()
assert Version.get_version_name() == override_name
assert Version.get_version_fork() == override_fork
assert Version.get_short_commit_hash() == override_commit_hash
assert Version.get_version_timestamp() == override_timestamp
def test_to_dict(self):
"""
@@ -931,8 +978,25 @@ class TestVersion(VersionBaseTest):
"""
self.write_test_version_file()
with patch("seedsigner.models.settings.Settings.HOSTNAME", Settings.SEEDSIGNER_OS):
# In SeedSigner OS, the version data comes from the version file.
assert Version.to_dict() == TEST__VERSION_DICT
extra_attr = "extra"
different_dict = {
VersionUtils.VERSIONFILE_ATTR__NAME: "v1.2.3-different",
VersionUtils.VERSIONFILE_ATTR__FORK: "DifferentFork",
VersionUtils.VERSIONFILE_ATTR__SHORT_COMMIT_HASH: "differenthash",
VersionUtils.VERSIONFILE_ATTR__TIMESTAMP: datetime(2026, 1, 1, 0, 0, 0), # stored internally as a datetime
extra_attr: "should be ignored",
}
Version.override_data(**different_dict)
output_dict = Version.to_dict()
assert output_dict[VersionUtils.VERSIONFILE_ATTR__NAME] == different_dict[VersionUtils.VERSIONFILE_ATTR__NAME]
assert output_dict[VersionUtils.VERSIONFILE_ATTR__FORK] == different_dict[VersionUtils.VERSIONFILE_ATTR__FORK]
assert output_dict[VersionUtils.VERSIONFILE_ATTR__SHORT_COMMIT_HASH] == different_dict[VersionUtils.VERSIONFILE_ATTR__SHORT_COMMIT_HASH]
assert output_dict[VersionUtils.VERSIONFILE_ATTR__TIMESTAMP] == different_dict[VersionUtils.VERSIONFILE_ATTR__TIMESTAMP].isoformat() # returned as a string
assert extra_attr not in output_dict
class TestNotAllowedInSeedSignerOSDecorator(BaseTest):
@@ -949,14 +1013,14 @@ class TestNotAllowedInSeedSignerOSDecorator(BaseTest):
if we run a decorated function while in SeedSigner OS.
"""
# Patch over the Settings.HOSTNAME value to simulate running in SeedSigner OS
with mock.patch("seedsigner.models.settings.Settings.HOSTNAME", Settings.SEEDSIGNER_OS):
with patch("seedsigner.models.settings.Settings.HOSTNAME", Settings.SEEDSIGNER_OS):
with pytest.raises(NotAllowedInSeedSignerOS):
self.dummy_function()
def test_allowed_outside_seedsigner_os(self):
# Now try with any other HOSTNAME
with mock.patch("seedsigner.models.settings.Settings.HOSTNAME", "my_dev_machine"):
with patch("seedsigner.models.settings.Settings.HOSTNAME", "my_dev_machine"):
assert self.dummy_function() == self.SUCCESS
+20 -33
View File
@@ -8,53 +8,40 @@ from seedsigner.helpers.version import Version, VersionUtils
CLI utility to extract the current version data and write to
`src/seedsigner/version.json`. Primarily used by the SeedSigner OS build process.
SeedSigner OS lifecycle:
* Build process runs this script to generate version.json.
* version.json is included in the SeedSigner OS image.
* SeedSigner OS reads version.json at runtime.
Notes:
* The SeedSigner OS build environment already relies on `git` being installed.
* This script can also be run in local dev but `git` shell commands are required.
* This script can also be run in local dev but note slight difference in how
version_timestamp is gathered.
* Tries to pull current version status via `git` shell commands, but has fallbacks
to directly parse .git/ files.
Version data:
* version_name:
* Check for the SEEDSIGNER_VERSION_NAME env var (provided in SeedSigner OS build
env).
* Will be the branch, tag, or commit hash being built.
* If running in local dev instead, this script will try to populate that env var
using `git` shell commands:
* Current git branch name
* Current git tag name
* Current git commit hash
* version_name: Retrieves the current git status by checking, in order:
* Current git branch name
* Current git tag name
* Current git commit hash
* version_fork:
* Pulls the current repo owner from the `git remote` shell command.
* version_timestamp:
* Pulls last git commit time from `git log`.
* The current repo owner.
* short_commit_hash:
* Pulls current git commit hash from `git` shell command.
* Current git short commit hash.
* version_timestamp:
* SeedSigner OS builder: Pulls last git commit time for the checked out
branch/tag/commit.
* Local dev: Scans the source python files for the most recent modified time.
"""
if __name__ == "__main__":
is_seedsigner_os_builder = VersionUtils._is_seedsigner_os_builder_env()
if not is_seedsigner_os_builder:
# Pull version_name from the current git state via `git` shell commands
version_name = VersionUtils._get_version_name_from_git_shell()
# Temporarily set the env var
os.environ[VersionUtils.ENV_VAR__SEEDSIGNER_OS_BUILDER__VERSION_NAME] = version_name
# As soon as `Version` is instantiated, it will gather all the version data
# according to the logic in `VersionUtils`.
version_info = Version.get_instance().to_dict()
# Write the version.json file.
version_file_path = VersionUtils._get_version_file_path()
with open(version_file_path, "w") as f:
json.dump(version_info, f, indent=4)
print(f"Wrote version info to: {version_file_path}")
print(json.dumps(version_info, indent=4))
# Clean up the temp env var if needed
if not is_seedsigner_os_builder:
del os.environ[VersionUtils.ENV_VAR__SEEDSIGNER_OS_BUILDER__VERSION_NAME]