safekeep 0.4.0__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. {safekeep-0.4.0/safekeep.egg-info → safekeep-0.5.0}/PKG-INFO +6 -5
  2. {safekeep-0.4.0 → safekeep-0.5.0}/README.md +5 -4
  3. {safekeep-0.4.0 → safekeep-0.5.0}/safekeep/__init__.py +1 -1
  4. {safekeep-0.4.0 → safekeep-0.5.0}/safekeep/cli.py +44 -8
  5. {safekeep-0.4.0 → safekeep-0.5.0/safekeep.egg-info}/PKG-INFO +6 -5
  6. {safekeep-0.4.0 → safekeep-0.5.0}/safekeep.egg-info/SOURCES.txt +1 -0
  7. {safekeep-0.4.0 → safekeep-0.5.0}/tests/test_cli.py +80 -3
  8. safekeep-0.5.0/tests/test_trend.py +80 -0
  9. {safekeep-0.4.0 → safekeep-0.5.0}/LICENSE +0 -0
  10. {safekeep-0.4.0 → safekeep-0.5.0}/pyproject.toml +0 -0
  11. {safekeep-0.4.0 → safekeep-0.5.0}/safekeep/config.py +0 -0
  12. {safekeep-0.4.0 → safekeep-0.5.0}/safekeep/copier.py +0 -0
  13. {safekeep-0.4.0 → safekeep-0.5.0}/safekeep/daemon.py +0 -0
  14. {safekeep-0.4.0 → safekeep-0.5.0}/safekeep/matcher.py +0 -0
  15. {safekeep-0.4.0 → safekeep-0.5.0}/safekeep/platform.py +0 -0
  16. {safekeep-0.4.0 → safekeep-0.5.0}/safekeep/volumes.py +0 -0
  17. {safekeep-0.4.0 → safekeep-0.5.0}/safekeep.egg-info/dependency_links.txt +0 -0
  18. {safekeep-0.4.0 → safekeep-0.5.0}/safekeep.egg-info/entry_points.txt +0 -0
  19. {safekeep-0.4.0 → safekeep-0.5.0}/safekeep.egg-info/top_level.txt +0 -0
  20. {safekeep-0.4.0 → safekeep-0.5.0}/setup.cfg +0 -0
  21. {safekeep-0.4.0 → safekeep-0.5.0}/tests/test_config.py +0 -0
  22. {safekeep-0.4.0 → safekeep-0.5.0}/tests/test_copier.py +0 -0
  23. {safekeep-0.4.0 → safekeep-0.5.0}/tests/test_daemon.py +0 -0
  24. {safekeep-0.4.0 → safekeep-0.5.0}/tests/test_matcher.py +0 -0
  25. {safekeep-0.4.0 → safekeep-0.5.0}/tests/test_platform.py +0 -0
  26. {safekeep-0.4.0 → safekeep-0.5.0}/tests/test_volumes.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: safekeep
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: Selective always-on backups for macOS and Linux: fswatch-driven copy of allow-listed files, never deletes
5
5
  License-Expression: MIT
6
6
  Project-URL: Repository, https://github.com/foxhound87/safekeep
@@ -182,7 +182,7 @@ safekeep <command> [--config PATH] [-v]
182
182
  | `run` | daemon: initial reconcile, fswatch loop, event dispatch, 24h timer |
183
183
  | `sync-once [--dry-run] [--project PATH] [--prune]` | a single pass: walks the source and copies whatever differs (`--dry-run` only prints what it would copy; `--prune` also removes dest files whose source still exists but is no longer included) |
184
184
  | `status` | read-only: config, sources, discovered projects with N rules, destination states |
185
- | `doctor` | diagnostics: config, fswatch + platform monitor, python, TCC/launchd plist (macOS), inotify limit and systemd unit (Linux) — exits non-zero if a fatal check fails |
185
+ | `doctor [--json]` | diagnostics: config, fswatch + platform monitor, python, TCC/launchd plist (macOS), inotify limit and systemd unit (Linux) — exits non-zero if a fatal check fails; `--json` prints the same checks as one JSON document on stdout with the same exit code, including a structured `permission_errors` count of the last 24h |
186
186
 
187
187
  ## Tests
188
188
 
