diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..78197c6 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,7 @@ +## Description + +## Checklist before merging + +* [ ] applied `ready-to-merge` label +* [ ] updated documentation ([README.md](../README.md)) +* [ ] updated version number ([pyproject.toml](../pyproject.toml)) diff --git a/.github/workflows/define-pylint.yml b/.github/workflows/define-pylint.yml index 8241cd1..002addf 100644 --- a/.github/workflows/define-pylint.yml +++ b/.github/workflows/define-pylint.yml @@ -1,4 +1,4 @@ -name: Define Pylint +name: Define pylint on: workflow_call: @@ -29,7 +29,7 @@ jobs: run: | python -m pip install --upgrade pip pip install pylint - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + if [ -f requirements.txt ]; then pip install --upgrade -r requirements.txt; fi - name: Analysing the code with pylint run: | pylint $(git ls-files '*.py') diff --git a/.github/workflows/define-pytest.yml b/.github/workflows/define-pytest.yml index a2d1579..da38454 100644 --- a/.github/workflows/define-pytest.yml +++ b/.github/workflows/define-pytest.yml @@ -1,4 +1,4 @@ -name: Define Pytest +name: Define pytest on: workflow_call: @@ -6,6 +6,10 @@ on: python-version: required: true type: string + pytest-args: + required: false + type: string + default: "" defaults: run: @@ -29,11 +33,11 @@ jobs: run: | python -m pip install --upgrade pip pip install pytest - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + if [ -f requirements.txt ]; then pip install --upgrade -r requirements.txt; fi - name: Install node uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 - name: Install mineflayer run: | npm install mineflayer @@ -49,6 +53,6 @@ jobs: touch password.txt echo $USERNAME >> password.txt echo $PASSWORD >> password.txt - - name: Run all tests with pytest + - name: Run tests with pytest run: | - pytest -rs + pytest src -rs --skip-linting ${{ inputs.pytest-args }} diff --git a/.github/workflows/release_package.yml b/.github/workflows/release_package.yml index 74bf8b7..183c99c 100644 --- a/.github/workflows/release_package.yml +++ b/.github/workflows/release_package.yml @@ -10,7 +10,7 @@ jobs: pylint: strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] uses: ./.github/workflows/define-pylint.yml with: python-version: ${{ matrix.python-version }} @@ -19,7 +19,7 @@ jobs: pytest: strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] uses: ./.github/workflows/define-pytest.yml with: python-version: ${{ matrix.python-version }} @@ -29,7 +29,7 @@ jobs: runs-on: self-hosted strategy: matrix: - python-version: ["3.13"] + python-version: ["3.14"] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} @@ -56,10 +56,10 @@ jobs: release-tag: ${{ steps.read-tag.outputs.release-tag }} steps: - uses: actions/checkout@v4 - - name: Set up Python 3.13 + - name: Set up Python 3.14 uses: actions/setup-python@v5 with: - python-version: "3.13" + python-version: "3.14" - name: Install dependencies run: | python -m pip install --upgrade pip @@ -73,17 +73,17 @@ jobs: echo "release-tag=$(echo $TAG)" >> $GITHUB_OUTPUT - uses: mukunku/tag-exists-action@v1.6.0 id: check-tag - with: + with: tag: ${{ steps.read-tag.outputs.release-tag }} - name: Fail if tag exists run: | echo "Tag ${{ steps.read-tag.outputs.release-tag }} exists!" exit 1 - if: steps.check-tag.outputs.exists == 'true' + if: steps.check-tag.outputs.exists == 'true' - name: Print tag if it doesn't exist run: | echo "Tag ${{ steps.read-tag.outputs.release-tag }} doesn't yet exist and can be created" - if: steps.check-tag.outputs.exists == 'false' + if: steps.check-tag.outputs.exists == 'false' release: needs: [pylint, pytest, build, tag] diff --git a/.github/workflows/remove-merge-label.yml b/.github/workflows/remove-merge-label.yml new file mode 100644 index 0000000..845fb23 --- /dev/null +++ b/.github/workflows/remove-merge-label.yml @@ -0,0 +1,21 @@ +name: Remove 'ready to merge' label + +on: + pull_request: + types: [closed, synchronize] + +permissions: + contents: read + id-token: write + pull-requests: write + +jobs: + remove_label: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions-ecosystem/action-remove-labels@v1 + if: (github.event.action == 'synchronize') || (github.event.pull_request.merged == true) + with: + labels: 'ready to merge' + fail_on_error: false diff --git a/.github/workflows/run-tests-on-labeled.yml b/.github/workflows/run-tests-on-labeled.yml new file mode 100644 index 0000000..1eb0e23 --- /dev/null +++ b/.github/workflows/run-tests-on-labeled.yml @@ -0,0 +1,26 @@ +name: Run all tests on label apply + +on: + pull_request: + types: [labeled] + +jobs: + pylint: + if: github.event.label.name == 'ready to merge' + strategy: + matrix: + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + uses: ./.github/workflows/define-pylint.yml + with: + python-version: ${{ matrix.python-version }} + secrets: inherit + + pytest: + if: github.event.label.name == 'ready to merge' + strategy: + matrix: + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + uses: ./.github/workflows/define-pytest.yml + with: + python-version: ${{ matrix.python-version }} + secrets: inherit diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index f678ac0..bccb25f 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -1,6 +1,6 @@ name: Run Tests -on: +on: push: branches: [main] pull_request: @@ -10,7 +10,7 @@ jobs: pylint: strategy: matrix: - python-version: ["3.8", "3.13"] + python-version: ["3.14"] uses: ./.github/workflows/define-pylint.yml with: python-version: ${{ matrix.python-version }} @@ -19,7 +19,7 @@ jobs: pytest: strategy: matrix: - python-version: ["3.8", "3.13"] + python-version: ["3.9", "3.14"] needs: pylint if: | (github.event_name == 'push' && github.ref_name == 'main') || @@ -28,4 +28,5 @@ jobs: uses: ./.github/workflows/define-pytest.yml with: python-version: ${{ matrix.python-version }} + pytest-args: "--skip-test-all" secrets: inherit diff --git a/.gitignore b/.gitignore index 6866edb..e605598 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,11 @@ -.vscode/ -venv*/ -testdir/ -testdir_persistent/ -__pycache__/ -mcserverwrapper/test/temp/ -*.egg-info/ -.pytest_cache/ -examples/temp/ -dist/ +*\__pycache__ +*\.pytest_cache +*\.mypy_cache +*\.ruff_cache +*\.vscode +*\venv* +*\dist +*\temp *.pyc +*.egg-info password.txt diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..52f0036 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,39 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace +- repo: https://github.com/codespell-project/codespell + rev: v2.4.1 + hooks: + - id: codespell + files: ^.*\.(py|md|yml)$ + exclude: > + (?x)^( + src/test/fuzzy_test.py| + src/test/alg_test.py + )$ + additional_dependencies: + - tomli +- repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.12.10 + hooks: + - id: ruff-check + args: + - --fix + - --unsafe-fixes +- repo: https://github.com/pycqa/isort + rev: 6.0.1 + hooks: + - id: isort + name: isort (python) +- repo: https://github.com/pycqa/pylint + rev: v3.3.8 + hooks: + - id: pylint + args: + - -d import-error + - -sn diff --git a/.pylintrc b/.pylintrc index 0663811..9ab1247 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,5 +1,8 @@ -[MAIN] -max-line-length=150 +[MASTER] + +init-hook="from pylint.config import find_default_config_files; import os, sys; sys.path.append(f'{os.path.dirname(next(find_default_config_files()))}/src')" + +# ignore=test [MESSAGES CONTROL] @@ -17,11 +20,19 @@ confidence= # no Warning level messages displayed, use "--disable=all --enable=classes # --disable=W". disable=too-many-arguments, + invalid-name, fixme, - bare-except, duplicate-code, too-few-public-methods, - global-statement, + too-many-instance-attributes, + too-many-positional-arguments, + logging-fstring-interpolation, too-many-branches, + too-many-locals, broad-exception-raised, - relative-beyond-top-level + global-statement, + wrong-import-order + +[FORMAT] + +max-line-length=120 diff --git a/CODE_STYLE.md b/CODE_STYLE.md new file mode 100644 index 0000000..ee67268 --- /dev/null +++ b/CODE_STYLE.md @@ -0,0 +1,28 @@ +# CODE STYLE + +To allow for a consistent code style within this repository, some rules are enforced. + +This file documents these rules. + +Note that these are not unchangeable, if given a good reason. + +## imports + +All imports have to be absolute. This is also enforced in the pre-commit hooks. + +## sections order in code files + +The order in which imports, constants, ... are defined in python source code files needs to be unified. + +The different sections have to follow this order: +* module-level docstring (what does this module do / contain) +* standard library imports (e.g. `import os`) +* third-party imports (e.g. `import numpy as np`) +* local imports (e.g. `from abllib import get_logger`) +* optional modules (e.g. `try_import_module("pykakasi")`) +* pylint comments (which checks to ignore in this file) +* module-level logger +* module-level constants +* everything else + +Note that not all sections need to be present. diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index a74be12..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,2 +0,0 @@ -include mcserverwrapper/tests/pytest.ini -recursive-include mcserverwrapper/test *.py diff --git a/README.md b/README.md index 6c1db55..f9c08b0 100644 --- a/README.md +++ b/README.md @@ -57,23 +57,41 @@ Finally, the server is stopped gracefully. More examples can be found in the **examples** folder. -## Run tests locally +## Development environment setup -In order to run tests locally, there are a few things that have to be setup: +If you want to contribute to this project, you need to set up your local environment. -### Add credentials for testing +### Clone the repository -To simulate a player connecting ot the server, it needs to authenticate against microsofts servers. This means, that a microsoft account which owns Minecraft is needed. Don't worry, the credentials are only saved locally. +Run the command +```bash +git clone https://github.com/mcserver-tools/mcserverwrapper +cd mcserverwrapper +``` +in your terminal. -Create a new file named *password.txt* in the repository root and add the following content (without the brackets): +### Install pip packages + +To install all optional as well as development python packages, run the following commands in the project root. + +Windows: +```bash +py -m venv venv +venv\Scripts\activate.bat +pip install -r requirements.txt ``` -(username-here) -(password-here) + +Linux: +```bash +python3 -m venv venv +source venv/bin/activate +pip install -r requirements.txt ``` ### Node Node 20 is needed to use MineFlayer. +The install instructions can be found below: #### Windows @@ -98,9 +116,41 @@ To actually run a minecraft server, a Java 21 JRE needs to be installed and adde Download it from (here)[https://adoptium.net/temurin/releases/]. +### Add credentials for testing + +To simulate a player connecting to the server, it needs to authenticate against microsofts servers. This means, that a microsoft account which owns Minecraft is needed. Don't worry, the credentials are only saved locally. + +Create a new file named *password.txt* in the repository root and add the following content (without the brackets): +``` +(username-here) +(password-here) +``` + ### Run tests After installing all of the requirements, the tests can be ran using ```bash -python -m pytest +python -m pytest src -rs --skip-linting --skip-test-all +``` + +If you want to run tests for all Minecraft versions, remove the `--skip-test-all` argument from the command above. + +> **Warning** +> Beware that testing all versions can take multiple hours! + +### Git pre-commit hooks + +Pre-commit hooks are used to check and autofix formatting issues and typos before you commit your changes. +Once installed, they run automatically if you run `git commit ...`. + +Using these is optional, but encouraged. + +```bash +pip install pre-commit +pre-commit install +``` + +To verify the installation and run all checks: +```bash +pre-commit run --all-files ``` diff --git a/examples/example_01.py b/examples/example_01.py index 24ac1aa..002512b 100644 --- a/examples/example_01.py +++ b/examples/example_01.py @@ -4,6 +4,7 @@ import pathlib import requests + from mcserverwrapper import Wrapper def download_server_jar(): diff --git a/mcserverwrapper/src/__init__.py b/mcserverwrapper/src/__init__.py deleted file mode 100644 index 098d188..0000000 --- a/mcserverwrapper/src/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -"""Export Wrapper class""" - -from .wrapper import Wrapper - -__exports__ = [ - Wrapper -] diff --git a/mcserverwrapper/src/server/__init__.py b/mcserverwrapper/src/server/__init__.py deleted file mode 100644 index 483038c..0000000 --- a/mcserverwrapper/src/server/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -"""Export server classes""" - -from .server_builder import ServerBuilder -from .base_server import BaseServer -from .paper_server import PaperServer -from .vanilla_server import VanillaServer - -__exports__ = [ - ServerBuilder, - BaseServer, - PaperServer, - VanillaServer -] diff --git a/mcserverwrapper/test/helpers/vanilla_helper.py b/mcserverwrapper/test/helpers/vanilla_helper.py deleted file mode 100644 index cba3975..0000000 --- a/mcserverwrapper/test/helpers/vanilla_helper.py +++ /dev/null @@ -1,86 +0,0 @@ -"""Helpers for testing Vanilla servers""" - -import os -from random import randint - -from mcserverwrapper import Wrapper - -from .common_helper import connect_mineflayer, setup_workspace, download_file, assert_port_is_free - -def run_vanilla_test_url(url, offline_mode=False): - """Run all tests for a single vanilla minecraft server url""" - - setup_workspace() - - jarfile = download_file(url) - - run_vanilla_test(jarfile, offline_mode) - -def run_vanilla_test(jarfile, offline_mode=False): - """Run all tests for a single vanilla minecraft server jar""" - - port = 25565 - while not assert_port_is_free(port, False): - port = randint(25500, 25600) - - if not offline_mode: - assert os.path.isfile("password.txt") - assert os.access("password.txt", os.R_OK) - with open("password.txt", "r", encoding="utf8") as f: - assert f.read().replace(" ", "").replace("\n", "") != "", "password.txt is empty" - - start_cmd = f"java -Xmx2G -jar {jarfile} nogui" - - server_property_args = { - "port": port, - "levt": "flat", - "untp": "false" - } - if offline_mode: - server_property_args["onli"] = "false" - - wrapper = Wrapper(os.path.join(os.getcwd(), "testdir", jarfile), server_start_command=start_cmd, print_output=False, - server_property_args=server_property_args) - wrapper.startup() - assert wrapper.server_running() - - while not wrapper.output_queue.empty(): - wrapper.output_queue.get() - - wrapper.send_command("/say Hello World") - - line = "" - while "Hello World" not in line: - line = wrapper.output_queue.get(timeout=10) - - # Mineflayer doesn't yet support 1.20.5+ - # https://github.com/PrismarineJS/mineflayer/issues/3405 - # https://github.com/PrismarineJS/mineflayer/issues/3406 - # MineFlayer doesn't (yet) support 1.7.10 - # https://github.com/PrismarineJS/mineflayer/issues/432 - # the other versions fail because of missing protocol data - if wrapper.get_version().name not in ["1.7.10", - "1.9.1", - "1.9.2", - "1.14.2", - "1.20.5", - "1.20.6", - "1.21", - "1.21.1", - "1.21.2", - "1.21.3", - "1.21.4", - "1.21.5"]: - bot = connect_mineflayer(port=port, offline_mode=offline_mode) - assert bot is not None - - line = "" - while "I spawned" not in line: - line = wrapper.output_queue.get(timeout=5) - - wrapper.send_command("/kick Developer", wait_time=1) - wrapper.server.kill() - - assert not wrapper.server_running() - # assert that the server process really stopped - assert wrapper.server.get_child_status(0.1) is not None diff --git a/pyproject.toml b/pyproject.toml index 175b0cf..5c7c93a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,35 +4,64 @@ build-backend = "setuptools.build_meta" [project] name = "mcserverwrapper" -version = "0.0.3" +version = "0.0.4rc1" authors = [ { name="Ableytner", email="ableytner@gmx.at" }, ] description = "A wrapper for minecraft servers, usable as a python package or from the console." readme = "README.md" -license = { file="LICENSE" } -requires-python = ">=3.10" +license = "GPL-3.0" +license-files = [ + "LICENSE", +] +requires-python = ">=3.9" classifiers = [ - "Programming Language :: Python :: 3", - "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", - "Operating System :: Microsoft :: Windows", + "Development Status :: 4 - Beta", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: 3 :: Only", + "Topic :: Software Development :: Libraries :: Python Modules" ] dependencies = [ - "requests", - "pexpect", - "mcstatus", + "mcstatus==12.0.5", + "pexpect==4.9.0", ] [project.optional-dependencies] dev = [ - "pytest", - "requests", - "bs4", - "pylint", - "javascript", + "bs4==0.0.2", + "javascript==1!1.2.6", + "pre-commit==4.3.0", + "pylint==3.3.9", + "pytest==8.4.2", + "requests==2.32.5", ] [project.urls] Repository = "https://github.com/mcserver-tools/mcserverwrapper" "Bug Tracker" = "https://github.com/mcserver-tools/mcserverwrapper/issues" + +[tool.ruff.lint] +select = ["TID252"] + +[tool.ruff.lint.flake8-tidy-imports] +# Disallow all relative imports. +ban-relative-imports = "all" + +[tool.isort] +lines_after_imports=1 + +[tool.mypy] +strict = false + +[[tool.mypy.overrides]] +module = ["dill.*"] +ignore_missing_imports = true diff --git a/requirements.txt b/requirements.txt index 681f53b..2df400b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,8 @@ -requests -pexpect -mcstatus -bs4 -javascript -pytest -pylint +bs4==0.0.2 +javascript==1!1.2.6 +mcstatus==12.0.5 +pexpect==4.9.0 +pre-commit==4.3.0 +pylint==3.3.9 +pytest==8.4.2 +requests==2.32.5 diff --git a/mcserverwrapper/main.py b/src/main.py similarity index 100% rename from mcserverwrapper/main.py rename to src/main.py diff --git a/mcserverwrapper/__init__.py b/src/mcserverwrapper/__init__.py similarity index 57% rename from mcserverwrapper/__init__.py rename to src/mcserverwrapper/__init__.py index 6d86c00..9394ebd 100644 --- a/mcserverwrapper/__init__.py +++ b/src/mcserverwrapper/__init__.py @@ -1,6 +1,6 @@ """Export Wrapper class""" -from mcserverwrapper.src import Wrapper +from mcserverwrapper.wrapper import Wrapper __exports__ = [ Wrapper diff --git a/mcserverwrapper/src/error.py b/src/mcserverwrapper/error.py similarity index 76% rename from mcserverwrapper/src/error.py rename to src/mcserverwrapper/error.py index 48af946..401302c 100644 --- a/mcserverwrapper/src/error.py +++ b/src/mcserverwrapper/error.py @@ -4,4 +4,4 @@ class McServerWrapperError(Exception): """The base exception, can be used to catch all other McServerWrapper-related errors""" class ServerExitedError(McServerWrapperError): - """An error occuring if the minecraft server unexpectedly crashed""" + """An error occurring if the minecraft server unexpectedly crashed""" diff --git a/mcserverwrapper/src/mcversion.py b/src/mcserverwrapper/mcversion.py similarity index 100% rename from mcserverwrapper/src/mcversion.py rename to src/mcserverwrapper/mcversion.py diff --git a/src/mcserverwrapper/server/__init__.py b/src/mcserverwrapper/server/__init__.py new file mode 100644 index 0000000..63f8f39 --- /dev/null +++ b/src/mcserverwrapper/server/__init__.py @@ -0,0 +1,13 @@ +"""Export server classes""" + +from mcserverwrapper.server.base_server import BaseServer +from mcserverwrapper.server.paper_server import PaperServer +from mcserverwrapper.server.server_builder import ServerBuilder +from mcserverwrapper.server.vanilla_server import VanillaServer + +__exports__ = [ + ServerBuilder, + BaseServer, + PaperServer, + VanillaServer +] diff --git a/mcserverwrapper/src/server/base_server.py b/src/mcserverwrapper/server/base_server.py similarity index 97% rename from mcserverwrapper/src/server/base_server.py rename to src/mcserverwrapper/server/base_server.py index c4b69b7..8d6bd79 100644 --- a/mcserverwrapper/src/server/base_server.py +++ b/src/mcserverwrapper/server/base_server.py @@ -1,4 +1,4 @@ -"""A module contining the base server class""" +"""A module containing the base server class""" from __future__ import annotations @@ -14,9 +14,9 @@ import pexpect from pexpect import popen_spawn -from ..mcversion import McVersion -from ..util import info_getter, logger -from ..error import ServerExitedError +from mcserverwrapper.error import ServerExitedError +from mcserverwrapper.mcversion import McVersion +from mcserverwrapper.util import info_getter, logger class BaseServer: """The base server, containing server type-independent functionality""" diff --git a/mcserverwrapper/src/server/forge_server.py b/src/mcserverwrapper/server/forge_server.py similarity index 94% rename from mcserverwrapper/src/server/forge_server.py rename to src/mcserverwrapper/server/forge_server.py index 01ade09..a259629 100644 --- a/mcserverwrapper/src/server/forge_server.py +++ b/src/mcserverwrapper/server/forge_server.py @@ -6,8 +6,9 @@ import os import re from zipfile import ZipFile -from .base_server import BaseServer -from ..mcversion import McVersion, McVersionType + +from mcserverwrapper.mcversion import McVersion, McVersionType +from mcserverwrapper.server.base_server import BaseServer class ForgeServer(BaseServer): """ diff --git a/mcserverwrapper/src/server/paper_server.py b/src/mcserverwrapper/server/paper_server.py similarity index 87% rename from mcserverwrapper/src/server/paper_server.py rename to src/mcserverwrapper/server/paper_server.py index 85787ae..56abdb4 100644 --- a/mcserverwrapper/src/server/paper_server.py +++ b/src/mcserverwrapper/server/paper_server.py @@ -2,8 +2,8 @@ from __future__ import annotations -from .base_server import BaseServer -from ..mcversion import McVersion, McVersionType +from mcserverwrapper.mcversion import McVersion, McVersionType +from mcserverwrapper.server.base_server import BaseServer class PaperServer(BaseServer): """ diff --git a/mcserverwrapper/src/server/server_builder.py b/src/mcserverwrapper/server/server_builder.py similarity index 93% rename from mcserverwrapper/src/server/server_builder.py rename to src/mcserverwrapper/server/server_builder.py index d0c36e7..7aecc17 100644 --- a/mcserverwrapper/src/server/server_builder.py +++ b/src/mcserverwrapper/server/server_builder.py @@ -5,12 +5,11 @@ import os from pathlib import Path -from mcserverwrapper.src.util import logger - -from .base_server import BaseServer -from .vanilla_server import VanillaServer -from .forge_server import ForgeServer -from ..mcversion import McVersion, McVersionType +from mcserverwrapper.mcversion import McVersion, McVersionType +from mcserverwrapper.server.base_server import BaseServer +from mcserverwrapper.server.forge_server import ForgeServer +from mcserverwrapper.server.vanilla_server import VanillaServer +from mcserverwrapper.util import logger DEFAULT_START_CMD = "java -Xmx4G -Xms4G -jar server.jar nogui" @@ -34,7 +33,7 @@ def from_jar(cls, jar_file: str) -> ServerBuilder: Args: jar_file (str): the full or relative path to the jar file to be used to start the server - + Returns: ServerBuilder: a new ServerBuilder instance """ diff --git a/mcserverwrapper/src/server/vanilla_server.py b/src/mcserverwrapper/server/vanilla_server.py similarity index 95% rename from mcserverwrapper/src/server/vanilla_server.py rename to src/mcserverwrapper/server/vanilla_server.py index 5b88810..ada41cf 100644 --- a/mcserverwrapper/src/server/vanilla_server.py +++ b/src/mcserverwrapper/server/vanilla_server.py @@ -6,8 +6,9 @@ import os import re from zipfile import ZipFile -from .base_server import BaseServer -from ..mcversion import McVersion, McVersionType + +from mcserverwrapper.mcversion import McVersion, McVersionType +from mcserverwrapper.server.base_server import BaseServer class VanillaServer(BaseServer): """Class representing a Vanilla (normal/unmodded) Minecraft server""" diff --git a/mcserverwrapper/src/server_properties_helper.py b/src/mcserverwrapper/server_properties_helper.py similarity index 99% rename from mcserverwrapper/src/server_properties_helper.py rename to src/mcserverwrapper/server_properties_helper.py index e673887..54fb267 100644 --- a/mcserverwrapper/src/server_properties_helper.py +++ b/src/mcserverwrapper/server_properties_helper.py @@ -5,7 +5,7 @@ import os from typing import Any -from .mcversion import McVersion +from mcserverwrapper.mcversion import McVersion ALL_PROPERTIES = [ "port", diff --git a/mcserverwrapper/src/util/__init__.py b/src/mcserverwrapper/util/__init__.py similarity index 58% rename from mcserverwrapper/src/util/__init__.py rename to src/mcserverwrapper/util/__init__.py index 6080966..25c8755 100644 --- a/mcserverwrapper/src/util/__init__.py +++ b/src/mcserverwrapper/util/__init__.py @@ -1,6 +1,6 @@ """Export util classes""" -from . import info_getter, logger +from mcserverwrapper.util import info_getter, logger __exports__ = [ info_getter, diff --git a/mcserverwrapper/src/util/info_getter.py b/src/mcserverwrapper/util/info_getter.py similarity index 68% rename from mcserverwrapper/src/util/info_getter.py rename to src/mcserverwrapper/util/info_getter.py index ab0f16d..57b09be 100644 --- a/mcserverwrapper/src/util/info_getter.py +++ b/src/mcserverwrapper/util/info_getter.py @@ -2,21 +2,8 @@ from __future__ import annotations -import sys - from mcstatus import JavaServer - -# version-specific code doesn't work well with pylnt -# https://github.com/pylint-dev/pylint/issues/7240 -# pylint: disable=import-error, no-name-in-module - -# python versions above 3.9 -if sys.version_info.minor > 9: - from mcstatus.responses import JavaStatusResponse -else: - from mcstatus.status_response import JavaStatusResponse - -# pylint: enable=import-error, no-name-in-module +from mcstatus.responses import JavaStatusResponse def ping_address_with_return(address, port, timeout=3) -> JavaStatusResponse | None: """Pings a given address/port combination and returns the result or None""" @@ -30,7 +17,7 @@ def ping_address_with_return(address, port, timeout=3) -> JavaStatusResponse | N return status except (TimeoutError, ConnectionAbortedError, ConnectionResetError, IOError): return None - # a bug with the library pinging the minecraft server + # a bug within mcstatus, we just retry except KeyError as keyerr: if "text" in keyerr.args or "#" in keyerr.args: print("Retrying...") diff --git a/mcserverwrapper/src/util/logger.py b/src/mcserverwrapper/util/logger.py similarity index 100% rename from mcserverwrapper/src/util/logger.py rename to src/mcserverwrapper/util/logger.py diff --git a/mcserverwrapper/src/wrapper.py b/src/mcserverwrapper/wrapper.py similarity index 96% rename from mcserverwrapper/src/wrapper.py rename to src/mcserverwrapper/wrapper.py index 31d3d60..65e1c8f 100644 --- a/mcserverwrapper/src/wrapper.py +++ b/src/mcserverwrapper/wrapper.py @@ -8,10 +8,10 @@ from threading import Thread from time import sleep -from .util import logger -from .server import ServerBuilder -from .mcversion import McVersion -from ..src import server_properties_helper +from mcserverwrapper import server_properties_helper +from mcserverwrapper.mcversion import McVersion +from mcserverwrapper.server import ServerBuilder +from mcserverwrapper.util import logger class Wrapper(): """The outer shell of the wrapper, handling inputs and outputs""" diff --git a/mcserverwrapper/test/__init__.py b/src/test/__init__.py similarity index 100% rename from mcserverwrapper/test/__init__.py rename to src/test/__init__.py diff --git a/mcserverwrapper/test/conftest.py b/src/test/conftest.py similarity index 55% rename from mcserverwrapper/test/conftest.py rename to src/test/conftest.py index 808585a..f8cadce 100644 --- a/mcserverwrapper/test/conftest.py +++ b/src/test/conftest.py @@ -1,36 +1,55 @@ -# pylint: disable=unused-wildcard-import -# pylint: disable=wildcard-import -# Needed for making pytest fixtures working correctly -# pylint: disable=wrong-import-position, unused-import - -""" - Pytest configuration -""" -import os -import sys -import pathlib +"""Pytest configuration""" -import pytest -from pytest import Metafunc, TestReport, Session, ExitCode - -from .helpers.common_helper import get_mcserver_log -from .integration_tests import test_forge +# pylint: disable=wrong-import-position, wrong-import-order, missing-function-docstring +import os # Adding source path to sys path +import pathlib +import sys + sys.path.append(os.path.join(os.path.dirname(os.path.realpath(__file__)), '../')) sys.path.append(f"{pathlib.Path(__file__).parent.parent}") sys.path.append(f"{pathlib.Path(__file__).parent}") +# pylint: enable=wrong-import-position -test_files_path = os.path.join(pathlib.Path(__file__).parent.resolve(), "temp") -if not os.path.isdir(test_files_path): - os.mkdir(test_files_path) - -from .fixtures import newest_server_jar +from test.helpers.common_helper import get_mcserver_log, get_vanilla_urls +from test.integration_tests import test_forge -from .helpers.common_helper import get_vanilla_urls -# pylint: enable=wrong-import-position +import pytest -def pytest_generate_tests(metafunc: Metafunc): +os.environ["DEBUG"] = "True" + +if not os.path.isdir("temp"): + os.mkdir("temp") + +def pytest_addoption(parser: pytest.Parser): + parser.addoption( + "--skip-linting", action="store_true", default=False, help="skip the pylint test" + ) + parser.addoption( + "--skip-test-all", action="store_true", default=False, help="skip testing all supported minecraft versions" + ) + +def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item]): + if config.getoption("--skip-linting"): + skip_pylint = pytest.mark.skip(reason="skipping code linting due to --skip-linting arg") + for item in items: + if item.name in ["test_pylint", "test_mypy"]: + item.add_marker(skip_pylint) + + if config.getoption("--skip-test-all"): + skip_pylint = pytest.mark.skip(reason="skipping testing all minecraft versions due to --skip-test-all arg") + for item in items: + if item.name.startswith("test_all[") or item.name.startswith("test_multiple["): + item.add_marker(skip_pylint) + + skip_pylint = pytest.mark.skip(reason="skipping testing in online mode as " + "https://github.com/PrismarineJS/prismarine-auth/pull/137 is not yet merged") + for item in items: + if item.name in ["test_single_online", "test_mineflayer"]: + item.add_marker(skip_pylint) + +def pytest_generate_tests(metafunc: pytest.Metafunc): """Pytest hook""" if "jar_version_tuple" in metafunc.fixturenames: @@ -58,7 +77,7 @@ def pytest_runtest_makereport(item, call): """Save mcserverwrapper.log to the current pytest item""" # execute all other hooks to obtain the report object - rep: TestReport = yield + rep: pytest.TestReport = yield # we only look at actual failing test calls, not setup/teardown if rep.when == "call" and rep.outcome == "failed": @@ -67,7 +86,7 @@ def pytest_runtest_makereport(item, call): return rep @pytest.hookimpl(trylast=True) -def pytest_sessionfinish(session: Session, exitstatus: ExitCode): +def pytest_sessionfinish(session: pytest.Session, exitstatus: pytest.ExitCode): """Print all saved mcserverwrapper.log after all tests finished""" if len(session.items) == 0: @@ -89,3 +108,6 @@ def pytest_sessionfinish(session: Session, exitstatus: ExitCode): for line in item.mcserverlog.split("\n"): print(line) # pylint: enable=unused-argument + +# pylint: disable-next=unused-wildcard-import, wildcard-import, wrong-import-order +from test.fixtures import * diff --git a/mcserverwrapper/test/fixtures.py b/src/test/fixtures.py similarity index 69% rename from mcserverwrapper/test/fixtures.py rename to src/test/fixtures.py index 407962f..fec17b8 100644 --- a/mcserverwrapper/test/fixtures.py +++ b/src/test/fixtures.py @@ -1,21 +1,20 @@ """Defines pytest fixtures""" -import shutil import os +import shutil +from test.helpers import common_helper import pytest import requests -from .helpers import common_helper - @pytest.fixture def newest_server_jar(): """Download the newest server jar version and return the jar file path""" url = "https://piston-data.mojang.com/v1/objects/8dd1a28015f51b1803213892b50b7b4fc76e594d/server.jar" - filename = os.path.join("testdir_persistent", "server.jar") + filename = os.path.join("temp", "testdir_persistent", "server.jar") - os.makedirs("testdir_persistent", exist_ok=True) + os.makedirs(os.path.join("temp", "testdir_persistent"), exist_ok=True) if not os.path.isfile(filename): req = requests.get(url, timeout=5) @@ -24,7 +23,7 @@ def newest_server_jar(): common_helper.setup_workspace() - testdir_filename = os.path.join("testdir", "server.jar") + testdir_filename = os.path.join("temp", "testdir", "server.jar") shutil.copyfile(filename, testdir_filename) return "server.jar" diff --git a/mcserverwrapper/test/helpers/common_helper.py b/src/test/helpers/common_helper.py similarity index 83% rename from mcserverwrapper/test/helpers/common_helper.py rename to src/test/helpers/common_helper.py index c64e740..1f20896 100644 --- a/mcserverwrapper/test/helpers/common_helper.py +++ b/src/test/helpers/common_helper.py @@ -1,22 +1,21 @@ """Common helper functions used during testing""" -from datetime import datetime, timedelta import errno -import socket import os +import re import shutil +import socket +from datetime import datetime, timedelta from threading import Thread from time import sleep -import re import pytest -from bs4 import BeautifulSoup import requests +from bs4 import BeautifulSoup +from javascript import once, require from requests.adapters import HTTPAdapter, Retry -from javascript import require, once - -from mcserverwrapper.src.util import logger +from mcserverwrapper.util import logger mineflayer = require('mineflayer') @@ -24,9 +23,9 @@ def setup_workspace(): """Setup the testing folder""" try: - if os.path.isdir("testdir"): - shutil.rmtree("testdir") - os.makedirs("testdir", exist_ok=True) + if os.path.isdir(os.path.join("temp", "testdir")): + shutil.rmtree(os.path.join("temp", "testdir")) + os.makedirs(os.path.join("temp", "testdir"), exist_ok=True) except PermissionError as e: pytest.skip("cannot access testdir") @@ -36,11 +35,11 @@ def setup_workspace(): def reset_workspace(): """Delete everything inside the testing folder""" - for entry in os.listdir("testdir"): - if os.path.isfile(os.path.join("testdir", entry)): - os.remove(os.path.join("testdir", entry)) + for entry in os.listdir(os.path.join("temp", "testdir")): + if os.path.isfile(os.path.join("temp", "testdir", entry)): + os.remove(os.path.join("temp", "testdir", entry)) else: - shutil.rmtree(os.path.join("testdir", entry)) + shutil.rmtree(os.path.join("temp", "testdir", entry)) def assert_port_is_free(port: int = 25565, strict=True) -> bool: """Skips the current test if the given port is not free""" @@ -78,14 +77,14 @@ def connect_mineflayer(address = "127.0.0.1", port = 25565, offline_mode=False): 'auth': 'microsoft', 'username': password[0], 'password': password[1], - 'hideErrors': False + 'hideErrors': False }) else: bot = mineflayer.createBot({ 'host': address, 'port': port, 'username': "Developer", - 'hideErrors': False + 'hideErrors': False }) bot_connected = [False] @@ -98,8 +97,8 @@ def func(bot, bot_connected): start_time = datetime.now() while not bot_connected[0]: - if (datetime.now() - start_time) > timedelta(seconds=30): - pytest.skip(f"Bot connection to {address}:{port} timed out") + if (datetime.now() - start_time) > timedelta(seconds=60): + raise Exception(f"Bot connection to {address}:{port} timed out") sleep(0.1) bot.chat('I spawned') @@ -109,7 +108,7 @@ def func(bot, bot_connected): def download_file(url, counter=""): """ Download the file from the given url and return its path - + Retry code from https://stackoverflow.com/a/35504626/15436169 """ @@ -120,7 +119,7 @@ def download_file(url, counter=""): backoff_factor=0.1) s.mount("https://", HTTPAdapter(max_retries=retries)) req = s.get(url, timeout=30) - with open(os.path.join("testdir", local_filename), 'wb') as file: + with open(os.path.join("temp", "testdir", local_filename), 'wb') as file: file.write(req.content) return local_filename @@ -131,6 +130,10 @@ def get_mcserver_log() -> str: print("Logger was not yet setup, cannot print logfile") return "" + if not os.path.isfile(logger.logfile_path): + print("logfile doesn't exist, cannot print logfile") + return "" + data = f"Printing out {logger.LOGFILE_NAME}:\n" with open(logger.logfile_path, "r", encoding="utf8") as f: for line in f.readlines(): @@ -169,7 +172,7 @@ def _version_valid(version): return False if vers_split[1] == 7 and (len(vers_split) < 3 or vers_split[2] < 10): return False - if ".".join([str(item) for item in vers_split]) in ["1.8"]: + if ".".join([str(item) for item in vers_split]) in ["1.8", "1.21.9"]: return False return True diff --git a/mcserverwrapper/test/helpers/forge_helper.py b/src/test/helpers/forge_helper.py similarity index 65% rename from mcserverwrapper/test/helpers/forge_helper.py rename to src/test/helpers/forge_helper.py index 9b7f991..2563bdd 100644 --- a/mcserverwrapper/test/helpers/forge_helper.py +++ b/src/test/helpers/forge_helper.py @@ -1,22 +1,22 @@ """Helpers for testing Forge servers""" import os -from random import randint import re import subprocess +from random import randint +from test.helpers.common_helper import (assert_port_is_free, download_file, + setup_workspace) import pytest from mcserverwrapper import Wrapper -from mcserverwrapper.src.error import ServerExitedError - -from .common_helper import assert_port_is_free, download_file, setup_workspace +from mcserverwrapper.error import ServerExitedError def install_forge(url: str): """Install a forge server from a given installer download url""" installer_jar = download_file(url) - testdir = os.path.join(os.getcwd(), "testdir") + testdir = os.path.join(os.getcwd(), "temp", "testdir") with subprocess.Popen(["java", "-jar", installer_jar, "--installServer"], stdout=subprocess.DEVNULL, @@ -65,29 +65,34 @@ def run_forge_test(jarfile, offline_mode=False): if offline_mode: server_property_args["onli"] = "false" - wrapper = Wrapper(os.path.join(os.getcwd(), "testdir", jarfile), server_start_command=start_cmd, print_output=False, + wrapper = Wrapper(os.path.join(os.getcwd(), "temp", "testdir", jarfile), + server_start_command=start_cmd, + print_output=False, server_property_args=server_property_args) try: wrapper.startup() except ServerExitedError: - # server exited because of invalid java verison + # server exited because of invalid java version pytest.skip("Server exited likely due to wrong java version") - assert wrapper.server_running() + try: + assert wrapper.server_running() - while not wrapper.output_queue.empty(): - wrapper.output_queue.get() + while not wrapper.output_queue.empty(): + wrapper.output_queue.get() - wrapper.send_command("/say Hello World") + wrapper.send_command("/say Hello World") - line = "" - while "Hello World" not in line: - line = wrapper.output_queue.get(timeout=10) + line = "" + while "Hello World" not in line: + line = wrapper.output_queue.get(timeout=10) - wrapper.send_command("/kick Developer", wait_time=1) - wrapper.server.kill() + wrapper.server.stop() - assert not wrapper.server_running() - # assert that the server process really stopped - assert wrapper.server.get_child_status(0.1) is not None + assert not wrapper.server_running() + # assert that the server process really stopped + assert wrapper.server.get_child_status(0.1) is not None + except BaseException: + wrapper.server.kill() + raise diff --git a/src/test/helpers/vanilla_helper.py b/src/test/helpers/vanilla_helper.py new file mode 100644 index 0000000..e852216 --- /dev/null +++ b/src/test/helpers/vanilla_helper.py @@ -0,0 +1,95 @@ +"""Helpers for testing Vanilla servers""" + +import os +from random import randint +from test.helpers.common_helper import (assert_port_is_free, + connect_mineflayer, download_file, + setup_workspace) + +from mcserverwrapper import Wrapper + +def run_vanilla_test_url(url, offline_mode=False): + """Run all tests for a single vanilla minecraft server url""" + + setup_workspace() + + jarfile = download_file(url) + + run_vanilla_test(jarfile, offline_mode) + +def run_vanilla_test(jarfile, offline_mode=False): + """Run all tests for a single vanilla minecraft server jar""" + + port = 25565 + while not assert_port_is_free(port, False): + port = randint(25500, 25600) + + if not offline_mode: + assert os.path.isfile("password.txt") + assert os.access("password.txt", os.R_OK) + with open("password.txt", "r", encoding="utf8") as f: + assert f.read().replace(" ", "").replace("\n", "") != "", "password.txt is empty" + + start_cmd = f"java -Xmx2G -jar {jarfile} nogui" + + server_property_args = { + "port": port, + "levt": "flat", + "untp": "false" + } + if offline_mode: + server_property_args["onli"] = "false" + + wrapper = Wrapper(os.path.join(os.getcwd(), "temp", "testdir", jarfile), + server_start_command=start_cmd, + print_output=False, + server_property_args=server_property_args) + wrapper.startup() + + try: + assert wrapper.server_running() + + while not wrapper.output_queue.empty(): + wrapper.output_queue.get() + + wrapper.send_command("/say Hello World") + + line = "" + while "Hello World" not in line: + line = wrapper.output_queue.get(timeout=10) + + # Mineflayer doesn't yet support 1.20.5+ + # https://github.com/PrismarineJS/mineflayer/issues/3405 + # https://github.com/PrismarineJS/mineflayer/issues/3406 + # MineFlayer doesn't (yet) support 1.7.10 + # https://github.com/PrismarineJS/mineflayer/issues/432 + # the other versions fail because of missing protocol data + if wrapper.get_version().name not in ["1.7.10", + "1.9.1", + "1.9.2", + "1.14.2", + "1.20.5", + "1.20.6", + "1.21", + "1.21.1", + "1.21.2", + "1.21.3", + "1.21.4", + "1.21.5"]: + bot = connect_mineflayer(port=port, offline_mode=offline_mode) + assert bot is not None + + line = "" + while "I spawned" not in line: + line = wrapper.output_queue.get(timeout=5) + + wrapper.send_command("/kick Developer", wait_time=1) + + wrapper.server.stop() + + assert not wrapper.server_running() + # assert that the server process really stopped + assert wrapper.server.get_child_status(0.1) is not None + except BaseException: + wrapper.server.kill() + raise diff --git a/mcserverwrapper/test/integration_tests/__init__.py b/src/test/integration_tests/__init__.py similarity index 100% rename from mcserverwrapper/test/integration_tests/__init__.py rename to src/test/integration_tests/__init__.py diff --git a/mcserverwrapper/test/integration_tests/test_forge.py b/src/test/integration_tests/test_forge.py similarity index 90% rename from mcserverwrapper/test/integration_tests/test_forge.py rename to src/test/integration_tests/test_forge.py index 1f77bbe..765aaac 100644 --- a/mcserverwrapper/test/integration_tests/test_forge.py +++ b/src/test/integration_tests/test_forge.py @@ -1,7 +1,8 @@ """Module containing tests for Forge servers""" -from ..helpers.forge_helper import run_forge_test_url +from test.helpers.forge_helper import run_forge_test_url +# pylint: disable=line-too-long FORGE_URLS = { "1.20.4": "https://maven.minecraftforge.net/net/minecraftforge/forge/1.20.4-49.0.31/forge-1.20.4-49.0.31-installer.jar", "1.20.3": "https://maven.minecraftforge.net/net/minecraftforge/forge/1.20.3-49.0.2/forge-1.20.3-49.0.2-installer.jar", @@ -12,11 +13,12 @@ "1.8.9": "https://maven.minecraftforge.net/net/minecraftforge/forge/1.8.9-11.15.1.2318-1.8.9/forge-1.8.9-11.15.1.2318-1.8.9-installer.jar", "1.7.10": "https://maven.minecraftforge.net/net/minecraftforge/forge/1.7.10-10.13.4.1614-1.7.10/forge-1.7.10-10.13.4.1614-1.7.10-installer.jar" } +# pylint: enable=line-too-long def test_multiple(forge_download_url: str): """Test multiple supported Forge server versions""" - run_forge_test_url(forge_download_url) + run_forge_test_url(forge_download_url, offline_mode=True) def test_single_online(): """Test a single Forge version in online mode""" diff --git a/mcserverwrapper/test/integration_tests/test_vanilla.py b/src/test/integration_tests/test_vanilla.py similarity index 67% rename from mcserverwrapper/test/integration_tests/test_vanilla.py rename to src/test/integration_tests/test_vanilla.py index d5b8478..4791c19 100644 --- a/mcserverwrapper/test/integration_tests/test_vanilla.py +++ b/src/test/integration_tests/test_vanilla.py @@ -1,17 +1,19 @@ """Module containing tests for Vanilla servers""" import os +from datetime import datetime, timedelta from random import randint +from test.helpers.common_helper import (assert_port_is_free, + connect_mineflayer, download_file, + setup_workspace) +from test.helpers.vanilla_helper import run_vanilla_test, run_vanilla_test_url +from test.testable_thread import TestableThread from time import sleep -from datetime import datetime, timedelta import pytest +from urllib3.exceptions import ReadTimeoutError -from mcserverwrapper import Wrapper -from mcserverwrapper.src import error -from ..helpers.common_helper import assert_port_is_free, download_file, connect_mineflayer, setup_workspace -from ..helpers.vanilla_helper import run_vanilla_test, run_vanilla_test_url -from ..testable_thread import TestableThread +from mcserverwrapper import Wrapper, error def test_all(jar_version_tuple): """Tests all of the vanilla minecraft versions""" @@ -31,6 +33,8 @@ def test_all(jar_version_tuple): raise TimeoutError("Test timed out") thread.join() + except ReadTimeoutError: + pytest.skip(f"Testing version {name} skipped: server jar download timed out") except TimeoutError as timeout_err: if "Test timed out" in timeout_err.args: pytest.fail(f"Testing version {name} timed out") @@ -47,7 +51,7 @@ def _test_download_all_jars(jar_download_url): setup_workspace() jarfile = download_file(url) - assert os.path.isfile(os.path.join("testdir", jarfile)) + assert os.path.isfile(os.path.join("temp", "testdir", jarfile)) def _test_broken_versions(): setup_workspace() @@ -90,40 +94,46 @@ def test_mineflayer(newest_server_jar): "untp": "false" } - wrapper = Wrapper(os.path.join(os.getcwd(), "testdir", newest_server_jar), + wrapper = Wrapper(os.path.join(os.getcwd(), "temp", "testdir", newest_server_jar), server_start_command=start_cmd, server_property_args=server_params, print_output=False) wrapper.startup() - assert wrapper.server_running() - while not wrapper.output_queue.empty(): - wrapper.output_queue.get() - wrapper.send_command("/say Hello World") + try: + assert wrapper.server_running() + while not wrapper.output_queue.empty(): + wrapper.output_queue.get() + + wrapper.send_command("/say Hello World") + + line = "" + while "Hello World" not in line: + line = wrapper.output_queue.get(timeout=5) - line = "" - while "Hello World" not in line: - line = wrapper.output_queue.get(timeout=5) + bot = connect_mineflayer(port=port) + assert bot is not None - bot = connect_mineflayer(port=port) - assert bot is not None + line = "" + while "I spawned" not in line: + line = wrapper.output_queue.get(timeout=5) - line = "" - while "I spawned" not in line: - line = wrapper.output_queue.get(timeout=5) + wrapper.send_command("/kick Developer", wait_time=1) - wrapper.send_command("/kick Developer", wait_time=1) - wrapper.server.kill() + wrapper.server.stop() - # assert that the server process really stopped - assert wrapper.server.get_child_status(0.1) is not None + # assert that the server process really stopped + assert wrapper.server.get_child_status(0.1) is not None + except BaseException: + wrapper.server.kill() + raise def test_invalid_start_params(newest_server_jar): """Test a server with an invalid startup command""" start_cmd = f"java -Xmx2G -jar {newest_server_jar}nogui" - wrapper = Wrapper(os.path.join(os.getcwd(), "testdir", newest_server_jar), + wrapper = Wrapper(os.path.join(os.getcwd(), "temp", "testdir", newest_server_jar), server_start_command=start_cmd, print_output=False) diff --git a/mcserverwrapper/test/module_tests/__init__.py b/src/test/module_tests/__init__.py similarity index 100% rename from mcserverwrapper/test/module_tests/__init__.py rename to src/test/module_tests/__init__.py diff --git a/mcserverwrapper/test/module_tests/test_server_properties_helper.py b/src/test/module_tests/test_server_properties_helper.py similarity index 83% rename from mcserverwrapper/test/module_tests/test_server_properties_helper.py rename to src/test/module_tests/test_server_properties_helper.py index d1c2a6a..e6ad072 100644 --- a/mcserverwrapper/test/module_tests/test_server_properties_helper.py +++ b/src/test/module_tests/test_server_properties_helper.py @@ -1,10 +1,9 @@ """Test the server_properties_helper methods""" -import pathlib import os -from mcserverwrapper.src.mcversion import McVersion, McVersionType -from ...src import server_properties_helper as sph +from mcserverwrapper import server_properties_helper as sph +from mcserverwrapper.mcversion import McVersion, McVersionType def test_get_mixed_params(): """Tests the helper with mixed params""" @@ -18,11 +17,10 @@ def test_get_mixed_params(): "use-native-transport": "false" } - props_path = os.path.join(pathlib.Path(__file__).parent.parent.resolve(), "temp") - with open(os.path.join(props_path, "server.properties"), "w+", encoding="utf8") as props_file: + with open(os.path.join("temp", "server.properties"), "w+", encoding="utf8") as props_file: props_file.write("\n".join([f"{key}={value}" for key, value in props.items()])) - result = sph.get_properties(props_path, McVersion("1.20", McVersionType.VANILLA)) + result = sph.get_properties("temp", McVersion("1.20", McVersionType.VANILLA)) assert len(result) == 5 assert result["maxp"] == props["max-players"] @@ -90,8 +88,7 @@ def test_params_with_file(): "a-final-prop=aha\n" + \ "online-mode=true\n" - props_path = os.path.join(pathlib.Path(__file__).parent.parent.resolve(), "temp") - with open(os.path.join(props_path, "server.properties"), "w+", encoding="utf8") as props_file: + with open(os.path.join("temp", "server.properties"), "w+", encoding="utf8") as props_file: props_file.write(props_test_data) props = { @@ -99,7 +96,7 @@ def test_params_with_file(): "untp": "true" } - result = sph.parse_properties_args(props_path, props, McVersion("1.20", McVersionType.VANILLA)) + result = sph.parse_properties_args("temp", props, McVersion("1.20", McVersionType.VANILLA)) # ensure that props didn't change assert len(props) == 2 @@ -176,8 +173,7 @@ def test_save_existing(): "a-final-prop=aha\n" + \ "online-mode=false" - props_path = os.path.join(pathlib.Path(__file__).parent.parent.resolve(), "temp") - with open(os.path.join(props_path, "server.properties"), "w+", encoding="utf8") as props_file: + with open(os.path.join("temp", "server.properties"), "w+", encoding="utf8") as props_file: props_file.write(props_test_data) props = { @@ -188,7 +184,7 @@ def test_save_existing(): "untp": "false" } - sph.save_properties(props_path, props) + sph.save_properties("temp", props) # ensure that props didn't change assert len(props) == 5 @@ -198,7 +194,7 @@ def test_save_existing(): assert props["levt"] == sph.DEFAULT_LEVEL_TYPE_POST_1_19 assert props["untp"] == "false" - with open(os.path.join(props_path, "server.properties"), "r", encoding="utf8") as props_file: + with open(os.path.join("temp", "server.properties"), "r", encoding="utf8") as props_file: lines = props_file.read().splitlines() assert len(lines) == 7 @@ -216,8 +212,7 @@ def test_save_new(): props_test_data = "some-other-prop=haha\n" + \ "a-final-prop=aha\n" - props_path = os.path.join(pathlib.Path(__file__).parent.parent.resolve(), "temp") - with open(os.path.join(props_path, "server.properties"), "w+", encoding="utf8") as props_file: + with open(os.path.join("temp", "server.properties"), "w+", encoding="utf8") as props_file: props_file.write(props_test_data) props = { @@ -228,7 +223,7 @@ def test_save_new(): "untp": "true" } - sph.save_properties(props_path, props) + sph.save_properties("temp", props) # ensure that props didn't change assert len(props) == 5 @@ -238,7 +233,7 @@ def test_save_new(): assert props["levt"] == "minecraft\\:normal" assert props["untp"] == "true" - with open(os.path.join(props_path, "server.properties"), "r", encoding="utf8") as props_file: + with open(os.path.join("temp", "server.properties"), "r", encoding="utf8") as props_file: lines = props_file.read().splitlines() assert len(lines) == 7 diff --git a/src/test/pytest.ini b/src/test/pytest.ini new file mode 100644 index 0000000..0f1738f --- /dev/null +++ b/src/test/pytest.ini @@ -0,0 +1,10 @@ +[pytest] +addopts=--tb short + --color=yes + --show-capture all +;print output to console + -s +log_format = [%(levelname)s] %(filename)s @%(lineno)d: %(message)s +;log_date_format=%Y/%m/%d %H:%M:%S +log_cli = true +log_cli_level = DEBUG diff --git a/mcserverwrapper/test/testable_thread.py b/src/test/testable_thread.py similarity index 100% rename from mcserverwrapper/test/testable_thread.py rename to src/test/testable_thread.py