1
0
mirror of https://github.com/gryf/fs-uae-wrapper.git synced 2026-02-02 22:25:47 +01:00

23 Commits
0.7.1 ... 0.8.3

Author SHA1 Message Date
68b1f2a787 Correct homepage in setup.cfg 2022-10-02 14:57:38 +02:00
75d2cff96c Drop Python2 support. 2022-09-02 19:01:56 +02:00
114984cbee Go with setup.cfg instead of setup.py. 2022-09-02 18:47:17 +02:00
b015e443eb Fix tests for file path calculations 2021-05-02 09:36:14 +02:00
874213aef9 Prevent from nonexistent paths in config.
In case of defining paths in config, which wrapper is trying to
normalize, sometimes it may happen, that paths can be either local or
remote - depending on the intention, i.e. one can add:

cdrom_drive_0: foo.iso

where foo can be either an unpacked file or just some companion image,
which exists within config file.

In this patch calculated path is checked against file existence, and if
it doesn't exists, original value is preserved.
2021-03-20 13:12:58 +01:00
1559591417 Changed python3 interpreter to default.
Currently, as a python3 interpreter, specific version has been chosen.
As fs-uae-wrapper doesn't use any special features from latest version
of the python3, it doesn't make sense to have fixed version of it. Just
let operating system pick right python 3 interpreter.
2019-10-06 18:09:32 +02:00
193e487bb9 Fix download link and typo in readme 2017-06-19 19:55:02 +02:00
eb9012f3fa Readme update 2017-01-12 21:09:12 +01:00
efbb5f6f05 Readme update 2017-01-09 06:41:16 +01:00
00e3cf9801 Introduced new configuration variable $WRAPPER
Calculation of file path may be performed in two different places -
first is the original location of the config file, second is on copied
one. Adding new option WRAPPER will take the same role as $CONFIG,
except it will be only parsed on copied configuration file.

An alternative would be to check existence in both places, but I'd like
to have it explicit.
2017-01-08 19:40:27 +01:00
388a8cc835 Fix for interpolate_variables function 2017-01-08 15:38:59 +01:00
83185011aa Fix for config options containing HOME variable 2017-01-08 15:37:29 +01:00
152446abbe Use logging for displaying messages
Also added debug logs
2017-01-08 13:58:47 +01:00
a918e4c9ff Added logging module 2017-01-08 13:17:41 +01:00
7931022777 Additional tests for util module 2017-01-08 12:35:51 +01:00
1a67b355ae Removed unused run function from savestate wrapper 2017-01-08 12:01:25 +01:00
6a026ecff1 Fix for typo in attribute name 2017-01-08 11:59:43 +01:00
bd0e3ea56a Added more tests for file_archive module 2017-01-08 11:56:40 +01:00
8a6ddda7c8 Make plain wrapper module use base.Base class 2017-01-08 11:21:43 +01:00
a14871c52f Get rid of run function for every wrapper module 2017-01-08 11:20:33 +01:00
8528d28a42 Get rid of unneeded _run_emulator parameter 2017-01-08 11:17:37 +01:00
ab4042f880 Refactor wrapper modules to have a main class with the same name 2017-01-07 13:13:18 +01:00
8892940339 Normalize paths for config options
Removing _kickstart_option in favor of _normalize_options method to
detect and replace relative paths with absolute ones. Dictionary with
changed options lands as a commandline switches for fs-uae during
execution.
2017-01-07 13:13:06 +01:00
22 changed files with 556 additions and 357 deletions

View File

@@ -2,7 +2,10 @@ language: python
env:
- TOXENV=py27
- TOXENV=py27-flake8
- TOXENV=py34
- TOXENV=py34-flake8
- TOXENV=py3
- TOXENV=py3-flake8
install: pip install tox
script: tox
before_install:
- sudo apt-get update
- sudo apt-get install -y python-tk python3-tk

View File