@@ -190,9 +190,10 @@ safekeep <command> [--config PATH] [-v]
190
190
  python3 -m unittest discover -s tests
191
191
  ```
192
192
 
193
- Stdlib (`unittest`) suite, zero dependencies: 175 tests covering the matcher,
194
- config, atomic copy, volumes, daemon, auto-discovery, CLI and the POSIX
195
- portability layer (platform helper, per-OS fswatch monitor, systemd template).
193
+ Stdlib (`unittest`) suite, zero dependencies: 206 tests covering the matcher,
194
+ config, atomic copy, volumes, daemon, auto-discovery, CLI, the permission-trend
195
+ wrapper and the POSIX portability layer (platform helper, per-OS fswatch
196
+ monitor, systemd template).
196
197
  `fswatch` is not needed to run the suite — the few `doctor` checks that talk to
197
198
  the real binary are skipped when it's missing — but CI installs it so the
198
199
  Linux job exercises the real `inotify` monitor.
@@ -157,7 +157,7 @@ safekeep <command> [--config PATH] [-v]
157
157
  | `run` | daemon: initial reconcile, fswatch loop, event dispatch, 24h timer |
158
158
  | `sync-once [--dry-run] [--project PATH] [--prune]` | a single pass: walks the source and copies whatever differs (`--dry-run` only prints what it would copy; `--prune` also removes dest files whose source still exists but is no longer included) |
159
159
  | `status` | read-only: config, sources, discovered projects with N rules, destination states |
160
- | `doctor` | diagnostics: config, fswatch + platform monitor, python, TCC/launchd plist (macOS), inotify limit and systemd unit (Linux) — exits non-zero if a fatal check fails |
160
+ | `doctor [--json]` | diagnostics: config, fswatch + platform monitor, python, TCC/launchd plist (macOS), inotify limit and systemd unit (Linux) — exits non-zero if a fatal check fails; `--json` prints the same checks as one JSON document on stdout with the same exit code, including a structured `permission_errors` count of the last 24h |
161
161
 
162
162
  ## Tests
163
163
 
@@ -165,9 +165,10 @@ safekeep <command> [--config PATH] [-v]
165
165
  python3 -m unittest discover -s tests
166
166
  ```
167
167
 
168
- Stdlib (`unittest`) suite, zero dependencies: 175 tests covering the matcher,
169
- config, atomic copy, volumes, daemon, auto-discovery, CLI and the POSIX
170
- portability layer (platform helper, per-OS fswatch monitor, systemd template).
168
+ Stdlib (`unittest`) suite, zero dependencies: 206 tests covering the matcher,
169
+ config, atomic copy, volumes, daemon, auto-discovery, CLI, the permission-trend
170
+ wrapper and the POSIX portability layer (platform helper, per-OS fswatch
171
+ monitor, systemd template).
171
172
  `fswatch` is not needed to run the suite — the few `doctor` checks that talk to
172
173
  the real binary are skipped when it's missing — but CI installs it so the
173
174
  Linux job exercises the real `inotify` monitor.
@@ -3,4 +3,4 @@ import logging
3
3
 
4
4
  logging.getLogger('safekeep').addHandler(logging.NullHandler())
5
5
 
6
- __version__ = '0.4.0'
6
+ __version__ = '0.5.0'
@@ -4,11 +4,15 @@ Entry point del package (`safekeep.cli:main`): `bin/safekeep.py` è solo lo
4
4
  shim che usano launchd/systemd e i test.
