Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c404ea6ea5 | ||
|
|
80e6fee2df | ||
|
|
cfbfcb6f4d | ||
|
|
dc8ce892f8 | ||
|
|
7ef733b36e | ||
|
|
58247bf06f | ||
|
|
b833fe0deb | ||
|
|
626b9e91d8 | ||
|
|
345ed69dc1
|
||
|
|
a317582b72
|
||
|
|
e01c21446f | ||
|
|
0bade103d0 | ||
|
|
1df5a31a7c | ||
|
|
3d3e9dae13
|
||
|
|
f6a04e3166
|
No files matched your search
@@ -77,3 +77,28 @@ jobs:
|
|||||||
and
|
and
|
||||||
( [ .dependencies[] | (.vulns // [])[] | select((.fix_versions | length) > 0) ] | length == 0 )
|
( [ .dependencies[] | (.vulns // [])[] | select((.fix_versions | length) > 0) ] | length == 0 )
|
||||||
' pip-audit.json
|
' pip-audit.json
|
||||||
|
|
||||||
|
tests:
|
||||||
|
name: tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.13"
|
||||||
|
|
||||||
|
- name: Install Poetry
|
||||||
|
run: pip install "poetry>=2,<3"
|
||||||
|
|
||||||
|
- name: Install project and dependencies
|
||||||
|
run: poetry install --with dev
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: poetry run pytest
|
||||||
@@ -77,3 +77,28 @@ jobs:
|
|||||||
and
|
and
|
||||||
( [ .dependencies[] | (.vulns // [])[] | select((.fix_versions | length) > 0) ] | length == 0 )
|
( [ .dependencies[] | (.vulns // [])[] | select((.fix_versions | length) > 0) ] | length == 0 )
|
||||||
' pip-audit.json
|
' pip-audit.json
|
||||||
|
|
||||||
|
tests:
|
||||||
|
name: tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.13"
|
||||||
|
|
||||||
|
- name: Install Poetry
|
||||||
|
run: pip install "poetry>=2,<3"
|
||||||
|
|
||||||
|
- name: Install project and dependencies
|
||||||
|
run: poetry install --with dev
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: poetry run pytest
|
||||||
+3
-2
@@ -316,7 +316,7 @@ The wrapper snapshot flow is:
|
|||||||
wrapper_cmd
|
wrapper_cmd
|
||||||
-> _extract_paths_from_command()
|
-> _extract_paths_from_command()
|
||||||
-> _common_snapshot_root()
|
-> _common_snapshot_root()
|
||||||
-> create auto-YYYYMMDD-HHMMSS state
|
-> create a unique auto-YYYYMMDD-HHMMSS[-N] state
|
||||||
-> _iter_entries_for_target() for each path
|
-> _iter_entries_for_target() for each path
|
||||||
-> insert each relative path once
|
-> insert each relative path once
|
||||||
-> run subprocess.run(wrapper_cmd)
|
-> run subprocess.run(wrapper_cmd)
|
||||||
@@ -798,7 +798,8 @@ The list of `PlannedChange` objects produced before any mutation occurs.
|
|||||||
The `chguard -- chmod|chown|chgrp ...` mode that saves an automatic pre-command snapshot before running the command.
|
The `chguard -- chmod|chown|chgrp ...` mode that saves an automatic pre-command snapshot before running the command.
|
||||||
|
|
||||||
**Auto snapshot**
|
**Auto snapshot**
|
||||||
A wrapper-created state named like `auto-YYYYMMDD-HHMMSS`.
|
A wrapper-created state named like `auto-YYYYMMDD-HHMMSS`, with a numeric
|
||||||
|
suffix when needed to keep same-second snapshots unique.
|
||||||
|
|
||||||
**Owner restore**
|
**Owner restore**
|
||||||
Restoring numeric uid/gid with `os.chown(..., follow_symlinks=False)`.
|
Restoring numeric uid/gid with `os.chown(..., follow_symlinks=False)`.
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ auto-20251230-161301
|
|||||||
```
|
```
|
||||||
|
|
||||||
Auto-generated snapshots are visually distinguished in the output so they are easy to identify.
|
Auto-generated snapshots are visually distinguished in the output so they are easy to identify.
|
||||||
|
Snapshots created during the same second use a numeric suffix to remain unique,
|
||||||
|
for example `auto-20251230-161301-1`.
|
||||||
|
|
||||||
### Scope control
|
### Scope control
|
||||||
Restore:
|
Restore:
|
||||||
@@ -85,7 +87,7 @@ It only concerns itself with **ownership** and **permissions**.
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### From GuardUtils package repo
|
### From the SysMD package repository
|
||||||
|
|
||||||
This is the preferred method of installation.
|
This is the preferred method of installation.
|
||||||
|
|
||||||
@@ -95,7 +97,7 @@ This is the preferred method of installation.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo mkdir -p /usr/share/keyrings
|
sudo mkdir -p /usr/share/keyrings
|
||||||
curl -fsSL https://repo.sysmd.uk/guardutils/guardutils.gpg | sudo gpg --dearmor -o /usr/share/keyrings/guardutils.gpg
|
curl -fsSL https://repo.sysmd.uk/sysmd/sysmd.gpg | sudo gpg --dearmor -o /usr/share/keyrings/sysmd.gpg
|
||||||
```
|
```
|
||||||
|
|
||||||
The GPG fingerprint is `0032C71FA6A11EF9567D4434C5C06BD4603C28B1`.
|
The GPG fingerprint is `0032C71FA6A11EF9567D4434C5C06BD4603C28B1`.
|
||||||
@@ -103,7 +105,7 @@ The GPG fingerprint is `0032C71FA6A11EF9567D4434C5C06BD4603C28B1`.
|
|||||||
#### 2) Add the APT source
|
#### 2) Add the APT source
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/guardutils.gpg] https://repo.sysmd.uk/guardutils/debian stable main" | sudo tee /etc/apt/sources.list.d/guardutils.list
|
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/sysmd.gpg] https://repo.sysmd.uk/sysmd/debian stable main" | sudo tee /etc/apt/sources.list.d/sysmd.list
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 3) Update and install
|
#### 3) Update and install
|
||||||
@@ -118,20 +120,20 @@ sudo apt install chguard
|
|||||||
#### 1) Import the GPG key
|
#### 1) Import the GPG key
|
||||||
|
|
||||||
```
|
```
|
||||||
sudo rpm --import https://repo.sysmd.uk/guardutils/guardutils.gpg
|
sudo rpm --import https://repo.sysmd.uk/sysmd/sysmd.gpg
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 2) Add the repository configuration
|
#### 2) Add the repository configuration
|
||||||
|
|
||||||
```
|
```
|
||||||
sudo tee /etc/yum.repos.d/guardutils.repo > /dev/null << 'EOF'
|
sudo tee /etc/yum.repos.d/sysmd.repo > /dev/null << 'EOF'
|
||||||
[guardutils]
|
[sysmd]
|
||||||
name=GuardUtils Repository
|
name=SysMD Repository
|
||||||
baseurl=https://repo.sysmd.uk/guardutils/rpm/$basearch
|
baseurl=https://repo.sysmd.uk/sysmd/rpm/$basearch
|
||||||
enabled=1
|
enabled=1
|
||||||
gpgcheck=1
|
gpgcheck=1
|
||||||
repo_gpgcheck=1
|
repo_gpgcheck=1
|
||||||
gpgkey=https://repo.sysmd.uk/guardutils/guardutils.gpg
|
gpgkey=https://repo.sysmd.uk/sysmd/sysmd.gpg
|
||||||
EOF
|
EOF
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+25
-3
@@ -6,6 +6,7 @@ import grp
|
|||||||
import importlib.metadata
|
import importlib.metadata
|
||||||
import os
|
import os
|
||||||
import pwd
|
import pwd
|
||||||
|
import sqlite3
|
||||||
import stat
|
import stat
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
@@ -166,6 +167,28 @@ def _common_snapshot_root(paths: list[Path]) -> Path:
|
|||||||
return Path(os.path.commonpath([str(p.resolve()) for p in paths]))
|
return Path(os.path.commonpath([str(p.resolve()) for p in paths]))
|
||||||
|
|
||||||
|
|
||||||
|
def _create_auto_state(
|
||||||
|
conn, root_path: Path, created_by_uid: int
|
||||||
|
) -> tuple[int, str]:
|
||||||
|
base_name = f"auto-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
|
||||||
|
suffix = 0
|
||||||
|
|
||||||
|
while True:
|
||||||
|
auto_name = base_name if suffix == 0 else f"{base_name}-{suffix}"
|
||||||
|
try:
|
||||||
|
state_id = create_state(
|
||||||
|
conn,
|
||||||
|
auto_name,
|
||||||
|
str(root_path),
|
||||||
|
created_by_uid,
|
||||||
|
commit=False,
|
||||||
|
)
|
||||||
|
except sqlite3.IntegrityError:
|
||||||
|
suffix += 1
|
||||||
|
else:
|
||||||
|
return state_id, auto_name
|
||||||
|
|
||||||
|
|
||||||
def _type_for_mode(mode: int) -> str | None:
|
def _type_for_mode(mode: int) -> str | None:
|
||||||
if stat.S_ISDIR(mode):
|
if stat.S_ISDIR(mode):
|
||||||
return "dir"
|
return "dir"
|
||||||
@@ -451,12 +474,11 @@ def main() -> None:
|
|||||||
paths = _extract_paths_from_command(wrapper_cmd)
|
paths = _extract_paths_from_command(wrapper_cmd)
|
||||||
|
|
||||||
if paths:
|
if paths:
|
||||||
auto_name = f"auto-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
|
|
||||||
root_path = _common_snapshot_root(paths)
|
root_path = _common_snapshot_root(paths)
|
||||||
|
|
||||||
with conn:
|
with conn:
|
||||||
state_id = create_state(
|
state_id, auto_name = _create_auto_state(
|
||||||
conn, auto_name, str(root_path), os.getuid(), commit=False
|
conn, root_path, os.getuid()
|
||||||
)
|
)
|
||||||
|
|
||||||
seen_paths: set[str] = set()
|
seen_paths: set[str] = set()
|
||||||
|
|||||||
Generated
+20
-25
@@ -1,15 +1,15 @@
|
|||||||
# This file is automatically @generated by Poetry 2.5.0 and should not be changed by hand.
|
# This file is automatically @generated by Poetry 2.4.0 and should not be changed by hand.
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "argcomplete"
|
name = "argcomplete"
|
||||||
version = "3.6.3"
|
version = "3.7.2"
|
||||||
description = "Bash tab completion for argparse"
|
description = "Bash tab completion for argparse"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.10"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "argcomplete-3.6.3-py3-none-any.whl", hash = "sha256:f5007b3a600ccac5d25bbce33089211dfd49eab4a7718da3f10e3082525a92ce"},
|
{file = "argcomplete-3.7.2-py3-none-any.whl", hash = "sha256:6029205678bdd9c1c728a155f5f9ecf5812393f969eef58807641a2bc2aa5b19"},
|
||||||
{file = "argcomplete-3.6.3.tar.gz", hash = "sha256:62e8ed4fd6a45864acc8235409461b72c9a28ee785a2011cc5eb78318786c89c"},
|
{file = "argcomplete-3.7.2.tar.gz", hash = "sha256:aad8b69a0b9969edb62db0d1752354c0d50717b10e0cbb00e2a958381b9fc6b9"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
@@ -54,14 +54,14 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "filelock"
|
name = "filelock"
|
||||||
version = "4.0.8"
|
version = "4.1.0"
|
||||||
description = "A platform independent file lock."
|
description = "A platform independent file lock."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.10"
|
python-versions = ">=3.10"
|
||||||
groups = ["main", "dev"]
|
groups = ["main", "dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "filelock-4.0.8-py3-none-any.whl", hash = "sha256:325ff22f358c18443b1fcdfa0a7aa3faec4b500c2807c554719da4567b533d31"},
|
{file = "filelock-4.1.0-py3-none-any.whl", hash = "sha256:2ce9818e3e2d8f284c1a964414447ef148d42a5fd5e2a477a7118e574b293ec1"},
|
||||||
{file = "filelock-4.0.8.tar.gz", hash = "sha256:733d9b6b153fc63672f86104324186818b6bbe9dd7db84e9bb9887b6a04a2775"},
|
{file = "filelock-4.1.0.tar.gz", hash = "sha256:ad7f724afef953e731b1cc39bcd3a09166d72ed7fcdf29e6e88b1c3235c6715d"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -153,21 +153,16 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "platformdirs"
|
name = "platformdirs"
|
||||||
version = "4.5.1"
|
version = "4.12.4"
|
||||||
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
|
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.10"
|
python-versions = ">=3.10"
|
||||||
groups = ["main", "dev"]
|
groups = ["main", "dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31"},
|
{file = "platformdirs-4.12.4-py3-none-any.whl", hash = "sha256:78bfb9db2a8471ed7eebe3c3c932da413911042994e699b384fbb4493fa872d7"},
|
||||||
{file = "platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda"},
|
{file = "platformdirs-4.12.4.tar.gz", hash = "sha256:63743c02414e755de4e31b8f68125c1407495b86c5a006e203c01ff8b9924250"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
docs = ["furo (>=2025.9.25)", "proselint (>=0.14)", "sphinx (>=8.2.3)", "sphinx-autodoc-typehints (>=3.2)"]
|
|
||||||
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.4.2)", "pytest-cov (>=7)", "pytest-mock (>=3.15.1)"]
|
|
||||||
type = ["mypy (>=1.18.2)"]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pluggy"
|
name = "pluggy"
|
||||||
version = "1.6.0"
|
version = "1.6.0"
|
||||||
@@ -186,14 +181,14 @@ testing = ["coverage", "pytest", "pytest-benchmark"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pre-commit"
|
name = "pre-commit"
|
||||||
version = "3.8.0"
|
version = "4.6.2"
|
||||||
description = "A framework for managing and maintaining multi-language pre-commit hooks."
|
description = "A framework for managing and maintaining multi-language pre-commit hooks."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.10"
|
||||||
groups = ["dev"]
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"},
|
{file = "pre_commit-4.6.2-py2.py3-none-any.whl", hash = "sha256:e2dde9a75d3bce11bd3831c26d134df00a2803c1d818be6a0383c3dcda25dc4e"},
|
||||||
{file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"},
|
{file = "pre_commit-4.6.2.tar.gz", hash = "sha256:8f5d7bfb021ecdbcd9d49d89847082dd24172ccde534390081a679ad046e2441"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -340,14 +335,14 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rich"
|
name = "rich"
|
||||||
version = "14.2.0"
|
version = "15.0.0"
|
||||||
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
|
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8.0"
|
python-versions = ">=3.9.0"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd"},
|
{file = "rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb"},
|
||||||
{file = "rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4"},
|
{file = "rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -379,4 +374,4 @@ python-discovery = ">=1.6"
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.1"
|
lock-version = "2.1"
|
||||||
python-versions = ">=3.11,<4.0"
|
python-versions = ">=3.11,<4.0"
|
||||||
content-hash = "16fd1da8878d300a16fcff44591a79e0b90041b844eba6c976a53766a3fd5ec3"
|
content-hash = "b7038c1e7741ec33308a455f42c3dc2e9430365483f42a33b2ef07cdcd819805"
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "chguard"
|
name = "chguard"
|
||||||
version = "0.6.0"
|
version = "0.6.1"
|
||||||
description = "Safety-first tool to snapshot and restore filesystem ownership and permissions."
|
description = "Safety-first tool to snapshot and restore filesystem ownership and permissions."
|
||||||
authors = [{ name = "Marco D'Aleo", email = "marco@marcodaleo.com" }]
|
authors = [{ name = "Marco D'Aleo", email = "marco@marcodaleo.com" }]
|
||||||
license = "GPL-3.0-or-later"
|
license = "GPL-3.0-or-later"
|
||||||
@@ -21,7 +21,7 @@ Repository = "https://git.sysmd.uk/guardutils/chguard"
|
|||||||
chguard = "chguard.cli:main"
|
chguard = "chguard.cli:main"
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
pre-commit = "^3.8"
|
pre-commit = "^4.0.0"
|
||||||
pytest = "^9.1.1"
|
pytest = "^9.1.1"
|
||||||
|
|
||||||
[tool.black]
|
[tool.black]
|
||||||
|
|||||||
@@ -2,9 +2,13 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import chguard.cli as cli
|
||||||
from chguard.cli import _restore_preview_rows, main
|
from chguard.cli import _restore_preview_rows, main
|
||||||
from chguard.db import connect, create_state, init_db
|
from chguard.db import connect, create_state, init_db
|
||||||
from chguard.restore import PlannedChange
|
from chguard.restore import PlannedChange
|
||||||
@@ -109,3 +113,52 @@ def test_restore_with_only_skipped_items_does_not_prompt(
|
|||||||
output = capsys.readouterr().out
|
output = capsys.readouterr().out
|
||||||
assert "missing path" in output
|
assert "missing path" in output
|
||||||
assert "No applicable changes" in output
|
assert "No applicable changes" in output
|
||||||
|
|
||||||
|
|
||||||
|
def test_wrapper_uses_unique_auto_names_within_same_second(
|
||||||
|
tmp_path: Path, monkeypatch
|
||||||
|
) -> None:
|
||||||
|
db_path = tmp_path / "states.db"
|
||||||
|
target = tmp_path / "target"
|
||||||
|
target.write_text("data", encoding="utf-8")
|
||||||
|
|
||||||
|
class FixedDatetime:
|
||||||
|
@classmethod
|
||||||
|
def now(cls) -> datetime:
|
||||||
|
return datetime(2026, 10, 10, 0, 10, 48)
|
||||||
|
|
||||||
|
monkeypatch.setattr(cli, "datetime", FixedDatetime)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
cli.subprocess,
|
||||||
|
"run",
|
||||||
|
lambda command: SimpleNamespace(returncode=0),
|
||||||
|
)
|
||||||
|
|
||||||
|
for _ in range(2):
|
||||||
|
monkeypatch.setattr(
|
||||||
|
sys,
|
||||||
|
"argv",
|
||||||
|
[
|
||||||
|
"chguard",
|
||||||
|
"--db",
|
||||||
|
str(db_path),
|
||||||
|
"--",
|
||||||
|
"chmod",
|
||||||
|
"600",
|
||||||
|
str(target),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit) as exc_info:
|
||||||
|
main()
|
||||||
|
|
||||||
|
assert exc_info.value.code == 0
|
||||||
|
|
||||||
|
conn = connect(db_path)
|
||||||
|
rows = conn.execute("SELECT name FROM states ORDER BY id").fetchall()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
assert rows == [
|
||||||
|
("auto-20261010-001048",),
|
||||||
|
("auto-20261010-001048-1",),
|
||||||
|
]
|
||||||
Reference in new issue
Block a user