@@ -12,7 +12,7 @@ This little utility is a wrapper on great FS-UAE_ emulator, to perform some
actions, like uncompressing archived files (CD ROMs images, filesystems),
launch the emulator and archive emulator save state.
As an example, if there is a collection of CD³² game files and one want to
As an example, if there is a collection of CD³² game files and you want to
provide configuration for each game, but want to keep ISO images with
corresponding files in archive (due to size of those images) than FS-UAE
Wrapper is a way to achieve this.
@@ -25,11 +25,10 @@ preferably in a archive file vs a bunch of files.
Requirements
============
- Python (2 or 3)
- Python 3
- `fs-uae`_ (obviously :)
Also, there should be archivers programs available for compress/decompress
archives. Fs-uae-wrapper supports several types of archives:
Fs-uae-wrapper supports several types of archives:
- `7z`_
- `lha`_
@@ -43,25 +42,35 @@ archives. Fs-uae-wrapper supports several types of archives:
- `zip`_
There are several archive types which are supported, ranging from tar
(compressed with gzip, bzip2 and xz), 7z, rar, zip. lha and lzx. All of those
formats should have corresponding decompressors available in the system,
otherwise archive extraction will fail.
All of those formats should have corresponding software available in the
system, otherwise archive extraction/compression will fail.
Installation
============
Just perform (preferably in ``virtualenv``) a command:
FS-UAE Wrapper is available on `CheeseShop`_ (or python package index if you
will). To install it, you can simply invoke (preferably in ``virtualenv``) a
command:
.. code:: shell-session
$ pip install fs-uae-wrapper
Note, that if ``virtualenv`` was used, there is no need for activating it every
time, since if invoke wrapper like this:
.. code:: shell-session
$ /path/to/virtualenv/bin/fs-uae-wrapper
you should be able run the wrapper properly. *Tested only on Linux :)*.
Usage
=====
After installation you should be able to access new command
``fs-uae-wrapper``, and its invocation is identical like ``fs-uae`` binary:
After installation you should be able to access new command ``fs-uae-wrapper``
(or use the full path to the ``virtualenv`` like on the section above), and it's
invocation is identical like ``fs-uae`` binary:
.. code:: shell-session
@@ -73,9 +82,9 @@ directly in fs-uae configuration or passed as an option:
.. code:: ini
[config]
...
# ...
wrapper = cd32
...
# ...
or
@@ -89,19 +98,40 @@ specific module will be loaded and depending on the module, there would be
performed specific tasks before ``fs-uae`` is launched and after it.
Assumption is, that configuration file are written in portable way, so the are
saved as `relative configuration file`_ (hence the name ``Config.fs-uae``, even
if the are named differently. If certain wrapper is specified, it will create
temporary directory and place the configuration file there as
saved as `relative configuration file`_ (hence the name ``Config.fs-uae``),
even if the are named differently. If certain wrapper is specified, it will
create temporary directory and place the configuration file there as
``Config.fs-uae``.
If no ``wrapper`` option would be passed either as an config option or
command line argument, all command line options will be passed to the fs-uae
executable as-is.
Note, that you can also pass all *wrapper* options via commandline in the very
same way as you can pass config options to `fs-uae`, so you don't have to
modify original configuration if you don't want to.
There is also new config variable introduced: ``$WRAPPER`` which have the same
role as ``$CONFIG``, but apply for copied config. For instance - in module
archive there are filesystem extracted to new location - to access this
filesystem relatively to the copied configuration file it is enough to provide
a config option:
.. code:: ini
[config]
wrapper = archive
# ...
hard_drive_0 = $WRAPPER/my_hardrive
which means, that we are expecting to have system files on ``my_hardrive`` in
directory, where configuration will be copied.
Modules
=======
Currently, three wrapper modules are available:
Currently, couple of wrapper modules are available:
- plain
- cd32
@@ -132,7 +162,7 @@ Options used:
* ``wrapper_gui_msg`` (optional) if set to "1", will display a graphical
message during extracting files
* ``wrapper_save_state`` (optional) if set to "1", will load/archive save state
directory, defined as ``$CONFIG/[save-state-dir-name]`` using provided
directory, defined as ``$WRAPPER/[save-state-dir-name]`` using provided
``wrapper_archiver`` archiver. If this option is enabled,
``wrapper_archiver`` will be required.
@@ -153,11 +183,11 @@ fragment of configuration file is saved as ``ChaosEngine.fs-uae``:
cdrom_drive_0 = Chaos Engine, The (1994)(Renegade)(M4)[!][CDD3445].cue
save_states_dir = $CONFIG/fs-uae-save/
save_states_dir = $WRAPPER/fs-uae-save/
joystick_port_1_mode = cd32 gamepad
platform = cd32
...
# ...
Command line invocation of the wrapper would be as follows:
@@ -169,7 +199,7 @@ Now, there several thing will happen:
- Config file will be read, and wrapper module will be found
- New temporary directory will be created
- Archive with game assists will be extracted in that directory
- Archive with game assets will be extracted in that directory
- Configuration file will be copied into that directory, and renamed to
``Config.fs-uae``
- If ``wrapper_save_state`` is set, and there is saved state archive, it also
@@ -198,7 +228,7 @@ Options used:
* ``wrapper_persist_data`` (optional) if set to "1", will compress (possibly
changed) data, replacing original archive
* ``wrapper_save_state`` (optional) if set to "1", will archive save state
directory, defined as ``$CONFIG/[save-state-dir-name]`` using provided
directory, defined as ``$WRAPPER/[save-state-dir-name]`` using provided
``wrapper_archiver`` archiver. If this option is enabled,
``wrapper_archiver`` will be required.
@@ -220,7 +250,7 @@ Example configuration:
wrapper_gui_msg = 1
wrapper_persist_data = 1
wrapper_save_state = 1
...
# ...
And execution is as usual:
@@ -259,12 +289,11 @@ set to value ``1`` in this module.
Example configuration:
.. code:: ini
:number-lines:
[config]
wrapper = savestate
wrapper_archiver = 7z
...
# ...
And execution is as usual:
@@ -295,3 +324,4 @@ This work is licensed on 3-clause BSD license. See LICENSE file for details.
.. _lzx: http://aminet.net/package/misc/unix/unlzx.c.readme
.. _tar: https://www.gnu.org/software/tar/
.. _zip: http://www.info-zip.org
.. _CheeseShop: https://pypi.python.org/pypi/fs-/fs-uae-wrapperuae-wrapper

View File

@@ -11,26 +11,25 @@ from fs_uae_wrapper import base
from fs_uae_wrapper import utils
class Archive(base.ArchiveBase):
class Wrapper(base.ArchiveBase):
"""
Class for performing extracting archive, copying emulator files, and
cleaning it back again
"""
def __init__(self, conf_file, fsuae_options, configuration):
super(Archive, self).__init__(conf_file, fsuae_options, configuration)
super(Wrapper, self).__init__(conf_file, fsuae_options, configuration)
self.archive_type = None
def run(self):
"""
Main function which accepts configuration file for FS-UAE
It will do as follows:
- set needed full path for asset files
- extract archive file
- copy configuration
- run the emulation
- optionally make archive save state
"""
if not super(Archive, self).run():
if not super(Wrapper, self).run():
return False
if not self._extract():
@@ -41,11 +40,7 @@ class Archive(base.ArchiveBase):
if not self._copy_conf():
return False
kick_opts = self._kickstart_option()
if kick_opts:
self.fsuae_options.update(kick_opts)
if not self._run_emulator(self.fsuae_options.list()):
if not self._run_emulator():
return False
if self._get_saves_dir():
@@ -78,13 +73,3 @@ class Archive(base.ArchiveBase):
shutil.move(arch, self.arch_filepath)
os.chdir(curdir)
return True
def run(config_file, fsuae_options, configuration):
"""Run fs-uae with provided config file and options"""
runner = Archive(config_file, fsuae_options, configuration)
try:
return runner.run()
finally:
runner.clean()

View File

@@ -1,9 +1,10 @@
"""
Base class for all wrapper modules
"""
import logging
import os
import sys
import shutil
import sys
import tempfile
from fs_uae_wrapper import utils
@@ -45,6 +46,7 @@ class Base(object):
return False
self.dir = tempfile.mkdtemp()
self._normalize_options()
self._set_assets_paths()
return True
@@ -55,33 +57,6 @@ class Base(object):
shutil.rmtree(self.dir)
return
def _kickstart_option(self):
"""
This is kind of hack - since we potentially can have a relative path
to kickstart directory, there is a need for getting this option from
configuration files (which unfortunately can be spanned all over the
different places, see https://fs-uae.net/configuration-files) and
check whether or not one of 'kickstarts_dir', 'kickstart_file' or
'kickstart_ext_file' options are set. In either case if one of those
options are set and are relative, they should be set to absolute path,
so that kickstart files can be found by relocated configuration file.
"""
conf = utils.get_config(self.conf_file)
kick = {}
for key in ('kickstart_file', 'kickstart_ext_file', 'kickstarts_dir'):
val = conf.get(key)
if val:
if not os.path.isabs(val):
val = utils.interpolate_variables(val, self.conf_file)
kick[key] = os.path.abspath(val)
else:
kick[key] = val
return kick
def _set_assets_paths(self):
"""
Set full paths for archive file (without extension) and for save state
@@ -104,11 +79,11 @@ class Base(object):
os.path.join(self.dir, 'Config.fs-uae'))
return True
def _run_emulator(self, fsuae_options):
"""execute fs-uae in provided directory"""
def _run_emulator(self):
"""execute fs-uae"""
curdir = os.path.abspath('.')
os.chdir(self.dir)
utils.run_command(['fs-uae'] + fsuae_options)
utils.run_command(['fs-uae'] + self.fsuae_options.list())
os.chdir(curdir)
return True
@@ -143,7 +118,7 @@ class Base(object):
curdir = os.path.abspath('.')
if not utils.create_archive(self.save_filename, '', [save_path]):
sys.stderr.write('Error: archiving save state failed\n')
logging.error('Error: archiving save state failed.')
os.chdir(curdir)
return False
@@ -169,7 +144,7 @@ class Base(object):
def _get_saves_dir(self):
"""
Return path to save state directory or None in cases:
- there is no save state dir set relative to config file
- there is no save state dir set relative to copied config file
- save state dir is set globally
- save state dir is set relative to the config file
- save state dir doesn't exists
@@ -178,9 +153,9 @@ class Base(object):
if not self.all_options.get('save_states_dir'):
return None
if self.all_options['save_states_dir'].startswith('$CONFIG') and \
if self.all_options['save_states_dir'].startswith('$WRAPPER') and \
'..' not in self.all_options['save_states_dir']:
save = self.all_options['save_states_dir'].replace('$CONFIG/', '')
save = self.all_options['save_states_dir'].replace('$WRAPPER/', '')
else:
return None
@@ -193,24 +168,82 @@ class Base(object):
return save
def _normalize_options(self):
"""
Search and replace values for options which starts with $CONFIG with
absolute path for all options.
Configuration file will be placed in new directory, therefore it is
needed to calculate new paths so that emulator can find assets.
"""
options = ['wrapper_archive', 'accelerator_rom', 'base_dir',
'cdrom_drive_0', 'cdroms_dir', 'controllers_dir',
'cpuboard_flash_ext_file', 'cpuboard_flash_file',
'floppies_dir', 'floppy_overlays_dir', 'fmv_rom',
'graphics_card_rom', 'hard_drives_dir', 'kickstart_file',
'kickstarts_dir', 'logs_dir', 'save_states_dir',
'screenshots_output_dir']
for num in range(20):
options.append('cdrom_image_%d' % num)
options.append('floppy_image_%d' % num)
for num in range(4):
options.append('floppy_drive_%d' % num)
for num in range(10):
options.append('hard_drive_%d' % num)
changed_options = {}
for key, val in utils.get_config(self.conf_file).items():
if key not in options:
continue
if val.startswith('/'):
continue
if val.startswith('~'):
continue
if val.startswith('$HOME'):
continue
if val.startswith('$WRAPPER'):
changed_options[key] = val.replace('$WRAPPER', self.dir)
continue
if val.startswith('$CONFIG'):
abspath = utils.interpolate_variables(val, self.conf_file)
changed_options[key] = abspath
continue
_val = os.path.abspath(val)
if os.path.exists(_val):
changed_options[key] = _val
else:
changed_options[key] = val
self.fsuae_options.update(changed_options)
def _validate_options(self):
"""Validate mandatory options"""
if 'wrapper' not in self.all_options:
sys.stderr.write("Configuration lacks of required "
"`wrapper' option.\n")
logging.error("Configuration lacks of required `wrapper' option.")
return False
if self.all_options.get('wrapper_save_state', '0') == '0':
return True
if 'wrapper_archiver' not in self.all_options:
sys.stderr.write("Configuration lacks of required "
"`wrapper_archiver' option.\n")
logging.error("Configuration lacks of required "
"`wrapper_archiver' option.")
return False
if not path.which(self.all_options['wrapper_archiver']):
sys.stderr.write("Cannot find archiver `%s'." %
self.all_options['wrapper_archiver'])
logging.error("Cannot find archiver `%s'.",
self.all_options['wrapper_archiver'])
return False
return True

View File

@@ -11,7 +11,7 @@ name.
from fs_uae_wrapper import base
class CD32(base.ArchiveBase):
class Wrapper(base.ArchiveBase):
"""
Class for performing extracting archive, copying emulator files, and
cleaning it back again
@@ -28,7 +28,7 @@ class CD32(base.ArchiveBase):
- run the emulation
- archive save state
"""
super(CD32, self).run()
super(Wrapper, self).run()
if not self._validate_options():
return False
@@ -39,24 +39,10 @@ class CD32(base.ArchiveBase):
if not method():
return False
kick_opts = self._kickstart_option()
if kick_opts:
self.fsuae_options.update(kick_opts)
if not self._run_emulator(self.fsuae_options.list()):
if not self._run_emulator():
return False
if self._get_saves_dir():
return self._save_save()
return True
def run(config_file, fsuae_options, configuration):
"""Run fs-uae with provided config file and options"""
runner = CD32(config_file, fsuae_options, configuration)
try:
return runner.run()
finally:
runner.clean()

View File

@@ -3,8 +3,8 @@ File archive classes
"""
import os
import subprocess
import sys
import re
import logging
from fs_uae_wrapper import path
@@ -17,18 +17,20 @@ class Archive(object):
def __init__(self):
self.archiver = path.which(self.ARCH)
self._compess = self.archiver
self._decompess = self.archiver
self._compress = self.archiver
self._decompress = self.archiver
def create(self, arch_name, files=None):
"""
Create archive. Return True on success, False otherwise.
"""
files = files if files else ['.']
result = subprocess.call([self._compess] + self.ADD + [arch_name]
logging.debug("Calling `%s %s %s %s'.", self._compress,
" ".join(self.ADD), arch_name, " ".join(files))
result = subprocess.call([self._compress] + self.ADD + [arch_name]
+ files)
if result != 0:
sys.stderr.write("Unable to create archive `%s'\n" % arch_name)
logging.error("Unable to create archive `%s'.", arch_name)
return False
return True
@@ -37,13 +39,15 @@ class Archive(object):
Extract archive. Return True on success, False otherwise.
"""
if not os.path.exists(arch_name):
sys.stderr.write("Archive `%s' doesn't exists.\n" % arch_name)
logging.error("Archive `%s' doesn't exists.", arch_name)
return False
result = subprocess.call([self._decompess] + self.EXTRACT +
logging.debug("Calling `%s %s %s'.", self._compress,
" ".join(self.ADD), arch_name)
result = subprocess.call([self._decompress] + self.EXTRACT +
[arch_name])
if result != 0:
sys.stderr.write("Unable to extract archive `%s'\n" % arch_name)
logging.error("Unable to extract archive `%s'.", arch_name)
return False
return True
@@ -55,10 +59,12 @@ class TarArchive(Archive):
def create(self, arch_name, files=None):
files = files if files else sorted(os.listdir('.'))
result = subprocess.call([self._compess] + self.ADD + [arch_name] +
logging.debug("Calling `%s %s %s %s'.", self._compress,
" ".join(self.ADD), arch_name, " ".join(files))
result = subprocess.call([self._compress] + self.ADD + [arch_name] +
files)
if result != 0:
sys.stderr.write("Unable to create archive `%s'\n" % arch_name)
logging.error("Unable to create archive `%s'.", arch_name)
return False
return True
@@ -86,7 +92,7 @@ class ZipArchive(Archive):
def __init__(self):
super(ZipArchive, self).__init__()
if self.archiver == 'zip':
self._decompess = path.which('unzip')
self._decompress = path.which('unzip')
ZipArchive.ADD = ['-r']
ZipArchive.EXTRACT = []
@@ -101,8 +107,8 @@ class LzxArchive(Archive):
@classmethod
def create(self, arch_name, files=None):
sys.stderr.write('Cannot create LZX archive. Only extracting is'
'supported\n')
logging.error('Cannot create LZX archive. Only extracting is'
'supported.')
return False
@@ -112,14 +118,16 @@ class RarArchive(Archive):
def create(self, arch_name, files=None):
files = files if files else sorted(os.listdir('.'))
if self.archiver == 'unrar':
sys.stderr.write('Cannot create RAR archive. Only extracting is'
'supported by unrar.\n')
logging.error('Cannot create RAR archive. Only extracting is'
'supported by unrar.')
return False
result = subprocess.call([self._compess] + self.ADD + [arch_name] +
logging.debug("Calling `%s %s %s %s'.", self._compress,
" ".join(self.ADD), arch_name, " ".join(files))
result = subprocess.call([self._compress] + self.ADD + [arch_name] +
files)
if result != 0:
sys.stderr.write("Unable to create archive `%s'\n" % arch_name)
logging.error("Unable to create archive `%s'.", arch_name)
return False
return True
@@ -174,13 +182,13 @@ def get_archiver(arch_name):
archiver = Archivers.get(ext)
if not archiver:
sys.stderr.write("Unable find archive type for `%s'\n" % arch_name)
logging.error("Unable find archive type for `%s'.", arch_name)
return None
archobj = archiver()
if archobj.archiver is None:
sys.stderr.write("Unable find executable for operating on files"
" `*%s'\n" % ext)
logging.error("Unable find executable for operating on files `*%s'.",
ext)
return None
return archobj

View File

@@ -1,27 +1,25 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Simple class for executing fs-uae with specified parameters. This is a
failsafe class for running fs-uae.
"""
import subprocess
import sys
from fs_uae_wrapper import base
from fs_uae_wrapper import utils
def run_plain(conf_file, fs_uae_options):
"""
Run the emulation.
conf_file is a path to the configuration,
fs_uae_options is an dict-like object which contains commandline options to
be passed to fs-uae
"""
try:
subprocess.call(['fs-uae'] + [conf_file] + fs_uae_options.list())
except subprocess.CalledProcessError:
sys.stderr.write('Warning: fs-uae returned non 0 exit code\n')
return True
class Wrapper(base.Base):
"""Simple class for running fs-uae"""
def run(self):
"""
Main function which run FS-UAE
"""
self._run_emulator()
def run(config_file, fs_uae_options, _):
"""Run fs-uae with provided config file and options"""
return run_plain(config_file, fs_uae_options)
def _run_emulator(self):
"""execute fs-uae"""
utils.run_command(['fs-uae'] + [self.conf_file] +
self.fsuae_options.list())
def clean(self):
"""Do the cleanup. Here - just do nothing"""
return

View File

@@ -4,7 +4,7 @@ Wrapper module with preserved save state in archive file.
from fs_uae_wrapper import base
class SaveState(base.Base):
class Wrapper(base.Base):
"""
Preserve save state.
"""
@@ -16,8 +16,7 @@ class SaveState(base.Base):
parameters
configuration: is config dictionary created out of config file
"""
super(SaveState, self).__init__(conf_file, fsuae_options,
configuration)
super(Wrapper, self).__init__(conf_file, fsuae_options, configuration)
self.arch_filepath = None
self.all_options['wrapper_save_state'] = '1'
@@ -31,7 +30,7 @@ class SaveState(base.Base):
- run the emulation
- optionally make archive save state
"""
if not super(SaveState, self).run():
if not super(Wrapper, self).run():
return False
self._load_save()
@@ -39,11 +38,7 @@ class SaveState(base.Base):
if not self._copy_conf():
return False
kick_opts = self._kickstart_option()
if kick_opts:
self.fsuae_options.update(kick_opts)
if not self._run_emulator(self.fsuae_options.list()):
if not self._run_emulator():
return False
if self._get_saves_dir():
@@ -51,13 +46,3 @@ class SaveState(base.Base):
return False
return True
def run(config_file, fsuae_options, configuration):
"""Run fs-uae with provided config file and options"""
runner = SaveState(config_file, fsuae_options, configuration)
try:
return runner.run()
finally:
runner.clean()

View File

@@ -1,14 +1,11 @@
"""
Misc utilities
"""
from distutils import spawn
import configparser
import logging
import os
import shutil
import subprocess
import sys
try:
import configparser
except ImportError:
import ConfigParser as configparser
from fs_uae_wrapper import message
from fs_uae_wrapper import file_archive
@@ -46,7 +43,7 @@ class CmdOption(dict):
def get_config_options(conf):
"""Read config file and return options as a dict"""
parser = configparser.SafeConfigParser()
parser = configparser.ConfigParser()
try:
parser.read(conf)
except configparser.ParsingError:
@@ -115,10 +112,10 @@ def run_command(cmd):
if not isinstance(cmd, list):
cmd = cmd.split()
logging.debug("Executing `%s'.", " ".join(cmd))
code = subprocess.call(cmd)
if code != 0:
sys.stderr.write('Command `{0}` returned non 0 exit '
'code\n'.format(cmd[0]))
logging.error('Command `%s` returned non 0 exit code.', cmd[0])
return False
return True
@@ -144,17 +141,19 @@ def interpolate_variables(string, config_path, base=None):
- $BASE
"""
_string = string
if '$CONFIG' in string:
string = string.replace('$CONFIG',
os.path.dirname(os.path.abspath(config_path)))
conf_path = os.path.dirname(os.path.abspath(config_path))
string = os.path.abspath(string.replace('$CONFIG', conf_path))
if '$HOME' in string:
string = string.replace('$HOME', os.path.expandvars('$HOME'))
if '$EXE' in string:
string = string.replace('$EXE', spawn.find_executable('fs-uae'))
string = string.replace('$EXE', shutil.which('fs-uae'))
if '$APP' in string:
string = string.replace('$APP', spawn.find_executable('fs-uae'))
string = string.replace('$APP', shutil.which('fs-uae'))
if '$DOCUMENTS' in string:
xdg_docs = os.getenv('XDG_DOCUMENTS_DIR',
@@ -165,7 +164,9 @@ def interpolate_variables(string, config_path, base=None):
if '$BASE' in string:
string = string.replace('$BASE', base)
return string
if os.path.exists(string):
return string
return _string
def get_config(conf_file):

View File

@@ -1,9 +1,9 @@
#!/usr/bin/env python
"""
Wrapper for FS-UAE to perform some actions before and or after running the
emulator, if appropriate option is enabled.
"""
import importlib
import logging
import os
import sys
@@ -11,6 +11,25 @@ from fs_uae_wrapper import utils
from fs_uae_wrapper import WRAPPER_KEY
def setup_logger(options):
"""Setup logger format and level"""
level = logging.WARNING
if options['wrapper_quiet']:
level = logging.ERROR
if options['wrapper_quiet'] > 1:
level = logging.CRITICAL
if options['wrapper_verbose']:
level = logging.INFO
if options['wrapper_verbose'] > 1:
level = logging.DEBUG
logging.basicConfig(level=level,
format="%(asctime)s %(levelname)s: %(message)s")
def parse_args():
"""
Look out for config file and for config options which would be blindly
@@ -18,7 +37,16 @@ def parse_args():
"""
fs_conf = None
options = utils.CmdOption()
options['wrapper_verbose'] = 0
options['wrapper_quiet'] = 0
for parameter in sys.argv[1:]:
if parameter in ['-v', '-q']:
if parameter == '-v':
options['wrapper_verbose'] += 1
if parameter == '-q':
options['wrapper_quiet'] += 1
continue
try:
options.add(parameter)
except AttributeError:
@@ -33,7 +61,7 @@ def parse_args():
def usage():
"""Print help"""
sys.stdout.write("Usage: %s [conf-file] [fs-uae-option...]\n\n"
sys.stdout.write("Usage: %s [conf-file] [-v] [-q] [fs-uae-option...]\n\n"
% sys.argv[0])
sys.stdout.write("Config file is not required, if `Config.fs-uae' "
"exists in the current\ndirectory, although it might "
@@ -46,24 +74,27 @@ def usage():
def run():
"""run wrapper module"""
config_file, cmd_options = parse_args()
config_file, fsuae_options = parse_args()
setup_logger(fsuae_options)
del fsuae_options['wrapper_verbose']
del fsuae_options['wrapper_quiet']
if 'help' in cmd_options:
if 'help' in fsuae_options:
usage()
sys.exit(0)
if not config_file:
sys.stderr.write('Error: Configuration file not found\nSee --help'
' for usage\n')
logging.error('Error: Configuration file not found. See --help'
' for usage')
sys.exit(1)
configuration = utils.get_config_options(config_file)
if configuration is None:
sys.stderr.write('Error: Configuration file have syntax issues\n')
logging.error('Error: Configuration file have syntax issues')
sys.exit(2)
wrapper_module = cmd_options.get(WRAPPER_KEY)
wrapper_module = fsuae_options.get(WRAPPER_KEY)
if not wrapper_module:
wrapper_module = configuration.get(WRAPPER_KEY)
@@ -74,13 +105,16 @@ def run():
wrapper = importlib.import_module('fs_uae_wrapper.' +
wrapper_module)
except ImportError:
sys.stderr.write("Error: provided wrapper module: `%s' doesn't "
"exists.\n" % wrapper_module)
logging.error("Error: provided wrapper module: `%s' doesn't "
"exists.", wrapper_module)
sys.exit(3)
if not wrapper.run(config_file, cmd_options, configuration):
runner = wrapper.Wrapper(config_file, fsuae_options, configuration)
try:
exit_code = runner.run()
finally:
runner.clean()
if not exit_code:
sys.exit(4)
if __name__ == "__main__":
run()

View File

@@ -1,16 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Run CD32 games using fsuae
"""
from fs_uae_wrapper import wrapper
def main():
"""run wrapper"""
wrapper.run()
if __name__ == "__main__":
main()

37
setup.cfg Normal file
View File

@@ -0,0 +1,37 @@
[metadata]
name = fs-uae-wrapper
summary = Automate archives and state for fs-uae
description = Automate archives and state for fs-uae
long_description = file: README.rst
author = Roman Dobosz
author_email = gryf73@gmail.com
url = https://github.com/gryf/fs-uae-wrapper
license = BSD
keywords = uae, fs-uae, amiga, emulator, wrapper
version = 0.8.3
classifier =
Development Status :: 5 - Production/Stable
Environment :: Console
Intended Audience :: End Users/Desktop
License :: OSI Approved :: BSD License
Operating System :: OS Independent
Programming Language :: Python :: 3
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Topic :: System :: Emulators
Topic :: Games/Entertainment
[install]
record = install.log
[options.entry_points]
console_scripts =
fs-uae-wrapper = fs_uae_wrapper.wrapper:run
[files]
packages =
fs_uae_wrapper
[bdist_wheel]
universal = 1

View File

@@ -1,31 +1,5 @@
#!/usr/bin/env python
"""
Setup for the fs-uae-wrapper
"""
from setuptools import setup
import setuptools
setup(name='fs-uae-wrapper',
packages=['fs_uae_wrapper'],
version='0.7.1',
description='Automate archives and state for fs-uae',
author='Roman Dobosz',
author_email='gryf73@gmail.com',
url='https://github.com/gryf/fs-uea-wrapper',
download_url='https://github.com/gryf/pygtktalog.git',
keywords=['uae', 'fs-uae', 'amiga', 'emulator', 'wrapper'],
scripts=['script/fs-uae-wrapper'],
classifiers=['Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.4',
'Development Status :: 4 - Beta',
'Environment :: Console',
'Intended Audience :: End Users/Desktop',
'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent',
'Topic :: System :: Emulators',
'Topic :: Games/Entertainment'],
long_description=open('README.rst').read(),
options={'test': {'verbose': False,
'coverage': False}})
setuptools.setup()

View File

@@ -30,7 +30,7 @@ class TestArchive(TestCase):
def test_validate_options(self, which):
which.return_value = 'unrar'
arch = archive.Archive('Config.fs-uae', utils.CmdOption(), {})
arch = archive.Wrapper('Config.fs-uae', utils.CmdOption(), {})
self.assertFalse(arch._validate_options())
arch.all_options = {'wrapper': 'archive'}
@@ -42,29 +42,26 @@ class TestArchive(TestCase):
@mock.patch('tempfile.mkdtemp')
@mock.patch('fs_uae_wrapper.path.which')
@mock.patch('fs_uae_wrapper.archive.Archive._make_archive')
@mock.patch('fs_uae_wrapper.archive.Wrapper._make_archive')
@mock.patch('fs_uae_wrapper.base.ArchiveBase._save_save')
@mock.patch('fs_uae_wrapper.base.ArchiveBase._get_saves_dir')
@mock.patch('fs_uae_wrapper.base.ArchiveBase._run_emulator')
@mock.patch('fs_uae_wrapper.base.ArchiveBase._kickstart_option')
@mock.patch('fs_uae_wrapper.base.ArchiveBase._copy_conf')
@mock.patch('fs_uae_wrapper.base.ArchiveBase._load_save')
@mock.patch('fs_uae_wrapper.base.ArchiveBase._extract')
def test_run(self, extract, load_save, copy_conf, kick_option,
run_emulator, get_save_dir, save_state, make_arch, which,
mkdtemp):
def test_run(self, extract, load_save, copy_conf, run_emulator,
get_save_dir, save_state, make_arch, which, mkdtemp):
extract.return_value = False
load_save.return_value = False
copy_conf.return_value = False
kick_option.return_value = False
run_emulator.return_value = False
get_save_dir.return_value = False
save_state.return_value = False
make_arch.return_value = False
which.return_value = 'rar'
arch = archive.Archive('Config.fs-uae', utils.CmdOption(), {})
arch = archive.Wrapper('Config.fs-uae', utils.CmdOption(), {})
self.assertFalse(arch.run())
arch.all_options = {'wrapper': 'archive',
@@ -82,10 +79,6 @@ class TestArchive(TestCase):
copy_conf.return_value = True
self.assertFalse(arch.run())
kick_option.return_value = {'foo': 'bar'}
self.assertFalse(arch.run())
self.assertDictEqual(arch.fsuae_options, {'foo': 'bar'})
run_emulator.return_value = True
self.assertFalse(arch.run())
@@ -110,7 +103,7 @@ class TestArchive(TestCase):
title.return_value = ''
carch.return_value = False
arch = archive.Archive('Config.fs-uae', utils.CmdOption(), {})
arch = archive.Wrapper('Config.fs-uae', utils.CmdOption(), {})
arch.dir = self.dirname
arch.arch_filepath = os.path.join(self.dirname, 'foo.tgz')
arch.all_options = {}

View File

@@ -47,28 +47,74 @@ class TestBase(TestCase):
bobj.clean()
self.assertFalse(os.path.exists(self.dirname))
@mock.patch('os.path.exists')
@mock.patch('fs_uae_wrapper.utils.get_config')
def test_kickstart_option(self, get_config):
def test_normalize_options(self, get_config, os_exists):
os_exists.return_value = True
bobj = base.Base('Config.fs-uae', utils.CmdOption(), {})
get_config.return_value = {'foo': 'bar'}
self.assertDictEqual(bobj._kickstart_option(), {})
get_config.return_value = {'kickstarts_dir': '/some/path'}
self.assertDictEqual(bobj._kickstart_option(),
{'kickstarts_dir': '/some/path'})
bobj._normalize_options()
self.assertDictEqual(bobj.fsuae_options, {})
os.chdir(self.dirname)
get_config.return_value = {'kickstarts_dir': '../some/path'}
get_config.return_value = {'fmv_rom': 'bar'}
bobj._normalize_options()
self.assertDictEqual(bobj.fsuae_options,
{'fmv_rom': os.path.join(self.dirname, 'bar')})
get_config.return_value = {'floppies_dir': '../some/path'}
bobj.fsuae_options = utils.CmdOption()
result = os.path.abspath(os.path.join(self.dirname, '../some/path'))
self.assertDictEqual(bobj._kickstart_option(),
{'kickstarts_dir': result})
bobj._normalize_options()
self.assertDictEqual(bobj.fsuae_options, {'floppies_dir': result})
bobj.conf_file = os.path.join(self.dirname, 'Config.fs-uae')
get_config.return_value = {'kickstarts_dir': '$CONFIG/../path'}
get_config.return_value = {'cdroms_dir': '$CONFIG/../path'}
bobj.fsuae_options = utils.CmdOption()
result = os.path.abspath(os.path.join(self.dirname, '../path'))
self.assertDictEqual(bobj._kickstart_option(),
{'kickstarts_dir': result})
bobj._normalize_options()
self.assertDictEqual(bobj.fsuae_options, {'cdroms_dir': result})
get_config.return_value = {'cdroms_dir': '$HOME/path'}
bobj.fsuae_options = utils.CmdOption()
bobj._normalize_options()
self.assertDictEqual(bobj.fsuae_options, {})
get_config.return_value = {'cdroms_dir': '$WRAPPER/path'}
bobj.fsuae_options = utils.CmdOption()
bobj.dir = self.dirname
bobj._normalize_options()
self.assertDictEqual(bobj.fsuae_options,
{'cdroms_dir': os.path.join(bobj.dir, 'path')})
get_config.return_value = {'cdroms_dir': '~/path'}
bobj.fsuae_options = utils.CmdOption()
bobj._normalize_options()
self.assertDictEqual(bobj.fsuae_options, {})
@mock.patch('os.path.exists')
@mock.patch('fs_uae_wrapper.utils.get_config')
def test_normalize_options_path_not_exists(self, get_config, os_exists):
os_exists.return_value = False
bobj = base.Base('Config.fs-uae', utils.CmdOption(), {})
get_config.return_value = {'kickstarts_dir': '/some/path'}
bobj._normalize_options()
self.assertDictEqual(bobj.fsuae_options, {})
os.chdir(self.dirname)
get_config.return_value = {'fmv_rom': 'bar'}
bobj._normalize_options()
self.assertDictEqual(bobj.fsuae_options, {'fmv_rom': 'bar'})
get_config.return_value = {'floppies_dir': '../some/path'}
bobj.fsuae_options = utils.CmdOption()
bobj._normalize_options()
self.assertDictEqual(bobj.fsuae_options,
{'floppies_dir': '../some/path'})
def test_set_assets_paths(self):
@@ -105,15 +151,22 @@ class TestBase(TestCase):
bobj = base.Base('Config.fs-uae', utils.CmdOption(), {})
bobj.dir = self.dirname
self.assertTrue(bobj._run_emulator([]))
self.assertTrue(bobj._run_emulator())
run.assert_called_once_with(['fs-uae'])
# Errors from emulator are not fatal to wrappers
run.reset_mock()
run.return_value = False
self.assertTrue(bobj._run_emulator([]))
self.assertTrue(bobj._run_emulator())
run.assert_called_once_with(['fs-uae'])
# pass the options
bobj.fsuae_options = utils.CmdOption({'foo': '1'})
run.reset_mock()
run.return_value = False
self.assertTrue(bobj._run_emulator())
run.assert_called_once_with(['fs-uae', '--foo'])
@mock.patch('fs_uae_wrapper.base.Base._get_saves_dir')
@mock.patch('fs_uae_wrapper.utils.create_archive')
def test_save_save(self, carch, saves_dir):
@@ -184,13 +237,13 @@ class TestBase(TestCase):
bobj.all_options['save_states_dir'] = '/some/path'
self.assertIsNone(bobj._get_saves_dir())
bobj.all_options['save_states_dir'] = '$CONFIG/../saves'
bobj.all_options['save_states_dir'] = '$WRAPPER/../saves'
self.assertIsNone(bobj._get_saves_dir())
bobj.all_options['save_states_dir'] = '/foo/$CONFIG/saves'
bobj.all_options['save_states_dir'] = '/foo/$WRAPPER/saves'
self.assertIsNone(bobj._get_saves_dir())
bobj.all_options['save_states_dir'] = '$CONFIG/saves'
bobj.all_options['save_states_dir'] = '$WRAPPER/saves'
self.assertIsNone(bobj._get_saves_dir())
path = os.path.join(self.dirname, 'saves')
@@ -202,7 +255,7 @@ class TestBase(TestCase):
os.mkdir(path)
self.assertEqual(bobj._get_saves_dir(), 'saves')
bobj.all_options['save_states_dir'] = '$CONFIG/saves/'
bobj.all_options['save_states_dir'] = '$WRAPPER/saves/'
self.assertEqual(bobj._get_saves_dir(), 'saves')
@mock.patch('fs_uae_wrapper.path.which')
@@ -240,6 +293,12 @@ class TestBase(TestCase):
'wrapper_archiver': '7z'}
self.assertTrue(bobj._validate_options())
which.return_value = None
bobj.all_options = {'wrapper': 'dummy',
'wrapper_save_state': '1',
'wrapper_archiver': '7z'}
self.assertFalse(bobj._validate_options())
@mock.patch('fs_uae_wrapper.path.which')
def test_run_clean(self, which):

View File

@@ -16,23 +16,21 @@ class TestCD32(TestCase):
@mock.patch('fs_uae_wrapper.base.ArchiveBase._save_save')
@mock.patch('fs_uae_wrapper.base.ArchiveBase._get_saves_dir')
@mock.patch('fs_uae_wrapper.base.ArchiveBase._run_emulator')
@mock.patch('fs_uae_wrapper.base.ArchiveBase._kickstart_option')
@mock.patch('fs_uae_wrapper.base.ArchiveBase._copy_conf')
@mock.patch('fs_uae_wrapper.base.ArchiveBase._load_save')
@mock.patch('fs_uae_wrapper.base.ArchiveBase._extract')
def test_run(self, extract, load_save, copy_conf, kick_option,
run_emulator, get_save_dir, save_state, which, mkdtemp):
def test_run(self, extract, load_save, copy_conf, run_emulator,
get_save_dir, save_state, which, mkdtemp):
extract.return_value = False
copy_conf.return_value = False
load_save.return_value = False
kick_option.return_value = {}
run_emulator.return_value = False
get_save_dir.return_value = False
save_state.return_value = False
which.return_value = 'unrar'
acd32 = cd32.CD32('Config.fs-uae', utils.CmdOption(), {})
acd32 = cd32.Wrapper('Config.fs-uae', utils.CmdOption(), {})
self.assertFalse(acd32.run())
acd32.all_options = {'wrapper': 'cd32',
@@ -50,10 +48,6 @@ class TestCD32(TestCase):
load_save.return_value = True
self.assertFalse(acd32.run())
kick_option.return_value = {'foo': 'bar'}
self.assertFalse(acd32.run())
self.assertDictEqual(acd32.fsuae_options, {'foo': 'bar'})
run_emulator.return_value = True
self.assertTrue(acd32.run())

View File

@@ -54,9 +54,18 @@ class TestArchive(TestCase):
call.assert_called_once_with(['false', 'a', 'foo', '.'])
call.reset_mock()
call.return_value = 1
self.assertFalse(arch.extract('foo'))
with open('foo', 'w') as fobj:
fobj.write('\n')
self.assertTrue(arch.extract('foo'))
call.return_value = 1
call.reset_mock()
self.assertFalse(arch.create('foo'))
call.assert_called_once_with(['false', 'a', 'foo', '.'])
call.reset_mock()
self.assertFalse(arch.extract('foo'))
call.assert_called_once_with(['false', 'x', 'foo'])
@@ -128,6 +137,12 @@ class TestArchive(TestCase):
self.assertTrue(arch.create('foo.tgz'))
call.assert_called_once_with(['tar', 'zcf', 'foo.tgz', 'bar', 'foo'])
call.reset_mock()
call.return_value = 1
arch = file_archive.TarArchive()
self.assertFalse(arch.create('foo.tar'))
call.assert_called_once_with(['tar', 'cf', 'foo.tar', 'bar', 'foo'])
@mock.patch('fs_uae_wrapper.path.which')
@mock.patch('subprocess.call')
def test_lha(self, call, which):
@@ -208,6 +223,11 @@ class TestArchive(TestCase):
self.assertFalse(arch.extract('foo'))
call.assert_called_once_with(['7z', 'x', 'foo'])
which.side_effect = ['zip', 'unzip']
arch = file_archive.ZipArchive()
self.assertEqual(arch._compress, 'zip')
self.assertEqual(arch._decompress, 'unzip')
@mock.patch('fs_uae_wrapper.path.which')
@mock.patch('subprocess.call')
def test_rar(self, call, which):
@@ -231,17 +251,23 @@ class TestArchive(TestCase):
self.assertTrue(arch.create('foo.rar'))
call.assert_called_once_with(['rar', 'a', 'foo.rar', 'bar', 'baz',
'directory', 'foo'])
call.return_value = 1
call.reset_mock()
self.assertFalse(arch.create('foo.rar'))
call.assert_called_once_with(['rar', 'a', 'foo.rar', 'bar', 'baz',
'directory', 'foo'])
with open('foo', 'w') as fobj:
fobj.write('\n')
call.reset_mock()
call.return_value = 1
self.assertFalse(arch.extract('foo'))
call.assert_called_once_with(['rar', 'x', 'foo'])
call.reset_mock()
call.return_value = 0
arch._compess = arch._decompess = arch.archiver = 'unrar'
arch._compress = arch._decompress = arch.archiver = 'unrar'
self.assertFalse(arch.create('foo'))
call.assert_not_called()
@@ -250,3 +276,42 @@ class TestArchive(TestCase):
call.return_value = 1
self.assertFalse(arch.extract('foo'))
call.assert_called_once_with(['unrar', 'x', 'foo'])
class TestArchivers(TestCase):
def test_get(self):
self.assertEqual(file_archive.Archivers.get('tar'),
file_archive.TarArchive)
self.assertEqual(file_archive.Archivers.get('tar.gz'),
file_archive.TarGzipArchive)
self.assertEqual(file_archive.Archivers.get('tgz'),
file_archive.TarGzipArchive)
self.assertEqual(file_archive.Archivers.get('tar.bz2'),
file_archive.TarBzip2Archive)
self.assertEqual(file_archive.Archivers.get('tar.xz'),
file_archive.TarXzArchive)
self.assertEqual(file_archive.Archivers.get('rar'),
file_archive.RarArchive)
self.assertEqual(file_archive.Archivers.get('7z'),
file_archive.SevenZArchive)
self.assertEqual(file_archive.Archivers.get('lha'),
file_archive.LhaArchive)
self.assertEqual(file_archive.Archivers.get('lzh'),
file_archive.LhaArchive)
self.assertEqual(file_archive.Archivers.get('lzx'),
file_archive.LzxArchive)
self.assertIsNone(file_archive.Archivers.get('ace'))
def test_get_extension_by_name(self):
archivers = file_archive.Archivers
self.assertEqual(archivers.get_extension_by_name('tar'), '.tar')
self.assertEqual(archivers.get_extension_by_name('tgz'), '.tar.gz')
self.assertEqual(archivers.get_extension_by_name('tar.bz2'),
'.tar.bz2')
self.assertEqual(archivers.get_extension_by_name('tar.xz'), '.tar.xz')
self.assertEqual(archivers.get_extension_by_name('rar'), '.rar')
self.assertEqual(archivers.get_extension_by_name('7z'), '.7z')
self.assertEqual(archivers.get_extension_by_name('lha'), '.lha')
self.assertEqual(archivers.get_extension_by_name('lzx'), '.lzx')
self.assertIsNone(archivers.get_extension_by_name('ace'))

View File

@@ -11,8 +11,12 @@ from fs_uae_wrapper import utils
class TestPlainModule(TestCase):
@mock.patch('subprocess.call')
def test_show(self, subprocess_call):
@mock.patch('fs_uae_wrapper.utils.run_command')
def test_run(self, run_command):
wrapper = plain.Wrapper('some.conf', utils.CmdOption(), {})
wrapper.run()
run_command.assert_called_once_with(['fs-uae', 'some.conf'])
plain.run('some.conf', utils.CmdOption(), None)
subprocess_call.assert_called_once()
def test_clean(self):
wrapper = plain.Wrapper('some.conf', utils.CmdOption(), {})
self.assertIsNone(wrapper.clean())

View File

@@ -12,7 +12,7 @@ from fs_uae_wrapper import savestate
from fs_uae_wrapper import utils
class TestArchive(TestCase):
class TestSaveState(TestCase):
def setUp(self):
self.dirname = mkdtemp()
@@ -31,20 +31,18 @@ class TestArchive(TestCase):
@mock.patch('fs_uae_wrapper.base.Base._save_save')
@mock.patch('fs_uae_wrapper.base.Base._get_saves_dir')
@mock.patch('fs_uae_wrapper.base.Base._run_emulator')
@mock.patch('fs_uae_wrapper.base.Base._kickstart_option')
@mock.patch('fs_uae_wrapper.base.Base._copy_conf')
@mock.patch('fs_uae_wrapper.base.Base._load_save')
def test_run(self, load_save, copy_conf, kick_option, run_emulator,
get_save_dir, save_state, which, mkdtemp):
def test_run(self, load_save, copy_conf, run_emulator, get_save_dir,
save_state, which, mkdtemp):
copy_conf.return_value = False
kick_option.return_value = False
run_emulator.return_value = False
get_save_dir.return_value = False
save_state.return_value = False
which.return_value = 'rar'
arch = savestate.SaveState('Config.fs-uae', utils.CmdOption(), {})
arch = savestate.Wrapper('Config.fs-uae', utils.CmdOption(), {})
self.assertFalse(arch.run())
arch.all_options = {'wrapper': 'savestate',
@@ -58,10 +56,6 @@ class TestArchive(TestCase):
copy_conf.return_value = True
self.assertFalse(arch.run())
kick_option.return_value = {'foo': 'bar'}
self.assertFalse(arch.run())
self.assertDictEqual(arch.fsuae_options, {'foo': 'bar'})
run_emulator.return_value = True
self.assertTrue(arch.run())
@@ -75,7 +69,7 @@ class TestArchive(TestCase):
def test_validate_options(self, which):
which.return_value = 'unrar'
arch = savestate.SaveState('Config.fs-uae', utils.CmdOption(), {})
arch = savestate.Wrapper('Config.fs-uae', utils.CmdOption(), {})
self.assertFalse(arch._validate_options())
arch.all_options['wrapper'] = 'savestate'

View File

@@ -83,7 +83,7 @@ class TestUtils(TestCase):
which.return_value = None
# No config
self.assertFalse(utils.operate_archive('non-existend.7z', 'foo', '',
self.assertFalse(utils.operate_archive('non-existent.7z', 'foo', '',
None))
# Archive type not known
@@ -114,45 +114,36 @@ class TestUtils(TestCase):
create.assert_called_once()
show.assert_called_once()
def test_extract_archive(self):
@mock.patch('fs_uae_wrapper.utils.operate_archive')
def test_extract_archive(self, operate):
os.chdir(self.dirname)
# No config
self.assertFalse(utils.extract_archive('non-existend.7z'))
operate.return_value = True
self.assertTrue(utils.extract_archive('arch.7z'))
operate.assert_called_once_with('arch.7z', 'extract', '', None)
# Archive type not known
with open('unsupported-archive.ace', 'w') as fobj:
fobj.write("\n")
self.assertFalse(utils.extract_archive('unsupported-archive.ace'))
operate.reset_mock()
operate.return_value = False
self.assertFalse(utils.extract_archive('arch.7z', 'MyFoo',
['foo', 'bar']))
operate.assert_called_once_with('arch.7z', 'extract',
"Extracting files for `MyFoo'. Please"
" be patient", ['foo', 'bar'])
# archive is known, but extraction will fail - we have an empty
# archive and there is no guarantee, that 7z exists on system where
# test will run
with open('supported-archive.7z', 'w') as fobj:
fobj.write("\n")
self.assertFalse(utils.extract_archive('supported-archive.7z'))
@mock.patch('fs_uae_wrapper.utils.operate_archive')
def test_create_archive(self, operate):
operate.return_value = True
self.assertTrue(utils.create_archive('arch.7z'))
operate.assert_called_once_with('arch.7z', 'create', '', None)
@mock.patch('fs_uae_wrapper.file_archive.Archive.create')
def test_create_archive(self, arch_create):
arch_create.return_value = True
os.chdir(self.dirname)
# No config
self.assertFalse(utils.extract_archive('non-existend.7z'))
# Archive type not known
with open('unsupported-archive.ace', 'w') as fobj:
fobj.write("\n")
self.assertFalse(utils.extract_archive('unsupported-archive.ace'))
# archive is known, but extraction will fail - we have an empty
# archive and there is no guarantee, that 7z exists on system where
# test will run
with open('supported-archive.7z', 'w') as fobj:
fobj.write("\n")
self.assertFalse(utils.extract_archive('supported-archive.7z'))
operate.reset_mock()
operate.return_value = False
self.assertFalse(utils.create_archive('arch.7z', 'MyFoo',
['foo', 'bar']))
operate.assert_called_once_with('arch.7z', 'create',
"Creating archive for `MyFoo'. Please"
" be patient", ['foo', 'bar'])
@mock.patch('fs_uae_wrapper.path.which')
@mock.patch('fs_uae_wrapper.file_archive.Archive.extract')
@@ -179,6 +170,37 @@ class TestUtils(TestCase):
self.assertDictEqual(conf, {'foo': '1', 'bar': 'zip'})
self.assertDictEqual(other, {'foo': '2', 'baz': '3'})
@mock.patch('subprocess.call')
def test_run_command(self, call):
call.return_value = 0
self.assertTrue(utils.run_command(['ls']))
call.assert_called_once_with(['ls'])
call.reset_mock()
self.assertTrue(utils.run_command('ls -l'))
call.assert_called_once_with(['ls', '-l'])
call.return_value = 1
call.reset_mock()
self.assertFalse(utils.run_command(['ls', '-l']))
call.assert_called_once_with(['ls', '-l'])
call.reset_mock()
self.assertFalse(utils.run_command('ls'))
call.assert_called_once_with(['ls'])
@mock.patch('os.path.exists')
def test_get_config(self, exists):
exists.return_value = False
os.chdir(self.dirname)
self.assertDictEqual(utils.get_config('foo'), {})
with open('conf.fs-uae', 'w') as fobj:
fobj.write("[conf]\nwrapper=foo\n")
self.assertDictEqual(utils.get_config('conf.fs-uae'),
{'wrapper': 'foo'})
class TestCmdOptions(TestCase):
@@ -212,33 +234,43 @@ class TestCmdOptions(TestCase):
self.assertListEqual(sorted(cmd.list()),
['--fast_memory=4096', '--fullscreen'])
@mock.patch('os.path.exists')
@mock.patch('os.getenv')
@mock.patch('os.path.expandvars')
@mock.patch('distutils.spawn.find_executable')
def test_interpolate_variables(self, find_exe, expandv, getenv):
def test_interpolate_variables(self, find_exe, expandv, getenv, os_exists):
os_exists.return_value = True
itrpl = utils.interpolate_variables
string = 'foo = $CONFIG/../path/to/smth'
string = '$CONFIG/../path/to/smth'
self.assertEqual(itrpl(string, '/home/user/Config.fs-uae'),
'foo = /home/user/../path/to/smth')
string = 'bar = $HOME'
'/home/path/to/smth')
string = '$HOME'
expandv.return_value = '/home/user'
self.assertEqual(itrpl(string, '/home/user/Config.fs-uae'),
'bar = /home/user')
'/home/user')
string = 'foo = $APP/$EXE'
string = '$APP/$EXE'
find_exe.return_value = '/usr/bin/fs-uae'
self.assertEqual(itrpl(string, '/home/user/Config.fs-uae'),
'foo = /usr/bin/fs-uae//usr/bin/fs-uae')
'/usr/bin/fs-uae//usr/bin/fs-uae')
string = 'docs = $DOCUMENTS'
string = '$DOCUMENTS'
getenv.return_value = '/home/user/Docs'
self.assertEqual(itrpl(string, '/home/user/Config.fs-uae'),
'docs = /home/user/Docs')
'/home/user/Docs')
string = 'baz = $BASE'
string = '$BASE'
self.assertEqual(itrpl(string, '/home/user/Config.fs-uae'),
'baz = $BASE')
'$BASE')
self.assertEqual(itrpl(string, '/home/user/Config.fs-uae', 'base'),
'baz = base')
'base')
@mock.patch('os.getenv')
@mock.patch('os.path.expandvars')
def test_interpolate_variables_path_not_exists(self, expandv, getenv):
itrpl = utils.interpolate_variables
string = '$CONFIG/../path/to/smth'
self.assertEqual(itrpl(string, '/home/user/Config.fs-uae'), string)

View File

@@ -28,7 +28,7 @@ class TestWrapper(TestCase):
os.unlink(self.fname)
sys.argv = self._argv[:]
@mock.patch('fs_uae_wrapper.plain.run')
@mock.patch('fs_uae_wrapper.plain.Wrapper.run')
def test_run(self, mock_plain_run):
sys.argv.append('--help')
@@ -67,11 +67,13 @@ class TestWrapper(TestCase):
def test_parse_args(self):
# Looking for configuration file... first, we have nothing
self.assertEqual(wrapper.parse_args(), (None, {}))
self.assertEqual(wrapper.parse_args(),
(None, {'wrapper_verbose': 0, 'wrapper_quiet': 0}))
# still no luck - nonexistent file
sys.argv.append('there-is-no-config.fs-uae')
self.assertEqual(wrapper.parse_args(), (None, {}))
self.assertEqual(wrapper.parse_args(),
(None, {'wrapper_verbose': 0, 'wrapper_quiet': 0}))
# lets make it
os.chdir(self.dirname)
@@ -79,7 +81,8 @@ class TestWrapper(TestCase):
fobj.write('\n')
self.assertEqual(wrapper.parse_args(),
('there-is-no-config.fs-uae', {}))
('there-is-no-config.fs-uae',
{'wrapper_verbose': 0, 'wrapper_quiet': 0}))
# remove argument, try to find default one
sys.argv.pop()
@@ -88,7 +91,9 @@ class TestWrapper(TestCase):
with open('Config.fs-uae', 'w') as fobj:
fobj.write('\n')
self.assertEqual(wrapper.parse_args(), ('Config.fs-uae', {}))
self.assertEqual(wrapper.parse_args(),
('Config.fs-uae',
{'wrapper_verbose': 0, 'wrapper_quiet': 0}))
# add --wrapper-foo and --wrapper-bar options
sys.argv.extend(['--wrapper=plain', '--wrapper_foo=1',
@@ -104,7 +109,9 @@ class TestWrapper(TestCase):
self.assertEqual(conf, 'Config.fs-uae')
self.assertDictEqual(fsopts, {'wrapper': 'plain',
'wrapper_foo': '1',
'wrapper_bar': 'false'})
'wrapper_bar': 'false',
'wrapper_verbose': 0,
'wrapper_quiet': 0})
# mix wrapper* params in commandline and config
sys.argv = ['fs-uae-wrapper',
@@ -120,4 +127,6 @@ class TestWrapper(TestCase):
self.assertDictEqual(fsopts, {'wrapper': 'plain',
'wrapper_bar': 'false',
'fullscreen': '1',
'fast_memory': '4096'})
'fast_memory': '4096',
'wrapper_verbose': 0,
'wrapper_quiet': 0})

15
tox.ini
View File

@@ -1,5 +1,5 @@
[tox]
envlist = py27,py34,py27-flake8,py34-flake8
envlist = py3,py3-flake8
usedevelop = True
@@ -10,16 +10,7 @@ commands = py.test --cov=fs_uae_wrapper --cov-report=term-missing
deps = -r{toxinidir}/test-requirements.txt
[testenv:py27]
deps = {[testenv]deps}
mock
[testenv:py34-flake8]
basepython = python3.4
deps = flake8
commands = flake8 {posargs}
[testenv:py27-flake8]
basepython = python2.7
[testenv:py3-flake8]
basepython = python3
deps = flake8
commands = flake8 {posargs}