import datetime import os import tempfile import unittest import scratch def make_pool(root, names=("01", "02")): for name in names: os.makedirs(os.path.join(root, name)) def no_changes(workspace_path): return [] class LocateRootsTests(unittest.TestCase): def test_MainScriptsFindASiblingPoolNamedAfterMain(self): main = os.path.abspath(os.path.join(os.sep, "work", "Checkout")) self.assertEqual((os.path.normpath(main), os.path.normpath(main + "-Scratch")), scratch.locate_roots(os.path.join(main, "Scripts"))) def test_WorkspaceScriptsFindTheSamePoolAndMain(self): main = os.path.abspath(os.path.join(os.sep, "work", "Checkout")) self.assertEqual((os.path.normpath(main), os.path.normpath(main + "-Scratch")), scratch.locate_roots(os.path.join(main + "-Scratch", "03", "Scripts"))) class WorkspaceNamesTests(unittest.TestCase): def test_MissingRootHasNoWorkspaces(self): self.assertEqual([], scratch.workspace_names(os.path.join(tempfile.gettempdir(), "no-such-workspaces-root"))) def test_ListsOnlyTwoDigitDirectoriesInOrder(self): with tempfile.TemporaryDirectory() as root: make_pool(root, ("02", "01", "locks", "1")) open(os.path.join(root, "03"), "w").close() self.assertEqual(["01", "02"], scratch.workspace_names(root)) class TakeTests(unittest.TestCase): def test_TakesTheFirstFreeWorkspaceAndRecordsTheHolder(self): with tempfile.TemporaryDirectory() as root: make_pool(root) path = scratch.take(root, session_id="abc", account="account2", task="Glass") self.assertEqual(os.path.join(root, "01"), path) holder = scratch.read_holder(root, "01") self.assertEqual(("abc", "account2", "Glass"), (holder["session_id"], holder["account"], holder["task"])) def test_SkipsLockedWorkspaces(self): with tempfile.TemporaryDirectory() as root: make_pool(root) scratch.take(root, "first", "account1", "") self.assertEqual(os.path.join(root, "02"), scratch.take(root, "second", "account1", "")) def test_FailsListingHoldersWhenEveryWorkspaceIsLocked(self): with tempfile.TemporaryDirectory() as root: make_pool(root, ("01",)) scratch.take(root, "holder-session", "account1", "the task") with self.assertRaises(scratch.NoFreeWorkspace) as caught: scratch.take(root, "late", "account1", "") self.assertIn("holder-session", str(caught.exception)) self.assertIn("the task", str(caught.exception)) def test_FailsWhenThereAreNoWorkspaces(self): with tempfile.TemporaryDirectory() as root: with self.assertRaises(scratch.NoFreeWorkspace): scratch.take(root, "late", "account1", "") class ReleaseTests(unittest.TestCase): def test_FreesTheWorkspaceForTheNextTaker(self): with tempfile.TemporaryDirectory() as root: make_pool(root, ("01",)) path = scratch.take(root, "a", "account1", "") scratch.release(root, path, has_changes=no_changes) self.assertIsNone(scratch.read_holder(root, "01")) self.assertEqual(path, scratch.take(root, "b", "account1", "")) def test_AcceptsAPathInsideTheWorkspace(self): with tempfile.TemporaryDirectory() as root: make_pool(root, ("01",)) path = scratch.take(root, "a", "account1", "") scratch.release(root, os.path.join(path, "Modules", "Glass"), has_changes=no_changes) self.assertIsNone(scratch.read_holder(root, "01")) def test_RefusesAndKeepsTheLockWhenThereAreChanges(self): with tempfile.TemporaryDirectory() as root: make_pool(root, ("01",)) path = scratch.take(root, "a", "account1", "") with self.assertRaises(scratch.WorkspaceHasChanges): scratch.release(root, path, has_changes=lambda workspace_path: ["M Modules\\Glass\\Foo.cs"]) self.assertIsNotNone(scratch.read_holder(root, "01")) def test_ReleasingAFreeWorkspaceIsHarmless(self): with tempfile.TemporaryDirectory() as root: make_pool(root, ("01",)) scratch.release(root, os.path.join(root, "01"), has_changes=no_changes) def test_ForgetsTheDeliveriesRecordedForTheWorkspace(self): with tempfile.TemporaryDirectory() as root: make_pool(root, ("01",)) path = scratch.take(root, "a", "account1", "") with open(scratch.deliveries_path(root, "01"), "w", encoding="utf-8") as record: record.write("{}") scratch.release(root, path, has_changes=no_changes) self.assertFalse(os.path.exists(scratch.deliveries_path(root, "01"))) def test_KeepsTheRecordedDeliveriesWhenThereAreChanges(self): with tempfile.TemporaryDirectory() as root: make_pool(root, ("01",)) path = scratch.take(root, "a", "account1", "") with open(scratch.deliveries_path(root, "01"), "w", encoding="utf-8") as record: record.write("{}") with self.assertRaises(scratch.WorkspaceHasChanges): scratch.release(root, path, has_changes=lambda workspace_path: ["M Modules\\Glass\\Foo.cs"]) self.assertTrue(os.path.exists(scratch.deliveries_path(root, "01"))) def test_RejectsAPathOutsideEveryWorkspace(self): with tempfile.TemporaryDirectory() as root: make_pool(root, ("01",)) with self.assertRaises(ValueError): scratch.release(root, os.path.join(root, "locks"), has_changes=no_changes) with self.assertRaises(ValueError): scratch.release(root, os.path.dirname(root), has_changes=no_changes) class DeliveriesPathTests(unittest.TestCase): def test_LivesBesideTheLocksAndNotInsideTheWorkspace(self): root = os.path.join("C:\\", "Pool") self.assertEqual(os.path.join(root, "locks", "03.deliveries.json"), scratch.deliveries_path(root, "03")) def test_IsNotTheLockFile(self): root = os.path.join("C:\\", "Pool") self.assertNotEqual(scratch.lock_path(root, "03"), scratch.deliveries_path(root, "03")) class UnversionedPathsTests(unittest.TestCase): workspace = os.path.join("C:\\", "Workspaces", "01") def status_line(self, flag, *parts): return "{} {}".format(flag, os.path.join(self.workspace, *parts)) def test_ReturnsOnlyUnversionedPaths(self): status = "\n".join([self.status_line("?", "Modules", "New.cs"), self.status_line("M", "Modules", "Old.cs"), self.status_line("A", "Modules", "Added.cs")]) self.assertEqual([os.path.join(self.workspace, "Modules", "New.cs")], scratch.unversioned_paths(status, self.workspace)) def test_LeavesBuildOutputAlone(self): status = "\n".join([self.status_line("?", "Bin", "Release", "A.dll"), self.status_line("?", "Junk", "x.obj")]) self.assertEqual([], scratch.unversioned_paths(status, self.workspace)) def test_IgnoresPathsOutsideTheWorkspace(self): status = "? " + os.path.join("C:\\", "ElectricRoot", "Modules", "New.cs") self.assertEqual([], scratch.unversioned_paths(status, self.workspace)) def test_IgnoresTextThatIsNotAStatusLine(self): self.assertEqual([], scratch.unversioned_paths("Performing status on external item at 'x'\n\n", self.workspace)) class StaleLocksTests(unittest.TestCase): def test_ReportsOnlyLocksOlderThanTheLimit(self): with tempfile.TemporaryDirectory() as root: make_pool(root, ("01", "02", "03")) now = datetime.datetime(2026, 10, 15, 12, 0, 0) scratch.take(root, "old", "account1", "", now=now - datetime.timedelta(days=11)) scratch.take(root, "recent", "account1", "", now=now - datetime.timedelta(days=9)) stale = scratch.stale_locks(root, now=now) self.assertEqual([("01", "old")], [(name, holder["session_id"]) for name, holder in stale]) if __name__ == "__main__": unittest.main()