test_bootstrap.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. """Bootstrap sequencing with a fake uv; no installs, downloads or services."""
  2. import json
  3. import os
  4. from pathlib import Path
  5. import shutil
  6. import subprocess
  7. import tempfile
  8. import unittest
  9. from unittest.mock import patch
  10. from scripts import download_model
  11. ROOT = Path(__file__).resolve().parents[1]
  12. BASH = shutil.which('bash') or ('C:/Program Files/Git/bin/bash.exe' if Path('C:/Program Files/Git/bin/bash.exe').exists() else None)
  13. @unittest.skipUnless(BASH, 'bash is unavailable')
  14. class BootstrapTests(unittest.TestCase):
  15. def run_script(self, action, fail='', env_exists=True, args=(), deploy_python=None):
  16. with tempfile.TemporaryDirectory() as tmp:
  17. root=Path(tmp)
  18. shutil.copy2(ROOT/'deploy.sh',root/'deploy.sh')
  19. shutil.copy2(ROOT/'.python-version',root/'.python-version')
  20. (root/'.env.example').write_text('EXAMPLE=1\n')
  21. if env_exists: (root/'.env').write_text('KEEP=unchanged\n')
  22. bin_dir=root/'bin'; bin_dir.mkdir()
  23. fake=bin_dir/'uv'
  24. fake.write_text('#!/bin/sh\nprintf "%s\\n" "$*" >> "$CALL_LOG"\ncase "$*" in *"$FAIL_MATCH"*) if [ -n "$FAIL_MATCH" ]; then exit 9; fi ;; esac\n')
  25. fake.chmod(0o755)
  26. env={**os.environ,'PATH':str(bin_dir)+os.pathsep+os.environ['PATH'],'CALL_LOG':str(root/'calls.log'),'FAIL_MATCH':fail}
  27. env.pop('DEPLOY_PYTHON', None)
  28. if deploy_python is not None: env['DEPLOY_PYTHON'] = deploy_python
  29. result=subprocess.run([BASH,str(root/'deploy.sh'),action,*args],env=env,capture_output=True,text=True)
  30. calls=(root/'calls.log').read_text() if (root/'calls.log').exists() else ''
  31. return result.returncode,calls,(root/'.env').read_text()
  32. def test_templates_and_dms_bootstrap_then_deploy(self):
  33. for action in ('templates','dms','start'):
  34. code,calls,env=self.run_script(action)
  35. self.assertEqual(code,0,calls)
  36. self.assertLess(calls.index('python install'),calls.index('sync --frozen'))
  37. self.assertLess(calls.index('sync --frozen'),calls.index('download_model.py'))
  38. lines=calls.splitlines()
  39. self.assertTrue(lines[-1].endswith('scripts/start_service.py'))
  40. if action == 'start':
  41. self.assertNotIn('update_data.py',calls)
  42. else:
  43. self.assertIn('update_data.py --mode '+action,lines[-2])
  44. self.assertLess(calls.index('download_model.py'),calls.index('update_data.py'))
  45. self.assertEqual(env,'KEEP=unchanged\n')
  46. def test_python_selection_is_consistent_throughout_pipeline(self):
  47. for override, expected in ((None, '3.14.5'), ('3.11', '3.11')):
  48. with self.subTest(override=override):
  49. code, calls, _ = self.run_script('templates', deploy_python=override)
  50. self.assertEqual(code, 0, calls)
  51. lines = calls.splitlines()
  52. self.assertEqual(lines[0], f'python install {expected}')
  53. self.assertEqual(lines[1], f'sync --frozen --python {expected}')
  54. for line in lines[2:]:
  55. self.assertTrue(line.startswith(f'run --frozen --no-sync --python {expected} python '), line)
  56. def test_restart_starts_with_restart_flag_without_data_update(self):
  57. code,calls,_=self.run_script('restart')
  58. self.assertEqual(code,0)
  59. self.assertNotIn('update_data.py',calls)
  60. self.assertTrue(calls.splitlines()[-1].endswith('start_service.py --restart'))
  61. def test_setup_never_runs_business_deployment(self):
  62. code,calls,_=self.run_script('setup')
  63. self.assertEqual(code,0)
  64. self.assertIn('download_model.py',calls)
  65. self.assertNotIn('update_data.py',calls)
  66. self.assertNotIn('start_service.py',calls)
  67. def test_environment_and_download_failures_stop_pipeline(self):
  68. for failure in ('sync --frozen','download_model.py'):
  69. code,calls,_=self.run_script('templates',failure)
  70. self.assertEqual(code,9)
  71. self.assertNotIn('update_data.py',calls)
  72. self.assertNotIn('scripts/start_service.py\n',calls)
  73. def test_update_failure_prevents_start(self):
  74. for action in ('templates','dms'):
  75. code,calls,_=self.run_script(action,'update_data.py')
  76. self.assertEqual(code,9)
  77. self.assertIn('update_data.py --mode '+action,calls)
  78. self.assertNotIn('scripts/start_service.py\n',calls)
  79. def test_start_failure_and_arguments(self):
  80. code,calls,_=self.run_script('start',args=('--host','127.0.0.1','--port','8123'))
  81. self.assertEqual(code,0)
  82. self.assertTrue(calls.splitlines()[-1].endswith('start_service.py --host 127.0.0.1 --port 8123'))
  83. code,_,_=self.run_script('start','start_service.py --host',args=('--host','127.0.0.1'))
  84. self.assertEqual(code,9)
  85. def test_invalid_config_prevents_model_and_update(self):
  86. code,calls,_=self.run_script('templates','--check-config')
  87. self.assertEqual(code,9)
  88. self.assertNotIn('download_model.py',calls)
  89. self.assertNotIn('update_data.py',calls)
  90. def test_missing_env_is_created_without_business_update(self):
  91. code,calls,env=self.run_script('templates',env_exists=False)
  92. self.assertEqual(code,2)
  93. self.assertEqual(calls,'')
  94. self.assertEqual(env,'EXAMPLE=1\n')
  95. class ModelTests(unittest.TestCase):
  96. def test_incomplete_shards_do_not_count_as_ready(self):
  97. with tempfile.TemporaryDirectory() as tmp:
  98. root=Path(tmp)
  99. for name in ('config.json','tokenizer.json','tokenizer_config.json'):
  100. (root/name).write_text('{}')
  101. (root/'modules.json').write_text('[{"type":"sentence_transformers.models.Normalize","path":"2_Normalize"}]')
  102. (root/'model.safetensors.index.json').write_text(json.dumps({'weight_map':{'a':'part1','b':'part2'}}))
  103. (root/'part1').write_text('weight')
  104. self.assertFalse(download_model.model_files_ready(root))
  105. (root/'part2').write_text('weight')
  106. self.assertTrue(download_model.model_files_ready(root))
  107. def test_ready_model_skips_network_but_verifies_loading(self):
  108. with patch.object(download_model,'get_embedding_model_dir',return_value=Path('model')), patch.object(download_model,'model_files_ready',return_value=True), patch.object(download_model,'verify_model') as verify, patch.object(download_model.subprocess,'run') as run:
  109. download_model.ensure_model()
  110. run.assert_not_called()
  111. verify.assert_called_once()
  112. def test_check_only_missing_model_never_downloads(self):
  113. with patch.object(download_model,'get_embedding_model_dir',return_value=Path('model')), patch.object(download_model,'model_files_ready',return_value=False), patch.object(download_model.subprocess,'run') as run:
  114. with self.assertRaises(RuntimeError): download_model.ensure_model(check_only=True)
  115. run.assert_not_called()
  116. if __name__=='__main__': unittest.main()