5
5
  """
6
6
  import argparse
7
+ import json
7
8
  import os
9
+ import re
8
10
  import shutil
9
11
  import subprocess
10
12
  import sys
13
+ from datetime import datetime
11
14
 
15
+ from safekeep import __version__
12
16
  from safekeep.config import ConfigError, discover_projects, parse_sync
13
17
  from safekeep.copier import TMP_INFIX
14
18
  from safekeep.daemon import Daemon, setup_logging
@@ -58,11 +62,14 @@ def build_parser():
58
62
  'non è più incluso dal matcher (default: mai cancellare)')
59
63
  sub.add_parser('status', parents=[common],
60
64
  help='config, progetti e stato delle dest — sola lettura')
61
- sub.add_parser('doctor', parents=[common],
62
- help='diagnostica: config, fswatch + monitor di piattaforma, python, '
63
- 'errori di permesso recenti nel log, TCC/plist (macOS), '
64
- 'inotify/systemd/linger (Linux) — exit ≠ 0 se un check '
65
- 'fatale fallisce')
65
+ doc = sub.add_parser('doctor', parents=[common],
66
+ help='diagnostica: config, fswatch + monitor di piattaforma, '
67
+ 'python, errori di permesso recenti nel log, TCC/plist '
68
+ '(macOS), inotify/systemd/linger (Linux) — exit ≠ 0 se '
69
+ 'un check fatale fallisce')
70
+ doc.add_argument('--json', action='store_true',
71
+ help='stessi check, stesso exit code, ma un documento JSON su '
72
+ 'stdout (SPEC.md §9.1)')
66
73
  return parser
67
74
 
68
75
 
@@ -144,12 +151,28 @@ def check_sync_files(daemon):
144
151
 
145
152
 
146
153
  def cmd_doctor(args):
147
- """Un check per riga con esito ✔/✗; exit 1 se almeno un check fatale fallisce."""
154
+ """Un check per riga con esito ✔/✗; exit 1 se almeno un check fatale fallisce.
155
+
156
+ Con `--json` le righe non vengono stampate: la stessa sequenza di check finisce
157
+ in un documento JSON su stdout, con lo stesso exit code (SPEC.md §9.1)."""
148
158
  fatal = False
159
+ checks, ids = [], {}
149
160
 
150
161
  def out(ok, msg, is_fatal=False):
151
162
  nonlocal fatal
152
- print(('✔ ' if ok else '✗ ') + msg)
163
+ if args.json:
164
+ # id = slug del messaggio (prefisso fino a ':' o '('), id uguali
165
+ # ripetuti suffissati _2/_3 così restano chiave usabile (SPEC §9.1)
166
+ base = re.split(r'[:\(]', msg, maxsplit=1)[0]
167
+ ident = re.sub(r'[^a-z0-9]+', '_', base.lower()).strip('_') or 'check'
168
+ ids[ident] = ids.get(ident, 0) + 1
169
+ if ids[ident] > 1:
170
+ ident = f'{ident}_{ids[ident]}'
171
+ checks.append({'id': ident,
172
+ 'status': 'ok' if ok else ('fail' if is_fatal else 'warn'),
173
+ 'message': msg})
174
+ else:
175
+ print(('✔ ' if ok else '✗ ') + msg)
153
176
  if not ok and is_fatal:
154
177
  fatal = True
155
178
 
@@ -346,7 +369,20 @@ def cmd_doctor(args):
346
369
  'senza login → loginctl enable-linger $USER — '
347
370
  'warning non fatale')
348
371
 
349
- return 1 if fatal else 0
372
+ code = 1 if fatal else 0
373
+ if args.json:
374
+ count, last = recent if recent is not None else (0, None)
375
+ print(json.dumps({
376
+ 'safekeep': __version__,
377
+ 'timestamp': datetime.now().isoformat(timespec='seconds'),
378
+ 'exit': code,
379
+ 'checks': checks,
380
+ 'permission_errors': {
381
+ 'count_24h': count,
382
+ 'last': last.strftime('%Y-%m-%d %H:%M:%S') if last else None,
383
+ },
384
+ }, ensure_ascii=False, indent=2))
385
+ return code
350
386
 
351
387
 
352
388
  def main(argv=None):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: safekeep
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: Selective always-on backups for macOS and Linux: fswatch-driven copy of allow-listed files, never deletes
5
5
  License-Expression: MIT
6
6
  Project-URL: Repository, https://github.com/foxhound87/safekeep
@@ -182,7 +182,7 @@ safekeep <command> [--config PATH] [-v]
182
182
  | `run` | daemon: initial reconcile, fswatch loop, event dispatch, 24h timer |
183
183
  | `sync-once [--dry-run] [--project PATH] [--prune]` | a single pass: walks the source and copies whatever differs (`--dry-run` only prints what it would copy; `--prune` also removes dest files whose source still exists but is no longer included) |
184
184
  | `status` | read-only: config, sources, discovered projects with N rules, destination states |
185
- | `doctor` | diagnostics: config, fswatch + platform monitor, python, TCC/launchd plist (macOS), inotify limit and systemd unit (Linux) — exits non-zero if a fatal check fails |
185
+ | `doctor [--json]` | diagnostics: config, fswatch + platform monitor, python, TCC/launchd plist (macOS), inotify limit and systemd unit (Linux) — exits non-zero if a fatal check fails; `--json` prints the same checks as one JSON document on stdout with the same exit code, including a structured `permission_errors` count of the last 24h |
186
186
 
187
187
  ## Tests
188
188
 
@@ -190,9 +190,10 @@ safekeep <command> [--config PATH] [-v]
190
190
  python3 -m unittest discover -s tests
191
191
  ```
192
192
 
193
- Stdlib (`unittest`) suite, zero dependencies: 175 tests covering the matcher,
194
- config, atomic copy, volumes, daemon, auto-discovery, CLI and the POSIX
195
- portability layer (platform helper, per-OS fswatch monitor, systemd template).
193
+ Stdlib (`unittest`) suite, zero dependencies: 206 tests covering the matcher,
194
+ config, atomic copy, volumes, daemon, auto-discovery, CLI, the permission-trend
195
+ wrapper and the POSIX portability layer (platform helper, per-OS fswatch
196
+ monitor, systemd template).
196
197
  `fswatch` is not needed to run the suite — the few `doctor` checks that talk to
197
198
  the real binary are skipped when it's missing — but CI installs it so the
198
199
  Linux job exercises the real `inotify` monitor.
@@ -20,4 +20,5 @@ tests/test_copier.py
20
20
  tests/test_daemon.py
21
21
  tests/test_matcher.py
22
22
  tests/test_platform.py
23
+ tests/test_trend.py
23
24
  tests/test_volumes.py
@@ -6,6 +6,7 @@ import argparse
6
6
  import contextlib
7
7
  import getpass
8
8
  import io
9
+ import json
9
10
  import os
10
11
  import shutil
11
12
  import subprocess
@@ -110,7 +111,7 @@ class DoctorTest(CliTestCase):
110
111
  """SPEC.md §14.4: su Linux il limite di watch basso è informativo —
111
112
  doctor esce 0, non 1. I rami Linux si esercitano patchando l'helper,
112
113
  senza girare su Linux."""
113
- args = argparse.Namespace(config=self.cfg, v=False)
114
+ args = argparse.Namespace(config=self.cfg, v=False, json=False)
114
115
  buf = io.StringIO()
115
116
  with mock.patch.object(cli, 'is_linux', return_value=True), \
116
117
  mock.patch.object(cli, 'is_darwin', return_value=False), \
@@ -329,7 +330,7 @@ class DoctorLingerTest(CliTestCase):
329
330
  self.addCleanup(shutil.rmtree, linger_dir, ignore_errors=True)
330
331
  for name in linger:
331
332
  open(os.path.join(linger_dir, name), 'w').close()
332
- args = argparse.Namespace(config=self.cfg, v=False)
333
+ args = argparse.Namespace(config=self.cfg, v=False, json=False)
333
334
  buf = io.StringIO()
334
335
  with mock.patch.dict(os.environ, {'HOME': self.home}), \
335
336
  mock.patch.object(cli, 'is_linux', return_value=True), \
@@ -368,7 +369,7 @@ class DoctorWslTest(CliTestCase):
368
369
 
369
370
  def doctor_wsl(self, distro='Ubuntu', systemd=False, dest='/mnt/c/backup'):
370
371
  cfg = write(os.path.join(self.tmp, 'wsl.cfg'), f'dest: {dest}\n')
371
- args = argparse.Namespace(config=cfg, v=False)
372
+ args = argparse.Namespace(config=cfg, v=False, json=False)
372
373
  buf = io.StringIO()
373
374
  with mock.patch.dict(os.environ, {'HOME': self.home}), \
374
375
  mock.patch.object(cli, 'is_wsl', return_value=True), \
@@ -426,5 +427,81 @@ class DoctorWslTest(CliTestCase):
426
427
  self.assertNotIn('drvfs', r.stdout)
427
428
 
428
429
 
430
+ class DoctorJsonTest(CliTestCase):
431
+ """SPEC.md §9.1: `doctor --json` — stessi check, stesso ordine, stesso exit
432
+ code della modalità normale, tutto in un documento JSON su stdout."""
433
+
434
+ def json_doctor(self, *args):
435
+ r = self.cli('doctor', '--json', *args)
436
+ # json.loads sul stdout intero: basta una riga di log mista e cade qui
437
+ return r, json.loads(r.stdout)
438
+
439
+ @staticmethod
440
+ def righe(stdout):
441
+ """`(status_atteso, message)` di ogni riga della modalità normale."""
442
+ out = []
443
+ for line in stdout.splitlines():
444
+ if not line.strip():
445
+ continue
446
+ status = 'ok' if line.startswith('✔') else 'fail-o-warn'
447
+ out.append((status, line[2:]))
448
+ return out
449
+
450
+ def test_checks_1a1_con_la_modalita_normale(self):
451
+ normale = self.cli('doctor', '--config', self.cfg)
452
+ r, doc = self.json_doctor('--config', self.cfg)
453
+ attese = self.righe(normale.stdout)
454
+ self.assertEqual([c['message'] for c in doc['checks']],
455
+ [msg for _, msg in attese],
456
+ 'stessi check, stesso ordine della modalità normale')
457
+ for (atteso, _), check in zip(attese, doc['checks']):
458
+ if atteso == 'ok':
459
+ self.assertEqual(check['status'], 'ok', check)
460
+ else: # riga ✗: warn o fail
461
+ self.assertIn(check['status'], ('warn', 'fail'), check)
462
+ self.assertEqual(len({c['id'] for c in doc['checks']}),
463
+ len(doc['checks']), 'id duplicati')
464
+ self.assertEqual(r.returncode, normale.returncode, 'stesso exit code')
465
+ self.assertEqual(doc['exit'], r.returncode)
466
+
467
+ def test_header_e_campi_decisionali(self):
468
+ _, doc = self.json_doctor('--config', self.cfg)
469
+ self.assertEqual(doc['safekeep'], cli.__version__)
470
+ datetime.fromisoformat(doc['timestamp']) # ISO 8601 valido
471
+ self.assertIsInstance(doc['checks'], list)
472
+ self.assertTrue(doc['checks'])
473
+ for check in doc['checks']:
474
+ self.assertEqual(set(check), {'id', 'status', 'message'})
475
+ self.assertIn(check['status'], ('ok', 'warn', 'fail'))
476
+ self.assertEqual(set(doc['permission_errors']), {'count_24h', 'last'})
477
+
478
+ def test_permission_errors_strutturati_sul_log_recente(self):
479
+ log = os.path.join(self.home, '.local/state/safekeep/safekeep.log')
480
+ stamp = (datetime.now() - timedelta(hours=3)).strftime('%Y-%m-%d %H:%M:%S')
481
+ write(log, f'{stamp},000 ERROR copia fallita: [Errno 1] Operation not permitted\n')
482
+ r, doc = self.json_doctor('--config', self.cfg)
483
+ perm = doc['permission_errors']
484
+ self.assertEqual(perm['count_24h'], 1)
485
+ self.assertEqual(perm['last'], stamp) # ultimo timestamp
486
+ check = next(c for c in doc['checks']
487
+ if 'errori di permesso' in c['message'])
488
+ self.assertEqual(check['status'], 'warn') # warning, NON fatale
489
+ self.assertEqual(r.returncode, 0, r.stdout)
490
+
491
+ def test_log_assente_count_zero_last_null(self):
492
+ _, doc = self.json_doctor('--config', self.cfg) # HOME finto: niente log
493
+ self.assertEqual(doc['permission_errors'],
494
+ {'count_24h': 0, 'last': None})
495
+
496
+ @unittest.skipUnless(shutil.which('fswatch'), 'fswatch non installato')
497
+ def test_check_fatale_esce_1_e_lo_dichiara(self):
498
+ bad = write(os.path.join(self.tmp, 'bad.cfg'), 'riga nuda invalida\n')
499
+ r, doc = self.json_doctor('--config', bad)
500
+ self.assertEqual(r.returncode, 1, r.stdout)
501
+ self.assertEqual(doc['exit'], 1)
502
+ self.assertTrue(any(c['status'] == 'fail' for c in doc['checks']),
503
+ 'il check fatale deve comparire come fail')
504
+
505
+
429
506
  if __name__ == '__main__':
430
507
  unittest.main()
@@ -0,0 +1,80 @@
1
+ """SPEC.md §9.2: `bin/safekeep-trend.sh` — wrapper trend errori di permesso.
2
+
3
+ Ogni run è un subprocess con HOME finto: niente scritture nella home reale e
4
+ niente tocco ai log reali. Copre: header scritto una sola volta (idempotente),
5
+ riga parseabile a 4 campi, riga appendata anche quando `doctor` esce 1, override
6
+ del path CSV via env.
7
+ """
8
+ import os
9
+ import shutil
10
+ import subprocess
11
+ import tempfile
12
+ import unittest
13
+
14
+ REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
15
+ TREND = os.path.join(REPO, 'bin', 'safekeep-trend.sh')
16
+ HEADER = 'timestamp,exit,count_24h,last'
17
+
18
+
19
+ @unittest.skipUnless(shutil.which('bash'), 'bash non disponibile')
20
+ class TrendTestCase(unittest.TestCase):
21
+ def setUp(self):
22
+ self.tmp = tempfile.mkdtemp()
23
+ self.addCleanup(shutil.rmtree, self.tmp, ignore_errors=True)
24
+ self.home = os.path.join(self.tmp, 'home') # HOME finto
25
+ os.makedirs(self.home)
26
+ self.csv = os.path.join(self.home, '.local', 'state', 'safekeep',
27
+ 'permission-trend.csv')
28
+
29
+ def trend(self, *args, csv=None):
30
+ env = dict(os.environ, HOME=self.home)
31
+ if csv is not None:
32
+ env['SAFEKEEP_TREND_CSV'] = csv
33
+ return subprocess.run(['bash', TREND, *args], capture_output=True,
34
+ text=True, env=env, cwd=REPO, timeout=120)
35
+
36
+ def lines(self, path=None):
37
+ with open(path or self.csv, encoding='utf-8') as fh:
38
+ return fh.read().splitlines()
39
+
40
+ def test_header_e_riga_parseabile(self):
41
+ r = self.trend()
42
+ self.assertEqual(r.returncode, 0, r.stderr)
43
+ lines = self.lines()
44
+ self.assertEqual(lines[0], HEADER)
45
+ self.assertEqual(len(lines), 2) # header + 1 run
46
+ row = lines[1].split(',')
47
+ self.assertEqual(len(row), 4) # split campi, niente quoting
48
+ self.assertEqual(row[1], '0') # exit di doctor (JSON §9.1)
49
+ self.assertEqual(int(row[2]), int(row[2])) # count_24h intero
50
+ self.assertTrue(row[0].startswith('20')) # timestamp ISO 8601
51
+
52
+ def test_run_multipla_non_duplica_header(self):
53
+ self.assertEqual(self.trend().returncode, 0)
54
+ self.assertEqual(self.trend().returncode, 0)
55
+ lines = self.lines()
56
+ self.assertEqual(len(lines), 3) # header + 2 run
57
+ self.assertEqual(lines.count(HEADER), 1) # header una sola volta
58
+ self.assertEqual(lines[0], HEADER)
59
+
60
+ def test_doctor_exit_1_genera_comunque_riga(self):
61
+ # config inesistente passata esplicitamente → check fatale → exit 1,
62
+ # ma l'exit code è un dato: la riga va scritta (SPEC §9.2)
63
+ missing = os.path.join(self.tmp, 'non-esiste.cfg')
64
+ r = self.trend('--config', missing)
65
+ self.assertEqual(r.returncode, 0, r.stderr)
66
+ row = self.lines()[1].split(',')
67
+ self.assertEqual(row[1], '1')
68
+ self.assertEqual(int(row[2]), int(row[2]))
69
+
70
+ def test_override_csv_path(self):
71
+ alt = os.path.join(self.tmp, 'alt', 'trend.csv')
72
+ r = self.trend(csv=alt)
73
+ self.assertEqual(r.returncode, 0, r.stderr)
74
+ self.assertTrue(os.path.exists(alt))
75
+ self.assertFalse(os.path.exists(self.csv)) # default non toccato
76
+ self.assertEqual(self.lines(alt)[0], HEADER)
77
+
78
+
79
+ if __name__ == '__main__':
80
+ unittest.main()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes