odoo-dev 0.3.0__tar.gz → 0.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/.claude/settings.local.json +2 -1
  2. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/PKG-INFO +1 -1
  3. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/pyproject.toml +1 -1
  4. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/src/odoo_dev/__init__.py +1 -1
  5. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/src/odoo_dev/commands/run.py +42 -16
  6. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/src/odoo_dev/commands/setup.py +17 -82
  7. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/tests/fixtures/odoo-empty/.gitignore +1 -0
  8. odoo_dev-0.3.2/tests/test_drop_database.py +165 -0
  9. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/tests/test_setup.py +50 -0
  10. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/.claude/settings.json +0 -0
  11. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/.gitignore +0 -0
  12. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/.gitmodules +0 -0
  13. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/.python-version +0 -0
  14. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/README.md +0 -0
  15. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/src/odoo_dev/cli.py +0 -0
  16. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/src/odoo_dev/commands/__init__.py +0 -0
  17. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/src/odoo_dev/commands/db.py +0 -0
  18. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/src/odoo_dev/commands/docker.py +0 -0
  19. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/src/odoo_dev/config.py +0 -0
  20. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/src/odoo_dev/templates/__init__.py +0 -0
  21. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/src/odoo_dev/templates/docker/Dockerfile +0 -0
  22. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/src/odoo_dev/templates/docker/__init__.py +0 -0
  23. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/src/odoo_dev/templates/docker/docker-entrypoint.sh +0 -0
  24. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/src/odoo_dev/templates/vscode/__init__.py +0 -0
  25. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/src/odoo_dev/templates/vscode/launch.json +0 -0
  26. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/src/odoo_dev/templates/vscode/settings.json +0 -0
  27. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/src/odoo_dev/templates/vscode/tasks.json +0 -0
  28. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/src/odoo_dev/utils/__init__.py +0 -0
  29. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/src/odoo_dev/utils/console.py +0 -0
  30. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/tests/__init__.py +0 -0
  31. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/tests/fixtures/__init__.py +0 -0
  32. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/tests/fixtures/odoo-empty/.git +0 -0
  33. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/tests/fixtures/odoo-empty/.gitlab-ci.yml +0 -0
  34. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/tests/fixtures/odoo-empty/.gitmodules +0 -0
  35. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/tests/fixtures/odoo-empty/.repos/.gitkeep +0 -0
  36. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/tests/fixtures/odoo-empty/Notes/Warning Upgrade all +0 -0
  37. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/tests/fixtures/odoo-empty/README.md +0 -0
  38. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/tests/fixtures/odoo-empty/addons/.gitkeep +0 -0
  39. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/tests/fixtures/odoo-empty/note.txt +0 -0
  40. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/tests/fixtures/odoo-empty/requirements.txt +0 -0
  41. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/tests/fixtures/odoo-empty/tools/abstorelsymlink.sh +0 -0
  42. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/tests/fixtures/odoo-empty/tools/adaptation.md +0 -0
  43. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/tests/fixtures/odoo-empty/tools/after_migration.sh +0 -0
  44. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/tests/fixtures/odoo-empty/tools/checkout2 +0 -0
  45. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/tests/fixtures/odoo-empty/tools/cleanup.sql +0 -0
  46. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/tests/fixtures/odoo-empty/tools/deploy +0 -0
  47. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/tests/fixtures/odoo-empty/tools/fix_mail_templates.sql +0 -0
  48. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/tests/fixtures/odoo-empty/tools/gitpull +0 -0
  49. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/tests/fixtures/odoo-empty/tools/main.py +0 -0
  50. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/tests/fixtures/odoo-empty/tools/modules_cleanup.sql +0 -0
  51. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/tests/fixtures/odoo-empty/tools/post-upgrade steps.md +0 -0
  52. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/tests/fixtures/odoo-empty/tools/pre_commit_hook.py +0 -0
  53. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/tests/fixtures/odoo-empty/tools/requirements.txt +0 -0
  54. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/tests/fixtures/odoo-empty/tools/test +0 -0
  55. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/tests/fixtures/odoo-empty/tools/update_addon_versions.py +0 -0
  56. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/tests/test_cli.py +0 -0
  57. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/tests/test_config.py +0 -0
  58. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/tests/test_db.py +0 -0
  59. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/tests/test_integration.py +0 -0
  60. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/tests/test_run.py +0 -0
  61. {odoo_dev-0.3.0 → odoo_dev-0.3.2}/uv.lock +0 -0
