@@ -290,3 +290,205 @@ def test_run_script_argv_is_str(tmp_path):
290290""" )
291291
292292 subprocess .run ([sys .executable , "-m" , "slipcover" , "--silent" , cmdfile ], check = True )
293+
294+
295+ @pytest .mark .skipif (sys .platform == 'win32' , reason = 'Fails due to weird PermissionError' )
296+ def test_wrap_alembic (tmp_path , monkeypatch ):
297+ """Test that Alembic migrations are covered when using wrap_alembic."""
298+ import json
299+
300+ # Create a minimal alembic setup
301+ migrations_dir = tmp_path / "migrations"
302+ versions_dir = migrations_dir / "versions"
303+ versions_dir .mkdir (parents = True )
304+
305+ # Create alembic.ini
306+ alembic_ini = tmp_path / "alembic.ini"
307+ alembic_ini .write_text (f"""
308+ [alembic]
309+ script_location = { migrations_dir }
310+ sqlalchemy.url = sqlite:///:memory:
311+ """ )
312+
313+ # Create env.py
314+ env_py = migrations_dir / "env.py"
315+ env_py .write_text ("""
316+ from alembic import context
317+
318+ def run_migrations_offline():
319+ context.configure(url="sqlite:///:memory:", literal_binds=True)
320+ with context.begin_transaction():
321+ context.run_migrations()
322+
323+ def run_migrations_online():
324+ from sqlalchemy import create_engine
325+ connectable = create_engine("sqlite:///:memory:")
326+ with connectable.connect() as connection:
327+ context.configure(connection=connection)
328+ with context.begin_transaction():
329+ context.run_migrations()
330+
331+ if context.is_offline_mode():
332+ run_migrations_offline()
333+ else:
334+ run_migrations_online()
335+ """ )
336+
337+ # Create script.py.mako (required by alembic)
338+ script_mako = migrations_dir / "script.py.mako"
339+ script_mako .write_text ("" )
340+
341+ # Create a migration file
342+ migration_file = versions_dir / "001_test_migration.py"
343+ migration_file .write_text ('''
344+ """test migration"""
345+ revision = '001'
346+ down_revision = None
347+
348+ def upgrade():
349+ x = 1 # line 8
350+ y = 2 # line 9
351+
352+ def downgrade():
353+ pass # line 12
354+ ''' )
355+
356+ # Create a script that runs the alembic migration
357+ script = tmp_path / "run_migration.py"
358+ script .write_text (f"""
359+ import sys
360+ sys.path.insert(0, '{ tmp_path } ')
361+ from alembic.config import Config
362+ from alembic import command
363+
364+ alembic_cfg = Config('{ alembic_ini } ')
365+ command.upgrade(alembic_cfg, 'head')
366+ """ )
367+
368+ monkeypatch .chdir (tmp_path )
369+
370+ out = tmp_path / "coverage.json"
371+ result = subprocess .run (
372+ [sys .executable , "-m" , "slipcover" , "--json" , "--out" , str (out ),
373+ "--source" , str (versions_dir ), str (script )],
374+ capture_output = True ,
375+ text = True
376+ )
377+
378+ # The script should complete (may have warnings, but not crash)
379+ assert result .returncode == 0 , f"stdout: { result .stdout } \n stderr: { result .stderr } "
380+
381+ with out .open () as f :
382+ cov = json .load (f )
383+
384+ # Check that the migration file was covered
385+ # The path might be stored as relative or absolute depending on the working directory
386+ migration_files = [k for k in cov ['files' ].keys () if '001_test_migration.py' in k ]
387+ assert migration_files , f"Migration file not in coverage: { list (cov ['files' ].keys ())} "
388+
389+ # Check that lines in the upgrade function were executed
390+ file_cov = cov ['files' ][migration_files [0 ]]
391+ executed_lines = file_cov ['executed_lines' ]
392+ # Lines 7 and 8 are inside upgrade() function (x=1, y=2)
393+ assert 7 in executed_lines or 8 in executed_lines , f"upgrade() lines not executed, executed: { executed_lines } "
394+
395+
396+ @pytest .mark .skipif (sys .platform == 'win32' , reason = 'Fails due to weird PermissionError' )
397+ def test_wrap_alembic_with_branch (tmp_path , monkeypatch ):
398+ """Test that Alembic migrations are covered with branch coverage enabled."""
399+ import json
400+
401+ # Create a minimal alembic setup
402+ migrations_dir = tmp_path / "migrations"
403+ versions_dir = migrations_dir / "versions"
404+ versions_dir .mkdir (parents = True )
405+
406+ # Create alembic.ini
407+ alembic_ini = tmp_path / "alembic.ini"
408+ alembic_ini .write_text (f"""
409+ [alembic]
410+ script_location = { migrations_dir }
411+ sqlalchemy.url = sqlite:///:memory:
412+ """ )
413+
414+ # Create env.py
415+ env_py = migrations_dir / "env.py"
416+ env_py .write_text ("""
417+ from alembic import context
418+
419+ def run_migrations_offline():
420+ context.configure(url="sqlite:///:memory:", literal_binds=True)
421+ with context.begin_transaction():
422+ context.run_migrations()
423+
424+ def run_migrations_online():
425+ from sqlalchemy import create_engine
426+ connectable = create_engine("sqlite:///:memory:")
427+ with connectable.connect() as connection:
428+ context.configure(connection=connection)
429+ with context.begin_transaction():
430+ context.run_migrations()
431+
432+ if context.is_offline_mode():
433+ run_migrations_offline()
434+ else:
435+ run_migrations_online()
436+ """ )
437+
438+ # Create script.py.mako (required by alembic)
439+ script_mako = migrations_dir / "script.py.mako"
440+ script_mako .write_text ("" )
441+
442+ # Create a migration file with a branch
443+ migration_file = versions_dir / "001_test_migration.py"
444+ migration_file .write_text ('''
445+ """test migration"""
446+ revision = '001'
447+ down_revision = None
448+
449+ def upgrade():
450+ x = 1
451+ if x > 0: # branch
452+ y = 2
453+ else:
454+ y = 3
455+
456+ def downgrade():
457+ pass
458+ ''' )
459+
460+ # Create a script that runs the alembic migration
461+ script = tmp_path / "run_migration.py"
462+ script .write_text (f"""
463+ import sys
464+ sys.path.insert(0, '{ tmp_path } ')
465+ from alembic.config import Config
466+ from alembic import command
467+
468+ alembic_cfg = Config('{ alembic_ini } ')
469+ command.upgrade(alembic_cfg, 'head')
470+ """ )
471+
472+ monkeypatch .chdir (tmp_path )
473+
474+ out = tmp_path / "coverage.json"
475+ result = subprocess .run (
476+ [sys .executable , "-m" , "slipcover" , "--branch" , "--json" , "--out" , str (out ),
477+ "--source" , str (versions_dir ), str (script )],
478+ capture_output = True ,
479+ text = True
480+ )
481+
482+ # The script should complete (may have warnings, but not crash)
483+ assert result .returncode == 0 , f"stdout: { result .stdout } \n stderr: { result .stderr } "
484+
485+ with out .open () as f :
486+ cov = json .load (f )
487+
488+ # Check that the migration file was covered
489+ migration_files = [k for k in cov ['files' ].keys () if '001_test_migration.py' in k ]
490+ assert migration_files , f"Migration file not in coverage: { list (cov ['files' ].keys ())} "
491+
492+ # Check that branch info is present
493+ file_cov = cov ['files' ][migration_files [0 ]]
494+ assert 'executed_branches' in file_cov , "Branch coverage not recorded"
0 commit comments