Compare commits
9 Commits
8c361c14ae
...
V3.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 1dd4b0a20c | |||
| 271ce73d09 | |||
| 1bd6c6b011 | |||
| d8bca535ac | |||
| 14257304f8 | |||
| 5738920c9c | |||
| 452862e0f3 | |||
| 999f47092d | |||
| 6441a92062 |
@@ -76,3 +76,6 @@ docker-compose.override.yml
|
|||||||
|
|
||||||
# Generato da packaging/windows/build.ps1 (TieMeasureFlow#82): moduli-strumento dagli entry point
|
# Generato da packaging/windows/build.ps1 (TieMeasureFlow#82): moduli-strumento dagli entry point
|
||||||
packaging/windows/_strumenti.py
|
packaging/windows/_strumenti.py
|
||||||
|
|
||||||
|
# #109: scritto da packaging/windows/sviluppo.ps1 (sha del submodule), non va versionato
|
||||||
|
/vision-engine-version.txt
|
||||||
|
|||||||
@@ -289,7 +289,7 @@ Nginx (dev): gzip abilitato, `client_max_body_size 50M`, `proxy_read_timeout 120
|
|||||||
|
|
||||||
Il runner di visione (`src/vision/`) e il suo worker FastAPI (`src/vision_worker/`) dipendono da `vendor/visionsuite`, un **submodule** che punta al repo TieVision-Core (`ssh://git@git.tielogic.xyz:222/Adriano/TieVision-Core.git`, ex visionsuite, rinominato il 15/10/2026; `vs-gui` e `vs-assistant` sono ora in TieVision-Studio e TieVision-Assistant, che TieMeasureFlow non usa; il path resta `vendor/visionsuite`): un `git clone` senza `--recurse-submodules` lascia la cartella vuota (chi ha già clonato: `git submodule update --init`). Le librerie di VisionSuite richiedono **Python 3.13**, mentre backend e frontend restano su Python 3.11 — è un albero a parte nel monorepo, isolato con il proprio extra `uv`.
|
Il runner di visione (`src/vision/`) e il suo worker FastAPI (`src/vision_worker/`) dipendono da `vendor/visionsuite`, un **submodule** che punta al repo TieVision-Core (`ssh://git@git.tielogic.xyz:222/Adriano/TieVision-Core.git`, ex visionsuite, rinominato il 15/10/2026; `vs-gui` e `vs-assistant` sono ora in TieVision-Studio e TieVision-Assistant, che TieMeasureFlow non usa; il path resta `vendor/visionsuite`): un `git clone` senza `--recurse-submodules` lascia la cartella vuota (chi ha già clonato: `git submodule update --init`). Le librerie di VisionSuite richiedono **Python 3.13**, mentre backend e frontend restano su Python 3.11 — è un albero a parte nel monorepo, isolato con il proprio extra `uv`.
|
||||||
|
|
||||||
Regola (Adriano, 07/10/2026): il submodule `vendor/visionsuite` segue `origin/main`: a ogni integrazione si porta all'ultimo commit (`git -C vendor/visionsuite fetch origin && git -C vendor/visionsuite checkout origin/main`) e si rilanciano i test della visione. La regola vale ora per `origin/main` di TieVision-Core. Pin attuale: 2e73691.
|
Regola (Adriano, 07/10/2026): il submodule `vendor/visionsuite` segue `origin/main`: a ogni integrazione si porta all'ultimo commit (`git -C vendor/visionsuite fetch origin && git -C vendor/visionsuite checkout origin/main`) e si rilanciano i test della visione. La regola vale ora per `origin/main` di TieVision-Core. Pin attuale: 9eb6f4c.
|
||||||
|
|
||||||
Il worker gira in un container separato (`Dockerfile.vision`), non pubblica porte verso l'esterno ed è raggiungibile solo dal server all'indirizzo interno `http://vision:8100`. Il container non ha accesso al repository Git del progetto principale, quindi il commit di VisionSuite in uso (`engine_version`) non può essere scoperto dentro l'immagine: va stampato al momento del build leggendo il commit dalla macchina che esegue `docker compose build`, con la variabile `VISION_ENGINE_VERSION`. Una build senza questa variabile fallisce subito, invece di produrre un worker che risponde con una versione indovinata:
|
Il worker gira in un container separato (`Dockerfile.vision`), non pubblica porte verso l'esterno ed è raggiungibile solo dal server all'indirizzo interno `http://vision:8100`. Il container non ha accesso al repository Git del progetto principale, quindi il commit di VisionSuite in uso (`engine_version`) non può essere scoperto dentro l'immagine: va stampato al momento del build leggendo il commit dalla macchina che esegue `docker compose build`, con la variabile `VISION_ENGINE_VERSION`. Una build senza questa variabile fallisce subito, invece di produrre un worker che risponde con una versione indovinata:
|
||||||
|
|
||||||
|
|||||||
@@ -4,14 +4,15 @@ Nuitka (che richiede ~20 minuti): stessi quattro server e stesse porte dell'exe
|
|||||||
8000 backend, 8100 visione, 8765 camera), stessa cartella dati %PROGRAMDATA%\Tielogic\TieMeasureFlow.
|
8000 backend, 8100 visione, 8765 camera), stessa cartella dati %PROGRAMDATA%\Tielogic\TieMeasureFlow.
|
||||||
Prima dell'avvio: ambiente Python 3.13 in build\venv313 (uv sync, solo con -Sincronizza o se manca),
|
Prima dell'avvio: ambiente Python 3.13 in build\venv313 (uv sync, solo con -Sincronizza o se manca),
|
||||||
tailwind.css e traduzioni .mo ricompilati, DLL di WeasyPrint in build\gtk (scaricate con
|
tailwind.css e traduzioni .mo ricompilati, DLL di WeasyPrint in build\gtk (scaricate con
|
||||||
pdf\prendi-gtk.ps1 se mancano).
|
pdf\prendi-gtk.ps1 se mancano). Scrive vision-engine-version.txt nella radice (sha del submodule
|
||||||
|
vendor/visionsuite) se git c'e'; senza git e senza il file si ferma con un errore chiaro (#109).
|
||||||
|
|
||||||
Come si usa (dalla radice del repo):
|
Come si usa (dalla radice del repo):
|
||||||
powershell -ExecutionPolicy Bypass -File packaging\windows\sviluppo.ps1 [-Sincronizza] [-Ascolto 0.0.0.0] [-EscludiCamere OECS]
|
powershell -ExecutionPolicy Bypass -File packaging\windows\sviluppo.ps1 [-Sincronizza] [-Ascolto 0.0.0.0] [-EscludiCamere OECS]
|
||||||
Dopo una modifica: Ctrl+C e rilancio (pochi secondi). Template e JS non richiedono altro;
|
Dopo una modifica: Ctrl+C e rilancio (pochi secondi). Template e JS non richiedono altro;
|
||||||
classi Tailwind nuove e msgid nuovi vengono ricompilati a ogni avvio.
|
classi Tailwind nuove e msgid nuovi vengono ricompilati a ogni avvio.
|
||||||
|
|
||||||
Dipende da: uv, npx (Node), la licenza tiemeasureflow di QUESTA macchina in
|
Dipende da: uv, npx (Node), git (opzionale: senza, serve vision-engine-version.txt gia' nella radice), la licenza tiemeasureflow di QUESTA macchina in
|
||||||
%PROGRAMDATA%\Tielogic\licenze\tiemeasureflow.json (impronta: avvio.py --impronta).
|
%PROGRAMDATA%\Tielogic\licenze\tiemeasureflow.json (impronta: avvio.py --impronta).
|
||||||
#>
|
#>
|
||||||
param(
|
param(
|
||||||
@@ -47,6 +48,24 @@ if (-not (Test-Path (Join-Path $gtk '*.dll'))) {
|
|||||||
}
|
}
|
||||||
$env:WEASYPRINT_DLL_DIRECTORIES = $gtk
|
$env:WEASYPRINT_DLL_DIRECTORIES = $gtk
|
||||||
|
|
||||||
|
# #109: il worker di visione rifiuta ogni job senza VISION_ENGINE_VERSION, che avvio.py legge da
|
||||||
|
# <radice>\vision-engine-version.txt (come nel pacchetto, build.ps1). Da sorgente lo scrive questo script.
|
||||||
|
$versione = Join-Path $radice 'vision-engine-version.txt'
|
||||||
|
$shaMotore = $null
|
||||||
|
# #109: solo se il submodule e' inizializzato (ha .git): altrimenti git risale al repo padre e darebbe lo sha di TMF
|
||||||
|
if ((Get-Command git -ErrorAction SilentlyContinue) -and (Test-Path (Join-Path $radice 'vendor\visionsuite\.git'))) {
|
||||||
|
$shaMotore = (git -C (Join-Path $radice 'vendor\visionsuite') rev-parse HEAD 2>$null)
|
||||||
|
if ($LASTEXITCODE -ne 0) { $shaMotore = $null }
|
||||||
|
}
|
||||||
|
if ($shaMotore) {
|
||||||
|
# 40 caratteri ASCII, UTF8 senza BOM e senza a capo (avvio.py fa .strip(), che non toglie il BOM)
|
||||||
|
[System.IO.File]::WriteAllText($versione, $shaMotore.Trim(), (New-Object System.Text.UTF8Encoding $false))
|
||||||
|
} elseif (Test-Path $versione) {
|
||||||
|
Write-Host "git non disponibile: uso $versione = $((Get-Content -Raw $versione).Trim())"
|
||||||
|
} else {
|
||||||
|
throw "Manca vision-engine-version.txt e git non puo' leggere vendor\visionsuite. Rimedio: crea l'archivio con 'uv run python scripts/archivio_sorgente.py --out tmf.zip' da un clone con git, oppure scrivi in $versione lo sha del submodule (40 caratteri, ASCII, senza BOM e senza a capo)."
|
||||||
|
}
|
||||||
|
|
||||||
if ($EscludiCamere) { $env:VS_CAMERA_ESCLUDI = $EscludiCamere } # #339: stesso percorso camera del PC fiera
|
if ($EscludiCamere) { $env:VS_CAMERA_ESCLUDI = $EscludiCamere } # #339: stesso percorso camera del PC fiera
|
||||||
Set-Location $radice
|
Set-Location $radice
|
||||||
& $py (Join-Path $PSScriptRoot 'avvio.py') --senza-browser --senza-finestre --host $Ascolto # errori in console, niente MessageBox
|
& $py (Join-Path $PSScriptRoot 'avvio.py') --senza-browser --senza-finestre --host $Ascolto # errori in console, niente MessageBox
|
||||||
|
|||||||
@@ -124,6 +124,16 @@ def test_variabili_del_contratto(avvio, tmp_path):
|
|||||||
assert len(e[k]) >= 32
|
assert len(e[k]) >= 32
|
||||||
|
|
||||||
|
|
||||||
|
def test_versione_motore_visione_letta_dalla_radice_sorgente(avvio, tmp_path, monkeypatch):
|
||||||
|
# #109: da sorgente il file vision-engine-version.txt sta nella radice del repo e arriva al worker
|
||||||
|
prog = tmp_path / "prog"
|
||||||
|
prog.mkdir()
|
||||||
|
(prog / "vision-engine-version.txt").write_bytes(b"a" * 40)
|
||||||
|
monkeypatch.setattr(avvio, "cartella_programma", lambda: prog)
|
||||||
|
avvio.imposta_ambiente(tmp_path / "dati", "127.0.0.1", {k: "x" * 32 for k in avvio.SEGRETI})
|
||||||
|
assert os.environ["VISION_ENGINE_VERSION"] == "a" * 40
|
||||||
|
|
||||||
|
|
||||||
def test_config_env_generato_una_volta_e_riletto(avvio, tmp_path):
|
def test_config_env_generato_una_volta_e_riletto(avvio, tmp_path):
|
||||||
dati = tmp_path / "Tielogic" / "TieMeasureFlow"
|
dati = tmp_path / "Tielogic" / "TieMeasureFlow"
|
||||||
primo = avvio.carica_segreti(dati)
|
primo = avvio.carica_segreti(dati)
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
"""Archivio sorgente di TieMeasureFlow per un PC senza git (TieMeasureFlow#109).
|
||||||
|
|
||||||
|
Cosa fa
|
||||||
|
Crea uno zip del repo a un commit, con dentro anche il submodule vendor/visionsuite (che
|
||||||
|
`git archive` da solo NON include) e il file vision-engine-version.txt nella radice dello zip.
|
||||||
|
Quel file contiene lo sha del submodule REGISTRATO nel commit (non la HEAD del checkout del
|
||||||
|
submodule): packaging/windows/avvio.py lo legge e lo passa al worker di visione come
|
||||||
|
VISION_ENGINE_VERSION; senza, il worker rifiuta ogni job. Formato: 40 caratteri ASCII,
|
||||||
|
senza BOM e senza a capo (avvio.py usa .strip(), che non toglie un BOM).
|
||||||
|
|
||||||
|
Come si usa (dalla radice del repo, con il submodule inizializzato)
|
||||||
|
uv run python scripts/archivio_sorgente.py --out C:/tmp/tmf.zip [--ref HEAD]
|
||||||
|
Esce 0 e stampa lo sha; esce 1 con un messaggio e senza lasciare lo zip se il submodule
|
||||||
|
non e' inizializzato o non ha il commit registrato (git submodule update --init vendor/visionsuite).
|
||||||
|
|
||||||
|
Dipende da: git nel PATH, solo libreria standard.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import zipfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
SUBMODULE = "vendor/visionsuite"
|
||||||
|
FILE_VERSIONE = "vision-engine-version.txt"
|
||||||
|
|
||||||
|
|
||||||
|
class ErroreArchivio(RuntimeError):
|
||||||
|
"""Errore chiaro da mostrare all'operatore (niente traceback)."""
|
||||||
|
|
||||||
|
|
||||||
|
def _git(cartella: Path, *args: str, binario: bool = False):
|
||||||
|
"""Esegue git in `cartella`; solleva ErroreArchivio con lo stderr se fallisce."""
|
||||||
|
r = subprocess.run(["git", "-C", str(cartella), *args], capture_output=True)
|
||||||
|
if r.returncode != 0:
|
||||||
|
raise ErroreArchivio(f"git {' '.join(args)} fallito in {cartella}: {r.stderr.decode(errors='replace').strip()}")
|
||||||
|
return r.stdout if binario else r.stdout.decode().strip()
|
||||||
|
|
||||||
|
|
||||||
|
def sha_registrato(repo: Path, ref: str) -> str:
|
||||||
|
"""Sha del submodule registrato nel commit `ref` (gitlink, modo 160000), non la HEAD del checkout."""
|
||||||
|
campi = _git(repo, "ls-tree", ref, SUBMODULE).split()
|
||||||
|
if len(campi) < 3 or campi[0] != "160000":
|
||||||
|
raise ErroreArchivio(f"{SUBMODULE} non e' un submodule registrato in {ref}")
|
||||||
|
return campi[2]
|
||||||
|
|
||||||
|
|
||||||
|
def crea_archivio(repo: Path, out: Path, ref: str = "HEAD") -> str:
|
||||||
|
"""Scrive lo zip in `out` e ritorna lo sha del submodule; in caso di errore non lascia nulla."""
|
||||||
|
repo, out = Path(repo), Path(out)
|
||||||
|
sha = sha_registrato(repo, ref)
|
||||||
|
sub = repo / SUBMODULE
|
||||||
|
# submodule non inizializzato o senza il commit registrato: errore prima di scrivere lo zip
|
||||||
|
if not (sub / ".git").exists():
|
||||||
|
raise ErroreArchivio(f"{SUBMODULE} non e' inizializzato: git submodule update --init {SUBMODULE}")
|
||||||
|
if subprocess.run(["git", "-C", str(sub), "cat-file", "-e", f"{sha}^{{commit}}"], capture_output=True).returncode != 0:
|
||||||
|
raise ErroreArchivio(f"{SUBMODULE} non ha il commit {sha}: git submodule update --init {SUBMODULE}")
|
||||||
|
|
||||||
|
zip_repo = _git(repo, "archive", "--format=zip", ref, binario=True)
|
||||||
|
zip_sub = _git(sub, "archive", "--format=zip", f"--prefix={SUBMODULE}/", sha, binario=True)
|
||||||
|
|
||||||
|
out.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
fd, tmp = tempfile.mkstemp(suffix=".zip.tmp", dir=out.parent) # scrittura atomica: o c'e' tutto o niente
|
||||||
|
os.close(fd)
|
||||||
|
try:
|
||||||
|
viste: set[str] = set()
|
||||||
|
with zipfile.ZipFile(tmp, "w", zipfile.ZIP_DEFLATED) as z:
|
||||||
|
for blob in (zip_repo, zip_sub):
|
||||||
|
with zipfile.ZipFile(io.BytesIO(blob)) as sorgente:
|
||||||
|
for info in sorgente.infolist():
|
||||||
|
if info.filename in viste:
|
||||||
|
continue # es. la cartella vendor/visionsuite/ del gitlink, gia' nel primo zip
|
||||||
|
viste.add(info.filename)
|
||||||
|
z.writestr(info, sorgente.read(info.filename), compress_type=zipfile.ZIP_DEFLATED)
|
||||||
|
if FILE_VERSIONE in viste:
|
||||||
|
raise ErroreArchivio(f"{FILE_VERSIONE} e' gia' nel repo a {ref}: va tolto dal versionamento")
|
||||||
|
# 40 caratteri ASCII, senza BOM e senza a capo (avvio.py fa .strip(), che non toglie un BOM)
|
||||||
|
z.writestr(FILE_VERSIONE, sha.encode("ascii"))
|
||||||
|
os.replace(tmp, out)
|
||||||
|
except BaseException:
|
||||||
|
if os.path.exists(tmp):
|
||||||
|
os.unlink(tmp)
|
||||||
|
raise
|
||||||
|
return sha
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None) -> int:
|
||||||
|
p = argparse.ArgumentParser(description="Zip del sorgente con submodule e vision-engine-version.txt (#109)")
|
||||||
|
p.add_argument("--out", required=True, type=Path, help="file .zip da creare")
|
||||||
|
p.add_argument("--ref", default="HEAD", help="commit da archiviare (default HEAD)")
|
||||||
|
a = p.parse_args(argv)
|
||||||
|
radice = Path(__file__).resolve().parents[1]
|
||||||
|
try:
|
||||||
|
sha = crea_archivio(radice, a.out, a.ref)
|
||||||
|
except ErroreArchivio as e:
|
||||||
|
print(f"ERRORE: {e}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
print(f"{a.out} creato; {FILE_VERSIONE} = {sha}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
"""Test di scripts/archivio_sorgente.py (TieMeasureFlow#109): repo temporaneo con un submodule finto.
|
||||||
|
|
||||||
|
Uso: uv run --no-project --python 3.13 --with pytest pytest -q scripts/tests/test_archivio_sorgente.py
|
||||||
|
"""
|
||||||
|
import importlib.util
|
||||||
|
import subprocess
|
||||||
|
import zipfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
_spec = importlib.util.spec_from_file_location(
|
||||||
|
"archivio_sorgente", Path(__file__).resolve().parents[1] / "archivio_sorgente.py")
|
||||||
|
archivio = importlib.util.module_from_spec(_spec)
|
||||||
|
_spec.loader.exec_module(archivio)
|
||||||
|
|
||||||
|
GIT = ["git", "-c", "core.autocrlf=false", "-c", "protocol.file.allow=always",
|
||||||
|
"-c", "user.name=t", "-c", "user.email=t@t", "-c", "commit.gpgsign=false"]
|
||||||
|
|
||||||
|
|
||||||
|
def _g(cartella, *args):
|
||||||
|
r = subprocess.run([*GIT, "-C", str(cartella), *args], capture_output=True, text=True)
|
||||||
|
assert r.returncode == 0, r.stderr
|
||||||
|
return r.stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _repo(tmp_path, nome):
|
||||||
|
d = tmp_path / nome
|
||||||
|
d.mkdir()
|
||||||
|
_g(d, "init", "-q", "-b", "main")
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def padre(tmp_path):
|
||||||
|
"""Repo padre con vendor/visionsuite registrato (submodule finto da un repo locale)."""
|
||||||
|
origine = _repo(tmp_path, "origine")
|
||||||
|
(origine / "motore.py").write_text("x = 1\n")
|
||||||
|
_g(origine, "add", ".")
|
||||||
|
_g(origine, "commit", "-q", "-m", "motore")
|
||||||
|
p = _repo(tmp_path, "padre")
|
||||||
|
(p / "app.py").write_text("print('tmf')\n")
|
||||||
|
_g(p, "add", ".")
|
||||||
|
_g(p, "commit", "-q", "-m", "app")
|
||||||
|
_g(p, "submodule", "add", "-q", str(origine), "vendor/visionsuite")
|
||||||
|
_g(p, "commit", "-q", "-m", "submodule")
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
def test_zip_con_repo_submodule_e_versione(padre, tmp_path):
|
||||||
|
atteso = _g(padre, "ls-tree", "HEAD", "vendor/visionsuite").split()[2]
|
||||||
|
out = tmp_path / "o" / "tmf.zip"
|
||||||
|
assert archivio.crea_archivio(padre, out) == atteso
|
||||||
|
with zipfile.ZipFile(out) as z:
|
||||||
|
nomi = z.namelist()
|
||||||
|
assert "app.py" in nomi and "vendor/visionsuite/motore.py" in nomi
|
||||||
|
dati = z.read("vision-engine-version.txt")
|
||||||
|
assert dati == atteso.encode("ascii") and len(dati) == 40
|
||||||
|
assert not dati.startswith(b"\xef\xbb\xbf") and b"\n" not in dati # niente BOM, niente a capo
|
||||||
|
|
||||||
|
|
||||||
|
def test_sha_resta_quello_registrato_se_il_submodule_avanza(padre, tmp_path):
|
||||||
|
registrato = _g(padre, "ls-tree", "HEAD", "vendor/visionsuite").split()[2]
|
||||||
|
sub = padre / "vendor" / "visionsuite"
|
||||||
|
(sub / "nuovo.py").write_text("y = 2\n")
|
||||||
|
_g(sub, "add", ".")
|
||||||
|
_g(sub, "commit", "-q", "-m", "avanti non registrato")
|
||||||
|
assert _g(sub, "rev-parse", "HEAD") != registrato
|
||||||
|
out = tmp_path / "tmf.zip"
|
||||||
|
assert archivio.crea_archivio(padre, out) == registrato
|
||||||
|
with zipfile.ZipFile(out) as z:
|
||||||
|
assert z.read("vision-engine-version.txt").decode() == registrato
|
||||||
|
assert "vendor/visionsuite/nuovo.py" not in z.namelist() # contenuto del commit registrato
|
||||||
|
|
||||||
|
|
||||||
|
def test_submodule_non_inizializzato_errore_e_nessuno_zip(padre, tmp_path):
|
||||||
|
clone = tmp_path / "clone"
|
||||||
|
_g(tmp_path, "clone", "-q", str(padre), str(clone)) # senza --recurse-submodules
|
||||||
|
out = tmp_path / "tmf.zip"
|
||||||
|
with pytest.raises(archivio.ErroreArchivio, match="inizializzato"):
|
||||||
|
archivio.crea_archivio(clone, out)
|
||||||
|
assert not out.exists() and not list(tmp_path.glob("*.tmp")) # nessuno zip ne' temporaneo
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_esce_1_con_messaggio(padre, tmp_path, capsys, monkeypatch):
|
||||||
|
clone = tmp_path / "clone"
|
||||||
|
_g(tmp_path, "clone", "-q", str(padre), str(clone))
|
||||||
|
originale = archivio.crea_archivio
|
||||||
|
# la CLI lavora sul repo che contiene lo script: la si dirotta sul clone senza submodule
|
||||||
|
monkeypatch.setattr(archivio, "crea_archivio", lambda r, o, ref="HEAD": originale(clone, o, ref))
|
||||||
|
assert archivio.main(["--out", str(tmp_path / "x.zip")]) == 1
|
||||||
|
assert "ERRORE" in capsys.readouterr().err
|
||||||
@@ -80,6 +80,9 @@ def _graph(dxf_sha: str, cal_sha: str, band_mm: float, min_points_pct: float, po
|
|||||||
"band_plus": float(band_mm),
|
"band_plus": float(band_mm),
|
||||||
"band_minus": float(band_mm),
|
"band_minus": float(band_mm),
|
||||||
"nominal_unit": "mm",
|
"nominal_unit": "mm",
|
||||||
|
# #127 (Adriano, 06/11/2026): LocateBlob normalizza l'angolo in (-90, 90], il pezzo a 180 o +-120 gradi
|
||||||
|
# verrebbe capovolto; "quarter" prova le pose ruotate (Core#343). Lo specchiato resta fuori.
|
||||||
|
"pose_search": "quarter",
|
||||||
# tolleranza di points_in_pct: nominale = limite, plus = 100 - limite
|
# tolleranza di points_in_pct: nominale = limite, plus = 100 - limite
|
||||||
"points_in_pct_nominal": float(min_points_pct),
|
"points_in_pct_nominal": float(min_points_pct),
|
||||||
"points_in_pct_plus": float(100.0 - min_points_pct),
|
"points_in_pct_plus": float(100.0 - min_points_pct),
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ from src.vision.costruttori import (
|
|||||||
DXF_FILE,
|
DXF_FILE,
|
||||||
CostruttoreError,
|
CostruttoreError,
|
||||||
_candidate_anchors,
|
_candidate_anchors,
|
||||||
|
_graph,
|
||||||
_maschera_pezzo,
|
_maschera_pezzo,
|
||||||
build_dxf_compare,
|
build_dxf_compare,
|
||||||
)
|
)
|
||||||
@@ -94,6 +95,74 @@ def test_ricetta_dxf_con_riferimento_gia_ruotato(tmp_path):
|
|||||||
assert risultato["outputs"]["dxf.points_in_pct"] > 95.0
|
assert risultato["outputs"]["dxf.points_in_pct"] > 95.0
|
||||||
|
|
||||||
|
|
||||||
|
# #127: pezzo a L (asimmetrico a 180 gradi: sample.dxf e' simmetrico e non distingue il
|
||||||
|
# capovolgimento). Un rettangolo con foro fuori centro NON basta: l'ICP di Core non lo risolve.
|
||||||
|
L_PROFILE_MM = [(-100, -50), (100, -50), (100, 50), (-20, 50), (-20, 10), (-100, 10)]
|
||||||
|
|
||||||
|
|
||||||
|
def _l_dxf(path: Path) -> bytes:
|
||||||
|
import ezdxf
|
||||||
|
|
||||||
|
doc = ezdxf.new()
|
||||||
|
doc.modelspace().add_lwpolyline(L_PROFILE_MM, close=True)
|
||||||
|
doc.saveas(str(path))
|
||||||
|
return path.read_bytes()
|
||||||
|
|
||||||
|
|
||||||
|
def _l_scene(angle_deg: float = 0.0) -> np.ndarray:
|
||||||
|
"""Il pezzo a L (scuro su bianco) a 4 px/mm; l'asse y del DXF e' verso l'alto."""
|
||||||
|
w, h = int(320 * PX_PER_MM), int(220 * PX_PER_MM)
|
||||||
|
cx, cy = w // 2, h // 2
|
||||||
|
img = np.full((h, w), 255, np.uint8)
|
||||||
|
pts = np.array([(cx + x * PX_PER_MM, cy - y * PX_PER_MM) for x, y in L_PROFILE_MM], np.int32)
|
||||||
|
cv2.fillPoly(img, [pts], 0)
|
||||||
|
if angle_deg:
|
||||||
|
m = cv2.getRotationMatrix2D((cx, cy), angle_deg, 1.0)
|
||||||
|
img = cv2.warpAffine(img, m, (w, h), flags=cv2.INTER_LINEAR, borderValue=255)
|
||||||
|
return img
|
||||||
|
|
||||||
|
|
||||||
|
def _pct_a_180(tmp_path, togli_quarter: bool) -> tuple[float, float]:
|
||||||
|
"""points_in_pct della ricetta costruita a 0 gradi: (sulla scena dritta, a 180 gradi)."""
|
||||||
|
risultato = build_dxf_compare(_l_scene(0.0), _l_dxf(tmp_path / "l.dxf"), PX_PER_MM, band_mm=2.0)
|
||||||
|
graph = risultato["graph"]
|
||||||
|
if togli_quarter: # misura di controllo: senza pose_search il pezzo a 180 va capovolto
|
||||||
|
for tool in graph["tools"]:
|
||||||
|
tool["values"].pop("pose_search", None)
|
||||||
|
for nome, b64 in risultato["files"].items():
|
||||||
|
(tmp_path / nome).write_bytes(base64.b64decode(b64))
|
||||||
|
|
||||||
|
def pct(img):
|
||||||
|
esito = run_graph(img, graph, calibration={"px_per_mm": PX_PER_MM}, recipe_dir=tmp_path)
|
||||||
|
assert not esito.failures, esito.failures
|
||||||
|
return esito.outputs["dxf.points_in_pct"]
|
||||||
|
return pct(_l_scene(0.0)), pct(_l_scene(180.0))
|
||||||
|
|
||||||
|
|
||||||
|
def test_ricetta_dxf_su_pezzo_capovolto_di_180(tmp_path):
|
||||||
|
# #127: con pose_search=quarter il pezzo a 180 gradi vale quanto quello dritto
|
||||||
|
pct_0, pct_180 = _pct_a_180(tmp_path, togli_quarter=False)
|
||||||
|
assert pct_0 >= 95.0
|
||||||
|
assert pct_180 >= 95.0 and abs(pct_180 - pct_0) <= 2.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_ricetta_dxf_senza_quarter_il_pezzo_a_180_e_capovolto(tmp_path):
|
||||||
|
# #127: controllo del test sopra - senza pose_search il disegno finisce capovolto
|
||||||
|
_, pct_180 = _pct_a_180(tmp_path, togli_quarter=True)
|
||||||
|
assert pct_180 < 20.0 # misurato ~2,8% (Core#343 riporta 3,3% su PR165)
|
||||||
|
|
||||||
|
|
||||||
|
def test_graph_scrive_pose_search_quarter_nel_compare_dxf():
|
||||||
|
# #127: il CompareDxf porta pose_search=quarter solo se c'e' l'ancora
|
||||||
|
con = _graph("a" * 64, "b" * 64, 1.0, 95.0, "dark",
|
||||||
|
{"row": 0.0, "col": 0.0, "angle_deg": 0.0}, (1.0, 2.0, 3.0))
|
||||||
|
dxf_tool = next(t for t in con["tools"] if t["kind"] == "CompareDxf")
|
||||||
|
assert dxf_tool["values"]["pose_search"] == "quarter"
|
||||||
|
senza = _graph("a" * 64, "b" * 64, 1.0, 95.0, "dark",
|
||||||
|
{"row": 0.0, "col": 0.0, "angle_deg": 0.0}, None)
|
||||||
|
assert [t["kind"] for t in senza["tools"]] == ["LocateBlob"]
|
||||||
|
|
||||||
|
|
||||||
def test_immagine_senza_pezzo_solleva():
|
def test_immagine_senza_pezzo_solleva():
|
||||||
bianca = np.full((600, 800), 255, np.uint8)
|
bianca = np.full((600, 800), 255, np.uint8)
|
||||||
with pytest.raises(CostruttoreError, match="pezzo"):
|
with pytest.raises(CostruttoreError, match="pezzo"):
|
||||||
|
|||||||
@@ -2464,6 +2464,7 @@ dependencies = [
|
|||||||
{ name = "visionsuite" },
|
{ name = "visionsuite" },
|
||||||
{ name = "vs-measure" },
|
{ name = "vs-measure" },
|
||||||
{ name = "vs-task" },
|
{ name = "vs-task" },
|
||||||
|
{ name = "zxing-cpp" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.optional-dependencies]
|
[package.optional-dependencies]
|
||||||
@@ -2485,6 +2486,7 @@ requires-dist = [
|
|||||||
{ name = "vs-pm2d", marker = "extra == 'locate'", editable = "vendor/visionsuite/packages/vs-pm2d" },
|
{ name = "vs-pm2d", marker = "extra == 'locate'", editable = "vendor/visionsuite/packages/vs-pm2d" },
|
||||||
{ name = "vs-task", editable = "vendor/visionsuite/packages/vs-task" },
|
{ name = "vs-task", editable = "vendor/visionsuite/packages/vs-task" },
|
||||||
{ name = "vs-tools", extras = ["locate", "dxf"], marker = "extra == 'dev'", editable = "vendor/visionsuite/packages/vs-tools" },
|
{ name = "vs-tools", extras = ["locate", "dxf"], marker = "extra == 'dev'", editable = "vendor/visionsuite/packages/vs-tools" },
|
||||||
|
{ name = "zxing-cpp", specifier = ">=3.1,<4" },
|
||||||
]
|
]
|
||||||
provides-extras = ["locate", "dxf", "dev"]
|
provides-extras = ["locate", "dxf", "dev"]
|
||||||
|
|
||||||
@@ -2787,3 +2789,35 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/3a/43/241f9615bcf8ba8903b3f0432da069e857fc4fd1783bd26183db53c4804b/zstandard-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2", size = 516118, upload-time = "2025-09-14T22:18:17.849Z" },
|
{ url = "https://files.pythonhosted.org/packages/3a/43/241f9615bcf8ba8903b3f0432da069e857fc4fd1783bd26183db53c4804b/zstandard-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2", size = 516118, upload-time = "2025-09-14T22:18:17.849Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f0/ef/da163ce2450ed4febf6467d77ccb4cd52c4c30ab45624bad26ca0a27260c/zstandard-0.25.0-cp314-cp314-win_arm64.whl", hash = "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d", size = 476940, upload-time = "2025-09-14T22:18:19.088Z" },
|
{ url = "https://files.pythonhosted.org/packages/f0/ef/da163ce2450ed4febf6467d77ccb4cd52c4c30ab45624bad26ca0a27260c/zstandard-0.25.0-cp314-cp314-win_arm64.whl", hash = "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d", size = 476940, upload-time = "2025-09-14T22:18:19.088Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zxing-cpp"
|
||||||
|
version = "3.1.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b9/30/ad0e0352c593712ebb47143571ff11b130812e2852d7540e7c80cdf23340/zxing_cpp-3.1.1.tar.gz", hash = "sha256:1051a521b21a9fe206702ad4186aeb195154e3e1badcd99576d030723f36382b", size = 1437030, upload-time = "2026-07-29T08:50:59.019Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/c4/d64c1b751561eee75706def600041e4c72642403864ac6c52588fdb54bb3/zxing_cpp-3.1.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:9e558cf4d6d0dd0ae1199541bc8fd01e8fb67e18673faa7ca96e50440fdd6f93", size = 912350, upload-time = "2026-07-29T08:50:23.952Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/01/1b/94067d5a5d324a30cd9862296171ec50cda58c9e31317eca53286aeab832/zxing_cpp-3.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ec41a833dc1697e5360b5d9e2620fab1f3e92892b890c31fe85b50a10ca05217", size = 865032, upload-time = "2026-07-29T08:50:25.304Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/ee/4ab8cf9594959e1dc8f3c0e234d225fd1080cecc349c99cac4850005055a/zxing_cpp-3.1.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:07ac611267b7220b769c182ae33473ee95aca1cc6c57e597755288b557848935", size = 1028402, upload-time = "2026-07-29T08:50:26.935Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/83/5af471c7ad3fbb11d3efba64b41aba9f209d5dcc2945ca6b0afb29a9fed0/zxing_cpp-3.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a5b32d719a5448f1b2f474e04d2db6ce41cc6973fb5c705d47dbe899361e5f9", size = 1102966, upload-time = "2026-07-29T08:50:28.428Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dd/f4/8b75505b3b2110146769006a0087e1517675af057bb1eaa7709ef8dd507a/zxing_cpp-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:0343458a0fdf3f99c9dcff74dd09be6bae5d0d87a2f99de7876014317b939996", size = 1005571, upload-time = "2026-07-29T08:50:29.806Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/e8/05b134e41abda4bb3aca00ea2bc16898c9c3641afce5ad4637fd4b1166f0/zxing_cpp-3.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:73a26e6e7c5fa411bfd690c374e3d4a7fdcb41ca57a047839365b0c985e325ad", size = 933099, upload-time = "2026-07-29T08:50:31.309Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/56/57/ac717270db6888973eba83e9832fe800808b555df0ebe34e37b6a6e07545/zxing_cpp-3.1.1-cp312-abi3-macosx_10_13_x86_64.whl", hash = "sha256:09dea611a7c9dc7c713a82303b15b733dc71abb1a77454b26b779e33671cef05", size = 911430, upload-time = "2026-07-29T08:50:32.625Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/70/f14831dd92d5c844a39c03ebe9ba185e073d4467d50b48dcf2a816cae0c5/zxing_cpp-3.1.1-cp312-abi3-macosx_11_0_arm64.whl", hash = "sha256:037cbcaeb0cb12497fc15ced23f6b778fce8a6a1d1bbffddbffd004c6225744d", size = 863740, upload-time = "2026-07-29T08:50:34.23Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0d/f3/3fb2c6c48e6f58382fbbd31965c7caafd81f75b7e6707b011bdb940adb5f/zxing_cpp-3.1.1-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f4dae01111f323f46736fc21f05c14dcaaac06cea5fdc8fd994ba19f6f918c6e", size = 1024253, upload-time = "2026-07-29T08:50:35.599Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/30/79683cf7139ee5325fbc68169eb8dc1cb2033ec43339b5f39de990f909a7/zxing_cpp-3.1.1-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9cf67341949946307d086b302cefd453fb47bc6d6ddc7d088839e9481982757b", size = 1096795, upload-time = "2026-07-29T08:50:36.896Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7d/14/055c5a68a50bdde8378ced94e63f9ce340311e51c87b947f9b95fe69f51a/zxing_cpp-3.1.1-cp312-abi3-win_amd64.whl", hash = "sha256:29f98a91148171460b47a942d137ecc90c4b8097636f23cca65263a56bb025d3", size = 1003677, upload-time = "2026-07-29T08:50:38.328Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5d/32/a827a99fa5e0aee382b5d464cbd2075e1911a69500116705f6695a6accd8/zxing_cpp-3.1.1-cp312-abi3-win_arm64.whl", hash = "sha256:04a8f8b78779ab9b637853a0329770791cfc3095d232c768dc4824b63901ebd0", size = 931324, upload-time = "2026-07-29T08:50:39.632Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b0/30/e98ce9c56bd1f1fe0a1fd0e5c39202da49baa3620031cb80ac7a04759ffb/zxing_cpp-3.1.1-cp313-cp313t-macosx_10_15_x86_64.whl", hash = "sha256:9d291fd958c26066aca97c4a416a9f15475a99c97b253cd4d2c6754a485b01e6", size = 915582, upload-time = "2026-07-29T08:50:41.286Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3d/d8/ab1db4571348e8756c2019425c72b3cb936f72c4a7c2af35687396381c36/zxing_cpp-3.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:670e2946232128b1ebba5b1f623e016ac8f8ad743ae3a0fb2e50b33180f216a2", size = 867699, upload-time = "2026-07-29T08:50:42.815Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/09/78a038367fd3d4fc00fa1f696672bfff002b4771814c3b20b1c392872043/zxing_cpp-3.1.1-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9efc7ed301846a8c060720f09bed8a29fefccef54b5106c291e4136ffe87d089", size = 1030204, upload-time = "2026-07-29T08:50:44.356Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/90/7b/0fc91d2d0463164268d06dd3e9b97520f9fe5c79dc6a954c92cd9ac92fbf/zxing_cpp-3.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f37e714ad4fd0ae4dd759b19fef25bd524a2865bc3ca8730b4e318c0cc7800e", size = 1104920, upload-time = "2026-07-29T08:50:45.639Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3b/9d/2adb3c88894b1e018739aae9bd2725733b55c14e3df090a0e01b2bffff14/zxing_cpp-3.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:93918148c1ed7ec60ff172b183ddc9dddfcb59e40867b0e98d79cc2d62a2b41d", size = 1009395, upload-time = "2026-07-29T08:50:47.118Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f8/f1/c7c93c2123701c12cda01ef02662ff010a79d31e86f67e9080d10d19013b/zxing_cpp-3.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:68b8cbd6797228eb983ab616b876cc744db319c64a9491a4806324afd04a8c48", size = 934267, upload-time = "2026-07-29T08:50:48.463Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d2/a8/8c005a5251734f57a30f1e85fa2a8965d53cd0df99d1abf642956153410e/zxing_cpp-3.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b4bd34f71868af0e34b000da4fc885c85a7f0ef37eecc0ec433ff27b263a5b7", size = 915637, upload-time = "2026-07-29T08:50:50.129Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5d/31/a2e693c9771b88e45dd7e52b56c85c169649123cf0eebfb32151efdfb356/zxing_cpp-3.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:94e342d390933b9678f71bf6005cf2125cdb27c2355c21fa194e3a672502aac6", size = 867750, upload-time = "2026-07-29T08:50:51.788Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f0/30/d2f7e626b4216bbb47783d7431cd27b151cfe5abeb22aa06f0b130095841/zxing_cpp-3.1.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71df8523deb2fb40b834238e6fa739e210e3a6e27c5b94a99b4106c08e339b9b", size = 1030274, upload-time = "2026-07-29T08:50:53.535Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4e/b9/c4b6db45a3a9f7e34a3faadcce78c2084f0bc2ce0ee8344d61f1149d2318/zxing_cpp-3.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:388626ac8df24f63c2bb17dcd42fd21daeeea6fd6759bd9b1c064b71142da07e", size = 1104873, upload-time = "2026-07-29T08:50:54.941Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c8/8e/8dbf8fcf4d466c7d9b5023ae4cf17da22f328efabd4d7107109ac7737155/zxing_cpp-3.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:fe8172f3c9b17f8fd40fba2ae0ba9728228caeff3587eabf5d99888891d02e62", size = 1032030, upload-time = "2026-07-29T08:50:56.169Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/47/38/e547ea4f9a7c8c24a1d3a59869540029e4bad467f9544084c3cee94eb6e0/zxing_cpp-3.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:1992231c161c3eaf5857f7bc35193b8ca621eba0debc51e752403657b88d542a", size = 956213, upload-time = "2026-07-29T08:50:57.524Z" },
|
||||||
|
]
|
||||||
|
|||||||
Vendored
+1
-1
Submodule vendor/visionsuite updated: 06b549a25d...9eb6f4cbce
Reference in New Issue
Block a user