test_release_retention.py 3.8 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
  1. """Retention regression tests only create and remove synthetic temporary releases."""
  2. import json
  3. import os
  4. from pathlib import Path
  5. import tempfile
  6. import unittest
  7. from unittest.mock import patch
  8. from step4_web.release_retention import prune_releases
  9. class RetentionTests(unittest.TestCase):
  10. def setUp(self):
  11. self.tmp=tempfile.TemporaryDirectory()
  12. self.root=Path(self.tmp.name).resolve()
  13. self.base=self.root/'.runtime/releases'
  14. self.base.mkdir(parents=True)
  15. self.manifest=self.root/'current_release.json'
  16. def tearDown(self): self.tmp.cleanup()
  17. def run_dir(self, i):
  18. path=self.base/f'{i:032x}'; path.mkdir()
  19. (path/'production').mkdir(); (path/'production/data.txt').write_text('synthetic')
  20. os.utime(path,(1700000000+i,1700000000+i))
  21. return path
  22. def publish(self, path, schema=None):
  23. self.manifest.write_text(json.dumps({'data_version':path.name,
  24. 'schema_file':str((schema or path)/'meta_graph_schema.json'),
  25. 'display_schema_file':str((schema or path)/'meta_graph_schema_display.json'),
  26. 'production_dir':str(path/'production'), 'assessment_file':str(path/'production/analysis.json')}))
  27. def test_tenth_kept_eleventh_removes_oldest_and_backlog_trimmed(self):
  28. runs=[self.run_dir(i) for i in range(10)]
  29. self.publish(runs[-1])
  30. self.assertEqual(prune_releases(self.root,self.manifest)['deleted'],[])
  31. runs.append(self.run_dir(10)); self.publish(runs[-1])
  32. result=prune_releases(self.root,self.manifest)
  33. self.assertEqual(result['deleted'],[runs[0].name]); self.assertFalse(runs[0].exists())
  34. self.assertEqual(result['remaining'],10)
  35. for i in range(11,15):self.run_dir(i)
  36. result=prune_releases(self.root,self.manifest)
  37. self.assertEqual(result['deleted'],[runs[i].name for i in range(1,5)])
  38. self.assertTrue(runs[-1].exists())
  39. def test_current_old_schema_and_running_failure_are_protected(self):
  40. runs=[self.run_dir(i) for i in range(12)]
  41. self.publish(runs[1],schema=runs[0])
  42. result=prune_releases(self.root,self.manifest,keep_paths=(runs[11],))
  43. self.assertEqual(result['deleted'],[runs[2].name,runs[3].name])
  44. self.assertTrue(all(runs[i].exists() for i in (0,1,11)))
  45. def test_timestamp_name_beats_modified_time_and_unknown_dirs_untouched(self):
  46. old=self.base/('20200101_080000_'+'a'*32); old.mkdir()
  47. for i in range(10):self.run_dir(i)
  48. unknown=self.base/'manual-backup'; unknown.mkdir()
  49. result=prune_releases(self.root,self.manifest)
  50. self.assertEqual(result['deleted'],[old.name]); self.assertTrue(unknown.exists())
  51. def test_invalid_manifest_fails_closed(self):
  52. for i in range(11):self.run_dir(i)
  53. self.manifest.write_text('{')
  54. with self.assertRaises(json.JSONDecodeError):prune_releases(self.root,self.manifest)
  55. self.assertEqual(len(list(self.base.iterdir())),11)
  56. def test_locked_directory_warns_without_failing_update(self):
  57. for i in range(11):self.run_dir(i)
  58. with patch('step4_web.release_retention.shutil.rmtree',side_effect=PermissionError):
  59. result=prune_releases(self.root,self.manifest)
  60. self.assertEqual(result['remaining'],11); self.assertTrue(result['warnings'])
  61. def test_nested_link_is_not_followed_or_deleted(self):
  62. runs=[self.run_dir(i) for i in range(11)]
  63. sentinel=runs[0]/'production/data.txt'
  64. with patch('step4_web.release_retention._is_link',side_effect=lambda p:p==sentinel):
  65. result=prune_releases(self.root,self.manifest)
  66. self.assertTrue(sentinel.exists())
  67. self.assertEqual(result['deleted'],[runs[1].name])
  68. self.assertTrue(result['warnings'])
  69. if __name__=='__main__':unittest.main()