Skip to content

Commit 0d5a38c

Browse files
authored
Merge pull request #156 from tomato42/cosmic-ray
Add mutation testing to CI
2 parents a4a8acd + 02c8350 commit 0d5a38c

14 files changed

+344
-46
lines changed

.github/workflows/ci.yml

+51-14
Original file line numberDiff line numberDiff line change
@@ -215,10 +215,10 @@ jobs:
215215
if: ${{ contains(matrix.tox-env, 'gmpyp') }}
216216
run: pip install gmpy
217217
- name: Install gmpy2 dependencies
218-
if: ${{ contains(matrix.tox-env, 'gmpy2') || contains(matrix.tox-env, 'instrumental') }}
218+
if: ${{ contains(matrix.tox-env, 'gmpy2') || contains(matrix.tox-env, 'instrumental') || matrix.mutation == 'true' }}
219219
run: sudo apt-get install -y libmpfr-dev libmpc-dev
220220
- name: Install gmpy2
221-
if: ${{ contains(matrix.tox-env, 'gmpy2') || contains(matrix.tox-env, 'instrumental') }}
221+
if: ${{ contains(matrix.tox-env, 'gmpy2') || contains(matrix.tox-env, 'instrumental') || matrix.mutation == 'true' }}
222222
run: pip install gmpy2
223223
- name: Install build dependencies (2.6)
224224
if: ${{ matrix.python-version == '2.6' }}
@@ -268,7 +268,8 @@ jobs:
268268
- name: Install mutation testing dependencies
269269
if: ${{ matrix.mutation == 'true' }}
270270
run: |
271-
pip install cosmic-ray
271+
pip install https://github.com/sixty-north/cosmic-ray/archive/master.zip
272+
pip install pytest-timeout
272273
- name: Display installed python package versions
273274
run: pip list
274275
- name: Test native speed
@@ -296,19 +297,40 @@ jobs:
296297
cosmic-ray init cosmic-ray.toml session-vs-master.sqlite
297298
git branch master origin/master
298299
cr-filter-git --config cosmic-ray.toml session-vs-master.sqlite
299-
cr-report session-vs-master.sqlite | tail -n 5
300+
cr-report session-vs-master.sqlite | tail -n 3
300301
- name: Exec mutation testing for PR
301302
if: ${{ matrix.mutation == 'true' && github.event.pull_request }}
302303
run: |
303-
cosmic-ray exec cosmic-ray.toml session-vs-master.sqlite
304+
systemd-run --user --scope -p MemoryMax=2G -p MemoryHigh=2G cosmic-ray --verbosity INFO exec cosmic-ray.toml session-vs-master.sqlite &
305+
cosmic_pid=$!
306+
for i in $(seq 1 600); do
307+
# wait for test execution at most 10 minutes
308+
kill -s 0 $cosmic_pid || break
309+
sleep 1
310+
done
311+
kill $cosmic_pid || true
312+
wait $cosmic_pid || true
304313
- name: Check test coverage for PR
305314
if: ${{ matrix.mutation == 'true' && github.event.pull_request }}
306315
run: |
307316
# remove not-executed results
308317
sqlite3 session-vs-master.sqlite "DELETE from work_results WHERE work_results.worker_outcome = 'SKIPPED'"
309-
cr-report session-vs-master.sqlite | tail -n 5
310-
# check if executed have at most 5% survival rate
311-
cr-rate --fail-over 5 session-vs-master.sqlite
318+
cr-report session-vs-master.sqlite | tail -n 3
319+
- name: Generate html report
320+
if: ${{ matrix.mutation == 'true' && github.event.pull_request }}
321+
run: |
322+
cr-html session-vs-master.sqlite > cosmic-ray.html
323+
- name: Archive mutation testing results
324+
if: ${{ matrix.mutation == 'true' && github.event.pull_request }}
325+
uses: actions/upload-artifact@v3
326+
with:
327+
name: mutation-PR-coverage-report
328+
path: cosmic-ray.html
329+
- name: Check test coverage for PR
330+
if: ${{ matrix.mutation == 'true' && github.event.pull_request }}
331+
run: |
332+
# check if executed have at most 50% survival rate
333+
cr-rate --estimate --confidence 99.9 --fail-over 50 session-vs-master.sqlite
312334
- name: instrumental test coverage on PR
313335
if: ${{ contains(matrix.opt-deps, 'instrumental') && github.event.pull_request }}
314336
env:
@@ -372,6 +394,9 @@ jobs:
372394
373395
mutation-prepare:
374396
name: Prepare job files for the mutation runners
397+
# use runner minutes on mutation testing only after the PR passed basic
398+
# testing
399+
needs: coveralls
375400
runs-on: ubuntu-latest
376401
steps:
377402
- uses: actions/checkout@v2
@@ -386,7 +411,8 @@ jobs:
386411
key: sessions-${{ github.sha }}
387412
- name: Install cosmic-ray
388413
run: |
389-
pip3 install cosmic-ray
414+
pip3 install https://github.com/sixty-north/cosmic-ray/archive/master.zip
415+
pip install pytest-timeout
390416
- name: Install dependencies
391417
run: |
392418
sudo apt-get install -y sqlite3
@@ -461,7 +487,8 @@ jobs:
461487
- name: Install build dependencies
462488
run: |
463489
pip install -r build-requirements.txt
464-
pip install cosmic-ray
490+
pip install https://github.com/sixty-north/cosmic-ray/archive/master.zip
491+
pip install pytest-timeout
465492
- name: Run mutation testing
466493
run: |
467494
cp sessions/session-${{ matrix.name }}.sqlite session.sqlite
@@ -608,7 +635,8 @@ jobs:
608635
key: sessions-${{ github.sha }}-19-done
609636
- name: Install cosmic-ray
610637
run: |
611-
pip3 install cosmic-ray
638+
pip3 install https://github.com/sixty-north/cosmic-ray/archive/master.zip
639+
pip install pytest-timeout
612640
- name: Install dependencies
613641
run: |
614642
sudo apt-get install -y sqlite3
@@ -621,13 +649,20 @@ jobs:
621649
- name: Report executed
622650
run: |
623651
cr-report session.sqlite | tail -n 3
624-
- name: Log survival estimate
625-
run: cr-rate --estimate --fail-over 32 --confidence 99.9 session.sqlite || true
652+
- name: Generate html report
653+
run: |
654+
cr-html session.sqlite > cosmic-ray.html
655+
- name: Archive mutation testing results
656+
uses: actions/upload-artifact@v3
657+
with:
658+
name: mutation-coverage-report
659+
path: cosmic-ray.html
626660
- name: Get mutation score
627661
run: |
628-
echo "print(100-$(cr-rate session.sqlite))" > print-score.py
662+
echo "print('{0:.2f}'.format(100-$(cr-rate session.sqlite)))" > print-score.py
629663
echo "MUT_SCORE=$(python print-score.py)" >> $GITHUB_ENV
630664
- name: Create mutation score badge
665+
if: ${{ !github.event.pull_request }}
631666
uses: schneegans/[email protected]
632667
with:
633668
auth: ${{ secrets.GIST_SECRET }}
@@ -638,3 +673,5 @@ jobs:
638673
valColorRange: ${{ env.MUT_SCORE }}
639674
maxColorRange: 100
640675
minColorRange: 0
676+
- name: Check survival estimate
677+
run: cr-rate --estimate --fail-over 32 --confidence 99.9 session.sqlite

