Skip to content

Commit 713a05c

Browse files
authored
Merge pull request #84 from monkeyman192/integration_tests
Start adding integration tests
2 parents 324cd73 + 5d6a760 commit 713a05c

File tree

21 files changed

+253
-86
lines changed

21 files changed

+253
-86
lines changed

.github/workflows/pipeline.yml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ on:
1818
jobs:
1919
lint_testing:
2020
name: Lint and test code
21-
runs-on: Windows-latest
21+
runs-on: Windows-2025
2222
strategy:
2323
matrix:
2424
py_ver: [
@@ -44,11 +44,18 @@ jobs:
4444
run: |
4545
uv run ruff check ./pymhf ./tests
4646
uv run ruff format --check ./pymhf ./tests
47+
- name: Install compiler
48+
id: install_gpp
49+
uses: rlalik/setup-cpp-compiler@master
50+
with:
51+
compiler: g++-14.2.0
52+
- name: Build test binaries
53+
run: g++ ./tests/programs/src/app.cpp -Wall -o ./tests/programs/app.exe
4754
- name: Run unit tests
4855
run: uv run pytest ./tests
4956
build:
5057
name: Build wheel and docs
51-
runs-on: Windows-latest
58+
runs-on: Windows-2025
5259
needs:
5360
- lint_testing
5461
steps:

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,6 @@ mods
1313
docs/api/*.rst
1414
# Explicitly keep this file as we aren't generating it automatically.
1515
!docs/api/index.rst
16+
17+
# Generated binaries for testing
18+
*.exe

docs/docs/change_log.rst

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ Current (0.1.17.dev)
55
--------------------
66

77
- Further improved partial structs to allow nesting references to themselves as a type (must be "indirect", ie. the type of a pointer, or dynamic array for example).
8-
- Added a fallback method to calculate the binary hash in case opening the file fails. Thanks to [@sparrow](https://github.com/samjviana) for implementing this.
8+
- Added a fallback method to calculate the binary hash in case opening the file fails. Thanks to `@sparrow <https://github.com/samjviana>`_ for implementing this.
9+
- Fixed some inssues around running python files directly with ``pymhf run``
10+
- Added the option to pass command line arguments to the function when ``pymhf`` starts the process itself.
11+
- Added the ``config_overrides`` argument to :py:func:`~pymhf.main.load_mod_file` to allow overriding the static config values.
12+
- Implemented the functionality so that if ``start_paused`` is True, the program will start automatically once injection is completed (no need for manual input any more).
913

1014
0.1.16 (16/08/2025)
1115
-------------------

pymhf/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,8 @@ def run():
225225
if op.isfile(plugin_name) and op.exists(plugin_name):
226226
# In this case we are running in stand-alone mode
227227
standalone = True
228+
if plugin_name.endswith(".py"):
229+
plugin_name = op.realpath(plugin_name)
228230

229231
if op.isdir(plugin_name) and op.exists(plugin_name):
230232
# In this case we are running a library directly from pymhf.

pymhf/core/_internal.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
MOD_SAVE_DIR: str = ""
2020
INCLUDED_ASSEMBLIES: dict[str, str] = {}
2121
CACHE_DIR: str = ""
22+
_SENTINEL_PTR: int = 0
2223

2324
_executor: ThreadPoolExecutor = None # type: ignore
2425

pymhf/core/_types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class pymhfConfig(TypedDict):
2121
exe: NotRequired[str]
2222
pid: NotRequired[int]
2323
steam_guid: NotRequired[int]
24+
args: NotRequired[list[str]]
2425
required_assemblies: NotRequired[list[str]]
2526
start_paused: NotRequired[bool]
2627
default_mod_save_dir: NotRequired[str]

pymhf/core/importing.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,9 @@ def import_file(fpath: str) -> Optional[ModuleType]:
8686
module.__name__ = module_name
8787
module.__spec__ = spec
8888
sys.modules[module_name] = module
89-
spec.loader.exec_module(module)
90-
return module
89+
if spec.loader:
90+
spec.loader.exec_module(module)
91+
return module
9192
else:
9293
print("failed")
9394
except Exception:

pymhf/core/memutils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ def pattern_to_bytes(patt: str) -> bytes:
237237
def _get_binary_info(binary: str) -> Optional[tuple[int, MODULEINFO]]:
238238
if binary not in cache.hm_cache:
239239
try:
240-
pm_process = pymem.Pymem(_internal.EXE_NAME)
240+
pm_process = pymem.Pymem(_internal.EXE_NAME, exact_match=True)
241241
handle = pm_process.process_handle
242242
if ((module := cache.module_map.get(binary)) is None) or (handle is None):
243243
return None

pymhf/core/process.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import _winapi
22
import ctypes
33
import ctypes.wintypes
4+
from subprocess import list2cmdline
45

56
kernel32 = ctypes.WinDLL("kernel32.dll")
67

@@ -27,7 +28,7 @@ class SECURITY_ATTRIBUTES(ctypes.Structure):
2728
]
2829

2930

30-
def start_process(binary_path: str, creationflags: int = 0x0):
31+
def start_process(args: list[str], creationflags: int = 0x0):
3132
# Start an executable similarly to subprocess.Popen
3233
# The functionality here is ripped directly from that implementation,
3334
# however we don't discard the thread_handle here so that we may use it
@@ -37,14 +38,15 @@ def start_process(binary_path: str, creationflags: int = 0x0):
3738
thread_attr = SECURITY_ATTRIBUTES()
3839
thread_attr.bInheritHandle = True
3940
# Start the process the internal way to get the thread handle.
41+
cmd_line = list2cmdline(args)
4042
handle_process, handle_thread, pid, tid = _winapi.CreateProcess(
4143
None,
42-
binary_path,
44+
cmd_line,
4345
ctypes.byref(proc_attr),
4446
ctypes.byref(thread_attr),
4547
False,
4648
creationflags,
47-
None,
49+
None, # type: ignore (it says it wants a dict, but complains if it actually gets one...)
4850
None,
4951
None,
5052
)

pymhf/injected.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,13 @@
6868

6969
_internal.LOAD_TYPE = LoadTypeEnum(_internal.LOAD_TYPE)
7070

71+
# Assign the sentinel and load it from the provided address which is allocated in this process by the
72+
# parent process. If the main process isn't actually waiting on this it won't ever be read, but it's
73+
# simpler to just allocate it anyway.
74+
sentinel = ctypes.c_bool(False)
75+
if _internal._SENTINEL_PTR:
76+
sentinel = ctypes.c_bool.from_address(_internal._SENTINEL_PTR)
77+
7178
_module_path = _internal.MODULE_PATH
7279
if op.isfile(_module_path):
7380
_module_path = op.dirname(_module_path)
@@ -201,7 +208,7 @@ def top_globals(limit: Optional[int] = 10):
201208

202209
executor = ThreadPoolExecutor(2, thread_name_prefix="pyMHF_Internal_Executor")
203210

204-
binary = pymem.Pymem(_internal.EXE_NAME)
211+
binary = pymem.Pymem(_internal.EXE_NAME, exact_match=True)
205212
cache.module_map = {x.name: x for x in pymem.process.enum_process_module(binary.process_handle)}
206213

207214
# Read the imports
@@ -294,6 +301,11 @@ def top_globals(limit: Optional[int] = 10):
294301
futures.append(executor.submit(gui.run))
295302

296303
logging.info(f"Serving on executor {server.sockets[0].getsockname()}")
304+
305+
# Finally, before we run forever, set the sentinel value to True so that if the main process was waiting
306+
# for the injected code to complete before starting the process it can now resume it.
307+
sentinel.value = True
308+
297309
loop.run_forever()
298310

299311
# Close the server.

0 commit comments

Comments
 (0)