What changed, and why it matters
This commit is a routine internal refactor of how the app reads its own version information (version name, fork, timestamp, commit hash). It consolidates singleton setup, renames constants and methods, adds GitHub Actions CI detection, and expands unit tests to reach 100% code coverage. There is no direct evidence in the commit that this fixes a security vulnerability or introduces a new attack path. The changes are mostly about making version reporting more reliable and testable.
No immediate security action required. Treat as normal code-quality/test-coverage refactor. If reviewing for release integrity, verify that GitHub Actions-derived version metadata cannot be manipulated by forked repository builds or pull-request builds, but that is a build-process concern not demonstrated by this diff.
Security signals we found
Refactor of version metadata singleton with no security claims
Addition of GitHub Actions CI environment variable parsing for version info
Removal of test-only reset/override methods (reduces test-only attack surface slightly)
Use of os.popen('git ...') and os.getenv remains unchanged in risk posture
Evidence from the diff
The patch refactors src/seedsigner/helpers/version.py: Version.get_instance() now lazily initializes the singleton instead of using a separate configure_instance(); test-only helpers reset_instance() and override_data() are removed; constants are renamed (e.g., ATTR__VERSION_NAME -> VERSIONFILE_ATTR__NAME); GitHub Actions env vars are now a first-class source for version name/fork/commit/timestamp; commit-hash helpers now consistently work with full hashes and truncate to 7 chars at the end. tools/write_versionfile.py and tests/test_version.py are updated to match. No cryptographic, input-validation, or privilege changes are visible. The only externally observable behavioral change is that CI builds will report versions derived from GITHUB_REF_NAME/GITHUB_SHA/GITHUB_REPOSITORY_OWNER, and local-dev fallback logic for commit hashes was reordered.
Changed components
src/seedsigner/helpers/version.pytests/test_version.pytools/write_versionfile.pyInspect captured patch +558 / −262
diff --git a/src/seedsigner/helpers/version.py b/src/seedsigner/helpers/version.py
index cb6644d..bf4b246 100644
--- a/src/seedsigner/helpers/version.py
+++ b/src/seedsigner/helpers/version.py
@@ -90,53 +90,20 @@ class Version(Singleton):
@classmethod
def get_instance(cls):
- # This is the only way to access the one and only instance
- if cls._instance:
- return cls._instance
- else:
+ """ This is the only way to access the one and only instance. """
+ if not cls._instance:
# Instantiate the one and only Version instance
- return cls.configure_instance()
-
-
- @classmethod
- def configure_instance(cls):
- # Must be called before the first get_instance() call
- if cls._instance:
- raise Exception("Instance already configured")
-
- # Create the one and only Version instance
- version = cls.__new__(cls)
- cls._instance = version
-
- # Populate version data
- version._version_name = VersionUtils.get_version_name()
- version._version_fork = VersionUtils.get_version_fork()
- version._version_timestamp = VersionUtils.get_version_timestamp()
- version._version_commit_hash = VersionUtils.get_version_commit_hash()
-
+ version = cls.__new__(cls)
+ cls._instance = version
+
+ # Populate version data
+ version._version_name = VersionUtils.get_version_name()
+ version._version_fork = VersionUtils.get_version_fork()
+ version._version_timestamp = VersionUtils.get_version_timestamp()
+ version._version_commit_hash = VersionUtils.get_short_commit_hash()
return cls._instance
- @classmethod
- @not_allowed_in_seedsigner_os
- def reset_instance(cls):
- """ Resets the singleton instance. Only used by the test suite between tests. """
- cls._instance = None
-
-
- @classmethod
- @not_allowed_in_seedsigner_os
- def override_data(cls, version_name, version_fork, version_timestamp, version_commit_hash):
- """
- Only used by the test suite to manually change version data between tests.
- """
- instance = cls.get_instance()
- instance._version_name = version_name
- instance._version_fork = version_fork
- instance._version_timestamp = version_timestamp
- instance._version_commit_hash = version_commit_hash
-
-
@classmethod
def get_version_name(cls) -> str | None:
return cls.get_instance()._version_name
@@ -178,13 +145,17 @@ class VersionUtils:
* One external http GET to github to get the most recent release tag.
********************************************************************************* """
- ENV_VAR__SEEDSIGNER_VERSION_NAME = "SEEDSIGNER_VERSION_NAME"
- VERSION_FILENAME = "version.json"
+ ENV_VAR__SEEDSIGNER_OS_BUILDER__VERSION_NAME = "SEEDSIGNER_VERSION_NAME"
+ ENV_VAR__GITHUB_ACTIONS__IS_CI = "CI"
+ ENV_VAR__GITHUB_ACTIONS__REF_NAME = "GITHUB_REF_NAME"
+ ENV_VAR__GITHUB_ACTIONS__SHA = "GITHUB_SHA"
+ ENV_VAR__GITHUB_ACTIONS__REPOSITORY_OWNER = "GITHUB_REPOSITORY_OWNER"
DOT_GIT_DIR_NAME = ".git" # defined to facilitate mocking in tests
- ATTR__VERSION_NAME = "name"
- ATTR__VERSION_FORK = "fork"
- ATTR__VERSION_TIMESTAMP = "timestamp"
- ATTR__VERSION_COMMIT_HASH = "commit_hash"
+ VERSIONFILE__FILENAME = "version.json"
+ VERSIONFILE_ATTR__NAME = "name"
+ VERSIONFILE_ATTR__FORK = "fork"
+ VERSIONFILE_ATTR__TIMESTAMP = "timestamp"
+ VERSIONFILE_ATTR__COMMIT_HASH = "commit_hash"
""" *************************************************************************************
@@ -207,7 +178,7 @@ class VersionUtils:
elif VersionUtils.is_github_actions_ci():
# In Github Actions CI, try to get the version name from env vars
- version_name = VersionUtils.get_version_name_from_github_actions_env_vars()
+ version_name = VersionUtils._get_version_name_from_github_actions_env_vars()
if version_name is not None:
return VersionUtils._prefix_version_name(version_name)
else:
@@ -241,6 +212,11 @@ class VersionUtils:
# The SeedSigner OS build process generates the version.json file which will
# already contain the fork name.
return VersionUtils._get_version_fork_from_version_file()
+
+ elif VersionUtils.is_github_actions_ci():
+ # In Github Actions CI, try to get the version name from env vars
+ return VersionUtils._get_version_fork_from_github_actions_env_vars()
+
else:
# In local dev we try to access the current git state via:
for get_version_fork_method in [
@@ -270,13 +246,17 @@ class VersionUtils:
raise Exception("Could not read the version timestamp from the version.json file.")
return datetime.fromisoformat(version_timestamp)
+ elif VersionUtils.is_github_actions_ci():
+ # In Github Actions CI `git` shell call should be available
+ return VersionUtils._get_version_timestamp_from_git_shell()
+
else:
# In local dev we use the last modified time of the source python files
return VersionUtils._get_version_timestamp_from_src_files()
@classmethod
- def get_version_commit_hash(cls) -> str | None:
+ def get_short_commit_hash(cls) -> str | None:
"""
Returns the short commit hash string.
@@ -284,25 +264,33 @@ class VersionUtils:
the .git/HEAD but is currently on a branch (not a tag or specific commit).
"""
if Settings.HOSTNAME == Settings.SEEDSIGNER_OS:
- return VersionUtils._get_version_commit_hash_from_version_file()
+ 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 local dev we try to access the current git state via:
- for get_commit_hash_method in [
- VersionUtils._get_version_commit_hash_from_git_shell,
- VersionUtils._get_commit_hash_from_git_HEAD,
+ for get_full_commit_hash_method in [
+ VersionUtils._get_full_commit_hash_from_git_shell,
+ VersionUtils._get_full_commit_hash_from_git_HEAD,
]:
- commit_hash = get_commit_hash_method()
- if commit_hash is not None:
- return commit_hash[:7]
+ 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 we're still here, we can try looking up based on the branch name
- branch_name = VersionUtils._get_version_name_from_git_shell_branch()
- if not branch_name:
- branch_name, commit_hash = VersionUtils._read_git_HEAD_file()
- if branch_name:
- commit_hash = VersionUtils._get_commit_hash_from_git_refs_heads(branch_name)
- if commit_hash is not None:
- return commit_hash[:7]
+ if full_commit_hash is not None:
+ return full_commit_hash[:7]
@classmethod
@@ -324,7 +312,7 @@ class VersionUtils:
def _get_version_file_path(cls) -> str:
# Have to back out of this file's location in the "helpers" dir to the main
# "seedsigner" dir.
- return os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", cls.VERSION_FILENAME))
+ return os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", cls.VERSIONFILE__FILENAME))
@classmethod
@@ -349,7 +337,7 @@ class VersionUtils:
"""
version_data = cls._read_version_file()
if version_data:
- return version_data.get(cls.ATTR__VERSION_NAME)
+ return version_data.get(cls.VERSIONFILE_ATTR__NAME)
@classmethod
@@ -359,7 +347,7 @@ class VersionUtils:
"""
version_data = cls._read_version_file()
if version_data:
- return version_data.get(cls.ATTR__VERSION_FORK)
+ return version_data.get(cls.VERSIONFILE_ATTR__FORK)
@classmethod
@@ -369,17 +357,17 @@ class VersionUtils:
"""
version_data = cls._read_version_file()
if version_data:
- return version_data.get(cls.ATTR__VERSION_TIMESTAMP)
+ return version_data.get(cls.VERSIONFILE_ATTR__TIMESTAMP)
@classmethod
- def _get_version_commit_hash_from_version_file(cls) -> str | None:
+ 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.ATTR__VERSION_COMMIT_HASH)
+ return version_data.get(cls.VERSIONFILE_ATTR__COMMIT_HASH)
@@ -394,18 +382,31 @@ class VersionUtils:
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_VERSION_NAME)
+ return os.getenv(cls.ENV_VAR__SEEDSIGNER_OS_BUILDER__VERSION_NAME)
@classmethod
def is_github_actions_ci(cls) -> bool:
- return os.getenv("CI") == "true"
+ return os.getenv(cls.ENV_VAR__GITHUB_ACTIONS__IS_CI) == "true"
@classmethod
- def get_version_name_from_github_actions_env_vars(cls) -> str | None:
+ def _get_version_name_from_github_actions_env_vars(cls) -> str | None:
# REF_NAME will be the branch or tag name; SHA is the full commit hash
- return os.getenv("GITHUB_REF_NAME") or os.getenv("GITHUB_SHA")[:7]
+ # TODO: Will REF_NAME ever be missing?
+ ref_name = os.getenv(cls.ENV_VAR__GITHUB_ACTIONS__REF_NAME)
+ sha = os.getenv(cls.ENV_VAR__GITHUB_ACTIONS__SHA)
+ return ref_name or (sha[:7] if sha else None)
+
+
+ @classmethod
+ def _get_version_fork_from_github_actions_env_vars(cls) -> str | None:
+ return os.getenv("GITHUB_REPOSITORY_OWNER")
+
+
+ @classmethod
+ def _get_full_commit_hash_from_github_actions_env_vars(cls) -> str | None:
+ return os.getenv(cls.ENV_VAR__GITHUB_ACTIONS__SHA)
@@ -431,7 +432,7 @@ class VersionUtils:
@not_allowed_in_seedsigner_os
def _read_git_HEAD_file(cls) -> tuple[str | None, str | None]:
"""
- Reads the .git/HEAD file and returns a tuple of (branch_name, commit_hash) where
+ Reads the .git/HEAD file and returns a tuple of (branch_name, full_commit_hash) where
only one will have a value.
If there is no .git/HEAD file detected, both values will be None.
@@ -439,7 +440,7 @@ class VersionUtils:
git_HEAD_path = os.path.join(cls._get_dot_git_dir(), "HEAD")
branch_name = None
- commit_hash = None
+ full_commit_hash = None
try:
with open(git_HEAD_path, "r") as f:
git_ref = f.read().strip()
@@ -448,14 +449,14 @@ class VersionUtils:
branch_name = git_ref.split("/")[-1]
else:
# If we're on a detached HEAD, the contents will just be the current
- # commit hash.
- commit_hash = git_ref
+ # full commit hash.
+ full_commit_hash = git_ref
except FileNotFoundError as e:
logger.debug(f"Ignoring {e}")
except Exception as e:
# Something unexpected happened. Flag it but keep going.
logger.error(traceback.format_exc())
- return (branch_name, commit_hash)
+ return (branch_name, full_commit_hash)
@classmethod
@@ -502,10 +503,10 @@ class VersionUtils:
@classmethod
@not_allowed_in_seedsigner_os
- def _get_commit_hash_from_git_HEAD(cls) -> str | None:
+ def _get_full_commit_hash_from_git_HEAD(cls) -> str | None:
"""
Reads the .git/HEAD file and, depending on the local dev git state, returns
- the short commit hash. Will only return a value if HEAD is detached (on a tag or
+ the full commit hash. Will only return a value if HEAD is detached (on a tag or
on a specific commit).
"""
branch_name, commit_hash = VersionUtils._read_git_HEAD_file()
@@ -514,10 +515,10 @@ class VersionUtils:
@classmethod
@not_allowed_in_seedsigner_os
- def _get_commit_hash_from_git_refs_heads(cls, branch_name: str) -> str | None:
+ def _get_full_commit_hash_from_git_refs_heads(cls, branch_name: str) -> str | None:
"""
- Reads the .git/refs/heads/<branch_name> file to get the current commit hash
- for the given branch.
+ When HEAD is on a branch, it doesn't tell us the commit hash directly. So we have
+ to look it up here in .git/refs/heads/<branch_name>.
"""
try:
git_ref_path = os.path.join(cls._get_dot_git_dir(), "refs", "heads", branch_name)
@@ -573,10 +574,11 @@ class VersionUtils:
lines = f.readlines()
in_origin_section = False
for line in lines:
+ line = line.strip()
if line.startswith("[remote \"origin\"]"):
# Next lines fetched will be the ones we care about
in_origin_section = True
- elif in_origin_section and line.strip().startswith("url ="):
+ elif in_origin_section and line.startswith("url ="):
# Found our "origin" url
remote_url = line.split("=", 1)[1].strip()
return cls._parse_git_remote_url(remote_url)
@@ -652,8 +654,11 @@ class VersionUtils:
@classmethod
@not_allowed_in_seedsigner_os
- def _get_version_name_from_git_shell_commit_hash(cls) -> str | None:
- commit_hash = os.popen("git rev-parse --short HEAD 2> /dev/null").read()
+ def _get_full_commit_hash_from_git_shell(cls) -> str | None:
+ """
+ Attempts to get the current git commit hash via shell `git` commands.
+ """
+ commit_hash = os.popen("git rev-parse HEAD").read()
return commit_hash.strip() if commit_hash else None
@@ -666,7 +671,7 @@ class VersionUtils:
return (
cls._get_version_name_from_git_shell_branch() or
cls._get_version_name_from_git_shell_tag() or
- cls._get_version_name_from_git_shell_commit_hash()
+ cls._get_full_commit_hash_from_git_shell()
)
@@ -678,25 +683,16 @@ class VersionUtils:
"""
# We expect to at least have a remote named "origin"
remote_url = os.popen("git remote get-url origin 2> /dev/null").read().strip()
- return cls._parse_git_remote_url(remote_url)
+ return cls._parse_git_remote_url(remote_url) if remote_url else None
@classmethod
@not_allowed_in_seedsigner_os
def _get_version_timestamp_from_git_shell(cls) -> datetime | None:
version_timestamp = os.popen("git log -1 --format=%cI").read().strip()
- # Parse the timestamp, ensure that it's in UTC, and omit tz info
- return datetime.fromisoformat(version_timestamp).astimezone(timezone.utc).replace(tzinfo=None)
-
-
- @classmethod
- @not_allowed_in_seedsigner_os
- def _get_version_commit_hash_from_git_shell(cls) -> str | None:
- """
- Attempts to get the current git commit hash via shell `git` commands.
- """
- commit_hash = os.popen("git rev-parse --short HEAD").read()
- return commit_hash.strip() if commit_hash else None
+ if version_timestamp:
+ # Parse the timestamp, ensure that it's in UTC, and omit tz info
+ return datetime.fromisoformat(version_timestamp).astimezone(timezone.utc).replace(tzinfo=None)
diff --git a/tests/test_version.py b/tests/test_version.py
index bdfc621..aec7583 100644
--- a/tests/test_version.py
+++ b/tests/test_version.py
@@ -14,19 +14,23 @@ from seedsigner.models.settings import Settings
# overrides
-TEST__VERSION_FILE_NAME = "version-test.json"
+TEST__VERSIONFILE__FILENAME = "version-test.json"
TEST__DOT_GIT_DIR_NAME = f"dot-git-test"
# Reusable test data
TEST__VERSION_NAME = "1.2.3" # Will require VersionUtils._prefix_version_name when verifying results
TEST__VERSION_FORK = "some_repo_owner"
-TEST__VERSION_TIMESTAMP = datetime.now()
-TEST__VERSION_COMMIT_HASH = "abcd123"
+TEST__VERSION_TIMESTAMP = datetime.now().astimezone(timezone.utc).replace(tzinfo=None)
+TEST__VERSION_BRANCH = "some_test_branch"
+TEST__VERSION_TAG = "some_test_tag"
+TEST__SEMANTIC_TAG = "1.2.3-rc1"
+TEST__SHORT_COMMIT_HASH = "c5efda3"
+TEST__FULL_COMMIT_HASH = "c5efda306c60877191013a6093d92cd0bfcccec8"
TEST__VERSION_DICT = {
- VersionUtils.ATTR__VERSION_NAME: TEST__VERSION_NAME,
- VersionUtils.ATTR__VERSION_FORK: TEST__VERSION_FORK,
- VersionUtils.ATTR__VERSION_TIMESTAMP: TEST__VERSION_TIMESTAMP.isoformat(),
- VersionUtils.ATTR__VERSION_COMMIT_HASH: TEST__VERSION_COMMIT_HASH,
+ VersionUtils.VERSIONFILE_ATTR__NAME: TEST__VERSION_NAME,
+ VersionUtils.VERSIONFILE_ATTR__FORK: TEST__VERSION_FORK,
+ VersionUtils.VERSIONFILE_ATTR__TIMESTAMP: TEST__VERSION_TIMESTAMP.isoformat(),
+ VersionUtils.VERSIONFILE_ATTR__COMMIT_HASH: TEST__SHORT_COMMIT_HASH,
}
# Mimic result of reading from version.json
@@ -38,12 +42,12 @@ class VersionBaseTest(BaseTest):
""" Sets up test-specific overrides and reusable methods and fixtures. """
@pytest.fixture(autouse=True, scope="class")
- def mock_version_file_name(self):
+ def mock_versionfile_filename(self):
"""
Every test in this class (and subclasses) will automatically run with this patch
applied (autouse=True), but the patch will not persist beyond the test class.
"""
- with patch.object(VersionUtils, 'VERSION_FILENAME', TEST__VERSION_FILE_NAME):
+ with patch.object(VersionUtils, 'VERSIONFILE__FILENAME', TEST__VERSIONFILE__FILENAME):
yield
@@ -73,7 +77,7 @@ class VersionBaseTest(BaseTest):
"""
Write the test version file to disk.
"""
- assert VersionUtils.VERSION_FILENAME == TEST__VERSION_FILE_NAME
+ assert VersionUtils.VERSIONFILE__FILENAME == TEST__VERSIONFILE__FILENAME
with open(VersionUtils._get_version_file_path(), "w") as f:
f.write(TEST__VERSION_FILE_CONTENTS)
@@ -83,22 +87,29 @@ class VersionBaseTest(BaseTest):
"""
Delete the test version file from disk.
"""
- assert VersionUtils.VERSION_FILENAME == TEST__VERSION_FILE_NAME
+ assert VersionUtils.VERSIONFILE__FILENAME == TEST__VERSIONFILE__FILENAME
try:
os.remove(VersionUtils._get_version_file_path())
except FileNotFoundError:
pass
+ def reset_version_singleton(self):
+ Version._instance = None
+
+
def setup_method(self):
super().setup_method()
def teardown_method(self):
super().teardown_method()
+
# Clean up any test version file created
self.delete_test_version_file()
+ self.reset_version_singleton()
+
class TestVersionBaseTest(VersionBaseTest):
@@ -106,7 +117,7 @@ class TestVersionBaseTest(VersionBaseTest):
"""
Ensure that the setup and teardown methods work as expected.
"""
- assert VersionUtils.VERSION_FILENAME == TEST__VERSION_FILE_NAME
+ assert VersionUtils.VERSIONFILE__FILENAME == TEST__VERSIONFILE__FILENAME
# During setup, the test version file should not exist
assert not os.path.exists(VersionUtils._get_version_file_path())
@@ -132,97 +143,7 @@ class TestVersionBaseTest(VersionBaseTest):
-class TestVersion(VersionBaseTest):
- def test_seedsigner_os_reads_from_version_file(self):
- """
- When running on SeedSigner OS, the version data should be read from the
- version.json file.
- """
- self.write_test_version_file()
-
- # Simulate running on SeedSigner OS
- with patch("seedsigner.models.settings.Settings.HOSTNAME", Settings.SEEDSIGNER_OS):
- assert Version.get_version_name() == VersionUtils._prefix_version_name(TEST__VERSION_NAME)
- assert Version.get_version_fork() == TEST__VERSION_FORK
- assert Version.get_version_timestamp() == TEST__VERSION_TIMESTAMP
- assert Version.get_version_commit_hash() == TEST__VERSION_COMMIT_HASH
-
-
- # def test_version_with_no_git_head(self):
- # """
- # If there is no .git/HEAD file, the hard-coded VERSION constant should be returned.
- # """
- # # mock out the os.path.exists call to always return False
- # with mock.patch("os.path.exists", return_value=False):
- # version = Version.get_version()
- # assert version == f"v{Version.VERSION}"
-
-
- # def test_version_with_actual_git_head(self):
- # """
- # If the .git dir exists on our actual filesystem right now, we should get a version
- # based on its contents.
-
- # NOTE: This test is potentially fragile as it depends on the test runner system's actual
- # git state. The get_version() call should be able to handle all possible git states, but
- # this does create the possibility of external variability.
- # """
- # fake_hardcoded_version = "fake.version.123"
- # with mock.patch.object(Version, 'VERSION', fake_hardcoded_version):
- # git_dot_dir = Version._get_dot_git_dir()
- # if os.path.exists(git_dot_dir):
- # assert Version.get_version() != f"v{fake_hardcoded_version}"
- # else:
- # # If there's no .git dir, mark this test as skipped
- # pytest.skip(f"No .git dir found at {git_dot_dir}, skipping test.")
-
-
- # def test_version_with_mocked_git_head(self):
- # """
- # If there is a .git/HEAD file, the version should report the current git branch
- # name, commit hash, or a matching tag.
- # """
- # with mock.patch("os.path.exists", return_value=True):
- # # Mock the HEAD file read in _read_HEAD_file to return our fake branch name
- # branch_name = "my_feature_branch"
- # git_HEAD_content = f"ref: refs/heads/{branch_name}"
- # with mock.patch("builtins.open", mock.mock_open(read_data=git_HEAD_content)):
- # version = VersionUtils._get_version_name_from_git_HEAD()
- # assert version == branch_name
-
- # # Mock the HEAD file read in _read_HEAD_file to return a fake commit hash
- # commit_hash = "abcdef1234567890"
- # with mock.patch("builtins.open", mock.mock_open(read_data=commit_hash)):
- # # Mock that there are no matching tags for this commit hash
- # with mock.patch.object(Version, '_get_matching_tag', return_value=None):
- # version = Version.get_version_name()
- # assert version == commit_hash[:7] # short commit hash
-
- # # Now mock that there is a matching tag for this commit hash
- # tag_name = "v1.2.3"
- # with mock.patch.object(Version, '_get_matching_tag', return_value=tag_name):
- # version = Version.get_version_name()
- # assert version == tag_name
-
-
- def test_get_last_edit(self):
- """
- Test that get_last_src_edit returns a sane datetime object. Assumes the system
- running this test has a reasonably correct system time.
- """
- last_edit = Version.get_version_timestamp()
- assert isinstance(last_edit, datetime)
-
- # Has to be more recent than the first SeedSigner v0.0.1 release
- known_past = datetime(2020, 12, 13)
- assert last_edit > known_past
-
- # Could not have happened tomorrow
- known_future = datetime.now().replace(year=datetime.now().year + 1)
- assert last_edit < known_future
-
-
-class TestVersionUtils(VersionBaseTest):
+class TestVersionUtils_BasicCalls(VersionBaseTest):
def test__prefix_version_name(self):
"""
Semantic versions should be prefixed with 'v' but others should be left as-is.
@@ -253,6 +174,37 @@ class TestVersionUtils(VersionBaseTest):
assert result == version_name, f"Expected '{result}' to equal input '{version_name}'"
+
+class TestVersionUtils_VersionFile(VersionBaseTest):
+ def test_seedsigner_os_reads_from_version_file(self):
+ """
+ Test the high-level basic public calls. Does just the minimal necessary mocking
+ since all the downstream helper methods are tested in detail elsewhere.
+
+ When running on SeedSigner OS, the version data should be read from the
+ version.json file.
+ """
+ self.write_test_version_file()
+
+ # Simulate running on SeedSigner OS
+ 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
+
+ # Expect errors if keys are missing from version.json
+ with mock.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 pytest.raises(Exception):
+ VersionUtils.get_version_timestamp()
+
+
+
def test__read_version_file(self):
"""
Low-level test for reading the version.json file.
@@ -261,10 +213,10 @@ class TestVersionUtils(VersionBaseTest):
version_data = VersionUtils._read_version_file()
assert version_data is not None
- assert version_data[VersionUtils.ATTR__VERSION_NAME] == TEST__VERSION_NAME
- assert version_data[VersionUtils.ATTR__VERSION_FORK] == TEST__VERSION_FORK
- assert version_data[VersionUtils.ATTR__VERSION_TIMESTAMP] == TEST__VERSION_TIMESTAMP.isoformat()
- assert version_data[VersionUtils.ATTR__VERSION_COMMIT_HASH] == TEST__VERSION_COMMIT_HASH
+ assert version_data[VersionUtils.VERSIONFILE_ATTR__NAME] == TEST__VERSION_NAME
+ assert version_data[VersionUtils.VERSIONFILE_ATTR__FORK] == TEST__VERSION_FORK
+ 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):
@@ -272,6 +224,179 @@ class TestVersionUtils(VersionBaseTest):
assert os.path.exists(VersionUtils._get_version_file_path()) is False
assert VersionUtils._read_version_file() is None
+ # 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
+
+ # 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_env_var(self):
+ assert os.environ.get(VersionUtils.ENV_VAR__SEEDSIGNER_OS_BUILDER__VERSION_NAME) is None
+ assert VersionUtils._get_version_name_from_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_env_var()
+ assert result == TEST__VERSION_NAME
+
+
+
+class TestVersionUtils_GithubActions(VersionBaseTest):
+ def test_github_actions_env_vars(self):
+ """
+ Test the high-level basic public calls. Does just the minimal necessary mocking
+ since all the downstream helper methods are tested in detail elsewhere.
+
+ When running in a GitHub Actions CI environment, the version data should be
+ 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):
+ # When running CI on a branch
+ with mock.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,
+ VersionUtils.ENV_VAR__GITHUB_ACTIONS__REPOSITORY_OWNER: TEST__VERSION_FORK,
+ }):
+ assert Version.get_version_name() == TEST__VERSION_BRANCH
+ assert Version.get_version_fork() == TEST__VERSION_FORK
+ assert Version.get_version_commit_hash() == TEST__SHORT_COMMIT_HASH
+ assert Version.get_version_timestamp() == TEST__VERSION_TIMESTAMP
+
+ # When running CI on a semantic tag
+ self.reset_version_singleton()
+ with mock.patch.dict(os.environ, {
+ VersionUtils.ENV_VAR__GITHUB_ACTIONS__IS_CI: "true",
+ VersionUtils.ENV_VAR__GITHUB_ACTIONS__REF_NAME: TEST__SEMANTIC_TAG,
+ }):
+ # Should be prefixed with "v"
+ assert Version.get_version_name() == f"v{TEST__SEMANTIC_TAG}"
+
+ # When running CI on a generic tag
+ self.reset_version_singleton()
+ with mock.patch.dict(os.environ, {
+ VersionUtils.ENV_VAR__GITHUB_ACTIONS__IS_CI: "true",
+ VersionUtils.ENV_VAR__GITHUB_ACTIONS__REF_NAME: TEST__VERSION_TAG,
+ }):
+ # Should NOT be prefixed with "v"
+ assert Version.get_version_name() == TEST__VERSION_TAG
+
+ # 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.
+ self.reset_version_singleton()
+ with mock.patch.dict(os.environ, {
+ VersionUtils.ENV_VAR__GITHUB_ACTIONS__IS_CI: "true",
+ VersionUtils.ENV_VAR__GITHUB_ACTIONS__SHA: TEST__FULL_COMMIT_HASH,
+ }):
+ assert Version.get_version_name() == TEST__SHORT_COMMIT_HASH
+
+ # When running CI on a commit (detached HEAD) with no REF_NAME and no SHA,
+ # raise error.
+ # Note: This scenario definitely would never happen. Just trying to get to
+ # 100% test coverage.
+ self.reset_version_singleton()
+ with mock.patch.dict(os.environ, {
+ VersionUtils.ENV_VAR__GITHUB_ACTIONS__IS_CI: "true",
+ }):
+ with pytest.raises(Exception):
+ Version.get_version_name()
+
+
+ def test_is_github_actions_ci(self):
+ """
+ is_github_actions_ci should return True only when the
+ ENV_VAR__GITHUB_ACTIONS__IS_CI env var is set to "true".
+ """
+ # Default should be False
+ assert VersionUtils.is_github_actions_ci() is False
+
+ with mock.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"}):
+ assert VersionUtils.is_github_actions_ci() is False
+
+ with mock.patch.dict(os.environ, {VersionUtils.ENV_VAR__GITHUB_ACTIONS__IS_CI: "1"}):
+ assert VersionUtils.is_github_actions_ci() is False
+
+
+ def test_get_version_name_from_github_actions_env_vars(self):
+ """
+ get_version_name_from_github_actions_env_vars should return the REF_NAME or SHA
+ env vars when set.
+ """
+ # Need to signal that we're in a GitHub Actions CI environment
+ with mock.patch.dict(os.environ, {VersionUtils.ENV_VAR__GITHUB_ACTIONS__IS_CI: "true"}):
+ assert os.environ.get(VersionUtils.ENV_VAR__GITHUB_ACTIONS__REF_NAME) is None
+ assert os.environ.get(VersionUtils.ENV_VAR__GITHUB_ACTIONS__SHA) is None
+ assert VersionUtils._get_version_name_from_github_actions_env_vars() is None
+
+ with mock.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
+
+ with mock.patch.dict(os.environ, {VersionUtils.ENV_VAR__GITHUB_ACTIONS__SHA: TEST__FULL_COMMIT_HASH}):
+ result = VersionUtils._get_version_name_from_github_actions_env_vars()
+ assert result == TEST__SHORT_COMMIT_HASH[:7]
+
+
+
+class TestVersionUtils_DotGitFiles(VersionBaseTest):
+ def test_local_dev_with_dot_git_dir_parsing(self, mock_popen: Mock):
+ """
+ Test the high-level basic public calls. Does just the minimal necessary mocking
+ since all the downstream helper methods are tested in detail elsewhere.
+
+ When running in a local dev environment without access to `git` shell commands,
+ the version data should be pulled from parsing the .git directory.
+
+ We pull in the `mock_popen` fixture here to simulate `git` shell commands failing.
+
+ Note that in local dev we use the last modified timestamp from the source files
+ rather than from git.
+ """
+ # If we're on a branch, getting the commit hash requires the
+ # .git/refs/heads/<branch> file.
+ with mock.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_version_timestamp_from_src_files=Mock(return_value=TEST__VERSION_TIMESTAMP),
+ ):
+ assert Version.get_version_name() == TEST__VERSION_BRANCH
+ assert Version.get_version_fork() == TEST__VERSION_FORK
+ assert Version.get_version_timestamp() == TEST__VERSION_TIMESTAMP
+ assert Version.get_version_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 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),
+ ):
+ self.reset_version_singleton()
+ assert Version.get_version_name() == TEST__VERSION_TAG
+ assert Version.get_version_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),
+ ):
+ self.reset_version_singleton()
+ assert Version.get_version_name() == TEST__SHORT_COMMIT_HASH
+ assert Version.get_version_commit_hash() == TEST__SHORT_COMMIT_HASH
+
def test__get_dot_git_dir(self):
"""
@@ -291,19 +416,17 @@ class TestVersionUtils(VersionBaseTest):
assert VersionUtils._read_git_HEAD_file() == (None, None)
# If we're on a branch...
- expected_branch = "my_test_branch"
- git_HEAD_content = f"ref: refs/heads/{expected_branch}"
+ git_HEAD_content = f"ref: refs/heads/{TEST__VERSION_BRANCH}"
with patch("builtins.open", mock.mock_open(read_data=git_HEAD_content)):
branch_name, commit_hash = VersionUtils._read_git_HEAD_file()
- assert branch_name == expected_branch
+ assert branch_name == TEST__VERSION_BRANCH
assert commit_hash is None
# If we're in a detached HEAD state at a specific commit hash...
- expected_commit_hash = "47212c98f1bf948e9918b672c4bb88b1c965aff4"
- with patch("builtins.open", mock.mock_open(read_data=expected_commit_hash)):
+ with patch("builtins.open", mock.mock_open(read_data=TEST__FULL_COMMIT_HASH)):
branch_name, commit_hash = VersionUtils._read_git_HEAD_file()
assert branch_name is None
- assert commit_hash == expected_commit_hash
+ assert commit_hash == TEST__FULL_COMMIT_HASH
# Gracefully handle a read error
with patch("builtins.open", side_effect=FileNotFoundError):
@@ -320,26 +443,146 @@ class TestVersionUtils(VersionBaseTest):
if a matching tag is found for the given commit hash.
"""
# No .git dir initially
- assert VersionUtils._get_matching_tag_from_git_refs_tags("anyhash") is None
-
- # Mock out the .git/refs/tags file read to return some fake tags
- fake_tags_content = """v1.0.0:abcd1234567890
-v1.2.3:deadbeefcafebabe
-v2.0.0-rc1:47212c98f1bf948e9918b672c4bb88b1c965aff4
-"""
- with patch("builtins.open", mock.mock_open(read_data=fake_tags_content)):
- # Existing tag
- tag_name = VersionUtils._get_matching_tag_from_git_refs_tags("deadbeefcafebabe")
- assert tag_name == "v1.2.3"
-
- # Another existing tag
- tag_name = VersionUtils._get_matching_tag_from_git_refs_tags("47212c98f1bf948e9918b672c4bb88b1c965aff4")
- assert tag_name == "v2.0.0-rc1"
-
- # Non-existing tag
- tag_name = VersionUtils._get_matching_tag_from_git_refs_tags("nonexistenthash")
+ assert os.path.exists(VersionUtils._get_dot_git_dir()) is False
+
+ # Mock the os.listdir of the .git/refs/tags directory to the target tag name
+ with patch("os.listdir", return_value=[TEST__VERSION_NAME]):
+ # Mock the open of the tag ref file to return our test commit hash
+ with patch("builtins.open", mock.mock_open(read_data=TEST__FULL_COMMIT_HASH)):
+ tag_name = VersionUtils._get_matching_tag_from_git_refs_tags(TEST__FULL_COMMIT_HASH)
+ assert tag_name == TEST__VERSION_NAME
+
+ # If no matching tag is found, should return None
+ with patch("os.listdir", return_value=["some_other_tag"]):
+ with patch("builtins.open", mock.mock_open(read_data="different_commit_hash")):
+ tag_name = VersionUtils._get_matching_tag_from_git_refs_tags(TEST__FULL_COMMIT_HASH)
+ assert tag_name is None
+
+ # If the refs/tags directory is missing, should return None
+ with patch("seedsigner.helpers.version.VersionUtils._get_dot_git_dir", return_value="/nonexistent/path"):
+ tag_name = VersionUtils._get_matching_tag_from_git_refs_tags(TEST__FULL_COMMIT_HASH)
assert tag_name is None
+ # If any other exception occurs, should return None
+ with patch("os.listdir", side_effect=Exception("Unexpected error")):
+ tag_name = VersionUtils._get_matching_tag_from_git_refs_tags(TEST__FULL_COMMIT_HASH)
+ assert tag_name is None
+
+
+ def test__get_version_name_from_git_HEAD(self):
+ # Should gracefully return None if there's no .git dir or .git/HEAD file.
+ assert VersionUtils._get_version_name_from_git_HEAD() == None
+
+ # 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)):
+ 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):
+ 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):
+ assert VersionUtils._get_version_name_from_git_HEAD() == TEST__SHORT_COMMIT_HASH
+
+
+ def test__get_commit_hash_from_git_HEAD(self):
+ # _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)):
+ assert VersionUtils._get_full_commit_hash_from_git_HEAD() == TEST__SHORT_COMMIT_HASH
+
+
+ def test__get_full_commit_hash_from_git_refs_heads(self):
+ """
+ _get_full_commit_hash_from_git_refs_heads should return the expected commit hash
+ for the given branch name.
+ """
+ # No .git dir initially
+ assert os.path.exists(VersionUtils._get_dot_git_dir()) is False
+ assert VersionUtils._get_full_commit_hash_from_git_refs_heads(TEST__VERSION_BRANCH) is None
+
+ # Mock the open of the branch ref file to return our test commit hash
+ with patch("builtins.open", mock.mock_open(read_data=TEST__FULL_COMMIT_HASH)):
+ with patch("os.path.exists", return_value=True):
+ commit_hash = VersionUtils._get_full_commit_hash_from_git_refs_heads(TEST__VERSION_BRANCH)
+ assert commit_hash == TEST__FULL_COMMIT_HASH
+
+ # If the branch ref file is missing, should return None
+ with patch("os.path.exists", return_value=False):
+ commit_hash = VersionUtils._get_full_commit_hash_from_git_refs_heads(TEST__VERSION_BRANCH)
+ assert commit_hash is None
+
+ # If any other exception occurs, should return None
+ with patch("builtins.open", side_effect=Exception("Unexpected error")):
+ commit_hash = VersionUtils._get_full_commit_hash_from_git_refs_heads(TEST__VERSION_BRANCH)
+ assert commit_hash is None
+
+
+ def test__parse_git_remote_url(self):
+ """
+ _parse_git_remote_url should return the expected repo owner from various
+ git remote url formats.
+ """
+ # Might be called with no remote url or unrecognized format
+ for url in [None, "", "what/is/this"]:
+ assert VersionUtils._parse_git_remote_url(url) is None
+
+ for url, expected_owner in [
+ ("https://github.com/SeedSigner/seedsigner.git", "SeedSigner"),
+ ("git@github.com:SeedSigner/seedsigner.git", "SeedSigner"),
+ ("https://gitlab.com/some_user/some-repo.git", "some_user"),
+ ("git@gitlab.com:other-user/some-repo.git", "other-user"),
+ ]:
+ assert VersionUtils._parse_git_remote_url(url) == expected_owner
+
+
+ def test__get_version_fork_from_git_config(self):
+ """
+ Test that _get_version_fork_from_git_config returns the expected repo owner from
+ the remote url.
+ """
+ expected_fork = "SeedSigner"
+ remote_url = f"git@github.com:{expected_fork}/seedsigner.git"
+ git_config = f"""
+ [some_section]
+ some_key = some_value
+ [remote "origin"]
+ url = {remote_url}
+ fetch = +refs/heads/*:refs/remotes/origin/*
+ [next_section]
+ next_key = next_value
+ """
+ with mock.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
+ git_config_no_origin = """
+ [some_section]
+ some_key = some_value
+ [next_section]
+ next_key = next_value
+ """
+ with mock.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
+ git_config_malformed_origin = """
+ [remote "origin"]
+ fetch = +refs/heads/*:refs/remotes/origin/*
+ [next_section]
+ next_key = next_value
+ """
+ with mock.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):
+ assert VersionUtils._get_version_fork_from_git_config() is None
+
def test__get_version_timestamp_from_src_files(self):
"""
@@ -362,16 +605,36 @@ v2.0.0-rc1:47212c98f1bf948e9918b672c4bb88b1c965aff4
+class TestVersionUtils_GitShell(VersionBaseTest):
+ def test_local_dev_with_git_shell_calls(self):
+ """
+ Test the high-level basic public calls. Does just the minimal necessary mocking
+ since all the downstream helper methods are tested in detail elsewhere.
+
+ When running in a local dev environment with access to `git` shell commands,
+ the version data should be pulled from those commands.
+
+ Note that in local dev we use the last modified timestamp from the source files
+ rather than from git.
+ """
+ with mock.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_version_timestamp_from_src_files=Mock(return_value=TEST__VERSION_TIMESTAMP),
+ _get_full_commit_hash_from_git_shell=Mock(return_value=TEST__SHORT_COMMIT_HASH),
+ ):
+ assert Version.get_version_name() == TEST__VERSION_BRANCH
+ assert Version.get_version_fork() == TEST__VERSION_FORK
+ assert Version.get_version_timestamp() == TEST__VERSION_TIMESTAMP
+ assert Version.get_version_commit_hash() == TEST__SHORT_COMMIT_HASH
+
def test__get_version_name_from_git_shell(self):
"""
Test that _get_version_name_from_git_shell returns the expected name depending on
the current git state
"""
- branch_name = "my_test_branch"
- tag_name = "my_test_tag"
- commit_hash = "abcd123"
-
# Default mock_popen return empty string; simulates no `git` shell command available
# or no local git data.
result = VersionUtils._get_version_name_from_git_shell()
@@ -380,32 +643,32 @@ v2.0.0-rc1:47212c98f1bf948e9918b672c4bb88b1c965aff4
# If we're on a branch, should return the branch name
with mock.patch.multiple(
"seedsigner.helpers.version.VersionUtils",
- _get_version_name_from_git_shell_branch=Mock(return_value=branch_name),
- _get_version_name_from_git_shell_tag=Mock(return_value=tag_name),
- _get_version_name_from_git_shell_commit_hash=Mock(return_value=commit_hash),
+ _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_full_commit_hash_from_git_shell=Mock(return_value=TEST__FULL_COMMIT_HASH),
):
result = VersionUtils._get_version_name_from_git_shell()
- assert result == branch_name
-
+ assert result == TEST__VERSION_BRANCH
+
# If we're on a tag, the detached HEAD state wipes out the branch name
with mock.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=tag_name),
- _get_version_name_from_git_shell_commit_hash=Mock(return_value=commit_hash),
+ _get_version_name_from_git_shell_tag=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 == tag_name
-
+ assert result == TEST__VERSION_TAG
+
# Similarly, if we're detached at a specific commit hash
with mock.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_version_name_from_git_shell_commit_hash=Mock(return_value=commit_hash),
+ _get_full_commit_hash_from_git_shell=Mock(return_value=TEST__FULL_COMMIT_HASH),
):
result = VersionUtils._get_version_name_from_git_shell()
- assert result == commit_hash[:7] # short hash
+ assert result == TEST__FULL_COMMIT_HASH
def test__get_version_fork_from_git_shell(self, mock_popen: Mock):
@@ -425,6 +688,10 @@ v2.0.0-rc1:47212c98f1bf948e9918b672c4bb88b1c965aff4
"""
Test that _get_version_timestamp_from_git_shell returns the expected datetime.
"""
+ # Should gracefully handle no `git` shell command available or no local git data.
+ mock_popen.return_value.read.return_value = ""
+ assert VersionUtils._get_version_timestamp_from_git_shell() is None
+
# Initial timestamp has timezone info
hour = 14
tz_offset = 1
@@ -440,15 +707,14 @@ v2.0.0-rc1:47212c98f1bf948e9918b672c4bb88b1c965aff4
assert VersionUtils._get_version_timestamp_from_git_shell() == expected_datetime
- def test__get_version_commit_hash_from_git_shell(self, mock_popen: Mock):
+ def test__get_short_commit_hash_from_git_shell(self, mock_popen: Mock):
"""
- Test that _get_version_commit_hash_from_git_shell returns the expected short commit hash.
+ Test that _get_full_commit_hash_from_git_shell returns the expected short commit hash.
"""
- commit_hash = "abcd123"
- mock_popen.return_value.read.return_value = commit_hash
+ mock_popen.return_value.read.return_value = TEST__SHORT_COMMIT_HASH
- result = VersionUtils._get_version_commit_hash_from_git_shell()
- assert result == commit_hash
+ result = VersionUtils._get_full_commit_hash_from_git_shell()
+ assert result == TEST__SHORT_COMMIT_HASH
def test__fetch_latest_seedsigner_release_tag(self, mock_popen: Mock):
@@ -507,6 +773,40 @@ v2.0.0-rc1:47212c98f1bf948e9918b672c4bb88b1c965aff4
+class TestVersion(VersionBaseTest):
+ """
+ `Version` is really just a way to store the version data; all the real work is done in
+ `VersionUtils` and all of those calls have already been fully covered by the above
+ tests. So there's nothing to really test here. Just providing minimal tests in order
+ to get full test coverage.
+ """
+ def test_basic_calls(self):
+ self.write_test_version_file()
+ 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_version_commit_hash() == TEST__VERSION_DICT[VersionUtils.VERSIONFILE_ATTR__COMMIT_HASH]
+ Version.get_version_timestamp() == TEST__VERSION_TIMESTAMP
+
+
+ def test_get_last_edit(self):
+ """
+ Test that get_last_src_edit returns a sane datetime object. Assumes the system
+ running this test has a reasonably correct system time.
+ """
+ last_edit = Version.get_version_timestamp()
+ assert isinstance(last_edit, datetime)
+
+ # Has to be more recent than the first SeedSigner v0.0.1 release
+ known_past = datetime(2020, 12, 13)
+ assert last_edit > known_past
+
+ # Could not have happened tomorrow
+ known_future = datetime.now().replace(year=datetime.now().year + 1)
+ assert last_edit < known_future
+
+
+
class TestNotAllowedInSeedSignerOSDecorator(BaseTest):
SUCCESS = "success"
@@ -536,10 +836,10 @@ class TestNotAllowedInSeedSignerOSDecorator(BaseTest):
class TestNotVersionBaseTest(BaseTest):
def test_version_file_name__not_patched(self):
"""
- Ensure that outside of VersionBaseTest, the VERSION_FILENAME patch does not
+ Ensure that outside of VersionBaseTest, the VERSIONFILE__FILENAME patch does not
persist.
"""
- assert VersionUtils.VERSION_FILENAME != TEST__VERSION_FILE_NAME
+ assert VersionUtils.VERSIONFILE__FILENAME != TEST__VERSIONFILE__FILENAME
def test_mock_popen__not_patched(self):
diff --git a/tools/write_versionfile.py b/tools/write_versionfile.py
index 89f736e..37b08a4 100644
--- a/tools/write_versionfile.py
+++ b/tools/write_versionfile.py
@@ -48,15 +48,15 @@ if __name__ == "__main__":
break
version_timestamp = VersionUtils._get_version_timestamp_from_git_shell()
- version_commit_hash = VersionUtils._get_version_commit_hash_from_git_shell()
+ version_commit_hash = VersionUtils._get_full_commit_hash_from_git_shell()
if not version_name or not version_timestamp or not version_commit_hash:
raise Exception("Could not determine version information from git.")
- version_info[VersionUtils.ATTR__VERSION_NAME] = version_name
- version_info[VersionUtils.ATTR__VERSION_FORK] = VersionUtils._get_version_fork_from_git_shell()
- version_info[VersionUtils.ATTR__VERSION_TIMESTAMP] = version_timestamp.isoformat()
- version_info[VersionUtils.ATTR__VERSION_COMMIT_HASH] = version_commit_hash[:7] # short hash
+ version_info[VersionUtils.VERSIONFILE_ATTR__NAME] = version_name
+ version_info[VersionUtils.VERSIONFILE_ATTR__FORK] = VersionUtils._get_version_fork_from_git_shell()
+ version_info[VersionUtils.VERSIONFILE_ATTR__TIMESTAMP] = version_timestamp.isoformat()
+ version_info[VersionUtils.VERSIONFILE_ATTR__COMMIT_HASH] = version_commit_hash[:7] # short hash
version_file_path = VersionUtils._get_version_file_path()
with open(version_file_path, "w") as f:
Why this scored 14/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.