cosmic-ray-12way.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
module-path = "src"
33
timeout = 20.0
44
excluded-modules = ['src/ecdsa/_sha3.py', 'src/ecdsa/_version.py', 'src/ecdsa/test*']
5-
test-command = "pytest -x --fast -m 'not slow' src/"
5+
test-command = "pytest --timeout=30 -x --fast -m 'not slow' src/"
66

77
[cosmic-ray.distributor]
88
name = "http"

cosmic-ray.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
module-path = "src"
33
timeout = 20.0
44
excluded-modules = ['src/ecdsa/_sha3.py', 'src/ecdsa/_version.py', 'src/ecdsa/test*']
5-
test-command = "pytest -x --fast -m 'not slow' src/"
5+
test-command = "pytest --timeout 30 -x --fast -m 'not slow' src/"
66

77
[cosmic-ray.distributor]
88
name = "local"

src/ecdsa/ellipticcurve.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -1532,7 +1532,9 @@ def double(self):
15321532

15331533
X3, Y3, Z3, T3 = self._double(X1, Y1, Z1, T1, p, a)
15341534

1535-
if not X3 or not T3:
1535+
# both Ed25519 and Ed448 have prime order, so no point added to
1536+
# itself will equal zero
1537+
if not X3 or not T3: # pragma: no branch
15361538
return INFINITY
15371539
return PointEdwards(self.__curve, X3, Y3, Z3, T3, self.__order)
15381540

src/ecdsa/keys.py

-6
Original file line numberDiff line numberDiff line change
@@ -1102,12 +1102,6 @@ def from_der(cls, string, hashfunc=sha1, valid_curve_encodings=None):
11021102

11031103
curve = Curve.from_der(algorithm_identifier, valid_curve_encodings)
11041104

1105-
if empty != b"":
1106-
raise der.UnexpectedDER(
1107-
"unexpected data after algorithm identifier: %s"
1108-
% binascii.hexlify(empty)
1109-
)
1110-
11111105
# Up next is an octet string containing an ECPrivateKey. Ignore
11121106
# the optional "attributes" and "publicKey" fields that come after.
11131107
s, _ = der.remove_octet_string(s)

src/ecdsa/numbertheory.py

+20-10
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@
2020
except NameError:
2121
xrange = range
2222
try:
23-
from gmpy2 import powmod
23+
from gmpy2 import powmod, mpz
2424

2525
GMPY2 = True
2626
GMPY = False
27-
except ImportError:
27+
except ImportError: # pragma: no branch
2828
GMPY2 = False
2929
try:
3030
from gmpy import mpz
@@ -33,8 +33,15 @@
3333
except ImportError:
3434
GMPY = False
3535

36+
37+
if GMPY2 or GMPY: # pragma: no branch
38+
integer_types = tuple(integer_types + (type(mpz(1)),))
39+
40+
3641
import math
3742
import warnings
43+
import random
44+
from .util import bit_length
3845

3946

4047
class Error(Exception):
@@ -216,14 +223,15 @@ def square_root_mod_prime(a, p):
216223
range_top = min(0x7FFFFFFF, p)
217224
else:
218225
range_top = p
219-
for b in xrange(2, range_top):
226+
for b in xrange(2, range_top): # pragma: no branch
220227
if jacobi(b * b - 4 * a, p) == -1:
221228
f = (a, -b, 1)
222229
ff = polynomial_exp_mod((0, 1), (p + 1) // 2, f, p)
223230
if ff[1]:
224231
raise SquareRootError("p is not prime")
225232
return ff[0]
226-
raise RuntimeError("No b found.")
233+
# just an assertion
234+
raise RuntimeError("No b found.") # pragma: no cover
227235

228236

229237
# because all the inverse_mod code is arch/environment specific, and coveralls
@@ -346,7 +354,7 @@ def factorization(n):
346354
q, r = divmod(n, d)
347355
if r == 0:
348356
count = 1
349-
while d <= n:
357+
while d <= n: # pragma: no branch
350358
n = q
351359
q, r = divmod(n, d)
352360
if r != 0:
@@ -370,7 +378,8 @@ def factorization(n):
370378
if r == 0: # d divides n. How many times?
371379
count = 1
372380
n = q
373-
while d <= n: # As long as d might still divide n,
381+
# As long as d might still divide n,
382+
while d <= n: # pragma: no branch
374383
q, r = divmod(n, d) # see if it does.
375384
if r != 0:
376385
break
@@ -555,16 +564,17 @@ def is_prime(n):
555564
return True
556565
else:
557566
return False
558-
559-
if gcd(n, 2 * 3 * 5 * 7 * 11) != 1:
567+
# 2310 = 2 * 3 * 5 * 7 * 11
568+
if gcd(n, 2310) != 1:
560569
return False
561570

562571
# Choose a number of iterations sufficient to reduce the
563572
# probability of accepting a composite below 2**-80
564573
# (from Menezes et al. Table 4.4):
565574

566575
t = 40
567-
n_bits = 1 + int(math.log(n, 2))
576+
n_bits = 1 + bit_length(n)
577+
assert 11 <= n_bits <= 16384
568578
for k, tt in (
569579
(100, 27),
570580
(150, 18),
@@ -591,7 +601,7 @@ def is_prime(n):
591601
s = s + 1
592602
r = r // 2
593603
for i in xrange(t):
594-
a = smallprimes[i]
604+
a = random.choice(smallprimes)
595605
y = pow(a, r, n)
596606
if y != 1 and y != n - 1:
597607
j = 1

src/ecdsa/test_ecdh.py

+7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
import sys
23
import shutil
34
import subprocess
45
import pytest
@@ -16,6 +17,8 @@
1617
NIST384p,
1718
NIST521p,
1819
BRAINPOOLP160r1,
20+
SECP112r2,
21+
SECP128r1,
1922
)
2023
from .curves import curves
2124
from .ecdh import (
@@ -29,6 +32,10 @@
2932
from .ellipticcurve import CurveEdTw
3033

3134

35+
if "--fast" in sys.argv: # pragma: no cover
36+
curves = [SECP112r2, SECP128r1]
37+
38+
3239
@pytest.mark.parametrize(
3340
"vcurve",
3441
curves,

src/ecdsa/test_eddsa.py

+22-1
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,27 @@ def test_ed25519_eq_x_different_y():
163163
assert a != b
164164

165165

166+
def test_ed25519_mul_by_order():
167+
g = PointEdwards(
168+
curve_ed25519,
169+
generator_ed25519.x(),
170+
generator_ed25519.y(),
171+
1,
172+
generator_ed25519.x() * generator_ed25519.y(),
173+
)
174+
175+
assert g * generator_ed25519.order() == INFINITY
176+
177+
178+
def test_radd():
179+
180+
a = PointEdwards(curve_ed25519, 1, 1, 1, 1)
181+
182+
p = INFINITY + a
183+
184+
assert p == a
185+
186+
166187
def test_ed25519_test_normalisation_and_scaling():
167188
x = generator_ed25519.x()
168189
y = generator_ed25519.y()
@@ -664,7 +685,7 @@ def test_invalid_r_value(self):
664685

665686

666687
HYP_SETTINGS = dict()
667-
if "--fast" in sys.argv:
688+
if "--fast" in sys.argv: # pragma: no cover
668689
HYP_SETTINGS["max_examples"] = 2
669690
else:
670691
HYP_SETTINGS["max_examples"] = 10

src/ecdsa/test_ellipticcurve.py

+31
Original file line numberDiff line numberDiff line change
@@ -223,3 +223,34 @@ def test_inequality_points(self):
223223
def test_inequality_points_diff_types(self):
224224
c = CurveFp(100, -3, 100)
225225
self.assertNotEqual(self.g_23, c)
226+
227+
def test_to_bytes_from_bytes(self):
228+
p = Point(self.c_23, 3, 10)
229+
230+
self.assertEqual(p, Point.from_bytes(self.c_23, p.to_bytes()))
231+
232+
def test_add_to_neg_self(self):
233+
p = Point(self.c_23, 3, 10)
234+
235+
self.assertEqual(INFINITY, p + (-p))
236+
237+
def test_add_to_infinity(self):
238+
p = Point(self.c_23, 3, 10)
239+
240+
self.assertIs(p, p + INFINITY)
241+
242+
def test_mul_infinity_by_scalar(self):
243+
self.assertIs(INFINITY, INFINITY * 10)
244+
245+
def test_mul_by_negative(self):
246+
p = Point(self.c_23, 3, 10)
247+
248+
self.assertEqual(p * -5, (-p) * 5)
249+
250+
def test_str_infinity(self):
251+
self.assertEqual(str(INFINITY), "infinity")
252+
253+
def test_str_point(self):
254+
p = Point(self.c_23, 3, 10)
255+
256+
self.assertEqual(str(p), "(3,10)")

src/ecdsa/test_jacobi.py

+6
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,12 @@ def test_equality_with_wrong_curves(self):
635635

636636
self.assertNotEqual(p_a, p_b)
637637

638+
def test_add_with_point_at_infinity(self):
639+
pj1 = PointJacobi(curve=CurveFp(23, 1, 1, 1), x=2, y=3, z=1, order=1)
640+
x, y, z = pj1._add(2, 3, 1, 5, 5, 0, 23)
641+
642+
self.assertEqual((x, y, z), (2, 3, 1))
643+
638644
def test_pickle(self):
639645
pj = PointJacobi(curve=CurveFp(23, 1, 1, 1), x=2, y=3, z=1, order=1)
640646
self.assertEqual(pickle.loads(pickle.dumps(pj)), pj)

0 commit comments

Comments
 (0)