@@ -6,7 +6,8 @@
6
6
  "Bash(ls:*)",
7
7
  "Bash(xargs:*)",
8
8
  "Bash(git commit:*)",
9
- "Bash(git push:*)"
9
+ "Bash(git push:*)",
10
+ "Bash(python -m pytest tests/test_drop_database.py -v -m slow)"
10
11
  ]
11
12
  }
12
13
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: odoo-dev
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: Odoo Development Environment Helper
5
5
  Project-URL: Homepage, https://git.bemade.org/bemade/odoo-dev
6
6
  Project-URL: Repository, https://git.bemade.org/bemade/odoo-dev
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "odoo-dev"
3
- version = "0.3.0"
3
+ version = "0.3.2"
4
4
  description = "Odoo Development Environment Helper"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -1,3 +1,3 @@
1
1
  """Odoo Development Environment Helper."""
2
2
 
3
- __version__ = "0.3.0"
3
+ __version__ = "0.3.2"
@@ -480,28 +480,35 @@ def _drop_database(cfg, db_name: str) -> None:
480
480
  venv_python = cfg.venv_path / "bin" / "python"
481
481
 
482
482
  # Try odoo-bin db drop first
483
- result = subprocess.run(
484
- [
485
- str(venv_python),
486
- str(cfg.odoo_bin),
487
- "-c",
488
- str(cfg.config_file),
489
- "db",
490
- "drop",
491
- db_name,
492
- ],
493
- capture_output=True,
494
- text=True,
495
- )
496
- if result.returncode == 0:
497
- return
483
+ try:
484
+ result = subprocess.run(
485
+ [
486
+ str(venv_python),
487
+ str(cfg.odoo_bin),
488
+ "-c",
489
+ str(cfg.config_file),
490
+ "db",
491
+ "drop",
492
+ db_name,
493
+ ],
494
+ capture_output=True,
495
+ text=True,
496
+ )
497
+ if result.returncode == 0:
498
+ return
499
+ except OSError:
500
+ pass
498
501
 
499
502
  # Fallback to dropdb
500
- warning(f"odoo-bin db drop failed, falling back to dropdb...")
503
+ warning("odoo-bin db drop failed, falling back to dropdb...")
501
504
  from odoo_dev.commands.db import _parse_db_config
502
505
 
503
506
  db_config = _parse_db_config(cfg.config_file)
504
507
  env = {**os.environ, "PGPASSWORD": db_config["password"]}
