Skip to content

Commit 47f5868

Browse files
authored
Merge pull request #67 from tcalmant/tests-review
Increase test coverage, fix modified UTF-8/__hash__ bugs, containerize CI
2 parents f009750 + b48824b commit 47f5868

16 files changed

Lines changed: 2278 additions & 78 deletions

‎.coveragerc‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,13 @@ source = javaobj/
33

44
[report]
55
include = javaobj/*
6+
7+
[paths]
8+
# The test suite is run in a container for each supported Python version (see
9+
# run_tests_containers.sh), where the tree is unpacked in /work. The data
10+
# files are then combined outside of the containers: this maps the
11+
# in-container paths back to the local checkout, so a single report covers
12+
# every version.
13+
source =
14+
javaobj/
15+
/work/javaobj/

‎.github/workflows/build-24.04.yml‎

Lines changed: 0 additions & 57 deletions
This file was deleted.

‎.github/workflows/ci-build.yml‎

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# Lints the code, runs the tests on every supported Python version, and
2+
# reports coverage.
3+
#
4+
# The tests run inside the matching official ``python:<version>`` container
5+
# via ./run_tests_containers.sh. This is what lets CI cover Python 2.7 and
6+
# 3.4-3.7 too -- actions/setup-python cannot provide those on a current
7+
# runner. javaobj.v3 requires Python 3.12+ and is skipped below that by
8+
# run_tests.sh.
9+
10+
name: CI Build
11+
12+
on:
13+
push:
14+
branches: ["main", "master"]
15+
tags: "**"
16+
pull_request:
17+
branches: ["main", "master"]
18+
19+
jobs:
20+
lint:
21+
timeout-minutes: 10
22+
runs-on: ubuntu-latest
23+
24+
steps:
25+
- uses: actions/checkout@v7
26+
- name: Set up Python
27+
uses: actions/setup-python@v7
28+
with:
29+
python-version: "3.14"
30+
- name: Install dependencies
31+
run: |
32+
python -m pip install --upgrade pip
33+
python -m pip install flake8
34+
- name: Lint with flake8
35+
run: |
36+
# stop the build if there are Python syntax errors or undefined names
37+
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
38+
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
39+
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
40+
41+
test:
42+
needs: lint
43+
timeout-minutes: 20
44+
runs-on: ubuntu-latest
45+
strategy:
46+
fail-fast: false
47+
matrix:
48+
# Every version declared as supported (pyproject.toml classifiers).
49+
# Each runs in its matching python:<version> container, which is the
50+
# only way to cover 2.7 and 3.4-3.7 on a modern runner.
51+
python-version:
52+
[
53+
"2.7",
54+
"3.4",
55+
"3.5",
56+
"3.6",
57+
"3.7",
58+
"3.8",
59+
"3.9",
60+
"3.10",
61+
"3.11",
62+
"3.12",
63+
"3.13",
64+
"3.14",
65+
]
66+
67+
steps:
68+
- uses: actions/checkout@v7
69+
- name: Set up Java
70+
uses: actions/setup-java@v5
71+
with:
72+
distribution: temurin
73+
java-version: "17"
74+
- name: Build the Java test fixtures
75+
# tests/java/pom.xml targets -source/-target 7, which javac has
76+
# refused since JDK 20: Temurin 17 keeps this working. The generated
77+
# .ser fixtures are then streamed into every container below, so none
78+
# of them need a JDK themselves.
79+
working-directory: tests/java
80+
run: mvn -B test
81+
- name: Run the suite in a Python ${{ matrix.python-version }} container
82+
# run_tests_containers.sh streams a clean copy of the tree into the
83+
# container and runs run_tests.sh there. The engine is pinned to
84+
# Docker (the runner ships both Docker and Podman, and the script
85+
# prefers Podman otherwise). JAVAOBJ_NO_MAVEN skips the redundant
86+
# in-container Maven call, since the fixtures were just built above.
87+
# COVERAGE_OUTPUT_DIR makes it copy the coverage data of this version
88+
# out of the container, so the coverage job below can combine them.
89+
env:
90+
CONTAINER_ENGINE: docker
91+
COVERAGE_OUTPUT_DIR: coverage-data
92+
ENGINE_RUN_ARGS: "-e JAVAOBJ_NO_MAVEN=1"
93+
run: ./run_tests_containers.sh ${{ matrix.python-version }}
94+
- name: Keep the coverage data of Python ${{ matrix.python-version }}
95+
uses: actions/upload-artifact@v7
96+
with:
97+
name: coverage-data-${{ matrix.python-version }}
98+
path: coverage-data/*.dat
99+
if-no-files-found: error
100+
retention-days: 1
101+
102+
coverage:
103+
# Every version of the matrix contributes: the report describes the whole
104+
# supported range, not just the one version a runner can install natively.
105+
needs: test
106+
timeout-minutes: 10
107+
runs-on: ubuntu-latest
108+
109+
steps:
110+
- uses: actions/checkout@v7
111+
- name: Set up Python
112+
uses: actions/setup-python@v7
113+
with:
114+
python-version: "3.14"
115+
- name: Get the coverage data of every version
116+
uses: actions/download-artifact@v8
117+
with:
118+
pattern: coverage-data-*
119+
merge-multiple: true
120+
path: coverage-data
121+
- name: Combine the coverage data
122+
# The data files come from containers, where the tree lives in /work:
123+
# .coveragerc maps that path back to this checkout.
124+
run: |
125+
python -m pip install --upgrade pip
126+
python -m pip install coverage
127+
coverage combine coverage-data/*.dat
128+
coverage report
129+
- name: Coveralls
130+
env:
131+
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
132+
run: |
133+
pip install coveralls
134+
coveralls

‎.gitignore‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,6 @@ nosetests.xml
5050
# uv
5151
.venv
5252
uv.lock
53+
54+
# Local container test runs (see run_tests_containers.sh)
55+
work/

‎README.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
[![GitHub repository](https://img.shields.io/badge/GitHub-Repository-black?logo=github)](https://github.com/tcalmant/python-javaobj/)
44
[![Latest Version](https://img.shields.io/pypi/v/javaobj-py3.svg)](https://pypi.python.org/pypi/javaobj-py3/)
55
[![License](https://img.shields.io/pypi/l/javaobj-py3.svg)](https://pypi.python.org/pypi/javaobj-py3/)
6-
[![CI Build](https://github.com/tcalmant/python-javaobj/actions/workflows/build-24.04.yml/badge.svg?branch=v3)](https://github.com/tcalmant/python-javaobj/actions/workflows/build-24.04.yml)
6+
[![CI Build](https://github.com/tcalmant/python-javaobj/actions/workflows/ci-build.yml/badge.svg?branch=master)](https://github.com/tcalmant/python-javaobj/actions/workflows/ci-build.yml)
77
[![Coveralls status](https://coveralls.io/repos/tcalmant/python-javaobj/badge.svg?branch=master)](https://coveralls.io/r/tcalmant/python-javaobj?branch=master)
88

99
*python-javaobj* is a python library that provides functions for reading and
@@ -122,7 +122,9 @@ You can find a sample usage in the *Custom Transformer* section in this file.
122122
* Python >= 3.12 for `v3`
123123
* `enum34` and `typing` when using Python <= 3.4 (installable with `pip`)
124124
* Maven 2+ (for building test data of serialized objects.
125-
You can skip it if you do not plan to run `tests.py`)
125+
The fixtures are already committed under `tests/`, so you only need Maven
126+
to regenerate them; skip it otherwise, or set `JAVAOBJ_NO_MAVEN=1` to
127+
disable the regeneration when running the test suite)
126128

127129
## Usage (V1 implementation)
128130

‎javaobj/modifiedutf8.py‎

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@
1515
:status: Alpha
1616
"""
1717

18-
from __future__ import unicode_literals
19-
2018
import sys
2119

2220
# Module version
@@ -26,6 +24,11 @@
2624
# Documentation strings format
2725
__docformat__ = "restructuredtext en"
2826

27+
# Note: this module deliberately does NOT `from __future__ import
28+
# unicode_literals`. UnicodeDecodeError requires its encoding-name,
29+
# object and reason arguments to be the *native* str type; on Python 2
30+
# that means plain bytes-string literals, not unicode ones.
31+
2932
# Encoding name: not cesu-8, which uses a different zero-byte
3033
NAME = "mutf8"
3134

@@ -170,14 +173,19 @@ def decoder(data):
170173

171174
def next_byte(_it, start, count):
172175
try:
173-
return next(_it)[1]
176+
return byte_to_int(next(_it)[1])
174177
except StopIteration:
175178
raise UnicodeDecodeError(
176179
NAME, data, start, start + count, "incomplete byte sequence"
177180
)
178181

179182
it = iter(enumerate(data))
180-
for i, d in it:
183+
for i, raw_d in it:
184+
# Iterating a Python 3 bytes object yields ints, but iterating a
185+
# Python 2 str yields single-character strings: normalize here so
186+
# the bitwise logic below works the same on both, while `data`
187+
# itself stays a real bytes-like object for UnicodeDecodeError.
188+
d = byte_to_int(raw_d)
181189
if d == 0x00: # 00000000
182190
raise UnicodeDecodeError(
183191
NAME, data, i, i + 1, "embedded zero-byte not allowed"
@@ -196,6 +204,11 @@ def next_byte(_it, start, count):
196204
for i1, dm in enumerate(DECODE_MAP[6]):
197205
d1 = next_byte(it, i, i1 + 1)
198206
value = dm.apply(d1, value, data, i, i1 + 1)
207+
# The 6 bytes reconstruct the supplementary
208+
# character's 20-bit offset from the surrogate
209+
# pair; add back the base to get the real
210+
# code point (U+10000..U+10FFFF).
211+
value += 0x10000
199212
else: # 1110xxxx
200213
value = d & 0x0F
201214
for i1, dm in enumerate(DECODE_MAP[3]):
@@ -228,7 +241,11 @@ def decode_modified_utf8(data, errors="strict"):
228241
:raises UnicodeDecodeError: sequence is invalid.
229242
"""
230243
value, length = "", 0
231-
it = iter(decoder(byte_to_int(d) for d in data))
244+
# decoder() normalizes each item internally (via byte_to_int) as it
245+
# iterates, so the original bytes-like `data` can be passed directly;
246+
# it also needs to stay a real bytes-like object here, since
247+
# UnicodeDecodeError requires one for the errors decoder() raises.
248+
it = iter(decoder(data))
232249
while True:
233250
try:
234251
value += next(it)
@@ -242,7 +259,10 @@ def decode_modified_utf8(data, errors="strict"):
242259
if errors == "ignore":
243260
pass
244261
elif errors == "replace":
245-
value += "\uFFFD"
262+
# Explicit u-prefix: without `unicode_literals` active in
263+
# this module, a plain literal would not interpret \u as
264+
# an escape sequence on Python 2.
265+
value += u"\uFFFD"
246266
length += 1
247267
return value, length
248268

‎javaobj/v1/transformers.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def __init__(self, unmarshaller):
6161
JavaObject.__init__(self)
6262

6363
def __hash__(self):
64-
return list.__hash__(self)
64+
return object.__hash__(self)
6565

6666
def __extra_loading__(self, unmarshaller, ident=0):
6767
# type: (JavaObjectUnmarshaller, int) -> None
@@ -115,7 +115,7 @@ def __init__(self, unmarshaller):
115115
JavaObject.__init__(self)
116116

117117
def __hash__(self):
118-
return dict.__hash__(self)
118+
return object.__hash__(self)
119119

120120
def __extra_loading__(self, unmarshaller, ident=0):
121121
# type: (JavaObjectUnmarshaller, int) -> None
@@ -171,7 +171,7 @@ def __init__(self, unmarshaller):
171171
JavaObject.__init__(self)
172172

173173
def __hash__(self):
174-
return set.__hash__(self)
174+
return object.__hash__(self)
175175

176176
def __extra_loading__(self, unmarshaller, ident=0):
177177
# type: (JavaObjectUnmarshaller, int) -> None

‎run_tests.sh‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#!/bin/bash
2+
#
3+
# Runs the python-javaobj test suite for the current Python interpreter.
4+
#
5+
# Used both inside the per-version containers driven by
6+
# run_tests_containers.sh, and directly in CI on the runner. javaobj.v3
7+
# requires Python 3.12+, so it (and its tests) are skipped below that version,
8+
# matching the compatibility table in README.md.
9+
#
10+
11+
set -uo pipefail
12+
13+
echo "Installing dependencies..."
14+
python -m pip install --upgrade pip
15+
pip install pytest coverage || exit 1
16+
if [ -f requirements.txt ]; then
17+
pip install -r requirements.txt || exit 1
18+
fi
19+
20+
python_supports_v3() {
21+
python -c 'import sys; sys.exit(0 if sys.version_info >= (3, 12) else 1)'
22+
}
23+
24+
if python_supports_v3
25+
then
26+
echo "Python 3.12+: running the full suite (v1, v2, v3)..."
27+
coverage run -m pytest
28+
rc=$?
29+
else
30+
echo "Python < 3.12: javaobj.v3 is unsupported, skipping it and its tests..."
31+
coverage run --omit='javaobj/v3/*,tests/test_v3.py' -m pytest --ignore=tests/test_v3.py
32+
rc=$?
33+
fi
34+
35+
coverage report
36+
37+
exit "$rc"

0 commit comments

Comments
 (0)