Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f82187c67 | ||
|
|
6f5b224b2f |
@@ -50,7 +50,8 @@ verify: all
|
||||
tests/verify_binary.sh $(DIST)/$(NAME)
|
||||
|
||||
test: asan
|
||||
tests/parity_test.sh
|
||||
tests/parity_test.sh # parita' col client Python (30 casi + artefatti)
|
||||
tests/progress_test.sh # barra di avanzamento (pipe/PTY/plain/off)
|
||||
|
||||
clean:
|
||||
rm -rf $(DIST) build tests/tmp
|
||||
|
||||
@@ -17,6 +17,20 @@ Autore: Matteo Benedetto — progetto derivato da `enne2/cellar` (commit `f5216b
|
||||
| Build | `x86_64` static, `i686` static, `aarch64` static (cross) |
|
||||
| Installazione locale | `~/.local/bin/cellar-cli` → symlink a `dist/cellar-cli` |
|
||||
|
||||
## Download dei binari pronti
|
||||
|
||||
Nella release **v1.0.0** sono allegati tre binari statici (nessuna dipendenza a runtime):
|
||||
|
||||
| file | architettura | ABI minima |
|
||||
|---|---|---|
|
||||
| `cellar-cli-1.0.0-linux-x86_64-static` | x86-64 (amd64) | Linux, ABI 3.2.0 |
|
||||
| `cellar-cli-1.0.0-linux-i686-static` | i686 (x86 32 bit) | Linux, ABI 3.2.0 |
|
||||
| `cellar-cli-1.0.0-linux-aarch64-static` | aarch64 (arm64) | Linux, ABI 3.7.0 |
|
||||
|
||||
Insieme a `SHA256SUMS.txt` per la verifica: `sha256sum -c SHA256SUMS.txt`.
|
||||
|
||||
Pubblicato su Gitea: <https://git.enne2.net/enne2/cellar-cli-c/releases/tag/v1.0.0>
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
@@ -67,6 +81,37 @@ cellar-cli --server http://10.8.0.3:8080 list # override del server
|
||||
|
||||
Exit code: `0` ok, `1` errore HTTP/rete/file, `2` argomenti non validi (come argparse).
|
||||
|
||||
## Barra di avanzamento
|
||||
|
||||
Durante `install`, `wizard-install` e `download` viene mostrata una barra su una riga,
|
||||
con due fasi (`Scaricamento` ed `Estrazione`):
|
||||
|
||||
```
|
||||
Scaricamento [████████████░░░░░░░░░░░░] 58% 6.7 MB/11.5 MB 3.0 MB/s ETA 00:01
|
||||
Estrazione [████████████████████████] 100% 11.5 MB/11.5 MB 178 MB/s membri: 20
|
||||
```
|
||||
|
||||
- **attiva solo se stdout è un terminale**: con output rediretto, in pipeline, in cron o
|
||||
nei log **non stampa nulla** — per questo l'output resta identico al client Python;
|
||||
- controllo con la variabile d'ambiente **`CELLAR_PROGRESS`**:
|
||||
- `auto` (default) → barra su terminale, silenzio altrove;
|
||||
- `bar` → barra anche con output rediretto;
|
||||
- `plain` → una riga ogni 10% (per log e cron, senza disegno);
|
||||
- `off` → disattivata;
|
||||
- **nessun codice ANSI**: solo `\r` e riempimento con spazi, quindi funziona anche su
|
||||
console vecchie e seriali; glifi ASCII (`#`/`-`) se la locale non è UTF-8, blocchi
|
||||
Unicode (`█`/`░`) se lo è;
|
||||
- larghezza adattata al terminale (`TIOCGWINSZ`, fallback 80 colonne, max 200) e misurata
|
||||
in **colonne**, non in byte (i glifi UTF-8 sono multi-byte);
|
||||
- ridisegno limitato a ~8 volte al secondo, velocità con media mobile, ETA mostrata solo
|
||||
quando la stima è affidabile;
|
||||
- `CELLAR_PROGRESS_DEBUG=1` stampa su stderr modalità/larghezza calcolate (diagnostica).
|
||||
|
||||
```bash
|
||||
CELLAR_PROGRESS=plain cellar-cli install 'Gioco' # output adatto a un log
|
||||
CELLAR_PROGRESS=off cellar-cli install 'Gioco' # nessuna barra
|
||||
```
|
||||
|
||||
## Retrocompatibilità (le scelte che contano)
|
||||
|
||||
- **Binario statico** (`-static`): nessuna dipendenza da glibc a runtime, quindi gira su
|
||||
@@ -114,6 +159,7 @@ sei casi di errore di argomenti, più verifiche sugli artefatti:
|
||||
| Confronto nomi case-insensitive | ASCII (`tolower`) invece di `str.lower()` Unicode |
|
||||
| EOF su una richiesta interattiva | Python solleva `EOFError` (exit 1); qui si stampa `EOFError: EOF when reading a line` e si esce **1**. Nei prompt con default (nome archivio, descrizione…) su EOF si applica il default, così i wizard restano usabili da script |
|
||||
| Upload multipart | In **streaming** (il client Python legge l'intero archivio in RAM) |
|
||||
| Barra di avanzamento | Aggiunta (il CLI Python non stampa nulla durante download/estrazione). Attiva solo su terminale, quindi l'output rediretto resta identico all'originale |
|
||||
| TLS | Non supportato (niente OpenSSL): serve `http://`, oppure un reverse proxy TLS. Il server Cellar è HTTP |
|
||||
| Timeout di rete | Connect 15 s, I/O 300 s (urllib non ha timeout) |
|
||||
| Redirect | Seguiti come `urllib.request`: GET/HEAD su 301/302/303/307/308, POST→GET su 301/302/303 |
|
||||
@@ -121,11 +167,17 @@ sei casi di errore di argomenti, più verifiche sugli artefatti:
|
||||
## Test e verifica
|
||||
|
||||
```bash
|
||||
make test # parità (usa la build ASan)
|
||||
make test # parità + barra (build ASan)
|
||||
C_BIN=dist/cellar-cli tests/parity_test.sh # parità con il binario statico
|
||||
C_BIN=dist/cellar-cli tests/progress_test.sh # barra: pipe silenziosa, PTY, plain, off
|
||||
make verify # file/ldd/ABI/simboli GLIBC/syscall/smoke test
|
||||
```
|
||||
|
||||
`tests/progress_test.sh` usa un server mock con throttling (`--throttle` byte/s) e
|
||||
`script -qec` per allocare un PTY: verifica che su pipe non ci sia alcun output di
|
||||
progresso, che su terminale compaiano le due fasi con ETA e riepilogo, che nessuna riga
|
||||
superi la larghezza del terminale e che non ci siano codici ANSI.
|
||||
|
||||
## Struttura
|
||||
|
||||
```
|
||||
@@ -140,6 +192,7 @@ src/config.{c,h} ~/.cellar.conf (INI in stile configparser)
|
||||
src/bottle.{c,h} record archivi, scansione bottiglie, bottle.yml, backup
|
||||
src/ui.{c,h} tabelle e JSON con larghezze in code point (come le f-string)
|
||||
src/ops.{c,h} list/upload/download/install
|
||||
src/progress.{c,h} barra di avanzamento (download + estrazione, env CELLAR_PROGRESS)
|
||||
src/wizard.{c,h} flussi interattivi
|
||||
src/main.c CLI e messaggi di errore in stile argparse
|
||||
third_party/ miniz (unlicense/MIT) + licenza
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
0c454b2c65ac872efcd19888f996f3de17e6d69541cfc8b2657c0a0bfaf9b23c cellar-cli-1.0.0-linux-x86_64-static
|
||||
16c7db8c6667ff46c4a276bf7985b2ebc1cb5078f079651414472d47ef5f9ad7 cellar-cli-1.0.0-linux-i686-static
|
||||
11ad5ae2b5916c21d0ed2f5fa7a640e787596da2df4f5d3685b1bb48505d8f7b cellar-cli-1.0.0-linux-aarch64-static
|
||||
+17
@@ -30,6 +30,7 @@ struct gz_reader {
|
||||
mz_stream zs;
|
||||
FILE *fp;
|
||||
char in[GZ_BUFSZ];
|
||||
unsigned long long total_in; /* byte compressi consumati */
|
||||
int stream_end;
|
||||
unsigned long crc;
|
||||
unsigned long long total_out;
|
||||
@@ -247,6 +248,11 @@ gz_reader *gzr_open(const char *path, cerror *e)
|
||||
free(r);
|
||||
return NULL;
|
||||
}
|
||||
/* i byte dell'header fanno parte dell'avanzamento */
|
||||
{
|
||||
long pos = ftell(r->fp);
|
||||
r->total_in = (pos > 0) ? (unsigned long long)pos : 0;
|
||||
}
|
||||
int rc = mz_inflateInit2(&r->zs, -15); /* deflate raw */
|
||||
if (rc != MZ_OK) {
|
||||
err_set(e, ERR_MISC, 0, "inflate init failed (%d)", rc);
|
||||
@@ -286,6 +292,7 @@ long gzr_read(gz_reader *r, void *buf, size_t cap, cerror *e)
|
||||
}
|
||||
break; /* EOF inatteso: lo segnala il chiamante */
|
||||
}
|
||||
r->total_in += (unsigned long long)n;
|
||||
r->zs.next_in = (unsigned char *)r->in;
|
||||
r->zs.avail_in = (unsigned int)n;
|
||||
}
|
||||
@@ -310,6 +317,16 @@ long gzr_read(gz_reader *r, void *buf, size_t cap, cerror *e)
|
||||
return (long)produced;
|
||||
}
|
||||
|
||||
void gzr_progress(gz_reader *r, long long *in_bytes, long long *out_bytes)
|
||||
{
|
||||
if (!r)
|
||||
return;
|
||||
if (in_bytes)
|
||||
*in_bytes = (long long)r->total_in;
|
||||
if (out_bytes)
|
||||
*out_bytes = (long long)r->total_out;
|
||||
}
|
||||
|
||||
void gzr_close(gz_reader *r)
|
||||
{
|
||||
if (!r)
|
||||
|
||||
@@ -19,6 +19,8 @@ int gzw_close(gz_writer *w, cerror *e);
|
||||
gz_reader *gzr_open(const char *path, cerror *e);
|
||||
/* Ritorna il numero di byte letti, 0 a EOF, -1 su errore. */
|
||||
long gzr_read(gz_reader *r, void *buf, size_t cap, cerror *e);
|
||||
/* Avanzamento: byte compressi consumati e byte decompressi prodotti. */
|
||||
void gzr_progress(gz_reader *r, long long *in_bytes, long long *out_bytes);
|
||||
void gzr_close(gz_reader *r);
|
||||
|
||||
#endif /* CELLAR_GZIP_H */
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "fsutil.h"
|
||||
#include "http.h"
|
||||
#include "json.h"
|
||||
#include "progress.h"
|
||||
#include "tar.h"
|
||||
#include "ui.h"
|
||||
|
||||
@@ -141,6 +142,7 @@ int ops_upload(const char *server, const char *filepath, const mp_field *fields,
|
||||
typedef struct {
|
||||
FILE *fp;
|
||||
long long written;
|
||||
progress *pr; /* opzionale: barra di avanzamento */
|
||||
} file_sink;
|
||||
|
||||
static int sink_write(void *ctx, const char *buf, size_t len, cerror *e)
|
||||
@@ -151,6 +153,8 @@ static int sink_write(void *ctx, const char *buf, size_t len, cerror *e)
|
||||
return -1;
|
||||
}
|
||||
s->written += (long long)len;
|
||||
if (s->pr)
|
||||
progress_update(s->pr, s->written);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -187,9 +191,12 @@ static int download_to(const char *server, long long archive_id, const char *des
|
||||
csresponse_free(&meta);
|
||||
return -1;
|
||||
}
|
||||
progress pr;
|
||||
progress_init(&pr, "Scaricamento", meta.content_length);
|
||||
file_sink sink;
|
||||
sink.fp = fp;
|
||||
sink.written = 0;
|
||||
sink.pr = ≺
|
||||
int rc = http_read_body(c, sink_write, &sink, NULL, e);
|
||||
if (fclose(fp) != 0 && rc == 0) {
|
||||
err_set_errno(e, ERR_FILE, "cannot write %s", dest);
|
||||
@@ -197,8 +204,11 @@ static int download_to(const char *server, long long archive_id, const char *des
|
||||
}
|
||||
http_close(c);
|
||||
csresponse_free(&meta);
|
||||
if (rc != 0)
|
||||
return -1;
|
||||
if (rc != 0) {
|
||||
progress_abort(&pr);
|
||||
return -1; /* come il client Python: il file parziale resta */
|
||||
}
|
||||
progress_finish(&pr);
|
||||
if (announce)
|
||||
printf("Downloaded to %s\n", dest);
|
||||
return 0;
|
||||
@@ -252,9 +262,12 @@ int ops_download(const char *server, long long archive_id, const char *output, c
|
||||
csresponse_free(&meta);
|
||||
return -1;
|
||||
}
|
||||
progress pr;
|
||||
progress_init(&pr, "Scaricamento", meta.content_length);
|
||||
file_sink sink;
|
||||
sink.fp = fp;
|
||||
sink.written = 0;
|
||||
sink.pr = ≺
|
||||
int rc = http_read_body(c, sink_write, &sink, NULL, e);
|
||||
if (fclose(fp) != 0 && rc == 0) {
|
||||
err_set_errno(e, ERR_FILE, "cannot write %s", dest);
|
||||
@@ -262,14 +275,25 @@ int ops_download(const char *server, long long archive_id, const char *output, c
|
||||
}
|
||||
http_close(c);
|
||||
csresponse_free(&meta);
|
||||
if (rc != 0)
|
||||
if (rc != 0) {
|
||||
progress_abort(&pr);
|
||||
return -1;
|
||||
}
|
||||
progress_finish(&pr);
|
||||
printf("Downloaded to %s\n", dest);
|
||||
return 0;
|
||||
}
|
||||
return download_to(server, archive_id, output, 1, e);
|
||||
}
|
||||
|
||||
static int extract_progress_cb(void *ctx, long long consumed, long long members)
|
||||
{
|
||||
progress *p = ctx;
|
||||
progress_set_members(p, members);
|
||||
progress_update(p, consumed);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ops_install_record(const char *server, const archive_rec *rec, const char *bottles_dir, int replace,
|
||||
const char *ref, cerror *e)
|
||||
{
|
||||
@@ -319,8 +343,18 @@ int ops_install_record(const char *server, const archive_rec *rec, const char *b
|
||||
|
||||
if (mkdir_p(extract_dir, e) != 0)
|
||||
goto cleanup;
|
||||
if (tar_extract(download_path, extract_dir, e) != 0)
|
||||
{
|
||||
struct stat arc_st;
|
||||
long long arc_size = (stat(download_path, &arc_st) == 0) ? (long long)arc_st.st_size : -1;
|
||||
progress px;
|
||||
progress_init(&px, "Estrazione", arc_size);
|
||||
int xrc = tar_extract_cb(download_path, extract_dir, extract_progress_cb, &px, e);
|
||||
if (xrc != 0) {
|
||||
progress_abort(&px);
|
||||
goto cleanup;
|
||||
}
|
||||
progress_finish(&px);
|
||||
}
|
||||
|
||||
strlist subdirs;
|
||||
strlist_init(&subdirs);
|
||||
|
||||
+336
@@ -0,0 +1,336 @@
|
||||
/* progress.c — implementazione della barra (vd. progress.h). */
|
||||
#include "progress.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define PROG_MIN_WIDTH 40
|
||||
#define PROG_MAX_WIDTH 200
|
||||
#define PROG_DRAW_INTERVAL 0.12 /* secondi tra due ridisegni */
|
||||
#define PROG_SAMPLE_INTERVAL 0.25
|
||||
#define PROG_ASCII_FULL '#'
|
||||
#define PROG_ASCII_EMPTY '-'
|
||||
#define PROG_UNI_FULL "\xe2\x96\x88" /* U+2588 */
|
||||
#define PROG_UNI_EMPTY "\xe2\x96\x91" /* U+2591 */
|
||||
|
||||
static double now_monotonic(void)
|
||||
{
|
||||
struct timespec ts;
|
||||
if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0)
|
||||
return 0.0;
|
||||
return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9;
|
||||
}
|
||||
|
||||
static int env_is_utf8(void)
|
||||
{
|
||||
const char *vars[] = {getenv("LC_ALL"), getenv("LC_CTYPE"), getenv("LANG")};
|
||||
for (size_t i = 0; i < sizeof(vars) / sizeof(vars[0]); i++) {
|
||||
const char *v = vars[i];
|
||||
if (!v || !*v)
|
||||
continue;
|
||||
char buf[64];
|
||||
size_t n = strlen(v);
|
||||
if (n >= sizeof(buf))
|
||||
n = sizeof(buf) - 1;
|
||||
for (size_t k = 0; k < n; k++)
|
||||
buf[k] = (char)tolower((unsigned char)v[k]);
|
||||
buf[n] = '\0';
|
||||
if (strstr(buf, "utf-8") || strstr(buf, "utf8"))
|
||||
return 1;
|
||||
/* la prima variabile impostata vince (come fa setlocale) */
|
||||
return 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
prog_mode progress_mode_from_env(void)
|
||||
{
|
||||
const char *v = getenv("CELLAR_PROGRESS");
|
||||
if (!v || !*v)
|
||||
return PROG_AUTO;
|
||||
if (!strcmp(v, "bar") || !strcmp(v, "1") || !strcmp(v, "on") || !strcmp(v, "yes"))
|
||||
return PROG_BAR;
|
||||
if (!strcmp(v, "plain") || !strcmp(v, "line"))
|
||||
return PROG_PLAIN;
|
||||
if (!strcmp(v, "off") || !strcmp(v, "0") || !strcmp(v, "none") || !strcmp(v, "no"))
|
||||
return PROG_OFF;
|
||||
return PROG_AUTO;
|
||||
}
|
||||
|
||||
static int terminal_width(void)
|
||||
{
|
||||
struct winsize ws;
|
||||
if (isatty(STDOUT_FILENO) && ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0) {
|
||||
int w = ws.ws_col;
|
||||
if (w < PROG_MIN_WIDTH)
|
||||
w = PROG_MIN_WIDTH;
|
||||
if (w > PROG_MAX_WIDTH)
|
||||
w = PROG_MAX_WIDTH;
|
||||
return w;
|
||||
}
|
||||
return 80;
|
||||
}
|
||||
|
||||
/* Dimensione in stile client Python: MiB etichettati MB. */
|
||||
static void fmt_size(long long bytes, char *out, size_t outsz)
|
||||
{
|
||||
double b = (double)bytes;
|
||||
if (bytes < 1024)
|
||||
snprintf(out, outsz, "%lld B", bytes);
|
||||
else if (bytes < 1024 * 1024)
|
||||
snprintf(out, outsz, "%.0f kB", b / 1024.0);
|
||||
else if (bytes < 1024LL * 1024 * 1024)
|
||||
snprintf(out, outsz, "%.1f MB", b / (1024.0 * 1024.0));
|
||||
else
|
||||
snprintf(out, outsz, "%.2f GB", b / (1024.0 * 1024.0 * 1024.0));
|
||||
}
|
||||
|
||||
static void fmt_speed(double bytes_per_sec, char *out, size_t outsz)
|
||||
{
|
||||
double b = bytes_per_sec;
|
||||
if (b < 1024)
|
||||
snprintf(out, outsz, "%.0f B/s", b);
|
||||
else if (b < 1024.0 * 1024.0)
|
||||
snprintf(out, outsz, "%.0f kB/s", b / 1024.0);
|
||||
else
|
||||
snprintf(out, outsz, "%.1f MB/s", b / (1024.0 * 1024.0));
|
||||
}
|
||||
|
||||
static void fmt_eta(double seconds, char *out, size_t outsz)
|
||||
{
|
||||
if (seconds < 0 || seconds > 99 * 3600) {
|
||||
snprintf(out, outsz, "--:--");
|
||||
return;
|
||||
}
|
||||
long s = (long)(seconds + 0.5);
|
||||
snprintf(out, outsz, "%02ld:%02ld", s / 60, s % 60);
|
||||
}
|
||||
|
||||
void progress_init(progress *p, const char *label, long long total)
|
||||
{
|
||||
memset(p, 0, sizeof(*p));
|
||||
p->mode = progress_mode_from_env();
|
||||
if (p->mode == PROG_AUTO) {
|
||||
/* auto: barra solo su terminale, silenzio se rediretto/in pipeline */
|
||||
p->mode = isatty(STDOUT_FILENO) ? PROG_BAR : PROG_OFF;
|
||||
}
|
||||
p->label = label ? label : "";
|
||||
p->total = total;
|
||||
p->last_percent = -1;
|
||||
p->plain_step = 0;
|
||||
p->width = terminal_width();
|
||||
p->unicode = env_is_utf8();
|
||||
p->t_start = now_monotonic();
|
||||
p->t_last_draw = -1.0;
|
||||
p->t_last_sample = p->t_start;
|
||||
if (getenv("CELLAR_PROGRESS_DEBUG"))
|
||||
fprintf(stderr, "[debug progress] mode=%d width=%d unicode=%d isatty=%d\n", (int)p->mode,
|
||||
p->width, p->unicode, isatty(STDOUT_FILENO));
|
||||
if (p->mode == PROG_PLAIN)
|
||||
printf("%s: avvio...\n", p->label);
|
||||
}
|
||||
|
||||
void progress_set_members(progress *p, long long members)
|
||||
{
|
||||
p->members = members;
|
||||
}
|
||||
|
||||
static void build_line(progress *p, char *line, size_t linesz, int final)
|
||||
{
|
||||
char done_s[32], total_s[32], speed_s[32], eta_s[16];
|
||||
fmt_size(p->done, done_s, sizeof(done_s));
|
||||
if (p->total > 0)
|
||||
fmt_size(p->total, total_s, sizeof(total_s));
|
||||
else
|
||||
snprintf(total_s, sizeof(total_s), "?");
|
||||
|
||||
int percent = -1;
|
||||
if (p->total > 0) {
|
||||
percent = (int)((p->done * 100) / p->total);
|
||||
if (percent > 100)
|
||||
percent = 100;
|
||||
}
|
||||
fmt_speed(p->rate, speed_s, sizeof(speed_s));
|
||||
double eta = -1.0;
|
||||
if (p->total > 0 && p->rate > 4096.0)
|
||||
eta = (double)(p->total - p->done) / p->rate;
|
||||
fmt_eta(eta, eta_s, sizeof(eta_s));
|
||||
|
||||
char members_s[48];
|
||||
members_s[0] = '\0';
|
||||
if (p->members > 0)
|
||||
snprintf(members_s, sizeof(members_s), " membri: %lld", p->members);
|
||||
|
||||
/* suffisso senza barra */
|
||||
char suffix[192];
|
||||
if (percent >= 0)
|
||||
snprintf(suffix, sizeof(suffix), "] %3d%% %s/%s %s%s", percent, done_s, total_s, speed_s,
|
||||
members_s);
|
||||
else
|
||||
snprintf(suffix, sizeof(suffix), "] %s %s%s", done_s, speed_s, members_s);
|
||||
if (!final && p->rate <= 4096.0) {
|
||||
/* senza stima affidabile non mostriamo ETA */
|
||||
} else if (eta >= 0) {
|
||||
char tmp[224];
|
||||
snprintf(tmp, sizeof(tmp), "%s ETA %s", suffix, eta_s);
|
||||
snprintf(suffix, sizeof(suffix), "%s", tmp);
|
||||
}
|
||||
|
||||
if (p->mode == PROG_PLAIN) {
|
||||
/* log-friendly: niente barra grafica, una riga per soglia */
|
||||
if (percent >= 0)
|
||||
snprintf(line, linesz, "%s: %d%% %s/%s %s%s", p->label, percent, done_s, total_s,
|
||||
speed_s, members_s);
|
||||
else
|
||||
snprintf(line, linesz, "%s: %s %s%s", p->label, done_s, speed_s, members_s);
|
||||
return;
|
||||
}
|
||||
|
||||
char prefix[96];
|
||||
snprintf(prefix, sizeof(prefix), "%s [", p->label);
|
||||
|
||||
size_t avail = 0;
|
||||
size_t plen = strlen(prefix), slen = strlen(suffix);
|
||||
if (p->width > (int)(plen + slen))
|
||||
avail = (size_t)p->width - plen - slen;
|
||||
if (avail < 8) {
|
||||
/* troppo stretta per la barra: riduce il suffisso */
|
||||
snprintf(line, linesz, "%s%s", prefix, suffix);
|
||||
return;
|
||||
}
|
||||
|
||||
size_t filled = 0;
|
||||
if (p->total > 0)
|
||||
filled = (size_t)((p->done * (long long)avail) / p->total);
|
||||
if (filled > avail)
|
||||
filled = avail;
|
||||
|
||||
size_t used = 0;
|
||||
int n = snprintf(line, linesz, "%s", prefix);
|
||||
if (n > 0)
|
||||
used = (size_t)n;
|
||||
const char *full = p->unicode ? PROG_UNI_FULL : NULL;
|
||||
const char *empty = p->unicode ? PROG_UNI_EMPTY : NULL;
|
||||
for (size_t i = 0; i < avail && used + 8 < linesz; i++) {
|
||||
if (i < filled) {
|
||||
if (full)
|
||||
used += (size_t)snprintf(line + used, linesz - used, "%s", full);
|
||||
else
|
||||
line[used++] = PROG_ASCII_FULL;
|
||||
} else {
|
||||
if (empty)
|
||||
used += (size_t)snprintf(line + used, linesz - used, "%s", empty);
|
||||
else
|
||||
line[used++] = PROG_ASCII_EMPTY;
|
||||
}
|
||||
}
|
||||
line[used] = '\0';
|
||||
snprintf(line + used, linesz - used, "%s", suffix);
|
||||
}
|
||||
|
||||
static void draw_line(progress *p, const char *line)
|
||||
{
|
||||
/* attenzione: i glifi della barra sono multi-byte in UTF-8, quindi il
|
||||
* conteggio per il ridisegno e la pulizia va fatto in COLONNE (code point),
|
||||
* non in byte, altrimenti la riga supera la larghezza del terminale. */
|
||||
size_t cols = utf8_count(line);
|
||||
if (getenv("CELLAR_PROGRESS_DEBUG"))
|
||||
fprintf(stderr, "[debug progress] line bytes=%zu cols=%zu\n", strlen(line), cols);
|
||||
fputs("\r", stdout);
|
||||
fputs(line, stdout);
|
||||
for (size_t i = cols; i < (size_t)p->last_cols; i++)
|
||||
fputc(' ', stdout);
|
||||
p->last_cols = (int)cols;
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
void progress_update(progress *p, long long done)
|
||||
{
|
||||
if (p->mode == PROG_OFF || p->finished)
|
||||
return;
|
||||
p->done = done;
|
||||
|
||||
double now = now_monotonic();
|
||||
if (now - p->t_last_sample >= PROG_SAMPLE_INTERVAL) {
|
||||
double dt = now - p->t_last_sample;
|
||||
long long delta = done - p->last_sample_done;
|
||||
if (dt > 0 && delta >= 0) {
|
||||
double inst = (double)delta / dt;
|
||||
p->rate = (p->rate > 0.0) ? (0.7 * p->rate + 0.3 * inst) : inst;
|
||||
}
|
||||
p->last_sample_done = done;
|
||||
p->t_last_sample = now;
|
||||
}
|
||||
|
||||
if (p->mode == PROG_PLAIN) {
|
||||
int percent = (p->total > 0) ? (int)((done * 100) / p->total) : 0;
|
||||
if (p->total > 0) {
|
||||
if (percent >= p->plain_step) {
|
||||
char line[512];
|
||||
build_line(p, line, sizeof(line), 0);
|
||||
printf("%s\n", line);
|
||||
p->plain_step += 10;
|
||||
}
|
||||
} else if (now - p->t_last_draw >= 3.0) {
|
||||
char line[512];
|
||||
build_line(p, line, sizeof(line), 0);
|
||||
printf("%s\n", line);
|
||||
p->t_last_draw = now;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/* PROG_BAR */
|
||||
int percent = (p->total > 0) ? (int)((done * 100) / p->total) : -1;
|
||||
if (p->t_last_draw > 0.0 && now - p->t_last_draw < PROG_DRAW_INTERVAL)
|
||||
return;
|
||||
if (percent == p->last_percent && p->total > 0 && done != p->total)
|
||||
return;
|
||||
p->last_percent = percent;
|
||||
p->t_last_draw = now;
|
||||
|
||||
char line[512];
|
||||
build_line(p, line, sizeof(line), 0);
|
||||
draw_line(p, line);
|
||||
}
|
||||
|
||||
void progress_finish(progress *p)
|
||||
{
|
||||
if (p->mode == PROG_OFF || p->finished)
|
||||
return;
|
||||
p->finished = 1;
|
||||
if (p->total > 0)
|
||||
p->done = p->total;
|
||||
|
||||
double elapsed = now_monotonic() - p->t_start;
|
||||
if (elapsed <= 0.0)
|
||||
elapsed = 0.001;
|
||||
double avg = (double)p->done / elapsed;
|
||||
|
||||
char done_s[32], speed_s[32];
|
||||
fmt_size(p->done, done_s, sizeof(done_s));
|
||||
fmt_speed(avg, speed_s, sizeof(speed_s));
|
||||
|
||||
if (p->mode == PROG_PLAIN) {
|
||||
printf("%s: completato (%s in %.1f s, %s)\n", p->label, done_s, elapsed, speed_s);
|
||||
return;
|
||||
}
|
||||
char line[512];
|
||||
build_line(p, line, sizeof(line), 1);
|
||||
draw_line(p, line);
|
||||
printf("\n%s: %s in %.1f s (%s)\n", p->label, done_s, elapsed, speed_s);
|
||||
}
|
||||
|
||||
void progress_abort(progress *p)
|
||||
{
|
||||
if (p->mode == PROG_OFF || p->finished)
|
||||
return;
|
||||
p->finished = 1;
|
||||
if (p->mode == PROG_BAR && p->last_cols > 0)
|
||||
printf("\n");
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/* progress.h — barra di avanzamento per download ed estrazione.
|
||||
*
|
||||
* Attiva solo quando ha senso: con CELLAR_PROGRESS=auto (default) disegna la
|
||||
* barra se stdout e' un terminale, e non stampa nulla se l'output e' rediretto
|
||||
* o in pipeline (cosi' l'output resta identico al client Python e i log
|
||||
* restano puliti). Modalita' forzabili: bar, plain, off.
|
||||
*
|
||||
* Nessun codice ANSI: solo '\r' e riempimento con spazi, quindi funziona anche
|
||||
* su console vecchie/seriali. Glyph ASCII di default, Unicode solo se la
|
||||
* locale e' UTF-8.
|
||||
*/
|
||||
#ifndef CELLAR_PROGRESS_H
|
||||
#define CELLAR_PROGRESS_H
|
||||
|
||||
#include "common.h"
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
typedef enum { PROG_AUTO = 0, PROG_BAR, PROG_PLAIN, PROG_OFF } prog_mode;
|
||||
|
||||
typedef struct {
|
||||
prog_mode mode;
|
||||
const char *label;
|
||||
long long total; /* -1 se sconosciuto */
|
||||
long long done;
|
||||
long long members; /* informazione opzionale (estrazione) */
|
||||
double t_start;
|
||||
double t_last_draw;
|
||||
double t_last_sample;
|
||||
double rate; /* byte/s, media mobile */
|
||||
long long last_sample_done;
|
||||
int last_percent;
|
||||
int last_cols; /* larghezza (colonne) dell'ultima riga disegnata, per ripulire */
|
||||
int plain_step; /* prossima soglia in modalita' plain */
|
||||
int width;
|
||||
int unicode;
|
||||
int finished;
|
||||
} progress;
|
||||
|
||||
/* Modalita' da CELLAR_PROGRESS (auto|bar|plain|off); default auto. */
|
||||
prog_mode progress_mode_from_env(void);
|
||||
/* Inizializza: in modalita' AUTO decide in base a isatty(stdout). */
|
||||
void progress_init(progress *p, const char *label, long long total);
|
||||
void progress_set_members(progress *p, long long members);
|
||||
/* Aggiorna con i byte completati (throttled: non ridisegna piu' di ~8 volte/s). */
|
||||
void progress_update(progress *p, long long done);
|
||||
/* Chiude la riga (newline) e stampa il riepilogo finale. */
|
||||
void progress_finish(progress *p);
|
||||
/* Percorso d'errore: chiude solo la riga, senza riepilogo. */
|
||||
void progress_abort(progress *p);
|
||||
|
||||
#endif /* CELLAR_PROGRESS_H */
|
||||
@@ -374,6 +374,9 @@ int tar_create(const char *archive_path, const char *rootpath, const char *arcna
|
||||
typedef struct {
|
||||
gz_reader *gz;
|
||||
long long remaining; /* byte non ancora letti del membro corrente */
|
||||
tar_progress_fn cb; /* opzionale: avanzamento */
|
||||
void *cb_ctx;
|
||||
long long members;
|
||||
} tr;
|
||||
|
||||
static int tr_read_exact(tr *t, void *buf, size_t len, cerror *e)
|
||||
@@ -389,6 +392,14 @@ static int tr_read_exact(tr *t, void *buf, size_t len, cerror *e)
|
||||
return -1;
|
||||
}
|
||||
off += (size_t)n;
|
||||
if (t->cb) {
|
||||
long long in_bytes = 0;
|
||||
gzr_progress(t->gz, &in_bytes, NULL);
|
||||
if (t->cb(t->cb_ctx, in_bytes, t->members) != 0) {
|
||||
err_set(e, ERR_MISC, 0, "operazione annullata");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -542,7 +553,7 @@ static int apply_times(const char *path, time_t sec, long nsec, int symlink_ok)
|
||||
return utimensat(AT_FDCWD, path, times, 0);
|
||||
}
|
||||
|
||||
int tar_extract(const char *archive_path, const char *destdir, cerror *e)
|
||||
int tar_extract_cb(const char *archive_path, const char *destdir, tar_progress_fn cb, void *ctx, cerror *e)
|
||||
{
|
||||
gz_reader *gz = gzr_open(archive_path, e);
|
||||
if (!gz)
|
||||
@@ -550,6 +561,9 @@ int tar_extract(const char *archive_path, const char *destdir, cerror *e)
|
||||
tr t;
|
||||
t.gz = gz;
|
||||
t.remaining = 0;
|
||||
t.cb = cb;
|
||||
t.cb_ctx = ctx;
|
||||
t.members = 0;
|
||||
|
||||
dirfixup *dirs = NULL;
|
||||
size_t ndirs = 0, cdirs = 0;
|
||||
@@ -854,6 +868,8 @@ int tar_extract(const char *archive_path, const char *destdir, cerror *e)
|
||||
}
|
||||
}
|
||||
|
||||
t.members++;
|
||||
|
||||
/* gli override valgono per un solo membro */
|
||||
pax_free(&next);
|
||||
free(gnu_longname);
|
||||
@@ -879,3 +895,8 @@ int tar_extract(const char *archive_path, const char *destdir, cerror *e)
|
||||
gzr_close(gz);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int tar_extract(const char *archive_path, const char *destdir, cerror *e)
|
||||
{
|
||||
return tar_extract_cb(archive_path, destdir, NULL, NULL, e);
|
||||
}
|
||||
|
||||
@@ -16,4 +16,9 @@ int tar_create(const char *archive_path, const char *rootpath, const char *arcna
|
||||
* (symlink/hardlink preservati, permessi e mtime ripristinati). */
|
||||
int tar_extract(const char *archive_path, const char *destdir, cerror *e);
|
||||
|
||||
/* Come tar_extract, ma notifica l'avanzamento: byte compressi consumati e
|
||||
* membri gia' processati. Il callback puo' ritornare -1 per interrompere. */
|
||||
typedef int (*tar_progress_fn)(void *ctx, long long consumed, long long members);
|
||||
int tar_extract_cb(const char *archive_path, const char *destdir, tar_progress_fn cb, void *ctx, cerror *e);
|
||||
|
||||
#endif /* CELLAR_TAR_H */
|
||||
|
||||
+32
-4
@@ -19,6 +19,7 @@ import json
|
||||
import os
|
||||
import re
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
@@ -75,8 +76,14 @@ def parse_multipart(body: bytes, content_type: str):
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
state: State
|
||||
throttle = 0 # byte/s, 0 = nessun limite (per testare la barra di avanzamento)
|
||||
protocol_version = "HTTP/1.1"
|
||||
|
||||
@classmethod
|
||||
def _pace(cls, nbytes: int) -> None:
|
||||
if cls.throttle > 0:
|
||||
time.sleep(nbytes / cls.throttle)
|
||||
|
||||
def log_message(self, *args): # silenzioso
|
||||
pass
|
||||
|
||||
@@ -90,7 +97,20 @@ class Handler(BaseHTTPRequestHandler):
|
||||
|
||||
def _body(self) -> bytes:
|
||||
length = int(self.headers.get("Content-Length") or 0)
|
||||
return self.rfile.read(length) if length else b""
|
||||
if not length:
|
||||
return b""
|
||||
if Handler.throttle <= 0:
|
||||
return self.rfile.read(length)
|
||||
chunks, left = [], length
|
||||
while left > 0:
|
||||
n = min(left, 65536)
|
||||
data = self.rfile.read(n)
|
||||
if not data:
|
||||
break
|
||||
chunks.append(data)
|
||||
left -= len(data)
|
||||
Handler._pace(len(data))
|
||||
return b"".join(chunks)
|
||||
|
||||
def do_GET(self):
|
||||
path = self.path.split("?")[0]
|
||||
@@ -113,13 +133,19 @@ class Handler(BaseHTTPRequestHandler):
|
||||
f = self.state.storage / r["stored_name"]
|
||||
if not f.exists():
|
||||
return self._json(404, {"detail": "Stored file not found."})
|
||||
data = f.read_bytes()
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", r.get("content_type") or "application/octet-stream")
|
||||
self.send_header("Content-Disposition", f'attachment; filename="{r["file_name"]}"')
|
||||
self.send_header("Content-Length", str(len(data)))
|
||||
self.send_header("Content-Length", str(f.stat().st_size))
|
||||
self.end_headers()
|
||||
return self.wfile.write(data)
|
||||
with f.open("rb") as fh:
|
||||
while True:
|
||||
chunk = fh.read(65536)
|
||||
if not chunk:
|
||||
break
|
||||
self.wfile.write(chunk)
|
||||
Handler._pace(len(chunk))
|
||||
return None
|
||||
return self._json(404, {"detail": "Archive not found."})
|
||||
if path == "/openapi.json":
|
||||
return self._json(200, {"info": {"title": "Bottle Archive Server", "version": "0.1.0"}})
|
||||
@@ -181,8 +207,10 @@ def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--port", type=int, default=18099)
|
||||
ap.add_argument("--state", required=True)
|
||||
ap.add_argument("--throttle", type=int, default=0, help="byte/s massimi (per testare la barra)")
|
||||
args = ap.parse_args()
|
||||
Handler.state = State(Path(args.state))
|
||||
Handler.throttle = args.throttle
|
||||
srv = ThreadingHTTPServer(("127.0.0.1", args.port), Handler)
|
||||
srv.serve_forever()
|
||||
|
||||
|
||||
Executable
+165
@@ -0,0 +1,165 @@
|
||||
#!/usr/bin/env bash
|
||||
# Test della barra di avanzamento (src/progress.c).
|
||||
#
|
||||
# Verifica:
|
||||
# 1. modalita' auto con output rediretto -> NESSUN output di progresso
|
||||
# (cosi' l'output resta identico al client Python: vedi parity_test.sh);
|
||||
# 2. modalita' auto su terminale (PTY via `script`) -> barra disegnata per il
|
||||
# download e per l'estrazione, con riga di riepilogo finale;
|
||||
# 3. CELLAR_PROGRESS=plain -> una riga per soglia del 10%, niente barra;
|
||||
# 4. CELLAR_PROGRESS=off -> nessun output di progresso anche su PTY;
|
||||
# 5. l'output finale (Downloaded to / Installed bottle) resta corretto e
|
||||
# l'archivio scaricato e' integro.
|
||||
set -uo pipefail
|
||||
|
||||
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
C_BIN="${C_BIN:-$ROOT/dist/cellar-cli}"
|
||||
PY="${PYTHON:-python3}"
|
||||
WORK="$ROOT/tests/tmp-progress"
|
||||
PORT="${PORT:-18097}"
|
||||
THROTTLE="${THROTTLE:-3000000}" # 3 MB/s: la barra disegna piu' fotogrammi
|
||||
SRV_PID=""
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
ok() { PASS=$((PASS + 1)); printf ' PASS %s\n' "$1"; }
|
||||
ko() { FAIL=$((FAIL + 1)); printf ' FAIL %s\n' "$1"; }
|
||||
|
||||
cleanup() {
|
||||
[ -n "$SRV_PID" ] && kill "$SRV_PID" 2>/dev/null
|
||||
wait 2>/dev/null
|
||||
return 0
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
fail_if_missing() {
|
||||
[ -x "$C_BIN" ] || { echo "binario non trovato: $C_BIN (esegui make all)"; exit 1; }
|
||||
command -v script >/dev/null || { echo "serve util-linux 'script' per il test su PTY"; exit 1; }
|
||||
}
|
||||
fail_if_missing
|
||||
|
||||
rm -rf "$WORK"
|
||||
mkdir -p "$WORK"/{state,inst,bottles,home}
|
||||
|
||||
# bottiglia di prova con un file da ~12 MB (comprimibile poco, cosi' il
|
||||
# download dura qualche secondo con il throttling)
|
||||
bash "$ROOT/tests/make_fake_bottle.sh" "$WORK/bottles" >/dev/null
|
||||
head -c 12000000 /dev/urandom >"$WORK/bottles/Fake Bottle/drive_c/big.bin"
|
||||
|
||||
"$PY" "$ROOT/tests/mock_server.py" --port "$PORT" --state "$WORK/state" --throttle "$THROTTLE" &
|
||||
SRV_PID=$!
|
||||
for _ in $(seq 60); do
|
||||
curl -sf "http://127.0.0.1:$PORT/health" >/dev/null 2>&1 && break
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
SRV="http://127.0.0.1:$PORT"
|
||||
export HOME="$WORK/home"
|
||||
|
||||
# archivio tar.gz valido (l'estrazione deve poter funzionare)
|
||||
"$PY" - "$WORK/bottles/Fake Bottle" "$WORK/big.tar.gz" <<'PYEOF'
|
||||
import sys, tarfile
|
||||
with tarfile.open(sys.argv[2], "w:gz") as tf:
|
||||
tf.add(sys.argv[1], arcname="Fake Bottle")
|
||||
PYEOF
|
||||
"$C_BIN" --server "$SRV" upload "$WORK/big.tar.gz" --name BigBottle \
|
||||
--bottle-name "Fake Bottle" >/dev/null 2>&1
|
||||
echo " (server mock pronto, archivio valido da $(stat -c%s "$WORK/big.tar.gz") byte caricato)"
|
||||
|
||||
# ---------------------------------------------------------------- 1. auto su pipe
|
||||
out=$(CELLAR_PROGRESS=auto "$C_BIN" --server "$SRV" download 1 "$WORK/dl-pipe.bin" 2>&1)
|
||||
if [ "$out" = "Downloaded to $WORK/dl-pipe.bin" ]; then
|
||||
ok "auto su pipe: nessun output di progresso"
|
||||
else
|
||||
ko "auto su pipe: nessun output di progresso (ottenuto: $(printf '%s' "$out" | head -2 | tr '\n' '|'))"
|
||||
fi
|
||||
|
||||
# ------------------------------------------------------------ 2. auto su PTY (barra)
|
||||
pty_out=$(script -qec "CELLAR_PROGRESS=auto '$C_BIN' --server '$SRV' install 'Fake Bottle' --bottles-dir '$WORK/inst'" /dev/null 2>&1 | tr '\r' '\n')
|
||||
if grep -q "Scaricamento \[" <<<"$pty_out"; then
|
||||
ok "PTY: barra del download disegnata"
|
||||
else
|
||||
ko "PTY: barra del download disegnata"
|
||||
fi
|
||||
if grep -q "Estrazione \[" <<<"$pty_out" || grep -q "^Estrazione:" <<<"$pty_out"; then
|
||||
ok "PTY: fase di estrazione tracciata"
|
||||
else
|
||||
ko "PTY: fase di estrazione tracciata"
|
||||
fi
|
||||
if grep -qE "^Scaricamento: [0-9.]+ (B|kB|MB|GB) in [0-9.]+ s" <<<"$pty_out"; then
|
||||
ok "PTY: riepilogo finale del download"
|
||||
else
|
||||
ko "PTY: riepilogo finale del download"
|
||||
fi
|
||||
if grep -q "^Installed bottle 'Fake Bottle' to " <<<"$pty_out"; then
|
||||
ok "PTY: output finale corretto dopo la barra"
|
||||
else
|
||||
ko "PTY: output finale corretto dopo la barra"
|
||||
fi
|
||||
if grep -q "ETA" <<<"$pty_out"; then
|
||||
ok "PTY: ETA mostrata durante il trasferimento"
|
||||
else
|
||||
ko "PTY: ETA mostrata durante il trasferimento"
|
||||
fi
|
||||
# nessun codice ANSI (retro-compatibilita' delle console)
|
||||
if grep -qP '\x1b\[' <<<"$pty_out"; then
|
||||
ko "PTY: nessun codice ANSI nel disegno"
|
||||
else
|
||||
ok "PTY: nessun codice ANSI nel disegno"
|
||||
fi
|
||||
# la barra non deve eccedere la larghezza del terminale (script: 80 colonne).
|
||||
# I glifi Unicode sono multi-byte: si contano i code point, non i byte.
|
||||
# solo le righe disegnate dalla barra (l'output normale puo' essere lungo)
|
||||
bar_lines=$(grep -E "^(Scaricamento|Estrazione)[ :]" <<<"$pty_out")
|
||||
longest=$(printf '%s' "$bar_lines" | "$PY" -c "import sys; print(max((len(l) for l in sys.stdin.read().split('\\n')), default=0))")
|
||||
if [ "${longest:-0}" -le 80 ]; then
|
||||
ok "PTY: righe entro 80 colonne (max $longest)"
|
||||
else
|
||||
ko "PTY: righe entro 80 colonne (max $longest)"
|
||||
printf '%s' "$bar_lines" | "$PY" -c "
|
||||
import sys
|
||||
lines = sys.stdin.buffer.read().decode('utf-8', 'replace').split('\n')
|
||||
for n, l in sorted(((len(x), x) for x in lines), reverse=True)[:3]:
|
||||
print(f' {n:3} |{l}|')
|
||||
"
|
||||
fi
|
||||
|
||||
# --------------------------------------------------------------- 3. modalita' plain
|
||||
plain_out=$(CELLAR_PROGRESS=plain "$C_BIN" --server "$SRV" download 1 "$WORK/dl-plain.bin" 2>&1)
|
||||
lines=$(grep -c "^Scaricamento: [0-9]" <<<"$plain_out")
|
||||
if [ "$lines" -ge 10 ]; then
|
||||
ok "plain: $lines righe di avanzamento (>=10)"
|
||||
else
|
||||
ko "plain: righe di avanzamento insufficienti ($lines)"
|
||||
fi
|
||||
if grep -q "^Scaricamento: completato" <<<"$plain_out" && ! grep -q "\[" <<<"$plain_out"; then
|
||||
ok "plain: riepilogo presente e nessuna barra grafica"
|
||||
else
|
||||
ko "plain: riepilogo presente e nessuna barra grafica"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------- 4. off
|
||||
off_out=$(script -qec "CELLAR_PROGRESS=off '$C_BIN' --server '$SRV' download 1 '$WORK/dl-off.bin'" /dev/null 2>&1 | tr '\r' '\n')
|
||||
if ! grep -qE "Scaricamento" <<<"$off_out"; then
|
||||
ok "off: nessun output di progresso (anche su PTY)"
|
||||
else
|
||||
ko "off: nessun output di progresso (anche su PTY)"
|
||||
fi
|
||||
|
||||
# ------------------------------------------------- 5. integrita' dell'artefatto
|
||||
if [ -f "$WORK/dl-pipe.bin" ] && cmp -s "$WORK/dl-pipe.bin" "$WORK/big.tar.gz"; then
|
||||
ok "integrita': il file scaricato coincide con quello caricato"
|
||||
else
|
||||
ko "integrita': il file scaricato coincide con quello caricato"
|
||||
fi
|
||||
if [ -f "$WORK/inst/Fake Bottle/bottle.yml" ]; then
|
||||
ok "install: bottiglia installata nonostante il disegno della barra"
|
||||
else
|
||||
ko "install: bottiglia installata nonostante il disegno della barra"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "=================================================="
|
||||
printf 'PASS: %d FAIL: %d\n' "$PASS" "$FAIL"
|
||||
[ "$FAIL" -eq 0 ] || exit 1
|
||||
echo "barra di avanzamento ok ✔"
|
||||
Binary file not shown.
@@ -0,0 +1,5 @@
|
||||
Name: 'Fake Bottle'
|
||||
Runner: soda-9.0-1
|
||||
Arch: win32
|
||||
Windows: win10
|
||||
Environment: Gaming
|
||||
@@ -0,0 +1 @@
|
||||
../drive_c/config.ini
|
||||
@@ -0,0 +1 @@
|
||||
hello world
|
||||
+1
@@ -0,0 +1 @@
|
||||
long path
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
/nonexistent/target
|
||||
@@ -0,0 +1 @@
|
||||
config
|
||||
@@ -0,0 +1 @@
|
||||
config.ini
|
||||
@@ -0,0 +1 @@
|
||||
unicode
|
||||
@@ -0,0 +1 @@
|
||||
binario
|
||||
@@ -0,0 +1 @@
|
||||
deep
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,4 @@
|
||||
[cellar]
|
||||
server = http://127.0.0.1:8080
|
||||
bottles_dir = /home/enne2/Dev/cellar-cli-c/tests/tmp-progress/home/.var/app/com.usebottles.bottles/data/bottles/bottles
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
Name: 'Fake Bottle'
|
||||
Runner: soda-9.0-1
|
||||
Arch: win32
|
||||
Windows: win10
|
||||
Environment: Gaming
|
||||
@@ -0,0 +1 @@
|
||||
../drive_c/config.ini
|
||||
@@ -0,0 +1 @@
|
||||
hello world
|
||||
+1
@@ -0,0 +1 @@
|
||||
long path
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
/nonexistent/target
|
||||
@@ -0,0 +1 @@
|
||||
config
|
||||
@@ -0,0 +1 @@
|
||||
config.ini
|
||||
@@ -0,0 +1 @@
|
||||
unicode
|
||||
+1
@@ -0,0 +1 @@
|
||||
binario
|
||||
@@ -0,0 +1 @@
|
||||
deep
|
||||
@@ -0,0 +1,18 @@
|
||||
[
|
||||
{
|
||||
"name": "BigBottle",
|
||||
"bottle_name": "Fake Bottle",
|
||||
"description": null,
|
||||
"tags": null,
|
||||
"arch": null,
|
||||
"runner": null,
|
||||
"windows_version": null,
|
||||
"id": 1,
|
||||
"file_name": "big.tar.gz",
|
||||
"stored_name": "b2b18dc0668e8b5b661c952112aeb58a.tar.gz",
|
||||
"content_type": "application/gzip",
|
||||
"size_bytes": 12009363,
|
||||
"sha256": "b2b18dc0668e8b5b661c952112aeb58a3b16a1552156b0098823e47ca89f2434",
|
||||
"created_at": "2026-09-20T15:44:25.190979+00:00"
|
||||
}
|
||||
]
|
||||
Binary file not shown.
Reference in New Issue
Block a user