508
+
509
+ # Terminate active connections first — dropdb will fail if any remain
510
+ _terminate_connections(db_name, db_config)
511
+
505
512
  result = subprocess.run(
506
513
  [
507
514
  "dropdb",
@@ -521,6 +528,25 @@ def _drop_database(cfg, db_name: str) -> None:
521
528
  warning(result.stderr.strip())
522
529
 
523
530
 
531
+ def _terminate_connections(db_name: str, db_config: dict[str, str]) -> None:
532
+ """Terminate all active connections to a database."""
533
+ env = {**os.environ, "PGPASSWORD": db_config["password"]}
534
+ subprocess.run(
535
+ [
536
+ "psql",
537
+ "-h", db_config["host"],
538
+ "-p", db_config["port"],
539
+ "-U", db_config["user"],
540
+ "-d", "postgres",
541
+ "-c",
542
+ f"SELECT pg_terminate_backend(pid) FROM pg_stat_activity "
543
+ f"WHERE datname = '{db_name}' AND pid <> pg_backend_pid()",
544
+ ],
545
+ capture_output=True,
546
+ text=True,
547
+ env=env,
548
+ )
549
+
524
550
  def _get_addons_path(config_file) -> str:
525
551
  """Extract addons_path from odoo.conf."""
526
552
  for line in config_file.read_text().splitlines():
@@ -98,18 +98,6 @@ def setup(
98
98
  "--clean",
99
99
  help="Clean up before setup (remove existing venv, etc.)",
100
100
  ),
101
- db_user: str = typer.Option(
102
- "odoo",
103
- "-u",
104
- "--db-user",
105
- help="Database user for odoo.conf",
106
- ),
107
- master_password: str = typer.Option(
108
- "admin",
109
- "-w",
110
- "--master-password",
111
- help="Master (admin) password for odoo.conf",
112
- ),
113
101
  ) -> None:
114
102
  """Complete setup: clone Odoo repos, configure VSCode, build image."""
115
103
  # Prompt for versions if not configured
@@ -138,13 +126,13 @@ def setup(
138
126
  warning(
139
127
  f"This will install system dependencies and Python {cfg.python_version} if needed"
140
128
  )
141
- setup_venv(db_user=db_user, master_password=master_password)
129
+ setup_venv()
142
130
 
143
131
  # Prompt for Docker setup
144
132
  warning("\nDo you want to set up the Docker environment?")
145
133
  if typer.confirm("Continue with Docker setup?", default=True):
146
134
  # Set up Docker files first
147
- _setup_docker_files(cfg, community_only=community, db_user=db_user, master_password=master_password)
135
+ _setup_docker_files(cfg, community_only=community)
148
136
 
149
137
  success("\nBuilding Docker image...")
150
138
  from odoo_dev.commands.docker import build
@@ -159,25 +147,15 @@ def setup(
159
147
  success(" - Debug with VSCode: Use the configured launch profiles")
160
148
 
161
149
 
162
- def setup_venv(
163
- db_user: str = typer.Option(
164
- "odoo",
165
- "-u",
166
- "--db-user",
167
- help="Database user for odoo.conf",
168
- ),
169
- master_password: str = typer.Option(
170
- "admin",
171
- "-w",
172
- "--master-password",
173
- help="Master (admin) password for odoo.conf",
174
- ),
175
- ) -> None:
150
+ def setup_venv() -> None:
176
151
  """Set up local Python virtual environment for development."""
177
152
  cfg = load_config()
178
153
 
154
+ # Initialize submodules before anything else
155
+ _init_submodules(cfg)
156
+
179
157
  # Create config file first
180
- _setup_odoo_config(cfg, db_user=db_user, master_password=master_password)
158
+ _setup_odoo_config(cfg)
181
159
 
182
160
  success(f"Setting up Python virtual environment for Odoo {cfg.odoo_version}...")
183
161
 
@@ -242,49 +220,6 @@ def setup_venv(
242
220
  [*venv_pip, "pytest", "pytest-odoo", "debugpy", "manifestoo", "coverage"]
243
221
  )
244
222
 
245
- # Install Python external dependencies declared in addon manifests (runtime + test)
246
- if cfg.addons_dir.is_dir():
247
- success("Installing addon Python dependencies from manifests...")
248
- venv_python = cfg.venv_path / "bin" / "python"
249
- # Runtime deps via manifestoo
250
- result = subprocess.run(
251
- [
252
- str(venv_python),
253
- "-m",
254
- "manifestoo",
255
- "--select-addons-dir",
256
- str(cfg.addons_dir),
257
- "list-external-dependencies",
258
- "python",
259
- ],
260
- capture_output=True,
261
- text=True,
262
- env={**os.environ, "ODOO_VERSION": "", "ODOO_SERIES": ""},
263
- )
264
- all_python_deps: set[str] = set(result.stdout.split())
265
- # Test deps via direct manifest scan (manifestoo doesn't read test_external_dependencies)
266
- import ast
267
- for manifest_path in cfg.addons_dir.glob("*/__manifest__.py"):
268
- try:
269
- manifest = ast.literal_eval(manifest_path.read_text())
270
- test_deps = manifest.get("test_external_dependencies", {}).get("python", [])
271
- all_python_deps.update(test_deps)
272
- except Exception:
273
- pass
274
- python_deps = " ".join(all_python_deps)
275
- if python_deps:
276
- subprocess.run(
277
- [
278
- "uv",
279
- "pip",
280
- "install",
281
- "--python",
282
- str(venv_python),
283
- *python_deps.split(),
284
- ],
285
- check=False,
286
- )
287
-
288
223
  success("\nVirtual environment setup complete!")
289
224
  success(f"To activate: source {cfg.venv_path}/bin/activate")
290
225
 
@@ -341,7 +276,7 @@ def _init_submodules(cfg) -> None:
341
276
  warning(f"Submodule initialization had issues: {result.stderr}")
342
277
 
343
278
 
344
- def _setup_odoo_config(cfg, community_only: bool = False, db_user: str = "odoo", master_password: str = "admin") -> None:
279
+ def _setup_odoo_config(cfg, community_only: bool = False) -> None:
345
280
  """Create odoo.conf configuration file."""
346
281
  conf_dir = cfg.project_dir / "conf"
347
282
  conf_file = conf_dir / "odoo.conf"
@@ -376,8 +311,8 @@ def _setup_odoo_config(cfg, community_only: bool = False, db_user: str = "odoo",
376
311
 
377
312
  config_content = f"""[options]
378
313
  addons_path = {",".join(addons_paths)}
379
- admin_passwd = {master_password}
380
- db_user = {db_user}
314
+ admin_passwd = admin
315
+ db_user = odoo
381
316
  db_password = odoo
382
317
  """
383
318
 
@@ -386,7 +321,7 @@ db_password = odoo
386
321
  success(f"Config file created at {conf_file}")
387
322
 
388
323
 
389
- def _setup_docker_files(cfg, community_only: bool = False, db_user: str = "odoo", master_password: str = "admin") -> None:
324
+ def _setup_docker_files(cfg, community_only: bool = False) -> None:
390
325
  """Copy Docker templates to .odoo-deploy directory."""
391
326
  success("Setting up Docker files...")
392
327
 
@@ -406,10 +341,10 @@ def _setup_docker_files(cfg, community_only: bool = False, db_user: str = "odoo"
406
341
  success(f"Copied {filename}")
407
342
 
408
343
  # Create Docker odoo.conf
409
- _setup_docker_odoo_config(cfg, community_only=community_only, db_user=db_user, master_password=master_password)
344
+ _setup_docker_odoo_config(cfg, community_only=community_only)
410
345
 
411
346
 
412
- def _generate_docker_odoo_conf(community_only: bool = False, db_user: str = "odoo", master_password: str = "admin") -> str:
347
+ def _generate_docker_odoo_conf(community_only: bool = False) -> str:
413
348
  """Generate Docker-specific odoo.conf content."""
414
349
  # Build addons path for Docker (using /opt/project paths)
415
350
  addons_paths = [
@@ -430,10 +365,10 @@ def _generate_docker_odoo_conf(community_only: bool = False, db_user: str = "odo
430
365
 
431
366
  return f"""[options]
432
367
  addons_path = {",".join(addons_paths)}
433
- admin_passwd = {master_password}
368
+ admin_passwd = admin
434
369
  db_host = db
435
370
  db_port = 5432
436
- db_user = {db_user}
371
+ db_user = odoo
437
372
  db_password = odoo
438
373
  http_port = 8069
439
374
  gevent_port = 8072
@@ -451,7 +386,7 @@ data_dir = /opt/odoo-filestore
451
386
  """
452
387
 
453
388
 
454
- def _setup_docker_odoo_config(cfg, community_only: bool = False, db_user: str = "odoo", master_password: str = "admin") -> None:
389
+ def _setup_docker_odoo_config(cfg, community_only: bool = False) -> None:
455
390
  """Create Docker-specific odoo.conf configuration file."""
456
391
  conf_file = cfg.docker_config_file
457
392
 
@@ -460,7 +395,7 @@ def _setup_docker_odoo_config(cfg, community_only: bool = False, db_user: str =
460
395
  return
461
396
 
462
397
  success("Creating Docker Odoo configuration file...")
463
- conf_file.write_text(_generate_docker_odoo_conf(community_only=community_only, db_user=db_user, master_password=master_password))
398
+ conf_file.write_text(_generate_docker_odoo_conf(community_only=community_only))
464
399
  conf_file.chmod(0o600)
465
400
  success(f"Docker config file created at {conf_file}")
466
401
 
@@ -75,3 +75,4 @@ tsconfig.json
75
75
 
76
76
  # Ctags
77
77
  /tags
78
+ .odoo-data/
@@ -0,0 +1,165 @@
1
+ """Integration tests for _drop_database.
2
+
3
+ These tests verify that test databases are actually dropped after a test run.
4
+ Requires a local PostgreSQL instance accessible via peer authentication.
5
+ """
6
+
7
+ import subprocess
8
+ import time
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+
12
+ import pytest
13
+
14
+
15
+ def _pg_run(cmd: list[str], **kwargs) -> subprocess.CompletedProcess:
16
+ """Run a PostgreSQL command using peer auth on the local socket."""
17
+ return subprocess.run(cmd, capture_output=True, text=True, **kwargs)
18
+
19
+
20
+ def _db_exists(db_name: str) -> bool:
21
+ result = _pg_run(["psql", "-d", "postgres", "-tAc",
22
+ f"SELECT 1 FROM pg_database WHERE datname = '{db_name}'"])
23
+ return result.stdout.strip() == "1"
24
+
25
+
26
+ def _create_db(db_name: str) -> None:
27
+ result = _pg_run(["createdb", db_name])
28
+ assert result.returncode == 0, f"createdb failed: {result.stderr}"
29
+
30
+
31
+ def _drop_db_force(db_name: str) -> None:
32
+ """Forcefully drop a database (for test cleanup)."""
33
+ _pg_run(["psql", "-d", "postgres", "-c",
34
+ f"DROP DATABASE IF EXISTS \"{db_name}\" WITH (FORCE)"])
35
+
36
+
37
+ @dataclass
38
+ class FakeConfig:
39
+ """Minimal config to satisfy _drop_database's interface."""
40
+ project_dir: Path
41
+ venv_path: Path
42
+ odoo_bin: Path
43
+ config_file: Path
44
+
45
+
46
+ def _make_config(tmp_path: Path) -> FakeConfig:
47
+ """Create a fake config with an odoo.conf using peer auth.
48
+
49
+ Sets up a real python symlink but a fake odoo-bin that always exits 1,
50
+ so the odoo-bin drop method fails cleanly (nonzero exit) and the
51
+ dropdb fallback path is exercised — matching real-world behavior where
52
+ 'odoo-bin db drop' fails.
53
+ """
54
+ conf = tmp_path / "conf"
55
+ conf.mkdir()
56
+ config_file = conf / "odoo.conf"
57
+ # Use peer auth (no password, local socket) matching the test environment
58
+ config_file.write_text(
59
+ "[options]\n"
60
+ "db_host = /var/run/postgresql\n"
61
+ "db_port = 5432\n"
62
+ f"db_user = {subprocess.run(['whoami'], capture_output=True, text=True).stdout.strip()}\n"
63
+ "db_password =\n"
64
+ )
65
+ # Create a venv with a real python and a fake odoo-bin that always fails
66
+ venv_path = tmp_path / ".venv"
67
+ venv_path.mkdir()
68
+ bin_dir = venv_path / "bin"
69
+ bin_dir.mkdir()
70
+ import sys
71
+ (bin_dir / "python").symlink_to(sys.executable)
72
+
73
+ fake_odoo_bin = tmp_path / "odoo-bin"
74
+ fake_odoo_bin.write_text("#!/usr/bin/env python3\nimport sys; sys.exit(1)\n")
75
+ fake_odoo_bin.chmod(0o755)
76
+
77
+ return FakeConfig(
78
+ project_dir=tmp_path,
79
+ venv_path=venv_path,
80
+ odoo_bin=fake_odoo_bin,
81
+ config_file=config_file,
82
+ )
83
+
84
+
85
+ @pytest.mark.slow
86
+ class TestDropDatabase:
87
+ """Verify _drop_database actually removes the database."""
88
+
89
+ def _unique_db(self) -> str:
90
+ return f"test_odoodev_{int(time.time() * 1000)}"
91
+
92
+ def test_drop_removes_database(self, tmp_path: Path):
93
+ """Basic case: database with no active connections is dropped."""
94
+ from odoo_dev.commands.run import _drop_database
95
+
96
+ db_name = self._unique_db()
97
+ cfg = _make_config(tmp_path)
98
+
99
+ _create_db(db_name)
100
+ assert _db_exists(db_name), "Precondition: database should exist"
101
+
102
+ try:
103
+ _drop_database(cfg, db_name)
104
+ assert not _db_exists(db_name), (
105
+ f"Database {db_name} still exists after _drop_database!"
106
+ )
107
+ finally:
108
+ _drop_db_force(db_name)
109
+
110
+ def test_drop_works_when_venv_missing(self, tmp_path: Path):
111
+ """When venv python doesn't exist, should fall back to dropdb
112
+ instead of crashing with FileNotFoundError."""
113
+ from odoo_dev.commands.run import _drop_database
114
+
115
+ db_name = self._unique_db()
116
+ cfg = _make_config(tmp_path)
117
+ # Remove the python symlink to simulate missing venv
118
+ (cfg.venv_path / "bin" / "python").unlink()
119
+
120
+ _create_db(db_name)
121
+ assert _db_exists(db_name), "Precondition: database should exist"
122
+
123
+ try:
124
+ _drop_database(cfg, db_name)
125
+ assert not _db_exists(db_name), (
126
+ f"Database {db_name} still exists after _drop_database!"
127
+ )
128
+ finally:
129
+ _drop_db_force(db_name)
130
+
131
+ def test_drop_removes_database_with_active_connections(self, tmp_path: Path):
132
+ """Simulates a lingering Odoo worker holding a connection open.
133
+
134
+ This is the primary failure mode: dropdb refuses to drop a database
135
+ that has active connections, and _drop_database doesn't terminate
136
+ them first.
137
+ """
138
+ from odoo_dev.commands.run import _drop_database
139
+
140
+ db_name = self._unique_db()
141
+ cfg = _make_config(tmp_path)
142
+
143
+ _create_db(db_name)
144
+ assert _db_exists(db_name), "Precondition: database should exist"
145
+
146
+ # Hold an active connection (simulates a lingering Odoo worker)
147
+ blocker = subprocess.Popen(
148
+ ["psql", "-d", db_name, "-c", "SELECT pg_sleep(300)"],
149
+ stdout=subprocess.PIPE,
150
+ stderr=subprocess.PIPE,
151
+ )
152
+
153
+ try:
154
+ # Give the connection a moment to establish
155
+ time.sleep(0.5)
156
+
157
+ _drop_database(cfg, db_name)
158
+ assert not _db_exists(db_name), (
159
+ f"Database {db_name} still exists after _drop_database! "
160
+ "Active connections were not terminated before drop."
161
+ )
162
+ finally:
163
+ blocker.terminate()
164
+ blocker.wait(timeout=5)
165
+ _drop_db_force(db_name)
@@ -158,6 +158,56 @@ class TestSetupDockerFiles:
158
158
  assert cfg.docker_config_file.read_text() == "existing config"
159
159
 
160
160
 
161
+ class TestInitSubmodules:
162
+ """Test _init_submodules is called correctly from setup_venv."""
163
+
164
+ def test_setup_venv_calls_init_submodules(self, tmp_path: Path):
165
+ """setup_venv should initialize submodules before setting up the venv."""
166
+ from unittest.mock import call, patch
167
+
168
+ cfg = _make_cfg(tmp_path)
169
+ # Create .gitmodules to trigger the submodule check
170
+ (tmp_path / ".gitmodules").write_text("[submodule]\n")
171
+
172
+ with patch("odoo_dev.commands.setup.load_config", return_value=cfg), \
173
+ patch("odoo_dev.commands.setup._init_submodules") as mock_init, \
174
+ patch("odoo_dev.commands.setup._setup_odoo_config"), \
175
+ patch("odoo_dev.commands.setup._install_system_dependencies"), \
176
+ patch("odoo_dev.commands.setup._update_python_path"), \
177
+ patch("subprocess.run", return_value=type("R", (), {"returncode": 0})()):
178
+ from odoo_dev.commands.setup import setup_venv
179
+ setup_venv()
180
+
181
+ mock_init.assert_called_once_with(cfg)
182
+
183
+ def test_init_submodules_skips_if_no_gitmodules(self, tmp_path: Path):
184
+ """_init_submodules should do nothing if .gitmodules does not exist."""
185
+ from odoo_dev.commands.setup import _init_submodules
186
+
187
+ cfg = _make_cfg(tmp_path)
188
+ with patch("subprocess.run") as mock_run:
189
+ _init_submodules(cfg)
190
+ mock_run.assert_not_called()
191
+
192
+ def test_init_submodules_runs_git_command(self, tmp_path: Path):
193
+ """_init_submodules should run git submodule update when .gitmodules exists."""
194
+ from odoo_dev.commands.setup import _init_submodules
195
+
196
+ cfg = _make_cfg(tmp_path)
197
+ (tmp_path / ".gitmodules").write_text("[submodule]\n")
198
+
199
+ with patch("subprocess.run", return_value=type("R", (), {"returncode": 0, "stderr": ""})()) as mock_run:
200
+ _init_submodules(cfg)
201
+
202
+ mock_run.assert_called_once()
203
+ args = mock_run.call_args[0][0]
204
+ assert "git" in args
205
+ assert "submodule" in args
206
+ assert "update" in args
207
+ assert "--init" in args
208
+ assert "--recursive" in args
209
+
210
+
161
211
  class TestVscodeSetup:
162
212
  """Test vscode() copies templates correctly."""
163
213
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes