def test_hashability(singlefile_content): d = { 't1': torf.Torrent(singlefile_content.path, comment='One'), 't2': torf.Torrent(singlefile_content.path, comment='Two') } assert d['t1'].comment == 'One' assert d['t2'].comment == 'Two'
def test_randomize_infohash(singlefile_content): t1 = torf.Torrent(singlefile_content.path) t2 = torf.Torrent(singlefile_content.path) t1.generate() t2.generate() t1.randomize_infohash = False t2.randomize_infohash = False assert t1.infohash == t2.infohash t1.randomize_infohash = True t2.randomize_infohash = True assert t1.infohash != t2.infohash
def assert_callback_called(torrent): t = torf.Torrent(torrent.path) cb = mock.Mock() cb.return_value = None success = t.generate(callback=cb, interval=0) assert success # Compare number of callback calls number_of_pieces = math.ceil(t.size / t.piece_size) exp_call_count = pytest.approx(number_of_pieces, abs=1) assert cb.call_count == exp_call_count # Compare arguments without filepaths (too complex for me) stripped_call_args_list = [ mock.call(args[0][0], args[0][2], args[0][3]) for args in cb.call_args_list ] exp_call_args_list = [ mock.call(t, i, number_of_pieces) for i in range(1, number_of_pieces + 1) ] # There can be slightly more actual calls than expected calls or vice versa call_num = min(len(stripped_call_args_list), len(exp_call_args_list)) stripped_call_args_list = stripped_call_args_list[:call_num] exp_call_args_list = exp_call_args_list[:call_num] assert stripped_call_args_list == exp_call_args_list # Make sure that the expected filepaths were reported to callback processed_filepaths = [] for args in cb.call_args_list: filepath = args[0][1] if filepath not in processed_filepaths: processed_filepaths.append(filepath) exp_filepaths = list(t.filepaths) assert processed_filepaths == exp_filepaths
def test_reuse_with_empty_file_list(with_callback, existing_torrents, create_file): # Create and prepare existing torrents existing_torrents = existing_torrents(my_torrents=( ('a.jpg', 'foo', { 'creation_date': 123 }), ('b.txt', 'bar', { 'creation_date': 456 }), ('c.mp4', 'baz', { 'creation_date': 789 }), ), ) # Create and prepare the torrent we want to generate new_torrent = torf.Torrent( path=create_file('just_a_file.jpg', 'foo'), exclude_globs=['*.jpg'], ) # Expect identical metainfo exp_joined_metainfo = copy.deepcopy(new_torrent.metainfo) exp_exception = RuntimeError('reuse() called while file list is empty') with pytest.raises(type(exp_exception), match=rf'^{re.escape(str(exp_exception))}$'): if with_callback: new_torrent.reuse(existing_torrents.location_paths, callback=Mock()) else: new_torrent.reuse(existing_torrents.location_paths) assert new_torrent.metainfo == exp_joined_metainfo
def _verify_mode(cfg): # Read existing torrent try: infile_torrent = torf.Torrent.read(cfg['in']) except torf.TorfError as e: raise MainError(e, errno=e.errno) # Create torrent from PATH try: path_torrent = torf.Torrent(path=cfg['PATH'], piece_size=infile_torrent.piece_size, private=infile_torrent.private) except torf.TorfError as e: raise MainError(e, errno=e.errno) if infile_torrent.mode is None: raise MainError(f'{cfg["in"]}: Empty torrent', errno=errno.ENOENT) # Instead of hashing the content on disk, we do some quick checks first. elif infile_torrent.mode == 'singlefile': # Single-file torrent if not os.path.isfile(path_torrent.path): raise MainError(f'{cfg["PATH"]}: {os.strerror(errno.EISDIR)}', errno=errno.EISDIR) local_path_size = os.path.getsize(os.path.realpath(path_torrent.path)) if local_path_size != infile_torrent.size: raise MainError(f'{cfg["PATH"]}: Mismatching size', errno=errno.EFBIG) elif infile_torrent.mode == 'multifile': # Multi-file torrent if not os.path.isdir(path_torrent.path): raise MainError(f'{cfg["PATH"]}: {os.strerror(errno.ENOTDIR)}', errno=errno.ENOTDIR) for file in infile_torrent.files: # Remove first directory in path to get path inside the torrent path_in_torrent = os.path.join(*(file.split(os.path.sep)[1:])) local_path = os.path.join(cfg['PATH'], path_in_torrent) # Check if file exists if not os.path.exists(local_path): raise MainError(f'{local_path}: {os.strerror(errno.ENOENT)}', errno=errno.ENOENT) # Check file sizes path_in_torrent_size = infile_torrent.file_size(path_in_torrent) local_path_size = os.path.getsize(local_path) if local_path_size != path_in_torrent_size: raise MainError(f'{local_path}: Mismatching size', errno=errno.EFBIG) # Finally, compare the metainfo hashes path_torrent.generate() if infile_torrent.infohash != path_torrent.infohash: raise MainError(f'{path_torrent.infohash}: Mismatching hash', errno=errno.EADV) print('{cfg["PATH"]} contains everything described in {cfg["in"]}')
def test_partial_size__singlefile__providing_path(tmp_path): (tmp_path / 'content.jpg').write_text('some data') t = torf.Torrent(tmp_path / 'content.jpg') for path in ('bar/foo.jpg', ['bar', 'foo.jpg']): with pytest.raises(torf.PathError) as excinfo: t.partial_size(path) assert excinfo.match(f'^bar/foo.jpg: Not specified in metainfo$')
def test_from_torrent_without_size(singlefile_content, multifile_content): for content in singlefile_content, multifile_content: t = torf.Torrent(content.path, trackers=['http://foo', 'http://bar']) t.generate() assert str(t.magnet(size=False)) == (f'magnet:?xt=urn:btih:{t.infohash}' f'&dn={quote_plus(t.name)}' f'&tr=http%3A%2F%2Ffoo&tr=http%3A%2F%2Fbar')
def test_partial_size__singlefile__providing_wrong_name(tmp_path): (tmp_path / 'content.jpg').write_text('some data') t = torf.Torrent(tmp_path / 'content.jpg') for path in ('foo.jpg', ['foo.jpg']): with pytest.raises(torf.PathError) as excinfo: t.partial_size(path) assert excinfo.match('^foo.jpg: Unknown path$')
def test_nonexisting_path(create_file): content_path = create_file('file.jpg', '<image data>') t = torf.Torrent(content_path) content_path.unlink() with pytest.raises(torf.ReadError) as e: t.generate() assert str(e.value) == f'{content_path}: No such file or directory'
def _create_mode(cfg): try: torrent = torf.Torrent( path=cfg['PATH'], name=cfg['name'] or None, exclude=cfg['exclude'], trackers=() if cfg['notracker'] else cfg['tracker'], webseeds=() if cfg['nowebseed'] else cfg['webseed'], private=False if cfg['noprivate'] else cfg['private'], source=None if cfg['nosource'] or not cfg['source'] else cfg['source'], randomize_infohash=False if cfg['noxseed'] else cfg['xseed'], comment=None if cfg['nocomment'] else cfg['comment'], created_by=None if cfg['nocreator'] else _config.DEFAULT_CREATOR ) except torf.TorfError as e: raise MainError(e, errno=e.errno) if not cfg['nodate']: try: torrent.creation_date = _util.parse_date(cfg['date'] or 'now') except ValueError: raise MainError(f'{cfg["date"]}: Invalid date', errno=errno.EINVAL) if cfg['max_piece_size']: max_piece_size = cfg['max_piece_size'] * 1048576 if torrent.piece_size > max_piece_size: try: torrent.piece_size = max_piece_size except torf.PieceSizeError as e: raise MainError(e, errno=errno.EINVAL) _check_output_file_exists(torrent, cfg) _show_torrent_info(torrent, cfg) _hash_pieces(torrent, cfg) _write_torrent(torrent, cfg)
def _create_torrents(directory, *items): torrents_directory = tmp_path / directory torrents_directory.mkdir(exist_ok=True) torrents = [] for item in items: torrent_name = item[0] create_args = item[1] torrent_kwargs = item[2] if isinstance(create_args, collections.abc.Sequence) and not isinstance( create_args, str): content_path = create_dir(torrent_name, *create_args) else: content_path = create_file(torrent_name, create_args) torrent = torf.Torrent(path=content_path, **torrent_kwargs) torrent_filepath = torrents_directory / f'{torrent_name}.torrent' torrent.generate() torrent.write(torrent_filepath) torrents.append( SimpleNamespace( torrent=torrent, torrent_path=torrent_filepath, content_path=content_path, )) print('created torrent:\n', torrents[-1].torrent_path, '\n', torrents[-1].torrent.metainfo) return torrents
def test_multifile_with_trackers(multifile_content): t = torf.Torrent(multifile_content.path, trackers=['http://foo', 'http://bar']) t.generate() assert t.magnet() == ( f'magnet:?xt=urn:btih:{t.infohash}&dn={quote_plus(t.name)}&xl={t.size}' '&tr=http%3A%2F%2Ffoo&tr=http%3A%2F%2Fbar')
def test_partial_size__singlefile__providing_path(tmpdir): content_path = tmpdir.join('content.jpg') content_path.write('some data') t = torf.Torrent(content_path) for path in ('bar/foo.jpg', ['bar', 'foo.jpg']): with pytest.raises(torf.PathNotFoundError) as excinfo: t.partial_size(path) assert excinfo.match(f'^bar/foo.jpg: No such file or directory$')
def test_with_empty_file(create_file): # Create content so we can set path content_path = create_file('file.jpg', '<image data>') t = torf.Torrent(content_path) content_path.write_text('') with pytest.raises(torf.PathError) as e: t.generate() assert str(e.value) == f'{t.path}: Empty or all files excluded'
def test_validate_is_called_first(monkeypatch): torrent = torf.Torrent() mock_validate = mock.MagicMock(side_effect=torf.MetainfoError('Mock error')) monkeypatch.setattr(torrent, 'validate', mock_validate) with pytest.raises(torf.MetainfoError) as excinfo: torrent.verify_filesize('some/path') assert excinfo.match(f'^Invalid metainfo: Mock error$') mock_validate.assert_called_once_with()
def test_validate_is_called_first(monkeypatch): torrent = torf.Torrent() mock_validate = mock.Mock(side_effect=torf.MetainfoError('Mock error')) monkeypatch.setattr(torrent, 'validate', mock_validate) with pytest.raises(torf.MetainfoError) as excinfo: torrent.verify('some/path') assert str(excinfo.value) == 'Invalid metainfo: Mock error' mock_validate.assert_called_once_with()
def parse_args(args): cfg = vars(_cliparser.parse_args(args)) # Validate creation date if cfg['date']: try: cfg['date'] = _utils.parse_date(cfg['date'] or 'now') except ValueError: raise _errors.CliError(f'{cfg["date"]}: Invalid date') # Validate max piece size if cfg['max_piece_size']: cfg['max_piece_size'] = cfg['max_piece_size'] * 1048576 if cfg['max_piece_size'] > torf.Torrent.piece_size_max: torf.Torrent.piece_size_max = cfg['max_piece_size'] try: torf.Torrent().piece_size = cfg['max_piece_size'] except torf.PieceSizeError as e: raise _errors.CliError(e) # Validate tracker URLs for tier in cfg['tracker']: for url in tier.split(','): try: torf.Torrent().trackers = url except torf.URLError as e: raise _errors.CliError(e) # Validate webseed URLs for webseed in cfg['webseed']: try: torf.Torrent().webseeds = (webseed, ) except torf.URLError as e: raise _errors.CliError(e) # Validate regular expressions for regex in itertools.chain(cfg['exclude_regex'], cfg['include_regex']): try: re.compile(regex) except re.error as e: raise _errors.CliError(f'Invalid regular expression: {regex}: ' f'{str(e)[0].upper()}{str(e)[1:]}') cfg['validate'] = not cfg['novalidate'] return cfg
def test_with_empty_directory(create_dir): # Create content so we can set path content_path = create_dir('empty', ('a file', '<data>')) t = torf.Torrent(content_path) (content_path / 'a file').unlink() with pytest.raises(torf.ReadError) as e: t.generate() assert str( e.value) == f'{content_path / "a file"}: No such file or directory'
def test_copy_before_ready(singlefile_content): t1 = torf.Torrent(singlefile_content.path, comment='Asdf.', randomize_infohash=True, webseeds=['http://foo']) assert not t1.is_ready t2 = t1.copy() assert t1 == t2 assert t1 is not t2
def test_with_all_files_excluded(create_dir): # Create content so we can set path content_path = create_dir('content', ('a.jpg', '<image data>'), ('b.jpg', '<image data>'), ('c.jpg', '<image data>')) t = torf.Torrent(content_path, exclude_globs=['*.jpg']) with pytest.raises(torf.PathError) as e: t.generate() assert str(e.value) == f'{t.path}: Empty or all files excluded'
def test_nonexisting_path(singlefile_content): content_path = singlefile_content.path + '.deletable' shutil.copyfile(singlefile_content.path, content_path) t = torf.Torrent(content_path) os.remove(content_path) with pytest.raises(torf.PathNotFoundError) as excinfo: t.generate() assert excinfo.match(f'^{content_path}: No such file or directory$')
def test_mode(singlefile_content, multifile_content): torrent = torf.Torrent() assert torrent.mode is None torrent.path = singlefile_content.path assert torrent.mode == 'singlefile' torrent.path = multifile_content.path assert torrent.mode == 'multifile' torrent.path = None assert torrent.mode == None
def _create_torrent_file(tmp_path, **kwargs): torrent_file = tmp_path / 'test.torrent' try: t = torf.Torrent(**kwargs) t.generate() t.write(torrent_file) yield torrent_file finally: if os.path.exists(torrent_file): os.remove(torrent_file)
def _create_torrent(tmpdir, **kwargs): torrent_file = str(tmpdir.join('test.torrent')) try: t = torf.Torrent(**kwargs) t.generate() t.write(torrent_file) yield torrent_file finally: if os.path.exists(torrent_file): os.remove(torrent_file)
def check_metainfo(content, tmpdir): t = torf.Torrent(content.path) t.piece_size = content.exp_metainfo['info']['piece length'] t.generate() t.write(tmpdir.join('torf.torrent'), overwrite=True) assert t.metainfo['info']['piece length'] == content.exp_metainfo['info'][ 'piece length'] assert t.metainfo['info']['pieces'] == content.exp_metainfo['info'][ 'pieces'] assert t.infohash == content.exp_attrs.infohash assert t.infohash_base32 == content.exp_attrs.infohash_base32
def test_with_empty_path(tmpdir): content_path = tmpdir.mkdir('empty') # Create content so we can set path content_file = content_path.join('file.jpg') content_file.write('foo') t = torf.Torrent(content_path) content_file.write('') with pytest.raises(torf.PathEmptyError) as excinfo: t.generate() assert excinfo.match(f'^{str(t.path)}: Empty directory$')
def test_from_torrent(singlefile_content, multifile_content): for content in singlefile_content, multifile_content: t = torf.Torrent(content.path, trackers=['http://foo', 'http://bar'], webseeds=['http://qux', 'http://quux']) t.generate() assert str(t.magnet()) == (f'magnet:?xt=urn:btih:{t.infohash}' f'&dn={quote_plus(t.name)}' f'&xl={t.size}' '&tr=http%3A%2F%2Ffoo&tr=http%3A%2F%2Fbar' '&ws=http%3A%2F%2Fqux&ws=http%3A%2F%2Fquux')
def test_reading_invalid_torrent_data_from_stdin(capsys, tmp_path, monkeypatch, clear_ansi, regex): torrent = torf.Torrent(name='Foo', comment='Bar.') r, w = os.pipe() monkeypatch.setattr(sys, 'stdin', os.fdopen(r)) os.fdopen(w, 'wb').write(torrent.dump(validate=False)) with patch('sys.exit') as mock_exit: run(['-i', '-']) mock_exit.assert_called_once_with(_errors.Code.READ) cap = capsys.readouterr() assert cap.out == '' assert cap.err == f"{_vars.__appname__}: Invalid metainfo: Missing 'piece length' in ['info']\n"
def test_max_torrent_file_size(create_file, existing_torrents, mocker): # Create and prepare existing torrents existing_torrents = existing_torrents( subpath1=( ('a', 'foo', { 'creation_date': 123 }), ('b', 'bar', { 'creation_date': 456 }), ('c', 'baz', { 'creation_date': 789 }), ), subpath2=( ('d', 'hey', { 'private': True }), ('e', 'ho', { 'comment': 'yo' }), ('f', 'oh', { 'comment': 'oy' }), ('g', 'ohh', { 'comment': 'oyy' }), ), ) # Make some torrents really big with open(existing_torrents.torrent_filepaths[1], 'wb') as f: f.truncate(20 * 1048576) with open(existing_torrents.torrent_filepaths[3], 'wb') as f: f.truncate(30 * 1048576) callback = Mock(return_value=None) new_torrent = torf.Torrent(path=create_file('just_a_file', 'foo')) return_value = new_torrent.reuse(existing_torrents.location_paths, callback=callback) assert return_value is False assert callback.call_args_list == [ call(new_torrent, str(existing_torrents.subpath1[0].torrent_path), 1, 5, False, None), call(new_torrent, str(existing_torrents.subpath1[2].torrent_path), 2, 5, False, None), call(new_torrent, str(existing_torrents.subpath2[1].torrent_path), 3, 5, False, None), call(new_torrent, str(existing_torrents.subpath2[2].torrent_path), 4, 5, False, None), call(new_torrent, str(existing_torrents.subpath2[3].torrent_path), 5, 5, False, None), ]
def test_equality(singlefile_content): kwargs = { 'trackers': ['https://localhost/'], 'comment': 'Foo', 'created_by': 'Bar' } t1 = torf.Torrent(singlefile_content.path, **kwargs) t2 = torf.Torrent(singlefile_content.path, **kwargs) assert t1 == t2 t1.metainfo['foo'] = 'bar' assert t1 != t2 del t1.metainfo['foo'] assert t1 == t2 t2.comment = 'asdf' assert t1 != t2 t2.comment = t1.comment assert t1 == t2 t1.trackers += ['https://remotehost'] assert t1 != t2 del t1.trackers[-1] assert t1 == t2