Expand compatibility stress coverage · bikini/patchwork@a5216f5 · GitHub
Skip to content

Commit a5216f5

Browse files
Expand compatibility stress coverage
1 parent b42f539 commit a5216f5

8 files changed

Lines changed: 642 additions & 305 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions

README.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,14 +222,32 @@ python examples/hello_obf.py
222222

223223
`examples/stress.py` is the torture test - decorators, generators, classes with `super()`, `match` statements, walrus, exceptions, `*args`/`**kwargs`, globals. If something doesn't work, it'll usually break here first.
224224

225+
`examples/modern.py` covers newer Python constructs: `dataclass`, structural
226+
pattern matching with capture binders, mapping/list/star patterns, async
227+
functions, context managers, properties, f-strings with format specs, bytes,
228+
closures, and `nonlocal`.
229+
230+
`examples/future_annotations.py` covers `from __future__ import annotations`
231+
and verifies that observable annotation strings are preserved.
232+
225233
## Tests
226234

227235
```sh
228236
python tests/test_obfuscator.py
229237
python tests/test_audit.py
238+
python tests/test_stress_matrix.py
230239
```
231240

232-
Runs every example through the obfuscator at multiple seeds, executes original and obfuscated versions, and checks stdout matches byte-for-byte. Also confirms different seeds produce different output and the same seed produces stable output.
241+
Runs every example through the obfuscator at multiple seeds, executes original
242+
and obfuscated versions, and checks stdout matches byte-for-byte. Also confirms
243+
different seeds produce different output and the same seed produces stable
244+
output.
245+
246+
The stress matrix generates deterministic Python programs with arithmetic,
247+
bitwise operations, pattern matching, closures, comprehensions, strings, bytes,
248+
and future annotations. It then runs them through multiple obfuscation seeds and
249+
option combinations, including disabled lazy loading, disabled renaming,
250+
disabled string encryption, and disabled opaque/junk transforms.
233251

234252
## Layout
235253

examples/future_annotations.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from __future__ import annotations
2+
3+
4+
class Node:
5+
def __init__(self, value: int, child: Node | None = None):
6+
self.value = value
7+
self.child = child
8+
9+
10+
def flatten(node: Node | None) -> list[int]:
11+
values: list[int] = []
12+
while node is not None:
13+
values.append(node.value)
14+
node = node.child
15+
return values
16+
17+
18+
def run() -> None:
19+
chain = Node(1, Node(2, Node(3)))
20+
print('annotations:', flatten(chain))
21+
print('future:', flatten.__annotations__)
22+
23+
24+
if __name__ == '__main__':
25+
run()

examples/modern.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
from __future__ import annotations
2+
3+
import asyncio
4+
from contextlib import contextmanager
5+
from dataclasses import dataclass
6+
7+
8+
@dataclass
9+
class Point:
10+
x: int
11+
y: int
12+
13+
14+
@contextmanager
15+
def tagged(name: str):
16+
yield f'<{name}>'
17+
18+
19+
def describe(value):
20+
match value:
21+
case Point(0, y):
22+
return f'y-axis:{y}'
23+
case Point(x=x, y=y) if x == y:
24+
return f'diagonal:{x}'
25+
case {'kind': 'pair', 'items': [first, *rest], **extra}:
26+
return f'pair:{first}:{len(rest)}:{sorted(extra)}'
27+
case ('wrap', inner as captured):
28+
return f'wrapped:{inner}:{captured}'
29+
case [head, *tail]:
30+
return f'list:{head}:{sum(tail)}'
31+
case _:
32+
return 'unknown'
33+
34+
35+
async def async_total(values):
36+
total = 0
37+
for value in values:
38+
await asyncio.sleep(0)
39+
total += value
40+
return total
41+
42+
43+
def make_counter(start=0):
44+
count = start
45+
46+
def next_value(step=1):
47+
nonlocal count
48+
count += step
49+
return count
50+
51+
return next_value
52+
53+
54+
class Box:
55+
def __init__(self, value):
56+
self.value = value
57+
58+
@property
59+
def doubled(self):
60+
return self.value * 2
61+
62+
63+
def run() -> None:
64+
items = [
65+
Point(0, 7),
66+
Point(4, 4),
67+
{'kind': 'pair', 'items': [3, 5, 8], 'tag': 'fib'},
68+
('wrap', 'token'),
69+
[1, 2, 3, 4],
70+
object(),
71+
]
72+
print('descriptions:', [describe(item) for item in items])
73+
print('async:', asyncio.run(async_total([1, 2, 3, 4])))
74+
counter = make_counter(10)
75+
print('counter:', [counter(), counter(5), counter()])
76+
with tagged('ctx') as label:
77+
print('context:', label)
78+
print('box:', Box(9).doubled)
79+
print('bytes:', bytes([65, 66, 67]).decode())
80+
print('format:', f'{255:#06x}:{3.14159:.2f}')
81+
82+
83+
if __name__ == '__main__':
84+
run()

patchwork/core.py

Lines changed: 98 additions & 79 deletions

0 commit comments

Comments
 (0)