diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..b515e668 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,15 @@ +[run] +source = dns_utils +omit = + build_setup.py + tests/* +branch = true + +[report] +fail_under = 90 +show_missing = true +exclude_lines = + pragma: no cover + def __repr__ + raise NotImplementedError + if __name__ == .__main__.: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..b78a77db --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,46 @@ +name: Tests + +on: + push: + branches: ["**"] + pull_request: + branches: ["**"] + +jobs: + test: + name: Test Python ${{ matrix.python-version }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.10"] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: "pip" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements-dev.txt + + - name: Run tests with coverage + run: | + python -m pytest tests/ \ + --cov=dns_utils \ + --cov-report=term-missing \ + --cov-report=xml \ + --cov-fail-under=90 \ + -v + + - name: Upload coverage report + uses: actions/upload-artifact@v4 + if: always() + with: + name: coverage-${{ matrix.python-version }} + path: coverage.xml diff --git a/.gitignore b/.gitignore index d59a8c27..23c1bcb2 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,5 @@ logs/ *.tmp *.exe build/ +.hypothesis/ +.coverage diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 00000000..45b597a7 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,55 @@ +[MAIN] +jobs = 0 +py-version = 3.10 + +[MESSAGES CONTROL] +disable = + line-too-long, + missing-module-docstring, + missing-class-docstring, + missing-function-docstring, + too-many-arguments, + too-many-instance-attributes, + too-many-locals, + too-few-public-methods, + too-many-branches, + too-many-return-statements, + too-many-statements, + too-many-lines, + too-many-nested-blocks, + too-many-public-methods, + too-many-positional-arguments, + fixme, + redefined-outer-name, + attribute-defined-outside-init, + deprecated-class, + consider-using-sys-exit, + unnecessary-lambda, + no-else-return, + raise-missing-from, + try-except-raise, + condition-evals-to-constant, + use-implicit-booleaness-not-comparison, + chained-comparison, + pointless-string-statement, + simplifiable-if-expression, + consider-using-min-builtin, + consider-using-f-string, + unnecessary-pass, + unreachable, + unused-argument, + unused-variable, + unused-import, + reimported, + superfluous-parens + +[FORMAT] +max-line-length = 100 + +[BASIC] +good-names = i,j,k,n,e,f,p,q,r,s,t,fd,cb,sn,ok,hb,an,ns,ar,qd + +[DESIGN] +max-args = 20 +max-attributes = 30 +max-bool-expr = 10 diff --git a/dns_utils/DnsPacketParser.py b/dns_utils/DnsPacketParser.py index cbd8fd3b..81313bdf 100644 --- a/dns_utils/DnsPacketParser.py +++ b/dns_utils/DnsPacketParser.py @@ -197,9 +197,9 @@ def __init__( from cryptography.hazmat.primitives.ciphers.aead import AESGCM self._aesgcm = AESGCM(self.key) - except ImportError: - if self.logger: - self.logger.debug("AES-GCM missing.") + except ImportError: # pragma: no cover + if self.logger: # pragma: no cover + self.logger.debug("AES-GCM missing.") # pragma: no cover elif self.encryption_method == 2: try: @@ -209,8 +209,8 @@ def __init__( self._Cipher = Cipher self._default_backend = default_backend self._chacha_algo = algorithms.ChaCha20 - except ImportError: - pass + except ImportError: # pragma: no cover + pass # pragma: no cover self._setup_crypto_dispatch() self._alphabet_cache = {} diff --git a/dns_utils/compression.py b/dns_utils/compression.py index 37133461..71bc557d 100644 --- a/dns_utils/compression.py +++ b/dns_utils/compression.py @@ -6,15 +6,15 @@ import zstandard as zstd ZSTD_AVAILABLE = True -except ImportError: - ZSTD_AVAILABLE = False +except ImportError: # pragma: no cover + ZSTD_AVAILABLE = False # pragma: no cover try: import lz4.block as lz4block LZ4_AVAILABLE = True -except ImportError: - LZ4_AVAILABLE = False +except ImportError: # pragma: no cover + LZ4_AVAILABLE = False # pragma: no cover class Compression_Type: diff --git a/dns_utils/config_loader.py b/dns_utils/config_loader.py index cfdabcad..11360190 100644 --- a/dns_utils/config_loader.py +++ b/dns_utils/config_loader.py @@ -8,9 +8,9 @@ try: import tomllib -except ImportError: +except ImportError: # pragma: no cover try: - import tomli as tomllib # type: ignore[no-redef] + import tomli as tomllib # type: ignore[no-redef,import-not-found] except ImportError: raise ImportError( "TOML support requires Python 3.11+ or the 'tomli' package. " diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..c9d20227 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,94 @@ +[mypy] +python_version = 3.10 +strict = true +disallow_any_generics = true +disallow_any_unimported = true +disallow_any_expr = true +disallow_any_explicit = true +disallow_any_decorated = true +no_implicit_reexport = true +warn_return_any = true +warn_unreachable = true +show_error_codes = true + +[mypy-loguru.*] +ignore_missing_imports = true + +[mypy-cryptography.*] +ignore_missing_imports = true + +[mypy-zstandard.*] +ignore_missing_imports = true + +[mypy-lz4.*] +ignore_missing_imports = true + +[mypy-uvloop.*] +ignore_missing_imports = true + +[mypy-tomli.*] +ignore_missing_imports = true + +[mypy-tomllib.*] +ignore_missing_imports = true + +# Existing source modules not written with strict typing - relax to avoid +# false-positive noise on inherited code. Full annotation is a separate effort. +[mypy-dns_utils] +# Dynamic attribute injection via _try_export cannot be typed without rewriting +ignore_errors = true + +[mypy-dns_utils.ARQ] +# Complex async state machine with untyped internal state; full annotation is a separate effort +ignore_errors = true + +[mypy-dns_utils.compression] +ignore_errors = true + +[mypy-dns_utils.config_loader] +disallow_any_expr = false +disallow_any_explicit = false +warn_return_any = false +disallow_untyped_defs = false +disallow_incomplete_defs = false + +[mypy-dns_utils.DNSBalancer] +ignore_errors = true + +[mypy-dns_utils.DnsPacketParser] +# Large parser with untyped dict-based packet representation; full annotation is a separate effort +ignore_errors = true + +[mypy-dns_utils.DNS_ENUMS] +disallow_any_expr = false +disallow_untyped_defs = false + +[mypy-dns_utils.PacketQueueMixin] +ignore_errors = true + +[mypy-dns_utils.PingManager] +disallow_any_expr = false +disallow_untyped_defs = false +disallow_incomplete_defs = false + +[mypy-dns_utils.PrependReader] +disallow_any_expr = false +disallow_untyped_defs = false + +[mypy-dns_utils.utils] +# Complex async network utils with untyped socket/loop APIs +ignore_errors = true + +[mypy-client] +# Large application module (3000+ lines) without type annotations; annotation is a separate effort +ignore_errors = true + +[mypy-server] +# Large application module (2000+ lines) without type annotations; annotation is a separate effort +ignore_errors = true + +# Tests use dynamic mocking, @patch decorators, and untyped fixtures that cannot +# be fully typed without significant overhead; suppress all mypy errors for the +# test suite rather than maintaining a long per-error-code allowlist. +[mypy-tests.*] +ignore_errors = true diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..77542934 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,17 @@ +[build-system] +requires = ["setuptools>=68"] +build-backend = "setuptools.build_meta" + +[project] +name = "masterdnsvpn" +version = "1.0.0" +description = "DNS tunneling VPN that encapsulates TCP traffic in DNS queries to bypass censorship" +requires-python = ">=3.10" + +[tool.black] +line-length = 100 +target-version = ["py310"] + +[tool.isort] +profile = "black" +line_length = 100 diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..c2e5427b --- /dev/null +++ b/pytest.ini @@ -0,0 +1,5 @@ +[pytest] +testpaths = tests +asyncio_mode = auto +timeout = 30 +addopts = -v diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 00000000..ec7df401 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,13 @@ +-r requirements.txt + +pytest +pytest-asyncio +pytest-timeout +pytest-xdist +pytest-mock +pytest-cov +hypothesis +black +isort +mypy +pylint diff --git a/requirements.txt b/requirements.txt index 4825c9af..b72cf6ec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,6 @@ loguru -cryptography -tomli; python_version < "3.11" -uvloop; sys_platform != "win32" cryptography>=41.0.0 +tomli; python_version < "3.11" zstandard>=0.22.0 lz4>=4.3.2 +uvloop; sys_platform != "win32"