test: add unit tests for rez.utils.base26 - #2156
Conversation
|
|
|
Hi @darrenhuai , thank you for the testing contribs. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #2156 +/- ##
==========================================
+ Coverage 61.29% 61.34% +0.04%
==========================================
Files 164 164
Lines 20568 20568
Branches 3575 3576 +1
==========================================
+ Hits 12607 12617 +10
+ Misses 7089 7081 -8
+ Partials 872 870 -2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Yes, both PRs were AI-assisted. Claude assisted in writing the test code, and drafting the commit messages and PR descriptions. I reviewed the diffs and the test/lint output before pushing each one and made the call on what to ship. Happy to add a disclosure to the PR title, is there a format you'd prefer, since the policy isn't finalized yet? Thank you! |
|
@darrenhuai Something as simple as what I'm putting on my PRs is fine for now, like this, in the PR description (not the title):
Just to explain (as a point of reference), we're not barring LLM-generated code, it's just so that we the maintainers can:
|
|
perfect thank you! will do so |
JeanChristopheMorinPerso
left a comment
There was a problem hiding this comment.
I threw a coding agent at your PR and it found two bugs in base26.py that would be very easy to fix in this PR if you want. I don't consider these blocking as they are probably corner cases.
| "os.listdir", return_value=['a', 'b', 'c'] | ||
| ), patch( | ||
| "os.path.islink", return_value=True | ||
| ), patch("os.symlink") as symlink: | ||
| result = create_unique_base26_symlink("/pkgs", "/source/2.0") | ||
|
|
||
| symlink.assert_called_once_with("/source/2.0", result) | ||
| self.assertTrue(result.endswith('d')) |
There was a problem hiding this comment.
| "os.listdir", return_value=['a', 'b', 'c'] | |
| ), patch( | |
| "os.path.islink", return_value=True | |
| ), patch("os.symlink") as symlink: | |
| result = create_unique_base26_symlink("/pkgs", "/source/2.0") | |
| symlink.assert_called_once_with("/source/2.0", result) | |
| self.assertTrue(result.endswith('d')) | |
| "os.listdir", return_value=['a', 'b', 'c', 'z', 'aa'] | |
| ), patch( | |
| "os.path.islink", return_value=True | |
| ), patch("os.symlink") as symlink: | |
| result = create_unique_base26_symlink("/pkgs", "/source/2.0") | |
| symlink.assert_called_once_with("/source/2.0", result) | |
| self.assertTrue(result.endswith('ab')) |
Doing this will highlight that there's actually a bug in create_unique_base26_symlink. max(names) should be replaced with max(names, key=lambda x: (len(x), x)).
That's because:
>>> max(['z', 'aa'])
'z'In other words, the tests have full coverage purely in terms of code, but not in terms of logic.
| for bad in ('A', 'a1', 'a-b', ' a', 'a '): | ||
| with self.assertRaises(ValueError): | ||
| get_next_base26(bad) |
There was a problem hiding this comment.
| for bad in ('A', 'a1', 'a-b', ' a', 'a '): | |
| with self.assertRaises(ValueError): | |
| get_next_base26(bad) | |
| for bad in ('A', 'a1', 'a-b', ' a', 'a '): | |
| with self.subTest(item=bad): | |
| with self.assertRaises(ValueError): | |
| get_next_base26(bad) |
This will more clearly show that we are testing multiple things.
ALso, note how there's a but in the get_next_base26 where it accepts a trailing \n. We should replace match with fullmatch.
a124135 to
20e08b4
Compare
|
@JeanChristopheMorinPerso both fixes are in as of 20e08b4 - apologies for pushing them without saying anything.
Worth flagging that this makes the PR no longer tests-only: it now touches Suite and linters were clean when I pushed. |
get_next_base26 and create_unique_base26_symlink had no dedicated tests. Covers the letter-increment/rollover logic directly, and the symlink creation/collision-retry logic via mocks (real symlink creation needs elevated privileges on some platforms, e.g. Windows without Developer Mode, so the filesystem boundary is mocked instead). Refs AcademySoftwareFoundation#2092 Signed-off-by: darrenhuai <60621295+darrenhuai@users.noreply.github.com>
Review feedback on the new base26 tests pointed out two real bugs that full coverage was masking: - create_unique_base26_symlink used max(names), which sorts lexicographically, so 'z' would beat 'aa' even though 'aa' is the later id. Added a mixed-length regression test and switched to max(names, key=lambda x: (len(x), x)). - get_next_base26 used re.match, which lets '$' match before a trailing newline, so inputs like 'a\n' slipped past validation. Switched to re.fullmatch and added newline cases to the invalid input test, wrapped in subTest per JeanChristopheMorinPerso's suggestion. Signed-off-by: darrenhuai <60621295+darrenhuai@users.noreply.github.com>
20e08b4 to
22ab8f4
Compare
There was a problem hiding this comment.
Pull request overview
Adds unit coverage for base-26 identifiers and symlink allocation while fixing validation and identifier ordering.
Changes:
- Tests increments, rollover, validation, symlink creation, and errors.
- Uses strict input matching.
- Orders mixed-length identifiers correctly.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
src/rez/utils/base26.py |
Fixes validation and identifier ordering. |
src/rez/tests/test_utils_base26.py |
Adds comprehensive unit tests. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| with patch( | ||
| "rez.utils.base26.find_matching_symlink", return_value=None | ||
| ), patch("os.listdir", return_value=[]), patch( | ||
| "os.symlink", side_effect=[exists_error, None] | ||
| ) as symlink: | ||
| result = create_unique_base26_symlink("/pkgs", "/source/1.0") | ||
|
|
||
| self.assertEqual(symlink.call_count, 2) | ||
| self.assertTrue(result.endswith('a')) |
os.listdir was pinned to [] for the whole test, so after losing the race for 'a' the retry recomputed prev=None and picked 'a' again - the assertions held whether or not the loop re-read the directory. listdir now returns [] then ['a'], and the test checks the first attempt was 'a' and the surviving link is 'b'. Signed-off-by: darrenhuai <60621295+darrenhuai@users.noreply.github.com>
a8863ee to
e4690dc
Compare
rez.utils.base26 had no tests at all - get_next_base26 (the letter-increment logic for variant shortlink IDs) and create_unique_base26_symlink were both uncovered.
Tests for get_next_base26 cover the increment/rollover cases directly (a->b, z->aa, az->ba, zz->aaa, etc) and invalid input. For create_unique_base26_symlink I mocked the filesystem calls (os.listdir, os.symlink, find_matching_symlink) instead of creating real symlinks, since that needs elevated privileges on Windows without Developer Mode - this still covers the existing-match short circuit, picking the next id after the highest existing one, the EEXIST retry-on-race path, non-EEXIST errors propagating, and giving up after too much contention.
Ran the full suite locally (332 passed), flake8 and ruff clean on the new file.
Refs #2092
Disclosure: Claude (Sonnet 5) was used to assist in writing the test code and drafting this commit message and PR description.