1
0
mirror of https://github.com/gryf/fs-uae-wrapper.git synced 2026-02-01 21:45:54 +01:00

20 Commits

Author SHA1 Message Date
7b0ef15eae Added dummy message class for systems with python without tk 2026-01-10 18:48:41 +01:00
59bd1b6029 Make tempfile prefixed with fs-uae-wrapper string.
Also, included test requirements into tox file, and fixed docs.
2025-09-25 18:50:28 +02:00
f5e6471555 Removed license classifier in favor of SPDX entry. 2025-04-18 16:05:37 +02:00
4c61c3d7ea Fix user provided whdload options 2024-09-14 17:00:28 +02:00
7e3d68624f Fixed packages find in pyproject 2024-09-14 15:59:25 +02:00
f311605019 Take version from git tag. 2024-09-14 11:49:44 +02:00
418e480fb5 Fixes for readme 2024-09-14 11:34:33 +02:00
118d758ec7 Trailing space 2024-09-14 10:57:50 +02:00
3b597e34ee Fix some lint errors and warnings 2024-09-14 10:53:58 +02:00
7b40974779 Fixed imports order 2024-09-14 09:22:41 +02:00
b4ab8ac4f5 Change GH action icon location 2024-09-13 21:29:54 +02:00
8d8d38d5c0 Move from travis to gh actions 2024-09-13 21:27:40 +02:00
bd0aa3dee4 Changed name for the tests workflow 2024-09-13 21:25:00 +02:00
d2a9f39fd9 Create python-app.yml 2024-09-13 21:02:58 +02:00
f1f64cf4d4 Added WHDLoad support 2024-09-13 18:18:35 +02:00
a5606272cd Fix unit tests 2024-09-13 18:16:45 +02:00
463f6ed705 Fixed deduction of archive name.
When in different directory, the archive name and config need to be
treated differently. This commit unify behavior for both cases.

Also, changing a bit logging format and added some more debugging
messages.
2024-09-12 20:29:34 +02:00
148d28dac2 Make archive name and archiver for savestate optional. 2024-09-11 21:23:49 +02:00
60139d1728 Move to pyproject. 2024-09-11 19:53:52 +02:00
68b1f2a787 Correct homepage in setup.cfg 2022-10-02 14:57:38 +02:00
27 changed files with 885 additions and 231 deletions

33
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,33 @@
# This workflow will install Python dependencies, run tests and lint with a single version of Python
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
name: Test
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.10
uses: actions/setup-python@v3
with:
python-version: "3.10"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8 pytest tox coverage pytest-cov pytest-pep8
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Test with pytest
run: |
tox

View File

@@ -1,11 +0,0 @@
language: python
env:
- TOXENV=py27
- TOXENV=py27-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

