Merge pull request 'Binárky pro Windows a macOS' (#9) from agents/claude/binarky-win-mac into main
testy / pytest (3.11) (push) Successful in 1s
testy / pytest (3.12) (push) Successful in 0s
testy / pytest (3.13) (push) Successful in 1s
testy / pytest (3.14) (push) Successful in 1s

Reviewed-on: #9
This commit was merged in pull request #9.
This commit is contained in:
david-spacil committed 2026-08-22 13:46:55 +02:00
commit 68a9f1d19f
8 files changed
+337 -31

No files matched your search

+124
View File
@@ -0,0 +1,124 @@
# Binárky pro Windows a macOS.
#
# PyInstaller neumí křížovou kompilaci: binárku pro každý systém musí postavit
# ten systém. Kupovat kvůli tomu Mac nebo krmit Windows VM by bylo přehnané,
# tak si tyhle stroje půjčujeme od GitHub Actions.
#
# GitHub je tu jen půjčená dílna, ne druhý domov projektu: repozitář se sem
# z Gitey zrcadlí a hotové soubory se posílají zpátky na tamní release přes
# Gitea API. Jedna stránka s releasy, ta původní.
#
# Linux se tu schválně nestaví — o ten se stará vlastní runner
# (.gitea/workflows/binarka.yml) a dělá to líp, proti glibc 2.17.
name: binárky
on:
push:
branches: [main]
tags: ["v*"]
workflow_dispatch:
env:
GITEA: https://gitea.spacilovi.eu
GITEA_REPO: david-spacil/dice-counter
jobs:
build:
strategy:
fail-fast: false
matrix:
include:
- os: windows-latest
asset: kostky-windows-x86_64.exe
built: dist/kostky.exe
- os: macos-13 # poslední Intel běhoun
asset: kostky-macos-x86_64
built: dist/kostky
- os: macos-latest # Apple Silicon
asset: kostky-macos-arm64
built: dist/kostky
runs-on: ${{ matrix.os }}
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@v10
- name: Testy
run: uv run --with-requirements requirements.txt --no-project pytest -q
- name: Binárka
run: ./build.sh
- name: Zkouška
run: |
# Binárka se musí umět rozbalit, najít šablony a začít poslouchat.
# Adresa se vnucuje, ať zkouška nezávisí na síti běhounu.
export DICE_HOST=127.0.0.1 DICE_PORT=8123 DICE_DB=zkouska/dice.db
"${{ matrix.built }}" &
for _ in $(seq 30); do
curl -sf -o /dev/null "http://127.0.0.1:8123/" && break
sleep 1
done
curl -sf -o /dev/null "http://127.0.0.1:8123/" || {
echo "Server se nerozeběhl." >&2; exit 1; }
curl -sf -o /dev/null "http://127.0.0.1:8123/static/style.css"
curl -sf "http://127.0.0.1:8123/board" | grep -q "127.0.0.1:8123"
test -f zkouska/dice.db
echo "Binárka jede."
- name: Na release
if: startsWith(github.ref, 'refs/tags/')
env:
TOKEN: ${{ secrets.GITEA_TOKEN }}
ASSET: ${{ matrix.asset }}
run: |
api="$GITEA/api/v1/repos/$GITEA_REPO"
auth="Authorization: token $TOKEN"
json() { uv run --no-project python -c "$@"; }
# Release u tagu být může, ale nemusí — dá se založit i ručně a
# sahají na něj čtyři buildy naráz, tak se po neúspěchu ptáme znovu.
for _ in 1 2 3; do
id=$(curl -s -H "$auth" "$api/releases/tags/${GITHUB_REF_NAME}" \
| json 'import json,sys; print(json.load(sys.stdin).get("id",""))')
[ -n "$id" ] && break
curl -s -o /dev/null -X POST -H "$auth" -H "Content-Type: application/json" \
-d "{\"tag_name\":\"${GITHUB_REF_NAME}\",\"name\":\"${GITHUB_REF_NAME}\"}" \
"$api/releases"
done
[ -n "$id" ] || { echo "Release ${GITHUB_REF_NAME} se nepodařilo najít." >&2; exit 1; }
# macOS zná jen shasum, Git Bash na Windows jen sha256sum. Formát
# výsledku je stejný, takže se dá ověřit kterýmkoli z nich.
if command -v sha256sum >/dev/null; then
soucet() { sha256sum "$@"; }
else
soucet() { shasum -a 256 "$@"; }
fi
# Součet se počítá nad jménem, pod kterým se soubor stahuje, aby
# šel ověřit rovnou: shasum -a 256 -c kostky-macos-arm64.sha256
cp "${{ matrix.built }}" "$ASSET"
soucet "$ASSET" | tee "$ASSET.sha256"
for file in "$ASSET" "$ASSET.sha256"; do
# Aby se dal build zopakovat, stará příloha stejného jména jde pryč.
curl -s -H "$auth" "$api/releases/$id/assets" \
| json 'import json,sys; [print(a["id"]) for a in json.load(sys.stdin) if a["name"] == sys.argv[1]]' "$file" \
| while read -r old; do
curl -sf -X DELETE -H "$auth" "$api/releases/$id/assets/$old"
done
curl -sf -o /dev/null -X POST -H "$auth" -F "attachment=@$file" \
"$api/releases/$id/assets?name=$file"
echo "$file pověšen na ${GITHUB_REF_NAME}"
done
+67 -16
View File
@@ -8,16 +8,45 @@ ten samý Adam jako z minulého měsíce. Žádné účty, žádná hesla.
## Spuštění ## Spuštění
Stáhni binárku z [releases](https://gitea.spacilovi.eu/david-spacil/dice-counter/releases) Stáhni binárku pro svůj systém z
a spusť ji: [releases](https://gitea.spacilovi.eu/david-spacil/dice-counter/releases):
| Systém | Soubor |
|---|---|
| Linux (x86-64) | `kostky-linux-x86_64` |
| macOS (Apple Silicon) | `kostky-macos-arm64` |
| macOS (Intel) | `kostky-macos-x86_64` |
| Windows (x86-64) | `kostky-windows-x86_64.exe` |
Nic se neinstaluje — Python, Flask i šablony jsou uvnitř.
Na Linuxu a macOS:
```bash ```bash
chmod +x kostky chmod +x kostky-*
./kostky ./kostky-linux-x86_64
``` ```
Nic se neinstaluje — Python, Flask i šablony jsou uvnitř. Zatím jen pro Linux Na Windows stačí na `.exe` poklepat.
na x86-64; postavené proti glibc 2.17, takže jede prakticky všude.
Linuxová binárka je postavená proti glibc 2.17, takže jede prakticky všude.
### Než to poprvé pustíš
Binárky nejsou podepsané — podpisové certifikáty stojí tisíce ročně a na
počitadlo kostek by to byl nesmysl. Systémy si toho všimnou:
- **macOS** stažený soubor označí za karanténní a odmítne ho spustit. Buď
značku sundej (`xattr -dr com.apple.quarantine kostky-macos-arm64`), nebo
soubor stáhni rovnou z terminálu přes `curl -LO` — tudy se karanténa
nenastavuje.
- **Windows** ukáže modré okno SmartScreenu. *Další informace* →
*Přesto spustit*. Občas si postěžuje i antivirus; u zabalených Python
programů je to běžný falešný poplach. Při prvním spuštění se ještě zeptá
brána firewall — bez povolení do místní sítě se telefon nepřipojí.
Kdo tomu nechce věřit, ověří si stažený soubor podle přiloženého součtu
(níž) nebo si ho postaví sám — `./build.sh`, zdrojáky jsou tady celé.
Ze zdrojáků to je stejně krátké: Ze zdrojáků to je stejně krátké:
@@ -53,7 +82,9 @@ server vypíše při startu.
| Spuštěno | Databáze | | Spuštěno | Databáze |
|---|---| |---|---|
| binárkou | `~/.local/share/kostky/dice.db` | | binárkou na Linuxu | `~/.local/share/kostky/dice.db` |
| binárkou na macOS | `~/Library/Application Support/kostky/dice.db` |
| binárkou na Windows | `%LOCALAPPDATA%\kostky\dice.db` |
| ze zdrojáků | `dice.db` v pracovním adresáři | | ze zdrojáků | `dice.db` v pracovním adresáři |
Binárka se při každém spuštění rozbaluje do dočasného adresáře a pouští se Binárka se při každém spuštění rozbaluje do dočasného adresáře a pouští se
@@ -101,6 +132,8 @@ při remíze na prvním místě se hraje dál.
| `web.py` | Flask aplikace | | `web.py` | Flask aplikace |
| `dice.py` | totéž v terminálu, bez ukládání | | `dice.py` | totéž v terminálu, bez ukládání |
| `build.sh`, `kostky.spec` | stavba binárky | | `build.sh`, `kostky.spec` | stavba binárky |
| `.gitea/workflows/` | testy a linuxová binárka doma |
| `.github/workflows/` | binárky pro Windows a macOS |
Terminálová verze zůstává funkční jako záloha: Terminálová verze zůstává funkční jako záloha:
@@ -135,26 +168,44 @@ Staví se proti samostatnému CPythonu od `uv`, ne proti systémovému. Ten je
slinkovaný s glibc 2.17; postavené proti Pythonu z Fedory 44 by to chtělo slinkovaný s glibc 2.17; postavené proti Pythonu z Fedory 44 by to chtělo
glibc 2.43 a nešlo by spustit skoro nikde — glibc drží zpětnou kompatibilitu, glibc 2.43 a nešlo by spustit skoro nikde — glibc drží zpětnou kompatibilitu,
ne dopřednou. Nic z hostitele se do binárky nedostane, takže kontejner k tomu ne dopřednou. Nic z hostitele se do binárky nedostane, takže kontejner k tomu
potřeba není. Výsledek je v `dist/kostky`. potřeba není. Výsledek je v `dist/`.
Stejný `build.sh` staví i na macOS a na Windows (v Git Bash) — jen tam bez
té starosti o glibc. Křížem to nejde: PyInstaller balí do výsledku interpret
a knihovny toho systému, na kterém běží, takže **binárku pro každý systém
musí postavit ten systém.**
Terminálová verze součástí binárky není; ta se pouští ze zdrojáků. Terminálová verze součástí binárky není; ta se pouští ze zdrojáků.
Releasy si tohle dělají samy. Otagovaný commit spustí ### Jak vznikají releasy
`.gitea/workflows/binarka.yml`, ta binárku postaví, projede testy, zkusí
ji nastartovat a pověsí ji na release jako `kostky-linux-x86_64`: Otagovaný commit spustí stavbu na obou stranách:
| Kde | Co staví | Workflow |
|---|---|---|
| vlastní runner u Gitey | Linux | `.gitea/workflows/binarka.yml` |
| GitHub Actions | Windows, macOS ×2 | `.github/workflows/binarky.yml` |
```bash ```bash
git tag v1.0.0 && git push origin v1.0.0 git tag v1.0.0 && git push origin v1.0.0
``` ```
Vedle binárky se věší i `kostky-linux-x86_64.sha256`, takže se stažený GitHub je tu jen půjčená dílna. Repozitář se tam z Gitey zrcadlí
soubor dá ověřit: ([mirror](https://github.com/david-spacil/dice-counter)), postavené soubory
se posílají zpátky na zdejší release přes Gitea API a projekt má pořád jednu
stránku s releasy — tuhle. Linux se na GitHubu schválně nestaví; doma to jde
proti staré glibc, a když GitHub vypadne, release má aspoň tu platformu,
na které to reálně poběží.
Každá stavba nejdřív projede testy a zkusí hotovou binárku nastartovat, než
ji kamkoli pověsí. Vedle každé visí i `.sha256`, takže se stažený soubor dá
ověřit:
```bash ```bash
sha256sum -c kostky-linux-x86_64.sha256 sha256sum -c kostky-linux-x86_64.sha256 # na macOS: shasum -a 256 -c
``` ```
Stejná stavba běží i nad každým pull requestem, jen se nikam nevěší. Nad pull requesty se Linux staví taky, jen se nikam nevěší.
## Testy ## Testy
@@ -165,5 +216,5 @@ uv run --with-requirements requirements.txt --no-project pytest
Nebo z připraveného prostředí prostě `pytest`. Nebo z připraveného prostředí prostě `pytest`.
Nad každým pull requestem a nad `main` běží `.gitea/workflows/testy.yml` — Nad každým pull requestem a nad `main` běží `.gitea/workflows/testy.yml` —
84 testů na Pythonu 3.11, 3.12, 3.13 i 3.14, a k tomu skriptovaná partie 99 testů na Pythonu 3.11, 3.12, 3.13 i 3.14, a k tomu skriptovaná partie
v terminálové verzi, na kterou pytest nesahá. v terminálové verzi, na kterou pytest nesahá.
+22 -9
View File
@@ -1,11 +1,15 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Postaví jednosouborovou binárku pro Linux do dist/kostky. # Postaví jednosouborovou binárku do dist/ — pro systém, na kterém běží.
# #
# Staví se proti samostatnému CPythonu od uv, ne proti systémovému. Ten je # PyInstaller neumí křížovou kompilaci: binárku pro každý systém musí postavit
# slinkovaný s glibc 2.17, takže binárka jede i na letitých systémech. # ten systém. Linux se staví doma na vlastním runneru, Windows a macOS na
# Postavená proti Pythonu z Fedory 44 by chtěla glibc 2.43 a nešla by # půjčených strojích GitHub Actions (.github/workflows/binarky.yml).
# spustit skoro nikde — glibc drží zpětnou kompatibilitu, ne dopřednou. #
# Nic z hostitele se do ní nedostane, takže kontejner k tomu není potřeba. # Staví se proti samostatnému CPythonu od uv, ne proti systémovému. Na Linuxu
# je slinkovaný s glibc 2.17, takže binárka jede i na letitých systémech.
# Postavená proti Pythonu z Fedory 44 by chtěla glibc 2.43 a nešla by spustit
# skoro nikde — glibc drží zpětnou kompatibilitu, ne dopřednou. Nic z hostitele
# se do ní nedostane, takže kontejner k tomu není potřeba.
set -euo pipefail set -euo pipefail
VERSION=${VERSION:-3.11} VERSION=${VERSION:-3.11}
@@ -13,10 +17,19 @@ BUILD=${BUILD:-.build}
uv python install "$VERSION" uv python install "$VERSION"
uv venv --quiet --managed-python --python "$VERSION" "$BUILD" uv venv --quiet --managed-python --python "$VERSION" "$BUILD"
uv pip install --quiet --python "$BUILD/bin/python" -r requirements.txt pyinstaller
"$BUILD/bin/pyinstaller" --clean --noconfirm \ # Windows dává spustitelné soubory venvu do Scripts/, zbytek světa do bin/.
python="$BUILD/bin/python"
pyinstaller="$BUILD/bin/pyinstaller"
if [ -d "$BUILD/Scripts" ]; then
python="$BUILD/Scripts/python.exe"
pyinstaller="$BUILD/Scripts/pyinstaller.exe"
fi
uv pip install --quiet --python "$python" -r requirements.txt pyinstaller
"$pyinstaller" --clean --noconfirm \
--distpath dist --workpath "$BUILD/work" kostky.spec --distpath dist --workpath "$BUILD/work" kostky.spec
echo echo
ls -lh dist/kostky ls -lh dist/
+6 -1
View File
@@ -5,6 +5,8 @@ Staví se přes `./build.sh`, ne ručně — ta se stará o to, aby výsledek je
i na starších systémech, než je ten tvůj. i na starších systémech, než je ten tvůj.
""" """
import sys
analysis = Analysis( analysis = Analysis(
["web.py"], ["web.py"],
pathex=["."], pathex=["."],
@@ -21,6 +23,9 @@ exe = EXE(
analysis.datas, analysis.datas,
name="kostky", name="kostky",
console=True, console=True,
strip=True, # Ořezání symbolů šetří pár megabajtů, ale jen na Linuxu. Na macOS by
# rozbilo podpis, který si PyInstaller sám přidává a bez kterého se
# binárka na Apple Silicon vůbec nespustí; na Windows nedělá nic.
strip=sys.platform == "linux",
upx=False, upx=False,
) )
+41 -2
View File
@@ -11,7 +11,9 @@ adresy a obecný typ rozhraní, ne to, co má kdo zrovna nainstalované.
import ipaddress import ipaddress
import os import os
import re
import socket import socket
import subprocess
from dataclasses import dataclass from dataclasses import dataclass
# Rozsah, ze kterého adresy přiděluje Tailscale (RFC 6598, CGNAT). # Rozsah, ze kterého adresy přiděluje Tailscale (RFC 6598, CGNAT).
@@ -123,8 +125,45 @@ def _from_interfaces() -> list[tuple[str, str]]:
return found return found
def parse_ifconfig(text: str) -> list[tuple[str, str]]:
"""Rozhraní a jejich IPv4 adresy z výstupu `ifconfig`.
Oddělená čistá funkce, ať se dá otestovat i bez macOS pod rukama.
"""
found = []
name = ""
for line in text.splitlines():
head = re.match(r"(\S+):", line)
if head:
name = head.group(1)
continue
addr = re.search(r"\binet (\d+\.\d+\.\d+\.\d+)", line)
if addr:
found.append((name, addr.group(1)))
return found
def _from_ifconfig() -> list[tuple[str, str]]:
"""Adresy přes `ifconfig`. Záloha pro macOS, kde ioctl výše nesedí.
Spouští se jen tam, kde předchozí cesta nic nenašla, takže na Linuxu
se neplatí nic. Trvá jednotky milisekund, cachovat to nemá cenu.
"""
try:
done = subprocess.run(["ifconfig", "-a"], capture_output=True,
text=True, timeout=5, check=True)
except (OSError, subprocess.SubprocessError):
return []
return parse_ifconfig(done.stdout)
def _from_hostname() -> list[tuple[str, str]]: def _from_hostname() -> list[tuple[str, str]]:
"""Záloha pro systémy, kde předchozí cesta nefunguje.""" """Poslední záloha. Na Windows vrátí adresy všech rozhraní, jinde
obvykle jen jednu — jména rozhraní se takhle nedozvíme."""
try: try:
infos = socket.getaddrinfo(socket.gethostname(), None, socket.AF_INET) infos = socket.getaddrinfo(socket.gethostname(), None, socket.AF_INET)
except OSError: except OSError:
@@ -144,7 +183,7 @@ def addresses() -> list[Address]:
primary = default_route_address() primary = default_route_address()
found = _from_interfaces() or _from_hostname() found = _from_interfaces() or _from_ifconfig() or _from_hostname()
if primary and primary not in [ip for _name, ip in found]: if primary and primary not in [ip for _name, ip in found]:
found.append(("", primary)) found.append(("", primary))
+13 -3
View File
@@ -16,20 +16,30 @@ from pathlib import Path
from core import FINAL_SCORE, Game, Turn from core import FINAL_SCORE, Game, Turn
def data_home() -> Path:
"""Adresář, kam podle zvyklostí systému patří data aplikací."""
if sys.platform == "win32":
local = os.environ.get("LOCALAPPDATA")
return Path(local) if local else Path.home() / "AppData" / "Local"
if sys.platform == "darwin":
return Path.home() / "Library" / "Application Support"
return Path(os.environ.get("XDG_DATA_HOME")
or Path.home() / ".local" / "share")
def default_db() -> Path: def default_db() -> Path:
"""Kam se ukládá databáze. """Kam se ukládá databáze.
Ze zdrojáků do pracovního adresáře — dá se tak mít víc rozehraných Ze zdrojáků do pracovního adresáře — dá se tak mít víc rozehraných
sad vedle sebe a je vidět, s čím se pracuje. Z binárky do datového sad vedle sebe a je vidět, s čím se pracuje. Z binárky do datového
adresáře podle XDG: binárka se spouští odkudkoli a rozbaluje se do adresáře podle systému: binárka se spouští odkudkoli a rozbaluje se do
dočasného adresáře, takže relativní cesta by databázi rozsypala po disku. dočasného adresáře, takže relativní cesta by databázi rozsypala po disku.
""" """
chosen = os.environ.get("DICE_DB") chosen = os.environ.get("DICE_DB")
if chosen: if chosen:
return Path(chosen) return Path(chosen)
if getattr(sys, "frozen", False): if getattr(sys, "frozen", False):
share = os.environ.get("XDG_DATA_HOME") or Path.home() / ".local" / "share" return data_home() / "kostky" / "dice.db"
return Path(share) / "kostky" / "dice.db"
return Path("dice.db") return Path("dice.db")
+47
View File
@@ -90,3 +90,50 @@ def test_v_systemu_se_neco_najde():
assert all(net.classify(a.ip, a.interface) for a in found) assert all(net.classify(a.ip, a.interface) for a in found)
assert len({a.ip for a in found}) == len(found) assert len({a.ip for a in found}) == len(found)
IFCONFIG_MACOS = """\
lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> mtu 16384
\toptions=1203<RXCSUM,TXCSUM,TXSTATUS,SW_TIMESTAMP>
\tinet 127.0.0.1 netmask 0xff000000
\tinet6 ::1 prefixlen 128
gif0: flags=8010<POINTOPOINT,MULTICAST> mtu 1280
en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
\tether 3c:22:fb:aa:bb:cc
\tinet6 fe80::14b0:6b1a:9e2f:1a4c%en0 prefixlen 64 secured scopeid 0xb
\tinet 192.168.0.42 netmask 0xffffff00 broadcast 192.168.0.255
\tmedia: autoselect
\tstatus: active
utun4: flags=8051<UP,POINTOPOINT,RUNNING,MULTICAST> mtu 1280
\tinet 100.99.98.97 --> 100.99.98.97 netmask 0xff000000
"""
def test_ifconfig_vytahne_rozhrani_a_adresy():
"""macOS na ioctl z Linuxu neslyší, tak se čte výstup ifconfigu."""
assert net.parse_ifconfig(IFCONFIG_MACOS) == [
("lo0", "127.0.0.1"),
("en0", "192.168.0.42"),
("utun4", "100.99.98.97"),
]
def test_ifconfig_si_neplete_inet6_s_inet():
"""`inet6` začíná stejně jako `inet`; IPv6 adresy tudy projít nesmí."""
assert net.parse_ifconfig("en0: flags=1\n\tinet6 fe80::1 prefixlen 64\n") == []
def test_adresy_z_ifconfigu_projdou_celym_retezem(monkeypatch):
"""Bez linuxového ioctl se sáhne po ifconfigu, teprve pak po hostname."""
monkeypatch.delenv("DICE_HOST", raising=False)
monkeypatch.setattr(net, "_from_interfaces", lambda: [])
monkeypatch.setattr(net, "_from_ifconfig",
lambda: net.parse_ifconfig(IFCONFIG_MACOS))
monkeypatch.setattr(net, "_from_hostname", lambda: pytest.fail("moc brzy"))
monkeypatch.setattr(net, "default_route_address", lambda: "192.168.0.42")
found = net.addresses()
assert [a.ip for a in found] == ["192.168.0.42", "100.99.98.97"]
assert found[0].primary and found[0].interface == "en0"
assert found[1].kind == "tailnet"
+17
View File
@@ -148,11 +148,28 @@ def test_umisteni_databaze_z_binarky(monkeypatch, tmp_path):
"""Binárka se rozbaluje do dočasného adresáře, databáze tam nesmí.""" """Binárka se rozbaluje do dočasného adresáře, databáze tam nesmí."""
monkeypatch.delenv("DICE_DB", raising=False) monkeypatch.delenv("DICE_DB", raising=False)
monkeypatch.setattr(storage.sys, "frozen", True, raising=False) monkeypatch.setattr(storage.sys, "frozen", True, raising=False)
monkeypatch.setattr(storage.sys, "platform", "linux")
monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path)) monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path))
assert storage.default_db() == tmp_path / "kostky" / "dice.db" assert storage.default_db() == tmp_path / "kostky" / "dice.db"
def test_data_home_podle_systemu(monkeypatch, tmp_path):
"""Každý systém má svůj adresář na data; XDG je jen ten linuxový."""
monkeypatch.setenv("LOCALAPPDATA", str(tmp_path / "AppData" / "Local"))
monkeypatch.setattr(Path, "home", classmethod(lambda cls: tmp_path))
monkeypatch.setattr(storage.sys, "platform", "win32")
assert storage.data_home() == tmp_path / "AppData" / "Local"
monkeypatch.setattr(storage.sys, "platform", "darwin")
assert storage.data_home() == tmp_path / "Library" / "Application Support"
monkeypatch.setattr(storage.sys, "platform", "linux")
monkeypatch.delenv("XDG_DATA_HOME", raising=False)
assert storage.data_home() == tmp_path / ".local" / "share"
def test_dice_db_prebije_oboje(monkeypatch, tmp_path): def test_dice_db_prebije_oboje(monkeypatch, tmp_path):
monkeypatch.setattr(storage.sys, "frozen", True, raising=False) monkeypatch.setattr(storage.sys, "frozen", True, raising=False)
monkeypatch.setenv("DICE_DB", str(tmp_path / "jinde.db")) monkeypatch.setenv("DICE_DB", str(tmp_path / "jinde.db"))