@@ -2,8 +2,8 @@
FS-UAE Wrapper FS-UAE Wrapper
============== ==============
.. image:: https://travis-ci.org/gryf/fs-uae-wrapper.svg?branch=master .. image:: https://github.com/gryf/fs-uae-wrapper/workflows/Test/badge.svg
:target: https://travis-ci.org/gryf/fs-uae-wrapper :target: https://github.com/gryf/fs-uae-wrapper/actions?query=workflow%3ATest+event%3Apush+branch%3Amaster
.. image:: https://img.shields.io/pypi/v/fs-uae-wrapper.svg .. image:: https://img.shields.io/pypi/v/fs-uae-wrapper.svg
:target: https://pypi.python.org/pypi/fs-uae-wrapper :target: https://pypi.python.org/pypi/fs-uae-wrapper
@@ -137,6 +137,7 @@ Currently, couple of wrapper modules are available:
- cd32 - cd32
- archive - archive
- savestate - savestate
- whdload
plain plain
----- -----
@@ -157,8 +158,8 @@ Options used:
* ``wrapper`` (required) with ``cd32`` as an value * ``wrapper`` (required) with ``cd32`` as an value
* ``wrapper_archive`` (required) path to the archive with CD32 iso/cue/wav * ``wrapper_archive`` (required) path to the archive with CD32 iso/cue/wav
* ``wrapper_archiver`` (conditionally required) archiver to use for storage * ``wrapper_archiver`` (optional) archiver to use for storage save state -
save state default ``7z``.
* ``wrapper_gui_msg`` (optional) if set to "1", will display a graphical * ``wrapper_gui_msg`` (optional) if set to "1", will display a graphical
message during extracting files message during extracting files
* ``wrapper_save_state`` (optional) if set to "1", will load/archive save state * ``wrapper_save_state`` (optional) if set to "1", will load/archive save state
@@ -175,7 +176,7 @@ fragment of configuration file is saved as ``ChaosEngine.fs-uae``:
[config] [config]
wrapper = cd32 wrapper = cd32
wrapper_archive = ChaosEngine.7z wrapper_archive = ChaosEngine.7z
wrapper_archiver = 7z wrapper_archiver = zip
wrapper_gui_msg = 1 wrapper_gui_msg = 1
amiga_model = CD32 amiga_model = CD32
@@ -219,10 +220,12 @@ archive
Options used: Options used:
* ``wrapper`` (required) with ``archive`` as an value * ``wrapper`` (required) with ``archive`` as an value
* ``wrapper_archive`` (required) path to the archive with assets (usually means * ``wrapper_archive`` (optional) path to the archive with assets (usually means
whole system directories, floppies or hard disk images) whole system directories, floppies or hard disk images), defaults to same
* ``wrapper_archiver`` (conditionally required) archiver to use for storage name as configuration file with some detected archive extension. Note, that
save state name is case sensitive
* ``wrapper_archiver`` (optional) archiver to use for storage save state -
default ``7z``.
* ``wrapper_gui_msg`` (optional) if set to "1", will display a graphical * ``wrapper_gui_msg`` (optional) if set to "1", will display a graphical
message during extracting files message during extracting files
* ``wrapper_persist_data`` (optional) if set to "1", will compress (possibly * ``wrapper_persist_data`` (optional) if set to "1", will compress (possibly
@@ -277,7 +280,8 @@ savestate
Options used: Options used:
* ``wrapper`` (required) with ``archive`` as an value * ``wrapper`` (required) with ``archive`` as an value
* ``wrapper_archiver`` (required) archiver to use for storage save state * ``wrapper_archiver`` (optional) archiver to use for storage save state -
default ``7z``.
This module is primarily used to run emulator with read only media attached This module is primarily used to run emulator with read only media attached
(like images of floppies or uncompressed CD-ROMs) and its purpose is to (like images of floppies or uncompressed CD-ROMs) and its purpose is to
@@ -311,6 +315,212 @@ The steps would be as follows:
- optionally create archive with save state (if save state directory place is - optionally create archive with save state (if save state directory place is
*not* a global one) *not* a global one)
whdload
-------
Options used:
* ``wrapper`` (required) with ``whdload`` as an value
* ``wrapper_whdload_base`` (required) path to the whdload base system. Usually
it's minimal system containing at least whdload executables in C, and config
in S. Read on below for further details.
* ``wrapper_whdload_options`` (optional) this option will replace the line in
``s:whdload-startup`` with specific ``whdload`` options for certain slave.
For reference look at WHDLoad documentation and/or on ``s:WHDLoad.prefs``.
Note, that ``Slave=`` option must not be used.
* ``wrapper_archive`` (optional) path to the whdload archive, defaults to same
name as configuration file with some detected archive extension. Note, that
name is case sensitive
This module is solely used with whdload distributed games (not just whdload
slave files, but whole games, which can be found on several places on the
internet).
Base image
~~~~~~~~~~
To make it work, first the absolute minimal image need to contain following
structure:
.. code::
.
├── C
│   ├── DIC
│   ├── Execute
│   ├── Patcher
│   ├── RawDIC
│   ├── SetPatch
│   ├── WHDLoad
│   └── WHDLoadCD32
└── S
├── startup-sequence
└── WHDLoad.prefs
where the minimum dependences are:
- ``Excecute`` from your copy of Workbench
- `WHDLoad`_ 18.9
- `SetPatch`_ 43.6
and the ``S/startup-sequence`` should at least contain:
.. code::
setpatch QUIET
IF EXISTS S:whdload-startup
Execute S:whdload-startup
EndIF
To leverage more pleasant UX, additionally those bits should be installed (or -
copied into base image filesystem):
- ``Assign`` and whatever commands you'll be use in scripts from your copy of
Workbench
- `uaequit`_ - this will allow to quit emulator, after quiting game
- `kgiconload`_ - tool for reading icon and executing *default tool* with
optionally defined tool types as parameters (in this case: WHDLoad)
- `SKick`_ optionally - for kickstart relocations. Also images of corresponding
kickstart ROM images will be needed.
and then ``s/startup-sequence`` might looks a follows:
.. code::
Assign >NIL: ENV: RAM:
Assign >NIL: T: RAM:
setpatch QUIET
IF EXISTS S:whdload-startup
Execute S:whdload-startup
EndIF
C:UAEquit
Creating base image archive
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Now, the tree for the minimal image could look like that:
.. code::
.
├── C
│   ├── Assign
│   ├── DIC
│   ├── Execute
│   ├── kgiconload
│   ├── Patcher
│   ├── RawDIC
│   ├── SetPatch
│   ├── UAEquit
│   ├── WHDLoad
│   └── WHDLoadCD32
└── S
├── startup-sequence
└── WHDLoad.prefs
to use relocation tables you'll need to place ``Kickstarts`` drawer into Devs
drawer. Also keep in mind, that corresponding kickstart rom images need to be
placed there as well, otherwise it may or may not work. Structure looks like
this:
.. code::
.
├── C
│   ├── Assign
│   ├── …
│   └── WHDLoadCD32
├── Devs
│   └── Kickstarts
│   ├── 39115_ROMKick.PAT
│   ├── 39115_ROMKick.RTB
│   ├── …
│   ├── kick40068.A4000.PAT
│   └── kick40068.A4000.RTB
└── S
├── startup-sequence
└── WHDLoad.prefs
Important: You'll need to prepare archive with base OS without top directory,
i.e. suppose you have prepared all the files in ``/tmp/baseos``:
.. code:: shell-session
$ pwd
/tmp
$ cd baseos
$ pwd
/tmp/basos
$ ls
C S
$ zip -r /tmp/base.zip .
adding: C/ (stored 0%)
adding: C/Assign (deflated 31%)
adding: C/WHDLoadCD32 (deflated 26%)
adding: C/RawDIC (deflated 46%)
adding: C/UAEquit (deflated 39%)
adding: C/Execute (deflated 42%)
adding: C/Patcher (deflated 56%)
adding: C/DIC (deflated 33%)
adding: C/kgiconload (deflated 49%)
adding: C/SetPatch (deflated 39%)
adding: C/WHDLoad (deflated 23%)
adding: S/ (stored 0%)
adding: S/startup-sequence (deflated 36%)
adding: S/WHDLoad.prefs (deflated 51%)
You can do it with other archivers as well, like 7z: ``7z a /tmp/base.7z .`` or
tar with different compressions: ``tar Jcf /tmp/base.tar.xz .``, ``tar zcf
/tmp/base.tgz .``, ``tar jcf /tmp/base.tar.bz2 .``. It should work with all
mentioned at the beginning of this document archivers.
Starting point is in ``S/startup-sequence`` file, where eventually
``S/whdload-startup`` is executed, which will be created by fs-uae-warpper
before execution by fs-uae.
Configuration
~~~~~~~~~~~~~
Now, to use whdload module with any of the WHDLoad game, you'll need to prepare
configuration for the wrapper.
Example configuration:
.. code:: ini
[config]
wrapper = whdload
wrapper_whdload_base = $CONFIG/whdload_base.7z
# ...
And execution is as usual:
.. code:: shell-session
$ fs-uae-wrapper ChaosEngine_v1.2_0106.fs-uae
Now, similar to the archive module, it will create temporary directory, unpack
base image there, unpack WHDLoad game archive, search for slave file, and
prepare ``s:whdload-startup``, and finally pass all the configuration to
fs-uae.
Limitations
===========
There is one limitation when using save ``wrapper_save_state`` option. In case
of floppies it should work without any issues, although save state for running
Workbench or WHDLoad games may or may not work. In the past there was an issue
with `fs-uae`_ where saving state was causing data corruption on the emulated
system. Use it with caution!
License License
======= =======
@@ -325,3 +535,8 @@ This work is licensed on 3-clause BSD license. See LICENSE file for details.
.. _tar: https://www.gnu.org/software/tar/ .. _tar: https://www.gnu.org/software/tar/
.. _zip: http://www.info-zip.org .. _zip: http://www.info-zip.org
.. _CheeseShop: https://pypi.python.org/pypi/fs-/fs-uae-wrapperuae-wrapper .. _CheeseShop: https://pypi.python.org/pypi/fs-/fs-uae-wrapperuae-wrapper
.. _WHDLoad: https://www.whdload.de
.. _uaequit: https://aminet.net/package/misc/emu/UAEquit
.. _SKick: https://aminet.net/package/util/boot/skick346
.. _SetPatch: https://aminet.net/package/util/boot/SetPatch_43.6b
.. _kgiconload: https://eab.abime.net/showpost.php?p=733614&postcount=92

View File

@@ -7,8 +7,7 @@ the temporary one.
import os import os
import shutil import shutil
from fs_uae_wrapper import base from fs_uae_wrapper import base, utils
from fs_uae_wrapper import utils
class Wrapper(base.ArchiveBase): class Wrapper(base.ArchiveBase):

View File

@@ -4,11 +4,9 @@ Base class for all wrapper modules
import logging import logging
import os import os
import shutil import shutil
import sys
import tempfile import tempfile
from fs_uae_wrapper import utils from fs_uae_wrapper import path, utils
from fs_uae_wrapper import path
class Base(object): class Base(object):
@@ -42,10 +40,11 @@ class Base(object):
- run the emulation - run the emulation
- archive save state - archive save state
""" """
logging.debug("run")
if not self._validate_options(): if not self._validate_options():
return False return False
self.dir = tempfile.mkdtemp() self.dir = tempfile.mkdtemp(prefix='fs-uae-wrapper-')
self._normalize_options() self._normalize_options()
self._set_assets_paths() self._set_assets_paths()
@@ -83,7 +82,7 @@ class Base(object):
"""execute fs-uae""" """execute fs-uae"""
curdir = os.path.abspath('.') curdir = os.path.abspath('.')
os.chdir(self.dir) os.chdir(self.dir)
utils.run_command(['fs-uae'] + self.fsuae_options.list()) utils.run_command(['fs-uae', *self.fsuae_options.list()])
os.chdir(curdir) os.chdir(curdir)
return True return True
@@ -176,13 +175,14 @@ class Base(object):
Configuration file will be placed in new directory, therefore it is Configuration file will be placed in new directory, therefore it is
needed to calculate new paths so that emulator can find assets. needed to calculate new paths so that emulator can find assets.
""" """
options = ['wrapper_archive', 'accelerator_rom', 'base_dir', logging.debug("_normalize_options")
'cdrom_drive_0', 'cdroms_dir', 'controllers_dir', options = ['wrapper_archive', 'wrapper_whdload_base',
'cpuboard_flash_ext_file', 'cpuboard_flash_file', 'accelerator_rom', 'base_dir', 'cdrom_drive_0',
'floppies_dir', 'floppy_overlays_dir', 'fmv_rom', 'cdroms_dir', 'controllers_dir', 'cpuboard_flash_ext_file',
'graphics_card_rom', 'hard_drives_dir', 'kickstart_file', 'cpuboard_flash_file', 'floppies_dir',
'kickstarts_dir', 'logs_dir', 'save_states_dir', 'floppy_overlays_dir', 'fmv_rom', 'graphics_card_rom',
'screenshots_output_dir'] 'hard_drives_dir', 'kickstart_file', 'kickstarts_dir',
'logs_dir', 'save_states_dir', 'screenshots_output_dir']
for num in range(20): for num in range(20):
options.append('cdrom_image_%d' % num) options.append('cdrom_image_%d' % num)
@@ -217,6 +217,7 @@ class Base(object):
if val.startswith('$CONFIG'): if val.startswith('$CONFIG'):
abspath = utils.interpolate_variables(val, self.conf_file) abspath = utils.interpolate_variables(val, self.conf_file)
changed_options[key] = abspath changed_options[key] = abspath
logging.info("%s: %s => %s", key, val, abspath)
continue continue
_val = os.path.abspath(val) _val = os.path.abspath(val)
@@ -237,9 +238,9 @@ class Base(object):
return True return True
if 'wrapper_archiver' not in self.all_options: if 'wrapper_archiver' not in self.all_options:
logging.error("Configuration lacks of required " logging.warning("Configuration lacks of optional "
"`wrapper_archiver' option.") "`wrapper_archiver' option, fall back to 7z")
return False self.all_options['wrapper_archiver'] = "7z"
if not path.which(self.all_options['wrapper_archiver']): if not path.which(self.all_options['wrapper_archiver']):
logging.error("Cannot find archiver `%s'.", logging.error("Cannot find archiver `%s'.",
@@ -282,6 +283,7 @@ class ArchiveBase(Base):
def _extract(self): def _extract(self):
"""Extract archive to temp dir""" """Extract archive to temp dir"""
logging.debug("_extract")
title = self._get_title() title = self._get_title()
curdir = os.path.abspath('.') curdir = os.path.abspath('.')
@@ -291,12 +293,36 @@ class ArchiveBase(Base):
return result return result
def _validate_options(self): def _validate_options(self):
logging.debug("_validate_options")
validation_result = super(ArchiveBase, self)._validate_options() validation_result = super(ArchiveBase, self)._validate_options()
if not validation_result:
return False
if 'wrapper_archive' not in self.all_options: if 'wrapper_archive' not in self.all_options:
sys.stderr.write("Configuration lacks of required " logging.warning("Configuration lacks of optional `wrapper_archive'"
"`wrapper_archive' option.\n") " option.\n")
wrapper_archive = self._get_wrapper_archive_name()
if wrapper_archive is None:
logging.error("Configuration lacks of optional "
"`wrapper_archive', cannot deduct the name by "
"configuration file name.\n")
validation_result = False validation_result = False
self.all_options['wrapper_archive'] = wrapper_archive
return validation_result return validation_result
def _get_wrapper_archive_name(self):
"""
Return full path to the archive name using configuration file
basename and appending one of the expected archive extensions.
"""
basename = os.path.splitext(os.path.basename(self.conf_file))[0]
file_list = os.listdir(os.path.dirname(self.conf_file))
for fname in file_list:
for ext in ('.7z', '.lha', '.lzx', '.zip', '.rar', '.tar', '.tgz',
'.tar.gz', '.tar.bz2', '.tar.xz'):
if ((basename + ext).lower() == fname.lower() and
basename == os.path.splitext(fname)[0]):
return fname
return None

View File

@@ -1,18 +1,18 @@
""" """
File archive classes File archive classes
""" """
import os
import subprocess
import re
import logging import logging
import os
import re
import subprocess
from fs_uae_wrapper import path from fs_uae_wrapper import path
class Archive(object): class Archive(object):
"""Base class for archive support""" """Base class for archive support"""
ADD = ['a'] ADD = ('a',)
EXTRACT = ['x'] EXTRACT = ('x',)
ARCH = 'false' ARCH = 'false'
def __init__(self): def __init__(self):
@@ -27,8 +27,8 @@ class Archive(object):
files = files if files else ['.'] files = files if files else ['.']
logging.debug("Calling `%s %s %s %s'.", self._compress, logging.debug("Calling `%s %s %s %s'.", self._compress,
" ".join(self.ADD), arch_name, " ".join(files)) " ".join(self.ADD), arch_name, " ".join(files))
result = subprocess.call([self._compress] + self.ADD + [arch_name] result = subprocess.call([self._compress, *self.ADD, arch_name,
+ files) *files])
if result != 0: if result != 0:
logging.error("Unable to create archive `%s'.", arch_name) logging.error("Unable to create archive `%s'.", arch_name)
return False return False
@@ -44,8 +44,7 @@ class Archive(object):
logging.debug("Calling `%s %s %s'.", self._compress, logging.debug("Calling `%s %s %s'.", self._compress,
" ".join(self.ADD), arch_name) " ".join(self.ADD), arch_name)
result = subprocess.call([self._decompress] + self.EXTRACT + result = subprocess.call([self._decompress, *self.EXTRACT, arch_name])
[arch_name])
if result != 0: if result != 0:
logging.error("Unable to extract archive `%s'.", arch_name) logging.error("Unable to extract archive `%s'.", arch_name)
return False return False
@@ -53,16 +52,16 @@ class Archive(object):
class TarArchive(Archive): class TarArchive(Archive):
ADD = ['cf'] ADD = ('cf',)
EXTRACT = ['xf'] EXTRACT = ('xf',)
ARCH = 'tar' ARCH = 'tar'
def create(self, arch_name, files=None): def create(self, arch_name, files=None):
files = files if files else sorted(os.listdir('.')) files = files if files else sorted(os.listdir('.'))
logging.debug("Calling `%s %s %s %s'.", self._compress, logging.debug("Calling `%s %s %s %s'.", self._compress,
" ".join(self.ADD), arch_name, " ".join(files)) " ".join(self.ADD), arch_name, " ".join(files))
result = subprocess.call([self._compress] + self.ADD + [arch_name] + result = subprocess.call([self._compress, *self.ADD, arch_name,
files) *files])
if result != 0: if result != 0:
logging.error("Unable to create archive `%s'.", arch_name) logging.error("Unable to create archive `%s'.", arch_name)
return False return False
@@ -70,15 +69,15 @@ class TarArchive(Archive):
class TarGzipArchive(TarArchive): class TarGzipArchive(TarArchive):
ADD = ['zcf'] ADD = ('zcf',)
class TarBzip2Archive(TarArchive): class TarBzip2Archive(TarArchive):
ADD = ['jcf'] ADD = ('jcf',)
class TarXzArchive(TarArchive): class TarXzArchive(TarArchive):
ADD = ['Jcf'] ADD = ('Jcf',)
class LhaArchive(Archive): class LhaArchive(Archive):
@@ -86,8 +85,8 @@ class LhaArchive(Archive):
class ZipArchive(Archive): class ZipArchive(Archive):
ADD = ['a', '-tzip'] ADD = ('a', '-tzip')
ARCH = ['7z', 'zip'] ARCH = ('7z', 'zip')
def __init__(self): def __init__(self):
super(ZipArchive, self).__init__() super(ZipArchive, self).__init__()
@@ -102,7 +101,7 @@ class SevenZArchive(Archive):
class LzxArchive(Archive): class LzxArchive(Archive):
EXTRACT = ['-x'] EXTRACT = ('-x',)
ARCH = 'unlzx' ARCH = 'unlzx'
@classmethod @classmethod
@@ -113,7 +112,7 @@ class LzxArchive(Archive):
class RarArchive(Archive): class RarArchive(Archive):
ARCH = ['rar', 'unrar'] ARCH = ('rar', 'unrar')
def create(self, arch_name, files=None): def create(self, arch_name, files=None):
files = files if files else sorted(os.listdir('.')) files = files if files else sorted(os.listdir('.'))
@@ -124,8 +123,8 @@ class RarArchive(Archive):
logging.debug("Calling `%s %s %s %s'.", self._compress, logging.debug("Calling `%s %s %s %s'.", self._compress,
" ".join(self.ADD), arch_name, " ".join(files)) " ".join(self.ADD), arch_name, " ".join(files))
result = subprocess.call([self._compress] + self.ADD + [arch_name] + result = subprocess.call([self._compress, *self.ADD, arch_name,
files) *files])
if result != 0: if result != 0:
logging.error("Unable to create archive `%s'.", arch_name) logging.error("Unable to create archive `%s'.", arch_name)
return False return False
@@ -134,7 +133,7 @@ class RarArchive(Archive):
class Archivers(object): class Archivers(object):
"""Archivers class""" """Archivers class"""
archivers = [{'arch': TarArchive, 'name': 'tar', 'ext': ['tar']}, archivers = ({'arch': TarArchive, 'name': 'tar', 'ext': ['tar']},
{'arch': TarGzipArchive, 'name': 'tgz', {'arch': TarGzipArchive, 'name': 'tgz',
'ext': ['tar.gz', 'tgz']}, 'ext': ['tar.gz', 'tgz']},
{'arch': TarBzip2Archive, 'name': 'tar.bz2', {'arch': TarBzip2Archive, 'name': 'tar.bz2',
@@ -144,7 +143,7 @@ class Archivers(object):
{'arch': SevenZArchive, 'name': '7z', 'ext': ['7z']}, {'arch': SevenZArchive, 'name': '7z', 'ext': ['7z']},
{'arch': ZipArchive, 'name': 'zip', 'ext': ['zip']}, {'arch': ZipArchive, 'name': 'zip', 'ext': ['zip']},
{'arch': LhaArchive, 'name': 'lha', 'ext': ['lha', 'lzh']}, {'arch': LhaArchive, 'name': 'lha', 'ext': ['lha', 'lzh']},
{'arch': LzxArchive, 'name': 'lzx', 'ext': ['lzx']}] {'arch': LzxArchive, 'name': 'lzx', 'ext': ['lzx']})
@classmethod @classmethod
def get(cls, extension): def get(cls, extension):

View File

@@ -3,19 +3,15 @@ Display message in separate process
""" """
import multiprocessing as mp import multiprocessing as mp
import sys import sys
try: import tkinter
import tkinter as tk import tkinter.ttk
from tkinter import ttk
except ImportError:
import Tkinter as tk
import ttk
class MessageGui(tk.Tk): class MessageGui(tkinter.Tk):
"""Simple gui for displaying a message""" """Simple gui for displaying a message"""
def __init__(self, parent=None, msg=""): def __init__(self, parent=None, msg=""):
tk.Tk.__init__(self, parent) tkinter.Tk.__init__(self, parent)
self.grid() self.grid()
self.resizable(False, False) self.resizable(False, False)
@@ -24,12 +20,13 @@ class MessageGui(tk.Tk):
# Display window without decorations # Display window without decorations
self.wm_attributes('-type', 'splash') self.wm_attributes('-type', 'splash')
self.frame = ttk.Frame(self, padding=5, borderwidth=0) self.frame = tkinter.ttk.Frame(self, padding=5, borderwidth=0)
self.frame.grid() self.frame.grid()
ttk.Label(self.frame, text=msg, relief="ridge", padding=10).grid() tkinter.ttk.Label(self.frame, text=msg, relief="ridge",
padding=10).grid()
if 'linux' in sys.platform: if 'linux' in sys.platform:
style = ttk.Style() style = tkinter.ttk.Style()
style.theme_use('clam') style.theme_use('clam')
def __call__(self): def __call__(self):

View File

@@ -0,0 +1,18 @@
"""
Display message as simple text on console
"""
import sys
class Message:
"""Just a fake message window for systems without TK"""
def __init__(self, msg):
self.msg = msg
self._process = None
def show(self):
sys.stdout.write(self.msg + "\n")
def close(self):
return None

View File

@@ -2,8 +2,7 @@
Simple class for executing fs-uae with specified parameters. This is a Simple class for executing fs-uae with specified parameters. This is a
failsafe class for running fs-uae. failsafe class for running fs-uae.
""" """
from fs_uae_wrapper import base from fs_uae_wrapper import base, utils
from fs_uae_wrapper import utils
class Wrapper(base.Base): class Wrapper(base.Base):
@@ -17,8 +16,8 @@ class Wrapper(base.Base):
def _run_emulator(self): def _run_emulator(self):
"""execute fs-uae""" """execute fs-uae"""
utils.run_command(['fs-uae'] + [self.conf_file] + utils.run_command(['fs-uae', self.conf_file,
self.fsuae_options.list()) *self.fsuae_options.list()])
def clean(self): def clean(self):
"""Do the cleanup. Here - just do nothing""" """Do the cleanup. Here - just do nothing"""

View File

@@ -4,11 +4,15 @@ Misc utilities
import configparser import configparser
import logging import logging
import os import os
import pathlib
import shutil import shutil
import subprocess import subprocess
from fs_uae_wrapper import message
from fs_uae_wrapper import file_archive from fs_uae_wrapper import file_archive
try:
from fs_uae_wrapper.message import Message
except ModuleNotFoundError:
from fs_uae_wrapper.nogui_message import Message
class CmdOption(dict): class CmdOption(dict):
@@ -19,8 +23,8 @@ class CmdOption(dict):
def add(self, option): def add(self, option):
"""parse and add option to the dictionary""" """parse and add option to the dictionary"""
if not option.startswith('--'): if not option.startswith('--'):
raise AttributeError("Cannot add option `%s' to the dictionary" % raise AttributeError(f"Cannot add option {option} to the "
option) f"dictionary")
if '=' in option: if '=' in option:
key, val = option.split('=', 1) key, val = option.split('=', 1)
key = key[2:].strip() key = key[2:].strip()
@@ -35,9 +39,9 @@ class CmdOption(dict):
ret_list = [] ret_list = []
for key, val in self.items(): for key, val in self.items():
if val != '1': if val != '1':
ret_list.append('--%(k)s=%(v)s' % {'k': key, 'v': val}) ret_list.append(f'--{key}={val}')
else: else:
ret_list.append('--%(k)s' % {'k': key}) ret_list.append(f'--{key}')
return ret_list return ret_list
@@ -64,7 +68,7 @@ def operate_archive(arch_name, operation, text, params):
if archiver is None: if archiver is None:
return False return False
msg = message.Message(text) msg = Message(text)
if text: if text:
msg.show() msg.show()
@@ -87,7 +91,7 @@ def create_archive(arch_name, title='', params=None):
""" """
msg = '' msg = ''
if title: if title:
msg = "Creating archive for `%s'. Please be patient" % title msg = f"Creating archive for `{title}'. Please be patient"
return operate_archive(arch_name, 'create', msg, params) return operate_archive(arch_name, 'create', msg, params)
@@ -97,7 +101,7 @@ def extract_archive(arch_name, title='', params=None):
""" """
msg = '' msg = ''
if title: if title:
msg = "Extracting files for `%s'. Please be patient" % title msg = f"Extracting files for `{title}'. Please be patient"
return operate_archive(arch_name, 'extract', msg, params) return operate_archive(arch_name, 'extract', msg, params)
@@ -143,8 +147,9 @@ def interpolate_variables(string, config_path, base=None):
_string = string _string = string
if '$CONFIG' in string: if '$CONFIG' in string:
conf_path = os.path.dirname(os.path.abspath(config_path)) conf_path = pathlib.Path(config_path).resolve().parent
string = os.path.abspath(string.replace('$CONFIG', conf_path)) string = str(pathlib.Path(string.replace('$CONFIG', str(conf_path)))
.resolve())
if '$HOME' in string: if '$HOME' in string:
string = string.replace('$HOME', os.path.expandvars('$HOME')) string = string.replace('$HOME', os.path.expandvars('$HOME'))

136
fs_uae_wrapper/whdload.py Normal file
View File

@@ -0,0 +1,136 @@
"""
Run fs-uae with WHDLoad games
It will use compressed base image and compressed directories.
"""
import logging
import os
from fs_uae_wrapper import base, utils
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(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:
- extract base image and archive file
- copy configuration
- run the emulation
"""
logging.debug("run")
if not super().run():
return False
if not self._extract():
return False
if not self._copy_conf():
return False
return self._run_emulator()
def _validate_options(self):
"""
Do the validation for the options, additionally check if there is
mandatory WHDLoad base OS images set.
"""
if not super()._validate_options():
return False
if not self.all_options.get('wrapper_whdload_base'):
logging.error("wrapper_whdload_base is not set in configuration, "
"exiting.")
return False
return True
def _extract(self):
"""Extract base image and then WHDLoad archive"""
base_image = self.fsuae_options['wrapper_whdload_base']
if not os.path.exists(base_image):
logging.error("Base image `%s` does't exists in provided "
"location.", base_image)
return False
curdir = os.path.abspath('.')
os.chdir(self.dir)
result = utils.extract_archive(base_image)
os.chdir(curdir)
if not result:
return False
if not super()._extract():
return False
return self._find_slave()
def _find_slave(self):
"""Find Slave file and create apropriate entry in S:whdload-startup"""
curdir = os.path.abspath('.')
os.chdir(self.dir)
# find slave name
slave_fname = None
slave_path = None
case_insensitvie_map = {}
# build case insensitive map of paths and find the slave file
for root, dirnames, fnames in os.walk('.'):
for dirname in dirnames:
full_path = os.path.normpath(os.path.join(root, dirname))
case_insensitvie_map[full_path.lower()] = full_path
for fname in fnames:
full_path = os.path.normpath(os.path.join(root, fname))
case_insensitvie_map[full_path.lower()] = full_path
if not slave_fname and fname.lower().endswith('.slave'):
slave_path, slave_fname = os.path.normpath(root), fname
if slave_fname is None:
logging.error("Cannot find .slave file in archive.")
return False
# find corresponfing info (an icon) fname
icon_fname = None
for fname in os.listdir(slave_path):
if (fname.lower().endswith('.info') and
os.path.splitext(slave_fname)[0].lower() ==
os.path.splitext(fname)[0].lower()):
icon_fname = fname
break
if icon_fname is None:
logging.error("Cannot find .info file corresponding to %s in "
"archive.", slave_fname)
return False
# find proper way to handle slave
# 1. check if there are user provided params
contents = f"cd {slave_path}\n"
if self.all_options.get('wrapper_whdload_options'):
contents = (f"{contents}"
f"C:whdload "
f"{self.all_options['wrapper_whdload_options']} "
f"Slave={slave_fname}\n")
else:
# no params, find if kgiconload is available
if case_insensitvie_map.get('c/kgiconload'):
contents = f"{contents}C:kgiconload {icon_fname}\n"
else:
# if not, just add common defaults
contents = (f"{contents}C:whdload Preload "
f"Slave={slave_fname}\n")
fname = os.path.join(case_insensitvie_map.get('s'), 'whdload-startup')
with open(fname, "w") as fobj:
fobj.write(contents)
os.chdir(curdir)
return True

View File

@@ -7,8 +7,7 @@ import logging
import os import os
import sys import sys
from fs_uae_wrapper import utils from fs_uae_wrapper import WRAPPER_KEY, utils
from fs_uae_wrapper import WRAPPER_KEY
def setup_logger(options): def setup_logger(options):
@@ -27,7 +26,8 @@ def setup_logger(options):
level = logging.DEBUG level = logging.DEBUG
logging.basicConfig(level=level, logging.basicConfig(level=level,
format="%(asctime)s %(levelname)s: %(message)s") format="%(asctime)s %(levelname)s\t%(filename)s:"
"%(lineno)d:\t\t%(message)s")
def parse_args(): def parse_args():

50
pyproject.toml Normal file
View File

@@ -0,0 +1,50 @@
[build-system]
requires = ["setuptools >= 77.0", "wheel", "setuptools-git-versioning"]
build-backend = "setuptools.build_meta"
[project]
name = "fs-uae-wrapper"
authors = [{name = "Roman Dobosz", email = "gryf73@gmail.com"}]
license = "BSD-3-Clause"
description = "Automate archives support and state saves for fs-uae"
readme = "README.rst"
requires-python = ">=3.8"
keywords = ["uae", "fs-uae", "amiga", "emulator", "wrapper"]
dynamic = ["version"]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Environment :: Console",
"Intended Audience :: End Users/Desktop",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: System :: Emulators",
"Topic :: Games/Entertainment"
]
[project.urls]
Homepage = "https://github.com/gryf/fs-uae-wrapper"
[project.scripts]
fs-uae-wrapper = "fs_uae_wrapper.wrapper:run"
[tool.setuptools]
packages = ["fs_uae_wrapper"]
[tool.distutils.bdist_wheel]
universal = true
[tool.setuptools-git-versioning]
enabled = true
[tool.ruff.lint]
select = [
"F", # pyflakes
"E", # pycodestyle
"I", # isort
"RUF", # ruff-specific rules
]

View File

@@ -1,36 +0,0 @@
[metadata]
name = fs-uae-wrapper
summary = Automate archives and state for fs-uae
description_file = README.rst
author = Roman Dobosz
author_email = gryf73@gmail.com
url = https://github.com/gryf/fs-uea-wrapper
license = BSD
keywords = uae, fs-uae, amiga, emulator, wrapper
version = 0.8.2
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,5 +0,0 @@
#!/usr/bin/env python
import setuptools
setuptools.setup()

View File

@@ -1,5 +0,0 @@
pytest
pytest-cov
pytest-pep8
coverage
flake8

View File

@@ -1,15 +1,9 @@
import os import os
import shutil import shutil
from tempfile import mkdtemp from tempfile import mkdtemp
from unittest import TestCase from unittest import TestCase, mock
try: from fs_uae_wrapper import archive, utils
from unittest import mock
except ImportError:
import mock
from fs_uae_wrapper import archive
from fs_uae_wrapper import utils
class TestArchive(TestCase): class TestArchive(TestCase):
@@ -26,23 +20,26 @@ class TestArchive(TestCase):
except OSError: except OSError:
pass pass
@mock.patch('fs_uae_wrapper.base.ArchiveBase._get_wrapper_archive_name')
@mock.patch('fs_uae_wrapper.path.which') @mock.patch('fs_uae_wrapper.path.which')
def test_validate_options(self, which): def test_validate_options(self, which, get_wrapper_arch_name):
which.return_value = 'unrar' which.return_value = 'unrar'
arch = archive.Wrapper('Config.fs-uae', utils.CmdOption(), {}) arch = archive.Wrapper('Config.fs-uae', utils.CmdOption(), {})
self.assertFalse(arch._validate_options()) self.assertFalse(arch._validate_options())
arch.all_options = {'wrapper': 'archive'}
arch.all_options['wrapper'] = 'archive' get_wrapper_arch_name.return_value = None
arch.all_options = {'wrapper': 'archive'}
self.assertFalse(arch._validate_options()) self.assertFalse(arch._validate_options())
get_wrapper_arch_name.return_value = 'fake_arch_filename'
arch.all_options['wrapper_archive'] = 'rar' arch.all_options['wrapper_archive'] = 'rar'
self.assertTrue(arch._validate_options()) self.assertTrue(arch._validate_options())
@mock.patch('tempfile.mkdtemp') @mock.patch('tempfile.mkdtemp')
@mock.patch('fs_uae_wrapper.path.which') @mock.patch('fs_uae_wrapper.path.which')
@mock.patch('fs_uae_wrapper.archive.Wrapper._make_archive') @mock.patch('fs_uae_wrapper.archive.Wrapper._make_archive')
@mock.patch('fs_uae_wrapper.base.ArchiveBase._get_wrapper_archive_name')
@mock.patch('fs_uae_wrapper.base.ArchiveBase._save_save') @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._get_saves_dir')
@mock.patch('fs_uae_wrapper.base.ArchiveBase._run_emulator') @mock.patch('fs_uae_wrapper.base.ArchiveBase._run_emulator')
@@ -50,7 +47,8 @@ class TestArchive(TestCase):
@mock.patch('fs_uae_wrapper.base.ArchiveBase._load_save') @mock.patch('fs_uae_wrapper.base.ArchiveBase._load_save')
@mock.patch('fs_uae_wrapper.base.ArchiveBase._extract') @mock.patch('fs_uae_wrapper.base.ArchiveBase._extract')
def test_run(self, extract, load_save, copy_conf, run_emulator, def test_run(self, extract, load_save, copy_conf, run_emulator,
get_save_dir, save_state, make_arch, which, mkdtemp): get_save_dir, save_state, get_wrapper_arch_name, make_arch,
which, mkdtemp):
extract.return_value = False extract.return_value = False
load_save.return_value = False load_save.return_value = False
@@ -58,6 +56,7 @@ class TestArchive(TestCase):
run_emulator.return_value = False run_emulator.return_value = False
get_save_dir.return_value = False get_save_dir.return_value = False
save_state.return_value = False save_state.return_value = False
get_wrapper_arch_name.return_value = "fake_arch_filename"
make_arch.return_value = False make_arch.return_value = False
which.return_value = 'rar' which.return_value = 'rar'

View File

@@ -1,16 +1,10 @@
import os import os
import sys
import shutil import shutil
from tempfile import mkstemp, mkdtemp import sys
from unittest import TestCase from tempfile import mkdtemp, mkstemp
from unittest import TestCase, mock
try: from fs_uae_wrapper import base, utils
from unittest import mock
except ImportError:
import mock
from fs_uae_wrapper import base
from fs_uae_wrapper import utils
class TestBase(TestCase): class TestBase(TestCase):
@@ -94,6 +88,11 @@ class TestBase(TestCase):
bobj._normalize_options() bobj._normalize_options()
self.assertDictEqual(bobj.fsuae_options, {}) self.assertDictEqual(bobj.fsuae_options, {})
get_config.return_value = {'random_item': 10}
bobj.fsuae_options = utils.CmdOption()
bobj._normalize_options()
self.assertDictEqual(bobj.fsuae_options, {})
@mock.patch('os.path.exists') @mock.patch('os.path.exists')
@mock.patch('fs_uae_wrapper.utils.get_config') @mock.patch('fs_uae_wrapper.utils.get_config')
def test_normalize_options_path_not_exists(self, get_config, os_exists): def test_normalize_options_path_not_exists(self, get_config, os_exists):
@@ -286,7 +285,7 @@ class TestBase(TestCase):
which.return_value = '7z' which.return_value = '7z'
bobj.all_options = {'wrapper': 'dummy', bobj.all_options = {'wrapper': 'dummy',
'wrapper_save_state': '1'} 'wrapper_save_state': '1'}
self.assertFalse(bobj._validate_options()) self.assertTrue(bobj._validate_options())
bobj.all_options = {'wrapper': 'dummy', bobj.all_options = {'wrapper': 'dummy',
'wrapper_save_state': '1', 'wrapper_save_state': '1',
@@ -390,16 +389,42 @@ class TestArchiveBase(TestCase):
self.assertFalse(bobj._extract()) self.assertFalse(bobj._extract())
utils_extract.assert_called_once_with(self.fname, '') utils_extract.assert_called_once_with(self.fname, '')
def test_validate_options(self): @mock.patch('fs_uae_wrapper.base.ArchiveBase._get_wrapper_archive_name')
def test_validate_options(self, get_wrapper_arch_name):
bobj = base.ArchiveBase('Config.fs-uae', utils.CmdOption(), {}) bobj = base.ArchiveBase('Config.fs-uae', utils.CmdOption(), {})
bobj.all_options = {} bobj.all_options = {}
self.assertFalse(bobj._validate_options()) self.assertFalse(bobj._validate_options())
get_wrapper_arch_name.return_value = None
bobj.all_options = {'wrapper': 'dummy'} bobj.all_options = {'wrapper': 'dummy'}
self.assertFalse(bobj._validate_options()) self.assertFalse(bobj._validate_options())
bobj.all_options = {'wrapper': 'dummy', bobj.all_options = {'wrapper': 'dummy',
'wrapper_archive': 'myarchive.7z'} 'wrapper_archive': 'myarchive.7z'}
self.assertTrue(bobj._validate_options()) self.assertTrue(bobj._validate_options())
@mock.patch('os.listdir')
def test_get_wrapper_archive_name(self, os_listdir):
os_listdir.return_value = 'no archive among other files'.split()
bobj = base.ArchiveBase('Config.fs-uae', utils.CmdOption(), {})
bobj.all_options = {'wrapper': 'dummy'}
self.assertIsNone(bobj._get_wrapper_archive_name())
os_listdir.return_value = 'no config.rar among other files'.split()
bobj = base.ArchiveBase('Config.fs-uae', utils.CmdOption(), {})
bobj.all_options = {'wrapper': 'dummy'}
self.assertIsNone(bobj._get_wrapper_archive_name())
os_listdir.return_value = 'file Config.TAR among other files'.split()
bobj = base.ArchiveBase('Config.fs-uae', utils.CmdOption(), {})
bobj.all_options = {'wrapper': 'dummy'}
self.assertEqual(bobj._get_wrapper_archive_name(), 'Config.TAR')
os_listdir.return_value = 'Config.lha FooBar_1.24b_20202.7z'.split()
bobj = base.ArchiveBase('FooBar_1.24b_20202.fs-uae',
utils.CmdOption(), {})
bobj.all_options = {'wrapper': 'dummy'}
self.assertEqual(bobj._get_wrapper_archive_name(),
'FooBar_1.24b_20202.7z')

View File

@@ -1,18 +1,13 @@
from unittest import TestCase from unittest import TestCase, mock
try: from fs_uae_wrapper import cd32, utils
from unittest import mock
except ImportError:
import mock
from fs_uae_wrapper import cd32
from fs_uae_wrapper import utils
class TestCD32(TestCase): class TestCD32(TestCase):
@mock.patch('tempfile.mkdtemp') @mock.patch('tempfile.mkdtemp')
@mock.patch('fs_uae_wrapper.path.which') @mock.patch('fs_uae_wrapper.path.which')
@mock.patch('fs_uae_wrapper.base.ArchiveBase._get_wrapper_archive_name')
@mock.patch('fs_uae_wrapper.base.ArchiveBase._save_save') @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._get_saves_dir')
@mock.patch('fs_uae_wrapper.base.ArchiveBase._run_emulator') @mock.patch('fs_uae_wrapper.base.ArchiveBase._run_emulator')
@@ -20,7 +15,8 @@ class TestCD32(TestCase):
@mock.patch('fs_uae_wrapper.base.ArchiveBase._load_save') @mock.patch('fs_uae_wrapper.base.ArchiveBase._load_save')
@mock.patch('fs_uae_wrapper.base.ArchiveBase._extract') @mock.patch('fs_uae_wrapper.base.ArchiveBase._extract')
def test_run(self, extract, load_save, copy_conf, run_emulator, def test_run(self, extract, load_save, copy_conf, run_emulator,
get_save_dir, save_state, which, mkdtemp): get_save_dir, save_state, get_wrapper_arch_name, which,
mkdtemp):
extract.return_value = False extract.return_value = False
copy_conf.return_value = False copy_conf.return_value = False
@@ -28,6 +24,7 @@ class TestCD32(TestCase):
run_emulator.return_value = False run_emulator.return_value = False
get_save_dir.return_value = False get_save_dir.return_value = False
save_state.return_value = False save_state.return_value = False
get_wrapper_arch_name.return_value = "fake_arch_filename"
which.return_value = 'unrar' which.return_value = 'unrar'
acd32 = cd32.Wrapper('Config.fs-uae', utils.CmdOption(), {}) acd32 = cd32.Wrapper('Config.fs-uae', utils.CmdOption(), {})

View File

@@ -1,12 +1,7 @@
import os import os
import shutil import shutil
from tempfile import mkdtemp from tempfile import mkdtemp
from unittest import TestCase from unittest import TestCase, mock
try:
from unittest import mock
except ImportError:
import mock
from fs_uae_wrapper import file_archive from fs_uae_wrapper import file_archive

View File

@@ -1,20 +1,12 @@
from unittest import TestCase
import os import os
from unittest import TestCase, mock
try:
from unittest import mock
except ImportError:
import mock
from fs_uae_wrapper import message from fs_uae_wrapper import message
from fs_uae_wrapper import nogui_message
if os.environ.get('DISPLAY'): if os.environ.get('DISPLAY'):
try:
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
except ImportError:
import Tkinter as tk
import ttk
class TestMessage(TestCase): class TestMessage(TestCase):
@@ -47,6 +39,19 @@ class TestMessage(TestCase):
msg._process.join.assert_called_once() msg._process.join.assert_called_once()
class TestNOPMessage(TestCase):
@mock.patch('sys.stdout.write')
def test_show(self, stdout_write):
msg = nogui_message.Message('display that')
msg.show()
stdout_write.assert_called_once()
def test_close(self):
msg = nogui_message.Message('display that')
self.assertIsNone(msg.close())
if os.environ.get('DISPLAY'): if os.environ.get('DISPLAY'):
# Tkinter needs graphic environment for the widgets # Tkinter needs graphic environment for the widgets
class TestSpawn(TestCase): class TestSpawn(TestCase):

View File

@@ -1,12 +1,6 @@
from unittest import TestCase from unittest import TestCase, mock
try: from fs_uae_wrapper import plain, utils
from unittest import mock
except ImportError:
import mock
from fs_uae_wrapper import plain
from fs_uae_wrapper import utils
class TestPlainModule(TestCase): class TestPlainModule(TestCase):

View File

@@ -1,15 +1,9 @@
import os import os
import shutil import shutil
from tempfile import mkdtemp from tempfile import mkdtemp
from unittest import TestCase from unittest import TestCase, mock
try: from fs_uae_wrapper import savestate, utils
from unittest import mock
except ImportError:
import mock
from fs_uae_wrapper import savestate
from fs_uae_wrapper import utils
class TestSaveState(TestCase): class TestSaveState(TestCase):
@@ -73,7 +67,7 @@ class TestSaveState(TestCase):
self.assertFalse(arch._validate_options()) self.assertFalse(arch._validate_options())
arch.all_options['wrapper'] = 'savestate' arch.all_options['wrapper'] = 'savestate'
self.assertFalse(arch._validate_options()) self.assertTrue(arch._validate_options())
arch.all_options['wrapper_archiver'] = 'rar' arch.all_options['wrapper_archiver'] = 'rar'
self.assertTrue(arch._validate_options()) self.assertTrue(arch._validate_options())

View File

@@ -1,13 +1,8 @@
import os import os
import sys
from tempfile import mkstemp, mkdtemp
from unittest import TestCase
import shutil import shutil
import sys
try: from tempfile import mkdtemp, mkstemp
from unittest import mock from unittest import TestCase, mock
except ImportError:
import mock
from fs_uae_wrapper import utils from fs_uae_wrapper import utils
@@ -237,8 +232,8 @@ class TestCmdOptions(TestCase):
@mock.patch('os.path.exists') @mock.patch('os.path.exists')
@mock.patch('os.getenv') @mock.patch('os.getenv')
@mock.patch('os.path.expandvars') @mock.patch('os.path.expandvars')
@mock.patch('distutils.spawn.find_executable') @mock.patch('shutil.which')
def test_interpolate_variables(self, find_exe, expandv, getenv, os_exists): def test_interpolate_variables(self, which, expandv, getenv, os_exists):
os_exists.return_value = True os_exists.return_value = True
itrpl = utils.interpolate_variables itrpl = utils.interpolate_variables
@@ -252,7 +247,7 @@ class TestCmdOptions(TestCase):
'/home/user') '/home/user')
string = '$APP/$EXE' string = '$APP/$EXE'
find_exe.return_value = '/usr/bin/fs-uae' which.return_value = '/usr/bin/fs-uae'
self.assertEqual(itrpl(string, '/home/user/Config.fs-uae'), self.assertEqual(itrpl(string, '/home/user/Config.fs-uae'),
'/usr/bin/fs-uae//usr/bin/fs-uae') '/usr/bin/fs-uae//usr/bin/fs-uae')

233
tests/test_whdload.py Normal file
View File

@@ -0,0 +1,233 @@
import os
import shutil
from tempfile import mkdtemp
from unittest import TestCase, mock
from fs_uae_wrapper import utils, whdload
class TestWHDLoad(TestCase):
def setUp(self):
self.dirname = mkdtemp()
self.curdir = os.path.abspath(os.curdir)
os.chdir(self.dirname)
def tearDown(self):
os.chdir(self.curdir)
try:
shutil.rmtree(self.dirname)
except OSError:
pass
@mock.patch('fs_uae_wrapper.base.ArchiveBase._validate_options')
def test_validate_options_arch_validation_fail(self, base_valid):
base_valid.return_value = False
wrapper = whdload.Wrapper('Config.fs-uae', utils.CmdOption(), {})
self.assertFalse(wrapper._validate_options())
@mock.patch('fs_uae_wrapper.base.ArchiveBase._validate_options')
def test_validate_options_no_base_image(self, base_valid):
base_valid.return_value = True
wrapper = whdload.Wrapper('Config.fs-uae', utils.CmdOption(), {})
self.assertFalse(wrapper._validate_options())
@mock.patch('fs_uae_wrapper.base.ArchiveBase._validate_options')
def test_validate_options_with_base_image_set(self, base_valid):
base_valid.return_value = True
wrapper = whdload.Wrapper('Config.fs-uae', utils.CmdOption(), {})
wrapper.all_options['wrapper_whdload_base'] = 'fake_base_fname.7z'
self.assertTrue(wrapper._validate_options())
@mock.patch('fs_uae_wrapper.base.ArchiveBase.run')
def test_run_base_run_fail(self, run):
run.return_value = False
wrapper = whdload.Wrapper('Config.fs-uae', utils.CmdOption(), {})
self.assertFalse(wrapper.run())
@mock.patch('fs_uae_wrapper.whdload.Wrapper._extract')
@mock.patch('fs_uae_wrapper.base.ArchiveBase.run')
def test_run_extract_fail(self, run, extract):
run.return_value = True
extract.return_value = False
wrapper = whdload.Wrapper('Config.fs-uae', utils.CmdOption(), {})
wrapper.all_options = {'wrapper': 'whdload',
'wrapper_archive': 'fake.tgz',
'wrapper_archiver': 'rar'}
self.assertFalse(wrapper.run())
@mock.patch('fs_uae_wrapper.base.ArchiveBase._copy_conf')
@mock.patch('fs_uae_wrapper.whdload.Wrapper._extract')
@mock.patch('fs_uae_wrapper.base.ArchiveBase.run')
def test_run_copy_conf_fail(self, run, extract, copy_conf):
run.return_value = True
extract.return_value = True
copy_conf.return_value = False
wrapper = whdload.Wrapper('Config.fs-uae', utils.CmdOption(), {})
self.assertFalse(wrapper.run())
@mock.patch('fs_uae_wrapper.base.ArchiveBase._run_emulator')
@mock.patch('fs_uae_wrapper.base.ArchiveBase._copy_conf')
@mock.patch('fs_uae_wrapper.whdload.Wrapper._extract')
@mock.patch('fs_uae_wrapper.base.ArchiveBase.run')
def test_run_emulator_fail(self, run, extract, copy_conf, run_emulator):
run.return_value = True
extract.return_value = True
copy_conf.return_value = True
run_emulator.return_value = False
wrapper = whdload.Wrapper('Config.fs-uae', utils.CmdOption(), {})
self.assertFalse(wrapper.run())
@mock.patch('fs_uae_wrapper.base.ArchiveBase._run_emulator')
@mock.patch('fs_uae_wrapper.base.ArchiveBase._copy_conf')
@mock.patch('fs_uae_wrapper.whdload.Wrapper._extract')
@mock.patch('fs_uae_wrapper.base.ArchiveBase.run')
def test_run_success(self, run, extract, copy_conf, run_emulator):
run.return_value = True
extract.return_value = True
copy_conf.return_value = True
run_emulator.return_value = True
wrapper = whdload.Wrapper('Config.fs-uae', utils.CmdOption(), {})
self.assertTrue(wrapper.run())
@mock.patch('os.path.exists')
def test_extract_nonexistent_image(self, exists):
exists.return_value = False
wrapper = whdload.Wrapper('Config.fs-uae', utils.CmdOption(), {})
wrapper.fsuae_options['wrapper_whdload_base'] = 'fakefilename'
self.assertFalse(wrapper._extract())
@mock.patch('os.chdir')
@mock.patch('os.path.exists')
def test_extract_extraction_failed(self, exists, chdir):
exists.return_value = True
wrapper = whdload.Wrapper('Config.fs-uae', utils.CmdOption(), {})
wrapper.fsuae_options['wrapper_whdload_base'] = 'fakefilename.7z'
self.assertFalse(wrapper._extract())
@mock.patch('fs_uae_wrapper.base.ArchiveBase._extract')
@mock.patch('fs_uae_wrapper.utils.extract_archive')
@mock.patch('os.chdir')
@mock.patch('os.path.exists')
def test_extract_extraction_of_whdload_arch_failed(self, exists, chdir,
image_extract,
arch_extract):
exists.return_value = True
image_extract.return_value = True
arch_extract.return_value = False
wrapper = whdload.Wrapper('Config.fs-uae', utils.CmdOption(), {})
wrapper.fsuae_options['wrapper_whdload_base'] = 'fakefilename'
self.assertFalse(wrapper._extract())
@mock.patch('fs_uae_wrapper.whdload.Wrapper._find_slave')
@mock.patch('fs_uae_wrapper.base.ArchiveBase._extract')
@mock.patch('fs_uae_wrapper.utils.extract_archive')
@mock.patch('os.chdir')
@mock.patch('os.path.exists')
def test_extract_slave_not_found(self, exists, chdir, image_extract,
arch_extract, find_slave):
exists.return_value = True
image_extract.return_value = True
arch_extract.return_value = True
find_slave.return_value = False
wrapper = whdload.Wrapper('Config.fs-uae', utils.CmdOption(), {})
wrapper.fsuae_options['wrapper_whdload_base'] = 'fakefilename'
self.assertFalse(wrapper._extract())
@mock.patch('fs_uae_wrapper.whdload.Wrapper._find_slave')
@mock.patch('fs_uae_wrapper.base.ArchiveBase._extract')
@mock.patch('fs_uae_wrapper.utils.extract_archive')
@mock.patch('os.chdir')
@mock.patch('os.path.exists')
def test_extract_success(self, exists, chdir, image_extract, arch_extract,
find_slave):
exists.return_value = True
image_extract.return_value = True
arch_extract.return_value = True
find_slave.return_value = True
wrapper = whdload.Wrapper('Config.fs-uae', utils.CmdOption(), {})
wrapper.fsuae_options['wrapper_whdload_base'] = 'fakefilename'
self.assertTrue(wrapper._extract())
@mock.patch('os.walk')
@mock.patch('os.chdir')
def test_find_slave_no_slave_file(self, chdir, walk):
walk.return_value = [(".", ('game'), ()),
('./game', (), ('foo', 'bar', 'baz'))]
wrapper = whdload.Wrapper('Config.fs-uae', utils.CmdOption(), {})
self.assertFalse(wrapper._find_slave())
@mock.patch('os.listdir')
@mock.patch('os.walk')
@mock.patch('os.chdir')
def test_find_slave_no_corresponding_icon(self, chdir, walk, listdir):
contents = ('foo', 'bar', 'baz.slave')
walk.return_value = [(".", ('game'), ()),
('./game', (), contents)]
listdir.return_value = contents
wrapper = whdload.Wrapper('Config.fs-uae', utils.CmdOption(), {})
self.assertFalse(wrapper._find_slave())
@mock.patch('os.listdir')
@mock.patch('os.walk')
@mock.patch('os.chdir')
def test_find_slave_success(self, chdir, walk, listdir):
contents = ('foo', 'bar', 'baz.slave', 'baz.info')
_open = mock.mock_open()
walk.return_value = [(".", ('C', 'S', 'game'), ()),
('./C', (), ('Assign', 'kgiconload')),
('./S', (), ()),
('./game', (), contents)]
listdir.return_value = contents
wrapper = whdload.Wrapper('Config.fs-uae', utils.CmdOption(), {})
with mock.patch('builtins.open', _open):
self.assertTrue(wrapper._find_slave())
handle = _open()
handle.write.assert_called_once_with('cd game\n'
'C:kgiconload baz.info\n')
@mock.patch('os.listdir')
@mock.patch('os.walk')
@mock.patch('os.chdir')
def test_find_slave_minial(self, chdir, walk, listdir):
contents = ('foo', 'bar', 'baz.slave', 'baz.info')
_open = mock.mock_open()
walk.return_value = [(".", ('C', 'S', 'game'), ()),
('./C', (), ('Assign', 'WHDLoad')),
('./S', (), ()),
('./game', (), contents)]
listdir.return_value = contents
wrapper = whdload.Wrapper('Config.fs-uae', utils.CmdOption(), {})
with mock.patch('builtins.open', _open):
self.assertTrue(wrapper._find_slave())
handle = _open()
handle.write.assert_called_once_with('cd game\nC:whdload Preload '
'Slave=baz.slave\n')
@mock.patch('os.listdir')
@mock.patch('os.walk')
@mock.patch('os.chdir')
def test_find_custom_options(self, chdir, walk, listdir):
contents = ('foo', 'bar', 'baz.slave', 'baz.info')
_open = mock.mock_open()
walk.return_value = [(".", ('C', 'S', 'game'), ()),
('./C', (), ('Assign', 'WHDLoad')),
('./S', (), ()),
('./game', (), contents)]
listdir.return_value = contents
wrapper = whdload.Wrapper('Config.fs-uae', utils.CmdOption(), {})
whdl_opts = 'Preload SplashDelay=0 MMU PAL'
wrapper.all_options['wrapper_whdload_options'] = whdl_opts
with mock.patch('builtins.open', _open):
self.assertTrue(wrapper._find_slave())
handle = _open()
handle.write.assert_called_once_with(f'cd game\nC:whdload {whdl_opts} '
'Slave=baz.slave\n')

View File

@@ -1,13 +1,8 @@
import os import os
import sys
from tempfile import mkstemp, mkdtemp
from unittest import TestCase
import shutil import shutil
import sys
try: from tempfile import mkdtemp, mkstemp
from unittest import mock from unittest import TestCase, mock
except ImportError:
import mock
from fs_uae_wrapper import wrapper from fs_uae_wrapper import wrapper
@@ -47,10 +42,7 @@ class TestWrapper(TestCase):
fobj.write('\n') fobj.write('\n')
wrapper.run() wrapper.run()
mock_plain_run.called_once_with('Config.fs-uae', mock_plain_run.assert_called_once()
['--fullscreen',
'--fade_out_duration=0'],
[])
# This will obviously fail for nonexistent module # This will obviously fail for nonexistent module
sys.argv.append('--wrapper=dummy_wrapper') sys.argv.append('--wrapper=dummy_wrapper')

View File

@@ -8,7 +8,12 @@ usedevelop=True
setenv = COVERAGE_FILE = .coverage setenv = COVERAGE_FILE = .coverage
commands = py.test --cov=fs_uae_wrapper --cov-report=term-missing commands = py.test --cov=fs_uae_wrapper --cov-report=term-missing
deps = -r{toxinidir}/test-requirements.txt deps =
pytest
pytest-cov
pytest-pep8
coverage
flake8
[testenv:py3-flake8] [testenv:py3-flake8]
basepython = python3 basepython = python3